├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── ISSUE_TEMPLATES
│ ├── bug.md
│ └── feature.md
└── workflows
│ └── main.yml
├── .gitignore
├── .opus_version
├── LICENSE.md
├── README.md
├── startup.lua
└── sys
├── apps
├── Files.lua
├── Help.lua
├── Lua.lua
├── Network.lua
├── Overview.lua
├── PackageManager.lua
├── Partition.lua
├── ShellLauncher.lua
├── Sniff.lua
├── System.lua
├── Tasks.lua
├── Version.lua
├── Welcome.lua
├── autorun.lua
├── cedit.lua
├── compat.lua
├── cshell.lua
├── fileui.lua
├── genotp.lua
├── inspect.lua
├── mount.lua
├── netdaemon.lua
├── network
│ ├── keygen.lua
│ ├── proxy.lua
│ ├── samba.lua
│ ├── snmp.lua
│ ├── telnet.lua
│ ├── transport.lua
│ ├── trust.lua
│ └── vnc.lua
├── package.lua
├── password.lua
├── pastebin.lua
├── shell.lua
├── system
│ ├── aliases.lua
│ ├── cloud.lua
│ ├── diskusage.lua
│ ├── kiosk.lua
│ ├── label.lua
│ ├── launcher.lua
│ ├── network.lua
│ ├── password.lua
│ ├── path.lua
│ ├── requires.lua
│ ├── settings.lua
│ ├── shell.lua
│ ├── theme.lua
│ └── trust.lua
├── telnet.lua
├── trust.lua
└── vnc.lua
├── autorun
├── clipboard.lua
├── complete.lua
├── hotkeys.lua
├── log.lua
├── version.lua
└── welcome.lua
├── boot
├── kiosk.lua
├── opus.lua
└── tlco.lua
├── etc
├── apps.db
└── fstab
├── help
├── CloudCatcher.txt
├── Networking.txt
├── Opus.txt
├── Overview.txt
├── Packages.txt
└── pastebin.txt
├── init
├── 1.device.lua
├── 2.vfs.lua
├── 3.modules.lua
├── 3.relay.lua
├── 3.sys.lua
├── 4.label.lua
├── 4.user.lua
├── 5.network.lua
├── 5.unpackage.lua
├── 6.packages.lua
└── 7.multishell.lua
├── kernel.lua
└── modules
└── opus
├── ansi.lua
├── array.lua
├── bulkget.lua
├── cbor.lua
├── class.lua
├── compress
├── lzw.lua
└── tar.lua
├── config.lua
├── crypto
├── chacha20.lua
├── ecc
│ ├── elliptic.lua
│ ├── fp.lua
│ ├── fq.lua
│ └── init.lua
└── sha2.lua
├── entry.lua
├── event.lua
├── fs
├── gitfs.lua
├── linkfs.lua
├── netfs.lua
├── ramfs.lua
└── urlfs.lua
├── fuzzy.lua
├── git.lua
├── gps.lua
├── history.lua
├── http
└── pastebin.lua
├── injector.lua
├── input.lua
├── json.lua
├── map.lua
├── nft.lua
├── packages.lua
├── peripheral.lua
├── point.lua
├── security.lua
├── socket.lua
├── sound.lua
├── sync.lua
├── terminal.lua
├── trace.lua
├── ui.lua
├── ui
├── blit.lua
├── canvas.lua
├── components
│ ├── Button.lua
│ ├── Checkbox.lua
│ ├── CheckboxGrid.lua
│ ├── Chooser.lua
│ ├── Dialog.lua
│ ├── DropMenu.lua
│ ├── DropMenuItem.lua
│ ├── Embedded.lua
│ ├── FileSelect.lua
│ ├── FlatButton.lua
│ ├── Form.lua
│ ├── Grid.lua
│ ├── Image.lua
│ ├── Menu.lua
│ ├── MenuBar.lua
│ ├── MenuItem.lua
│ ├── MiniSlideOut.lua
│ ├── NftImage.lua
│ ├── Notification.lua
│ ├── Page.lua
│ ├── ProgressBar.lua
│ ├── Question.lua
│ ├── QuickSelect.lua
│ ├── ScrollBar.lua
│ ├── ScrollingGrid.lua
│ ├── SlideOut.lua
│ ├── Slider.lua
│ ├── StatusBar.lua
│ ├── Tab.lua
│ ├── TabBar.lua
│ ├── TabBarMenuItem.lua
│ ├── Tabs.lua
│ ├── Text.lua
│ ├── TextArea.lua
│ ├── TextEntry.lua
│ ├── Throttle.lua
│ ├── TitleBar.lua
│ ├── VerticalMeter.lua
│ ├── Viewport.lua
│ ├── Wizard.lua
│ └── WizardPage.lua
├── region.lua
├── transition.lua
└── tween.lua
└── util.lua
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Versions**
27 | What version of Minecraft, CC:Tweaked, Plethora (if applicable), Opus branch are you using
28 | - MC : [e.g. 1.12.2]
29 | - CC:T : [e.g. 1.88]
30 | - Opus : [e.g. develop-1.8]
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATES/bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug Report
3 | about: Did something go wrong? File an issue!
4 | title: Good titles include first line of stack trace or short summary of problem
5 | labels: bug
6 | ---
7 |
8 | # Details
9 |
10 | ## Further context
11 |
12 | ## Versions
13 | Branch:
14 |
15 | Opus Version:
16 |
17 | CraftOS Version:
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATES/feature.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Enhancement
3 | about: Suggest a new feature or change to Opus.
4 | labels: enhancement
5 | ---
6 |
7 | # Summary
8 |
9 | ## Additional Context
10 |
11 | ## Related
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the action will run. Triggers the workflow on push or pull request
6 | # events but only for the master branch
7 | on:
8 | push:
9 | branches: [ develop-1.8 ]
10 | pull_request:
11 | branches: [ develop-1.8 ]
12 |
13 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
14 | jobs:
15 | # This workflow contains a single job called "build"
16 | build:
17 | # The type of runner that the job will run on
18 | runs-on: ubuntu-latest
19 |
20 | # Steps represent a sequence of tasks that will be executed as part of the job
21 | steps:
22 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
23 | - uses: actions/checkout@v2
24 |
25 | - name: Create version file
26 | run: |
27 | echo `date` > .opus_version
28 | git log >> .opus_version
29 |
30 | - name: Commit version file
31 | uses: alexesprit/action-update-file@main
32 | with:
33 | branch: 'develop-1.8'
34 | file-path: .opus_version
35 | commit-msg: Update version date
36 | github-token: ${{ secrets.GITHUB_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /ignore
2 | .project
3 |
--------------------------------------------------------------------------------
/.opus_version:
--------------------------------------------------------------------------------
1 | Mon Jul 4 04:09:12 UTC 2022
2 | commit 3150525ee2024fc605669093b89f75f0c741a81f
3 | Author: Kan18 <24967425+Kan18@users.noreply.github.com>
4 | Date: Mon Jul 4 00:08:59 2022 -0400
5 |
6 | Fix #48 (shell resolving issue) (#58)
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016-2019 kepler155c
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Opus OS for computercraft
2 |
3 |
4 |
5 | ## Features
6 | * Multitasking OS - run programs in separate tabs
7 | * Telnet (wireless remote shell)
8 | * VNC (wireless screen sharing)
9 | * UI API
10 | * Turtle API (includes true pathfinding based on the ASTAR algorithm)
11 | * Remote file system access (you can access the file system of any computer in wireless range)
12 | * File manager
13 | * Lua REPL with GUI
14 | * Run scripts on single or groups of computers (GUI)
15 | * Turtle follow (with GPS) and turtle come to you (without GPS)
16 |
17 | ## Install
18 | ```
19 | pastebin run UzGHLbNC
20 | ```
21 |
--------------------------------------------------------------------------------
/sys/apps/Help.lua:
--------------------------------------------------------------------------------
1 | local fuzzy = require('opus.fuzzy')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | local help = _G.help
6 |
7 | UI:configure('Help', ...)
8 |
9 | local topics = { }
10 | for _,topic in pairs(help.topics()) do
11 | table.insert(topics, { name = topic, lname = topic:lower() })
12 | end
13 |
14 | UI:addPage('main', UI.Page {
15 | UI.Text {
16 | x = 3, y = 2,
17 | value = 'Search',
18 | },
19 | UI.TextEntry {
20 | x = 10, y = 2, ex = -3,
21 | limit = 32,
22 | },
23 | grid = UI.ScrollingGrid {
24 | y = 4,
25 | values = topics,
26 | columns = {
27 | { heading = 'Topic', key = 'name' },
28 | },
29 | sortColumn = 'lname',
30 | },
31 | accelerators = {
32 | [ 'control-q' ] = 'quit',
33 | enter = 'grid_select',
34 | },
35 | eventHandler = function(self, event)
36 | if event.type == 'quit' then
37 | UI:quit()
38 |
39 | elseif event.type == 'grid_select' then
40 | if self.grid:getSelected() then
41 | UI:setPage('topic', self.grid:getSelected().name)
42 | end
43 |
44 | elseif event.type == 'text_change' then
45 | if not event.text then
46 | self.grid.sortColumn = 'lname'
47 | else
48 | self.grid.sortColumn = 'score'
49 | self.grid.inverseSort = false
50 | local pattern = event.text:lower()
51 | for _,v in pairs(self.grid.values) do
52 | v.score = -fuzzy(v.lname, pattern)
53 | end
54 | end
55 | self.grid:update()
56 | self.grid:setIndex(1)
57 | self.grid:draw()
58 |
59 | else
60 | return UI.Page.eventHandler(self, event)
61 | end
62 | end,
63 | })
64 |
65 | UI:addPage('topic', UI.Page {
66 | backgroundColor = 'black',
67 | titleBar = UI.TitleBar {
68 | title = 'text',
69 | event = 'back',
70 | },
71 | helpText = UI.TextArea {
72 | x = 2, ex = -1, y = 3, ey = -2,
73 | },
74 | accelerators = {
75 | [ 'control-q' ] = 'back',
76 | backspace = 'back',
77 | },
78 | enable = function(self, name)
79 | local f = help.lookup(name)
80 |
81 | self.titleBar.title = name
82 | self.helpText:setText(f and Util.readFile(f) or 'No help available for ' .. name)
83 |
84 | return UI.Page.enable(self)
85 | end,
86 | eventHandler = function(self, event)
87 | if event.type == 'back' then
88 | UI:setPage('main')
89 | end
90 | return UI.Page.eventHandler(self, event)
91 | end,
92 | })
93 |
94 | local args = Util.parse(...)
95 | UI:setPage(args[1] and 'topic' or 'main', args[1])
96 | UI:start()
97 |
--------------------------------------------------------------------------------
/sys/apps/ShellLauncher.lua:
--------------------------------------------------------------------------------
1 | local kernel = _G.kernel
2 | local os = _G.os
3 | local shell = _ENV.shell
4 |
5 | local launcherTab = kernel.getCurrent()
6 | launcherTab.noFocus = true
7 |
8 | kernel.hook('kernel_focus', function(_, eventData)
9 | local focusTab = eventData and eventData[1]
10 | if focusTab == launcherTab.uid then
11 | local previousTab = eventData[2]
12 | local nextTab = launcherTab
13 | if not previousTab then
14 | for _, v in pairs(kernel.routines) do
15 | if not v.hidden and v.uid > nextTab.uid then
16 | nextTab = v
17 | end
18 | end
19 | end
20 | if nextTab == launcherTab then
21 | shell.switchTab(shell.openTab('shell'))
22 | else
23 | shell.switchTab(nextTab.uid)
24 | end
25 | end
26 | end)
27 |
28 | os.pullEventRaw('kernel_halt')
29 |
--------------------------------------------------------------------------------
/sys/apps/System.lua:
--------------------------------------------------------------------------------
1 | local UI = require('opus.ui')
2 | local Util = require('opus.util')
3 |
4 | local fs = _G.fs
5 | local shell = _ENV.shell
6 |
7 | UI:configure('System', ...)
8 |
9 | local function loadDirectory(dir)
10 | local plugins = { }
11 | for _, file in pairs(fs.list(dir)) do
12 | local s, m = Util.run(_ENV, fs.combine(dir, file))
13 | if not s and m then
14 | _G.printError('Error loading: ' .. file)
15 | error(m or 'Unknown error')
16 | elseif s and m then
17 | table.insert(plugins, { tab = m, name = m.title, description = m.description })
18 | end
19 | end
20 | return plugins
21 | end
22 |
23 | local programDir = fs.getDir(_ENV.arg[0])
24 | local plugins = loadDirectory(fs.combine(programDir, 'system'), { })
25 |
26 | local page = UI.Page {
27 | tabs = UI.Tabs {
28 | settings = UI.Tab {
29 | title = 'Category',
30 | grid = UI.ScrollingGrid {
31 | x = 2, y = 2, ex = -2, ey = -2,
32 | columns = {
33 | { heading = 'Name', key = 'name' },
34 | { heading = 'Description', key = 'description' },
35 | },
36 | sortColumn = 'name',
37 | autospace = true,
38 | values = plugins,
39 | },
40 | accelerators = {
41 | grid_select = 'category_select',
42 | }
43 | },
44 | },
45 | notification = UI.Notification(),
46 | accelerators = {
47 | [ 'control-q' ] = 'quit',
48 | },
49 | eventHandler = function(self, event)
50 | if event.type == 'quit' then
51 | UI:quit()
52 |
53 | elseif event.type == 'category_select' then
54 | local tab = event.selected.tab
55 |
56 | if not self.tabs[tab.title] then
57 | self.tabs:add({ [ tab.title ] = tab })
58 | end
59 | self.tabs:selectTab(tab)
60 | return true
61 |
62 | elseif event.type == 'success_message' then
63 | self.notification:success(event.message)
64 |
65 | elseif event.type == 'info_message' then
66 | self.notification:info(event.message)
67 |
68 | elseif event.type == 'error_message' then
69 | self.notification:error(event.message)
70 |
71 | elseif event.type == 'tab_activate' then
72 | event.activated:focusFirst()
73 |
74 | else
75 | return UI.Page.eventHandler(self, event)
76 | end
77 | return true
78 | end,
79 | }
80 |
81 | UI:setPage(page)
82 | UI:start()
83 |
--------------------------------------------------------------------------------
/sys/apps/Tasks.lua:
--------------------------------------------------------------------------------
1 | local Event = require('opus.event')
2 | local UI = require('opus.ui')
3 |
4 | local kernel = _G.kernel
5 | local multishell = _ENV.multishell
6 | local tasks = multishell and multishell.getTabs and multishell.getTabs() or kernel.routines
7 |
8 | UI:configure('Tasks', ...)
9 |
10 | local page = UI.Page {
11 | menuBar = UI.MenuBar {
12 | buttons = {
13 | { text = 'Activate', event = 'activate' },
14 | { text = 'Terminate', event = 'terminate' },
15 | { text = 'Inspect', event = 'inspect' },
16 | },
17 | },
18 | grid = UI.ScrollingGrid {
19 | y = 2,
20 | columns = {
21 | { heading = 'ID', key = 'uid', width = 3 },
22 | { heading = 'Title', key = 'title' },
23 | { heading = 'Status', key = 'status' },
24 | { heading = 'Time', key = 'timestamp' },
25 | },
26 | values = tasks,
27 | sortColumn = 'uid',
28 | autospace = true,
29 | getDisplayValues = function (_, row)
30 | local elapsed = os.clock()-row.timestamp
31 | return {
32 | uid = row.uid,
33 | title = row.title,
34 | status = row.isDead and 'error' or coroutine.status(row.co),
35 | timestamp = elapsed < 60 and
36 | string.format("%ds", math.floor(elapsed)) or
37 | string.format("%sm", math.floor(elapsed/6)/10),
38 | }
39 | end
40 | },
41 | accelerators = {
42 | [ 'control-q' ] = 'quit',
43 | [ ' ' ] = 'activate',
44 | t = 'terminate',
45 | },
46 | eventHandler = function (self, event)
47 | local t = self.grid:getSelected()
48 | if t then
49 | if event.type == 'activate' or event.type == 'grid_select' then
50 | multishell.setFocus(t.uid)
51 | elseif event.type == 'terminate' then
52 | multishell.terminate(t.uid)
53 | elseif event.type == 'inspect' then
54 | multishell.openTab(_ENV, {
55 | path = 'sys/apps/Lua.lua',
56 | args = { t },
57 | focused = true,
58 | })
59 | end
60 | end
61 | if event.type == 'quit' then
62 | UI:quit()
63 | end
64 | UI.Page.eventHandler(self, event)
65 | end
66 | }
67 |
68 | Event.onInterval(1, function()
69 | page.grid:update()
70 | page.grid:draw()
71 | page:sync()
72 | end)
73 |
74 | UI:setPage(page)
75 | UI:start()
76 |
--------------------------------------------------------------------------------
/sys/apps/Version.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 | local UI = require('opus.ui')
3 |
4 | local shell = _ENV.shell
5 |
6 | local config = Config.load('version')
7 | if not config.current then
8 | return
9 | end
10 |
11 | UI:setPage(UI.Page {
12 | UI.Text {
13 | x = 2, y = 2, ex = -2,
14 | align = 'center',
15 | value = 'Opus has been updated.',
16 | textColor = 'yellow',
17 | },
18 | UI.TextArea {
19 | x = 2, y = 4, ey = -8,
20 | value = config.details,
21 | },
22 | UI.Button {
23 | x = 2, y = -6, width = 21,
24 | event = 'skip',
25 | text = 'Skip this version',
26 | },
27 | UI.Button {
28 | x = 2, y = -4, width = 21,
29 | event = 'remind',
30 | text = 'Remind me tomorrow',
31 | },
32 | UI.Button {
33 | x = 2, y = -2, width = 21,
34 | event = 'update',
35 | text = 'Update'
36 | },
37 | eventHandler = function(self, event)
38 | if event.type == 'skip' then
39 | config.skip = config.current
40 | Config.update('version', config)
41 | UI:quit()
42 |
43 | elseif event.type == 'remind' then
44 | UI:quit()
45 |
46 | elseif event.type == 'update' then
47 | shell.openForegroundTab('update update')
48 | UI:quit()
49 | end
50 | return UI.Page.eventHandler(self, event)
51 | end,
52 | })
53 |
54 | UI:start()
55 |
--------------------------------------------------------------------------------
/sys/apps/autorun.lua:
--------------------------------------------------------------------------------
1 | local Packages = require('opus.packages')
2 |
3 | local colors = _G.colors
4 | local fs = _G.fs
5 | local keys = _G.keys
6 | local multishell = _ENV.multishell
7 | local os = _G.os
8 | local shell = _ENV.shell
9 | local term = _G.term
10 |
11 | local success = true
12 |
13 | local function runDir(directory)
14 | if not fs.exists(directory) then
15 | return true
16 | end
17 |
18 | local files = fs.list(directory)
19 | table.sort(files)
20 |
21 | for _,file in ipairs(files) do
22 | os.sleep(0)
23 | local result, err = shell.run(directory .. '/' .. file)
24 |
25 | if result then
26 | if term.isColor() then
27 | term.setTextColor(colors.green)
28 | end
29 | term.write('[PASS] ')
30 | term.setTextColor(colors.white)
31 | term.write(fs.combine(directory, file))
32 | print()
33 | else
34 | if term.isColor() then
35 | term.setTextColor(colors.red)
36 | end
37 | term.write('[FAIL] ')
38 | term.setTextColor(colors.white)
39 | term.write(fs.combine(directory, file))
40 | if err then
41 | _G.printError('\n' .. err)
42 | end
43 | print()
44 | success = false
45 | end
46 | end
47 | end
48 |
49 | runDir('sys/autorun')
50 | for _, package in pairs(Packages:installedSorted()) do
51 | local packageDir = 'packages/' .. package.name .. '/autorun'
52 | runDir(packageDir)
53 | end
54 | runDir('usr/autorun')
55 |
56 | if not success then
57 | if multishell then
58 | multishell.setFocus(multishell.getCurrent())
59 | end
60 | _G.printError('A startup program has errored')
61 | print('Press enter to continue')
62 |
63 | while true do
64 | local e, code = os.pullEventRaw('key')
65 | if e == 'terminate' or e == 'key' and code == keys.enter then
66 | break
67 | end
68 | end
69 | end
70 |
71 |
--------------------------------------------------------------------------------
/sys/apps/cedit.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 |
3 | local multishell = _ENV.multishell
4 | local os = _G.os
5 | local read = _G.read
6 | local shell = _ENV.shell
7 |
8 | local args = { ... }
9 | if not args[1] then
10 | error('Syntax: cedit ')
11 | end
12 |
13 | if not _G.http.websocket then
14 | error('Requires CC: Tweaked')
15 | end
16 |
17 | if not _G.cloud_catcher then
18 | local key = Config.load('cloud').key
19 |
20 | if not key then
21 | print('Visit https://cloud-catcher.squiddev.cc')
22 | print('Paste key: ')
23 | key = read()
24 | if #key == 0 then
25 | return
26 | end
27 | end
28 |
29 | -- open an unfocused tab
30 | local id = shell.openTab('cloud ' .. key)
31 | print('Connecting...')
32 | while not _G.cloud_catcher do
33 | os.sleep(.2)
34 | end
35 | multishell.setTitle(id, 'Cloud')
36 | end
37 |
38 | shell.run('cloud edit ' .. table.unpack({ ... }))
39 |
--------------------------------------------------------------------------------
/sys/apps/compat.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | -- some programs expect to be run in the global scope
4 | -- ie. busted, moonscript
5 |
6 | -- create a new environment mimicing pure lua
7 |
8 | local fs = _G.fs
9 | local shell = _ENV.shell
10 |
11 | local env = Util.shallowCopy(_G)
12 | Util.merge(env, _ENV)
13 | env._G = env
14 |
15 | env.arg = { ... }
16 | env.arg[0] = shell.resolveProgram(table.remove(env.arg, 1) or error('file name is required'))
17 |
18 | _G.requireInjector(env, fs.getDir(env.arg[0]))
19 |
20 | local s, m = Util.run(env, env.arg[0], table.unpack(env.arg))
21 |
22 | if not s then
23 | error(m, -1)
24 | end
25 |
--------------------------------------------------------------------------------
/sys/apps/cshell.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 |
3 | local read = _G.read
4 | local shell = _ENV.shell
5 |
6 | if not _G.http.websocket then
7 | error('Requires CC: Tweaked')
8 | end
9 |
10 | if not _G.cloud_catcher then
11 | local key = Config.load('cloud').key
12 |
13 | if not key then
14 | print('Visit https://cloud-catcher.squiddev.cc')
15 | print('Paste key: ')
16 | key = read()
17 | if #key == 0 then
18 | return
19 | end
20 | end
21 | print('Connecting...')
22 | shell.run('cloud ' .. key)
23 | end
24 |
--------------------------------------------------------------------------------
/sys/apps/fileui.lua:
--------------------------------------------------------------------------------
1 | local UI = require('opus.ui')
2 | local Util = require('opus.util')
3 |
4 | local shell = _ENV.shell
5 | local multishell = _ENV.multishell
6 |
7 | -- fileui [--path=path] [--exec=filename] [--title=title]
8 |
9 | local page = UI.Page {
10 | fileselect = UI.FileSelect { },
11 | eventHandler = function(self, event)
12 | if event.type == 'select_file' then
13 | self.selected = event.file
14 | UI:quit()
15 |
16 | elseif event.type == 'select_cancel' then
17 | UI:quit()
18 | end
19 |
20 | return UI.Page.eventHandler(self, event)
21 | end,
22 | }
23 |
24 | local _, args = Util.parse(...)
25 |
26 | if args.title and multishell then
27 | multishell.setTitle(multishell.getCurrent(), args.title)
28 | end
29 |
30 | UI:setPage(page, args.path)
31 | UI:start()
32 | UI.term:setCursorBlink(false)
33 |
34 | if args.exec and page.selected then
35 | shell.openForegroundTab(string.format('%s %s', args.exec, page.selected))
36 | return
37 | end
38 |
39 | return page.selected
40 |
--------------------------------------------------------------------------------
/sys/apps/genotp.lua:
--------------------------------------------------------------------------------
1 | local SHA = require("opus.crypto.sha2")
2 |
3 | local acceptableCharacters = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}
4 | local acceptableCharactersLen = #acceptableCharacters
5 |
6 | local password = ""
7 |
8 | for _i = 1, 8 do
9 | password = password .. acceptableCharacters[math.random(acceptableCharactersLen)]
10 | end
11 |
12 | os.queueEvent("set_otp", SHA.compute(password))
13 |
14 | print("Your one-time password is: " .. password)
--------------------------------------------------------------------------------
/sys/apps/mount.lua:
--------------------------------------------------------------------------------
1 | local args = { ... }
2 |
3 | local target = table.remove(args, 1)
4 | target = shell.resolve(target)
5 |
6 | fs.mount(target, table.unpack(args))
7 |
--------------------------------------------------------------------------------
/sys/apps/netdaemon.lua:
--------------------------------------------------------------------------------
1 | local Event = require('opus.event')
2 | local Util = require('opus.util')
3 |
4 | local device = _G.device
5 | local fs = _G.fs
6 | local network = _G.network
7 | local os = _G.os
8 | local printError = _G.printError
9 |
10 | if not device.wireless_modem then
11 | return
12 | end
13 |
14 | print('Net daemon starting')
15 | -- don't close as multiple computers may be sharing the
16 | -- wireless modem
17 | --device.wireless_modem.closeAll()
18 |
19 | for _,file in pairs(fs.list('sys/apps/network')) do
20 | local fn, msg = Util.run(_ENV, 'sys/apps/network/' .. file)
21 | if not fn then
22 | printError(msg)
23 | end
24 | end
25 |
26 | Event.on('device_detach', function()
27 | if not device.wireless_modem then
28 | Event.exitPullEvents()
29 | end
30 | end)
31 |
32 | print('Net daemon started')
33 | os.queueEvent('network_up')
34 | Event.pullEvents()
35 |
36 | for _,c in pairs(network) do
37 | c.active = false
38 | os.queueEvent('network_detach', c)
39 | end
40 | os.queueEvent('network_down')
41 | Event.pullEvent('network_down')
42 |
43 | Util.clear(network)
44 |
45 | print('Net daemon stopped')
46 |
--------------------------------------------------------------------------------
/sys/apps/network/keygen.lua:
--------------------------------------------------------------------------------
1 | local ECC = require('opus.crypto.ecc')
2 | local Event = require('opus.event')
3 | local Util = require('opus.util')
4 |
5 | local network = _G.network
6 | local os = _G.os
7 |
8 | local keyPairs = { }
9 |
10 | local function generateKeyPair()
11 | local key = { }
12 | for _ = 1, 32 do
13 | table.insert(key, math.random(0, 0xFF))
14 | end
15 | local privateKey = setmetatable(key, Util.byteArrayMT)
16 | return privateKey, ECC.publicKey(privateKey)
17 | end
18 |
19 | getmetatable(network).__index.getKeyPair = function()
20 | local keys = table.remove(keyPairs)
21 | os.queueEvent('generate_keypair')
22 | if not keys then
23 | return generateKeyPair()
24 | end
25 | return table.unpack(keys)
26 | end
27 |
28 | -- Generate key pairs in the background as this is a time-consuming process
29 | Event.on('generate_keypair', function()
30 | while true do
31 | os.sleep(5)
32 | local timer = Util.timer()
33 | table.insert(keyPairs, { generateKeyPair() })
34 | _G._syslog('Generated keypair in ' .. timer())
35 | if #keyPairs >= 3 then
36 | break
37 | end
38 | end
39 | end)
40 |
--------------------------------------------------------------------------------
/sys/apps/network/proxy.lua:
--------------------------------------------------------------------------------
1 | local Event = require('opus.event')
2 | local Socket = require('opus.socket')
3 | local Util = require('opus.util')
4 |
5 | local function getProxy(path)
6 | local x = Util.split(path, '(.-)/')
7 | local proxy = _G
8 | for _, v in pairs(x) do
9 | proxy = proxy[v]
10 | if not proxy then
11 | break
12 | end
13 | end
14 | return proxy
15 | end
16 |
17 | local function proxyConnection(socket)
18 | local path = socket:read(2)
19 | if path then
20 | local api = getProxy(path)
21 |
22 | if not api then
23 | print('proxy: invalid API')
24 | socket:close()
25 | return
26 | end
27 |
28 | local methods = { }
29 | for k,v in pairs(api) do
30 | if type(v) == 'function' then
31 | table.insert(methods, k)
32 | end
33 | end
34 | socket:write(methods)
35 |
36 | while true do
37 | local data = socket:read()
38 | if not data then
39 | print('proxy: lost connection from ' .. socket.dhost)
40 | break
41 | end
42 | socket:write({ api[data[1]](table.unpack(data, 2)) })
43 | end
44 | end
45 | end
46 |
47 | Event.addRoutine(function()
48 | print('proxy: listening on port 188')
49 | while true do
50 | local socket = Socket.server(188)
51 |
52 | print('proxy: connection from ' .. socket.dhost)
53 |
54 | Event.addRoutine(function()
55 | local s, m = pcall(proxyConnection, socket)
56 | print('proxy: closing connection to ' .. socket.dhost)
57 | socket:close()
58 | if not s and m then
59 | print('Proxy error')
60 | _G.printError(m)
61 | end
62 | end)
63 | end
64 | end)
65 |
--------------------------------------------------------------------------------
/sys/apps/network/samba.lua:
--------------------------------------------------------------------------------
1 | local Event = require('opus.event')
2 | local Socket = require('opus.socket')
3 |
4 | local fs = _G.fs
5 |
6 | local fileUid = 0
7 | local fileHandles = { }
8 |
9 | local function remoteOpen(fn, fl)
10 | local fh = fs.open(fn, fl)
11 | if fh then
12 | local methods = { 'close', 'write', 'writeLine', 'flush', 'read', 'readLine', 'readAll', }
13 | fileUid = fileUid + 1
14 | fileHandles[fileUid] = fh
15 |
16 | local vfh = {
17 | methods = { },
18 | fileUid = fileUid,
19 | }
20 |
21 | for _,m in ipairs(methods) do
22 | if fh[m] then
23 | table.insert(vfh.methods, m)
24 | end
25 | end
26 | return vfh
27 | end
28 | end
29 |
30 | local function remoteFileOperation(fileId, op, ...)
31 | local fh = fileHandles[fileId]
32 | if fh then
33 | return fh[op](...)
34 | end
35 | end
36 |
37 | local function sambaConnection(socket)
38 | while true do
39 | local msg = socket:read()
40 | if not msg then
41 | break
42 | end
43 | local fn = fs[msg.fn]
44 | if msg.fn == 'open' then
45 | fn = remoteOpen
46 | elseif msg.fn == 'fileOp' then
47 | fn = remoteFileOperation
48 | end
49 | local ret
50 | local s, m = pcall(function()
51 | ret = fn(table.unpack(msg.args))
52 | end)
53 | if not s and m then
54 | _G.printError('samba: ' .. m)
55 | end
56 | socket:write({ response = ret })
57 | end
58 |
59 | print('samba: Connection closed')
60 | end
61 |
62 | Event.addRoutine(function()
63 | print('samba: listening on port 139')
64 |
65 | while true do
66 | local socket = Socket.server(139)
67 |
68 | Event.addRoutine(function()
69 | print('samba: connection from ' .. socket.dhost)
70 | local s, m = pcall(sambaConnection, socket)
71 | print('samba: closing connection to ' .. socket.dhost)
72 | socket:close()
73 | if not s and m then
74 | print('Samba error')
75 | _G.printError(m)
76 | end
77 | end)
78 | end
79 | end)
80 |
81 | Event.on('network_attach', function(_, computer)
82 | fs.mount(fs.combine('network', computer.label), 'netfs', computer.id)
83 | end)
84 |
85 | Event.on('network_detach', function(_, computer)
86 | print('samba: detaching ' .. computer.label)
87 | fs.unmount(fs.combine('network', computer.label))
88 | end)
89 |
--------------------------------------------------------------------------------
/sys/apps/network/telnet.lua:
--------------------------------------------------------------------------------
1 | local Event = require('opus.event')
2 | local Socket = require('opus.socket')
3 | local Util = require('opus.util')
4 |
5 | local kernel = _G.kernel
6 | local shell = _ENV.shell
7 | local term = _G.term
8 | local window = _G.window
9 |
10 | local function telnetHost(socket, mode)
11 | local methods = { 'clear', 'clearLine', 'setCursorPos', 'write', 'blit',
12 | 'setTextColor', 'setTextColour', 'setBackgroundColor',
13 | 'setBackgroundColour', 'scroll', 'setCursorBlink', }
14 |
15 | local termInfo = socket:read(5)
16 | if not termInfo then
17 | _G.printError('read failed')
18 | return
19 | end
20 |
21 | local win = window.create(_G.device.terminal, 1, 1, termInfo.width, termInfo.height, false)
22 | win.setCursorPos(table.unpack(termInfo.pos))
23 |
24 | for _,k in pairs(methods) do
25 | local fn = win[k]
26 | win[k] = function(...)
27 |
28 | if not socket.queue then
29 | socket.queue = { }
30 | Event.onTimeout(0, function()
31 | socket:write(socket.queue)
32 | socket.queue = nil
33 | end)
34 | end
35 |
36 | table.insert(socket.queue, {
37 | f = k,
38 | args = { ... },
39 | })
40 | fn(...)
41 | end
42 | end
43 |
44 | local shellThread = kernel.run(_ENV, {
45 | window = win,
46 | title = mode .. ' client',
47 | hidden = true,
48 | fn = function()
49 | Util.run(kernel.makeEnv(_ENV), shell.resolveProgram('shell'), table.unpack(termInfo.program))
50 | if socket.queue then
51 | socket:write(socket.queue)
52 | end
53 | socket:close()
54 | end,
55 | })
56 |
57 | Event.addRoutine(function()
58 | while true do
59 | local data = socket:read()
60 | if not data then
61 | shellThread:resume('terminate')
62 | break
63 | end
64 | local previousTerm = term.current()
65 | shellThread:resume(table.unpack(data))
66 | term.redirect(previousTerm)
67 | end
68 | end)
69 | end
70 |
71 | Event.addRoutine(function()
72 | print('ssh: listening on port 22')
73 | while true do
74 | local socket = Socket.server(22, { ENCRYPT = true })
75 |
76 | print('ssh: connection from ' .. socket.dhost)
77 |
78 | Event.addRoutine(function()
79 | local s, m = pcall(telnetHost, socket, 'SSH')
80 | if not s and m then
81 | print('ssh error')
82 | _G.printError(m)
83 | end
84 | end)
85 | end
86 | end)
87 |
88 | Event.addRoutine(function()
89 | print('telnet: listening on port 23')
90 | while true do
91 | local socket = Socket.server(23)
92 |
93 | print('telnet: connection from ' .. socket.dhost)
94 |
95 | Event.addRoutine(function()
96 | local s, m = pcall(telnetHost, socket, 'Telnet')
97 | if not s and m then
98 | print('Telnet error')
99 | _G.printError(m)
100 | end
101 | end)
102 | end
103 | end)
104 |
--------------------------------------------------------------------------------
/sys/apps/network/trust.lua:
--------------------------------------------------------------------------------
1 | local Crypto = require('opus.crypto.chacha20')
2 | local Event = require('opus.event')
3 | local Security = require('opus.security')
4 | local Socket = require('opus.socket')
5 | local Util = require('opus.util')
6 |
7 | local trustId = '01c3ba27fe01383a03a1785276d99df27c3edcef68fbf231ca'
8 |
9 | local oneTimePassword -- nil by default
10 |
11 | local function validateData(data, password, dhost)
12 | local s
13 | s, data = pcall(Crypto.decrypt, data, password)
14 |
15 | if s and data and type(data) == "table" and data.pk and data.dh == dhost then
16 | local trustList = Util.readTable('usr/.known_hosts') or { }
17 | trustList[data.dh] = data.pk
18 | Util.writeTable('usr/.known_hosts', trustList)
19 | return true
20 | else
21 | return false
22 | end
23 | end
24 |
25 | local function trustConnection(socket)
26 | local data = socket:read(2)
27 | if data then
28 | local password = Security.getPassword()
29 | if not password then
30 | socket:write({ msg = 'No password has been set' })
31 | else
32 | if validateData(data, password, socket.dhost) then
33 | print("Accepted trust from " .. socket.dhost)
34 | socket:write({ success = true, msg = 'Trust accepted' })
35 | return
36 | end
37 |
38 | if oneTimePassword then
39 | if validateData(data, oneTimePassword, socket.dhost) then
40 | print("Accepted trust from " .. socket.dhost .. "using one-time password")
41 | socket:write({ success = true, msg = 'Trust accepted - this one-time password will not be usable again' })
42 | oneTimePassword = nil -- Make sure nobody can use the one-time password again
43 | return
44 | end
45 | end
46 |
47 | socket:write({ msg = 'Invalid password' })
48 | end
49 | end
50 | end
51 |
52 | Event.addRoutine(function()
53 | print('trust: listening on port 19')
54 |
55 | while true do
56 | local socket = Socket.server(19, { identifier = trustId })
57 |
58 | print('trust: connection from ' .. socket.dhost)
59 |
60 | local s, m = pcall(trustConnection, socket)
61 | socket:close()
62 | if not s and m then
63 | print('Trust error')
64 | _G.printError(m)
65 | end
66 | end
67 | end)
68 |
69 | Event.addRoutine(function()
70 | while true do
71 | local _event, password = os.pullEvent("set_otp")
72 |
73 | oneTimePassword = password
74 | print("got new one-time password")
75 | end
76 | end)
--------------------------------------------------------------------------------
/sys/apps/network/vnc.lua:
--------------------------------------------------------------------------------
1 | local Event = require('opus.event')
2 | local Socket = require('opus.socket')
3 | local Util = require('opus.util')
4 |
5 | local os = _G.os
6 | local terminal = _G.device.terminal
7 |
8 | local function vncHost(socket)
9 | local methods = { 'blit', 'clear', 'clearLine', 'setCursorPos', 'write',
10 | 'setTextColor', 'setTextColour', 'setBackgroundColor',
11 | 'setBackgroundColour', 'scroll', 'setCursorBlink', }
12 |
13 | local oldTerm = Util.shallowCopy(terminal)
14 |
15 | for _,k in pairs(methods) do
16 | terminal[k] = function(...)
17 | if not socket.queue then
18 | socket.queue = { }
19 | Event.onTimeout(0, function()
20 | socket:write(socket.queue)
21 | socket.queue = nil
22 | end)
23 | end
24 | table.insert(socket.queue, {
25 | f = k,
26 | args = { ... },
27 | })
28 | oldTerm[k](...)
29 | end
30 | end
31 |
32 | while true do
33 | local data = socket:read()
34 | if not data then
35 | print('vnc: closing connection to ' .. socket.dhost)
36 | break
37 | end
38 |
39 | if data.type == 'shellRemote' then
40 | os.queueEvent(table.unpack(data.event))
41 | elseif data.type == 'termInfo' then
42 | terminal.getSize = function()
43 | return data.width, data.height
44 | end
45 | os.queueEvent('term_resize')
46 | end
47 | end
48 |
49 | for k,v in pairs(oldTerm) do
50 | terminal[k] = v
51 | end
52 | os.queueEvent('term_resize')
53 | end
54 |
55 | Event.addRoutine(function()
56 |
57 | print('vnc: listening on port 5900')
58 |
59 | while true do
60 | local socket = Socket.server(5900)
61 |
62 | print('vnc: connection from ' .. socket.dhost)
63 |
64 | -- no new process - only 1 connection allowed
65 | -- due to term size issues
66 | local s, m = pcall(vncHost, socket)
67 | socket:close()
68 | if not s and m then
69 | print('vnc error')
70 | _G.printError(m)
71 | end
72 | end
73 | end)
74 |
75 | Event.addRoutine(function()
76 |
77 | print('svnc: listening on port 5901')
78 |
79 | while true do
80 | local socket = Socket.server(5901, { ENCRYPT = true })
81 |
82 | print('svnc: connection from ' .. socket.dhost)
83 |
84 | -- no new process - only 1 connection allowed
85 | -- due to term size issues
86 | local s, m = pcall(vncHost, socket)
87 | socket:close()
88 | if not s and m then
89 | print('vnc error')
90 | _G.printError(m)
91 | end
92 | end
93 | end)
94 |
--------------------------------------------------------------------------------
/sys/apps/password.lua:
--------------------------------------------------------------------------------
1 | local Security = require('opus.security')
2 | local SHA = require('opus.crypto.sha2')
3 | local Terminal = require('opus.terminal')
4 |
5 | local password = Terminal.readPassword('Enter new password: ')
6 |
7 | if password then
8 | Security.updatePassword(SHA.compute(password))
9 | print('Password updated')
10 | end
11 |
--------------------------------------------------------------------------------
/sys/apps/pastebin.lua:
--------------------------------------------------------------------------------
1 | local function printUsage()
2 | print( "Usages:" )
3 | print( "pastebin put " )
4 | print( "pastebin get " )
5 | print( "pastebin run " )
6 | end
7 |
8 | if not http then
9 | printError( "Pastebin requires http API" )
10 | printError( "Set http_enable to true in ComputerCraft.cfg" )
11 | return
12 | end
13 |
14 | local pastebin = require('opus.http.pastebin')
15 |
16 | local tArgs = { ... }
17 | local sCommand = tArgs[1]
18 |
19 | if sCommand == "put" then
20 | -- Upload a file to pastebin.com
21 |
22 | if #tArgs < 2 then
23 | printUsage()
24 | return
25 | end
26 |
27 | -- Determine file to upload
28 | local sFile = tArgs[2]
29 | local sPath = shell.resolve( sFile )
30 | if not fs.exists( sPath ) or fs.isDir( sPath ) then
31 | print( "No such file" )
32 | return
33 | end
34 |
35 | print( "Connecting to pastebin.com... " )
36 |
37 | local resp, msg = pastebin.put(sPath)
38 |
39 | if resp then
40 | print( "Uploaded as " .. resp )
41 | print( "Run \"pastebin get "..resp.."\" to download anywhere" )
42 |
43 | else
44 | printError( msg )
45 | end
46 |
47 | elseif sCommand == "get" then
48 | -- Download a file from pastebin.com
49 |
50 | if #tArgs < 3 then
51 | printUsage()
52 | return
53 | end
54 |
55 | local sCode = pastebin.parseCode(tArgs[2])
56 | if not sCode then
57 | return false, "Invalid pastebin code. The code is the ID at the end of the pastebin.com URL."
58 | end
59 |
60 | -- Determine file to download
61 | local sFile = tArgs[3]
62 | local sPath = shell.resolve( sFile )
63 | if fs.exists( sPath ) then
64 | printError( "File already exists" )
65 | return
66 | end
67 |
68 | print( "Connecting to pastebin.com... " )
69 |
70 | local resp, msg = pastebin.get(sCode, sPath)
71 |
72 | if resp then
73 | print( "Downloaded as " .. sPath )
74 | else
75 | printError( msg )
76 | end
77 |
78 | elseif sCommand == "run" then
79 | -- Download and run a file from pastebin.com
80 |
81 | if #tArgs < 2 then
82 | printUsage()
83 | return
84 | end
85 |
86 | local sCode = pastebin.parseCode(tArgs[2])
87 | if not sCode then
88 | return false, "Invalid pastebin code. The code is the ID at the end of the pastebin.com URL."
89 | end
90 |
91 | print( "Connecting to pastebin.com... " )
92 |
93 | local res, msg = pastebin.download(sCode)
94 | if not res then
95 | printError( msg )
96 | return res, msg
97 | end
98 |
99 | res, msg = load(res, sCode, "t", _ENV)
100 | if not res then
101 | printError( msg )
102 | return res, msg
103 | end
104 |
105 | res, msg = pcall(res, table.unpack(tArgs, 3))
106 | if not res then
107 | printError( msg )
108 | end
109 | else
110 |
111 | printUsage()
112 | return
113 | end
114 |
115 |
--------------------------------------------------------------------------------
/sys/apps/system/aliases.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 | local UI = require('opus.ui')
3 |
4 | local kernel = _G.kernel
5 |
6 | local aliasTab = UI.Tab {
7 | title = 'Aliases',
8 | description = 'Shell aliases',
9 | alias = UI.TextEntry {
10 | x = 2, y = 2, ex = -2,
11 | limit = 32,
12 | shadowText = 'Alias',
13 | },
14 | path = UI.TextEntry {
15 | y = 3, x = 2, ex = -2,
16 | shadowText = 'Program path',
17 | accelerators = {
18 | enter = 'new_alias',
19 | },
20 | },
21 | grid = UI.Grid {
22 | x = 2, y = 5, ex = -2, ey = -2,
23 | sortColumn = 'alias',
24 | columns = {
25 | { heading = 'Alias', key = 'alias' },
26 | { heading = 'Program', key = 'path' },
27 | },
28 | accelerators = {
29 | delete = 'delete_alias',
30 | },
31 | },
32 | }
33 |
34 | function aliasTab.grid:draw()
35 | self.values = { }
36 | local env = Config.load('shell')
37 | for k in pairs(kernel.getShell().aliases()) do
38 | kernel.getShell().clearAlias(k)
39 | end
40 | for k,v in pairs(env.aliases) do
41 | table.insert(self.values, { alias = k, path = v })
42 | kernel.getShell().setAlias(k, v)
43 | end
44 | self:update()
45 | UI.Grid.draw(self)
46 | end
47 |
48 | function aliasTab:eventHandler(event)
49 | if event.type == 'delete_alias' then
50 | local env = Config.load('shell', { aliases = { } })
51 | env.aliases[self.grid:getSelected().alias] = nil
52 | Config.update('shell', env)
53 | self.grid:setIndex(self.grid:getIndex())
54 | self.grid:draw()
55 | self:emit({ type = 'success_message', message = 'Aliases updated' })
56 | return true
57 |
58 | elseif event.type == 'new_alias' then
59 | local env = Config.load('shell', { aliases = { } })
60 | env.aliases[self.alias.value] = self.path.value
61 | Config.update('shell', env)
62 | self.alias:reset()
63 | self.path:reset()
64 | self:draw()
65 | self:setFocus(self.alias)
66 | self:emit({ type = 'success_message', message = 'Aliases updated' })
67 | return true
68 | end
69 | end
70 |
71 | return aliasTab
72 |
--------------------------------------------------------------------------------
/sys/apps/system/cloud.lua:
--------------------------------------------------------------------------------
1 | local Ansi = require('opus.ansi')
2 | local Config = require('opus.config')
3 | local UI = require('opus.ui')
4 |
5 | if _G.http.websocket then
6 | local config = Config.load('cloud')
7 |
8 | local tab = UI.Tab {
9 | title = 'Cloud',
10 | description = 'Cloud Catcher options',
11 | [1] = UI.Window {
12 | x = 2, y = 2, ex = -2, ey = 4,
13 | },
14 | key = UI.TextEntry {
15 | x = 3, ex = -3, y = 3,
16 | limit = 32,
17 | value = config.key,
18 | shadowText = 'Cloud key',
19 | accelerators = {
20 | enter = 'update_key',
21 | },
22 | },
23 | button = UI.Button {
24 | x = -8, ex = -2, y = -2,
25 | text = 'Apply',
26 | event = 'update_key',
27 | },
28 | labelText = UI.TextArea {
29 | x = 2, ex = -2, y = 5, ey = -4,
30 | textColor = 'yellow',
31 | backgroundColor = 'black',
32 | marginLeft = 1, marginRight = 1, marginTop = 1,
33 | value = string.format(
34 | [[Use a non-changing cloud key. Note that only a single computer can use this session at one time.
35 | To obtain a key, visit:
36 | %shttps://cloud-catcher.squiddev.cc%s then bookmark:
37 | %shttps://cloud-catcher.squiddev.cc/?id=KEY
38 | ]],
39 | Ansi.white, Ansi.reset, Ansi.white),
40 | },
41 | }
42 |
43 | function tab:eventHandler(event)
44 | if event.type == 'update_key' then
45 | if self.key.value then
46 | config.key = self.key.value
47 | else
48 | config.key = nil
49 | end
50 | Config.update('cloud', config)
51 | self:emit({ type = 'success_message', message = 'Updated' })
52 | end
53 | end
54 |
55 | return tab
56 | end
57 |
58 |
--------------------------------------------------------------------------------
/sys/apps/system/kiosk.lua:
--------------------------------------------------------------------------------
1 | local UI = require('opus.ui')
2 |
3 | local colors = _G.colors
4 | local peripheral = _G.peripheral
5 | local settings = _G.settings
6 |
7 | return peripheral.find('monitor') and UI.Tab {
8 | title = 'Kiosk',
9 | description = 'Kiosk options',
10 | form = UI.Form {
11 | x = 2, y = 2, ex = -2, ey = 5,
12 | manualControls = true,
13 | monitor = UI.Chooser {
14 | formLabel = 'Monitor', formKey = 'monitor',
15 | },
16 | textScale = UI.Chooser {
17 | formLabel = 'Font Size', formKey = 'textScale',
18 | nochoice = 'Small',
19 | choices = {
20 | { name = 'Small', value = '.5' },
21 | { name = 'Large', value = '1' },
22 | },
23 | help = 'Adjust text scaling',
24 | },
25 | },
26 | labelText = UI.TextArea {
27 | x = 2, ex = -2, y = 7, ey = -2,
28 | textColor = colors.yellow,
29 | backgroundColor = colors.black,
30 | value = 'Settings apply to kiosk mode selected during startup'
31 | },
32 | enable = function(self)
33 | local choices = { }
34 |
35 | peripheral.find('monitor', function(side)
36 | table.insert(choices, { name = side, value = side })
37 | end)
38 |
39 | self.form.monitor.choices = choices
40 | self.form.monitor.value = settings.get('kiosk.monitor')
41 |
42 | self.form.textScale.value = settings.get('kiosk.textscale')
43 |
44 | UI.Tab.enable(self)
45 | end,
46 | eventHandler = function(self, event)
47 | if event.type == 'choice_change' then
48 | if self.form.monitor.value then
49 | settings.set('kiosk.monitor', self.form.monitor.value)
50 | end
51 | if self.form.textScale.value then
52 | settings.set('kiosk.textscale', self.form.textScale.value)
53 | end
54 | settings.save('.settings')
55 | end
56 | end
57 | }
58 |
--------------------------------------------------------------------------------
/sys/apps/system/label.lua:
--------------------------------------------------------------------------------
1 | local UI = require('opus.ui')
2 | local Util = require('opus.util')
3 |
4 | local fs = _G.fs
5 | local os = _G.os
6 |
7 | return UI.Tab {
8 | title = 'Label',
9 | description = 'Set the computer label',
10 | labelText = UI.Text {
11 | x = 3, y = 3,
12 | value = 'Label'
13 | },
14 | label = UI.TextEntry {
15 | x = 9, y = 3, ex = -4,
16 | limit = 32,
17 | value = os.getComputerLabel(),
18 | accelerators = {
19 | enter = 'update_label',
20 | },
21 | },
22 | [1] = UI.Window {
23 | x = 2, y = 2, ex = -2, ey = 4,
24 | },
25 | grid = UI.ScrollingGrid {
26 | x = 2, y = 5, ex = -2, ey = -2,
27 | values = {
28 | { name = '', value = '' },
29 | { name = 'CC version', value = Util.getVersion() },
30 | { name = 'Lua version', value = _VERSION },
31 | { name = 'MC version', value = Util.getMinecraftVersion() },
32 | { name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
33 | { name = 'Computer ID', value = tostring(os.getComputerID()) },
34 | { name = 'Day', value = tostring(os.day()) },
35 | },
36 | disableHeader = true,
37 | inactive = true,
38 | columns = {
39 | { key = 'name', width = 12 },
40 | { key = 'value', textColor = colors.yellow },
41 | },
42 | },
43 | eventHandler = function(self, event)
44 | if event.type == 'update_label' and self.label.value then
45 | os.setComputerLabel(self.label.value)
46 | self:emit({ type = 'success_message', message = 'Label updated' })
47 | return true
48 | end
49 | end,
50 | }
51 |
--------------------------------------------------------------------------------
/sys/apps/system/launcher.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 | local UI = require('opus.ui')
3 |
4 | local colors = _G.colors
5 | local fs = _G.fs
6 |
7 | local config = Config.load('multishell')
8 |
9 | local tab = UI.Tab {
10 | title = 'Launcher',
11 | description = 'Set the application launcher',
12 | [1] = UI.Window {
13 | x = 2, y = 2, ex = -2, ey = 5,
14 | },
15 | launcherLabel = UI.Text {
16 | x = 3, y = 3,
17 | value = 'Launcher',
18 | },
19 | launcher = UI.Chooser {
20 | x = 13, y = 3, width = 12,
21 | choices = {
22 | { name = 'Overview', value = 'sys/apps/Overview.lua' },
23 | { name = 'Shell', value = 'sys/apps/ShellLauncher.lua' },
24 | { name = 'Custom', value = 'custom' },
25 | },
26 | },
27 | custom = UI.TextEntry {
28 | x = 13, ex = -3, y = 4,
29 | shadowText = 'File name',
30 | },
31 | button = UI.Button {
32 | x = -8, ex = -2, y = -2,
33 | text = 'Apply',
34 | event = 'update',
35 | },
36 | labelText = UI.TextArea {
37 | x = 2, ex = -2, y = 6, ey = -4,
38 | backgroundColor = colors.black,
39 | textColor = colors.yellow,
40 | marginLeft = 1, marginRight = 1, marginTop = 1,
41 | value = 'Choose an application launcher',
42 | },
43 | }
44 |
45 | function tab:enable()
46 | local launcher = config.launcher and 'custom' or 'sys/apps/Overview.lua'
47 |
48 | for _, v in pairs(self.launcher.choices) do
49 | if v.value == config.launcher then
50 | launcher = v.value
51 | break
52 | end
53 | end
54 |
55 | UI.Tab.enable(self)
56 |
57 | self.launcher.value = launcher
58 | self.custom.enabled = launcher == 'custom'
59 | end
60 |
61 | function tab:eventHandler(event)
62 | if event.type == 'choice_change' then
63 | self.custom.enabled = event.value == 'custom'
64 | if self.custom.enabled then
65 | self.custom.value = config.launcher
66 | end
67 | self:draw()
68 |
69 | elseif event.type == 'update' then
70 | local launcher
71 |
72 | if self.launcher.value ~= 'custom' then
73 | launcher = self.launcher.value
74 | elseif fs.exists(self.custom.value) and not fs.isDir(self.custom.value) then
75 | launcher = self.custom.value
76 | end
77 |
78 | if launcher then
79 | config.launcher = launcher
80 | Config.update('multishell', config)
81 | self:emit({ type = 'success_message', message = 'Updated' })
82 | else
83 | self:emit({ type = 'error_message', message = 'Invalid file' })
84 | end
85 | end
86 | end
87 |
88 | return tab
89 |
--------------------------------------------------------------------------------
/sys/apps/system/network.lua:
--------------------------------------------------------------------------------
1 | local Ansi = require('opus.ansi')
2 | local Config = require('opus.config')
3 | local UI = require('opus.ui')
4 |
5 | local colors = _G.colors
6 | local device = _G.device
7 |
8 | return UI.Tab {
9 | title = 'Network',
10 | description = 'Networking options',
11 | info = UI.TextArea {
12 | x = 2, y = 5, ex = -2, ey = -2,
13 | backgroundColor = colors.black,
14 | marginLeft = 1, marginRight = 1, marginTop = 1,
15 | value = string.format(
16 | [[%sSet the primary modem used for wireless communications.%s
17 |
18 | Reboot to take effect.]], Ansi.yellow, Ansi.reset)
19 | },
20 | [1] = UI.Window {
21 | x = 2, y = 2, ex = -2, ey = 4,
22 | },
23 | label = UI.Text {
24 | x = 3, y = 3,
25 | value = 'Modem',
26 | },
27 | modem = UI.Chooser {
28 | x = 10, ex = -3, y = 3,
29 | nochoice = 'auto',
30 | },
31 | enable = function(self)
32 | local width = 7
33 | local choices = {
34 | { name = 'auto', value = 'auto' },
35 | { name = 'disable', value = 'none' },
36 | }
37 |
38 | for k,v in pairs(device) do
39 | if v.isWireless and v.isWireless() and k ~= 'wireless_modem' then
40 | table.insert(choices, { name = k, value = v.name })
41 | width = math.max(width, #k)
42 | end
43 | end
44 |
45 | self.modem.choices = choices
46 | --self.modem.width = width + 4
47 |
48 | local config = Config.load('os')
49 | self.modem.value = config.wirelessModem or 'auto'
50 |
51 | UI.Tab.enable(self)
52 | end,
53 | eventHandler = function(self, event)
54 | if event.type == 'choice_change' then
55 | local config = Config.load('os')
56 | config.wirelessModem = self.modem.value
57 | Config.update('os', config)
58 | self:emit({ type = 'success_message', message = 'reboot to take effect' })
59 | return true
60 | end
61 | end
62 | }
63 |
--------------------------------------------------------------------------------
/sys/apps/system/password.lua:
--------------------------------------------------------------------------------
1 | local Security = require('opus.security')
2 | local SHA = require('opus.crypto.sha2')
3 | local UI = require('opus.ui')
4 |
5 | return UI.Tab {
6 | title = 'Password',
7 | description = 'Wireless network password',
8 | [1] = UI.Window {
9 | x = 2, y = 2, ex = -2, ey = 4,
10 | },
11 | newPass = UI.TextEntry {
12 | x = 3, ex = -3, y = 3,
13 | limit = 32,
14 | mask = true,
15 | shadowText = 'new password',
16 | accelerators = {
17 | enter = 'new_password',
18 | },
19 | },
20 | button = UI.Button {
21 | x = -8, ex = -2, y = -2,
22 | text = 'Apply',
23 | event = 'update_password',
24 | },
25 | info = UI.TextArea {
26 | x = 2, ex = -2, y = 5, ey = -4,
27 | backgroundColor = 'black',
28 | textColor = 'yellow',
29 | inactive = true,
30 | marginLeft = 1, marginRight = 1, marginTop = 1,
31 | value = 'Add a password to enable other computers to connect to this one.',
32 | },
33 | eventHandler = function(self, event)
34 | if event.type == 'update_password' then
35 | if not self.newPass.value or #self.newPass.value == 0 then
36 | self:emit({ type = 'error_message', message = 'Invalid password' })
37 |
38 | else
39 | Security.updatePassword(SHA.compute(self.newPass.value))
40 | self:emit({ type = 'success_message', message = 'Password updated' })
41 | end
42 | return true
43 | end
44 | end
45 | }
46 |
--------------------------------------------------------------------------------
/sys/apps/system/path.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | local tab = UI.Tab {
6 | title = 'Path',
7 | description = 'Set the shell path',
8 | tabClose = true,
9 | [1] = UI.Window {
10 | x = 2, y = 2, ex = -2, ey = 4,
11 | },
12 | entry = UI.TextEntry {
13 | x = 3, y = 3, ex = -3,
14 | shadowText = 'enter new path',
15 | accelerators = {
16 | enter = 'update_path',
17 | },
18 | help = 'add a new path',
19 | },
20 | grid = UI.Grid {
21 | x = 2, y = 6, ex = -2, ey = -3,
22 | disableHeader = true,
23 | columns = { { key = 'value' } },
24 | autospace = true,
25 | sortColumn = 'index',
26 | help = 'double-click to remove, shift-arrow to move',
27 | accelerators = {
28 | delete = 'remove',
29 | },
30 | },
31 | statusBar = UI.StatusBar { },
32 | accelerators = {
33 | [ 'shift-up' ] = 'move_up',
34 | [ 'shift-down' ] = 'move_down',
35 | },
36 | }
37 |
38 | function tab:updateList(path)
39 | self.grid.values = { }
40 | for k,v in ipairs(Util.split(path, '(.-):')) do
41 | table.insert(self.grid.values, { index = k, value = v })
42 | end
43 | self.grid:update()
44 | end
45 |
46 | function tab:enable()
47 | local env = Config.load('shell')
48 | self:updateList(env.path)
49 | UI.Tab.enable(self)
50 | end
51 |
52 | function tab:save()
53 | local t = { }
54 | for _, v in ipairs(self.grid.values) do
55 | table.insert(t, v.value)
56 | end
57 | local env = Config.load('shell')
58 | env.path = table.concat(t, ':')
59 | self:updateList(env.path)
60 | Config.update('shell', env)
61 | end
62 |
63 | function tab:eventHandler(event)
64 | if event.type == 'update_path' and self.entry.value then
65 | table.insert(self.grid.values, {
66 | value = self.entry.value,
67 | })
68 | self:save()
69 | self.entry:reset()
70 | self.entry:draw()
71 | self.grid:draw()
72 | return true
73 |
74 | elseif event.type == 'grid_select' or event.type == 'remove' then
75 | local selected = self.grid:getSelected()
76 | if selected then
77 | table.remove(self.grid.values, selected.index)
78 | self:save()
79 | self.grid:draw()
80 | end
81 |
82 | elseif event.type == 'focus_change' then
83 | self.statusBar:setStatus(event.focused.help)
84 |
85 | elseif event.type == 'move_up' then
86 | local entry = self.grid:getSelected()
87 | if entry.index > 1 then
88 | table.insert(self.grid.values, entry.index - 1, table.remove(self.grid.values, entry.index))
89 | self.grid:setIndex(entry.index - 1)
90 | self:save()
91 | self.grid:draw()
92 | end
93 |
94 | elseif event.type == 'move_down' then
95 | local entry = self.grid:getSelected()
96 | if entry.index < #self.grid.values then
97 | table.insert(self.grid.values, entry.index + 1, table.remove(self.grid.values, entry.index))
98 | self.grid:setIndex(entry.index + 1)
99 | self:save()
100 | self.grid:draw()
101 | end
102 | end
103 | end
104 |
105 | return tab
106 |
--------------------------------------------------------------------------------
/sys/apps/system/requires.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | local tab = UI.Tab {
6 | title = 'Requires',
7 | description = 'Require path',
8 | tabClose = true,
9 | entry = UI.TextEntry {
10 | x = 2, y = 2, ex = -2,
11 | shadowText = 'Enter new require path',
12 | accelerators = {
13 | enter = 'update_path',
14 | },
15 | help = 'add a new path (reboot required)',
16 | },
17 | grid = UI.Grid {
18 | y = 4, ey = -3,
19 | disableHeader = true,
20 | columns = { { key = 'value' } },
21 | autospace = true,
22 | sortColumn = 'index',
23 | help = 'double-click to remove, shift-arrow to move',
24 | accelerators = {
25 | delete = 'remove',
26 | },
27 | },
28 | statusBar = UI.StatusBar { },
29 | accelerators = {
30 | [ 'shift-up' ] = 'move_up',
31 | [ 'shift-down' ] = 'move_down',
32 | },
33 | }
34 |
35 | function tab:updateList(lua_path)
36 | self.grid.values = { }
37 | for k,v in ipairs(Util.split(lua_path, '(.-);')) do
38 | table.insert(self.grid.values, { index = k, value = v })
39 | end
40 | self.grid:update()
41 | end
42 |
43 | function tab:enable()
44 | local env = Config.load('shell')
45 | self:updateList(env.lua_path)
46 | UI.Tab.enable(self)
47 | end
48 |
49 | function tab:save()
50 | local t = { }
51 | for _, v in ipairs(self.grid.values) do
52 | table.insert(t, v.value)
53 | end
54 | local env = Config.load('shell')
55 | env.lua_path = table.concat(t, ';')
56 | self:updateList(env.lua_path)
57 | Config.update('shell', env)
58 | end
59 |
60 | function tab:eventHandler(event)
61 | if event.type == 'update_path' then
62 | table.insert(self.grid.values, {
63 | value = self.entry.value,
64 | })
65 | self:save()
66 | self.entry:reset()
67 | self.entry:draw()
68 | self.grid:draw()
69 | return true
70 |
71 | elseif event.type == 'grid_select' or event.type == 'remove' then
72 | local selected = self.grid:getSelected()
73 | if selected then
74 | table.remove(self.grid.values, selected.index)
75 | self:save()
76 | self.grid:draw()
77 | end
78 |
79 | elseif event.type == 'focus_change' then
80 | self.statusBar:setStatus(event.focused.help)
81 |
82 | elseif event.type == 'move_up' then
83 | local entry = self.grid:getSelected()
84 | if entry.index > 1 then
85 | table.insert(self.grid.values, entry.index - 1, table.remove(self.grid.values, entry.index))
86 | self.grid:setIndex(entry.index - 1)
87 | self:save()
88 | self.grid:draw()
89 | end
90 |
91 | elseif event.type == 'move_down' then
92 | local entry = self.grid:getSelected()
93 | if entry.index < #self.grid.values then
94 | table.insert(self.grid.values, entry.index + 1, table.remove(self.grid.values, entry.index))
95 | self.grid:setIndex(entry.index + 1)
96 | self:save()
97 | self.grid:draw()
98 | end
99 | end
100 | end
101 |
102 | --this needs rework - see 4.user.lua
103 | --return tab
104 |
--------------------------------------------------------------------------------
/sys/apps/system/settings.lua:
--------------------------------------------------------------------------------
1 | local UI = require('opus.ui')
2 |
3 | local settings = _G.settings
4 |
5 | local transform = {
6 | string = tostring,
7 | number = tonumber,
8 | }
9 |
10 | return settings and UI.Tab {
11 | title = 'Settings',
12 | description = 'Computercraft settings',
13 | grid = UI.Grid {
14 | x = 2, y = 2, ex = -2, ey = -2,
15 | sortColumn = 'name',
16 | columns = {
17 | { heading = 'Setting', key = 'name' },
18 | { heading = 'Value', key = 'value' },
19 | },
20 | },
21 | editor = UI.SlideOut {
22 | y = -6, height = 6,
23 | titleBar = UI.TitleBar {
24 | event = 'slide_hide',
25 | title = 'Enter value',
26 | },
27 | form = UI.Form {
28 | y = 2,
29 | value = UI.TextEntry {
30 | formIndex = 1,
31 | formLabel = 'Value',
32 | formKey = 'value',
33 | },
34 | validateField = function(self, entry)
35 | if entry.value then
36 | return transform[self.type](entry.value)
37 | end
38 | return true
39 | end,
40 | },
41 | accelerators = {
42 | form_cancel = 'slide_hide',
43 | },
44 | show = function(self, entry)
45 | self.form.type = type(entry.value) or 'string'
46 | self.form:setValues(entry)
47 | self.titleBar.title = entry.name
48 | UI.SlideOut.show(self)
49 | end,
50 | eventHandler = function(self, event)
51 | if event.type == 'form_complete' then
52 | if not event.values.value then
53 | settings.unset(event.values.name)
54 | self.parent:reload()
55 | else
56 | event.values.value = transform[self.form.type](event.values.value)
57 | settings.set(event.values.name, event.values.value)
58 | end
59 | self.parent.grid:draw()
60 | self:hide()
61 | settings.save('.settings')
62 | end
63 | return UI.SlideOut.eventHandler(self, event)
64 | end,
65 | },
66 | reload = function(self)
67 | local values = { }
68 | for _,v in pairs(settings.getNames()) do
69 | table.insert(values, {
70 | name = v,
71 | value = settings.get(v) or false,
72 | })
73 | end
74 | self.grid:setValues(values)
75 | self.grid:setIndex(1)
76 | end,
77 | enable = function(self)
78 | self:reload()
79 | UI.Tab.enable(self)
80 | end,
81 | eventHandler = function(self, event)
82 | if event.type == 'grid_select' then
83 | if type(event.selected.value) == 'boolean' then
84 | event.selected.value = not event.selected.value
85 | settings.set(event.selected.name, event.selected.value)
86 | settings.save('.settings')
87 | self.grid:draw()
88 | else
89 | self.editor:show(event.selected)
90 | end
91 | return true
92 | end
93 | end,
94 | }
95 |
--------------------------------------------------------------------------------
/sys/apps/system/theme.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | local colors = _G.colors
6 |
7 | local allColors = { }
8 | for k,v in pairs(colors) do
9 | if type(v) == 'number' then
10 | table.insert(allColors, { name = k, value = v })
11 | end
12 | end
13 |
14 | local allSettings = { }
15 | for k,v in pairs(UI.theme.colors) do
16 | allSettings[k] = { name = k, value = v }
17 | end
18 |
19 | return UI.Tab {
20 | title = 'Theme',
21 | description = 'Theme colors',
22 | grid1 = UI.ScrollingGrid {
23 | y = 2, ey = -10, x = 2, ex = -17,
24 | disableHeader = true,
25 | columns = { { key = 'name' } },
26 | values = allSettings,
27 | sortColumn = 'name',
28 | },
29 | grid2 = UI.ScrollingGrid {
30 | y = 2, ey = -10, x = -14, ex = -2,
31 | disableHeader = true,
32 | columns = { { key = 'name' } },
33 | values = allColors,
34 | sortColumn = 'name',
35 | getRowTextColor = function(self, row)
36 | local selected = self.parent.grid1:getSelected()
37 | if selected.value == row.value then
38 | return colors.yellow
39 | end
40 | return UI.Grid.getRowTextColor(self, row)
41 | end
42 | },
43 | button = UI.Button {
44 | x = -9, y = -2,
45 | text = 'Update',
46 | event = 'update',
47 | },
48 | display = UI.Window {
49 | x = 2, ex = -2, y = -8, height = 5,
50 | textColor = colors.black,
51 | backgroundColor = colors.black,
52 | draw = function(self)
53 | self:clear()
54 |
55 | self:write(1, 1, Util.widthify(' Local Global Device', self.width),
56 | allSettings.secondary.value)
57 |
58 | self:write(2, 2, 'enter command ',
59 | colors.black, colors.gray)
60 |
61 | self:write(1, 3, ' Formatted ',
62 | allSettings.primary.value)
63 |
64 | self:write(12, 3, Util.widthify(' Output ', self.width - 11),
65 | allSettings.tertiary.value)
66 |
67 | self:write(1, 4, Util.widthify(' Key', self.width),
68 | allSettings.primary.value)
69 | end,
70 | },
71 | eventHandler = function(self, event)
72 | if event.type == 'grid_focus_row' and event.element == self.grid1 then
73 | self.grid2:draw()
74 |
75 | elseif event.type == 'grid_select' and event.element == self.grid2 then
76 | self.grid1:getSelected().value = event.selected.value
77 | self.display:draw()
78 | self.grid2:draw()
79 |
80 | elseif event.type == 'update' then
81 | local config = Config.load('ui.theme', { colors = { } })
82 | for k,v in pairs(allSettings) do
83 | config.colors[k] = v.value
84 | end
85 | Config.update('ui.theme', config)
86 | end
87 | return UI.Tab.eventHandler(self, event)
88 | end
89 | }
90 |
--------------------------------------------------------------------------------
/sys/apps/system/trust.lua:
--------------------------------------------------------------------------------
1 | local UI = require("opus.ui")
2 | local Util = require("opus.util")
3 | local SHA = require('opus.crypto.sha2')
4 |
5 | local function split(s)
6 | local b = ""
7 | for i = 1, #s, 5 do
8 | b = b .. s:sub(i, i+4)
9 | if i ~= #s-4 then
10 | b = b .. "-"
11 | end
12 | end
13 | return b
14 | end
15 |
16 | return UI.Tab {
17 | title = 'Trust',
18 | description = 'Manage trusted devices',
19 | grid = UI.Grid {
20 | x = 2, y = 2, ex = -2, ey = -3,
21 | autospace = true,
22 | sortColumn = 'id',
23 | columns = {
24 | { heading = 'Computer ID', key = 'id'},
25 | { heading = 'Identity', key = 'pkey'}
26 | }
27 | },
28 | statusBar = UI.StatusBar { values = 'double-click to revoke trust' },
29 | reload = function(self)
30 | local values = {}
31 | for k,v in pairs(Util.readTable('usr/.known_hosts') or {}) do
32 | table.insert(values, {
33 | id = k,
34 | pkey = split(SHA.compute(v):sub(-20):upper()) -- Obfuscate private key for visual ident
35 | })
36 | end
37 | self.grid:setValues(values)
38 | self.grid:setIndex(1)
39 | end,
40 | enable = function(self)
41 | self:reload()
42 | UI.Tab.enable(self)
43 | end,
44 | eventHandler = function(self, event)
45 | if event.type == 'grid_select' then
46 | local hosts = Util.readTable('usr/.known_hosts')
47 | hosts[event.selected.id] = nil
48 | Util.writeTable('usr/.known_hosts', hosts)
49 | self:reload()
50 | else
51 | return UI.Tab.eventHandler(self, event)
52 | end
53 | return true
54 | end
55 | }
--------------------------------------------------------------------------------
/sys/apps/telnet.lua:
--------------------------------------------------------------------------------
1 | local Event = require('opus.event')
2 | local Socket = require('opus.socket')
3 | local Terminal = require('opus.terminal')
4 | local Util = require('opus.util')
5 |
6 | local multishell = _ENV.multishell
7 | local os = _G.os
8 | local read = _G.read
9 | local shell = _ENV.shell
10 | local term = _G.term
11 |
12 | local args, options = Util.parse(...)
13 |
14 | local remoteId = tonumber(table.remove(args, 1) or '')
15 | if not remoteId then
16 | print('Enter host ID')
17 | remoteId = tonumber(read())
18 | end
19 |
20 | if not remoteId then
21 | error('Syntax: telnet ID [PROGRAM] [ARGS]')
22 | end
23 |
24 | if multishell then
25 | multishell.setTitle(multishell.getCurrent(),
26 | (options.s and 'Secure ' or 'Telnet ') .. remoteId)
27 | end
28 |
29 | local socket, msg, reason
30 |
31 | while true do
32 | socket, msg, reason = Socket.connect(remoteId, options.s and 22 or 23)
33 |
34 | if socket then
35 | break
36 | elseif reason ~= 'NOTRUST' then
37 | error(msg)
38 | end
39 |
40 | local s, m = shell.run('trust ' .. remoteId)
41 | if not s then
42 | error(m)
43 | end
44 | end
45 |
46 | local ct = Util.shallowCopy(term.current())
47 | if not ct.isColor() then
48 | Terminal.toGrayscale(ct)
49 | end
50 |
51 | local w, h = ct.getSize()
52 | socket:write({
53 | width = w,
54 | height = h,
55 | isColor = ct.isColor(),
56 | program = args,
57 | pos = { ct.getCursorPos() },
58 | })
59 |
60 | Event.addRoutine(function()
61 | while true do
62 | local data = socket:read()
63 | if not data then
64 | break
65 | end
66 | for _,v in ipairs(data) do
67 | ct[v.f](table.unpack(v.args))
68 | end
69 | end
70 | end)
71 |
72 | --ct.clear()
73 | --ct.setCursorPos(1, 1)
74 |
75 | local filter = Util.transpose {
76 | 'char', 'paste', 'key', 'key_up', 'terminate',
77 | 'mouse_scroll', 'mouse_click', 'mouse_drag', 'mouse_up',
78 | }
79 |
80 | while true do
81 | local e = { os.pullEventRaw() }
82 | local event = e[1]
83 |
84 | if filter[event] then
85 | socket:write(e)
86 | else
87 | Event.processEvent(e)
88 | end
89 |
90 | if not socket.connected then
91 | -- print()
92 | -- print('Connection lost')
93 | -- print('Press enter to exit')
94 | -- pcall(read)
95 | break
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/sys/apps/trust.lua:
--------------------------------------------------------------------------------
1 | local Crypto = require('opus.crypto.chacha20')
2 | local Security = require('opus.security')
3 | local SHA = require('opus.crypto.sha2')
4 | local Socket = require('opus.socket')
5 | local Terminal = require('opus.terminal')
6 |
7 | local os = _G.os
8 |
9 | local remoteId
10 | local args = { ... }
11 |
12 | if #args == 1 then
13 | remoteId = tonumber(args[1])
14 | else
15 | print('Enter host ID')
16 | remoteId = tonumber(_G.read())
17 | end
18 |
19 | if not remoteId then
20 | error('Syntax: trust ')
21 | end
22 |
23 | local password = Terminal.readPassword('Enter password: ')
24 |
25 | if not password then
26 | error('Invalid password')
27 | end
28 |
29 | print('connecting...')
30 | local trustId = '01c3ba27fe01383a03a1785276d99df27c3edcef68fbf231ca'
31 | local socket, msg = Socket.connect(remoteId, 19, { identifier = trustId })
32 |
33 | if not socket then
34 | error(msg)
35 | end
36 |
37 | local identifier = Security.getIdentifier()
38 |
39 | socket:write(Crypto.encrypt({ pk = identifier, dh = os.getComputerID() }, SHA.compute(password)))
40 |
41 | local data = socket:read(2)
42 | socket:close()
43 |
44 | if data and data.success then
45 | print(data.msg)
46 | elseif data then
47 | error(data.msg)
48 | else
49 | error('No response')
50 | end
51 |
--------------------------------------------------------------------------------
/sys/apps/vnc.lua:
--------------------------------------------------------------------------------
1 | local Event = require('opus.event')
2 | local Socket = require('opus.socket')
3 | local Terminal = require('opus.terminal')
4 | local Util = require('opus.util')
5 |
6 | local colors = _G.colors
7 | local multishell = _ENV.multishell
8 | local os = _G.os
9 | local shell = _ENV.shell
10 | local term = _G.term
11 |
12 | local remoteId
13 | local args, options = Util.parse(...)
14 | if #args == 1 then
15 | remoteId = tonumber(args[1])
16 | else
17 | print('Enter host ID')
18 | remoteId = tonumber(_G.read())
19 | end
20 |
21 | if not remoteId then
22 | error('Syntax: vnc ')
23 | end
24 |
25 | if multishell then
26 | multishell.setTitle(multishell.getCurrent(),
27 | (options.s and 'SVNC-' or 'VNC-') .. remoteId)
28 | end
29 |
30 | local function connect()
31 | local socket, msg, reason = Socket.connect(remoteId, options.s and 5901 or 5900)
32 |
33 | if reason == 'NOTRUST' then
34 | local s, m = shell.run('trust ' .. remoteId)
35 | if not s then
36 | return s, m
37 | end
38 | socket, msg = Socket.connect(remoteId, 5900)
39 | end
40 |
41 | if not socket then
42 | return false, msg
43 | end
44 |
45 | local function writeTermInfo()
46 | local w, h = term.getSize()
47 | socket:write({
48 | type = 'termInfo',
49 | width = w,
50 | height = h,
51 | isColor = term.isColor(),
52 | })
53 | end
54 |
55 | writeTermInfo()
56 |
57 | local ct = Util.shallowCopy(term.current())
58 |
59 | if not ct.isColor() then
60 | Terminal.toGrayscale(ct)
61 | end
62 |
63 | ct.clear()
64 | ct.setCursorPos(1, 1)
65 |
66 | Event.addRoutine(function()
67 | while true do
68 | local data = socket:read()
69 | if not data then
70 | break
71 | end
72 | for _,v in ipairs(data) do
73 | ct[v.f](table.unpack(v.args))
74 | end
75 | end
76 | end)
77 |
78 | local filter = Util.transpose({
79 | 'char', 'paste', 'key', 'key_up',
80 | 'mouse_scroll', 'mouse_click', 'mouse_drag', 'mouse_up',
81 | })
82 |
83 | while true do
84 | local e = Event.pullEvent()
85 | local event = e[1]
86 |
87 | if not socket.connected then
88 | break
89 | end
90 |
91 | if filter[event] then
92 | socket:write({
93 | type = 'shellRemote',
94 | event = e,
95 | })
96 | elseif event == 'term_resize' then
97 | writeTermInfo()
98 | elseif event == 'terminate' then
99 | socket:close()
100 | ct.setBackgroundColor(colors.black)
101 | ct.clear()
102 | ct.setCursorPos(1, 1)
103 | return true
104 | end
105 | end
106 | return false, "Connection Lost"
107 | end
108 |
109 | while true do
110 | term.clear()
111 | term.setCursorPos(1, 1)
112 |
113 | print('connecting...')
114 | local s, m = connect()
115 | if s then
116 | break
117 | end
118 |
119 | term.setBackgroundColor(colors.black)
120 | term.setTextColor(colors.white)
121 | term.clear()
122 | term.setCursorPos(1, 1)
123 | print(m)
124 | print('\nPress any key to exit')
125 | print('\nRetrying in ... ')
126 | local x, y = term.getCursorPos()
127 | for i = 5, 1, -1 do
128 | local timerId = os.startTimer(1)
129 | term.setCursorPos(x, y)
130 | term.write(i)
131 | repeat
132 | local e, id = os.pullEvent()
133 | if e == 'char' or e == 'key' then
134 | return
135 | end
136 | until e == 'timer' and id == timerId
137 | end
138 | end
139 |
--------------------------------------------------------------------------------
/sys/autorun/clipboard.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local kernel = _G.kernel
4 | local keyboard = _G.device.keyboard
5 | local os = _G.os
6 | local textutils = _G.textutils
7 |
8 | kernel.hook('clipboard_copy', function(_, args)
9 | keyboard.clipboard = args[1]
10 | end)
11 |
12 | local function queuePaste()
13 | local data = keyboard.clipboard
14 |
15 | if type(data) == 'table' then
16 | local s, m = pcall(textutils.serialize, data)
17 | data = s and m or Util.tostring(data)
18 | end
19 |
20 | if data then
21 | os.queueEvent('paste', data)
22 | end
23 | end
24 |
25 | kernel.hook('clipboard_paste', queuePaste)
26 | keyboard.addHotkey('shift-paste', queuePaste)
27 |
--------------------------------------------------------------------------------
/sys/autorun/complete.lua:
--------------------------------------------------------------------------------
1 | local fs = _G.fs
2 |
3 | local function completeMultipleChoice(sText, tOptions, bAddSpaces)
4 | local tResults = { }
5 | for n = 1,#tOptions do
6 | local sOption = tOptions[n]
7 | if #sOption + (bAddSpaces and 1 or 0) > #sText and string.sub(sOption, 1, #sText) == sText then
8 | local sResult = string.sub(sOption, #sText + 1)
9 | if bAddSpaces then
10 | table.insert(tResults, sResult .. " ")
11 | else
12 | table.insert(tResults, sResult)
13 | end
14 | end
15 | end
16 | return tResults
17 | end
18 |
19 | _ENV.shell.setCompletionFunction("sys/apps/package.lua",
20 | function(_, index, text)
21 | if index == 1 then
22 | return completeMultipleChoice(text, { "install ", "update ", "uninstall ", "updateall ", "refresh" })
23 | end
24 | end)
25 |
26 | _ENV.shell.setCompletionFunction("sys/apps/inspect.lua",
27 | function(_, index, text)
28 | if index == 1 then
29 | local components = { }
30 | for _, f in pairs(fs.list('sys/modules/opus/ui/components')) do
31 | table.insert(components, (f:gsub("%.lua$", "")))
32 | end
33 | return completeMultipleChoice(text, components)
34 | end
35 | end)
36 |
--------------------------------------------------------------------------------
/sys/autorun/hotkeys.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local kernel = _G.kernel
4 | local keyboard = _G.device.keyboard
5 | local multishell = _ENV.multishell
6 |
7 | if multishell and multishell.getTabs then
8 | -- restart tab
9 | keyboard.addHotkey('control-backspace', function()
10 | local tab = kernel.getFocused()
11 | if tab and not tab.noTerminate then
12 | multishell.terminate(tab.uid)
13 | multishell.openTab(tab.env, {
14 | path = tab.path,
15 | args = tab.args,
16 | focused = true,
17 | })
18 | end
19 | end)
20 | end
21 |
22 | -- next tab
23 | keyboard.addHotkey('control-tab', function()
24 | local visibleTabs = { }
25 | local currentTab = kernel.getFocused()
26 |
27 | local function compareTab(a, b)
28 | return a.uid < b.uid
29 | end
30 | for _,tab in Util.spairs(kernel.routines, compareTab) do
31 | if not tab.hidden and not tab.noFocus then
32 | table.insert(visibleTabs, tab)
33 | end
34 | end
35 |
36 | for k,tab in ipairs(visibleTabs) do
37 | if tab.uid == currentTab.uid then
38 | if k < #visibleTabs then
39 | kernel.raise(visibleTabs[k + 1].uid)
40 | return
41 | end
42 | end
43 | end
44 | if #visibleTabs > 0 then
45 | kernel.raise(visibleTabs[1].uid)
46 | end
47 | end)
48 |
--------------------------------------------------------------------------------
/sys/autorun/log.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | Adds a task and the control-d hotkey to view the kernel log.
3 | --]]
4 |
5 | local kernel = _G.kernel
6 | local keyboard = _G.device.keyboard
7 | local os = _G.os
8 |
9 | local function systemLog()
10 | local routine = kernel.getCurrent()
11 |
12 | kernel.hook('mouse_scroll', function(_, eventData)
13 | local dir, y = eventData[1], eventData[3]
14 |
15 | if y > 1 then
16 | local currentTab = kernel.getFocused()
17 | if currentTab == routine then
18 | if currentTab.terminal.scrollUp then
19 | if dir == -1 then
20 | currentTab.terminal.scrollUp()
21 | else
22 | currentTab.terminal.scrollDown()
23 | end
24 | end
25 | end
26 | end
27 | end)
28 |
29 | keyboard.addHotkey('control-d', function()
30 | local current = kernel.getFocused()
31 | if current.uid ~= routine.uid then
32 | kernel.raise(routine.uid)
33 | elseif kernel.routines[2] then
34 | kernel.raise(kernel.routines[2].uid)
35 | end
36 | end)
37 |
38 | os.pullEventRaw('terminate')
39 | keyboard.removeHotkey('control-d')
40 | end
41 |
42 | kernel.run(_ENV, {
43 | title = 'System Log',
44 | fn = systemLog,
45 | noTerminate = true,
46 | hidden = true,
47 | })
48 |
--------------------------------------------------------------------------------
/sys/autorun/version.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 | local Util = require('opus.util')
3 |
4 | local fs = _G.fs
5 | local shell = _ENV.shell
6 |
7 | local URL = 'https://raw.githubusercontent.com/kepler155c/opus/%s/.opus_version'
8 |
9 | if fs.exists('.opus_version') then
10 | local f = fs.open('.opus_version', 'r')
11 | local date = f.readLine()
12 | f.close()
13 | date = type(date) == 'string' and Util.split(date)[1]
14 |
15 | local today = os.date('%j')
16 | local config = Config.load('version', {
17 | packages = date,
18 | checked = today,
19 | })
20 |
21 | -- check if packages need an update
22 | if date ~= config.packages then
23 | config.packages = date
24 | Config.update('version', config)
25 | print('Updating packages')
26 | shell.run('package updateall')
27 | os.reboot()
28 | end
29 |
30 | if type(date) == 'string' and #date > 0 then
31 | if config.checked ~= today then
32 | config.checked = today
33 | Config.update('version', config)
34 | print('Checking for new version')
35 | pcall(function()
36 | local c = Util.httpGet(string.format(URL, _G.OPUS_BRANCH))
37 | if c then
38 | local lines = Util.split(c)
39 | local revdate = table.remove(lines, 1)
40 | if date ~= revdate and config.skip ~= revdate then
41 | config.current = revdate
42 | config.details = table.concat(lines, '\n')
43 | Config.update('version', config)
44 | print('New version available')
45 | if _ENV.multishell then
46 | shell.openForegroundTab('sys/apps/Version.lua')
47 | end
48 | end
49 | end
50 | end)
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/sys/autorun/welcome.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 | local Util = require('opus.util')
3 |
4 | local fs = _G.fs
5 | local os = _G.os
6 | local shell = _ENV.shell
7 |
8 | local config = Config.load('os')
9 | if not config.welcomed then
10 | config.welcomed = true
11 | config.securityUpdate = true
12 | config.readNotes = 1
13 | Config.update('os', config)
14 | if shell.openForegroundTab then
15 | shell.openForegroundTab('Welcome')
16 | end
17 | end
18 |
19 | if not config.securityUpdate then
20 | config.securityUpdate = true
21 | config.secretKey = nil
22 | config.password = nil
23 | config.readNotes = 1
24 | Config.update('os', config)
25 |
26 | fs.delete('usr/.known_hosts')
27 |
28 | Util.writeFile('sys/notes_1.txt', [[
29 | An important security update has been applied.
30 |
31 | Unfortunately, this update has reset the
32 | password on the system. You can set a new
33 | password in System->System->Password.
34 |
35 | All computers that you connect to will also
36 | need to be updated as well.
37 |
38 | Also, I have changed the location for apis.
39 | This will require you to update all installed
40 | packages. Sorry !
41 |
42 | Thanks for your patience. And... thanks to
43 | Anavrins for the much improved security.
44 | ]])
45 | end
46 |
47 | if fs.exists('sys/notes_1.txt') and shell.openForegroundTab then
48 | shell.openForegroundTab('edit sys/notes_1.txt')
49 | os.sleep(2)
50 | fs.delete('sys/notes_1.txt')
51 | end
52 |
--------------------------------------------------------------------------------
/sys/boot/kiosk.lua:
--------------------------------------------------------------------------------
1 | local os = _G.os
2 | local parallel = _G.parallel
3 | local peripheral = _G.peripheral
4 | local settings = _G.settings
5 | local term = _G.term
6 |
7 | local name = settings.get('kiosk.monitor')
8 |
9 | if not name then
10 | peripheral.find('monitor', function(s)
11 | name = s
12 | end)
13 | end
14 |
15 | local mon = name and peripheral.wrap(name)
16 |
17 | if mon then
18 | print("Opus OS is running in Kiosk mode, and the screen will be redirected to the monitor. To undo this, go to the boot option menu by pressing a key while booting, then select the option 2.")
19 | term.redirect(mon)
20 | mon.setTextScale(tonumber(settings.get('kiosk.textscale')) or 1)
21 |
22 | parallel.waitForAny(
23 | function()
24 | os.run(_ENV, '/sys/boot/opus.lua')
25 | end,
26 |
27 | function()
28 | while true do
29 | local event, side, x, y = os.pullEventRaw('monitor_touch')
30 |
31 | if event == 'monitor_touch' and side == name then
32 | os.queueEvent('mouse_click', 1, x, y)
33 | os.queueEvent('mouse_up', 1, x, y)
34 | end
35 | end
36 | end
37 | )
38 | else
39 | os.run(_ENV, '/sys/boot/opus.lua')
40 | end
41 |
--------------------------------------------------------------------------------
/sys/boot/opus.lua:
--------------------------------------------------------------------------------
1 | local fs = _G.fs
2 |
3 | -- override bios function to use the global scope of the current env
4 | function _G.loadstring(string, chunkname)
5 | return load(string, chunkname, nil, getfenv(2)._G)
6 | end
7 |
8 | -- override bios function to include the actual filename
9 | function _G.loadfile(filename, mode, env)
10 | -- Support the previous `loadfile(filename, env)` form instead.
11 | if type(mode) == "table" and env == nil then
12 | mode, env = nil, mode
13 | end
14 |
15 | local file = fs.open(filename, "r")
16 | if not file then return nil, "File not found" end
17 |
18 | local func, err = load(file.readAll(), '@' .. filename, mode, env)
19 | file.close()
20 | return func, err
21 | end
22 |
23 | local sandboxEnv = setmetatable({ }, { __index = _G })
24 | for k,v in pairs(_ENV) do
25 | sandboxEnv[k] = v
26 | end
27 |
28 | -- Install require shim
29 | _G.requireInjector = loadfile('sys/modules/opus/injector.lua', _ENV)()
30 |
31 | local function run(file, ...)
32 | local env = setmetatable({ }, { __index = _G })
33 | for k,v in pairs(sandboxEnv) do
34 | env[k] = v
35 | end
36 |
37 | _G.requireInjector(env)
38 |
39 | local s, m = loadfile(file, env)
40 | if s then
41 | return s(...)
42 | end
43 | error('Error loading ' .. file .. '\n' .. m)
44 | end
45 |
46 | _G._syslog = function() end
47 | _G.OPUS_BRANCH = 'develop-1.8'
48 |
49 | local s, m = pcall(run, 'sys/apps/shell.lua', 'sys/kernel.lua', ...)
50 |
51 | if not s then
52 | print('\nError loading Opus OS\n')
53 | _G.printError(m .. '\n')
54 | end
55 |
56 | if fs.restore then
57 | fs.restore()
58 | end
59 |
--------------------------------------------------------------------------------
/sys/boot/tlco.lua:
--------------------------------------------------------------------------------
1 | local run = os.run
2 | local shutdown = os.shutdown
3 |
4 | local args = {...} -- keep the args so that they can be passed to opus.lua
5 |
6 | os.run = function()
7 | os.run = run
8 | end
9 |
10 | os.shutdown = function()
11 | os.shutdown = shutdown
12 |
13 | _ENV.multishell = nil -- prevent sys/apps/shell.lua erroring for odd reasons
14 |
15 | local success, err = pcall(function()
16 | run(_ENV, 'sys/boot/opus.lua', table.unpack(args))
17 | end)
18 | term.redirect(term.native())
19 | if success then
20 | print("Opus OS abruptly stopped.")
21 | else
22 | printError("Opus OS errored.")
23 | printError(err)
24 | end
25 | print("Press any key to continue.")
26 | os.pullEvent("key")
27 | shutdown()
28 | end
29 |
30 | shell.exit()
--------------------------------------------------------------------------------
/sys/etc/fstab:
--------------------------------------------------------------------------------
1 | sys/apps/pain.lua urlfs https://github.com/LDDestroier/CC/raw/master/pain.lua
2 | sys/apps/update.lua urlfs http://pastebin.com/raw/UzGHLbNC
3 | sys/apps/Enchat.lua urlfs https://raw.githubusercontent.com/LDDestroier/enchat/master/enchat3.lua
4 | sys/apps/cloud.lua urlfs https://cloud-catcher.squiddev.cc/cloud.lua
5 | rom/modules/main/opus linkfs sys/modules/opus
--------------------------------------------------------------------------------
/sys/help/CloudCatcher.txt:
--------------------------------------------------------------------------------
1 | Cloud Catcher is a web terminal for ComputerCraft, allowing you to interact with any in-game computer in the browser, as well as edit files remotely!
2 |
3 | To get started, visit https://cloud-catcher.squiddev.cc for a session key.
4 |
5 | Within Files, press 'c' to edit a file using Cloud Catcher.
6 |
--------------------------------------------------------------------------------
/sys/help/Networking.txt:
--------------------------------------------------------------------------------
1 | Wireless Networking
2 | ===================
3 | To establish one-way trust between two computers with modems, run the following in a shell prompt:
4 |
5 | On the target computer, run:
6 | > password
7 |
8 | On the source computer, run:
9 | > trust
--------------------------------------------------------------------------------
/sys/help/Opus.txt:
--------------------------------------------------------------------------------
1 | Shortcut Keys
2 | =============
3 | * Control-o: Show the Overview
4 | * Control-tab: Cycle to next tab
5 | * Control-shift-tab: Cycle to previous tab
6 | * Control-d: Show/toggle logging screen
7 | * Control-c: Copy (in most applications)
8 | * Control-shift-v: Paste from internal clipboard
9 | * Control-shift-click: Open the clicked UI element in Lua
10 |
11 | Running Custom Programs
12 | =======================
13 | To create a program that runs at startup, create a file in /usr/autorun to start the program. Example:
14 |
15 | In file: /usr/autorun/startup.lua
16 | shell.openForegroundTab('myprogram')
17 |
18 | There are 3 different ways to open tabs:
19 |
20 | * shell.openTab : background tab
21 | * shell.openForegroundTab : focused tab
22 | * shell.openHiddenTab : appears only in the Tasks application
23 |
24 | Copy / Paste
25 | ============
26 | Opus can paste from both the system clipboard and the internal clipboard.
27 |
28 | * control-v: for normal clipboard
29 | * control-shift-v: for internal clipboard
30 |
--------------------------------------------------------------------------------
/sys/help/Overview.txt:
--------------------------------------------------------------------------------
1 | Overview is the main application launcher.
2 |
3 | Shortcut keys
4 | =============
5 | * s: Shell
6 | * l: Lua application
7 | * f: Files
8 | * e: Edit an application (or right-click)
9 | * n: Network
10 | * control-n: Add a new application
11 | * delete: Delete an application
12 |
13 | Adding a new application
14 | ========================
15 | The run entry can be either a disk file or a URL.
16 | Icons must be in NFT format with a height of 3 and a width of 3 to 8 characters. Magenta is used for transparency.
--------------------------------------------------------------------------------
/sys/help/Packages.txt:
--------------------------------------------------------------------------------
1 | Opus applications are grouped into packages with a common theme.
2 |
3 | To install a package, use either the System -> Packages program or the package command line program.
4 |
5 | Shell usage:
6 |
7 | > package list
8 | > package install
9 | > package update
10 | > package updateall
11 | > package uninstall
12 |
13 | Package definitions are located in usr/config/packages. This file can be modified to add custom packages.
14 |
--------------------------------------------------------------------------------
/sys/help/pastebin.txt:
--------------------------------------------------------------------------------
1 | pastebin is a program for uploading files to and downloading files from pastebin.com. This is useful for sharing programs with other players.
2 | The HTTP API must be enabled in ComputerCraft.cfg to use this program.
3 |
4 | ex:
5 | "pastebin put foo" will upload the file "foo" to pastebin.com, and print the URL.
6 | "pastebin get xq5gc7LB foo" will download the file from the URL http://pastebin.com/xq5gc7LB, and save it as "foo".
7 | "pastebin run CxaWmPrX" will download the file from the URL http://pastebin.com/CxaWmPrX, and immediately run it.
8 |
9 | Functions in the pastebin API:
10 | pastebin.get( code, filepath )
11 | pastebin.put( filepath )
12 | pastebin.download( code )
13 | pastebin.upload( pastename, text )
14 | pastebin.parseCode( code )
15 |
16 |
--------------------------------------------------------------------------------
/sys/init/1.device.lua:
--------------------------------------------------------------------------------
1 | local Peripheral = require('opus.peripheral')
2 |
3 | _G.device = Peripheral.getList()
4 |
5 | _G.device.terminal = _G.kernel.terminal
6 | _G.device.terminal.side = 'terminal'
7 | _G.device.terminal.type = 'terminal'
8 | _G.device.terminal.name = 'terminal'
9 |
10 | _G.device.keyboard = {
11 | side = 'keyboard',
12 | type = 'keyboard',
13 | name = 'keyboard',
14 | hotkeys = { },
15 | state = { },
16 | }
17 |
18 | _G.device.mouse = {
19 | side = 'mouse',
20 | type = 'mouse',
21 | name = 'mouse',
22 | state = { },
23 | }
24 |
25 | local Input = require('opus.input')
26 | local Util = require('opus.util')
27 |
28 | local device = _G.device
29 | local kernel = _G.kernel
30 | local keyboard = _G.device.keyboard
31 | local keys = _G.keys
32 | local mouse = _G.device.mouse
33 | local os = _G.os
34 |
35 | kernel.hook('peripheral', function(_, eventData)
36 | local side = eventData[1]
37 | if side then
38 | local dev = Peripheral.addDevice(device, side)
39 | if dev then
40 | os.queueEvent('device_attach', dev.name)
41 | end
42 | end
43 | end)
44 |
45 | kernel.hook('peripheral_detach', function(_, eventData)
46 | local side = eventData[1]
47 | if side then
48 | for _, dev in pairs(Util.findAll(device, 'side', side)) do
49 | os.queueEvent('device_detach', dev.name)
50 | device[dev.name] = nil
51 | end
52 | end
53 | end)
54 |
55 | local modifiers = Util.transpose {
56 | keys.leftCtrl, keys.rightCtrl,
57 | keys.leftShift, keys.rightShift,
58 | keys.leftAlt, keys.rightAlt,
59 | }
60 |
61 | kernel.hook({ 'key', 'char', 'paste' }, function(event, eventData)
62 | local code = eventData[1]
63 |
64 | -- maintain global keyboard modifier state
65 | if event == 'key' and modifiers[code] then
66 | keyboard.state[code] = true
67 | end
68 |
69 | -- and fire hotkeys
70 | local hotkey = Input:translate(event, eventData[1], eventData[2])
71 |
72 | if hotkey and keyboard.hotkeys[hotkey.code] then
73 | keyboard.hotkeys[hotkey.code](event, eventData)
74 | return true
75 | end
76 | end)
77 |
78 | kernel.hook('key_up', function(_, eventData)
79 | local code = eventData[1]
80 |
81 | if modifiers[code] then
82 | keyboard.state[code] = nil
83 | end
84 | end)
85 |
86 | kernel.hook({ 'mouse_click', 'mouse_up', 'mouse_drag' }, function(event, eventData)
87 | local button = eventData[1]
88 | if event == 'mouse_click' then
89 | mouse.state[button] = true
90 | else
91 | if not mouse.state[button] then
92 | return true -- ensure mouse ups are only generated if a mouse down was sent
93 | end
94 | if event == 'mouse_up' then
95 | mouse.state[button] = nil
96 | end
97 | end
98 | end)
99 |
100 | function keyboard.addHotkey(code, fn)
101 | keyboard.hotkeys[code] = fn
102 | end
103 |
104 | function keyboard.removeHotkey(code)
105 | keyboard.hotkeys[code] = nil
106 | end
107 |
--------------------------------------------------------------------------------
/sys/init/3.relay.lua:
--------------------------------------------------------------------------------
1 | local device = _G.device
2 | local kernel = _G.kernel
3 |
4 | local function register(v)
5 | if v and v.isWireless and v.isAccessPoint and v.getNamesRemote then
6 | v._children = { }
7 | for _, name in pairs(v.getNamesRemote()) do
8 | local dev = v.getMethodsRemote(name)
9 | if dev then
10 | dev.name = name
11 | dev.side = v.side
12 | dev.type = v.getTypeRemote(name)
13 | device[name] = dev
14 | end
15 | end
16 | end
17 | end
18 |
19 | for _,v in pairs(device) do
20 | register(v)
21 | end
22 |
23 | -- register oc devices as peripherals
24 | kernel.hook('device_attach', function(_, eventData)
25 | register(device[eventData[1]])
26 | end)
27 |
--------------------------------------------------------------------------------
/sys/init/3.sys.lua:
--------------------------------------------------------------------------------
1 | local fs = _G.fs
2 | local os = _G.os
3 |
4 | fs.loadTab('sys/etc/fstab')
5 |
6 | -- add some Lua compatibility functions
7 | function os.remove(a)
8 | if fs.exists(a) then
9 | local s = pcall(fs.delete, a)
10 | return s and true or nil, a .. ': Unable to remove file'
11 | end
12 | return nil, a .. ': No such file or directory'
13 | end
14 |
15 | os.execute = function(cmd)
16 | local env = _G.getfenv(2)
17 | if not cmd then
18 | return env.shell and 1 or 0
19 | end
20 |
21 | if not env.shell then
22 | return 0
23 | end
24 |
25 | local s, m = env.shell.run('sys/apps/shell.lua ' .. cmd)
26 |
27 | if not s then
28 | return 1, m
29 | end
30 |
31 | return 0
32 | end
33 |
34 | os.tmpname = function()
35 | local fname
36 | repeat
37 | fname = 'tmp/a' .. math.random(1, 32768)
38 | until not fs.exists(fname)
39 |
40 | return fname
41 | end
42 |
43 | -- non-standard - will raise error instead
44 | os.exit = function(code)
45 | error(code or 0)
46 | end
--------------------------------------------------------------------------------
/sys/init/4.label.lua:
--------------------------------------------------------------------------------
1 | local os = _G.os
2 | local peripheral = _G.peripheral
3 |
4 | -- Default label
5 | if not os.getComputerLabel() then
6 | local id = os.getComputerID()
7 |
8 | if _G.turtle then
9 | os.setComputerLabel('turtle_' .. id)
10 |
11 | elseif _G.pocket then
12 | os.setComputerLabel('pocket_' .. id)
13 |
14 | elseif _G.commands then
15 | os.setComputerLabel('command_' .. id)
16 |
17 | elseif peripheral.find('neuralInterface') then
18 | os.setComputerLabel('neural_' .. id)
19 |
20 | else
21 | os.setComputerLabel('computer_' .. id)
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/sys/init/4.user.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local fs = _G.fs
4 | local shell = _ENV.shell
5 |
6 | if not fs.exists('usr/apps') then
7 | fs.makeDir('usr/apps')
8 | end
9 | if not fs.exists('usr/autorun') then
10 | fs.makeDir('usr/autorun')
11 | end
12 |
13 | -- move the fstab out of config so that the config directory
14 | -- can be remapped to another disk (and for consistency)
15 | if fs.exists('usr/config/fstab') and not fs.exists('usr/etc/fstab') then
16 | fs.move('usr/config/fstab', 'usr/etc/fstab')
17 | end
18 | fs.loadTab('usr/etc/fstab')
19 |
20 | -- TODO: Temporary
21 | local upgrade = Util.readTable('usr/config/shell')
22 | if upgrade and (not upgrade.upgraded or upgrade.upgraded ~= 1) then
23 | fs.delete('usr/config/shell')
24 | end
25 |
26 | if not fs.exists('usr/config/shell') then
27 | Util.writeTable('usr/config/shell', {
28 | aliases = shell.aliases(),
29 | path = '/usr/apps',
30 | lua_path = package.path,
31 | upgraded = 1,
32 | })
33 | end
34 |
35 | local config = Util.readTable('usr/config/shell')
36 | if config.aliases then
37 | for k in pairs(shell.aliases()) do
38 | shell.clearAlias(k)
39 | end
40 | for k,v in pairs(config.aliases) do
41 | shell.setAlias(k, v)
42 | end
43 | end
44 |
45 | local path = config.path and Util.split(config.path, '(.-):') or { }
46 | table.insert(path, '/sys/apps')
47 | for _, v in pairs(Util.split(shell.path(), '(.-):')) do
48 | table.insert(path, v)
49 | end
50 |
51 | shell.setPath(table.concat(path, ':'))
52 |
53 | --_G.LUA_PATH = config.lua_path
54 | --_G.settings.set('mbs.shell.require_path', config.lua_path)
55 |
--------------------------------------------------------------------------------
/sys/init/5.network.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 |
3 | local device = _G.device
4 | local kernel = _G.kernel
5 | local os = _G.os
6 |
7 | do
8 | local config = Config.load('os')
9 | _G.network = setmetatable({ }, { __index = {
10 | getGroup = function() return config.group end,
11 | setGroup = function(name)
12 | config.group = name
13 | end
14 | }})
15 | end
16 |
17 | local function startNetwork()
18 | kernel.run(_ENV, {
19 | title = 'Net daemon',
20 | path = 'sys/apps/netdaemon.lua',
21 | hidden = true,
22 | })
23 | end
24 |
25 | local function setModem(dev)
26 | if not device.wireless_modem and dev.isWireless() then
27 | local config = Config.load('os')
28 |
29 | if not config.wirelessModem or
30 | config.wirelessModem == 'auto' or
31 | dev.name == config.wirelessModem then
32 |
33 | device.wireless_modem = dev
34 | os.queueEvent('device_attach', 'wireless_modem')
35 | return dev
36 | end
37 | end
38 | end
39 |
40 | -- create a psuedo-device named 'wireless_modem'
41 | kernel.hook('device_attach', function(_, eventData)
42 | local dev = device[eventData[1]]
43 | if dev and dev.type == 'modem' then
44 | if setModem(dev) then
45 | startNetwork()
46 | end
47 | end
48 | end)
49 |
50 | kernel.hook('device_detach', function(_, eventData)
51 | if device.wireless_modem and eventData[1] == device.wireless_modem.name then
52 | device['wireless_modem'] = nil
53 | os.queueEvent('device_detach', 'wireless_modem')
54 | end
55 | end)
56 |
57 | for _,dev in pairs(device) do
58 | if dev.type == 'modem' then
59 | if setModem(dev) then
60 | break
61 | end
62 | end
63 | end
64 |
65 | if device.wireless_modem then
66 | print('waiting for network...')
67 | startNetwork()
68 | os.pullEvent('network_up')
69 | end
70 |
--------------------------------------------------------------------------------
/sys/init/5.unpackage.lua:
--------------------------------------------------------------------------------
1 | local LZW = require('opus.compress.lzw')
2 | local Tar = require('opus.compress.tar')
3 | local Util = require('opus.util')
4 |
5 | local fs = _G.fs
6 |
7 | if not fs.exists('packages') or not fs.isDir('packages') then
8 | return
9 | end
10 |
11 | for _, name in pairs(fs.list('packages')) do
12 | local fullName = fs.combine('packages', name)
13 | local packageName = name:match('(.+)%.tar%.lzw$')
14 | if packageName and not fs.isDir(fullName) then
15 | local dir = fs.combine('packages', packageName)
16 | if not fs.exists(dir) then
17 | local s, m = pcall(function()
18 | fs.mount(dir, 'ramfs', 'directory')
19 |
20 | local c = Util.readFile(fullName, 'rb')
21 |
22 | Tar.untar_string(LZW.decompress(c), dir)
23 | end)
24 | if not s then
25 | fs.delete(dir)
26 | print('failed to extract ' .. fullName)
27 | print(m)
28 | end
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/sys/init/6.packages.lua:
--------------------------------------------------------------------------------
1 | local Packages = require('opus.packages')
2 | local Util = require('opus.util')
3 |
4 | local fs = _G.fs
5 | local help = _G.help
6 | local shell = _ENV.shell
7 |
8 | local appPaths = Util.split(shell.path(), '(.-):')
9 | local helpPaths = Util.split(help.path(), '(.-):')
10 |
11 | table.insert(helpPaths, '/sys/help')
12 |
13 | for name in pairs(Packages:installed()) do
14 | local packageDir = fs.combine('packages', name)
15 |
16 | local fstabPath = fs.combine(packageDir, 'etc/fstab')
17 | if fs.exists(fstabPath) then
18 | fs.loadTab(fstabPath)
19 | end
20 |
21 | table.insert(appPaths, 1, '/' .. packageDir)
22 |
23 | local apiPath = fs.combine(packageDir, 'apis') -- TODO: rename dir to 'modules' (someday)
24 | if fs.exists(apiPath) then
25 | fs.mount(fs.combine('rom/modules/main', name), 'linkfs', apiPath)
26 | end
27 |
28 | local helpPath = '/' .. fs.combine(packageDir, 'help')
29 | if fs.exists(helpPath) then
30 | table.insert(helpPaths, helpPath)
31 | end
32 | end
33 |
34 | help.setPath(table.concat(helpPaths, ':'))
35 | shell.setPath(table.concat(appPaths, ':'))
36 |
37 | local function runDir(directory)
38 | local files = fs.list(directory)
39 | table.sort(files)
40 |
41 | for _,file in ipairs(files) do
42 | os.sleep(0)
43 | local result, err = shell.run(directory .. '/' .. file)
44 | if not result and err then
45 | _G.printError('\n' .. err)
46 | end
47 | end
48 | end
49 |
50 | for _, package in pairs(Packages:installedSorted()) do
51 | local packageDir = 'packages/' .. package.name .. '/init'
52 | if fs.exists(packageDir) and fs.isDir(packageDir) then
53 | runDir(packageDir)
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/sys/modules/opus/ansi.lua:
--------------------------------------------------------------------------------
1 | local Ansi = setmetatable({ }, {
2 | __call = function(_, ...)
3 | local str = '\027['
4 | for k,v in ipairs({ ...}) do
5 | if k == 1 then
6 | str = str .. v
7 | else
8 | str = str .. ';' .. v
9 | end
10 | end
11 | return str .. 'm'
12 | end
13 | })
14 |
15 | Ansi.codes = {
16 | reset = 0,
17 | white = 1,
18 | orange = 2,
19 | magenta = 3,
20 | lightBlue = 4,
21 | yellow = 5,
22 | lime = 6,
23 | pink = 7,
24 | gray = 8,
25 | lightGray = 9,
26 | cyan = 10,
27 | purple = 11,
28 | blue = 12,
29 | brown = 13,
30 | green = 14,
31 | red = 15,
32 | black = 16,
33 | onwhite = 21,
34 | onorange = 22,
35 | onmagenta = 23,
36 | onlightBlue = 24,
37 | onyellow = 25,
38 | onlime = 26,
39 | onpink = 27,
40 | ongray = 28,
41 | onlightGray = 29,
42 | oncyan = 30,
43 | onpurple = 31,
44 | onblue = 32,
45 | onbrown = 33,
46 | ongreen = 34,
47 | onred = 35,
48 | onblack = 36,
49 | }
50 |
51 | for k,v in pairs(Ansi.codes) do
52 | Ansi[k] = Ansi(v)
53 | end
54 |
55 | return Ansi
56 |
--------------------------------------------------------------------------------
/sys/modules/opus/array.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local Array = { }
4 |
5 | function Array.filter(it, f)
6 | local ot = { }
7 | for _,v in pairs(it) do
8 | if f(v) then
9 | table.insert(ot, v)
10 | end
11 | end
12 | return ot
13 | end
14 |
15 | function Array.removeByValue(t, e)
16 | for k,v in pairs(t) do
17 | if v == e then
18 | table.remove(t, k)
19 | return e
20 | end
21 | end
22 | end
23 |
24 | Array.find = Util.find
25 |
26 | return Array
27 |
--------------------------------------------------------------------------------
/sys/modules/opus/bulkget.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local parallel = _G.parallel
4 |
5 | local BulkGet = { }
6 |
7 | function BulkGet.download(list, callback)
8 | local t = { }
9 | local failed = false
10 |
11 | for _ = 1, 5 do
12 | table.insert(t, function()
13 | while true do
14 | local entry = table.remove(list)
15 | if not entry then
16 | break
17 | end
18 | local s, m = Util.download(entry.url, entry.path)
19 | if not s then
20 | failed = true
21 | end
22 | callback(entry, s, m)
23 | if failed then
24 | break
25 | end
26 | end
27 | end)
28 | end
29 |
30 | parallel.waitForAll(table.unpack(t))
31 | end
32 |
33 | return BulkGet
34 |
--------------------------------------------------------------------------------
/sys/modules/opus/class.lua:
--------------------------------------------------------------------------------
1 | -- From http://lua-users.org/wiki/SimpleLuaClasses
2 | -- (with some modifications)
3 |
4 | -- class.lua
5 | -- Compatible with Lua 5.1 (not 5.0).
6 | return function(base)
7 | local c = { } -- a new class instance
8 | if type(base) == 'table' then
9 | -- our new class is a shallow copy of the base class!
10 | if base._preload then
11 | base = base._preload(base)
12 | end
13 | for i,v in pairs(base) do
14 | c[i] = v
15 | end
16 | c._base = base
17 | end
18 | -- the class will be the metatable for all its objects,
19 | -- and they will look up their methods in it.
20 | c.__index = c
21 |
22 | -- expose a constructor which can be called by ()
23 | setmetatable(c, {
24 | __call = function(class_tbl, ...)
25 | local obj = { }
26 | setmetatable(obj,c)
27 | if class_tbl.init then
28 | class_tbl.init(obj, ...)
29 | else
30 | -- make sure that any stuff from the base class is initialized!
31 | if base and base.init then
32 | base.init(obj, ...)
33 | end
34 | end
35 | return obj
36 | end
37 | })
38 |
39 | c.is_a =
40 | function(self, klass)
41 | local m = getmetatable(self)
42 | while m do
43 | if m == klass then return true end
44 | m = m._base
45 | end
46 | return false
47 | end
48 | return c
49 | end
50 |
--------------------------------------------------------------------------------
/sys/modules/opus/compress/lzw.lua:
--------------------------------------------------------------------------------
1 | -- see: https://github.com/Rochet2/lualzw
2 | -- MIT License - Copyright (c) 2016 Rochet2
3 |
4 | local char = string.char
5 | local type = type
6 | local sub = string.sub
7 | local tconcat = table.concat
8 |
9 | local SIGC = 'LZWC'
10 |
11 | local basedictcompress = {}
12 | local basedictdecompress = {}
13 | for i = 0, 255 do
14 | local ic, iic = char(i), char(i, 0)
15 | basedictcompress[ic] = iic
16 | basedictdecompress[iic] = ic
17 | end
18 |
19 | local function dictAddA(str, dict, a, b)
20 | if a >= 256 then
21 | a, b = 0, b+1
22 | if b >= 256 then
23 | dict = {}
24 | b = 1
25 | end
26 | end
27 | dict[str] = char(a,b)
28 | a = a+1
29 | return dict, a, b
30 | end
31 |
32 | local function compress(input)
33 | if type(input) ~= "string" then
34 | error ("string expected, got "..type(input))
35 | end
36 | local len = #input
37 | if len <= 1 then
38 | return input
39 | end
40 |
41 | local dict = {}
42 | local a, b = 0, 1
43 |
44 | local result = { SIGC }
45 | local resultlen = 1
46 | local n = 2
47 | local word = ""
48 | for i = 1, len do
49 | local c = sub(input, i, i)
50 | local wc = word..c
51 | if not (basedictcompress[wc] or dict[wc]) then
52 | local write = basedictcompress[word] or dict[word]
53 | if not write then
54 | error "algorithm error, could not fetch word"
55 | end
56 | result[n] = write
57 | resultlen = resultlen + #write
58 | n = n+1
59 | if len <= resultlen then
60 | return input
61 | end
62 | dict, a, b = dictAddA(wc, dict, a, b)
63 | word = c
64 | else
65 | word = wc
66 | end
67 | end
68 | result[n] = basedictcompress[word] or dict[word]
69 | resultlen = resultlen+#result[n]
70 | if len <= resultlen then
71 | return input
72 | end
73 | return tconcat(result)
74 | end
75 |
76 | local function dictAddB(str, dict, a, b)
77 | if a >= 256 then
78 | a, b = 0, b+1
79 | if b >= 256 then
80 | dict = {}
81 | b = 1
82 | end
83 | end
84 | dict[char(a,b)] = str
85 | a = a+1
86 | return dict, a, b
87 | end
88 |
89 | local function decompress(input)
90 | if type(input) ~= "string" then
91 | error( "string expected, got "..type(input))
92 | end
93 |
94 | if #input < 4 then
95 | return input
96 | end
97 |
98 | local control = sub(input, 1, 4)
99 | if control ~= SIGC then
100 | return input
101 | end
102 | input = sub(input, 5)
103 | local len = #input
104 |
105 | if len < 2 then
106 | error("invalid input - not a compressed string")
107 | end
108 |
109 | local dict = {}
110 | local a, b = 0, 1
111 |
112 | local result = {}
113 | local n = 1
114 | local last = sub(input, 1, 2)
115 | result[n] = basedictdecompress[last] or dict[last]
116 | n = n+1
117 | for i = 3, len, 2 do
118 | local code = sub(input, i, i+1)
119 | local lastStr = basedictdecompress[last] or dict[last]
120 | if not lastStr then
121 | error( "could not find last from dict. Invalid input?")
122 | end
123 | local toAdd = basedictdecompress[code] or dict[code]
124 | if toAdd then
125 | result[n] = toAdd
126 | n = n+1
127 | dict, a, b = dictAddB(lastStr..sub(toAdd, 1, 1), dict, a, b)
128 | else
129 | local tmp = lastStr..sub(lastStr, 1, 1)
130 | result[n] = tmp
131 | n = n+1
132 | dict, a, b = dictAddB(tmp, dict, a, b)
133 | end
134 | last = code
135 | end
136 | return tconcat(result)
137 | end
138 |
139 | return {
140 | compress = compress,
141 | decompress = decompress,
142 | }
143 |
--------------------------------------------------------------------------------
/sys/modules/opus/config.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local fs = _G.fs
4 |
5 | local Config = { }
6 |
7 | function Config.load(fname, data)
8 | local filename = 'usr/config/' .. fname
9 | data = data or { }
10 |
11 | if not fs.exists('usr/config') then
12 | fs.makeDir('usr/config')
13 | end
14 |
15 | if not fs.exists(filename) then
16 | Util.writeTable(filename, data)
17 | else
18 | local contents = Util.readTable(filename) or
19 | error('Configuration file is corrupt:' .. filename)
20 |
21 | Util.merge(data, contents)
22 | end
23 |
24 | return data
25 | end
26 |
27 | function Config.update(fname, data)
28 | local filename = 'usr/config/' .. fname
29 | Util.writeTable(filename, data)
30 | end
31 |
32 | return Config
33 |
--------------------------------------------------------------------------------
/sys/modules/opus/crypto/ecc/init.lua:
--------------------------------------------------------------------------------
1 | local fq = require('opus.crypto.ecc.fq')
2 | local elliptic = require('opus.crypto.ecc.elliptic')
3 | local sha256 = require('opus.crypto.sha2')
4 | local Util = require('opus.util')
5 |
6 |
7 | local os = _G.os
8 | local unpack = table.unpack
9 | local mt = Util.byteArrayMT
10 |
11 | local q = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532}
12 |
13 | local sLen = 24
14 | local eLen = 24
15 |
16 | local function hashModQ(sk)
17 | local hash = sha256.hmac({0x00}, sk)
18 | local x
19 | repeat
20 | hash = sha256.digest(hash)
21 | x = fq.fromBytes(hash)
22 | until fq.cmp(x, q) <= 0
23 |
24 | return x
25 | end
26 |
27 | local function publicKey(sk)
28 | local x = hashModQ(sk)
29 |
30 | local Y = elliptic.scalarMulG(x)
31 | local pk = elliptic.pointEncode(Y)
32 |
33 | return setmetatable(pk, mt)
34 | end
35 |
36 | local function exchange(sk, pk)
37 | local Y = elliptic.pointDecode(pk)
38 | local x = hashModQ(sk)
39 |
40 | local Z = elliptic.scalarMul(x, Y)
41 | Z = elliptic.pointScale(Z)
42 |
43 | local ss = fq.bytes(Z[2])
44 | return sha256.digest(ss)
45 | end
46 |
47 | local function sign(sk, message)
48 | message = type(message) == "table" and string.char(unpack(message)) or message
49 | sk = type(sk) == "table" and string.char(unpack(sk)) or sk
50 | local epoch = tostring(os.epoch("utc"))
51 | local x = hashModQ(sk)
52 | local k = hashModQ(message .. epoch .. sk)
53 |
54 | local R = elliptic.scalarMulG(k)
55 | R = string.char(unpack(elliptic.pointEncode(R)))
56 | local e = hashModQ(R .. message)
57 | local s = fq.sub(k, fq.mul(x, e))
58 |
59 | e = fq.bytes(e)
60 | s = fq.bytes(s)
61 |
62 | local sig = {unpack(e)}
63 |
64 | for i = 1, #s do
65 | sig[#sig + 1] = s[i]
66 | end
67 |
68 | return setmetatable(sig, mt)
69 | end
70 |
71 | local function verify(pk, message, sig)
72 | local Y = elliptic.pointDecode(pk)
73 | local e = {unpack(sig, 1, eLen)}
74 | local s = {unpack(sig, eLen + 1, eLen + sLen)}
75 |
76 | e = fq.fromBytes(e)
77 | s = fq.fromBytes(s)
78 |
79 | local R = elliptic.pointAdd(elliptic.scalarMulG(s), elliptic.scalarMul(e, Y))
80 | R = string.char(unpack(elliptic.pointEncode(R)))
81 | local e2 = hashModQ(R .. message)
82 |
83 | return fq.eq(e2, e)
84 | end
85 |
86 | return {
87 | publicKey = publicKey,
88 | exchange = exchange,
89 | sign = sign,
90 | verify = verify,
91 | }
92 |
--------------------------------------------------------------------------------
/sys/modules/opus/fs/gitfs.lua:
--------------------------------------------------------------------------------
1 | local git = require('opus.git')
2 |
3 | local fs = _G.fs
4 |
5 | local gitfs = { }
6 |
7 | function gitfs.mount(dir, repo)
8 | if not repo then
9 | error('gitfs syntax: repo')
10 | end
11 |
12 | local list = git.list(repo)
13 | for path, entry in pairs(list) do
14 | if not fs.exists(fs.combine(dir, path)) then
15 | local node = fs.mount(fs.combine(dir, path), 'urlfs', entry.url)
16 | node.size = entry.size
17 | end
18 | end
19 | end
20 |
21 | return gitfs
22 |
--------------------------------------------------------------------------------
/sys/modules/opus/fs/linkfs.lua:
--------------------------------------------------------------------------------
1 | local fs = _G.fs
2 |
3 | local linkfs = { }
4 |
5 | -- TODO: implement broken links
6 |
7 | local methods = { 'exists', 'getFreeSpace', 'getSize', 'attributes',
8 | 'isDir', 'isReadOnly', 'list', 'makeDir', 'open', 'getDrive' }
9 |
10 | for _,m in pairs(methods) do
11 | linkfs[m] = function(node, dir, ...)
12 | dir = linkfs.resolve(node, dir)
13 | return fs[m](dir, ...)
14 | end
15 | end
16 |
17 | function linkfs.resolve(node, dir)
18 | return dir:gsub(node.mountPoint, node.source, 1)
19 | end
20 |
21 | function linkfs.mount(path, source)
22 | if not source then
23 | error('Source is required')
24 | end
25 | source = fs.combine(source, '')
26 | if not fs.exists(source) then
27 | error('Source is missing')
28 | end
29 | if path == source then
30 | return
31 | end
32 | if fs.isDir(source) then
33 | return {
34 | source = source,
35 | nodes = { },
36 | }
37 | end
38 | return {
39 | source = source
40 | }
41 | end
42 |
43 | function linkfs.copy(node, s, t)
44 | s = s:gsub(node.mountPoint, node.source, 1)
45 | t = t:gsub(node.mountPoint, node.source, 1)
46 | return fs.copy(s, t)
47 | end
48 |
49 | function linkfs.delete(node, dir)
50 | if dir == node.mountPoint then
51 | fs.unmount(node.mountPoint)
52 | else
53 | dir = dir:gsub(node.mountPoint, node.source, 1)
54 | return fs.delete(dir)
55 | end
56 | end
57 |
58 | function linkfs.find(node, spec)
59 | spec = spec:gsub(node.mountPoint, node.source, 1)
60 |
61 | local list = fs.find(spec)
62 | for k,f in ipairs(list) do
63 | list[k] = f:gsub(node.source, node.mountPoint, 1)
64 | end
65 |
66 | return list
67 | end
68 |
69 | function linkfs.move(node, s, t)
70 | s = s:gsub(node.mountPoint, node.source, 1)
71 | t = t:gsub(node.mountPoint, node.source, 1)
72 | return fs.move(s, t)
73 | end
74 |
75 | return linkfs
--------------------------------------------------------------------------------
/sys/modules/opus/fs/netfs.lua:
--------------------------------------------------------------------------------
1 | local Socket = require('opus.socket')
2 | local synchronized = require('opus.sync').sync
3 |
4 | local fs = _G.fs
5 |
6 | local netfs = { }
7 |
8 | local function remoteCommand(node, msg)
9 | for _ = 1, 2 do
10 | if not node.socket then
11 | node.socket = Socket.connect(node.id, 139)
12 | end
13 |
14 | if not node.socket then
15 | error('netfs: Unable to establish connection to ' .. node.id)
16 | fs.unmount(node.mountPoint)
17 | return
18 | end
19 |
20 | local ret
21 | synchronized(node.socket, function()
22 | node.socket:write(msg)
23 | ret = node.socket:read(1)
24 | end)
25 |
26 | if ret then
27 | return ret.response
28 | end
29 | node.socket:close()
30 | node.socket = nil
31 | end
32 | error('netfs: Connection failed', 2)
33 | end
34 |
35 | local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx', 'attributes' }
36 |
37 | local function resolve(node, dir)
38 | -- TODO: Wrong ! (does not support names with dashes)
39 | dir = dir:gsub(node.mountPoint, '', 1)
40 | return fs.combine(node.source, dir)
41 | end
42 |
43 | for _,m in pairs(methods) do
44 | netfs[m] = function(node, dir)
45 | dir = resolve(node, dir)
46 |
47 | return remoteCommand(node, {
48 | fn = m,
49 | args = { dir },
50 | })
51 | end
52 | end
53 |
54 | function netfs.mount(_, id, source)
55 | if not id or not tonumber(id) then
56 | error('ramfs syntax: computerId [directory]')
57 | end
58 | return {
59 | id = tonumber(id),
60 | nodes = { },
61 | source = source or '',
62 | }
63 | end
64 |
65 | function netfs.getDrive()
66 | return 'net'
67 | end
68 |
69 | function netfs.complete(node, partial, dir, includeFiles, includeSlash)
70 | dir = resolve(node, dir)
71 |
72 | return remoteCommand(node, {
73 | fn = 'complete',
74 | args = { partial, dir, includeFiles, includeSlash },
75 | })
76 | end
77 |
78 | function netfs.copy(node, s, t)
79 | s = resolve(node, s)
80 | t = resolve(node, t)
81 |
82 | return remoteCommand(node, {
83 | fn = 'copy',
84 | args = { s, t },
85 | })
86 | end
87 |
88 | function netfs.isDir(node, dir)
89 | if dir == node.mountPoint and node.source == '' then
90 | return true
91 | end
92 | return remoteCommand(node, {
93 | fn = 'isDir',
94 | args = { resolve(node, dir) },
95 | })
96 | end
97 |
98 | function netfs.isReadOnly(node, dir)
99 | if dir == node.mountPoint and node.source == '' then
100 | return false
101 | end
102 | return remoteCommand(node, {
103 | fn = 'isReadOnly',
104 | args = { resolve(node, dir) },
105 | })
106 | end
107 |
108 | function netfs.getSize(node, dir)
109 | if dir == node.mountPoint and node.source == '' then
110 | return 0
111 | end
112 | return remoteCommand(node, {
113 | fn = 'getSize',
114 | args = { resolve(node, dir) },
115 | })
116 | end
117 |
118 | function netfs.find(node, spec)
119 | spec = resolve(node, spec)
120 | local list = remoteCommand(node, {
121 | fn = 'find',
122 | args = { spec },
123 | })
124 |
125 | for k,f in ipairs(list) do
126 | list[k] = fs.combine(node.mountPoint, f)
127 | end
128 |
129 | return list
130 | end
131 |
132 | function netfs.move(node, s, t)
133 | s = resolve(node, s)
134 | t = resolve(node, t)
135 |
136 | return remoteCommand(node, {
137 | fn = 'move',
138 | args = { s, t },
139 | })
140 | end
141 |
142 | function netfs.open(node, fn, fl)
143 | fn = resolve(node, fn)
144 |
145 | local vfh = remoteCommand(node, {
146 | fn = 'open',
147 | args = { fn, fl },
148 | })
149 |
150 | if vfh then
151 | vfh.node = node
152 | for _,m in ipairs(vfh.methods) do
153 | vfh[m] = function(...)
154 | return remoteCommand(node, {
155 | fn = 'fileOp',
156 | args = { vfh.fileUid, m, ... },
157 | })
158 | end
159 | end
160 | end
161 |
162 | return vfh
163 | end
164 |
165 | return netfs
166 |
--------------------------------------------------------------------------------
/sys/modules/opus/fs/urlfs.lua:
--------------------------------------------------------------------------------
1 | --local rttp = require('rttp')
2 | local Util = require('opus.util')
3 |
4 | local fs = _G.fs
5 |
6 | local urlfs = { }
7 |
8 | function urlfs.mount(path, url, force)
9 | if not url then
10 | error('URL is required')
11 | end
12 |
13 | -- only mount if the file does not exist already
14 | if not fs.exists(path) or force then
15 | return {
16 | url = url,
17 | created = os.epoch('utc'),
18 | modification = os.epoch('utc'),
19 | }
20 | end
21 | end
22 |
23 | function urlfs.attributes(node, path)
24 | return path == node.mountPoint and {
25 | created = node.created,
26 | isDir = false,
27 | modification = node.modification,
28 | size = node.size or 0,
29 | }
30 | end
31 |
32 | function urlfs.delete(node, path)
33 | if path == node.mountPoint then
34 | fs.unmount(path)
35 | end
36 | end
37 |
38 | function urlfs.exists(node, path)
39 | return path == node.mountPoint
40 | end
41 |
42 | function urlfs.getSize(node, path)
43 | return path == node.mountPoint and node.size or 0
44 | end
45 |
46 | function urlfs.isReadOnly()
47 | return false
48 | end
49 |
50 | function urlfs.isDir()
51 | return false
52 | end
53 |
54 | function urlfs.getDrive()
55 | return 'url'
56 | end
57 |
58 | function urlfs.open(node, fn, fl)
59 | if fl == 'w' or fl == 'wb' then
60 | fs.delete(fn)
61 | return fs.open(fn, fl)
62 | end
63 |
64 | if fl ~= 'r' and fl ~= 'rb' then
65 | error('Unsupported mode')
66 | end
67 |
68 | local c = node.cache
69 | if not c then
70 | c = Util.httpGet(node.url)
71 | if c then
72 | node.cache = c
73 | node.size = #c
74 | end
75 | end
76 |
77 | if not c then
78 | return
79 | end
80 |
81 | local ctr = 0
82 | local lines
83 |
84 | if fl == 'r' then
85 | return {
86 | read = function()
87 | ctr = ctr + 1
88 | return c:sub(ctr, ctr)
89 | end,
90 | readLine = function()
91 | if not lines then
92 | lines = Util.split(c)
93 | end
94 | ctr = ctr + 1
95 | return lines[ctr]
96 | end,
97 | readAll = function()
98 | return c
99 | end,
100 | close = function()
101 | lines = nil
102 | end,
103 | }
104 | end
105 | return {
106 | readAll = function()
107 | return c
108 | end,
109 | read = function()
110 | ctr = ctr + 1
111 | return c:sub(ctr, ctr):byte()
112 | end,
113 | close = function()
114 | ctr = 0
115 | end,
116 | }
117 | end
118 |
119 | return urlfs
120 |
--------------------------------------------------------------------------------
/sys/modules/opus/fuzzy.lua:
--------------------------------------------------------------------------------
1 | local find = string.find
2 | local floor = math.floor
3 | local min = math.min
4 | local max = math.max
5 | local sub = string.sub
6 |
7 | -- https://rosettacode.org/wiki/Jaro_distance (ported to lua)
8 | return function(s1, s2)
9 | local l1, l2 = #s1, #s2;
10 | if l1 == 0 then
11 | return l2 == 0 and 1.0 or 0.0
12 | end
13 |
14 | local match_distance = max(floor(max(l1, l2) / 2) - 1, 0)
15 | local s1_matches = { }
16 | local s2_matches = { }
17 | local matches = 0
18 |
19 | for i = 1, l1 do
20 | local _end = min(i + match_distance + 1, l2)
21 | for k = max(1, i - match_distance), _end do
22 | if not s2_matches[k] and sub(s1, i, i) == sub(s2, k, k) then
23 | s1_matches[i] = true
24 | s2_matches[k] = true
25 | matches = matches + 1
26 | break
27 | end
28 | end
29 | end
30 | if matches == 0 then
31 | return 0.0
32 | end
33 |
34 | local t = 0.0
35 | local k = 1
36 | for i = 1, l1 do
37 | if s1_matches[i] then
38 | while not s2_matches[k] do
39 | k = k + 1
40 | end
41 | if sub(s1, i, i) ~= sub(s2, k, k) then
42 | t = t + 0.5
43 | end
44 | k = k + 1
45 | end
46 | end
47 |
48 | -- provide a major boost for exact matches
49 | local b = 0.0
50 | if find(s1, s2, 1, true) then
51 | b = b + .5
52 | end
53 |
54 | local m = matches
55 | return (m / l1 + m / l2 + (m - t) / m) / 3.0 + b
56 | end
57 |
--------------------------------------------------------------------------------
/sys/modules/opus/git.lua:
--------------------------------------------------------------------------------
1 | local json = require('opus.json')
2 | local Util = require('opus.util')
3 |
4 | local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1'
5 | local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s'
6 | local TREE_HEADERS = {}
7 | local git = { }
8 |
9 | if _G._GIT_API_KEY then
10 | TREE_HEADERS.Authorization = 'token ' .. _G._GIT_API_KEY
11 | end
12 |
13 | function git.list(repository)
14 | local t = Util.split(repository, '(.-)/')
15 |
16 | local user = table.remove(t, 1)
17 | local repo = table.remove(t, 1)
18 | local branch = table.remove(t, 1) or 'master'
19 | local path
20 |
21 | if not Util.empty(t) then
22 | path = table.concat(t, '/') .. '/'
23 | end
24 |
25 | local function getContents()
26 | local dataUrl = string.format(TREE_URL, user, repo, branch)
27 | local contents, msg = Util.httpGet(dataUrl, TREE_HEADERS)
28 | if not contents then
29 | error(string.format('Failed to download %s\n%s', dataUrl, msg), 2)
30 | else
31 | return json.decode(contents)
32 | end
33 | end
34 |
35 | local data = getContents() or error('Invalid repository')
36 |
37 | if data.message and data.message:find("API rate limit exceeded") then
38 | error("Out of API calls, try again later")
39 | end
40 |
41 | if data.message and data.message == "Not found" then
42 | error("Invalid repository")
43 | end
44 |
45 | local list = { }
46 | for _,v in pairs(data.tree) do
47 | if v.type == "blob" then
48 | v.path = v.path:gsub("%s","%%20")
49 | if not path then
50 | list[v.path] = {
51 | url = string.format(FILE_URL, user, repo, branch, v.path),
52 | size = v.size,
53 | }
54 | elseif Util.startsWith(v.path, path) then
55 | local p = string.sub(v.path, #path)
56 | list[p] = {
57 | url = string.format(FILE_URL, user, repo, branch, path .. p),
58 | size = v.size,
59 | }
60 | end
61 | end
62 | end
63 |
64 | return list
65 | end
66 |
67 | return git
68 |
--------------------------------------------------------------------------------
/sys/modules/opus/history.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local History = { }
4 | local History_mt = { __index = History }
5 |
6 | function History.load(filename, limit)
7 |
8 | local self = setmetatable({
9 | limit = limit,
10 | filename = filename,
11 | }, History_mt)
12 |
13 | self.entries = Util.readLines(filename) or { }
14 | self.pos = #self.entries + 1
15 |
16 | return self
17 | end
18 |
19 | function History:add(line)
20 | if line ~= self.entries[#self.entries] then
21 | table.insert(self.entries, line)
22 | if self.limit then
23 | while #self.entries > self.limit do
24 | table.remove(self.entries, 1)
25 | end
26 | end
27 | Util.writeLines(self.filename, self.entries)
28 | self.pos = #self.entries + 1
29 | end
30 | end
31 |
32 | function History:reset()
33 | self.pos = #self.entries + 1
34 | end
35 |
36 | function History:back()
37 | if self.pos > 1 then
38 | self.pos = self.pos - 1
39 | return self.entries[self.pos]
40 | end
41 | end
42 |
43 | function History:forward()
44 | if self.pos <= #self.entries then
45 | self.pos = self.pos + 1
46 | return self.entries[self.pos]
47 | end
48 | end
49 |
50 | return History
51 |
--------------------------------------------------------------------------------
/sys/modules/opus/http/pastebin.lua:
--------------------------------------------------------------------------------
1 | --- Parse the pastebin code from the given code or URL
2 | local function parseCode(paste)
3 | local patterns = {
4 | "^([%a%d]+)$",
5 | "^https?://pastebin.com/([%a%d]+)$",
6 | "^pastebin.com/([%a%d]+)$",
7 | "^https?://pastebin.com/raw/([%a%d]+)$",
8 | "^pastebin.com/raw/([%a%d]+)$",
9 | }
10 |
11 | for i = 1, #patterns do
12 | local code = paste:match(patterns[i])
13 | if code then
14 | return code
15 | end
16 | end
17 |
18 | return nil
19 | end
20 |
21 | -- Download the contents of a paste
22 | local function download(code)
23 | if type(code) ~= "string" then
24 | error("bad argument #1 (expected string, got " .. type(code) .. ")", 2)
25 | end
26 |
27 | if not http then
28 | return false, "Pastebin requires http API"
29 | end
30 |
31 | -- Add a cache buster so that spam protection is re-checked
32 | local cacheBuster = ("%x"):format(math.random(0, 2 ^ 30))
33 | local response, err = http.get(
34 | "https://pastebin.com/raw/" .. textutils.urlEncode(code) .. "?cb=" .. cacheBuster
35 | )
36 |
37 | if not response then
38 | return response, err
39 | end
40 |
41 | -- If spam protection is activated, we get redirected to /paste with Content-Type: text/html
42 | local headers = response.getResponseHeaders()
43 | if not headers["Content-Type"] or not headers["Content-Type"]:find("^text/plain") then
44 | return false, "Pastebin blocked due to spam protection"
45 | end
46 |
47 | local contents = response.readAll()
48 | response.close()
49 | return contents
50 | end
51 |
52 | -- Upload text to pastebin
53 | local function upload(name, text)
54 | if not http then
55 | return false, "Pastebin requires http API"
56 | end
57 |
58 | -- POST the contents to pastebin
59 | local key = "0ec2eb25b6166c0c27a394ae118ad829"
60 | local response = http.post(
61 | "https://pastebin.com/api/api_post.php",
62 | "api_option=paste&" ..
63 | "api_dev_key=" .. key .. "&" ..
64 | "api_paste_format=lua&" ..
65 | "api_paste_name=" .. textutils.urlEncode(name) .. "&" ..
66 | "api_paste_code=" .. textutils.urlEncode(text)
67 | )
68 |
69 | if not response then
70 | return false, "Failed."
71 | end
72 |
73 | local contents = response.readAll()
74 | response.close()
75 |
76 | return string.match(contents, "[^/]+$")
77 | end
78 |
79 | -- Download the contents to a file from pastebin
80 | local function get(code, path)
81 | if type(code) ~= "string" then
82 | error( "bad argument #1 (expected string, got " .. type(code) .. ")", 2)
83 | end
84 |
85 | if type(path) ~= "string" then
86 | error("bad argument #2 (expected string, got " .. type(path) .. ")", 2)
87 | end
88 |
89 | local res, msg = download(code)
90 | if not res then
91 | return res, msg
92 | end
93 |
94 | local file = fs.open(path, "w")
95 | file.write(res)
96 | file.close()
97 |
98 | return true
99 | end
100 |
101 | -- Upload a file to pastebin.com
102 | local function put(path)
103 | if type(path) ~= "string" then
104 | error("bad argument #1 (expected string, got " .. type(path) .. ")", 2)
105 | end
106 |
107 | -- Determine file to upload
108 | if not fs.exists(path) or fs.isDir(path) then
109 | return false, "No such file"
110 | end
111 |
112 | -- Read in the file
113 | local name = fs.getName(path)
114 | local file = fs.open(path, "r")
115 | local text = file.readAll()
116 | file.close()
117 |
118 | return upload(name, text)
119 | end
120 |
121 | return {
122 | download = download,
123 | upload = upload,
124 | get = get,
125 | put = put,
126 | parseCode = parseCode,
127 | }
128 |
129 |
--------------------------------------------------------------------------------
/sys/modules/opus/injector.lua:
--------------------------------------------------------------------------------
1 | -- https://www.lua.org/manual/5.1/manual.html#pdf-require
2 | -- https://github.com/LuaDist/lua/blob/d2e7e7d4d43ff9068b279a617c5b2ca2c2771676/src/loadlib.c
3 |
4 | local defaultPath = { }
5 |
6 | do
7 | local function split(str)
8 | local t = { }
9 | local function helper(line) table.insert(t, line) return "" end
10 | helper((str:gsub('(.-);', helper)))
11 | return t
12 | end
13 |
14 | local function insert(p)
15 | for _,v in pairs(defaultPath) do
16 | if v == p then
17 | return
18 | end
19 | end
20 | table.insert(defaultPath, p)
21 |
22 | end
23 |
24 | local paths = '?.lua;?/init.lua;'
25 | paths = paths .. '/usr/modules/?.lua;/usr/modules/?/init.lua;'
26 | paths = paths .. '/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua;'
27 | paths = paths .. '/sys/modules/?.lua;/sys/modules/?/init.lua'
28 |
29 | for _,v in pairs(split(paths)) do
30 | insert(v)
31 | end
32 |
33 | local luaPaths = package and package.path and split(package.path) or { }
34 | for _,v in pairs(luaPaths) do
35 | if v ~= '?' then
36 | insert(v)
37 | end
38 | end
39 | end
40 |
41 | local DEFAULT_PATH = table.concat(defaultPath, ';')
42 |
43 | local fs = _G.fs
44 | local os = _G.os
45 | local string = _G.string
46 |
47 | -- Add require and package to the environment
48 | return function(env, programDir)
49 | local function preloadSearcher(modname)
50 | if env.package.preload[modname] then
51 | return function()
52 | return env.package.preload[modname](modname, env)
53 | end
54 | end
55 | end
56 |
57 | local function pathSearcher(modname)
58 | local fname = modname:gsub('%.', '/')
59 |
60 | for pattern in string.gmatch(env.package.path, "[^;]+") do
61 | local sPath = string.gsub(pattern, "%?", fname)
62 |
63 | if programDir and sPath:sub(1, 1) ~= "/" then
64 | sPath = fs.combine(programDir, sPath)
65 | end
66 | if fs.exists(sPath) and not fs.isDir(sPath) then
67 | return loadfile(fs.combine(sPath, ''), env)
68 | end
69 | end
70 | end
71 |
72 | -- place package and require function into env
73 | env.package = {
74 | path = env.LUA_PATH or _G.LUA_PATH or DEFAULT_PATH,
75 | cpath = '',
76 | config = '/\n:\n?\n!\n-',
77 | preload = { },
78 | loaded = {
79 | bit32 = bit32,
80 | coroutine = coroutine,
81 | _G = env._G,
82 | io = io,
83 | math = math,
84 | os = os,
85 | string = string,
86 | table = table,
87 | debug = debug,
88 | utf8 = utf8,
89 | },
90 | loaders = {
91 | preloadSearcher,
92 | pathSearcher,
93 | }
94 | }
95 | env.package.loaded.package = env.package
96 |
97 | local sentinel = { }
98 |
99 | function env.require(modname)
100 | if env.package.loaded[modname] then
101 | if env.package.loaded[modname] == sentinel then
102 | error("loop or previous error loading module '" .. modname .. "'", 0)
103 | end
104 | return env.package.loaded[modname]
105 | end
106 |
107 | local t = { }
108 | for _,searcher in ipairs(env.package.loaders) do
109 | local fn, msg = searcher(modname)
110 | if type(fn) == 'function' then
111 | env.package.loaded[modname] = sentinel
112 |
113 | local module = fn(modname, env) or true
114 |
115 | env.package.loaded[modname] = module
116 | return module
117 | end
118 | if msg then
119 | table.insert(t, msg)
120 | end
121 | end
122 |
123 | if #t > 0 then
124 | error(table.concat(t, '\n'), 2)
125 | end
126 | error('Unable to find module ' .. modname, 2)
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/sys/modules/opus/map.lua:
--------------------------------------------------------------------------------
1 | -- convience functions for tables with key/value pairs
2 | local Util = require('opus.util')
3 |
4 | local Map = { }
5 |
6 | -- TODO: refactor
7 | Map.merge = Util.merge
8 | Map.shallowCopy = Util.shallowCopy
9 | Map.find = Util.find
10 | Map.filter = Util.filter
11 | Map.transpose = Util.transpose
12 |
13 | function Map.removeMatches(t, values)
14 | local function matchAll(entry)
15 | for k, v in pairs(values) do
16 | if entry[k] ~= v then
17 | return
18 | end
19 | end
20 | return true
21 | end
22 |
23 | for _, key in pairs(Util.keys(t)) do
24 | if matchAll(t[key]) then
25 | t[key] = nil
26 | end
27 | end
28 | end
29 |
30 | -- remove table entries if passed function returns false
31 | function Map.prune(t, fn)
32 | for _,k in pairs(Util.keys(t)) do
33 | local v = t[k]
34 | if type(v) == 'table' then
35 | t[k] = Map.prune(v, fn)
36 | end
37 | if not fn(t[k]) then
38 | t[k] = nil
39 | end
40 | end
41 | return t
42 | end
43 |
44 | function Map.size(list)
45 | local length = 0
46 | for _ in pairs(list) do
47 | length = length + 1
48 | end
49 | return length
50 | end
51 |
52 | return Map
53 |
--------------------------------------------------------------------------------
/sys/modules/opus/nft.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local colors = _G.colors
4 |
5 | local NFT = { }
6 |
7 | -- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/
8 |
9 | local hexToColor = { }
10 | for n = 1, 16 do
11 | hexToColor[string.sub("0123456789abcdef", n, n)] = 2 ^ (n - 1)
12 | end
13 | local colorToHex = Util.transpose(hexToColor)
14 |
15 | local function getColourOf(hex)
16 | return hexToColor[hex]
17 | end
18 |
19 | function NFT.parse(imageText)
20 | local image = {
21 | fg = { },
22 | bg = { },
23 | text = { },
24 | }
25 |
26 | local num = 1
27 | local lines = Util.split(imageText)
28 | while #lines[#lines] == 0 do
29 | table.remove(lines, #lines)
30 | end
31 |
32 | for _,sLine in ipairs(lines) do
33 | table.insert(image.fg, { })
34 | table.insert(image.bg, { })
35 | table.insert(image.text, { })
36 |
37 | --As we're no longer 1-1, we keep track of what index to write to
38 | local writeIndex = 1
39 | --Tells us if we've hit a 30 or 31 (BG and FG respectively)- next char specifies the curr colour
40 |
41 | local tcol, bcol = colors.white,colors.black
42 | local cx, sx = 1, 0
43 | while sx < #sLine do
44 | sx = sx + 1
45 | if sLine:sub(sx,sx) == "\30" then
46 | bcol = getColourOf(sLine:sub(sx+1,sx+1))
47 | sx = sx + 1
48 | elseif sLine:sub(sx,sx) == "\31" then
49 | tcol = getColourOf(sLine:sub(sx+1,sx+1))
50 | sx = sx + 1
51 | else
52 | image.bg[num][writeIndex] = bcol
53 | image.fg[num][writeIndex] = tcol
54 | image.text[num][writeIndex] = sLine:sub(sx,sx)
55 | writeIndex = writeIndex + 1
56 | cx = cx + 1
57 | end
58 | end
59 | image.height = num
60 | if not image.width or writeIndex - 1 > image.width then
61 | image.width = writeIndex - 1
62 | end
63 | num = num+1
64 | end
65 | return image
66 | end
67 |
68 | function NFT.transparency(image)
69 | for y = 1, image.height do
70 | for _,key in pairs(Util.keys(image.fg[y])) do
71 | if image.fg[y][key] == colors.magenta then
72 | image.fg[y][key] = nil
73 | end
74 | end
75 | for _,key in pairs(Util.keys(image.bg[y])) do
76 | if image.bg[y][key] == colors.magenta then
77 | image.bg[y][key] = nil
78 | end
79 | end
80 | end
81 | end
82 |
83 | function NFT.load(path)
84 | local imageText = Util.readFile(path)
85 | if not imageText then
86 | error('Unable to read image file')
87 | end
88 | return NFT.parse(imageText)
89 | end
90 |
91 | function NFT.save(image, filename)
92 | local bgcode, txcode = '\30', '\31'
93 | local output = { }
94 |
95 | for y = 1, image.height do
96 | local lastBG, lastFG
97 | if image.text[y] then
98 | for x = 1, #image.text[y] do
99 | local bg = image.bg[y][x] or colors.magenta
100 | if bg ~= lastBG then
101 | lastBG = bg
102 | table.insert(output, bgcode .. colorToHex[bg])
103 | end
104 |
105 | local fg = image.fg[y][x] or colors.magenta
106 | if fg ~= lastFG then
107 | lastFG = fg
108 | table.insert(output, txcode .. colorToHex[fg])
109 | end
110 |
111 | table.insert(output, image.text[y][x])
112 | end
113 | end
114 |
115 | if y < image.height then
116 | table.insert(output, '\n')
117 | end
118 | end
119 | Util.writeFile(filename, table.concat(output))
120 | end
121 |
122 | return NFT
123 |
--------------------------------------------------------------------------------
/sys/modules/opus/packages.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local fs = _G.fs
4 | local textutils = _G.textutils
5 |
6 | local PACKAGE_DIR = 'packages'
7 |
8 | local Packages = { }
9 |
10 | function Packages:installed()
11 | local list = { }
12 |
13 | if fs.exists(PACKAGE_DIR) then
14 | for _, dir in pairs(fs.list(PACKAGE_DIR)) do
15 | local path = fs.combine(fs.combine(PACKAGE_DIR, dir), '.package')
16 | list[dir] = Util.readTable(path)
17 | end
18 | end
19 |
20 | return list
21 | end
22 |
23 | function Packages:installedSorted()
24 | local list = { }
25 |
26 | for k, v in pairs(self.installed()) do
27 | v.name = k
28 | v.deps = { }
29 | table.insert(list, v)
30 | for _, v2 in pairs(v.required or { }) do
31 | v.deps[v2] = true
32 | end
33 | end
34 |
35 | table.sort(list, function(a, b)
36 | return not not (b.deps and b.deps[a.name])
37 | end)
38 |
39 | table.sort(list, function(a, b)
40 | return not (a.deps and a.deps[b.name])
41 | end)
42 |
43 | return list
44 | end
45 |
46 | function Packages:list()
47 | if not fs.exists('usr/config/packages') then
48 | self:downloadList()
49 | end
50 | return Util.readTable('usr/config/packages') or { }
51 | end
52 |
53 | function Packages:isInstalled(package)
54 | return self:installed()[package]
55 | end
56 |
57 | function Packages:downloadList()
58 | local packages = {
59 | [ 'develop-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/develop-1.8/packages.list',
60 | [ 'master-1.8' ] = 'https://raw.githubusercontent.com/kepler155c/opus-apps/master-1.8/packages.list',
61 | }
62 |
63 | if packages[_G.OPUS_BRANCH] then
64 | Util.download(packages[_G.OPUS_BRANCH], 'usr/config/packages')
65 | end
66 | end
67 |
68 | function Packages:downloadManifest(package)
69 | local list = self:list()
70 | local url = list and list[package]
71 |
72 | if url then
73 | local c = Util.httpGet(url)
74 | if c then
75 | c = textutils.unserialize(c)
76 | if c then
77 | c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH)
78 | return c
79 | end
80 | end
81 | end
82 | end
83 |
84 | function Packages:getManifest(package)
85 | local fname = 'packages/' .. package .. '/.package'
86 | if fs.exists(fname) then
87 | local c = Util.readTable(fname)
88 | if c and c.repository then
89 | c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH)
90 | return c
91 | end
92 | end
93 | return self:downloadManifest(package)
94 | end
95 |
96 | return Packages
97 |
--------------------------------------------------------------------------------
/sys/modules/opus/peripheral.lua:
--------------------------------------------------------------------------------
1 | local Util = require('opus.util')
2 |
3 | local Peripheral = Util.shallowCopy(_G.peripheral)
4 |
5 | function Peripheral.getList()
6 | if _G.device then
7 | return _G.device
8 | end
9 |
10 | local deviceList = { }
11 | for _,side in pairs(Peripheral.getNames()) do
12 | Peripheral.addDevice(deviceList, side)
13 | end
14 |
15 | return deviceList
16 | end
17 |
18 | function Peripheral.addDevice(deviceList, side)
19 | local name = side
20 | pcall(function()
21 | local ptype = Peripheral.getType(side)
22 | local dev = Peripheral.wrap(side)
23 |
24 | if not ptype or not dev then
25 | return
26 | end
27 |
28 | if ptype == 'modem' then
29 | if not Peripheral.call(name, 'isWireless') then
30 | -- ptype = 'wireless_modem'
31 | -- else
32 | ptype = 'wired_modem'
33 | if dev.isAccessPoint then
34 | -- avoid open computer relays being registered
35 | -- as 'wired_modem'
36 | ptype = dev.getMetadata().name or 'wired_modem'
37 | end
38 | end
39 | end
40 |
41 | local sides = {
42 | front = true,
43 | back = true,
44 | top = true,
45 | bottom = true,
46 | left = true,
47 | right = true
48 | }
49 |
50 | if sides[name] then
51 | local i = 1
52 | local uniqueName = ptype
53 | while deviceList[uniqueName] do
54 | uniqueName = ptype .. '_' .. i
55 | i = i + 1
56 | end
57 | name = uniqueName
58 | end
59 |
60 | -- this can randomly fail
61 | if not deviceList[name] then
62 | deviceList[name] = dev
63 |
64 | if deviceList[name] then
65 | Util.merge(deviceList[name], {
66 | name = name,
67 | type = ptype,
68 | side = side,
69 | })
70 | end
71 | end
72 | end)
73 |
74 | return deviceList[name]
75 | end
76 |
77 | function Peripheral.getBySide(side)
78 | return Util.find(Peripheral.getList(), 'side', side)
79 | end
80 |
81 | function Peripheral.getByType(typeName)
82 | return Util.find(Peripheral.getList(), 'type', typeName)
83 | end
84 |
85 | function Peripheral.getByMethod(method)
86 | for _,p in pairs(Peripheral.getList()) do
87 | if p[method] then
88 | return p
89 | end
90 | end
91 | end
92 |
93 | -- match any of the passed arguments
94 | function Peripheral.get(args)
95 |
96 | if type(args) == 'string' then
97 | args = { type = args }
98 | end
99 |
100 | if args.name then
101 | return _G.device[args.name]
102 | end
103 |
104 | if args.type then
105 | local p = Peripheral.getByType(args.type)
106 | if p then
107 | return p
108 | end
109 | end
110 |
111 | if args.method then
112 | local p = Peripheral.getByMethod(args.method)
113 | if p then
114 | return p
115 | end
116 | end
117 |
118 | if args.side then
119 | local p = Peripheral.getBySide(args.side)
120 | if p then
121 | return p
122 | end
123 | end
124 | end
125 |
126 | return Peripheral
127 |
--------------------------------------------------------------------------------
/sys/modules/opus/security.lua:
--------------------------------------------------------------------------------
1 | local Config = require('opus.config')
2 |
3 | local Security = { }
4 |
5 | function Security.verifyPassword(password)
6 | local current = Security.getPassword()
7 | return current and password == current
8 | end
9 |
10 | function Security.hasPassword()
11 | return not not Security.getPassword()
12 | end
13 |
14 | function Security.getIdentifier()
15 | local config = Config.load('os')
16 |
17 | if not config.identifier then
18 | local key = { }
19 | for _ = 1, 32 do
20 | table.insert(key, ("%02x"):format(math.random(0, 0xFF)))
21 | end
22 | config.identifier = table.concat(key)
23 |
24 | Config.update('os', config)
25 | end
26 |
27 | return config.identifier
28 | end
29 |
30 | function Security.updatePassword(password)
31 | local config = Config.load('os')
32 | config.password = password
33 | Config.update('os', config)
34 | end
35 |
36 | function Security.getPassword()
37 | return Config.load('os').password
38 | end
39 |
40 | return Security
41 |
--------------------------------------------------------------------------------
/sys/modules/opus/sound.lua:
--------------------------------------------------------------------------------
1 | local peripheral = _G.peripheral
2 |
3 | local Sound = {
4 | _volume = 1,
5 | }
6 |
7 | function Sound.play(sound, vol)
8 | peripheral.find('speaker', function(_, s)
9 | s.playSound('minecraft:' .. sound, vol or Sound._volume)
10 | end)
11 | end
12 |
13 | function Sound.setVolume(volume)
14 | Sound._volume = math.max(0, math.min(volume, 1))
15 | end
16 |
17 | return Sound
18 |
--------------------------------------------------------------------------------
/sys/modules/opus/sync.lua:
--------------------------------------------------------------------------------
1 | local Sync = {
2 | syncLocks = { }
3 | }
4 |
5 | local os = _G.os
6 |
7 | function Sync.sync(obj, fn)
8 | local key = tostring(obj)
9 | if Sync.syncLocks[key] then
10 | local cos = tostring(coroutine.running())
11 | table.insert(Sync.syncLocks[key], cos)
12 | repeat
13 | local _, co = os.pullEvent('sync_lock')
14 | until co == cos
15 | else
16 | Sync.syncLocks[key] = { }
17 | end
18 | local s, m = pcall(fn)
19 | local co = table.remove(Sync.syncLocks[key], 1)
20 | if co then
21 | os.queueEvent('sync_lock', co)
22 | else
23 | Sync.syncLocks[key] = nil
24 | end
25 | if not s then
26 | error(m)
27 | end
28 | end
29 |
30 | function Sync.lock(obj)
31 | local key = tostring(obj)
32 | if Sync.syncLocks[key] then
33 | local cos = tostring(coroutine.running())
34 | table.insert(Sync.syncLocks[key], cos)
35 | repeat
36 | local _, co = os.pullEvent('sync_lock')
37 | until co == cos
38 | else
39 | Sync.syncLocks[key] = { }
40 | end
41 | end
42 |
43 | function Sync.release(obj)
44 | local key = tostring(obj)
45 | if not Sync.syncLocks[key] then
46 | error('Sync.release: Lock was not obtained', 2)
47 | end
48 | local co = table.remove(Sync.syncLocks[key], 1)
49 | if co then
50 | os.queueEvent('sync_lock', co)
51 | else
52 | Sync.syncLocks[key] = nil
53 | end
54 | end
55 |
56 | function Sync.isLocked(obj)
57 | local key = tostring(obj)
58 | return not not Sync.syncLocks[key]
59 | end
60 |
61 | return Sync
62 |
--------------------------------------------------------------------------------
/sys/modules/opus/trace.lua:
--------------------------------------------------------------------------------
1 | -- stack trace by SquidDev (MIT License)
2 | -- https://raw.githubusercontent.com/SquidDev-CC/mbs/master/lib/stack_trace.lua
3 |
4 | local type = type
5 | local debug_traceback = type(debug) == "table" and type(debug.traceback) == "function" and debug.traceback
6 |
7 | local function traceback(x)
8 | -- Attempt to detect error() and error("xyz", 0).
9 | -- This probably means they're erroring the program intentionally and so we
10 | -- shouldn't display anything.
11 | if x == nil or (type(x) == "string" and not x:find(":%d+:")) then
12 | return x
13 | end
14 |
15 | if x and x:match(':%d+: 0$') then
16 | return x
17 | end
18 |
19 | if debug_traceback then
20 | -- The parens are important, as they prevent a tail call occuring, meaning
21 | -- the stack level is preserved. This ensures the code behaves identically
22 | -- on LuaJ and PUC Lua.
23 | return (debug_traceback(tostring(x), 2))
24 | else
25 | local level = 3
26 | local out = { tostring(x), "stack traceback:" }
27 | while true do
28 | local _, msg = pcall(error, "", level)
29 | if msg == "" then break end
30 |
31 | out[#out + 1] = " " .. msg
32 | level = level + 1
33 | end
34 |
35 | return table.concat(out, "\n")
36 | end
37 | end
38 |
39 | local function trim_traceback(stack)
40 | local trace = { }
41 | local filters = {
42 | "%[C%]: in function 'xpcall'",
43 | "(...tail calls...)",
44 | "xpcall: $",
45 | "trace.lua:%d+:",
46 | "stack traceback:",
47 | }
48 |
49 | for line in stack:gmatch("([^\n]*)\n?") do table.insert(trace, line) end
50 |
51 | local err = { }
52 | while true do
53 | local line = table.remove(trace, 1)
54 | if not line or line == 'stack traceback:' then
55 | break
56 | end
57 | table.insert(err, line)
58 | end
59 | err = table.concat(err, '\n')
60 |
61 | local function matchesFilter(line)
62 | for _, filter in pairs(filters) do
63 | if line:match(filter) then
64 | return true
65 | end
66 | end
67 | end
68 |
69 | local t = { }
70 | for _, line in pairs(trace) do
71 | if not matchesFilter(line) then
72 | line = line:gsub("in function", "in"):gsub('%w+/', '')
73 | table.insert(t, line)
74 | end
75 | end
76 |
77 | return err, t
78 | end
79 |
80 | return function (fn, ...)
81 | local args = { ... }
82 | local res = table.pack(xpcall(function()
83 | return fn(table.unpack(args))
84 | end, traceback))
85 |
86 | if not res[1] and res[2] ~= nil then
87 | local err, trace = trim_traceback(res[2])
88 |
89 | if err:match(':%d+: 0$') then
90 | return true
91 | end
92 |
93 | if #trace > 0 then
94 | _G._syslog('\n' .. err .. '\n' .. 'stack traceback:')
95 | for _, v in ipairs(trace) do
96 | _G._syslog(v)
97 | end
98 | end
99 |
100 | return res[1], err, trace
101 | end
102 |
103 | return table.unpack(res, 1, res.n)
104 | end
105 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Button.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | UI.Button = class(UI.Window)
6 | UI.Button.defaults = {
7 | UIElement = 'Button',
8 | text = 'button',
9 | backgroundColor = 'lightGray',
10 | backgroundFocusColor = 'gray',
11 | textFocusColor = 'white',
12 | textInactiveColor = 'gray',
13 | textColor = 'black',
14 | centered = true,
15 | height = 1,
16 | focusIndicator = ' ',
17 | event = 'button_press',
18 | accelerators = {
19 | [ ' ' ] = 'button_activate',
20 | enter = 'button_activate',
21 | mouse_click = 'button_activate',
22 | mouse_doubleclick = 'button_activate',
23 | mouse_tripleclick = 'button_activate',
24 | }
25 | }
26 | function UI.Button:layout()
27 | if not self.width and not self.ex then
28 | self.width = self.noPadding and #self.text or #self.text + 2
29 | end
30 | UI.Window.layout(self)
31 | end
32 |
33 | function UI.Button:draw()
34 | local fg = self.textColor
35 | local bg = self.backgroundColor
36 | local ind = ' '
37 | if self.focused then
38 | bg = self:getProperty('backgroundFocusColor')
39 | fg = self:getProperty('textFocusColor')
40 | ind = self.focusIndicator
41 | elseif self.inactive then
42 | fg = self:getProperty('textInactiveColor')
43 | end
44 | local text = self.noPadding and self.text or ind .. self.text .. ' '
45 | if self.centered then
46 | self:clear(bg)
47 | self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg)
48 | else
49 | self:write(1, 1, Util.widthify(text, self.width), bg, fg)
50 | end
51 | end
52 |
53 | function UI.Button:focus()
54 | if self.focused then
55 | self:scrollIntoView()
56 | end
57 | self:draw()
58 | end
59 |
60 | function UI.Button:eventHandler(event)
61 | if event.type == 'button_activate' then
62 | self:emit({ type = self.event, button = self, element = self })
63 | return true
64 | end
65 | return false
66 | end
67 |
68 | function UI.Button.example()
69 | return UI.Window {
70 | button1 = UI.Button {
71 | x = 2, y = 2,
72 | text = 'Press',
73 | },
74 | button2 = UI.Button {
75 | x = 2, y = 4,
76 | backgroundColor = 'green',
77 | event = 'custom_event',
78 | },
79 | button3 = UI.Button {
80 | x = 12, y = 2,
81 | height = 5,
82 | event = 'big_event',
83 | text = 'large button'
84 | }
85 | }
86 | end
87 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Checkbox.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.Checkbox = class(UI.Window)
5 | UI.Checkbox.defaults = {
6 | UIElement = 'Checkbox',
7 | nochoice = 'Select',
8 | checkedIndicator = UI.extChars and '\4' or 'X',
9 | leftMarker = UI.extChars and '\124' or '[',
10 | rightMarker = UI.extChars and '\124' or ']',
11 | value = false,
12 | textColor = 'white',
13 | backgroundColor = 'black',
14 | backgroundFocusColor = 'lightGray',
15 | event = 'checkbox_change',
16 | height = 1,
17 | width = 3,
18 | accelerators = {
19 | [ ' ' ] = 'checkbox_toggle',
20 | mouse_click = 'checkbox_toggle',
21 | }
22 | }
23 | function UI.Checkbox:layout()
24 | self.width = self.label and #self.label + 4 or 3
25 | UI.Window.layout(self)
26 | end
27 |
28 | function UI.Checkbox:draw()
29 | local bg = self.focused and self.backgroundFocusColor or self.backgroundColor
30 | local x = 1
31 | if self.label then
32 | self:write(1, 1, self.label, self.labelBackgroundColor)
33 | x = #self.label + 2
34 | end
35 | self:write(x, 1, self.leftMarker, self.backgroundColor, self.textColor)
36 | self:write(x + 1, 1, not self.value and ' ' or self.checkedIndicator, bg)
37 | self:write(x + 2, 1, self.rightMarker, self.backgroundColor, self.textColor)
38 | end
39 |
40 | function UI.Checkbox:focus()
41 | self:draw()
42 | end
43 |
44 | function UI.Checkbox:setValue(v)
45 | self.value = not not v
46 | end
47 |
48 | function UI.Checkbox:reset()
49 | self.value = false
50 | self:draw()
51 | end
52 |
53 | function UI.Checkbox:eventHandler(event)
54 | if event.type == 'checkbox_toggle' then
55 | self.value = not self.value
56 | self:emit({ type = self.event, checked = self.value, element = self })
57 | self:draw()
58 | return true
59 | end
60 | end
61 |
62 | function UI.Checkbox.example()
63 | return UI.Window {
64 | ex1 = UI.Checkbox {
65 | label = 'test',
66 | x = 2, y = 2,
67 | },
68 | ex2 = UI.Checkbox {
69 | x = 2, y = 4,
70 | },
71 | }
72 | end
73 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/CheckboxGrid.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | local function safeValue(v)
5 | local t = type(v)
6 | if t == 'string' or t == 'number' then
7 | return v
8 | end
9 | return tostring(v)
10 | end
11 |
12 | UI.CheckboxGrid = class(UI.Grid)
13 | UI.CheckboxGrid.defaults = {
14 | UIElement = 'CheckboxGrid',
15 | checkedKey = 'checked',
16 | accelerators = {
17 | [ ' ' ] = 'grid_toggle',
18 | key_enter = 'grid_toggle',
19 | },
20 | }
21 | function UI.CheckboxGrid:drawRow(sb, row, focused, bg, fg)
22 | local ind = focused and self.focusIndicator or ' '
23 |
24 | for _,col in pairs(self.columns) do
25 | sb:write(ind .. safeValue(row[col.key] or ''),
26 | col.cw + 1,
27 | col.align,
28 | col.backgroundColor or bg,
29 | col.textColor or fg)
30 | ind = ' '
31 | end
32 | end
33 |
34 | function UI.CheckboxGrid:eventHandler(event)
35 | if event.type == 'grid_toggle' and self.selected then
36 | self.selected.checked = not self.selected.checked
37 | self:draw()
38 | self:emit({ type = 'grid_check', checked = self.selected, element = self })
39 | else
40 | return UI.Grid.eventHandler(self, event)
41 | end
42 | end
43 |
44 | function UI.CheckboxGrid.example()
45 | return UI.CheckboxGrid {
46 | values = {
47 | { checked = false, name = 'unchecked' },
48 | { checked = true, name = 'checked' },
49 | },
50 | columns = {
51 | { heading = 'Checked', key = 'checked' },
52 | { heading = 'Data', key = 'name', }
53 | },
54 | }
55 | end
56 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Chooser.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | local colors = _G.colors
6 |
7 | UI.Chooser = class(UI.Window)
8 | UI.Chooser.defaults = {
9 | UIElement = 'Chooser',
10 | choices = { },
11 | nochoice = 'Select',
12 | backgroundFocusColor = colors.lightGray,
13 | textInactiveColor = colors.gray,
14 | leftIndicator = UI.extChars and '\171' or '<',
15 | rightIndicator = UI.extChars and '\187' or '>',
16 | height = 1,
17 | accelerators = {
18 | [ ' ' ] = 'choice_next',
19 | right = 'choice_next',
20 | left = 'choice_prev',
21 | }
22 | }
23 | function UI.Chooser:layout()
24 | if not self.width and not self.ex then
25 | self.width = 1
26 | for _,v in pairs(self.choices) do
27 | if #v.name > self.width then
28 | self.width = #v.name
29 | end
30 | end
31 | self.width = self.width + 4
32 | end
33 | UI.Window.layout(self)
34 | end
35 |
36 | function UI.Chooser:draw()
37 | local bg = self.focused and self.backgroundFocusColor or self.backgroundColor
38 | local fg = self.inactive and self.textInactiveColor or self.textColor
39 | local choice = Util.find(self.choices, 'value', self.value)
40 | local value = choice and choice.name or self.nochoice
41 |
42 | self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black)
43 | self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width - 4) .. ' ', bg, fg)
44 | self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black)
45 | end
46 |
47 | function UI.Chooser:focus()
48 | self:draw()
49 | end
50 |
51 | function UI.Chooser:eventHandler(event)
52 | if event.type == 'choice_next' then
53 | local _,k = Util.find(self.choices, 'value', self.value)
54 | local choice
55 | if not k then k = 0 end
56 | if k and k < #self.choices then
57 | choice = self.choices[k + 1]
58 | else
59 | choice = self.choices[1]
60 | end
61 | self.value = choice.value
62 | self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
63 | self:draw()
64 | return true
65 |
66 | elseif event.type == 'choice_prev' then
67 | local _,k = Util.find(self.choices, 'value', self.value)
68 | local choice
69 | if k and k > 1 then
70 | choice = self.choices[k - 1]
71 | else
72 | choice = self.choices[#self.choices]
73 | end
74 | self.value = choice.value
75 | self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
76 | self:draw()
77 | return true
78 |
79 | elseif event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
80 | if event.x == 1 then
81 | self:emit({ type = 'choice_prev' })
82 | return true
83 | elseif event.x == self.width then
84 | self:emit({ type = 'choice_next' })
85 | return true
86 | end
87 | end
88 | end
89 |
90 | function UI.Chooser.example()
91 | return UI.Window {
92 | a = UI.Chooser {
93 | x = 2, y = 2,
94 | choices = {
95 | { name = 'choice1', value = 'value1' },
96 | { name = 'choice2', value = 'value2' },
97 | { name = 'choice3', value = 'value3' },
98 | },
99 | value = 'value2',
100 | },
101 | b = UI.Chooser {
102 | x = 2, y = 4,
103 | choices = {
104 | { name = 'choice1', value = 'value1' },
105 | { name = 'choice2', value = 'value2' },
106 | { name = 'choice3', value = 'value3' },
107 | },
108 | }
109 | }
110 | end
111 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Dialog.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.Dialog = class(UI.SlideOut)
5 | UI.Dialog.defaults = {
6 | UIElement = 'Dialog',
7 | height = 7,
8 | noFill = true,
9 | okEvent ='dialog_ok',
10 | cancelEvent = 'dialog_cancel',
11 | }
12 | function UI.Dialog:postInit()
13 | self.y = -self.height
14 | self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title })
15 | end
16 |
17 | function UI.Dialog:eventHandler(event)
18 | if event.type == 'dialog_cancel' then
19 | self:hide()
20 | end
21 | return UI.SlideOut.eventHandler(self, event)
22 | end
23 |
24 | function UI.Dialog.example()
25 | return UI.Dialog {
26 | title = 'Enter Starting Level',
27 | height = 7,
28 | form = UI.Form {
29 | y = 3, x = 2, height = 4,
30 | event = 'setStartLevel',
31 | cancelEvent = 'slide_hide',
32 | text = UI.Text {
33 | x = 5, y = 1, width = 20,
34 | textColor = 'gray',
35 | },
36 | textEntry = UI.TextEntry {
37 | formKey = 'level',
38 | x = 15, y = 1, width = 7,
39 | },
40 | },
41 | statusBar = UI.StatusBar(),
42 | enable = function(self)
43 | require('opus.event').onTimeout(0, function()
44 | self:show()
45 | self:sync()
46 | end)
47 | end,
48 | }
49 | end
50 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/DropMenu.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | UI.DropMenu = class(UI.MenuBar)
6 | UI.DropMenu.defaults = {
7 | UIElement = 'DropMenu',
8 | backgroundColor = 'white',
9 | buttonClass = 'DropMenuItem',
10 | }
11 | function UI.DropMenu:layout()
12 | UI.MenuBar.layout(self)
13 |
14 | local maxWidth = 1
15 | for y,child in ipairs(self.children) do
16 | child.x = 1
17 | child.y = y
18 | if #(child.text or '') > maxWidth then
19 | maxWidth = #child.text
20 | end
21 | end
22 | for _,child in ipairs(self.children) do
23 | child.width = maxWidth + 2
24 | if child.spacer then
25 | child.inactive = true
26 | child.text = string.rep('-', child.width - 2)
27 | end
28 | end
29 |
30 | self.height = #self.children + 1
31 | self.width = maxWidth + 2
32 |
33 | if self.x + self.width > self.parent.width then
34 | self.x = self.parent.width - self.width + 1
35 | end
36 |
37 | self:reposition(self.x, self.y, self.width, self.height)
38 | end
39 |
40 | function UI.DropMenu:enable()
41 | local menuBar = self.parent:find(self.menuUid)
42 | local hasActive
43 |
44 | for _,c in pairs(self.children) do
45 | if not c.spacer and menuBar then
46 | c.inactive = not menuBar:getActive(c)
47 | end
48 | if not c.inactive then
49 | hasActive = true
50 | end
51 | end
52 |
53 | -- jump through a lot of hoops if all selections are inactive
54 | -- there's gotta be a better way
55 | -- lots of exception code just to handle drop menus
56 | self.focus = not hasActive and function() end
57 |
58 | UI.Window.enable(self)
59 | if self.focus then
60 | self:setFocus(self)
61 | else
62 | self:focusFirst()
63 | end
64 | self:draw()
65 | end
66 |
67 | function UI.DropMenu:disable()
68 | UI.Window.disable(self)
69 | self:remove()
70 | end
71 |
72 | function UI.DropMenu:eventHandler(event)
73 | if event.type == 'focus_lost' and self.enabled then
74 | if not (Util.contains(self.children, event.focused) or event.focused == self) then
75 | self:disable()
76 | end
77 | elseif event.type == 'mouse_out' and self.enabled then
78 | self:disable()
79 | self:setFocus(self.parent:find(self.lastFocus))
80 | else
81 | return UI.MenuBar.eventHandler(self, event)
82 | end
83 | return true
84 | end
85 |
86 | function UI.DropMenu.example()
87 | return UI.MenuBar {
88 | buttons = {
89 | { text = 'File', dropdown = {
90 | { text = 'Run', event = 'run' },
91 | { text = 'Shell s', event = 'shell' },
92 | { spacer = true },
93 | { text = 'Quit ^q', event = 'quit' },
94 | } },
95 | { text = 'Edit', dropdown = {
96 | { text = 'Copy', event = 'run' },
97 | { text = 'Paste s', event = 'shell' },
98 | } },
99 | { text = '\187',
100 | x = -3,
101 | dropdown = {
102 | { text = 'Associations', event = 'associate' },
103 | } },
104 | }
105 | }
106 | end
107 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/DropMenuItem.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.DropMenuItem = class(UI.Button)
5 | UI.DropMenuItem.defaults = {
6 | UIElement = 'DropMenuItem',
7 | textColor = 'black',
8 | backgroundColor = 'white',
9 | textFocusColor = 'white',
10 | textInactiveColor = 'lightGray',
11 | backgroundFocusColor = 'lightGray',
12 | }
13 | function UI.DropMenuItem:eventHandler(event)
14 | if event.type == 'button_activate' then
15 | self.parent:disable()
16 | end
17 | return UI.Button.eventHandler(self, event)
18 | end
19 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Embedded.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local Event = require('opus.event')
3 | local Terminal = require('opus.terminal')
4 | local UI = require('opus.ui')
5 |
6 | UI.Embedded = class(UI.Window)
7 | UI.Embedded.defaults = {
8 | UIElement = 'Embedded',
9 | backgroundColor = 'black',
10 | textColor = 'white',
11 | maxScroll = 100,
12 | accelerators = {
13 | up = 'scroll_up',
14 | down = 'scroll_down',
15 | }
16 | }
17 | function UI.Embedded:layout()
18 | UI.Window.layout(self)
19 |
20 | if not self.win then
21 | local t
22 | function self.render()
23 | if not t then
24 | t = Event.onTimeout(0, function()
25 | t = nil
26 | if self.focused then
27 | self:setCursorPos(self.win.getCursorPos())
28 | end
29 | self:sync()
30 | end)
31 | end
32 | end
33 | self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false)
34 | self.win.canvas = self
35 | self.win.setMaxScroll(self.maxScroll)
36 | self.win.setCursorPos(1, 1)
37 | self.win.setBackgroundColor(self.backgroundColor)
38 | self.win.setTextColor(self.textColor)
39 | self.win.clear()
40 | end
41 | end
42 |
43 | function UI.Embedded:draw()
44 | self:dirty()
45 | end
46 |
47 | function UI.Embedded:focus()
48 | -- allow scrolling
49 | if self.focused then
50 | self:setCursorBlink(self.win.getCursorBlink())
51 | end
52 | end
53 |
54 | function UI.Embedded:enable()
55 | UI.Window.enable(self)
56 | self.win.setVisible(true)
57 | self:dirty()
58 | end
59 |
60 | function UI.Embedded:disable()
61 | self.win.setVisible(false)
62 | UI.Window.disable(self)
63 | end
64 |
65 | function UI.Embedded:eventHandler(event)
66 | if event.type == 'scroll_up' then
67 | self.win.scrollUp()
68 | return true
69 | elseif event.type == 'scroll_down' then
70 | self.win.scrollDown()
71 | return true
72 | end
73 | end
74 |
75 | function UI.Embedded.example()
76 | local Util = require('opus.util')
77 | local term = _G.term
78 |
79 | return UI.Embedded {
80 | y = 2, x = 2, ex = -2, ey = -2,
81 | enable = function (self)
82 | UI.Embedded.enable(self)
83 | Event.addRoutine(function()
84 | local oterm = term.redirect(self.win)
85 | Util.run(_ENV, '/sys/apps/shell.lua')
86 | term.redirect(oterm)
87 | end)
88 | end,
89 | eventHandler = function(self, event)
90 | if event.type == 'key' then
91 | return true
92 | end
93 | return UI.Embedded.eventHandler(self, event)
94 | end
95 | }
96 | end
97 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/FileSelect.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | local fs = _G.fs
6 |
7 | UI.FileSelect = class(UI.Window)
8 | UI.FileSelect.defaults = {
9 | UIElement = 'FileSelect',
10 | }
11 | function UI.FileSelect:postInit()
12 | self.grid = UI.ScrollingGrid {
13 | x = 2, y = 2, ex = -2, ey = -4,
14 | dir = '/',
15 | sortColumn = 'name',
16 | columns = {
17 | { heading = 'Name', key = 'name' },
18 | { heading = 'Size', key = 'size', width = 5 }
19 | },
20 | getDisplayValues = function(_, row)
21 | return {
22 | name = row.name,
23 | size = row.size and Util.toBytes(row.size),
24 | }
25 | end,
26 | getRowTextColor = function(_, file)
27 | return file.isDir and 'cyan' or file.isReadOnly and 'pink' or 'white'
28 | end,
29 | sortCompare = function(self, a, b)
30 | if self.sortColumn == 'size' then
31 | return a.size < b.size
32 | end
33 | if a.isDir == b.isDir then
34 | return a.name:lower() < b.name:lower()
35 | end
36 | return a.isDir
37 | end,
38 | draw = function(self)
39 | local files = fs.listEx(self.dir)
40 | if #self.dir > 0 then
41 | table.insert(files, {
42 | name = '..',
43 | isDir = true,
44 | })
45 | end
46 | self:setValues(files)
47 | self:setIndex(1)
48 | UI.Grid.draw(self)
49 | end,
50 | }
51 | self.path = UI.TextEntry {
52 | x = 2, y = -2, ex = -11,
53 | accelerators = {
54 | enter = 'path_enter',
55 | }
56 | }
57 | self.cancel = UI.Button {
58 | x = -9, y = -2,
59 | text = 'Cancel',
60 | event = 'select_cancel',
61 | }
62 | end
63 |
64 | function UI.FileSelect:draw()
65 | self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), 'black', 'gray')
66 | self:drawChildren()
67 | end
68 |
69 | function UI.FileSelect:enable(path)
70 | self:setPath(path or '')
71 | UI.Window.enable(self)
72 | end
73 |
74 | function UI.FileSelect:setPath(path)
75 | self.grid.dir = path
76 | while not fs.isDir(self.grid.dir) do
77 | self.grid.dir = fs.getDir(self.grid.dir)
78 | end
79 | self.path.value = self.grid.dir
80 | end
81 |
82 | function UI.FileSelect:eventHandler(event)
83 | if event.type == 'grid_select' then
84 | self.grid.dir = fs.combine(self.grid.dir, event.selected.name)
85 | self.path.value = self.grid.dir
86 | if event.selected.isDir then
87 | self.grid:draw()
88 | self.path:draw()
89 | else
90 | self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self })
91 | end
92 | return true
93 |
94 | elseif event.type == 'path_enter' then
95 | if self.path.value then
96 | if fs.isDir(self.path.value) then
97 | self:setPath(self.path.value)
98 | self.grid:draw()
99 | self.path:draw()
100 | else
101 | self:emit({ type = 'select_file', file = '/' .. self.path.value, element = self })
102 | end
103 | end
104 | return true
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/FlatButton.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.FlatButton = class(UI.Button)
5 | UI.FlatButton.defaults = {
6 | UIElement = 'FlatButton',
7 | textColor = 'black',
8 | textFocusColor = 'white',
9 | noPadding = true,
10 | }
11 | function UI.FlatButton:setParent()
12 | self.backgroundColor = self.parent:getProperty('backgroundColor')
13 | self.backgroundFocusColor = self.backgroundColor
14 |
15 | UI.Button.setParent(self)
16 | end
17 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Image.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | local lookup = '0123456789abcdef'
6 |
7 | -- handle files produced by Paint
8 | UI.Image = class(UI.Window)
9 | UI.Image.defaults = {
10 | UIElement = 'Image',
11 | event = 'button_press',
12 | }
13 | function UI.Image:postInit()
14 | if self.filename then
15 | self.image = Util.readLines(self.filename)
16 | end
17 |
18 | if self.image and not (self.height or self.ey) then
19 | self.height = #self.image
20 | end
21 | if self.image and not (self.width or self.ex) then
22 | for i = 1, self.height do
23 | self.width = math.max(self.width or 0, #self.image[i])
24 | end
25 | end
26 | end
27 |
28 | function UI.Image:draw()
29 | self:clear()
30 | if self.image then
31 | for y = 1, #self.image do
32 | local line = self.image[y]
33 | for x = 1, #line do
34 | local ch = lookup:find(line:sub(x, x))
35 | if ch then
36 | self:write(x, y, ' ', 2 ^ (ch - 1))
37 | end
38 | end
39 | end
40 | end
41 | self:drawChildren()
42 | end
43 |
44 | function UI.Image:setImage(image)
45 | self.image = image
46 | end
47 |
48 | function UI.Image.example()
49 | return UI.Image {
50 | backgroundColor = 'primary',
51 | filename = 'test.paint',
52 | }
53 | end
54 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Menu.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.Menu = class(UI.Grid)
5 | UI.Menu.defaults = {
6 | UIElement = 'Menu',
7 | disableHeader = true,
8 | columns = { { heading = 'Prompt', key = 'prompt', width = 20 } },
9 | menuItems = { },
10 | }
11 | function UI.Menu:postInit()
12 | self.values = self.menuItems
13 | self.pageSize = #self.menuItems
14 | end
15 |
16 | function UI.Menu:layout()
17 | self.itemWidth = 1
18 | for _,v in pairs(self.values) do
19 | if #v.prompt > self.itemWidth then
20 | self.itemWidth = #v.prompt
21 | end
22 | end
23 | self.columns[1].width = self.itemWidth
24 |
25 | if self.centered then
26 | self:center()
27 | else
28 | self.width = self.itemWidth + 2
29 | end
30 | UI.Grid.layout(self)
31 | end
32 |
33 | function UI.Menu:center()
34 | self.x = (self.width - self.itemWidth + 2) / 2
35 | self.width = self.itemWidth + 2
36 | end
37 |
38 | function UI.Menu:eventHandler(event)
39 | if event.type == 'key' then
40 | if event.key == 'enter' then
41 | local selected = self.menuItems[self.index]
42 | self:emit({
43 | type = selected.event or 'menu_select',
44 | selected = selected
45 | })
46 | return true
47 | end
48 | elseif event.type == 'mouse_click' then
49 | if event.y <= #self.menuItems then
50 | UI.Grid.setIndex(self, event.y)
51 | local selected = self.menuItems[self.index]
52 | self:emit({
53 | type = selected.event or 'menu_select',
54 | selected = selected
55 | })
56 | return true
57 | end
58 | end
59 | return UI.Grid.eventHandler(self, event)
60 | end
61 |
62 | function UI.Menu.example()
63 | return UI.Menu {
64 | x = 2, y = 2, height = 3,
65 | menuItems = {
66 | { prompt = 'Start', event = 'start' },
67 | { prompt = 'Continue', event = 'continue' },
68 | { prompt = 'Quit', event = 'quit' }
69 | }
70 | }
71 | end
72 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/MenuBar.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.MenuBar = class(UI.Window)
5 | UI.MenuBar.defaults = {
6 | UIElement = 'MenuBar',
7 | buttons = { },
8 | height = 1,
9 | backgroundColor = 'secondary',
10 | textColor = 'black',
11 | spacing = 2,
12 | lastx = 1,
13 | buttonClass = 'MenuItem',
14 | }
15 | function UI.MenuBar:postInit()
16 | self:addButtons(self.buttons)
17 | end
18 |
19 | function UI.MenuBar:addButtons(buttons)
20 | if not self.children then
21 | self.children = { }
22 | end
23 |
24 | for _,button in pairs(buttons) do
25 | if button.index then -- don't sort unless needed
26 | table.sort(buttons, function(a, b)
27 | return (a.index or 999) < (b.index or 999)
28 | end)
29 | break
30 | end
31 | end
32 |
33 | for _,button in pairs(buttons) do
34 | if button.UIElement then
35 | table.insert(self.children, button)
36 | else
37 | local buttonProperties = {
38 | x = self.lastx,
39 | width = #(button.text or 'button') + self.spacing,
40 | centered = false,
41 | backgroundColor = self.backgroundColor,
42 | }
43 | self.lastx = self.lastx + buttonProperties.width
44 | UI:mergeProperties(buttonProperties, button)
45 |
46 | button = UI[self.buttonClass](buttonProperties)
47 | if button.name then
48 | self[button.name] = button
49 | else
50 | table.insert(self.children, button)
51 | end
52 | end
53 | end
54 | if self.parent then
55 | self:initChildren()
56 | end
57 | end
58 |
59 | function UI.MenuBar:getActive(menuItem)
60 | return not menuItem.inactive
61 | end
62 |
63 | function UI.MenuBar:eventHandler(event)
64 | if event.type == 'button_press' and event.button.dropdown then
65 | local function getPosition(element)
66 | local x, y = 1, 1
67 | repeat
68 | x = element.x + x - 1
69 | y = element.y + y - 1
70 | element = element.parent
71 | until not element
72 | return x, y
73 | end
74 |
75 | local x, y = getPosition(event.button)
76 |
77 | local menu = UI.DropMenu {
78 | buttons = event.button.dropdown,
79 | x = x,
80 | y = y + 1,
81 | lastFocus = event.button.uid,
82 | menuUid = self.uid,
83 | }
84 | self.parent:add({ dropmenu = menu })
85 |
86 | return true
87 | end
88 | end
89 |
90 | function UI.MenuBar.example()
91 | return UI.MenuBar {
92 | buttons = {
93 | { text = 'Choice1', event = 'event1' },
94 | { text = 'Choice2', event = 'event2', inactive = true },
95 | { text = 'Choice3', event = 'event3' },
96 | }
97 | }
98 | end
99 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/MenuItem.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.MenuItem = class(UI.FlatButton)
5 | UI.MenuItem.defaults = {
6 | UIElement = 'MenuItem',
7 | noPadding = false,
8 | textInactiveColor = 'gray',
9 | }
10 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/MiniSlideOut.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.MiniSlideOut = class(UI.SlideOut)
5 | UI.MiniSlideOut.defaults = {
6 | UIElement = 'MiniSlideOut',
7 | noFill = true,
8 | backgroundColor = 'primary',
9 | height = 1,
10 | }
11 | function UI.MiniSlideOut:postInit()
12 | self.close_button = UI.Button {
13 | x = -1,
14 | backgroundColor = self.backgroundColor,
15 | backgroundFocusColor = self.backgroundColor,
16 | text = 'x',
17 | event = 'slide_hide',
18 | noPadding = true,
19 | }
20 | if self.label then
21 | self.label_text = UI.Text {
22 | x = 2,
23 | value = self.label,
24 | }
25 | end
26 | end
27 |
28 | function UI.MiniSlideOut:show(...)
29 | UI.SlideOut.show(self, ...)
30 | self:addTransition('slideLeft', { easing = 'outBounce' })
31 | end
32 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/NftImage.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.NftImage = class(UI.Window)
5 | UI.NftImage.defaults = {
6 | UIElement = 'NftImage',
7 | }
8 | function UI.NftImage:postInit()
9 | if self.image and not (self.ey or self.height) then
10 | self.height = self.image.height
11 | end
12 | if self.image and not (self.ex or self.width) then
13 | self.width = self.image.width
14 | end
15 | end
16 |
17 | function UI.NftImage:draw()
18 | self:clear()
19 |
20 | if self.image then
21 | -- due to blittle, the background and foreground transparent
22 | -- color is the same as the background color
23 | local bg = self:getProperty('backgroundColor')
24 | for y = 1, self.image.height do
25 | for x = 1, #self.image.text[y] do
26 | self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x] or bg)
27 | end
28 | end
29 | end
30 | end
31 |
32 | function UI.NftImage:setImage(image)
33 | self.image = image
34 | end
35 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Notification.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local Event = require('opus.event')
3 | local Sound = require('opus.sound')
4 | local UI = require('opus.ui')
5 | local Util = require('opus.util')
6 |
7 | UI.Notification = class(UI.Window)
8 | UI.Notification.defaults = {
9 | UIElement = 'Notification',
10 | backgroundColor = 'gray',
11 | closeInd = UI.extChars and '\215' or '*',
12 | height = 3,
13 | timeout = 3,
14 | anchor = 'bottom',
15 | }
16 | function UI.Notification.draw()
17 | end
18 |
19 | function UI.Notification.enable()
20 | end
21 |
22 | function UI.Notification:error(value, timeout)
23 | self.backgroundColor = 'red'
24 | Sound.play('entity.villager.no', .5)
25 | self:display(value, timeout)
26 | end
27 |
28 | function UI.Notification:info(value, timeout)
29 | self.backgroundColor = 'lightGray'
30 | self:display(value, timeout)
31 | end
32 |
33 | function UI.Notification:success(value, timeout)
34 | self.backgroundColor = 'green'
35 | self:display(value, timeout)
36 | end
37 |
38 | function UI.Notification:cancel()
39 | if self.timer then
40 | Event.off(self.timer)
41 | self.timer = nil
42 | end
43 |
44 | self:disable()
45 | end
46 |
47 | function UI.Notification:display(value, timeout)
48 | local lines = Util.wordWrap(value, self.width - 3)
49 |
50 | self.enabled = true
51 | self.height = #lines
52 |
53 | if self.anchor == 'bottom' then
54 | self.y = self.parent.height - self.height + 1
55 | self:addTransition('expandUp', { ticks = self.height })
56 | else
57 | self.y = 1
58 | end
59 |
60 | self:reposition(self.x, self.y, self.width, self.height)
61 | self:raise()
62 | self:clear()
63 | for k,v in pairs(lines) do
64 | self:write(2, k, v)
65 | end
66 | self:write(self.width, 1, self.closeInd)
67 |
68 | if self.timer then
69 | Event.off(self.timer)
70 | self.timer = nil
71 | end
72 |
73 | timeout = timeout or self.timeout
74 | if timeout > 0 then
75 | self.timer = Event.onTimeout(timeout, function()
76 | self:cancel()
77 | self:sync()
78 | end)
79 | else
80 | self:sync()
81 | end
82 | end
83 |
84 | function UI.Notification:eventHandler(event)
85 | if event.type == 'mouse_click' then
86 | if event.x == self.width then
87 | self:cancel()
88 | return true
89 | end
90 | end
91 | end
92 |
93 | function UI.Notification.example()
94 | return UI.Window {
95 | notify1 = UI.Notification {
96 | anchor = 'top',
97 | },
98 | notify2 = UI.Notification { },
99 | button1 = UI.Button {
100 | x = 2, y = 3,
101 | text = 'example 1',
102 | event = 'test_success',
103 | },
104 | button2 = UI.Button {
105 | x = 2, y = 5,
106 | text = 'example 2',
107 | event = 'test_error',
108 | },
109 | eventHandler = function (self, event)
110 | if event.type == 'test_success' then
111 | self.notify1:success('Example text')
112 | elseif event.type == 'test_error' then
113 | self.notify2:error([[Example text test test
114 | test test test test test
115 | test test test]], 0)
116 | end
117 | end,
118 | }
119 | end
120 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Page.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | UI.Page = class(UI.Window)
6 | UI.Page.defaults = {
7 | UIElement = 'Page',
8 | accelerators = {
9 | down = 'focus_next',
10 | scroll_down = 'focus_next',
11 | enter = 'focus_next',
12 | tab = 'focus_next',
13 | ['shift-tab' ] = 'focus_prev',
14 | up = 'focus_prev',
15 | scroll_up = 'focus_prev',
16 | },
17 | backgroundColor = 'primary',
18 | textColor = 'white',
19 | }
20 | function UI.Page:postInit()
21 | self.parent = self.parent or UI.term
22 | self.__target = self
23 | end
24 |
25 | function UI.Page:sync()
26 | if self.enabled then
27 | self:checkFocus()
28 | self.parent:setCursorBlink(self.focused and self.focused.cursorBlink)
29 | self.parent:sync()
30 | end
31 | end
32 |
33 | function UI.Page:capture(child)
34 | self.__target = child
35 | end
36 |
37 | function UI.Page:release(child)
38 | if self.__target == child then
39 | self.__target = self
40 | end
41 | end
42 |
43 | function UI.Page:pointToChild(x, y)
44 | if self.__target == self then
45 | return UI.Window.pointToChild(self, x, y)
46 | end
47 |
48 | local function getPosition(element)
49 | local x, y = 1, 1
50 | repeat
51 | x = element.x + x - 1
52 | y = element.y + y - 1
53 | element = element.parent
54 | until not element
55 | return x, y
56 | end
57 |
58 | local absX, absY = getPosition(self.__target)
59 | return self.__target:pointToChild(x - absX + self.__target.x, y - absY + self.__target.y)
60 | end
61 |
62 | function UI.Page:getFocusables()
63 | if self.__target == self or not self.__target.modal then
64 | return UI.Window.getFocusables(self)
65 | end
66 | return self.__target:getFocusables()
67 | end
68 |
69 | function UI.Page:getFocused()
70 | return self.focused
71 | end
72 |
73 | function UI.Page:focusPrevious()
74 | local function getPreviousFocus(focused)
75 | local focusables = self:getFocusables()
76 | local k = Util.contains(focusables, focused)
77 | if k then
78 | if k > 1 then
79 | return focusables[k - 1]
80 | end
81 | return focusables[#focusables]
82 | end
83 | end
84 |
85 | local focused = getPreviousFocus(self.focused)
86 | if focused then
87 | self:setFocus(focused)
88 | end
89 | end
90 |
91 | function UI.Page:focusNext()
92 | local function getNextFocus(focused)
93 | local focusables = self:getFocusables()
94 | local k = Util.contains(focusables, focused)
95 | if k then
96 | if k < #focusables then
97 | return focusables[k + 1]
98 | end
99 | return focusables[1]
100 | end
101 | end
102 |
103 | local focused = getNextFocus(self.focused)
104 | if focused then
105 | self:setFocus(focused)
106 | end
107 | end
108 |
109 | function UI.Page:setFocus(child)
110 | if not child or not child.focus then
111 | return
112 | end
113 |
114 | if self.focused and self.focused ~= child then
115 | self.focused.focused = false
116 | self.focused:focus()
117 | self.focused:emit({ type = 'focus_lost', focused = child, unfocused = self.focused })
118 | end
119 |
120 | self.focused = child
121 | if not child.focused then
122 | child.focused = true
123 | child:emit({ type = 'focus_change', focused = child })
124 | end
125 |
126 | child:focus()
127 | end
128 |
129 | function UI.Page:checkFocus()
130 | if not self.focused or not self.focused.enabled then
131 | self.__target:focusFirst()
132 | end
133 | end
134 |
135 | function UI.Page:eventHandler(event)
136 | if self.focused then
137 | if event.type == 'focus_next' then
138 | self:focusNext()
139 | return true
140 | elseif event.type == 'focus_prev' then
141 | self:focusPrevious()
142 | return true
143 | end
144 | end
145 | end
146 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/ProgressBar.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.ProgressBar = class(UI.Window)
5 | UI.ProgressBar.defaults = {
6 | UIElement = 'ProgressBar',
7 | backgroundColor = 'gray',
8 | height = 1,
9 | progressColor = 'lime',
10 | progressChar = UI.extChars and '\153' or ' ',
11 | fillChar = ' ',
12 | fillColor = 'gray',
13 | textColor = 'green',
14 | value = 0,
15 | }
16 | function UI.ProgressBar:draw()
17 | local width = math.ceil(self.value / 100 * self.width)
18 |
19 | self:fillArea(width + 1, 1, self.width - width, self.height, self.fillChar, nil, self.fillColor)
20 | self:fillArea(1, 1, width, self.height, self.progressChar, self.progressColor)
21 | end
22 |
23 | function UI.ProgressBar.example()
24 | return UI.ProgressBar {
25 | x = 2, ex = -2, y = 2, height = 2,
26 | focus = function() end,
27 | enable = function(self)
28 | require('opus.event').onInterval(.25, function()
29 | self.value = self.value == 100 and 0 or self.value + 5
30 | self:draw()
31 | self:sync()
32 | end)
33 | return UI.ProgressBar.enable(self)
34 | end
35 | }
36 | end
37 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Question.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.Question = class(UI.MiniSlideOut)
5 | UI.Question.defaults = {
6 | UIElement = 'Question',
7 | accelerators = {
8 | y = 'question_yes',
9 | n = 'question_no',
10 | }
11 | }
12 | function UI.Question:postInit()
13 | local x = self.label and #self.label + 3 or 1
14 |
15 | self.yes_button = UI.Button {
16 | x = x,
17 | text = 'Yes',
18 | backgroundColor = 'primary',
19 | event = 'question_yes',
20 | }
21 | self.no_button = UI.Button {
22 | x = x + 5,
23 | text = 'No',
24 | backgroundColor = 'primary',
25 | event = 'question_no',
26 | }
27 | end
28 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/QuickSelect.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local fuzzy = require('opus.fuzzy')
3 | local UI = require('opus.ui')
4 |
5 | local fs = _G.fs
6 | local _insert = table.insert
7 |
8 | UI.QuickSelect = class(UI.Window)
9 | UI.QuickSelect.defaults = {
10 | UIElement = 'QuickSelect',
11 | }
12 | function UI.QuickSelect:postInit()
13 | self.filterEntry = UI.TextEntry {
14 | x = 2, y = 2, ex = -2,
15 | shadowText = 'File name',
16 | accelerators = {
17 | [ 'enter' ] = 'accept',
18 | [ 'up' ] = 'grid_up',
19 | [ 'down' ] = 'grid_down',
20 | },
21 | }
22 | self.grid = UI.ScrollingGrid {
23 | x = 2, y = 3, ex = -2, ey = -4,
24 | disableHeader = true,
25 | columns = {
26 | { key = 'name' },
27 | { key = 'dir', textColor = 'lightGray' },
28 | },
29 | accelerators = {
30 | grid_select = 'accept',
31 | },
32 | }
33 | self.cancel = UI.Button {
34 | x = -9, y = -2,
35 | text = 'Cancel',
36 | event = 'select_cancel',
37 | }
38 | end
39 |
40 | function UI.QuickSelect:draw()
41 | self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), 'black', 'gray')
42 | self:drawChildren()
43 | end
44 |
45 | function UI.QuickSelect:applyFilter(filter)
46 | if filter then
47 | filter = filter:lower()
48 | self.grid.sortColumn = 'score'
49 |
50 | for _,v in pairs(self.grid.values) do
51 | v.score = -fuzzy(v.lname, filter)
52 | end
53 | else
54 | self.grid.sortColumn = 'lname'
55 | end
56 |
57 | self.grid:update()
58 | self.grid:setIndex(1)
59 | end
60 |
61 | function UI.QuickSelect.getFiles()
62 | local t = { }
63 | local function recurse(dir)
64 | local files = fs.list(dir)
65 | for _,f in ipairs(files) do
66 | local fullName = fs.combine(dir, f)
67 | if fs.isDir(fullName) then
68 | -- skip virtual dirs
69 | if f ~= '.git' and fs.native.isDir(fullName) then
70 | recurse(fullName)
71 | end
72 | else
73 | _insert(t, {
74 | name = f,
75 | dir = dir,
76 | lname = f:lower(),
77 | fullName = fullName,
78 | })
79 | end
80 | end
81 | end
82 | recurse('')
83 | return t
84 | end
85 |
86 | function UI.QuickSelect:enable()
87 | self.grid.values = self:getFiles()
88 | self:applyFilter()
89 | self.filterEntry:reset()
90 | UI.Window.enable(self)
91 | end
92 |
93 | function UI.QuickSelect:eventHandler(event)
94 | if event.type == 'grid_up' then
95 | self.grid:emit({ type = 'scroll_up' })
96 | return true
97 |
98 | elseif event.type == 'grid_down' then
99 | self.grid:emit({ type = 'scroll_down' })
100 | return true
101 |
102 | elseif event.type == 'accept' then
103 | local sel = self.grid:getSelected()
104 | if sel then
105 | self:emit({ type = 'select_file', file = sel.fullName, element = self })
106 | end
107 | return true
108 |
109 | elseif event.type == 'text_change' then
110 | self:applyFilter(event.text)
111 | self.grid:draw()
112 | return true
113 |
114 | end
115 | end
116 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/ScrollBar.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | local colors = _G.colors
6 |
7 | UI.ScrollBar = class(UI.Window)
8 | UI.ScrollBar.defaults = {
9 | UIElement = 'ScrollBar',
10 | lineChar = '\166',
11 | sliderChar = UI.extChars and '\127' or '#',
12 | upArrowChar = UI.extChars and '\30' or '^',
13 | downArrowChar = UI.extChars and '\31' or 'v',
14 | scrollbarColor = colors.lightGray,
15 | width = 1,
16 | x = -1,
17 | ey = -1,
18 | }
19 | function UI.ScrollBar:draw()
20 | local parent = self.target or self.parent --self:find(self.target)
21 | local view = parent:getViewArea()
22 |
23 | self:clear()
24 |
25 | -- ...
26 | self:write(1, 1, ' ', view.fill)
27 |
28 | if view.totalHeight > view.height then
29 | local maxScroll = view.totalHeight - view.height
30 | local percent = view.offsetY / maxScroll
31 | local sliderSize = math.max(1, Util.round(view.height / view.totalHeight * (view.height - 2)))
32 | local x = self.width
33 |
34 | local row = view.y
35 | if not view.static then -- does the container scroll ?
36 | self:reposition(self.x, self.y, self.width, view.totalHeight)
37 | end
38 |
39 | for i = 1, view.height - 2 do
40 | self:write(x, row + i, self.lineChar, nil, self.scrollbarColor)
41 | end
42 |
43 | local y = Util.round((view.height - 2 - sliderSize) * percent)
44 | for i = 1, sliderSize do
45 | self:write(x, row + y + i, self.sliderChar, nil, self.scrollbarColor)
46 | end
47 |
48 | local color = self.scrollbarColor
49 | if view.offsetY > 0 then
50 | color = colors.white
51 | end
52 | self:write(x, row, self.upArrowChar, nil, color)
53 |
54 | color = self.scrollbarColor
55 | if view.offsetY + view.height < view.totalHeight then
56 | color = colors.white
57 | end
58 | self:write(x, row + view.height - 1, self.downArrowChar, nil, color)
59 | end
60 | end
61 |
62 | function UI.ScrollBar:eventHandler(event)
63 | if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
64 | if event.x == 1 then
65 | local parent = self.target or self.parent --self:find(self.target)
66 | local view = parent:getViewArea()
67 | if view.totalHeight > view.height then
68 | if event.y == view.y then
69 | parent:emit({ type = 'scroll_up'})
70 | elseif event.y == view.y + view.height - 1 then
71 | parent:emit({ type = 'scroll_down'})
72 | else
73 | local percent = (event.y - view.y) / (view.height - 2)
74 | local y = math.floor((view.totalHeight - view.height) * percent)
75 | parent :emit({ type = 'scroll_to', offset = y })
76 | end
77 | end
78 | return true
79 | end
80 | end
81 | end
82 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/ScrollingGrid.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | UI.ScrollingGrid = class(UI.Grid)
6 | UI.ScrollingGrid.defaults = {
7 | UIElement = 'ScrollingGrid',
8 | scrollOffset = 0,
9 | marginRight = 1,
10 | }
11 | function UI.ScrollingGrid:postInit()
12 | self.scrollBar = UI.ScrollBar()
13 | end
14 |
15 | function UI.ScrollingGrid:drawRows()
16 | UI.Grid.drawRows(self)
17 | self.scrollBar:draw()
18 | end
19 |
20 | function UI.ScrollingGrid:getViewArea()
21 | local y = 1
22 | if not self.disableHeader then
23 | y = y + self.headerHeight
24 | end
25 |
26 | return {
27 | static = true, -- the container doesn't scroll
28 | y = y, -- scrollbar Y
29 | height = self.pageSize, -- viewable height
30 | totalHeight = Util.size(self.values), -- total height
31 | offsetY = self.scrollOffset, -- scroll offset
32 | fill = not self.disableHeader and self.headerBackgroundColor,
33 | }
34 | end
35 |
36 | function UI.ScrollingGrid:getStartRow()
37 | local ts = Util.size(self.values)
38 | if ts < self.pageSize then
39 | self.scrollOffset = 0
40 | end
41 | return self.scrollOffset + 1
42 | end
43 |
44 | function UI.ScrollingGrid:setIndex(index)
45 | if index < self.scrollOffset + 1 then
46 | self.scrollOffset = index - 1
47 | elseif index - self.scrollOffset > self.pageSize then
48 | self.scrollOffset = index - self.pageSize
49 | end
50 |
51 | if self.scrollOffset < 0 then
52 | self.scrollOffset = 0
53 | else
54 | local ts = Util.size(self.values)
55 | if self.pageSize + self.scrollOffset + 1 > ts then
56 | self.scrollOffset = math.max(0, ts - self.pageSize)
57 | end
58 | end
59 | UI.Grid.setIndex(self, index)
60 | end
61 |
62 | function UI.ScrollingGrid.example()
63 | local values = { }
64 | for i = 1, 20 do
65 | table.insert(values, { key = 'key' .. i, value = 'value' .. i })
66 | end
67 | return UI.ScrollingGrid {
68 | values = values,
69 | sortColumn = 'key',
70 | columns = {
71 | { heading = 'key', key = 'key' },
72 | { heading = 'value', key = 'value' },
73 | },
74 | accelerators = {
75 | grid_select = 'custom_select',
76 | }
77 | }
78 | end
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/SlideOut.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.SlideOut = class(UI.Window)
5 | UI.SlideOut.defaults = {
6 | UIElement = 'SlideOut',
7 | transitionHint = 'expandUp',
8 | modal = true,
9 | }
10 | function UI.SlideOut:enable()
11 | end
12 |
13 | function UI.SlideOut:toggle()
14 | if self.enabled then
15 | self:hide()
16 | else
17 | self:show()
18 | end
19 | end
20 |
21 | function UI.SlideOut:show(...)
22 | UI.Window.enable(self, ...)
23 | self:draw()
24 | self:focusFirst()
25 | end
26 |
27 | function UI.SlideOut:hide()
28 | self:disable()
29 | end
30 |
31 | function UI.SlideOut:draw()
32 | if not self.noFill then
33 | self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), 'black', 'gray')
34 | end
35 | self:drawChildren()
36 | end
37 |
38 | function UI.SlideOut:eventHandler(event)
39 | if event.type == 'slide_show' then
40 | self:show()
41 | return true
42 |
43 | elseif event.type == 'slide_hide' then
44 | self:hide()
45 | return true
46 | end
47 | end
48 |
49 | function UI.SlideOut.example()
50 | return UI.Window {
51 | y = 3,
52 | backgroundColor = 2048,
53 | button = UI.Button {
54 | x = 2, y = 5,
55 | text = 'show',
56 | },
57 | slideOut = UI.SlideOut {
58 | backgroundColor = 16,
59 | y = -7, height = 4, x = 3, ex = -3,
60 | titleBar = UI.TitleBar {
61 | title = 'test',
62 | },
63 | button = UI.Button {
64 | x = 2, y = 2,
65 | text = 'hide',
66 | --visualize = true,
67 | },
68 | },
69 | eventHandler = function (self, event)
70 | if event.type == 'button_press' then
71 | self.slideOut:toggle()
72 | end
73 | end,
74 | }
75 | end
76 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Slider.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | UI.Slider = class(UI.Window)
6 | UI.Slider.defaults = {
7 | UIElement = 'Slider',
8 | height = 1,
9 | barChar = UI.extChars and '\140' or '-',
10 | barColor = 'gray',
11 | sliderChar = UI.extChars and '\143' or '\124',
12 | sliderColor = 'blue',
13 | sliderFocusColor = 'lightBlue',
14 | leftBorder = UI.extChars and '\141' or '\124',
15 | rightBorder = UI.extChars and '\142' or '\124',
16 | labelWidth = 0,
17 | value = 0,
18 | min = 0,
19 | max = 100,
20 | event = 'slider_update',
21 | transform = function(v) return Util.round(v, 2) end,
22 | accelerators = {
23 | right = 'slide_right',
24 | left = 'slide_left',
25 | }
26 | }
27 | function UI.Slider:setValue(value)
28 | self.value = self.transform(tonumber(value) or self.min)
29 | self.value = Util.clamp(self.value, self.min, self.max)
30 | self:draw()
31 | end
32 |
33 | function UI.Slider:reset() -- form support
34 | self.value = self.min
35 | self:draw()
36 | end
37 |
38 | function UI.Slider:focus()
39 | self:draw()
40 | end
41 |
42 | function UI.Slider:getSliderWidth()
43 | local labelWidth = self.labelWidth > 0 and self.labelWidth + 1
44 | return self.width - (labelWidth or 0)
45 | end
46 |
47 | function UI.Slider:draw()
48 | local labelWidth = self.labelWidth > 0 and self.labelWidth + 1
49 | local width = self.width - (labelWidth or 0)
50 | local range = self.max - self.min
51 | local perc = (self.value - self.min) / range
52 | local progress = Util.clamp(1 + width * perc, 1, width)
53 |
54 | local bar = self.leftBorder .. string.rep(self.barChar, width - 2) .. self.rightBorder
55 | self:write(1, 1, bar, nil, self.barColor)
56 | self:write(progress, 1, self.sliderChar, nil, self.focused and self.sliderFocusColor or self.sliderColor)
57 | if labelWidth then
58 | self:write(self.width - labelWidth + 2, 1, Util.widthify(tostring(self.value), self.labelWidth))
59 | end
60 | end
61 |
62 | function UI.Slider:eventHandler(event)
63 | if event.type == "mouse_down" or event.type == "mouse_drag" then
64 | local pos = event.x - 1
65 | if event.type == 'mouse_down' then
66 | self.anchor = event.x - 1
67 | else
68 | pos = self.anchor + event.dx
69 | end
70 | local range = self.max - self.min
71 | local i = pos / (self:getSliderWidth() - 1)
72 | self:setValue(self.min + (i * range))
73 | self:emit({ type = self.event, value = self.value, element = self })
74 | return true
75 |
76 | elseif event.type == 'slide_left' or event.type == 'slide_right' then
77 | local range = self.max - self.min
78 | local step = range / (self:getSliderWidth() - 1)
79 | if event.type == 'slide_left' then
80 | self:setValue(self.value - step)
81 | else
82 | self:setValue(self.value + step)
83 | end
84 | self:emit({ type = self.event, value = self.value, element = self })
85 | return true
86 | end
87 | end
88 |
89 | function UI.Slider.example()
90 | return UI.Window {
91 | UI.Slider {
92 | y = 2, x = 2, ex = -2,
93 | min = 0, max = 1,
94 | },
95 | UI.Slider {
96 | y = 4, x = 2, ex = -2,
97 | min = -1, max = 1,
98 | labelWidth = 5,
99 | },
100 | UI.Slider {
101 | y = 6, x = 2, ex = -2,
102 | min = 0, max = 100,
103 | labelWidth = 3,
104 | transform = math.floor,
105 | },
106 | }
107 | end
108 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/StatusBar.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local Event = require('opus.event')
3 | local UI = require('opus.ui')
4 | local Util = require('opus.util')
5 |
6 | UI.StatusBar = class(UI.Window)
7 | UI.StatusBar.defaults = {
8 | UIElement = 'StatusBar',
9 | backgroundColor = 'lightGray',
10 | textColor = 'gray',
11 | height = 1,
12 | ey = -1,
13 | }
14 | function UI.StatusBar:layout()
15 | UI.Window.layout(self)
16 | -- Can only have 1 adjustable width
17 | if self.columns then
18 | local w = self.width - #self.columns - 1
19 | for _,c in pairs(self.columns) do
20 | if c.width then
21 | c.cw = c.width -- computed width
22 | w = w - c.width
23 | end
24 | end
25 | for _,c in pairs(self.columns) do
26 | if not c.width then
27 | c.cw = w
28 | end
29 | end
30 | end
31 | end
32 |
33 | function UI.StatusBar:setStatus(status)
34 | if self.values ~= status then
35 | self.values = status
36 | self:draw()
37 | end
38 | end
39 |
40 | function UI.StatusBar:setValue(name, value)
41 | if not self.values then
42 | self.values = { }
43 | end
44 | self.values[name] = value
45 | end
46 |
47 | function UI.StatusBar:getValue(name)
48 | if self.values then
49 | return self.values[name]
50 | end
51 | end
52 |
53 | function UI.StatusBar:timedStatus(status, timeout)
54 | self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor)
55 | Event.onTimeout(timeout or 3, function()
56 | if self.enabled then
57 | self:draw()
58 | self:sync()
59 | end
60 | end)
61 | end
62 |
63 | function UI.StatusBar:getColumnWidth(name)
64 | local c = Util.find(self.columns, 'key', name)
65 | return c and c.cw
66 | end
67 |
68 | function UI.StatusBar:setColumnWidth(name, width)
69 | local c = Util.find(self.columns, 'key', name)
70 | if c then
71 | c.cw = width
72 | end
73 | end
74 |
75 | function UI.StatusBar:draw()
76 | if not self.values then
77 | self:clear()
78 | elseif type(self.values) == 'string' then
79 | self:write(1, 1, Util.widthify(' ' .. self.values, self.width))
80 | else
81 | local x = 2
82 | self:clear()
83 | for _,c in ipairs(self.columns) do
84 | local s = Util.widthify(tostring(self.values[c.key] or ''), c.cw)
85 | self:write(x, 1, s, c.bg, c.fg)
86 | x = x + c.cw + 1
87 | end
88 | end
89 | end
90 |
91 | function UI.StatusBar.example()
92 | return UI.Window {
93 | status1 = UI.StatusBar { values = 'standard' },
94 | status2 = UI.StatusBar {
95 | ey = -3,
96 | columns = {
97 | { key = 'field1' },
98 | { key = 'field2', width = 6 },
99 | },
100 | values = {
101 | field1 = 'test',
102 | field2 = '42',
103 | }
104 | }
105 | }
106 | end
107 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Tab.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.Tab = class(UI.Window)
5 | UI.Tab.defaults = {
6 | UIElement = 'Tab',
7 | title = 'tab',
8 | y = 2,
9 | }
10 |
11 | function UI.Tab:draw()
12 | if not self.noFill then
13 | self:fillArea(1, 1, self.width, self.height, string.rep('\127', self.width), colors.black, colors.gray)
14 | end
15 | self:drawChildren()
16 | end
17 |
18 | function UI.Tab:enable()
19 | UI.Window.enable(self)
20 | self:emit({ type = 'tab_activate', activated = self })
21 | end
22 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/TabBar.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | UI.TabBar = class(UI.MenuBar)
6 | UI.TabBar.defaults = {
7 | UIElement = 'TabBar',
8 | buttonClass = 'TabBarMenuItem',
9 | backgroundColor = 'black',
10 | }
11 | function UI.TabBar:enable()
12 | UI.MenuBar.enable(self)
13 | if not Util.find(self.children, 'selected', true) then
14 | local menuItem = self:getFocusables()[1]
15 | if menuItem then
16 | menuItem.selected = true
17 | end
18 | end
19 | end
20 |
21 | function UI.TabBar:eventHandler(event)
22 | if event.type == 'tab_select' then
23 | local selected, si = Util.find(self.children, 'uid', event.button.uid)
24 | local previous, pi = Util.find(self.children, 'selected', true)
25 |
26 | if si ~= pi then
27 | selected.selected = true
28 | if previous then
29 | previous.selected = false
30 | self:emit({ type = 'tab_change', current = si, last = pi, tab = selected })
31 | end
32 | end
33 | self:draw(self)
34 | end
35 | return UI.MenuBar.eventHandler(self, event)
36 | end
37 |
38 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/TabBarMenuItem.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.TabBarMenuItem = class(UI.Button)
5 | UI.TabBarMenuItem.defaults = {
6 | UIElement = 'TabBarMenuItem',
7 | event = 'tab_select',
8 | }
9 | function UI.TabBarMenuItem:draw()
10 | if self.selected then
11 | self.backgroundColor = self:getProperty('selectedBackgroundColor')
12 | self.backgroundFocusColor = self.backgroundColor
13 | self.textColor = self:getProperty('selectedTextColor')
14 | else
15 | self.backgroundColor = self:getProperty('unselectedBackgroundColor')
16 | self.backgroundFocusColor = self.backgroundColor
17 | self.textColor = self:getProperty('unselectedTextColor')
18 | end
19 | UI.Button.draw(self)
20 | end
21 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Tabs.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | UI.Tabs = class(UI.Window)
6 | UI.Tabs.docs = { }
7 | UI.Tabs.defaults = {
8 | UIElement = 'Tabs',
9 | selectedBackgroundColor = 'primary',
10 | unselectedBackgroundColor = 'tertiary',
11 | unselectedTextColor = 'lightGray',
12 | selectedTextColor = 'black',
13 | }
14 | function UI.Tabs:postInit()
15 | self:add(self)
16 | end
17 |
18 | function UI.Tabs:add(children)
19 | local buttons = { }
20 | for _,child in pairs(children) do
21 | if type(child) == 'table' and child.UIElement and child.UIElement == 'Tab' then
22 | child.y = 2
23 | table.insert(buttons, {
24 | index = child.index,
25 | text = child.title,
26 | event = 'tab_select',
27 | tabUid = child.uid,
28 | })
29 | end
30 | end
31 |
32 | if not self.tabBar then
33 | self.tabBar = UI.TabBar({
34 | buttons = buttons,
35 | backgroundColor = self.barBackgroundColor,
36 | })
37 | else
38 | self.tabBar:addButtons(buttons)
39 | end
40 |
41 | if self.parent then
42 | local enabled = self.enabled
43 |
44 | -- don't enable children upon add
45 | self.enabled = nil
46 | UI.Window.add(self, children)
47 | self.enabled = enabled
48 | end
49 | end
50 |
51 | UI.Tabs.docs.selectTab = [[selectTab(TAB)
52 | Make to the passed tab active.]]
53 | function UI.Tabs:selectTab(tab)
54 | local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid)
55 | if menuItem then
56 | if self.enabled then
57 | self.tabBar:emit({ type = 'tab_select', button = { uid = menuItem.uid } })
58 | else
59 | local previous = Util.find(self.tabBar.children, 'selected', true)
60 | if previous then
61 | previous.selected = false
62 | end
63 | menuItem.selected = true
64 | end
65 | end
66 | end
67 |
68 | function UI.Tabs:setActive(tab, active)
69 | local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid)
70 | if menuItem then
71 | menuItem.inactive = not active
72 | end
73 | end
74 |
75 | function UI.Tabs:enable()
76 | self.enabled = true
77 | self.tabBar:enable()
78 |
79 | local menuItem = Util.find(self.tabBar.children, 'selected', true)
80 | self:enableTab(menuItem.tabUid)
81 | end
82 |
83 | function UI.Tabs:enableTab(tabUid, hint)
84 | for child in self:eachChild() do
85 | child.transitionHint = hint
86 | if child.uid == tabUid then
87 | if not child.enabled then
88 | child:enable()
89 | end
90 | elseif child.UIElement == 'Tab' then
91 | if child.enabled then
92 | child:disable()
93 | end
94 | end
95 | end
96 | end
97 |
98 | function UI.Tabs:eventHandler(event)
99 | if event.type == 'tab_change' then
100 | local tab = self:find(event.tab.tabUid)
101 | local hint = event.current > event.last and 'slideLeft' or 'slideRight'
102 |
103 | self:enableTab(event.tab.tabUid, hint)
104 | tab:draw()
105 | return true
106 | end
107 | end
108 |
109 | function UI.Tabs.example()
110 | return UI.Tabs {
111 | tab1 = UI.Tab {
112 | index = 1,
113 | title = 'tab1',
114 | entry = UI.TextEntry { y = 3, shadowText = 'text' },
115 | },
116 | tab2 = UI.Tab {
117 | index = 2,
118 | title = 'tab2',
119 | subtabs = UI.Tabs {
120 | x = 3, y = 2, ex = -3, ey = -2,
121 | tab1 = UI.Tab {
122 | index = 1,
123 | title = 'tab4',
124 | entry = UI.TextEntry { y = 3, shadowText = 'text' },
125 | },
126 | tab3 = UI.Tab {
127 | index = 2,
128 | title = 'tab5',
129 | },
130 | },
131 | },
132 | tab3 = UI.Tab {
133 | index = 3,
134 | title = 'tab3',
135 | },
136 | enable = function(self)
137 | UI.Tabs.enable(self)
138 | self:setActive(self.tab3, false)
139 | end,
140 | }
141 | end
142 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Text.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 | local Util = require('opus.util')
4 |
5 | UI.Text = class(UI.Window)
6 | UI.Text.defaults = {
7 | UIElement = 'Text',
8 | value = '',
9 | height = 1,
10 | }
11 | function UI.Text:layout()
12 | if not self.width and not self.ex then
13 | self.width = #tostring(self.value)
14 | end
15 | UI.Window.layout(self)
16 | end
17 |
18 | function UI.Text:draw()
19 | self:write(1, 1, Util.widthify(self.value, self.width, self.align))
20 | end
21 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/TextArea.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.TextArea = class(UI.Viewport)
5 | UI.TextArea.defaults = {
6 | UIElement = 'TextArea',
7 | marginRight = 2,
8 | value = '',
9 | showScrollBar = true,
10 | }
11 | function UI.TextArea:setValue(text)
12 | self:reset()
13 | self.value = text
14 | self:draw()
15 | end
16 | UI.TextArea.setText = UI.TextArea.setValue -- deprecate
17 |
18 | function UI.TextArea.focus()
19 | -- allow keyboard scrolling
20 | end
21 |
22 | function UI.TextArea:draw()
23 | self:clear()
24 | self:print(self.value)
25 | self:drawChildren()
26 | end
27 |
28 | function UI.TextArea.example()
29 | local Ansi = require('opus.ansi')
30 | return UI.Window {
31 | backgroundColor = 2048,
32 | t1 = UI.TextArea {
33 | ey = 3,
34 | value = 'sample text\nabc'
35 | },
36 | t2 = UI.TextArea {
37 | y = 5,
38 | backgroundColor = 'green',
39 | value = string.format([[now %%is the %stime %sfor%s all good men to come to the aid of their country.
40 | 1
41 | 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
42 | 3
43 | 4
44 | 5
45 | 6
46 | 7
47 | 8]], Ansi.yellow, Ansi.onred, Ansi.reset),
48 | }
49 | }
50 | end
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Throttle.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | local colors = _G.colors
5 | local os = _G.os
6 |
7 | UI.Throttle = class(UI.Window)
8 | UI.Throttle.defaults = {
9 | UIElement = 'Throttle',
10 | backgroundColor = colors.gray,
11 | bordercolor = colors.cyan,
12 | height = 4,
13 | width = 10,
14 | timeout = .075,
15 | ctr = 0,
16 | image = {
17 | ' //) (O )~@ &~&-( ?Q ',
18 | ' //) (O )- @ \\-( ?) && ',
19 | ' //) (O ), @ \\-(?) && ',
20 | ' //) (O ). @ \\-d ) (@ '
21 | }
22 | }
23 | function UI.Throttle:layout()
24 | self.x = math.ceil((self.parent.width - self.width) / 2)
25 | self.y = math.ceil((self.parent.height - self.height) / 2)
26 | self:reposition(self.x, self.y, self.width, self.height)
27 | end
28 |
29 | function UI.Throttle:enable()
30 | self.c = os.clock()
31 | self.ctr = 0
32 | end
33 |
34 | function UI.Throttle:update()
35 | local cc = os.clock()
36 | if cc > self.c + self.timeout then
37 | os.sleep(0)
38 | self.c = os.clock()
39 | self.enabled = true
40 | self:clear(self.borderColor)
41 | local image = self.image[self.ctr + 1]
42 | local width = self.width - 2
43 | for i = 0, #self.image do
44 | self:write(2, i + 1, image:sub(width * i + 1, width * i + width),
45 | self.backgroundColor, self.textColor)
46 | end
47 |
48 | self.ctr = (self.ctr + 1) % #self.image
49 |
50 | self:sync()
51 | end
52 | end
53 |
54 | function UI.Throttle.example()
55 | return UI.Window {
56 | button1 = UI.Button {
57 | x = 2, y = 2,
58 | text = 'Test',
59 | },
60 | throttle = UI.Throttle {
61 | textColor = colors.yellow,
62 | borderColor = colors.green,
63 | },
64 | eventHandler = function (self, event)
65 | if event.type == 'button_press' then
66 | for _ = 1, 40 do
67 | self.throttle:update()
68 | os.sleep(.05)
69 | end
70 | self.throttle:disable()
71 | end
72 | end,
73 | }
74 | end
75 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/TitleBar.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.TitleBar = class(UI.Window)
5 | UI.TitleBar.defaults = {
6 | UIElement = 'TitleBar',
7 | height = 1,
8 | title = '',
9 | frameChar = UI.extChars and '\140' or '-',
10 | closeInd = UI.extChars and '\215' or '*',
11 | }
12 | function UI.TitleBar:draw()
13 | self:fillArea(2, 1, self.width - 2, 1, self.frameChar)
14 | self:centeredWrite(1, string.format(' %s ', self.title))
15 | if self.previousPage or self.event then
16 | self:write(self.width - 1, 1, ' ' .. self.closeInd)
17 | end
18 | end
19 |
20 | function UI.TitleBar:eventHandler(event)
21 | if event.type == 'mouse_click' then
22 | if (self.previousPage or self.event) and event.x == self.width then
23 | if self.event then
24 | self:emit({ type = self.event, element = self })
25 | elseif type(self.previousPage) == 'string' or
26 | type(self.previousPage) == 'table' then
27 | UI:setPage(self.previousPage)
28 | else
29 | UI:setPreviousPage()
30 | end
31 | return true
32 | end
33 |
34 | elseif event.type == 'mouse_down' then
35 | self.anchor = { x = event.x, y = event.y, ox = self.parent.x, oy = self.parent.y, h = self.parent.height }
36 |
37 | elseif event.type == 'mouse_drag' then
38 | if self.expand == 'height' then
39 | local d = event.dy
40 | if self.anchor.h - d > 0 and self.anchor.oy + d > 0 then
41 | self.parent:reposition(self.parent.x, self.anchor.oy + event.dy, self.width, self.anchor.h - d)
42 | end
43 |
44 | elseif self.moveable then
45 | local d = event.dy
46 | if self.anchor.oy + d > 0 and self.anchor.oy + d <= self.parent.parent.height then
47 | self.parent:move(self.anchor.ox + event.dx, self.anchor.oy + event.dy)
48 | end
49 | end
50 | end
51 | end
52 |
53 | function UI.TitleBar.example()
54 | return UI.Window {
55 | win1 = UI.Window {
56 | x = 9, y = 2, ex = -7, ey = -3,
57 | backgroundColor = 'green',
58 | titleBar = UI.TitleBar {
59 | title = 'A really, really, really long title', moveable = true,
60 | },
61 | button1 = UI.Button {
62 | x = 2, y = 3,
63 | text = 'Press',
64 | },
65 | focus = function (self)
66 | self:raise()
67 | end,
68 | },
69 | win2 = UI.Window {
70 | x = 7, y = 3, ex = -9, ey = -2,
71 | backgroundColor = 'orange',
72 | titleBar = UI.TitleBar {
73 | title = 'test', moveable = true,
74 | event = 'none',
75 | },
76 | button1 = UI.Button {
77 | x = 2, y = 3,
78 | text = 'Press',
79 | },
80 | focus = UI.Window.raise,
81 | },
82 | draw = function(self, isBG)
83 | for i = 1, self.height do
84 | self:write(1, i, self.filler or '')
85 | end
86 | if not isBG then
87 | for _,v in pairs(self.children) do
88 | v:draw()
89 | end
90 | end
91 | end,
92 | enable = function (self)
93 | require('opus.event').onInterval(.5, function()
94 | self.filler = string.rep(string.char(math.random(33, 126)), self.width)
95 | self:draw(true)
96 | self:sync()
97 | end)
98 | UI.Window.enable(self)
99 | end
100 | }
101 | end
102 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/VerticalMeter.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.VerticalMeter = class(UI.Window)
5 | UI.VerticalMeter.defaults = {
6 | UIElement = 'VerticalMeter',
7 | backgroundColor = 'gray',
8 | meterColor = 'lime',
9 | width = 1,
10 | value = 0,
11 | }
12 | function UI.VerticalMeter:draw()
13 | local height = self.height - math.ceil(self.value / 100 * self.height)
14 | self:clear()
15 | self:clearArea(1, height + 1, self.width, self.height, self.meterColor)
16 | end
17 |
18 | function UI.VerticalMeter.example()
19 | return UI.VerticalMeter {
20 | x = 2, width = 3, y = 2, ey = -2,
21 | focus = function() end,
22 | enable = function(self)
23 | require('opus.event').onInterval(.25, function()
24 | self.value = self.value == 100 and 0 or self.value + 5
25 | self:draw()
26 | self:sync()
27 | end)
28 | return UI.VerticalMeter.enable(self)
29 | end
30 | }
31 | end
32 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/Viewport.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.Viewport = class(UI.Window)
5 | UI.Viewport.defaults = {
6 | UIElement = 'Viewport',
7 | accelerators = {
8 | down = 'scroll_down',
9 | up = 'scroll_up',
10 | home = 'scroll_top',
11 | left = 'scroll_left',
12 | right = 'scroll_right',
13 | [ 'end' ] = 'scroll_bottom',
14 | pageUp = 'scroll_pageUp',
15 | [ 'control-b' ] = 'scroll_pageUp',
16 | pageDown = 'scroll_pageDown',
17 | [ 'control-f' ] = 'scroll_pageDown',
18 | },
19 | }
20 | function UI.Viewport:postInit()
21 | if self.showScrollBar then
22 | self.scrollBar = UI.ScrollBar()
23 | end
24 | end
25 |
26 | function UI.Viewport:setScrollPosition(offy, offx) -- argh - reverse
27 | local oldOffy = self.offy
28 | self.offy = math.max(offy, 0)
29 | self.offy = math.min(self.offy, math.max(#self.lines, self.height) - self.height)
30 | if self.offy ~= oldOffy then
31 | if self.scrollBar then
32 | self.scrollBar:draw()
33 | end
34 | self.offy = offy
35 | self:dirty(true)
36 | end
37 |
38 | local oldOffx = self.offx
39 | self.offx = math.max(offx or 0, 0)
40 | self.offx = math.min(self.offx, math.max(#self.lines[1], self.width) - self.width)
41 | if self.offx ~= oldOffx then
42 | if self.scrollBar then
43 | --self.scrollBar:draw()
44 | end
45 | self.offx = offx or 0
46 | self:dirty(true)
47 | end
48 | end
49 |
50 | function UI.Viewport:blit(x, y, text, bg, fg)
51 | if y > #self.lines then
52 | self:resizeBuffer(self.width, y)
53 | end
54 | return UI.Window.blit(self, x, y, text, bg, fg)
55 | end
56 |
57 | function UI.Viewport:write(x, y, text, bg, fg)
58 | if y > #self.lines then
59 | self:resizeBuffer(self.width, y)
60 | end
61 | return UI.Window.write(self, x, y, text, bg, fg)
62 | end
63 |
64 | function UI.Viewport:setViewHeight(h)
65 | if h > #self.lines then
66 | self:resizeBuffer(self.width, h)
67 | end
68 | end
69 |
70 | function UI.Viewport:reset()
71 | self.offy = 0
72 | for i = self.height + 1, #self.lines do
73 | self.lines[i] = nil
74 | end
75 | end
76 |
77 | function UI.Viewport:getViewArea()
78 | return {
79 | y = (self.offy or 0) + 1,
80 | height = self.height,
81 | totalHeight = #self.lines,
82 | offsetY = self.offy or 0,
83 | }
84 | end
85 |
86 | function UI.Viewport:eventHandler(event)
87 | if #self.lines <= self.height then
88 | return
89 | end
90 | if event.type == 'scroll_down' then
91 | self:setScrollPosition(self.offy + 1, self.offx)
92 | elseif event.type == 'scroll_up' then
93 | self:setScrollPosition(self.offy - 1, self.offx)
94 | elseif event.type == 'scroll_left' then
95 | self:setScrollPosition(self.offy, self.offx - 1)
96 | elseif event.type == 'scroll_right' then
97 | self:setScrollPosition(self.offy, self.offx + 1)
98 | elseif event.type == 'scroll_top' then
99 | self:setScrollPosition(0, 0)
100 | elseif event.type == 'scroll_bottom' then
101 | self:setScrollPosition(10000000, 0)
102 | elseif event.type == 'scroll_pageUp' then
103 | self:setScrollPosition(self.offy - self.height, self.offx)
104 | elseif event.type == 'scroll_pageDown' then
105 | self:setScrollPosition(self.offy + self.height, self.offx)
106 | elseif event.type == 'scroll_to' then
107 | self:setScrollPosition(event.offset, 0)
108 | else
109 | return false
110 | end
111 | return true
112 | end
113 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/components/WizardPage.lua:
--------------------------------------------------------------------------------
1 | local class = require('opus.class')
2 | local UI = require('opus.ui')
3 |
4 | UI.WizardPage = class(UI.Window)
5 | UI.WizardPage.defaults = {
6 | UIElement = 'WizardPage',
7 | ey = -2,
8 | }
9 | function UI.WizardPage.validate()
10 | return true
11 | end
12 |
--------------------------------------------------------------------------------
/sys/modules/opus/ui/transition.lua:
--------------------------------------------------------------------------------
1 | local Tween = require('opus.ui.tween')
2 |
3 | local Transition = { }
4 |
5 | function Transition.slideLeft(canvas, args)
6 | local ticks = args.ticks or 6
7 | local easing = args.easing or 'inCirc'
8 | local pos = { x = canvas.ex }
9 | local tween = Tween.new(ticks, pos, { x = canvas.x }, easing)
10 |
11 | canvas:move(pos.x, canvas.y)
12 |
13 | return function()
14 | local finished = tween:update(1)
15 | canvas:move(math.floor(pos.x), canvas.y)
16 | canvas:dirty(true)
17 | return not finished
18 | end
19 | end
20 |
21 | function Transition.slideRight(canvas, args)
22 | local ticks = args.ticks or 6
23 | local easing = args.easing or 'inCirc'
24 | local pos = { x = -canvas.width }
25 | local tween = Tween.new(ticks, pos, { x = 1 }, easing)
26 |
27 | canvas:move(pos.x, canvas.y)
28 |
29 | return function()
30 | local finished = tween:update(1)
31 | canvas:move(math.floor(pos.x), canvas.y)
32 | canvas:dirty(true)
33 | return not finished
34 | end
35 | end
36 |
37 | function Transition.expandUp(canvas, args)
38 | local ticks = args.ticks or 3
39 | local easing = args.easing or 'linear'
40 | local pos = { y = canvas.ey + 1 }
41 | local tween = Tween.new(ticks, pos, { y = canvas.y }, easing)
42 |
43 | canvas:move(canvas.x, pos.y)
44 |
45 | return function()
46 | local finished = tween:update(1)
47 | canvas:move(canvas.x, math.floor(pos.y))
48 | canvas.parent:dirty(true)
49 | return not finished
50 | end
51 | end
52 |
53 | function Transition.shake(canvas, args)
54 | local ticks = args.ticks or 8
55 | local i = ticks
56 |
57 | return function()
58 | i = -i
59 | canvas:move(canvas.x + i, canvas.y)
60 | if i > 0 then
61 | i = i - 2
62 | end
63 | return i ~= 0
64 | end
65 | end
66 |
67 | function Transition.shuffle(canvas, args)
68 | local ticks = args.ticks or 4
69 | local easing = args.easing or 'linear'
70 | local t = { }
71 |
72 | for _,child in pairs(canvas.children) do
73 | t[child] = Tween.new(ticks, child, { x = child.x, y = child.y }, easing)
74 | child.x = math.random(1, canvas.parent.width)
75 | child.y = math.random(1, canvas.parent.height)
76 | end
77 |
78 | return function()
79 | local finished
80 | for child, tween in pairs(t) do
81 | finished = tween:update(1)
82 | child:move(math.floor(child.x), math.floor(child.y))
83 | end
84 | return not finished
85 | end
86 | end
87 |
88 | return Transition
89 |
--------------------------------------------------------------------------------