Compare commits
531 Commits
master
...
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 | ||
|
|
13ec8ea04f | ||
|
|
911fec9118 | ||
|
|
1528bab3ac | ||
|
|
dc9d174085 | ||
|
|
1108c173d7 | ||
|
|
0830e46fb8 | ||
|
|
743959c1fa | ||
|
|
dd4211745e | ||
|
|
0df22efdc2 | ||
|
|
a8a4ceb85d | ||
|
|
f533e42c0c | ||
|
|
1b9450017d | ||
|
|
cac15722b8 | ||
|
|
7fd93e8a8b | ||
|
|
84b2b8ce63 | ||
|
|
22a432492c | ||
|
|
8d3f5329f2 | ||
|
|
8e9ff9c626 | ||
|
|
31b3787695 | ||
|
|
fb0f3e567a | ||
|
|
f6d1cfc7ee | ||
|
|
39ba226a82 | ||
|
|
fe0ca72b8b | ||
|
|
2721840596 | ||
|
|
9b8b5238b0 | ||
|
|
8b187f2813 | ||
|
|
153b0b86ff | ||
|
|
a9634cb438 | ||
|
|
3460dd68b2 | ||
|
|
f9221e67be | ||
|
|
852ad193f0 | ||
|
|
05c99b583a | ||
|
|
f5b99d91e5 | ||
|
|
955f11042b | ||
|
|
a625b52bad | ||
|
|
98ec840db1 | ||
|
|
af981dd1f8 | ||
|
|
91a05c07dd | ||
|
|
fc69d4be83 | ||
|
|
f0846c8daa |
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(self, ...)
|
|
||||||
local str = '\027['
|
|
||||||
for k,v in ipairs({ ...}) do
|
|
||||||
if k == 1 then
|
|
||||||
str = str .. v
|
|
||||||
else
|
|
||||||
str = str .. ';' .. v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return str .. 'm'
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
Ansi.codes = {
|
|
||||||
reset = 0,
|
|
||||||
white = 1,
|
|
||||||
orange = 2,
|
|
||||||
magenta = 3,
|
|
||||||
lightBlue = 4,
|
|
||||||
yellow = 5,
|
|
||||||
lime = 6,
|
|
||||||
pink = 7,
|
|
||||||
gray = 8,
|
|
||||||
lightGray = 9,
|
|
||||||
cyan = 10,
|
|
||||||
purple = 11,
|
|
||||||
blue = 12,
|
|
||||||
brown = 13,
|
|
||||||
green = 14,
|
|
||||||
red = 15,
|
|
||||||
black = 16,
|
|
||||||
onwhite = 21,
|
|
||||||
onorange = 22,
|
|
||||||
onmagenta = 23,
|
|
||||||
onlightBlue = 24,
|
|
||||||
onyellow = 25,
|
|
||||||
onlime = 26,
|
|
||||||
onpink = 27,
|
|
||||||
ongray = 28,
|
|
||||||
onlightGray = 29,
|
|
||||||
oncyan = 30,
|
|
||||||
onpurple = 31,
|
|
||||||
onblue = 32,
|
|
||||||
onbrown = 33,
|
|
||||||
ongreen = 34,
|
|
||||||
onred = 35,
|
|
||||||
onblack = 36,
|
|
||||||
}
|
|
||||||
|
|
||||||
for k,v in pairs(Ansi.codes) do
|
|
||||||
Ansi[k] = Ansi(v)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Ansi
|
|
||||||
@@ -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,24 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local Config = { }
|
|
||||||
|
|
||||||
Config.load = function(fname, data)
|
|
||||||
local filename = 'usr/config/' .. fname
|
|
||||||
|
|
||||||
if not fs.exists('usr/config') then
|
|
||||||
fs.makeDir('usr/config')
|
|
||||||
end
|
|
||||||
|
|
||||||
if not fs.exists(filename) then
|
|
||||||
Util.writeTable(filename, data)
|
|
||||||
else
|
|
||||||
Util.merge(data, Util.readTable(filename) or { })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Config.update = function(fname, data)
|
|
||||||
local filename = 'usr/config/' .. fname
|
|
||||||
Util.writeTable(filename, data)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Config
|
|
||||||
@@ -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,216 +0,0 @@
|
|||||||
local Event = {
|
|
||||||
uid = 1, -- unique id for handlers
|
|
||||||
routines = { }, -- coroutines
|
|
||||||
types = { }, -- event handlers
|
|
||||||
timers = { }, -- named timers
|
|
||||||
terminate = false,
|
|
||||||
}
|
|
||||||
|
|
||||||
local Routine = { }
|
|
||||||
|
|
||||||
function Routine:isDead()
|
|
||||||
if not self.co then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return coroutine.status(self.co) == 'dead'
|
|
||||||
end
|
|
||||||
|
|
||||||
function Routine:terminate()
|
|
||||||
if self.co then
|
|
||||||
self:resume('terminate')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Routine:resume(event, ...)
|
|
||||||
|
|
||||||
if not self.co then
|
|
||||||
error('Cannot resume a dead routine')
|
|
||||||
end
|
|
||||||
|
|
||||||
if not self.filter or self.filter == event or event == "terminate" then
|
|
||||||
local s, m = coroutine.resume(self.co, event, ...)
|
|
||||||
|
|
||||||
if coroutine.status(self.co) == 'dead' then
|
|
||||||
self.co = nil
|
|
||||||
self.filter = nil
|
|
||||||
Event.routines[self.uid] = nil
|
|
||||||
else
|
|
||||||
self.filter = m
|
|
||||||
end
|
|
||||||
|
|
||||||
if not s and event ~= 'terminate' then
|
|
||||||
error('\n' .. (m or 'Error processing event'))
|
|
||||||
end
|
|
||||||
|
|
||||||
return s, m
|
|
||||||
end
|
|
||||||
|
|
||||||
return true, self.filter
|
|
||||||
end
|
|
||||||
|
|
||||||
local function nextUID()
|
|
||||||
Event.uid = Event.uid + 1
|
|
||||||
return Event.uid - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.on(event, fn)
|
|
||||||
|
|
||||||
local handlers = Event.types[event]
|
|
||||||
if not handlers then
|
|
||||||
handlers = { }
|
|
||||||
Event.types[event] = handlers
|
|
||||||
end
|
|
||||||
|
|
||||||
local handler = {
|
|
||||||
uid = nextUID(),
|
|
||||||
event = event,
|
|
||||||
fn = fn,
|
|
||||||
}
|
|
||||||
handlers[handler.uid] = handler
|
|
||||||
setmetatable(handler, { __index = Routine })
|
|
||||||
|
|
||||||
return handler
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.off(h)
|
|
||||||
if h and h.event then
|
|
||||||
Event.types[h.event][h.uid] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function addTimer(interval, recurring, fn)
|
|
||||||
|
|
||||||
local timerId = os.startTimer(interval)
|
|
||||||
local handler
|
|
||||||
|
|
||||||
handler = Event.on('timer', function(t, id)
|
|
||||||
if timerId == id then
|
|
||||||
fn(t, id)
|
|
||||||
if recurring then
|
|
||||||
timerId = os.startTimer(interval)
|
|
||||||
else
|
|
||||||
Event.off(handler)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
return handler
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.onInterval(interval, fn)
|
|
||||||
return addTimer(interval, true, fn)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.onTimeout(timeout, fn)
|
|
||||||
return addTimer(timeout, false, fn)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.addNamedTimer(name, interval, recurring, fn)
|
|
||||||
Event.cancelNamedTimer(name)
|
|
||||||
Event.timers[name] = addTimer(interval, recurring, fn)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.cancelNamedTimer(name)
|
|
||||||
local timer = Event.timers[name]
|
|
||||||
if timer then
|
|
||||||
Event.off(timer)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.waitForEvent(event, timeout)
|
|
||||||
local timerId = os.startTimer(timeout)
|
|
||||||
repeat
|
|
||||||
local e = { os.pullEvent() }
|
|
||||||
if e[1] == event then
|
|
||||||
return table.unpack(e)
|
|
||||||
end
|
|
||||||
until e[1] == 'timer' and e[2] == timerId
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.addRoutine(fn)
|
|
||||||
local r = {
|
|
||||||
co = coroutine.create(fn),
|
|
||||||
uid = nextUID()
|
|
||||||
}
|
|
||||||
setmetatable(r, { __index = Routine })
|
|
||||||
Event.routines[r.uid] = r
|
|
||||||
|
|
||||||
r:resume()
|
|
||||||
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.pullEvents(...)
|
|
||||||
|
|
||||||
for _, fn in ipairs({ ... }) do
|
|
||||||
Event.addRoutine(fn)
|
|
||||||
end
|
|
||||||
|
|
||||||
repeat
|
|
||||||
local e = Event.pullEvent()
|
|
||||||
until e[1] == 'terminate'
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.exitPullEvents()
|
|
||||||
Event.terminate = true
|
|
||||||
os.sleep(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function processHandlers(event)
|
|
||||||
|
|
||||||
local handlers = Event.types[event]
|
|
||||||
if handlers then
|
|
||||||
for _,h in pairs(handlers) do
|
|
||||||
if not h.co then
|
|
||||||
-- callbacks are single threaded (only 1 co per handler)
|
|
||||||
h.co = coroutine.create(h.fn)
|
|
||||||
Event.routines[h.uid] = h
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function tokeys(t)
|
|
||||||
local keys = { }
|
|
||||||
for k in pairs(t) do
|
|
||||||
keys[#keys+1] = k
|
|
||||||
end
|
|
||||||
return keys
|
|
||||||
end
|
|
||||||
|
|
||||||
local function processRoutines(...)
|
|
||||||
|
|
||||||
local keys = tokeys(Event.routines)
|
|
||||||
for _,key in ipairs(keys) do
|
|
||||||
local r = Event.routines[key]
|
|
||||||
if r then
|
|
||||||
r:resume(...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.processEvent(e)
|
|
||||||
processHandlers(e[1])
|
|
||||||
processRoutines(table.unpack(e))
|
|
||||||
end
|
|
||||||
|
|
||||||
function Event.pullEvent(eventType)
|
|
||||||
|
|
||||||
while true do
|
|
||||||
local e = { os.pullEventRaw() }
|
|
||||||
|
|
||||||
processHandlers(e[1])
|
|
||||||
processRoutines(table.unpack(e))
|
|
||||||
|
|
||||||
if Event.terminate or e[1] == 'terminate' then
|
|
||||||
Event.terminate = false
|
|
||||||
return { 'terminate' }
|
|
||||||
end
|
|
||||||
|
|
||||||
if not eventType or e[1] == eventType then
|
|
||||||
return e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Event
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
local git = require('git')
|
|
||||||
|
|
||||||
local gitfs = { }
|
|
||||||
|
|
||||||
function gitfs.mount(dir, repo)
|
|
||||||
if not repo then
|
|
||||||
error('gitfs syntax: repo')
|
|
||||||
end
|
|
||||||
|
|
||||||
local list = git.list(repo)
|
|
||||||
for path, entry in pairs(list) do
|
|
||||||
if not fs.exists(fs.combine(dir, path)) then
|
|
||||||
local node = fs.mount(fs.combine(dir, path), 'urlfs', entry.url)
|
|
||||||
node.size = entry.size
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return gitfs
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
local linkfs = { }
|
|
||||||
|
|
||||||
local methods = { 'exists', 'getFreeSpace', 'getSize',
|
|
||||||
'isDir', 'isReadOnly', 'list', 'listEx', 'makeDir', 'open', 'getDrive' }
|
|
||||||
|
|
||||||
for _,m in pairs(methods) do
|
|
||||||
linkfs[m] = function(node, dir, ...)
|
|
||||||
dir = dir:gsub(node.mountPoint, node.source, 1)
|
|
||||||
return fs[m](dir, ...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function linkfs.mount(dir, source)
|
|
||||||
if not source then
|
|
||||||
error('Source is required')
|
|
||||||
end
|
|
||||||
source = fs.combine(source, '')
|
|
||||||
if fs.isDir(source) then
|
|
||||||
return {
|
|
||||||
source = source,
|
|
||||||
nodes = { },
|
|
||||||
}
|
|
||||||
end
|
|
||||||
return {
|
|
||||||
source = source
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function linkfs.copy(node, s, t)
|
|
||||||
s = s:gsub(node.mountPoint, node.source, 1)
|
|
||||||
t = t:gsub(node.mountPoint, node.source, 1)
|
|
||||||
return fs.copy(s, t)
|
|
||||||
end
|
|
||||||
|
|
||||||
function linkfs.delete(node, dir)
|
|
||||||
if dir == node.mountPoint then
|
|
||||||
fs.unmount(node.mountPoint)
|
|
||||||
else
|
|
||||||
dir = dir:gsub(node.mountPoint, node.source, 1)
|
|
||||||
return fs.delete(dir)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function linkfs.find(node, spec)
|
|
||||||
spec = spec:gsub(node.mountPoint, node.source, 1)
|
|
||||||
|
|
||||||
local list = fs.find(spec)
|
|
||||||
for k,f in ipairs(list) do
|
|
||||||
list[k] = f:gsub(node.source, node.mountPoint, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
return list
|
|
||||||
end
|
|
||||||
|
|
||||||
function linkfs.move(node, s, t)
|
|
||||||
s = s:gsub(node.mountPoint, node.source, 1)
|
|
||||||
t = t:gsub(node.mountPoint, node.source, 1)
|
|
||||||
return fs.move(s, t)
|
|
||||||
end
|
|
||||||
|
|
||||||
return linkfs
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
local Socket = require('socket')
|
|
||||||
local synchronized = require('sync')
|
|
||||||
|
|
||||||
local netfs = { }
|
|
||||||
|
|
||||||
local function remoteCommand(node, msg)
|
|
||||||
|
|
||||||
for i = 1, 2 do
|
|
||||||
if not node.socket then
|
|
||||||
node.socket = Socket.connect(node.id, 139)
|
|
||||||
end
|
|
||||||
|
|
||||||
if not node.socket then
|
|
||||||
error('netfs: Unable to establish connection to ' .. node.id)
|
|
||||||
fs.unmount(node.mountPoint)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local ret
|
|
||||||
synchronized(node.socket, function()
|
|
||||||
node.socket:write(msg)
|
|
||||||
ret = node.socket:read(1)
|
|
||||||
end)
|
|
||||||
|
|
||||||
if ret then
|
|
||||||
return ret.response
|
|
||||||
end
|
|
||||||
node.socket:close()
|
|
||||||
node.socket = nil
|
|
||||||
end
|
|
||||||
error('netfs: Connection failed', 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx' }
|
|
||||||
|
|
||||||
local function resolveDir(dir, node)
|
|
||||||
dir = dir:gsub(node.mountPoint, '', 1)
|
|
||||||
return fs.combine(node.directory, dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
for _,m in pairs(methods) do
|
|
||||||
netfs[m] = function(node, dir)
|
|
||||||
dir = resolveDir(dir, node)
|
|
||||||
|
|
||||||
return remoteCommand(node, {
|
|
||||||
fn = m,
|
|
||||||
args = { dir },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.mount(dir, id, directory)
|
|
||||||
if not id or not tonumber(id) then
|
|
||||||
error('ramfs syntax: computerId [directory]')
|
|
||||||
end
|
|
||||||
return {
|
|
||||||
id = tonumber(id),
|
|
||||||
nodes = { },
|
|
||||||
directory = directory or '',
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.getDrive()
|
|
||||||
return 'net'
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.complete(node, partial, dir, includeFiles, includeSlash)
|
|
||||||
dir = resolveDir(dir, node)
|
|
||||||
|
|
||||||
return remoteCommand(node, {
|
|
||||||
fn = 'complete',
|
|
||||||
args = { partial, dir, includeFiles, includeSlash },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.copy(node, s, t)
|
|
||||||
s = resolveDir(s, node)
|
|
||||||
t = resolveDir(t, node)
|
|
||||||
|
|
||||||
return remoteCommand(node, {
|
|
||||||
fn = 'copy',
|
|
||||||
args = { s, t },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.isDir(node, dir)
|
|
||||||
if dir == node.mountPoint and node.directory == '' then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return remoteCommand(node, {
|
|
||||||
fn = 'isDir',
|
|
||||||
args = { resolveDir(dir, node) },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.isReadOnly(node, dir)
|
|
||||||
if dir == node.mountPoint and node.directory == '' then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
return remoteCommand(node, {
|
|
||||||
fn = 'isReadOnly',
|
|
||||||
args = { resolveDir(dir, node) },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.getSize(node, dir)
|
|
||||||
if dir == node.mountPoint and node.directory == '' then
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
return remoteCommand(node, {
|
|
||||||
fn = 'getSize',
|
|
||||||
args = { resolveDir(dir, node) },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.find(node, spec)
|
|
||||||
spec = resolveDir(spec, node)
|
|
||||||
local list = remoteCommand(node, {
|
|
||||||
fn = 'find',
|
|
||||||
args = { spec },
|
|
||||||
})
|
|
||||||
|
|
||||||
for k,f in ipairs(list) do
|
|
||||||
list[k] = fs.combine(node.mountPoint, f)
|
|
||||||
end
|
|
||||||
|
|
||||||
return list
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.move(node, s, t)
|
|
||||||
s = resolveDir(s, node)
|
|
||||||
t = resolveDir(t, node)
|
|
||||||
|
|
||||||
return remoteCommand(node, {
|
|
||||||
fn = 'move',
|
|
||||||
args = { s, t },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function netfs.open(node, fn, fl)
|
|
||||||
fn = resolveDir(fn, node)
|
|
||||||
|
|
||||||
local vfh = remoteCommand(node, {
|
|
||||||
fn = 'open',
|
|
||||||
args = { fn, fl },
|
|
||||||
})
|
|
||||||
|
|
||||||
if vfh then
|
|
||||||
vfh.node = node
|
|
||||||
for _,m in ipairs(vfh.methods) do
|
|
||||||
vfh[m] = function(...)
|
|
||||||
return remoteCommand(node, {
|
|
||||||
fn = 'fileOp',
|
|
||||||
args = { vfh.fileUid, m, ... },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return vfh
|
|
||||||
end
|
|
||||||
|
|
||||||
return netfs
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local ramfs = { }
|
|
||||||
|
|
||||||
function ramfs.mount(dir, nodeType)
|
|
||||||
if nodeType == 'directory' then
|
|
||||||
return {
|
|
||||||
nodes = { },
|
|
||||||
size = 0,
|
|
||||||
}
|
|
||||||
elseif nodeType == 'file' then
|
|
||||||
return {
|
|
||||||
size = 0,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
error('ramfs syntax: [directory, file]')
|
|
||||||
end
|
|
||||||
|
|
||||||
function ramfs.delete(node, dir)
|
|
||||||
if node.mountPoint == dir then
|
|
||||||
fs.unmount(node.mountPoint)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function ramfs.exists(node, fn)
|
|
||||||
return node.mountPoint == fn
|
|
||||||
end
|
|
||||||
|
|
||||||
function ramfs.getSize(node)
|
|
||||||
return node.size
|
|
||||||
end
|
|
||||||
|
|
||||||
function ramfs.isReadOnly()
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function ramfs.makeDir(node, dir)
|
|
||||||
fs.mount(dir, 'ramfs', 'directory')
|
|
||||||
end
|
|
||||||
|
|
||||||
function ramfs.isDir(node)
|
|
||||||
return not not node.nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
function ramfs.getDrive()
|
|
||||||
return 'ram'
|
|
||||||
end
|
|
||||||
|
|
||||||
function ramfs.list(node, dir, full)
|
|
||||||
if node.nodes and node.mountPoint == dir then
|
|
||||||
local files = { }
|
|
||||||
for k,v in pairs(node.nodes) do
|
|
||||||
table.insert(files, k)
|
|
||||||
end
|
|
||||||
return files
|
|
||||||
end
|
|
||||||
error('Not a directory')
|
|
||||||
end
|
|
||||||
|
|
||||||
function ramfs.open(node, fn, fl)
|
|
||||||
|
|
||||||
if fl ~= 'r' and fl ~= 'w' and fl ~= 'rb' and fl ~= 'wb' then
|
|
||||||
error('Unsupported mode')
|
|
||||||
end
|
|
||||||
|
|
||||||
if fl == 'r' then
|
|
||||||
if node.mountPoint ~= fn then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local ctr = 0
|
|
||||||
local lines
|
|
||||||
return {
|
|
||||||
readLine = function()
|
|
||||||
if not lines then
|
|
||||||
lines = Util.split(node.contents)
|
|
||||||
end
|
|
||||||
ctr = ctr + 1
|
|
||||||
return lines[ctr]
|
|
||||||
end,
|
|
||||||
readAll = function()
|
|
||||||
return node.contents
|
|
||||||
end,
|
|
||||||
close = function()
|
|
||||||
lines = nil
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
elseif fl == 'w' then
|
|
||||||
node = fs.mount(fn, 'ramfs', 'file')
|
|
||||||
|
|
||||||
local c = ''
|
|
||||||
return {
|
|
||||||
write = function(str)
|
|
||||||
c = c .. str
|
|
||||||
end,
|
|
||||||
writeLine = function(str)
|
|
||||||
c = c .. str .. '\n'
|
|
||||||
end,
|
|
||||||
flush = function()
|
|
||||||
node.contents = c
|
|
||||||
node.size = #c
|
|
||||||
end,
|
|
||||||
close = function()
|
|
||||||
node.contents = c
|
|
||||||
node.size = #c
|
|
||||||
c = nil
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
elseif fl == 'rb' then
|
|
||||||
if node.mountPoint ~= fn or not node.contents then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local ctr = 0
|
|
||||||
return {
|
|
||||||
read = function()
|
|
||||||
ctr = ctr + 1
|
|
||||||
return node.contents[ctr]
|
|
||||||
end,
|
|
||||||
close = function()
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
|
|
||||||
elseif fl == 'wb' then
|
|
||||||
node = fs.mount(fn, 'ramfs', 'file')
|
|
||||||
|
|
||||||
local c = { }
|
|
||||||
return {
|
|
||||||
write = function(b)
|
|
||||||
table.insert(c, b)
|
|
||||||
end,
|
|
||||||
flush = function()
|
|
||||||
node.contents = c
|
|
||||||
node.size = #c
|
|
||||||
end,
|
|
||||||
close = function()
|
|
||||||
node.contents = c
|
|
||||||
node.size = #c
|
|
||||||
c = nil
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return ramfs
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
local synchronized = require('sync')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local urlfs = { }
|
|
||||||
|
|
||||||
function urlfs.mount(dir, url)
|
|
||||||
if not url then
|
|
||||||
error('URL is required')
|
|
||||||
end
|
|
||||||
return {
|
|
||||||
url = url,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function urlfs.delete(node, dir)
|
|
||||||
fs.unmount(dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
function urlfs.exists()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function urlfs.getSize(node)
|
|
||||||
return node.size or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function urlfs.isReadOnly()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
function urlfs.isDir()
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function urlfs.getDrive()
|
|
||||||
return 'url'
|
|
||||||
end
|
|
||||||
|
|
||||||
function urlfs.open(node, fn, fl)
|
|
||||||
|
|
||||||
if fl == 'w' or fl == 'wb' then
|
|
||||||
fs.delete(fn)
|
|
||||||
return fs.open(fn, fl)
|
|
||||||
end
|
|
||||||
|
|
||||||
if fl ~= 'r' and fl ~= 'rb' then
|
|
||||||
error('Unsupported mode')
|
|
||||||
end
|
|
||||||
|
|
||||||
local c = node.cache
|
|
||||||
if not c then
|
|
||||||
synchronized(node.url, function()
|
|
||||||
c = Util.download(node.url)
|
|
||||||
end)
|
|
||||||
if c then
|
|
||||||
node.cache = c
|
|
||||||
node.size = #c
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not c then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local ctr = 0
|
|
||||||
local lines
|
|
||||||
|
|
||||||
if fl == 'r' then
|
|
||||||
return {
|
|
||||||
readLine = function()
|
|
||||||
if not lines then
|
|
||||||
lines = Util.split(c)
|
|
||||||
end
|
|
||||||
ctr = ctr + 1
|
|
||||||
return lines[ctr]
|
|
||||||
end,
|
|
||||||
readAll = function()
|
|
||||||
return c
|
|
||||||
end,
|
|
||||||
close = function()
|
|
||||||
lines = nil
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
return {
|
|
||||||
read = function()
|
|
||||||
ctr = ctr + 1
|
|
||||||
return c:sub(ctr, ctr):byte()
|
|
||||||
end,
|
|
||||||
close = function()
|
|
||||||
ctr = 0
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
return urlfs
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
local json = require('json')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1'
|
|
||||||
local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s'
|
|
||||||
|
|
||||||
local git = { }
|
|
||||||
|
|
||||||
function git.list(repo)
|
|
||||||
|
|
||||||
local t = Util.split(repo, '(.-)/')
|
|
||||||
|
|
||||||
local user = t[1]
|
|
||||||
local repo = t[2]
|
|
||||||
local branch = t[3] or 'master'
|
|
||||||
|
|
||||||
local dataUrl = string.format(TREE_URL, user, repo, branch)
|
|
||||||
local contents = Util.download(dataUrl)
|
|
||||||
|
|
||||||
if not contents then
|
|
||||||
error('Invalid repository')
|
|
||||||
end
|
|
||||||
|
|
||||||
local data = json.decode(contents)
|
|
||||||
|
|
||||||
if data.message and data.message:find("API rate limit exceeded") then
|
|
||||||
error("Out of API calls, try again later")
|
|
||||||
end
|
|
||||||
|
|
||||||
if data.message and data.message == "Not found" then
|
|
||||||
error("Invalid repository")
|
|
||||||
end
|
|
||||||
|
|
||||||
local list = { }
|
|
||||||
|
|
||||||
for k,v in pairs(data.tree) do
|
|
||||||
if v.type == "blob" then
|
|
||||||
v.path = v.path:gsub("%s","%%20")
|
|
||||||
list[v.path] = {
|
|
||||||
url = string.format(FILE_URL, user, repo, branch, v.path),
|
|
||||||
size = v.size,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return list
|
|
||||||
end
|
|
||||||
|
|
||||||
return git
|
|
||||||
152
sys/apis/gps.lua
152
sys/apis/gps.lua
@@ -1,152 +0,0 @@
|
|||||||
local GPS = { }
|
|
||||||
|
|
||||||
function GPS.locate(timeout, debug)
|
|
||||||
local pt = { }
|
|
||||||
timeout = timeout or 10
|
|
||||||
pt.x, pt.y, pt.z = gps.locate(timeout, debug)
|
|
||||||
if pt.x then
|
|
||||||
return pt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function GPS.isAvailable()
|
|
||||||
return device.wireless_modem and GPS.locate()
|
|
||||||
end
|
|
||||||
|
|
||||||
function GPS.getPoint(timeout, debug)
|
|
||||||
|
|
||||||
local pt = GPS.locate(timeout, debug)
|
|
||||||
if not pt then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
pt.x = math.floor(pt.x)
|
|
||||||
pt.y = math.floor(pt.y)
|
|
||||||
pt.z = math.floor(pt.z)
|
|
||||||
|
|
||||||
if pocket then
|
|
||||||
pt.y = pt.y - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
return pt
|
|
||||||
end
|
|
||||||
|
|
||||||
function GPS.getHeading(timeout)
|
|
||||||
|
|
||||||
if not turtle then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local apt = GPS.locate(timeout)
|
|
||||||
if not apt then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local heading = turtle.point.heading
|
|
||||||
|
|
||||||
while not turtle.forward() do
|
|
||||||
turtle.turnRight()
|
|
||||||
if turtle.getHeading() == heading then
|
|
||||||
printError('GPS.getPoint: Unable to move forward')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local bpt = GPS.locate()
|
|
||||||
if not bpt then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if apt.x < bpt.x then
|
|
||||||
return 0
|
|
||||||
elseif apt.z < bpt.z then
|
|
||||||
return 1
|
|
||||||
elseif apt.x > bpt.x then
|
|
||||||
return 2
|
|
||||||
end
|
|
||||||
return 3
|
|
||||||
end
|
|
||||||
|
|
||||||
function GPS.getPointAndHeading(timeout)
|
|
||||||
local heading = GPS.getHeading(timeout)
|
|
||||||
if heading then
|
|
||||||
local pt = GPS.getPoint()
|
|
||||||
if pt then
|
|
||||||
pt.heading = heading
|
|
||||||
end
|
|
||||||
return pt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- from stock gps API
|
|
||||||
local function trilaterate( A, B, C )
|
|
||||||
local a2b = B.position - A.position
|
|
||||||
local a2c = C.position - A.position
|
|
||||||
|
|
||||||
if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local d = a2b:length()
|
|
||||||
local ex = a2b:normalize( )
|
|
||||||
local i = ex:dot( a2c )
|
|
||||||
local ey = (a2c - (ex * i)):normalize()
|
|
||||||
local j = ey:dot( a2c )
|
|
||||||
local ez = ex:cross( ey )
|
|
||||||
|
|
||||||
local r1 = A.distance
|
|
||||||
local r2 = B.distance
|
|
||||||
local r3 = C.distance
|
|
||||||
|
|
||||||
local x = (r1*r1 - r2*r2 + d*d) / (2*d)
|
|
||||||
local y = (r1*r1 - r3*r3 - x*x + (x-i)*(x-i) + j*j) / (2*j)
|
|
||||||
|
|
||||||
local result = A.position + (ex * x) + (ey * y)
|
|
||||||
|
|
||||||
local zSquared = r1*r1 - x*x - y*y
|
|
||||||
if zSquared > 0 then
|
|
||||||
local z = math.sqrt( zSquared )
|
|
||||||
local result1 = result + (ez * z)
|
|
||||||
local result2 = result - (ez * z)
|
|
||||||
|
|
||||||
local rounded1, rounded2 = result1:round(), result2:round()
|
|
||||||
if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then
|
|
||||||
return rounded1, rounded2
|
|
||||||
else
|
|
||||||
return rounded1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return result:round()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function narrow( p1, p2, fix )
|
|
||||||
local dist1 = math.abs( (p1 - fix.position):length() - fix.distance )
|
|
||||||
local dist2 = math.abs( (p2 - fix.position):length() - fix.distance )
|
|
||||||
|
|
||||||
if math.abs(dist1 - dist2) < 0.05 then
|
|
||||||
return p1, p2
|
|
||||||
elseif dist1 < dist2 then
|
|
||||||
return p1:round()
|
|
||||||
else
|
|
||||||
return p2:round()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
-- end stock gps api
|
|
||||||
|
|
||||||
function GPS.trilaterate(tFixes)
|
|
||||||
local pos1, pos2 = trilaterate(tFixes[1], tFixes[2], tFixes[3])
|
|
||||||
|
|
||||||
if pos2 then
|
|
||||||
pos1, pos2 = narrow(pos1, pos2, tFixes[4])
|
|
||||||
end
|
|
||||||
|
|
||||||
if pos1 and pos2 then
|
|
||||||
print("Ambiguous position")
|
|
||||||
print("Could be "..pos1.x..","..pos1.y..","..pos1.z.." or "..pos2.x..","..pos2.y..","..pos2.z )
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
return pos1
|
|
||||||
end
|
|
||||||
|
|
||||||
return GPS
|
|
||||||
@@ -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,162 +0,0 @@
|
|||||||
local DEFAULT_UPATH = 'https://raw.githubusercontent.com/kepler155c/opus/master/sys/apis'
|
|
||||||
local PASTEBIN_URL = 'http://pastebin.com/raw'
|
|
||||||
local GIT_URL = 'https://raw.githubusercontent.com'
|
|
||||||
|
|
||||||
-- fix broken http get
|
|
||||||
local syncLocks = { }
|
|
||||||
|
|
||||||
local function sync(obj, fn)
|
|
||||||
local key = tostring(obj)
|
|
||||||
if syncLocks[key] then
|
|
||||||
local cos = tostring(coroutine.running())
|
|
||||||
table.insert(syncLocks[key], cos)
|
|
||||||
repeat
|
|
||||||
local _, co = os.pullEvent('sync_lock')
|
|
||||||
until co == cos
|
|
||||||
else
|
|
||||||
syncLocks[key] = { }
|
|
||||||
end
|
|
||||||
local s, m = pcall(fn)
|
|
||||||
local co = table.remove(syncLocks[key], 1)
|
|
||||||
if co then
|
|
||||||
os.queueEvent('sync_lock', co)
|
|
||||||
else
|
|
||||||
syncLocks[key] = nil
|
|
||||||
end
|
|
||||||
if not s then
|
|
||||||
error(m)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function loadUrl(url)
|
|
||||||
local c
|
|
||||||
sync(url, function()
|
|
||||||
local h = http.get(url)
|
|
||||||
if h then
|
|
||||||
c = h.readAll()
|
|
||||||
h.close()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
if c and #c > 0 then
|
|
||||||
return c
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function requireWrapper(env)
|
|
||||||
|
|
||||||
local function standardSearcher(modname, env, shell)
|
|
||||||
if package.loaded[modname] then
|
|
||||||
return function()
|
|
||||||
return package.loaded[modname]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function shellSearcher(modname, env, shell)
|
|
||||||
local fname = modname:gsub('%.', '/') .. '.lua'
|
|
||||||
|
|
||||||
if shell and type(shell.dir) == 'function' then
|
|
||||||
local path = shell.resolve(fname)
|
|
||||||
if fs.exists(path) and not fs.isDir(path) then
|
|
||||||
return loadfile(path, env)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function pathSearcher(modname, env, shell)
|
|
||||||
local fname = modname:gsub('%.', '/') .. '.lua'
|
|
||||||
|
|
||||||
for dir in string.gmatch(package.path, "[^:]+") do
|
|
||||||
local path = fs.combine(dir, fname)
|
|
||||||
if fs.exists(path) and not fs.isDir(path) then
|
|
||||||
return loadfile(path, env)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- require('BniCQPVf')
|
|
||||||
local function pastebinSearcher(modname, env, shell)
|
|
||||||
if #modname == 8 and not modname:match('%W') then
|
|
||||||
local url = PASTEBIN_URL .. '/' .. modname
|
|
||||||
local c = loadUrl(url)
|
|
||||||
if c then
|
|
||||||
return load(c, modname, nil, env)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- require('kepler155c.opus.master.sys.apis.util')
|
|
||||||
local function gitSearcher(modname, env, shell)
|
|
||||||
local fname = modname:gsub('%.', '/') .. '.lua'
|
|
||||||
local _, count = fname:gsub("/", "")
|
|
||||||
if count >= 3 then
|
|
||||||
local url = GIT_URL .. '/' .. fname
|
|
||||||
local c = loadUrl(url)
|
|
||||||
if c then
|
|
||||||
return load(c, modname, nil, env)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function urlSearcher(modname, env, shell)
|
|
||||||
local fname = modname:gsub('%.', '/') .. '.lua'
|
|
||||||
|
|
||||||
if fname:sub(1, 1) ~= '/' then
|
|
||||||
for entry in string.gmatch(package.upath, "[^;]+") do
|
|
||||||
local url = entry .. '/' .. fname
|
|
||||||
local c = loadUrl(url)
|
|
||||||
if c then
|
|
||||||
return load(c, modname, nil, env)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- place package and require function into env
|
|
||||||
package = {
|
|
||||||
path = LUA_PATH or 'sys/apis',
|
|
||||||
upath = LUA_UPATH or DEFAULT_UPATH,
|
|
||||||
config = '/\n:\n?\n!\n-',
|
|
||||||
loaded = {
|
|
||||||
math = math,
|
|
||||||
string = string,
|
|
||||||
table = table,
|
|
||||||
io = io,
|
|
||||||
os = os,
|
|
||||||
},
|
|
||||||
loaders = {
|
|
||||||
standardSearcher,
|
|
||||||
shellSearcher,
|
|
||||||
pathSearcher,
|
|
||||||
pastebinSearcher,
|
|
||||||
gitSearcher,
|
|
||||||
urlSearcher,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function require(modname)
|
|
||||||
|
|
||||||
for _,searcher in ipairs(package.loaders) do
|
|
||||||
local fn, msg = searcher(modname, env, shell)
|
|
||||||
if fn then
|
|
||||||
local module, msg = fn(modname, env)
|
|
||||||
if not module then
|
|
||||||
error(msg or (modname .. ' module returned nil'), 2)
|
|
||||||
end
|
|
||||||
package.loaded[modname] = module
|
|
||||||
return module
|
|
||||||
end
|
|
||||||
if msg then
|
|
||||||
error(msg, 2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
error('Unable to find module ' .. modname)
|
|
||||||
end
|
|
||||||
|
|
||||||
return require -- backwards compatible
|
|
||||||
end
|
|
||||||
|
|
||||||
return function(env)
|
|
||||||
setfenv(requireWrapper, env)
|
|
||||||
return requireWrapper(env)
|
|
||||||
end
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
-- credit ElvishJerricco
|
|
||||||
-- http://pastebin.com/raw.php?i=4nRg9CHU
|
|
||||||
|
|
||||||
local json = { }
|
|
||||||
|
|
||||||
------------------------------------------------------------------ utils
|
|
||||||
local controls = {["\n"]="\\n", ["\r"]="\\r", ["\t"]="\\t", ["\b"]="\\b", ["\f"]="\\f", ["\""]="\\\"", ["\\"]="\\\\"}
|
|
||||||
|
|
||||||
local function isArray(t)
|
|
||||||
local max = 0
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
if type(k) ~= "number" then
|
|
||||||
return false
|
|
||||||
elseif k > max then
|
|
||||||
max = k
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return max == #t
|
|
||||||
end
|
|
||||||
|
|
||||||
local whites = {['\n']=true; ['\r']=true; ['\t']=true; [' ']=true; [',']=true; [':']=true}
|
|
||||||
local function removeWhite(str)
|
|
||||||
while whites[str:sub(1, 1)] do
|
|
||||||
str = str:sub(2)
|
|
||||||
end
|
|
||||||
return str
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------ encoding
|
|
||||||
|
|
||||||
local function encodeCommon(val, pretty, tabLevel, tTracking)
|
|
||||||
local str = ""
|
|
||||||
|
|
||||||
-- Tabbing util
|
|
||||||
local function tab(s)
|
|
||||||
str = str .. ("\t"):rep(tabLevel) .. s
|
|
||||||
end
|
|
||||||
|
|
||||||
local function arrEncoding(val, bracket, closeBracket, iterator, loopFunc)
|
|
||||||
str = str .. bracket
|
|
||||||
if pretty then
|
|
||||||
str = str .. "\n"
|
|
||||||
tabLevel = tabLevel + 1
|
|
||||||
end
|
|
||||||
for k,v in iterator(val) do
|
|
||||||
tab("")
|
|
||||||
loopFunc(k,v)
|
|
||||||
str = str .. ","
|
|
||||||
if pretty then str = str .. "\n" end
|
|
||||||
end
|
|
||||||
if pretty then
|
|
||||||
tabLevel = tabLevel - 1
|
|
||||||
end
|
|
||||||
if str:sub(-2) == ",\n" then
|
|
||||||
str = str:sub(1, -3) .. "\n"
|
|
||||||
elseif str:sub(-1) == "," then
|
|
||||||
str = str:sub(1, -2)
|
|
||||||
end
|
|
||||||
tab(closeBracket)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Table encoding
|
|
||||||
if type(val) == "table" then
|
|
||||||
assert(not tTracking[val], "Cannot encode a table holding itself recursively")
|
|
||||||
tTracking[val] = true
|
|
||||||
if isArray(val) then
|
|
||||||
arrEncoding(val, "[", "]", ipairs, function(k,v)
|
|
||||||
str = str .. encodeCommon(v, pretty, tabLevel, tTracking)
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
arrEncoding(val, "{", "}", pairs, function(k,v)
|
|
||||||
assert(type(k) == "string", "JSON object keys must be strings", 2)
|
|
||||||
str = str .. encodeCommon(k, pretty, tabLevel, tTracking)
|
|
||||||
str = str .. (pretty and ": " or ":") .. encodeCommon(v, pretty, tabLevel, tTracking)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
-- String encoding
|
|
||||||
elseif type(val) == "string" then
|
|
||||||
str = '"' .. val:gsub("[%c\"\\]", controls) .. '"'
|
|
||||||
-- Number encoding
|
|
||||||
elseif type(val) == "number" or type(val) == "boolean" then
|
|
||||||
str = tostring(val)
|
|
||||||
else
|
|
||||||
error("JSON only supports arrays, objects, numbers, booleans, and strings", 2)
|
|
||||||
end
|
|
||||||
return str
|
|
||||||
end
|
|
||||||
|
|
||||||
function json.encode(val)
|
|
||||||
return encodeCommon(val, false, 0, {})
|
|
||||||
end
|
|
||||||
|
|
||||||
function json.encodePretty(val)
|
|
||||||
return encodeCommon(val, true, 0, {})
|
|
||||||
end
|
|
||||||
|
|
||||||
------------------------------------------------------------------ decoding
|
|
||||||
|
|
||||||
local decodeControls = {}
|
|
||||||
for k,v in pairs(controls) do
|
|
||||||
decodeControls[v] = k
|
|
||||||
end
|
|
||||||
|
|
||||||
local function parseBoolean(str)
|
|
||||||
if str:sub(1, 4) == "true" then
|
|
||||||
return true, removeWhite(str:sub(5))
|
|
||||||
else
|
|
||||||
return false, removeWhite(str:sub(6))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function parseNull(str)
|
|
||||||
return nil, removeWhite(str:sub(5))
|
|
||||||
end
|
|
||||||
|
|
||||||
local numChars = {['e']=true; ['E']=true; ['+']=true; ['-']=true; ['.']=true}
|
|
||||||
local function parseNumber(str)
|
|
||||||
local i = 1
|
|
||||||
while numChars[str:sub(i, i)] or tonumber(str:sub(i, i)) do
|
|
||||||
i = i + 1
|
|
||||||
end
|
|
||||||
local val = tonumber(str:sub(1, i - 1))
|
|
||||||
str = removeWhite(str:sub(i))
|
|
||||||
return val, str
|
|
||||||
end
|
|
||||||
|
|
||||||
local function parseString(str)
|
|
||||||
str = str:sub(2)
|
|
||||||
local s = ""
|
|
||||||
while str:sub(1,1) ~= "\"" do
|
|
||||||
local next = str:sub(1,1)
|
|
||||||
str = str:sub(2)
|
|
||||||
assert(next ~= "\n", "Unclosed string")
|
|
||||||
|
|
||||||
if next == "\\" then
|
|
||||||
local escape = str:sub(1,1)
|
|
||||||
str = str:sub(2)
|
|
||||||
|
|
||||||
next = assert(decodeControls[next..escape], "Invalid escape character")
|
|
||||||
end
|
|
||||||
|
|
||||||
s = s .. next
|
|
||||||
end
|
|
||||||
return s, removeWhite(str:sub(2))
|
|
||||||
end
|
|
||||||
|
|
||||||
function json.parseArray(str)
|
|
||||||
str = removeWhite(str:sub(2))
|
|
||||||
|
|
||||||
local val = {}
|
|
||||||
local i = 1
|
|
||||||
while str:sub(1, 1) ~= "]" do
|
|
||||||
local v
|
|
||||||
v, str = json.parseValue(str)
|
|
||||||
val[i] = v
|
|
||||||
i = i + 1
|
|
||||||
str = removeWhite(str)
|
|
||||||
end
|
|
||||||
str = removeWhite(str:sub(2))
|
|
||||||
return val, str
|
|
||||||
end
|
|
||||||
|
|
||||||
function json.parseValue(str)
|
|
||||||
local fchar = str:sub(1, 1)
|
|
||||||
if fchar == "{" then
|
|
||||||
return json.parseObject(str)
|
|
||||||
elseif fchar == "[" then
|
|
||||||
return json.parseArray(str)
|
|
||||||
elseif tonumber(fchar) ~= nil or numChars[fchar] then
|
|
||||||
return parseNumber(str)
|
|
||||||
elseif str:sub(1, 4) == "true" or str:sub(1, 5) == "false" then
|
|
||||||
return parseBoolean(str)
|
|
||||||
elseif fchar == "\"" then
|
|
||||||
return parseString(str)
|
|
||||||
elseif str:sub(1, 4) == "null" then
|
|
||||||
return parseNull(str)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function json.parseMember(str)
|
|
||||||
local k, val
|
|
||||||
k, str = json.parseValue(str)
|
|
||||||
val, str = json.parseValue(str)
|
|
||||||
return k, val, str
|
|
||||||
end
|
|
||||||
|
|
||||||
function json.parseObject(str)
|
|
||||||
str = removeWhite(str:sub(2))
|
|
||||||
|
|
||||||
local val = {}
|
|
||||||
while str:sub(1, 1) ~= "}" do
|
|
||||||
local k, v = nil, nil
|
|
||||||
k, v, str = json.parseMember(str)
|
|
||||||
val[k] = v
|
|
||||||
str = removeWhite(str)
|
|
||||||
end
|
|
||||||
str = removeWhite(str:sub(2))
|
|
||||||
return val, str
|
|
||||||
end
|
|
||||||
|
|
||||||
function json.decode(str)
|
|
||||||
str = removeWhite(str)
|
|
||||||
return json.parseValue(str)
|
|
||||||
end
|
|
||||||
|
|
||||||
function json.decodeFromFile(path)
|
|
||||||
local file = assert(fs.open(path, "r"))
|
|
||||||
local decoded = json.decode(file.readAll())
|
|
||||||
file.close()
|
|
||||||
return decoded
|
|
||||||
end
|
|
||||||
|
|
||||||
return json
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
-- Various assertion function for API methods argument-checking
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
|
|
||||||
-- Dependancies
|
|
||||||
local _PATH = (...):gsub('%.core.assert$','')
|
|
||||||
local Utils = require (_PATH .. '.core.utils')
|
|
||||||
|
|
||||||
-- Local references
|
|
||||||
local lua_type = type
|
|
||||||
local floor = math.floor
|
|
||||||
local concat = table.concat
|
|
||||||
local next = next
|
|
||||||
local pairs = pairs
|
|
||||||
local getmetatable = getmetatable
|
|
||||||
|
|
||||||
-- Is I an integer ?
|
|
||||||
local function isInteger(i)
|
|
||||||
return lua_type(i) ==('number') and (floor(i)==i)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Override lua_type to return integers
|
|
||||||
local function type(v)
|
|
||||||
return isInteger(v) and 'int' or lua_type(v)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Does the given array contents match a predicate type ?
|
|
||||||
local function arrayContentsMatch(t,...)
|
|
||||||
local n_count = Utils.arraySize(t)
|
|
||||||
if n_count < 1 then return false end
|
|
||||||
local init_count = t[0] and 0 or 1
|
|
||||||
local n_count = (t[0] and n_count-1 or n_count)
|
|
||||||
local types = {...}
|
|
||||||
if types then types = concat(types) end
|
|
||||||
for i=init_count,n_count,1 do
|
|
||||||
if not t[i] then return false end
|
|
||||||
if types then
|
|
||||||
if not types:match(type(t[i])) then return false end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Checks if arg is a valid array map
|
|
||||||
local function isMap(m)
|
|
||||||
if not arrayContentsMatch(m, 'table') then return false end
|
|
||||||
local lsize = Utils.arraySize(m[next(m)])
|
|
||||||
for k,v in pairs(m) do
|
|
||||||
if not arrayContentsMatch(m[k], 'string', 'int') then return false end
|
|
||||||
if Utils.arraySize(v)~=lsize then return false end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Checks if s is a valid string map
|
|
||||||
local function isStringMap(s)
|
|
||||||
if lua_type(s) ~= 'string' then return false end
|
|
||||||
local w
|
|
||||||
for row in s:gmatch('[^\n\r]+') do
|
|
||||||
if not row then return false end
|
|
||||||
w = w or #row
|
|
||||||
if w ~= #row then return false end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Does instance derive straight from class
|
|
||||||
local function derives(instance, class)
|
|
||||||
return getmetatable(instance) == class
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Does instance inherits from class
|
|
||||||
local function inherits(instance, class)
|
|
||||||
return (getmetatable(getmetatable(instance)) == class)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Is arg a boolean
|
|
||||||
local function isBoolean(b)
|
|
||||||
return (b==true or b==false)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Is arg nil ?
|
|
||||||
local function isNil(n)
|
|
||||||
return (n==nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function matchType(value, types)
|
|
||||||
return types:match(type(value))
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
arrayContentsMatch = arrayContentsMatch,
|
|
||||||
derives = derives,
|
|
||||||
inherits = inherits,
|
|
||||||
isInteger = isInteger,
|
|
||||||
isBool = isBoolean,
|
|
||||||
isMap = isMap,
|
|
||||||
isStrMap = isStringMap,
|
|
||||||
isOutOfRange = isOutOfRange,
|
|
||||||
isNil = isNil,
|
|
||||||
type = type,
|
|
||||||
matchType = matchType
|
|
||||||
}
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -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,98 +0,0 @@
|
|||||||
--- Heuristic functions for search algorithms.
|
|
||||||
-- A <a href="http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html">distance heuristic</a>
|
|
||||||
-- provides an *estimate of the optimal distance cost* from a given location to a target.
|
|
||||||
-- As such, it guides the pathfinder to the goal, helping it to decide which route is the best.
|
|
||||||
--
|
|
||||||
-- This script holds the definition of some built-in heuristics available through jumper.
|
|
||||||
--
|
|
||||||
-- Distance functions are internally used by the `pathfinder` to evaluate the optimal path
|
|
||||||
-- from the start location to the goal. These functions share the same prototype:
|
|
||||||
-- local function myHeuristic(nodeA, nodeB)
|
|
||||||
-- -- function body
|
|
||||||
-- end
|
|
||||||
-- Jumper features some built-in distance heuristics, namely `MANHATTAN`, `EUCLIDIAN`, `DIAGONAL`, `CARDINTCARD`.
|
|
||||||
-- You can also supply your own heuristic function, following the same template as above.
|
|
||||||
|
|
||||||
|
|
||||||
local abs = math.abs
|
|
||||||
local sqrt = math.sqrt
|
|
||||||
local sqrt2 = sqrt(2)
|
|
||||||
local max, min = math.max, math.min
|
|
||||||
|
|
||||||
local Heuristics = {}
|
|
||||||
--- Manhattan distance.
|
|
||||||
-- <br/>This heuristic is the default one being used by the `pathfinder` object.
|
|
||||||
-- <br/>Evaluates as <code>distance = |dx|+|dy|</code>
|
|
||||||
-- @class function
|
|
||||||
-- @tparam node nodeA a node
|
|
||||||
-- @tparam node nodeB another node
|
|
||||||
-- @treturn number the distance from __nodeA__ to __nodeB__
|
|
||||||
-- @usage
|
|
||||||
-- -- First method
|
|
||||||
-- pathfinder:setHeuristic('MANHATTAN')
|
|
||||||
-- -- Second method
|
|
||||||
-- local Distance = require ('jumper.core.heuristics')
|
|
||||||
-- pathfinder:setHeuristic(Distance.MANHATTAN)
|
|
||||||
function Heuristics.MANHATTAN(nodeA, nodeB)
|
|
||||||
local dx = abs(nodeA._x - nodeB._x)
|
|
||||||
local dy = abs(nodeA._y - nodeB._y)
|
|
||||||
local dz = abs(nodeA._z - nodeB._z)
|
|
||||||
return (dx + dy + dz)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Euclidian distance.
|
|
||||||
-- <br/>Evaluates as <code>distance = squareRoot(dx*dx+dy*dy)</code>
|
|
||||||
-- @class function
|
|
||||||
-- @tparam node nodeA a node
|
|
||||||
-- @tparam node nodeB another node
|
|
||||||
-- @treturn number the distance from __nodeA__ to __nodeB__
|
|
||||||
-- @usage
|
|
||||||
-- -- First method
|
|
||||||
-- pathfinder:setHeuristic('EUCLIDIAN')
|
|
||||||
-- -- Second method
|
|
||||||
-- local Distance = require ('jumper.core.heuristics')
|
|
||||||
-- pathfinder:setHeuristic(Distance.EUCLIDIAN)
|
|
||||||
function Heuristics.EUCLIDIAN(nodeA, nodeB)
|
|
||||||
local dx = nodeA._x - nodeB._x
|
|
||||||
local dy = nodeA._y - nodeB._y
|
|
||||||
local dz = nodeA._z - nodeB._z
|
|
||||||
return sqrt(dx*dx+dy*dy+dz*dz)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Diagonal distance.
|
|
||||||
-- <br/>Evaluates as <code>distance = max(|dx|, abs|dy|)</code>
|
|
||||||
-- @class function
|
|
||||||
-- @tparam node nodeA a node
|
|
||||||
-- @tparam node nodeB another node
|
|
||||||
-- @treturn number the distance from __nodeA__ to __nodeB__
|
|
||||||
-- @usage
|
|
||||||
-- -- First method
|
|
||||||
-- pathfinder:setHeuristic('DIAGONAL')
|
|
||||||
-- -- Second method
|
|
||||||
-- local Distance = require ('jumper.core.heuristics')
|
|
||||||
-- pathfinder:setHeuristic(Distance.DIAGONAL)
|
|
||||||
function Heuristics.DIAGONAL(nodeA, nodeB)
|
|
||||||
local dx = abs(nodeA._x - nodeB._x)
|
|
||||||
local dy = abs(nodeA._y - nodeB._y)
|
|
||||||
return max(dx,dy)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Cardinal/Intercardinal distance.
|
|
||||||
-- <br/>Evaluates as <code>distance = min(dx, dy)*squareRoot(2) + max(dx, dy) - min(dx, dy)</code>
|
|
||||||
-- @class function
|
|
||||||
-- @tparam node nodeA a node
|
|
||||||
-- @tparam node nodeB another node
|
|
||||||
-- @treturn number the distance from __nodeA__ to __nodeB__
|
|
||||||
-- @usage
|
|
||||||
-- -- First method
|
|
||||||
-- pathfinder:setHeuristic('CARDINTCARD')
|
|
||||||
-- -- Second method
|
|
||||||
-- local Distance = require ('jumper.core.heuristics')
|
|
||||||
-- pathfinder:setHeuristic(Distance.CARDINTCARD)
|
|
||||||
function Heuristics.CARDINTCARD(nodeA, nodeB)
|
|
||||||
local dx = abs(nodeA._x - nodeB._x)
|
|
||||||
local dy = abs(nodeA._y - nodeB._y)
|
|
||||||
return min(dx,dy) * sqrt2 + max(dx,dy) - min(dx,dy)
|
|
||||||
end
|
|
||||||
|
|
||||||
return Heuristics
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
local addNode(self, node, nextNode, ed)
|
|
||||||
if not self._pathDB[node] then self._pathDB[node] = {} end
|
|
||||||
self._pathDB[node][ed] = (nextNode == ed and node or nextNode)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Path lookupTable
|
|
||||||
local lookupTable = {}
|
|
||||||
lookupTable.__index = lookupTable
|
|
||||||
|
|
||||||
function lookupTable:new()
|
|
||||||
local lut = {_pathDB = {}}
|
|
||||||
return setmetatable(lut, lookupTable)
|
|
||||||
end
|
|
||||||
|
|
||||||
function lookupTable:addPath(path)
|
|
||||||
local st, ed = path._nodes[1], path._nodes[#path._nodes]
|
|
||||||
for node, count in path:nodes() do
|
|
||||||
local nextNode = path._nodes[count+1]
|
|
||||||
if nextNode then addNode(self, node, nextNode, ed) end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function lookupTable:hasPath(nodeA, nodeB)
|
|
||||||
local found
|
|
||||||
found = self._pathDB[nodeA] and self._path[nodeA][nodeB]
|
|
||||||
if found then return true, true end
|
|
||||||
found = self._pathDB[nodeB] and self._path[nodeB][nodeA]
|
|
||||||
if found then return true, false end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
return lookupTable
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
--- The Node class.
|
|
||||||
-- The `node` represents a cell (or a tile) on a collision map. Basically, for each single cell (tile)
|
|
||||||
-- in the collision map passed-in upon initialization, a `node` object will be generated
|
|
||||||
-- and then cached within the `grid`.
|
|
||||||
--
|
|
||||||
-- In the following implementation, nodes can be compared using the `<` operator. The comparison is
|
|
||||||
-- made with regards of their `f` cost. From a given node being examined, the `pathfinder` will expand the search
|
|
||||||
-- to the next neighbouring node having the lowest `f` cost. See `core.bheap` for more details.
|
|
||||||
--
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
|
|
||||||
local assert = assert
|
|
||||||
|
|
||||||
--- The `Node` class.<br/>
|
|
||||||
-- This class is callable.
|
|
||||||
-- Therefore,_ <code>Node(...)</code> _acts as a shortcut to_ <code>Node:new(...)</code>.
|
|
||||||
-- @type Node
|
|
||||||
local Node = {}
|
|
||||||
Node.__index = Node
|
|
||||||
|
|
||||||
--- Inits a new `node`
|
|
||||||
-- @class function
|
|
||||||
-- @tparam int x the x-coordinate of the node on the collision map
|
|
||||||
-- @tparam int y the y-coordinate of the node on the collision map
|
|
||||||
-- @treturn node a new `node`
|
|
||||||
-- @usage local node = Node(3,4)
|
|
||||||
function Node:new(x,y,z)
|
|
||||||
return setmetatable({_x = x, _y = y, _z = z, _clearance = {}}, Node)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Enables the use of operator '<' to compare nodes.
|
|
||||||
-- Will be used to sort a collection of nodes in a binary heap on the basis of their F-cost
|
|
||||||
function Node.__lt(A,B) return (A._f < B._f) end
|
|
||||||
|
|
||||||
--- Returns x-coordinate of a `node`
|
|
||||||
-- @class function
|
|
||||||
-- @treturn number the x-coordinate of the `node`
|
|
||||||
-- @usage local x = node:getX()
|
|
||||||
function Node:getX() return self._x end
|
|
||||||
|
|
||||||
--- Returns y-coordinate of a `node`
|
|
||||||
-- @class function
|
|
||||||
-- @treturn number the y-coordinate of the `node`
|
|
||||||
-- @usage local y = node:getY()
|
|
||||||
function Node:getY() return self._y end
|
|
||||||
|
|
||||||
function Node:getZ() return self._z end
|
|
||||||
|
|
||||||
--- Returns x and y coordinates of a `node`
|
|
||||||
-- @class function
|
|
||||||
-- @treturn number the x-coordinate of the `node`
|
|
||||||
-- @treturn number the y-coordinate of the `node`
|
|
||||||
-- @usage local x, y = node:getPos()
|
|
||||||
function Node:getPos() return self._x, self._y, self._z end
|
|
||||||
|
|
||||||
--- Returns the amount of true [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric)
|
|
||||||
-- for a given `node`
|
|
||||||
-- @class function
|
|
||||||
-- @tparam string|int|func walkable the value for walkable locations in the collision map array.
|
|
||||||
-- @treturn int the clearance of the `node`
|
|
||||||
-- @usage
|
|
||||||
-- -- Assuming walkable was 0
|
|
||||||
-- local clearance = node:getClearance(0)
|
|
||||||
function Node:getClearance(walkable)
|
|
||||||
return self._clearance[walkable]
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Removes the clearance value for a given walkable.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam string|int|func walkable the value for walkable locations in the collision map array.
|
|
||||||
-- @treturn node self (the calling `node` itself, can be chained)
|
|
||||||
-- @usage
|
|
||||||
-- -- Assuming walkable is defined
|
|
||||||
-- node:removeClearance(walkable)
|
|
||||||
function Node:removeClearance(walkable)
|
|
||||||
self._clearance[walkable] = nil
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Clears temporary cached attributes of a `node`.
|
|
||||||
-- Deletes the attributes cached within a given node after a pathfinding call.
|
|
||||||
-- This function is internally used by the search algorithms, so you should not use it explicitely.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn node self (the calling `node` itself, can be chained)
|
|
||||||
-- @usage
|
|
||||||
-- local thisNode = Node(1,2)
|
|
||||||
-- thisNode:reset()
|
|
||||||
function Node:reset()
|
|
||||||
self._g, self._h, self._f = nil, nil, nil
|
|
||||||
self._opened, self._closed, self._parent = nil, nil, nil
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
return setmetatable(Node,
|
|
||||||
{__call = function(self,...)
|
|
||||||
return Node:new(...)
|
|
||||||
end}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
--- The Path class.
|
|
||||||
-- The `path` class is a structure which represents a path (ordered set of nodes) from a start location to a goal.
|
|
||||||
-- An instance from this class would be a result of a request addressed to `Pathfinder:getPath`.
|
|
||||||
--
|
|
||||||
-- This module is internally used by the library on purpose.
|
|
||||||
-- It should normally not be used explicitely, yet it remains fully accessible.
|
|
||||||
--
|
|
||||||
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
|
|
||||||
-- Dependencies
|
|
||||||
local _PATH = (...):match('(.+)%.path$')
|
|
||||||
local Heuristic = require (_PATH .. '.heuristics')
|
|
||||||
|
|
||||||
-- Local references
|
|
||||||
local abs, max = math.abs, math.max
|
|
||||||
local t_insert, t_remove = table.insert, table.remove
|
|
||||||
|
|
||||||
--- The `Path` class.<br/>
|
|
||||||
-- This class is callable.
|
|
||||||
-- Therefore, <em><code>Path(...)</code></em> acts as a shortcut to <em><code>Path:new(...)</code></em>.
|
|
||||||
-- @type Path
|
|
||||||
local Path = {}
|
|
||||||
Path.__index = Path
|
|
||||||
|
|
||||||
--- Inits a new `path`.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn path a `path`
|
|
||||||
-- @usage local p = Path()
|
|
||||||
function Path:new()
|
|
||||||
return setmetatable({_nodes = {}}, Path)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Iterates on each single `node` along a `path`. At each step of iteration,
|
|
||||||
-- returns the `node` plus a count value. Aliased as @{Path:nodes}
|
|
||||||
-- @class function
|
|
||||||
-- @treturn node a `node`
|
|
||||||
-- @treturn int the count for the number of nodes
|
|
||||||
-- @see Path:nodes
|
|
||||||
-- @usage
|
|
||||||
-- for node, count in p:iter() do
|
|
||||||
-- ...
|
|
||||||
-- end
|
|
||||||
function Path:iter()
|
|
||||||
local i,pathLen = 1,#self._nodes
|
|
||||||
return function()
|
|
||||||
if self._nodes[i] then
|
|
||||||
i = i+1
|
|
||||||
return self._nodes[i-1],i-1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Iterates on each single `node` along a `path`. At each step of iteration,
|
|
||||||
-- returns a `node` plus a count value. Alias for @{Path:iter}
|
|
||||||
-- @class function
|
|
||||||
-- @name Path:nodes
|
|
||||||
-- @treturn node a `node`
|
|
||||||
-- @treturn int the count for the number of nodes
|
|
||||||
-- @see Path:iter
|
|
||||||
-- @usage
|
|
||||||
-- for node, count in p:nodes() do
|
|
||||||
-- ...
|
|
||||||
-- end
|
|
||||||
Path.nodes = Path.iter
|
|
||||||
|
|
||||||
--- Evaluates the `path` length
|
|
||||||
-- @class function
|
|
||||||
-- @treturn number the `path` length
|
|
||||||
-- @usage local len = p:getLength()
|
|
||||||
function Path:getLength()
|
|
||||||
local len = 0
|
|
||||||
for i = 2,#self._nodes do
|
|
||||||
len = len + Heuristic.EUCLIDIAN(self._nodes[i], self._nodes[i-1])
|
|
||||||
end
|
|
||||||
return len
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Counts the number of steps.
|
|
||||||
-- Returns the number of waypoints (nodes) in the current path.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam node node a node to be added to the path
|
|
||||||
-- @tparam[opt] int index the index at which the node will be inserted. If omitted, the node will be appended after the last node in the path.
|
|
||||||
-- @treturn path self (the calling `path` itself, can be chained)
|
|
||||||
-- @usage local nSteps = p:countSteps()
|
|
||||||
function Path:addNode(node, index)
|
|
||||||
index = index or #self._nodes+1
|
|
||||||
t_insert(self._nodes, index, node)
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
--- `Path` filling modifier. Interpolates between non contiguous nodes along a `path`
|
|
||||||
-- to build a fully continuous `path`. This maybe useful when using search algorithms such as Jump Point Search.
|
|
||||||
-- Does the opposite of @{Path:filter}
|
|
||||||
-- @class function
|
|
||||||
-- @treturn path self (the calling `path` itself, can be chained)
|
|
||||||
-- @see Path:filter
|
|
||||||
-- @usage p:fill()
|
|
||||||
function Path:fill()
|
|
||||||
local i = 2
|
|
||||||
local xi,yi,dx,dy
|
|
||||||
local N = #self._nodes
|
|
||||||
local incrX, incrY
|
|
||||||
while true do
|
|
||||||
xi,yi = self._nodes[i]._x,self._nodes[i]._y
|
|
||||||
dx,dy = xi-self._nodes[i-1]._x,yi-self._nodes[i-1]._y
|
|
||||||
if (abs(dx) > 1 or abs(dy) > 1) then
|
|
||||||
incrX = dx/max(abs(dx),1)
|
|
||||||
incrY = dy/max(abs(dy),1)
|
|
||||||
t_insert(self._nodes, i, self._grid:getNodeAt(self._nodes[i-1]._x + incrX, self._nodes[i-1]._y +incrY))
|
|
||||||
N = N+1
|
|
||||||
else i=i+1
|
|
||||||
end
|
|
||||||
if i>N then break end
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- `Path` compression modifier. Given a `path`, eliminates useless nodes to return a lighter `path`
|
|
||||||
-- consisting of straight moves. Does the opposite of @{Path:fill}
|
|
||||||
-- @class function
|
|
||||||
-- @treturn path self (the calling `path` itself, can be chained)
|
|
||||||
-- @see Path:fill
|
|
||||||
-- @usage p:filter()
|
|
||||||
function Path:filter()
|
|
||||||
local i = 2
|
|
||||||
local xi,yi,dx,dy, olddx, olddy
|
|
||||||
xi,yi = self._nodes[i]._x, self._nodes[i]._y
|
|
||||||
dx, dy = xi - self._nodes[i-1]._x, yi-self._nodes[i-1]._y
|
|
||||||
while true do
|
|
||||||
olddx, olddy = dx, dy
|
|
||||||
if self._nodes[i+1] then
|
|
||||||
i = i+1
|
|
||||||
xi, yi = self._nodes[i]._x, self._nodes[i]._y
|
|
||||||
dx, dy = xi - self._nodes[i-1]._x, yi - self._nodes[i-1]._y
|
|
||||||
if olddx == dx and olddy == dy then
|
|
||||||
t_remove(self._nodes, i-1)
|
|
||||||
i = i - 1
|
|
||||||
end
|
|
||||||
else break end
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Clones a `path`.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn path a `path`
|
|
||||||
-- @usage local p = path:clone()
|
|
||||||
function Path:clone()
|
|
||||||
local p = Path:new()
|
|
||||||
for node in self:nodes() do p:addNode(node) end
|
|
||||||
return p
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Checks if a `path` is equal to another. It also supports *filtered paths* (see @{Path:filter}).
|
|
||||||
-- @class function
|
|
||||||
-- @tparam path p2 a path
|
|
||||||
-- @treturn boolean a boolean
|
|
||||||
-- @usage print(myPath:isEqualTo(anotherPath))
|
|
||||||
function Path:isEqualTo(p2)
|
|
||||||
local p1 = self:clone():filter()
|
|
||||||
local p2 = p2:clone():filter()
|
|
||||||
for node, count in p1:nodes() do
|
|
||||||
if not p2._nodes[count] then return false end
|
|
||||||
local n = p2._nodes[count]
|
|
||||||
if n._x~=node._x or n._y~=node._y then return false end
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Reverses a `path`.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn path self (the calling `path` itself, can be chained)
|
|
||||||
-- @usage myPath:reverse()
|
|
||||||
function Path:reverse()
|
|
||||||
local _nodes = {}
|
|
||||||
for i = #self._nodes,1,-1 do
|
|
||||||
_nodes[#_nodes+1] = self._nodes[i]
|
|
||||||
end
|
|
||||||
self._nodes = _nodes
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Appends a given `path` to self.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam path p a path
|
|
||||||
-- @treturn path self (the calling `path` itself, can be chained)
|
|
||||||
-- @usage myPath:append(anotherPath)
|
|
||||||
function Path:append(p)
|
|
||||||
for node in p:nodes() do self:addNode(node) end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
return setmetatable(Path,
|
|
||||||
{__call = function(self,...)
|
|
||||||
return Path:new(...)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
end
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
-- Various utilities for Jumper top-level modules
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
|
|
||||||
-- Dependencies
|
|
||||||
local _PATH = (...):gsub('%.utils$','')
|
|
||||||
local Path = require (_PATH .. '.path')
|
|
||||||
local Node = require (_PATH .. '.node')
|
|
||||||
|
|
||||||
-- Local references
|
|
||||||
local pairs = pairs
|
|
||||||
local type = type
|
|
||||||
local t_insert = table.insert
|
|
||||||
local assert = assert
|
|
||||||
local coroutine = coroutine
|
|
||||||
|
|
||||||
-- Raw array items count
|
|
||||||
local function arraySize(t)
|
|
||||||
local count = 0
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
count = count+1
|
|
||||||
end
|
|
||||||
return count
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Parses a string map and builds an array map
|
|
||||||
local function stringMapToArray(str)
|
|
||||||
local map = {}
|
|
||||||
local w, h
|
|
||||||
for line in str:gmatch('[^\n\r]+') do
|
|
||||||
if line then
|
|
||||||
w = not w and #line or w
|
|
||||||
assert(#line == w, 'Error parsing map, rows must have the same size!')
|
|
||||||
h = (h or 0) + 1
|
|
||||||
map[h] = {}
|
|
||||||
for char in line:gmatch('.') do
|
|
||||||
map[h][#map[h]+1] = char
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return map
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Collects and returns the keys of a given array
|
|
||||||
local function getKeys(t)
|
|
||||||
local keys = {}
|
|
||||||
for k,v in pairs(t) do keys[#keys+1] = k end
|
|
||||||
return keys
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Calculates the bounds of a 2d array
|
|
||||||
local function getArrayBounds(map)
|
|
||||||
local min_x, max_x
|
|
||||||
local min_y, max_y
|
|
||||||
for y in pairs(map) do
|
|
||||||
min_y = not min_y and y or (y<min_y and y or min_y)
|
|
||||||
max_y = not max_y and y or (y>max_y and y or max_y)
|
|
||||||
for x in pairs(map[y]) do
|
|
||||||
min_x = not min_x and x or (x<min_x and x or min_x)
|
|
||||||
max_x = not max_x and x or (x>max_x and x or max_x)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return min_x,max_x,min_y,max_y
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Converts an array to a set of nodes
|
|
||||||
local function arrayToNodes(map)
|
|
||||||
local min_x, max_x
|
|
||||||
local min_y, max_y
|
|
||||||
local min_z, max_z
|
|
||||||
local nodes = {}
|
|
||||||
for y in pairs(map) do
|
|
||||||
min_y = not min_y and y or (y<min_y and y or min_y)
|
|
||||||
max_y = not max_y and y or (y>max_y and y or max_y)
|
|
||||||
nodes[y] = {}
|
|
||||||
for x in pairs(map[y]) do
|
|
||||||
min_x = not min_x and x or (x<min_x and x or min_x)
|
|
||||||
max_x = not max_x and x or (x>max_x and x or max_x)
|
|
||||||
nodes[y][x] = {}
|
|
||||||
for z in pairs(map[y][x]) do
|
|
||||||
min_z = not min_z and z or (z<min_z and z or min_z)
|
|
||||||
max_z = not max_z and z or (z>max_z and z or max_z)
|
|
||||||
nodes[y][x][z] = Node:new(x,y,z)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return nodes,
|
|
||||||
(min_x or 0), (max_x or 0),
|
|
||||||
(min_y or 0), (max_y or 0),
|
|
||||||
(min_z or 0), (max_z or 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Iterator, wrapped within a coroutine
|
|
||||||
-- Iterates around a given position following the outline of a square
|
|
||||||
local function around()
|
|
||||||
local iterf = function(x0, y0, z0, s)
|
|
||||||
local x, y, z = x0-s, y0-s, z0-s
|
|
||||||
coroutine.yield(x, y, z)
|
|
||||||
repeat
|
|
||||||
x = x + 1
|
|
||||||
coroutine.yield(x,y,z)
|
|
||||||
until x == x0+s
|
|
||||||
repeat
|
|
||||||
y = y + 1
|
|
||||||
coroutine.yield(x,y,z)
|
|
||||||
until y == y0 + s
|
|
||||||
repeat
|
|
||||||
z = z + 1
|
|
||||||
coroutine.yield(x,y,z)
|
|
||||||
until z == z0 + s
|
|
||||||
repeat
|
|
||||||
x = x - 1
|
|
||||||
coroutine.yield(x, y,z)
|
|
||||||
until x == x0-s
|
|
||||||
repeat
|
|
||||||
y = y - 1
|
|
||||||
coroutine.yield(x,y,z)
|
|
||||||
until y == y0-s+1
|
|
||||||
repeat
|
|
||||||
z = z - 1
|
|
||||||
coroutine.yield(x,y,z)
|
|
||||||
until z == z0-s+1
|
|
||||||
end
|
|
||||||
return coroutine.create(iterf)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Extract a path from a given start/end position
|
|
||||||
local function traceBackPath(finder, node, startNode)
|
|
||||||
local path = Path:new()
|
|
||||||
path._grid = finder._grid
|
|
||||||
while true do
|
|
||||||
if node._parent then
|
|
||||||
t_insert(path._nodes,1,node)
|
|
||||||
node = node._parent
|
|
||||||
else
|
|
||||||
t_insert(path._nodes,1,startNode)
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Lookup for value in a table
|
|
||||||
local indexOf = function(t,v)
|
|
||||||
for i = 1,#t do
|
|
||||||
if t[i] == v then return i end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Is i out of range
|
|
||||||
local function outOfRange(i,low,up)
|
|
||||||
return (i< low or i > up)
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
arraySize = arraySize,
|
|
||||||
getKeys = getKeys,
|
|
||||||
indexOf = indexOf,
|
|
||||||
outOfRange = outOfRange,
|
|
||||||
getArrayBounds = getArrayBounds,
|
|
||||||
arrayToNodes = arrayToNodes,
|
|
||||||
strToMap = stringMapToArray,
|
|
||||||
around = around,
|
|
||||||
drAround = drAround,
|
|
||||||
traceBackPath = traceBackPath
|
|
||||||
}
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,429 +0,0 @@
|
|||||||
--- The Grid class.
|
|
||||||
-- Implementation of the `grid` class.
|
|
||||||
-- The `grid` is a implicit graph which represents the 2D
|
|
||||||
-- world map layout on which the `pathfinder` object will run.
|
|
||||||
-- During a search, the `pathfinder` object needs to save some critical values. These values are cached within each `node`
|
|
||||||
-- object, and the whole set of nodes are tight inside the `grid` object itself.
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
|
|
||||||
-- Dependencies
|
|
||||||
local _PATH = (...):gsub('%.grid$','')
|
|
||||||
|
|
||||||
-- Local references
|
|
||||||
local Utils = require (_PATH .. '.core.utils')
|
|
||||||
local Assert = require (_PATH .. '.core.assert')
|
|
||||||
local Node = require (_PATH .. '.core.node')
|
|
||||||
|
|
||||||
-- Local references
|
|
||||||
local pairs = pairs
|
|
||||||
local assert = assert
|
|
||||||
local next = next
|
|
||||||
local setmetatable = setmetatable
|
|
||||||
local floor = math.floor
|
|
||||||
local coroutine = coroutine
|
|
||||||
|
|
||||||
-- Offsets for straights moves
|
|
||||||
local straightOffsets = {
|
|
||||||
{x = 1, y = 0, z = 0} --[[W]], {x = -1, y = 0, z = 0}, --[[E]]
|
|
||||||
{x = 0, y = 1, z = 0} --[[S]], {x = 0, y = -1, z = 0}, --[[N]]
|
|
||||||
{x = 0, y = 0, z = 1} --[[U]], {x = 0, y = -0, z = -1}, --[[D]]
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Offsets for diagonal moves
|
|
||||||
local diagonalOffsets = {
|
|
||||||
{x = -1, y = -1} --[[NW]], {x = 1, y = -1}, --[[NE]]
|
|
||||||
{x = -1, y = 1} --[[SW]], {x = 1, y = 1}, --[[SE]]
|
|
||||||
}
|
|
||||||
|
|
||||||
--- The `Grid` class.<br/>
|
|
||||||
-- This class is callable.
|
|
||||||
-- Therefore,_ <code>Grid(...)</code> _acts as a shortcut to_ <code>Grid:new(...)</code>.
|
|
||||||
-- @type Grid
|
|
||||||
local Grid = {}
|
|
||||||
Grid.__index = Grid
|
|
||||||
|
|
||||||
-- Specialized grids
|
|
||||||
local PreProcessGrid = setmetatable({},Grid)
|
|
||||||
local PostProcessGrid = setmetatable({},Grid)
|
|
||||||
PreProcessGrid.__index = PreProcessGrid
|
|
||||||
PostProcessGrid.__index = PostProcessGrid
|
|
||||||
PreProcessGrid.__call = function (self,x,y,z)
|
|
||||||
return self:getNodeAt(x,y,z)
|
|
||||||
end
|
|
||||||
PostProcessGrid.__call = function (self,x,y,z,create)
|
|
||||||
if create then return self:getNodeAt(x,y,z) end
|
|
||||||
return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z]
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Inits a new `grid`
|
|
||||||
-- @class function
|
|
||||||
-- @tparam table|string map A collision map - (2D array) with consecutive indices (starting at 0 or 1)
|
|
||||||
-- or a `string` with line-break chars (<code>\n</code> or <code>\r</code>) as row delimiters.
|
|
||||||
-- @tparam[opt] bool cacheNodeAtRuntime When __true__, returns an empty `grid` instance, so that
|
|
||||||
-- later on, indexing a non-cached `node` will cause it to be created and cache within the `grid` on purpose (i.e, when needed).
|
|
||||||
-- This is a __memory-safe__ option, in case your dealing with some tight memory constraints.
|
|
||||||
-- Defaults to __false__ when omitted.
|
|
||||||
-- @treturn grid a new `grid` instance
|
|
||||||
-- @usage
|
|
||||||
-- -- A simple 3x3 grid
|
|
||||||
-- local myGrid = Grid:new({{0,0,0},{0,0,0},{0,0,0}})
|
|
||||||
--
|
|
||||||
-- -- A memory-safe 3x3 grid
|
|
||||||
-- myGrid = Grid('000\n000\n000', true)
|
|
||||||
function Grid:new(map, cacheNodeAtRuntime)
|
|
||||||
if type(map) == 'string' then
|
|
||||||
assert(Assert.isStrMap(map), 'Wrong argument #1. Not a valid string map')
|
|
||||||
map = Utils.strToMap(map)
|
|
||||||
end
|
|
||||||
--assert(Assert.isMap(map),('Bad argument #1. Not a valid map'))
|
|
||||||
assert(Assert.isBool(cacheNodeAtRuntime) or Assert.isNil(cacheNodeAtRuntime),
|
|
||||||
('Bad argument #2. Expected \'boolean\', got %s.'):format(type(cacheNodeAtRuntime)))
|
|
||||||
if cacheNodeAtRuntime then
|
|
||||||
return PostProcessGrid:new(map,walkable)
|
|
||||||
end
|
|
||||||
return PreProcessGrid:new(map,walkable)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Checks if `node` at [x,y] is __walkable__.
|
|
||||||
-- Will check if `node` at location [x,y] both *exists* on the collision map and *is walkable*
|
|
||||||
-- @class function
|
|
||||||
-- @tparam int x the x-location of the node
|
|
||||||
-- @tparam int y the y-location of the node
|
|
||||||
-- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}).
|
|
||||||
-- Defaults to __false__ when omitted.
|
|
||||||
-- If this parameter is a function, it should be prototyped as __f(value)__ and return a `boolean`:
|
|
||||||
-- __true__ when value matches a __walkable__ `node`, __false__ otherwise. If this parameter is not given
|
|
||||||
-- while location [x,y] __is valid__, this actual function returns __true__.
|
|
||||||
-- @tparam[optchain] int clearance the amount of clearance needed. Defaults to 1 (normal clearance) when not given.
|
|
||||||
-- @treturn bool __true__ if `node` exists and is __walkable__, __false__ otherwise
|
|
||||||
-- @usage
|
|
||||||
-- -- Always true
|
|
||||||
-- print(myGrid:isWalkableAt(2,3))
|
|
||||||
--
|
|
||||||
-- -- True if node at [2,3] collision map value is 0
|
|
||||||
-- print(myGrid:isWalkableAt(2,3,0))
|
|
||||||
--
|
|
||||||
-- -- True if node at [2,3] collision map value is 0 and has a clearance higher or equal to 2
|
|
||||||
-- print(myGrid:isWalkableAt(2,3,0,2))
|
|
||||||
--
|
|
||||||
function Grid:isWalkableAt(x, y, z, walkable, clearance)
|
|
||||||
local nodeValue = self._map[y] and self._map[y][x] and self._map[y][x][z]
|
|
||||||
if nodeValue then
|
|
||||||
if not walkable then return true end
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
local hasEnoughClearance = not clearance and true or false
|
|
||||||
if not hasEnoughClearance then
|
|
||||||
if not self._isAnnotated[walkable] then return false end
|
|
||||||
local node = self:getNodeAt(x,y,z)
|
|
||||||
local nodeClearance = node:getClearance(walkable)
|
|
||||||
hasEnoughClearance = (nodeClearance >= clearance)
|
|
||||||
end
|
|
||||||
if self._eval then
|
|
||||||
return walkable(nodeValue) and hasEnoughClearance
|
|
||||||
end
|
|
||||||
return ((nodeValue == walkable) and hasEnoughClearance)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the `grid` width.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn int the `grid` width
|
|
||||||
-- @usage print(myGrid:getWidth())
|
|
||||||
function Grid:getWidth()
|
|
||||||
return self._width
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the `grid` height.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn int the `grid` height
|
|
||||||
-- @usage print(myGrid:getHeight())
|
|
||||||
function Grid:getHeight()
|
|
||||||
return self._height
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the collision map.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn map the collision map (see @{Grid:new})
|
|
||||||
-- @usage local map = myGrid:getMap()
|
|
||||||
function Grid:getMap()
|
|
||||||
return self._map
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the set of nodes.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn {{node,...},...} an array of nodes
|
|
||||||
-- @usage local nodes = myGrid:getNodes()
|
|
||||||
function Grid:getNodes()
|
|
||||||
return self._nodes
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the `grid` bounds. Returned values corresponds to the upper-left
|
|
||||||
-- and lower-right coordinates (in tile units) of the actual `grid` instance.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn int the upper-left corner x-coordinate
|
|
||||||
-- @treturn int the upper-left corner y-coordinate
|
|
||||||
-- @treturn int the lower-right corner x-coordinate
|
|
||||||
-- @treturn int the lower-right corner y-coordinate
|
|
||||||
-- @usage local left_x, left_y, right_x, right_y = myGrid:getBounds()
|
|
||||||
function Grid:getBounds()
|
|
||||||
return self._min_x, self._min_y, self._min_z, self._max_x, self._max_y, self._max_z
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam node node a given `node`
|
|
||||||
-- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}).
|
|
||||||
-- Defaults to __false__ when omitted.
|
|
||||||
-- @tparam[optchain] bool allowDiagonal when __true__, allows adjacent nodes are included (8-neighbours).
|
|
||||||
-- Defaults to __false__ when omitted.
|
|
||||||
-- @tparam[optchain] bool tunnel When __true__, allows the `pathfinder` to tunnel through walls when heading diagonally.
|
|
||||||
-- @tparam[optchain] int clearance When given, will prune for the neighbours set all nodes having a clearance value lower than the passed-in value
|
|
||||||
-- Defaults to __false__ when omitted.
|
|
||||||
-- @treturn {node,...} an array of nodes neighbouring a given node
|
|
||||||
-- @usage
|
|
||||||
-- local aNode = myGrid:getNodeAt(5,6)
|
|
||||||
-- local neighbours = myGrid:getNeighbours(aNode, 0, true)
|
|
||||||
function Grid:getNeighbours(node, walkable, allowDiagonal, tunnel, clearance)
|
|
||||||
local neighbours = {}
|
|
||||||
for i = 1,#straightOffsets do
|
|
||||||
local n = self:getNodeAt(
|
|
||||||
node._x + straightOffsets[i].x,
|
|
||||||
node._y + straightOffsets[i].y,
|
|
||||||
node._z + straightOffsets[i].z
|
|
||||||
)
|
|
||||||
if n and self:isWalkableAt(n._x, n._y, n._z, walkable, clearance) then
|
|
||||||
neighbours[#neighbours+1] = n
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not allowDiagonal then return neighbours end
|
|
||||||
|
|
||||||
tunnel = not not tunnel
|
|
||||||
for i = 1,#diagonalOffsets do
|
|
||||||
local n = self:getNodeAt(
|
|
||||||
node._x + diagonalOffsets[i].x,
|
|
||||||
node._y + diagonalOffsets[i].y
|
|
||||||
)
|
|
||||||
if n and self:isWalkableAt(n._x, n._y, walkable, clearance) then
|
|
||||||
if tunnel then
|
|
||||||
neighbours[#neighbours+1] = n
|
|
||||||
else
|
|
||||||
local skipThisNode = false
|
|
||||||
local n1 = self:getNodeAt(node._x+diagonalOffsets[i].x, node._y)
|
|
||||||
local n2 = self:getNodeAt(node._x, node._y+diagonalOffsets[i].y)
|
|
||||||
if ((n1 and n2) and not self:isWalkableAt(n1._x, n1._y, walkable, clearance) and not self:isWalkableAt(n2._x, n2._y, walkable, clearance)) then
|
|
||||||
skipThisNode = true
|
|
||||||
end
|
|
||||||
if not skipThisNode then neighbours[#neighbours+1] = n end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return neighbours
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Grid iterator. Iterates on every single node
|
|
||||||
-- in the `grid`. Passing __lx, ly, ex, ey__ arguments will iterate
|
|
||||||
-- only on nodes inside the bounding-rectangle delimited by those given coordinates.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam[opt] int lx the leftmost x-coordinate of the rectangle. Default to the `grid` leftmost x-coordinate (see @{Grid:getBounds}).
|
|
||||||
-- @tparam[optchain] int ly the topmost y-coordinate of the rectangle. Default to the `grid` topmost y-coordinate (see @{Grid:getBounds}).
|
|
||||||
-- @tparam[optchain] int ex the rightmost x-coordinate of the rectangle. Default to the `grid` rightmost x-coordinate (see @{Grid:getBounds}).
|
|
||||||
-- @tparam[optchain] int ey the bottom-most y-coordinate of the rectangle. Default to the `grid` bottom-most y-coordinate (see @{Grid:getBounds}).
|
|
||||||
-- @treturn node a `node` on the collision map, upon each iteration step
|
|
||||||
-- @treturn int the iteration count
|
|
||||||
-- @usage
|
|
||||||
-- for node, count in myGrid:iter() do
|
|
||||||
-- print(node:getX(), node:getY(), count)
|
|
||||||
-- end
|
|
||||||
function Grid:iter(lx,ly,lz,ex,ey,ez)
|
|
||||||
local min_x = lx or self._min_x
|
|
||||||
local min_y = ly or self._min_y
|
|
||||||
local min_z = lz or self._min_z
|
|
||||||
local max_x = ex or self._max_x
|
|
||||||
local max_y = ey or self._max_y
|
|
||||||
local max_z = ez or self._max_z
|
|
||||||
|
|
||||||
local x, y, z
|
|
||||||
z = min_z
|
|
||||||
return function()
|
|
||||||
x = not x and min_x or x+1
|
|
||||||
if x > max_x then
|
|
||||||
x = min_x
|
|
||||||
y = y+1
|
|
||||||
end
|
|
||||||
y = not y and min_y or y+1
|
|
||||||
if y > max_y then
|
|
||||||
y = min_y
|
|
||||||
z = z+1
|
|
||||||
end
|
|
||||||
if z > max_z then
|
|
||||||
z = nil
|
|
||||||
end
|
|
||||||
return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z] or self:getNodeAt(x,y,z)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Grid iterator. Iterates on each node along the outline (border) of a squared area
|
|
||||||
-- centered on the given node.
|
|
||||||
-- @tparam node node a given `node`
|
|
||||||
-- @tparam[opt] int radius the area radius (half-length). Defaults to __1__ when not given.
|
|
||||||
-- @treturn node a `node` at each iteration step
|
|
||||||
-- @usage
|
|
||||||
-- for node in myGrid:around(node, 2) do
|
|
||||||
-- ...
|
|
||||||
-- end
|
|
||||||
function Grid:around(node, radius)
|
|
||||||
local x, y, z = node._x, node._y, node._z
|
|
||||||
radius = radius or 1
|
|
||||||
local _around = Utils.around()
|
|
||||||
local _nodes = {}
|
|
||||||
repeat
|
|
||||||
local state, x, y, z = coroutine.resume(_around,x,y,z,radius)
|
|
||||||
local nodeAt = state and self:getNodeAt(x, y, z)
|
|
||||||
if nodeAt then _nodes[#_nodes+1] = nodeAt end
|
|
||||||
until (not state)
|
|
||||||
local _i = 0
|
|
||||||
return function()
|
|
||||||
_i = _i+1
|
|
||||||
return _nodes[_i]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Each transformation. Calls the given function on each `node` in the `grid`,
|
|
||||||
-- passing the `node` as the first argument to function __f__.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam func f a function prototyped as __f(node,...)__
|
|
||||||
-- @tparam[opt] vararg ... args to be passed to function __f__
|
|
||||||
-- @treturn grid self (the calling `grid` itself, can be chained)
|
|
||||||
-- @usage
|
|
||||||
-- local function printNode(node)
|
|
||||||
-- print(node:getX(), node:getY())
|
|
||||||
-- end
|
|
||||||
-- myGrid:each(printNode)
|
|
||||||
function Grid:each(f,...)
|
|
||||||
for node in self:iter() do f(node,...) end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Each (in range) transformation. Calls a function on each `node` in the range of a rectangle of cells,
|
|
||||||
-- passing the `node` as the first argument to function __f__.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam int lx the leftmost x-coordinate coordinate of the rectangle
|
|
||||||
-- @tparam int ly the topmost y-coordinate of the rectangle
|
|
||||||
-- @tparam int ex the rightmost x-coordinate of the rectangle
|
|
||||||
-- @tparam int ey the bottom-most y-coordinate of the rectangle
|
|
||||||
-- @tparam func f a function prototyped as __f(node,...)__
|
|
||||||
-- @tparam[opt] vararg ... args to be passed to function __f__
|
|
||||||
-- @treturn grid self (the calling `grid` itself, can be chained)
|
|
||||||
-- @usage
|
|
||||||
-- local function printNode(node)
|
|
||||||
-- print(node:getX(), node:getY())
|
|
||||||
-- end
|
|
||||||
-- myGrid:eachRange(1,1,8,8,printNode)
|
|
||||||
function Grid:eachRange(lx,ly,ex,ey,f,...)
|
|
||||||
for node in self:iter(lx,ly,ex,ey) do f(node,...) end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Map transformation.
|
|
||||||
-- Calls function __f(node,...)__ on each `node` in a given range, passing the `node` as the first arg to function __f__ and replaces
|
|
||||||
-- it with the returned value. Therefore, the function should return a `node`.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam func f a function prototyped as __f(node,...)__
|
|
||||||
-- @tparam[opt] vararg ... args to be passed to function __f__
|
|
||||||
-- @treturn grid self (the calling `grid` itself, can be chained)
|
|
||||||
-- @usage
|
|
||||||
-- local function nothing(node)
|
|
||||||
-- return node
|
|
||||||
-- end
|
|
||||||
-- myGrid:imap(nothing)
|
|
||||||
function Grid:imap(f,...)
|
|
||||||
for node in self:iter() do
|
|
||||||
node = f(node,...)
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Map in range transformation.
|
|
||||||
-- Calls function __f(node,...)__ on each `node` in a rectangle range, passing the `node` as the first argument to the function and replaces
|
|
||||||
-- it with the returned value. Therefore, the function should return a `node`.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam int lx the leftmost x-coordinate coordinate of the rectangle
|
|
||||||
-- @tparam int ly the topmost y-coordinate of the rectangle
|
|
||||||
-- @tparam int ex the rightmost x-coordinate of the rectangle
|
|
||||||
-- @tparam int ey the bottom-most y-coordinate of the rectangle
|
|
||||||
-- @tparam func f a function prototyped as __f(node,...)__
|
|
||||||
-- @tparam[opt] vararg ... args to be passed to function __f__
|
|
||||||
-- @treturn grid self (the calling `grid` itself, can be chained)
|
|
||||||
-- @usage
|
|
||||||
-- local function nothing(node)
|
|
||||||
-- return node
|
|
||||||
-- end
|
|
||||||
-- myGrid:imap(1,1,6,6,nothing)
|
|
||||||
function Grid:imapRange(lx,ly,ex,ey,f,...)
|
|
||||||
for node in self:iter(lx,ly,ex,ey) do
|
|
||||||
node = f(node,...)
|
|
||||||
end
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Specialized grids
|
|
||||||
-- Inits a preprocessed grid
|
|
||||||
function PreProcessGrid:new(map)
|
|
||||||
local newGrid = {}
|
|
||||||
newGrid._map = map
|
|
||||||
newGrid._nodes, newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y, newGrid._min_z, newGrid._max_z = Utils.arrayToNodes(newGrid._map)
|
|
||||||
newGrid._width = (newGrid._max_x-newGrid._min_x)+1
|
|
||||||
newGrid._height = (newGrid._max_y-newGrid._min_y)+1
|
|
||||||
newGrid._length = (newGrid._max_z-newGrid._min_z)+1
|
|
||||||
newGrid._isAnnotated = {}
|
|
||||||
return setmetatable(newGrid,PreProcessGrid)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Inits a postprocessed grid
|
|
||||||
function PostProcessGrid:new(map)
|
|
||||||
local newGrid = {}
|
|
||||||
newGrid._map = map
|
|
||||||
newGrid._nodes = {}
|
|
||||||
newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y = Utils.getArrayBounds(newGrid._map)
|
|
||||||
newGrid._width = (newGrid._max_x-newGrid._min_x)+1
|
|
||||||
newGrid._height = (newGrid._max_y-newGrid._min_y)+1
|
|
||||||
newGrid._isAnnotated = {}
|
|
||||||
return setmetatable(newGrid,PostProcessGrid)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the `node` at location [x,y].
|
|
||||||
-- @class function
|
|
||||||
-- @name Grid:getNodeAt
|
|
||||||
-- @tparam int x the x-coordinate coordinate
|
|
||||||
-- @tparam int y the y-coordinate coordinate
|
|
||||||
-- @treturn node a `node`
|
|
||||||
-- @usage local aNode = myGrid:getNodeAt(2,2)
|
|
||||||
|
|
||||||
-- Gets the node at location <x,y> on a preprocessed grid
|
|
||||||
function PreProcessGrid:getNodeAt(x,y,z)
|
|
||||||
return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z] or nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Gets the node at location <x,y> on a postprocessed grid
|
|
||||||
function PostProcessGrid:getNodeAt(x,y,z)
|
|
||||||
if not x or not y or not z then return end
|
|
||||||
if Utils.outOfRange(x,self._min_x,self._max_x) then return end
|
|
||||||
if Utils.outOfRange(y,self._min_y,self._max_y) then return end
|
|
||||||
if Utils.outOfRange(z,self._min_z,self._max_z) then return end
|
|
||||||
if not self._nodes[y] then self._nodes[y] = {} end
|
|
||||||
if not self._nodes[y][x] then self._nodes[y][x] = {} end
|
|
||||||
if not self._nodes[y][x][z] then self._nodes[y][x][z] = Node:new(x,y,z) end
|
|
||||||
return self._nodes[y][x][z]
|
|
||||||
end
|
|
||||||
|
|
||||||
return setmetatable(Grid,{
|
|
||||||
__call = function(self,...)
|
|
||||||
return self:new(...)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,412 +0,0 @@
|
|||||||
--[[
|
|
||||||
The following License applies to all files within the jumper directory.
|
|
||||||
|
|
||||||
Note that this is only a partial copy of the full jumper code base. Also,
|
|
||||||
the code was modified to support 3D maps.
|
|
||||||
--]]
|
|
||||||
|
|
||||||
--[[
|
|
||||||
This work is under MIT-LICENSE
|
|
||||||
Copyright (c) 2012-2013 Roland Yonaba.
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
--]]
|
|
||||||
|
|
||||||
--- The Pathfinder class
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Implementation of the `pathfinder` class.
|
|
||||||
|
|
||||||
local _VERSION = ""
|
|
||||||
local _RELEASEDATE = ""
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
|
|
||||||
-- Dependencies
|
|
||||||
local _PATH = (...):gsub('%.pathfinder$','')
|
|
||||||
local Utils = require (_PATH .. '.core.utils')
|
|
||||||
local Assert = require (_PATH .. '.core.assert')
|
|
||||||
local Heap = require (_PATH .. '.core.bheap')
|
|
||||||
local Heuristic = require (_PATH .. '.core.heuristics')
|
|
||||||
local Grid = require (_PATH .. '.grid')
|
|
||||||
local Path = require (_PATH .. '.core.path')
|
|
||||||
|
|
||||||
-- Internalization
|
|
||||||
local t_insert, t_remove = table.insert, table.remove
|
|
||||||
local floor = math.floor
|
|
||||||
local pairs = pairs
|
|
||||||
local assert = assert
|
|
||||||
local type = type
|
|
||||||
local setmetatable, getmetatable = setmetatable, getmetatable
|
|
||||||
|
|
||||||
--- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper.
|
|
||||||
--
|
|
||||||
-- <li>[A*](http://en.wikipedia.org/wiki/A*_search_algorithm)</li>
|
|
||||||
-- <li>[Dijkstra](http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)</li>
|
|
||||||
-- <li>[Theta Astar](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/)</li>
|
|
||||||
-- <li>[BFS](http://en.wikipedia.org/wiki/Breadth-first_search)</li>
|
|
||||||
-- <li>[DFS](http://en.wikipedia.org/wiki/Depth-first_search)</li>
|
|
||||||
-- <li>[JPS](http://harablog.wordpress.com/2011/09/07/jump-point-search/)</li>
|
|
||||||
-- @finder Finders
|
|
||||||
-- @see Pathfinder:getFinders
|
|
||||||
local Finders = {
|
|
||||||
['ASTAR'] = require (_PATH .. '.search.astar'),
|
|
||||||
-- ['DIJKSTRA'] = require (_PATH .. '.search.dijkstra'),
|
|
||||||
-- ['THETASTAR'] = require (_PATH .. '.search.thetastar'),
|
|
||||||
['BFS'] = require (_PATH .. '.search.bfs'),
|
|
||||||
-- ['DFS'] = require (_PATH .. '.search.dfs'),
|
|
||||||
-- ['JPS'] = require (_PATH .. '.search.jps')
|
|
||||||
}
|
|
||||||
|
|
||||||
-- Will keep track of all nodes expanded during the search
|
|
||||||
-- to easily reset their properties for the next pathfinding call
|
|
||||||
local toClear = {}
|
|
||||||
|
|
||||||
--- Search modes. Refers to the search modes. In ORTHOGONAL mode, 4-directions are only possible when moving,
|
|
||||||
-- including North, East, West, South. In DIAGONAL mode, 8-directions are possible when moving,
|
|
||||||
-- including North, East, West, South and adjacent directions.
|
|
||||||
--
|
|
||||||
-- <li>ORTHOGONAL</li>
|
|
||||||
-- <li>DIAGONAL</li>
|
|
||||||
-- @mode Modes
|
|
||||||
-- @see Pathfinder:getModes
|
|
||||||
local searchModes = {['DIAGONAL'] = true, ['ORTHOGONAL'] = true}
|
|
||||||
|
|
||||||
-- Performs a traceback from the goal node to the start node
|
|
||||||
-- Only happens when the path was found
|
|
||||||
|
|
||||||
--- The `Pathfinder` class.<br/>
|
|
||||||
-- This class is callable.
|
|
||||||
-- Therefore,_ <code>Pathfinder(...)</code> _acts as a shortcut to_ <code>Pathfinder:new(...)</code>.
|
|
||||||
-- @type Pathfinder
|
|
||||||
local Pathfinder = {}
|
|
||||||
Pathfinder.__index = Pathfinder
|
|
||||||
|
|
||||||
--- Inits a new `pathfinder`
|
|
||||||
-- @class function
|
|
||||||
-- @tparam grid grid a `grid`
|
|
||||||
-- @tparam[opt] string finderName the name of the `Finder` (search algorithm) to be used for search.
|
|
||||||
-- Defaults to `ASTAR` when not given (see @{Pathfinder:getFinders}).
|
|
||||||
-- @tparam[optchain] string|int|func walkable the value for __walkable__ nodes.
|
|
||||||
-- If this parameter is a function, it should be prototyped as __f(value)__, returning a boolean:
|
|
||||||
-- __true__ when value matches a __walkable__ `node`, __false__ otherwise.
|
|
||||||
-- @treturn pathfinder a new `pathfinder` instance
|
|
||||||
-- @usage
|
|
||||||
-- -- Example one
|
|
||||||
-- local finder = Pathfinder:new(myGrid, 'ASTAR', 0)
|
|
||||||
--
|
|
||||||
-- -- Example two
|
|
||||||
-- local function walkable(value)
|
|
||||||
-- return value > 0
|
|
||||||
-- end
|
|
||||||
-- local finder = Pathfinder(myGrid, 'JPS', walkable)
|
|
||||||
function Pathfinder:new(grid, finderName, walkable)
|
|
||||||
local newPathfinder = {}
|
|
||||||
setmetatable(newPathfinder, Pathfinder)
|
|
||||||
--newPathfinder:setGrid(grid)
|
|
||||||
newPathfinder:setFinder(finderName)
|
|
||||||
--newPathfinder:setWalkable(walkable)
|
|
||||||
newPathfinder:setMode('DIAGONAL')
|
|
||||||
newPathfinder:setHeuristic('MANHATTAN')
|
|
||||||
newPathfinder:setTunnelling(false)
|
|
||||||
return newPathfinder
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Evaluates [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric)
|
|
||||||
-- for the whole `grid`. It should be called only once, unless the collision map or the
|
|
||||||
-- __walkable__ attribute changes. The clearance values are calculated and cached within the grid nodes.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
|
|
||||||
-- @usage myFinder:annotateGrid()
|
|
||||||
function Pathfinder:annotateGrid()
|
|
||||||
assert(self._walkable, 'Finder must implement a walkable value')
|
|
||||||
for x=self._grid._max_x,self._grid._min_x,-1 do
|
|
||||||
for y=self._grid._max_y,self._grid._min_y,-1 do
|
|
||||||
local node = self._grid:getNodeAt(x,y)
|
|
||||||
if self._grid:isWalkableAt(x,y,self._walkable) then
|
|
||||||
local nr = self._grid:getNodeAt(node._x+1, node._y)
|
|
||||||
local nrd = self._grid:getNodeAt(node._x+1, node._y+1)
|
|
||||||
local nd = self._grid:getNodeAt(node._x, node._y+1)
|
|
||||||
if nr and nrd and nd then
|
|
||||||
local m = nrd._clearance[self._walkable] or 0
|
|
||||||
m = (nd._clearance[self._walkable] or 0)<m and (nd._clearance[self._walkable] or 0) or m
|
|
||||||
m = (nr._clearance[self._walkable] or 0)<m and (nr._clearance[self._walkable] or 0) or m
|
|
||||||
node._clearance[self._walkable] = m+1
|
|
||||||
else
|
|
||||||
node._clearance[self._walkable] = 1
|
|
||||||
end
|
|
||||||
else node._clearance[self._walkable] = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self._grid._isAnnotated[self._walkable] = true
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Removes [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric)values.
|
|
||||||
-- Clears cached clearance values for the current __walkable__.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
|
|
||||||
-- @usage myFinder:clearAnnotations()
|
|
||||||
function Pathfinder:clearAnnotations()
|
|
||||||
assert(self._walkable, 'Finder must implement a walkable value')
|
|
||||||
for node in self._grid:iter() do
|
|
||||||
node:removeClearance(self._walkable)
|
|
||||||
end
|
|
||||||
self._grid._isAnnotated[self._walkable] = false
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Sets the `grid`. Defines the given `grid` as the one on which the `pathfinder` will perform the search.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam grid grid a `grid`
|
|
||||||
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
|
|
||||||
-- @usage myFinder:setGrid(myGrid)
|
|
||||||
function Pathfinder:setGrid(grid)
|
|
||||||
assert(Assert.inherits(grid, Grid), 'Wrong argument #1. Expected a \'grid\' object')
|
|
||||||
self._grid = grid
|
|
||||||
self._grid._eval = self._walkable and type(self._walkable) == 'function'
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the `grid`. This is a reference to the actual `grid` used by the `pathfinder`.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn grid the `grid`
|
|
||||||
-- @usage local myGrid = myFinder:getGrid()
|
|
||||||
function Pathfinder:getGrid()
|
|
||||||
return self._grid
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Sets the __walkable__ value or function.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam string|int|func walkable the value for walkable nodes.
|
|
||||||
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
|
|
||||||
-- @usage
|
|
||||||
-- -- Value '0' is walkable
|
|
||||||
-- myFinder:setWalkable(0)
|
|
||||||
--
|
|
||||||
-- -- Any value greater than 0 is walkable
|
|
||||||
-- myFinder:setWalkable(function(n)
|
|
||||||
-- return n>0
|
|
||||||
-- end
|
|
||||||
function Pathfinder:setWalkable(walkable)
|
|
||||||
assert(Assert.matchType(walkable,'stringintfunctionnil'),
|
|
||||||
('Wrong argument #1. Expected \'string\', \'number\' or \'function\', got %s.'):format(type(walkable)))
|
|
||||||
self._walkable = walkable
|
|
||||||
self._grid._eval = type(self._walkable) == 'function'
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Gets the __walkable__ value or function.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn string|int|func the `walkable` value or function
|
|
||||||
-- @usage local walkable = myFinder:getWalkable()
|
|
||||||
function Pathfinder:getWalkable()
|
|
||||||
return self._walkable
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Defines the `finder`. It refers to the search algorithm used by the `pathfinder`.
|
|
||||||
-- Default finder is `ASTAR`. Use @{Pathfinder:getFinders} to get the list of available finders.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam string finderName the name of the `finder` to be used for further searches.
|
|
||||||
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
|
|
||||||
-- @usage
|
|
||||||
-- --To use Breadth-First-Search
|
|
||||||
-- myFinder:setFinder('BFS')
|
|
||||||
-- @see Pathfinder:getFinders
|
|
||||||
function Pathfinder:setFinder(finderName)
|
|
||||||
if not finderName then
|
|
||||||
if not self._finder then
|
|
||||||
finderName = 'ASTAR'
|
|
||||||
else return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
assert(Finders[finderName],'Not a valid finder name!')
|
|
||||||
self._finder = finderName
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the name of the `finder` being used.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn string the name of the `finder` to be used for further searches.
|
|
||||||
-- @usage local finderName = myFinder:getFinder()
|
|
||||||
function Pathfinder:getFinder()
|
|
||||||
return self._finder
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the list of all available finders names.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn {string,...} array of built-in finders names.
|
|
||||||
-- @usage
|
|
||||||
-- local finders = myFinder:getFinders()
|
|
||||||
-- for i, finderName in ipairs(finders) do
|
|
||||||
-- print(i, finderName)
|
|
||||||
-- end
|
|
||||||
function Pathfinder:getFinders()
|
|
||||||
return Utils.getKeys(Finders)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Sets a heuristic. This is a function internally used by the `pathfinder` to find the optimal path during a search.
|
|
||||||
-- Use @{Pathfinder:getHeuristics} to get the list of all available `heuristics`. One can also define
|
|
||||||
-- his own `heuristic` function.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam func|string heuristic `heuristic` function, prototyped as __f(dx,dy)__ or as a `string`.
|
|
||||||
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
|
|
||||||
-- @see Pathfinder:getHeuristics
|
|
||||||
-- @see core.heuristics
|
|
||||||
-- @usage myFinder:setHeuristic('MANHATTAN')
|
|
||||||
function Pathfinder:setHeuristic(heuristic)
|
|
||||||
assert(Heuristic[heuristic] or (type(heuristic) == 'function'),'Not a valid heuristic!')
|
|
||||||
self._heuristic = Heuristic[heuristic] or heuristic
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the `heuristic` used. Returns the function itself.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn func the `heuristic` function being used by the `pathfinder`
|
|
||||||
-- @see core.heuristics
|
|
||||||
-- @usage local h = myFinder:getHeuristic()
|
|
||||||
function Pathfinder:getHeuristic()
|
|
||||||
return self._heuristic
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Gets the list of all available `heuristics`.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn {string,...} array of heuristic names.
|
|
||||||
-- @see core.heuristics
|
|
||||||
-- @usage
|
|
||||||
-- local heur = myFinder:getHeuristic()
|
|
||||||
-- for i, heuristicName in ipairs(heur) do
|
|
||||||
-- ...
|
|
||||||
-- end
|
|
||||||
function Pathfinder:getHeuristics()
|
|
||||||
return Utils.getKeys(Heuristic)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Defines the search `mode`.
|
|
||||||
-- The default search mode is the `DIAGONAL` mode, which implies 8-possible directions when moving (north, south, east, west and diagonals).
|
|
||||||
-- In `ORTHOGONAL` mode, only 4-directions are allowed (north, south, east and west).
|
|
||||||
-- Use @{Pathfinder:getModes} to get the list of all available search modes.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam string mode the new search `mode`.
|
|
||||||
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
|
|
||||||
-- @see Pathfinder:getModes
|
|
||||||
-- @see Modes
|
|
||||||
-- @usage myFinder:setMode('ORTHOGONAL')
|
|
||||||
function Pathfinder:setMode(mode)
|
|
||||||
assert(searchModes[mode],'Invalid mode')
|
|
||||||
self._allowDiagonal = (mode == 'DIAGONAL')
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns the search mode.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn string the current search mode
|
|
||||||
-- @see Modes
|
|
||||||
-- @usage local mode = myFinder:getMode()
|
|
||||||
function Pathfinder:getMode()
|
|
||||||
return (self._allowDiagonal and 'DIAGONAL' or 'ORTHOGONAL')
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Gets the list of all available search modes.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn {string,...} array of search modes.
|
|
||||||
-- @see Modes
|
|
||||||
-- @usage local modes = myFinder:getModes()
|
|
||||||
-- for modeName in ipairs(modes) do
|
|
||||||
-- ...
|
|
||||||
-- end
|
|
||||||
function Pathfinder:getModes()
|
|
||||||
return Utils.getKeys(searchModes)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Enables tunnelling. Defines the ability for the `pathfinder` to tunnel through walls when heading diagonally.
|
|
||||||
-- This feature __is not compatible__ with Jump Point Search algorithm (i.e. enabling it will not affect Jump Point Search)
|
|
||||||
-- @class function
|
|
||||||
-- @tparam bool bool a boolean
|
|
||||||
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
|
|
||||||
-- @usage myFinder:setTunnelling(true)
|
|
||||||
function Pathfinder:setTunnelling(bool)
|
|
||||||
assert(Assert.isBool(bool), ('Wrong argument #1. Expected boolean, got %s'):format(type(bool)))
|
|
||||||
self._tunnel = bool
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Returns tunnelling feature state.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn bool tunnelling feature actual state
|
|
||||||
-- @usage local isTunnellingEnabled = myFinder:getTunnelling()
|
|
||||||
function Pathfinder:getTunnelling()
|
|
||||||
return self._tunnel
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Calculates a `path`. Returns the `path` from location __[startX, startY]__ to location __[endX, endY]__.
|
|
||||||
-- Both locations must exist on the collision map. The starting location can be unwalkable.
|
|
||||||
-- @class function
|
|
||||||
-- @tparam int startX the x-coordinate for the starting location
|
|
||||||
-- @tparam int startY the y-coordinate for the starting location
|
|
||||||
-- @tparam int endX the x-coordinate for the goal location
|
|
||||||
-- @tparam int endY the y-coordinate for the goal location
|
|
||||||
-- @tparam int clearance the amount of clearance (i.e the pathing agent size) to consider
|
|
||||||
-- @treturn path a path (array of nodes) when found, otherwise nil
|
|
||||||
-- @usage local path = myFinder:getPath(1,1,5,5)
|
|
||||||
function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh, clearance)
|
|
||||||
|
|
||||||
self:reset()
|
|
||||||
local startNode = self._grid:getNodeAt(startX, startY, startZ)
|
|
||||||
local endNode = self._grid:getNodeAt(endX, endY, endZ)
|
|
||||||
if not startNode or not endNode then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
startNode._heading = ih
|
|
||||||
endNode._heading = oh
|
|
||||||
|
|
||||||
assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ))
|
|
||||||
assert(endNode and self._grid:isWalkableAt(endX, endY, endZ),
|
|
||||||
('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ))
|
|
||||||
local _endNode = Finders[self._finder](self, startNode, endNode, clearance, toClear)
|
|
||||||
if _endNode then
|
|
||||||
return Utils.traceBackPath(self, _endNode, startNode)
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Resets the `pathfinder`. This function is called internally between successive pathfinding calls, so you should not
|
|
||||||
-- use it explicitely, unless under specific circumstances.
|
|
||||||
-- @class function
|
|
||||||
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
|
|
||||||
-- @usage local path, len = myFinder:getPath(1,1,5,5)
|
|
||||||
function Pathfinder:reset()
|
|
||||||
for node in pairs(toClear) do node:reset() end
|
|
||||||
toClear = {}
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
-- Returns Pathfinder class
|
|
||||||
Pathfinder._VERSION = _VERSION
|
|
||||||
Pathfinder._RELEASEDATE = _RELEASEDATE
|
|
||||||
return setmetatable(Pathfinder,{
|
|
||||||
__call = function(self,...)
|
|
||||||
return self:new(...)
|
|
||||||
end
|
|
||||||
})
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
-- Astar algorithm
|
|
||||||
-- This actual implementation of A-star is based on
|
|
||||||
-- [Nash A. & al. pseudocode](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/)
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
|
|
||||||
-- Internalization
|
|
||||||
local ipairs = ipairs
|
|
||||||
local huge = math.huge
|
|
||||||
|
|
||||||
-- Dependancies
|
|
||||||
local _PATH = (...):match('(.+)%.search.astar$')
|
|
||||||
local Heuristics = require (_PATH .. '.core.heuristics')
|
|
||||||
local Heap = require (_PATH.. '.core.bheap')
|
|
||||||
|
|
||||||
-- Updates G-cost
|
|
||||||
local function computeCost(node, neighbour, finder, clearance, heuristic)
|
|
||||||
local mCost, heading = heuristic(neighbour, node) -- Heuristics.EUCLIDIAN(neighbour, node)
|
|
||||||
|
|
||||||
if node._g + mCost < neighbour._g then
|
|
||||||
neighbour._parent = node
|
|
||||||
neighbour._g = node._g + mCost
|
|
||||||
neighbour._heading = heading
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Updates vertex node-neighbour
|
|
||||||
local function updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval)
|
|
||||||
local oldG = neighbour._g
|
|
||||||
local cmpCost = overrideCostEval or computeCost
|
|
||||||
cmpCost(node, neighbour, finder, clearance, heuristic)
|
|
||||||
if neighbour._g < oldG then
|
|
||||||
local nClearance = neighbour._clearance[finder._walkable]
|
|
||||||
local pushThisNode = clearance and nClearance and (nClearance >= clearance)
|
|
||||||
if (clearance and pushThisNode) or (not clearance) then
|
|
||||||
if neighbour._opened then neighbour._opened = false end
|
|
||||||
neighbour._h = heuristic(endNode, neighbour)
|
|
||||||
neighbour._f = neighbour._g + neighbour._h
|
|
||||||
openList:push(neighbour)
|
|
||||||
neighbour._opened = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Calculates a path.
|
|
||||||
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
|
|
||||||
return function (finder, startNode, endNode, clearance, toClear, overrideHeuristic, overrideCostEval)
|
|
||||||
|
|
||||||
local heuristic = overrideHeuristic or finder._heuristic
|
|
||||||
local openList = Heap()
|
|
||||||
startNode._g = 0
|
|
||||||
startNode._h = heuristic(endNode, startNode)
|
|
||||||
startNode._f = startNode._g + startNode._h
|
|
||||||
openList:push(startNode)
|
|
||||||
toClear[startNode] = true
|
|
||||||
startNode._opened = true
|
|
||||||
|
|
||||||
while not openList:empty() do
|
|
||||||
local node = openList:pop()
|
|
||||||
node._closed = true
|
|
||||||
if node == endNode then return node end
|
|
||||||
local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel)
|
|
||||||
for i = 1,#neighbours do
|
|
||||||
local neighbour = neighbours[i]
|
|
||||||
if not neighbour._closed then
|
|
||||||
toClear[neighbour] = true
|
|
||||||
if not neighbour._opened then
|
|
||||||
neighbour._g = huge
|
|
||||||
neighbour._parent = nil
|
|
||||||
end
|
|
||||||
updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[
|
|
||||||
printf('x:%d y:%d z:%d g:%d', node._x, node._y, node._z, node._g)
|
|
||||||
for i = 1,#neighbours do
|
|
||||||
local n = neighbours[i]
|
|
||||||
printf('x:%d y:%d z:%d f:%f g:%f h:%d', n._x, n._y, n._z, n._f, n._g, n._heading or -1)
|
|
||||||
end
|
|
||||||
--]]
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
-- Breadth-First search algorithm
|
|
||||||
|
|
||||||
if (...) then
|
|
||||||
-- Internalization
|
|
||||||
local t_remove = table.remove
|
|
||||||
|
|
||||||
local function breadth_first_search(finder, openList, node, endNode, clearance, toClear)
|
|
||||||
local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel)
|
|
||||||
for i = 1,#neighbours do
|
|
||||||
local neighbour = neighbours[i]
|
|
||||||
if not neighbour._closed and not neighbour._opened then
|
|
||||||
local nClearance = neighbour._clearance[finder._walkable]
|
|
||||||
local pushThisNode = clearance and nClearance and (nClearance >= clearance)
|
|
||||||
if (clearance and pushThisNode) or (not clearance) then
|
|
||||||
openList[#openList+1] = neighbour
|
|
||||||
neighbour._opened = true
|
|
||||||
neighbour._parent = node
|
|
||||||
toClear[neighbour] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Calculates a path.
|
|
||||||
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
|
|
||||||
return function (finder, startNode, endNode, clearance, toClear)
|
|
||||||
|
|
||||||
local openList = {} -- We'll use a FIFO queue (simple array)
|
|
||||||
openList[1] = startNode
|
|
||||||
startNode._opened = true
|
|
||||||
toClear[startNode] = true
|
|
||||||
|
|
||||||
local node
|
|
||||||
while (#openList > 0) do
|
|
||||||
node = openList[1]
|
|
||||||
t_remove(openList,1)
|
|
||||||
node._closed = true
|
|
||||||
if node == endNode then return node end
|
|
||||||
breadth_first_search(finder, openList, node, endNode, clearance, toClear)
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -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,76 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local NFT = { }
|
|
||||||
|
|
||||||
-- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/
|
|
||||||
|
|
||||||
local tColourLookup = { }
|
|
||||||
for n = 1, 16 do
|
|
||||||
tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getColourOf(hex)
|
|
||||||
return tColourLookup[hex:byte()]
|
|
||||||
end
|
|
||||||
|
|
||||||
function NFT.parse(imageText)
|
|
||||||
local image = {
|
|
||||||
fg = { },
|
|
||||||
bg = { },
|
|
||||||
text = { },
|
|
||||||
}
|
|
||||||
|
|
||||||
local num = 1
|
|
||||||
local index = 1
|
|
||||||
for _,sLine in ipairs(Util.split(imageText)) do
|
|
||||||
table.insert(image.fg, { })
|
|
||||||
table.insert(image.bg, { })
|
|
||||||
table.insert(image.text, { })
|
|
||||||
|
|
||||||
--As we're no longer 1-1, we keep track of what index to write to
|
|
||||||
local writeIndex = 1
|
|
||||||
--Tells us if we've hit a 30 or 31 (BG and FG respectively)- next char specifies the curr colour
|
|
||||||
local bgNext, fgNext = false, false
|
|
||||||
--The current background and foreground colours
|
|
||||||
local currBG, currFG = nil,nil
|
|
||||||
for i = 1, #sLine do
|
|
||||||
local nextChar = string.sub(sLine, i, i)
|
|
||||||
if nextChar:byte() == 30 then
|
|
||||||
bgNext = true
|
|
||||||
elseif nextChar:byte() == 31 then
|
|
||||||
fgNext = true
|
|
||||||
elseif bgNext then
|
|
||||||
currBG = getColourOf(nextChar)
|
|
||||||
bgNext = false
|
|
||||||
elseif fgNext then
|
|
||||||
currFG = getColourOf(nextChar)
|
|
||||||
fgNext = false
|
|
||||||
else
|
|
||||||
if nextChar ~= " " and currFG == nil then
|
|
||||||
currFG = colours.white
|
|
||||||
end
|
|
||||||
image.bg[num][writeIndex] = currBG
|
|
||||||
image.fg[num][writeIndex] = currFG
|
|
||||||
image.text[num][writeIndex] = nextChar
|
|
||||||
writeIndex = writeIndex + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
image.height = num
|
|
||||||
if not image.width or writeIndex - 1 > image.width then
|
|
||||||
image.width = writeIndex - 1
|
|
||||||
end
|
|
||||||
num = num+1
|
|
||||||
end
|
|
||||||
return image
|
|
||||||
end
|
|
||||||
|
|
||||||
function NFT.load(path)
|
|
||||||
|
|
||||||
local imageText = Util.readFile(path)
|
|
||||||
if not imageText then
|
|
||||||
error('Unable to read image file')
|
|
||||||
end
|
|
||||||
return NFT.parse(imageText)
|
|
||||||
end
|
|
||||||
|
|
||||||
return NFT
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
local Opus = { }
|
|
||||||
|
|
||||||
local function runDir(directory, open)
|
|
||||||
if not fs.exists(directory) then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local success = true
|
|
||||||
local files = fs.list(directory)
|
|
||||||
table.sort(files)
|
|
||||||
|
|
||||||
for _,file in ipairs(files) do
|
|
||||||
os.sleep(0)
|
|
||||||
local result, err = open(directory .. '/' .. file)
|
|
||||||
if result then
|
|
||||||
if term.isColor() then
|
|
||||||
term.setTextColor(colors.green)
|
|
||||||
end
|
|
||||||
term.write('[PASS] ')
|
|
||||||
term.setTextColor(colors.white)
|
|
||||||
term.write(fs.combine(directory, file))
|
|
||||||
else
|
|
||||||
if term.isColor() then
|
|
||||||
term.setTextColor(colors.red)
|
|
||||||
end
|
|
||||||
term.write('[FAIL] ')
|
|
||||||
term.setTextColor(colors.white)
|
|
||||||
term.write(fs.combine(directory, file))
|
|
||||||
if err then
|
|
||||||
printError(err)
|
|
||||||
end
|
|
||||||
success = false
|
|
||||||
end
|
|
||||||
print()
|
|
||||||
end
|
|
||||||
|
|
||||||
return success
|
|
||||||
end
|
|
||||||
|
|
||||||
function Opus.loadServices()
|
|
||||||
return runDir('sys/services', shell.openHiddenTab)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Opus.autorun()
|
|
||||||
local s = runDir('sys/autorun', shell.run)
|
|
||||||
return runDir('usr/autorun', shell.run) and s
|
|
||||||
end
|
|
||||||
|
|
||||||
return Opus
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local Peripheral = { }
|
|
||||||
|
|
||||||
local function getDeviceList()
|
|
||||||
|
|
||||||
if _G.device then
|
|
||||||
return _G.device
|
|
||||||
end
|
|
||||||
|
|
||||||
local deviceList = { }
|
|
||||||
|
|
||||||
for _,side in pairs(peripheral.getNames()) do
|
|
||||||
Peripheral.addDevice(deviceList, side)
|
|
||||||
end
|
|
||||||
|
|
||||||
return deviceList
|
|
||||||
end
|
|
||||||
|
|
||||||
function Peripheral.addDevice(deviceList, side)
|
|
||||||
local name = side
|
|
||||||
local ptype = peripheral.getType(side)
|
|
||||||
|
|
||||||
if not ptype then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if ptype == 'modem' then
|
|
||||||
if peripheral.call(name, 'isWireless') then
|
|
||||||
ptype = 'wireless_modem'
|
|
||||||
else
|
|
||||||
ptype = 'wired_modem'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local sides = {
|
|
||||||
front = true,
|
|
||||||
back = true,
|
|
||||||
top = true,
|
|
||||||
bottom = true,
|
|
||||||
left = true,
|
|
||||||
right = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if sides[name] then
|
|
||||||
local i = 1
|
|
||||||
local uniqueName = ptype
|
|
||||||
while deviceList[uniqueName] do
|
|
||||||
uniqueName = ptype .. '_' .. i
|
|
||||||
i = i + 1
|
|
||||||
end
|
|
||||||
name = uniqueName
|
|
||||||
end
|
|
||||||
|
|
||||||
local s, m pcall(function() deviceList[name] = peripheral.wrap(side) end)
|
|
||||||
if not s and m then
|
|
||||||
printError('wrap failed')
|
|
||||||
printError(m)
|
|
||||||
end
|
|
||||||
|
|
||||||
if deviceList[name] then
|
|
||||||
Util.merge(deviceList[name], {
|
|
||||||
name = name,
|
|
||||||
type = ptype,
|
|
||||||
side = side,
|
|
||||||
})
|
|
||||||
|
|
||||||
return deviceList[name]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Peripheral.getBySide(side)
|
|
||||||
return Util.find(getDeviceList(), 'side', side)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Peripheral.getByType(typeName)
|
|
||||||
return Util.find(getDeviceList(), 'type', typeName)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Peripheral.getByMethod(method)
|
|
||||||
for _,p in pairs(getDeviceList()) do
|
|
||||||
if p[method] then
|
|
||||||
return p
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- match any of the passed arguments
|
|
||||||
function Peripheral.get(args)
|
|
||||||
|
|
||||||
if type(args) == 'string' then
|
|
||||||
args = { type = args }
|
|
||||||
end
|
|
||||||
|
|
||||||
args = args or { type = pType }
|
|
||||||
|
|
||||||
if args.type then
|
|
||||||
local p = Peripheral.getByType(args.type)
|
|
||||||
if p then
|
|
||||||
return p
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if args.method then
|
|
||||||
local p = Peripheral.getByMethod(args.method)
|
|
||||||
if p then
|
|
||||||
return p
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if args.side then
|
|
||||||
local p = Peripheral.getBySide(args.side)
|
|
||||||
if p then
|
|
||||||
return p
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Peripheral
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local Point = { }
|
|
||||||
|
|
||||||
function Point.copy(pt)
|
|
||||||
return { x = pt.x, y = pt.y, z = pt.z }
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.same(pta, ptb)
|
|
||||||
return pta.x == ptb.x and
|
|
||||||
pta.y == ptb.y and
|
|
||||||
pta.z == ptb.z
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.above(pt)
|
|
||||||
return { x = pt.x, y = pt.y + 1, z = pt.z, heading = pt.heading }
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.below(pt)
|
|
||||||
return { x = pt.x, y = pt.y - 1, z = pt.z, heading = pt.heading }
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.subtract(a, b)
|
|
||||||
a.x = a.x - b.x
|
|
||||||
a.y = a.y - b.y
|
|
||||||
a.z = a.z - b.z
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Euclidian distance
|
|
||||||
function Point.pythagoreanDistance(a, b)
|
|
||||||
return math.sqrt(
|
|
||||||
math.pow(a.x - b.x, 2) +
|
|
||||||
math.pow(a.y - b.y, 2) +
|
|
||||||
math.pow(a.z - b.z, 2))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- turtle distance (manhattan)
|
|
||||||
function Point.turtleDistance(a, b)
|
|
||||||
if a.y and b.y then
|
|
||||||
return math.abs(a.x - b.x) +
|
|
||||||
math.abs(a.y - b.y) +
|
|
||||||
math.abs(a.z - b.z)
|
|
||||||
else
|
|
||||||
return math.abs(a.x - b.x) +
|
|
||||||
math.abs(a.z - b.z)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.calculateTurns(ih, oh)
|
|
||||||
if ih == oh then
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
if (ih % 2) == (oh % 2) then
|
|
||||||
return 2
|
|
||||||
end
|
|
||||||
return 1
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.calculateHeading(pta, ptb)
|
|
||||||
|
|
||||||
local heading
|
|
||||||
|
|
||||||
if (pta.heading % 2) == 0 and pta.z ~= ptb.z then
|
|
||||||
if ptb.z > pta.z then
|
|
||||||
heading = 1
|
|
||||||
else
|
|
||||||
heading = 3
|
|
||||||
end
|
|
||||||
elseif (pta.heading % 2) == 1 and pta.x ~= ptb.x then
|
|
||||||
if ptb.x > pta.x then
|
|
||||||
heading = 0
|
|
||||||
else
|
|
||||||
heading = 2
|
|
||||||
end
|
|
||||||
elseif pta.heading == 0 and pta.x > ptb.x then
|
|
||||||
heading = 2
|
|
||||||
elseif pta.heading == 2 and pta.x < ptb.x then
|
|
||||||
heading = 0
|
|
||||||
elseif pta.heading == 1 and pta.z > ptb.z then
|
|
||||||
heading = 3
|
|
||||||
elseif pta.heading == 3 and pta.z < ptb.z then
|
|
||||||
heading = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
return heading or pta.heading
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Calculate distance to location including turns
|
|
||||||
-- also returns the resulting heading
|
|
||||||
function Point.calculateMoves(pta, ptb, distance)
|
|
||||||
local heading = pta.heading
|
|
||||||
local moves = distance or Point.turtleDistance(pta, ptb)
|
|
||||||
if (pta.heading % 2) == 0 and pta.z ~= ptb.z then
|
|
||||||
moves = moves + 1
|
|
||||||
if ptb.heading and (ptb.heading % 2 == 1) then
|
|
||||||
heading = ptb.heading
|
|
||||||
elseif ptb.z > pta.z then
|
|
||||||
heading = 1
|
|
||||||
else
|
|
||||||
heading = 3
|
|
||||||
end
|
|
||||||
elseif (pta.heading % 2) == 1 and pta.x ~= ptb.x then
|
|
||||||
moves = moves + 1
|
|
||||||
if ptb.heading and (ptb.heading % 2 == 0) then
|
|
||||||
heading = ptb.heading
|
|
||||||
elseif ptb.x > pta.x then
|
|
||||||
heading = 0
|
|
||||||
else
|
|
||||||
heading = 2
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if ptb.heading then
|
|
||||||
if heading ~= ptb.heading then
|
|
||||||
moves = moves + Point.calculateTurns(heading, ptb.heading)
|
|
||||||
heading = ptb.heading
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return moves, heading
|
|
||||||
end
|
|
||||||
|
|
||||||
-- given a set of points, find the one taking the least moves
|
|
||||||
function Point.closest(reference, pts)
|
|
||||||
local lpt, lm -- lowest
|
|
||||||
for _,pt in pairs(pts) do
|
|
||||||
local m = Point.calculateMoves(reference, pt)
|
|
||||||
if not lm or m < lm then
|
|
||||||
lpt = pt
|
|
||||||
lm = m
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return lpt
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.eachClosest(spt, ipts, fn)
|
|
||||||
|
|
||||||
local pts = Util.shallowCopy(ipts)
|
|
||||||
while #pts > 0 do
|
|
||||||
local pt = Point.closest(spt, pts)
|
|
||||||
local r = fn(pt)
|
|
||||||
if r then
|
|
||||||
return r
|
|
||||||
end
|
|
||||||
Util.removeByValue(pts, pt)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.adjacentPoints(pt)
|
|
||||||
local pts = { }
|
|
||||||
|
|
||||||
for _, hi in pairs(turtle.getHeadings()) do
|
|
||||||
table.insert(pts, { x = pt.x + hi.xd, y = pt.y + hi.yd, z = pt.z + hi.zd })
|
|
||||||
end
|
|
||||||
|
|
||||||
return pts
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.normalizeBox(box)
|
|
||||||
return {
|
|
||||||
x = math.min(box.x, box.ex),
|
|
||||||
y = math.min(box.y, box.ey),
|
|
||||||
z = math.min(box.z, box.ez),
|
|
||||||
ex = math.max(box.x, box.ex),
|
|
||||||
ey = math.max(box.y, box.ey),
|
|
||||||
ez = math.max(box.z, box.ez),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
function Point.inBox(pt, box)
|
|
||||||
return pt.x >= box.x and
|
|
||||||
pt.y >= box.y and
|
|
||||||
pt.z >= box.z and
|
|
||||||
pt.x <= box.ex and
|
|
||||||
pt.y <= box.ey and
|
|
||||||
pt.z <= box.ez
|
|
||||||
end
|
|
||||||
|
|
||||||
return Point
|
|
||||||
|
|
||||||
--[[
|
|
||||||
Box = { }
|
|
||||||
|
|
||||||
function Box.contain(boundingBox, containedBox)
|
|
||||||
|
|
||||||
local shiftX = boundingBox.ax - containedBox.ax
|
|
||||||
if shiftX > 0 then
|
|
||||||
containedBox.ax = containedBox.ax + shiftX
|
|
||||||
containedBox.bx = containedBox.bx + shiftX
|
|
||||||
end
|
|
||||||
local shiftZ = boundingBox.az - containedBox.az
|
|
||||||
if shiftZ > 0 then
|
|
||||||
containedBox.az = containedBox.az + shiftZ
|
|
||||||
containedBox.bz = containedBox.bz + shiftZ
|
|
||||||
end
|
|
||||||
|
|
||||||
shiftX = boundingBox.bx - containedBox.bx
|
|
||||||
if shiftX < 0 then
|
|
||||||
containedBox.ax = containedBox.ax + shiftX
|
|
||||||
containedBox.bx = containedBox.bx + shiftX
|
|
||||||
end
|
|
||||||
shiftZ = boundingBox.bz - containedBox.bz
|
|
||||||
if shiftZ < 0 then
|
|
||||||
containedBox.az = containedBox.az + shiftZ
|
|
||||||
containedBox.bz = containedBox.bz + shiftZ
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--]]
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
local Config = require('config')
|
|
||||||
|
|
||||||
local config = { }
|
|
||||||
|
|
||||||
local Security = { }
|
|
||||||
|
|
||||||
function Security.verifyPassword(password)
|
|
||||||
Config.load('os', config)
|
|
||||||
return config.password and password == config.password
|
|
||||||
end
|
|
||||||
|
|
||||||
function Security.getSecretKey()
|
|
||||||
Config.load('os', config)
|
|
||||||
if not config.secretKey then
|
|
||||||
config.secretKey = math.random(100000, 999999)
|
|
||||||
Config.update('os', config)
|
|
||||||
end
|
|
||||||
return config.secretKey
|
|
||||||
end
|
|
||||||
|
|
||||||
function Security.getPublicKey()
|
|
||||||
|
|
||||||
local exchange = {
|
|
||||||
base = 11,
|
|
||||||
primeMod = 625210769
|
|
||||||
}
|
|
||||||
|
|
||||||
local function modexp(base, exponent, modulo)
|
|
||||||
local remainder = base
|
|
||||||
|
|
||||||
for i = 1, exponent-1 do
|
|
||||||
remainder = remainder * remainder
|
|
||||||
if remainder >= modulo then
|
|
||||||
remainder = remainder % modulo
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return remainder
|
|
||||||
end
|
|
||||||
|
|
||||||
local secretKey = Security.getSecretKey()
|
|
||||||
return modexp(exchange.base, secretKey, exchange.primeMod)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Security.updatePassword(password)
|
|
||||||
Config.load('os', config)
|
|
||||||
config.password = password
|
|
||||||
Config.update('os', config)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Security.getPassword()
|
|
||||||
Config.load('os', config)
|
|
||||||
return config.password
|
|
||||||
end
|
|
||||||
|
|
||||||
return Security
|
|
||||||
@@ -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,196 +0,0 @@
|
|||||||
local Crypto = require('crypto')
|
|
||||||
local Logger = require('logger')
|
|
||||||
local Security = require('security')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local socketClass = { }
|
|
||||||
|
|
||||||
function socketClass:read(timeout)
|
|
||||||
|
|
||||||
local data, distance = transport.read(self)
|
|
||||||
if data then
|
|
||||||
return data, distance
|
|
||||||
end
|
|
||||||
|
|
||||||
if not self.connected then
|
|
||||||
Logger.log('socket', 'read: No connection')
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local timerId = os.startTimer(timeout or 5)
|
|
||||||
|
|
||||||
while true do
|
|
||||||
local e, id = os.pullEvent()
|
|
||||||
|
|
||||||
if e == 'transport_' .. self.sport then
|
|
||||||
data, distance = transport.read(self)
|
|
||||||
if data then
|
|
||||||
os.cancelTimer(timerId)
|
|
||||||
return data, distance
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif e == 'timer' and id == timerId then
|
|
||||||
if timeout or not self.connected then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
timerId = os.startTimer(5)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function socketClass:write(data)
|
|
||||||
if self.connected then
|
|
||||||
transport.write(self, {
|
|
||||||
type = 'DATA',
|
|
||||||
seq = self.wseq,
|
|
||||||
data = data,
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function socketClass:ping()
|
|
||||||
if self.connected then
|
|
||||||
transport.write(self, {
|
|
||||||
type = 'PING',
|
|
||||||
seq = self.wseq,
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function socketClass:close()
|
|
||||||
if self.connected then
|
|
||||||
Logger.log('socket', 'closing socket ' .. self.sport)
|
|
||||||
self.transmit(self.dport, self.dhost, {
|
|
||||||
type = 'DISC',
|
|
||||||
})
|
|
||||||
self.connected = false
|
|
||||||
end
|
|
||||||
device.wireless_modem.close(self.sport)
|
|
||||||
transport.close(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
local Socket = { }
|
|
||||||
|
|
||||||
local function loopback(port, sport, msg)
|
|
||||||
os.queueEvent('modem_message', 'loopback', port, sport, msg, 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function newSocket(isLoopback)
|
|
||||||
for i = 16384, 32767 do
|
|
||||||
if not device.wireless_modem.isOpen(i) then
|
|
||||||
local socket = {
|
|
||||||
shost = os.getComputerID(),
|
|
||||||
sport = i,
|
|
||||||
transmit = device.wireless_modem.transmit,
|
|
||||||
wseq = math.random(100, 100000),
|
|
||||||
rseq = math.random(100, 100000),
|
|
||||||
timers = { },
|
|
||||||
messages = { },
|
|
||||||
}
|
|
||||||
setmetatable(socket, { __index = socketClass })
|
|
||||||
|
|
||||||
device.wireless_modem.open(socket.sport)
|
|
||||||
|
|
||||||
if isLoopback then
|
|
||||||
socket.transmit = loopback
|
|
||||||
end
|
|
||||||
return socket
|
|
||||||
end
|
|
||||||
end
|
|
||||||
error('No ports available')
|
|
||||||
end
|
|
||||||
|
|
||||||
function Socket.connect(host, port)
|
|
||||||
|
|
||||||
local socket = newSocket(host == os.getComputerID())
|
|
||||||
socket.dhost = host
|
|
||||||
Logger.log('socket', 'connecting to ' .. port)
|
|
||||||
|
|
||||||
socket.transmit(port, socket.sport, {
|
|
||||||
type = 'OPEN',
|
|
||||||
shost = socket.shost,
|
|
||||||
dhost = socket.dhost,
|
|
||||||
t = Crypto.encrypt({ ts = os.time(), seq = socket.seq }, Security.getPublicKey()),
|
|
||||||
rseq = socket.wseq,
|
|
||||||
wseq = socket.rseq,
|
|
||||||
})
|
|
||||||
|
|
||||||
local timerId = os.startTimer(3)
|
|
||||||
repeat
|
|
||||||
local e, id, sport, dport, msg = os.pullEvent()
|
|
||||||
if e == 'modem_message' and
|
|
||||||
sport == socket.sport and
|
|
||||||
msg.dhost == socket.shost and
|
|
||||||
msg.type == 'CONN' then
|
|
||||||
|
|
||||||
socket.dport = dport
|
|
||||||
socket.connected = true
|
|
||||||
Logger.log('socket', 'connection established to %d %d->%d',
|
|
||||||
host, socket.sport, socket.dport)
|
|
||||||
|
|
||||||
os.cancelTimer(timerId)
|
|
||||||
|
|
||||||
transport.open(socket)
|
|
||||||
|
|
||||||
return socket
|
|
||||||
end
|
|
||||||
until e == 'timer' and id == timerId
|
|
||||||
|
|
||||||
socket:close()
|
|
||||||
end
|
|
||||||
|
|
||||||
local function trusted(msg, port)
|
|
||||||
|
|
||||||
if port == 19 or msg.shost == os.getComputerID() then
|
|
||||||
-- no auth for trust server or loopback
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
local trustList = Util.readTable('usr/.known_hosts') or { }
|
|
||||||
local pubKey = trustList[msg.shost]
|
|
||||||
|
|
||||||
if pubKey then
|
|
||||||
local data = Crypto.decrypt(msg.t or '', pubKey)
|
|
||||||
|
|
||||||
--local sharedKey = modexp(pubKey, exchange.secretKey, public.primeMod)
|
|
||||||
return data.ts and tonumber(data.ts) and math.abs(os.time() - data.ts) < 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Socket.server(port)
|
|
||||||
|
|
||||||
device.wireless_modem.open(port)
|
|
||||||
Logger.log('socket', 'Waiting for connections on port ' .. port)
|
|
||||||
|
|
||||||
while true do
|
|
||||||
local e, _, sport, dport, msg = os.pullEvent('modem_message')
|
|
||||||
|
|
||||||
if sport == port and
|
|
||||||
msg and
|
|
||||||
msg.dhost == os.getComputerID() and
|
|
||||||
msg.type == 'OPEN' then
|
|
||||||
|
|
||||||
if trusted(msg, port) then
|
|
||||||
local socket = newSocket(msg.shost == os.getComputerID())
|
|
||||||
socket.dport = dport
|
|
||||||
socket.dhost = msg.shost
|
|
||||||
socket.connected = true
|
|
||||||
socket.wseq = msg.wseq
|
|
||||||
socket.rseq = msg.rseq
|
|
||||||
socket.transmit(socket.dport, socket.sport, {
|
|
||||||
type = 'CONN',
|
|
||||||
dhost = socket.dhost,
|
|
||||||
shost = socket.shost,
|
|
||||||
})
|
|
||||||
Logger.log('socket', 'Connection established %d->%d', socket.sport, socket.dport)
|
|
||||||
|
|
||||||
transport.open(socket)
|
|
||||||
return socket
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return Socket
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
local syncLocks = { }
|
|
||||||
|
|
||||||
return function(obj, fn)
|
|
||||||
local key = tostring(obj)
|
|
||||||
if syncLocks[key] then
|
|
||||||
local cos = tostring(coroutine.running())
|
|
||||||
table.insert(syncLocks[key], cos)
|
|
||||||
repeat
|
|
||||||
local _, co = os.pullEvent('sync_lock')
|
|
||||||
until co == cos
|
|
||||||
else
|
|
||||||
syncLocks[key] = { }
|
|
||||||
end
|
|
||||||
local s, m = pcall(fn)
|
|
||||||
local co = table.remove(syncLocks[key], 1)
|
|
||||||
if co then
|
|
||||||
os.queueEvent('sync_lock', co)
|
|
||||||
else
|
|
||||||
syncLocks[key] = nil
|
|
||||||
end
|
|
||||||
if not s then
|
|
||||||
error(m)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local Terminal = { }
|
|
||||||
|
|
||||||
local _sgsub = string.gsub
|
|
||||||
|
|
||||||
function Terminal.scrollable(ct, size)
|
|
||||||
|
|
||||||
local size = size or 25
|
|
||||||
local w, h = ct.getSize()
|
|
||||||
local win = window.create(ct, 1, 1, w, h + size, true)
|
|
||||||
local oldWin = Util.shallowCopy(win)
|
|
||||||
local scrollPos = 0
|
|
||||||
|
|
||||||
local function drawScrollbar(oldPos, newPos)
|
|
||||||
local x, y = oldWin.getCursorPos()
|
|
||||||
|
|
||||||
local pos = math.floor(oldPos / size * (h - 1))
|
|
||||||
oldWin.setCursorPos(w, oldPos + pos + 1)
|
|
||||||
oldWin.write(' ')
|
|
||||||
|
|
||||||
pos = math.floor(newPos / size * (h - 1))
|
|
||||||
oldWin.setCursorPos(w, newPos + pos + 1)
|
|
||||||
oldWin.write('#')
|
|
||||||
|
|
||||||
oldWin.setCursorPos(x, y)
|
|
||||||
end
|
|
||||||
|
|
||||||
win.setCursorPos = function(x, y)
|
|
||||||
oldWin.setCursorPos(x, y)
|
|
||||||
if y > scrollPos + h then
|
|
||||||
win.scrollTo(y - h)
|
|
||||||
elseif y < scrollPos then
|
|
||||||
win.scrollTo(y - 2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
win.scrollUp = function()
|
|
||||||
win.scrollTo(scrollPos - 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
win.scrollDown = function()
|
|
||||||
win.scrollTo(scrollPos + 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
win.scrollTo = function(p)
|
|
||||||
p = math.min(math.max(p, 0), size)
|
|
||||||
if p ~= scrollPos then
|
|
||||||
drawScrollbar(scrollPos, p)
|
|
||||||
scrollPos = p
|
|
||||||
--local w, h = win.getSize()
|
|
||||||
win.reposition(1, -scrollPos + 1, w, h + size)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
win.clear = function()
|
|
||||||
oldWin.clear()
|
|
||||||
scrollPos = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
drawScrollbar(0, 0)
|
|
||||||
|
|
||||||
return win
|
|
||||||
end
|
|
||||||
|
|
||||||
function Terminal.toGrayscale(ct)
|
|
||||||
|
|
||||||
local scolors = {
|
|
||||||
[ colors.white ] = colors.white,
|
|
||||||
[ colors.orange ] = colors.lightGray,
|
|
||||||
[ colors.magenta ] = colors.lightGray,
|
|
||||||
[ colors.lightBlue ] = colors.lightGray,
|
|
||||||
[ colors.yellow ] = colors.lightGray,
|
|
||||||
[ colors.lime ] = colors.lightGray,
|
|
||||||
[ colors.pink ] = colors.lightGray,
|
|
||||||
[ colors.gray ] = colors.gray,
|
|
||||||
[ colors.lightGray ] = colors.lightGray,
|
|
||||||
[ colors.cyan ] = colors.lightGray,
|
|
||||||
[ colors.purple ] = colors.gray,
|
|
||||||
[ colors.blue ] = colors.gray,
|
|
||||||
[ colors.brown ] = colors.gray,
|
|
||||||
[ colors.green ] = colors.lightGray,
|
|
||||||
[ colors.red ] = colors.gray,
|
|
||||||
[ colors.black ] = colors.black,
|
|
||||||
}
|
|
||||||
|
|
||||||
local methods = { 'setBackgroundColor', 'setBackgroundColour',
|
|
||||||
'setTextColor', 'setTextColour' }
|
|
||||||
for _,v in pairs(methods) do
|
|
||||||
local fn = ct[v]
|
|
||||||
ct[v] = function(c)
|
|
||||||
fn(scolors[c])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local bcolors = {
|
|
||||||
[ '1' ] = '8',
|
|
||||||
[ '2' ] = '8',
|
|
||||||
[ '3' ] = '8',
|
|
||||||
[ '4' ] = '8',
|
|
||||||
[ '5' ] = '8',
|
|
||||||
[ '6' ] = '8',
|
|
||||||
[ '9' ] = '8',
|
|
||||||
[ 'a' ] = '7',
|
|
||||||
[ 'b' ] = '7',
|
|
||||||
[ 'c' ] = '7',
|
|
||||||
[ 'd' ] = '8',
|
|
||||||
[ 'e' ] = '7',
|
|
||||||
}
|
|
||||||
|
|
||||||
local function translate(s)
|
|
||||||
if s then
|
|
||||||
for k,v in pairs(bcolors) do
|
|
||||||
s = _sgsub(s, k, v)
|
|
||||||
end
|
|
||||||
-- s = _sgsub(s, "%d+", bcolors) -- not working in cc 1.75 ???
|
|
||||||
end
|
|
||||||
return s
|
|
||||||
end
|
|
||||||
|
|
||||||
local fn = ct.blit
|
|
||||||
ct.blit = function(text, fg, bg)
|
|
||||||
fn(text, translate(fg), translate(bg))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Terminal.getNullTerm(ct)
|
|
||||||
local nt = Terminal.copy(ct)
|
|
||||||
|
|
||||||
local methods = { 'blit', 'clear', 'clearLine', 'scroll',
|
|
||||||
'setCursorBlink', 'setCursorPos', 'write' }
|
|
||||||
for _,v in pairs(methods) do
|
|
||||||
nt[v] = function() end
|
|
||||||
end
|
|
||||||
|
|
||||||
return nt
|
|
||||||
end
|
|
||||||
|
|
||||||
function Terminal.copy(it, ot)
|
|
||||||
ot = ot or { }
|
|
||||||
for k,v in pairs(it) do
|
|
||||||
if type(v) == 'function' then
|
|
||||||
ot[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ot
|
|
||||||
end
|
|
||||||
|
|
||||||
function Terminal.mirror(ct, dt)
|
|
||||||
for k,f in pairs(ct) do
|
|
||||||
ct[k] = function(...)
|
|
||||||
local ret = { f(...) }
|
|
||||||
if dt[k] then
|
|
||||||
dt[k](...)
|
|
||||||
end
|
|
||||||
return unpack(ret)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Terminal.readPassword(prompt)
|
|
||||||
if prompt then
|
|
||||||
term.write(prompt)
|
|
||||||
end
|
|
||||||
local fn = term.current().write
|
|
||||||
term.current().write = function() end
|
|
||||||
local s
|
|
||||||
pcall(function() s = read(prompt) end)
|
|
||||||
term.current().write = fn
|
|
||||||
|
|
||||||
if s == '' then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
return s
|
|
||||||
end
|
|
||||||
|
|
||||||
return Terminal
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
requireInjector(getfenv(1))
|
|
||||||
|
|
||||||
local Grid = require ("jumper.grid")
|
|
||||||
local Pathfinder = require ("jumper.pathfinder")
|
|
||||||
local Point = require('point')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local WALKABLE = 0
|
|
||||||
|
|
||||||
local function createMap(dim)
|
|
||||||
local map = { }
|
|
||||||
for z = 1, dim.ez do
|
|
||||||
local row = {}
|
|
||||||
for x = 1, dim.ex do
|
|
||||||
local col = { }
|
|
||||||
for y = 1, dim.ey do
|
|
||||||
table.insert(col, WALKABLE)
|
|
||||||
end
|
|
||||||
table.insert(row, col)
|
|
||||||
end
|
|
||||||
table.insert(map, row)
|
|
||||||
end
|
|
||||||
|
|
||||||
return map
|
|
||||||
end
|
|
||||||
|
|
||||||
local function addBlock(map, dim, b)
|
|
||||||
map[b.z + dim.oz][b.x + dim.ox][b.y + dim.oy] = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
-- map shrinks/grows depending upon blocks encountered
|
|
||||||
-- the map will encompass any blocks encountered, the turtle position, and the destination
|
|
||||||
local function mapDimensions(dest, blocks, boundingBox)
|
|
||||||
local sx, sz, sy = turtle.point.x, turtle.point.z, turtle.point.y
|
|
||||||
local ex, ez, ey = turtle.point.x, turtle.point.z, turtle.point.y
|
|
||||||
|
|
||||||
local function adjust(pt)
|
|
||||||
if pt.x < sx then
|
|
||||||
sx = pt.x
|
|
||||||
end
|
|
||||||
if pt.z < sz then
|
|
||||||
sz = pt.z
|
|
||||||
end
|
|
||||||
if pt.y < sy then
|
|
||||||
sy = pt.y
|
|
||||||
end
|
|
||||||
if pt.x > ex then
|
|
||||||
ex = pt.x
|
|
||||||
end
|
|
||||||
if pt.z > ez then
|
|
||||||
ez = pt.z
|
|
||||||
end
|
|
||||||
if pt.y > ey then
|
|
||||||
ey = pt.y
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
adjust(dest)
|
|
||||||
|
|
||||||
for _,b in ipairs(blocks) do
|
|
||||||
adjust(b)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- expand one block out in all directions
|
|
||||||
if boundingBox then
|
|
||||||
sx = math.max(sx - 1, boundingBox.x)
|
|
||||||
sz = math.max(sz - 1, boundingBox.z)
|
|
||||||
sy = math.max(sy - 1, boundingBox.y)
|
|
||||||
ex = math.min(ex + 1, boundingBox.ex)
|
|
||||||
ez = math.min(ez + 1, boundingBox.ez)
|
|
||||||
ey = math.min(ey + 1, boundingBox.ey)
|
|
||||||
else
|
|
||||||
sx = sx - 1
|
|
||||||
sz = sz - 1
|
|
||||||
sy = sy - 1
|
|
||||||
ex = ex + 1
|
|
||||||
ez = ez + 1
|
|
||||||
ey = ey + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
ex = ex - sx + 1,
|
|
||||||
ez = ez - sz + 1,
|
|
||||||
ey = ey - sy + 1,
|
|
||||||
ox = -sx + 1,
|
|
||||||
oz = -sz + 1,
|
|
||||||
oy = -sy + 1
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- shifting and coordinate flipping
|
|
||||||
local function pointToMap(dim, pt)
|
|
||||||
return { x = pt.x + dim.ox, z = pt.y + dim.oy, y = pt.z + dim.oz }
|
|
||||||
end
|
|
||||||
|
|
||||||
local function nodeToPoint(dim, node)
|
|
||||||
return { x = node:getX() - dim.ox, z = node:getY() - dim.oz, y = node:getZ() - dim.oy }
|
|
||||||
end
|
|
||||||
|
|
||||||
local heuristic = function(n, node)
|
|
||||||
|
|
||||||
local m, h = Point.calculateMoves(
|
|
||||||
{ x = node._x, z = node._y, y = node._z, heading = node._heading },
|
|
||||||
{ x = n._x, z = n._y, y = n._z, heading = n._heading })
|
|
||||||
|
|
||||||
return m, h
|
|
||||||
end
|
|
||||||
|
|
||||||
local function dimsAreEqual(d1, d2)
|
|
||||||
return d1.ex == d2.ex and
|
|
||||||
d1.ey == d2.ey and
|
|
||||||
d1.ez == d2.ez and
|
|
||||||
d1.ox == d2.ox and
|
|
||||||
d1.oy == d2.oy and
|
|
||||||
d1.oz == d2.oz
|
|
||||||
end
|
|
||||||
|
|
||||||
-- turtle sensor returns blocks in relation to the world - not turtle orientation
|
|
||||||
-- so cannot figure out block location unless we know our orientation in the world
|
|
||||||
-- really kinda dumb since it returns the coordinates as offsets of our location
|
|
||||||
-- instead of true coordinates
|
|
||||||
local function addSensorBlocks(blocks, sblocks)
|
|
||||||
|
|
||||||
for _,b in pairs(sblocks) do
|
|
||||||
if b.type ~= 'AIR' then
|
|
||||||
local pt = { x = turtle.point.x, y = turtle.point.y + b.y, z = turtle.point.z }
|
|
||||||
pt.x = pt.x - b.x
|
|
||||||
pt.z = pt.z - b.z -- this will only work if we were originally facing west
|
|
||||||
local found = false
|
|
||||||
for _,ob in pairs(blocks) do
|
|
||||||
if pt.x == ob.x and pt.y == ob.y and pt.z == ob.z then
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not found then
|
|
||||||
table.insert(blocks, pt)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function selectDestination(pts, box, map, dim)
|
|
||||||
|
|
||||||
while #pts > 0 do
|
|
||||||
local pt = Point.closest(turtle.point, pts)
|
|
||||||
|
|
||||||
if (box and not Point.inBox(pt, box)) or
|
|
||||||
map[pt.z + dim.oz][pt.x + dim.ox][pt.y + dim.oy] == 1 then
|
|
||||||
Util.removeByValue(pts, pt)
|
|
||||||
else
|
|
||||||
return pt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function pathTo(dest, options)
|
|
||||||
|
|
||||||
local blocks = options.blocks or turtle.getState().blocks or { }
|
|
||||||
local dests = options.dest or { dest } -- support alternative destinations
|
|
||||||
local box = options.box or turtle.getState().box
|
|
||||||
|
|
||||||
local lastDim = nil
|
|
||||||
local map = nil
|
|
||||||
local grid = nil
|
|
||||||
|
|
||||||
if box then
|
|
||||||
box = Point.normalizeBox(box)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Creates a pathfinder object
|
|
||||||
local myFinder = Pathfinder(grid, 'ASTAR', walkable)
|
|
||||||
|
|
||||||
myFinder:setMode('ORTHOGONAL')
|
|
||||||
myFinder:setHeuristic(heuristic)
|
|
||||||
|
|
||||||
while turtle.point.x ~= dest.x or turtle.point.z ~= dest.z or turtle.point.y ~= dest.y do
|
|
||||||
|
|
||||||
-- map expands as we encounter obstacles
|
|
||||||
local dim = mapDimensions(dest, blocks, box)
|
|
||||||
|
|
||||||
-- reuse map if possible
|
|
||||||
if not lastDim or not dimsAreEqual(dim, lastDim) then
|
|
||||||
map = createMap(dim)
|
|
||||||
-- Creates a grid object
|
|
||||||
grid = Grid(map)
|
|
||||||
myFinder:setGrid(grid)
|
|
||||||
myFinder:setWalkable(WALKABLE)
|
|
||||||
|
|
||||||
lastDim = dim
|
|
||||||
end
|
|
||||||
|
|
||||||
for _,b in ipairs(blocks) do
|
|
||||||
addBlock(map, dim, b)
|
|
||||||
end
|
|
||||||
|
|
||||||
dest = selectDestination(dests, box, map, dim)
|
|
||||||
if not dest then
|
|
||||||
-- error('failed to reach destination')
|
|
||||||
return false, 'failed to reach destination'
|
|
||||||
end
|
|
||||||
if turtle.point.x == dest.x and turtle.point.z == dest.z and turtle.point.y == dest.y then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Define start and goal locations coordinates
|
|
||||||
local startPt = pointToMap(dim, turtle.point)
|
|
||||||
local endPt = pointToMap(dim, dest)
|
|
||||||
|
|
||||||
-- Calculates the path, and its length
|
|
||||||
local path = myFinder:getPath(startPt.x, startPt.y, startPt.z, turtle.point.heading, endPt.x, endPt.y, endPt.z, dest.heading)
|
|
||||||
|
|
||||||
if not path then
|
|
||||||
Util.removeByValue(dests, dest)
|
|
||||||
else
|
|
||||||
for node, count in path:nodes() do
|
|
||||||
local pt = nodeToPoint(dim, node)
|
|
||||||
|
|
||||||
if turtle.abort then
|
|
||||||
return false, 'aborted'
|
|
||||||
end
|
|
||||||
|
|
||||||
-- use single turn method so the turtle doesn't turn around
|
|
||||||
-- when encountering obstacles -- IS THIS RIGHT ??
|
|
||||||
if not turtle.gotoSingleTurn(pt.x, pt.z, pt.y, node.heading) then
|
|
||||||
table.insert(blocks, pt)
|
|
||||||
--if device.turtlesensorenvironment then
|
|
||||||
-- addSensorBlocks(blocks, device.turtlesensorenvironment.sonicScan())
|
|
||||||
--end
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if dest.heading then
|
|
||||||
turtle.setHeading(dest.heading)
|
|
||||||
end
|
|
||||||
return dest
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
pathfind = function(dest, options)
|
|
||||||
options = options or { }
|
|
||||||
--if not options.blocks and turtle.gotoPoint(dest) then
|
|
||||||
-- return dest
|
|
||||||
--end
|
|
||||||
return pathTo(dest, options)
|
|
||||||
end,
|
|
||||||
|
|
||||||
-- set a global bounding box
|
|
||||||
-- box can be overridden by passing box in pathfind options
|
|
||||||
setBox = function(box)
|
|
||||||
turtle.getState().box = box
|
|
||||||
end,
|
|
||||||
|
|
||||||
setBlocks = function(blocks)
|
|
||||||
turtle.getState().blocks = blocks
|
|
||||||
end,
|
|
||||||
|
|
||||||
reset = function()
|
|
||||||
turtle.getState().box = nil
|
|
||||||
turtle.getState().blocks = nil
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
3276
sys/apis/ui.lua
3276
sys/apis/ui.lua
File diff suppressed because it is too large
Load Diff
@@ -1,364 +0,0 @@
|
|||||||
local class = require('class')
|
|
||||||
local Region = require('ui.region')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local _srep = string.rep
|
|
||||||
local _ssub = string.sub
|
|
||||||
|
|
||||||
local mapColorToGray = {
|
|
||||||
[ colors.white ] = colors.white,
|
|
||||||
[ colors.orange ] = colors.lightGray,
|
|
||||||
[ colors.magenta ] = colors.lightGray,
|
|
||||||
[ colors.lightBlue ] = colors.lightGray,
|
|
||||||
[ colors.yellow ] = colors.lightGray,
|
|
||||||
[ colors.lime ] = colors.lightGray,
|
|
||||||
[ colors.pink ] = colors.lightGray,
|
|
||||||
[ colors.gray ] = colors.gray,
|
|
||||||
[ colors.lightGray ] = colors.lightGray,
|
|
||||||
[ colors.cyan ] = colors.lightGray,
|
|
||||||
[ colors.purple ] = colors.gray,
|
|
||||||
[ colors.blue ] = colors.gray,
|
|
||||||
[ colors.brown ] = colors.gray,
|
|
||||||
[ colors.green ] = colors.lightGray,
|
|
||||||
[ colors.red ] = colors.gray,
|
|
||||||
[ colors.black ] = colors.black,
|
|
||||||
}
|
|
||||||
|
|
||||||
local mapColorToPaint = { }
|
|
||||||
for n = 1, 16 do
|
|
||||||
mapColorToPaint[2 ^ (n - 1)] = _ssub("0123456789abcdef", n, n)
|
|
||||||
end
|
|
||||||
|
|
||||||
local mapGrayToPaint = { }
|
|
||||||
for n = 0, 15 do
|
|
||||||
local gs = mapColorToGray[2 ^ n]
|
|
||||||
mapGrayToPaint[2 ^ n] = mapColorToPaint[gs]
|
|
||||||
end
|
|
||||||
|
|
||||||
local Canvas = class()
|
|
||||||
function Canvas:init(args)
|
|
||||||
|
|
||||||
self.x = 1
|
|
||||||
self.y = 1
|
|
||||||
self.layers = { }
|
|
||||||
|
|
||||||
Util.merge(self, args)
|
|
||||||
|
|
||||||
self.height = self.ey - self.y + 1
|
|
||||||
self.width = self.ex - self.x + 1
|
|
||||||
|
|
||||||
self.lines = { }
|
|
||||||
for i = 1, self.height do
|
|
||||||
self.lines[i] = { }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:resize(w, h)
|
|
||||||
for i = self.height, h do
|
|
||||||
self.lines[i] = { }
|
|
||||||
end
|
|
||||||
|
|
||||||
while #self.lines > h do
|
|
||||||
table.remove(self.lines, #self.lines)
|
|
||||||
end
|
|
||||||
|
|
||||||
if w ~= self.width then
|
|
||||||
for i = 1, self.height do
|
|
||||||
self.lines[i] = { }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
self.ex = self.x + w - 1
|
|
||||||
self.ey = self.y + h - 1
|
|
||||||
|
|
||||||
self.width = w
|
|
||||||
self.height = h
|
|
||||||
|
|
||||||
self:dirty()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:colorToPaintColor(c)
|
|
||||||
if self.isColor then
|
|
||||||
return mapColorToPaint[c]
|
|
||||||
end
|
|
||||||
return mapGrayToPaint[c]
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:copy()
|
|
||||||
local b = Canvas({ x = self.x, y = self.y, ex = self.ex, ey = self.ey })
|
|
||||||
for i = 1, self.ey - self.y + 1 do
|
|
||||||
b.lines[i].text = self.lines[i].text
|
|
||||||
b.lines[i].fg = self.lines[i].fg
|
|
||||||
b.lines[i].bg = self.lines[i].bg
|
|
||||||
end
|
|
||||||
return b
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:addLayer(layer, bg, fg)
|
|
||||||
local canvas = Canvas({
|
|
||||||
x = layer.x,
|
|
||||||
y = layer.y,
|
|
||||||
ex = layer.x + layer.width - 1,
|
|
||||||
ey = layer.y + layer.height - 1,
|
|
||||||
isColor = self.isColor,
|
|
||||||
})
|
|
||||||
canvas:clear(bg, fg)
|
|
||||||
|
|
||||||
canvas.parent = self
|
|
||||||
table.insert(self.layers, canvas)
|
|
||||||
return canvas
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:removeLayer()
|
|
||||||
for k, layer in pairs(self.parent.layers) do
|
|
||||||
if layer == self then
|
|
||||||
self:setVisible(false)
|
|
||||||
table.remove(self.parent.layers, k)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:setVisible(visible)
|
|
||||||
self.visible = visible
|
|
||||||
if not visible then
|
|
||||||
self.parent:dirty()
|
|
||||||
-- set parent's lines to dirty for each line in self
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:write(x, y, text, bg, fg)
|
|
||||||
if bg then
|
|
||||||
bg = _srep(self:colorToPaintColor(bg), #text)
|
|
||||||
end
|
|
||||||
if fg then
|
|
||||||
fg = _srep(self:colorToPaintColor(fg), #text)
|
|
||||||
end
|
|
||||||
self:writeBlit(x, y, text, bg, fg)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:writeBlit(x, y, text, bg, fg)
|
|
||||||
if y > 0 and y <= self.height and x <= self.width then
|
|
||||||
|
|
||||||
local width = #text
|
|
||||||
|
|
||||||
-- fix ffs
|
|
||||||
if x < 1 then
|
|
||||||
text = _ssub(text, 2 - x)
|
|
||||||
if bg then
|
|
||||||
bg = _ssub(bg, 2 - x)
|
|
||||||
end
|
|
||||||
if bg then
|
|
||||||
fg = _ssub(fg, 2 - x)
|
|
||||||
end
|
|
||||||
width = width + x - 1
|
|
||||||
x = 1
|
|
||||||
end
|
|
||||||
|
|
||||||
if x + width - 1 > self.width then
|
|
||||||
text = _ssub(text, 1, self.width - x + 1)
|
|
||||||
if bg then
|
|
||||||
bg = _ssub(bg, 1, self.width - x + 1)
|
|
||||||
end
|
|
||||||
if bg then
|
|
||||||
fg = _ssub(fg, 1, self.width - x + 1)
|
|
||||||
end
|
|
||||||
width = #text
|
|
||||||
end
|
|
||||||
|
|
||||||
if width > 0 then
|
|
||||||
|
|
||||||
local function replace(sstr, pos, rstr, width)
|
|
||||||
if pos == 1 and width == self.width then
|
|
||||||
return rstr
|
|
||||||
elseif pos == 1 then
|
|
||||||
return rstr .. _ssub(sstr, pos+width)
|
|
||||||
elseif pos + width > self.width then
|
|
||||||
return _ssub(sstr, 1, pos-1) .. rstr
|
|
||||||
end
|
|
||||||
return _ssub(sstr, 1, pos-1) .. rstr .. _ssub(sstr, pos+width)
|
|
||||||
end
|
|
||||||
|
|
||||||
local line = self.lines[y]
|
|
||||||
line.dirty = true
|
|
||||||
line.text = replace(line.text, x, text, width)
|
|
||||||
if fg then
|
|
||||||
line.fg = replace(line.fg, x, fg, width)
|
|
||||||
end
|
|
||||||
if bg then
|
|
||||||
line.bg = replace(line.bg, x, bg, width)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:writeLine(y, text, fg, bg)
|
|
||||||
self.lines[y].dirty = true
|
|
||||||
self.lines[y].text = text
|
|
||||||
self.lines[y].fg = fg
|
|
||||||
self.lines[y].bg = bg
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:reset()
|
|
||||||
self.regions = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:clear(bg, fg)
|
|
||||||
local width = self.ex - self.x + 1
|
|
||||||
local text = _srep(' ', width)
|
|
||||||
fg = _srep(self:colorToPaintColor(fg), width)
|
|
||||||
bg = _srep(self:colorToPaintColor(bg), width)
|
|
||||||
for i = 1, self.ey - self.y + 1 do
|
|
||||||
self:writeLine(i, text, fg, bg)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:punch(rect)
|
|
||||||
if not self.regions then
|
|
||||||
self.regions = Region.new(self.x, self.y, self.ex, self.ey)
|
|
||||||
end
|
|
||||||
self.regions:subRect(rect.x, rect.y, rect.ex, rect.ey)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:blitClipped(device)
|
|
||||||
for _,region in ipairs(self.regions.region) do
|
|
||||||
self:blit(device,
|
|
||||||
{ x = region[1] - self.x + 1,
|
|
||||||
y = region[2] - self.y + 1,
|
|
||||||
ex = region[3]- self.x + 1,
|
|
||||||
ey = region[4] - self.y + 1 },
|
|
||||||
{ x = region[1], y = region[2] })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:redraw(device)
|
|
||||||
self:reset()
|
|
||||||
if #self.layers > 0 then
|
|
||||||
for _,layer in pairs(self.layers) do
|
|
||||||
self:punch(layer)
|
|
||||||
end
|
|
||||||
self:blitClipped(device)
|
|
||||||
else
|
|
||||||
self:blit(device)
|
|
||||||
end
|
|
||||||
self:clean()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:isDirty()
|
|
||||||
for _, line in pairs(self.lines) do
|
|
||||||
if line.dirty then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:dirty()
|
|
||||||
for _, line in pairs(self.lines) do
|
|
||||||
line.dirty = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:clean()
|
|
||||||
for y, line in pairs(self.lines) do
|
|
||||||
line.dirty = false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:render(device, layers) --- redrawAll ?
|
|
||||||
layers = layers or self.layers
|
|
||||||
if #layers > 0 then
|
|
||||||
self.regions = Region.new(self.x, self.y, self.ex, self.ey)
|
|
||||||
local l = Util.shallowCopy(layers)
|
|
||||||
for _, canvas in ipairs(layers) do
|
|
||||||
table.remove(l, 1)
|
|
||||||
if canvas.visible then
|
|
||||||
self:punch(canvas)
|
|
||||||
canvas:render(device, l)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self:blitClipped(device)
|
|
||||||
self:reset()
|
|
||||||
else
|
|
||||||
self:blit(device)
|
|
||||||
end
|
|
||||||
self:clean()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas:blit(device, src, tgt)
|
|
||||||
src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 }
|
|
||||||
tgt = tgt or self
|
|
||||||
|
|
||||||
for i = 0, src.ey - src.y do
|
|
||||||
local line = self.lines[src.y + i]
|
|
||||||
if line and line.dirty then
|
|
||||||
local t, fg, bg = line.text, line.fg, line.bg
|
|
||||||
if src.x > 1 or src.ex < self.ex then
|
|
||||||
t = _ssub(t, src.x, src.ex)
|
|
||||||
fg = _ssub(fg, src.x, src.ex)
|
|
||||||
bg = _ssub(bg, src.x, src.ex)
|
|
||||||
end
|
|
||||||
--if tgt.y + i > self.ey then -- wrong place to do clipping ??
|
|
||||||
-- break
|
|
||||||
--end
|
|
||||||
device.setCursorPos(tgt.x, tgt.y + i)
|
|
||||||
device.blit(t, fg, bg)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Canvas.convertWindow(win, parent, x, y)
|
|
||||||
|
|
||||||
local w, h = win.getSize()
|
|
||||||
|
|
||||||
win.canvas = Canvas({
|
|
||||||
x = x,
|
|
||||||
y = y,
|
|
||||||
ex = x + w - 1,
|
|
||||||
ey = y + h - 1,
|
|
||||||
isColor = win.isColor(),
|
|
||||||
})
|
|
||||||
|
|
||||||
function win.clear()
|
|
||||||
win.canvas:clear(win.getBackgroundColor(), win.getTextColor())
|
|
||||||
end
|
|
||||||
|
|
||||||
function win.clearLine()
|
|
||||||
local x, y = win.getCursorPos()
|
|
||||||
win.canvas:write(1,
|
|
||||||
y,
|
|
||||||
_srep(' ', win.canvas.width),
|
|
||||||
win.getBackgroundColor(),
|
|
||||||
win.getTextColor())
|
|
||||||
end
|
|
||||||
|
|
||||||
function win.write(str)
|
|
||||||
local x, y = win.getCursorPos()
|
|
||||||
win.canvas:write(x,
|
|
||||||
y,
|
|
||||||
str,
|
|
||||||
win.getBackgroundColor(),
|
|
||||||
win.getTextColor())
|
|
||||||
end
|
|
||||||
|
|
||||||
function win.blit(text, fg, bg)
|
|
||||||
local x, y = win.getCursorPos()
|
|
||||||
win.canvas:writeBlit(x, y, text, bg, fg)
|
|
||||||
end
|
|
||||||
|
|
||||||
function win.redraw()
|
|
||||||
win.canvas:redraw(parent)
|
|
||||||
end
|
|
||||||
|
|
||||||
function win.scroll()
|
|
||||||
error('CWin:scroll: not implemented')
|
|
||||||
end
|
|
||||||
|
|
||||||
function win.reposition(x, y, width, height)
|
|
||||||
win.canvas.x, win.canvas.y = x, y
|
|
||||||
win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
|
|
||||||
end
|
|
||||||
|
|
||||||
win.clear()
|
|
||||||
end
|
|
||||||
|
|
||||||
return Canvas
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
return function(args)
|
|
||||||
|
|
||||||
local columns = {
|
|
||||||
{ heading = 'Name', key = 'name' },
|
|
||||||
}
|
|
||||||
|
|
||||||
if UI.term.width > 28 then
|
|
||||||
table.insert(columns,
|
|
||||||
{ heading = 'Size', key = 'size', width = 5 }
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
args = args or { }
|
|
||||||
|
|
||||||
local selectFile = UI.Dialog {
|
|
||||||
x = args.x or 3,
|
|
||||||
y = args.y or 2,
|
|
||||||
z = args.z or 2,
|
|
||||||
-- rex = args.rex or -3,
|
|
||||||
-- rey = args.rey or -3,
|
|
||||||
height = args.height,
|
|
||||||
width = args.width,
|
|
||||||
title = 'Select file',
|
|
||||||
grid = UI.ScrollingGrid {
|
|
||||||
x = 2,
|
|
||||||
y = 2,
|
|
||||||
ex = -2,
|
|
||||||
ey = -4,
|
|
||||||
path = '',
|
|
||||||
sortColumn = 'name',
|
|
||||||
columns = columns,
|
|
||||||
},
|
|
||||||
path = UI.TextEntry {
|
|
||||||
x = 2,
|
|
||||||
y = -2,
|
|
||||||
ex = -11,
|
|
||||||
limit = 256,
|
|
||||||
accelerators = {
|
|
||||||
enter = 'path_enter',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cancel = UI.Button {
|
|
||||||
text = 'Cancel',
|
|
||||||
x = -9,
|
|
||||||
y = -2,
|
|
||||||
event = 'cancel',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectFile:enable(path, fn)
|
|
||||||
self:setPath(path)
|
|
||||||
self.fn = fn
|
|
||||||
UI.Dialog.enable(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function selectFile:setPath(path)
|
|
||||||
self.grid.dir = path
|
|
||||||
while not fs.isDir(self.grid.dir) do
|
|
||||||
self.grid.dir = fs.getDir(self.grid.dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
self.path.value = self.grid.dir
|
|
||||||
end
|
|
||||||
|
|
||||||
function selectFile.grid:draw()
|
|
||||||
local files = fs.listEx(self.dir)
|
|
||||||
if #self.dir > 0 then
|
|
||||||
table.insert(files, {
|
|
||||||
name = '..',
|
|
||||||
isDir = true,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
self:setValues(files)
|
|
||||||
self:setIndex(1)
|
|
||||||
UI.Grid.draw(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function selectFile.grid:getDisplayValues(row)
|
|
||||||
if row.size then
|
|
||||||
row = Util.shallowCopy(row)
|
|
||||||
row.size = Util.toBytes(row.size)
|
|
||||||
end
|
|
||||||
return row
|
|
||||||
end
|
|
||||||
|
|
||||||
function selectFile.grid:getRowTextColor(file, selected)
|
|
||||||
if file.isDir then
|
|
||||||
return colors.cyan
|
|
||||||
end
|
|
||||||
if file.isReadOnly then
|
|
||||||
return colors.pink
|
|
||||||
end
|
|
||||||
return colors.white
|
|
||||||
end
|
|
||||||
|
|
||||||
function selectFile.grid:sortCompare(a, b)
|
|
||||||
if self.sortColumn == 'size' then
|
|
||||||
return a.size < b.size
|
|
||||||
end
|
|
||||||
if a.isDir == b.isDir then
|
|
||||||
return a.name:lower() < b.name:lower()
|
|
||||||
end
|
|
||||||
return a.isDir
|
|
||||||
end
|
|
||||||
|
|
||||||
function selectFile:eventHandler(event)
|
|
||||||
|
|
||||||
if event.type == 'grid_select' then
|
|
||||||
self.grid.dir = fs.combine(self.grid.dir, event.selected.name)
|
|
||||||
self.path.value = self.grid.dir
|
|
||||||
if event.selected.isDir then
|
|
||||||
self.grid:draw()
|
|
||||||
self.path:draw()
|
|
||||||
else
|
|
||||||
UI:setPreviousPage()
|
|
||||||
self.fn(self.path.value)
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif event.type == 'path_enter' then
|
|
||||||
if fs.isDir(self.path.value) then
|
|
||||||
self:setPath(self.path.value)
|
|
||||||
self.grid:draw()
|
|
||||||
self.path:draw()
|
|
||||||
else
|
|
||||||
UI:setPreviousPage()
|
|
||||||
self.fn(self.path.value)
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif event.type == 'cancel' then
|
|
||||||
UI:setPreviousPage()
|
|
||||||
self.fn()
|
|
||||||
else
|
|
||||||
return UI.Dialog.eventHandler(self, event)
|
|
||||||
end
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
return selectFile
|
|
||||||
end
|
|
||||||
@@ -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,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,591 +0,0 @@
|
|||||||
local Util = { }
|
|
||||||
|
|
||||||
function Util.tryTimed(timeout, f, ...)
|
|
||||||
local c = os.clock()
|
|
||||||
repeat
|
|
||||||
local ret = f(...)
|
|
||||||
if ret then
|
|
||||||
return ret
|
|
||||||
end
|
|
||||||
until os.clock()-c >= timeout
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.tryTimes(attempts, f, ...)
|
|
||||||
local result
|
|
||||||
for i = 1, attempts do
|
|
||||||
result = { f(...) }
|
|
||||||
if result[1] then
|
|
||||||
return unpack(result)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return unpack(result)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.throttle(fn)
|
|
||||||
local ts = os.clock()
|
|
||||||
local timeout = .095
|
|
||||||
return function(...)
|
|
||||||
local nts = os.clock()
|
|
||||||
if nts > ts + timeout then
|
|
||||||
os.sleep(0)
|
|
||||||
ts = os.clock()
|
|
||||||
if fn then
|
|
||||||
fn(...)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.tostring(pattern, ...)
|
|
||||||
|
|
||||||
local function serialize(tbl, width)
|
|
||||||
local str = '{\n'
|
|
||||||
for k, v in pairs(tbl) do
|
|
||||||
local value
|
|
||||||
if type(v) == 'table' then
|
|
||||||
value = string.format('table: %d', Util.size(v))
|
|
||||||
else
|
|
||||||
value = tostring(v)
|
|
||||||
end
|
|
||||||
str = str .. string.format(' %s: %s\n', k, value)
|
|
||||||
end
|
|
||||||
if #str < width then
|
|
||||||
str = str:gsub('\n', '') .. ' }'
|
|
||||||
else
|
|
||||||
str = str .. '}'
|
|
||||||
end
|
|
||||||
return str
|
|
||||||
end
|
|
||||||
|
|
||||||
if type(pattern) == 'string' then
|
|
||||||
return string.format(pattern, ...)
|
|
||||||
elseif type(pattern) == 'table' then
|
|
||||||
return serialize(pattern, term.current().getSize())
|
|
||||||
end
|
|
||||||
return tostring(pattern)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.print(pattern, ...)
|
|
||||||
print(Util.tostring(pattern, ...))
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.getVersion()
|
|
||||||
local version
|
|
||||||
|
|
||||||
if _CC_VERSION then
|
|
||||||
version = tonumber(_CC_VERSION:gmatch('[%d]+%.?[%d][%d]', '%1')())
|
|
||||||
end
|
|
||||||
if not version and _HOST then
|
|
||||||
version = tonumber(_HOST:gmatch('[%d]+%.?[%d][%d]', '%1')())
|
|
||||||
end
|
|
||||||
|
|
||||||
return version or 1.7
|
|
||||||
end
|
|
||||||
|
|
||||||
-- http://lua-users.org/wiki/SimpleRound
|
|
||||||
function Util.round(num, idp)
|
|
||||||
local mult = 10^(idp or 0)
|
|
||||||
return math.floor(num * mult + 0.5) / mult
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.random(max, min)
|
|
||||||
min = min or 0
|
|
||||||
return math.random(0, max-min) + min
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ Table functions ]] --
|
|
||||||
function Util.clear(t)
|
|
||||||
local keys = Util.keys(t)
|
|
||||||
for _,k in pairs(keys) do
|
|
||||||
t[k] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.empty(t)
|
|
||||||
return not next(t)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.key(t, value)
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
if v == value then
|
|
||||||
return k
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.keys(t)
|
|
||||||
local keys = {}
|
|
||||||
for k in pairs(t) do
|
|
||||||
keys[#keys+1] = k
|
|
||||||
end
|
|
||||||
return keys
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.merge(obj, args)
|
|
||||||
if args then
|
|
||||||
for k,v in pairs(args) do
|
|
||||||
obj[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.deepMerge(obj, args)
|
|
||||||
if args then
|
|
||||||
for k,v in pairs(args) do
|
|
||||||
if type(v) == 'table' then
|
|
||||||
if not obj[k] then
|
|
||||||
obj[k] = { }
|
|
||||||
end
|
|
||||||
Util.deepMerge(obj[k], v)
|
|
||||||
else
|
|
||||||
obj[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.transpose(t)
|
|
||||||
local tt = { }
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
tt[v] = k
|
|
||||||
end
|
|
||||||
return tt
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.find(t, name, value)
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
if v[name] == value then
|
|
||||||
return v, k
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.findAll(t, name, value)
|
|
||||||
local rt = { }
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
if v[name] == value then
|
|
||||||
table.insert(rt, v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return rt
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.shallowCopy(t)
|
|
||||||
local t2 = { }
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
t2[k] = v
|
|
||||||
end
|
|
||||||
return t2
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.deepCopy(t)
|
|
||||||
if type(t) ~= 'table' then
|
|
||||||
return t
|
|
||||||
end
|
|
||||||
--local mt = getmetatable(t)
|
|
||||||
local res = {}
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
if type(v) == 'table' then
|
|
||||||
v = Util.deepCopy(v)
|
|
||||||
end
|
|
||||||
res[k] = v
|
|
||||||
end
|
|
||||||
--setmetatable(res,mt)
|
|
||||||
return res
|
|
||||||
end
|
|
||||||
|
|
||||||
-- http://snippets.luacode.org/?p=snippets/Filter_a_table_in-place_119
|
|
||||||
function Util.filterInplace(t, predicate)
|
|
||||||
local j = 1
|
|
||||||
|
|
||||||
for i = 1,#t do
|
|
||||||
local v = t[i]
|
|
||||||
if predicate(v) then
|
|
||||||
t[j] = v
|
|
||||||
j = j + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
while t[j] ~= nil do
|
|
||||||
t[j] = nil
|
|
||||||
j = j + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
return t
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.filter(it, f)
|
|
||||||
local ot = { }
|
|
||||||
for k,v in pairs(it) do
|
|
||||||
if f(k, v) then
|
|
||||||
ot[k] = v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return ot
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.size(list)
|
|
||||||
if type(list) == 'table' then
|
|
||||||
local length = 0
|
|
||||||
table.foreach(list, function() length = length + 1 end)
|
|
||||||
return length
|
|
||||||
end
|
|
||||||
return 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.removeByValue(t, e)
|
|
||||||
for k,v in pairs(t) do
|
|
||||||
if v == e then
|
|
||||||
table.remove(t, k)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.each(list, func)
|
|
||||||
for index, value in pairs(list) do
|
|
||||||
func(value, index, list)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- http://stackoverflow.com/questions/15706270/sort-a-table-in-lua
|
|
||||||
function Util.spairs(t, order)
|
|
||||||
local keys = Util.keys(t)
|
|
||||||
|
|
||||||
-- if order function given, sort by it by passing the table and keys a, b,
|
|
||||||
-- otherwise just sort the keys
|
|
||||||
if order then
|
|
||||||
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
|
|
||||||
else
|
|
||||||
table.sort(keys)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- return the iterator function
|
|
||||||
local i = 0
|
|
||||||
return function()
|
|
||||||
i = i + 1
|
|
||||||
if keys[i] then
|
|
||||||
return keys[i], t[keys[i]]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.first(t, order)
|
|
||||||
local keys = Util.keys(t)
|
|
||||||
if order then
|
|
||||||
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
|
|
||||||
else
|
|
||||||
table.sort(keys)
|
|
||||||
end
|
|
||||||
return keys[1], t[keys[1]]
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ File functions ]]--
|
|
||||||
function Util.readFile(fname)
|
|
||||||
local f = fs.open(fname, "r")
|
|
||||||
if f then
|
|
||||||
local t = f.readAll()
|
|
||||||
f.close()
|
|
||||||
return t
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.writeFile(fname, data)
|
|
||||||
local file = io.open(fname, "w")
|
|
||||||
if not file then
|
|
||||||
error('Unable to open ' .. fname, 2)
|
|
||||||
end
|
|
||||||
file:write(data)
|
|
||||||
file:close()
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.readLines(fname)
|
|
||||||
local file = fs.open(fname, "r")
|
|
||||||
if file then
|
|
||||||
local t = {}
|
|
||||||
local line = file.readLine()
|
|
||||||
while line do
|
|
||||||
table.insert(t, line)
|
|
||||||
line = file.readLine()
|
|
||||||
end
|
|
||||||
file.close()
|
|
||||||
return t
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.writeLines(fname, lines)
|
|
||||||
local file = fs.open(fname, 'w')
|
|
||||||
if file then
|
|
||||||
for _,line in ipairs(lines) do
|
|
||||||
line = file.writeLine(line)
|
|
||||||
end
|
|
||||||
file.close()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.readTable(fname)
|
|
||||||
local t = Util.readFile(fname)
|
|
||||||
if t then
|
|
||||||
return textutils.unserialize(t)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.writeTable(fname, data)
|
|
||||||
Util.writeFile(fname, textutils.serialize(data))
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.loadTable(fname)
|
|
||||||
local fc = Util.readFile(fname)
|
|
||||||
if not fc then
|
|
||||||
return false, 'Unable to read file'
|
|
||||||
end
|
|
||||||
local s, m = loadstring('return ' .. fc, fname)
|
|
||||||
if s then
|
|
||||||
s, m = pcall(s)
|
|
||||||
if s then
|
|
||||||
return m
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return s, m
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ loading and running functions ]] --
|
|
||||||
function Util.download(url, filename)
|
|
||||||
local h = http.get(url)
|
|
||||||
if not h then
|
|
||||||
error('Failed to download ' .. url)
|
|
||||||
end
|
|
||||||
local contents = h.readAll()
|
|
||||||
h.close()
|
|
||||||
if not contents then
|
|
||||||
error('Failed to download ' .. url)
|
|
||||||
end
|
|
||||||
|
|
||||||
if filename then
|
|
||||||
Util.writeFile(filename, contents)
|
|
||||||
end
|
|
||||||
return contents
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.loadUrl(url, env) -- loadfile equivalent
|
|
||||||
local s, m = pcall(function()
|
|
||||||
local c = Util.download(url)
|
|
||||||
return load(c, url, nil, env)
|
|
||||||
end)
|
|
||||||
|
|
||||||
if s then
|
|
||||||
return m
|
|
||||||
end
|
|
||||||
return s, m
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.runUrl(env, url, ...) -- os.run equivalent
|
|
||||||
setmetatable(env, { __index = _G })
|
|
||||||
local fn, m = Util.loadUrl(url, env)
|
|
||||||
if fn then
|
|
||||||
local args = { ... }
|
|
||||||
return pcall(function() return fn(table.unpack(args)) end)
|
|
||||||
end
|
|
||||||
return fn, m
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.run(env, path, ...)
|
|
||||||
setmetatable(env, { __index = _G })
|
|
||||||
local fn, m = loadfile(path, env)
|
|
||||||
if fn then
|
|
||||||
local args = { ... }
|
|
||||||
return pcall(function() return fn(table.unpack(args)) end)
|
|
||||||
end
|
|
||||||
return fn, m
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.runFunction(env, fn, ...)
|
|
||||||
setfenv(fn, env)
|
|
||||||
setmetatable(env, { __index = _G })
|
|
||||||
|
|
||||||
local args = { ... }
|
|
||||||
return pcall(function() return fn(table.unpack(args)) end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--[[ String functions ]] --
|
|
||||||
function Util.toBytes(n)
|
|
||||||
if n >= 1000000 or n <= -1000000 then
|
|
||||||
return string.format('%sM', Util.round(n/1000000, 1))
|
|
||||||
elseif n >= 1000 or n <= -1000 then
|
|
||||||
return string.format('%sK', Util.round(n/1000, 1))
|
|
||||||
end
|
|
||||||
return tostring(n)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.insertString(os, is, pos)
|
|
||||||
return os:sub(1, pos - 1) .. is .. os:sub(pos)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.split(str, pattern)
|
|
||||||
pattern = pattern or "(.-)\n"
|
|
||||||
local t = {}
|
|
||||||
local function helper(line) table.insert(t, line) return "" end
|
|
||||||
helper((str:gsub(pattern, helper)))
|
|
||||||
return t
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.matches(str, pattern)
|
|
||||||
pattern = pattern or '%S+'
|
|
||||||
local t = { }
|
|
||||||
for s in str:gmatch(pattern) do
|
|
||||||
table.insert(t, s)
|
|
||||||
end
|
|
||||||
return t
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.startsWidth(s, match)
|
|
||||||
return string.sub(s, 1, #match) == match
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.widthify(s, len)
|
|
||||||
s = s or ''
|
|
||||||
local slen = #s
|
|
||||||
if slen < len then
|
|
||||||
s = s .. string.rep(' ', len - #s)
|
|
||||||
elseif slen > len then
|
|
||||||
s = s:sub(1, len)
|
|
||||||
end
|
|
||||||
return s
|
|
||||||
end
|
|
||||||
|
|
||||||
-- http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
|
|
||||||
function Util.trim(s)
|
|
||||||
return s:find'^%s*$' and '' or s:match'^%s*(.*%S)'
|
|
||||||
end
|
|
||||||
|
|
||||||
-- trim whitespace from left end of string
|
|
||||||
function Util.triml(s)
|
|
||||||
return s:match'^%s*(.*)'
|
|
||||||
end
|
|
||||||
|
|
||||||
-- trim whitespace from right end of string
|
|
||||||
function Util.trimr(s)
|
|
||||||
return s:find'^%s*$' and '' or s:match'^(.*%S)'
|
|
||||||
end
|
|
||||||
-- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
|
|
||||||
|
|
||||||
-- word wrapping based on:
|
|
||||||
-- https://www.rosettacode.org/wiki/Word_wrap#Lua and
|
|
||||||
-- http://lua-users.org/wiki/StringRecipes
|
|
||||||
local function paragraphwrap(text, linewidth, res)
|
|
||||||
linewidth = linewidth or 75
|
|
||||||
local spaceleft = linewidth
|
|
||||||
local line = { }
|
|
||||||
|
|
||||||
for word in text:gmatch("%S+") do
|
|
||||||
local len = #word + 1
|
|
||||||
|
|
||||||
--if colorMode then
|
|
||||||
-- word:gsub('()@([@%d])', function(pos, c) len = len - 2 end)
|
|
||||||
--end
|
|
||||||
|
|
||||||
if len > spaceleft then
|
|
||||||
table.insert(res, table.concat(line, ' '))
|
|
||||||
line = { word }
|
|
||||||
spaceleft = linewidth - len - 1
|
|
||||||
else
|
|
||||||
table.insert(line, word)
|
|
||||||
spaceleft = spaceleft - len
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(res, table.concat(line, ' '))
|
|
||||||
return table.concat(res, '\n')
|
|
||||||
end
|
|
||||||
-- end word wrapping
|
|
||||||
|
|
||||||
function Util.wordWrap(str, limit)
|
|
||||||
|
|
||||||
local longLines = Util.split(str)
|
|
||||||
local lines = { }
|
|
||||||
|
|
||||||
for _,line in ipairs(longLines) do
|
|
||||||
paragraphwrap(line, limit, lines)
|
|
||||||
end
|
|
||||||
|
|
||||||
return lines
|
|
||||||
end
|
|
||||||
|
|
||||||
-- http://lua-users.org/wiki/AlternativeGetOpt
|
|
||||||
local function getopt( arg, options )
|
|
||||||
local tab = {}
|
|
||||||
for k, v in ipairs(arg) do
|
|
||||||
if type(v) == 'string' then
|
|
||||||
if string.sub( v, 1, 2) == "--" then
|
|
||||||
local x = string.find( v, "=", 1, true )
|
|
||||||
if x then tab[ string.sub( v, 3, x-1 ) ] = string.sub( v, x+1 )
|
|
||||||
else tab[ string.sub( v, 3 ) ] = true
|
|
||||||
end
|
|
||||||
elseif string.sub( v, 1, 1 ) == "-" then
|
|
||||||
local y = 2
|
|
||||||
local l = string.len(v)
|
|
||||||
local jopt
|
|
||||||
while ( y <= l ) do
|
|
||||||
jopt = string.sub( v, y, y )
|
|
||||||
if string.find( options, jopt, 1, true ) then
|
|
||||||
if y < l then
|
|
||||||
tab[ jopt ] = string.sub( v, y+1 )
|
|
||||||
y = l
|
|
||||||
else
|
|
||||||
tab[ jopt ] = arg[ k + 1 ]
|
|
||||||
end
|
|
||||||
else
|
|
||||||
tab[ jopt ] = true
|
|
||||||
end
|
|
||||||
y = y + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return tab
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.showOptions(options)
|
|
||||||
print('Arguments: ')
|
|
||||||
for k, v in pairs(options) do
|
|
||||||
print(string.format('-%s %s', v.arg, v.desc))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function Util.getOptions(options, args, ignoreInvalid)
|
|
||||||
local argLetters = ''
|
|
||||||
for _,o in pairs(options) do
|
|
||||||
if o.type ~= 'flag' then
|
|
||||||
argLetters = argLetters .. o.arg
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local rawOptions = getopt(args, argLetters)
|
|
||||||
local argCount = 0
|
|
||||||
|
|
||||||
for k,ro in pairs(rawOptions) do
|
|
||||||
local found = false
|
|
||||||
for _,o in pairs(options) do
|
|
||||||
if o.arg == k then
|
|
||||||
found = true
|
|
||||||
if o.type == 'number' then
|
|
||||||
o.value = tonumber(ro)
|
|
||||||
elseif o.type == 'help' then
|
|
||||||
Util.showOptions(options)
|
|
||||||
return false
|
|
||||||
else
|
|
||||||
o.value = ro
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if not found and not ignoreInvalid then
|
|
||||||
print('Invalid argument')
|
|
||||||
Util.showOptions(options)
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return true, Util.size(rawOptions)
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
return Util
|
|
||||||
@@ -1,26 +1,34 @@
|
|||||||
requireInjector(getfenv(1))
|
local Config = require('opus.config')
|
||||||
|
local Event = require('opus.event')
|
||||||
|
local pastebin = require('opus.http.pastebin')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local Config = require('config')
|
local colors = _G.colors
|
||||||
local Event = require('event')
|
local fs = _G.fs
|
||||||
local UI = require('ui')
|
local multishell = _ENV.multishell
|
||||||
local Util = require('util')
|
local os = _G.os
|
||||||
|
local shell = _ENV.shell
|
||||||
|
|
||||||
|
local FILE = 1
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'Files')
|
|
||||||
UI:configure('Files', ...)
|
UI:configure('Files', ...)
|
||||||
|
|
||||||
local config = {
|
local config = Config.load('Files', {
|
||||||
showHidden = false,
|
showHidden = false,
|
||||||
showDirSizes = false,
|
showDirSizes = false,
|
||||||
|
})
|
||||||
|
config.associations = config.associations or {
|
||||||
|
nft = 'pain',
|
||||||
|
txt = 'edit',
|
||||||
}
|
}
|
||||||
|
|
||||||
Config.load('Files', config)
|
|
||||||
|
|
||||||
local copied = { }
|
local copied = { }
|
||||||
local marked = { }
|
local marked = { }
|
||||||
local directories = { }
|
local directories = { }
|
||||||
local cutMode = false
|
local cutMode = false
|
||||||
|
|
||||||
function formatSize(size)
|
local function formatSize(size)
|
||||||
if size >= 1000000 then
|
if size >= 1000000 then
|
||||||
return string.format('%dM', math.floor(size/1000000, 2))
|
return string.format('%dM', math.floor(size/1000000, 2))
|
||||||
elseif size >= 1000 then
|
elseif size >= 1000 then
|
||||||
@@ -33,69 +41,164 @@ local Browser = UI.Page {
|
|||||||
menuBar = UI.MenuBar {
|
menuBar = UI.MenuBar {
|
||||||
buttons = {
|
buttons = {
|
||||||
{ text = '^-', event = 'updir' },
|
{ text = '^-', event = 'updir' },
|
||||||
{ text = 'File', event = 'dropdown', dropdown = 'fileMenu' },
|
{ text = 'File', dropdown = {
|
||||||
{ text = 'Edit', event = 'dropdown', dropdown = 'editMenu' },
|
{ text = 'Run', event = 'run', flags = FILE },
|
||||||
{ text = 'View', event = 'dropdown', dropdown = 'viewMenu' },
|
{ text = 'Edit e', event = 'edit', flags = FILE },
|
||||||
},
|
{ text = 'Cloud edit c', event = 'cedit', flags = FILE },
|
||||||
},
|
{ text = 'Pastebin put p', event = 'pastebin', flags = FILE },
|
||||||
fileMenu = UI.DropMenu {
|
|
||||||
buttons = {
|
|
||||||
{ text = 'Run', event = 'run' },
|
|
||||||
{ text = 'Edit e', event = 'edit' },
|
|
||||||
{ text = 'Shell s', event = 'shell' },
|
{ text = 'Shell s', event = 'shell' },
|
||||||
UI.Text { value = ' ------------ ' },
|
{ spacer = true },
|
||||||
{ text = 'Quit q', event = 'quit' },
|
{ text = 'Quit ^q', event = 'quit' },
|
||||||
UI.Text { },
|
} },
|
||||||
}
|
{ text = 'Edit', dropdown = {
|
||||||
},
|
|
||||||
editMenu = UI.DropMenu {
|
|
||||||
buttons = {
|
|
||||||
{ 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.Text { value = ' --------------- ' },
|
{ 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.Text { value = ' --------------- ' },
|
{ spacer = true },
|
||||||
{ text = 'Delete del', event = 'delete' },
|
{ text = 'Delete del', event = 'delete' },
|
||||||
UI.Text { },
|
} },
|
||||||
}
|
{ text = 'View', dropdown = {
|
||||||
},
|
|
||||||
viewMenu = UI.DropMenu {
|
|
||||||
buttons = {
|
|
||||||
{ text = 'Refresh r', event = 'refresh' },
|
{ text = 'Refresh r', event = 'refresh' },
|
||||||
{ 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' },
|
||||||
UI.Text { },
|
} },
|
||||||
}
|
{ 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 = 6 },
|
{ 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',
|
||||||
[ 'control-h' ] = 'toggle_hidden',
|
[ 'control-h' ] = 'toggle_hidden',
|
||||||
|
[ 'control-s' ] = 'toggle_dirSize',
|
||||||
[ 'control-x' ] = 'cut',
|
[ 'control-x' ] = 'cut',
|
||||||
[ 'control-c' ] = 'copy',
|
[ 'control-c' ] = 'copy',
|
||||||
paste = 'paste',
|
paste = 'paste',
|
||||||
@@ -107,71 +210,26 @@ function Browser:enable()
|
|||||||
self:setFocus(self.grid)
|
self:setFocus(self.grid)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Browser.grid:sortCompare(a, b)
|
function Browser.menuBar.getActive(_, menuItem)
|
||||||
if self.sortColumn == 'fsize' then
|
local file = Browser.grid:getSelected()
|
||||||
return a.size < b.size
|
if menuItem.flags == FILE then
|
||||||
elseif self.sortColumn == 'flags' then
|
return file and not file.isDir
|
||||||
return a.flags < b.flags
|
|
||||||
end
|
|
||||||
if a.isDir == b.isDir then
|
|
||||||
return a.name:lower() < b.name:lower()
|
|
||||||
end
|
|
||||||
return a.isDir
|
|
||||||
end
|
|
||||||
|
|
||||||
function Browser.grid:getRowTextColor(file, selected)
|
|
||||||
if file.marked then
|
|
||||||
return colors.green
|
|
||||||
end
|
|
||||||
if file.isDir then
|
|
||||||
return colors.cyan
|
|
||||||
end
|
|
||||||
if file.isReadOnly then
|
|
||||||
return colors.pink
|
|
||||||
end
|
|
||||||
return colors.white
|
|
||||||
end
|
|
||||||
|
|
||||||
function Browser.grid:getRowBackgroundColorX(file, selected)
|
|
||||||
if selected then
|
|
||||||
return colors.gray
|
|
||||||
end
|
|
||||||
return self.backgroundColor
|
|
||||||
end
|
|
||||||
|
|
||||||
function Browser.grid:eventHandler(event)
|
|
||||||
if event.type == 'copy' then -- let copy be handled by parent
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
return UI.ScrollingGrid.eventHandler(self, event)
|
|
||||||
end
|
|
||||||
|
|
||||||
function Browser.statusBar:draw()
|
|
||||||
if self.parent.dir then
|
|
||||||
local info = '#:' .. Util.size(self.parent.dir.files)
|
|
||||||
local numMarked = Util.size(marked)
|
|
||||||
if numMarked > 0 then
|
|
||||||
info = info .. ' M:' .. numMarked
|
|
||||||
end
|
|
||||||
self:setValue('info', info)
|
|
||||||
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
|
|
||||||
UI.StatusBar.draw(self)
|
|
||||||
end
|
end
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
function Browser:setStatus(status, ...)
|
function Browser:setStatus(status, ...)
|
||||||
self.statusBar:timedStatus(string.format(status, ...))
|
self.notification:info(string.format(status, ...))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Browser:unmarkAll()
|
function Browser.unmarkAll()
|
||||||
for k,m in pairs(marked) do
|
for _,m in pairs(marked) do
|
||||||
m.marked = false
|
m.marked = false
|
||||||
end
|
end
|
||||||
Util.clear(marked)
|
Util.clear(marked)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Browser:getDirectory(directory)
|
function Browser:getDirectory(directory)
|
||||||
|
|
||||||
local s, dir = pcall(function()
|
local s, dir = pcall(function()
|
||||||
|
|
||||||
local dir = directories[directory]
|
local dir = directories[directory]
|
||||||
@@ -195,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)
|
||||||
@@ -205,11 +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.directory = directory
|
file.flags = file.fstype or ' '
|
||||||
file.flags = ''
|
|
||||||
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)
|
||||||
@@ -217,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
|
||||||
@@ -233,13 +288,12 @@ 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
|
||||||
self.dir.index = self.grid:getIndex()
|
self.dir.index = self.grid:getIndex()
|
||||||
end
|
end
|
||||||
DIR = fs.combine('', dirName)
|
local DIR = fs.combine('', dirName)
|
||||||
shell.setDir(DIR)
|
shell.setDir(DIR)
|
||||||
local s, dir = self:getDirectory(DIR)
|
local s, dir = self:getDirectory(DIR)
|
||||||
if s then
|
if s then
|
||||||
@@ -259,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()
|
||||||
@@ -280,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)
|
||||||
@@ -334,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("(.*/)"))
|
||||||
@@ -345,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 k,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
|
||||||
@@ -378,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 k,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
|
||||||
@@ -399,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,80 +1,96 @@
|
|||||||
requireInjector(getfenv(1))
|
local fuzzy = require('opus.fuzzy')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local Event = require('event')
|
local help = _G.help
|
||||||
local UI = require('ui')
|
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'Help')
|
|
||||||
UI:configure('Help', ...)
|
UI:configure('Help', ...)
|
||||||
|
|
||||||
local files = { }
|
local topics = { }
|
||||||
for _,f in pairs(help.topics()) do
|
for _,topic in pairs(help.topics()) do
|
||||||
table.insert(files, { name = f })
|
table.insert(topics, { name = topic, lname = topic:lower() })
|
||||||
end
|
end
|
||||||
|
|
||||||
local page = UI.Page {
|
UI:addPage('main', UI.Page {
|
||||||
labelText = UI.Text {
|
UI.Text {
|
||||||
x = 3, y = 2,
|
x = 3, y = 2,
|
||||||
value = 'Search',
|
value = 'Search',
|
||||||
},
|
},
|
||||||
filter = UI.TextEntry {
|
UI.TextEntry {
|
||||||
x = 10, y = 2, ex = -3,
|
x = 10, y = 2, ex = -3,
|
||||||
limit = 32,
|
limit = 32,
|
||||||
},
|
},
|
||||||
grid = UI.ScrollingGrid {
|
grid = UI.ScrollingGrid {
|
||||||
y = 4,
|
y = 4,
|
||||||
values = files,
|
values = topics,
|
||||||
columns = {
|
columns = {
|
||||||
{ heading = 'Name', key = 'name' },
|
{ heading = 'Topic', key = 'name' },
|
||||||
},
|
},
|
||||||
sortColumn = 'name',
|
sortColumn = 'lname',
|
||||||
},
|
},
|
||||||
accelerators = {
|
accelerators = {
|
||||||
q = 'quit',
|
[ 'control-q' ] = 'quit',
|
||||||
enter = 'grid_select',
|
enter = 'grid_select',
|
||||||
},
|
},
|
||||||
}
|
eventHandler = function(self, event)
|
||||||
|
|
||||||
local function showHelp(name)
|
|
||||||
UI.term:reset()
|
|
||||||
shell.run('help ' .. name)
|
|
||||||
print('Press enter to return')
|
|
||||||
repeat
|
|
||||||
os.pullEvent('key')
|
|
||||||
local _, k = os.pullEvent('key_up')
|
|
||||||
until k == keys.enter
|
|
||||||
end
|
|
||||||
|
|
||||||
function page:eventHandler(event)
|
|
||||||
|
|
||||||
if event.type == 'quit' then
|
if event.type == 'quit' then
|
||||||
Event.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
|
||||||
showHelp(self.grid:getSelected().name)
|
UI:setPage('topic', self.grid:getSelected().name)
|
||||||
self:setFocus(self.filter)
|
|
||||||
self:draw()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif event.type == 'text_change' then
|
elseif event.type == 'text_change' then
|
||||||
local text = event.text
|
if not event.text then
|
||||||
if #text == 0 then
|
self.grid.sortColumn = 'lname'
|
||||||
self.grid.values = files
|
|
||||||
else
|
else
|
||||||
self.grid.values = { }
|
self.grid.sortColumn = 'score'
|
||||||
for _,f in pairs(files) do
|
self.grid.inverseSort = false
|
||||||
if string.find(f.name, 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
|
|
||||||
UI.Page.eventHandler(self, event)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
UI:setPage(page)
|
else
|
||||||
UI:pullEvents()
|
return UI.Page.eventHandler(self, event)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
|
UI:addPage('topic', UI.Page {
|
||||||
|
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()
|
||||||
|
|||||||
194
sys/apps/Lua.lua
194
sys/apps/Lua.lua
@@ -1,20 +1,21 @@
|
|||||||
requireInjector = requireInjector or load(http.get('https://raw.githubusercontent.com/kepler155c/opus/master/sys/apis/injector.lua').readAll())()
|
local History = require('opus.history')
|
||||||
requireInjector(getfenv(1))
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local Event = require('event')
|
local colors = _G.colors
|
||||||
local History = require('history')
|
local os = _G.os
|
||||||
local UI = require('ui')
|
local textutils = _G.textutils
|
||||||
local Util = require('util')
|
local term = _G.term
|
||||||
|
|
||||||
local sandboxEnv = setmetatable(Util.shallowCopy(getfenv(1)), { __index = _G })
|
local sandboxEnv = setmetatable(Util.shallowCopy(_ENV), { __index = _G })
|
||||||
sandboxEnv.exit = function() Event.exitPullEvents() end
|
sandboxEnv.exit = function() UI:quit() end
|
||||||
sandboxEnv._echo = function( ... ) return ... end
|
sandboxEnv._echo = function( ... ) return { ... } end
|
||||||
requireInjector(sandboxEnv)
|
_G.requireInjector(sandboxEnv)
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'Lua')
|
|
||||||
UI:configure('Lua', ...)
|
UI:configure('Lua', ...)
|
||||||
|
|
||||||
local command = ''
|
local command = ''
|
||||||
|
local counter = 1
|
||||||
local history = History.load('usr/.lua_history', 25)
|
local history = History.load('usr/.lua_history', 25)
|
||||||
|
|
||||||
local page = UI.Page {
|
local page = UI.Page {
|
||||||
@@ -28,17 +29,20 @@ 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',
|
||||||
down = 'history_forward',
|
down = 'history_forward',
|
||||||
mouse_rightclick = 'clear_prompt',
|
mouse_rightclick = 'clear_prompt',
|
||||||
-- [ 'control-space' ] = 'autocomplete',
|
[ 'control-space' ] = 'autocomplete',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
grid = UI.ScrollingGrid {
|
tabs = UI.Tabs {
|
||||||
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' },
|
||||||
@@ -46,17 +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,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page.grid = page.tabs.formatted.grid
|
||||||
|
page.output = page.tabs.output.output
|
||||||
|
|
||||||
function page:setPrompt(value, focus)
|
function page:setPrompt(value, focus)
|
||||||
self.prompt:setValue(value)
|
self.prompt:setValue(value)
|
||||||
self.prompt.scroll = 0
|
|
||||||
self.prompt:setPosition(#value)
|
|
||||||
self.prompt:updateScroll()
|
|
||||||
|
|
||||||
if value:sub(-1) == ')' then
|
if value:sub(-1) == ')' then
|
||||||
self.prompt:setPosition(#value - 1)
|
self.prompt:setPosition(#value - 1)
|
||||||
|
else
|
||||||
|
self.prompt:setPosition(#value)
|
||||||
end
|
end
|
||||||
|
|
||||||
self.prompt:draw()
|
self.prompt:draw()
|
||||||
@@ -66,12 +87,11 @@ 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)
|
||||||
|
|
||||||
local sLine = oLine:sub(1, x)
|
local sLine = oLine:sub(1, x)
|
||||||
local nStartPos = sLine:find("[a-zA-Z0-9_%.]+$")
|
local nStartPos = sLine:find("[a-zA-Z0-9_%.]+$")
|
||||||
if nStartPos then
|
if nStartPos then
|
||||||
@@ -81,10 +101,7 @@ local function autocomplete(env, oLine, x)
|
|||||||
if #sLine > 0 then
|
if #sLine > 0 then
|
||||||
local results = textutils.complete(sLine, env)
|
local results = textutils.complete(sLine, env)
|
||||||
|
|
||||||
if #results == 0 then
|
if #results == 1 then
|
||||||
-- setError('No completions available')
|
|
||||||
|
|
||||||
elseif #results == 1 then
|
|
||||||
return Util.insertString(oLine, results[1], x + 1)
|
return Util.insertString(oLine, results[1], x + 1)
|
||||||
|
|
||||||
elseif #results > 1 then
|
elseif #results > 1 then
|
||||||
@@ -100,8 +117,6 @@ local function autocomplete(env, oLine, x)
|
|||||||
end
|
end
|
||||||
if #prefix > 0 then
|
if #prefix > 0 then
|
||||||
return Util.insertString(oLine, prefix, x + 1)
|
return Util.insertString(oLine, prefix, x + 1)
|
||||||
else
|
|
||||||
-- setStatus('Too many results')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -109,32 +124,31 @@ local function autocomplete(env, oLine, x)
|
|||||||
end
|
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('getfenv(0)')
|
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('getfenv(1)')
|
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 = { }
|
|
||||||
for _,side in pairs(peripheral.getNames()) do
|
|
||||||
local key = string.format('%s:%s', peripheral.getType(side), side)
|
|
||||||
sandboxEnv.device[ key ] = peripheral.wrap(side)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
self:setPrompt('device', true)
|
self:setPrompt('device', true)
|
||||||
self:executeStatement('device')
|
self:executeStatement('device')
|
||||||
|
|
||||||
@@ -152,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 = { }
|
||||||
@@ -172,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
|
||||||
|
|
||||||
@@ -187,8 +198,7 @@ function page:setResult(result)
|
|||||||
local t = { }
|
local t = { }
|
||||||
|
|
||||||
local function safeValue(v)
|
local function safeValue(v)
|
||||||
local t = type(v)
|
if type(v) == 'string' or type(v) == 'number' then
|
||||||
if t == 'string' or t == 'number' then
|
|
||||||
return v
|
return v
|
||||||
end
|
end
|
||||||
return tostring(v)
|
return tostring(v)
|
||||||
@@ -206,7 +216,7 @@ function page:setResult(result)
|
|||||||
if Util.size(v) == 0 then
|
if Util.size(v) == 0 then
|
||||||
entry.value = 'table: (empty)'
|
entry.value = 'table: (empty)'
|
||||||
else
|
else
|
||||||
entry.value = 'table'
|
entry.value = tostring(v)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.insert(t, entry)
|
table.insert(t, entry)
|
||||||
@@ -220,12 +230,10 @@ 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)
|
||||||
|
|
||||||
local entry = self:getSelected()
|
local entry = self:getSelected()
|
||||||
|
|
||||||
local function commandAppend()
|
local function commandAppend()
|
||||||
@@ -259,9 +267,10 @@ function page.grid:eventHandler(event)
|
|||||||
elseif event.type == 'grid_select' then
|
elseif event.type == 'grid_select' then
|
||||||
page:setPrompt(commandAppend(), true)
|
page:setPrompt(commandAppend(), true)
|
||||||
page:executeStatement(commandAppend())
|
page:executeStatement(commandAppend())
|
||||||
|
|
||||||
elseif event.type == 'copy' then
|
elseif event.type == 'copy' then
|
||||||
if entry then
|
if entry then
|
||||||
clipboard.setData(entry.rawValue)
|
os.queueEvent('clipboard_copy', entry.rawValue)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
return UI.ScrollingGrid.eventHandler(self, event)
|
return UI.ScrollingGrid.eventHandler(self, event)
|
||||||
@@ -270,48 +279,93 @@ function page.grid:eventHandler(event)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function page:rawExecute(s)
|
function page:rawExecute(s)
|
||||||
local fn, m = load('return _echo(' ..s.. ');', 'lua', nil, sandboxEnv)
|
local fn, m
|
||||||
|
local wrapped
|
||||||
|
|
||||||
|
fn = load('return (' ..s.. ')', 'lua', nil, sandboxEnv)
|
||||||
|
|
||||||
if fn then
|
if fn then
|
||||||
m = { pcall(fn) }
|
fn = load('return {' ..s.. '}', 'lua', nil, sandboxEnv)
|
||||||
fn = table.remove(m, 1)
|
wrapped = true
|
||||||
if #m == 1 then
|
|
||||||
m = m[1]
|
|
||||||
end
|
|
||||||
return fn, m
|
|
||||||
end
|
end
|
||||||
|
|
||||||
fn, m = load(s, 'lua', nil, sandboxEnv)
|
local t = os.clock()
|
||||||
if fn then
|
if fn then
|
||||||
fn, m = pcall(fn)
|
fn, m = pcall(fn)
|
||||||
|
if #m <= 1 and wrapped then
|
||||||
|
m = m[1]
|
||||||
|
end
|
||||||
|
else
|
||||||
|
fn, m = load(s, 'lua', nil, sandboxEnv)
|
||||||
|
if fn then
|
||||||
|
t = os.clock()
|
||||||
|
fn, m = pcall(fn)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if fn then
|
||||||
|
t = os.clock() - t
|
||||||
|
|
||||||
|
local bg, fg = term.getBackgroundColor(), term.getTextColor()
|
||||||
|
term.setTextColor(colors.cyan)
|
||||||
|
term.setBackgroundColor(colors.black)
|
||||||
|
term.write(string.format('out [%.2f]: ', t))
|
||||||
|
term.setBackgroundColor(bg)
|
||||||
|
term.setTextColor(fg)
|
||||||
|
if m or wrapped then
|
||||||
|
Util.print(m or 'nil')
|
||||||
|
else
|
||||||
|
print()
|
||||||
|
end
|
||||||
|
else
|
||||||
|
_G.printError(m)
|
||||||
end
|
end
|
||||||
|
|
||||||
return fn, m
|
return fn, m
|
||||||
end
|
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
|
||||||
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,31 +1,51 @@
|
|||||||
requireInjector(getfenv(1))
|
local Config = require('opus.config')
|
||||||
|
local Event = require('opus.event')
|
||||||
|
local Socket = require('opus.socket')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local Event = require('event')
|
local device = _G.device
|
||||||
local Socket = require('socket')
|
local network = _G.network
|
||||||
local UI = require('ui')
|
local shell = _ENV.shell
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'Network')
|
|
||||||
UI:configure('Network', ...)
|
UI:configure('Network', ...)
|
||||||
|
|
||||||
local gridColumns = {
|
local gridColumns = {
|
||||||
{ heading = 'Label', key = 'label' },
|
{ heading = 'Label', key = 'label' },
|
||||||
{ heading = 'Dist', key = 'distance' },
|
{ heading = 'Dist', key = 'distance', align = 'right' },
|
||||||
{ heading = 'Status', key = 'status' },
|
{ heading = 'Status', key = 'status' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local config = Config.load('network', { })
|
||||||
|
|
||||||
if UI.term.width >= 30 then
|
if UI.term.width >= 30 then
|
||||||
table.insert(gridColumns, { heading = 'Fuel', key = 'fuel', width = 5 })
|
table.insert(gridColumns, { heading = 'Fuel', key = 'fuel', width = 5, align = 'right' })
|
||||||
table.insert(gridColumns, { heading = 'Uptime', key = 'uptime' })
|
end
|
||||||
|
if UI.term.width >= 40 then
|
||||||
|
table.insert(gridColumns, { heading = 'Uptime', key = 'uptime', align = 'right' })
|
||||||
end
|
end
|
||||||
|
|
||||||
local page = UI.Page {
|
local page = UI.Page {
|
||||||
menuBar = UI.MenuBar {
|
menuBar = UI.MenuBar {
|
||||||
buttons = {
|
buttons = {
|
||||||
{ text = 'Telnet', event = 'telnet' },
|
{ text = 'Connect', dropdown = {
|
||||||
{ text = 'VNC', event = 'vnc' },
|
{ text = 'Telnet t', event = 'telnet' },
|
||||||
{ text = 'Trust', event = 'trust' },
|
{ text = 'VNC v', event = 'vnc' },
|
||||||
{ text = 'Reboot', event = 'reboot' },
|
{ spacer = true },
|
||||||
|
{ text = 'Reboot r', event = 'reboot' },
|
||||||
|
} },
|
||||||
|
{ text = 'Trust', dropdown = {
|
||||||
|
{ text = 'Establish', event = 'trust' },
|
||||||
|
} },
|
||||||
|
{
|
||||||
|
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 {
|
||||||
@@ -34,16 +54,71 @@ 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 = {
|
||||||
q = 'quit',
|
t = 'telnet',
|
||||||
|
v = 'vnc',
|
||||||
|
r = 'reboot',
|
||||||
|
[ 'control-q' ] = 'quit',
|
||||||
c = 'clear',
|
c = 'clear',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
local function sendCommand(host, command)
|
local function sendCommand(host, command)
|
||||||
|
|
||||||
if not device.wireless_modem then
|
if not device.wireless_modem then
|
||||||
page.notification:error('Wireless modem not present')
|
page.notification:error('Wireless modem not present')
|
||||||
return
|
return
|
||||||
@@ -62,60 +137,120 @@ 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' or event.type == 'grid_select' then
|
if event.type == 'telnet' then
|
||||||
multishell.openTab({
|
shell.openForegroundTab('telnet ' .. t.id)
|
||||||
path = 'sys/apps/telnet.lua',
|
|
||||||
focused = true,
|
|
||||||
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
|
||||||
|
Util.clear(network)
|
||||||
|
page.grid:update()
|
||||||
|
page.grid:draw()
|
||||||
|
|
||||||
elseif event.type == 'trust' then
|
elseif event.type == 'trust' then
|
||||||
shell.openForegroundTab('trust ' .. t.id)
|
shell.openForegroundTab('trust ' .. t.id)
|
||||||
|
|
||||||
elseif event.type == 'reboot' then
|
elseif event.type == 'reboot' then
|
||||||
sendCommand(t.id, 'reboot')
|
sendCommand(t.id, 'reboot')
|
||||||
|
|
||||||
elseif event.type == 'shutdown' then
|
elseif event.type == 'shutdown' then
|
||||||
sendCommand(t.id, 'shutdown')
|
sendCommand(t.id, 'shutdown')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if event.type == 'quit' then
|
|
||||||
Event.exitPullEvents()
|
if event.type == 'help' then
|
||||||
|
shell.switchTab(shell.openTab('Help Networking'))
|
||||||
|
|
||||||
|
elseif event.type == 'ports' then
|
||||||
|
self.ports.grid:update()
|
||||||
|
self.ports:show()
|
||||||
|
|
||||||
|
-- self.portsHandler = Event.onInterval(3, function()
|
||||||
|
-- self.ports.grid:update()
|
||||||
|
-- self.ports.grid:draw()
|
||||||
|
-- self:sync()
|
||||||
|
-- end)
|
||||||
|
|
||||||
|
elseif event.type == 'ports_update' then
|
||||||
|
self.ports.grid:update()
|
||||||
|
self.ports.grid:draw()
|
||||||
|
self:sync()
|
||||||
|
|
||||||
|
elseif event.type == 'ports_hide' then
|
||||||
|
Event.off(self.portsHandler)
|
||||||
|
self.ports:hide()
|
||||||
|
|
||||||
|
elseif event.type == 'show_trusted' then
|
||||||
|
config.showTrusted = true
|
||||||
|
Config.update('network', config)
|
||||||
|
|
||||||
|
elseif event.type == 'quit' then
|
||||||
|
UI:quit()
|
||||||
end
|
end
|
||||||
UI.Page.eventHandler(self, event)
|
UI.Page.eventHandler(self, event)
|
||||||
end
|
end
|
||||||
|
|
||||||
function page.grid:getRowTextColor(row, selected)
|
function page.menuBar:getActive(menuItem)
|
||||||
if not row.active then
|
local t = page.grid:getSelected()
|
||||||
return colors.orange
|
if menuItem.modem then
|
||||||
|
return not not device.wireless_modem
|
||||||
end
|
end
|
||||||
return UI.Grid.getRowTextColor(self, row, selected)
|
return menuItem.noCheck or not not t
|
||||||
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()
|
||||||
@@ -124,14 +259,14 @@ Event.onInterval(1, function()
|
|||||||
page:sync()
|
page:sync()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Event.on('device_attach', function(h, deviceName)
|
Event.on('device_attach', function(_, deviceName)
|
||||||
if deviceName == 'wireless_modem' then
|
if deviceName == 'wireless_modem' then
|
||||||
page.notification:success('Modem connected')
|
page.notification:success('Modem connected')
|
||||||
page:sync()
|
page:sync()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
Event.on('device_detach', function(h, deviceName)
|
Event.on('device_detach', function(_, deviceName)
|
||||||
if deviceName == 'wireless_modem' then
|
if deviceName == 'wireless_modem' then
|
||||||
page.notification:error('Wireless modem not attached')
|
page.notification:error('Wireless modem not attached')
|
||||||
page:sync()
|
page:sync()
|
||||||
@@ -143,4 +278,4 @@ if not device.wireless_modem then
|
|||||||
end
|
end
|
||||||
|
|
||||||
UI:setPage(page)
|
UI:setPage(page)
|
||||||
UI:pullEvents()
|
UI:start()
|
||||||
|
|||||||
@@ -1,30 +1,48 @@
|
|||||||
requireInjector(getfenv(1))
|
local Array = require('opus.array')
|
||||||
|
local class = require('opus.class')
|
||||||
|
local Config = require('opus.config')
|
||||||
|
local Event = require('opus.event')
|
||||||
|
local NFT = require('opus.nft')
|
||||||
|
local Packages = require('opus.packages')
|
||||||
|
local SHA = require('opus.crypto.sha2')
|
||||||
|
local Tween = require('opus.ui.tween')
|
||||||
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local class = require('class')
|
local device = _G.device
|
||||||
local Config = require('config')
|
local fs = _G.fs
|
||||||
local Event = require('event')
|
local os = _G.os
|
||||||
local FileUI = require('ui.fileui')
|
local pocket = _G.pocket
|
||||||
local NFT = require('nft')
|
local shell = _ENV.shell
|
||||||
local SHA1 = require('sha1')
|
local term = _G.term
|
||||||
local Tween = require('ui.tween')
|
local turtle = _G.turtle
|
||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local REGISTRY_DIR = 'usr/.registry'
|
--[[
|
||||||
local TEMPLATE = [[
|
turtle: 39x13
|
||||||
local env = { }
|
computer: 51x19
|
||||||
for k,v in pairs(getfenv(1)) do
|
pocket: 26x20
|
||||||
env[k] = v
|
|
||||||
end
|
|
||||||
setmetatable(env, { __index = _G })
|
|
||||||
|
|
||||||
local s, m = os.run(env, 'sys/apps/appRun.lua', %s, ...)
|
|
||||||
if not s then
|
|
||||||
error(m)
|
|
||||||
end
|
|
||||||
]]
|
]]
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'Overview')
|
if not _ENV.multishell then
|
||||||
|
error('multishell is required')
|
||||||
|
end
|
||||||
|
|
||||||
|
local REGISTRY_DIR = 'usr/.registry'
|
||||||
|
|
||||||
|
-- iconExt:gsub('.', function(b) return '\\' .. b:byte() end)
|
||||||
|
local DEFAULT_ICON = NFT.parse('\30\55\31\48\136\140\140\140\132\
|
||||||
|
\30\48\31\55\149\31\48\128\128\128\30\55\149\
|
||||||
|
\30\55\31\48\138\143\143\143\133')
|
||||||
|
local TRANS_ICON = NFT.parse('\0302\0312\32\32\32\32\32\
|
||||||
|
\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 = {
|
||||||
@@ -33,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,128 +228,51 @@ local function loadApplications()
|
|||||||
end
|
end
|
||||||
|
|
||||||
Util.each(applications, function(v, k) v.key = k end)
|
Util.each(applications, function(v, k) v.key = k end)
|
||||||
applications = Util.filter(applications, function(_, a)
|
applications = Util.filter(applications, function(a)
|
||||||
if a.disabled then
|
if a.disabled then
|
||||||
return false
|
return false
|
||||||
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 = { }
|
||||||
table.insert(buttons, { text = 'Recent', event = 'category' })
|
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, event = 'category' })
|
table.insert(buttons, {
|
||||||
|
text = f.category,
|
||||||
|
width = 8,
|
||||||
|
selected = config.currentCategory == f.category
|
||||||
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
table.insert(buttons, { text = '+', event = 'new' })
|
table.sort(buttons, function(a, b) return a.text < b.text end)
|
||||||
|
table.insert(buttons, 1, { text = 'Recent' })
|
||||||
|
|
||||||
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:init(args)
|
page.container:setCategory(config.currentCategory or 'Apps')
|
||||||
UI.TabBar.init(self, args)
|
|
||||||
self.x = 1
|
|
||||||
self.width = 8
|
|
||||||
self.height = nil
|
|
||||||
self.ey = -1
|
|
||||||
for k,c in pairs(self.children) do
|
|
||||||
c.x = 1
|
|
||||||
c.y = k + 1
|
|
||||||
c.width = 8
|
|
||||||
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.ViewportWindow {
|
|
||||||
x = cx,
|
|
||||||
y = cy,
|
|
||||||
},
|
|
||||||
notification = UI.Notification(),
|
|
||||||
accelerators = {
|
|
||||||
r = 'refresh',
|
|
||||||
e = 'edit',
|
|
||||||
f = 'files',
|
|
||||||
s = 'shell',
|
|
||||||
l = 'lua',
|
|
||||||
[ 'control-l' ] = 'refresh',
|
|
||||||
[ 'control-n' ] = 'new',
|
|
||||||
delete = 'delete',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function page:draw()
|
|
||||||
self.tabBar:draw()
|
|
||||||
self.container:draw()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
UI.Icon = class(UI.Window)
|
UI.Icon = class(UI.Window)
|
||||||
function UI.Icon:init(args)
|
UI.Icon.defaults = {
|
||||||
local defaults = {
|
|
||||||
UIElement = 'Icon',
|
UIElement = 'Icon',
|
||||||
width = 14,
|
width = 14,
|
||||||
height = 4,
|
height = 4,
|
||||||
}
|
}
|
||||||
UI:setProperties(defaults, args)
|
|
||||||
UI.Window.init(self, defaults)
|
|
||||||
end
|
|
||||||
|
|
||||||
function UI.Icon:eventHandler(event)
|
function UI.Icon:eventHandler(event)
|
||||||
if event.type == 'mouse_click' then
|
if event.type == 'mouse_click' then
|
||||||
self:setFocus(self.button)
|
self:setFocus(self.button)
|
||||||
@@ -194,74 +286,80 @@ function UI.Icon:eventHandler(event)
|
|||||||
return UI.Window.eventHandler(self, event)
|
return UI.Window.eventHandler(self, event)
|
||||||
end
|
end
|
||||||
|
|
||||||
function page.container:setCategory(categoryName)
|
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 = '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
|
||||||
@@ -270,53 +368,69 @@ function page.container:setCategory(categoryName)
|
|||||||
local col, row = gutter, 2
|
local col, row = gutter, 2
|
||||||
local count = #self.children
|
local count = #self.children
|
||||||
|
|
||||||
local r = math.random(1, 4)
|
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)
|
||||||
elseif r == 4 then
|
elseif r == 4 then
|
||||||
child.x = self.width - col
|
child.x = self.width - col
|
||||||
child.y = row
|
child.y = row
|
||||||
|
elseif r == 5 then
|
||||||
|
child.x = col
|
||||||
|
child.y = row
|
||||||
|
if k == #self.children then
|
||||||
|
child.x = self.width
|
||||||
|
child.y = self.height - 3
|
||||||
end
|
end
|
||||||
child.tween = Tween.new(6, child, { x = col, y = row }, 'linear')
|
elseif r == 6 then
|
||||||
|
child.x = col
|
||||||
|
child.y = 1
|
||||||
|
elseif r == 7 then
|
||||||
|
child.x = 1
|
||||||
|
child.y = self.height - 3
|
||||||
|
end
|
||||||
|
child.tween = Tween.new(frames, child, { x = col, y = row }, 'inQuad')
|
||||||
|
|
||||||
|
if not animate then
|
||||||
|
child.x = col
|
||||||
|
child.y = row
|
||||||
|
end
|
||||||
|
|
||||||
|
self:setViewHeight(row + (config.listMode and 1 or 4))
|
||||||
|
|
||||||
if k < count then
|
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
|
||||||
local transition = { i = 1, parent = self, children = self.children }
|
local function transition()
|
||||||
function transition:update(device)
|
local i = 1
|
||||||
self.parent:clear()
|
return function()
|
||||||
for _,child in ipairs(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)
|
end
|
||||||
child:draw()
|
i = i + 1
|
||||||
|
return i <= frames
|
||||||
end
|
end
|
||||||
self.canvas:blit(device, self, self)
|
|
||||||
self.i = self.i + 1
|
|
||||||
return self.i < 7
|
|
||||||
end
|
end
|
||||||
self:addTransition(transition)
|
self:addTransition(transition)
|
||||||
end
|
end
|
||||||
|
|
||||||
function page.container:draw()
|
|
||||||
UI.ViewportWindow.draw(self)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function page:refresh()
|
function page:refresh()
|
||||||
@@ -332,12 +446,9 @@ function page:resize()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function page:eventHandler(event)
|
function page:eventHandler(event)
|
||||||
|
if event.type == 'tab_select' then
|
||||||
if event.type == 'category' then
|
self.container:setCategory(event.button.text, true)
|
||||||
self.tabBar:selectTab(event.button.text)
|
|
||||||
self.container:setCategory(event.button.text)
|
|
||||||
self.container:draw()
|
self.container:draw()
|
||||||
self:sync()
|
|
||||||
|
|
||||||
config.currentCategory = event.button.text
|
config.currentCategory = event.button.text
|
||||||
Config.update('Overview', config)
|
Config.update('Overview', config)
|
||||||
@@ -354,44 +465,29 @@ 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
|
||||||
event.focused.parent:scrollIntoView()
|
event.focused.parent:scrollIntoView()
|
||||||
end
|
end
|
||||||
|
|
||||||
elseif event.type == 'tab_change' then
|
elseif event.type == 'refresh' then -- remove this after fixing notification
|
||||||
if event.current > event.last then
|
|
||||||
--self.container:setTransition(UI.effect.slideLeft)
|
|
||||||
else
|
|
||||||
--self.container:setTransition(UI.effect.slideRight)
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif event.type == 'refresh' then
|
|
||||||
loadApplications()
|
loadApplications()
|
||||||
self:refresh()
|
self:refresh()
|
||||||
self:draw()
|
self:draw()
|
||||||
@@ -400,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()
|
||||||
@@ -414,152 +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,
|
|
||||||
})
|
|
||||||
--fileui:setTransition(UI.effect.explode)
|
|
||||||
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)
|
||||||
|
|
||||||
Event.pullEvents()
|
loadApplications()
|
||||||
UI.term:reset()
|
|
||||||
|
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,184 +1,82 @@
|
|||||||
requireInjector(getfenv(1))
|
local UI = require('opus.ui')
|
||||||
|
local Util = require('opus.util')
|
||||||
|
|
||||||
local Config = require('config')
|
local fs = _G.fs
|
||||||
local Event = require('event')
|
local shell = _ENV.shell
|
||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'System')
|
|
||||||
UI:configure('System', ...)
|
UI:configure('System', ...)
|
||||||
|
|
||||||
local env = {
|
local function loadDirectory(dir)
|
||||||
path = shell.path(),
|
local plugins = { }
|
||||||
aliases = shell.aliases(),
|
for _, file in pairs(fs.list(dir)) do
|
||||||
lua_path = LUA_PATH,
|
local s, m = Util.run(_ENV, fs.combine(dir, file))
|
||||||
}
|
if not s and m then
|
||||||
Config.load('shell', env)
|
_G.printError('Error loading: ' .. file)
|
||||||
|
error(m or 'Unknown error')
|
||||||
|
elseif s and m then
|
||||||
|
table.insert(plugins, { tab = m, name = m.title, description = m.description })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return plugins
|
||||||
|
end
|
||||||
|
|
||||||
local systemPage = UI.Page {
|
local programDir = fs.getDir(_ENV.arg[0])
|
||||||
|
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,
|
|
||||||
values = paths,
|
|
||||||
disableHeader = true,
|
|
||||||
columns = { { key = 'value' } },
|
|
||||||
autospace = true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
aliasTab = UI.Window {
|
|
||||||
tabTitle = 'Aliases',
|
|
||||||
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,
|
|
||||||
values = aliases,
|
|
||||||
autospace = true,
|
|
||||||
sortColumn = 'alias',
|
|
||||||
columns = {
|
|
||||||
{ heading = 'Alias', key = 'alias' },
|
|
||||||
{ heading = 'Program', key = 'path' },
|
|
||||||
},
|
|
||||||
accelerators = {
|
|
||||||
delete = 'delete_alias',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
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 = _MC_VERSION or 'unknown' },
|
|
||||||
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
|
|
||||||
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
|
|
||||||
{ name = 'Day', value = tostring(os.day()) },
|
|
||||||
},
|
|
||||||
selectable = false,
|
|
||||||
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)
|
||||||
|
|
||||||
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 = { }
|
|
||||||
local aliases = { }
|
|
||||||
for k,v in pairs(env.aliases) do
|
|
||||||
table.insert(self.values, { alias = k, path = v })
|
|
||||||
end
|
|
||||||
self:update()
|
|
||||||
UI.Grid.draw(self)
|
|
||||||
end
|
|
||||||
|
|
||||||
function systemPage.tabs.aliasTab:eventHandler(event)
|
|
||||||
|
|
||||||
if event.type == 'delete_alias' then
|
|
||||||
env.aliases[self.grid:getSelected().alias] = nil
|
|
||||||
self.grid:setIndex(self.grid:getIndex())
|
|
||||||
self.grid:draw()
|
|
||||||
Config.update('shell', env)
|
|
||||||
systemPage.notification:success('reboot to take effect')
|
|
||||||
return true
|
|
||||||
|
|
||||||
elseif event.type == 'new_alias' then
|
|
||||||
env.aliases[self.alias.value] = self.path.value
|
|
||||||
self.alias:reset()
|
|
||||||
self.path:reset()
|
|
||||||
self:draw()
|
|
||||||
self:setFocus(self.alias)
|
|
||||||
Config.update('shell', env)
|
|
||||||
systemPage.notification:success('reboot to take effect')
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function systemPage.tabs.infoTab:eventHandler(event)
|
|
||||||
if event.type == 'update_label' then
|
|
||||||
os.setComputerLabel(self.label.value)
|
|
||||||
systemPage.notification:success('Label updated')
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function systemPage:eventHandler(event)
|
|
||||||
|
|
||||||
if event.type == 'quit' then
|
if event.type == 'quit' then
|
||||||
Event.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)
|
||||||
Event.pullEvents()
|
UI:start()
|
||||||
UI.term:reset()
|
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
requireInjector(getfenv(1))
|
|
||||||
|
|
||||||
local Event = require('event')
|
|
||||||
local UI = require('ui')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
multishell.setTitle(multishell.getCurrent(), 'Tabs')
|
|
||||||
UI:configure('Tabs', ...)
|
|
||||||
|
|
||||||
local page = UI.Page {
|
|
||||||
menuBar = UI.MenuBar {
|
|
||||||
buttons = {
|
|
||||||
{ text = 'Activate', event = 'activate' },
|
|
||||||
{ text = 'Terminate', event = 'terminate' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
grid = UI.ScrollingGrid {
|
|
||||||
y = 2,
|
|
||||||
columns = {
|
|
||||||
{ heading = 'ID', key = 'tabId', width = 4 },
|
|
||||||
{ heading = 'Title', key = 'title' },
|
|
||||||
{ heading = 'Status', key = 'status' },
|
|
||||||
{ heading = 'Time', key = 'timestamp' },
|
|
||||||
},
|
|
||||||
values = multishell.getTabs(),
|
|
||||||
sortColumn = 'title',
|
|
||||||
autospace = true,
|
|
||||||
},
|
|
||||||
accelerators = {
|
|
||||||
q = 'quit',
|
|
||||||
space = 'activate',
|
|
||||||
t = 'terminate',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function page:eventHandler(event)
|
|
||||||
local t = self.grid:getSelected()
|
|
||||||
if t then
|
|
||||||
if event.type == 'activate' or event.type == 'grid_select' then
|
|
||||||
multishell.setFocus(t.tabId)
|
|
||||||
elseif event.type == 'terminate' then
|
|
||||||
multishell.terminate(t.tabId)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if event.type == 'quit' then
|
|
||||||
Event.exitPullEvents()
|
|
||||||
end
|
|
||||||
UI.Page.eventHandler(self, event)
|
|
||||||
end
|
|
||||||
|
|
||||||
function page.grid:getDisplayValues(row)
|
|
||||||
row = Util.shallowCopy(row)
|
|
||||||
local elapsed = os.clock()-row.timestamp
|
|
||||||
if elapsed < 60 then
|
|
||||||
row.timestamp = string.format("%ds", math.floor(elapsed))
|
|
||||||
else
|
|
||||||
row.timestamp = string.format("%sm", math.floor(elapsed/6)/10)
|
|
||||||
end
|
|
||||||
if row.isDead then
|
|
||||||
row.status = 'error'
|
|
||||||
else
|
|
||||||
row.status = coroutine.status(row.co)
|
|
||||||
end
|
|
||||||
return row
|
|
||||||
end
|
|
||||||
|
|
||||||
Event.onInterval(1, function()
|
|
||||||
page.grid:update()
|
|
||||||
page.grid:draw()
|
|
||||||
page:sync()
|
|
||||||
end)
|
|
||||||
|
|
||||||
UI:setPage(page)
|
|
||||||
UI:pullEvents()
|
|
||||||
75
sys/apps/Tasks.lua
Normal file
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,624 +0,0 @@
|
|||||||
-- Default label
|
|
||||||
if not os.getComputerLabel() then
|
|
||||||
local id = os.getComputerID()
|
|
||||||
if turtle then
|
|
||||||
os.setComputerLabel('turtle_' .. id)
|
|
||||||
elseif pocket then
|
|
||||||
os.setComputerLabel('pocket_' .. id)
|
|
||||||
elseif commands then
|
|
||||||
os.setComputerLabel('command_' .. id)
|
|
||||||
else
|
|
||||||
os.setComputerLabel('computer_' .. id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
multishell.term = term.current()
|
|
||||||
|
|
||||||
local defaultEnv = { }
|
|
||||||
for k,v in pairs(getfenv(1)) do
|
|
||||||
defaultEnv[k] = v
|
|
||||||
end
|
|
||||||
|
|
||||||
requireInjector(getfenv(1))
|
|
||||||
|
|
||||||
local Config = require('config')
|
|
||||||
local Opus = require('opus')
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local SESSION_FILE = 'usr/config/multishell.session'
|
|
||||||
|
|
||||||
local parentTerm = term.current()
|
|
||||||
local w,h = parentTerm.getSize()
|
|
||||||
local tabs = {}
|
|
||||||
local currentTab
|
|
||||||
local _tabId = 0
|
|
||||||
local overviewTab
|
|
||||||
local runningTab
|
|
||||||
local tabsDirty = false
|
|
||||||
local closeInd = '*'
|
|
||||||
|
|
||||||
if Util.getVersion() >= 1.79 then
|
|
||||||
closeInd = '\215'
|
|
||||||
end
|
|
||||||
|
|
||||||
local config = {
|
|
||||||
standard = {
|
|
||||||
textColor = colors.lightGray,
|
|
||||||
tabBarTextColor = colors.lightGray,
|
|
||||||
focusTextColor = colors.white,
|
|
||||||
backgroundColor = colors.gray,
|
|
||||||
tabBarBackgroundColor = colors.gray,
|
|
||||||
focusBackgroundColor = colors.gray,
|
|
||||||
},
|
|
||||||
color = {
|
|
||||||
textColor = colors.lightGray,
|
|
||||||
tabBarTextColor = colors.lightGray,
|
|
||||||
focusTextColor = colors.white,
|
|
||||||
backgroundColor = colors.gray,
|
|
||||||
tabBarBackgroundColor = colors.gray,
|
|
||||||
focusBackgroundColor = colors.gray,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Config.load('multishell', config)
|
|
||||||
|
|
||||||
local _colors = config.standard
|
|
||||||
if parentTerm.isColor() then
|
|
||||||
_colors = config.color
|
|
||||||
end
|
|
||||||
|
|
||||||
local function redrawMenu()
|
|
||||||
if not tabsDirty then
|
|
||||||
os.queueEvent('multishell', 'draw')
|
|
||||||
tabsDirty = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Draw menu
|
|
||||||
local function draw()
|
|
||||||
tabsDirty = false
|
|
||||||
|
|
||||||
parentTerm.setBackgroundColor( _colors.tabBarBackgroundColor )
|
|
||||||
if currentTab and currentTab.isOverview then
|
|
||||||
parentTerm.setTextColor( _colors.focusTextColor )
|
|
||||||
else
|
|
||||||
parentTerm.setTextColor( _colors.tabBarTextColor )
|
|
||||||
end
|
|
||||||
parentTerm.setCursorPos( 1, 1 )
|
|
||||||
parentTerm.clearLine()
|
|
||||||
parentTerm.write('+')
|
|
||||||
|
|
||||||
local tabX = 2
|
|
||||||
local function compareTab(a, b)
|
|
||||||
return a.tabId < b.tabId
|
|
||||||
end
|
|
||||||
for _,tab in Util.spairs(tabs, compareTab) do
|
|
||||||
|
|
||||||
if tab.hidden and tab ~= currentTab or tab.isOverview then
|
|
||||||
tab.sx = nil
|
|
||||||
tab.ex = nil
|
|
||||||
else
|
|
||||||
tab.sx = tabX + 1
|
|
||||||
tab.ex = tabX + #tab.title
|
|
||||||
tabX = tabX + #tab.title + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for _,tab in Util.spairs(tabs) do
|
|
||||||
if tab.sx then
|
|
||||||
if tab == currentTab then
|
|
||||||
parentTerm.setTextColor(_colors.focusTextColor)
|
|
||||||
parentTerm.setBackgroundColor(_colors.focusBackgroundColor)
|
|
||||||
else
|
|
||||||
parentTerm.setTextColor(_colors.textColor)
|
|
||||||
parentTerm.setBackgroundColor(_colors.backgroundColor)
|
|
||||||
end
|
|
||||||
parentTerm.setCursorPos(tab.sx, 1)
|
|
||||||
parentTerm.write(tab.title)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if currentTab and not currentTab.isOverview then
|
|
||||||
parentTerm.setTextColor(_colors.focusTextColor)
|
|
||||||
parentTerm.setBackgroundColor(_colors.backgroundColor)
|
|
||||||
parentTerm.setCursorPos( w, 1 )
|
|
||||||
parentTerm.write(closeInd)
|
|
||||||
end
|
|
||||||
|
|
||||||
if currentTab then
|
|
||||||
currentTab.window.restoreCursor()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function selectTab( tab )
|
|
||||||
if not tab then
|
|
||||||
for _,ftab in pairs(tabs) do
|
|
||||||
if not ftab.hidden then
|
|
||||||
tab = ftab
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if not tab then
|
|
||||||
tab = overviewTab
|
|
||||||
end
|
|
||||||
|
|
||||||
if currentTab and currentTab ~= tab then
|
|
||||||
currentTab.window.setVisible(false)
|
|
||||||
if tab and not currentTab.hidden then
|
|
||||||
tab.previousTabId = currentTab.tabId
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if tab then
|
|
||||||
currentTab = tab
|
|
||||||
tab.window.setVisible(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function resumeTab(tab, event, eventData)
|
|
||||||
if not tab or coroutine.status(tab.co) == 'dead' then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not tab.filter or tab.filter == event or event == "terminate" then
|
|
||||||
eventData = eventData or { }
|
|
||||||
term.redirect(tab.terminal)
|
|
||||||
local previousTab = runningTab
|
|
||||||
runningTab = tab
|
|
||||||
local ok, result = coroutine.resume(tab.co, event, unpack(eventData))
|
|
||||||
tab.terminal = term.current()
|
|
||||||
if ok then
|
|
||||||
tab.filter = result
|
|
||||||
else
|
|
||||||
printError(result)
|
|
||||||
end
|
|
||||||
|
|
||||||
runningTab = previousTab
|
|
||||||
|
|
||||||
return ok, result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function nextTabId()
|
|
||||||
_tabId = _tabId + 1
|
|
||||||
return _tabId
|
|
||||||
end
|
|
||||||
|
|
||||||
local function launchProcess(tab)
|
|
||||||
|
|
||||||
tab.tabId = nextTabId()
|
|
||||||
tab.timestamp = os.clock()
|
|
||||||
tab.window = window.create(parentTerm, 1, 2, w, h - 1, false)
|
|
||||||
tab.terminal = tab.window
|
|
||||||
tab.env = Util.shallowCopy(tab.env or defaultEnv)
|
|
||||||
|
|
||||||
tab.co = coroutine.create(function()
|
|
||||||
|
|
||||||
local result, err
|
|
||||||
|
|
||||||
if tab.fn then
|
|
||||||
result, err = Util.runFunction(tab.env, tab.fn, table.unpack(tab.args or { } ))
|
|
||||||
elseif tab.path then
|
|
||||||
result, err = Util.run(tab.env, tab.path, table.unpack(tab.args or { } ))
|
|
||||||
else
|
|
||||||
err = 'multishell: invalid tab'
|
|
||||||
end
|
|
||||||
|
|
||||||
if not result and err and err ~= 'Terminated' then
|
|
||||||
if err then
|
|
||||||
printError(tostring(err))
|
|
||||||
end
|
|
||||||
printError('Press enter to close')
|
|
||||||
tab.isDead = true
|
|
||||||
while true do
|
|
||||||
local e, code = os.pullEventRaw('key')
|
|
||||||
if e == 'terminate' or e == 'key' and code == keys.enter then
|
|
||||||
if tab.isOverview then
|
|
||||||
os.queueEvent('multishell', 'terminate')
|
|
||||||
end
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
tabs[tab.tabId] = nil
|
|
||||||
if tab == currentTab then
|
|
||||||
local previousTab
|
|
||||||
if tab.previousTabId then
|
|
||||||
previousTab = tabs[tab.previousTabId]
|
|
||||||
end
|
|
||||||
selectTab(previousTab)
|
|
||||||
end
|
|
||||||
redrawMenu()
|
|
||||||
saveSession()
|
|
||||||
end)
|
|
||||||
|
|
||||||
tabs[tab.tabId] = tab
|
|
||||||
|
|
||||||
resumeTab(tab)
|
|
||||||
|
|
||||||
return tab
|
|
||||||
end
|
|
||||||
|
|
||||||
local function resizeWindows()
|
|
||||||
local windowY = 2
|
|
||||||
local windowHeight = h-1
|
|
||||||
|
|
||||||
local keys = Util.keys(tabs)
|
|
||||||
for _,key in pairs(keys) do
|
|
||||||
local tab = tabs[key]
|
|
||||||
local x,y = tab.window.getCursorPos()
|
|
||||||
if y > windowHeight then
|
|
||||||
tab.window.scroll( y - windowHeight )
|
|
||||||
tab.window.setCursorPos( x, windowHeight )
|
|
||||||
end
|
|
||||||
tab.window.reposition( 1, windowY, w, windowHeight )
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Pass term_resize to all processes
|
|
||||||
local keys = Util.keys(tabs)
|
|
||||||
for _,key in pairs(keys) do
|
|
||||||
resumeTab(tabs[key], "term_resize")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function saveSession()
|
|
||||||
local t = { }
|
|
||||||
for _,process in pairs(tabs) do
|
|
||||||
if process.path and not process.isOverview and not process.hidden then
|
|
||||||
table.insert(t, {
|
|
||||||
path = process.path,
|
|
||||||
args = process.args,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
--Util.writeTable(SESSION_FILE, t)
|
|
||||||
end
|
|
||||||
|
|
||||||
local control
|
|
||||||
local hotkeys = { }
|
|
||||||
|
|
||||||
local function processKeyEvent(event, code)
|
|
||||||
if event == 'key_up' then
|
|
||||||
if code == keys.leftCtrl or code == keys.rightCtrl then
|
|
||||||
control = false
|
|
||||||
end
|
|
||||||
elseif event == 'char' then
|
|
||||||
control = false
|
|
||||||
elseif event == 'key' then
|
|
||||||
if code == keys.leftCtrl or code == keys.rightCtrl then
|
|
||||||
control = true
|
|
||||||
elseif control then
|
|
||||||
local hotkey = hotkeys[code]
|
|
||||||
control = false
|
|
||||||
if hotkey then
|
|
||||||
hotkey()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.addHotkey(code, fn)
|
|
||||||
hotkeys[code] = fn
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.removeHotkey(code)
|
|
||||||
hotkeys[code] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.getFocus()
|
|
||||||
return currentTab.tabId
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.setFocus(tabId)
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if tab then
|
|
||||||
selectTab(tab)
|
|
||||||
redrawMenu()
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.getTitle(tabId)
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if tab then
|
|
||||||
return tab.title
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.setTitle(tabId, sTitle)
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if tab then
|
|
||||||
tab.title = sTitle or ''
|
|
||||||
redrawMenu()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.getCurrent()
|
|
||||||
if runningTab then
|
|
||||||
return runningTab.tabId
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.getTab(tabId)
|
|
||||||
return tabs[tabId]
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.terminate(tabId)
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if tab and not tab.isOverview then
|
|
||||||
if coroutine.status(tab.co) ~= 'dead' then
|
|
||||||
--os.queueEvent('multishell', 'terminate', tab)
|
|
||||||
resumeTab(tab, "terminate")
|
|
||||||
else
|
|
||||||
tabs[tabId] = nil
|
|
||||||
if tab == currentTab then
|
|
||||||
local previousTab
|
|
||||||
if tab.previousTabId then
|
|
||||||
previousTab = tabs[tab.previousTabId]
|
|
||||||
end
|
|
||||||
selectTab(previousTab)
|
|
||||||
end
|
|
||||||
redrawMenu()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.getTabs()
|
|
||||||
return tabs
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.launch( tProgramEnv, sProgramPath, ... )
|
|
||||||
-- backwards compatibility
|
|
||||||
return multishell.openTab({
|
|
||||||
env = tProgramEnv,
|
|
||||||
path = sProgramPath,
|
|
||||||
args = { ... },
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.openTab(tab)
|
|
||||||
|
|
||||||
if not tab.title and tab.path then
|
|
||||||
tab.title = fs.getName(tab.path)
|
|
||||||
end
|
|
||||||
|
|
||||||
tab.title = tab.title or 'untitled'
|
|
||||||
|
|
||||||
local previousTerm = term.current()
|
|
||||||
launchProcess(tab)
|
|
||||||
term.redirect(previousTerm)
|
|
||||||
|
|
||||||
if tab.hidden then
|
|
||||||
if coroutine.status(tab.co) == 'dead' or tab.isDead then
|
|
||||||
tab.hidden = false
|
|
||||||
end
|
|
||||||
elseif tab.focused then
|
|
||||||
multishell.setFocus(tab.tabId)
|
|
||||||
else
|
|
||||||
redrawMenu()
|
|
||||||
end
|
|
||||||
|
|
||||||
if not tab.hidden then
|
|
||||||
saveSession()
|
|
||||||
end
|
|
||||||
|
|
||||||
return tab.tabId
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.hideTab(tabId)
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if tab then
|
|
||||||
tab.hidden = true
|
|
||||||
redrawMenu()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.unhideTab(tabId)
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if tab then
|
|
||||||
tab.hidden = false
|
|
||||||
redrawMenu()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function multishell.getCount()
|
|
||||||
local count
|
|
||||||
for _,tab in pairs(tabs) do
|
|
||||||
count = count + 1
|
|
||||||
end
|
|
||||||
return count
|
|
||||||
end
|
|
||||||
|
|
||||||
-- control-o - overview
|
|
||||||
multishell.addHotkey(24, function()
|
|
||||||
multishell.setFocus(overviewTab.tabId)
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- control-backspace
|
|
||||||
multishell.addHotkey(14, function()
|
|
||||||
local tabId = multishell.getFocus()
|
|
||||||
local tab = tabs[tabId]
|
|
||||||
if not tab.isOverview then
|
|
||||||
os.queueEvent('multishell', 'terminateTab', tabId)
|
|
||||||
tab = Util.shallowCopy(tab)
|
|
||||||
tab.isDead = false
|
|
||||||
tab.focused = true
|
|
||||||
multishell.openTab(tab)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- control-tab - next tab
|
|
||||||
multishell.addHotkey(15, function()
|
|
||||||
local function compareTab(a, b)
|
|
||||||
return a.tabId < b.tabId
|
|
||||||
end
|
|
||||||
local visibleTabs = { }
|
|
||||||
for _,tab in Util.spairs(tabs, compareTab) do
|
|
||||||
if not tab.hidden then
|
|
||||||
table.insert(visibleTabs, tab)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
for k,tab in ipairs(visibleTabs) do
|
|
||||||
if tab.tabId == currentTab.tabId then
|
|
||||||
if k < #visibleTabs then
|
|
||||||
multishell.setFocus(visibleTabs[k + 1].tabId)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if #visibleTabs > 0 then
|
|
||||||
multishell.setFocus(visibleTabs[1].tabId)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
local function startup()
|
|
||||||
local hasError
|
|
||||||
local session = Util.readTable(SESSION_FILE)
|
|
||||||
|
|
||||||
local overviewId = multishell.openTab({
|
|
||||||
path = 'sys/apps/Overview.lua',
|
|
||||||
focused = true,
|
|
||||||
hidden = true,
|
|
||||||
isOverview = true,
|
|
||||||
})
|
|
||||||
overviewTab = tabs[overviewId]
|
|
||||||
|
|
||||||
if not Opus.loadServices() then
|
|
||||||
hasError = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if not Opus.autorun() then
|
|
||||||
hasError = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if session then
|
|
||||||
for _,v in pairs(session) do
|
|
||||||
multishell.openTab(v)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if hasError then
|
|
||||||
print()
|
|
||||||
error('An autorun program has errored')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Begin
|
|
||||||
parentTerm.clear()
|
|
||||||
|
|
||||||
multishell.openTab({
|
|
||||||
focused = true,
|
|
||||||
fn = startup,
|
|
||||||
env = defaultEnv,
|
|
||||||
title = 'Autorun',
|
|
||||||
})
|
|
||||||
|
|
||||||
if not overviewTab or coroutine.status(overviewTab.co) == 'dead' then
|
|
||||||
--error('Overview aborted')
|
|
||||||
end
|
|
||||||
|
|
||||||
if not currentTab then
|
|
||||||
multishell.setFocus(overviewTab.tabId)
|
|
||||||
end
|
|
||||||
|
|
||||||
draw()
|
|
||||||
|
|
||||||
local lastClicked
|
|
||||||
|
|
||||||
while true do
|
|
||||||
|
|
||||||
-- Get the event
|
|
||||||
local tEventData = { os.pullEventRaw() }
|
|
||||||
local sEvent = table.remove(tEventData, 1)
|
|
||||||
|
|
||||||
if sEvent == 'key_up' then
|
|
||||||
processKeyEvent(sEvent, tEventData[1])
|
|
||||||
end
|
|
||||||
|
|
||||||
if sEvent == "term_resize" then
|
|
||||||
-- Resize event
|
|
||||||
w,h = parentTerm.getSize()
|
|
||||||
resizeWindows()
|
|
||||||
redrawMenu()
|
|
||||||
|
|
||||||
elseif sEvent == 'multishell' then
|
|
||||||
local action = tEventData[1]
|
|
||||||
|
|
||||||
if action == 'terminate' then
|
|
||||||
break
|
|
||||||
elseif action == 'terminateTab' then
|
|
||||||
multishell.terminate(tEventData[2])
|
|
||||||
elseif action == 'draw' then
|
|
||||||
draw()
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif sEvent == "char" or
|
|
||||||
sEvent == "key" or
|
|
||||||
sEvent == "paste" or
|
|
||||||
sEvent == "terminate" then
|
|
||||||
|
|
||||||
processKeyEvent(sEvent, tEventData[1])
|
|
||||||
|
|
||||||
-- Keyboard event - Passthrough to current process
|
|
||||||
resumeTab(currentTab, sEvent, tEventData)
|
|
||||||
|
|
||||||
elseif sEvent == "mouse_click" then
|
|
||||||
local button, x, y = tEventData[1], tEventData[2], tEventData[3]
|
|
||||||
lastClicked = nil
|
|
||||||
if y == 1 then
|
|
||||||
-- Switch process
|
|
||||||
local w, h = parentTerm.getSize()
|
|
||||||
if x == 1 then
|
|
||||||
multishell.setFocus(overviewTab.tabId)
|
|
||||||
elseif x == w then
|
|
||||||
if currentTab then
|
|
||||||
multishell.terminate(currentTab.tabId)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
for _,tab in pairs(tabs) do
|
|
||||||
if not tab.hidden and tab.sx then
|
|
||||||
if x >= tab.sx and x <= tab.ex then
|
|
||||||
multishell.setFocus(tab.tabId)
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif currentTab then
|
|
||||||
-- Passthrough to current process
|
|
||||||
lastClicked = currentTab
|
|
||||||
resumeTab(currentTab, sEvent, { button, x, y-1 })
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif sEvent == "mouse_up" then
|
|
||||||
if currentTab and lastClicked == currentTab then
|
|
||||||
local button, x, y = tEventData[1], tEventData[2], tEventData[3]
|
|
||||||
resumeTab(currentTab, sEvent, { button, x, y-1 })
|
|
||||||
end
|
|
||||||
|
|
||||||
elseif sEvent == "mouse_drag" or sEvent == "mouse_scroll" then
|
|
||||||
-- Other mouse event
|
|
||||||
local p1, x, y = tEventData[1], tEventData[2], tEventData[3]
|
|
||||||
if currentTab and (y ~= 1) then
|
|
||||||
if currentTab.terminal.scrollUp then
|
|
||||||
if p1 == -1 then
|
|
||||||
currentTab.terminal.scrollUp()
|
|
||||||
else
|
|
||||||
currentTab.terminal.scrollDown()
|
|
||||||
end
|
|
||||||
else
|
|
||||||
-- Passthrough to current process
|
|
||||||
resumeTab(currentTab, sEvent, { p1, x, y-1 })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
else
|
|
||||||
-- Other event
|
|
||||||
-- Passthrough to all processes
|
|
||||||
local keys = Util.keys(tabs)
|
|
||||||
for _,key in pairs(keys) do
|
|
||||||
resumeTab(tabs[key], sEvent, tEventData)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
45
sys/apps/netdaemon.lua
Normal file
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 @@
|
|||||||
requireInjector(getfenv(1))
|
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
|
||||||
|
|
||||||
643
sys/apps/shell
643
sys/apps/shell
@@ -1,643 +0,0 @@
|
|||||||
local parentShell = shell
|
|
||||||
|
|
||||||
shell = { }
|
|
||||||
multishell = multishell or { }
|
|
||||||
|
|
||||||
local sandboxEnv = setmetatable({ }, { __index = _G })
|
|
||||||
for k,v in pairs(getfenv(1)) do
|
|
||||||
sandboxEnv[k] = v
|
|
||||||
end
|
|
||||||
sandboxEnv.shell = shell
|
|
||||||
sandboxEnv.multishell = multishell
|
|
||||||
|
|
||||||
requireInjector(getfenv(1))
|
|
||||||
|
|
||||||
local Util = require('util')
|
|
||||||
|
|
||||||
local DIR = (parentShell and parentShell.dir()) or ""
|
|
||||||
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
|
|
||||||
local ALIASES = (parentShell and parentShell.aliases()) or {}
|
|
||||||
local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {}
|
|
||||||
|
|
||||||
local bExit = false
|
|
||||||
local tProgramStack = {}
|
|
||||||
|
|
||||||
local function parseCommandLine( ... )
|
|
||||||
local sLine = table.concat( { ... }, " " )
|
|
||||||
local tWords = {}
|
|
||||||
local bQuoted = false
|
|
||||||
for match in string.gmatch( sLine .. "\"", "(.-)\"" ) do
|
|
||||||
if bQuoted then
|
|
||||||
table.insert( tWords, match )
|
|
||||||
else
|
|
||||||
for m in string.gmatch( match, "[^ \t]+" ) do
|
|
||||||
table.insert( tWords, m )
|
|
||||||
end
|
|
||||||
end
|
|
||||||
bQuoted = not bQuoted
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.remove(tWords, 1), tWords
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Install shell API
|
|
||||||
function shell.run(...)
|
|
||||||
|
|
||||||
local path, args = parseCommandLine(...)
|
|
||||||
local isUrl = not not path:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
|
|
||||||
|
|
||||||
if not isUrl then
|
|
||||||
path = shell.resolveProgram(path)
|
|
||||||
end
|
|
||||||
|
|
||||||
if path then
|
|
||||||
tProgramStack[#tProgramStack + 1] = path
|
|
||||||
local oldTitle
|
|
||||||
|
|
||||||
if multishell and multishell.getTitle then
|
|
||||||
oldTitle = multishell.getTitle(multishell.getCurrent())
|
|
||||||
multishell.setTitle(multishell.getCurrent(), fs.getName(path))
|
|
||||||
end
|
|
||||||
|
|
||||||
local result, err
|
|
||||||
|
|
||||||
local env = Util.shallowCopy(sandboxEnv)
|
|
||||||
if isUrl then
|
|
||||||
result, err = Util.runUrl(env, path, unpack(args))
|
|
||||||
else
|
|
||||||
result, err = Util.run(env, path, unpack(args))
|
|
||||||
end
|
|
||||||
tProgramStack[#tProgramStack] = nil
|
|
||||||
|
|
||||||
if multishell and multishell.getTitle then
|
|
||||||
multishell.setTitle(multishell.getCurrent(), oldTitle or 'shell')
|
|
||||||
end
|
|
||||||
|
|
||||||
return result, err
|
|
||||||
end
|
|
||||||
return false, 'No such program'
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.exit()
|
|
||||||
bExit = true
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.dir() return DIR end
|
|
||||||
function shell.setDir(d) DIR = d end
|
|
||||||
function shell.path() return PATH end
|
|
||||||
function shell.setPath(p) PATH = p end
|
|
||||||
|
|
||||||
function shell.resolve( _sPath )
|
|
||||||
local sStartChar = string.sub( _sPath, 1, 1 )
|
|
||||||
if sStartChar == "/" or sStartChar == "\\" then
|
|
||||||
return fs.combine( "", _sPath )
|
|
||||||
else
|
|
||||||
return fs.combine(DIR, _sPath )
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.resolveProgram( _sCommand )
|
|
||||||
|
|
||||||
if ALIASES[ _sCommand ] ~= nil then
|
|
||||||
_sCommand = ALIASES[ _sCommand ]
|
|
||||||
end
|
|
||||||
|
|
||||||
local path = shell.resolve(_sCommand)
|
|
||||||
if fs.exists(path) and not fs.isDir(path) then
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
if fs.exists(path .. '.lua') then
|
|
||||||
return path .. '.lua'
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If the path is a global path, use it directly
|
|
||||||
local sStartChar = string.sub( _sCommand, 1, 1 )
|
|
||||||
if sStartChar == "/" or sStartChar == "\\" then
|
|
||||||
local sPath = fs.combine( "", _sCommand )
|
|
||||||
if fs.exists( sPath ) and not fs.isDir( sPath ) then
|
|
||||||
return sPath
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Otherwise, look on the path variable
|
|
||||||
for sPath in string.gmatch(PATH or '', "[^:]+") do
|
|
||||||
sPath = fs.combine(sPath, _sCommand )
|
|
||||||
if fs.exists( sPath ) and not fs.isDir( sPath ) then
|
|
||||||
return sPath
|
|
||||||
end
|
|
||||||
if fs.exists(sPath .. '.lua') then
|
|
||||||
return sPath .. '.lua'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Not found
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.programs( _bIncludeHidden )
|
|
||||||
local tItems = {}
|
|
||||||
|
|
||||||
-- Add programs from the path
|
|
||||||
for sPath in string.gmatch(PATH, "[^:]+") do
|
|
||||||
sPath = shell.resolve(sPath)
|
|
||||||
if fs.isDir( sPath ) then
|
|
||||||
local tList = fs.list( sPath )
|
|
||||||
for n,sFile in pairs( tList ) do
|
|
||||||
if not fs.isDir( fs.combine( sPath, sFile ) ) and
|
|
||||||
(_bIncludeHidden or string.sub( sFile, 1, 1 ) ~= ".") then
|
|
||||||
tItems[ sFile ] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Sort and return
|
|
||||||
local tItemList = {}
|
|
||||||
for sItem, b in pairs( tItems ) do
|
|
||||||
table.insert( tItemList, sItem )
|
|
||||||
end
|
|
||||||
table.sort( tItemList )
|
|
||||||
return tItemList
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.complete(sLine) end
|
|
||||||
function shell.completeProgram(sProgram) end
|
|
||||||
|
|
||||||
function shell.setCompletionFunction(sProgram, fnComplete)
|
|
||||||
tCompletionInfo[sProgram] = { fnComplete = fnComplete }
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.getCompletionInfo()
|
|
||||||
return tCompletionInfo
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.getRunningProgram()
|
|
||||||
return tProgramStack[#tProgramStack]
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.setAlias( _sCommand, _sProgram )
|
|
||||||
ALIASES[ _sCommand ] = _sProgram
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.clearAlias( _sCommand )
|
|
||||||
ALIASES[ _sCommand ] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.aliases()
|
|
||||||
local tCopy = {}
|
|
||||||
for sAlias, sCommand in pairs(ALIASES) do
|
|
||||||
tCopy[sAlias] = sCommand
|
|
||||||
end
|
|
||||||
return tCopy
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.newTab(tabInfo, ...)
|
|
||||||
local path, args = parseCommandLine(...)
|
|
||||||
path = shell.resolveProgram(path)
|
|
||||||
|
|
||||||
if path then
|
|
||||||
tabInfo.path = path
|
|
||||||
tabInfo.env = sandboxEnv
|
|
||||||
tabInfo.args = Util.shallowCopy(args)
|
|
||||||
tabInfo.title = fs.getName(path)
|
|
||||||
|
|
||||||
if path ~= 'sys/apps/shell' then
|
|
||||||
table.insert(tabInfo.args, 1, tabInfo.path)
|
|
||||||
tabInfo.path = 'sys/apps/shell'
|
|
||||||
end
|
|
||||||
return multishell.openTab(tabInfo)
|
|
||||||
end
|
|
||||||
return nil, 'No such program'
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.openTab( ... )
|
|
||||||
return shell.newTab({ }, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.openForegroundTab( ... )
|
|
||||||
return shell.newTab({ focused = true }, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.openHiddenTab( ... )
|
|
||||||
return shell.newTab({ hidden = true }, ...)
|
|
||||||
end
|
|
||||||
|
|
||||||
function shell.switchTab(tabId)
|
|
||||||
multishell.setFocus(tabId)
|
|
||||||
end
|
|
||||||
|
|
||||||
local tArgs = { ... }
|
|
||||||
if #tArgs > 0 then
|
|
||||||
|
|
||||||
local path, args = parseCommandLine(...)
|
|
||||||
|
|
||||||
if not path then
|
|
||||||
error('No such program')
|
|
||||||
end
|
|
||||||
|
|
||||||
local isUrl = not not path:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
|
|
||||||
if not isUrl then
|
|
||||||
path = shell.resolveProgram(path)
|
|
||||||
if not path then
|
|
||||||
error('No such program')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local fn, err
|
|
||||||
|
|
||||||
if isUrl then
|
|
||||||
fn, err = Util.loadUrl(path, getfenv(1))
|
|
||||||
else
|
|
||||||
fn, err = loadfile(path, getfenv(1))
|
|
||||||
end
|
|
||||||
|
|
||||||
if not fn then
|
|
||||||
error(err)
|
|
||||||
end
|
|
||||||
|
|
||||||
tProgramStack[#tProgramStack + 1] = path
|
|
||||||
return fn(table.unpack(args))
|
|
||||||
end
|
|
||||||
|
|
||||||
local Config = require('config')
|
|
||||||
local History = require('history')
|
|
||||||
|
|
||||||
local config = {
|
|
||||||
standard = {
|
|
||||||
textColor = colors.white,
|
|
||||||
commandTextColor = colors.lightGray,
|
|
||||||
directoryTextColor = colors.gray,
|
|
||||||
directoryBackgroundColor = colors.black,
|
|
||||||
promptTextColor = colors.gray,
|
|
||||||
promptBackgroundColor = colors.black,
|
|
||||||
directoryColor = colors.gray,
|
|
||||||
},
|
|
||||||
color = {
|
|
||||||
textColor = colors.white,
|
|
||||||
commandTextColor = colors.yellow,
|
|
||||||
directoryTextColor = colors.orange,
|
|
||||||
directoryBackgroundColor = colors.black,
|
|
||||||
promptTextColor = colors.blue,
|
|
||||||
promptBackgroundColor = colors.black,
|
|
||||||
directoryColor = colors.green,
|
|
||||||
},
|
|
||||||
displayDirectory = true,
|
|
||||||
}
|
|
||||||
|
|
||||||
--Config.load('shell', config)
|
|
||||||
|
|
||||||
local _colors = config.standard
|
|
||||||
if term.isColor() then
|
|
||||||
_colors = config.color
|
|
||||||
end
|
|
||||||
|
|
||||||
local function autocompleteFile(results, words)
|
|
||||||
|
|
||||||
local function getBaseDir(path)
|
|
||||||
if #path > 1 then
|
|
||||||
if path:sub(-1) ~= '/' then
|
|
||||||
path = fs.getDir(path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if path:sub(1, 1) == '/' then
|
|
||||||
path = fs.combine(path, '')
|
|
||||||
else
|
|
||||||
path = fs.combine(shell.dir(), path)
|
|
||||||
end
|
|
||||||
while not fs.isDir(path) do
|
|
||||||
path = fs.getDir(path)
|
|
||||||
end
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
|
|
||||||
local function getRawPath(path)
|
|
||||||
local baseDir = ''
|
|
||||||
if path:sub(1, 1) ~= '/' then
|
|
||||||
baseDir = shell.dir()
|
|
||||||
end
|
|
||||||
if #path > 1 then
|
|
||||||
if path:sub(-1) ~= '/' then
|
|
||||||
path = fs.getDir(path)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if fs.isDir(fs.combine(baseDir, path)) then
|
|
||||||
return path
|
|
||||||
end
|
|
||||||
return fs.getDir(path)
|
|
||||||
end
|
|
||||||
|
|
||||||
local match = words[#words] or ''
|
|
||||||
local startDir = getBaseDir(match)
|
|
||||||
local rawPath = getRawPath(match)
|
|
||||||
|
|
||||||
if fs.isDir(startDir) then
|
|
||||||
local files = fs.list(startDir)
|
|
||||||
for _,f in pairs(files) do
|
|
||||||
local path = fs.combine(rawPath, f)
|
|
||||||
if fs.isDir(fs.combine(startDir, f)) then
|
|
||||||
results[path .. '/'] = 'directory'
|
|
||||||
else
|
|
||||||
results[path .. ' '] = 'program'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function autocompleteProgram(results, words)
|
|
||||||
if #words == 1 then
|
|
||||||
local files = shell.programs(true)
|
|
||||||
for _,f in ipairs(files) do
|
|
||||||
results[f .. ' '] = 'program'
|
|
||||||
end
|
|
||||||
for f in pairs(ALIASES) do
|
|
||||||
results[f .. ' '] = 'program'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function autocompleteArgument(results, program, words)
|
|
||||||
local word = ''
|
|
||||||
if #words > 1 then
|
|
||||||
word = words[#words]
|
|
||||||
end
|
|
||||||
|
|
||||||
local tInfo = tCompletionInfo[program]
|
|
||||||
local args = tInfo.fnComplete(shell, #words - 1, word, words)
|
|
||||||
if args then
|
|
||||||
Util.filterInplace(args, function(f)
|
|
||||||
return not Util.key(args, f .. '/')
|
|
||||||
end)
|
|
||||||
for _,arg in ipairs(args) do
|
|
||||||
results[word .. arg] = 'argument'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function autocomplete(line, suggestions)
|
|
||||||
local words = { }
|
|
||||||
for word in line:gmatch("%S+") do
|
|
||||||
table.insert(words, word)
|
|
||||||
end
|
|
||||||
if line:match(' $') then
|
|
||||||
table.insert(words, '')
|
|
||||||
end
|
|
||||||
|
|
||||||
local results = { }
|
|
||||||
|
|
||||||
if #words == 0 then
|
|
||||||
files = autocompleteFile(results, words)
|
|
||||||
else
|
|
||||||
local program = shell.resolveProgram(words[1])
|
|
||||||
if tCompletionInfo[program] then
|
|
||||||
autocompleteArgument(results, program, words)
|
|
||||||
else
|
|
||||||
autocompleteProgram(results, words)
|
|
||||||
autocompleteFile(results, words)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local match = words[#words] or ''
|
|
||||||
local files = { }
|
|
||||||
for f in pairs(results) do
|
|
||||||
if f:sub(1, #match) == match then
|
|
||||||
table.insert(files, f)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if #files == 1 then
|
|
||||||
words[#words] = files[1]
|
|
||||||
return table.concat(words, ' ')
|
|
||||||
elseif #files > 1 and suggestions then
|
|
||||||
print()
|
|
||||||
|
|
||||||
local word = words[#words] or ''
|
|
||||||
local prefix = word:match("(.*/)") or ''
|
|
||||||
if #prefix > 0 then
|
|
||||||
for _,f in ipairs(files) do
|
|
||||||
if f:match("^" .. prefix) ~= prefix then
|
|
||||||
prefix = ''
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local tDirs, tFiles = { }, { }
|
|
||||||
for _,f in ipairs(files) do
|
|
||||||
if results[f] == 'directory' then
|
|
||||||
f = f:gsub(prefix, '', 1)
|
|
||||||
table.insert(tDirs, f)
|
|
||||||
else
|
|
||||||
f = f:gsub(prefix, '', 1)
|
|
||||||
table.insert(tFiles, f)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(tDirs)
|
|
||||||
table.sort(tFiles)
|
|
||||||
|
|
||||||
if #tDirs > 0 and #tDirs < #tFiles then
|
|
||||||
local w = term.getSize()
|
|
||||||
local nMaxLen = w / 8
|
|
||||||
for n, sItem in pairs(files) do
|
|
||||||
nMaxLen = math.max(string.len(sItem) + 1, nMaxLen)
|
|
||||||
end
|
|
||||||
local nCols = math.floor(w / nMaxLen)
|
|
||||||
if #tDirs < nCols then
|
|
||||||
for i = #tDirs + 1, nCols do
|
|
||||||
table.insert(tDirs, '')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if #tDirs > 0 then
|
|
||||||
textutils.tabulate(_colors.directoryColor, tDirs, colors.white, tFiles)
|
|
||||||
else
|
|
||||||
textutils.tabulate(colors.white, tFiles)
|
|
||||||
end
|
|
||||||
|
|
||||||
term.setTextColour(_colors.promptTextColor)
|
|
||||||
term.setBackgroundColor(_colors.promptBackgroundColor)
|
|
||||||
write("$ " )
|
|
||||||
|
|
||||||
term.setTextColour(_colors.commandTextColor)
|
|
||||||
term.setBackgroundColor(colors.black)
|
|
||||||
return line
|
|
||||||
elseif #files > 1 then
|
|
||||||
|
|
||||||
-- ugly (complete as much as possible)
|
|
||||||
local word = words[#words] or ''
|
|
||||||
local i = #word + 1
|
|
||||||
while true do
|
|
||||||
local ch
|
|
||||||
for _,f in ipairs(files) do
|
|
||||||
if #f < i then
|
|
||||||
words[#words] = string.sub(f, 1, i - 1)
|
|
||||||
return table.concat(words, ' ')
|
|
||||||
end
|
|
||||||
if not ch then
|
|
||||||
ch = string.sub(f, i, i)
|
|
||||||
elseif string.sub(f, i, i) ~= ch then
|
|
||||||
if i == #word + 1 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
words[#words] = string.sub(f, 1, i - 1)
|
|
||||||
return table.concat(words, ' ')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
i = i + 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function shellRead(history)
|
|
||||||
term.setCursorBlink( true )
|
|
||||||
|
|
||||||
local sLine = ""
|
|
||||||
local nPos = 0
|
|
||||||
local lastPattern
|
|
||||||
|
|
||||||
local w = term.getSize()
|
|
||||||
local sx = term.getCursorPos()
|
|
||||||
|
|
||||||
history:reset()
|
|
||||||
|
|
||||||
local function redraw( sReplace )
|
|
||||||
local nScroll = 0
|
|
||||||
if sx + nPos >= w then
|
|
||||||
nScroll = (sx + nPos) - w
|
|
||||||
end
|
|
||||||
|
|
||||||
local cx,cy = term.getCursorPos()
|
|
||||||
term.setCursorPos( sx, cy )
|
|
||||||
if sReplace then
|
|
||||||
term.write( string.rep( sReplace, math.max( string.len(sLine) - nScroll, 0 ) ) )
|
|
||||||
else
|
|
||||||
term.write( string.sub( sLine, nScroll + 1 ) )
|
|
||||||
end
|
|
||||||
term.setCursorPos( sx + nPos - nScroll, cy )
|
|
||||||
end
|
|
||||||
|
|
||||||
while true do
|
|
||||||
local sEvent, param, param2 = os.pullEventRaw()
|
|
||||||
|
|
||||||
if sEvent == "char" then
|
|
||||||
sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
|
|
||||||
nPos = nPos + 1
|
|
||||||
redraw()
|
|
||||||
elseif sEvent == "paste" then
|
|
||||||
sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
|
|
||||||
nPos = nPos + string.len( param )
|
|
||||||
redraw()
|
|
||||||
elseif sEvent == 'mouse_click' and param == 2 then
|
|
||||||
redraw(string.rep(' ', #sLine))
|
|
||||||
sLine = ''
|
|
||||||
nPos = 0
|
|
||||||
redraw()
|
|
||||||
elseif sEvent == 'terminate' then
|
|
||||||
bExit = true
|
|
||||||
break
|
|
||||||
elseif sEvent == "key" then
|
|
||||||
if param == keys.enter then
|
|
||||||
-- Enter
|
|
||||||
break
|
|
||||||
elseif param == keys.tab then
|
|
||||||
if nPos == #sLine then
|
|
||||||
local showSuggestions = lastPattern == sLine
|
|
||||||
lastPattern = sLine
|
|
||||||
|
|
||||||
local cline = autocomplete(sLine, showSuggestions)
|
|
||||||
if cline then
|
|
||||||
sLine = cline
|
|
||||||
nPos = #sLine
|
|
||||||
redraw()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
elseif param == keys.left then
|
|
||||||
if nPos > 0 then
|
|
||||||
nPos = nPos - 1
|
|
||||||
redraw()
|
|
||||||
end
|
|
||||||
elseif param == keys.right then
|
|
||||||
if nPos < string.len(sLine) then
|
|
||||||
redraw(" ")
|
|
||||||
nPos = nPos + 1
|
|
||||||
redraw()
|
|
||||||
end
|
|
||||||
elseif param == keys.up or param == keys.down then
|
|
||||||
redraw(" ")
|
|
||||||
if param == keys.up then
|
|
||||||
sLine = history:back()
|
|
||||||
else
|
|
||||||
sLine = history:forward()
|
|
||||||
end
|
|
||||||
if sLine then
|
|
||||||
nPos = string.len(sLine)
|
|
||||||
else
|
|
||||||
sLine = ""
|
|
||||||
nPos = 0
|
|
||||||
end
|
|
||||||
redraw()
|
|
||||||
elseif param == keys.backspace then
|
|
||||||
if nPos > 0 then
|
|
||||||
redraw(" ")
|
|
||||||
sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 )
|
|
||||||
nPos = nPos - 1
|
|
||||||
redraw()
|
|
||||||
end
|
|
||||||
elseif param == keys.home then
|
|
||||||
redraw(" ")
|
|
||||||
nPos = 0
|
|
||||||
redraw()
|
|
||||||
elseif param == keys.delete then
|
|
||||||
if nPos < string.len(sLine) then
|
|
||||||
redraw(" ")
|
|
||||||
sLine = string.sub( sLine, 1, nPos ) .. string.sub( sLine, nPos + 2 )
|
|
||||||
redraw()
|
|
||||||
end
|
|
||||||
elseif param == keys["end"] then
|
|
||||||
redraw(" ")
|
|
||||||
nPos = string.len(sLine)
|
|
||||||
redraw()
|
|
||||||
end
|
|
||||||
elseif sEvent == "term_resize" then
|
|
||||||
w = term.getSize()
|
|
||||||
redraw()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local cx, cy = term.getCursorPos()
|
|
||||||
term.setCursorPos( w + 1, cy )
|
|
||||||
print()
|
|
||||||
term.setCursorBlink( false )
|
|
||||||
return sLine
|
|
||||||
end
|
|
||||||
|
|
||||||
local history = History.load('usr/.shell_history', 25)
|
|
||||||
|
|
||||||
while not bExit do
|
|
||||||
if config.displayDirectory then
|
|
||||||
term.setTextColour(_colors.directoryTextColor)
|
|
||||||
term.setBackgroundColor(_colors.directoryBackgroundColor)
|
|
||||||
print('==' .. os.getComputerLabel() .. ':/' .. DIR)
|
|
||||||
end
|
|
||||||
term.setTextColour(_colors.promptTextColor)
|
|
||||||
term.setBackgroundColor(_colors.promptBackgroundColor)
|
|
||||||
write("$ " )
|
|
||||||
term.setTextColour(_colors.commandTextColor)
|
|
||||||
term.setBackgroundColor(colors.black)
|
|
||||||
local sLine = shellRead(history)
|
|
||||||
if bExit then -- terminated
|
|
||||||
break
|
|
||||||
end
|
|
||||||
sLine = Util.trim(sLine)
|
|
||||||
if #sLine > 0 and sLine ~= 'exit' then
|
|
||||||
history:add(sLine)
|
|
||||||
end
|
|
||||||
term.setTextColour(_colors.textColor)
|
|
||||||
if #sLine > 0 then
|
|
||||||
local result, err = shell.run( sLine )
|
|
||||||
if not result and err then
|
|
||||||
printError(err)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
680
sys/apps/shell.lua
Normal file
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
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user