Advertisement
Inksaver

Menu Library (2023/07/23)

Apr 7th, 2020 (edited)
2,056
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 17.49 KB | None | 0 0
  1. version = 20230723.1200
  2. --[[
  3.     Last edited: see version YYYYMMDD.HHMM
  4.     This is meant to be used as a library for any of your programs.
  5.     Save it as menu.lua preferably in a subfolder called 'lib'
  6.     To use it:
  7.     local menu = require("lib.menu")
  8.    
  9.     local prompt = "Choose your option:"
  10.     -- table of options, whatever text you like from 1 to 10 for turtles, 1 to 16 for computers
  11.     local options = {"First choice", "Second choice", "Third choice", "Fourth choice", "Fifth choice",
  12.                     "Sixth choice","Seventh choice", "Eighth choice", "Ninth choice", "Tenth choice"}              
  13.     local choice = menu.new(prompt, options) OR local choice = menu.menu(prompt, options)
  14.     if choice == nil then
  15.         print ("You chose to cancel the menu")
  16.     else
  17.         print("You chose option no "..choice..": "..options[choice])
  18.     end
  19.    
  20.     menu.clear()
  21.     local line, test, age = 1, "", 0, 0
  22.     test, line = menu.getString("Type your name", true, 2, 10, line, colors.green, colors.black)
  23.     line = line + 1
  24.     age, line = menu.getInteger("Type your age", 5, 99, line, colors.black, colors.green)
  25.     line = line + 1
  26. ]]
  27. local menuPrompt = "Type number (q to quit) + Enter "
  28. local width, height  = term.getSize()
  29. local blank = (" "):rep(width)
  30. local delay = 2
  31. local sep   = "~"
  32. local lib = {}
  33.  
  34. function lib.clear()
  35.     -- clear the terminal and reset cursor
  36.     term.clear()
  37.     term.setCursorPos(1, 1)
  38. end
  39.  
  40. function lib.clearInputField(row)
  41.     --[[ use setCursorPos to delete a line of text ]]
  42.     local cols, rows = term.getSize()
  43.     term.setCursorPos(1, row)
  44.     print(blank)                    -- module variable
  45.     if rows - row > 1 then
  46.         term.setCursorPos(1, row + 1)   -- next line in case overflow
  47.         print(blank)
  48.     end
  49.     term.setCursorPos(1, row)       -- reset
  50. end
  51.  
  52. function lib.formatPrompt(prompt)
  53.     if prompt:endsWith(" ") then
  54.         return prompt
  55.     end
  56.    
  57.     return prompt.." "
  58. end
  59.  
  60. function lib.padLeft(text, length, char)
  61.     --[[Pads str to length len with char from left]]
  62.     if char == nil then char = ' ' end
  63.     local padding = ''
  64.     for i = 1, length - #text do
  65.         padding = padding..char
  66.     end
  67.     return padding..text
  68. end
  69.  
  70. function lib.padRight(text, length, char)
  71.     --[[
  72.     Pads string to length len with chars from right
  73.     test = lib.padRight("test", 10, "+") -> "test++++++"]]
  74.     char = char or " "
  75.     --if char == nil then char = ' ' end
  76.     local padding = ''
  77.     for i = 1, length - #text do
  78.         padding = padding..char
  79.     end
  80.     return text..padding
  81. end
  82.  
  83. function lib.resetColors()
  84.     if term.isColor() then
  85.         term.setTextColor(colors.white)
  86.         term.setBackgroundColor(colors.black)
  87.     end
  88. end
  89.  
  90. function lib.trim(text)
  91.     --[[ trim leading and trailing spaces ]]
  92.     return (text:gsub("^%s*(.-)%s*$", "%1"))
  93. end
  94.  
  95. function lib.print(text, fg, bg, width)
  96.     local cols, rows = term.getSize()
  97.     width = width or cols
  98.     if width == 0 then width = cols end
  99.     if width > cols then width = cols end
  100.     local currentbg = colors.black
  101.     local currentfg = colors.white
  102.     if term.isColor() then
  103.         fg = fg or term.getTextColor()
  104.         bg = bg or term.getBackgroundColor()
  105.         currentbg = term.getBackgroundColor()
  106.         currentfg = term.getTextColor()
  107.         term.setTextColor(fg)
  108.         term.setBackgroundColor(bg)
  109.     end
  110.     print(lib.padRight(text, width, " "))
  111.     if term.isColor() then
  112.         term.setTextColor(currentfg)
  113.         term.setBackgroundColor(currentbg)
  114.     end
  115. end
  116.  
  117. function lib.enterToContinue(prompt, fg, bg)
  118.     prompt = prompt or "Enter to continue "
  119.     term.setCursorPos(1, height)
  120.     lib.write(prompt, fg , bg, nil, true)
  121.     read()
  122. end
  123.  
  124. function lib.write(text, fg, bg, width, isInput, cr)
  125.     cr = cr or false
  126.     local cols, rows = term.getSize()
  127.     isInput = isInput or false
  128.     width = width or cols
  129.     if width == 0 then width = cols end
  130.     if width > cols then width = cols end
  131.     local currentbg = colors.black
  132.     local currentfg = colors.white
  133.     if term.isColor() then
  134.         fg = fg or term.getTextColor()
  135.         bg = bg or term.getBackgroundColor()
  136.         currentbg = term.getBackgroundColor()
  137.         currentfg = term.getTextColor()
  138.         term.setTextColor(fg)
  139.         term.setBackgroundColor(bg)
  140.     end
  141.     write(lib.padRight(text, width, " "))
  142.     if term.isColor() then
  143.         term.setTextColor(currentfg)
  144.         term.setBackgroundColor(currentbg)
  145.     end
  146.     local _, row = term.getCursorPos()
  147.     if isInput then
  148.         term.setCursorPos(#text + 1, row)
  149.     end
  150.     if cr then
  151.         term.setCursorPos(1, row)
  152.     end
  153. end
  154.  
  155. local function clear()
  156.     lib.clear()
  157.     return 1, 1
  158. end
  159.  
  160. function string:count(c)
  161.     --[[ count number of occurences of c ]]
  162.     local _,n = self:gsub(c,"")
  163.     return n
  164. end
  165.  
  166. function string:endsWith(ending)
  167.     --[[ get ending character of a string ]]
  168.     return ending == "" or self:sub(-#ending) == ending
  169. end
  170.  
  171. function string:split(sSeparator, nMax, bRegexp, noEmpty)
  172.     --[[return a table split with sSeparator. noEmpty removes empty elements
  173.         use: tblSplit = SplitTest:split('~',[nil], [nil], false) or tblSplit = string.split(SplitTest, '~')]]  
  174.     assert(sSeparator ~= '','separator must not be empty string')
  175.     assert(nMax == nil or nMax >= 1, 'nMax must be >= 1 and not nil')
  176.     if noEmpty == nil then noEmpty = true end
  177.  
  178.     local aRecord = {}
  179.     local newRecord = {}
  180.     -- self refers to the 'string' being split
  181.     if self:len() > 0 then
  182.         local bPlain = not bRegexp
  183.         nMax = nMax or -1
  184.  
  185.         local nField, nStart = 1, 1
  186.         local nFirst,nLast = self:find(sSeparator, nStart, bPlain)
  187.         while nFirst and nMax ~= 0 do
  188.             aRecord[nField] = self:sub(nStart, nFirst-1)
  189.             nField = nField+1
  190.             nStart = nLast+1
  191.             nFirst,nLast = self:find(sSeparator, nStart, bPlain)
  192.             nMax = nMax-1
  193.         end
  194.         aRecord[nField] = self:sub(nStart)
  195.        
  196.         if noEmpty then --split on newline preserves empty values
  197.             for i = 1, #aRecord do
  198.                 if aRecord[i] ~= "" then
  199.                     table.insert(newRecord, aRecord[i])
  200.                 end
  201.             end
  202.         else
  203.             newRecord = aRecord
  204.         end
  205.     end
  206.    
  207.     return newRecord
  208. end
  209.  
  210. local function colourText(row, text, reset)
  211.     --[[
  212.         This uses the default char ~ to separate colour strings                
  213.         change the line:  sep = "~" as required                                
  214.         other possibles are ` (backtick) ¬ (NOT) or any character you will not use in text
  215.         example text = "~red~This is a line of red text"
  216.     ]]
  217.     row = row or 1 
  218.     term.setCursorPos(1, row)
  219.     local newline = false
  220.     if reset == nil then reset = true end
  221.     row = row + text:count('\n')        -- account for embedded newline characters
  222.     if text:find(sep) ~= nil then                   -- text has colour tags in it eg  hello ~red~world
  223.         local lineParts = text:split(sep)
  224.         for i = 1, #lineParts do
  225.             part = lineParts[i]
  226.             if colors[part]~= nil then      -- is 'red' in the colors dictionary?
  227.                 -- change the TextColour
  228.                 if term.isColor() then
  229.                     term.setTextColor(colors[part])
  230.                 end
  231.             else                                    -- not a colour command so print it out without newline
  232.                 write(part) -- NOT term.write as this does not use \n
  233.             end
  234.         end
  235.         if reset and term.isColor() then
  236.             term.setTextColor(colors.white)
  237.         end
  238.     else -- no colour tags in the text
  239.         write(text)
  240.         if not text:endsWith("\n") then
  241.             newline = true
  242.         end
  243.     end
  244.     if newline then
  245.         write("\n")    -- Add newline to complete the print command
  246.         row = row + 1
  247.     end
  248.    
  249.     return row
  250. end
  251.  
  252. local function errorMessage(row, errorType, userInput, minValue, maxValue)
  253.     --local function drawError(prompt, options, width, height, errorNo, currentLine)
  254.     --[[ Display error message to the user for <delay> seconds ]]
  255.     minValue = minValue or 0
  256.     maxValue = maxValue or 0
  257.     local message = "Just Enter or spacebar doesn't work" -- default for "noinput"
  258.     if errorType == "string" then
  259.         message = "Text length -> "..minValue.." to "..maxValue.." characters"
  260.     elseif errorType == "bool" then
  261.         message = "Answer must start with y or n"
  262.     elseif errorType == "nan" then
  263.         message = "Enter a number "..userInput.." -> ?"
  264.     elseif errorType == "notint" then
  265.         message = "Enter a whole number only"
  266.     elseif errorType == "range" then
  267.         message = "Enter number from "..minValue.." to "..maxValue
  268.     elseif errorType == "modifier" then
  269.         message = "Number "..minValue.." to "..maxValue.." with 'h' or 'i' eg 3h"
  270.     end
  271.  
  272.     lib.clearInputField(row)        -- clear row just used
  273.     lib.write(message, colors.white, colors.red, width, false)
  274.     sleep(delay)
  275.     lib.clearInputField(row)
  276. end
  277.  
  278. local function processInput(prompt, minValue, maxValue, dataType, row, fg, bg, default)
  279.     --[[ validate input, raise error messages until input is valid ]]
  280.     minValue = minValue or 0
  281.     maxValue = maxValue or 1000000000
  282.     dataType = dataType or "string"
  283.     fg       = fg or colors.white
  284.     bg       = bg or colors.black
  285.     local col, line = term.getCursorPos()
  286.     row = row or line
  287.     local startRow = row
  288.     local validInput = false
  289.     local userInput
  290.     if type(prompt) == "table" then
  291.         assert(type(fg)  == "table", "Prompt is a table. Matching number of fg colours required")
  292.     end
  293.     while not validInput do
  294.         for i = startRow, row do
  295.             lib.clearInputField(row)
  296.         end
  297.         lib.clearInputField(row)
  298.         if type(prompt) == "table" then
  299.             row = startRow
  300.             term.setCursorPos(1, row)
  301.             for i = 1, #prompt do
  302.                 if i < #prompt then
  303.                     lib.print(prompt[i], fg[i], bg)
  304.                     row = row + 1
  305.                 else -- last line of multi-line prompt
  306.                     prompt[i] = lib.formatPrompt(prompt[i])
  307.                     lib.write(prompt[i], fg[i], bg, 0, true)
  308.                 end
  309.             end
  310.         else
  311.             prompt = lib.formatPrompt(prompt)
  312.             lib.write(prompt, fg, bg, width, true)
  313.         end
  314.         --lib.write(prompt..">", fg, bg, width, true)
  315.         userInput = read():trim()
  316.         local output = userInput
  317.         if dataType == "string" then
  318.             if userInput:len() == 0 and minValue > 0 then
  319.                 if default ~= nil then
  320.                     return default, row
  321.                 else
  322.                     errorMessage(row, "noinput", output, minValue, maxValue)
  323.                 end
  324.             else
  325.                 if userInput:len() < minValue or userInput:len() > maxValue then
  326.                     errorMessage(row, "string", output, minValue, maxValue)
  327.                 else
  328.                     validInput = true
  329.                 end
  330.             end
  331.         else
  332.             if userInput:len() == 0 then
  333.                 if default ~= nil then
  334.                     return default, row
  335.                 else
  336.                     errorMessage(row, "noinput", output, minValue, maxValue)
  337.                 end
  338.             else
  339.                 if dataType == "bool" then     
  340.                     if userInput:sub(1, 1):lower() == "y" then
  341.                         userInput = true
  342.                         validInput = true
  343.                     elseif userInput:sub(1, 1):lower() == "n" then
  344.                         userInput = false
  345.                         validInput = true
  346.                     else
  347.                         errorMessage(row, "bool", output, minValue, maxValue)
  348.                     end
  349.                 else
  350.                     if dataType == "int" or dataType == "float" then
  351.                         userInput = tonumber(userInput)        
  352.                     end
  353.                     if userInput == nil then
  354.                         errorMessage(row, "nan", output, minValue, maxValue)
  355.                     else
  356.                         if userInput >= minValue and userInput <= maxValue then
  357.                             if math.floor(userInput / 1) ~= userInput and dataType == "int"  then
  358.                                 errorMessage(row, "notint", output, minValue, maxValue)
  359.                             else
  360.                                 validInput = true
  361.                             end
  362.                         else
  363.                             errorMessage(row, "range", output, minValue, maxValue)
  364.                         end
  365.                     end
  366.                 end
  367.             end
  368.         end
  369.     end
  370.     return userInput, row
  371. end
  372.  
  373. local function drawMenu(prompt, options, width, height, pp, altMenuPrompt)
  374.     local mPrompt = menuPrompt -- assign local variable to global value
  375.     if altMenuPrompt ~= nil then
  376.         mPrompt = lib.formatPrompt(altMenuPrompt) -- use different prompt
  377.     else
  378.         if not pp.allowModifier then
  379.             mPrompt = "Type number + Enter "
  380.         end
  381.     end
  382.     local numOptions = #options
  383.     if numOptions > height - 3 then
  384.         numOptions = height - 3
  385.     end
  386.     lib.clear()
  387.     lib.print(prompt, pp.prompt, colors.black, width)
  388.     for i = 1, numOptions do
  389.         local trimOption = string.sub(options[i], 1, width - 5)
  390.         if i < 10 then
  391.             lib.print("\t"..i..") ".."\t"..trimOption, pp.itemColours[i], colors.black, width)
  392.         else
  393.             lib.print("\t"..i..") "..trimOption, pp.itemColours[i], colors.black, width)
  394.         end
  395.     end
  396.     lib.write(mPrompt, pp.menuPrompt, colors.black, width, true)
  397.     lib.resetColors()
  398.  
  399.     return term.getCursorPos()
  400. end
  401.  
  402. local function menu(prompt, options, pp, altMenuPrompt)
  403.     --turtle   terminal size = 39 x 13 -- max menu options = 10, max option length = 34
  404.     --computer terminal size = 51 x 19 -- max menu options = 16, max option length = 47
  405.     --local width, height = term.getSize()
  406.     local errorType = ""
  407.     local choice = nil
  408.     local modifier = ""
  409.     local numOptions = #options
  410.     if pp == nil then -- no prettyprint table supplied
  411.         pp = {}
  412.         pp.prompt = colors.green
  413.         pp.itemColours = {}
  414.         for i = 1, numOptions do
  415.             table.insert(pp.itemColours, colors.white)
  416.         end
  417.         pp.menuPrompt = colors.yellow
  418.         pp.allowModifier = false
  419.     else
  420.         assert(#options == #pp.itemColours, "Menu options (".. #options..") pp.itemColours (".. #pp.itemColours..") do not match")
  421.     end
  422.     prompt = lib.formatPrompt(prompt)
  423.     while choice == nil and modifier == "" do
  424.         local col, row = drawMenu(prompt, options, width, height, pp, altMenuPrompt)
  425.         local isValid, isModifierValid, isNumberValid = false, false, false
  426.         term.setCursorPos(col, row)
  427.         if errorType ~= "" then
  428.             errorMessage(row, errorType, choice, 1, #options)
  429.             --lib.write(menuPrompt, pp.menuPrompt, colors.black, width, true)
  430.             col, row = drawMenu(prompt, options, width, height, pp)
  431.         end
  432.         choice = read()
  433.         if choice == "" then -- enter only
  434.             errorType = "noinput"
  435.             choice = nil
  436.         else
  437.             local test = tonumber(choice)
  438.             if test == nil then -- may have 3h 5i 2help etc
  439.                 if pp.allowModifier then
  440.                     if choice:lower():find("q") ~= nil then -- q(uit) entered
  441.                         return nil, "q" -- quit chosen
  442.                     elseif choice:lower():find("h") ~= nil then --h(elp)
  443.                         modifier = "h"          -- help chosen
  444.                         isModifierValid = true
  445.                         choice = tonumber(choice:sub(1, #choice - 1))
  446.                         if choice ~= nil then
  447.                             isNumberValid = true
  448.                         end
  449.                     elseif choice:lower():find("i") ~= nil then
  450.                         modifier = "i"          -- i(nventory) chosen
  451.                         isModifierValid = true
  452.                         choice = tonumber(choice:sub(1, #choice - 1))
  453.                         if choice ~= nil then
  454.                             isNumberValid = true
  455.                         end
  456.                     end
  457.                 else
  458.                     errorType = "nan"
  459.                     choice = nil
  460.                 end
  461.             else -- user entered a number only
  462.                 choice = tonumber(choice)
  463.                 isNumberValid = true
  464.             end
  465.             if isNumberValid then -- number only or extracted number + modifier
  466.                 if math.floor(choice / 1) ~= choice then   
  467.                     errorType = "notint" -- integer only
  468.                     choice = nil
  469.                     modifier = ""
  470.                 elseif choice < 1 or choice > #options then
  471.                     errorType = "range"
  472.                     choice = nil
  473.                     modifier = ""
  474.                 end
  475.             else
  476.                 errorType = "modifier" -- number missing
  477.                 choice = nil
  478.                 modifier = ""
  479.             end
  480.         end
  481.     end
  482.     lib.clear()
  483.     return choice, modifier -- nil, "q" = quit | #, "h" = help needed for that choice | #, "" number chosen
  484. end
  485.  
  486. local function tchelper(first, rest)
  487.     return first:upper()..rest:lower()
  488. end
  489.  
  490. local function toTitle(inputText) --converts any string to Title Case
  491.     return inputText:gsub("(%a)([%w_']*)", tchelper)
  492. end
  493.  
  494. local function getBool(prompt, row, fg, bg, default)
  495.     --[[ Return a boolean. Based on y(es)/ n(o) response ]]
  496.     return processInput(prompt, 1, 3, "bool", row, fg, bg, default)
  497. end
  498.  
  499. local function getFloat(prompt, minVal, maxVal, row, fg, bg, default)
  500.     return getNumber(prompt, minVal, maxVal, row, fg, bg, default)
  501. end
  502.  
  503. local function getInteger(prompt, minValue, maxValue, row, fg, bg, default)
  504.     --[[ Return an integer. minValue and maxValue are given defaults if not passed ]]
  505.     minValue = minValue or 0
  506.     maxValue = maxValue or 65536
  507.  
  508.     return processInput(prompt, minValue, maxValue, "int", row, fg, bg, default) -- userInput, row
  509. end
  510.  
  511. local function getNumber(prompt, minValue, maxValue, row, fg, bg, default)
  512.     --[[ Return a real number. minValue and maxValue are given defaults if not passed ]]
  513.     minValue = minValue or -1000000000
  514.     maxValue = maxValue or  1000000000
  515.  
  516.     return processInput(prompt, minValue, maxValue, "float", row, fg, bg, default)
  517. end
  518.  
  519. local function getString(prompt, withTitle, minValue, maxValue, row, fg, bg, default)
  520. --[[ Return a string. withTitle, minValue and maxValue are given defaults if not passed ]]
  521.     withTitle = withTitle or false
  522.     minInt = minInt or 1
  523.     maxInt = maxInt or 20
  524.    
  525.     local userInput, row = processInput(prompt, minValue, maxValue, "string", row, fg, bg, default)
  526.     if withTitle then
  527.         userInput = toTitle(userInput)
  528.     end
  529.    
  530.     return userInput, row
  531. end
  532.  
  533. string.trim = lib.trim
  534. string.padRight = lib.padRight      -- if using as library: string.PadRight = Xconsole.PadRight
  535. string.padLeft  = lib.padLeft       -- if using as library: string.PadLeft = Xconsole.PadLeft
  536.  
  537. return
  538. {
  539.     clear = clear,              -- clear terminal, return 1,1
  540.     menu = menu,                -- call menu function
  541.     new = menu,                 -- call menu function using new()
  542.     getBool = getBool,          -- call getBool function
  543.     getBoolean = getBool,       -- call getBool function using getBoolean()
  544.     getNumber = getNumber,      -- call getNumber function
  545.     getFloat = getNumber,       -- call getNumber function using getFloat()
  546.     getInteger = getInteger,    -- call getInteger function
  547.     getString = getString,      -- call getString function
  548.     colourPrint = lib.print,    -- call lib.print function (print using specified colours)
  549.     colorPrint = lib.print,     -- call lib.print function (print using specified colours) using colorPrint()
  550.     colourWrite = lib.write,    -- call lib.write function (write using specified colours)
  551.     colorWrite = lib.write,     -- call lib.write function (write using specified colours) using colorWrite()
  552.     colourText = colourText,    -- call colourText function (print text with embedded colour strings)
  553.     colorText = colourText,     -- call colourText function (print text with embedded colour strings) using colorText()
  554.     enterToContinue = lib.enterToContinue, -- calls lib.enterToContinue to request user input
  555.     padLeft = lib.padLeft,      -- call lib.padLeft
  556.     padRight = lib.padRight     -- call lib.padRight
  557. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement