View difference between Paste ID: WH1APpZ8 and qSiLE2Ae
SHOW: | | - or go back to the newest paste.
1
w = {}
2
3
-- APIs
4
5
-- properties
6
local data = { }
7
local data_name = nil
8
local data_handlers = { }
9
10
local device_handlers = {}
11
12
local event_handlers = {}
13
local event_timers = {}
14
15
local monitors = {}
16
local monitor_textScale = 0.5
17
18
local page_handlers = {}
19
local page_endText = ""
20
local page_callbackDisplay
21
local page_callbackKey
22
23
local run_refreshPeriod_s = 3.0
24
local status_period_s = 1.0
25
26
local styles = {
27
  normal   = { front = colors.black    , back = colors.lightGray },
28
  good     = { front = colors.lime     , back = colors.lightGray },
29
  bad      = { front = colors.red      , back = colors.lightGray },
30
  disabled = { front = colors.gray     , back = colors.lightGray },
31
  help     = { front = colors.white    , back = colors.blue      },
32
  header   = { front = colors.orange   , back = colors.black     },
33
  control  = { front = colors.white    , back = colors.blue      },
34
  selected = { front = colors.black    , back = colors.lightBlue },
35
  warning  = { front = colors.white    , back = colors.red       },
36
  success  = { front = colors.white    , back = colors.lime      },
37
}
38
39
----------- Terminal & monitor support
40
41
local function setMonitorColorFrontBack(colorFront, colorBackground)
42
  term.setTextColor(colorFront)
43
  term.setBackgroundColor(colorBackground)
44
  if monitors ~= nil then
45
    for key, monitor in pairs(monitors) do
46
      monitor.setTextColor(colorFront)
47
      monitor.setBackgroundColor(colorBackground)
48
    end
49
  end
50
end
51
52
local function write(text)
53
  term.write(text)
54
  if monitors ~= nil then
55
    for key, monitor in pairs(monitors) do
56
      if key ~= data.radar_monitorIndex then
57
        monitor.write(text)
58
      end
59
    end
60
  end
61
end
62
63
local function getCursorPos()
64
  local x, y = term.getCursorPos()
65
  return x, y
66
end
67
68
local function setCursorPos(x, y)
69
  term.setCursorPos(x, y)
70
  if monitors ~= nil then
71
    for key, monitor in pairs(monitors) do
72
      if key ~= data.radar_monitorIndex then
73
        monitor.setCursorPos(x, y)
74
      end
75
    end
76
  end
77
end
78
79
local function getResolution()
80
  local sizeX, sizeY = term.getSize()
81
  return sizeX, sizeY
82
end
83
84
local function setColorNormal()
85
  w.setMonitorColorFrontBack(styles.normal.front, styles.normal.back)
86
end
87
88
local function setColorGood()
89
  w.setMonitorColorFrontBack(styles.good.front, styles.good.back)
90
end
91
92
local function setColorBad()
93
  w.setMonitorColorFrontBack(styles.bad.front, styles.bad.back)
94
end
95
96
local function setColorDisabled()
97
  w.setMonitorColorFrontBack(styles.disabled.front, styles.disabled.back)
98
end
99
100
local function setColorHelp()
101
  w.setMonitorColorFrontBack(styles.help.front, styles.help.back)
102
end
103
104
local function setColorHeader()
105
  w.setMonitorColorFrontBack(styles.header.front, styles.header.back)
106
end
107
108
local function setColorControl()
109
  w.setMonitorColorFrontBack(styles.control.front, styles.control.back)
110
end
111
112
local function setColorSelected()
113
  w.setMonitorColorFrontBack(styles.selected.front, styles.selected.back)
114
end
115
116
local function setColorWarning()
117
  w.setMonitorColorFrontBack(styles.warning.front, styles.warning.back)
118
end
119
120
local function setColorSuccess()
121
  w.setMonitorColorFrontBack(styles.success.front, styles.success.back)
122
end
123
124
local function clear(colorFront, colorBack)
125
  if colorFront == nil or colorBack == nil then
126
    w.setColorNormal()
127
  else
128
    w.setMonitorColorFrontBack(colorFront, colorBack)
129
  end
130
  term.clear()
131
  if monitors ~= nil then
132
    for key, monitor in pairs(monitors) do
133
      if key ~= data.radar_monitorIndex then
134
        monitor.clear()
135
      end
136
    end
137
  end
138
  w.setCursorPos(1, 1)
139
end
140
141
local function clearLine()
142
  term.clearLine()
143
  if monitors ~= nil then
144
    for key, monitor in pairs(monitors) do
145
      if key ~= data.radar_monitorIndex then
146
        monitor.clearLine()
147
      end
148
    end
149
  end
150
  local x, y = w.getCursorPos()
151
  w.setCursorPos(1, y)
152
end
153
154
local function writeLn(text)
155
  w.write(text)
156
  local x, y = w.getCursorPos()
157
  local xSize, ySize = w.getResolution()
158
  if y > ySize - 1 then
159
    y = 1
160
  end
161
  w.setCursorPos(1, y + 1)
162
end
163
164
local function writeMultiLine(text)
165
  local textToParse = text or ""
166
  for line in string.gmatch(textToParse, "[^\n]+") do
167
    if line ~= "" then
168
      w.writeLn(line)
169
    end
170
  end
171
end
172
173
local function writeCentered(y, text)
174
  local unused
175
  if text == nil then
176
    text = y
177
    unused, y = w.getCursorPos()
178
  end
179
  
180
  w.setCursorPos((51 - text:len()) / 2, y)
