44 Commits

Author SHA1 Message Date
MayaTheShy
4542644068 Add stable inventory-manager listing, rename main to unstable
- inventory-manager now points to stable branch (known working)
- inventory-manager-unstable points to main branch (dev/experimental)
2026-03-29 15:58:30 -04:00
MayaTheShy
b4b5f13b23 Update platform package URL to point to the correct master branch 2026-03-28 22:17:04 -04:00
MayaTheShy
d1e752c516 Add platform package URL to packages.list 2026-03-26 14:45:32 -04:00
MayaTheShy
83baef8f82 Strip trailing slash from Gitea repository path in listGitea function 2026-03-22 18:27:47 -04:00
MayaTheShy
d0cb420a08 Refactor installer.lua to enhance functionality and improve user experience 2026-03-22 17:56:15 -04:00
MayaTheShy
eae757627a Update git branch references in installer.lua to align with main branch structure 2026-03-22 17:52:18 -04:00
MayaTheShy
7a99fc662d Update update.lua URL in fstab to point to the correct installer script 2026-03-22 17:50:37 -04:00
MayaTheShy
5beb965aad Update installation instructions to reference the correct installer script 2026-03-22 17:50:32 -04:00
MayaTheShy
1e0bcb841a Add initial installer script with configuration and download functionality 2026-03-22 17:50:26 -04:00
MayaTheShy
8ade008390 Update installation instructions to use wget for startup script 2026-03-22 17:39:01 -04:00
MayaTheShy
fee8cf4f46 Enhance ProgressBar to support customizable text overlay and display percentage value 2026-03-22 16:51:13 -04:00
MayaTheShy
f2460aa5e9 Add bar rendering functionality to Writer and enhance row background color handling in Grid 2026-03-22 16:51:06 -04:00
MayaTheShy
aff772b851 Add validate handler to install selected packages from Welcome wizard 2026-03-22 16:15:46 -04:00
MayaTheShy
04751c3ba9 Enhance welcome screen with optional package selection and updated instructions 2026-03-22 16:13:28 -04:00
MayaTheShy
f53d503129 Update version URL to point to the correct repository 2026-03-22 15:56:54 -04:00
MayaTheShy
50fd8622ff Enhance package introduction display with improved color formatting 2026-03-22 15:56:47 -04:00
MayaTheShy
19edc6dc97 Update welcome screen with recommended packages and correct repository link 2026-03-22 15:56:21 -04:00
MayaTheShy
31b4fd52c9 Enhance package management UI with selection functionality and batch actions 2026-03-22 15:56:16 -04:00
MayaTheShy
ab26539660 Add initial packages list for remote package management 2026-03-22 15:56:12 -04:00
MayaTheShy
9490b713ca Add support for exclude filters in package installation 2026-03-22 15:51:04 -04:00
MayaTheShy
a914786116 Update package sources to include develop branch and improve download logic 2026-03-22 15:50:46 -04:00
MayaTheShy
03f00fc2a4 Refactor git module to support Gitea and improve error handling 2026-03-22 15:50:40 -04:00
MayaTheShy
5775954d7c Capture fs locally before setting _ENV=nil in json.lua 2026-03-22 15:19:33 -04:00
Kan18
857c0e252d Update json.lua 2026-03-22 15:18:07 -04:00
Kan18
7bd993a12d oops 2026-03-22 15:18:07 -04:00
Kan18
7d36c9aafe Fix Util.getVersion 2026-03-22 15:18:07 -04:00
Kan18
28733d77e8 oops 2026-03-22 15:18:07 -04:00
Kan18
be13d87266 actually set it to 30 2026-03-22 15:18:07 -04:00
Kan18
73616676dc oops 2026-03-22 15:18:07 -04:00
Kan18
e3ea71f5a3 Increase discovery message interval, distribute messages
Previously on large server(s), there was an issue where because
chunkloaded computers all started up at once, so they all had the same
discovery message sending timer. This prevents that by starting each off
with a random offset. Also increases the interval for sending messages
from 15 s to 30 s, so that messages (which are the same, a lot of the
time) get sent less often.
2026-03-22 15:18:07 -04:00
Kan18
fd6f39770a Sanitize label in samba.lua
Prevent labels from having .., /, *, control characters, or quotes (this
generally messes things up) and limit them to a reasonable length. We
might possibly also want to do this in snmp.lua, I'm not sure if that
will break things though
2026-03-22 15:18:07 -04:00
Kan18
88dda25911 genotp.lua changes
Make genotp more clearly state that the password can be used once but
allows permanent access (sorry for not making this clear earlier.) Use a
slightly longer password, and allow uppercase characters except for some
that could be ambiguous
2026-03-22 15:18:07 -04:00
MayaTheShy
53b38b7286 Fix variable reference for HMAC key assignment in setupCrypto function 2026-03-22 11:21:50 -04:00
MayaTheShy
c1b2e03fd6 Implement HMAC verification for encrypted messages and attach HMAC during encryption 2026-03-22 11:21:44 -04:00
MayaTheShy
4a233b1c55 Add trustKey to password update and implement getTrustKey function for ChaCha20 protocol 2026-03-22 11:21:02 -04:00
MayaTheShy
9e06241ac3 Refactor trustConnection function to use getTrustKey instead of getPassword for improved security 2026-03-22 11:20:57 -04:00
MayaTheShy
39caa32908 Implement PBKDF2 password hashing with salt for enhanced security 2026-03-22 11:19:55 -04:00
MayaTheShy
9103c44658 Remove SHA hashing from password update process 2026-03-22 11:19:47 -04:00
MayaTheShy
882894685c Refactor linkfs and netfs resolve functions for improved path handling 2026-03-22 04:09:43 -04:00
MayaTheShy
ba49f7ca7d Upgrade ChaCha20 rounds from 8 to 20 for enhanced security 2026-03-22 04:09:39 -04:00
MayaTheShy
f3c35afe07 Add debug inspector toggle for control-shift mouse click in UI 2026-03-22 04:09:10 -04:00
MayaTheShy
8a6896e276 Change default branch name from 'master' to 'main' in git.list function 2026-03-22 04:09:05 -04:00
MayaTheShy
a18a8b7140 Fix URL scheme for update.lua in fstab 2026-03-22 04:08:58 -04:00
MayaTheShy
6d6b43daf7 Add memory profiling script for CC:Tweaked / Opus OS 2026-03-22 04:08:53 -04:00
30 changed files with 1175 additions and 157 deletions

View File

@@ -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
View 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()

View File

@@ -51,7 +51,7 @@ local config = {
}
Config.load('Overview', config)
local extSupport = Util.getVersion() >= 1.76
local extSupport = Util.supportsExtChars()
local applications = { }
local buttons = { }

View File

@@ -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 '

View File

@@ -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,

View File

@@ -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
View 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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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')

View File

@@ -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
View 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",
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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),

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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()

View File

@@ -170,22 +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]'))
function Util.compareVersion(major, minor)
local currentMajor, currentMinor = Util.getVersion()
return currentMajor > major or currentMajor == major and currentMinor >= minor
end
-- stopgap fix for icons breaking on 1.100.x CC versions
-- TODO: Make this cleaner and more resiliant
if version == 1.1 then
version = 1.999
end
end
return version or 1.7
function Util.supportsExtChars()
return Util.compareVersion(1, 76)
end
function Util.getMinecraftVersion()