This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
malianx's profile picture

Coroutines and Read()

Started by malianx, 16 April 2012 - 06:49 PM
malianx #1
Posted 16 April 2012 - 08:49 PM
Is it possible to use a coroutine to keep a loop going (clock or timer) while waiting for input from the user with Read()?
OminousPenguin #2
Posted 16 April 2012 - 09:49 PM
Disclaimer: I started with Lua three days ago.

Lua coroutines are not system threads - only one coroutine is running at any one time so you can't do actual simultaneous processing.

However, it would probably be possible to simulate read() by listening to key events and maintaining input in a buffer. This way you could handle timer events between keystrokes.
Cloudy #3
Posted 16 April 2012 - 09:55 PM
What you suggest is possible, regardless of the above fact. Try using the parallel.waitForAll(function1, function2) command in the parallel API. You would supply it with two separate functions, each of which has a loop inside. One which keeps a timer going, the other that checks for input using "read" and does something based on that input.
malianx #4
Posted 16 April 2012 - 10:19 PM
What you suggest is possible, regardless of the above fact. Try using the parallel.waitForAll(function1, function2) command in the parallel API. You would supply it with two separate functions, each of which has a loop inside. One which keeps a timer going, the other that checks for input using "read" and does something based on that input.

I'd like to have a running clock going, during a menu that takes userInput.

How can I have it wait for input, but not halt the other function while it does? Not what I'm going to end up with, but this is the effect I want:


local function Time()
  while true do
    --term.clear()
    --term.setCursorPos(1,1)
    print(textutils.formatTime(os.time(), false))
  sleep(0.25)
  end
end
local function Input()
  while true do
    print("Waiting for Input")
userInput = read()
if userInput ~= nill then
	  print("Got Input")
   break
end
  end
end
parallel.waitForAll( Time(), Input()  )
OminousPenguin #5
Posted 16 April 2012 - 10:49 PM
What you suggest is possible, regardless of the above fact. Try using the parallel.waitForAll(function1, function2) command in the parallel API. You would supply it with two separate functions, each of which has a loop inside. One which keeps a timer going, the other that checks for input using "read" and does something based on that input.

Wouldn't read() block and not yield until the user entered something?

I'd like to have a running clock going, during a menu that takes userInput.

How can I have it wait for input, but not halt the other function while it does? Not what I'm going to end up with, but this is the effect I want:

I'll see if I can put something together for you.

Edit: Ok so I'm basically reimplementing read. brb
Cloudy #6
Posted 17 April 2012 - 12:44 AM
The parallel API uses coroutines. Coroutines are cooperative multitasking - provided you pass control back to each thread after getting the actual events, it works fine - and that's what the parallel API does.

Look into Lua coroutines. Sure, reimplementing read would work (copy the read from the bios and alter that, rather than doing it from scratch) however, it is in most cases easier just to use the parallel API or handle the coroutines manually.
malianx #7
Posted 17 April 2012 - 01:06 AM
The parallel API uses coroutines. Coroutines are cooperative multitasking - provided you pass control back to each thread after getting the actual events, it works fine - and that's what the parallel API does.

Look into Lua coroutines. Sure, reimplementing read would work (copy the read from the bios and alter that, rather than doing it from scratch) however, it is in most cases easier just to use the parallel API or handle the coroutines manually.

Can you suggest a method for making the coroutine yield and resume even though read, and event methods stop execution all together? That's what's holding me up.
OminousPenguin #8
Posted 17 April 2012 - 01:26 AM
Ok after about 40 mins playing around I've got this. Damn Cloudy, copying read from bios would probably have been a good idea!

My code:
Spoiler


local line = 1
local cursorPos = 1
local input = ""
local length = 0
local keys = {}
keys[14] = function() -- Backspace
  if cursorPos~=1 then
   input = string.sub(input,0,cursorPos-2)..(string.sub(input,cursorPos) or "")
   cursorPos = cursorPos - 1
   length = length - 1
   term.setCursorPos(cursorPos+2,line)
   term.write(string.sub(input,cursorPos).." ")
  end
end
keys[203] = function() -- Left Arrow
  if cursorPos~=1 then cursorPos = cursorPos - 1 end
end
keys[205] = function() -- Right Arrow
  if cursorPos~=length+1 then cursorPos = cursorPos + 1 end
