mirror of
https://github.com/20kdc/OC-KittenOS.git
synced 2024-11-16 15:38:05 +11:00
Initial commit
Currently eeprog pirate-speak language is unsupported, but... oh well.
This commit is contained in:
commit
2ae3f9a93a
192
API Documentation
Normal file
192
API Documentation
Normal file
@ -0,0 +1,192 @@
|
||||
KittenOS App Construction
|
||||
|
||||
Firstly, an 'app' is,
|
||||
in essence, a process,
|
||||
with one window.
|
||||
The app's initial Lua
|
||||
script must return 3
|
||||
values: app, w, h.
|
||||
App contains callbacks,
|
||||
w and h are the app's
|
||||
initial window size.
|
||||
In the below section,
|
||||
"dbool" means a boolean
|
||||
which, if true, redraws
|
||||
the whole window.
|
||||
|
||||
Callbacks are:
|
||||
update():dbool
|
||||
key(ascii, scan, down):
|
||||
dbool
|
||||
clipboard(text):dbool
|
||||
get_ch(x, y):character
|
||||
event(t, ...):dbool
|
||||
^ The above is only used
|
||||
under any one of these
|
||||
conditions:
|
||||
1. The event is a
|
||||
modem_message, and
|
||||
the app requested
|
||||
and got one of the
|
||||
following APIs:
|
||||
c.modem, c.tunnel
|
||||
(implemented as a
|
||||
special case in
|
||||
the getAPI for
|
||||
those APIs,
|
||||
giving access to
|
||||
s.modem_message)
|
||||
2. The app managed to
|
||||
get the API named
|
||||
"s.<signal type>".
|
||||
3. The app managed to
|
||||
successfully get
|
||||
the root API
|
||||
rpc(cPkg, cAid, ...):any
|
||||
The application named
|
||||
by cPkg, process cAid,
|
||||
called your app via
|
||||
the "proc" API.
|
||||
The return value is
|
||||
passed on, to the
|
||||
calling application.
|
||||
|
||||
-KittenOS API Reference--
|
||||
|
||||
KittenOS initializes apps
|
||||
with global functions,
|
||||
but only one library
|
||||
table: "A".
|
||||
|
||||
This table contains all
|
||||
basic KittenOS funcs,
|
||||
in the order they are
|
||||
written in the kernel.
|
||||
|
||||
A.listApps()
|
||||
Returns a list of apps
|
||||
that can be launched.
|
||||
(Not running processes,
|
||||
see Request Doc/"proc")
|
||||
Used to allow launcher
|
||||
to avoid requiring a
|
||||
special permission for
|
||||
almost no reason.
|
||||
|
||||
A.launchApp(pkg)
|
||||
Launches an application,
|
||||
from the Lua file:
|
||||
"apps/"..pkg..".lua" on
|
||||
the primary filesystem.
|
||||
The package name must
|
||||
match the Lua pattern:
|
||||
"[a-zA-Z%-_\x80-\xFF]+"
|
||||
The app ID is returned.
|
||||
|
||||
A.opencfg(mode)
|
||||
Opens the file:
|
||||
"cfgs/"..pkg on the
|
||||
primary filesystem.
|
||||
This allows storage of
|
||||
any and all data an app
|
||||
may store without any
|
||||
user involvement.
|
||||
This is not to be used
|
||||
for documents, except
|
||||
as part of a recovery
|
||||
system.
|
||||
|
||||
A.openfile(type, mode)
|
||||
Asks the user to pick a
|
||||
file, and opens it with
|
||||
a specific file mode.
|
||||
This invokes the script
|
||||
"tfilemgr.lua".
|
||||
File modes are 'r and 'w
|
||||
and both are binary.
|
||||
Should the mode be nil,
|
||||
the file manager is
|
||||
opened without any way
|
||||
to open a file.
|
||||
This is useful for
|
||||
file management, and is
|
||||
the implementation of
|
||||
the "filemgr" app.
|
||||
|
||||
A.request(...)
|
||||
Allows you to get APIs.
|
||||
For a list, look in the
|
||||
kernel for "getAPI".
|
||||
Some APIs may require
|
||||
permission, and the
|
||||
request may cause added
|
||||
callbacks to be called,
|
||||
e.g. the "net" API will
|
||||
cause "modem_message"
|
||||
calls on those signals.
|
||||
|
||||
A.timer(t)
|
||||
Sets the "update"
|
||||
callback to be called
|
||||
in a given number of
|
||||
seconds. Note that only
|
||||
one timer can be going
|
||||
at a given time, and
|
||||
passing <= 0 to timer
|
||||
will disable it.
|
||||
|
||||
A.resize(w, h)
|
||||
Resize the running app,
|
||||
among other things,
|
||||
forcing a complete
|
||||
redraw of the app.
|
||||
|
||||
A.die()
|
||||
Kill the running app,
|
||||
immediately.
|
||||
Do not call functions
|
||||
after this is called.
|
||||
|
||||
-KittenOS File API-------
|
||||
This is the interface
|
||||
exposed by filewrap.lua,
|
||||
when loaded.
|
||||
It is not a very good
|
||||
interface, but it works.
|
||||
|
||||
file.read(bytes)
|
||||
Read some amount of
|
||||
bytes from the file.
|
||||
Returns nil on EOF.
|
||||
|
||||
file.write(str)
|
||||
Write some amount of
|
||||
bytes to the file.
|
||||
The bytes are in str,
|
||||
as per the old standard
|
||||
of byte arrays being
|
||||
strings.
|
||||
|
||||
file.close()
|
||||
Closes the file.
|
||||
|
||||
-KittenOS Security Notes-
|
||||
KittenOS has a few parts
|
||||
which add security, but
|
||||
all of them follow one
|
||||
principle: Kernel-local
|
||||
objects are private, and
|
||||
passing an environment
|
||||
to the "load" function
|
||||
will ensure this.
|
||||
Assuming this axiom is
|
||||
true, KittenOS allows
|
||||
access to anything only
|
||||
if the user explicitly
|
||||
allows it.
|
||||
Files are handled on a
|
||||
per-file basis, unless
|
||||
the "fs" permission is
|
||||
granted (which allows
|
||||
complete access to the
|
||||
filesystem).
|
149
Request Documentation
Normal file
149
Request Documentation
Normal file
@ -0,0 +1,149 @@
|
||||
Read API Documentation,
|
||||
before reading this.
|
||||
|
||||
Note that access to any
|
||||
component can be gotten
|
||||
by prefixing the type
|
||||
of component with "c.",
|
||||
but unless interfacing
|
||||
with an external device,
|
||||
use "stat" and "randr".
|
||||
("randr" covers your
|
||||
multi-screen needs.)
|
||||
In a truly extreme case,
|
||||
like an installer, you
|
||||
can use "c.filesystem".
|
||||
Signals from the devices
|
||||
are not relayed, as I
|
||||
am unsure of a way to
|
||||
verify the source.
|
||||
(It seems to be a default
|
||||
to have the device as
|
||||
the first parameter, but
|
||||
I don't want to rely on
|
||||
this behavior.)
|
||||
|
||||
There are exceptions:
|
||||
A successful "c.modem" or
|
||||
"c.tunnel" request will
|
||||
enable the modem_message
|
||||
event to make it
|
||||
somewhat useful.
|
||||
A similar thing occurs
|
||||
for "c.chat_box",
|
||||
and "root" allows seeing
|
||||
all events.
|
||||
|
||||
Another way to get at
|
||||
signals is the
|
||||
"s.<signal>" permission.
|
||||
Like the "c.<component>"
|
||||
permission, it's a tiny
|
||||
general means of getting
|
||||
events that the kernel
|
||||
has no way (or reason)
|
||||
to access.
|
||||
|
||||
The interface has a
|
||||
function to return an
|
||||
iterator of proxies,
|
||||
called "list".
|
||||
The interface may have a
|
||||
"primary" field if the
|
||||
system has a 'primary'
|
||||
choice for that device.
|
||||
|
||||
-------------------------
|
||||
KittenOS A.request()
|
||||
Acceptable Values:
|
||||
|
||||
"math", "table", "string"
|
||||
, "unicode": Standard,
|
||||
if indirect, access to
|
||||
those Lua APIs.
|
||||
(Done via metatable to
|
||||
preserve memory.)
|
||||
Note that the "unicode"
|
||||
API has an additional
|
||||
function:
|
||||
safeTextFormat(txt,ptr)
|
||||
adds spaces after wide
|
||||
characters, making it
|
||||
easier to display text
|
||||
with wide characters
|
||||
in. "ptr" can be a
|
||||
point in the string,
|
||||
and is also adjusted -
|
||||
this can be used for
|
||||
e.g. console cursors.
|
||||
Returns atxt, aptr.
|
||||
|
||||
"root": The kernel _ENV.
|
||||
Essentially everything.
|
||||
|
||||
"stat": Contains:
|
||||
(names preserved, but
|
||||
namespaces aren't)
|
||||
computer:
|
||||
totalMemory freeMemory
|
||||
energy maxEnergy
|
||||
os:
|
||||
clock date difftime
|
||||
time
|
||||
Also contains
|
||||
"component.list", but
|
||||
named "componentList".
|
||||
|
||||
"proc": Contains:
|
||||
aid: A field containing
|
||||
the running app ID.
|
||||
Usually pkg .. "-" .. n
|
||||
where 'n' is a number.
|
||||
pkg: A field containing
|
||||
the app name (aka. pkg)
|
||||
listApps()
|
||||
Returns a table of
|
||||
{package, aid} entries.
|
||||
|
||||
sendRPC(aid, ...)
|
||||
Causes the "rpc" event
|
||||
to be called, with the
|
||||
given data parameters.
|
||||
|
||||
"lang":
|
||||
getLanguage(): Gets the
|
||||
current system language
|
||||
getTable(): Loads and
|
||||
calls the Lua file at
|
||||
"/lang/<lang>/<pkg>.lua"
|
||||
Failing that, returns
|
||||
nil
|
||||
|
||||
"setlang": Actually one
|
||||
function, setting the
|
||||
system language.
|
||||
|
||||
"kill": Contains:
|
||||
killApp(aid)
|
||||
Kills a process.
|
||||
|
||||
"randr": Contains:
|
||||
getResolution,
|
||||
maxResolution,
|
||||
setResolution,
|
||||
(the above work
|
||||
as on a GPU)
|
||||
iterateScreens,
|
||||
iterateGPUs
|
||||
(the above iterate over
|
||||
*non-primary* proxies,
|
||||
direct control of the
|
||||
primary screen is not
|
||||
allowed)
|
||||
"c.modem": See the notes
|
||||
above on the "c." setup,
|
||||
but note that this adds
|
||||
the "modem_message"
|
||||
callback.
|
||||
"c.<component type>":
|
||||
See the notes above.
|
69
apps/batmon.lua
Normal file
69
apps/batmon.lua
Normal file
@ -0,0 +1,69 @@
|
||||
local math, stat = A.request("math", "stat")
|
||||
local app = {}
|
||||
-- How much did energy change
|
||||
-- over 1 second?
|
||||
local lastChange = 0
|
||||
local lastValue = nil
|
||||
local lastTimer = nil
|
||||
local usage = {
|
||||
"[####]:",
|
||||
"[###:]:",
|
||||
"[### ]:",
|
||||
"[##: ]:",
|
||||
"[## ]:",
|
||||
"[#: ]:",
|
||||
"[# ]:",
|
||||
"[: ]:",
|
||||
"[ ]:",
|
||||
"WARNING"
|
||||
}
|
||||
local function getText(y)
|
||||
if y == 2 then
|
||||
if not lastChange then
|
||||
return "Wait..."
|
||||
end
|
||||
local ind = "Dc. "
|
||||
local wc = lastChange
|
||||
local wv = stat.energy()
|
||||
if wc > 0 then
|
||||
wc = -wc
|
||||
wv = stat.maxEnergy() - wv
|
||||
ind = "Ch. "
|
||||
end
|
||||
local m = math.floor((wv / -wc) / 60)
|
||||
return ind .. m .. "m"
|
||||
end
|
||||
local dec = stat.energy() / stat.maxEnergy()
|
||||
-- dec is from 0 to 1.
|
||||
local potential = math.floor(dec * #usage)
|
||||
if potential < 0 then potential = 1 end
|
||||
if potential >= #usage then potential = #usage - 1 end
|
||||
return usage[#usage - potential]
|
||||
end
|
||||
function app.key(ka, kc, down)
|
||||
if down then
|
||||
if ka == ("C"):byte() then
|
||||
A.die()
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
function app.update()
|
||||
local nv = stat.energy()
|
||||
if lastValue then
|
||||
lastChange = (nv - lastValue) / lastTimer
|
||||
end
|
||||
lastValue = nv
|
||||
lastTimer = 10
|
||||
if lastChange then
|
||||
if lastChange > 10 then
|
||||
lastTimer = 1
|
||||
end
|
||||
end
|
||||
A.timer(lastTimer)
|
||||
return true
|
||||
end
|
||||
function app.get_ch(x, y)
|
||||
return getText(y):sub(x, x)
|
||||
end
|
||||
return app, 7, 2
|
78
apps/eeprog.lua
Normal file
78
apps/eeprog.lua
Normal file
@ -0,0 +1,78 @@
|
||||
local lang, unicode = A.request("lang", "unicode")
|
||||
local eeprom = A.request("c.eeprom")
|
||||
if eeprom then
|
||||
eeprom = eeprom.list()()
|
||||
end
|
||||
|
||||
local langTable = lang.getTable()
|
||||
local function G(text)
|
||||
if langTable then
|
||||
if langTable[text] then
|
||||
return langTable[text]
|
||||
end
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
local postFlash = false
|
||||
local label = ""
|
||||
local app = {}
|
||||
function app.key(ka, kc, down)
|
||||
if down then
|
||||
if postFlash then
|
||||
if ka ~= 0 then
|
||||
if ka == 8 then
|
||||
label = unicode.sub(label, 1, unicode.len(label) - 1)
|
||||
return true
|
||||
end
|
||||
if ka == 13 then
|
||||
eeprom.setLabel(label)
|
||||
postFlash = false
|
||||
return true
|
||||
end
|
||||
label = label .. unicode.char(ka)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
if ka == ("r"):byte() then
|
||||
local f = A.openfile(G("EEPROM Dump"), "w")
|
||||
if f then
|
||||
f.write(eeprom.get())
|
||||
f.close()
|
||||
end
|
||||
end
|
||||
if ka == ("w"):byte() then
|
||||
local f = A.openfile(G("EEPROM to flash"), "r")
|
||||
if f then
|
||||
local txt = f.read(128)
|
||||
local ch = ""
|
||||
while txt do
|
||||
ch = ch .. txt
|
||||
txt = f.read(128)
|
||||
end
|
||||
eeprom.set(ch)
|
||||
postFlash = true
|
||||
label = ""
|
||||
return true
|
||||
end
|
||||
end
|
||||
if ka == ("C"):byte() then
|
||||
A.die()
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
-- this string must be the longest, kind of bad but oh well
|
||||
-- at least it's not a forced 29 chars...
|
||||
local baseString = unicode.safeTextFormat(G("EEPROMFlash! (R)ead, (W)rite?"))
|
||||
function app.get_ch(x, y)
|
||||
if postFlash then
|
||||
return unicode.sub(unicode.safeTextFormat(G("Label: ") .. label), x, x)
|
||||
end
|
||||
if not eeprom then
|
||||
return unicode.sub(unicode.safeTextFormat(G("No EEPROM installed?")), x, x)
|
||||
end
|
||||
return unicode.sub(baseString, x, x)
|
||||
end
|
||||
return app, unicode.len(baseString), 1
|
4
apps/filemgr.lua
Normal file
4
apps/filemgr.lua
Normal file
@ -0,0 +1,4 @@
|
||||
-- Launch the File Manager.
|
||||
-- What more could be said?
|
||||
A.openfile("any file", nil)
|
||||
return {update = A.die}, 0, 0
|
267
apps/installer.lua
Normal file
267
apps/installer.lua
Normal file
@ -0,0 +1,267 @@
|
||||
local lang, setlang, math, table, unicode, fs = A.request("lang", "setlang", "math", "table", "unicode", "c.filesystem")
|
||||
|
||||
local options = {}
|
||||
local optionCallback = function (index) end
|
||||
local cursor = 1
|
||||
local inited = false
|
||||
|
||||
local languages = {}
|
||||
local languagesMenu = {}
|
||||
local languageNames = {
|
||||
["en"] = "English",
|
||||
["de"] = "German",
|
||||
["ru"] = "Russian",
|
||||
["jbo"] = "Lojban",
|
||||
["ja"] = "Japanese",
|
||||
["kw"] = "Cornish",
|
||||
["nl"] = "Dutch",
|
||||
["pl"] = "Polish",
|
||||
["pt"] = "Portugese",
|
||||
["zh"] = "Chinese",
|
||||
["it"] = "Italian",
|
||||
["ga"] = "Irish",
|
||||
["fr"] = "French",
|
||||
["es"] = "Spanish",
|
||||
["pirate"] = "I be speakin' Pirate!"
|
||||
}
|
||||
for k, v in pairs(languageNames) do
|
||||
if fs.primary.exists("lang/" .. k .. "/installer.lua") or (k == "en") then
|
||||
table.insert(languages, k)
|
||||
table.insert(languagesMenu, v)
|
||||
end
|
||||
end
|
||||
|
||||
local langTable = nil
|
||||
local function G(text)
|
||||
if langTable then
|
||||
if langTable[text] then
|
||||
return langTable[text]
|
||||
end
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
-- Config
|
||||
local appDeny = {}
|
||||
local installFS = nil
|
||||
local installLang = nil
|
||||
|
||||
-- Stages
|
||||
local startLanguageSel = nil
|
||||
local startFSSel = nil
|
||||
local startAppSel = nil
|
||||
local startFSConfirm = nil
|
||||
local startInstall = nil
|
||||
|
||||
-- Stuff for actual install
|
||||
local runningInstall = nil
|
||||
local runningInstallPoint = 0
|
||||
|
||||
local function setOptions(ol, callback)
|
||||
options = ol
|
||||
optionCallback = callback
|
||||
cursor = 1
|
||||
local maxlen = 1
|
||||
for k, v in ipairs(options) do
|
||||
options[k] = unicode.safeTextFormat(v)
|
||||
local l = unicode.len(v)
|
||||
if l > maxlen then maxlen = l end
|
||||
end
|
||||
A.resize(maxlen + 1, #options)
|
||||
end
|
||||
|
||||
local app = {}
|
||||
function startLanguageSel()
|
||||
setOptions(languagesMenu, function (i)
|
||||
setlang(languages[i])
|
||||
langTable = lang.getTable()
|
||||
startAppSel()
|
||||
end)
|
||||
end
|
||||
function startAppSel()
|
||||
local al = A.listApps()
|
||||
table.sort(al)
|
||||
local tbl = {}
|
||||
table.insert(tbl, G("KittenOS Installer"))
|
||||
table.insert(tbl, G("Applications to install:"))
|
||||
for _, v in ipairs(al) do
|
||||
table.insert(tbl, G("Install Application: ") .. v .. " [" .. G(tostring(not appDeny[v])) .. "]")
|
||||
end
|
||||
table.insert(tbl, G("<Confirm>"))
|
||||
setOptions(tbl, function (i)
|
||||
if i >= 3 and i < #tbl then
|
||||
appDeny[al[i - 2]] = not appDeny[al[i - 2]]
|
||||
startAppSel()
|
||||
return
|
||||
end
|
||||
if i == #tbl then
|
||||
startFSSel()
|
||||
end
|
||||
end)
|
||||
end
|
||||
function startFSSel()
|
||||
local fsl = {}
|
||||
for fsp in fs.list() do
|
||||
if fsp ~= fs.primary then
|
||||
table.insert(fsl, fsp.address)
|
||||
end
|
||||
end
|
||||
local tbl = {}
|
||||
table.insert(tbl, G("KittenOS Installer"))
|
||||
table.insert(tbl, G("Filesystem to target:"))
|
||||
for _, v in ipairs(fsl) do
|
||||
table.insert(tbl, "<" .. v .. ">")
|
||||
end
|
||||
setOptions(tbl, function (i)
|
||||
if i > 2 then
|
||||
for fsp in fs.list() do
|
||||
if fsp.address == fsl[i - 2] then
|
||||
installFS = fsp
|
||||
startFSConfirm()
|
||||
return
|
||||
end
|
||||
end
|
||||
startFSSel()
|
||||
end
|
||||
end)
|
||||
end
|
||||
function startFSConfirm()
|
||||
local tbl = {}
|
||||
table.insert(tbl, G("KittenOS Installer"))
|
||||
table.insert(tbl, G("Are you sure you want to install to FS:"))
|
||||
table.insert(tbl, installFS.address)
|
||||
table.insert(tbl, G("These applications will be installed:"))
|
||||
local other = nil
|
||||
for _, v in ipairs(A.listApps()) do
|
||||
if not appDeny[v] then
|
||||
if other then
|
||||
table.insert(tbl, other .. ", " .. v)
|
||||
other = nil
|
||||
else
|
||||
other = v
|
||||
end
|
||||
end
|
||||
end
|
||||
if other then
|
||||
table.insert(tbl, other)
|
||||
end
|
||||
table.insert(tbl, G("<Yes>"))
|
||||
table.insert(tbl, G("<No, change settings>"))
|
||||
setOptions(tbl, function (i)
|
||||
if i == (#tbl - 1) then
|
||||
-- first, create directories.
|
||||
local function forceMakeDirectory(s)
|
||||
if installFS.exists(s) then
|
||||
if not installFS.isDirectory(s) then
|
||||
installFS.remove(s)
|
||||
end
|
||||
end
|
||||
installFS.makeDirectory(s)
|
||||
end
|
||||
installLang = lang.getLanguage()
|
||||
forceMakeDirectory("apps")
|
||||
forceMakeDirectory("cfgs")
|
||||
forceMakeDirectory("lang")
|
||||
forceMakeDirectory("lang/" .. installLang)
|
||||
runningInstall = {
|
||||
-- in order of importance
|
||||
"init.lua",
|
||||
"policykit.lua",
|
||||
"tfilemgr.lua",
|
||||
"filewrap.lua",
|
||||
"language"
|
||||
}
|
||||
for _, v in ipairs(A.listApps()) do
|
||||
if not appDeny[v] then
|
||||
table.insert(runningInstall, "apps/" .. v .. ".lua")
|
||||
if fs.primary.exists("lang/" .. installLang .. "/" .. v .. ".lua") then
|
||||
table.insert(runningInstall, "lang/" .. installLang .. "/" .. v .. ".lua")
|
||||
end
|
||||
end
|
||||
end
|
||||
runningInstallPoint = 1
|
||||
startInstall()
|
||||
end
|
||||
if i == #tbl then
|
||||
startAppSel()
|
||||
end
|
||||
end)
|
||||
end
|
||||
function startInstall()
|
||||
local percent = math.floor((runningInstallPoint / #runningInstall) * 100)
|
||||
local tbl = {
|
||||
G("Installing.") .. " " .. percent .. "%"
|
||||
}
|
||||
setOptions(tbl, function (i) end)
|
||||
A.timer(1)
|
||||
end
|
||||
function startComplete()
|
||||
setOptions({G("Installation complete."), G("Press Shift-C to leave.")}, function (i) end)
|
||||
end
|
||||
function app.update()
|
||||
if runningInstall then
|
||||
if runningInstall[runningInstallPoint] then
|
||||
local txt = runningInstall[runningInstallPoint]
|
||||
-- perform copy
|
||||
local h2 = installFS.open(txt, "wb")
|
||||
if txt == "language" then
|
||||
if installLang then
|
||||
installFS.write(h2, installLang)
|
||||
else
|
||||
installFS.write(h2, "en")
|
||||
end
|
||||
else
|
||||
local h = fs.primary.open(txt, "rb")
|
||||
local chk = fs.primary.read(h, 1024)
|
||||
while chk do
|
||||
installFS.write(h2, chk)
|
||||
chk = fs.primary.read(h, 1024)
|
||||
end
|
||||
fs.primary.close(h)
|
||||
end
|
||||
installFS.close(h2)
|
||||
startInstall()
|
||||
runningInstallPoint = runningInstallPoint + 1
|
||||
else
|
||||
runningInstall = nil
|
||||
startComplete()
|
||||
end
|
||||
return true
|
||||
end
|
||||
-- should only be called once, but just in case
|
||||
if not inited then
|
||||
startLanguageSel()
|
||||
inited = true
|
||||
end
|
||||
return true
|
||||
end
|
||||
function app.get_ch(x, y)
|
||||
if x == 1 then
|
||||
if y == cursor then return ">" else return " " end
|
||||
end
|
||||
local s = options[y]
|
||||
if not s then s = "FIXME" end
|
||||
return unicode.sub(s, x - 1, x - 1)
|
||||
end
|
||||
function app.key(ka, kc, down)
|
||||
if down then
|
||||
if kc == 200 then
|
||||
cursor = cursor - 1
|
||||
if cursor < 1 then cursor = 1 end
|
||||
return true
|
||||
end
|
||||
if kc == 208 then
|
||||
cursor = cursor + 1
|
||||
if cursor > #options then cursor = #options end
|
||||
return true
|
||||
end
|
||||
if ka == 13 then
|
||||
optionCallback(cursor)
|
||||
return true
|
||||
end
|
||||
if ka == ("C"):byte() then
|
||||
A.die()
|
||||
end
|
||||
end
|
||||
end
|
||||
return app, 1, 1
|
30
apps/keycodes.lua
Normal file
30
apps/keycodes.lua
Normal file
@ -0,0 +1,30 @@
|
||||
local math, table = A.request("math", "table")
|
||||
local app = {}
|
||||
local strs = {"", "Shift-C to quit."}
|
||||
local keys = {}
|
||||
local function rebuildKeys()
|
||||
local keylist = {}
|
||||
for k, v in pairs(keys) do
|
||||
if v then
|
||||
table.insert(keylist, k)
|
||||
end
|
||||
end
|
||||
table.sort(keylist)
|
||||
strs[1] = ""
|
||||
for _, v in ipairs(keylist) do
|
||||
strs[1] = strs[1] .. v .. " "
|
||||
end
|
||||
end
|
||||
app.key = function(ka, kc, down)
|
||||
if ka == ("C"):byte() and down then
|
||||
A.die()
|
||||
return false
|
||||
end
|
||||
keys[kc] = down
|
||||
rebuildKeys()
|
||||
return true
|
||||
end
|
||||
app.get_ch = function (x, y)
|
||||
return (strs[y]):sub(x, x)
|
||||
end
|
||||
return app, 20, 2
|
38
apps/launcher.lua
Normal file
38
apps/launcher.lua
Normal file
@ -0,0 +1,38 @@
|
||||
-- Application launcher
|
||||
local table, unicode = A.request("table", "unicode")
|
||||
local apps = A.listApps()
|
||||
local maxlen = 1
|
||||
for _, v in ipairs(apps) do
|
||||
if unicode.len(v) > maxlen then maxlen = unicode.len(v) end
|
||||
end
|
||||
local app = {}
|
||||
local cursor = 1
|
||||
function app.get_ch(x, y)
|
||||
if x == 1 then
|
||||
if y == cursor then return ">" else return " " end
|
||||
end
|
||||
local s = apps[y]
|
||||
if not s then s = "FIXME" end
|
||||
return unicode.sub(unicode.safeTextFormat(s), x - 1, x - 1)
|
||||
end
|
||||
function app.key(ka, kc, down)
|
||||
if down then
|
||||
if kc == 200 then
|
||||
cursor = cursor - 1
|
||||
if cursor < 1 then cursor = 1 end
|
||||
return true
|
||||
end
|
||||
if kc == 208 then
|
||||
cursor = cursor + 1
|
||||
if cursor > #apps then cursor = #apps end
|
||||
return true
|
||||
end
|
||||
if ka == 13 then
|
||||
A.launchApp(apps[cursor])
|
||||
end
|
||||
if ka == ("C"):byte() then
|
||||
A.die()
|
||||
end
|
||||
end
|
||||
end
|
||||
return app, maxlen + 1, #apps
|
39
apps/lineclip.lua
Normal file
39
apps/lineclip.lua
Normal file
@ -0,0 +1,39 @@
|
||||
local unicode, proc = A.request("unicode", "proc")
|
||||
for _, v in ipairs(proc.listApps()) do
|
||||
if v[2] == "lineclip" then
|
||||
if v[1] ~= proc.aid then
|
||||
A.die()
|
||||
return {}, 1, 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local app = {}
|
||||
local board = ""
|
||||
function app.rpc(srcP, srcD, cmd, txt)
|
||||
if type(cmd) ~= "string" then
|
||||
return ""
|
||||
end
|
||||
if cmd == "copy" then
|
||||
if type(txt) ~= "string" then
|
||||
error("RPC->lineclip: bad text")
|
||||
end
|
||||
board = txt
|
||||
A.resize(unicode.len(board), 1)
|
||||
end
|
||||
if cmd == "paste" then
|
||||
return board
|
||||
end
|
||||
end
|
||||
function app.get_ch(x, y)
|
||||
return unicode.sub(unicode.safeTextFormat(board), x, x)
|
||||
end
|
||||
function app.key(ka, kc, down)
|
||||
if down and ka == ("C"):byte() then
|
||||
A.die()
|
||||
end
|
||||
return false
|
||||
end
|
||||
function app.update()
|
||||
return true
|
||||
end
|
||||
return app, 8, 1
|
19
apps/memusage.lua
Normal file
19
apps/memusage.lua
Normal file
@ -0,0 +1,19 @@
|
||||
local math, stat = A.request("math", "stat")
|
||||
local app = {}
|
||||
app.key = function(ka, kc, down)
|
||||
if ka == ("C"):byte() and down then
|
||||
A.die()
|
||||
end
|
||||
end
|
||||
local strs = {"", "Shift-C to quit."}
|
||||
app.update = function ()
|
||||
local tm = stat.totalMemory()
|
||||
local um = math.floor((tm - stat.freeMemory()) / 1024)
|
||||
strs[1] = um .. ":" .. math.floor(tm / 1024)
|
||||
A.timer(1)
|
||||
return true
|
||||
end
|
||||
app.get_ch = function (x, y)
|
||||
return (strs[y]):sub(x, x)
|
||||
end
|
||||
return app, 16, 2
|
72
apps/modeset.lua
Normal file
72
apps/modeset.lua
Normal file
@ -0,0 +1,72 @@
|
||||
-- resset: resolution changer
|
||||
-- Typed from within.
|
||||
local math, randr = A.request("math", "randr")
|
||||
local app = {}
|
||||
local mW, mH = randr.maxResolution()
|
||||
-- important on 5.3, and 5.3 prevents
|
||||
-- the nasty memory self-destructs!
|
||||
mW = math.floor(mW) mH = math.floor(mH)
|
||||
local sW, sH = randr.getResolution()
|
||||
sW = math.floor(sW) sH = math.floor(sH)
|
||||
local function mkstr(title, w, h)
|
||||
return title .. ":" .. math.floor(w) .. "x" .. math.floor(h)
|
||||
end
|
||||
function app.get_ch(x, y)
|
||||
local strs = {
|
||||
mkstr("cur", randr.getResolution()),
|
||||
mkstr("new", sW, sH)
|
||||
}
|
||||
return strs[y]:sub(x, x)
|
||||
end
|
||||
local function modres(w, h)
|
||||
sW = sW + w
|
||||
sH = sH + h
|
||||
if sW > mW then sW = mW end
|
||||
if sH > mH then sH = mH end
|
||||
if sW < 1 then sW = 1 end
|
||||
if sH < 1 then sH = 1 end
|
||||
return true
|
||||
end
|
||||
function app.key(ka, kc, down)
|
||||
if down then
|
||||
if kc == 200 then
|
||||
return modres(0, -1)
|
||||
end
|
||||
if kc == 208 then
|
||||
return modres(0, 1)
|
||||
end
|
||||
if kc == 203 then
|
||||
return modres(-1, 0)
|
||||
end
|
||||
if kc == 205 then
|
||||
return modres(1, 0)
|
||||
end
|
||||
if ka == 13 then
|
||||
if randr.setResolution(sW, sH) then
|
||||
pcall(function()
|
||||
local f = A.opencfg("w")
|
||||
f.write(sW .. " " .. sH)
|
||||
f.close()
|
||||
end)
|
||||
end
|
||||
return true
|
||||
end
|
||||
if ka == ("C"):byte() then
|
||||
A.die()
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
-- Config stuff!
|
||||
pcall(function()
|
||||
local f = A.opencfg("r")
|
||||
if f then
|
||||
local txt = f.read(64)
|
||||
local nt = txt:gmatch("[0-9]+")
|
||||
sW = math.floor(tonumber(nt()))
|
||||
sH = math.floor(tonumber(nt()))
|
||||
modres(0, 0)
|
||||
f.close()
|
||||
end
|
||||
end)
|
||||
return app, 12, 2
|
352
apps/textedit.lua
Normal file
352
apps/textedit.lua
Normal file
@ -0,0 +1,352 @@
|
||||
-- 'Femto': Text Editor
|
||||
-- Formatting proc. for
|
||||
-- this file is the def.
|
||||
-- size of a textedit win
|
||||
local lang,
|
||||
table,
|
||||
unicode,
|
||||
math,
|
||||
proc =
|
||||
A.request("lang",
|
||||
"table",
|
||||
"unicode",
|
||||
"math",
|
||||
"proc")
|
||||
local lines = {
|
||||
"Femto: Text Editor",
|
||||
"^W : Close, ^S : Save",
|
||||
"^A : Load , ^Q : New.",
|
||||
"^C : Copy Line,",
|
||||
"^V : Paste Line",
|
||||
"^<arrows>: Resize Win",
|
||||
"'^' is Control.",
|
||||
"Now with wide text!",
|
||||
"Yay!"
|
||||
}
|
||||
local linesTranslated =
|
||||
lang.getTable()
|
||||
if linesTranslated then
|
||||
lines = linesTranslated
|
||||
end
|
||||
|
||||
local cursorX = 1
|
||||
local cursorY = 1
|
||||
local cFlash = true
|
||||
local ctrlFlag = false
|
||||
local sW, sH = 25, 8
|
||||
|
||||
local app = {}
|
||||
|
||||
local function splitCur()
|
||||
local s = lines[cursorY]
|
||||
local st = unicode.sub
|
||||
(s, 1, cursorX - 1)
|
||||
local en = unicode.sub
|
||||
(s, cursorX)
|
||||
return st, en
|
||||
end
|
||||
|
||||
local function
|
||||
clampCursorX()
|
||||
local s = lines[cursorY]
|
||||
if unicode.len(s) <
|
||||
(cursorX - 1) then
|
||||
cursorX =
|
||||
unicode.len(s) + 1
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Save/Load
|
||||
local function save()
|
||||
ctrlFlag = false
|
||||
local txt =
|
||||
A.openfile("text", "w")
|
||||
if txt then
|
||||
for k, v in
|
||||
ipairs(lines) do
|
||||
if k ~= 1 then
|
||||
txt.write("\n" .. v)
|
||||
else
|
||||
txt.write(v)
|
||||
end
|
||||
end
|
||||
txt.close()
|
||||
end
|
||||
end
|
||||
local function load()
|
||||
ctrlFlag = false
|
||||
local txt =
|
||||
A.openfile("text", "r")
|
||||
if txt then
|
||||
lines = {}
|
||||
local lb = ""
|
||||
while true do
|
||||
local l = txt.read(64)
|
||||
if not l then
|
||||
table.insert
|
||||
(lines, lb)
|
||||
cursorX = 1
|
||||
cursorY = 1
|
||||
txt.close()
|
||||
return
|
||||
end
|
||||
local lp =
|
||||
l:find("\n")
|
||||
while lp do
|
||||
lb = lb .. l:sub(1,
|
||||
lp - 1)
|
||||
table.insert
|
||||
(lines, lb)
|
||||
lb = ""
|
||||
l = l:sub(lp + 1)
|
||||
lp = l:find("\n")
|
||||
end
|
||||
lb = lb .. l
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function app.get_ch(x, y)
|
||||
-- do rY first since unw
|
||||
-- only requires that
|
||||
-- horizontal stuff be
|
||||
-- messed with...
|
||||
-- ...thankfully
|
||||
local rY = (y + cursorY)
|
||||
- math.floor(sH / 2)
|
||||
|
||||
-- rX is difficult!
|
||||
local rX = 1
|
||||
local Xthold =
|
||||
math.floor(sW / 2)
|
||||
if cursorX > Xthold then
|
||||
rX = rX + (cursorX -
|
||||
Xthold)
|
||||
end
|
||||
local line = lines[rY]
|
||||
if not line then return
|
||||
"¬" end
|
||||
local _, cursorXP =
|
||||
unicode.safeTextFormat(
|
||||
line, cursorX)
|
||||
line, rX =
|
||||
unicode.safeTextFormat(
|
||||
line, rX)
|
||||
|
||||
-- 1-based cambias stuff
|
||||
rX = rX + (x - 1)
|
||||
if rX == cursorXP then
|
||||
if rY == cursorY then
|
||||
if cFlash then
|
||||
return "_"
|
||||
end
|
||||
end
|
||||
end
|
||||
return unicode.sub(line,
|
||||
rX, rX)
|
||||
end
|
||||
-- communicate with the
|
||||
-- "lineclip" clipboard,
|
||||
-- for inter-window copy
|
||||
function lineclip(c, m)
|
||||
for _, v in
|
||||
ipairs(proc.listApps())
|
||||
do
|
||||
if v[2] == "lineclip"
|
||||
then
|
||||
return proc.sendRPC(
|
||||
v[1], c, m)
|
||||
end
|
||||
end
|
||||
local aid = A.launchApp(
|
||||
"lineclip")
|
||||
ctrlFlag = false
|
||||
if aid then return
|
||||
proc.sendRPC(aid, c, m)
|
||||
end
|
||||
return ""
|
||||
end
|
||||
-- add a single character
|
||||
function putLetter(ch)
|
||||
if ch == "\r" then
|
||||
local a, b = splitCur()
|
||||
lines[cursorY] = a
|
||||
table.insert(lines,
|
||||
cursorY + 1, b)
|
||||
cursorY = cursorY + 1
|
||||
cursorX = 1
|
||||
return
|
||||
end
|
||||
local a, b = splitCur()
|
||||
a = a .. ch
|
||||
lines[cursorY] = a .. b
|
||||
cursorX =
|
||||
unicode.len(a) + 1
|
||||
end
|
||||
function app.key(ka, kc,
|
||||
down)
|
||||
if kc == 29 then
|
||||
ctrlFlag = down
|
||||
return false
|
||||
end
|
||||
if ctrlFlag then
|
||||
if not down then
|
||||
return false end
|
||||
if kc == 17 -- W
|
||||
then A.die() end
|
||||
if kc == 200 then
|
||||
sH = sH - 1
|
||||
if sH == 0 then
|
||||
sH = 1 end
|
||||
A.resize(sW, sH)
|
||||
end
|
||||
if kc == 208 then
|
||||
sH = sH + 1
|
||||
A.resize(sW, sH)
|
||||
end
|
||||
if kc == 203 then
|
||||
sW = sW - 1
|
||||
if sW == 0 then
|
||||
sW = 1 end
|
||||
A.resize(sW, sH)
|
||||
end
|
||||
if kc == 205 then
|
||||
sW = sW + 1
|
||||
A.resize(sW, sH)
|
||||
end
|
||||
if kc == 31 -- S
|
||||
then return save() end
|
||||
if kc == 30 -- A
|
||||
then return load() end
|
||||
if kc == 16 -- Q
|
||||
then lines = {""}
|
||||
cursorX = 1
|
||||
cursorY = 1
|
||||
return true end
|
||||
if kc == 46 -- C
|
||||
then lineclip("copy",
|
||||
lines[cursorY]) end
|
||||
if kc == 47 then -- V
|
||||
table.insert(lines,
|
||||
cursorY,
|
||||
lineclip("paste"))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
-- action keys
|
||||
if not down then
|
||||
return false
|
||||
end
|
||||
if kc == 200
|
||||
or kc == 201 then
|
||||
local moveAmount = 1
|
||||
if kc == 201 then
|
||||
moveAmount =
|
||||
math.floor(sH / 2)
|
||||
end
|
||||
cursorY = cursorY -
|
||||
moveAmount
|
||||
if cursorY < 1 then
|
||||
cursorY = 1 end
|
||||
clampCursorX()
|
||||
return true
|
||||
end
|
||||
if kc == 208
|
||||
or kc == 209 then
|
||||
local moveAmount = 1
|
||||
if kc == 209 then
|
||||
moveAmount =
|
||||
math.floor(sH / 2)
|
||||
end
|
||||
cursorY = cursorY +
|
||||
moveAmount
|
||||
if cursorY> #lines then
|
||||
cursorY = #lines end
|
||||
clampCursorX()
|
||||
return true
|
||||
end
|
||||
if kc == 203 then
|
||||
if cursorX > 1 then
|
||||
cursorX = cursorX - 1
|
||||
else
|
||||
if cursorY > 1 then
|
||||
cursorY = cursorY - 1
|
||||
cursorX = unicode.len
|
||||
(lines[cursorY]) + 1
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
if kc == 205 then
|
||||
cursorX = cursorX + 1
|
||||
if clampCursorX() then
|
||||
if cursorY < #lines
|
||||
then
|
||||
cursorY = cursorY + 1
|
||||
cursorX = 1
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
if kc == 199 then
|
||||
cursorX = 1
|
||||
return true
|
||||
end
|
||||
if kc == 207 then
|
||||
cursorX = unicode.len(
|
||||
lines[cursorY]) + 1
|
||||
return true
|
||||
end
|
||||
if ka == 8 then
|
||||
if cursorX == 1 then
|
||||
if cursorY == 1 then
|
||||
return false
|
||||
end
|
||||
local l = table.remove
|
||||
(lines, cursorY)
|
||||
cursorY = cursorY - 1
|
||||
cursorX = unicode.len(
|
||||
lines[cursorY]) + 1
|
||||
lines[cursorY] =
|
||||
lines[cursorY] .. l
|
||||
else
|
||||
local a, b =splitCur()
|
||||
a = unicode.sub(a, 1,
|
||||
unicode.len(a) - 1)
|
||||
lines[cursorY] = a.. b
|
||||
cursorX = cursorX - 1
|
||||
end
|
||||
return true
|
||||
end
|
||||
if ka ~= 0 then
|
||||
putLetter
|
||||
(unicode.char(ka))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
function app.clipboard(t)
|
||||
for i = 1,
|
||||
unicode.len(t) do
|
||||
local c =
|
||||
unicode.sub(t, i, i)
|
||||
if c ~= "\r" then
|
||||
if c == "\n" then
|
||||
c = "\r"
|
||||
end
|
||||
putLetter(c)
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
function app.update()
|
||||
cFlash = not cFlash
|
||||
A.timer(0.5)
|
||||
return true
|
||||
end
|
||||
return app, sW, sH
|
70
filewrap.lua
Normal file
70
filewrap.lua
Normal file
@ -0,0 +1,70 @@
|
||||
-- File Wrapper
|
||||
local fwrap = {}
|
||||
local appTables = {}
|
||||
-- NOTE: May not be error-sandboxed.
|
||||
-- Be careful.
|
||||
function fwrap.appDead(aid)
|
||||
if appTables[aid] then
|
||||
for k, v in ipairs(appTables[aid]) do
|
||||
pcall(function()
|
||||
local prox = component.proxy(v.device)
|
||||
if prox then
|
||||
prox.close(v.handle)
|
||||
end
|
||||
end)
|
||||
end
|
||||
appTables[aid] = nil
|
||||
end
|
||||
end
|
||||
function fwrap.canFree()
|
||||
for _, v in pairs(appTables) do
|
||||
if v then
|
||||
if #v > 0 then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
-- Always error-sandboxed, let errors throw
|
||||
function fwrap.open(aid, path, mode)
|
||||
local finst = {}
|
||||
finst.device = path[1]
|
||||
finst.file = path[2]
|
||||
finst.handle = component.invoke(finst.device, "open", finst.file, mode .. "b")
|
||||
if not appTables[aid] then
|
||||
appTables[aid] = {}
|
||||
end
|
||||
table.insert(appTables, finst)
|
||||
local function closer()
|
||||
pcall(function()
|
||||
component.invoke(finst.device, "close", finst.handle)
|
||||
end)
|
||||
for k, v in ipairs(appTables[aid]) do
|
||||
if v == finst then
|
||||
table.remove(appTables[aid], k)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
if mode == "r" then
|
||||
return {
|
||||
close = closer,
|
||||
read = function (len)
|
||||
if type(len) ~= "number" then error("Length of read must be number") end
|
||||
return component.invoke(finst.device, "read", finst.handle, len)
|
||||
end
|
||||
}
|
||||
end
|
||||
if mode == "w" then
|
||||
return {
|
||||
close = closer,
|
||||
write = function (txt)
|
||||
if type(txt) ~= "string" then error("Write data must be string-bytearray") end
|
||||
return component.invoke(finst.device, "write", finst.handle, txt)
|
||||
end
|
||||
}
|
||||
end
|
||||
error("Bad mode")
|
||||
end
|
||||
return fwrap
|
687
init.lua
Normal file
687
init.lua
Normal file
@ -0,0 +1,687 @@
|
||||
-- KittenOS
|
||||
|
||||
-- ISO 639 language code.
|
||||
local systemLanguage = "en"
|
||||
|
||||
function component.get(tp)
|
||||
local a = component.list(tp, true)()
|
||||
if not a then return nil end
|
||||
return component.proxy(a)
|
||||
end
|
||||
|
||||
local primaryDisk = component.proxy(computer.getBootAddress())
|
||||
local langFile = primaryDisk.open("language", "rb")
|
||||
if langFile then
|
||||
systemLanguage = primaryDisk.read(langFile, 64)
|
||||
primaryDisk.close(langFile)
|
||||
end
|
||||
local function loadfile(s, e)
|
||||
local h = primaryDisk.open(s)
|
||||
if h then
|
||||
local ch = ""
|
||||
local c = primaryDisk.read(h, 256)
|
||||
while c do
|
||||
ch = ch .. c
|
||||
c = primaryDisk.read(h, 256)
|
||||
end
|
||||
primaryDisk.close(h)
|
||||
return load(ch, "=" .. s, "t", e)
|
||||
end
|
||||
return nil, "File Unreadable"
|
||||
end
|
||||
|
||||
-- Must be a sane source of time in seconds.
|
||||
local function saneTime()
|
||||
return computer.uptime()
|
||||
end
|
||||
local primaryScreen = component.get("screen")
|
||||
local primaryGPU = component.get("gpu")
|
||||
|
||||
primaryGPU.bind(primaryScreen.address)
|
||||
local scrW, scrH = 50, 16
|
||||
local frameH = 1
|
||||
local redrawWorldSoon = true
|
||||
primaryGPU.setResolution(scrW, scrH)
|
||||
primaryGPU.setBackground(0x000000)
|
||||
primaryGPU.setForeground(0xFFFFFF)
|
||||
primaryGPU.fill(1, 1, scrW, scrH, "#")
|
||||
-- apps maps aid (Running application ID, like Process ID) to app data
|
||||
-- appZ handles Z-order
|
||||
local apps = {}
|
||||
local appZ = {}
|
||||
local function sanech(ch, bch)
|
||||
if not bch then bch = " " end
|
||||
if not ch then return bch end
|
||||
if unicode.len(ch) ~= 1 then return bch end
|
||||
return ch
|
||||
end
|
||||
|
||||
-- aid
|
||||
local launchApp = nil
|
||||
-- text, die
|
||||
local dialogApp = nil
|
||||
-- aid, pkg, txt
|
||||
local openDialog = nil
|
||||
-- Component of the system that handles file access,
|
||||
-- does cleanup after dead apps, etc.
|
||||
local fileWrapper = nil
|
||||
local drawing = false
|
||||
local function handleEv(aid, evt, ...)
|
||||
local f = apps[aid].i[evt]
|
||||
if not f then return end
|
||||
local r2 = {pcall(f, ...)}
|
||||
if r2[1] then
|
||||
return select(2, table.unpack(r2))
|
||||
else
|
||||
-- possible error during death
|
||||
if apps[aid] then
|
||||
-- error, override instance immediately.
|
||||
local i, w, h = dialogApp(r2[2], apps[aid].A.die)
|
||||
-- REALLY BAD STUFF (not much choice)
|
||||
local od = drawing
|
||||
drawing = false
|
||||
apps[aid].i = i
|
||||
--apps[aid].i = {}
|
||||
apps[aid].A.resize(w, h)
|
||||
drawing = od
|
||||
else
|
||||
openDialog("error-" .. aid, "*error-during-death", r2[2])
|
||||
end
|
||||
end
|
||||
end
|
||||
local needRedraw = {}
|
||||
local function handleEvNRD(aid, ...)
|
||||
local doRedraw = handleEv(aid, ...)
|
||||
if doRedraw then needRedraw[aid] = true end
|
||||
end
|
||||
-- This function is critical to wide text support.
|
||||
-- The measures taken below mean that the *system* can deal with wide text,
|
||||
-- but applications need to have the same spacing rules in place.
|
||||
-- Since these spacing rules,
|
||||
-- and the ability to get a wide-char point from a "normal" point,
|
||||
-- are probably beneficial to everybody anyway,
|
||||
-- they're exposed here as a unicode function.
|
||||
function unicode.safeTextFormat(s, ptr)
|
||||
local res = ""
|
||||
if not ptr then ptr = 1 end
|
||||
local aptr = 1
|
||||
for i = 1, unicode.len(s) do
|
||||
local ch = unicode.sub(s, i, i)
|
||||
local ex = unicode.charWidth(ch)
|
||||
if i < ptr then
|
||||
aptr = aptr + ex
|
||||
end
|
||||
for j = 2, ex do
|
||||
ch = ch .. " "
|
||||
end
|
||||
res = res .. ch
|
||||
end
|
||||
return res, aptr
|
||||
end
|
||||
-- Do not let wide characters cause weirdness outside home window!!!
|
||||
local function wideCharSpillFilter(ch, x, doNotTouch)
|
||||
if (x + 1) == doNotTouch then
|
||||
if unicode.isWide(ch) then
|
||||
return "%"
|
||||
end
|
||||
end
|
||||
return ch
|
||||
end
|
||||
local function getChar(x, y)
|
||||
-- Note: The colours used here tend to
|
||||
-- game the autoselect so this works
|
||||
-- without any depth nastiness.
|
||||
for i = 1, #appZ do
|
||||
local k = appZ[(#appZ + 1) - i]
|
||||
local title = unicode.safeTextFormat(k)
|
||||
local v = apps[k]
|
||||
if x >= v.x and x < (v.x + v.w) then
|
||||
if y == v.y then
|
||||
local bgc = 0x80FFFF
|
||||
local bch = "-"
|
||||
if i == 1 then bgc = 0x808080 bch = "+" end
|
||||
local ch = sanech(unicode.sub(title, (x - v.x) + 1, (x - v.x) + 1), bch)
|
||||
return 0x000000, bgc, wideCharSpillFilter(ch, x, v.x + v.w)
|
||||
else
|
||||
if y > v.y and y < (v.y + v.h + frameH) then
|
||||
-- get char from app
|
||||
local ch = sanech(handleEv(k, "get_ch", (x - v.x) + 1, (y - (v.y + frameH)) + 1))
|
||||
return 0xFFFFFF, 0x000000, wideCharSpillFilter(ch, x, v.x + v.w)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return 0xFFFFFF, 0x000000, " "
|
||||
end
|
||||
local function failDrawing()
|
||||
if drawing then error("Cannot call when drawing.") end
|
||||
end
|
||||
local function redrawSection(x, y, w, h)
|
||||
drawing = true
|
||||
primaryGPU.setBackground(0x000000)
|
||||
primaryGPU.setForeground(0xFFFFFF)
|
||||
local cfg, cbg = 0xFFFFFF, 0x000000
|
||||
--primaryGPU.fill(x, y, w, h, " ")
|
||||
for ly = 1, h do
|
||||
local buf = ""
|
||||
local bufX = x
|
||||
-- Wide characters are annoying.
|
||||
local wideCharacterAdvance = 0
|
||||
for lx = 1, w do
|
||||
if wideCharacterAdvance == 0 then
|
||||
local fg, bg, tx = getChar(x + (lx - 1), y + (ly - 1))
|
||||
local flush = false
|
||||
if fg ~= cfg then flush = true end
|
||||
if bg ~= cbg then flush = true end
|
||||
if flush then
|
||||
if buf:len() > 0 then
|
||||
primaryGPU.set(bufX, y + (ly - 1), buf)
|
||||
end
|
||||
buf = ""
|
||||
bufX = x + (lx - 1)
|
||||
end
|
||||
if fg ~= cfg then primaryGPU.setForeground(fg) cfg = fg end
|
||||
if bg ~= cbg then primaryGPU.setBackground(bg) cbg = bg end
|
||||
buf = buf .. tx
|
||||
wideCharacterAdvance = unicode.charWidth(tx) - 1
|
||||
else
|
||||
-- nothing to add to buffer, since the extra "letters" don't count
|
||||
wideCharacterAdvance = wideCharacterAdvance - 1
|
||||
end
|
||||
end
|
||||
primaryGPU.set(bufX, y + (ly - 1), buf)
|
||||
end
|
||||
drawing = false
|
||||
end
|
||||
local function redrawApp(aid, onlyBar)
|
||||
local h = frameH
|
||||
if not onlyBar then h = h + apps[aid].h end
|
||||
redrawSection(apps[aid].x, apps[aid].y, apps[aid].w, h)
|
||||
end
|
||||
local function hideApp(aid, deferRedraw)
|
||||
local function sk()
|
||||
for ri, v in ipairs(appZ) do
|
||||
if v == aid then table.remove(appZ, ri) return end
|
||||
end
|
||||
end
|
||||
sk()
|
||||
if not deferRedraw then
|
||||
redrawApp(aid)
|
||||
local newFocus = appZ[#appZ]
|
||||
if newFocus then redrawApp(newFocus, true) end
|
||||
end
|
||||
return {apps[aid].x, apps[aid].y, apps[aid].w, apps[aid].h + frameH}
|
||||
end
|
||||
local function focusApp(aid, focusUndrawn)
|
||||
hideApp(aid, true) -- just ensure it's not on stack, no need to redraw as it won't move
|
||||
local lastFocus = appZ[#appZ]
|
||||
table.insert(appZ, aid)
|
||||
-- make the focus indicator disappear should one exist.
|
||||
-- focusUndrawn indicates that the focus was transient and never got drawn
|
||||
if lastFocus and (not focusUndrawn) then redrawApp(lastFocus, true) end
|
||||
-- Finally, make absolutely sure the application is shown on the screen
|
||||
redrawApp(aid)
|
||||
end
|
||||
local function moveApp(aid, x, y)
|
||||
local section = hideApp(aid, true) -- remove from stack, do NOT redraw
|
||||
apps[aid].x = x -- (prevents interim focus weirdness)
|
||||
apps[aid].y = y
|
||||
-- put back on stack, redrawing destination, but not the
|
||||
-- interim focus target (since we made sure NOT to redraw that)
|
||||
focusApp(aid, true)
|
||||
redrawSection(table.unpack(section)) -- handle source cleanup
|
||||
end
|
||||
local function ofsApp(aid, x, y)
|
||||
moveApp(aid, apps[aid].x + x, apps[aid].y + y)
|
||||
end
|
||||
local function killApp(aid)
|
||||
hideApp(aid)
|
||||
apps[aid] = nil
|
||||
if fileWrapper then
|
||||
fileWrapper.appDead(aid)
|
||||
if fileWrapper.canFree() then
|
||||
fileWrapper = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local getLCopy = nil
|
||||
function getLCopy(t)
|
||||
if type(t) == "table" then
|
||||
local t2 = {}
|
||||
setmetatable(t2, {__index = function(a, k) return getLCopy(t[k]) end})
|
||||
return t2
|
||||
else
|
||||
return t
|
||||
end
|
||||
end
|
||||
-- Used to ensure the "primary" device is safe
|
||||
-- while allowing complete control otherwise.
|
||||
local function omittingComponentL(o, t)
|
||||
local i = component.list(t, true)
|
||||
return function()
|
||||
local ii = i()
|
||||
if ii == o then ii = i() end
|
||||
if not ii then return nil end
|
||||
return component.proxy(ii)
|
||||
end
|
||||
end
|
||||
-- Allows for simple "Control any of these connected to the system" APIs,
|
||||
-- for things the OS shouldn't be poking it's nose in.
|
||||
local function basicComponentSW(t, primary)
|
||||
return {
|
||||
list = function()
|
||||
local i = component.list(t, true)
|
||||
return function ()
|
||||
local ii = i()
|
||||
if not ii then return nil end
|
||||
return component.proxy(ii)
|
||||
end
|
||||
end,
|
||||
primary = primary
|
||||
}
|
||||
end
|
||||
local function getAPI(s, cAid, cPkg, access)
|
||||
if s == "math" then return getLCopy(math) end
|
||||
if s == "table" then return getLCopy(table) end
|
||||
if s == "string" then return getLCopy(string) end
|
||||
if s == "unicode" then return getLCopy(unicode) end
|
||||
if s == "root" then return _ENV end
|
||||
if s == "stat" then return {
|
||||
totalMemory = computer.totalMemory,
|
||||
freeMemory = computer.freeMemory,
|
||||
energy = computer.energy,
|
||||
maxEnergy = computer.maxEnergy,
|
||||
clock = os.clock,
|
||||
date = os.date,
|
||||
difftime = os.difftime,
|
||||
time = os.time,
|
||||
componentList = component.list
|
||||
} end
|
||||
if s == "proc" then return {
|
||||
aid = cAid,
|
||||
pkg = cPkg,
|
||||
listApps = function ()
|
||||
local t = {}
|
||||
local t2 = {}
|
||||
for k, v in pairs(apps) do
|
||||
table.insert(t, k)
|
||||
t2[k] = v.pkg
|
||||
end
|
||||
table.sort(t)
|
||||
local t3 = {}
|
||||
for k, v in ipairs(t) do
|
||||
t3[k] = {v, t2[v]}
|
||||
end
|
||||
return t3
|
||||
end,
|
||||
sendRPC = function (aid, ...)
|
||||
if type(aid) ~= "string" then error("aid must be string") end
|
||||
if not apps[aid] then error("RPC target does not exist.") end
|
||||
return handleEv(aid, "rpc", cPkg, cAid, ...)
|
||||
end
|
||||
} end
|
||||
if s == "lang" then return {
|
||||
getLanguage = function ()
|
||||
return systemLanguage
|
||||
end,
|
||||
getTable = function ()
|
||||
local ca, cb = loadfile("lang/" .. systemLanguage .. "/" .. cPkg .. ".lua", {})
|
||||
if not ca then return nil, cb end
|
||||
ca, cb = pcall(ca)
|
||||
if not ca then return nil, cb end
|
||||
return cb
|
||||
end
|
||||
} end
|
||||
if s == "setlang" then return function (lang)
|
||||
if type(lang) ~= "string" then error("Language must be string") end
|
||||
systemLanguage = lang
|
||||
pcall(function ()
|
||||
local langFile = primaryDisk.open("language", "wb")
|
||||
if langFile then
|
||||
primaryDisk.write(langFile, systemLanguage)
|
||||
primaryDisk.close(langFile)
|
||||
end
|
||||
end)
|
||||
end end
|
||||
if s == "kill" then return {
|
||||
killApp = function (aid)
|
||||
if type(aid) ~= "string" then error("aid must be string") end
|
||||
if apps[aid] then killApp(aid) end
|
||||
end
|
||||
} end
|
||||
if s == "randr" then return {
|
||||
getResolution = primaryGPU.getResolution,
|
||||
maxResolution = primaryGPU.maxResolution,
|
||||
setResolution = function (w, h)
|
||||
failDrawing()
|
||||
if primaryGPU.setResolution(w, h) then
|
||||
scrW = w
|
||||
scrH = h
|
||||
redrawWorldSoon = true
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
iterateScreens = function()
|
||||
-- List all screens, but do NOT give a primary screen.
|
||||
return omittingComponentL(primaryScreen.address, "screen")
|
||||
end,
|
||||
iterateGPUs = function()
|
||||
-- List all GPUs, but do NOT give a primary GPU.
|
||||
return omittingComponentL(primaryGPU.address, "gpu")
|
||||
end
|
||||
} end
|
||||
|
||||
if s == "c.modem" then access["s.modem_message"] = true end
|
||||
if s == "c.tunnel" then access["s.modem_message"] = true end
|
||||
if s == "c.chat_box" then access["s.chat_message"] = true end
|
||||
|
||||
if s == "c.filesystem" then return basicComponentSW("filesystem", primaryDisk) end
|
||||
if s == "c.screen" then return basicComponentSW("screen", primaryScreen) end
|
||||
if s == "c.gpu" then return basicComponentSW("gpu", primaryGPU) end
|
||||
if s:sub(1, 2) == "c." then return basicComponentSW(s:sub(3)) end
|
||||
return nil
|
||||
end
|
||||
local function launchAppCore(aid, pkg, f)
|
||||
if apps[aid] then return end
|
||||
local function fD()
|
||||
-- stops potentially nasty situations
|
||||
if not apps[aid] then error("App already dead") end
|
||||
end
|
||||
local A = {
|
||||
listApps = function (a) fD()
|
||||
local appList = {}
|
||||
for _, v in ipairs(primaryDisk.list("apps")) do
|
||||
if v:sub(v:len() - 3) == ".lua" then
|
||||
table.insert(appList, v:sub(1, v:len() - 4))
|
||||
end
|
||||
end
|
||||
return appList
|
||||
end,
|
||||
launchApp = function (a) fD()
|
||||
if type(a) ~= "string" then error("App IDs are strings") end
|
||||
if a:gmatch("[a-zA-Z%-_\x80-\xFF]+")() ~= a then error("App '" .. a .. "' does not seem sane") end
|
||||
failDrawing()
|
||||
return launchApp(a)
|
||||
end,
|
||||
opencfg = function (openmode) fD()
|
||||
if type(openmode) ~= "string" then
|
||||
error("Openmode must be nil or string.")
|
||||
end
|
||||
local ok = false
|
||||
if openmode == "r" then ok = true end
|
||||
if openmode == "w" then ok = true end
|
||||
if not ok then error("Bad openmode.") end
|
||||
if not fileWrapper then
|
||||
fileWrapper = loadfile("filewrap.lua", _ENV)()
|
||||
end
|
||||
return fileWrapper.open(aid, {primaryDisk.address, "cfgs/" .. pkg}, openmode)
|
||||
end,
|
||||
openfile = function (filetype, openmode) fD()
|
||||
if openmode ~= nil then if type(openmode) ~= "string" then
|
||||
error("Openmode must be nil or string.")
|
||||
end end
|
||||
if type(filetype) ~= "string" then error("Filetype must be string.") end
|
||||
filetype = aid .. ": " .. filetype
|
||||
failDrawing()
|
||||
redrawWorldSoon = true
|
||||
local rs, rt = pcall(function()
|
||||
if fileWrapper then
|
||||
if fileWrapper.canFree() then
|
||||
fileWrapper = nil
|
||||
end
|
||||
end
|
||||
local r = loadfile("tfilemgr.lua", _ENV)(filetype, openmode, primaryGPU)
|
||||
if r and openmode then
|
||||
if not fileWrapper then
|
||||
fileWrapper = loadfile("filewrap.lua", _ENV)()
|
||||
end
|
||||
return fileWrapper.open(aid, r, openmode) -- 'r' is table {drive, dir}
|
||||
end
|
||||
end)
|
||||
if not rs then
|
||||
openDialog("*fmgr", "FMerr/" .. aid, rt)
|
||||
else
|
||||
return rt
|
||||
end
|
||||
end,
|
||||
request = function (...) fD()
|
||||
failDrawing()
|
||||
local requests = {...}
|
||||
-- If the same process requests a permission twice,
|
||||
-- just let it.
|
||||
-- "needed" is true if we still need permission.
|
||||
-- first pass confirms we need permission,
|
||||
-- second pass asks for it
|
||||
local needed = false
|
||||
for _, v in ipairs(requests) do
|
||||
if type(v) == "string" then
|
||||
if not apps[aid].hasAccess[v] then
|
||||
needed = true
|
||||
end
|
||||
if apps[aid].denyAccess[v] then
|
||||
return nil -- Don't even bother.
|
||||
end
|
||||
end
|
||||
end
|
||||
if needed then
|
||||
local r, d = loadfile("policykit.lua", _ENV)(primaryGPU, pkg, requests)
|
||||
if r then
|
||||
needed = false
|
||||
end
|
||||
if not d then
|
||||
redrawWorldSoon = true
|
||||
end
|
||||
end
|
||||
local results = {}
|
||||
for _, v in ipairs(requests) do
|
||||
if type(v) == "string" then
|
||||
if not needed then
|
||||
table.insert(results, getAPI(v, aid, pkg, apps[aid].hasAccess))
|
||||
apps[aid].hasAccess[v] = true
|
||||
else
|
||||
apps[aid].denyAccess[v] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return table.unpack(results)
|
||||
end,
|
||||
timer = function (ud) fD()
|
||||
if type(ud) ~= "number" then error("Timer must take number.") end
|
||||
failDrawing()
|
||||
if ud > 0 then
|
||||
apps[aid].nextUpdate = saneTime() + ud
|
||||
else
|
||||
apps[aid].nextUpdate = nil
|
||||
end
|
||||
end,
|
||||
resize = function (w, h) fD()
|
||||
if type(w) ~= "number" then error("Width must be number.") end
|
||||
if type(h) ~= "number" then error("Height must be number.") end
|
||||
w = math.floor(w)
|
||||
h = math.floor(h)
|
||||
if w < 1 then w = 1 end
|
||||
if h < 1 then h = 1 end
|
||||
failDrawing()
|
||||
local dW = apps[aid].w
|
||||
local dH = apps[aid].h
|
||||
if dW < w then dW = w end
|
||||
if dH < h then dH = h end
|
||||
apps[aid].w = w
|
||||
apps[aid].h = h
|
||||
redrawSection(apps[aid].x, apps[aid].y, dW, dH + frameH)
|
||||
end,
|
||||
die = function () fD()
|
||||
failDrawing()
|
||||
killApp(aid)
|
||||
end
|
||||
}
|
||||
apps[aid] = {}
|
||||
apps[aid].pkg = pkg
|
||||
local iDummy = {} -- Dummy object to keep app valid during init.
|
||||
apps[aid].i = iDummy
|
||||
apps[aid].A = A
|
||||
apps[aid].nextUpdate = saneTime()
|
||||
apps[aid].hasAccess = {}
|
||||
apps[aid].denyAccess = {}
|
||||
apps[aid].x = 1
|
||||
apps[aid].y = 1
|
||||
apps[aid].w = 1
|
||||
apps[aid].h = 1
|
||||
local i, w, h = f(A)
|
||||
if apps[aid] then
|
||||
-- If the app triggered an error handler,
|
||||
-- then the instance could be replaced, make this act OK
|
||||
if apps[aid].i == iDummy then
|
||||
apps[aid].i = i
|
||||
apps[aid].w = w
|
||||
apps[aid].h = h
|
||||
end
|
||||
focusApp(aid)
|
||||
return aid
|
||||
end
|
||||
-- App self-destructed
|
||||
end
|
||||
function launchApp(pkg)
|
||||
local aid = 0
|
||||
while apps[pkg .. "-" .. aid] do aid = aid + 1 end
|
||||
aid = pkg .. "-" .. aid
|
||||
return launchAppCore(aid, pkg, function (A)
|
||||
local f, fe = loadfile("apps/" .. pkg .. ".lua", {
|
||||
A = A,
|
||||
assert = assert, ipairs = ipairs,
|
||||
load = load, next = next,
|
||||
pairs = pairs, pcall = pcall,
|
||||
xpcall = xpcall, rawequal = rawequal,
|
||||
rawget = rawget, rawlen = rawlen,
|
||||
rawset = rawset, select = select,
|
||||
type = type, error = error,
|
||||
tonumber = tonumber, tostring = tostring
|
||||
})
|
||||
if not f then
|
||||
return dialogApp(fe, A.die)
|
||||
end
|
||||
local ok, app, ww, wh = pcall(f)
|
||||
if ok and ww and wh then
|
||||
ww = math.floor(ww)
|
||||
wh = math.floor(wh)
|
||||
if ww < 1 then ww = 1 end
|
||||
if wh < 1 then wh = 1 end
|
||||
return app, ww, wh
|
||||
end
|
||||
if ok and not ww then app = "No Size" end
|
||||
if ok and not wh then app = "No Size" end
|
||||
return dialogApp(app, A.die)
|
||||
end)
|
||||
end
|
||||
-- emergency dialog app
|
||||
function dialogApp(fe, die)
|
||||
fe = tostring(fe)
|
||||
local ww, wh = 32, 1
|
||||
wh = math.floor(unicode.len(fe) / ww) + 1
|
||||
return {
|
||||
key = function (ka, kc, down) if ka == 13 and down then die() end end,
|
||||
update = function() end, get_ch = function (x, y)
|
||||
local p = x + ((y - 1) * ww)
|
||||
return unicode.sub(fe, p, p)
|
||||
end
|
||||
}, ww, wh
|
||||
end
|
||||
function openDialog(pkg, aid, txt)
|
||||
launchAppCore(pkg, aid, function (A) return dialogApp(txt, A.die) end)
|
||||
end
|
||||
|
||||
-- Perhaps outsource this to a file???
|
||||
openDialog("Welcome to KittenOS", "~welcome",
|
||||
--2345678901234567890123456789012
|
||||
"Alt-(arrow key): Move window. " ..
|
||||
"Alt-Enter: Start 'launcher'. " ..
|
||||
"Shift-C will generally stop apps" ..
|
||||
" which don't care about text, or" ..
|
||||
" don't want any text right now. " ..
|
||||
"Tab: Switch window.")
|
||||
|
||||
-- main WM
|
||||
local isAltDown = false
|
||||
local function key(ka, kc, down)
|
||||
local focus = appZ[#appZ]
|
||||
if kc == 56 then isAltDown = down end
|
||||
if isAltDown then
|
||||
if kc == 200 then
|
||||
if focus and down then ofsApp(focus, 0, -1) end return
|
||||
end
|
||||
if kc == 208 then
|
||||
if focus and down then ofsApp(focus, 0, 1) end return
|
||||
end
|
||||
if kc == 203 then
|
||||
if focus and down then ofsApp(focus, -1, 0) end return
|
||||
end
|
||||
if kc == 205 then
|
||||
if focus and down then ofsApp(focus, 1, 0) end return
|
||||
end
|
||||
if kc == 46 then
|
||||
if focus and down then killApp(focus) end return
|
||||
end
|
||||
if ka == 13 then
|
||||
if down then launchApp("launcher") end return
|
||||
end
|
||||
end
|
||||
if kc == 15 then
|
||||
if focus and down then focusApp(appZ[1]) end
|
||||
return
|
||||
end
|
||||
if focus then
|
||||
handleEvNRD(focus, "key", ka, kc, down)
|
||||
end
|
||||
end
|
||||
|
||||
while true do
|
||||
local maxTime = 480
|
||||
local now = saneTime()
|
||||
for k, v in pairs(apps) do
|
||||
if v.nextUpdate then
|
||||
local timeIn = v.nextUpdate - now
|
||||
if timeIn <= 0 then
|
||||
v.nextUpdate = nil
|
||||
handleEvNRD(k, "update")
|
||||
if v.nextUpdate then
|
||||
timeIn = v.nextUpdate - now
|
||||
end
|
||||
end
|
||||
if timeIn > 0 then
|
||||
maxTime = math.min(maxTime, timeIn)
|
||||
end
|
||||
end
|
||||
end
|
||||
for k, v in pairs(needRedraw) do if v then
|
||||
if apps[k] and not redrawWorldSoon then
|
||||
redrawApp(k)
|
||||
end
|
||||
needRedraw[k] = nil
|
||||
end end
|
||||
if redrawWorldSoon then
|
||||
redrawWorldSoon = false
|
||||
redrawSection(1, 1, scrW, scrH)
|
||||
end
|
||||
local signal = {computer.pullSignal(maxTime)}
|
||||
local t, p1, p2, p3, p4 = table.unpack(signal)
|
||||
if t then
|
||||
for k, v in pairs(apps) do
|
||||
if v.hasAccess["root"] or v.hasAccess["s." .. t] then
|
||||
handleEvNRD(v, "event", table.unpack(signal))
|
||||
end
|
||||
end
|
||||
if t == "key_down" then
|
||||
key(p2, p3, true)
|
||||
end
|
||||
if t == "key_up" then
|
||||
key(p2, p3, false)
|
||||
end
|
||||
if t == "clipboard" then
|
||||
local focus = appZ[#appZ]
|
||||
if focus then
|
||||
handleEvNRD(focus, "clipboard", p2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
16
lang/pirate/installer.lua
Normal file
16
lang/pirate/installer.lua
Normal file
@ -0,0 +1,16 @@
|
||||
return {
|
||||
["KittenOS Installer"] = "Kitt'n Ohess Install'a!",
|
||||
["Applications to install:"] = "Sails to fit:",
|
||||
["Install Application: "] = "Fit Sail: ",
|
||||
["true"] = "Yar",
|
||||
["false"] = "Nay",
|
||||
["<Confirm>"] = "<Continu' with ye fittin'!>",
|
||||
["Filesystem to target:"] = "Island to land cargo:",
|
||||
["Are you sure you want to install to FS:"] = "Are 'ya sure you wanna land on:",
|
||||
["These applications will be installed:"] = "These sails will be fitted:",
|
||||
["<Yes>"] = "<Aye!>",
|
||||
["<No, change settings>"] = "<Nay, I changed me mind>",
|
||||
["Installing."] = "Sailin'!",
|
||||
["Installation complete."] = "Arrived at island!",
|
||||
["Press Shift-C to leave."] = "Push Shift-Cee to disembark!"
|
||||
}
|
14
lang/pirate/textedit.lua
Normal file
14
lang/pirate/textedit.lua
Normal file
@ -0,0 +1,14 @@
|
||||
return {
|
||||
"Femto, a scribe for ye future!",
|
||||
"The wheels are as follows:",
|
||||
"Control and ye arrow keys",
|
||||
" change the porthole size...",
|
||||
"Control and C copies a line,",
|
||||
"Control and V pastes a line.",
|
||||
"Control and Q wipes the parchment,",
|
||||
"Control and W sinks the ship.",
|
||||
"Control and A takes a parchment from ye logs,",
|
||||
"Control and S puts the parchment back.",
|
||||
"Now supports wide scrawlin's.",
|
||||
"For example, 'Yay!'.",
|
||||
}
|
80
policykit.lua
Normal file
80
policykit.lua
Normal file
@ -0,0 +1,80 @@
|
||||
local gpu, aid, requests = ...
|
||||
if #requests == 1 then
|
||||
local permits = {
|
||||
-- This is a list of specific permits for specific known apps.
|
||||
-- Do not put an app here lightly - find another way.
|
||||
}
|
||||
end
|
||||
if aid == "launcher" then return true, true end
|
||||
local restrictions = {
|
||||
-- | |
|
||||
["root"] = "Completely, absolutely control the device.",
|
||||
["randr"] = "Control displays and GPUs.", -- not precisely true but close enough
|
||||
["stat"] = "Read energy, sage, memory usage and time.",
|
||||
["setlang"] = "Change the system language.",
|
||||
["kill"] = "Kill other processes.",
|
||||
["c.filesystem"] = "Access filesystems directly (virus risk!).",
|
||||
["c.drive"] = "Access unmanaged drives directly.",
|
||||
["c.modem"] = "Send to and receive from the network.",
|
||||
["c.tunnel"] = "Use Linked Cards, receive from all modems.",
|
||||
["s.modem_message"] = "Listen to all network messages.",
|
||||
["c.internet"] = "Connect to the real-life Internet.",
|
||||
["c.robot"] = "Control the 'robot' abilities.",
|
||||
["c.drone"] = "Control the 'drone' abilities.",
|
||||
["c.redstone"] = "Control Redstone Cards and I/O Blocks.",
|
||||
["c.screen"] = "Screw up screens directly. <USE RANDR!!!>",
|
||||
["c.gpu"] = "Screw up GPUs directly. <USE RANDR!!!>",
|
||||
["c.eeprom"] = "Modify EEPROMs. Extremely dangerous.",
|
||||
["c.debug"] = "Modify the game world. Beyond dangerous.",
|
||||
["c.printer3d"] = "Use connected 3D Printers.",
|
||||
-- disk_drive seems safe enough, same with keyboard
|
||||
["s.key_down"] = "Potentially act as a keylogger. (down)",
|
||||
["s.key_up"] = "Potentially act as a keylogger. (up)",
|
||||
-- COMPUTRONICS
|
||||
["c.chat_box"] = "Listen and talk to players.",
|
||||
["s.chat_message"] = "Listen in on players talking."
|
||||
}
|
||||
local centre = ""
|
||||
local sW, sH = gpu.getResolution()
|
||||
for i = 1, math.floor((sW / 2) - 7) do
|
||||
centre = centre .. " "
|
||||
end
|
||||
local text = {
|
||||
centre .. "Security Alert",
|
||||
"",
|
||||
" '" .. aid .. "' would like to:",
|
||||
"",
|
||||
}
|
||||
local automaticOK = true
|
||||
for _, v in ipairs(requests) do
|
||||
if v ~= nil then
|
||||
if type(v) == "string" then
|
||||
if restrictions[v] then
|
||||
automaticOK = false
|
||||
table.insert(text, " + " .. restrictions[v])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Nothing restricted.
|
||||
if automaticOK then return true, true end
|
||||
table.insert(text, "")
|
||||
table.insert(text, " If you agree, press 'y', else 'n'.")
|
||||
gpu.setForeground(0xFFFFFF)
|
||||
gpu.setBackground(0x000000)
|
||||
gpu.fill(1, 1, sW, sH, " ")
|
||||
for k, v in ipairs(text) do
|
||||
gpu.set(1, k, v)
|
||||
end
|
||||
text = nil
|
||||
while true do
|
||||
local t, p1, p2, p3, p4 = computer.pullSignal()
|
||||
if t == "key_down" then
|
||||
if p2 == ("y"):byte() then
|
||||
return true, false
|
||||
end
|
||||
if p2 == ("n"):byte() then
|
||||
return false, false
|
||||
end
|
||||
end
|
||||
end
|
127
readme.md
Normal file
127
readme.md
Normal file
@ -0,0 +1,127 @@
|
||||
# KittenOS: A graphical OpenComputers OS that runs light.
|
||||
|
||||
## Why?
|
||||
Because OpenOS needs *two* Tier 1 memory chips to run a text editor,
|
||||
in a basic console environment that reminds me of DOS more than a Unix,
|
||||
despite the inspirations.
|
||||
This OS only needs one Tier 1 memory chip - that's 192KiB...
|
||||
|
||||
## Couldn't save a file from a text editor on a 192KiB system.
|
||||
|
||||
Switch to Lua 5.3 - Lua 5.2 has a nasty habit of leaking a big chunk of memory.
|
||||
|
||||
Given this hasn't happened with Lua 5.3, I can only guess this is an issue with 5.2.
|
||||
|
||||
(Tested on OpenComputers 1.6.0.7-rc.1 on Minecraft 1.7.10.
|
||||
Judging by the "collectgarbage" call in machine.lua,
|
||||
they may have the same problem???)
|
||||
|
||||
## Why the complicated permissions and sandboxing?
|
||||
|
||||
Because I wanted to try out some security ideas, like not letting
|
||||
applications access files unless the user explicitly authorizes access
|
||||
to that file. Aka "ransomware prevention".
|
||||
|
||||
Yes, I know it uses memory, but I actually had *too much* memory.
|
||||
|
||||
## Why is the kernel so big and yet in one file?
|
||||
|
||||
File overhead is one of the things I suspect caused OpenOS's bloat.
|
||||
(It helps that writing the kernel like this reduces boot time -
|
||||
only one file has to be loaded for the system to boot.
|
||||
More files are required to use some functions which are used relatively
|
||||
rarely - like the file manager - as a tradeoff on memory.)
|
||||
|
||||
## Why does get_ch exist? Why not use a window buffer?
|
||||
|
||||
Because memory.
|
||||
This way, 80x25 and 160x50 should use around about the same amount of memory.
|
||||
It also fit nicely with the way only sections of the screen are drawn.
|
||||
|
||||
## Why do the window titlebars look inverted on Tier 3 hardware, but not Tier 2?
|
||||
|
||||
There was a bit of value trickery required to get the same general effect on all 3 tiers.
|
||||
|
||||
## Why do I have to return true in so many functions if I want a redraw?
|
||||
|
||||
Because if you redraw all the time, you use more energy.
|
||||
|
||||
(Though energy's really a secondary priority to memory, costs resources
|
||||
all the same.)
|
||||
|
||||
## Why aren't (partial redraws/GPU copys/GPU copys for moving windows) supported?
|
||||
|
||||
They didn't seem to be a requirement once I optimized the GPU call count.
|
||||
|
||||
If the system had still been running slow after that, I'd have done it.
|
||||
|
||||
## Why does the text editor use up so much (energy/time)?
|
||||
|
||||
The text editor is probably one of the bigger windows in KittenOS.
|
||||
Try making it smaller with the controls it gives.
|
||||
|
||||
## What's "lineclip"?
|
||||
|
||||
A poor excuse for a clipboard.
|
||||
You don't need to interact with it,
|
||||
though *Shift-C* on it kills it (thus clearing the clipboard).
|
||||
|
||||
## Why is *Shift-C* used everywhere to safely close things?
|
||||
|
||||
Because Enter is used as an action button, Escape's right out,
|
||||
Delete's probably missing on keyboards by now,
|
||||
*Ctrl-C* is also copy, and anything with Alt in it
|
||||
is supposed to be a Window Manager trap.
|
||||
|
||||
## Why is there no "kill it" button?
|
||||
|
||||
I'm sticking it here so that people don't make using this a habit -
|
||||
so that application developers can do something before app death.
|
||||
|
||||
If you must ask, *Alt-C* will kill the currently focused app.
|
||||
|
||||
## An app infinite-looped / ate memory and the system died.
|
||||
|
||||
Yep. There isn't much of a way to protect against memory-eaters.
|
||||
On the other hand, you do know not to start that app again, and it
|
||||
didn't get a chance to do harm.
|
||||
|
||||
## Isn't building a window manager into the OS kind of, uh, monolithic?
|
||||
|
||||
Given the inaccuracies relative to real computers anyway,
|
||||
I think of it as that OpenComputers itself is the kernel,
|
||||
but it can only handle a single task, written in Lua,
|
||||
so people build on top of it to make an interface.
|
||||
|
||||
(And given the memory limitations, having a cooperatively multitasking
|
||||
microkernel which then gives all it's capabilities to the window
|
||||
manager would succeed only in complicating things and then using all
|
||||
memory, in that precise order.
|
||||
This way accomplishes the same thing, and it's simpler.)
|
||||
|
||||
## How's multilingual support?
|
||||
|
||||
Multilingual support exists, but the languages are Pirate and English.
|
||||
Most applications are bare-bones enough that they don't have any strings
|
||||
that need to be translated - I consider this a plus.
|
||||
|
||||
The infrastructure for a system language exists, but is not really in use.
|
||||
(This includes the installer copying language files for the selected language,
|
||||
but the only thing with a language selector at the moment is the installer.
|
||||
Language selection is performed by editing the "language" file
|
||||
at the drive root and rebooting, or switching language during install.)
|
||||
|
||||
The infrastructure is quite minimal so as not to bloat the system too badly -
|
||||
it's up to the applications to decide how to implement multi-language support,
|
||||
but the system can load files from lang/<language>/<pkg>.lua to aid in this.
|
||||
(Why loading files? To avoid having every single language loaded at once.
|
||||
Why loading Lua files? To avoid making this feature bloat the system.)
|
||||
|
||||
As for the issue of wide characters (Chinese/Japanese/Korean support):
|
||||
|
||||
Wide characters are supported in all supplied apps that handle text,
|
||||
including the text editor -
|
||||
the helper API function unicode.safeTextFormat should make these things easier.
|
||||
|
||||
(safeTextFormat is allowed to rearrange the text however it needs to for
|
||||
display - this leaves the possibility of RTL layout open.)
|
234
tfilemgr.lua
Normal file
234
tfilemgr.lua
Normal file
@ -0,0 +1,234 @@
|
||||
-- The File Manager (manager of files).
|
||||
-- Args:
|
||||
local filetype, openmode, gpu = ...
|
||||
local fileManager = nil
|
||||
function fileManager(filetype, openmode)
|
||||
-- Like policykit, this is a trusted gateway.
|
||||
-- Note that The File Manager just returns a path {fs, path}.
|
||||
-- The File Wrapper is given that path, and the open mode.
|
||||
local title = nil
|
||||
-- Valid open modes are:
|
||||
-- nil: The File Wrapper should not be invoked -
|
||||
-- likely a "file manager launcher" application.
|
||||
if openmode == nil then title = "File Manager" end
|
||||
-- "r": Open the file for reading. Binary mode is assumed.
|
||||
if openmode == "r" then
|
||||
title = "Read " .. filetype
|
||||
end
|
||||
if openmode == "w" then
|
||||
title = "Write " .. filetype
|
||||
end
|
||||
-- "w": Open the file for truncate-writing, again binary assumed.
|
||||
if not title then error("Bad openmode") end
|
||||
|
||||
local scrW, scrH = gpu.getResolution()
|
||||
gpu.setBackground(0)
|
||||
gpu.setForeground(0xFFFFFF)
|
||||
local function cls()
|
||||
gpu.fill(1, 1, scrW, scrH, " ")
|
||||
end
|
||||
local function menuKey(cursor, el, text, ka, kc, allowEntry)
|
||||
if ka == 13 then
|
||||
-- entry denied, so we hit here.
|
||||
return cursor, text
|
||||
end
|
||||
if kc == 200 then
|
||||
cursor = cursor - 1
|
||||
if cursor < 1 then cursor = el end
|
||||
return cursor, text, false, true
|
||||
end
|
||||
if kc == 208 then
|
||||
cursor = cursor + 1
|
||||
if cursor > el then cursor = 1 end
|
||||
return cursor, text, false, true
|
||||
end
|
||||
if allowEntry then
|
||||
if ka == 8 then
|
||||
return cursor, unicode.sub(text, 1, unicode.len(text) - 1), true
|
||||
end
|
||||
if (ka ~= 0) and (ka ~= ("/"):byte()) and (ka ~= ("\\"):byte()) then
|
||||
text = text .. unicode.char(ka)
|
||||
return cursor, text, true
|
||||
end
|
||||
end
|
||||
return cursor, text
|
||||
end
|
||||
local function menu(title, entries, allowEntry)
|
||||
cls()
|
||||
gpu.fill(1, 1, scrW, 1, "-")
|
||||
gpu.set(1, 1, title)
|
||||
local cursor = 1
|
||||
local escrH = scrH
|
||||
local entryText = ""
|
||||
local cursorBlinky = false
|
||||
if allowEntry then escrH = scrH - 1 end
|
||||
while true do
|
||||
for y = 2, escrH do
|
||||
local o = cursor + (y - 8)
|
||||
local s = tostring(entries[o])
|
||||
if not entries[o] then s = "" end
|
||||
if o == cursor then s = ">" .. s else s = " " .. s end
|
||||
gpu.fill(1, y, scrW, 1, " ")
|
||||
gpu.set(1, y, s)
|
||||
end
|
||||
cursorBlinky = not cursorBlinky
|
||||
if allowEntry then
|
||||
gpu.fill(1, scrH, scrW, 1, " ")
|
||||
if cursorBlinky then
|
||||
gpu.set(1, scrH, ":" .. entryText)
|
||||
else
|
||||
gpu.set(1, scrH, ":" .. entryText .. "_")
|
||||
end
|
||||
end
|
||||
local t, p1, p2, p3, p4 = computer.pullSignal(1)
|
||||
if t == "key_down" then
|
||||
if p2 == 13 then
|
||||
if allowEntry then
|
||||
if entryText ~= "" then
|
||||
return entryText
|
||||
end
|
||||
else
|
||||
return entries[cursor]
|
||||
end
|
||||
end
|
||||
cursor, entryText, search, lookup = menuKey(cursor, #entries, entryText, p2, p3, allowEntry)
|
||||
if search then
|
||||
for k, v in ipairs(entries) do
|
||||
if v:sub(1, v:len()) == entryText then cursor = k end
|
||||
end
|
||||
end
|
||||
if lookup then
|
||||
entryText = entries[cursor]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local currentDir = nil
|
||||
local currentDrive = nil
|
||||
local function listDir(dv, dr)
|
||||
if dv == nil then
|
||||
local l = {}
|
||||
local t = {}
|
||||
for c in component.list("filesystem") do
|
||||
l[c] = {c, "/"}
|
||||
table.insert(t, c)
|
||||
end
|
||||
return l, t, "Filesystems"
|
||||
end
|
||||
local names = component.invoke(dv, "list", dr)
|
||||
local l = {}
|
||||
for k, v in ipairs(names) do
|
||||
if component.invoke(dv, "isDirectory", dr .. v) then
|
||||
l[v] = {dv, dr .. v}
|
||||
end
|
||||
end
|
||||
return l, names, dv .. ":" .. dr
|
||||
end
|
||||
local function isDir(dv, dr)
|
||||
if dv == nil then return true end
|
||||
return component.invoke(dv, "isDirectory", dr)
|
||||
end
|
||||
|
||||
local tagMkdir = "// Create Directory //"
|
||||
local tagCancel = "// Cancel //"
|
||||
local tagOpen = "// Open //"
|
||||
local tagDelete = "// Delete //"
|
||||
local tagRename = "// Rename //"
|
||||
local tagCopy = "// Copy //"
|
||||
local tagBack = ".."
|
||||
|
||||
local function textEntry(title)
|
||||
local txt = menu(title, {tagCancel}, true)
|
||||
if txt ~= tagCancel then return txt end
|
||||
return nil
|
||||
end
|
||||
local function report(title)
|
||||
menu(title, {"OK"}, false)
|
||||
end
|
||||
|
||||
local history = {}
|
||||
local function navigate(ndr, ndd)
|
||||
table.insert(history, {currentDrive, currentDir})
|
||||
currentDrive, currentDir = ndr, ndd
|
||||
end
|
||||
while true do
|
||||
local map, sl, name = listDir(currentDrive, currentDir)
|
||||
if #history ~= 0 then
|
||||
table.insert(sl, tagBack)
|
||||
end
|
||||
table.insert(sl, tagCancel)
|
||||
if currentDrive then
|
||||
table.insert(sl, tagMkdir)
|
||||
end
|
||||
local str = menu(title .. " " .. name, sl, (openmode == "w") and currentDrive)
|
||||
if str == tagBack then
|
||||
local r = table.remove(history, #history)
|
||||
currentDrive, currentDir = table.unpack(r)
|
||||
else
|
||||
if str == tagCancel then return nil end
|
||||
if str == tagMkdir then
|
||||
local nam = textEntry("Create Directory...")
|
||||
if nam then
|
||||
component.invoke(currentDrive, "makeDirectory", currentDir .. nam)
|
||||
end
|
||||
else
|
||||
if map[str] then
|
||||
if map[str][1] and currentDrive then
|
||||
local act = menu(name .. ":" .. str, {tagOpen, tagRename, tagDelete, tagCancel})
|
||||
if act == tagOpen then
|
||||
navigate(table.unpack(map[str]))
|
||||
end
|
||||
if act == tagRename then
|
||||
local s = textEntry("Rename " .. str)
|
||||
if s then
|
||||
component.invoke(map[str][1], "rename", map[str][2], currentDir .. s)
|
||||
end
|
||||
end
|
||||
if act == tagDelete then
|
||||
component.invoke(map[str][1], "remove", map[str][2])
|
||||
end
|
||||
else
|
||||
navigate(table.unpack(map[str]))
|
||||
end
|
||||
else
|
||||
if openmode == "w" then return {currentDrive, currentDir .. str} end
|
||||
local r = currentDir .. str
|
||||
local subTag = "Size: " .. math.ceil(component.invoke(currentDrive, "size", r) / 1024) .. "KiB"
|
||||
if openmode == "r" then subTag = tagOpen end
|
||||
local act = menu(name .. ":" .. str, {subTag, tagRename, tagCopy, tagDelete, tagCancel})
|
||||
if act == tagOpen then return {currentDrive, currentDir .. str} end
|
||||
if act == tagRename then
|
||||
local s = textEntry("Rename " .. str)
|
||||
component.invoke(currentDrive, "rename", currentDir .. str, currentDir .. s)
|
||||
end
|
||||
if act == tagCopy then
|
||||
local f2 = fileManager("Copy " .. str, "w")
|
||||
if f2 then
|
||||
local h = component.invoke(currentDrive, "open", currentDir .. str, "rb")
|
||||
if not h then
|
||||
report("Couldn't open file!")
|
||||
else
|
||||
local h2 = component.invoke(f2[1], "open", f2[2], "wb")
|
||||
if not h2 then
|
||||
report("Couldn't open dest. file!")
|
||||
else
|
||||
local chk = component.invoke(currentDrive, "read", h, 128)
|
||||
while chk do
|
||||
component.invoke(f2[1], "write", h2, chk)
|
||||
chk = component.invoke(currentDrive, "read", h, 128)
|
||||
end
|
||||
end
|
||||
component.invoke(currentDrive, "close", h)
|
||||
end
|
||||
end
|
||||
end
|
||||
if act == tagDelete then
|
||||
component.invoke(currentDrive, "remove", currentDir .. str)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return fileManager(filetype, openmode)
|
Loading…
Reference in New Issue
Block a user