Latest update: I've added a new gui API, ggui.
The current release has only a limited set of gui elements - labels, buttons, text fields, and graphic elements for displaying images created in paint. However, I've tried to make these core elements as robust as possible. Text fields support all the keys you would expect, remember the cursor position, and even support standard gui form behavior where enter moves focus to the next control and, if it's a button, activates it. Graphic elements can slice a subsection out of a source image, allowing variable-sized images, and letting multiple graphic elements be stored in a single paint file. All elements use styles, which can be overwritten to change text alignment, image alignment, foreground and background colors, etc., with support for automatically detecting and switching styles for color or b/w computers. All in all, a pretty solid foundation, I think, but it is a WIP. Documentation will come later, for now, you'll have to settle for the two sample programs, you can get them and the api on pastebin using this handy installer pastebin get fq8SinNB.
More examples coming soon, several of them are already written but I want to polish and test them a bit more before I post!
Decided to stop making more threads and pull all my general computer APIs into a single thread, since I've been extending them to interoperate more and more. You can still use all of them independently as well, of course.
Overview
The following are the APIs in this thread:
goroutine
A very handy API for more advanced coroutine applications. Sometimes parallel is plenty for a job, but sometimes you need a bit more freedom. Goroutine gives you that. Used within a single application, it gives you the ability to dynamically spawn new coroutines on the fly, assign terminal redirects to specific coroutines, kill a coroutine from another, assign events to be received only by specific coroutines, and to wait not just for a given type of event, but an event with a given set of parameters. If run properly from startup, it can do all that and more, allowing programs to spawn background routines that continue after the program that spawned them exits.
redirect
The buffer api creates buffer objects that can be used with term.redirect. Buffers can be any size, they support color or black and white, and you can easily copy a specific area of the buffer to the screen. Buffers can also be made "active," which will make them "write through" to the terminal. In this mode, you can create multiple buffers, each mapped to a different area of the screen, each of those areas having it's own cursor position and color settings, able to be cleared and scrolled without affecting the rest of the screen, and allowing use of print() with the expected word-wrapping behavior.
ctrlkeys
This little guy provides a coroutine you can run in the background - either with goroutines, or just using parallel.waitForAny - that detects certain patterns of key events in order to - quite accurately - generate new "ctrl_key" events when a user holds down control and then presses certain keys. Unfortunately, it only works with the "typable" keys, that generate char events - a-z, 1-0, punctuation, etc - and not with the special keys like enter, backspace, or the arrow keys.
ggui (BETA)
The latest addition, still a work in progress, is a basic gui library. My hope is to make it easy to use, with helpful error messages and a high tolerance for doing the wrong thing.
Now for the detailed info on each one!
goroutine
Spoiler
Can be run "globally" if set up in startup, or can be set up by an individual program. Start it up by calling goroutine.run and passing it your "main" function, which will become the master coroutine.All variations of the "spawn" function cause a coroutine to be immediately run once - until it's first pullEvent or other yield - before returning to the code that called spawn. Coroutines must be given unique names when spawned, or the spawn…() functions will return nil.
Functions
Spoiler
goroutine.run(function)The first function you'll call, just pass it your "main" function and spawn any additional coroutines from it.
goroutine.launch()
Intended to be called only from startup, and only with proper checking that goroutine has not already been launched!! (see the example under "Usage" below) Calling this from startup without proper checking will put your computer in an infinite loop, requiring a boot floppy to recover it!
goroutine.spawn(name,func,…)
The generic spawn function, creates a new coroutine with the specified name, running the function "func", and runs it once, then continues executing after the call to "spawn." The new coroutine will be a child of the coroutine that called spawn, and will end when that coroutine ends, if it hasn't ended already. Any extra parameters after "func" will be arguments passed to func. The new coroutine will use it's parent's term redirect, with the top-level coroutine always using term.native as it's redirect. Returns a table describing the goroutine, or nil and an error message if it failed.
goroutine.spawnWithRedirect(name,func,redirect,…)
Like spawn, but allows you to specify a redirect target for this coroutine to use. term will be redirected to this target every time the coroutine resumes. This would allow, for example, making one coroutine always output to a monitor peripheral, without having to manually redirect and restore the terminal.
goroutine.spawnPeer(name,function,…)
Just like spawn, except instead of having the current coroutine as a parent, it shares the current coroutine's parent. This means it will not end when the current coroutine ends, but instead keep running as long as the current coroutine's parent runs.
goroutine.spawnPeerWithRedirect(name,function,redirect,…)
Combination of spawnPeer and spawnWithRedirect, spawns as a peer with the specified redirect.
goroutine.spawnBackground(name,function,…)
Another variant, spawnBackground creates a coroutine has the top-level routine as it's parent. If goroutines is run only from in your program, then this means it will be a child of main, and will still exit if main exits. However, if goroutine is launched properly in your startup file, then the coroutine will be a child of the special "root" coroutine, and will continue running even after the program exits. Background coroutines are all redirected to a special "nil" redirect, so calls to print(), write() or any of the term methods will have no effect.
goroutine.kill(coName)
Kills the coroutine named coName. Coroutines cannot use this function to kill themselves, and should exit by either returning from their top function or by calling error(). Returns true if successful, otherwise false and an error message.
goroutine.assignEvent(coName,sEvent)
Assigns all events of type sEvent to be sent exclusively to the coroutine named coName. If it exists and is alive, that coroutine will be sent any events of this type that arrive. The coroutine may choose to pass on the event by calling goroutine.passEvent, but if it does not do so, then no other coroutines will see this event at all. Can only be called by the main coroutine, as a basic security precaution, so any event assignments your program needs should be made in main. Event assignments are automatically released if the coroutine they are assigned to exits or errors.
goroutine.passEvent(coName)
Passes the currently-handled event to the named coroutine. passEvent has no effect unless it is called from a coroutine whose most-recent event was assigned to it by assignEvent, or passed to it by another coroutine that it was assigned to calling passEvent. The coroutine the event is passed to is not run immediately, but will run and receive the event as soon as the current coroutine exits. If coName is nil or "", the event will be passed to all waiting coroutines as if it were not assigned.
goroutine.releaseEvent(event)
Releases a previously-assigned event, causing it to be sent to all waiting coroutines normally again. Can only be called by the main coroutine, or by the coroutine to which the event is assigned.
goroutine.waitForEvent(event, …)
behaves like os.pullEvent, but accepts more than one parameter. All passed parameters will be used as additional filters, matched to the corresponding event parameters, and the function will exit only when all specified arguments match the event's arguments. Parameters can be "skipped" by passing nil, allowing filtering to ignore the value of some parameters.
Example:
goroutine.waitForEvent("key",keys.enter) --would wait for the enter key to be pressed
goroutine.waitForEvent("rednet_message",40) -- would wait for a rednet message from computer with id 40
goroutine.waitForEvent("redent_message",nil,"ping") -- waits for a rednet message "ping" from any computer
goroutine.findNamedCoroutine(coName)
Finds and returns the coroutine object, as returned by the spawn functions, with the specified name.
goroutine.running()
returns the coroutine object for the code that called running()
goroutine.list()
Returns a table containing an array of names of all active coroutines.
Events
Spoiler
The goroutine api generates the following events, which can be handled by your program:"coroutine_start", coName
Sent whenever a new coroutine is spawned.
"coroutine_end", coName
Sent when a coroutine ends for any reason
"coroutine_error", coName, err
Sent when a coroutine exits unexpectedly due to an error() call. err is the text of the error message generated.
coroutine_error is always followed by a coroutine_end message.
Usage
Spoiler
Goroutine can be used in two ways. First, it can be launched in startup, using goroutine.launch(), so that the main shell itself is a coroutine it manages. In this setup, programs run from the shell can use spawnBackground to create coroutines that will continue running even after those programs exit. In fact, such background routines will may continue running even if the shell itself exits, causing the computer to appear to "hang." This is expected but undesired behavior that may be addressed in a future version. It should still respond to ctrl+r or ctrl+s to reboot or shutdown.Example code to properly launch goroutines from a startup file:
Spoiler
--this should always be the very first thing in your startup file.
--any other code before these two lines will run twice, which may have unintended effects.
os.loadAPI("apis/goroutine") --change the path as necessary to where you've stored goroutine
goroutine.launch()
--the rest of your startup code goes here
pastebin: Cifjwb0ZThe second way is to simply launch goroutine from within a program, using goroutine.run(). In this way, you get most of the benefits of the goroutine api, but you will not be able to leave a coroutine running after your program exits (though you could spawn a new shell in a coroutine within your program, making it appear that your program has exited). Programs written this way should not, in general, be affected if goroutine in fact was installed as the top-level coroutine, so unless your program absolutely requires a startup-level launch of goroutines, it should be written in this way for maximum compatibility.
Example goroutine program structure:
Spoiler
--first check if goroutine is loaded and if not load it
if not goroutine then
local ok= os.loadAPI("api/goroutine") --change this to the right path to where you saved it
if not ok then
error("program requires goroutine api to run!")
end
end
--any variables you want to be visible to all coroutines should be declared at the top
local myVariable=1
--declare any coroutine functions next
local function myCoroutine()
--each coroutine will usualy be built around an event loop, like this one.
--in this example it pulls rednet messages, but it could be any event type, or even multiple types.
while true do
local _, sender, message, dist = os.pullEvent("rednet_message")
--do stuff with the message here...
print("rednet message from "..sender..": "..message)
end
end
--your main function, this will be the top-level coroutine. The function name can be whatever you want.
local function main()
--often coroutines will be spawned right at the start, to run in the background during the whole program
--they can be spawned at any time and from any coroutine, though. In fact, if you ONLY spawn coroutines
--here, you may not need goroutine at all, the parallel functions might do the job fine.
goroutine.spawn("rednetListener", myCoroutine)
--main is ultimately a coroutine like any other, and usually you'll have an program loop of some kind
while true do
local _, key=os.pullEvent("key")
if key==keys.backspace then
--break out of the loop
break
end
end
--once this guy exits, his children will exit to, so any cleanup can go here
end
--this launches your main coroutine in a way that will work whether goroutines is launched from startup or not
goroutine.run(main)
--code after this will run only after your coroutines all exit.
pastebin : jRXVC1AZShell Utilities
Spoiler
These programs serve as example programs, but also act as a handy set of basic utility applications for use from the CraftOS shell when launching goroutines from startup. These programs are highly recommended!ps
lists the actively running processes (coroutines). The "ids" provided are really just indices, and will change when processes are started or stopped.
Spoiler
local list=goroutine.list()
print("Active coroutines:")
for i=1,#list do
print(string.format(" %2d : ",i)..list[i])
end
bg
This lets you run a program in the background. It's output will be compeltely suppressed, so it will not affect the display. It is intended to use to launch programs created to run in the background, for example, waiting for and responding to special rednet commands.
Spoiler
if not goroutine then
if not os.loadAPI("goroutine") then
print("goroutine API is required!")
return
end
end
--args
local args={...}
local function usage()
print("usage:\nbg <program> [args...]")
error()
end
if #args<1 then
usage()
end
local program=args[1]
program=shell.resolveProgram(program)
if not program then
print("Couldn't find program '"..program.."'!")
return
end
local name=program
local index=0
while goroutine.findNamedCoroutine(name) do
index=index+1
name=program..index
end
if goroutine.isActive() then
--just spawn!
print("Spawning '"..program.."' in background.")
goroutine.spawnBackground(name, shell.run, ...)
else
--not running we have to run ourselves.
local function main(...)
goroutine.spawnBackground(name, shell.run, ...)
print("Launching new local shell. Exit this shell to terminate monitor program!")
shell.run("shell")
end
goroutine.run(main,...)
end
kill
This program can be used from the shell to terminate a background process. It takes one argument, the index or name of the coroutine to be killed.
Spoiler
local l=goroutine.list()
local args={...}
if args[1]==nil then
print("usage:\nkill <procname>\nkill <procnumber>")
end
local procnum=tonumber(args[1])
if procnum and procnum>0 and procnum<=#l then
print("Killing coroutine '"..l[procnum].."'")
goroutine.kill(l[procnum])
return
end
for i=1,#l do
if l[i]==args[1] then
print("Killing coroutine '"..l[i].."'")
goroutine.kill(l[i])
return
end
end
print("That coroutine does not exist")
monitor
This program is like the vanilla monitor program, except that it spawns the program as a coroutine and immediately returns to a shell prompt.
Spoiler
if not goroutine then
if not os.loadAPI("goroutine") then
print("goroutine API is required!")
return
end
end
--args
local args={...}
local function usage()
print("usage:\nmonitor <side> <program> [args...]")
error()
end
if #args<2 then
usage()
end
local side=args[1]
local program=args[2]
local progArgs={select(2,...)}
if peripheral.getType(side)~="monitor" then
print("There is not a monitor on side '"..side.."'!")
return
end
program=shell.resolveProgram(program)
if not program then
print("Couldn't find program '"..program.."'!")
return
end
local function runOnMonitor()
local co=coroutine.create(shell.run)
local event=progArgs
local filter={}
while true do
local ok, err=coroutine.resume(co,unpack(event))
if not ok then
error(err)
end
event = { os.pullEventRaw() }
--transform monitor_touch events to this monitor
if event[1]=="monitor_touch" and event[2]==side then
event[1]="monitor_click"
event[2]=1
end
end
end
if goroutine.isActive() then
--just spawn!
print("Spawning "..program.." on side "..side)
goroutine.spawnWithRedirect(side..":"..program, runOnMonitor, peripheral.wrap(side))
else
--not running we have to run ourselves.
local function main()
goroutine.spawnWithRedirect(side..":"..program, runOnMonitor, peripheral.wrap(side))
print("Launching new local shell. Exit this shell to terminate monitor program!")
shell.run("shell")
end
end
Source
pastebin
pastebin get SUW8cq9j goroutine
redirect
Spoiler
The redirect api has only one directly-called function, createRedirectBuffer, which creates and returns a new redirect buffer object, but that object has all the methods necessary to be used as a redirect target on basic and advanced computers, as well as a few extra methods for managing the buffer and drawing it's contents to the screen.There are two ways to get the contents of a buffer to the screen. The first is explicitly calling buffer.blit, which allows you to copy all or part of the buffer to some position on the screen. The second us with the buffer.makeActive method, which allows you to map the buffer to some portion of the screen, which will cause anything written to the buffer to immediately draw to that area of the screen as well. The first is useful for implementing a form of back buffering, or for allowing switching between two or more screens of data without having to rebuild those screens each time you switch. The second is useful for splitting the screen into smaller areas, each of which can be written to independently. Each of these buffered areas would have it's own independent cursor position and active colors, as well as scrolling and clearing without affecting the rest of the screen.
Redirect buffers can certainly be used independently, but they're at their most when combined with goroutines. goroutines has built-in support for redirect targets (any redirect targets, not just those created by the redirect api), and by combining the two you can easily do things like create a chat application that has one coroutine reading input, mapped to a single line buffer at the bottom of the screen, while another coroutine using a buffer fitting the rest of the screen prints the scrolling chat history.
A note about the buffer objects: in order to be compatible with term.redirect, the buffer object methods are wrapped to supply the buffer object as a parameter automatically, so despite being lua objects, you call their methods with "." instead of ":"
Functions
Spoiler
API FunctionsSpoiler
redirect.createRedirectBuffer(width,height,fg, bg, isColor)Creates a new redirect buffer, of the specified width and height. fg and bg are optional parameters specifying the text (fg) and background (bg) colors for the buffer, which will be used to fill the new buffer as well as being the initial color for calls to buffer.write(). isColor is optional as well, and is a boolean to specify if the buffer should be color or not. Defaults to whatever the terminal running the program is.
examples:
--creates a full-screen buffer, with the default colors (white on black).
myBuffer=redirect.createRedirectBuffer(51,19)
--creates a 20 by 10 buffer, with a blue background and yellow text. Forces the buffer to be color even if this computer is not.
myBuffer=redirect.createRedirectBuffer(51,19,colors.yellow,colors.blue,true)
Buffer Object Methods
Spoiler
these are called on the object returned by createRedirectBufferIn addition to these methods, they have all the standard methods of any terminal redirect object: write, scroll, setCursorPos, isColor, getCursorPos, setTextColor, setBackgroundColor, setCursorBlink, clear, and clearLine.
buffer.resize(width,height)
Resizes the buffer without destroying it's contents. If the new size is smaller, text past the new edges will disappear. If it is larger, the new area will be whitespace. Works basically like resizing arrays of monitor peripherals by placing more or breaking some while they are in use. Note that if a buffer is made smaller, and then enlarged back to original size, the text that had disappeared will still be present, if the lines have not scrolled off the screen and clear() or clearLine() have not been called.
buffer.blit(sourceX, sourceY, destX, destY, width, height)
Blits, or copies, part or all of the buffer to the active terminal object. Note that where this draws is controlled by term.redirect, so if you call blit() while you have redirected the terminal to this buffer, it will copy to itself, which may not be what you want - though it can be useful!
With no parameters, using all defaults, this buffer.blit() will copy the entire screen.
parameters:
sourceX and sourceY define the position in the buffer to use as the top-left corner of the copied area. Defaults to 1,1 if not specified.
destX and destY define the position on the screen (or current redirect) to copy to. Default to 1,1.
width, height are the width and height of the area to copy. They default to the dimensions of the buffer.
buffer.makeActive(scrX,scrY)
Makes this buffer "active," meaning any changes to the buffer will immediately be presented to the screen. Unlike blit, an active buffer draws directly to term.native, ignoring the current redirect and going straight to the screen. scrX and scrY are optional, defaulting to 1,1, and specify the position on the screen that the top-left corner of the buffer will write to.
Buffer Object Data
Spoiler
In addition to methods there are variables stored in the buffer objects that can be accessed directly to get information about the buffer.buffer.width, buffer.height
the width and height of the buffer
buffer.curX, buffer.curY
the cursor position on the buffer (same as returned by buffer.getCursorPos())
buffer.cursorBlink
boolean indicating if the cursor is blinking or not. Same value passed in the last call to buffer.setCursorBlink
buffer.textColor, buffer.backgroundColor
the current text and background color of the buffer
Raw buffer access
In addition to these members, the buffer object can be indexed as a two-dimensional array to access the characters and colors at every character position in the buffer.
buffer[y][x].char the character at x,y
buffer[y][x].backgroundColor the background color at x, y
buffer[y][x].textColor the foreground color at x, y
It is also possible to draw by directly modifying this array, but drawing this way is not recommended when also using buffer.makeActive, as changes will not be detected or drawn to the screen automatically. This method works fine if you are using buffer.blit() to draw changes to the screen.
Example Programs
Spoiler
Example using buffers alone are coming, as soon as I have time and figure out what those examples ought to be.Source
http://pastebin.com/fU9Kj9zr
pastebin get fU9Kj9zr redirect
ctrlkeys
Spoiler
When loaded and launched as a background coroutine, either with goroutines or parallel.waitForAny, will generate new ctrl_key events for many common keyboard shortcuts. It cannot generate all possible ctrl+key combinations, only those where the key is one that can send a "char" event, such as a-z, 0-9, and any other punctuation or symbol keys. Due to the way ctrl-keys are detected, it can't generate ctrl+tab, ctrl+enter, ctrl+arrow, or any other special key combination events. There is absolutely nothing I can do about this, believe me, if it was possible I would've implemented them as well!API Functions
Spoiler
ctrlkeys.waitForAny(…)ctrlkeys.waitForAll(…)
These work exactly like parallel.waitForAny and parallel.waitForAll, except that all the functions will receive ctrl_key events. For info on using them, see the wiki documentation for the Parallel API
ctrlkeys.run(…)
This function is exactly like shell.run(), except the program will get ctrl_key events. For information on using it, see the wiki documentation for shell.run
ctrlkeys.coGenCtrlKeyEvents()
This function should not be called directly; it is the actual function used to create a coroutine that generates ctrlkey events. It can be passed directly into parallel.waitForAny or any of the goroutine.spawn() functions.
Events
Spoiler
"ctrl_key", key, char, bShift, bAltThe event itself, the whole point of the api! key is the key that was pressed while holding ctrl, same as returned in normal "key" events. char is the character that would have been typed; the char sent with the key will depend on whether shift was pressed or not.
bShift and bAlt are true if shift and alt, respectively, were pressed as well.
A note about bShift and bAlt, they are not 100% reliable! Due to quirks in CC events, I can know for a fact that ctrl is still held when a character key is pressed, but not shift or alt. The bottom line is that bShift and bAlt do not tell you if shift and alt were still held down when the character key was pressed. What they do tell you is whether shift or alt were pressed after control and before the key. They are not exactly what you might expect, but they are at least consistent in what they are.
Usage
Spoiler
Basic UsageSpoiler
This simple example runs ctrlkeys and just prints to the screen each event, exiting when ctrl-T is pressed (doesn't have to be held!)
--load the api first
os.loadAPI("ctrlkeys")
local function dumpKeys()
while true do
e={os.pullEvent()}
if e[1]=="ctrl_key" and e[2]==keys.t then
print("ctrl-t detected, exiting...")
return
end
--print the event and it's parameters
local str=" "
for i=1,#e do
str=str..tostring(e[i]).." "
end
print(str)
end
end
--run the function
ctrlkeys.waitForAny(dumpKeys)
http://pastebin.com/ttrYf9Dupastebin get ttrYf9Du ctrlkeySample
Using with goroutine
Spoiler
Any time goroutine is active, whether activated in startup or by a program, just add this line to top of your main coroutine function, or to your startup file after launching goroutine:
goroutine.spawn(ctrlkeys.coGenCtrlKeyEvents)
There you go! If you do this in startup, all programs will automatically get key_ctrl events without having to do anything special. Most standard programs will simply ignore the events, but ctrlkeys does not block any events, so having it installed won't prevent any programs not made for it from functioning as usual.
http://pastebin.com/d6CmF10y
pastebin get d6CmF10y ctrlkeys
ggui
Spoiler
ggui is a WIP still in the beta stage.Proper documentation will come as the feature set gets finalized.
current features:
Basic gui elements - labels, edit boxes, graphic elements, and buttons.
Multi-monitor programs using "screens", which are containers for collections of gui elements
Managed event handling - the pullEvent loop is hidden away inside the api, and you simply tell the api what functions to call when different events occur and then call ggui.run!
Anchor-based positioning - elements have a position and size, with the position interpreted based on anchorX and anchorY values. By default anchored "left" and "top", and behaving as you would expect, can be anchored right, bottom, and center as well. Position is interpreted as an offset from that edge, so position 1,1 with anchors left and top will put your element in the top-left corner, while the same position with anchors right and bottom will go bottom-right. Center anchors use the position as the center of the element,
styles - customizable styles allow control of all aspects of the gui elements appearance, which can be set up with separate color and mono versions, allowing your programs to easily support both color or black and white computers.
planned features:
more gui elements to work with, including
- popup menus
- radio button groups
- checkboxes
- screen-based dialog boxes
- tabbed pages
…and more.
Downloads
I've put together some sample programs to help test and demo the new API; you can download them all together with this installer:
Demo Installer - pastebin run fq8SinNB ggui - will install it to the ggui directory; feel free to replace ggui with the directory of your choice.
or, if you'd just like to look at the code, you can go to the api and examples directly on pastebin:
ggui api - pastebin get i0EyjZ2W ggui
example login with splash screen - hV53TCNx
sample redstone monitoring/control program - RNTj60Rd - "Monitor" program for some reason!
Screenshots of the sample programs
Spoiler
redstone sample on a 2x4 monitor:
Advanced Examples
multishell
not sure if this even works anymore, haven't bothered testing, seems slightly redundant with a built-in multishell program on all advanced computers these days…
pastebin: e4Uf8M3p
More on this program can be found in it's thread, here
More coming
Basic 1-on-1 chat program
License
Use these however you like; if you're distributing software that includes or is derived from any of my APIs, I just that you include a link to this thread and credit me, as a basic courtesy. Well, unless you make something really crappy, in which case, please don't give me any credit.