15 Commits

Author SHA1 Message Date
kepler155c@gmail.com
5933f8c40f can now use named colors 2020-04-21 22:32:12 -06:00
kepler155c@gmail.com
e703c7f7b6 changes for deprecated ui methods 2020-04-17 20:40:34 -06:00
kepler155c@gmail.com
9d2a76f4ea color rework + cleanup 2020-04-16 23:13:19 -06:00
kepler155c@gmail.com
3e41996b9b cleanup + theme editor 2020-04-14 14:12:04 -06:00
kepler155c@gmail.com
9eeec8719c cleanup 2020-04-12 18:46:26 -06:00
kepler155c@gmail.com
775871c548 mouse triple click + textEntry scroll ind 2020-04-10 22:51:18 -06:00
kepler155c@gmail.com
cd6ef0da50 use layout() where appropriate and cleanup 2020-04-09 16:08:54 -06:00
kepler155c@gmail.com
8fe6e0806c refactor + new transitions 2020-04-06 00:12:46 -06:00
kepler155c@gmail.com
7659b81d49 more editor work 2020-04-04 20:56:53 -06:00
kepler155c@gmail.com
cd58ecd861 minor tweaks 2020-04-03 18:57:57 -06:00
kepler155c@gmail.com
d88ef00652 bugfixes + tweaks for editor 2.0 2020-04-02 21:28:42 -06:00
kepler155c@gmail.com
fc1a308193 list mode for overview 2020-03-31 23:43:45 -06:00
kepler155c@gmail.com
3313fb986c minor tweaks 2020-03-31 17:23:12 -06:00
kepler155c@gmail.com
613212e751 Merge remote-tracking branch 'origin/develop-1.8' into ui-enhancements-2.0 2020-03-31 09:59:15 -06:00
kepler155c@gmail.com
5a874c1944 canvas overhaul 2020-03-31 09:57:23 -06:00
123 changed files with 1608 additions and 4058 deletions

View File

@@ -1,30 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Versions**
What version of Minecraft, CC:Tweaked, Plethora (if applicable), Opus branch are you using
- MC : [e.g. 1.12.2]
- CC:T : [e.g. 1.88]
- Opus : [e.g. develop-1.8]

View File

@@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,17 +0,0 @@
---
name: Bug Report
about: Did something go wrong? File an issue!
title: Good titles include first line of stack trace or short summary of problem
labels: bug
---
<!--- THIS IS A COMMENT. IT WILL NOT APPEAR IN THE FINAL ISSUE, DO NOT DELETE THESE. -->
# Details
<!--- Put a description of the bug here. (Ex. I crashed when running Opus.) -->
## Further context
<!--- Stack trace (surrounded in three backticks, ), supplementary media such as screenshots and video, etc -->
## Versions
Branch:
Opus Version: <!--- (Do NOT put Latest unless you are unsure) -->
CraftOS Version:

View File

@@ -1,13 +0,0 @@
---
name: Enhancement
about: Suggest a new feature or change to Opus.
labels: enhancement
---
<!--- THIS IS A COMMENT. IT WILL NOT APPEAR IN THE FINAL ISSUE. DO NOT REMOVE THEM. -->
# Summary
<!--- Summarize what you want. -->
## Additional Context
<!--- Comcept art, screenshots, and other relevant media. -->
## Related
<!--- Delete this category if not used. -->
<!--- Use this category for relevant links, if present. -->

View File

@@ -1,36 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the master branch
on:
push:
branches: [ develop-1.8 ]
pull_request:
branches: [ develop-1.8 ]
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Create version file
run: |
echo `date` > .opus_version
git log >> .opus_version
- name: Commit version file
uses: alexesprit/action-update-file@main
with:
branch: 'develop-1.8'
file-path: .opus_version
commit-msg: Update version date
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,6 +0,0 @@
Mon Jul 4 04:09:12 UTC 2022
commit 3150525ee2024fc605669093b89f75f0c741a81f
Author: Kan18 <24967425+Kan18@users.noreply.github.com>
Date: Mon Jul 4 00:08:59 2022 -0400
Fix #48 (shell resolving issue) (#58)

View File

@@ -1,7 +1,5 @@
# Opus OS for computercraft
<img src="https://github.com/kepler155c/opus-wiki/blob/master/assets/images/opus.gif?raw=true" width="540" height="360">
## Features
* Multitasking OS - run programs in separate tabs
* Telnet (wireless remote shell)
@@ -16,5 +14,5 @@
## 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

