terminalluacopas

how to implement the event loop in copas and how does it work?


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!


Solution

  • there's a number of issue with the code you provided.

    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,
    })