· 7 months ago · Feb 18, 2025, 05:45 PM
1--Menu API made by MrJohnDowe
2--Version 1.0.1
3
4function titleize(text)
5local x = term.getSize()
6if #text+2 > x then return text:upper() end
7text = " "..string.upper(text).." "
8for i=1, math.floor((x-#text)/2) do
9 text = "="..text.."="
10end
11return text
12end
13function sentenceCaseTable(tab) --Expects a prepared table or sequentially numbered
14 local toRet = {} --So we don't go modifying people's tables
15 for a, b in pairs(tab) do
16 if type(b) == "table" then
17 local toMod = (b.value or b.text) --Priority to value so you can have pretty yet functional tables
18 table.insert(toRet[a], toMod:sub(1,1):upper()..toMod(2))
19 elseif type(b) == "string" then
20 toRet[a] = b:sub(1,1):upper()..b:sub(2)
21 end
22 end
23 return toRet
24 end
25
26function prepareTable(tab)
27 local toRet = {}
28 for a,b in pairs(tab) do
29 table.insert(toRet, {key = a, value = b})
30 end
31 return toRet
32end
33
34function menu(title, description, textTable, isNumbered, titleAlign, textAlign, prefixCharacter, suffixCharacter, spaceCharacter, incrementFunction)
35local x, y = term.getSize() --Screen size
36local currIndex, scroll = 1, 0 --currIndex is the item from the table it is on, scroll is how many down it should go.
37local titleLines, descriptionLines = 0,0 --How many lines the title and description take up
38local alignments = { left = "left", center = "center", right = "right" } --Used for checking if alignment is valid
39if not (title and textTable) then error("Requires title and menu list",2) end
40if not type(textTable) == "table" and #textTable >= 1 then error("Menu list must be a table with values",2) end
41if isNumbered == nil then isNumbered = true end --Setting isNumbered default
42titleAlign = alignments[titleAlign] or alignments.center --Default title alignment
43textAlign = alignments[textAlign] or alignments.left --Default options alignment
44prefixCharacter = prefixCharacter or "["
45suffixCharacter = suffixCharacter or "]"
46spaceCharacter = spaceCharacter or "."
47for i=1, #textTable do
48 if type(textTable[i]) ~= "table" then
49 textTable[i] = {text = textTable[i]} --If it is given without key and value pairs
50 end
51 textTable[i].text = textTable[i].text or textTable[i].value --This allows you to have tables of text, function pairs. So my function returns 1, and you call input[1].func()
52 textTable[i].key = textTable[i].key or i
53end
54local function align(text, alignment) --Used to align text to a certain direction
55 if alignment == "left" then return 1 end
56 if alignment == "center" then return (x/2)-(#text/2) end
57 if alignment == "right" then return x - #text+1 end
58 error("Invalid Alignment",3) --Three because is only called by output
59end
60local function seperateLines(text) --Separates multi-line text into a table
61 if type(text) ~= "string" then error("Separate Lines expects string, got "..type(text),2) end
62 local toRet = {}
63 local originalText = text --I do this because it may break the gsub if I modify while iterating
64 while true do
65 local count = 0
66 text = originalText
67 if #toRet >= 1 and not text:match("[^ ]") then --If there are no non-space characters, you are done
68 toRet[#toRet] = toRet[#toRet]..text:match(" *$") --Get buffer spaces at end
69 return toRet, #toRet
70 end
71 table.insert(toRet, "")
72 if #toRet == 1 then --We want to add in buffer spaces at the beginning
73 toRet[1] = toRet[1]..text:match("^ *")
74 count = #toRet[1]
75 originalText = originalText:sub(#toRet[1]+1)
76 end
77 for word in text:gmatch("[^ ]+ *") do --Non space characters with an optional space(s) at the end
78 local toBreak
79 local found = word:find("\n")--This makes newLines actually work (hopefully)
80 if found then
81 word = word:sub(1,found-1)
82 originalText = originalText:sub(1,found-1)..originalText:sub(found+1) --Cut out the newline
83 toBreak = true --If this line should be cut off
84 end
85 count = count + #word --Counts characters so we don't go over limit
86 if count <= x or #word > x then --The second is for emergencies, if the word is longer than a line, put it here anyways
87 toRet[#toRet] = toRet[#toRet]..word
88 originalText = originalText:sub(#word+1) --Sub out the beginning
89 if toBreak then break end
90 else
91 break --Go to next line
92 end
93 end
94 end
95end
96
97local function output(text,y, alignment, assumeSingle) --My own term.write with more control
98 local originalAlignment, printTab, lines = alignment --Setting locals
99 if type(text) == "table" then --Assuming this is from seperateLines
100 printTab, lines = text, #text
101 elseif assumeSingle then --Saves from doing seperateLines on all the menu options
102 printTab, lines = {text}, 1
103 else
104 printTab, lines = seperateLines(text)
105 end
106 for i=1, lines do
107 local x = align(printTab[i], alignment)
108 term.setCursorPos(x,y+i-1) ---1 because it will always be at least +1
109 term.clearLine()
110 term.write(printTab[i])
111 --term.write(" Writing to "..tostring(x)..","..tostring(y+i-1).." lines "..tostring(lines)) --Debug
112 --os.pullEvent("char")
113 end
114end
115
116title, titleLines = seperateLines(title)
117if description then --descriptionLines is how many lines the description takes up
118 description, descriptionLines = seperateLines(description)
119end
120local upperLines = descriptionLines + titleLines --The title line, descriptions, plus extra line
121if upperLines > y-3 then error("Top takes up too many lines",2) end --So at least two options are on screen
122local top, bottom = 1, (y-upperLines) --These two are used to determine what options are on the screen right now (through scroll)
123while true do
124 while currIndex <= top and top > 1 do --If index is at top, scroll up
125 scroll = scroll - 1
126 top, bottom = top - 1, bottom - 1
127 end
128 while currIndex >= bottom and bottom < #textTable do --If at bottom scroll down. Change to > instead of >= to only do on bottom line. Same for above
129 scroll = scroll + 1
130 top, bottom = top + 1, bottom + 1
131 end
132 term.clear()
133 output(title,1, titleAlign) --Print title
134 if descriptionLines >= 1 then --Not an else because we don't want to print nothing
135 output(description,titleLines+1, titleAlign)
136 end
137 for i = 1, math.min(y - upperLines,#textTable) do --The min because may be fewer table entries than the screen is big
138 local prefix, suffix = "", "" --Stuff like spaces and numbers
139 if isNumbered then prefix = tostring(textTable[i+scroll].key)..spaceCharacter.." " end --Attaches a number to the front
140 if i + scroll == currIndex then prefix = prefixCharacter.." "..prefix; suffix = suffix.." "..suffixCharacter --Puts brackets on the one highlighted
141 elseif textAlign == "left" then for i=1, #prefixCharacter+1 do prefix = " "..prefix end --This helps alignment
142 elseif textAlign == "right" then for i=1, #suffixCharacter+1 do suffix = suffix.." " end --Same as above
143 end
144 local toPrint = prefix..textTable[i+scroll].text..suffix
145 if #toPrint > x then term.clear(); term.setCursorPos(1,1); error("Menu item "..tostring(i+scroll).." is longer than one line. Cannot Print",2) end
146 output(toPrint, i + upperLines, textAlign, true)
147 end
148 if type(incrementFunction) ~= "function" then --This allows you to have your own custom logic for how to shift up and down and press enter.
149 incrementFunction = defaultMenuKeyHandler--e.g. You could use redstone on left to increment, right to decrement, front to press enter.
150 end
151 action = incrementFunction(currIndex, #textTable)
152 if type(action) == number or tonumber(action) then
153 local num = tonumber(action)
154 if num <= #textTable and num > 0 then
155 currIndex = num
156 end
157 elseif action == "up" and currIndex > 1 then
158 currIndex = currIndex - 1
159 elseif action == "down" and currIndex < #textTable then
160 currIndex = currIndex + 1
161 elseif action == "enter" then
162 return textTable[currIndex].text, textTable[currIndex].key, currIndex, textTable[currIndex].value
163 end
164end
165end
166
167function defaultMenuKeyHandler(index, maxIndex)
168 while true do --So it doesn't redraw every time button pressed
169 _, key = os.pullEvent("key")
170 if key == 200 then
171 if index == 1 then return maxIndex end --Go to bottom if at top
172 return "up"
173 elseif key == 208 then
174 if index == maxIndex then return 1 end --Go to top if at bottom
175 return "down"
176 elseif key == 28 then return "enter"
177 elseif key >= 2 and key <= 11 then return key-1 --This is for quickly selected a menu option
178 end
179 end
180end