end
keys[28] = function() -- Enter
  -- replace this with whatever you want to do with your input.
  term.setCursorPos(cursorPos+2,line+1)
  term.write(" ")
  term.setCursorPos(1,line+1)
  shell.run(input)
  _,line = term.getCursorPos()
  input=""
  length=0
  cursorPos=1
  term.setCursorPos(1,line)
  term.write(">")
  term.setCursorPos(3,line+1)
  term.write("^")
  timer = os.startTimer(0.25)
end

term.clear()
term.setCursorPos(1,1)
term.write(">")
term.setCursorPos(3,2)
term.write("^")
local timer = os.startTimer(0.25)
while true do
local eventType, p1 = os.pullEvent()
if eventType=="char" then
  term.setCursorPos(cursorPos+2,line+1)
  term.write(" ")
  if cursorPos-1==length then
   input = input..p1
   term.setCursorPos(cursorPos+2,line)
   term.write(p1)
  elseif cursorPos==1 then
   input = p1..input
  else
   input = string.sub(input,0,cursorPos-1)..p1..string.sub(input,cursorPos)
   term.setCursorPos(cursorPos+2,line)
   term.write(string.sub(input,cursorPos))
  end
  cursorPos = cursorPos + 1
  length = length + 1
  if cursorPos<length then
   term.setCursorPos(cursorPos+2,line)
   print(string.sub(input,cursorPos))
  end
  term.setCursorPos(cursorPos+2,line+1)
  print("^")
elseif eventType=="key" then
  local f = keys[p1]
  if type(f) == "function" then
   term.setCursorPos(cursorPos+2,line+1)
   term.write(" ")
   f()
  end
elseif eventType=="timer" and p1==timer then
  term.setCursorPos(42,1)
  print("		")
  term.setCursorPos(42,1)
  print(textutils.formatTime(os.time(), false))
  timer = os.startTimer(0.25)
end
end

Edit: Well that was 40 mins wasted. Modifying read from bios took 8 lines and 2 minutes.
I've marked the two places where I've added code with – OminousPenguin

Modified read from bios:
Spoiler

local function read( _sReplaceChar, _tHistory )
term.setCursorBlink( true )
local timer = os.startTimer(0.25) -- OminousPenguin
	local sLine = ""
local nHistoryPos = nil
local nPos = 0
	if _sReplaceChar then
  _sReplaceChar = string.sub( _sReplaceChar, 1, 1 )
end

local w, h = term.getSize()
local sx, sy = term.getCursorPos()
local function redraw()
  local nScroll = 0
  if sx + nPos >= w then
   nScroll = (sx + nPos) - w
  end
  
  term.setCursorPos( sx, sy )
  term.write( string.rep(" ", w - sx + 1) )
  term.setCursorPos( sx, sy )
  if _sReplaceChar then
   term.write( string.rep(_sReplaceChar, string.len(sLine) - nScroll) )
  else
   term.write( string.sub( sLine, nScroll + 1 ) )
  end
  term.setCursorPos( sx + nPos - nScroll, sy )
end

while true do
  local sEvent, param = os.pullEvent()
  if sEvent == "char" then
   sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
   nPos = nPos + 1
   redraw()
  
  elseif sEvent == "key" then
	  if param == 28 then
	-- Enter
	break
  
   elseif param == 203 then
	-- Left
	if nPos > 0 then
	 nPos = nPos - 1
	 redraw()
	end
  
   elseif param == 205 then
	-- Right  
	if nPos < string.len(sLine) then
	 nPos = nPos + 1
	 redraw()
	end
  
   elseif param == 200 or param == 208 then
				-- Up or down
	if _tHistory then
	 if param == 200 then
	  -- Up
	  if nHistoryPos == nil then
	   if #_tHistory > 0 then
		nHistoryPos = #_tHistory
	   end
	  elseif nHistoryPos > 1 then
	   nHistoryPos = nHistoryPos - 1
	  end
	 else
	  -- Down
	  if nHistoryPos == #_tHistory then
	   nHistoryPos = nil
	  elseif nHistoryPos ~= nil then
	   nHistoryPos = nHistoryPos + 1
	  end	
	 end
	
	 if nHistoryPos then
					 sLine = _tHistory[nHistoryPos]
					 nPos = string.len( sLine )
					else
	  sLine = ""
	  nPos = 0
	 end
	 redraw()
				end
   elseif param == 14 then
	-- Backspace
	if nPos > 0 then
	 sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 )
	 nPos = nPos - 1	
	 redraw()
	end
   end
  elseif sEvent == "timer" and param==timer then  -- OminousPenguin
   term.setCursorPos(42,1)
   print("		")
   term.setCursorPos(42,1)
   print(textutils.formatTime(os.time(), false))
   timer = os.startTimer(0.25)
   redraw()
  end