181
  term.write(text)
182
  if monitors ~= nil then
183
    for key, monitor in pairs(monitors) do
184
      if key ~= data.radar_monitorIndex then
185
        local xSize, ySize = monitor.getSize()
186
        if xSize ~= nil then
187
          monitor.setCursorPos((xSize - text:len()) / 2, y)
188
          monitor.write(text)
189
        end
190
      end
191
    end
192
  end
193
  w.setCursorPos(1, y + 1)
194
end
195
196
local function writeFullLine(text)
197
  w.write(text)
198
  local xSize, ySize = w.getResolution()
199
  local xCursor, yCursor = w.getCursorPos()
200
  for i = xCursor, xSize do
201
    w.write(" ")
202
  end
203
  w.setCursorPos(1, yCursor + 1)
204
end
205
206
----------- Page support
207
208
local function page_begin(text)
209
  w.clear()
210
  w.setCursorPos(1, 1)
211
  w.setColorHeader()
212
  w.clearLine()
213
  w.writeCentered(1, text)
214
  w.status_refresh()
215
  w.setCursorPos(1, 2)
216
  w.setColorNormal()
217
end
218
219
local function page_colors()
220
  w.clear(colors.white, colors.black)
221
  for key, value in pairs(colors) do
222
    local text = string.format("%12s", key)
223
    w.setMonitorColorFrontBack(colors.white, colors.black)
224
    w.write(text .. " ")
225
    w.setMonitorColorFrontBack(value, colors.black)
226
    w.write(" " .. text .. " ")
227
    w.setMonitorColorFrontBack(colors.black, value)
228
    w.write(" " .. text .. " ")
229
    w.setMonitorColorFrontBack(colors.white, value)
230
    w.write(" " .. text .. " ")
231
    w.setMonitorColorFrontBack(value, colors.white)
232
    w.write(" " .. text .. " ")
233
    w.writeLn("")
234
  end
235
  w.writeLn("")
236
  local index = 0
237
  for key, value in pairs(styles) do
238
    local text = string.format("%12s", key)
239
    if index % 2 == 0 then
240
      w.setMonitorColorFrontBack(colors.white, colors.black)
241
      w.write(text .. " ")
242
      w.setMonitorColorFrontBack(value.front, value.back)
243
      w.write(" " .. text .. " ")
244
    else
245
      w.setMonitorColorFrontBack(value.front, value.back)
246
      w.write(" " .. text .. " ")
247
      w.setMonitorColorFrontBack(colors.white, colors.black)
248
      w.write(text .. " ")
249
      w.writeLn("")
250
    end
251
    index = index + 1
252
  end
253
  w.setMonitorColorFrontBack(colors.white, colors.black)
254
end
255
256
local function page_end()
257
  w.setCursorPos(1, 18)
258
  w.setColorControl()
259
  w.writeFullLine(page_endText)
260
end
261
262
local function page_getCallbackDisplay()
263
  return page_callbackDisplay
264
end
265
266
local function page_register(index, callbackDisplay, callbackKey)
267
  page_handlers[index] = { display = callbackDisplay, key = callbackKey }
268
end
269
270
local function page_setEndText(text)
271
  page_endText = text
272
end
273
274
----------- Status line support
275
276
local status_clockTarget = -1 -- < 0 when stopped, < clock when elapsed, > clock when ticking
277
local status_isWarning = false
278
local status_line = 0
279
local status_text = ""
280
local function status_clear()
281
  if status_clockTarget > 0 then
282
    status_clockTarget = -1
283
    w.event_timer_stop("status")
284
    local xSize, ySize = w.getResolution()
285
    w.setCursorPos(1, ySize)
286
    w.setColorNormal()
287
    w.clearLine()
288
  end
289
end
290
local function status_isActive()
291
  return status_clockTarget > 0 and w.event_clock() < status_clockTarget
292
end
293
local function status_show(isWarning, text)
294
  if isWarning or not w.status_isActive() then
295
    status_line = 1
296
    status_isWarning = isWarning
297
    status_text = {}
298
    local textToParse = (text and text ~= "") and text or "???"
299
    for line in string.gmatch(textToParse, "[^\n]+") do
300
      if line ~= "" then
301
        table.insert(status_text, line)
302
      end
303
    end
304
    if isWarning then
305
      status_clockTarget = w.event_clock() + 1.0 * #status_text
306
    else
307
      status_clockTarget = w.event_clock() + 0.5 * #status_text
308
    end
309
    w.event_timer_start("status", status_period_s, "timer_status")
310
  end
311
  -- always refresh as a visual clue
312
  w.status_refresh()
313
end
314
local function status_refresh()
315
  if status_clockTarget > 0 then
316
    if w.event_clock() > status_clockTarget and status_line == 1 then
317
      w.status_clear()
318
    else
319
      local xSize, ySize = w.getResolution()
320
      w.setCursorPos(1, ySize)
321
      w.setColorNormal()
322
      w.clearLine()
323
324
      if status_isWarning then
325
        w.setColorWarning()
326
      else
327
        w.setColorSuccess()
328
      end
329
      local text = status_text[status_line]
330
      w.writeCentered(" " .. text .. " ")
331
      w.setColorNormal()
332
    end
333
  end
334
end
335
local function status_showWarning(text)
336
  w.status_show(true, text)
337
end
338
local function status_showSuccess(text)
339
  w.status_show(false, text)
340
end
341
local function status_tick()
342
  if status_clockTarget > -1 then
343
    local clockCurrent = w.event_clock()
344
    if clockCurrent > status_clockTarget then
