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
|
||||
```
|
||||
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)
|
||||
|
||||
local extSupport = Util.getVersion() >= 1.76
|
||||
local extSupport = Util.supportsExtChars()
|
||||
|
||||
local applications = { }
|
||||
local buttons = { }
|
||||
|
||||
@@ -16,25 +16,27 @@ local page = UI.Page {
|
||||
x = 2, ex = 14, y = 2, ey = -6,
|
||||
values = { },
|
||||
columns = {
|
||||
{ heading = 'Package', key = 'name' },
|
||||
{ heading = ' Package', key = 'displayName' },
|
||||
},
|
||||
sortColumn = 'name',
|
||||
autospace = true,
|
||||
help = 'Select a package',
|
||||
help = 'Space to select, Enter to toggle',
|
||||
},
|
||||
add = UI.Button {
|
||||
installSelected = UI.Button {
|
||||
x = 2, y = -3,
|
||||
text = ' + ',
|
||||
event = 'action',
|
||||
help = 'Install or update',
|
||||
event = 'batch_action',
|
||||
operation = 'install',
|
||||
operationText = 'Install',
|
||||
help = 'Install or update selected',
|
||||
},
|
||||
remove = UI.Button {
|
||||
removeSelected = UI.Button {
|
||||
x = 8, y = -3,
|
||||
text = ' - ',
|
||||
event = 'action',
|
||||
event = 'batch_action',
|
||||
operation = 'uninstall',
|
||||
operationText = 'Remove',
|
||||
help = 'Remove',
|
||||
help = 'Remove selected',
|
||||
},
|
||||
updateall = UI.Button {
|
||||
ex = -2, y = -3, width = 12,
|
||||
@@ -89,7 +91,9 @@ function page:loadPackages()
|
||||
end
|
||||
table.insert(self.grid.values, {
|
||||
installed = not not Packages:isInstalled(k),
|
||||
selected = false,
|
||||
name = k,
|
||||
displayName = k,
|
||||
manifest = manifest,
|
||||
})
|
||||
end
|
||||
@@ -104,12 +108,62 @@ function page:loadPackages()
|
||||
end
|
||||
|
||||
function page.grid:getRowTextColor(row, selected)
|
||||
if row.selected then
|
||||
return colors.cyan
|
||||
end
|
||||
if row.installed then
|
||||
return colors.yellow
|
||||
end
|
||||
return UI.Grid.getRowTextColor(self, row, selected)
|
||||
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()
|
||||
self.output.win:clear()
|
||||
UI.SlideOut.show(self)
|
||||
@@ -134,11 +188,7 @@ function page:run(operation, name)
|
||||
end
|
||||
|
||||
function page:updateSelection(selected)
|
||||
self.add.operation = selected.installed and 'update' or 'install'
|
||||
self.add.operationText = selected.installed and 'Update' or 'Install'
|
||||
self.remove.inactive = not selected.installed
|
||||
self.add:draw()
|
||||
self.remove:draw()
|
||||
-- no-op: buttons are always active for batch operations
|
||||
end
|
||||
|
||||
function page:eventHandler(event)
|
||||
@@ -152,7 +202,14 @@ function page:eventHandler(event)
|
||||
Ansi.yellow, manifest.title,
|
||||
Ansi.white, manifest.description))
|
||||
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
|
||||
config.compression = not config.compression
|
||||
@@ -160,34 +217,69 @@ function page:eventHandler(event)
|
||||
|
||||
elseif event.type == 'updateall' then
|
||||
self.operation = 'updateall'
|
||||
self.operationTargets = { }
|
||||
self.action.button.text = ' Begin '
|
||||
self.action.button.event = 'begin'
|
||||
self.action.titleBar.title = 'Update All'
|
||||
self.action:show()
|
||||
|
||||
elseif event.type == 'action' then
|
||||
local selected = self.grid:getSelected()
|
||||
if selected then
|
||||
self.operation = event.button.operation
|
||||
self.action.button.text = event.button.operationText
|
||||
self.action.titleBar.title = selected.manifest.title
|
||||
self.action.button.text = ' Begin '
|
||||
self.action.button.event = 'begin'
|
||||
self.action:show()
|
||||
elseif event.type == 'batch_action' then
|
||||
local targets = self:getSelectedPackages()
|
||||
local operation = event.button.operation
|
||||
|
||||
-- fall back to focused row if nothing selected
|
||||
if #targets == 0 then
|
||||
local focused = self.grid:getSelected()
|
||||
if focused then
|
||||
targets = { focused }
|
||||
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
|
||||
self.action:hide()
|
||||
|
||||
elseif event.type == 'begin' then
|
||||
if self.operation == 'updateall' then
|
||||
self:run(self.operation, '')
|
||||
self:run('updateall', '')
|
||||
else
|
||||
local selected = self.grid:getSelected()
|
||||
self:run(self.operation, selected.name)
|
||||
selected.installed = Packages:isInstalled(selected.name)
|
||||
|
||||
self:updateSelection(selected)
|
||||
for _, target in ipairs(self.operationTargets) do
|
||||
local op = self.operation
|
||||
if op == 'install' and target.installed then
|
||||
op = 'update'
|
||||
end
|
||||
self:run(op, target.name)
|
||||
target.installed = Packages:isInstalled(target.name)
|
||||
end
|
||||
self:clearSelection()
|
||||
self:updateStatus()
|
||||
end
|
||||
|
||||
self.action.button.text = ' Done '
|
||||
|
||||
@@ -2,6 +2,7 @@ local Ansi = require('opus.ansi')
|
||||
local Security = require('opus.security')
|
||||
local SHA = require('opus.crypto.sha2')
|
||||
local UI = require('opus.ui')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local colors = _G.colors
|
||||
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.
|
||||
|
||||
%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
|
||||
|
||||
Anavrins: Encryption/security/custom apps
|
||||
@@ -28,7 +29,7 @@ LDDestroier: Art design + custom apps
|
||||
Lemmmy: Application improvements
|
||||
|
||||
%sContribute at:%s
|
||||
https://github.com/kepler155c/opus]]
|
||||
https://git.spatulaa.com/MayaTheShy/Opus]]
|
||||
|
||||
local page = UI.Page {
|
||||
wizard = UI.Wizard {
|
||||
@@ -93,17 +94,54 @@ local page = UI.Page {
|
||||
},
|
||||
packages = UI.WizardPage {
|
||||
index = 4,
|
||||
button = UI.Button {
|
||||
x = 3, y = -3,
|
||||
text = 'Open Package Manager',
|
||||
event = 'packages',
|
||||
},
|
||||
intro = UI.TextArea {
|
||||
textColor = colors.yellow,
|
||||
inactive = true,
|
||||
x = 3, ex = -3, y = 2, ey = -4,
|
||||
x = 3, ex = -3, y = 2, ey = 5,
|
||||
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 {
|
||||
index = 5,
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
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 password = ""
|
||||
|
||||
for _i = 1, 8 do
|
||||
for i = 1, 10 do
|
||||
password = password .. acceptableCharacters[math.random(acceptableCharactersLen)]
|
||||
end
|
||||
|
||||
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')
|
||||
end
|
||||
|
||||
local function sanitizeLabel(computer)
|
||||
return (computer.id.."_"..computer.label:gsub("[%c%.\"'/%*]", "")):sub(1, 40)
|
||||
end
|
||||
|
||||
Event.addRoutine(function()
|
||||
print('samba: listening on port 139')
|
||||
|
||||
@@ -79,10 +83,10 @@ Event.addRoutine(function()
|
||||
end)
|
||||
|
||||
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)
|
||||
|
||||
Event.on('network_detach', function(_, computer)
|
||||
print('samba: detaching ' .. computer.label)
|
||||
fs.unmount(fs.combine('network', computer.label))
|
||||
print('samba: detaching ' .. sanitizeLabel(computer))
|
||||
fs.unmount(fs.combine('network', sanitizeLabel(computer)))
|
||||
end)
|
||||
|
||||
@@ -152,7 +152,7 @@ local function getSlots()
|
||||
end
|
||||
|
||||
local function sendInfo()
|
||||
if os.clock() - infoTimer >= 1 then -- don't flood
|
||||
if os.clock() - infoTimer >= 5 then -- don't flood
|
||||
infoTimer = os.clock()
|
||||
info.label = os.getComputerLabel()
|
||||
info.uptime = math.floor(os.clock())
|
||||
@@ -194,16 +194,25 @@ local function sendInfo()
|
||||
end
|
||||
end
|
||||
|
||||
-- every 10 seconds, send out this computer's info
|
||||
Event.onInterval(10, function()
|
||||
sendInfo()
|
||||
local function cleanNetwork()
|
||||
for _,c in pairs(_G.network) do
|
||||
local elapsed = os.clock()-c.timestamp
|
||||
if c.active and elapsed > 15 then
|
||||
if c.active and elapsed > 50 then
|
||||
c.active = false
|
||||
os.queueEvent('network_detach', c)
|
||||
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)
|
||||
|
||||
Event.on('turtle_response', function()
|
||||
@@ -213,4 +222,5 @@ Event.on('turtle_response', function()
|
||||
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 Event = require('opus.event')
|
||||
local SHA = require('opus.crypto.sha2')
|
||||
|
||||
local network = _G.network
|
||||
local os = _G.os
|
||||
@@ -35,7 +36,19 @@ function transport.read(socket)
|
||||
local data = table.remove(socket.messages, 1)
|
||||
if data 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
|
||||
return table.unpack(data)
|
||||
end
|
||||
@@ -78,7 +91,15 @@ Event.on('transport_encrypt', function()
|
||||
|
||||
if socket and socket.connected then
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,11 +25,11 @@ end
|
||||
local function trustConnection(socket)
|
||||
local data = socket:read(2)
|
||||
if data then
|
||||
local password = Security.getPassword()
|
||||
if not password then
|
||||
local trustKey = Security.getTrustKey()
|
||||
if not trustKey then
|
||||
socket:write({ msg = 'No password has been set' })
|
||||
else
|
||||
if validateData(data, password, socket.dhost) then
|
||||
if validateData(data, trustKey, socket.dhost) then
|
||||
print("Accepted trust from " .. socket.dhost)
|
||||
socket:write({ success = true, msg = 'Trust accepted' })
|
||||
return
|
||||
|
||||
@@ -78,6 +78,19 @@ local function install(name, isUpdate, ignoreDeps)
|
||||
local packageDir = fs.combine('packages', name)
|
||||
|
||||
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
|
||||
-- TODO: figure out whether to run
|
||||
-- install/uninstall for the package
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
local Security = require('opus.security')
|
||||
local SHA = require('opus.crypto.sha2')
|
||||
local Terminal = require('opus.terminal')
|
||||
|
||||
local password = Terminal.readPassword('Enter new password: ')
|
||||
|
||||
if password then
|
||||
Security.updatePassword(SHA.compute(password))
|
||||
Security.updatePassword(password)
|
||||
print('Password updated')
|
||||
end
|
||||
|
||||
@@ -26,12 +26,12 @@ return UI.Tab {
|
||||
x = 2, y = 5, ex = -2, ey = -2,
|
||||
values = {
|
||||
{ name = '', value = '' },
|
||||
{ name = 'CC version', value = Util.getVersion() },
|
||||
{ name = 'Lua version', value = _VERSION },
|
||||
{ name = 'MC version', value = Util.getMinecraftVersion() },
|
||||
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
|
||||
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
|
||||
{ name = 'Day', value = tostring(os.day()) },
|
||||
{ name = 'CC version', value = ("%d.%d"):format(Util.getVersion()) },
|
||||
{ name = 'Lua version', value = _VERSION },
|
||||
{ name = 'MC version', value = Util.getMinecraftVersion() },
|
||||
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
|
||||
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
|
||||
{ name = 'Day', value = tostring(os.day()) },
|
||||
},
|
||||
disableHeader = true,
|
||||
inactive = true,
|
||||
|
||||
@@ -4,7 +4,7 @@ local Util = require('opus.util')
|
||||
local fs = _G.fs
|
||||
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
|
||||
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/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/cloud.lua urlfs https://cloud-catcher.squiddev.cc/cloud.lua
|
||||
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 overviewId
|
||||
local tabsDirty = false
|
||||
local closeInd = Util.getVersion() >= 1.76 and '\215' or '*'
|
||||
local closeInd = Util.supportsExtChars() and '\215' or '*'
|
||||
local multishell = { }
|
||||
|
||||
_ENV.multishell = multishell
|
||||
|
||||
@@ -5,7 +5,7 @@ local cbor = require('opus.cbor')
|
||||
local sha2 = require('opus.crypto.sha2')
|
||||
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 band = bit32.band
|
||||
|
||||
@@ -15,7 +15,11 @@ for _,m in pairs(methods) do
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
function linkfs.mount(path, source)
|
||||
@@ -41,8 +45,8 @@ function linkfs.mount(path, source)
|
||||
end
|
||||
|
||||
function linkfs.copy(node, s, t)
|
||||
s = s:gsub(node.mountPoint, node.source, 1)
|
||||
t = t:gsub(node.mountPoint, node.source, 1)
|
||||
s = linkfs.resolve(node, s)
|
||||
t = linkfs.resolve(node, t)
|
||||
return fs.copy(s, t)
|
||||
end
|
||||
|
||||
@@ -50,25 +54,29 @@ function linkfs.delete(node, dir)
|
||||
if dir == node.mountPoint then
|
||||
fs.unmount(node.mountPoint)
|
||||
else
|
||||
dir = dir:gsub(node.mountPoint, node.source, 1)
|
||||
dir = linkfs.resolve(node, dir)
|
||||
return fs.delete(dir)
|
||||
end
|
||||
end
|
||||
|
||||
function linkfs.find(node, spec)
|
||||
spec = spec:gsub(node.mountPoint, node.source, 1)
|
||||
spec = linkfs.resolve(node, spec)
|
||||
|
||||
local list = fs.find(spec)
|
||||
local src = node.source
|
||||
local mp = node.mountPoint
|
||||
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
|
||||
|
||||
return list
|
||||
end
|
||||
|
||||
function linkfs.move(node, s, t)
|
||||
s = s:gsub(node.mountPoint, node.source, 1)
|
||||
t = t:gsub(node.mountPoint, node.source, 1)
|
||||
s = linkfs.resolve(node, s)
|
||||
t = linkfs.resolve(node, t)
|
||||
return fs.move(s, t)
|
||||
end
|
||||
|
||||
|
||||
@@ -35,8 +35,10 @@ end
|
||||
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx', 'attributes' }
|
||||
|
||||
local function resolve(node, dir)
|
||||
-- TODO: Wrong ! (does not support names with dashes)
|
||||
dir = dir:gsub(node.mountPoint, '', 1)
|
||||
local mp = node.mountPoint
|
||||
if dir:sub(1, #mp) == mp then
|
||||
dir = dir:sub(#mp + 1)
|
||||
end
|
||||
return fs.combine(node.source, dir)
|
||||
end
|
||||
|
||||
@@ -53,7 +55,7 @@ end
|
||||
|
||||
function netfs.mount(_, id, source)
|
||||
if not id or not tonumber(id) then
|
||||
error('ramfs syntax: computerId [directory]')
|
||||
error('netfs syntax: computerId [directory]')
|
||||
end
|
||||
return {
|
||||
id = tonumber(id),
|
||||
|
||||
@@ -1,67 +1,91 @@
|
||||
local json = require('opus.json')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local 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_TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1'
|
||||
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 git = { }
|
||||
|
||||
if _G._GIT_API_KEY then
|
||||
TREE_HEADERS.Authorization = 'token ' .. _G._GIT_API_KEY
|
||||
TREE_HEADERS.Authorization = 'token ' .. _G._GIT_API_KEY
|
||||
end
|
||||
|
||||
function git.list(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 'master'
|
||||
local path
|
||||
|
||||
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)
|
||||
local function parseTree(data, path, fileUrlFn)
|
||||
if data.message then
|
||||
if data.message:find("API rate limit exceeded") then
|
||||
error("Out of API calls, try again later")
|
||||
end
|
||||
if data.message == "Not found" or data.message == "Not Found" then
|
||||
error("Invalid repository")
|
||||
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 = { }
|
||||
for _,v in pairs(data.tree) do
|
||||
for _, v in pairs(data.tree) do
|
||||
if v.type == "blob" then
|
||||
v.path = v.path:gsub("%s","%%20")
|
||||
v.path = v.path:gsub("%s", "%%20")
|
||||
if not path then
|
||||
list[v.path] = {
|
||||
url = string.format(FILE_URL, user, repo, branch, v.path),
|
||||
url = fileUrlFn(v.path),
|
||||
size = v.size,
|
||||
}
|
||||
elseif Util.startsWith(v.path, path) then
|
||||
local p = string.sub(v.path, #path)
|
||||
list[p] = {
|
||||
url = string.format(FILE_URL, user, repo, branch, path .. p),
|
||||
url = fileUrlFn(path .. p),
|
||||
size = v.size,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return list
|
||||
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
|
||||
|
||||
@@ -39,6 +39,7 @@ if register_global_module_table then
|
||||
_G[global_module_name] = json
|
||||
end
|
||||
|
||||
local fs = fs
|
||||
local _ENV = nil -- blocking globals in Lua 5.2
|
||||
|
||||
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 textutils = _G.textutils
|
||||
@@ -7,6 +8,21 @@ local PACKAGE_DIR = '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()
|
||||
local list = { }
|
||||
|
||||
@@ -55,13 +71,30 @@ function Packages:isInstalled(package)
|
||||
end
|
||||
|
||||
function Packages:downloadList()
|
||||
local packages = {
|
||||
[ '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',
|
||||
}
|
||||
local sources = Config.load('package').sources or DEFAULT_SOURCES
|
||||
local packages = { }
|
||||
|
||||
if packages[_G.OPUS_BRANCH] then
|
||||
Util.download(packages[_G.OPUS_BRANCH], 'usr/config/packages')
|
||||
for _, source in ipairs(sources) do
|
||||
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
|
||||
|
||||
|
||||
@@ -1,10 +1,38 @@
|
||||
local Config = require('opus.config')
|
||||
local SHA = require('opus.crypto.sha2')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local PBKDF2_ITERATIONS = 100
|
||||
|
||||
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)
|
||||
local current = Security.getPassword()
|
||||
return current and password == current
|
||||
local stored = Security.getPassword()
|
||||
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
|
||||
|
||||
function Security.hasPassword()
|
||||
@@ -28,8 +56,16 @@ function Security.getIdentifier()
|
||||
end
|
||||
|
||||
function Security.updatePassword(password)
|
||||
local salt = generateSalt()
|
||||
local derived = SHA.pbkdf2(password, Util.hexToByteArray(salt), PBKDF2_ITERATIONS)
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -37,4 +73,15 @@ function Security.getPassword()
|
||||
return Config.load('os').password
|
||||
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
|
||||
|
||||
@@ -107,7 +107,7 @@ end
|
||||
local function setupCrypto(socket, isClient)
|
||||
socket.sharedKey = ECC.exchange(socket.privKey, socket.remotePubKey)
|
||||
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(
|
||||
SHA.pbkdf2(socket.sharedKey, isClient and "3rseed" or "4sseed", 1))
|
||||
|
||||
@@ -44,7 +44,7 @@ function UI:init()
|
||||
tertiary = colors.gray,
|
||||
}
|
||||
}
|
||||
self.extChars = Util.getVersion() >= 1.76
|
||||
self.extChars = Util.supportsExtChars()
|
||||
|
||||
local function keyFunction(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 currentPage = self:getActivePage()
|
||||
|
||||
if ie.code == 'control-shift-mouse_click' then -- hack
|
||||
local event = currentPage:pointToChild(x, y)
|
||||
_ENV.multishell.openTab(_ENV, {
|
||||
path = 'sys/apps/Lua.lua',
|
||||
args = { event.element, self, _ENV },
|
||||
focused = true })
|
||||
if ie.code == 'control-shift-mouse_click' then -- debug inspector
|
||||
local Config = require('opus.config')
|
||||
local debugCfg = Config.load('os', { debug_inspector = false })
|
||||
if debugCfg.debug_inspector then
|
||||
local event = currentPage:pointToChild(x, y)
|
||||
_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
|
||||
self:click(currentPage, ie)
|
||||
|
||||
@@ -26,6 +26,28 @@ function Writer:write(s, width, align, bg, fg)
|
||||
self.x = self.x + width
|
||||
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)
|
||||
if self.x <= self.element.width then
|
||||
self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg)
|
||||
@@ -47,6 +69,7 @@ UI.Grid.defaults = {
|
||||
textSelectedColor = 'white',
|
||||
backgroundColor = 'black',
|
||||
backgroundSelectedColor = 'gray',
|
||||
alternateRowColor = nil,
|
||||
headerBackgroundColor = 'primary',
|
||||
headerTextColor = 'white',
|
||||
headerSortColor = 'yellow',
|
||||
@@ -317,7 +340,7 @@ function UI.Grid:drawRows()
|
||||
local row = self:getDisplayValues(rawRow, key)
|
||||
|
||||
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 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 ' '
|
||||
|
||||
for _,col in pairs(self.columns) do
|
||||
sb:write(ind .. safeValue(row[col.key] or ''),
|
||||
col.cw + 1,
|
||||
col.align,
|
||||
col.backgroundColor or bg,
|
||||
col.textColor or fg)
|
||||
if col.barColumn then
|
||||
-- Bar column: render a colored fill bar
|
||||
local ratio = tonumber(row[col.key]) or 0
|
||||
ratio = math.max(0, math.min(1, ratio))
|
||||
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 = ' '
|
||||
end
|
||||
end
|
||||
@@ -354,13 +391,16 @@ function UI.Grid:getRowTextColor(row, selected)
|
||||
return self.textColor
|
||||
end
|
||||
|
||||
function UI.Grid:getRowBackgroundColor(row, selected)
|
||||
function UI.Grid:getRowBackgroundColor(row, selected, index)
|
||||
if selected then
|
||||
if self.focused then
|
||||
return self.backgroundSelectedColor
|
||||
end
|
||||
return self.unfocusedBackgroundSelectedColor
|
||||
end
|
||||
if self.alternateRowColor and index then
|
||||
return index % 2 == 0 and self.alternateRowColor or self.backgroundColor
|
||||
end
|
||||
return self.backgroundColor
|
||||
end
|
||||
|
||||
|
||||
@@ -12,12 +12,35 @@ UI.ProgressBar.defaults = {
|
||||
fillColor = 'gray',
|
||||
textColor = 'green',
|
||||
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()
|
||||
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(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
|
||||
|
||||
function UI.ProgressBar.example()
|
||||
|
||||
@@ -170,16 +170,19 @@ function Util.print(pattern, ...)
|
||||
end
|
||||
|
||||
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
|
||||
version = tonumber(_G._CC_VERSION:match('[%d]+%.?[%d][%d]'))
|
||||
end
|
||||
if not version and _G._HOST then
|
||||
version = tonumber(_G._HOST:match('[%d]+%.?[%d][%d]'))
|
||||
end
|
||||
function Util.compareVersion(major, minor)
|
||||
local currentMajor, currentMinor = Util.getVersion()
|
||||
return currentMajor > major or currentMajor == major and currentMinor >= minor
|
||||
end
|
||||
|
||||
return version or 1.7
|
||||
function Util.supportsExtChars()
|
||||
return Util.compareVersion(1, 76)
|
||||
end
|
||||
|
||||
function Util.getMinecraftVersion()
|
||||
|
||||
Reference in New Issue
Block a user