end

term.setCursorBlink( false )
term.setCursorPos( w + 1, sy )
print()

return sLine
end
Cloudy #9
Posted 17 April 2012 - 03:33 AM
This is a quick coroutine example. I'm typing on my iPhone while in bed so expect errors in syntax and spelling :-p

function getInput()
  local input
  while true do
    input = io.read()
    if input == "blah" then
      --do stuff
    end
  end
end

function updateTimer()
  local timer = os.startTimer(10)
  local event, param
  while true do
    event, param = os.pullEvent()
    if event == "timer" and param == timer then
      timer = os.startTimer(10)
      -- do stuff
    end
  end
end

while true do
  parralel.waitForAll(getInput, updateTimer)
end


Untested as on phone but should work. Off topic: after writing that I got a 404 on my wifi. Thank god for copy and paste.
OminousPenguin #10
Posted 17 April 2012 - 03:46 AM
Tested - works:


function getInput()
  local input
  while true do
    input = io.read()
    if input == "blah" then
      --do stuff
    end
  end
end
function updateTimer()
  local timer = os.startTimer(3)
  local event, param
local x, y
  while true do
    event, param = os.pullEvent()
    if event == "timer" and param == timer then
       x, y = term.getCursorPos()
       term.setCursorPos(42,1)
       timer = os.startTimer(3)
       term.setCursorPos(42,1)
       print("		")
       term.setCursorPos(42,1)
       print(textutils.formatTime(os.time(), false))
       term.setCursorPos(x,y)
     end
  end
end
while true do
  parallel.waitForAll(getInput, updateTimer)
end

I was thinking read() blocks but of course it calls pullEvent which calls pullEventRaw which yields.
OminousPenguin #11
Posted 17 April 2012 - 07:09 PM
Far better to use this:


timer = os.startTimer(0.25) -- change the time to whatever you want

os.pullEvent = function(filter)
  local evt, p1, p2, p3, p4, p5 = os.pullEventRaw(filter)
  if evt == "terminate" then
    print( "Terminated" )
    error()
  elseif evt == "timer" and p1 == timer then
    x,y = term.getCursorPos()
    term.setCursorPos(42,1)
    print("        ")
    term.setCursorPos(42,1)
    print(textutils.formatTime(os.time(), false))
    timer = os.startTimer(0.25)
    term.setCursorPos(x,y)
  end
  return evt, p1, p2, p3, p4, p5
end
Cloudy #12
Posted 17 April 2012 - 07:41 PM
I dunno - I'm always uneasy about overwriting the os.pullEvent inside your program. Means you need to do cleanup afterwards to ensure you don't alter any programs running afterwards.
OminousPenguin #13
Posted 17 April 2012 - 09:11 PM
I can see there would be cases where that would be a concern, but don't think this is one. If you use a (hopefully) unique name for your global timer variable, then it's unlikely to interfere with another program.

ominousPenguinTimer = os.startTimer…..

… and p1==ominousPenguinTimer then


Do you agree?
Cloudy #14
Posted 17 April 2012 - 10:06 PM
I mean overwriting the os.pullEvent. Nothing to do with the timer. Regardless, it is just personal choice.
malianx #15
Posted 17 April 2012 - 10:59 PM
Far better to use this:


timer = os.startTimer(0.25) -- change the time to whatever you want

os.pullEvent = function(filter)
  local evt, p1, p2, p3, p4, p5 = os.pullEventRaw(filter)
  if evt == "terminate" then
	print( "Terminated" )
	error()
  elseif evt == "timer" and p1 == timer then
	x,y = term.getCursorPos()
	term.setCursorPos(42,1)
	print("		")
	term.setCursorPos(42,1)
	print(textutils.formatTime(os.time(), false))
	timer = os.startTimer(0.25)
	term.setCursorPos(x,y)
  end
  return evt, p1, p2, p3, p4, p5
end

Brilliant, thank you.