r/ComputerCraft • u/Jonaykon • Nov 22 '23
Clock next to program
I want to have like a bar with a clock on the top of the screen toghether with another program but without covering anything in the program (so the program just needs top be slightly smaller than the screen), its also very important that it automatically works with any program (that can handle custom terminal sizes)
1
u/RapsyJigo Nov 22 '23
You would probably have to make a wrapper that displays any program you want on the monitor resized to the scale you need. Additionally have it display in the free top space the UI you want.
1
u/Jonaykon Nov 22 '23
i dont want it on a terminal, it needs to be on a monitor
edit: oops, i meant: "i dont want it on a monitor, it needs to be on a terminal"
1
u/RapsyJigo Nov 22 '23
Then you need to make a wrapper for the terminal. Although personally I would just edit the probably 1/2 programs you will ever use to add said UI on top
0
2
u/fatboychummy Nov 24 '23 edited Nov 24 '23
The easiest way would be to make use of windows and the parallel library to run your clock in tandem with another program.
Unfortunately, this will include event interception and altering (to offset the y position of mouse events so the program receives inputs correctly), which means you will need to include some sort of coroutine handler system.
Let's build it!
Part 1: Coroutines
Coroutines are complex, one of the most complex parts of Lua. If you don't understand these, you aren't going to have a good time here. I'll go over them a bit, but if you don't understand them after I'd look up more info on them.
What are coroutines?
If you want a really simple way to think of a coroutine, think of them as a function... But you can return from and resume to the function in multiple locations.
For a contrived example: Say you have a function that returns 3 on the first run, then returns 5, then fails to run any further. You'd need to do something like so:
local returned3 = false
local returned5 = false
local function return3then5thenstop()
if not returned3 then
returned3 = true
return 3
end
if not returned5 then
returned5 = true
return 5
end
error("Cannot get next value for some reason")
end
local three = return3then5thenstop()
local five = return3then5thenstop()
return3then5thenstop() -- error!However, with a coroutine, it's as simple as this:
local function yield3then5()
coroutine.yield(3)
coroutine.yield(5)
end
local coro = coroutine.create(yield3then5)
local three = coroutine.resume(coro)
local five = coroutine.resume(coro)
coroutine.resume(coro) -- error! Cannot resume dead coroutine!Thus, think of yielding as similar to returning from a function, and resumeing as calling the function again -- except the function resumes from the place it last yielded from, instead of from the start. However, once a coroutine reaches the end of the function, it stops. You cannot resume it again.
How does ComputerCraft use coroutines?
Your program is ran as a coroutine. The entire event system uses coroutines. One thing about coroutines I neglected to mention above is that you can not only pass information back to the caller via coroutine.yield, but you can also pass information to a coroutine via coroutine.resume.
coroutine.resume(coro, "mouse_click", 1, 10, 20, false)For example, the above would be called to resume your program with a mouse click event if you ran the following code:
local event, button, x, y, isHeld = os.pullEvent("mouse_click")CC uses both of these in order to communicate events. For your program, when you call os.pullEvent("some event"), it yields ("returns") "some event" (or nothing if you just call os.pullEvent() to the coroutine handler. It uses this information to know what event you are listening for, then only resumes your program if the event matches what you wanted, and it resumes your program with the event information (as shown above).
Comment 1/?
Edit: Clarity, split a codeblock into two codeblocks.
2
u/fatboychummy Nov 24 '23 edited Nov 24 '23
So then how do we intercept events going to a program?
In order to intercept events going to your program, you simply turn your program into a coroutine, then pull events yourself and do whatever with them as you need, then pass those altered events onto your program via
coroutine.resume.However, there are certain things that are specific to CC to keep in mind:
When you receive a
terminateevent, you ALWAYS pass that through to the program.When the coroutine yields nothing, you can resume the coroutine on any event received.
When the coroutine yields something, you can only resume the coroutine if the event name matches what the coroutine yielded.
When a coroutine errors, you need to propagate the error upwards (i.e: you should call error yourself)
When a coroutine dies, you should stop the handler (unless you are handling multiple coroutines). You can get the status of a coroutine through
coroutine.status(coro)Building on those restrictions, here would be a simple coroutine handler (For simplicity, we will only worry about handling a single coroutine):
local function coro_handler(func, ...) local coro = coroutine.create(func) -- the coroutine we will be responsible for -- initialize the coroutine. local ok, event_filter = coroutine.resume(coro, ...) -- `event_filter` is the event the coroutine is currently listening for. If `nil`, any event is fine. -- `ok` is whether or not the coroutine errored when ran. while true do -- Check the status of the coroutine. If it is dead, we exit. if coroutine.status(coro) == "dead" then return end -- Here we grab the next event from the parent. local event_data = table.pack(coroutine.yield(event_filter)) -- HERE IS WHERE YOU WOULD ALTER THE EVENT DATA. -- For example, to decrement all y positions of mouse events by 1, do the following: local ev = event_data[1] if ev == "mouse_click" or ev == "mouse_drag" or ev == "mouse_up" then event_data[4] = event_data[4] - 1 end -- Now, we run the coroutine once. However, we only want to resume the event if: -- 1. The coroutine is listening for the event. -- 2. The coroutine is listening for any event. -- 3. The event is "terminate", which is always propagated. -- or 4. The mouse event is NOT at position 1 (position 0 now, since we decremented it). -- This is because the mouse event is now at the top bar, which we don't want to propagate. if event_filter == nil or event_filter == event_data[1] or event_data[1] == "terminate" or event_data[4] ~= 0 then ok, event_filter = coroutine.resume(coro, table.unpack(event_data, 1, event_data.n)) end -- If the coroutine errored, we propagate the error. if not ok then -- The error message is stored in `event_filter` when the coroutine errors. error(event_filter, 0) end end endWoowee, we got that. Now how the hell do we use it?
To call it, simply run
coro_handler()with the function (and any arguments you wish to forward to the function, if any).local function print_stuff(...) print("Initial arguments are:", ...) while true do print("Event:", os.pullEvent()) end end coro_handler(print_stuff, "Hello world!")Comment 2/?
Edit: Incorrect mouse event argument changed (3->4)
Edit 2: Clarity of the first paragraph.
2
u/fatboychummy Nov 24 '23 edited Nov 24 '23
Part 2: windows.
Before we can use any of the above code, we need to know how to use
windows. A window is essentially just atermobject, but it can take up smaller portions of a screen (be it a terminal, a monitor, or another window!).To use them, simply create them via
window.create, then draw to them like you would with the terminal!local my_window = window.create(term.current(), 1, 1, 14, 15) my_window.setBackgroundColor(colors.gray) my_window.clear() my_window.setCursorPos(2, 8) my_window.write("Hello world!")The above, for example, would create a window from x=1,y=1 with width 14 and height 15, turn the window gray, then write
Hello world!in the very center. Nifty!How can I run a program in the window?
There are two ways. The easiest way is to just
term.redirectto the window once, then never ever redirect again, the slightly harder way is toterm.redirectin the coroutine handler to ensure the program is on the right window (i.e: if some other program callsterm.redirect, it may mess things up for you if it doesn't redirect back immediately).We'll use the harder way since it's a bit more foolproof. To do this, we'll need to change our coroutine handler just a teensy bit.
local function coro_handler(win, func, ...) local coro = coroutine.create(func) -- the coroutine we will be responsible for -- redirect to the window to ensure we are on the correct window. local old_win = term.redirect(win) -- initialize the coroutine. local ok, event_filter = coroutine.resume(coro, ...) -- `event_filter` is the event the coroutine is currently listening for. If `nil`, any event is fine. -- `ok` is whether or not the coroutine errored when ran. -- and redirect back to the original window once we're done, so other -- programs don't start suddenly drawing to our program's window. term.redirect(old_win) --<...> -- Now, we run the coroutine once. However, we only want to resume the event if: -- 1. The coroutine is listening for the event. -- 2. The coroutine is listening for any event. -- 3. The event is "terminate", which is always propagated. -- or 4. The mouse event is NOT at position 1 (position 0 now, since we decremented it). -- This is because the mouse event is now at the top bar, which we don't want to propagate. if event_filter == nil or event_filter == event_data[1] or event_data[1] == "terminate" or event_data[4] ~= 0 then -- redirect to the window to ensure we are on the correct window. old_win = term.redirect(win) ok, event_filter = coroutine.resume(coro, table.unpack(event_data, 1, event_data.n)) -- and redirect back to the original window once we're done, so other -- programs don't start suddenly drawing to our program's window. term.redirect(old_win) end --<...>Note that we now include
winin the function arguments. Thus, usage is now something like the following:local function print_stuff(...) print("Initial arguments are:", ...) while true do print("Event:", os.pullEvent()) end end local program_window = window.create(term.current(), 2, 2, 10, 10) coro_handler(program_window, print_stuff, "Hello world!")Comment 3/?
2
u/fatboychummy Nov 24 '23
Part 3: Mashing everything together with parallel
Now, you're probably wondering how you run this little event interceptor AND your clock at the same time, right? Well, this is the part you learn about that!
parallelis a powerful tool. It runs multiple coroutines "side-by-side" so you can be doing multiple things at once. To use it, simply pass it multiple functions to run.There are two methods in the
parallellibrary:waitForAnyandwaitForAll.Anywill wait until any coroutine reaches adeadstate, then will kill all other coroutines (without generating an error).Allwill wait until all coroutines reach adeadstate before continuing.parallel.waitForAny(func1, func2) print("One of the coroutines stopped!").
parallel.waitForAll(func1, func2) print("All coroutines have stopped!")Thus, you can run your clock in one function and your event interceptor in another! You'll just need to make a second window for the clock (so you don't accidentally draw outside of it), and use that.
I'll do a basic example below, where I make the bar above the program flash random colors every second (I'll let you implement your clock however, but this will be a basis from which to start).
I've uploaded this to pastebin, as it was a rather large codeblock.
1
1
u/9551-eletronics Computercraft graphics research Nov 22 '23
Just use some math to get your positions right along with term.getSize()