Compare commits
127 Commits
ui-enhance
...
Merith-TK/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b328ae309 | ||
|
|
4104750539 | ||
|
|
3150525ee2 | ||
|
|
7c5f749f02 | ||
|
|
7d9029c706 | ||
|
|
ce6a741690 | ||
|
|
16d233df5d | ||
|
|
f6d09abd54 | ||
|
|
bff84370b2 | ||
|
|
e74db02371 | ||
|
|
2b75fbd1ee | ||
|
|
65bf756084 | ||
|
|
2ff4ee5670 | ||
|
|
5488a7d732 | ||
|
|
37c4ca102c | ||
|
|
ac51771b12 | ||
|
|
6fdce03a10 | ||
|
|
ac91d8ed08 | ||
|
|
2c0db368fa | ||
|
|
a77deb72ec | ||
|
|
01d8d65178 | ||
|
|
f36e5470b1 | ||
|
|
2520f53046 | ||
|
|
f4494d6103 | ||
|
|
48f32946ec | ||
|
|
c24a5a7115 | ||
|
|
624af53f4e | ||
|
|
816ea366ab | ||
|
|
b45cd45bcb | ||
|
|
45f1bd1c5d | ||
|
|
1cb2c5f785 | ||
|
|
42bd4b2b69 | ||
|
|
156b604a58 | ||
|
|
7df4a47ba0 | ||
|
|
9bf91a8762 | ||
|
|
b69dcdeffa | ||
|
|
947f502c6d | ||
|
|
a2af4405e7 | ||
|
|
1bf6daedff | ||
|
|
2fdcc338ad | ||
|
|
4f39604c63 | ||
|
|
e9580d67eb | ||
|
|
039fa73749 | ||
|
|
2d942ef001 | ||
|
|
4d02f75a19 | ||
|
|
6c5cc508b1 | ||
|
|
e7fcd68a0f | ||
|
|
7dfa80f5f3 | ||
|
|
788f6e7de0 | ||
|
|
704ef46b62 | ||
|
|
d678eeeaca | ||
|
|
6a3b38922b | ||
|
|
6009f22d8e | ||
|
|
f951f10df5 | ||
|
|
5c4ab57ec8 | ||
|
|
0359a89e12 | ||
|
|
4796e9e77a | ||
|
|
18b7f540ab | ||
|
|
2105799524 | ||
|
|
b6f439e8dc | ||
|
|
41c0758857 | ||
|
|
d1565c62e0 | ||
|
|
f00bece02b | ||
|
|
9297223640 | ||
|
|
f38afbbd36 | ||
|
|
7b225a7747 | ||
|
|
a7069f7ea8 | ||
|
|
a4f4f34576 | ||
|
|
1cce4aad03 | ||
|
|
26bbb50981 | ||
|
|
97bfae10fb | ||
|
|
985830fcfd | ||
|
|
b93d69c261 | ||
|
|
a7e3318226 | ||
|
|
c7c594d6c3 | ||
|
|
90ce2bb1a5 | ||
|
|
2629f2a172 | ||
|
|
8279c1ae12 | ||
|
|
bd911e80e8 | ||
|
|
a3a819256f | ||
|
|
cfc18e10cd | ||
|
|
b0db0b86bd | ||
|
|
71bbd2d457 | ||
|
|
db9c05fa89 | ||
|
|
3f80faa0fe | ||
|
|
8b14399a20 | ||
|
|
4e6c5172d1 | ||
|
|
c24411717a | ||
|
|
51724ccd78 | ||
|
|
3d3e5400cf | ||
|
|
4018d4bcfb | ||
|
|
4485dd2bd5 | ||
|
|
72560b1de0 | ||
|
|
3d02230742 | ||
|
|
456d2d301f | ||
|
|
cdf8645c88 | ||
|
|
d7f41f2bce | ||
|
|
e2ba9e2a03 | ||
|
|
447f4daa92 | ||
|
|
5afb21b68e | ||
|
|
3488da649e | ||
|
|
7f92286fb1 | ||
|
|
4a46d67304 | ||
|
|
3cbdf7b97b | ||
|
|
74d3d1df33 | ||
|
|
9c97849cff | ||
|
|
c1cbcf6b61 | ||
|
|
49b986499c | ||
|
|
fae7596182 | ||
|
|
cb55b1daab | ||
|
|
fd61416750 | ||
|
|
c622b2f7fb | ||
|
|
98f4bf7a7e | ||
|
|
c5e0ac0af6 | ||
|
|
0188e886c0 | ||
|
|
55f444c37e | ||
|
|
3368fa3266 | ||
|
|
e721eb68b6 | ||
|
|
1543f4d624 | ||
|
|
287adb1235 | ||
|
|
e116caf16e | ||
|
|
d72ae3de4a | ||
|
|
602d12afc5 | ||
|
|
ef9f0e09b6 | ||
|
|
b0d2ce0199 | ||
|
|
7224d441ca | ||
|
|
cdd0b6c4d2 |
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
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]
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
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.
|
||||
17
.github/ISSUE_TEMPLATES/bug.md
vendored
Normal file
17
.github/ISSUE_TEMPLATES/bug.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
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:
|
||||
13
.github/ISSUE_TEMPLATES/feature.md
vendored
Normal file
13
.github/ISSUE_TEMPLATES/feature.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
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. -->
|
||||
36
.github/workflows/main.yml
vendored
Normal file
36
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# 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 }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/ignore
|
||||
.project
|
||||
|
||||
6
.opus_version
Normal file
6
.opus_version
Normal file
@@ -0,0 +1,6 @@
|
||||
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)
|
||||
@@ -1,5 +1,7 @@
|
||||
# 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)
|
||||
@@ -14,5 +16,5 @@
|
||||
|
||||
## Install
|
||||
```
|
||||
pastebin run uzghlbnc
|
||||
pastebin run UzGHLbNC
|
||||
```
|
||||
|
||||
23
startup.lua
23
startup.lua
@@ -29,9 +29,10 @@ local function loadBootOptions()
|
||||
preload = { },
|
||||
menu = {
|
||||
{ prompt = os.version() },
|
||||
{ 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' } },
|
||||
{ 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' } },
|
||||
},
|
||||
}))
|
||||
f.close()
|
||||
@@ -41,6 +42,20 @@ 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
|
||||
|
||||
@@ -127,7 +142,7 @@ local function splash()
|
||||
local opus = {
|
||||
'fffff00',
|
||||
'ffff07000',
|
||||
'ff00770b00 4444',
|
||||
'ff00770b00f4444',
|
||||
'ff077777444444444',
|
||||
'f07777744444444444',
|
||||
'f0000777444444444',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
local Alt = require('opus.alternate')
|
||||
local Config = require('opus.config')
|
||||
local Event = require('opus.event')
|
||||
local pastebin = require('opus.http.pastebin')
|
||||
@@ -77,21 +76,65 @@ local Browser = UI.Page {
|
||||
grid = UI.ScrollingGrid {
|
||||
columns = {
|
||||
{ heading = 'Name', key = 'name' },
|
||||
{ key = 'flags', width = 2 },
|
||||
{ heading = 'Size', key = 'fsize', width = 5 },
|
||||
{ key = 'flags', width = 3, textColor = 'lightGray' },
|
||||
{ heading = 'Size', key = 'fsize', width = 5, textColor = 'yellow' },
|
||||
},
|
||||
sortColumn = 'name',
|
||||
y = 2, ey = -2,
|
||||
sortCompare = function(self, a, b)
|
||||
if self.sortColumn == 'fsize' then
|
||||
return a.size < b.size
|
||||
elseif self.sortColumn == 'flags' then
|
||||
return a.flags < b.flags
|
||||
end
|
||||
if a.isDir == b.isDir then
|
||||
return a.name:lower() < b.name:lower()
|
||||
end
|
||||
return a.isDir
|
||||
end,
|
||||
getRowTextColor = function(_, file)
|
||||
if file.marked then
|
||||
return colors.green
|
||||
end
|
||||
if file.isDir then
|
||||
return colors.cyan
|
||||
end
|
||||
if file.isReadOnly then
|
||||
return colors.pink
|
||||
end
|
||||
return colors.white
|
||||
end,
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'copy' then -- let copy be handled by parent
|
||||
return false
|
||||
end
|
||||
return UI.ScrollingGrid.eventHandler(self, event)
|
||||
end
|
||||
},
|
||||
statusBar = UI.StatusBar {
|
||||
columns = {
|
||||
{ key = 'status' },
|
||||
{ key = 'totalSize', width = 6 },
|
||||
},
|
||||
draw = function(self)
|
||||
if self.parent.dir then
|
||||
local info = '#:' .. Util.size(self.parent.dir.files)
|
||||
local numMarked = Util.size(marked)
|
||||
if numMarked > 0 then
|
||||
info = info .. ' M:' .. numMarked
|
||||
end
|
||||
self:setValue('info', info)
|
||||
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
|
||||
UI.StatusBar.draw(self)
|
||||
end
|
||||
end,
|
||||
},
|
||||
question = UI.Question {
|
||||
y = -2, x = -19,
|
||||
label = 'Delete',
|
||||
},
|
||||
notification = UI.Notification { },
|
||||
associations = UI.SlideOut {
|
||||
backgroundColor = colors.cyan,
|
||||
menuBar = UI.MenuBar {
|
||||
buttons = {
|
||||
{ text = 'Save', event = 'save' },
|
||||
@@ -99,7 +142,7 @@ local Browser = UI.Page {
|
||||
},
|
||||
},
|
||||
grid = UI.ScrollingGrid {
|
||||
x = 2, ex = -6, y = 3, ey = -5,
|
||||
x = 2, ex = -6, y = 3, ey = -8,
|
||||
columns = {
|
||||
{ heading = 'Extension', key = 'name' },
|
||||
{ heading = 'Program', key = 'value' },
|
||||
@@ -114,8 +157,11 @@ local Browser = UI.Page {
|
||||
x = -4, y = 6,
|
||||
text = '-', event = 'remove_entry', help = 'Remove',
|
||||
},
|
||||
[1] = UI.Window {
|
||||
x = 2, y = -6, ex = -6, ey = -3,
|
||||
},
|
||||
form = UI.Form {
|
||||
x = 3, y = -3, ey = -2,
|
||||
x = 3, y = -5, ex = -7, ey = -3,
|
||||
margin = 1,
|
||||
manualControls = true,
|
||||
[1] = UI.TextEntry {
|
||||
@@ -130,16 +176,13 @@ local Browser = UI.Page {
|
||||
formLabel = 'Program', formKey = 'value',
|
||||
shadowText = 'program',
|
||||
required = true,
|
||||
limit = 128,
|
||||
},
|
||||
add = UI.Button {
|
||||
x = -11, y = 1,
|
||||
text = 'Add', event = 'add_association',
|
||||
},
|
||||
},
|
||||
statusBar = UI.StatusBar {
|
||||
backgroundColor = colors.cyan,
|
||||
},
|
||||
statusBar = UI.StatusBar { },
|
||||
},
|
||||
accelerators = {
|
||||
[ 'control-q' ] = 'quit',
|
||||
@@ -167,7 +210,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
|
||||
@@ -175,56 +218,11 @@ function Browser.menuBar:getActive(menuItem)
|
||||
return true
|
||||
end
|
||||
|
||||
function Browser.grid:sortCompare(a, b)
|
||||
if self.sortColumn == 'fsize' then
|
||||
return a.size < b.size
|
||||
elseif self.sortColumn == 'flags' then
|
||||
return a.flags < b.flags
|
||||
end
|
||||
if a.isDir == b.isDir then
|
||||
return a.name:lower() < b.name:lower()
|
||||
end
|
||||
return a.isDir
|
||||
end
|
||||
|
||||
function Browser.grid:getRowTextColor(file)
|
||||
if file.marked then
|
||||
return colors.green
|
||||
end
|
||||
if file.isDir then
|
||||
return colors.cyan
|
||||
end
|
||||
if file.isReadOnly then
|
||||
return colors.pink
|
||||
end
|
||||
return colors.white
|
||||
end
|
||||
|
||||
function Browser.grid:eventHandler(event)
|
||||
if event.type == 'copy' then -- let copy be handled by parent
|
||||
return false
|
||||
end
|
||||
return UI.ScrollingGrid.eventHandler(self, event)
|
||||
end
|
||||
|
||||
function Browser.statusBar:draw()
|
||||
if self.parent.dir then
|
||||
local info = '#:' .. Util.size(self.parent.dir.files)
|
||||
local numMarked = Util.size(marked)
|
||||
if numMarked > 0 then
|
||||
info = info .. ' M:' .. numMarked
|
||||
end
|
||||
self:setValue('info', info)
|
||||
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
|
||||
UI.StatusBar.draw(self)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
@@ -255,7 +253,6 @@ function Browser:getDirectory(directory)
|
||||
end
|
||||
|
||||
function Browser:updateDirectory(dir)
|
||||
|
||||
dir.size = 0
|
||||
dir.totalSize = 0
|
||||
Util.clear(dir.files)
|
||||
@@ -265,10 +262,11 @@ function Browser:updateDirectory(dir)
|
||||
dir.size = #files
|
||||
for _, file in pairs(files) do
|
||||
file.fullName = fs.combine(dir.name, file.name)
|
||||
file.flags = ''
|
||||
file.flags = file.fstype or ' '
|
||||
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)
|
||||
@@ -276,11 +274,9 @@ function Browser:updateDirectory(dir)
|
||||
dir.totalSize = dir.totalSize + file.size
|
||||
file.fsize = formatSize(file.size)
|
||||
end
|
||||
file.flags = 'D'
|
||||
end
|
||||
if file.isReadOnly then
|
||||
file.flags = file.flags .. 'R'
|
||||
file.flags = file.flags .. 'D'
|
||||
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
|
||||
@@ -344,7 +340,7 @@ function Browser:eventHandler(event)
|
||||
local file = self.grid:getSelected()
|
||||
|
||||
if event.type == 'quit' then
|
||||
Event.exitPullEvents()
|
||||
UI:quit()
|
||||
|
||||
elseif event.type == 'edit' and file then
|
||||
self:run('edit', file.name)
|
||||
@@ -354,7 +350,7 @@ function Browser:eventHandler(event)
|
||||
self:setStatus('Started cloud edit')
|
||||
|
||||
elseif event.type == 'shell' then
|
||||
self:run(Alt.get('shell'))
|
||||
self:run('shell')
|
||||
|
||||
elseif event.type == 'refresh' then
|
||||
self:updateDirectory(self.dir)
|
||||
@@ -432,28 +428,25 @@ function Browser:eventHandler(event)
|
||||
|
||||
elseif event.type == 'delete' then
|
||||
if self:hasMarked() then
|
||||
local width = self.statusBar:getColumnWidth('status')
|
||||
self.statusBar:setColumnWidth('status', UI.term.width)
|
||||
self.statusBar:setValue('status', 'Delete marked? (y/n)')
|
||||
self.statusBar:draw()
|
||||
self.statusBar:sync()
|
||||
local _, ch = os.pullEvent('char')
|
||||
if ch == 'y' or ch == 'Y' then
|
||||
for _,m in pairs(marked) do
|
||||
pcall(function()
|
||||
fs.delete(m.fullName)
|
||||
end)
|
||||
end
|
||||
end
|
||||
marked = { }
|
||||
self.statusBar:setColumnWidth('status', width)
|
||||
self.statusBar:setValue('status', '/' .. self.dir.name)
|
||||
self:updateDirectory(self.dir)
|
||||
|
||||
self.statusBar:draw()
|
||||
self.grid:draw()
|
||||
self:setFocus(self.grid)
|
||||
self.question:show()
|
||||
end
|
||||
return true
|
||||
|
||||
elseif event.type == 'question_yes' then
|
||||
for _,m in pairs(marked) do
|
||||
pcall(fs.delete, m.fullName)
|
||||
end
|
||||
marked = { }
|
||||
self:updateDirectory(self.dir)
|
||||
|
||||
self.question:hide()
|
||||
self.statusBar:draw()
|
||||
self.grid:draw()
|
||||
self:setFocus(self.grid)
|
||||
|
||||
elseif event.type == 'question_no' then
|
||||
self.question:hide()
|
||||
self:setFocus(self.grid)
|
||||
|
||||
elseif event.type == 'copy' or event.type == 'cut' then
|
||||
if self:hasMarked() then
|
||||
@@ -472,7 +465,7 @@ function Browser:eventHandler(event)
|
||||
|
||||
elseif event.type == 'paste' then
|
||||
for _,m in pairs(copied) do
|
||||
local s, m = pcall(function()
|
||||
pcall(function()
|
||||
if cutMode then
|
||||
fs.move(m.fullName, fs.combine(self.dir.name, m.name))
|
||||
else
|
||||
@@ -549,6 +542,4 @@ local args = Util.parse(...)
|
||||
Browser:setDir(args[1] or shell.dir())
|
||||
|
||||
UI:setPage(Browser)
|
||||
|
||||
Event.pullEvents()
|
||||
UI.term:reset()
|
||||
UI:start()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
local fuzzy = require('opus.fuzzy')
|
||||
local UI = require('opus.ui')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local colors = _G.colors
|
||||
local help = _G.help
|
||||
|
||||
UI:configure('Help', ...)
|
||||
@@ -12,11 +12,11 @@ for _,topic in pairs(help.topics()) do
|
||||
end
|
||||
|
||||
UI:addPage('main', UI.Page {
|
||||
labelText = UI.Text {
|
||||
UI.Text {
|
||||
x = 3, y = 2,
|
||||
value = 'Search',
|
||||
},
|
||||
filter = UI.TextEntry {
|
||||
UI.TextEntry {
|
||||
x = 10, y = 2, ex = -3,
|
||||
limit = 32,
|
||||
},
|
||||
@@ -38,25 +38,24 @@ UI:addPage('main', UI.Page {
|
||||
|
||||
elseif event.type == 'grid_select' then
|
||||
if self.grid:getSelected() then
|
||||
local name = self.grid:getSelected().name
|
||||
|
||||
UI:setPage('topic', name)
|
||||
UI:setPage('topic', self.grid:getSelected().name)
|
||||
end
|
||||
|
||||
elseif event.type == 'text_change' then
|
||||
if not event.text then
|
||||
self.grid.values = topics
|
||||
self.grid.sortColumn = 'lname'
|
||||
else
|
||||
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
|
||||
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)
|
||||
end
|
||||
end
|
||||
self.grid:update()
|
||||
self.grid:setIndex(1)
|
||||
self.grid:draw()
|
||||
|
||||
else
|
||||
return UI.Page.eventHandler(self, event)
|
||||
end
|
||||
@@ -64,13 +63,12 @@ UI:addPage('main', UI.Page {
|
||||
})
|
||||
|
||||
UI:addPage('topic', UI.Page {
|
||||
backgroundColor = colors.black,
|
||||
backgroundColor = 'black',
|
||||
titleBar = UI.TitleBar {
|
||||
title = 'text',
|
||||
event = 'back',
|
||||
},
|
||||
helpText = UI.TextArea {
|
||||
backgroundColor = colors.black,
|
||||
x = 2, ex = -1, y = 3, ey = -2,
|
||||
},
|
||||
accelerators = {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
-- 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')
|
||||
@@ -10,10 +7,8 @@ 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() _exit = true end
|
||||
sandboxEnv.exit = function() UI:quit() end
|
||||
sandboxEnv._echo = function( ... ) return { ... } end
|
||||
_G.requireInjector(sandboxEnv)
|
||||
|
||||
@@ -34,7 +29,6 @@ local page = UI.Page {
|
||||
prompt = UI.TextEntry {
|
||||
y = 2,
|
||||
shadowText = 'enter command',
|
||||
limit = 1024,
|
||||
accelerators = {
|
||||
enter = 'command_enter',
|
||||
up = 'history_back',
|
||||
@@ -45,8 +39,9 @@ local page = UI.Page {
|
||||
},
|
||||
tabs = UI.Tabs {
|
||||
y = 3,
|
||||
[1] = UI.Tab {
|
||||
tabTitle = 'Formatted',
|
||||
formatted = UI.Tab {
|
||||
title = 'Formatted',
|
||||
index = 1,
|
||||
grid = UI.ScrollingGrid {
|
||||
columns = {
|
||||
{ heading = 'Key', key = 'name' },
|
||||
@@ -56,19 +51,25 @@ local page = UI.Page {
|
||||
autospace = true,
|
||||
},
|
||||
},
|
||||
[2] = UI.Tab {
|
||||
tabTitle = 'Output',
|
||||
output = UI.Tab {
|
||||
title = 'Output',
|
||||
index = 2,
|
||||
backgroundColor = 'black',
|
||||
output = UI.Embedded {
|
||||
visible = true,
|
||||
y = 2,
|
||||
maxScroll = 1000,
|
||||
backgroundColor = colors.black,
|
||||
backgroundColor = 'black',
|
||||
},
|
||||
draw = function(self)
|
||||
self:write(1, 1, string.rep('\131', self.width), 'black', 'primary')
|
||||
self:drawChildren()
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
page.grid = page.tabs[1].grid
|
||||
page.output = page.tabs[2].output
|
||||
page.grid = page.tabs.formatted.grid
|
||||
page.output = page.tabs.output.output
|
||||
|
||||
function page:setPrompt(value, focus)
|
||||
self.prompt:setValue(value)
|
||||
@@ -133,31 +134,18 @@ 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[2])
|
||||
self.tabs:selectTab(self.tabs.output)
|
||||
|
||||
elseif event.type == 'autocomplete' then
|
||||
local value = self.prompt.value or ''
|
||||
local sz = #value
|
||||
local pos = self.prompt.entry.pos
|
||||
self:setPrompt(autocomplete(sandboxEnv, value, self.prompt.entry.pos))
|
||||
self.prompt:setPosition(pos + #value - sz)
|
||||
self.prompt:setPosition(pos + #(self.prompt.value or '') - sz)
|
||||
self.prompt:updateCursor()
|
||||
|
||||
elseif event.type == 'device' then
|
||||
@@ -196,8 +184,7 @@ function page:eventHandler(event)
|
||||
command = nil
|
||||
self.grid:setValues(t)
|
||||
self.grid:setIndex(1)
|
||||
self.grid:adjustWidth()
|
||||
self:draw()
|
||||
self.grid:draw()
|
||||
end
|
||||
return true
|
||||
|
||||
@@ -243,8 +230,7 @@ function page:setResult(result)
|
||||
end
|
||||
self.grid:setValues(t)
|
||||
self.grid:setIndex(1)
|
||||
self.grid:adjustWidth()
|
||||
self:draw()
|
||||
self.grid:draw()
|
||||
end
|
||||
|
||||
function page.grid:eventHandler(event)
|
||||
@@ -362,7 +348,7 @@ function page:executeStatement(statement)
|
||||
term.redirect(oterm)
|
||||
counter = counter + 1
|
||||
|
||||
if s and m then
|
||||
if s and type(m) ~= "nil" then
|
||||
self:setResult(m)
|
||||
else
|
||||
self.grid:setValues({ })
|
||||
@@ -371,10 +357,6 @@ function page:executeStatement(statement)
|
||||
self:emit({ type = 'show_output' })
|
||||
end
|
||||
end
|
||||
|
||||
if _exit then
|
||||
UI:exitPullEvents()
|
||||
end
|
||||
end
|
||||
|
||||
local args = Util.parse(...)
|
||||
@@ -382,7 +364,8 @@ if args[1] then
|
||||
command = 'args[1]'
|
||||
sandboxEnv.args = args
|
||||
page:setResult(args[1])
|
||||
page:setPrompt(command)
|
||||
end
|
||||
|
||||
UI:setPage(page)
|
||||
UI:pullEvents()
|
||||
UI:start()
|
||||
|
||||
@@ -4,10 +4,8 @@ local Socket = require('opus.socket')
|
||||
local UI = require('opus.ui')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local colors = _G.colors
|
||||
local device = _G.device
|
||||
local network = _G.network
|
||||
local os = _G.os
|
||||
local shell = _ENV.shell
|
||||
|
||||
UI:configure('Network', ...)
|
||||
@@ -56,14 +54,45 @@ local page = UI.Page {
|
||||
columns = gridColumns,
|
||||
sortColumn = 'label',
|
||||
autospace = true,
|
||||
getRowTextColor = function(self, row, selected)
|
||||
if not row.active then
|
||||
return 'lightGray'
|
||||
end
|
||||
return UI.Grid.getRowTextColor(self, row, selected)
|
||||
end,
|
||||
getDisplayValues = function(_, row)
|
||||
row = Util.shallowCopy(row)
|
||||
if row.uptime then
|
||||
if row.uptime < 60 then
|
||||
row.uptime = string.format("%ds", math.floor(row.uptime))
|
||||
elseif row.uptime < 3600 then
|
||||
row.uptime = string.format("%sm", math.floor(row.uptime / 60))
|
||||
else
|
||||
row.uptime = string.format("%sh", math.floor(row.uptime / 3600))
|
||||
end
|
||||
end
|
||||
if row.fuel then
|
||||
row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or ''
|
||||
end
|
||||
if row.distance then
|
||||
row.distance = Util.toBytes(Util.round(row.distance, 1))
|
||||
end
|
||||
return row
|
||||
end,
|
||||
},
|
||||
ports = UI.SlideOut {
|
||||
titleBar = UI.TitleBar {
|
||||
title = 'Ports',
|
||||
event = 'ports_hide',
|
||||
},
|
||||
grid = UI.ScrollingGrid {
|
||||
menuBar = UI.MenuBar {
|
||||
y = 2,
|
||||
buttons = {
|
||||
{ text = 'Refresh', event = 'ports_update' },
|
||||
}
|
||||
},
|
||||
grid = UI.ScrollingGrid {
|
||||
y = 3,
|
||||
columns = {
|
||||
{ heading = 'Port', key = 'port' },
|
||||
{ heading = 'State', key = 'state' },
|
||||
@@ -72,31 +101,12 @@ local page = UI.Page {
|
||||
sortColumn = 'port',
|
||||
autospace = true,
|
||||
},
|
||||
},
|
||||
help = UI.SlideOut {
|
||||
backgroundColor = colors.cyan,
|
||||
x = 5, ex = -5, height = 8, y = -8,
|
||||
titleBar = UI.TitleBar {
|
||||
title = 'Network Help',
|
||||
event = 'slide_hide',
|
||||
},
|
||||
text = UI.TextArea {
|
||||
x = 2, y = 2,
|
||||
backgroundColor = colors.cyan,
|
||||
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',
|
||||
}
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'grid_select' then
|
||||
shell.openForegroundTab('Sniff ' .. event.selected.port)
|
||||
end
|
||||
return UI.SlideOut.eventHandler(self, event)
|
||||
end,
|
||||
},
|
||||
notification = UI.Notification { },
|
||||
accelerators = {
|
||||
@@ -127,13 +137,6 @@ local function sendCommand(host, command)
|
||||
end
|
||||
end
|
||||
|
||||
function page.ports:eventHandler(event)
|
||||
if event.type == 'grid_select' then
|
||||
shell.openForegroundTab('Sniff ' .. event.selected.port)
|
||||
end
|
||||
return UI.SlideOut.eventHandler(self, event)
|
||||
end
|
||||
|
||||
function page.ports.grid:update()
|
||||
local transport = network:getTransport()
|
||||
|
||||
@@ -185,12 +188,14 @@ 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)
|
||||
@@ -209,17 +214,22 @@ function page:eventHandler(event)
|
||||
end
|
||||
|
||||
if event.type == 'help' then
|
||||
self.help:show()
|
||||
shell.switchTab(shell.openTab('Help Networking'))
|
||||
|
||||
elseif event.type == 'ports' then
|
||||
self.ports.grid:update()
|
||||
self.ports:show()
|
||||
|
||||
self.portsHandler = Event.onInterval(3, function()
|
||||
-- self.portsHandler = Event.onInterval(3, function()
|
||||
-- self.ports.grid:update()
|
||||
-- self.ports.grid:draw()
|
||||
-- self:sync()
|
||||
-- end)
|
||||
|
||||
elseif event.type == 'ports_update' then
|
||||
self.ports.grid:update()
|
||||
self.ports.grid:draw()
|
||||
self:sync()
|
||||
end)
|
||||
|
||||
elseif event.type == 'ports_hide' then
|
||||
Event.off(self.portsHandler)
|
||||
@@ -230,7 +240,7 @@ function page:eventHandler(event)
|
||||
Config.update('network', config)
|
||||
|
||||
elseif event.type == 'quit' then
|
||||
Event.exitPullEvents()
|
||||
UI:quit()
|
||||
end
|
||||
UI.Page.eventHandler(self, event)
|
||||
end
|
||||
@@ -243,33 +253,6 @@ function page.menuBar:getActive(menuItem)
|
||||
return menuItem.noCheck or not not t
|
||||
end
|
||||
|
||||
function page.grid:getRowTextColor(row, selected)
|
||||
if not row.active then
|
||||
return colors.lightGray
|
||||
end
|
||||
return UI.Grid.getRowTextColor(self, row, selected)
|
||||
end
|
||||
|
||||
function page.grid:getDisplayValues(row)
|
||||
row = Util.shallowCopy(row)
|
||||
if row.uptime then
|
||||
if row.uptime < 60 then
|
||||
row.uptime = string.format("%ds", math.floor(row.uptime))
|
||||
elseif row.uptime < 3600 then
|
||||
row.uptime = string.format("%sm", math.floor(row.uptime / 60))
|
||||
else
|
||||
row.uptime = string.format("%sh", math.floor(row.uptime / 3600))
|
||||
end
|
||||
end
|
||||
if row.fuel then
|
||||
row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or ''
|
||||
end
|
||||
if row.distance then
|
||||
row.distance = Util.toBytes(Util.round(row.distance, 1))
|
||||
end
|
||||
return row
|
||||
end
|
||||
|
||||
Event.onInterval(1, function()
|
||||
page.grid:update()
|
||||
page.grid:draw()
|
||||
@@ -295,4 +278,4 @@ if not device.wireless_modem then
|
||||
end
|
||||
|
||||
UI:setPage(page)
|
||||
UI:pullEvents()
|
||||
UI:start()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
local Alt = require('opus.alternate')
|
||||
local Array = require('opus.array')
|
||||
local class = require('opus.class')
|
||||
local Config = require('opus.config')
|
||||
local Event = require('opus.event')
|
||||
@@ -9,7 +9,6 @@ local Tween = require('opus.ui.tween')
|
||||
local UI = require('opus.ui')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local colors = _G.colors
|
||||
local device = _G.device
|
||||
local fs = _G.fs
|
||||
local os = _G.os
|
||||
@@ -18,14 +17,25 @@ local shell = _ENV.shell
|
||||
local term = _G.term
|
||||
local turtle = _G.turtle
|
||||
|
||||
--[[
|
||||
turtle: 39x13
|
||||
computer: 51x19
|
||||
pocket: 26x20
|
||||
]]
|
||||
|
||||
if not _ENV.multishell then
|
||||
error('multishell is required')
|
||||
end
|
||||
|
||||
local REGISTRY_DIR = 'usr/.registry'
|
||||
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")
|
||||
|
||||
-- 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\
|
||||
\0302\0312\32\32\32\32\32\
|
||||
\0302\0312\32\32\32\32\32')
|
||||
|
||||
-- overview
|
||||
local uid = _ENV.multishell.getCurrent()
|
||||
@@ -65,6 +75,7 @@ local function parseIcon(iconText)
|
||||
if icon.height > 3 or icon.width > 8 then
|
||||
error('Must be an NFT image - 3 rows, 8 cols max')
|
||||
end
|
||||
NFT.transparency(icon)
|
||||
end
|
||||
return icon
|
||||
end)
|
||||
@@ -76,45 +87,39 @@ local function parseIcon(iconText)
|
||||
return s, m
|
||||
end
|
||||
|
||||
UI.VerticalTabBar = class(UI.TabBar)
|
||||
function UI.VerticalTabBar:setParent()
|
||||
self.x = 1
|
||||
self.width = 8
|
||||
self.height = nil
|
||||
self.ey = -2
|
||||
UI.TabBar.setParent(self)
|
||||
for k,c in pairs(self.children) do
|
||||
c.x = 1
|
||||
c.y = k + 1
|
||||
c.ox, c.oy = c.x, c.y
|
||||
c.ow = 8
|
||||
c.width = 8
|
||||
end
|
||||
end
|
||||
|
||||
local cx = 9
|
||||
local cy = 1
|
||||
|
||||
local page = UI.Page {
|
||||
container = UI.Viewport {
|
||||
x = cx,
|
||||
y = cy,
|
||||
x = 9, y = 1,
|
||||
},
|
||||
tabBar = UI.TabBar {
|
||||
ey = -2,
|
||||
width = 8,
|
||||
selectedBackgroundColor = 'primary',
|
||||
backgroundColor = 'tertiary',
|
||||
unselectedTextColor = 'lightGray',
|
||||
layout = function(self)
|
||||
self.height = nil
|
||||
UI.TabBar.layout(self)
|
||||
end,
|
||||
},
|
||||
tray = UI.Window {
|
||||
y = -1, width = 8,
|
||||
backgroundColor = colors.lightGray,
|
||||
newApp = UI.Button {
|
||||
backgroundColor = 'tertiary',
|
||||
newApp = UI.FlatButton {
|
||||
x = 2,
|
||||
text = '+', event = 'new',
|
||||
},
|
||||
--[[
|
||||
volume = UI.Button {
|
||||
x = 3,
|
||||
text = '\15', event = 'volume',
|
||||
},]]
|
||||
mode = UI.FlatButton {
|
||||
x = 4,
|
||||
text = '=', event = 'display_mode',
|
||||
},
|
||||
help = UI.FlatButton {
|
||||
x = 6,
|
||||
text = '?', event = 'help',
|
||||
},
|
||||
},
|
||||
editor = UI.SlideOut {
|
||||
y = -12, height = 12,
|
||||
backgroundColor = colors.cyan,
|
||||
titleBar = UI.TitleBar {
|
||||
title = 'Edit Application',
|
||||
event = 'slide_hide',
|
||||
@@ -122,7 +127,7 @@ local page = UI.Page {
|
||||
form = UI.Form {
|
||||
y = 2, ey = -2,
|
||||
[1] = UI.TextEntry {
|
||||
formLabel = 'Title', formKey = 'title', limit = 11, help = 'Application title',
|
||||
formLabel = 'Title', formKey = 'title', limit = 11, width = 13, help = 'Application title',
|
||||
required = true,
|
||||
},
|
||||
[2] = UI.TextEntry {
|
||||
@@ -130,23 +135,50 @@ local page = UI.Page {
|
||||
required = true,
|
||||
},
|
||||
[3] = UI.TextEntry {
|
||||
formLabel = 'Category', formKey = 'category', limit = 11, help = 'Category of application',
|
||||
formLabel = 'Category', formKey = 'category', limit = 6, width = 8, help = 'Category of application',
|
||||
required = true,
|
||||
},
|
||||
iconFile = UI.TextEntry {
|
||||
x = 11, ex = -12, y = 7,
|
||||
limit = 128, help = 'Path to icon file',
|
||||
shadowText = 'Path to icon file',
|
||||
editIcon = UI.Button {
|
||||
x = 11, y = 6,
|
||||
text = 'Edit', event = 'editIcon', help = 'Edit icon file',
|
||||
},
|
||||
loadIcon = UI.Button {
|
||||
x = 11, y = 9,
|
||||
x = 11, y = 8,
|
||||
text = 'Load', event = 'loadIcon', help = 'Load icon file',
|
||||
},
|
||||
helpIcon = UI.Button {
|
||||
x = 11, y = 8,
|
||||
text = 'Load', event = 'loadIcon', help = 'Load icon file',
|
||||
},
|
||||
image = UI.NftImage {
|
||||
backgroundColor = colors.black,
|
||||
y = 7, x = 2, height = 3, width = 8,
|
||||
backgroundColor = 'black',
|
||||
y = 6, x = 2, height = 3, width = 8,
|
||||
},
|
||||
},
|
||||
file_open = UI.FileSelect {
|
||||
modal = true,
|
||||
enable = function() end,
|
||||
transitionHint = 'expandUp',
|
||||
show = function(self)
|
||||
UI.FileSelect.enable(self)
|
||||
self:focusFirst()
|
||||
self:draw()
|
||||
end,
|
||||
disable = function(self)
|
||||
UI.FileSelect.disable(self)
|
||||
self.parent:focusFirst()
|
||||
-- need to recapture as we are opening a modal within another modal
|
||||
self.parent:capture(self.parent)
|
||||
end,
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'select_cancel' then
|
||||
self:disable()
|
||||
elseif event.type == 'select_file' then
|
||||
self:disable()
|
||||
end
|
||||
return UI.FileSelect.eventHandler(self, event)
|
||||
end,
|
||||
},
|
||||
notification = UI.Notification(),
|
||||
statusBar = UI.StatusBar(),
|
||||
},
|
||||
@@ -205,7 +237,7 @@ local function loadApplications()
|
||||
return requirements[a.requires]
|
||||
end
|
||||
|
||||
return true -- Util.startsWith(a.run, 'http') or shell.resolveProgram(a.run)
|
||||
return true
|
||||
end)
|
||||
|
||||
local categories = { }
|
||||
@@ -215,6 +247,7 @@ local function loadApplications()
|
||||
categories[f.category] = true
|
||||
table.insert(buttons, {
|
||||
text = f.category,
|
||||
width = 8,
|
||||
selected = config.currentCategory == f.category
|
||||
})
|
||||
end
|
||||
@@ -222,13 +255,13 @@ local function loadApplications()
|
||||
table.sort(buttons, function(a, b) return a.text < b.text end)
|
||||
table.insert(buttons, 1, { text = 'Recent' })
|
||||
|
||||
Util.removeByValue(page.children, page.tabBar)
|
||||
for k,v in pairs(buttons) do
|
||||
v.x = 1
|
||||
v.y = k + 1
|
||||
end
|
||||
|
||||
page:add {
|
||||
tabBar = UI.VerticalTabBar {
|
||||
buttons = buttons,
|
||||
},
|
||||
}
|
||||
page.tabBar.children = { }
|
||||
page.tabBar:addButtons(buttons)
|
||||
|
||||
--page.tabBar:selectTab(config.currentCategory or 'Apps')
|
||||
page.container:setCategory(config.currentCategory or 'Apps')
|
||||
@@ -243,7 +276,6 @@ UI.Icon.defaults = {
|
||||
function UI.Icon:eventHandler(event)
|
||||
if event.type == 'mouse_click' then
|
||||
self:setFocus(self.button)
|
||||
--self:emit({ type = self.button.event, button = self.button })
|
||||
return true
|
||||
elseif event.type == 'mouse_doubleclick' then
|
||||
self:emit({ type = self.button.event, button = self.button })
|
||||
@@ -259,37 +291,23 @@ function page.container:setCategory(categoryName, animate)
|
||||
self.children = { }
|
||||
self:reset()
|
||||
|
||||
local function filter(it, f)
|
||||
local ot = { }
|
||||
for _,v in pairs(it) do
|
||||
if f(v) then
|
||||
table.insert(ot, v)
|
||||
end
|
||||
end
|
||||
return ot
|
||||
end
|
||||
|
||||
local filtered
|
||||
local filtered = { }
|
||||
|
||||
if categoryName == 'Recent' then
|
||||
filtered = { }
|
||||
|
||||
for _,v in ipairs(config.Recent) do
|
||||
local app = Util.find(applications, 'key', v)
|
||||
if app then -- and fs.exists(app.run) then
|
||||
if app then
|
||||
table.insert(filtered, app)
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
filtered = filter(applications, function(a)
|
||||
return a.category == categoryName -- and fs.exists(a.run)
|
||||
filtered = Array.filter(applications, function(a)
|
||||
return a.category == categoryName
|
||||
end)
|
||||
table.sort(filtered, function(a, b) return a.title < b.title end)
|
||||
end
|
||||
|
||||
for _,program in ipairs(filtered) do
|
||||
|
||||
local icon
|
||||
if extSupport and program.iconExt then
|
||||
icon = parseIcon(program.iconExt)
|
||||
@@ -304,27 +322,43 @@ function page.container:setCategory(categoryName, animate)
|
||||
local title = ellipsis(program.title, 8)
|
||||
|
||||
local width = math.max(icon.width + 2, #title + 2)
|
||||
table.insert(self.children, UI.Icon({
|
||||
width = width,
|
||||
image = UI.NftImage({
|
||||
x = math.floor((width - icon.width) / 2) + 1,
|
||||
image = icon,
|
||||
width = 5,
|
||||
height = 3,
|
||||
}),
|
||||
button = UI.Button({
|
||||
x = math.floor((width - #title - 2) / 2) + 1,
|
||||
y = 4,
|
||||
text = title,
|
||||
backgroundColor = self.backgroundColor,
|
||||
backgroundFocusColor = colors.gray,
|
||||
textColor = colors.white,
|
||||
textFocusColor = colors.white,
|
||||
width = #title + 2,
|
||||
event = 'button',
|
||||
app = program,
|
||||
}),
|
||||
}))
|
||||
if config.listMode then
|
||||
table.insert(self.children, UI.Icon {
|
||||
width = self.width - 2,
|
||||
height = 1,
|
||||
UI.Button {
|
||||
x = 1, ex = -1,
|
||||
text = program.title,
|
||||
centered = false,
|
||||
backgroundColor = self:getProperty('backgroundColor'),
|
||||
backgroundFocusColor = 'gray',
|
||||
textColor = 'white',
|
||||
textFocusColor = 'white',
|
||||
event = 'button',
|
||||
app = program,
|
||||
}
|
||||
})
|
||||
else
|
||||
table.insert(self.children, UI.Icon({
|
||||
width = width,
|
||||
image = UI.NftImage({
|
||||
x = math.floor((width - icon.width) / 2) + 1,
|
||||
image = icon,
|
||||
}),
|
||||
button = UI.Button({
|
||||
x = math.floor((width - #title - 2) / 2) + 1,
|
||||
y = 4,
|
||||
text = title,
|
||||
backgroundColor = self:getProperty('backgroundColor'),
|
||||
backgroundFocusColor = 'gray',
|
||||
textColor = 'white',
|
||||
textFocusColor = 'white',
|
||||
width = #title + 2,
|
||||
event = 'button',
|
||||
app = program,
|
||||
}),
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
||||
local gutter = 2
|
||||
@@ -334,7 +368,8 @@ function page.container:setCategory(categoryName, animate)
|
||||
local col, row = gutter, 2
|
||||
local count = #self.children
|
||||
|
||||
local r = math.random(1, 5)
|
||||
local r = math.random(1, 7)
|
||||
local frames = 5
|
||||
-- reposition all children
|
||||
for k,child in ipairs(self.children) do
|
||||
if r == 1 then
|
||||
@@ -356,19 +391,27 @@ function page.container:setCategory(categoryName, animate)
|
||||
child.x = self.width
|
||||
child.y = self.height - 3
|
||||
end
|
||||
elseif r == 6 then
|
||||
child.x = col
|
||||
child.y = 1
|
||||
elseif r == 7 then
|
||||
child.x = 1
|
||||
child.y = self.height - 3
|
||||
end
|
||||
child.tween = Tween.new(6, child, { x = col, y = row }, 'linear')
|
||||
child.tween = Tween.new(frames, child, { x = col, y = row }, 'inQuad')
|
||||
|
||||
if not animate then
|
||||
child.x = col
|
||||
child.y = row
|
||||
end
|
||||
|
||||
self:setViewHeight(row + (config.listMode and 1 or 4))
|
||||
|
||||
if k < count then
|
||||
col = col + child.width
|
||||
if col + self.children[k + 1].width + gutter - 2 > self.width then
|
||||
col = gutter
|
||||
row = row + 5
|
||||
row = row + (config.listMode and 1 or 5)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -378,15 +421,12 @@ function page.container:setCategory(categoryName, animate)
|
||||
local function transition()
|
||||
local i = 1
|
||||
return function()
|
||||
self:clear()
|
||||
for _,child in pairs(self.children) do
|
||||
child.tween:update(1)
|
||||
child.x = math.floor(child.x)
|
||||
child.y = math.floor(child.y)
|
||||
child:draw()
|
||||
child:move(math.floor(child.x), math.floor(child.y))
|
||||
end
|
||||
i = i + 1
|
||||
return i < 7
|
||||
return i <= frames
|
||||
end
|
||||
end
|
||||
self:addTransition(transition)
|
||||
@@ -428,16 +468,19 @@ function page:eventHandler(event)
|
||||
shell.switchTab(shell.openTab(event.button.app.run))
|
||||
|
||||
elseif event.type == 'shell' then
|
||||
shell.switchTab(shell.openTab(Alt.get('shell')))
|
||||
shell.switchTab(shell.openTab('shell'))
|
||||
|
||||
elseif event.type == 'lua' then
|
||||
shell.switchTab(shell.openTab(Alt.get('lua')))
|
||||
shell.switchTab(shell.openTab('Lua'))
|
||||
|
||||
elseif event.type == 'files' then
|
||||
shell.switchTab(shell.openTab(Alt.get('files')))
|
||||
shell.switchTab(shell.openTab('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'))
|
||||
|
||||
elseif event.type == 'focus_change' then
|
||||
if event.focused.parent.UIElement == 'Icon' then
|
||||
@@ -473,6 +516,13 @@ function page:eventHandler(event)
|
||||
end
|
||||
self.editor:show({ category = category })
|
||||
|
||||
elseif event.type == 'display_mode' then
|
||||
config.listMode = not config.listMode
|
||||
Config.update('Overview', config)
|
||||
loadApplications()
|
||||
self:refresh()
|
||||
self:draw()
|
||||
|
||||
elseif event.type == 'edit' then
|
||||
local focused = page:getFocused()
|
||||
if focused.app then
|
||||
@@ -480,7 +530,7 @@ function page:eventHandler(event)
|
||||
end
|
||||
|
||||
else
|
||||
UI.Page.eventHandler(self, event)
|
||||
return UI.Page.eventHandler(self, event)
|
||||
end
|
||||
return true
|
||||
end
|
||||
@@ -502,11 +552,6 @@ function page.editor:show(app)
|
||||
self:focusFirst()
|
||||
end
|
||||
|
||||
function page.editor.form.image:draw()
|
||||
self:clear()
|
||||
UI.NftImage.draw(self)
|
||||
end
|
||||
|
||||
function page.editor:updateApplications(app)
|
||||
if not app.key then
|
||||
app.key = SHA.compute(app.title)
|
||||
@@ -516,36 +561,51 @@ function page.editor:updateApplications(app)
|
||||
loadApplications()
|
||||
end
|
||||
|
||||
function page.editor:loadImage(filename)
|
||||
local s, m = pcall(function()
|
||||
local iconLines = Util.readFile(filename)
|
||||
if not iconLines then
|
||||
error('Must be an NFT image - 3 rows, 8 cols max')
|
||||
end
|
||||
local icon, m = parseIcon(iconLines)
|
||||
if not icon then
|
||||
error(m)
|
||||
end
|
||||
if extSupport then
|
||||
self.form.values.iconExt = iconLines
|
||||
else
|
||||
self.form.values.icon = iconLines
|
||||
end
|
||||
self.form.image:setImage(icon)
|
||||
self.form.image:draw()
|
||||
end)
|
||||
if not s and m then
|
||||
local msg = m:gsub('.*: (.*)', '%1')
|
||||
self.notification:error(msg)
|
||||
end
|
||||
end
|
||||
|
||||
function page.editor:eventHandler(event)
|
||||
if event.type == 'form_cancel' or event.type == 'cancel' then
|
||||
self:hide()
|
||||
|
||||
elseif event.type == 'focus_change' then
|
||||
self.statusBar:setStatus(event.focused.help or '')
|
||||
self.statusBar:draw()
|
||||
|
||||
elseif event.type == 'editIcon' then
|
||||
local filename = '/tmp/editing.nft'
|
||||
NFT.save(self.form.image.image or TRANS_ICON, filename)
|
||||
local success = shell.run('pain.lua ' .. filename)
|
||||
self.parent:dirty(true)
|
||||
if success then
|
||||
self:loadImage(filename)
|
||||
end
|
||||
|
||||
elseif event.type == 'select_file' then
|
||||
self:loadImage(event.file)
|
||||
|
||||
elseif event.type == 'loadIcon' then
|
||||
local s, m = pcall(function()
|
||||
local iconLines = Util.readFile(self.form.iconFile.value)
|
||||
if not iconLines then
|
||||
error('Must be an NFT image - 3 rows, 8 cols max')
|
||||
end
|
||||
local icon, m = parseIcon(iconLines)
|
||||
if not icon then
|
||||
error(m)
|
||||
end
|
||||
if extSupport then
|
||||
self.form.values.iconExt = iconLines
|
||||
else
|
||||
self.form.values.icon = iconLines
|
||||
end
|
||||
self.form.image:setImage(icon)
|
||||
self.form.image:draw()
|
||||
end)
|
||||
if not s and m then
|
||||
local msg = m:gsub('.*: (.*)', '%1')
|
||||
self.notification:error(msg)
|
||||
end
|
||||
self.file_open:show()
|
||||
|
||||
elseif event.type == 'form_invalid' then
|
||||
self.notification:error(event.message)
|
||||
@@ -554,8 +614,6 @@ function page.editor:eventHandler(event)
|
||||
local values = self.form.values
|
||||
self:hide()
|
||||
self:updateApplications(values)
|
||||
--page:refresh()
|
||||
--page:draw()
|
||||
config.currentCategory = values.category
|
||||
Config.update('Overview', config)
|
||||
os.queueEvent('overview_refresh')
|
||||
@@ -565,10 +623,6 @@ function page.editor:eventHandler(event)
|
||||
return true
|
||||
end
|
||||
|
||||
UI:setPages({
|
||||
main = page,
|
||||
})
|
||||
|
||||
local function reload()
|
||||
loadApplications()
|
||||
page:refresh()
|
||||
@@ -594,5 +648,4 @@ end)
|
||||
loadApplications()
|
||||
|
||||
UI:setPage(page)
|
||||
|
||||
UI:pullEvents()
|
||||
UI:start()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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')
|
||||
@@ -8,9 +9,11 @@ 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 = -5,
|
||||
x = 2, ex = 14, y = 2, ey = -6,
|
||||
values = { },
|
||||
columns = {
|
||||
{ heading = 'Package', key = 'name' },
|
||||
@@ -21,13 +24,13 @@ local page = UI.Page {
|
||||
},
|
||||
add = UI.Button {
|
||||
x = 2, y = -3,
|
||||
text = 'Install',
|
||||
text = ' + ',
|
||||
event = 'action',
|
||||
help = 'Install or update',
|
||||
},
|
||||
remove = UI.Button {
|
||||
x = 12, y = -3,
|
||||
text = 'Remove ',
|
||||
x = 8, y = -3,
|
||||
text = ' - ',
|
||||
event = 'action',
|
||||
operation = 'uninstall',
|
||||
operationText = 'Remove',
|
||||
@@ -41,10 +44,17 @@ local page = UI.Page {
|
||||
},
|
||||
description = UI.TextArea {
|
||||
x = 16, y = 3, ey = -5,
|
||||
marginRight = 0, marginLeft = 0,
|
||||
marginRight = 2, marginLeft = 0,
|
||||
},
|
||||
UI.Checkbox {
|
||||
x = 3, y = -5,
|
||||
label = 'Compress',
|
||||
textColor = 'yellow',
|
||||
backgroundColor = 'primary',
|
||||
value = config.compression,
|
||||
help = 'Compress packages (experimental)',
|
||||
},
|
||||
action = UI.SlideOut {
|
||||
backgroundColor = colors.cyan,
|
||||
titleBar = UI.TitleBar {
|
||||
event = 'hide-action',
|
||||
},
|
||||
@@ -103,8 +113,6 @@ 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)
|
||||
@@ -128,7 +136,6 @@ end
|
||||
function page:updateSelection(selected)
|
||||
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()
|
||||
@@ -141,12 +148,16 @@ function page:eventHandler(event)
|
||||
elseif event.type == 'grid_focus_row' then
|
||||
local manifest = event.selected.manifest
|
||||
|
||||
self.description.value = string.format('%s%s\n\n%s%s',
|
||||
self.description:setValue(string.format('%s%s\n\n%s%s',
|
||||
Ansi.yellow, manifest.title,
|
||||
Ansi.white, manifest.description)
|
||||
Ansi.white, manifest.description))
|
||||
self.description:draw()
|
||||
self:updateSelection(event.selected)
|
||||
|
||||
elseif event.type == 'checkbox_change' then
|
||||
config.compression = not config.compression
|
||||
Config.update('package', config)
|
||||
|
||||
elseif event.type == 'updateall' then
|
||||
self.operation = 'updateall'
|
||||
self.action.button.text = ' Begin '
|
||||
@@ -184,7 +195,7 @@ function page:eventHandler(event)
|
||||
self.action.button:draw()
|
||||
|
||||
elseif event.type == 'quit' then
|
||||
UI:exitPullEvents()
|
||||
UI:quit()
|
||||
end
|
||||
UI.Page.eventHandler(self, event)
|
||||
end
|
||||
@@ -196,4 +207,4 @@ Packages:downloadList()
|
||||
page:loadPackages()
|
||||
page:sync()
|
||||
|
||||
UI:pullEvents()
|
||||
UI:start()
|
||||
|
||||
238
sys/apps/Partition.lua
Normal file
238
sys/apps/Partition.lua
Normal file
@@ -0,0 +1,238 @@
|
||||
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()
|
||||
@@ -1,5 +1,3 @@
|
||||
local Alt = require('opus.alternate')
|
||||
|
||||
local kernel = _G.kernel
|
||||
local os = _G.os
|
||||
local shell = _ENV.shell
|
||||
@@ -20,7 +18,7 @@ kernel.hook('kernel_focus', function(_, eventData)
|
||||
end
|
||||
end
|
||||
if nextTab == launcherTab then
|
||||
shell.switchTab(shell.openTab(Alt.get('shell')))
|
||||
shell.switchTab(shell.openTab('shell'))
|
||||
else
|
||||
shell.switchTab(nextTab.uid)
|
||||
end
|
||||
|
||||
@@ -5,14 +5,13 @@ local Util = require('opus.util')
|
||||
local colors = _G.colors
|
||||
local device = _G.device
|
||||
local textutils = _G.textutils
|
||||
local peripheral = _G.peripheral
|
||||
local multishell = _ENV.multishell
|
||||
|
||||
local gridColumns = {}
|
||||
table.insert(gridColumns, { heading = '#', key = 'id', width = 5, align = 'right' })
|
||||
table.insert(gridColumns, { heading = 'Port', key = 'portid', width = 5, align = 'right' })
|
||||
table.insert(gridColumns, { heading = 'Reply', key = 'replyid', width = 5, align = 'right' })
|
||||
if UI.defaultDevice.width > 50 then
|
||||
if UI.term.width > 50 then
|
||||
table.insert(gridColumns, { heading = 'Dist', key = 'distance', width = 6, align = 'right' })
|
||||
end
|
||||
table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' })
|
||||
@@ -42,12 +41,13 @@ local page = UI.Page {
|
||||
|
||||
configSlide = UI.SlideOut {
|
||||
y = -11,
|
||||
titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close' },
|
||||
titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close', backgroundColor = colors.black },
|
||||
accelerators = { ['backspace'] = 'config_close' },
|
||||
configTabs = UI.Tabs {
|
||||
y = 2,
|
||||
filterTab = UI.Tab {
|
||||
tabTitle = 'Filter',
|
||||
title = 'Filter',
|
||||
noFill = true,
|
||||
filterGridText = UI.Text {
|
||||
x = 2, y = 2,
|
||||
value = 'ID filter',
|
||||
@@ -93,7 +93,7 @@ local page = UI.Page {
|
||||
},
|
||||
},
|
||||
modemTab = UI.Tab {
|
||||
tabTitle = 'Modem',
|
||||
title = 'Modem',
|
||||
channelGrid = UI.ScrollingGrid {
|
||||
x = 2, y = 2,
|
||||
width = 12, height = 5,
|
||||
@@ -130,7 +130,6 @@ local page = UI.Page {
|
||||
title = 'Packet Information',
|
||||
event = 'packet_close',
|
||||
},
|
||||
backgroundColor = colors.cyan,
|
||||
accelerators = {
|
||||
['backspace'] = 'packet_close',
|
||||
['left'] = 'prev_packet',
|
||||
@@ -256,7 +255,7 @@ function page.packetSlide:eventHandler(event)
|
||||
page:setFocus(page.packetGrid)
|
||||
|
||||
elseif event.type == 'packet_lua' then
|
||||
multishell.openTab({ path = 'sys/apps/Lua.lua', args = { self.currentPacket.message }, focused = true })
|
||||
multishell.openTab(_ENV, { path = 'sys/apps/Lua.lua', args = { self.currentPacket.message }, focused = true })
|
||||
|
||||
elseif event.type == 'prev_packet' then
|
||||
local c = self.currentPacket
|
||||
@@ -280,7 +279,7 @@ function page.packetSlide:eventHandler(event)
|
||||
end
|
||||
|
||||
function page.packetGrid:getDisplayValues(row)
|
||||
local row = Util.shallowCopy(row)
|
||||
row = Util.shallowCopy(row)
|
||||
row.distance = Util.toBytes(Util.round(row.distance), 2)
|
||||
return row
|
||||
end
|
||||
@@ -356,7 +355,7 @@ function page:eventHandler(event)
|
||||
self.packetSlide:show(event.selected)
|
||||
|
||||
elseif event.type == 'quit' then
|
||||
Event.exitPullEvents()
|
||||
UI:quit()
|
||||
|
||||
else return UI.Page.eventHandler(self, event)
|
||||
end
|
||||
@@ -386,4 +385,4 @@ if args[1] then
|
||||
end
|
||||
|
||||
UI:setPage(page)
|
||||
UI:pullEvents()
|
||||
UI:start()
|
||||
|
||||
@@ -6,62 +6,6 @@ local shell = _ENV.shell
|
||||
|
||||
UI:configure('System', ...)
|
||||
|
||||
local systemPage = UI.Page {
|
||||
tabs = UI.Tabs {
|
||||
settings = UI.Tab {
|
||||
tabTitle = 'Category',
|
||||
grid = UI.ScrollingGrid {
|
||||
y = 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:exitPullEvents()
|
||||
|
||||
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
|
||||
@@ -70,16 +14,69 @@ 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.tabTitle, description = m.description })
|
||||
table.insert(plugins, { tab = m, name = m.title, description = m.description })
|
||||
end
|
||||
end
|
||||
return plugins
|
||||
end
|
||||
|
||||
local programDir = fs.getDir(shell.getRunningProgram())
|
||||
local programDir = fs.getDir(_ENV.arg[0])
|
||||
local plugins = loadDirectory(fs.combine(programDir, 'system'), { })
|
||||
|
||||
systemPage.tabs.settings.grid:setValues(plugins)
|
||||
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()
|
||||
|
||||
UI:setPage(systemPage)
|
||||
UI:pullEvents()
|
||||
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:start()
|
||||
|
||||
@@ -3,6 +3,7 @@ local UI = require('opus.ui')
|
||||
|
||||
local kernel = _G.kernel
|
||||
local multishell = _ENV.multishell
|
||||
local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines
|
||||
|
||||
UI:configure('Tasks', ...)
|
||||
|
||||
@@ -11,6 +12,7 @@ local page = UI.Page {
|
||||
buttons = {
|
||||
{ text = 'Activate', event = 'activate' },
|
||||
{ text = 'Terminate', event = 'terminate' },
|
||||
{ text = 'Inspect', event = 'inspect' },
|
||||
},
|
||||
},
|
||||
grid = UI.ScrollingGrid {
|
||||
@@ -21,7 +23,7 @@ local page = UI.Page {
|
||||
{ heading = 'Status', key = 'status' },
|
||||
{ heading = 'Time', key = 'timestamp' },
|
||||
},
|
||||
values = kernel.routines,
|
||||
values = tasks,
|
||||
sortColumn = 'uid',
|
||||
autospace = true,
|
||||
getDisplayValues = function (_, row)
|
||||
@@ -38,7 +40,7 @@ local page = UI.Page {
|
||||
},
|
||||
accelerators = {
|
||||
[ 'control-q' ] = 'quit',
|
||||
space = 'activate',
|
||||
[ ' ' ] = 'activate',
|
||||
t = 'terminate',
|
||||
},
|
||||
eventHandler = function (self, event)
|
||||
@@ -48,10 +50,16 @@ 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
|
||||
Event.exitPullEvents()
|
||||
UI:quit()
|
||||
end
|
||||
UI.Page.eventHandler(self, event)
|
||||
end
|
||||
@@ -64,4 +72,4 @@ Event.onInterval(1, function()
|
||||
end)
|
||||
|
||||
UI:setPage(page)
|
||||
UI:pullEvents()
|
||||
UI:start()
|
||||
|
||||
54
sys/apps/Version.lua
Normal file
54
sys/apps/Version.lua
Normal file
@@ -0,0 +1,54 @@
|
||||
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()
|
||||
@@ -33,87 +33,85 @@ https://github.com/kepler155c/opus]]
|
||||
local page = UI.Page {
|
||||
wizard = UI.Wizard {
|
||||
ey = -2,
|
||||
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),
|
||||
},
|
||||
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.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.WizardPage {
|
||||
index = 2,
|
||||
labelText = UI.Text {
|
||||
x = 3, y = 2,
|
||||
value = 'Label'
|
||||
},
|
||||
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,
|
||||
label = UI.TextEntry {
|
||||
x = 9, y = 2, ex = -3,
|
||||
limit = 32,
|
||||
value = os.getComputerLabel(),
|
||||
},
|
||||
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),
|
||||
},
|
||||
intro = UI.TextArea {
|
||||
textColor = colors.yellow,
|
||||
inactive = true,
|
||||
x = 3, ex = -3, y = 4, ey = -3,
|
||||
value = string.format(labelIntro, 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),
|
||||
},
|
||||
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,
|
||||
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),
|
||||
},
|
||||
},
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -131,7 +129,7 @@ function page:eventHandler(event)
|
||||
shell.openForegroundTab('PackageManager')
|
||||
|
||||
elseif event.type == 'wizard_complete' or event.type == 'cancel' then
|
||||
UI.exitPullEvents()
|
||||
UI:quit()
|
||||
|
||||
else
|
||||
return UI.Page.eventHandler(self, event)
|
||||
@@ -140,4 +138,4 @@ function page:eventHandler(event)
|
||||
end
|
||||
|
||||
UI:setPage(page)
|
||||
UI:pullEvents()
|
||||
UI:start()
|
||||
|
||||
24
sys/apps/compat.lua
Normal file
24
sys/apps/compat.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
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
|
||||
39
sys/apps/fileui.lua
Normal file
39
sys/apps/fileui.lua
Normal file
@@ -0,0 +1,39 @@
|
||||
local UI = require('opus.ui')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local shell = _ENV.shell
|
||||
local multishell = _ENV.multishell
|
||||
|
||||
-- fileui [--path=path] [--exec=filename] [--title=title]
|
||||
|
||||
local page = UI.Page {
|
||||
fileselect = UI.FileSelect { },
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'select_file' then
|
||||
self.selected = event.file
|
||||
UI:quit()
|
||||
|
||||
elseif event.type == 'select_cancel' then
|
||||
UI:quit()
|
||||
end
|
||||
|
||||
return UI.Page.eventHandler(self, event)
|
||||
end,
|
||||
}
|
||||
|
||||
local _, args = Util.parse(...)
|
||||
|
||||
if args.title and multishell then
|
||||
multishell.setTitle(multishell.getCurrent(), args.title)
|
||||
end
|
||||
|
||||
UI:setPage(page, args.path)
|
||||
UI:start()
|
||||
UI.term:setCursorBlink(false)
|
||||
|
||||
if args.exec and page.selected then
|
||||
shell.openForegroundTab(string.format('%s %s', args.exec, page.selected))
|
||||
return
|
||||
end
|
||||
|
||||
return page.selected
|
||||
14
sys/apps/genotp.lua
Normal file
14
sys/apps/genotp.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
local SHA = require("opus.crypto.sha2")
|
||||
|
||||
local acceptableCharacters = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
|
||||
local acceptableCharactersLen = #acceptableCharacters
|
||||
|
||||
local password = ""
|
||||
|
||||
for _i = 1, 8 do
|
||||
password = password .. acceptableCharacters[math.random(acceptableCharactersLen)]
|
||||
end
|
||||
|
||||
os.queueEvent("set_otp", SHA.compute(password))
|
||||
|
||||
print("Your one-time password is: " .. password)
|
||||
@@ -49,7 +49,7 @@ page = UI.Page {
|
||||
backgroundColor = colors.red,
|
||||
y = '50%',
|
||||
properties = UI.Tab {
|
||||
tabTitle = 'Properties',
|
||||
title = 'Properties',
|
||||
grid = UI.ScrollingGrid {
|
||||
headerBackgroundColor = colors.red,
|
||||
sortColumn = 'key',
|
||||
@@ -64,7 +64,7 @@ page = UI.Page {
|
||||
},
|
||||
methodsTab = UI.Tab {
|
||||
index = 2,
|
||||
tabTitle = 'Methods',
|
||||
title = 'Methods',
|
||||
grid = UI.ScrollingGrid {
|
||||
ex = '50%',
|
||||
headerBackgroundColor = colors.red,
|
||||
@@ -85,7 +85,7 @@ page = UI.Page {
|
||||
},
|
||||
events = UI.Tab {
|
||||
index = 1,
|
||||
tabTitle = 'Events',
|
||||
title = '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({
|
||||
multishell.openTab(_ENV, {
|
||||
path = 'sys/apps/Lua.lua',
|
||||
args = { event.selected.raw },
|
||||
focused = true,
|
||||
@@ -133,6 +133,12 @@ page = UI.Page {
|
||||
},
|
||||
},
|
||||
},
|
||||
accelerators = {
|
||||
['shift-right'] = 'size',
|
||||
['shift-left' ] = 'size',
|
||||
['shift-up' ] = 'size',
|
||||
['shift-down' ] = 'size',
|
||||
},
|
||||
eventHandler = function (self, event)
|
||||
if event.type == 'focus_change' and isRelevant(event.focused) then
|
||||
focused = event.focused
|
||||
@@ -144,7 +150,6 @@ page = UI.Page {
|
||||
})
|
||||
end
|
||||
self.tabs.properties.grid:setValues(t)
|
||||
self.tabs.properties.grid:update()
|
||||
self.tabs.properties.grid:draw()
|
||||
|
||||
t = { }
|
||||
@@ -156,7 +161,6 @@ page = UI.Page {
|
||||
end
|
||||
end
|
||||
self.tabs.methodsTab.grid:setValues(t)
|
||||
self.tabs.methodsTab.grid:update()
|
||||
self.tabs.methodsTab.grid:draw()
|
||||
|
||||
elseif event.type == 'edit_property' then
|
||||
@@ -168,6 +172,19 @@ page = UI.Page {
|
||||
|
||||
elseif event.type == 'editor_apply' then
|
||||
self.editor:hide()
|
||||
|
||||
elseif event.type == 'size' then
|
||||
local sizing = {
|
||||
['shift-right'] = { 1, 0 },
|
||||
['shift-left' ] = { -1, 0 },
|
||||
['shift-up' ] = { 0, -1 },
|
||||
['shift-down' ] = { 0, 1 },
|
||||
}
|
||||
self.ox = math.max(self.ox + sizing[event.ie.code][1], 1)
|
||||
self.oy = math.max(self.oy + sizing[event.ie.code][2], 1)
|
||||
UI.term:clear()
|
||||
self:resize()
|
||||
self:draw()
|
||||
end
|
||||
|
||||
return UI.Page.eventHandler(self, event)
|
||||
@@ -175,4 +192,4 @@ page = UI.Page {
|
||||
}
|
||||
|
||||
UI:setPage(page)
|
||||
UI:pullEvents()
|
||||
UI:start()
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
_G.requireInjector(_ENV)
|
||||
|
||||
local Event = require('opus.event')
|
||||
local Util = require('opus.util')
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Event.on('generate_keypair', function()
|
||||
table.insert(keyPairs, { generateKeyPair() })
|
||||
_G._syslog('Generated keypair in ' .. timer())
|
||||
if #keyPairs >= 3 then
|
||||
break
|
||||
break
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -31,21 +31,14 @@ local function snmpConnection(socket)
|
||||
socket:write('pong')
|
||||
|
||||
elseif msg.type == 'script' then
|
||||
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
|
||||
kernel.run(_ENV, {
|
||||
chunk = msg.args,
|
||||
title = 'script',
|
||||
})
|
||||
|
||||
elseif msg.type == 'scriptEx' then
|
||||
local s, m = pcall(function()
|
||||
local env = setmetatable(Util.shallowCopy(_ENV), { __index = _G })
|
||||
local env = kernel.makeEnv(_ENV)
|
||||
local fn, m = load(msg.args, 'script', nil, env)
|
||||
if not fn then
|
||||
error(m)
|
||||
|
||||
@@ -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,18 +41,17 @@ local function telnetHost(socket, mode)
|
||||
end
|
||||
end
|
||||
|
||||
local shellThread = kernel.run({
|
||||
terminal = win,
|
||||
local shellThread = kernel.run(_ENV, {
|
||||
window = win,
|
||||
title = mode .. ' client',
|
||||
hidden = true,
|
||||
co = coroutine.create(function()
|
||||
Util.run(_ENV, Alt.get('shell'), table.unpack(termInfo.program))
|
||||
fn = function()
|
||||
Util.run(kernel.makeEnv(_ENV), shell.resolveProgram('shell'), table.unpack(termInfo.program))
|
||||
if socket.queue then
|
||||
socket:write(socket.queue)
|
||||
end
|
||||
socket:close()
|
||||
end)
|
||||
end,
|
||||
})
|
||||
|
||||
Event.addRoutine(function()
|
||||
|
||||
@@ -6,6 +6,22 @@ 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
|
||||
@@ -13,17 +29,22 @@ local function trustConnection(socket)
|
||||
if not password then
|
||||
socket:write({ msg = 'No password has been set' })
|
||||
else
|
||||
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)
|
||||
|
||||
if validateData(data, password, socket.dhost) then
|
||||
print("Accepted trust from " .. socket.dhost)
|
||||
socket:write({ success = true, msg = 'Trust accepted' })
|
||||
else
|
||||
socket:write({ msg = 'Invalid password' })
|
||||
return
|
||||
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
|
||||
@@ -44,3 +65,12 @@ 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)
|
||||
@@ -1,6 +1,9 @@
|
||||
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
|
||||
@@ -16,9 +19,8 @@ local function makeSandbox()
|
||||
end
|
||||
|
||||
local function Syntax(msg)
|
||||
_G.printError(msg)
|
||||
print('\nSyntax: Package list | install [name] ... | update [name] | uninstall [name]')
|
||||
error(0)
|
||||
print('Syntax: package list | install [name] ... | update [name] | updateall | uninstall [name]\n')
|
||||
error(msg)
|
||||
end
|
||||
|
||||
local function progress(max)
|
||||
@@ -76,6 +78,11 @@ local function install(name, isUpdate, ignoreDeps)
|
||||
local packageDir = fs.combine('packages', name)
|
||||
|
||||
local list = Git.list(manifest.repository)
|
||||
-- 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 = { }
|
||||
@@ -96,6 +103,12 @@ 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
|
||||
@@ -152,6 +165,7 @@ if action == 'uninstall' then
|
||||
|
||||
local packageDir = fs.combine('packages', name)
|
||||
fs.delete(packageDir)
|
||||
fs.delete(packageDir .. '.tar.lzw')
|
||||
print('removed: ' .. packageDir)
|
||||
return
|
||||
end
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
local parentShell = _ENV.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 Util = require('opus.util')
|
||||
|
||||
local fs = _G.fs
|
||||
local settings = _G.settings
|
||||
local shell = _ENV.shell
|
||||
|
||||
local DIR = (parentShell and parentShell.dir()) or ""
|
||||
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
|
||||
@@ -25,16 +16,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
|
||||
@@ -43,37 +34,75 @@ local function tokenise( ... )
|
||||
return tWords
|
||||
end
|
||||
|
||||
local function run(env, ...)
|
||||
local args = tokenise(...)
|
||||
local command = table.remove(args, 1) or error('No such program')
|
||||
local isUrl = not not command:match("^(https?:)")
|
||||
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,
|
||||
|
||||
local path, loadFn
|
||||
if isUrl then
|
||||
path = command
|
||||
loadFn = Util.loadUrl
|
||||
else
|
||||
path = shell.resolveProgram(command) or error('No such program')
|
||||
loadFn = loadfile
|
||||
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 args = tokenise(...)
|
||||
if #args == 0 then
|
||||
error('No such program')
|
||||
end
|
||||
|
||||
local fn, err = loadFn(path, env)
|
||||
if not fn then
|
||||
error(err)
|
||||
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)
|
||||
end
|
||||
|
||||
if _ENV.multishell then
|
||||
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), fs.getName(path):match('([^%.]+)'))
|
||||
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), pi.title)
|
||||
end
|
||||
|
||||
if isUrl then
|
||||
tProgramStack[#tProgramStack + 1] = path -- path:match("^https?://([^/:]+:?[0-9]*/?.*)$")
|
||||
else
|
||||
tProgramStack[#tProgramStack + 1] = path
|
||||
end
|
||||
tProgramStack[#tProgramStack + 1] = pi
|
||||
|
||||
env[ "arg" ] = { [0] = path, table.unpack(args) }
|
||||
local r = { fn(table.unpack(args)) }
|
||||
pi.env[ "arg" ] = { [0] = pi.path, table.unpack(pi.args) }
|
||||
local r = { O_v_O(table.unpack(pi.args)) }
|
||||
|
||||
tProgramStack[#tProgramStack] = nil
|
||||
|
||||
@@ -88,10 +117,7 @@ function shell.run(...)
|
||||
oldTitle = _ENV.multishell.getTitle(_ENV.multishell.getCurrent())
|
||||
end
|
||||
|
||||
local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G })
|
||||
_G.requireInjector(env)
|
||||
|
||||
local r = { trace(run, env, ...) }
|
||||
local r = { trace(run, ...) }
|
||||
|
||||
if _ENV.multishell then
|
||||
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), oldTitle or 'shell')
|
||||
@@ -105,7 +131,14 @@ function shell.exit()
|
||||
end
|
||||
|
||||
function shell.dir() return DIR end
|
||||
function shell.setDir(d) DIR = d 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.path() return PATH end
|
||||
function shell.setPath(p) PATH = p end
|
||||
|
||||
@@ -118,49 +151,41 @@ function shell.resolve( _sPath )
|
||||
end
|
||||
end
|
||||
|
||||
function shell.resolveProgram( _sCommand )
|
||||
function shell.resolveProgram(_sCommand)
|
||||
if tAliases[_sCommand] ~= nil then
|
||||
_sCommand = tAliases[_sCommand]
|
||||
end
|
||||
|
||||
if _sCommand:match("^(https?:)") then
|
||||
return _sCommand
|
||||
local function check(f)
|
||||
return fs.exists(f) and not fs.isDir(f) and f
|
||||
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'
|
||||
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
|
||||
end
|
||||
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
|
||||
-- Not found
|
||||
return nil
|
||||
-- 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')
|
||||
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
|
||||
@@ -177,96 +202,82 @@ 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
|
||||
|
||||
local function completeProgram( sLine )
|
||||
if #sLine > 0 and string.sub( sLine, 1, 1 ) == "/" then
|
||||
function shell.completeProgram(sLine)
|
||||
if #sLine > 0 and string.sub(sLine, 1, 1) == '/' then
|
||||
-- Add programs from the root
|
||||
return fs.complete( sLine, "", true, false )
|
||||
else
|
||||
local tResults = {}
|
||||
local tSeen = {}
|
||||
return fs.complete(sLine, '', true, false)
|
||||
end
|
||||
|
||||
-- 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
|
||||
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
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
-- 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
|
||||
-- Sort and return
|
||||
table.sort(tResults)
|
||||
return tResults
|
||||
end
|
||||
|
||||
function shell.complete(sLine)
|
||||
if #sLine > 0 then
|
||||
local tWords = tokenise( sLine )
|
||||
local nIndex = #tWords
|
||||
if string.sub( sLine, #sLine, #sLine ) == " " then
|
||||
nIndex = nIndex + 1
|
||||
end
|
||||
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
|
||||
|
||||
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
|
||||
local tWords = tokenise(sLine)
|
||||
local nIndex = #tWords
|
||||
if string.sub(sLine, #sLine, #sLine) == ' ' and #Util.trim(sLine) > 0 then
|
||||
nIndex = nIndex + 1
|
||||
end
|
||||
end
|
||||
|
||||
function shell.completeProgram( sProgram )
|
||||
return completeProgram( sProgram )
|
||||
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)
|
||||
end
|
||||
return results
|
||||
|
||||
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)
|
||||
end
|
||||
return results and #results > 0 and results
|
||||
or fs.complete(sPart, shell.dir(), true, false)
|
||||
end
|
||||
end
|
||||
|
||||
function shell.setCompletionFunction(sProgram, fnComplete)
|
||||
@@ -278,23 +289,25 @@ function shell.getCompletionInfo()
|
||||
end
|
||||
|
||||
function shell.getRunningProgram()
|
||||
return tProgramStack[#tProgramStack] and tProgramStack[#tProgramStack].path
|
||||
end
|
||||
|
||||
function shell.getRunningInfo()
|
||||
return tProgramStack[#tProgramStack]
|
||||
end
|
||||
|
||||
function shell.setEnv(name, value)
|
||||
_ENV[name] = value
|
||||
sandboxEnv[name] = value
|
||||
-- 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
|
||||
end
|
||||
|
||||
function shell.getEnv()
|
||||
return sandboxEnv
|
||||
end
|
||||
|
||||
function shell.setAlias( _sCommand, _sProgram )
|
||||
function shell.setAlias(_sCommand, _sProgram)
|
||||
tAliases[_sCommand] = _sProgram
|
||||
end
|
||||
|
||||
function shell.clearAlias( _sCommand )
|
||||
function shell.clearAlias(_sCommand)
|
||||
tAliases[_sCommand] = nil
|
||||
end
|
||||
|
||||
@@ -313,7 +326,6 @@ function shell.newTab(tabInfo, ...)
|
||||
|
||||
if path then
|
||||
tabInfo.path = path
|
||||
tabInfo.env = Util.shallowCopy(sandboxEnv)
|
||||
tabInfo.args = args
|
||||
tabInfo.title = fs.getName(path):match('([^%.]+)')
|
||||
|
||||
@@ -321,25 +333,21 @@ function shell.newTab(tabInfo, ...)
|
||||
table.insert(tabInfo.args, 1, tabInfo.path)
|
||||
tabInfo.path = 'sys/apps/shell.lua'
|
||||
end
|
||||
return _ENV.multishell.openTab(tabInfo)
|
||||
return _ENV.multishell.openTab(_ENV, tabInfo)
|
||||
end
|
||||
return nil, 'No such program'
|
||||
end
|
||||
|
||||
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
|
||||
if not _ENV.multishell then
|
||||
function shell.newTab()
|
||||
error('Multishell is not available')
|
||||
end
|
||||
end
|
||||
|
||||
function shell.openTab(...)
|
||||
return shell.newTab({ }, ...)
|
||||
end
|
||||
|
||||
function shell.openForegroundTab( ... )
|
||||
return shell.newTab({ focused = true }, ...)
|
||||
end
|
||||
@@ -354,10 +362,7 @@ end
|
||||
|
||||
local tArgs = { ... }
|
||||
if #tArgs > 0 then
|
||||
local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G })
|
||||
_G.requireInjector(env)
|
||||
|
||||
return run(env, ...)
|
||||
return run(...)
|
||||
end
|
||||
|
||||
local Config = require('opus.config')
|
||||
@@ -374,23 +379,16 @@ 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,
|
||||
@@ -407,43 +405,15 @@ if not _colors.backgroundColor then
|
||||
_colors.fileColor = colors.white
|
||||
end
|
||||
|
||||
local palette = { }
|
||||
for n = 1, 16 do
|
||||
palette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n)
|
||||
if not terminal.scrollUp then
|
||||
terminal = Terminal.window(term.current())
|
||||
terminal.setMaxScroll(200)
|
||||
oldTerm = term.redirect(terminal)
|
||||
term.setBackgroundColor(_colors.backgroundColor)
|
||||
term.clear()
|
||||
end
|
||||
|
||||
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 palette = terminal.canvas.palette
|
||||
|
||||
local function autocomplete(line)
|
||||
local words = { }
|
||||
@@ -457,14 +427,7 @@ local function autocomplete(line)
|
||||
words = { '' }
|
||||
end
|
||||
|
||||
local results
|
||||
|
||||
local program = shell.resolveProgram(words[1])
|
||||
if tCompletionInfo[program] then
|
||||
results = autocompleteArgument(program, words) or { }
|
||||
else
|
||||
results = autocompleteAnything(line, words) or { }
|
||||
end
|
||||
local results = shell.complete(line) or { }
|
||||
|
||||
Util.filterInplace(results, function(f)
|
||||
return not Util.key(results, f .. '/')
|
||||
@@ -477,8 +440,8 @@ local function autocomplete(line)
|
||||
if #results == 1 then
|
||||
words[#words] = results[1]
|
||||
return table.concat(words, ' ')
|
||||
elseif #results > 1 then
|
||||
|
||||
elseif #results > 1 then
|
||||
local function someComplete()
|
||||
-- ugly (complete as much as possible)
|
||||
local word = words[#words] or ''
|
||||
@@ -487,16 +450,16 @@ local function autocomplete(line)
|
||||
local ch
|
||||
for _,f in ipairs(results) do
|
||||
if #f < i then
|
||||
words[#words] = string.sub(f, 1, i - 1)
|
||||
words[#words] = _sub(f, 1, i - 1)
|
||||
return table.concat(words, ' ')
|
||||
end
|
||||
if not ch then
|
||||
ch = string.sub(f, i, i)
|
||||
elseif string.sub(f, i, i) ~= ch then
|
||||
ch = _sub(f, i, i)
|
||||
elseif _sub(f, i, i) ~= ch then
|
||||
if i == #word + 1 then
|
||||
return
|
||||
end
|
||||
words[#words] = string.sub(f, 1, i - 1)
|
||||
words[#words] = _sub(f, 1, i - 1)
|
||||
return table.concat(words, ' ')
|
||||
end
|
||||
end
|
||||
@@ -539,10 +502,9 @@ local function autocomplete(line)
|
||||
local tw = term.getSize()
|
||||
local nMaxLen = tw / 8
|
||||
for _,sItem in pairs(results) do
|
||||
nMaxLen = math.max(string.len(sItem) + 1, nMaxLen)
|
||||
nMaxLen = math.max(_len(sItem) + 1, nMaxLen)
|
||||
end
|
||||
local w = term.getSize()
|
||||
local nCols = math.floor(w / nMaxLen)
|
||||
local nCols = math.floor(tw / nMaxLen)
|
||||
if #tDirs < nCols then
|
||||
for _ = #tDirs + 1, nCols do
|
||||
table.insert(tDirs, '')
|
||||
@@ -557,11 +519,9 @@ 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
|
||||
@@ -588,17 +548,16 @@ local function shellRead(history)
|
||||
term.setCursorPos(3, cy)
|
||||
entry.value = entry.value or ''
|
||||
local filler = #entry.value < lastLen
|
||||
and string.rep(' ', lastLen - #entry.value)
|
||||
and _rep(' ', lastLen - #entry.value)
|
||||
or ''
|
||||
local str = string.sub(entry.value, entry.scroll + 1, entry.width + entry.scroll) .. filler
|
||||
local str = _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
|
||||
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)
|
||||
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)
|
||||
end
|
||||
term.blit(str, fg, bg)
|
||||
updateCursor()
|
||||
@@ -650,6 +609,11 @@ 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 ''
|
||||
@@ -661,6 +625,7 @@ local function shellRead(history)
|
||||
end
|
||||
|
||||
elseif event == "term_resize" then
|
||||
terminal.reposition(1, 1, oldTerm.getSize())
|
||||
entry.width = term.getSize() - 3
|
||||
entry:updateScroll()
|
||||
redraw()
|
||||
@@ -668,30 +633,26 @@ local function shellRead(history)
|
||||
end
|
||||
|
||||
print()
|
||||
term.setCursorBlink( false )
|
||||
term.setCursorBlink(false)
|
||||
return entry.value or ''
|
||||
end
|
||||
|
||||
local history = History.load('usr/.shell_history', 25)
|
||||
local history = History.load('usr/.shell_history', 100)
|
||||
|
||||
term.setBackgroundColor(_colors.backgroundColor)
|
||||
term.clear()
|
||||
|
||||
if settings.get("motd.enabled") then
|
||||
if settings.get("motd.enable") 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
|
||||
@@ -703,6 +664,11 @@ 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
|
||||
|
||||
@@ -4,7 +4,7 @@ local UI = require('opus.ui')
|
||||
local kernel = _G.kernel
|
||||
|
||||
local aliasTab = UI.Tab {
|
||||
tabTitle = 'Aliases',
|
||||
title = 'Aliases',
|
||||
description = 'Shell aliases',
|
||||
alias = UI.TextEntry {
|
||||
x = 2, y = 2, ex = -2,
|
||||
@@ -13,14 +13,13 @@ local aliasTab = UI.Tab {
|
||||
},
|
||||
path = UI.TextEntry {
|
||||
y = 3, x = 2, ex = -2,
|
||||
limit = 256,
|
||||
shadowText = 'Program path',
|
||||
accelerators = {
|
||||
enter = 'new_alias',
|
||||
},
|
||||
},
|
||||
grid = UI.Grid {
|
||||
y = 5,
|
||||
x = 2, y = 5, ex = -2, ey = -2,
|
||||
sortColumn = 'alias',
|
||||
columns = {
|
||||
{ heading = 'Alias', key = 'alias' },
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
local Array = require('opus.array')
|
||||
local Config = require('opus.config')
|
||||
local UI = require('opus.ui')
|
||||
|
||||
local colors = _G.colors
|
||||
|
||||
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' },
|
||||
}
|
||||
},
|
||||
statusBar = UI.StatusBar {
|
||||
values = 'Double-click to set as preferred'
|
||||
},
|
||||
}
|
||||
|
||||
function tab.choices:getRowTextColor(row)
|
||||
if row == self.values[1] then
|
||||
return colors.yellow
|
||||
end
|
||||
return UI.Grid.getRowTextColor(self, row)
|
||||
end
|
||||
|
||||
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
|
||||
@@ -2,18 +2,17 @@ local Ansi = require('opus.ansi')
|
||||
local Config = require('opus.config')
|
||||
local UI = require('opus.ui')
|
||||
|
||||
local colors = _G.colors
|
||||
|
||||
-- -t80x30
|
||||
|
||||
if _G.http.websocket then
|
||||
local config = Config.load('cloud')
|
||||
|
||||
local tab = UI.Tab {
|
||||
tabTitle = 'Cloud',
|
||||
title = 'Cloud',
|
||||
description = 'Cloud Catcher options',
|
||||
[1] = UI.Window {
|
||||
x = 2, y = 2, ex = -2, ey = 4,
|
||||
},
|
||||
key = UI.TextEntry {
|
||||
x = 3, ex = -3, y = 2,
|
||||
x = 3, ex = -3, y = 3,
|
||||
limit = 32,
|
||||
value = config.key,
|
||||
shadowText = 'Cloud key',
|
||||
@@ -22,14 +21,15 @@ if _G.http.websocket then
|
||||
},
|
||||
},
|
||||
button = UI.Button {
|
||||
x = 3, y = 4,
|
||||
text = 'Update',
|
||||
x = -8, ex = -2, y = -2,
|
||||
text = 'Apply',
|
||||
event = 'update_key',
|
||||
},
|
||||
labelText = UI.TextArea {
|
||||
x = 3, ex = -3, y = 6,
|
||||
textColor = colors.yellow,
|
||||
marginLeft = 0, marginRight = 0,
|
||||
x = 2, ex = -2, y = 5, ey = -4,
|
||||
textColor = 'yellow',
|
||||
backgroundColor = 'black',
|
||||
marginLeft = 1, marginRight = 1, marginTop = 1,
|
||||
value = string.format(
|
||||
[[Use a non-changing cloud key. Note that only a single computer can use this session at one time.
|
||||
To obtain a key, visit:
|
||||
|
||||
@@ -16,12 +16,12 @@ local NftImages = {
|
||||
}
|
||||
|
||||
local tab = UI.Tab {
|
||||
tabTitle = 'Disks Usage',
|
||||
title = 'Disks Usage',
|
||||
description = 'Visualise HDD and disks usage',
|
||||
|
||||
drives = UI.ScrollingGrid {
|
||||
x = 2, y = 1,
|
||||
ex = '47%', ey = -7,
|
||||
x = 2, y = 2,
|
||||
ex = '47%', ey = -8,
|
||||
columns = {
|
||||
{ heading = 'Drive', key = 'name' },
|
||||
{ heading = 'Side' ,key = 'side', textColor = colors.yellow }
|
||||
@@ -30,7 +30,7 @@ local tab = UI.Tab {
|
||||
},
|
||||
infos = UI.Grid {
|
||||
x = '52%', y = 2,
|
||||
ex = -2, ey = -4,
|
||||
ex = -2, ey = -8,
|
||||
disableHeader = true,
|
||||
unfocusedBackgroundSelectedColor = colors.black,
|
||||
inactive = true,
|
||||
@@ -40,18 +40,23 @@ local tab = UI.Tab {
|
||||
{ key = 'value', align = 'right', textColor = colors.yellow },
|
||||
}
|
||||
},
|
||||
|
||||
[1] = UI.Window {
|
||||
x = 2, y = -6, ex = -2, ey = -2,
|
||||
backgroundColor = colors.black,
|
||||
},
|
||||
progress = UI.ProgressBar {
|
||||
x = 11, y = -2,
|
||||
ex = -2,
|
||||
x = 11, y = -3,
|
||||
ex = -3,
|
||||
},
|
||||
percentage = UI.Text {
|
||||
x = 11, y = -3,
|
||||
ex = '47%',
|
||||
align = 'center',
|
||||
y = -4, width = 5,
|
||||
x = 12,
|
||||
--align = 'center',
|
||||
backgroundColor = colors.black,
|
||||
},
|
||||
icon = UI.NftImage {
|
||||
x = 2, y = -5,
|
||||
x = 2, y = -6, ey = -2,
|
||||
backgroundColor = colors.black,
|
||||
image = NFT.parse(NftImages.blank)
|
||||
},
|
||||
}
|
||||
@@ -131,6 +136,17 @@ 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)
|
||||
@@ -142,11 +158,4 @@ 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
|
||||
|
||||
@@ -4,11 +4,11 @@ local colors = _G.colors
|
||||
local peripheral = _G.peripheral
|
||||
local settings = _G.settings
|
||||
|
||||
local tab = UI.Tab {
|
||||
tabTitle = 'Kiosk',
|
||||
return peripheral.find('monitor') and UI.Tab {
|
||||
title = 'Kiosk',
|
||||
description = 'Kiosk options',
|
||||
form = UI.Form {
|
||||
x = 2, ex = -2,
|
||||
x = 2, y = 2, ex = -2, ey = 5,
|
||||
manualControls = true,
|
||||
monitor = UI.Chooser {
|
||||
formLabel = 'Monitor', formKey = 'monitor',
|
||||
@@ -22,41 +22,36 @@ local tab = UI.Tab {
|
||||
},
|
||||
help = 'Adjust text scaling',
|
||||
},
|
||||
labelText = UI.TextArea {
|
||||
x = 2, ex = -2, y = 5,
|
||||
textColor = colors.yellow,
|
||||
value = 'Settings apply to kiosk mode selected during startup'
|
||||
},
|
||||
},
|
||||
}
|
||||
labelText = UI.TextArea {
|
||||
x = 2, ex = -2, y = 7, ey = -2,
|
||||
textColor = colors.yellow,
|
||||
backgroundColor = colors.black,
|
||||
value = 'Settings apply to kiosk mode selected during startup'
|
||||
},
|
||||
enable = function(self)
|
||||
local choices = { }
|
||||
|
||||
function tab:enable()
|
||||
local choices = { }
|
||||
peripheral.find('monitor', function(side)
|
||||
table.insert(choices, { name = side, value = side })
|
||||
end)
|
||||
|
||||
peripheral.find('monitor', function(side)
|
||||
table.insert(choices, { name = side, value = side })
|
||||
end)
|
||||
self.form.monitor.choices = choices
|
||||
self.form.monitor.value = settings.get('kiosk.monitor')
|
||||
|
||||
self.form.monitor.choices = choices
|
||||
self.form.monitor.value = settings.get('kiosk.monitor')
|
||||
self.form.textScale.value = settings.get('kiosk.textscale')
|
||||
|
||||
self.form.textScale.value = settings.get('kiosk.textscale')
|
||||
|
||||
UI.Tab.enable(self)
|
||||
end
|
||||
|
||||
function tab:eventHandler(event)
|
||||
if event.type == 'choice_change' then
|
||||
if self.form.monitor.value then
|
||||
settings.set('kiosk.monitor', self.form.monitor.value)
|
||||
UI.Tab.enable(self)
|
||||
end,
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'choice_change' then
|
||||
if self.form.monitor.value then
|
||||
settings.set('kiosk.monitor', self.form.monitor.value)
|
||||
end
|
||||
if self.form.textScale.value then
|
||||
settings.set('kiosk.textscale', self.form.textScale.value)
|
||||
end
|
||||
settings.save('.settings')
|
||||
end
|
||||
if self.form.textScale.value then
|
||||
settings.set('kiosk.textscale', self.form.textScale.value)
|
||||
end
|
||||
settings.save('.settings')
|
||||
end
|
||||
end
|
||||
|
||||
if peripheral.find('monitor') then
|
||||
return tab
|
||||
end
|
||||
}
|
||||
|
||||
@@ -4,23 +4,26 @@ local Util = require('opus.util')
|
||||
local fs = _G.fs
|
||||
local os = _G.os
|
||||
|
||||
local labelTab = UI.Tab {
|
||||
tabTitle = 'Label',
|
||||
return UI.Tab {
|
||||
title = 'Label',
|
||||
description = 'Set the computer label',
|
||||
labelText = UI.Text {
|
||||
x = 3, y = 2,
|
||||
x = 3, y = 3,
|
||||
value = 'Label'
|
||||
},
|
||||
label = UI.TextEntry {
|
||||
x = 9, y = 2, ex = -4,
|
||||
x = 9, y = 3, ex = -4,
|
||||
limit = 32,
|
||||
value = os.getComputerLabel(),
|
||||
accelerators = {
|
||||
enter = 'update_label',
|
||||
},
|
||||
},
|
||||
[1] = UI.Window {
|
||||
x = 2, y = 2, ex = -2, ey = 4,
|
||||
},
|
||||
grid = UI.ScrollingGrid {
|
||||
y = 3,
|
||||
x = 2, y = 5, ex = -2, ey = -2,
|
||||
values = {
|
||||
{ name = '', value = '' },
|
||||
{ name = 'CC version', value = Util.getVersion() },
|
||||
@@ -30,20 +33,18 @@ local labelTab = UI.Tab {
|
||||
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
|
||||
{ name = 'Day', value = tostring(os.day()) },
|
||||
},
|
||||
disableHeader = true,
|
||||
inactive = true,
|
||||
columns = {
|
||||
{ key = 'name', width = 12 },
|
||||
{ key = 'value' },
|
||||
{ key = 'value', textColor = colors.yellow },
|
||||
},
|
||||
},
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'update_label' and self.label.value then
|
||||
os.setComputerLabel(self.label.value)
|
||||
self:emit({ type = 'success_message', message = 'Label updated' })
|
||||
return true
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
function labelTab:eventHandler(event)
|
||||
if event.type == 'update_label' and self.label.value then
|
||||
os.setComputerLabel(self.label.value)
|
||||
self:emit({ type = 'success_message', message = 'Label updated' })
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return labelTab
|
||||
|
||||
@@ -7,14 +7,17 @@ local fs = _G.fs
|
||||
local config = Config.load('multishell')
|
||||
|
||||
local tab = UI.Tab {
|
||||
tabTitle = 'Launcher',
|
||||
title = 'Launcher',
|
||||
description = 'Set the application launcher',
|
||||
[1] = UI.Window {
|
||||
x = 2, y = 2, ex = -2, ey = 5,
|
||||
},
|
||||
launcherLabel = UI.Text {
|
||||
x = 3, y = 2,
|
||||
x = 3, y = 3,
|
||||
value = 'Launcher',
|
||||
},
|
||||
launcher = UI.Chooser {
|
||||
x = 13, y = 2, width = 12,
|
||||
x = 13, y = 3, width = 12,
|
||||
choices = {
|
||||
{ name = 'Overview', value = 'sys/apps/Overview.lua' },
|
||||
{ name = 'Shell', value = 'sys/apps/ShellLauncher.lua' },
|
||||
@@ -22,18 +25,19 @@ local tab = UI.Tab {
|
||||
},
|
||||
},
|
||||
custom = UI.TextEntry {
|
||||
x = 13, ex = -3, y = 3,
|
||||
limit = 128,
|
||||
x = 13, ex = -3, y = 4,
|
||||
shadowText = 'File name',
|
||||
},
|
||||
button = UI.Button {
|
||||
x = 3, y = 5,
|
||||
text = 'Update',
|
||||
x = -8, ex = -2, y = -2,
|
||||
text = 'Apply',
|
||||
event = 'update',
|
||||
},
|
||||
labelText = UI.TextArea {
|
||||
x = 3, ex = -3, y = 7,
|
||||
x = 2, ex = -2, y = 6, ey = -4,
|
||||
backgroundColor = colors.black,
|
||||
textColor = colors.yellow,
|
||||
marginLeft = 1, marginRight = 1, marginTop = 1,
|
||||
value = 'Choose an application launcher',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,59 +2,61 @@ local Ansi = require('opus.ansi')
|
||||
local Config = require('opus.config')
|
||||
local UI = require('opus.ui')
|
||||
|
||||
local colors = _G.colors
|
||||
local device = _G.device
|
||||
|
||||
local tab = UI.Tab {
|
||||
tabTitle = 'Network',
|
||||
return UI.Tab {
|
||||
title = 'Network',
|
||||
description = 'Networking options',
|
||||
info = UI.TextArea {
|
||||
x = 3, y = 4,
|
||||
x = 2, y = 5, ex = -2, ey = -2,
|
||||
backgroundColor = colors.black,
|
||||
marginLeft = 1, marginRight = 1, marginTop = 1,
|
||||
value = string.format(
|
||||
[[%sSet the primary modem used for wireless communications.%s
|
||||
|
||||
Reboot to take effect.]], Ansi.yellow, Ansi.reset)
|
||||
},
|
||||
[1] = UI.Window {
|
||||
x = 2, y = 2, ex = -2, ey = 4,
|
||||
},
|
||||
label = UI.Text {
|
||||
x = 3, y = 2,
|
||||
x = 3, y = 3,
|
||||
value = 'Modem',
|
||||
},
|
||||
modem = UI.Chooser {
|
||||
x = 10, ex = -3, y = 2,
|
||||
x = 10, ex = -3, y = 3,
|
||||
nochoice = 'auto',
|
||||
},
|
||||
}
|
||||
enable = function(self)
|
||||
local width = 7
|
||||
local choices = {
|
||||
{ name = 'auto', value = 'auto' },
|
||||
{ name = 'disable', value = 'none' },
|
||||
}
|
||||
|
||||
function tab:enable()
|
||||
local width = 7
|
||||
local choices = {
|
||||
{ name = 'auto', value = 'auto' },
|
||||
{ name = 'disable', value = 'none' },
|
||||
}
|
||||
for k,v in pairs(device) do
|
||||
if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
|
||||
table.insert(choices, { name = k, value = v.name })
|
||||
width = math.max(width, #k)
|
||||
end
|
||||
end
|
||||
|
||||
for k,v in pairs(device) do
|
||||
if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
|
||||
table.insert(choices, { name = k, value = v.name })
|
||||
width = math.max(width, #k)
|
||||
self.modem.choices = choices
|
||||
--self.modem.width = width + 4
|
||||
|
||||
local config = Config.load('os')
|
||||
self.modem.value = config.wirelessModem or 'auto'
|
||||
|
||||
UI.Tab.enable(self)
|
||||
end,
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'choice_change' then
|
||||
local config = Config.load('os')
|
||||
config.wirelessModem = self.modem.value
|
||||
Config.update('os', config)
|
||||
self:emit({ type = 'success_message', message = 'reboot to take effect' })
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
self.modem.choices = choices
|
||||
--self.modem.width = width + 4
|
||||
|
||||
local config = Config.load('os')
|
||||
self.modem.value = config.wirelessModem or 'auto'
|
||||
|
||||
UI.Tab.enable(self)
|
||||
end
|
||||
|
||||
function tab:eventHandler(event)
|
||||
if event.type == 'choice_change' then
|
||||
local config = Config.load('os')
|
||||
config.wirelessModem = self.modem.value
|
||||
Config.update('os', config)
|
||||
self:emit({ type = 'success_message', message = 'reboot to take effect' })
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return tab
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ local Security = require('opus.security')
|
||||
local SHA = require('opus.crypto.sha2')
|
||||
local UI = require('opus.ui')
|
||||
|
||||
local colors = _G.colors
|
||||
|
||||
local passwordTab = UI.Tab {
|
||||
tabTitle = 'Password',
|
||||
return UI.Tab {
|
||||
title = 'Password',
|
||||
description = 'Wireless network password',
|
||||
[1] = UI.Window {
|
||||
x = 2, y = 2, ex = -2, ey = 4,
|
||||
},
|
||||
newPass = UI.TextEntry {
|
||||
x = 3, ex = -3, y = 3,
|
||||
limit = 32,
|
||||
@@ -17,28 +18,28 @@ local passwordTab = UI.Tab {
|
||||
},
|
||||
},
|
||||
button = UI.Button {
|
||||
x = 3, y = 5,
|
||||
text = 'Update',
|
||||
x = -8, ex = -2, y = -2,
|
||||
text = 'Apply',
|
||||
event = 'update_password',
|
||||
},
|
||||
info = UI.TextArea {
|
||||
x = 3, ex = -3, y = 7,
|
||||
textColor = colors.yellow,
|
||||
x = 2, ex = -2, y = 5, ey = -4,
|
||||
backgroundColor = 'black',
|
||||
textColor = 'yellow',
|
||||
inactive = true,
|
||||
marginLeft = 1, marginRight = 1, marginTop = 1,
|
||||
value = 'Add a password to enable other computers to connect to this one.',
|
||||
}
|
||||
}
|
||||
function passwordTab:eventHandler(event)
|
||||
if event.type == 'update_password' then
|
||||
if not self.newPass.value or #self.newPass.value == 0 then
|
||||
self:emit({ type = 'error_message', message = 'Invalid password' })
|
||||
},
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'update_password' then
|
||||
if not self.newPass.value or #self.newPass.value == 0 then
|
||||
self:emit({ type = 'error_message', message = 'Invalid password' })
|
||||
|
||||
else
|
||||
Security.updatePassword(SHA.compute(self.newPass.value))
|
||||
self:emit({ type = 'success_message', message = 'Password updated' })
|
||||
else
|
||||
Security.updatePassword(SHA.compute(self.newPass.value))
|
||||
self:emit({ type = 'success_message', message = 'Password updated' })
|
||||
end
|
||||
return true
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return passwordTab
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ local UI = require('opus.ui')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local tab = UI.Tab {
|
||||
tabTitle = 'Path',
|
||||
title = 'Path',
|
||||
description = 'Set the shell path',
|
||||
tabClose = true,
|
||||
[1] = UI.Window {
|
||||
x = 2, y = 2, ex = -2, ey = 4,
|
||||
},
|
||||
entry = UI.TextEntry {
|
||||
x = 2, y = 2, ex = -2,
|
||||
limit = 256,
|
||||
x = 3, y = 3, ex = -3,
|
||||
shadowText = 'enter new path',
|
||||
accelerators = {
|
||||
enter = 'update_path',
|
||||
@@ -16,7 +18,7 @@ local tab = UI.Tab {
|
||||
help = 'add a new path',
|
||||
},
|
||||
grid = UI.Grid {
|
||||
y = 4, ey = -3,
|
||||
x = 2, y = 6, ex = -2, ey = -3,
|
||||
disableHeader = true,
|
||||
columns = { { key = 'value' } },
|
||||
autospace = true,
|
||||
|
||||
@@ -3,12 +3,11 @@ local UI = require('opus.ui')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local tab = UI.Tab {
|
||||
tabTitle = 'Requires',
|
||||
title = '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',
|
||||
|
||||
@@ -2,48 +2,93 @@ local UI = require('opus.ui')
|
||||
|
||||
local settings = _G.settings
|
||||
|
||||
if settings then
|
||||
local settingsTab = UI.Tab {
|
||||
tabTitle = 'Settings',
|
||||
description = 'Computercraft configurable settings',
|
||||
grid = UI.Grid {
|
||||
y = 2,
|
||||
autospace = true,
|
||||
sortColumn = 'name',
|
||||
columns = {
|
||||
{ heading = 'Setting', key = 'name' },
|
||||
{ heading = 'Value', key = 'value' },
|
||||
},
|
||||
},
|
||||
}
|
||||
local transform = {
|
||||
string = tostring,
|
||||
number = tonumber,
|
||||
}
|
||||
|
||||
function settingsTab:enable()
|
||||
return settings and UI.Tab {
|
||||
title = 'Settings',
|
||||
description = 'Computercraft settings',
|
||||
grid = UI.Grid {
|
||||
x = 2, y = 2, ex = -2, ey = -2,
|
||||
sortColumn = 'name',
|
||||
columns = {
|
||||
{ heading = 'Setting', key = 'name' },
|
||||
{ heading = 'Value', key = 'value' },
|
||||
},
|
||||
},
|
||||
editor = UI.SlideOut {
|
||||
y = -6, height = 6,
|
||||
titleBar = UI.TitleBar {
|
||||
event = 'slide_hide',
|
||||
title = 'Enter value',
|
||||
},
|
||||
form = UI.Form {
|
||||
y = 2,
|
||||
value = UI.TextEntry {
|
||||
formIndex = 1,
|
||||
formLabel = 'Value',
|
||||
formKey = 'value',
|
||||
},
|
||||
validateField = function(self, entry)
|
||||
if entry.value then
|
||||
return transform[self.type](entry.value)
|
||||
end
|
||||
return true
|
||||
end,
|
||||
},
|
||||
accelerators = {
|
||||
form_cancel = 'slide_hide',
|
||||
},
|
||||
show = function(self, entry)
|
||||
self.form.type = type(entry.value) or 'string'
|
||||
self.form:setValues(entry)
|
||||
self.titleBar.title = entry.name
|
||||
UI.SlideOut.show(self)
|
||||
end,
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'form_complete' then
|
||||
if not event.values.value then
|
||||
settings.unset(event.values.name)
|
||||
self.parent:reload()
|
||||
else
|
||||
event.values.value = transform[self.form.type](event.values.value)
|
||||
settings.set(event.values.name, event.values.value)
|
||||
end
|
||||
self.parent.grid:draw()
|
||||
self:hide()
|
||||
settings.save('.settings')
|
||||
end
|
||||
return UI.SlideOut.eventHandler(self, event)
|
||||
end,
|
||||
},
|
||||
reload = function(self)
|
||||
local values = { }
|
||||
for _,v in pairs(settings.getNames()) do
|
||||
local value = settings.get(v)
|
||||
if not value then
|
||||
value = false
|
||||
end
|
||||
table.insert(values, {
|
||||
name = v,
|
||||
value = value,
|
||||
value = settings.get(v) or false,
|
||||
})
|
||||
end
|
||||
self.grid:setValues(values)
|
||||
self.grid:setIndex(1)
|
||||
end,
|
||||
enable = function(self)
|
||||
self:reload()
|
||||
UI.Tab.enable(self)
|
||||
end
|
||||
|
||||
function settingsTab:eventHandler(event)
|
||||
end,
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'grid_select' then
|
||||
if not event.selected.value or type(event.selected.value) == 'boolean' then
|
||||
if type(event.selected.value) == 'boolean' then
|
||||
event.selected.value = not event.selected.value
|
||||
settings.set(event.selected.name, event.selected.value)
|
||||
settings.save('.settings')
|
||||
self.grid:draw()
|
||||
else
|
||||
self.editor:show(event.selected)
|
||||
end
|
||||
settings.set(event.selected.name, event.selected.value)
|
||||
settings.save('.settings')
|
||||
self.grid:draw()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return settingsTab
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
@@ -18,9 +18,7 @@ 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,
|
||||
@@ -28,7 +26,7 @@ local defaults = {
|
||||
local _colors = config.color or Util.shallowCopy(defaults)
|
||||
|
||||
local allSettings = { }
|
||||
for k, v in pairs(defaults) do
|
||||
for k in pairs(defaults) do
|
||||
table.insert(allSettings, { name = k })
|
||||
end
|
||||
|
||||
@@ -38,29 +36,34 @@ if not _colors.backgroundColor then
|
||||
_colors.fileColor = colors.white
|
||||
end
|
||||
|
||||
local tab = UI.Tab {
|
||||
tabTitle = 'Shell',
|
||||
return UI.Tab {
|
||||
title = 'Shell',
|
||||
description = 'Shell options',
|
||||
grid1 = UI.ScrollingGrid {
|
||||
y = 2, ey = -10, x = 3, ex = -16,
|
||||
y = 2, ey = -10, x = 2, ex = -17,
|
||||
disableHeader = true,
|
||||
columns = { { key = 'name' } },
|
||||
values = allSettings,
|
||||
sortColumn = 'name',
|
||||
},
|
||||
grid2 = UI.ScrollingGrid {
|
||||
y = 2, ey = -10, x = -14, ex = -3,
|
||||
y = 2, ey = -10, x = -14, ex = -2,
|
||||
disableHeader = true,
|
||||
columns = { { key = 'name' } },
|
||||
values = allColors,
|
||||
sortColumn = 'name',
|
||||
},
|
||||
directoryLabel = UI.Text {
|
||||
x = 2, y = -2,
|
||||
value = 'Display directory',
|
||||
getRowTextColor = function(self, row)
|
||||
local selected = self.parent.grid1:getSelected()
|
||||
if _colors[selected.name] == row.value then
|
||||
return colors.yellow
|
||||
end
|
||||
return UI.Grid.getRowTextColor(self, row)
|
||||
end
|
||||
},
|
||||
directory = UI.Checkbox {
|
||||
x = 20, y = -2,
|
||||
x = 2, y = -2,
|
||||
labelBackgroundColor = colors.black,
|
||||
label = 'Directory',
|
||||
value = config.displayDirectory
|
||||
},
|
||||
reset = UI.Button {
|
||||
@@ -74,69 +77,57 @@ local tab = UI.Tab {
|
||||
event = 'update',
|
||||
},
|
||||
display = UI.Window {
|
||||
x = 3, ex = -3, y = -8, height = 5,
|
||||
x = 2, ex = -2, y = -8, height = 5,
|
||||
draw = function(self)
|
||||
self:clear(_colors.backgroundColor)
|
||||
local offset = 0
|
||||
if config.displayDirectory then
|
||||
self:write(1, 1,
|
||||
'==' .. os.getComputerLabel() .. ':/dir/etc',
|
||||
_colors.backgroundColor, _colors.directoryTextColor)
|
||||
offset = 1
|
||||
end
|
||||
|
||||
self:write(1, 1 + offset, '$ ',
|
||||
_colors.backgroundColor, _colors.promptTextColor)
|
||||
|
||||
self:write(3, 1 + offset, 'ls /',
|
||||
_colors.backgroundColor, _colors.commandTextColor)
|
||||
|
||||
self:write(1, 2 + offset, 'sys usr',
|
||||
_colors.backgroundColor, _colors.directoryColor)
|
||||
|
||||
self:write(1, 3 + offset, 'startup',
|
||||
_colors.backgroundColor, _colors.fileColor)
|
||||
end,
|
||||
},
|
||||
eventHandler = function(self, event)
|
||||
if event.type =='checkbox_change' then
|
||||
config.displayDirectory = not not event.checked
|
||||
self.display:draw()
|
||||
|
||||
elseif event.type == 'grid_focus_row' and event.element == self.grid1 then
|
||||
self.grid2:draw()
|
||||
|
||||
elseif event.type == 'grid_select' and event.element == self.grid2 then
|
||||
_colors[self.grid1:getSelected().name] = event.selected.value
|
||||
self.display:draw()
|
||||
self.grid2:draw()
|
||||
|
||||
elseif event.type == 'reset' then
|
||||
config.color = defaults
|
||||
config.displayDirectory = true
|
||||
self.directory.value = true
|
||||
_colors = Util.shallowCopy(defaults)
|
||||
|
||||
Config.update('shellprompt', config)
|
||||
self:draw()
|
||||
|
||||
elseif event.type == 'update' then
|
||||
config.color = _colors
|
||||
Config.update('shellprompt', config)
|
||||
|
||||
end
|
||||
return UI.Tab.eventHandler(self, event)
|
||||
end
|
||||
}
|
||||
|
||||
function tab.grid2:getRowTextColor(row)
|
||||
local selected = tab.grid1:getSelected()
|
||||
if _colors[selected.name] == row.value then
|
||||
return colors.yellow
|
||||
end
|
||||
return UI.Grid.getRowTextColor(self, row)
|
||||
end
|
||||
|
||||
function tab.display:draw()
|
||||
self:clear(_colors.backgroundColor)
|
||||
local offset = 0
|
||||
if config.displayDirectory then
|
||||
self:write(1, 1,
|
||||
'==' .. os.getComputerLabel() .. ':/dir/etc',
|
||||
_colors.directoryBackgroundColor, _colors.directoryTextColor)
|
||||
offset = 1
|
||||
end
|
||||
|
||||
self:write(1, 1 + offset, '$ ',
|
||||
_colors.promptBackgroundColor, _colors.promptTextColor)
|
||||
|
||||
self:write(3, 1 + offset, 'ls /',
|
||||
_colors.backgroundColor, _colors.commandTextColor)
|
||||
|
||||
self:write(1, 2 + offset, 'sys usr',
|
||||
_colors.backgroundColor, _colors.directoryColor)
|
||||
|
||||
self:write(1, 3 + offset, 'startup',
|
||||
_colors.backgroundColor, _colors.fileColor)
|
||||
end
|
||||
|
||||
function tab:eventHandler(event)
|
||||
if event.type =='checkbox_change' then
|
||||
config.displayDirectory = not not event.checked
|
||||
self.display:draw()
|
||||
|
||||
elseif event.type == 'grid_focus_row' and event.element == self.grid1 then
|
||||
self.grid2:draw()
|
||||
|
||||
elseif event.type == 'grid_select' and event.element == self.grid2 then
|
||||
_colors[tab.grid1:getSelected().name] = event.selected.value
|
||||
self.display:draw()
|
||||
self.grid2:draw()
|
||||
|
||||
elseif event.type == 'reset' then
|
||||
config.color = defaults
|
||||
config.displayDirectory = true
|
||||
self.directory.value = true
|
||||
_colors = Util.shallowCopy(defaults)
|
||||
|
||||
Config.update('shellprompt', config)
|
||||
self:draw()
|
||||
|
||||
elseif event.type == 'update' then
|
||||
config.color = _colors
|
||||
Config.update('shellprompt', config)
|
||||
|
||||
end
|
||||
return UI.Tab.eventHandler(self, event)
|
||||
end
|
||||
|
||||
return tab
|
||||
|
||||
89
sys/apps/system/theme.lua
Normal file
89
sys/apps/system/theme.lua
Normal file
@@ -0,0 +1,89 @@
|
||||
local Config = require('opus.config')
|
||||
local UI = require('opus.ui')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local colors = _G.colors
|
||||
|
||||
local allColors = { }
|
||||
for k,v in pairs(colors) do
|
||||
if type(v) == 'number' then
|
||||
table.insert(allColors, { name = k, value = v })
|
||||
end
|
||||
end
|
||||
|
||||
local allSettings = { }
|
||||
for k,v in pairs(UI.theme.colors) do
|
||||
allSettings[k] = { name = k, value = v }
|
||||
end
|
||||
|
||||
return UI.Tab {
|
||||
title = 'Theme',
|
||||
description = 'Theme colors',
|
||||
grid1 = UI.ScrollingGrid {
|
||||
y = 2, ey = -10, x = 2, ex = -17,
|
||||
disableHeader = true,
|
||||
columns = { { key = 'name' } },
|
||||
values = allSettings,
|
||||
sortColumn = 'name',
|
||||
},
|
||||
grid2 = UI.ScrollingGrid {
|
||||
y = 2, ey = -10, x = -14, ex = -2,
|
||||
disableHeader = true,
|
||||
columns = { { key = 'name' } },
|
||||
values = allColors,
|
||||
sortColumn = 'name',
|
||||
getRowTextColor = function(self, row)
|
||||
local selected = self.parent.grid1:getSelected()
|
||||
if selected.value == row.value then
|
||||
return colors.yellow
|
||||
end
|
||||
return UI.Grid.getRowTextColor(self, row)
|
||||
end
|
||||
},
|
||||
button = UI.Button {
|
||||
x = -9, y = -2,
|
||||
text = 'Update',
|
||||
event = 'update',
|
||||
},
|
||||
display = UI.Window {
|
||||
x = 2, ex = -2, y = -8, height = 5,
|
||||
textColor = colors.black,
|
||||
backgroundColor = colors.black,
|
||||
draw = function(self)
|
||||
self:clear()
|
||||
|
||||
self:write(1, 1, Util.widthify(' Local Global Device', self.width),
|
||||
allSettings.secondary.value)
|
||||
|
||||
self:write(2, 2, 'enter command ',
|
||||
colors.black, colors.gray)
|
||||
|
||||
self:write(1, 3, ' Formatted ',
|
||||
allSettings.primary.value)
|
||||
|
||||
self:write(12, 3, Util.widthify(' Output ', self.width - 11),
|
||||
allSettings.tertiary.value)
|
||||
|
||||
self:write(1, 4, Util.widthify(' Key', self.width),
|
||||
allSettings.primary.value)
|
||||
end,
|
||||
},
|
||||
eventHandler = function(self, event)
|
||||
if event.type == 'grid_focus_row' and event.element == self.grid1 then
|
||||
self.grid2:draw()
|
||||
|
||||
elseif event.type == 'grid_select' and event.element == self.grid2 then
|
||||
self.grid1:getSelected().value = event.selected.value
|
||||
self.display:draw()
|
||||
self.grid2:draw()
|
||||
|
||||
elseif event.type == 'update' then
|
||||
local config = Config.load('ui.theme', { colors = { } })
|
||||
for k,v in pairs(allSettings) do
|
||||
config.colors[k] = v.value
|
||||
end
|
||||
Config.update('ui.theme', config)
|
||||
end
|
||||
return UI.Tab.eventHandler(self, event)
|
||||
end
|
||||
}
|
||||
55
sys/apps/system/trust.lua
Normal file
55
sys/apps/system/trust.lua
Normal file
@@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
@@ -9,7 +9,7 @@ kernel.hook('clipboard_copy', function(_, args)
|
||||
keyboard.clipboard = args[1]
|
||||
end)
|
||||
|
||||
keyboard.addHotkey('shift-paste', function()
|
||||
local function queuePaste()
|
||||
local data = keyboard.clipboard
|
||||
|
||||
if type(data) == 'table' then
|
||||
@@ -20,4 +20,7 @@ keyboard.addHotkey('shift-paste', function()
|
||||
if data then
|
||||
os.queueEvent('paste', data)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
kernel.hook('clipboard_paste', queuePaste)
|
||||
keyboard.addHotkey('shift-paste', queuePaste)
|
||||
|
||||
@@ -10,9 +10,8 @@ if multishell and multishell.getTabs then
|
||||
local tab = kernel.getFocused()
|
||||
if tab and not tab.noTerminate then
|
||||
multishell.terminate(tab.uid)
|
||||
multishell.openTab({
|
||||
multishell.openTab(tab.env, {
|
||||
path = tab.path,
|
||||
env = tab.env,
|
||||
args = tab.args,
|
||||
focused = true,
|
||||
})
|
||||
|
||||
@@ -4,29 +4,18 @@
|
||||
|
||||
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 and not currentTab.terminal.noAutoScroll then
|
||||
if currentTab.terminal.scrollUp then
|
||||
if dir == -1 then
|
||||
currentTab.terminal.scrollUp()
|
||||
else
|
||||
@@ -50,7 +39,7 @@ local function systemLog()
|
||||
keyboard.removeHotkey('control-d')
|
||||
end
|
||||
|
||||
kernel.run({
|
||||
kernel.run(_ENV, {
|
||||
title = 'System Log',
|
||||
fn = systemLog,
|
||||
noTerminate = true,
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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')
|
||||
53
sys/autorun/version.lua
Normal file
53
sys/autorun/version.lua
Normal file
@@ -0,0 +1,53 @@
|
||||
local Config = require('opus.config')
|
||||
local Util = require('opus.util')
|
||||
|
||||
local fs = _G.fs
|
||||
local shell = _ENV.shell
|
||||
|
||||
local URL = 'https://raw.githubusercontent.com/kepler155c/opus/%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
|
||||
@@ -21,7 +21,7 @@ if mon then
|
||||
|
||||
parallel.waitForAny(
|
||||
function()
|
||||
os.run(_ENV, '/sys/boot/opus.boot')
|
||||
os.run(_ENV, '/sys/boot/opus.lua')
|
||||
end,
|
||||
|
||||
function()
|
||||
@@ -36,5 +36,5 @@ if mon then
|
||||
end
|
||||
)
|
||||
else
|
||||
os.run(_ENV, '/sys/boot/opus.boot')
|
||||
os.run(_ENV, '/sys/boot/opus.lua')
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
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
|
||||
58
sys/boot/opus.lua
Normal file
58
sys/boot/opus.lua
Normal file
@@ -0,0 +1,58 @@
|
||||
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
|
||||
@@ -1,15 +0,0 @@
|
||||
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')
|
||||
30
sys/boot/tlco.lua
Normal file
30
sys/boot/tlco.lua
Normal file
@@ -0,0 +1,30 @@
|
||||
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()
|
||||
@@ -50,7 +50,10 @@
|
||||
title = "System",
|
||||
category = "System",
|
||||
icon = "\030 \0307\031f| \010\0307\031f---o\030 \031 \010\030 \009 \0307\031f| ",
|
||||
iconExt = "\030 \0318\138\0308\031 \130\0318\128\031 \129\030 \0318\133\010\030 \0318\143\0308\128\0317\143\0318\128\030 \143\010\030 \0318\138\135\143\139\133",
|
||||
iconExt = "22070b02424\
|
||||
0277044\
|
||||
7071724",
|
||||
--iconExt = "\030 \0318\138\0308\031 \130\0318\128\031 \129\030 \0318\133\010\030 \0318\143\0308\128\0317\143\0318\128\030 \143\010\030 \0318\138\135\143\139\133",
|
||||
run = "System.lua",
|
||||
},
|
||||
[ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = {
|
||||
@@ -133,4 +136,10 @@
|
||||
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",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,4 @@ sys/apps/pain.lua urlfs https://github.com/LDDestroier/CC/raw/master/pain.lua
|
||||
sys/apps/update.lua urlfs http://pastebin.com/raw/UzGHLbNC
|
||||
sys/apps/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
|
||||
9
sys/help/Networking.txt
Normal file
9
sys/help/Networking.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
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>
|
||||
@@ -1,30 +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 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.
|
||||
@@ -6,17 +6,7 @@ Shortcut Keys
|
||||
* Control-d: Show/toggle logging screen
|
||||
* Control-c: Copy (in most applications)
|
||||
* Control-shift-v: Paste from internal clipboard
|
||||
* 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>
|
||||
* Control-shift-click: Open the clicked UI element in Lua
|
||||
|
||||
Running Custom Programs
|
||||
=======================
|
||||
@@ -6,10 +6,11 @@ Shortcut keys
|
||||
* l: Lua application
|
||||
* f: Files
|
||||
* e: Edit an application (or right-click)
|
||||
* n: Network
|
||||
* control-n: Add a new application
|
||||
* delete: Delete an application
|
||||
|
||||
Adding a new application
|
||||
========================
|
||||
The run entry can be either a disk file or a URL.
|
||||
Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters.
|
||||
Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. Magenta is used for transparency.
|
||||
13
sys/help/Packages.txt
Normal file
13
sys/help/Packages.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
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.
|
||||
@@ -1,5 +1,3 @@
|
||||
_G.requireInjector(_ENV)
|
||||
|
||||
local Peripheral = require('opus.peripheral')
|
||||
|
||||
_G.device = Peripheral.getList()
|
||||
|
||||
@@ -4,11 +4,8 @@ if fs.native then
|
||||
return
|
||||
end
|
||||
|
||||
_G.requireInjector(_ENV)
|
||||
local Util = require('opus.util')
|
||||
|
||||
-- TODO: support getDrive for virtual nodes
|
||||
|
||||
fs.native = Util.shallowCopy(fs)
|
||||
|
||||
local fstypes = { }
|
||||
@@ -22,8 +19,11 @@ for k,fn in pairs(fs) do
|
||||
end
|
||||
end
|
||||
|
||||
function nativefs.list(node, dir)
|
||||
function nativefs.resolve(_, dir)
|
||||
return dir
|
||||
end
|
||||
|
||||
function nativefs.list(node, dir)
|
||||
local files
|
||||
if fs.native.isDir(dir) then
|
||||
files = fs.native.list(dir)
|
||||
@@ -83,6 +83,18 @@ 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
|
||||
@@ -138,8 +150,10 @@ local function getNode(dir)
|
||||
return node
|
||||
end
|
||||
|
||||
fs.getNode = getNode
|
||||
|
||||
local methods = { 'delete', 'getFreeSpace', 'exists', 'isDir', 'getSize',
|
||||
'isReadOnly', 'makeDir', 'getDrive', 'list', 'open' }
|
||||
'isReadOnly', 'makeDir', 'getDrive', 'list', 'open', 'attributes' }
|
||||
|
||||
for _,m in pairs(methods) do
|
||||
fs[m] = function(dir, ...)
|
||||
@@ -149,6 +163,12 @@ 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)
|
||||
@@ -158,6 +178,13 @@ 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)
|
||||
@@ -168,20 +195,22 @@ function fs.listEx(dir)
|
||||
local t = { }
|
||||
local files = node.fs.list(node, dir)
|
||||
|
||||
pcall(function()
|
||||
for _,f in ipairs(files) do
|
||||
for _,f in ipairs(files) do
|
||||
pcall(function()
|
||||
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
|
||||
|
||||
@@ -206,12 +235,12 @@ function fs.copy(s, t)
|
||||
end
|
||||
|
||||
else
|
||||
local sf = Util.readFile(s)
|
||||
local sf = Util.readFile(s, 'rb')
|
||||
if not sf then
|
||||
error('No such file')
|
||||
end
|
||||
|
||||
Util.writeFile(t, sf)
|
||||
Util.writeFile(t, sf, 'wb')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -265,11 +294,17 @@ local function getfstype(fstype)
|
||||
end
|
||||
|
||||
function fs.mount(path, fstype, ...)
|
||||
|
||||
local vfs = getfstype(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)
|
||||
@@ -284,12 +319,16 @@ 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
|
||||
@@ -355,4 +394,4 @@ function fs.restore()
|
||||
local native = fs.native
|
||||
Util.clear(fs)
|
||||
Util.merge(fs, native)
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,46 @@
|
||||
local fs = _G.fs
|
||||
local fs = _G.fs
|
||||
local os = _G.os
|
||||
|
||||
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
|
||||
@@ -10,6 +10,13 @@ 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
|
||||
@@ -45,5 +52,3 @@ 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')
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
_G.requireInjector(_ENV)
|
||||
|
||||
local Config = require('opus.config')
|
||||
|
||||
local device = _G.device
|
||||
@@ -17,7 +15,7 @@ do
|
||||
end
|
||||
|
||||
local function startNetwork()
|
||||
kernel.run({
|
||||
kernel.run(_ENV, {
|
||||
title = 'Net daemon',
|
||||
path = 'sys/apps/netdaemon.lua',
|
||||
hidden = true,
|
||||
|
||||
31
sys/init/5.unpackage.lua
Normal file
31
sys/init/5.unpackage.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
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
|
||||
@@ -13,8 +13,14 @@ 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')
|
||||
|
||||
local apiPath = fs.combine(packageDir, 'apis') -- TODO: rename dir to 'modules' (someday)
|
||||
if fs.exists(apiPath) then
|
||||
fs.mount(fs.combine('rom/modules/main', name), 'linkfs', apiPath)
|
||||
end
|
||||
@@ -23,12 +29,27 @@ 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, ':'))
|
||||
shell.setPath(table.concat(appPaths, ':'))
|
||||
|
||||
local function runDir(directory)
|
||||
local files = fs.list(directory)
|
||||
table.sort(files)
|
||||
|
||||
for _,file in ipairs(files) do
|
||||
os.sleep(0)
|
||||
local result, err = shell.run(directory .. '/' .. file)
|
||||
if not result and err then
|
||||
_G.printError('\n' .. err)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, package in pairs(Packages:installedSorted()) do
|
||||
local packageDir = 'packages/' .. package.name .. '/init'
|
||||
if fs.exists(packageDir) and fs.isDir(packageDir) then
|
||||
runDir(packageDir)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
_G.requireInjector(_ENV)
|
||||
|
||||
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
|
||||
@@ -10,7 +8,6 @@ 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
|
||||
@@ -20,9 +17,9 @@ local tabsDirty = false
|
||||
local closeInd = Util.getVersion() >= 1.76 and '\215' or '*'
|
||||
local multishell = { }
|
||||
|
||||
shell.setEnv('multishell', multishell)
|
||||
_ENV.multishell = multishell
|
||||
|
||||
multishell.term = parentTerm --deprecated use device.terminal
|
||||
kernel.window.reposition(1, 2, w, h - 1)
|
||||
|
||||
local config = {
|
||||
standard = {
|
||||
@@ -47,6 +44,7 @@ local config = {
|
||||
Config.load('multishell', config)
|
||||
|
||||
local _colors = parentTerm.isColor() and config.color or config.standard
|
||||
local palette = parentTerm.isColor() and Blit.colorPalette or Blit.grayscalePalette
|
||||
|
||||
local function redrawMenu()
|
||||
if not tabsDirty then
|
||||
@@ -94,57 +92,70 @@ function multishell.getTabs()
|
||||
return kernel.routines
|
||||
end
|
||||
|
||||
function multishell.launch( tProgramEnv, sProgramPath, ... )
|
||||
function multishell.launch(env, path, ...)
|
||||
-- backwards compatibility
|
||||
return multishell.openTab({
|
||||
env = tProgramEnv,
|
||||
path = sProgramPath,
|
||||
return multishell.openTab(env, {
|
||||
path = path,
|
||||
args = { ... },
|
||||
})
|
||||
end
|
||||
|
||||
local function xprun(env, path, ...)
|
||||
setmetatable(env, { __index = _G })
|
||||
local fn, m = loadfile(path, env)
|
||||
if fn then
|
||||
return trace(fn, ...)
|
||||
local function chain(orig, fn)
|
||||
if not orig then
|
||||
return fn
|
||||
end
|
||||
return fn, m
|
||||
|
||||
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
|
||||
})
|
||||
end
|
||||
|
||||
function multishell.openTab(tab)
|
||||
function multishell.openTab(env, 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)
|
||||
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)
|
||||
-- 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')
|
||||
if tonumber(err) then
|
||||
tab.terminal.setTextColor(colors.orange)
|
||||
print('Process exited with error code: ' .. err)
|
||||
printError('Process exited with error code: ' .. err)
|
||||
elseif err then
|
||||
printError(tostring(err))
|
||||
end
|
||||
tab.terminal.setTextColor(colors.white)
|
||||
print('\nPress enter to close')
|
||||
routine.isDead = true
|
||||
routine.hidden = false
|
||||
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
|
||||
redrawMenu()
|
||||
while true do
|
||||
local e, code = os.pullEventRaw('key')
|
||||
@@ -155,14 +166,17 @@ function multishell.openTab(tab)
|
||||
end
|
||||
end)
|
||||
|
||||
kernel.launch(routine)
|
||||
local routine, message = kernel.run(env, tab)
|
||||
|
||||
if tab.focused then
|
||||
multishell.setFocus(routine.uid)
|
||||
else
|
||||
redrawMenu()
|
||||
if routine then
|
||||
if tab.focused then
|
||||
multishell.setFocus(routine.uid)
|
||||
else
|
||||
redrawMenu()
|
||||
end
|
||||
end
|
||||
return routine.uid
|
||||
|
||||
return routine and routine.uid, message
|
||||
end
|
||||
|
||||
function multishell.hideTab(tabId)
|
||||
@@ -207,17 +221,11 @@ end)
|
||||
kernel.hook('multishell_redraw', function()
|
||||
tabsDirty = false
|
||||
|
||||
local function write(x, text, bg, fg)
|
||||
parentTerm.setBackgroundColor(bg)
|
||||
parentTerm.setTextColor(fg)
|
||||
parentTerm.setCursorPos(x, 1)
|
||||
parentTerm.write(text)
|
||||
end
|
||||
|
||||
local bg = _colors.tabBarBackgroundColor
|
||||
parentTerm.setBackgroundColor(bg)
|
||||
parentTerm.setCursorPos(1, 1)
|
||||
parentTerm.clearLine()
|
||||
local blit = Blit(w, {
|
||||
bg = _colors.tabBarBackgroundColor,
|
||||
fg = _colors.textColor,
|
||||
palette = palette,
|
||||
})
|
||||
|
||||
local currentTab = kernel.getFocused()
|
||||
|
||||
@@ -254,21 +262,27 @@ kernel.hook('multishell_redraw', function()
|
||||
tabX = tabX + tab.width
|
||||
if tab ~= currentTab then
|
||||
local textColor = tab.isDead and _colors.errorColor or _colors.textColor
|
||||
write(tab.sx, tab.title:sub(1, tab.width - 1),
|
||||
blit:write(tab.sx, tab.title:sub(1, tab.width - 1),
|
||||
_colors.backgroundColor, textColor)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if currentTab then
|
||||
write(currentTab.sx - 1,
|
||||
' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ',
|
||||
_colors.focusBackgroundColor, _colors.focusTextColor)
|
||||
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)
|
||||
end
|
||||
if not currentTab.noTerminate then
|
||||
write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor)
|
||||
blit:write(w, closeInd, nil, _colors.focusTextColor)
|
||||
end
|
||||
end
|
||||
|
||||
parentTerm.setCursorPos(1, 1)
|
||||
parentTerm.blit(blit.text, blit.fg, blit.bg)
|
||||
|
||||
if currentTab and currentTab.window then
|
||||
currentTab.window.restoreCursor()
|
||||
end
|
||||
@@ -297,56 +311,65 @@ kernel.hook('term_resize', function(_, eventData)
|
||||
end)
|
||||
|
||||
kernel.hook('mouse_click', function(_, eventData)
|
||||
local x, y = eventData[2], eventData[3]
|
||||
if not eventData[4] then
|
||||
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
|
||||
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
|
||||
end
|
||||
return true
|
||||
end
|
||||
return true
|
||||
eventData[3] = eventData[3] - 1
|
||||
end
|
||||
eventData[3] = eventData[3] - 1
|
||||
end)
|
||||
|
||||
kernel.hook({ 'mouse_up', 'mouse_drag' }, function(_, eventData)
|
||||
eventData[3] = eventData[3] - 1
|
||||
if not eventData[4] then
|
||||
eventData[3] = eventData[3] - 1
|
||||
end
|
||||
end)
|
||||
|
||||
kernel.hook('mouse_scroll', function(_, eventData)
|
||||
if eventData[3] == 1 then
|
||||
return true
|
||||
if not eventData[4] then
|
||||
if eventData[3] == 1 then
|
||||
return true
|
||||
end
|
||||
eventData[3] = eventData[3] - 1
|
||||
end
|
||||
eventData[3] = eventData[3] - 1
|
||||
end)
|
||||
|
||||
kernel.hook('kernel_ready', function()
|
||||
local env = Util.shallowCopy(shell.getEnv())
|
||||
_G.requireInjector(env)
|
||||
|
||||
overviewId = multishell.openTab({
|
||||
path = config.launcher or 'sys/apps/Overview.lua',
|
||||
overviewId = multishell.openTab(_ENV, {
|
||||
path = 'sys/apps/shell.lua',
|
||||
args = { config.launcher or 'sys/apps/Overview.lua' },
|
||||
isOverview = true,
|
||||
noTerminate = true,
|
||||
focused = true,
|
||||
title = '+',
|
||||
env = env,
|
||||
onExit = function(_, s, m)
|
||||
if not s then
|
||||
kernel.halt(s, m)
|
||||
end
|
||||
end,
|
||||
})
|
||||
multishell.setTitle(overviewId, '+')
|
||||
|
||||
multishell.openTab({
|
||||
multishell.openTab(_ENV, {
|
||||
path = 'sys/apps/shell.lua',
|
||||
args = { 'sys/apps/autorun.lua' },
|
||||
title = 'Autorun',
|
||||
|
||||
183
sys/kernel.lua
183
sys/kernel.lua
@@ -1,7 +1,6 @@
|
||||
_G.requireInjector(_ENV)
|
||||
|
||||
local Array = require('opus.array')
|
||||
local Terminal = require('opus.terminal')
|
||||
local trace = require('opus.trace')
|
||||
local Util = require('opus.util')
|
||||
|
||||
_G.kernel = {
|
||||
@@ -21,7 +20,7 @@ local w, h = term.getSize()
|
||||
kernel.terminal = term.current()
|
||||
|
||||
kernel.window = Terminal.window(kernel.terminal, 1, 1, w, h, false)
|
||||
kernel.window.setMaxScroll(100)
|
||||
kernel.window.setMaxScroll(200)
|
||||
|
||||
local focusedRoutineEvents = Util.transpose {
|
||||
'char', 'key', 'key_up',
|
||||
@@ -30,10 +29,8 @@ local focusedRoutineEvents = Util.transpose {
|
||||
}
|
||||
|
||||
_G._syslog = function(pattern, ...)
|
||||
local oldTerm = term.redirect(kernel.window)
|
||||
kernel.window.scrollBottom()
|
||||
Util.print(pattern, ...)
|
||||
term.redirect(oldTerm)
|
||||
kernel.window.print(Util.tostring(pattern, ...))
|
||||
end
|
||||
|
||||
-- any function that runs in a kernel hook does not run in
|
||||
@@ -52,19 +49,23 @@ function kernel.hook(event, fn)
|
||||
end
|
||||
end
|
||||
|
||||
-- you can only unhook from within the function that hooked
|
||||
-- you *should* only unhook from within the function that hooked
|
||||
function kernel.unhook(event, fn)
|
||||
local eventHooks = kernel.hooks[event]
|
||||
if eventHooks then
|
||||
Array.removeByValue(eventHooks, fn)
|
||||
if #eventHooks == 0 then
|
||||
kernel.hooks[event] = nil
|
||||
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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local Routine = { }
|
||||
|
||||
local function switch(routine, previous)
|
||||
if routine then
|
||||
if previous and previous.window then
|
||||
@@ -82,6 +83,8 @@ 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
|
||||
@@ -91,35 +94,64 @@ function Routine:resume(event, ...)
|
||||
local previousTerm = term.redirect(self.terminal)
|
||||
|
||||
local previous = kernel.running
|
||||
kernel.running = self -- stupid shell set title
|
||||
kernel.running = self
|
||||
local ok, result = coroutine.resume(self.co, event, ...)
|
||||
kernel.running = previous
|
||||
|
||||
if ok then
|
||||
self.filter = result
|
||||
else
|
||||
_G.printError(result)
|
||||
end
|
||||
|
||||
self.filter = result
|
||||
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
|
||||
@@ -132,51 +164,34 @@ function kernel.getShell()
|
||||
return shell
|
||||
end
|
||||
|
||||
function kernel.newRoutine(args)
|
||||
-- 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)
|
||||
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 Util.shallowCopy(shell.getEnv())
|
||||
routine.env = args.env or kernel.makeEnv(env, routine.path and fs.getDir(routine.path))
|
||||
routine.terminal = routine.terminal or routine.window
|
||||
|
||||
return routine
|
||||
end
|
||||
|
||||
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
|
||||
function kernel.run(env, args)
|
||||
local routine = kernel.newRoutine(env, args)
|
||||
local s, m = routine:run()
|
||||
return s and routine, m
|
||||
end
|
||||
|
||||
function kernel.raise(uid)
|
||||
@@ -194,8 +209,6 @@ function kernel.raise(uid)
|
||||
end
|
||||
|
||||
switch(routine, previous)
|
||||
-- local previous = eventData[2]
|
||||
-- local routine = kernel.find(previous)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
@@ -223,8 +236,8 @@ function kernel.find(uid)
|
||||
return Util.find(kernel.routines, 'uid', uid)
|
||||
end
|
||||
|
||||
function kernel.halt()
|
||||
os.queueEvent('kernel_halt')
|
||||
function kernel.halt(status, message)
|
||||
os.queueEvent('kernel_halt', status, message)
|
||||
end
|
||||
|
||||
function kernel.event(event, eventData)
|
||||
@@ -266,19 +279,24 @@ function kernel.event(event, eventData)
|
||||
end
|
||||
|
||||
function kernel.start()
|
||||
local s, m = pcall(function()
|
||||
local s, m
|
||||
local s2, m2 = 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 then
|
||||
if (not s and m) or (not s2 and m2) then
|
||||
kernel.window.setVisible(true)
|
||||
term.redirect(kernel.window)
|
||||
print('\nCrash detected\n')
|
||||
_G.printError(m)
|
||||
_G.printError(m or m2)
|
||||
end
|
||||
term.redirect(kernel.terminal)
|
||||
end
|
||||
@@ -295,9 +313,10 @@ 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)
|
||||
error(m, -1)
|
||||
end
|
||||
os.sleep(0)
|
||||
end
|
||||
@@ -311,15 +330,15 @@ local function init(...)
|
||||
term.redirect(kernel.window)
|
||||
shell.run('sys/apps/autorun.lua')
|
||||
|
||||
local shellWindow = window.create(kernel.terminal, 1, 1, w, h, false)
|
||||
local s, m = kernel.run({
|
||||
local win = window.create(kernel.terminal, 1, 1, w, h, true)
|
||||
local s, m = kernel.run(_ENV, {
|
||||
title = args[1],
|
||||
path = 'sys/apps/shell.lua',
|
||||
args = args,
|
||||
haltOnExit = true,
|
||||
haltOnError = true,
|
||||
terminal = shellWindow,
|
||||
window = shellWindow,
|
||||
window = win,
|
||||
onExit = function(_, s, m)
|
||||
kernel.halt(s, m)
|
||||
end,
|
||||
})
|
||||
if s then
|
||||
kernel.raise(s.uid)
|
||||
@@ -330,11 +349,15 @@ local function init(...)
|
||||
end
|
||||
end
|
||||
|
||||
kernel.run({
|
||||
kernel.run(_ENV, {
|
||||
fn = init,
|
||||
title = 'init',
|
||||
haltOnError = true,
|
||||
args = { ... },
|
||||
onExit = function(_, status, message)
|
||||
if not status then
|
||||
kernel.halt(status, message)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
kernel.start()
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
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
|
||||
@@ -1,3 +1,5 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local Array = { }
|
||||
|
||||
function Array.filter(it, f)
|
||||
@@ -14,9 +16,11 @@ function Array.removeByValue(t, e)
|
||||
for k,v in pairs(t) do
|
||||
if v == e then
|
||||
table.remove(t, k)
|
||||
break
|
||||
return e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Array.find = Util.find
|
||||
|
||||
return Array
|
||||
|
||||
142
sys/modules/opus/compress/lzw.lua
Normal file
142
sys/modules/opus/compress/lzw.lua
Normal file
@@ -0,0 +1,142 @@
|
||||
-- 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,
|
||||
}
|
||||
270
sys/modules/opus/compress/tar.lua
Normal file
270
sys/modules/opus/compress/tar.lua
Normal file
@@ -0,0 +1,270 @@
|
||||
|
||||
-- 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,
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local fs = _G.fs
|
||||
local shell = _ENV.shell
|
||||
|
||||
local Config = { }
|
||||
|
||||
@@ -25,23 +24,6 @@ function Config.load(fname, data)
|
||||
return data
|
||||
end
|
||||
|
||||
function Config.loadWithCheck(fname, data)
|
||||
local filename = 'usr/config/' .. fname
|
||||
|
||||
if not fs.exists(filename) then
|
||||
Config.load(fname, data)
|
||||
print()
|
||||
print('The configuration file has been created.')
|
||||
print('The file name is: ' .. filename)
|
||||
print()
|
||||
_G.printError('Press enter to configure')
|
||||
_G.read()
|
||||
shell.run('edit ' .. filename)
|
||||
end
|
||||
|
||||
return Config.load(fname, data)
|
||||
end
|
||||
|
||||
function Config.update(fname, data)
|
||||
local filename = 'usr/config/' .. fname
|
||||
Util.writeTable(filename, data)
|
||||
|
||||
@@ -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(function() _syslog('throttle') end)
|
||||
local throttle = Util.throttle()
|
||||
local out = {}
|
||||
local state = initState(key, nonce, cntr)
|
||||
local blockAmt = math.floor(#data/64)
|
||||
|
||||
@@ -41,16 +41,21 @@ end
|
||||
|
||||
function Entry:updateScroll()
|
||||
local ps = self.scroll
|
||||
local value = _val(self.value)
|
||||
if self.pos > #value then
|
||||
self.pos = #value
|
||||
local len = #_val(self.value)
|
||||
if self.pos > len then
|
||||
self.pos = len
|
||||
self.scroll = 0 -- ??
|
||||
end
|
||||
if self.pos - self.scroll > self.width then
|
||||
self.scroll = self.pos - self.width
|
||||
self.scroll = math.max(0, 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)
|
||||
end
|
||||
end
|
||||
if ps ~= self.scroll then
|
||||
self.textChanged = true
|
||||
end
|
||||
@@ -217,6 +222,10 @@ function Entry:paste(ie)
|
||||
end
|
||||
end
|
||||
|
||||
function Entry.forcePaste()
|
||||
os.queueEvent('clipboard_paste')
|
||||
end
|
||||
|
||||
function Entry:clearLine()
|
||||
if #_val(self.value) > 0 then
|
||||
self:reset()
|
||||
@@ -225,7 +234,9 @@ end
|
||||
|
||||
function Entry:markBegin()
|
||||
if not self.mark.active then
|
||||
self.mark.active = true
|
||||
if #_val(self.value) > 0 then
|
||||
self.mark.active = true
|
||||
end
|
||||
self.mark.anchor = { x = self.pos }
|
||||
end
|
||||
end
|
||||
@@ -262,6 +273,8 @@ function Entry:markLeft()
|
||||
self:markBegin()
|
||||
if self:moveLeft() then
|
||||
self:markFinish()
|
||||
else
|
||||
self.mark.continue = self.mark.active
|
||||
end
|
||||
end
|
||||
|
||||
@@ -269,6 +282,8 @@ function Entry:markRight()
|
||||
self:markBegin()
|
||||
if self:moveRight() then
|
||||
self:markFinish()
|
||||
else
|
||||
self.mark.continue = self.mark.active
|
||||
end
|
||||
end
|
||||
|
||||
@@ -296,6 +311,8 @@ function Entry:markNextWord()
|
||||
self:markBegin()
|
||||
if self:moveWordRight() then
|
||||
self:markFinish()
|
||||
else
|
||||
self.mark.continue = self.mark.active
|
||||
end
|
||||
end
|
||||
|
||||
@@ -303,6 +320,8 @@ function Entry:markPrevWord()
|
||||
self:markBegin()
|
||||
if self:moveWordLeft() then
|
||||
self:markFinish()
|
||||
else
|
||||
self.mark.continue = self.mark.active
|
||||
end
|
||||
end
|
||||
|
||||
@@ -321,6 +340,8 @@ function Entry:markHome()
|
||||
self:markBegin()
|
||||
if self:moveHome() then
|
||||
self:markFinish()
|
||||
else
|
||||
self.mark.continue = self.mark.active
|
||||
end
|
||||
end
|
||||
|
||||
@@ -328,6 +349,8 @@ function Entry:markEnd()
|
||||
self:markBegin()
|
||||
if self:moveEnd() then
|
||||
self:markFinish()
|
||||
else
|
||||
self.mark.continue = self.mark.active
|
||||
end
|
||||
end
|
||||
|
||||
@@ -363,9 +386,10 @@ local mappings = {
|
||||
--[ 'control-d' ] = Entry.cutNextWord,
|
||||
[ 'control-x' ] = Entry.cut,
|
||||
[ 'paste' ] = Entry.paste,
|
||||
-- [ 'control-y' ] = Entry.paste, -- well this won't work...
|
||||
[ 'control-y' ] = Entry.forcePaste, -- well this won't work...
|
||||
|
||||
[ 'mouse_doubleclick' ] = Entry.markWord,
|
||||
[ 'mouse_tripleclick' ] = Entry.markAll,
|
||||
[ 'shift-left' ] = Entry.markLeft,
|
||||
[ 'shift-right' ] = Entry.markRight,
|
||||
[ 'mouse_down' ] = Entry.markAnchor,
|
||||
|
||||
@@ -58,6 +58,14 @@ 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
|
||||
|
||||
@@ -4,17 +4,21 @@ local linkfs = { }
|
||||
|
||||
-- TODO: implement broken links
|
||||
|
||||
local methods = { 'exists', 'getFreeSpace', 'getSize',
|
||||
'isDir', 'isReadOnly', 'list', 'listEx', 'makeDir', 'open', 'getDrive' }
|
||||
local methods = { 'exists', 'getFreeSpace', 'getSize', 'attributes',
|
||||
'isDir', 'isReadOnly', 'list', 'makeDir', 'open', 'getDrive' }
|
||||
|
||||
for _,m in pairs(methods) do
|
||||
linkfs[m] = function(node, dir, ...)
|
||||
dir = dir:gsub(node.mountPoint, node.source, 1)
|
||||
dir = linkfs.resolve(node, dir)
|
||||
return fs[m](dir, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function linkfs.mount(_, source)
|
||||
function linkfs.resolve(node, dir)
|
||||
return dir:gsub(node.mountPoint, node.source, 1)
|
||||
end
|
||||
|
||||
function linkfs.mount(path, source)
|
||||
if not source then
|
||||
error('Source is required')
|
||||
end
|
||||
@@ -22,6 +26,9 @@ function linkfs.mount(_, 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,
|
||||
@@ -65,4 +72,4 @@ function linkfs.move(node, s, t)
|
||||
return fs.move(s, t)
|
||||
end
|
||||
|
||||
return linkfs
|
||||
return linkfs
|
||||
@@ -6,7 +6,6 @@ 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)
|
||||
@@ -33,17 +32,17 @@ local function remoteCommand(node, msg)
|
||||
error('netfs: Connection failed', 2)
|
||||
end
|
||||
|
||||
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx' }
|
||||
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx', 'attributes' }
|
||||
|
||||
local function resolveDir(dir, node)
|
||||
local function resolve(node, dir)
|
||||
-- TODO: Wrong ! (does not support names with dashes)
|
||||
dir = dir:gsub(node.mountPoint, '', 1)
|
||||
return fs.combine(node.directory, dir)
|
||||
return fs.combine(node.source, dir)
|
||||
end
|
||||
|
||||
for _,m in pairs(methods) do
|
||||
netfs[m] = function(node, dir)
|
||||
dir = resolveDir(dir, node)
|
||||
dir = resolve(node, dir)
|
||||
|
||||
return remoteCommand(node, {
|
||||
fn = m,
|
||||
@@ -52,14 +51,14 @@ for _,m in pairs(methods) do
|
||||
end
|
||||
end
|
||||
|
||||
function netfs.mount(_, id, directory)
|
||||
function netfs.mount(_, id, source)
|
||||
if not id or not tonumber(id) then
|
||||
error('ramfs syntax: computerId [directory]')
|
||||
end
|
||||
return {
|
||||
id = tonumber(id),
|
||||
nodes = { },
|
||||
directory = directory or '',
|
||||
source = source or '',
|
||||
}
|
||||
end
|
||||
|
||||
@@ -68,7 +67,7 @@ function netfs.getDrive()
|
||||
end
|
||||
|
||||
function netfs.complete(node, partial, dir, includeFiles, includeSlash)
|
||||
dir = resolveDir(dir, node)
|
||||
dir = resolve(node, dir)
|
||||
|
||||
return remoteCommand(node, {
|
||||
fn = 'complete',
|
||||
@@ -77,8 +76,8 @@ function netfs.complete(node, partial, dir, includeFiles, includeSlash)
|
||||
end
|
||||
|
||||
function netfs.copy(node, s, t)
|
||||
s = resolveDir(s, node)
|
||||
t = resolveDir(t, node)
|
||||
s = resolve(node, s)
|
||||
t = resolve(node, t)
|
||||
|
||||
return remoteCommand(node, {
|
||||
fn = 'copy',
|
||||
@@ -87,37 +86,37 @@ function netfs.copy(node, s, t)
|
||||
end
|
||||
|
||||
function netfs.isDir(node, dir)
|
||||
if dir == node.mountPoint and node.directory == '' then
|
||||
if dir == node.mountPoint and node.source == '' then
|
||||
return true
|
||||
end
|
||||
return remoteCommand(node, {
|
||||
fn = 'isDir',
|
||||
args = { resolveDir(dir, node) },
|
||||
args = { resolve(node, dir) },
|
||||
})
|
||||
end
|
||||
|
||||
function netfs.isReadOnly(node, dir)
|
||||
if dir == node.mountPoint and node.directory == '' then
|
||||
if dir == node.mountPoint and node.source == '' then
|
||||
return false
|
||||
end
|
||||
return remoteCommand(node, {
|
||||
fn = 'isReadOnly',
|
||||
args = { resolveDir(dir, node) },
|
||||
args = { resolve(node, dir) },
|
||||
})
|
||||
end
|
||||
|
||||
function netfs.getSize(node, dir)
|
||||
if dir == node.mountPoint and node.directory == '' then
|
||||
if dir == node.mountPoint and node.source == '' then
|
||||
return 0
|
||||
end
|
||||
return remoteCommand(node, {
|
||||
fn = 'getSize',
|
||||
args = { resolveDir(dir, node) },
|
||||
args = { resolve(node, dir) },
|
||||
})
|
||||
end
|
||||
|
||||
function netfs.find(node, spec)
|
||||
spec = resolveDir(spec, node)
|
||||
spec = resolve(node, spec)
|
||||
local list = remoteCommand(node, {
|
||||
fn = 'find',
|
||||
args = { spec },
|
||||
@@ -131,8 +130,8 @@ function netfs.find(node, spec)
|
||||
end
|
||||
|
||||
function netfs.move(node, s, t)
|
||||
s = resolveDir(s, node)
|
||||
t = resolveDir(t, node)
|
||||
s = resolve(node, s)
|
||||
t = resolve(node, t)
|
||||
|
||||
return remoteCommand(node, {
|
||||
fn = 'move',
|
||||
@@ -141,7 +140,7 @@ function netfs.move(node, s, t)
|
||||
end
|
||||
|
||||
function netfs.open(node, fn, fl)
|
||||
fn = resolveDir(fn, node)
|
||||
fn = resolve(node, fn)
|
||||
|
||||
local vfh = remoteCommand(node, {
|
||||
fn = 'open',
|
||||
|
||||
@@ -9,15 +9,28 @@ 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)
|
||||
@@ -40,8 +53,10 @@ function ramfs.makeDir(_, dir)
|
||||
fs.mount(dir, 'ramfs', 'directory')
|
||||
end
|
||||
|
||||
function ramfs.isDir(node)
|
||||
return not not node.nodes
|
||||
function ramfs.isDir(node, dir)
|
||||
if node.mountPoint == dir then
|
||||
return not not node.nodes
|
||||
end
|
||||
end
|
||||
|
||||
function ramfs.getDrive()
|
||||
@@ -64,32 +79,70 @@ function ramfs.list(node, dir)
|
||||
end
|
||||
|
||||
function ramfs.open(node, fn, fl)
|
||||
|
||||
if fl ~= 'r' and fl ~= 'w' and fl ~= 'rb' and fl ~= 'wb' then
|
||||
local modes = Util.transpose { 'r', 'w', 'rb', 'wb', 'a' }
|
||||
if not modes[fl] 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()
|
||||
ctr = ctr + 1
|
||||
return node.contents:sub(ctr, ctr)
|
||||
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
|
||||
end,
|
||||
readLine = function()
|
||||
if not lines then
|
||||
lines = Util.split(node.contents)
|
||||
lines = Util.split(c)
|
||||
end
|
||||
ctr = ctr + 1
|
||||
return lines[ctr]
|
||||
end,
|
||||
readAll = function()
|
||||
return node.contents
|
||||
return c
|
||||
end,
|
||||
close = function()
|
||||
lines = nil
|
||||
@@ -121,11 +174,30 @@ 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 {
|
||||
read = function()
|
||||
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
|
||||
ctr = ctr + 1
|
||||
return node.contents[ctr]
|
||||
return c[ctr]
|
||||
end,
|
||||
close = function()
|
||||
end,
|
||||
@@ -137,7 +209,13 @@ function ramfs.open(node, fn, fl)
|
||||
local c = { }
|
||||
return {
|
||||
write = function(b)
|
||||
table.insert(c, 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
|
||||
end,
|
||||
flush = function()
|
||||
node.contents = c
|
||||
|
||||
@@ -5,29 +5,46 @@ local fs = _G.fs
|
||||
|
||||
local urlfs = { }
|
||||
|
||||
function urlfs.mount(_, url)
|
||||
function urlfs.mount(path, url, force)
|
||||
if not url then
|
||||
error('URL is required')
|
||||
end
|
||||
return {
|
||||
url = url,
|
||||
|
||||
-- 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,
|
||||
}
|
||||
end
|
||||
|
||||
function urlfs.delete(_, dir)
|
||||
fs.unmount(dir)
|
||||
function urlfs.delete(node, path)
|
||||
if path == node.mountPoint then
|
||||
fs.unmount(path)
|
||||
end
|
||||
end
|
||||
|
||||
function urlfs.exists()
|
||||
return true
|
||||
function urlfs.exists(node, path)
|
||||
return path == node.mountPoint
|
||||
end
|
||||
|
||||
function urlfs.getSize(node)
|
||||
return node.size or 0
|
||||
function urlfs.getSize(node, path)
|
||||
return path == node.mountPoint and node.size or 0
|
||||
end
|
||||
|
||||
function urlfs.isReadOnly()
|
||||
return true
|
||||
return false
|
||||
end
|
||||
|
||||
function urlfs.isDir()
|
||||
@@ -50,14 +67,6 @@ 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
|
||||
@@ -94,6 +103,9 @@ function urlfs.open(node, fn, fl)
|
||||
}
|
||||
end
|
||||
return {
|
||||
readAll = function()
|
||||
return c
|
||||
end,
|
||||
read = function()
|
||||
ctr = ctr + 1
|
||||
return c:sub(ctr, ctr):byte()
|
||||
|
||||
56
sys/modules/opus/fuzzy.lua
Normal file
56
sys/modules/opus/fuzzy.lua
Normal file
@@ -0,0 +1,56 @@
|
||||
local find = string.find
|
||||
local floor = math.floor
|
||||
local min = math.min
|
||||
local max = math.max
|
||||
local sub = string.sub
|
||||
|
||||
-- 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
|
||||
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
|
||||
@@ -24,9 +24,9 @@ function git.list(repository)
|
||||
|
||||
local function getContents()
|
||||
local dataUrl = string.format(TREE_URL, user, repo, branch)
|
||||
local contents, msg = Util.httpGet(dataUrl,TREE_HEADERS)
|
||||
local contents, msg = Util.httpGet(dataUrl, TREE_HEADERS)
|
||||
if not contents then
|
||||
error(_sformat('Failed to download %s\n%s', dataUrl, msg), 2)
|
||||
error(string.format('Failed to download %s\n%s', dataUrl, msg), 2)
|
||||
else
|
||||
return json.decode(contents)
|
||||
end
|
||||
|
||||
@@ -20,13 +20,12 @@ function GPS.locate(timeout, debug)
|
||||
|
||||
local modem = device.wireless_modem
|
||||
local closeChannel = false
|
||||
local selfID = os.getComputerID()
|
||||
if not modem.isOpen(selfID) then
|
||||
modem.open(selfID)
|
||||
if not modem.isOpen(GPS.CHANNEL_GPS) then
|
||||
modem.open(GPS.CHANNEL_GPS)
|
||||
closeChannel = true
|
||||
end
|
||||
|
||||
modem.transmit(GPS.CHANNEL_GPS, selfID, "PING")
|
||||
modem.transmit(GPS.CHANNEL_GPS, GPS.CHANNEL_GPS, "PING")
|
||||
|
||||
local fixes = {}
|
||||
local pos = nil
|
||||
@@ -34,7 +33,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 == selfID and reply == GPS.CHANNEL_GPS and dist then
|
||||
if side == modem.side and chan == GPS.CHANNEL_GPS 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)),
|
||||
@@ -60,12 +59,12 @@ function GPS.locate(timeout, debug)
|
||||
end
|
||||
|
||||
if closeChannel then
|
||||
modem.close(selfID)
|
||||
modem.close(GPS.CHANNEL_GPS)
|
||||
end
|
||||
if debug then
|
||||
print("Position is "..pos.x..","..pos.y..","..pos.z)
|
||||
end
|
||||
return vector.new(pos.x, pos.y, pos.z)
|
||||
return pos and vector.new(pos.x, pos.y, pos.z)
|
||||
end
|
||||
|
||||
function GPS.isAvailable()
|
||||
|
||||
@@ -1,40 +1,51 @@
|
||||
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
|
||||
-- https://www.lua.org/manual/5.1/manual.html#pdf-require
|
||||
-- https://github.com/LuaDist/lua/blob/d2e7e7d4d43ff9068b279a617c5b2ca2c2771676/src/loadlib.c
|
||||
|
||||
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
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
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 DEFAULT_PATH = table.concat(defaultPath, ';')
|
||||
|
||||
local fs = _G.fs
|
||||
local os = _G.os
|
||||
local string = _G.string
|
||||
|
||||
-- Add require and package to the environment
|
||||
return function(env)
|
||||
return function(env, programDir)
|
||||
local function preloadSearcher(modname)
|
||||
if env.package.preload[modname] then
|
||||
return function()
|
||||
@@ -43,77 +54,75 @@ return function(env)
|
||||
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
|
||||
|
||||
sPath = fs.combine(fs.getDir(env.shell.getRunningProgram() or ''), sPath)
|
||||
if programDir and sPath:sub(1, 1) ~= "/" then
|
||||
sPath = fs.combine(programDir, sPath)
|
||||
end
|
||||
if fs.exists(sPath) and not fs.isDir(sPath) then
|
||||
return loadfile(sPath, env)
|
||||
return loadfile(fs.combine(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,
|
||||
config = '/\n:\n?\n!\n-',
|
||||
path = env.LUA_PATH or _G.LUA_PATH or DEFAULT_PATH,
|
||||
cpath = '',
|
||||
config = '/\n:\n?\n!\n-',
|
||||
preload = { },
|
||||
loaded = {
|
||||
loaded = {
|
||||
bit32 = bit32,
|
||||
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 fn then
|
||||
local module, msg2 = fn(modname, env)
|
||||
if not module then
|
||||
error(msg2 or (modname .. ' module returned nil'), 2)
|
||||
end
|
||||
if type(fn) == 'function' then
|
||||
env.package.loaded[modname] = sentinel
|
||||
|
||||
local module = fn(modname, env) or true
|
||||
|
||||
env.package.loaded[modname] = module
|
||||
return module
|
||||
end
|
||||
if msg then
|
||||
error(msg, 2)
|
||||
table.insert(t, msg)
|
||||
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
|
||||
|
||||
@@ -50,18 +50,20 @@ function input:toCode(ch, code)
|
||||
table.insert(result, 'alt')
|
||||
end
|
||||
|
||||
if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or
|
||||
code == keys.leftShift or code == keys.rightShift then
|
||||
if code and modifiers[code] then
|
||||
table.insert(result, 'shift')
|
||||
elseif #ch == 1 then
|
||||
table.insert(result, ch:upper())
|
||||
else
|
||||
table.insert(result, 'shift')
|
||||
if ch then -- some weird things happen with control/command on mac
|
||||
if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or
|
||||
code == keys.leftShift or code == keys.rightShift then
|
||||
if code and modifiers[code] then
|
||||
table.insert(result, 'shift')
|
||||
elseif #ch == 1 then
|
||||
table.insert(result, ch:upper())
|
||||
else
|
||||
table.insert(result, 'shift')
|
||||
table.insert(result, ch)
|
||||
end
|
||||
elseif not code or not modifiers[code] then
|
||||
table.insert(result, ch)
|
||||
end
|
||||
elseif not code or not modifiers[code] then
|
||||
table.insert(result, ch)
|
||||
end
|
||||
|
||||
return table.concat(result, '-')
|
||||
@@ -118,6 +120,7 @@ function input:translate(event, code, p1, p2)
|
||||
local buttons = { 'mouse_click', 'mouse_rightclick' }
|
||||
self.mch = buttons[code]
|
||||
self.mfired = nil
|
||||
self.anchor = { x = p1, y = p2 }
|
||||
return {
|
||||
code = input:toCode('mouse_down', 255),
|
||||
button = code,
|
||||
@@ -132,6 +135,8 @@ function input:translate(event, code, p1, p2)
|
||||
button = code,
|
||||
x = p1,
|
||||
y = p2,
|
||||
dx = p1 - self.anchor.x,
|
||||
dy = p2 - self.anchor.y,
|
||||
}
|
||||
|
||||
elseif event == 'mouse_up' then
|
||||
@@ -141,18 +146,26 @@ function input:translate(event, code, p1, p2)
|
||||
p1 == self.x and p2 == self.y and
|
||||
(clock - self.timer < .5) then
|
||||
|
||||
self.mch = 'mouse_doubleclick'
|
||||
self.timer = nil
|
||||
self.clickCount = self.clickCount + 1
|
||||
if self.clickCount == 3 then
|
||||
self.mch = 'mouse_tripleclick'
|
||||
self.timer = nil
|
||||
self.clickCount = 1
|
||||
else
|
||||
self.mch = 'mouse_doubleclick'
|
||||
end
|
||||
else
|
||||
self.timer = os.clock()
|
||||
self.x = p1
|
||||
self.y = p2
|
||||
self.clickCount = 1
|
||||
end
|
||||
self.mfired = input:toCode(self.mch, 255)
|
||||
else
|
||||
self.mch = 'mouse_up'
|
||||
self.mfired = input:toCode(self.mch, 255)
|
||||
end
|
||||
|
||||
return {
|
||||
code = self.mfired,
|
||||
button = code,
|
||||
@@ -176,11 +189,22 @@ function input:translate(event, code, p1, p2)
|
||||
end
|
||||
end
|
||||
|
||||
function input:test()
|
||||
if not ({ ...})[1] then
|
||||
local colors = _G.colors
|
||||
local term = _G.term
|
||||
|
||||
while true do
|
||||
local ch = self:translate(os.pullEvent())
|
||||
local e = { os.pullEvent() }
|
||||
local ch = input:translate(table.unpack(e))
|
||||
if ch then
|
||||
Util.print(ch)
|
||||
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')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,7 @@ 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)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
local Util = require('opus.util')
|
||||
|
||||
local colors = _G.colors
|
||||
|
||||
local NFT = { }
|
||||
|
||||
-- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/
|
||||
|
||||
local tColourLookup = { }
|
||||
local hexToColor = { }
|
||||
for n = 1, 16 do
|
||||
tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
|
||||
hexToColor[string.sub("0123456789abcdef", n, n)] = 2 ^ (n - 1)
|
||||
end
|
||||
local colorToHex = Util.transpose(hexToColor)
|
||||
|
||||
local function getColourOf(hex)
|
||||
return tColourLookup[hex:byte()]
|
||||
return hexToColor[hex]
|
||||
end
|
||||
|
||||
function NFT.parse(imageText)
|
||||
@@ -62,8 +65,22 @@ function NFT.parse(imageText)
|
||||
return image
|
||||
end
|
||||
|
||||
function NFT.load(path)
|
||||
function NFT.transparency(image)
|
||||
for y = 1, image.height do
|
||||
for _,key in pairs(Util.keys(image.fg[y])) do
|
||||
if image.fg[y][key] == colors.magenta then
|
||||
image.fg[y][key] = nil
|
||||
end
|
||||
end
|
||||
for _,key in pairs(Util.keys(image.bg[y])) do
|
||||
if image.bg[y][key] == colors.magenta then
|
||||
image.bg[y][key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function NFT.load(path)
|
||||
local imageText = Util.readFile(path)
|
||||
if not imageText then
|
||||
error('Unable to read image file')
|
||||
@@ -71,4 +88,35 @@ function NFT.load(path)
|
||||
return NFT.parse(imageText)
|
||||
end
|
||||
|
||||
function NFT.save(image, filename)
|
||||
local bgcode, txcode = '\30', '\31'
|
||||
local output = { }
|
||||
|
||||
for y = 1, image.height do
|
||||
local lastBG, lastFG
|
||||
if image.text[y] then
|
||||
for x = 1, #image.text[y] do
|
||||
local bg = image.bg[y][x] or colors.magenta
|
||||
if bg ~= lastBG then
|
||||
lastBG = bg
|
||||
table.insert(output, bgcode .. colorToHex[bg])
|
||||
end
|
||||
|
||||
local fg = image.fg[y][x] or colors.magenta
|
||||
if fg ~= lastFG then
|
||||
lastFG = fg
|
||||
table.insert(output, txcode .. colorToHex[fg])
|
||||
end
|
||||
|
||||
table.insert(output, image.text[y][x])
|
||||
end
|
||||
end
|
||||
|
||||
if y < image.height then
|
||||
table.insert(output, '\n')
|
||||
end
|
||||
end
|
||||
Util.writeFile(filename, table.concat(output))
|
||||
end
|
||||
|
||||
return NFT
|
||||
|
||||
@@ -57,7 +57,7 @@ end
|
||||
function Packages:downloadList()
|
||||
local packages = {
|
||||
[ 'develop-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/packages.list',
|
||||
[ 'master-1.8' ] = 'https://pastebin.com/raw/pexZpAxt',
|
||||
[ 'master-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/master-1.8/packages.list',
|
||||
}
|
||||
|
||||
if packages[_G.OPUS_BRANCH] then
|
||||
|
||||
@@ -6,12 +6,11 @@ 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 = network.getTransport().read(self)
|
||||
local data, distance = _G.network.getTransport().read(self)
|
||||
if data then
|
||||
return data, distance
|
||||
end
|
||||
@@ -26,7 +25,7 @@ function socketClass:read(timeout)
|
||||
local e, id = os.pullEvent()
|
||||
|
||||
if e == 'transport_' .. self.uid then
|
||||
data, distance = network.getTransport().read(self)
|
||||
data, distance = _G.network.getTransport().read(self)
|
||||
if data then
|
||||
os.cancelTimer(timerId)
|
||||
return data, distance
|
||||
@@ -47,7 +46,7 @@ end
|
||||
|
||||
function socketClass:write(data)
|
||||
if self.connected then
|
||||
network.getTransport().write(self, {
|
||||
_G.network.getTransport().write(self, {
|
||||
type = 'DATA',
|
||||
seq = self.wseq,
|
||||
data = data,
|
||||
@@ -58,7 +57,7 @@ end
|
||||
|
||||
function socketClass:ping()
|
||||
if self.connected then
|
||||
network.getTransport().ping(self)
|
||||
_G.network.getTransport().ping(self)
|
||||
return true
|
||||
end
|
||||
end
|
||||
@@ -72,7 +71,7 @@ function socketClass:close()
|
||||
self.connected = false
|
||||
end
|
||||
device.wireless_modem.close(self.sport)
|
||||
network.getTransport().close(self)
|
||||
_G.network.getTransport().close(self)
|
||||
end
|
||||
|
||||
local Socket = { }
|
||||
@@ -127,9 +126,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 = unpack(options.keypair)
|
||||
socket.privKey, socket.pubKey = table.unpack(options.keypair)
|
||||
else
|
||||
socket.privKey, socket.pubKey = network.getKeyPair()
|
||||
socket.privKey, socket.pubKey = _G.network.getKeyPair()
|
||||
end
|
||||
local identifier = options and options.identifier or Security.getIdentifier()
|
||||
|
||||
@@ -159,7 +158,7 @@ function Socket.connect(host, port, options)
|
||||
socket.remotePubKey = Util.hexToByteArray(msg.pk)
|
||||
socket.options = msg.options or { }
|
||||
setupCrypto(socket, true)
|
||||
network.getTransport().open(socket)
|
||||
_G.network.getTransport().open(socket)
|
||||
return socket
|
||||
|
||||
elseif msg.type == 'NOPASS' then
|
||||
@@ -192,7 +191,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 = network.getKeyPair()
|
||||
socket.privKey, socket.pubKey = _G.network.getKeyPair()
|
||||
setupCrypto(socket)
|
||||
return true
|
||||
end
|
||||
@@ -241,7 +240,7 @@ function Socket.server(port, options)
|
||||
options = socket.options.ENCRYPT and { ENCRYPT = true },
|
||||
})
|
||||
|
||||
network.getTransport().open(socket)
|
||||
_G.network.getTransport().open(socket)
|
||||
return socket
|
||||
|
||||
else
|
||||
|
||||
@@ -33,64 +33,69 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
|
||||
end
|
||||
|
||||
local win = { }
|
||||
local maxScroll = 100
|
||||
local maxScroll
|
||||
local cx, cy = 1, 1
|
||||
local blink = false
|
||||
local bg, fg = parent.getBackgroundColor(), parent.getTextColor()
|
||||
local _bg, _fg = colors.black, colors.white
|
||||
|
||||
local canvas = Canvas({
|
||||
win.canvas = Canvas({
|
||||
x = sx,
|
||||
y = sy,
|
||||
width = w,
|
||||
height = h,
|
||||
isColor = parent.isColor(),
|
||||
offy = 0,
|
||||
bg = _bg,
|
||||
fg = _fg,
|
||||
})
|
||||
|
||||
win.canvas = canvas
|
||||
|
||||
local function update()
|
||||
if isVisible then
|
||||
canvas:render(parent)
|
||||
win.canvas:render(parent)
|
||||
win.setCursorPos(cx, cy)
|
||||
end
|
||||
end
|
||||
|
||||
local function scrollTo(y)
|
||||
y = math.max(0, y)
|
||||
y = math.min(#canvas.lines - canvas.height, y)
|
||||
y = math.min(#win.canvas.lines - win.canvas.height, y)
|
||||
|
||||
if y ~= canvas.offy then
|
||||
canvas.offy = y
|
||||
canvas:dirty()
|
||||
if y ~= win.canvas.offy then
|
||||
win.canvas.offy = y
|
||||
win.canvas:dirty()
|
||||
update()
|
||||
end
|
||||
end
|
||||
|
||||
function win.write(str)
|
||||
str = tostring(str) or ''
|
||||
canvas:write(cx, cy + canvas.offy, str, bg, fg)
|
||||
win.canvas:write(cx, cy + win.canvas.offy, str, win.canvas.bg, win.canvas.fg)
|
||||
win.setCursorPos(cx + #str, cy)
|
||||
update()
|
||||
end
|
||||
|
||||
function win.blit(str, fg, bg)
|
||||
canvas:blit(cx, cy + canvas.offy, str, bg, fg)
|
||||
win.canvas:blit(cx, cy + win.canvas.offy, str, bg, fg)
|
||||
win.setCursorPos(cx + #str, cy)
|
||||
update()
|
||||
end
|
||||
|
||||
function win.clear()
|
||||
canvas.offy = 0
|
||||
for i = #canvas.lines, canvas.height + 1, -1 do
|
||||
canvas.lines[i] = nil
|
||||
win.canvas.offy = 0
|
||||
for i = #win.canvas.lines, win.canvas.height + 1, -1 do
|
||||
win.canvas.lines[i] = nil
|
||||
end
|
||||
canvas:clear(bg, fg)
|
||||
win.canvas:clear()
|
||||
update()
|
||||
end
|
||||
|
||||
function win.getLine(n)
|
||||
local line = win.canvas.lines[n]
|
||||
return line.text, line.fg, line.bg
|
||||
end
|
||||
|
||||
function win.clearLine()
|
||||
canvas:clearLine(cy + canvas.offy, bg, fg)
|
||||
win.canvas:clearLine(cy + win.canvas.offy)
|
||||
win.setCursorPos(cx, cy)
|
||||
update()
|
||||
end
|
||||
@@ -102,10 +107,14 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
|
||||
function win.setCursorPos(x, y)
|
||||
cx, cy = math.floor(x), math.floor(y)
|
||||
if isVisible then
|
||||
parent.setCursorPos(cx + canvas.x - 1, cy + canvas.y - 1)
|
||||
parent.setCursorPos(cx + win.canvas.x - 1, cy + win.canvas.y - 1)
|
||||
end
|
||||
end
|
||||
|
||||
function win.getCursorBlink()
|
||||
return blink
|
||||
end
|
||||
|
||||
function win.setCursorBlink(b)
|
||||
blink = b
|
||||
if isVisible then
|
||||
@@ -114,12 +123,12 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
|
||||
end
|
||||
|
||||
function win.isColor()
|
||||
return canvas.isColor
|
||||
return win.canvas.isColor
|
||||
end
|
||||
win.isColour = win.isColor
|
||||
|
||||
function win.setTextColor(c)
|
||||
fg = c
|
||||
win.canvas.fg = c
|
||||
end
|
||||
win.setTextColour = win.setTextColor
|
||||
|
||||
@@ -139,38 +148,38 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
|
||||
win.setPaletteColour = win.setPaletteColor
|
||||
|
||||
function win.setBackgroundColor(c)
|
||||
bg = c
|
||||
win.canvas.bg = c
|
||||
end
|
||||
win.setBackgroundColour = win.setBackgroundColor
|
||||
|
||||
function win.getSize()
|
||||
return canvas.width, canvas.height
|
||||
return win.canvas.width, win.canvas.height
|
||||
end
|
||||
|
||||
function win.scroll(n)
|
||||
n = n or 1
|
||||
if n > 0 then
|
||||
local lines = #canvas.lines
|
||||
local lines = #win.canvas.lines
|
||||
for i = 1, n do
|
||||
canvas.lines[lines + i] = { }
|
||||
canvas:clearLine(lines + i, bg, fg)
|
||||
win.canvas.lines[lines + i] = { }
|
||||
win.canvas:clearLine(lines + i)
|
||||
end
|
||||
while #canvas.lines > maxScroll do
|
||||
table.remove(canvas.lines, 1)
|
||||
while #win.canvas.lines > (maxScroll or win.canvas.height) do
|
||||
table.remove(win.canvas.lines, 1)
|
||||
end
|
||||
scrollTo(#canvas.lines)
|
||||
canvas:dirty()
|
||||
scrollTo(#win.canvas.lines)
|
||||
win.canvas:dirty()
|
||||
update()
|
||||
end
|
||||
end
|
||||
|
||||
function win.getTextColor()
|
||||
return fg
|
||||
return win.canvas.fg
|
||||
end
|
||||
win.getTextColour = win.getTextColor
|
||||
|
||||
function win.getBackgroundColor()
|
||||
return bg
|
||||
return win.canvas.bg
|
||||
end
|
||||
win.getBackgroundColour = win.getBackgroundColor
|
||||
|
||||
@@ -178,7 +187,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
|
||||
if visible ~= isVisible then
|
||||
isVisible = visible
|
||||
if isVisible then
|
||||
canvas:dirty()
|
||||
win.canvas:dirty()
|
||||
update()
|
||||
end
|
||||
end
|
||||
@@ -186,7 +195,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
|
||||
|
||||
function win.redraw()
|
||||
if isVisible then
|
||||
canvas:dirty()
|
||||
win.canvas:dirty()
|
||||
update()
|
||||
end
|
||||
end
|
||||
@@ -194,27 +203,58 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
|
||||
function win.restoreCursor()
|
||||
if isVisible then
|
||||
win.setCursorPos(cx, cy)
|
||||
win.setTextColor(fg)
|
||||
win.setTextColor(win.canvas.fg)
|
||||
win.setCursorBlink(blink)
|
||||
end
|
||||
end
|
||||
|
||||
function win.getPosition()
|
||||
return canvas.x, canvas.y
|
||||
return win.canvas.x, win.canvas.y
|
||||
end
|
||||
|
||||
function win.reposition(x, y, width, height)
|
||||
canvas.x, canvas.y = x, y
|
||||
canvas:resize(width or canvas.width, height or canvas.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()
|
||||
end
|
||||
|
||||
--[[ Additional methods ]]--
|
||||
function win.scrollDown()
|
||||
scrollTo(canvas.offy + 1)
|
||||
scrollTo(win.canvas.offy + 1)
|
||||
end
|
||||
|
||||
function win.scrollUp()
|
||||
scrollTo(canvas.offy - 1)
|
||||
scrollTo(win.canvas.offy - 1)
|
||||
end
|
||||
|
||||
function win.scrollTop()
|
||||
@@ -222,7 +262,7 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
|
||||
end
|
||||
|
||||
function win.scrollBottom()
|
||||
scrollTo(#canvas.lines)
|
||||
scrollTo(#win.canvas.lines)
|
||||
end
|
||||
|
||||
function win.setMaxScroll(ms)
|
||||
@@ -230,37 +270,108 @@ function Terminal.window(parent, sx, sy, w, h, isVisible)
|
||||
end
|
||||
|
||||
function win.getCanvas()
|
||||
return canvas
|
||||
return win.canvas
|
||||
end
|
||||
|
||||
function win.getParent()
|
||||
return parent
|
||||
end
|
||||
|
||||
canvas:clear()
|
||||
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
|
||||
end
|
||||
|
||||
-- get windows contents
|
||||
function Terminal.getContents(win, parent)
|
||||
local oblit, oscp = parent.blit, parent.setCursorPos
|
||||
local lines = { }
|
||||
function Terminal.getContents(win)
|
||||
if not win.getLine then
|
||||
error('window is required')
|
||||
end
|
||||
|
||||
parent.blit = function(text, fg, bg)
|
||||
lines[#lines + 1] = {
|
||||
local lines = { }
|
||||
local _, h = win.getSize()
|
||||
|
||||
for i = 1, h do
|
||||
local text, fg, bg = win.getLine(i)
|
||||
lines[i] = {
|
||||
text = text,
|
||||
fg = fg,
|
||||
bg = bg,
|
||||
}
|
||||
end
|
||||
parent.setCursorPos = function() end
|
||||
|
||||
win.setVisible(true)
|
||||
win.redraw()
|
||||
|
||||
parent.blit = oblit
|
||||
parent.setCursorPos = oscp
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
@@ -12,6 +12,10 @@ 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
|
||||
@@ -32,78 +36,68 @@ local function traceback(x)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
local function trim_traceback(stack)
|
||||
local trace = { }
|
||||
local filters = {
|
||||
"%[C%]: in function 'xpcall'",
|
||||
"(...tail calls...)",
|
||||
"xpcall: $",
|
||||
"trace.lua:%d+:",
|
||||
"stack traceback:",
|
||||
}
|
||||
|
||||
-- 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
|
||||
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
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
ttarget[#ttarget] = nil -- remove 2 calls added by the added xpcall
|
||||
ttarget[#ttarget] = nil
|
||||
|
||||
return ttarget
|
||||
return err, t
|
||||
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] then
|
||||
trace = traceback("trace.lua:1:")
|
||||
end
|
||||
local ok, err = res[1], res[2]
|
||||
if not res[1] and res[2] ~= nil then
|
||||
local err, trace = trim_traceback(res[2])
|
||||
|
||||
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
|
||||
if err:match(':%d+: 0$') then
|
||||
return true
|
||||
end
|
||||
|
||||
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)
|
||||
if #trace > 0 then
|
||||
_G._syslog('\n' .. err .. '\n' .. 'stack traceback:')
|
||||
for _, v in ipairs(trace) do
|
||||
_G._syslog(v)
|
||||
end
|
||||
table.insert(trace, keep_starts, " ...")
|
||||
end
|
||||
|
||||
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")
|
||||
return res[1], err, trace
|
||||
end
|
||||
|
||||
return table.unpack(res, 1, res.n)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user