345
      w.status_clear()
346
    else
347
      status_line = (status_line % #status_text) + 1
348
      w.status_refresh()
349
    end
350
  end
351
end
352
353
----------- Formatting
354
355
local function format_float(value, nbchar)
356
  local str = "?"
357
  if value ~= nil then
358
    if type(value) == "number" then
359
      str = string.format("%g", value)
360
    else
361
      str = type(value)
362
    end
363
  end
364
  if nbchar ~= nil then
365
    str = string.sub("               " .. str, -nbchar)
366
  end
367
  return str
368
end
369
370
local function format_integer(value, nbchar)
371
  local str = "?"
372
  if value ~= nil then
373
    if type(value) == "number" then
374
      str = string.format("%d", math.floor(value))
375
    else
376
      str = type(value)
377
    end
378
  end
379
  if nbchar ~= nil then
380
    str = string.sub("               " .. str, -nbchar)
381
  end
382
  return str
383
end
384
385
local function format_boolean(value, strTrue, strFalse)
386
  if value ~= nil then
387
    if type(value) == "boolean" then
388
      if value then
389
        return strTrue
390
      else
391
        return strFalse
392
      end
393
    else
394
      return type(value)
395
    end
396
  end
397
  return "?"
398
end
399
400
local function format_string(value, nbchar)
401
  local str = "?"
402
  if value ~= nil then
403
    str = "" .. value
404
  end
405
  if nbchar ~= nil then
406
    if #str > math.abs(nbchar) then
407
      str = string.sub(str, 1, math.abs(nbchar) - 1) .. "~"
408
    else
409
      str = string.sub(str .. "                                                  ", 1, nbchar)
410
    end
411
  end
412
  return str
413
end
414
415
local function format_address(value)
416
  local str = "?"
417
  if value ~= nil then
418
    str = "" .. value
419
  end
420
  str = string.sub(str, 10, 100)
421
  return str
422
end
423
424
local function format_charNumber(value)
425
  if  value ~= nil
426
  and type(value) == "number"
427
  and value <= 255
428
  and value >= 0 then
429
    return string.char(value)
430
  else
431
    return string.char(0)
432
  end
433
end
434
435
----------- Input controls
436
437
local function input_readInteger(currentValue)
438
  local inputAbort = false
439
  local input = w.format_integer(currentValue)
440
  if input == "0" then
441
    input = ""
442
  end
443
  local ignoreNextChar = false
444
  local x, y = w.getCursorPos()
445
  
446
  term.setCursorBlink(true)
447
  repeat
448
    w.setCursorPos(x, y)
449
    w.setColorNormal()
450
    w.write(input .. "            ")
451
    input = string.sub(input, -9)
452
    w.setCursorPos(x + #input, y)
453
    
454
    local params = { os.pullEventRaw() }
455
    local eventName = params[1]
456
    local firstParam = params[2]
457
    if firstParam == nil then firstParam = "none" end
458
    if eventName == "key" then
459
      local keycode = params[2]
460
      
461
      if keycode >= 2 and keycode <= 10 then -- 1 to 9
462
        input = input .. w.format_string(keycode - 1)
463
        ignoreNextChar = true
464
      elseif keycode == 11 or keycode == 82 then -- 0 & keypad 0
465
        input = input .. "0"
466
        ignoreNextChar = true
467
      elseif keycode >= 79 and keycode <= 81 then -- keypad 1 to 3
468
        input = input .. w.format_string(keycode - 78)
469
        ignoreNextChar = true
470
      elseif keycode >= 75 and keycode <= 77 then -- keypad 4 to 6
471
        input = input .. w.format_string(keycode - 71)
472
        ignoreNextChar = true
473
      elseif keycode >= 71 and keycode <= 73 then -- keypad 7 to 9
474
        input = input .. w.format_string(keycode - 64)
475
        ignoreNextChar = true
476
      elseif keycode == 14 then -- Backspace
477
        input = string.sub(input, 1, string.len(input) - 1)
478
        ignoreNextChar = true
479
      elseif keycode == 211 then -- Delete
480
        input = ""
481
        ignoreNextChar = true
482
      elseif keycode == 28 then -- Enter
483
        inputAbort = true
484
        ignoreNextChar = true
485
      elseif keycode == 74 or keycode == 12 or keycode == 49 then -- - on numeric keypad or - on US top or n letter
486
        if string.sub(input, 1, 1) == "-" then
487
          input = string.sub(input, 2)
488
        else
489
          input = "-" .. input
490
        end
491
        ignoreNextChar = true
492
      elseif keycode == 78 then -- +
493
        if string.sub(input, 1, 1) == "-" then
494
          input = string.sub(input, 2)
495
        end
496
        ignoreNextChar = true
497
      else
498
        ignoreNextChar = false
499
        -- w.status_showWarning("Key " .. keycode .. " is not supported here")
500
      end
501
      
502
    elseif eventName == "char" then
503
      local character = params[2]
504
      if ignoreNextChar then
505
        ignoreNextChar = false
506
        -- w.status_showWarning("Ignored char #" .. string.byte(character) .. " '" .. character .. "'")
507
      elseif character >= '0' and character <= '9' then -- 0 to 9
508
        input = input .. character
509
      elseif character == '-' or character == 'n' or character == 'N' then -- - or N
510
        if string.sub(input, 1, 1) == "-" then
511
          input = string.sub(input, 2)
512
        else
513
          input = "-" .. input
514
        end
515
      elseif character == '+' or character == 'p' or character == 'P' then -- + or P
516
        if string.sub(input, 1, 1) == "-" then
517
          input = string.sub(input, 2)
518
        end
519
      else
520
        w.status_showWarning("Key '" .. character .. "' is not supported here (" .. string.byte(character) .. ")")
521
      end
522
      
523
    elseif eventName == "terminate" then
524
      inputAbort = true
525
      
526
    else
527
      local isSupported, needRedraw = w.event_handler(eventName, firstParam)
528
      if not isSupported then
529
        w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
530
      end
531
    end
532
  until inputAbort
533
  term.setCursorBlink(false)
534
  w.setCursorPos(1, y + 1)
535
  if input == "" or input == "-" then
536
    return currentValue
537
  else
538
    return tonumber(input)
539
  end
540
end
541
542
local function input_readText(currentValue)
543
  local inputAbort = false
544
  local input = w.format_string(currentValue)
545
  local ignoreNextChar = false
546
  local x, y = w.getCursorPos()
547
  
548
  term.setCursorBlink(true)
549
  repeat
550
    -- update display clearing extra characters
551
    w.setCursorPos(x, y)
552
    w.setColorNormal()
553
    w.write(w.format_string(input, 37))
554
    -- truncate input and set caret position
555
    input = string.sub(input, -36)
556
    w.setCursorPos(x + #input, y)
557
    
558
    local params = { os.pullEventRaw() }
559
    local eventName = params[1]
560
    local firstParam = params[2]
561
    if firstParam == nil then firstParam = "none" end
562
    if eventName == "key" then
563
      local keycode = params[2]
564
      
565
      if keycode == 14 then -- Backspace
566
        input = string.sub(input, 1, string.len(input) - 1)
567
        ignoreNextChar = true
568
      elseif keycode == 211 then -- Delete
569
        input = ""
570
        ignoreNextChar = true
571
      elseif keycode == 28 then -- Enter
572
        inputAbort = true
573
        ignoreNextChar = true
574
      else
575
        ignoreNextChar = false
576
        -- w.status_showWarning("Key " .. keycode .. " is not supported here")
577
      end
578
      
579
    elseif eventName == "char" then
580
      local character = params[2]
581
      if ignoreNextChar then
582
        ignoreNextChar = false
583
        -- w.status_showWarning("Ignored char #" .. string.byte(character) .. " '" .. character .. "'")
584
      elseif character >= ' ' and character <= '~' then -- any ASCII table minus controls and DEL
585
        input = input .. character
586
      else
587
        w.status_showWarning("Key '" .. character .. "' is not supported here (" .. string.byte(character) .. ")")
588
      end
589
      
590
    elseif eventName == "terminate" then
591
      inputAbort = true
592
      
593
    else
594
      local isSupported, needRedraw = w.event_handler(eventName, firstParam)
595
      if not isSupported then
596
        w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
597
      end
598
    end
599
  until inputAbort
600
  term.setCursorBlink(false)
601
  w.setCursorPos(1, y + 1)
602
  if input == "" then
603
    return currentValue
604
  else
605
    return input
606
  end
607
end
608
609
local function input_readConfirmation(message)
610
  if message == nil then
611
    message = "Are you sure? (Y/n)"
612
  end
613
  w.status_showWarning(message)
614
  repeat
615
    local params = { os.pullEventRaw() }
616
    local eventName = params[1]
617
    local firstParam = params[2]
618
    if firstParam == nil then firstParam = "none" end
619
    if eventName == "key" then
620
      local keycode = params[2]
621
      
622
      if keycode == 28 then -- Return or Enter
623
        w.status_clear()
624
        return true
625
      end
626
      
627
    elseif eventName == "char" then
628
      local character = params[2]
629
      w.status_clear()
630
      if character == 'y' or character == 'Y' then -- Y
631
        return true
632
      else
633
        return false
634
      end
635
      
636
    elseif eventName == "terminate" then
637
      return false
638
      
639
    else
640
      local isSupported, needRedraw = w.event_handler(eventName, firstParam)
641
      if not isSupported then
642
        w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
643
      end
644
    end
645
    if not w.status_isActive() then
646
      w.status_showWarning(message)
647
    end
648
  until false
649
end
650
651
local function input_readEnum(currentValue, list, toValue, toDescription, noValue)
652
  local inputAbort = false
653
  local inputKey = nil
654
  local input = nil
655
  local inputDescription = nil
656
  local ignoreNextChar = false
657
  local x, y = w.getCursorPos()
658
  
659
  w.setCursorPos(1, 17)
660
  for key, entry in pairs(list) do
661
    if toValue(entry) == currentValue then
662
      inputKey = key
663
    end
664
  end
665
  
666
  term.setCursorBlink(true)
667
  repeat
668
    w.setCursorPos(x, y)
669
    w.setColorNormal()
670
    if #list == 0 then
671
      inputKey = nil
672
    end
673
    if inputKey == nil then
674
      if currentValue ~= nil then
675
        input = noValue
676
        inputDescription = "Press enter to return previous entry"
677
      else
678
        input = noValue
679
        inputDescription = "Press enter to close listing"
680
      end
681
    else
682
      if inputKey < 1 then
683
        inputKey = #list
684
      elseif inputKey > #list then
685
        inputKey = 1
686
      end
687
      
688
      input = toValue(list[inputKey])
689
      inputDescription = toDescription(list[inputKey])
690
    end
691
    w.setColorNormal()
692
    w.write(input .. "                                                  ")
693
    w.setCursorPos(1, y + 1)
694
    w.setColorDisabled()
695
    w.write(inputDescription .. "                                                  ")
696
    
697
    local params = { os.pullEventRaw() }
698
    local eventName = params[1]
699
    local firstParam = params[2]
700
    if firstParam == nil then firstParam = "none" end
701
    if eventName == "key" then
702
      local keycode = params[2]
703
      
704
      if keycode == 14 or keycode == 211 then -- Backspace or Delete
705
        inputKey = nil
706
        ignoreNextChar = true
707
      elseif keycode == 200 or keycode == 203 or keycode == 78 then -- Up or Left or +
708
        if inputKey == nil then
709
          inputKey = 1
710
        else
711
          inputKey = inputKey - 1
712
        end
713
        ignoreNextChar = true
714
      elseif keycode == 208 or keycode == 205 or keycode == 74 then -- Down or Right or -
715
        if inputKey == nil then
716
          inputKey = 1
717
        else
718
          inputKey = inputKey + 1
719
        end
720
        ignoreNextChar = true
721
      elseif keycode == 28 then -- Enter
722
        inputAbort = true
723
        ignoreNextChar = true
724
      else
725
        ignoreNextChar = false
726
        -- w.status_showWarning("Key " .. keycode .. " is not supported here")
727
      end
728
      
729
    elseif eventName == "char" then
730
      local character = params[2]
731
      if ignoreNextChar then
732
        ignoreNextChar = false
733
        -- w.status_showWarning("Ignored char #" .. string.byte(character) .. " '" .. character .. "'")
734
      elseif character == '+' then -- +
735
        if inputKey == nil then
736
          inputKey = 1
737
        else
738
          inputKey = inputKey - 1
739
        end
740
      elseif character == '-' then -- -
741
        if inputKey == nil then
742
          inputKey = 1
743
        else
744
          inputKey = inputKey + 1
745
        end
746
      else
747
        w.status_showWarning("Key '" .. character .. "' is not supported here (" .. string.byte(character) .. ")")
748
      end
749
      
750
    elseif eventName == "terminate" then
751
      inputAbort = true
752
      
753
    elseif not w.event_handler(eventName, firstParam) then
754
      w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
755
    end
756
  until inputAbort
757
  term.setCursorBlink(false)
758
  w.setCursorPos(1, y + 1)
759
  w.clearLine()
760
  if inputKey == nil then
761
    return nil
762
  else
763
    return toValue(list[inputKey])
764
  end
765
end
766
767
----------- Event handlers
768
769
local function reboot()
770
  os.reboot()
771
end
772
773
local function sleep(delay)
774
  os.sleep(delay)
775
end
776
777
-- return a global clock measured in second
778
local function event_clock()
779
  return os.clock()
780
end
781
782
local function event_timer_start(name_, period_s_, eventId_)
783
  local name = name_ or "-nameless-"
784
  local eventId = eventId_ or "timer_" .. name
785
  -- check for an already active timer
786
  local countActives = 0
787
  for id, entry in pairs(event_timers) do
788
    if entry.name == name and entry.active then -- already one started
789
      countActives = countActives + 1
790
    end
791
  end
792
  if countActives > 0 then
793
    if name ~= "status" then -- don't report status timer overlaps to prevent a stack overflow
794
      w.status_showWarning("Timer already started for " .. name)
795
    end
796
    return
797
  end
798
  -- start a new timer
799
  local period_s = period_s_ or 1.0
800
  local id = os.startTimer(period_s)
801
  event_timers[id] = {
802
    active = true,
803
    eventId = eventId,
804
    name = name,
805
    period_s = period_s
806
  }
807
end
808
809
local function event_timer_stop(name_)
810
  local name = name_ or "-nameless-"
811
  for id, entry in pairs(event_timers) do
812
    if entry.name == name then
813
      if entry.active then -- kill any active one
814
        entry.active = false
815
        os.cancelTimer(id)
816
      else -- purge all legacy ones
817
        event_timers[id] = nil
818
      end
819
    end
820
  end
821
end
822
823
local function event_timer_stopAll()
824
  for id, entry in pairs(event_timers) do
825
    if entry.active then
826
      event_timers[id] = nil
827
      os.cancelTimer(id)
828
    end
829
  end
830
end
831
832
local function event_timer_tick(id)
833
  local entry = event_timers[id]
834
  local isUnknown = entry == nil
835
  if isUnknown then -- unknown id, report a warning
836
    w.status_showWarning("Timer #" .. id .. " is unknown")
837
    return
838
  end
839
  if not entry.active then -- dying timer, just ignore it
840
    return
841
  end
842
  -- resolve the timer
843
  os.queueEvent(entry.eventId, nil, nil, nil)
844
  -- CC timers are one shot, so we start a new one, if still needed
845
  if entry.active then
846
    entry.active = false
847
    w.event_timer_start(entry.name, entry.period_s)
848
  end
849
end
850
851
local function event_register(eventName, callback)
852
  event_handlers[eventName] = callback
853
end
854
855
-- returns isSupported, needRedraw
856
local function event_handler(eventName, param)
857
  local needRedraw = false
858
  if event_handlers[eventName] ~= nil then
859
    needRedraw = event_handlers[eventName](eventName, param)
860
  elseif eventName == "redstone" then
861
    w.event_redstone(param)
862
  elseif eventName == "timer" then
863
    w.event_timer_tick(param)
864
  elseif eventName == "key_up" then
865
  elseif eventName == "mouse_click" then
866
    w.status_showSuccess("Use the keyboard, Luke!")
867
  elseif eventName == "mouse_up" then
868
  elseif eventName == "mouse_drag" then
869
  elseif eventName == "mouse_scroll" then
870
  elseif eventName == "monitor_touch" then
871
  elseif eventName == "monitor_resize" then
872
  elseif eventName == "disk" then
873
  elseif eventName == "disk_eject" then
874
  elseif eventName == "peripheral" then
875
  elseif eventName == "peripheral_detach" then
876
  -- not supported: task_complete, rednet_message, modem_message
877
  else
878
    return false, needRedraw
879
  end
880
  return true, needRedraw
881
end
882
883
----------- Redstone support
884
885
local tblRedstoneState = {-- Remember redstone state on each side
886
  ["top"] = rs.getInput("top"),
887
  ["front"] = rs.getInput("front"),
888
  ["left"] = rs.getInput("left"),
889
  ["right"] = rs.getInput("right"),
890
  ["back"] = rs.getInput("back"),
891
  ["bottom"] = rs.getInput("bottom"),
892
}
893
894
local function event_redstone()
895
  -- Event only returns nil so we need to check sides manually
896
  local message = ""
897
  for side, state in pairs(tblRedstoneState) do
898
    if rs.getInput(side) ~= state then
899
      -- print(side .. " is now " .. tostring(rs.getInput(side)))
900
      message = message .. side .. " "
901
      tblRedstoneState[side] = rs.getInput(side)
902
    end
903
  end
904
  if message ~= "" then
905
    message = "Redstone changed on " .. message
906
    w.status_showWarning(message)
907
  end
908
end
909
910
----------- Configuration
911
912
local function data_get()
913
  return data
914
end
915
916
local function data_inspect(key, value)
917
  local stringValue = type(value) .. ","
918
  if type(value) == "boolean" then
919
    if value then
920
      stringValue = "true,"
921
    else
922
      stringValue = "false,"
923
    end
924
  elseif type(value) == "number" then
925
    stringValue = value .. ","
926
  elseif type(value) == "string" then
927
    stringValue = "'" .. value .. "',"
928
  elseif type(value) == "table" then
929
    stringValue = "{"
930
  end
931
  print(" " .. key .. " = " .. stringValue)
932
  if type(value) == "table" then
933
    for subkey, subvalue in pairs(value) do
934
      w.data_inspect(subkey, subvalue)
935
    end
936
    print("}")
937
  end
938
end
939
940
local function data_read()
941
  w.data_shouldUpdateName()
942
  
943
  data = { }
944
  if fs.exists("shipdata.txt") then
945
    local size = fs.getSize("shipdata.txt")
946
    if size > 0 then
947
      local file = fs.open("shipdata.txt", "r")
948
      if file ~= nil then
949
        local rawData = file.readAll()
950
        if rawData ~= nil then
951
          data = textutils.unserialize(rawData)
952
        end
953
        file.close()
954
        if data == nil then
955
          data = {}
956
        end
957
      end
958
    end
959
  end
960
  
961
  for name, handlers in pairs(data_handlers) do
962
    handlers.read(data)
963
  end
964
end
965
966
local function data_save()
967
  for name, handlers in pairs(data_handlers) do
968
    handlers.save(data)
969
  end
970
  
971
  local file = fs.open("shipdata.txt", "w")
972
  if file ~= nil then
973
    file.writeLine(textutils.serialize(data))
974
    file.close()
975
  else
976
    w.status_showWarning("No file system")
977
    w.sleep(3.0)
978
  end
979
end
980
981
local function data_getName()
982
  if data_name ~= nil then
983
    return data_name
984
  else
985
    return "-noname-"
986
  end
987
end
988
989
local function data_setName()
990
  -- check if any named component is connected
991
  local componentName = "computer"
992
  for name, handlers in pairs(data_handlers) do
993
    if handlers.name ~= nil then
994
      componentName = name
995
    end
996
  end
997
  
998
  -- ask for a new name
999
  w.page_begin("<==== Set " .. componentName .. " name ====>")
1000
  w.setCursorPos(1, 4)
1001
  w.setColorHelp()
1002
  w.writeFullLine(" Press enter to validate.")
1003
  w.setCursorPos(1, 3)
1004
  w.setColorNormal()
1005
  w.write("Enter " .. componentName .. " name: ")
1006
  data_name = w.input_readText(data_name)
1007
  
1008
  -- update computer name
1009
  os.setComputerLabel(data_name)
1010
  
1011
  -- update connected components
1012
  for name, handlers in pairs(data_handlers) do
1013
    if handlers.name ~= nil then
1014
      handlers.name(data_name)
1015
    end
1016
  end
1017
  
1018
  -- w.reboot() -- not needed
1019
end
1020
1021
local function data_shouldUpdateName()
1022
  local shouldUpdateName = false
1023
  
1024
  -- check computer name
1025
  data_name = os.getComputerLabel()
1026
  if data_name == nil then
1027
    shouldUpdateName = true
1028
    data_name = "" .. os.getComputerID()
1029
  end
1030
  
1031
  -- check connected components names
1032
  for name, handlers in pairs(data_handlers) do
1033
    if handlers.name ~= nil then
1034
      local componentName = handlers.name()
1035
      if componentName == "" then
1036
        shouldUpdateName = true
1037
      elseif shouldUpdateName then
1038
        data_name = componentName
1039
      elseif data_name ~= componentName then
1040
        shouldUpdateName = true
1041
        data_name = componentName
1042
      end
1043
    end
1044
  end
1045
  
1046
  return shouldUpdateName
1047
end
1048
1049
local function data_splitString(source, sep_)
1050
  local sep = sep_ or ":"
1051
  local fields = {}
1052
  local pattern = string.format("([^%s]+)", sep)
1053
  source:gsub(pattern, function(c) fields[#fields + 1] = c end)
1054
  return fields
1055
end
1056
1057
local function data_register(name, callbackRead, callbackSave, callbackName)
1058
  -- read/save callbacks are always defined
1059
  if callbackRead == nil then
1060
    callbackRead = function() end
1061
  end
1062
  if callbackSave == nil then
1063
    callbackSave = function() end
1064
  end
1065
  
1066
  -- name callback is nil when not defined
1067
  
1068
  data_handlers[name] = { read = callbackRead, save = callbackSave, name = callbackName }
1069
end
1070
1071
----------- Devices
1072
1073
local function device_get(address)
1074
  return peripheral.wrap(address)
1075
end
1076
1077
local function device_getMonitors()
1078
  return monitors
1079
end
1080
1081
local function device_register(deviceType, callbackRegister, callbackUnregister)
1082
  device_handlers[deviceType] = { register = callbackRegister, unregister = callbackUnregister }
1083
end
1084
1085
----------- Main loop
1086
1087
1088
local function boot()
1089
  if not term.isColor() then
1090
    print("Advanced computer required")
1091
    error()
1092
  end
1093
  print("loading...")
1094
  
1095
  math.randomseed(os.time())
1096
  
1097
  -- read configuration
1098
  w.data_read()
1099
  w.clear()
1100
  print("data_read...")
1101
  
1102
  -- initial scanning
1103
  monitors = {}
1104
  w.page_begin(data_name .. " - Connecting...")
1105
  w.writeLn("")
1106
  
1107
  local sides = peripheral.getNames()
1108
  for key, address in pairs(sides) do
1109
    w.sleep(0)
1110
    w.write("Checking " .. address .. " ")
1111
    local deviceType = peripheral.getType(address)
1112
    w.write(deviceType .. " ")
1113
    if deviceType == "monitor" then
1114
      w.write("wrapping!")
1115
      local lmonitor = w.device_get(address)
1116
      table.insert(monitors, lmonitor)
1117
      lmonitor.setTextScale(monitor_textScale)
1118
    else
1119
      local handlers = device_handlers[deviceType]
1120
      if handlers ~= nil then
1121
        w.write("wrapping!")
1122
        handlers.register(deviceType, address, w.device_get(address))
1123
      end
1124
    end
1125
    
1126
    w.writeLn("")
1127
  end
1128
  
1129
  -- synchronize computer and connected components names
1130
  local shouldUpdateName = w.data_shouldUpdateName()
1131
  if shouldUpdateName then
1132
    w.data_setName()
1133
  end
1134
  
1135
  -- peripheral boot up
1136
  if page_handlers['0'] == nil then
1137
    w.status_showWarning("Missing handler for connection page '0'!")
1138
    error()
1139
  end
1140
  page_handlers['0'].display(true)
1141
end
1142
1143
local function run()
1144
  local abort = false
1145
  local refresh = true
1146
  local ignoreNextChar = false
1147
  
1148
  local function selectPage(index)
1149
    if page_handlers[index] ~= nil then
1150
      page_callbackDisplay = page_handlers[index].display
1151
      page_callbackKey = page_handlers[index].key
1152
      refresh = true
1153
      return true
1154
    end
1155
    return false
1156
  end
1157
  
1158
  -- start refresh timer
1159
  w.event_register("timer_refresh", function() return page_callbackDisplay ~= page_handlers['0'].display end )
1160
  w.event_register("timer_status" , function() w.status_tick() return false end )
1161
  w.event_timer_start("refresh", run_refreshPeriod_s, "timer_refresh")
1162
  
1163
  -- register common events
1164
  w.event_register("enabled" , function(eventName, param) w.status_showWarning("Enabled '" .. w.format_string(param) .. "'") end )
1165
  w.event_register("disabled", function(eventName, param) w.status_showWarning("Disabled '" .. w.format_string(param) .. "'") end )
1166
  
1167
  -- main loop
1168
  selectPage('0')
1169
  repeat
1170
    if refresh then
1171
      w.clear()
1172
      page_callbackDisplay(false)
1173
      w.page_end()
1174
      refresh = false
1175
    end
1176
    local params = { os.pullEventRaw() }
1177
    local eventName = params[1]
1178
    local firstParam = params[2]
1179
    if firstParam == nil then firstParam = "none" end
1180
    -- w.writeLn("...")
1181
    -- w.writeLn("Event '" .. eventName .. "', " .. firstParam .. " received")
1182
    -- w.sleep(0.2)
1183
    
1184
    if eventName == "key" then
1185
      local keycode = params[2]
1186
      
1187
      ignoreNextChar = false
1188
      if keycode == 11 or keycode == 82 then -- 0
1189
        if selectPage('0') then
1190
          ignoreNextChar = true
1191
        end
1192
      elseif keycode == 2 or keycode == 79 then -- 1
1193
        if selectPage('1') then
1194
          ignoreNextChar = true
1195
        end
1196
      elseif keycode == 3 or keycode == 80 then -- 2
1197
        if selectPage('2') then
1198
          ignoreNextChar = true
1199
        end
1200
      elseif keycode == 4 or keycode == 81 then -- 3
1201
        if selectPage('3') then
1202
          ignoreNextChar = true
1203
        end
1204
      elseif keycode == 5 or keycode == 82 then -- 4
1205
        if selectPage('4') then
1206
          ignoreNextChar = true
1207
        end
1208
      elseif keycode == 6 or keycode == 83 then -- 5
1209
        if selectPage('5') then
1210
          ignoreNextChar = true
1211
        end
1212
      elseif page_callbackKey ~= nil and page_callbackKey("", keycode) then
1213
        refresh = true
1214
      else
1215
        ignoreNextChar = false
1216
        -- w.status_showWarning("Key " .. keycode .. " is not supported here")
1217
      end
1218
      
1219
    elseif eventName == "char" then
1220
      local character = params[2]
1221
      if ignoreNextChar then
1222
        ignoreNextChar = false
1223
        -- w.status_showWarning("Ignored char #" .. string.byte(character) .. " '" .. character .. "'")
1224
--      elseif character == 'x' or character == 'X' then -- x for eXit
1225
--        -- os.pullEventRaw() -- remove key_up event
1226
--        abort = true
1227
      elseif character == '0' then
1228
        selectPage('0')
1229
      elseif character == '1' then
1230
        selectPage('1')
1231
      elseif character == '2' then
1232
        selectPage('2')
1233
      elseif character == '3' then
1234
        selectPage('3')
1235
      elseif character == '4' then
1236
        selectPage('4')
1237
      elseif character == '5' then
1238
        selectPage('5')
1239
      elseif page_callbackKey ~= nil and page_callbackKey(character, -1) then
1240
        refresh = true
1241
      elseif string.byte(character) ~= 0 then -- not a control char
1242
        w.status_showWarning("Key '" .. character .. "' is not supported here (" .. string.byte(character) .. ")")
1243
      end
1244
      
1245
    elseif eventName == "terminate" then
1246
      abort = true
1247
      
1248
    else
1249
      local isSupported, needRedraw = w.event_handler(eventName, firstParam)
1250
      if not isSupported then
1251
        w.status_showWarning("Event '" .. eventName .. "', " .. firstParam .. " is unsupported")
1252
      end
1253
      refresh = needRedraw
1254
    end
1255
  until abort
1256
  
1257
  -- stop refresh timer
1258
  w.event_timer_stop("refresh")
1259
  w.event_timer_stopAll()
1260
end
1261
1262
local function close()
1263
  w.clear(colors.white, colors.black)
1264
  for key, handlers in pairs(device_handlers) do
1265
    w.writeLn("Closing " .. key)
1266
    if handlers.unregister ~= nil then
1267
      handlers.unregister(key)
1268
    end
1269
  end
1270
  
1271
  w.clear(colors.white, colors.black)
1272
  w.setCursorPos(1, 1)
1273
  w.writeLn("Program closed")
1274
  w.writeLn("Type startup to return to home page")
1275
end
1276
1277
-- we simulate LUA library behavior, so code is closer between CC and OC
1278
w = {
1279
  setMonitorColorFrontBack = setMonitorColorFrontBack,
1280
  write = write,
1281
  getCursorPos = getCursorPos,
1282
  setCursorPos = setCursorPos,
1283
  getResolution = getResolution,
1284
  setColorNormal = setColorNormal,
1285
  setColorGood = setColorGood,
1286
  setColorBad = setColorBad,
1287
  setColorDisabled = setColorDisabled,
1288
  setColorHelp = setColorHelp,
1289
  setColorHeader = setColorHeader,
1290
  setColorControl = setColorControl,
1291
  setColorSelected = setColorSelected,
1292
  setColorWarning = setColorWarning,
1293
  setColorSuccess = setColorSuccess,
1294
  clear = clear,
1295
  clearLine = clearLine,
1296
  writeLn = writeLn,
1297
  writeMultiLine = writeMultiLine,
1298
  writeCentered = writeCentered,
1299
  writeFullLine = writeFullLine,
1300
  page_begin = page_begin,
1301
  page_colors = page_colors,
1302
  page_end = page_end,
1303
  page_getCallbackDisplay = page_getCallbackDisplay,
1304
  page_register = page_register,
1305
  page_setEndText = page_setEndText,
1306
  status_clear = status_clear,
1307
  status_isActive = status_isActive,
1308
  status_show = status_show,
1309
  status_refresh = status_refresh,
1310
  status_showWarning = status_showWarning,
1311
  status_showSuccess = status_showSuccess,
1312
  status_tick = status_tick,
1313
  format_float = format_float,
1314
  format_integer = format_integer,
1315
  format_boolean = format_boolean,
1316
  format_string = format_string,
1317
  format_address = format_address,
1318
  format_charNumber = format_charNumber,
1319
  input_readInteger = input_readInteger,
1320
  input_readText = input_readText,
1321
  input_readConfirmation = input_readConfirmation,
1322
  input_readEnum = input_readEnum,
1323
  reboot = reboot,
1324
  sleep = sleep,
1325
  event_clock = event_clock,
1326
  event_timer_start = event_timer_start,
1327
  event_timer_stop = event_timer_stop,
1328
  event_timer_stopAll = event_timer_stopAll,
1329
  event_timer_tick = event_timer_tick,
1330
  event_register = event_register,
1331
  event_handler = event_handler,
1332
  event_redstone = event_redstone,
1333
  data_get = data_get,
1334
  data_inspect = data_inspect,
1335
  data_read = data_read,
1336
  data_save = data_save,
1337
  data_getName = data_getName,
1338
  data_setName = data_setName,
1339
  data_shouldUpdateName = data_shouldUpdateName,
1340
  data_splitString = data_splitString,
1341
  data_register = data_register,
1342
  device_get = device_get,
1343
  device_getMonitors = device_getMonitors,
1344
  device_register = device_register,
1345
  boot = boot,
1346
  run = run,
1347
  close = close,
1348
}