I don't have much time at the moment so there may be some formatting problems.
Yesterday I was contacted by a fellow forums member on whether they could use PearOS’s code to make their own OS. They wanted to make an OS with an interface similar to Android, however, PearOS isn’t very suitable as it is heavily window based. It tried to find a tutorial in the Tutorials section that I could send back to him to get them started. To my surprise I couldn’t find a single one, there were tutorials on how to make buttons and so on, but none on how to make a fully blown graphical OS. After a quick Google I found 3-part video tutorial by NDF-Jay, but the coding practices weren’t very good and it didn’t really allow for much future development as nothing was modular. So, I’ve decided to make a tutorial on how to make an OS, that uses a very similar layout and system to PearOS, so you could say that this is also an explanation of how PearOS works. This OS will not have windows, it won’t be based on any existing OS. All it is designed to do is get you started and teach good and sustainable coding practices.
Prerequisites
What I highly recommend you use:
• An emulator, such as CCEmu or CCDesk. I’ll be using CCEmu because it’s less buggy, draw the screen a lot faster and it doesn’t trim the top pixels, which CCDesk does for some reason. See what I mean here. This may change in future though.
• A text editor. I highly recommend Sublime Text 2, it support OS X, Linux and Windows. But really, anything will do. Try and avoid using the built in editor, traversing a 1000+ line file in it is not nice.
• Patience. Don’t expect these things to come easy, I’ve been coding for almost 5 years and have had a lot of experience with Object-Oriented coding for a number of years. This tutorial will try to make the best and most efficient product it can, so it’s not going always going to use common and easy methods.
A Brief Intro to Object-Oriented Programming
Before we get stuck in creating our OS, I’d like to go over Object-Oriented Programming (referred to as OOP from now on). My explaination is going to be breif, and won’t be as good as other’s you may find on the internet, so I recommend you take a look around until you have a good idea of what it is.
For example, take a lemon. The lemon is in the citrus family, and it is a fruit. We could show this in Lua as the following:
Fruit = {
Colour = colours.yellow,
}
Citrus = {
__index = Fruit,
PH = 3,
}
Lemon = {
__index = Citrus,
}
In this (rather shocking) example, every Lemon has all the traits of Citrus, which has all the traits of Fruit. So the Lemon’s colour would be yellow and it would have a PH of 3. This is a very brief example, I highly recommend you check out this tutorial or others to get a better idea.
Program/Coding Style
Finally, before we begging I would like to go over the style and format of my code/programs.
First of all, I am from New Zealand (I’m just living in Australia, but the same applies), and rather than ‘color’ we use ‘colour’, so I’ll be using that through out the tutorial, however, you are welcome to use the spelling you’d like to use. I also tend to capitalise all function and variable names, just because it looks nicer. But it does not matter what you do in this regard, it still works exactly the same.
However, I also tend to break my programs up in to files (PearOS as around 70 files). But due to the nature of this tutorial, we won’t be doing that as much as I normally would. However, when I make a folder I request that you do that same so it’s easier to help you if you run in to trouble.
Anyway, on with the tutorial.
Getting Started
Open an Advanced Computer, if you are using an emulator (which you should) set the ID to 0, purely because its quicker if when you reopen the computer. Open the computers folder in your text editor and make a file called ‘startup’.
Enter the following in to the file:
if not term.isColour() then
error("Please use an Advanced Computer")
end
shell.run("System/OS")
init()
What this code is doing is check that the computer supports colour, loading the OS then running the init method. We use ‘init’ to keep the code clean and make it obvious what code runs at startup, PearOS also uses an init method to catch errors, but we won’t be doing this.
Enter the following in to the file:
function init()
term.setBackgroundColour(colours.lightGrey)
term.clear()
term.setCursorPos(22,10)
term.write("Hello!")
end
If you’ve done everything correctly so far you should see “Hello!” in the middle of the screen. You may have noticed that we are taking quite a few steps to do 2 basic functions, and when we get in to drawing areas of colour this will only get worse. In every major project I’ve done in ComputerCraft (which is pretty much just PearOS and Heroes of Tlön, which never got released) I’ve used my (currently unreleased, but I might release it for the sake of it) Drawing API. If you have your own, take a look at how this works before you use your own, it’s essentiall that it has all the same features. You can find my Drawing API here (I haven’t looked at it for some time so it may have some faults), however, I encourage you to only take a look at it rather than use it for the tutorial.
A quick (or not so quick) note on naming objects.
In Objective-C (the main language for making OS X applications) most objects/group of functions start with prefixes, most of them have ‘NS’ (for example, ‘NSString’). In short NS stands for NExTSTEP (yes, that is the correct grammar/spelling/what ever you want to call it), OS X is based on it. If you want more information, Google it, it’s not important for our subject. Having a prefix is a great way knowing what an object is (e.g. a core operating system object, or the input from the user. You don’t want to accidentally write over an operating system object.). The prefix it used in PearOS was ‘OS’, I’m not going to use that purely so I don’t mix files up at some point, so I’ll be using CC (ComputerCraft) but it chose your own. It doesn’t matter, but keep it short and sweet.
Drawing Library
Now we are going to create our drawing library. As my prefix is ‘CC’ I’ll be calling it ‘CCDrawing’, I recommend you use the same format. At the top of the ‘OS’ file add the following code:
CCDrawing = {
DrawCharacters = function (_x, _y, _string, _textColour, _backgroundColour)
term.setBackgroundColour(_backgroundColour)
term.setTextColour(_textColour)
term.setCursorPos(_x, _y)
term.write(_string)
end,
}
This will make drawing text in different colours a lot quicker and easier. It also allows you to easily implement a buffer, which we won’t do or discuss. In your init function replace the content with the below:
function init()
CCDrawing.DrawCharacters(22,10, "Hello!", colours.white, colours.lightGrey)
end
We have reduced our code from 4 lines to 1 line. (Yes, I know, technically we’ve increased it by around 10, but when you’re calling this dozens of times it does pay off.) If you run the code now you’ll see that while it does display the text, the reset of the screen is still black. Lets add a function to draw an area of colour, which we can use for many things from drawing a menu’s background to clearing the screen.
Add the following below the DrawCharacters end statement:
DrawArea = function (_x, _y, _w, _h, _character, _textColour, _bgColour)
--width must be greater than 1, other wise we get a stack overflow
if _w > 0 then
_w = _w * -1
elseif _w == 0 then
_w = 1
end
if _h > 0 then
_h = _h * -1
elseif _h == 0 then
_h = 1
end
local sRow = ""
for ix = 1, _w do
sRow = sRow .. _character
end
for iy = 1, _h do
local currY = _y + iy - 1
CCDrawing.DrawCharacters(_x, currY, sRow, _textColour, _bgColour)
end
end
What this does is first makes sure that the width and height is greater than 0, it then makes a string that is a long as the width given and draws it so an area with the size given is made. Let's add this to our init code. Make sure you add this before the DrawCharacters line.
CCDrawing.DrawArea(1,1,51,19," ",colours.white, colours.lightGrey)
You may have noticed that I've used 51 and 19 for the width and height, these are the screen dimentions. However, you should never use set numbers. Let's add a value to the drawing api so we can easily get the screen size with out having to dedicate a few lines to retrieve them.
At the very top of the code add this line:
local _w, _h = term.getSize()
This gets our screen size and stores it in two variables. We then assign those values to the screen size, add this at the top of the drawing code:
Screen = {
Width = _w,
Height = _h
},
Now we can change our DrawArea line to this:
CCDrawing.DrawArea(1, 1, CCDrawing.Screen.Width, CCDrawing.Screen.Height, " ", colours.white, colours.lightGrey)
Now, there are other ways to do this, this is one of the tidier and easier ways. It doesn't change when the screen size changes, but if you want to add that it's not too hard.
Making Buttons
Alright, lets make our first button!
Our button will be be a subclass (meaning a child/descendant, like the Lemon or Citrus was) of three objects in this order:
CCButton
CCControl
CCEntity
CCObject
Each part of that tree has its own functions. CCButton deals with all of the drawing code and creation of a new button. CCControl handles the clicking, whether or not it's disabled, whether it's selected, etc. CCEntity handles the position and size. Finally, CCObject manages the objects ID.
Now, you may be asking your self why we bother having all these different objects. The answer is simple. All the functions that CCButton has to do are specific to it self, nothing else can use them. However, if we want to add more controls later on such as a menu then we can simply make it a subclass of CCControl. We can do the same for CCEntity, which is essentially anything thats on the screen (in the case of PearOS, the dock for example). And every single object inherits from CCObject so we can keep track of them and add functions that all objects need. This is exactly how PearOS works and very very close to how OS X works.
Lets start with CCObject. Immediately after CCDrawing finishes add the following code:
CCObject = {
Type = "CCObject",
ID = 0
}
Not much happens in OSObjects, buts its good to have.
Now we'll add CCEntity, after CCObject finishes add the following:
CCEntity = {
__index = CCObject,
X = 0,
Y = 0,
Width = 0,
Height = 0,
Title = "",
}
A few things happening here. '__index' is the object is inherits from (as Lemon does from Citrus). X, Y, etc. are the possition and size of the object. Title is the title of the object which most entities will have.
Let now add CCControl, again, add this below CCEntity.
CCControl = {
__index = CCEntity,
Action = nil,
Enabled = true
}
Not a huge amount going on here, but it is important. Action is the function that is called when the object is clicked and enabled is whether or not the object can be clicked.
Finally, lets add CCButton.
CCButton = {
__index = CCControl,
Type = "CCButton",
TextColour = colours.white,
BackgroundColour = colours.grey,
Height = 1,
New = function(self, _x, _y, _title, _action)
local new = {} -- the new instance
setmetatable( new, {__index = self} )
new.Width = string.len(_title)+2
new.X = _x
new.Y = _y
new.Title = _title
new.Action = _action
new.Enabled = true
return new
end,
Draw = function(self)
CCDrawing.DrawCharacters(self.X,self.Y, " "..self.Title.." ", self.TextColour, self.BackgroundColour)
end
}
Self referes to the object that called the function, however, to do this you have to use a colon ( : ), so for example 'ourButton:Draw()' is the same as ourButton.Draw(ourButton). It's just a way to shorten your code and make sure you are getting the right object.
TextColour and BackgroundColour are the, as their name suggests, the text and background colours. We do this so we can change the colours easily. Height is 1 by default, you can change this, or you could add it as another argument in New; but for now 1 is fine. The New function is called when ever we need a new instance of a button and Draw is called when we want to draw it on the screen.
Let's test our new button framework, remove the 'DrawCharacters' line in init and replace it with this:
local ourButton = CCButton:New(19, 10, "Hello Again!", function() os.reboot() end)
ourButton:Draw()
If you've done everything correctly you should see a grey box in the middle of the screen with "Hello Again!". If you don't, try to find the error and see if you can fix it on your own. If you've tried to fix it but you can't post the error message below.
You may have noticed at the end of the the first line that we have a function with os.reboot in it. This will be called when we click it. However, we don't have anyway of detecting whether the button has been clicked, so lets do that now.
At the very top of the file, just after the term.getSize line, add the following:
CCInterfaceEntities = {
List = {},
Add = function(_entity)
table.insert(CCInterfaceEntities.List, _entity)
end
}
This is our place to store our on-screen objects (entities), every time the screen is clicked or drawn it will go though the list and either find the object to click or draw them. We have an Add method so we can easily add objects, you could add a remove method if you want but we won't be doing this.Now that we have our interface entities list, remove the 'ourButton:Draw()' line and replace it with this:
CCInterfaceEntities.Add(ourButton)
If you run your code now, you shouldn't see your button, this is because it's no being draw yet. Lets add a drawing function. Place this just before init.
function DrawScreen()
for _,entity in ipairs(CCInterfaceEntities.List) do
entity:Draw()
end
end
This goes through every entity on the list and draws it.
Capturing Mouse Clicks
Now we need to add away to capture clicks. Add this just after the DrawScreen function.
function EventHandler()
while true do
local event, arg, x, y = os.pullEventRaw()
if event == "mouse_click" then
for _,entity in pairs(CCInterfaceEntities.List) do
--check if the click overlaps an entities
if x >= entity.X and x >=entity.X + entity.Width - 1 and y >= entity.Y and y >=entity.Y + entity.Height then
if entity.Action then
entity:Action()
break
end
end
end
end
end
end
This waits for an event, and if it's a mouse click it will, as the drawing function does, loop through all of the entities and see if the click overlaps the entity, if it does it checks if the entity has an action and if it does it runs it. After the action has been run the loop breaks (stops) to prevent any other entities being clicked.
So, thats the end of the tutorial. I hope you've learnt a lot and taken some valuable practices etc. out of it. You can use this code as you wish, make your own OS out of if you like. You don't have to credit me, but it would be appreciated if you link back to this tutorial. Remember, this is very brief and can't really be called an OS yet, even by ComputerCraft standards. I have left it to you to make the OS of your dreams with a solid framework behind it. If you have any problems or questions leave a comment below or PM me. You can find this tutorials code (the OS file, there's no point uploading the two line startup file) here. However, I encourage you to only use it you have a problem with your code.
Download the Tutorial code.