565 Commits
master ... main

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
Anavrins
39522ee5b1 Cleanup 2020-03-30 02:07:20 -04:00
Anavrins
369070e19c Merge pull request #30 from Wojbie/Git-fix
Update git.lua to use headers authorization.
2020-02-09 21:35:52 -05:00
Wojbie
210c5f5a11 Update git.lua to use headers authorization. 2020-02-09 23:20:03 +01:00
Anavrins
942f0bda92 GPS overhaul
Changes how GPS works to avoid returning ambiguous coordinates and nan errors
2019-12-27 01:11:36 -05:00
Anavrins
cc80e08407 gps.lua: Added nil and nan checks 2019-12-11 11:38:12 -05:00
Anavrins
9e3cf50ccc socket.lua: Option to pass user generated keypairs 2019-12-11 11:20:12 -05:00
Anavrins
6204c46cc4 Fix empty TextEntry in cloud config 2019-12-11 11:18:02 -05:00
kepler155c@gmail.com
db2a28f04c Merge branch 'develop-1.8' of https://github.com/kepler155c/opus into develop-1.8 2019-12-07 12:05:03 -07:00
kepler155c@gmail.com
28b2ba3386 tweaks 2019-12-07 12:04:58 -07:00
Anavrins
31f43067bf Change how Sniff.lua detects modem 2019-12-06 20:14:18 -05:00
kepler155c@gmail.com
424fff7842 apparently read was added to fs.open "r" mode in 2017 2019-12-05 13:44:35 -07:00
kepler155c@gmail.com
1e675a2e35 revert multiple sub-canvas mouse click 2019-12-01 13:25:26 -07:00
kepler155c@gmail.com
ffa412c59d ui fixes 2019-11-18 14:32:10 -07:00
kepler155c@gmail.com
a3a8c64be8 UI docs 2019-11-16 22:12:02 -07:00
kepler155c@gmail.com
efa1a5bbf5 clipping for transistions + tab ordering via index + more examples 2019-11-15 12:51:44 -07:00
kepler155c@gmail.com
14057c2bf9 moar ui examples 2019-11-14 15:43:20 -07:00
kepler155c@gmail.com
3241326a2f accelerators for any event + more inspect examples 2019-11-13 21:50:00 -07:00
kepler155c@gmail.com
db48031c7c funnel all events through emit 2019-11-13 15:17:23 -07:00
kepler155c@gmail.com
0a828fecc5 oops 2019-11-13 14:38:24 -07:00
kepler155c@gmail.com
65c6ebf711 properly handle empty text entry fields (including transformations) 2019-11-13 14:24:43 -07:00
kepler155c@gmail.com
053003f429 inspect cleanup 2019-11-12 23:04:31 -07:00
kepler155c@gmail.com
25405f15c8 UI inspector 2019-11-12 21:13:17 -07:00
kepler155c@gmail.com
674c6af509 alternate support 2019-11-10 15:58:36 -07:00
kepler155c@gmail.com
c9fd7efc26 alternative optimize 2019-11-09 22:56:36 -07:00
kepler155c@gmail.com
8a5d30a441 oops 2019-11-09 22:33:16 -07:00
kepler155c@gmail.com
4a6af34d7c alternative updates 2019-11-09 22:26:11 -07:00
kepler155c@gmail.com
340e37da82 package manager wip 2019-11-09 22:01:48 -07:00
kepler155c@gmail.com
9ef4d9ef64 package manager install/uninstall directives 2019-11-09 21:15:07 -07:00
kepler155c@gmail.com
57ea46dde7 alternatives 2019-11-08 19:54:41 -07:00
kepler155c@gmail.com
6d8d62d309 tweaks 2019-11-07 22:50:11 -07:00
kepler155c@gmail.com
e9717c4def Events shows all + tweaks 2019-11-07 18:00:54 -07:00
kepler155c@gmail.com
f2cf20c274 cleanup 2019-11-05 11:18:21 -07:00
kepler155c@gmail.com
fba2d48a4b minor fixes for user errors 2019-11-02 12:23:40 -06:00
kepler155c@gmail.com
5c35a8383e lock computer 2019-11-01 22:37:08 -06:00
kepler155c@gmail.com
aaab059cee prepare for lock screen 2019-11-01 16:35:58 -06:00
kepler155c@gmail.com
e5a5f76fb3 restructure 2019-10-30 22:49:30 -06:00
kepler155c@gmail.com
ad447f36b5 Merge branch 'develop-1.8' of https://github.com/kepler155c/opus into develop-1.8 2019-10-28 20:02:14 -06:00
kepler155c@gmail.com
774d3ed415 improve startup, dont rely on debug (cbor) 2019-10-28 20:01:57 -06:00
xAnavrins
457c8a8a52 Fix forms numeric validation 2019-08-18 14:59:20 -04:00
Kan18
3512b2441d update license / readme (#18)
* Update README.md

the installer already reboots for you

* Update LICENSE.md
2019-08-13 11:14:45 -06:00
Kan18
2f597d0dc4 support MOTDs (#19) 2019-08-13 11:14:11 -06:00
kepler155c@gmail.com
6e6d4b81cd keyboard handling when not using opus os 2019-08-03 08:33:32 -06:00
kepler155c
47cb7d0fd7 Merge pull request #17 from Kan18/patch-1
Remove confusion over kiosk mode (#7)
2019-08-03 08:10:06 -06:00
Kan18
748dcd8f82 Remove confusion over kiosk mode (#7) 2019-07-31 18:21:18 -04:00
kepler155c@gmail.com
7e8dc5bd49 fix for new text entry behaviour + oc relay fix 2019-07-27 19:07:34 -06:00
kepler155c
25bfce0e40 Merge pull request #15 from Kan18/patch-1
fix no password error
2019-07-27 13:43:58 -06:00
kepler155c
8c1ea278fc Merge pull request #16 from Kan18/patch-2
Update upgraded.lua
2019-07-27 13:43:23 -06:00
xAnavrins
c1430f6dac GPS ambiguous position fix 2019-07-27 01:33:59 -04:00
Kan18
9c4aac27f8 Update upgraded.lua
Kind of make the program look better as the 'sys/apis' line is the only one not to do fs.exists. Also fixes error when opus is nested inside a potatOS instance.
2019-07-25 23:04:20 -04:00
Kan18
b739c0c77e fix no password error 2019-07-25 22:51:16 -04:00
xAnavrins
b3efbc7438 Distance filter in Sniffer 2019-07-24 00:45:00 -04:00
xAnavrins
cd2024d7ce TextEntry number transform 2019-07-24 00:37:41 -04:00
kepler155c@gmail.com
e084c733f8 consistent command line processing-usage 2019-07-23 13:29:43 -06:00
kepler155c@gmail.com
5dd3001ffd dont close modem ports on network startup (due to multiple computers may be using same modem) 2019-07-22 22:57:22 -06:00
kepler155c@gmail.com
c18945467f ability to specify index in form fields 2019-07-22 22:21:41 -06:00
kepler155c@gmail.com
ae98aaf9d5 lint warnings 2019-07-21 10:16:47 -06:00
kepler155c@gmail.com
ec7fc5bb23 fix ui default device via args 2019-07-21 03:50:06 -04:00
kepler155c@gmail.com
30b7199976 path fix + compat changes 2019-07-20 16:23:48 -06:00
Kan18
09be81be27 lua 5.2 compatibility (#14)
compatibility update - making a few changes in next commit...
2019-07-20 16:19:30 -06:00
kepler155c@gmail.com
9ce61b5d98 oops 2019-07-19 10:12:25 -06:00
kepler155c@gmail.com
92686381c7 manipulators aaarrrggghhh 2019-07-19 10:06:34 -06:00
xAnavrins
d8dd17333c Disk Usage icon transparency 2019-07-18 01:00:22 -04:00
kepler155c@gmail.com
4093d4cd0d fix issue with manipulator not working after relog 2019-07-17 15:29:26 -06:00
kepler155c@gmail.com
e124666575 Merge branch 'develop-1.8' of https://github.com/kepler155c/opus into develop-1.8 2019-07-17 15:27:44 -06:00
xAnavrins
672cca3084 ui theme generator + better module handling 2019-07-16 23:21:33 -04:00
xAnavrins
c8f200ebb6 Fix apps.db and legacy icon transparency 2019-07-16 22:49:54 -04:00
kepler155c@gmail.com
87c7e5ff7e ui theme generator + better module handling 2019-07-15 20:08:30 -06:00
kepler155c@gmail.com
c43fe19f1d sliders in forms + form component setValue (hopefully didnt break anything) 2019-07-12 12:47:51 -06:00
kepler155c@gmail.com
51e40a1dd7 Anavrins Slider component 2019-07-12 02:59:36 -06:00
kepler155c@gmail.com
43c86c263b upper/lowercase transformations for TextEntry 2019-07-11 10:36:17 -06:00
kepler155c@gmail.com
e9559165a4 icon transparency 2019-07-11 00:42:57 -06:00
kepler155c@gmail.com
0e1b712adc ntf transparency 2019-07-10 19:02:46 -06:00
kepler155c@gmail.com
8f17657263 make page/window themeable - oops 2019-07-10 11:06:29 -06:00
kepler155c@gmail.com
4c2d121562 reduce net traffic 2019-07-08 13:52:53 -04:00
kepler155c@gmail.com
ccc7693f46 minor tweaks 2019-07-08 00:16:28 -04:00
kepler155c@gmail.com
86cd6e3c0e app icons 2019-07-07 01:14:39 -04:00
kepler155c@gmail.com
cf87c29cf6 lint warnings + old file cleanup 2019-07-06 23:52:29 -04:00
kepler155c
6bb1ba7669 Merge pull request #13 from xAnavrins/develop-1.8
New sniffer app
2019-07-06 23:42:45 -04:00
xAnavrins
ae2ea81d1d Show serialized packet in packetGrid 2019-07-06 21:56:21 -04:00
xAnavrins
f1b9dcc4f4 Last oopsie 2019-07-05 19:45:29 -04:00
xAnavrins
88d06267ea More refactor 2019-07-05 19:10:44 -04:00
xAnavrins
4a089ecd85 Added icons to diskusage 2019-07-05 02:14:28 -04:00
xAnavrins
08eac79109 Oops 2019-07-04 21:46:09 -04:00
xAnavrins
ddb5433c01 New sniffer app 2019-07-04 21:33:47 -04:00
kepler155c@gmail.com
0b222207ba package manager improvements 2019-07-03 10:44:30 -04:00
kepler155c@gmail.com
9456d31881 oops 2019-07-02 14:32:41 -04:00
kepler155c@gmail.com
2f5aea912b diskusage 2019-07-02 14:25:17 -04:00
kepler155c@gmail.com
6ba458646f control-q instead of q for exitting apps + grid column override colors 2019-07-02 14:11:33 -04:00
kepler155c@gmail.com
1dcb6d67b7 tweaks + Anavrins disk usage system module 2019-07-02 10:19:08 -04:00
kepler155c@gmail.com
aec5ac0121 oops again 2019-07-01 20:39:34 -04:00
kepler155c@gmail.com
cd7122921f oops 2019-07-01 19:20:38 -04:00
kepler155c@gmail.com
9e85a0dae1 shuffled some files around 2019-07-01 17:22:19 -04:00
kepler155c@gmail.com
61a26d7c55 encryption perf 2019-06-30 19:53:26 -04:00
kepler155c@gmail.com
159dc622fd oops 2019-06-30 17:40:57 -04:00
kepler155c@gmail.com
86e918667c encrypt optimization 2019-06-30 17:20:55 -04:00
kepler155c@gmail.com
721cd840b3 encrypt improvements 2019-06-30 14:47:45 -04:00
kepler155c@gmail.com
67779ab814 secure telnet 2019-06-29 23:43:21 -04:00
kepler155c@gmail.com
1c29197983 security - final round? 2019-06-29 22:02:00 -04:00
kepler155c@gmail.com
e75a357209 security updates 2019-06-29 16:35:33 -04:00
kepler155c@gmail.com
69522e61d4 better info on upgrade 2019-06-29 08:50:11 -04:00
kepler155c@gmail.com
5283de18ed oops 2019-06-29 04:04:51 -04:00
kepler155c@gmail.com
00293033c8 security update round 2 2019-06-29 02:44:30 -04:00
kepler155c@gmail.com
d90aa0e2fd cleanup 2019-06-28 14:22:03 -04:00
kepler155c@gmail.com
343ce7fdc2 move apis into rom/modules/main for shell compatibility 2019-06-28 13:50:02 -04:00
kepler155c@gmail.com
c3d52c1aab crypto cleanup 2019-06-28 06:33:47 -04:00
kepler155c@gmail.com
bcd33af599 The big Anavrins security update (round 1) 2019-06-27 21:08:46 -04:00
kepler155c@gmail.com
97a442e999 socket update 2019-06-27 16:29:12 -04:00
kepler155c@gmail.com
3c22a872b0 spaces->tabs + cleanup + pathing fixes 2019-06-18 15:19:24 -04:00
kepler155c@gmail.com
82ec4db50f input rework - again 2019-05-26 11:05:37 -04:00
kepler155c@gmail.com
310879a801 sanitize discovery messages 2019-05-20 15:19:39 -04:00
kepler155c@gmail.com
43163053a5 Lua: timing 2019-05-12 10:02:46 -04:00
kepler155c@gmail.com
bf870479c4 mark with drag fix 2019-05-04 06:11:22 -04:00
kepler155c@gmail.com
c44dc765da change _debug to _syslog 2019-05-03 15:30:09 -04:00
kepler155c@gmail.com
59552d4217 canvas update for resizing 2019-04-27 19:33:21 -04:00
kepler155c@gmail.com
f4bcd61116 tweaks 2019-04-25 14:44:36 -04:00
kepler155c@gmail.com
0e3cb15356 blacklist items for turtle digging 2019-04-24 22:09:36 -04:00
kepler155c@gmail.com
366ab1736b remove old startup 2019-04-24 20:32:38 -04:00
kepler155c
a5fc89beee Merge pull request #9 from hugeblank/startup-revamp
improve startup option screen
2019-04-24 20:26:38 -04:00
hugeblank
0c811ef892 improve startup option screen
- Replaced the old startup alternate menu with a cleaner crisper interface
- Up and down arrows as well as the mouse scroll wheel will move the cursor up or down. Enter will proceed to boot.
- OR if the option desired is between 1 and 9, pressing the corresponding key will select and boot that option.
- Appended .lua to the startup file
2019-04-24 00:42:23 -07:00
kepler155c@gmail.com
9e4eb1207e better shell launcher 2019-04-23 14:48:39 -04:00
kepler155c@gmail.com
8a030cbc50 altgr combos 2019-04-22 02:07:34 -04:00
kepler155c@gmail.com
d32030bec4 foreign keyboards 2019-04-21 23:41:17 -04:00
kepler155c@gmail.com
43a6019b9f oc relay oops 2019-04-20 14:07:21 -04:00
kepler155c@gmail.com
7749e14cad protected network services 2019-04-20 13:48:13 -04:00
kepler155c@gmail.com
62a3bc1360 sanitize network info a bit 2019-04-20 12:34:45 -04:00
kepler155c@gmail.com
3beae64ad8 http binary mode 2019-04-19 19:03:34 -04:00
kepler155c@gmail.com
54660c6089 api change for modems 2019-04-18 11:13:28 -04:00
kepler155c@gmail.com
e6d1e58ce2 drop dowm menus resize fix 2019-04-14 17:44:29 -04:00
kepler155c@gmail.com
e1fd4be3a5 icon update 2019-04-14 10:04:22 -04:00
kepler155c@gmail.com
263a32094c pastebin oops 2019-04-12 09:31:28 -04:00
kepler155c@gmail.com
8220ed1066 pastebin oops 2019-04-12 08:55:38 -04:00
kepler155c@gmail.com
a77d55e33f pastebin rework round 3 2019-04-12 08:33:41 -04:00
kepler155c@gmail.com
f011e7b14f pastebin rewrite 2019-04-11 08:41:23 -04:00
kepler155c@gmail.com
c633e935f8 network group wip 2019-04-08 10:23:04 -04:00
kepler155c@gmail.com
70733ab4f2 network group wip + virtual dirs + better trust 2019-04-08 09:30:47 -04:00
kepler155c@gmail.com
9413785248 oops 2019-04-07 11:18:54 -04:00
kepler155c@gmail.com
4f9bd8eb0f support open os command line programs 2019-04-07 10:09:47 -04:00
kepler155c@gmail.com
737ac095de fix menu separators + sys dir restructure 2019-03-31 15:16:45 -04:00
kepler155c@gmail.com
19159730a4 better text entry - bugfixes 2019-03-29 15:47:01 -04:00
kepler155c@gmail.com
c1baf107c1 better text entry -- oops 2019-03-29 10:45:43 -04:00
kepler155c@gmail.com
78402e89cd better text entry 2019-03-29 10:44:21 -04:00
kepler155c@gmail.com
c9ef150605 better text entry 2019-03-29 09:56:56 -04:00
kepler155c@gmail.com
30bad69417 tweaks 2019-03-28 08:53:14 -04:00
kepler155c@gmail.com
517376db28 update issues 2019-03-28 07:39:34 -04:00
kepler155c@gmail.com
b71ca0d545 autorun overhaul + shell input readline commands + launcher option + shell colors 2019-03-28 07:29:01 -04:00
kepler155c@gmail.com
8fede6f507 run autorun programs in shell mode 2019-03-27 15:21:31 -04:00
kepler155c@gmail.com
82f6d3451d pastebin as an api 2019-03-26 09:08:39 -04:00
kepler155c@gmail.com
ade354660f notifications anchor option 2019-03-26 02:10:10 -04:00
kepler155c@gmail.com
daa86d50b2 package manager update + UI built-in extended char detection 2019-03-26 00:31:25 -04:00
kepler155c@gmail.com
3f9c219f6b keyboard handling 2019-03-24 17:00:42 -04:00
kepler155c@gmail.com
398953af54 input: dont generate control-char combos 2019-03-24 16:29:49 -04:00
kepler155c@gmail.com
8c50447625 input: dont generate control-char combos 2019-03-24 16:05:38 -04:00
kepler155c@gmail.com
7bab4e5bf5 ui built in theme for ext chars 2019-03-24 03:44:38 -04:00
kepler155c@gmail.com
235e8d11de cleanup 2019-03-23 12:20:48 -04:00
kepler155c@gmail.com
ea8e981880 better cloud edit 2019-03-22 01:40:29 -04:00
kepler155c@gmail.com
cd6ed79d11 array api 2019-03-19 01:42:42 -04:00
kepler155c@gmail.com
e5312ca6ab better errors + modules as devices (try #2) + pretty lua output 2019-03-17 23:54:44 -04:00
kepler155c@gmail.com
340636806c modules as devices-fail 2019-03-15 09:21:24 -04:00
kepler155c@gmail.com
0c91082003 modules as devices 2019-03-15 08:26:56 -04:00
kepler155c@gmail.com
e2508c9037 cleanup 2019-03-12 20:04:28 -04:00
kepler155c@gmail.com
86be65f026 oops 2 2019-03-12 02:00:07 -04:00
kepler155c@gmail.com
08791e87a3 oops 2019-03-11 23:54:00 -04:00
kepler155c@gmail.com
04af12b452 remove dependency on device global 2019-03-11 23:48:22 -04:00
kepler155c@gmail.com
bd9b2825be icon update - thx LDD 2019-03-10 19:36:56 -04:00
kepler155c@gmail.com
a569c2c900 pathing heading possible fix 2019-03-08 20:50:50 -05:00
kepler155c@gmail.com
6f7e27827e phase out peripheral.lookup 2019-03-08 16:47:39 -05:00
kepler155c@gmail.com
a9d03b06e9 phase out peripheral.lookup 2019-03-08 16:25:56 -05:00
kepler155c@gmail.com
89f95ca45b stack traces 2019-03-07 13:14:16 -05:00
kepler155c@gmail.com
a4145951f7 bulk download 2019-03-06 16:56:41 -05:00
kepler155c@gmail.com
ce9ffa6c3a alignment update 2019-03-06 11:48:09 -05:00
kepler155c@gmail.com
7a57d74e68 neural status 2019-03-01 14:16:55 -05:00
kepler155c@gmail.com
1e011c8bbd refactor 2019-03-01 11:27:27 -05:00
kepler155c@gmail.com
69e6af2314 rename justify property to align 2019-02-27 17:15:20 -05:00
kepler155c@gmail.com
4dc6062043 cleanup 2019-02-27 08:10:09 -05:00
kepler155c@gmail.com
cc1a2bfbf4 fix ui resizing (due to canvas changes) 2019-02-26 18:03:09 -05:00
kepler155c@gmail.com
ad60fce9b0 util refactor 2019-02-26 08:17:53 -05:00
kepler155c@gmail.com
8f4324f1d8 map convience functions (refactor) 2019-02-25 09:02:40 -05:00
kepler155c@gmail.com
687fc05445 Event cleanup 2019-02-24 06:58:26 -05:00
kepler155c@gmail.com
8ae3b33374 network sniffer 2019-02-23 18:36:17 -05:00
kepler155c@gmail.com
1c859029f1 cleanup 2019-02-23 07:27:35 -05:00
kepler155c@gmail.com
4485a751bd turtle gps rework 2019-02-22 03:52:47 -05:00
kepler155c@gmail.com
fc46239f44 turtle status 2019-02-21 02:03:41 -05:00
kepler155c@gmail.com
ab4fd29d16 turtle follow 2019-02-20 08:52:27 -05:00
kepler155c@gmail.com
c307f4020c term bugs + kiosk config 2019-02-20 01:24:37 -05:00
kepler155c@gmail.com
da35988d50 kiosk settings 2019-02-19 08:55:11 -05:00
kepler155c@gmail.com
c2ee88ba0e fix cursor positioning 2019-02-18 03:17:29 -05:00
kepler155c@gmail.com
440b829f62 begin moving turtle specific code from opus into turtle package 2019-02-18 02:56:28 -05:00
kepler155c@gmail.com
a36051bf78 remove trace for now 2019-02-13 20:17:01 -05:00
kepler155c@gmail.com
ae6d8e8ebd rename shell -> shell.lua 2019-02-12 17:03:50 -05:00
kepler155c@gmail.com
2d411df7d1 scrolling term 2019-02-11 20:25:12 -05:00
kepler155c@gmail.com
d769b35672 pull out shell scrolling til fixed 2019-02-10 23:40:23 -05:00
kepler155c@gmail.com
2eab8b99de canvas fix 2019-02-10 13:08:54 -05:00
kepler155c@gmail.com
fd672ac74e checkbox width bug, scroll canvas 2019-02-09 21:41:51 -05:00
kepler155c@gmail.com
195dbd365a check for recursive requires + Lua update 2019-02-08 21:02:41 -05:00
kepler155c@gmail.com
8c1abb21ca resizing + injector fixes 2019-02-08 19:21:20 -05:00
kepler155c@gmail.com
72b3c7bac9 canvas improvements 2019-02-07 10:09:40 -05:00
kepler155c@gmail.com
8333b3bbec oops 2019-02-07 03:15:40 -05:00
kepler155c@gmail.com
53e0fa3795 fix lua path, mwm, cleanup injector 2019-02-07 03:10:05 -05:00
kepler155c@gmail.com
915085ac5f ui overhaul 2019-02-05 23:03:57 -05:00
kepler155c@gmail.com
89400ac1bd canvas use in UI overhaul 2019-01-30 16:27:09 -05:00
kepler155c@gmail.com
817c345672 canvas use in UI overhaul 2019-01-30 15:53:09 -05:00
kepler155c@gmail.com
3574d26caa canvas use in UI overhaul 2019-01-30 15:11:41 -05:00
kepler155c@gmail.com
02629e266b Overview update 2019-01-28 17:54:00 -05:00
kepler155c@gmail.com
dddb2a6b97 network update --wip 2019-01-27 00:26:41 -05:00
kepler155c@gmail.com
b320c92551 require rework round 3 2019-01-26 22:58:02 -05:00
kepler155c@gmail.com
496e95a6c4 trigger event instead of os.queueEvent 2019-01-26 00:27:56 -05:00
kepler155c@gmail.com
66f9481e7d ui checkbox oops 2019-01-23 19:25:09 -05:00
kepler155c@gmail.com
2438a40677 cloud catcher 2019-01-23 15:43:12 -05:00
kepler155c@gmail.com
073f4c1a6d cloud edit + logger removal 2019-01-23 15:28:43 -05:00
kepler155c@gmail.com
dc915337a0 optional label for checkboxes 2019-01-20 19:20:13 -05:00
kepler155c@gmail.com
8c794d9107 use mounts for web apps 2019-01-20 01:28:23 -05:00
kepler155c@gmail.com
db832025c9 welcome screen 2019-01-19 22:05:05 -05:00
kepler155c@gmail.com
5a758f0292 specify kiosk monitor 2019-01-18 08:48:30 -05:00
kepler155c@gmail.com
2fd2b552fc cleanup + app changes 2019-01-17 23:33:19 -05:00
kepler155c@gmail.com
644fd0352f rework turtle policies 2019-01-17 13:31:05 -05:00
kepler155c@gmail.com
eabfde64e9 safe digging 2019-01-13 13:24:37 -05:00
kepler155c@gmail.com
9edb4e7d5b fix package preload (for cloud-catcher) 2019-01-12 19:51:56 -05:00
kepler155c@gmail.com
cb5f690cf4 add git api key support 2019-01-12 10:17:56 -05:00
kepler155c@gmail.com
25031bfdc2 ui grid header sizing 2019-01-11 16:04:31 -05:00
kepler155c@gmail.com
712ffdb97c package manager ui update 2019-01-11 10:01:06 -05:00
kepler155c@gmail.com
7c26258f88 kiosk mode 2019-01-10 02:27:02 -05:00
kepler155c@gmail.com
3cc368f33a kiosk mode 2019-01-10 02:22:06 -05:00
kepler155c@gmail.com
f0a65d87ce kiosk mode 2019-01-09 08:50:42 -05:00
kepler155c@gmail.com
4977ea94d7 package manager 2019-01-09 06:59:19 -05:00
kepler155c@gmail.com
4387264960 package manager updates 2019-01-08 11:38:19 -05:00
kepler155c@gmail.com
b761b3429a dont require heading for point.closest 2019-01-08 04:40:58 -05:00
kepler155c@gmail.com
f5294d4fce better options icon 2019-01-05 10:35:25 -05:00
kepler155c@gmail.com
5ae6434d62 fix ui setTab 2019-01-03 12:54:14 -05:00
kepler155c@gmail.com
aef22987fa stereo sound! 2019-01-02 23:56:01 -05:00
kepler155c@gmail.com
02b12d87dc multi-device input support in UI 2019-01-02 10:33:47 -05:00
kepler155c@gmail.com
0e921edaf5 add util.any 2019-01-02 03:04:24 -05:00
kepler155c@gmail.com
6ef23908de UI page not calling disable on window 2019-01-01 07:29:44 -05:00
kepler155c@gmail.com
c95c22ff4d inactivate tabs 2018-12-31 11:35:13 -05:00
kepler155c@gmail.com
da20397b22 file associations 2018-12-30 13:54:09 -05:00
kepler155c
6143671dfb Update README.md 2018-12-30 13:53:39 -05:00
kepler155c@gmail.com
49849ecfea file associations 2018-12-30 09:50:02 -05:00
kepler155c@gmail.com
852bf3f6a6 require order issue 2018-12-30 06:50:59 -05:00
kepler155c@gmail.com
f19d439314 upgrade shell config file 2018-12-30 06:15:16 -05:00
kepler155c@gmail.com
09f436a3c2 more help 2018-12-30 00:07:29 -05:00
kepler155c@gmail.com
89e47f8b93 file associations 2018-12-29 22:33:58 -05:00
kepler155c@gmail.com
44d2d13e40 env cleanup 2018-12-27 03:08:11 -05:00
kepler155c@gmail.com
e3a8e4e790 env cleanup 2018-12-27 03:05:12 -05:00
kepler155c@gmail.com
8cfeaad061 turtle.unequip + justified headings bug 2018-12-27 00:45:03 -05:00
kepler155c@gmail.com
0b326589c2 reorg app db 2018-12-26 04:01:40 -05:00
kepler155c@gmail.com
6343589183 javascript style table functions 2018-12-23 19:49:28 -05:00
kepler155c@gmail.com
1264b0149b require overhaul part 2 2018-12-23 17:32:06 -05:00
kepler155c@gmail.com
26564cbcc1 custom help files + redo System UI 2018-12-22 22:11:16 -05:00
kepler155c@gmail.com
3de03bef22 manipulator device :( 2018-12-21 20:58:41 -05:00
kepler155c@gmail.com
e14a71ab6a Pointt.iterateClosest 2018-12-21 19:47:39 -05:00
kepler155c@gmail.com
5d38c307b3 disable red server 2018-12-20 19:28:26 -05:00
kepler155c@gmail.com
7d64b0c6db update shell with require 2018-12-16 22:17:19 -05:00
kepler155c@gmail.com
0aa0841b76 better turtle.condense 2018-12-16 02:44:01 -05:00
kepler155c@gmail.com
bd5cba8656 better turtle.condense 2018-12-16 02:28:16 -05:00
kepler155c@gmail.com
abec4a9740 dynamic overview shortcuts 2018-12-10 10:33:56 -05:00
kepler155c
a6c8ecccd4 Merge pull request #5 from sorucoder/develop-1.8
Removed util.random, added util.randomFloat
2018-12-09 15:43:26 -05:00
sorucoder
eb8b823674 Fixed util.signum 2018-12-09 14:52:52 -05:00
sorucoder
2e94b61dff Fixed util.randomFloat
Fixed if both max or min is nil, it returns a number between 0 and 1
2018-12-09 01:04:24 -05:00
sorucoder
30640fd415 Fixed util.randomFloat
Fixed if max is nil, that it behaves like math.random()
2018-12-09 01:01:20 -05:00
sorucoder
3f3a2deaa8 Removed util.random, added util.randomFloat
Removed util.random as it is a duplicate of math.random. Added util.randomFloat, which returns a random floating point number.
2018-12-09 00:58:05 -05:00
kepler155c
68217cfd52 Merge pull request #4 from sorucoder/develop-1.8
Added util.signum, util.clamp, and fixed util.round
2018-12-09 00:40:15 -05:00
sorucoder
3c9f03477a Added util.signum, util.clamp, and fixed util.round
Added commonly used math operations. Fixed bug where util.round(-0.5) returns 0.
2018-12-09 00:37:23 -05:00
kepler155c@gmail.com
70a98e33d7 switch branch to develop-1.8 2018-12-08 14:10:46 -05:00
kepler155c@gmail.com
bc4f6cdb95 merge from master-1.8 2018-12-08 14:09:38 -05:00
kepler155c@gmail.com
cc9c204670 switch branch to master-1.8 2018-12-08 14:08:44 -05:00
kepler155c@gmail.com
9ee76a3ac6 merge from master-1.8 2018-12-08 13:26:56 -05:00
kepler155c@gmail.com
3953ef08cc new release 2018-12-08 13:25:30 -05:00
kepler155c@gmail.com
00fea37f3f manipulators! 2018-12-06 23:13:43 -05:00
kepler155c@gmail.com
6674c715e0 pcall driver init 2018-12-05 21:21:30 -05:00
kepler155c@gmail.com
72142e12cc toggle show trusted in network app 2018-12-04 21:16:48 -05:00
kepler155c@gmail.com
7d17a21aa4 rework wizard page handling 2018-12-04 20:55:08 -05:00
kepler155c@gmail.com
0e3fb88f1d set sound volume 2018-12-04 01:33:58 -05:00
kepler155c@gmail.com
c9a42e2502 transport timeout on dead process 2018-12-02 18:40:19 -05:00
kepler155c@gmail.com
2517612622 manipulator :( 2018-12-02 15:10:21 -05:00
kepler155c@gmail.com
c5a821f264 manipulator :( 2018-12-02 14:35:58 -05:00
kepler155c@gmail.com
34c653ca1b manipulator device driver 2018-12-01 19:09:24 -05:00
kepler155c@gmail.com
1fb5379b11 grid setSelected, update tron url/icon 2018-12-01 00:55:42 -05:00
kepler155c@gmail.com
beae4e6eaf grid sort event 2018-11-30 04:53:59 -05:00
kepler155c@gmail.com
880a81f655 socket dim / clock issue 2018-11-29 15:12:25 -05:00
kepler155c@gmail.com
eb799004f8 sounds 2018-11-26 20:53:28 -05:00
kepler155c@gmail.com
a0d47a5f1a allow up to 10 sec diff in connect time due to clock mismatch in dims 2018-11-24 21:45:10 -05:00
kepler155c@gmail.com
0545ff0c68 package manager fix 2018-11-23 12:14:47 -05:00
kepler155c@gmail.com
95178b0452 package manager fix 2018-11-23 11:57:32 -05:00
kepler155c@gmail.com
9c43a261a8 reduce coroutine creation 2018-11-22 19:57:00 -05:00
kepler155c@gmail.com
d5896ca873 reduce coroutine creation 2018-11-22 13:52:45 -05:00
kepler155c@gmail.com
39e8307511 update rabbit 2018-11-21 22:06:00 -05:00
kepler155c@gmail.com
99687c3d28 new release prep 2018-11-21 12:10:20 -05:00
kepler155c@gmail.com
3b0ac4e338 new release prep 2018-11-21 09:24:55 -05:00
kepler155c@gmail.com
407b9f38fe reenable storage manager 2018-11-18 11:11:58 -05:00
kepler155c@gmail.com
c6fd959371 new icons - thx LDD 2018-11-14 21:37:05 -05:00
kepler155c@gmail.com
ccd1711d20 fix titlebar look 2018-11-13 21:31:55 -05:00
kepler155c@gmail.com
62ac7a52ec overview cleanup + chooser better events 2018-11-12 20:21:35 -05:00
kepler155c@gmail.com
f9359cf77f wizard - modem fixes 2018-11-12 15:22:29 -05:00
kepler155c@gmail.com
17d55e75ce support preferred wireless modems 2018-11-11 14:45:04 -05:00
kepler155c@gmail.com
4721b6aec1 vnc auto-reconnect 2018-11-10 13:04:32 -05:00
kepler155c@gmail.com
b3a061d39b vnc auto-reconnect 2018-11-10 13:00:22 -05:00
kepler155c@gmail.com
10a0c3a724 modem packet validation 2018-11-09 15:27:13 -05:00
kepler155c@gmail.com
68ee419d83 fix invalid msg crashing socket 2018-11-09 15:07:55 -05:00
kepler155c@gmail.com
cb03e56a1f package manager UI 2018-11-06 17:28:21 -05:00
kepler155c@gmail.com
1a4ef3e581 package manager UI 2018-11-06 16:43:24 -05:00
kepler155c@gmail.com
99b2fa1b0b package manager 2018-11-04 13:00:37 -05:00
kepler155c@gmail.com
67be1e0f2f package management 2018-11-03 18:25:49 -04:00
kepler155c@gmail.com
c478781cba package management 2018-11-03 18:13:41 -04:00
kepler155c@gmail.com
88f126bf2f sync module improvements 2018-10-31 19:38:09 -04:00
kepler155c@gmail.com
846569952c rename debug function 2018-10-31 00:05:29 -04:00
kepler155c@gmail.com
9484153f38 on the fly grid table filtering in ui 2018-10-29 22:03:46 -04:00
kepler155c@gmail.com
b564b1e9be parameter validation 2018-10-27 21:34:34 -04:00
kepler155c@gmail.com
7220b372bd new checkbox control 2018-10-26 18:45:22 -04:00
kepler155c@gmail.com
c75938bef9 prune method in utils 2018-10-24 19:13:05 -04:00
kepler155c@gmail.com
605e923d48 ui improvements - disable shell scrolling 2018-10-24 06:50:16 -04:00
kepler155c@gmail.com
adc5eb286d ui improvements 2018-10-23 22:33:40 -04:00
kepler155c@gmail.com
a59fef30f0 ui improvements + wireless modem over wired network 2018-10-23 03:05:47 -04:00
kepler155c@gmail.com
0cd709a525 missing pages definition 2018-10-21 18:48:08 -04:00
kepler155c@gmail.com
97c4b7a090 package manager wip 2018-10-21 04:46:40 -04:00
kepler155c@gmail.com
b7176e55ad package manager wip 2018-10-20 20:35:48 -04:00
kepler155c@gmail.com
8b50127f21 new icon set + scanning miner 2018-10-15 16:05:43 -04:00
kepler155c@gmail.com
ecb3af4672 new icon set + scanning miner 2018-10-14 15:35:40 -04:00
kepler155c@gmail.com
e6a98b34cb turtle not pathing backwards 2018-04-02 16:59:47 -04:00
kepler155c@gmail.com
9ee63da66f turtle coords reversed 2018-04-01 23:25:33 -04:00
kepler155c@gmail.com
6ed8468a65 System GPS page bugfix 2018-04-01 20:09:47 -04:00
kepler155c@gmail.com
4836a6a3a2 System GPS page bugfix 2018-04-01 20:03:07 -04:00
kepler155c@gmail.com
bbcb8ecd6e 1.12 updates 2018-04-01 18:40:08 -04:00
kepler155c@gmail.com
8db4fed19a 1.12 fixes 2018-03-30 17:01:56 -04:00
kepler155c@gmail.com
3dd351cc86 rttp initial version -- insecure 2018-03-30 13:12:46 -04:00
kepler155c@gmail.com
a17677730f 1.7.10 compatibility 2018-03-21 13:42:29 -04:00
kepler155c@gmail.com
daed3aac38 1.7.10 compatibility 2018-02-24 09:38:02 -05:00
kepler155c@gmail.com
fc9981bff0 neural + copy file path 2018-02-19 22:19:49 -05:00
kepler155c@gmail.com
00461483e1 neural support 2018-02-09 08:01:02 -05:00
kepler155c@gmail.com
25a917a220 adjust network timeout 2018-02-08 02:55:14 -05:00
kepler155c@gmail.com
098e9650ac adjust network timeout 2018-02-08 02:39:56 -05:00
kepler155c@gmail.com
d6a77ba487 neural apps 2018-02-08 02:32:28 -05:00
kepler155c@gmail.com
f16c7b632b error handling 2018-02-06 08:45:43 -05:00
kepler155c@gmail.com
cceb6c8409 security fix 2018-02-03 20:03:51 -05:00
kepler155c@gmail.com
15a627f7e9 remove os api modifications 2018-01-26 06:37:49 -05:00
kepler155c@gmail.com
742d1337a8 installer branches 2018-01-24 18:55:21 -05:00
kepler155c@gmail.com
8879f91c62 installer branches 2018-01-24 18:48:13 -05:00
kepler155c@gmail.com
f5add40808 installer branches 2018-01-24 18:45:47 -05:00
kepler155c@gmail.com
3e8e202e1e installer branches 2018-01-24 18:21:07 -05:00
kepler155c@gmail.com
8e61c0ef46 installer branches 2018-01-24 18:18:27 -05:00
kepler155c@gmail.com
a0173ee7e6 installer branches 2018-01-24 18:13:34 -05:00
kepler155c@gmail.com
5a32fe208e format and installer branches 2018-01-24 17:39:38 -05:00
kepler155c@gmail.com
1eea0d7cd8 embedded windows 2018-01-21 22:08:30 -05:00
kepler155c@gmail.com
8451c94abe transition to kernel 2018-01-21 17:22:59 -05:00
kepler155c@gmail.com
e6ed08315e transition to kernel 2018-01-21 06:09:25 -05:00
kepler155c@gmail.com
438e35688b transition to kernel 2018-01-21 05:59:27 -05:00
kepler155c@gmail.com
e59400eb2b transition to kernel 2018-01-21 05:44:13 -05:00
kepler155c@gmail.com
1c1eb9b782 transition to kernel 2018-01-20 07:18:13 -05:00
kepler155c@gmail.com
d85e9b96b2 transition to kernel 2018-01-15 20:38:30 -05:00
kepler155c@gmail.com
bd37d32750 transition to kernel 2018-01-15 16:28:10 -05:00
kepler155c@gmail.com
7db1db2957 transition to kernel 2018-01-14 18:48:18 -05:00
kepler155c@gmail.com
fd1d10a656 transition to kernel 2018-01-14 18:28:23 -05:00
kepler155c@gmail.com
30dd2a2b16 transition to kernel 2018-01-14 16:44:43 -05:00
kepler155c@gmail.com
9de9452dd3 transition to kernel 2018-01-13 23:40:53 -05:00
kepler155c@gmail.com
bd37b57780 transition to kernel 2018-01-13 15:17:26 -05:00
kepler155c@gmail.com
fc8d44b60d transition tot kernel 2018-01-11 20:53:32 -05:00
kepler155c@gmail.com
d224f5df25 move multishell functionality to kernel 2018-01-10 16:46:37 -05:00
kepler155c@gmail.com
13ec8ea04f peripheral overhaul 2018-01-06 22:25:33 -05:00
kepler155c@gmail.com
911fec9118 peripheral overhaul 2018-01-06 06:07:49 -05:00
kepler155c@gmail.com
1528bab3ac open editor for config on first run 2018-01-04 03:30:59 -05:00
kepler155c@gmail.com
dc9d174085 shadow text color override 2018-01-03 02:43:17 -05:00
kepler155c@gmail.com
1108c173d7 pass raw values for grid overrides 2017-12-27 23:55:30 -05:00
kepler155c@gmail.com
0830e46fb8 fix slideout 2017-12-23 01:47:54 -05:00
kepler155c@gmail.com
743959c1fa better emits 2017-12-16 00:07:22 -05:00
kepler155c@gmail.com
dd4211745e modal windows 2017-12-13 01:37:31 -05:00
kepler155c@gmail.com
0df22efdc2 slide out for UI + turtle.has 2017-12-11 11:31:41 -05:00
kepler155c@gmail.com
a8a4ceb85d api cleanup 2017-11-15 00:08:42 -05:00
kepler155c@gmail.com
f533e42c0c turtle api 2017-10-31 02:01:18 -04:00
kepler155c@gmail.com
1b9450017d refactor + cleanup 2017-10-27 20:24:48 -04:00
kepler155c@gmail.com
cac15722b8 proxy + pathfinding optimization 2017-10-26 18:56:55 -04:00
kepler155c@gmail.com
7fd93e8a8b proxy apis over wireless 2017-10-24 23:01:40 -04:00
kepler155c@gmail.com
84b2b8ce63 cleanup 2017-10-23 19:33:53 -04:00
kepler155c@gmail.com
22a432492c naming 2017-10-22 19:10:29 -04:00
kepler155c@gmail.com
8d3f5329f2 misc 2017-10-22 01:30:26 -04:00
kepler155c@gmail.com
8e9ff9c626 input redo 2017-10-20 13:57:24 -04:00
kepler155c@gmail.com
31b3787695 input redo + env pollution 2017-10-20 04:23:17 -04:00
kepler155c@gmail.com
fb0f3e567a redo tabs + wizard 2017-10-18 19:51:55 -04:00
kepler155c@gmail.com
f6d1cfc7ee grid event 2017-10-17 22:30:23 -04:00
kepler155c@gmail.com
39ba226a82 cleanup 2017-10-16 16:54:50 -04:00
kepler155c@gmail.com
fe0ca72b8b redo input translation -round 2 2017-10-16 00:02:42 -04:00
kepler155c@gmail.com
2721840596 redo input translation + shift-paste 2017-10-15 19:55:05 -04:00
kepler155c@gmail.com
9b8b5238b0 multishell hooks 2017-10-15 02:36:54 -04:00
kepler155c@gmail.com
8b187f2813 multishell hooks 2017-10-14 03:41:54 -04:00
kepler155c@gmail.com
153b0b86ff autocomplete 2017-10-13 16:30:47 -04:00
kepler155c@gmail.com
a9634cb438 version checking + ui cleanup 2017-10-12 17:58:35 -04:00
kepler155c@gmail.com
3460dd68b2 cleanup + global clipboard 2017-10-11 22:39:04 -04:00
kepler155c@gmail.com
f9221e67be cleanup 2017-10-11 16:31:48 -04:00
kepler155c@gmail.com
852ad193f0 simplify ui 2017-10-11 11:37:52 -04:00
kepler155c@gmail.com
05c99b583a friendlier networking + adding tabs 2017-10-09 13:08:38 -04:00
kepler155c@gmail.com
f5b99d91e5 tabs update + object identity 2017-10-09 00:26:19 -04:00
kepler155c@gmail.com
955f11042b http fixes 2017-10-08 20:03:01 -04:00
kepler155c@gmail.com
a625b52bad lint 2017-10-08 17:45:01 -04:00
kepler155c@gmail.com
98ec840db1 UI improvements 2017-10-07 23:03:18 -04:00
kepler155c@gmail.com
af981dd1f8 transition refactor + inactive elements 2017-10-07 00:27:41 -04:00
kepler155c@gmail.com
91a05c07dd drop menus 2017-10-06 13:39:47 -04:00
kepler155c@gmail.com
fc69d4be83 canvas palette 2017-10-06 03:07:24 -04:00
kepler155c@gmail.com
f0846c8daa id upgrade fixes 2017-10-05 18:05:57 -04:00
249 changed files with 24736 additions and 15871 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,6 +1,6 @@
MIT License MIT License
Copyright (c) 2016-2017 kepler155c Copyright (c) 2016-2019 kepler155c
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

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)
@@ -12,15 +14,7 @@
* Run scripts on single or groups of computers (GUI) * Run scripts on single or groups of computers (GUI)
* Turtle follow (with GPS) and turtle come to you (without GPS) * Turtle follow (with GPS) and turtle come to you (without GPS)
## Install (MC 1.8+) ## Install
``` ```
pastebin run uzghlbnc wget run https://git.spatulaa.com/MayaTheShy/Opus/raw/branch/main/installer.lua
```
Select either master-1.8 for stable or develop-1.8 for the upcoming release
## Install (MC 1.7)
```
pastebin run sj4VMVJj
reboot
``` ```

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

45
startup
View File

@@ -1,45 +0,0 @@
local bootOptions = {
{ prompt = 'Default Shell', file = '/sys/boot/default.boot' },
{ prompt = 'Opus' , file = '/sys/boot/multishell.boot' },
-- { prompt = 'TLCO' , file = '/sys/boot/tlco.boot' },
}
local bootOption = 2
local function startupMenu()
while true do
term.clear()
term.setCursorPos(1, 1)
print('Select startup mode')
print()
for k,option in pairs(bootOptions) do
print(k .. ' : ' .. option.prompt)
end
print('')
term.write('> ')
local ch = tonumber(read())
if ch and bootOptions[ch] then
return ch
end
end
term.clear()
term.setCursorPos(1, 1)
end
term.clear()
term.setCursorPos(1, 1)
print('Starting OS')
print()
print('Press any key for menu')
local timerId = os.startTimer(1.5)
while true do
local e, id = os.pullEvent()
if e == 'timer' and id == timerId then
break
end
if e == 'char' then
bootOption = startupMenu()
break
end
end
os.run(getfenv(1), bootOptions[bootOption].file)

196
startup.lua Normal file
View File

@@ -0,0 +1,196 @@
--[[
.startup.boot
delay
description: delays amount before starting the default selection
default: 1.5
preload
description : runs before menu is displayed, can be used for password
locking, drive encryption, etc.
example : { [1] = '/path/somefile.lua', [2] = 'path2/another.lua' }
menu
description: array of menu entries (see .startup.boot for examples)
]]
local colors = _G.colors
local fs = _G.fs
local keys = _G.keys
local os = _G.os
local settings = _G.settings
local term = _G.term
local textutils = _G.textutils
local function loadBootOptions()
if not fs.exists('.startup.boot') then
local f = fs.open('.startup.boot', 'w')
f.write(textutils.serialize({
delay = 1.5,
preload = { },
menu = {
{ prompt = os.version() },
{ prompt = 'Opus' , args = { '/sys/boot/opus.lua' } },
{ prompt = 'Opus Shell' , args = { '/sys/boot/opus.lua', '/sys/apps/shell.lua' } },
{ prompt = 'Opus Kiosk' , args = { '/sys/boot/kiosk.lua' } },
{ prompt = 'Opus TLCO' , args = { '/sys/boot/tlco.lua' } },
},
}))
f.close()
end
local f = fs.open('.startup.boot', 'r')
local options = textutils.unserialize(f.readAll())
f.close()
-- Backwards compatibility for .startup.boot files created before sys/boot files' extensions were changed
local changed = false
for _, item in pairs(options.menu) do
if item.args and item.args[1]:match("/?sys/boot/%l+%.boot") then
item.args[1] = item.args[1]:gsub("%.boot", "%.lua")
changed = true
end
end
if changed then
local f = fs.open(".startup.boot", "w")
f.write(textutils.serialize(options))
f.close()
end
return options
end
local bootOptions = loadBootOptions()
local bootOption = 2
if settings then
settings.load('.settings')
bootOption = tonumber(settings.get('opus.boot_option')) or bootOption
end
local function startupMenu()
local x, y = term.getSize()
local align, selected = 0, bootOption
local function redraw()
local title = "Boot Options:"
term.clear()
term.setTextColor(colors.white)
term.setCursorPos((x/2)-(#title/2), (y/2)-(#bootOptions.menu/2)-1)
term.write(title)
for i, item in pairs(bootOptions.menu) do
local txt = i .. ". " .. item.prompt
term.setCursorPos((x/2)-(align/2), (y/2)-(#bootOptions.menu/2)+i)
term.write(txt)
end
end
for _, item in pairs(bootOptions.menu) do
if #item.prompt > align then
align = #item.prompt
end
end
redraw()
while true do
term.setCursorPos((x/2)-(align/2)-2, (y/2)-(#bootOptions.menu/2)+selected)
term.setTextColor(term.isColor() and colors.yellow or colors.lightGray)
term.write(">")
local event, key = os.pullEvent()
if event == "mouse_scroll" then
key = key == 1 and keys.down or keys.up
elseif event == 'key_up' then
key = nil -- only process key events
end
if key == keys.enter or key == keys.right then
return selected
elseif key == keys.down then
if selected == #bootOptions.menu then
selected = 0
end
selected = selected + 1
elseif key == keys.up then
if selected == 1 then
selected = #bootOptions.menu + 1
end
selected = selected - 1
elseif event == 'char' then
key = tonumber(key) or 0
if bootOptions.menu[key] then
return key
end
end
local cx, cy = term.getCursorPos()
term.setCursorPos(cx-1, cy)
term.write(" ")
end
end
local function splash()
local w, h = term.current().getSize()
term.setTextColor(colors.white)
if not term.isColor() then
local str = 'Opus OS'
term.setCursorPos((w - #str) / 2, h / 2)
term.write(str)
else
term.setBackgroundColor(colors.black)
term.clear()
local opus = {
'fffff00',
'ffff07000',
'ff00770b00f4444',
'ff077777444444444',
'f07777744444444444',
'f0000777444444444',
'070000111744444',
'777770000',
'7777000000',
'70700000000',
'077000000000',
}
for k,line in ipairs(opus) do
term.setCursorPos((w - 18) / 2, k + (h - #opus) / 2)
term.blit(string.rep(' ', #line), string.rep('a', #line), line)
end
end
local str = 'Press any key for menu'
term.setCursorPos((w - #str) / 2, h)
term.write(str)
end
for _, v in pairs(bootOptions.preload) do
os.run(_ENV, v)
end
term.clear()
splash()
local timerId = os.startTimer(bootOptions.delay)
while true do
local e, id = os.pullEvent()
if e == 'timer' and id == timerId then
break
end
if e == 'char' or e == 'key' then
bootOption = startupMenu()
if settings then
settings.set('opus.boot_option', bootOption)
settings.save('.settings')
end
break
end
end
term.clear()
term.setCursorPos(1, 1)
if bootOptions.menu[bootOption].args then
os.run(_ENV, table.unpack(bootOptions.menu[bootOption].args))
else
print(bootOptions.menu[bootOption].prompt)
end

View File

@@ -1,55 +0,0 @@
local Ansi = setmetatable({ }, {
__call = function(self, ...)
local str = '\027['
for k,v in ipairs({ ...}) do
if k == 1 then
str = str .. v
else
str = str .. ';' .. v
end
end
return str .. 'm'
end
})
Ansi.codes = {
reset = 0,
white = 1,
orange = 2,
magenta = 3,
lightBlue = 4,
yellow = 5,
lime = 6,
pink = 7,
gray = 8,
lightGray = 9,
cyan = 10,
purple = 11,
blue = 12,
brown = 13,
green = 14,
red = 15,
black = 16,
onwhite = 21,
onorange = 22,
onmagenta = 23,
onlightBlue = 24,
onyellow = 25,
onlime = 26,
onpink = 27,
ongray = 28,
onlightGray = 29,
oncyan = 30,
onpurple = 31,
onblue = 32,
onbrown = 33,
ongreen = 34,
onred = 35,
onblack = 36,
}
for k,v in pairs(Ansi.codes) do
Ansi[k] = Ansi(v)
end
return Ansi

View File

@@ -1,46 +0,0 @@
-- From http://lua-users.org/wiki/SimpleLuaClasses
-- (with some modifications)
-- class.lua
-- Compatible with Lua 5.1 (not 5.0).
return function(base)
local c = { } -- a new class instance
if type(base) == 'table' then
-- our new class is a shallow copy of the base class!
for i,v in pairs(base) do
c[i] = v
end
c._base = base
end
-- the class will be the metatable for all its objects,
-- and they will look up their methods in it.
c.__index = c
-- expose a constructor which can be called by <classname>(<args>)
setmetatable(c, {
__call = function(class_tbl, ...)
local obj = {}
setmetatable(obj,c)
if class_tbl.init then
class_tbl.init(obj, ...)
else
-- make sure that any stuff from the base class is initialized!
if base and base.init then
base.init(obj, ...)
end
end
return obj
end
})
c.is_a =
function(self, klass)
local m = getmetatable(self)
while m do
if m == klass then return true end
m = m._base
end
return false
end
return c
end

View File

@@ -1,24 +0,0 @@
local Util = require('util')
local Config = { }
Config.load = function(fname, data)
local filename = 'usr/config/' .. fname
if not fs.exists('usr/config') then
fs.makeDir('usr/config')
end
if not fs.exists(filename) then
Util.writeTable(filename, data)
else
Util.merge(data, Util.readTable(filename) or { })
end
end
Config.update = function(fname, data)
local filename = 'usr/config/' .. fname
Util.writeTable(filename, data)
end
return Config

View File

@@ -1,150 +0,0 @@
-- https://github.com/PixelToast/ComputerCraft/blob/master/apis/enc
local Crypto = { }
local function serialize(t)
local sType = type(t)
if sType == "table" then
local lstcnt=0
for k,v in pairs(t) do
lstcnt = lstcnt + 1
end
local result = "{"
local aset=1
for k,v in pairs(t) do
if k==aset then
result = result..serialize(v)..","
aset=aset+1
else
result = result..("["..serialize(k).."]="..serialize(v)..",")
end
end
result = result.."}"
return result
elseif sType == "string" then
return string.format("%q",t)
elseif sType == "number" or sType == "boolean" or sType == "nil" then
return tostring(t)
elseif sType == "function" then
local status,data=pcall(string.dump,t)
if status then
data2=""
for char in string.gmatch(data,".") do
data2=data2..zfill(string.byte(char))
end
return 'f("'..data2..'")'
else
error("Invalid function: "..data)
end
else
error("Could not serialize type "..sType..".")
end
end
local function unserialize( s )
local func, e = loadstring( "return "..s, "serialize" )
if not func then
return s,e
else
setfenv( func, {
f=function(S)
return loadstring(splitnum(S))
end,
})
return func()
end
end
local function splitnum(S)
local Out=""
for l1=1,#S,2 do
local l2=(#S-l1)+1
local function sure(N,n)
if (l2-n)<1 then N="0" end
return N
end
local CNum=tonumber("0x"..sure(string.sub(S,l2-1,l2-1),1) .. sure(string.sub(S,l2,l2),0))
Out=string.char(CNum)..Out
end
return Out
end
local function zfill(N)
N=string.format("%X",N)
Zs=""
if #N==1 then
Zs="0"
end
return Zs..N
end
local function wrap(N)
return N-(math.floor(N/256)*256)
end
local function checksum(S)
local sum=0
for char in string.gmatch(S,".") do
math.randomseed(string.byte(char)+sum)
sum=sum+math.random(0,9999)
end
math.randomseed(sum)
return sum
end
local function genkey(len,psw)
checksum(psw)
local key={}
local tKeys={}
for l1=1,len do
local num=math.random(1,len)
while tKeys[num] do
num=math.random(1,len)
end
tKeys[num]=true
key[l1]={num,math.random(0,255)}
end
return key
end
function Crypto.encrypt(data,psw)
data=serialize(data)
local chs=checksum(data)
local key=genkey(#data,psw)
local out={}
local cnt=1
for char in string.gmatch(data,".") do
table.insert(out,key[cnt][1],zfill(wrap(string.byte(char)+key[cnt][2])),chars)
cnt=cnt+1
end
return string.sub(serialize({chs,table.concat(out)}),2,-3)
end
function Crypto.decrypt(data,psw)
local oData=data
data=unserialize("{"..data.."}")
if type(data)~="table" then
return oData
end
local chs=data[1]
data=data[2]
local key=genkey((#data)/2,psw)
local sKey={}
for k,v in pairs(key) do
sKey[v[1]]={k,v[2]}
end
local str=splitnum(data)
local cnt=1
local out={}
for char in string.gmatch(str,".") do
table.insert(out,sKey[cnt][1],string.char(wrap(string.byte(char)-sKey[cnt][2])))
cnt=cnt+1
end
out=table.concat(out)
if checksum(out or "")==chs then
return unserialize(out)
end
return oData,out,chs
end
return Crypto

View File

@@ -1,216 +0,0 @@
local Event = {
uid = 1, -- unique id for handlers
routines = { }, -- coroutines
types = { }, -- event handlers
timers = { }, -- named timers
terminate = false,
}
local Routine = { }
function Routine:isDead()
if not self.co then
return true
end
return coroutine.status(self.co) == 'dead'
end
function Routine:terminate()
if self.co then
self:resume('terminate')
end
end
function Routine:resume(event, ...)
if not self.co then
error('Cannot resume a dead routine')
end
if not self.filter or self.filter == event or event == "terminate" then
local s, m = coroutine.resume(self.co, event, ...)
if coroutine.status(self.co) == 'dead' then
self.co = nil
self.filter = nil
Event.routines[self.uid] = nil
else
self.filter = m
end
if not s and event ~= 'terminate' then
error('\n' .. (m or 'Error processing event'))
end
return s, m
end
return true, self.filter
end
local function nextUID()
Event.uid = Event.uid + 1
return Event.uid - 1
end
function Event.on(event, fn)
local handlers = Event.types[event]
if not handlers then
handlers = { }
Event.types[event] = handlers
end
local handler = {
uid = nextUID(),
event = event,
fn = fn,
}
handlers[handler.uid] = handler
setmetatable(handler, { __index = Routine })
return handler
end
function Event.off(h)
if h and h.event then
Event.types[h.event][h.uid] = nil
end
end
local function addTimer(interval, recurring, fn)
local timerId = os.startTimer(interval)
local handler
handler = Event.on('timer', function(t, id)
if timerId == id then
fn(t, id)
if recurring then
timerId = os.startTimer(interval)
else
Event.off(handler)
end
end
end)
return handler
end
function Event.onInterval(interval, fn)
return addTimer(interval, true, fn)
end
function Event.onTimeout(timeout, fn)
return addTimer(timeout, false, fn)
end
function Event.addNamedTimer(name, interval, recurring, fn)
Event.cancelNamedTimer(name)
Event.timers[name] = addTimer(interval, recurring, fn)
end
function Event.cancelNamedTimer(name)
local timer = Event.timers[name]
if timer then
Event.off(timer)
end
end
function Event.waitForEvent(event, timeout)
local timerId = os.startTimer(timeout)
repeat
local e = { os.pullEvent() }
if e[1] == event then
return table.unpack(e)
end
until e[1] == 'timer' and e[2] == timerId
end
function Event.addRoutine(fn)
local r = {
co = coroutine.create(fn),
uid = nextUID()
}
setmetatable(r, { __index = Routine })
Event.routines[r.uid] = r
r:resume()
return r
end
function Event.pullEvents(...)
for _, fn in ipairs({ ... }) do
Event.addRoutine(fn)
end
repeat
local e = Event.pullEvent()
until e[1] == 'terminate'
end
function Event.exitPullEvents()
Event.terminate = true
os.sleep(0)
end
local function processHandlers(event)
local handlers = Event.types[event]
if handlers then
for _,h in pairs(handlers) do
if not h.co then
-- callbacks are single threaded (only 1 co per handler)
h.co = coroutine.create(h.fn)
Event.routines[h.uid] = h
end
end
end
end
local function tokeys(t)
local keys = { }
for k in pairs(t) do
keys[#keys+1] = k
end
return keys
end
local function processRoutines(...)
local keys = tokeys(Event.routines)
for _,key in ipairs(keys) do
local r = Event.routines[key]
if r then
r:resume(...)
end
end
end
function Event.processEvent(e)
processHandlers(e[1])
processRoutines(table.unpack(e))
end
function Event.pullEvent(eventType)
while true do
local e = { os.pullEventRaw() }
processHandlers(e[1])
processRoutines(table.unpack(e))
if Event.terminate or e[1] == 'terminate' then
Event.terminate = false
return { 'terminate' }
end
if not eventType or e[1] == eventType then
return e
end
end
end
return Event

View File

@@ -1,19 +0,0 @@
local git = require('git')
local gitfs = { }
function gitfs.mount(dir, repo)
if not repo then
error('gitfs syntax: repo')
end
local list = git.list(repo)
for path, entry in pairs(list) do
if not fs.exists(fs.combine(dir, path)) then
local node = fs.mount(fs.combine(dir, path), 'urlfs', entry.url)
node.size = entry.size
end
end
end
return gitfs

View File

@@ -1,61 +0,0 @@
local linkfs = { }
local methods = { 'exists', 'getFreeSpace', 'getSize',
'isDir', 'isReadOnly', 'list', 'listEx', 'makeDir', 'open', 'getDrive' }
for _,m in pairs(methods) do
linkfs[m] = function(node, dir, ...)
dir = dir:gsub(node.mountPoint, node.source, 1)
return fs[m](dir, ...)
end
end
function linkfs.mount(dir, source)
if not source then
error('Source is required')
end
source = fs.combine(source, '')
if fs.isDir(source) then
return {
source = source,
nodes = { },
}
end
return {
source = source
}
end
function linkfs.copy(node, s, t)
s = s:gsub(node.mountPoint, node.source, 1)
t = t:gsub(node.mountPoint, node.source, 1)
return fs.copy(s, t)
end
function linkfs.delete(node, dir)
if dir == node.mountPoint then
fs.unmount(node.mountPoint)
else
dir = dir:gsub(node.mountPoint, node.source, 1)
return fs.delete(dir)
end
end
function linkfs.find(node, spec)
spec = spec:gsub(node.mountPoint, node.source, 1)
local list = fs.find(spec)
for k,f in ipairs(list) do
list[k] = f:gsub(node.source, node.mountPoint, 1)
end
return list
end
function linkfs.move(node, s, t)
s = s:gsub(node.mountPoint, node.source, 1)
t = t:gsub(node.mountPoint, node.source, 1)
return fs.move(s, t)
end
return linkfs

View File

@@ -1,163 +0,0 @@
local Socket = require('socket')
local synchronized = require('sync')
local netfs = { }
local function remoteCommand(node, msg)
for i = 1, 2 do
if not node.socket then
node.socket = Socket.connect(node.id, 139)
end
if not node.socket then
error('netfs: Unable to establish connection to ' .. node.id)
fs.unmount(node.mountPoint)
return
end
local ret
synchronized(node.socket, function()
node.socket:write(msg)
ret = node.socket:read(1)
end)
if ret then
return ret.response
end
node.socket:close()
node.socket = nil
end
error('netfs: Connection failed', 2)
end
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx' }
local function resolveDir(dir, node)
dir = dir:gsub(node.mountPoint, '', 1)
return fs.combine(node.directory, dir)
end
for _,m in pairs(methods) do
netfs[m] = function(node, dir)
dir = resolveDir(dir, node)
return remoteCommand(node, {
fn = m,
args = { dir },
})
end
end
function netfs.mount(dir, id, directory)
if not id or not tonumber(id) then
error('ramfs syntax: computerId [directory]')
end
return {
id = tonumber(id),
nodes = { },
directory = directory or '',
}
end
function netfs.getDrive()
return 'net'
end
function netfs.complete(node, partial, dir, includeFiles, includeSlash)
dir = resolveDir(dir, node)
return remoteCommand(node, {
fn = 'complete',
args = { partial, dir, includeFiles, includeSlash },
})
end
function netfs.copy(node, s, t)
s = resolveDir(s, node)
t = resolveDir(t, node)
return remoteCommand(node, {
fn = 'copy',
args = { s, t },
})
end
function netfs.isDir(node, dir)
if dir == node.mountPoint and node.directory == '' then
return true
end
return remoteCommand(node, {
fn = 'isDir',
args = { resolveDir(dir, node) },
})
end
function netfs.isReadOnly(node, dir)
if dir == node.mountPoint and node.directory == '' then
return false
end
return remoteCommand(node, {
fn = 'isReadOnly',
args = { resolveDir(dir, node) },
})
end
function netfs.getSize(node, dir)
if dir == node.mountPoint and node.directory == '' then
return 0
end
return remoteCommand(node, {
fn = 'getSize',
args = { resolveDir(dir, node) },
})
end
function netfs.find(node, spec)
spec = resolveDir(spec, node)
local list = remoteCommand(node, {
fn = 'find',
args = { spec },
})
for k,f in ipairs(list) do
list[k] = fs.combine(node.mountPoint, f)
end
return list
end
function netfs.move(node, s, t)
s = resolveDir(s, node)
t = resolveDir(t, node)
return remoteCommand(node, {
fn = 'move',
args = { s, t },
})
end
function netfs.open(node, fn, fl)
fn = resolveDir(fn, node)
local vfh = remoteCommand(node, {
fn = 'open',
args = { fn, fl },
})
if vfh then
vfh.node = node
for _,m in ipairs(vfh.methods) do
vfh[m] = function(...)
return remoteCommand(node, {
fn = 'fileOp',
args = { vfh.fileUid, m, ... },
})
end
end
end
return vfh
end
return netfs

View File

@@ -1,145 +0,0 @@
local Util = require('util')
local ramfs = { }
function ramfs.mount(dir, nodeType)
if nodeType == 'directory' then
return {
nodes = { },
size = 0,
}
elseif nodeType == 'file' then
return {
size = 0,
}
end
error('ramfs syntax: [directory, file]')
end
function ramfs.delete(node, dir)
if node.mountPoint == dir then
fs.unmount(node.mountPoint)
end
end
function ramfs.exists(node, fn)
return node.mountPoint == fn
end
function ramfs.getSize(node)
return node.size
end
function ramfs.isReadOnly()
return false
end
function ramfs.makeDir(node, dir)
fs.mount(dir, 'ramfs', 'directory')
end
function ramfs.isDir(node)
return not not node.nodes
end
function ramfs.getDrive()
return 'ram'
end
function ramfs.list(node, dir, full)
if node.nodes and node.mountPoint == dir then
local files = { }
for k,v in pairs(node.nodes) do
table.insert(files, k)
end
return files
end
error('Not a directory')
end
function ramfs.open(node, fn, fl)
if fl ~= 'r' and fl ~= 'w' and fl ~= 'rb' and fl ~= 'wb' then
error('Unsupported mode')
end
if fl == 'r' then
if node.mountPoint ~= fn then
return
end
local ctr = 0
local lines
return {
readLine = function()
if not lines then
lines = Util.split(node.contents)
end
ctr = ctr + 1
return lines[ctr]
end,
readAll = function()
return node.contents
end,
close = function()
lines = nil
end,
}
elseif fl == 'w' then
node = fs.mount(fn, 'ramfs', 'file')
local c = ''
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,
}
elseif fl == 'rb' then
if node.mountPoint ~= fn or not node.contents then
return
end
local ctr = 0
return {
read = function()
ctr = ctr + 1
return node.contents[ctr]
end,
close = function()
end,
}
elseif fl == 'wb' then
node = fs.mount(fn, 'ramfs', 'file')
local c = { }
return {
write = function(b)
table.insert(c, b)
end,
flush = function()
node.contents = c
node.size = #c
end,
close = function()
node.contents = c
node.size = #c
c = nil
end,
}
end
end
return ramfs

View File

@@ -1,96 +0,0 @@
local synchronized = require('sync')
local Util = require('util')
local urlfs = { }
function urlfs.mount(dir, url)
if not url then
error('URL is required')
end
return {
url = url,
}
end
function urlfs.delete(node, dir)
fs.unmount(dir)
end
function urlfs.exists()
return true
end
function urlfs.getSize(node)
return node.size or 0
end
function urlfs.isReadOnly()
return true
end
function urlfs.isDir()
return false
end
function urlfs.getDrive()
return 'url'
end
function urlfs.open(node, fn, fl)
if fl == 'w' or fl == 'wb' then
fs.delete(fn)
return fs.open(fn, fl)
end
if fl ~= 'r' and fl ~= 'rb' then
error('Unsupported mode')
end
local c = node.cache
if not c then
synchronized(node.url, function()
c = Util.download(node.url)
end)
if c then
node.cache = c
node.size = #c
end
end
if not c then
return
end
local ctr = 0
local lines
if fl == 'r' then
return {
readLine = function()
if not lines then
lines = Util.split(c)
end
ctr = ctr + 1
return lines[ctr]
end,
readAll = function()
return c
end,
close = function()
lines = nil
end,
}
end
return {
read = function()
ctr = ctr + 1
return c:sub(ctr, ctr):byte()
end,
close = function()
ctr = 0
end,
}
end
return urlfs

View File

@@ -1,49 +0,0 @@
local json = require('json')
local Util = require('util')
local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1'
local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s'
local git = { }
function git.list(repo)
local t = Util.split(repo, '(.-)/')
local user = t[1]
local repo = t[2]
local branch = t[3] or 'master'
local dataUrl = string.format(TREE_URL, user, repo, branch)
local contents = Util.download(dataUrl)
if not contents then
error('Invalid repository')
end
local data = json.decode(contents)
if data.message and data.message:find("API rate limit exceeded") then
error("Out of API calls, try again later")
end
if data.message and data.message == "Not found" then
error("Invalid repository")
end
local list = { }
for k,v in pairs(data.tree) do
if v.type == "blob" then
v.path = v.path:gsub("%s","%%20")
list[v.path] = {
url = string.format(FILE_URL, user, repo, branch, v.path),
size = v.size,
}
end
end
return list
end
return git

View File

@@ -1,152 +0,0 @@
local GPS = { }
function GPS.locate(timeout, debug)
local pt = { }
timeout = timeout or 10
pt.x, pt.y, pt.z = gps.locate(timeout, debug)
if pt.x then
return pt
end
end
function GPS.isAvailable()
return device.wireless_modem and GPS.locate()
end
function GPS.getPoint(timeout, debug)
local pt = GPS.locate(timeout, debug)
if not pt then
return
end
pt.x = math.floor(pt.x)
pt.y = math.floor(pt.y)
pt.z = math.floor(pt.z)
if pocket then
pt.y = pt.y - 1
end
return pt
end
function GPS.getHeading(timeout)
if not turtle then
return
end
local apt = GPS.locate(timeout)
if not apt then
return
end
local heading = turtle.point.heading
while not turtle.forward() do
turtle.turnRight()
if turtle.getHeading() == heading then
printError('GPS.getPoint: Unable to move forward')
return
end
end
local bpt = GPS.locate()
if not bpt then
return
end
if apt.x < bpt.x then
return 0
elseif apt.z < bpt.z then
return 1
elseif apt.x > bpt.x then
return 2
end
return 3
end
function GPS.getPointAndHeading(timeout)
local heading = GPS.getHeading(timeout)
if heading then
local pt = GPS.getPoint()
if pt then
pt.heading = heading
end
return pt
end
end
-- from stock gps API
local function trilaterate( A, B, C )
local a2b = B.position - A.position
local a2c = C.position - A.position
if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then
return nil
end
local d = a2b:length()
local ex = a2b:normalize( )
local i = ex:dot( a2c )
local ey = (a2c - (ex * i)):normalize()
local j = ey:dot( a2c )
local ez = ex:cross( ey )
local r1 = A.distance
local r2 = B.distance
local r3 = C.distance
local x = (r1*r1 - r2*r2 + d*d) / (2*d)
local y = (r1*r1 - r3*r3 - x*x + (x-i)*(x-i) + j*j) / (2*j)
local result = A.position + (ex * x) + (ey * y)
local zSquared = r1*r1 - x*x - y*y
if zSquared > 0 then
local z = math.sqrt( zSquared )
local result1 = result + (ez * z)
local result2 = result - (ez * z)
local rounded1, rounded2 = result1:round(), result2:round()
if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then
return rounded1, rounded2
else
return rounded1
end
end
return result:round()
end
local function narrow( p1, p2, fix )
local dist1 = math.abs( (p1 - fix.position):length() - fix.distance )
local dist2 = math.abs( (p2 - fix.position):length() - fix.distance )
if math.abs(dist1 - dist2) < 0.05 then
return p1, p2
elseif dist1 < dist2 then
return p1:round()
else
return p2:round()
end
end
-- end stock gps api
function GPS.trilaterate(tFixes)
local pos1, pos2 = trilaterate(tFixes[1], tFixes[2], tFixes[3])
if pos2 then
pos1, pos2 = narrow(pos1, pos2, tFixes[4])
end
if pos1 and pos2 then
print("Ambiguous position")
print("Could be "..pos1.x..","..pos1.y..","..pos1.z.." or "..pos2.x..","..pos2.y..","..pos2.z )
return
end
return pos1
end
return GPS

View File

@@ -1,50 +0,0 @@
local Util = require('util')
local History = { }
local History_mt = { __index = History }
function History.load(filename, limit)
local self = setmetatable({
limit = limit,
filename = filename,
}, History_mt)
self.entries = Util.readLines(filename) or { }
self.pos = #self.entries + 1
return self
end
function History:add(line)
if line ~= self.entries[#self.entries] then
table.insert(self.entries, line)
if self.limit then
while #self.entries > self.limit do
table.remove(self.entries, 1)
end
end
Util.writeLines(self.filename, self.entries)
self.pos = #self.entries + 1
end
end
function History:reset()
self.pos = #self.entries + 1
end
function History:back()
if self.pos > 1 then
self.pos = self.pos - 1
return self.entries[self.pos]
end
end
function History:forward()
if self.pos <= #self.entries then
self.pos = self.pos + 1
return self.entries[self.pos]
end
end
return History

View File

@@ -1,162 +0,0 @@
local DEFAULT_UPATH = 'https://raw.githubusercontent.com/kepler155c/opus/master/sys/apis'
local PASTEBIN_URL = 'http://pastebin.com/raw'
local GIT_URL = 'https://raw.githubusercontent.com'
-- fix broken http get
local syncLocks = { }
local function sync(obj, fn)
local key = tostring(obj)
if syncLocks[key] then
local cos = tostring(coroutine.running())
table.insert(syncLocks[key], cos)
repeat
local _, co = os.pullEvent('sync_lock')
until co == cos
else
syncLocks[key] = { }
end
local s, m = pcall(fn)
local co = table.remove(syncLocks[key], 1)
if co then
os.queueEvent('sync_lock', co)
else
syncLocks[key] = nil
end
if not s then
error(m)
end
end
local function loadUrl(url)
local c
sync(url, function()
local h = http.get(url)
if h then
c = h.readAll()
h.close()
end
end)
if c and #c > 0 then
return c
end
end
local function requireWrapper(env)
local function standardSearcher(modname, env, shell)
if package.loaded[modname] then
return function()
return package.loaded[modname]
end
end
end
local function shellSearcher(modname, env, shell)
local fname = modname:gsub('%.', '/') .. '.lua'
if shell and type(shell.dir) == 'function' then
local path = shell.resolve(fname)
if fs.exists(path) and not fs.isDir(path) then
return loadfile(path, env)
end
end
end
local function pathSearcher(modname, env, shell)
local fname = modname:gsub('%.', '/') .. '.lua'
for dir in string.gmatch(package.path, "[^:]+") do
local path = fs.combine(dir, fname)
if fs.exists(path) and not fs.isDir(path) then
return loadfile(path, env)
end
end
end
-- require('BniCQPVf')
local function pastebinSearcher(modname, env, shell)
if #modname == 8 and not modname:match('%W') then
local url = PASTEBIN_URL .. '/' .. modname
local c = loadUrl(url)
if c then
return load(c, modname, nil, env)
end
end
end
-- require('kepler155c.opus.master.sys.apis.util')
local function gitSearcher(modname, env, shell)
local fname = modname:gsub('%.', '/') .. '.lua'
local _, count = fname:gsub("/", "")
if count >= 3 then
local url = GIT_URL .. '/' .. fname
local c = loadUrl(url)
if c then
return load(c, modname, nil, env)
end
end
end
local function urlSearcher(modname, env, shell)
local fname = modname:gsub('%.', '/') .. '.lua'
if fname:sub(1, 1) ~= '/' then
for entry in string.gmatch(package.upath, "[^;]+") do
local url = entry .. '/' .. fname
local c = loadUrl(url)
if c then
return load(c, modname, nil, env)
end
end
end
end
-- place package and require function into env
package = {
path = LUA_PATH or 'sys/apis',
upath = LUA_UPATH or DEFAULT_UPATH,
config = '/\n:\n?\n!\n-',
loaded = {
math = math,
string = string,
table = table,
io = io,
os = os,
},
loaders = {
standardSearcher,
shellSearcher,
pathSearcher,
pastebinSearcher,
gitSearcher,
urlSearcher,
}
}
function require(modname)
for _,searcher in ipairs(package.loaders) do
local fn, msg = searcher(modname, env, shell)
if fn then
local module, msg = fn(modname, env)
if not module then
error(msg or (modname .. ' module returned nil'), 2)
end
package.loaded[modname] = module
return module
end
if msg then
error(msg, 2)
end
end
error('Unable to find module ' .. modname)
end
return require -- backwards compatible
end
return function(env)
setfenv(requireWrapper, env)
return requireWrapper(env)
end

View File

@@ -1,213 +0,0 @@
-- credit ElvishJerricco
-- http://pastebin.com/raw.php?i=4nRg9CHU
local json = { }
------------------------------------------------------------------ utils
local controls = {["\n"]="\\n", ["\r"]="\\r", ["\t"]="\\t", ["\b"]="\\b", ["\f"]="\\f", ["\""]="\\\"", ["\\"]="\\\\"}
local function isArray(t)
local max = 0
for k,v in pairs(t) do
if type(k) ~= "number" then
return false
elseif k > max then
max = k
end
end
return max == #t
end
local whites = {['\n']=true; ['\r']=true; ['\t']=true; [' ']=true; [',']=true; [':']=true}
local function removeWhite(str)
while whites[str:sub(1, 1)] do
str = str:sub(2)
end
return str
end
------------------------------------------------------------------ encoding
local function encodeCommon(val, pretty, tabLevel, tTracking)
local str = ""
-- Tabbing util
local function tab(s)
str = str .. ("\t"):rep(tabLevel) .. s
end
local function arrEncoding(val, bracket, closeBracket, iterator, loopFunc)
str = str .. bracket
if pretty then
str = str .. "\n"
tabLevel = tabLevel + 1
end
for k,v in iterator(val) do
tab("")
loopFunc(k,v)
str = str .. ","
if pretty then str = str .. "\n" end
end
if pretty then
tabLevel = tabLevel - 1
end
if str:sub(-2) == ",\n" then
str = str:sub(1, -3) .. "\n"
elseif str:sub(-1) == "," then
str = str:sub(1, -2)
end
tab(closeBracket)
end
-- Table encoding
if type(val) == "table" then
assert(not tTracking[val], "Cannot encode a table holding itself recursively")
tTracking[val] = true
if isArray(val) then
arrEncoding(val, "[", "]", ipairs, function(k,v)
str = str .. encodeCommon(v, pretty, tabLevel, tTracking)
end)
else
arrEncoding(val, "{", "}", pairs, function(k,v)
assert(type(k) == "string", "JSON object keys must be strings", 2)
str = str .. encodeCommon(k, pretty, tabLevel, tTracking)
str = str .. (pretty and ": " or ":") .. encodeCommon(v, pretty, tabLevel, tTracking)
end)
end
-- String encoding
elseif type(val) == "string" then
str = '"' .. val:gsub("[%c\"\\]", controls) .. '"'
-- Number encoding
elseif type(val) == "number" or type(val) == "boolean" then
str = tostring(val)
else
error("JSON only supports arrays, objects, numbers, booleans, and strings", 2)
end
return str
end
function json.encode(val)
return encodeCommon(val, false, 0, {})
end
function json.encodePretty(val)
return encodeCommon(val, true, 0, {})
end
------------------------------------------------------------------ decoding
local decodeControls = {}
for k,v in pairs(controls) do
decodeControls[v] = k
end
local function parseBoolean(str)
if str:sub(1, 4) == "true" then
return true, removeWhite(str:sub(5))
else
return false, removeWhite(str:sub(6))
end
end
local function parseNull(str)
return nil, removeWhite(str:sub(5))
end
local numChars = {['e']=true; ['E']=true; ['+']=true; ['-']=true; ['.']=true}
local function parseNumber(str)
local i = 1
while numChars[str:sub(i, i)] or tonumber(str:sub(i, i)) do
i = i + 1
end
local val = tonumber(str:sub(1, i - 1))
str = removeWhite(str:sub(i))
return val, str
end
local function parseString(str)
str = str:sub(2)
local s = ""
while str:sub(1,1) ~= "\"" do
local next = str:sub(1,1)
str = str:sub(2)
assert(next ~= "\n", "Unclosed string")
if next == "\\" then
local escape = str:sub(1,1)
str = str:sub(2)
next = assert(decodeControls[next..escape], "Invalid escape character")
end
s = s .. next
end
return s, removeWhite(str:sub(2))
end
function json.parseArray(str)
str = removeWhite(str:sub(2))
local val = {}
local i = 1
while str:sub(1, 1) ~= "]" do
local v
v, str = json.parseValue(str)
val[i] = v
i = i + 1
str = removeWhite(str)
end
str = removeWhite(str:sub(2))
return val, str
end
function json.parseValue(str)
local fchar = str:sub(1, 1)
if fchar == "{" then
return json.parseObject(str)
elseif fchar == "[" then
return json.parseArray(str)
elseif tonumber(fchar) ~= nil or numChars[fchar] then
return parseNumber(str)
elseif str:sub(1, 4) == "true" or str:sub(1, 5) == "false" then
return parseBoolean(str)
elseif fchar == "\"" then
return parseString(str)
elseif str:sub(1, 4) == "null" then
return parseNull(str)
end
end
function json.parseMember(str)
local k, val
k, str = json.parseValue(str)
val, str = json.parseValue(str)
return k, val, str
end
function json.parseObject(str)
str = removeWhite(str:sub(2))
local val = {}
while str:sub(1, 1) ~= "}" do
local k, v = nil, nil
k, v, str = json.parseMember(str)
val[k] = v
str = removeWhite(str)
end
str = removeWhite(str:sub(2))
return val, str
end
function json.decode(str)
str = removeWhite(str)
return json.parseValue(str)
end
function json.decodeFromFile(path)
local file = assert(fs.open(path, "r"))
local decoded = json.decode(file.readAll())
file.close()
return decoded
end
return json

View File

@@ -1,105 +0,0 @@
-- Various assertion function for API methods argument-checking
if (...) then
-- Dependancies
local _PATH = (...):gsub('%.core.assert$','')
local Utils = require (_PATH .. '.core.utils')
-- Local references
local lua_type = type
local floor = math.floor
local concat = table.concat
local next = next
local pairs = pairs
local getmetatable = getmetatable
-- Is I an integer ?
local function isInteger(i)
return lua_type(i) ==('number') and (floor(i)==i)
end
-- Override lua_type to return integers
local function type(v)
return isInteger(v) and 'int' or lua_type(v)
end
-- Does the given array contents match a predicate type ?
local function arrayContentsMatch(t,...)
local n_count = Utils.arraySize(t)
if n_count < 1 then return false end
local init_count = t[0] and 0 or 1
local n_count = (t[0] and n_count-1 or n_count)
local types = {...}
if types then types = concat(types) end
for i=init_count,n_count,1 do
if not t[i] then return false end
if types then
if not types:match(type(t[i])) then return false end
end
end
return true
end
-- Checks if arg is a valid array map
local function isMap(m)
if not arrayContentsMatch(m, 'table') then return false end
local lsize = Utils.arraySize(m[next(m)])
for k,v in pairs(m) do
if not arrayContentsMatch(m[k], 'string', 'int') then return false end
if Utils.arraySize(v)~=lsize then return false end
end
return true
end
-- Checks if s is a valid string map
local function isStringMap(s)
if lua_type(s) ~= 'string' then return false end
local w
for row in s:gmatch('[^\n\r]+') do
if not row then return false end
w = w or #row
if w ~= #row then return false end
end
return true
end
-- Does instance derive straight from class
local function derives(instance, class)
return getmetatable(instance) == class
end
-- Does instance inherits from class
local function inherits(instance, class)
return (getmetatable(getmetatable(instance)) == class)
end
-- Is arg a boolean
local function isBoolean(b)
return (b==true or b==false)
end
-- Is arg nil ?
local function isNil(n)
return (n==nil)
end
local function matchType(value, types)
return types:match(type(value))
end
return {
arrayContentsMatch = arrayContentsMatch,
derives = derives,
inherits = inherits,
isInteger = isInteger,
isBool = isBoolean,
isMap = isMap,
isStrMap = isStringMap,
isOutOfRange = isOutOfRange,
isNil = isNil,
type = type,
matchType = matchType
}
end

View File

@@ -1,175 +0,0 @@
--- A light implementation of Binary heaps data structure.
-- While running a search, some search algorithms (Astar, Dijkstra, Jump Point Search) have to maintains
-- a list of nodes called __open list__. Retrieve from this list the lowest cost node can be quite slow,
-- as it normally requires to skim through the full set of nodes stored in this list. This becomes a real
-- problem especially when dozens of nodes are being processed (on large maps).
--
-- The current module implements a <a href="http://www.policyalmanac.org/games/binaryHeaps.htm">binary heap</a>
-- data structure, from which the search algorithm will instantiate an open list, and cache the nodes being
-- examined during a search. As such, retrieving the lower-cost node is faster and globally makes the search end
-- up quickly.
--
-- This module is internally used by the library on purpose.
-- It should normally not be used explicitely, yet it remains fully accessible.
--
--[[
Notes:
This lighter implementation of binary heaps, based on :
https://github.com/Yonaba/Binary-Heaps
--]]
if (...) then
-- Dependency
local Utils = require((...):gsub('%.bheap$','.utils'))
-- Local reference
local floor = math.floor
-- Default comparison function
local function f_min(a,b) return a < b end
-- Percolates up
local function percolate_up(heap, index)
if index == 1 then return end
local pIndex
if index <= 1 then return end
if index%2 == 0 then
pIndex = index/2
else pIndex = (index-1)/2
end
if not heap._sort(heap._heap[pIndex], heap._heap[index]) then
heap._heap[pIndex], heap._heap[index] =
heap._heap[index], heap._heap[pIndex]
percolate_up(heap, pIndex)
end
end
-- Percolates down
local function percolate_down(heap,index)
local lfIndex,rtIndex,minIndex
lfIndex = 2*index
rtIndex = lfIndex + 1
if rtIndex > heap._size then
if lfIndex > heap._size then return
else minIndex = lfIndex end
else
if heap._sort(heap._heap[lfIndex],heap._heap[rtIndex]) then
minIndex = lfIndex
else
minIndex = rtIndex
end
end
if not heap._sort(heap._heap[index],heap._heap[minIndex]) then
heap._heap[index],heap._heap[minIndex] = heap._heap[minIndex],heap._heap[index]
percolate_down(heap,minIndex)
end
end
-- Produces a new heap
local function newHeap(template,comp)
return setmetatable({_heap = {},
_sort = comp or f_min, _size = 0},
template)
end
--- The `heap` class.<br/>
-- This class is callable.
-- _Therefore,_ <code>heap(...)</code> _is used to instantiate new heaps_.
-- @type heap
local heap = setmetatable({},
{__call = function(self,...)
return newHeap(self,...)
end})
heap.__index = heap
--- Checks if a `heap` is empty
-- @class function
-- @treturn bool __true__ of no item is queued in the heap, __false__ otherwise
-- @usage
-- if myHeap:empty() then
-- print('Heap is empty!')
-- end
function heap:empty()
return (self._size==0)
end
--- Clears the `heap` (removes all items queued in the heap)
-- @class function
-- @treturn heap self (the calling `heap` itself, can be chained)
-- @usage myHeap:clear()
function heap:clear()
self._heap = {}
self._size = 0
self._sort = self._sort or f_min
return self
end
--- Adds a new item in the `heap`
-- @class function
-- @tparam value item a new value to be queued in the heap
-- @treturn heap self (the calling `heap` itself, can be chained)
-- @usage
-- myHeap:push(1)
-- -- or, with chaining
-- myHeap:push(1):push(2):push(4)
function heap:push(item)
if item then
self._size = self._size + 1
self._heap[self._size] = item
percolate_up(self, self._size)
end
return self
end
--- Pops from the `heap`.
-- Removes and returns the lowest cost item (with respect to the comparison function being used) from the `heap`.
-- @class function
-- @treturn value a value previously pushed into the heap
-- @usage
-- while not myHeap:empty() do
-- local lowestValue = myHeap:pop()
-- ...
-- end
function heap:pop()
local root
if self._size > 0 then
root = self._heap[1]
self._heap[1] = self._heap[self._size]
self._heap[self._size] = nil
self._size = self._size-1
if self._size>1 then
percolate_down(self, 1)
end
end
return root
end
--- Restores the `heap` property.
-- Reorders the `heap` with respect to the comparison function being used.
-- When given argument __item__ (a value existing in the `heap`), will sort from that very item in the `heap`.
-- Otherwise, the whole `heap` will be cheacked.
-- @class function
-- @tparam[opt] value item the modified value
-- @treturn heap self (the calling `heap` itself, can be chained)
-- @usage myHeap:heapify()
function heap:heapify(item)
if self._size == 0 then return end
if item then
local i = Utils.indexOf(self._heap,item)
if i then
percolate_down(self, i)
percolate_up(self, i)
end
return
end
for i = floor(self._size/2),1,-1 do
percolate_down(self,i)
end
return self
end
return heap
end

View File

@@ -1,98 +0,0 @@
--- Heuristic functions for search algorithms.
-- A <a href="http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html">distance heuristic</a>
-- provides an *estimate of the optimal distance cost* from a given location to a target.
-- As such, it guides the pathfinder to the goal, helping it to decide which route is the best.
--
-- This script holds the definition of some built-in heuristics available through jumper.
--
-- Distance functions are internally used by the `pathfinder` to evaluate the optimal path
-- from the start location to the goal. These functions share the same prototype:
-- local function myHeuristic(nodeA, nodeB)
-- -- function body
-- end
-- Jumper features some built-in distance heuristics, namely `MANHATTAN`, `EUCLIDIAN`, `DIAGONAL`, `CARDINTCARD`.
-- You can also supply your own heuristic function, following the same template as above.
local abs = math.abs
local sqrt = math.sqrt
local sqrt2 = sqrt(2)
local max, min = math.max, math.min
local Heuristics = {}
--- Manhattan distance.
-- <br/>This heuristic is the default one being used by the `pathfinder` object.
-- <br/>Evaluates as <code>distance = |dx|+|dy|</code>
-- @class function
-- @tparam node nodeA a node
-- @tparam node nodeB another node
-- @treturn number the distance from __nodeA__ to __nodeB__
-- @usage
-- -- First method
-- pathfinder:setHeuristic('MANHATTAN')
-- -- Second method
-- local Distance = require ('jumper.core.heuristics')
-- pathfinder:setHeuristic(Distance.MANHATTAN)
function Heuristics.MANHATTAN(nodeA, nodeB)
local dx = abs(nodeA._x - nodeB._x)
local dy = abs(nodeA._y - nodeB._y)
local dz = abs(nodeA._z - nodeB._z)
return (dx + dy + dz)
end
--- Euclidian distance.
-- <br/>Evaluates as <code>distance = squareRoot(dx*dx+dy*dy)</code>
-- @class function
-- @tparam node nodeA a node
-- @tparam node nodeB another node
-- @treturn number the distance from __nodeA__ to __nodeB__
-- @usage
-- -- First method
-- pathfinder:setHeuristic('EUCLIDIAN')
-- -- Second method
-- local Distance = require ('jumper.core.heuristics')
-- pathfinder:setHeuristic(Distance.EUCLIDIAN)
function Heuristics.EUCLIDIAN(nodeA, nodeB)
local dx = nodeA._x - nodeB._x
local dy = nodeA._y - nodeB._y
local dz = nodeA._z - nodeB._z
return sqrt(dx*dx+dy*dy+dz*dz)
end
--- Diagonal distance.
-- <br/>Evaluates as <code>distance = max(|dx|, abs|dy|)</code>
-- @class function
-- @tparam node nodeA a node
-- @tparam node nodeB another node
-- @treturn number the distance from __nodeA__ to __nodeB__
-- @usage
-- -- First method
-- pathfinder:setHeuristic('DIAGONAL')
-- -- Second method
-- local Distance = require ('jumper.core.heuristics')
-- pathfinder:setHeuristic(Distance.DIAGONAL)
function Heuristics.DIAGONAL(nodeA, nodeB)
local dx = abs(nodeA._x - nodeB._x)
local dy = abs(nodeA._y - nodeB._y)
return max(dx,dy)
end
--- Cardinal/Intercardinal distance.
-- <br/>Evaluates as <code>distance = min(dx, dy)*squareRoot(2) + max(dx, dy) - min(dx, dy)</code>
-- @class function
-- @tparam node nodeA a node
-- @tparam node nodeB another node
-- @treturn number the distance from __nodeA__ to __nodeB__
-- @usage
-- -- First method
-- pathfinder:setHeuristic('CARDINTCARD')
-- -- Second method
-- local Distance = require ('jumper.core.heuristics')
-- pathfinder:setHeuristic(Distance.CARDINTCARD)
function Heuristics.CARDINTCARD(nodeA, nodeB)
local dx = abs(nodeA._x - nodeB._x)
local dy = abs(nodeA._y - nodeB._y)
return min(dx,dy) * sqrt2 + max(dx,dy) - min(dx,dy)
end
return Heuristics

View File

@@ -1,32 +0,0 @@
local addNode(self, node, nextNode, ed)
if not self._pathDB[node] then self._pathDB[node] = {} end
self._pathDB[node][ed] = (nextNode == ed and node or nextNode)
end
-- Path lookupTable
local lookupTable = {}
lookupTable.__index = lookupTable
function lookupTable:new()
local lut = {_pathDB = {}}
return setmetatable(lut, lookupTable)
end
function lookupTable:addPath(path)
local st, ed = path._nodes[1], path._nodes[#path._nodes]
for node, count in path:nodes() do
local nextNode = path._nodes[count+1]
if nextNode then addNode(self, node, nextNode, ed) end
end
end
function lookupTable:hasPath(nodeA, nodeB)
local found
found = self._pathDB[nodeA] and self._path[nodeA][nodeB]
if found then return true, true end
found = self._pathDB[nodeB] and self._path[nodeB][nodeA]
if found then return true, false end
return false
end
return lookupTable

View File

@@ -1,100 +0,0 @@
--- The Node class.
-- The `node` represents a cell (or a tile) on a collision map. Basically, for each single cell (tile)
-- in the collision map passed-in upon initialization, a `node` object will be generated
-- and then cached within the `grid`.
--
-- In the following implementation, nodes can be compared using the `<` operator. The comparison is
-- made with regards of their `f` cost. From a given node being examined, the `pathfinder` will expand the search
-- to the next neighbouring node having the lowest `f` cost. See `core.bheap` for more details.
--
if (...) then
local assert = assert
--- The `Node` class.<br/>
-- This class is callable.
-- Therefore,_ <code>Node(...)</code> _acts as a shortcut to_ <code>Node:new(...)</code>.
-- @type Node
local Node = {}
Node.__index = Node
--- Inits a new `node`
-- @class function
-- @tparam int x the x-coordinate of the node on the collision map
-- @tparam int y the y-coordinate of the node on the collision map
-- @treturn node a new `node`
-- @usage local node = Node(3,4)
function Node:new(x,y,z)
return setmetatable({_x = x, _y = y, _z = z, _clearance = {}}, Node)
end
-- Enables the use of operator '<' to compare nodes.
-- Will be used to sort a collection of nodes in a binary heap on the basis of their F-cost
function Node.__lt(A,B) return (A._f < B._f) end
--- Returns x-coordinate of a `node`
-- @class function
-- @treturn number the x-coordinate of the `node`
-- @usage local x = node:getX()
function Node:getX() return self._x end
--- Returns y-coordinate of a `node`
-- @class function
-- @treturn number the y-coordinate of the `node`
-- @usage local y = node:getY()
function Node:getY() return self._y end
function Node:getZ() return self._z end
--- Returns x and y coordinates of a `node`
-- @class function
-- @treturn number the x-coordinate of the `node`
-- @treturn number the y-coordinate of the `node`
-- @usage local x, y = node:getPos()
function Node:getPos() return self._x, self._y, self._z end
--- Returns the amount of true [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric)
-- for a given `node`
-- @class function
-- @tparam string|int|func walkable the value for walkable locations in the collision map array.
-- @treturn int the clearance of the `node`
-- @usage
-- -- Assuming walkable was 0
-- local clearance = node:getClearance(0)
function Node:getClearance(walkable)
return self._clearance[walkable]
end
--- Removes the clearance value for a given walkable.
-- @class function
-- @tparam string|int|func walkable the value for walkable locations in the collision map array.
-- @treturn node self (the calling `node` itself, can be chained)
-- @usage
-- -- Assuming walkable is defined
-- node:removeClearance(walkable)
function Node:removeClearance(walkable)
self._clearance[walkable] = nil
return self
end
--- Clears temporary cached attributes of a `node`.
-- Deletes the attributes cached within a given node after a pathfinding call.
-- This function is internally used by the search algorithms, so you should not use it explicitely.
-- @class function
-- @treturn node self (the calling `node` itself, can be chained)
-- @usage
-- local thisNode = Node(1,2)
-- thisNode:reset()
function Node:reset()
self._g, self._h, self._f = nil, nil, nil
self._opened, self._closed, self._parent = nil, nil, nil
return self
end
return setmetatable(Node,
{__call = function(self,...)
return Node:new(...)
end}
)
end

View File

@@ -1,201 +0,0 @@
--- The Path class.
-- The `path` class is a structure which represents a path (ordered set of nodes) from a start location to a goal.
-- An instance from this class would be a result of a request addressed to `Pathfinder:getPath`.
--
-- This module is internally used by the library on purpose.
-- It should normally not be used explicitely, yet it remains fully accessible.
--
if (...) then
-- Dependencies
local _PATH = (...):match('(.+)%.path$')
local Heuristic = require (_PATH .. '.heuristics')
-- Local references
local abs, max = math.abs, math.max
local t_insert, t_remove = table.insert, table.remove
--- The `Path` class.<br/>
-- This class is callable.
-- Therefore, <em><code>Path(...)</code></em> acts as a shortcut to <em><code>Path:new(...)</code></em>.
-- @type Path
local Path = {}
Path.__index = Path
--- Inits a new `path`.
-- @class function
-- @treturn path a `path`
-- @usage local p = Path()
function Path:new()
return setmetatable({_nodes = {}}, Path)
end
--- Iterates on each single `node` along a `path`. At each step of iteration,
-- returns the `node` plus a count value. Aliased as @{Path:nodes}
-- @class function
-- @treturn node a `node`
-- @treturn int the count for the number of nodes
-- @see Path:nodes
-- @usage
-- for node, count in p:iter() do
-- ...
-- end
function Path:iter()
local i,pathLen = 1,#self._nodes
return function()
if self._nodes[i] then
i = i+1
return self._nodes[i-1],i-1
end
end
end
--- Iterates on each single `node` along a `path`. At each step of iteration,
-- returns a `node` plus a count value. Alias for @{Path:iter}
-- @class function
-- @name Path:nodes
-- @treturn node a `node`
-- @treturn int the count for the number of nodes
-- @see Path:iter
-- @usage
-- for node, count in p:nodes() do
-- ...
-- end
Path.nodes = Path.iter
--- Evaluates the `path` length
-- @class function
-- @treturn number the `path` length
-- @usage local len = p:getLength()
function Path:getLength()
local len = 0
for i = 2,#self._nodes do
len = len + Heuristic.EUCLIDIAN(self._nodes[i], self._nodes[i-1])
end
return len
end
--- Counts the number of steps.
-- Returns the number of waypoints (nodes) in the current path.
-- @class function
-- @tparam node node a node to be added to the path
-- @tparam[opt] int index the index at which the node will be inserted. If omitted, the node will be appended after the last node in the path.
-- @treturn path self (the calling `path` itself, can be chained)
-- @usage local nSteps = p:countSteps()
function Path:addNode(node, index)
index = index or #self._nodes+1
t_insert(self._nodes, index, node)
return self
end
--- `Path` filling modifier. Interpolates between non contiguous nodes along a `path`
-- to build a fully continuous `path`. This maybe useful when using search algorithms such as Jump Point Search.
-- Does the opposite of @{Path:filter}
-- @class function
-- @treturn path self (the calling `path` itself, can be chained)
-- @see Path:filter
-- @usage p:fill()
function Path:fill()
local i = 2
local xi,yi,dx,dy
local N = #self._nodes
local incrX, incrY
while true do
xi,yi = self._nodes[i]._x,self._nodes[i]._y
dx,dy = xi-self._nodes[i-1]._x,yi-self._nodes[i-1]._y
if (abs(dx) > 1 or abs(dy) > 1) then
incrX = dx/max(abs(dx),1)
incrY = dy/max(abs(dy),1)
t_insert(self._nodes, i, self._grid:getNodeAt(self._nodes[i-1]._x + incrX, self._nodes[i-1]._y +incrY))
N = N+1
else i=i+1
end
if i>N then break end
end
return self
end
--- `Path` compression modifier. Given a `path`, eliminates useless nodes to return a lighter `path`
-- consisting of straight moves. Does the opposite of @{Path:fill}
-- @class function
-- @treturn path self (the calling `path` itself, can be chained)
-- @see Path:fill
-- @usage p:filter()
function Path:filter()
local i = 2
local xi,yi,dx,dy, olddx, olddy
xi,yi = self._nodes[i]._x, self._nodes[i]._y
dx, dy = xi - self._nodes[i-1]._x, yi-self._nodes[i-1]._y
while true do
olddx, olddy = dx, dy
if self._nodes[i+1] then
i = i+1
xi, yi = self._nodes[i]._x, self._nodes[i]._y
dx, dy = xi - self._nodes[i-1]._x, yi - self._nodes[i-1]._y
if olddx == dx and olddy == dy then
t_remove(self._nodes, i-1)
i = i - 1
end
else break end
end
return self
end
--- Clones a `path`.
-- @class function
-- @treturn path a `path`
-- @usage local p = path:clone()
function Path:clone()
local p = Path:new()
for node in self:nodes() do p:addNode(node) end
return p
end
--- Checks if a `path` is equal to another. It also supports *filtered paths* (see @{Path:filter}).
-- @class function
-- @tparam path p2 a path
-- @treturn boolean a boolean
-- @usage print(myPath:isEqualTo(anotherPath))
function Path:isEqualTo(p2)
local p1 = self:clone():filter()
local p2 = p2:clone():filter()
for node, count in p1:nodes() do
if not p2._nodes[count] then return false end
local n = p2._nodes[count]
if n._x~=node._x or n._y~=node._y then return false end
end
return true
end
--- Reverses a `path`.
-- @class function
-- @treturn path self (the calling `path` itself, can be chained)
-- @usage myPath:reverse()
function Path:reverse()
local _nodes = {}
for i = #self._nodes,1,-1 do
_nodes[#_nodes+1] = self._nodes[i]
end
self._nodes = _nodes
return self
end
--- Appends a given `path` to self.
-- @class function
-- @tparam path p a path
-- @treturn path self (the calling `path` itself, can be chained)
-- @usage myPath:append(anotherPath)
function Path:append(p)
for node in p:nodes() do self:addNode(node) end
return self
end
return setmetatable(Path,
{__call = function(self,...)
return Path:new(...)
end
})
end

View File

@@ -1,168 +0,0 @@
-- Various utilities for Jumper top-level modules
if (...) then
-- Dependencies
local _PATH = (...):gsub('%.utils$','')
local Path = require (_PATH .. '.path')
local Node = require (_PATH .. '.node')
-- Local references
local pairs = pairs
local type = type
local t_insert = table.insert
local assert = assert
local coroutine = coroutine
-- Raw array items count
local function arraySize(t)
local count = 0
for k,v in pairs(t) do
count = count+1
end
return count
end
-- Parses a string map and builds an array map
local function stringMapToArray(str)
local map = {}
local w, h
for line in str:gmatch('[^\n\r]+') do
if line then
w = not w and #line or w
assert(#line == w, 'Error parsing map, rows must have the same size!')
h = (h or 0) + 1
map[h] = {}
for char in line:gmatch('.') do
map[h][#map[h]+1] = char
end
end
end
return map
end
-- Collects and returns the keys of a given array
local function getKeys(t)
local keys = {}
for k,v in pairs(t) do keys[#keys+1] = k end
return keys
end
-- Calculates the bounds of a 2d array
local function getArrayBounds(map)
local min_x, max_x
local min_y, max_y
for y in pairs(map) do
min_y = not min_y and y or (y<min_y and y or min_y)
max_y = not max_y and y or (y>max_y and y or max_y)
for x in pairs(map[y]) do
min_x = not min_x and x or (x<min_x and x or min_x)
max_x = not max_x and x or (x>max_x and x or max_x)
end
end
return min_x,max_x,min_y,max_y
end
-- Converts an array to a set of nodes
local function arrayToNodes(map)
local min_x, max_x
local min_y, max_y
local min_z, max_z
local nodes = {}
for y in pairs(map) do
min_y = not min_y and y or (y<min_y and y or min_y)
max_y = not max_y and y or (y>max_y and y or max_y)
nodes[y] = {}
for x in pairs(map[y]) do
min_x = not min_x and x or (x<min_x and x or min_x)
max_x = not max_x and x or (x>max_x and x or max_x)
nodes[y][x] = {}
for z in pairs(map[y][x]) do
min_z = not min_z and z or (z<min_z and z or min_z)
max_z = not max_z and z or (z>max_z and z or max_z)
nodes[y][x][z] = Node:new(x,y,z)
end
end
end
return nodes,
(min_x or 0), (max_x or 0),
(min_y or 0), (max_y or 0),
(min_z or 0), (max_z or 0)
end
-- Iterator, wrapped within a coroutine
-- Iterates around a given position following the outline of a square
local function around()
local iterf = function(x0, y0, z0, s)
local x, y, z = x0-s, y0-s, z0-s
coroutine.yield(x, y, z)
repeat
x = x + 1
coroutine.yield(x,y,z)
until x == x0+s
repeat
y = y + 1
coroutine.yield(x,y,z)
until y == y0 + s
repeat
z = z + 1
coroutine.yield(x,y,z)
until z == z0 + s
repeat
x = x - 1
coroutine.yield(x, y,z)
until x == x0-s
repeat
y = y - 1
coroutine.yield(x,y,z)
until y == y0-s+1
repeat
z = z - 1
coroutine.yield(x,y,z)
until z == z0-s+1
end
return coroutine.create(iterf)
end
-- Extract a path from a given start/end position
local function traceBackPath(finder, node, startNode)
local path = Path:new()
path._grid = finder._grid
while true do
if node._parent then
t_insert(path._nodes,1,node)
node = node._parent
else
t_insert(path._nodes,1,startNode)
return path
end
end
end
-- Lookup for value in a table
local indexOf = function(t,v)
for i = 1,#t do
if t[i] == v then return i end
end
return nil
end
-- Is i out of range
local function outOfRange(i,low,up)
return (i< low or i > up)
end
return {
arraySize = arraySize,
getKeys = getKeys,
indexOf = indexOf,
outOfRange = outOfRange,
getArrayBounds = getArrayBounds,
arrayToNodes = arrayToNodes,
strToMap = stringMapToArray,
around = around,
drAround = drAround,
traceBackPath = traceBackPath
}
end

View File

@@ -1,429 +0,0 @@
--- The Grid class.
-- Implementation of the `grid` class.
-- The `grid` is a implicit graph which represents the 2D
-- world map layout on which the `pathfinder` object will run.
-- During a search, the `pathfinder` object needs to save some critical values. These values are cached within each `node`
-- object, and the whole set of nodes are tight inside the `grid` object itself.
if (...) then
-- Dependencies
local _PATH = (...):gsub('%.grid$','')
-- Local references
local Utils = require (_PATH .. '.core.utils')
local Assert = require (_PATH .. '.core.assert')
local Node = require (_PATH .. '.core.node')
-- Local references
local pairs = pairs
local assert = assert
local next = next
local setmetatable = setmetatable
local floor = math.floor
local coroutine = coroutine
-- Offsets for straights moves
local straightOffsets = {
{x = 1, y = 0, z = 0} --[[W]], {x = -1, y = 0, z = 0}, --[[E]]
{x = 0, y = 1, z = 0} --[[S]], {x = 0, y = -1, z = 0}, --[[N]]
{x = 0, y = 0, z = 1} --[[U]], {x = 0, y = -0, z = -1}, --[[D]]
}
-- Offsets for diagonal moves
local diagonalOffsets = {
{x = -1, y = -1} --[[NW]], {x = 1, y = -1}, --[[NE]]
{x = -1, y = 1} --[[SW]], {x = 1, y = 1}, --[[SE]]
}
--- The `Grid` class.<br/>
-- This class is callable.
-- Therefore,_ <code>Grid(...)</code> _acts as a shortcut to_ <code>Grid:new(...)</code>.
-- @type Grid
local Grid = {}
Grid.__index = Grid
-- Specialized grids
local PreProcessGrid = setmetatable({},Grid)
local PostProcessGrid = setmetatable({},Grid)
PreProcessGrid.__index = PreProcessGrid
PostProcessGrid.__index = PostProcessGrid
PreProcessGrid.__call = function (self,x,y,z)
return self:getNodeAt(x,y,z)
end
PostProcessGrid.__call = function (self,x,y,z,create)
if create then return self:getNodeAt(x,y,z) end
return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z]
end
--- Inits a new `grid`
-- @class function
-- @tparam table|string map A collision map - (2D array) with consecutive indices (starting at 0 or 1)
-- or a `string` with line-break chars (<code>\n</code> or <code>\r</code>) as row delimiters.
-- @tparam[opt] bool cacheNodeAtRuntime When __true__, returns an empty `grid` instance, so that
-- later on, indexing a non-cached `node` will cause it to be created and cache within the `grid` on purpose (i.e, when needed).
-- This is a __memory-safe__ option, in case your dealing with some tight memory constraints.
-- Defaults to __false__ when omitted.
-- @treturn grid a new `grid` instance
-- @usage
-- -- A simple 3x3 grid
-- local myGrid = Grid:new({{0,0,0},{0,0,0},{0,0,0}})
--
-- -- A memory-safe 3x3 grid
-- myGrid = Grid('000\n000\n000', true)
function Grid:new(map, cacheNodeAtRuntime)
if type(map) == 'string' then
assert(Assert.isStrMap(map), 'Wrong argument #1. Not a valid string map')
map = Utils.strToMap(map)
end
--assert(Assert.isMap(map),('Bad argument #1. Not a valid map'))
assert(Assert.isBool(cacheNodeAtRuntime) or Assert.isNil(cacheNodeAtRuntime),
('Bad argument #2. Expected \'boolean\', got %s.'):format(type(cacheNodeAtRuntime)))
if cacheNodeAtRuntime then
return PostProcessGrid:new(map,walkable)
end
return PreProcessGrid:new(map,walkable)
end
--- Checks if `node` at [x,y] is __walkable__.
-- Will check if `node` at location [x,y] both *exists* on the collision map and *is walkable*
-- @class function
-- @tparam int x the x-location of the node
-- @tparam int y the y-location of the node
-- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}).
-- Defaults to __false__ when omitted.
-- If this parameter is a function, it should be prototyped as __f(value)__ and return a `boolean`:
-- __true__ when value matches a __walkable__ `node`, __false__ otherwise. If this parameter is not given
-- while location [x,y] __is valid__, this actual function returns __true__.
-- @tparam[optchain] int clearance the amount of clearance needed. Defaults to 1 (normal clearance) when not given.
-- @treturn bool __true__ if `node` exists and is __walkable__, __false__ otherwise
-- @usage
-- -- Always true
-- print(myGrid:isWalkableAt(2,3))
--
-- -- True if node at [2,3] collision map value is 0
-- print(myGrid:isWalkableAt(2,3,0))
--
-- -- True if node at [2,3] collision map value is 0 and has a clearance higher or equal to 2
-- print(myGrid:isWalkableAt(2,3,0,2))
--
function Grid:isWalkableAt(x, y, z, walkable, clearance)
local nodeValue = self._map[y] and self._map[y][x] and self._map[y][x][z]
if nodeValue then
if not walkable then return true end
else
return false
end
local hasEnoughClearance = not clearance and true or false
if not hasEnoughClearance then
if not self._isAnnotated[walkable] then return false end
local node = self:getNodeAt(x,y,z)
local nodeClearance = node:getClearance(walkable)
hasEnoughClearance = (nodeClearance >= clearance)
end
if self._eval then
return walkable(nodeValue) and hasEnoughClearance
end
return ((nodeValue == walkable) and hasEnoughClearance)
end
--- Returns the `grid` width.
-- @class function
-- @treturn int the `grid` width
-- @usage print(myGrid:getWidth())
function Grid:getWidth()
return self._width
end
--- Returns the `grid` height.
-- @class function
-- @treturn int the `grid` height
-- @usage print(myGrid:getHeight())
function Grid:getHeight()
return self._height
end
--- Returns the collision map.
-- @class function
-- @treturn map the collision map (see @{Grid:new})
-- @usage local map = myGrid:getMap()
function Grid:getMap()
return self._map
end
--- Returns the set of nodes.
-- @class function
-- @treturn {{node,...},...} an array of nodes
-- @usage local nodes = myGrid:getNodes()
function Grid:getNodes()
return self._nodes
end
--- Returns the `grid` bounds. Returned values corresponds to the upper-left
-- and lower-right coordinates (in tile units) of the actual `grid` instance.
-- @class function
-- @treturn int the upper-left corner x-coordinate
-- @treturn int the upper-left corner y-coordinate
-- @treturn int the lower-right corner x-coordinate
-- @treturn int the lower-right corner y-coordinate
-- @usage local left_x, left_y, right_x, right_y = myGrid:getBounds()
function Grid:getBounds()
return self._min_x, self._min_y, self._min_z, self._max_x, self._max_y, self._max_z
end
--- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`.
-- @class function
-- @tparam node node a given `node`
-- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}).
-- Defaults to __false__ when omitted.
-- @tparam[optchain] bool allowDiagonal when __true__, allows adjacent nodes are included (8-neighbours).
-- Defaults to __false__ when omitted.
-- @tparam[optchain] bool tunnel When __true__, allows the `pathfinder` to tunnel through walls when heading diagonally.
-- @tparam[optchain] int clearance When given, will prune for the neighbours set all nodes having a clearance value lower than the passed-in value
-- Defaults to __false__ when omitted.
-- @treturn {node,...} an array of nodes neighbouring a given node
-- @usage
-- local aNode = myGrid:getNodeAt(5,6)
-- local neighbours = myGrid:getNeighbours(aNode, 0, true)
function Grid:getNeighbours(node, walkable, allowDiagonal, tunnel, clearance)
local neighbours = {}
for i = 1,#straightOffsets do
local n = self:getNodeAt(
node._x + straightOffsets[i].x,
node._y + straightOffsets[i].y,
node._z + straightOffsets[i].z
)
if n and self:isWalkableAt(n._x, n._y, n._z, walkable, clearance) then
neighbours[#neighbours+1] = n
end
end
if not allowDiagonal then return neighbours end
tunnel = not not tunnel
for i = 1,#diagonalOffsets do
local n = self:getNodeAt(
node._x + diagonalOffsets[i].x,
node._y + diagonalOffsets[i].y
)
if n and self:isWalkableAt(n._x, n._y, walkable, clearance) then
if tunnel then
neighbours[#neighbours+1] = n
else
local skipThisNode = false
local n1 = self:getNodeAt(node._x+diagonalOffsets[i].x, node._y)
local n2 = self:getNodeAt(node._x, node._y+diagonalOffsets[i].y)
if ((n1 and n2) and not self:isWalkableAt(n1._x, n1._y, walkable, clearance) and not self:isWalkableAt(n2._x, n2._y, walkable, clearance)) then
skipThisNode = true
end
if not skipThisNode then neighbours[#neighbours+1] = n end
end
end
end
return neighbours
end
--- Grid iterator. Iterates on every single node
-- in the `grid`. Passing __lx, ly, ex, ey__ arguments will iterate
-- only on nodes inside the bounding-rectangle delimited by those given coordinates.
-- @class function
-- @tparam[opt] int lx the leftmost x-coordinate of the rectangle. Default to the `grid` leftmost x-coordinate (see @{Grid:getBounds}).
-- @tparam[optchain] int ly the topmost y-coordinate of the rectangle. Default to the `grid` topmost y-coordinate (see @{Grid:getBounds}).
-- @tparam[optchain] int ex the rightmost x-coordinate of the rectangle. Default to the `grid` rightmost x-coordinate (see @{Grid:getBounds}).
-- @tparam[optchain] int ey the bottom-most y-coordinate of the rectangle. Default to the `grid` bottom-most y-coordinate (see @{Grid:getBounds}).
-- @treturn node a `node` on the collision map, upon each iteration step
-- @treturn int the iteration count
-- @usage
-- for node, count in myGrid:iter() do
-- print(node:getX(), node:getY(), count)
-- end
function Grid:iter(lx,ly,lz,ex,ey,ez)
local min_x = lx or self._min_x
local min_y = ly or self._min_y
local min_z = lz or self._min_z
local max_x = ex or self._max_x
local max_y = ey or self._max_y
local max_z = ez or self._max_z
local x, y, z
z = min_z
return function()
x = not x and min_x or x+1
if x > max_x then
x = min_x
y = y+1
end
y = not y and min_y or y+1
if y > max_y then
y = min_y
z = z+1
end
if z > max_z then
z = nil
end
return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z] or self:getNodeAt(x,y,z)
end
end
--- Grid iterator. Iterates on each node along the outline (border) of a squared area
-- centered on the given node.
-- @tparam node node a given `node`
-- @tparam[opt] int radius the area radius (half-length). Defaults to __1__ when not given.
-- @treturn node a `node` at each iteration step
-- @usage
-- for node in myGrid:around(node, 2) do
-- ...
-- end
function Grid:around(node, radius)
local x, y, z = node._x, node._y, node._z
radius = radius or 1
local _around = Utils.around()
local _nodes = {}
repeat
local state, x, y, z = coroutine.resume(_around,x,y,z,radius)
local nodeAt = state and self:getNodeAt(x, y, z)
if nodeAt then _nodes[#_nodes+1] = nodeAt end
until (not state)
local _i = 0
return function()
_i = _i+1
return _nodes[_i]
end
end
--- Each transformation. Calls the given function on each `node` in the `grid`,
-- passing the `node` as the first argument to function __f__.
-- @class function
-- @tparam func f a function prototyped as __f(node,...)__
-- @tparam[opt] vararg ... args to be passed to function __f__
-- @treturn grid self (the calling `grid` itself, can be chained)
-- @usage
-- local function printNode(node)
-- print(node:getX(), node:getY())
-- end
-- myGrid:each(printNode)
function Grid:each(f,...)
for node in self:iter() do f(node,...) end
return self
end
--- Each (in range) transformation. Calls a function on each `node` in the range of a rectangle of cells,
-- passing the `node` as the first argument to function __f__.
-- @class function
-- @tparam int lx the leftmost x-coordinate coordinate of the rectangle
-- @tparam int ly the topmost y-coordinate of the rectangle
-- @tparam int ex the rightmost x-coordinate of the rectangle
-- @tparam int ey the bottom-most y-coordinate of the rectangle
-- @tparam func f a function prototyped as __f(node,...)__
-- @tparam[opt] vararg ... args to be passed to function __f__
-- @treturn grid self (the calling `grid` itself, can be chained)
-- @usage
-- local function printNode(node)
-- print(node:getX(), node:getY())
-- end
-- myGrid:eachRange(1,1,8,8,printNode)
function Grid:eachRange(lx,ly,ex,ey,f,...)
for node in self:iter(lx,ly,ex,ey) do f(node,...) end
return self
end
--- Map transformation.
-- Calls function __f(node,...)__ on each `node` in a given range, passing the `node` as the first arg to function __f__ and replaces
-- it with the returned value. Therefore, the function should return a `node`.
-- @class function
-- @tparam func f a function prototyped as __f(node,...)__
-- @tparam[opt] vararg ... args to be passed to function __f__
-- @treturn grid self (the calling `grid` itself, can be chained)
-- @usage
-- local function nothing(node)
-- return node
-- end
-- myGrid:imap(nothing)
function Grid:imap(f,...)
for node in self:iter() do
node = f(node,...)
end
return self
end
--- Map in range transformation.
-- Calls function __f(node,...)__ on each `node` in a rectangle range, passing the `node` as the first argument to the function and replaces
-- it with the returned value. Therefore, the function should return a `node`.
-- @class function
-- @tparam int lx the leftmost x-coordinate coordinate of the rectangle
-- @tparam int ly the topmost y-coordinate of the rectangle
-- @tparam int ex the rightmost x-coordinate of the rectangle
-- @tparam int ey the bottom-most y-coordinate of the rectangle
-- @tparam func f a function prototyped as __f(node,...)__
-- @tparam[opt] vararg ... args to be passed to function __f__
-- @treturn grid self (the calling `grid` itself, can be chained)
-- @usage
-- local function nothing(node)
-- return node
-- end
-- myGrid:imap(1,1,6,6,nothing)
function Grid:imapRange(lx,ly,ex,ey,f,...)
for node in self:iter(lx,ly,ex,ey) do
node = f(node,...)
end
return self
end
-- Specialized grids
-- Inits a preprocessed grid
function PreProcessGrid:new(map)
local newGrid = {}
newGrid._map = map
newGrid._nodes, newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y, newGrid._min_z, newGrid._max_z = Utils.arrayToNodes(newGrid._map)
newGrid._width = (newGrid._max_x-newGrid._min_x)+1
newGrid._height = (newGrid._max_y-newGrid._min_y)+1
newGrid._length = (newGrid._max_z-newGrid._min_z)+1
newGrid._isAnnotated = {}
return setmetatable(newGrid,PreProcessGrid)
end
-- Inits a postprocessed grid
function PostProcessGrid:new(map)
local newGrid = {}
newGrid._map = map
newGrid._nodes = {}
newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y = Utils.getArrayBounds(newGrid._map)
newGrid._width = (newGrid._max_x-newGrid._min_x)+1
newGrid._height = (newGrid._max_y-newGrid._min_y)+1
newGrid._isAnnotated = {}
return setmetatable(newGrid,PostProcessGrid)
end
--- Returns the `node` at location [x,y].
-- @class function
-- @name Grid:getNodeAt
-- @tparam int x the x-coordinate coordinate
-- @tparam int y the y-coordinate coordinate
-- @treturn node a `node`
-- @usage local aNode = myGrid:getNodeAt(2,2)
-- Gets the node at location <x,y> on a preprocessed grid
function PreProcessGrid:getNodeAt(x,y,z)
return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z] or nil
end
-- Gets the node at location <x,y> on a postprocessed grid
function PostProcessGrid:getNodeAt(x,y,z)
if not x or not y or not z then return end
if Utils.outOfRange(x,self._min_x,self._max_x) then return end
if Utils.outOfRange(y,self._min_y,self._max_y) then return end
if Utils.outOfRange(z,self._min_z,self._max_z) then return end
if not self._nodes[y] then self._nodes[y] = {} end
if not self._nodes[y][x] then self._nodes[y][x] = {} end
if not self._nodes[y][x][z] then self._nodes[y][x][z] = Node:new(x,y,z) end
return self._nodes[y][x][z]
end
return setmetatable(Grid,{
__call = function(self,...)
return self:new(...)
end
})
end

View File

@@ -1,412 +0,0 @@
--[[
The following License applies to all files within the jumper directory.
Note that this is only a partial copy of the full jumper code base. Also,
the code was modified to support 3D maps.
--]]
--[[
This work is under MIT-LICENSE
Copyright (c) 2012-2013 Roland Yonaba.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "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, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--]]
--- The Pathfinder class
--
-- Implementation of the `pathfinder` class.
local _VERSION = ""
local _RELEASEDATE = ""
if (...) then
-- Dependencies
local _PATH = (...):gsub('%.pathfinder$','')
local Utils = require (_PATH .. '.core.utils')
local Assert = require (_PATH .. '.core.assert')
local Heap = require (_PATH .. '.core.bheap')
local Heuristic = require (_PATH .. '.core.heuristics')
local Grid = require (_PATH .. '.grid')
local Path = require (_PATH .. '.core.path')
-- Internalization
local t_insert, t_remove = table.insert, table.remove
local floor = math.floor
local pairs = pairs
local assert = assert
local type = type
local setmetatable, getmetatable = setmetatable, getmetatable
--- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper.
--
-- <li>[A*](http://en.wikipedia.org/wiki/A*_search_algorithm)</li>
-- <li>[Dijkstra](http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)</li>
-- <li>[Theta Astar](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/)</li>
-- <li>[BFS](http://en.wikipedia.org/wiki/Breadth-first_search)</li>
-- <li>[DFS](http://en.wikipedia.org/wiki/Depth-first_search)</li>
-- <li>[JPS](http://harablog.wordpress.com/2011/09/07/jump-point-search/)</li>
-- @finder Finders
-- @see Pathfinder:getFinders
local Finders = {
['ASTAR'] = require (_PATH .. '.search.astar'),
-- ['DIJKSTRA'] = require (_PATH .. '.search.dijkstra'),
-- ['THETASTAR'] = require (_PATH .. '.search.thetastar'),
['BFS'] = require (_PATH .. '.search.bfs'),
-- ['DFS'] = require (_PATH .. '.search.dfs'),
-- ['JPS'] = require (_PATH .. '.search.jps')
}
-- Will keep track of all nodes expanded during the search
-- to easily reset their properties for the next pathfinding call
local toClear = {}
--- Search modes. Refers to the search modes. In ORTHOGONAL mode, 4-directions are only possible when moving,
-- including North, East, West, South. In DIAGONAL mode, 8-directions are possible when moving,
-- including North, East, West, South and adjacent directions.
--
-- <li>ORTHOGONAL</li>
-- <li>DIAGONAL</li>
-- @mode Modes
-- @see Pathfinder:getModes
local searchModes = {['DIAGONAL'] = true, ['ORTHOGONAL'] = true}
-- Performs a traceback from the goal node to the start node
-- Only happens when the path was found
--- The `Pathfinder` class.<br/>
-- This class is callable.
-- Therefore,_ <code>Pathfinder(...)</code> _acts as a shortcut to_ <code>Pathfinder:new(...)</code>.
-- @type Pathfinder
local Pathfinder = {}
Pathfinder.__index = Pathfinder
--- Inits a new `pathfinder`
-- @class function
-- @tparam grid grid a `grid`
-- @tparam[opt] string finderName the name of the `Finder` (search algorithm) to be used for search.
-- Defaults to `ASTAR` when not given (see @{Pathfinder:getFinders}).
-- @tparam[optchain] string|int|func walkable the value for __walkable__ nodes.
-- If this parameter is a function, it should be prototyped as __f(value)__, returning a boolean:
-- __true__ when value matches a __walkable__ `node`, __false__ otherwise.
-- @treturn pathfinder a new `pathfinder` instance
-- @usage
-- -- Example one
-- local finder = Pathfinder:new(myGrid, 'ASTAR', 0)
--
-- -- Example two
-- local function walkable(value)
-- return value > 0
-- end
-- local finder = Pathfinder(myGrid, 'JPS', walkable)
function Pathfinder:new(grid, finderName, walkable)
local newPathfinder = {}
setmetatable(newPathfinder, Pathfinder)
--newPathfinder:setGrid(grid)
newPathfinder:setFinder(finderName)
--newPathfinder:setWalkable(walkable)
newPathfinder:setMode('DIAGONAL')
newPathfinder:setHeuristic('MANHATTAN')
newPathfinder:setTunnelling(false)
return newPathfinder
end
--- Evaluates [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric)
-- for the whole `grid`. It should be called only once, unless the collision map or the
-- __walkable__ attribute changes. The clearance values are calculated and cached within the grid nodes.
-- @class function
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage myFinder:annotateGrid()
function Pathfinder:annotateGrid()
assert(self._walkable, 'Finder must implement a walkable value')
for x=self._grid._max_x,self._grid._min_x,-1 do
for y=self._grid._max_y,self._grid._min_y,-1 do
local node = self._grid:getNodeAt(x,y)
if self._grid:isWalkableAt(x,y,self._walkable) then
local nr = self._grid:getNodeAt(node._x+1, node._y)
local nrd = self._grid:getNodeAt(node._x+1, node._y+1)
local nd = self._grid:getNodeAt(node._x, node._y+1)
if nr and nrd and nd then
local m = nrd._clearance[self._walkable] or 0
m = (nd._clearance[self._walkable] or 0)<m and (nd._clearance[self._walkable] or 0) or m
m = (nr._clearance[self._walkable] or 0)<m and (nr._clearance[self._walkable] or 0) or m
node._clearance[self._walkable] = m+1
else
node._clearance[self._walkable] = 1
end
else node._clearance[self._walkable] = 0
end
end
end
self._grid._isAnnotated[self._walkable] = true
return self
end
--- Removes [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric)values.
-- Clears cached clearance values for the current __walkable__.
-- @class function
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage myFinder:clearAnnotations()
function Pathfinder:clearAnnotations()
assert(self._walkable, 'Finder must implement a walkable value')
for node in self._grid:iter() do
node:removeClearance(self._walkable)
end
self._grid._isAnnotated[self._walkable] = false
return self
end
--- Sets the `grid`. Defines the given `grid` as the one on which the `pathfinder` will perform the search.
-- @class function
-- @tparam grid grid a `grid`
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage myFinder:setGrid(myGrid)
function Pathfinder:setGrid(grid)
assert(Assert.inherits(grid, Grid), 'Wrong argument #1. Expected a \'grid\' object')
self._grid = grid
self._grid._eval = self._walkable and type(self._walkable) == 'function'
return self
end
--- Returns the `grid`. This is a reference to the actual `grid` used by the `pathfinder`.
-- @class function
-- @treturn grid the `grid`
-- @usage local myGrid = myFinder:getGrid()
function Pathfinder:getGrid()
return self._grid
end
--- Sets the __walkable__ value or function.
-- @class function
-- @tparam string|int|func walkable the value for walkable nodes.
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage
-- -- Value '0' is walkable
-- myFinder:setWalkable(0)
--
-- -- Any value greater than 0 is walkable
-- myFinder:setWalkable(function(n)
-- return n>0
-- end
function Pathfinder:setWalkable(walkable)
assert(Assert.matchType(walkable,'stringintfunctionnil'),
('Wrong argument #1. Expected \'string\', \'number\' or \'function\', got %s.'):format(type(walkable)))
self._walkable = walkable
self._grid._eval = type(self._walkable) == 'function'
return self
end
--- Gets the __walkable__ value or function.
-- @class function
-- @treturn string|int|func the `walkable` value or function
-- @usage local walkable = myFinder:getWalkable()
function Pathfinder:getWalkable()
return self._walkable
end
--- Defines the `finder`. It refers to the search algorithm used by the `pathfinder`.
-- Default finder is `ASTAR`. Use @{Pathfinder:getFinders} to get the list of available finders.
-- @class function
-- @tparam string finderName the name of the `finder` to be used for further searches.
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage
-- --To use Breadth-First-Search
-- myFinder:setFinder('BFS')
-- @see Pathfinder:getFinders
function Pathfinder:setFinder(finderName)
if not finderName then
if not self._finder then
finderName = 'ASTAR'
else return
end
end
assert(Finders[finderName],'Not a valid finder name!')
self._finder = finderName
return self
end
--- Returns the name of the `finder` being used.
-- @class function
-- @treturn string the name of the `finder` to be used for further searches.
-- @usage local finderName = myFinder:getFinder()
function Pathfinder:getFinder()
return self._finder
end
--- Returns the list of all available finders names.
-- @class function
-- @treturn {string,...} array of built-in finders names.
-- @usage
-- local finders = myFinder:getFinders()
-- for i, finderName in ipairs(finders) do
-- print(i, finderName)
-- end
function Pathfinder:getFinders()
return Utils.getKeys(Finders)
end
--- Sets a heuristic. This is a function internally used by the `pathfinder` to find the optimal path during a search.
-- Use @{Pathfinder:getHeuristics} to get the list of all available `heuristics`. One can also define
-- his own `heuristic` function.
-- @class function
-- @tparam func|string heuristic `heuristic` function, prototyped as __f(dx,dy)__ or as a `string`.
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @see Pathfinder:getHeuristics
-- @see core.heuristics
-- @usage myFinder:setHeuristic('MANHATTAN')
function Pathfinder:setHeuristic(heuristic)
assert(Heuristic[heuristic] or (type(heuristic) == 'function'),'Not a valid heuristic!')
self._heuristic = Heuristic[heuristic] or heuristic
return self
end
--- Returns the `heuristic` used. Returns the function itself.
-- @class function
-- @treturn func the `heuristic` function being used by the `pathfinder`
-- @see core.heuristics
-- @usage local h = myFinder:getHeuristic()
function Pathfinder:getHeuristic()
return self._heuristic
end
--- Gets the list of all available `heuristics`.
-- @class function
-- @treturn {string,...} array of heuristic names.
-- @see core.heuristics
-- @usage
-- local heur = myFinder:getHeuristic()
-- for i, heuristicName in ipairs(heur) do
-- ...
-- end
function Pathfinder:getHeuristics()
return Utils.getKeys(Heuristic)
end
--- Defines the search `mode`.
-- The default search mode is the `DIAGONAL` mode, which implies 8-possible directions when moving (north, south, east, west and diagonals).
-- In `ORTHOGONAL` mode, only 4-directions are allowed (north, south, east and west).
-- Use @{Pathfinder:getModes} to get the list of all available search modes.
-- @class function
-- @tparam string mode the new search `mode`.
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @see Pathfinder:getModes
-- @see Modes
-- @usage myFinder:setMode('ORTHOGONAL')
function Pathfinder:setMode(mode)
assert(searchModes[mode],'Invalid mode')
self._allowDiagonal = (mode == 'DIAGONAL')
return self
end
--- Returns the search mode.
-- @class function
-- @treturn string the current search mode
-- @see Modes
-- @usage local mode = myFinder:getMode()
function Pathfinder:getMode()
return (self._allowDiagonal and 'DIAGONAL' or 'ORTHOGONAL')
end
--- Gets the list of all available search modes.
-- @class function
-- @treturn {string,...} array of search modes.
-- @see Modes
-- @usage local modes = myFinder:getModes()
-- for modeName in ipairs(modes) do
-- ...
-- end
function Pathfinder:getModes()
return Utils.getKeys(searchModes)
end
--- Enables tunnelling. Defines the ability for the `pathfinder` to tunnel through walls when heading diagonally.
-- This feature __is not compatible__ with Jump Point Search algorithm (i.e. enabling it will not affect Jump Point Search)
-- @class function
-- @tparam bool bool a boolean
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage myFinder:setTunnelling(true)
function Pathfinder:setTunnelling(bool)
assert(Assert.isBool(bool), ('Wrong argument #1. Expected boolean, got %s'):format(type(bool)))
self._tunnel = bool
return self
end
--- Returns tunnelling feature state.
-- @class function
-- @treturn bool tunnelling feature actual state
-- @usage local isTunnellingEnabled = myFinder:getTunnelling()
function Pathfinder:getTunnelling()
return self._tunnel
end
--- Calculates a `path`. Returns the `path` from location __[startX, startY]__ to location __[endX, endY]__.
-- Both locations must exist on the collision map. The starting location can be unwalkable.
-- @class function
-- @tparam int startX the x-coordinate for the starting location
-- @tparam int startY the y-coordinate for the starting location
-- @tparam int endX the x-coordinate for the goal location
-- @tparam int endY the y-coordinate for the goal location
-- @tparam int clearance the amount of clearance (i.e the pathing agent size) to consider
-- @treturn path a path (array of nodes) when found, otherwise nil
-- @usage local path = myFinder:getPath(1,1,5,5)
function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh, clearance)
self:reset()
local startNode = self._grid:getNodeAt(startX, startY, startZ)
local endNode = self._grid:getNodeAt(endX, endY, endZ)
if not startNode or not endNode then
return nil
end
startNode._heading = ih
endNode._heading = oh
assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ))
assert(endNode and self._grid:isWalkableAt(endX, endY, endZ),
('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ))
local _endNode = Finders[self._finder](self, startNode, endNode, clearance, toClear)
if _endNode then
return Utils.traceBackPath(self, _endNode, startNode)
end
return nil
end
--- Resets the `pathfinder`. This function is called internally between successive pathfinding calls, so you should not
-- use it explicitely, unless under specific circumstances.
-- @class function
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage local path, len = myFinder:getPath(1,1,5,5)
function Pathfinder:reset()
for node in pairs(toClear) do node:reset() end
toClear = {}
return self
end
-- Returns Pathfinder class
Pathfinder._VERSION = _VERSION
Pathfinder._RELEASEDATE = _RELEASEDATE
return setmetatable(Pathfinder,{
__call = function(self,...)
return self:new(...)
end
})
end

View File

@@ -1,88 +0,0 @@
-- Astar algorithm
-- This actual implementation of A-star is based on
-- [Nash A. & al. pseudocode](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/)
if (...) then
-- Internalization
local ipairs = ipairs
local huge = math.huge
-- Dependancies
local _PATH = (...):match('(.+)%.search.astar$')
local Heuristics = require (_PATH .. '.core.heuristics')
local Heap = require (_PATH.. '.core.bheap')
-- Updates G-cost
local function computeCost(node, neighbour, finder, clearance, heuristic)
local mCost, heading = heuristic(neighbour, node) -- Heuristics.EUCLIDIAN(neighbour, node)
if node._g + mCost < neighbour._g then
neighbour._parent = node
neighbour._g = node._g + mCost
neighbour._heading = heading
end
end
-- Updates vertex node-neighbour
local function updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval)
local oldG = neighbour._g
local cmpCost = overrideCostEval or computeCost
cmpCost(node, neighbour, finder, clearance, heuristic)
if neighbour._g < oldG then
local nClearance = neighbour._clearance[finder._walkable]
local pushThisNode = clearance and nClearance and (nClearance >= clearance)
if (clearance and pushThisNode) or (not clearance) then
if neighbour._opened then neighbour._opened = false end
neighbour._h = heuristic(endNode, neighbour)
neighbour._f = neighbour._g + neighbour._h
openList:push(neighbour)
neighbour._opened = true
end
end
end
-- Calculates a path.
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
return function (finder, startNode, endNode, clearance, toClear, overrideHeuristic, overrideCostEval)
local heuristic = overrideHeuristic or finder._heuristic
local openList = Heap()
startNode._g = 0
startNode._h = heuristic(endNode, startNode)
startNode._f = startNode._g + startNode._h
openList:push(startNode)
toClear[startNode] = true
startNode._opened = true
while not openList:empty() do
local node = openList:pop()
node._closed = true
if node == endNode then return node end
local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel)
for i = 1,#neighbours do
local neighbour = neighbours[i]
if not neighbour._closed then
toClear[neighbour] = true
if not neighbour._opened then
neighbour._g = huge
neighbour._parent = nil
end
updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval)
end
end
--[[
printf('x:%d y:%d z:%d g:%d', node._x, node._y, node._z, node._g)
for i = 1,#neighbours do
local n = neighbours[i]
printf('x:%d y:%d z:%d f:%f g:%f h:%d', n._x, n._y, n._z, n._f, n._g, n._heading or -1)
end
--]]
end
return nil
end
end

View File

@@ -1,46 +0,0 @@
-- Breadth-First search algorithm
if (...) then
-- Internalization
local t_remove = table.remove
local function breadth_first_search(finder, openList, node, endNode, clearance, toClear)
local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel)
for i = 1,#neighbours do
local neighbour = neighbours[i]
if not neighbour._closed and not neighbour._opened then
local nClearance = neighbour._clearance[finder._walkable]
local pushThisNode = clearance and nClearance and (nClearance >= clearance)
if (clearance and pushThisNode) or (not clearance) then
openList[#openList+1] = neighbour
neighbour._opened = true
neighbour._parent = node
toClear[neighbour] = true
end
end
end
end
-- Calculates a path.
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
return function (finder, startNode, endNode, clearance, toClear)
local openList = {} -- We'll use a FIFO queue (simple array)
openList[1] = startNode
startNode._opened = true
toClear[startNode] = true
local node
while (#openList > 0) do
node = openList[1]
t_remove(openList,1)
node._closed = true
if node == endNode then return node end
breadth_first_search(finder, openList, node, endNode, clearance, toClear)
end
return nil
end
end

View File

@@ -1,133 +0,0 @@
local Logger = {
fn = function() end,
filteredEvents = { },
}
function Logger.setLogger(fn)
Logger.fn = fn
end
function Logger.disable()
Logger.setLogger(function() end)
end
function Logger.setDaemonLogging()
Logger.setLogger(function (text)
os.queueEvent('log', { text = text })
end)
end
function Logger.setMonitorLogging()
local debugMon = device.monitor
if not debugMon then
debugMon.setTextScale(.5)
debugMon.clear()
debugMon.setCursorPos(1, 1)
Logger.setLogger(function(text)
debugMon.write(text)
debugMon.scroll(-1)
debugMon.setCursorPos(1, 1)
end)
end
end
function Logger.setScreenLogging()
Logger.setLogger(function(text)
local x, y = term.getCursorPos()
if x ~= 1 then
local sx, sy = term.getSize()
term.setCursorPos(1, sy)
--term.scroll(1)
end
print(text)
end)
end
function Logger.setWirelessLogging()
if device.wireless_modem then
Logger.filter('modem_message')
Logger.filter('modem_receive')
Logger.filter('rednet_message')
Logger.setLogger(function(text)
device.wireless_modem.transmit(59998, os.getComputerID(), {
type = 'log', contents = text
})
end)
Logger.debug('Logging enabled')
return true
end
end
function Logger.setFileLogging(fileName)
fs.delete(fileName)
Logger.setLogger(function (text)
local logFile
local mode = 'w'
if fs.exists(fileName) then
mode = 'a'
end
local file = io.open(fileName, mode)
if file then
file:write(text)
file:write('\n')
file:close()
end
end)
end
function Logger.log(category, value, ...)
if Logger.filteredEvents[category] then
return
end
if type(value) == 'table' then
local str
for k,v in pairs(value) do
if not str then
str = '{ '
else
str = str .. ', '
end
str = str .. k .. '=' .. tostring(v)
end
if str then
value = str .. ' }'
else
value = '{ }'
end
elseif type(value) == 'string' then
local args = { ... }
if #args > 0 then
value = string.format(value, unpack(args))
end
else
value = tostring(value)
end
Logger.fn(category .. ': ' .. value)
end
function Logger.debug(value, ...)
Logger.log('debug', value, ...)
end
function Logger.logNestedTable(t, indent)
for _,v in ipairs(t) do
if type(v) == 'table' then
log('table')
logNestedTable(v) --, indent+1)
else
log(v)
end
end
end
function Logger.filter( ...)
local events = { ... }
for _,event in pairs(events) do
Logger.filteredEvents[event] = true
end
end
return Logger

View File

@@ -1,76 +0,0 @@
local Util = require('util')
local NFT = { }
-- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/
local tColourLookup = { }
for n = 1, 16 do
tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
end
local function getColourOf(hex)
return tColourLookup[hex:byte()]
end
function NFT.parse(imageText)
local image = {
fg = { },
bg = { },
text = { },
}
local num = 1
local index = 1
for _,sLine in ipairs(Util.split(imageText)) do
table.insert(image.fg, { })
table.insert(image.bg, { })
table.insert(image.text, { })
--As we're no longer 1-1, we keep track of what index to write to
local writeIndex = 1
--Tells us if we've hit a 30 or 31 (BG and FG respectively)- next char specifies the curr colour
local bgNext, fgNext = false, false
--The current background and foreground colours
local currBG, currFG = nil,nil
for i = 1, #sLine do
local nextChar = string.sub(sLine, i, i)
if nextChar:byte() == 30 then
bgNext = true
elseif nextChar:byte() == 31 then
fgNext = true
elseif bgNext then
currBG = getColourOf(nextChar)
bgNext = false
elseif fgNext then
currFG = getColourOf(nextChar)
fgNext = false
else
if nextChar ~= " " and currFG == nil then
currFG = colours.white
end
image.bg[num][writeIndex] = currBG
image.fg[num][writeIndex] = currFG
image.text[num][writeIndex] = nextChar
writeIndex = writeIndex + 1
end
end
image.height = num
if not image.width or writeIndex - 1 > image.width then
image.width = writeIndex - 1
end
num = num+1
end
return image
end
function NFT.load(path)
local imageText = Util.readFile(path)
if not imageText then
error('Unable to read image file')
end
return NFT.parse(imageText)
end
return NFT

View File

@@ -1,49 +0,0 @@
local Opus = { }
local function runDir(directory, open)
if not fs.exists(directory) then
return true
end
local success = true
local files = fs.list(directory)
table.sort(files)
for _,file in ipairs(files) do
os.sleep(0)
local result, err = open(directory .. '/' .. file)
if result then
if term.isColor() then
term.setTextColor(colors.green)
end
term.write('[PASS] ')
term.setTextColor(colors.white)
term.write(fs.combine(directory, file))
else
if term.isColor() then
term.setTextColor(colors.red)
end
term.write('[FAIL] ')
term.setTextColor(colors.white)
term.write(fs.combine(directory, file))
if err then
printError(err)
end
success = false
end
print()
end
return success
end
function Opus.loadServices()
return runDir('sys/services', shell.openHiddenTab)
end
function Opus.autorun()
local s = runDir('sys/autorun', shell.run)
return runDir('usr/autorun', shell.run) and s
end
return Opus

View File

@@ -1,119 +0,0 @@
local Util = require('util')
local Peripheral = { }
local function getDeviceList()
if _G.device then
return _G.device
end
local deviceList = { }
for _,side in pairs(peripheral.getNames()) do
Peripheral.addDevice(deviceList, side)
end
return deviceList
end
function Peripheral.addDevice(deviceList, side)
local name = side
local ptype = peripheral.getType(side)
if not ptype then
return
end
if ptype == 'modem' then
if peripheral.call(name, 'isWireless') then
ptype = 'wireless_modem'
else
ptype = 'wired_modem'
end
end
local sides = {
front = true,
back = true,
top = true,
bottom = true,
left = true,
right = true
}
if sides[name] then
local i = 1
local uniqueName = ptype
while deviceList[uniqueName] do
uniqueName = ptype .. '_' .. i
i = i + 1
end
name = uniqueName
end
local s, m pcall(function() deviceList[name] = peripheral.wrap(side) end)
if not s and m then
printError('wrap failed')
printError(m)
end
if deviceList[name] then
Util.merge(deviceList[name], {
name = name,
type = ptype,
side = side,
})
return deviceList[name]
end
end
function Peripheral.getBySide(side)
return Util.find(getDeviceList(), 'side', side)
end
function Peripheral.getByType(typeName)
return Util.find(getDeviceList(), 'type', typeName)
end
function Peripheral.getByMethod(method)
for _,p in pairs(getDeviceList()) do
if p[method] then
return p
end
end
end
-- match any of the passed arguments
function Peripheral.get(args)
if type(args) == 'string' then
args = { type = args }
end
args = args or { type = pType }
if args.type then
local p = Peripheral.getByType(args.type)
if p then
return p
end
end
if args.method then
local p = Peripheral.getByMethod(args.method)
if p then
return p
end
end
if args.side then
local p = Peripheral.getBySide(args.side)
if p then
return p
end
end
end
return Peripheral

View File

@@ -1,208 +0,0 @@
local Util = require('util')
local Point = { }
function Point.copy(pt)
return { x = pt.x, y = pt.y, z = pt.z }
end
function Point.same(pta, ptb)
return pta.x == ptb.x and
pta.y == ptb.y and
pta.z == ptb.z
end
function Point.above(pt)
return { x = pt.x, y = pt.y + 1, z = pt.z, heading = pt.heading }
end
function Point.below(pt)
return { x = pt.x, y = pt.y - 1, z = pt.z, heading = pt.heading }
end
function Point.subtract(a, b)
a.x = a.x - b.x
a.y = a.y - b.y
a.z = a.z - b.z
end
-- Euclidian distance
function Point.pythagoreanDistance(a, b)
return math.sqrt(
math.pow(a.x - b.x, 2) +
math.pow(a.y - b.y, 2) +
math.pow(a.z - b.z, 2))
end
-- turtle distance (manhattan)
function Point.turtleDistance(a, b)
if a.y and b.y then
return math.abs(a.x - b.x) +
math.abs(a.y - b.y) +
math.abs(a.z - b.z)
else
return math.abs(a.x - b.x) +
math.abs(a.z - b.z)
end
end
function Point.calculateTurns(ih, oh)
if ih == oh then
return 0
end
if (ih % 2) == (oh % 2) then
return 2
end
return 1
end
function Point.calculateHeading(pta, ptb)
local heading
if (pta.heading % 2) == 0 and pta.z ~= ptb.z then
if ptb.z > pta.z then
heading = 1
else
heading = 3
end
elseif (pta.heading % 2) == 1 and pta.x ~= ptb.x then
if ptb.x > pta.x then
heading = 0
else
heading = 2
end
elseif pta.heading == 0 and pta.x > ptb.x then
heading = 2
elseif pta.heading == 2 and pta.x < ptb.x then
heading = 0
elseif pta.heading == 1 and pta.z > ptb.z then
heading = 3
elseif pta.heading == 3 and pta.z < ptb.z then
heading = 1
end
return heading or pta.heading
end
-- Calculate distance to location including turns
-- also returns the resulting heading
function Point.calculateMoves(pta, ptb, distance)
local heading = pta.heading
local moves = distance or Point.turtleDistance(pta, ptb)
if (pta.heading % 2) == 0 and pta.z ~= ptb.z then
moves = moves + 1
if ptb.heading and (ptb.heading % 2 == 1) then
heading = ptb.heading
elseif ptb.z > pta.z then
heading = 1
else
heading = 3
end
elseif (pta.heading % 2) == 1 and pta.x ~= ptb.x then
moves = moves + 1
if ptb.heading and (ptb.heading % 2 == 0) then
heading = ptb.heading
elseif ptb.x > pta.x then
heading = 0
else
heading = 2
end
end
if ptb.heading then
if heading ~= ptb.heading then
moves = moves + Point.calculateTurns(heading, ptb.heading)
heading = ptb.heading
end
end
return moves, heading
end
-- given a set of points, find the one taking the least moves
function Point.closest(reference, pts)
local lpt, lm -- lowest
for _,pt in pairs(pts) do
local m = Point.calculateMoves(reference, pt)
if not lm or m < lm then
lpt = pt
lm = m
end
end
return lpt
end
function Point.eachClosest(spt, ipts, fn)
local pts = Util.shallowCopy(ipts)
while #pts > 0 do
local pt = Point.closest(spt, pts)
local r = fn(pt)
if r then
return r
end
Util.removeByValue(pts, pt)
end
end
function Point.adjacentPoints(pt)
local pts = { }
for _, hi in pairs(turtle.getHeadings()) do
table.insert(pts, { x = pt.x + hi.xd, y = pt.y + hi.yd, z = pt.z + hi.zd })
end
return pts
end
function Point.normalizeBox(box)
return {
x = math.min(box.x, box.ex),
y = math.min(box.y, box.ey),
z = math.min(box.z, box.ez),
ex = math.max(box.x, box.ex),
ey = math.max(box.y, box.ey),
ez = math.max(box.z, box.ez),
}
end
function Point.inBox(pt, box)
return pt.x >= box.x and
pt.y >= box.y and
pt.z >= box.z and
pt.x <= box.ex and
pt.y <= box.ey and
pt.z <= box.ez
end
return Point
--[[
Box = { }
function Box.contain(boundingBox, containedBox)
local shiftX = boundingBox.ax - containedBox.ax
if shiftX > 0 then
containedBox.ax = containedBox.ax + shiftX
containedBox.bx = containedBox.bx + shiftX
end
local shiftZ = boundingBox.az - containedBox.az
if shiftZ > 0 then
containedBox.az = containedBox.az + shiftZ
containedBox.bz = containedBox.bz + shiftZ
end
shiftX = boundingBox.bx - containedBox.bx
if shiftX < 0 then
containedBox.ax = containedBox.ax + shiftX
containedBox.bx = containedBox.bx + shiftX
end
shiftZ = boundingBox.bz - containedBox.bz
if shiftZ < 0 then
containedBox.az = containedBox.az + shiftZ
containedBox.bz = containedBox.bz + shiftZ
end
end
--]]

View File

@@ -1,56 +0,0 @@
local Config = require('config')
local config = { }
local Security = { }
function Security.verifyPassword(password)
Config.load('os', config)
return config.password and password == config.password
end
function Security.getSecretKey()
Config.load('os', config)
if not config.secretKey then
config.secretKey = math.random(100000, 999999)
Config.update('os', config)
end
return config.secretKey
end
function Security.getPublicKey()
local exchange = {
base = 11,
primeMod = 625210769
}
local function modexp(base, exponent, modulo)
local remainder = base
for i = 1, exponent-1 do
remainder = remainder * remainder
if remainder >= modulo then
remainder = remainder % modulo
end
end
return remainder
end
local secretKey = Security.getSecretKey()
return modexp(exchange.base, secretKey, exchange.primeMod)
end
function Security.updatePassword(password)
Config.load('os', config)
config.password = password
Config.update('os', config)
end
function Security.getPassword()
Config.load('os', config)
return config.password
end
return Security

View File

@@ -1,297 +0,0 @@
local sha1 = {
_VERSION = "sha.lua 0.5.0",
_URL = "https://github.com/kikito/sha.lua",
_DESCRIPTION = [[
SHA-1 secure hash computation, and HMAC-SHA1 signature computation in Lua (5.1)
Based on code originally by Jeffrey Friedl (http://regex.info/blog/lua/sha1)
And modified by Eike Decker - (http://cube3d.de/uploads/Main/sha1.txt)
]],
_LICENSE = [[
MIT LICENSE
Copyright (c) 2013 Enrique Garcia Cota + Eike Decker + Jeffrey Friedl
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"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, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
-----------------------------------------------------------------------------------
-- loading this file (takes a while but grants a boost of factor 13)
local PRELOAD_CACHE = false
local BLOCK_SIZE = 64 -- 512 bits
-- local storing of global functions (minor speedup)
local floor,modf = math.floor,math.modf
local char,format,rep = string.char,string.format,string.rep
-- merge 4 bytes to an 32 bit word
local function bytes_to_w32(a,b,c,d) return a*0x1000000+b*0x10000+c*0x100+d end
-- split a 32 bit word into four 8 bit numbers
local function w32_to_bytes(i)
return floor(i/0x1000000)%0x100,floor(i/0x10000)%0x100,floor(i/0x100)%0x100,i%0x100
end
-- shift the bits of a 32 bit word. Don't use negative values for "bits"
local function w32_rot(bits,a)
local b2 = 2^(32-bits)
local a,b = modf(a/b2)
return a+b*b2*(2^(bits))
end
-- caching function for functions that accept 2 arguments, both of values between
-- 0 and 255. The function to be cached is passed, all values are calculated
-- during loading and a function is returned that returns the cached values (only)
local function cache2arg(fn)
if not PRELOAD_CACHE then return fn end
local lut = {}
for i=0,0xffff do
local a,b = floor(i/0x100),i%0x100
lut[i] = fn(a,b)
end
return function(a,b)
return lut[a*0x100+b]
end
end
-- splits an 8-bit number into 8 bits, returning all 8 bits as booleans
local function byte_to_bits(b)
local b = function(n)
local b = floor(b/n)
return b%2==1
end
return b(1),b(2),b(4),b(8),b(16),b(32),b(64),b(128)
end
-- builds an 8bit number from 8 booleans
local function bits_to_byte(a,b,c,d,e,f,g,h)
local function n(b,x) return b and x or 0 end
return n(a,1)+n(b,2)+n(c,4)+n(d,8)+n(e,16)+n(f,32)+n(g,64)+n(h,128)
end
-- bitwise "and" function for 2 8bit number
local band = cache2arg (function(a,b)
local A,B,C,D,E,F,G,H = byte_to_bits(b)
local a,b,c,d,e,f,g,h = byte_to_bits(a)
return bits_to_byte(
A and a, B and b, C and c, D and d,
E and e, F and f, G and g, H and h)
end)
-- bitwise "or" function for 2 8bit numbers
local bor = cache2arg(function(a,b)
local A,B,C,D,E,F,G,H = byte_to_bits(b)
local a,b,c,d,e,f,g,h = byte_to_bits(a)
return bits_to_byte(
A or a, B or b, C or c, D or d,
E or e, F or f, G or g, H or h)
end)
-- bitwise "xor" function for 2 8bit numbers
local bxor = cache2arg(function(a,b)
local A,B,C,D,E,F,G,H = byte_to_bits(b)
local a,b,c,d,e,f,g,h = byte_to_bits(a)
return bits_to_byte(
A ~= a, B ~= b, C ~= c, D ~= d,
E ~= e, F ~= f, G ~= g, H ~= h)
end)
-- bitwise complement for one 8bit number
local function bnot(x)
return 255-(x % 256)
end
-- creates a function to combine to 32bit numbers using an 8bit combination function
local function w32_comb(fn)
return function(a,b)
local aa,ab,ac,ad = w32_to_bytes(a)
local ba,bb,bc,bd = w32_to_bytes(b)
return bytes_to_w32(fn(aa,ba),fn(ab,bb),fn(ac,bc),fn(ad,bd))
end
end
-- create functions for and, xor and or, all for 2 32bit numbers
local w32_and = w32_comb(band)
local w32_xor = w32_comb(bxor)
local w32_or = w32_comb(bor)
-- xor function that may receive a variable number of arguments
local function w32_xor_n(a,...)
local aa,ab,ac,ad = w32_to_bytes(a)
for i=1,select('#',...) do
local ba,bb,bc,bd = w32_to_bytes(select(i,...))
aa,ab,ac,ad = bxor(aa,ba),bxor(ab,bb),bxor(ac,bc),bxor(ad,bd)
end
return bytes_to_w32(aa,ab,ac,ad)
end
-- combining 3 32bit numbers through binary "or" operation
local function w32_or3(a,b,c)
local aa,ab,ac,ad = w32_to_bytes(a)
local ba,bb,bc,bd = w32_to_bytes(b)
local ca,cb,cc,cd = w32_to_bytes(c)
return bytes_to_w32(
bor(aa,bor(ba,ca)), bor(ab,bor(bb,cb)), bor(ac,bor(bc,cc)), bor(ad,bor(bd,cd))
)
end
-- binary complement for 32bit numbers
local function w32_not(a)
return 4294967295-(a % 4294967296)
end
-- adding 2 32bit numbers, cutting off the remainder on 33th bit
local function w32_add(a,b) return (a+b) % 4294967296 end
-- adding n 32bit numbers, cutting off the remainder (again)
local function w32_add_n(a,...)
for i=1,select('#',...) do
a = (a+select(i,...)) % 4294967296
end
return a
end
-- converting the number to a hexadecimal string
local function w32_to_hexstring(w) return format("%08x",w) end
local function hex_to_binary(hex)
return hex:gsub('..', function(hexval)
return string.char(tonumber(hexval, 16))
end)
end
-- building the lookuptables ahead of time (instead of littering the source code
-- with precalculated values)
local xor_with_0x5c = {}
local xor_with_0x36 = {}
for i=0,0xff do
xor_with_0x5c[char(i)] = char(bxor(i,0x5c))
xor_with_0x36[char(i)] = char(bxor(i,0x36))
end
-----------------------------------------------------------------------------
-- calculating the SHA1 for some text
function sha1.sha1(msg)
local H0,H1,H2,H3,H4 = 0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476,0xC3D2E1F0
local msg_len_in_bits = #msg * 8
local first_append = char(0x80) -- append a '1' bit plus seven '0' bits
local non_zero_message_bytes = #msg +1 +8 -- the +1 is the appended bit 1, the +8 are for the final appended length
local current_mod = non_zero_message_bytes % 64
local second_append = current_mod>0 and rep(char(0), 64 - current_mod) or ""
-- now to append the length as a 64-bit number.
local B1, R1 = modf(msg_len_in_bits / 0x01000000)
local B2, R2 = modf( 0x01000000 * R1 / 0x00010000)
local B3, R3 = modf( 0x00010000 * R2 / 0x00000100)
local B4 = 0x00000100 * R3
local L64 = char( 0) .. char( 0) .. char( 0) .. char( 0) -- high 32 bits
.. char(B1) .. char(B2) .. char(B3) .. char(B4) -- low 32 bits
msg = msg .. first_append .. second_append .. L64
assert(#msg % 64 == 0)
local chunks = #msg / 64
local W = { }
local start, A, B, C, D, E, f, K, TEMP
local chunk = 0
while chunk < chunks do
--
-- break chunk up into W[0] through W[15]
--
start,chunk = chunk * 64 + 1,chunk + 1
for t = 0, 15 do
W[t] = bytes_to_w32(msg:byte(start, start + 3))
start = start + 4
end
--
-- build W[16] through W[79]
--
for t = 16, 79 do
-- For t = 16 to 79 let Wt = S1(Wt-3 XOR Wt-8 XOR Wt-14 XOR Wt-16).
W[t] = w32_rot(1, w32_xor_n(W[t-3], W[t-8], W[t-14], W[t-16]))
end
A,B,C,D,E = H0,H1,H2,H3,H4
for t = 0, 79 do
if t <= 19 then
-- (B AND C) OR ((NOT B) AND D)
f = w32_or(w32_and(B, C), w32_and(w32_not(B), D))
K = 0x5A827999
elseif t <= 39 then
-- B XOR C XOR D
f = w32_xor_n(B, C, D)
K = 0x6ED9EBA1
elseif t <= 59 then
-- (B AND C) OR (B AND D) OR (C AND D
f = w32_or3(w32_and(B, C), w32_and(B, D), w32_and(C, D))
K = 0x8F1BBCDC
else
-- B XOR C XOR D
f = w32_xor_n(B, C, D)
K = 0xCA62C1D6
end
-- TEMP = S5(A) + ft(B,C,D) + E + Wt + Kt;
A,B,C,D,E = w32_add_n(w32_rot(5, A), f, E, W[t], K),
A, w32_rot(30, B), C, D
end
-- Let H0 = H0 + A, H1 = H1 + B, H2 = H2 + C, H3 = H3 + D, H4 = H4 + E.
H0,H1,H2,H3,H4 = w32_add(H0, A),w32_add(H1, B),w32_add(H2, C),w32_add(H3, D),w32_add(H4, E)
end
local f = w32_to_hexstring
return f(H0) .. f(H1) .. f(H2) .. f(H3) .. f(H4)
end
function sha1.binary(msg)
return hex_to_binary(sha1.sha1(msg))
end
function sha1.hmac(key, text)
assert(type(key) == 'string', "key passed to sha1.hmac should be a string")
assert(type(text) == 'string', "text passed to sha1.hmac should be a string")
if #key > BLOCK_SIZE then
key = sha1.binary(key)
end
local key_xord_with_0x36 = key:gsub('.', xor_with_0x36) .. string.rep(string.char(0x36), BLOCK_SIZE - #key)
local key_xord_with_0x5c = key:gsub('.', xor_with_0x5c) .. string.rep(string.char(0x5c), BLOCK_SIZE - #key)
return sha1.sha1(key_xord_with_0x5c .. sha1.binary(key_xord_with_0x36 .. text))
end
function sha1.hmac_binary(key, text)
return hex_to_binary(sha1.hmac(key, text))
end
setmetatable(sha1, {__call = function(_,msg) return sha1.sha1(msg) end })
return sha1

View File

@@ -1,196 +0,0 @@
local Crypto = require('crypto')
local Logger = require('logger')
local Security = require('security')
local Util = require('util')
local socketClass = { }
function socketClass:read(timeout)
local data, distance = transport.read(self)
if data then
return data, distance
end
if not self.connected then
Logger.log('socket', 'read: No connection')
return
end
local timerId = os.startTimer(timeout or 5)
while true do
local e, id = os.pullEvent()
if e == 'transport_' .. self.sport then
data, distance = transport.read(self)
if data then
os.cancelTimer(timerId)
return data, distance
end
elseif e == 'timer' and id == timerId then
if timeout or not self.connected then
break
end
timerId = os.startTimer(5)
end
end
end
function socketClass:write(data)
if self.connected then
transport.write(self, {
type = 'DATA',
seq = self.wseq,
data = data,
})
return true
end
end
function socketClass:ping()
if self.connected then
transport.write(self, {
type = 'PING',
seq = self.wseq,
})
return true
end
end
function socketClass:close()
if self.connected then
Logger.log('socket', 'closing socket ' .. self.sport)
self.transmit(self.dport, self.dhost, {
type = 'DISC',
})
self.connected = false
end
device.wireless_modem.close(self.sport)
transport.close(self)
end
local Socket = { }
local function loopback(port, sport, msg)
os.queueEvent('modem_message', 'loopback', port, sport, msg, 0)
end
local function newSocket(isLoopback)
for i = 16384, 32767 do
if not device.wireless_modem.isOpen(i) then
local socket = {
shost = os.getComputerID(),
sport = i,
transmit = device.wireless_modem.transmit,
wseq = math.random(100, 100000),
rseq = math.random(100, 100000),
timers = { },
messages = { },
}
setmetatable(socket, { __index = socketClass })
device.wireless_modem.open(socket.sport)
if isLoopback then
socket.transmit = loopback
end
return socket
end
end
error('No ports available')
end
function Socket.connect(host, port)
local socket = newSocket(host == os.getComputerID())
socket.dhost = host
Logger.log('socket', 'connecting to ' .. port)
socket.transmit(port, socket.sport, {
type = 'OPEN',
shost = socket.shost,
dhost = socket.dhost,
t = Crypto.encrypt({ ts = os.time(), seq = socket.seq }, Security.getPublicKey()),
rseq = socket.wseq,
wseq = socket.rseq,
})
local timerId = os.startTimer(3)
repeat
local e, id, sport, dport, msg = os.pullEvent()
if e == 'modem_message' and
sport == socket.sport and
msg.dhost == socket.shost and
msg.type == 'CONN' then
socket.dport = dport
socket.connected = true
Logger.log('socket', 'connection established to %d %d->%d',
host, socket.sport, socket.dport)
os.cancelTimer(timerId)
transport.open(socket)
return socket
end
until e == 'timer' and id == timerId
socket:close()
end
local function trusted(msg, port)
if port == 19 or msg.shost == os.getComputerID() then
-- no auth for trust server or loopback
return true
end
local trustList = Util.readTable('usr/.known_hosts') or { }
local pubKey = trustList[msg.shost]
if pubKey then
local data = Crypto.decrypt(msg.t or '', pubKey)
--local sharedKey = modexp(pubKey, exchange.secretKey, public.primeMod)
return data.ts and tonumber(data.ts) and math.abs(os.time() - data.ts) < 1
end
end
function Socket.server(port)
device.wireless_modem.open(port)
Logger.log('socket', 'Waiting for connections on port ' .. port)
while true do
local e, _, sport, dport, msg = os.pullEvent('modem_message')
if sport == port and
msg and
msg.dhost == os.getComputerID() and
msg.type == 'OPEN' then
if trusted(msg, port) then
local socket = newSocket(msg.shost == os.getComputerID())
socket.dport = dport
socket.dhost = msg.shost
socket.connected = true
socket.wseq = msg.wseq
socket.rseq = msg.rseq
socket.transmit(socket.dport, socket.sport, {
type = 'CONN',
dhost = socket.dhost,
shost = socket.shost,
})
Logger.log('socket', 'Connection established %d->%d', socket.sport, socket.dport)
transport.open(socket)
return socket
end
end
end
end
return Socket

View File

@@ -1,24 +0,0 @@
local syncLocks = { }
return function(obj, fn)
local key = tostring(obj)
if syncLocks[key] then
local cos = tostring(coroutine.running())
table.insert(syncLocks[key], cos)
repeat
local _, co = os.pullEvent('sync_lock')
until co == cos
else
syncLocks[key] = { }
end
local s, m = pcall(fn)
local co = table.remove(syncLocks[key], 1)
if co then
os.queueEvent('sync_lock', co)
else
syncLocks[key] = nil
end
if not s then
error(m)
end
end

View File

@@ -1,177 +0,0 @@
local Util = require('util')
local Terminal = { }
local _sgsub = string.gsub
function Terminal.scrollable(ct, size)
local size = size or 25
local w, h = ct.getSize()
local win = window.create(ct, 1, 1, w, h + size, true)
local oldWin = Util.shallowCopy(win)
local scrollPos = 0
local function drawScrollbar(oldPos, newPos)
local x, y = oldWin.getCursorPos()
local pos = math.floor(oldPos / size * (h - 1))
oldWin.setCursorPos(w, oldPos + pos + 1)
oldWin.write(' ')
pos = math.floor(newPos / size * (h - 1))
oldWin.setCursorPos(w, newPos + pos + 1)
oldWin.write('#')
oldWin.setCursorPos(x, y)
end
win.setCursorPos = function(x, y)
oldWin.setCursorPos(x, y)
if y > scrollPos + h then
win.scrollTo(y - h)
elseif y < scrollPos then
win.scrollTo(y - 2)
end
end
win.scrollUp = function()
win.scrollTo(scrollPos - 1)
end
win.scrollDown = function()
win.scrollTo(scrollPos + 1)
end
win.scrollTo = function(p)
p = math.min(math.max(p, 0), size)
if p ~= scrollPos then
drawScrollbar(scrollPos, p)
scrollPos = p
--local w, h = win.getSize()
win.reposition(1, -scrollPos + 1, w, h + size)
end
end
win.clear = function()
oldWin.clear()
scrollPos = 0
end
drawScrollbar(0, 0)
return win
end
function Terminal.toGrayscale(ct)
local scolors = {
[ colors.white ] = colors.white,
[ colors.orange ] = colors.lightGray,
[ colors.magenta ] = colors.lightGray,
[ colors.lightBlue ] = colors.lightGray,
[ colors.yellow ] = colors.lightGray,
[ colors.lime ] = colors.lightGray,
[ colors.pink ] = colors.lightGray,
[ colors.gray ] = colors.gray,
[ colors.lightGray ] = colors.lightGray,
[ colors.cyan ] = colors.lightGray,
[ colors.purple ] = colors.gray,
[ colors.blue ] = colors.gray,
[ colors.brown ] = colors.gray,
[ colors.green ] = colors.lightGray,
[ colors.red ] = colors.gray,
[ colors.black ] = colors.black,
}
local methods = { 'setBackgroundColor', 'setBackgroundColour',
'setTextColor', 'setTextColour' }
for _,v in pairs(methods) do
local fn = ct[v]
ct[v] = function(c)
fn(scolors[c])
end
end
local bcolors = {
[ '1' ] = '8',
[ '2' ] = '8',
[ '3' ] = '8',
[ '4' ] = '8',
[ '5' ] = '8',
[ '6' ] = '8',
[ '9' ] = '8',
[ 'a' ] = '7',
[ 'b' ] = '7',
[ 'c' ] = '7',
[ 'd' ] = '8',
[ 'e' ] = '7',
}
local function translate(s)
if s then
for k,v in pairs(bcolors) do
s = _sgsub(s, k, v)
end
-- s = _sgsub(s, "%d+", bcolors) -- not working in cc 1.75 ???
end
return s
end
local fn = ct.blit
ct.blit = function(text, fg, bg)
fn(text, translate(fg), translate(bg))
end
end
function Terminal.getNullTerm(ct)
local nt = Terminal.copy(ct)
local methods = { 'blit', 'clear', 'clearLine', 'scroll',
'setCursorBlink', 'setCursorPos', 'write' }
for _,v in pairs(methods) do
nt[v] = function() end
end
return nt
end
function Terminal.copy(it, ot)
ot = ot or { }
for k,v in pairs(it) do
if type(v) == 'function' then
ot[k] = v
end
end
return ot
end
function Terminal.mirror(ct, dt)
for k,f in pairs(ct) do
ct[k] = function(...)
local ret = { f(...) }
if dt[k] then
dt[k](...)
end
return unpack(ret)
end
end
end
function Terminal.readPassword(prompt)
if prompt then
term.write(prompt)
end
local fn = term.current().write
term.current().write = function() end
local s
pcall(function() s = read(prompt) end)
term.current().write = fn
if s == '' then
return
end
return s
end
return Terminal

View File

@@ -1,265 +0,0 @@
requireInjector(getfenv(1))
local Grid = require ("jumper.grid")
local Pathfinder = require ("jumper.pathfinder")
local Point = require('point')
local Util = require('util')
local WALKABLE = 0
local function createMap(dim)
local map = { }
for z = 1, dim.ez do
local row = {}
for x = 1, dim.ex do
local col = { }
for y = 1, dim.ey do
table.insert(col, WALKABLE)
end
table.insert(row, col)
end
table.insert(map, row)
end
return map
end
local function addBlock(map, dim, b)
map[b.z + dim.oz][b.x + dim.ox][b.y + dim.oy] = 1
end
-- map shrinks/grows depending upon blocks encountered
-- the map will encompass any blocks encountered, the turtle position, and the destination
local function mapDimensions(dest, blocks, boundingBox)
local sx, sz, sy = turtle.point.x, turtle.point.z, turtle.point.y
local ex, ez, ey = turtle.point.x, turtle.point.z, turtle.point.y
local function adjust(pt)
if pt.x < sx then
sx = pt.x
end
if pt.z < sz then
sz = pt.z
end
if pt.y < sy then
sy = pt.y
end
if pt.x > ex then
ex = pt.x
end
if pt.z > ez then
ez = pt.z
end
if pt.y > ey then
ey = pt.y
end
end
adjust(dest)
for _,b in ipairs(blocks) do
adjust(b)
end
-- expand one block out in all directions
if boundingBox then
sx = math.max(sx - 1, boundingBox.x)
sz = math.max(sz - 1, boundingBox.z)
sy = math.max(sy - 1, boundingBox.y)
ex = math.min(ex + 1, boundingBox.ex)
ez = math.min(ez + 1, boundingBox.ez)
ey = math.min(ey + 1, boundingBox.ey)
else
sx = sx - 1
sz = sz - 1
sy = sy - 1
ex = ex + 1
ez = ez + 1
ey = ey + 1
end
return {
ex = ex - sx + 1,
ez = ez - sz + 1,
ey = ey - sy + 1,
ox = -sx + 1,
oz = -sz + 1,
oy = -sy + 1
}
end
-- shifting and coordinate flipping
local function pointToMap(dim, pt)
return { x = pt.x + dim.ox, z = pt.y + dim.oy, y = pt.z + dim.oz }
end
local function nodeToPoint(dim, node)
return { x = node:getX() - dim.ox, z = node:getY() - dim.oz, y = node:getZ() - dim.oy }
end
local heuristic = function(n, node)
local m, h = Point.calculateMoves(
{ x = node._x, z = node._y, y = node._z, heading = node._heading },
{ x = n._x, z = n._y, y = n._z, heading = n._heading })
return m, h
end
local function dimsAreEqual(d1, d2)
return d1.ex == d2.ex and
d1.ey == d2.ey and
d1.ez == d2.ez and
d1.ox == d2.ox and
d1.oy == d2.oy and
d1.oz == d2.oz
end
-- turtle sensor returns blocks in relation to the world - not turtle orientation
-- so cannot figure out block location unless we know our orientation in the world
-- really kinda dumb since it returns the coordinates as offsets of our location
-- instead of true coordinates
local function addSensorBlocks(blocks, sblocks)
for _,b in pairs(sblocks) do
if b.type ~= 'AIR' then
local pt = { x = turtle.point.x, y = turtle.point.y + b.y, z = turtle.point.z }
pt.x = pt.x - b.x
pt.z = pt.z - b.z -- this will only work if we were originally facing west
local found = false
for _,ob in pairs(blocks) do
if pt.x == ob.x and pt.y == ob.y and pt.z == ob.z then
found = true
break
end
end
if not found then
table.insert(blocks, pt)
end
end
end
end
local function selectDestination(pts, box, map, dim)
while #pts > 0 do
local pt = Point.closest(turtle.point, pts)
if (box and not Point.inBox(pt, box)) or
map[pt.z + dim.oz][pt.x + dim.ox][pt.y + dim.oy] == 1 then
Util.removeByValue(pts, pt)
else
return pt
end
end
end
local function pathTo(dest, options)
local blocks = options.blocks or turtle.getState().blocks or { }
local dests = options.dest or { dest } -- support alternative destinations
local box = options.box or turtle.getState().box
local lastDim = nil
local map = nil
local grid = nil
if box then
box = Point.normalizeBox(box)
end
-- Creates a pathfinder object
local myFinder = Pathfinder(grid, 'ASTAR', walkable)
myFinder:setMode('ORTHOGONAL')
myFinder:setHeuristic(heuristic)
while turtle.point.x ~= dest.x or turtle.point.z ~= dest.z or turtle.point.y ~= dest.y do
-- map expands as we encounter obstacles
local dim = mapDimensions(dest, blocks, box)
-- reuse map if possible
if not lastDim or not dimsAreEqual(dim, lastDim) then
map = createMap(dim)
-- Creates a grid object
grid = Grid(map)
myFinder:setGrid(grid)
myFinder:setWalkable(WALKABLE)
lastDim = dim
end
for _,b in ipairs(blocks) do
addBlock(map, dim, b)
end
dest = selectDestination(dests, box, map, dim)
if not dest then
-- error('failed to reach destination')
return false, 'failed to reach destination'
end
if turtle.point.x == dest.x and turtle.point.z == dest.z and turtle.point.y == dest.y then
break
end
-- Define start and goal locations coordinates
local startPt = pointToMap(dim, turtle.point)
local endPt = pointToMap(dim, dest)
-- Calculates the path, and its length
local path = myFinder:getPath(startPt.x, startPt.y, startPt.z, turtle.point.heading, endPt.x, endPt.y, endPt.z, dest.heading)
if not path then
Util.removeByValue(dests, dest)
else
for node, count in path:nodes() do
local pt = nodeToPoint(dim, node)
if turtle.abort then
return false, 'aborted'
end
-- use single turn method so the turtle doesn't turn around
-- when encountering obstacles -- IS THIS RIGHT ??
if not turtle.gotoSingleTurn(pt.x, pt.z, pt.y, node.heading) then
table.insert(blocks, pt)
--if device.turtlesensorenvironment then
-- addSensorBlocks(blocks, device.turtlesensorenvironment.sonicScan())
--end
break
end
end
end
end
if dest.heading then
turtle.setHeading(dest.heading)
end
return dest
end
return {
pathfind = function(dest, options)
options = options or { }
--if not options.blocks and turtle.gotoPoint(dest) then
-- return dest
--end
return pathTo(dest, options)
end,
-- set a global bounding box
-- box can be overridden by passing box in pathfind options
setBox = function(box)
turtle.getState().box = box
end,
setBlocks = function(blocks)
turtle.getState().blocks = blocks
end,
reset = function()
turtle.getState().box = nil
turtle.getState().blocks = nil
end,
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,364 +0,0 @@
local class = require('class')
local Region = require('ui.region')
local Util = require('util')
local _srep = string.rep
local _ssub = string.sub
local mapColorToGray = {
[ colors.white ] = colors.white,
[ colors.orange ] = colors.lightGray,
[ colors.magenta ] = colors.lightGray,
[ colors.lightBlue ] = colors.lightGray,
[ colors.yellow ] = colors.lightGray,
[ colors.lime ] = colors.lightGray,
[ colors.pink ] = colors.lightGray,
[ colors.gray ] = colors.gray,
[ colors.lightGray ] = colors.lightGray,
[ colors.cyan ] = colors.lightGray,
[ colors.purple ] = colors.gray,
[ colors.blue ] = colors.gray,
[ colors.brown ] = colors.gray,
[ colors.green ] = colors.lightGray,
[ colors.red ] = colors.gray,
[ colors.black ] = colors.black,
}
local mapColorToPaint = { }
for n = 1, 16 do
mapColorToPaint[2 ^ (n - 1)] = _ssub("0123456789abcdef", n, n)
end
local mapGrayToPaint = { }
for n = 0, 15 do
local gs = mapColorToGray[2 ^ n]
mapGrayToPaint[2 ^ n] = mapColorToPaint[gs]
end
local Canvas = class()
function Canvas:init(args)
self.x = 1
self.y = 1
self.layers = { }
Util.merge(self, args)
self.height = self.ey - self.y + 1
self.width = self.ex - self.x + 1
self.lines = { }
for i = 1, self.height do
self.lines[i] = { }
end
end
function Canvas:resize(w, h)
for i = self.height, h do
self.lines[i] = { }
end
while #self.lines > h do
table.remove(self.lines, #self.lines)
end
if w ~= self.width then
for i = 1, self.height do
self.lines[i] = { }
end
end
self.ex = self.x + w - 1
self.ey = self.y + h - 1
self.width = w
self.height = h
self:dirty()
end
function Canvas:colorToPaintColor(c)
if self.isColor then
return mapColorToPaint[c]
end
return mapGrayToPaint[c]
end
function Canvas:copy()
local b = Canvas({ x = self.x, y = self.y, ex = self.ex, ey = self.ey })
for i = 1, self.ey - self.y + 1 do
b.lines[i].text = self.lines[i].text
b.lines[i].fg = self.lines[i].fg
b.lines[i].bg = self.lines[i].bg
end
return b
end
function Canvas:addLayer(layer, bg, fg)
local canvas = Canvas({
x = layer.x,
y = layer.y,
ex = layer.x + layer.width - 1,
ey = layer.y + layer.height - 1,
isColor = self.isColor,
})
canvas:clear(bg, fg)
canvas.parent = self
table.insert(self.layers, canvas)
return canvas
end
function Canvas:removeLayer()
for k, layer in pairs(self.parent.layers) do
if layer == self then
self:setVisible(false)
table.remove(self.parent.layers, k)
break
end
end
end
function Canvas:setVisible(visible)
self.visible = visible
if not visible then
self.parent:dirty()
-- set parent's lines to dirty for each line in self
end
end
function Canvas:write(x, y, text, bg, fg)
if bg then
bg = _srep(self:colorToPaintColor(bg), #text)
end
if fg then
fg = _srep(self:colorToPaintColor(fg), #text)
end
self:writeBlit(x, y, text, bg, fg)
end
function Canvas:writeBlit(x, y, text, bg, fg)
if y > 0 and y <= self.height and x <= self.width then
local width = #text
-- fix ffs
if x < 1 then
text = _ssub(text, 2 - x)
if bg then
bg = _ssub(bg, 2 - x)
end
if bg then
fg = _ssub(fg, 2 - x)
end
width = width + x - 1
x = 1
end
if x + width - 1 > self.width then
text = _ssub(text, 1, self.width - x + 1)
if bg then
bg = _ssub(bg, 1, self.width - x + 1)
end
if bg then
fg = _ssub(fg, 1, self.width - x + 1)
end
width = #text
end
if width > 0 then
local function replace(sstr, pos, rstr, width)
if pos == 1 and width == self.width then
return rstr
elseif pos == 1 then
return rstr .. _ssub(sstr, pos+width)
elseif pos + width > self.width then
return _ssub(sstr, 1, pos-1) .. rstr
end
return _ssub(sstr, 1, pos-1) .. rstr .. _ssub(sstr, pos+width)
end
local line = self.lines[y]
line.dirty = true
line.text = replace(line.text, x, text, width)
if fg then
line.fg = replace(line.fg, x, fg, width)
end
if bg then
line.bg = replace(line.bg, x, bg, width)
end
end
end
end
function Canvas:writeLine(y, text, fg, bg)
self.lines[y].dirty = true
self.lines[y].text = text
self.lines[y].fg = fg
self.lines[y].bg = bg
end
function Canvas:reset()
self.regions = nil
end
function Canvas:clear(bg, fg)
local width = self.ex - self.x + 1
local text = _srep(' ', width)
fg = _srep(self:colorToPaintColor(fg), width)
bg = _srep(self:colorToPaintColor(bg), width)
for i = 1, self.ey - self.y + 1 do
self:writeLine(i, text, fg, bg)
end
end
function Canvas:punch(rect)
if not self.regions then
self.regions = Region.new(self.x, self.y, self.ex, self.ey)
end
self.regions:subRect(rect.x, rect.y, rect.ex, rect.ey)
end
function Canvas:blitClipped(device)
for _,region in ipairs(self.regions.region) do
self:blit(device,
{ x = region[1] - self.x + 1,
y = region[2] - self.y + 1,
ex = region[3]- self.x + 1,
ey = region[4] - self.y + 1 },
{ x = region[1], y = region[2] })
end
end
function Canvas:redraw(device)
self:reset()
if #self.layers > 0 then
for _,layer in pairs(self.layers) do
self:punch(layer)
end
self:blitClipped(device)
else
self:blit(device)
end
self:clean()
end
function Canvas:isDirty()
for _, line in pairs(self.lines) do
if line.dirty then
return true
end
end
end
function Canvas:dirty()
for _, line in pairs(self.lines) do
line.dirty = true
end
end
function Canvas:clean()
for y, line in pairs(self.lines) do
line.dirty = false
end
end
function Canvas:render(device, layers) --- redrawAll ?
layers = layers or self.layers
if #layers > 0 then
self.regions = Region.new(self.x, self.y, self.ex, self.ey)
local l = Util.shallowCopy(layers)
for _, canvas in ipairs(layers) do
table.remove(l, 1)
if canvas.visible then
self:punch(canvas)
canvas:render(device, l)
end
end
self:blitClipped(device)
self:reset()
else
self:blit(device)
end
self:clean()
end
function Canvas:blit(device, src, tgt)
src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 }
tgt = tgt or self
for i = 0, src.ey - src.y do
local line = self.lines[src.y + i]
if line and line.dirty then
local t, fg, bg = line.text, line.fg, line.bg
if src.x > 1 or src.ex < self.ex then
t = _ssub(t, src.x, src.ex)
fg = _ssub(fg, src.x, src.ex)
bg = _ssub(bg, src.x, src.ex)
end
--if tgt.y + i > self.ey then -- wrong place to do clipping ??
-- break
--end
device.setCursorPos(tgt.x, tgt.y + i)
device.blit(t, fg, bg)
end
end
end
function Canvas.convertWindow(win, parent, x, y)
local w, h = win.getSize()
win.canvas = Canvas({
x = x,
y = y,
ex = x + w - 1,
ey = y + h - 1,
isColor = win.isColor(),
})
function win.clear()
win.canvas:clear(win.getBackgroundColor(), win.getTextColor())
end
function win.clearLine()
local x, y = win.getCursorPos()
win.canvas:write(1,
y,
_srep(' ', win.canvas.width),
win.getBackgroundColor(),
win.getTextColor())
end
function win.write(str)
local x, y = win.getCursorPos()
win.canvas:write(x,
y,
str,
win.getBackgroundColor(),
win.getTextColor())
end
function win.blit(text, fg, bg)
local x, y = win.getCursorPos()
win.canvas:writeBlit(x, y, text, bg, fg)
end
function win.redraw()
win.canvas:redraw(parent)
end
function win.scroll()
error('CWin:scroll: not implemented')
end
function win.reposition(x, y, width, height)
win.canvas.x, win.canvas.y = x, y
win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
end
win.clear()
end
return Canvas

View File

@@ -1,142 +0,0 @@
local UI = require('ui')
local Util = require('util')
return function(args)
local columns = {
{ heading = 'Name', key = 'name' },
}
if UI.term.width > 28 then
table.insert(columns,
{ heading = 'Size', key = 'size', width = 5 }
)
end
args = args or { }
local selectFile = UI.Dialog {
x = args.x or 3,
y = args.y or 2,
z = args.z or 2,
-- rex = args.rex or -3,
-- rey = args.rey or -3,
height = args.height,
width = args.width,
title = 'Select file',
grid = UI.ScrollingGrid {
x = 2,
y = 2,
ex = -2,
ey = -4,
path = '',
sortColumn = 'name',
columns = columns,
},
path = UI.TextEntry {
x = 2,
y = -2,
ex = -11,
limit = 256,
accelerators = {
enter = 'path_enter',
}
},
cancel = UI.Button {
text = 'Cancel',
x = -9,
y = -2,
event = 'cancel',
},
}
function selectFile:enable(path, fn)
self:setPath(path)
self.fn = fn
UI.Dialog.enable(self)
end
function selectFile:setPath(path)
self.grid.dir = path
while not fs.isDir(self.grid.dir) do
self.grid.dir = fs.getDir(self.grid.dir)
end
self.path.value = self.grid.dir
end
function selectFile.grid:draw()
local files = fs.listEx(self.dir)
if #self.dir > 0 then
table.insert(files, {
name = '..',
isDir = true,
})
end
self:setValues(files)
self:setIndex(1)
UI.Grid.draw(self)
end
function selectFile.grid:getDisplayValues(row)
if row.size then
row = Util.shallowCopy(row)
row.size = Util.toBytes(row.size)
end
return row
end
function selectFile.grid:getRowTextColor(file, selected)
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end
function selectFile.grid:sortCompare(a, b)
if self.sortColumn == 'size' then
return a.size < b.size
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end
function selectFile:eventHandler(event)
if event.type == 'grid_select' then
self.grid.dir = fs.combine(self.grid.dir, event.selected.name)
self.path.value = self.grid.dir
if event.selected.isDir then
self.grid:draw()
self.path:draw()
else
UI:setPreviousPage()
self.fn(self.path.value)
end
elseif event.type == 'path_enter' then
if fs.isDir(self.path.value) then
self:setPath(self.path.value)
self.grid:draw()
self.path:draw()
else
UI:setPreviousPage()
self.fn(self.path.value)
end
elseif event.type == 'cancel' then
UI:setPreviousPage()
self.fn()
else
return UI.Dialog.eventHandler(self, event)
end
return true
end
return selectFile
end

View File

@@ -1,196 +0,0 @@
local class = require('class')
local UI = require('ui')
local Event = require('event')
local Peripheral = require('peripheral')
--[[-- Glasses device --]]--
local Glasses = class()
function Glasses:init(args)
local defaults = {
backgroundColor = colors.black,
textColor = colors.white,
textScale = .5,
backgroundOpacity = .5,
multiplier = 2.6665,
-- multiplier = 2.333,
}
defaults.width, defaults.height = term.getSize()
UI:setProperties(defaults, args)
UI:setProperties(self, defaults)
self.bridge = Peripheral.get({
type = 'openperipheral_bridge',
method = 'addBox',
})
self.bridge.clear()
self.setBackgroundColor = function(...) end
self.setTextColor = function(...) end
self.t = { }
for i = 1, self.height do
self.t[i] = {
text = string.rep(' ', self.width+1),
--text = self.bridge.addText(0, 40+i*4, string.rep(' ', self.width+1), 0xffffff),
bg = { },
textFields = { },
}
end
end
function Glasses:setBackgroundBox(boxes, ax, bx, y, bgColor)
local colors = {
[ colors.black ] = 0x000000,
[ colors.brown ] = 0x7F664C,
[ colors.blue ] = 0x253192,
[ colors.red ] = 0xFF0000,
[ colors.gray ] = 0x272727,
[ colors.lime ] = 0x426A0D,
[ colors.green ] = 0x2D5628,
[ colors.white ] = 0xFFFFFF
}
local function overlap(box, ax, bx)
if bx < box.ax or ax > box.bx then
return false
end
return true
end
for _,box in pairs(boxes) do
if overlap(box, ax, bx) then
if box.bgColor == bgColor then
ax = math.min(ax, box.ax)
bx = math.max(bx, box.bx)
box.ax = box.bx + 1
elseif ax == box.ax then
box.ax = bx + 1
elseif ax > box.ax then
if bx < box.bx then
table.insert(boxes, { -- split
ax = bx + 1,
bx = box.bx,
bgColor = box.bgColor
})
box.bx = ax - 1
break
else
box.ax = box.bx + 1
end
elseif ax < box.ax then
if bx > box.bx then
box.ax = box.bx + 1 -- delete
else
box.ax = bx + 1
end
end
end
end
if bgColor ~= colors.black then
table.insert(boxes, {
ax = ax,
bx = bx,
bgColor = bgColor
})
end
local deleted
repeat
deleted = false
for k,box in pairs(boxes) do
if box.ax > box.bx then
if box.box then
box.box.delete()
end
table.remove(boxes, k)
deleted = true
break
end
if not box.box then
box.box = self.bridge.addBox(
math.floor(self.x + (box.ax - 1) * self.multiplier),
self.y + y * 4,
math.ceil((box.bx - box.ax + 1) * self.multiplier),
4,
colors[bgColor],
self.backgroundOpacity)
else
box.box.setX(self.x + math.floor((box.ax - 1) * self.multiplier))
box.box.setWidth(math.ceil((box.bx - box.ax + 1) * self.multiplier))
end
end
until not deleted
end
function Glasses:write(x, y, text, bg)
if x < 1 then
error(' less ', 6)
end
if y <= #self.t then
local line = self.t[y]
local str = line.text
str = str:sub(1, x-1) .. text .. str:sub(x + #text)
self.t[y].text = str
for _,tf in pairs(line.textFields) do
tf.delete()
end
line.textFields = { }
local function split(st)
local words = { }
local offset = 0
while true do
local b,e,w = st:find('(%S+)')
if not b then
break
end
table.insert(words, {
offset = b + offset - 1,
text = w,
})
offset = offset + e
st = st:sub(e + 1)
end
return words
end
local words = split(str)
for _,word in pairs(words) do
local tf = self.bridge.addText(self.x + word.offset * self.multiplier,
self.y+y*4, '', 0xffffff)
tf.setScale(self.textScale)
tf.setZ(1)
tf.setText(word.text)
table.insert(line.textFields, tf)
end
self:setBackgroundBox(line.bg, x, x + #text - 1, y, bg)
end
end
function Glasses:clear(bg)
for _,line in pairs(self.t) do
for _,tf in pairs(line.textFields) do
tf.delete()
end
line.textFields = { }
line.text = string.rep(' ', self.width+1)
-- self.t[i].text.setText('')
end
end
function Glasses:reset()
self:clear()
self.bridge.clear()
self.bridge.sync()
end
function Glasses:sync()
self.bridge.sync()
end
return Glasses

View File

@@ -1,367 +0,0 @@
local tween = {
_VERSION = 'tween 2.1.1',
_DESCRIPTION = 'tweening for lua',
_URL = 'https://github.com/kikito/tween.lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 Enrique García Cota, Yuichi Tateno, Emmanuel Oga
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"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, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
-- easing
-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits.
-- For all easing functions:
-- t = time == how much time has to pass for the tweening to complete
-- b = begin == starting property value
-- c = change == ending - beginning
-- d = duration == running time. How much time has passed *right now*
local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin
-- linear
local function linear(t, b, c, d) return c * t / d + b end
-- quad
local function inQuad(t, b, c, d) return c * pow(t / d, 2) + b end
local function outQuad(t, b, c, d)
t = t / d
return -c * t * (t - 2) + b
end
local function inOutQuad(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 2) + b end
return -c / 2 * ((t - 1) * (t - 3) - 1) + b
end
local function outInQuad(t, b, c, d)
if t < d / 2 then return outQuad(t * 2, b, c / 2, d) end
return inQuad((t * 2) - d, b + c / 2, c / 2, d)
end
-- cubic
local function inCubic (t, b, c, d) return c * pow(t / d, 3) + b end
local function outCubic(t, b, c, d) return c * (pow(t / d - 1, 3) + 1) + b end
local function inOutCubic(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * t * t * t + b end
t = t - 2
return c / 2 * (t * t * t + 2) + b
end
local function outInCubic(t, b, c, d)
if t < d / 2 then return outCubic(t * 2, b, c / 2, d) end
return inCubic((t * 2) - d, b + c / 2, c / 2, d)
end
-- quart
local function inQuart(t, b, c, d) return c * pow(t / d, 4) + b end
local function outQuart(t, b, c, d) return -c * (pow(t / d - 1, 4) - 1) + b end
local function inOutQuart(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 4) + b end
return -c / 2 * (pow(t - 2, 4) - 2) + b
end
local function outInQuart(t, b, c, d)
if t < d / 2 then return outQuart(t * 2, b, c / 2, d) end
return inQuart((t * 2) - d, b + c / 2, c / 2, d)
end
-- quint
local function inQuint(t, b, c, d) return c * pow(t / d, 5) + b end
local function outQuint(t, b, c, d) return c * (pow(t / d - 1, 5) + 1) + b end
local function inOutQuint(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 5) + b end
return c / 2 * (pow(t - 2, 5) + 2) + b
end
local function outInQuint(t, b, c, d)
if t < d / 2 then return outQuint(t * 2, b, c / 2, d) end
return inQuint((t * 2) - d, b + c / 2, c / 2, d)
end
-- sine
local function inSine(t, b, c, d) return -c * cos(t / d * (pi / 2)) + c + b end
local function outSine(t, b, c, d) return c * sin(t / d * (pi / 2)) + b end
local function inOutSine(t, b, c, d) return -c / 2 * (cos(pi * t / d) - 1) + b end
local function outInSine(t, b, c, d)
if t < d / 2 then return outSine(t * 2, b, c / 2, d) end
return inSine((t * 2) -d, b + c / 2, c / 2, d)
end
-- expo
local function inExpo(t, b, c, d)
if t == 0 then return b end
return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001
end
local function outExpo(t, b, c, d)
if t == d then return b + c end
return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b
end
local function inOutExpo(t, b, c, d)
if t == 0 then return b end
if t == d then return b + c end
t = t / d * 2
if t < 1 then return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 end
return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b
end
local function outInExpo(t, b, c, d)
if t < d / 2 then return outExpo(t * 2, b, c / 2, d) end
return inExpo((t * 2) - d, b + c / 2, c / 2, d)
end
-- circ
local function inCirc(t, b, c, d) return(-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) end
local function outCirc(t, b, c, d) return(c * sqrt(1 - pow(t / d - 1, 2)) + b) end
local function inOutCirc(t, b, c, d)
t = t / d * 2
if t < 1 then return -c / 2 * (sqrt(1 - t * t) - 1) + b end
t = t - 2
return c / 2 * (sqrt(1 - t * t) + 1) + b
end
local function outInCirc(t, b, c, d)
if t < d / 2 then return outCirc(t * 2, b, c / 2, d) end
return inCirc((t * 2) - d, b + c / 2, c / 2, d)
end
-- elastic
local function calculatePAS(p,a,c,d)
p, a = p or d * 0.3, a or 0
if a < abs(c) then return p, c, p / 4 end -- p, a, s
return p, a, p / (2 * pi) * asin(c/a) -- p,a,s
end
local function inElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
t = t - 1
return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b
end
local function outElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b
end
local function inOutElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d * 2
if t == 2 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
t = t - 1
if t < 0 then return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p ) * 0.5 + c + b
end
local function outInElastic(t, b, c, d, a, p)
if t < d / 2 then return outElastic(t * 2, b, c / 2, d, a, p) end
return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p)
end
-- back
local function inBack(t, b, c, d, s)
s = s or 1.70158
t = t / d
return c * t * t * ((s + 1) * t - s) + b
end
local function outBack(t, b, c, d, s)
s = s or 1.70158
t = t / d - 1
return c * (t * t * ((s + 1) * t + s) + 1) + b
end
local function inOutBack(t, b, c, d, s)
s = (s or 1.70158) * 1.525
t = t / d * 2
if t < 1 then return c / 2 * (t * t * ((s + 1) * t - s)) + b end
t = t - 2
return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b
end
local function outInBack(t, b, c, d, s)
if t < d / 2 then return outBack(t * 2, b, c / 2, d, s) end
return inBack((t * 2) - d, b + c / 2, c / 2, d, s)
end
-- bounce
local function outBounce(t, b, c, d)
t = t / d
if t < 1 / 2.75 then return c * (7.5625 * t * t) + b end
if t < 2 / 2.75 then
t = t - (1.5 / 2.75)
return c * (7.5625 * t * t + 0.75) + b
elseif t < 2.5 / 2.75 then
t = t - (2.25 / 2.75)
return c * (7.5625 * t * t + 0.9375) + b
end
t = t - (2.625 / 2.75)
return c * (7.5625 * t * t + 0.984375) + b
end
local function inBounce(t, b, c, d) return c - outBounce(d - t, 0, c, d) + b end
local function inOutBounce(t, b, c, d)
if t < d / 2 then return inBounce(t * 2, 0, c, d) * 0.5 + b end
return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b
end
local function outInBounce(t, b, c, d)
if t < d / 2 then return outBounce(t * 2, b, c / 2, d) end
return inBounce((t * 2) - d, b + c / 2, c / 2, d)
end
tween.easing = {
linear = linear,
inQuad = inQuad, outQuad = outQuad, inOutQuad = inOutQuad, outInQuad = outInQuad,
inCubic = inCubic, outCubic = outCubic, inOutCubic = inOutCubic, outInCubic = outInCubic,
inQuart = inQuart, outQuart = outQuart, inOutQuart = inOutQuart, outInQuart = outInQuart,
inQuint = inQuint, outQuint = outQuint, inOutQuint = inOutQuint, outInQuint = outInQuint,
inSine = inSine, outSine = outSine, inOutSine = inOutSine, outInSine = outInSine,
inExpo = inExpo, outExpo = outExpo, inOutExpo = inOutExpo, outInExpo = outInExpo,
inCirc = inCirc, outCirc = outCirc, inOutCirc = inOutCirc, outInCirc = outInCirc,
inElastic = inElastic, outElastic = outElastic, inOutElastic = inOutElastic, outInElastic = outInElastic,
inBack = inBack, outBack = outBack, inOutBack = inOutBack, outInBack = outInBack,
inBounce = inBounce, outBounce = outBounce, inOutBounce = inOutBounce, outInBounce = outInBounce
}
-- private stuff
local function copyTables(destination, keysTable, valuesTable)
valuesTable = valuesTable or keysTable
local mt = getmetatable(keysTable)
if mt and getmetatable(destination) == nil then
setmetatable(destination, mt)
end
for k,v in pairs(keysTable) do
if type(v) == 'table' then
destination[k] = copyTables({}, v, valuesTable[k])
else
destination[k] = valuesTable[k]
end
end
return destination
end
local function checkSubjectAndTargetRecursively(subject, target, path)
path = path or {}
local targetType, newPath
for k,targetValue in pairs(target) do
targetType, newPath = type(targetValue), copyTables({}, path)
table.insert(newPath, tostring(k))
if targetType == 'number' then
assert(type(subject[k]) == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' is missing from subject or isn't a number")
elseif targetType == 'table' then
checkSubjectAndTargetRecursively(subject[k], targetValue, newPath)
else
assert(targetType == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' must be a number or table of numbers")
end
end
end
local function checkNewParams(duration, subject, target, easing)
assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration))
local tsubject = type(subject)
assert(tsubject == 'table' or tsubject == 'userdata', "subject must be a table or userdata. Was " .. tostring(subject))
assert(type(target)== 'table', "target must be a table. Was " .. tostring(target))
assert(type(easing)=='function', "easing must be a function. Was " .. tostring(easing))
checkSubjectAndTargetRecursively(subject, target)
end
local function getEasingFunction(easing)
easing = easing or "linear"
if type(easing) == 'string' then
local name = easing
easing = tween.easing[name]
if type(easing) ~= 'function' then
error("The easing function name '" .. name .. "' is invalid")
end
end
return easing
end
local function performEasingOnSubject(subject, target, initial, clock, duration, easing)
local t,b,c,d
for k,v in pairs(target) do
if type(v) == 'table' then
performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing)
else
t,b,c,d = clock, initial[k], v - initial[k], duration
subject[k] = easing(t,b,c,d)
end
end
end
-- Tween methods
local Tween = {}
local Tween_mt = {__index = Tween}
function Tween:set(clock)
assert(type(clock) == 'number', "clock must be a positive number or 0")
self.initial = self.initial or copyTables({}, self.target, self.subject)
self.clock = clock
if self.clock <= 0 then
self.clock = 0
copyTables(self.subject, self.initial)
elseif self.clock >= self.duration then -- the tween has expired
self.clock = self.duration
copyTables(self.subject, self.target)
else
performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing)
end
return self.clock >= self.duration
end
function Tween:reset()
return self:set(0)
end
function Tween:update(dt)
assert(type(dt) == 'number', "dt must be a number")
return self:set(self.clock + dt)
end
-- Public interface
function tween.new(duration, subject, target, easing)
easing = getEasingFunction(easing)
checkNewParams(duration, subject, target, easing)
return setmetatable({
duration = duration,
subject = subject,
target = target,
easing = easing,
clock = 0
}, Tween_mt)
end
return tween

View File

@@ -1,591 +0,0 @@
local Util = { }
function Util.tryTimed(timeout, f, ...)
local c = os.clock()
repeat
local ret = f(...)
if ret then
return ret
end
until os.clock()-c >= timeout
end
function Util.tryTimes(attempts, f, ...)
local result
for i = 1, attempts do
result = { f(...) }
if result[1] then
return unpack(result)
end
end
return unpack(result)
end
function Util.throttle(fn)
local ts = os.clock()
local timeout = .095
return function(...)
local nts = os.clock()
if nts > ts + timeout then
os.sleep(0)
ts = os.clock()
if fn then
fn(...)
end
end
end
end
function Util.tostring(pattern, ...)
local function serialize(tbl, width)
local str = '{\n'
for k, v in pairs(tbl) do
local value
if type(v) == 'table' then
value = string.format('table: %d', Util.size(v))
else
value = tostring(v)
end
str = str .. string.format(' %s: %s\n', k, value)
end
if #str < width then
str = str:gsub('\n', '') .. ' }'
else
str = str .. '}'
end
return str
end
if type(pattern) == 'string' then
return string.format(pattern, ...)
elseif type(pattern) == 'table' then
return serialize(pattern, term.current().getSize())
end
return tostring(pattern)
end
function Util.print(pattern, ...)
print(Util.tostring(pattern, ...))
end
function Util.getVersion()
local version
if _CC_VERSION then
version = tonumber(_CC_VERSION:gmatch('[%d]+%.?[%d][%d]', '%1')())
end
if not version and _HOST then
version = tonumber(_HOST:gmatch('[%d]+%.?[%d][%d]', '%1')())
end
return version or 1.7
end
-- http://lua-users.org/wiki/SimpleRound
function Util.round(num, idp)
local mult = 10^(idp or 0)
return math.floor(num * mult + 0.5) / mult
end
function Util.random(max, min)
min = min or 0
return math.random(0, max-min) + min
end
--[[ Table functions ]] --
function Util.clear(t)
local keys = Util.keys(t)
for _,k in pairs(keys) do
t[k] = nil
end
end
function Util.empty(t)
return not next(t)
end
function Util.key(t, value)
for k,v in pairs(t) do
if v == value then
return k
end
end
end
function Util.keys(t)
local keys = {}
for k in pairs(t) do
keys[#keys+1] = k
end
return keys
end
function Util.merge(obj, args)
if args then
for k,v in pairs(args) do
obj[k] = v
end
end
end
function Util.deepMerge(obj, args)
if args then
for k,v in pairs(args) do
if type(v) == 'table' then
if not obj[k] then
obj[k] = { }
end
Util.deepMerge(obj[k], v)
else
obj[k] = v
end
end
end
end
function Util.transpose(t)
local tt = { }
for k,v in pairs(t) do
tt[v] = k
end
return tt
end
function Util.find(t, name, value)
for k,v in pairs(t) do
if v[name] == value then
return v, k
end
end
end
function Util.findAll(t, name, value)
local rt = { }
for k,v in pairs(t) do
if v[name] == value then
table.insert(rt, v)
end
end
return rt
end
function Util.shallowCopy(t)
local t2 = { }
for k,v in pairs(t) do
t2[k] = v
end
return t2
end
function Util.deepCopy(t)
if type(t) ~= 'table' then
return t
end
--local mt = getmetatable(t)
local res = {}
for k,v in pairs(t) do
if type(v) == 'table' then
v = Util.deepCopy(v)
end
res[k] = v
end
--setmetatable(res,mt)
return res
end
-- http://snippets.luacode.org/?p=snippets/Filter_a_table_in-place_119
function Util.filterInplace(t, predicate)
local j = 1
for i = 1,#t do
local v = t[i]
if predicate(v) then
t[j] = v
j = j + 1
end
end
while t[j] ~= nil do
t[j] = nil
j = j + 1
end
return t
end
function Util.filter(it, f)
local ot = { }
for k,v in pairs(it) do
if f(k, v) then
ot[k] = v
end
end
return ot
end
function Util.size(list)
if type(list) == 'table' then
local length = 0
table.foreach(list, function() length = length + 1 end)
return length
end
return 0
end
function Util.removeByValue(t, e)
for k,v in pairs(t) do
if v == e then
table.remove(t, k)
break
end
end
end
function Util.each(list, func)
for index, value in pairs(list) do
func(value, index, list)
end
end
-- http://stackoverflow.com/questions/15706270/sort-a-table-in-lua
function Util.spairs(t, order)
local keys = Util.keys(t)
-- if order function given, sort by it by passing the table and keys a, b,
-- otherwise just sort the keys
if order then
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
else
table.sort(keys)
end
-- return the iterator function
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], t[keys[i]]
end
end
end
function Util.first(t, order)
local keys = Util.keys(t)
if order then
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
else
table.sort(keys)
end
return keys[1], t[keys[1]]
end
--[[ File functions ]]--
function Util.readFile(fname)
local f = fs.open(fname, "r")
if f then
local t = f.readAll()
f.close()
return t
end
end
function Util.writeFile(fname, data)
local file = io.open(fname, "w")
if not file then
error('Unable to open ' .. fname, 2)
end
file:write(data)
file:close()
end
function Util.readLines(fname)
local file = fs.open(fname, "r")
if file then
local t = {}
local line = file.readLine()
while line do
table.insert(t, line)
line = file.readLine()
end
file.close()
return t
end
end
function Util.writeLines(fname, lines)
local file = fs.open(fname, 'w')
if file then
for _,line in ipairs(lines) do
line = file.writeLine(line)
end
file.close()
return true
end
end
function Util.readTable(fname)
local t = Util.readFile(fname)
if t then
return textutils.unserialize(t)
end
end
function Util.writeTable(fname, data)
Util.writeFile(fname, textutils.serialize(data))
end
function Util.loadTable(fname)
local fc = Util.readFile(fname)
if not fc then
return false, 'Unable to read file'
end
local s, m = loadstring('return ' .. fc, fname)
if s then
s, m = pcall(s)
if s then
return m
end
end
return s, m
end
--[[ loading and running functions ]] --
function Util.download(url, filename)
local h = http.get(url)
if not h then
error('Failed to download ' .. url)
end
local contents = h.readAll()
h.close()
if not contents then
error('Failed to download ' .. url)
end
if filename then
Util.writeFile(filename, contents)
end
return contents
end
function Util.loadUrl(url, env) -- loadfile equivalent
local s, m = pcall(function()
local c = Util.download(url)
return load(c, url, nil, env)
end)
if s then
return m
end
return s, m
end
function Util.runUrl(env, url, ...) -- os.run equivalent
setmetatable(env, { __index = _G })
local fn, m = Util.loadUrl(url, env)
if fn then
local args = { ... }
return pcall(function() return fn(table.unpack(args)) end)
end
return fn, m
end
function Util.run(env, path, ...)
setmetatable(env, { __index = _G })
local fn, m = loadfile(path, env)
if fn then
local args = { ... }
return pcall(function() return fn(table.unpack(args)) end)
end
return fn, m
end
function Util.runFunction(env, fn, ...)
setfenv(fn, env)
setmetatable(env, { __index = _G })
local args = { ... }
return pcall(function() return fn(table.unpack(args)) end)
end
--[[ String functions ]] --
function Util.toBytes(n)
if n >= 1000000 or n <= -1000000 then
return string.format('%sM', Util.round(n/1000000, 1))
elseif n >= 1000 or n <= -1000 then
return string.format('%sK', Util.round(n/1000, 1))
end
return tostring(n)
end
function Util.insertString(os, is, pos)
return os:sub(1, pos - 1) .. is .. os:sub(pos)
end
function Util.split(str, pattern)
pattern = pattern or "(.-)\n"
local t = {}
local function helper(line) table.insert(t, line) return "" end
helper((str:gsub(pattern, helper)))
return t
end
function Util.matches(str, pattern)
pattern = pattern or '%S+'
local t = { }
for s in str:gmatch(pattern) do
table.insert(t, s)
end
return t
end
function Util.startsWidth(s, match)
return string.sub(s, 1, #match) == match
end
function Util.widthify(s, len)
s = s or ''
local slen = #s
if slen < len then
s = s .. string.rep(' ', len - #s)
elseif slen > len then
s = s:sub(1, len)
end
return s
end
-- http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
function Util.trim(s)
return s:find'^%s*$' and '' or s:match'^%s*(.*%S)'
end
-- trim whitespace from left end of string
function Util.triml(s)
return s:match'^%s*(.*)'
end
-- trim whitespace from right end of string
function Util.trimr(s)
return s:find'^%s*$' and '' or s:match'^(.*%S)'
end
-- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
-- word wrapping based on:
-- https://www.rosettacode.org/wiki/Word_wrap#Lua and
-- http://lua-users.org/wiki/StringRecipes
local function paragraphwrap(text, linewidth, res)
linewidth = linewidth or 75
local spaceleft = linewidth
local line = { }
for word in text:gmatch("%S+") do
local len = #word + 1
--if colorMode then
-- word:gsub('()@([@%d])', function(pos, c) len = len - 2 end)
--end
if len > spaceleft then
table.insert(res, table.concat(line, ' '))
line = { word }
spaceleft = linewidth - len - 1
else
table.insert(line, word)
spaceleft = spaceleft - len
end
end
table.insert(res, table.concat(line, ' '))
return table.concat(res, '\n')
end
-- end word wrapping
function Util.wordWrap(str, limit)
local longLines = Util.split(str)
local lines = { }
for _,line in ipairs(longLines) do
paragraphwrap(line, limit, lines)
end
return lines
end
-- http://lua-users.org/wiki/AlternativeGetOpt
local function getopt( arg, options )
local tab = {}
for k, v in ipairs(arg) do
if type(v) == 'string' then
if string.sub( v, 1, 2) == "--" then
local x = string.find( v, "=", 1, true )
if x then tab[ string.sub( v, 3, x-1 ) ] = string.sub( v, x+1 )
else tab[ string.sub( v, 3 ) ] = true
end
elseif string.sub( v, 1, 1 ) == "-" then
local y = 2
local l = string.len(v)
local jopt
while ( y <= l ) do
jopt = string.sub( v, y, y )
if string.find( options, jopt, 1, true ) then
if y < l then
tab[ jopt ] = string.sub( v, y+1 )
y = l
else
tab[ jopt ] = arg[ k + 1 ]
end
else
tab[ jopt ] = true
end
y = y + 1
end
end
end
end
return tab
end
function Util.showOptions(options)
print('Arguments: ')
for k, v in pairs(options) do
print(string.format('-%s %s', v.arg, v.desc))
end
end
function Util.getOptions(options, args, ignoreInvalid)
local argLetters = ''
for _,o in pairs(options) do
if o.type ~= 'flag' then
argLetters = argLetters .. o.arg
end
end
local rawOptions = getopt(args, argLetters)
local argCount = 0
for k,ro in pairs(rawOptions) do
local found = false
for _,o in pairs(options) do
if o.arg == k then
found = true
if o.type == 'number' then
o.value = tonumber(ro)
elseif o.type == 'help' then
Util.showOptions(options)
return false
else
o.value = ro
end
end
end
if not found and not ignoreInvalid then
print('Invalid argument')
Util.showOptions(options)
return false
end
end
return true, Util.size(rawOptions)
end
return Util

View File

@@ -1,410 +1,545 @@
requireInjector(getfenv(1)) local Config = require('opus.config')
local Event = require('opus.event')
local pastebin = require('opus.http.pastebin')
local UI = require('opus.ui')
local Util = require('opus.util')
local Config = require('config') local colors = _G.colors
local Event = require('event') local fs = _G.fs
local UI = require('ui') local multishell = _ENV.multishell
local Util = require('util') local os = _G.os
local shell = _ENV.shell
local FILE = 1
multishell.setTitle(multishell.getCurrent(), 'Files')
UI:configure('Files', ...) UI:configure('Files', ...)
local config = { local config = Config.load('Files', {
showHidden = false, showHidden = false,
showDirSizes = false, showDirSizes = false,
})
config.associations = config.associations or {
nft = 'pain',
txt = 'edit',
} }
Config.load('Files', config)
local copied = { } local copied = { }
local marked = { } local marked = { }
local directories = { } local directories = { }
local cutMode = false local cutMode = false
function formatSize(size) local function formatSize(size)
if size >= 1000000 then if size >= 1000000 then
return string.format('%dM', math.floor(size/1000000, 2)) return string.format('%dM', math.floor(size/1000000, 2))
elseif size >= 1000 then elseif size >= 1000 then
return string.format('%dK', math.floor(size/1000, 2)) return string.format('%dK', math.floor(size/1000, 2))
end end
return size return size
end end
local Browser = UI.Page { local Browser = UI.Page {
menuBar = UI.MenuBar { menuBar = UI.MenuBar {
buttons = { buttons = {
{ text = '^-', event = 'updir' }, { text = '^-', event = 'updir' },
{ text = 'File', event = 'dropdown', dropdown = 'fileMenu' }, { text = 'File', dropdown = {
{ text = 'Edit', event = 'dropdown', dropdown = 'editMenu' }, { text = 'Run', event = 'run', flags = FILE },
{ text = 'View', event = 'dropdown', dropdown = 'viewMenu' }, { text = 'Edit e', event = 'edit', flags = FILE },
}, { text = 'Cloud edit c', event = 'cedit', flags = FILE },
}, { text = 'Pastebin put p', event = 'pastebin', flags = FILE },
fileMenu = UI.DropMenu { { text = 'Shell s', event = 'shell' },
buttons = { { spacer = true },
{ text = 'Run', event = 'run' }, { text = 'Quit ^q', event = 'quit' },
{ text = 'Edit e', event = 'edit' }, } },
{ text = 'Shell s', event = 'shell' }, { text = 'Edit', dropdown = {
UI.Text { value = ' ------------ ' }, { text = 'Cut ^x', event = 'cut' },
{ text = 'Quit q', event = 'quit' }, { text = 'Copy ^c', event = 'copy' },
UI.Text { }, { text = 'Copy path ', event = 'copy_path' },
} { text = 'Paste ^v', event = 'paste' },
}, { spacer = true },
editMenu = UI.DropMenu { { text = 'Mark m', event = 'mark' },
buttons = { { text = 'Unmark all u', event = 'unmark' },
{ text = 'Cut ^x', event = 'cut' }, { spacer = true },
{ text = 'Copy ^c', event = 'copy' }, { text = 'Delete del', event = 'delete' },
{ text = 'Paste ^v', event = 'paste' }, } },
UI.Text { value = ' --------------- ' }, { text = 'View', dropdown = {
{ text = 'Mark m', event = 'mark' }, { text = 'Refresh r', event = 'refresh' },
{ text = 'Unmark all u', event = 'unmark' }, { text = 'Hidden ^h', event = 'toggle_hidden' },
UI.Text { value = ' --------------- ' }, { text = 'Dir Size ^s', event = 'toggle_dirSize' },
{ text = 'Delete del', event = 'delete' }, } },
UI.Text { }, { text = '\187',
} x = -3,
}, dropdown = {
viewMenu = UI.DropMenu { { text = 'Associations', event = 'associate' },
buttons = { } },
{ text = 'Refresh r', event = 'refresh' }, },
{ text = 'Hidden ^h', event = 'toggle_hidden' }, },
{ text = 'Dir Size ^s', event = 'toggle_dirSize' }, grid = UI.ScrollingGrid {
UI.Text { }, columns = {
} { heading = 'Name', key = 'name' },
}, { key = 'flags', width = 3, textColor = 'lightGray' },
grid = UI.ScrollingGrid { { heading = 'Size', key = 'fsize', width = 5, textColor = 'yellow' },
columns = { },
{ heading = 'Name', key = 'name' }, sortColumn = 'name',
{ key = 'flags', width = 2 }, y = 2, ey = -2,
{ heading = 'Size', key = 'fsize', width = 6 }, sortCompare = function(self, a, b)
}, if self.sortColumn == 'fsize' then
sortColumn = 'name', return a.size < b.size
y = 2, ey = -2, elseif self.sortColumn == 'flags' then
}, return a.flags < b.flags
statusBar = UI.StatusBar { end
columns = { if a.isDir == b.isDir then
{ key = 'status' }, return a.name:lower() < b.name:lower()
{ key = 'totalSize', width = 6 }, end
}, return a.isDir
}, end,
accelerators = { getRowTextColor = function(_, file)
q = 'quit', if file.marked then
e = 'edit', return colors.green
s = 'shell', end
r = 'refresh', if file.isDir then
space = 'mark', return colors.cyan
backspace = 'updir', end
m = 'move', if file.isReadOnly then
u = 'unmark', return colors.pink
d = 'delete', end
delete = 'delete', return colors.white
[ 'control-h' ] = 'toggle_hidden', end,
[ 'control-x' ] = 'cut', eventHandler = function(self, event)
[ 'control-c' ] = 'copy', if event.type == 'copy' then -- let copy be handled by parent
paste = 'paste', return false
}, end
return UI.ScrollingGrid.eventHandler(self, event)
end
},
statusBar = UI.StatusBar {
columns = {
{ key = 'status' },
{ key = 'totalSize', width = 6 },
},
draw = function(self)
if self.parent.dir then
local info = '#:' .. Util.size(self.parent.dir.files)
local numMarked = Util.size(marked)
if numMarked > 0 then
info = info .. ' M:' .. numMarked
end
self:setValue('info', info)
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
UI.StatusBar.draw(self)
end
end,
},
question = UI.Question {
y = -2, x = -19,
label = 'Delete',
},
notification = UI.Notification { },
associations = UI.SlideOut {
menuBar = UI.MenuBar {
buttons = {
{ text = 'Save', event = 'save' },
{ text = 'Cancel', event = 'cancel' },
},
},
grid = UI.ScrollingGrid {
x = 2, ex = -6, y = 3, ey = -8,
columns = {
{ heading = 'Extension', key = 'name' },
{ heading = 'Program', key = 'value' },
},
autospace = true,
sortColumn = 'name',
accelerators = {
delete = 'remove_entry',
},
},
remove = UI.Button {
x = -4, y = 6,
text = '-', event = 'remove_entry', help = 'Remove',
},
[1] = UI.Window {
x = 2, y = -6, ex = -6, ey = -3,
},
form = UI.Form {
x = 3, y = -5, ex = -7, ey = -3,
margin = 1,
manualControls = true,
[1] = UI.TextEntry {
width = 20,
formLabel = 'Extension', formKey = 'name',
shadowText = 'extension',
required = true,
limit = 64,
},
[2] = UI.TextEntry {
width = 20,
formLabel = 'Program', formKey = 'value',
shadowText = 'program',
required = true,
},
add = UI.Button {
x = -11, y = 1,
text = 'Add', event = 'add_association',
},
},
statusBar = UI.StatusBar { },
},
accelerators = {
[ 'control-q' ] = 'quit',
c = 'cedit',
e = 'edit',
s = 'shell',
p = 'pastebin',
r = 'refresh',
[ ' ' ] = 'mark',
m = 'mark',
backspace = 'updir',
u = 'unmark',
d = 'delete',
delete = 'delete',
[ 'control-h' ] = 'toggle_hidden',
[ 'control-s' ] = 'toggle_dirSize',
[ 'control-x' ] = 'cut',
[ 'control-c' ] = 'copy',
paste = 'paste',
},
} }
function Browser:enable() function Browser:enable()
UI.Page.enable(self) UI.Page.enable(self)
self:setFocus(self.grid) self:setFocus(self.grid)
end end
function Browser.grid:sortCompare(a, b) function Browser.menuBar.getActive(_, menuItem)
if self.sortColumn == 'fsize' then local file = Browser.grid:getSelected()
return a.size < b.size if menuItem.flags == FILE then
elseif self.sortColumn == 'flags' then return file and not file.isDir
return a.flags < b.flags end
end return true
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end
function Browser.grid:getRowTextColor(file, selected)
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:getRowBackgroundColorX(file, selected)
if selected then
return colors.gray
end
return self.backgroundColor
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 end
function Browser:setStatus(status, ...) function Browser:setStatus(status, ...)
self.statusBar:timedStatus(string.format(status, ...)) self.notification:info(string.format(status, ...))
end end
function Browser:unmarkAll() function Browser.unmarkAll()
for k,m in pairs(marked) do for _,m in pairs(marked) do
m.marked = false m.marked = false
end end
Util.clear(marked) Util.clear(marked)
end end
function Browser:getDirectory(directory) function Browser:getDirectory(directory)
local s, dir = pcall(function()
local s, dir = pcall(function() local dir = directories[directory]
if not dir then
dir = {
name = directory,
size = 0,
files = { },
totalSize = 0,
index = 1
}
directories[directory] = dir
end
local dir = directories[directory] self:updateDirectory(dir)
if not dir then
dir = {
name = directory,
size = 0,
files = { },
totalSize = 0,
index = 1
}
directories[directory] = dir
end
self:updateDirectory(dir) return dir
end)
return dir return s, dir
end)
return s, dir
end end
function Browser:updateDirectory(dir) function Browser:updateDirectory(dir)
dir.size = 0
dir.totalSize = 0
Util.clear(dir.files)
dir.size = 0 local files = fs.listEx(dir.name)
dir.totalSize = 0 if files then
Util.clear(dir.files) dir.size = #files
for _, file in pairs(files) do
file.fullName = fs.combine(dir.name, file.name)
file.flags = file.fstype or ' '
if not file.isDir then
dir.totalSize = dir.totalSize + file.size
file.fsize = formatSize(file.size)
file.flags = file.flags .. ' '
else
if config.showDirSizes then
file.size = fs.getSize(file.fullName, true)
local files = fs.listEx(dir.name) dir.totalSize = dir.totalSize + file.size
if files then file.fsize = formatSize(file.size)
dir.size = #files end
for _, file in pairs(files) do file.flags = file.flags .. 'D'
file.fullName = fs.combine(dir.name, file.name) end
file.directory = directory file.flags = file.flags .. (file.isReadOnly and 'R' or ' ')
file.flags = '' if config.showHidden or file.name:sub(1, 1) ~= '.' then
if not file.isDir then dir.files[file.fullName] = file
dir.totalSize = dir.totalSize + file.size end
file.fsize = formatSize(file.size) end
else end
if config.showDirSizes then
file.size = fs.getSize(file.fullName, true)
dir.totalSize = dir.totalSize + file.size
file.fsize = formatSize(file.size)
end
file.flags = 'D'
end
if file.isReadOnly then
file.flags = file.flags .. 'R'
end
if config.showHidden or file.name:sub(1, 1) ~= '.' then
dir.files[file.fullName] = file
end
end
end
-- self.grid:update() -- self.grid:update()
-- self.grid:setIndex(dir.index) -- self.grid:setIndex(dir.index)
self.grid:setValues(dir.files) self.grid:setValues(dir.files)
end end
function Browser:setDir(dirName, noStatus) function Browser:setDir(dirName, noStatus)
self:unmarkAll()
self:unmarkAll() if self.dir then
self.dir.index = self.grid:getIndex()
end
local DIR = fs.combine('', dirName)
shell.setDir(DIR)
local s, dir = self:getDirectory(DIR)
if s then
self.dir = dir
elseif noStatus then
error(dir)
else
self:setStatus(dir)
self:setDir('', true)
return
end
if self.dir then if not noStatus then
self.dir.index = self.grid:getIndex() self.statusBar:setValue('status', '/' .. self.dir.name)
end self.statusBar:draw()
DIR = fs.combine('', dirName) end
shell.setDir(DIR) self.grid:setIndex(self.dir.index)
local s, dir = self:getDirectory(DIR)
if s then
self.dir = dir
elseif noStatus then
error(dir)
else
self:setStatus(dir)
self:setDir('', true)
return
end
if not noStatus then
self.statusBar:setValue('status', '/' .. self.dir.name)
self.statusBar:draw()
end
self.grid:setIndex(self.dir.index)
end end
function Browser:run(path, ...) function Browser:run(...)
local tabId = shell.openTab(path, ...) if multishell then
multishell.setFocus(tabId) local tabId = shell.openTab(...)
multishell.setFocus(tabId)
else
shell.run(...)
Event.terminate = false
self:draw()
end
end end
function Browser:hasMarked() function Browser:hasMarked()
if Util.size(marked) == 0 then if Util.size(marked) == 0 then
local file = self.grid:getSelected() local file = self.grid:getSelected()
if file then if file then
file.marked = true file.marked = true
marked[file.fullName] = file marked[file.fullName] = file
self.grid:draw() self.grid:draw()
end end
end end
return Util.size(marked) > 0 return Util.size(marked) > 0
end end
function Browser:eventHandler(event) 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)
elseif event.type == 'shell' then elseif event.type == 'cedit' and file then
self:run('sys/apps/shell') self:run('cedit', file.name)
self:setStatus('Started cloud edit')
elseif event.type == 'refresh' then elseif event.type == 'shell' then
self:updateDirectory(self.dir) self:run('shell')
self.grid:draw()
self:setStatus('Refreshed')
elseif event.type == 'toggle_hidden' then elseif event.type == 'refresh' then
config.showHidden = not config.showHidden self:updateDirectory(self.dir)
Config.update('Files', config) self.grid:draw()
self:setStatus('Refreshed')
self:updateDirectory(self.dir) elseif event.type == 'associate' then
self.grid:draw() self.associations:show()
if not config.showHidden then
self:setStatus('Hiding hidden')
else
self:setStatus('Displaying hidden')
end
elseif event.type == 'toggle_dirSize' then elseif event.type == 'pastebin' then
config.showDirSizes = not config.showDirSizes if file and not file.isDir then
Config.update('Files', config) local s, m = pastebin.put(file.fullName)
if s then
os.queueEvent('clipboard_copy', s)
self.notification:success(string.format('Uploaded as %s', s), 0)
else
self.notification:error(m)
end
end
self:updateDirectory(self.dir) elseif event.type == 'toggle_hidden' then
self.grid:draw() config.showHidden = not config.showHidden
if config.showDirSizes then Config.update('Files', config)
self:setStatus('Displaying dir sizes')
end
elseif event.type == 'mark' and file then self:updateDirectory(self.dir)
file.marked = not file.marked self.grid:draw()
if file.marked then if not config.showHidden then
marked[file.fullName] = file self:setStatus('Hiding hidden')
else else
marked[file.fullName] = nil self:setStatus('Displaying hidden')
end end
self.grid:draw()
self.statusBar:draw()
elseif event.type == 'unmark' then elseif event.type == 'toggle_dirSize' then
self:unmarkAll() config.showDirSizes = not config.showDirSizes
self.grid:draw() Config.update('Files', config)
self:setStatus('Marked files cleared')
elseif event.type == 'grid_select' or event.type == 'run' then self:updateDirectory(self.dir)
if file then self.grid:draw()
if file.isDir then if config.showDirSizes then
self:setDir(file.fullName) self:setStatus('Displaying dir sizes')
else end
self:run(file.name)
end
end
elseif event.type == 'updir' then elseif event.type == 'mark' and file then
local dir = (self.dir.name:match("(.*/)")) file.marked = not file.marked
self:setDir(dir or '/') if file.marked then
marked[file.fullName] = file
else
marked[file.fullName] = nil
end
self.grid:draw()
self.statusBar:draw()
elseif event.type == 'delete' then elseif event.type == 'unmark' then
if self:hasMarked() then self:unmarkAll()
local width = self.statusBar:getColumnWidth('status') self.grid:draw()
self.statusBar:setColumnWidth('status', UI.term.width) self:setStatus('Marked files cleared')
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 k,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() elseif event.type == 'grid_select' or event.type == 'run' then
self.grid:draw() if file then
self:setFocus(self.grid) if file.isDir then
end self:setDir(file.fullName)
else
local ext = file.name:match('%.(%w+)$')
if ext and config.associations[ext] then
self:run(config.associations[ext], '/' .. file.fullName)
else
self:run(file.name)
end
end
end
elseif event.type == 'copy' or event.type == 'cut' then elseif event.type == 'updir' then
if self:hasMarked() then local dir = (self.dir.name:match("(.*/)"))
cutMode = event.type == 'cut' self:setDir(dir or '/')
Util.clear(copied)
Util.merge(copied, marked)
--self:unmarkAll()
self.grid:draw()
self:setStatus('Copied %d file(s)', Util.size(copied))
end
elseif event.type == 'paste' then elseif event.type == 'delete' then
for k,m in pairs(copied) do if self:hasMarked() then
local s, m = pcall(function() self.question:show()
if cutMode then end
fs.move(m.fullName, fs.combine(self.dir.name, m.name)) return true
else
fs.copy(m.fullName, fs.combine(self.dir.name, m.name))
end
end)
end
self:updateDirectory(self.dir)
self.grid:draw()
self:setStatus('Pasted ' .. Util.size(copied) .. ' file(s)')
else elseif event.type == 'question_yes' then
return UI.Page.eventHandler(self, event) for _,m in pairs(marked) do
end pcall(fs.delete, m.fullName)
self:setFocus(self.grid) end
return true marked = { }
self:updateDirectory(self.dir)
self.question:hide()
self.statusBar:draw()
self.grid:draw()
self:setFocus(self.grid)
elseif event.type == 'question_no' then
self.question:hide()
self:setFocus(self.grid)
elseif event.type == 'copy' or event.type == 'cut' then
if self:hasMarked() then
cutMode = event.type == 'cut'
Util.clear(copied)
Util.merge(copied, marked)
--self:unmarkAll()
self.grid:draw()
self:setStatus('Copied %d file(s)', Util.size(copied))
end
elseif event.type == 'copy_path' then
if file then
os.queueEvent('clipboard_copy', file.fullName)
end
elseif event.type == 'paste' then
for _,m in pairs(copied) do
pcall(function()
if cutMode then
fs.move(m.fullName, fs.combine(self.dir.name, m.name))
else
fs.copy(m.fullName, fs.combine(self.dir.name, m.name))
end
end)
end
self:updateDirectory(self.dir)
self.grid:draw()
self:setStatus('Pasted ' .. Util.size(copied) .. ' file(s)')
else
return UI.Page.eventHandler(self, event)
end
self:setFocus(self.grid)
return true
end
--[[ Associations slide out ]] --
function Browser.associations:show()
self.grid.values = { }
for k, v in pairs(config.associations) do
table.insert(self.grid.values, {
name = k,
value = v,
})
end
self.grid:update()
UI.SlideOut.show(self)
self:setFocus(self.form[1])
end
function Browser.associations:eventHandler(event)
if event.type == 'remove_entry' then
local row = self.grid:getSelected()
if row then
Util.removeByValue(self.grid.values, row)
self.grid:update()
self.grid:draw()
end
elseif event.type == 'add_association' then
if self.form:save() then
local entry = Util.find(self.grid.values, 'name', self.form[1].value) or { }
entry.name = self.form[1].value
entry.value = self.form[2].value
table.insert(self.grid.values, entry)
self.form[1]:reset()
self.form[2]:reset()
self.grid:update()
self.grid:draw()
end
elseif event.type == 'cancel' then
self:hide()
elseif event.type == 'save' then
config.associations = { }
for _, v in pairs(self.grid.values) do
config.associations[v.name] = v.value
end
Config.update('Files', config)
self:hide()
else
return UI.SlideOut.eventHandler(self, event)
end
return true
end end
--[[-- Startup logic --]]-- --[[-- Startup logic --]]--
local args = { ... } 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,80 +1,96 @@
requireInjector(getfenv(1)) local fuzzy = require('opus.fuzzy')
local UI = require('opus.ui')
local Util = require('opus.util')
local Event = require('event') local help = _G.help
local UI = require('ui')
multishell.setTitle(multishell.getCurrent(), 'Help')
UI:configure('Help', ...) UI:configure('Help', ...)
local files = { } local topics = { }
for _,f in pairs(help.topics()) do for _,topic in pairs(help.topics()) do
table.insert(files, { name = f }) table.insert(topics, { name = topic, lname = topic:lower() })
end end
local page = 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,
}, },
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 4, y = 4,
values = files, values = topics,
columns = { columns = {
{ heading = 'Name', key = 'name' }, { heading = 'Topic', key = 'name' },
}, },
sortColumn = 'name', sortColumn = 'lname',
}, },
accelerators = { accelerators = {
q = 'quit', [ 'control-q' ] = 'quit',
enter = 'grid_select', enter = 'grid_select',
}, },
} eventHandler = function(self, event)
if event.type == 'quit' then
UI:quit()
local function showHelp(name) elseif event.type == 'grid_select' then
UI.term:reset() if self.grid:getSelected() then
shell.run('help ' .. name) UI:setPage('topic', self.grid:getSelected().name)
print('Press enter to return') end
repeat
os.pullEvent('key')
local _, k = os.pullEvent('key_up')
until k == keys.enter
end
function page:eventHandler(event) elseif event.type == 'text_change' then
if not event.text then
self.grid.sortColumn = 'lname'
else
self.grid.sortColumn = 'score'
self.grid.inverseSort = false
local pattern = event.text:lower()
for _,v in pairs(self.grid.values) do
v.score = -fuzzy(v.lname, pattern)
end
end
self.grid:update()
self.grid:setIndex(1)
self.grid:draw()
if event.type == 'quit' then else
Event.exitPullEvents() return UI.Page.eventHandler(self, event)
end
end,
})
elseif event.type == 'grid_select' then UI:addPage('topic', UI.Page {
if self.grid:getSelected() then backgroundColor = 'black',
showHelp(self.grid:getSelected().name) titleBar = UI.TitleBar {
self:setFocus(self.filter) title = 'text',
self:draw() event = 'back',
end },
helpText = UI.TextArea {
x = 2, ex = -1, y = 3, ey = -2,
},
accelerators = {
[ 'control-q' ] = 'back',
backspace = 'back',
},
enable = function(self, name)
local f = help.lookup(name)
elseif event.type == 'text_change' then self.titleBar.title = name
local text = event.text self.helpText:setText(f and Util.readFile(f) or 'No help available for ' .. name)
if #text == 0 then
self.grid.values = files
else
self.grid.values = { }
for _,f in pairs(files) do
if string.find(f.name, text) then
table.insert(self.grid.values, f)
end
end
end
self.grid:update()
self.grid:setIndex(1)
self.grid:draw()
else
UI.Page.eventHandler(self, event)
end
end
UI:setPage(page) return UI.Page.enable(self)
UI:pullEvents() end,
eventHandler = function(self, event)
if event.type == 'back' then
UI:setPage('main')
end
return UI.Page.eventHandler(self, event)
end,
})
local args = Util.parse(...)
UI:setPage(args[1] and 'topic' or 'main', args[1])
UI:start()

View File

@@ -1,317 +1,371 @@
requireInjector = requireInjector or load(http.get('https://raw.githubusercontent.com/kepler155c/opus/master/sys/apis/injector.lua').readAll())() local History = require('opus.history')
requireInjector(getfenv(1)) local UI = require('opus.ui')
local Util = require('opus.util')
local Event = require('event') local colors = _G.colors
local History = require('history') local os = _G.os
local UI = require('ui') local textutils = _G.textutils
local Util = require('util') local term = _G.term
local sandboxEnv = setmetatable(Util.shallowCopy(getfenv(1)), { __index = _G }) local sandboxEnv = setmetatable(Util.shallowCopy(_ENV), { __index = _G })
sandboxEnv.exit = function() Event.exitPullEvents() end sandboxEnv.exit = function() UI:quit() end
sandboxEnv._echo = function( ... ) return ... end sandboxEnv._echo = function( ... ) return { ... } end
requireInjector(sandboxEnv) _G.requireInjector(sandboxEnv)
multishell.setTitle(multishell.getCurrent(), 'Lua')
UI:configure('Lua', ...) UI:configure('Lua', ...)
local command = '' local command = ''
local counter = 1
local history = History.load('usr/.lua_history', 25) local history = History.load('usr/.lua_history', 25)
local page = UI.Page { local page = UI.Page {
menuBar = UI.MenuBar { menuBar = UI.MenuBar {
buttons = { buttons = {
{ text = 'Local', event = 'local' }, { text = 'Local', event = 'local' },
{ text = 'Global', event = 'global' }, { text = 'Global', event = 'global' },
{ text = 'Device', event = 'device', name = 'Device' }, { text = 'Device', event = 'device', name = 'Device' },
}, },
}, },
prompt = UI.TextEntry { prompt = UI.TextEntry {
y = 2, y = 2,
shadowText = 'enter command', shadowText = 'enter command',
limit = 256, accelerators = {
accelerators = { enter = 'command_enter',
enter = 'command_enter', up = 'history_back',
up = 'history_back', down = 'history_forward',
down = 'history_forward', mouse_rightclick = 'clear_prompt',
mouse_rightclick = 'clear_prompt', [ 'control-space' ] = 'autocomplete',
-- [ 'control-space' ] = 'autocomplete', },
}, },
}, tabs = UI.Tabs {
grid = UI.ScrollingGrid { y = 3,
y = 3, formatted = UI.Tab {
columns = { title = 'Formatted',
{ heading = 'Key', key = 'name' }, index = 1,
{ heading = 'Value', key = 'value' }, grid = UI.ScrollingGrid {
}, columns = {
sortColumn = 'name', { heading = 'Key', key = 'name' },
autospace = true, { heading = 'Value', key = 'value' },
}, },
notification = UI.Notification(), sortColumn = 'name',
autospace = true,
},
},
output = UI.Tab {
title = 'Output',
index = 2,
backgroundColor = 'black',
output = UI.Embedded {
y = 2,
maxScroll = 1000,
backgroundColor = 'black',
},
draw = function(self)
self:write(1, 1, string.rep('\131', self.width), 'black', 'primary')
self:drawChildren()
end,
},
},
} }
page.grid = page.tabs.formatted.grid
page.output = page.tabs.output.output
function page:setPrompt(value, focus) function page:setPrompt(value, focus)
self.prompt:setValue(value) self.prompt:setValue(value)
self.prompt.scroll = 0
self.prompt:setPosition(#value)
self.prompt:updateScroll()
if value:sub(-1) == ')' then if value:sub(-1) == ')' then
self.prompt:setPosition(#value - 1) self.prompt:setPosition(#value - 1)
end else
self.prompt:setPosition(#value)
end
self.prompt:draw() self.prompt:draw()
if focus then if focus then
page:setFocus(self.prompt) page:setFocus(self.prompt)
end end
end end
function page:enable() function page:enable()
self:setFocus(self.prompt) UI.Page.enable(self)
UI.Page.enable(self) self:setFocus(self.prompt)
end end
local function autocomplete(env, oLine, x) local function autocomplete(env, oLine, x)
local sLine = oLine:sub(1, x)
local nStartPos = sLine:find("[a-zA-Z0-9_%.]+$")
if nStartPos then
sLine = sLine:sub(nStartPos)
end
local sLine = oLine:sub(1, x) if #sLine > 0 then
local nStartPos = sLine:find("[a-zA-Z0-9_%.]+$") local results = textutils.complete(sLine, env)
if nStartPos then
sLine = sLine:sub(nStartPos)
end
if #sLine > 0 then if #results == 1 then
local results = textutils.complete(sLine, env) return Util.insertString(oLine, results[1], x + 1)
if #results == 0 then elseif #results > 1 then
-- setError('No completions available') local prefix = results[1]
for n = 1, #results do
elseif #results == 1 then local result = results[n]
return Util.insertString(oLine, results[1], x + 1) while #prefix > 0 do
if result:find(prefix, 1, true) == 1 then
elseif #results > 1 then break
local prefix = results[1] end
for n = 1, #results do prefix = prefix:sub(1, #prefix - 1)
local result = results[n] end
while #prefix > 0 do end
if result:find(prefix, 1, true) == 1 then if #prefix > 0 then
break return Util.insertString(oLine, prefix, x + 1)
end end
prefix = prefix:sub(1, #prefix - 1) end
end end
end return oLine
if #prefix > 0 then
return Util.insertString(oLine, prefix, x + 1)
else
-- setStatus('Too many results')
end
end
end
return oLine
end end
function page:eventHandler(event) function page:eventHandler(event)
if event.type == 'global' then
self:setPrompt('_G', true)
self:executeStatement('_G')
command = nil
if event.type == 'global' then elseif event.type == 'local' then
self:setPrompt('', true) self:setPrompt('_ENV', true)
self:executeStatement('getfenv(0)') self:executeStatement('_ENV')
command = nil command = nil
elseif event.type == 'local' then elseif event.type == 'tab_select' then
self:setPrompt('', true) self:setFocus(self.prompt)
self:executeStatement('getfenv(1)')
command = nil
elseif event.type == 'autocomplete' then elseif event.type == 'show_output' then
local sz = #self.prompt.value self.tabs:selectTab(self.tabs.output)
local pos = self.prompt.pos
self:setPrompt(autocomplete(sandboxEnv, self.prompt.value, self.prompt.pos))
self.prompt:setPosition(pos + #self.prompt.value - sz)
self.prompt:updateCursor()
elseif event.type == 'device' then elseif event.type == 'autocomplete' then
if not _G.device then local value = self.prompt.value or ''
sandboxEnv.device = { } local sz = #value
for _,side in pairs(peripheral.getNames()) do local pos = self.prompt.entry.pos
local key = string.format('%s:%s', peripheral.getType(side), side) self:setPrompt(autocomplete(sandboxEnv, value, self.prompt.entry.pos))
sandboxEnv.device[ key ] = peripheral.wrap(side) self.prompt:setPosition(pos + #(self.prompt.value or '') - sz)
end self.prompt:updateCursor()
end
self:setPrompt('device', true)
self:executeStatement('device')
elseif event.type == 'history_back' then elseif event.type == 'device' then
local value = history:back() self:setPrompt('device', true)
if value then self:executeStatement('device')
self:setPrompt(value)
end
elseif event.type == 'history_forward' then elseif event.type == 'history_back' then
self:setPrompt(history:forward() or '') local value = history:back()
if value then
self:setPrompt(value)
end
elseif event.type == 'clear_prompt' then elseif event.type == 'history_forward' then
self:setPrompt('') self:setPrompt(history:forward() or '')
history:reset()
elseif event.type == 'command_enter' then elseif event.type == 'clear_prompt' then
local s = tostring(self.prompt.value) self:setPrompt('')
history:reset()
if #s > 0 then elseif event.type == 'command_enter' then
history:add(s) local s = tostring(self.prompt.value or '')
history:back()
self:executeStatement(s)
else
local t = { }
for k = #history.entries, 1, -1 do
table.insert(t, {
name = #t + 1,
value = history.entries[k],
isHistory = true,
pos = k,
})
end
history:reset()
command = nil
self.grid:setValues(t)
self.grid:setIndex(1)
self.grid:adjustWidth()
self:draw()
end
return true
else if #s > 0 then
return UI.Page.eventHandler(self, event) self:executeStatement(s)
end else
return true local t = { }
for k = #history.entries, 1, -1 do
table.insert(t, {
name = #t + 1,
value = history.entries[k],
isHistory = true,
pos = k,
})
end
history:reset()
command = nil
self.grid:setValues(t)
self.grid:setIndex(1)
self.grid:draw()
end
return true
else
return UI.Page.eventHandler(self, event)
end
return true
end end
function page:setResult(result) function page:setResult(result)
local t = { } local t = { }
local function safeValue(v) local function safeValue(v)
local t = type(v) if type(v) == 'string' or type(v) == 'number' then
if t == 'string' or t == 'number' then return v
return v end
end return tostring(v)
return tostring(v) end
end
if type(result) == 'table' then if type(result) == 'table' then
for k,v in pairs(result) do for k,v in pairs(result) do
local entry = { local entry = {
name = safeValue(k), name = safeValue(k),
rawName = k, rawName = k,
value = safeValue(v), value = safeValue(v),
rawValue = v, rawValue = v,
} }
if type(v) == 'table' then if type(v) == 'table' then
if Util.size(v) == 0 then if Util.size(v) == 0 then
entry.value = 'table: (empty)' entry.value = 'table: (empty)'
else else
entry.value = 'table' entry.value = tostring(v)
end end
end end
table.insert(t, entry) table.insert(t, entry)
end end
else else
table.insert(t, { table.insert(t, {
name = type(result), name = type(result),
value = tostring(result), value = tostring(result),
rawValue = result, rawValue = 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)
local entry = self:getSelected()
local entry = self:getSelected() local function commandAppend()
if entry.isHistory then
--history.setPosition(entry.pos)
return entry.value
end
if type(entry.rawValue) == 'function' then
if command then
return command .. '.' .. entry.name .. '()'
end
return entry.name .. '()'
end
if command then
if type(entry.rawName) == 'number' then
return command .. '[' .. entry.name .. ']'
end
if entry.name:match("%W") or
entry.name:sub(1, 1):match("%d") then
return command .. "['" .. tostring(entry.name) .. "']"
end
return command .. '.' .. entry.name
end
return entry.name
end
local function commandAppend() if event.type == 'grid_focus_row' then
if entry.isHistory then if self.focused then
--history.setPosition(entry.pos) page:setPrompt(commandAppend())
return entry.value end
end elseif event.type == 'grid_select' then
if type(entry.rawValue) == 'function' then page:setPrompt(commandAppend(), true)
if command then page:executeStatement(commandAppend())
return command .. '.' .. entry.name .. '()'
end
return entry.name .. '()'
end
if command then
if type(entry.rawName) == 'number' then
return command .. '[' .. entry.name .. ']'
end
if entry.name:match("%W") or
entry.name:sub(1, 1):match("%d") then
return command .. "['" .. tostring(entry.name) .. "']"
end
return command .. '.' .. entry.name
end
return entry.name
end
if event.type == 'grid_focus_row' then elseif event.type == 'copy' then
if self.focused then if entry then
page:setPrompt(commandAppend()) os.queueEvent('clipboard_copy', entry.rawValue)
end end
elseif event.type == 'grid_select' then else
page:setPrompt(commandAppend(), true) return UI.ScrollingGrid.eventHandler(self, event)
page:executeStatement(commandAppend()) end
elseif event.type == 'copy' then return true
if entry then
clipboard.setData(entry.rawValue)
end
else
return UI.ScrollingGrid.eventHandler(self, event)
end
return true
end end
function page:rawExecute(s) function page:rawExecute(s)
local fn, m = load('return _echo(' ..s.. ');', 'lua', nil, sandboxEnv) local fn, m
if fn then local wrapped
m = { pcall(fn) }
fn = table.remove(m, 1)
if #m == 1 then
m = m[1]
end
return fn, m
end
fn, m = load(s, 'lua', nil, sandboxEnv) fn = load('return (' ..s.. ')', 'lua', nil, sandboxEnv)
if fn then
fn, m = pcall(fn)
end
return fn, m if fn then
fn = load('return {' ..s.. '}', 'lua', nil, sandboxEnv)
wrapped = true
end
local t = os.clock()
if fn then
fn, m = pcall(fn)
if #m <= 1 and wrapped then
m = m[1]
end
else
fn, m = load(s, 'lua', nil, sandboxEnv)
if fn then
t = os.clock()
fn, m = pcall(fn)
end
end
if fn then
t = os.clock() - t
local bg, fg = term.getBackgroundColor(), term.getTextColor()
term.setTextColor(colors.cyan)
term.setBackgroundColor(colors.black)
term.write(string.format('out [%.2f]: ', t))
term.setBackgroundColor(bg)
term.setTextColor(fg)
if m or wrapped then
Util.print(m or 'nil')
else
print()
end
else
_G.printError(m)
end
return fn, m
end end
function page:executeStatement(statement) function page:executeStatement(statement)
command = statement
command = statement history:add(statement)
history:back()
local s, m = self:rawExecute(command) local s, m
local oterm = term.redirect(self.output.win)
self.output.win.scrollBottom()
local bg, fg = term.getBackgroundColor(), term.getTextColor()
term.setBackgroundColor(colors.black)
term.setTextColor(colors.green)
term.write(string.format('in [%d]: ', counter))
term.setBackgroundColor(bg)
term.setTextColor(fg)
print(tostring(statement))
if s and m then pcall(function()
self:setResult(m) s, m = self:rawExecute(command)
else end)
self.grid:setValues({ })
self.grid:draw() term.redirect(oterm)
if m then counter = counter + 1
self.notification:error(m, 5)
end if s and type(m) ~= "nil" then
end self:setResult(m)
else
self.grid:setValues({ })
self.grid:draw()
if m and not self.output.enabled then
self:emit({ type = 'show_output' })
end
end
end end
local args = { ... } local args = Util.parse(...)
if args[1] then 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)
Event.pullEvents() UI:start()
UI.term:reset()

View File

@@ -1,146 +1,281 @@
requireInjector(getfenv(1)) local Config = require('opus.config')
local Event = require('opus.event')
local Socket = require('opus.socket')
local UI = require('opus.ui')
local Util = require('opus.util')
local Event = require('event') local device = _G.device
local Socket = require('socket') local network = _G.network
local UI = require('ui') local shell = _ENV.shell
local Util = require('util')
multishell.setTitle(multishell.getCurrent(), 'Network')
UI:configure('Network', ...) UI:configure('Network', ...)
local gridColumns = { local gridColumns = {
{ heading = 'Label', key = 'label' }, { heading = 'Label', key = 'label' },
{ heading = 'Dist', key = 'distance' }, { heading = 'Dist', key = 'distance', align = 'right' },
{ heading = 'Status', key = 'status' }, { heading = 'Status', key = 'status' },
} }
local config = Config.load('network', { })
if UI.term.width >= 30 then if UI.term.width >= 30 then
table.insert(gridColumns, { heading = 'Fuel', key = 'fuel', width = 5 }) table.insert(gridColumns, { heading = 'Fuel', key = 'fuel', width = 5, align = 'right' })
table.insert(gridColumns, { heading = 'Uptime', key = 'uptime' }) end
if UI.term.width >= 40 then
table.insert(gridColumns, { heading = 'Uptime', key = 'uptime', align = 'right' })
end end
local page = UI.Page { local page = UI.Page {
menuBar = UI.MenuBar { menuBar = UI.MenuBar {
buttons = { buttons = {
{ text = 'Telnet', event = 'telnet' }, { text = 'Connect', dropdown = {
{ text = 'VNC', event = 'vnc' }, { text = 'Telnet t', event = 'telnet' },
{ text = 'Trust', event = 'trust' }, { text = 'VNC v', event = 'vnc' },
{ text = 'Reboot', event = 'reboot' }, { spacer = true },
}, { text = 'Reboot r', event = 'reboot' },
}, } },
grid = UI.ScrollingGrid { { text = 'Trust', dropdown = {
y = 2, { text = 'Establish', event = 'trust' },
values = network, } },
columns = gridColumns, {
sortColumn = 'label', text = '\187',
autospace = true, x = -3,
}, dropdown = {
notification = UI.Notification { }, { text = 'Port Status', event = 'ports', modem = true },
accelerators = { { spacer = true },
q = 'quit', { text = 'Help', event = 'help', noCheck = true },
c = 'clear', },
}, },
},
},
grid = UI.ScrollingGrid {
y = 2,
values = network,
columns = gridColumns,
sortColumn = 'label',
autospace = true,
getRowTextColor = function(self, row, selected)
if not row.active then
return 'lightGray'
end
return UI.Grid.getRowTextColor(self, row, selected)
end,
getDisplayValues = function(_, row)
row = Util.shallowCopy(row)
if row.uptime then
if row.uptime < 60 then
row.uptime = string.format("%ds", math.floor(row.uptime))
elseif row.uptime < 3600 then
row.uptime = string.format("%sm", math.floor(row.uptime / 60))
else
row.uptime = string.format("%sh", math.floor(row.uptime / 3600))
end
end
if row.fuel then
row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or ''
end
if row.distance then
row.distance = Util.toBytes(Util.round(row.distance, 1))
end
return row
end,
},
ports = UI.SlideOut {
titleBar = UI.TitleBar {
title = 'Ports',
event = 'ports_hide',
},
menuBar = UI.MenuBar {
y = 2,
buttons = {
{ text = 'Refresh', event = 'ports_update' },
}
},
grid = UI.ScrollingGrid {
y = 3,
columns = {
{ heading = 'Port', key = 'port' },
{ heading = 'State', key = 'state' },
{ heading = 'Connection', key = 'connection' },
},
sortColumn = 'port',
autospace = true,
},
eventHandler = function(self, event)
if event.type == 'grid_select' then
shell.openForegroundTab('Sniff ' .. event.selected.port)
end
return UI.SlideOut.eventHandler(self, event)
end,
},
notification = UI.Notification { },
accelerators = {
t = 'telnet',
v = 'vnc',
r = 'reboot',
[ 'control-q' ] = 'quit',
c = 'clear',
},
} }
local function sendCommand(host, command) local function sendCommand(host, command)
if not device.wireless_modem then
page.notification:error('Wireless modem not present')
return
end
if not device.wireless_modem then page.notification:info('Connecting')
page.notification:error('Wireless modem not present') page:sync()
return
end
page.notification:info('Connecting') local socket = Socket.connect(host, 161)
page:sync() if socket then
socket:write({ type = command })
socket:close()
page.notification:success('Command sent')
else
page.notification:error('Failed to connect')
end
end
local socket = Socket.connect(host, 161) function page.ports.grid:update()
if socket then local transport = network:getTransport()
socket:write({ type = command })
socket:close() local function findConnection(port)
page.notification:success('Command sent') if transport then
else for _,socket in pairs(transport.sockets) do
page.notification:error('Failed to connect') if socket.sport == port then
end return socket
end
end
end
end
local connections = { }
pcall(function() -- guard against modem removal
if device.wireless_modem then
for i = 0, 65535 do
if device.wireless_modem.isOpen(i) then
local conn = {
port = i
}
local socket = findConnection(i)
if socket then
conn.state = 'CONNECTED'
local host = socket.dhost
if network[host] then
host = network[host].label
end
conn.connection = host .. ':' .. socket.dport
else
conn.state = 'LISTEN'
end
table.insert(connections, conn)
end
end
end
end)
self.values = connections
UI.Grid.update(self)
end end
function page:eventHandler(event) function page:eventHandler(event)
local t = self.grid:getSelected() local t = self.grid:getSelected()
if t then if t then
if event.type == 'telnet' or event.type == 'grid_select' then if event.type == 'telnet' then
multishell.openTab({ shell.openForegroundTab('telnet ' .. t.id)
path = 'sys/apps/telnet.lua',
focused = true, elseif event.type == 'vnc' then
args = { t.id }, shell.openForegroundTab('vnc.lua ' .. t.id)
title = t.label, --[[
}) os.queueEvent('overview_shortcut', {
elseif event.type == 'vnc' then title = t.label,
multishell.openTab({ category = "VNC",
path = 'sys/apps/vnc.lua', icon = "\010\030 \009\009\031e\\\031 \031e/\031dn\010\030 \009\009 \031e\\/\031 \031bc",
focused = true, run = "vnc.lua " .. t.id,
args = { t.id }, })
title = t.label, --]]
})
elseif event.type == 'trust' then elseif event.type == 'clear' then
shell.openForegroundTab('trust ' .. t.id) Util.clear(network)
elseif event.type == 'reboot' then page.grid:update()
sendCommand(t.id, 'reboot') page.grid:draw()
elseif event.type == 'shutdown' then
sendCommand(t.id, 'shutdown') elseif event.type == 'trust' then
end shell.openForegroundTab('trust ' .. t.id)
end
if event.type == 'quit' then elseif event.type == 'reboot' then
Event.exitPullEvents() sendCommand(t.id, 'reboot')
end
UI.Page.eventHandler(self, event) elseif event.type == 'shutdown' then
sendCommand(t.id, 'shutdown')
end
end
if event.type == 'help' then
shell.switchTab(shell.openTab('Help Networking'))
elseif event.type == 'ports' then
self.ports.grid:update()
self.ports:show()
-- self.portsHandler = Event.onInterval(3, function()
-- self.ports.grid:update()
-- self.ports.grid:draw()
-- self:sync()
-- end)
elseif event.type == 'ports_update' then
self.ports.grid:update()
self.ports.grid:draw()
self:sync()
elseif event.type == 'ports_hide' then
Event.off(self.portsHandler)
self.ports:hide()
elseif event.type == 'show_trusted' then
config.showTrusted = true
Config.update('network', config)
elseif event.type == 'quit' then
UI:quit()
end
UI.Page.eventHandler(self, event)
end end
function page.grid:getRowTextColor(row, selected) function page.menuBar:getActive(menuItem)
if not row.active then local t = page.grid:getSelected()
return colors.orange if menuItem.modem then
end return not not device.wireless_modem
return UI.Grid.getRowTextColor(self, row, selected) end
end return menuItem.noCheck or not not t
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))
else
row.uptime = string.format("%sm", math.floor(row.uptime/6)/10)
end
end
if row.fuel then
row.fuel = Util.toBytes(row.fuel)
end
if row.distance then
row.distance = Util.round(row.distance, 1)
end
return row
end end
Event.onInterval(1, function() Event.onInterval(1, function()
page.grid:update() page.grid:update()
page.grid:draw() page.grid:draw()
page:sync() page:sync()
end) end)
Event.on('device_attach', function(h, deviceName) Event.on('device_attach', function(_, deviceName)
if deviceName == 'wireless_modem' then if deviceName == 'wireless_modem' then
page.notification:success('Modem connected') page.notification:success('Modem connected')
page:sync() page:sync()
end end
end) end)
Event.on('device_detach', function(h, deviceName) Event.on('device_detach', function(_, deviceName)
if deviceName == 'wireless_modem' then if deviceName == 'wireless_modem' then
page.notification:error('Wireless modem not attached') page.notification:error('Wireless modem not attached')
page:sync() page:sync()
end end
end) end)
if not device.wireless_modem then if not device.wireless_modem then
page.notification:error('Wireless modem not attached') page.notification:error('Wireless modem not attached')
end end
UI:setPage(page) UI:setPage(page)
UI:pullEvents() UI:start()

File diff suppressed because it is too large Load Diff

302
sys/apps/PackageManager.lua Normal file
View File

@@ -0,0 +1,302 @@
local Ansi = require('opus.ansi')
local Config = require('opus.config')
local Packages = require('opus.packages')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local term = _G.term
UI:configure('PackageManager', ...)
local config = Config.load('package')
local page = UI.Page {
grid = UI.ScrollingGrid {
x = 2, ex = 14, y = 2, ey = -6,
values = { },
columns = {
{ heading = ' Package', key = 'displayName' },
},
sortColumn = 'name',
autospace = true,
help = 'Space to select, Enter to toggle',
},
installSelected = UI.Button {
x = 2, y = -3,
text = ' + ',
event = 'batch_action',
operation = 'install',
operationText = 'Install',
help = 'Install or update selected',
},
removeSelected = UI.Button {
x = 8, y = -3,
text = ' - ',
event = 'batch_action',
operation = 'uninstall',
operationText = 'Remove',
help = 'Remove selected',
},
updateall = UI.Button {
ex = -2, y = -3, width = 12,
text = 'Update All',
event = 'updateall',
help = 'Update all installed packages',
},
description = UI.TextArea {
x = 16, y = 3, ey = -5,
marginRight = 2, marginLeft = 0,
},
UI.Checkbox {
x = 3, y = -5,
label = 'Compress',
textColor = 'yellow',
backgroundColor = 'primary',
value = config.compression,
help = 'Compress packages (experimental)',
},
action = UI.SlideOut {
titleBar = UI.TitleBar {
event = 'hide-action',
},
button = UI.Button {
x = -10, y = 3,
text = ' Begin ', event = 'begin',
},
output = UI.Embedded {
y = 5, ey = -2, x = 2, ex = -2,
visible = true,
},
},
statusBar = UI.StatusBar { },
accelerators = {
[ 'control-q' ] = 'quit',
},
}
function page:loadPackages()
self.grid.values = { }
self.statusBar:setStatus('Downloading...')
self:sync()
for k in pairs(Packages:list()) do
local manifest = Packages:getManifest(k)
if not manifest then
manifest = {
invalid = true,
description = 'Unable to download manifest',
title = '',
}
end
table.insert(self.grid.values, {
installed = not not Packages:isInstalled(k),
selected = false,
name = k,
displayName = k,
manifest = manifest,
})
end
self.grid:update()
self.grid:setIndex(1)
self.grid:emit({
type = 'grid_focus_row',
selected = self.grid:getSelected(),
element = self.grid,
})
self.statusBar:setStatus('Updated packages')
end
function page.grid:getRowTextColor(row, selected)
if row.selected then
return colors.cyan
end
if row.installed then
return colors.yellow
end
return UI.Grid.getRowTextColor(self, row, selected)
end
function page:getSelectedPackages()
local selected = { }
for _, row in pairs(self.grid.values) do
if row.selected then
table.insert(selected, row)
end
end
return selected
end
function page:getSelectedCount()
local count = 0
for _, row in pairs(self.grid.values) do
if row.selected then
count = count + 1
end
end
return count
end
function page:toggleSelection(row)
row.selected = not row.selected
row.displayName = row.selected and ('\187 ' .. row.name) or row.name
self.grid:draw()
self:updateStatus()
end
function page:clearSelection()
for _, row in pairs(self.grid.values) do
row.selected = false
row.displayName = row.name
end
self.grid:draw()
end
function page:updateStatus()
local count = self:getSelectedCount()
if count > 0 then
self.statusBar:setStatus(count .. ' package(s) selected')
else
local focused = self.grid:getSelected()
if focused then
self.statusBar:setStatus(focused.installed and 'Installed' or '')
end
end
end
function page.action:show()
self.output.win:clear()
UI.SlideOut.show(self)
end
function page:run(operation, name)
local oterm = term.redirect(self.action.output.win)
self.action.output:clear()
local cmd = string.format('package %s %s', operation, name)
term.setCursorPos(1, 1)
term.clear()
term.setTextColor(colors.yellow)
print(cmd .. '\n')
term.setTextColor(colors.white)
local s, m = Util.run(_ENV, '/sys/apps/package.lua', operation, name)
if not s and m then
_G.printError(m)
end
term.redirect(oterm)
self.action.output:draw()
end
function page:updateSelection(selected)
-- no-op: buttons are always active for batch operations
end
function page:eventHandler(event)
if event.type == 'focus_change' then
self.statusBar:setStatus(event.focused.help)
elseif event.type == 'grid_focus_row' then
local manifest = event.selected.manifest
self.description:setValue(string.format('%s%s\n\n%s%s',
Ansi.yellow, manifest.title,
Ansi.white, manifest.description))
self.description:draw()
self:updateStatus()
elseif event.type == 'grid_select' then
-- Space or Enter toggles selection
local row = self.grid:getSelected()
if row then
self:toggleSelection(row)
end
elseif event.type == 'checkbox_change' then
config.compression = not config.compression
Config.update('package', config)
elseif event.type == 'updateall' then
self.operation = 'updateall'
self.operationTargets = { }
self.action.button.text = ' Begin '
self.action.button.event = 'begin'
self.action.titleBar.title = 'Update All'
self.action:show()
elseif event.type == 'batch_action' then
local targets = self:getSelectedPackages()
local operation = event.button.operation
-- fall back to focused row if nothing selected
if #targets == 0 then
local focused = self.grid:getSelected()
if focused then
targets = { focused }
end
end
if #targets == 0 then return end
-- for install: update already-installed packages, install new ones
if operation == 'install' then
self.operation = 'install'
else
-- filter to only installed packages for uninstall
local installed = { }
for _, t in ipairs(targets) do
if t.installed then
table.insert(installed, t)
end
end
targets = installed
if #targets == 0 then return end
self.operation = 'uninstall'
end
self.operationTargets = targets
local title = #targets == 1
and targets[1].manifest.title
or (#targets .. ' packages')
self.action.button.text = ' Begin '
self.action.button.event = 'begin'
self.action.titleBar.title = string.format('%s %s',
operation == 'install' and 'Install/Update' or 'Remove', title)
self.action:show()
elseif event.type == 'hide-action' then
self.action:hide()
elseif event.type == 'begin' then
if self.operation == 'updateall' then
self:run('updateall', '')
else
for _, target in ipairs(self.operationTargets) do
local op = self.operation
if op == 'install' and target.installed then
op = 'update'
end
self:run(op, target.name)
target.installed = Packages:isInstalled(target.name)
end
self:clearSelection()
self:updateStatus()
end
self.action.button.text = ' Done '
self.action.button.event = 'hide-action'
self.action.button:draw()
elseif event.type == 'quit' then
UI:quit()
end
UI.Page.eventHandler(self, event)
end
UI:setPage(page)
page.statusBar:setStatus('Downloading...')
page:sync()
Packages:downloadList()
page:loadPackages()
page:sync()
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

@@ -0,0 +1,28 @@
local kernel = _G.kernel
local os = _G.os
local shell = _ENV.shell
local launcherTab = kernel.getCurrent()
launcherTab.noFocus = true
kernel.hook('kernel_focus', function(_, eventData)
local focusTab = eventData and eventData[1]
if focusTab == launcherTab.uid then
local previousTab = eventData[2]
local nextTab = launcherTab
if not previousTab then
for _, v in pairs(kernel.routines) do
if not v.hidden and v.uid > nextTab.uid then
nextTab = v
end
end
end
if nextTab == launcherTab then
shell.switchTab(shell.openTab('shell'))
else
shell.switchTab(nextTab.uid)
end
end
end)
os.pullEventRaw('kernel_halt')

388
sys/apps/Sniff.lua Normal file
View File

@@ -0,0 +1,388 @@
local UI = require('opus.ui')
local Event = require('opus.event')
local Util = require('opus.util')
local colors = _G.colors
local device = _G.device
local textutils = _G.textutils
local multishell = _ENV.multishell
local gridColumns = {}
table.insert(gridColumns, { heading = '#', key = 'id', width = 5, align = 'right' })
table.insert(gridColumns, { heading = 'Port', key = 'portid', width = 5, align = 'right' })
table.insert(gridColumns, { heading = 'Reply', key = 'replyid', width = 5, align = 'right' })
if UI.term.width > 50 then
table.insert(gridColumns, { heading = 'Dist', key = 'distance', width = 6, align = 'right' })
end
table.insert(gridColumns, { heading = 'Msg', key = 'packetStr' })
local page = UI.Page {
paused = false,
index = 1,
notification = UI.Notification { },
accelerators = { ['control-q'] = 'quit' },
menuBar = UI.MenuBar {
buttons = {
{ text = 'Pause', event = 'pause_click', name = 'pauseButton' },
{ text = 'Clear', event = 'clear_click' },
{ text = 'Config', event = 'config_click' },
},
},
packetGrid = UI.ScrollingGrid {
y = 2,
maxPacket = 300,
inverseSort = true,
sortColumn = 'id',
columns = gridColumns,
accelerators = { ['space'] = 'pause_click' },
},
configSlide = UI.SlideOut {
y = -11,
titleBar = UI.TitleBar { title = 'Sniffer Config', event = 'config_close', backgroundColor = colors.black },
accelerators = { ['backspace'] = 'config_close' },
configTabs = UI.Tabs {
y = 2,
filterTab = UI.Tab {
title = 'Filter',
noFill = true,
filterGridText = UI.Text {
x = 2, y = 2,
value = 'ID filter',
},
filterGrid = UI.ScrollingGrid {
x = 2, y = 3,
width = 10, height = 4,
disableHeader = true,
columns = {
{ key = 'id', width = 5 },
},
},
filterEntry = UI.TextEntry {
x = 2, y = 8,
width = 7,
shadowText = 'ID',
limit = 5,
accelerators = { enter = 'filter_add' },
},
filterAdd = UI.Button {
x = 10, y = 8,
text = '+',
event = 'filter_add',
},
filterAllCheck = UI.Checkbox {
x = 14, y = 8,
value = false,
},
filterAddText = UI.Text {
x = 18, y = 8,
value = "Use ID filter",
},
rangeText = UI.Text {
x = 15, y = 2,
value = "Distance filter",
},
rangeEntry = UI.TextEntry {
x = 15, y = 3,
width = 10,
limit = 8,
shadowText = 'Range',
transform = 'number',
},
},
modemTab = UI.Tab {
title = 'Modem',
channelGrid = UI.ScrollingGrid {
x = 2, y = 2,
width = 12, height = 5,
autospace = true,
columns = {{ heading = 'Open Ports', key = 'port' }},
},
modemGrid = UI.ScrollingGrid {
x = 15, y = 2,
ex = -2, height = 5,
autospace = true,
columns = {
{ heading = 'Side', key = 'side' },
{ heading = 'Type', key = 'type' },
},
},
channelEntry = UI.TextEntry {
x = 2, y = 8,
width = 7,
shadowText = 'ID',
limit = 5,
accelerators = { enter = 'channel_add' },
},
channelAdd = UI.Button {
x = 10, y = 8,
text = '+',
event = 'channel_add',
},
},
},
},
packetSlide = UI.SlideOut {
titleBar = UI.TitleBar {
title = 'Packet Information',
event = 'packet_close',
},
accelerators = {
['backspace'] = 'packet_close',
['left'] = 'prev_packet',
['right'] = 'next_packet',
},
packetMeta = UI.Grid {
x = 2, y = 2,
ex = 23, height = 4,
inactive = true,
columns = {
{ key = 'text' },
{ key = 'value', align = 'right', textColor = colors.yellow },
},
values = {
port = { text = 'Port' },
reply = { text = 'Reply' },
dist = { text = 'Distance' },
}
},
packetButton = UI.Button {
x = 25, y = 5,
text = 'Open in Lua',
event = 'packet_lua',
},
packetData = UI.TextArea {
y = 7, ey = -1,
backgroundColor = colors.black,
},
},
}
local filterConfig = page.configSlide.configTabs.filterTab
local modemConfig = page.configSlide.configTabs.modemTab
function filterConfig:eventHandler(event)
if event.type == 'filter_add' then
local id = tonumber(self.filterEntry.value)
if id then self.filterGrid.values[id] = { id = id }
self.filterGrid:update()
self.filterEntry:reset()
self:draw()
end
elseif event.type == 'grid_select' then
self.filterGrid.values[event.selected.id] = nil
self.filterGrid:update()
self.filterGrid:draw()
else return UI.Tab.eventHandler(self, event)
end
return true
end
function modemConfig:loadChannel()
for chan = 0, 65535 do
self.currentModem.openChannels[chan] = self.currentModem.device.isOpen(chan) and { port = chan } or nil
end
self.channelGrid:setValues(self.currentModem.openChannels)
self.currentModem.loaded = true
end
function modemConfig:enable()
if not self.currentModem.loaded then
self:loadChannel()
end
UI.Tab.enable(self)
end
function modemConfig:eventHandler(event)
if event.type == 'channel_add' then
local id = tonumber(modemConfig.channelEntry.value)
if id then
self.currentModem.openChannels[id] = { port = id }
self.currentModem.device.open(id)
self.channelGrid:setValues(self.currentModem.openChannels)
self.channelGrid:update()
self.channelEntry:reset()
self:draw()
end
elseif event.type == 'grid_select' then
if event.element == self.channelGrid then
self.currentModem.openChannels[event.selected.port] = nil
self.currentModem.device.close(event.selected.port)
self.channelGrid:setValues(self.currentModem.openChannels)
page.configSlide.configTabs.modemTab.channelGrid:update()
page.configSlide.configTabs.modemTab.channelGrid:draw()
elseif event.element == self.modemGrid then
self.currentModem = event.selected
page.notification:info("Loading channel list")
page:sync()
modemConfig:loadChannel()
page.notification:success("Now using modem on " .. self.currentModem.side)
self.channelGrid:draw()
end
else return UI.Tab.eventHandler(self, event)
end
return true
end
function page.packetSlide:setPacket(packet)
self.currentPacket = packet
local p, res = pcall(textutils.serialize, page.packetSlide.currentPacket.message)
self.packetData.textColor = p and colors.white or colors.red
self.packetData:setText(res)
self.packetMeta.values.port.value = page.packetSlide.currentPacket.portid
self.packetMeta.values.reply.value = page.packetSlide.currentPacket.replyid
self.packetMeta.values.dist.value = Util.round(page.packetSlide.currentPacket.distance, 2)
end
function page.packetSlide:show(packet)
self:setPacket(packet)
UI.SlideOut.show(self)
end
function page.packetSlide:eventHandler(event)
if event.type == 'packet_close' then
self:hide()
page:setFocus(page.packetGrid)
elseif event.type == 'packet_lua' then
multishell.openTab(_ENV, { path = 'sys/apps/Lua.lua', args = { self.currentPacket.message }, focused = true })
elseif event.type == 'prev_packet' then
local c = self.currentPacket
local n = page.packetGrid.values[c.id - 1]
if n then
self:setPacket(n)
self:draw()
end
elseif event.type == 'next_packet' then
local c = self.currentPacket
local n = page.packetGrid.values[c.id + 1]
if n then
self:setPacket(n)
self:draw()
end
else return UI.SlideOut.eventHandler(self, event)
end
return true
end
function page.packetGrid:getDisplayValues(row)
row = Util.shallowCopy(row)
row.distance = Util.toBytes(Util.round(row.distance), 2)
return row
end
function page.packetGrid:addPacket(packet)
if not page.paused and (packet.distance <= (filterConfig.rangeEntry.value or math.huge)) and (not filterConfig.filterAllCheck.value or filterConfig.filterGrid.values[packet.portid]) then
page.index = page.index + 1
local _, res = pcall(textutils.serialize, packet.message)
packet.packetStr = res:gsub("\n%s*", "")
table.insert(self.values, packet)
end
if #self.values > self.maxPacket then
local t = { }
for i = 10, #self.values do
t[i - 9] = self.values[i]
end
self:setValues(t)
end
self:update()
self:draw()
page:sync()
end
function page:enable()
modemConfig.modems = {}
Util.each(_G.device, function(dev)
if dev.type == "modem" then
modemConfig.modems[dev.side] = {
type = dev.isWireless() and 'Wireless' or 'Wired',
side = dev.side,
openChannels = { },
device = dev,
loaded = false
}
end
end)
modemConfig.currentModem = device.wireless_modem and
modemConfig.modems[device.wireless_modem.side] or
device.wired_modem and
modemConfig.modems[device.wired_modem.side] or
nil
modemConfig.modemGrid.values = modemConfig.modems
modemConfig.modemGrid:update()
modemConfig.modemGrid:setSelected(modemConfig.currentModem)
UI.Page.enable(self)
end
function page:eventHandler(event)
if event.type == 'pause_click' then
self.paused = not self.paused
self.menuBar.pauseButton.text = self.paused and 'Resume' or 'Pause'
self.notification:success(self.paused and 'Paused' or 'Resumed', 2)
self.menuBar:draw()
elseif event.type == 'clear_click' then
self.packetGrid:setValues({ })
self.notification:success('Cleared', 2)
self.packetGrid:draw()
elseif event.type == 'config_click' then
self.configSlide:show()
self:setFocus(filterConfig.filterEntry)
elseif event.type == 'config_close' then
self.configSlide:hide()
self:setFocus(self.packetGrid)
elseif event.type == 'grid_select' then
self.packetSlide:show(event.selected)
elseif event.type == 'quit' then
UI:quit()
else return UI.Page.eventHandler(self, event)
end
return true
end
Event.on('modem_message', function(_, side, chan, reply, msg, dist)
if modemConfig.currentModem.side == side then
page.packetGrid:addPacket({
id = page.index,
portid = chan,
replyid = reply,
message = msg,
distance = dist or -1,
})
end
end)
local args = Util.parse(...)
if args[1] then
local id = tonumber(args[1])
if id then
filterConfig.filterGrid.values[id] = { id = id }
filterConfig.filterAllCheck:setValue(true)
filterConfig.filterGrid:update()
end
end
UI:setPage(page)
UI:start()

View File

@@ -1,184 +1,82 @@
requireInjector(getfenv(1)) local UI = require('opus.ui')
local Util = require('opus.util')
local Config = require('config') local fs = _G.fs
local Event = require('event') local shell = _ENV.shell
local UI = require('ui')
local Util = require('util')
multishell.setTitle(multishell.getCurrent(), 'System')
UI:configure('System', ...) UI:configure('System', ...)
local env = { local function loadDirectory(dir)
path = shell.path(), local plugins = { }
aliases = shell.aliases(), for _, file in pairs(fs.list(dir)) do
lua_path = LUA_PATH, local s, m = Util.run(_ENV, fs.combine(dir, file))
} if not s and m then
Config.load('shell', env) _G.printError('Error loading: ' .. file)
error(m or 'Unknown error')
elseif s and m then
table.insert(plugins, { tab = m, name = m.title, description = m.description })
end
end
return plugins
end
local systemPage = UI.Page { local programDir = fs.getDir(_ENV.arg[0])
tabs = UI.Tabs { local plugins = loadDirectory(fs.combine(programDir, 'system'), { })
pathTab = UI.Window {
tabTitle = 'Path',
entry = UI.TextEntry {
x = 2, y = 2, ex = -2,
limit = 256,
value = shell.path(),
shadowText = 'enter system path',
accelerators = {
enter = 'update_path',
},
},
grid = UI.Grid {
y = 4,
values = paths,
disableHeader = true,
columns = { { key = 'value' } },
autospace = true,
},
},
aliasTab = UI.Window { local page = UI.Page {
tabTitle = 'Aliases', tabs = UI.Tabs {
alias = UI.TextEntry { settings = UI.Tab {
x = 2, y = 2, ex = -2, title = 'Category',
limit = 32, grid = UI.ScrollingGrid {
shadowText = 'Alias', x = 2, y = 2, ex = -2, ey = -2,
}, columns = {
path = UI.TextEntry { { heading = 'Name', key = 'name' },
y = 3, x = 2, ex = -2, { heading = 'Description', key = 'description' },
limit = 256, },
shadowText = 'Program path', sortColumn = 'name',
accelerators = { autospace = true,
enter = 'new_alias', values = plugins,
}, },
}, accelerators = {
grid = UI.Grid { grid_select = 'category_select',
y = 5, }
values = aliases, },
autospace = true, },
sortColumn = 'alias', notification = UI.Notification(),
columns = { accelerators = {
{ heading = 'Alias', key = 'alias' }, [ 'control-q' ] = 'quit',
{ heading = 'Program', key = 'path' }, },
}, eventHandler = function(self, event)
accelerators = { if event.type == 'quit' then
delete = 'delete_alias', UI:quit()
},
},
},
infoTab = UI.Window { elseif event.type == 'category_select' then
tabTitle = 'Info', local tab = event.selected.tab
labelText = UI.Text {
x = 3, y = 2, if not self.tabs[tab.title] then
value = 'Label' self.tabs:add({ [ tab.title ] = tab })
}, end
label = UI.TextEntry { self.tabs:selectTab(tab)
x = 9, y = 2, ex = -4, return true
limit = 32,
value = os.getComputerLabel(), elseif event.type == 'success_message' then
accelerators = { self.notification:success(event.message)
enter = 'update_label',
}, elseif event.type == 'info_message' then
}, self.notification:info(event.message)
grid = UI.ScrollingGrid {
y = 3, elseif event.type == 'error_message' then
values = { self.notification:error(event.message)
{ name = '', value = '' },
{ name = 'CC version', value = Util.getVersion() }, elseif event.type == 'tab_activate' then
{ name = 'Lua version', value = _VERSION }, event.activated:focusFirst()
{ name = 'MC version', value = _MC_VERSION or 'unknown' },
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) }, else
{ name = 'Computer ID', value = tostring(os.getComputerID()) }, return UI.Page.eventHandler(self, event)
{ name = 'Day', value = tostring(os.day()) }, end
}, return true
selectable = false, end,
columns = {
{ key = 'name', width = 12 },
{ key = 'value' },
},
},
},
},
notification = UI.Notification(),
accelerators = {
q = 'quit',
},
} }
function systemPage.tabs.pathTab.grid:draw() UI:setPage(page)
self.values = { } UI:start()
for _,v in ipairs(Util.split(env.path, '(.-):')) do
table.insert(self.values, { value = v })
end
self:update()
UI.Grid.draw(self)
end
function systemPage.tabs.pathTab:eventHandler(event)
if event.type == 'update_path' then
env.path = self.entry.value
self.grid:setIndex(self.grid:getIndex())
self.grid:draw()
Config.update('shell', env)
systemPage.notification:success('reboot to take effect')
return true
end
end
function systemPage.tabs.aliasTab.grid:draw()
self.values = { }
local aliases = { }
for k,v in pairs(env.aliases) do
table.insert(self.values, { alias = k, path = v })
end
self:update()
UI.Grid.draw(self)
end
function systemPage.tabs.aliasTab:eventHandler(event)
if event.type == 'delete_alias' then
env.aliases[self.grid:getSelected().alias] = nil
self.grid:setIndex(self.grid:getIndex())
self.grid:draw()
Config.update('shell', env)
systemPage.notification:success('reboot to take effect')
return true
elseif event.type == 'new_alias' then
env.aliases[self.alias.value] = self.path.value
self.alias:reset()
self.path:reset()
self:draw()
self:setFocus(self.alias)
Config.update('shell', env)
systemPage.notification:success('reboot to take effect')
return true
end
end
function systemPage.tabs.infoTab:eventHandler(event)
if event.type == 'update_label' then
os.setComputerLabel(self.label.value)
systemPage.notification:success('Label updated')
return true
end
end
function systemPage:eventHandler(event)
if event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'tab_activate' then
event.activated:focusFirst()
else
return UI.Page.eventHandler(self, event)
end
return true
end
UI:setPage(systemPage)
Event.pullEvents()
UI.term:reset()

View File

@@ -1,74 +0,0 @@
requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui')
local Util = require('util')
multishell.setTitle(multishell.getCurrent(), 'Tabs')
UI:configure('Tabs', ...)
local page = UI.Page {
menuBar = UI.MenuBar {
buttons = {
{ text = 'Activate', event = 'activate' },
{ text = 'Terminate', event = 'terminate' },
},
},
grid = UI.ScrollingGrid {
y = 2,
columns = {
{ heading = 'ID', key = 'tabId', width = 4 },
{ heading = 'Title', key = 'title' },
{ heading = 'Status', key = 'status' },
{ heading = 'Time', key = 'timestamp' },
},
values = multishell.getTabs(),
sortColumn = 'title',
autospace = true,
},
accelerators = {
q = 'quit',
space = 'activate',
t = 'terminate',
},
}
function page:eventHandler(event)
local t = self.grid:getSelected()
if t then
if event.type == 'activate' or event.type == 'grid_select' then
multishell.setFocus(t.tabId)
elseif event.type == 'terminate' then
multishell.terminate(t.tabId)
end
end
if event.type == 'quit' then
Event.exitPullEvents()
end
UI.Page.eventHandler(self, event)
end
function page.grid:getDisplayValues(row)
row = Util.shallowCopy(row)
local elapsed = os.clock()-row.timestamp
if elapsed < 60 then
row.timestamp = string.format("%ds", math.floor(elapsed))
else
row.timestamp = string.format("%sm", math.floor(elapsed/6)/10)
end
if row.isDead then
row.status = 'error'
else
row.status = coroutine.status(row.co)
end
return row
end
Event.onInterval(1, function()
page.grid:update()
page.grid:draw()
page:sync()
end)
UI:setPage(page)
UI:pullEvents()

75
sys/apps/Tasks.lua Normal file
View File

@@ -0,0 +1,75 @@
local Event = require('opus.event')
local UI = require('opus.ui')
local kernel = _G.kernel
local multishell = _ENV.multishell
local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines
UI:configure('Tasks', ...)
local page = UI.Page {
menuBar = UI.MenuBar {
buttons = {
{ text = 'Activate', event = 'activate' },
{ text = 'Terminate', event = 'terminate' },
{ text = 'Inspect', event = 'inspect' },
},
},
grid = UI.ScrollingGrid {
y = 2,
columns = {
{ heading = 'ID', key = 'uid', width = 3 },
{ heading = 'Title', key = 'title' },
{ heading = 'Status', key = 'status' },
{ heading = 'Time', key = 'timestamp' },
},
values = tasks,
sortColumn = 'uid',
autospace = true,
getDisplayValues = function (_, row)
local elapsed = os.clock()-row.timestamp
return {
uid = row.uid,
title = row.title,
status = row.isDead and 'error' or coroutine.status(row.co),
timestamp = elapsed < 60 and
string.format("%ds", math.floor(elapsed)) or
string.format("%sm", math.floor(elapsed/6)/10),
}
end
},
accelerators = {
[ 'control-q' ] = 'quit',
[ ' ' ] = 'activate',
t = 'terminate',
},
eventHandler = function (self, event)
local t = self.grid:getSelected()
if t then
if event.type == 'activate' or event.type == 'grid_select' then
multishell.setFocus(t.uid)
elseif event.type == 'terminate' then
multishell.terminate(t.uid)
elseif event.type == 'inspect' then
multishell.openTab(_ENV, {
path = 'sys/apps/Lua.lua',
args = { t },
focused = true,
})
end
end
if event.type == 'quit' then
UI:quit()
end
UI.Page.eventHandler(self, event)
end
}
Event.onInterval(1, function()
page.grid:update()
page.grid:draw()
page:sync()
end)
UI:setPage(page)
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()

179
sys/apps/Welcome.lua Normal file
View File

@@ -0,0 +1,179 @@
local Ansi = require('opus.ansi')
local Security = require('opus.security')
local SHA = require('opus.crypto.sha2')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local os = _G.os
local shell = _ENV.shell
local splashIntro = [[First Time Setup
%sThanks for installing Opus OS. The next screens will prompt you for basic settings for this computer.]]
local labelIntro = [[Set a friendly name for this computer.
%sNo spaces recommended.]]
local passwordIntro = [[A password is required for wireless access.
%sLeave blank to skip.]]
local packagesIntro = [[Optional Packages
%sSelect packages to install with this computer. You can always add more later from the Package Manager.]]
local contributorsIntro = [[Contributors%s
Anavrins: Encryption/security/custom apps
Community: Several selected applications
hugeblank: Startup screen improvements
LDDestroier: Art design + custom apps
Lemmmy: Application improvements
%sContribute at:%s
https://git.spatulaa.com/MayaTheShy/Opus]]
local page = UI.Page {
wizard = UI.Wizard {
ey = -2,
splash = UI.WizardPage {
index = 1,
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 2, ey = -2,
value = string.format(splashIntro, Ansi.white),
},
},
label = UI.WizardPage {
index = 2,
labelText = UI.Text {
x = 3, y = 2,
value = 'Label'
},
label = UI.TextEntry {
x = 9, y = 2, ex = -3,
limit = 32,
value = os.getComputerLabel(),
},
intro = UI.TextArea {
textColor = colors.yellow,
inactive = true,
x = 3, ex = -3, y = 4, ey = -3,
value = string.format(labelIntro, Ansi.white),
},
validate = function (self)
if self.label.value then
os.setComputerLabel(self.label.value)
end
return true
end,
},
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),
},
},
},
notification = UI.Notification { },
}
function page:eventHandler(event)
if event.type == 'skip' then
self.wizard:emit({ type = 'nextView' })
elseif event.type == 'view_enabled' then
event.view:focusFirst()
elseif event.type == 'packages' then
shell.openForegroundTab('PackageManager')
elseif event.type == 'wizard_complete' or event.type == 'cancel' then
UI:quit()
else
return UI.Page.eventHandler(self, event)
end
return true
end
UI:setPage(page)
UI:start()

70
sys/apps/autorun.lua Normal file
View File

@@ -0,0 +1,70 @@
local Packages = require('opus.packages')
local colors = _G.colors
local fs = _G.fs
local keys = _G.keys
local multishell = _ENV.multishell
local os = _G.os
local shell = _ENV.shell
local term = _G.term
local success = true
local function runDir(directory)
if not fs.exists(directory) then
return true
end
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 result then
if term.isColor() then
term.setTextColor(colors.green)
end
term.write('[PASS] ')
term.setTextColor(colors.white)
term.write(fs.combine(directory, file))
print()
else
if term.isColor() then
term.setTextColor(colors.red)
end
term.write('[FAIL] ')
term.setTextColor(colors.white)
term.write(fs.combine(directory, file))
if err then
_G.printError('\n' .. err)
end
print()
success = false
end
end
end
runDir('sys/autorun')
for _, package in pairs(Packages:installedSorted()) do
local packageDir = 'packages/' .. package.name .. '/autorun'
runDir(packageDir)
end
runDir('usr/autorun')
if not success then
if multishell then
multishell.setFocus(multishell.getCurrent())
end
_G.printError('A startup program has errored')
print('Press enter to continue')
while true do
local e, code = os.pullEventRaw('key')
if e == 'terminate' or e == 'key' and code == keys.enter then
break
end
end
end

38
sys/apps/cedit.lua Normal file
View File

@@ -0,0 +1,38 @@
local Config = require('opus.config')
local multishell = _ENV.multishell
local os = _G.os
local read = _G.read
local shell = _ENV.shell
local args = { ... }
if not args[1] then
error('Syntax: cedit <filename>')
end
if not _G.http.websocket then
error('Requires CC: Tweaked')
end
if not _G.cloud_catcher then
local key = Config.load('cloud').key
if not key then
print('Visit https://cloud-catcher.squiddev.cc')
print('Paste key: ')
key = read()
if #key == 0 then
return
end
end
-- open an unfocused tab
local id = shell.openTab('cloud ' .. key)
print('Connecting...')
while not _G.cloud_catcher do
os.sleep(.2)
end
multishell.setTitle(id, 'Cloud')
end
shell.run('cloud edit ' .. table.unpack({ ... }))

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

23
sys/apps/cshell.lua Normal file
View File

@@ -0,0 +1,23 @@
local Config = require('opus.config')
local read = _G.read
local shell = _ENV.shell
if not _G.http.websocket then
error('Requires CC: Tweaked')
end
if not _G.cloud_catcher then
local key = Config.load('cloud').key
if not key then
print('Visit https://cloud-catcher.squiddev.cc')
print('Paste key: ')
key = read()
if #key == 0 then
return
end
end
print('Connecting...')
shell.run('cloud ' .. key)
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)

195
sys/apps/inspect.lua Normal file
View File

@@ -0,0 +1,195 @@
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local multishell = _ENV.multishell
local name = ({ ... })[1] or error('Syntax: inspect COMPONENT')
local events = { }
local page, lastEvent, focused
local function isRelevant(el)
return page.testContainer == el or el.parent and isRelevant(el.parent)
end
local emitter = UI.Window.emit
function UI.Window:emit(event)
if event ~= lastEvent and isRelevant(self) then
lastEvent = event
local t = { }
for k,v in pairs(event) do
if k ~= 'type' and k ~= 'recorded' then
table.insert(t, k .. ':' .. (type(v) == 'table' and (v.UIElement and v.uid or 'tbl') or tostring(v)))
end
end
table.insert(events, 1, { type = event.type, value = table.concat(t, ' '), raw = event })
while #events > 20 do
table.remove(events)
end
page.tabs.events.grid:update()
if page.tabs.events.enabled then
page.tabs.events.grid:draw()
end
end
return emitter(self, event)
end
-- do not load component until emit hook is in place
local component = UI[name] and UI[name]() or error('Invalid component')
if not component.example then
error('No example present')
end
page = UI.Page {
testContainer = UI.Window {
ey = '50%',
testing = component.example(),
},
tabs = UI.Tabs {
backgroundColor = colors.red,
y = '50%',
properties = UI.Tab {
title = 'Properties',
grid = UI.ScrollingGrid {
headerBackgroundColor = colors.red,
sortColumn = 'key',
columns = {
{ heading = 'key', key = 'key' },
{ heading = 'value', key = 'value', }
},
accelerators = {
grid_select = 'edit_property',
},
},
},
methodsTab = UI.Tab {
index = 2,
title = 'Methods',
grid = UI.ScrollingGrid {
ex = '50%',
headerBackgroundColor = colors.red,
sortColumn = 'key',
columns = {
{ heading = 'key', key = 'key' },
},
},
docs = UI.TextArea {
x = '50%',
backgroundColor = colors.black,
},
eventHandler = function (self, event)
if event.type == 'grid_focus_row' and focused then
self.docs:setText(focused:getDoc(event.selected.key) or '')
end
end,
},
events = UI.Tab {
index = 1,
title = 'Events',
UI.MenuBar {
y = -1,
backgroundColor = colors.red,
buttons = {
{ text = 'Clear' },
}
},
grid = UI.ScrollingGrid {
ey = -2,
headerBackgroundColor = colors.red,
values = events,
autospace = true,
columns = {
{ heading = 'type', key = 'type' },
{ heading = 'value', key = 'value', }
},
},
eventHandler = function (self, event)
if event.type == 'button_press' then
Util.clear(self.grid.values)
self.grid:update()
self.grid:draw()
elseif event.type == 'grid_select' then
multishell.openTab(_ENV, {
path = 'sys/apps/Lua.lua',
args = { event.selected.raw },
focused = true,
})
end
end
}
},
editor = UI.SlideOut {
y = -4, height = 4,
backgroundColor = colors.green,
titleBar = UI.TitleBar {
event = 'editor_cancel',
title = 'Enter value',
},
entry = UI.TextEntry {
y = 3, x = 2, ex = 10,
accelerators = {
enter = 'editor_apply',
},
},
},
accelerators = {
['shift-right'] = 'size',
['shift-left' ] = 'size',
['shift-up' ] = 'size',
['shift-down' ] = 'size',
},
eventHandler = function (self, event)
if event.type == 'focus_change' and isRelevant(event.focused) then
focused = event.focused
local t = { }
for k,v in pairs(event.focused) do
table.insert(t, {
key = k,
value = tostring(v),
})
end
self.tabs.properties.grid:setValues(t)
self.tabs.properties.grid:draw()
t = { }
for k,v in pairs(getmetatable(event.focused)) do
if type(v) == 'function' then
table.insert(t, {
key = k,
})
end
end
self.tabs.methodsTab.grid:setValues(t)
self.tabs.methodsTab.grid:draw()
elseif event.type == 'edit_property' then
self.editor.entry.value = event.selected.value
self.editor:show()
elseif event.type == 'editor_cancel' then
self.editor:hide()
elseif event.type == 'editor_apply' then
self.editor:hide()
elseif event.type == 'size' then
local sizing = {
['shift-right'] = { 1, 0 },
['shift-left' ] = { -1, 0 },
['shift-up' ] = { 0, -1 },
['shift-down' ] = { 0, 1 },
}
self.ox = math.max(self.ox + sizing[event.ie.code][1], 1)
self.oy = math.max(self.oy + sizing[event.ie.code][2], 1)
UI.term:clear()
self:resize()
self:draw()
end
return UI.Page.eventHandler(self, event)
end
}
UI:setPage(page)
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

@@ -3,4 +3,4 @@ local args = { ... }
local target = table.remove(args, 1) local target = table.remove(args, 1)
target = shell.resolve(target) target = shell.resolve(target)
fs.mount(target, unpack(args)) fs.mount(target, table.unpack(args))

View File

@@ -1,624 +0,0 @@
-- Default label
if not os.getComputerLabel() then
local id = os.getComputerID()
if turtle then
os.setComputerLabel('turtle_' .. id)
elseif pocket then
os.setComputerLabel('pocket_' .. id)
elseif commands then
os.setComputerLabel('command_' .. id)
else
os.setComputerLabel('computer_' .. id)
end
end
multishell.term = term.current()
local defaultEnv = { }
for k,v in pairs(getfenv(1)) do
defaultEnv[k] = v
end
requireInjector(getfenv(1))
local Config = require('config')
local Opus = require('opus')
local Util = require('util')
local SESSION_FILE = 'usr/config/multishell.session'
local parentTerm = term.current()
local w,h = parentTerm.getSize()
local tabs = {}
local currentTab
local _tabId = 0
local overviewTab
local runningTab
local tabsDirty = false
local closeInd = '*'
if Util.getVersion() >= 1.79 then
closeInd = '\215'
end
local config = {
standard = {
textColor = colors.lightGray,
tabBarTextColor = colors.lightGray,
focusTextColor = colors.white,
backgroundColor = colors.gray,
tabBarBackgroundColor = colors.gray,
focusBackgroundColor = colors.gray,
},
color = {
textColor = colors.lightGray,
tabBarTextColor = colors.lightGray,
focusTextColor = colors.white,
backgroundColor = colors.gray,
tabBarBackgroundColor = colors.gray,
focusBackgroundColor = colors.gray,
},
}
Config.load('multishell', config)
local _colors = config.standard
if parentTerm.isColor() then
_colors = config.color
end
local function redrawMenu()
if not tabsDirty then
os.queueEvent('multishell', 'draw')
tabsDirty = true
end
end
-- Draw menu
local function draw()
tabsDirty = false
parentTerm.setBackgroundColor( _colors.tabBarBackgroundColor )
if currentTab and currentTab.isOverview then
parentTerm.setTextColor( _colors.focusTextColor )
else
parentTerm.setTextColor( _colors.tabBarTextColor )
end
parentTerm.setCursorPos( 1, 1 )
parentTerm.clearLine()
parentTerm.write('+')
local tabX = 2
local function compareTab(a, b)
return a.tabId < b.tabId
end
for _,tab in Util.spairs(tabs, compareTab) do
if tab.hidden and tab ~= currentTab or tab.isOverview then
tab.sx = nil
tab.ex = nil
else
tab.sx = tabX + 1
tab.ex = tabX + #tab.title
tabX = tabX + #tab.title + 1
end
end
for _,tab in Util.spairs(tabs) do
if tab.sx then
if tab == currentTab then
parentTerm.setTextColor(_colors.focusTextColor)
parentTerm.setBackgroundColor(_colors.focusBackgroundColor)
else
parentTerm.setTextColor(_colors.textColor)
parentTerm.setBackgroundColor(_colors.backgroundColor)
end
parentTerm.setCursorPos(tab.sx, 1)
parentTerm.write(tab.title)
end
end
if currentTab and not currentTab.isOverview then
parentTerm.setTextColor(_colors.focusTextColor)
parentTerm.setBackgroundColor(_colors.backgroundColor)
parentTerm.setCursorPos( w, 1 )
parentTerm.write(closeInd)
end
if currentTab then
currentTab.window.restoreCursor()
end
end
local function selectTab( tab )
if not tab then
for _,ftab in pairs(tabs) do
if not ftab.hidden then
tab = ftab
break
end
end
end
if not tab then
tab = overviewTab
end
if currentTab and currentTab ~= tab then
currentTab.window.setVisible(false)
if tab and not currentTab.hidden then
tab.previousTabId = currentTab.tabId
end
end
if tab then
currentTab = tab
tab.window.setVisible(true)
end
end
local function resumeTab(tab, event, eventData)
if not tab or coroutine.status(tab.co) == 'dead' then
return
end
if not tab.filter or tab.filter == event or event == "terminate" then
eventData = eventData or { }
term.redirect(tab.terminal)
local previousTab = runningTab
runningTab = tab
local ok, result = coroutine.resume(tab.co, event, unpack(eventData))
tab.terminal = term.current()
if ok then
tab.filter = result
else
printError(result)
end
runningTab = previousTab
return ok, result
end
end
local function nextTabId()
_tabId = _tabId + 1
return _tabId
end
local function launchProcess(tab)
tab.tabId = nextTabId()
tab.timestamp = os.clock()
tab.window = window.create(parentTerm, 1, 2, w, h - 1, false)
tab.terminal = tab.window
tab.env = Util.shallowCopy(tab.env or defaultEnv)
tab.co = coroutine.create(function()
local result, err
if tab.fn then
result, err = Util.runFunction(tab.env, tab.fn, table.unpack(tab.args or { } ))
elseif tab.path then
result, err = Util.run(tab.env, tab.path, table.unpack(tab.args or { } ))
else
err = 'multishell: invalid tab'
end
if not result and err and err ~= 'Terminated' then
if err then
printError(tostring(err))
end
printError('Press enter to close')
tab.isDead = true
while true do
local e, code = os.pullEventRaw('key')
if e == 'terminate' or e == 'key' and code == keys.enter then
if tab.isOverview then
os.queueEvent('multishell', 'terminate')
end
break
end
end
end
tabs[tab.tabId] = nil
if tab == currentTab then
local previousTab
if tab.previousTabId then
previousTab = tabs[tab.previousTabId]
end
selectTab(previousTab)
end
redrawMenu()
saveSession()
end)
tabs[tab.tabId] = tab
resumeTab(tab)
return tab
end
local function resizeWindows()
local windowY = 2
local windowHeight = h-1
local keys = Util.keys(tabs)
for _,key in pairs(keys) do
local tab = tabs[key]
local x,y = tab.window.getCursorPos()
if y > windowHeight then
tab.window.scroll( y - windowHeight )
tab.window.setCursorPos( x, windowHeight )
end
tab.window.reposition( 1, windowY, w, windowHeight )
end
-- Pass term_resize to all processes
local keys = Util.keys(tabs)
for _,key in pairs(keys) do
resumeTab(tabs[key], "term_resize")
end
end
local function saveSession()
local t = { }
for _,process in pairs(tabs) do
if process.path and not process.isOverview and not process.hidden then
table.insert(t, {
path = process.path,
args = process.args,
})
end
end
--Util.writeTable(SESSION_FILE, t)
end
local control
local hotkeys = { }
local function processKeyEvent(event, code)
if event == 'key_up' then
if code == keys.leftCtrl or code == keys.rightCtrl then
control = false
end
elseif event == 'char' then
control = false
elseif event == 'key' then
if code == keys.leftCtrl or code == keys.rightCtrl then
control = true
elseif control then
local hotkey = hotkeys[code]
control = false
if hotkey then
hotkey()
end
end
end
end
function multishell.addHotkey(code, fn)
hotkeys[code] = fn
end
function multishell.removeHotkey(code)
hotkeys[code] = nil
end
function multishell.getFocus()
return currentTab.tabId
end
function multishell.setFocus(tabId)
local tab = tabs[tabId]
if tab then
selectTab(tab)
redrawMenu()
return true
end
return false
end
function multishell.getTitle(tabId)
local tab = tabs[tabId]
if tab then
return tab.title
end
end
function multishell.setTitle(tabId, sTitle)
local tab = tabs[tabId]
if tab then
tab.title = sTitle or ''
redrawMenu()
end
end
function multishell.getCurrent()
if runningTab then
return runningTab.tabId
end
end
function multishell.getTab(tabId)
return tabs[tabId]
end
function multishell.terminate(tabId)
local tab = tabs[tabId]
if tab and not tab.isOverview then
if coroutine.status(tab.co) ~= 'dead' then
--os.queueEvent('multishell', 'terminate', tab)
resumeTab(tab, "terminate")
else
tabs[tabId] = nil
if tab == currentTab then
local previousTab
if tab.previousTabId then
previousTab = tabs[tab.previousTabId]
end
selectTab(previousTab)
end
redrawMenu()
end
end
end
function multishell.getTabs()
return tabs
end
function multishell.launch( tProgramEnv, sProgramPath, ... )
-- backwards compatibility
return multishell.openTab({
env = tProgramEnv,
path = sProgramPath,
args = { ... },
})
end
function multishell.openTab(tab)
if not tab.title and tab.path then
tab.title = fs.getName(tab.path)
end
tab.title = tab.title or 'untitled'
local previousTerm = term.current()
launchProcess(tab)
term.redirect(previousTerm)
if tab.hidden then
if coroutine.status(tab.co) == 'dead' or tab.isDead then
tab.hidden = false
end
elseif tab.focused then
multishell.setFocus(tab.tabId)
else
redrawMenu()
end
if not tab.hidden then
saveSession()
end
return tab.tabId
end
function multishell.hideTab(tabId)
local tab = tabs[tabId]
if tab then
tab.hidden = true
redrawMenu()
end
end
function multishell.unhideTab(tabId)
local tab = tabs[tabId]
if tab then
tab.hidden = false
redrawMenu()
end
end
function multishell.getCount()
local count
for _,tab in pairs(tabs) do
count = count + 1
end
return count
end
-- control-o - overview
multishell.addHotkey(24, function()
multishell.setFocus(overviewTab.tabId)
end)
-- control-backspace
multishell.addHotkey(14, function()
local tabId = multishell.getFocus()
local tab = tabs[tabId]
if not tab.isOverview then
os.queueEvent('multishell', 'terminateTab', tabId)
tab = Util.shallowCopy(tab)
tab.isDead = false
tab.focused = true
multishell.openTab(tab)
end
end)
-- control-tab - next tab
multishell.addHotkey(15, function()
local function compareTab(a, b)
return a.tabId < b.tabId
end
local visibleTabs = { }
for _,tab in Util.spairs(tabs, compareTab) do
if not tab.hidden then
table.insert(visibleTabs, tab)
end
end
for k,tab in ipairs(visibleTabs) do
if tab.tabId == currentTab.tabId then
if k < #visibleTabs then
multishell.setFocus(visibleTabs[k + 1].tabId)
return
end
end
end
if #visibleTabs > 0 then
multishell.setFocus(visibleTabs[1].tabId)
end
end)
local function startup()
local hasError
local session = Util.readTable(SESSION_FILE)
local overviewId = multishell.openTab({
path = 'sys/apps/Overview.lua',
focused = true,
hidden = true,
isOverview = true,
})
overviewTab = tabs[overviewId]
if not Opus.loadServices() then
hasError = true
end
if not Opus.autorun() then
hasError = true
end
if session then
for _,v in pairs(session) do
multishell.openTab(v)
end
end
if hasError then
print()
error('An autorun program has errored')
end
end
-- Begin
parentTerm.clear()
multishell.openTab({
focused = true,
fn = startup,
env = defaultEnv,
title = 'Autorun',
})
if not overviewTab or coroutine.status(overviewTab.co) == 'dead' then
--error('Overview aborted')
end
if not currentTab then
multishell.setFocus(overviewTab.tabId)
end
draw()
local lastClicked
while true do
-- Get the event
local tEventData = { os.pullEventRaw() }
local sEvent = table.remove(tEventData, 1)
if sEvent == 'key_up' then
processKeyEvent(sEvent, tEventData[1])
end
if sEvent == "term_resize" then
-- Resize event
w,h = parentTerm.getSize()
resizeWindows()
redrawMenu()
elseif sEvent == 'multishell' then
local action = tEventData[1]
if action == 'terminate' then
break
elseif action == 'terminateTab' then
multishell.terminate(tEventData[2])
elseif action == 'draw' then
draw()
end
elseif sEvent == "char" or
sEvent == "key" or
sEvent == "paste" or
sEvent == "terminate" then
processKeyEvent(sEvent, tEventData[1])
-- Keyboard event - Passthrough to current process
resumeTab(currentTab, sEvent, tEventData)
elseif sEvent == "mouse_click" then
local button, x, y = tEventData[1], tEventData[2], tEventData[3]
lastClicked = nil
if y == 1 then
-- Switch process
local w, h = parentTerm.getSize()
if x == 1 then
multishell.setFocus(overviewTab.tabId)
elseif x == w then
if currentTab then
multishell.terminate(currentTab.tabId)
end
else
for _,tab in pairs(tabs) do
if not tab.hidden and tab.sx then
if x >= tab.sx and x <= tab.ex then
multishell.setFocus(tab.tabId)
break
end
end
end
end
elseif currentTab then
-- Passthrough to current process
lastClicked = currentTab
resumeTab(currentTab, sEvent, { button, x, y-1 })
end
elseif sEvent == "mouse_up" then
if currentTab and lastClicked == currentTab then
local button, x, y = tEventData[1], tEventData[2], tEventData[3]
resumeTab(currentTab, sEvent, { button, x, y-1 })
end
elseif sEvent == "mouse_drag" or sEvent == "mouse_scroll" then
-- Other mouse event
local p1, x, y = tEventData[1], tEventData[2], tEventData[3]
if currentTab and (y ~= 1) then
if currentTab.terminal.scrollUp then
if p1 == -1 then
currentTab.terminal.scrollUp()
else
currentTab.terminal.scrollDown()
end
else
-- Passthrough to current process
resumeTab(currentTab, sEvent, { p1, x, y-1 })
end
end
else
-- Other event
-- Passthrough to all processes
local keys = Util.keys(tabs)
for _,key in pairs(keys) do
resumeTab(tabs[key], sEvent, tEventData)
end
end
end

45
sys/apps/netdaemon.lua Normal file
View File

@@ -0,0 +1,45 @@
local Event = require('opus.event')
local Util = require('opus.util')
local device = _G.device
local fs = _G.fs
local network = _G.network
local os = _G.os
local printError = _G.printError
if not device.wireless_modem then
return
end
print('Net daemon starting')
-- don't close as multiple computers may be sharing the
-- wireless modem
--device.wireless_modem.closeAll()
for _,file in pairs(fs.list('sys/apps/network')) do
local fn, msg = Util.run(_ENV, 'sys/apps/network/' .. file)
if not fn then
printError(msg)
end
end
Event.on('device_detach', function()
if not device.wireless_modem then
Event.exitPullEvents()
end
end)
print('Net daemon started')
os.queueEvent('network_up')
Event.pullEvents()
for _,c in pairs(network) do
c.active = false
os.queueEvent('network_detach', c)
end
os.queueEvent('network_down')
Event.pullEvent('network_down')
Util.clear(network)
print('Net daemon stopped')

View File

@@ -0,0 +1,39 @@
local ECC = require('opus.crypto.ecc')
local Event = require('opus.event')
local Util = require('opus.util')
local network = _G.network
local os = _G.os
local keyPairs = { }
local function generateKeyPair()
local key = { }
for _ = 1, 32 do
table.insert(key, math.random(0, 0xFF))
end
local privateKey = setmetatable(key, Util.byteArrayMT)
return privateKey, ECC.publicKey(privateKey)
end
getmetatable(network).__index.getKeyPair = function()
local keys = table.remove(keyPairs)
os.queueEvent('generate_keypair')
if not keys then
return generateKeyPair()
end
return table.unpack(keys)
end
-- Generate key pairs in the background as this is a time-consuming process
Event.on('generate_keypair', function()
while true do
os.sleep(5)
local timer = Util.timer()
table.insert(keyPairs, { generateKeyPair() })
_G._syslog('Generated keypair in ' .. timer())
if #keyPairs >= 3 then
break
end
end
end)

View File

@@ -0,0 +1,64 @@
local Event = require('opus.event')
local Socket = require('opus.socket')
local Util = require('opus.util')
local function getProxy(path)
local x = Util.split(path, '(.-)/')
local proxy = _G
for _, v in pairs(x) do
proxy = proxy[v]
if not proxy then
break
end
end
return proxy
end
local function proxyConnection(socket)
local path = socket:read(2)
if path then
local api = getProxy(path)
if not api then
print('proxy: invalid API')
socket:close()
return
end
local methods = { }
for k,v in pairs(api) do
if type(v) == 'function' then
table.insert(methods, k)
end
end
socket:write(methods)
while true do
local data = socket:read()
if not data then
print('proxy: lost connection from ' .. socket.dhost)
break
end
socket:write({ api[data[1]](table.unpack(data, 2)) })
end
end
end
Event.addRoutine(function()
print('proxy: listening on port 188')
while true do
local socket = Socket.server(188)
print('proxy: connection from ' .. socket.dhost)
Event.addRoutine(function()
local s, m = pcall(proxyConnection, socket)
print('proxy: closing connection to ' .. socket.dhost)
socket:close()
if not s and m then
print('Proxy error')
_G.printError(m)
end
end)
end
end)

View File

@@ -0,0 +1,92 @@
local Event = require('opus.event')
local Socket = require('opus.socket')
local fs = _G.fs
local fileUid = 0
local fileHandles = { }
local function remoteOpen(fn, fl)
local fh = fs.open(fn, fl)
if fh then
local methods = { 'close', 'write', 'writeLine', 'flush', 'read', 'readLine', 'readAll', }
fileUid = fileUid + 1
fileHandles[fileUid] = fh
local vfh = {
methods = { },
fileUid = fileUid,
}
for _,m in ipairs(methods) do
if fh[m] then
table.insert(vfh.methods, m)
end
end
return vfh
end
end
local function remoteFileOperation(fileId, op, ...)
local fh = fileHandles[fileId]
if fh then
return fh[op](...)
end
end
local function sambaConnection(socket)
while true do
local msg = socket:read()
if not msg then
break
end
local fn = fs[msg.fn]
if msg.fn == 'open' then
fn = remoteOpen
elseif msg.fn == 'fileOp' then
fn = remoteFileOperation
end
local ret
local s, m = pcall(function()
ret = fn(table.unpack(msg.args))
end)
if not s and m then
_G.printError('samba: ' .. m)
end
socket:write({ response = ret })
end
print('samba: Connection closed')
end
local function sanitizeLabel(computer)
return (computer.id.."_"..computer.label:gsub("[%c%.\"'/%*]", "")):sub(1, 40)
end
Event.addRoutine(function()
print('samba: listening on port 139')
while true do
local socket = Socket.server(139)
Event.addRoutine(function()
print('samba: connection from ' .. socket.dhost)
local s, m = pcall(sambaConnection, socket)
print('samba: closing connection to ' .. socket.dhost)
socket:close()
if not s and m then
print('Samba error')
_G.printError(m)
end
end)
end
end)
Event.on('network_attach', function(_, computer)
fs.mount(fs.combine('network', sanitizeLabel(computer)), 'netfs', computer.id)
end)
Event.on('network_detach', function(_, computer)
print('samba: detaching ' .. sanitizeLabel(computer))
fs.unmount(fs.combine('network', sanitizeLabel(computer)))
end)

226
sys/apps/network/snmp.lua Normal file
View File

@@ -0,0 +1,226 @@
local Event = require('opus.event')
local GPS = require('opus.gps')
local Socket = require('opus.socket')
local Util = require('opus.util')
local device = _G.device
local kernel = _G.kernel
local network = _G.network
local os = _G.os
local turtle = _G.turtle
-- move this into gps api
local gpsRequested
local gpsLastPoint
local gpsLastRequestTime
local function snmpConnection(socket)
while true do
local msg = socket:read()
if not msg then
break
end
if msg.type == 'reboot' then
os.reboot()
elseif msg.type == 'shutdown' then
os.shutdown()
elseif msg.type == 'ping' then
socket:write('pong')
elseif msg.type == 'script' then
kernel.run(_ENV, {
chunk = msg.args,
title = 'script',
})
elseif msg.type == 'scriptEx' then
local s, m = pcall(function()
local env = kernel.makeEnv(_ENV)
local fn, m = load(msg.args, 'script', nil, env)
if not fn then
error(m)
end
return { fn() }
end)
if s then
socket:write(m)
else
socket:write({ s, m })
end
elseif msg.type == 'gps' then
if gpsRequested then
repeat
os.sleep(0)
until not gpsRequested
end
if gpsLastPoint and os.clock() - gpsLastRequestTime < .5 then
socket:write(gpsLastPoint)
else
gpsRequested = true
local pt = GPS.getPoint(2)
if pt then
socket:write(pt)
else
print('snmp: Unable to get GPS point')
end
gpsRequested = false
gpsLastPoint = pt
if pt then
gpsLastRequestTime = os.clock()
end
end
elseif msg.type == 'info' then
local info = {
id = os.getComputerID(),
label = os.getComputerLabel(),
uptime = math.floor(os.clock()),
}
if turtle then
info.fuel = turtle.getFuelLevel()
info.status = turtle.getStatus()
end
socket:write(info)
end
end
end
Event.addRoutine(function()
print('snmp: listening on port 161')
while true do
local socket = Socket.server(161)
Event.addRoutine(function()
print('snmp: connection from ' .. socket.dhost)
local s, m = pcall(snmpConnection, socket)
print('snmp: closing connection to ' .. socket.dhost)
if not s and m then
print('snmp error')
_G.printError(m)
end
end)
end
end)
device.wireless_modem.open(999)
print('discovery: listening on port 999')
Event.on('modem_message', function(_, _, sport, id, info, distance)
if sport == 999 and tonumber(id) and type(info) == 'table' then
if type(info.label) == 'string' and type(info.id) == 'number' then
if not network[id] then
network[id] = { }
end
Util.merge(network[id], info)
network[id].distance = type(distance) == 'number' and distance
network[id].timestamp = os.clock()
if not network[id].label then
network[id].label = 'unknown'
end
if not network[id].active then
network[id].active = true
os.queueEvent('network_attach', network[id])
end
else
print('discovery: Invalid alive message ' .. id)
end
end
end)
local info = {
id = os.getComputerID()
}
local infoTimer = os.clock()
local function getSlots()
return Util.reduce(turtle.getInventory(), function(acc, v)
if v.count > 0 then
acc[v.index .. ',' .. v.count] = v.key
end
return acc
end, { })
end
local function sendInfo()
if os.clock() - infoTimer >= 5 then -- don't flood
infoTimer = os.clock()
info.label = os.getComputerLabel()
info.uptime = math.floor(os.clock())
info.group = network.getGroup()
if turtle and turtle.getStatus then
info.fuel = turtle.getFuelLevel()
info.status = turtle.getStatus()
info.point = turtle.point
info.inv = getSlots()
info.slotIndex = turtle.getSelectedSlot()
end
if device.neuralInterface then
info.status = device.neuralInterface.status
if not info.status and device.neuralInterface.getMetaOwner then
pcall(function()
local meta = device.neuralInterface.getMetaOwner()
local states = {
isWet = 'Swimming',
isElytraFlying = 'Flying',
isBurning = 'Burning',
isDead = 'Deceased',
isOnLadder = 'Climbing',
isRiding = 'Riding',
isSneaking = 'Sneaking',
isSprinting = 'Running',
}
for k,v in pairs(states) do
if meta[k] then
info.status = v
break
end
end
info.status = info.status or 'health: ' ..
math.floor(meta.health / meta.maxHealth * 100)
end)
end
end
device.wireless_modem.transmit(999, os.getComputerID(), info)
end
end
local function cleanNetwork()
for _,c in pairs(_G.network) do
local elapsed = os.clock()-c.timestamp
if c.active and elapsed > 50 then
c.active = false
os.queueEvent('network_detach', c)
end
end
end
-- every 30 seconds, send out this computer's info
-- send with offset so that messages are evenly distributed and do not all come at once
Event.onTimeout(math.random() * 30, function()
sendInfo()
cleanNetwork()
Event.onInterval(30, function()
sendInfo()
cleanNetwork()
end)
end)
Event.on('turtle_response', function()
if turtle.getStatus() ~= info.status or
turtle.fuel ~= info.fuel then
sendInfo()
end
end)
-- send info early so that computers show soon after booting
Event.onTimeout(math.random() * 2 + 1, sendInfo)

103
sys/apps/network/telnet.lua Normal file
View File

@@ -0,0 +1,103 @@
local Event = require('opus.event')
local Socket = require('opus.socket')
local Util = require('opus.util')
local kernel = _G.kernel
local shell = _ENV.shell
local term = _G.term
local window = _G.window
local function telnetHost(socket, mode)
local methods = { 'clear', 'clearLine', 'setCursorPos', 'write', 'blit',
'setTextColor', 'setTextColour', 'setBackgroundColor',
'setBackgroundColour', 'scroll', 'setCursorBlink', }
local termInfo = socket:read(5)
if not termInfo then
_G.printError('read failed')
return
end
local win = window.create(_G.device.terminal, 1, 1, termInfo.width, termInfo.height, false)
win.setCursorPos(table.unpack(termInfo.pos))
for _,k in pairs(methods) do
local fn = win[k]
win[k] = function(...)
if not socket.queue then
socket.queue = { }
Event.onTimeout(0, function()
socket:write(socket.queue)
socket.queue = nil
end)
end
table.insert(socket.queue, {
f = k,
args = { ... },
})
fn(...)
end
end
local shellThread = kernel.run(_ENV, {
window = win,
title = mode .. ' client',
hidden = true,
fn = function()
Util.run(kernel.makeEnv(_ENV), shell.resolveProgram('shell'), table.unpack(termInfo.program))
if socket.queue then
socket:write(socket.queue)
end
socket:close()
end,
})
Event.addRoutine(function()
while true do
local data = socket:read()
if not data then
shellThread:resume('terminate')
break
end
local previousTerm = term.current()
shellThread:resume(table.unpack(data))
term.redirect(previousTerm)
end
end)
end
Event.addRoutine(function()
print('ssh: listening on port 22')
while true do
local socket = Socket.server(22, { ENCRYPT = true })
print('ssh: connection from ' .. socket.dhost)
Event.addRoutine(function()
local s, m = pcall(telnetHost, socket, 'SSH')
if not s and m then
print('ssh error')
_G.printError(m)
end
end)
end
end)
Event.addRoutine(function()
print('telnet: listening on port 23')
while true do
local socket = Socket.server(23)
print('telnet: connection from ' .. socket.dhost)
Event.addRoutine(function()
local s, m = pcall(telnetHost, socket, 'Telnet')
if not s and m then
print('Telnet error')
_G.printError(m)
end
end)
end
end)

View File

@@ -0,0 +1,172 @@
--[[
Low level socket protocol implementation.
* sequencing
* background read buffering
]]--
local Crypto = require('opus.crypto.chacha20')
local Event = require('opus.event')
local SHA = require('opus.crypto.sha2')
local network = _G.network
local os = _G.os
local computerId = os.getComputerID()
local transport = {
timers = { },
sockets = { },
encryptQueue = { },
UID = 0,
}
getmetatable(network).__index.getTransport = function()
return transport
end
function transport.open(socket)
transport.UID = transport.UID + 1
transport.sockets[socket.sport] = socket
socket.activityTimer = os.clock()
socket.uid = transport.UID
end
function transport.read(socket)
local data = table.remove(socket.messages, 1)
if data then
if socket.options.ENCRYPT then
local ciphertext = data[1]
-- Verify HMAC if present (new protocol)
if socket.hmackey and type(ciphertext) == 'table' and ciphertext.hmac then
local expected = SHA.hmac(
ciphertext[1] .. ciphertext[2],
socket.hmackey
):toHex()
if expected ~= ciphertext.hmac then
_G._syslog('transport: HMAC verification failed on port ' .. socket.sport)
return nil
end
end
return table.unpack(Crypto.decrypt(ciphertext, socket.enckey)), data[2]
end
return table.unpack(data)
end
end
function transport.write(socket, msg)
if socket.options.ENCRYPT then
if #transport.encryptQueue == 0 then
os.queueEvent('transport_encrypt')
end
table.insert(transport.encryptQueue, { socket.sport, msg })
else
socket.transmit(socket.dport, socket.dhost, msg)
end
socket.wseq = socket.wrng:nextInt(5)
end
function transport.ping(socket)
if os.clock() - socket.activityTimer > 10 then
socket.activityTimer = os.clock()
socket.transmit(socket.dport, socket.dhost, {
type = 'PING',
seq = -1,
})
local timerId = os.startTimer(3)
transport.timers[timerId] = socket
socket.timers[-1] = timerId
end
end
function transport.close(socket)
transport.sockets[socket.sport] = nil
end
Event.on('transport_encrypt', function()
while #transport.encryptQueue > 0 do
local entry = table.remove(transport.encryptQueue, 1)
local socket = transport.sockets[entry[1]]
if socket and socket.connected then
local msg = entry[2]
local encrypted = Crypto.encrypt({ msg.data }, socket.enckey)
-- Attach HMAC if key is available
if socket.hmackey then
encrypted.hmac = SHA.hmac(
encrypted[1] .. encrypted[2],
socket.hmackey
):toHex()
end
msg.data = encrypted
socket.transmit(socket.dport, socket.dhost, msg)
end
end
end)
Event.on('timer', function(_, timerId)
local socket = transport.timers[timerId]
if socket and socket.connected then
print('transport timeout - closing socket ' .. socket.sport)
socket:close()
transport.timers[timerId] = nil
end
end)
Event.on('modem_message', function(_, _, dport, dhost, msg, distance)
if dhost == computerId and type(msg) == 'table' then
local socket = transport.sockets[dport]
if socket and socket.connected then
if socket.co and coroutine.status(socket.co) == 'dead' then
_G._syslog('socket coroutine dead')
socket:close()
elseif msg.type == 'DISC' then
-- received disconnect from other end
if msg.seq == socket.rseq then
if socket.connected then
os.queueEvent('transport_' .. socket.uid)
end
socket.connected = false
socket:close()
end
elseif msg.type == 'ACK' then
local ackTimerId = socket.timers[msg.seq]
if ackTimerId then
os.cancelTimer(ackTimerId)
socket.timers[msg.seq] = nil
socket.activityTimer = os.clock()
transport.timers[ackTimerId] = nil
end
elseif msg.type == 'PING' then
socket.activityTimer = os.clock()
socket.transmit(socket.dport, socket.dhost, {
type = 'ACK',
seq = msg.seq,
})
elseif msg.type == 'DATA' and msg.data then
if msg.seq ~= socket.rseq then
print('transport seq error ' .. socket.sport)
_syslog(msg.data)
_syslog('expected ' .. socket.rseq)
_syslog('got ' .. msg.seq)
else
socket.activityTimer = os.clock()
socket.rseq = socket.rrng:nextInt(5)
table.insert(socket.messages, { msg.data, distance })
if not socket.messages[2] then -- table size is 1
os.queueEvent('transport_' .. socket.uid)
end
end
end
end
end
end)

View File

@@ -0,0 +1,76 @@
local Crypto = require('opus.crypto.chacha20')
local Event = require('opus.event')
local Security = require('opus.security')
local Socket = require('opus.socket')
local Util = require('opus.util')
local trustId = '01c3ba27fe01383a03a1785276d99df27c3edcef68fbf231ca'
local oneTimePassword -- nil by default
local function validateData(data, password, dhost)
local s
s, data = pcall(Crypto.decrypt, data, password)
if s and data and type(data) == "table" and data.pk and data.dh == dhost then
local trustList = Util.readTable('usr/.known_hosts') or { }
trustList[data.dh] = data.pk
Util.writeTable('usr/.known_hosts', trustList)
return true
else
return false
end
end
local function trustConnection(socket)
local data = socket:read(2)
if data then
local trustKey = Security.getTrustKey()
if not trustKey then
socket:write({ msg = 'No password has been set' })
else
if validateData(data, trustKey, socket.dhost) then
print("Accepted trust from " .. socket.dhost)
socket:write({ success = true, msg = 'Trust accepted' })
return
end
if oneTimePassword then
if validateData(data, oneTimePassword, socket.dhost) then
print("Accepted trust from " .. socket.dhost .. "using one-time password")
socket:write({ success = true, msg = 'Trust accepted - this one-time password will not be usable again' })
oneTimePassword = nil -- Make sure nobody can use the one-time password again
return
end
end
socket:write({ msg = 'Invalid password' })
end
end
end
Event.addRoutine(function()
print('trust: listening on port 19')
while true do
local socket = Socket.server(19, { identifier = trustId })
print('trust: connection from ' .. socket.dhost)
local s, m = pcall(trustConnection, socket)
socket:close()
if not s and m then
print('Trust error')
_G.printError(m)
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)

93
sys/apps/network/vnc.lua Normal file
View File

@@ -0,0 +1,93 @@
local Event = require('opus.event')
local Socket = require('opus.socket')
local Util = require('opus.util')
local os = _G.os
local terminal = _G.device.terminal
local function vncHost(socket)
local methods = { 'blit', 'clear', 'clearLine', 'setCursorPos', 'write',
'setTextColor', 'setTextColour', 'setBackgroundColor',
'setBackgroundColour', 'scroll', 'setCursorBlink', }
local oldTerm = Util.shallowCopy(terminal)
for _,k in pairs(methods) do
terminal[k] = function(...)
if not socket.queue then
socket.queue = { }
Event.onTimeout(0, function()
socket:write(socket.queue)
socket.queue = nil
end)
end
table.insert(socket.queue, {
f = k,
args = { ... },
})
oldTerm[k](...)
end
end
while true do
local data = socket:read()
if not data then
print('vnc: closing connection to ' .. socket.dhost)
break
end
if data.type == 'shellRemote' then
os.queueEvent(table.unpack(data.event))
elseif data.type == 'termInfo' then
terminal.getSize = function()
return data.width, data.height
end
os.queueEvent('term_resize')
end
end
for k,v in pairs(oldTerm) do
terminal[k] = v
end
os.queueEvent('term_resize')
end
Event.addRoutine(function()
print('vnc: listening on port 5900')
while true do
local socket = Socket.server(5900)
print('vnc: connection from ' .. socket.dhost)
-- no new process - only 1 connection allowed
-- due to term size issues
local s, m = pcall(vncHost, socket)
socket:close()
if not s and m then
print('vnc error')
_G.printError(m)
end
end
end)
Event.addRoutine(function()
print('svnc: listening on port 5901')
while true do
local socket = Socket.server(5901, { ENCRYPT = true })
print('svnc: connection from ' .. socket.dhost)
-- no new process - only 1 connection allowed
-- due to term size issues
local s, m = pcall(vncHost, socket)
socket:close()
if not s and m then
print('vnc error')
_G.printError(m)
end
end
end)

186
sys/apps/package.lua Normal file
View File

@@ -0,0 +1,186 @@
local BulkGet = require('opus.bulkget')
local Config = require('opus.config')
local Git = require('opus.git')
local LZW = require('opus.compress.lzw')
local Packages = require('opus.packages')
local Tar = require('opus.compress.tar')
local Util = require('opus.util')
local fs = _G.fs
local term = _G.term
local args = { ... }
local action = table.remove(args, 1)
local function makeSandbox()
local sandbox = setmetatable(Util.shallowCopy(_ENV), { __index = _G })
_G.requireInjector(sandbox)
return sandbox
end
local function Syntax(msg)
print('Syntax: package list | install [name] ... | update [name] | updateall | uninstall [name]\n')
error(msg)
end
local function progress(max)
-- modified from: https://pastebin.com/W5ZkVYSi (apemanzilla)
local _, y = term.getCursorPos()
local wide, _ = term.getSize()
term.setCursorPos(1, y)
term.write("[")
term.setCursorPos(wide - 6, y)
term.write("]")
local done = 0
return function()
done = done + 1
local value = done / max
term.setCursorPos(2,y)
term.write(("="):rep(math.floor(value * (wide - 8))))
local percent = math.floor(value * 100) .. "%"
term.setCursorPos(wide - percent:len(),y)
term.write(percent)
end
end
local function runScript(script)
if script then
local s, m = pcall(function()
local fn, m = load(script, 'script', nil, makeSandbox())
if not fn then
error(m)
end
fn()
end)
if not s and m then
_G.printError(m)
end
end
end
local function install(name, isUpdate, ignoreDeps)
local manifest = Packages:downloadManifest(name) or error('Invalid package')
if not ignoreDeps then
if manifest.required then
for _, v in pairs(manifest.required) do
if isUpdate or not Packages:isInstalled(v) then
install(v, isUpdate)
end
end
end
end
print(string.format('%s: %s',
isUpdate and 'Updating' or 'Installing',
name))
local packageDir = fs.combine('packages', name)
local list = Git.list(manifest.repository)
-- apply exclude filters from manifest
if manifest.exclude then
for path in pairs(list) do
for _, pattern in ipairs(manifest.exclude) do
if path:match(pattern) then
list[path] = nil
break
end
end
end
end
-- clear out contents before install/update
-- TODO: figure out whether to run
-- install/uninstall for the package
fs.delete(packageDir)
local showProgress = progress(Util.size(list))
local getList = { }
for path, entry in pairs(list) do
table.insert(getList, {
path = fs.combine(packageDir, path),
url = entry.url
})
end
BulkGet.download(getList, function(_, s, m)
if not s then
error(m)
end
showProgress()
end)
if not isUpdate then
runScript(manifest.install)
end
if Config.load('package').compression then
local c = Tar.tar_string(packageDir)
Util.writeFile(packageDir .. '.tar.lzw', LZW.compress(c), 'wb')
fs.delete(packageDir)
end
end
if action == 'list' then
for k in pairs(Packages:list()) do
Util.print('[%s] %s', Packages:isInstalled(k) and 'x' or ' ', k)
end
return
end
if action == 'install' then
local name = args[1] or Syntax('Invalid package')
if Packages:isInstalled(name) then
error('Package is already installed')
end
install(name)
print('installation complete\n')
_G.printError('Reboot is required')
return
end
if action == 'refresh' then
print('Downloading...')
Packages:downloadList()
print('refresh complete')
return
end
if action == 'updateall' then
for name in pairs(Packages:installed()) do
install(name, true, true)
end
print('updateall complete')
return
end
if action == 'update' then
local name = args[1] or Syntax('Invalid package')
if not Packages:isInstalled(name) then
error('Package is not installed')
end
install(name, true)
print('update complete')
return
end
if action == 'uninstall' then
local name = args[1] or Syntax('Invalid package')
if not Packages:isInstalled(name) then
error('Package is not installed')
end
local manifest = Packages:getManifest(name)
runScript(manifest.uninstall)
local packageDir = fs.combine('packages', name)
fs.delete(packageDir)
fs.delete(packageDir .. '.tar.lzw')
print('removed: ' .. packageDir)
return
end
Syntax('Invalid command')

View File

@@ -1,12 +1,9 @@
requireInjector(getfenv(1)) local Security = require('opus.security')
local Terminal = require('opus.terminal')
local Security = require('security')
local SHA1 = require('sha1')
local Terminal = require('terminal')
local password = Terminal.readPassword('Enter new password: ') local password = Terminal.readPassword('Enter new password: ')
if password then if password then
Security.updatePassword(SHA1.sha1(password)) Security.updatePassword(password)
print('Password updated') print('Password updated')
end end

114
sys/apps/pastebin.lua Normal file
View File

@@ -0,0 +1,114 @@
local function printUsage()
print( "Usages:" )
print( "pastebin put <filename>" )
print( "pastebin get <code> <filename>" )
print( "pastebin run <code> <arguments>" )
end
if not http then
printError( "Pastebin requires http API" )
printError( "Set http_enable to true in ComputerCraft.cfg" )
return
end
local pastebin = require('opus.http.pastebin')
local tArgs = { ... }
local sCommand = tArgs[1]
if sCommand == "put" then
-- Upload a file to pastebin.com
if #tArgs < 2 then
printUsage()
return
end
-- Determine file to upload
local sFile = tArgs[2]
local sPath = shell.resolve( sFile )
if not fs.exists( sPath ) or fs.isDir( sPath ) then
print( "No such file" )
return
end
print( "Connecting to pastebin.com... " )
local resp, msg = pastebin.put(sPath)
if resp then
print( "Uploaded as " .. resp )
print( "Run \"pastebin get "..resp.."\" to download anywhere" )
else
printError( msg )
end
elseif sCommand == "get" then
-- Download a file from pastebin.com
if #tArgs < 3 then
printUsage()
return
end
local sCode = pastebin.parseCode(tArgs[2])
if not sCode then
return false, "Invalid pastebin code. The code is the ID at the end of the pastebin.com URL."
end
-- Determine file to download
local sFile = tArgs[3]
local sPath = shell.resolve( sFile )
if fs.exists( sPath ) then
printError( "File already exists" )
return
end
print( "Connecting to pastebin.com... " )
local resp, msg = pastebin.get(sCode, sPath)
if resp then
print( "Downloaded as " .. sPath )
else
printError( msg )
end
elseif sCommand == "run" then
-- Download and run a file from pastebin.com
if #tArgs < 2 then
printUsage()
return
end
local sCode = pastebin.parseCode(tArgs[2])
if not sCode then
return false, "Invalid pastebin code. The code is the ID at the end of the pastebin.com URL."
end
print( "Connecting to pastebin.com... " )
local res, msg = pastebin.download(sCode)
if not res then
printError( msg )
return res, msg
end
res, msg = load(res, sCode, "t", _ENV)
if not res then
printError( msg )
return res, msg
end
res, msg = pcall(res, table.unpack(tArgs, 3))
if not res then
printError( msg )
end
else
printUsage()
return
end

View File

@@ -1,643 +0,0 @@
local parentShell = shell
shell = { }
multishell = multishell or { }
local sandboxEnv = setmetatable({ }, { __index = _G })
for k,v in pairs(getfenv(1)) do
sandboxEnv[k] = v
end
sandboxEnv.shell = shell
sandboxEnv.multishell = multishell
requireInjector(getfenv(1))
local Util = require('util')
local DIR = (parentShell and parentShell.dir()) or ""
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
local ALIASES = (parentShell and parentShell.aliases()) or {}
local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {}
local bExit = false
local tProgramStack = {}
local function parseCommandLine( ... )
local sLine = table.concat( { ... }, " " )
local tWords = {}
local bQuoted = false
for match in string.gmatch( sLine .. "\"", "(.-)\"" ) do
if bQuoted then
table.insert( tWords, match )
else
for m in string.gmatch( match, "[^ \t]+" ) do
table.insert( tWords, m )
end
end
bQuoted = not bQuoted
end
return table.remove(tWords, 1), tWords
end
-- Install shell API
function shell.run(...)
local path, args = parseCommandLine(...)
local isUrl = not not path:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
if not isUrl then
path = shell.resolveProgram(path)
end
if path then
tProgramStack[#tProgramStack + 1] = path
local oldTitle
if multishell and multishell.getTitle then
oldTitle = multishell.getTitle(multishell.getCurrent())
multishell.setTitle(multishell.getCurrent(), fs.getName(path))
end
local result, err
local env = Util.shallowCopy(sandboxEnv)
if isUrl then
result, err = Util.runUrl(env, path, unpack(args))
else
result, err = Util.run(env, path, unpack(args))
end
tProgramStack[#tProgramStack] = nil
if multishell and multishell.getTitle then
multishell.setTitle(multishell.getCurrent(), oldTitle or 'shell')
end
return result, err
end
return false, 'No such program'
end
function shell.exit()
bExit = true
end
function shell.dir() return DIR end
function shell.setDir(d) DIR = d end
function shell.path() return PATH end
function shell.setPath(p) PATH = p end
function shell.resolve( _sPath )
local sStartChar = string.sub( _sPath, 1, 1 )
if sStartChar == "/" or sStartChar == "\\" then
return fs.combine( "", _sPath )
else
return fs.combine(DIR, _sPath )
end
end
function shell.resolveProgram( _sCommand )
if ALIASES[ _sCommand ] ~= nil then
_sCommand = ALIASES[ _sCommand ]
end
local path = shell.resolve(_sCommand)
if fs.exists(path) and not fs.isDir(path) then
return path
end
if fs.exists(path .. '.lua') then
return path .. '.lua'
end
-- If the path is a global path, use it directly
local sStartChar = string.sub( _sCommand, 1, 1 )
if sStartChar == "/" or sStartChar == "\\" then
local sPath = fs.combine( "", _sCommand )
if fs.exists( sPath ) and not fs.isDir( sPath ) then
return sPath
end
return nil
end
-- Otherwise, look on the path variable
for sPath in string.gmatch(PATH or '', "[^:]+") do
sPath = fs.combine(sPath, _sCommand )
if fs.exists( sPath ) and not fs.isDir( sPath ) then
return sPath
end
if fs.exists(sPath .. '.lua') then
return sPath .. '.lua'
end
end
-- Not found
return nil
end
function shell.programs( _bIncludeHidden )
local tItems = {}
-- Add programs from the path
for sPath in string.gmatch(PATH, "[^:]+") do
sPath = shell.resolve(sPath)
if fs.isDir( sPath ) then
local tList = fs.list( sPath )
for n,sFile in pairs( tList ) do
if not fs.isDir( fs.combine( sPath, sFile ) ) and
(_bIncludeHidden or string.sub( sFile, 1, 1 ) ~= ".") then
tItems[ sFile ] = true
end
end
end
end
-- Sort and return
local tItemList = {}
for sItem, b in pairs( tItems ) do
table.insert( tItemList, sItem )
end
table.sort( tItemList )
return tItemList
end
function shell.complete(sLine) end
function shell.completeProgram(sProgram) end
function shell.setCompletionFunction(sProgram, fnComplete)
tCompletionInfo[sProgram] = { fnComplete = fnComplete }
end
function shell.getCompletionInfo()
return tCompletionInfo
end
function shell.getRunningProgram()
return tProgramStack[#tProgramStack]
end
function shell.setAlias( _sCommand, _sProgram )
ALIASES[ _sCommand ] = _sProgram
end
function shell.clearAlias( _sCommand )
ALIASES[ _sCommand ] = nil
end
function shell.aliases()
local tCopy = {}
for sAlias, sCommand in pairs(ALIASES) do
tCopy[sAlias] = sCommand
end
return tCopy
end
function shell.newTab(tabInfo, ...)
local path, args = parseCommandLine(...)
path = shell.resolveProgram(path)
if path then
tabInfo.path = path
tabInfo.env = sandboxEnv
tabInfo.args = Util.shallowCopy(args)
tabInfo.title = fs.getName(path)
if path ~= 'sys/apps/shell' then
table.insert(tabInfo.args, 1, tabInfo.path)
tabInfo.path = 'sys/apps/shell'
end
return multishell.openTab(tabInfo)
end
return nil, 'No such program'
end
function shell.openTab( ... )
return shell.newTab({ }, ...)
end
function shell.openForegroundTab( ... )
return shell.newTab({ focused = true }, ...)
end
function shell.openHiddenTab( ... )
return shell.newTab({ hidden = true }, ...)
end
function shell.switchTab(tabId)
multishell.setFocus(tabId)
end
local tArgs = { ... }
if #tArgs > 0 then
local path, args = parseCommandLine(...)
if not path then
error('No such program')
end
local isUrl = not not path:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
if not isUrl then
path = shell.resolveProgram(path)
if not path then
error('No such program')
end
end
local fn, err
if isUrl then
fn, err = Util.loadUrl(path, getfenv(1))
else
fn, err = loadfile(path, getfenv(1))
end
if not fn then
error(err)
end
tProgramStack[#tProgramStack + 1] = path
return fn(table.unpack(args))
end
local Config = require('config')
local History = require('history')
local config = {
standard = {
textColor = colors.white,
commandTextColor = colors.lightGray,
directoryTextColor = colors.gray,
directoryBackgroundColor = colors.black,
promptTextColor = colors.gray,
promptBackgroundColor = colors.black,
directoryColor = colors.gray,
},
color = {
textColor = colors.white,
commandTextColor = colors.yellow,
directoryTextColor = colors.orange,
directoryBackgroundColor = colors.black,
promptTextColor = colors.blue,
promptBackgroundColor = colors.black,
directoryColor = colors.green,
},
displayDirectory = true,
}
--Config.load('shell', config)
local _colors = config.standard
if term.isColor() then
_colors = config.color
end
local function autocompleteFile(results, words)
local function getBaseDir(path)
if #path > 1 then
if path:sub(-1) ~= '/' then
path = fs.getDir(path)
end
end
if path:sub(1, 1) == '/' then
path = fs.combine(path, '')
else
path = fs.combine(shell.dir(), path)
end
while not fs.isDir(path) do
path = fs.getDir(path)
end
return path
end
local function getRawPath(path)
local baseDir = ''
if path:sub(1, 1) ~= '/' then
baseDir = shell.dir()
end
if #path > 1 then
if path:sub(-1) ~= '/' then
path = fs.getDir(path)
end
end
if fs.isDir(fs.combine(baseDir, path)) then
return path
end
return fs.getDir(path)
end
local match = words[#words] or ''
local startDir = getBaseDir(match)
local rawPath = getRawPath(match)
if fs.isDir(startDir) then
local files = fs.list(startDir)
for _,f in pairs(files) do
local path = fs.combine(rawPath, f)
if fs.isDir(fs.combine(startDir, f)) then
results[path .. '/'] = 'directory'
else
results[path .. ' '] = 'program'
end
end
end
end
local function autocompleteProgram(results, words)
if #words == 1 then
local files = shell.programs(true)
for _,f in ipairs(files) do
results[f .. ' '] = 'program'
end
for f in pairs(ALIASES) do
results[f .. ' '] = 'program'
end
end
end
local function autocompleteArgument(results, program, words)
local word = ''
if #words > 1 then
word = words[#words]
end
local tInfo = tCompletionInfo[program]
local args = tInfo.fnComplete(shell, #words - 1, word, words)
if args then
Util.filterInplace(args, function(f)
return not Util.key(args, f .. '/')
end)
for _,arg in ipairs(args) do
results[word .. arg] = 'argument'
end
end
end
local function autocomplete(line, suggestions)
local words = { }
for word in line:gmatch("%S+") do
table.insert(words, word)
end
if line:match(' $') then
table.insert(words, '')
end
local results = { }
if #words == 0 then
files = autocompleteFile(results, words)
else
local program = shell.resolveProgram(words[1])
if tCompletionInfo[program] then
autocompleteArgument(results, program, words)
else
autocompleteProgram(results, words)
autocompleteFile(results, words)
end
end
local match = words[#words] or ''
local files = { }
for f in pairs(results) do
if f:sub(1, #match) == match then
table.insert(files, f)
end
end
if #files == 1 then
words[#words] = files[1]
return table.concat(words, ' ')
elseif #files > 1 and suggestions then
print()
local word = words[#words] or ''
local prefix = word:match("(.*/)") or ''
if #prefix > 0 then
for _,f in ipairs(files) do
if f:match("^" .. prefix) ~= prefix then
prefix = ''
break
end
end
end
local tDirs, tFiles = { }, { }
for _,f in ipairs(files) do
if results[f] == 'directory' then
f = f:gsub(prefix, '', 1)
table.insert(tDirs, f)
else
f = f:gsub(prefix, '', 1)
table.insert(tFiles, f)
end
end
table.sort(tDirs)
table.sort(tFiles)
if #tDirs > 0 and #tDirs < #tFiles then
local w = term.getSize()
local nMaxLen = w / 8
for n, sItem in pairs(files) do
nMaxLen = math.max(string.len(sItem) + 1, nMaxLen)
end
local nCols = math.floor(w / nMaxLen)
if #tDirs < nCols then
for i = #tDirs + 1, nCols do
table.insert(tDirs, '')
end
end
end
if #tDirs > 0 then
textutils.tabulate(_colors.directoryColor, tDirs, colors.white, tFiles)
else
textutils.tabulate(colors.white, tFiles)
end
term.setTextColour(_colors.promptTextColor)
term.setBackgroundColor(_colors.promptBackgroundColor)
write("$ " )
term.setTextColour(_colors.commandTextColor)
term.setBackgroundColor(colors.black)
return line
elseif #files > 1 then
-- ugly (complete as much as possible)
local word = words[#words] or ''
local i = #word + 1
while true do
local ch
for _,f in ipairs(files) do
if #f < i then
words[#words] = string.sub(f, 1, i - 1)
return table.concat(words, ' ')
end
if not ch then
ch = string.sub(f, i, i)
elseif string.sub(f, i, i) ~= ch then
if i == #word + 1 then
return
end
words[#words] = string.sub(f, 1, i - 1)
return table.concat(words, ' ')
end
end
i = i + 1
end
end
end
local function shellRead(history)
term.setCursorBlink( true )
local sLine = ""
local nPos = 0
local lastPattern
local w = term.getSize()
local sx = term.getCursorPos()
history:reset()
local function redraw( sReplace )
local nScroll = 0
if sx + nPos >= w then
nScroll = (sx + nPos) - w
end
local cx,cy = term.getCursorPos()
term.setCursorPos( sx, cy )
if sReplace then
term.write( string.rep( sReplace, math.max( string.len(sLine) - nScroll, 0 ) ) )
else
term.write( string.sub( sLine, nScroll + 1 ) )
end
term.setCursorPos( sx + nPos - nScroll, cy )
end
while true do
local sEvent, param, param2 = os.pullEventRaw()
if sEvent == "char" then
sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
nPos = nPos + 1
redraw()
elseif sEvent == "paste" then
sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
nPos = nPos + string.len( param )
redraw()
elseif sEvent == 'mouse_click' and param == 2 then
redraw(string.rep(' ', #sLine))
sLine = ''
nPos = 0
redraw()
elseif sEvent == 'terminate' then
bExit = true
break
elseif sEvent == "key" then
if param == keys.enter then
-- Enter
break
elseif param == keys.tab then
if nPos == #sLine then
local showSuggestions = lastPattern == sLine
lastPattern = sLine
local cline = autocomplete(sLine, showSuggestions)
if cline then
sLine = cline
nPos = #sLine
redraw()
end
end
elseif param == keys.left then
if nPos > 0 then
nPos = nPos - 1
redraw()
end
elseif param == keys.right then
if nPos < string.len(sLine) then
redraw(" ")
nPos = nPos + 1
redraw()
end
elseif param == keys.up or param == keys.down then
redraw(" ")
if param == keys.up then
sLine = history:back()
else
sLine = history:forward()
end
if sLine then
nPos = string.len(sLine)
else
sLine = ""
nPos = 0
end
redraw()
elseif param == keys.backspace then
if nPos > 0 then
redraw(" ")
sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 )
nPos = nPos - 1
redraw()
end
elseif param == keys.home then
redraw(" ")
nPos = 0
redraw()
elseif param == keys.delete then
if nPos < string.len(sLine) then
redraw(" ")
sLine = string.sub( sLine, 1, nPos ) .. string.sub( sLine, nPos + 2 )
redraw()
end
elseif param == keys["end"] then
redraw(" ")
nPos = string.len(sLine)
redraw()
end
elseif sEvent == "term_resize" then
w = term.getSize()
redraw()
end
end
local cx, cy = term.getCursorPos()
term.setCursorPos( w + 1, cy )
print()
term.setCursorBlink( false )
return sLine
end
local history = History.load('usr/.shell_history', 25)
while not bExit do
if config.displayDirectory then
term.setTextColour(_colors.directoryTextColor)
term.setBackgroundColor(_colors.directoryBackgroundColor)
print('==' .. os.getComputerLabel() .. ':/' .. DIR)
end
term.setTextColour(_colors.promptTextColor)
term.setBackgroundColor(_colors.promptBackgroundColor)
write("$ " )
term.setTextColour(_colors.commandTextColor)
term.setBackgroundColor(colors.black)
local sLine = shellRead(history)
if bExit then -- terminated
break
end
sLine = Util.trim(sLine)
if #sLine > 0 and sLine ~= 'exit' then
history:add(sLine)
end
term.setTextColour(_colors.textColor)
if #sLine > 0 then
local result, err = shell.run( sLine )
if not result and err then
printError(err)
end
end
end

680
sys/apps/shell.lua Normal file
View File

@@ -0,0 +1,680 @@
local parentShell = _ENV.shell
_ENV.shell = { }
local trace = require('opus.trace')
local Util = require('opus.util')
local fs = _G.fs
local settings = _G.settings
local shell = _ENV.shell
local DIR = (parentShell and parentShell.dir()) or ""
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
local tAliases = (parentShell and parentShell.aliases()) or {}
local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {}
local bExit = false
local tProgramStack = {}
local function tokenise(...)
local sLine = table.concat({ ... }, ' ')
local tWords = { }
local bQuoted = false
for match in string.gmatch(sLine .. "\"", "(.-)\"") do
if bQuoted then
table.insert(tWords, match)
else
for m in string.gmatch(match, "[^ \t]+") do
table.insert(tWords, m)
end
end
bQuoted = not bQuoted
end
return tWords
end
local defaultHandlers = {
function(env, command, args)
return command:match("^(https?:)") and {
title = fs.getName(command),
path = command,
args = args,
load = Util.loadUrl,
env = env,
}
end,
function(env, command, args)
command = env.shell.resolveProgram(command)
or error('No such program')
_G.requireInjector(env, fs.getDir(command))
return {
title = fs.getName(command):match('([^%.]+)'),
path = command,
args = args,
load = loadfile,
env = env,
}
end,
}
function shell.getHandlers()
if parentShell and parentShell.getHandlers then
return parentShell.getHandlers()
end
return defaultHandlers
end
local handlers = shell.getHandlers()
function shell.registerHandler(fn)
table.insert(handlers, 1, fn)
end
local function handleCommand(env, command, args)
for _,v in pairs(handlers) do
local pi = v(env, command, args)
if pi then
return pi
end
end
end
local function run(...)
local args = tokenise(...)
if #args == 0 then
error('No such program')
end
local pi = handleCommand(shell.makeEnv(_ENV), table.remove(args, 1), args)
local O_v_O, err = pi.load(pi.path, pi.env)
if not O_v_O then
error(err, -1)
end
if _ENV.multishell then
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), pi.title)
end
tProgramStack[#tProgramStack + 1] = pi
pi.env[ "arg" ] = { [0] = pi.path, table.unpack(pi.args) }
local r = { O_v_O(table.unpack(pi.args)) }
tProgramStack[#tProgramStack] = nil
return table.unpack(r)
end
-- Install shell API
function shell.run(...)
local oldTitle
if _ENV.multishell then
oldTitle = _ENV.multishell.getTitle(_ENV.multishell.getCurrent())
end
local r = { trace(run, ...) }
if _ENV.multishell then
_ENV.multishell.setTitle(_ENV.multishell.getCurrent(), oldTitle or 'shell')
end
return table.unpack(r)
end
function shell.exit()
bExit = true
end
function shell.dir() return DIR end
function shell.setDir(d)
d = fs.combine(d, '')
if not fs.isDir(d) then
error("Not a directory", 2)
end
DIR = d
end
function shell.path() return PATH end
function shell.setPath(p) PATH = p end
function shell.resolve( _sPath )
local sStartChar = string.sub( _sPath, 1, 1 )
if sStartChar == "/" or sStartChar == "\\" then
return fs.combine( "", _sPath )
else
return fs.combine(DIR, _sPath )
end
end
function shell.resolveProgram(_sCommand)
if tAliases[_sCommand] ~= nil then
_sCommand = tAliases[_sCommand]
end
local function check(f)
return fs.exists(f) and not fs.isDir(f) and f
end
local function inPath()
-- Otherwise, look on the path variable
for sPath in string.gmatch(PATH or '', "[^:]+") do
sPath = fs.combine(sPath, _sCommand )
if check(sPath) then
return sPath
end
if check(sPath .. '.lua') then
return sPath .. '.lua'
end
end
end
-- so... even if you are in the rom directory and you run:
-- 'packages/common/edit.lua', allow this even though it
-- does not use a leading slash. Ideally, fs.combine would
-- provide the leading slash... but it does not.
return (not _sCommand:find('/')) and inPath()
or check(shell.resolve(_sCommand))
or check(shell.resolve(_sCommand) .. '.lua')
or check(_sCommand)
or check(_sCommand .. '.lua')
end
function shell.programs(_bIncludeHidden)
local tItems = { }
-- Add programs from the path
for sPath in string.gmatch(PATH, "[^:]+") do
sPath = shell.resolve(sPath)
if fs.isDir( sPath ) then
local tList = fs.list( sPath )
for _,sFile in pairs( tList ) do
if not fs.isDir( fs.combine( sPath, sFile ) ) and
(_bIncludeHidden or string.sub( sFile, 1, 1 ) ~= ".") then
tItems[ sFile ] = true
end
end
end
end
-- Sort and return
local tItemList = { }
for sItem in pairs(tItems) do
table.insert(tItemList, sItem)
end
table.sort(tItemList)
return tItemList
end
function shell.completeProgram(sLine)
if #sLine > 0 and string.sub(sLine, 1, 1) == '/' then
-- Add programs from the root
return fs.complete(sLine, '', true, false)
end
local tResults = { }
local tSeen = { }
-- Add aliases
for sAlias in pairs( tAliases ) do
if #sAlias > #sLine and string.sub(sAlias, 1, #sLine) == sLine then
local sResult = string.sub(sAlias, #sLine + 1)
if not tSeen[sResult] then
table.insert(tResults, sResult .. ' ')
tSeen[sResult] = true
end
end
end
-- Add programs from the path
local tPrograms = shell.programs()
for n=1,#tPrograms do
local sProgram = tPrograms[n]
if #sProgram >= #sLine and string.sub(sProgram, 1, #sLine) == sLine then
local sResult = string.sub(sProgram, #sLine + 1)
if not tSeen[sResult] then
table.insert(tResults, sResult .. ' ')
tSeen[sResult] = true
end
end
end
-- Sort and return
table.sort(tResults)
return tResults
end
function shell.complete(sLine)
local tWords = tokenise(sLine)
local nIndex = #tWords
if string.sub(sLine, #sLine, #sLine) == ' ' and #Util.trim(sLine) > 0 then
nIndex = nIndex + 1
end
if nIndex == 0 then
return fs.complete('', shell.dir(), true, false)
elseif nIndex == 1 then
local results = shell.completeProgram(tWords[1] or '')
for _, v in pairs(fs.complete(table.concat(tWords, ' '), shell.dir(), true, false)) do
table.insert(results, v)
end
return results
else
local sPath = shell.resolveProgram(tWords[1])
local sPart = tWords[nIndex] or ''
local tPreviousParts = tWords
tPreviousParts[nIndex] = nil
local results
local tInfo = tCompletionInfo[sPath]
if tInfo then
results = tInfo.fnComplete(shell, nIndex - 1, sPart, tPreviousParts)
end
return results and #results > 0 and results
or fs.complete(sPart, shell.dir(), true, false)
end
end
function shell.setCompletionFunction(sProgram, fnComplete)
tCompletionInfo[sProgram] = { fnComplete = fnComplete }
end
function shell.getCompletionInfo()
return tCompletionInfo
end
function shell.getRunningProgram()
return tProgramStack[#tProgramStack] and tProgramStack[#tProgramStack].path
end
function shell.getRunningInfo()
return tProgramStack[#tProgramStack]
end
-- convenience function for making a runnable env
function shell.makeEnv(env, dir)
env = setmetatable(Util.shallowCopy(env), { __index = _G })
_G.requireInjector(env, dir)
return env
end
function shell.setAlias(_sCommand, _sProgram)
tAliases[_sCommand] = _sProgram
end
function shell.clearAlias(_sCommand)
tAliases[_sCommand] = nil
end
function shell.aliases()
local tCopy = {}
for sAlias, sCommand in pairs(tAliases) do
tCopy[sAlias] = sCommand
end
return tCopy
end
function shell.newTab(tabInfo, ...)
local args = tokenise(...)
local path = table.remove(args, 1)
path = shell.resolveProgram(path)
if path then
tabInfo.path = path
tabInfo.args = args
tabInfo.title = fs.getName(path):match('([^%.]+)')
if path ~= 'sys/apps/shell.lua' then
table.insert(tabInfo.args, 1, tabInfo.path)
tabInfo.path = 'sys/apps/shell.lua'
end
return _ENV.multishell.openTab(_ENV, tabInfo)
end
return nil, 'No such program'
end
if not _ENV.multishell then
function shell.newTab()
error('Multishell is not available')
end
end
function shell.openTab(...)
return shell.newTab({ }, ...)
end
function shell.openForegroundTab( ... )
return shell.newTab({ focused = true }, ...)
end
function shell.openHiddenTab( ... )
return shell.newTab({ hidden = true }, ...)
end
function shell.switchTab(tabId)
_ENV.multishell.setFocus(tabId)
end
local tArgs = { ... }
if #tArgs > 0 then
return run(...)
end
local Config = require('opus.config')
local Entry = require('opus.entry')
local History = require('opus.history')
local Input = require('opus.input')
local Sound = require('opus.sound')
local Terminal = require('opus.terminal')
local colors = _G.colors
local os = _G.os
local term = _G.term
local textutils = _G.textutils
local oldTerm
local terminal = term.current()
local _len = string.len
local _rep = string.rep
local _sub = string.sub
local config = {
color = {
textColor = colors.white,
commandTextColor = colors.yellow,
directoryTextColor = colors.orange,
promptTextColor = colors.blue,
directoryColor = colors.green,
fileColor = colors.white,
backgroundColor = colors.black,
},
displayDirectory = true,
}
Config.load('shellprompt', config)
local _colors = config.color
-- temp
if not _colors.backgroundColor then
_colors.backgroundColor = colors.black
_colors.fileColor = colors.white
end
if not terminal.scrollUp then
terminal = Terminal.window(term.current())
terminal.setMaxScroll(200)
oldTerm = term.redirect(terminal)
term.setBackgroundColor(_colors.backgroundColor)
term.clear()
end
local palette = terminal.canvas.palette
local function autocomplete(line)
local words = { }
for word in line:gmatch("%S+") do
table.insert(words, word)
end
if line:match(' $') then
table.insert(words, '')
end
if #words == 0 then
words = { '' }
end
local results = shell.complete(line) or { }
Util.filterInplace(results, function(f)
return not Util.key(results, f .. '/')
end)
local w = words[#words] or ''
for k,arg in pairs(results) do
results[k] = w .. arg
end
if #results == 1 then
words[#words] = results[1]
return table.concat(words, ' ')
elseif #results > 1 then
local function someComplete()
-- ugly (complete as much as possible)
local word = words[#words] or ''
local i = #word + 1
while true do
local ch
for _,f in ipairs(results) do
if #f < i then
words[#words] = _sub(f, 1, i - 1)
return table.concat(words, ' ')
end
if not ch then
ch = _sub(f, i, i)
elseif _sub(f, i, i) ~= ch then
if i == #word + 1 then
return
end
words[#words] = _sub(f, 1, i - 1)
return table.concat(words, ' ')
end
end
i = i + 1
end
end
local t = someComplete()
if t then
return t
end
print()
local word = words[#words] or ''
local prefix = word:match("(.*/)") or ''
if #prefix > 0 then
for _,f in ipairs(results) do
if f:match("^" .. prefix) ~= prefix then
prefix = ''
break
end
end
end
local tDirs, tFiles = { }, { }
for _,f in ipairs(results) do
if fs.isDir(shell.resolve(f)) then
f = f:gsub(prefix, '', 1)
table.insert(tDirs, f)
else
f = f:gsub(prefix, '', 1)
table.insert(tFiles, f)
end
end
table.sort(tDirs)
table.sort(tFiles)
if #tDirs > 0 and #tDirs < #tFiles then
local tw = term.getSize()
local nMaxLen = tw / 8
for _,sItem in pairs(results) do
nMaxLen = math.max(_len(sItem) + 1, nMaxLen)
end
local nCols = math.floor(tw / nMaxLen)
if #tDirs < nCols then
for _ = #tDirs + 1, nCols do
table.insert(tDirs, '')
end
end
end
if #tDirs > 0 then
textutils.tabulate(_colors.directoryColor, tDirs, _colors.fileColor, tFiles)
else
textutils.tabulate(_colors.fileColor, tFiles)
end
term.setTextColour(_colors.promptTextColor)
term.write("$ " )
term.setTextColour(_colors.commandTextColor)
return line
end
end
local function shellRead(history)
local lastLen = 0
local entry = Entry({
width = term.getSize() - 3,
offset = 3,
})
history:reset()
term.setCursorBlink(true)
local function updateCursor()
term.setCursorPos(3 + entry.pos - entry.scroll, select(2, term.getCursorPos()))
end
local function redraw()
if terminal.scrollBottom then
terminal.scrollBottom()
end
local _,cy = term.getCursorPos()
term.setCursorPos(3, cy)
entry.value = entry.value or ''
local filler = #entry.value < lastLen
and _rep(' ', lastLen - #entry.value)
or ''
local str = _sub(entry.value, entry.scroll + 1, entry.width + entry.scroll) .. filler
local fg = _rep(palette[_colors.commandTextColor], #str)
local bg = _rep(palette[_colors.backgroundColor], #str)
if entry.mark.active then
bg = _rep('f', entry.mark.x) ..
_rep('7', entry.mark.ex - entry.mark.x) ..
_rep('f', #entry.value - entry.mark.ex + #filler + 1)
bg = _sub(bg, entry.scroll + 1, entry.scroll + #str)
end
term.blit(str, fg, bg)
updateCursor()
lastLen = #entry.value
end
while true do
local event, p1, p2, p3 = os.pullEventRaw()
local ie = Input:translate(event, p1, p2, p3)
if ie then
if ie.code == 'scroll_up' and terminal.scrollUp then
terminal.scrollUp()
elseif ie.code == 'scroll_down' and terminal.scrollDown then
terminal.scrollDown()
elseif ie.code == 'terminate' then
bExit = true
break
elseif ie.code == 'enter' then
break
elseif ie.code == 'up' or ie.code == 'control-p' or
ie.code == 'down' or ie.code == 'control-n' then
entry:reset()
if ie.code == 'up' or ie.code == 'control-p' then
entry.value = history:back() or ''
else
entry.value = history:forward() or ''
end
entry.pos = #entry.value
entry:updateScroll()
redraw()
elseif ie.code == 'tab' then
entry.value = entry.value or ''
if entry.pos == #entry.value then
local cline = autocomplete(entry.value)
if cline then
entry.value = cline
entry.pos = #entry.value
entry:unmark()
entry:updateScroll()
redraw()
else
Sound.play('entity.villager.no')
end
end
elseif ie.code == 'control-l' then
term.clear()
term.setCursorPos(1, 0) -- Y:0 ?
break
else
entry:process(ie)
entry.value = entry.value or ''
if entry.textChanged then
redraw()
elseif entry.posChanged then
updateCursor()
end
end
elseif event == "term_resize" then
terminal.reposition(1, 1, oldTerm.getSize())
entry.width = term.getSize() - 3
entry:updateScroll()
redraw()
end
end
print()
term.setCursorBlink(false)
return entry.value or ''
end
local history = History.load('usr/.shell_history', 100)
term.setBackgroundColor(_colors.backgroundColor)
if settings.get("motd.enable") then
shell.run("motd")
end
while not bExit do
if config.displayDirectory then
term.setTextColour(_colors.directoryTextColor)
print('==' .. os.getComputerLabel() .. ':/' .. DIR)
end
term.setTextColour(_colors.promptTextColor)
term.write("$ " )
term.setTextColour(_colors.commandTextColor)
local sLine = shellRead(history)
if bExit then -- terminated
break
end
sLine = Util.trim(sLine)
if #sLine > 0 and sLine ~= 'exit' then
history:add(sLine)
end
term.setTextColour(_colors.textColor)
if #sLine > 0 then
local result, err = shell.run(sLine)
local cx = term.getCursorPos()
if cx ~= 1 then
print()
end
term.setBackgroundColor(_colors.backgroundColor)
if not result and err then
_G.printError(err)
end
end
end
if oldTerm then
term.redirect(oldTerm)
end

View File

@@ -0,0 +1,71 @@
local Config = require('opus.config')
local UI = require('opus.ui')
local kernel = _G.kernel
local aliasTab = UI.Tab {
title = 'Aliases',
description = 'Shell aliases',
alias = UI.TextEntry {
x = 2, y = 2, ex = -2,
limit = 32,
shadowText = 'Alias',
},
path = UI.TextEntry {
y = 3, x = 2, ex = -2,
shadowText = 'Program path',
accelerators = {
enter = 'new_alias',
},
},
grid = UI.Grid {
x = 2, y = 5, ex = -2, ey = -2,
sortColumn = 'alias',
columns = {
{ heading = 'Alias', key = 'alias' },
{ heading = 'Program', key = 'path' },
},
accelerators = {
delete = 'delete_alias',
},
},
}
function aliasTab.grid:draw()
self.values = { }
local env = Config.load('shell')
for k in pairs(kernel.getShell().aliases()) do
kernel.getShell().clearAlias(k)
end
for k,v in pairs(env.aliases) do
table.insert(self.values, { alias = k, path = v })
kernel.getShell().setAlias(k, v)
end
self:update()
UI.Grid.draw(self)
end
function aliasTab:eventHandler(event)
if event.type == 'delete_alias' then
local env = Config.load('shell', { aliases = { } })
env.aliases[self.grid:getSelected().alias] = nil
Config.update('shell', env)
self.grid:setIndex(self.grid:getIndex())
self.grid:draw()
self:emit({ type = 'success_message', message = 'Aliases updated' })
return true
elseif event.type == 'new_alias' then
local env = Config.load('shell', { aliases = { } })
env.aliases[self.alias.value] = self.path.value
Config.update('shell', env)
self.alias:reset()
self.path:reset()
self:draw()
self:setFocus(self.alias)
self:emit({ type = 'success_message', message = 'Aliases updated' })
return true
end
end
return aliasTab

57
sys/apps/system/cloud.lua Normal file
View File

@@ -0,0 +1,57 @@
local Ansi = require('opus.ansi')
local Config = require('opus.config')
local UI = require('opus.ui')
if _G.http.websocket then
local config = Config.load('cloud')
local tab = UI.Tab {
title = 'Cloud',
description = 'Cloud Catcher options',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
key = UI.TextEntry {
x = 3, ex = -3, y = 3,
limit = 32,
value = config.key,
shadowText = 'Cloud key',
accelerators = {
enter = 'update_key',
},
},
button = UI.Button {
x = -8, ex = -2, y = -2,
text = 'Apply',
event = 'update_key',
},
labelText = UI.TextArea {
x = 2, ex = -2, y = 5, ey = -4,
textColor = 'yellow',
backgroundColor = 'black',
marginLeft = 1, marginRight = 1, marginTop = 1,
value = string.format(
[[Use a non-changing cloud key. Note that only a single computer can use this session at one time.
To obtain a key, visit:
%shttps://cloud-catcher.squiddev.cc%s then bookmark:
%shttps://cloud-catcher.squiddev.cc/?id=KEY
]],
Ansi.white, Ansi.reset, Ansi.white),
},
}
function tab:eventHandler(event)
if event.type == 'update_key' then
if self.key.value then
config.key = self.key.value
else
config.key = nil
end
Config.update('cloud', config)
self:emit({ type = 'success_message', message = 'Updated' })
end
end
return tab
end

View File

@@ -0,0 +1,161 @@
local UI = require('opus.ui')
local Event = require('opus.event')
local NFT = require('opus.nft')
local colors = _G.colors
local fs = _G.fs
local os = _G.os
local peripheral = _G.peripheral
local NftImages = {
blank = '\0308\0317\153\153\153\153\153\153\153\153\010\0307\0318\153\153\153\153\153\153\153\153\010\0308\0317\153\153\153\153\153\153\153\153\010\0307\0318\153\153\153\153\153\153\153\153\010\0308\0317\153\153\153\153\153\153\153\153',
drive = '\030 \031 \030b\031b\128\0308\0318\128\128\030f\149\030b\149\031 \139\010\030 \031 \030b\031b\128\128\128\128\128\128\010\030 \031 \030b\031b\128\0300\0317____\030b\031b\128\010\030 \031 \030b\031b\128\0300\0317____\030b\031b\128',
ram = '\030 \031 \128\0318\144\144\144\144\144\031 \128\010\0308\031 \157\0307\0317\128\128\128\128\128\030 \0318\145\010\030 \0318\136\0307\0317\128\0307\0310RAM\0307\128\030 \0318\132\010\0308\031 \157\0307\0317\128\128\128\128\128\030 \0318\145\010\030 \031 \128\0318\129\129\129\129\129\031 \128',
rom = '\030 \031 \128\0318\144\144\144\144\144\031 \128\010\0308\031 \157\0307\0317\128\128\128\128\128\030 \0318\145\010\030 \0318\136\0307\0317\128\0307\0310ROM\0307\128\030 \0318\132\010\0308\031 \157\0307\0317\128\128\128\128\128\030 \0318\145\010\030 \031 \128\0318\129\129\129\129\129\031 \128',
hdd = '\030 \031 \0307\0317\128\0300\135\131\139\0307\128\010\030 \031 \0300\0317\149\0310\128\0307\131\0300\128\0307\149\010\030 \031 \0307\0310\130\0300\0317\144\0308\0310\133\0307\159\129\010\030 \031 \0308\0317\149\129\142\159\0307\128\010\030 \031 \030 \0317\143\143\143\143\143',
}
local tab = UI.Tab {
title = 'Disks Usage',
description = 'Visualise HDD and disks usage',
drives = UI.ScrollingGrid {
x = 2, y = 2,
ex = '47%', ey = -8,
columns = {
{ heading = 'Drive', key = 'name' },
{ heading = 'Side' ,key = 'side', textColor = colors.yellow }
},
sortColumn = 'name',
},
infos = UI.Grid {
x = '52%', y = 2,
ex = -2, ey = -8,
disableHeader = true,
unfocusedBackgroundSelectedColor = colors.black,
inactive = true,
backgroundSelectedColor = colors.black,
columns = {
{ key = 'name' },
{ key = 'value', align = 'right', textColor = colors.yellow },
}
},
[1] = UI.Window {
x = 2, y = -6, ex = -2, ey = -2,
backgroundColor = colors.black,
},
progress = UI.ProgressBar {
x = 11, y = -3,
ex = -3,
},
percentage = UI.Text {
y = -4, width = 5,
x = 12,
--align = 'center',
backgroundColor = colors.black,
},
icon = UI.NftImage {
x = 2, y = -6, ey = -2,
backgroundColor = colors.black,
image = NFT.parse(NftImages.blank)
},
}
local function getDrives()
local unique = { ['hdd'] = true, ['virt'] = true }
local drives = { { name = 'hdd', side = '' } }
for _, drive in pairs(fs.list('/')) do
local side = fs.getDrive(drive)
if side and not unique[side] then
unique[side] = true
table.insert(drives, { name = drive, side = side })
end
end
return drives
end
local function getDriveInfo(p)
local files, dirs, total = 0, 0, 0
if p == "hdd" then p = "/" end
p = fs.combine(p, '')
local drive = fs.getDrive(p)
local function recurse(path)
if fs.getDrive(path) == drive then
if fs.isDir(path) then
if path ~= p then
total = total + 500
dirs = dirs + 1
end
for _, v in pairs(fs.list(path)) do
recurse(fs.combine(path, v))
end
else
local sz = fs.getSize(path)
files = files + 1
if drive == 'rom' then
total = total + sz
else
total = total + math.max(500, sz)
end
end
end
end
recurse(p)
local info = {}
table.insert(info, { name = 'Type', value = peripheral.getType(drive) or drive })
table.insert(info, { name = 'Used', value = total })
table.insert(info, { name = 'Total', value = total + fs.getFreeSpace(p) })
table.insert(info, { name = 'Free', value = fs.getFreeSpace(p) })
table.insert(info, { })
table.insert(info, { name = 'Files', value = files })
table.insert(info, { name = 'Dirs', value = dirs })
return info, math.floor((total / (total + fs.getFreeSpace(p))) * 100)
end
function tab:updateInfo()
local selected = self.drives:getSelected()
local info, percent = getDriveInfo(selected and selected.name or self.drives.values[1].name)
self.infos:setValues(info)
self.progress.value = percent
self.percentage.value = ('%#3d%%'):format(percent)
self.icon.image = NFT.parse(NftImages[info[1].value] or NftImages.blank)
self:draw()
end
function tab:updateDrives()
local drives = getDrives()
self.drives:setValues(drives)
end
function tab:enable()
self:updateDrives()
self:updateInfo()
UI.Tab.enable(self)
self.handler = Event.on({ 'disk', 'disk_eject' }, function()
os.sleep(1)
tab:updateDrives()
tab:updateInfo()
tab:sync()
end)
end
function tab:disable()
Event.off(self.handler)
UI.Tab.disable(self)
end
function tab:eventHandler(event)
if event.type == 'grid_focus_row' then
self:updateInfo()
else
return UI.Tab.eventHandler(self, event)
end
return true
end
return tab

57
sys/apps/system/kiosk.lua Normal file
View File

@@ -0,0 +1,57 @@
local UI = require('opus.ui')
local colors = _G.colors
local peripheral = _G.peripheral
local settings = _G.settings
return peripheral.find('monitor') and UI.Tab {
title = 'Kiosk',
description = 'Kiosk options',
form = UI.Form {
x = 2, y = 2, ex = -2, ey = 5,
manualControls = true,
monitor = UI.Chooser {
formLabel = 'Monitor', formKey = 'monitor',
},
textScale = UI.Chooser {
formLabel = 'Font Size', formKey = 'textScale',
nochoice = 'Small',
choices = {
{ name = 'Small', value = '.5' },
{ name = 'Large', value = '1' },
},
help = 'Adjust text scaling',
},
},
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 = { }
peripheral.find('monitor', function(side)
table.insert(choices, { name = side, value = side })
end)
self.form.monitor.choices = choices
self.form.monitor.value = settings.get('kiosk.monitor')
self.form.textScale.value = settings.get('kiosk.textscale')
UI.Tab.enable(self)
end,
eventHandler = function(self, event)
if event.type == 'choice_change' then
if self.form.monitor.value then
settings.set('kiosk.monitor', self.form.monitor.value)
end
if self.form.textScale.value then
settings.set('kiosk.textscale', self.form.textScale.value)
end
settings.save('.settings')
end
end
}

50
sys/apps/system/label.lua Normal file
View File

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

View File

@@ -0,0 +1,88 @@
local Config = require('opus.config')
local UI = require('opus.ui')
local colors = _G.colors
local fs = _G.fs
local config = Config.load('multishell')
local tab = UI.Tab {
title = 'Launcher',
description = 'Set the application launcher',
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 5,
},
launcherLabel = UI.Text {
x = 3, y = 3,
value = 'Launcher',
},
launcher = UI.Chooser {
x = 13, y = 3, width = 12,
choices = {
{ name = 'Overview', value = 'sys/apps/Overview.lua' },
{ name = 'Shell', value = 'sys/apps/ShellLauncher.lua' },
{ name = 'Custom', value = 'custom' },
},
},
custom = UI.TextEntry {
x = 13, ex = -3, y = 4,
shadowText = 'File name',
},
button = UI.Button {
x = -8, ex = -2, y = -2,
text = 'Apply',
event = 'update',
},
labelText = UI.TextArea {
x = 2, ex = -2, y = 6, ey = -4,
backgroundColor = colors.black,
textColor = colors.yellow,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = 'Choose an application launcher',
},
}
function tab:enable()
local launcher = config.launcher and 'custom' or 'sys/apps/Overview.lua'
for _, v in pairs(self.launcher.choices) do
if v.value == config.launcher then
launcher = v.value
break
end
end
UI.Tab.enable(self)
self.launcher.value = launcher
self.custom.enabled = launcher == 'custom'
end
function tab:eventHandler(event)
if event.type == 'choice_change' then
self.custom.enabled = event.value == 'custom'
if self.custom.enabled then
self.custom.value = config.launcher
end
self:draw()
elseif event.type == 'update' then
local launcher
if self.launcher.value ~= 'custom' then
launcher = self.launcher.value
elseif fs.exists(self.custom.value) and not fs.isDir(self.custom.value) then
launcher = self.custom.value
end
if launcher then
config.launcher = launcher
Config.update('multishell', config)
self:emit({ type = 'success_message', message = 'Updated' })
else
self:emit({ type = 'error_message', message = 'Invalid file' })
end
end
end
return tab

View File

@@ -0,0 +1,62 @@
local Ansi = require('opus.ansi')
local Config = require('opus.config')
local UI = require('opus.ui')
local colors = _G.colors
local device = _G.device
return UI.Tab {
title = 'Network',
description = 'Networking options',
info = UI.TextArea {
x = 2, y = 5, ex = -2, ey = -2,
backgroundColor = colors.black,
marginLeft = 1, marginRight = 1, marginTop = 1,
value = string.format(
[[%sSet the primary modem used for wireless communications.%s
Reboot to take effect.]], Ansi.yellow, Ansi.reset)
},
[1] = UI.Window {
x = 2, y = 2, ex = -2, ey = 4,
},
label = UI.Text {
x = 3, y = 3,
value = 'Modem',
},
modem = UI.Chooser {
x = 10, ex = -3, y = 3,
nochoice = 'auto',
},
enable = function(self)
local width = 7
local choices = {
{ name = 'auto', value = 'auto' },
{ name = 'disable', value = 'none' },
}
for k,v in pairs(device) do
if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
table.insert(choices, { name = k, value = v.name })
width = math.max(width, #k)
end
end
self.modem.choices = choices
--self.modem.width = width + 4
local config = Config.load('os')
self.modem.value = config.wirelessModem or 'auto'
UI.Tab.enable(self)
end,
eventHandler = function(self, event)
if event.type == 'choice_change' then
local config = Config.load('os')
config.wirelessModem = self.modem.value
Config.update('os', config)
self:emit({ type = 'success_message', message = 'reboot to take effect' })
return true
end
end
}

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