I come from a JS background and I am pretty new to lua and terminals, i have a basic terminal application. I wanted to understand how you would go about implementing let's say a progress bar that runs in a nonblocking way, right now the progress bar runs but you cannot do any tasks or add an input while the progress bar runs, I have been trying to implement it but i am honestly just confused, I wanted a functionality like this:
[==============]40%
press 'q' to go back
You can switch between the menu while the progress bar runs.
Here's the code for the main application and the progress bar ui file :
main.lua
local sys = require("system")
local UI = require("ui")
local copas = require("copas")
function displayMenu()
print("=============")
print("1. Check Time")
print("2. Get Mono Time")
print("3. Give Feedback")
print("4. Progress Bar Demo")
print("6. Exit")
print("=============")
end
function sleep()
local input = io.read()
local gotInput = sys.readkey(input)
print(gotInput)
end
function getTime()
local time = sys.gettime()
local date = os.date("Current Time: %Y-%m-%d %H:%M:%S", time)
print(date)
end
function monoTime()
local response = sys.monotime()
print(response)
end
function UI.prompt(message)
print(message .. " (yes/no):")
local response = io.read()
if response == "yes" then
return true
else return false end
end
function uiPrompt()
local response = UI.prompt("Do you like lua?")
if response == true then
print("Thats great!")
else
print("So sad to hear :(")
end
end
while true do
displayMenu()
io.write("Select an Option: ")
local choice = tonumber(io.read())
if choice == 1 then
getTime()
elseif choice == 2 then
monoTime()
elseif choice == 3 then
uiPrompt()
elseif choice == 4 then
copas.addthread(
function ()
local total = 100
for i=1, total do
UI.progressBar(i, total)
copas.pause(0.1)
end
print()
end)
elseif choice==6 then
break
end
end
copas.loop()
ui.lua
UI={}
function UI.progressBar(current, total)
local widthOfBar = 50
local progress = math.floor((current / total) * widthOfBar)
local remaining = widthOfBar - progress
local bar = "[" .. string.rep("=", progress) .. string.rep(" ", remaining) .. "]"
io.write("\r" .. bar .. math.floor((current / total) * 100) .. "%") -- carriage return for progress bar to stay on the same line
io.flush()
end
copas.loop()
return UI
If anyone could explain and point out what i am doing wrong that would be great. One more thing, are there other better libraries for handling non-block functions Thanks!
there's a number of issue with the code you provided.
calling copas.loop()
, only makes sense if you first add 'work' to the scheduler (a task, timer, server socket)
io.read should not be used. Since it will block the process, without ever yielding to the Copas scheduler. Use system.readansi()
instead, and give it copas.pause
as the sleep method such that Copas can go of and do other work while this task is waiting.
for readansi
to work properly, the terminal must be set up properly, make it non-blocking for reading, and disable canonical mode (canonical is "line mode", it reads a line, until a user presses enter)
I added comments to your code, and fixed the above items:
local sys = require("system")
-- local UI = require("ui")
local copas = require("copas")
-- define the UI library in line here
local UI = {}
function UI.progressBar(current, total)
local widthOfBar = 50
local progress = math.floor((current / total) * widthOfBar)
local remaining = widthOfBar - progress
local bar = "[" .. string.rep("=", progress) .. string.rep(" ", remaining) .. "]"
io.write("\r" .. bar .. math.floor((current / total) * 100) .. "%") -- carriage return for progress bar to stay on the same line
io.flush()
end
function UI.prompt(message)
print(message .. " (y/n):")
--local response = io.read() -- io.read is blocking, use readansi instead
local response = sys.readansi(math.huge, copas.pause) -- use readansi, and pass a NON-blocking sleep function for use with Copas
if response == "y" then -- readansi only return 1 character
return true
elseif response == "n" then -- check for the other result as well
return false
else -- report an error and retry the prompt
print("Invalid input")
return UI.prompt(message)
end
end
-- end of UI library definition
local function displayMenu()
print("=============")
print("1. Check Time")
print("2. Get Mono Time")
print("3. Give Feedback")
print("4. Progress Bar Demo")
print("6. Exit")
print("=============")
end
local function getTime()
local time = math.floor(sys.gettime()) -- wrapped in math.floor to make it an integer
local date = os.date("Current Time: %Y-%m-%d %H:%M:%S", time)
print(date)
end
local function monoTime()
local response = sys.monotime()
print(response)
end
local function uiPrompt()
local response = UI.prompt("Do you like lua?")
if response == true then
print("Thats great!")
else
print("So sad to hear :(")
end
end
-- instead of just running this loop, wrap it in a Copas task.
-- when calling `copas.loop()` below, execution will start. Copas will
-- keep running until all tasks have exited.
copas.addthread(function () -- added
while true do
displayMenu()
io.write("Select an Option: ")
--local choice = tonumber(io.read()) -- io.read is blocking, nothing will ever run until the user presses enter. so we shouldn't use it.
local char = sys.readansi(math.huge, copas.pause) -- use readansi, and pass a NON-blocking sleep function for use with Copas
-- if no input is available, it will call copas.pause to wait a bit, and then try again, until a key was actually pressed,
-- or a timeout occurs (but since we pass "math.huge" here, it will wait forever).
-- copas.pause (when called by readansi) will not just sleep (and block the current thread), but will yield to the Copas scheduler, the scheduler will
-- then check if there are any other tasks that need to run, and if so, it will run them. Only when the sleep period
-- has passed, will the current task be resumed by the scheduler. The effects:
-- 1. from the perspective of the code here, it looks like the code blocks, it will not return until a key is pressed
-- 2. from the perspective of readansi, it will try in a loop, and sleep (copas pause) short periods in between.
-- 3. from the perspective of the Copas scheduler, it will not block, it will keep running other tasks, everytime readansi
-- calls copas.pause, and then resume the readansi task when the sleep period has passed.
local choice = tonumber(char) -- convert the string to an actual number
if choice == 1 then
getTime()
elseif choice == 2 then
monoTime()
elseif choice == 3 then
uiPrompt()
elseif choice == 4 then
copas.addthread(function ()
local total = 100
for i=1, total do
UI.progressBar(i, total)
copas.pause(0.1)
end
print()
end)
elseif choice == 6 then
break
end
end
end) -- added: end of `copas.addthread`
-- before starting the loop, we must configure the terminal
-- setup Windows console to handle ANSI processing
sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT)
-- setup Posix to disable canonical mode and echo
local of_attr = sys.tcgetattr(io.stdin)
sys.setnonblock(io.stdin, true)
sys.tcsetattr(io.stdin, sys.TCSANOW, {
lflag = of_attr.lflag - sys.L_ICANON - sys.L_ECHO, -- disable canonical mode and echo
})
copas.loop() -- this will exit once all tasks defined are finished
-- after exiting restore terminal configuration
-- windows
sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) - sys.COF_VIRTUAL_TERMINAL_PROCESSING)
sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_VIRTUAL_TERMINAL_INPUT)
-- posix
local of_attr = sys.tcgetattr(io.stdin)
sys.setnonblock(io.stdin, false)
sys.tcsetattr(io.stdin, sys.TCSANOW, {
lflag = of_attr.lflag + sys.L_ICANON + sys.L_ECHO,
})