├── .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 | --------------------------------------------------------------------------------