As it stands, your menu structure is what we call "hardcoded": you've written actual code that specifies how the menu should appear and how it should react. Hardcoding is fine for very simple projects, but it becomes less practical as things get more complex. So the idea is to separate the data outlining the menu away from the code, sticking it into a bunch of variables instead, such that changes to that data don't require changes to the code which processes it. Tables, of course, tend offer the best method of handling any set of data larger than "one single value".
Let's say we build a table like this (spacing optional):
local menus = {
{["header"] = "Start", {"CraftOS", "Shutdown", "Restart"}, {runShell, os.shutdown, os.reboot}},
{["header"] = "Apps", {"Clock"}, {runClock}},
{["header"] = "Options", {"Password"}, {runPasswordSetup}},
{["header"] = "NhUI", {"Versions", "Uninstall", "About"}, {runUpdater, runUninstallCheck, runAbout}}
}
Note that values we don't assign keys get numeric indexes automatically. Lua reads the above table as if it were written like this:
local menus = {
[1] = {["header"] = "Start", [1] = {[1] = "CraftOS", [2] = "Shutdown", [3] = "Restart"}, [2] = {[1] = runShell, [2] = os.shutdown, [3] = os.reboot}},
[2] = {["header"] = "Apps", [1] = {[1] = "Clock"}, [2] = {[1] = runClock}},
[3] = {["header"] = "Options", [1] = {[1] = "Password"}, [2] = {[1] = runPasswordSetup}},
[4] = {["header"] = "NhUI", [1] = {[1] = "Versions", [2] = "Uninstall", [3] = "About"}, [2] = {[1] = runUpdater, [2] = runUninstallCheck, [3] = runAbout}}
}
So for example, if we refer to menus[4][2][1], we get the "runUpdater" function pointer. Also note that non-numeric keys are ignored by most of Lua's table-related functions and keywords; for example, #menus[1] gives us the number two, as the "header" key is ignored when counting the values, and the numerically indexed values in menus[1] consist of two tables (menus[1][1] and menus[1][2]).
With all this info condensed into one place, we can then have the script itself generate
more useful info in the table for us during init (the point before you begin your main program loop) - specifically, info about where each menu item should be rendered, and hence where the user must click to activate it. The
rows related to each item are already fairly obvious (if "Shutdown" is the second menu item on the "Start" menu, then it should be rendered two down from the top row, placing it on the third), but columns are a bit of a different story.
local colBump = 1
for i = 1, #menus do
local thisMenu = menus[i]
thisMenu.xStart = colBump
thisMenu.xEnd = colBump + #thisMenu.header + 1
colBump = colBump + #thisMenu.header + 5
end
So now we know that, for example, the column to write the header for the "Options" menu is menus[3].xStart. We also know that a click where x >= menus[3].xStart and x <= menus[3].xEnd and y == 1 is targetting that Options menu, too.
For the entries inside the menus, we need to know their lengths as well - and we can even go through and pad them with spaces and numbers, making all the entries within the same menu the same length and ready to draw straight to the screen later. Let's say we expand our above loop:
local colBump = 1
for i = 1, #menus do
local thisMenu = menus[i]
thisMenu.xStart = colBump
thisMenu.xEnd = colBump + #thisMenu.header + 1
local thisSubMenu, maxStringLen = menus[i][1], 0
for j = 1, #thisSubMenu do
if #thisSubMenu[j] > maxStringLen then maxStringLen = #thisSubMenu[j] end
end
maxStringLen = maxStringLen + 2
thisMenu.xSubMenuEnd = colBump + maxStringLen - 1
for j = 1, #thisSubMenu do
thisSubMenu[j] = tostring(j) .. "|" .. thisSubMenu[j] .. string.rep(" ", maxStringLen - #thisSubMenu[j])
end
colBump = colBump + #thisMenu.header + 5
end
menus[?].xSubMenuEnd now acts as menus[?].xEnd does, but for the items in the menu instead of for the menu header.
Let's say we want to draw our menus. We'll scrap the menuBar() function, and combine its functionality into the drawMenus() function. Bearing in mind that "s" indicates the index of the opened menu (or is set to 0 if no menu is open at all):
local function drawMenus()
term.setTextColor(deskColors.menuTextColor)
term.setBackgroundColor(deskColors.menuColor)
for i = 1, #menus do
local thisMenu = menus[i]
term.setCursorPos(thisMenu.xStart, 1)
term.write((s == 0 and tostring(i) or "-") .. "|" .. thisMenu.header) --# See http://lua-users.org/wiki/TernaryOperator
end
if s > 0 then
local thisSubMenu, x = menus[s][1], menus[s].xStart
for i = 1, #thisSubMenu do
term.setCursorPos(x, i + 1)
term.write(thisSubMenu[i])
end
end
end
That only leaves user input to deal with:
local function runDesktop()
s=0 --# selection rule
while true do
local event,b,x,y=os.pullEventRaw()
if event == "mouse_click" and b == 1 then
if y == 1 then
--# A click to the top row:
s = 0
for i = 1, #menus do
if x >= menus[i].xStart and x <= menus[i].xEnd then
s = i
break
end
end
elseif s > 0 and y <= #menus[s][1] + 1 and x >= menus[s].xStart and x <= menus[s].xSubMenuEnd then
menus[s][2][y - 1]() --# Run the function for the selected menu item.
s = 0
else
s = 0
end
drawMenus()
elseif event == "char" then
b = string.byte(b) - 48 --# "1" is character 49, "2" is character 50, etc
if s == 0 and b > 0 and b <= #menus then
s = b
drawMenus()
elseif s > 0 and b > 0 and b <= #menus[s][1] then
menus[s][2][b]()
s = 0
drawMenus()
end
end
end
end
At this point, just about all hardcoding is removed - it's pretty trivial to add or shuffle menu items (or entire new menus) simply by messing with the table.