@@ -29,10 +29,9 @@ local function loadBootOptions()
preload = { },
menu = {
{ prompt = os.version() },
{ prompt = 'Opus' , args = { '/sys/boot/opus.lua' } },
{ prompt = 'Opus Shell' , args = { '/sys/boot/opus.lua', '/sys/apps/shell.lua' } },
{ prompt = 'Opus Kiosk' , args = { '/sys/boot/kiosk.lua' } },
{ prompt = 'Opus TLCO' , args = { '/sys/boot/tlco.lua' } },
{ prompt = 'Opus' , args = { '/sys/boot/opus.boot' } },
{ prompt = 'Opus Shell' , args = { '/sys/boot/opus.boot', 'sys/apps/shell.lua' } },
{ prompt = 'Opus Kiosk' , args = { '/sys/boot/kiosk.boot' } },
},
}))
f.close()
@@ -42,20 +41,6 @@ local function loadBootOptions()
local options = textutils.unserialize(f.readAll())
f.close()
-- Backwards compatibility for .startup.boot files created before sys/boot files' extensions were changed
local changed = false
for _, item in pairs(options.menu) do
if item.args and item.args[1]:match("/?sys/boot/%l+%.boot") then
item.args[1] = item.args[1]:gsub("%.boot", "%.lua")
changed = true
end
end
if changed then
local f = fs.open(".startup.boot", "w")
f.write(textutils.serialize(options))
f.close()
end
return options
end
@@ -142,7 +127,7 @@ local function splash()
local opus = {
'fffff00',
'ffff07000',
'ff00770b00f4444',
'ff00770b00 4444',
'ff077777444444444',
'f07777744444444444',
'f0000777444444444',

View File

@@ -1,3 +1,4 @@
local Alt = require('opus.alternate')
local Config = require('opus.config')
local Event = require('opus.event')
local pastebin = require('opus.http.pastebin')
@@ -76,8 +77,8 @@ local Browser = UI.Page {
grid = UI.ScrollingGrid {
columns = {
{ heading = 'Name', key = 'name' },
{ key = 'flags', width = 3, textColor = 'lightGray' },
{ heading = 'Size', key = 'fsize', width = 5, textColor = 'yellow' },
{ key = 'flags', width = 2 },
{ heading = 'Size', key = 'fsize', width = 5 },
},
sortColumn = 'name',
y = 2, ey = -2,
@@ -176,6 +177,7 @@ local Browser = UI.Page {
formLabel = 'Program', formKey = 'value',
shadowText = 'program',
required = true,
limit = 128,
},
add = UI.Button {
x = -11, y = 1,
@@ -210,7 +212,7 @@ function Browser:enable()
self:setFocus(self.grid)
end
function Browser.menuBar.getActive(_, menuItem)
function Browser.menuBar:getActive(menuItem)
local file = Browser.grid:getSelected()
if menuItem.flags == FILE then
return file and not file.isDir
@@ -222,7 +224,7 @@ function Browser:setStatus(status, ...)
self.notification:info(string.format(status, ...))
end
function Browser.unmarkAll()
function Browser:unmarkAll()
for _,m in pairs(marked) do
m.marked = false
end
@@ -262,11 +264,10 @@ function Browser:updateDirectory(dir)
dir.size = #files
for _, file in pairs(files) do
file.fullName = fs.combine(dir.name, file.name)
file.flags = file.fstype or ' '
file.flags = ''
if not file.isDir then
dir.totalSize = dir.totalSize + file.size
file.fsize = formatSize(file.size)
file.flags = file.flags .. ' '
else
if config.showDirSizes then
file.size = fs.getSize(file.fullName, true)
@@ -274,9 +275,11 @@ function Browser:updateDirectory(dir)
dir.totalSize = dir.totalSize + file.size
file.fsize = formatSize(file.size)
end
file.flags = file.flags .. 'D'
file.flags = 'D'
end
if file.isReadOnly then
file.flags = file.flags .. 'R'
end
file.flags = file.flags .. (file.isReadOnly and 'R' or ' ')
if config.showHidden or file.name:sub(1, 1) ~= '.' then
dir.files[file.fullName] = file
end
@@ -350,7 +353,7 @@ function Browser:eventHandler(event)
self:setStatus('Started cloud edit')
elseif event.type == 'shell' then
self:run('shell')
self:run(Alt.get('shell'))
elseif event.type == 'refresh' then
self:updateDirectory(self.dir)
@@ -465,7 +468,7 @@ function Browser:eventHandler(event)
elseif event.type == 'paste' then
for _,m in pairs(copied) do
pcall(function()
local s, m = pcall(function()
if cutMode then
fs.move(m.fullName, fs.combine(self.dir.name, m.name))
else

View File

@@ -1,4 +1,3 @@
local fuzzy = require('opus.fuzzy')
local UI = require('opus.ui')
local Util = require('opus.util')
@@ -43,13 +42,13 @@ UI:addPage('main', UI.Page {
elseif event.type == 'text_change' then
if not event.text then
self.grid.sortColumn = 'lname'
self.grid.values = topics
else
self.grid.sortColumn = 'score'
self.grid.inverseSort = false
local pattern = event.text:lower()
for _,v in pairs(self.grid.values) do
v.score = -fuzzy(v.lname, pattern)
self.grid.values = { }
for _,f in pairs(topics) do
if string.find(f.lname, event.text:lower()) then
table.insert(self.grid.values, f)
end
end
end
self.grid:update()

View File

@@ -1,3 +1,6 @@
-- Lua may be called from outside of shell - inject a require
_G.requireInjector(_ENV)
local History = require('opus.history')
local UI = require('opus.ui')
local Util = require('opus.util')
@@ -7,8 +10,10 @@ local os = _G.os
local textutils = _G.textutils
local term = _G.term
local _exit
local sandboxEnv = setmetatable(Util.shallowCopy(_ENV), { __index = _G })
sandboxEnv.exit = function() UI:quit() end
sandboxEnv.exit = function() _exit = true end
sandboxEnv._echo = function( ... ) return { ... } end
_G.requireInjector(sandboxEnv)
@@ -29,6 +34,7 @@ local page = UI.Page {
prompt = UI.TextEntry {
y = 2,
shadowText = 'enter command',
limit = 1024,
accelerators = {
enter = 'command_enter',
up = 'history_back',
@@ -39,9 +45,8 @@ local page = UI.Page {
},
tabs = UI.Tabs {
y = 3,
formatted = UI.Tab {
title = 'Formatted',
index = 1,
[1] = UI.Tab {
tabTitle = 'Formatted',
grid = UI.ScrollingGrid {
columns = {
{ heading = 'Key', key = 'name' },
@@ -51,9 +56,8 @@ local page = UI.Page {
autospace = true,
},
},
output = UI.Tab {
title = 'Output',
index = 2,
[2] = UI.Tab {
tabTitle = 'Output',
backgroundColor = 'black',
output = UI.Embedded {
y = 2,
@@ -68,8 +72,8 @@ local page = UI.Page {
},
}
page.grid = page.tabs.formatted.grid
page.output = page.tabs.output.output
page.grid = page.tabs[1].grid
page.output = page.tabs[2].output
function page:setPrompt(value, focus)
self.prompt:setValue(value)
@@ -134,11 +138,24 @@ function page:eventHandler(event)
self:executeStatement('_ENV')
command = nil
elseif event.type == 'hide_output' then
self.output:disable()
self.titleBar.oy = -1
self.titleBar.event = 'show_output'
self.titleBar.closeInd = '^'
self.titleBar:resize()
self.grid.ey = -2
self.grid:resize()
self:draw()
elseif event.type == 'tab_select' then
self:setFocus(self.prompt)
elseif event.type == 'show_output' then
self.tabs:selectTab(self.tabs.output)
self.tabs:selectTab(self.tabs[2])
elseif event.type == 'autocomplete' then
local value = self.prompt.value or ''
@@ -184,7 +201,7 @@ function page:eventHandler(event)
command = nil
self.grid:setValues(t)
self.grid:setIndex(1)
self.grid:draw()
self:draw()
end
return true
@@ -230,7 +247,7 @@ function page:setResult(result)
end
self.grid:setValues(t)
self.grid:setIndex(1)
self.grid:draw()
self:draw()
end
function page.grid:eventHandler(event)
@@ -348,7 +365,7 @@ function page:executeStatement(statement)
term.redirect(oterm)
counter = counter + 1
if s and type(m) ~= "nil" then
if s and m then
self:setResult(m)
else
self.grid:setValues({ })
@@ -357,6 +374,10 @@ function page:executeStatement(statement)
self:emit({ type = 'show_output' })
end
end
if _exit then
UI:quit()
end
end
local args = Util.parse(...)

View File

@@ -6,6 +6,7 @@ local Util = require('opus.util')
local device = _G.device
local network = _G.network
local os = _G.os
local shell = _ENV.shell
UI:configure('Network', ...)
@@ -85,14 +86,8 @@ local page = UI.Page {
title = 'Ports',
event = 'ports_hide',
},
menuBar = UI.MenuBar {
y = 2,
buttons = {
{ text = 'Refresh', event = 'ports_update' },
}
},
grid = UI.ScrollingGrid {
y = 3,
y = 2,
columns = {
{ heading = 'Port', key = 'port' },
{ heading = 'State', key = 'state' },
@@ -108,6 +103,30 @@ local page = UI.Page {
return UI.SlideOut.eventHandler(self, event)
end,
},
help = UI.SlideOut {
x = 5, ex = -5, height = 8, y = -8,
titleBar = UI.TitleBar {
title = 'Network Help',
event = 'slide_hide',
},
text = UI.TextArea {
x = 1, y = 2,
marginLeft = 1,
value = [[
In order to connect to another computer:
1. The target computer must have a password set (run 'password' from the shell prompt).
2. From this computer, click trust and enter the password for that computer.
This only needs to be done once.
]],
},
accelerators = {
q = 'slide_hide',
}
},
notification = UI.Notification { },
accelerators = {
t = 'telnet',
@@ -188,14 +207,12 @@ function page:eventHandler(event)
elseif event.type == 'vnc' then
shell.openForegroundTab('vnc.lua ' .. t.id)
--[[
os.queueEvent('overview_shortcut', {
title = t.label,
category = "VNC",
icon = "\010\030 \009\009\031e\\\031 \031e/\031dn\010\030 \009\009 \031e\\/\031 \031bc",
run = "vnc.lua " .. t.id,
})
--]]
elseif event.type == 'clear' then
Util.clear(network)
@@ -214,22 +231,17 @@ function page:eventHandler(event)
end
if event.type == 'help' then
shell.switchTab(shell.openTab('Help Networking'))
self.help:show()
elseif event.type == 'ports' then
self.ports.grid:update()
self.ports:show()
-- self.portsHandler = Event.onInterval(3, function()
-- self.ports.grid:update()
-- self.ports.grid:draw()
-- self:sync()
-- end)
elseif event.type == 'ports_update' then
self.portsHandler = Event.onInterval(3, function()
self.ports.grid:update()
self.ports.grid:draw()
self:sync()
end)
elseif event.type == 'ports_hide' then
Event.off(self.portsHandler)

View File

@@ -1,3 +1,4 @@
local Alt = require('opus.alternate')
local Array = require('opus.array')
local class = require('opus.class')
local Config = require('opus.config')
@@ -28,14 +29,12 @@ if not _ENV.multishell then
end
local REGISTRY_DIR = 'usr/.registry'
-- iconExt:gsub('.', function(b) return '\\' .. b:byte() end)
local DEFAULT_ICON = NFT.parse('\30\55\31\48\136\140\140\140\132\
\30\48\31\55\149\31\48\128\128\128\30\55\149\
\30\55\31\48\138\143\143\143\133')
local TRANS_ICON = NFT.parse('\0302\0312\32\32\32\32\32\
local DEFAULT_ICON = NFT.parse("\0308\0317\153\153\153\153\153\
\0307\0318\153\153\153\153\153\
\0308\0317\153\153\153\153\153")
local TRANS_ICON = NFT.parse("\0302\0312\32\32\32\32\32\
\0302\0312\32\32\32\32\32\
\0302\0312\32\32\32\32\32')
\0302\0312\32\32\32\32\32")
-- overview
local uid = _ENV.multishell.getCurrent()
@@ -51,7 +50,7 @@ local config = {
}
Config.load('Overview', config)
local extSupport = Util.supportsExtChars()
local extSupport = Util.getVersion() >= 1.76
local applications = { }
local buttons = { }
@@ -96,7 +95,6 @@ local page = UI.Page {
width = 8,
selectedBackgroundColor = 'primary',
backgroundColor = 'tertiary',
unselectedTextColor = 'lightGray',
layout = function(self)
self.height = nil
UI.TabBar.layout(self)
@@ -468,16 +466,16 @@ function page:eventHandler(event)
shell.switchTab(shell.openTab(event.button.app.run))
elseif event.type == 'shell' then
shell.switchTab(shell.openTab('shell'))
shell.switchTab(shell.openTab(Alt.get('shell')))
elseif event.type == 'lua' then
shell.switchTab(shell.openTab('Lua'))
shell.switchTab(shell.openTab(Alt.get('lua')))
elseif event.type == 'files' then
shell.switchTab(shell.openTab('Files'))
shell.switchTab(shell.openTab(Alt.get('files')))
elseif event.type == 'network' then
shell.switchTab(shell.openTab('Network'))
shell.switchTab(shell.openTab('network'))
elseif event.type == 'help' then
shell.switchTab(shell.openTab('Help Overview'))

View File

@@ -1,5 +1,4 @@
local Ansi = require('opus.ansi')
local Config = require('opus.config')
local Packages = require('opus.packages')
local UI = require('opus.ui')
local Util = require('opus.util')
@@ -9,34 +8,30 @@ local term = _G.term
UI:configure('PackageManager', ...)
local config = Config.load('package')
local page = UI.Page {
grid = UI.ScrollingGrid {
x = 2, ex = 14, y = 2, ey = -6,
x = 2, ex = 14, y = 2, ey = -5,
values = { },
columns = {
{ heading = ' Package', key = 'displayName' },
{ heading = 'Package', key = 'name' },
},
sortColumn = 'name',
autospace = true,
help = 'Space to select, Enter to toggle',
help = 'Select a package',
},
installSelected = UI.Button {
add = UI.Button {
x = 2, y = -3,
text = ' + ',
event = 'batch_action',
operation = 'install',
operationText = 'Install',
help = 'Install or update selected',
text = 'Install',
event = 'action',
help = 'Install or update',
},
removeSelected = UI.Button {
x = 8, y = -3,
text = ' - ',
event = 'batch_action',
remove = UI.Button {
x = 12, y = -3,
text = 'Remove ',
event = 'action',
operation = 'uninstall',
operationText = 'Remove',
help = 'Remove selected',
help = 'Remove',
},
updateall = UI.Button {
ex = -2, y = -3, width = 12,
@@ -46,15 +41,7 @@ local page = UI.Page {
},
description = UI.TextArea {
x = 16, y = 3, ey = -5,
marginRight = 2, marginLeft = 0,
},
UI.Checkbox {
x = 3, y = -5,
label = 'Compress',
textColor = 'yellow',
backgroundColor = 'primary',
value = config.compression,
help = 'Compress packages (experimental)',
marginRight = 0, marginLeft = 0,
},
action = UI.SlideOut {
titleBar = UI.TitleBar {
@@ -91,9 +78,7 @@ function page:loadPackages()
end
table.insert(self.grid.values, {
installed = not not Packages:isInstalled(k),
selected = false,
name = k,
displayName = k,
manifest = manifest,
})
end
@@ -108,65 +93,17 @@ 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)
--self.output:draw()
--self.output.win.redraw()
end
function page:run(operation, name)
@@ -188,7 +125,12 @@ function page:run(operation, name)
end
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.add.text = selected.installed and 'Update' or 'Install'
self.remove.inactive = not selected.installed
self.add:draw()
self.remove:draw()
end
function page:eventHandler(event)
@@ -198,88 +140,42 @@ function page:eventHandler(event)
elseif event.type == 'grid_focus_row' then
local manifest = event.selected.manifest
self.description:setValue(string.format('%s%s\n\n%s%s',
self.description.value = string.format('%s%s\n\n%s%s',
Ansi.yellow, manifest.title,
Ansi.white, manifest.description))
Ansi.white, manifest.description)
self.description:draw()
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
Config.update('package', config)
self:updateSelection(event.selected)
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 == '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
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()
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('updateall', '')
self:run(self.operation, '')
else
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()
local selected = self.grid:getSelected()
self:run(self.operation, selected.name)
selected.installed = Packages:isInstalled(selected.name)
self:updateSelection(selected)
end
self.action.button.text = ' Done '

View File

@@ -1,238 +0,0 @@
local Ansi = require('opus.ansi')
local Event = require('opus.event')
local UI = require('opus.ui')
local Util = require('opus.util')
local fs = _G.fs
local peripheral = _G.peripheral
local source, target
local function getDriveInfo(tgt)
local total = 0
local throttle = Util.throttle()
tgt = fs.combine(tgt, '')
local src = fs.getNode(tgt).source or tgt
local function recurse(path)
throttle()
if fs.isDir(path) then
if path ~= src then
total = total + 500
end
for _, v in pairs(fs.native.list(path)) do
recurse(fs.combine(path, v))
end
else
local sz = fs.getSize(path)
total = total + math.max(500, sz)
end
end
recurse(src)
local drive = fs.getDrive(src)
return {
path = tgt,
drive = drive,
type = peripheral.getType(drive) or drive,
used = total,
free = fs.getFreeSpace(src),
mountPoint = src,
}
end
local function getDrives(exclude)
local drives = { }
for _, path in pairs(fs.native.list('/')) do
local side = fs.getDrive(path)
if side and not drives[side] and not fs.isReadOnly(path) and side ~= exclude then
if side == 'hdd' then
path = ''
end
drives[side] = getDriveInfo(path)
end
end
return drives
end
local page = UI.Page {
wizard = UI.Wizard {
ey = -2,
partitions = UI.WizardPage {
index = 1,
info = UI.TextArea {
x = 3, y = 2, ex = -3, ey = 5,
value = [[Move the contents of a directory to another disk. A link will be created to point to that location.]]
},
grid = UI.Grid {
x = 2, y = 7, ex = -2, ey = -2,
columns = {
{ heading = 'Path', key = 'path', textColor = 'yellow', width = 10 },
{ heading = 'Mount Point', key = 'mountPoint' },
{ heading = 'Used', key = 'used', width = 6 },
},
sortColumn = 'path',
getDisplayValues = function (_, row)
row = Util.shallowCopy(row)
row.used = Util.toBytes(row.used)
return row
end,
enable = function(self)
Event.onTimeout(0, function()
local mounts = {
usr = getDriveInfo('usr/config'),
packages = getDriveInfo('packages'),
}
self:setValues(mounts)
self:draw()
self:sync()
end)
self:setValues({ })
UI.Grid.enable(self)
end,
},
validate = function(self)
target = self.grid:getSelected()
return not not target
end,
},
mounts = UI.WizardPage {
index = 2,
info = UI.TextArea {
x = 3, y = 2, ex = -3, ey = 5,
value = [[Select the target disk. Labeled computers can be inserted into disk drives for larger volumes.]]
},
grid = UI.Grid {
x = 2, y = 7, ex = -2, ey = -2,
columns = {
{ heading = 'Path', key = 'path', textColor = 'yellow', width = 10 },
{ heading = 'Type', key = 'type' },
{ heading = 'Side', key = 'drive' },
{ heading = 'Free', key = 'free', width = 6 },
},
sortColumn = 'path',
getDisplayValues = function (_, row)
row = Util.shallowCopy(row)
row.free = Util.toBytes(row.free)
return row
end,
getRowTextColor = function(self, row)
if row.free < target.used then
return 'lightGray'
end
return UI.Grid.getRowTextColor(self, row)
end,
enable = function(self)
Event.on({ 'disk', 'disk_eject', 'partition_update' }, function()
self:setValues(getDrives(target.drive))
self:draw()
self:sync()
end)
os.queueEvent('partition_update')
self:setValues({ })
UI.Grid.enable(self)
end,
},
validate = function(self)
source = self.grid:getSelected()
if not source then
self:emit({ type = 'notify', message = 'No drive selected' })
elseif source.free < target.used then
self:emit({ type = 'notify', message = 'Insufficient disk space' })
else
return true
end
end,
},
confirm = UI.WizardPage {
index = 3,
info = UI.TextArea {
x = 2, y = 2, ex = -2, ey = -2,
marginTop = 1, marginLeft = 1,
backgroundColor = 'black',
},
enable = function(self)
local fstab = Util.readFile('usr/etc/fstab')
local lines = { }
table.insert(lines, string.format('%sReview changes%s\n', Ansi.yellow, Ansi.reset))
if fstab then
for _,l in ipairs(Util.split(fstab)) do
l = Util.trim(l)
if #l > 0 and l:sub(1, 1) ~= '#' then
local m = Util.matches(l)
if m and m[1] and m[1] == target.path then
table.insert(lines, string.format('Removed from usr/etc/fstab:\n%s%s%s\n', Ansi.red, l, Ansi.reset))
end
end
end
end
local t = target.path
local s = fs.combine(source.path .. '/' .. target.path, '')
if t ~= s then
table.insert(lines, string.format('Added to usr/etc/fstab:\n%s%s linkfs %s%s\n', Ansi.green, t, s, Ansi.reset))
end
table.insert(lines, string.format('Move directory:\n%s/%s -> /%s', Ansi.green, target.mountPoint, s))
self.info:setText(table.concat(lines, '\n'))
UI.WizardPage.enable(self)
end,
validate = function(self)
if self.changesApplied then
return true
end
local fstab = Util.readFile('usr/etc/fstab')
local lines = { }
if fstab then
for _,l in ipairs(Util.split(fstab)) do
table.insert(lines, l)
l = Util.trim(l)
if #l > 0 and l:sub(1, 1) ~= '#' then
local m = Util.matches(l)
if m and m[1] and m[1] == target.path then
fs.unmount(m[1])
table.remove(lines)
end
end
end
end
local t = target.path
local s = fs.combine(source.path .. '/' .. target.path, '')
fs.move('/' .. target.mountPoint, '/' .. s)
if t ~= s then
table.insert(lines, string.format('%s linkfs %s', t, s))
fs.mount(t, 'linkfs', s)
end
Util.writeFile('usr/etc/fstab', table.concat(lines, '\n'))
self.parent.nextButton.text = 'Exit'
self.parent.cancelButton:disable()
self.parent.previousButton:disable()
self.changesApplied = true
self.info:setValue('Changes have been applied')
self.parent:draw()
end,
},
},
notification = UI.Notification { },
eventHandler = function(self, event)
if event.type == 'notify' then
self.notification:error(event.message)
elseif event.type == 'accept' or event.type == 'cancel' then
UI:quit()
end
return UI.Page.eventHandler(self, event)
end,
}
UI:disableEffects()
UI:setPage(page)
UI:start()

View File

@@ -1,3 +1,5 @@
local Alt = require('opus.alternate')
local kernel = _G.kernel
local os = _G.os
local shell = _ENV.shell
@@ -18,7 +20,7 @@ kernel.hook('kernel_focus', function(_, eventData)
end
end
if nextTab == launcherTab then
shell.switchTab(shell.openTab('shell'))
shell.switchTab(shell.openTab(Alt.get('shell')))
else
shell.switchTab(nextTab.uid)
end

View File

@@ -46,7 +46,7 @@ local page = UI.Page {
configTabs = UI.Tabs {
y = 2,
filterTab = UI.Tab {
title = 'Filter',
tabTitle = 'Filter',
noFill = true,
filterGridText = UI.Text {
x = 2, y = 2,
@@ -93,7 +93,7 @@ local page = UI.Page {
},
},
modemTab = UI.Tab {
title = 'Modem',
tabTitle = 'Modem',
channelGrid = UI.ScrollingGrid {
x = 2, y = 2,
width = 12, height = 5,
@@ -255,7 +255,7 @@ function page.packetSlide:eventHandler(event)
page:setFocus(page.packetGrid)
elseif event.type == 'packet_lua' then
multishell.openTab(_ENV, { path = 'sys/apps/Lua.lua', args = { self.currentPacket.message }, focused = true })
multishell.openTab({ path = 'sys/apps/Lua.lua', args = { self.currentPacket.message }, focused = true })
elseif event.type == 'prev_packet' then
local c = self.currentPacket

View File

@@ -6,6 +6,62 @@ local shell = _ENV.shell
UI:configure('System', ...)
local systemPage = UI.Page {
tabs = UI.Tabs {
settings = UI.Tab {
tabTitle = 'Category',
grid = UI.ScrollingGrid {
x = 2, y = 2, ex = -2, ey = -2,
columns = {
{ heading = 'Name', key = 'name' },
{ heading = 'Description', key = 'description' },
},
sortColumn = 'name',
autospace = true,
},
},
},
notification = UI.Notification(),
accelerators = {
[ 'control-q' ] = 'quit',
},
}
function systemPage.tabs.settings:eventHandler(event)
if event.type == 'grid_select' then
local tab = event.selected.tab
if not systemPage.tabs[tab.tabTitle] then
systemPage.tabs:add({ [ tab.tabTitle ] = tab })
tab:disable()
end
systemPage.tabs:selectTab(tab)
--self.parent:draw()
return true
end
end
function systemPage:eventHandler(event)
if event.type == 'quit' then
UI:quit()
elseif event.type == 'success_message' then
self.notification:success(event.message)
elseif event.type == 'info_message' then
self.notification:info(event.message)
elseif event.type == 'error_message' then
self.notification:error(event.message)
elseif event.type == 'tab_activate' then
event.activated:focusFirst()
else
return UI.Page.eventHandler(self, event)
end
return true
end
local function loadDirectory(dir)
local plugins = { }
for _, file in pairs(fs.list(dir)) do
@@ -14,69 +70,16 @@ local function loadDirectory(dir)
_G.printError('Error loading: ' .. file)
error(m or 'Unknown error')
elseif s and m then
table.insert(plugins, { tab = m, name = m.title, description = m.description })
table.insert(plugins, { tab = m, name = m.tabTitle, description = m.description })
end
end
return plugins
end
local programDir = fs.getDir(_ENV.arg[0])
local programDir = fs.getDir(shell.getRunningProgram())
local plugins = loadDirectory(fs.combine(programDir, 'system'), { })
local page = UI.Page {
tabs = UI.Tabs {
settings = UI.Tab {
title = 'Category',
grid = UI.ScrollingGrid {
x = 2, y = 2, ex = -2, ey = -2,
columns = {
{ heading = 'Name', key = 'name' },
{ heading = 'Description', key = 'description' },
},
sortColumn = 'name',
autospace = true,
values = plugins,
},
accelerators = {
grid_select = 'category_select',
}
},
},
notification = UI.Notification(),
accelerators = {
[ 'control-q' ] = 'quit',
},
eventHandler = function(self, event)
if event.type == 'quit' then
UI:quit()
systemPage.tabs.settings.grid:setValues(plugins)
elseif event.type == 'category_select' then
local tab = event.selected.tab
if not self.tabs[tab.title] then
self.tabs:add({ [ tab.title ] = tab })
end
self.tabs:selectTab(tab)
return true
elseif event.type == 'success_message' then
self.notification:success(event.message)
elseif event.type == 'info_message' then
self.notification:info(event.message)
elseif event.type == 'error_message' then
self.notification:error(event.message)
elseif event.type == 'tab_activate' then
event.activated:focusFirst()
else
return UI.Page.eventHandler(self, event)
end
return true
end,
}
UI:setPage(page)
UI:setPage(systemPage)
UI:start()

View File

@@ -12,7 +12,6 @@ local page = UI.Page {
buttons = {
{ text = 'Activate', event = 'activate' },
{ text = 'Terminate', event = 'terminate' },
{ text = 'Inspect', event = 'inspect' },
},
},
grid = UI.ScrollingGrid {
@@ -40,7 +39,7 @@ local page = UI.Page {
},
accelerators = {
[ 'control-q' ] = 'quit',
[ ' ' ] = 'activate',
space = 'activate',
t = 'terminate',
},
eventHandler = function (self, event)
@@ -50,12 +49,6 @@ local page = UI.Page {
multishell.setFocus(t.uid)
elseif event.type == 'terminate' then
multishell.terminate(t.uid)
elseif event.type == 'inspect' then
multishell.openTab(_ENV, {
path = 'sys/apps/Lua.lua',
args = { t },
focused = true,
})
end
end
if event.type == 'quit' then

View File

@@ -1,54 +0,0 @@
local Config = require('opus.config')
local UI = require('opus.ui')
local shell = _ENV.shell
local config = Config.load('version')
if not config.current then
return
end
UI:setPage(UI.Page {
UI.Text {
x = 2, y = 2, ex = -2,
align = 'center',
value = 'Opus has been updated.',
textColor = 'yellow',
},
UI.TextArea {
x = 2, y = 4, ey = -8,
value = config.details,
},
UI.Button {
x = 2, y = -6, width = 21,
event = 'skip',
text = 'Skip this version',
},
UI.Button {
x = 2, y = -4, width = 21,
event = 'remind',
text = 'Remind me tomorrow',
},
UI.Button {
x = 2, y = -2, width = 21,
event = 'update',
text = 'Update'
},
eventHandler = function(self, event)
if event.type == 'skip' then
config.skip = config.current
Config.update('version', config)
UI:quit()
elseif event.type == 'remind' then
UI:quit()
elseif event.type == 'update' then
shell.openForegroundTab('update update')
UI:quit()
end
return UI.Page.eventHandler(self, event)
end,
})
UI:start()

View File

@@ -2,7 +2,6 @@ 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
@@ -17,9 +16,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 = [[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
Anavrins: Encryption/security/custom apps
@@ -29,127 +28,92 @@ LDDestroier: Art design + custom apps
Lemmmy: Application improvements
%sContribute at:%s
https://git.spatulaa.com/MayaTheShy/Opus]]
https://github.com/kepler155c/opus]]
local page = UI.Page {
wizard = UI.Wizard {
ey = -2,
splash = UI.WizardPage {
index = 1,
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 2, ey = -2,
value = string.format(splashIntro, Ansi.white),
pages = {
splash = UI.WizardPage {
index = 1,
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 2, ey = -2,
value = string.format(splashIntro, Ansi.white),
},
},
},
label = UI.WizardPage {
index = 2,
labelText = UI.Text {
x = 3, y = 2,
value = 'Label'
label = UI.WizardPage {
index = 2,
labelText = UI.Text {
x = 3, y = 2,
value = 'Label'
},
label = UI.TextEntry {
x = 9, y = 2, ex = -3,
limit = 32,
value = os.getComputerLabel(),
},
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 4, ey = -3,
value = string.format(labelIntro, Ansi.white),
},
validate = function (self)
if self.label.value then
os.setComputerLabel(self.label.value)
end
return true
end,
},
label = UI.TextEntry {
x = 9, y = 2, ex = -3,
limit = 32,
value = os.getComputerLabel(),
password = UI.WizardPage {
index = 3,
passwordLabel = UI.Text {
x = 3, y = 2,
value = 'Password'
},
newPass = UI.TextEntry {
x = 12, ex = -3, y = 2,
limit = 32,
mask = true,
shadowText = 'password',
},
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 5, ey = -3,
value = string.format(passwordIntro, Ansi.white),
},
validate = function (self)
if type(self.newPass.value) == "string" and #self.newPass.value > 0 then
Security.updatePassword(SHA.compute(self.newPass.value))
end
return true
end,
},
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 4, ey = -3,
value = string.format(labelIntro, Ansi.white),
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,
value = string.format(packagesIntro, Ansi.white),
},
},
validate = function (self)
if self.label.value then
os.setComputerLabel(self.label.value)
end
return true
end,
},
password = UI.WizardPage {
index = 3,
passwordLabel = UI.Text {
x = 3, y = 2,
value = 'Password'
},
newPass = UI.TextEntry {
x = 12, ex = -3, y = 2,
limit = 32,
mask = true,
shadowText = 'password',
},
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 5, ey = -3,
value = string.format(passwordIntro, Ansi.white),
},
validate = function (self)
if type(self.newPass.value) == "string" and #self.newPass.value > 0 then
Security.updatePassword(SHA.compute(self.newPass.value))
end
return true
end,
},
packages = UI.WizardPage {
index = 4,
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
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,
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 2, ey = -2,
value = string.format(contributorsIntro, Ansi.white, Ansi.yellow, Ansi.white),
contributors = UI.WizardPage {
index = 5,
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 2, ey = -2,
value = string.format(contributorsIntro, Ansi.white, Ansi.yellow, Ansi.white),
},
},
},
},

View File

@@ -1,24 +0,0 @@
local Util = require('opus.util')
-- some programs expect to be run in the global scope
-- ie. busted, moonscript
-- create a new environment mimicing pure lua
local fs = _G.fs
local shell = _ENV.shell
local env = Util.shallowCopy(_G)
Util.merge(env, _ENV)
env._G = env
env.arg = { ... }
env.arg[0] = shell.resolveProgram(table.remove(env.arg, 1) or error('file name is required'))
_G.requireInjector(env, fs.getDir(env.arg[0]))
local s, m = Util.run(env, env.arg[0], table.unpack(env.arg))
if not s then
error(m, -1)
end

View File

@@ -17,7 +17,7 @@ local page = UI.Page {
UI:quit()
end
return UI.Page.eventHandler(self, event)
return UI.FileSelect.eventHandler(self, event)
end,
}

View File

@@ -1,22 +0,0 @@
local SHA = require("opus.crypto.sha2")
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, 10 do
password = password .. acceptableCharacters[math.random(acceptableCharactersLen)]
end
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)

View File

@@ -49,7 +49,7 @@ page = UI.Page {
backgroundColor = colors.red,
y = '50%',
properties = UI.Tab {
title = 'Properties',
tabTitle = 'Properties',
grid = UI.ScrollingGrid {
headerBackgroundColor = colors.red,
sortColumn = 'key',
@@ -64,7 +64,7 @@ page = UI.Page {
},
methodsTab = UI.Tab {
index = 2,
title = 'Methods',
tabTitle = 'Methods',
grid = UI.ScrollingGrid {
ex = '50%',
headerBackgroundColor = colors.red,
@@ -85,7 +85,7 @@ page = UI.Page {
},
events = UI.Tab {
index = 1,
title = 'Events',
tabTitle = 'Events',
UI.MenuBar {
y = -1,
backgroundColor = colors.red,
@@ -110,7 +110,7 @@ page = UI.Page {
self.grid:draw()
elseif event.type == 'grid_select' then
multishell.openTab(_ENV, {
multishell.openTab({
path = 'sys/apps/Lua.lua',
args = { event.selected.raw },
focused = true,

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

@@ -1,3 +1,5 @@
_G.requireInjector(_ENV)
local Event = require('opus.event')
local Util = require('opus.util')

View File

@@ -59,10 +59,6 @@ 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')
@@ -83,10 +79,10 @@ Event.addRoutine(function()
end)
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)
Event.on('network_detach', function(_, computer)
print('samba: detaching ' .. sanitizeLabel(computer))
fs.unmount(fs.combine('network', sanitizeLabel(computer)))
print('samba: detaching ' .. computer.label)
fs.unmount(fs.combine('network', computer.label))
end)

View File

@@ -31,14 +31,21 @@ local function snmpConnection(socket)
socket:write('pong')
elseif msg.type == 'script' then
kernel.run(_ENV, {
chunk = msg.args,
title = 'script',
})
local env = setmetatable(Util.shallowCopy(_ENV), { __index = _G })
local fn, err = load(msg.args, 'script', nil, env)
if fn then
kernel.run({
fn = fn,
env = env,
title = 'script',
})
else
_G.printError(err)
end
elseif msg.type == 'scriptEx' then
local s, m = pcall(function()
local env = kernel.makeEnv(_ENV)
local env = setmetatable(Util.shallowCopy(_ENV), { __index = _G })
local fn, m = load(msg.args, 'script', nil, env)
if not fn then
error(m)
@@ -152,7 +159,7 @@ local function getSlots()
end
local function sendInfo()
if os.clock() - infoTimer >= 5 then -- don't flood
if os.clock() - infoTimer >= 1 then -- don't flood
infoTimer = os.clock()
info.label = os.getComputerLabel()
info.uptime = math.floor(os.clock())
@@ -194,25 +201,16 @@ local function sendInfo()
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
local elapsed = os.clock()-c.timestamp
if c.active and elapsed > 50 then
if c.active and elapsed > 15 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()
@@ -222,5 +220,4 @@ Event.on('turtle_response', function()
end
end)
-- send info early so that computers show soon after booting
Event.onTimeout(math.random() * 2 + 1, sendInfo)
Event.onTimeout(1, sendInfo)

View File

@@ -1,9 +1,9 @@
local Alt = require('opus.alternate')
local Event = require('opus.event')
local Socket = require('opus.socket')
local Util = require('opus.util')
local kernel = _G.kernel
local shell = _ENV.shell
local term = _G.term
local window = _G.window
@@ -41,17 +41,18 @@ local function telnetHost(socket, mode)
end
end
local shellThread = kernel.run(_ENV, {
local shellThread = kernel.run({
terminal = win,
window = win,
title = mode .. ' client',
hidden = true,
fn = function()
Util.run(kernel.makeEnv(_ENV), shell.resolveProgram('shell'), table.unpack(termInfo.program))
co = coroutine.create(function()
Util.run(_ENV, Alt.get('shell'), table.unpack(termInfo.program))
if socket.queue then
socket:write(socket.queue)
end
socket:close()
end,
end)
})
Event.addRoutine(function()

View File

@@ -7,7 +7,6 @@
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
@@ -36,19 +35,7 @@ function transport.read(socket)
local data = table.remove(socket.messages, 1)
if data then
if socket.options.ENCRYPT then
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]
return table.unpack(Crypto.decrypt(data[1], socket.enckey)), data[2]
end
return table.unpack(data)
end
@@ -91,15 +78,7 @@ Event.on('transport_encrypt', function()
if socket and socket.connected then
local msg = entry[2]
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
msg.data = Crypto.encrypt({ msg.data }, socket.enckey)
socket.transmit(socket.dport, socket.dhost, msg)
end
end

View File

@@ -6,45 +6,24 @@ local Util = require('opus.util')
local trustId = '01c3ba27fe01383a03a1785276d99df27c3edcef68fbf231ca'
local oneTimePassword -- nil by default
local function validateData(data, password, dhost)
local s
s, data = pcall(Crypto.decrypt, data, password)
if s and data and type(data) == "table" and data.pk and data.dh == dhost then
local trustList = Util.readTable('usr/.known_hosts') or { }
trustList[data.dh] = data.pk
Util.writeTable('usr/.known_hosts', trustList)
return true
else
return false
end
end
local function trustConnection(socket)
local data = socket:read(2)
if data then
local trustKey = Security.getTrustKey()
if not trustKey then
local password = Security.getPassword()
if not password then
socket:write({ msg = 'No password has been set' })
else
if validateData(data, trustKey, socket.dhost) then
print("Accepted trust from " .. socket.dhost)
local s
s, data = pcall(Crypto.decrypt, data, password)
if s and data and data.pk and data.dh == socket.dhost then
local trustList = Util.readTable('usr/.known_hosts') or { }
trustList[data.dh] = data.pk
Util.writeTable('usr/.known_hosts', trustList)
socket:write({ success = true, msg = 'Trust accepted' })
return
else
socket:write({ msg = 'Invalid password' })
end
if oneTimePassword then
if validateData(data, oneTimePassword, socket.dhost) then
print("Accepted trust from " .. socket.dhost .. "using one-time password")
socket:write({ success = true, msg = 'Trust accepted - this one-time password will not be usable again' })
oneTimePassword = nil -- Make sure nobody can use the one-time password again
return
end
end
socket:write({ msg = 'Invalid password' })
end
end
end
@@ -65,12 +44,3 @@ Event.addRoutine(function()
end
end
end)
Event.addRoutine(function()
while true do
local _event, password = os.pullEvent("set_otp")
oneTimePassword = password
print("got new one-time password")
end
end)

View File

@@ -1,9 +1,6 @@
local BulkGet = require('opus.bulkget')
local Config = require('opus.config')
local Git = require('opus.git')
local LZW = require('opus.compress.lzw')
local Packages = require('opus.packages')
local Tar = require('opus.compress.tar')
local Util = require('opus.util')
local fs = _G.fs
@@ -19,8 +16,9 @@ local function makeSandbox()
end
local function Syntax(msg)
print('Syntax: package list | install [name] ... | update [name] | updateall | uninstall [name]\n')
error(msg)
_G.printError(msg)
print('\nSyntax: Package list | install [name] ... | update [name] | uninstall [name]')
error(0)
end
local function progress(max)
@@ -78,24 +76,6 @@ 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
fs.delete(packageDir)
local showProgress = progress(Util.size(list))
local getList = { }
@@ -116,12 +96,6 @@ local function install(name, isUpdate, ignoreDeps)
if not isUpdate then
runScript(manifest.install)
end
if Config.load('package').compression then
local c = Tar.tar_string(packageDir)
Util.writeFile(packageDir .. '.tar.lzw', LZW.compress(c), 'wb')
fs.delete(packageDir)
end
end
if action == 'list' then
@@ -178,7 +152,6 @@ if action == 'uninstall' then
local packageDir = fs.combine('packages', name)
fs.delete(packageDir)
fs.delete(packageDir .. '.tar.lzw')
print('removed: ' .. packageDir)
return
end

View File

@@ -1,9 +1,10 @@
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(password)
Security.updatePassword(SHA.compute(password))
print('Password updated')
end

View File

@@ -1,12 +1,21 @@
local parentShell = _ENV.shell
_ENV.shell = { }
local trace = require('opus.trace')
local Util = require('opus.util')
local fs = _G.fs
local settings = _G.settings
local shell = _ENV.shell
local fs = _G.fs
local settings = _G.settings
local shell = _ENV.shell
local sandboxEnv = setmetatable({ }, { __index = _G })
for k,v in pairs(_ENV) do
sandboxEnv[k] = v
end
sandboxEnv.shell = shell
_G.requireInjector(_ENV)
local trace = require('opus.trace')
local Util = require('opus.util')
local DIR = (parentShell and parentShell.dir()) or ""
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
@@ -16,16 +25,16 @@ local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {}
local bExit = false
local tProgramStack = {}
local function tokenise(...)
local sLine = table.concat({ ... }, ' ')
local tWords = { }
local function tokenise( ... )
local sLine = table.concat( { ... }, " " )
local tWords = {}
local bQuoted = false
for match in string.gmatch(sLine .. "\"", "(.-)\"") do
for match in string.gmatch( sLine .. "\"", "(.-)\"" ) do
if bQuoted then
table.insert(tWords, match)
table.insert( tWords, match )
else
for m in string.gmatch(match, "[^ \t]+") do
table.insert(tWords, m)
for m in string.gmatch( match, "[^ \t]+" ) do
table.insert( tWords, m )
end
end
bQuoted = not bQuoted
@@ -34,75 +43,37 @@ local function tokenise(...)
return tWords
end
local defaultHandlers = {
function(env, command, args)
return command:match("^(https?:)") and {
title = fs.getName(command),
path = command,
args = args,
load = Util.loadUrl,
env = env,
}
end,
function(env, command, args)
command = env.shell.resolveProgram(command)
or error('No such program')
_G.requireInjector(env, fs.getDir(command))
return {
title = fs.getName(command):match('([^%.]+)'),
path = command,
args = args,
load = loadfile,
env = env,
}
end,
}
function shell.getHandlers()
if parentShell and parentShell.getHandlers then
return parentShell.getHandlers()
end
return defaultHandlers
end
local handlers = shell.getHandlers()
function shell.registerHandler(fn)
table.insert(handlers, 1, fn)
end
local function handleCommand(env, command, args)
for _,v in pairs(handlers) do
local pi = v(env, command, args)
if pi then
return pi
end
end
end
local function run(...)
local function run(env, ...)
local args = tokenise(...)
if #args == 0 then
error('No such program')
local command = table.remove(args, 1) or error('No such program')
local isUrl = not not command:match("^(https?:)")
local path, loadFn
if isUrl then
path = command
loadFn = Util.loadUrl
else
path = shell.resolveProgram(command) or error('No such program')
loadFn = loadfile
end
local pi = handleCommand(shell.makeEnv(_ENV), table.remove(args, 1), args)
local O_v_O, err = pi.load(pi.path, pi.env)
if not O_v_O then
error(err, -1)
local fn, err = loadFn(path, env)
if not fn then
error(err)
end
if _ENV.multishell then
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), pi.title)
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), fs.getName(path):match('([^%.]+)'))
end
tProgramStack[#tProgramStack + 1] = pi
tProgramStack[#tProgramStack + 1] = {
path = path, -- path:match("^https?://([^/:]+:?[0-9]*/?.*)$")
env = env,
args = args,
}
pi.env[ "arg" ] = { [0] = pi.path, table.unpack(pi.args) }
local r = { O_v_O(table.unpack(pi.args)) }
env[ "arg" ] = { [0] = path, table.unpack(args) }
local r = { fn(table.unpack(args)) }
tProgramStack[#tProgramStack] = nil
@@ -117,7 +88,10 @@ function shell.run(...)
oldTitle = _ENV.multishell.getTitle(_ENV.multishell.getCurrent())
end
local r = { trace(run, ...) }
local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G })
_G.requireInjector(env)
local r = { trace(run, env, ...) }
if _ENV.multishell then
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), oldTitle or 'shell')
@@ -131,14 +105,7 @@ function shell.exit()
end
function shell.dir() return DIR end
function shell.setDir(d)
d = fs.combine(d, '')
if not fs.isDir(d) then
error("Not a directory", 2)
end
DIR = d
end
function shell.setDir(d) DIR = d end
function shell.path() return PATH end
function shell.setPath(p) PATH = p end
@@ -151,41 +118,49 @@ function shell.resolve( _sPath )
end
end
function shell.resolveProgram(_sCommand)
function shell.resolveProgram( _sCommand )
if tAliases[_sCommand] ~= nil then
_sCommand = tAliases[_sCommand]
end
local function check(f)
return fs.exists(f) and not fs.isDir(f) and f
if _sCommand:match("^(https?:)") then
return _sCommand
end
local function inPath()
-- Otherwise, look on the path variable
for sPath in string.gmatch(PATH or '', "[^:]+") do
sPath = fs.combine(sPath, _sCommand )
if check(sPath) then
return sPath
end
if check(sPath .. '.lua') then
return sPath .. '.lua'
end
local path = shell.resolve(_sCommand)
if fs.exists(path) and not fs.isDir(path) then
return path
end
if fs.exists(path .. '.lua') then
return path .. '.lua'
end
-- If the path is a global path, use it directly
local sStartChar = string.sub( _sCommand, 1, 1 )
if sStartChar == "/" or sStartChar == "\\" then
local sPath = fs.combine( "", _sCommand )
if fs.exists( sPath ) and not fs.isDir( sPath ) then
return sPath
end
return nil
end
-- Otherwise, look on the path variable
for sPath in string.gmatch(PATH or '', "[^:]+") do
sPath = fs.combine(sPath, _sCommand )
if fs.exists( sPath ) and not fs.isDir( sPath ) then
return sPath
end
if fs.exists(sPath .. '.lua') then
return sPath .. '.lua'
end
end
-- so... even if you are in the rom directory and you run:
-- 'packages/common/edit.lua', allow this even though it
-- does not use a leading slash. Ideally, fs.combine would
-- provide the leading slash... but it does not.
return (not _sCommand:find('/')) and inPath()
or check(shell.resolve(_sCommand))
or check(shell.resolve(_sCommand) .. '.lua')
or check(_sCommand)
or check(_sCommand .. '.lua')
-- Not found
return nil
end
function shell.programs(_bIncludeHidden)
local tItems = { }
function shell.programs( _bIncludeHidden )
local tItems = {}
-- Add programs from the path
for sPath in string.gmatch(PATH, "[^:]+") do
@@ -202,84 +177,98 @@ function shell.programs(_bIncludeHidden)
end
-- Sort and return
local tItemList = { }
for sItem in pairs(tItems) do
table.insert(tItemList, sItem)
local tItemList = {}
for sItem in pairs( tItems ) do
table.insert( tItemList, sItem )
end
table.sort(tItemList)
table.sort( tItemList )
return tItemList
end
function shell.completeProgram(sLine)
if #sLine > 0 and string.sub(sLine, 1, 1) == '/' then
local function completeProgram( sLine )
if #sLine > 0 and string.sub( sLine, 1, 1 ) == "/" then
-- Add programs from the root
return fs.complete(sLine, '', true, false)
end
return fs.complete( sLine, "", true, false )
else
local tResults = {}
local tSeen = {}
local tResults = { }
local tSeen = { }
-- Add aliases
for sAlias in pairs( tAliases ) do
if #sAlias > #sLine and string.sub(sAlias, 1, #sLine) == sLine then
local sResult = string.sub(sAlias, #sLine + 1)
if not tSeen[sResult] then
table.insert(tResults, sResult .. ' ')
tSeen[sResult] = true
-- Add aliases
for sAlias in pairs( tAliases ) do
if #sAlias > #sLine and string.sub( sAlias, 1, #sLine ) == sLine then
local sResult = string.sub( sAlias, #sLine + 1 )
if not tSeen[ sResult ] then
table.insert( tResults, sResult )
tSeen[ sResult ] = true
end
end
end
end
-- Add programs from the path
local tPrograms = shell.programs()
for n=1,#tPrograms do
local sProgram = tPrograms[n]
if #sProgram >= #sLine and string.sub(sProgram, 1, #sLine) == sLine then
local sResult = string.sub(sProgram, #sLine + 1)
if not tSeen[sResult] then
table.insert(tResults, sResult .. ' ')
tSeen[sResult] = true
-- Add programs from the path
local tPrograms = shell.programs()
for n=1,#tPrograms do
local sProgram = tPrograms[n]
if #sProgram > #sLine and string.sub( sProgram, 1, #sLine ) == sLine then
local sResult = string.sub( sProgram, #sLine + 1 )
if not tSeen[ sResult ] then
table.insert( tResults, sResult )
tSeen[ sResult ] = true
end
end
end
end
-- Sort and return
table.sort(tResults)
return tResults
-- Sort and return
table.sort( tResults )
return tResults
end
end
local function completeProgramArgument( sProgram, nArgument, sPart, tPreviousParts )
local tInfo = tCompletionInfo[ sProgram ]
if tInfo then
return tInfo.fnComplete( shell, nArgument, sPart, tPreviousParts )
end
return nil
end
function shell.complete(sLine)
local tWords = tokenise(sLine)
local nIndex = #tWords
if string.sub(sLine, #sLine, #sLine) == ' ' and #Util.trim(sLine) > 0 then
nIndex = nIndex + 1
end
if nIndex == 0 then
return fs.complete('', shell.dir(), true, false)
elseif nIndex == 1 then
local results = shell.completeProgram(tWords[1] or '')
for _, v in pairs(fs.complete(table.concat(tWords, ' '), shell.dir(), true, false)) do
table.insert(results, v)
if #sLine > 0 then
local tWords = tokenise( sLine )
local nIndex = #tWords
if string.sub( sLine, #sLine, #sLine ) == " " then
nIndex = nIndex + 1
end
return results
if nIndex == 1 then
local sBit = tWords[1] or ""
local sPath = shell.resolveProgram( sBit )
if tCompletionInfo[ sPath ] then
return { " " }
else
local tResults = completeProgram( sBit )
for n=1,#tResults do
local sResult = tResults[n]
local cPath = shell.resolveProgram( sBit .. sResult )
if tCompletionInfo[ cPath ] then
tResults[n] = sResult .. " "
end
end
return tResults
end
else
local sPath = shell.resolveProgram(tWords[1])
local sPart = tWords[nIndex] or ''
local tPreviousParts = tWords
tPreviousParts[nIndex] = nil
local results
local tInfo = tCompletionInfo[sPath]
if tInfo then
results = tInfo.fnComplete(shell, nIndex - 1, sPart, tPreviousParts)
elseif nIndex > 1 then
local sPath = shell.resolveProgram( tWords[1] )
local sPart = tWords[nIndex] or ""
local tPreviousParts = tWords
tPreviousParts[nIndex] = nil
return completeProgramArgument( sPath , nIndex - 1, sPart, tPreviousParts )
end
return results and #results > 0 and results
or fs.complete(sPart, shell.dir(), true, false)
end
end
function shell.completeProgram( sProgram )
return completeProgram( sProgram )
end
function shell.setCompletionFunction(sProgram, fnComplete)
tCompletionInfo[sProgram] = { fnComplete = fnComplete }
end
@@ -296,18 +285,20 @@ function shell.getRunningInfo()
return tProgramStack[#tProgramStack]
end
-- convenience function for making a runnable env
function shell.makeEnv(env, dir)
env = setmetatable(Util.shallowCopy(env), { __index = _G })
_G.requireInjector(env, dir)
return env
function shell.setEnv(name, value)
_ENV[name] = value
sandboxEnv[name] = value
end
function shell.setAlias(_sCommand, _sProgram)
function shell.getEnv()
return sandboxEnv
end
function shell.setAlias( _sCommand, _sProgram )
tAliases[_sCommand] = _sProgram
end
function shell.clearAlias(_sCommand)
function shell.clearAlias( _sCommand )
tAliases[_sCommand] = nil
end
@@ -326,6 +317,7 @@ function shell.newTab(tabInfo, ...)
if path then
tabInfo.path = path
tabInfo.env = Util.shallowCopy(sandboxEnv)
tabInfo.args = args
tabInfo.title = fs.getName(path):match('([^%.]+)')
@@ -333,21 +325,25 @@ function shell.newTab(tabInfo, ...)
table.insert(tabInfo.args, 1, tabInfo.path)
tabInfo.path = 'sys/apps/shell.lua'
end
return _ENV.multishell.openTab(_ENV, tabInfo)
return _ENV.multishell.openTab(tabInfo)
end
return nil, 'No such program'
end
if not _ENV.multishell then
function shell.newTab()
error('Multishell is not available')
function shell.openTab( ... )
-- needs to use multishell.launch .. so we can run with stock multishell
local tWords = tokenise( ... )
local sCommand = tWords[1]
if sCommand then
local sPath = shell.resolveProgram(sCommand)
if sPath == "sys/apps/shell.lua" then
return _ENV.multishell.launch(Util.shallowCopy(sandboxEnv), sPath, table.unpack(tWords, 2))
else
return _ENV.multishell.launch(Util.shallowCopy(sandboxEnv), "sys/apps/shell.lua", sCommand, table.unpack(tWords, 2))
end
end
end
function shell.openTab(...)
return shell.newTab({ }, ...)
end
function shell.openForegroundTab( ... )
return shell.newTab({ focused = true }, ...)
end
@@ -362,7 +358,10 @@ end
local tArgs = { ... }
if #tArgs > 0 then
return run(...)
local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G })
_G.requireInjector(env)
return run(env, ...)
end
local Config = require('opus.config')
@@ -379,16 +378,23 @@ local textutils = _G.textutils
local oldTerm
local terminal = term.current()
local _len = string.len
local _rep = string.rep
local _sub = string.sub
if not terminal.scrollUp then
terminal = Terminal.window(term.current())
terminal.setMaxScroll(200)
oldTerm = term.redirect(terminal)
end
local config = {
color = {
textColor = colors.white,
commandTextColor = colors.yellow,
directoryTextColor = colors.orange,
directoryBackgroundColor = colors.black,
promptTextColor = colors.blue,
promptBackgroundColor = colors.black,
directoryColor = colors.green,
fileColor = colors.white,
backgroundColor = colors.black,
@@ -405,15 +411,43 @@ if not _colors.backgroundColor then
_colors.fileColor = colors.white
end
if not terminal.scrollUp then
terminal = Terminal.window(term.current())
terminal.setMaxScroll(200)
oldTerm = term.redirect(terminal)
term.setBackgroundColor(_colors.backgroundColor)
term.clear()
local palette = { }
for n = 1, 16 do
palette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n)
end
local palette = terminal.canvas.palette
if not term.isColor() then
_colors = { }
for k, v in pairs(config.color) do
_colors[k] = Terminal.colorToGrayscale(v)
end
for n = 1, 16 do
palette[2 ^ (n - 1)] = _sub("088888878877787f", n, n)
end
end
local function autocompleteArgument(program, words)
local word = ''
if #words > 1 then
word = words[#words]
end
local tInfo = tCompletionInfo[program]
return tInfo.fnComplete(shell, #words - 1, word, words)
end
local function autocompleteAnything(line, words)
local results = shell.complete(line)
if results and #results == 0 and #words == 1 then
results = nil
end
if not results then
results = fs.complete(words[#words] or '', shell.dir(), true, false)
end
return results
end
local function autocomplete(line)
local words = { }
@@ -427,7 +461,14 @@ local function autocomplete(line)
words = { '' }
end
local results = shell.complete(line) or { }
local results
local program = shell.resolveProgram(words[1])
if tCompletionInfo[program] then
results = autocompleteArgument(program, words) or { }
else
results = autocompleteAnything(line, words) or { }
end
Util.filterInplace(results, function(f)
return not Util.key(results, f .. '/')
@@ -440,8 +481,8 @@ local function autocomplete(line)
if #results == 1 then
words[#words] = results[1]
return table.concat(words, ' ')
elseif #results > 1 then
local function someComplete()
-- ugly (complete as much as possible)
local word = words[#words] or ''
@@ -450,16 +491,16 @@ local function autocomplete(line)
local ch
for _,f in ipairs(results) do
if #f < i then
words[#words] = _sub(f, 1, i - 1)
words[#words] = string.sub(f, 1, i - 1)
return table.concat(words, ' ')
end
if not ch then
ch = _sub(f, i, i)
elseif _sub(f, i, i) ~= ch then
ch = string.sub(f, i, i)
elseif string.sub(f, i, i) ~= ch then
if i == #word + 1 then
return
end
words[#words] = _sub(f, 1, i - 1)
words[#words] = string.sub(f, 1, i - 1)
return table.concat(words, ' ')
end
end
@@ -502,9 +543,10 @@ local function autocomplete(line)
local tw = term.getSize()
local nMaxLen = tw / 8
for _,sItem in pairs(results) do
nMaxLen = math.max(_len(sItem) + 1, nMaxLen)
nMaxLen = math.max(string.len(sItem) + 1, nMaxLen)
end
local nCols = math.floor(tw / nMaxLen)
local w = term.getSize()
local nCols = math.floor(w / nMaxLen)
if #tDirs < nCols then
for _ = #tDirs + 1, nCols do
table.insert(tDirs, '')
@@ -519,9 +561,11 @@ local function autocomplete(line)
end
term.setTextColour(_colors.promptTextColor)
term.setBackgroundColor(_colors.promptBackgroundColor)
term.write("$ " )
term.setTextColour(_colors.commandTextColor)
term.setBackgroundColor(_colors.backgroundColor)
return line
end
end
@@ -548,16 +592,17 @@ local function shellRead(history)
term.setCursorPos(3, cy)
entry.value = entry.value or ''
local filler = #entry.value < lastLen
and _rep(' ', lastLen - #entry.value)
and string.rep(' ', lastLen - #entry.value)
or ''
local str = _sub(entry.value, entry.scroll + 1, entry.width + entry.scroll) .. filler
local str = string.sub(entry.value, entry.scroll + 1, entry.width + entry.scroll) .. filler
local fg = _rep(palette[_colors.commandTextColor], #str)
local bg = _rep(palette[_colors.backgroundColor], #str)
if entry.mark.active then
bg = _rep('f', entry.mark.x) ..
_rep('7', entry.mark.ex - entry.mark.x) ..
_rep('f', #entry.value - entry.mark.ex + #filler + 1)
bg = _sub(bg, entry.scroll + 1, entry.scroll + #str)
local sx = entry.mark.x - entry.scroll + 1
local ex = entry.mark.ex - entry.scroll + 1
bg = string.rep('f', sx - 1) ..
string.rep('7', ex - sx) ..
string.rep('f', #str - ex + 1)
end
term.blit(str, fg, bg)
updateCursor()
@@ -609,11 +654,6 @@ local function shellRead(history)
end
end
elseif ie.code == 'control-l' then
term.clear()
term.setCursorPos(1, 0) -- Y:0 ?
break
else
entry:process(ie)
entry.value = entry.value or ''
@@ -625,7 +665,6 @@ local function shellRead(history)
end
elseif event == "term_resize" then
terminal.reposition(1, 1, oldTerm.getSize())
entry.width = term.getSize() - 3
entry:updateScroll()
redraw()
@@ -633,26 +672,30 @@ local function shellRead(history)
end
print()
term.setCursorBlink(false)
term.setCursorBlink( false )
return entry.value or ''
end
local history = History.load('usr/.shell_history', 100)
local history = History.load('usr/.shell_history', 25)
term.setBackgroundColor(_colors.backgroundColor)
term.clear()
if settings.get("motd.enable") then
if settings.get("motd.enabled") then
shell.run("motd")
end
while not bExit do
if config.displayDirectory then
term.setTextColour(_colors.directoryTextColor)
term.setBackgroundColor(_colors.directoryBackgroundColor)
print('==' .. os.getComputerLabel() .. ':/' .. DIR)
end
term.setTextColour(_colors.promptTextColor)
term.setBackgroundColor(_colors.promptBackgroundColor)
term.write("$ " )
term.setTextColour(_colors.commandTextColor)
term.setBackgroundColor(_colors.backgroundColor)
local sLine = shellRead(history)
if bExit then -- terminated
break
@@ -664,11 +707,6 @@ while not bExit do
term.setTextColour(_colors.textColor)
if #sLine > 0 then
local result, err = shell.run(sLine)
local cx = term.getCursorPos()
if cx ~= 1 then
print()
end
term.setBackgroundColor(_colors.backgroundColor)
if not result and err then
_G.printError(err)
end

View File

@@ -4,7 +4,7 @@ local UI = require('opus.ui')
local kernel = _G.kernel
local aliasTab = UI.Tab {
title = 'Aliases',
tabTitle = 'Aliases',
description = 'Shell aliases',
alias = UI.TextEntry {
x = 2, y = 2, ex = -2,
@@ -13,6 +13,7 @@ local aliasTab = UI.Tab {
},
path = UI.TextEntry {
y = 3, x = 2, ex = -2,
limit = 256,
shadowText = 'Program path',
accelerators = {
enter = 'new_alias',

View File

@@ -0,0 +1,77 @@
local Array = require('opus.array')
local Config = require('opus.config')
local UI = require('opus.ui')
local tab = UI.Tab {
tabTitle = 'Preferred',
description = 'Select preferred applications',
apps = UI.ScrollingGrid {
x = 2, y = 2,
ex = 12, ey = -3,
columns = {
{ key = 'name' },
},
sortColumn = 'name',
disableHeader = true,
},
choices = UI.Grid {
x = 14, y = 2,
ex = -2, ey = -3,
disableHeader = true,
columns = {
{ key = 'file' },
},
getRowTextColor = function(self, row)
if row == self.values[1] then
return 'yellow'
end
return UI.Grid.getRowTextColor(self, row)
end,
},
statusBar = UI.StatusBar {
values = 'Double-click to set as preferred'
},
}
function tab:updateChoices()
local app = self.apps:getSelected().name
local choices = { }
for _, v in pairs(self.config[app]) do
table.insert(choices, { file = v })
end
self.choices:setValues(choices)
self.choices:draw()
end
function tab:enable()
self.config = Config.load('alternate')
local apps = { }
for k, _ in pairs(self.config) do
table.insert(apps, { name = k })
end
self.apps:setValues(apps)
self:updateChoices()
UI.Tab.enable(self)
end
function tab:eventHandler(event)
if event.type == 'grid_focus_row' and event.element == self.apps then
self:updateChoices()
elseif event.type == 'grid_select' and event.element == self.choices then
local app = self.apps:getSelected().name
Array.removeByValue(self.config[app], event.selected.file)
table.insert(self.config[app], 1, event.selected.file)
self:updateChoices()
Config.update('alternate', self.config)
else
return UI.Tab.eventHandler(self, event)
end
return true
end
return tab

View File

@@ -6,7 +6,7 @@ if _G.http.websocket then
local config = Config.load('cloud')
local tab = UI.Tab {
title = 'Cloud',
tabTitle = 'Cloud',
description = 'Cloud Catcher options',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,

View File

@@ -16,7 +16,7 @@ local NftImages = {
}
local tab = UI.Tab {
title = 'Disks Usage',
tabTitle = 'Disks Usage',
description = 'Visualise HDD and disks usage',
drives = UI.ScrollingGrid {
@@ -136,17 +136,6 @@ function tab:enable()
self:updateDrives()
self:updateInfo()
UI.Tab.enable(self)
self.handler = Event.on({ 'disk', 'disk_eject' }, function()
os.sleep(1)
tab:updateDrives()
tab:updateInfo()
tab:sync()
end)
end
function tab:disable()
Event.off(self.handler)
UI.Tab.disable(self)
end
function tab:eventHandler(event)
@@ -158,4 +147,11 @@ function tab:eventHandler(event)
return true
end
Event.on({ 'disk', 'disk_eject' }, function()
os.sleep(1)
tab:updateDrives()
tab:updateInfo()
tab:sync()
end)
return tab

View File

@@ -5,7 +5,7 @@ local peripheral = _G.peripheral
local settings = _G.settings
return peripheral.find('monitor') and UI.Tab {
title = 'Kiosk',
tabTitle = 'Kiosk',
description = 'Kiosk options',
form = UI.Form {
x = 2, y = 2, ex = -2, ey = 5,

View File

@@ -5,7 +5,7 @@ local fs = _G.fs
local os = _G.os
return UI.Tab {
title = 'Label',
tabTitle = 'Label',
description = 'Set the computer label',
labelText = UI.Text {
x = 3, y = 3,
@@ -26,12 +26,12 @@ return UI.Tab {
x = 2, y = 5, ex = -2, ey = -2,
values = {
{ name = '', value = '' },
{ 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()) },
{ 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()) },
},
disableHeader = true,
inactive = true,

View File

@@ -7,7 +7,7 @@ local fs = _G.fs
local config = Config.load('multishell')
local tab = UI.Tab {
title = 'Launcher',
tabTitle = 'Launcher',
description = 'Set the application launcher',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 5,
@@ -26,6 +26,7 @@ local tab = UI.Tab {
},
custom = UI.TextEntry {
x = 13, ex = -3, y = 4,
limit = 128,
shadowText = 'File name',
},
button = UI.Button {

View File

@@ -6,7 +6,7 @@ local colors = _G.colors
local device = _G.device
return UI.Tab {
title = 'Network',
tabTitle = 'Network',
description = 'Networking options',
info = UI.TextArea {
x = 2, y = 5, ex = -2, ey = -2,

View File

@@ -3,7 +3,7 @@ local SHA = require('opus.crypto.sha2')
local UI = require('opus.ui')
return UI.Tab {
title = 'Password',
tabTitle = 'Password',
description = 'Wireless network password',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,

View File

@@ -3,7 +3,7 @@ local UI = require('opus.ui')
local Util = require('opus.util')
local tab = UI.Tab {
title = 'Path',
tabTitle = 'Path',
description = 'Set the shell path',
tabClose = true,
[1] = UI.Window {
@@ -11,6 +11,7 @@ local tab = UI.Tab {
},
entry = UI.TextEntry {
x = 3, y = 3, ex = -3,
limit = 256,
shadowText = 'enter new path',
accelerators = {
enter = 'update_path',

View File

@@ -3,11 +3,12 @@ local UI = require('opus.ui')
local Util = require('opus.util')
local tab = UI.Tab {
title = 'Requires',
tabTitle = 'Requires',
description = 'Require path',
tabClose = true,
entry = UI.TextEntry {
x = 2, y = 2, ex = -2,
limit = 256,
shadowText = 'Enter new require path',
accelerators = {
enter = 'update_path',

View File

@@ -8,7 +8,7 @@ local transform = {
}
return settings and UI.Tab {
title = 'Settings',
tabTitle = 'Settings',
description = 'Computercraft settings',
grid = UI.Grid {
x = 2, y = 2, ex = -2, ey = -2,
@@ -27,6 +27,7 @@ return settings and UI.Tab {
form = UI.Form {
y = 2,
value = UI.TextEntry {
limit = 256,
formIndex = 1,
formLabel = 'Value',
formKey = 'value',

View File

@@ -18,7 +18,9 @@ local defaults = {
textColor = colors.white,
commandTextColor = colors.yellow,
directoryTextColor = colors.orange,
directoryBackgroundColor = colors.black,
promptTextColor = colors.blue,
promptBackgroundColor = colors.black,
directoryColor = colors.green,
fileColor = colors.white,
backgroundColor = colors.black,
@@ -37,7 +39,7 @@ if not _colors.backgroundColor then
end
return UI.Tab {
title = 'Shell',
tabTitle = 'Shell',
description = 'Shell options',
grid1 = UI.ScrollingGrid {
y = 2, ey = -10, x = 2, ex = -17,
@@ -84,12 +86,12 @@ return UI.Tab {
if config.displayDirectory then
self:write(1, 1,
'==' .. os.getComputerLabel() .. ':/dir/etc',
_colors.backgroundColor, _colors.directoryTextColor)
_colors.directoryBackgroundColor, _colors.directoryTextColor)
offset = 1
end
self:write(1, 1 + offset, '$ ',
_colors.backgroundColor, _colors.promptTextColor)
_colors.promptBackgroundColor, _colors.promptTextColor)
self:write(3, 1 + offset, 'ls /',
_colors.backgroundColor, _colors.commandTextColor)

View File

@@ -12,12 +12,12 @@ for k,v in pairs(colors) do
end
local allSettings = { }
for k,v in pairs(UI.theme.colors) do
for k,v in pairs(UI.colors) do
allSettings[k] = { name = k, value = v }
end
return UI.Tab {
title = 'Theme',
tabTitle = 'Theme',
description = 'Theme colors',
grid1 = UI.ScrollingGrid {
y = 2, ey = -10, x = 2, ex = -17,

View File

@@ -1,55 +0,0 @@
local UI = require("opus.ui")
local Util = require("opus.util")
local SHA = require('opus.crypto.sha2')
local function split(s)
local b = ""
for i = 1, #s, 5 do
b = b .. s:sub(i, i+4)
if i ~= #s-4 then
b = b .. "-"
end
end
return b
end
return UI.Tab {
title = 'Trust',
description = 'Manage trusted devices',
grid = UI.Grid {
x = 2, y = 2, ex = -2, ey = -3,
autospace = true,
sortColumn = 'id',
columns = {
{ heading = 'Computer ID', key = 'id'},
{ heading = 'Identity', key = 'pkey'}
}
},
statusBar = UI.StatusBar { values = 'double-click to revoke trust' },
reload = function(self)
local values = {}
for k,v in pairs(Util.readTable('usr/.known_hosts') or {}) do
table.insert(values, {
id = k,
pkey = split(SHA.compute(v):sub(-20):upper()) -- Obfuscate private key for visual ident
})
end
self.grid:setValues(values)
self.grid:setIndex(1)
end,
enable = function(self)
self:reload()
UI.Tab.enable(self)
end,
eventHandler = function(self, event)
if event.type == 'grid_select' then
local hosts = Util.readTable('usr/.known_hosts')
hosts[event.selected.id] = nil
Util.writeTable('usr/.known_hosts', hosts)
self:reload()
else
return UI.Tab.eventHandler(self, event)
end
return true
end
}

View File

@@ -10,8 +10,9 @@ if multishell and multishell.getTabs then
local tab = kernel.getFocused()
if tab and not tab.noTerminate then
multishell.terminate(tab.uid)
multishell.openTab(tab.env, {
multishell.openTab({
path = tab.path,
env = tab.env,
args = tab.args,
focused = true,
})

View File

@@ -4,18 +4,29 @@
local kernel = _G.kernel
local keyboard = _G.device.keyboard
local multishell = _ENV.multishell
local os = _G.os
local term = _G.term
local function systemLog()
local routine = kernel.getCurrent()
if multishell and multishell.openTab then
local w, h = kernel.window.getSize()
kernel.window.reposition(1, 2, w, h - 1)
routine.terminal = kernel.window
routine.window = kernel.window
term.redirect(kernel.window)
end
kernel.hook('mouse_scroll', function(_, eventData)
local dir, y = eventData[1], eventData[3]
if y > 1 then
local currentTab = kernel.getFocused()
if currentTab == routine then
if currentTab.terminal.scrollUp then
if currentTab.terminal.scrollUp and not currentTab.terminal.noAutoScroll then
if dir == -1 then
currentTab.terminal.scrollUp()
else
@@ -39,7 +50,7 @@ local function systemLog()
keyboard.removeHotkey('control-d')
end
kernel.run(_ENV, {
kernel.run({
title = 'System Log',
fn = systemLog,
noTerminate = true,

24
sys/autorun/upgraded.lua Normal file
View File

@@ -0,0 +1,24 @@
local fs = _G.fs
local function deleteIfExists(path)
if fs.exists(path) then
fs.delete(path)
print("Deleted outdated file at: "..path)
end
end
-- cleanup outdated files
deleteIfExists('sys/apps/shell')
deleteIfExists('sys/etc/app.db')
deleteIfExists('sys/extensions')
deleteIfExists('sys/network')
deleteIfExists('startup')
deleteIfExists('sys/apps/system/turtle.lua')
deleteIfExists('sys/autorun/gps.lua')
deleteIfExists('sys/autorun/gpshost.lua')
deleteIfExists('sys/apps/network/redserver.lua')
deleteIfExists('sys/apis')
deleteIfExists('sys/autorun/apps.lua')
deleteIfExists('sys/init/6.tl3.lua')
-- remove this file
-- deleteIfExists('sys/autorun/upgraded.lua')

View File

@@ -1,53 +0,0 @@
local Config = require('opus.config')
local Util = require('opus.util')
local fs = _G.fs
local shell = _ENV.shell
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')
local date = f.readLine()
f.close()
date = type(date) == 'string' and Util.split(date)[1]
local today = os.date('%j')
local config = Config.load('version', {
packages = date,
checked = today,
})
-- check if packages need an update
if date ~= config.packages then
config.packages = date
Config.update('version', config)
print('Updating packages')
shell.run('package updateall')
os.reboot()
end
if type(date) == 'string' and #date > 0 then
if config.checked ~= today then
config.checked = today
Config.update('version', config)
print('Checking for new version')
pcall(function()
local c = Util.httpGet(string.format(URL, _G.OPUS_BRANCH))
if c then
local lines = Util.split(c)
local revdate = table.remove(lines, 1)
if date ~= revdate and config.skip ~= revdate then
config.current = revdate
config.details = table.concat(lines, '\n')
Config.update('version', config)
print('New version available')
if _ENV.multishell then
shell.openForegroundTab('sys/apps/Version.lua')
end
end
end
end)
end
end
end

View File

@@ -21,7 +21,7 @@ if mon then
parallel.waitForAny(
function()
os.run(_ENV, '/sys/boot/opus.lua')
os.run(_ENV, '/sys/boot/opus.boot')
end,
function()
@@ -36,5 +36,5 @@ if mon then
end
)
else
os.run(_ENV, '/sys/boot/opus.lua')
os.run(_ENV, '/sys/boot/opus.boot')
end

36
sys/boot/opus.boot Normal file
View File

@@ -0,0 +1,36 @@
local fs = _G.fs
local sandboxEnv = setmetatable({ }, { __index = _G })
for k,v in pairs(_ENV) do
sandboxEnv[k] = v
end
local function run(file, ...)
local env = setmetatable({ }, { __index = _G })
for k,v in pairs(sandboxEnv) do
env[k] = v
end
local s, m = loadfile(file, env)
if s then
return s(...)
end
error('Error loading ' .. file .. '\n' .. m)
end
_G._syslog = function() end
_G.OPUS_BRANCH = 'develop-1.8'
-- Install require shim
_G.requireInjector = run('sys/modules/opus/injector.lua')
local s, m = pcall(run, 'sys/apps/shell.lua', 'sys/kernel.lua', ...)
if not s then
print('\nError loading Opus OS\n')
_G.printError(m .. '\n')
end
if fs.restore then
fs.restore()
end

View File

@@ -1,58 +0,0 @@
local fs = _G.fs
-- override bios function to use the global scope of the current env
function _G.loadstring(string, chunkname)
return load(string, chunkname, nil, getfenv(2)._G)
end
-- override bios function to include the actual filename
function _G.loadfile(filename, mode, env)
-- Support the previous `loadfile(filename, env)` form instead.
if type(mode) == "table" and env == nil then
mode, env = nil, mode
end
local file = fs.open(filename, "r")
if not file then return nil, "File not found" end
local func, err = load(file.readAll(), '@' .. filename, mode, env)
file.close()
return func, err
end
local sandboxEnv = setmetatable({ }, { __index = _G })
for k,v in pairs(_ENV) do
sandboxEnv[k] = v
end
-- Install require shim
_G.requireInjector = loadfile('sys/modules/opus/injector.lua', _ENV)()
local function run(file, ...)
local env = setmetatable({ }, { __index = _G })
for k,v in pairs(sandboxEnv) do
env[k] = v
end
_G.requireInjector(env)
local s, m = loadfile(file, env)
if s then
return s(...)
end
error('Error loading ' .. file .. '\n' .. m)
end
_G._syslog = function() end
_G.OPUS_BRANCH = 'develop-1.8'
local s, m = pcall(run, 'sys/apps/shell.lua', 'sys/kernel.lua', ...)
if not s then
print('\nError loading Opus OS\n')
_G.printError(m .. '\n')
end
if fs.restore then
fs.restore()
end

15
sys/boot/tlco.boot Normal file
View File

@@ -0,0 +1,15 @@
local pullEvent = os.pullEventRaw
local shutdown = os.shutdown
os.pullEventRaw = function()
error('')
end
os.shutdown = function()
os.pullEventRaw = pullEvent
os.shutdown = shutdown
os.run(getfenv(1), 'sys/boot/opus.boot')
end
os.queueEvent('modem_message')

View File

@@ -1,30 +0,0 @@
local run = os.run
local shutdown = os.shutdown
local args = {...} -- keep the args so that they can be passed to opus.lua
os.run = function()
os.run = run
end
os.shutdown = function()
os.shutdown = shutdown
_ENV.multishell = nil -- prevent sys/apps/shell.lua erroring for odd reasons
local success, err = pcall(function()
run(_ENV, 'sys/boot/opus.lua', table.unpack(args))
end)
term.redirect(term.native())
if success then
print("Opus OS abruptly stopped.")
else
printError("Opus OS errored.")
printError(err)
end
print("Press any key to continue.")
os.pullEvent("key")
shutdown()
end
shell.exit()

View File

@@ -136,10 +136,4 @@
iconExt = "\030 \031 \128\0307\143\131\131\131\131\143\030 \128\010\0307\031 \129\0317\128\0319\136\0309\031b\136\132\0307\0319\132\0317\128\031 \130\010\030 \0317\130\143\0307\128\128\128\128\030 \143\129",
run = "/rom/programs/fun/dj",
},
[ "4dbdd221e957eff27cc47796f3ed8447290f71c7ad8b95e5bd828b31c1858f15" ] = {
title = "Partition",
category = "System",
iconExt = "\30\55\31\55\128\30\48\135\131\139\30\55\128\128\128\10\30\48\31\55\149\31\48\128\30\55\145\30\48\31\56\140\30\55\157\144\144\10\30\55\31\55\128\31\48\139\143\135\31\55\128\31\56\142\133",
run = "Partition",
}
}

View File

@@ -1,5 +1,6 @@
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/cloud.lua urlfs https://cloud-catcher.squiddev.cc/cloud.lua
sys/apps/nfttrans.lua urlfs https://pastebin.com/raw/e8XrzeDY
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

@@ -1,9 +0,0 @@
Wireless Networking
===================
To establish one-way trust between two computers with modems, run the following in a shell prompt:
On the target computer, run:
> password
On the source computer, run:
> trust <target computer ID>

View File

@@ -6,7 +6,17 @@ Shortcut Keys
* Control-d: Show/toggle logging screen
* Control-c: Copy (in most applications)
* Control-shift-v: Paste from internal clipboard
* Control-shift-click: Open the clicked UI element in Lua
* Control-shift-doubleclick: Open in Lua the clicked UI element
Wireless Networking
===================
To establish one-way trust between two computers with modems, run the following in a shell prompt:
On the target computer, run:
> password
On the source computer, run:
> trust <target computer ID>
Running Custom Programs
=======================

View File

@@ -0,0 +1,30 @@
Opus applications are grouped into packages with a common theme.
To install a package, use either the System -> Packages program or the package command line program.
Shell usage:
> package list
> package install <name>
> package update <name>
> package uninstall <name>
Package definitions are located in usr/apps/packages. This file can be modified to add custom packages.
Current stable packages
=======================
* core
Programming and miscellaneous applications. Also contains drivers needed for other packages.
* builder
A program for creating structures from schematic files using a turtle (requires core).
* farms
Various programs for farming resources (wood, crops, animals).
* milo
An A/E like storage implementation (requires core).
* miners
Mining programs.

View File

@@ -1,13 +0,0 @@
Opus applications are grouped into packages with a common theme.
To install a package, use either the System -> Packages program or the package command line program.
Shell usage:
> package list
> package install <name>
> package update <name>
> package updateall
> package uninstall <name>
Package definitions are located in usr/config/packages. This file can be modified to add custom packages.

View File

@@ -19,10 +19,6 @@ for k,fn in pairs(fs) do
end
end
function nativefs.resolve(_, dir)
return dir
end
function nativefs.list(node, dir)
local files
if fs.native.isDir(dir) then
@@ -83,18 +79,6 @@ function nativefs.isDir(node, dir)
return fs.native.isDir(dir)
end
function nativefs.attributes(node, path)
if node.mountPoint == path then
return {
created = node.created or os.epoch('utc'),
modification = node.modification or os.epoch('utc'),
isDir = not not node.nodes,
size = node.size or 0,
}
end
return fs.native.attributes(path)
end
function nativefs.exists(node, dir)
if node.mountPoint == dir then
return true
@@ -150,10 +134,8 @@ local function getNode(dir)
return node
end
fs.getNode = getNode
local methods = { 'delete', 'getFreeSpace', 'exists', 'isDir', 'getSize',
'isReadOnly', 'makeDir', 'getDrive', 'list', 'open', 'attributes' }
'isReadOnly', 'makeDir', 'getDrive', 'list', 'open' }
for _,m in pairs(methods) do
fs[m] = function(dir, ...)
@@ -163,12 +145,6 @@ for _,m in pairs(methods) do
end
end
-- if a link, return the source for this link
function fs.resolve(dir)
local n = getNode(dir)
return n.fs.resolve and n.fs.resolve(n, dir) or dir
end
function fs.complete(partial, dir, includeFiles, includeSlash)
dir = fs.combine(dir, '')
local node = getNode(dir)
@@ -178,13 +154,6 @@ function fs.complete(partial, dir, includeFiles, includeSlash)
return fs.native.complete(partial, dir, includeFiles, includeSlash)
end
local displayFlags = {
urlfs = 'U',
linkfs = 'L',
ramfs = 'T',
netfs = 'N',
}
function fs.listEx(dir)
dir = fs.combine(dir, '')
local node = getNode(dir)
@@ -195,22 +164,20 @@ function fs.listEx(dir)
local t = { }
local files = node.fs.list(node, dir)
for _,f in ipairs(files) do
pcall(function()
pcall(function()
for _,f in ipairs(files) do
local fullName = fs.combine(dir, f)
local n = fs.getNode(fullName)
local file = {
name = f,
isDir = fs.isDir(fullName),
isReadOnly = fs.isReadOnly(fullName),
fstype = n.mountPoint == fullName and displayFlags[n.fstype],
}
if not file.isDir then
file.size = fs.getSize(fullName)
end
table.insert(t, file)
end)
end
end
end)
return t
end
@@ -235,12 +202,12 @@ function fs.copy(s, t)
end
else
local sf = Util.readFile(s, 'rb')
local sf = Util.readFile(s)
if not sf then
error('No such file')
end
Util.writeFile(t, sf, 'wb')
Util.writeFile(t, sf)
end
end
@@ -298,13 +265,6 @@ function fs.mount(path, fstype, ...)
if not vfs then
error('Invalid file system type')
end
-- get the mount point for the path
-- ie. if packages is mapped to disk/packages
-- and a request to mount /packages/foo
-- then use disk/packages/foo as the mountPoint
path = fs.resolve(path)
local node = vfs.mount(path, ...)
if node then
local parts = splitpath(path)
@@ -319,16 +279,12 @@ function fs.mount(path, fstype, ...)
tp.nodes[d] = Util.shallowCopy(tp)
tp.nodes[d].nodes = { }
tp.nodes[d].mountPoint = fs.combine(tp.mountPoint, d)
tp.nodes[d].created = os.epoch('utc')
tp.nodes[d].modification = os.epoch('utc')
end
tp = tp.nodes[d]
end
node.fs = vfs
node.fstype = fstype
node.created = node.created or os.epoch('utc')
node.modification = node.modification or os.epoch('utc')
if not targetName then
node.mountPoint = ''
fs.nodes = node
@@ -394,4 +350,4 @@ function fs.restore()
local native = fs.native
Util.clear(fs)
Util.merge(fs, native)
end
end

View File

@@ -1,46 +1,3 @@
local fs = _G.fs
local os = _G.os
local fs = _G.fs
fs.loadTab('sys/etc/fstab')
-- add some Lua compatibility functions
function os.remove(a)
if fs.exists(a) then
local s = pcall(fs.delete, a)
return s and true or nil, a .. ': Unable to remove file'
end
return nil, a .. ': No such file or directory'
end
os.execute = function(cmd)
local env = _G.getfenv(2)
if not cmd then
return env.shell and 1 or 0
end
if not env.shell then
return 0
end
local s, m = env.shell.run('sys/apps/shell.lua ' .. cmd)
if not s then
return 1, m
end
return 0
end
os.tmpname = function()
local fname
repeat
fname = 'tmp/a' .. math.random(1, 32768)
until not fs.exists(fname)
return fname
end
-- non-standard - will raise error instead
os.exit = function(code)
error(code or 0)
end

View File

@@ -10,13 +10,6 @@ if not fs.exists('usr/autorun') then
fs.makeDir('usr/autorun')
end
-- move the fstab out of config so that the config directory
-- can be remapped to another disk (and for consistency)
if fs.exists('usr/config/fstab') and not fs.exists('usr/etc/fstab') then
fs.move('usr/config/fstab', 'usr/etc/fstab')
end
fs.loadTab('usr/etc/fstab')
-- TODO: Temporary
local upgrade = Util.readTable('usr/config/shell')
if upgrade and (not upgrade.upgraded or upgrade.upgraded ~= 1) then
@@ -52,3 +45,5 @@ shell.setPath(table.concat(path, ':'))
--_G.LUA_PATH = config.lua_path
--_G.settings.set('mbs.shell.require_path', config.lua_path)
fs.loadTab('usr/config/fstab')

View File

@@ -15,7 +15,7 @@ do
end
local function startNetwork()
kernel.run(_ENV, {
kernel.run({
title = 'Net daemon',
path = 'sys/apps/netdaemon.lua',
hidden = true,

View File

@@ -1,31 +0,0 @@
local LZW = require('opus.compress.lzw')
local Tar = require('opus.compress.tar')
local Util = require('opus.util')
local fs = _G.fs
if not fs.exists('packages') or not fs.isDir('packages') then
return
end
for _, name in pairs(fs.list('packages')) do
local fullName = fs.combine('packages', name)
local packageName = name:match('(.+)%.tar%.lzw$')
if packageName and not fs.isDir(fullName) then
local dir = fs.combine('packages', packageName)
if not fs.exists(dir) then
local s, m = pcall(function()
fs.mount(dir, 'ramfs', 'directory')
local c = Util.readFile(fullName, 'rb')
Tar.untar_string(LZW.decompress(c), dir)
end)
if not s then
fs.delete(dir)
print('failed to extract ' .. fullName)
print(m)
end
end
end
end

View File

@@ -13,14 +13,8 @@ table.insert(helpPaths, '/sys/help')
for name in pairs(Packages:installed()) do
local packageDir = fs.combine('packages', name)
local fstabPath = fs.combine(packageDir, 'etc/fstab')
if fs.exists(fstabPath) then
fs.loadTab(fstabPath)
end
table.insert(appPaths, 1, '/' .. packageDir)
local apiPath = fs.combine(packageDir, 'apis') -- TODO: rename dir to 'modules' (someday)
local apiPath = fs.combine(packageDir, 'apis')
if fs.exists(apiPath) then
fs.mount(fs.combine('rom/modules/main', name), 'linkfs', apiPath)
end
@@ -29,6 +23,11 @@ for name in pairs(Packages:installed()) do
if fs.exists(helpPath) then
table.insert(helpPaths, helpPath)
end
local fstabPath = fs.combine(packageDir, 'etc/fstab')
if fs.exists(fstabPath) then
fs.loadTab(fstabPath)
end
end
help.setPath(table.concat(helpPaths, ':'))

View File

@@ -1,5 +1,6 @@
local Blit = require('opus.ui.blit')
local Config = require('opus.config')
local trace = require('opus.trace')
local Util = require('opus.util')
local colors = _G.colors
@@ -8,18 +9,19 @@ local kernel = _G.kernel
local keys = _G.keys
local os = _G.os
local printError = _G.printError
local shell = _ENV.shell
local window = _G.window
local parentTerm = _G.device.terminal
local w,h = parentTerm.getSize()
local overviewId
local tabsDirty = false
local closeInd = Util.supportsExtChars() and '\215' or '*'
local closeInd = Util.getVersion() >= 1.76 and '\215' or '*'
local multishell = { }
_ENV.multishell = multishell
shell.setEnv('multishell', multishell)
kernel.window.reposition(1, 2, w, h - 1)
multishell.term = parentTerm --deprecated use device.terminal
local config = {
standard = {
@@ -92,70 +94,57 @@ function multishell.getTabs()
return kernel.routines
end
function multishell.launch(env, path, ...)
function multishell.launch( tProgramEnv, sProgramPath, ... )
-- backwards compatibility
return multishell.openTab(env, {
path = path,
return multishell.openTab({
env = tProgramEnv,
path = sProgramPath,
args = { ... },
})
end
local function chain(orig, fn)
if not orig then
return fn
local function xprun(env, path, ...)
setmetatable(env, { __index = _G })
local fn, m = loadfile(path, env)
if fn then
return trace(fn, ...)
end
if type(orig) == 'table' then
table.insert(orig, fn)
return orig
end
return setmetatable({ orig, fn }, {
__call = function(self, ...)
for _,v in pairs(self) do
v(...)
end
end
})
return fn, m
end
function multishell.openTab(env, tab)
function multishell.openTab(tab)
if not tab.title and tab.path then
tab.title = fs.getName(tab.path):match('([^%.]+)')
end
tab.title = tab.title or 'untitled'
tab.window = tab.window or window.create(parentTerm, 1, 2, w, h - 1, false)
-- require('opus.terminal').window(parentTerm, 1, 2, w, h - 1, false)
tab.onExit = chain(tab.onExit, function(self, result, err, stack)
if not result and err and err ~= 'Terminated' then
self.terminal.setTextColor(colors.white)
self.terminal.setCursorBlink(false)
print('\nThe program terminated with an error.\n')
tab.terminal = tab.terminal or tab.window
local routine = kernel.newRoutine(tab)
routine.co = coroutine.create(function()
local result, err
if tab.fn then
result, err = Util.runFunction(routine.env, tab.fn, table.unpack(tab.args or { } ))
elseif tab.path then
result, err = xprun(routine.env, tab.path, table.unpack(tab.args or { } ))
else
err = 'multishell: invalid tab'
end
if not result and err and err ~= 'Terminated' or (err and err ~= 0) then
tab.terminal.setBackgroundColor(colors.black)
if tonumber(err) then
printError('Process exited with error code: ' .. err)
tab.terminal.setTextColor(colors.orange)
print('Process exited with error code: ' .. err)
elseif err then
printError(tostring(err))
end
if type(stack) == 'table' and #stack > 0 then
local _, cy = self.terminal.getCursorPos()
local _, th = self.terminal.getSize()
self.terminal.setTextColor(colors.white)
if cy < th - 4 then
print('\nstack traceback:')
for _, v in ipairs(stack or { }) do
_, cy = self.terminal.getCursorPos()
if cy > th - 3 then
print(' ...')
break
end
print(v)
end
end
end
self.terminal.setTextColor(parentTerm.isColor() and colors.yellow or colors.white)
_G.write('\nPress enter to close')
self.isDead = true
self.hidden = false
tab.terminal.setTextColor(colors.white)
print('\nPress enter to close')
routine.isDead = true
routine.hidden = false
redrawMenu()
while true do
local e, code = os.pullEventRaw('key')
@@ -166,17 +155,14 @@ function multishell.openTab(env, tab)
end
end)
local routine, message = kernel.run(env, tab)
kernel.launch(routine)
if routine then
if tab.focused then
multishell.setFocus(routine.uid)
else
redrawMenu()
end
if tab.focused then
multishell.setFocus(routine.uid)
else
redrawMenu()
end
return routine and routine.uid, message
return routine.uid
end
function multishell.hideTab(tabId)
@@ -270,10 +256,9 @@ kernel.hook('multishell_redraw', function()
if currentTab then
if currentTab.sx then
local textColor = currentTab.isDead and _colors.errorColor or _colors.focusTextColor
blit:write(currentTab.sx - 1,
' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ',
_colors.focusBackgroundColor, textColor)
_colors.focusBackgroundColor, _colors.focusTextColor)
end
if not currentTab.noTerminate then
blit:write(w, closeInd, nil, _colors.focusTextColor)
@@ -311,65 +296,52 @@ kernel.hook('term_resize', function(_, eventData)
end)
kernel.hook('mouse_click', function(_, eventData)
if not eventData[4] then
local x, y = eventData[2], eventData[3]
local x, y = eventData[2], eventData[3]
if y == 1 then
if x == 1 then
multishell.setFocus(overviewId)
elseif x == w then
local currentTab = kernel.getFocused()
if currentTab then
multishell.terminate(currentTab.uid)
end
else
for _,tab in pairs(kernel.routines) do
if not tab.hidden and tab.sx then
if x >= tab.sx and x <= tab.ex then
multishell.setFocus(tab.uid)
break
end
if y == 1 then
if x == 1 then
multishell.setFocus(overviewId)
elseif x == w then
local currentTab = kernel.getFocused()
if currentTab then
multishell.terminate(currentTab.uid)
end
else
for _,tab in pairs(kernel.routines) do
if not tab.hidden and tab.sx then
if x >= tab.sx and x <= tab.ex then
multishell.setFocus(tab.uid)
break
end
end
end
return true
end
eventData[3] = eventData[3] - 1
return true
end
eventData[3] = eventData[3] - 1
end)
kernel.hook({ 'mouse_up', 'mouse_drag' }, function(_, eventData)
if not eventData[4] then
eventData[3] = eventData[3] - 1
end
eventData[3] = eventData[3] - 1
end)
kernel.hook('mouse_scroll', function(_, eventData)
if not eventData[4] then
if eventData[3] == 1 then
return true
end
eventData[3] = eventData[3] - 1
if eventData[3] == 1 then
return true
end
eventData[3] = eventData[3] - 1
end)
kernel.hook('kernel_ready', function()
overviewId = multishell.openTab(_ENV, {
path = 'sys/apps/shell.lua',
args = { config.launcher or 'sys/apps/Overview.lua' },
overviewId = multishell.openTab({
path = config.launcher or 'sys/apps/Overview.lua',
isOverview = true,
noTerminate = true,
focused = true,
title = '+',
onExit = function(_, s, m)
if not s then
kernel.halt(s, m)
end
end,
})
multishell.setTitle(overviewId, '+')
multishell.openTab(_ENV, {
multishell.openTab({
path = 'sys/apps/shell.lua',
args = { 'sys/apps/autorun.lua' },
title = 'Autorun',

View File

@@ -1,6 +1,5 @@
local Array = require('opus.array')
local Terminal = require('opus.terminal')
local trace = require('opus.trace')
local Util = require('opus.util')
_G.kernel = {
@@ -20,7 +19,7 @@ local w, h = term.getSize()
kernel.terminal = term.current()
kernel.window = Terminal.window(kernel.terminal, 1, 1, w, h, false)
kernel.window.setMaxScroll(200)
kernel.window.setMaxScroll(100)
local focusedRoutineEvents = Util.transpose {
'char', 'key', 'key_up',
@@ -29,8 +28,10 @@ local focusedRoutineEvents = Util.transpose {
}
_G._syslog = function(pattern, ...)
local oldTerm = term.redirect(kernel.window)
kernel.window.scrollBottom()
kernel.window.print(Util.tostring(pattern, ...))
Util.print(pattern, ...)
term.redirect(oldTerm)
end
-- any function that runs in a kernel hook does not run in
@@ -49,23 +50,19 @@ function kernel.hook(event, fn)
end
end
-- you *should* only unhook from within the function that hooked
-- you can only unhook from within the function that hooked
function kernel.unhook(event, fn)
if type(event) == 'table' then
for _,v in pairs(event) do
kernel.unhook(v, fn)
end
else
local eventHooks = kernel.hooks[event]
if eventHooks then
Array.removeByValue(eventHooks, fn)
if #eventHooks == 0 then
kernel.hooks[event] = nil
end
local eventHooks = kernel.hooks[event]
if eventHooks then
Array.removeByValue(eventHooks, fn)
if #eventHooks == 0 then
kernel.hooks[event] = nil
end
end
end
local Routine = { }
local function switch(routine, previous)
if routine then
if previous and previous.window then
@@ -83,8 +80,6 @@ local function switch(routine, previous)
end
end
local Routine = { }
function Routine:resume(event, ...)
if not self.co or coroutine.status(self.co) == 'dead' then
return
@@ -94,64 +89,35 @@ function Routine:resume(event, ...)
local previousTerm = term.redirect(self.terminal)
local previous = kernel.running
kernel.running = self
kernel.running = self -- stupid shell set title
local ok, result = coroutine.resume(self.co, event, ...)
kernel.running = previous
self.filter = result
if ok then
self.filter = result
else
_G.printError(result)
end
self.terminal = term.current()
term.redirect(previousTerm)
if not ok and self.haltOnError then
error(result, -1)
end
if coroutine.status(self.co) == 'dead' then
Array.removeByValue(kernel.routines, self)
if #kernel.routines > 0 then
switch(kernel.routines[1])
end
if self.haltOnExit then
kernel.halt()
end
end
return ok, result
end
end
function Routine:run()
self.co = self.co or coroutine.create(function()
local result, err, fn, stack
if self.fn then
fn = self.fn
_G.setfenv(fn, self.env)
elseif self.path then
fn, err = loadfile(self.path, self.env)
elseif self.chunk then
fn, err = load(self.chunk, self.title, nil, self.env)
end
if fn then
result, err, stack = trace(fn, table.unpack(self.args or { } ))
else
err = err or 'kernel: invalid routine'
end
pcall(self.onExit, self, result, err, stack)
self:cleanup()
if not result then
error(err)
end
end)
table.insert(kernel.routines, self)
return self:resume()
end
-- override if any post processing is required
function Routine:onExit(status, message) -- self, status, message
if not status and message ~= 'Terminated' then
_G.printError(message)
end
end
function Routine:cleanup()
Array.removeByValue(kernel.routines, self)
if #kernel.routines > 0 then
switch(kernel.routines[1])
end
end
function kernel.getFocused()
return kernel.routines[1]
end
@@ -164,34 +130,51 @@ function kernel.getShell()
return shell
end
-- each routine inherits the parent's env
function kernel.makeEnv(env, dir)
env = setmetatable(Util.shallowCopy(env or _ENV), { __index = _G })
_G.requireInjector(env, dir)
return env
end
function kernel.newRoutine(env, args)
function kernel.newRoutine(args)
kernel.UID = kernel.UID + 1
local routine = setmetatable({
uid = kernel.UID,
timestamp = os.clock(),
terminal = kernel.window,
window = kernel.window,
title = 'untitled',
}, { __index = Routine })
Util.merge(routine, args)
routine.env = args.env or kernel.makeEnv(env, routine.path and fs.getDir(routine.path))
routine.terminal = routine.terminal or routine.window
routine.env = args.env or Util.shallowCopy(shell.getEnv())
return routine
end
function kernel.run(env, args)
local routine = kernel.newRoutine(env, args)
local s, m = routine:run()
return s and routine, m
function kernel.launch(routine)
routine.co = routine.co or coroutine.create(function()
local result, err
if routine.fn then
result, err = Util.runFunction(routine.env, routine.fn, table.unpack(routine.args or { } ))
elseif routine.path then
result, err = Util.run(routine.env, routine.path, table.unpack(routine.args or { } ))
else
err = 'kernel: invalid routine'
end
if not result and err ~= 'Terminated' then
error(err or 'Error occurred', 2)
end
end)
table.insert(kernel.routines, routine)
local s, m = routine:resume()
return not s and s or routine.uid, m
end
function kernel.run(args)
local routine = kernel.newRoutine(args)
kernel.launch(routine)
return routine
end
function kernel.raise(uid)
@@ -209,6 +192,8 @@ function kernel.raise(uid)
end
switch(routine, previous)
-- local previous = eventData[2]
-- local routine = kernel.find(previous)
return true
end
return false
@@ -236,8 +221,8 @@ function kernel.find(uid)
return Util.find(kernel.routines, 'uid', uid)
end
function kernel.halt(status, message)
os.queueEvent('kernel_halt', status, message)
function kernel.halt()
os.queueEvent('kernel_halt')
end
function kernel.event(event, eventData)
@@ -279,24 +264,19 @@ function kernel.event(event, eventData)
end
function kernel.start()
local s, m
local s2, m2 = pcall(function()
local s, m = pcall(function()
repeat
local eventData = { os.pullEventRaw() }
local event = table.remove(eventData, 1)
kernel.event(event, eventData)
if event == 'kernel_halt' then
s = eventData[1]
m = eventData[2]
end
until event == 'kernel_halt'
end)
if (not s and m) or (not s2 and m2) then
if not s then
kernel.window.setVisible(true)
term.redirect(kernel.window)
print('\nCrash detected\n')
_G.printError(m or m2)
_G.printError(m)
end
term.redirect(kernel.terminal)
end
@@ -313,10 +293,9 @@ local function init(...)
for _,file in ipairs(files) do
local level = file:match('(%d).%S+.lua') or 99
if tonumber(level) <= runLevel then
-- All init programs run under the original shell
local s, m = shell.run(fs.combine(dir, file))
if not s then
error(m, -1)
error(m)
end
os.sleep(0)
end
@@ -330,15 +309,15 @@ local function init(...)
term.redirect(kernel.window)
shell.run('sys/apps/autorun.lua')
local win = window.create(kernel.terminal, 1, 1, w, h, true)
local s, m = kernel.run(_ENV, {
local shellWindow = window.create(kernel.terminal, 1, 1, w, h, false)
local s, m = kernel.run({
title = args[1],
path = 'sys/apps/shell.lua',
args = args,
window = win,
onExit = function(_, s, m)
kernel.halt(s, m)
end,
haltOnExit = true,
haltOnError = true,
terminal = shellWindow,
window = shellWindow,
})
if s then
kernel.raise(s.uid)
@@ -349,15 +328,11 @@ local function init(...)
end
end
kernel.run(_ENV, {
kernel.run({
fn = init,
title = 'init',
haltOnError = true,
args = { ... },
onExit = function(_, status, message)
if not status then
kernel.halt(status, message)
end
end,
})
kernel.start()

View File

@@ -0,0 +1,48 @@
local Array = require('opus.array')
local Config = require('opus.config')
local Util = require('opus.util')
local function getConfig()
return Config.load('alternate', {
shell = {
'sys/apps/shell.lua',
'rom/programs/shell.lua',
},
lua = {
'sys/apps/Lua.lua',
'rom/programs/lua.lua',
},
files = {
'sys/apps/Files.lua',
}
})
end
local Alt = { }
function Alt.get(key)
return getConfig()[key][1]
end
function Alt.set(key, value)
local config = getConfig()
Array.removeByValue(config[key], value)
table.insert(config[key], 1, value)
Config.update('alternate', config)
end
function Alt.remove(key, value)
local config = getConfig()
Array.removeByValue(config[key], value)
Config.update('alternate', config)
end
function Alt.add(key, value)
local config = getConfig()
if not Util.contains(config[key], value) then
table.insert(config[key], value)
Config.update('alternate', config)
end
end
return Alt

View File

@@ -1,142 +0,0 @@
-- see: https://github.com/Rochet2/lualzw
-- MIT License - Copyright (c) 2016 Rochet2
local char = string.char
local type = type
local sub = string.sub
local tconcat = table.concat
local SIGC = 'LZWC'
local basedictcompress = {}
local basedictdecompress = {}
for i = 0, 255 do
local ic, iic = char(i), char(i, 0)
basedictcompress[ic] = iic
basedictdecompress[iic] = ic
end
local function dictAddA(str, dict, a, b)
if a >= 256 then
a, b = 0, b+1
if b >= 256 then
dict = {}
b = 1
end
end
dict[str] = char(a,b)
a = a+1
return dict, a, b
end
local function compress(input)
if type(input) ~= "string" then
error ("string expected, got "..type(input))
end
local len = #input
if len <= 1 then
return input
end
local dict = {}
local a, b = 0, 1
local result = { SIGC }
local resultlen = 1
local n = 2
local word = ""
for i = 1, len do
local c = sub(input, i, i)
local wc = word..c
if not (basedictcompress[wc] or dict[wc]) then
local write = basedictcompress[word] or dict[word]
if not write then
error "algorithm error, could not fetch word"
end
result[n] = write
resultlen = resultlen + #write
n = n+1
if len <= resultlen then
return input
end
dict, a, b = dictAddA(wc, dict, a, b)
word = c
else
word = wc
end
end
result[n] = basedictcompress[word] or dict[word]
resultlen = resultlen+#result[n]
if len <= resultlen then
return input
end
return tconcat(result)
end
local function dictAddB(str, dict, a, b)
if a >= 256 then
a, b = 0, b+1
if b >= 256 then
dict = {}
b = 1
end
end
dict[char(a,b)] = str
a = a+1
return dict, a, b
end
local function decompress(input)
if type(input) ~= "string" then
error( "string expected, got "..type(input))
end
if #input < 4 then
return input
end
local control = sub(input, 1, 4)
if control ~= SIGC then
return input
end
input = sub(input, 5)
local len = #input
if len < 2 then
error("invalid input - not a compressed string")
end
local dict = {}
local a, b = 0, 1
local result = {}
local n = 1
local last = sub(input, 1, 2)
result[n] = basedictdecompress[last] or dict[last]
n = n+1
for i = 3, len, 2 do
local code = sub(input, i, i+1)
local lastStr = basedictdecompress[last] or dict[last]
if not lastStr then
error( "could not find last from dict. Invalid input?")
end
local toAdd = basedictdecompress[code] or dict[code]
if toAdd then
result[n] = toAdd
n = n+1
dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b)
else
local tmp = lastStr..sub(lastStr, 1, 1)
result[n] = tmp
n = n+1
dict, a, b = dictAddB(tmp, dict, a, b)
end
last = code
end
return tconcat(result)
end
return {
compress = compress,
decompress = decompress,
}

View File

@@ -1,270 +0,0 @@
-- see: https://github.com/luarocks/luarocks/blob/master/src/luarocks/tools/tar.lua
-- A pure-Lua implementation of untar (unpacking .tar archives)
local Util = require('opus.util')
local fs = _G.fs
local _sub = string.sub
local blocksize = 512
local function get_typeflag(flag)
if flag == "0" or flag == "\0" then return "file"
elseif flag == "1" then return "link"
elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU
elseif flag == "3" then return "character"
elseif flag == "4" then return "block"
elseif flag == "5" then return "directory"
elseif flag == "6" then return "fifo"
elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU
elseif flag == "x" then return "next file"
elseif flag == "g" then return "global extended header"
elseif flag == "L" then return "long name"
elseif flag == "K" then return "long link name"
end
return "unknown"
end
local function octal_to_number(octal)
local exp = 0
local number = 0
octal = octal:gsub("%s", "")
for i = #octal,1,-1 do
local digit = tonumber(octal:sub(i,i))
if not digit then
break
end
number = number + (digit * 8^exp)
exp = exp + 1
end
return number
end
local function checksum_header(block)
local sum = 256
for i = 1,148 do
local b = block:byte(i) or 0
sum = sum + b
end
for i = 157,500 do
local b = block:byte(i) or 0
sum = sum + b
end
return sum
end
local function nullterm(s)
return s:match("^[^%z]*")
end
local function read_header_block(block)
local header = {}
header.name = nullterm(block:sub(1,100))
header.mode = nullterm(block:sub(101,108)):gsub(" ", "")
header.uid = octal_to_number(nullterm(block:sub(109,116)))
header.gid = octal_to_number(nullterm(block:sub(117,124)))
header.size = octal_to_number(nullterm(block:sub(125,136)))
header.mtime = octal_to_number(nullterm(block:sub(137,148)))
header.chksum = octal_to_number(nullterm(block:sub(149,156)))
header.typeflag = get_typeflag(block:sub(157,157))
header.linkname = nullterm(block:sub(158,257))
header.magic = block:sub(258,263)
header.version = block:sub(264,265)
header.uname = nullterm(block:sub(266,297))
header.gname = nullterm(block:sub(298,329))
header.devmajor = octal_to_number(nullterm(block:sub(330,337)))
header.devminor = octal_to_number(nullterm(block:sub(338,345)))
header.prefix = block:sub(346,500)
if not checksum_header(block) == header.chksum then
return false, "Failed header checksum"
end
return header
end
local function untar_stream(tar_handle, destdir, verbose)
assert(type(destdir) == "string")
local long_name, long_link_name
local ok, err
local make_dir = function(a)
if not fs.exists(a) then
fs.makeDir(a)
end
return true
end
while true do
local block
repeat
block = tar_handle:read(blocksize)
until (not block) or checksum_header(block) > 256
if not block then break end
if #block < blocksize then
ok, err = nil, "Invalid block size -- corrupted file?"
break
end
local header
header, err = read_header_block(block)
if not header then
ok = false
break
end
local file_data = tar_handle:read(math.ceil(header.size / blocksize) * blocksize):sub(1,header.size)
if header.typeflag == "long name" then
long_name = nullterm(file_data)
elseif header.typeflag == "long link name" then
long_link_name = nullterm(file_data)
else
if long_name then
header.name = long_name
long_name = nil
end
if long_link_name then
header.name = long_link_name
long_link_name = nil
end
end
local pathname = fs.combine(destdir, header.name)
if header.typeflag == "directory" then
ok, err = make_dir(pathname)
if not ok then
break
end
elseif header.typeflag == "file" then
local dirname = fs.getDir(pathname)
if dirname ~= "" then
ok, err = make_dir(dirname)
if not ok then
break
end
end
local file_handle
if verbose then
print(pathname)
end
file_handle, err = io.open(pathname, "wb")
if not file_handle then
ok = nil
break
end
file_handle:write(file_data)
file_handle:close()
end
end
return ok, err
end
local function untar_string(str, destdir, verbose)
local ctr = 1
local len = #str
local handle = {
read = function(_, n)
if ctr < len then
local s = _sub(str, ctr, ctr + n - 1)
ctr = ctr + n
return s
end
end
}
return untar_stream(handle, destdir, verbose)
end
local function untar(filename, destdir, verbose)
assert(type(filename) == "string")
assert(type(destdir) == "string")
local tar_handle = io.open(filename, "rb")
if not tar_handle then return nil, "Error opening file "..filename end
local ok, err = untar_stream(tar_handle, destdir, verbose)
tar_handle:close()
return ok, err
end
local function create_header_block(filename, abspath)
local block = ('\0'):rep(blocksize)
local function number_to_octal(n)
return ('%o'):format(n)
end
local function ins(pos, istr)
block = block:sub(1, pos - 1) .. istr .. block:sub(pos + #istr)
end
ins(1, filename) -- header
ins(125, number_to_octal(fs.getSize(abspath)))
ins(157, '0') -- typeflag
ins(149, number_to_octal(checksum_header(block)))
return block
end
local function tar_stream(tar_handle, root, files)
if not files then
files = { }
local function recurse(rel)
local abs = fs.combine(root, rel)
for _,f in ipairs(fs.list(abs)) do
local fullName = fs.combine(abs, f)
if fs.isDir(fullName) then -- skip virtual dirs
recurse(fs.combine(rel, f))
else
table.insert(files, fs.combine(rel, f))
end
end
end
recurse('')
end
for _, file in pairs(files) do
local abs = fs.combine(root, file)
local block = create_header_block(file, abs)
tar_handle:write(block)
local f = Util.readFile(abs, 'rb')
tar_handle:write(f)
local padding = #f % blocksize
if padding > 0 then
tar_handle:write(('\0'):rep(blocksize - padding))
end
end
end
local function tar_string(root, files)
local t = { }
local handle = {
write = function(_, s)
table.insert(t, s)
end
}
tar_stream(handle, root, files)
return table.concat(t)
end
-- the bare minimum for this program to untar
local function tar(filename, root, files)
assert(type(filename) == "string")
assert(type(root) == "string")
local tar_handle = io.open(filename, "wb")
if not tar_handle then return nil, "Error opening file "..filename end
local ok, err = tar_stream(tar_handle, root, files)
tar_handle:close()
return ok, err
end
return {
tar = tar,
untar = untar,
tar_string = tar_string,
untar_string = untar_string,
}

View File

@@ -5,7 +5,7 @@ local cbor = require('opus.cbor')
local sha2 = require('opus.crypto.sha2')
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 band = bit32.band
@@ -100,7 +100,7 @@ local function crypt(data, key, nonce, cntr, round)
cntr = tonumber(cntr) or 1
round = tonumber(round) or 20
local throttle = Util.throttle()
local throttle = Util.throttle(function() _syslog('throttle') end)
local out = {}
local state = initState(key, nonce, cntr)
local blockAmt = math.floor(#data/64)

View File

@@ -47,13 +47,13 @@ function Entry:updateScroll()
self.scroll = 0 -- ??
end
if self.pos - self.scroll > self.width then
self.scroll = math.max(0, self.pos - self.width)
self.scroll = self.pos - self.width
elseif self.pos < self.scroll then
self.scroll = self.pos
end
if self.scroll > 0 then
if self.scroll + self.width > len then
self.scroll = math.max(0, len - self.width)
self.scroll = len - self.width
end
end
if ps ~= self.scroll then
@@ -222,7 +222,7 @@ function Entry:paste(ie)
end
end
function Entry.forcePaste()
function Entry:forcePaste()
os.queueEvent('clipboard_paste')
end
@@ -234,9 +234,7 @@ end
function Entry:markBegin()
if not self.mark.active then
if #_val(self.value) > 0 then
self.mark.active = true
end
self.mark.active = true
self.mark.anchor = { x = self.pos }
end
end
@@ -273,8 +271,6 @@ function Entry:markLeft()
self:markBegin()
if self:moveLeft() then
self:markFinish()
else
self.mark.continue = self.mark.active
end
end
@@ -282,8 +278,6 @@ function Entry:markRight()
self:markBegin()
if self:moveRight() then
self:markFinish()
else
self.mark.continue = self.mark.active
end
end
@@ -311,8 +305,6 @@ function Entry:markNextWord()
self:markBegin()
if self:moveWordRight() then
self:markFinish()
else
self.mark.continue = self.mark.active
end
end
@@ -320,8 +312,6 @@ function Entry:markPrevWord()
self:markBegin()
if self:moveWordLeft() then
self:markFinish()
else
self.mark.continue = self.mark.active
end
end
@@ -340,8 +330,6 @@ function Entry:markHome()
self:markBegin()
if self:moveHome() then
self:markFinish()
else
self.mark.continue = self.mark.active
end
end
@@ -349,8 +337,6 @@ function Entry:markEnd()
self:markBegin()
if self:moveEnd() then
self:markFinish()
else
self.mark.continue = self.mark.active
end
end

View File

@@ -58,14 +58,6 @@ function Routine:resume(event, ...)
else
s, m = coroutine.resume(self.co, event, ...)
end
if not s and event ~= 'terminate' then
if m and type(debug) == 'table' and debug.traceback then
local t = (debug.traceback(self.co, 1)) or ''
m = m .. '\n' .. t:match('%d\n(.+)')
end
end
if self:isDead() then
self.co = nil
self.filter = nil

View File

@@ -4,25 +4,17 @@ local linkfs = { }
-- TODO: implement broken links
local methods = { 'exists', 'getFreeSpace', 'getSize', 'attributes',
'isDir', 'isReadOnly', 'list', 'makeDir', 'open', 'getDrive' }
local methods = { 'exists', 'getFreeSpace', 'getSize',
'isDir', 'isReadOnly', 'list', 'listEx', 'makeDir', 'open', 'getDrive' }
for _,m in pairs(methods) do
linkfs[m] = function(node, dir, ...)
dir = linkfs.resolve(node, dir)
dir = dir:gsub(node.mountPoint, node.source, 1)
return fs[m](dir, ...)
end
end
function linkfs.resolve(node, dir)
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)
function linkfs.mount(_, source)
if not source then
error('Source is required')
end
@@ -30,9 +22,6 @@ function linkfs.mount(path, source)
if not fs.exists(source) then
error('Source is missing')
end
if path == source then
return
end
if fs.isDir(source) then
return {
source = source,
@@ -45,8 +34,8 @@ function linkfs.mount(path, source)
end
function linkfs.copy(node, s, t)
s = linkfs.resolve(node, s)
t = linkfs.resolve(node, t)
s = s:gsub(node.mountPoint, node.source, 1)
t = t:gsub(node.mountPoint, node.source, 1)
return fs.copy(s, t)
end
@@ -54,30 +43,26 @@ function linkfs.delete(node, dir)
if dir == node.mountPoint then
fs.unmount(node.mountPoint)
else
dir = linkfs.resolve(node, dir)
dir = dir:gsub(node.mountPoint, node.source, 1)
return fs.delete(dir)
end
end
function linkfs.find(node, spec)
spec = linkfs.resolve(node, spec)
spec = spec:gsub(node.mountPoint, node.source, 1)
local list = fs.find(spec)
local src = node.source
local mp = node.mountPoint
for k,f in ipairs(list) do
if f:sub(1, #src) == src then
list[k] = mp .. f:sub(#src + 1)
end
list[k] = f:gsub(node.source, node.mountPoint, 1)
end
return list
end
function linkfs.move(node, s, t)
s = linkfs.resolve(node, s)
t = linkfs.resolve(node, t)
s = s:gsub(node.mountPoint, node.source, 1)
t = t:gsub(node.mountPoint, node.source, 1)
return fs.move(s, t)
end
return linkfs
return linkfs

View File

@@ -6,6 +6,7 @@ local fs = _G.fs
local netfs = { }
local function remoteCommand(node, msg)
for _ = 1, 2 do
if not node.socket then
node.socket = Socket.connect(node.id, 139)
@@ -32,19 +33,17 @@ local function remoteCommand(node, msg)
error('netfs: Connection failed', 2)
end
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx', 'attributes' }
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx' }
local function resolve(node, dir)
local mp = node.mountPoint
if dir:sub(1, #mp) == mp then
dir = dir:sub(#mp + 1)
end
return fs.combine(node.source, dir)
local function resolveDir(dir, node)
-- TODO: Wrong ! (does not support names with dashes)
dir = dir:gsub(node.mountPoint, '', 1)
return fs.combine(node.directory, dir)
end
for _,m in pairs(methods) do
netfs[m] = function(node, dir)
dir = resolve(node, dir)
dir = resolveDir(dir, node)
return remoteCommand(node, {
fn = m,
@@ -53,14 +52,14 @@ for _,m in pairs(methods) do
end
end
function netfs.mount(_, id, source)
function netfs.mount(_, id, directory)
if not id or not tonumber(id) then
error('netfs syntax: computerId [directory]')
error('ramfs syntax: computerId [directory]')
end
return {
id = tonumber(id),
nodes = { },
source = source or '',
directory = directory or '',
}
end
@@ -69,7 +68,7 @@ function netfs.getDrive()
end
function netfs.complete(node, partial, dir, includeFiles, includeSlash)
dir = resolve(node, dir)
dir = resolveDir(dir, node)
return remoteCommand(node, {
fn = 'complete',
@@ -78,8 +77,8 @@ function netfs.complete(node, partial, dir, includeFiles, includeSlash)
end
function netfs.copy(node, s, t)
s = resolve(node, s)
t = resolve(node, t)
s = resolveDir(s, node)
t = resolveDir(t, node)
return remoteCommand(node, {
fn = 'copy',
@@ -88,37 +87,37 @@ function netfs.copy(node, s, t)
end
function netfs.isDir(node, dir)
if dir == node.mountPoint and node.source == '' then
if dir == node.mountPoint and node.directory == '' then
return true
end
return remoteCommand(node, {
fn = 'isDir',
args = { resolve(node, dir) },
args = { resolveDir(dir, node) },
})
end
function netfs.isReadOnly(node, dir)
if dir == node.mountPoint and node.source == '' then
if dir == node.mountPoint and node.directory == '' then
return false
end
return remoteCommand(node, {
fn = 'isReadOnly',
args = { resolve(node, dir) },
args = { resolveDir(dir, node) },
})
end
function netfs.getSize(node, dir)
if dir == node.mountPoint and node.source == '' then
if dir == node.mountPoint and node.directory == '' then
return 0
end
return remoteCommand(node, {
fn = 'getSize',
args = { resolve(node, dir) },
args = { resolveDir(dir, node) },
})
end
function netfs.find(node, spec)
spec = resolve(node, spec)
spec = resolveDir(spec, node)
local list = remoteCommand(node, {
fn = 'find',
args = { spec },
@@ -132,8 +131,8 @@ function netfs.find(node, spec)
end
function netfs.move(node, s, t)
s = resolve(node, s)
t = resolve(node, t)
s = resolveDir(s, node)
t = resolveDir(t, node)
return remoteCommand(node, {
fn = 'move',
@@ -142,7 +141,7 @@ function netfs.move(node, s, t)
end
function netfs.open(node, fn, fl)
fn = resolve(node, fn)
fn = resolveDir(fn, node)
local vfh = remoteCommand(node, {
fn = 'open',

View File

@@ -9,28 +9,15 @@ function ramfs.mount(_, nodeType)
return {
nodes = { },
size = 0,
created = os.epoch('utc'),
modification = os.epoch('utc'),
}
elseif nodeType == 'file' then
return {
size = 0,
created = os.epoch('utc'),
modification = os.epoch('utc'),
}
end
error('ramfs syntax: [directory, file]')
end
function ramfs.attributes(node)
return {
created = node.created,
isDir = not not node.nodes,
modification = node.modification,
size = node.size,
}
end
function ramfs.delete(node, dir)
if node.mountPoint == dir then
fs.unmount(node.mountPoint)
@@ -53,10 +40,8 @@ function ramfs.makeDir(_, dir)
fs.mount(dir, 'ramfs', 'directory')
end
function ramfs.isDir(node, dir)
if node.mountPoint == dir then
return not not node.nodes
end
function ramfs.isDir(node)
return not not node.nodes
end
function ramfs.getDrive()
@@ -79,70 +64,32 @@ function ramfs.list(node, dir)
end
function ramfs.open(node, fn, fl)
local modes = Util.transpose { 'r', 'w', 'rb', 'wb', 'a' }
if not modes[fl] then
if fl ~= 'r' and fl ~= 'w' and fl ~= 'rb' and fl ~= 'wb' then
error('Unsupported mode')
end
if fl == 'a' then
if node.mountPoint ~= fn then
fl = 'w'
else
local c = type(node.contents) == 'table'
and string.char(table.unpack(node.contents))
or node.contents
or ''
return {
write = function(str)
c = c .. str
end,
writeLine = function(str)
c = c .. str .. '\n'
end,
flush = function()
node.contents = c
node.size = #c
end,
close = function()
node.contents = c
node.size = #c
c = nil
end,
}
end
end
if fl == 'r' then
if node.mountPoint ~= fn then
return
end
local c = type(node.contents) == 'table'
and string.char(table.unpack(node.contents))
or node.contents
local ctr = 0
local lines
return {
read = function(n)
n = n or 1
if ctr >= node.size then
return
end
local t = c:sub(ctr + 1, ctr + n)
ctr = ctr + n
return t
read = function()
ctr = ctr + 1
return node.contents:sub(ctr, ctr)
end,
readLine = function()
if not lines then
lines = Util.split(c)
lines = Util.split(node.contents)
end
ctr = ctr + 1
return lines[ctr]
end,
readAll = function()
return c
return node.contents
end,
close = function()
lines = nil
@@ -174,30 +121,11 @@ function ramfs.open(node, fn, fl)
return
end
local c = node.contents
if type(node.contents) == 'string' then
c = { }
for i = 1, node.size do
c[i] = node.contents:sub(i, i):byte()
end
end
local ctr = 0
return {
readAll = function()
return string.char(table.unpack(c))
end,
read = function(n)
if n and n > 1 and ctr < node.size then
-- some programs open in rb, when it should have
-- been opened in r - attempt to support multiple read
-- if nils are present in data, this will fail
local t = string.char(table.unpack(c, ctr + 1, ctr + n))
ctr = ctr + n
return t
end
read = function()
ctr = ctr + 1
return c[ctr]
return node.contents[ctr]
end,
close = function()
end,
@@ -209,13 +137,7 @@ function ramfs.open(node, fn, fl)
local c = { }
return {
write = function(b)
if type(b) == 'number' then
table.insert(c, b)
else
for i = 1, #b do
table.insert(c, b:sub(i, i):byte())
end
end
table.insert(c, b)
end,
flush = function()
node.contents = c

View File

@@ -5,46 +5,29 @@ local fs = _G.fs
local urlfs = { }
function urlfs.mount(path, url, force)
function urlfs.mount(_, url)
if not url then
error('URL is required')
end
-- only mount if the file does not exist already
if not fs.exists(path) or force then
return {
url = url,
created = os.epoch('utc'),
modification = os.epoch('utc'),
}
end
end
function urlfs.attributes(node, path)
return path == node.mountPoint and {
created = node.created,
isDir = false,
modification = node.modification,
size = node.size or 0,
return {
url = url,
}
end
function urlfs.delete(node, path)
if path == node.mountPoint then
fs.unmount(path)
end
function urlfs.delete(_, dir)
fs.unmount(dir)
end
function urlfs.exists(node, path)
return path == node.mountPoint
function urlfs.exists()
return true
end
function urlfs.getSize(node, path)
return path == node.mountPoint and node.size or 0
function urlfs.getSize(node)
return node.size or 0
end
function urlfs.isReadOnly()
return false
return true
end
function urlfs.isDir()
@@ -67,6 +50,14 @@ function urlfs.open(node, fn, fl)
local c = node.cache
if not c then
--[[
if node.url:match("^(rttps?:)") then
local s, response = rttp.get(node.url)
c = s and response.statusCode == 200 and response.data
else
c = Util.httpGet(node.url)
end
]]--
c = Util.httpGet(node.url)
if c then
node.cache = c
@@ -103,9 +94,6 @@ function urlfs.open(node, fn, fl)
}
end
return {
readAll = function()
return c
end,
read = function()
ctr = ctr + 1
return c:sub(ctr, ctr):byte()

View File

@@ -1,56 +1,21 @@
local find = string.find
local floor = math.floor
local min = math.min
local max = math.max
local sub = string.sub
-- Based on Squid's fuzzy search
-- https://github.com/SquidDev-CC/artist/blob/vnext/artist/lib/match.lua
--
-- not very fuzzy anymore
-- https://rosettacode.org/wiki/Jaro_distance (ported to lua)
return function(s1, s2)
local l1, l2 = #s1, #s2;
if l1 == 0 then
return l2 == 0 and 1.0 or 0.0
local SCORE_WEIGHT = 1000
local LEADING_LETTER_PENALTY = -30
local LEADING_LETTER_PENALTY_MAX = -90
local _find = string.find
local _max = math.max
return function(str, pattern)
local start = _find(str, pattern, 1, true)
if start then
-- All letters before the current one are considered leading, so add them to our penalty
return SCORE_WEIGHT
+ _max(LEADING_LETTER_PENALTY * (start - 1), LEADING_LETTER_PENALTY_MAX)
- (#str - #pattern)
end
local match_distance = max(floor(max(l1, l2) / 2) - 1, 0)
local s1_matches = { }
local s2_matches = { }
local matches = 0
for i = 1, l1 do
local _end = min(i + match_distance + 1, l2)
for k = max(1, i - match_distance), _end do
if not s2_matches[k] and sub(s1, i, i) == sub(s2, k, k) then
s1_matches[i] = true
s2_matches[k] = true
matches = matches + 1
break
end
end
end
if matches == 0 then
return 0.0
end
local t = 0.0
local k = 1
for i = 1, l1 do
if s1_matches[i] then
while not s2_matches[k] do
k = k + 1
end
if sub(s1, i, i) ~= sub(s2, k, k) then
t = t + 0.5
end
k = k + 1
end
end
-- provide a major boost for exact matches
local b = 0.0
if find(s1, s2, 1, true) then
b = b + .5
end
local m = matches
return (m / l1 + m / l2 + (m - t) / m) / 3.0 + b
end

View File

@@ -1,91 +1,67 @@
local json = require('opus.json')
local Util = require('opus.util')
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_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 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
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")
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)
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 = fileUrlFn(v.path),
url = string.format(FILE_URL, user, repo, branch, v.path),
size = v.size,
}
elseif Util.startsWith(v.path, path) then
local p = string.sub(v.path, #path)
list[p] = {
url = fileUrlFn(path .. p),
url = string.format(FILE_URL, user, repo, branch, 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

@@ -20,12 +20,13 @@ function GPS.locate(timeout, debug)
local modem = device.wireless_modem
local closeChannel = false
if not modem.isOpen(GPS.CHANNEL_GPS) then
modem.open(GPS.CHANNEL_GPS)
local selfID = os.getComputerID()
if not modem.isOpen(selfID) then
modem.open(selfID)
closeChannel = true
end
modem.transmit(GPS.CHANNEL_GPS, GPS.CHANNEL_GPS, "PING")
modem.transmit(GPS.CHANNEL_GPS, selfID, "PING")
local fixes = {}
local pos = nil
@@ -33,7 +34,7 @@ function GPS.locate(timeout, debug)
while true do
local e, side, chan, reply, msg, dist = os.pullEvent()
if e == "modem_message" then
if side == modem.side and chan == GPS.CHANNEL_GPS and reply == GPS.CHANNEL_GPS and dist then
if side == modem.side and chan == selfID and reply == GPS.CHANNEL_GPS and dist then
if type(msg) == "table" and #msg == 3 and tonumber(msg[1]) and tonumber(msg[2]) and tonumber(msg[3]) then
local fix = {
position = vector.new(unpack(msg)),
@@ -59,7 +60,7 @@ function GPS.locate(timeout, debug)
end
if closeChannel then
modem.close(GPS.CHANNEL_GPS)
modem.close(selfID)
end
if debug then
print("Position is "..pos.x..","..pos.y..","..pos.z)

View File

@@ -1,51 +1,40 @@
-- https://www.lua.org/manual/5.1/manual.html#pdf-require
-- https://github.com/LuaDist/lua/blob/d2e7e7d4d43ff9068b279a617c5b2ca2c2771676/src/loadlib.c
local function split(str, pattern)
local t = { }
local function helper(line) table.insert(t, line) return "" end
helper((str:gsub(pattern, helper)))
return t
end
local defaultPath = { }
do
local function split(str)
local t = { }
local function helper(line) table.insert(t, line) return "" end
helper((str:gsub('(.-);', helper)))
return t
end
local function insert(p)
for _,v in pairs(defaultPath) do
if v == p then
return
end
end
table.insert(defaultPath, p)
end
local paths = '?.lua;?/init.lua;'
paths = paths .. '/usr/modules/?.lua;/usr/modules/?/init.lua;'
paths = paths .. '/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua;'
paths = paths .. '/sys/modules/?.lua;/sys/modules/?/init.lua'
for _,v in pairs(split(paths)) do
insert(v)
end
local luaPaths = package and package.path and split(package.path) or { }
for _,v in pairs(luaPaths) do
if v ~= '?' then
insert(v)
end
local hasMain
local luaPaths = package and package.path and split(package.path, '(.-);') or { }
for i = 1, #luaPaths do
if luaPaths[i] == '?' or luaPaths[i] == '?.lua' or luaPaths[i] == '?/init.lua' then
luaPaths[i] = nil
elseif string.find(luaPaths[i], '/rom/modules/main') then
hasMain = true
end
end
local DEFAULT_PATH = table.concat(defaultPath, ';')
table.insert(luaPaths, 1, '?.lua')
table.insert(luaPaths, 2, '?/init.lua')
table.insert(luaPaths, 3, '/usr/modules/?.lua')
table.insert(luaPaths, 4, '/usr/modules/?/init.lua')
if not hasMain then
table.insert(luaPaths, 5, '/rom/modules/main/?')
table.insert(luaPaths, 6, '/rom/modules/main/?.lua')
table.insert(luaPaths, 7, '/rom/modules/main/?/init.lua')
end
table.insert(luaPaths, '/sys/modules/?.lua')
table.insert(luaPaths, '/sys/modules/?/init.lua')
local DEFAULT_PATH = table.concat(luaPaths, ';')
local fs = _G.fs
local os = _G.os
local string = _G.string
-- Add require and package to the environment
return function(env, programDir)
return function(env)
local function preloadSearcher(modname)
if env.package.preload[modname] then
return function()
@@ -54,75 +43,77 @@ return function(env, programDir)
end
end
local function loadedSearcher(modname)
if env.package.loaded[modname] then
return function()
return env.package.loaded[modname]
end
end
end
local sentinel = { }
local function pathSearcher(modname)
if env.package.loaded[modname] == sentinel then
error("loop or previous error loading module '" .. modname .. "'", 0)
end
env.package.loaded[modname] = sentinel
local fname = modname:gsub('%.', '/')
for pattern in string.gmatch(env.package.path, "[^;]+") do
local sPath = string.gsub(pattern, "%?", fname)
-- TODO: if there's no shell, we should not be checking relative paths below
-- as they will resolve to root directory
if env.shell and
type(env.shell.getRunningProgram) == 'function' and
sPath:sub(1, 1) ~= "/" then
if programDir and sPath:sub(1, 1) ~= "/" then
sPath = fs.combine(programDir, sPath)
sPath = fs.combine(fs.getDir(env.shell.getRunningProgram() or ''), sPath)
end
if fs.exists(sPath) and not fs.isDir(sPath) then
return loadfile(fs.combine(sPath, ''), env)
return loadfile(sPath, env)
end
end
end
-- place package and require function into env
env.package = {
path = env.LUA_PATH or _G.LUA_PATH or DEFAULT_PATH,
cpath = '',
config = '/\n:\n?\n!\n-',
path = env.LUA_PATH or _G.LUA_PATH or DEFAULT_PATH,
config = '/\n:\n?\n!\n-',
preload = { },
loaded = {
bit32 = bit32,
loaded = {
coroutine = coroutine,
_G = env._G,
io = io,
math = math,
os = os,
string = string,
table = table,
debug = debug,
utf8 = utf8,
},
loaders = {
preloadSearcher,
loadedSearcher,
pathSearcher,
}
}
env.package.loaded.package = env.package
local sentinel = { }
function env.require(modname)
if env.package.loaded[modname] then
if env.package.loaded[modname] == sentinel then
error("loop or previous error loading module '" .. modname .. "'", 0)
end
return env.package.loaded[modname]
end
local t = { }
for _,searcher in ipairs(env.package.loaders) do
local fn, msg = searcher(modname)
if type(fn) == 'function' then
env.package.loaded[modname] = sentinel
local module = fn(modname, env) or true
if fn then
local module, msg2 = fn(modname, env)
if not module then
error(msg2 or (modname .. ' module returned nil'), 2)
end
env.package.loaded[modname] = module
return module
end
if msg then
table.insert(t, msg)
error(msg, 2)
end
end
if #t > 0 then
error(table.concat(t, '\n'), 2)
end
error('Unable to find module ' .. modname, 2)
end
return env.require -- backwards compatible
end

View File

@@ -189,22 +189,11 @@ function input:translate(event, code, p1, p2)
end
end
if not ({ ...})[1] then
local colors = _G.colors
local term = _G.term
function input:test()
while true do
local e = { os.pullEvent() }
local ch = input:translate(table.unpack(e))
local ch = self:translate(os.pullEvent())
if ch then
term.setTextColor(colors.white)
print(table.unpack(e))
term.setTextColor(colors.lime)
local t = { }
for k,v in pairs(ch) do
table.insert(t, k .. ':' .. v)
end
print('--> ' .. table.concat(t, ' ') .. '\n')
Util.print(ch)
end
end
end

View File

@@ -39,7 +39,6 @@ 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

@@ -8,7 +8,6 @@ Map.merge = Util.merge
Map.shallowCopy = Util.shallowCopy
Map.find = Util.find
Map.filter = Util.filter
Map.transpose = Util.transpose
function Map.removeMatches(t, values)
local function matchAll(entry)

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 textutils = _G.textutils
@@ -8,21 +7,6 @@ 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 = { }
@@ -71,30 +55,13 @@ function Packages:isInstalled(package)
end
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://pastebin.com/raw/pexZpAxt',
}
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)
if packages[_G.OPUS_BRANCH] then
Util.download(packages[_G.OPUS_BRANCH], 'usr/config/packages')
end
end

View File

@@ -1,38 +1,10 @@
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 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
local current = Security.getPassword()
return current and password == current
end
function Security.hasPassword()
@@ -56,16 +28,8 @@ 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 = {
hash = derived:toHex(),
salt = salt,
iter = PBKDF2_ITERATIONS,
trustKey = SHA.compute(password),
}
config.password = password
Config.update('os', config)
end
@@ -73,15 +37,4 @@ 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

@@ -6,11 +6,12 @@ local Util = require('opus.util')
local device = _G.device
local os = _G.os
local network = _G.network
local socketClass = { }
function socketClass:read(timeout)
local data, distance = _G.network.getTransport().read(self)
local data, distance = network.getTransport().read(self)
if data then
return data, distance
end
@@ -25,7 +26,7 @@ function socketClass:read(timeout)
local e, id = os.pullEvent()
if e == 'transport_' .. self.uid then
data, distance = _G.network.getTransport().read(self)
data, distance = network.getTransport().read(self)
if data then
os.cancelTimer(timerId)
return data, distance
@@ -46,7 +47,7 @@ end
function socketClass:write(data)
if self.connected then
_G.network.getTransport().write(self, {
network.getTransport().write(self, {
type = 'DATA',
seq = self.wseq,
data = data,
@@ -57,7 +58,7 @@ end
function socketClass:ping()
if self.connected then
_G.network.getTransport().ping(self)
network.getTransport().ping(self)
return true
end
end
@@ -71,7 +72,7 @@ function socketClass:close()
self.connected = false
end
device.wireless_modem.close(self.sport)
_G.network.getTransport().close(self)
network.getTransport().close(self)
end
local Socket = { }
@@ -107,7 +108,7 @@ end
local function setupCrypto(socket, isClient)
socket.sharedKey = ECC.exchange(socket.privKey, socket.remotePubKey)
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(
SHA.pbkdf2(socket.sharedKey, isClient and "3rseed" or "4sseed", 1))
@@ -126,9 +127,9 @@ function Socket.connect(host, port, options)
local socket = newSocket(host == os.getComputerID())
socket.dhost = tonumber(host)
if options and options.keypair then
socket.privKey, socket.pubKey = table.unpack(options.keypair)
socket.privKey, socket.pubKey = unpack(options.keypair)
else
socket.privKey, socket.pubKey = _G.network.getKeyPair()
socket.privKey, socket.pubKey = network.getKeyPair()
end
local identifier = options and options.identifier or Security.getIdentifier()
@@ -158,7 +159,7 @@ function Socket.connect(host, port, options)
socket.remotePubKey = Util.hexToByteArray(msg.pk)
socket.options = msg.options or { }
setupCrypto(socket, true)
_G.network.getTransport().open(socket)
network.getTransport().open(socket)
return socket
elseif msg.type == 'NOPASS' then
@@ -191,7 +192,7 @@ local function trusted(socket, msg, options)
if data and data.ts and tonumber(data.ts) then
if math.abs(os.epoch('utc') - data.ts) < 4096 then
socket.remotePubKey = Util.hexToByteArray(data.pk)
socket.privKey, socket.pubKey = _G.network.getKeyPair()
socket.privKey, socket.pubKey = network.getKeyPair()
setupCrypto(socket)
return true
end
@@ -240,7 +241,7 @@ function Socket.server(port, options)
options = socket.options.ENCRYPT and { ENCRYPT = true },
})
_G.network.getTransport().open(socket)
network.getTransport().open(socket)
return socket
else

View File

@@ -33,10 +33,10 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end
local win = { }
local maxScroll
local maxScroll = 100
local cx, cy = 1, 1
local blink = false
local _bg, _fg = colors.black, colors.white
local _bg, _fg = parent.getBackgroundColor(), parent.getTextColor()
win.canvas = Canvas({
x = sx,
@@ -164,7 +164,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
win.canvas.lines[lines + i] = { }
win.canvas:clearLine(lines + i)
end
while #win.canvas.lines > (maxScroll or win.canvas.height) do
while #win.canvas.lines > maxScroll do
table.remove(win.canvas.lines, 1)
end
scrollTo(#win.canvas.lines)
@@ -213,39 +213,8 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
end
function win.reposition(x, y, width, height)
if not maxScroll then
win.canvas:move(x, y)
win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
return
end
-- special processing for scrolling terminal like windows
local delta = height - win.canvas.height
if delta > 0 then -- grow
for _ = 1, delta do
win.canvas.lines[#win.canvas.lines + 1] = { }
win.canvas:clearLine(#win.canvas.lines)
end
elseif delta < 0 then -- shrink
for _ = delta + 1, 0 do
if cy < win.canvas.height then
win.canvas.lines[#win.canvas.lines] = nil
else
cy = cy - 1
win.canvas.offy = win.canvas.offy + 1
end
end
end
win.canvas:resizeBuffer(width, #win.canvas.lines)
win.canvas.height = height
win.canvas.width = width
win.canvas:move(x, y)
update()
win.canvas.x, win.canvas.y = x, y
win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
end
--[[ Additional methods ]]--
@@ -277,79 +246,6 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
return parent
end
function win.writeX(sText)
-- expect(1, sText, "string", "number")
local nLinesPrinted = 0
local function newLine()
if cy + 1 <= win.canvas.height then
cx, cy = 1, cy + 1
else
cx, cy = 1, win.canvas.height
win.scroll(1)
end
nLinesPrinted = nLinesPrinted + 1
end
-- Print the line with proper word wrapping
sText = tostring(sText)
while #sText > 0 do
local whitespace = string.match(sText, "^[ \t]+")
if whitespace then
-- Print whitespace
win.write(whitespace)
sText = string.sub(sText, #whitespace + 1)
end
local newline = string.match(sText, "^\n")
if newline then
-- Print newlines
newLine()
sText = string.sub(sText, 2)
end
local text = string.match(sText, "^[^ \t\n]+")
if text then
sText = string.sub(sText, #text + 1)
if #text > win.canvas.width then
-- Print a multiline word
while #text > 0 do
if cx > win.canvas.width then
newLine()
end
win.write(text)
text = string.sub(text, win.canvas.width - cx + 2)
end
else
-- Print a word normally
if cx + #text - 1 > win.canvas.width then
newLine()
end
win.write(text)
end
end
end
return nLinesPrinted
end
function win.print(...)
local vis = isVisible
isVisible = false
local nLinesPrinted = 0
local nLimit = select("#", ...)
for n = 1, nLimit do
local s = tostring(select(n, ...))
if n < nLimit then
s = s .. "\t"
end
nLinesPrinted = nLinesPrinted + win.writeX(s)
end
nLinesPrinted = nLinesPrinted + win.writeX("\n")
isVisible = vis
update()
return nLinesPrinted
end
win.canvas:clear()
return win

View File

@@ -12,10 +12,6 @@ local function traceback(x)
return x
end
if x and x:match(':%d+: 0$') then
return x
end
if debug_traceback then
-- The parens are important, as they prevent a tail call occuring, meaning
-- the stack level is preserved. This ensures the code behaves identically
@@ -36,68 +32,78 @@ local function traceback(x)
end
end
local function trim_traceback(stack)
local trace = { }
local filters = {
"%[C%]: in function 'xpcall'",
"(...tail calls...)",
"xpcall: $",
"trace.lua:%d+:",
"stack traceback:",
}
local function trim_traceback(target, marker)
local ttarget, tmarker = {}, {}
for line in target:gmatch("([^\n]*)\n?") do ttarget[#ttarget + 1] = line end
for line in marker:gmatch("([^\n]*)\n?") do tmarker[#tmarker + 1] = line end
for line in stack:gmatch("([^\n]*)\n?") do table.insert(trace, line) end
local err = { }
while true do
local line = table.remove(trace, 1)
if not line or line == 'stack traceback:' then
break
end
table.insert(err, line)
end
err = table.concat(err, '\n')
local function matchesFilter(line)
for _, filter in pairs(filters) do
if line:match(filter) then
return true
end
end
-- Trim identical suffixes
local t_len, m_len = #ttarget, #tmarker
while t_len >= 3 and ttarget[t_len] == tmarker[m_len] do
table.remove(ttarget, t_len)
t_len, m_len = t_len - 1, m_len - 1
end
local t = { }
for _, line in pairs(trace) do
if not matchesFilter(line) then
line = line:gsub("in function", "in"):gsub('%w+/', '')
table.insert(t, line)
end
-- Trim elements from this file and xpcall invocations
while t_len >= 1 and ttarget[t_len]:find("^\tstack_trace%.lua:%d+:") or
ttarget[t_len] == "\t[C]: in function 'xpcall'" or ttarget[t_len] == " xpcall: " do
table.remove(ttarget, t_len)
t_len = t_len - 1
end
return err, t
ttarget[#ttarget] = nil -- remove 2 calls added by the added xpcall
ttarget[#ttarget] = nil
return ttarget
end
--- Run a function with
return function (fn, ...)
-- So this is rather grim: we need to get the full traceback and current one and remove
-- the common prefix
local trace
local args = { ... }
-- xpcall in Lua 5.1 does not accept parameters
-- which is not ideal
local res = table.pack(xpcall(function()
return fn(table.unpack(args))
end, traceback))
if not res[1] and res[2] ~= nil then
local err, trace = trim_traceback(res[2])
if not res[1] then
trace = traceback("trace.lua:1:")
end
local ok, err = res[1], res[2]
if err:match(':%d+: 0$') then
return true
if not ok and err ~= nil then
trace = trim_traceback(err, trace)
-- Find the position where the stack traceback actually starts
local trace_starts
for i = #trace, 1, -1 do
if trace[i] == "stack traceback:" then trace_starts = i; break end
end
if #trace > 0 then
_G._syslog('\n' .. err .. '\n' .. 'stack traceback:')
for _, v in ipairs(trace) do
_G._syslog(v)
for _, line in pairs(trace) do
_G._syslog(line)
end
-- If this traceback is more than 15 elements long, keep the first 9, last 5
-- and put an ellipsis between the rest
local max = 10
if trace_starts and #trace - trace_starts > max then
local keep_starts = trace_starts + 7
for i = #trace - trace_starts - max, 0, -1 do
table.remove(trace, keep_starts + i)
end
table.insert(trace, keep_starts, " ...")
end
return res[1], err, trace
for k, line in pairs(trace) do
trace[k] = line:gsub("in function", " in")
end
return false, table.remove(trace, 1), table.concat(trace, "\n")
end
return table.unpack(res, 1, res.n)

Some files were not shown because too many files have changed in this diff Show More