├── .eslintrc.json ├── .github └── workflows │ ├── build-node-binaries.yml │ └── tests.yml ├── .gitignore ├── .prettierrc ├── .smalltalk.ston ├── .squot ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── getElements.js ├── images ├── google.png ├── lively.png ├── squeakjs.png └── youtube.png ├── logo ├── hat.svg ├── magicmouse.png ├── magicmouse.svg └── squeak.svg ├── package.json ├── packages ├── BaselineOfMagicMouse.package │ ├── .filetree │ ├── .squot-contents │ ├── BaselineOfMagicMouse.class │ │ ├── README.md │ │ ├── instance │ │ │ └── baseline..st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── monticello.meta │ │ ├── categories.st │ │ └── initializers.st │ └── properties.json ├── MagicMouse-Core.package │ ├── .filetree │ ├── .squot-contents │ ├── AttachableFileStream.extension │ │ ├── instance │ │ │ └── asMMBufferedStream.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMBrowser.class │ │ ├── README.md │ │ ├── class │ │ │ ├── open.st │ │ │ ├── openOn..st │ │ │ ├── rickRoll.st │ │ │ └── searchFor..st │ │ ├── instance │ │ │ ├── addOrRemoveBookmark.st │ │ │ ├── bookmarkButtonState.st │ │ │ ├── bookmarks.st │ │ │ ├── buildButtonBarWith..st │ │ │ ├── buildTopBarWith..st │ │ │ ├── buildWith..st │ │ │ ├── changeLocation..st │ │ │ ├── dragExtractedObject..st │ │ │ ├── exploreExtractedObject.st │ │ │ ├── extractedObjects..st │ │ │ ├── extractedObjects.st │ │ │ ├── fullscreen..st │ │ │ ├── getBrowserMorph.st │ │ │ ├── initialize.st │ │ │ ├── locationText..st │ │ │ ├── locationText.st │ │ │ ├── searchFor..st │ │ │ ├── searchText..st │ │ │ ├── searchText.st │ │ │ ├── selectedExtractedObject..st │ │ │ ├── selectedExtractedObject.st │ │ │ ├── showBookmarks.st │ │ │ └── structuredDataChanged..st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMBrowserMorph.class │ │ ├── README.md │ │ ├── class │ │ │ ├── chromeProfilePath..st │ │ │ ├── chromeProfilePath.st │ │ │ ├── debugEnabled..st │ │ │ ├── debugEnabled.st │ │ │ ├── debugNodejs..st │ │ │ ├── debugNodejs.st │ │ │ ├── doNotUsePrebuiltBinary..st │ │ │ ├── doNotUsePrebuiltBinary.st │ │ │ ├── gitRepositoryPath..st │ │ │ ├── gitRepositoryPath.st │ │ │ ├── initialize.st │ │ │ ├── logo.st │ │ │ ├── open.st │ │ │ ├── openOn..st │ │ │ ├── portalsOnRightClick..st │ │ │ ├── portalsOnRightClick.st │ │ │ ├── runChromeHeadless..st │ │ │ ├── runChromeHeadless.st │ │ │ ├── shutDown..st │ │ │ ├── startUp..st │ │ │ └── title.st │ │ ├── instance │ │ │ ├── acceptDroppingMorph.event..st │ │ │ ├── assertNodeScriptExists.st │ │ │ ├── changeLocation..st │ │ │ ├── connect..st │ │ │ ├── contentOfField.changedTo..st │ │ │ ├── delete.st │ │ │ ├── dispose.st │ │ │ ├── doRefresh.st │ │ │ ├── domainObjectFieldFor.object..st │ │ │ ├── drawOn..st │ │ │ ├── droppedDomainObject.at..st │ │ │ ├── droppedString.at..st │ │ │ ├── eventTypeCode..st │ │ │ ├── extent..st │ │ │ ├── fullscreen..st │ │ │ ├── getIdAndRectFrom.startAt..st │ │ │ ├── gitClone.named..st │ │ │ ├── goBack.st │ │ │ ├── goForward.st │ │ │ ├── handleCodePortal..st │ │ │ ├── handleFormFields..st │ │ │ ├── handleGitClone..st │ │ │ ├── handlePortal..st │ │ │ ├── handlePortalImage..st │ │ │ ├── handlePortalMorph..st │ │ │ ├── handlePortalRefreshData..st │ │ │ ├── handleStructuredData..st │ │ │ ├── handlesKeyboard..st │ │ │ ├── handlesMouseDown..st │ │ │ ├── handlesMouseMove..st │ │ │ ├── handlesMouseWheel..st │ │ │ ├── imageUpdaterProcess..st │ │ │ ├── imageUpdaterProcess.st │ │ │ ├── initialize.st │ │ │ ├── intoWorld..st │ │ │ ├── isConnected.st │ │ │ ├── isFullscreen.st │ │ │ ├── keyStroke..st │ │ │ ├── keyboardFocusColor.st │ │ │ ├── location.st │ │ │ ├── locationUpdated..st │ │ │ ├── mouseDown..st │ │ │ ├── mouseMove..st │ │ │ ├── mouseUp..st │ │ │ ├── mouseWheel..st │ │ │ ├── movePortal.rect..st │ │ │ ├── nameFromDomainObject..st │ │ │ ├── outOfWorld..st │ │ │ ├── portalMorphs..st │ │ │ ├── portalMorphs.st │ │ │ ├── possibleDomainObjectFieldsFor..st │ │ │ ├── preferredExtent.st │ │ │ ├── process..st │ │ │ ├── process.st │ │ │ ├── refreshPortals..st │ │ │ ├── searchFor..st │ │ │ ├── sendCommand.withBuffer..st │ │ │ ├── sendEvent..st │ │ │ ├── sendEvent.withBuffer..st │ │ │ ├── sendEvent.withNumber..st │ │ │ ├── sendEvent.withNumber.andNumber..st │ │ │ ├── sendEvent.withPoint..st │ │ │ ├── sendEvent.withString..st │ │ │ ├── updateImage..st │ │ │ ├── urlFromDomainObject..st │ │ │ ├── viewportSize..st │ │ │ ├── viewportSize.st │ │ │ └── wantsDroppedMorph.event..st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMBufferedStream.class │ │ ├── README.md │ │ ├── instance │ │ │ ├── binary.st │ │ │ ├── maxReadBufferSize.st │ │ │ └── waitForDataReady.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMFastJPEGReader.class │ │ ├── README.md │ │ ├── instance │ │ │ ├── nextImage..st │ │ │ └── nextImage.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMPluginDownloader.class │ │ ├── README.md │ │ ├── class │ │ │ ├── download..st │ │ │ ├── download.st │ │ │ ├── downloadForAllPlatforms.st │ │ │ ├── downloadIfConfirmed.st │ │ │ ├── downloadIfNeededAndConfirmed.st │ │ │ ├── filenameDict.st │ │ │ ├── filenameForCurrentPlatform.st │ │ │ └── removeDownloads.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMProcessWrapper.class │ │ ├── README.md │ │ ├── class │ │ │ └── newForOS.st │ │ ├── instance │ │ │ ├── disableDebug.st │ │ │ ├── enableDebug.st │ │ │ ├── initialize.st │ │ │ ├── process..st │ │ │ ├── process.st │ │ │ ├── startCommand.withArguments..st │ │ │ ├── startNodeScript.arguments.nodeArguments..st │ │ │ ├── stdinPutBuffer..st │ │ │ ├── stdinPutChar..st │ │ │ ├── stdinPutUint32..st │ │ │ ├── stdinPutUint8..st │ │ │ ├── stdoutNextBuffer..st │ │ │ ├── stdoutNextChar.st │ │ │ ├── stdoutNextUint32.st │ │ │ ├── stdoutNextUint8.st │ │ │ └── terminate.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMTextMorphWithModel.class │ │ ├── README.md │ │ ├── instance │ │ │ ├── aboutToStyle..st │ │ │ └── initialize.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMUnixProcessWrapper.class │ │ ├── README.md │ │ ├── instance │ │ │ ├── startCommand.withArguments..st │ │ │ ├── stdinPutBuffer..st │ │ │ ├── stdinPutUint32..st │ │ │ ├── stdinPutUint8..st │ │ │ ├── stdoutNextBuffer..st │ │ │ ├── stdoutNextUint32.st │ │ │ ├── stdoutNextUint8.st │ │ │ └── terminate.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMWindowsProcessWrapperFFI.class │ │ ├── README.md │ │ ├── instance │ │ │ ├── peek..st │ │ │ ├── readBuffer.length..st │ │ │ ├── startCommand.withArguments..st │ │ │ ├── stderrNextBuffer..st │ │ │ ├── stdinPutBuffer..st │ │ │ ├── stdinPutUint32..st │ │ │ ├── stdinPutUint8..st │ │ │ ├── stdoutNextBuffer..st │ │ │ ├── stdoutNextUint32.st │ │ │ ├── stdoutNextUint8.st │ │ │ ├── terminate.st │ │ │ └── waitFor.at..st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── MMWindowsProcessWrapperPW.class │ │ ├── README.md │ │ ├── instance │ │ │ ├── startCommand.withArguments..st │ │ │ ├── stdinPutBuffer..st │ │ │ ├── stdinPutUint32..st │ │ │ ├── stdinPutUint8..st │ │ │ ├── stdoutNextBuffer..st │ │ │ ├── stdoutNextUint32.st │ │ │ ├── stdoutNextUint8.st │ │ │ └── terminate.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── Object.extension │ │ ├── class │ │ │ └── rickRoll.st │ │ ├── instance │ │ │ └── rickRoll.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── SearchBar.extension │ │ ├── instance │ │ │ └── magicSearch.in..st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── SequenceableCollection.extension │ │ ├── instance │ │ │ └── findFirst.startingAt..st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── TextEditor.extension │ │ ├── instance │ │ │ └── browseClassFromIt.st │ │ ├── methodProperties.json │ │ └── properties.json │ ├── monticello.meta │ │ ├── categories.st │ │ ├── initializers.st │ │ ├── postscript.st │ │ └── preambleOfRemoval.st │ └── properties.json └── MagicMouse-Tests.package │ ├── .filetree │ ├── .squot-contents │ ├── MMBrowserMorphTest.class │ ├── README.md │ ├── instance │ │ ├── testHandleStructuredData.st │ │ └── testHandleStructuredDataEmpty.st │ ├── methodProperties.json │ └── properties.json │ ├── monticello.meta │ ├── categories.st │ └── initializers.st │ └── properties.json ├── patches └── puppeteer-core+1.19.0.patch ├── run.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true, 5 | "commonjs": true, 6 | "es2021": true 7 | }, 8 | "extends": ["eslint:recommended", "prettier"], 9 | "parserOptions": { 10 | "ecmaVersion": 13 11 | }, 12 | "rules": { 13 | "no-constant-condition": ["error", { "checkLoops": false }] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/build-node-binaries.yml: -------------------------------------------------------------------------------- 1 | name: Build Node.js Binaries 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Use Node.js 14.x 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 14.x 18 | 19 | - name: Get yarn cache directory path 20 | id: yarn-cache-dir-path 21 | run: echo "::set-output name=dir::$(yarn cache dir)" 22 | - uses: actions/cache@v2 23 | with: 24 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 25 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-yarn- 28 | 29 | 30 | - run: yarn install --frozen-lockfile 31 | - run: yarn lint 32 | - run: yarn build 33 | 34 | - name: Upload Binaries 35 | run: | 36 | go get -u github.com/tcnksm/ghr 37 | $HOME/go/bin/ghr -replace -b "Version https://github.com/cmfcmf/MagicMouse/commit/$GITHUB_SHA" latest ./build/ 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | smalltalk: [Squeak64-trunk, Squeak64-5.3] 15 | name: ${{ matrix.smalltalk }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: hpi-swa/setup-smalltalkCI@v1 19 | with: 20 | smalltalk-image: ${{ matrix.smalltalk }} 21 | - run: smalltalkci -s ${{ matrix.smalltalk }} 22 | shell: bash 23 | timeout-minutes: 15 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | 3 | # Created by https://www.gitignore.io/api/node 4 | # Edit at https://www.gitignore.io/?templates=node 5 | 6 | ### Node ### 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Optional REPL history 59 | .node_repl_history 60 | 61 | # Output of 'npm pack' 62 | *.tgz 63 | 64 | # Yarn Integrity file 65 | .yarn-integrity 66 | 67 | # dotenv environment variables file 68 | .env 69 | .env.test 70 | 71 | # parcel-bundler cache (https://parceljs.org/) 72 | .cache 73 | 74 | # next.js build output 75 | .next 76 | 77 | # nuxt.js build output 78 | .nuxt 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless/ 85 | 86 | # FuseBox cache 87 | .fusebox/ 88 | 89 | # DynamoDB Local files 90 | .dynamodb/ 91 | 92 | # End of https://www.gitignore.io/api/node 93 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "printWidth": 100 4 | } 5 | -------------------------------------------------------------------------------- /.smalltalk.ston: -------------------------------------------------------------------------------- 1 | SmalltalkCISpec { 2 | #loading : [ 3 | SCIMetacelloLoadSpec { 4 | #baseline : 'MagicMouse', 5 | #directory : 'packages', 6 | #platforms : [ #squeak ], 7 | #load : 'tests' 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.squot: -------------------------------------------------------------------------------- 1 | OrderedDictionary { 2 | 'packages/BaselineOfMagicMouse.package' : #SquotCypressCodeSerializer, 3 | 'packages/MagicMouse-Core.package' : #SquotCypressCodeSerializer, 4 | 'packages/MagicMouse-Tests.package' : #SquotCypressCodeSerializer 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 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 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach", 11 | "port": 9229 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christian Flach 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 | # MagicMouse Logo MagicMouse Webbrowser 2 | 3 | [![Tests](https://github.com/cmfcmf/MagicMouse/actions/workflows/tests.yml/badge.svg)](https://github.com/cmfcmf/MagicMouse/actions/workflows/tests.yml) 4 | [![Build Node.js Binaries](https://github.com/cmfcmf/MagicMouse/actions/workflows/build-node-binaries.yml/badge.svg)](https://github.com/cmfcmf/MagicMouse/actions/workflows/build-node-binaries.yml) 5 | 6 | MagicMouse is a webbrowser for [Squeak](https://squeak.org). It uses Chrome/Chromium under the hood to render websites and displays them as ImageMorphs right inside Squeak. 7 | 8 | MagicMouse runs on all platforms that run Squeak and Chrome, but works best and is most tested on Windows. 9 | 10 | | | | 11 | | ---------------------------------------------- | ------------------------------------------------- | 12 | | ![Screenshot of Google](images/google.png) | ![Screenshot of YouTube](images/youtube.png) | 13 | | ![Screenshot of SqueakJS](images/squeakjs.png) | ![Screenshot of Lively Kernel](images/lively.png) | 14 | 15 | ## Features 16 | 17 | - Browse the web with a real browser right from Squeak. 18 | - Watch YouTube videos in Squeak. 19 | - Right-click on images and code to transform them into `Morph`s. I call those "Portals". Because portals are cool. 20 | - Supercharged search bar: 21 | - If you type something with at least one space, your search terms are googled. 22 | - If you start your search with `!s `, your search terms are used to search https://squeak.org. 23 | - `CTRL+L` toggles fullscreen. 24 | - Dropped texts are typed into form fields. 25 | - _browseIt_ any URL to open it in a browser. 26 | - Rick roll yourself! Send `rickRoll` to your favourite class or object to get started. 27 | - Open browsers reconnect after resuming Squeak. 28 | - Create and browse bookmarks. 29 | - [Home Desktop System](https://github.com/hpi-swa-lab/home-desktop-system) integration: 30 | - Extracts structured data into `DomainObject`s 31 | - Searches for dropped `DomainObject`s 32 | - Fills form fields with dropped `DomainObject`s 33 | - [Squot](https://github.com/hpi-swa/Squot) integration: 34 | - When browsing GitHub repositories, displays a direct download button that opens the repository in Squot. 35 | - [Google Slides](https://slides.google.com) integration: 36 | - Display morphs right inside your presentation! Any rectangular boxes added to the slides whose text starts with `!` will be evaluated. 37 | - [PolyCode](https://github.com/hpi-swa-lab/pp19-6-code-editor) integration: 38 | - Display mp3s, mp4s, svgs and more inside the code editor using MagicMouse. 39 | 40 | ## Installation 41 | 42 | ⚠ **See below for installation instructions if you want to develop MagicMouse!** ⚠ 43 | 44 | 1. Install a recent version of Chrome or Chromium. MagicMouse uses the Chrome DevTools protocol to communicate with the browser and relies on the newish and experimental [`startScreencast`](https://chromedevtools.github.io/devtools-protocol/tot/Page#method-startScreencast) functionality. Chrome 76 works for me. 45 | 2. Install Squeak 5.2+ (you probably have that already :D). 46 | 3. Install MagicMouse via Metacello. 47 | 48 | ```smalltalk 49 | Metacello new 50 | baseline: 'MagicMouse'; 51 | repository: 'github://cmfcmf/MagicMouse:master/packages'; 52 | load. 53 | ``` 54 | 55 | You will be prompted to download a binary that acts as a bridge between Squeak and Chrome. See the `MMPluginDownloader` for details. Binaries are stored at [Bintray](https://bintray.com/cmfcmf/MagicMouse/node-bridge/latest?tab=files#files/). 56 | 57 | ## Usage 58 | 59 | MagicMouse provides two core classes for you to use: `MMBrowserMorph`, a simple image morph, and `MMBrowser`, a webbrowser with location and search bar. 60 | 61 | In general, be advised that there is little to no error handling in place right now. If your webbrowser is no longer responding, there is a good chance it crashed. It is helpful to enable the `Enable debug` preference and watch the Transcript output to troubleshoot problems. 62 | 63 | ### `MMBrowserMorph` 64 | 65 | This ImageMorph displays a website. It intercepts keyboard and mouse events and sends them to Chrome. 66 | Open it by using the `open` or `openOn:` messages: 67 | 68 | ```smalltalk 69 | MMBrowserMorph open "open with default url" 70 | 71 | MMBrowserMorph openOn: 'https://github.com/cmfcmf/MagicMouse' 72 | ``` 73 | 74 | ### `MMBrowser` 75 | 76 | This tool features a location bar, a Google search bar and the browser content itself. Open it using: 77 | 78 | ```smalltalk 79 | MMBrowser open "open with default url" 80 | 81 | MMBrowser openOn: 'https://github.com/cmfcmf/MagicMouse' 82 | ``` 83 | 84 | ## How it works 85 | 86 | There are three components to make it work: Squeak, an intermediate Node.js script and Chrome. 87 | 88 | ### `run.js` <-[DevTools Protocol via WebSocket]-> Chrome 89 | 90 | If you are using the prebuilt binary (default and recommended for people who don't want to develop MagicMouse), the `run.js` script is packaged together with Node.js into a single binary. 91 | We use Node.js and the [puppeteer-core](https://www.npmjs.com/package/puppeteer-core) library to spawn and communicate with Chrome/Chromium. Puppeteer uses the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) to connect to Chrome. The DevTools protocol uses WebSockets for communication. The required Node.js script is located inside this repostiory and called `run.js`. It spawns the browser and then calls `startScreencast()` which instructs Chrome to regularly take a screenshot of the current window and send it to Node.js. Chrome is smart enough to only take screenshots when the page visually changes. 92 | 93 | ### Squeak <-[Pipe]-> `run.js` 94 | 95 | Squeak communicates with the `run.js` script via `stdout`, `stdin` and `stderr`. Depending on the operating system, we either use OSProcess (Unix) or FFI (Windows) to pipe these streams into Squeak. We used ProcessWrapper on Windows previously, but found it rather unreliable. 96 | 97 | ### `stdout` and `stdin` 98 | 99 | `stdout` and `stdin` are used for the communication between Squeak and `run.js`. We use a simple, custom protocol for communication. Each message starts with a `uint32_t` that specifies the length of the message in bytes. The following `char` identifies the type of the message. The rest of the message is specific to the message type. Some example messages include: 100 | 101 | #### `stdout`: `run.js` -> Squeak 102 | 103 | - browser screenshots 104 | - location changes 105 | - portal data (i.e., the data of an image that was requested by a "portal creation request") 106 | - portal positions (everytime the page is re-rendered, an updated list of all portal positions is sent to Squeak) 107 | 108 | #### `stdin`: Squeak -> `run.js` 109 | 110 | - mouse movement, mouse events 111 | - keyboard events 112 | - viewport size changes 113 | - portal creation requests 114 | - location change requests 115 | 116 | ### `stderr` 117 | 118 | `stderr` is used for debugging only. The `run.js` script prints debug information to `stderr`, which is read by Squeak and written to the Transcript if the `Enable debug` option is set. 119 | 120 | ## Installation for Developers 121 | 122 | The installation is a bit cumbersome at the moment: 123 | 124 | 1. Install MagicMouse via Metacello like described above. This will also download necessary dependencies. 125 | 2. Install MagicMouse via the Git Browser. 126 | 3. Clone this repository using your command-line Git client (**not** Squit). Choose a different folder than when you cloned using Squit. 127 | 4. Run `yarn install` within the cloned repository. You may need to install [Yarn](https://yarnpkg.com/lang/en/). 128 | 5. Open the Squeak Perference Browser and 129 | - Change the `Git Repository Path` in the MagicMouse category to match the location of the cloned repository. 130 | - Check the `Do not use prebuilt binary` option. 131 | - You probably also want to enable the `Enable debug` property to log debug output to the Transcript. 132 | 133 | ## Contributing 134 | 135 | Your contribution to this project is highly appreciated. If you are contributing to this project please note that the Smalltalk / Squeak part of the project uses the [poppy-print](https://github.com/hpi-swa-teaching/poppy-print) code formatter by @tom95. 136 | 137 | To install this formatter execute 138 | 139 | ```smalltalk 140 | Metacello new baseline: 'PoppyPrint'; repository: 'github://tom95/poppy-print/packages'; load. 141 | ``` 142 | 143 | in a workspace. Then the following can be used to format the smalltalk code: 144 | 145 | ```smalltalk 146 | PPFormatter formatPackage: 'MagicMouse'. 147 | ``` 148 | 149 | The nodejs part of the project also has a formatter called prettier. Additionally, it is linted via eslint. 150 | 151 | To format and lint the code with automated fixing use `yarn format`. 152 | 153 | Please make sure that all your submitted code is formatted with the given formatters and that the nodejs part is linted, before merging your contribution. 154 | -------------------------------------------------------------------------------- /getElements.js: -------------------------------------------------------------------------------- 1 | const getElements = (fn, ...args) => { 2 | const ID_ATTRIBUTE = "data-magic-mouse-id"; 3 | 4 | const getIdOf = async (element) => { 5 | let id = element.getAttribute(ID_ATTRIBUTE); 6 | if (!id) { 7 | id = await window.uuid(); 8 | element.setAttribute(ID_ATTRIBUTE, id); 9 | } 10 | return id; 11 | }; 12 | 13 | // Based on this Gist by @ciaranj 14 | // https://gist.github.com/ciaranj/7177fb342102e571db2784dc831f868b 15 | // which is based on this StackOverflow answer by Édouard Mercier: 16 | // https://stackoverflow.com/a/50916681/2560557 17 | const calculateContainsWindow = (element) => { 18 | const imageComputedStyle = window.getComputedStyle(element); 19 | const imageObjectFit = imageComputedStyle.getPropertyValue("object-fit"); 20 | const coords = {}; 21 | const imagePositions = imageComputedStyle.getPropertyValue("object-position").split(" "); 22 | let naturalWidth = element.naturalWidth; 23 | let naturalHeight = element.naturalHeight; 24 | if (element.tagName === "VIDEO") { 25 | naturalWidth = element.videoWidth; 26 | naturalHeight = element.videoHeight; 27 | } else if (element.tagName === "CANVAS") { 28 | naturalWidth = element.width; 29 | naturalHeight = element.height; 30 | } 31 | const horizontalPercentage = parseInt(imagePositions[0]) / 100; 32 | const verticalPercentage = parseInt(imagePositions[1]) / 100; 33 | const naturalRatio = naturalWidth / naturalHeight; 34 | const visibleRatio = element.clientWidth / element.clientHeight; 35 | 36 | if (imageObjectFit === "none") { 37 | coords.sourceWidth = element.clientWidth; 38 | coords.sourceHeight = element.clientHeight; 39 | coords.sourceX = (naturalWidth - element.clientWidth) * horizontalPercentage; 40 | coords.sourceY = (naturalHeight - element.clientHeight) * verticalPercentage; 41 | coords.destinationWidthPercentage = 1; 42 | coords.destinationHeightPercentage = 1; 43 | coords.destinationXPercentage = 0; 44 | coords.destinationYPercentage = 0; 45 | } else if (imageObjectFit === "contain" || imageObjectFit === "scale-down") { 46 | // TODO: handle the "scale-down" appropriately, once its meaning will be clear 47 | coords.sourceWidth = naturalWidth; 48 | coords.sourceHeight = naturalHeight; 49 | coords.sourceX = 0; 50 | coords.sourceY = 0; 51 | if (naturalRatio > visibleRatio) { 52 | coords.destinationWidthPercentage = 1; 53 | coords.destinationHeightPercentage = 54 | naturalHeight / element.clientHeight / (naturalWidth / element.clientWidth); 55 | coords.destinationXPercentage = 0; 56 | coords.destinationYPercentage = 57 | (1 - coords.destinationHeightPercentage) * verticalPercentage; 58 | } else { 59 | coords.destinationWidthPercentage = 60 | naturalWidth / element.clientWidth / (naturalHeight / element.clientHeight); 61 | coords.destinationHeightPercentage = 1; 62 | coords.destinationXPercentage = 63 | (1 - coords.destinationWidthPercentage) * horizontalPercentage; 64 | coords.destinationYPercentage = 0; 65 | } 66 | } else if (imageObjectFit === "cover") { 67 | if (naturalRatio > visibleRatio) { 68 | coords.sourceWidth = naturalHeight * visibleRatio; 69 | coords.sourceHeight = naturalHeight; 70 | coords.sourceX = (naturalWidth - coords.sourceWidth) * horizontalPercentage; 71 | coords.sourceY = 0; 72 | } else { 73 | coords.sourceWidth = naturalWidth; 74 | coords.sourceHeight = naturalWidth / visibleRatio; 75 | coords.sourceX = 0; 76 | coords.sourceY = (naturalHeight - coords.sourceHeight) * verticalPercentage; 77 | } 78 | coords.destinationWidthPercentage = 1; 79 | coords.destinationHeightPercentage = 1; 80 | coords.destinationXPercentage = 0; 81 | coords.destinationYPercentage = 0; 82 | } else if (imageObjectFit === "fill") { 83 | coords.sourceWidth = naturalWidth; 84 | coords.sourceHeight = naturalHeight; 85 | coords.sourceX = 0; 86 | coords.sourceY = 0; 87 | coords.destinationWidthPercentage = 1; 88 | coords.destinationHeightPercentage = 1; 89 | coords.destinationXPercentage = 0; 90 | coords.destinationYPercentage = 0; 91 | } else { 92 | console.error( 93 | "unexpected 'object-fit' attribute with value '" + imageObjectFit + "' relative to", 94 | ); 95 | } 96 | return coords; 97 | }; 98 | 99 | // Convert an element to a base64-encoded string. 100 | const img2Base64 = (img) => { 101 | const canvas = document.createElement("canvas"); 102 | canvas.width = img.naturalWidth; 103 | canvas.height = img.naturalHeight; 104 | const ctx = canvas.getContext("2d"); 105 | ctx.drawImage(img, 0, 0); 106 | return canvas.toDataURL("image/png").split(",")[1]; 107 | }; 108 | 109 | const extractImg = async (img, includeData = true) => { 110 | const rect = img.getBoundingClientRect(); 111 | 112 | const style = window.getComputedStyle(img); 113 | const offsetX = parseInt(style.paddingLeft, 10); 114 | const offsetY = parseInt(style.paddingTop, 10); 115 | 116 | const w = img.width; 117 | const h = img.height; 118 | 119 | return { 120 | id: await getIdOf(img), 121 | type: "img", 122 | x: rect.x + offsetX, 123 | y: rect.y + offsetY, 124 | w, 125 | h, 126 | data: includeData ? img2Base64(img) : null, 127 | }; 128 | }; 129 | 130 | const extractCanvas = async (canvas, includeData = true) => { 131 | const rect = canvas.getBoundingClientRect(); 132 | 133 | const style = window.getComputedStyle(canvas); 134 | const offsetX = parseInt(style.paddingLeft, 10); 135 | const offsetY = parseInt(style.paddingTop, 10); 136 | 137 | let x = rect.x + offsetX; 138 | let y = rect.y + offsetY; 139 | let w = rect.width; 140 | let h = rect.height; 141 | 142 | if (style.objectFit === "contain" && style.objectPosition === "50% 50%") { 143 | const c = calculateContainsWindow(canvas); 144 | x += c.destinationXPercentage * rect.width; 145 | y += c.destinationYPercentage * rect.height; 146 | w = c.destinationWidthPercentage * w; 147 | h = c.destinationHeightPercentage * h; 148 | } 149 | 150 | return { 151 | id: await getIdOf(canvas), 152 | type: "canvas", 153 | x, 154 | y, 155 | w, 156 | h, 157 | data: includeData ? canvas.toDataURL("image/png").split(",")[1] : null, 158 | }; 159 | }; 160 | 161 | const extractPre = async (element, includeData = true) => { 162 | const viewport = { 163 | w: document.documentElement.clientWidth, 164 | h: document.documentElement.clientHeight, 165 | }; 166 | 167 | const rect = element.getBoundingClientRect(); 168 | 169 | let dw = 0; 170 | if (rect.right > viewport.w) { 171 | dw = rect.right - viewport.w; 172 | } 173 | let dh = 0; 174 | if (rect.bottom > viewport.h) { 175 | dh = rect.bottom - viewport.h; 176 | } 177 | 178 | const id = await getIdOf(element); 179 | 180 | return { 181 | id, 182 | type: "pre", 183 | x: rect.x, 184 | y: rect.y, 185 | w: rect.width - dw, 186 | h: rect.height - dh, 187 | data: includeData ? element.textContent : null, 188 | }; 189 | }; 190 | 191 | const extract = async (element, includeData = true) => { 192 | if (element.tagName === "IMG") { 193 | return extractImg(element, includeData); 194 | } else if (element.tagName === "CANVAS") { 195 | return extractCanvas(element, includeData); 196 | } else if (element.tagName === "PRE") { 197 | return extractPre(element, includeData); 198 | } else { 199 | const rect = element.getBoundingClientRect(); 200 | return { 201 | id: await getIdOf(element), 202 | type: "other", 203 | x: rect.x, 204 | y: rect.y, 205 | w: rect.width, 206 | h: rect.height, 207 | data: null, 208 | }; 209 | } 210 | }; 211 | 212 | const extractElements = (x, y) => { 213 | return Promise.all( 214 | document 215 | .elementsFromPoint(x, y) 216 | .filter( 217 | (element) => 218 | element.tagName === "IMG" || element.tagName === "CANVAS" || element.tagName === "PRE", 219 | ) 220 | .map(async (element) => extract(element)), 221 | ); 222 | }; 223 | 224 | const refreshInfo = () => { 225 | const infos = []; 226 | 227 | document 228 | .querySelectorAll(`[${ID_ATTRIBUTE}]`) 229 | .forEach((element) => infos.push(extract(element, false))); 230 | 231 | return Promise.all(infos); 232 | }; 233 | 234 | switch (fn) { 235 | case "extractElements": 236 | return extractElements(...args); 237 | case "refreshInfo": 238 | return refreshInfo(...args); 239 | } 240 | }; 241 | 242 | module.exports = { 243 | getElements, 244 | }; 245 | -------------------------------------------------------------------------------- /images/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/images/google.png -------------------------------------------------------------------------------- /images/lively.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/images/lively.png -------------------------------------------------------------------------------- /images/squeakjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/images/squeakjs.png -------------------------------------------------------------------------------- /images/youtube.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/images/youtube.png -------------------------------------------------------------------------------- /logo/magicmouse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/logo/magicmouse.png -------------------------------------------------------------------------------- /logo/magicmouse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 32 | 39 | 41 | 45 | 49 | 50 | 57 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 84 | 94 | 104 | 114 | 124 | 126 | 130 | 134 | 138 | 142 | 146 | 147 | 157 | 164 | 171 | 178 | 187 | 190 | 194 | 198 | 199 | 209 | 212 | 216 | 220 | 221 | 231 | 234 | 238 | 242 | 243 | 252 | 259 | 269 | 279 | 281 | 285 | 289 | 290 | 291 | 311 | 313 | 314 | 316 | image/svg+xml 317 | 319 | 320 | 321 | 322 | 323 | 328 | 332 | 335 | 338 | 342 | 347 | 351 | 356 | 361 | 366 | 370 | 375 | 380 | 385 | 390 | 395 | 400 | 401 | 407 | 423 | 429 | 445 | 461 | 477 | 483 | 499 | 515 | 531 | 547 | 563 | 568 | 573 | 589 | 594 | 600 | 606 | 607 | 608 | 609 | 610 | 611 | -------------------------------------------------------------------------------- /logo/squeak.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "author": { 4 | "name": "Christian Flach", 5 | "email": "cmfcmf.flach@gmail.com" 6 | }, 7 | "scripts": { 8 | "build": "pkg --targets node16-win,node16-macos,node16-linux --public --output ./build/magicmouse ./run.js", 9 | "postinstall": "patch-package", 10 | "lint": "eslint --cache --quiet ./ && prettier --list-different --config .prettierrc \"*.js\"", 11 | "format": "eslint --cache --fix --quiet . && prettier --write --config .prettierrc \"*.js\"" 12 | }, 13 | "dependencies": { 14 | "awaitify-stream": "^1.0.2", 15 | "chrome-finder": "^1.0.5", 16 | "get-pixels": "^3.3.2", 17 | "patch-package": "^6.1.2", 18 | "postinstall-postinstall": "^2.0.0", 19 | "puppeteer-core": "1.19.0", 20 | "smart-buffer": "^4.1.0", 21 | "uuid": "^8.3.2" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^8.5.0", 25 | "eslint-config-prettier": "^8.3.0", 26 | "pkg": "^5.5.1", 27 | "prettier": "^2.5.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/BaselineOfMagicMouse.package/.filetree: -------------------------------------------------------------------------------- 1 | { 2 | "noMethodMetaData" : true, 3 | "separateMethodMetaAndSource" : false, 4 | "useCypressPropertiesFile" : true } 5 | -------------------------------------------------------------------------------- /packages/BaselineOfMagicMouse.package/.squot-contents: -------------------------------------------------------------------------------- 1 | SquotTrackedObjectMetadata { 2 | #objectClassName : #PackageInfo, 3 | #objectsReplacedByNames : true, 4 | #serializer : #SquotCypressCodeSerializer 5 | } -------------------------------------------------------------------------------- /packages/BaselineOfMagicMouse.package/BaselineOfMagicMouse.class/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/packages/BaselineOfMagicMouse.package/BaselineOfMagicMouse.class/README.md -------------------------------------------------------------------------------- /packages/BaselineOfMagicMouse.package/BaselineOfMagicMouse.class/instance/baseline..st: -------------------------------------------------------------------------------- 1 | baseline 2 | baseline: spec 3 | 4 | spec for: #'common' do: [ 5 | spec 6 | baseline: 'Cmfcmf' with: [ 7 | spec 8 | repository: 'github://cmfcmf/SqueakUtils:master'; 9 | loads: #('winffi')]; 10 | project: 'OSProcess' with: [ 11 | spec 12 | className: 'ConfigurationOfOSProcess'; 13 | repository: 'http://www.squeaksource.com/MetacelloRepository'; 14 | versionString: #'stable']; 15 | package: 'JSON' with: [spec repository: 'http://www.squeaksource.com/JSON']; 16 | package: 'MagicMouse-Core' with: [spec requires: #('Cmfcmf' 'OSProcess' 'JSON')]; 17 | package: 'MagicMouse-Tests' with: [spec requires: #('MagicMouse-Core')]. 18 | spec 19 | group: 'default' with: #('MagicMouse-Core'); 20 | group: 'tests' with: #('MagicMouse-Tests'); 21 | yourself]; 22 | yourself -------------------------------------------------------------------------------- /packages/BaselineOfMagicMouse.package/BaselineOfMagicMouse.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "baseline:" : "cmfcmf 8/3/2019 13:37" } } 6 | -------------------------------------------------------------------------------- /packages/BaselineOfMagicMouse.package/BaselineOfMagicMouse.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "BaselineOfMagicMouse", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "", 8 | "instvars" : [ 9 | ], 10 | "name" : "BaselineOfMagicMouse", 11 | "pools" : [ 12 | ], 13 | "super" : "BaselineOf", 14 | "type" : "normal" } 15 | -------------------------------------------------------------------------------- /packages/BaselineOfMagicMouse.package/monticello.meta/categories.st: -------------------------------------------------------------------------------- 1 | SystemOrganization addCategory: #BaselineOfMagicMouse! 2 | -------------------------------------------------------------------------------- /packages/BaselineOfMagicMouse.package/monticello.meta/initializers.st: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/packages/BaselineOfMagicMouse.package/monticello.meta/initializers.st -------------------------------------------------------------------------------- /packages/BaselineOfMagicMouse.package/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/.filetree: -------------------------------------------------------------------------------- 1 | { 2 | "noMethodMetaData" : true, 3 | "separateMethodMetaAndSource" : false, 4 | "useCypressPropertiesFile" : true } 5 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/.squot-contents: -------------------------------------------------------------------------------- 1 | SquotTrackedObjectMetadata { 2 | #objectClassName : #PackageInfo, 3 | #objectsReplacedByNames : true, 4 | #serializer : #SquotCypressCodeSerializer 5 | } -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/AttachableFileStream.extension/instance/asMMBufferedStream.st: -------------------------------------------------------------------------------- 1 | *MagicMouse-Core 2 | asMMBufferedStream 3 | "Answer a replacement for this object, with asynchronous event handling 4 | and buffered output. Do not close the ioHandle when this object is finalized." 5 | 6 | self keepOpen. 7 | ^ MMBufferedStream 8 | name: self name 9 | attachTo: self ioHandle 10 | writable: self isReadOnly not 11 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/AttachableFileStream.extension/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "asMMBufferedStream" : "cf 4/18/2019 19:14" } } 6 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/AttachableFileStream.extension/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "AttachableFileStream" } 3 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/README.md: -------------------------------------------------------------------------------- 1 | I am a browser with address and location bar as well as bookmarks. 2 | Open myself by doit'ing one of 3 | 4 | MMBrowser open 5 | MMBrowser openOn: 'https://squeak.org' 6 | MMBrowser searchFor: 'squeak' -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/class/open.st: -------------------------------------------------------------------------------- 1 | instance creation 2 | open 3 | 4 | ^ ToolBuilder open: self new -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/class/openOn..st: -------------------------------------------------------------------------------- 1 | instance creation 2 | openOn: aUrl 3 | 4 | | model | 5 | model := self new. 6 | model locationText: aUrl. 7 | ^ ToolBuilder open: model -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/class/rickRoll.st: -------------------------------------------------------------------------------- 1 | instance creation 2 | rickRoll 3 | 4 | | model instance | 5 | model := self new. 6 | model locationText: 'https://www.youtube.com/embed/DLzxrzFCyOs?autoplay=1&loop=1&controls=0&modestbranding=1&showinfo=0'. 7 | instance := ToolBuilder open: model. 8 | model fullscreen: true. 9 | ^ instance -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/class/searchFor..st: -------------------------------------------------------------------------------- 1 | instance creation 2 | searchFor: aString 3 | 4 | | model | 5 | model := self new. 6 | ToolBuilder open: model. 7 | model searchFor: aString -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/addOrRemoveBookmark.st: -------------------------------------------------------------------------------- 1 | bookmarks 2 | addOrRemoveBookmark 3 | 4 | | url | 5 | url := self locationText asString. 6 | Bookmarks remove: url ifAbsent: [Bookmarks add: url] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/bookmarkButtonState.st: -------------------------------------------------------------------------------- 1 | bookmarks 2 | bookmarkButtonState 3 | 4 | ^ self bookmarks includes: self locationText asString -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/bookmarks.st: -------------------------------------------------------------------------------- 1 | bookmarks 2 | bookmarks 3 | 4 | ^ Bookmarks ifNil: [Bookmarks := OrderedCollection new] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/buildButtonBarWith..st: -------------------------------------------------------------------------------- 1 | building 2 | buildButtonBarWith: aBuilder 3 | 4 | ^ { 5 | aBuilder pluggableButtonSpec new 6 | model: self; 7 | action: #showBookmarks; 8 | label: 'Bookmarks'; 9 | yourself. 10 | aBuilder pluggableButtonSpec new 11 | model: self; 12 | action: [PreferenceBrowser open selectedCategory: #MagicMouse]; 13 | label: 'Preferences'; 14 | yourself} -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/buildTopBarWith..st: -------------------------------------------------------------------------------- 1 | building 2 | buildTopBarWith: aBuilder 3 | 4 | ^ { 5 | aBuilder pluggableButtonSpec new 6 | frame: ((LayoutFrame fractions: (0 @ 0 extent: 0 @ 1)) 7 | rightOffset: 25; 8 | yourself); 9 | model: self; 10 | help: 'back'; 11 | action: [browser goBack]; 12 | label: '<'; 13 | yourself. 14 | aBuilder pluggableButtonSpec new 15 | frame: ((LayoutFrame fractions: (0 @ 0 extent: 0 @ 1)) 16 | leftOffset: 25; 17 | rightOffset: 50; 18 | yourself); 19 | model: self; 20 | help: 'forward'; 21 | action: [browser goForward]; 22 | label: '>'; 23 | yourself. 24 | aBuilder pluggableInputFieldSpec new 25 | model: self; 26 | hardLineWrap: false; 27 | softLineWrap: false; 28 | getText: #locationText; 29 | setText: #changeLocation:; 30 | askBeforeDiscardingEdits: false; 31 | help: 'Location'; 32 | frame: ((LayoutFrame fractions: (0 @ 0 extent: 0.8 @ 1)) 33 | leftOffset: 50; 34 | rightOffset: -50; 35 | yourself); 36 | yourself. 37 | aBuilder pluggableButtonSpec new 38 | frame: ((LayoutFrame fractions: (0.8 @ 0 corner: 0.8 @ 1)) 39 | leftOffset: -50; 40 | rightOffset: -25; 41 | yourself); 42 | model: self; 43 | label: 'B'; 44 | action: #addOrRemoveBookmark; 45 | state: #bookmarkButtonState; 46 | help: 'bookmark'; 47 | yourself. 48 | aBuilder pluggableButtonSpec new 49 | frame: ((LayoutFrame fractions: (0.8 @ 0 corner: 0.8 @ 1)) 50 | leftOffset: -25; 51 | yourself); 52 | model: self; 53 | label: 'R'; 54 | action: [browser doRefresh]; 55 | help: 'refresh'; 56 | yourself. 57 | aBuilder pluggableInputFieldSpec new 58 | model: self; 59 | hardLineWrap: false; 60 | softLineWrap: false; 61 | getText: #searchText; 62 | setText: #searchFor:; 63 | askBeforeDiscardingEdits: false; 64 | help: 'Search with Google'; 65 | frame: (LayoutFrame fractions: (0.8 @ 0 extent: 0.2 @ 1)); 66 | yourself} -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/buildWith..st: -------------------------------------------------------------------------------- 1 | building 2 | buildWith: aBuilder 3 | 4 | ^ aBuilder build: (aBuilder pluggableWindowSpec new 5 | label: MMBrowserMorph title; 6 | extent: 600 @ 400; 7 | model: self; 8 | children: { 9 | aBuilder pluggablePanelSpec new 10 | wantsResizeHandles: true; 11 | frame: (LayoutFrame fractions: (0 @ 0 extent: 1 @ 0) offsets: (0 @ 0 extent: 0 @ 25)); 12 | children: (self buildTopBarWith: aBuilder); 13 | yourself. 14 | aBuilder pluggablePanelSpec new 15 | frame: (LayoutFrame fractions: (0 @ 0 extent: 1 @ 0) offsets: (0 @ 25 extent: 0 @ 25)); 16 | layout: #horizontal; 17 | children: (self buildButtonBarWith: aBuilder); 18 | yourself. 19 | aBuilder pluggablePanelSpec new 20 | wantsResizeHandles: true; 21 | frame: (LayoutFrame fractions: (0 @ 0 extent: 1 @ 1) offsets: (0 @ 50 extent: 0 @ -50)); 22 | spacing: 5; 23 | children: { 24 | aBuilder pluggablePanelSpec new 25 | model: self; 26 | frame: (0 @ 0 extent: 0.8 @ 1); 27 | children: #getBrowserMorph; 28 | yourself. 29 | aBuilder pluggableListSpec new 30 | model: self; 31 | frame: (0.8 @ 0 corner: 1 @ 1); 32 | list: #extractedObjects; 33 | doubleClick: #exploreExtractedObject; 34 | getSelected: #selectedExtractedObject; 35 | setSelected: #selectedExtractedObject:; 36 | dragItem: #dragExtractedObject:; 37 | yourself}; 38 | yourself}; 39 | yourself) -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/changeLocation..st: -------------------------------------------------------------------------------- 1 | public 2 | changeLocation: aText 3 | 4 | self locationText: aText. 5 | browser changeLocation: self locationText -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/dragExtractedObject..st: -------------------------------------------------------------------------------- 1 | extracted objects 2 | dragExtractedObject: aNumber 3 | 4 | | domainObject | 5 | domainObject := extractedObjects at: aNumber. 6 | domainObject undelete. 7 | 8 | ^ domainObject -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/exploreExtractedObject.st: -------------------------------------------------------------------------------- 1 | extracted objects 2 | exploreExtractedObject 3 | 4 | self selectedExtractedObject ifNotNil: [:object | object explore] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/extractedObjects..st: -------------------------------------------------------------------------------- 1 | accessing 2 | extractedObjects: aCollection 3 | 4 | extractedObjects := aCollection. 5 | self changed: #extractedObjects -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/extractedObjects.st: -------------------------------------------------------------------------------- 1 | accessing 2 | extractedObjects 3 | 4 | ^ extractedObjects -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/fullscreen..st: -------------------------------------------------------------------------------- 1 | public 2 | fullscreen: aBoolean 3 | 4 | browser fullscreen: aBoolean -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/getBrowserMorph.st: -------------------------------------------------------------------------------- 1 | building 2 | getBrowserMorph 3 | 4 | browser isConnected ifFalse: [ 5 | browser connect: self locationText. 6 | browser layoutFrame: (LayoutFrame fractions: (0 @ 0 extent: 1 @ 1)). 7 | browser when: #locationChanged send: #locationText: to: self. 8 | browser when: #structuredDataChanged send: #structuredDataChanged: to: self]. 9 | ^ {browser} -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/initialize.st: -------------------------------------------------------------------------------- 1 | initialize-release 2 | initialize 3 | 4 | super initialize. 5 | locationText := 'https://google.com'. 6 | searchText := ''. 7 | browser := MMBrowserMorph new. 8 | extractedObjects := OrderedCollection new -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/locationText..st: -------------------------------------------------------------------------------- 1 | accessing 2 | locationText: aText 3 | 4 | | url | 5 | url := aText asString. 6 | ((url findString: 'https://') = 0 and: [(url findString: 'http://') = 0]) ifTrue: [url := 'https://', url]. 7 | locationText := url. 8 | self changed: #locationText. 9 | self changed: #bookmarkButtonState -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/locationText.st: -------------------------------------------------------------------------------- 1 | accessing 2 | locationText 3 | 4 | ^ locationText -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/searchFor..st: -------------------------------------------------------------------------------- 1 | public 2 | searchFor: aText 3 | 4 | | search | 5 | search := aText asString. 6 | self searchText: search. 7 | browser searchFor: search -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/searchText..st: -------------------------------------------------------------------------------- 1 | accessing 2 | searchText: aString 3 | 4 | searchText := aString. 5 | self changed: #searchText -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/searchText.st: -------------------------------------------------------------------------------- 1 | accessing 2 | searchText 3 | 4 | ^ searchText -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/selectedExtractedObject..st: -------------------------------------------------------------------------------- 1 | extracted objects 2 | selectedExtractedObject: anObject 3 | 4 | selectedExtractedObject := anObject -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/selectedExtractedObject.st: -------------------------------------------------------------------------------- 1 | extracted objects 2 | selectedExtractedObject 3 | 4 | ^ selectedExtractedObject -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/showBookmarks.st: -------------------------------------------------------------------------------- 1 | bookmarks 2 | showBookmarks 3 | 4 | | url | 5 | self bookmarks ifEmpty: [^ UIManager default inform: 'You have not created any bookmarks yet.']. 6 | url := UIManager default chooseFrom: self bookmarks values: self bookmarks. 7 | url ifNil: [^ self]. 8 | self changeLocation: url -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/instance/structuredDataChanged..st: -------------------------------------------------------------------------------- 1 | extracted objects 2 | structuredDataChanged: data 3 | 4 | | objects domainObject | 5 | domainObject := Smalltalk classNamed: #DomainObject. 6 | domainObject ifNil: [^ self]. 7 | 8 | objects := data collect: [:each | | object | 9 | object := domainObject new delete. 10 | each associationsDo: [:assoc | | field | 11 | field := assoc key asLegalSelector asSymbol. 12 | field = #name ifTrue: [field := #title]. 13 | object perform: field asMutator with: assoc value]. 14 | object]. 15 | objects ifEmpty: [ 16 | objects := { 17 | domainObject new delete 18 | title: self locationText asString; 19 | url: self locationText asString; 20 | yourself}]. 21 | self extractedObjects: objects -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | "open" : "cmfcmf 8/3/2019 19:12", 4 | "openOn:" : "cmfcmf 8/3/2019 19:12", 5 | "rickRoll" : "cmfcmf 6/30/2019 17:41", 6 | "searchFor:" : "cmfcmf 8/3/2019 19:12" }, 7 | "instance" : { 8 | "addOrRemoveBookmark" : "cmfcmf 7/7/2019 11:40", 9 | "bookmarkButtonState" : "cmfcmf 7/7/2019 11:38", 10 | "bookmarks" : "cmfcmf 7/7/2019 11:35", 11 | "buildButtonBarWith:" : "MB 1/30/2022 10:33", 12 | "buildTopBarWith:" : "MB 1/30/2022 10:33", 13 | "buildWith:" : "MB 1/30/2022 10:33", 14 | "changeLocation:" : "cf 5/17/2019 09:19", 15 | "dragExtractedObject:" : "MB 1/30/2022 10:33", 16 | "exploreExtractedObject" : "cmfcmf 6/11/2019 09:24", 17 | "extractedObjects" : "cmfcmf 6/11/2019 08:54", 18 | "extractedObjects:" : "cmfcmf 6/11/2019 08:55", 19 | "fullscreen:" : "cmfcmf 6/30/2019 17:35", 20 | "getBrowserMorph" : "MB 1/30/2022 10:33", 21 | "initialize" : "MB 1/30/2022 10:33", 22 | "locationText" : "cf 5/1/2019 15:47", 23 | "locationText:" : "MB 1/30/2022 10:33", 24 | "searchFor:" : "cf 5/27/2019 21:35", 25 | "searchText" : "cf 5/1/2019 15:47", 26 | "searchText:" : "cf 5/1/2019 15:58", 27 | "selectedExtractedObject" : "cmfcmf 6/11/2019 09:06", 28 | "selectedExtractedObject:" : "cmfcmf 6/11/2019 09:06", 29 | "showBookmarks" : "cmfcmf 7/7/2019 11:37", 30 | "structuredDataChanged:" : "MB 1/30/2022 10:33" } } 31 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowser.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | "Bookmarks" ], 7 | "commentStamp" : "cmfcmf 8/3/2019 19:12", 8 | "instvars" : [ 9 | "locationText", 10 | "searchText", 11 | "browser", 12 | "extractedObjects", 13 | "selectedExtractedObject" ], 14 | "name" : "MMBrowser", 15 | "pools" : [ 16 | ], 17 | "super" : "Model", 18 | "type" : "normal" } 19 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/README.md: -------------------------------------------------------------------------------- 1 | I am an ImageMorph that connects to Chrome/Chromium to display a website. 2 | Open myself by doit'ing one of 3 | 4 | MMBrowser open 5 | MMBrowser openOn: 'https://squeak.org' -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/chromeProfilePath..st: -------------------------------------------------------------------------------- 1 | preferences 2 | chromeProfilePath: aString 3 | 4 | ChromeProfilePath := aString -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/chromeProfilePath.st: -------------------------------------------------------------------------------- 1 | preferences 2 | chromeProfilePath 3 | 4 | 5 | ^ ChromeProfilePath ifNil: [(FileDirectory default / 'chromeUserDir') name] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/debugEnabled..st: -------------------------------------------------------------------------------- 1 | preferences 2 | debugEnabled: aBoolean 3 | 4 | DebugEnabled := aBoolean -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/debugEnabled.st: -------------------------------------------------------------------------------- 1 | preferences 2 | debugEnabled 3 | 4 | 5 | ^ DebugEnabled ifNil: [false] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/debugNodejs..st: -------------------------------------------------------------------------------- 1 | preferences 2 | debugNodejs: aBoolean 3 | 4 | DebugNodejs := aBoolean -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/debugNodejs.st: -------------------------------------------------------------------------------- 1 | preferences 2 | debugNodejs 3 | 4 | 5 | ^ DebugNodejs ifNil: [false] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/doNotUsePrebuiltBinary..st: -------------------------------------------------------------------------------- 1 | preferences 2 | doNotUsePrebuiltBinary: aBoolean 3 | 4 | DoNotUsePrebuiltBinary := aBoolean -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/doNotUsePrebuiltBinary.st: -------------------------------------------------------------------------------- 1 | preferences 2 | doNotUsePrebuiltBinary 3 | 4 | 5 | ^ DoNotUsePrebuiltBinary ifNil: [false] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/gitRepositoryPath..st: -------------------------------------------------------------------------------- 1 | preferences 2 | gitRepositoryPath: aString 3 | 4 | GitRepositoryPath := aString -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/gitRepositoryPath.st: -------------------------------------------------------------------------------- 1 | preferences 2 | gitRepositoryPath 3 | 4 | 5 | ^ GitRepositoryPath ifNil: ['.'] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/initialize.st: -------------------------------------------------------------------------------- 1 | class initialization 2 | initialize 3 | "MMBrowserMorph initialize" 4 | 5 | Smalltalk addToStartUpList: self. 6 | Smalltalk addToShutDownList: self -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/open.st: -------------------------------------------------------------------------------- 1 | instance creation 2 | open 3 | 4 | ^ self openOn: 'https://google.com' -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/openOn..st: -------------------------------------------------------------------------------- 1 | instance creation 2 | openOn: aUrl 3 | 4 | ^ self new 5 | connect: aUrl; 6 | openInWindowLabeled: self title; 7 | yourself -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/portalsOnRightClick..st: -------------------------------------------------------------------------------- 1 | preferences 2 | portalsOnRightClick: aBoolean 3 | 4 | PortalsOnRightClick := aBoolean -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/portalsOnRightClick.st: -------------------------------------------------------------------------------- 1 | preferences 2 | portalsOnRightClick 3 | 4 | 5 | ^ PortalsOnRightClick ifNil: [true] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/runChromeHeadless..st: -------------------------------------------------------------------------------- 1 | preferences 2 | runChromeHeadless: aBoolean 3 | 4 | RunChromeHeadless := aBoolean -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/runChromeHeadless.st: -------------------------------------------------------------------------------- 1 | preferences 2 | runChromeHeadless 3 | 4 | 5 | ^ RunChromeHeadless ifNil: [true] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/shutDown..st: -------------------------------------------------------------------------------- 1 | system startup 2 | shutDown: quitting 3 | 4 | quitting ifTrue: [self allInstancesDo: #dispose] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/startUp..st: -------------------------------------------------------------------------------- 1 | system startup 2 | startUp: resuming 3 | 4 | resuming ifTrue: [ 5 | DockingBarMorph allInstancesDo: [:each | each searchBarMorph instVarNamed: 'setTextSelector' put: #magicSearch:in:]. 6 | self allInstancesDo: [:each | each connect: each location]] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/class/title.st: -------------------------------------------------------------------------------- 1 | accessing 2 | title 3 | 4 | ^ 'MagicMouse Webbrowser' -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/acceptDroppingMorph.event..st: -------------------------------------------------------------------------------- 1 | dropping/grabbing 2 | acceptDroppingMorph: aMorph event: evt 3 | 4 | | position target | 5 | position := evt position - self position. 6 | target := (aMorph isKindOf: TransferMorph) 7 | ifTrue: [ 8 | aMorph passenger isText 9 | ifTrue: [aMorph passenger asString] 10 | ifFalse: [aMorph passenger]] 11 | ifFalse: [ 12 | (aMorph isSystemWindow and: [aMorph model isKindOf: StringHolder]) 13 | ifTrue: [aMorph model contents asString] 14 | ifFalse: [aMorph]]. 15 | 16 | (Smalltalk classNamed: #DomainObject) ifNotNil: [:domainObject | (target isKindOf: domainObject) ifTrue: [self droppedDomainObject: target at: position]]. 17 | 18 | target isString ifTrue: [self droppedString: target at: position] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/assertNodeScriptExists.st: -------------------------------------------------------------------------------- 1 | connecting 2 | assertNodeScriptExists 3 | 4 | | path directory | 5 | path := self class gitRepositoryPath. 6 | directory := FileDirectory on: path. 7 | self 8 | assert: [directory exists] 9 | description: 'You need to specify a valid directory in the Preference Browser'. 10 | self 11 | assert: [directory fileExists: 'run.js'] 12 | description: 'The specified directory exists but is missing the run.js file. Make sure to NOT specify the path to the directory cloned via the Git Browser, but rather a manually cloned directory!'. 13 | self 14 | assert: [directory directoryExists: 'node_modules'] 15 | description: 'You need to run yarn install inside the MagicMouse git directory.' -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/changeLocation..st: -------------------------------------------------------------------------------- 1 | public 2 | changeLocation: aUrl 3 | 4 | location := aUrl. 5 | self sendCommand: $l withBuffer: aUrl asByteArray -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/connect..st: -------------------------------------------------------------------------------- 1 | connecting 2 | connect: aUrl 3 | 4 | | path arguments | 5 | process := MMProcessWrapper newForOS. 6 | self class debugEnabled ifTrue: [process enableDebug]. 7 | arguments := { 8 | aUrl. 9 | viewportSize x. 10 | viewportSize y. 11 | self class runChromeHeadless ifTrue: ['headless'] ifFalse: ['windowed']. 12 | self class chromeProfilePath}. 13 | self class doNotUsePrebuiltBinary 14 | ifTrue: [ 15 | self assertNodeScriptExists. 16 | path := self class gitRepositoryPath, FileDirectory slash, 'run.js'. 17 | process 18 | startNodeScript: path 19 | arguments: arguments 20 | nodeArguments: (self class debugNodejs ifTrue: [{'--inspect'}] ifFalse: [{}])] 21 | ifFalse: [ 22 | path := (FileDirectory default / MMPluginDownloader filenameForCurrentPlatform) fullName. 23 | process startCommand: path withArguments: arguments]. 24 | 25 | location := aUrl. 26 | 27 | imageUpdaterProcess := [ 28 | [ | length command payload | 29 | length := process stdoutNextUint32. 30 | payload := process stdoutNextBuffer: length. 31 | command := payload unsignedCharAt: 1. 32 | command caseOf: { 33 | [$i] -> [self updateImage: payload]. 34 | [$h] -> [self handlePortal: payload]. 35 | [$l] -> [self locationUpdated: payload]. 36 | [$s] -> [self handleStructuredData: payload]. 37 | [$g] -> [self handleGitClone: payload]. 38 | [$f] -> [self handleFormFields: payload]}] repeat] fork -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/contentOfField.changedTo..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | contentOfField: anId changedTo: text 3 | 4 | | bytes content id | 5 | id := anId squeakToUtf8. 6 | content := text asString squeakToUtf8. 7 | bytes := ByteArray new: 8 + id byteSize + content byteSize. 8 | bytes signedLongAt: 1 put: id byteSize. 9 | bytes replaceFrom: 5 to: 5 + id byteSize - 1 with: id asByteArray. 10 | 11 | bytes signedLongAt: 5 + id byteSize put: content byteSize. 12 | bytes 13 | replaceFrom: 5 + id byteSize + 4 14 | to: 5 + id byteSize + 4 + content byteSize - 1 15 | with: content asByteArray. 16 | self sendCommand: $t withBuffer: bytes -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/delete.st: -------------------------------------------------------------------------------- 1 | disconnecting 2 | delete 3 | 4 | super delete. 5 | self dispose -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/dispose.st: -------------------------------------------------------------------------------- 1 | disconnecting 2 | dispose 3 | 4 | process ifNotNil: [:p | 5 | p terminate. 6 | process := nil]. 7 | imageUpdaterProcess ifNotNil: [:p | 8 | p terminate. 9 | imageUpdaterProcess := nil] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/doRefresh.st: -------------------------------------------------------------------------------- 1 | public 2 | doRefresh 3 | 4 | self changeLocation: location -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/domainObjectFieldFor.object..st: -------------------------------------------------------------------------------- 1 | domain objects 2 | domainObjectFieldFor: aString object: aDomainObject 3 | 4 | | fields | 5 | fields := self possibleDomainObjectFieldsFor: aString. 6 | fields do: [:each | (aDomainObject respondsTo: each) ifTrue: [^ each]]. 7 | ^ #unknown -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/drawOn..st: -------------------------------------------------------------------------------- 1 | drawing 2 | drawOn: aCanvas 3 | "Draw my background color to cover the areas when my viewport doesn't match my extent." 4 | 5 | aCanvas fillRectangle: self bounds color: self color. 6 | super drawOn: aCanvas -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/droppedDomainObject.at..st: -------------------------------------------------------------------------------- 1 | dropping/grabbing 2 | droppedDomainObject: aDomainObject at: aPoint 3 | 4 | | bytes | 5 | tmpDomainObject := aDomainObject. 6 | bytes := ByteArray new: 8. 7 | bytes 8 | signedLongAt: 1 put: aPoint x; 9 | signedLongAt: 5 put: aPoint y. 10 | self sendCommand: $f withBuffer: bytes -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/droppedString.at..st: -------------------------------------------------------------------------------- 1 | dropping/grabbing 2 | droppedString: aString at: aPoint 3 | 4 | | bytes strBuffer | 5 | strBuffer := aString withUnixLineEndings squeakToUtf8 asByteArray. 6 | bytes := ByteArray new: 8. 7 | bytes 8 | signedLongAt: 1 put: aPoint x; 9 | signedLongAt: 5 put: aPoint y. 10 | self sendCommand: $s withBuffer: bytes, strBuffer -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/eventTypeCode..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | eventTypeCode: aType 3 | 4 | ^ (Dictionary newFrom: { 5 | #mouseLeftUp -> 0. 6 | #mouseLeftDown -> 1. 7 | #mouseRightUp -> 2. 8 | #mouseRightDown -> 3. 9 | #mouseMove -> 4. 10 | #keyPress -> 5. 11 | #resize -> 6. 12 | #mouseWheel -> 7. 13 | #portal -> 8. 14 | #back -> 9. 15 | #forward -> 10}) 16 | at: aType 17 | ifAbsent: [self error: 'unknown event type'] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/extent..st: -------------------------------------------------------------------------------- 1 | geometry 2 | extent: aPoint 3 | 4 | (viewportSize ~= aPoint and: [self isConnected]) ifTrue: [self sendEvent: #resize withPoint: aPoint]. 5 | 6 | viewportSize := aPoint. 7 | super extent: aPoint -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/fullscreen..st: -------------------------------------------------------------------------------- 1 | public 2 | fullscreen: aBoolean 3 | 4 | aBoolean 5 | ifTrue: [ 6 | previousOwner := self owner. 7 | self 8 | beSticky; 9 | openInWorld: self world; 10 | extent: self world extent; 11 | position: 0 @ 0] 12 | ifFalse: [ 13 | self beUnsticky. 14 | previousOwner addMorph: self. 15 | previousOwner := nil] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/getIdAndRectFrom.startAt..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | getIdAndRectFrom: payload startAt: start 3 | 4 | | id x y w h idEnd | 5 | idEnd := payload findFirst: [:each | each = 0] startingAt: start. 6 | id := (payload copyFrom: start to: idEnd - 1) asString utf8ToSqueak. 7 | x := payload signedLongAt: idEnd + 1. 8 | y := payload signedLongAt: idEnd + 5. 9 | w := payload signedLongAt: idEnd + 9. 10 | h := payload signedLongAt: idEnd + 13. 11 | 12 | ^ Array with: id with: (x @ y extent: w @ h) with: idEnd + 17 -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/gitClone.named..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | gitClone: url named: name 3 | "This code is based on SquitBrowser>>actionProjectClone" 4 | 5 | | browser workingCopy directory repositoryPath | 6 | browser := SquitBrowser new. 7 | self assert: (browser isRemoteSupported: url). 8 | 9 | workingCopy := SquotWorkingCopy newOnObjectMemory. 10 | workingCopy name: name. 11 | directory := UIManager default chooseDirectory: 'Repository directory'. 12 | directory ifNil: [^ self]. 13 | repositoryPath := directory pathName. 14 | workingCopy repository: (SquitRepository new initializeInDirectory: (FileSystem disk root resolve: repositoryPath)). 15 | workingCopy loadedHistorian: (workingCopy repository historianNamed: 'master'). 16 | workingCopy register. 17 | 18 | browser withRemoteErrorHandlingDo: [ 19 | browser 20 | handlingCredentialsOf: workingCopy loadedHistorian 21 | do: [workingCopy repository cloneFrom: url] 22 | ifCanceled: []]. 23 | browser refresh. 24 | ToolBuilder open: browser -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/goBack.st: -------------------------------------------------------------------------------- 1 | public 2 | goBack 3 | 4 | self sendEvent: #back -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/goForward.st: -------------------------------------------------------------------------------- 1 | public 2 | goForward 3 | 4 | self sendEvent: #forward -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handleCodePortal..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | handleCodePortal: payload 3 | 4 | | info data morph id next rect | 5 | self assert: [$c = (payload unsignedCharAt: 2)]. 6 | info := self getIdAndRectFrom: payload startAt: 3. 7 | id := info first. 8 | rect := info second. 9 | next := info third. 10 | 11 | data := (payload copyFrom: next to: payload size) asString utf8ToSqueak. 12 | 13 | morph := MMTextMorphWithModel new. 14 | morph 15 | position: self position + rect origin; 16 | extent: rect extent; 17 | useDefaultStyler; 18 | setText: data; 19 | yourself. 20 | self addMorph: morph. 21 | self portalMorphs at: id put: morph. 22 | World activeHand newKeyboardFocus: morph -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handleFormFields..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | handleFormFields: payload 3 | 4 | | start fields descriptions ids | 5 | start := 6. 6 | fields := OrderedCollection new. 7 | descriptions := OrderedCollection new. 8 | ids := OrderedCollection new. 9 | (payload signedLongAt: 2) timesRepeat: [ | h w x y descriptionSize description id idSize | 10 | x := payload signedLongAt: start. 11 | y := payload signedLongAt: start + 4. 12 | w := payload signedLongAt: start + 8. 13 | h := payload signedLongAt: start + 12. 14 | descriptionSize := payload unsignedLongAt: start + 16. 15 | description := payload copyFrom: start + 20 to: start + 20 + descriptionSize - 1. 16 | start := start + 20 + descriptionSize. 17 | idSize := payload unsignedLongAt: start. 18 | id := payload copyFrom: start + 4 to: start + 4 + idSize - 1. 19 | 20 | fields add: (x @ y extent: w @ h). 21 | descriptions add: description asString utf8ToSqueak. 22 | ids add: id asString utf8ToSqueak. 23 | start := start + 4 + idSize]. 24 | 25 | "Looks like there is no form where the DomainObject was dropped." 26 | fields ifEmpty: [ 27 | (self urlFromDomainObject: tmpDomainObject) ifNotNil: [:url | 28 | self changeLocation: url. 29 | ^ tmpDomainObject := nil]. 30 | (self nameFromDomainObject: tmpDomainObject) ifNotNil: [:name | 31 | self searchFor: name. 32 | ^ tmpDomainObject := nil]]. 33 | 34 | self submorphs 35 | select: [:each | each isKindOf: HObjectFieldMorph] 36 | thenDo: [:each | self removeMorph: each]. 37 | fields withIndexDo: [:boundingBox :index | | description id fieldMorph | 38 | description := descriptions at: index. 39 | id := ids at: index. 40 | fieldMorph := (HObjectFieldMorph 41 | for: tmpDomainObject 42 | wanting: (self domainObjectFieldFor: description object: tmpDomainObject)) 43 | position: self position + boundingBox topLeft; 44 | extent: boundingBox extent; 45 | when: #textChanged 46 | evaluate: [:text | self contentOfField: id changedTo: text]; 47 | yourself. 48 | self addMorph: fieldMorph. 49 | self portalMorphs at: id put: fieldMorph. 50 | self contentOfField: id changedTo: fieldMorph getTextFromInnerMorphs]. 51 | 52 | tmpDomainObject := nil -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handleGitClone..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | handleGitClone: payload 3 | 4 | | url name nameLength | 5 | nameLength := payload unsignedLongAt: 2. 6 | name := (payload copyFrom: 6 to: 6 + nameLength - 1) asString utf8ToSqueak. 7 | url := (payload copyFrom: 6 + nameLength to: payload size) asString utf8ToSqueak. 8 | 9 | self gitClone: url named: name -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handlePortal..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | handlePortal: payload 3 | 4 | (payload unsignedCharAt: 2) caseOf: { 5 | [$i] -> [self handlePortalImage: payload]. 6 | [$m] -> [self handlePortalMorph: payload]. 7 | [$c] -> [self handleCodePortal: payload]. 8 | [$r] -> [self handlePortalRefreshData: payload]} -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handlePortalImage..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | handlePortalImage: payload 3 | 4 | | data form morph id info next rect | 5 | self assert: [$i = (payload unsignedCharAt: 2)]. 6 | info := self getIdAndRectFrom: payload startAt: 3. 7 | id := info first. 8 | rect := info second. 9 | next := info third. 10 | 11 | data := payload copyFrom: next to: payload size. 12 | 13 | form := Form fromBinaryStream: data readStream. 14 | form := form scaledToSize: rect extent. 15 | 16 | morph := ImageMorph new. 17 | morph 18 | position: self position + rect origin; 19 | image: form. 20 | self portalMorphs at: id put: morph. 21 | self addMorph: morph. 22 | morph addHalo -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handlePortalMorph..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | handlePortalMorph: payload 3 | 4 | | info data id next rect morph | 5 | self assert: [$m = (payload unsignedCharAt: 2)]. 6 | info := self getIdAndRectFrom: payload startAt: 3. 7 | id := info first. 8 | rect := info second. 9 | next := info third. 10 | 11 | data := (payload copyFrom: next to: payload size) asString utf8ToSqueak. 12 | morph := Compiler evaluate: data allButFirst. 13 | morph 14 | position: self position + rect origin; 15 | extent: rect extent; 16 | yourself. 17 | self addMorph: morph. 18 | self portalMorphs at: id put: morph -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handlePortalRefreshData..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | handlePortalRefreshData: payload 3 | 4 | | start portalInfos | 5 | self assert: [$r = (payload unsignedCharAt: 2)]. 6 | start := 7. 7 | portalInfos := OrderedCollection new. 8 | (payload signedLongAt: 3) timesRepeat: [ | portalInfo rect | 9 | portalInfo := self getIdAndRectFrom: payload startAt: start. 10 | portalInfos add: portalInfo. 11 | start := portalInfo third]. 12 | 13 | self refreshPortals: portalInfos. 14 | 15 | portalInfos do: [:each | | id rect | 16 | id := each first. 17 | rect := each second. 18 | self movePortal: id rect: rect] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handleStructuredData..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | handleStructuredData: payload 3 | 4 | | start structuredData | 5 | start := 6. 6 | structuredData := OrderedCollection new. 7 | (payload unsignedLongAt: 2) timesRepeat: [ | json length | 8 | length := payload unsignedLongAt: start. 9 | json := payload copyFrom: start + 4 to: start + 4 + length - 1. 10 | structuredData add: (Json readFrom: json asString utf8ToSqueak readStream). 11 | start := start + 4 + length]. 12 | self triggerEvent: #structuredDataChanged with: structuredData. 13 | ^ structuredData -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handlesKeyboard..st: -------------------------------------------------------------------------------- 1 | events 2 | handlesKeyboard: evt 3 | 4 | ^ true -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handlesMouseDown..st: -------------------------------------------------------------------------------- 1 | events 2 | handlesMouseDown: anEvent 3 | 4 | ^ true -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handlesMouseMove..st: -------------------------------------------------------------------------------- 1 | events 2 | handlesMouseMove: anEvent 3 | 4 | ^ true -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/handlesMouseWheel..st: -------------------------------------------------------------------------------- 1 | events 2 | handlesMouseWheel: anEvent 3 | 4 | ^ true -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/imageUpdaterProcess..st: -------------------------------------------------------------------------------- 1 | accessing 2 | imageUpdaterProcess: anObject 3 | 4 | imageUpdaterProcess := anObject -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/imageUpdaterProcess.st: -------------------------------------------------------------------------------- 1 | accessing 2 | imageUpdaterProcess 3 | 4 | ^ imageUpdaterProcess -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/initialize.st: -------------------------------------------------------------------------------- 1 | initialization 2 | initialize 3 | 4 | viewportSize := 300 @ 300. 5 | super initialize. 6 | self clipSubmorphs: true. 7 | self color: Color white. 8 | "Mark the image as opaque. Although we render PNGs, the browser should never send us an image of the webpage that includes transparency." 9 | self isOpaque: true. 10 | self image: self class logo. 11 | self portalMorphs: Dictionary new -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/intoWorld..st: -------------------------------------------------------------------------------- 1 | initialization 2 | intoWorld: aWorld 3 | 4 | super intoWorld: aWorld. 5 | self isConnected ifFalse: [self connect: self location] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/isConnected.st: -------------------------------------------------------------------------------- 1 | accessing 2 | isConnected 3 | 4 | ^ process notNil -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/isFullscreen.st: -------------------------------------------------------------------------------- 1 | accessing 2 | isFullscreen 3 | 4 | ^ previousOwner notNil -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/keyStroke..st: -------------------------------------------------------------------------------- 1 | events 2 | keyStroke: anEvent 3 | 4 | anEvent keyString = '' 5 | ifTrue: [self fullscreen: previousOwner isNil] 6 | ifFalse: [self sendEvent: #keyPress withString: anEvent keyString] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/keyboardFocusColor.st: -------------------------------------------------------------------------------- 1 | events 2 | keyboardFocusColor 3 | 4 | ^ nil -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/location.st: -------------------------------------------------------------------------------- 1 | accessing 2 | location 3 | 4 | ^ location -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/locationUpdated..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | locationUpdated: payload 3 | 4 | location := (payload copyFrom: 2 to: payload size) asString utf8ToSqueak. 5 | self refreshPortals: {}. 6 | self triggerEvent: #locationChanged with: location -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/mouseDown..st: -------------------------------------------------------------------------------- 1 | events 2 | mouseDown: anEvent 3 | 4 | | position | 5 | position := anEvent position - self position max: 0 @ 0. 6 | (self class portalsOnRightClick and: [anEvent yellowButtonChanged]) 7 | ifTrue: [self sendEvent: #portal withPoint: position] 8 | ifFalse: [ 9 | self 10 | sendEvent: #mouseMove withPoint: position; 11 | sendEvent: (anEvent yellowButtonChanged 12 | ifTrue: [#mouseRightDown] 13 | ifFalse: [#mouseLeftDown]). 14 | self activeHand keyboardFocus == self ifTrue: [^ self]. 15 | self activeHand newKeyboardFocus: self] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/mouseMove..st: -------------------------------------------------------------------------------- 1 | events 2 | mouseMove: anEvent 3 | 4 | | position | 5 | position := anEvent position - self position max: 0 @ 0. 6 | self sendEvent: #mouseMove withPoint: position -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/mouseUp..st: -------------------------------------------------------------------------------- 1 | events 2 | mouseUp: anEvent 3 | 4 | | position | 5 | position := anEvent position - self position max: 0 @ 0. 6 | self 7 | sendEvent: #mouseMove withPoint: position; 8 | sendEvent: (anEvent yellowButtonChanged ifTrue: [#mouseRightUp] ifFalse: [#mouseLeftUp]) -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/mouseWheel..st: -------------------------------------------------------------------------------- 1 | events 2 | mouseWheel: anEvent 3 | 4 | self sendEvent: #mouseWheel withNumber: (anEvent isWheelUp ifTrue: [0] ifFalse: [1]) -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/movePortal.rect..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | movePortal: id rect: rect 3 | 4 | self portalMorphs at: id ifPresent: [:morph | 5 | morph 6 | position: self position + rect origin; 7 | extent: rect extent] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/nameFromDomainObject..st: -------------------------------------------------------------------------------- 1 | domain objects 2 | nameFromDomainObject: aDomainObject 3 | 4 | | name | 5 | name := aDomainObject ? #fullName. 6 | name ifNil: [ | firstName lastName | 7 | firstName := aDomainObject ? #firstName. 8 | lastName := aDomainObject ? #lastName. 9 | (firstName notNil and: [lastName notNil]) ifTrue: [name := '{1} {2}' format: {firstName. lastName}]]. 10 | "name ifNil: [name := aDomainObject ? #name]." 11 | name ifNil: [name := aDomainObject ? #title]. 12 | name ifNil: [name := aDomainObject ? #headline]. 13 | name ifNil: [name := aDomainObject ? #content]. 14 | name ifNil: [name := aDomainObject ? #description]. 15 | 16 | ^ name -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/outOfWorld..st: -------------------------------------------------------------------------------- 1 | initialization 2 | outOfWorld: aWorld 3 | 4 | super outOfWorld: aWorld. 5 | self dispose -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/portalMorphs..st: -------------------------------------------------------------------------------- 1 | accessing 2 | portalMorphs: aDictionary 3 | 4 | portalMorphs := aDictionary -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/portalMorphs.st: -------------------------------------------------------------------------------- 1 | accessing 2 | portalMorphs 3 | 4 | ^ portalMorphs -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/possibleDomainObjectFieldsFor..st: -------------------------------------------------------------------------------- 1 | domain objects 2 | possibleDomainObjectFieldsFor: aString 3 | 4 | | names separators | 5 | names := OrderedCollection new. 6 | names add: aString asCamelCase withFirstCharacterDownshifted asSymbol. 7 | 8 | separators := #(#/ #or #, #';'). 9 | separators do: [:separator | 10 | aString 11 | splitBy: separator 12 | do: [:each | each ~= aString ifTrue: [names add: each asCamelCase withFirstCharacterDownshifted asSymbol]]]. 13 | 14 | names addAll: (aString 15 | caseOf: { 16 | ['address line 1'] -> [#(#street)]. 17 | ['address1'] -> [#(#street)]. 18 | ['postcode'] -> [#(#postalCode)]. 19 | ['firstname'] -> [#(#firstName)]. 20 | ['lastname'] -> [#(#lastName)]} 21 | otherwise: [#()]). 22 | 23 | ^ names -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/preferredExtent.st: -------------------------------------------------------------------------------- 1 | accessing 2 | preferredExtent 3 | 4 | ^ viewportSize -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/process..st: -------------------------------------------------------------------------------- 1 | accessing 2 | process: anObject 3 | 4 | process := anObject -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/process.st: -------------------------------------------------------------------------------- 1 | accessing 2 | process 3 | 4 | ^ process -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/refreshPortals..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | refreshPortals: portalInfos 3 | 4 | self portalMorphs keysAndValuesDo: [:id :morph | | isMyChild isOnPage | 5 | isMyChild := morph owner = self. 6 | isOnPage := portalInfos anySatisfy: [:each | id = each first]. 7 | (isMyChild and: [isOnPage not]) ifTrue: [morph delete]. 8 | (isMyChild not or: [isOnPage not]) ifTrue: [self portalMorphs removeKey: id]] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/searchFor..st: -------------------------------------------------------------------------------- 1 | public 2 | searchFor: aString 3 | 4 | self changeLocation: 'https://www.google.com/search?q=', aString encodeForHTTP -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/sendCommand.withBuffer..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | sendCommand: aCharacter withBuffer: aBuffer 3 | 4 | self process 5 | stdinPutUint32: aBuffer size + 1; 6 | stdinPutChar: aCharacter; 7 | stdinPutBuffer: aBuffer asByteArray -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/sendEvent..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | sendEvent: anEventType 3 | 4 | self sendEvent: anEventType withBuffer: #() -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/sendEvent.withBuffer..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | sendEvent: anEventType withBuffer: aBuffer 3 | 4 | self process 5 | stdinPutUint32: aBuffer size + 2; 6 | stdinPutChar: $e; 7 | stdinPutUint8: (self eventTypeCode: anEventType); 8 | stdinPutBuffer: aBuffer asByteArray -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/sendEvent.withNumber..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | sendEvent: anEventType withNumber: aNumber 3 | 4 | self sendEvent: anEventType withBuffer: ((ByteArray new: 4) 5 | unsignedLongAt: 1 put: aNumber asInteger; 6 | yourself) -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/sendEvent.withNumber.andNumber..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | sendEvent: anEventType withNumber: firstNumber andNumber: secondNumber 3 | 4 | self sendEvent: anEventType withBuffer: ((ByteArray new: 8) 5 | unsignedLongAt: 1 put: firstNumber asInteger; 6 | unsignedLongAt: 5 put: secondNumber asInteger; 7 | yourself) -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/sendEvent.withPoint..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | sendEvent: anEventType withPoint: aPoint 3 | 4 | self sendEvent: anEventType withNumber: aPoint x andNumber: aPoint y -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/sendEvent.withString..st: -------------------------------------------------------------------------------- 1 | -> to chrome 2 | sendEvent: anEventType withString: aString 3 | 4 | self sendEvent: anEventType withBuffer: aString squeakToUtf8 asByteArray -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/updateImage..st: -------------------------------------------------------------------------------- 1 | <- from chrome 2 | updateImage: payload 3 | 4 | | format png form | 5 | format := payload unsignedCharAt: 2. 6 | format = $r 7 | ifTrue: [ | width height | 8 | width := payload unsignedLongAt: 3. 9 | height := payload unsignedLongAt: 7. 10 | png := payload copyFrom: 11 to: payload size. 11 | form := Form extent: width @ height depth: 32. 12 | 1 to: width * height - 1 do: [:index | 13 | form bits 14 | at: index 15 | put: (png at: index * 4 + 3) asInteger << 0 + ((png at: index * 4 + 2) asInteger << 8) + ((png at: index * 4 + 1) asInteger << 16) + ((png at: index * 4 + 0) asInteger << 24)]] 16 | ifFalse: [ | reader | 17 | format caseOf: { 18 | [$p] -> [ 19 | reader := PNGReadWriter on: (payload copyFrom: 3 to: payload size) readStream. 20 | form := reader nextImage]. 21 | [$j] -> [ 22 | reader := MMFastJPEGReader on: (payload readStream 23 | skip: 2; 24 | yourself). 25 | form := reader nextImage: self image]}]. 26 | form extent > viewportSize ifTrue: ["We need to crop the incoming image if we don't run Chrome in headless mode 27 | or if we just resized the window and Chrome still sends us a bigger image than now requested." 28 | form := form contentsOfArea: (0 @ 0 extent: viewportSize)]. 29 | 30 | self image: form -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/urlFromDomainObject..st: -------------------------------------------------------------------------------- 1 | domain objects 2 | urlFromDomainObject: aDomainObject 3 | 4 | | url | 5 | ^ #(#url #website #homepage #uri #domain) 6 | detect: [:message | 7 | url := aDomainObject ? message. 8 | url notNil and: [url notEmpty]] 9 | ifFound: [:message | url] 10 | ifNone: [nil] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/viewportSize..st: -------------------------------------------------------------------------------- 1 | accessing 2 | viewportSize: anObject 3 | 4 | viewportSize := anObject -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/viewportSize.st: -------------------------------------------------------------------------------- 1 | accessing 2 | viewportSize 3 | 4 | ^ viewportSize -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/instance/wantsDroppedMorph.event..st: -------------------------------------------------------------------------------- 1 | dropping/grabbing 2 | wantsDroppedMorph: aMorph event: evt 3 | 4 | (aMorph isKindOf: TransferMorph) ifTrue: [ | passenger | 5 | passenger := aMorph passenger. 6 | (Smalltalk classNamed: #DomainObject) ifNotNil: [:domainObject | (passenger isKindOf: domainObject) ifTrue: [^ true]]. 7 | passenger isText ifTrue: [^ true]]. 8 | 9 | (aMorph isSystemWindow and: [aMorph model isKindOf: Workspace]) ifTrue: [^ true]. 10 | 11 | ^ false -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | "chromeProfilePath" : "MB 1/30/2022 10:33", 4 | "chromeProfilePath:" : "cmfcmf 11/11/2020 20:04", 5 | "debugEnabled" : "MB 1/30/2022 10:33", 6 | "debugEnabled:" : "cmfcmf 11/11/2020 19:53", 7 | "debugNodejs" : "MB 1/30/2022 10:33", 8 | "debugNodejs:" : "cmfcmf 11/11/2020 20:05", 9 | "doNotUsePrebuiltBinary" : "MB 1/30/2022 10:33", 10 | "doNotUsePrebuiltBinary:" : "cmfcmf 11/11/2020 20:05", 11 | "gitRepositoryPath" : "MB 1/30/2022 10:33", 12 | "gitRepositoryPath:" : "cmfcmf 11/11/2020 20:05", 13 | "initialize" : "MB 1/30/2022 10:33", 14 | "logo" : "MB 1/30/2022 10:33", 15 | "open" : "cmfcmf 6/30/2019 13:11", 16 | "openOn:" : "cmfcmf 6/30/2019 13:11", 17 | "portalsOnRightClick" : "MB 1/30/2022 10:33", 18 | "portalsOnRightClick:" : "cmfcmf 11/11/2020 20:05", 19 | "runChromeHeadless" : "MB 1/30/2022 10:33", 20 | "runChromeHeadless:" : "cmfcmf 11/11/2020 20:06", 21 | "shutDown:" : "cmfcmf 6/27/2019 10:20", 22 | "startUp:" : "MB 1/30/2022 10:33", 23 | "title" : "cmfcmf 6/30/2019 13:10" }, 24 | "instance" : { 25 | "acceptDroppingMorph:event:" : "MB 1/30/2022 10:33", 26 | "assertNodeScriptExists" : "MB 1/30/2022 10:33", 27 | "changeLocation:" : "cmfcmf 6/19/2019 20:17", 28 | "connect:" : "MB 1/30/2022 10:33", 29 | "contentOfField:changedTo:" : "MB 1/30/2022 10:33", 30 | "delete" : "cmfcmf 8/3/2019 19:16", 31 | "dispose" : "MB 1/30/2022 10:33", 32 | "doRefresh" : "cmfcmf 7/2/2019 08:20", 33 | "domainObjectFieldFor:object:" : "MB 1/30/2022 10:33", 34 | "drawOn:" : "MB 1/30/2022 10:33", 35 | "droppedDomainObject:at:" : "MB 1/30/2022 10:33", 36 | "droppedString:at:" : "MB 1/30/2022 10:33", 37 | "eventTypeCode:" : "MB 1/30/2022 10:33", 38 | "extent:" : "MB 1/30/2022 10:33", 39 | "fullscreen:" : "MB 1/30/2022 10:33", 40 | "getIdAndRectFrom:startAt:" : "MB 1/30/2022 10:33", 41 | "gitClone:named:" : "MB 1/30/2022 10:33", 42 | "goBack" : "cmfcmf 7/2/2019 08:21", 43 | "goForward" : "cmfcmf 7/2/2019 08:21", 44 | "handleCodePortal:" : "MB 1/30/2022 10:33", 45 | "handleFormFields:" : "MB 1/30/2022 10:33", 46 | "handleGitClone:" : "MB 1/30/2022 10:33", 47 | "handlePortal:" : "MB 1/30/2022 10:33", 48 | "handlePortalImage:" : "MB 1/30/2022 10:33", 49 | "handlePortalMorph:" : "MB 1/30/2022 10:33", 50 | "handlePortalRefreshData:" : "MB 1/30/2022 10:33", 51 | "handleStructuredData:" : "cmfcmf 8/3/2019 19:35", 52 | "handlesKeyboard:" : "cmfcmf 7/3/2019 09:15", 53 | "handlesMouseDown:" : "T 4/18/2019 20:02", 54 | "handlesMouseMove:" : "cf 4/22/2019 14:02", 55 | "handlesMouseWheel:" : "TB 4/19/2019 09:54", 56 | "imageUpdaterProcess" : "cf 5/13/2019 15:43", 57 | "imageUpdaterProcess:" : "MB 1/30/2022 10:33", 58 | "initialize" : "MB 1/30/2022 10:33", 59 | "intoWorld:" : "cmfcmf 7/7/2019 11:03", 60 | "isConnected" : "cmfcmf 7/3/2019 09:15", 61 | "isFullscreen" : "cmfcmf 7/7/2019 19:03", 62 | "keyStroke:" : "cmfcmf 6/30/2019 14:24", 63 | "keyboardFocusColor" : "TB 4/19/2019 10:02", 64 | "location" : "cmfcmf 6/19/2019 20:18", 65 | "locationUpdated:" : "cmfcmf 7/8/2019 17:41", 66 | "mouseDown:" : "MB 1/30/2022 10:33", 67 | "mouseMove:" : "MB 1/30/2022 10:33", 68 | "mouseUp:" : "MB 1/30/2022 10:33", 69 | "mouseWheel:" : "cmfcmf 7/3/2019 09:16", 70 | "movePortal:rect:" : "cf 5/13/2019 16:36", 71 | "nameFromDomainObject:" : "MB 1/30/2022 10:33", 72 | "outOfWorld:" : "cmfcmf 6/30/2019 11:37", 73 | "portalMorphs" : "cf 5/13/2019 15:43", 74 | "portalMorphs:" : "cf 5/13/2019 15:43", 75 | "possibleDomainObjectFieldsFor:" : "MB 1/30/2022 10:33", 76 | "preferredExtent" : "T 4/18/2019 21:14", 77 | "process" : "cf 5/13/2019 15:43", 78 | "process:" : "MB 1/30/2022 10:33", 79 | "refreshPortals:" : "MB 1/30/2022 10:33", 80 | "searchFor:" : "cmfcmf 8/3/2019 19:29", 81 | "sendCommand:withBuffer:" : "MB 1/30/2022 10:33", 82 | "sendEvent:" : "cf 4/22/2019 22:32", 83 | "sendEvent:withBuffer:" : "MB 1/30/2022 10:33", 84 | "sendEvent:withNumber:" : "cf 4/22/2019 22:34", 85 | "sendEvent:withNumber:andNumber:" : "cf 4/22/2019 22:35", 86 | "sendEvent:withPoint:" : "cf 4/22/2019 22:35", 87 | "sendEvent:withString:" : "cmfcmf 7/3/2019 09:17", 88 | "updateImage:" : "MB 1/30/2022 10:33", 89 | "urlFromDomainObject:" : "MB 1/30/2022 10:33", 90 | "viewportSize" : "cf 5/13/2019 15:43", 91 | "viewportSize:" : "MB 1/30/2022 10:33", 92 | "wantsDroppedMorph:event:" : "MB 1/30/2022 10:33" } } 93 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBrowserMorph.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | "ChromeProfilePath", 7 | "DebugEnabled", 8 | "DebugNodejs", 9 | "DoNotUsePrebuiltBinary", 10 | "GitRepositoryPath", 11 | "Logo", 12 | "PortalsOnRightClick", 13 | "RunChromeHeadless" ], 14 | "commentStamp" : "cmfcmf 8/3/2019 19:11", 15 | "instvars" : [ 16 | "process", 17 | "viewportSize", 18 | "imageUpdaterProcess", 19 | "portalMorphs", 20 | "location", 21 | "tmpDomainObject", 22 | "previousOwner" ], 23 | "name" : "MMBrowserMorph", 24 | "pools" : [ 25 | ], 26 | "super" : "ImageMorph", 27 | "type" : "normal" } 28 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBufferedStream.class/README.md: -------------------------------------------------------------------------------- 1 | I am a custom stream that is used by MMUnixProcessWrapper to communicate with the process. -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBufferedStream.class/instance/binary.st: -------------------------------------------------------------------------------- 1 | properties-setting 2 | binary 3 | "We need to overwrite this method, because it doesn't work when using BufferedAsyncFileReadStreams. If we don't change the readBuffer, the call to 'self uint32' fails, because the underlying collection returns characters." 4 | 5 | super binary. 6 | self readBuffer: (ReadWriteStream with: ByteArray new) -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBufferedStream.class/instance/maxReadBufferSize.st: -------------------------------------------------------------------------------- 1 | defaults 2 | maxReadBufferSize 3 | 4 | self flag: #todo. 5 | "This buffer will still overflow." 6 | ^ 400000000 -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBufferedStream.class/instance/waitForDataReady.st: -------------------------------------------------------------------------------- 1 | read ahead buffer 2 | waitForDataReady 3 | "Block until at least one character is available in the readBuffer. This is not 4 | thread safe, and only one Process is permitted to send this message." 5 | 6 | self flag: #todo. 7 | " for some reason the browser stops working after some time when simply using 'self dataAvailableSemaphore wait'" 8 | (self dataAvailableSemaphore waitTimeoutMSecs: 30000) ifTrue: [self error: 'Did not receive a frame in 30 seconds'] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBufferedStream.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "binary" : "MB 1/30/2022 10:33", 6 | "maxReadBufferSize" : "MB 1/30/2022 10:33", 7 | "waitForDataReady" : "MB 1/30/2022 10:33" } } 8 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMBufferedStream.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "cmfcmf 8/3/2019 19:05", 8 | "instvars" : [ 9 | ], 10 | "name" : "MMBufferedStream", 11 | "pools" : [ 12 | ], 13 | "super" : "BufferedAsyncFileReadStream", 14 | "type" : "normal" } 15 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMFastJPEGReader.class/README.md: -------------------------------------------------------------------------------- 1 | I am a faster alternative to the JPEGReadWriter2 by allowing my users to provide me with a form to reuse for the next image. -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMFastJPEGReader.class/instance/nextImage..st: -------------------------------------------------------------------------------- 1 | public access 2 | nextImage: aForm 3 | "Process the next image, but try to reuse aForm. Reuse is not guranteed." 4 | 5 | | bytes width height components jpegDecompressStruct jpegErrorMgr2Struct form | 6 | bytes := stream upToEnd. 7 | stream close. 8 | jpegDecompressStruct := ByteArray new: self primJPEGDecompressStructSize. 9 | jpegErrorMgr2Struct := ByteArray new: self primJPEGErrorMgr2StructSize. 10 | self primJPEGReadHeader: jpegDecompressStruct fromByteArray: bytes errorMgr: jpegErrorMgr2Struct. 11 | width := self primImageWidth: jpegDecompressStruct. 12 | height := self primImageHeight: jpegDecompressStruct. 13 | components := self primImageNumComponents: jpegDecompressStruct. 14 | 15 | "If the provided form has the same size and depth, reuse it instead of creating a new form." 16 | (aForm extent = (width @ height) and: [aForm depth = 32]) 17 | ifTrue: [form := aForm] 18 | ifFalse: [form := Form extent: width @ height depth: 32]. 19 | 20 | self 21 | primJPEGReadImage: jpegDecompressStruct 22 | fromByteArray: bytes 23 | onForm: form 24 | doDithering: true 25 | errorMgr: jpegErrorMgr2Struct. 26 | ^ form -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMFastJPEGReader.class/instance/nextImage.st: -------------------------------------------------------------------------------- 1 | public access 2 | nextImage 3 | "Create a dummy form the nextImage: call can reuse." 4 | 5 | ^ self nextImage: (Form extent: 1 @ 1) -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMFastJPEGReader.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "nextImage" : "MB 1/30/2022 10:33", 6 | "nextImage:" : "MB 1/30/2022 10:33" } } 7 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMFastJPEGReader.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "cmfcmf 8/3/2019 19:03", 8 | "instvars" : [ 9 | ], 10 | "name" : "MMFastJPEGReader", 11 | "pools" : [ 12 | ], 13 | "super" : "JPEGReadWriter2", 14 | "type" : "normal" } 15 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/README.md: -------------------------------------------------------------------------------- 1 | I am a helper class to download prebuilt binaries. I am called postLoad of the MagicMouse-Core package. -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/class/download..st: -------------------------------------------------------------------------------- 1 | downloading 2 | download: aString 3 | 4 | UIManager default informUserDuring: [:bar | | document fileStream | 5 | bar value: 'Downloading plugin'. 6 | document := ('https://github.com/cmfcmf/MagicMouse/releases/download/latest/', aString) asUrl retrieveContents. 7 | bar value: 'Saving plugin'. 8 | fileStream := FileDirectory default fileNamed: aString. 9 | fileStream 10 | binary; 11 | nextPutAll: document content; 12 | close. 13 | OSProcess isUnix ifTrue: [OSProcess command: 'chmod +x ', fileStream fullName]] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/class/download.st: -------------------------------------------------------------------------------- 1 | downloading 2 | download 3 | 4 | self download: self filenameForCurrentPlatform -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/class/downloadForAllPlatforms.st: -------------------------------------------------------------------------------- 1 | downloading 2 | downloadForAllPlatforms 3 | 4 | self filenameDict valuesDo: [:each | self download: each] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/class/downloadIfConfirmed.st: -------------------------------------------------------------------------------- 1 | downloading 2 | downloadIfConfirmed 3 | 4 | | confirmed | 5 | confirmed := true. 6 | Smalltalk at: #SmalltalkCI ifAbsent: ["Don't ask for confirmation when running SmalltalkCI" 7 | confirmed := self confirm: 'To use MagicMouse, you need to download/update an additional binary to interface with Chrome/Chromium. Do you want to continue and download now?']. 8 | confirmed ifTrue: [self download] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/class/downloadIfNeededAndConfirmed.st: -------------------------------------------------------------------------------- 1 | downloading 2 | downloadIfNeededAndConfirmed 3 | 4 | (FileDirectory default fileExists: self filenameForCurrentPlatform) ifFalse: [self downloadIfConfirmed] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/class/filenameDict.st: -------------------------------------------------------------------------------- 1 | filenames 2 | filenameDict 3 | 4 | ^ Dictionary newFrom: {'Win32' -> 'magicmouse-win.exe'. 'Mac OS' -> 'magicmouse-macos'. 'unix' -> 'magicmouse-linux'} -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/class/filenameForCurrentPlatform.st: -------------------------------------------------------------------------------- 1 | filenames 2 | filenameForCurrentPlatform 3 | 4 | ^ self filenameDict at: Smalltalk platformName -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/class/removeDownloads.st: -------------------------------------------------------------------------------- 1 | deleting 2 | removeDownloads 3 | 4 | self filenameDict valuesDo: [:each | FileDirectory default deleteFileNamed: each ifAbsent: []] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | "download" : "cmfcmf 8/3/2019 16:21", 4 | "download:" : "MB 1/30/2022 10:33", 5 | "downloadForAllPlatforms" : "MB 1/30/2022 10:33", 6 | "downloadIfConfirmed" : "MB 1/30/2022 10:33", 7 | "downloadIfNeededAndConfirmed" : "MB 1/30/2022 10:33", 8 | "filenameDict" : "MB 1/30/2022 10:33", 9 | "filenameForCurrentPlatform" : "cmfcmf 8/3/2019 16:23", 10 | "removeDownloads" : "MB 1/30/2022 10:33" }, 11 | "instance" : { 12 | } } 13 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMPluginDownloader.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "cmfcmf 8/3/2019 19:01", 8 | "instvars" : [ 9 | ], 10 | "name" : "MMPluginDownloader", 11 | "pools" : [ 12 | ], 13 | "super" : "Object", 14 | "type" : "normal" } 15 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/README.md: -------------------------------------------------------------------------------- 1 | I am an abstract superclass for operating-specific implementations of spawning, wrapping and terminating a process. -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/class/newForOS.st: -------------------------------------------------------------------------------- 1 | instance creation 2 | newForOS 3 | 4 | ^ OSProcess isWindows 5 | ifTrue: [ 6 | "Smalltalk platformSubtype = 'X64' 7 | ifTrue: [" 8 | MMWindowsProcessWrapperFFI new 9 | "] 10 | ifFalse: [MMWindowsProcessWrapperPW new]"] 11 | ifFalse: [MMUnixProcessWrapper new] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/disableDebug.st: -------------------------------------------------------------------------------- 1 | accessing 2 | disableDebug 3 | 4 | debug := false -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/enableDebug.st: -------------------------------------------------------------------------------- 1 | accessing 2 | enableDebug 3 | "Enable debugging to the Transcript. Must be called before a process is started." 4 | 5 | debug := true. 6 | Transcript class forceUpdate ifTrue: [Transcript show: 'WARNING: The "Force transcript updates to screen" preference is enabled. Consider to disable it to improve performance.'] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/initialize.st: -------------------------------------------------------------------------------- 1 | initialize-release 2 | initialize 3 | 4 | super initialize. 5 | debug := false -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/process..st: -------------------------------------------------------------------------------- 1 | accessing 2 | process: anObject 3 | 4 | process := anObject -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/process.st: -------------------------------------------------------------------------------- 1 | accessing 2 | process 3 | 4 | ^ process -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/startCommand.withArguments..st: -------------------------------------------------------------------------------- 1 | starting 2 | startCommand: command withArguments: arguments 3 | 4 | self subclassResponsibility -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/startNodeScript.arguments.nodeArguments..st: -------------------------------------------------------------------------------- 1 | starting 2 | startNodeScript: aString arguments: arguments nodeArguments: nodeArguments 3 | 4 | | node | 5 | node := OSProcess isWindows ifTrue: ['node'] ifFalse: ['/usr/bin/node']. 6 | self startCommand: node withArguments: nodeArguments, {aString. '--'}, arguments -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/stdinPutBuffer..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutBuffer: aByteArray 3 | 4 | self subclassResponsibility -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/stdinPutChar..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutChar: aCharacter 3 | 4 | self stdinPutUint8: aCharacter asciiValue -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/stdinPutUint32..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutUint32: anInteger 3 | 4 | self subclassResponsibility -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/stdinPutUint8..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutUint8: anInteger 3 | 4 | self subclassResponsibility -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/stdoutNextBuffer..st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextBuffer: anInteger 3 | 4 | self subclassResponsibility -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/stdoutNextChar.st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextChar 3 | 4 | ^ self stdoutNextUint8 asCharacter -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/stdoutNextUint32.st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextUint32 3 | 4 | self subclassResponsibility -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/stdoutNextUint8.st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextUint8 3 | 4 | self subclassResponsibility -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/instance/terminate.st: -------------------------------------------------------------------------------- 1 | stopping 2 | terminate 3 | 4 | self subclassResponsibility -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | "newForOS" : "MB 1/30/2022 10:33" }, 4 | "instance" : { 5 | "disableDebug" : "cmfcmf 8/3/2019 19:00", 6 | "enableDebug" : "MB 1/30/2022 10:33", 7 | "initialize" : "cmfcmf 8/3/2019 19:00", 8 | "process" : "cmfcmf 8/3/2019 19:00", 9 | "process:" : "cmfcmf 8/3/2019 19:00", 10 | "startCommand:withArguments:" : "cmfcmf 8/3/2019 19:00", 11 | "startNodeScript:arguments:nodeArguments:" : "MB 1/30/2022 10:33", 12 | "stdinPutBuffer:" : "cmfcmf 8/3/2019 19:00", 13 | "stdinPutChar:" : "cmfcmf 8/3/2019 19:00", 14 | "stdinPutUint32:" : "cmfcmf 8/3/2019 19:00", 15 | "stdinPutUint8:" : "cmfcmf 8/3/2019 19:00", 16 | "stdoutNextBuffer:" : "cmfcmf 8/3/2019 19:00", 17 | "stdoutNextChar" : "cmfcmf 8/3/2019 19:00", 18 | "stdoutNextUint32" : "cmfcmf 8/3/2019 19:00", 19 | "stdoutNextUint8" : "cmfcmf 8/3/2019 19:00", 20 | "terminate" : "cmfcmf 8/3/2019 19:00" } } 21 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMProcessWrapper.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "cmfcmf 8/3/2019 18:59", 8 | "instvars" : [ 9 | "process", 10 | "debug" ], 11 | "name" : "MMProcessWrapper", 12 | "pools" : [ 13 | ], 14 | "super" : "Object", 15 | "type" : "normal" } 16 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMTextMorphWithModel.class/README.md: -------------------------------------------------------------------------------- 1 | I am a PluggableTextMorphPlus that doesn't require a model. I an used to display code portals. -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMTextMorphWithModel.class/instance/aboutToStyle..st: -------------------------------------------------------------------------------- 1 | morphic ui 2 | aboutToStyle: aStyler 3 | 4 | ^ true -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMTextMorphWithModel.class/instance/initialize.st: -------------------------------------------------------------------------------- 1 | initialization 2 | initialize 3 | 4 | super initialize. 5 | self model: self -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMTextMorphWithModel.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "aboutToStyle:" : "cf 5/13/2019 08:41", 6 | "initialize" : "cf 5/13/2019 08:46" } } 7 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMTextMorphWithModel.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "cmfcmf 8/3/2019 18:52", 8 | "instvars" : [ 9 | ], 10 | "name" : "MMTextMorphWithModel", 11 | "pools" : [ 12 | ], 13 | "super" : "PluggableTextMorphPlus", 14 | "type" : "normal" } 15 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/README.md: -------------------------------------------------------------------------------- 1 | I am a wrapper around a Unix process managed using OSProcess. -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/instance/startCommand.withArguments..st: -------------------------------------------------------------------------------- 1 | starting 2 | startCommand: command withArguments: arguments 3 | 4 | | stdout outPipe stderr errPipe stdin inPipe | 5 | outPipe := OSPipe nonBlockingPipe. 6 | stdout := outPipe writer. 7 | 8 | errPipe := OSPipe nonBlockingPipe. 9 | stderr := errPipe writer. 10 | 11 | inPipe := OSPipe blockingPipe. 12 | stdin := inPipe reader. 13 | stdinWriter := inPipe writer binary. 14 | 15 | process := UnixProcess 16 | forkJob: command 17 | arguments: (arguments collect: #asString) 18 | environment: nil 19 | descriptors: {stdin. stdout. stderr}. 20 | 21 | stdoutReader := outPipe reader asMMBufferedStream. 22 | stdoutReader 23 | setBlocking; 24 | binary. 25 | 26 | debug ifTrue: [ | stderrReader | 27 | Transcript clear. 28 | stderrReader := errPipe reader. 29 | stderrReader ascii. 30 | [ 31 | [stderrReader closed] whileFalse: [ 32 | stderrReader next 33 | ifNil: [(Delay forSeconds: 1) wait] 34 | ifNotNil: [:next | Transcript show: next]]] fork] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/instance/stdinPutBuffer..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutBuffer: aBuffer 3 | 4 | aBuffer ifNotEmpty: [ 5 | stdinWriter 6 | nextPutAll: aBuffer; 7 | flush] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/instance/stdinPutUint32..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutUint32: anInteger 3 | 4 | stdinWriter 5 | uint32: anInteger; 6 | flush -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/instance/stdinPutUint8..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutUint8: anInteger 3 | 4 | stdinWriter 5 | nextPut: anInteger; 6 | flush -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/instance/stdoutNextBuffer..st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextBuffer: anInteger 3 | 4 | ^ stdoutReader next: anInteger -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/instance/stdoutNextUint32.st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextUint32 3 | 4 | ^ stdoutReader uint32 -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/instance/stdoutNextUint8.st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextUint8 3 | 4 | ^ stdoutReader next -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/instance/terminate.st: -------------------------------------------------------------------------------- 1 | stopping 2 | terminate 3 | 4 | self process 5 | closeStreams; 6 | sigterm -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "startCommand:withArguments:" : "MB 1/30/2022 10:33", 6 | "stdinPutBuffer:" : "MB 1/30/2022 10:33", 7 | "stdinPutUint32:" : "cmfcmf 8/3/2019 18:58", 8 | "stdinPutUint8:" : "cmfcmf 8/3/2019 18:58", 9 | "stdoutNextBuffer:" : "cf 4/24/2019 09:42", 10 | "stdoutNextUint32" : "cmfcmf 8/3/2019 18:58", 11 | "stdoutNextUint8" : "cmfcmf 8/3/2019 18:58", 12 | "terminate" : "cmfcmf 8/3/2019 18:58" } } 13 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMUnixProcessWrapper.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "cmfcmf 8/3/2019 18:57", 8 | "instvars" : [ 9 | "stdinWriter", 10 | "stdoutReader" ], 11 | "name" : "MMUnixProcessWrapper", 12 | "pools" : [ 13 | ], 14 | "super" : "MMProcessWrapper", 15 | "type" : "normal" } 16 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/README.md: -------------------------------------------------------------------------------- 1 | I am a wrapper around a Windows process managed using FFI. -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/peek..st: -------------------------------------------------------------------------------- 1 | private 2 | peek: aHandle 3 | 4 | ^ CMFWinFFI peek: (aHandle getHandle unsignedLongLongAt: 1) -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/readBuffer.length..st: -------------------------------------------------------------------------------- 1 | private 2 | readBuffer: aHandle length: anInteger 3 | 4 | | bytesRead buffer | 5 | buffer := ByteArray new: anInteger. 6 | 7 | bytesRead := CMFWinFFI makePointerOf: ExternalType ulong. 8 | CMFWinFFI noError: [ 9 | CMFWinFFI 10 | readFile: (aHandle getHandle unsignedLongLongAt: 1) 11 | buffer: buffer 12 | bytesToRead: anInteger 13 | bytesRead: bytesRead 14 | overlapped: nil]. 15 | bytesRead := bytesRead getHandle unsignedLongAt: 1. 16 | self assert: bytesRead = anInteger. 17 | 18 | ^ buffer -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/startCommand.withArguments..st: -------------------------------------------------------------------------------- 1 | starting 2 | startCommand: command withArguments: arguments 3 | 4 | | startupInfo pipe commandLine | 5 | self flag: #todo. 6 | "Properly escape arguments?" 7 | commandLine := command, ' ', ((arguments collect: [:arg | '"', arg, '"']) joinSeparatedBy: ' '). 8 | 9 | pipe := CMFWinFFI makePipe: false. 10 | stdinRead := pipe first. 11 | stdinWrite := pipe second. 12 | 13 | pipe := CMFWinFFI makePipe: true size: 1000000000. 14 | stdoutRead := pipe first. 15 | stdoutWrite := pipe second. 16 | 17 | debug ifTrue: [ 18 | pipe := CMFWinFFI makePipe: true. 19 | stderrRead := pipe first. 20 | stderrWrite := pipe second]. 21 | 22 | startupInfo := CMFWinStartupInfo new. 23 | startupInfo 24 | cb: CMFWinStartupInfo byteSize; 25 | dwFlags: (1 bitOr: 256); 26 | wShowWindow: 0; 27 | hStdInput: (stdinRead getHandle unsignedLongLongAt: 1); 28 | hStdOutput: (stdoutWrite getHandle unsignedLongLongAt: 1); 29 | yourself. 30 | "STARTF_USESHOWWINDOW" 31 | "STARTF_USESTDHANDLES" 32 | "SW_HIDE" 33 | debug ifTrue: [startupInfo hStdError: (stderrWrite getHandle unsignedLongLongAt: 1)]. 34 | 35 | processInformation := CMFWinProcessInformation new. 36 | CMFWinFFI noError: [ 37 | CMFWinFFI 38 | createProcessApplicationName: nil 39 | commandLine: commandLine 40 | processAttributes: nil 41 | threadAttributes: nil 42 | inheritHandles: true 43 | creationFlags: 0 44 | environment: nil 45 | currentDirectory: nil 46 | startupInfo: startupInfo 47 | processInformation: processInformation]. 48 | 49 | debug ifTrue: [ 50 | Transcript clear. 51 | [ 52 | [ 53 | [ | bytesAvailable | 54 | bytesAvailable := self peek: stderrRead. 55 | bytesAvailable > 0 ifTrue: [Transcript show: (self stderrNextBuffer: bytesAvailable) asString]. 56 | (Delay forMilliseconds: 200) wait. 57 | true] ifError: [false]] whileTrue] fork] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/stderrNextBuffer..st: -------------------------------------------------------------------------------- 1 | stderr 2 | stderrNextBuffer: anInteger 3 | 4 | self waitFor: anInteger at: stderrRead. 5 | ^ self readBuffer: stderrRead length: anInteger -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/stdinPutBuffer..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutBuffer: aBuffer 3 | 4 | | bytesWritten | 5 | bytesWritten := CMFWinFFI makePointerOf: ExternalType ulong. 6 | CMFWinFFI noError: [ 7 | CMFWinFFI 8 | writeFile: (stdinWrite getHandle unsignedLongLongAt: 1) 9 | buffer: aBuffer asByteArray 10 | bytesToWrite: aBuffer size 11 | bytesWritten: bytesWritten 12 | overlapped: nil]. 13 | bytesWritten := bytesWritten getHandle unsignedLongAt: 1. 14 | self assert: [bytesWritten = aBuffer size] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/stdinPutUint32..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutUint32: anInteger 3 | 4 | | buffer | 5 | buffer := ByteArray new: 4. 6 | buffer byteAt: 1 put: (anInteger digitAt: 4). 7 | buffer byteAt: 2 put: (anInteger digitAt: 3). 8 | buffer byteAt: 3 put: (anInteger digitAt: 2). 9 | buffer byteAt: 4 put: (anInteger digitAt: 1). 10 | self stdinPutBuffer: buffer -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/stdinPutUint8..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutUint8: anInteger 3 | 4 | | buffer | 5 | buffer := ByteArray new: 1. 6 | buffer byteAt: 1 put: anInteger. 7 | self stdinPutBuffer: buffer -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/stdoutNextBuffer..st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextBuffer: anInteger 3 | 4 | self waitFor: anInteger at: stdoutRead. 5 | ^ self readBuffer: stdoutRead length: anInteger -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/stdoutNextUint32.st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextUint32 3 | 4 | | n buffer | 5 | buffer := self stdoutNextBuffer: 4. 6 | n := buffer byteAt: 1. 7 | n := (n bitShift: 8) + (buffer byteAt: 2). 8 | n := (n bitShift: 8) + (buffer byteAt: 3). 9 | n := (n bitShift: 8) + (buffer byteAt: 4). 10 | ^ n -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/stdoutNextUint8.st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextUint8 3 | 4 | | buffer | 5 | buffer := self stdoutNextBuffer: 1. 6 | ^ buffer byteAt: 1 -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/terminate.st: -------------------------------------------------------------------------------- 1 | stopping 2 | terminate 3 | 4 | processInformation ifNotNil: [ 5 | CMFWinFFI terminateProcess: processInformation hProcess exitCode: 0. 6 | 7 | CMFWinFFI closeHandle: processInformation hThread. 8 | CMFWinFFI closeHandle: processInformation hProcess]. 9 | 10 | [CMFWinFFI closeHandle: (stdinRead getHandle unsignedLongLongAt: 1)] ifError: []. 11 | [CMFWinFFI closeHandle: (stdinWrite getHandle unsignedLongLongAt: 1)] ifError: []. 12 | 13 | [CMFWinFFI closeHandle: (stdoutRead getHandle unsignedLongLongAt: 1)] ifError: []. 14 | [CMFWinFFI closeHandle: (stdoutWrite getHandle unsignedLongLongAt: 1)] ifError: []. 15 | 16 | [stderrRead ifNotNil: [CMFWinFFI closeHandle: (stderrRead getHandle unsignedLongLongAt: 1)]] ifError: []. 17 | [stderrWrite ifNotNil: [CMFWinFFI closeHandle: (stderrWrite getHandle unsignedLongLongAt: 1)]] ifError: [] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/instance/waitFor.at..st: -------------------------------------------------------------------------------- 1 | private 2 | waitFor: anInteger at: aHandle 3 | 4 | [(self peek: aHandle) < anInteger] whileTrue: [ 5 | (Delay forMilliseconds: 5) wait 6 | "Processor yield"] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "peek:" : "cf 5/12/2019 15:16", 6 | "readBuffer:length:" : "MB 1/30/2022 10:33", 7 | "startCommand:withArguments:" : "MB 1/30/2022 10:33", 8 | "stderrNextBuffer:" : "cf 5/12/2019 14:36", 9 | "stdinPutBuffer:" : "MB 1/30/2022 10:33", 10 | "stdinPutUint32:" : "cf 5/12/2019 14:44", 11 | "stdinPutUint8:" : "cf 5/12/2019 14:02", 12 | "stdoutNextBuffer:" : "cf 5/12/2019 14:36", 13 | "stdoutNextUint32" : "cmfcmf 8/3/2019 18:58", 14 | "stdoutNextUint8" : "cmfcmf 8/3/2019 18:58", 15 | "terminate" : "MB 1/30/2022 10:33", 16 | "waitFor:at:" : "MB 1/30/2022 10:33" } } 17 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperFFI.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "cmfcmf 8/3/2019 18:56", 8 | "instvars" : [ 9 | "stdinRead", 10 | "stdinWrite", 11 | "stdoutRead", 12 | "stdoutWrite", 13 | "stderrRead", 14 | "stderrWrite", 15 | "processInformation" ], 16 | "name" : "MMWindowsProcessWrapperFFI", 17 | "pools" : [ 18 | ], 19 | "super" : "MMProcessWrapper", 20 | "type" : "normal" } 21 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/README.md: -------------------------------------------------------------------------------- 1 | I am a wrapper around ProcessWrapper (funny, eh?). As ProcessWrapper is rather unstable, I am not currently in use. Instead, use MMWindowsProcessWrapperFFI, a wrapper around FFI. -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/instance/startCommand.withArguments..st: -------------------------------------------------------------------------------- 1 | starting 2 | startCommand: command withArguments: arguments 3 | 4 | self process: ProcessWrapper new. 5 | self process 6 | useStdout; 7 | useStderr. 8 | self flag: #todo. 9 | "Properly escape arguments" 10 | self process startWithShellCommand: command, ' ', ((arguments collect: [:arg | '"', arg, '"']) joinSeparatedBy: ' '). 11 | self process stdoutStream binary. 12 | self process stderrStream ascii. 13 | 14 | debug ifTrue: [ 15 | Transcript clear. 16 | [[self process stderrStream atEnd] whileFalse: [Transcript show: self process stderrStream next]] fork] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/instance/stdinPutBuffer..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutBuffer: aBuffer 3 | 4 | aBuffer ifNotEmpty: [self process writeToStdin: aBuffer asByteArray] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/instance/stdinPutUint32..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutUint32: anInteger 3 | 4 | self stdinPutBuffer: {anInteger digitAt: 4. anInteger digitAt: 3. anInteger digitAt: 2. anInteger digitAt: 1} -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/instance/stdinPutUint8..st: -------------------------------------------------------------------------------- 1 | stdin 2 | stdinPutUint8: anInteger 3 | 4 | self stdinPutBuffer: {anInteger} -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/instance/stdoutNextBuffer..st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextBuffer: anInteger 3 | 4 | ^ self process stdoutStream next: anInteger -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/instance/stdoutNextUint32.st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextUint32 3 | 4 | | n | 5 | n := self process stdoutStream next. 6 | n := (n bitShift: 8) + self process stdoutStream next. 7 | n := (n bitShift: 8) + self process stdoutStream next. 8 | n := (n bitShift: 8) + self process stdoutStream next. 9 | ^ n -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/instance/stdoutNextUint8.st: -------------------------------------------------------------------------------- 1 | stdout 2 | stdoutNextUint8 3 | 4 | ^ self process stdoutStream next -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/instance/terminate.st: -------------------------------------------------------------------------------- 1 | stopping 2 | terminate 3 | 4 | self flag: #todo. 5 | "ProcessWrapper>>terminate never returns, therefore we just ask the process to end :(" 6 | [ 7 | self 8 | stdinPutUint32: 1; 9 | stdinPutChar: $k] ifError: ["Silently ignore errors" 10 | ] -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "startCommand:withArguments:" : "MB 1/30/2022 10:33", 6 | "stdinPutBuffer:" : "cmfcmf 8/3/2019 18:53", 7 | "stdinPutUint32:" : "MB 1/30/2022 10:33", 8 | "stdinPutUint8:" : "cmfcmf 8/3/2019 18:53", 9 | "stdoutNextBuffer:" : "cmfcmf 8/3/2019 18:53", 10 | "stdoutNextUint32" : "cmfcmf 8/3/2019 18:53", 11 | "stdoutNextUint8" : "cmfcmf 8/3/2019 18:53", 12 | "terminate" : "MB 1/30/2022 10:33" } } 13 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/MMWindowsProcessWrapperPW.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Core", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "cmfcmf 8/3/2019 18:54", 8 | "instvars" : [ 9 | ], 10 | "name" : "MMWindowsProcessWrapperPW", 11 | "pools" : [ 12 | ], 13 | "super" : "MMProcessWrapper", 14 | "type" : "normal" } 15 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/Object.extension/class/rickRoll.st: -------------------------------------------------------------------------------- 1 | *MagicMouse-Core 2 | rickRoll 3 | 4 | MMBrowser rickRoll -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/Object.extension/instance/rickRoll.st: -------------------------------------------------------------------------------- 1 | *MagicMouse-Core 2 | rickRoll 3 | 4 | self class rickRoll -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/Object.extension/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | "rickRoll" : "cmfcmf 6/30/2019 17:42" }, 4 | "instance" : { 5 | "rickRoll" : "cmfcmf 6/30/2019 17:42" } } 6 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/Object.extension/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Object" } 3 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/SearchBar.extension/instance/magicSearch.in..st: -------------------------------------------------------------------------------- 1 | *MagicMouse-Core 2 | magicSearch: text in: morph 3 | 4 | (text beginsWith: '!s') 5 | ifTrue: [^ MMBrowser searchFor: 'site:squeak.org ', (text copyAfter: $ )]. 6 | (text includes: $ ) ifTrue: [^ MMBrowser searchFor: text]. 7 | (text beginsWith: '!') ifTrue: [^ MMBrowser searchFor: (text copyAfter: $!)]. 8 | ^ self smartSearch: text in: morph -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/SearchBar.extension/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "magicSearch:in:" : "cmfcmf 6/30/2019 17:49" } } 6 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/SearchBar.extension/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "SearchBar" } 3 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/SequenceableCollection.extension/instance/findFirst.startingAt..st: -------------------------------------------------------------------------------- 1 | *MagicMouse-Core 2 | findFirst: aBlock startingAt: anInteger 3 | "Return the index of my first element for which aBlock evaluates as true." 4 | 5 | | index | 6 | index := anInteger - 1. 7 | [(index := index + 1) <= self size] whileTrue: 8 | [(aBlock value: (self at: index)) ifTrue: [^index]]. 9 | ^ 0 -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/SequenceableCollection.extension/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "findFirst:startingAt:" : "cf 5/13/2019 15:03" } } 6 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/SequenceableCollection.extension/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "SequenceableCollection" } 3 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/TextEditor.extension/instance/browseClassFromIt.st: -------------------------------------------------------------------------------- 1 | *MagicMouse-Core 2 | browseClassFromIt 3 | "Launch a hierarchy browser for the class indicated by the current selection. If multiple classes matching the selection exist, let the user choose among them." 4 | | aClass text | 5 | text := self selection string withBlanksTrimmed. 6 | self lineSelectAndEmptyCheck: [ ^ self ]. 7 | aClass := UIManager default 8 | classFromPattern: text 9 | withCaption: 'choose a class to browse...' 10 | in: model environment. 11 | aClass ifNil: [ 12 | ((text beginsWith: 'http://') or: [text beginsWith: 'https://']) ifTrue: [ 13 | ^ MMBrowser openOn: text]. 14 | ^ morph flash ]. 15 | self systemNavigation 16 | spawnHierarchyForClass: aClass 17 | selector: nil -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/TextEditor.extension/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "browseClassFromIt" : "cmfcmf 7/3/2019 11:24" } } 6 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/TextEditor.extension/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "TextEditor" } 3 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/monticello.meta/categories.st: -------------------------------------------------------------------------------- 1 | SystemOrganization addCategory: #'MagicMouse-Core'! 2 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/monticello.meta/initializers.st: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/packages/MagicMouse-Core.package/monticello.meta/initializers.st -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/monticello.meta/postscript.st: -------------------------------------------------------------------------------- 1 | (PackageInfo named: 'MagicMouse-Core') postscript: 'MMPluginDownloader downloadIfConfirmed'! 2 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/monticello.meta/preambleOfRemoval.st: -------------------------------------------------------------------------------- 1 | (PackageInfo named: 'MagicMouse-Core') preambleOfRemoval: 'MMPluginDownloader removeDownloads'! 2 | -------------------------------------------------------------------------------- /packages/MagicMouse-Core.package/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/.filetree: -------------------------------------------------------------------------------- 1 | { 2 | "noMethodMetaData" : true, 3 | "separateMethodMetaAndSource" : false, 4 | "useCypressPropertiesFile" : true } 5 | -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/.squot-contents: -------------------------------------------------------------------------------- 1 | SquotTrackedObjectMetadata { 2 | #objectClassName : #PackageInfo, 3 | #objectsReplacedByNames : true, 4 | #serializer : #SquotCypressCodeSerializer 5 | } -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/MMBrowserMorphTest.class/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/packages/MagicMouse-Tests.package/MMBrowserMorphTest.class/README.md -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/MMBrowserMorphTest.class/instance/testHandleStructuredData.st: -------------------------------------------------------------------------------- 1 | as yet unclassified 2 | testHandleStructuredData 3 | 4 | | payload result | 5 | payload := ByteArray new: 11. 6 | payload unsignedCharAt: 1 put: $s. 7 | payload unsignedLongAt: 2 put: 1. 8 | payload unsignedLongAt: 6 put: 2. 9 | payload unsignedCharAt: 10 put: ${. 10 | payload unsignedCharAt: 11 put: $}. 11 | result := MMBrowserMorph new handleStructuredData: payload. 12 | self assert: result size equals: 1. 13 | self assert: result first size equals: 0 -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/MMBrowserMorphTest.class/instance/testHandleStructuredDataEmpty.st: -------------------------------------------------------------------------------- 1 | as yet unclassified 2 | testHandleStructuredDataEmpty 3 | 4 | | payload result | 5 | payload := ByteArray new: 5. 6 | payload unsignedCharAt: 1 put: $s. 7 | payload unsignedLongAt: 2 put: 0. 8 | result := MMBrowserMorph new handleStructuredData: payload. 9 | self assert: result size equals: 0 -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/MMBrowserMorphTest.class/methodProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "class" : { 3 | }, 4 | "instance" : { 5 | "testHandleStructuredData" : "MB 1/30/2022 10:33", 6 | "testHandleStructuredDataEmpty" : "MB 1/30/2022 10:33" } } 7 | -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/MMBrowserMorphTest.class/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "category" : "MagicMouse-Tests", 3 | "classinstvars" : [ 4 | ], 5 | "classvars" : [ 6 | ], 7 | "commentStamp" : "", 8 | "instvars" : [ 9 | ], 10 | "name" : "MMBrowserMorphTest", 11 | "pools" : [ 12 | ], 13 | "super" : "TestCase", 14 | "type" : "normal" } 15 | -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/monticello.meta/categories.st: -------------------------------------------------------------------------------- 1 | SystemOrganization addCategory: #'MagicMouse-Tests'! 2 | -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/monticello.meta/initializers.st: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmfcmf/MagicMouse/14f228841339e8a82ba7044389764a9ca53c0372/packages/MagicMouse-Tests.package/monticello.meta/initializers.st -------------------------------------------------------------------------------- /packages/MagicMouse-Tests.package/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /patches/puppeteer-core+1.19.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/puppeteer-core/lib/Events.js b/node_modules/puppeteer-core/lib/Events.js 2 | index c994de5..fbc9d65 100644 3 | --- a/node_modules/puppeteer-core/lib/Events.js 4 | +++ b/node_modules/puppeteer-core/lib/Events.js 5 | @@ -36,6 +36,7 @@ const Events = { 6 | Popup: 'popup', 7 | WorkerCreated: 'workercreated', 8 | WorkerDestroyed: 'workerdestroyed', 9 | + ScreencastFrame: 'screencastframe' 10 | }, 11 | 12 | Browser: { 13 | diff --git a/node_modules/puppeteer-core/lib/Page.js b/node_modules/puppeteer-core/lib/Page.js 14 | index 0a8acb5..64d5558 100644 15 | --- a/node_modules/puppeteer-core/lib/Page.js 16 | +++ b/node_modules/puppeteer-core/lib/Page.js 17 | @@ -124,6 +124,7 @@ class Page extends EventEmitter { 18 | client.on('Performance.metrics', event => this._emitMetrics(event)); 19 | client.on('Log.entryAdded', event => this._onLogEntryAdded(event)); 20 | client.on('Page.fileChooserOpened', event => this._onFileChooser(event)); 21 | + client.on('Page.screencastFrame', event => this.emit(Events.Page.ScreencastFrame, event)); 22 | this._target._isClosedPromise.then(() => { 23 | this.emit(Events.Page.Close); 24 | this._closed = true; 25 | @@ -176,6 +177,54 @@ class Page extends EventEmitter { 26 | }); 27 | } 28 | 29 | + /** 30 | + * @param {!Object=} options 31 | + * @return {!Promise} 32 | + */ 33 | + async startScreencast(options = {}) { 34 | + if (options.format) 35 | + console.assert(options.format === 'png' || options.format === 'jpeg', 'Unknown options.format value: ' + options.format); 36 | + 37 | + if (options.quality) { 38 | + console.assert(screenshotType === 'jpeg', 'options.quality is unsupported for the ' + screenshotType + ' screenshots'); 39 | + console.assert(typeof options.quality === 'number', 'Expected options.quality to be a number but found ' + (typeof options.quality)); 40 | + console.assert(Number.isInteger(options.quality), 'Expected options.quality to be an integer'); 41 | + console.assert(options.quality >= 0 && options.quality <= 100, 'Expected options.quality to be between 0 and 100 (inclusive), got ' + options.quality); 42 | + } 43 | + if (options.maxWidth) { 44 | + console.assert(typeof options.maxWidth === 'number', 'Expected options.maxWidth to be a number but found ' + (typeof options.maxWidth)); 45 | + console.assert(Number.isInteger(options.maxWidth), 'Expected options.maxWidth to be an integer'); 46 | + console.assert(options.maxHeight >= 0, 'Expected options.maxWidth to be greater than 0, got ' + options.maxWidth); 47 | + } 48 | + if (options.maxHeight) { 49 | + console.assert(typeof options.maxHeight === 'number', 'Expected options.maxHeight to be a number but found ' + (typeof options.maxHeight)); 50 | + console.assert(Number.isInteger(options.maxHeight), 'Expected options.maxHeight to be an integer'); 51 | + console.assert(options.maxHeight >= 0, 'Expected options.maxHeight to be greater than 0, got ' + options.maxHeight); 52 | + } 53 | + if (options.everyNthFrame) { 54 | + console.assert(typeof options.everyNthFrame === 'number', 'Expected options.everyNthFrame to be a number but found ' + (typeof options.everyNthFrame)); 55 | + console.assert(Number.isInteger(options.everyNthFrame), 'Expected options.everyNthFrame to be an integer'); 56 | + console.assert(options.everyNthFrame >= 0, 'Expected options.everyNthFrame to be greater than 0, got ' + options.everyNthFrame); 57 | + } 58 | + await this._client.send('Page.startScreencast', options); 59 | + } 60 | + 61 | + /** 62 | + * @param {!number} sessionId 63 | + * @return {!Promise} 64 | + */ 65 | + async screencastFrameAck(sessionId) { 66 | + console.assert(sessionId, 'Expected sessionId'); 67 | + await this._client.send('Page.screencastFrameAck', { sessionId }); 68 | + } 69 | + 70 | + /** 71 | + * @return {!Promise} 72 | + */ 73 | + async stopScreencast() { 74 | + await this._client.send('Page.stopScreencast'); 75 | + } 76 | + 77 | /** 78 | * @param {!{longitude: number, latitude: number, accuracy: (number|undefined)}} options 79 | */ 80 | -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer-core"); 2 | const SmartBuffer = require("smart-buffer").SmartBuffer; 3 | const getPixels = require("get-pixels"); 4 | const findChrome = require("chrome-finder"); 5 | const awaitifyStream = require("awaitify-stream"); 6 | const { getElements } = require("./getElements"); 7 | const uuid = require("uuid").v1; 8 | 9 | Array.prototype.flat = function () { 10 | return this.reduce((acc, x) => acc.concat(x), []); 11 | }; 12 | Array.prototype.flatMap = function (mapper) { 13 | return this.map(mapper).flat(); 14 | }; 15 | 16 | SmartBuffer.prototype.writeStringPrependSize = function (string) { 17 | const buffer = Buffer.from(string, "utf8"); 18 | this.writeUInt32LE(buffer.length); 19 | this.writeBuffer(buffer); 20 | }; 21 | 22 | const ID_ATTRIBUTE = "data-magic-mouse-id"; 23 | const IMAGE_FORMAT = "jpeg"; // "raw", "jpeg", "png" 24 | 25 | console.error("Node.js arguments", process.argv); 26 | console.error("Image format", IMAGE_FORMAT); 27 | 28 | const run = async () => { 29 | const url = process.argv[process.argv.length - 5]; 30 | const screenSize = { 31 | x: parseInt(process.argv[process.argv.length - 4], 10), 32 | y: parseInt(process.argv[process.argv.length - 3], 10), 33 | }; 34 | const headless = process.argv[process.argv.length - 2] === "headless"; 35 | const chromeProfilePath = process.argv[process.argv.length - 1]; 36 | 37 | const terminate = async () => { 38 | console.error("Terminating..."); 39 | // await page.stopScreencast(); 40 | await browser.close(); 41 | process.exit(); 42 | }; 43 | 44 | const sendCommand = (buffer) => { 45 | const lenBuffer = new Buffer(4); 46 | lenBuffer.writeUInt32BE(buffer.length); 47 | process.stdout.write(lenBuffer); 48 | process.stdout.write(buffer); 49 | }; 50 | 51 | const browser = await puppeteer.launch({ 52 | ignoreDefaultArgs: true, 53 | args: [ 54 | "--enable-automation", 55 | ...(headless ? ["--headless"] : []), 56 | // '--start-fullscreen', 57 | "--force-device-scale-factor=1", 58 | `--user-data-dir=${chromeProfilePath}`, 59 | ], 60 | executablePath: findChrome(), 61 | }); 62 | const page = await browser.newPage(); 63 | await page.setBypassCSP(true); 64 | await page.setUserAgent( 65 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36", 66 | ); 67 | await page.setViewport({ width: screenSize.x, height: screenSize.y }); 68 | 69 | process.on("SIGTERM", terminate); 70 | 71 | const instrumentGoogleSlides = async () => { 72 | if (!page.url().startsWith("https://docs.google.com/presentation")) { 73 | return; 74 | } 75 | console.error("Instrumenting Google Slides"); 76 | const morphPositions = ( 77 | await Promise.all( 78 | page 79 | .frames() 80 | .filter((frame) => !frame.isDetached()) 81 | .map(async (frame) => 82 | frame.evaluate( 83 | (ID_ATTRIBUTE) => 84 | Promise.all( 85 | Array.from( 86 | new Set( 87 | Array.from( 88 | document.querySelectorAll('.punch-viewer-svgpage g[id*="paragraph"]'), 89 | ).map((element) => element.parentNode), 90 | ), 91 | ) 92 | .map((element) => ({ 93 | element, 94 | text: Array.from(element.querySelectorAll('g[id*="paragraph"]')) 95 | .map((paragraph) => 96 | Array.from(paragraph.querySelectorAll(".sketchy-text-content-text text")) 97 | .map((text) => text.textContent) 98 | .join(" "), 99 | ) 100 | .join("\r"), 101 | })) 102 | .filter((tmp) => tmp.text.startsWith("!")) 103 | .map((tmp) => { 104 | let element = tmp.element; 105 | let path = null; 106 | do { 107 | element = element.parentNode; 108 | path = element.querySelector("path"); 109 | } while (path === null); 110 | return { ...tmp, path }; 111 | }) 112 | .map(async (tmp) => { 113 | const rect = tmp.path.getBoundingClientRect(); 114 | 115 | let id = tmp.path.getAttribute(ID_ATTRIBUTE); 116 | if (!id) { 117 | id = await window.uuid(); 118 | tmp.path.setAttribute(ID_ATTRIBUTE, id); 119 | } 120 | 121 | return { 122 | id, 123 | type: "morph", 124 | x: rect.x, 125 | y: rect.y, 126 | w: rect.width, 127 | h: rect.height, 128 | data: tmp.text, 129 | }; 130 | }), 131 | ), 132 | ID_ATTRIBUTE, 133 | ), 134 | ), 135 | ) 136 | ).flat(); 137 | 138 | console.error("Google Slides Morph Positions", morphPositions); 139 | morphPositions.forEach(sendPortalDataCommand); 140 | }; 141 | 142 | const instrumentGitHub = async () => { 143 | if (!page.url().startsWith("https://github.com/")) { 144 | return; 145 | } 146 | console.error("Instrumenting clone button"); 147 | page.evaluate(() => { 148 | const bar = document.getElementsByClassName("file-navigation"); 149 | if (bar.length === 0) { 150 | return; 151 | } 152 | 153 | const urlField = document.querySelector(".https-clone-options input"); 154 | if (!urlField) { 155 | return; 156 | } 157 | const cloneUrl = urlField.value; 158 | let name = cloneUrl.split("/")[4]; 159 | name = name.substr(0, name.length - 4); 160 | bar[0].insertAdjacentHTML( 161 | "beforeEnd", 162 | `Clone to Squeak`, 163 | ); 164 | 165 | const normalCloneButton = document.getElementsByClassName("get-repo-select-menu"); 166 | if (normalCloneButton.length === 0 || normalCloneButton[0].children.length === 0) { 167 | return; 168 | } 169 | normalCloneButton[0].children[0].classList.remove("btn-primary"); 170 | }); 171 | }; 172 | 173 | const parseLDJsons = async () => { 174 | const ldJsons = await page.$$eval('script[type="application/ld+json"]', (nodes) => 175 | nodes.map((node) => JSON.parse(node.innerText)), 176 | ); 177 | console.error("LD JSON", ldJsons); 178 | const buf = new SmartBuffer(); 179 | buf.writeString("s"); 180 | buf.writeUInt32LE(ldJsons.length); 181 | ldJsons.forEach((ldJson) => { 182 | const json = JSON.stringify(ldJson); 183 | buf.writeStringPrependSize(json); 184 | }); 185 | await sendCommand(buf.toBuffer()); 186 | }; 187 | 188 | const ignoreExecutionContextDestroyed = (error) => { 189 | // Sometimes the frame is destroyed right after navigation, thus throwing an error 190 | // when trying to evaluate a function in the frame's context. That's why we catch 191 | // those errors here. 192 | if ( 193 | !error.message.endsWith( 194 | "Execution context was destroyed, most likely because of a navigation.", 195 | ) && 196 | !error.message.endsWith("Cannot find context with specified id") 197 | ) { 198 | throw error; 199 | } 200 | }; 201 | 202 | page.on("dialog", async (dialog) => { 203 | // TODO: We could send the dialog contents to Squeak and ask the user what to do. 204 | // For now, accept all dialogs, so that the page remains responsive. 205 | console.error("Dialog!", dialog.defaultValue(), dialog.message(), dialog.type()); 206 | await dialog.accept(); 207 | }); 208 | 209 | page.on("framenavigated", async (frame) => { 210 | await new Promise((resolve) => setTimeout(() => resolve(), 50)); 211 | try { 212 | await parseLDJsons(); 213 | await instrumentGoogleSlides(); 214 | await instrumentGitHub(); 215 | 216 | if (!frame.parentFrame()) { 217 | const url = page.url(); 218 | console.error(`Navigating to ${url}`); 219 | const buf = new SmartBuffer(); 220 | buf.writeString("l"); 221 | buf.writeString(url); 222 | sendCommand(buf.toBuffer()); 223 | } 224 | } catch (error) { 225 | ignoreExecutionContextDestroyed(error); 226 | } 227 | }); 228 | 229 | page.on("domcontentloaded", async () => { 230 | await parseLDJsons(); 231 | // TODO: Is this needed when we have framenavigated? 232 | // await instrumentGitHub(); 233 | // await instrumentGoogleSlides(); 234 | }); 235 | 236 | const refreshTrackedElements = async () => { 237 | const trackedElements = ( 238 | await Promise.all( 239 | page 240 | .frames() 241 | .filter((frame) => !frame.isDetached()) 242 | .map((frame) => 243 | frame.evaluate(getElements, "refreshInfo").catch((error) => { 244 | ignoreExecutionContextDestroyed(error); 245 | return []; 246 | }), 247 | ), 248 | ) 249 | ).flat(); 250 | // console.error(trackedElements); 251 | const buf = new SmartBuffer(); 252 | buf.writeString("hr"); // portal refresh 253 | buf.writeUInt32LE(trackedElements.length); 254 | trackedElements.forEach((element) => 255 | buf 256 | .writeStringNT(element.id) 257 | .writeInt32LE(element.x) 258 | .writeInt32LE(element.y) 259 | .writeInt32LE(element.w) 260 | .writeInt32LE(element.h), 261 | ); 262 | sendCommand(buf.toBuffer()); 263 | }; 264 | 265 | page.on("screencastframe", async (frame) => { 266 | const screenshot = Buffer.from(frame.data, "base64"); 267 | 268 | const buf = new SmartBuffer(); 269 | if (IMAGE_FORMAT === "png") { 270 | buf.writeString("ip"); 271 | buf.writeBuffer(screenshot); 272 | } else if (IMAGE_FORMAT === "jpeg") { 273 | buf.writeString("ij"); 274 | buf.writeBuffer(screenshot); 275 | } else if (IMAGE_FORMAT === "raw") { 276 | const pixels = await new Promise((resolve, reject) => 277 | getPixels(screenshot, "image/png", (err, pixels) => { 278 | if (err) { 279 | reject(err); 280 | } else { 281 | resolve(pixels); 282 | } 283 | }), 284 | ); 285 | buf.writeString("ir"); 286 | buf.writeUInt32LE(pixels.shape[0]); 287 | buf.writeUInt32LE(pixels.shape[1]); 288 | buf.writeBuffer(Buffer.from(pixels.data)); 289 | } else { 290 | throw new Error(`Unsupported image format ${IMAGE_FORMAT}.`); 291 | } 292 | sendCommand(buf.toBuffer()); 293 | 294 | await refreshTrackedElements(); 295 | 296 | await page.screencastFrameAck(frame.sessionId); 297 | console.error(`Sent frame at ${Date.now() / 1000}`); 298 | }); 299 | 300 | await page.exposeFunction("uuid", () => uuid()); 301 | await page.exposeFunction("gitClone", async (name, url) => { 302 | console.error("GIT CLONE", name, url); 303 | const buf = new SmartBuffer(); 304 | buf.writeString("g"); 305 | buf.writeStringPrependSize(name); 306 | buf.writeString(url); 307 | await sendCommand(buf.toBuffer()); 308 | }); 309 | 310 | const sendPortalDataCommand = (data) => { 311 | const buf = new SmartBuffer(); 312 | switch (data.type) { 313 | case "img": 314 | case "canvas": 315 | buf.writeString("hi"); 316 | break; 317 | case "pre": 318 | buf.writeString("hc"); 319 | break; 320 | case "morph": 321 | buf.writeString("hm"); 322 | break; 323 | } 324 | buf.writeStringNT(data.id); 325 | buf.writeInt32LE(data.x); 326 | buf.writeInt32LE(data.y); 327 | buf.writeInt32LE(data.w); 328 | buf.writeInt32LE(data.h); 329 | switch (data.type) { 330 | case "img": 331 | case "canvas": 332 | buf.writeBuffer(Buffer.from(data.data, "base64")); 333 | break; 334 | case "pre": 335 | case "morph": 336 | buf.writeString(data.data); 337 | break; 338 | } 339 | sendCommand(buf.toBuffer()); 340 | }; 341 | 342 | // TODO: This throws an error in case the page can't be reached (e.g., when you have no network connection) 343 | console.error(`Navigating to ${url}`); 344 | await page.goto(url); 345 | 346 | console.error("Starting screencast..."); 347 | await page.startScreencast({ 348 | format: IMAGE_FORMAT === "jpeg" ? "jpeg" : "png", 349 | everyNthFrame: 1, 350 | }); 351 | console.error("Recording screencast..."); 352 | 353 | const reader = awaitifyStream.createReader(process.stdin); 354 | while (true) { 355 | const size = (await reader.readAsync(4)).readUInt32BE(); 356 | console.error(`Waiting for payload of size ${size}`); 357 | const command = String.fromCharCode((await reader.readAsync(1)).readUInt8()); 358 | const payload = 359 | size > 1 ? SmartBuffer.fromBuffer(await reader.readAsync(size - 1)) : new SmartBuffer(); 360 | console.error(`Received command ${command} with payload of size ${size}.`); 361 | 362 | switch (command) { 363 | case "s": { 364 | const x = payload.readInt32LE(); 365 | const y = payload.readInt32LE(); 366 | const str = payload.readString(); 367 | console.error(`Dropped String at ${x}@${y}: "${str}"`); 368 | 369 | await page.evaluateHandle( 370 | (x, y, str) => { 371 | const field = document 372 | .elementsFromPoint(x, y) 373 | .find( 374 | (element) => 375 | ["INPUT", "TEXTAREA"].includes(element.tagName) || 376 | element.contentEditable === "true", 377 | ); 378 | if (!field) { 379 | return; 380 | } 381 | if (field.tagName === "INPUT") { 382 | field.value = str; 383 | } else { 384 | field.innerText = str; 385 | } 386 | }, 387 | x, 388 | y, 389 | str, 390 | ); 391 | break; 392 | } 393 | case "f": { 394 | const x = payload.readInt32LE(); 395 | const y = payload.readInt32LE(); 396 | console.error(`DROPPED MORPH AT ${x}@${y}`); 397 | 398 | const form = await page.evaluateHandle( 399 | (x, y) => document.elementsFromPoint(x, y).find((element) => element.tagName === "FORM"), 400 | x, 401 | y, 402 | ); 403 | 404 | const fields = 405 | (await form.jsonValue()) !== undefined 406 | ? await form.$$("input:not([type=checkbox])") /* , select */ 407 | : []; 408 | 409 | const buf = new SmartBuffer(); 410 | buf.writeString("f"); 411 | const inputs = ( 412 | await Promise.all( 413 | fields.map(async (field) => { 414 | if ((await (await field.getProperty("offsetParent")).jsonValue()) === null) { 415 | return undefined; 416 | } 417 | let description = await (await field.getProperty("placeholder")).jsonValue(); 418 | if (description === undefined || description.length === 0) { 419 | description = await (await field.getProperty("name")).jsonValue(); 420 | } 421 | if (description === undefined || description.length === 0) { 422 | return undefined; 423 | } 424 | const box = (await field.boxModel()).content; 425 | console.error(box); 426 | const boundingBox = { 427 | x: Math.round(box[0].x), 428 | y: Math.round(box[0].y), 429 | width: Math.round(box[2].x - box[0].x), 430 | height: Math.round(box[2].y - box[0].y), 431 | }; 432 | console.error(boundingBox); 433 | return { 434 | boundingBox, 435 | description: description, 436 | id: await ( 437 | await page.evaluateHandle( 438 | async (element, ID_ATTRIBUTE) => { 439 | let id = element.getAttribute(ID_ATTRIBUTE); 440 | if (!id) { 441 | id = await window.uuid(); 442 | element.setAttribute(ID_ATTRIBUTE, id); 443 | } 444 | return id; 445 | }, 446 | field, 447 | ID_ATTRIBUTE, 448 | ) 449 | ).jsonValue(), 450 | }; 451 | }), 452 | ) 453 | ).filter((input) => input !== undefined); 454 | buf.writeInt32LE(inputs.length); 455 | inputs.forEach(({ boundingBox, description, id }) => { 456 | buf.writeInt32LE(boundingBox.x); 457 | buf.writeInt32LE(boundingBox.y); 458 | buf.writeInt32LE(boundingBox.width); 459 | buf.writeInt32LE(boundingBox.height); 460 | buf.writeStringPrependSize(description); 461 | buf.writeStringPrependSize(id); 462 | }); 463 | sendCommand(buf.toBuffer()); 464 | break; 465 | } 466 | case "t": { 467 | const id = payload.readString(payload.readInt32LE()); 468 | const text = payload.readString(payload.readInt32LE()); 469 | console.error(`Update Text: ${id} ${text}`); 470 | await page.evaluate( 471 | (id, text, ID_ATTRIBUTE) => { 472 | const element = document.querySelector(`[${ID_ATTRIBUTE}="${id}"]`); 473 | if (!element) { 474 | return; 475 | } 476 | if (element.tagName === "INPUT") { 477 | element.value = text; 478 | } else if (element.tagName === "SELECT") { 479 | const option = Array.from(element.options).find( 480 | (option) => option.innerText.toLocaleLowerCase() === text.toLocaleLowerCase(), 481 | ); 482 | if (option) { 483 | element.value = option.value; 484 | } 485 | } 486 | }, 487 | id, 488 | text.trim(), 489 | ID_ATTRIBUTE, 490 | ); 491 | break; 492 | } 493 | case "k": { 494 | // This command is necessary because the Squeak ProcessWrapper is unable to terminate processes. 495 | await terminate(); 496 | break; 497 | } 498 | case "l": { 499 | const url = payload.readString(); 500 | console.error(`Navigating to ${url}`); 501 | try { 502 | await page.goto(url); 503 | } catch (error) { 504 | console.error(error); 505 | } 506 | break; 507 | } 508 | case "e": { 509 | // TODO check read boundaries for each event type 510 | const eventType = payload.readUInt8(); 511 | console.error("Received event type", eventType); 512 | switch (eventType) { 513 | case 0: 514 | page.mouse.up({ button: "left" }); 515 | break; 516 | case 1: 517 | page.mouse.down({ button: "left" }); 518 | break; 519 | case 2: 520 | page.mouse.up({ button: "right" }); 521 | break; 522 | case 3: 523 | page.mouse.down({ button: "right" }); 524 | break; 525 | case 4: { 526 | const x = payload.readUInt32LE(); 527 | const y = payload.readUInt32LE(); 528 | console.error("Moving to", x, y); 529 | page.mouse.move(x, y); 530 | break; 531 | } 532 | case 5: { 533 | let squeakKeyString = payload.readString(size, "ascii"); 534 | 535 | let keyName; 536 | const modifiers = []; 537 | if (squeakKeyString.startsWith(" when pressing CTRL+A on Windows 539 | squeakKeyString = squeakKeyString.replace("Cmd-", ""); 540 | } 541 | if (squeakKeyString.startsWith("" 554 | ) { 555 | squeakKeyString = squeakKeyString[1]; 556 | } 557 | 558 | if (squeakKeyString.length > 1) { 559 | const conversion = { 560 | "": "Space", 561 | "": "Tab", 562 | "": "Enter", 563 | "": "Enter", 564 | "": "Enter", 565 | "": "Backspace", 566 | "": "Delete", 567 | "": "Escape", 568 | "": "ArrowDown", 569 | "": "ArrowUp", 570 | "": "ArrowLeft", 571 | "": "ArrowRight", 572 | "": "End", 573 | "": "Home", 574 | "": "PageDown", 575 | "": "PageUp", 576 | // '': '', // TODO 577 | "": "Insert", 578 | }; 579 | keyName = conversion[squeakKeyString]; 580 | if (keyName === undefined) { 581 | console.error("Unknown key", squeakKeyString); 582 | break; 583 | } 584 | } else { 585 | keyName = squeakKeyString; 586 | } 587 | console.error("Typing", squeakKeyString, modifiers); 588 | try { 589 | await Promise.all(modifiers.map((key) => page.keyboard.down(key))); 590 | await page.keyboard.press(keyName); 591 | await Promise.all(modifiers.map((key) => page.keyboard.up(key))); 592 | } catch (error) { 593 | console.error(error); 594 | } 595 | break; 596 | } 597 | case 6: { 598 | const x = payload.readUInt32LE(); 599 | const y = payload.readUInt32LE(); 600 | page.setViewport({ width: x, height: y }); 601 | break; 602 | } 603 | case 7: { 604 | const y = payload.readUInt32LE(); 605 | console.error("scroll", y); 606 | page.evaluate((y) => window.scrollBy(0, y == 0 ? -20 : 20), y); 607 | break; 608 | } 609 | case 8: { 610 | // These are relative to the visible part of the page, not the top left corner 611 | const x = payload.readUInt32LE(); 612 | const y = payload.readUInt32LE(); 613 | console.error(`Portal event at ${x},${y}`); 614 | 615 | let elements; 616 | try { 617 | elements = await page.evaluate(getElements, "extractElements", x, y); 618 | } catch (error) { 619 | elements = []; 620 | console.error(error); 621 | } 622 | if (elements.length === 0) { 623 | console.error("No elements found to create a portal"); 624 | break; 625 | } 626 | const element = elements[0]; 627 | console.error({ 628 | id: element.id, 629 | type: element.type, 630 | x: element.x, 631 | y: element.y, 632 | w: element.w, 633 | h: element.h, 634 | }); 635 | 636 | sendPortalDataCommand(element); 637 | break; 638 | } 639 | case 9: { 640 | console.error("go back"); 641 | await page.goBack(); 642 | break; 643 | } 644 | case 10: { 645 | console.error("go forward"); 646 | await page.goForward(); 647 | break; 648 | } 649 | } 650 | break; 651 | } 652 | } 653 | } 654 | }; 655 | 656 | // We catch all errors, log them to the console and exit. 657 | run().catch((error) => console.error(error)); 658 | --------------------------------------------------------------------------------