Compare commits
44 Commits
Merith-TK/
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4542644068 | ||
|
|
b4b5f13b23 | ||
|
|
d1e752c516 | ||
|
|
83baef8f82 | ||
|
|
d0cb420a08 | ||
|
|
eae757627a | ||
|
|
7a99fc662d | ||
|
|
5beb965aad | ||
|
|
1e0bcb841a | ||
|
|
8ade008390 | ||
|
|
fee8cf4f46 | ||
|
|
f2460aa5e9 | ||
|
|
aff772b851 | ||
|
|
04751c3ba9 | ||
|
|
f53d503129 | ||
|
|
50fd8622ff | ||
|
|
19edc6dc97 | ||
|
|
31b4fd52c9 | ||
|
|
ab26539660 | ||
|
|
9490b713ca | ||
|
|
a914786116 | ||
|
|
03f00fc2a4 | ||
|
|
5775954d7c | ||
|
|
857c0e252d | ||
|
|
7bd993a12d | ||
|
|
7d36c9aafe | ||
|
|
28733d77e8 | ||
|
|
be13d87266 | ||
|
|
73616676dc | ||
|
|
e3ea71f5a3 | ||
|
|
fd6f39770a | ||
|
|
88dda25911 | ||
|
|
53b38b7286 | ||
|
|
c1b2e03fd6 | ||
|
|
4a233b1c55 | ||
|
|
9e06241ac3 | ||
|
|
39caa32908 | ||
|
|
9103c44658 | ||
|
|
882894685c | ||
|
|
ba49f7ca7d | ||
|
|
f3c35afe07 | ||
|
|
8a6896e276 | ||
|
|
a18a8b7140 | ||
|
|
6d6b43daf7 |
@@ -16,5 +16,5 @@
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
```
|
```
|
||||||
pastebin run UzGHLbNC
|
wget run https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/main/installer.lua
|
||||||
```
|
```
|
||||||
|
|||||||
448
installer.lua
Normal file
448
installer.lua
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
--[[
|
||||||
|
Opus OS Installer
|
||||||
|
Self-contained installer that downloads from Gitea
|
||||||
|
]]
|
||||||
|
|
||||||
|
local GITEA_HOST = 'git.spatulaa.com'
|
||||||
|
local GITEA_USER = 'MayaTheShy'
|
||||||
|
local GITEA_REPO = 'Opus'
|
||||||
|
local GITEA_BRANCH = 'main'
|
||||||
|
|
||||||
|
local TREE_URL = 'https://%s/api/v1/repos/%s/%s/git/trees/%s?recursive=1'
|
||||||
|
local FILE_URL = 'https://%s/%s/%s/raw/branch/%s/%s'
|
||||||
|
|
||||||
|
local colors = _G.colors
|
||||||
|
local fs = _G.fs
|
||||||
|
local http = _G.http
|
||||||
|
local os = _G.os
|
||||||
|
local parallel = _G.parallel
|
||||||
|
local term = _G.term
|
||||||
|
local textutils = _G.textutils
|
||||||
|
|
||||||
|
local args = { ... }
|
||||||
|
local mode = args[1] or 'install'
|
||||||
|
|
||||||
|
-- Utility functions
|
||||||
|
|
||||||
|
local function httpGet(url)
|
||||||
|
local h, msg = http.get(url)
|
||||||
|
if h then
|
||||||
|
local content = h.readAll()
|
||||||
|
h.close()
|
||||||
|
return content
|
||||||
|
end
|
||||||
|
return nil, msg
|
||||||
|
end
|
||||||
|
|
||||||
|
local function download(url, path)
|
||||||
|
local dir = fs.getDir(path)
|
||||||
|
if dir ~= '' and not fs.exists(dir) then
|
||||||
|
fs.makeDir(dir)
|
||||||
|
end
|
||||||
|
local content, msg = httpGet(url)
|
||||||
|
if content then
|
||||||
|
local f = fs.open(path, 'w')
|
||||||
|
f.write(content)
|
||||||
|
f.close()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false, msg or 'Download failed'
|
||||||
|
end
|
||||||
|
|
||||||
|
local function formatSize(size)
|
||||||
|
if size >= 1000000 then
|
||||||
|
return string.format('%dM', math.floor(size / 1000000))
|
||||||
|
elseif size >= 1000 then
|
||||||
|
return string.format('%dK', math.floor(size / 1000))
|
||||||
|
end
|
||||||
|
return tostring(size)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function center(text, y)
|
||||||
|
local w = term.getSize()
|
||||||
|
term.setCursorPos(math.floor((w - #text) / 2) + 1, y)
|
||||||
|
term.write(text)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawHeader(title)
|
||||||
|
local w = term.getSize()
|
||||||
|
term.setBackgroundColor(colors.cyan)
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
term.clearLine()
|
||||||
|
center(title, 1)
|
||||||
|
term.setBackgroundColor(colors.black)
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function drawProgressBar(y, pct)
|
||||||
|
local w = term.getSize()
|
||||||
|
local barW = w - 4
|
||||||
|
local filled = math.floor(barW * pct / 100)
|
||||||
|
|
||||||
|
term.setCursorPos(3, y)
|
||||||
|
term.setBackgroundColor(colors.gray)
|
||||||
|
term.write(string.rep(' ', barW))
|
||||||
|
term.setCursorPos(3, y)
|
||||||
|
term.setBackgroundColor(colors.lime)
|
||||||
|
term.write(string.rep(' ', filled))
|
||||||
|
term.setBackgroundColor(colors.black)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function waitForKey(prompt)
|
||||||
|
local _, h = term.getSize()
|
||||||
|
if prompt then
|
||||||
|
term.setTextColor(colors.lightGray)
|
||||||
|
center(prompt, h)
|
||||||
|
end
|
||||||
|
os.pullEvent('key')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get file listing from Gitea API
|
||||||
|
|
||||||
|
local function getFileList(branch)
|
||||||
|
local url = string.format(TREE_URL, GITEA_HOST, GITEA_USER, GITEA_REPO, branch)
|
||||||
|
local content, msg = httpGet(url)
|
||||||
|
if not content then
|
||||||
|
return nil, 'Failed to get file list: ' .. (msg or 'unknown error')
|
||||||
|
end
|
||||||
|
|
||||||
|
local data = textutils.unserialiseJSON(content)
|
||||||
|
if not data then
|
||||||
|
return nil, 'Invalid response from server'
|
||||||
|
end
|
||||||
|
if data.message then
|
||||||
|
return nil, data.message
|
||||||
|
end
|
||||||
|
|
||||||
|
local files = {}
|
||||||
|
local totalSize = 0
|
||||||
|
for _, v in pairs(data.tree) do
|
||||||
|
if v.type == 'blob' then
|
||||||
|
local path = v.path:gsub('%s', '%%20')
|
||||||
|
files[path] = {
|
||||||
|
url = string.format(FILE_URL, GITEA_HOST, GITEA_USER, GITEA_REPO, branch, path),
|
||||||
|
size = v.size or 0,
|
||||||
|
}
|
||||||
|
totalSize = totalSize + math.max(500, v.size or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return files, nil, totalSize
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Splash screen
|
||||||
|
|
||||||
|
local function showSplash()
|
||||||
|
term.clear()
|
||||||
|
drawHeader('Opus OS Installer')
|
||||||
|
|
||||||
|
local _, h = term.getSize()
|
||||||
|
local y = 3
|
||||||
|
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
center('Opus OS v1.0', y)
|
||||||
|
y = y + 2
|
||||||
|
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
center('By: Kepler', y)
|
||||||
|
y = y + 2
|
||||||
|
|
||||||
|
term.setTextColor(colors.lightGray)
|
||||||
|
local desc = {
|
||||||
|
'A user friendly operating system',
|
||||||
|
'featuring multitasking, networking,',
|
||||||
|
'and automation.',
|
||||||
|
}
|
||||||
|
for _, line in ipairs(desc) do
|
||||||
|
center(line, y)
|
||||||
|
y = y + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
y = y + 1
|
||||||
|
term.setTextColor(colors.gray)
|
||||||
|
center('Source: ' .. GITEA_HOST, y)
|
||||||
|
|
||||||
|
waitForKey('Press any key to continue...')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- License screen
|
||||||
|
|
||||||
|
local function showLicense()
|
||||||
|
term.clear()
|
||||||
|
drawHeader('License Review')
|
||||||
|
|
||||||
|
local _, h = term.getSize()
|
||||||
|
term.setCursorPos(2, 3)
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
print(' Copyright (c) 2018 Kepler')
|
||||||
|
print()
|
||||||
|
term.setTextColor(colors.lightGray)
|
||||||
|
|
||||||
|
local license = [[
|
||||||
|
Permission is hereby granted, free of
|
||||||
|
charge, to any person obtaining a copy
|
||||||
|
of this software to deal in the Software
|
||||||
|
without restriction, including without
|
||||||
|
limitation the rights to use, copy,
|
||||||
|
modify, merge, publish, distribute,
|
||||||
|
sublicense, and/or sell copies of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS",
|
||||||
|
WITHOUT WARRANTY OF ANY KIND.]]
|
||||||
|
|
||||||
|
print(license)
|
||||||
|
|
||||||
|
waitForKey('Press any key to accept...')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Branch selection (only shown for full install)
|
||||||
|
|
||||||
|
local function selectBranch()
|
||||||
|
local branches = {
|
||||||
|
{ branch = 'main', description = 'Main (Stable)' },
|
||||||
|
{ branch = 'develop-1.8', description = '1.8+ Unstable' },
|
||||||
|
{ branch = 'master-1.8', description = '1.8+ Stable' },
|
||||||
|
}
|
||||||
|
|
||||||
|
local selected = 1
|
||||||
|
|
||||||
|
while true do
|
||||||
|
term.clear()
|
||||||
|
drawHeader('Select Branch')
|
||||||
|
|
||||||
|
local _, h = term.getSize()
|
||||||
|
term.setCursorPos(2, 3)
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
print(' Choose which branch to install:')
|
||||||
|
print()
|
||||||
|
|
||||||
|
for i, b in ipairs(branches) do
|
||||||
|
term.setCursorPos(3, 4 + i)
|
||||||
|
if i == selected then
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
term.write('> ')
|
||||||
|
else
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
term.write(' ')
|
||||||
|
end
|
||||||
|
term.write(b.branch)
|
||||||
|
term.setTextColor(colors.gray)
|
||||||
|
term.write(' - ' .. b.description)
|
||||||
|
end
|
||||||
|
|
||||||
|
term.setTextColor(colors.lightGray)
|
||||||
|
center('Up/Down to select, Enter to confirm', h)
|
||||||
|
|
||||||
|
local event, key = os.pullEvent('key')
|
||||||
|
if key == keys.up then
|
||||||
|
selected = selected > 1 and selected - 1 or #branches
|
||||||
|
elseif key == keys.down then
|
||||||
|
selected = selected < #branches and selected + 1 or 1
|
||||||
|
elseif key == keys.enter then
|
||||||
|
return branches[selected].branch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- File list review
|
||||||
|
|
||||||
|
local function showFileReview(files, totalSize)
|
||||||
|
term.clear()
|
||||||
|
drawHeader('Review Files')
|
||||||
|
|
||||||
|
local _, h = term.getSize()
|
||||||
|
local count = 0
|
||||||
|
for _ in pairs(files) do count = count + 1 end
|
||||||
|
|
||||||
|
term.setCursorPos(2, 3)
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
print(string.format(' %d files to install', count))
|
||||||
|
print()
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
print(string.format(' Space required: %s', formatSize(totalSize)))
|
||||||
|
|
||||||
|
local diskFree = fs.getFreeSpace('/')
|
||||||
|
if totalSize > diskFree then
|
||||||
|
term.setTextColor(colors.red)
|
||||||
|
print(string.format(' Free space: %s (NOT ENOUGH!)', formatSize(diskFree)))
|
||||||
|
else
|
||||||
|
term.setTextColor(colors.lime)
|
||||||
|
print(string.format(' Free space: %s', formatSize(diskFree)))
|
||||||
|
end
|
||||||
|
|
||||||
|
print()
|
||||||
|
term.setTextColor(colors.lightGray)
|
||||||
|
|
||||||
|
-- Show first few files
|
||||||
|
local shown = 0
|
||||||
|
for path in pairs(files) do
|
||||||
|
if shown < (h - 11) then
|
||||||
|
term.setCursorPos(3, 9 + shown)
|
||||||
|
local display = path
|
||||||
|
local w = term.getSize()
|
||||||
|
if #display > w - 4 then
|
||||||
|
display = '...' .. display:sub(-(w - 7))
|
||||||
|
end
|
||||||
|
term.write(display)
|
||||||
|
shown = shown + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if count > shown then
|
||||||
|
term.setCursorPos(3, 9 + shown)
|
||||||
|
term.setTextColor(colors.gray)
|
||||||
|
term.write(string.format('... and %d more files', count - shown))
|
||||||
|
end
|
||||||
|
|
||||||
|
waitForKey('Press any key to begin install...')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Install files with progress
|
||||||
|
|
||||||
|
local function installFiles(files)
|
||||||
|
if mode == 'update' then
|
||||||
|
fs.delete('sys')
|
||||||
|
end
|
||||||
|
|
||||||
|
term.clear()
|
||||||
|
drawHeader('Installing...')
|
||||||
|
|
||||||
|
local _, h = term.getSize()
|
||||||
|
local total = 0
|
||||||
|
for _ in pairs(files) do total = total + 1 end
|
||||||
|
|
||||||
|
local fileList = {}
|
||||||
|
for path, entry in pairs(files) do
|
||||||
|
table.insert(fileList, { path = path, url = entry.url })
|
||||||
|
end
|
||||||
|
|
||||||
|
local completed = 0
|
||||||
|
local failed = 0
|
||||||
|
local lastFile = ''
|
||||||
|
|
||||||
|
-- Download with 5 parallel workers
|
||||||
|
local workers = {}
|
||||||
|
for _ = 1, 5 do
|
||||||
|
table.insert(workers, function()
|
||||||
|
while true do
|
||||||
|
local entry = table.remove(fileList)
|
||||||
|
if not entry then break end
|
||||||
|
|
||||||
|
lastFile = entry.path
|
||||||
|
local s, m = download(entry.url, entry.path)
|
||||||
|
if not s then
|
||||||
|
failed = failed + 1
|
||||||
|
end
|
||||||
|
completed = completed + 1
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Progress display alongside downloads
|
||||||
|
table.insert(workers, function()
|
||||||
|
while completed < total do
|
||||||
|
local pct = math.floor(completed / total * 100)
|
||||||
|
|
||||||
|
term.setCursorPos(2, 4)
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
term.clearLine()
|
||||||
|
term.write(string.format(' Progress: %d/%d (%d%%)', completed, total, pct))
|
||||||
|
|
||||||
|
term.setCursorPos(2, 6)
|
||||||
|
term.setTextColor(colors.lightGray)
|
||||||
|
term.clearLine()
|
||||||
|
local w = term.getSize()
|
||||||
|
local display = lastFile
|
||||||
|
if #display > w - 4 then
|
||||||
|
display = '...' .. display:sub(-(w - 7))
|
||||||
|
end
|
||||||
|
term.write(' ' .. display)
|
||||||
|
|
||||||
|
drawProgressBar(h - 3, pct)
|
||||||
|
|
||||||
|
if failed > 0 then
|
||||||
|
term.setCursorPos(2, 8)
|
||||||
|
term.setTextColor(colors.red)
|
||||||
|
term.write(string.format(' Failed: %d', failed))
|
||||||
|
end
|
||||||
|
|
||||||
|
os.sleep(0.1)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
parallel.waitForAll(table.unpack(workers))
|
||||||
|
|
||||||
|
-- Final progress update
|
||||||
|
drawProgressBar(h - 3, 100)
|
||||||
|
term.setCursorPos(2, 4)
|
||||||
|
term.setTextColor(colors.lime)
|
||||||
|
term.clearLine()
|
||||||
|
term.write(string.format(' Complete: %d/%d files', completed, total))
|
||||||
|
|
||||||
|
term.setCursorPos(2, 6)
|
||||||
|
term.clearLine()
|
||||||
|
|
||||||
|
if failed > 0 then
|
||||||
|
term.setCursorPos(2, 8)
|
||||||
|
term.setTextColor(colors.red)
|
||||||
|
term.write(string.format(' %d files failed to download', failed))
|
||||||
|
end
|
||||||
|
|
||||||
|
return failed == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Done screen
|
||||||
|
|
||||||
|
local function showDone(success)
|
||||||
|
local _, h = term.getSize()
|
||||||
|
|
||||||
|
term.setCursorPos(2, h - 1)
|
||||||
|
if success then
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
center('Install complete! Rebooting...', h - 1)
|
||||||
|
os.sleep(2)
|
||||||
|
os.reboot()
|
||||||
|
else
|
||||||
|
term.setTextColor(colors.red)
|
||||||
|
center('Install finished with errors.', h - 1)
|
||||||
|
waitForKey('Press any key to exit...')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Main installer flow
|
||||||
|
|
||||||
|
local function main()
|
||||||
|
local branch = GITEA_BRANCH
|
||||||
|
|
||||||
|
if mode == 'install' then
|
||||||
|
showSplash()
|
||||||
|
showLicense()
|
||||||
|
branch = selectBranch()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Get file list
|
||||||
|
term.clear()
|
||||||
|
drawHeader('Opus OS Installer')
|
||||||
|
term.setCursorPos(2, 4)
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
term.write(' Fetching file list...')
|
||||||
|
|
||||||
|
local files, err, totalSize = getFileList(branch)
|
||||||
|
if not files then
|
||||||
|
term.setCursorPos(2, 6)
|
||||||
|
term.setTextColor(colors.red)
|
||||||
|
print(' Error: ' .. (err or 'unknown'))
|
||||||
|
waitForKey('Press any key to exit...')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if mode == 'install' then
|
||||||
|
showFileReview(files, totalSize)
|
||||||
|
end
|
||||||
|
|
||||||
|
local success = installFiles(files)
|
||||||
|
showDone(success)
|
||||||
|
end
|
||||||
|
|
||||||
|
main()
|
||||||
@@ -51,7 +51,7 @@ local config = {
|
|||||||
}
|
}
|
||||||
Config.load('Overview', config)
|
Config.load('Overview', config)
|
||||||
|
|
||||||
local extSupport = Util.getVersion() >= 1.76
|
local extSupport = Util.supportsExtChars()
|
||||||
|
|
||||||
local applications = { }
|
local applications = { }
|
||||||
local buttons = { }
|
local buttons = { }
|
||||||
|
|||||||
@@ -16,25 +16,27 @@ local page = UI.Page {
|
|||||||
x = 2, ex = 14, y = 2, ey = -6,
|
x = 2, ex = 14, y = 2, ey = -6,
|
||||||
values = { },
|
values = { },
|
||||||
columns = {
|
columns = {
|
||||||
{ heading = 'Package', key = 'name' },
|
{ heading = ' Package', key = 'displayName' },
|
||||||
},
|
},
|
||||||
sortColumn = 'name',
|
sortColumn = 'name',
|
||||||
autospace = true,
|
autospace = true,
|
||||||
help = 'Select a package',
|
help = 'Space to select, Enter to toggle',
|
||||||
},
|
},
|
||||||
add = UI.Button {
|
installSelected = UI.Button {
|
||||||
x = 2, y = -3,
|
x = 2, y = -3,
|
||||||
text = ' + ',
|
text = ' + ',
|
||||||
event = 'action',
|
event = 'batch_action',
|
||||||
help = 'Install or update',
|
operation = 'install',
|
||||||
|
operationText = 'Install',
|
||||||
|
help = 'Install or update selected',
|
||||||
},
|
},
|
||||||
remove = UI.Button {
|
removeSelected = UI.Button {
|
||||||
x = 8, y = -3,
|
x = 8, y = -3,
|
||||||
text = ' - ',
|
text = ' - ',
|
||||||
event = 'action',
|
event = 'batch_action',
|
||||||
operation = 'uninstall',
|
operation = 'uninstall',
|
||||||
operationText = 'Remove',
|
operationText = 'Remove',
|
||||||
help = 'Remove',
|
help = 'Remove selected',
|
||||||
},
|
},
|
||||||
updateall = UI.Button {
|
updateall = UI.Button {
|
||||||
ex = -2, y = -3, width = 12,
|
ex = -2, y = -3, width = 12,
|
||||||
@@ -89,7 +91,9 @@ function page:loadPackages()
|
|||||||
end
|
end
|
||||||
table.insert(self.grid.values, {
|
table.insert(self.grid.values, {
|
||||||
installed = not not Packages:isInstalled(k),
|
installed = not not Packages:isInstalled(k),
|
||||||
|
selected = false,
|
||||||
name = k,
|
name = k,
|
||||||
|
displayName = k,
|
||||||
manifest = manifest,
|
manifest = manifest,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@@ -104,12 +108,62 @@ function page:loadPackages()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function page.grid:getRowTextColor(row, selected)
|
function page.grid:getRowTextColor(row, selected)
|
||||||
|
if row.selected then
|
||||||
|
return colors.cyan
|
||||||
|
end
|
||||||
if row.installed then
|
if row.installed then
|
||||||
return colors.yellow
|
return colors.yellow
|
||||||
end
|
end
|
||||||
return UI.Grid.getRowTextColor(self, row, selected)
|
return UI.Grid.getRowTextColor(self, row, selected)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function page:getSelectedPackages()
|
||||||
|
local selected = { }
|
||||||
|
for _, row in pairs(self.grid.values) do
|
||||||
|
if row.selected then
|
||||||
|
table.insert(selected, row)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return selected
|
||||||
|
end
|
||||||
|
|
||||||
|
function page:getSelectedCount()
|
||||||
|
local count = 0
|
||||||
|
for _, row in pairs(self.grid.values) do
|
||||||
|
if row.selected then
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return count
|
||||||
|
end
|
||||||
|
|
||||||
|
function page:toggleSelection(row)
|
||||||
|
row.selected = not row.selected
|
||||||
|
row.displayName = row.selected and ('\187 ' .. row.name) or row.name
|
||||||
|
self.grid:draw()
|
||||||
|
self:updateStatus()
|
||||||
|
end
|
||||||
|
|
||||||
|
function page:clearSelection()
|
||||||
|
for _, row in pairs(self.grid.values) do
|
||||||
|
row.selected = false
|
||||||
|
row.displayName = row.name
|
||||||
|
end
|
||||||
|
self.grid:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
function page:updateStatus()
|
||||||
|
local count = self:getSelectedCount()
|
||||||
|
if count > 0 then
|
||||||
|
self.statusBar:setStatus(count .. ' package(s) selected')
|
||||||
|
else
|
||||||
|
local focused = self.grid:getSelected()
|
||||||
|
if focused then
|
||||||
|
self.statusBar:setStatus(focused.installed and 'Installed' or '')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function page.action:show()
|
function page.action:show()
|
||||||
self.output.win:clear()
|
self.output.win:clear()
|
||||||
UI.SlideOut.show(self)
|
UI.SlideOut.show(self)
|
||||||
@@ -134,11 +188,7 @@ function page:run(operation, name)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function page:updateSelection(selected)
|
function page:updateSelection(selected)
|
||||||
self.add.operation = selected.installed and 'update' or 'install'
|
-- no-op: buttons are always active for batch operations
|
||||||
self.add.operationText = selected.installed and 'Update' or 'Install'
|
|
||||||
self.remove.inactive = not selected.installed
|
|
||||||
self.add:draw()
|
|
||||||
self.remove:draw()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function page:eventHandler(event)
|
function page:eventHandler(event)
|
||||||
@@ -152,7 +202,14 @@ function page:eventHandler(event)
|
|||||||
Ansi.yellow, manifest.title,
|
Ansi.yellow, manifest.title,
|
||||||
Ansi.white, manifest.description))
|
Ansi.white, manifest.description))
|
||||||
self.description:draw()
|
self.description:draw()
|
||||||
self:updateSelection(event.selected)
|
self:updateStatus()
|
||||||
|
|
||||||
|
elseif event.type == 'grid_select' then
|
||||||
|
-- Space or Enter toggles selection
|
||||||
|
local row = self.grid:getSelected()
|
||||||
|
if row then
|
||||||
|
self:toggleSelection(row)
|
||||||
|
end
|
||||||
|
|
||||||
elseif event.type == 'checkbox_change' then
|
elseif event.type == 'checkbox_change' then
|
||||||
config.compression = not config.compression
|
config.compression = not config.compression
|
||||||
@@ -160,34 +217,69 @@ function page:eventHandler(event)
|
|||||||
|
|
||||||
elseif event.type == 'updateall' then
|
elseif event.type == 'updateall' then
|
||||||
self.operation = 'updateall'
|
self.operation = 'updateall'
|
||||||
|
self.operationTargets = { }
|
||||||
self.action.button.text = ' Begin '
|
self.action.button.text = ' Begin '
|
||||||
self.action.button.event = 'begin'
|
self.action.button.event = 'begin'
|
||||||
self.action.titleBar.title = 'Update All'
|
self.action.titleBar.title = 'Update All'
|
||||||
self.action:show()
|
self.action:show()
|
||||||
|
|
||||||
elseif event.type == 'action' then
|
elseif event.type == 'batch_action' then
|
||||||
local selected = self.grid:getSelected()
|
local targets = self:getSelectedPackages()
|
||||||
if selected then
|
local operation = event.button.operation
|
||||||
self.operation = event.button.operation
|
|
||||||
self.action.button.text = event.button.operationText
|
-- fall back to focused row if nothing selected
|
||||||
self.action.titleBar.title = selected.manifest.title
|
if #targets == 0 then
|
||||||
self.action.button.text = ' Begin '
|
local focused = self.grid:getSelected()
|
||||||
self.action.button.event = 'begin'
|
if focused then
|
||||||
self.action:show()
|
targets = { focused }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if #targets == 0 then return end
|
||||||
|
|
||||||
|
-- for install: update already-installed packages, install new ones
|
||||||
|
if operation == 'install' then
|
||||||
|
self.operation = 'install'
|
||||||
|
else
|
||||||
|
-- filter to only installed packages for uninstall
|
||||||
|
local installed = { }
|
||||||
|
for _, t in ipairs(targets) do
|
||||||
|
if t.installed then
|
||||||
|
table.insert(installed, t)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
targets = installed
|
||||||
|
if #targets == 0 then return end
|
||||||
|
self.operation = 'uninstall'
|
||||||
|
end
|
||||||
|
|
||||||
|
self.operationTargets = targets
|
||||||
|
local title = #targets == 1
|
||||||
|
and targets[1].manifest.title
|
||||||
|
or (#targets .. ' packages')
|
||||||
|
self.action.button.text = ' Begin '
|
||||||
|
self.action.button.event = 'begin'
|
||||||
|
self.action.titleBar.title = string.format('%s %s',
|
||||||
|
operation == 'install' and 'Install/Update' or 'Remove', title)
|
||||||
|
self.action:show()
|
||||||
|
|
||||||
elseif event.type == 'hide-action' then
|
elseif event.type == 'hide-action' then
|
||||||
self.action:hide()
|
self.action:hide()
|
||||||
|
|
||||||
elseif event.type == 'begin' then
|
elseif event.type == 'begin' then
|
||||||
if self.operation == 'updateall' then
|
if self.operation == 'updateall' then
|
||||||
self:run(self.operation, '')
|
self:run('updateall', '')
|
||||||
else
|
else
|
||||||
local selected = self.grid:getSelected()
|
for _, target in ipairs(self.operationTargets) do
|
||||||
self:run(self.operation, selected.name)
|
local op = self.operation
|
||||||
selected.installed = Packages:isInstalled(selected.name)
|
if op == 'install' and target.installed then
|
||||||
|
op = 'update'
|
||||||
self:updateSelection(selected)
|
end
|
||||||
|
self:run(op, target.name)
|
||||||
|
target.installed = Packages:isInstalled(target.name)
|
||||||
|
end
|
||||||
|
self:clearSelection()
|
||||||
|
self:updateStatus()
|
||||||
end
|
end
|
||||||
|
|
||||||
self.action.button.text = ' Done '
|
self.action.button.text = ' Done '
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ local Ansi = require('opus.ansi')
|
|||||||
local Security = require('opus.security')
|
local Security = require('opus.security')
|
||||||
local SHA = require('opus.crypto.sha2')
|
local SHA = require('opus.crypto.sha2')
|
||||||
local UI = require('opus.ui')
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local colors = _G.colors
|
local colors = _G.colors
|
||||||
local os = _G.os
|
local os = _G.os
|
||||||
@@ -16,9 +17,9 @@ local labelIntro = [[Set a friendly name for this computer.
|
|||||||
local passwordIntro = [[A password is required for wireless access.
|
local passwordIntro = [[A password is required for wireless access.
|
||||||
|
|
||||||
%sLeave blank to skip.]]
|
%sLeave blank to skip.]]
|
||||||
local packagesIntro = [[Setup Complete
|
local packagesIntro = [[Optional Packages
|
||||||
|
|
||||||
%sOpen the package manager to add software to this computer.]]
|
%sSelect packages to install with this computer. You can always add more later from the Package Manager.]]
|
||||||
local contributorsIntro = [[Contributors%s
|
local contributorsIntro = [[Contributors%s
|
||||||
|
|
||||||
Anavrins: Encryption/security/custom apps
|
Anavrins: Encryption/security/custom apps
|
||||||
@@ -28,7 +29,7 @@ LDDestroier: Art design + custom apps
|
|||||||
Lemmmy: Application improvements
|
Lemmmy: Application improvements
|
||||||
|
|
||||||
%sContribute at:%s
|
%sContribute at:%s
|
||||||
https://github.com/kepler155c/opus]]
|
https://git.spatulaa.com/MayaTheShy/Opus]]
|
||||||
|
|
||||||
local page = UI.Page {
|
local page = UI.Page {
|
||||||
wizard = UI.Wizard {
|
wizard = UI.Wizard {
|
||||||
@@ -93,17 +94,54 @@ local page = UI.Page {
|
|||||||
},
|
},
|
||||||
packages = UI.WizardPage {
|
packages = UI.WizardPage {
|
||||||
index = 4,
|
index = 4,
|
||||||
button = UI.Button {
|
|
||||||
x = 3, y = -3,
|
|
||||||
text = 'Open Package Manager',
|
|
||||||
event = 'packages',
|
|
||||||
},
|
|
||||||
intro = UI.TextArea {
|
intro = UI.TextArea {
|
||||||
textColor = colors.yellow,
|
textColor = colors.yellow,
|
||||||
inactive = true,
|
inactive = true,
|
||||||
x = 3, ex = -3, y = 2, ey = -4,
|
x = 3, ex = -3, y = 2, ey = 5,
|
||||||
value = string.format(packagesIntro, Ansi.white),
|
value = string.format(packagesIntro, Ansi.white),
|
||||||
},
|
},
|
||||||
|
chkTurtle = UI.Checkbox {
|
||||||
|
x = 5, y = 7,
|
||||||
|
label = 'RemoteTurtle',
|
||||||
|
textColor = 'yellow',
|
||||||
|
backgroundColor = 'primary',
|
||||||
|
value = false,
|
||||||
|
},
|
||||||
|
lblTurtle = UI.Text {
|
||||||
|
x = 22, y = 7,
|
||||||
|
value = 'Turtle control + web dashboard',
|
||||||
|
textColor = colors.lightGray,
|
||||||
|
},
|
||||||
|
chkInventory = UI.Checkbox {
|
||||||
|
x = 5, y = 9,
|
||||||
|
label = 'Inventory Manager',
|
||||||
|
textColor = 'yellow',
|
||||||
|
backgroundColor = 'primary',
|
||||||
|
value = false,
|
||||||
|
},
|
||||||
|
lblInventory = UI.Text {
|
||||||
|
x = 28, y = 9,
|
||||||
|
value = 'Storage automation system',
|
||||||
|
textColor = colors.lightGray,
|
||||||
|
},
|
||||||
|
button = UI.Button {
|
||||||
|
x = 3, y = -3,
|
||||||
|
text = 'More Packages...',
|
||||||
|
event = 'packages',
|
||||||
|
},
|
||||||
|
validate = function(self)
|
||||||
|
local toInstall = { }
|
||||||
|
if self.chkTurtle.value then
|
||||||
|
table.insert(toInstall, 'remoteturtle')
|
||||||
|
end
|
||||||
|
if self.chkInventory.value then
|
||||||
|
table.insert(toInstall, 'inventory-manager')
|
||||||
|
end
|
||||||
|
for _, pkg in ipairs(toInstall) do
|
||||||
|
shell.run('package install ' .. pkg)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end,
|
||||||
},
|
},
|
||||||
contributors = UI.WizardPage {
|
contributors = UI.WizardPage {
|
||||||
index = 5,
|
index = 5,
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
local SHA = require("opus.crypto.sha2")
|
local SHA = require("opus.crypto.sha2")
|
||||||
|
|
||||||
local acceptableCharacters = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
|
local acceptableCharacters = {}
|
||||||
|
for c = 0, 127 do
|
||||||
|
local char = string.char(c)
|
||||||
|
-- exclude potentially ambiguous characters
|
||||||
|
if char:match("[1-9a-zA-Z]") and char:match("[^OIl]") then
|
||||||
|
table.insert(acceptableCharacters, char)
|
||||||
|
end
|
||||||
|
end
|
||||||
local acceptableCharactersLen = #acceptableCharacters
|
local acceptableCharactersLen = #acceptableCharacters
|
||||||
|
|
||||||
local password = ""
|
local password = ""
|
||||||
|
|
||||||
for _i = 1, 8 do
|
for i = 1, 10 do
|
||||||
password = password .. acceptableCharacters[math.random(acceptableCharactersLen)]
|
password = password .. acceptableCharacters[math.random(acceptableCharactersLen)]
|
||||||
end
|
end
|
||||||
|
|
||||||
os.queueEvent("set_otp", SHA.compute(password))
|
os.queueEvent("set_otp", SHA.compute(password))
|
||||||
|
|
||||||
print("Your one-time password is: " .. password)
|
print("This allows one other device to permanently gain access to this device.")
|
||||||
|
print("Use the trust settings in System to revert this.")
|
||||||
|
print("Your one-time password is: " .. password)
|
||||||
|
|||||||
200
sys/apps/memprofile.lua
Normal file
200
sys/apps/memprofile.lua
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
-- Memory Profiler for CC:Tweaked / Opus OS
|
||||||
|
-- Usage: memprofile [--watch] [--interval <seconds>]
|
||||||
|
--
|
||||||
|
-- Shows current Lua memory usage with breakdown estimates.
|
||||||
|
-- Use --watch to continuously monitor.
|
||||||
|
-- Useful for detecting memory leaks and understanding overhead.
|
||||||
|
|
||||||
|
local args = { ... }
|
||||||
|
local watch = false
|
||||||
|
local interval = 3
|
||||||
|
|
||||||
|
for i, arg in ipairs(args) do
|
||||||
|
if arg == '--watch' or arg == '-w' then
|
||||||
|
watch = true
|
||||||
|
elseif arg == '--interval' or arg == '-i' then
|
||||||
|
interval = tonumber(args[i + 1]) or 3
|
||||||
|
elseif arg == '--help' or arg == '-h' then
|
||||||
|
print('Usage: memprofile [--watch] [--interval <secs>]')
|
||||||
|
print('')
|
||||||
|
print('Options:')
|
||||||
|
print(' --watch, -w Continuously monitor memory')
|
||||||
|
print(' --interval, -i N Update interval in seconds (default: 3)')
|
||||||
|
print(' --help, -h Show this help')
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local term = _G.term
|
||||||
|
local os = _G.os
|
||||||
|
|
||||||
|
local function formatBytes(bytes)
|
||||||
|
if bytes < 1024 then
|
||||||
|
return string.format('%d B', bytes)
|
||||||
|
elseif bytes < 1024 * 1024 then
|
||||||
|
return string.format('%.1f KB', bytes / 1024)
|
||||||
|
else
|
||||||
|
return string.format('%.2f MB', bytes / (1024 * 1024))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function countTable(t, seen)
|
||||||
|
if type(t) ~= 'table' or seen[t] then return 0, 0 end
|
||||||
|
seen[t] = true
|
||||||
|
local entries = 0
|
||||||
|
local nested = 0
|
||||||
|
for k, v in pairs(t) do
|
||||||
|
entries = entries + 1
|
||||||
|
if type(v) == 'table' then
|
||||||
|
local e, n = countTable(v, seen)
|
||||||
|
nested = nested + 1 + e
|
||||||
|
entries = entries + n
|
||||||
|
end
|
||||||
|
if type(k) == 'table' then
|
||||||
|
local e, n = countTable(k, seen)
|
||||||
|
nested = nested + 1 + e
|
||||||
|
entries = entries + n
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return entries, nested
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getSnapshot()
|
||||||
|
-- Force a full GC cycle to get accurate usage
|
||||||
|
collectgarbage('collect')
|
||||||
|
collectgarbage('collect')
|
||||||
|
local memKB = collectgarbage('count') -- returns KB as float
|
||||||
|
|
||||||
|
local snapshot = {
|
||||||
|
totalKB = memKB,
|
||||||
|
totalBytes = math.floor(memKB * 1024),
|
||||||
|
timestamp = os.clock(),
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Count entries in major global tables
|
||||||
|
local seen = {}
|
||||||
|
local globals = {}
|
||||||
|
|
||||||
|
local interesting = {
|
||||||
|
{ name = '_G (globals)', tbl = _G },
|
||||||
|
{ name = 'kernel', tbl = _G.kernel },
|
||||||
|
{ name = 'network', tbl = _G.network },
|
||||||
|
{ name = 'device', tbl = _G.device },
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item in ipairs(interesting) do
|
||||||
|
if type(item.tbl) == 'table' then
|
||||||
|
local entries, nested = countTable(item.tbl, seen)
|
||||||
|
table.insert(globals, {
|
||||||
|
name = item.name,
|
||||||
|
entries = entries,
|
||||||
|
nested = nested,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
snapshot.globals = globals
|
||||||
|
|
||||||
|
-- Count routines if kernel is available
|
||||||
|
if _G.kernel and _G.kernel.routines then
|
||||||
|
snapshot.routines = #_G.kernel.routines
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Count loaded modules
|
||||||
|
if package and package.loaded then
|
||||||
|
local count = 0
|
||||||
|
for _ in pairs(package.loaded) do
|
||||||
|
count = count + 1
|
||||||
|
end
|
||||||
|
snapshot.loadedModules = count
|
||||||
|
end
|
||||||
|
|
||||||
|
return snapshot
|
||||||
|
end
|
||||||
|
|
||||||
|
local function printSnapshot(snap, prev)
|
||||||
|
term.clear()
|
||||||
|
term.setCursorPos(1, 1)
|
||||||
|
|
||||||
|
local w = term.getSize()
|
||||||
|
local sep = string.rep('-', w)
|
||||||
|
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
print('=== Memory Profile ===')
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
print('')
|
||||||
|
|
||||||
|
-- Total memory
|
||||||
|
local memStr = formatBytes(snap.totalBytes)
|
||||||
|
local deltaStr = ''
|
||||||
|
if prev then
|
||||||
|
local delta = snap.totalBytes - prev.totalBytes
|
||||||
|
if delta > 0 then
|
||||||
|
deltaStr = string.format(' (+%s)', formatBytes(delta))
|
||||||
|
term.setTextColor(colors.red)
|
||||||
|
elseif delta < 0 then
|
||||||
|
deltaStr = string.format(' (-%s)', formatBytes(-delta))
|
||||||
|
term.setTextColor(colors.green)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
print(string.format('Total Memory: %s%s', memStr, deltaStr))
|
||||||
|
print(string.format('Uptime: %.1fs', snap.timestamp))
|
||||||
|
print('')
|
||||||
|
|
||||||
|
-- Table sizes
|
||||||
|
term.setTextColor(colors.lightBlue)
|
||||||
|
print('Global Tables:')
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
print(sep)
|
||||||
|
print(string.format(' %-20s %8s %8s', 'Name', 'Entries', 'Nested'))
|
||||||
|
print(sep)
|
||||||
|
for _, g in ipairs(snap.globals) do
|
||||||
|
print(string.format(' %-20s %8d %8d', g.name, g.entries, g.nested))
|
||||||
|
end
|
||||||
|
print(sep)
|
||||||
|
print('')
|
||||||
|
|
||||||
|
-- Kernel info
|
||||||
|
if snap.routines then
|
||||||
|
term.setTextColor(colors.lightBlue)
|
||||||
|
print('Kernel:')
|
||||||
|
term.setTextColor(colors.white)
|
||||||
|
print(string.format(' Active routines: %d', snap.routines))
|
||||||
|
end
|
||||||
|
|
||||||
|
if snap.loadedModules then
|
||||||
|
print(string.format(' Loaded modules: %d', snap.loadedModules))
|
||||||
|
end
|
||||||
|
|
||||||
|
print('')
|
||||||
|
|
||||||
|
-- CC:Tweaked limits
|
||||||
|
term.setTextColor(colors.gray)
|
||||||
|
print('Note: CC:Tweaked default memory limit is ~128MB per computer.')
|
||||||
|
print('High memory usage may cause slowdowns or crashes.')
|
||||||
|
|
||||||
|
if watch then
|
||||||
|
print('')
|
||||||
|
term.setTextColor(colors.yellow)
|
||||||
|
print(string.format('Refreshing every %ds... (Ctrl+T to stop)', interval))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function run()
|
||||||
|
local prev = nil
|
||||||
|
|
||||||
|
if watch then
|
||||||
|
while true do
|
||||||
|
local snap = getSnapshot()
|
||||||
|
printSnapshot(snap, prev)
|
||||||
|
prev = snap
|
||||||
|
os.sleep(interval)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local snap = getSnapshot()
|
||||||
|
printSnapshot(snap, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
run()
|
||||||
@@ -59,6 +59,10 @@ local function sambaConnection(socket)
|
|||||||
print('samba: Connection closed')
|
print('samba: Connection closed')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function sanitizeLabel(computer)
|
||||||
|
return (computer.id.."_"..computer.label:gsub("[%c%.\"'/%*]", "")):sub(1, 40)
|
||||||
|
end
|
||||||
|
|
||||||
Event.addRoutine(function()
|
Event.addRoutine(function()
|
||||||
print('samba: listening on port 139')
|
print('samba: listening on port 139')
|
||||||
|
|
||||||
@@ -79,10 +83,10 @@ Event.addRoutine(function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
Event.on('network_attach', function(_, computer)
|
Event.on('network_attach', function(_, computer)
|
||||||
fs.mount(fs.combine('network', computer.label), 'netfs', computer.id)
|
fs.mount(fs.combine('network', sanitizeLabel(computer)), 'netfs', computer.id)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Event.on('network_detach', function(_, computer)
|
Event.on('network_detach', function(_, computer)
|
||||||
print('samba: detaching ' .. computer.label)
|
print('samba: detaching ' .. sanitizeLabel(computer))
|
||||||
fs.unmount(fs.combine('network', computer.label))
|
fs.unmount(fs.combine('network', sanitizeLabel(computer)))
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ local function getSlots()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function sendInfo()
|
local function sendInfo()
|
||||||
if os.clock() - infoTimer >= 1 then -- don't flood
|
if os.clock() - infoTimer >= 5 then -- don't flood
|
||||||
infoTimer = os.clock()
|
infoTimer = os.clock()
|
||||||
info.label = os.getComputerLabel()
|
info.label = os.getComputerLabel()
|
||||||
info.uptime = math.floor(os.clock())
|
info.uptime = math.floor(os.clock())
|
||||||
@@ -194,16 +194,25 @@ local function sendInfo()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- every 10 seconds, send out this computer's info
|
local function cleanNetwork()
|
||||||
Event.onInterval(10, function()
|
|
||||||
sendInfo()
|
|
||||||
for _,c in pairs(_G.network) do
|
for _,c in pairs(_G.network) do
|
||||||
local elapsed = os.clock()-c.timestamp
|
local elapsed = os.clock()-c.timestamp
|
||||||
if c.active and elapsed > 15 then
|
if c.active and elapsed > 50 then
|
||||||
c.active = false
|
c.active = false
|
||||||
os.queueEvent('network_detach', c)
|
os.queueEvent('network_detach', c)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- every 30 seconds, send out this computer's info
|
||||||
|
-- send with offset so that messages are evenly distributed and do not all come at once
|
||||||
|
Event.onTimeout(math.random() * 30, function()
|
||||||
|
sendInfo()
|
||||||
|
cleanNetwork()
|
||||||
|
Event.onInterval(30, function()
|
||||||
|
sendInfo()
|
||||||
|
cleanNetwork()
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Event.on('turtle_response', function()
|
Event.on('turtle_response', function()
|
||||||
@@ -213,4 +222,5 @@ Event.on('turtle_response', function()
|
|||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Event.onTimeout(1, sendInfo)
|
-- send info early so that computers show soon after booting
|
||||||
|
Event.onTimeout(math.random() * 2 + 1, sendInfo)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
local Crypto = require('opus.crypto.chacha20')
|
local Crypto = require('opus.crypto.chacha20')
|
||||||
local Event = require('opus.event')
|
local Event = require('opus.event')
|
||||||
|
local SHA = require('opus.crypto.sha2')
|
||||||
|
|
||||||
local network = _G.network
|
local network = _G.network
|
||||||
local os = _G.os
|
local os = _G.os
|
||||||
@@ -35,7 +36,19 @@ function transport.read(socket)
|
|||||||
local data = table.remove(socket.messages, 1)
|
local data = table.remove(socket.messages, 1)
|
||||||
if data then
|
if data then
|
||||||
if socket.options.ENCRYPT then
|
if socket.options.ENCRYPT then
|
||||||
return table.unpack(Crypto.decrypt(data[1], socket.enckey)), data[2]
|
local ciphertext = data[1]
|
||||||
|
-- Verify HMAC if present (new protocol)
|
||||||
|
if socket.hmackey and type(ciphertext) == 'table' and ciphertext.hmac then
|
||||||
|
local expected = SHA.hmac(
|
||||||
|
ciphertext[1] .. ciphertext[2],
|
||||||
|
socket.hmackey
|
||||||
|
):toHex()
|
||||||
|
if expected ~= ciphertext.hmac then
|
||||||
|
_G._syslog('transport: HMAC verification failed on port ' .. socket.sport)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return table.unpack(Crypto.decrypt(ciphertext, socket.enckey)), data[2]
|
||||||
end
|
end
|
||||||
return table.unpack(data)
|
return table.unpack(data)
|
||||||
end
|
end
|
||||||
@@ -78,7 +91,15 @@ Event.on('transport_encrypt', function()
|
|||||||
|
|
||||||
if socket and socket.connected then
|
if socket and socket.connected then
|
||||||
local msg = entry[2]
|
local msg = entry[2]
|
||||||
msg.data = Crypto.encrypt({ msg.data }, socket.enckey)
|
local encrypted = Crypto.encrypt({ msg.data }, socket.enckey)
|
||||||
|
-- Attach HMAC if key is available
|
||||||
|
if socket.hmackey then
|
||||||
|
encrypted.hmac = SHA.hmac(
|
||||||
|
encrypted[1] .. encrypted[2],
|
||||||
|
socket.hmackey
|
||||||
|
):toHex()
|
||||||
|
end
|
||||||
|
msg.data = encrypted
|
||||||
socket.transmit(socket.dport, socket.dhost, msg)
|
socket.transmit(socket.dport, socket.dhost, msg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ end
|
|||||||
local function trustConnection(socket)
|
local function trustConnection(socket)
|
||||||
local data = socket:read(2)
|
local data = socket:read(2)
|
||||||
if data then
|
if data then
|
||||||
local password = Security.getPassword()
|
local trustKey = Security.getTrustKey()
|
||||||
if not password then
|
if not trustKey then
|
||||||
socket:write({ msg = 'No password has been set' })
|
socket:write({ msg = 'No password has been set' })
|
||||||
else
|
else
|
||||||
if validateData(data, password, socket.dhost) then
|
if validateData(data, trustKey, socket.dhost) then
|
||||||
print("Accepted trust from " .. socket.dhost)
|
print("Accepted trust from " .. socket.dhost)
|
||||||
socket:write({ success = true, msg = 'Trust accepted' })
|
socket:write({ success = true, msg = 'Trust accepted' })
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -78,6 +78,19 @@ local function install(name, isUpdate, ignoreDeps)
|
|||||||
local packageDir = fs.combine('packages', name)
|
local packageDir = fs.combine('packages', name)
|
||||||
|
|
||||||
local list = Git.list(manifest.repository)
|
local list = Git.list(manifest.repository)
|
||||||
|
|
||||||
|
-- apply exclude filters from manifest
|
||||||
|
if manifest.exclude then
|
||||||
|
for path in pairs(list) do
|
||||||
|
for _, pattern in ipairs(manifest.exclude) do
|
||||||
|
if path:match(pattern) then
|
||||||
|
list[path] = nil
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- clear out contents before install/update
|
-- clear out contents before install/update
|
||||||
-- TODO: figure out whether to run
|
-- TODO: figure out whether to run
|
||||||
-- install/uninstall for the package
|
-- install/uninstall for the package
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
local Security = require('opus.security')
|
local Security = require('opus.security')
|
||||||
local SHA = require('opus.crypto.sha2')
|
|
||||||
local Terminal = require('opus.terminal')
|
local Terminal = require('opus.terminal')
|
||||||
|
|
||||||
local password = Terminal.readPassword('Enter new password: ')
|
local password = Terminal.readPassword('Enter new password: ')
|
||||||
|
|
||||||
if password then
|
if password then
|
||||||
Security.updatePassword(SHA.compute(password))
|
Security.updatePassword(password)
|
||||||
print('Password updated')
|
print('Password updated')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ return UI.Tab {
|
|||||||
x = 2, y = 5, ex = -2, ey = -2,
|
x = 2, y = 5, ex = -2, ey = -2,
|
||||||
values = {
|
values = {
|
||||||
{ name = '', value = '' },
|
{ name = '', value = '' },
|
||||||
{ name = 'CC version', value = Util.getVersion() },
|
{ name = 'CC version', value = ("%d.%d"):format(Util.getVersion()) },
|
||||||
{ name = 'Lua version', value = _VERSION },
|
{ name = 'Lua version', value = _VERSION },
|
||||||
{ name = 'MC version', value = Util.getMinecraftVersion() },
|
{ name = 'MC version', value = Util.getMinecraftVersion() },
|
||||||
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
|
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
|
||||||
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
|
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
|
||||||
{ name = 'Day', value = tostring(os.day()) },
|
{ name = 'Day', value = tostring(os.day()) },
|
||||||
},
|
},
|
||||||
disableHeader = true,
|
disableHeader = true,
|
||||||
inactive = true,
|
inactive = true,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ local Util = require('opus.util')
|
|||||||
local fs = _G.fs
|
local fs = _G.fs
|
||||||
local shell = _ENV.shell
|
local shell = _ENV.shell
|
||||||
|
|
||||||
local URL = 'https://raw.githubusercontent.com/kepler155c/opus/%s/.opus_version'
|
local URL = 'https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/%s/.opus_version'
|
||||||
|
|
||||||
if fs.exists('.opus_version') then
|
if fs.exists('.opus_version') then
|
||||||
local f = fs.open('.opus_version', 'r')
|
local f = fs.open('.opus_version', 'r')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
sys/apps/pain.lua urlfs https://github.com/LDDestroier/CC/raw/master/pain.lua
|
sys/apps/pain.lua urlfs https://github.com/LDDestroier/CC/raw/master/pain.lua
|
||||||
sys/apps/update.lua urlfs http://pastebin.com/raw/UzGHLbNC
|
sys/apps/update.lua urlfs https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/main/installer.lua
|
||||||
sys/apps/Enchat.lua urlfs https://raw.githubusercontent.com/LDDestroier/enchat/master/enchat3.lua
|
sys/apps/Enchat.lua urlfs https://raw.githubusercontent.com/LDDestroier/enchat/master/enchat3.lua
|
||||||
sys/apps/cloud.lua urlfs https://cloud-catcher.squiddev.cc/cloud.lua
|
sys/apps/cloud.lua urlfs https://cloud-catcher.squiddev.cc/cloud.lua
|
||||||
rom/modules/main/opus linkfs sys/modules/opus
|
rom/modules/main/opus linkfs sys/modules/opus
|
||||||
6
sys/etc/packages.list
Normal file
6
sys/etc/packages.list
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
["platform"] = "https://git.spatulaa.com/MayaTheShy/cc-platform-core/raw/branch/master/.package",
|
||||||
|
["remoteturtle"] = "https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/.package",
|
||||||
|
["inventory-manager"] = "https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/stable/.package",
|
||||||
|
["inventory-manager-unstable"] = "https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/.package",
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ local parentTerm = _G.device.terminal
|
|||||||
local w,h = parentTerm.getSize()
|
local w,h = parentTerm.getSize()
|
||||||
local overviewId
|
local overviewId
|
||||||
local tabsDirty = false
|
local tabsDirty = false
|
||||||
local closeInd = Util.getVersion() >= 1.76 and '\215' or '*'
|
local closeInd = Util.supportsExtChars() and '\215' or '*'
|
||||||
local multishell = { }
|
local multishell = { }
|
||||||
|
|
||||||
_ENV.multishell = multishell
|
_ENV.multishell = multishell
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ local cbor = require('opus.cbor')
|
|||||||
local sha2 = require('opus.crypto.sha2')
|
local sha2 = require('opus.crypto.sha2')
|
||||||
local Util = require('opus.util')
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local ROUNDS = 8 -- Adjust this for speed tradeoff
|
local ROUNDS = 20 -- Standard ChaCha20 (was 8, upgraded for security)
|
||||||
|
|
||||||
local bxor = bit32.bxor
|
local bxor = bit32.bxor
|
||||||
local band = bit32.band
|
local band = bit32.band
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ for _,m in pairs(methods) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
function linkfs.resolve(node, dir)
|
function linkfs.resolve(node, dir)
|
||||||
return dir:gsub(node.mountPoint, node.source, 1)
|
local mp = node.mountPoint
|
||||||
|
if dir:sub(1, #mp) == mp then
|
||||||
|
return node.source .. dir:sub(#mp + 1)
|
||||||
|
end
|
||||||
|
return dir
|
||||||
end
|
end
|
||||||
|
|
||||||
function linkfs.mount(path, source)
|
function linkfs.mount(path, source)
|
||||||
@@ -41,8 +45,8 @@ function linkfs.mount(path, source)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function linkfs.copy(node, s, t)
|
function linkfs.copy(node, s, t)
|
||||||
s = s:gsub(node.mountPoint, node.source, 1)
|
s = linkfs.resolve(node, s)
|
||||||
t = t:gsub(node.mountPoint, node.source, 1)
|
t = linkfs.resolve(node, t)
|
||||||
return fs.copy(s, t)
|
return fs.copy(s, t)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -50,25 +54,29 @@ function linkfs.delete(node, dir)
|
|||||||
if dir == node.mountPoint then
|
if dir == node.mountPoint then
|
||||||
fs.unmount(node.mountPoint)
|
fs.unmount(node.mountPoint)
|
||||||
else
|
else
|
||||||
dir = dir:gsub(node.mountPoint, node.source, 1)
|
dir = linkfs.resolve(node, dir)
|
||||||
return fs.delete(dir)
|
return fs.delete(dir)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function linkfs.find(node, spec)
|
function linkfs.find(node, spec)
|
||||||
spec = spec:gsub(node.mountPoint, node.source, 1)
|
spec = linkfs.resolve(node, spec)
|
||||||
|
|
||||||
local list = fs.find(spec)
|
local list = fs.find(spec)
|
||||||
|
local src = node.source
|
||||||
|
local mp = node.mountPoint
|
||||||
for k,f in ipairs(list) do
|
for k,f in ipairs(list) do
|
||||||
list[k] = f:gsub(node.source, node.mountPoint, 1)
|
if f:sub(1, #src) == src then
|
||||||
|
list[k] = mp .. f:sub(#src + 1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return list
|
return list
|
||||||
end
|
end
|
||||||
|
|
||||||
function linkfs.move(node, s, t)
|
function linkfs.move(node, s, t)
|
||||||
s = s:gsub(node.mountPoint, node.source, 1)
|
s = linkfs.resolve(node, s)
|
||||||
t = t:gsub(node.mountPoint, node.source, 1)
|
t = linkfs.resolve(node, t)
|
||||||
return fs.move(s, t)
|
return fs.move(s, t)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,10 @@ end
|
|||||||
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx', 'attributes' }
|
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx', 'attributes' }
|
||||||
|
|
||||||
local function resolve(node, dir)
|
local function resolve(node, dir)
|
||||||
-- TODO: Wrong ! (does not support names with dashes)
|
local mp = node.mountPoint
|
||||||
dir = dir:gsub(node.mountPoint, '', 1)
|
if dir:sub(1, #mp) == mp then
|
||||||
|
dir = dir:sub(#mp + 1)
|
||||||
|
end
|
||||||
return fs.combine(node.source, dir)
|
return fs.combine(node.source, dir)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ end
|
|||||||
|
|
||||||
function netfs.mount(_, id, source)
|
function netfs.mount(_, id, source)
|
||||||
if not id or not tonumber(id) then
|
if not id or not tonumber(id) then
|
||||||
error('ramfs syntax: computerId [directory]')
|
error('netfs syntax: computerId [directory]')
|
||||||
end
|
end
|
||||||
return {
|
return {
|
||||||
id = tonumber(id),
|
id = tonumber(id),
|
||||||
|
|||||||
@@ -1,67 +1,91 @@
|
|||||||
local json = require('opus.json')
|
local json = require('opus.json')
|
||||||
local Util = require('opus.util')
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1'
|
local GITHUB_TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1'
|
||||||
local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s'
|
local GITHUB_FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s'
|
||||||
|
local GITEA_TREE_URL = 'https://%s/api/v1/repos/%s/%s/git/trees/%s?recursive=1'
|
||||||
|
local GITEA_FILE_URL = 'https://%s/%s/%s/raw/branch/%s/%s'
|
||||||
local TREE_HEADERS = {}
|
local TREE_HEADERS = {}
|
||||||
local git = { }
|
local git = { }
|
||||||
|
|
||||||
if _G._GIT_API_KEY then
|
if _G._GIT_API_KEY then
|
||||||
TREE_HEADERS.Authorization = 'token ' .. _G._GIT_API_KEY
|
TREE_HEADERS.Authorization = 'token ' .. _G._GIT_API_KEY
|
||||||
end
|
end
|
||||||
|
|
||||||
function git.list(repository)
|
local function parseTree(data, path, fileUrlFn)
|
||||||
local t = Util.split(repository, '(.-)/')
|
if data.message then
|
||||||
|
if data.message:find("API rate limit exceeded") then
|
||||||
local user = table.remove(t, 1)
|
error("Out of API calls, try again later")
|
||||||
local repo = table.remove(t, 1)
|
end
|
||||||
local branch = table.remove(t, 1) or 'master'
|
if data.message == "Not found" or data.message == "Not Found" then
|
||||||
local path
|
error("Invalid repository")
|
||||||
|
|
||||||
if not Util.empty(t) then
|
|
||||||
path = table.concat(t, '/') .. '/'
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getContents()
|
|
||||||
local dataUrl = string.format(TREE_URL, user, repo, branch)
|
|
||||||
local contents, msg = Util.httpGet(dataUrl, TREE_HEADERS)
|
|
||||||
if not contents then
|
|
||||||
error(string.format('Failed to download %s\n%s', dataUrl, msg), 2)
|
|
||||||
else
|
|
||||||
return json.decode(contents)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local data = getContents() or error('Invalid repository')
|
|
||||||
|
|
||||||
if data.message and data.message:find("API rate limit exceeded") then
|
|
||||||
error("Out of API calls, try again later")
|
|
||||||
end
|
|
||||||
|
|
||||||
if data.message and data.message == "Not found" then
|
|
||||||
error("Invalid repository")
|
|
||||||
end
|
|
||||||
|
|
||||||
local list = { }
|
local list = { }
|
||||||
for _,v in pairs(data.tree) do
|
for _, v in pairs(data.tree) do
|
||||||
if v.type == "blob" then
|
if v.type == "blob" then
|
||||||
v.path = v.path:gsub("%s","%%20")
|
v.path = v.path:gsub("%s", "%%20")
|
||||||
if not path then
|
if not path then
|
||||||
list[v.path] = {
|
list[v.path] = {
|
||||||
url = string.format(FILE_URL, user, repo, branch, v.path),
|
url = fileUrlFn(v.path),
|
||||||
size = v.size,
|
size = v.size,
|
||||||
}
|
}
|
||||||
elseif Util.startsWith(v.path, path) then
|
elseif Util.startsWith(v.path, path) then
|
||||||
local p = string.sub(v.path, #path)
|
local p = string.sub(v.path, #path)
|
||||||
list[p] = {
|
list[p] = {
|
||||||
url = string.format(FILE_URL, user, repo, branch, path .. p),
|
url = fileUrlFn(path .. p),
|
||||||
size = v.size,
|
size = v.size,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return list
|
return list
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function fetchTree(url)
|
||||||
|
local contents, msg = Util.httpGet(url, TREE_HEADERS)
|
||||||
|
if not contents then
|
||||||
|
error(string.format('Failed to download %s\n%s', url, msg), 2)
|
||||||
|
end
|
||||||
|
return json.decode(contents) or error('Invalid repository')
|
||||||
|
end
|
||||||
|
|
||||||
|
-- GitHub: user/repo/branch/subdir/
|
||||||
|
local function listGithub(repository)
|
||||||
|
local t = Util.split(repository, '(.-)/')
|
||||||
|
local user = table.remove(t, 1)
|
||||||
|
local repo = table.remove(t, 1)
|
||||||
|
local branch = table.remove(t, 1) or 'main'
|
||||||
|
local path = not Util.empty(t) and (table.concat(t, '/') .. '/') or nil
|
||||||
|
|
||||||
|
local data = fetchTree(string.format(GITHUB_TREE_URL, user, repo, branch))
|
||||||
|
return parseTree(data, path, function(p)
|
||||||
|
return string.format(GITHUB_FILE_URL, user, repo, branch, p)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Gitea: gitea://host/user/repo/branch/subdir/
|
||||||
|
local function listGitea(host, remainder)
|
||||||
|
remainder = remainder:gsub('/$', '') -- strip trailing slash
|
||||||
|
local t = Util.split(remainder, '(.-)/')
|
||||||
|
local user = table.remove(t, 1)
|
||||||
|
local repo = table.remove(t, 1)
|
||||||
|
local branch = table.remove(t, 1) or 'main'
|
||||||
|
local path = not Util.empty(t) and (table.concat(t, '/') .. '/') or nil
|
||||||
|
|
||||||
|
local data = fetchTree(string.format(GITEA_TREE_URL, host, user, repo, branch))
|
||||||
|
return parseTree(data, path, function(p)
|
||||||
|
return string.format(GITEA_FILE_URL, host, user, repo, branch, p)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
function git.list(repository)
|
||||||
|
local host_type, host, rest = repository:match('^(%w+)://(.-)/(.*)')
|
||||||
|
if host_type == 'gitea' then
|
||||||
|
return listGitea(host, rest)
|
||||||
|
end
|
||||||
|
return listGithub(repository)
|
||||||
|
end
|
||||||
|
|
||||||
return git
|
return git
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ if register_global_module_table then
|
|||||||
_G[global_module_name] = json
|
_G[global_module_name] = json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local fs = fs
|
||||||
local _ENV = nil -- blocking globals in Lua 5.2
|
local _ENV = nil -- blocking globals in Lua 5.2
|
||||||
|
|
||||||
pcall (function()
|
pcall (function()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
local Util = require('opus.util')
|
local Config = require('opus.config')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local fs = _G.fs
|
local fs = _G.fs
|
||||||
local textutils = _G.textutils
|
local textutils = _G.textutils
|
||||||
@@ -7,6 +8,21 @@ local PACKAGE_DIR = 'packages'
|
|||||||
|
|
||||||
local Packages = { }
|
local Packages = { }
|
||||||
|
|
||||||
|
-- Default package sources (upstream GitHub + self-hosted Gitea)
|
||||||
|
local DEFAULT_SOURCES = {
|
||||||
|
{
|
||||||
|
name = 'opus-apps',
|
||||||
|
branches = {
|
||||||
|
[ 'develop-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/packages.list',
|
||||||
|
[ 'master-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/master-1.8/packages.list',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name = 'spatulaa',
|
||||||
|
url = 'https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/main/sys/etc/packages.list',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
function Packages:installed()
|
function Packages:installed()
|
||||||
local list = { }
|
local list = { }
|
||||||
|
|
||||||
@@ -55,13 +71,30 @@ function Packages:isInstalled(package)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Packages:downloadList()
|
function Packages:downloadList()
|
||||||
local packages = {
|
local sources = Config.load('package').sources or DEFAULT_SOURCES
|
||||||
[ 'develop-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/packages.list',
|
local packages = { }
|
||||||
[ 'master-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/master-1.8/packages.list',
|
|
||||||
}
|
|
||||||
|
|
||||||
if packages[_G.OPUS_BRANCH] then
|
for _, source in ipairs(sources) do
|
||||||
Util.download(packages[_G.OPUS_BRANCH], 'usr/config/packages')
|
local url = source.url
|
||||||
|
if source.branches then
|
||||||
|
url = source.branches[_G.OPUS_BRANCH]
|
||||||
|
end
|
||||||
|
|
||||||
|
if url then
|
||||||
|
local content = Util.httpGet(url)
|
||||||
|
if content then
|
||||||
|
local list = textutils.unserialize(content)
|
||||||
|
if list then
|
||||||
|
for k, v in pairs(list) do
|
||||||
|
packages[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if next(packages) then
|
||||||
|
Util.writeTable('usr/config/packages', packages)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,38 @@
|
|||||||
local Config = require('opus.config')
|
local Config = require('opus.config')
|
||||||
|
local SHA = require('opus.crypto.sha2')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
|
local PBKDF2_ITERATIONS = 100
|
||||||
|
|
||||||
local Security = { }
|
local Security = { }
|
||||||
|
|
||||||
|
local function generateSalt()
|
||||||
|
local salt = { }
|
||||||
|
for _ = 1, 16 do
|
||||||
|
salt[#salt + 1] = math.random(0, 0xFF)
|
||||||
|
end
|
||||||
|
return setmetatable(salt, Util.byteArrayMT):toHex()
|
||||||
|
end
|
||||||
|
|
||||||
function Security.verifyPassword(password)
|
function Security.verifyPassword(password)
|
||||||
local current = Security.getPassword()
|
local stored = Security.getPassword()
|
||||||
return current and password == current
|
if not stored then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- New format: { hash = hex, salt = hex, iter = N }
|
||||||
|
if type(stored) == 'table' and stored.hash and stored.salt then
|
||||||
|
local iter = stored.iter or PBKDF2_ITERATIONS
|
||||||
|
local derived = SHA.pbkdf2(password, Util.hexToByteArray(stored.salt), iter)
|
||||||
|
return derived:toHex() == stored.hash
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Legacy format: plain SHA-256 hex string
|
||||||
|
if type(stored) == 'string' then
|
||||||
|
return SHA.compute(password) == stored
|
||||||
|
end
|
||||||
|
|
||||||
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
function Security.hasPassword()
|
function Security.hasPassword()
|
||||||
@@ -28,8 +56,16 @@ function Security.getIdentifier()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Security.updatePassword(password)
|
function Security.updatePassword(password)
|
||||||
|
local salt = generateSalt()
|
||||||
|
local derived = SHA.pbkdf2(password, Util.hexToByteArray(salt), PBKDF2_ITERATIONS)
|
||||||
|
|
||||||
local config = Config.load('os')
|
local config = Config.load('os')
|
||||||
config.password = password
|
config.password = {
|
||||||
|
hash = derived:toHex(),
|
||||||
|
salt = salt,
|
||||||
|
iter = PBKDF2_ITERATIONS,
|
||||||
|
trustKey = SHA.compute(password),
|
||||||
|
}
|
||||||
Config.update('os', config)
|
Config.update('os', config)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -37,4 +73,15 @@ function Security.getPassword()
|
|||||||
return Config.load('os').password
|
return Config.load('os').password
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Returns the trust key for ChaCha20-based trust protocol.
|
||||||
|
-- Compatible with both new (PBKDF2 table) and legacy (SHA-256 string) formats.
|
||||||
|
function Security.getTrustKey()
|
||||||
|
local stored = Security.getPassword()
|
||||||
|
if type(stored) == 'table' then
|
||||||
|
return stored.trustKey
|
||||||
|
end
|
||||||
|
-- Legacy: the stored string IS the SHA-256 hex
|
||||||
|
return stored
|
||||||
|
end
|
||||||
|
|
||||||
return Security
|
return Security
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ end
|
|||||||
local function setupCrypto(socket, isClient)
|
local function setupCrypto(socket, isClient)
|
||||||
socket.sharedKey = ECC.exchange(socket.privKey, socket.remotePubKey)
|
socket.sharedKey = ECC.exchange(socket.privKey, socket.remotePubKey)
|
||||||
socket.enckey = SHA.pbkdf2(socket.sharedKey, "1enc", 1)
|
socket.enckey = SHA.pbkdf2(socket.sharedKey, "1enc", 1)
|
||||||
--self.hmackey = SHA.pbkdf2(self.sharedKey, "2hmac", 1)
|
socket.hmackey = SHA.pbkdf2(socket.sharedKey, "2hmac", 1)
|
||||||
|
|
||||||
socket.rrng = Crypto.newRNG(
|
socket.rrng = Crypto.newRNG(
|
||||||
SHA.pbkdf2(socket.sharedKey, isClient and "3rseed" or "4sseed", 1))
|
SHA.pbkdf2(socket.sharedKey, isClient and "3rseed" or "4sseed", 1))
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function UI:init()
|
|||||||
tertiary = colors.gray,
|
tertiary = colors.gray,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.extChars = Util.getVersion() >= 1.76
|
self.extChars = Util.supportsExtChars()
|
||||||
|
|
||||||
local function keyFunction(event, code, held)
|
local function keyFunction(event, code, held)
|
||||||
local ie = Input:translate(event, code, held)
|
local ie = Input:translate(event, code, held)
|
||||||
@@ -115,12 +115,16 @@ function UI:init()
|
|||||||
local ie = Input:translate('mouse_up', button, x, y)
|
local ie = Input:translate('mouse_up', button, x, y)
|
||||||
local currentPage = self:getActivePage()
|
local currentPage = self:getActivePage()
|
||||||
|
|
||||||
if ie.code == 'control-shift-mouse_click' then -- hack
|
if ie.code == 'control-shift-mouse_click' then -- debug inspector
|
||||||
local event = currentPage:pointToChild(x, y)
|
local Config = require('opus.config')
|
||||||
_ENV.multishell.openTab(_ENV, {
|
local debugCfg = Config.load('os', { debug_inspector = false })
|
||||||
path = 'sys/apps/Lua.lua',
|
if debugCfg.debug_inspector then
|
||||||
args = { event.element, self, _ENV },
|
local event = currentPage:pointToChild(x, y)
|
||||||
focused = true })
|
_ENV.multishell.openTab(_ENV, {
|
||||||
|
path = 'sys/apps/Lua.lua',
|
||||||
|
args = { event.element, self, _ENV },
|
||||||
|
focused = true })
|
||||||
|
end
|
||||||
|
|
||||||
elseif ie and currentPage and currentPage.parent.device.side == side then
|
elseif ie and currentPage and currentPage.parent.device.side == side then
|
||||||
self:click(currentPage, ie)
|
self:click(currentPage, ie)
|
||||||
|
|||||||
@@ -26,6 +26,28 @@ function Writer:write(s, width, align, bg, fg)
|
|||||||
self.x = self.x + width
|
self.x = self.x + width
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Writer:writeBar(width, ratio, barColor, emptyColor, text, textColor)
|
||||||
|
local filled = math.floor(ratio * width)
|
||||||
|
local empty = width - filled
|
||||||
|
if filled > 0 then
|
||||||
|
self.element:write(self.x, self.y, _rep(' ', filled), barColor)
|
||||||
|
end
|
||||||
|
if empty > 0 then
|
||||||
|
self.element:write(self.x + filled, self.y, _rep(' ', empty), emptyColor)
|
||||||
|
end
|
||||||
|
if text and #text > 0 and textColor then
|
||||||
|
local tx = self.x + math.floor((width - #text) / 2)
|
||||||
|
for i = 1, #text do
|
||||||
|
local cx = tx + i - 1
|
||||||
|
if cx >= self.x and cx < self.x + width then
|
||||||
|
local bg = (cx - self.x) < filled and barColor or emptyColor
|
||||||
|
self.element:write(cx, self.y, text:sub(i, i), bg, textColor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
self.x = self.x + width
|
||||||
|
end
|
||||||
|
|
||||||
function Writer:finish(bg)
|
function Writer:finish(bg)
|
||||||
if self.x <= self.element.width then
|
if self.x <= self.element.width then
|
||||||
self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg)
|
self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg)
|
||||||
@@ -47,6 +69,7 @@ UI.Grid.defaults = {
|
|||||||
textSelectedColor = 'white',
|
textSelectedColor = 'white',
|
||||||
backgroundColor = 'black',
|
backgroundColor = 'black',
|
||||||
backgroundSelectedColor = 'gray',
|
backgroundSelectedColor = 'gray',
|
||||||
|
alternateRowColor = nil,
|
||||||
headerBackgroundColor = 'primary',
|
headerBackgroundColor = 'primary',
|
||||||
headerTextColor = 'white',
|
headerTextColor = 'white',
|
||||||
headerSortColor = 'yellow',
|
headerSortColor = 'yellow',
|
||||||
@@ -317,7 +340,7 @@ function UI.Grid:drawRows()
|
|||||||
local row = self:getDisplayValues(rawRow, key)
|
local row = self:getDisplayValues(rawRow, key)
|
||||||
|
|
||||||
local selected = index == self.index and not self.inactive
|
local selected = index == self.index and not self.inactive
|
||||||
local bg = self:getRowBackgroundColor(rawRow, selected)
|
local bg = self:getRowBackgroundColor(rawRow, selected, index)
|
||||||
local fg = self:getRowTextColor(rawRow, selected)
|
local fg = self:getRowTextColor(rawRow, selected)
|
||||||
local focused = self.focused and selected
|
local focused = self.focused and selected
|
||||||
|
|
||||||
@@ -335,11 +358,25 @@ function UI.Grid:drawRow(sb, row, focused, bg, fg)
|
|||||||
local ind = focused and self.focusIndicator or ' '
|
local ind = focused and self.focusIndicator or ' '
|
||||||
|
|
||||||
for _,col in pairs(self.columns) do
|
for _,col in pairs(self.columns) do
|
||||||
sb:write(ind .. safeValue(row[col.key] or ''),
|
if col.barColumn then
|
||||||
col.cw + 1,
|
-- Bar column: render a colored fill bar
|
||||||
col.align,
|
local ratio = tonumber(row[col.key]) or 0
|
||||||
col.backgroundColor or bg,
|
ratio = math.max(0, math.min(1, ratio))
|
||||||
col.textColor or fg)
|
local barColor = col.barColor or colors.lime
|
||||||
|
local emptyColor = col.barEmptyColor or bg
|
||||||
|
if type(col.barColor) == 'function' then
|
||||||
|
barColor = col.barColor(row, ratio)
|
||||||
|
end
|
||||||
|
local barText = col.barText and (type(col.barText) == 'function' and col.barText(row, ratio) or col.barText) or nil
|
||||||
|
sb:write(ind, 1, nil, bg, fg)
|
||||||
|
sb:writeBar(col.cw, ratio, barColor, emptyColor, barText, col.barTextColor or colors.white)
|
||||||
|
else
|
||||||
|
sb:write(ind .. safeValue(row[col.key] or ''),
|
||||||
|
col.cw + 1,
|
||||||
|
col.align,
|
||||||
|
col.backgroundColor or bg,
|
||||||
|
col.textColor or fg)
|
||||||
|
end
|
||||||
ind = ' '
|
ind = ' '
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -354,13 +391,16 @@ function UI.Grid:getRowTextColor(row, selected)
|
|||||||
return self.textColor
|
return self.textColor
|
||||||
end
|
end
|
||||||
|
|
||||||
function UI.Grid:getRowBackgroundColor(row, selected)
|
function UI.Grid:getRowBackgroundColor(row, selected, index)
|
||||||
if selected then
|
if selected then
|
||||||
if self.focused then
|
if self.focused then
|
||||||
return self.backgroundSelectedColor
|
return self.backgroundSelectedColor
|
||||||
end
|
end
|
||||||
return self.unfocusedBackgroundSelectedColor
|
return self.unfocusedBackgroundSelectedColor
|
||||||
end
|
end
|
||||||
|
if self.alternateRowColor and index then
|
||||||
|
return index % 2 == 0 and self.alternateRowColor or self.backgroundColor
|
||||||
|
end
|
||||||
return self.backgroundColor
|
return self.backgroundColor
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,35 @@ UI.ProgressBar.defaults = {
|
|||||||
fillColor = 'gray',
|
fillColor = 'gray',
|
||||||
textColor = 'green',
|
textColor = 'green',
|
||||||
value = 0,
|
value = 0,
|
||||||
|
showText = false, -- display value text overlay centered on bar
|
||||||
|
textOverlay = nil, -- custom overlay text (string or function(value) -> string)
|
||||||
|
textOverlayColor = 'white', -- text overlay foreground color
|
||||||
}
|
}
|
||||||
function UI.ProgressBar:draw()
|
function UI.ProgressBar:draw()
|
||||||
local width = math.ceil(self.value / 100 * self.width)
|
local width = math.ceil(self.value / 100 * self.width)
|
||||||
|
|
||||||
self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor)
|
self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor)
|
||||||
self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor)
|
self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor)
|
||||||
|
|
||||||
|
if self.showText then
|
||||||
|
local text
|
||||||
|
if self.textOverlay then
|
||||||
|
text = type(self.textOverlay) == 'function'
|
||||||
|
and self.textOverlay(self.value)
|
||||||
|
or tostring(self.textOverlay)
|
||||||
|
else
|
||||||
|
text = math.floor(self.value) .. '%'
|
||||||
|
end
|
||||||
|
local midY = math.ceil(self.height / 2)
|
||||||
|
local x = math.floor((self.width - #text) / 2) + 1
|
||||||
|
for i = 1, #text do
|
||||||
|
local cx = x + i - 1
|
||||||
|
if cx >= 1 and cx <= self.width then
|
||||||
|
local bg = cx <= width and self.progressColor or self.fillColor
|
||||||
|
self:write(cx, midY, text:sub(i, i), bg, self.textOverlayColor)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function UI.ProgressBar.example()
|
function UI.ProgressBar.example()
|
||||||
|
|||||||
@@ -170,16 +170,19 @@ function Util.print(pattern, ...)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Util.getVersion()
|
function Util.getVersion()
|
||||||
local version
|
local versionString = _G._HOST or _G._CC_VERSION
|
||||||
|
local versionMajor, versionMinor = versionString:match("(%d+)%.(%d+)")
|
||||||
|
-- ex.: 1.89 would return 1, 89
|
||||||
|
return tonumber(versionMajor), tonumber(versionMinor)
|
||||||
|
end
|
||||||
|
|
||||||
if _G._CC_VERSION then
|
function Util.compareVersion(major, minor)
|
||||||
version = tonumber(_G._CC_VERSION:match('[%d]+%.?[%d][%d]'))
|
local currentMajor, currentMinor = Util.getVersion()
|
||||||
end
|
return currentMajor > major or currentMajor == major and currentMinor >= minor
|
||||||
if not version and _G._HOST then
|
end
|
||||||
version = tonumber(_G._HOST:match('[%d]+%.?[%d][%d]'))
|
|
||||||
end
|
|
||||||
|
|
||||||
return version or 1.7
|
function Util.supportsExtChars()
|
||||||
|
return Util.compareVersion(1, 76)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Util.getMinecraftVersion()
|
function Util.getMinecraftVersion()
|
||||||
|
|||||||
Reference in New Issue
Block a user