170 Commits

Author SHA1 Message Date
MayaTheShy
4542644068 Add stable inventory-manager listing, rename main to unstable
- inventory-manager now points to stable branch (known working)
- inventory-manager-unstable points to main branch (dev/experimental)
2026-03-29 15:58:30 -04:00
MayaTheShy
b4b5f13b23 Update platform package URL to point to the correct master branch 2026-03-28 22:17:04 -04:00
MayaTheShy
d1e752c516 Add platform package URL to packages.list 2026-03-26 14:45:32 -04:00
MayaTheShy
83baef8f82 Strip trailing slash from Gitea repository path in listGitea function 2026-03-22 18:27:47 -04:00
MayaTheShy
d0cb420a08 Refactor installer.lua to enhance functionality and improve user experience 2026-03-22 17:56:15 -04:00
MayaTheShy
eae757627a Update git branch references in installer.lua to align with main branch structure 2026-03-22 17:52:18 -04:00
MayaTheShy
7a99fc662d Update update.lua URL in fstab to point to the correct installer script 2026-03-22 17:50:37 -04:00
MayaTheShy
5beb965aad Update installation instructions to reference the correct installer script 2026-03-22 17:50:32 -04:00
MayaTheShy
1e0bcb841a Add initial installer script with configuration and download functionality 2026-03-22 17:50:26 -04:00
MayaTheShy
8ade008390 Update installation instructions to use wget for startup script 2026-03-22 17:39:01 -04:00
MayaTheShy
fee8cf4f46 Enhance ProgressBar to support customizable text overlay and display percentage value 2026-03-22 16:51:13 -04:00
MayaTheShy
f2460aa5e9 Add bar rendering functionality to Writer and enhance row background color handling in Grid 2026-03-22 16:51:06 -04:00
MayaTheShy
aff772b851 Add validate handler to install selected packages from Welcome wizard 2026-03-22 16:15:46 -04:00
MayaTheShy
04751c3ba9 Enhance welcome screen with optional package selection and updated instructions 2026-03-22 16:13:28 -04:00
MayaTheShy
f53d503129 Update version URL to point to the correct repository 2026-03-22 15:56:54 -04:00
MayaTheShy
50fd8622ff Enhance package introduction display with improved color formatting 2026-03-22 15:56:47 -04:00
MayaTheShy
19edc6dc97 Update welcome screen with recommended packages and correct repository link 2026-03-22 15:56:21 -04:00
MayaTheShy
31b4fd52c9 Enhance package management UI with selection functionality and batch actions 2026-03-22 15:56:16 -04:00
MayaTheShy
ab26539660 Add initial packages list for remote package management 2026-03-22 15:56:12 -04:00
MayaTheShy
9490b713ca Add support for exclude filters in package installation 2026-03-22 15:51:04 -04:00
MayaTheShy
a914786116 Update package sources to include develop branch and improve download logic 2026-03-22 15:50:46 -04:00
MayaTheShy
03f00fc2a4 Refactor git module to support Gitea and improve error handling 2026-03-22 15:50:40 -04:00
MayaTheShy
5775954d7c Capture fs locally before setting _ENV=nil in json.lua 2026-03-22 15:19:33 -04:00
Kan18
857c0e252d Update json.lua 2026-03-22 15:18:07 -04:00
Kan18
7bd993a12d oops 2026-03-22 15:18:07 -04:00
Kan18
7d36c9aafe Fix Util.getVersion 2026-03-22 15:18:07 -04:00
Kan18
28733d77e8 oops 2026-03-22 15:18:07 -04:00
Kan18
be13d87266 actually set it to 30 2026-03-22 15:18:07 -04:00
Kan18
73616676dc oops 2026-03-22 15:18:07 -04:00
Kan18
e3ea71f5a3 Increase discovery message interval, distribute messages
Previously on large server(s), there was an issue where because
chunkloaded computers all started up at once, so they all had the same
discovery message sending timer. This prevents that by starting each off
with a random offset. Also increases the interval for sending messages
from 15 s to 30 s, so that messages (which are the same, a lot of the
time) get sent less often.
2026-03-22 15:18:07 -04:00
Kan18
fd6f39770a Sanitize label in samba.lua
Prevent labels from having .., /, *, control characters, or quotes (this
generally messes things up) and limit them to a reasonable length. We
might possibly also want to do this in snmp.lua, I'm not sure if that
will break things though
2026-03-22 15:18:07 -04:00
Kan18
88dda25911 genotp.lua changes
Make genotp more clearly state that the password can be used once but
allows permanent access (sorry for not making this clear earlier.) Use a
slightly longer password, and allow uppercase characters except for some
that could be ambiguous
2026-03-22 15:18:07 -04:00
MayaTheShy
53b38b7286 Fix variable reference for HMAC key assignment in setupCrypto function 2026-03-22 11:21:50 -04:00
MayaTheShy
c1b2e03fd6 Implement HMAC verification for encrypted messages and attach HMAC during encryption 2026-03-22 11:21:44 -04:00
MayaTheShy
4a233b1c55 Add trustKey to password update and implement getTrustKey function for ChaCha20 protocol 2026-03-22 11:21:02 -04:00
MayaTheShy
9e06241ac3 Refactor trustConnection function to use getTrustKey instead of getPassword for improved security 2026-03-22 11:20:57 -04:00
MayaTheShy
39caa32908 Implement PBKDF2 password hashing with salt for enhanced security 2026-03-22 11:19:55 -04:00
MayaTheShy
9103c44658 Remove SHA hashing from password update process 2026-03-22 11:19:47 -04:00
MayaTheShy
882894685c Refactor linkfs and netfs resolve functions for improved path handling 2026-03-22 04:09:43 -04:00
MayaTheShy
ba49f7ca7d Upgrade ChaCha20 rounds from 8 to 20 for enhanced security 2026-03-22 04:09:39 -04:00
MayaTheShy
f3c35afe07 Add debug inspector toggle for control-shift mouse click in UI 2026-03-22 04:09:10 -04:00
MayaTheShy
8a6896e276 Change default branch name from 'master' to 'main' in git.list function 2026-03-22 04:09:05 -04:00
MayaTheShy
a18a8b7140 Fix URL scheme for update.lua in fstab 2026-03-22 04:08:58 -04:00
MayaTheShy
6d6b43daf7 Add memory profiling script for CC:Tweaked / Opus OS 2026-03-22 04:08:53 -04:00
github-actions[bot]
4104750539 Update version date 2022-07-04 04:09:14 +00:00
Kan18
3150525ee2 Fix #48 (shell resolving issue) (#58) 2022-07-04 00:08:59 -04:00
github-actions[bot]
7c5f749f02 Update version date 2021-11-17 04:00:12 +00:00
xAnavrins
7d9029c706 Reduce peripheral calls in Network app
Add a manual refresh button instead
2021-11-16 22:59:19 -05:00
github-actions[bot]
ce6a741690 Update version date 2021-02-20 01:11:28 +00:00
Anavrins
16d233df5d Actually working bug/feature templates 2021-02-19 20:11:13 -05:00
github-actions[bot]
f6d09abd54 Update version date 2021-02-20 00:57:43 +00:00
Tomo
bff84370b2 Create issue templates (#50) 2021-02-19 19:57:25 -05:00
github-actions[bot]
e74db02371 Update version date 2021-02-16 02:29:40 +00:00
Anavrins
2b75fbd1ee fix: up-to-date package on master branch 2021-02-15 21:28:57 -05:00
github-actions[bot]
65bf756084 Update version date 2020-12-31 02:50:07 +00:00
Luca
2ff4ee5670 fix: boolean value false is now correctly displayed in Lua.lua (#51) 2020-12-30 21:49:51 -05:00
github-actions[bot]
5488a7d732 Update version date 2020-10-26 21:19:02 +00:00
Anavrins
37c4ca102c Maybe fix github actions 2020-10-26 17:18:37 -04:00
Anavrins
ac51771b12 Fix kiosk bug from #37 2020-10-25 02:40:21 -04:00
github-actions[bot]
6fdce03a10 Update version date 2020-08-24 05:41:41 +00:00
Boom
ac91d8ed08 Trust manager (#46) 2020-08-24 01:41:25 -04:00
github-actions[bot]
2c0db368fa Update version date 2020-08-24 05:20:58 +00:00
Kan18
a77deb72ec Add a one-time password system (#47)
This commit adds a one-time password system. Users can generate
passwords by using the `genotp` command (or manually queuing a set_otp
event with the SHA-256 hash of the one-time password they want to set)
and these passwords will be valid, along with the normal password,
either until the computer shuts down (or the net daemon is killed), a
new one-time password is generated, or the one-time password is used.
2020-08-24 01:20:42 -04:00
github-actions[bot]
01d8d65178 Update version date 2020-08-21 23:15:31 +00:00
Anavrins
f36e5470b1 Anonymize GPS
Damn you gollark!
2020-08-21 19:15:07 -04:00
github-actions[bot]
2520f53046 Update version date 2020-08-04 02:00:28 +00:00
Kan18
f4494d6103 Fix untar (#42)
small fix - untar_stream is supposed to be getting the handle, not the filename
2020-08-03 20:00:11 -06:00
github-actions[bot]
48f32946ec Update version date 2020-07-26 00:52:59 +00:00
kepler155c@gmail.com
c24a5a7115 help cleanup 2020-07-25 18:52:41 -06:00
github-actions[bot]
624af53f4e Update version date 2020-07-25 00:41:36 +00:00
Kan18
816ea366ab TLCO fix & boot file extension change (#37)
This commit fixes the TLCO boot option (which hasn't been working for a
while now), and also changes boot file extensions from .boot to .lua.
2020-07-24 18:41:21 -06:00
github-actions[bot]
b45cd45bcb Update version date 2020-07-25 00:37:14 +00:00
Kan18
45f1bd1c5d Put pastebin link in the right case (Fixes #40) (#41)
Pastebin recently removed support for case-insensitive links with no warning. This puts the original casing of the paste ID in README.md, instead of all-lowercase. Some other things might also need to be changed!
2020-07-24 18:36:57 -06:00
github-actions[bot]
1cb2c5f785 Update version date 2020-06-16 01:55:00 +00:00
kepler155c@gmail.com
42bd4b2b69 remove preferred apps/alternates - same can be accomplished using aliases 2020-06-15 19:54:42 -06:00
github-actions[bot]
156b604a58 Update version date 2020-06-14 02:26:36 +00:00
kepler155c@gmail.com
7df4a47ba0 oops 2020-06-13 20:26:17 -06:00
github-actions[bot]
9bf91a8762 Update version date 2020-06-13 18:18:21 +00:00
kepler155c@gmail.com
b69dcdeffa shell cleanup 2020-06-13 12:18:04 -06:00
github-actions[bot]
947f502c6d Update version date 2020-06-11 19:29:04 +00:00
kepler155c@gmail.com
a2af4405e7 require cleanup + compatibility fixes 2020-06-11 13:28:43 -06:00
github-actions[bot]
1bf6daedff Update version date 2020-06-11 01:46:52 +00:00
kepler155c@gmail.com
2fdcc338ad transparent compatiblity for moonscript 2020-06-10 19:46:34 -06:00
github-actions[bot]
4f39604c63 Update version date 2020-06-10 00:17:16 +00:00
kepler155c@gmail.com
e9580d67eb pure lua compatiblity fixes for moonlight, busted, etc 2020-06-09 18:16:51 -06:00
github-actions[bot]
039fa73749 Update version date 2020-06-06 17:00:25 +00:00
kepler155c@gmail.com
2d942ef001 experimental packages fix https://github.com/kepler155c/opus/issues/35 2020-06-06 11:00:07 -06:00
github-actions[bot]
4d02f75a19 Update version date 2020-06-06 03:38:48 +00:00
kepler155c@gmail.com
6c5cc508b1 moonscript support + cleanup 2020-06-05 21:38:26 -06:00
github-actions[bot]
e7fcd68a0f Update version date 2020-06-04 04:31:10 +00:00
kepler155c
7dfa80f5f3 Update README.md 2020-06-03 22:30:54 -06:00
github-actions[bot]
788f6e7de0 Update version date 2020-06-04 03:12:11 +00:00
kepler155c@gmail.com
704ef46b62 packages stored in compressed state - experimental (with config option) 2020-06-03 21:11:54 -06:00
github-actions[bot]
d678eeeaca Update version date 2020-06-04 02:41:07 +00:00
kepler155c@gmail.com
6a3b38922b packages stored in compressed state - experimental (with config option) 2020-06-03 20:40:48 -06:00
github-actions[bot]
6009f22d8e Update version date 2020-06-01 22:53:29 +00:00
kepler155c@gmail.com
f951f10df5 Merge branch 'develop-1.8' of https://github.com/kepler155c/opus into develop-1.8 2020-06-01 16:53:09 -06:00
kepler155c@gmail.com
5c4ab57ec8 binary support fixes 2020-06-01 16:53:01 -06:00
github-actions[bot]
0359a89e12 Update version date 2020-06-01 05:50:47 +00:00
kepler155c@gmail.com
4796e9e77a ramfs bugfixes 2020-05-31 23:50:30 -06:00
github-actions[bot]
18b7f540ab Update version date 2020-05-31 02:06:14 +00:00
kepler155c@gmail.com
2105799524 minor cleanup 2020-05-30 20:05:53 -06:00
github-actions[bot]
b6f439e8dc Update version date 2020-05-29 04:55:20 +00:00
kepler155c@gmail.com
41c0758857 Merge branch 'develop-1.8' of https://github.com/kepler155c/opus into develop-1.8 2020-05-28 22:54:53 -06:00
kepler155c@gmail.com
d1565c62e0 oops in loadfile replacement 2020-05-28 22:54:48 -06:00
github-actions[bot]
f00bece02b Update version date 2020-05-29 02:15:35 +00:00
kepler155c@gmail.com
9297223640 proper fix for builtin broken http.get - try 2 2020-05-28 20:15:19 -06:00
github-actions[bot]
f38afbbd36 Update version date 2020-05-29 02:10:25 +00:00
kepler155c@gmail.com
7b225a7747 proper fix for builtin broken http.get 2020-05-28 20:10:01 -06:00
github-actions[bot]
a7069f7ea8 Update version date 2020-05-26 03:48:56 +00:00
kepler155c@gmail.com
a4f4f34576 minor cleanup 2020-05-25 21:48:37 -06:00
github-actions[bot]
1cce4aad03 Update version date 2020-05-24 03:45:25 +00:00
kepler155c@gmail.com
26bbb50981 debugger support 2020-05-23 21:45:05 -06:00
github-actions[bot]
97bfae10fb Update version date 2020-05-19 23:09:31 +00:00
kepler155c@gmail.com
985830fcfd better fuzzy matching + vfs type flag in Files 2020-05-19 17:09:10 -06:00
github-actions[bot]
b93d69c261 Update version date 2020-05-18 01:37:11 +00:00
kepler155c@gmail.com
a7e3318226 add standard lua os methods, another fix for vfs links within links, allow write on urlfs mounted files 2020-05-17 19:36:33 -06:00
github-actions[bot]
c7c594d6c3 Update version date 2020-05-13 03:26:28 +00:00
kepler155c@gmail.com
90ce2bb1a5 Improved error messages 2020-05-12 21:25:37 -06:00
github-actions[bot]
2629f2a172 Update version date 2020-05-11 23:26:37 +00:00
kepler155c@gmail.com
8279c1ae12 environments, error messages, and stack traces, oh my!
Changed the way processes are launched.
multishell.openTab and kernel.run now accept the current environment as a parameter.
That new process inherits a copy of the passed environment.
Reduces complexity as the calling process is not required to create a suitable env.
Stack traces have been greatly improved and now include the stack for coroutines that error.
2020-05-11 17:25:58 -06:00
github-actions[bot]
bd911e80e8 Update version date 2020-05-10 20:04:57 +00:00
kepler155c@gmail.com
a3a819256f fix resizing scrolling terminals 2020-05-10 14:04:20 -06:00
github-actions[bot]
cfc18e10cd Update version date 2020-05-09 04:33:20 +00:00
kepler155c@gmail.com
b0db0b86bd kernel improvements 2020-05-08 22:32:44 -06:00
github-actions[bot]
71bbd2d457 Update version date 2020-05-08 17:10:12 +00:00
devomaa
db9c05fa89 Update Overview.lua to work with N hotkey (#33) 2020-05-08 11:09:32 -06:00
github-actions[bot]
3f80faa0fe Update version date 2020-05-08 03:35:30 +00:00
kepler155c@gmail.com
8b14399a20 a temporary fix for vfs/delete 2020-05-07 21:34:50 -06:00
github-actions[bot]
4e6c5172d1 Update version date 2020-05-07 00:01:53 +00:00
kepler155c@gmail.com
c24411717a optionally show value for slider component - remove some unneeded limits for textEntries 2020-05-06 18:01:10 -06:00
github-actions[bot]
51724ccd78 Update version date 2020-05-05 23:26:40 +00:00
kepler155c@gmail.com
3d3e5400cf tidy up env creation - round 1 2020-05-05 17:25:58 -06:00
github-actions[bot]
4018d4bcfb Update version date 2020-05-05 06:46:37 +00:00
Anavrins
4485dd2bd5 Remove default limit in TextEntry 2020-05-05 02:45:42 -04:00
github-actions[bot]
72560b1de0 Update version date 2020-05-05 03:07:05 +00:00
kepler155c@gmail.com
3d02230742 major oops - fix poluted envs 2020-05-04 21:06:23 -06:00
Anavrins
456d2d301f Merge branch 'develop-1.8' of https://github.com/kepler155c/opus into develop-1.8 2020-05-04 21:19:41 -04:00
github-actions[bot]
cdf8645c88 Update version date 2020-05-04 22:45:09 +00:00
kepler155c@gmail.com
d7f41f2bce multishell/kernel support for windows on different devices 2020-05-04 16:41:35 -06:00
Anavrins
e2ba9e2a03 Fix bug in CheckboxGrid 2020-05-04 03:55:55 -04:00
github-actions[bot]
447f4daa92 Update version date 2020-05-02 05:16:44 +00:00
kepler155c@gmail.com
5afb21b68e more work on update notification 2020-05-01 23:16:10 -06:00
github-actions[bot]
3488da649e Update version date 2020-05-02 05:10:19 +00:00
kepler155c@gmail.com
7f92286fb1 more work on update notification 2020-05-01 23:09:46 -06:00
github-actions[bot]
4a46d67304 Update version date 2020-05-02 04:40:52 +00:00
kepler155c@gmail.com
3cbdf7b97b more work on update notification 2020-05-01 22:40:17 -06:00
github-actions[bot]
74d3d1df33 Update version date 2020-05-02 04:26:51 +00:00
kepler155c@gmail.com
9c97849cff more work on update notification 2020-05-01 22:26:15 -06:00
github-actions[bot]
c1cbcf6b61 Update version date 2020-05-01 04:14:02 +00:00
kepler155c@gmail.com
49b986499c Merge branch 'develop-1.8' of https://github.com/kepler155c/opus into develop-1.8 2020-04-30 22:13:09 -06:00
kepler155c@gmail.com
fae7596182 update notification 2020-04-30 22:13:03 -06:00
github-actions[bot]
cb55b1daab Update version date 2020-05-01 04:07:10 +00:00
kepler155c@gmail.com
fd61416750 Merge branch 'develop-1.8' of https://github.com/kepler155c/opus into develop-1.8 2020-04-30 22:05:57 -06:00
kepler155c@gmail.com
c622b2f7fb update notification 2020-04-30 22:05:23 -06:00
github-actions[bot]
98f4bf7a7e Update version date 2020-05-01 03:57:20 +00:00
kepler155c@gmail.com
c5e0ac0af6 github actions test 2020-04-30 21:56:44 -06:00
kepler155c@gmail.com
0188e886c0 github actions test 2020-04-30 21:48:55 -06:00
kepler155c@gmail.com
55f444c37e github actions test 2020-04-30 21:38:38 -06:00
kepler155c@gmail.com
3368fa3266 update notification 2020-04-30 21:27:07 -06:00
kepler155c@gmail.com
e721eb68b6 update notification 2020-04-30 20:22:44 -06:00
kepler155c@gmail.com
1543f4d624 github actions test 2020-04-30 19:08:29 -06:00
kepler155c@gmail.com
287adb1235 auto upgrade packages on base opus update - github actions wip 2020-04-30 18:51:36 -06:00
kepler155c@gmail.com
e116caf16e fix netfs issues - implement fs.attributes 2020-04-27 15:44:09 -06:00
kepler155c@gmail.com
d72ae3de4a vfs bugfix 2020-04-26 22:07:18 -06:00
kepler155c@gmail.com
602d12afc5 vfs bugfix 2020-04-26 20:36:50 -06:00
kepler155c@gmail.com
ef9f0e09b6 partition manager + tab/wizard rework 2020-04-26 19:39:58 -06:00
kepler155c@gmail.com
b0d2ce0199 minor bugfixes - cleanup 2020-04-22 23:37:12 -06:00
kepler155c
7224d441ca Ui enhancements 2.0 (#31)
* canvas overhaul

* minor tweaks

* list mode for overview

* bugfixes + tweaks for editor 2.0

* minor tweaks

* more editor work

* refactor + new transitions

* use layout() where appropriate and cleanup

* mouse triple click + textEntry scroll ind

* cleanup

* cleanup + theme editor

* color rework + cleanup

* changes for deprecated ui methods

* can now use named colors
2020-04-21 22:40:59 -06:00
Anavrins
cdd0b6c4d2 Temporary fix for blit crashing with spaces 2020-04-20 19:33:22 -04:00
154 changed files with 6355 additions and 3207 deletions

30
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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]

View 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
View 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
View 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
View 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
View File

@@ -1 +1,2 @@
/ignore /ignore
.project

6
.opus_version Normal file
View 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)

View File

@@ -1,5 +1,7 @@
# Opus OS for computercraft # 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 ## Features
* Multitasking OS - run programs in separate tabs * Multitasking OS - run programs in separate tabs
* Telnet (wireless remote shell) * Telnet (wireless remote shell)
@@ -14,5 +16,5 @@
## Install ## Install
``` ```
pastebin run uzghlbnc wget run https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/main/installer.lua
``` ```

448
installer.lua Normal file
View File

@@ -0,0 +1,448 @@
--[[
Opus OS Installer
Self-contained installer that downloads from Gitea
]]
local GITEA_HOST = 'git.spatulaa.com'
local GITEA_USER = 'MayaTheShy'
local GITEA_REPO = 'Opus'
local GITEA_BRANCH = 'main'
local TREE_URL = 'https://%s/api/v1/repos/%s/%s/git/trees/%s?recursive=1'
local FILE_URL = 'https://%s/%s/%s/raw/branch/%s/%s'
local colors = _G.colors
local fs = _G.fs
local http = _G.http
local os = _G.os
local parallel = _G.parallel
local term = _G.term
local textutils = _G.textutils
local args = { ... }
local mode = args[1] or 'install'
-- Utility functions
local function httpGet(url)
local h, msg = http.get(url)
if h then
local content = h.readAll()
h.close()
return content
end
return nil, msg
end
local function download(url, path)
local dir = fs.getDir(path)
if dir ~= '' and not fs.exists(dir) then
fs.makeDir(dir)
end
local content, msg = httpGet(url)
if content then
local f = fs.open(path, 'w')
f.write(content)
f.close()
return true
end
return false, msg or 'Download failed'
end
local function formatSize(size)
if size >= 1000000 then
return string.format('%dM', math.floor(size / 1000000))
elseif size >= 1000 then
return string.format('%dK', math.floor(size / 1000))
end
return tostring(size)
end
local function center(text, y)
local w = term.getSize()
term.setCursorPos(math.floor((w - #text) / 2) + 1, y)
term.write(text)
end
local function drawHeader(title)
local w = term.getSize()
term.setBackgroundColor(colors.cyan)
term.setTextColor(colors.white)
term.setCursorPos(1, 1)
term.clearLine()
center(title, 1)
term.setBackgroundColor(colors.black)
term.setTextColor(colors.white)
end
local function drawProgressBar(y, pct)
local w = term.getSize()
local barW = w - 4
local filled = math.floor(barW * pct / 100)
term.setCursorPos(3, y)
term.setBackgroundColor(colors.gray)
term.write(string.rep(' ', barW))
term.setCursorPos(3, y)
term.setBackgroundColor(colors.lime)
term.write(string.rep(' ', filled))
term.setBackgroundColor(colors.black)
end
local function waitForKey(prompt)
local _, h = term.getSize()
if prompt then
term.setTextColor(colors.lightGray)
center(prompt, h)
end
os.pullEvent('key')
end
-- Get file listing from Gitea API
local function getFileList(branch)
local url = string.format(TREE_URL, GITEA_HOST, GITEA_USER, GITEA_REPO, branch)
local content, msg = httpGet(url)
if not content then
return nil, 'Failed to get file list: ' .. (msg or 'unknown error')
end
local data = textutils.unserialiseJSON(content)
if not data then
return nil, 'Invalid response from server'
end
if data.message then
return nil, data.message
end
local files = {}
local totalSize = 0
for _, v in pairs(data.tree) do
if v.type == 'blob' then
local path = v.path:gsub('%s', '%%20')
files[path] = {
url = string.format(FILE_URL, GITEA_HOST, GITEA_USER, GITEA_REPO, branch, path),
size = v.size or 0,
}
totalSize = totalSize + math.max(500, v.size or 0)
end
end
return files, nil, totalSize
end
-- Splash screen
local function showSplash()
term.clear()
drawHeader('Opus OS Installer')
local _, h = term.getSize()
local y = 3
term.setTextColor(colors.yellow)
center('Opus OS v1.0', y)
y = y + 2
term.setTextColor(colors.white)
center('By: Kepler', y)
y = y + 2
term.setTextColor(colors.lightGray)
local desc = {
'A user friendly operating system',
'featuring multitasking, networking,',
'and automation.',
}
for _, line in ipairs(desc) do
center(line, y)
y = y + 1
end
y = y + 1
term.setTextColor(colors.gray)
center('Source: ' .. GITEA_HOST, y)
waitForKey('Press any key to continue...')
end
-- License screen
local function showLicense()
term.clear()
drawHeader('License Review')
local _, h = term.getSize()
term.setCursorPos(2, 3)
term.setTextColor(colors.yellow)
print(' Copyright (c) 2018 Kepler')
print()
term.setTextColor(colors.lightGray)
local license = [[
Permission is hereby granted, free of
charge, to any person obtaining a copy
of this software to deal in the Software
without restriction, including without
limitation the rights to use, copy,
modify, merge, publish, distribute,
sublicense, and/or sell copies of the
Software.
THE SOFTWARE IS PROVIDED "AS IS",
WITHOUT WARRANTY OF ANY KIND.]]
print(license)
waitForKey('Press any key to accept...')
end
-- Branch selection (only shown for full install)
local function selectBranch()
local branches = {
{ branch = 'main', description = 'Main (Stable)' },
{ branch = 'develop-1.8', description = '1.8+ Unstable' },
{ branch = 'master-1.8', description = '1.8+ Stable' },
}
local selected = 1
while true do
term.clear()
drawHeader('Select Branch')
local _, h = term.getSize()
term.setCursorPos(2, 3)
term.setTextColor(colors.yellow)
print(' Choose which branch to install:')
print()
for i, b in ipairs(branches) do
term.setCursorPos(3, 4 + i)
if i == selected then
term.setTextColor(colors.yellow)
term.write('> ')
else
term.setTextColor(colors.white)
term.write(' ')
end
term.write(b.branch)
term.setTextColor(colors.gray)
term.write(' - ' .. b.description)
end
term.setTextColor(colors.lightGray)
center('Up/Down to select, Enter to confirm', h)
local event, key = os.pullEvent('key')
if key == keys.up then
selected = selected > 1 and selected - 1 or #branches
elseif key == keys.down then
selected = selected < #branches and selected + 1 or 1
elseif key == keys.enter then
return branches[selected].branch
end
end
end
-- File list review
local function showFileReview(files, totalSize)
term.clear()
drawHeader('Review Files')
local _, h = term.getSize()
local count = 0
for _ in pairs(files) do count = count + 1 end
term.setCursorPos(2, 3)
term.setTextColor(colors.yellow)
print(string.format(' %d files to install', count))
print()
term.setTextColor(colors.white)
print(string.format(' Space required: %s', formatSize(totalSize)))
local diskFree = fs.getFreeSpace('/')
if totalSize > diskFree then
term.setTextColor(colors.red)
print(string.format(' Free space: %s (NOT ENOUGH!)', formatSize(diskFree)))
else
term.setTextColor(colors.lime)
print(string.format(' Free space: %s', formatSize(diskFree)))
end
print()
term.setTextColor(colors.lightGray)
-- Show first few files
local shown = 0
for path in pairs(files) do
if shown < (h - 11) then
term.setCursorPos(3, 9 + shown)
local display = path
local w = term.getSize()
if #display > w - 4 then
display = '...' .. display:sub(-(w - 7))
end
term.write(display)
shown = shown + 1
end
end
if count > shown then
term.setCursorPos(3, 9 + shown)
term.setTextColor(colors.gray)
term.write(string.format('... and %d more files', count - shown))
end
waitForKey('Press any key to begin install...')
end
-- Install files with progress
local function installFiles(files)
if mode == 'update' then
fs.delete('sys')
end
term.clear()
drawHeader('Installing...')
local _, h = term.getSize()
local total = 0
for _ in pairs(files) do total = total + 1 end
local fileList = {}
for path, entry in pairs(files) do
table.insert(fileList, { path = path, url = entry.url })
end
local completed = 0
local failed = 0
local lastFile = ''
-- Download with 5 parallel workers
local workers = {}
for _ = 1, 5 do
table.insert(workers, function()
while true do
local entry = table.remove(fileList)
if not entry then break end
lastFile = entry.path
local s, m = download(entry.url, entry.path)
if not s then
failed = failed + 1
end
completed = completed + 1
end
end)
end
-- Progress display alongside downloads
table.insert(workers, function()
while completed < total do
local pct = math.floor(completed / total * 100)
term.setCursorPos(2, 4)
term.setTextColor(colors.white)
term.clearLine()
term.write(string.format(' Progress: %d/%d (%d%%)', completed, total, pct))
term.setCursorPos(2, 6)
term.setTextColor(colors.lightGray)
term.clearLine()
local w = term.getSize()
local display = lastFile
if #display > w - 4 then
display = '...' .. display:sub(-(w - 7))
end
term.write(' ' .. display)
drawProgressBar(h - 3, pct)
if failed > 0 then
term.setCursorPos(2, 8)
term.setTextColor(colors.red)
term.write(string.format(' Failed: %d', failed))
end
os.sleep(0.1)
end
end)
parallel.waitForAll(table.unpack(workers))
-- Final progress update
drawProgressBar(h - 3, 100)
term.setCursorPos(2, 4)
term.setTextColor(colors.lime)
term.clearLine()
term.write(string.format(' Complete: %d/%d files', completed, total))
term.setCursorPos(2, 6)
term.clearLine()
if failed > 0 then
term.setCursorPos(2, 8)
term.setTextColor(colors.red)
term.write(string.format(' %d files failed to download', failed))
end
return failed == 0
end
-- Done screen
local function showDone(success)
local _, h = term.getSize()
term.setCursorPos(2, h - 1)
if success then
term.setTextColor(colors.yellow)
center('Install complete! Rebooting...', h - 1)
os.sleep(2)
os.reboot()
else
term.setTextColor(colors.red)
center('Install finished with errors.', h - 1)
waitForKey('Press any key to exit...')
end
end
-- Main installer flow
local function main()
local branch = GITEA_BRANCH
if mode == 'install' then
showSplash()
showLicense()
branch = selectBranch()
end
-- Get file list
term.clear()
drawHeader('Opus OS Installer')
term.setCursorPos(2, 4)
term.setTextColor(colors.white)
term.write(' Fetching file list...')
local files, err, totalSize = getFileList(branch)
if not files then
term.setCursorPos(2, 6)
term.setTextColor(colors.red)
print(' Error: ' .. (err or 'unknown'))
waitForKey('Press any key to exit...')
return
end
if mode == 'install' then
showFileReview(files, totalSize)
end
local success = installFiles(files)
showDone(success)
end
main()

View File

@@ -29,9 +29,10 @@ local function loadBootOptions()
preload = { }, preload = { },
menu = { menu = {
{ prompt = os.version() }, { prompt = os.version() },
{ prompt = 'Opus' , args = { '/sys/boot/opus.boot' } }, { prompt = 'Opus' , args = { '/sys/boot/opus.lua' } },
{ prompt = 'Opus Shell' , args = { '/sys/boot/opus.boot', 'sys/apps/shell.lua' } }, { prompt = 'Opus Shell' , args = { '/sys/boot/opus.lua', '/sys/apps/shell.lua' } },
{ prompt = 'Opus Kiosk' , args = { '/sys/boot/kiosk.boot' } }, { prompt = 'Opus Kiosk' , args = { '/sys/boot/kiosk.lua' } },
{ prompt = 'Opus TLCO' , args = { '/sys/boot/tlco.lua' } },
}, },
})) }))
f.close() f.close()
@@ -41,6 +42,20 @@ local function loadBootOptions()
local options = textutils.unserialize(f.readAll()) local options = textutils.unserialize(f.readAll())
f.close() 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 return options
end end
@@ -127,7 +142,7 @@ local function splash()
local opus = { local opus = {
'fffff00', 'fffff00',
'ffff07000', 'ffff07000',
'ff00770b00 4444', 'ff00770b00f4444',
'ff077777444444444', 'ff077777444444444',
'f07777744444444444', 'f07777744444444444',
'f0000777444444444', 'f0000777444444444',

View File

@@ -1,4 +1,3 @@
local Alt = require('opus.alternate')
local Config = require('opus.config') local Config = require('opus.config')
local Event = require('opus.event') local Event = require('opus.event')
local pastebin = require('opus.http.pastebin') local pastebin = require('opus.http.pastebin')
@@ -77,21 +76,65 @@ local Browser = UI.Page {
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
columns = { columns = {
{ heading = 'Name', key = 'name' }, { heading = 'Name', key = 'name' },
{ key = 'flags', width = 2 }, { key = 'flags', width = 3, textColor = 'lightGray' },
{ heading = 'Size', key = 'fsize', width = 5 }, { heading = 'Size', key = 'fsize', width = 5, textColor = 'yellow' },
}, },
sortColumn = 'name', sortColumn = 'name',
y = 2, ey = -2, 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 { statusBar = UI.StatusBar {
columns = { columns = {
{ key = 'status' }, { key = 'status' },
{ key = 'totalSize', width = 6 }, { 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 { }, notification = UI.Notification { },
associations = UI.SlideOut { associations = UI.SlideOut {
backgroundColor = colors.cyan,
menuBar = UI.MenuBar { menuBar = UI.MenuBar {
buttons = { buttons = {
{ text = 'Save', event = 'save' }, { text = 'Save', event = 'save' },
@@ -99,7 +142,7 @@ local Browser = UI.Page {
}, },
}, },
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
x = 2, ex = -6, y = 3, ey = -5, x = 2, ex = -6, y = 3, ey = -8,
columns = { columns = {
{ heading = 'Extension', key = 'name' }, { heading = 'Extension', key = 'name' },
{ heading = 'Program', key = 'value' }, { heading = 'Program', key = 'value' },
@@ -114,8 +157,11 @@ local Browser = UI.Page {
x = -4, y = 6, x = -4, y = 6,
text = '-', event = 'remove_entry', help = 'Remove', text = '-', event = 'remove_entry', help = 'Remove',
}, },
[1] = UI.Window {
x = 2, y = -6, ex = -6, ey = -3,
},
form = UI.Form { form = UI.Form {
x = 3, y = -3, ey = -2, x = 3, y = -5, ex = -7, ey = -3,
margin = 1, margin = 1,
manualControls = true, manualControls = true,
[1] = UI.TextEntry { [1] = UI.TextEntry {
@@ -130,16 +176,13 @@ local Browser = UI.Page {
formLabel = 'Program', formKey = 'value', formLabel = 'Program', formKey = 'value',
shadowText = 'program', shadowText = 'program',
required = true, required = true,
limit = 128,
}, },
add = UI.Button { add = UI.Button {
x = -11, y = 1, x = -11, y = 1,
text = 'Add', event = 'add_association', text = 'Add', event = 'add_association',
}, },
}, },
statusBar = UI.StatusBar { statusBar = UI.StatusBar { },
backgroundColor = colors.cyan,
},
}, },
accelerators = { accelerators = {
[ 'control-q' ] = 'quit', [ 'control-q' ] = 'quit',
@@ -167,7 +210,7 @@ function Browser:enable()
self:setFocus(self.grid) self:setFocus(self.grid)
end end
function Browser.menuBar:getActive(menuItem) function Browser.menuBar.getActive(_, menuItem)
local file = Browser.grid:getSelected() local file = Browser.grid:getSelected()
if menuItem.flags == FILE then if menuItem.flags == FILE then
return file and not file.isDir return file and not file.isDir
@@ -175,56 +218,11 @@ function Browser.menuBar:getActive(menuItem)
return true return true
end 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, ...) function Browser:setStatus(status, ...)
self.notification:info(string.format(status, ...)) self.notification:info(string.format(status, ...))
end end
function Browser:unmarkAll() function Browser.unmarkAll()
for _,m in pairs(marked) do for _,m in pairs(marked) do
m.marked = false m.marked = false
end end
@@ -255,7 +253,6 @@ function Browser:getDirectory(directory)
end end
function Browser:updateDirectory(dir) function Browser:updateDirectory(dir)
dir.size = 0 dir.size = 0
dir.totalSize = 0 dir.totalSize = 0
Util.clear(dir.files) Util.clear(dir.files)
@@ -265,10 +262,11 @@ function Browser:updateDirectory(dir)
dir.size = #files dir.size = #files
for _, file in pairs(files) do for _, file in pairs(files) do
file.fullName = fs.combine(dir.name, file.name) file.fullName = fs.combine(dir.name, file.name)
file.flags = '' file.flags = file.fstype or ' '
if not file.isDir then if not file.isDir then
dir.totalSize = dir.totalSize + file.size dir.totalSize = dir.totalSize + file.size
file.fsize = formatSize(file.size) file.fsize = formatSize(file.size)
file.flags = file.flags .. ' '
else else
if config.showDirSizes then if config.showDirSizes then
file.size = fs.getSize(file.fullName, true) file.size = fs.getSize(file.fullName, true)
@@ -276,11 +274,9 @@ function Browser:updateDirectory(dir)
dir.totalSize = dir.totalSize + file.size dir.totalSize = dir.totalSize + file.size
file.fsize = formatSize(file.size) file.fsize = formatSize(file.size)
end end
file.flags = 'D' file.flags = file.flags .. 'D'
end
if file.isReadOnly then
file.flags = file.flags .. 'R'
end end
file.flags = file.flags .. (file.isReadOnly and 'R' or ' ')
if config.showHidden or file.name:sub(1, 1) ~= '.' then if config.showHidden or file.name:sub(1, 1) ~= '.' then
dir.files[file.fullName] = file dir.files[file.fullName] = file
end end
@@ -344,7 +340,7 @@ function Browser:eventHandler(event)
local file = self.grid:getSelected() local file = self.grid:getSelected()
if event.type == 'quit' then if event.type == 'quit' then
Event.exitPullEvents() UI:quit()
elseif event.type == 'edit' and file then elseif event.type == 'edit' and file then
self:run('edit', file.name) self:run('edit', file.name)
@@ -354,7 +350,7 @@ function Browser:eventHandler(event)
self:setStatus('Started cloud edit') self:setStatus('Started cloud edit')
elseif event.type == 'shell' then elseif event.type == 'shell' then
self:run(Alt.get('shell')) self:run('shell')
elseif event.type == 'refresh' then elseif event.type == 'refresh' then
self:updateDirectory(self.dir) self:updateDirectory(self.dir)
@@ -432,28 +428,25 @@ function Browser:eventHandler(event)
elseif event.type == 'delete' then elseif event.type == 'delete' then
if self:hasMarked() then if self:hasMarked() then
local width = self.statusBar:getColumnWidth('status') self.question:show()
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)
end 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 elseif event.type == 'copy' or event.type == 'cut' then
if self:hasMarked() then if self:hasMarked() then
@@ -472,7 +465,7 @@ function Browser:eventHandler(event)
elseif event.type == 'paste' then elseif event.type == 'paste' then
for _,m in pairs(copied) do for _,m in pairs(copied) do
local s, m = pcall(function() pcall(function()
if cutMode then if cutMode then
fs.move(m.fullName, fs.combine(self.dir.name, m.name)) fs.move(m.fullName, fs.combine(self.dir.name, m.name))
else else
@@ -549,6 +542,4 @@ local args = Util.parse(...)
Browser:setDir(args[1] or shell.dir()) Browser:setDir(args[1] or shell.dir())
UI:setPage(Browser) UI:setPage(Browser)
UI:start()
Event.pullEvents()
UI.term:reset()

View File

@@ -1,7 +1,7 @@
local fuzzy = require('opus.fuzzy')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local help = _G.help local help = _G.help
UI:configure('Help', ...) UI:configure('Help', ...)
@@ -12,11 +12,11 @@ for _,topic in pairs(help.topics()) do
end end
UI:addPage('main', UI.Page { UI:addPage('main', UI.Page {
labelText = UI.Text { UI.Text {
x = 3, y = 2, x = 3, y = 2,
value = 'Search', value = 'Search',
}, },
filter = UI.TextEntry { UI.TextEntry {
x = 10, y = 2, ex = -3, x = 10, y = 2, ex = -3,
limit = 32, limit = 32,
}, },
@@ -38,25 +38,24 @@ UI:addPage('main', UI.Page {
elseif event.type == 'grid_select' then elseif event.type == 'grid_select' then
if self.grid:getSelected() then if self.grid:getSelected() then
local name = self.grid:getSelected().name UI:setPage('topic', self.grid:getSelected().name)
UI:setPage('topic', name)
end end
elseif event.type == 'text_change' then elseif event.type == 'text_change' then
if not event.text then if not event.text then
self.grid.values = topics self.grid.sortColumn = 'lname'
else else
self.grid.values = { } self.grid.sortColumn = 'score'
for _,f in pairs(topics) do self.grid.inverseSort = false
if string.find(f.lname, event.text:lower()) then local pattern = event.text:lower()
table.insert(self.grid.values, f) for _,v in pairs(self.grid.values) do
end v.score = -fuzzy(v.lname, pattern)
end end
end end
self.grid:update() self.grid:update()
self.grid:setIndex(1) self.grid:setIndex(1)
self.grid:draw() self.grid:draw()
else else
return UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
end end
@@ -64,13 +63,12 @@ UI:addPage('main', UI.Page {
}) })
UI:addPage('topic', UI.Page { UI:addPage('topic', UI.Page {
backgroundColor = colors.black, backgroundColor = 'black',
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
title = 'text', title = 'text',
event = 'back', event = 'back',
}, },
helpText = UI.TextArea { helpText = UI.TextArea {
backgroundColor = colors.black,
x = 2, ex = -1, y = 3, ey = -2, x = 2, ex = -1, y = 3, ey = -2,
}, },
accelerators = { accelerators = {

View File

@@ -1,6 +1,3 @@
-- Lua may be called from outside of shell - inject a require
_G.requireInjector(_ENV)
local History = require('opus.history') local History = require('opus.history')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
@@ -10,10 +7,8 @@ local os = _G.os
local textutils = _G.textutils local textutils = _G.textutils
local term = _G.term local term = _G.term
local _exit
local sandboxEnv = setmetatable(Util.shallowCopy(_ENV), { __index = _G }) 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 sandboxEnv._echo = function( ... ) return { ... } end
_G.requireInjector(sandboxEnv) _G.requireInjector(sandboxEnv)
@@ -34,7 +29,6 @@ local page = UI.Page {
prompt = UI.TextEntry { prompt = UI.TextEntry {
y = 2, y = 2,
shadowText = 'enter command', shadowText = 'enter command',
limit = 1024,
accelerators = { accelerators = {
enter = 'command_enter', enter = 'command_enter',
up = 'history_back', up = 'history_back',
@@ -45,8 +39,9 @@ local page = UI.Page {
}, },
tabs = UI.Tabs { tabs = UI.Tabs {
y = 3, y = 3,
[1] = UI.Tab { formatted = UI.Tab {
tabTitle = 'Formatted', title = 'Formatted',
index = 1,
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
columns = { columns = {
{ heading = 'Key', key = 'name' }, { heading = 'Key', key = 'name' },
@@ -56,19 +51,25 @@ local page = UI.Page {
autospace = true, autospace = true,
}, },
}, },
[2] = UI.Tab { output = UI.Tab {
tabTitle = 'Output', title = 'Output',
index = 2,
backgroundColor = 'black',
output = UI.Embedded { output = UI.Embedded {
visible = true, y = 2,
maxScroll = 1000, 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.grid = page.tabs.formatted.grid
page.output = page.tabs[2].output page.output = page.tabs.output.output
function page:setPrompt(value, focus) function page:setPrompt(value, focus)
self.prompt:setValue(value) self.prompt:setValue(value)
@@ -133,31 +134,18 @@ function page:eventHandler(event)
self:executeStatement('_ENV') self:executeStatement('_ENV')
command = nil 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 elseif event.type == 'tab_select' then
self:setFocus(self.prompt) self:setFocus(self.prompt)
elseif event.type == 'show_output' then elseif event.type == 'show_output' then
self.tabs:selectTab(self.tabs[2]) self.tabs:selectTab(self.tabs.output)
elseif event.type == 'autocomplete' then elseif event.type == 'autocomplete' then
local value = self.prompt.value or '' local value = self.prompt.value or ''
local sz = #value local sz = #value
local pos = self.prompt.entry.pos local pos = self.prompt.entry.pos
self:setPrompt(autocomplete(sandboxEnv, value, 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() self.prompt:updateCursor()
elseif event.type == 'device' then elseif event.type == 'device' then
@@ -196,8 +184,7 @@ function page:eventHandler(event)
command = nil command = nil
self.grid:setValues(t) self.grid:setValues(t)
self.grid:setIndex(1) self.grid:setIndex(1)
self.grid:adjustWidth() self.grid:draw()
self:draw()
end end
return true return true
@@ -243,8 +230,7 @@ function page:setResult(result)
end end
self.grid:setValues(t) self.grid:setValues(t)
self.grid:setIndex(1) self.grid:setIndex(1)
self.grid:adjustWidth() self.grid:draw()
self:draw()
end end
function page.grid:eventHandler(event) function page.grid:eventHandler(event)
@@ -362,7 +348,7 @@ function page:executeStatement(statement)
term.redirect(oterm) term.redirect(oterm)
counter = counter + 1 counter = counter + 1
if s and m then if s and type(m) ~= "nil" then
self:setResult(m) self:setResult(m)
else else
self.grid:setValues({ }) self.grid:setValues({ })
@@ -371,10 +357,6 @@ function page:executeStatement(statement)
self:emit({ type = 'show_output' }) self:emit({ type = 'show_output' })
end end
end end
if _exit then
UI:exitPullEvents()
end
end end
local args = Util.parse(...) local args = Util.parse(...)
@@ -382,7 +364,8 @@ if args[1] then
command = 'args[1]' command = 'args[1]'
sandboxEnv.args = args sandboxEnv.args = args
page:setResult(args[1]) page:setResult(args[1])
page:setPrompt(command)
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@@ -4,10 +4,8 @@ local Socket = require('opus.socket')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local device = _G.device local device = _G.device
local network = _G.network local network = _G.network
local os = _G.os
local shell = _ENV.shell local shell = _ENV.shell
UI:configure('Network', ...) UI:configure('Network', ...)
@@ -56,14 +54,45 @@ local page = UI.Page {
columns = gridColumns, columns = gridColumns,
sortColumn = 'label', sortColumn = 'label',
autospace = true, 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 { ports = UI.SlideOut {
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
title = 'Ports', title = 'Ports',
event = 'ports_hide', event = 'ports_hide',
}, },
grid = UI.ScrollingGrid { menuBar = UI.MenuBar {
y = 2, y = 2,
buttons = {
{ text = 'Refresh', event = 'ports_update' },
}
},
grid = UI.ScrollingGrid {
y = 3,
columns = { columns = {
{ heading = 'Port', key = 'port' }, { heading = 'Port', key = 'port' },
{ heading = 'State', key = 'state' }, { heading = 'State', key = 'state' },
@@ -72,31 +101,12 @@ local page = UI.Page {
sortColumn = 'port', sortColumn = 'port',
autospace = true, autospace = true,
}, },
}, eventHandler = function(self, event)
help = UI.SlideOut { if event.type == 'grid_select' then
backgroundColor = colors.cyan, shell.openForegroundTab('Sniff ' .. event.selected.port)
x = 5, ex = -5, height = 8, y = -8, end
titleBar = UI.TitleBar { return UI.SlideOut.eventHandler(self, event)
title = 'Network Help', end,
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',
}
}, },
notification = UI.Notification { }, notification = UI.Notification { },
accelerators = { accelerators = {
@@ -127,13 +137,6 @@ local function sendCommand(host, command)
end end
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() function page.ports.grid:update()
local transport = network:getTransport() local transport = network:getTransport()
@@ -185,12 +188,14 @@ function page:eventHandler(event)
elseif event.type == 'vnc' then elseif event.type == 'vnc' then
shell.openForegroundTab('vnc.lua ' .. t.id) shell.openForegroundTab('vnc.lua ' .. t.id)
--[[
os.queueEvent('overview_shortcut', { os.queueEvent('overview_shortcut', {
title = t.label, title = t.label,
category = "VNC", category = "VNC",
icon = "\010\030 \009\009\031e\\\031 \031e/\031dn\010\030 \009\009 \031e\\/\031 \031bc", icon = "\010\030 \009\009\031e\\\031 \031e/\031dn\010\030 \009\009 \031e\\/\031 \031bc",
run = "vnc.lua " .. t.id, run = "vnc.lua " .. t.id,
}) })
--]]
elseif event.type == 'clear' then elseif event.type == 'clear' then
Util.clear(network) Util.clear(network)
@@ -209,17 +214,22 @@ function page:eventHandler(event)
end end
if event.type == 'help' then if event.type == 'help' then
self.help:show() shell.switchTab(shell.openTab('Help Networking'))
elseif event.type == 'ports' then elseif event.type == 'ports' then
self.ports.grid:update() self.ports.grid:update()
self.ports:show() 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:update()
self.ports.grid:draw() self.ports.grid:draw()
self:sync() self:sync()
end)
elseif event.type == 'ports_hide' then elseif event.type == 'ports_hide' then
Event.off(self.portsHandler) Event.off(self.portsHandler)
@@ -230,7 +240,7 @@ function page:eventHandler(event)
Config.update('network', config) Config.update('network', config)
elseif event.type == 'quit' then elseif event.type == 'quit' then
Event.exitPullEvents() UI:quit()
end end
UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
@@ -243,33 +253,6 @@ function page.menuBar:getActive(menuItem)
return menuItem.noCheck or not not t return menuItem.noCheck or not not t
end 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() Event.onInterval(1, function()
page.grid:update() page.grid:update()
page.grid:draw() page.grid:draw()
@@ -295,4 +278,4 @@ if not device.wireless_modem then
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@@ -1,4 +1,4 @@
local Alt = require('opus.alternate') local Array = require('opus.array')
local class = require('opus.class') local class = require('opus.class')
local Config = require('opus.config') local Config = require('opus.config')
local Event = require('opus.event') local Event = require('opus.event')
@@ -9,7 +9,6 @@ local Tween = require('opus.ui.tween')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local colors = _G.colors
local device = _G.device local device = _G.device
local fs = _G.fs local fs = _G.fs
local os = _G.os local os = _G.os
@@ -18,14 +17,25 @@ local shell = _ENV.shell
local term = _G.term local term = _G.term
local turtle = _G.turtle local turtle = _G.turtle
--[[
turtle: 39x13
computer: 51x19
pocket: 26x20
]]
if not _ENV.multishell then if not _ENV.multishell then
error('multishell is required') error('multishell is required')
end end
local REGISTRY_DIR = 'usr/.registry' local REGISTRY_DIR = 'usr/.registry'
local DEFAULT_ICON = NFT.parse("\0308\0317\153\153\153\153\153\
\0307\0318\153\153\153\153\153\ -- iconExt:gsub('.', function(b) return '\\' .. b:byte() end)
\0308\0317\153\153\153\153\153") 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 -- overview
local uid = _ENV.multishell.getCurrent() local uid = _ENV.multishell.getCurrent()
@@ -41,7 +51,7 @@ local config = {
} }
Config.load('Overview', config) Config.load('Overview', config)
local extSupport = Util.getVersion() >= 1.76 local extSupport = Util.supportsExtChars()
local applications = { } local applications = { }
local buttons = { } local buttons = { }
@@ -65,6 +75,7 @@ local function parseIcon(iconText)
if icon.height > 3 or icon.width > 8 then if icon.height > 3 or icon.width > 8 then
error('Must be an NFT image - 3 rows, 8 cols max') error('Must be an NFT image - 3 rows, 8 cols max')
end end
NFT.transparency(icon)
end end
return icon return icon
end) end)
@@ -76,45 +87,39 @@ local function parseIcon(iconText)
return s, m return s, m
end 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 { local page = UI.Page {
container = UI.Viewport { container = UI.Viewport {
x = cx, x = 9, y = 1,
y = cy, },
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 { tray = UI.Window {
y = -1, width = 8, y = -1, width = 8,
backgroundColor = colors.lightGray, backgroundColor = 'tertiary',
newApp = UI.Button { newApp = UI.FlatButton {
x = 2,
text = '+', event = 'new', text = '+', event = 'new',
}, },
--[[ mode = UI.FlatButton {
volume = UI.Button { x = 4,
x = 3, text = '=', event = 'display_mode',
text = '\15', event = 'volume', },
},]] help = UI.FlatButton {
x = 6,
text = '?', event = 'help',
},
}, },
editor = UI.SlideOut { editor = UI.SlideOut {
y = -12, height = 12, y = -12, height = 12,
backgroundColor = colors.cyan,
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
title = 'Edit Application', title = 'Edit Application',
event = 'slide_hide', event = 'slide_hide',
@@ -122,7 +127,7 @@ local page = UI.Page {
form = UI.Form { form = UI.Form {
y = 2, ey = -2, y = 2, ey = -2,
[1] = UI.TextEntry { [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, required = true,
}, },
[2] = UI.TextEntry { [2] = UI.TextEntry {
@@ -130,23 +135,50 @@ local page = UI.Page {
required = true, required = true,
}, },
[3] = UI.TextEntry { [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, required = true,
}, },
iconFile = UI.TextEntry { editIcon = UI.Button {
x = 11, ex = -12, y = 7, x = 11, y = 6,
limit = 128, help = 'Path to icon file', text = 'Edit', event = 'editIcon', help = 'Edit icon file',
shadowText = 'Path to icon file',
}, },
loadIcon = UI.Button { 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', text = 'Load', event = 'loadIcon', help = 'Load icon file',
}, },
image = UI.NftImage { image = UI.NftImage {
backgroundColor = colors.black, backgroundColor = 'black',
y = 7, x = 2, height = 3, width = 8, 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(), notification = UI.Notification(),
statusBar = UI.StatusBar(), statusBar = UI.StatusBar(),
}, },
@@ -205,7 +237,7 @@ local function loadApplications()
return requirements[a.requires] return requirements[a.requires]
end end
return true -- Util.startsWith(a.run, 'http') or shell.resolveProgram(a.run) return true
end) end)
local categories = { } local categories = { }
@@ -215,6 +247,7 @@ local function loadApplications()
categories[f.category] = true categories[f.category] = true
table.insert(buttons, { table.insert(buttons, {
text = f.category, text = f.category,
width = 8,
selected = config.currentCategory == f.category selected = config.currentCategory == f.category
}) })
end end
@@ -222,13 +255,13 @@ local function loadApplications()
table.sort(buttons, function(a, b) return a.text < b.text end) table.sort(buttons, function(a, b) return a.text < b.text end)
table.insert(buttons, 1, { text = 'Recent' }) 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 { page.tabBar.children = { }
tabBar = UI.VerticalTabBar { page.tabBar:addButtons(buttons)
buttons = buttons,
},
}
--page.tabBar:selectTab(config.currentCategory or 'Apps') --page.tabBar:selectTab(config.currentCategory or 'Apps')
page.container:setCategory(config.currentCategory or 'Apps') page.container:setCategory(config.currentCategory or 'Apps')
@@ -243,7 +276,6 @@ UI.Icon.defaults = {
function UI.Icon:eventHandler(event) function UI.Icon:eventHandler(event)
if event.type == 'mouse_click' then if event.type == 'mouse_click' then
self:setFocus(self.button) self:setFocus(self.button)
--self:emit({ type = self.button.event, button = self.button })
return true return true
elseif event.type == 'mouse_doubleclick' then elseif event.type == 'mouse_doubleclick' then
self:emit({ type = self.button.event, button = self.button }) self:emit({ type = self.button.event, button = self.button })
@@ -259,37 +291,23 @@ function page.container:setCategory(categoryName, animate)
self.children = { } self.children = { }
self:reset() self:reset()
local function filter(it, f) local filtered = { }
local ot = { }
for _,v in pairs(it) do
if f(v) then
table.insert(ot, v)
end
end
return ot
end
local filtered
if categoryName == 'Recent' then if categoryName == 'Recent' then
filtered = { }
for _,v in ipairs(config.Recent) do for _,v in ipairs(config.Recent) do
local app = Util.find(applications, 'key', v) local app = Util.find(applications, 'key', v)
if app then -- and fs.exists(app.run) then if app then
table.insert(filtered, app) table.insert(filtered, app)
end end
end end
else else
filtered = filter(applications, function(a) filtered = Array.filter(applications, function(a)
return a.category == categoryName -- and fs.exists(a.run) return a.category == categoryName
end) end)
table.sort(filtered, function(a, b) return a.title < b.title end) table.sort(filtered, function(a, b) return a.title < b.title end)
end end
for _,program in ipairs(filtered) do for _,program in ipairs(filtered) do
local icon local icon
if extSupport and program.iconExt then if extSupport and program.iconExt then
icon = parseIcon(program.iconExt) icon = parseIcon(program.iconExt)
@@ -304,27 +322,43 @@ function page.container:setCategory(categoryName, animate)
local title = ellipsis(program.title, 8) local title = ellipsis(program.title, 8)
local width = math.max(icon.width + 2, #title + 2) local width = math.max(icon.width + 2, #title + 2)
table.insert(self.children, UI.Icon({ if config.listMode then
width = width, table.insert(self.children, UI.Icon {
image = UI.NftImage({ width = self.width - 2,
x = math.floor((width - icon.width) / 2) + 1, height = 1,
image = icon, UI.Button {
width = 5, x = 1, ex = -1,
height = 3, text = program.title,
}), centered = false,
button = UI.Button({ backgroundColor = self:getProperty('backgroundColor'),
x = math.floor((width - #title - 2) / 2) + 1, backgroundFocusColor = 'gray',
y = 4, textColor = 'white',
text = title, textFocusColor = 'white',
backgroundColor = self.backgroundColor, event = 'button',
backgroundFocusColor = colors.gray, app = program,
textColor = colors.white, }
textFocusColor = colors.white, })
width = #title + 2, else
event = 'button', table.insert(self.children, UI.Icon({
app = program, 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 end
local gutter = 2 local gutter = 2
@@ -334,7 +368,8 @@ function page.container:setCategory(categoryName, animate)
local col, row = gutter, 2 local col, row = gutter, 2
local count = #self.children local count = #self.children
local r = math.random(1, 5) local r = math.random(1, 7)
local frames = 5
-- reposition all children -- reposition all children
for k,child in ipairs(self.children) do for k,child in ipairs(self.children) do
if r == 1 then if r == 1 then
@@ -356,19 +391,27 @@ function page.container:setCategory(categoryName, animate)
child.x = self.width child.x = self.width
child.y = self.height - 3 child.y = self.height - 3
end end
elseif r == 6 then
child.x = col
child.y = 1
elseif r == 7 then
child.x = 1
child.y = self.height - 3
end 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 if not animate then
child.x = col child.x = col
child.y = row child.y = row
end end
self:setViewHeight(row + (config.listMode and 1 or 4))
if k < count then if k < count then
col = col + child.width col = col + child.width
if col + self.children[k + 1].width + gutter - 2 > self.width then if col + self.children[k + 1].width + gutter - 2 > self.width then
col = gutter col = gutter
row = row + 5 row = row + (config.listMode and 1 or 5)
end end
end end
end end
@@ -378,15 +421,12 @@ function page.container:setCategory(categoryName, animate)
local function transition() local function transition()
local i = 1 local i = 1
return function() return function()
self:clear()
for _,child in pairs(self.children) do for _,child in pairs(self.children) do
child.tween:update(1) child.tween:update(1)
child.x = math.floor(child.x) child:move(math.floor(child.x), math.floor(child.y))
child.y = math.floor(child.y)
child:draw()
end end
i = i + 1 i = i + 1
return i < 7 return i <= frames
end end
end end
self:addTransition(transition) self:addTransition(transition)
@@ -428,16 +468,19 @@ function page:eventHandler(event)
shell.switchTab(shell.openTab(event.button.app.run)) shell.switchTab(shell.openTab(event.button.app.run))
elseif event.type == 'shell' then elseif event.type == 'shell' then
shell.switchTab(shell.openTab(Alt.get('shell'))) shell.switchTab(shell.openTab('shell'))
elseif event.type == 'lua' then elseif event.type == 'lua' then
shell.switchTab(shell.openTab(Alt.get('lua'))) shell.switchTab(shell.openTab('Lua'))
elseif event.type == 'files' then elseif event.type == 'files' then
shell.switchTab(shell.openTab(Alt.get('files'))) shell.switchTab(shell.openTab('Files'))
elseif event.type == 'network' then 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 elseif event.type == 'focus_change' then
if event.focused.parent.UIElement == 'Icon' then if event.focused.parent.UIElement == 'Icon' then
@@ -473,6 +516,13 @@ function page:eventHandler(event)
end end
self.editor:show({ category = category }) 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 elseif event.type == 'edit' then
local focused = page:getFocused() local focused = page:getFocused()
if focused.app then if focused.app then
@@ -480,7 +530,7 @@ function page:eventHandler(event)
end end
else else
UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
end end
return true return true
end end
@@ -502,11 +552,6 @@ function page.editor:show(app)
self:focusFirst() self:focusFirst()
end end
function page.editor.form.image:draw()
self:clear()
UI.NftImage.draw(self)
end
function page.editor:updateApplications(app) function page.editor:updateApplications(app)
if not app.key then if not app.key then
app.key = SHA.compute(app.title) app.key = SHA.compute(app.title)
@@ -516,36 +561,51 @@ function page.editor:updateApplications(app)
loadApplications() loadApplications()
end 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) function page.editor:eventHandler(event)
if event.type == 'form_cancel' or event.type == 'cancel' then if event.type == 'form_cancel' or event.type == 'cancel' then
self:hide() self:hide()
elseif event.type == 'focus_change' then elseif event.type == 'focus_change' then
self.statusBar:setStatus(event.focused.help or '') 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 elseif event.type == 'loadIcon' then
local s, m = pcall(function() self.file_open:show()
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
elseif event.type == 'form_invalid' then elseif event.type == 'form_invalid' then
self.notification:error(event.message) self.notification:error(event.message)
@@ -554,8 +614,6 @@ function page.editor:eventHandler(event)
local values = self.form.values local values = self.form.values
self:hide() self:hide()
self:updateApplications(values) self:updateApplications(values)
--page:refresh()
--page:draw()
config.currentCategory = values.category config.currentCategory = values.category
Config.update('Overview', config) Config.update('Overview', config)
os.queueEvent('overview_refresh') os.queueEvent('overview_refresh')
@@ -565,10 +623,6 @@ function page.editor:eventHandler(event)
return true return true
end end
UI:setPages({
main = page,
})
local function reload() local function reload()
loadApplications() loadApplications()
page:refresh() page:refresh()
@@ -594,5 +648,4 @@ end)
loadApplications() loadApplications()
UI:setPage(page) UI:setPage(page)
UI:start()
UI:pullEvents()

View File

@@ -1,4 +1,5 @@
local Ansi = require('opus.ansi') local Ansi = require('opus.ansi')
local Config = require('opus.config')
local Packages = require('opus.packages') local Packages = require('opus.packages')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
@@ -8,30 +9,34 @@ local term = _G.term
UI:configure('PackageManager', ...) UI:configure('PackageManager', ...)
local config = Config.load('package')
local page = UI.Page { local page = UI.Page {
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
x = 2, ex = 14, y = 2, ey = -5, x = 2, ex = 14, y = 2, ey = -6,
values = { }, values = { },
columns = { columns = {
{ heading = 'Package', key = 'name' }, { heading = ' Package', key = 'displayName' },
}, },
sortColumn = 'name', sortColumn = 'name',
autospace = true, autospace = true,
help = 'Select a package', help = 'Space to select, Enter to toggle',
}, },
add = UI.Button { installSelected = UI.Button {
x = 2, y = -3, x = 2, y = -3,
text = 'Install', text = ' + ',
event = 'action', event = 'batch_action',
help = 'Install or update', operation = 'install',
operationText = 'Install',
help = 'Install or update selected',
}, },
remove = UI.Button { removeSelected = UI.Button {
x = 12, y = -3, x = 8, y = -3,
text = 'Remove ', text = ' - ',
event = 'action', event = 'batch_action',
operation = 'uninstall', operation = 'uninstall',
operationText = 'Remove', operationText = 'Remove',
help = 'Remove', help = 'Remove selected',
}, },
updateall = UI.Button { updateall = UI.Button {
ex = -2, y = -3, width = 12, ex = -2, y = -3, width = 12,
@@ -41,10 +46,17 @@ local page = UI.Page {
}, },
description = UI.TextArea { description = UI.TextArea {
x = 16, y = 3, ey = -5, 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 { action = UI.SlideOut {
backgroundColor = colors.cyan,
titleBar = UI.TitleBar { titleBar = UI.TitleBar {
event = 'hide-action', event = 'hide-action',
}, },
@@ -79,7 +91,9 @@ function page:loadPackages()
end end
table.insert(self.grid.values, { table.insert(self.grid.values, {
installed = not not Packages:isInstalled(k), installed = not not Packages:isInstalled(k),
selected = false,
name = k, name = k,
displayName = k,
manifest = manifest, manifest = manifest,
}) })
end end
@@ -94,17 +108,65 @@ function page:loadPackages()
end end
function page.grid:getRowTextColor(row, selected) function page.grid:getRowTextColor(row, selected)
if row.selected then
return colors.cyan
end
if row.installed then if row.installed then
return colors.yellow return colors.yellow
end end
return UI.Grid.getRowTextColor(self, row, selected) return UI.Grid.getRowTextColor(self, row, selected)
end end
function page:getSelectedPackages()
local selected = { }
for _, row in pairs(self.grid.values) do
if row.selected then
table.insert(selected, row)
end
end
return selected
end
function page:getSelectedCount()
local count = 0
for _, row in pairs(self.grid.values) do
if row.selected then
count = count + 1
end
end
return count
end
function page:toggleSelection(row)
row.selected = not row.selected
row.displayName = row.selected and ('\187 ' .. row.name) or row.name
self.grid:draw()
self:updateStatus()
end
function page:clearSelection()
for _, row in pairs(self.grid.values) do
row.selected = false
row.displayName = row.name
end
self.grid:draw()
end
function page:updateStatus()
local count = self:getSelectedCount()
if count > 0 then
self.statusBar:setStatus(count .. ' package(s) selected')
else
local focused = self.grid:getSelected()
if focused then
self.statusBar:setStatus(focused.installed and 'Installed' or '')
end
end
end
function page.action:show() function page.action:show()
self.output.win:clear() self.output.win:clear()
UI.SlideOut.show(self) UI.SlideOut.show(self)
--self.output:draw()
--self.output.win.redraw()
end end
function page:run(operation, name) function page:run(operation, name)
@@ -126,12 +188,7 @@ function page:run(operation, name)
end end
function page:updateSelection(selected) function page:updateSelection(selected)
self.add.operation = selected.installed and 'update' or 'install' -- no-op: buttons are always active for batch operations
self.add.operationText = selected.installed and 'Update' or 'Install'
self.add.text = selected.installed and 'Update' or 'Install'
self.remove.inactive = not selected.installed
self.add:draw()
self.remove:draw()
end end
function page:eventHandler(event) function page:eventHandler(event)
@@ -141,42 +198,88 @@ function page:eventHandler(event)
elseif event.type == 'grid_focus_row' then elseif event.type == 'grid_focus_row' then
local manifest = event.selected.manifest 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.yellow, manifest.title,
Ansi.white, manifest.description) Ansi.white, manifest.description))
self.description:draw() self.description:draw()
self:updateSelection(event.selected) self:updateStatus()
elseif event.type == 'grid_select' then
-- Space or Enter toggles selection
local row = self.grid:getSelected()
if row then
self:toggleSelection(row)
end
elseif event.type == 'checkbox_change' then
config.compression = not config.compression
Config.update('package', config)
elseif event.type == 'updateall' then elseif event.type == 'updateall' then
self.operation = 'updateall' self.operation = 'updateall'
self.operationTargets = { }
self.action.button.text = ' Begin ' self.action.button.text = ' Begin '
self.action.button.event = 'begin' self.action.button.event = 'begin'
self.action.titleBar.title = 'Update All' self.action.titleBar.title = 'Update All'
self.action:show() self.action:show()
elseif event.type == 'action' then elseif event.type == 'batch_action' then
local selected = self.grid:getSelected() local targets = self:getSelectedPackages()
if selected then local operation = event.button.operation
self.operation = event.button.operation
self.action.button.text = event.button.operationText -- fall back to focused row if nothing selected
self.action.titleBar.title = selected.manifest.title if #targets == 0 then
self.action.button.text = ' Begin ' local focused = self.grid:getSelected()
self.action.button.event = 'begin' if focused then
self.action:show() targets = { focused }
end
end end
if #targets == 0 then return end
-- for install: update already-installed packages, install new ones
if operation == 'install' then
self.operation = 'install'
else
-- filter to only installed packages for uninstall
local installed = { }
for _, t in ipairs(targets) do
if t.installed then
table.insert(installed, t)
end
end
targets = installed
if #targets == 0 then return end
self.operation = 'uninstall'
end
self.operationTargets = targets
local title = #targets == 1
and targets[1].manifest.title
or (#targets .. ' packages')
self.action.button.text = ' Begin '
self.action.button.event = 'begin'
self.action.titleBar.title = string.format('%s %s',
operation == 'install' and 'Install/Update' or 'Remove', title)
self.action:show()
elseif event.type == 'hide-action' then elseif event.type == 'hide-action' then
self.action:hide() self.action:hide()
elseif event.type == 'begin' then elseif event.type == 'begin' then
if self.operation == 'updateall' then if self.operation == 'updateall' then
self:run(self.operation, '') self:run('updateall', '')
else else
local selected = self.grid:getSelected() for _, target in ipairs(self.operationTargets) do
self:run(self.operation, selected.name) local op = self.operation
selected.installed = Packages:isInstalled(selected.name) if op == 'install' and target.installed then
op = 'update'
self:updateSelection(selected) end
self:run(op, target.name)
target.installed = Packages:isInstalled(target.name)
end
self:clearSelection()
self:updateStatus()
end end
self.action.button.text = ' Done ' self.action.button.text = ' Done '
@@ -184,7 +287,7 @@ function page:eventHandler(event)
self.action.button:draw() self.action.button:draw()
elseif event.type == 'quit' then elseif event.type == 'quit' then
UI:exitPullEvents() UI:quit()
end end
UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
@@ -196,4 +299,4 @@ Packages:downloadList()
page:loadPackages() page:loadPackages()
page:sync() page:sync()
UI:pullEvents() UI:start()

238
sys/apps/Partition.lua Normal file
View 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()

View File

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

View File

@@ -5,14 +5,13 @@ local Util = require('opus.util')
local colors = _G.colors local colors = _G.colors
local device = _G.device local device = _G.device
local textutils = _G.textutils local textutils = _G.textutils
local peripheral = _G.peripheral
local multishell = _ENV.multishell local multishell = _ENV.multishell
local gridColumns = {} local gridColumns = {}
table.insert(gridColumns, { heading = '#', key = 'id', width = 5, align = 'right' }) 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 = 'Port', key = 'portid', width = 5, align = 'right' })
table.insert(gridColumns, { heading = 'Reply', key = 'replyid', 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' }) table.insert(gridColumns, { heading = 'Dist', key = 'distance', width = 6, align = 'right' })
end end
table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' }) table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' })
@@ -42,12 +41,13 @@ local page = UI.Page {
configSlide = UI.SlideOut { configSlide = UI.SlideOut {
y = -11, 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' }, accelerators = { ['backspace'] = 'config_close' },
configTabs = UI.Tabs { configTabs = UI.Tabs {
y = 2, y = 2,
filterTab = UI.Tab { filterTab = UI.Tab {
tabTitle = 'Filter', title = 'Filter',
noFill = true,
filterGridText = UI.Text { filterGridText = UI.Text {
x = 2, y = 2, x = 2, y = 2,
value = 'ID filter', value = 'ID filter',
@@ -93,7 +93,7 @@ local page = UI.Page {
}, },
}, },
modemTab = UI.Tab { modemTab = UI.Tab {
tabTitle = 'Modem', title = 'Modem',
channelGrid = UI.ScrollingGrid { channelGrid = UI.ScrollingGrid {
x = 2, y = 2, x = 2, y = 2,
width = 12, height = 5, width = 12, height = 5,
@@ -130,7 +130,6 @@ local page = UI.Page {
title = 'Packet Information', title = 'Packet Information',
event = 'packet_close', event = 'packet_close',
}, },
backgroundColor = colors.cyan,
accelerators = { accelerators = {
['backspace'] = 'packet_close', ['backspace'] = 'packet_close',
['left'] = 'prev_packet', ['left'] = 'prev_packet',
@@ -256,7 +255,7 @@ function page.packetSlide:eventHandler(event)
page:setFocus(page.packetGrid) page:setFocus(page.packetGrid)
elseif event.type == 'packet_lua' then 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 elseif event.type == 'prev_packet' then
local c = self.currentPacket local c = self.currentPacket
@@ -280,7 +279,7 @@ function page.packetSlide:eventHandler(event)
end end
function page.packetGrid:getDisplayValues(row) function page.packetGrid:getDisplayValues(row)
local row = Util.shallowCopy(row) row = Util.shallowCopy(row)
row.distance = Util.toBytes(Util.round(row.distance), 2) row.distance = Util.toBytes(Util.round(row.distance), 2)
return row return row
end end
@@ -356,7 +355,7 @@ function page:eventHandler(event)
self.packetSlide:show(event.selected) self.packetSlide:show(event.selected)
elseif event.type == 'quit' then elseif event.type == 'quit' then
Event.exitPullEvents() UI:quit()
else return UI.Page.eventHandler(self, event) else return UI.Page.eventHandler(self, event)
end end
@@ -386,4 +385,4 @@ if args[1] then
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

View File

@@ -6,62 +6,6 @@ local shell = _ENV.shell
UI:configure('System', ...) 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 function loadDirectory(dir)
local plugins = { } local plugins = { }
for _, file in pairs(fs.list(dir)) do for _, file in pairs(fs.list(dir)) do
@@ -70,16 +14,69 @@ local function loadDirectory(dir)
_G.printError('Error loading: ' .. file) _G.printError('Error loading: ' .. file)
error(m or 'Unknown error') error(m or 'Unknown error')
elseif s and m then 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
end end
return plugins return plugins
end end
local programDir = fs.getDir(shell.getRunningProgram()) local programDir = fs.getDir(_ENV.arg[0])
local plugins = loadDirectory(fs.combine(programDir, 'system'), { }) 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) elseif event.type == 'category_select' then
UI:pullEvents() 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()

View File

@@ -3,6 +3,7 @@ local UI = require('opus.ui')
local kernel = _G.kernel local kernel = _G.kernel
local multishell = _ENV.multishell local multishell = _ENV.multishell
local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines
UI:configure('Tasks', ...) UI:configure('Tasks', ...)
@@ -11,6 +12,7 @@ local page = UI.Page {
buttons = { buttons = {
{ text = 'Activate', event = 'activate' }, { text = 'Activate', event = 'activate' },
{ text = 'Terminate', event = 'terminate' }, { text = 'Terminate', event = 'terminate' },
{ text = 'Inspect', event = 'inspect' },
}, },
}, },
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
@@ -21,7 +23,7 @@ local page = UI.Page {
{ heading = 'Status', key = 'status' }, { heading = 'Status', key = 'status' },
{ heading = 'Time', key = 'timestamp' }, { heading = 'Time', key = 'timestamp' },
}, },
values = kernel.routines, values = tasks,
sortColumn = 'uid', sortColumn = 'uid',
autospace = true, autospace = true,
getDisplayValues = function (_, row) getDisplayValues = function (_, row)
@@ -38,7 +40,7 @@ local page = UI.Page {
}, },
accelerators = { accelerators = {
[ 'control-q' ] = 'quit', [ 'control-q' ] = 'quit',
space = 'activate', [ ' ' ] = 'activate',
t = 'terminate', t = 'terminate',
}, },
eventHandler = function (self, event) eventHandler = function (self, event)
@@ -48,10 +50,16 @@ local page = UI.Page {
multishell.setFocus(t.uid) multishell.setFocus(t.uid)
elseif event.type == 'terminate' then elseif event.type == 'terminate' then
multishell.terminate(t.uid) multishell.terminate(t.uid)
elseif event.type == 'inspect' then
multishell.openTab(_ENV, {
path = 'sys/apps/Lua.lua',
args = { t },
focused = true,
})
end end
end end
if event.type == 'quit' then if event.type == 'quit' then
Event.exitPullEvents() UI:quit()
end end
UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
@@ -64,4 +72,4 @@ Event.onInterval(1, function()
end) end)
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

54
sys/apps/Version.lua Normal file
View 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()

View File

@@ -2,6 +2,7 @@ local Ansi = require('opus.ansi')
local Security = require('opus.security') local Security = require('opus.security')
local SHA = require('opus.crypto.sha2') local SHA = require('opus.crypto.sha2')
local UI = require('opus.ui') local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors local colors = _G.colors
local os = _G.os local os = _G.os
@@ -16,9 +17,9 @@ local labelIntro = [[Set a friendly name for this computer.
local passwordIntro = [[A password is required for wireless access. local passwordIntro = [[A password is required for wireless access.
%sLeave blank to skip.]] %sLeave blank to skip.]]
local packagesIntro = [[Setup Complete local packagesIntro = [[Optional Packages
%sOpen the package manager to add software to this computer.]] %sSelect packages to install with this computer. You can always add more later from the Package Manager.]]
local contributorsIntro = [[Contributors%s local contributorsIntro = [[Contributors%s
Anavrins: Encryption/security/custom apps Anavrins: Encryption/security/custom apps
@@ -28,92 +29,127 @@ LDDestroier: Art design + custom apps
Lemmmy: Application improvements Lemmmy: Application improvements
%sContribute at:%s %sContribute at:%s
https://github.com/kepler155c/opus]] https://git.spatulaa.com/MayaTheShy/Opus]]
local page = UI.Page { local page = UI.Page {
wizard = UI.Wizard { wizard = UI.Wizard {
ey = -2, ey = -2,
pages = { splash = UI.WizardPage {
splash = UI.WizardPage { index = 1,
index = 1, intro = UI.TextArea {
intro = UI.TextArea { textColor = colors.yellow,
textColor = colors.yellow, inactive = true,
inactive = true, x = 3, ex = -3, y = 2, ey = -2,
x = 3, ex = -3, y = 2, ey = -2, value = string.format(splashIntro, Ansi.white),
value = string.format(splashIntro, Ansi.white),
},
}, },
label = UI.WizardPage { },
index = 2, label = UI.WizardPage {
labelText = UI.Text { index = 2,
x = 3, y = 2, labelText = UI.Text {
value = 'Label' 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,
}, },
password = UI.WizardPage { label = UI.TextEntry {
index = 3, x = 9, y = 2, ex = -3,
passwordLabel = UI.Text { limit = 32,
x = 3, y = 2, value = os.getComputerLabel(),
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 { intro = UI.TextArea {
index = 4, textColor = colors.yellow,
button = UI.Button { inactive = true,
x = 3, y = -3, x = 3, ex = -3, y = 4, ey = -3,
text = 'Open Package Manager', value = string.format(labelIntro, Ansi.white),
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 { validate = function (self)
index = 5, if self.label.value then
intro = UI.TextArea { os.setComputerLabel(self.label.value)
textColor = colors.yellow, end
inactive = true, return true
x = 3, ex = -3, y = 2, ey = -2, end,
value = string.format(contributorsIntro, Ansi.white, Ansi.yellow, Ansi.white), },
}, password = UI.WizardPage {
index = 3,
passwordLabel = UI.Text {
x = 3, y = 2,
value = 'Password'
},
newPass = UI.TextEntry {
x = 12, ex = -3, y = 2,
limit = 32,
mask = true,
shadowText = 'password',
},
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 5, ey = -3,
value = string.format(passwordIntro, Ansi.white),
},
validate = function (self)
if type(self.newPass.value) == "string" and #self.newPass.value > 0 then
Security.updatePassword(SHA.compute(self.newPass.value))
end
return true
end,
},
packages = UI.WizardPage {
index = 4,
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 2, ey = 5,
value = string.format(packagesIntro, Ansi.white),
},
chkTurtle = UI.Checkbox {
x = 5, y = 7,
label = 'RemoteTurtle',
textColor = 'yellow',
backgroundColor = 'primary',
value = false,
},
lblTurtle = UI.Text {
x = 22, y = 7,
value = 'Turtle control + web dashboard',
textColor = colors.lightGray,
},
chkInventory = UI.Checkbox {
x = 5, y = 9,
label = 'Inventory Manager',
textColor = 'yellow',
backgroundColor = 'primary',
value = false,
},
lblInventory = UI.Text {
x = 28, y = 9,
value = 'Storage automation system',
textColor = colors.lightGray,
},
button = UI.Button {
x = 3, y = -3,
text = 'More Packages...',
event = 'packages',
},
validate = function(self)
local toInstall = { }
if self.chkTurtle.value then
table.insert(toInstall, 'remoteturtle')
end
if self.chkInventory.value then
table.insert(toInstall, 'inventory-manager')
end
for _, pkg in ipairs(toInstall) do
shell.run('package install ' .. pkg)
end
return true
end,
},
contributors = UI.WizardPage {
index = 5,
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 2, ey = -2,
value = string.format(contributorsIntro, Ansi.white, Ansi.yellow, Ansi.white),
}, },
}, },
}, },
@@ -131,7 +167,7 @@ function page:eventHandler(event)
shell.openForegroundTab('PackageManager') shell.openForegroundTab('PackageManager')
elseif event.type == 'wizard_complete' or event.type == 'cancel' then elseif event.type == 'wizard_complete' or event.type == 'cancel' then
UI.exitPullEvents() UI:quit()
else else
return UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
@@ -140,4 +176,4 @@ function page:eventHandler(event)
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

24
sys/apps/compat.lua Normal file
View 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
View 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

22
sys/apps/genotp.lua Normal file
View File

@@ -0,0 +1,22 @@
local SHA = require("opus.crypto.sha2")
local acceptableCharacters = {}
for c = 0, 127 do
local char = string.char(c)
-- exclude potentially ambiguous characters
if char:match("[1-9a-zA-Z]") and char:match("[^OIl]") then
table.insert(acceptableCharacters, char)
end
end
local acceptableCharactersLen = #acceptableCharacters
local password = ""
for i = 1, 10 do
password = password .. acceptableCharacters[math.random(acceptableCharactersLen)]
end
os.queueEvent("set_otp", SHA.compute(password))
print("This allows one other device to permanently gain access to this device.")
print("Use the trust settings in System to revert this.")
print("Your one-time password is: " .. password)

View File

@@ -49,7 +49,7 @@ page = UI.Page {
backgroundColor = colors.red, backgroundColor = colors.red,
y = '50%', y = '50%',
properties = UI.Tab { properties = UI.Tab {
tabTitle = 'Properties', title = 'Properties',
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
headerBackgroundColor = colors.red, headerBackgroundColor = colors.red,
sortColumn = 'key', sortColumn = 'key',
@@ -64,7 +64,7 @@ page = UI.Page {
}, },
methodsTab = UI.Tab { methodsTab = UI.Tab {
index = 2, index = 2,
tabTitle = 'Methods', title = 'Methods',
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
ex = '50%', ex = '50%',
headerBackgroundColor = colors.red, headerBackgroundColor = colors.red,
@@ -85,7 +85,7 @@ page = UI.Page {
}, },
events = UI.Tab { events = UI.Tab {
index = 1, index = 1,
tabTitle = 'Events', title = 'Events',
UI.MenuBar { UI.MenuBar {
y = -1, y = -1,
backgroundColor = colors.red, backgroundColor = colors.red,
@@ -110,7 +110,7 @@ page = UI.Page {
self.grid:draw() self.grid:draw()
elseif event.type == 'grid_select' then elseif event.type == 'grid_select' then
multishell.openTab({ multishell.openTab(_ENV, {
path = 'sys/apps/Lua.lua', path = 'sys/apps/Lua.lua',
args = { event.selected.raw }, args = { event.selected.raw },
focused = true, 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) eventHandler = function (self, event)
if event.type == 'focus_change' and isRelevant(event.focused) then if event.type == 'focus_change' and isRelevant(event.focused) then
focused = event.focused focused = event.focused
@@ -144,7 +150,6 @@ page = UI.Page {
}) })
end end
self.tabs.properties.grid:setValues(t) self.tabs.properties.grid:setValues(t)
self.tabs.properties.grid:update()
self.tabs.properties.grid:draw() self.tabs.properties.grid:draw()
t = { } t = { }
@@ -156,7 +161,6 @@ page = UI.Page {
end end
end end
self.tabs.methodsTab.grid:setValues(t) self.tabs.methodsTab.grid:setValues(t)
self.tabs.methodsTab.grid:update()
self.tabs.methodsTab.grid:draw() self.tabs.methodsTab.grid:draw()
elseif event.type == 'edit_property' then elseif event.type == 'edit_property' then
@@ -168,6 +172,19 @@ page = UI.Page {
elseif event.type == 'editor_apply' then elseif event.type == 'editor_apply' then
self.editor:hide() 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 end
return UI.Page.eventHandler(self, event) return UI.Page.eventHandler(self, event)
@@ -175,4 +192,4 @@ page = UI.Page {
} }
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

200
sys/apps/memprofile.lua Normal file
View File

@@ -0,0 +1,200 @@
-- Memory Profiler for CC:Tweaked / Opus OS
-- Usage: memprofile [--watch] [--interval <seconds>]
--
-- Shows current Lua memory usage with breakdown estimates.
-- Use --watch to continuously monitor.
-- Useful for detecting memory leaks and understanding overhead.
local args = { ... }
local watch = false
local interval = 3
for i, arg in ipairs(args) do
if arg == '--watch' or arg == '-w' then
watch = true
elseif arg == '--interval' or arg == '-i' then
interval = tonumber(args[i + 1]) or 3
elseif arg == '--help' or arg == '-h' then
print('Usage: memprofile [--watch] [--interval <secs>]')
print('')
print('Options:')
print(' --watch, -w Continuously monitor memory')
print(' --interval, -i N Update interval in seconds (default: 3)')
print(' --help, -h Show this help')
return
end
end
local term = _G.term
local os = _G.os
local function formatBytes(bytes)
if bytes < 1024 then
return string.format('%d B', bytes)
elseif bytes < 1024 * 1024 then
return string.format('%.1f KB', bytes / 1024)
else
return string.format('%.2f MB', bytes / (1024 * 1024))
end
end
local function countTable(t, seen)
if type(t) ~= 'table' or seen[t] then return 0, 0 end
seen[t] = true
local entries = 0
local nested = 0
for k, v in pairs(t) do
entries = entries + 1
if type(v) == 'table' then
local e, n = countTable(v, seen)
nested = nested + 1 + e
entries = entries + n
end
if type(k) == 'table' then
local e, n = countTable(k, seen)
nested = nested + 1 + e
entries = entries + n
end
end
return entries, nested
end
local function getSnapshot()
-- Force a full GC cycle to get accurate usage
collectgarbage('collect')
collectgarbage('collect')
local memKB = collectgarbage('count') -- returns KB as float
local snapshot = {
totalKB = memKB,
totalBytes = math.floor(memKB * 1024),
timestamp = os.clock(),
}
-- Count entries in major global tables
local seen = {}
local globals = {}
local interesting = {
{ name = '_G (globals)', tbl = _G },
{ name = 'kernel', tbl = _G.kernel },
{ name = 'network', tbl = _G.network },
{ name = 'device', tbl = _G.device },
}
for _, item in ipairs(interesting) do
if type(item.tbl) == 'table' then
local entries, nested = countTable(item.tbl, seen)
table.insert(globals, {
name = item.name,
entries = entries,
nested = nested,
})
end
end
snapshot.globals = globals
-- Count routines if kernel is available
if _G.kernel and _G.kernel.routines then
snapshot.routines = #_G.kernel.routines
end
-- Count loaded modules
if package and package.loaded then
local count = 0
for _ in pairs(package.loaded) do
count = count + 1
end
snapshot.loadedModules = count
end
return snapshot
end
local function printSnapshot(snap, prev)
term.clear()
term.setCursorPos(1, 1)
local w = term.getSize()
local sep = string.rep('-', w)
term.setTextColor(colors.yellow)
print('=== Memory Profile ===')
term.setTextColor(colors.white)
print('')
-- Total memory
local memStr = formatBytes(snap.totalBytes)
local deltaStr = ''
if prev then
local delta = snap.totalBytes - prev.totalBytes
if delta > 0 then
deltaStr = string.format(' (+%s)', formatBytes(delta))
term.setTextColor(colors.red)
elseif delta < 0 then
deltaStr = string.format(' (-%s)', formatBytes(-delta))
term.setTextColor(colors.green)
end
end
term.setTextColor(colors.white)
print(string.format('Total Memory: %s%s', memStr, deltaStr))
print(string.format('Uptime: %.1fs', snap.timestamp))
print('')
-- Table sizes
term.setTextColor(colors.lightBlue)
print('Global Tables:')
term.setTextColor(colors.white)
print(sep)
print(string.format(' %-20s %8s %8s', 'Name', 'Entries', 'Nested'))
print(sep)
for _, g in ipairs(snap.globals) do
print(string.format(' %-20s %8d %8d', g.name, g.entries, g.nested))
end
print(sep)
print('')
-- Kernel info
if snap.routines then
term.setTextColor(colors.lightBlue)
print('Kernel:')
term.setTextColor(colors.white)
print(string.format(' Active routines: %d', snap.routines))
end
if snap.loadedModules then
print(string.format(' Loaded modules: %d', snap.loadedModules))
end
print('')
-- CC:Tweaked limits
term.setTextColor(colors.gray)
print('Note: CC:Tweaked default memory limit is ~128MB per computer.')
print('High memory usage may cause slowdowns or crashes.')
if watch then
print('')
term.setTextColor(colors.yellow)
print(string.format('Refreshing every %ds... (Ctrl+T to stop)', interval))
end
end
local function run()
local prev = nil
if watch then
while true do
local snap = getSnapshot()
printSnapshot(snap, prev)
prev = snap
os.sleep(interval)
end
else
local snap = getSnapshot()
printSnapshot(snap, nil)
end
end
run()

View File

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

View File

@@ -33,7 +33,7 @@ Event.on('generate_keypair', function()
table.insert(keyPairs, { generateKeyPair() }) table.insert(keyPairs, { generateKeyPair() })
_G._syslog('Generated keypair in ' .. timer()) _G._syslog('Generated keypair in ' .. timer())
if #keyPairs >= 3 then if #keyPairs >= 3 then
break break
end end
end end
end) end)

View File

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

View File

@@ -31,21 +31,14 @@ local function snmpConnection(socket)
socket:write('pong') socket:write('pong')
elseif msg.type == 'script' then elseif msg.type == 'script' then
local env = setmetatable(Util.shallowCopy(_ENV), { __index = _G }) kernel.run(_ENV, {
local fn, err = load(msg.args, 'script', nil, env) chunk = msg.args,
if fn then title = 'script',
kernel.run({ })
fn = fn,
env = env,
title = 'script',
})
else
_G.printError(err)
end
elseif msg.type == 'scriptEx' then elseif msg.type == 'scriptEx' then
local s, m = pcall(function() 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) local fn, m = load(msg.args, 'script', nil, env)
if not fn then if not fn then
error(m) error(m)
@@ -159,7 +152,7 @@ local function getSlots()
end end
local function sendInfo() local function sendInfo()
if os.clock() - infoTimer >= 1 then -- don't flood if os.clock() - infoTimer >= 5 then -- don't flood
infoTimer = os.clock() infoTimer = os.clock()
info.label = os.getComputerLabel() info.label = os.getComputerLabel()
info.uptime = math.floor(os.clock()) info.uptime = math.floor(os.clock())
@@ -201,16 +194,25 @@ local function sendInfo()
end end
end end
-- every 10 seconds, send out this computer's info local function cleanNetwork()
Event.onInterval(10, function()
sendInfo()
for _,c in pairs(_G.network) do for _,c in pairs(_G.network) do
local elapsed = os.clock()-c.timestamp local elapsed = os.clock()-c.timestamp
if c.active and elapsed > 15 then if c.active and elapsed > 50 then
c.active = false c.active = false
os.queueEvent('network_detach', c) os.queueEvent('network_detach', c)
end end
end end
end
-- every 30 seconds, send out this computer's info
-- send with offset so that messages are evenly distributed and do not all come at once
Event.onTimeout(math.random() * 30, function()
sendInfo()
cleanNetwork()
Event.onInterval(30, function()
sendInfo()
cleanNetwork()
end)
end) end)
Event.on('turtle_response', function() Event.on('turtle_response', function()
@@ -220,4 +222,5 @@ Event.on('turtle_response', function()
end end
end) end)
Event.onTimeout(1, sendInfo) -- send info early so that computers show soon after booting
Event.onTimeout(math.random() * 2 + 1, sendInfo)

View File

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

View File

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

View File

@@ -6,24 +6,45 @@ local Util = require('opus.util')
local trustId = '01c3ba27fe01383a03a1785276d99df27c3edcef68fbf231ca' 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 function trustConnection(socket)
local data = socket:read(2) local data = socket:read(2)
if data then if data then
local password = Security.getPassword() local trustKey = Security.getTrustKey()
if not password then if not trustKey then
socket:write({ msg = 'No password has been set' }) socket:write({ msg = 'No password has been set' })
else else
local s if validateData(data, trustKey, socket.dhost) then
s, data = pcall(Crypto.decrypt, data, password) print("Accepted trust from " .. socket.dhost)
if s and data and data.pk and data.dh == socket.dhost then
local trustList = Util.readTable('usr/.known_hosts') or { }
trustList[data.dh] = data.pk
Util.writeTable('usr/.known_hosts', trustList)
socket:write({ success = true, msg = 'Trust accepted' }) socket:write({ success = true, msg = 'Trust accepted' })
else return
socket:write({ msg = 'Invalid password' })
end 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 end
end end
@@ -44,3 +65,12 @@ Event.addRoutine(function()
end end
end end
end) end)
Event.addRoutine(function()
while true do
local _event, password = os.pullEvent("set_otp")
oneTimePassword = password
print("got new one-time password")
end
end)

View File

@@ -1,6 +1,9 @@
local BulkGet = require('opus.bulkget') local BulkGet = require('opus.bulkget')
local Config = require('opus.config')
local Git = require('opus.git') local Git = require('opus.git')
local LZW = require('opus.compress.lzw')
local Packages = require('opus.packages') local Packages = require('opus.packages')
local Tar = require('opus.compress.tar')
local Util = require('opus.util') local Util = require('opus.util')
local fs = _G.fs local fs = _G.fs
@@ -16,9 +19,8 @@ local function makeSandbox()
end end
local function Syntax(msg) local function Syntax(msg)
_G.printError(msg) print('Syntax: package list | install [name] ... | update [name] | updateall | uninstall [name]\n')
print('\nSyntax: Package list | install [name] ... | update [name] | uninstall [name]') error(msg)
error(0)
end end
local function progress(max) local function progress(max)
@@ -76,6 +78,24 @@ local function install(name, isUpdate, ignoreDeps)
local packageDir = fs.combine('packages', name) local packageDir = fs.combine('packages', name)
local list = Git.list(manifest.repository) local list = Git.list(manifest.repository)
-- apply exclude filters from manifest
if manifest.exclude then
for path in pairs(list) do
for _, pattern in ipairs(manifest.exclude) do
if path:match(pattern) then
list[path] = nil
break
end
end
end
end
-- clear out contents before install/update
-- TODO: figure out whether to run
-- install/uninstall for the package
fs.delete(packageDir)
local showProgress = progress(Util.size(list)) local showProgress = progress(Util.size(list))
local getList = { } local getList = { }
@@ -96,6 +116,12 @@ local function install(name, isUpdate, ignoreDeps)
if not isUpdate then if not isUpdate then
runScript(manifest.install) runScript(manifest.install)
end 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 end
if action == 'list' then if action == 'list' then
@@ -152,6 +178,7 @@ if action == 'uninstall' then
local packageDir = fs.combine('packages', name) local packageDir = fs.combine('packages', name)
fs.delete(packageDir) fs.delete(packageDir)
fs.delete(packageDir .. '.tar.lzw')
print('removed: ' .. packageDir) print('removed: ' .. packageDir)
return return
end end

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,18 +2,17 @@ local Ansi = require('opus.ansi')
local Config = require('opus.config') local Config = require('opus.config')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
-- -t80x30
if _G.http.websocket then if _G.http.websocket then
local config = Config.load('cloud') local config = Config.load('cloud')
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Cloud', title = 'Cloud',
description = 'Cloud Catcher options', description = 'Cloud Catcher options',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
key = UI.TextEntry { key = UI.TextEntry {
x = 3, ex = -3, y = 2, x = 3, ex = -3, y = 3,
limit = 32, limit = 32,
value = config.key, value = config.key,
shadowText = 'Cloud key', shadowText = 'Cloud key',
@@ -22,14 +21,15 @@ if _G.http.websocket then
}, },
}, },
button = UI.Button { button = UI.Button {
x = 3, y = 4, x = -8, ex = -2, y = -2,
text = 'Update', text = 'Apply',
event = 'update_key', event = 'update_key',
}, },
labelText = UI.TextArea { labelText = UI.TextArea {
x = 3, ex = -3, y = 6, x = 2, ex = -2, y = 5, ey = -4,
textColor = colors.yellow, textColor = 'yellow',
marginLeft = 0, marginRight = 0, backgroundColor = 'black',
marginLeft = 1, marginRight = 1, marginTop = 1,
value = string.format( value = string.format(
[[Use a non-changing cloud key. Note that only a single computer can use this session at one time. [[Use a non-changing cloud key. Note that only a single computer can use this session at one time.
To obtain a key, visit: To obtain a key, visit:

View File

@@ -16,12 +16,12 @@ local NftImages = {
} }
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Disks Usage', title = 'Disks Usage',
description = 'Visualise HDD and disks usage', description = 'Visualise HDD and disks usage',
drives = UI.ScrollingGrid { drives = UI.ScrollingGrid {
x = 2, y = 1, x = 2, y = 2,
ex = '47%', ey = -7, ex = '47%', ey = -8,
columns = { columns = {
{ heading = 'Drive', key = 'name' }, { heading = 'Drive', key = 'name' },
{ heading = 'Side' ,key = 'side', textColor = colors.yellow } { heading = 'Side' ,key = 'side', textColor = colors.yellow }
@@ -30,7 +30,7 @@ local tab = UI.Tab {
}, },
infos = UI.Grid { infos = UI.Grid {
x = '52%', y = 2, x = '52%', y = 2,
ex = -2, ey = -4, ex = -2, ey = -8,
disableHeader = true, disableHeader = true,
unfocusedBackgroundSelectedColor = colors.black, unfocusedBackgroundSelectedColor = colors.black,
inactive = true, inactive = true,
@@ -40,18 +40,23 @@ local tab = UI.Tab {
{ key = 'value', align = 'right', textColor = colors.yellow }, { key = 'value', align = 'right', textColor = colors.yellow },
} }
}, },
[1] = UI.Window {
x = 2, y = -6, ex = -2, ey = -2,
backgroundColor = colors.black,
},
progress = UI.ProgressBar { progress = UI.ProgressBar {
x = 11, y = -2, x = 11, y = -3,
ex = -2, ex = -3,
}, },
percentage = UI.Text { percentage = UI.Text {
x = 11, y = -3, y = -4, width = 5,
ex = '47%', x = 12,
align = 'center', --align = 'center',
backgroundColor = colors.black,
}, },
icon = UI.NftImage { icon = UI.NftImage {
x = 2, y = -5, x = 2, y = -6, ey = -2,
backgroundColor = colors.black,
image = NFT.parse(NftImages.blank) image = NFT.parse(NftImages.blank)
}, },
} }
@@ -131,6 +136,17 @@ function tab:enable()
self:updateDrives() self:updateDrives()
self:updateInfo() self:updateInfo()
UI.Tab.enable(self) 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 end
function tab:eventHandler(event) function tab:eventHandler(event)
@@ -142,11 +158,4 @@ function tab:eventHandler(event)
return true return true
end end
Event.on({ 'disk', 'disk_eject' }, function()
os.sleep(1)
tab:updateDrives()
tab:updateInfo()
tab:sync()
end)
return tab return tab

View File

@@ -4,11 +4,11 @@ local colors = _G.colors
local peripheral = _G.peripheral local peripheral = _G.peripheral
local settings = _G.settings local settings = _G.settings
local tab = UI.Tab { return peripheral.find('monitor') and UI.Tab {
tabTitle = 'Kiosk', title = 'Kiosk',
description = 'Kiosk options', description = 'Kiosk options',
form = UI.Form { form = UI.Form {
x = 2, ex = -2, x = 2, y = 2, ex = -2, ey = 5,
manualControls = true, manualControls = true,
monitor = UI.Chooser { monitor = UI.Chooser {
formLabel = 'Monitor', formKey = 'monitor', formLabel = 'Monitor', formKey = 'monitor',
@@ -22,41 +22,36 @@ local tab = UI.Tab {
}, },
help = 'Adjust text scaling', 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() peripheral.find('monitor', function(side)
local choices = { } table.insert(choices, { name = side, value = side })
end)
peripheral.find('monitor', function(side) self.form.monitor.choices = choices
table.insert(choices, { name = side, value = side }) self.form.monitor.value = settings.get('kiosk.monitor')
end)
self.form.monitor.choices = choices self.form.textScale.value = settings.get('kiosk.textscale')
self.form.monitor.value = settings.get('kiosk.monitor')
self.form.textScale.value = settings.get('kiosk.textscale') UI.Tab.enable(self)
end,
UI.Tab.enable(self) eventHandler = function(self, event)
end if event.type == 'choice_change' then
if self.form.monitor.value then
function tab:eventHandler(event) settings.set('kiosk.monitor', self.form.monitor.value)
if event.type == 'choice_change' then end
if self.form.monitor.value then if self.form.textScale.value then
settings.set('kiosk.monitor', self.form.monitor.value) settings.set('kiosk.textscale', self.form.textScale.value)
end
settings.save('.settings')
end end
if self.form.textScale.value then
settings.set('kiosk.textscale', self.form.textScale.value)
end
settings.save('.settings')
end end
end }
if peripheral.find('monitor') then
return tab
end

View File

@@ -4,46 +4,47 @@ local Util = require('opus.util')
local fs = _G.fs local fs = _G.fs
local os = _G.os local os = _G.os
local labelTab = UI.Tab { return UI.Tab {
tabTitle = 'Label', title = 'Label',
description = 'Set the computer label', description = 'Set the computer label',
labelText = UI.Text { labelText = UI.Text {
x = 3, y = 2, x = 3, y = 3,
value = 'Label' value = 'Label'
}, },
label = UI.TextEntry { label = UI.TextEntry {
x = 9, y = 2, ex = -4, x = 9, y = 3, ex = -4,
limit = 32, limit = 32,
value = os.getComputerLabel(), value = os.getComputerLabel(),
accelerators = { accelerators = {
enter = 'update_label', enter = 'update_label',
}, },
}, },
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 3, x = 2, y = 5, ex = -2, ey = -2,
values = { values = {
{ name = '', value = '' }, { name = '', value = '' },
{ name = 'CC version', value = Util.getVersion() }, { name = 'CC version', value = ("%d.%d"):format(Util.getVersion()) },
{ name = 'Lua version', value = _VERSION }, { name = 'Lua version', value = _VERSION },
{ name = 'MC version', value = Util.getMinecraftVersion() }, { name = 'MC version', value = Util.getMinecraftVersion() },
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) }, { name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
{ name = 'Computer ID', value = tostring(os.getComputerID()) }, { name = 'Computer ID', value = tostring(os.getComputerID()) },
{ name = 'Day', value = tostring(os.day()) }, { name = 'Day', value = tostring(os.day()) },
}, },
disableHeader = true,
inactive = true, inactive = true,
columns = { columns = {
{ key = 'name', width = 12 }, { 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

View File

@@ -7,14 +7,17 @@ local fs = _G.fs
local config = Config.load('multishell') local config = Config.load('multishell')
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Launcher', title = 'Launcher',
description = 'Set the application launcher', description = 'Set the application launcher',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 5,
},
launcherLabel = UI.Text { launcherLabel = UI.Text {
x = 3, y = 2, x = 3, y = 3,
value = 'Launcher', value = 'Launcher',
}, },
launcher = UI.Chooser { launcher = UI.Chooser {
x = 13, y = 2, width = 12, x = 13, y = 3, width = 12,
choices = { choices = {
{ name = 'Overview', value = 'sys/apps/Overview.lua' }, { name = 'Overview', value = 'sys/apps/Overview.lua' },
{ name = 'Shell', value = 'sys/apps/ShellLauncher.lua' }, { name = 'Shell', value = 'sys/apps/ShellLauncher.lua' },
@@ -22,18 +25,19 @@ local tab = UI.Tab {
}, },
}, },
custom = UI.TextEntry { custom = UI.TextEntry {
x = 13, ex = -3, y = 3, x = 13, ex = -3, y = 4,
limit = 128,
shadowText = 'File name', shadowText = 'File name',
}, },
button = UI.Button { button = UI.Button {
x = 3, y = 5, x = -8, ex = -2, y = -2,
text = 'Update', text = 'Apply',
event = 'update', event = 'update',
}, },
labelText = UI.TextArea { labelText = UI.TextArea {
x = 3, ex = -3, y = 7, x = 2, ex = -2, y = 6, ey = -4,
backgroundColor = colors.black,
textColor = colors.yellow, textColor = colors.yellow,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = 'Choose an application launcher', value = 'Choose an application launcher',
}, },
} }

View File

@@ -2,59 +2,61 @@ local Ansi = require('opus.ansi')
local Config = require('opus.config') local Config = require('opus.config')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors
local device = _G.device local device = _G.device
local tab = UI.Tab { return UI.Tab {
tabTitle = 'Network', title = 'Network',
description = 'Networking options', description = 'Networking options',
info = UI.TextArea { 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( value = string.format(
[[%sSet the primary modem used for wireless communications.%s [[%sSet the primary modem used for wireless communications.%s
Reboot to take effect.]], Ansi.yellow, Ansi.reset) Reboot to take effect.]], Ansi.yellow, Ansi.reset)
}, },
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
label = UI.Text { label = UI.Text {
x = 3, y = 2, x = 3, y = 3,
value = 'Modem', value = 'Modem',
}, },
modem = UI.Chooser { modem = UI.Chooser {
x = 10, ex = -3, y = 2, x = 10, ex = -3, y = 3,
nochoice = 'auto', nochoice = 'auto',
}, },
} enable = function(self)
local width = 7
local choices = {
{ name = 'auto', value = 'auto' },
{ name = 'disable', value = 'none' },
}
function tab:enable() for k,v in pairs(device) do
local width = 7 if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
local choices = { table.insert(choices, { name = k, value = v.name })
{ name = 'auto', value = 'auto' }, width = math.max(width, #k)
{ name = 'disable', value = 'none' }, end
} end
for k,v in pairs(device) do self.modem.choices = choices
if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then --self.modem.width = width + 4
table.insert(choices, { name = k, value = v.name })
width = math.max(width, #k) 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
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

View File

@@ -2,11 +2,12 @@ local Security = require('opus.security')
local SHA = require('opus.crypto.sha2') local SHA = require('opus.crypto.sha2')
local UI = require('opus.ui') local UI = require('opus.ui')
local colors = _G.colors return UI.Tab {
title = 'Password',
local passwordTab = UI.Tab {
tabTitle = 'Password',
description = 'Wireless network password', description = 'Wireless network password',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
newPass = UI.TextEntry { newPass = UI.TextEntry {
x = 3, ex = -3, y = 3, x = 3, ex = -3, y = 3,
limit = 32, limit = 32,
@@ -17,28 +18,28 @@ local passwordTab = UI.Tab {
}, },
}, },
button = UI.Button { button = UI.Button {
x = 3, y = 5, x = -8, ex = -2, y = -2,
text = 'Update', text = 'Apply',
event = 'update_password', event = 'update_password',
}, },
info = UI.TextArea { info = UI.TextArea {
x = 3, ex = -3, y = 7, x = 2, ex = -2, y = 5, ey = -4,
textColor = colors.yellow, backgroundColor = 'black',
textColor = 'yellow',
inactive = true, inactive = true,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = 'Add a password to enable other computers to connect to this one.', value = 'Add a password to enable other computers to connect to this one.',
} },
} eventHandler = function(self, event)
function passwordTab:eventHandler(event) if event.type == 'update_password' then
if event.type == 'update_password' then if not self.newPass.value or #self.newPass.value == 0 then
if not self.newPass.value or #self.newPass.value == 0 then self:emit({ type = 'error_message', message = 'Invalid password' })
self:emit({ type = 'error_message', message = 'Invalid password' })
else else
Security.updatePassword(SHA.compute(self.newPass.value)) Security.updatePassword(SHA.compute(self.newPass.value))
self:emit({ type = 'success_message', message = 'Password updated' }) self:emit({ type = 'success_message', message = 'Password updated' })
end
return true
end end
return true
end end
end }
return passwordTab

View File

@@ -3,12 +3,14 @@ local UI = require('opus.ui')
local Util = require('opus.util') local Util = require('opus.util')
local tab = UI.Tab { local tab = UI.Tab {
tabTitle = 'Path', title = 'Path',
description = 'Set the shell path', description = 'Set the shell path',
tabClose = true, tabClose = true,
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
entry = UI.TextEntry { entry = UI.TextEntry {
x = 2, y = 2, ex = -2, x = 3, y = 3, ex = -3,
limit = 256,
shadowText = 'enter new path', shadowText = 'enter new path',
accelerators = { accelerators = {
enter = 'update_path', enter = 'update_path',
@@ -16,7 +18,7 @@ local tab = UI.Tab {
help = 'add a new path', help = 'add a new path',
}, },
grid = UI.Grid { grid = UI.Grid {
y = 4, ey = -3, x = 2, y = 6, ex = -2, ey = -3,
disableHeader = true, disableHeader = true,
columns = { { key = 'value' } }, columns = { { key = 'value' } },
autospace = true, autospace = true,

View File

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

View File

@@ -2,48 +2,93 @@ local UI = require('opus.ui')
local settings = _G.settings local settings = _G.settings
if settings then local transform = {
local settingsTab = UI.Tab { string = tostring,
tabTitle = 'Settings', number = tonumber,
description = 'Computercraft configurable settings', }
grid = UI.Grid {
y = 2,
autospace = true,
sortColumn = 'name',
columns = {
{ heading = 'Setting', key = 'name' },
{ heading = 'Value', key = 'value' },
},
},
}
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 = { } local values = { }
for _,v in pairs(settings.getNames()) do for _,v in pairs(settings.getNames()) do
local value = settings.get(v)
if not value then
value = false
end
table.insert(values, { table.insert(values, {
name = v, name = v,
value = value, value = settings.get(v) or false,
}) })
end end
self.grid:setValues(values) self.grid:setValues(values)
self.grid:setIndex(1)
end,
enable = function(self)
self:reload()
UI.Tab.enable(self) UI.Tab.enable(self)
end end,
eventHandler = function(self, event)
function settingsTab:eventHandler(event)
if event.type == 'grid_select' then 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 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 end
settings.set(event.selected.name, event.selected.value)
settings.save('.settings')
self.grid:draw()
return true return true
end end
end end,
}
return settingsTab
end

View File

@@ -18,9 +18,7 @@ local defaults = {
textColor = colors.white, textColor = colors.white,
commandTextColor = colors.yellow, commandTextColor = colors.yellow,
directoryTextColor = colors.orange, directoryTextColor = colors.orange,
directoryBackgroundColor = colors.black,
promptTextColor = colors.blue, promptTextColor = colors.blue,
promptBackgroundColor = colors.black,
directoryColor = colors.green, directoryColor = colors.green,
fileColor = colors.white, fileColor = colors.white,
backgroundColor = colors.black, backgroundColor = colors.black,
@@ -28,7 +26,7 @@ local defaults = {
local _colors = config.color or Util.shallowCopy(defaults) local _colors = config.color or Util.shallowCopy(defaults)
local allSettings = { } local allSettings = { }
for k, v in pairs(defaults) do for k in pairs(defaults) do
table.insert(allSettings, { name = k }) table.insert(allSettings, { name = k })
end end
@@ -38,29 +36,34 @@ if not _colors.backgroundColor then
_colors.fileColor = colors.white _colors.fileColor = colors.white
end end
local tab = UI.Tab { return UI.Tab {
tabTitle = 'Shell', title = 'Shell',
description = 'Shell options', description = 'Shell options',
grid1 = UI.ScrollingGrid { grid1 = UI.ScrollingGrid {
y = 2, ey = -10, x = 3, ex = -16, y = 2, ey = -10, x = 2, ex = -17,
disableHeader = true, disableHeader = true,
columns = { { key = 'name' } }, columns = { { key = 'name' } },
values = allSettings, values = allSettings,
sortColumn = 'name', sortColumn = 'name',
}, },
grid2 = UI.ScrollingGrid { grid2 = UI.ScrollingGrid {
y = 2, ey = -10, x = -14, ex = -3, y = 2, ey = -10, x = -14, ex = -2,
disableHeader = true, disableHeader = true,
columns = { { key = 'name' } }, columns = { { key = 'name' } },
values = allColors, values = allColors,
sortColumn = 'name', sortColumn = 'name',
}, getRowTextColor = function(self, row)
directoryLabel = UI.Text { local selected = self.parent.grid1:getSelected()
x = 2, y = -2, if _colors[selected.name] == row.value then
value = 'Display directory', return colors.yellow
end
return UI.Grid.getRowTextColor(self, row)
end
}, },
directory = UI.Checkbox { directory = UI.Checkbox {
x = 20, y = -2, x = 2, y = -2,
labelBackgroundColor = colors.black,
label = 'Directory',
value = config.displayDirectory value = config.displayDirectory
}, },
reset = UI.Button { reset = UI.Button {
@@ -74,69 +77,57 @@ local tab = UI.Tab {
event = 'update', event = 'update',
}, },
display = UI.Window { 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
View 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
View 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
}

View File

@@ -9,7 +9,7 @@ kernel.hook('clipboard_copy', function(_, args)
keyboard.clipboard = args[1] keyboard.clipboard = args[1]
end) end)
keyboard.addHotkey('shift-paste', function() local function queuePaste()
local data = keyboard.clipboard local data = keyboard.clipboard
if type(data) == 'table' then if type(data) == 'table' then
@@ -20,4 +20,7 @@ keyboard.addHotkey('shift-paste', function()
if data then if data then
os.queueEvent('paste', data) os.queueEvent('paste', data)
end end
end) end
kernel.hook('clipboard_paste', queuePaste)
keyboard.addHotkey('shift-paste', queuePaste)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,7 +50,10 @@
title = "System", title = "System",
category = "System", category = "System",
icon = "\030 \0307\031f| \010\0307\031f---o\030 \031 \010\030 \009 \0307\031f| ", 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 = "22€070†b02‹4Ÿ24\
02—7Ž704ˆ4€€€€\
7ƒ07„1ƒ7‹24ƒƒ",
--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", run = "System.lua",
}, },
[ "2a4d562b1d9a9c90bdede6fac8ce4f7402462b86" ] = { [ "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", 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", run = "/rom/programs/fun/dj",
}, },
[ "4dbdd221e957eff27cc47796f3ed8447290f71c7ad8b95e5bd828b31c1858f15" ] = {
title = "Partition",
category = "System",
iconExt = "\30\55\31\55\128\30\48\135\131\139\30\55\128\128\128\10\30\48\31\55\149\31\48\128\30\55\145\30\48\31\56\140\30\55\157\144\144\10\30\55\31\55\128\31\48\139\143\135\31\55\128\31\56\142\133",
run = "Partition",
}
} }

View File

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

6
sys/etc/packages.list Normal file
View File

@@ -0,0 +1,6 @@
{
["platform"] = "https://git.spatulaa.com/MayaTheShy/cc-platform-core/raw/branch/master/.package",
["remoteturtle"] = "https://git.spatulaa.com/MayaTheShy/remoteturtle/raw/branch/master/.package",
["inventory-manager"] = "https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/stable/.package",
["inventory-manager-unstable"] = "https://git.spatulaa.com/MayaTheShy/Inventory-Manager-CC/raw/branch/main/.package",
}

9
sys/help/Networking.txt Normal file
View 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>

View File

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

View File

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

View File

@@ -6,10 +6,11 @@ Shortcut keys
* l: Lua application * l: Lua application
* f: Files * f: Files
* e: Edit an application (or right-click) * e: Edit an application (or right-click)
* n: Network
* control-n: Add a new application * control-n: Add a new application
* delete: Delete an application * delete: Delete an application
Adding a new application Adding a new application
======================== ========================
The run entry can be either a disk file or a URL. 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
View 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.

View File

@@ -1,5 +1,3 @@
_G.requireInjector(_ENV)
local Peripheral = require('opus.peripheral') local Peripheral = require('opus.peripheral')
_G.device = Peripheral.getList() _G.device = Peripheral.getList()

View File

@@ -4,11 +4,8 @@ if fs.native then
return return
end end
_G.requireInjector(_ENV)
local Util = require('opus.util') local Util = require('opus.util')
-- TODO: support getDrive for virtual nodes
fs.native = Util.shallowCopy(fs) fs.native = Util.shallowCopy(fs)
local fstypes = { } local fstypes = { }
@@ -22,8 +19,11 @@ for k,fn in pairs(fs) do
end end
end end
function nativefs.list(node, dir) function nativefs.resolve(_, dir)
return dir
end
function nativefs.list(node, dir)
local files local files
if fs.native.isDir(dir) then if fs.native.isDir(dir) then
files = fs.native.list(dir) files = fs.native.list(dir)
@@ -83,6 +83,18 @@ function nativefs.isDir(node, dir)
return fs.native.isDir(dir) return fs.native.isDir(dir)
end 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) function nativefs.exists(node, dir)
if node.mountPoint == dir then if node.mountPoint == dir then
return true return true
@@ -138,8 +150,10 @@ local function getNode(dir)
return node return node
end end
fs.getNode = getNode
local methods = { 'delete', 'getFreeSpace', 'exists', 'isDir', 'getSize', local methods = { 'delete', 'getFreeSpace', 'exists', 'isDir', 'getSize',
'isReadOnly', 'makeDir', 'getDrive', 'list', 'open' } 'isReadOnly', 'makeDir', 'getDrive', 'list', 'open', 'attributes' }
for _,m in pairs(methods) do for _,m in pairs(methods) do
fs[m] = function(dir, ...) fs[m] = function(dir, ...)
@@ -149,6 +163,12 @@ for _,m in pairs(methods) do
end end
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) function fs.complete(partial, dir, includeFiles, includeSlash)
dir = fs.combine(dir, '') dir = fs.combine(dir, '')
local node = getNode(dir) local node = getNode(dir)
@@ -158,6 +178,13 @@ function fs.complete(partial, dir, includeFiles, includeSlash)
return fs.native.complete(partial, dir, includeFiles, includeSlash) return fs.native.complete(partial, dir, includeFiles, includeSlash)
end end
local displayFlags = {
urlfs = 'U',
linkfs = 'L',
ramfs = 'T',
netfs = 'N',
}
function fs.listEx(dir) function fs.listEx(dir)
dir = fs.combine(dir, '') dir = fs.combine(dir, '')
local node = getNode(dir) local node = getNode(dir)
@@ -168,20 +195,22 @@ function fs.listEx(dir)
local t = { } local t = { }
local files = node.fs.list(node, dir) 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 fullName = fs.combine(dir, f)
local n = fs.getNode(fullName)
local file = { local file = {
name = f, name = f,
isDir = fs.isDir(fullName), isDir = fs.isDir(fullName),
isReadOnly = fs.isReadOnly(fullName), isReadOnly = fs.isReadOnly(fullName),
fstype = n.mountPoint == fullName and displayFlags[n.fstype],
} }
if not file.isDir then if not file.isDir then
file.size = fs.getSize(fullName) file.size = fs.getSize(fullName)
end end
table.insert(t, file) table.insert(t, file)
end end)
end) end
return t return t
end end
@@ -206,12 +235,12 @@ function fs.copy(s, t)
end end
else else
local sf = Util.readFile(s) local sf = Util.readFile(s, 'rb')
if not sf then if not sf then
error('No such file') error('No such file')
end end
Util.writeFile(t, sf) Util.writeFile(t, sf, 'wb')
end end
end end
@@ -265,11 +294,17 @@ local function getfstype(fstype)
end end
function fs.mount(path, fstype, ...) function fs.mount(path, fstype, ...)
local vfs = getfstype(fstype) local vfs = getfstype(fstype)
if not vfs then if not vfs then
error('Invalid file system type') error('Invalid file system type')
end 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, ...) local node = vfs.mount(path, ...)
if node then if node then
local parts = splitpath(path) local parts = splitpath(path)
@@ -284,12 +319,16 @@ function fs.mount(path, fstype, ...)
tp.nodes[d] = Util.shallowCopy(tp) tp.nodes[d] = Util.shallowCopy(tp)
tp.nodes[d].nodes = { } tp.nodes[d].nodes = { }
tp.nodes[d].mountPoint = fs.combine(tp.mountPoint, d) tp.nodes[d].mountPoint = fs.combine(tp.mountPoint, d)
tp.nodes[d].created = os.epoch('utc')
tp.nodes[d].modification = os.epoch('utc')
end end
tp = tp.nodes[d] tp = tp.nodes[d]
end end
node.fs = vfs node.fs = vfs
node.fstype = fstype node.fstype = fstype
node.created = node.created or os.epoch('utc')
node.modification = node.modification or os.epoch('utc')
if not targetName then if not targetName then
node.mountPoint = '' node.mountPoint = ''
fs.nodes = node fs.nodes = node
@@ -355,4 +394,4 @@ function fs.restore()
local native = fs.native local native = fs.native
Util.clear(fs) Util.clear(fs)
Util.merge(fs, native) Util.merge(fs, native)
end end

View File

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

View File

@@ -10,6 +10,13 @@ if not fs.exists('usr/autorun') then
fs.makeDir('usr/autorun') fs.makeDir('usr/autorun')
end 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 -- TODO: Temporary
local upgrade = Util.readTable('usr/config/shell') local upgrade = Util.readTable('usr/config/shell')
if upgrade and (not upgrade.upgraded or upgrade.upgraded ~= 1) then 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.LUA_PATH = config.lua_path
--_G.settings.set('mbs.shell.require_path', config.lua_path) --_G.settings.set('mbs.shell.require_path', config.lua_path)
fs.loadTab('usr/config/fstab')

View File

@@ -1,5 +1,3 @@
_G.requireInjector(_ENV)
local Config = require('opus.config') local Config = require('opus.config')
local device = _G.device local device = _G.device
@@ -17,7 +15,7 @@ do
end end
local function startNetwork() local function startNetwork()
kernel.run({ kernel.run(_ENV, {
title = 'Net daemon', title = 'Net daemon',
path = 'sys/apps/netdaemon.lua', path = 'sys/apps/netdaemon.lua',
hidden = true, hidden = true,

31
sys/init/5.unpackage.lua Normal file
View 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

View File

@@ -13,8 +13,14 @@ table.insert(helpPaths, '/sys/help')
for name in pairs(Packages:installed()) do for name in pairs(Packages:installed()) do
local packageDir = fs.combine('packages', name) 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) 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 if fs.exists(apiPath) then
fs.mount(fs.combine('rom/modules/main', name), 'linkfs', apiPath) fs.mount(fs.combine('rom/modules/main', name), 'linkfs', apiPath)
end end
@@ -23,12 +29,27 @@ for name in pairs(Packages:installed()) do
if fs.exists(helpPath) then if fs.exists(helpPath) then
table.insert(helpPaths, helpPath) table.insert(helpPaths, helpPath)
end end
local fstabPath = fs.combine(packageDir, 'etc/fstab')
if fs.exists(fstabPath) then
fs.loadTab(fstabPath)
end
end end
help.setPath(table.concat(helpPaths, ':')) help.setPath(table.concat(helpPaths, ':'))
shell.setPath(table.concat(appPaths, ':')) 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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
local Util = require('opus.util')
local Array = { } local Array = { }
function Array.filter(it, f) function Array.filter(it, f)
@@ -14,9 +16,11 @@ function Array.removeByValue(t, e)
for k,v in pairs(t) do for k,v in pairs(t) do
if v == e then if v == e then
table.remove(t, k) table.remove(t, k)
break return e
end end
end end
end end
Array.find = Util.find
return Array return Array

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

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

View File

@@ -1,7 +1,6 @@
local Util = require('opus.util') local Util = require('opus.util')
local fs = _G.fs local fs = _G.fs
local shell = _ENV.shell
local Config = { } local Config = { }
@@ -25,23 +24,6 @@ function Config.load(fname, data)
return data return data
end 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) function Config.update(fname, data)
local filename = 'usr/config/' .. fname local filename = 'usr/config/' .. fname
Util.writeTable(filename, data) Util.writeTable(filename, data)

View File

@@ -5,7 +5,7 @@ local cbor = require('opus.cbor')
local sha2 = require('opus.crypto.sha2') local sha2 = require('opus.crypto.sha2')
local Util = require('opus.util') local Util = require('opus.util')
local ROUNDS = 8 -- Adjust this for speed tradeoff local ROUNDS = 20 -- Standard ChaCha20 (was 8, upgraded for security)
local bxor = bit32.bxor local bxor = bit32.bxor
local band = bit32.band local band = bit32.band
@@ -100,7 +100,7 @@ local function crypt(data, key, nonce, cntr, round)
cntr = tonumber(cntr) or 1 cntr = tonumber(cntr) or 1
round = tonumber(round) or 20 round = tonumber(round) or 20
local throttle = Util.throttle(function() _syslog('throttle') end) local throttle = Util.throttle()
local out = {} local out = {}
local state = initState(key, nonce, cntr) local state = initState(key, nonce, cntr)
local blockAmt = math.floor(#data/64) local blockAmt = math.floor(#data/64)

View File

@@ -41,16 +41,21 @@ end
function Entry:updateScroll() function Entry:updateScroll()
local ps = self.scroll local ps = self.scroll
local value = _val(self.value) local len = #_val(self.value)
if self.pos > #value then if self.pos > len then
self.pos = #value self.pos = len
self.scroll = 0 -- ?? self.scroll = 0 -- ??
end end
if self.pos - self.scroll > self.width then 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 elseif self.pos < self.scroll then
self.scroll = self.pos self.scroll = self.pos
end 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 if ps ~= self.scroll then
self.textChanged = true self.textChanged = true
end end
@@ -217,6 +222,10 @@ function Entry:paste(ie)
end end
end end
function Entry.forcePaste()
os.queueEvent('clipboard_paste')
end
function Entry:clearLine() function Entry:clearLine()
if #_val(self.value) > 0 then if #_val(self.value) > 0 then
self:reset() self:reset()
@@ -225,7 +234,9 @@ end
function Entry:markBegin() function Entry:markBegin()
if not self.mark.active then 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 } self.mark.anchor = { x = self.pos }
end end
end end
@@ -262,6 +273,8 @@ function Entry:markLeft()
self:markBegin() self:markBegin()
if self:moveLeft() then if self:moveLeft() then
self:markFinish() self:markFinish()
else
self.mark.continue = self.mark.active
end end
end end
@@ -269,6 +282,8 @@ function Entry:markRight()
self:markBegin() self:markBegin()
if self:moveRight() then if self:moveRight() then
self:markFinish() self:markFinish()
else
self.mark.continue = self.mark.active
end end
end end
@@ -296,6 +311,8 @@ function Entry:markNextWord()
self:markBegin() self:markBegin()
if self:moveWordRight() then if self:moveWordRight() then
self:markFinish() self:markFinish()
else
self.mark.continue = self.mark.active
end end
end end
@@ -303,6 +320,8 @@ function Entry:markPrevWord()
self:markBegin() self:markBegin()
if self:moveWordLeft() then if self:moveWordLeft() then
self:markFinish() self:markFinish()
else
self.mark.continue = self.mark.active
end end
end end
@@ -321,6 +340,8 @@ function Entry:markHome()
self:markBegin() self:markBegin()
if self:moveHome() then if self:moveHome() then
self:markFinish() self:markFinish()
else
self.mark.continue = self.mark.active
end end
end end
@@ -328,6 +349,8 @@ function Entry:markEnd()
self:markBegin() self:markBegin()
if self:moveEnd() then if self:moveEnd() then
self:markFinish() self:markFinish()
else
self.mark.continue = self.mark.active
end end
end end
@@ -363,9 +386,10 @@ local mappings = {
--[ 'control-d' ] = Entry.cutNextWord, --[ 'control-d' ] = Entry.cutNextWord,
[ 'control-x' ] = Entry.cut, [ 'control-x' ] = Entry.cut,
[ 'paste' ] = Entry.paste, [ '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_doubleclick' ] = Entry.markWord,
[ 'mouse_tripleclick' ] = Entry.markAll,
[ 'shift-left' ] = Entry.markLeft, [ 'shift-left' ] = Entry.markLeft,
[ 'shift-right' ] = Entry.markRight, [ 'shift-right' ] = Entry.markRight,
[ 'mouse_down' ] = Entry.markAnchor, [ 'mouse_down' ] = Entry.markAnchor,

View File

@@ -58,6 +58,14 @@ function Routine:resume(event, ...)
else else
s, m = coroutine.resume(self.co, event, ...) s, m = coroutine.resume(self.co, event, ...)
end 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 if self:isDead() then
self.co = nil self.co = nil
self.filter = nil self.filter = nil

View File

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

View File

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

View File

@@ -9,15 +9,28 @@ function ramfs.mount(_, nodeType)
return { return {
nodes = { }, nodes = { },
size = 0, size = 0,
created = os.epoch('utc'),
modification = os.epoch('utc'),
} }
elseif nodeType == 'file' then elseif nodeType == 'file' then
return { return {
size = 0, size = 0,
created = os.epoch('utc'),
modification = os.epoch('utc'),
} }
end end
error('ramfs syntax: [directory, file]') error('ramfs syntax: [directory, file]')
end 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) function ramfs.delete(node, dir)
if node.mountPoint == dir then if node.mountPoint == dir then
fs.unmount(node.mountPoint) fs.unmount(node.mountPoint)
@@ -40,8 +53,10 @@ function ramfs.makeDir(_, dir)
fs.mount(dir, 'ramfs', 'directory') fs.mount(dir, 'ramfs', 'directory')
end end
function ramfs.isDir(node) function ramfs.isDir(node, dir)
return not not node.nodes if node.mountPoint == dir then
return not not node.nodes
end
end end
function ramfs.getDrive() function ramfs.getDrive()
@@ -64,32 +79,70 @@ function ramfs.list(node, dir)
end end
function ramfs.open(node, fn, fl) function ramfs.open(node, fn, fl)
local modes = Util.transpose { 'r', 'w', 'rb', 'wb', 'a' }
if fl ~= 'r' and fl ~= 'w' and fl ~= 'rb' and fl ~= 'wb' then if not modes[fl] then
error('Unsupported mode') error('Unsupported mode')
end 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 fl == 'r' then
if node.mountPoint ~= fn then if node.mountPoint ~= fn then
return return
end end
local c = type(node.contents) == 'table'
and string.char(table.unpack(node.contents))
or node.contents
local ctr = 0 local ctr = 0
local lines local lines
return { return {
read = function() read = function(n)
ctr = ctr + 1 n = n or 1
return node.contents:sub(ctr, ctr) if ctr >= node.size then
return
end
local t = c:sub(ctr + 1, ctr + n)
ctr = ctr + n
return t
end, end,
readLine = function() readLine = function()
if not lines then if not lines then
lines = Util.split(node.contents) lines = Util.split(c)
end end
ctr = ctr + 1 ctr = ctr + 1
return lines[ctr] return lines[ctr]
end, end,
readAll = function() readAll = function()
return node.contents return c
end, end,
close = function() close = function()
lines = nil lines = nil
@@ -121,11 +174,30 @@ function ramfs.open(node, fn, fl)
return return
end 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 local ctr = 0
return { 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 ctr = ctr + 1
return node.contents[ctr] return c[ctr]
end, end,
close = function() close = function()
end, end,
@@ -137,7 +209,13 @@ function ramfs.open(node, fn, fl)
local c = { } local c = { }
return { return {
write = function(b) 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, end,
flush = function() flush = function()
node.contents = c node.contents = c

View File

@@ -5,29 +5,46 @@ local fs = _G.fs
local urlfs = { } local urlfs = { }
function urlfs.mount(_, url) function urlfs.mount(path, url, force)
if not url then if not url then
error('URL is required') error('URL is required')
end 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 end
function urlfs.delete(_, dir) function urlfs.delete(node, path)
fs.unmount(dir) if path == node.mountPoint then
fs.unmount(path)
end
end end
function urlfs.exists() function urlfs.exists(node, path)
return true return path == node.mountPoint
end end
function urlfs.getSize(node) function urlfs.getSize(node, path)
return node.size or 0 return path == node.mountPoint and node.size or 0
end end
function urlfs.isReadOnly() function urlfs.isReadOnly()
return true return false
end end
function urlfs.isDir() function urlfs.isDir()
@@ -50,14 +67,6 @@ function urlfs.open(node, fn, fl)
local c = node.cache local c = node.cache
if not c then 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) c = Util.httpGet(node.url)
if c then if c then
node.cache = c node.cache = c
@@ -94,6 +103,9 @@ function urlfs.open(node, fn, fl)
} }
end end
return { return {
readAll = function()
return c
end,
read = function() read = function()
ctr = ctr + 1 ctr = ctr + 1
return c:sub(ctr, ctr):byte() return c:sub(ctr, ctr):byte()

View 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

View File

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

View File

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

View File

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

View File

@@ -50,18 +50,20 @@ function input:toCode(ch, code)
table.insert(result, 'alt') table.insert(result, 'alt')
end end
if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or if ch then -- some weird things happen with control/command on mac
code == keys.leftShift or code == keys.rightShift then if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or
if code and modifiers[code] then code == keys.leftShift or code == keys.rightShift then
table.insert(result, 'shift') if code and modifiers[code] then
elseif #ch == 1 then table.insert(result, 'shift')
table.insert(result, ch:upper()) elseif #ch == 1 then
else table.insert(result, ch:upper())
table.insert(result, 'shift') else
table.insert(result, 'shift')
table.insert(result, ch)
end
elseif not code or not modifiers[code] then
table.insert(result, ch) table.insert(result, ch)
end end
elseif not code or not modifiers[code] then
table.insert(result, ch)
end end
return table.concat(result, '-') return table.concat(result, '-')
@@ -118,6 +120,7 @@ function input:translate(event, code, p1, p2)
local buttons = { 'mouse_click', 'mouse_rightclick' } local buttons = { 'mouse_click', 'mouse_rightclick' }
self.mch = buttons[code] self.mch = buttons[code]
self.mfired = nil self.mfired = nil
self.anchor = { x = p1, y = p2 }
return { return {
code = input:toCode('mouse_down', 255), code = input:toCode('mouse_down', 255),
button = code, button = code,
@@ -132,6 +135,8 @@ function input:translate(event, code, p1, p2)
button = code, button = code,
x = p1, x = p1,
y = p2, y = p2,
dx = p1 - self.anchor.x,
dy = p2 - self.anchor.y,
} }
elseif event == 'mouse_up' then elseif event == 'mouse_up' then
@@ -141,18 +146,26 @@ function input:translate(event, code, p1, p2)
p1 == self.x and p2 == self.y and p1 == self.x and p2 == self.y and
(clock - self.timer < .5) then (clock - self.timer < .5) then
self.mch = 'mouse_doubleclick' self.clickCount = self.clickCount + 1
self.timer = nil if self.clickCount == 3 then
self.mch = 'mouse_tripleclick'
self.timer = nil
self.clickCount = 1
else
self.mch = 'mouse_doubleclick'
end
else else
self.timer = os.clock() self.timer = os.clock()
self.x = p1 self.x = p1
self.y = p2 self.y = p2
self.clickCount = 1
end end
self.mfired = input:toCode(self.mch, 255) self.mfired = input:toCode(self.mch, 255)
else else
self.mch = 'mouse_up' self.mch = 'mouse_up'
self.mfired = input:toCode(self.mch, 255) self.mfired = input:toCode(self.mch, 255)
end end
return { return {
code = self.mfired, code = self.mfired,
button = code, button = code,
@@ -176,11 +189,22 @@ function input:translate(event, code, p1, p2)
end end
end end
function input:test() if not ({ ...})[1] then
local colors = _G.colors
local term = _G.term
while true do while true do
local ch = self:translate(os.pullEvent()) local e = { os.pullEvent() }
local ch = input:translate(table.unpack(e))
if ch then 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 end
end end

View File

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

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