1 Commits

Author SHA1 Message Date
Merith
2b328ae309 stopgap fix for opus icons on newer MC 2022-07-04 13:08:31 +00:00
30 changed files with 157 additions and 1175 deletions

View File

@@ -16,5 +16,5 @@
## Install ## Install
``` ```
wget run https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/main/installer.lua pastebin run UzGHLbNC
``` ```

View File

@@ -1,448 +0,0 @@
--[[
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) Config.load('Overview', config)
local extSupport = Util.supportsExtChars() local extSupport = Util.getVersion() >= 1.76
local applications = { } local applications = { }
local buttons = { } local buttons = { }

View File

@@ -16,27 +16,25 @@ local page = UI.Page {
x = 2, ex = 14, y = 2, ey = -6, x = 2, ex = 14, y = 2, ey = -6,
values = { }, values = { },
columns = { columns = {
{ heading = ' Package', key = 'displayName' }, { heading = 'Package', key = 'name' },
}, },
sortColumn = 'name', sortColumn = 'name',
autospace = true, autospace = true,
help = 'Space to select, Enter to toggle', help = 'Select a package',
}, },
installSelected = UI.Button { add = UI.Button {
x = 2, y = -3, x = 2, y = -3,
text = ' + ', text = ' + ',
event = 'batch_action', event = 'action',
operation = 'install', help = 'Install or update',
operationText = 'Install',
help = 'Install or update selected',
}, },
removeSelected = UI.Button { remove = UI.Button {
x = 8, y = -3, x = 8, y = -3,
text = ' - ', text = ' - ',
event = 'batch_action', event = 'action',
operation = 'uninstall', operation = 'uninstall',
operationText = 'Remove', operationText = 'Remove',
help = 'Remove selected', help = 'Remove',
}, },
updateall = UI.Button { updateall = UI.Button {
ex = -2, y = -3, width = 12, ex = -2, y = -3, width = 12,
@@ -91,9 +89,7 @@ function page:loadPackages()
end end
table.insert(self.grid.values, { table.insert(self.grid.values, {
installed = not not Packages:isInstalled(k), installed = not not Packages:isInstalled(k),
selected = false,
name = k, name = k,
displayName = k,
manifest = manifest, manifest = manifest,
}) })
end end
@@ -108,62 +104,12 @@ function page:loadPackages()
end end
function page.grid:getRowTextColor(row, selected) function page.grid:getRowTextColor(row, selected)
if row.selected then
return colors.cyan
end
if row.installed then if row.installed then
return colors.yellow return colors.yellow
end end
return UI.Grid.getRowTextColor(self, row, selected) return UI.Grid.getRowTextColor(self, row, selected)
end end
function page:getSelectedPackages()
local selected = { }
for _, row in pairs(self.grid.values) do
if row.selected then
table.insert(selected, row)
end
end
return selected
end
function page:getSelectedCount()
local count = 0
for _, row in pairs(self.grid.values) do
if row.selected then
count = count + 1
end
end
return count
end
function page:toggleSelection(row)
row.selected = not row.selected
row.displayName = row.selected and ('\187 ' .. row.name) or row.name
self.grid:draw()
self:updateStatus()
end
function page:clearSelection()
for _, row in pairs(self.grid.values) do
row.selected = false
row.displayName = row.name
end
self.grid:draw()
end
function page:updateStatus()
local count = self:getSelectedCount()
if count > 0 then
self.statusBar:setStatus(count .. ' package(s) selected')
else
local focused = self.grid:getSelected()
if focused then
self.statusBar:setStatus(focused.installed and 'Installed' or '')
end
end
end
function page.action:show() function page.action:show()
self.output.win:clear() self.output.win:clear()
UI.SlideOut.show(self) UI.SlideOut.show(self)
@@ -188,7 +134,11 @@ function page:run(operation, name)
end end
function page:updateSelection(selected) function page:updateSelection(selected)
-- no-op: buttons are always active for batch operations 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()
end end
function page:eventHandler(event) function page:eventHandler(event)
@@ -202,14 +152,7 @@ function page:eventHandler(event)
Ansi.yellow, manifest.title, Ansi.yellow, manifest.title,
Ansi.white, manifest.description)) Ansi.white, manifest.description))
self.description:draw() self.description:draw()
self:updateStatus() self:updateSelection(event.selected)
elseif event.type == 'grid_select' then
-- Space or Enter toggles selection
local row = self.grid:getSelected()
if row then
self:toggleSelection(row)
end
elseif event.type == 'checkbox_change' then elseif event.type == 'checkbox_change' then
config.compression = not config.compression config.compression = not config.compression
@@ -217,69 +160,34 @@ function page:eventHandler(event)
elseif event.type == 'updateall' then elseif event.type == 'updateall' then
self.operation = 'updateall' self.operation = 'updateall'
self.operationTargets = { }
self.action.button.text = ' Begin ' self.action.button.text = ' Begin '
self.action.button.event = 'begin' self.action.button.event = 'begin'
self.action.titleBar.title = 'Update All' self.action.titleBar.title = 'Update All'
self.action:show() self.action:show()
elseif event.type == 'batch_action' then elseif event.type == 'action' then
local targets = self:getSelectedPackages() local selected = self.grid:getSelected()
local operation = event.button.operation if selected then
self.operation = event.button.operation
-- fall back to focused row if nothing selected self.action.button.text = event.button.operationText
if #targets == 0 then self.action.titleBar.title = selected.manifest.title
local focused = self.grid:getSelected() self.action.button.text = ' Begin '
if focused then self.action.button.event = 'begin'
targets = { focused } self.action:show()
end
end end
if #targets == 0 then return end
-- for install: update already-installed packages, install new ones
if operation == 'install' then
self.operation = 'install'
else
-- filter to only installed packages for uninstall
local installed = { }
for _, t in ipairs(targets) do
if t.installed then
table.insert(installed, t)
end
end
targets = installed
if #targets == 0 then return end
self.operation = 'uninstall'
end
self.operationTargets = targets
local title = #targets == 1
and targets[1].manifest.title
or (#targets .. ' packages')
self.action.button.text = ' Begin '
self.action.button.event = 'begin'
self.action.titleBar.title = string.format('%s %s',
operation == 'install' and 'Install/Update' or 'Remove', title)
self.action:show()
elseif event.type == 'hide-action' then elseif event.type == 'hide-action' then
self.action:hide() self.action:hide()
elseif event.type == 'begin' then elseif event.type == 'begin' then
if self.operation == 'updateall' then if self.operation == 'updateall' then
self:run('updateall', '') self:run(self.operation, '')
else else
for _, target in ipairs(self.operationTargets) do local selected = self.grid:getSelected()
local op = self.operation self:run(self.operation, selected.name)
if op == 'install' and target.installed then selected.installed = Packages:isInstalled(selected.name)
op = 'update'
end self:updateSelection(selected)
self:run(op, target.name)
target.installed = Packages:isInstalled(target.name)
end
self:clearSelection()
self:updateStatus()
end end
self.action.button.text = ' Done ' self.action.button.text = ' Done '

View File

@@ -2,7 +2,6 @@ local Ansi = require('opus.ansi')
local Security = require('opus.security') local Security = require('opus.security')
local SHA = require('opus.crypto.sha2') local SHA = require('opus.crypto.sha2')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors local colors = _G.colors
local os = _G.os local os = _G.os
@@ -17,9 +16,9 @@ local labelIntro = [[Set a friendly name for this computer.
local passwordIntro = [[A password is required for wireless access. local passwordIntro = [[A password is required for wireless access.
%sLeave blank to skip.]] %sLeave blank to skip.]]
local packagesIntro = [[Optional Packages local packagesIntro = [[Setup Complete
%sSelect packages to install with this computer. You can always add more later from the Package Manager.]] %sOpen the package manager to add software to this computer.]]
local contributorsIntro = [[Contributors%s local contributorsIntro = [[Contributors%s
Anavrins: Encryption/security/custom apps Anavrins: Encryption/security/custom apps
@@ -29,7 +28,7 @@ LDDestroier: Art design + custom apps
Lemmmy: Application improvements Lemmmy: Application improvements
%sContribute at:%s %sContribute at:%s
https://git.spatulaa.com/MayaTheShy/Opus]] https://github.com/kepler155c/opus]]
local page = UI.Page { local page = UI.Page {
wizard = UI.Wizard { wizard = UI.Wizard {
@@ -94,54 +93,17 @@ local page = UI.Page {
}, },
packages = UI.WizardPage { packages = UI.WizardPage {
index = 4, index = 4,
button = UI.Button {
x = 3, y = -3,
text = 'Open Package Manager',
event = 'packages',
},
intro = UI.TextArea { intro = UI.TextArea {
textColor = colors.yellow, textColor = colors.yellow,
inactive = true, inactive = true,
x = 3, ex = -3, y = 2, ey = 5, x = 3, ex = -3, y = 2, ey = -4,
value = string.format(packagesIntro, Ansi.white), value = string.format(packagesIntro, Ansi.white),
}, },
chkTurtle = UI.Checkbox {
x = 5, y = 7,
label = 'RemoteTurtle',
textColor = 'yellow',
backgroundColor = 'primary',
value = false,
},
lblTurtle = UI.Text {
x = 22, y = 7,
value = 'Turtle control + web dashboard',
textColor = colors.lightGray,
},
chkInventory = UI.Checkbox {
x = 5, y = 9,
label = 'Inventory Manager',
textColor = 'yellow',
backgroundColor = 'primary',
value = false,
},
lblInventory = UI.Text {
x = 28, y = 9,
value = 'Storage automation system',
textColor = colors.lightGray,
},
button = UI.Button {
x = 3, y = -3,
text = 'More Packages...',
event = 'packages',
},
validate = function(self)
local toInstall = { }
if self.chkTurtle.value then
table.insert(toInstall, 'remoteturtle')
end
if self.chkInventory.value then
table.insert(toInstall, 'inventory-manager')
end
for _, pkg in ipairs(toInstall) do
shell.run('package install ' .. pkg)
end
return true
end,
}, },
contributors = UI.WizardPage { contributors = UI.WizardPage {
index = 5, index = 5,

View File

@@ -1,22 +1,14 @@
local SHA = require("opus.crypto.sha2") local SHA = require("opus.crypto.sha2")
local acceptableCharacters = {} 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"}
for c = 0, 127 do
local char = string.char(c)
-- exclude potentially ambiguous characters
if char:match("[1-9a-zA-Z]") and char:match("[^OIl]") then
table.insert(acceptableCharacters, char)
end
end
local acceptableCharactersLen = #acceptableCharacters local acceptableCharactersLen = #acceptableCharacters
local password = "" local password = ""
for i = 1, 10 do for _i = 1, 8 do
password = password .. acceptableCharacters[math.random(acceptableCharactersLen)] password = password .. acceptableCharacters[math.random(acceptableCharactersLen)]
end end
os.queueEvent("set_otp", SHA.compute(password)) os.queueEvent("set_otp", SHA.compute(password))
print("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) print("Your one-time password is: " .. password)

View File

@@ -1,200 +0,0 @@
-- 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,10 +59,6 @@ local function sambaConnection(socket)
print('samba: Connection closed') print('samba: Connection closed')
end end
local function sanitizeLabel(computer)
return (computer.id.."_"..computer.label:gsub("[%c%.\"'/%*]", "")):sub(1, 40)
end
Event.addRoutine(function() Event.addRoutine(function()
print('samba: listening on port 139') print('samba: listening on port 139')
@@ -83,10 +79,10 @@ Event.addRoutine(function()
end) end)
Event.on('network_attach', function(_, computer) Event.on('network_attach', function(_, computer)
fs.mount(fs.combine('network', sanitizeLabel(computer)), 'netfs', computer.id) fs.mount(fs.combine('network', computer.label), 'netfs', computer.id)
end) end)
Event.on('network_detach', function(_, computer) Event.on('network_detach', function(_, computer)
print('samba: detaching ' .. sanitizeLabel(computer)) print('samba: detaching ' .. computer.label)
fs.unmount(fs.combine('network', sanitizeLabel(computer))) fs.unmount(fs.combine('network', computer.label))
end) end)

View File

@@ -152,7 +152,7 @@ local function getSlots()
end end
local function sendInfo() local function sendInfo()
if os.clock() - infoTimer >= 5 then -- don't flood if os.clock() - infoTimer >= 1 then -- don't flood
infoTimer = os.clock() infoTimer = os.clock()
info.label = os.getComputerLabel() info.label = os.getComputerLabel()
info.uptime = math.floor(os.clock()) info.uptime = math.floor(os.clock())
@@ -194,25 +194,16 @@ local function sendInfo()
end end
end end
local function cleanNetwork() -- every 10 seconds, send out this computer's info
Event.onInterval(10, function()
sendInfo()
for _,c in pairs(_G.network) do for _,c in pairs(_G.network) do
local elapsed = os.clock()-c.timestamp local elapsed = os.clock()-c.timestamp
if c.active and elapsed > 50 then if c.active and elapsed > 15 then
c.active = false c.active = false
os.queueEvent('network_detach', c) os.queueEvent('network_detach', c)
end end
end end
end
-- every 30 seconds, send out this computer's info
-- send with offset so that messages are evenly distributed and do not all come at once
Event.onTimeout(math.random() * 30, function()
sendInfo()
cleanNetwork()
Event.onInterval(30, function()
sendInfo()
cleanNetwork()
end)
end) end)
Event.on('turtle_response', function() Event.on('turtle_response', function()
@@ -222,5 +213,4 @@ Event.on('turtle_response', function()
end end
end) end)
-- send info early so that computers show soon after booting Event.onTimeout(1, sendInfo)
Event.onTimeout(math.random() * 2 + 1, sendInfo)

View File

@@ -7,7 +7,6 @@
local Crypto = require('opus.crypto.chacha20') local Crypto = require('opus.crypto.chacha20')
local Event = require('opus.event') local Event = require('opus.event')
local SHA = require('opus.crypto.sha2')
local network = _G.network local network = _G.network
local os = _G.os local os = _G.os
@@ -36,19 +35,7 @@ function transport.read(socket)
local data = table.remove(socket.messages, 1) local data = table.remove(socket.messages, 1)
if data then if data then
if socket.options.ENCRYPT then if socket.options.ENCRYPT then
local ciphertext = data[1] return table.unpack(Crypto.decrypt(data[1], socket.enckey)), data[2]
-- Verify HMAC if present (new protocol)
if socket.hmackey and type(ciphertext) == 'table' and ciphertext.hmac then
local expected = SHA.hmac(
ciphertext[1] .. ciphertext[2],
socket.hmackey
):toHex()
if expected ~= ciphertext.hmac then
_G._syslog('transport: HMAC verification failed on port ' .. socket.sport)
return nil
end
end
return table.unpack(Crypto.decrypt(ciphertext, socket.enckey)), data[2]
end end
return table.unpack(data) return table.unpack(data)
end end
@@ -91,15 +78,7 @@ Event.on('transport_encrypt', function()
if socket and socket.connected then if socket and socket.connected then
local msg = entry[2] local msg = entry[2]
local encrypted = Crypto.encrypt({ msg.data }, socket.enckey) msg.data = Crypto.encrypt({ msg.data }, socket.enckey)
-- Attach HMAC if key is available
if socket.hmackey then
encrypted.hmac = SHA.hmac(
encrypted[1] .. encrypted[2],
socket.hmackey
):toHex()
end
msg.data = encrypted
socket.transmit(socket.dport, socket.dhost, msg) socket.transmit(socket.dport, socket.dhost, msg)
end end
end end

View File

@@ -25,11 +25,11 @@ end
local function trustConnection(socket) local function trustConnection(socket)
local data = socket:read(2) local data = socket:read(2)
if data then if data then
local trustKey = Security.getTrustKey() local password = Security.getPassword()
if not trustKey then if not password then
socket:write({ msg = 'No password has been set' }) socket:write({ msg = 'No password has been set' })
else else
if validateData(data, trustKey, socket.dhost) then if validateData(data, password, socket.dhost) then
print("Accepted trust from " .. socket.dhost) print("Accepted trust from " .. socket.dhost)
socket:write({ success = true, msg = 'Trust accepted' }) socket:write({ success = true, msg = 'Trust accepted' })
return return

View File

@@ -78,19 +78,6 @@ local function install(name, isUpdate, ignoreDeps)
local packageDir = fs.combine('packages', name) local packageDir = fs.combine('packages', name)
local list = Git.list(manifest.repository) local list = Git.list(manifest.repository)
-- apply exclude filters from manifest
if manifest.exclude then
for path in pairs(list) do
for _, pattern in ipairs(manifest.exclude) do
if path:match(pattern) then
list[path] = nil
break
end
end
end
end
-- clear out contents before install/update -- clear out contents before install/update
-- TODO: figure out whether to run -- TODO: figure out whether to run
-- install/uninstall for the package -- install/uninstall for the package

View File

@@ -1,9 +1,10 @@
local Security = require('opus.security') local Security = require('opus.security')
local SHA = require('opus.crypto.sha2')
local Terminal = require('opus.terminal') local Terminal = require('opus.terminal')
local password = Terminal.readPassword('Enter new password: ') local password = Terminal.readPassword('Enter new password: ')
if password then if password then
Security.updatePassword(password) Security.updatePassword(SHA.compute(password))
print('Password updated') print('Password updated')
end end

View File

@@ -26,12 +26,12 @@ return UI.Tab {
x = 2, y = 5, ex = -2, ey = -2, x = 2, y = 5, ex = -2, ey = -2,
values = { values = {
{ name = '', value = '' }, { name = '', value = '' },
{ name = 'CC version', value = ("%d.%d"):format(Util.getVersion()) }, { name = 'CC version', value = Util.getVersion() },
{ name = 'Lua version', value = _VERSION }, { name = 'Lua version', value = _VERSION },
{ name = 'MC version', value = Util.getMinecraftVersion() }, { name = 'MC version', value = Util.getMinecraftVersion() },
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) }, { name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
{ name = 'Computer ID', value = tostring(os.getComputerID()) }, { name = 'Computer ID', value = tostring(os.getComputerID()) },
{ name = 'Day', value = tostring(os.day()) }, { name = 'Day', value = tostring(os.day()) },
}, },
disableHeader = true, disableHeader = true,
inactive = true, inactive = true,

View File

@@ -4,7 +4,7 @@ local Util = require('opus.util')
local fs = _G.fs local fs = _G.fs
local shell = _ENV.shell local shell = _ENV.shell
local URL = 'https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/%s/.opus_version' local URL = 'https://raw.githubusercontent.com/kepler155c/opus/%s/.opus_version'
if fs.exists('.opus_version') then if fs.exists('.opus_version') then
local f = fs.open('.opus_version', 'r') local f = fs.open('.opus_version', 'r')

View File

@@ -1,5 +1,5 @@
sys/apps/pain.lua urlfs https://github.com/LDDestroier/CC/raw/master/pain.lua sys/apps/pain.lua urlfs https://github.com/LDDestroier/CC/raw/master/pain.lua
sys/apps/update.lua urlfs https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/main/installer.lua sys/apps/update.lua urlfs http://pastebin.com/raw/UzGHLbNC
sys/apps/Enchat.lua urlfs https://raw.githubusercontent.com/LDDestroier/enchat/master/enchat3.lua sys/apps/Enchat.lua urlfs https://raw.githubusercontent.com/LDDestroier/enchat/master/enchat3.lua
sys/apps/cloud.lua urlfs https://cloud-catcher.squiddev.cc/cloud.lua sys/apps/cloud.lua urlfs https://cloud-catcher.squiddev.cc/cloud.lua
rom/modules/main/opus linkfs sys/modules/opus rom/modules/main/opus linkfs sys/modules/opus

View File

@@ -1,6 +0,0 @@
{
["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 w,h = parentTerm.getSize()
local overviewId local overviewId
local tabsDirty = false local tabsDirty = false
local closeInd = Util.supportsExtChars() and '\215' or '*' local closeInd = Util.getVersion() >= 1.76 and '\215' or '*'
local multishell = { } local multishell = { }
_ENV.multishell = multishell _ENV.multishell = multishell

View File

@@ -5,7 +5,7 @@ local cbor = require('opus.cbor')
local sha2 = require('opus.crypto.sha2') local sha2 = require('opus.crypto.sha2')
local Util = require('opus.util') local Util = require('opus.util')
local ROUNDS = 20 -- Standard ChaCha20 (was 8, upgraded for security) local ROUNDS = 8 -- Adjust this for speed tradeoff
local bxor = bit32.bxor local bxor = bit32.bxor
local band = bit32.band local band = bit32.band

View File

@@ -15,11 +15,7 @@ for _,m in pairs(methods) do
end end
function linkfs.resolve(node, dir) function linkfs.resolve(node, dir)
local mp = node.mountPoint return dir:gsub(node.mountPoint, node.source, 1)
if dir:sub(1, #mp) == mp then
return node.source .. dir:sub(#mp + 1)
end
return dir
end end
function linkfs.mount(path, source) function linkfs.mount(path, source)
@@ -45,8 +41,8 @@ function linkfs.mount(path, source)
end end
function linkfs.copy(node, s, t) function linkfs.copy(node, s, t)
s = linkfs.resolve(node, s) s = s:gsub(node.mountPoint, node.source, 1)
t = linkfs.resolve(node, t) t = t:gsub(node.mountPoint, node.source, 1)
return fs.copy(s, t) return fs.copy(s, t)
end end
@@ -54,29 +50,25 @@ function linkfs.delete(node, dir)
if dir == node.mountPoint then if dir == node.mountPoint then
fs.unmount(node.mountPoint) fs.unmount(node.mountPoint)
else else
dir = linkfs.resolve(node, dir) dir = dir:gsub(node.mountPoint, node.source, 1)
return fs.delete(dir) return fs.delete(dir)
end end
end end
function linkfs.find(node, spec) function linkfs.find(node, spec)
spec = linkfs.resolve(node, spec) spec = spec:gsub(node.mountPoint, node.source, 1)
local list = fs.find(spec) local list = fs.find(spec)
local src = node.source
local mp = node.mountPoint
for k,f in ipairs(list) do for k,f in ipairs(list) do
if f:sub(1, #src) == src then list[k] = f:gsub(node.source, node.mountPoint, 1)
list[k] = mp .. f:sub(#src + 1)
end
end end
return list return list
end end
function linkfs.move(node, s, t) function linkfs.move(node, s, t)
s = linkfs.resolve(node, s) s = s:gsub(node.mountPoint, node.source, 1)
t = linkfs.resolve(node, t) t = t:gsub(node.mountPoint, node.source, 1)
return fs.move(s, t) return fs.move(s, t)
end end

View File

@@ -35,10 +35,8 @@ end
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx', 'attributes' } local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx', 'attributes' }
local function resolve(node, dir) local function resolve(node, dir)
local mp = node.mountPoint -- TODO: Wrong ! (does not support names with dashes)
if dir:sub(1, #mp) == mp then dir = dir:gsub(node.mountPoint, '', 1)
dir = dir:sub(#mp + 1)
end
return fs.combine(node.source, dir) return fs.combine(node.source, dir)
end end
@@ -55,7 +53,7 @@ end
function netfs.mount(_, id, source) function netfs.mount(_, id, source)
if not id or not tonumber(id) then if not id or not tonumber(id) then
error('netfs syntax: computerId [directory]') error('ramfs syntax: computerId [directory]')
end end
return { return {
id = tonumber(id), id = tonumber(id),

View File

@@ -1,91 +1,67 @@
local json = require('opus.json') local json = require('opus.json')
local Util = require('opus.util') local Util = require('opus.util')
local GITHUB_TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1' local 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 FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s'
local GITEA_TREE_URL = 'https://%s/api/v1/repos/%s/%s/git/trees/%s?recursive=1'
local GITEA_FILE_URL = 'https://%s/%s/%s/raw/branch/%s/%s'
local TREE_HEADERS = {} local TREE_HEADERS = {}
local git = { } local git = { }
if _G._GIT_API_KEY then if _G._GIT_API_KEY then
TREE_HEADERS.Authorization = 'token ' .. _G._GIT_API_KEY TREE_HEADERS.Authorization = 'token ' .. _G._GIT_API_KEY
end end
local function parseTree(data, path, fileUrlFn) function git.list(repository)
if data.message then local t = Util.split(repository, '(.-)/')
if data.message:find("API rate limit exceeded") then
error("Out of API calls, try again later") local user = table.remove(t, 1)
end local repo = table.remove(t, 1)
if data.message == "Not found" or data.message == "Not Found" then local branch = table.remove(t, 1) or 'master'
error("Invalid repository") 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)
end end
end end
local data = getContents() or error('Invalid repository')
if data.message and data.message:find("API rate limit exceeded") then
error("Out of API calls, try again later")
end
if data.message and data.message == "Not found" then
error("Invalid repository")
end
local list = { } local list = { }
for _, v in pairs(data.tree) do for _,v in pairs(data.tree) do
if v.type == "blob" then if v.type == "blob" then
v.path = v.path:gsub("%s", "%%20") v.path = v.path:gsub("%s","%%20")
if not path then if not path then
list[v.path] = { list[v.path] = {
url = fileUrlFn(v.path), url = string.format(FILE_URL, user, repo, branch, v.path),
size = v.size, size = v.size,
} }
elseif Util.startsWith(v.path, path) then elseif Util.startsWith(v.path, path) then
local p = string.sub(v.path, #path) local p = string.sub(v.path, #path)
list[p] = { list[p] = {
url = fileUrlFn(path .. p), url = string.format(FILE_URL, user, repo, branch, path .. p),
size = v.size, size = v.size,
} }
end end
end end
end end
return list return list
end end
local function fetchTree(url)
local contents, msg = Util.httpGet(url, TREE_HEADERS)
if not contents then
error(string.format('Failed to download %s\n%s', url, msg), 2)
end
return json.decode(contents) or error('Invalid repository')
end
-- GitHub: user/repo/branch/subdir/
local function listGithub(repository)
local t = Util.split(repository, '(.-)/')
local user = table.remove(t, 1)
local repo = table.remove(t, 1)
local branch = table.remove(t, 1) or 'main'
local path = not Util.empty(t) and (table.concat(t, '/') .. '/') or nil
local data = fetchTree(string.format(GITHUB_TREE_URL, user, repo, branch))
return parseTree(data, path, function(p)
return string.format(GITHUB_FILE_URL, user, repo, branch, p)
end)
end
-- Gitea: gitea://host/user/repo/branch/subdir/
local function listGitea(host, remainder)
remainder = remainder:gsub('/$', '') -- strip trailing slash
local t = Util.split(remainder, '(.-)/')
local user = table.remove(t, 1)
local repo = table.remove(t, 1)
local branch = table.remove(t, 1) or 'main'
local path = not Util.empty(t) and (table.concat(t, '/') .. '/') or nil
local data = fetchTree(string.format(GITEA_TREE_URL, host, user, repo, branch))
return parseTree(data, path, function(p)
return string.format(GITEA_FILE_URL, host, user, repo, branch, p)
end)
end
function git.list(repository)
local host_type, host, rest = repository:match('^(%w+)://(.-)/(.*)')
if host_type == 'gitea' then
return listGitea(host, rest)
end
return listGithub(repository)
end
return git return git

View File

@@ -39,7 +39,6 @@ if register_global_module_table then
_G[global_module_name] = json _G[global_module_name] = json
end end
local fs = fs
local _ENV = nil -- blocking globals in Lua 5.2 local _ENV = nil -- blocking globals in Lua 5.2
pcall (function() pcall (function()

View File

@@ -1,5 +1,4 @@
local Config = require('opus.config') local Util = require('opus.util')
local Util = require('opus.util')
local fs = _G.fs local fs = _G.fs
local textutils = _G.textutils local textutils = _G.textutils
@@ -8,21 +7,6 @@ local PACKAGE_DIR = 'packages'
local Packages = { } local Packages = { }
-- Default package sources (upstream GitHub + self-hosted Gitea)
local DEFAULT_SOURCES = {
{
name = 'opus-apps',
branches = {
[ 'develop-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/packages.list',
[ 'master-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/master-1.8/packages.list',
},
},
{
name = 'spatulaa',
url = 'https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/main/sys/etc/packages.list',
},
}
function Packages:installed() function Packages:installed()
local list = { } local list = { }
@@ -71,30 +55,13 @@ function Packages:isInstalled(package)
end end
function Packages:downloadList() function Packages:downloadList()
local sources = Config.load('package').sources or DEFAULT_SOURCES local packages = {
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',
}
for _, source in ipairs(sources) do if packages[_G.OPUS_BRANCH] then
local url = source.url Util.download(packages[_G.OPUS_BRANCH], 'usr/config/packages')
if source.branches then
url = source.branches[_G.OPUS_BRANCH]
end
if url then
local content = Util.httpGet(url)
if content then
local list = textutils.unserialize(content)
if list then
for k, v in pairs(list) do
packages[k] = v
end
end
end
end
end
if next(packages) then
Util.writeTable('usr/config/packages', packages)
end end
end end

View File

@@ -1,38 +1,10 @@
local Config = require('opus.config') local Config = require('opus.config')
local SHA = require('opus.crypto.sha2')
local Util = require('opus.util')
local PBKDF2_ITERATIONS = 100
local Security = { } local Security = { }
local function generateSalt()
local salt = { }
for _ = 1, 16 do
salt[#salt + 1] = math.random(0, 0xFF)
end
return setmetatable(salt, Util.byteArrayMT):toHex()
end
function Security.verifyPassword(password) function Security.verifyPassword(password)
local stored = Security.getPassword() local current = Security.getPassword()
if not stored then return current and password == current
return false
end
-- New format: { hash = hex, salt = hex, iter = N }
if type(stored) == 'table' and stored.hash and stored.salt then
local iter = stored.iter or PBKDF2_ITERATIONS
local derived = SHA.pbkdf2(password, Util.hexToByteArray(stored.salt), iter)
return derived:toHex() == stored.hash
end
-- Legacy format: plain SHA-256 hex string
if type(stored) == 'string' then
return SHA.compute(password) == stored
end
return false
end end
function Security.hasPassword() function Security.hasPassword()
@@ -56,16 +28,8 @@ function Security.getIdentifier()
end end
function Security.updatePassword(password) function Security.updatePassword(password)
local salt = generateSalt()
local derived = SHA.pbkdf2(password, Util.hexToByteArray(salt), PBKDF2_ITERATIONS)
local config = Config.load('os') local config = Config.load('os')
config.password = { config.password = password
hash = derived:toHex(),
salt = salt,
iter = PBKDF2_ITERATIONS,
trustKey = SHA.compute(password),
}
Config.update('os', config) Config.update('os', config)
end end
@@ -73,15 +37,4 @@ function Security.getPassword()
return Config.load('os').password return Config.load('os').password
end end
-- Returns the trust key for ChaCha20-based trust protocol.
-- Compatible with both new (PBKDF2 table) and legacy (SHA-256 string) formats.
function Security.getTrustKey()
local stored = Security.getPassword()
if type(stored) == 'table' then
return stored.trustKey
end
-- Legacy: the stored string IS the SHA-256 hex
return stored
end
return Security return Security

View File

@@ -107,7 +107,7 @@ end
local function setupCrypto(socket, isClient) local function setupCrypto(socket, isClient)
socket.sharedKey = ECC.exchange(socket.privKey, socket.remotePubKey) socket.sharedKey = ECC.exchange(socket.privKey, socket.remotePubKey)
socket.enckey = SHA.pbkdf2(socket.sharedKey, "1enc", 1) socket.enckey = SHA.pbkdf2(socket.sharedKey, "1enc", 1)
socket.hmackey = SHA.pbkdf2(socket.sharedKey, "2hmac", 1) --self.hmackey = SHA.pbkdf2(self.sharedKey, "2hmac", 1)
socket.rrng = Crypto.newRNG( socket.rrng = Crypto.newRNG(
SHA.pbkdf2(socket.sharedKey, isClient and "3rseed" or "4sseed", 1)) SHA.pbkdf2(socket.sharedKey, isClient and "3rseed" or "4sseed", 1))

View File

@@ -44,7 +44,7 @@ function UI:init()
tertiary = colors.gray, tertiary = colors.gray,
} }
} }
self.extChars = Util.supportsExtChars() self.extChars = Util.getVersion() >= 1.76
local function keyFunction(event, code, held) local function keyFunction(event, code, held)
local ie = Input:translate(event, code, held) local ie = Input:translate(event, code, held)
@@ -115,16 +115,12 @@ function UI:init()
local ie = Input:translate('mouse_up', button, x, y) local ie = Input:translate('mouse_up', button, x, y)
local currentPage = self:getActivePage() local currentPage = self:getActivePage()
if ie.code == 'control-shift-mouse_click' then -- debug inspector if ie.code == 'control-shift-mouse_click' then -- hack
local Config = require('opus.config') local event = currentPage:pointToChild(x, y)
local debugCfg = Config.load('os', { debug_inspector = false }) _ENV.multishell.openTab(_ENV, {
if debugCfg.debug_inspector then path = 'sys/apps/Lua.lua',
local event = currentPage:pointToChild(x, y) args = { event.element, self, _ENV },
_ENV.multishell.openTab(_ENV, { focused = true })
path = 'sys/apps/Lua.lua',
args = { event.element, self, _ENV },
focused = true })
end
elseif ie and currentPage and currentPage.parent.device.side == side then elseif ie and currentPage and currentPage.parent.device.side == side then
self:click(currentPage, ie) self:click(currentPage, ie)

View File

@@ -26,28 +26,6 @@ function Writer:write(s, width, align, bg, fg)
self.x = self.x + width self.x = self.x + width
end end
function Writer:writeBar(width, ratio, barColor, emptyColor, text, textColor)
local filled = math.floor(ratio * width)
local empty = width - filled
if filled > 0 then
self.element:write(self.x, self.y, _rep(' ', filled), barColor)
end
if empty > 0 then
self.element:write(self.x + filled, self.y, _rep(' ', empty), emptyColor)
end
if text and #text > 0 and textColor then
local tx = self.x + math.floor((width - #text) / 2)
for i = 1, #text do
local cx = tx + i - 1
if cx >= self.x and cx < self.x + width then
local bg = (cx - self.x) < filled and barColor or emptyColor
self.element:write(cx, self.y, text:sub(i, i), bg, textColor)
end
end
end
self.x = self.x + width
end
function Writer:finish(bg) function Writer:finish(bg)
if self.x <= self.element.width then if self.x <= self.element.width then
self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg) self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg)
@@ -69,7 +47,6 @@ UI.Grid.defaults = {
textSelectedColor = 'white', textSelectedColor = 'white',
backgroundColor = 'black', backgroundColor = 'black',
backgroundSelectedColor = 'gray', backgroundSelectedColor = 'gray',
alternateRowColor = nil,
headerBackgroundColor = 'primary', headerBackgroundColor = 'primary',
headerTextColor = 'white', headerTextColor = 'white',
headerSortColor = 'yellow', headerSortColor = 'yellow',
@@ -340,7 +317,7 @@ function UI.Grid:drawRows()
local row = self:getDisplayValues(rawRow, key) local row = self:getDisplayValues(rawRow, key)
local selected = index == self.index and not self.inactive local selected = index == self.index and not self.inactive
local bg = self:getRowBackgroundColor(rawRow, selected, index) local bg = self:getRowBackgroundColor(rawRow, selected)
local fg = self:getRowTextColor(rawRow, selected) local fg = self:getRowTextColor(rawRow, selected)
local focused = self.focused and selected local focused = self.focused and selected
@@ -358,25 +335,11 @@ function UI.Grid:drawRow(sb, row, focused, bg, fg)
local ind = focused and self.focusIndicator or ' ' local ind = focused and self.focusIndicator or ' '
for _,col in pairs(self.columns) do for _,col in pairs(self.columns) do
if col.barColumn then sb:write(ind .. safeValue(row[col.key] or ''),
-- Bar column: render a colored fill bar col.cw + 1,
local ratio = tonumber(row[col.key]) or 0 col.align,
ratio = math.max(0, math.min(1, ratio)) col.backgroundColor or bg,
local barColor = col.barColor or colors.lime col.textColor or fg)
local emptyColor = col.barEmptyColor or bg
if type(col.barColor) == 'function' then
barColor = col.barColor(row, ratio)
end
local barText = col.barText and (type(col.barText) == 'function' and col.barText(row, ratio) or col.barText) or nil
sb:write(ind, 1, nil, bg, fg)
sb:writeBar(col.cw, ratio, barColor, emptyColor, barText, col.barTextColor or colors.white)
else
sb:write(ind .. safeValue(row[col.key] or ''),
col.cw + 1,
col.align,
col.backgroundColor or bg,
col.textColor or fg)
end
ind = ' ' ind = ' '
end end
end end
@@ -391,16 +354,13 @@ function UI.Grid:getRowTextColor(row, selected)
return self.textColor return self.textColor
end end
function UI.Grid:getRowBackgroundColor(row, selected, index) function UI.Grid:getRowBackgroundColor(row, selected)
if selected then if selected then
if self.focused then if self.focused then
return self.backgroundSelectedColor return self.backgroundSelectedColor
end end
return self.unfocusedBackgroundSelectedColor return self.unfocusedBackgroundSelectedColor
end end
if self.alternateRowColor and index then
return index % 2 == 0 and self.alternateRowColor or self.backgroundColor
end
return self.backgroundColor return self.backgroundColor
end end

View File

@@ -12,35 +12,12 @@ UI.ProgressBar.defaults = {
fillColor = 'gray', fillColor = 'gray',
textColor = 'green', textColor = 'green',
value = 0, value = 0,
showText = false, -- display value text overlay centered on bar
textOverlay = nil, -- custom overlay text (string or function(value) -> string)
textOverlayColor = 'white', -- text overlay foreground color
} }
function UI.ProgressBar:draw() function UI.ProgressBar:draw()
local width = math.ceil(self.value / 100 * self.width) local width = math.ceil(self.value / 100 * self.width)
self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor) self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor)
self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor) self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor)
if self.showText then
local text
if self.textOverlay then
text = type(self.textOverlay) == 'function'
and self.textOverlay(self.value)
or tostring(self.textOverlay)
else
text = math.floor(self.value) .. '%'
end
local midY = math.ceil(self.height / 2)
local x = math.floor((self.width - #text) / 2) + 1
for i = 1, #text do
local cx = x + i - 1
if cx >= 1 and cx <= self.width then
local bg = cx <= width and self.progressColor or self.fillColor
self:write(cx, midY, text:sub(i, i), bg, self.textOverlayColor)
end
end
end
end end
function UI.ProgressBar.example() function UI.ProgressBar.example()

View File

@@ -170,19 +170,22 @@ function Util.print(pattern, ...)
end end
function Util.getVersion() function Util.getVersion()
local versionString = _G._HOST or _G._CC_VERSION local version
local versionMajor, versionMinor = versionString:match("(%d+)%.(%d+)")
-- ex.: 1.89 would return 1, 89
return tonumber(versionMajor), tonumber(versionMinor)
end
function Util.compareVersion(major, minor) if _G._CC_VERSION then
local currentMajor, currentMinor = Util.getVersion() version = tonumber(_G._CC_VERSION:match('[%d]+%.?[%d][%d]'))
return currentMajor > major or currentMajor == major and currentMinor >= minor end
end if not version and _G._HOST then
version = tonumber(_G._HOST:match('[%d]+%.?[%d][%d]'))
function Util.supportsExtChars() -- stopgap fix for icons breaking on 1.100.x CC versions
return Util.compareVersion(1, 76) -- TODO: Make this cleaner and more resiliant
if version == 1.1 then
version = 1.999
end
end
return version or 1.7
end end
function Util.getMinecraftVersion() function Util.getMinecraftVersion()