├── media ├── icon.png ├── debugger.png ├── screenshot.png ├── close-dark.svg ├── close-light.svg ├── settings-dark.svg ├── settings-light.svg ├── root-folder-opened-dark.svg ├── root-folder-opened-light.svg ├── remote-explorer-dark.svg ├── remote-explorer-light.svg ├── sidebar-icon.svg ├── sidebar-icon-dark.svg ├── sidebar-icon-light.svg ├── computer.svg └── monitor.svg ├── .gitignore ├── .vscodeignore ├── .vscode ├── extensions.json └── launch.json ├── jsconfig.json ├── test ├── suite │ ├── extension.test.js │ └── index.js └── runTest.js ├── .eslintrc.json ├── LICENSE ├── CHANGELOG.md ├── README.md ├── package.json ├── index.html └── extension.js /media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCJack123/vscode-craftos-pc/HEAD/media/icon.png -------------------------------------------------------------------------------- /media/debugger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCJack123/vscode-craftos-pc/HEAD/media/debugger.png -------------------------------------------------------------------------------- /media/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCJack123/vscode-craftos-pc/HEAD/media/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | /.git 3 | /index.old.html 4 | /node_modules 5 | /vsc-extension-quickstart.md 6 | *.vsix -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | vsc-extension-quickstart.md 6 | **/jsconfig.json 7 | **/*.map 8 | **/.eslintrc.json 9 | .zsh_history -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "checkJs": true, /* Typecheck .js files. */ 6 | "lib": [ 7 | "es6" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /media/close-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/close-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/settings-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/settings-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/suite/extension.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | const vscode = require('vscode'); 6 | // const myExtension = require('../extension'); 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.equal(-1, [1, 2, 3].indexOf(5)); 13 | assert.equal(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "ecmaFeatures": { 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-const-assign": "warn", 18 | "no-this-before-super": "warn", 19 | "no-undef": "warn", 20 | "no-unreachable": "warn", 21 | "no-unused-vars": "warn", 22 | "constructor-super": "warn", 23 | "valid-typeof": "warn" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /media/root-folder-opened-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/root-folder-opened-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/remote-explorer-dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/remote-explorer-light.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/runTest.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { runTests } = require('vscode-test'); 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../'); 10 | 11 | // The path to the extension test script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ] 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/test/suite/index" 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/suite/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Mocha = require('mocha'); 3 | const glob = require('glob'); 4 | 5 | function run() { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd' 9 | }); 10 | // Use any mocha API 11 | mocha.useColors(true); 12 | 13 | const testsRoot = path.resolve(__dirname, '..'); 14 | 15 | return new Promise((c, e) => { 16 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 17 | if (err) { 18 | return e(err); 19 | } 20 | 21 | // Add files to the test suite 22 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 23 | 24 | try { 25 | // Run the mocha test 26 | mocha.run(failures => { 27 | if (failures > 0) { 28 | e(new Error(`${failures} tests failed.`)); 29 | } else { 30 | c(); 31 | } 32 | }); 33 | } catch (err) { 34 | console.error(err); 35 | e(err); 36 | } 37 | }); 38 | }); 39 | } 40 | 41 | module.exports = { 42 | run 43 | }; 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 JackMacWindows 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. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "craftos-pc" extension will be documented in this file. 4 | 5 | ## 1.2.3 6 | 7 | * Added a new very prominent button to open files 8 | * Added an extra alert when the files are already open 9 | 10 | ## 1.2.2 11 | 12 | * Fixed an issue preventing uploading files between 48-64 kB in size 13 | 14 | ## 1.2.1 15 | 16 | * Added automatic detection of user installations on Windows 17 | * Adjusted some error messages to be more descriptive 18 | * Added a new output channel for debugging messages 19 | 20 | ## 1.2.0 21 | 22 | * Added debugger support for CraftOS-PC v2.7 and later 23 | * Added support for Visual Studio Live Share 24 | * This currently requires manually allowing the extension to communicate 25 | 26 | ## 1.1.8 27 | 28 | * Fixed a bug causing disconnections when sending large data packets 29 | 30 | ## 1.1.6 31 | 32 | * Added Run Script button to quickly run files in a new CraftOS-PC instance 33 | 34 | ## 1.1.5 35 | 36 | * Added history to Open WebSocket button 37 | 38 | ## 1.1.4 39 | 40 | * Terminal windows now automatically resize to fit the screen 41 | * Fixed an issue causing the bug info prompt from 1.1.3 to not appear 42 | 43 | ## 1.1.3 44 | 45 | * Added more information about VS Code "certificate has expired" bug 46 | * Added a "CraftOS-PC: Force Close Connection" to close the connection immediately without waiting for a response 47 | * Fixed an issue causing remote screens to go black 48 | 49 | ## 1.1.1 50 | 51 | * Fixed mouse_up event not being sent 52 | 53 | ## 1.1.0 54 | 55 | * Added ability to connect to WebSocket servers 56 | * Added integration with new remote.craftos-pc.cc service (beta) 57 | * Added support for raw mode 1.1 specification 58 | * Added URI handler for WebSocket links 59 | * Fixed security vulnerability in glob-parent dependency 60 | 61 | ## 1.0.2 62 | 63 | * Fixed wrong mouse buttons being sent 64 | * Fixed drag coordinates in the margins of the screen 65 | * Fixed mouse drag events firing in the same cell after click 66 | 67 | ## 1.0.1 68 | 69 | * Fixed mouse events not being sent to the window 70 | 71 | ## 1.0.0 72 | 73 | * Added support for custom fonts 74 | * Font files must be in the exact same format as ComputerCraft fonts (with the same outer padding area) 75 | * Added close buttons to each window, as well as a global quit button 76 | * Added buttons to open a new window with the selected computer's data directory 77 | * This requires either CraftOS-PC v2.5.6 or later, or computers labeled "Computer >id<" 78 | * Added button to open the configuration 79 | * Added paste event detection 80 | * Added icons for monitors 81 | * Updated extension icon to CraftOS-PC v2.4's new icon 82 | * Fixed duplicate drag events being sent for the same character cell 83 | * Fixed mouse events sending the wrong coordinates 84 | * Fixed the computer background not being drawn properly 85 | * Upgraded y18n and lodash to fix vulnerabilities (#3, #4) 86 | * Reformatted code to be a bit more clean 87 | 88 | ## 0.2.1 89 | 90 | * Added an error message if the executable is missing 91 | * Fixed `mouse_click` events being sent instead of `mouse_drag` 92 | 93 | ## 0.2.0 94 | 95 | * Fixed performance issues causing high CPU usage and major slowdown 96 | * Render speed should now be about the same as in standard GUI mode 97 | * Added `craftos-pc.additionalArguments` setting 98 | * Added command to close the emulator session without having to close each window 99 | * Fixed a bug causing CraftOS-PC to not start on Windows when a workspace is open 100 | 101 | ## 0.1.1 102 | 103 | Fixes a bug where the wrong key events were being sent (e.g. `key_up` when pressing a key down). Also fixes `char` events being sent with modifier keys held. 104 | 105 | Download the latest build of CraftOS-PC (from 7/27/20 or later) to fix a bug with events being sent to the wrong window, as well as a bug preventing Ctrl-R/S/T from working properly. 106 | 107 | ## 0.1.0 108 | 109 | First public alpha release. -------------------------------------------------------------------------------- /media/sidebar-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CraftOS-PC 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /media/sidebar-icon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CraftOS-PC 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /media/sidebar-icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CraftOS-PC 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /media/computer.svg: -------------------------------------------------------------------------------- 1 | 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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CraftOS-PC for VS Code README 2 | 3 | An extension for Visual Studio Code adding a bunch of new features to help you write ComputerCraft code easier through CraftOS-PC. 4 | 5 | ## Features 6 | 7 | * Support for built-in CraftOS-PC terminals in VS Code 8 | * Quickly access computer data directories and the configuration 9 | * Run ComputerCraft Lua scripts in CraftOS-PC 10 | * Browse files on the connected computer in the current workspace 11 | * Connect to CraftOS-PC raw mode WebSocket servers 12 | * Use the remote.craftos-pc.cc service to open any ComputerCraft computer in VS Code (beta) 13 | * Debug code directly in VS Code using the native debugger interface 14 | 15 | ![Screenshot](media/screenshot.png) 16 | 17 | ![Debugger screenshot](media/debugger.png) 18 | 19 | ## Requirements 20 | 21 | * CraftOS-PC v2.3 or later (https://www.craftos-pc.cc) 22 | * **If on Windows, make sure to install the console version as well under Optional components in the installer** 23 | * If installed in a non-standard directory (such as in your user directory), make sure to set `craftos-pc.executablePath` in the settings 24 | * See "Known issues" for caveats for certain CraftOS-PC versions 25 | * If you only want to use remote.craftos-pc.cc, you do not have to install CraftOS-PC 26 | 27 | ## Recommended Extensions 28 | 29 | * [ComputerCraft by JackMacWindows (me!)](https://marketplace.visualstudio.com/items?itemName=jackmacwindows.vscode-computercraft) for ComputerCraft autocomplete 30 | * [Lua by sumneko](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) for Lua syntax highlighting & linting 31 | 32 | ## Extension Settings 33 | 34 | This extension contributes the following settings: 35 | 36 | * `craftos-pc.executablePath.[windows|mac|linux|all]`: Path to the CraftOS-PC executable depending on the platform. This should be an absolute path to an executable supporting console output (on Windows, this must be pointing to a copy of `CraftOS-PC_console.exe`, which is optionally available in the installer). 37 | * `craftos-pc.dataPath`: Path to the data directory storing computer files, configuration, etc. 38 | * `craftos-pc.additionalArguments`: Additional command-line arguments to send to CraftOS-PC, separated by spaces. 39 | * `craftos-pc.customFont.path`: The path to a custom font, if desired. Must be a path to a valid image, or 'hdfont' to automatically find the HD font. Unlike normal CraftOS-PC, this may point to non-BMP files as well. 40 | 41 | ## Known Issues 42 | 43 | * Occasionally, keyboard input may stop working. To fix this, click outside the CraftOS-PC window and then back in. 44 | * Scroll events do not report the position of the scroll. This is a limitation of JavaScript. 45 | * Live Share support does not respect read-only mode due to a limitation in the Live Share API. 46 | * Some versions of CraftOS-PC have bugs that interfere with the functioning of this extension: 47 | * The debugger only works on CraftOS-PC v2.7 or later. 48 | * Filesystem access only works on CraftOS-PC v2.6 or later, or any server implementing raw mode 1.1 or later. 49 | * v2.5.4-v2.5.5: Creating a new window results in a crash. This is fixed in v2.6. 50 | * v2.5.1-v2.5.1.1: CraftOS-PC often crashes in raw mode on these versions. This is fixed in v2.5.2. 51 | * v2.3-v2.3.4: All events are sent to the first window, and all windows have the same ID. This is fixed in v2.4. 52 | 53 | ### Live Share 54 | 55 | The VS Code Live Share extension uses a permission list to check whether an extension is allowed to use certain parts of the API. Unfortunately, that list does not include this extension (yet), so Live Share support requires creating a special config file to enable it manually. 56 | 57 | To fix this, create a file called `.vs-liveshare-settings.json` in your home folder, and paste this inside the file: 58 | 59 | ```json 60 | { 61 | "extensionPermissions": { 62 | "JackMacWindows.craftos-pc": [ 63 | "shareServices" 64 | ] 65 | } 66 | } 67 | ``` 68 | 69 | Then reload VS Code and run Live Share again. 70 | 71 | ## Release Notes 72 | 73 | ## 1.2.3 74 | 75 | * Added a new very prominent button to open files 76 | * Added an extra alert when the files are already open 77 | 78 | ## 1.2.2 79 | 80 | * Fixed an issue preventing uploading files between 48-64 kB in size 81 | 82 | ## 1.2.1 83 | 84 | * Added automatic detection of user installations on Windows 85 | * Adjusted some error messages to be more descriptive 86 | * Added a new output channel for debugging messages 87 | 88 | ## 1.2.0 89 | 90 | * Added debugger support for CraftOS-PC v2.7 and later 91 | * Added support for Visual Studio Live Share 92 | * This currently requires manually allowing the extension to communicate 93 | 94 | ## 1.1.8 95 | 96 | * Fixed a bug causing disconnections when sending large data packets 97 | 98 | ## 1.1.6 99 | 100 | * Added Run Script button to quickly run files in a new CraftOS-PC instance 101 | 102 | ## 1.1.5 103 | 104 | * Added history to Open WebSocket button 105 | 106 | ## 1.1.4 107 | 108 | * Terminal windows now automatically resize to fit the screen 109 | * Fixed an issue causing the bug info prompt from 1.1.3 to not appear 110 | 111 | ## 1.1.3 112 | 113 | * Added more information about VS Code "certificate has expired" bug 114 | * Added a "CraftOS-PC: Force Close Connection" to close the connection immediately without waiting for a response 115 | * Fixed an issue causing remote screens to go black 116 | 117 | ## 1.1.1 118 | 119 | * Fixed mouse_up event not being sent 120 | 121 | ## 1.1.0 122 | 123 | * Added ability to connect to WebSocket servers 124 | * Added integration with new remote.craftos-pc.cc service (beta) 125 | * Added support for raw mode 1.1 specification 126 | * Added URI handler for WebSocket links 127 | * Fixed security vulnerability in glob-parent dependency 128 | 129 | ## 1.0.2 130 | 131 | * Fixed wrong mouse buttons being sent 132 | * Fixed drag coordinates in the margins of the screen 133 | * Fixed mouse drag events firing in the same cell after click 134 | 135 | ## 1.0.1 136 | 137 | * Fixed mouse events not being sent to the window 138 | 139 | ## 1.0.0 140 | 141 | * Added support for custom fonts 142 | * Font files must be in the exact same format as ComputerCraft fonts (with the same outer padding area) 143 | * Added close buttons to each window, as well as a global quit button 144 | * Added buttons to open a new window with the selected computer's data directory 145 | * This requires either CraftOS-PC v2.5.6 or later, or computers labeled "Computer >id<" 146 | * Added button to open the configuration 147 | * Added paste event detection 148 | * Added icons for monitors 149 | * Updated extension icon to CraftOS-PC v2.4's new icon 150 | * Fixed duplicate drag events being sent for the same character cell 151 | * Fixed mouse events sending the wrong coordinates 152 | * Fixed the computer background not being drawn properly 153 | * Upgraded y18n and lodash to fix vulnerabilities (#3, #4) 154 | * Reformatted code to be a bit more clean 155 | 156 | ### 0.2.1 157 | 158 | * Added an error message if the executable is missing 159 | * Fixed `mouse_click` events being sent instead of `mouse_drag` 160 | 161 | ### 0.2.0 162 | 163 | * Fixed performance issues causing high CPU usage and major slowdown 164 | * Render speed should now be about the same as in standard GUI mode 165 | * Added `craftos-pc.additionalArguments` setting 166 | * Added command to close the emulator session without having to close each window 167 | * Fixed a bug causing CraftOS-PC to not start on Windows when a workspace is open 168 | 169 | ### 0.1.1 170 | 171 | Fixes a bug where the wrong key events were being sent (e.g. `key_up` when pressing a key down). Also fixes `char` events being sent with modifier keys held. 172 | 173 | Download the latest build of CraftOS-PC (from 7/27/20 or later) to fix a bug with events being sent to the wrong window, as well as a bug preventing Ctrl-R/S/T from working properly. 174 | 175 | ### 0.1.0 176 | 177 | First public alpha release. 178 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "craftos-pc", 3 | "displayName": "CraftOS-PC for VS Code", 4 | "description": "Adds the ability to open CraftOS-PC windows inside VS Code, as well as some ease-of-use functionality to make ComputerCraft programming easier.", 5 | "version": "1.2.3", 6 | "engines": { 7 | "vscode": "^1.41.0" 8 | }, 9 | "categories": [ 10 | "Other" 11 | ], 12 | "keywords": [ 13 | "computercraft", 14 | "craftos", 15 | "terminal", 16 | "craftos-pc" 17 | ], 18 | "author": { 19 | "name": "JackMacWindows" 20 | }, 21 | "publisher": "JackMacWindows", 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/MCJack123/vscode-craftos-pc" 25 | }, 26 | "icon": "media/icon.png", 27 | "activationEvents": [ 28 | "onStartupFinished" 29 | ], 30 | "main": "./extension.js", 31 | "contributes": { 32 | "commands": [ 33 | { 34 | "command": "craftos-pc.open", 35 | "title": "CraftOS-PC: Start Session" 36 | }, 37 | { 38 | "command": "craftos-pc.open-websocket", 39 | "title": "CraftOS-PC: Open WebSocket Connection..." 40 | }, 41 | { 42 | "command": "craftos-pc.open-new-remote", 43 | "title": "CraftOS-PC: Open New remote.craftos-pc.cc Session" 44 | }, 45 | { 46 | "command": "craftos-pc.open-window", 47 | "title": "CraftOS-PC: Open Window with ID..." 48 | }, 49 | { 50 | "command": "craftos-pc.open-computer-data", 51 | "title": "CraftOS-PC: Open Data Directory for Computer...", 52 | "icon": { 53 | "light": "media/root-folder-opened-light.svg", 54 | "dark": "media/root-folder-opened-dark.svg" 55 | } 56 | }, 57 | { 58 | "command": "craftos-pc.open-remote-data", 59 | "title": "CraftOS-PC: Open Remote Data for Window...", 60 | "icon": { 61 | "light": "media/remote-explorer-light.svg", 62 | "dark": "media/remote-explorer-dark.svg" 63 | } 64 | }, 65 | { 66 | "command": "craftos-pc.open-config", 67 | "title": "CraftOS-PC: Open Configuration", 68 | "icon": { 69 | "light": "media/settings-light.svg", 70 | "dark": "media/settings-dark.svg" 71 | } 72 | }, 73 | { 74 | "command": "craftos-pc.close", 75 | "title": "CraftOS-PC: Close Session", 76 | "icon": { 77 | "light": "media/close-light.svg", 78 | "dark": "media/close-dark.svg" 79 | } 80 | }, 81 | { 82 | "command": "craftos-pc.close-window", 83 | "title": "CraftOS-PC: Close Window", 84 | "icon": { 85 | "light": "media/close-light.svg", 86 | "dark": "media/close-dark.svg" 87 | } 88 | }, 89 | { 90 | "command": "craftos-pc.kill", 91 | "title": "CraftOS-PC: Force Close Connection" 92 | }, 93 | { 94 | "command": "craftos-pc.clear-history", 95 | "title": "CraftOS-PC: Clear WebSocket History" 96 | }, 97 | { 98 | "command": "craftos-pc.run-file", 99 | "title": "CraftOS-PC: Run Script", 100 | "icon": { 101 | "light": "media/sidebar-icon-light.svg", 102 | "dark": "media/sidebar-icon-dark.svg" 103 | } 104 | } 105 | ], 106 | "viewsContainers": { 107 | "activitybar": [ 108 | { 109 | "id": "craftos-terminals", 110 | "title": "CraftOS Terminals", 111 | "icon": "media/sidebar-icon.svg" 112 | } 113 | ] 114 | }, 115 | "views": { 116 | "craftos-terminals": [ 117 | { 118 | "id": "craftos-computers", 119 | "name": "Computers" 120 | }, 121 | { 122 | "id": "craftos-monitors", 123 | "name": "Monitors" 124 | }, 125 | { 126 | "id": "craftos-open-files", 127 | "name": "Open Files" 128 | } 129 | ] 130 | }, 131 | "viewsWelcome": [ 132 | { 133 | "view": "craftos-computers", 134 | "contents": "You must start CraftOS-PC before using this tab.\n[Start CraftOS-PC](command:craftos-pc.open)\n[Open WebSocket...](command:craftos-pc.open-websocket)\n[Connect to Remote (Beta)](command:craftos-pc.open-new-remote)" 135 | }, 136 | { 137 | "view": "craftos-open-files", 138 | "contents": "Click this button to open remote files.\n[Open Remote Files](command:craftos-pc.open-primary-remote-data)" 139 | } 140 | ], 141 | "menus": { 142 | "view/item/context": [ 143 | { 144 | "command": "craftos-pc.open-remote-data", 145 | "when": "view == craftos-computers && viewItem == data-available", 146 | "group": "inline@1" 147 | }, 148 | { 149 | "command": "craftos-pc.close-window", 150 | "when": "view == craftos-computers || view == craftos-monitors", 151 | "group": "inline@2" 152 | } 153 | ], 154 | "view/title": [ 155 | { 156 | "command": "craftos-pc.open-config", 157 | "when": "view == craftos-computers", 158 | "group": "navigation@1" 159 | }, 160 | { 161 | "command": "craftos-pc.close", 162 | "when": "view == craftos-computers", 163 | "group": "navigation@2" 164 | } 165 | ], 166 | "editor/title": [ 167 | { 168 | "command": "craftos-pc.run-file", 169 | "when": "resourceLangId == lua && isFileSystemResource", 170 | "group": "navigation" 171 | } 172 | ] 173 | }, 174 | "configuration": { 175 | "title": "CraftOS-PC", 176 | "properties": { 177 | "craftos-pc.executablePath.windows": { 178 | "type": "string", 179 | "default": "C:\\Program Files\\CraftOS-PC\\CraftOS-PC_console.exe", 180 | "description": "The path to the CraftOS-PC executable on Windows. Must be the console version.", 181 | "scope": "machine-overridable" 182 | }, 183 | "craftos-pc.executablePath.mac": { 184 | "type": "string", 185 | "default": "/Applications/CraftOS-PC.app/Contents/MacOS/craftos", 186 | "description": "The path to the CraftOS-PC executable (NOT the application) on macOS.", 187 | "scope": "machine-overridable" 188 | }, 189 | "craftos-pc.executablePath.linux": { 190 | "type": "string", 191 | "default": "/usr/bin/craftos", 192 | "description": "The path to the CraftOS-PC executable on Linux.", 193 | "scope": "machine-overridable" 194 | }, 195 | "craftos-pc.executablePath.all": { 196 | "type": "string", 197 | "default": null, 198 | "description": "The path to the CraftOS-PC executable. Overrides any platform defaults if set.", 199 | "scope": "machine-overridable" 200 | }, 201 | "craftos-pc.dataPath": { 202 | "type": "string", 203 | "default": null, 204 | "description": "The path to the data directory to use for CraftOS-PC. This defaults to the standard data directory location.", 205 | "scope": "window" 206 | }, 207 | "craftos-pc.additionalArguments": { 208 | "type": "string", 209 | "default": null, 210 | "description": "Additional command-line arguments to pass to CraftOS-PC.", 211 | "scope": "machine-overridable" 212 | }, 213 | "craftos-pc.customFont.path": { 214 | "type": "string", 215 | "default": null, 216 | "description": "The path to a custom font, if desired. Must be a path to a valid image, or 'hdfont' to automatically find the HD font.", 217 | "scope": "machine-overridable" 218 | } 219 | } 220 | }, 221 | "debuggers": [ 222 | { 223 | "type": "craftos-pc", 224 | "label": "CraftOS-PC Debugger", 225 | "runtime": "node", 226 | "configurationAttributes": { 227 | "launch": { 228 | "required": [], 229 | "properties": { 230 | "program": { 231 | "type": "string", 232 | "description": "Program to launch when debugging" 233 | } 234 | } 235 | }, 236 | "attach": { 237 | "required": [], 238 | "properties": { 239 | "port": { 240 | "type": "number", 241 | "description": "Port to attach to" 242 | }, 243 | "host": { 244 | "type": "string", 245 | "description": "Host address of the debuggee" 246 | } 247 | } 248 | } 249 | }, 250 | "initialConfigurations": [ 251 | { 252 | "type": "craftos-pc", 253 | "request": "launch", 254 | "name": "Debug CraftOS-PC" 255 | } 256 | ] 257 | } 258 | ], 259 | "breakpoints": [ 260 | { 261 | "language": "lua" 262 | } 263 | ] 264 | }, 265 | "scripts": { 266 | "lint": "eslint .", 267 | "pretest": "npm run lint", 268 | "test": "node ./test/runTest.js" 269 | }, 270 | "devDependencies": { 271 | "@types/glob": "^7.1.1", 272 | "@types/mocha": "^7.0.1", 273 | "@types/node": "^12.11.7", 274 | "@types/vscode": "^1.41.0", 275 | "eslint": "^6.8.0", 276 | "glob": "^7.1.6", 277 | "mocha": "^10.1.0", 278 | "typescript": "^3.7.5", 279 | "vscode-test": "^1.3.0" 280 | }, 281 | "dependencies": { 282 | "semver": "^7.5.2", 283 | "vsls": "^1.0.4753", 284 | "ws": "^7.5.10" 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /media/monitor.svg: -------------------------------------------------------------------------------- 1 | 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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | CraftOS-PC for VS Code 4 | 5 | 116 | 195 | 362 | 363 | 364 | 365 | 366 | 367 |
368 |
369 |
370 |
371 |
372 | 398 | 399 | 400 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const child_process = require('child_process'); 2 | const { SIGINT } = require('constants'); 3 | const fs = require('fs'); 4 | const https = require('https'); 5 | const os = require('os'); 6 | const path = require('path'); 7 | const process = require('process'); 8 | const semver = require('semver'); 9 | const URL = require('url').URL; 10 | const vscode = require('vscode'); 11 | const vsls = require('vsls'); 12 | const WebSocket = require('ws'); 13 | 14 | var windows = {}; 15 | var crcTable = null; 16 | var extcontext = null; 17 | /** @type vscode.OutputChannel */ 18 | var log = {appendLine: () => {}}; 19 | 20 | function makeCRCTable() { 21 | let c; 22 | let crcTable = []; 23 | for (let n = 0; n < 256; n++) { 24 | c = n; 25 | for (let k = 0; k < 8; k++) c = c & 1 ? 0xEDB88320 ^ (c >>> 1) : c >>> 1; 26 | crcTable[n] = c; 27 | } 28 | return crcTable; 29 | } 30 | 31 | function crc32(str) { 32 | crcTable = crcTable || makeCRCTable(); 33 | let crc = 0 ^ (-1); 34 | for (let i = 0; i < str.length; i++) { 35 | crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF]; 36 | } 37 | return (crc ^ (-1)) >>> 0; 38 | }; 39 | 40 | function bufferstream(str) { 41 | let retval = {} 42 | retval.pos = 0 43 | retval.str = str 44 | retval.readUInt64 = function() { 45 | let r = Number(this.str.readBigUInt64LE(this.pos)); 46 | this.pos += 8; 47 | return r; 48 | } 49 | retval.readUInt32 = function() { 50 | let r = this.str.readUInt32LE(this.pos); 51 | this.pos += 4; 52 | return r; 53 | } 54 | retval.readUInt16 = function() { 55 | let r = this.str.readUInt16LE(this.pos); 56 | this.pos += 2; 57 | return r; 58 | } 59 | retval.get = function() { 60 | return this.str.readUInt8(this.pos++); 61 | }; 62 | retval.putback = function() {this.pos--;} 63 | return retval; 64 | } 65 | 66 | function validateURL(str) { 67 | try { 68 | let url = new URL(str); 69 | return url.protocol.toLowerCase() == "ws:" || url.protocol.toLowerCase() == "wss:"; 70 | } catch (e) {return false;} 71 | } 72 | 73 | const computer_provider = { 74 | getChildren: element => { 75 | if ((element === undefined || element === null) && process_connection !== null) { 76 | let arr = []; 77 | for (let w in windows) if (!windows[w].isMonitor) arr.push({title: windows[w].term.title, id: w}); 78 | return arr; 79 | } else return null; 80 | }, 81 | getTreeItem: element => { 82 | let r = new vscode.TreeItem(element.title); 83 | r.iconPath = vscode.Uri.file(path.join(extcontext.extensionPath, 'media/computer.svg')); 84 | r.command = {command: "craftos-pc.open-window", title: "CraftOS-PC: Open Window", arguments: [element]}; 85 | if (supportsFilesystem) r.contextValue = "data-available"; 86 | return r; 87 | }, 88 | _onDidChangeTreeData: new vscode.EventEmitter(), 89 | }; 90 | computer_provider.onDidChangeTreeData = computer_provider._onDidChangeTreeData.event; 91 | 92 | const monitor_provider = { 93 | getChildren: element => { 94 | if ((element === undefined || element === null) && process_connection !== null) { 95 | let arr = []; 96 | for (let w in windows) if (windows[w].isMonitor) arr.push({title: windows[w].term.title, id: w}); 97 | return arr; 98 | } 99 | else return null; 100 | }, 101 | getTreeItem: element => { 102 | let r = new vscode.TreeItem(element.title); 103 | r.iconPath = vscode.Uri.file(path.join(extcontext.extensionPath, 'media/monitor.svg')); 104 | r.command = {command: "craftos-pc.open-window", title: "CraftOS-PC: Open Window", arguments: [element]}; 105 | return r; 106 | }, 107 | _onDidChangeTreeData: new vscode.EventEmitter(), 108 | } 109 | monitor_provider.onDidChangeTreeData = monitor_provider._onDidChangeTreeData.event; 110 | 111 | var process_connection = null; 112 | var data_continuation = null; 113 | var nextDataRequestID = 0; 114 | var dataRequestCallbacks = {}; 115 | var isVersion11 = false; 116 | var useBinaryChecksum = false; 117 | var supportsFilesystem = false; 118 | var gotMessage = false; 119 | var didShowBetaMessage = false; 120 | var processFeatures = {}; 121 | /** @type vsls.LiveShare|null */ 122 | var liveshare = null; 123 | /** @type vsls.SharedServiceProxy|null */ 124 | var vslsClient = null; 125 | /** @type vsls.SharedService|null */ 126 | var vslsServer = null; 127 | 128 | function getSetting(name) { 129 | const config = vscode.workspace.getConfiguration(name); 130 | if (config.get("all") !== null && config.get("all") !== "") return config.get("all"); 131 | else if (os.platform() === "win32") return config.get("windows").replace(/%([^%]+)%/g, (_, n) => process.env[n] || ('%' + n + '%')); 132 | else if (os.platform() === "darwin") return config.get("mac").replace(/\$(\w+)/g, (_, n) => process.env[n] || ('$' + n)).replace(/\${([^}]+)}/g, (_, n) => process.env[n] || ('${' + n + '}')); 133 | else if (os.platform() === "linux") return config.get("linux").replace(/\$(\w+)/g, (_, n) => process.env[n] || ('$' + n)).replace(/\${([^}]+)}/g, (_, n) => process.env[n] || ('${' + n + '}')); 134 | else return null; 135 | } 136 | 137 | function getDataPath() { 138 | const config = vscode.workspace.getConfiguration("craftos-pc"); 139 | if (config.get("dataPath") !== null && config.get("dataPath") !== "") return config.get("dataPath"); 140 | else if (os.platform() === "win32") return "%appdata%\\CraftOS-PC".replace(/%([^%]+)%/g, (_, n) => process.env[n] || ('%' + n + '%')); 141 | else if (os.platform() === "darwin") return "$HOME/Library/Application Support/CraftOS-PC".replace(/\$(\w+)/g, (_, n) => process.env[n] || ('$' + n)).replace(/\${([^}]+)}/g, (_, n) => process.env[n] || ('${' + n + '}')) 142 | else if (os.platform() === "linux") return "$HOME/.local/craftos-pc".replace(/\$(\w+)/g, (_, n) => process.env[n] || ('$' + n)).replace(/\${([^}]+)}/g, (_, n) => process.env[n] || ('${' + n + '}')) 143 | else return null; 144 | } 145 | 146 | function getExecutable() { 147 | let path = getSetting("craftos-pc.executablePath"); 148 | if (path !== null && fs.existsSync(path)) return path; 149 | if (os.platform() === "win32") { 150 | path = "%localappdata%\\Programs\\CraftOS-PC\\CraftOS-PC_console.exe".replace(/%([^%]+)%/g, (_, n) => process.env[n] || ('%' + n + '%')); 151 | if (fs.existsSync(path)) return path; 152 | } 153 | if (path !== null && os.platform() === "win32" && fs.existsSync(path.replace("_console", ""))) { 154 | vscode.window.showErrorMessage("The CraftOS-PC installation is missing the console version, which is required for this extension to function. Please run the installer again, making sure to check the 'Console build for raw mode' box."); 155 | return null; 156 | } 157 | vscode.window.showErrorMessage("The CraftOS-PC executable could not be found. Check the path in the settings. If you haven't installed CraftOS-PC yet, [download it from the official website.](https://www.craftos-pc.cc)"); 158 | return null; 159 | } 160 | 161 | function closeAllWindows() { 162 | for (let k in windows) if (windows[k].panel !== undefined) windows[k].panel.dispose(); 163 | windows = {}; 164 | computer_provider._onDidChangeTreeData.fire(null); 165 | monitor_provider._onDidChangeTreeData.fire(null); 166 | if (vslsServer !== null) vslsServer.notify("windows", {}); 167 | } 168 | 169 | function queueDataRequest(id, type, path, path2) { 170 | if (process_connection === null) return new Promise((resolve, reject) => reject(new Error("Path does not exist"))); 171 | let filedata = undefined; 172 | if ((type & 0xF1) === 0x11) { 173 | filedata = path2; 174 | path2 = undefined; 175 | } 176 | const pathbuf = Buffer.from(path, "latin1"); 177 | const path2buf = (typeof path2 === "string" ? Buffer.from(path2, "latin1") : null); 178 | const data = Buffer.alloc(5 + pathbuf.length + (typeof path2 === "string" ? path2buf.length + 1 : 0)); 179 | data[0] = 7; 180 | data[1] = id; 181 | data[2] = type; 182 | data[3] = nextDataRequestID; 183 | nextDataRequestID = (nextDataRequestID + 1) & 0xFF; 184 | pathbuf.copy(data, 4); 185 | if (typeof path2 === "string") path2buf.copy(data, 5 + pathbuf.length); 186 | const b64 = data.toString('base64'); 187 | const packet = "!CPC" + ("000" + b64.length.toString(16)).slice(-4) + b64 + ("0000000" + crc32(useBinaryChecksum ? data.toString("binary") : b64).toString(16)).slice(-8) + "\n"; 188 | process_connection.stdin.write(packet, 'utf8'); 189 | if (typeof filedata !== "undefined") { 190 | const data2 = Buffer.alloc(8 + filedata.length); 191 | data2[0] = 9; 192 | data2[1] = id; 193 | data2[2] = 0; 194 | data2[3] = data[3]; 195 | data2.writeInt32LE(filedata.length, 4); 196 | filedata.copy(data2, 8); 197 | const b642 = data2.toString('base64'); 198 | const packet2 = (b642.length > 65535 ? "!CPD" + ("00000000000" + b642.length.toString(16)).slice(-12) : "!CPC" + ("000" + b642.length.toString(16)).slice(-4)) + b642 + ("0000000" + crc32(useBinaryChecksum ? data2.toString("binary") : b642).toString(16)).slice(-8) + "\n"; 199 | process_connection.stdin.write(packet2, 'utf8'); 200 | } 201 | return new Promise((resolve, reject) => { 202 | let tid = setTimeout(() => { 203 | delete dataRequestCallbacks[data[3]]; 204 | log.appendLine("Could not get info for " + path + " (request " + data[3] + ")"); 205 | reject(new Error("Timeout")); 206 | }, 3000); 207 | dataRequestCallbacks[data[3]] = (data, err) => { 208 | clearTimeout(tid); 209 | if (!err) resolve(data); 210 | else reject(err); 211 | } 212 | }); 213 | } 214 | 215 | function checkVersion(silent) { 216 | const exe_path = getSetting("craftos-pc.executablePath"); 217 | if (exe_path === null) return; 218 | child_process.execFile(exe_path, ["--version"], {windowsHide: true}, (err, stdout, stderr) => { 219 | if (err) { 220 | if (!silent) vscode.window.showErrorMessage("Failed to detect CraftOS-PC version (error: " + err.message + "). Please check that the path is correct and working properly."); 221 | return; 222 | } 223 | let version = (stdout.match(/CraftOS-PC v([\d\.]+)/) || [])[1]; 224 | if (version === undefined || version === null) { 225 | if (!silent) vscode.window.showErrorMessage("Failed to detect CraftOS-PC version (error: no version number detected)." + (os.platform === "win32" ? "Make sure the path is pointing to CraftOS-PC_console.exe, and not CraftOS-PC.exe." : "")); 226 | return; 227 | } 228 | log.appendLine("Detected CraftOS-PC " + version); 229 | version = semver.coerce(version); 230 | processFeatures.debugger = semver.gt(version, "2.6.6"); 231 | processFeatures.filesystem = semver.gte(version, "2.6.0"); 232 | if (!silent) { 233 | if (version == "2.5.4" || version == "2.5.5") vscode.window.showWarningMessage("This version of CraftOS-PC crashes when using multiple windows. Update to a newer version to fix this."); 234 | else if (version == "2.5.1") vscode.window.showWarningMessage("This version of CraftOS-PC often crashes when using the extension. Update to a newer version to fix this."); 235 | else if (semver.lt(version, "2.4.0")) vscode.window.showWarningMessage("This version of CraftOS-PC does not support using multiple windows properly. Update to a newer version to fix this."); 236 | } 237 | }); 238 | } 239 | 240 | /** 241 | * @implements {vscode.FileSystemProvider} 242 | */ 243 | class RawFileSystemProvider { 244 | constructor() { 245 | this._onDidChangeFile = new vscode.EventEmitter() 246 | this.onDidChangeFile = this._onDidChangeFile.event 247 | } 248 | /** 249 | * @param {vscode.Uri} source 250 | * @param {vscode.Uri} destination 251 | * @param {{overwrite: boolean}} options 252 | */ 253 | copy(source, destination, options) { 254 | if (process_connection === null) throw vscode.FileSystemError.Unavailable("Computer connection not open yet"); 255 | if (!supportsFilesystem) throw vscode.FileSystemError.Unavailable("Connected computer doesn't support filesystems"); 256 | if (source.authority !== destination.authority) throw vscode.FileSystemError.Unavailable("Cannot move across computers"); 257 | return queueDataRequest(parseInt(source.authority), 12, source.path, destination.path); 258 | } 259 | /** 260 | * @param {vscode.Uri} uri 261 | */ 262 | createDirectory(uri) { 263 | if (process_connection === null) throw vscode.FileSystemError.Unavailable("Computer connection not open yet"); 264 | if (!supportsFilesystem) throw vscode.FileSystemError.Unavailable("Connected computer doesn't support filesystems"); 265 | return queueDataRequest(parseInt(uri.authority), 10, uri.path); 266 | } 267 | /** 268 | * @param {vscode.Uri} uri 269 | * @param {{recursive: boolean}} options 270 | */ 271 | delete(uri, options) { 272 | // Warning: ignores options.recursive (always true) 273 | if (process_connection === null) throw vscode.FileSystemError.Unavailable("Computer connection not open yet"); 274 | if (!supportsFilesystem) throw vscode.FileSystemError.Unavailable("Connected computer doesn't support filesystems"); 275 | return queueDataRequest(parseInt(uri.authority), 11, uri.path); 276 | } 277 | /** 278 | * @param {vscode.Uri} uri 279 | */ 280 | readDirectory(uri) { 281 | if (process_connection === null) throw vscode.FileSystemError.Unavailable("Computer connection not open yet"); 282 | if (!supportsFilesystem) throw vscode.FileSystemError.Unavailable("Connected computer doesn't support filesystems"); 283 | return new Promise(resolve => { 284 | queueDataRequest(parseInt(uri.authority), 7, uri.path).then(files => { 285 | let arr = []; 286 | let promises = []; 287 | for (let f of files) { 288 | promises.push(new Promise(resolve => queueDataRequest(parseInt(uri.authority), 1, path.join(uri.path, f)).then(isDir => { 289 | arr.push([f, isDir ? vscode.FileType.Directory : vscode.FileType.File]); 290 | resolve(); 291 | }).catch(() => { 292 | arr.push([f, vscode.FileType.Unknown]); 293 | resolve(); 294 | }))); 295 | } 296 | return Promise.all(promises).then(() => { 297 | arr.sort((a, b) => { 298 | if (a[1] == vscode.FileType.File && b[1] == vscode.FileType.Directory) return 1; 299 | else if (b[1] == vscode.FileType.File && a[1] == vscode.FileType.Directory) return -1; 300 | else return a[0].localeCompare(b[0]); 301 | }); 302 | resolve(arr); 303 | }); 304 | }); 305 | }); 306 | } 307 | /** 308 | * @param {vscode.Uri} uri 309 | */ 310 | readFile(uri) { 311 | if (process_connection === null) throw vscode.FileSystemError.Unavailable("Computer connection not open yet"); 312 | if (!supportsFilesystem) throw vscode.FileSystemError.Unavailable("Connected computer doesn't support filesystems"); 313 | return queueDataRequest(parseInt(uri.authority), 20, uri.path).then(data => Uint8Array.from(data)); 314 | } 315 | /** 316 | * @param {vscode.Uri} oldUri 317 | * @param {vscode.Uri} newUri 318 | * @param {{overwrite: boolean}} options 319 | */ 320 | rename(oldUri, newUri, options) { 321 | // Warning: ignores overwrite (always false) 322 | if (process_connection === null) throw vscode.FileSystemError.Unavailable("Computer connection not open yet"); 323 | if (!supportsFilesystem) throw vscode.FileSystemError.Unavailable("Connected computer doesn't support filesystems"); 324 | if (oldUri.authority !== newUri.authority) throw vscode.FileSystemError.Unavailable("Cannot move across computers"); 325 | return queueDataRequest(parseInt(oldUri.authority), 13, oldUri.path, newUri.path); 326 | } 327 | /** 328 | * @param {vscode.Uri} uri 329 | */ 330 | stat(uri) { 331 | if (process_connection === null) throw vscode.FileSystemError.Unavailable("Computer connection not open yet"); 332 | if (!supportsFilesystem) throw vscode.FileSystemError.Unavailable("Connected computer doesn't support filesystems"); 333 | return queueDataRequest(parseInt(uri.authority), 8, uri.path).then(attributes => { 334 | if (attributes === null) throw vscode.FileSystemError.FileNotFound(uri); 335 | return { 336 | ctime: attributes.created.getTime(), 337 | mtime: attributes.modified.getTime(), 338 | size: attributes.size, 339 | type: attributes.isDir ? vscode.FileType.Directory : vscode.FileType.File 340 | } 341 | }); 342 | } 343 | /** 344 | * @param {vscode.Uri} uri 345 | * @param {{excludes: string[], recursive: boolean}} options 346 | */ 347 | watch(uri, options) { 348 | // unimplemented 349 | if (process_connection === null) throw vscode.FileSystemError.Unavailable("Computer connection not open yet"); 350 | if (!supportsFilesystem) throw vscode.FileSystemError.Unavailable("Connected computer doesn't support filesystems"); 351 | return null; 352 | } 353 | /** 354 | * @param {vscode.Uri} uri 355 | * @param {Uint8Array} content 356 | * @param {{create: boolean, overwrite: boolean}} options 357 | */ 358 | writeFile(uri, content, options) { 359 | if (process_connection === null) throw vscode.FileSystemError.Unavailable("Computer connection not open yet"); 360 | if (!supportsFilesystem) throw vscode.FileSystemError.Unavailable("Connected computer doesn't support filesystems"); 361 | return queueDataRequest(parseInt(uri.authority), 21, uri.path, content); 362 | } 363 | } 364 | 365 | const debugAdapterFactory = { 366 | /** 367 | * @param {vscode.DebugSession} session 368 | */ 369 | createDebugAdapterDescriptor: (session, executable) => { 370 | if (session.configuration.request === "launch") { 371 | if (!processFeatures.debugger) { 372 | vscode.window.showErrorMessage("This version of CraftOS-PC does not support debugging. Please update to the latest version."); 373 | return null; 374 | } 375 | const exe_path = getSetting("craftos-pc.executablePath"); 376 | if (exe_path === null) { 377 | vscode.window.showErrorMessage("Please set the path to the CraftOS-PC executable in the settings."); 378 | return null; 379 | } 380 | if (!fs.existsSync(exe_path)) { 381 | vscode.window.showErrorMessage("The CraftOS-PC executable could not be found. Check the path in the settings." + (os.platform() === "win32" ? " If you installed CraftOS-PC without administrator privileges, you will need to set the path manually. Also make sure CraftOS-PC_console.exe exists in the install directory - if not, reinstall CraftOS-PC with the Console build component enabled." : "")); 382 | return null; 383 | } 384 | const dir = vscode.workspace.getConfiguration("craftos-pc").get("dataPath"); 385 | let args = vscode.workspace.getConfiguration("craftos-pc").get("additionalArguments"); 386 | if (args !== null) {args = args.split(' '); args.push("--exec"); args.push("periphemu.create(0,'debug_adapter')")} 387 | else args = ["--exec", "periphemu.create(0,'debug_adapter')"]; 388 | if (dir !== null) args.splice(0, 0, "-d", dir); 389 | return new vscode.DebugAdapterExecutable(exe_path, args); 390 | } else if (session.configuration.request === "attach") { 391 | return new vscode.DebugAdapterServer(session.configuration.port || 12100, session.configuration.host); 392 | } else return undefined; 393 | } 394 | } 395 | 396 | /** 397 | * @param {Buffer} chunk data 398 | */ 399 | function processDataChunk(chunk) { 400 | if (typeof chunk === "string") chunk = new Buffer(chunk, "utf8"); 401 | if (data_continuation !== null) { 402 | chunk = Buffer.concat([data_continuation, chunk]); 403 | data_continuation = null; 404 | } 405 | while (chunk.length > 0) { 406 | let off; 407 | if (chunk.subarray(0, 4).toString() === "!CPC") off = 8; 408 | else if (chunk.subarray(0, 4).toString() === "!CPD" && isVersion11) off = 16; 409 | else { 410 | log.appendLine("Invalid message"); 411 | return; 412 | } 413 | const size = parseInt(chunk.subarray(4, off).toString(), 16); 414 | if (size > chunk.length + 9) { 415 | data_continuation = chunk; 416 | return; 417 | } 418 | const data = Buffer.from(chunk.subarray(off, size + off).toString(), 'base64'); 419 | const good_checksum = parseInt(chunk.subarray(size + off, size + off + 8).toString(), 16); 420 | const data_checksum = crc32(useBinaryChecksum ? data.toString("binary") : chunk.subarray(off, size + off).toString()); 421 | if (good_checksum !== data_checksum) { 422 | log.appendLine("Bad checksum: expected " + good_checksum.toString(16) + ", got " + data_checksum.toString(16)); 423 | chunk = chunk.subarray(size + 16); 424 | while (String.fromCharCode(chunk[0]).match(/\s/)) chunk = chunk.subarray(1); 425 | continue; 426 | } 427 | let term = {} 428 | const stream = bufferstream(data); 429 | const type = stream.get(); 430 | const id = stream.get(); 431 | if (!gotMessage && type == 4) process_connection.stdin.write("!CPC0008BgADAA==498C93D2\n"); // 0x0003 432 | gotMessage = true; 433 | let winid = null; 434 | if (type === 0) { 435 | term.mode = stream.get(); 436 | term.blink = stream.get() === 1; 437 | term.width = stream.readUInt16(); 438 | term.height = stream.readUInt16(); 439 | term.cursorX = stream.readUInt16(); 440 | term.cursorY = stream.readUInt16(); 441 | stream.readUInt32(); 442 | term.screen = {} 443 | term.colors = {} 444 | term.pixels = {} 445 | if (term.mode === 0) { 446 | let c = stream.get(); 447 | let n = stream.get(); 448 | for (let y = 0; y < term.height; y++) { 449 | term.screen[y] = {} 450 | for (let x = 0; x < term.width; x++) { 451 | term.screen[y][x] = c; 452 | n--; 453 | if (n === 0) { 454 | c = stream.get(); 455 | n = stream.get(); 456 | } 457 | } 458 | } 459 | for (let y = 0; y < term.height; y++) { 460 | term.colors[y] = {} 461 | for (let x = 0; x < term.width; x++) { 462 | term.colors[y][x] = c; 463 | n--; 464 | if (n === 0) { 465 | c = stream.get(); 466 | n = stream.get(); 467 | } 468 | } 469 | } 470 | stream.putback(); 471 | stream.putback(); 472 | } else if (term.mode === 1 || term.mode === 2) { 473 | let c = stream.get(); 474 | let n = stream.get(); 475 | for (let y = 0; y < term.height * 9; y++) { 476 | term.pixels[y] = {} 477 | for (let x = 0; x < term.width * 6; x++) { 478 | term.pixels[y][x] = c; 479 | n--; 480 | if (n === 0) { 481 | c = stream.get(); 482 | n = stream.get(); 483 | } 484 | } 485 | } 486 | stream.putback(); 487 | stream.putback(); 488 | } 489 | term.palette = {} 490 | if (term.mode === 0 || term.mode === 1) { 491 | for (let i = 0; i < 16; i++) { 492 | term.palette[i] = {} 493 | term.palette[i].r = stream.get(); 494 | term.palette[i].g = stream.get(); 495 | term.palette[i].b = stream.get(); 496 | } 497 | } else if (term.mode === 2) { 498 | for (let i = 0; i < 256; i++) { 499 | term.palette[i] = {} 500 | term.palette[i].r = stream.get(); 501 | term.palette[i].g = stream.get(); 502 | term.palette[i].b = stream.get(); 503 | } 504 | } 505 | } else if (type === 4) { 506 | const type2 = stream.get(); 507 | if (type2 === 2) { 508 | if (process_connection.connected) { 509 | process_connection.stdin.write("\n", "utf8"); 510 | process_connection.disconnect(); 511 | } else { 512 | process_connection.kill(SIGINT); 513 | //vscode.window.showWarningMessage("The CraftOS-PC worker process did not close correctly. Some changes may not have been saved.") 514 | } 515 | closeAllWindows(); 516 | return; 517 | } else if (type2 === 1) { 518 | if (windows[id].panel !== undefined) windows[id].panel.dispose(); 519 | delete windows[id]; 520 | computer_provider._onDidChangeTreeData.fire(null); 521 | monitor_provider._onDidChangeTreeData.fire(null); 522 | if (vslsServer !== null) vslsServer.notify("windows", windows); 523 | chunk = chunk.subarray(size + 16); 524 | while (String.fromCharCode(chunk[0]).match(/\s/)) chunk = chunk.subarray(1); 525 | continue; 526 | } else if (type2 === 0) { 527 | winid = stream.get(); 528 | term.width = stream.readUInt16(); 529 | term.height = stream.readUInt16(); 530 | term.title = ""; 531 | for (let c = stream.get(); c !== 0; c = stream.get()) term.title += String.fromCharCode(c); 532 | if (windows[id] !== undefined) { 533 | windows[id].isMonitor = typeof term.title === "string" && term.title.indexOf("Monitor") !== -1; 534 | if (winid > 0) { 535 | windows[id].computerID = winid - 1; 536 | windows[id].isMonitor = false; 537 | } else if (typeof term.title === "string" && term.title.match(/Computer \d+$/)) { 538 | windows[id].computerID = parseInt(term.title.match(/Computer (\d+)$/)[1]); 539 | } 540 | } 541 | } 542 | } else if (type === 5) { 543 | const flags = stream.readUInt32(); 544 | let title = ""; 545 | for (let c = stream.get(); c !== 0; c = stream.get()) title += String.fromCharCode(c); 546 | let message = ""; 547 | for (let c = stream.get(); c !== 0; c = stream.get()) message += String.fromCharCode(c); 548 | switch (flags) { 549 | case 0x10: vscode.window.showErrorMessage("CraftOS-PC: " + title + ": " + message); break; 550 | case 0x20: vscode.window.showWarningMessage("CraftOS-PC: " + title + ": " + message); break; 551 | case 0x40: vscode.window.showInformationMessage("CraftOS-PC: " + title + ": " + message); break; 552 | } 553 | } else if (type === 6) { 554 | const flags = stream.readUInt16(); 555 | isVersion11 = true; 556 | useBinaryChecksum = (flags & 1) === 1; 557 | supportsFilesystem = (flags & 2) === 2; 558 | computer_provider._onDidChangeTreeData.fire(null); 559 | if (vslsServer !== null) vslsServer.notify("flags", {isVersion11: isVersion11, useBinaryChecksum: useBinaryChecksum, supportsFilesystem: false}); 560 | } else if (type === 8) { 561 | const reqtype = stream.get(); 562 | const reqid = stream.get(); 563 | if (!dataRequestCallbacks[reqid]) { 564 | log.appendLine("Got stray response for request ID " + reqid + ", ignoring."); 565 | chunk = chunk.subarray(size + 16); 566 | while (String.fromCharCode(chunk[0]).match(/\s/)) chunk = chunk.subarray(1); 567 | continue; 568 | } 569 | switch (reqtype) { 570 | case 0: case 1: case 2: { 571 | const ok = stream.get(); 572 | if (ok === 0) dataRequestCallbacks[reqid](false); 573 | else if (ok === 1) dataRequestCallbacks[reqid](true); 574 | else dataRequestCallbacks[reqid](null, new Error("Operation failed")); 575 | break; 576 | } case 3: case 5: case 6: { 577 | const size = stream.readUInt32(); 578 | if (size === 0xFFFFFFFF) dataRequestCallbacks[reqid](null, new Error("Operation failed")); 579 | else dataRequestCallbacks[reqid](size); 580 | break; 581 | } case 4: { 582 | let str = ""; 583 | for (let c = stream.get(); c !== 0; c = stream.get()) str += String.fromCharCode(c); 584 | if (str !== "") dataRequestCallbacks[reqid](str); 585 | else dataRequestCallbacks[reqid](null, new Error("Operation failed")); 586 | break; 587 | } case 7: case 9: { 588 | const size = stream.readUInt32(); 589 | if (size === 0xFFFFFFFF) dataRequestCallbacks[reqid](null, new Error("Operation failed")); 590 | else { 591 | let arr = []; 592 | for (let i = 0; i < size; i++) { 593 | arr[i] = ""; 594 | for (let c = stream.get(); c !== 0; c = stream.get()) arr[i] += String.fromCharCode(c); 595 | } 596 | dataRequestCallbacks[reqid](arr); 597 | } 598 | break; 599 | } case 8: { 600 | let attr = {}; 601 | attr.size = stream.readUInt32(); 602 | attr.created = new Date(stream.readUInt64()); 603 | attr.modified = new Date(stream.readUInt64()); 604 | attr.isDir = stream.get() !== 0; 605 | attr.isReadOnly = stream.get() !== 0; 606 | const ok = stream.get(); 607 | if (ok === 0) dataRequestCallbacks[reqid](attr); 608 | else if (ok === 1) dataRequestCallbacks[reqid](null); 609 | else dataRequestCallbacks[reqid](null, new Error("Operation failed")); 610 | break; 611 | } case 10: case 11: case 12: case 13: 612 | case 16: case 17: case 18: case 19: 613 | case 20: case 21: case 22: case 23: { 614 | let str = ""; 615 | for (let c = stream.get(); c !== 0; c = stream.get()) str += String.fromCharCode(c); 616 | if (str === "") dataRequestCallbacks[reqid](); 617 | else dataRequestCallbacks[reqid](null, new Error(str)); 618 | break; 619 | } 620 | } 621 | delete dataRequestCallbacks[reqid]; 622 | } else if (type === 9) { 623 | const fail = stream.get(); 624 | const reqid = stream.get(); 625 | if (!dataRequestCallbacks[reqid]) { 626 | log.appendLine("Got stray data response for request ID " + reqid + ", ignoring."); 627 | chunk = chunk.subarray(size + 16); 628 | while (String.fromCharCode(chunk[0]).match(/\s/)) chunk = chunk.subarray(1); 629 | continue; 630 | } 631 | const size = stream.readUInt32(); 632 | const data = Buffer.alloc(size); 633 | stream.str.copy(data, 0, stream.pos); 634 | if (fail) dataRequestCallbacks[reqid](null, new Error(data.toString())); 635 | else dataRequestCallbacks[reqid](data); 636 | delete dataRequestCallbacks[reqid]; 637 | } 638 | let newWindow = false; 639 | if (windows[id] === undefined) {windows[id] = {}; newWindow = true;} 640 | if (windows[id].term === undefined) windows[id].term = {}; 641 | for (let k in term) windows[id].term[k] = term[k]; 642 | if (windows[id].isMonitor === undefined) { 643 | windows[id].isMonitor = typeof windows[id].term.title === "string" && windows[id].term.title.indexOf("Monitor") !== -1; 644 | if (winid !== null && winid > 0) { 645 | windows[id].computerID = winid - 1; 646 | windows[id].isMonitor = false; 647 | } else if (typeof windows[id].term.title === "string" && windows[id].term.title.match(/Computer \d+$/)) { 648 | windows[id].computerID = parseInt(windows[id].term.title.match(/Computer (\d+)$/)[1]); 649 | } 650 | } 651 | if (windows[id].panel !== undefined) { 652 | windows[id].panel.webview.postMessage(windows[id].term); 653 | windows[id].panel.title = windows[id].term.title || "CraftOS-PC Terminal"; 654 | } 655 | if (vslsServer !== null) { 656 | if (newWindow) vslsServer.notify("windows", windows); 657 | else vslsServer.notify("term", {id: id, term: windows[id].term, refresh: type === 4}); 658 | } 659 | if (type === 4) { 660 | computer_provider._onDidChangeTreeData.fire(null); 661 | monitor_provider._onDidChangeTreeData.fire(null); 662 | } 663 | chunk = chunk.subarray(size + off + 8); 664 | while (String.fromCharCode(chunk[0]).match(/\s/)) chunk = chunk.subarray(1); 665 | } 666 | } 667 | 668 | function connectToProcess(extra_args) { 669 | if (process_connection !== null) return true; 670 | const exe_path = getExecutable(); 671 | if (exe_path === null) { 672 | return false; 673 | } 674 | const dir = vscode.workspace.getConfiguration("craftos-pc").get("dataPath"); 675 | let process_options = { 676 | windowsHide: true 677 | }; 678 | let args = vscode.workspace.getConfiguration("craftos-pc").get("additionalArguments"); 679 | if (args !== null) {args = args.split(' '); args.push("--raw");} 680 | else args = ["--raw"]; 681 | if (dir !== null) args.splice(-1, 0, "-d", dir); 682 | if (extra_args !== null) args = args.concat(extra_args); 683 | log.appendLine("Running: " + exe_path + " " + args.join(" ")); 684 | try { 685 | process_connection = child_process.spawn(exe_path, args, process_options); 686 | } catch (e) { 687 | vscode.window.showErrorMessage("The CraftOS-PC worker process could not be launched. Check the path to the executable in the settings."); 688 | log.appendLine(e); 689 | return; 690 | } 691 | process_connection.on("error", () => { 692 | vscode.window.showErrorMessage("The CraftOS-PC worker process could not be launched. Check the path to the executable in the settings."); 693 | process_connection = null; 694 | closeAllWindows(); 695 | }); 696 | process_connection.on("exit", code => { 697 | vscode.window.showInformationMessage(`The CraftOS-PC worker process exited with code ${code}.`) 698 | process_connection = null; 699 | closeAllWindows(); 700 | }); 701 | process_connection.on("disconnect", () => { 702 | vscode.window.showErrorMessage(`The CraftOS-PC worker process was disconnected from the window.`) 703 | process_connection = null; 704 | closeAllWindows(); 705 | }); 706 | process_connection.on("close", () => { 707 | //vscode.window.showInformationMessage(`The CraftOS-PC worker process closed all IO streams with code ${code}.`) 708 | process_connection = null; 709 | closeAllWindows(); 710 | }); 711 | process_connection.stdout.on("data", processDataChunk); 712 | process_connection.stderr.on("data", data => { 713 | log.appendLine(data.toString()); 714 | }); 715 | //process_connection.stdin.write("!CPC0008BgACAA==FBAC4FC2\n"); // 0x0002 716 | process_connection.stdin.write("!CPC0008BgADAA==498C93D2\n"); // 0x0003 717 | //process_connection.stdin.write("!CPC0008BgAGAA==0E2CE902\n"); // 0x0006 718 | //process_connection.stdin.write("!CPC0008BgAHAA==8C7C7ED3\n"); // 0x0007 719 | vscode.window.showInformationMessage("A new CraftOS-PC worker process has been started."); 720 | openPanel(0, true); 721 | gotMessage = false; 722 | data_continuation = null; 723 | nextDataRequestID = 0; 724 | dataRequestCallbacks = {}; 725 | isVersion11 = false; 726 | useBinaryChecksum = false; 727 | supportsFilesystem = false; 728 | } 729 | 730 | function connectToWebSocket(url) { 731 | if (process_connection !== null || url === undefined) return true; 732 | const socket = new WebSocket(url); 733 | // We insert a small shim here so we don't have to rewrite the other code. 734 | process_connection = { 735 | connected: socket.readyState == WebSocket.OPEN, 736 | disconnect: () => socket.close(), 737 | kill: () => socket.close(), 738 | stdin: {write: data => {for (let i = 0; i < data.length; i += 65530) socket.send(data.substring(i, Math.min(i + 65530, data.length)))}} 739 | }; 740 | socket.on("open", () => { 741 | //socket.send("!CPC0008BgACAA==FBAC4FC2\n"); // 0x0002 742 | //socket.send("!CPC0008BgADAA==498C93D2\n"); // 0x0003 743 | //socket.send("!CPC0008BgAGAA==0E2CE902\n"); // 0x0006 744 | socket.send("!CPC0008BgAHAA==8C7C7ED3\n"); // 0x0007 745 | vscode.window.showInformationMessage("Successfully connected to the WebSocket server."); 746 | process_connection.connected = true; 747 | openPanel(0, true); 748 | }); 749 | socket.on("error", e => { 750 | if (e.message.match("certificate has expired")) 751 | vscode.window.showErrorMessage("A bug in VS Code is causing the connection to fail. Please go to https://www.craftos-pc.cc/docs/remote#certificate-has-expired-error to fix it.", "Open Page", "OK").then(res => { 752 | if (res === "OK") return; 753 | vscode.env.openExternal(vscode.Uri.parse("https://www.craftos-pc.cc/docs/remote#certificate-has-expired-error")); 754 | }); 755 | else vscode.window.showErrorMessage("An error occurred while connecting to the server: " + e.message); 756 | process_connection = null; 757 | closeAllWindows(); 758 | }); 759 | socket.on("close", () => { 760 | vscode.window.showInformationMessage("Disconnected from the WebSocket server."); 761 | process_connection = null; 762 | closeAllWindows(); 763 | }); 764 | socket.on("message", processDataChunk); 765 | gotMessage = false; 766 | data_continuation = null; 767 | nextDataRequestID = 0; 768 | dataRequestCallbacks = {}; 769 | isVersion11 = false; 770 | useBinaryChecksum = false; 771 | supportsFilesystem = false; 772 | } 773 | 774 | function openPanel(id, force) { 775 | if (!force && (extcontext === null || windows[id] === undefined)) return; 776 | if (windows[id] !== undefined && windows[id].panel !== undefined) { 777 | windows[id].panel.reveal(); 778 | return; 779 | } 780 | const customFont = vscode.workspace.getConfiguration("craftos-pc.customFont"); 781 | let fontPath = customFont.get("path"); 782 | if (fontPath === "hdfont") { 783 | const execPath = getSetting("craftos-pc.executablePath"); 784 | if (os.platform() === "win32") fontPath = execPath.replace(/[\/\\][^\/\\]+$/, "/") + "hdfont.bmp"; 785 | else if (os.platform() === "darwin" && execPath.indexOf("MacOS/craftos") !== -1) fontPath = execPath.replace(/MacOS\/[^\/]+$/, "") + "Resources/hdfont.bmp"; 786 | else if (os.platform() === "darwin" || (os.platform() === "linux" && !fs.existsSync("/usr/share/craftos/hdfont.bmp"))) fontPath = "/usr/local/share/craftos/hdfont.bmp"; 787 | else if (os.platform() === "linux") fontPath = "/usr/share/craftos/hdfont.bmp"; 788 | if (!fs.existsSync(fontPath)) { 789 | vscode.window.showWarningMessage("The path to the HD font could not be found; the default font will be used instead. Please set the path to the HD font manually."); 790 | fontPath = null; 791 | } 792 | } 793 | const panel = vscode.window.createWebviewPanel( 794 | 'craftos-pc', 795 | 'CraftOS-PC Terminal', 796 | vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn || vscode.ViewColumn.One, 797 | { 798 | enableScripts: true, 799 | retainContextWhenHidden: true, 800 | localResourceRoots: (fontPath !== null && fontPath !== "") ? [vscode.Uri.file(fontPath.replace(/[\/\\][^\/\\]*$/, ""))] : null 801 | } 802 | ); 803 | extcontext.subscriptions.push(panel); 804 | // Get path to resource on disk 805 | const onDiskPath = vscode.Uri.file(path.join(extcontext.extensionPath, 'index.html')); 806 | panel.iconPath = windows[id] && windows[id].isMonitor ? vscode.Uri.file(path.join(extcontext.extensionPath, 'media/monitor.svg')) : vscode.Uri.file(path.join(extcontext.extensionPath, 'media/computer.svg')); 807 | panel.webview.html = fs.readFileSync(onDiskPath.fsPath, 'utf8'); 808 | panel.webview.onDidReceiveMessage(message => { 809 | if (typeof message !== "object" || process_connection === null) return; 810 | if (message.getFontPath === true) { 811 | if (fontPath !== null && fontPath !== "") panel.webview.postMessage({fontPath: panel.webview.asWebviewUri(vscode.Uri.file(fontPath)).toString()}); 812 | return; 813 | } 814 | const data = Buffer.alloc(message.data.length / 2 + 2); 815 | data[0] = message.type; 816 | data[1] = id; 817 | Buffer.from(message.data, 'hex').copy(data, 2) 818 | const b64 = data.toString('base64'); 819 | const packet = "!CPC" + ("000" + b64.length.toString(16)).slice(-4) + b64 + ("0000000" + crc32(useBinaryChecksum ? data.toString("binary") : b64).toString(16)).slice(-8) + "\n"; 820 | process_connection.stdin.write(packet, 'utf8'); 821 | }); 822 | panel.onDidChangeViewState(e => { 823 | if (e.webviewPanel.active && windows[id].term !== undefined) { 824 | e.webviewPanel.webview.postMessage(windows[id].term); 825 | e.webviewPanel.title = windows[id].term.title || "CraftOS-PC Terminal"; 826 | } 827 | }); 828 | panel.onDidDispose(() => {if (windows[id].panel !== undefined) delete windows[id].panel;}); 829 | let newWindow = false; 830 | if (windows[id] === undefined) {windows[id] = {}; newWindow = true;} 831 | windows[id].panel = panel; 832 | if (windows[id].term !== undefined) { 833 | windows[id].panel.webview.postMessage(windows[id].term); 834 | windows[id].panel.title = windows[id].term.title || "CraftOS-PC Terminal"; 835 | } 836 | if (newWindow && vslsServer !== null) vslsServer.notify("windows", windows); 837 | } 838 | 839 | /** 840 | * @param {vscode.ExtensionContext} context 841 | */ 842 | function activate(context) { 843 | 844 | log.appendLine("The CraftOS-PC extension is now active."); 845 | 846 | extcontext = context; 847 | 848 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.open', () => { 849 | if (process_connection !== null) { 850 | vscode.window.showErrorMessage("Please close the current connection before using this command."); 851 | return; 852 | } 853 | return connectToProcess(); 854 | })); 855 | 856 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.open-websocket', obj => { 857 | if (process_connection !== null) { 858 | vscode.window.showErrorMessage("Please close the current connection before using this command."); 859 | return; 860 | } 861 | if (typeof obj === "string") return connectToWebSocket(obj); 862 | let wsHistory = context.globalState.get("JackMacWindows.craftos-pc/websocket-history", [""]); 863 | let quickPick = vscode.window.createQuickPick(); 864 | quickPick.items = wsHistory.map(val => {return {label: val}}); 865 | quickPick.title = "Enter the WebSocket URL:"; 866 | quickPick.placeholder = "wss://"; 867 | quickPick.canSelectMany = false; 868 | quickPick.onDidChangeValue(() => { 869 | wsHistory[0] = quickPick.value; 870 | quickPick.items = wsHistory.map(val => {return {label: val}}); 871 | }); 872 | quickPick.onDidAccept(() => { 873 | let str = quickPick.selectedItems[0].label; 874 | if (!validateURL(str)) vscode.window.showErrorMessage("The URL you entered is not valid."); 875 | else { 876 | wsHistory[0] = str; 877 | if (wsHistory.slice(1).includes(str)) 878 | wsHistory.splice(wsHistory.slice(1).indexOf(str)+1, 1); 879 | wsHistory.unshift(""); 880 | context.globalState.update("JackMacWindows.craftos-pc/websocket-history", wsHistory); 881 | connectToWebSocket(str); 882 | } 883 | }); 884 | quickPick.show(); 885 | })); 886 | 887 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.clear-history', () => { 888 | context.globalState.update("JackMacWindows.craftos-pc/websocket-history", [""]); 889 | })); 890 | 891 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.open-new-remote', () => { 892 | if (process_connection !== null) { 893 | vscode.window.showErrorMessage("Please close the current connection before using this command."); 894 | return; 895 | } 896 | if (!didShowBetaMessage) { 897 | vscode.window.showWarningMessage("remote.craftos-pc.cc is currently in beta. Be aware that things may not work as expected. If you run into issues, please report them [on GitHub](https://github.com/MCJack123/remote.craftos-pc.cc/issues). If things break, use Shift+Ctrl+P (Shift+Cmd+P on Mac), then type 'reload window' and press Enter."); 898 | didShowBetaMessage = true; 899 | } 900 | https.get("https://remote.craftos-pc.cc/new", res => { 901 | if (Math.floor(res.statusCode / 100) !== 2) { 902 | vscode.window.showErrorMessage("Could not connect to remote.craftos-pc.cc: HTTP " + res.statusCode); 903 | res.resume(); 904 | return; 905 | } 906 | res.setEncoding('utf8'); 907 | let id = ""; 908 | res.on('data', chunk => id += chunk); 909 | res.on('end', () => { 910 | vscode.env.clipboard.writeText("wget run https://remote.craftos-pc.cc/server.lua " + id); 911 | vscode.window.showInformationMessage("A command has been copied to the clipboard. Paste that into the ComputerCraft computer to establish the connection."); 912 | connectToWebSocket("wss://remote.craftos-pc.cc/" + id); 913 | }); 914 | }).on('error', e => { 915 | if (e.message.match("certificate has expired")) 916 | vscode.window.showErrorMessage("A bug in VS Code is causing the connection to fail. Please go to https://www.craftos-pc.cc/docs/remote#certificate-has-expired-error to fix it.", "Open Page", "OK").then(res => { 917 | if (res === "OK") return; 918 | vscode.env.openExternal(vscode.Uri.parse("https://www.craftos-pc.cc/docs/remote#certificate-has-expired-error")); 919 | }); 920 | else vscode.window.showErrorMessage("Could not connect to remote.craftos-pc.cc: " + e.message); 921 | }); 922 | })); 923 | 924 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.open-window', obj => { 925 | if (process_connection === null) { 926 | vscode.window.showErrorMessage("Please open CraftOS-PC before using this command."); 927 | return; 928 | } 929 | if (typeof obj === "object") openPanel(parseInt(obj.id)); 930 | else vscode.window.showInputBox({prompt: "Enter the window ID:", validateInput: str => isNaN(parseInt(str)) ? "Invalid number" : null}).then(id => openPanel(parseInt(id))); 931 | })); 932 | 933 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.open-config', () => { 934 | if (getDataPath() === null) { 935 | vscode.window.showErrorMessage("Please set the path to the CraftOS-PC data directory manually."); 936 | return; 937 | } 938 | vscode.commands.executeCommand("vscode.open", vscode.Uri.file(getDataPath() + "/config/global.json")); 939 | })); 940 | 941 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.open-computer-data', obj => { 942 | if (getDataPath() === null) { 943 | vscode.window.showErrorMessage("Please set the path to the CraftOS-PC data directory manually."); 944 | return; 945 | } 946 | if (typeof obj === "object") { 947 | vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(getDataPath() + "/computer/" + windows[parseInt(obj.id)].computerID), {forceNewWindow: true}); 948 | } else { 949 | vscode.window.showInputBox({prompt: "Enter the computer ID:", validateInput: str => isNaN(parseInt(str)) ? "Invalid number" : null}).then(value => { 950 | if (!fs.existsSync(getDataPath() + "/computer/" + value)) vscode.window.showErrorMessage("The computer ID provided does not exist."); 951 | else vscode.commands.executeCommand("vscode.openFolder", vscode.Uri.file(getDataPath() + "/computer/" + value)); 952 | }); 953 | } 954 | })); 955 | 956 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.open-remote-data', obj => { 957 | if (process_connection === null) { 958 | vscode.window.showErrorMessage("Please open CraftOS-PC before using this command."); 959 | return; 960 | } else if (!supportsFilesystem) { 961 | vscode.window.showErrorMessage("This connection does not support file system access."); 962 | return; 963 | } else if (!vscode.workspace.workspaceFile) { 964 | vscode.window.showWarningMessage("Due to technical limitations, opening the computer data will cause the connection to close. Please restart the connection after running this. Are you sure you want to continue?", "No", "Yes").then(opt => { 965 | if (opt === "No") return; 966 | // haha code duplication goes brrrrr 967 | if (typeof obj === "object") { 968 | deactivate(); 969 | vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, null, {name: obj.title.replace(/^.*: */, ""), uri: vscode.Uri.parse(`craftos-pc://${obj.id}/`)}); 970 | } else { 971 | vscode.window.showInputBox({prompt: "Enter the window ID:", validateInput: str => isNaN(parseInt(str)) ? "Invalid number" : null}).then(value => { 972 | if (typeof windows[value] !== "object") vscode.window.showErrorMessage("The window ID provided does not exist."); 973 | else vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, null, {name: windows[value].title.replace(/^.*: */, ""), uri: vscode.Uri.parse(`craftos-pc://${value}/`)}); 974 | }); 975 | } 976 | }); 977 | return; 978 | } 979 | if (typeof obj === "object") { 980 | vscode.window.showInformationMessage("A workspace is already open; the files have been added to the current workspace."); 981 | vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, null, {name: obj.title.replace(/^.*: */, ""), uri: vscode.Uri.parse(`craftos-pc://${obj.id}/`)}); 982 | } else { 983 | vscode.window.showInputBox({prompt: "Enter the window ID:", validateInput: str => isNaN(parseInt(str)) ? "Invalid number" : null}).then(value => { 984 | if (typeof windows[value] !== "object") vscode.window.showErrorMessage("The window ID provided does not exist."); 985 | else vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders.length : 0, null, {name: windows[value].title.replace(/^.*: */, ""), uri: vscode.Uri.parse(`craftos-pc://${value}/`)}); 986 | }); 987 | } 988 | })); 989 | 990 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.open-primary-remote-data', obj => { 991 | vscode.commands.executeCommand("craftos-pc.open-remote-data", {title: "", id: 0}) 992 | })); 993 | 994 | context.subscriptions.push(vscode.commands.registerCommand('craftos-pc.close', () => { 995 | if (process_connection === null) { 996 | vscode.window.showErrorMessage("Please open CraftOS-PC before using this command."); 997 | return; 998 | } 999 | process_connection.stdin.write(useBinaryChecksum ? "!CPC000CBAACAAAAAAAA2C7A548B\n" : "!CPC000CBAACAAAAAAAA3AB9B910\n", "utf8"); 1000 | })); 1001 | 1002 | context.subscriptions.push(vscode.commands.registerCommand("craftos-pc.close-window", obj => { 1003 | if (process_connection === null) { 1004 | vscode.window.showErrorMessage("Please open CraftOS-PC before using this command."); 1005 | return; 1006 | } 1007 | let id; 1008 | if (typeof obj === "object") id = new Promise(resolve => resolve(obj.id)); 1009 | else id = vscode.window.showInputBox({prompt: "Enter the window ID:", validateInput: str => isNaN(parseInt(str)) ? "Invalid number" : null}); 1010 | id.then(id => { 1011 | const data = Buffer.alloc(9); 1012 | data.fill(0); 1013 | data[0] = 4; 1014 | data[1] = parseInt(id); 1015 | data[2] = 1; 1016 | const b64 = data.toString("base64"); 1017 | process_connection.stdin.write("!CPC000C" + b64 + ("0000000" + crc32(useBinaryChecksum ? data.toString("binary") : b64).toString(16)).slice(-8) + "\n\n", "utf8"); 1018 | }); 1019 | })); 1020 | 1021 | context.subscriptions.push(vscode.commands.registerCommand("craftos-pc.kill", () => { 1022 | if (process_connection === null) { 1023 | vscode.window.showErrorMessage("Please open CraftOS-PC before using this command."); 1024 | return; 1025 | } 1026 | process_connection.stdin.write(useBinaryChecksum ? "!CPC000CBAACAAAAAAAA2C7A548B\n" : "!CPC000CBAACAAAAAAAA3AB9B910\n", "utf8"); 1027 | process_connection.kill(SIGINT); 1028 | process_connection = null; 1029 | })); 1030 | 1031 | context.subscriptions.push(vscode.commands.registerCommand("craftos-pc.run-file", path => { 1032 | if (process_connection === null) { 1033 | if (!path) { 1034 | if (vscode.window.activeTextEditor === undefined || vscode.window.activeTextEditor.document.uri.scheme !== "file") { 1035 | vscode.window.showErrorMessage("Please open or save a file on disk before using this command."); 1036 | return; 1037 | } 1038 | path = vscode.window.activeTextEditor.document.uri.fsPath; 1039 | } else if (typeof path === "object" && path instanceof vscode.Uri) { 1040 | if (path.scheme !== "file") { 1041 | vscode.window.showErrorMessage("Please open or save a file on disk before using this command."); 1042 | return; 1043 | } 1044 | path = path.fsPath; 1045 | } 1046 | return connectToProcess(["--script", path]); 1047 | } else vscode.window.showErrorMessage("Please close CraftOS-PC before using this command."); 1048 | })); 1049 | 1050 | context.subscriptions.push(vscode.workspace.registerFileSystemProvider("craftos-pc", new RawFileSystemProvider())); 1051 | context.subscriptions.push(vscode.window.registerUriHandler({handleUri: uri => { 1052 | vscode.commands.executeCommand("craftos-pc.open-websocket", uri.path.replace(/^\//, "")); 1053 | }})); 1054 | 1055 | context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("craftos-pc", debugAdapterFactory)); 1056 | 1057 | log = vscode.window.createOutputChannel("CraftOS-PC"); 1058 | 1059 | let change_timer = null; 1060 | vscode.workspace.onDidChangeConfiguration(event => { 1061 | if (event.affectsConfiguration("craftos-pc.executablePath")) { 1062 | // Use a timer to debounce quick autosaves 1063 | if (change_timer !== null) clearTimeout(change_timer); 1064 | change_timer = setTimeout(() => {change_timer = null; checkVersion(false);}, 3000); 1065 | } 1066 | }); 1067 | 1068 | vscode.window.createTreeView("craftos-computers", {"treeDataProvider": computer_provider}); 1069 | vscode.window.createTreeView("craftos-monitors", {"treeDataProvider": monitor_provider}); 1070 | 1071 | vsls.getApi(context.extension.id).then(api => { 1072 | if (api === null) return; 1073 | liveshare = api; 1074 | const updateSession = () => { 1075 | if (liveshare.session.role === vsls.Role.Host && vslsServer === null) { 1076 | if (vslsClient !== null && process_connection === null) { 1077 | windows = {}; 1078 | computer_provider._onDidChangeTreeData.fire(null); 1079 | monitor_provider._onDidChangeTreeData.fire(null); 1080 | vslsClient = null; 1081 | } 1082 | liveshare.shareService("terminal").then(svc => { 1083 | vslsServer = svc; 1084 | vslsServer.onNotify("packet", data => { 1085 | // TODO: Add actual access control (MicrosoftDocs/live-share#1716) 1086 | const peer = liveshare.peers.find(a => a.peerNumber == data.peer); 1087 | if (process_connection !== null && (peer.access === vsls.Access.ReadWrite || peer.access === vsls.Access.Owner)) process_connection.stdin.write(data.data, "utf8"); 1088 | }); 1089 | vslsServer.onNotify("get-windows", () => { 1090 | vslsServer.notify("windows", windows); 1091 | vslsServer.notify("flags", {isVersion11: isVersion11, useBinaryChecksum: useBinaryChecksum, supportsFilesystem: false}); 1092 | }); 1093 | }).catch(err => vscode.window.showErrorMessage("Could not create Live Share service: " + err)); 1094 | } else if (liveshare.session.role === vsls.Role.Guest && vslsClient === null) { 1095 | vslsServer = null; 1096 | liveshare.getSharedService("terminal").then(svc => { 1097 | vslsClient = svc; 1098 | vslsClient.onNotify("windows", param => { 1099 | let newwindows = {}; 1100 | for (let id in param) newwindows[id] = {term: param[id].term, isMonitor: param[id].isMonitor, panel: windows[id] ? windows[id].panel : undefined}; 1101 | for (let id in windows) if (!newwindows[id] && windows[id].panel) windows[id].panel.dispose(); 1102 | windows = newwindows; 1103 | computer_provider._onDidChangeTreeData.fire(null); 1104 | monitor_provider._onDidChangeTreeData.fire(null); 1105 | }); 1106 | vslsClient.onNotify("term", param => { 1107 | windows[param.id].term = param.term 1108 | if (windows[param.id].panel) { 1109 | windows[param.id].panel.webview.postMessage(param.term); 1110 | windows[param.id].panel.title = param.term.title || "CraftOS-PC Terminal"; 1111 | } 1112 | if (param.refresh) { 1113 | computer_provider._onDidChangeTreeData.fire(null); 1114 | monitor_provider._onDidChangeTreeData.fire(null); 1115 | } 1116 | }); 1117 | vslsClient.onNotify("flags", param => { 1118 | isVersion11 = param.isVersion11; 1119 | useBinaryChecksum = param.useBinaryChecksum; 1120 | supportsFilesystem = param.supportsFilesystem; 1121 | }); 1122 | process_connection = { 1123 | connected: true, 1124 | disconnect: () => {}, 1125 | kill: () => {}, 1126 | stdin: {write: data => { 1127 | if (liveshare.session.access === vsls.Access.ReadWrite || liveshare.session.access === vsls.Access.Owner) vslsClient.notify("packet", {data: data, peer: liveshare.session.peerNumber}); 1128 | }} 1129 | }; 1130 | vslsClient.notify("get-windows", {}); 1131 | }).catch(err => vscode.window.showErrorMessage("Could not connect to Live Share service: " + err)); 1132 | } else if (liveshare.session.role === vsls.Role.None) { 1133 | if (vslsClient !== null && process_connection !== null && process_connection.isLiveShare) { 1134 | windows = {}; 1135 | computer_provider._onDidChangeTreeData.fire(null); 1136 | monitor_provider._onDidChangeTreeData.fire(null); 1137 | vslsClient = null; 1138 | process_connection = null; 1139 | } 1140 | vslsServer = null; 1141 | } 1142 | }; 1143 | liveshare.onDidChangeSession(updateSession); 1144 | liveshare.onDidChangePeers(() => { 1145 | if (vslsServer !== null) vslsServer.notify("windows", windows); 1146 | }); 1147 | if (liveshare.session.role !== vsls.Role.None) updateSession(); 1148 | }); 1149 | 1150 | checkVersion(true); 1151 | } 1152 | exports.activate = activate; 1153 | 1154 | // this method is called when your extension is deactivated 1155 | function deactivate() { 1156 | if (process_connection) { 1157 | if (process_connection.connected) { 1158 | process_connection.stdin.write(useBinaryChecksum ? "!CPC000CBAACAAAAAAAA2C7A548B\n" : "!CPC000CBAACAAAAAAAA3AB9B910\n", "utf8"); 1159 | process_connection.disconnect(); 1160 | if (process_connection.isWS) process_connection = null; 1161 | } else { 1162 | process_connection.kill(SIGINT); 1163 | if (process_connection.isWS) process_connection = null; 1164 | } 1165 | } 1166 | closeAllWindows(); 1167 | } 1168 | 1169 | module.exports = { 1170 | activate, 1171 | deactivate 1172 | } 1173 | --------------------------------------------------------------------------------