Compare commits
491 Commits
develop
...
develop-1.
| Author | SHA1 | Date | |
|---|---|---|---|
| 100b23793c | |||
|
|
9b502553e0 | ||
|
|
c247097e20 | ||
|
|
2597724dab | ||
|
|
5f153721ea | ||
|
|
0a42551ab7 | ||
|
|
76b310d873 | ||
|
|
f26f443b9d | ||
|
|
8fbcc7b8bc | ||
|
|
f7ba900930 | ||
|
|
4104750539 | ||
|
|
3150525ee2 | ||
|
|
7c5f749f02 | ||
|
|
7d9029c706 | ||
|
|
ce6a741690 | ||
|
|
16d233df5d | ||
|
|
f6d09abd54 | ||
|
|
bff84370b2 | ||
|
|
e74db02371 | ||
|
|
2b75fbd1ee | ||
|
|
65bf756084 | ||
|
|
2ff4ee5670 | ||
|
|
5488a7d732 | ||
|
|
37c4ca102c | ||
|
|
ac51771b12 | ||
|
|
6fdce03a10 | ||
|
|
ac91d8ed08 | ||
|
|
2c0db368fa | ||
|
|
a77deb72ec | ||
|
|
01d8d65178 | ||
|
|
f36e5470b1 | ||
|
|
2520f53046 | ||
|
|
f4494d6103 | ||
|
|
48f32946ec | ||
|
|
c24a5a7115 | ||
|
|
624af53f4e | ||
|
|
816ea366ab | ||
|
|
b45cd45bcb | ||
|
|
45f1bd1c5d | ||
|
|
1cb2c5f785 | ||
|
|
42bd4b2b69 | ||
|
|
156b604a58 | ||
|
|
7df4a47ba0 | ||
|
|
9bf91a8762 | ||
|
|
b69dcdeffa | ||
|
|
947f502c6d | ||
|
|
a2af4405e7 | ||
|
|
1bf6daedff | ||
|
|
2fdcc338ad | ||
|
|
4f39604c63 | ||
|
|
e9580d67eb | ||
|
|
039fa73749 | ||
|
|
2d942ef001 | ||
|
|
4d02f75a19 | ||
|
|
6c5cc508b1 | ||
|
|
e7fcd68a0f | ||
|
|
7dfa80f5f3 | ||
|
|
788f6e7de0 | ||
|
|
704ef46b62 | ||
|
|
d678eeeaca | ||
|
|
6a3b38922b | ||
|
|
6009f22d8e | ||
|
|
f951f10df5 | ||
|
|
5c4ab57ec8 | ||
|
|
0359a89e12 | ||
|
|
4796e9e77a | ||
|
|
18b7f540ab | ||
|
|
2105799524 | ||
|
|
b6f439e8dc | ||
|
|
41c0758857 | ||
|
|
d1565c62e0 | ||
|
|
f00bece02b | ||
|
|
9297223640 | ||
|
|
f38afbbd36 | ||
|
|
7b225a7747 | ||
|
|
a7069f7ea8 | ||
|
|
a4f4f34576 | ||
|
|
1cce4aad03 | ||
|
|
26bbb50981 | ||
|
|
97bfae10fb | ||
|
|
985830fcfd | ||
|
|
b93d69c261 | ||
|
|
a7e3318226 | ||
|
|
c7c594d6c3 | ||
|
|
90ce2bb1a5 | ||
|
|
2629f2a172 | ||
|
|
8279c1ae12 | ||
|
|
bd911e80e8 | ||
|
|
a3a819256f | ||
|
|
cfc18e10cd | ||
|
|
b0db0b86bd | ||
|
|
71bbd2d457 | ||
|
|
db9c05fa89 | ||
|
|
3f80faa0fe | ||
|
|
8b14399a20 | ||
|
|
4e6c5172d1 | ||
|
|
c24411717a | ||
|
|
51724ccd78 | ||
|
|
3d3e5400cf | ||
|
|
4018d4bcfb | ||
|
|
4485dd2bd5 | ||
|
|
72560b1de0 | ||
|
|
3d02230742 | ||
|
|
456d2d301f | ||
|
|
cdf8645c88 | ||
|
|
d7f41f2bce | ||
|
|
e2ba9e2a03 | ||
|
|
447f4daa92 | ||
|
|
5afb21b68e | ||
|
|
3488da649e | ||
|
|
7f92286fb1 | ||
|
|
4a46d67304 | ||
|
|
3cbdf7b97b | ||
|
|
74d3d1df33 | ||
|
|
9c97849cff | ||
|
|
c1cbcf6b61 | ||
|
|
49b986499c | ||
|
|
fae7596182 | ||
|
|
cb55b1daab | ||
|
|
fd61416750 | ||
|
|
c622b2f7fb | ||
|
|
98f4bf7a7e | ||
|
|
c5e0ac0af6 | ||
|
|
0188e886c0 | ||
|
|
55f444c37e | ||
|
|
3368fa3266 | ||
|
|
e721eb68b6 | ||
|
|
1543f4d624 | ||
|
|
287adb1235 | ||
|
|
e116caf16e | ||
|
|
d72ae3de4a | ||
|
|
602d12afc5 | ||
|
|
ef9f0e09b6 | ||
|
|
b0d2ce0199 | ||
|
|
7224d441ca | ||
|
|
cdd0b6c4d2 | ||
|
|
39522ee5b1 | ||
|
|
369070e19c | ||
|
|
210c5f5a11 | ||
|
|
942f0bda92 | ||
|
|
cc80e08407 | ||
|
|
9e3cf50ccc | ||
|
|
6204c46cc4 | ||
|
|
db2a28f04c | ||
|
|
28b2ba3386 | ||
|
|
31f43067bf | ||
|
|
424fff7842 | ||
|
|
1e675a2e35 | ||
|
|
ffa412c59d | ||
|
|
a3a8c64be8 | ||
|
|
efa1a5bbf5 | ||
|
|
14057c2bf9 | ||
|
|
3241326a2f | ||
|
|
db48031c7c | ||
|
|
0a828fecc5 | ||
|
|
65c6ebf711 | ||
|
|
053003f429 | ||
|
|
25405f15c8 | ||
|
|
674c6af509 | ||
|
|
c9fd7efc26 | ||
|
|
8a5d30a441 | ||
|
|
4a6af34d7c | ||
|
|
340e37da82 | ||
|
|
9ef4d9ef64 | ||
|
|
57ea46dde7 | ||
|
|
6d8d62d309 | ||
|
|
e9717c4def | ||
|
|
f2cf20c274 | ||
|
|
fba2d48a4b | ||
|
|
5c35a8383e | ||
|
|
aaab059cee | ||
|
|
e5a5f76fb3 | ||
|
|
ad447f36b5 | ||
|
|
774d3ed415 | ||
|
|
457c8a8a52 | ||
|
|
3512b2441d | ||
|
|
2f597d0dc4 | ||
|
|
6e6d4b81cd | ||
|
|
47cb7d0fd7 | ||
|
|
748dcd8f82 | ||
|
|
7e8dc5bd49 | ||
|
|
25bfce0e40 | ||
|
|
8c1ea278fc | ||
|
|
c1430f6dac | ||
|
|
9c4aac27f8 | ||
|
|
b739c0c77e | ||
|
|
b3efbc7438 | ||
|
|
cd2024d7ce | ||
|
|
e084c733f8 | ||
|
|
5dd3001ffd | ||
|
|
c18945467f | ||
|
|
ae98aaf9d5 | ||
|
|
ec7fc5bb23 | ||
|
|
30b7199976 | ||
|
|
09be81be27 | ||
|
|
9ce61b5d98 | ||
|
|
92686381c7 | ||
|
|
d8dd17333c | ||
|
|
4093d4cd0d | ||
|
|
e124666575 | ||
|
|
672cca3084 | ||
|
|
c8f200ebb6 | ||
|
|
87c7e5ff7e | ||
|
|
c43fe19f1d | ||
|
|
51e40a1dd7 | ||
|
|
43c86c263b | ||
|
|
e9559165a4 | ||
|
|
0e1b712adc | ||
|
|
8f17657263 | ||
|
|
4c2d121562 | ||
|
|
ccc7693f46 | ||
|
|
86cd6e3c0e | ||
|
|
cf87c29cf6 | ||
|
|
6bb1ba7669 | ||
|
|
ae2ea81d1d | ||
|
|
f1b9dcc4f4 | ||
|
|
88d06267ea | ||
|
|
4a089ecd85 | ||
|
|
08eac79109 | ||
|
|
ddb5433c01 | ||
|
|
0b222207ba | ||
|
|
9456d31881 | ||
|
|
2f5aea912b | ||
|
|
6ba458646f | ||
|
|
1dcb6d67b7 | ||
|
|
aec5ac0121 | ||
|
|
cd7122921f | ||
|
|
9e85a0dae1 | ||
|
|
61a26d7c55 | ||
|
|
159dc622fd | ||
|
|
86e918667c | ||
|
|
721cd840b3 | ||
|
|
67779ab814 | ||
|
|
1c29197983 | ||
|
|
e75a357209 | ||
|
|
69522e61d4 | ||
|
|
5283de18ed | ||
|
|
00293033c8 | ||
|
|
d90aa0e2fd | ||
|
|
343ce7fdc2 | ||
|
|
c3d52c1aab | ||
|
|
bcd33af599 | ||
|
|
97a442e999 | ||
|
|
3c22a872b0 | ||
|
|
82ec4db50f | ||
|
|
310879a801 | ||
|
|
43163053a5 | ||
|
|
bf870479c4 | ||
|
|
c44dc765da | ||
|
|
59552d4217 | ||
|
|
f4bcd61116 | ||
|
|
0e3cb15356 | ||
|
|
366ab1736b | ||
|
|
a5fc89beee | ||
|
|
0c811ef892 | ||
|
|
9e4eb1207e | ||
|
|
8a030cbc50 | ||
|
|
d32030bec4 | ||
|
|
43a6019b9f | ||
|
|
7749e14cad | ||
|
|
62a3bc1360 | ||
|
|
3beae64ad8 | ||
|
|
54660c6089 | ||
|
|
e6d1e58ce2 | ||
|
|
e1fd4be3a5 | ||
|
|
263a32094c | ||
|
|
8220ed1066 | ||
|
|
a77d55e33f | ||
|
|
f011e7b14f | ||
|
|
c633e935f8 | ||
|
|
70733ab4f2 | ||
|
|
9413785248 | ||
|
|
4f9bd8eb0f | ||
|
|
737ac095de | ||
|
|
19159730a4 | ||
|
|
c1baf107c1 | ||
|
|
78402e89cd | ||
|
|
c9ef150605 | ||
|
|
30bad69417 | ||
|
|
517376db28 | ||
|
|
b71ca0d545 | ||
|
|
8fede6f507 | ||
|
|
82f6d3451d | ||
|
|
ade354660f | ||
|
|
daa86d50b2 | ||
|
|
3f9c219f6b | ||
|
|
398953af54 | ||
|
|
8c50447625 | ||
|
|
7bab4e5bf5 | ||
|
|
235e8d11de | ||
|
|
ea8e981880 | ||
|
|
cd6ed79d11 | ||
|
|
e5312ca6ab | ||
|
|
340636806c | ||
|
|
0c91082003 | ||
|
|
e2508c9037 | ||
|
|
86be65f026 | ||
|
|
08791e87a3 | ||
|
|
04af12b452 | ||
|
|
bd9b2825be | ||
|
|
a569c2c900 | ||
|
|
6f7e27827e | ||
|
|
a9d03b06e9 | ||
|
|
89f95ca45b | ||
|
|
a4145951f7 | ||
|
|
ce9ffa6c3a | ||
|
|
7a57d74e68 | ||
|
|
1e011c8bbd | ||
|
|
69e6af2314 | ||
|
|
4dc6062043 | ||
|
|
cc1a2bfbf4 | ||
|
|
ad60fce9b0 | ||
|
|
8f4324f1d8 | ||
|
|
687fc05445 | ||
|
|
8ae3b33374 | ||
|
|
1c859029f1 | ||
|
|
4485a751bd | ||
|
|
fc46239f44 | ||
|
|
ab4fd29d16 | ||
|
|
c307f4020c | ||
|
|
da35988d50 | ||
|
|
c2ee88ba0e | ||
|
|
440b829f62 | ||
|
|
a36051bf78 | ||
|
|
ae6d8e8ebd | ||
|
|
2d411df7d1 | ||
|
|
d769b35672 | ||
|
|
2eab8b99de | ||
|
|
fd672ac74e | ||
|
|
195dbd365a | ||
|
|
8c1abb21ca | ||
|
|
72b3c7bac9 | ||
|
|
8333b3bbec | ||
|
|
53e0fa3795 | ||
|
|
915085ac5f | ||
|
|
89400ac1bd | ||
|
|
817c345672 | ||
|
|
3574d26caa | ||
|
|
02629e266b | ||
|
|
dddb2a6b97 | ||
|
|
b320c92551 | ||
|
|
496e95a6c4 | ||
|
|
66f9481e7d | ||
|
|
2438a40677 | ||
|
|
073f4c1a6d | ||
|
|
dc915337a0 | ||
|
|
8c794d9107 | ||
|
|
db832025c9 | ||
|
|
5a758f0292 | ||
|
|
2fd2b552fc | ||
|
|
644fd0352f | ||
|
|
eabfde64e9 | ||
|
|
9edb4e7d5b | ||
|
|
cb5f690cf4 | ||
|
|
25031bfdc2 | ||
|
|
712ffdb97c | ||
|
|
7c26258f88 | ||
|
|
3cc368f33a | ||
|
|
f0a65d87ce | ||
|
|
4977ea94d7 | ||
|
|
4387264960 | ||
|
|
b761b3429a | ||
|
|
f5294d4fce | ||
|
|
5ae6434d62 | ||
|
|
aef22987fa | ||
|
|
02b12d87dc | ||
|
|
0e921edaf5 | ||
|
|
6ef23908de | ||
|
|
c95c22ff4d | ||
|
|
da20397b22 | ||
|
|
6143671dfb | ||
|
|
49849ecfea | ||
|
|
852bf3f6a6 | ||
|
|
f19d439314 | ||
|
|
09f436a3c2 | ||
|
|
89e47f8b93 | ||
|
|
44d2d13e40 | ||
|
|
e3a8e4e790 | ||
|
|
8cfeaad061 | ||
|
|
0b326589c2 | ||
|
|
6343589183 | ||
|
|
1264b0149b | ||
|
|
26564cbcc1 | ||
|
|
3de03bef22 | ||
|
|
e14a71ab6a | ||
|
|
5d38c307b3 | ||
|
|
7d64b0c6db | ||
|
|
0aa0841b76 | ||
|
|
bd5cba8656 | ||
|
|
abec4a9740 | ||
|
|
a6c8ecccd4 | ||
|
|
eb8b823674 | ||
|
|
2e94b61dff | ||
|
|
30640fd415 | ||
|
|
3f3a2deaa8 | ||
|
|
68217cfd52 | ||
|
|
3c9f03477a | ||
|
|
70a98e33d7 | ||
|
|
bc4f6cdb95 | ||
|
|
cc9c204670 | ||
|
|
9ee76a3ac6 | ||
|
|
3953ef08cc | ||
|
|
00fea37f3f | ||
|
|
6674c715e0 | ||
|
|
72142e12cc | ||
|
|
7d17a21aa4 | ||
|
|
0e3fb88f1d | ||
|
|
c9a42e2502 | ||
|
|
2517612622 | ||
|
|
c5a821f264 | ||
|
|
34c653ca1b | ||
|
|
1fb5379b11 | ||
|
|
beae4e6eaf | ||
|
|
880a81f655 | ||
|
|
eb799004f8 | ||
|
|
a0d47a5f1a | ||
|
|
0545ff0c68 | ||
|
|
95178b0452 | ||
|
|
9c43a261a8 | ||
|
|
d5896ca873 | ||
|
|
39e8307511 | ||
|
|
99687c3d28 | ||
|
|
3b0ac4e338 | ||
|
|
407b9f38fe | ||
|
|
c6fd959371 | ||
|
|
ccd1711d20 | ||
|
|
62ac7a52ec | ||
|
|
f9359cf77f | ||
|
|
17d55e75ce | ||
|
|
4721b6aec1 | ||
|
|
b3a061d39b | ||
|
|
10a0c3a724 | ||
|
|
68ee419d83 | ||
|
|
cb03e56a1f | ||
|
|
1a4ef3e581 | ||
|
|
99b2fa1b0b | ||
|
|
67be1e0f2f | ||
|
|
c478781cba | ||
|
|
88f126bf2f | ||
|
|
846569952c | ||
|
|
9484153f38 | ||
|
|
b564b1e9be | ||
|
|
7220b372bd | ||
|
|
c75938bef9 | ||
|
|
605e923d48 | ||
|
|
adc5eb286d | ||
|
|
a59fef30f0 | ||
|
|
0cd709a525 | ||
|
|
97c4b7a090 | ||
|
|
b7176e55ad | ||
|
|
8b50127f21 | ||
|
|
ecb3af4672 | ||
|
|
e6a98b34cb | ||
|
|
9ee63da66f | ||
|
|
6ed8468a65 | ||
|
|
4836a6a3a2 | ||
|
|
bbcb8ecd6e | ||
|
|
8db4fed19a | ||
|
|
3dd351cc86 | ||
|
|
a17677730f | ||
|
|
daed3aac38 | ||
|
|
fc9981bff0 | ||
|
|
00461483e1 | ||
|
|
25a917a220 | ||
|
|
098e9650ac | ||
|
|
d6a77ba487 | ||
|
|
f16c7b632b | ||
|
|
cceb6c8409 | ||
|
|
15a627f7e9 | ||
|
|
742d1337a8 | ||
|
|
8879f91c62 | ||
|
|
f5add40808 | ||
|
|
3e8e202e1e | ||
|
|
8e61c0ef46 | ||
|
|
a0173ee7e6 | ||
|
|
5a32fe208e | ||
|
|
1eea0d7cd8 | ||
|
|
8451c94abe | ||
|
|
e6ed08315e | ||
|
|
438e35688b | ||
|
|
e59400eb2b | ||
|
|
1c1eb9b782 | ||
|
|
d85e9b96b2 | ||
|
|
bd37d32750 | ||
|
|
7db1db2957 | ||
|
|
fd1d10a656 | ||
|
|
30dd2a2b16 | ||
|
|
9de9452dd3 | ||
|
|
bd37b57780 | ||
|
|
fc8d44b60d | ||
|
|
d224f5df25 |
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
30
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Versions**
|
||||||
|
What version of Minecraft, CC:Tweaked, Plethora (if applicable), Opus branch are you using
|
||||||
|
- MC : [e.g. 1.12.2]
|
||||||
|
- CC:T : [e.g. 1.88]
|
||||||
|
- Opus : [e.g. develop-1.8]
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
17
.github/ISSUE_TEMPLATES/bug.md
vendored
Normal file
17
.github/ISSUE_TEMPLATES/bug.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Did something go wrong? File an issue!
|
||||||
|
title: Good titles include first line of stack trace or short summary of problem
|
||||||
|
labels: bug
|
||||||
|
---
|
||||||
|
<!--- THIS IS A COMMENT. IT WILL NOT APPEAR IN THE FINAL ISSUE, DO NOT DELETE THESE. -->
|
||||||
|
# Details
|
||||||
|
<!--- Put a description of the bug here. (Ex. I crashed when running Opus.) -->
|
||||||
|
## Further context
|
||||||
|
<!--- Stack trace (surrounded in three backticks, ‘‘‘), supplementary media such as screenshots and video, etc -->
|
||||||
|
## Versions
|
||||||
|
Branch:
|
||||||
|
|
||||||
|
Opus Version: <!--- (Do NOT put Latest unless you are unsure) -->
|
||||||
|
|
||||||
|
CraftOS Version:
|
||||||
13
.github/ISSUE_TEMPLATES/feature.md
vendored
Normal file
13
.github/ISSUE_TEMPLATES/feature.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: Enhancement
|
||||||
|
about: Suggest a new feature or change to Opus.
|
||||||
|
labels: enhancement
|
||||||
|
---
|
||||||
|
<!--- THIS IS A COMMENT. IT WILL NOT APPEAR IN THE FINAL ISSUE. DO NOT REMOVE THEM. -->
|
||||||
|
# Summary
|
||||||
|
<!--- Summarize what you want. -->
|
||||||
|
## Additional Context
|
||||||
|
<!--- Comcept art, screenshots, and other relevant media. -->
|
||||||
|
## Related
|
||||||
|
<!--- Delete this category if not used. -->
|
||||||
|
<!--- Use this category for relevant links, if present. -->
|
||||||
36
.github/workflows/main.yml
vendored
Normal file
36
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# This is a basic workflow to help you get started with Actions
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
# Controls when the action will run. Triggers the workflow on push or pull request
|
||||||
|
# events but only for the master branch
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ develop-1.8 ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ develop-1.8 ]
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
# This workflow contains a single job called "build"
|
||||||
|
build:
|
||||||
|
# The type of runner that the job will run on
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Create version file
|
||||||
|
run: |
|
||||||
|
echo `date` > .opus_version
|
||||||
|
git log >> .opus_version
|
||||||
|
|
||||||
|
- name: Commit version file
|
||||||
|
uses: alexesprit/action-update-file@main
|
||||||
|
with:
|
||||||
|
branch: 'develop-1.8'
|
||||||
|
file-path: .opus_version
|
||||||
|
commit-msg: Update version date
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
/ignore
|
/ignore
|
||||||
|
.project
|
||||||
|
|||||||
6
.opus_version
Normal file
6
.opus_version
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Mon Jul 4 04:09:12 UTC 2022
|
||||||
|
commit 3150525ee2024fc605669093b89f75f0c741a81f
|
||||||
|
Author: Kan18 <24967425+Kan18@users.noreply.github.com>
|
||||||
|
Date: Mon Jul 4 00:08:59 2022 -0400
|
||||||
|
|
||||||
|
Fix #48 (shell resolving issue) (#58)
|
||||||
@@ -1,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
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Opus OS for computercraft
|
# Opus OS for computercraft
|
||||||
|
|
||||||
|
<img src="https://github.com/kepler155c/opus-wiki/blob/master/assets/images/opus.gif?raw=true" width="540" height="360">
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
* Multitasking OS - run programs in separate tabs
|
* Multitasking OS - run programs in separate tabs
|
||||||
* Telnet (wireless remote shell)
|
* Telnet (wireless remote shell)
|
||||||
@@ -14,6 +16,5 @@
|
|||||||
|
|
||||||
## Install
|
## Install
|
||||||
```
|
```
|
||||||
pastebin run sj4VMVJj
|
pastebin run UzGHLbNC
|
||||||
reboot
|
|
||||||
```
|
```
|
||||||
|
|||||||
45
startup
45
startup
@@ -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
196
startup.lua
Normal 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
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
local Ansi = setmetatable({ }, {
|
|
||||||
__call = function(_, ...)
|
|
||||||
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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local fs = _G.fs
|
|
||||||
local shell = _ENV.shell
|
|
||||||
|
|
||||||
local Config = { }
|
|
||||||
|
|
||||||
function Config.load(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
|
|
||||||
|
|
||||||
function Config.loadWithCheck(fname, data)
|
|
||||||
local filename = 'usr/config/' .. fname
|
|
||||||
|
|
||||||
if not fs.exists(filename) then
|
|
||||||
Config.load(fname, data)
|
|
||||||
print()
|
|
||||||
print('The configuration file has been created.')
|
|
||||||
print('The file name is: ' .. filename)
|
|
||||||
print()
|
|
||||||
_G.printError('Press enter to configure')
|
|
||||||
_G.read()
|
|
||||||
shell.run('edit ' .. filename)
|
|
||||||
end
|
|
||||||
|
|
||||||
Config.load(fname, data)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Config.update(fname, data)
|
|
||||||
local filename = 'usr/config/' .. fname
|
|
||||||
Util.writeTable(filename, data)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Config
|
|
||||||
@@ -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
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
local os = _G.os
|
|
||||||
|
|
||||||
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 coroutine.status(self.co) == 'running' then
|
|
||||||
--return
|
|
||||||
--end
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
local git = require('git')
|
|
||||||
|
|
||||||
local fs = _G.fs
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
local fs = _G.fs
|
|
||||||
|
|
||||||
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(_, 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
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
local Socket = require('socket')
|
|
||||||
local synchronized = require('sync')
|
|
||||||
|
|
||||||
local fs = _G.fs
|
|
||||||
|
|
||||||
local netfs = { }
|
|
||||||
|
|
||||||
local function remoteCommand(node, msg)
|
|
||||||
|
|
||||||
for _ = 1, 2 do
|
|
||||||
if not node.socket then
|
|
||||||
node.socket = Socket.connect(node.id, 139)
|
|
||||||
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(_, 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
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local fs = _G.fs
|
|
||||||
|
|
||||||
local ramfs = { }
|
|
||||||
|
|
||||||
function ramfs.mount(_, 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(_, 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)
|
|
||||||
if node.nodes and node.mountPoint == dir then
|
|
||||||
local files = { }
|
|
||||||
for k 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
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local fs = _G.fs
|
|
||||||
|
|
||||||
local urlfs = { }
|
|
||||||
|
|
||||||
function urlfs.mount(_, url)
|
|
||||||
if not url then
|
|
||||||
error('URL is required')
|
|
||||||
end
|
|
||||||
return {
|
|
||||||
url = url,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function urlfs.delete(_, 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
|
|
||||||
c = Util.httpGet(node.url)
|
|
||||||
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
|
|
||||||
@@ -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(repository)
|
|
||||||
|
|
||||||
local t = Util.split(repository, '(.-)/')
|
|
||||||
|
|
||||||
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 _,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
|
|
||||||
155
sys/apis/gps.lua
155
sys/apis/gps.lua
@@ -1,155 +0,0 @@
|
|||||||
local GPS = { }
|
|
||||||
|
|
||||||
local device = _G.device
|
|
||||||
local gps = _G.gps
|
|
||||||
local turtle = _G.turtle
|
|
||||||
|
|
||||||
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 _G.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
|
|
||||||
_G.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
|
|
||||||
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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
local DEFAULT_UPATH = 'https://raw.githubusercontent.com/kepler155c/opus/develop/sys/apis'
|
|
||||||
local PASTEBIN_URL = 'http://pastebin.com/raw'
|
|
||||||
local GIT_URL = 'https://raw.githubusercontent.com'
|
|
||||||
|
|
||||||
local fs = _G.fs
|
|
||||||
local http = _G.http
|
|
||||||
local os = _G.os
|
|
||||||
|
|
||||||
if not http._patched then
|
|
||||||
-- 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
|
|
||||||
fn()
|
|
||||||
local co = table.remove(syncLocks[key], 1)
|
|
||||||
if co then
|
|
||||||
os.queueEvent('sync_lock', co)
|
|
||||||
else
|
|
||||||
syncLocks[key] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- todo -- completely replace http.get with function that
|
|
||||||
-- checks for success on permanent redirects (minecraft 1.75 bug)
|
|
||||||
|
|
||||||
http._patched = http.get
|
|
||||||
function http.get(url, headers)
|
|
||||||
local s, m
|
|
||||||
sync(url, function()
|
|
||||||
s, m = http._patched(url, headers)
|
|
||||||
end)
|
|
||||||
return s, m
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function loadUrl(url)
|
|
||||||
local c
|
|
||||||
local h = http.get(url)
|
|
||||||
if h then
|
|
||||||
c = h.readAll()
|
|
||||||
h.close()
|
|
||||||
end
|
|
||||||
if c and #c > 0 then
|
|
||||||
return c
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function requireWrapper(env)
|
|
||||||
|
|
||||||
local function standardSearcher(modname)
|
|
||||||
if package.loaded[modname] then
|
|
||||||
return function()
|
|
||||||
return package.loaded[modname]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function shellSearcher(modname)
|
|
||||||
local fname = modname:gsub('%.', '/') .. '.lua'
|
|
||||||
|
|
||||||
if env.shell and type(env.shell.dir) == 'function' then
|
|
||||||
local path = env.shell.resolve(fname)
|
|
||||||
if fs.exists(path) and not fs.isDir(path) then
|
|
||||||
return loadfile(path, env)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function pathSearcher(modname)
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
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 env.require(modname)
|
|
||||||
|
|
||||||
for _,searcher in ipairs(package.loaders) do
|
|
||||||
local fn, msg = searcher(modname)
|
|
||||||
if fn then
|
|
||||||
local module, msg2 = fn(modname, env)
|
|
||||||
if not module then
|
|
||||||
error(msg2 or (modname .. ' module returned nil'), 2)
|
|
||||||
end
|
|
||||||
package.loaded[modname] = module
|
|
||||||
return module
|
|
||||||
end
|
|
||||||
if msg then
|
|
||||||
error(msg, 2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
error('Unable to find module ' .. modname)
|
|
||||||
end
|
|
||||||
|
|
||||||
return env.require -- backwards compatible
|
|
||||||
end
|
|
||||||
|
|
||||||
return function(env)
|
|
||||||
env = env or getfenv(2)
|
|
||||||
setfenv(requireWrapper, env)
|
|
||||||
return requireWrapper(env)
|
|
||||||
end
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local keys = _G.keys
|
|
||||||
local os = _G.os
|
|
||||||
|
|
||||||
local modifiers = Util.transpose {
|
|
||||||
keys.leftCtrl, keys.rightCtrl,
|
|
||||||
keys.leftShift, keys.rightShift,
|
|
||||||
keys.leftAlt, keys.rightAlt,
|
|
||||||
}
|
|
||||||
|
|
||||||
local input = {
|
|
||||||
pressed = { },
|
|
||||||
}
|
|
||||||
|
|
||||||
function input:modifierPressed()
|
|
||||||
return self.pressed[keys.leftCtrl] or
|
|
||||||
self.pressed[keys.rightCtrl] or
|
|
||||||
self.pressed[keys.leftAlt] or
|
|
||||||
self.pressed[keys.rightAlt]
|
|
||||||
end
|
|
||||||
|
|
||||||
function input:toCode(ch, code)
|
|
||||||
local result = { }
|
|
||||||
|
|
||||||
if self.pressed[keys.leftCtrl] or self.pressed[keys.rightCtrl] then
|
|
||||||
table.insert(result, 'control')
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.pressed[keys.leftAlt] or self.pressed[keys.rightAlt] then
|
|
||||||
table.insert(result, 'alt')
|
|
||||||
end
|
|
||||||
|
|
||||||
if self.pressed[keys.leftShift] or self.pressed[keys.rightShift] then
|
|
||||||
if code and modifiers[code] then
|
|
||||||
table.insert(result, 'shift')
|
|
||||||
elseif #ch == 1 then
|
|
||||||
table.insert(result, ch:upper())
|
|
||||||
else
|
|
||||||
table.insert(result, 'shift')
|
|
||||||
table.insert(result, ch)
|
|
||||||
end
|
|
||||||
elseif not code or not modifiers[code] then
|
|
||||||
table.insert(result, ch)
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.concat(result, '-')
|
|
||||||
end
|
|
||||||
|
|
||||||
function input:reset()
|
|
||||||
self.pressed = { }
|
|
||||||
self.fired = nil
|
|
||||||
|
|
||||||
self.timer = nil
|
|
||||||
self.mch = nil
|
|
||||||
self.mfired = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function input:translate(event, code, p1, p2)
|
|
||||||
if event == 'key' then
|
|
||||||
if p1 then -- key is held down
|
|
||||||
if not modifiers[code] then
|
|
||||||
self.fired = true
|
|
||||||
return input:toCode(keys.getName(code), code)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
self.pressed[code] = true
|
|
||||||
if self:modifierPressed() and not modifiers[code] or code == 57 then
|
|
||||||
self.fired = true
|
|
||||||
return input:toCode(keys.getName(code), code)
|
|
||||||
else
|
|
||||||
self.fired = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif event == 'char' then
|
|
||||||
if not self:modifierPressed() then
|
|
||||||
self.fired = true
|
|
||||||
return input:toCode(code)
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif event == 'key_up' then
|
|
||||||
if not self.fired then
|
|
||||||
if self.pressed[code] then
|
|
||||||
self.fired = true
|
|
||||||
local ch = input:toCode(keys.getName(code), code)
|
|
||||||
self.pressed[code] = nil
|
|
||||||
return ch
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self.pressed[code] = nil
|
|
||||||
|
|
||||||
elseif event == 'paste' then
|
|
||||||
self.pressed[keys.leftCtrl] = nil
|
|
||||||
self.pressed[keys.rightCtrl] = nil
|
|
||||||
self.fired = true
|
|
||||||
return input:toCode('paste', 255)
|
|
||||||
|
|
||||||
elseif event == 'mouse_click' then
|
|
||||||
local buttons = { 'mouse_click', 'mouse_rightclick' }
|
|
||||||
self.mch = buttons[code]
|
|
||||||
self.mfired = nil
|
|
||||||
|
|
||||||
elseif event == 'mouse_drag' then
|
|
||||||
self.mfired = true
|
|
||||||
self.fired = true
|
|
||||||
return input:toCode('mouse_drag', 255)
|
|
||||||
|
|
||||||
elseif event == 'mouse_up' then
|
|
||||||
if not self.mfired then
|
|
||||||
local clock = os.clock()
|
|
||||||
if self.timer and
|
|
||||||
p1 == self.x and p2 == self.y and
|
|
||||||
(clock - self.timer < .5) then
|
|
||||||
|
|
||||||
self.mch = 'mouse_doubleclick'
|
|
||||||
self.timer = nil
|
|
||||||
else
|
|
||||||
self.timer = os.clock()
|
|
||||||
self.x = p1
|
|
||||||
self.y = p2
|
|
||||||
end
|
|
||||||
self.mfired = input:toCode(self.mch, 255)
|
|
||||||
else
|
|
||||||
self.mch = 'mouse_up'
|
|
||||||
self.mfired = input:toCode(self.mch, 255)
|
|
||||||
end
|
|
||||||
self.fired = true
|
|
||||||
return self.mfired
|
|
||||||
|
|
||||||
elseif event == "mouse_scroll" then
|
|
||||||
local directions = {
|
|
||||||
[ -1 ] = 'scrollUp',
|
|
||||||
[ 1 ] = 'scrollDown'
|
|
||||||
}
|
|
||||||
self.fired = true
|
|
||||||
return input:toCode(directions[code], 255)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function input:test()
|
|
||||||
while true do
|
|
||||||
local ch = self:translate(os.pullEvent())
|
|
||||||
if ch then
|
|
||||||
print('GOT: ' .. tostring(ch))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return input
|
|
||||||
@@ -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
|
|
||||||
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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,41 +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 Node = {}
|
|
||||||
Node.__index = Node
|
|
||||||
|
|
||||||
function Node:new(x,y,z)
|
|
||||||
return setmetatable({x = x, y = y, z = z }, 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
|
|
||||||
|
|
||||||
function Node:getX() return self.x end
|
|
||||||
function Node:getY() return self.y end
|
|
||||||
function Node:getZ() return self.z 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.
|
|
||||||
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(_,...)
|
|
||||||
return Node:new(...)
|
|
||||||
end}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
@@ -1,67 +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
|
|
||||||
|
|
||||||
local t_remove = table.remove
|
|
||||||
|
|
||||||
local Path = {}
|
|
||||||
Path.__index = 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}
|
|
||||||
-- @usage
|
|
||||||
-- for node, count in p:iter() do
|
|
||||||
-- ...
|
|
||||||
-- end
|
|
||||||
function Path:nodes()
|
|
||||||
local i = 1
|
|
||||||
return function()
|
|
||||||
if self._nodes[i] then
|
|
||||||
i = i+1
|
|
||||||
return self._nodes[i-1],i-1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
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,zi,dx,dy,dz, olddx, olddy, olddz
|
|
||||||
xi,yi,zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z
|
|
||||||
dx, dy,dz = xi - self._nodes[i-1].x, yi-self._nodes[i-1].y, zi-self._nodes[i-1].z
|
|
||||||
while true do
|
|
||||||
olddx, olddy, olddz = dx, dy, dz
|
|
||||||
if self._nodes[i+1] then
|
|
||||||
i = i+1
|
|
||||||
xi, yi, zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z
|
|
||||||
dx, dy, dz = xi - self._nodes[i-1].x, yi - self._nodes[i-1].y, zi - self._nodes[i-1].z
|
|
||||||
if olddx == dx and olddy == dy and olddz == dz then
|
|
||||||
t_remove(self._nodes, i-1)
|
|
||||||
i = i - 1
|
|
||||||
end
|
|
||||||
else break end
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
return setmetatable(Path,
|
|
||||||
{__call = function(_,...)
|
|
||||||
return Path:new(...)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
end
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
-- Various utilities for Jumper top-level modules
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
|
|
||||||
-- Dependencies
|
|
||||||
local _PATH = (...):gsub('%.utils$','')
|
|
||||||
local Path = require (_PATH .. '.path')
|
|
||||||
|
|
||||||
-- Local references
|
|
||||||
local pairs = pairs
|
|
||||||
local t_insert = table.insert
|
|
||||||
|
|
||||||
-- Raw array items count
|
|
||||||
local function arraySize(t)
|
|
||||||
local count = 0
|
|
||||||
for _ in pairs(t) do
|
|
||||||
count = count+1
|
|
||||||
end
|
|
||||||
return count
|
|
||||||
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,
|
|
||||||
indexOf = indexOf,
|
|
||||||
outOfRange = outOfRange,
|
|
||||||
traceBackPath = traceBackPath
|
|
||||||
}
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,101 +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 Node = require (_PATH .. '.core.node')
|
|
||||||
|
|
||||||
-- Local references
|
|
||||||
local setmetatable = setmetatable
|
|
||||||
|
|
||||||
-- 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]]
|
|
||||||
}
|
|
||||||
|
|
||||||
local Grid = {}
|
|
||||||
Grid.__index = Grid
|
|
||||||
|
|
||||||
function Grid:new(dim)
|
|
||||||
local newGrid = { }
|
|
||||||
newGrid._min_x, newGrid._max_x = dim.x, dim.ex
|
|
||||||
newGrid._min_y, newGrid._max_y = dim.y, dim.ey
|
|
||||||
newGrid._min_z, newGrid._max_z = dim.z, dim.ez
|
|
||||||
newGrid._nodes = { }
|
|
||||||
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
|
|
||||||
return setmetatable(newGrid,Grid)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Grid:isWalkableAt(x, y, z)
|
|
||||||
local node = self:getNodeAt(x,y,z)
|
|
||||||
return node and node.walkable ~= 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function Grid:getWidth()
|
|
||||||
return self._width
|
|
||||||
end
|
|
||||||
|
|
||||||
function Grid:getHeight()
|
|
||||||
return self._height
|
|
||||||
end
|
|
||||||
|
|
||||||
function Grid:getNodes()
|
|
||||||
return self._nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
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`.
|
|
||||||
-- @treturn {node,...} an array of nodes neighbouring a given node
|
|
||||||
function Grid:getNeighbours(node)
|
|
||||||
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) then
|
|
||||||
neighbours[#neighbours+1] = n
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return neighbours
|
|
||||||
end
|
|
||||||
|
|
||||||
function Grid: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
|
|
||||||
|
|
||||||
-- inefficient
|
|
||||||
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
|
|
||||||
@@ -1,119 +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.
|
|
||||||
--]]
|
|
||||||
|
|
||||||
local _VERSION = ""
|
|
||||||
local _RELEASEDATE = ""
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
|
|
||||||
-- Dependencies
|
|
||||||
local _PATH = (...):gsub('%.pathfinder$','')
|
|
||||||
local Utils = require (_PATH .. '.core.utils')
|
|
||||||
|
|
||||||
-- Internalization
|
|
||||||
local pairs = pairs
|
|
||||||
local assert = assert
|
|
||||||
local setmetatable = setmetatable
|
|
||||||
|
|
||||||
--- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper.
|
|
||||||
-- <li>[A*](http://en.wikipedia.org/wiki/A*_search_algorithm)</li>
|
|
||||||
local Finders = {
|
|
||||||
['ASTAR'] = require (_PATH .. '.search.astar'),
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Will keep track of all nodes expanded during the search
|
|
||||||
-- to easily reset their properties for the next pathfinding call
|
|
||||||
local toClear = {}
|
|
||||||
|
|
||||||
-- Performs a traceback from the goal node to the start node
|
|
||||||
-- Only happens when the path was found
|
|
||||||
|
|
||||||
local Pathfinder = {}
|
|
||||||
Pathfinder.__index = Pathfinder
|
|
||||||
|
|
||||||
function Pathfinder:new(heuristic)
|
|
||||||
local newPathfinder = {}
|
|
||||||
setmetatable(newPathfinder, Pathfinder)
|
|
||||||
self._finder = Finders.ASTAR
|
|
||||||
self._heuristic = heuristic
|
|
||||||
return newPathfinder
|
|
||||||
end
|
|
||||||
|
|
||||||
function Pathfinder:setGrid(grid)
|
|
||||||
self._grid = grid
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Calculates a `path`. Returns the `path` from start to end location
|
|
||||||
-- Both locations must exist on the collision map. The starting location can be unwalkable.
|
|
||||||
-- @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)
|
|
||||||
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 = self._finder(self, startNode, endNode, 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
|
|
||||||
@@ -1,77 +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 huge = math.huge
|
|
||||||
|
|
||||||
-- Dependancies
|
|
||||||
local _PATH = (...):match('(.+)%.search.astar$')
|
|
||||||
local Heap = require (_PATH.. '.core.bheap')
|
|
||||||
|
|
||||||
-- Updates G-cost
|
|
||||||
local function computeCost(node, neighbour, 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(openList, node, neighbour, endNode, heuristic)
|
|
||||||
local oldG = neighbour._g
|
|
||||||
computeCost(node, neighbour, heuristic)
|
|
||||||
if neighbour._g < oldG 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
|
|
||||||
|
|
||||||
-- Calculates a path.
|
|
||||||
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
|
|
||||||
return function (finder, startNode, endNode, toClear)
|
|
||||||
local openList = Heap()
|
|
||||||
startNode._g = 0
|
|
||||||
startNode._h = finder._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)
|
|
||||||
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(openList, node, neighbour, endNode, finder._heuristic)
|
|
||||||
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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,80 +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 lines = Util.split(imageText)
|
|
||||||
while #lines[#lines] == 0 do
|
|
||||||
table.remove(lines, #lines)
|
|
||||||
end
|
|
||||||
|
|
||||||
for _,sLine in ipairs(lines) 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 = _G.colors.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
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
local Event = require('event')
|
|
||||||
local Socket = require('socket')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local Peripheral = Util.shallowCopy(_G.peripheral)
|
|
||||||
|
|
||||||
function Peripheral.getList()
|
|
||||||
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
|
|
||||||
_G.printError('wrap failed')
|
|
||||||
_G.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(Peripheral.getList(), 'side', side)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Peripheral.getByType(typeName)
|
|
||||||
return Util.find(Peripheral.getList(), 'type', typeName)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Peripheral.getByMethod(method)
|
|
||||||
for _,p in pairs(Peripheral.getList()) 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
|
|
||||||
|
|
||||||
if args.name then
|
|
||||||
return _G.device[args.name]
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
local function getProxy(pi)
|
|
||||||
local socket = Socket.connect(pi.host, 189)
|
|
||||||
|
|
||||||
if not socket then
|
|
||||||
error("Timed out attaching peripheral: " .. pi.uri)
|
|
||||||
end
|
|
||||||
|
|
||||||
socket:write(pi.path)
|
|
||||||
local proxy = socket:read(3)
|
|
||||||
|
|
||||||
if not proxy then
|
|
||||||
error("Timed out attaching peripheral: " .. pi.uri)
|
|
||||||
end
|
|
||||||
|
|
||||||
local methods = proxy.methods
|
|
||||||
proxy.methods = nil
|
|
||||||
|
|
||||||
for _,method in pairs(methods) do
|
|
||||||
proxy[method] = function(...)
|
|
||||||
socket:write({ fn = method, args = { ... } })
|
|
||||||
local resp = socket:read()
|
|
||||||
if not resp then
|
|
||||||
error("Timed out communicating with peripheral: " .. pi.uri)
|
|
||||||
end
|
|
||||||
return table.unpack(resp)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if proxy.blit then
|
|
||||||
local methods = { 'clear', 'clearLine', 'setCursorPos', 'write', 'blit',
|
|
||||||
'setTextColor', 'setTextColour', 'setBackgroundColor',
|
|
||||||
'setBackgroundColour', 'scroll', 'setCursorBlink', }
|
|
||||||
local queue = nil
|
|
||||||
|
|
||||||
for _,method in pairs(methods) do
|
|
||||||
proxy[method] = function(...)
|
|
||||||
if not queue then
|
|
||||||
queue = { }
|
|
||||||
Event.onTimeout(0, function()
|
|
||||||
if not socket:write({ fn = 'fastBlit', args = { queue } }) then
|
|
||||||
error("Timed out communicating with peripheral: " .. pi.uri)
|
|
||||||
end
|
|
||||||
queue = nil
|
|
||||||
socket:read()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(queue, {
|
|
||||||
fn = method,
|
|
||||||
args = { ... },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if proxy.type == 'monitor' then
|
|
||||||
Event.addRoutine(function()
|
|
||||||
while true do
|
|
||||||
local event = socket:read()
|
|
||||||
if not event then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
if not Util.empty(event) then
|
|
||||||
os.queueEvent(table.unpack(event))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
return proxy
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Parse a uri into it's components
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
monitor = { name = 'monitor' }
|
|
||||||
side/top = { side = 'top' }
|
|
||||||
method/list = { method = 'list' }
|
|
||||||
12://name/monitor = { host = 12, name = 'monitor' }
|
|
||||||
]]--
|
|
||||||
local function parse(uri)
|
|
||||||
local pi = Util.split(uri:gsub('^%d*://', ''), '(.-)/')
|
|
||||||
|
|
||||||
if #pi == 1 then
|
|
||||||
pi = {
|
|
||||||
'name',
|
|
||||||
pi[1],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
host = uri:match('^(%d*)%:'), -- 12
|
|
||||||
uri = uri, -- 12://name/monitor
|
|
||||||
path = uri:gsub('^%d*://', ''), -- name/monitor
|
|
||||||
[ pi[1] ] = pi[2], -- name = 'monitor'
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function Peripheral.lookup(uri)
|
|
||||||
local pi = parse(uri)
|
|
||||||
|
|
||||||
if pi.host then
|
|
||||||
return getProxy(pi)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Peripheral.get(pi)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Peripheral
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local Point = { }
|
|
||||||
|
|
||||||
Point.directions = {
|
|
||||||
[ 0 ] = { xd = 1, zd = 0, yd = 0, heading = 0, direction = 'east' },
|
|
||||||
[ 1 ] = { xd = 0, zd = 1, yd = 0, heading = 1, direction = 'south' },
|
|
||||||
[ 2 ] = { xd = -1, zd = 0, yd = 0, heading = 2, direction = 'west' },
|
|
||||||
[ 3 ] = { xd = 0, zd = -1, yd = 0, heading = 3, direction = 'north' },
|
|
||||||
[ 4 ] = { xd = 0, zd = 0, yd = 1, heading = 4, direction = 'up' },
|
|
||||||
[ 5 ] = { xd = 0, zd = 0, yd = -1, heading = 5, direction = 'down' },
|
|
||||||
}
|
|
||||||
|
|
||||||
Point.facings = {
|
|
||||||
[ 0 ] = Point.directions[0],
|
|
||||||
[ 1 ] = Point.directions[1],
|
|
||||||
[ 2 ] = Point.directions[2],
|
|
||||||
[ 3 ] = Point.directions[3],
|
|
||||||
east = Point.directions[0],
|
|
||||||
south = Point.directions[1],
|
|
||||||
west = Point.directions[2],
|
|
||||||
north = Point.directions[3],
|
|
||||||
}
|
|
||||||
|
|
||||||
Point.headings = {
|
|
||||||
[ 0 ] = Point.directions[0],
|
|
||||||
[ 1 ] = Point.directions[1],
|
|
||||||
[ 2 ] = Point.directions[2],
|
|
||||||
[ 3 ] = Point.directions[3],
|
|
||||||
[ 4 ] = Point.directions[4],
|
|
||||||
[ 5 ] = Point.directions[5],
|
|
||||||
east = Point.directions[0],
|
|
||||||
south = Point.directions[1],
|
|
||||||
west = Point.directions[2],
|
|
||||||
north = Point.directions[3],
|
|
||||||
up = Point.directions[4],
|
|
||||||
down = Point.directions[5],
|
|
||||||
}
|
|
||||||
|
|
||||||
Point.EAST = 0
|
|
||||||
Point.SOUTH = 1
|
|
||||||
Point.WEST = 2
|
|
||||||
Point.NORTH = 3
|
|
||||||
Point.UP = 4
|
|
||||||
Point.DOWN = 5
|
|
||||||
|
|
||||||
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.distance(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
|
|
||||||
local xd, zd = pta.x - ptb.x, pta.z - ptb.z
|
|
||||||
|
|
||||||
if (pta.heading % 2) == 0 and zd ~= 0 then
|
|
||||||
heading = zd < 0 and 1 or 3
|
|
||||||
elseif (pta.heading % 2) == 1 and xd ~= 0 then
|
|
||||||
heading = xd < 0 and 0 or 2
|
|
||||||
elseif pta.heading == 0 and xd > 0 then
|
|
||||||
heading = 2
|
|
||||||
elseif pta.heading == 2 and xd < 0 then
|
|
||||||
heading = 0
|
|
||||||
elseif pta.heading == 1 and zd > 0 then
|
|
||||||
heading = 3
|
|
||||||
elseif pta.heading == 3 and zd < 0 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)
|
|
||||||
if #pts == 1 then
|
|
||||||
return pts[1]
|
|
||||||
end
|
|
||||||
|
|
||||||
local lm, lpt = math.huge
|
|
||||||
for _,pt in pairs(pts) do
|
|
||||||
local distance = Point.turtleDistance(reference, pt)
|
|
||||||
if distance < lm then
|
|
||||||
local m = Point.calculateMoves(reference, pt, distance)
|
|
||||||
if m < lm then
|
|
||||||
lpt = pt
|
|
||||||
lm = m
|
|
||||||
end
|
|
||||||
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 i = 0, 5 do
|
|
||||||
local hi = Point.headings[i]
|
|
||||||
table.insert(pts, { x = pt.x + hi.xd, y = pt.y + hi.yd, z = pt.z + hi.zd })
|
|
||||||
end
|
|
||||||
|
|
||||||
return pts
|
|
||||||
end
|
|
||||||
|
|
||||||
-- get the point nearest A that is in the direction of B
|
|
||||||
function Point.nearestTo(pta, ptb)
|
|
||||||
local heading
|
|
||||||
|
|
||||||
if pta.x < ptb.x then
|
|
||||||
heading = 0
|
|
||||||
elseif pta.z < ptb.z then
|
|
||||||
heading = 1
|
|
||||||
elseif pta.x > ptb.x then
|
|
||||||
heading = 2
|
|
||||||
elseif pta.z > ptb.z then
|
|
||||||
heading = 3
|
|
||||||
elseif pta.y < ptb.y then
|
|
||||||
heading = 4
|
|
||||||
elseif pta.y > ptb.y then
|
|
||||||
heading = 5
|
|
||||||
end
|
|
||||||
|
|
||||||
if heading then
|
|
||||||
return {
|
|
||||||
x = pta.x + Point.headings[heading].xd,
|
|
||||||
y = pta.y + Point.headings[heading].yd,
|
|
||||||
z = pta.z + Point.headings[heading].zd,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
return pta -- error ?
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.rotate(pt, facing)
|
|
||||||
local x, z = pt.x, pt.z
|
|
||||||
if facing == 1 then
|
|
||||||
pt.x = z
|
|
||||||
pt.z = -x
|
|
||||||
elseif facing == 2 then
|
|
||||||
pt.x = -x
|
|
||||||
pt.z = -z
|
|
||||||
elseif facing == 3 then
|
|
||||||
pt.x = -z
|
|
||||||
pt.z = x
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.makeBox(pt1, pt2)
|
|
||||||
return {
|
|
||||||
x = pt1.x,
|
|
||||||
y = pt1.y,
|
|
||||||
z = pt1.z,
|
|
||||||
ex = pt2.x,
|
|
||||||
ey = pt2.y,
|
|
||||||
ez = pt2.z,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- expand box to include point
|
|
||||||
function Point.expandBox(box, pt)
|
|
||||||
if pt.x < box.x then
|
|
||||||
box.x = pt.x
|
|
||||||
elseif pt.x > box.ex then
|
|
||||||
box.ex = pt.x
|
|
||||||
end
|
|
||||||
if pt.y < box.y then
|
|
||||||
box.y = pt.y
|
|
||||||
elseif pt.y > box.ey then
|
|
||||||
box.ey = pt.y
|
|
||||||
end
|
|
||||||
if pt.z < box.z then
|
|
||||||
box.z = pt.z
|
|
||||||
elseif pt.z > box.ez then
|
|
||||||
box.ez = pt.z
|
|
||||||
end
|
|
||||||
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
|
|
||||||
|
|
||||||
function Point.closestPointInBox(pt, box)
|
|
||||||
local cpt = {
|
|
||||||
x = math.abs(pt.x - box.x) < math.abs(pt.x - box.ex) and box.x or box.ex,
|
|
||||||
y = math.abs(pt.y - box.y) < math.abs(pt.y - box.ey) and box.y or box.ey,
|
|
||||||
z = math.abs(pt.z - box.z) < math.abs(pt.z - box.ez) and box.z or box.ez,
|
|
||||||
}
|
|
||||||
cpt.x = pt.x > box.x and pt.x < box.ex and pt.x or cpt.x
|
|
||||||
cpt.y = pt.y > box.y and pt.y < box.ey and pt.y or cpt.y
|
|
||||||
cpt.z = pt.z > box.z and pt.z < box.ez and pt.z or cpt.z
|
|
||||||
|
|
||||||
return cpt
|
|
||||||
end
|
|
||||||
|
|
||||||
return Point
|
|
||||||
@@ -1,60 +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.hasPassword()
|
|
||||||
return not not 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 _ = 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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
local Crypto = require('crypto')
|
|
||||||
local Logger = require('logger')
|
|
||||||
local Security = require('security')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local device = _G.device
|
|
||||||
local os = _G.os
|
|
||||||
|
|
||||||
local socketClass = { }
|
|
||||||
|
|
||||||
function socketClass:read(timeout)
|
|
||||||
|
|
||||||
local data, distance = _G.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 = _G.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)
|
|
||||||
self:ping()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function socketClass:write(data)
|
|
||||||
if self.connected then
|
|
||||||
_G.transport.write(self, {
|
|
||||||
type = 'DATA',
|
|
||||||
seq = self.wseq,
|
|
||||||
data = data,
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function socketClass:ping()
|
|
||||||
if self.connected then
|
|
||||||
_G.transport.ping(self)
|
|
||||||
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)
|
|
||||||
_G.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 = tonumber(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 then
|
|
||||||
|
|
||||||
os.cancelTimer(timerId)
|
|
||||||
|
|
||||||
if msg.type == 'CONN' then
|
|
||||||
|
|
||||||
socket.dport = dport
|
|
||||||
socket.connected = true
|
|
||||||
Logger.log('socket', 'connection established to %d %d->%d',
|
|
||||||
host, socket.sport, socket.dport)
|
|
||||||
|
|
||||||
_G.transport.open(socket)
|
|
||||||
|
|
||||||
return socket
|
|
||||||
elseif msg.type == 'REJE' then
|
|
||||||
return false, 'Password not set on target or not trusted'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
until e == 'timer' and id == timerId
|
|
||||||
|
|
||||||
socket:close()
|
|
||||||
|
|
||||||
return false, 'Connection timed out'
|
|
||||||
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
|
|
||||||
|
|
||||||
if not Security.hasPassword() then
|
|
||||||
-- no password has been set on this computer
|
|
||||||
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 _, _, sport, dport, msg = os.pullEvent('modem_message')
|
|
||||||
|
|
||||||
if sport == port and
|
|
||||||
msg and
|
|
||||||
msg.dhost == os.getComputerID() and
|
|
||||||
msg.type == 'OPEN' then
|
|
||||||
|
|
||||||
local socket = newSocket(msg.shost == os.getComputerID())
|
|
||||||
socket.dport = dport
|
|
||||||
socket.dhost = msg.shost
|
|
||||||
socket.wseq = msg.wseq
|
|
||||||
socket.rseq = msg.rseq
|
|
||||||
|
|
||||||
if trusted(msg, port) then
|
|
||||||
socket.connected = true
|
|
||||||
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)
|
|
||||||
|
|
||||||
_G.transport.open(socket)
|
|
||||||
return socket
|
|
||||||
end
|
|
||||||
|
|
||||||
socket.transmit(socket.dport, socket.sport, {
|
|
||||||
type = 'REJE',
|
|
||||||
dhost = socket.dhost,
|
|
||||||
shost = socket.shost,
|
|
||||||
})
|
|
||||||
socket:close()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Socket
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
local syncLocks = { }
|
|
||||||
|
|
||||||
local os = _G.os
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local colors = _G.colors
|
|
||||||
local term = _G.term
|
|
||||||
local _gsub = string.gsub
|
|
||||||
|
|
||||||
local Terminal = { }
|
|
||||||
|
|
||||||
function Terminal.scrollable(ct, size)
|
|
||||||
|
|
||||||
local w, h = ct.getSize()
|
|
||||||
local win = _G.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
|
|
||||||
s = _gsub(s, "%w", bcolors)
|
|
||||||
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 = _G.read(prompt) end)
|
|
||||||
term.current().write = fn
|
|
||||||
|
|
||||||
if s == '' then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
return s
|
|
||||||
end
|
|
||||||
|
|
||||||
return Terminal
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
_G.requireInjector()
|
|
||||||
|
|
||||||
local Grid = require('jumper.grid')
|
|
||||||
local Pathfinder = require('jumper.pathfinder')
|
|
||||||
local Point = require('point')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local turtle = _G.turtle
|
|
||||||
|
|
||||||
local function addBlock(grid, b, dim)
|
|
||||||
if Point.inBox(b, dim) then
|
|
||||||
local node = grid:getNodeAt(b.x, b.y, b.z)
|
|
||||||
if node then
|
|
||||||
node.walkable = 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
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, dests)
|
|
||||||
local box = Point.makeBox(turtle.point, turtle.point)
|
|
||||||
|
|
||||||
Point.expandBox(box, dest)
|
|
||||||
|
|
||||||
for _,d in pairs(dests) do
|
|
||||||
Point.expandBox(box, d)
|
|
||||||
end
|
|
||||||
|
|
||||||
for _,b in pairs(blocks) do
|
|
||||||
Point.expandBox(box, b)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- expand one block out in all directions
|
|
||||||
if boundingBox then
|
|
||||||
box.x = math.max(box.x - 1, boundingBox.x)
|
|
||||||
box.z = math.max(box.z - 1, boundingBox.z)
|
|
||||||
box.y = math.max(box.y - 1, boundingBox.y)
|
|
||||||
box.ex = math.min(box.ex + 1, boundingBox.ex)
|
|
||||||
box.ez = math.min(box.ez + 1, boundingBox.ez)
|
|
||||||
box.ey = math.min(box.ey + 1, boundingBox.ey)
|
|
||||||
else
|
|
||||||
box.x = box.x - 1
|
|
||||||
box.z = box.z - 1
|
|
||||||
box.y = box.y - 1
|
|
||||||
box.ex = box.ex + 1
|
|
||||||
box.ez = box.ez + 1
|
|
||||||
box.ey = box.ey + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
return box
|
|
||||||
end
|
|
||||||
|
|
||||||
local function nodeToPoint(node)
|
|
||||||
return { x = node.x, y = node.y, z = node.z, heading = node.heading }
|
|
||||||
end
|
|
||||||
|
|
||||||
local function heuristic(n, node)
|
|
||||||
return Point.calculateMoves(node, n)
|
|
||||||
-- { x = node.x, y = node.y, z = node.z, heading = node.heading },
|
|
||||||
-- { x = n.x, y = n.y, z = n.z, heading = n.heading })
|
|
||||||
end
|
|
||||||
|
|
||||||
local function dimsAreEqual(d1, d2)
|
|
||||||
return d1.ex == d2.ex and
|
|
||||||
d1.ey == d2.ey and
|
|
||||||
d1.ez == d2.ez and
|
|
||||||
d1.x == d2.x and
|
|
||||||
d1.y == d2.y and
|
|
||||||
d1.z == d2.z
|
|
||||||
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, grid)
|
|
||||||
while #pts > 0 do
|
|
||||||
local pt = Point.closest(turtle.point, pts)
|
|
||||||
if box and not Point.inBox(pt, box) then
|
|
||||||
Util.removeByValue(pts, pt)
|
|
||||||
else
|
|
||||||
if grid:isWalkableAt(pt.x, pt.y, pt.z) then
|
|
||||||
return pt
|
|
||||||
end
|
|
||||||
Util.removeByValue(pts, 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
|
|
||||||
local grid
|
|
||||||
|
|
||||||
if box then
|
|
||||||
box = Point.normalizeBox(box)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Creates a pathfinder object
|
|
||||||
local finder = Pathfinder(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, dests)
|
|
||||||
|
|
||||||
-- reuse map if possible
|
|
||||||
if not lastDim or not dimsAreEqual(dim, lastDim) then
|
|
||||||
-- Creates a grid object
|
|
||||||
grid = Grid(dim)
|
|
||||||
finder:setGrid(grid)
|
|
||||||
|
|
||||||
lastDim = dim
|
|
||||||
end
|
|
||||||
for _,b in pairs(blocks) do
|
|
||||||
addBlock(grid, b, dim)
|
|
||||||
end
|
|
||||||
|
|
||||||
dest = selectDestination(dests, box, grid)
|
|
||||||
if not dest then
|
|
||||||
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 = turtle.point
|
|
||||||
|
|
||||||
-- Calculates the path, and its length
|
|
||||||
local path = finder:getPath(
|
|
||||||
startPt.x, startPt.y, startPt.z, turtle.point.heading,
|
|
||||||
dest.x, dest.y, dest.z, dest.heading)
|
|
||||||
|
|
||||||
if not path then
|
|
||||||
Util.removeByValue(dests, dest)
|
|
||||||
else
|
|
||||||
path:filter()
|
|
||||||
|
|
||||||
for node in path:nodes() do
|
|
||||||
local pt = nodeToPoint(node)
|
|
||||||
|
|
||||||
if turtle.isAborted() then
|
|
||||||
return false, 'aborted'
|
|
||||||
end
|
|
||||||
|
|
||||||
--if this is the next to last node
|
|
||||||
--and we are traveling up or down, then the
|
|
||||||
--heading for this node should be the heading of the last node
|
|
||||||
--or, maybe..
|
|
||||||
--if last node is up or down (or either?)
|
|
||||||
|
|
||||||
-- use single turn method so the turtle doesn't turn around
|
|
||||||
-- when encountering obstacles
|
|
||||||
if not turtle.gotoSingleTurn(pt.x, pt.y, pt.z, pt.heading) then
|
|
||||||
local bpt = Point.nearestTo(turtle.point, pt)
|
|
||||||
|
|
||||||
table.insert(blocks, bpt)
|
|
||||||
-- really need to check if the block we ran into was a turtle.
|
|
||||||
-- if so, this block should be temporary (1-2 secs)
|
|
||||||
|
|
||||||
--local side = turtle.getSide(turtle.point, pt)
|
|
||||||
--if turtle.isTurtleAtSide(side) then
|
|
||||||
-- pt.timestamp = os.clock() + ?
|
|
||||||
--end
|
|
||||||
-- if dim has not changed, then need to update grid with
|
|
||||||
-- walkable = nil (after time has elapsed)
|
|
||||||
|
|
||||||
--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,
|
|
||||||
|
|
||||||
addBlock = function(block)
|
|
||||||
if turtle.getState().blocks then
|
|
||||||
table.insert(turtle.getState().blocks, block)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
reset = function()
|
|
||||||
turtle.getState().box = nil
|
|
||||||
turtle.getState().blocks = nil
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
3285
sys/apis/ui.lua
3285
sys/apis/ui.lua
File diff suppressed because it is too large
Load Diff
@@ -1,368 +0,0 @@
|
|||||||
local class = require('class')
|
|
||||||
local Region = require('ui.region')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local _rep = string.rep
|
|
||||||
local _sub = string.sub
|
|
||||||
local _gsub = string.gsub
|
|
||||||
local colors = _G.colors
|
|
||||||
|
|
||||||
local Canvas = class()
|
|
||||||
|
|
||||||
Canvas.colorPalette = { }
|
|
||||||
Canvas.darkPalette = { }
|
|
||||||
Canvas.grayscalePalette = { }
|
|
||||||
|
|
||||||
for n = 1, 16 do
|
|
||||||
Canvas.colorPalette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n)
|
|
||||||
Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n)
|
|
||||||
Canvas.darkPalette[2 ^ (n - 1)] = _sub("8777777f77fff77f", n, n)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:init(args)
|
|
||||||
self.x = 1
|
|
||||||
self.y = 1
|
|
||||||
self.layers = { }
|
|
||||||
|
|
||||||
Util.merge(self, args)
|
|
||||||
|
|
||||||
self.ex = self.x + self.width - 1
|
|
||||||
self.ey = self.y + self.height - 1
|
|
||||||
|
|
||||||
if not self.palette then
|
|
||||||
if self.isColor then
|
|
||||||
self.palette = Canvas.colorPalette
|
|
||||||
else
|
|
||||||
self.palette = Canvas.grayscalePalette
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.lines = { }
|
|
||||||
for i = 1, self.height do
|
|
||||||
self.lines[i] = { }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:move(x, y)
|
|
||||||
self.x, self.y = x, y
|
|
||||||
self.ex = self.x + self.width - 1
|
|
||||||
self.ey = self.y + self.height - 1
|
|
||||||
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] = { dirty = true }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.ex = self.x + w - 1
|
|
||||||
self.ey = self.y + h - 1
|
|
||||||
self.width = w
|
|
||||||
self.height = h
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:copy()
|
|
||||||
local b = Canvas({
|
|
||||||
x = self.x,
|
|
||||||
y = self.y,
|
|
||||||
width = self.width,
|
|
||||||
height = self.height,
|
|
||||||
isColor = self.isColor,
|
|
||||||
})
|
|
||||||
for i = 1, self.height 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)
|
|
||||||
local canvas = Canvas({
|
|
||||||
x = layer.x,
|
|
||||||
y = layer.y,
|
|
||||||
width = layer.width,
|
|
||||||
height = layer.height,
|
|
||||||
isColor = self.isColor,
|
|
||||||
})
|
|
||||||
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 = _rep(self.palette[bg], #text)
|
|
||||||
end
|
|
||||||
if fg then
|
|
||||||
fg = _rep(self.palette[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 = _sub(text, 2 - x)
|
|
||||||
if bg then
|
|
||||||
bg = _sub(bg, 2 - x)
|
|
||||||
end
|
|
||||||
if bg then
|
|
||||||
fg = _sub(fg, 2 - x)
|
|
||||||
end
|
|
||||||
width = width + x - 1
|
|
||||||
x = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
if x + width - 1 > self.width then
|
|
||||||
text = _sub(text, 1, self.width - x + 1)
|
|
||||||
if bg then
|
|
||||||
bg = _sub(bg, 1, self.width - x + 1)
|
|
||||||
end
|
|
||||||
if bg then
|
|
||||||
fg = _sub(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 .. _sub(sstr, pos+width)
|
|
||||||
elseif pos + width > self.width then
|
|
||||||
return _sub(sstr, 1, pos-1) .. rstr
|
|
||||||
end
|
|
||||||
return _sub(sstr, 1, pos-1) .. rstr .. _sub(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 text = _rep(' ', self.width)
|
|
||||||
fg = _rep(self.palette[fg or colors.white], self.width)
|
|
||||||
bg = _rep(self.palette[bg or colors.black], self.width)
|
|
||||||
for i = 1, self.height 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 _, 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 = _sub(t, src.x, src.ex)
|
|
||||||
fg = _sub(fg, src.x, src.ex)
|
|
||||||
bg = _sub(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:applyPalette(palette)
|
|
||||||
|
|
||||||
local lookup = { }
|
|
||||||
for n = 1, 16 do
|
|
||||||
lookup[self.palette[2 ^ (n - 1)]] = palette[2 ^ (n - 1)]
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, l in pairs(self.lines) do
|
|
||||||
l.fg = _gsub(l.fg, '%w', lookup)
|
|
||||||
l.bg = _gsub(l.bg, '%w', lookup)
|
|
||||||
l.dirty = true
|
|
||||||
end
|
|
||||||
|
|
||||||
self.palette = palette
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas.convertWindow(win, parent, wx, wy)
|
|
||||||
local w, h = win.getSize()
|
|
||||||
|
|
||||||
win.canvas = Canvas({
|
|
||||||
x = wx,
|
|
||||||
y = wy,
|
|
||||||
width = w,
|
|
||||||
height = h,
|
|
||||||
isColor = win.isColor(),
|
|
||||||
})
|
|
||||||
|
|
||||||
function win.clear()
|
|
||||||
win.canvas:clear(win.getBackgroundColor(), win.getTextColor())
|
|
||||||
end
|
|
||||||
|
|
||||||
function win.clearLine()
|
|
||||||
local _, y = win.getCursorPos()
|
|
||||||
win.canvas:write(1,
|
|
||||||
y,
|
|
||||||
_rep(' ', 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('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
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local colors = _G.colors
|
|
||||||
local fs = _G.fs
|
|
||||||
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
local Tween = require('ui.tween')
|
|
||||||
|
|
||||||
local Transition = { }
|
|
||||||
|
|
||||||
function Transition.slideLeft(args)
|
|
||||||
local ticks = args.ticks or 6
|
|
||||||
local easing = args.easing or 'outQuint'
|
|
||||||
local pos = { x = args.ex }
|
|
||||||
local tween = Tween.new(ticks, pos, { x = args.x }, easing)
|
|
||||||
local lastScreen = args.canvas:copy()
|
|
||||||
|
|
||||||
return function(device)
|
|
||||||
local finished = tween:update(1)
|
|
||||||
local x = math.floor(pos.x)
|
|
||||||
lastScreen:dirty()
|
|
||||||
lastScreen:blit(device, {
|
|
||||||
x = args.ex - x + args.x,
|
|
||||||
y = args.y,
|
|
||||||
ex = args.ex,
|
|
||||||
ey = args.ey },
|
|
||||||
{ x = args.x, y = args.y })
|
|
||||||
args.canvas:blit(device, {
|
|
||||||
x = args.x,
|
|
||||||
y = args.y,
|
|
||||||
ex = args.ex - x + args.x,
|
|
||||||
ey = args.ey },
|
|
||||||
{ x = x, y = args.y })
|
|
||||||
return not finished
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Transition.slideRight(args)
|
|
||||||
local ticks = args.ticks or 6
|
|
||||||
local easing = args.easing or'outQuint'
|
|
||||||
local pos = { x = args.x }
|
|
||||||
local tween = Tween.new(ticks, pos, { x = args.ex }, easing)
|
|
||||||
local lastScreen = args.canvas:copy()
|
|
||||||
|
|
||||||
return function(device)
|
|
||||||
local finished = tween:update(1)
|
|
||||||
local x = math.floor(pos.x)
|
|
||||||
lastScreen:dirty()
|
|
||||||
lastScreen:blit(device, {
|
|
||||||
x = args.x,
|
|
||||||
y = args.y,
|
|
||||||
ex = args.ex - x + args.x,
|
|
||||||
ey = args.ey },
|
|
||||||
{ x = x, y = args.y })
|
|
||||||
args.canvas:blit(device, {
|
|
||||||
x = args.ex - x + args.x,
|
|
||||||
y = args.y,
|
|
||||||
ex = args.ex,
|
|
||||||
ey = args.ey },
|
|
||||||
{ x = args.x, y = args.y })
|
|
||||||
return not finished
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Transition.expandUp(args)
|
|
||||||
local ticks = args.ticks or 3
|
|
||||||
local easing = args.easing or 'linear'
|
|
||||||
local pos = { y = args.ey + 1 }
|
|
||||||
local tween = Tween.new(ticks, pos, { y = args.y }, easing)
|
|
||||||
|
|
||||||
return function(device)
|
|
||||||
local finished = tween:update(1)
|
|
||||||
args.canvas:blit(device, nil, { x = args.x, y = math.floor(pos.y) })
|
|
||||||
return not finished
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Transition.grow(args)
|
|
||||||
local ticks = args.ticks or 3
|
|
||||||
local easing = args.easing or 'linear'
|
|
||||||
local tween = Tween.new(ticks,
|
|
||||||
{ x = args.width / 2 - 1, y = args.height / 2 - 1, w = 1, h = 1 },
|
|
||||||
{ x = 1, y = 1, w = args.width, h = args.height }, easing)
|
|
||||||
|
|
||||||
return function(device)
|
|
||||||
local finished = tween:update(1)
|
|
||||||
local subj = tween.subject
|
|
||||||
local rect = { x = math.floor(subj.x), y = math.floor(subj.y) }
|
|
||||||
rect.ex = math.floor(rect.x + subj.w - 1)
|
|
||||||
rect.ey = math.floor(rect.y + subj.h - 1)
|
|
||||||
args.canvas:blit(device, rect, { x = args.x + rect.x - 1, y = args.y + rect.y - 1})
|
|
||||||
return not finished
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Transition
|
|
||||||
@@ -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
|
|
||||||
@@ -1,638 +0,0 @@
|
|||||||
local Util = { }
|
|
||||||
|
|
||||||
local fs = _G.fs
|
|
||||||
local http = _G.http
|
|
||||||
local os = _G.os
|
|
||||||
local term = _G.term
|
|
||||||
local textutils = _G.textutils
|
|
||||||
|
|
||||||
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 _ = 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 _G._CC_VERSION then
|
|
||||||
version = tonumber(_G._CC_VERSION:match('[%d]+%.?[%d][%d]'))
|
|
||||||
end
|
|
||||||
if not version and _G._HOST then
|
|
||||||
version = tonumber(_G._HOST:match('[%d]+%.?[%d][%d]'))
|
|
||||||
end
|
|
||||||
|
|
||||||
return version or 1.7
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.getMinecraftVersion()
|
|
||||||
local mcVersion = _G._MC_VERSION or 'unknown'
|
|
||||||
if _G._HOST then
|
|
||||||
local version = _G._HOST:match('%S+ %S+ %((%S.+)%)')
|
|
||||||
if version then
|
|
||||||
mcVersion = version:match('Minecraft (%S+)') or version
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return mcVersion
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.checkMinecraftVersion(minVersion)
|
|
||||||
local version = Util.getMinecraftVersion()
|
|
||||||
local function convert(v)
|
|
||||||
local m1, m2, m3 = v:match('(%d)%.(%d)%.?(%d?)')
|
|
||||||
return tonumber(m1) * 10000 + tonumber(m2) * 100 + (tonumber(m3) or 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
return convert(version) >= convert(tostring(minVersion))
|
|
||||||
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.contains(t, value)
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
if v == value then
|
|
||||||
return k
|
|
||||||
end
|
|
||||||
end
|
|
||||||
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 _,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(v) then
|
|
||||||
ot[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ot
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.size(list)
|
|
||||||
if type(list) == 'table' then
|
|
||||||
local length = 0
|
|
||||||
for _ in pairs(list) do
|
|
||||||
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
|
|
||||||
|
|
||||||
function Util.rpairs(t)
|
|
||||||
local tkeys = Util.keys(t)
|
|
||||||
local i = #tkeys
|
|
||||||
return function()
|
|
||||||
local key = tkeys[i]
|
|
||||||
local k,v = key, t[key]
|
|
||||||
i = i - 1
|
|
||||||
if v then
|
|
||||||
return k, v
|
|
||||||
end
|
|
||||||
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
|
|
||||||
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.httpGet(url, headers)
|
|
||||||
local h, msg = http.get(url, headers)
|
|
||||||
if h then
|
|
||||||
local contents = h.readAll()
|
|
||||||
h.close()
|
|
||||||
return contents
|
|
||||||
end
|
|
||||||
return h, msg
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.download(url, filename)
|
|
||||||
local contents = Util.httpGet(url)
|
|
||||||
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 c, msg = Util.httpGet(url)
|
|
||||||
if not c then
|
|
||||||
return c, msg
|
|
||||||
end
|
|
||||||
return load(c, url, nil, env)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.runUrl(env, url, ...) -- os.run equivalent
|
|
||||||
setmetatable(env, { __index = _G })
|
|
||||||
local fn, m = Util.loadUrl(url, env)
|
|
||||||
if fn then
|
|
||||||
return pcall(fn, ...)
|
|
||||||
end
|
|
||||||
return fn, m
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.run(env, path, ...)
|
|
||||||
setmetatable(env, { __index = _G })
|
|
||||||
local fn, m = loadfile(path, env)
|
|
||||||
if fn then
|
|
||||||
return pcall(fn, ...)
|
|
||||||
end
|
|
||||||
return fn, m
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.runFunction(env, fn, ...)
|
|
||||||
setfenv(fn, env)
|
|
||||||
setmetatable(env, { __index = _G })
|
|
||||||
return pcall(fn, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ String functions ]] --
|
|
||||||
function Util.toBytes(n)
|
|
||||||
if not tonumber(n) then error('Util.toBytes: n must be a number', 2) end
|
|
||||||
if n >= 1000000 or n <= -1000000 then
|
|
||||||
return string.format('%sM', math.floor(n/1000000 * 10) / 10)
|
|
||||||
elseif n >= 1000 or n <= -1000 then
|
|
||||||
return string.format('%sK', math.floor(n/1000 * 10) / 10)
|
|
||||||
end
|
|
||||||
return tostring(n)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.insertString(str, istr, pos)
|
|
||||||
return str:sub(1, pos - 1) .. istr .. str: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 _, 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)
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
_G.requireInjector()
|
local Config = require('opus.config')
|
||||||
|
local Event = require('opus.event')
|
||||||
local Config = require('config')
|
local pastebin = require('opus.http.pastebin')
|
||||||
local Event = require('event')
|
local UI = require('opus.ui')
|
||||||
local UI = require('ui')
|
local Util = require('opus.util')
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local colors = _G.colors
|
local colors = _G.colors
|
||||||
local fs = _G.fs
|
local fs = _G.fs
|
||||||
@@ -11,16 +10,19 @@ local multishell = _ENV.multishell
|
|||||||
local os = _G.os
|
local os = _G.os
|
||||||
local shell = _ENV.shell
|
local shell = _ENV.shell
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'Files')
|
local FILE = 1
|
||||||
|
|
||||||
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 = { }
|
||||||
@@ -40,20 +42,23 @@ local Browser = UI.Page {
|
|||||||
buttons = {
|
buttons = {
|
||||||
{ text = '^-', event = 'updir' },
|
{ text = '^-', event = 'updir' },
|
||||||
{ text = 'File', dropdown = {
|
{ text = 'File', dropdown = {
|
||||||
{ text = 'Run', event = 'run' },
|
{ text = 'Run', event = 'run', flags = FILE },
|
||||||
{ text = 'Edit e', event = 'edit' },
|
{ text = 'Edit e', event = 'edit', flags = FILE },
|
||||||
|
{ text = 'Cloud edit c', event = 'cedit', flags = FILE },
|
||||||
|
{ text = 'Pastebin put p', event = 'pastebin', flags = FILE },
|
||||||
{ text = 'Shell s', event = 'shell' },
|
{ text = 'Shell s', event = 'shell' },
|
||||||
UI.MenuBar.spacer,
|
{ spacer = true },
|
||||||
{ text = 'Quit q', event = 'quit' },
|
{ text = 'Quit ^q', event = 'quit' },
|
||||||
} },
|
} },
|
||||||
{ text = 'Edit', dropdown = {
|
{ text = 'Edit', dropdown = {
|
||||||
{ text = 'Cut ^x', event = 'cut' },
|
{ text = 'Cut ^x', event = 'cut' },
|
||||||
{ text = 'Copy ^c', event = 'copy' },
|
{ text = 'Copy ^c', event = 'copy' },
|
||||||
|
{ text = 'Copy path ', event = 'copy_path' },
|
||||||
{ text = 'Paste ^v', event = 'paste' },
|
{ text = 'Paste ^v', event = 'paste' },
|
||||||
UI.MenuBar.spacer,
|
{ spacer = true },
|
||||||
{ text = 'Mark m', event = 'mark' },
|
{ text = 'Mark m', event = 'mark' },
|
||||||
{ text = 'Unmark all u', event = 'unmark' },
|
{ text = 'Unmark all u', event = 'unmark' },
|
||||||
UI.MenuBar.spacer,
|
{ spacer = true },
|
||||||
{ text = 'Delete del', event = 'delete' },
|
{ text = 'Delete del', event = 'delete' },
|
||||||
} },
|
} },
|
||||||
{ text = 'View', dropdown = {
|
{ text = 'View', dropdown = {
|
||||||
@@ -61,31 +66,134 @@ local Browser = UI.Page {
|
|||||||
{ text = 'Hidden ^h', event = 'toggle_hidden' },
|
{ text = 'Hidden ^h', event = 'toggle_hidden' },
|
||||||
{ text = 'Dir Size ^s', event = 'toggle_dirSize' },
|
{ text = 'Dir Size ^s', event = 'toggle_dirSize' },
|
||||||
} },
|
} },
|
||||||
|
{ text = '\187',
|
||||||
|
x = -3,
|
||||||
|
dropdown = {
|
||||||
|
{ text = 'Associations', event = 'associate' },
|
||||||
|
} },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid = UI.ScrollingGrid {
|
grid = UI.ScrollingGrid {
|
||||||
columns = {
|
columns = {
|
||||||
{ heading = 'Name', key = 'name' },
|
{ heading = 'Name', key = 'name' },
|
||||||
{ key = 'flags', width = 2 },
|
{ key = 'flags', width = 3, textColor = 'lightGray' },
|
||||||
{ heading = 'Size', key = 'fsize', width = 5 },
|
{ heading = 'Size', key = 'fsize', width = 5, textColor = 'yellow' },
|
||||||
},
|
},
|
||||||
sortColumn = 'name',
|
sortColumn = 'name',
|
||||||
y = 2, ey = -2,
|
y = 2, ey = -2,
|
||||||
|
sortCompare = function(self, a, b)
|
||||||
|
if self.sortColumn == 'fsize' then
|
||||||
|
return a.size < b.size
|
||||||
|
elseif self.sortColumn == 'flags' then
|
||||||
|
return a.flags < b.flags
|
||||||
|
end
|
||||||
|
if a.isDir == b.isDir then
|
||||||
|
return a.name:lower() < b.name:lower()
|
||||||
|
end
|
||||||
|
return a.isDir
|
||||||
|
end,
|
||||||
|
getRowTextColor = function(_, file)
|
||||||
|
if file.marked then
|
||||||
|
return colors.green
|
||||||
|
end
|
||||||
|
if file.isDir then
|
||||||
|
return colors.cyan
|
||||||
|
end
|
||||||
|
if file.isReadOnly then
|
||||||
|
return colors.pink
|
||||||
|
end
|
||||||
|
return colors.white
|
||||||
|
end,
|
||||||
|
eventHandler = function(self, event)
|
||||||
|
if event.type == 'copy' then -- let copy be handled by parent
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return UI.ScrollingGrid.eventHandler(self, event)
|
||||||
|
end
|
||||||
},
|
},
|
||||||
statusBar = UI.StatusBar {
|
statusBar = UI.StatusBar {
|
||||||
columns = {
|
columns = {
|
||||||
{ key = 'status' },
|
{ key = 'status' },
|
||||||
{ key = 'totalSize', width = 6 },
|
{ key = 'totalSize', width = 6 },
|
||||||
},
|
},
|
||||||
|
draw = function(self)
|
||||||
|
if self.parent.dir then
|
||||||
|
local info = '#:' .. Util.size(self.parent.dir.files)
|
||||||
|
local numMarked = Util.size(marked)
|
||||||
|
if numMarked > 0 then
|
||||||
|
info = info .. ' M:' .. numMarked
|
||||||
|
end
|
||||||
|
self:setValue('info', info)
|
||||||
|
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
|
||||||
|
UI.StatusBar.draw(self)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
question = UI.Question {
|
||||||
|
y = -2, x = -19,
|
||||||
|
label = 'Delete',
|
||||||
|
},
|
||||||
|
notification = UI.Notification { },
|
||||||
|
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 = {
|
accelerators = {
|
||||||
q = 'quit',
|
[ 'control-q' ] = 'quit',
|
||||||
|
c = 'cedit',
|
||||||
e = 'edit',
|
e = 'edit',
|
||||||
s = 'shell',
|
s = 'shell',
|
||||||
|
p = 'pastebin',
|
||||||
r = 'refresh',
|
r = 'refresh',
|
||||||
space = 'mark',
|
[ ' ' ] = 'mark',
|
||||||
|
m = 'mark',
|
||||||
backspace = 'updir',
|
backspace = 'updir',
|
||||||
m = 'move',
|
|
||||||
u = 'unmark',
|
u = 'unmark',
|
||||||
d = 'delete',
|
d = 'delete',
|
||||||
delete = 'delete',
|
delete = 'delete',
|
||||||
@@ -102,66 +210,19 @@ function Browser:enable()
|
|||||||
self:setFocus(self.grid)
|
self:setFocus(self.grid)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Browser.menuBar:getActive(menuItem)
|
function Browser.menuBar.getActive(_, menuItem)
|
||||||
local file = Browser.grid:getSelected()
|
local file = Browser.grid:getSelected()
|
||||||
if file then
|
if menuItem.flags == FILE then
|
||||||
if menuItem.event == 'edit' or menuItem.event == 'run' then
|
return file and not file.isDir
|
||||||
return not file.isDir
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function Browser.grid:sortCompare(a, b)
|
|
||||||
if self.sortColumn == 'fsize' then
|
|
||||||
return a.size < b.size
|
|
||||||
elseif self.sortColumn == 'flags' then
|
|
||||||
return a.flags < b.flags
|
|
||||||
end
|
|
||||||
if a.isDir == b.isDir then
|
|
||||||
return a.name:lower() < b.name:lower()
|
|
||||||
end
|
|
||||||
return a.isDir
|
|
||||||
end
|
|
||||||
|
|
||||||
function Browser.grid:getRowTextColor(file)
|
|
||||||
if file.marked then
|
|
||||||
return colors.green
|
|
||||||
end
|
|
||||||
if file.isDir then
|
|
||||||
return colors.cyan
|
|
||||||
end
|
|
||||||
if file.isReadOnly then
|
|
||||||
return colors.pink
|
|
||||||
end
|
|
||||||
return colors.white
|
|
||||||
end
|
|
||||||
|
|
||||||
function Browser.grid:eventHandler(event)
|
|
||||||
if event.type == 'copy' then -- let copy be handled by parent
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
return UI.ScrollingGrid.eventHandler(self, event)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Browser.statusBar:draw()
|
|
||||||
if self.parent.dir then
|
|
||||||
local info = '#:' .. Util.size(self.parent.dir.files)
|
|
||||||
local numMarked = Util.size(marked)
|
|
||||||
if numMarked > 0 then
|
|
||||||
info = info .. ' M:' .. numMarked
|
|
||||||
end
|
|
||||||
self:setValue('info', info)
|
|
||||||
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
|
|
||||||
UI.StatusBar.draw(self)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Browser:setStatus(status, ...)
|
function Browser:setStatus(status, ...)
|
||||||
self.statusBar:timedStatus(string.format(status, ...))
|
self.notification:info(string.format(status, ...))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Browser:unmarkAll()
|
function Browser.unmarkAll()
|
||||||
for _,m in pairs(marked) do
|
for _,m in pairs(marked) do
|
||||||
m.marked = false
|
m.marked = false
|
||||||
end
|
end
|
||||||
@@ -192,7 +253,6 @@ function Browser:getDirectory(directory)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Browser:updateDirectory(dir)
|
function Browser:updateDirectory(dir)
|
||||||
|
|
||||||
dir.size = 0
|
dir.size = 0
|
||||||
dir.totalSize = 0
|
dir.totalSize = 0
|
||||||
Util.clear(dir.files)
|
Util.clear(dir.files)
|
||||||
@@ -202,10 +262,11 @@ function Browser:updateDirectory(dir)
|
|||||||
dir.size = #files
|
dir.size = #files
|
||||||
for _, file in pairs(files) do
|
for _, file in pairs(files) do
|
||||||
file.fullName = fs.combine(dir.name, file.name)
|
file.fullName = fs.combine(dir.name, file.name)
|
||||||
file.flags = ''
|
file.flags = file.fstype or ' '
|
||||||
if not file.isDir then
|
if not file.isDir then
|
||||||
dir.totalSize = dir.totalSize + file.size
|
dir.totalSize = dir.totalSize + file.size
|
||||||
file.fsize = formatSize(file.size)
|
file.fsize = formatSize(file.size)
|
||||||
|
file.flags = file.flags .. ' '
|
||||||
else
|
else
|
||||||
if config.showDirSizes then
|
if config.showDirSizes then
|
||||||
file.size = fs.getSize(file.fullName, true)
|
file.size = fs.getSize(file.fullName, true)
|
||||||
@@ -213,11 +274,9 @@ function Browser:updateDirectory(dir)
|
|||||||
dir.totalSize = dir.totalSize + file.size
|
dir.totalSize = dir.totalSize + file.size
|
||||||
file.fsize = formatSize(file.size)
|
file.fsize = formatSize(file.size)
|
||||||
end
|
end
|
||||||
file.flags = 'D'
|
file.flags = file.flags .. 'D'
|
||||||
end
|
|
||||||
if file.isReadOnly then
|
|
||||||
file.flags = file.flags .. 'R'
|
|
||||||
end
|
end
|
||||||
|
file.flags = file.flags .. (file.isReadOnly and 'R' or ' ')
|
||||||
if config.showHidden or file.name:sub(1, 1) ~= '.' then
|
if config.showHidden or file.name:sub(1, 1) ~= '.' then
|
||||||
dir.files[file.fullName] = file
|
dir.files[file.fullName] = file
|
||||||
end
|
end
|
||||||
@@ -229,7 +288,6 @@ function Browser:updateDirectory(dir)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Browser:setDir(dirName, noStatus)
|
function Browser:setDir(dirName, noStatus)
|
||||||
|
|
||||||
self:unmarkAll()
|
self:unmarkAll()
|
||||||
|
|
||||||
if self.dir then
|
if self.dir then
|
||||||
@@ -255,9 +313,15 @@ function Browser:setDir(dirName, noStatus)
|
|||||||
self.grid:setIndex(self.dir.index)
|
self.grid:setIndex(self.dir.index)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Browser:run(path, ...)
|
function Browser:run(...)
|
||||||
local tabId = shell.openTab(path, ...)
|
if multishell then
|
||||||
|
local tabId = shell.openTab(...)
|
||||||
multishell.setFocus(tabId)
|
multishell.setFocus(tabId)
|
||||||
|
else
|
||||||
|
shell.run(...)
|
||||||
|
Event.terminate = false
|
||||||
|
self:draw()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Browser:hasMarked()
|
function Browser:hasMarked()
|
||||||
@@ -276,19 +340,37 @@ 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 == 'cedit' and file then
|
||||||
|
self:run('cedit', file.name)
|
||||||
|
self:setStatus('Started cloud edit')
|
||||||
|
|
||||||
elseif event.type == 'shell' then
|
elseif event.type == 'shell' then
|
||||||
self:run('sys/apps/shell')
|
self:run('shell')
|
||||||
|
|
||||||
elseif event.type == 'refresh' then
|
elseif event.type == 'refresh' then
|
||||||
self:updateDirectory(self.dir)
|
self:updateDirectory(self.dir)
|
||||||
self.grid:draw()
|
self.grid:draw()
|
||||||
self:setStatus('Refreshed')
|
self:setStatus('Refreshed')
|
||||||
|
|
||||||
|
elseif event.type == 'associate' then
|
||||||
|
self.associations:show()
|
||||||
|
|
||||||
|
elseif event.type == 'pastebin' then
|
||||||
|
if file and not file.isDir then
|
||||||
|
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
|
||||||
|
|
||||||
elseif event.type == 'toggle_hidden' then
|
elseif event.type == 'toggle_hidden' then
|
||||||
config.showHidden = not config.showHidden
|
config.showHidden = not config.showHidden
|
||||||
Config.update('Files', config)
|
Config.update('Files', config)
|
||||||
@@ -330,10 +412,15 @@ function Browser:eventHandler(event)
|
|||||||
if file then
|
if file then
|
||||||
if file.isDir then
|
if file.isDir then
|
||||||
self:setDir(file.fullName)
|
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
|
else
|
||||||
self:run(file.name)
|
self:run(file.name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
elseif event.type == 'updir' then
|
elseif event.type == 'updir' then
|
||||||
local dir = (self.dir.name:match("(.*/)"))
|
local dir = (self.dir.name:match("(.*/)"))
|
||||||
@@ -341,28 +428,25 @@ function Browser:eventHandler(event)
|
|||||||
|
|
||||||
elseif event.type == 'delete' then
|
elseif event.type == 'delete' then
|
||||||
if self:hasMarked() then
|
if self:hasMarked() then
|
||||||
local width = self.statusBar:getColumnWidth('status')
|
self.question:show()
|
||||||
self.statusBar:setColumnWidth('status', UI.term.width)
|
|
||||||
self.statusBar:setValue('status', 'Delete marked? (y/n)')
|
|
||||||
self.statusBar:draw()
|
|
||||||
self.statusBar:sync()
|
|
||||||
local _, ch = os.pullEvent('char')
|
|
||||||
if ch == 'y' or ch == 'Y' then
|
|
||||||
for _,m in pairs(marked) do
|
|
||||||
pcall(function()
|
|
||||||
fs.delete(m.fullName)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
return true
|
||||||
|
|
||||||
|
elseif event.type == 'question_yes' then
|
||||||
|
for _,m in pairs(marked) do
|
||||||
|
pcall(fs.delete, m.fullName)
|
||||||
end
|
end
|
||||||
marked = { }
|
marked = { }
|
||||||
self.statusBar:setColumnWidth('status', width)
|
|
||||||
self.statusBar:setValue('status', '/' .. self.dir.name)
|
|
||||||
self:updateDirectory(self.dir)
|
self:updateDirectory(self.dir)
|
||||||
|
|
||||||
|
self.question:hide()
|
||||||
self.statusBar:draw()
|
self.statusBar:draw()
|
||||||
self.grid:draw()
|
self.grid:draw()
|
||||||
self:setFocus(self.grid)
|
self:setFocus(self.grid)
|
||||||
end
|
|
||||||
|
elseif event.type == 'question_no' then
|
||||||
|
self.question:hide()
|
||||||
|
self:setFocus(self.grid)
|
||||||
|
|
||||||
elseif event.type == 'copy' or event.type == 'cut' then
|
elseif event.type == 'copy' or event.type == 'cut' then
|
||||||
if self:hasMarked() then
|
if self:hasMarked() then
|
||||||
@@ -374,9 +458,14 @@ function Browser:eventHandler(event)
|
|||||||
self:setStatus('Copied %d file(s)', Util.size(copied))
|
self:setStatus('Copied %d file(s)', Util.size(copied))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
elseif event.type == 'copy_path' then
|
||||||
|
if file then
|
||||||
|
os.queueEvent('clipboard_copy', file.fullName)
|
||||||
|
end
|
||||||
|
|
||||||
elseif event.type == 'paste' then
|
elseif event.type == 'paste' then
|
||||||
for _,m in pairs(copied) do
|
for _,m in pairs(copied) do
|
||||||
local s, m = pcall(function()
|
pcall(function()
|
||||||
if cutMode then
|
if cutMode then
|
||||||
fs.move(m.fullName, fs.combine(self.dir.name, m.name))
|
fs.move(m.fullName, fs.combine(self.dir.name, m.name))
|
||||||
else
|
else
|
||||||
@@ -395,12 +484,62 @@ function Browser:eventHandler(event)
|
|||||||
return true
|
return true
|
||||||
end
|
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
|
||||||
|
|
||||||
--[[-- 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()
|
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
_G.requireInjector()
|
local fuzzy = require('opus.fuzzy')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local colors = _G.colors
|
|
||||||
local help = _G.help
|
local help = _G.help
|
||||||
local multishell = _ENV.multishell
|
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'Help')
|
|
||||||
UI:configure('Help', ...)
|
UI:configure('Help', ...)
|
||||||
|
|
||||||
local topics = { }
|
local topics = { }
|
||||||
for _,topic in pairs(help.topics()) do
|
for _,topic in pairs(help.topics()) do
|
||||||
if help.lookup(topic) then
|
table.insert(topics, { name = topic, lname = topic:lower() })
|
||||||
table.insert(topics, { name = topic })
|
|
||||||
end
|
|
||||||
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,
|
||||||
},
|
},
|
||||||
@@ -32,70 +26,71 @@ local page = UI.Page {
|
|||||||
columns = {
|
columns = {
|
||||||
{ heading = 'Topic', 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)
|
||||||
|
|
||||||
local topicPage = UI.Page {
|
|
||||||
backgroundColor = colors.black,
|
|
||||||
titleBar = UI.TitleBar {
|
|
||||||
title = 'text',
|
|
||||||
previousPage = true,
|
|
||||||
},
|
|
||||||
helpText = UI.TextArea {
|
|
||||||
backgroundColor = colors.black,
|
|
||||||
x = 2, ex = -1, y = 3, ey = -2,
|
|
||||||
},
|
|
||||||
accelerators = {
|
|
||||||
q = 'back',
|
|
||||||
backspace = 'back',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function topicPage:eventHandler(event)
|
|
||||||
if event.type == 'back' then
|
|
||||||
UI:setPreviousPage()
|
|
||||||
end
|
|
||||||
return UI.Page.eventHandler(self, event)
|
|
||||||
end
|
|
||||||
|
|
||||||
function page:eventHandler(event)
|
|
||||||
if event.type == 'quit' then
|
if event.type == 'quit' then
|
||||||
UI:exitPullEvents()
|
UI:quit()
|
||||||
|
|
||||||
elseif event.type == 'grid_select' then
|
elseif event.type == 'grid_select' then
|
||||||
if self.grid:getSelected() then
|
if self.grid:getSelected() then
|
||||||
local name = self.grid:getSelected().name
|
UI:setPage('topic', self.grid:getSelected().name)
|
||||||
local f = help.lookup(name)
|
|
||||||
|
|
||||||
topicPage.titleBar.title = name
|
|
||||||
topicPage.helpText:setText(Util.readFile(f))
|
|
||||||
|
|
||||||
UI:setPage(topicPage)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif event.type == 'text_change' then
|
elseif event.type == 'text_change' then
|
||||||
if #event.text == 0 then
|
if not event.text then
|
||||||
self.grid.values = topics
|
self.grid.sortColumn = 'lname'
|
||||||
else
|
else
|
||||||
self.grid.values = { }
|
self.grid.sortColumn = 'score'
|
||||||
for _,f in pairs(topics) do
|
self.grid.inverseSort = false
|
||||||
if string.find(f.name, event.text) then
|
local pattern = event.text:lower()
|
||||||
table.insert(self.grid.values, f)
|
for _,v in pairs(self.grid.values) do
|
||||||
end
|
v.score = -fuzzy(v.lname, pattern)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self.grid:update()
|
self.grid:update()
|
||||||
self.grid:setIndex(1)
|
self.grid:setIndex(1)
|
||||||
self.grid:draw()
|
self.grid:draw()
|
||||||
|
|
||||||
else
|
else
|
||||||
return UI.Page.eventHandler(self, event)
|
return UI.Page.eventHandler(self, event)
|
||||||
end
|
end
|
||||||
end
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
UI:setPage(page)
|
UI:addPage('topic', UI.Page {
|
||||||
UI:pullEvents()
|
backgroundColor = 'black',
|
||||||
|
titleBar = UI.TitleBar {
|
||||||
|
title = 'text',
|
||||||
|
event = 'back',
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.titleBar.title = name
|
||||||
|
self.helpText:setText(f and Util.readFile(f) or 'No help available for ' .. name)
|
||||||
|
|
||||||
|
return UI.Page.enable(self)
|
||||||
|
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()
|
||||||
|
|||||||
162
sys/apps/Lua.lua
162
sys/apps/Lua.lua
@@ -1,28 +1,22 @@
|
|||||||
local injector = _G.requireInjector or load(http.get('https://raw.githubusercontent.com/kepler155c/opus/master/sys/apis/injector.lua').readAll())()
|
local History = require('opus.history')
|
||||||
injector()
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
local Event = require('event')
|
|
||||||
local History = require('history')
|
|
||||||
local Peripheral = require('peripheral')
|
|
||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local colors = _G.colors
|
local colors = _G.colors
|
||||||
local multishell = _ENV.multishell
|
|
||||||
local os = _G.os
|
local os = _G.os
|
||||||
local textutils = _G.textutils
|
local textutils = _G.textutils
|
||||||
|
local term = _G.term
|
||||||
|
|
||||||
local sandboxEnv = setmetatable(Util.shallowCopy(_ENV), { __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
|
||||||
injector(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 extChars = Util.getVersion() > 1.76
|
|
||||||
|
|
||||||
local page = UI.Page {
|
local page = UI.Page {
|
||||||
menuBar = UI.MenuBar {
|
menuBar = UI.MenuBar {
|
||||||
@@ -35,7 +29,6 @@ local page = UI.Page {
|
|||||||
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',
|
||||||
@@ -44,12 +37,12 @@ local page = UI.Page {
|
|||||||
[ 'control-space' ] = 'autocomplete',
|
[ 'control-space' ] = 'autocomplete',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
indicator = UI.Text {
|
tabs = UI.Tabs {
|
||||||
backgroundColor = colors.black,
|
|
||||||
y = 2, x = -1, width = 1,
|
|
||||||
},
|
|
||||||
grid = UI.ScrollingGrid {
|
|
||||||
y = 3,
|
y = 3,
|
||||||
|
formatted = UI.Tab {
|
||||||
|
title = 'Formatted',
|
||||||
|
index = 1,
|
||||||
|
grid = UI.ScrollingGrid {
|
||||||
columns = {
|
columns = {
|
||||||
{ heading = 'Key', key = 'name' },
|
{ heading = 'Key', key = 'name' },
|
||||||
{ heading = 'Value', key = 'value' },
|
{ heading = 'Value', key = 'value' },
|
||||||
@@ -57,28 +50,34 @@ local page = UI.Page {
|
|||||||
sortColumn = 'name',
|
sortColumn = 'name',
|
||||||
autospace = true,
|
autospace = true,
|
||||||
},
|
},
|
||||||
notification = UI.Notification(),
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
function page.indicator:showResult(s)
|
page.grid = page.tabs.formatted.grid
|
||||||
local values = {
|
page.output = page.tabs.output.output
|
||||||
[ true ] = { c = colors.green, i = (extChars and '\003') or '+' },
|
|
||||||
[ false ] = { c = colors.red, i = 'x' }
|
|
||||||
}
|
|
||||||
|
|
||||||
self.textColor = values[s].c
|
|
||||||
self.value = values[s].i
|
|
||||||
self:draw()
|
|
||||||
end
|
|
||||||
|
|
||||||
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)
|
||||||
|
else
|
||||||
|
self.prompt:setPosition(#value)
|
||||||
end
|
end
|
||||||
|
|
||||||
self.prompt:draw()
|
self.prompt:draw()
|
||||||
@@ -88,8 +87,8 @@ function page:setPrompt(value, focus)
|
|||||||
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)
|
||||||
@@ -126,26 +125,30 @@ end
|
|||||||
|
|
||||||
function page:eventHandler(event)
|
function page:eventHandler(event)
|
||||||
if event.type == 'global' then
|
if event.type == 'global' then
|
||||||
self:setPrompt('', true)
|
self:setPrompt('_G', true)
|
||||||
self:executeStatement('_G')
|
self:executeStatement('_G')
|
||||||
command = nil
|
command = nil
|
||||||
|
|
||||||
elseif event.type == 'local' then
|
elseif event.type == 'local' then
|
||||||
self:setPrompt('', true)
|
self:setPrompt('_ENV', true)
|
||||||
self:executeStatement('_ENV')
|
self:executeStatement('_ENV')
|
||||||
command = nil
|
command = nil
|
||||||
|
|
||||||
|
elseif event.type == 'tab_select' then
|
||||||
|
self:setFocus(self.prompt)
|
||||||
|
|
||||||
|
elseif event.type == 'show_output' then
|
||||||
|
self.tabs:selectTab(self.tabs.output)
|
||||||
|
|
||||||
elseif event.type == 'autocomplete' then
|
elseif event.type == 'autocomplete' then
|
||||||
local sz = #self.prompt.value
|
local value = self.prompt.value or ''
|
||||||
local pos = self.prompt.pos
|
local sz = #value
|
||||||
self:setPrompt(autocomplete(sandboxEnv, self.prompt.value, self.prompt.pos))
|
local pos = self.prompt.entry.pos
|
||||||
self.prompt:setPosition(pos + #self.prompt.value - sz)
|
self:setPrompt(autocomplete(sandboxEnv, value, self.prompt.entry.pos))
|
||||||
|
self.prompt:setPosition(pos + #(self.prompt.value or '') - sz)
|
||||||
self.prompt:updateCursor()
|
self.prompt:updateCursor()
|
||||||
|
|
||||||
elseif event.type == 'device' then
|
elseif event.type == 'device' then
|
||||||
if not _G.device then
|
|
||||||
sandboxEnv.device = Peripheral.getList()
|
|
||||||
end
|
|
||||||
self:setPrompt('device', true)
|
self:setPrompt('device', true)
|
||||||
self:executeStatement('device')
|
self:executeStatement('device')
|
||||||
|
|
||||||
@@ -163,11 +166,9 @@ function page:eventHandler(event)
|
|||||||
history:reset()
|
history:reset()
|
||||||
|
|
||||||
elseif event.type == 'command_enter' then
|
elseif event.type == 'command_enter' then
|
||||||
local s = tostring(self.prompt.value)
|
local s = tostring(self.prompt.value or '')
|
||||||
|
|
||||||
if #s > 0 then
|
if #s > 0 then
|
||||||
history:add(s)
|
|
||||||
history:back()
|
|
||||||
self:executeStatement(s)
|
self:executeStatement(s)
|
||||||
else
|
else
|
||||||
local t = { }
|
local t = { }
|
||||||
@@ -183,8 +184,7 @@ function page:eventHandler(event)
|
|||||||
command = nil
|
command = nil
|
||||||
self.grid:setValues(t)
|
self.grid:setValues(t)
|
||||||
self.grid:setIndex(1)
|
self.grid:setIndex(1)
|
||||||
self.grid:adjustWidth()
|
self.grid:draw()
|
||||||
self:draw()
|
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
|
|
||||||
@@ -230,8 +230,7 @@ function page:setResult(result)
|
|||||||
end
|
end
|
||||||
self.grid:setValues(t)
|
self.grid:setValues(t)
|
||||||
self.grid:setIndex(1)
|
self.grid:setIndex(1)
|
||||||
self.grid:adjustWidth()
|
self.grid:draw()
|
||||||
self:draw()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function page.grid:eventHandler(event)
|
function page.grid:eventHandler(event)
|
||||||
@@ -281,25 +280,46 @@ end
|
|||||||
|
|
||||||
function page:rawExecute(s)
|
function page:rawExecute(s)
|
||||||
local fn, m
|
local fn, m
|
||||||
|
local wrapped
|
||||||
|
|
||||||
fn = load('return (' ..s.. ')', 'lua', nil, sandboxEnv)
|
fn = load('return (' ..s.. ')', 'lua', nil, sandboxEnv)
|
||||||
|
|
||||||
if fn then
|
if fn then
|
||||||
fn = load('return {' ..s.. '}', 'lua', nil, sandboxEnv)
|
fn = load('return {' ..s.. '}', 'lua', nil, sandboxEnv)
|
||||||
|
wrapped = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local t = os.clock()
|
||||||
if fn then
|
if fn then
|
||||||
fn, m = pcall(fn)
|
fn, m = pcall(fn)
|
||||||
if #m == 1 then
|
if #m <= 1 and wrapped then
|
||||||
m = m[1]
|
m = m[1]
|
||||||
end
|
end
|
||||||
return fn, m
|
else
|
||||||
end
|
|
||||||
|
|
||||||
fn, m = load(s, 'lua', nil, sandboxEnv)
|
fn, m = load(s, 'lua', nil, sandboxEnv)
|
||||||
if fn then
|
if fn then
|
||||||
|
t = os.clock()
|
||||||
fn, m = pcall(fn)
|
fn, m = pcall(fn)
|
||||||
end
|
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
|
return fn, m
|
||||||
end
|
end
|
||||||
@@ -307,27 +327,45 @@ end
|
|||||||
function page:executeStatement(statement)
|
function page:executeStatement(statement)
|
||||||
command = statement
|
command = statement
|
||||||
|
|
||||||
local s, m = self:rawExecute(command)
|
history:add(statement)
|
||||||
|
history:back()
|
||||||
|
|
||||||
if s and m then
|
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))
|
||||||
|
|
||||||
|
pcall(function()
|
||||||
|
s, m = self:rawExecute(command)
|
||||||
|
end)
|
||||||
|
|
||||||
|
term.redirect(oterm)
|
||||||
|
counter = counter + 1
|
||||||
|
|
||||||
|
if s and type(m) ~= "nil" then
|
||||||
self:setResult(m)
|
self:setResult(m)
|
||||||
else
|
else
|
||||||
self.grid:setValues({ })
|
self.grid:setValues({ })
|
||||||
self.grid:draw()
|
self.grid:draw()
|
||||||
if m then
|
if m and not self.output.enabled then
|
||||||
self.notification:error(m, 5)
|
self:emit({ type = 'show_output' })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self.indicator:showResult(not not s)
|
|
||||||
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()
|
|
||||||
|
|||||||
@@ -1,29 +1,28 @@
|
|||||||
_G.requireInjector()
|
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 Socket = require('socket')
|
|
||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local colors = _G.colors
|
|
||||||
local device = _G.device
|
local device = _G.device
|
||||||
local multishell = _ENV.multishell
|
|
||||||
local network = _G.network
|
local network = _G.network
|
||||||
local os = _G.os
|
|
||||||
local shell = _ENV.shell
|
local shell = _ENV.shell
|
||||||
|
|
||||||
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 {
|
||||||
@@ -32,15 +31,21 @@ local page = UI.Page {
|
|||||||
{ text = 'Connect', dropdown = {
|
{ text = 'Connect', dropdown = {
|
||||||
{ text = 'Telnet t', event = 'telnet' },
|
{ text = 'Telnet t', event = 'telnet' },
|
||||||
{ text = 'VNC v', event = 'vnc' },
|
{ text = 'VNC v', event = 'vnc' },
|
||||||
UI.MenuBar.spacer,
|
{ spacer = true },
|
||||||
{ text = 'Reboot r', event = 'reboot' },
|
{ text = 'Reboot r', event = 'reboot' },
|
||||||
} },
|
} },
|
||||||
--{ text = 'Chat', event = 'chat' },
|
|
||||||
{ text = 'Trust', dropdown = {
|
{ text = 'Trust', dropdown = {
|
||||||
{ text = 'Establish', event = 'trust' },
|
{ text = 'Establish', event = 'trust' },
|
||||||
{ text = 'Remove', event = 'untrust' },
|
|
||||||
} },
|
} },
|
||||||
{ text = 'Help', event = 'help' },
|
{
|
||||||
|
text = '\187',
|
||||||
|
x = -3,
|
||||||
|
dropdown = {
|
||||||
|
{ text = 'Port Status', event = 'ports', modem = true },
|
||||||
|
{ spacer = true },
|
||||||
|
{ text = 'Help', event = 'help', noCheck = true },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid = UI.ScrollingGrid {
|
grid = UI.ScrollingGrid {
|
||||||
@@ -49,13 +54,66 @@ local page = UI.Page {
|
|||||||
columns = gridColumns,
|
columns = gridColumns,
|
||||||
sortColumn = 'label',
|
sortColumn = 'label',
|
||||||
autospace = true,
|
autospace = true,
|
||||||
|
getRowTextColor = function(self, row, selected)
|
||||||
|
if not row.active then
|
||||||
|
return 'lightGray'
|
||||||
|
end
|
||||||
|
return UI.Grid.getRowTextColor(self, row, selected)
|
||||||
|
end,
|
||||||
|
getDisplayValues = function(_, row)
|
||||||
|
row = Util.shallowCopy(row)
|
||||||
|
if row.uptime then
|
||||||
|
if row.uptime < 60 then
|
||||||
|
row.uptime = string.format("%ds", math.floor(row.uptime))
|
||||||
|
elseif row.uptime < 3600 then
|
||||||
|
row.uptime = string.format("%sm", math.floor(row.uptime / 60))
|
||||||
|
else
|
||||||
|
row.uptime = string.format("%sh", math.floor(row.uptime / 3600))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if row.fuel then
|
||||||
|
row.fuel = row.fuel > 0 and Util.toBytes(row.fuel) or ''
|
||||||
|
end
|
||||||
|
if row.distance then
|
||||||
|
row.distance = Util.toBytes(Util.round(row.distance, 1))
|
||||||
|
end
|
||||||
|
return row
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
ports = UI.SlideOut {
|
||||||
|
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 { },
|
notification = UI.Notification { },
|
||||||
accelerators = {
|
accelerators = {
|
||||||
t = 'telnet',
|
t = 'telnet',
|
||||||
v = 'vnc',
|
v = 'vnc',
|
||||||
r = 'reboot',
|
r = 'reboot',
|
||||||
q = 'quit',
|
[ 'control-q' ] = 'quit',
|
||||||
c = 'clear',
|
c = 'clear',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -79,23 +137,66 @@ local function sendCommand(host, command)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function page.ports.grid:update()
|
||||||
|
local transport = network:getTransport()
|
||||||
|
|
||||||
|
local function findConnection(port)
|
||||||
|
if transport then
|
||||||
|
for _,socket in pairs(transport.sockets) do
|
||||||
|
if socket.sport == port then
|
||||||
|
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
|
||||||
|
|
||||||
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' then
|
if event.type == 'telnet' then
|
||||||
multishell.openTab({
|
shell.openForegroundTab('telnet ' .. t.id)
|
||||||
path = 'sys/apps/telnet.lua',
|
|
||||||
focused = true,
|
|
||||||
args = { t.id },
|
|
||||||
title = t.label,
|
|
||||||
})
|
|
||||||
elseif event.type == 'vnc' then
|
elseif event.type == 'vnc' then
|
||||||
multishell.openTab({
|
shell.openForegroundTab('vnc.lua ' .. t.id)
|
||||||
path = 'sys/apps/vnc.lua',
|
--[[
|
||||||
focused = true,
|
os.queueEvent('overview_shortcut', {
|
||||||
args = { t.id },
|
|
||||||
title = t.label,
|
title = t.label,
|
||||||
|
category = "VNC",
|
||||||
|
icon = "\010\030 \009\009\031e\\\031 \031e/\031dn\010\030 \009\009 \031e\\/\031 \031bc",
|
||||||
|
run = "vnc.lua " .. t.id,
|
||||||
})
|
})
|
||||||
|
--]]
|
||||||
|
|
||||||
elseif event.type == 'clear' then
|
elseif event.type == 'clear' then
|
||||||
Util.clear(network)
|
Util.clear(network)
|
||||||
page.grid:update()
|
page.grid:update()
|
||||||
@@ -104,18 +205,6 @@ function page:eventHandler(event)
|
|||||||
elseif event.type == 'trust' then
|
elseif event.type == 'trust' then
|
||||||
shell.openForegroundTab('trust ' .. t.id)
|
shell.openForegroundTab('trust ' .. t.id)
|
||||||
|
|
||||||
elseif event.type == 'untrust' then
|
|
||||||
local trustList = Util.readTable('usr/.known_hosts') or { }
|
|
||||||
trustList[t.id] = nil
|
|
||||||
Util.writeTable('usr/.known_hosts', trustList)
|
|
||||||
|
|
||||||
elseif event.type == 'chat' then
|
|
||||||
multishell.openTab({
|
|
||||||
path = 'sys/apps/shell',
|
|
||||||
args = { 'chat join opusChat-' .. t.id .. ' guest-' .. os.getComputerID() },
|
|
||||||
title = 'Chatroom',
|
|
||||||
focused = true,
|
|
||||||
})
|
|
||||||
elseif event.type == 'reboot' then
|
elseif event.type == 'reboot' then
|
||||||
sendCommand(t.id, 'reboot')
|
sendCommand(t.id, 'reboot')
|
||||||
|
|
||||||
@@ -123,65 +212,45 @@ function page:eventHandler(event)
|
|||||||
sendCommand(t.id, 'shutdown')
|
sendCommand(t.id, 'shutdown')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if event.type == 'help' then
|
if event.type == 'help' then
|
||||||
UI:setPage(UI.Dialog {
|
shell.switchTab(shell.openTab('Help Networking'))
|
||||||
title = 'Network Help',
|
|
||||||
height = 10,
|
|
||||||
backgroundColor = colors.white,
|
|
||||||
text = UI.TextArea {
|
|
||||||
x = 2, y = 2,
|
|
||||||
backgroundColor = colors.white,
|
|
||||||
value = [[
|
|
||||||
In order to connect to another computer:
|
|
||||||
|
|
||||||
1. The target computer must have a password set (run 'password' from the shell prompt).
|
elseif event.type == 'ports' then
|
||||||
2. From this computer, click trust and enter the password for that computer.
|
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)
|
||||||
|
|
||||||
This only needs to be done once.
|
|
||||||
]],
|
|
||||||
},
|
|
||||||
accelerators = {
|
|
||||||
q = 'cancel',
|
|
||||||
}
|
|
||||||
})
|
|
||||||
elseif event.type == 'quit' then
|
elseif event.type == 'quit' then
|
||||||
Event.exitPullEvents()
|
UI:quit()
|
||||||
end
|
end
|
||||||
UI.Page.eventHandler(self, event)
|
UI.Page.eventHandler(self, event)
|
||||||
end
|
end
|
||||||
|
|
||||||
function page.menuBar:getActive(menuItem)
|
function page.menuBar:getActive(menuItem)
|
||||||
local t = page.grid:getSelected()
|
local t = page.grid:getSelected()
|
||||||
if menuItem.event == 'untrust' then
|
if menuItem.modem then
|
||||||
local trustList = Util.readTable('usr/.known_hosts') or { }
|
return not not device.wireless_modem
|
||||||
return t and trustList[t.id]
|
|
||||||
end
|
end
|
||||||
return not not t
|
return menuItem.noCheck or not not t
|
||||||
end
|
|
||||||
|
|
||||||
function page.grid:getRowTextColor(row, selected)
|
|
||||||
if not row.active then
|
|
||||||
return colors.orange
|
|
||||||
end
|
|
||||||
return UI.Grid.getRowTextColor(self, row, selected)
|
|
||||||
end
|
|
||||||
|
|
||||||
function page.grid:getDisplayValues(row)
|
|
||||||
row = Util.shallowCopy(row)
|
|
||||||
if row.uptime then
|
|
||||||
if row.uptime < 60 then
|
|
||||||
row.uptime = string.format("%ds", math.floor(row.uptime))
|
|
||||||
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()
|
||||||
@@ -209,4 +278,4 @@ if not device.wireless_modem then
|
|||||||
end
|
end
|
||||||
|
|
||||||
UI:setPage(page)
|
UI:setPage(page)
|
||||||
UI:pullEvents()
|
UI:start()
|
||||||
|
|||||||
@@ -1,24 +1,48 @@
|
|||||||
_G.requireInjector()
|
local Array = require('opus.array')
|
||||||
|
local class = require('opus.class')
|
||||||
local class = require('class')
|
local Config = require('opus.config')
|
||||||
local Config = require('config')
|
local Event = require('opus.event')
|
||||||
local Event = require('event')
|
local NFT = require('opus.nft')
|
||||||
local FileUI = require('ui.fileui')
|
local Packages = require('opus.packages')
|
||||||
local NFT = require('nft')
|
local SHA = require('opus.crypto.sha2')
|
||||||
local SHA1 = require('sha1')
|
local Tween = require('opus.ui.tween')
|
||||||
local Tween = require('ui.tween')
|
local UI = require('opus.ui')
|
||||||
local UI = require('ui')
|
local Util = require('opus.util')
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
|
local device = _G.device
|
||||||
local fs = _G.fs
|
local fs = _G.fs
|
||||||
local multishell = _ENV.multishell
|
local os = _G.os
|
||||||
local pocket = _G.pocket
|
local pocket = _G.pocket
|
||||||
|
local shell = _ENV.shell
|
||||||
local term = _G.term
|
local term = _G.term
|
||||||
local turtle = _G.turtle
|
local turtle = _G.turtle
|
||||||
|
|
||||||
|
--[[
|
||||||
|
turtle: 39x13
|
||||||
|
computer: 51x19
|
||||||
|
pocket: 26x20
|
||||||
|
]]
|
||||||
|
|
||||||
|
if not _ENV.multishell then
|
||||||
|
error('multishell is required')
|
||||||
|
end
|
||||||
|
|
||||||
local REGISTRY_DIR = 'usr/.registry'
|
local REGISTRY_DIR = 'usr/.registry'
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'Overview')
|
-- iconExt:gsub('.', function(b) return '\\' .. b:byte() end)
|
||||||
|
local DEFAULT_ICON = NFT.parse('\30\55\31\48\136\140\140\140\132\
|
||||||
|
\30\48\31\55\149\31\48\128\128\128\30\55\149\
|
||||||
|
\30\55\31\48\138\143\143\143\133')
|
||||||
|
local TRANS_ICON = NFT.parse('\0302\0312\32\32\32\32\32\
|
||||||
|
\0302\0312\32\32\32\32\32\
|
||||||
|
\0302\0312\32\32\32\32\32')
|
||||||
|
|
||||||
|
-- overview
|
||||||
|
local uid = _ENV.multishell.getCurrent()
|
||||||
|
device.keyboard.addHotkey('control-o', function()
|
||||||
|
_ENV.multishell.setFocus(uid)
|
||||||
|
end)
|
||||||
|
|
||||||
UI:configure('Overview', ...)
|
UI:configure('Overview', ...)
|
||||||
|
|
||||||
local config = {
|
local config = {
|
||||||
@@ -27,19 +51,170 @@ local config = {
|
|||||||
}
|
}
|
||||||
Config.load('Overview', config)
|
Config.load('Overview', config)
|
||||||
|
|
||||||
|
local extSupport = Util.supportsExtChars()
|
||||||
|
|
||||||
local applications = { }
|
local applications = { }
|
||||||
|
local buttons = { }
|
||||||
|
|
||||||
local function loadApplications()
|
local sx, sy = term.current().getSize()
|
||||||
|
local maxRecent = math.ceil(sx * sy / 62)
|
||||||
|
|
||||||
local requirements = {
|
local function ellipsis(s, len)
|
||||||
turtle = function() return turtle end,
|
if #s > len then
|
||||||
advancedTurtle = function() return turtle and term.isColor() end,
|
s = s:sub(1, len - 2) .. '..'
|
||||||
pocket = function() return pocket end,
|
end
|
||||||
advancedPocket = function() return pocket and term.isColor() end,
|
return s
|
||||||
advancedComputer = function() return not turtle and not pocket and term.isColor() end,
|
end
|
||||||
|
|
||||||
|
local function parseIcon(iconText)
|
||||||
|
local icon
|
||||||
|
|
||||||
|
local s, m = pcall(function()
|
||||||
|
icon = NFT.parse(iconText)
|
||||||
|
if icon then
|
||||||
|
if icon.height > 3 or icon.width > 8 then
|
||||||
|
error('Must be an NFT image - 3 rows, 8 cols max')
|
||||||
|
end
|
||||||
|
NFT.transparency(icon)
|
||||||
|
end
|
||||||
|
return icon
|
||||||
|
end)
|
||||||
|
|
||||||
|
if s then
|
||||||
|
return icon
|
||||||
|
end
|
||||||
|
|
||||||
|
return s, m
|
||||||
|
end
|
||||||
|
|
||||||
|
local page = UI.Page {
|
||||||
|
container = UI.Viewport {
|
||||||
|
x = 9, y = 1,
|
||||||
|
},
|
||||||
|
tabBar = UI.TabBar {
|
||||||
|
ey = -2,
|
||||||
|
width = 8,
|
||||||
|
selectedBackgroundColor = 'primary',
|
||||||
|
backgroundColor = 'tertiary',
|
||||||
|
unselectedTextColor = 'lightGray',
|
||||||
|
layout = function(self)
|
||||||
|
self.height = nil
|
||||||
|
UI.TabBar.layout(self)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
tray = UI.Window {
|
||||||
|
y = -1, width = 8,
|
||||||
|
backgroundColor = 'tertiary',
|
||||||
|
newApp = UI.FlatButton {
|
||||||
|
x = 2,
|
||||||
|
text = '+', event = 'new',
|
||||||
|
},
|
||||||
|
mode = UI.FlatButton {
|
||||||
|
x = 4,
|
||||||
|
text = '=', event = 'display_mode',
|
||||||
|
},
|
||||||
|
help = UI.FlatButton {
|
||||||
|
x = 6,
|
||||||
|
text = '?', event = 'help',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editor = UI.SlideOut {
|
||||||
|
y = -12, height = 12,
|
||||||
|
titleBar = UI.TitleBar {
|
||||||
|
title = 'Edit Application',
|
||||||
|
event = 'slide_hide',
|
||||||
|
},
|
||||||
|
form = UI.Form {
|
||||||
|
y = 2, ey = -2,
|
||||||
|
[1] = UI.TextEntry {
|
||||||
|
formLabel = 'Title', formKey = 'title', limit = 11, width = 13, help = 'Application title',
|
||||||
|
required = true,
|
||||||
|
},
|
||||||
|
[2] = UI.TextEntry {
|
||||||
|
formLabel = 'Run', formKey = 'run', limit = 100, help = 'Full path to application',
|
||||||
|
required = true,
|
||||||
|
},
|
||||||
|
[3] = UI.TextEntry {
|
||||||
|
formLabel = 'Category', formKey = 'category', limit = 6, width = 8, help = 'Category of application',
|
||||||
|
required = true,
|
||||||
|
},
|
||||||
|
editIcon = UI.Button {
|
||||||
|
x = 11, y = 6,
|
||||||
|
text = 'Edit', event = 'editIcon', help = 'Edit icon file',
|
||||||
|
},
|
||||||
|
loadIcon = UI.Button {
|
||||||
|
x = 11, y = 8,
|
||||||
|
text = 'Load', event = 'loadIcon', help = 'Load icon file',
|
||||||
|
},
|
||||||
|
helpIcon = UI.Button {
|
||||||
|
x = 11, y = 8,
|
||||||
|
text = 'Load', event = 'loadIcon', help = 'Load icon file',
|
||||||
|
},
|
||||||
|
image = UI.NftImage {
|
||||||
|
backgroundColor = 'black',
|
||||||
|
y = 6, x = 2, height = 3, width = 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
file_open = UI.FileSelect {
|
||||||
|
modal = true,
|
||||||
|
enable = function() end,
|
||||||
|
transitionHint = 'expandUp',
|
||||||
|
show = function(self)
|
||||||
|
UI.FileSelect.enable(self)
|
||||||
|
self:focusFirst()
|
||||||
|
self:draw()
|
||||||
|
end,
|
||||||
|
disable = function(self)
|
||||||
|
UI.FileSelect.disable(self)
|
||||||
|
self.parent:focusFirst()
|
||||||
|
-- need to recapture as we are opening a modal within another modal
|
||||||
|
self.parent:capture(self.parent)
|
||||||
|
end,
|
||||||
|
eventHandler = function(self, event)
|
||||||
|
if event.type == 'select_cancel' then
|
||||||
|
self:disable()
|
||||||
|
elseif event.type == 'select_file' then
|
||||||
|
self:disable()
|
||||||
|
end
|
||||||
|
return UI.FileSelect.eventHandler(self, event)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
notification = UI.Notification(),
|
||||||
|
statusBar = UI.StatusBar(),
|
||||||
|
},
|
||||||
|
notification = UI.Notification(),
|
||||||
|
accelerators = {
|
||||||
|
r = 'refresh',
|
||||||
|
e = 'edit',
|
||||||
|
f = 'files',
|
||||||
|
s = 'shell',
|
||||||
|
l = 'lua',
|
||||||
|
n = 'network',
|
||||||
|
[ 'control-n' ] = 'new',
|
||||||
|
delete = 'delete',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
applications = Util.readTable('sys/etc/app.db')
|
local function loadApplications()
|
||||||
|
local requirements = {
|
||||||
|
turtle = not not turtle,
|
||||||
|
advancedTurtle = turtle and term.isColor(),
|
||||||
|
advanced = term.isColor(),
|
||||||
|
pocket = not not pocket,
|
||||||
|
advancedPocket = pocket and term.isColor(),
|
||||||
|
advancedComputer = not turtle and not pocket and term.isColor(),
|
||||||
|
neuralInterface = not not device.neuralInterface,
|
||||||
|
}
|
||||||
|
|
||||||
|
applications = Util.readTable('sys/etc/apps.db')
|
||||||
|
|
||||||
|
for dir in pairs(Packages:installed()) do
|
||||||
|
local path = fs.combine('packages/' .. dir, 'etc/apps.db')
|
||||||
|
if fs.exists(path) then
|
||||||
|
local apps = Util.readTable(path) or { }
|
||||||
|
Util.merge(applications, apps)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if fs.exists(REGISTRY_DIR) then
|
if fs.exists(REGISTRY_DIR) then
|
||||||
local files = fs.list(REGISTRY_DIR)
|
local files = fs.list(REGISTRY_DIR)
|
||||||
@@ -59,106 +234,38 @@ local function loadApplications()
|
|||||||
end
|
end
|
||||||
|
|
||||||
if a.requires then
|
if a.requires then
|
||||||
local fn = requirements[a.requires]
|
return requirements[a.requires]
|
||||||
if fn and not fn() then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return true -- Util.startsWidth(a.run, 'http') or shell.resolveProgram(a.run)
|
return true
|
||||||
end)
|
end)
|
||||||
end
|
|
||||||
|
|
||||||
loadApplications()
|
|
||||||
|
|
||||||
local defaultIcon = NFT.parse("\03180\031711\03180\
|
|
||||||
\031800\03171\03180\
|
|
||||||
\03171\031800\03171")
|
|
||||||
|
|
||||||
local sx, sy = term.current().getSize()
|
|
||||||
local maxRecent = math.ceil(sx * sy / 62)
|
|
||||||
|
|
||||||
local function elipse(s, len)
|
|
||||||
if #s > len then
|
|
||||||
s = s:sub(1, len - 2) .. '..'
|
|
||||||
end
|
|
||||||
return s
|
|
||||||
end
|
|
||||||
|
|
||||||
local buttons = { }
|
|
||||||
local categories = { }
|
local categories = { }
|
||||||
|
buttons = { }
|
||||||
for _,f in pairs(applications) do
|
for _,f in pairs(applications) do
|
||||||
if not categories[f.category] then
|
if not categories[f.category] then
|
||||||
categories[f.category] = true
|
categories[f.category] = true
|
||||||
table.insert(buttons, { text = f.category })
|
table.insert(buttons, {
|
||||||
|
text = f.category,
|
||||||
|
width = 8,
|
||||||
|
selected = config.currentCategory == f.category
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.sort(buttons, function(a, b) return a.text < b.text end)
|
table.sort(buttons, function(a, b) return a.text < b.text end)
|
||||||
table.insert(buttons, 1, { text = 'Recent' })
|
table.insert(buttons, 1, { text = 'Recent' })
|
||||||
table.insert(buttons, { text = '+', event = 'new' })
|
|
||||||
|
|
||||||
local function parseIcon(iconText)
|
for k,v in pairs(buttons) do
|
||||||
local icon
|
v.x = 1
|
||||||
|
v.y = k + 1
|
||||||
local s, m = pcall(function()
|
|
||||||
icon = NFT.parse(iconText)
|
|
||||||
if icon then
|
|
||||||
if icon.height > 3 or icon.width > 8 then
|
|
||||||
error('Invalid size')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return icon
|
|
||||||
end)
|
|
||||||
|
|
||||||
if s then
|
|
||||||
return icon
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return s, m
|
page.tabBar.children = { }
|
||||||
end
|
page.tabBar:addButtons(buttons)
|
||||||
|
|
||||||
UI.VerticalTabBar = class(UI.TabBar)
|
--page.tabBar:selectTab(config.currentCategory or 'Apps')
|
||||||
function UI.VerticalTabBar:setParent()
|
page.container:setCategory(config.currentCategory or 'Apps')
|
||||||
self.x = 1
|
|
||||||
self.width = 8
|
|
||||||
self.height = nil
|
|
||||||
self.ey = -1
|
|
||||||
UI.TabBar.setParent(self)
|
|
||||||
for k,c in pairs(self.children) do
|
|
||||||
c.x = 1
|
|
||||||
c.y = k + 1
|
|
||||||
c.ox, c.oy = c.x, c.y
|
|
||||||
c.width = 8
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
local cx = 9
|
|
||||||
local cy = 1
|
|
||||||
if sx < 30 then
|
|
||||||
UI.VerticalTabBar = UI.TabBar
|
|
||||||
cx = 1
|
|
||||||
cy = 2
|
|
||||||
end
|
|
||||||
|
|
||||||
local page = UI.Page {
|
|
||||||
tabBar = UI.VerticalTabBar {
|
|
||||||
buttons = buttons,
|
|
||||||
},
|
|
||||||
container = UI.Viewport {
|
|
||||||
x = cx,
|
|
||||||
y = cy,
|
|
||||||
},
|
|
||||||
notification = UI.Notification(),
|
|
||||||
accelerators = {
|
|
||||||
r = 'refresh',
|
|
||||||
e = 'edit',
|
|
||||||
f = 'files',
|
|
||||||
s = 'shell',
|
|
||||||
l = 'lua',
|
|
||||||
[ 'control-n' ] = 'new',
|
|
||||||
delete = 'delete',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
UI.Icon = class(UI.Window)
|
UI.Icon = class(UI.Window)
|
||||||
UI.Icon.defaults = {
|
UI.Icon.defaults = {
|
||||||
@@ -180,75 +287,79 @@ function UI.Icon:eventHandler(event)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function page.container:setCategory(categoryName, animate)
|
function page.container:setCategory(categoryName, animate)
|
||||||
|
|
||||||
-- reset the viewport window
|
-- reset the viewport window
|
||||||
self.children = { }
|
self.children = { }
|
||||||
self.offy = 0
|
self:reset()
|
||||||
|
|
||||||
local function filter(it, f)
|
local filtered = { }
|
||||||
local ot = { }
|
|
||||||
for _,v in pairs(it) do
|
|
||||||
if f(v) then
|
|
||||||
table.insert(ot, v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ot
|
|
||||||
end
|
|
||||||
|
|
||||||
local filtered
|
|
||||||
|
|
||||||
if categoryName == 'Recent' then
|
if categoryName == 'Recent' then
|
||||||
filtered = { }
|
|
||||||
|
|
||||||
for _,v in ipairs(config.Recent) do
|
for _,v in ipairs(config.Recent) do
|
||||||
local app = Util.find(applications, 'key', v)
|
local app = Util.find(applications, 'key', v)
|
||||||
if app then -- and fs.exists(app.run) then
|
if app then
|
||||||
table.insert(filtered, app)
|
table.insert(filtered, app)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
else
|
else
|
||||||
filtered = filter(applications, function(a)
|
filtered = Array.filter(applications, function(a)
|
||||||
return a.category == categoryName -- and fs.exists(a.run)
|
return a.category == categoryName
|
||||||
end)
|
end)
|
||||||
table.sort(filtered, function(a, b) return a.title < b.title end)
|
table.sort(filtered, function(a, b) return a.title < b.title end)
|
||||||
end
|
end
|
||||||
|
|
||||||
for _,program in ipairs(filtered) do
|
for _,program in ipairs(filtered) do
|
||||||
|
|
||||||
local icon
|
local icon
|
||||||
if program.icon then
|
if extSupport and program.iconExt then
|
||||||
|
icon = parseIcon(program.iconExt)
|
||||||
|
end
|
||||||
|
if not icon and program.icon then
|
||||||
icon = parseIcon(program.icon)
|
icon = parseIcon(program.icon)
|
||||||
end
|
end
|
||||||
if not icon then
|
if not icon then
|
||||||
icon = defaultIcon
|
icon = DEFAULT_ICON
|
||||||
end
|
end
|
||||||
|
|
||||||
local title = elipse(program.title, 8)
|
local title = ellipsis(program.title, 8)
|
||||||
|
|
||||||
local width = math.max(icon.width + 2, #title + 2)
|
local width = math.max(icon.width + 2, #title + 2)
|
||||||
|
if config.listMode then
|
||||||
|
table.insert(self.children, UI.Icon {
|
||||||
|
width = self.width - 2,
|
||||||
|
height = 1,
|
||||||
|
UI.Button {
|
||||||
|
x = 1, ex = -1,
|
||||||
|
text = program.title,
|
||||||
|
centered = false,
|
||||||
|
backgroundColor = self:getProperty('backgroundColor'),
|
||||||
|
backgroundFocusColor = 'gray',
|
||||||
|
textColor = 'white',
|
||||||
|
textFocusColor = 'white',
|
||||||
|
event = 'button',
|
||||||
|
app = program,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else
|
||||||
table.insert(self.children, UI.Icon({
|
table.insert(self.children, UI.Icon({
|
||||||
width = width,
|
width = width,
|
||||||
image = UI.NftImage({
|
image = UI.NftImage({
|
||||||
x = math.floor((width - icon.width) / 2) + 1,
|
x = math.floor((width - icon.width) / 2) + 1,
|
||||||
image = icon,
|
image = icon,
|
||||||
width = 5,
|
|
||||||
height = 3,
|
|
||||||
}),
|
}),
|
||||||
button = UI.Button({
|
button = UI.Button({
|
||||||
x = math.floor((width - #title - 2) / 2) + 1,
|
x = math.floor((width - #title - 2) / 2) + 1,
|
||||||
y = 4,
|
y = 4,
|
||||||
text = title,
|
text = title,
|
||||||
backgroundColor = self.backgroundColor,
|
backgroundColor = self:getProperty('backgroundColor'),
|
||||||
backgroundFocusColor = colors.gray,
|
backgroundFocusColor = 'gray',
|
||||||
textColor = colors.white,
|
textColor = 'white',
|
||||||
textFocusColor = colors.white,
|
textFocusColor = 'white',
|
||||||
width = #title + 2,
|
width = #title + 2,
|
||||||
event = 'button',
|
event = 'button',
|
||||||
app = program,
|
app = program,
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local gutter = 2
|
local gutter = 2
|
||||||
if UI.term.width <= 26 then
|
if UI.term.width <= 26 then
|
||||||
@@ -257,15 +368,16 @@ function page.container:setCategory(categoryName, animate)
|
|||||||
local col, row = gutter, 2
|
local col, row = gutter, 2
|
||||||
local count = #self.children
|
local count = #self.children
|
||||||
|
|
||||||
local r = math.random(1, 5)
|
local r = math.random(1, 7)
|
||||||
|
local frames = 5
|
||||||
-- reposition all children
|
-- reposition all children
|
||||||
for k,child in ipairs(self.children) do
|
for k,child in ipairs(self.children) do
|
||||||
if r == 1 then
|
if r == 1 then
|
||||||
child.x = math.random(1, self.width)
|
child.x = math.random(1, self.width)
|
||||||
child.y = math.random(1, self.height)
|
child.y = math.random(1, self.height - 3)
|
||||||
elseif r == 2 then
|
elseif r == 2 then
|
||||||
child.x = self.width
|
child.x = self.width
|
||||||
child.y = self.height
|
child.y = self.height - 3
|
||||||
elseif r == 3 then
|
elseif r == 3 then
|
||||||
child.x = math.floor(self.width / 2)
|
child.x = math.floor(self.width / 2)
|
||||||
child.y = math.floor(self.height / 2)
|
child.y = math.floor(self.height / 2)
|
||||||
@@ -277,40 +389,44 @@ function page.container:setCategory(categoryName, animate)
|
|||||||
child.y = row
|
child.y = row
|
||||||
if k == #self.children then
|
if k == #self.children then
|
||||||
child.x = self.width
|
child.x = self.width
|
||||||
child.y = self.height
|
child.y = self.height - 3
|
||||||
end
|
end
|
||||||
|
elseif r == 6 then
|
||||||
|
child.x = col
|
||||||
|
child.y = 1
|
||||||
|
elseif r == 7 then
|
||||||
|
child.x = 1
|
||||||
|
child.y = self.height - 3
|
||||||
end
|
end
|
||||||
child.tween = Tween.new(6, child, { x = col, y = row }, 'linear')
|
child.tween = Tween.new(frames, child, { x = col, y = row }, 'inQuad')
|
||||||
|
|
||||||
if not animate then
|
if not animate then
|
||||||
child.x = col
|
child.x = col
|
||||||
child.y = row
|
child.y = row
|
||||||
end
|
end
|
||||||
|
|
||||||
|
self:setViewHeight(row + (config.listMode and 1 or 4))
|
||||||
|
|
||||||
if k < count then
|
if k < count then
|
||||||
col = col + child.width
|
col = col + child.width
|
||||||
if col + self.children[k + 1].width + gutter - 2 > self.width then
|
if col + self.children[k + 1].width + gutter - 2 > self.width then
|
||||||
col = gutter
|
col = gutter
|
||||||
row = row + 5
|
row = row + (config.listMode and 1 or 5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
self:initChildren()
|
self:initChildren()
|
||||||
if animate then -- need to fix transitions under layers
|
if animate then
|
||||||
local function transition(args)
|
local function transition()
|
||||||
local i = 1
|
local i = 1
|
||||||
return function(device)
|
return function()
|
||||||
self:clear()
|
|
||||||
for _,child in pairs(self.children) do
|
for _,child in pairs(self.children) do
|
||||||
child.tween:update(1)
|
child.tween:update(1)
|
||||||
child.x = math.floor(child.x)
|
child:move(math.floor(child.x), math.floor(child.y))
|
||||||
child.y = math.floor(child.y)
|
|
||||||
child:draw()
|
|
||||||
end
|
end
|
||||||
args.canvas:blit(device, args, args)
|
|
||||||
i = i + 1
|
i = i + 1
|
||||||
return i < 7
|
return i <= frames
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
self:addTransition(transition)
|
self:addTransition(transition)
|
||||||
@@ -330,7 +446,6 @@ function page:resize()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function page:eventHandler(event)
|
function page:eventHandler(event)
|
||||||
|
|
||||||
if event.type == 'tab_select' then
|
if event.type == 'tab_select' then
|
||||||
self.container:setCategory(event.button.text, true)
|
self.container:setCategory(event.button.text, true)
|
||||||
self.container:draw()
|
self.container:draw()
|
||||||
@@ -350,30 +465,22 @@ function page:eventHandler(event)
|
|||||||
table.remove(config.Recent, maxRecent + 1)
|
table.remove(config.Recent, maxRecent + 1)
|
||||||
end
|
end
|
||||||
Config.update('Overview', config)
|
Config.update('Overview', config)
|
||||||
multishell.openTab({
|
shell.switchTab(shell.openTab(event.button.app.run))
|
||||||
title = event.button.app.title,
|
|
||||||
path = 'sys/apps/shell',
|
|
||||||
args = { event.button.app.run },
|
|
||||||
focused = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
elseif event.type == 'shell' then
|
elseif event.type == 'shell' then
|
||||||
multishell.openTab({
|
shell.switchTab(shell.openTab('shell'))
|
||||||
path = 'sys/apps/shell',
|
|
||||||
focused = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
elseif event.type == 'lua' then
|
elseif event.type == 'lua' then
|
||||||
multishell.openTab({
|
shell.switchTab(shell.openTab('Lua'))
|
||||||
path = 'sys/apps/Lua.lua',
|
|
||||||
focused = true,
|
|
||||||
})
|
|
||||||
|
|
||||||
elseif event.type == 'files' then
|
elseif event.type == 'files' then
|
||||||
multishell.openTab({
|
shell.switchTab(shell.openTab('Files'))
|
||||||
path = 'sys/apps/Files.lua',
|
|
||||||
focused = true,
|
elseif event.type == 'network' then
|
||||||
})
|
shell.switchTab(shell.openTab('Network'))
|
||||||
|
|
||||||
|
elseif event.type == 'help' then
|
||||||
|
shell.switchTab(shell.openTab('Help Overview'))
|
||||||
|
|
||||||
elseif event.type == 'focus_change' then
|
elseif event.type == 'focus_change' then
|
||||||
if event.focused.parent.UIElement == 'Icon' then
|
if event.focused.parent.UIElement == 'Icon' then
|
||||||
@@ -389,9 +496,13 @@ function page:eventHandler(event)
|
|||||||
elseif event.type == 'delete' then
|
elseif event.type == 'delete' then
|
||||||
local focused = page:getFocused()
|
local focused = page:getFocused()
|
||||||
if focused.app then
|
if focused.app then
|
||||||
|
if focused.app.filename then
|
||||||
|
fs.delete(focused.app.filename)
|
||||||
|
else
|
||||||
focused.app.disabled = true
|
focused.app.disabled = true
|
||||||
local filename = focused.app.filename or fs.combine(REGISTRY_DIR, focused.app.key)
|
local filename = focused.app.filename or fs.combine(REGISTRY_DIR, focused.app.key)
|
||||||
Util.writeTable(filename, focused.app)
|
Util.writeTable(filename, focused.app)
|
||||||
|
end
|
||||||
loadApplications()
|
loadApplications()
|
||||||
page:refresh()
|
page:refresh()
|
||||||
page:draw()
|
page:draw()
|
||||||
@@ -403,150 +514,138 @@ function page:eventHandler(event)
|
|||||||
if config.currentCategory ~= 'Recent' then
|
if config.currentCategory ~= 'Recent' then
|
||||||
category = config.currentCategory or 'Apps'
|
category = config.currentCategory or 'Apps'
|
||||||
end
|
end
|
||||||
UI:setPage('editor', { category = category })
|
self.editor:show({ category = category })
|
||||||
|
|
||||||
|
elseif event.type == 'display_mode' then
|
||||||
|
config.listMode = not config.listMode
|
||||||
|
Config.update('Overview', config)
|
||||||
|
loadApplications()
|
||||||
|
self:refresh()
|
||||||
|
self:draw()
|
||||||
|
|
||||||
elseif event.type == 'edit' then
|
elseif event.type == 'edit' then
|
||||||
local focused = page:getFocused()
|
local focused = page:getFocused()
|
||||||
if focused.app then
|
if focused.app then
|
||||||
UI:setPage('editor', focused.app)
|
self.editor:show(focused.app)
|
||||||
end
|
end
|
||||||
|
|
||||||
else
|
else
|
||||||
UI.Page.eventHandler(self, event)
|
return UI.Page.eventHandler(self, event)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local formWidth = math.max(UI.term.width - 8, 26)
|
function page.editor:show(app)
|
||||||
|
|
||||||
local editor = UI.Dialog {
|
|
||||||
height = 11,
|
|
||||||
width = formWidth,
|
|
||||||
title = 'Edit Application',
|
|
||||||
form = UI.Form {
|
|
||||||
y = 2,
|
|
||||||
height = 9,
|
|
||||||
title = UI.TextEntry {
|
|
||||||
formLabel = 'Title', formKey = 'title', limit = 11, help = 'Application title',
|
|
||||||
required = true,
|
|
||||||
},
|
|
||||||
run = UI.TextEntry {
|
|
||||||
formLabel = 'Run', formKey = 'run', limit = 100, help = 'Full path to application',
|
|
||||||
required = true,
|
|
||||||
},
|
|
||||||
category = UI.TextEntry {
|
|
||||||
formLabel = 'Category', formKey = 'category', limit = 11, help = 'Category of application',
|
|
||||||
required = true,
|
|
||||||
},
|
|
||||||
loadIcon = UI.Button {
|
|
||||||
x = 11, y = 6,
|
|
||||||
text = 'Icon', event = 'loadIcon', help = 'Select icon'
|
|
||||||
},
|
|
||||||
image = UI.NftImage {
|
|
||||||
y = 6, x = 2, height = 3, width = 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
statusBar = UI.StatusBar(),
|
|
||||||
iconFile = '',
|
|
||||||
}
|
|
||||||
|
|
||||||
function editor:enable(app)
|
|
||||||
if app then
|
if app then
|
||||||
self.form:setValues(app)
|
self.form:setValues(app)
|
||||||
|
|
||||||
local icon
|
local icon
|
||||||
if app.icon then
|
if extSupport and app.iconExt then
|
||||||
|
icon = parseIcon(app.iconExt)
|
||||||
|
end
|
||||||
|
if not icon and app.icon then
|
||||||
icon = parseIcon(app.icon)
|
icon = parseIcon(app.icon)
|
||||||
end
|
end
|
||||||
self.form.image:setImage(icon)
|
self.form.image:setImage(icon)
|
||||||
end
|
end
|
||||||
UI.Dialog.enable(self)
|
UI.SlideOut.show(self)
|
||||||
self:focusFirst()
|
self:focusFirst()
|
||||||
end
|
end
|
||||||
|
|
||||||
function editor.form.image:draw()
|
function page.editor:updateApplications(app)
|
||||||
self:clear()
|
|
||||||
UI.NftImage.draw(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function editor:updateApplications(app)
|
|
||||||
if not app.key then
|
if not app.key then
|
||||||
app.key = SHA1.sha1(app.title)
|
app.key = SHA.compute(app.title)
|
||||||
end
|
end
|
||||||
local filename = app.filename or fs.combine(REGISTRY_DIR, app.key)
|
local filename = app.filename or fs.combine(REGISTRY_DIR, app.key)
|
||||||
Util.writeTable(filename, app)
|
Util.writeTable(filename, app)
|
||||||
loadApplications()
|
loadApplications()
|
||||||
end
|
end
|
||||||
|
|
||||||
function editor:eventHandler(event)
|
function page.editor:loadImage(filename)
|
||||||
|
|
||||||
if event.type == 'form_cancel' or event.type == 'cancel' then
|
|
||||||
UI:setPreviousPage()
|
|
||||||
|
|
||||||
elseif event.type == 'focus_change' then
|
|
||||||
self.statusBar:setStatus(event.focused.help or '')
|
|
||||||
self.statusBar:draw()
|
|
||||||
|
|
||||||
elseif event.type == 'loadIcon' then
|
|
||||||
local fileui = FileUI({
|
|
||||||
x = self.x,
|
|
||||||
y = self.y,
|
|
||||||
z = 2,
|
|
||||||
width = self.width,
|
|
||||||
height = self.height,
|
|
||||||
})
|
|
||||||
UI:setPage(fileui, fs.getDir(self.iconFile), function(fileName)
|
|
||||||
if fileName then
|
|
||||||
self.iconFile = fileName
|
|
||||||
local s, m = pcall(function()
|
local s, m = pcall(function()
|
||||||
local iconLines = Util.readFile(fileName)
|
local iconLines = Util.readFile(filename)
|
||||||
if not iconLines then
|
if not iconLines then
|
||||||
error('Unable to load file')
|
error('Must be an NFT image - 3 rows, 8 cols max')
|
||||||
end
|
end
|
||||||
local icon, m = parseIcon(iconLines)
|
local icon, m = parseIcon(iconLines)
|
||||||
if not icon then
|
if not icon then
|
||||||
error(m)
|
error(m)
|
||||||
end
|
end
|
||||||
|
if extSupport then
|
||||||
|
self.form.values.iconExt = iconLines
|
||||||
|
else
|
||||||
self.form.values.icon = iconLines
|
self.form.values.icon = iconLines
|
||||||
|
end
|
||||||
self.form.image:setImage(icon)
|
self.form.image:setImage(icon)
|
||||||
self.form.image:draw()
|
self.form.image:draw()
|
||||||
end)
|
end)
|
||||||
if not s and m then
|
if not s and m then
|
||||||
local msg = m:gsub('.*: (.*)', '%1')
|
local msg = m:gsub('.*: (.*)', '%1')
|
||||||
page.notification:error(msg)
|
self.notification:error(msg)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
function page.editor:eventHandler(event)
|
||||||
|
if event.type == 'form_cancel' or event.type == 'cancel' then
|
||||||
|
self:hide()
|
||||||
|
|
||||||
|
elseif event.type == 'focus_change' then
|
||||||
|
self.statusBar:setStatus(event.focused.help or '')
|
||||||
|
|
||||||
|
elseif event.type == 'editIcon' then
|
||||||
|
local filename = '/tmp/editing.nft'
|
||||||
|
NFT.save(self.form.image.image or TRANS_ICON, filename)
|
||||||
|
local success = shell.run('pain.lua ' .. filename)
|
||||||
|
self.parent:dirty(true)
|
||||||
|
if success then
|
||||||
|
self:loadImage(filename)
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif event.type == 'select_file' then
|
||||||
|
self:loadImage(event.file)
|
||||||
|
|
||||||
|
elseif event.type == 'loadIcon' then
|
||||||
|
self.file_open:show()
|
||||||
|
|
||||||
elseif event.type == 'form_invalid' then
|
elseif event.type == 'form_invalid' then
|
||||||
page.notification:error(event.message)
|
self.notification:error(event.message)
|
||||||
|
|
||||||
elseif event.type == 'form_complete' then
|
elseif event.type == 'form_complete' then
|
||||||
local values = self.form.values
|
local values = self.form.values
|
||||||
UI:setPreviousPage()
|
self:hide()
|
||||||
self:updateApplications(values)
|
self:updateApplications(values)
|
||||||
page:refresh()
|
config.currentCategory = values.category
|
||||||
page:draw()
|
Config.update('Overview', config)
|
||||||
|
os.queueEvent('overview_refresh')
|
||||||
else
|
else
|
||||||
return UI.Dialog.eventHandler(self, event)
|
return UI.SlideOut.eventHandler(self, event)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
UI:setPages({
|
local function reload()
|
||||||
editor = editor,
|
|
||||||
main = page,
|
|
||||||
})
|
|
||||||
|
|
||||||
Event.on('os_register_app', function()
|
|
||||||
loadApplications()
|
loadApplications()
|
||||||
page:refresh()
|
page:refresh()
|
||||||
page:draw()
|
page:draw()
|
||||||
page:sync()
|
page:sync()
|
||||||
|
end
|
||||||
|
|
||||||
|
Event.on('overview_shortcut', function(_, app)
|
||||||
|
if not app.key then
|
||||||
|
app.key = SHA.compute(app.title)
|
||||||
|
end
|
||||||
|
local filename = app.filename or fs.combine(REGISTRY_DIR, app.key)
|
||||||
|
if not fs.exists(filename) then
|
||||||
|
Util.writeTable(filename, app)
|
||||||
|
reload()
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
page.tabBar:selectTab(config.currentCategory or 'Apps')
|
Event.on('overview_refresh', function()
|
||||||
page.container:setCategory(config.currentCategory or 'Apps')
|
reload()
|
||||||
UI:setPage(page)
|
end)
|
||||||
|
|
||||||
UI:pullEvents()
|
loadApplications()
|
||||||
|
|
||||||
|
UI:setPage(page)
|
||||||
|
UI:start()
|
||||||
|
|||||||
210
sys/apps/PackageManager.lua
Normal file
210
sys/apps/PackageManager.lua
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
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 = 'name' },
|
||||||
|
},
|
||||||
|
sortColumn = 'name',
|
||||||
|
autospace = true,
|
||||||
|
help = 'Select a package',
|
||||||
|
},
|
||||||
|
add = UI.Button {
|
||||||
|
x = 2, y = -3,
|
||||||
|
text = ' + ',
|
||||||
|
event = 'action',
|
||||||
|
help = 'Install or update',
|
||||||
|
},
|
||||||
|
remove = UI.Button {
|
||||||
|
x = 8, y = -3,
|
||||||
|
text = ' - ',
|
||||||
|
event = 'action',
|
||||||
|
operation = 'uninstall',
|
||||||
|
operationText = 'Remove',
|
||||||
|
help = 'Remove',
|
||||||
|
},
|
||||||
|
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),
|
||||||
|
name = 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.installed then
|
||||||
|
return colors.yellow
|
||||||
|
end
|
||||||
|
return UI.Grid.getRowTextColor(self, row, selected)
|
||||||
|
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)
|
||||||
|
self.add.operation = selected.installed and 'update' or 'install'
|
||||||
|
self.add.operationText = selected.installed and 'Update' or 'Install'
|
||||||
|
self.remove.inactive = not selected.installed
|
||||||
|
self.add:draw()
|
||||||
|
self.remove:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
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:updateSelection(event.selected)
|
||||||
|
|
||||||
|
elseif event.type == 'checkbox_change' then
|
||||||
|
config.compression = not config.compression
|
||||||
|
Config.update('package', config)
|
||||||
|
|
||||||
|
elseif event.type == 'updateall' then
|
||||||
|
self.operation = 'updateall'
|
||||||
|
self.action.button.text = ' Begin '
|
||||||
|
self.action.button.event = 'begin'
|
||||||
|
self.action.titleBar.title = 'Update All'
|
||||||
|
self.action:show()
|
||||||
|
|
||||||
|
elseif event.type == 'action' then
|
||||||
|
local selected = self.grid:getSelected()
|
||||||
|
if selected then
|
||||||
|
self.operation = event.button.operation
|
||||||
|
self.action.button.text = event.button.operationText
|
||||||
|
self.action.titleBar.title = selected.manifest.title
|
||||||
|
self.action.button.text = ' Begin '
|
||||||
|
self.action.button.event = 'begin'
|
||||||
|
self.action:show()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif event.type == 'hide-action' then
|
||||||
|
self.action:hide()
|
||||||
|
|
||||||
|
elseif event.type == 'begin' then
|
||||||
|
if self.operation == 'updateall' then
|
||||||
|
self:run(self.operation, '')
|
||||||
|
else
|
||||||
|
local selected = self.grid:getSelected()
|
||||||
|
self:run(self.operation, selected.name)
|
||||||
|
selected.installed = Packages:isInstalled(selected.name)
|
||||||
|
|
||||||
|
self:updateSelection(selected)
|
||||||
|
end
|
||||||
|
|
||||||
|
self.action.button.text = ' Done '
|
||||||
|
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
238
sys/apps/Partition.lua
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
local Ansi = require('opus.ansi')
|
||||||
|
local Event = require('opus.event')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
|
local fs = _G.fs
|
||||||
|
local peripheral = _G.peripheral
|
||||||
|
|
||||||
|
local source, target
|
||||||
|
|
||||||
|
local function getDriveInfo(tgt)
|
||||||
|
local total = 0
|
||||||
|
local throttle = Util.throttle()
|
||||||
|
|
||||||
|
tgt = fs.combine(tgt, '')
|
||||||
|
local src = fs.getNode(tgt).source or tgt
|
||||||
|
|
||||||
|
local function recurse(path)
|
||||||
|
throttle()
|
||||||
|
if fs.isDir(path) then
|
||||||
|
if path ~= src then
|
||||||
|
total = total + 500
|
||||||
|
end
|
||||||
|
for _, v in pairs(fs.native.list(path)) do
|
||||||
|
recurse(fs.combine(path, v))
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local sz = fs.getSize(path)
|
||||||
|
total = total + math.max(500, sz)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
recurse(src)
|
||||||
|
|
||||||
|
local drive = fs.getDrive(src)
|
||||||
|
return {
|
||||||
|
path = tgt,
|
||||||
|
drive = drive,
|
||||||
|
type = peripheral.getType(drive) or drive,
|
||||||
|
used = total,
|
||||||
|
free = fs.getFreeSpace(src),
|
||||||
|
mountPoint = src,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getDrives(exclude)
|
||||||
|
local drives = { }
|
||||||
|
|
||||||
|
for _, path in pairs(fs.native.list('/')) do
|
||||||
|
local side = fs.getDrive(path)
|
||||||
|
if side and not drives[side] and not fs.isReadOnly(path) and side ~= exclude then
|
||||||
|
if side == 'hdd' then
|
||||||
|
path = ''
|
||||||
|
end
|
||||||
|
drives[side] = getDriveInfo(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return drives
|
||||||
|
end
|
||||||
|
|
||||||
|
local page = UI.Page {
|
||||||
|
wizard = UI.Wizard {
|
||||||
|
ey = -2,
|
||||||
|
partitions = UI.WizardPage {
|
||||||
|
index = 1,
|
||||||
|
info = UI.TextArea {
|
||||||
|
x = 3, y = 2, ex = -3, ey = 5,
|
||||||
|
value = [[Move the contents of a directory to another disk. A link will be created to point to that location.]]
|
||||||
|
},
|
||||||
|
grid = UI.Grid {
|
||||||
|
x = 2, y = 7, ex = -2, ey = -2,
|
||||||
|
columns = {
|
||||||
|
{ heading = 'Path', key = 'path', textColor = 'yellow', width = 10 },
|
||||||
|
{ heading = 'Mount Point', key = 'mountPoint' },
|
||||||
|
{ heading = 'Used', key = 'used', width = 6 },
|
||||||
|
},
|
||||||
|
sortColumn = 'path',
|
||||||
|
getDisplayValues = function (_, row)
|
||||||
|
row = Util.shallowCopy(row)
|
||||||
|
row.used = Util.toBytes(row.used)
|
||||||
|
return row
|
||||||
|
end,
|
||||||
|
enable = function(self)
|
||||||
|
Event.onTimeout(0, function()
|
||||||
|
local mounts = {
|
||||||
|
usr = getDriveInfo('usr/config'),
|
||||||
|
packages = getDriveInfo('packages'),
|
||||||
|
}
|
||||||
|
self:setValues(mounts)
|
||||||
|
self:draw()
|
||||||
|
self:sync()
|
||||||
|
end)
|
||||||
|
self:setValues({ })
|
||||||
|
UI.Grid.enable(self)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
validate = function(self)
|
||||||
|
target = self.grid:getSelected()
|
||||||
|
return not not target
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
mounts = UI.WizardPage {
|
||||||
|
index = 2,
|
||||||
|
info = UI.TextArea {
|
||||||
|
x = 3, y = 2, ex = -3, ey = 5,
|
||||||
|
value = [[Select the target disk. Labeled computers can be inserted into disk drives for larger volumes.]]
|
||||||
|
},
|
||||||
|
grid = UI.Grid {
|
||||||
|
x = 2, y = 7, ex = -2, ey = -2,
|
||||||
|
columns = {
|
||||||
|
{ heading = 'Path', key = 'path', textColor = 'yellow', width = 10 },
|
||||||
|
{ heading = 'Type', key = 'type' },
|
||||||
|
{ heading = 'Side', key = 'drive' },
|
||||||
|
{ heading = 'Free', key = 'free', width = 6 },
|
||||||
|
},
|
||||||
|
sortColumn = 'path',
|
||||||
|
getDisplayValues = function (_, row)
|
||||||
|
row = Util.shallowCopy(row)
|
||||||
|
row.free = Util.toBytes(row.free)
|
||||||
|
return row
|
||||||
|
end,
|
||||||
|
getRowTextColor = function(self, row)
|
||||||
|
if row.free < target.used then
|
||||||
|
return 'lightGray'
|
||||||
|
end
|
||||||
|
return UI.Grid.getRowTextColor(self, row)
|
||||||
|
end,
|
||||||
|
enable = function(self)
|
||||||
|
Event.on({ 'disk', 'disk_eject', 'partition_update' }, function()
|
||||||
|
self:setValues(getDrives(target.drive))
|
||||||
|
self:draw()
|
||||||
|
self:sync()
|
||||||
|
end)
|
||||||
|
os.queueEvent('partition_update')
|
||||||
|
self:setValues({ })
|
||||||
|
UI.Grid.enable(self)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
validate = function(self)
|
||||||
|
source = self.grid:getSelected()
|
||||||
|
if not source then
|
||||||
|
self:emit({ type = 'notify', message = 'No drive selected' })
|
||||||
|
elseif source.free < target.used then
|
||||||
|
self:emit({ type = 'notify', message = 'Insufficient disk space' })
|
||||||
|
else
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
confirm = UI.WizardPage {
|
||||||
|
index = 3,
|
||||||
|
info = UI.TextArea {
|
||||||
|
x = 2, y = 2, ex = -2, ey = -2,
|
||||||
|
marginTop = 1, marginLeft = 1,
|
||||||
|
backgroundColor = 'black',
|
||||||
|
},
|
||||||
|
enable = function(self)
|
||||||
|
local fstab = Util.readFile('usr/etc/fstab')
|
||||||
|
local lines = { }
|
||||||
|
table.insert(lines, string.format('%sReview changes%s\n', Ansi.yellow, Ansi.reset))
|
||||||
|
if fstab then
|
||||||
|
for _,l in ipairs(Util.split(fstab)) do
|
||||||
|
l = Util.trim(l)
|
||||||
|
if #l > 0 and l:sub(1, 1) ~= '#' then
|
||||||
|
local m = Util.matches(l)
|
||||||
|
if m and m[1] and m[1] == target.path then
|
||||||
|
table.insert(lines, string.format('Removed from usr/etc/fstab:\n%s%s%s\n', Ansi.red, l, Ansi.reset))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local t = target.path
|
||||||
|
local s = fs.combine(source.path .. '/' .. target.path, '')
|
||||||
|
if t ~= s then
|
||||||
|
table.insert(lines, string.format('Added to usr/etc/fstab:\n%s%s linkfs %s%s\n', Ansi.green, t, s, Ansi.reset))
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(lines, string.format('Move directory:\n%s/%s -> /%s', Ansi.green, target.mountPoint, s))
|
||||||
|
|
||||||
|
self.info:setText(table.concat(lines, '\n'))
|
||||||
|
UI.WizardPage.enable(self)
|
||||||
|
end,
|
||||||
|
validate = function(self)
|
||||||
|
if self.changesApplied then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local fstab = Util.readFile('usr/etc/fstab')
|
||||||
|
local lines = { }
|
||||||
|
if fstab then
|
||||||
|
for _,l in ipairs(Util.split(fstab)) do
|
||||||
|
table.insert(lines, l)
|
||||||
|
l = Util.trim(l)
|
||||||
|
if #l > 0 and l:sub(1, 1) ~= '#' then
|
||||||
|
local m = Util.matches(l)
|
||||||
|
if m and m[1] and m[1] == target.path then
|
||||||
|
fs.unmount(m[1])
|
||||||
|
table.remove(lines)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local t = target.path
|
||||||
|
local s = fs.combine(source.path .. '/' .. target.path, '')
|
||||||
|
|
||||||
|
fs.move('/' .. target.mountPoint, '/' .. s)
|
||||||
|
|
||||||
|
if t ~= s then
|
||||||
|
table.insert(lines, string.format('%s linkfs %s', t, s))
|
||||||
|
fs.mount(t, 'linkfs', s)
|
||||||
|
end
|
||||||
|
|
||||||
|
Util.writeFile('usr/etc/fstab', table.concat(lines, '\n'))
|
||||||
|
|
||||||
|
self.parent.nextButton.text = 'Exit'
|
||||||
|
self.parent.cancelButton:disable()
|
||||||
|
self.parent.previousButton:disable()
|
||||||
|
|
||||||
|
self.changesApplied = true
|
||||||
|
self.info:setValue('Changes have been applied')
|
||||||
|
self.parent:draw()
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
notification = UI.Notification { },
|
||||||
|
eventHandler = function(self, event)
|
||||||
|
if event.type == 'notify' then
|
||||||
|
self.notification:error(event.message)
|
||||||
|
elseif event.type == 'accept' or event.type == 'cancel' then
|
||||||
|
UI:quit()
|
||||||
|
end
|
||||||
|
return UI.Page.eventHandler(self, event)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
UI:disableEffects()
|
||||||
|
UI:setPage(page)
|
||||||
|
UI:start()
|
||||||
28
sys/apps/ShellLauncher.lua
Normal file
28
sys/apps/ShellLauncher.lua
Normal 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
388
sys/apps/Sniff.lua
Normal 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()
|
||||||
@@ -1,324 +1,82 @@
|
|||||||
_G.requireInjector()
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
local Config = require('config')
|
|
||||||
local Security = require('security')
|
|
||||||
local SHA1 = require('sha1')
|
|
||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local fs = _G.fs
|
local fs = _G.fs
|
||||||
local multishell = _ENV.multishell
|
|
||||||
local os = _G.os
|
|
||||||
local settings = _G.settings
|
|
||||||
local shell = _ENV.shell
|
local shell = _ENV.shell
|
||||||
local turtle = _G.turtle
|
|
||||||
|
|
||||||
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 = _ENV.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])
|
||||||
|
local plugins = loadDirectory(fs.combine(programDir, 'system'), { })
|
||||||
|
|
||||||
|
local page = UI.Page {
|
||||||
tabs = UI.Tabs {
|
tabs = UI.Tabs {
|
||||||
pathTab = UI.Window {
|
settings = UI.Tab {
|
||||||
tabTitle = 'Path',
|
title = 'Category',
|
||||||
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,
|
|
||||||
disableHeader = true,
|
|
||||||
columns = { { key = 'value' } },
|
|
||||||
autospace = true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
aliasTab = UI.Window {
|
|
||||||
tabTitle = 'Alias',
|
|
||||||
alias = UI.TextEntry {
|
|
||||||
x = 2, y = 2, ex = -2,
|
|
||||||
limit = 32,
|
|
||||||
shadowText = 'Alias',
|
|
||||||
},
|
|
||||||
path = UI.TextEntry {
|
|
||||||
y = 3, x = 2, ex = -2,
|
|
||||||
limit = 256,
|
|
||||||
shadowText = 'Program path',
|
|
||||||
accelerators = {
|
|
||||||
enter = 'new_alias',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid = UI.Grid {
|
|
||||||
y = 5,
|
|
||||||
sortColumn = 'alias',
|
|
||||||
columns = {
|
|
||||||
{ heading = 'Alias', key = 'alias' },
|
|
||||||
{ heading = 'Program', key = 'path' },
|
|
||||||
},
|
|
||||||
accelerators = {
|
|
||||||
delete = 'delete_alias',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
passwordTab = UI.Window {
|
|
||||||
tabTitle = 'Password',
|
|
||||||
oldPass = UI.TextEntry {
|
|
||||||
x = 2, y = 2, ex = -2,
|
|
||||||
limit = 32,
|
|
||||||
mask = true,
|
|
||||||
shadowText = 'old password',
|
|
||||||
inactive = not Security.getPassword(),
|
|
||||||
},
|
|
||||||
newPass = UI.TextEntry {
|
|
||||||
y = 3, x = 2, ex = -2,
|
|
||||||
limit = 32,
|
|
||||||
mask = true,
|
|
||||||
shadowText = 'new password',
|
|
||||||
accelerators = {
|
|
||||||
enter = 'new_password',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
button = UI.Button {
|
|
||||||
x = 2, y = 5,
|
|
||||||
text = 'Update',
|
|
||||||
event = 'update_password',
|
|
||||||
},
|
|
||||||
info = UI.TextArea {
|
|
||||||
x = 2, ex = -2,
|
|
||||||
y = 7,
|
|
||||||
inactive = true,
|
|
||||||
value = 'Add a password to enable other computers to connect to this one.',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
infoTab = UI.Window {
|
|
||||||
tabTitle = 'Info',
|
|
||||||
labelText = UI.Text {
|
|
||||||
x = 3, y = 2,
|
|
||||||
value = 'Label'
|
|
||||||
},
|
|
||||||
label = UI.TextEntry {
|
|
||||||
x = 9, y = 2, ex = -4,
|
|
||||||
limit = 32,
|
|
||||||
value = os.getComputerLabel(),
|
|
||||||
accelerators = {
|
|
||||||
enter = 'update_label',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid = UI.ScrollingGrid {
|
grid = UI.ScrollingGrid {
|
||||||
y = 3,
|
x = 2, y = 2, ex = -2, ey = -2,
|
||||||
values = {
|
|
||||||
{ name = '', value = '' },
|
|
||||||
{ name = 'CC version', value = Util.getVersion() },
|
|
||||||
{ name = 'Lua version', value = _VERSION },
|
|
||||||
{ name = 'MC version', value = Util.getMinecraftVersion() },
|
|
||||||
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
|
|
||||||
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
|
|
||||||
{ name = 'Day', value = tostring(os.day()) },
|
|
||||||
},
|
|
||||||
inactive = true,
|
|
||||||
columns = {
|
columns = {
|
||||||
{ key = 'name', width = 12 },
|
{ heading = 'Name', key = 'name' },
|
||||||
{ key = 'value' },
|
{ heading = 'Description', key = 'description' },
|
||||||
},
|
},
|
||||||
|
sortColumn = 'name',
|
||||||
|
autospace = true,
|
||||||
|
values = plugins,
|
||||||
},
|
},
|
||||||
|
accelerators = {
|
||||||
|
grid_select = 'category_select',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
notification = UI.Notification(),
|
notification = UI.Notification(),
|
||||||
accelerators = {
|
accelerators = {
|
||||||
q = 'quit',
|
[ 'control-q' ] = 'quit',
|
||||||
},
|
},
|
||||||
}
|
eventHandler = function(self, event)
|
||||||
|
|
||||||
if turtle then
|
|
||||||
local Home = require('turtle.home')
|
|
||||||
|
|
||||||
local values = { }
|
|
||||||
Config.load('gps', values.home or { })
|
|
||||||
|
|
||||||
systemPage.tabs:add({
|
|
||||||
gpsTab = UI.Window {
|
|
||||||
tabTitle = 'GPS',
|
|
||||||
labelText = UI.Text {
|
|
||||||
x = 3, y = 2,
|
|
||||||
value = 'On restart, return to this location'
|
|
||||||
},
|
|
||||||
grid = UI.Grid {
|
|
||||||
x = 3, ex = -3, y = 4,
|
|
||||||
height = 2,
|
|
||||||
values = values,
|
|
||||||
inactive = true,
|
|
||||||
columns = {
|
|
||||||
{ heading = 'x', key = 'x' },
|
|
||||||
{ heading = 'y', key = 'y' },
|
|
||||||
{ heading = 'z', key = 'z' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
button1 = UI.Button {
|
|
||||||
x = 3, y = 7,
|
|
||||||
text = 'Set home',
|
|
||||||
event = 'gps_set',
|
|
||||||
},
|
|
||||||
button2 = UI.Button {
|
|
||||||
ex = -3, y = 7, width = 7,
|
|
||||||
text = 'Clear',
|
|
||||||
event = 'gps_clear',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
function systemPage.tabs.gpsTab:eventHandler(event)
|
|
||||||
if event.type == 'gps_set' then
|
|
||||||
systemPage.notification:info('Determining location', 10)
|
|
||||||
systemPage:sync()
|
|
||||||
if Home.set() then
|
|
||||||
Config.load('gps', values)
|
|
||||||
self.grid:setValues(values.home or { })
|
|
||||||
self.grid:draw()
|
|
||||||
systemPage.notification:success('Location set')
|
|
||||||
else
|
|
||||||
systemPage.notification:error('Unable to determine location')
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
elseif event.type == 'gps_clear' then
|
|
||||||
fs.delete('usr/config/gps')
|
|
||||||
self.grid:setValues({ })
|
|
||||||
self.grid:draw()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if settings then
|
|
||||||
local values = { }
|
|
||||||
for _,v in pairs(settings.getNames()) do
|
|
||||||
table.insert(values, {
|
|
||||||
name = v,
|
|
||||||
value = not not settings.get(v),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
systemPage.tabs:add({
|
|
||||||
settingsTab = UI.Window {
|
|
||||||
tabTitle = 'Settings',
|
|
||||||
grid = UI.Grid {
|
|
||||||
y = 1,
|
|
||||||
values = values,
|
|
||||||
autospace = true,
|
|
||||||
sortColumn = 'name',
|
|
||||||
columns = {
|
|
||||||
{ heading = 'Setting', key = 'name' },
|
|
||||||
{ heading = 'Value', key = 'value' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
function systemPage.tabs.settingsTab:eventHandler(event)
|
|
||||||
if event.type == 'grid_select' then
|
|
||||||
event.selected.value = not event.selected.value
|
|
||||||
settings.set(event.selected.name, event.selected.value)
|
|
||||||
settings.save('.settings')
|
|
||||||
self.grid:draw()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function systemPage.tabs.pathTab.grid:draw()
|
|
||||||
self.values = { }
|
|
||||||
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 = { }
|
|
||||||
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.passwordTab:eventHandler(event)
|
|
||||||
if event.type == 'update_password' then
|
|
||||||
if #self.newPass.value == 0 then
|
|
||||||
systemPage.notification:error('Invalid password')
|
|
||||||
elseif Security.getPassword() and not Security.verifyPassword(SHA1.sha1(self.oldPass.value)) then
|
|
||||||
systemPage.notification:error('Passwords do not match')
|
|
||||||
else
|
|
||||||
Security.updatePassword(SHA1.sha1(self.newPass.value))
|
|
||||||
self.oldPass.inactive = false
|
|
||||||
systemPage.notification:success('Password updated')
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
if event.type == 'quit' then
|
||||||
UI:exitPullEvents()
|
UI:quit()
|
||||||
|
|
||||||
|
elseif event.type == 'category_select' then
|
||||||
|
local tab = event.selected.tab
|
||||||
|
|
||||||
|
if not self.tabs[tab.title] then
|
||||||
|
self.tabs:add({ [ tab.title ] = tab })
|
||||||
|
end
|
||||||
|
self.tabs:selectTab(tab)
|
||||||
|
return true
|
||||||
|
|
||||||
|
elseif event.type == 'success_message' then
|
||||||
|
self.notification:success(event.message)
|
||||||
|
|
||||||
|
elseif event.type == 'info_message' then
|
||||||
|
self.notification:info(event.message)
|
||||||
|
|
||||||
|
elseif event.type == 'error_message' then
|
||||||
|
self.notification:error(event.message)
|
||||||
|
|
||||||
elseif event.type == 'tab_activate' then
|
elseif event.type == 'tab_activate' then
|
||||||
event.activated:focusFirst()
|
event.activated:focusFirst()
|
||||||
|
|
||||||
else
|
else
|
||||||
return UI.Page.eventHandler(self, event)
|
return UI.Page.eventHandler(self, event)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
UI:setPage(systemPage)
|
UI:setPage(page)
|
||||||
UI:pullEvents()
|
UI:start()
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
_G.requireInjector()
|
|
||||||
|
|
||||||
local Event = require('event')
|
|
||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local multishell = _ENV.multishell
|
|
||||||
|
|
||||||
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
75
sys/apps/Tasks.lua
Normal 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
54
sys/apps/Version.lua
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
local Config = require('opus.config')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
|
||||||
|
local shell = _ENV.shell
|
||||||
|
|
||||||
|
local config = Config.load('version')
|
||||||
|
if not config.current then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
UI:setPage(UI.Page {
|
||||||
|
UI.Text {
|
||||||
|
x = 2, y = 2, ex = -2,
|
||||||
|
align = 'center',
|
||||||
|
value = 'Opus has been updated.',
|
||||||
|
textColor = 'yellow',
|
||||||
|
},
|
||||||
|
UI.TextArea {
|
||||||
|
x = 2, y = 4, ey = -8,
|
||||||
|
value = config.details,
|
||||||
|
},
|
||||||
|
UI.Button {
|
||||||
|
x = 2, y = -6, width = 21,
|
||||||
|
event = 'skip',
|
||||||
|
text = 'Skip this version',
|
||||||
|
},
|
||||||
|
UI.Button {
|
||||||
|
x = 2, y = -4, width = 21,
|
||||||
|
event = 'remind',
|
||||||
|
text = 'Remind me tomorrow',
|
||||||
|
},
|
||||||
|
UI.Button {
|
||||||
|
x = 2, y = -2, width = 21,
|
||||||
|
event = 'update',
|
||||||
|
text = 'Update'
|
||||||
|
},
|
||||||
|
eventHandler = function(self, event)
|
||||||
|
if event.type == 'skip' then
|
||||||
|
config.skip = config.current
|
||||||
|
Config.update('version', config)
|
||||||
|
UI:quit()
|
||||||
|
|
||||||
|
elseif event.type == 'remind' then
|
||||||
|
UI:quit()
|
||||||
|
|
||||||
|
elseif event.type == 'update' then
|
||||||
|
shell.openForegroundTab('update update')
|
||||||
|
UI:quit()
|
||||||
|
end
|
||||||
|
return UI.Page.eventHandler(self, event)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
UI:start()
|
||||||
141
sys/apps/Welcome.lua
Normal file
141
sys/apps/Welcome.lua
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
local Ansi = require('opus.ansi')
|
||||||
|
local Security = require('opus.security')
|
||||||
|
local SHA = require('opus.crypto.sha2')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
|
||||||
|
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 = [[Setup Complete
|
||||||
|
|
||||||
|
%sOpen the package manager to add software to this computer.]]
|
||||||
|
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://github.com/kepler155c/opus]]
|
||||||
|
|
||||||
|
local page = UI.Page {
|
||||||
|
wizard = UI.Wizard {
|
||||||
|
ey = -2,
|
||||||
|
splash = UI.WizardPage {
|
||||||
|
index = 1,
|
||||||
|
intro = UI.TextArea {
|
||||||
|
textColor = colors.yellow,
|
||||||
|
inactive = true,
|
||||||
|
x = 3, ex = -3, y = 2, ey = -2,
|
||||||
|
value = string.format(splashIntro, Ansi.white),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
button = UI.Button {
|
||||||
|
x = 3, y = -3,
|
||||||
|
text = 'Open Package Manager',
|
||||||
|
event = 'packages',
|
||||||
|
},
|
||||||
|
intro = UI.TextArea {
|
||||||
|
textColor = colors.yellow,
|
||||||
|
inactive = true,
|
||||||
|
x = 3, ex = -3, y = 2, ey = -4,
|
||||||
|
value = string.format(packagesIntro, Ansi.white),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
contributors = UI.WizardPage {
|
||||||
|
index = 5,
|
||||||
|
intro = UI.TextArea {
|
||||||
|
textColor = colors.yellow,
|
||||||
|
inactive = true,
|
||||||
|
x = 3, ex = -3, y = 2, ey = -2,
|
||||||
|
value = string.format(contributorsIntro, Ansi.white, Ansi.yellow, Ansi.white),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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
70
sys/apps/autorun.lua
Normal 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
38
sys/apps/cedit.lua
Normal 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
24
sys/apps/compat.lua
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
|
-- some programs expect to be run in the global scope
|
||||||
|
-- ie. busted, moonscript
|
||||||
|
|
||||||
|
-- create a new environment mimicing pure lua
|
||||||
|
|
||||||
|
local fs = _G.fs
|
||||||
|
local shell = _ENV.shell
|
||||||
|
|
||||||
|
local env = Util.shallowCopy(_G)
|
||||||
|
Util.merge(env, _ENV)
|
||||||
|
env._G = env
|
||||||
|
|
||||||
|
env.arg = { ... }
|
||||||
|
env.arg[0] = shell.resolveProgram(table.remove(env.arg, 1) or error('file name is required'))
|
||||||
|
|
||||||
|
_G.requireInjector(env, fs.getDir(env.arg[0]))
|
||||||
|
|
||||||
|
local s, m = Util.run(env, env.arg[0], table.unpack(env.arg))
|
||||||
|
|
||||||
|
if not s then
|
||||||
|
error(m, -1)
|
||||||
|
end
|
||||||
23
sys/apps/cshell.lua
Normal file
23
sys/apps/cshell.lua
Normal 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
39
sys/apps/fileui.lua
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
|
local shell = _ENV.shell
|
||||||
|
local multishell = _ENV.multishell
|
||||||
|
|
||||||
|
-- fileui [--path=path] [--exec=filename] [--title=title]
|
||||||
|
|
||||||
|
local page = UI.Page {
|
||||||
|
fileselect = UI.FileSelect { },
|
||||||
|
eventHandler = function(self, event)
|
||||||
|
if event.type == 'select_file' then
|
||||||
|
self.selected = event.file
|
||||||
|
UI:quit()
|
||||||
|
|
||||||
|
elseif event.type == 'select_cancel' then
|
||||||
|
UI:quit()
|
||||||
|
end
|
||||||
|
|
||||||
|
return UI.Page.eventHandler(self, event)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local _, args = Util.parse(...)
|
||||||
|
|
||||||
|
if args.title and multishell then
|
||||||
|
multishell.setTitle(multishell.getCurrent(), args.title)
|
||||||
|
end
|
||||||
|
|
||||||
|
UI:setPage(page, args.path)
|
||||||
|
UI:start()
|
||||||
|
UI.term:setCursorBlink(false)
|
||||||
|
|
||||||
|
if args.exec and page.selected then
|
||||||
|
shell.openForegroundTab(string.format('%s %s', args.exec, page.selected))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
return page.selected
|
||||||
22
sys/apps/genotp.lua
Normal file
22
sys/apps/genotp.lua
Normal 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
195
sys/apps/inspect.lua
Normal 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()
|
||||||
@@ -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))
|
||||||
|
|||||||
@@ -1,616 +0,0 @@
|
|||||||
local sandboxEnv = { }
|
|
||||||
for k,v in pairs(_ENV) do
|
|
||||||
sandboxEnv[k] = v
|
|
||||||
end
|
|
||||||
|
|
||||||
_G.requireInjector()
|
|
||||||
|
|
||||||
local Config = require('config')
|
|
||||||
local Input = require('input')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local colors = _G.colors
|
|
||||||
local fs = _G.fs
|
|
||||||
local keys = _G.keys
|
|
||||||
local multishell = _ENV.multishell
|
|
||||||
local os = _G.os
|
|
||||||
local printError = _G.printError
|
|
||||||
local shell = _ENV.shell
|
|
||||||
local term = _G.term
|
|
||||||
local window = _G.window
|
|
||||||
|
|
||||||
local parentTerm = term.current()
|
|
||||||
local w,h = parentTerm.getSize()
|
|
||||||
local tabs = { }
|
|
||||||
local currentTab
|
|
||||||
local _tabId = 0
|
|
||||||
local overviewId
|
|
||||||
local runningTab
|
|
||||||
local tabsDirty = false
|
|
||||||
local closeInd = '*'
|
|
||||||
local hooks = { }
|
|
||||||
local hotkeys = { }
|
|
||||||
local downState = { }
|
|
||||||
|
|
||||||
multishell.term = term.current()
|
|
||||||
|
|
||||||
-- Default label
|
|
||||||
if not os.getComputerLabel() then
|
|
||||||
local id = os.getComputerID()
|
|
||||||
if _G.turtle then
|
|
||||||
os.setComputerLabel('turtle_' .. id)
|
|
||||||
elseif _G.pocket then
|
|
||||||
os.setComputerLabel('pocket_' .. id)
|
|
||||||
elseif _G.commands then
|
|
||||||
os.setComputerLabel('command_' .. id)
|
|
||||||
else
|
|
||||||
os.setComputerLabel('computer_' .. id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if Util.getVersion() >= 1.76 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_redraw')
|
|
||||||
tabsDirty = 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 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 = tabs[overviewId]
|
|
||||||
end
|
|
||||||
|
|
||||||
if currentTab and currentTab ~= tab then
|
|
||||||
currentTab.window.setVisible(false)
|
|
||||||
if coroutine.status(currentTab.co) == 'suspended' then
|
|
||||||
-- the process that opens a new tab won't get the lose focus event
|
|
||||||
-- os.queueEvent('multishell_notifyfocus', currentTab.tabId)
|
|
||||||
--resumeTab(currentTab, 'multishell_losefocus')
|
|
||||||
end
|
|
||||||
if tab and not currentTab.hidden then
|
|
||||||
tab.previousTabId = currentTab.tabId
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if tab ~= currentTab then
|
|
||||||
currentTab = tab
|
|
||||||
tab.window.setVisible(true)
|
|
||||||
resumeTab(tab, 'multishell_focus')
|
|
||||||
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 sandboxEnv)
|
|
||||||
|
|
||||||
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
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
tabs[tab.tabId] = nil
|
|
||||||
if tab == currentTab then
|
|
||||||
local previousTab
|
|
||||||
if tab.previousTabId then
|
|
||||||
previousTab = tabs[tab.previousTabId]
|
|
||||||
if previousTab and previousTab.hidden then
|
|
||||||
previousTab = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
selectTab(previousTab)
|
|
||||||
end
|
|
||||||
redrawMenu()
|
|
||||||
end)
|
|
||||||
|
|
||||||
tabs[tab.tabId] = tab
|
|
||||||
resumeTab(tab)
|
|
||||||
return tab
|
|
||||||
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, title)
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if tab then
|
|
||||||
if not tab.isOverview then
|
|
||||||
tab.title = title or ''
|
|
||||||
end
|
|
||||||
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)
|
|
||||||
os.queueEvent('multishell_terminate', tabId)
|
|
||||||
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
|
|
||||||
|
|
||||||
return tab.tabId
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.hideTab(tabId)
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if tab then
|
|
||||||
tab.hidden = true
|
|
||||||
if currentTab.tabId == tabId then
|
|
||||||
selectTab(tabs[currentTab.previousTabId])
|
|
||||||
end
|
|
||||||
redrawMenu()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.unhideTab(tabId)
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if tab then
|
|
||||||
tab.hidden = false
|
|
||||||
redrawMenu()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.getCount()
|
|
||||||
return Util.size(tabs)
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.hook(event, fn)
|
|
||||||
if type(event) == 'table' then
|
|
||||||
for _,v in pairs(event) do
|
|
||||||
multishell.hook(v, fn)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
if not hooks[event] then
|
|
||||||
hooks[event] = { }
|
|
||||||
end
|
|
||||||
table.insert(hooks[event], fn)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- you can only unhook from within the function that hooked
|
|
||||||
function multishell.unhook(event, fn)
|
|
||||||
local eventHooks = hooks[event]
|
|
||||||
if eventHooks then
|
|
||||||
Util.removeByValue(eventHooks, fn)
|
|
||||||
if #eventHooks == 0 then
|
|
||||||
hooks[event] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
multishell.hook('multishell_terminate', function(_, eventData)
|
|
||||||
local tabId = eventData[1] or -1
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
|
|
||||||
if tab and not tab.isOverview then
|
|
||||||
if coroutine.status(tab.co) ~= 'dead' then
|
|
||||||
resumeTab(tab, "terminate")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end)
|
|
||||||
|
|
||||||
multishell.hook('multishell_redraw', function()
|
|
||||||
tabsDirty = false
|
|
||||||
|
|
||||||
local function write(x, text, bg, fg)
|
|
||||||
parentTerm.setBackgroundColor(bg)
|
|
||||||
parentTerm.setTextColor(fg)
|
|
||||||
parentTerm.setCursorPos(x, 1)
|
|
||||||
parentTerm.write(text)
|
|
||||||
end
|
|
||||||
|
|
||||||
local bg = _colors.tabBarBackgroundColor
|
|
||||||
parentTerm.setBackgroundColor(bg)
|
|
||||||
parentTerm.setCursorPos(1, 1)
|
|
||||||
parentTerm.clearLine()
|
|
||||||
|
|
||||||
local function compareTab(a, b)
|
|
||||||
return a.tabId < b.tabId
|
|
||||||
end
|
|
||||||
|
|
||||||
for _,tab in pairs(tabs) do
|
|
||||||
if tab.hidden and tab ~= currentTab then
|
|
||||||
tab.width = 0
|
|
||||||
else
|
|
||||||
tab.width = #tab.title + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function width()
|
|
||||||
local tw = 0
|
|
||||||
Util.each(tabs, function(t) tw = tw + t.width end)
|
|
||||||
return tw
|
|
||||||
end
|
|
||||||
|
|
||||||
while width() > w - 3 do
|
|
||||||
local tab = select(2,
|
|
||||||
Util.spairs(tabs, function(a, b) return a.width > b.width end)())
|
|
||||||
tab.width = tab.width - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local tabX = 0
|
|
||||||
for _,tab in Util.spairs(tabs, compareTab) do
|
|
||||||
if tab.width > 0 then
|
|
||||||
tab.sx = tabX + 1
|
|
||||||
tab.ex = tabX + tab.width
|
|
||||||
tabX = tabX + tab.width
|
|
||||||
if tab ~= currentTab then
|
|
||||||
write(tab.sx, tab.title:sub(1, tab.width - 1),
|
|
||||||
_colors.backgroundColor, _colors.textColor)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if currentTab then
|
|
||||||
write(currentTab.sx - 1,
|
|
||||||
' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ',
|
|
||||||
_colors.focusBackgroundColor, _colors.focusTextColor)
|
|
||||||
if not currentTab.isOverview then
|
|
||||||
write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if currentTab then
|
|
||||||
currentTab.window.restoreCursor()
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
|
||||||
end)
|
|
||||||
|
|
||||||
multishell.hook('term_resize', function(_, eventData)
|
|
||||||
if not eventData[1] then --- TEST
|
|
||||||
w,h = parentTerm.getSize()
|
|
||||||
|
|
||||||
local windowHeight = h-1
|
|
||||||
|
|
||||||
for _,key in pairs(Util.keys(tabs)) 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, 2, w, windowHeight)
|
|
||||||
end
|
|
||||||
|
|
||||||
redrawMenu()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- downstate should be stored in the tab (maybe)
|
|
||||||
multishell.hook('key_up', function(_, eventData)
|
|
||||||
local code = eventData[1]
|
|
||||||
|
|
||||||
if downState[code] ~= currentTab then
|
|
||||||
downState[code] = nil
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
downState[code] = nil
|
|
||||||
end)
|
|
||||||
|
|
||||||
multishell.hook('key', function(_, eventData)
|
|
||||||
local code = eventData[1]
|
|
||||||
local firstPress = not eventData[2]
|
|
||||||
|
|
||||||
if firstPress then
|
|
||||||
downState[code] = currentTab
|
|
||||||
else
|
|
||||||
--key was pressed initially in a previous window
|
|
||||||
if downState[code] ~= currentTab then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
multishell.hook({ 'key', 'key_up', 'char', 'paste' }, function(event, eventData)
|
|
||||||
local code = Input:translate(event, eventData[1], eventData[2])
|
|
||||||
|
|
||||||
if code and hotkeys[code] then
|
|
||||||
hotkeys[code](event, eventData)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
multishell.hook('mouse_click', function(_, eventData)
|
|
||||||
local x, y = eventData[2], eventData[3]
|
|
||||||
if y == 1 then
|
|
||||||
if x == 1 then
|
|
||||||
multishell.setFocus(overviewId)
|
|
||||||
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
|
|
||||||
downState.mouse = nil
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
downState.mouse = currentTab
|
|
||||||
eventData[3] = eventData[3] - 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
multishell.hook({ 'mouse_up', 'mouse_drag' }, function(event, eventData)
|
|
||||||
if downState.mouse ~= currentTab then
|
|
||||||
-- don't send mouse up as the mouse click event was on another window
|
|
||||||
if event == 'mouse_up' then
|
|
||||||
downState.mouse = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
return true -- stop propagation
|
|
||||||
end
|
|
||||||
eventData[3] = eventData[3] - 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
multishell.hook('mouse_scroll', function(_, eventData)
|
|
||||||
local dir, y = eventData[1], eventData[3]
|
|
||||||
|
|
||||||
if y == 1 then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
if currentTab.terminal.scrollUp then
|
|
||||||
if dir == -1 then
|
|
||||||
currentTab.terminal.scrollUp()
|
|
||||||
else
|
|
||||||
currentTab.terminal.scrollDown()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
eventData[3] = y - 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
local function startup()
|
|
||||||
local success = true
|
|
||||||
|
|
||||||
local function runDir(directory, open)
|
|
||||||
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 = 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
|
|
||||||
_G.printError(err)
|
|
||||||
end
|
|
||||||
success = false
|
|
||||||
end
|
|
||||||
print()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
runDir('sys/services', shell.openHiddenTab)
|
|
||||||
runDir('sys/autorun', shell.run)
|
|
||||||
runDir('usr/autorun', shell.run)
|
|
||||||
|
|
||||||
if not success then
|
|
||||||
print()
|
|
||||||
error('An autorun program has errored')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
overviewId = multishell.openTab({
|
|
||||||
path = 'sys/apps/Overview.lua',
|
|
||||||
isOverview = true,
|
|
||||||
})
|
|
||||||
tabs[overviewId].title = '+'
|
|
||||||
|
|
||||||
multishell.openTab({
|
|
||||||
focused = true,
|
|
||||||
fn = startup,
|
|
||||||
title = 'Autorun',
|
|
||||||
})
|
|
||||||
|
|
||||||
local currentTabEvents = Util.transpose {
|
|
||||||
'char', 'key', 'key_up',
|
|
||||||
'mouse_click', 'mouse_drag', 'mouse_scroll', 'mouse_up',
|
|
||||||
'paste', 'terminate',
|
|
||||||
}
|
|
||||||
|
|
||||||
while true do
|
|
||||||
local tEventData = { os.pullEventRaw() }
|
|
||||||
local sEvent = table.remove(tEventData, 1)
|
|
||||||
local stopPropagation
|
|
||||||
|
|
||||||
local eventHooks = hooks[sEvent]
|
|
||||||
if eventHooks then
|
|
||||||
for i = #eventHooks, 1, -1 do
|
|
||||||
stopPropagation = eventHooks[i](sEvent, tEventData)
|
|
||||||
if stopPropagation then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not stopPropagation then
|
|
||||||
if currentTabEvents[sEvent] then
|
|
||||||
resumeTab(currentTab, sEvent, tEventData)
|
|
||||||
|
|
||||||
else
|
|
||||||
-- Passthrough to all processes
|
|
||||||
for _,key in pairs(Util.keys(tabs)) do
|
|
||||||
resumeTab(tabs[key], sEvent, tEventData)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
45
sys/apps/netdaemon.lua
Normal file
45
sys/apps/netdaemon.lua
Normal 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')
|
||||||
39
sys/apps/network/keygen.lua
Normal file
39
sys/apps/network/keygen.lua
Normal 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)
|
||||||
64
sys/apps/network/proxy.lua
Normal file
64
sys/apps/network/proxy.lua
Normal 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)
|
||||||
92
sys/apps/network/samba.lua
Normal file
92
sys/apps/network/samba.lua
Normal 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
226
sys/apps/network/snmp.lua
Normal 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
103
sys/apps/network/telnet.lua
Normal 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)
|
||||||
151
sys/apps/network/transport.lua
Normal file
151
sys/apps/network/transport.lua
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
--[[
|
||||||
|
Low level socket protocol implementation.
|
||||||
|
|
||||||
|
* sequencing
|
||||||
|
* background read buffering
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local Crypto = require('opus.crypto.chacha20')
|
||||||
|
local Event = require('opus.event')
|
||||||
|
|
||||||
|
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
|
||||||
|
return table.unpack(Crypto.decrypt(data[1], 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]
|
||||||
|
msg.data = Crypto.encrypt({ msg.data }, socket.enckey)
|
||||||
|
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)
|
||||||
76
sys/apps/network/trust.lua
Normal file
76
sys/apps/network/trust.lua
Normal 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 password = Security.getPassword()
|
||||||
|
if not password then
|
||||||
|
socket:write({ msg = 'No password has been set' })
|
||||||
|
else
|
||||||
|
if validateData(data, password, 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
93
sys/apps/network/vnc.lua
Normal 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)
|
||||||
173
sys/apps/package.lua
Normal file
173
sys/apps/package.lua
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
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)
|
||||||
|
-- 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')
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
_G.requireInjector()
|
local Security = require('opus.security')
|
||||||
|
local SHA = require('opus.crypto.sha2')
|
||||||
local Security = require('security')
|
local Terminal = require('opus.terminal')
|
||||||
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(SHA.compute(password))
|
||||||
print('Password updated')
|
print('Password updated')
|
||||||
end
|
end
|
||||||
|
|||||||
114
sys/apps/pastebin.lua
Normal file
114
sys/apps/pastebin.lua
Normal 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
|
||||||
|
|
||||||
664
sys/apps/shell
664
sys/apps/shell
@@ -1,664 +0,0 @@
|
|||||||
local parentShell = _ENV.shell
|
|
||||||
|
|
||||||
_ENV.shell = { }
|
|
||||||
_ENV.multishell = _ENV.multishell or { }
|
|
||||||
|
|
||||||
local fs = _G.fs
|
|
||||||
local shell = _ENV.shell
|
|
||||||
local multishell = _ENV.multishell
|
|
||||||
|
|
||||||
local sandboxEnv = setmetatable({ }, { __index = _G })
|
|
||||||
for k,v in pairs(_ENV) do
|
|
||||||
sandboxEnv[k] = v
|
|
||||||
end
|
|
||||||
sandboxEnv.shell = shell
|
|
||||||
sandboxEnv.multishell = multishell
|
|
||||||
|
|
||||||
_G.requireInjector()
|
|
||||||
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
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 function run(env, ...)
|
|
||||||
local args = tokenise(...)
|
|
||||||
local command = table.remove(args, 1) or error('No such program')
|
|
||||||
local isUrl = not not command:match("^(https?:)")
|
|
||||||
|
|
||||||
local path, loadFn
|
|
||||||
if isUrl then
|
|
||||||
path = command
|
|
||||||
loadFn = Util.loadUrl
|
|
||||||
else
|
|
||||||
path = shell.resolveProgram(command) or error('No such program')
|
|
||||||
loadFn = loadfile
|
|
||||||
end
|
|
||||||
|
|
||||||
local fn, err = loadFn(path, env)
|
|
||||||
if not fn then
|
|
||||||
error(err)
|
|
||||||
end
|
|
||||||
|
|
||||||
if multishell and multishell.setTitle then
|
|
||||||
multishell.setTitle(multishell.getCurrent(), fs.getName(path))
|
|
||||||
end
|
|
||||||
|
|
||||||
if isUrl then
|
|
||||||
tProgramStack[#tProgramStack + 1] = path:match("^https?://([^/:]+:?[0-9]*/?.*)$")
|
|
||||||
else
|
|
||||||
tProgramStack[#tProgramStack + 1] = path
|
|
||||||
end
|
|
||||||
|
|
||||||
local r = { fn(table.unpack(args)) }
|
|
||||||
|
|
||||||
tProgramStack[#tProgramStack] = nil
|
|
||||||
|
|
||||||
return table.unpack(r)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Install shell API
|
|
||||||
function shell.run(...)
|
|
||||||
local oldTitle
|
|
||||||
|
|
||||||
if multishell and multishell.getTitle then
|
|
||||||
oldTitle = multishell.getTitle(multishell.getCurrent())
|
|
||||||
end
|
|
||||||
|
|
||||||
local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G })
|
|
||||||
local r = { pcall(run, env, ...) }
|
|
||||||
|
|
||||||
if multishell and multishell.setTitle then
|
|
||||||
multishell.setTitle(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) 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 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 _,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
|
|
||||||
|
|
||||||
local function completeProgram( sLine )
|
|
||||||
if #sLine > 0 and string.sub( sLine, 1, 1 ) == "/" then
|
|
||||||
-- Add programs from the root
|
|
||||||
return fs.complete( sLine, "", true, false )
|
|
||||||
else
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
local function completeProgramArgument( sProgram, nArgument, sPart, tPreviousParts )
|
|
||||||
local tInfo = tCompletionInfo[ sProgram ]
|
|
||||||
if tInfo then
|
|
||||||
return tInfo.fnComplete( shell, nArgument, sPart, tPreviousParts )
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.complete(sLine)
|
|
||||||
if #sLine > 0 then
|
|
||||||
local tWords = tokenise( sLine )
|
|
||||||
local nIndex = #tWords
|
|
||||||
if string.sub( sLine, #sLine, #sLine ) == " " then
|
|
||||||
nIndex = nIndex + 1
|
|
||||||
end
|
|
||||||
if nIndex == 1 then
|
|
||||||
local sBit = tWords[1] or ""
|
|
||||||
local sPath = shell.resolveProgram( sBit )
|
|
||||||
if tCompletionInfo[ sPath ] then
|
|
||||||
return { " " }
|
|
||||||
else
|
|
||||||
local tResults = completeProgram( sBit )
|
|
||||||
for n=1,#tResults do
|
|
||||||
local sResult = tResults[n]
|
|
||||||
local cPath = shell.resolveProgram( sBit .. sResult )
|
|
||||||
if tCompletionInfo[ cPath ] then
|
|
||||||
tResults[n] = sResult .. " "
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return tResults
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif nIndex > 1 then
|
|
||||||
local sPath = shell.resolveProgram( tWords[1] )
|
|
||||||
local sPart = tWords[nIndex] or ""
|
|
||||||
local tPreviousParts = tWords
|
|
||||||
tPreviousParts[nIndex] = nil
|
|
||||||
return completeProgramArgument( sPath , nIndex - 1, sPart, tPreviousParts )
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.completeProgram( sProgram )
|
|
||||||
return 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 )
|
|
||||||
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.env = sandboxEnv
|
|
||||||
tabInfo.args = 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 env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G })
|
|
||||||
return run(env, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
local Config = require('config')
|
|
||||||
local History = require('history')
|
|
||||||
|
|
||||||
local colors = _G.colors
|
|
||||||
local keys = _G.keys
|
|
||||||
local os = _G.os
|
|
||||||
local term = _G.term
|
|
||||||
local textutils = _G.textutils
|
|
||||||
|
|
||||||
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('shellprompt', config)
|
|
||||||
|
|
||||||
local _colors = config.standard
|
|
||||||
if term.isColor() then
|
|
||||||
_colors = config.color
|
|
||||||
end
|
|
||||||
|
|
||||||
local function autocompleteArgument(program, words)
|
|
||||||
local word = ''
|
|
||||||
if #words > 1 then
|
|
||||||
word = words[#words]
|
|
||||||
end
|
|
||||||
|
|
||||||
local tInfo = tCompletionInfo[program]
|
|
||||||
return tInfo.fnComplete(shell, #words - 1, word, words)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function autocompleteAnything(line, words)
|
|
||||||
local results = shell.complete(line)
|
|
||||||
|
|
||||||
if results and #results == 0 and #words == 1 then
|
|
||||||
results = nil
|
|
||||||
end
|
|
||||||
if not results then
|
|
||||||
results = fs.complete(words[#words] or '', shell.dir(), true, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
return results
|
|
||||||
end
|
|
||||||
|
|
||||||
local function autocomplete(line)
|
|
||||||
local words = { }
|
|
||||||
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
|
|
||||||
|
|
||||||
local program = shell.resolveProgram(words[1])
|
|
||||||
if tCompletionInfo[program] then
|
|
||||||
results = autocompleteArgument(program, words) or { }
|
|
||||||
else
|
|
||||||
results = autocompleteAnything(line, words) or { }
|
|
||||||
end
|
|
||||||
|
|
||||||
Util.filterInplace(results, function(f)
|
|
||||||
return not Util.key(results, f .. '/')
|
|
||||||
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] = 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
|
|
||||||
|
|
||||||
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(string.len(sItem) + 1, nMaxLen)
|
|
||||||
end
|
|
||||||
local nCols = math.floor(w / 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.white, tFiles)
|
|
||||||
else
|
|
||||||
textutils.tabulate(colors.white, tFiles)
|
|
||||||
end
|
|
||||||
|
|
||||||
term.setTextColour(_colors.promptTextColor)
|
|
||||||
term.setBackgroundColor(_colors.promptBackgroundColor)
|
|
||||||
term.write("$ " )
|
|
||||||
|
|
||||||
term.setTextColour(_colors.commandTextColor)
|
|
||||||
term.setBackgroundColor(colors.black)
|
|
||||||
return line
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function shellRead(history)
|
|
||||||
term.setCursorBlink( true )
|
|
||||||
|
|
||||||
local sLine = ""
|
|
||||||
local nPos = 0
|
|
||||||
|
|
||||||
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 _,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 = 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 cline = autocomplete(sLine)
|
|
||||||
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 _, 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)
|
|
||||||
term.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
|
|
||||||
_G.printError(err)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
680
sys/apps/shell.lua
Normal file
680
sys/apps/shell.lua
Normal 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
|
||||||
71
sys/apps/system/aliases.lua
Normal file
71
sys/apps/system/aliases.lua
Normal 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
57
sys/apps/system/cloud.lua
Normal 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
|
||||||
|
|
||||||
161
sys/apps/system/diskusage.lua
Normal file
161
sys/apps/system/diskusage.lua
Normal 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
57
sys/apps/system/kiosk.lua
Normal 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
50
sys/apps/system/label.lua
Normal 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,
|
||||||
|
}
|
||||||
88
sys/apps/system/launcher.lua
Normal file
88
sys/apps/system/launcher.lua
Normal 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
|
||||||
62
sys/apps/system/network.lua
Normal file
62
sys/apps/system/network.lua
Normal 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
|
||||||
|
}
|
||||||
45
sys/apps/system/password.lua
Normal file
45
sys/apps/system/password.lua
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
local Security = require('opus.security')
|
||||||
|
local SHA = require('opus.crypto.sha2')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
|
||||||
|
return UI.Tab {
|
||||||
|
title = 'Password',
|
||||||
|
description = 'Wireless network password',
|
||||||
|
[1] = UI.Window {
|
||||||
|
x = 2, y = 2, ex = -2, ey = 4,
|
||||||
|
},
|
||||||
|
newPass = UI.TextEntry {
|
||||||
|
x = 3, ex = -3, y = 3,
|
||||||
|
limit = 32,
|
||||||
|
mask = true,
|
||||||
|
shadowText = 'new password',
|
||||||
|
accelerators = {
|
||||||
|
enter = 'new_password',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
button = UI.Button {
|
||||||
|
x = -8, ex = -2, y = -2,
|
||||||
|
text = 'Apply',
|
||||||
|
event = 'update_password',
|
||||||
|
},
|
||||||
|
info = UI.TextArea {
|
||||||
|
x = 2, ex = -2, y = 5, ey = -4,
|
||||||
|
backgroundColor = 'black',
|
||||||
|
textColor = 'yellow',
|
||||||
|
inactive = true,
|
||||||
|
marginLeft = 1, marginRight = 1, marginTop = 1,
|
||||||
|
value = 'Add a password to enable other computers to connect to this one.',
|
||||||
|
},
|
||||||
|
eventHandler = function(self, event)
|
||||||
|
if event.type == 'update_password' then
|
||||||
|
if not self.newPass.value or #self.newPass.value == 0 then
|
||||||
|
self:emit({ type = 'error_message', message = 'Invalid password' })
|
||||||
|
|
||||||
|
else
|
||||||
|
Security.updatePassword(SHA.compute(self.newPass.value))
|
||||||
|
self:emit({ type = 'success_message', message = 'Password updated' })
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
105
sys/apps/system/path.lua
Normal file
105
sys/apps/system/path.lua
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
local Config = require('opus.config')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
|
local tab = UI.Tab {
|
||||||
|
title = 'Path',
|
||||||
|
description = 'Set the shell path',
|
||||||
|
tabClose = true,
|
||||||
|
[1] = UI.Window {
|
||||||
|
x = 2, y = 2, ex = -2, ey = 4,
|
||||||
|
},
|
||||||
|
entry = UI.TextEntry {
|
||||||
|
x = 3, y = 3, ex = -3,
|
||||||
|
shadowText = 'enter new path',
|
||||||
|
accelerators = {
|
||||||
|
enter = 'update_path',
|
||||||
|
},
|
||||||
|
help = 'add a new path',
|
||||||
|
},
|
||||||
|
grid = UI.Grid {
|
||||||
|
x = 2, y = 6, ex = -2, ey = -3,
|
||||||
|
disableHeader = true,
|
||||||
|
columns = { { key = 'value' } },
|
||||||
|
autospace = true,
|
||||||
|
sortColumn = 'index',
|
||||||
|
help = 'double-click to remove, shift-arrow to move',
|
||||||
|
accelerators = {
|
||||||
|
delete = 'remove',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statusBar = UI.StatusBar { },
|
||||||
|
accelerators = {
|
||||||
|
[ 'shift-up' ] = 'move_up',
|
||||||
|
[ 'shift-down' ] = 'move_down',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function tab:updateList(path)
|
||||||
|
self.grid.values = { }
|
||||||
|
for k,v in ipairs(Util.split(path, '(.-):')) do
|
||||||
|
table.insert(self.grid.values, { index = k, value = v })
|
||||||
|
end
|
||||||
|
self.grid:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
function tab:enable()
|
||||||
|
local env = Config.load('shell')
|
||||||
|
self:updateList(env.path)
|
||||||
|
UI.Tab.enable(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function tab:save()
|
||||||
|
local t = { }
|
||||||
|
for _, v in ipairs(self.grid.values) do
|
||||||
|
table.insert(t, v.value)
|
||||||
|
end
|
||||||
|
local env = Config.load('shell')
|
||||||
|
env.path = table.concat(t, ':')
|
||||||
|
self:updateList(env.path)
|
||||||
|
Config.update('shell', env)
|
||||||
|
end
|
||||||
|
|
||||||
|
function tab:eventHandler(event)
|
||||||
|
if event.type == 'update_path' and self.entry.value then
|
||||||
|
table.insert(self.grid.values, {
|
||||||
|
value = self.entry.value,
|
||||||
|
})
|
||||||
|
self:save()
|
||||||
|
self.entry:reset()
|
||||||
|
self.entry:draw()
|
||||||
|
self.grid:draw()
|
||||||
|
return true
|
||||||
|
|
||||||
|
elseif event.type == 'grid_select' or event.type == 'remove' then
|
||||||
|
local selected = self.grid:getSelected()
|
||||||
|
if selected then
|
||||||
|
table.remove(self.grid.values, selected.index)
|
||||||
|
self:save()
|
||||||
|
self.grid:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif event.type == 'focus_change' then
|
||||||
|
self.statusBar:setStatus(event.focused.help)
|
||||||
|
|
||||||
|
elseif event.type == 'move_up' then
|
||||||
|
local entry = self.grid:getSelected()
|
||||||
|
if entry.index > 1 then
|
||||||
|
table.insert(self.grid.values, entry.index - 1, table.remove(self.grid.values, entry.index))
|
||||||
|
self.grid:setIndex(entry.index - 1)
|
||||||
|
self:save()
|
||||||
|
self.grid:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif event.type == 'move_down' then
|
||||||
|
local entry = self.grid:getSelected()
|
||||||
|
if entry.index < #self.grid.values then
|
||||||
|
table.insert(self.grid.values, entry.index + 1, table.remove(self.grid.values, entry.index))
|
||||||
|
self.grid:setIndex(entry.index + 1)
|
||||||
|
self:save()
|
||||||
|
self.grid:draw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return tab
|
||||||
103
sys/apps/system/requires.lua
Normal file
103
sys/apps/system/requires.lua
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
local Config = require('opus.config')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
|
local tab = UI.Tab {
|
||||||
|
title = 'Requires',
|
||||||
|
description = 'Require path',
|
||||||
|
tabClose = true,
|
||||||
|
entry = UI.TextEntry {
|
||||||
|
x = 2, y = 2, ex = -2,
|
||||||
|
shadowText = 'Enter new require path',
|
||||||
|
accelerators = {
|
||||||
|
enter = 'update_path',
|
||||||
|
},
|
||||||
|
help = 'add a new path (reboot required)',
|
||||||
|
},
|
||||||
|
grid = UI.Grid {
|
||||||
|
y = 4, ey = -3,
|
||||||
|
disableHeader = true,
|
||||||
|
columns = { { key = 'value' } },
|
||||||
|
autospace = true,
|
||||||
|
sortColumn = 'index',
|
||||||
|
help = 'double-click to remove, shift-arrow to move',
|
||||||
|
accelerators = {
|
||||||
|
delete = 'remove',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
statusBar = UI.StatusBar { },
|
||||||
|
accelerators = {
|
||||||
|
[ 'shift-up' ] = 'move_up',
|
||||||
|
[ 'shift-down' ] = 'move_down',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function tab:updateList(lua_path)
|
||||||
|
self.grid.values = { }
|
||||||
|
for k,v in ipairs(Util.split(lua_path, '(.-);')) do
|
||||||
|
table.insert(self.grid.values, { index = k, value = v })
|
||||||
|
end
|
||||||
|
self.grid:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
function tab:enable()
|
||||||
|
local env = Config.load('shell')
|
||||||
|
self:updateList(env.lua_path)
|
||||||
|
UI.Tab.enable(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
function tab:save()
|
||||||
|
local t = { }
|
||||||
|
for _, v in ipairs(self.grid.values) do
|
||||||
|
table.insert(t, v.value)
|
||||||
|
end
|
||||||
|
local env = Config.load('shell')
|
||||||
|
env.lua_path = table.concat(t, ';')
|
||||||
|
self:updateList(env.lua_path)
|
||||||
|
Config.update('shell', env)
|
||||||
|
end
|
||||||
|
|
||||||
|
function tab:eventHandler(event)
|
||||||
|
if event.type == 'update_path' then
|
||||||
|
table.insert(self.grid.values, {
|
||||||
|
value = self.entry.value,
|
||||||
|
})
|
||||||
|
self:save()
|
||||||
|
self.entry:reset()
|
||||||
|
self.entry:draw()
|
||||||
|
self.grid:draw()
|
||||||
|
return true
|
||||||
|
|
||||||
|
elseif event.type == 'grid_select' or event.type == 'remove' then
|
||||||
|
local selected = self.grid:getSelected()
|
||||||
|
if selected then
|
||||||
|
table.remove(self.grid.values, selected.index)
|
||||||
|
self:save()
|
||||||
|
self.grid:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif event.type == 'focus_change' then
|
||||||
|
self.statusBar:setStatus(event.focused.help)
|
||||||
|
|
||||||
|
elseif event.type == 'move_up' then
|
||||||
|
local entry = self.grid:getSelected()
|
||||||
|
if entry.index > 1 then
|
||||||
|
table.insert(self.grid.values, entry.index - 1, table.remove(self.grid.values, entry.index))
|
||||||
|
self.grid:setIndex(entry.index - 1)
|
||||||
|
self:save()
|
||||||
|
self.grid:draw()
|
||||||
|
end
|
||||||
|
|
||||||
|
elseif event.type == 'move_down' then
|
||||||
|
local entry = self.grid:getSelected()
|
||||||
|
if entry.index < #self.grid.values then
|
||||||
|
table.insert(self.grid.values, entry.index + 1, table.remove(self.grid.values, entry.index))
|
||||||
|
self.grid:setIndex(entry.index + 1)
|
||||||
|
self:save()
|
||||||
|
self.grid:draw()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
--this needs rework - see 4.user.lua
|
||||||
|
--return tab
|
||||||
94
sys/apps/system/settings.lua
Normal file
94
sys/apps/system/settings.lua
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
local UI = require('opus.ui')
|
||||||
|
|
||||||
|
local settings = _G.settings
|
||||||
|
|
||||||
|
local transform = {
|
||||||
|
string = tostring,
|
||||||
|
number = tonumber,
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings and UI.Tab {
|
||||||
|
title = 'Settings',
|
||||||
|
description = 'Computercraft settings',
|
||||||
|
grid = UI.Grid {
|
||||||
|
x = 2, y = 2, ex = -2, ey = -2,
|
||||||
|
sortColumn = 'name',
|
||||||
|
columns = {
|
||||||
|
{ heading = 'Setting', key = 'name' },
|
||||||
|
{ heading = 'Value', key = 'value' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
editor = UI.SlideOut {
|
||||||
|
y = -6, height = 6,
|
||||||
|
titleBar = UI.TitleBar {
|
||||||
|
event = 'slide_hide',
|
||||||
|
title = 'Enter value',
|
||||||
|
},
|
||||||
|
form = UI.Form {
|
||||||
|
y = 2,
|
||||||
|
value = UI.TextEntry {
|
||||||
|
formIndex = 1,
|
||||||
|
formLabel = 'Value',
|
||||||
|
formKey = 'value',
|
||||||
|
},
|
||||||
|
validateField = function(self, entry)
|
||||||
|
if entry.value then
|
||||||
|
return transform[self.type](entry.value)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
accelerators = {
|
||||||
|
form_cancel = 'slide_hide',
|
||||||
|
},
|
||||||
|
show = function(self, entry)
|
||||||
|
self.form.type = type(entry.value) or 'string'
|
||||||
|
self.form:setValues(entry)
|
||||||
|
self.titleBar.title = entry.name
|
||||||
|
UI.SlideOut.show(self)
|
||||||
|
end,
|
||||||
|
eventHandler = function(self, event)
|
||||||
|
if event.type == 'form_complete' then
|
||||||
|
if not event.values.value then
|
||||||
|
settings.unset(event.values.name)
|
||||||
|
self.parent:reload()
|
||||||
|
else
|
||||||
|
event.values.value = transform[self.form.type](event.values.value)
|
||||||
|
settings.set(event.values.name, event.values.value)
|
||||||
|
end
|
||||||
|
self.parent.grid:draw()
|
||||||
|
self:hide()
|
||||||
|
settings.save('.settings')
|
||||||
|
end
|
||||||
|
return UI.SlideOut.eventHandler(self, event)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
reload = function(self)
|
||||||
|
local values = { }
|
||||||
|
for _,v in pairs(settings.getNames()) do
|
||||||
|
table.insert(values, {
|
||||||
|
name = v,
|
||||||
|
value = settings.get(v) or false,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
self.grid:setValues(values)
|
||||||
|
self.grid:setIndex(1)
|
||||||
|
end,
|
||||||
|
enable = function(self)
|
||||||
|
self:reload()
|
||||||
|
UI.Tab.enable(self)
|
||||||
|
end,
|
||||||
|
eventHandler = function(self, event)
|
||||||
|
if event.type == 'grid_select' then
|
||||||
|
if type(event.selected.value) == 'boolean' then
|
||||||
|
event.selected.value = not event.selected.value
|
||||||
|
settings.set(event.selected.name, event.selected.value)
|
||||||
|
settings.save('.settings')
|
||||||
|
self.grid:draw()
|
||||||
|
else
|
||||||
|
self.editor:show(event.selected)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
}
|
||||||
133
sys/apps/system/shell.lua
Normal file
133
sys/apps/system/shell.lua
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
local Config = require('opus.config')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
|
local colors = _G.colors
|
||||||
|
local os = _G.os
|
||||||
|
|
||||||
|
local config = Config.load('shellprompt')
|
||||||
|
|
||||||
|
local allColors = { }
|
||||||
|
for k,v in pairs(colors) do
|
||||||
|
if type(v) == 'number' then
|
||||||
|
table.insert(allColors, { name = k, value = v })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local defaults = {
|
||||||
|
textColor = colors.white,
|
||||||
|
commandTextColor = colors.yellow,
|
||||||
|
directoryTextColor = colors.orange,
|
||||||
|
promptTextColor = colors.blue,
|
||||||
|
directoryColor = colors.green,
|
||||||
|
fileColor = colors.white,
|
||||||
|
backgroundColor = colors.black,
|
||||||
|
}
|
||||||
|
local _colors = config.color or Util.shallowCopy(defaults)
|
||||||
|
|
||||||
|
local allSettings = { }
|
||||||
|
for k in pairs(defaults) do
|
||||||
|
table.insert(allSettings, { name = k })
|
||||||
|
end
|
||||||
|
|
||||||
|
-- temp
|
||||||
|
if not _colors.backgroundColor then
|
||||||
|
_colors.backgroundColor = colors.black
|
||||||
|
_colors.fileColor = colors.white
|
||||||
|
end
|
||||||
|
|
||||||
|
return UI.Tab {
|
||||||
|
title = 'Shell',
|
||||||
|
description = 'Shell options',
|
||||||
|
grid1 = UI.ScrollingGrid {
|
||||||
|
y = 2, ey = -10, x = 2, ex = -17,
|
||||||
|
disableHeader = true,
|
||||||
|
columns = { { key = 'name' } },
|
||||||
|
values = allSettings,
|
||||||
|
sortColumn = 'name',
|
||||||
|
},
|
||||||
|
grid2 = UI.ScrollingGrid {
|
||||||
|
y = 2, ey = -10, x = -14, ex = -2,
|
||||||
|
disableHeader = true,
|
||||||
|
columns = { { key = 'name' } },
|
||||||
|
values = allColors,
|
||||||
|
sortColumn = 'name',
|
||||||
|
getRowTextColor = function(self, row)
|
||||||
|
local selected = self.parent.grid1:getSelected()
|
||||||
|
if _colors[selected.name] == row.value then
|
||||||
|
return colors.yellow
|
||||||
|
end
|
||||||
|
return UI.Grid.getRowTextColor(self, row)
|
||||||
|
end
|
||||||
|
},
|
||||||
|
directory = UI.Checkbox {
|
||||||
|
x = 2, y = -2,
|
||||||
|
labelBackgroundColor = colors.black,
|
||||||
|
label = 'Directory',
|
||||||
|
value = config.displayDirectory
|
||||||
|
},
|
||||||
|
reset = UI.Button {
|
||||||
|
x = -18, y = -2,
|
||||||
|
text = 'Reset',
|
||||||
|
event = 'reset',
|
||||||
|
},
|
||||||
|
button = UI.Button {
|
||||||
|
x = -9, y = -2,
|
||||||
|
text = 'Update',
|
||||||
|
event = 'update',
|
||||||
|
},
|
||||||
|
display = UI.Window {
|
||||||
|
x = 2, ex = -2, y = -8, height = 5,
|
||||||
|
draw = function(self)
|
||||||
|
self:clear(_colors.backgroundColor)
|
||||||
|
local offset = 0
|
||||||
|
if config.displayDirectory then
|
||||||
|
self:write(1, 1,
|
||||||
|
'==' .. os.getComputerLabel() .. ':/dir/etc',
|
||||||
|
_colors.backgroundColor, _colors.directoryTextColor)
|
||||||
|
offset = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
self:write(1, 1 + offset, '$ ',
|
||||||
|
_colors.backgroundColor, _colors.promptTextColor)
|
||||||
|
|
||||||
|
self:write(3, 1 + offset, 'ls /',
|
||||||
|
_colors.backgroundColor, _colors.commandTextColor)
|
||||||
|
|
||||||
|
self:write(1, 2 + offset, 'sys usr',
|
||||||
|
_colors.backgroundColor, _colors.directoryColor)
|
||||||
|
|
||||||
|
self:write(1, 3 + offset, 'startup',
|
||||||
|
_colors.backgroundColor, _colors.fileColor)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
eventHandler = function(self, event)
|
||||||
|
if event.type =='checkbox_change' then
|
||||||
|
config.displayDirectory = not not event.checked
|
||||||
|
self.display:draw()
|
||||||
|
|
||||||
|
elseif event.type == 'grid_focus_row' and event.element == self.grid1 then
|
||||||
|
self.grid2:draw()
|
||||||
|
|
||||||
|
elseif event.type == 'grid_select' and event.element == self.grid2 then
|
||||||
|
_colors[self.grid1:getSelected().name] = event.selected.value
|
||||||
|
self.display:draw()
|
||||||
|
self.grid2:draw()
|
||||||
|
|
||||||
|
elseif event.type == 'reset' then
|
||||||
|
config.color = defaults
|
||||||
|
config.displayDirectory = true
|
||||||
|
self.directory.value = true
|
||||||
|
_colors = Util.shallowCopy(defaults)
|
||||||
|
|
||||||
|
Config.update('shellprompt', config)
|
||||||
|
self:draw()
|
||||||
|
|
||||||
|
elseif event.type == 'update' then
|
||||||
|
config.color = _colors
|
||||||
|
Config.update('shellprompt', config)
|
||||||
|
|
||||||
|
end
|
||||||
|
return UI.Tab.eventHandler(self, event)
|
||||||
|
end
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user