├── .terserrc.js ├── .gitattributes ├── .npmrc ├── .prettierignore ├── src ├── types.d.ts ├── button.ts ├── tsconfig.json ├── themes.ts ├── model.ts ├── config.ts ├── element.ts └── terminal.ts ├── .eslintrc.js ├── .gitignore ├── .github ├── renovate.json └── workflows │ └── CI.yml ├── spec ├── custom-runner.js ├── config-spec.js ├── terminal-spec.js ├── model-spec.js ├── themes-spec.js └── element-spec.js ├── LICENSE ├── keymaps └── terminal.json ├── styles └── terminal.less ├── dist ├── xterm-addon-web-links.3abc7089.js └── xterm-addon-web-links.3abc7089.js.map ├── package.json ├── menus └── terminal.json ├── README.md └── CHANGELOG.md /.terserrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("terser-config-atomic") 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | # don't diff machine generated files 4 | pnpm-lock.yaml 5 | dist/ -diff 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=* 2 | package-lock=false 3 | lockfile=true 4 | prefer-frozen-lockfile=false 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | pnpm-lock.yaml 4 | changelog.md 5 | coverage 6 | build 7 | dist 8 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "electron" { 2 | export const shell: { 3 | openExternal(url: string): void 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "eslint-config-atomic", 3 | ignorePatterns: ["dist/", "node_modules/"], 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS metadata 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # Node 6 | node_modules 7 | 8 | # TypeScript 9 | *.tsbuildinfo 10 | 11 | # Build directories 12 | dist 13 | 14 | # linter 15 | .parcel-cache 16 | pnpm-lock.yaml 17 | -------------------------------------------------------------------------------- /src/button.ts: -------------------------------------------------------------------------------- 1 | import { ToolBarManager, getToolBarManager } from "atom/tool-bar" 2 | 3 | let toolbar: ToolBarManager | undefined = undefined 4 | 5 | export function consumeToolBar(getToolBar: getToolBarManager) { 6 | toolbar = getToolBar("atomic-terminal") // getting toolbar object 7 | if (atom.config.get("atomic-terminal.toolbarButton")) { 8 | addToolbarButton() 9 | } 10 | } 11 | 12 | export function addToolbarButton() { 13 | toolbar?.addButton({ 14 | icon: "terminal", 15 | tooltip: "Open Terminal", 16 | callback: "atomic-terminal:open", 17 | }) 18 | } 19 | 20 | export function removeToolbarButton() { 21 | toolbar?.removeItems() 22 | } 23 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "schedule": ["every weekend"], 3 | "labels": ["dependencies"], 4 | "separateMajorMinor": "false", 5 | "packageRules": [ 6 | { 7 | "matchDepTypes": ["devDependencies"], 8 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest", "lockFileMaintenance", "rollback", "bump"], 9 | "groupName": "devDependencies", 10 | "semanticCommitType": "chore", 11 | "automerge": true 12 | }, 13 | { 14 | "matchDepTypes": ["dependencies"], 15 | "matchUpdateTypes": ["major", "minor", "patch", "pin", "digest", "lockFileMaintenance", "rollback", "bump"], 16 | "groupName": "dependencies", 17 | "semanticCommitType": "fix" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "strictNullChecks": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "noImplicitReturns": true, 8 | "noImplicitAny": true, 9 | "noImplicitThis": true, 10 | "noFallthroughCasesInSwitch": true, 11 | 12 | "downlevelIteration": true, 13 | "declaration": true, 14 | "emitDecoratorMetadata": true, 15 | "experimentalDecorators": true, 16 | "incremental": true, 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | "preserveSymlinks": true, 20 | "removeComments": true, 21 | 22 | "lib": ["ES2018", "dom"], 23 | "target": "ES2018", 24 | 25 | "esModuleInterop": true, 26 | "module": "commonjs", 27 | "moduleResolution": "node", 28 | "importHelpers": false, 29 | "outDir": "../dist", 30 | "useUnknownInCatchVariables": false 31 | }, 32 | "compileOnSave": false 33 | } 34 | -------------------------------------------------------------------------------- /spec/custom-runner.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { execSync } from "child_process" 4 | import { createRunner } from "atom-jasmine3-test-runner" 5 | 6 | function setDefaultSettings(namespace, settings) { 7 | for (const name in settings) { 8 | const setting = settings[name] 9 | if (setting.type === "object") { 10 | setDefaultSettings(`${namespace}.${name}`, setting.properties) 11 | } else { 12 | atom.config.set(`${namespace}.${name}`, setting.default) 13 | } 14 | } 15 | } 16 | 17 | execSync("npm run build.tsc", { cwd: __dirname }) 18 | 19 | module.exports = createRunner( 20 | { 21 | specHelper: { 22 | attachToDom: true, 23 | customMatchers: true, 24 | }, 25 | }, 26 | () => { 27 | // eslint-disable-next-line no-console 28 | const warn = console.warn.bind(console) 29 | beforeEach(() => { 30 | const { config } = require("../dist/config") 31 | setDefaultSettings("atomic-terminal", config) 32 | spyOn(console, "warn").and.callFake((...args) => { 33 | if (args[0].includes("not attached to the DOM")) { 34 | return 35 | } 36 | warn(...args) 37 | }) 38 | }) 39 | } 40 | ) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | atomic-terminal 3 | Copyright (c) 2020 atom-community. All Rights Reserved. 4 | 5 | x-terminal 6 | Copyright (c) 2020 UziTech All Rights Reserved. 7 | Copyright (c) 2020 bus-stop All Rights Reserved. 8 | 9 | atom-xterm 10 | Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 11 | Copyright 2017-2018 Andres Mejia . All Rights Reserved. 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 14 | software and associated documentation files (the "Software"), to deal in the Software 15 | without restriction, including without limitation the rights to use, copy, modify, 16 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 20 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 21 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | Test: 10 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 11 | name: ${{ matrix.os }} - Atom ${{ matrix.channel }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | channel: [stable, beta] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v2 21 | - name: Install Atom 22 | uses: UziTech/action-setup-atom@v1 23 | with: 24 | channel: ${{ matrix.channel }} 25 | 26 | - name: Cache 27 | uses: actions/cache@v2 28 | with: 29 | path: | 30 | ./node_modules/ 31 | ~/.npm 32 | ~/AppData/Roaming/npm-cache 33 | key: "cache--OS:${{ matrix.os }}-Atom:${{ matrix.channel }}-${{ hashFiles('./package.json') }}" 34 | 35 | - name: Install Dependencies 36 | run: | 37 | apm install --production 38 | npm install --only=dev 39 | 40 | - name: Run Tests 👩🏾‍💻 41 | run: npm run test 42 | 43 | - name: Run Integration Tests 👩🏾‍💻 44 | run: npm run integration.test 45 | 46 | Lint: 47 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 48 | runs-on: ubuntu-latest 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | steps: 52 | - uses: actions/checkout@v2 53 | with: 54 | fetch-depth: 0 55 | - name: Commit lint ✨ 56 | uses: wagoid/commitlint-github-action@v4 57 | 58 | - name: Cache 59 | uses: actions/cache@v2 60 | with: 61 | path: | 62 | ./node_modules/ 63 | ~/.npm 64 | ~/AppData/Roaming/npm-cache 65 | key: "cache--OS:${{ matrix.os }}-${{ hashFiles('./package.json') }}" 66 | 67 | - name: Install dependencies 68 | run: npm install 69 | 70 | - name: Format ✨ 71 | run: npm run test.format 72 | 73 | - name: Lint ✨ 74 | run: npm run test.lint 75 | 76 | Release: 77 | needs: [Test, Lint] 78 | if: | 79 | github.ref == 'refs/heads/master' && 80 | github.event.repository.fork == false 81 | runs-on: ubuntu-latest 82 | steps: 83 | - name: Checkout Code 84 | uses: actions/checkout@v2 85 | - name: Install Atom 86 | uses: UziTech/action-setup-atom@v1 87 | - name: Install Node 88 | uses: dcodeIO/setup-node-nvm@master 89 | with: 90 | node-version: 12.14.1 91 | - name: Install Dependencies 92 | run: npm install 93 | - name: Build and Commit 94 | run: npm run build-commit 95 | - name: Release 🎉 96 | uses: cycjimmy/semantic-release-action@v2 97 | with: 98 | extends: "@semantic-release/apm-config" 99 | env: 100 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 101 | ATOM_ACCESS_TOKEN: ${{ secrets.ATOM_ACCESS_TOKEN }} 102 | -------------------------------------------------------------------------------- /keymaps/terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | ".platform-linux atom-workspace": { 3 | "ctrl-`": "atomic-terminal:open", 4 | "ctrl-~": "atomic-terminal:open", 5 | "ctrl-shift-t": "atomic-terminal:open", 6 | "ctrl-alt-shift-t": "atomic-terminal:open", 7 | "ctrl-alt-shift-up": "atomic-terminal:open-up", 8 | "ctrl-alt-shift-down": "atomic-terminal:open-down", 9 | "ctrl-alt-shift-left": "atomic-terminal:open-left", 10 | "ctrl-alt-shift-right": "atomic-terminal:open-right", 11 | "ctrl-alt-shift-i": "atomic-terminal:open-bottom-dock", 12 | "ctrl-alt-shift-u": "atomic-terminal:open-left-dock", 13 | "ctrl-alt-shift-o": "atomic-terminal:open-right-dock", 14 | "ctrl-alt-shift-f": "atomic-terminal:focus" 15 | }, 16 | ".platform-win32 atom-workspace": { 17 | "ctrl-`": "atomic-terminal:open", 18 | "ctrl-~": "atomic-terminal:open", 19 | "ctrl-shift-t": "atomic-terminal:open", 20 | "ctrl-alt-shift-t": "atomic-terminal:open", 21 | "ctrl-alt-shift-up": "atomic-terminal:open-up", 22 | "ctrl-alt-shift-down": "atomic-terminal:open-down", 23 | "ctrl-alt-shift-left": "atomic-terminal:open-left", 24 | "ctrl-alt-shift-right": "atomic-terminal:open-right", 25 | "ctrl-alt-shift-i": "atomic-terminal:open-bottom-dock", 26 | "ctrl-alt-shift-u": "atomic-terminal:open-left-dock", 27 | "ctrl-alt-shift-o": "atomic-terminal:open-right-dock", 28 | "ctrl-alt-shift-f": "atomic-terminal:focus" 29 | }, 30 | ".platform-darwin atom-workspace": { 31 | "cmd-`": "atomic-terminal:open", 32 | "cmd-~": "atomic-terminal:open", 33 | "cmd-t": "atomic-terminal:open", 34 | "cmd-alt-shift-t": "atomic-terminal:open", 35 | "cmd-alt-shift-up": "atomic-terminal:open-up", 36 | "cmd-alt-shift-down": "atomic-terminal:open-down", 37 | "cmd-alt-shift-left": "atomic-terminal:open-left", 38 | "cmd-alt-shift-right": "atomic-terminal:open-right", 39 | "cmd-alt-shift-i": "atomic-terminal:open-bottom-dock", 40 | "cmd-alt-shift-u": "atomic-terminal:open-left-dock", 41 | "cmd-alt-shift-o": "atomic-terminal:open-right-dock", 42 | "cmd-alt-shift-f": "atomic-terminal:focus" 43 | }, 44 | ".platform-linux atomic-terminal": { 45 | "ctrl-insert": "atomic-terminal:copy", 46 | "ctrl-shift-c": "atomic-terminal:copy", 47 | "shift-insert": "atomic-terminal:paste", 48 | "ctrl-shift-v": "atomic-terminal:paste", 49 | "ctrl-shift-u": "atomic-terminal:unfocus", 50 | "ctrl-l": "atomic-terminal:clear" 51 | }, 52 | ".platform-win32 atomic-terminal": { 53 | "ctrl-insert": "atomic-terminal:copy", 54 | "ctrl-shift-c": "atomic-terminal:copy", 55 | "shift-insert": "atomic-terminal:paste", 56 | "ctrl-shift-v": "atomic-terminal:paste", 57 | "ctrl-shift-u": "atomic-terminal:unfocus", 58 | "ctrl-l": "atomic-terminal:clear" 59 | }, 60 | ".platform-darwin atomic-terminal": { 61 | "ctrl-shift-c": "atomic-terminal:copy", 62 | "ctrl-insert": "atomic-terminal:copy", 63 | "ctrl-shift-v": "atomic-terminal:paste", 64 | "shift-insert": "atomic-terminal:paste", 65 | "cmd-c": "atomic-terminal:copy", 66 | "cmd-v": "atomic-terminal:paste", 67 | "ctrl-shift-u": "atomic-terminal:unfocus", 68 | "ctrl-l": "atomic-terminal:clear" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /spec/config-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { getDefaultShell, setAutoShell } from "../dist/config" 4 | 5 | describe("config", () => { 6 | describe("getDefaultShell()", () => { 7 | const savedPlatform = process.platform 8 | let savedEnv 9 | 10 | beforeEach(() => { 11 | savedEnv = JSON.parse(JSON.stringify(process.env)) 12 | }) 13 | 14 | afterEach(() => { 15 | process.env = savedEnv 16 | Object.defineProperty(process, "platform", { 17 | value: savedPlatform, 18 | }) 19 | }) 20 | 21 | it("on win32 without COMSPEC set", () => { 22 | Object.defineProperty(process, "platform", { 23 | value: "win32", 24 | }) 25 | if (process.env.COMSPEC) { 26 | delete process.env.COMSPEC 27 | } 28 | expect(getDefaultShell()).toBe("cmd.exe") 29 | }) 30 | 31 | it("on win32 with COMSPEC set", () => { 32 | Object.defineProperty(process, "platform", { 33 | value: "win32", 34 | }) 35 | const expected = "somecommand.exe" 36 | process.env.COMSPEC = expected 37 | expect(getDefaultShell()).toBe(expected) 38 | }) 39 | 40 | it("on linux without SHELL set", () => { 41 | Object.defineProperty(process, "platform", { 42 | value: "linux", 43 | }) 44 | if (process.env.SHELL) { 45 | delete process.env.SHELL 46 | } 47 | expect(getDefaultShell()).toBe("/bin/sh") 48 | }) 49 | 50 | it("on linux with SHELL set", () => { 51 | Object.defineProperty(process, "platform", { 52 | value: "linux", 53 | }) 54 | const expected = "somecommand" 55 | process.env.SHELL = expected 56 | expect(getDefaultShell()).toBe(expected) 57 | }) 58 | }) 59 | 60 | describe("setAutoShell()", () => { 61 | const savedPlatform = process.platform 62 | 63 | beforeEach(() => { 64 | Object.defineProperty(process, "platform", { 65 | value: "win32", 66 | }) 67 | atom.config.set("atomic-terminal.shell", getDefaultShell()) 68 | }) 69 | 70 | afterEach(() => { 71 | Object.defineProperty(process, "platform", { 72 | value: savedPlatform, 73 | }) 74 | }) 75 | 76 | it("should set terminal.shell to pwsh", async () => { 77 | const shell = "path/to/pwsh.exe" 78 | await setAutoShell(async (file) => { 79 | if (file === "pwsh.exe") { 80 | return shell 81 | } 82 | throw new Error("ENOENT") 83 | }) 84 | 85 | expect(atom.config.get("atomic-terminal.shell")).toBe(shell) 86 | }) 87 | 88 | it("should set terminal.shell to powershell", async () => { 89 | const shell = "path/to/powershell.exe" 90 | await setAutoShell(async (file) => { 91 | if (file === "powershell.exe") { 92 | return shell 93 | } 94 | throw new Error("ENOENT") 95 | }) 96 | 97 | expect(atom.config.get("atomic-terminal.shell")).toBe(shell) 98 | }) 99 | 100 | it("should set terminal.shell to powershell", async () => { 101 | const shell = getDefaultShell() 102 | await setAutoShell(async () => { 103 | throw new Error("ENOENT") 104 | }) 105 | 106 | expect(atom.config.get("atomic-terminal.shell")).toBe(shell) 107 | }) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /styles/terminal.less: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 4 | * Copyright (c) 2020 UziTech All Rights Reserved. 5 | * Copyright (c) 2020 bus-stop All Rights Reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 8 | * software and associated documentation files (the "Software"), to deal in the Software 9 | * without restriction, including without limitation the rights to use, copy, modify, 10 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 11 | * permit persons to whom the Software is furnished to do so. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 14 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 15 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 16 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 18 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | */ 20 | 21 | @app-background-color: #000000; 22 | @text-color: #ffffff; 23 | @background-color-selected: #4d4d4d; 24 | @text-color-highlight: #ffffff; 25 | @terminal-color-black: #2e3436; 26 | @terminal-color-red: #cc0000; 27 | @terminal-color-green: #4e9a06; 28 | @terminal-color-yellow: #c4a000; 29 | @terminal-color-blue: #3465a4; 30 | @terminal-color-magenta: #75507b; 31 | @terminal-color-cyan: #06989a; 32 | @terminal-color-white: #d3d7cf; 33 | @terminal-color-bright-black: #555753; 34 | @terminal-color-bright-red: #ef2929; 35 | @terminal-color-bright-green: #8ae234; 36 | @terminal-color-bright-yellow: #fce94f; 37 | @terminal-color-bright-blue: #729fcf; 38 | @terminal-color-bright-magenta: #ad7fa8; 39 | @terminal-color-bright-cyan: #34e2e2; 40 | @terminal-color-bright-white: #eeeeec; 41 | 42 | @import "ui-variables"; 43 | @import "syntax-variables"; 44 | 45 | :root { 46 | --standard-app-background-color: @app-background-color; 47 | --standard-text-color: @text-color; 48 | --standard-background-color-selected: @background-color-selected; 49 | --standard-text-color-highlight: @text-color-highlight; 50 | 51 | --standard-color-black: @terminal-color-black; 52 | --standard-color-red: @terminal-color-red; 53 | --standard-color-green: @terminal-color-green; 54 | --standard-color-yellow: @terminal-color-yellow; 55 | --standard-color-blue: @terminal-color-blue; 56 | --standard-color-magenta: @terminal-color-magenta; 57 | --standard-color-cyan: @terminal-color-cyan; 58 | --standard-color-white: @terminal-color-white; 59 | --standard-color-bright-black: @terminal-color-bright-black; 60 | --standard-color-bright-red: @terminal-color-bright-red; 61 | --standard-color-bright-green: @terminal-color-bright-green; 62 | --standard-color-bright-yellow: @terminal-color-bright-yellow; 63 | --standard-color-bright-blue: @terminal-color-bright-blue; 64 | --standard-color-bright-magenta: @terminal-color-bright-magenta; 65 | --standard-color-bright-cyan: @terminal-color-bright-cyan; 66 | --standard-color-bright-white: @terminal-color-bright-white; 67 | } 68 | 69 | // Load the static styles here. 70 | @import (less) "../node_modules/xterm/css/xterm.css"; 71 | 72 | atomic-terminal { 73 | .terminal.xterm { 74 | padding-left: @component-padding; 75 | padding-top: @component-padding; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /dist/xterm-addon-web-links.3abc7089.js: -------------------------------------------------------------------------------- 1 | ("undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{}).parcelRequire66c3.register("cY6Ka",(function(e,t){var r,i;r=window,i=function(){return(e=>{function t(i){if(r[i])return r[i].exports;var n=r[i]={i:i,l:!1,exports:{}};return e[i].call(n.exports,n,n.exports,t),n.l=!0,n.exports}var r={};return t.m=e,t.c=r,t.d=(e,r,i)=>{t.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:i})},t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=(e,r)=>{if(1&r&&(e=t(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(t.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)t.d(i,n,(t=>e[t]).bind(null,n));return i},t.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return t.d(r,"a",r),r},t.o=(e,t)=>({}.hasOwnProperty.call(e,t)),t.p="",t(t.s=0)})([function(e,t,r){"use strict";function i(e,t){var r=window.open();r?(r.opener=null,r.location.href=t):console.warn("Opening link blocked as opener could not be cleared")}Object.defineProperty(t,"__esModule",{value:!0}),t.WebLinksAddon=void 0;var n=r(1),o=RegExp("(?:^|[^\\da-z\\.-]+)((https?:\\/\\/)((([\\da-z\\.-]+)\\.([a-z\\.]{2,6}))|((\\d{1,3}\\.){3}\\d{1,3})|(localhost))(:\\d{1,5})?((\\/[\\/\\w\\.\\-%~:+@]*)*([^:\"'\\s]))?(\\?[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&'*+,:;~\\=\\.\\-]*)?(#[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&'*+,:;~\\=\\.\\-]*)?)($|[^\\/\\w\\.\\-%]+)"),a=function(){function e(e,t,r){void 0===e&&(e=i),void 0===t&&(t={}),void 0===r&&(r=!1),this._handler=e,this._options=t,this._useLinkProvider=r,this._options.matchIndex=1}return e.prototype.activate=function(e){this._terminal=e,this._useLinkProvider&&"registerLinkProvider"in this._terminal?this._linkProvider=this._terminal.registerLinkProvider(new n.WebLinkProvider(this._terminal,o,this._handler)):this._linkMatcherId=this._terminal.registerLinkMatcher(o,this._handler,this._options)},e.prototype.dispose=function(){var e;void 0!==this._linkMatcherId&&void 0!==this._terminal&&this._terminal.deregisterLinkMatcher(this._linkMatcherId),null===(e=this._linkProvider)||void 0===e||e.dispose()},e}();t.WebLinksAddon=a},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0;var i=function(){function e(e,t,r){this._terminal=e,this._regex=t,this._handler=r}return e.prototype.provideLinks=function(e,t){t(n.computeLink(e,this._regex,this._terminal,this._handler))},e}();t.WebLinkProvider=i;var n=(()=>{function e(){}return e.computeLink=(t,r,i,n)=>{for(var o,a=RegExp(r.source,(r.flags||"")+"g"),d=e._translateBufferLineToStringWithWrap(t-1,!1,i),s=d[0],l=d[1],u=-1,f=[];null!==(o=a.exec(s));){var c=o[1];if(!c){console.log("match found without corresponding matchIndex");break}if(u=s.indexOf(c,u+1),a.lastIndex=u+c.length,0>u)break;for(var p=u+c.length,h=l+1;p>i.cols;)p-=i.cols,h++;f.push({range:{start:{x:u+1,y:l+1},end:{x:p,y:h}},text:c,activate:n})}return f},e._translateBufferLineToStringWithWrap=(e,t,r)=>{var i,n,o="";do{if(!(d=r.buffer.active.getLine(e)))break;d.isWrapped&&e--,n=d.isWrapped}while(n);var a=e;do{var d,s=r.buffer.active.getLine(e+1);if(i=!!s&&s.isWrapped,!(d=r.buffer.active.getLine(e)))break;o+=d.translateToString(!i&&t).substring(0,r.cols),e++}while(i);return[o,a]},e})();t.LinkComputer=n}])},"object"==typeof e.exports?e.exports=i():"function"==typeof define&&define.amd?define([],i):"object"==typeof e.exports?e.exports.WebLinksAddon=i():r.WebLinksAddon=i()})); 2 | //# sourceMappingURL=xterm-addon-web-links.3abc7089.js.map 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atomic-terminal", 3 | "title": "Atom Terminal", 4 | "main": "./dist/terminal.js", 5 | "repository": "https://github.com/atom-community/terminal", 6 | "bugs": { 7 | "url": "https://github.com/atom-community/terminal/issues/new/choose" 8 | }, 9 | "version": "1.1.8", 10 | "description": "The xterm based terminal for Atom", 11 | "keywords": [ 12 | "terminal", 13 | "xterm", 14 | "term", 15 | "console", 16 | "shell", 17 | "emulator", 18 | "pty", 19 | "tty", 20 | "comspec", 21 | "command-line", 22 | "bash", 23 | "sh", 24 | "powershell", 25 | "cmd" 26 | ], 27 | "activationHooks": [ 28 | "core:loaded-shell-environment" 29 | ], 30 | "atomTestRunner": "./spec/custom-runner", 31 | "license": "MIT", 32 | "engines": { 33 | "atom": ">=1.52 <2.0.0", 34 | "electron": ">=6.x" 35 | }, 36 | "providedServices": { 37 | "platformioIDETerminal": { 38 | "description": "Run commands and open terminals.", 39 | "versions": { 40 | "1.1.0": "providePlatformIOIDEService" 41 | } 42 | }, 43 | "terminal": { 44 | "description": "Change the terminal.", 45 | "versions": { 46 | "1.0.0": "provideTerminalService" 47 | } 48 | } 49 | }, 50 | "consumedServices": { 51 | "tool-bar": { 52 | "versions": { 53 | "^0 || ^1": "consumeToolBar" 54 | } 55 | } 56 | }, 57 | "dependencies": { 58 | "fs-extra": "^10.0.0", 59 | "node-pty-prebuilt-multiarch": "^0.10.0", 60 | "uuid": "^8.3.2", 61 | "which": "^2.0.2", 62 | "xterm": "4.13.0", 63 | "xterm-addon-fit": "0.5.0", 64 | "xterm-addon-ligatures": "^0.5.1", 65 | "xterm-addon-web-links": "0.4.0", 66 | "xterm-addon-webgl": "0.11.1" 67 | }, 68 | "devDependencies": { 69 | "@types/atom": "1.40.11", 70 | "@types/fs-extra": "^9.0.12", 71 | "@types/jasmine": "^3.8.1", 72 | "@types/node": "^16.3.3", 73 | "@types/resize-observer-browser": "^0.1.6", 74 | "@types/uuid": "^8.3.1", 75 | "@types/which": "^2.0.1", 76 | "atom-jasmine3-test-runner": "^5.2.7", 77 | "build-commit": "0.1.4", 78 | "cross-env": "7.0.3", 79 | "eslint-config-atomic": "^1.16.2", 80 | "parcel": "2.0.0-rc.0", 81 | "prettier-config-atomic": "^2.0.5", 82 | "shx": "0.3.3", 83 | "temp": "^0.9.4", 84 | "terser-config-atomic": "^0.1.1", 85 | "typescript": "^4.3.5" 86 | }, 87 | "scripts": { 88 | "format": "prettier --write .", 89 | "test.format": "prettier . --check", 90 | "lint": "eslint . --fix", 91 | "test.lint": "eslint .", 92 | "test": "atom --test spec", 93 | "integration.test": "npm run build && atom --test ./spec/terminal-spec.js", 94 | "clean": "shx rm -rf dist ./.parcel-cache", 95 | "build.tsc": "tsc -p src/tsconfig.json", 96 | "dev": "tsc --watch -p src/tsconfig.json", 97 | "build": "cross-env NODE_ENV=production parcel build --target main ./src/terminal.ts --no-cache", 98 | "build-commit": "npm run clean && build-commit -o dist", 99 | "prepare": "npm run build" 100 | }, 101 | "prettier": "prettier-config-atomic", 102 | "deserializers": { 103 | "TerminalModel": "deserializeTerminalModel" 104 | }, 105 | "targets": { 106 | "main": { 107 | "context": "electron-renderer", 108 | "includeNodeModules": { 109 | "atom": false, 110 | "electron": false, 111 | "node-pty-prebuilt-multiarch": false 112 | }, 113 | "isLibrary": true, 114 | "optimize": true 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /menus/terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "context-menu": { 3 | "atomic-terminal": [ 4 | { 5 | "label": "Open New Terminal", 6 | "command": "atomic-terminal:open" 7 | }, 8 | { 9 | "label": "Close", 10 | "command": "atomic-terminal:close" 11 | }, 12 | { 13 | "label": "Restart", 14 | "command": "atomic-terminal:restart" 15 | }, 16 | { 17 | "label": "Clear", 18 | "command": "atomic-terminal:clear" 19 | }, 20 | { 21 | "type": "separator" 22 | }, 23 | { 24 | "label": "Copy", 25 | "command": "atomic-terminal:copy" 26 | }, 27 | { 28 | "label": "Paste", 29 | "command": "atomic-terminal:paste" 30 | }, 31 | { 32 | "type": "separator" 33 | }, 34 | { 35 | "label": "Close All Terminals", 36 | "command": "atomic-terminal:close-all" 37 | } 38 | ], 39 | "atom-text-editor, .tree-view, .tab-bar": [ 40 | { 41 | "label": "Terminal", 42 | "submenu": [ 43 | { 44 | "label": "Open New Terminal", 45 | "command": "atomic-terminal:open-context-menu" 46 | }, 47 | { 48 | "label": "Open New Terminal (Split Up)", 49 | "command": "atomic-terminal:open-up-context-menu" 50 | }, 51 | { 52 | "label": "Open New Terminal (Split Down)", 53 | "command": "atomic-terminal:open-down-context-menu" 54 | }, 55 | { 56 | "label": "Open New Terminal (Split Left)", 57 | "command": "atomic-terminal:open-left-context-menu" 58 | }, 59 | { 60 | "label": "Open New Terminal (Split Right)", 61 | "command": "atomic-terminal:open-right-context-menu" 62 | }, 63 | { 64 | "label": "Open New Terminal (Bottom Dock)", 65 | "command": "atomic-terminal:open-bottom-dock-context-menu" 66 | }, 67 | { 68 | "label": "Open New Terminal (Left Dock)", 69 | "command": "atomic-terminal:open-left-dock-context-menu" 70 | }, 71 | { 72 | "label": "Open New Terminal (Right Dock)", 73 | "command": "atomic-terminal:open-right-dock-context-menu" 74 | }, 75 | { 76 | "label": "Close All Terminals", 77 | "command": "atomic-terminal:close-all" 78 | }, 79 | { 80 | "label": "Focus Last Terminal", 81 | "command": "atomic-terminal:focus" 82 | } 83 | ] 84 | } 85 | ] 86 | }, 87 | "menu": [ 88 | { 89 | "label": "Packages", 90 | "submenu": [ 91 | { 92 | "label": "Terminal", 93 | "submenu": [ 94 | { 95 | "label": "Open New Terminal", 96 | "command": "atomic-terminal:open" 97 | }, 98 | { 99 | "label": "Open New Terminal (Split Up)", 100 | "command": "atomic-terminal:open-up" 101 | }, 102 | { 103 | "label": "Open New Terminal (Split Down)", 104 | "command": "atomic-terminal:open-down" 105 | }, 106 | { 107 | "label": "Open New Terminal (Split Left)", 108 | "command": "atomic-terminal:open-left" 109 | }, 110 | { 111 | "label": "Open New Terminal (Split Right)", 112 | "command": "atomic-terminal:open-right" 113 | }, 114 | { 115 | "label": "Open New Terminal (Bottom Dock)", 116 | "command": "atomic-terminal:open-bottom-dock" 117 | }, 118 | { 119 | "label": "Open New Terminal (Left Dock)", 120 | "command": "atomic-terminal:open-left-dock" 121 | }, 122 | { 123 | "label": "Open New Terminal (Right Dock)", 124 | "command": "atomic-terminal:open-right-dock" 125 | }, 126 | { 127 | "label": "Close All Terminals", 128 | "command": "atomic-terminal:close-all" 129 | }, 130 | { 131 | "label": "Focus Last Terminal", 132 | "command": "atomic-terminal:focus" 133 | } 134 | ] 135 | } 136 | ] 137 | } 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /spec/terminal-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { getInstance } from "../dist/terminal" 4 | 5 | describe("terminal", () => { 6 | let terminal 7 | 8 | beforeEach(() => { 9 | terminal = getInstance() 10 | }) 11 | 12 | describe("unfocus()", () => { 13 | it("focuses atom-workspace", async () => { 14 | jasmine.attachToDOM(atom.views.getView(atom.workspace)) 15 | const model = await terminal.openInCenterOrDock(atom.workspace) 16 | await model.initializedPromise 17 | await model.element.createTerminal() 18 | 19 | expect(model.element).toHaveFocus() 20 | terminal.unfocus() 21 | expect(model.element).not.toHaveFocus() 22 | }) 23 | }) 24 | 25 | describe("runCommands()", () => { 26 | let activeTerminal, newTerminal, commands 27 | beforeEach(() => { 28 | activeTerminal = { 29 | element: { 30 | initializedPromise: Promise.resolve(), 31 | }, 32 | runCommand: jasmine.createSpy("activeTerminal.runCommand"), 33 | } 34 | newTerminal = { 35 | element: { 36 | initializedPromise: Promise.resolve(), 37 | }, 38 | runCommand: jasmine.createSpy("newTerminal.runCommand"), 39 | } 40 | commands = ["command 1", "command 2"] 41 | spyOn(terminal, "getActiveTerminal").and.returnValue(activeTerminal) 42 | spyOn(terminal, "open").and.returnValue(newTerminal) 43 | }) 44 | 45 | it("runs commands in new terminal", async () => { 46 | atom.config.set("atomic-terminal.runInActive", false) 47 | await terminal.runCommands(commands) 48 | 49 | expect(terminal.getActiveTerminal).not.toHaveBeenCalled() 50 | expect(newTerminal.runCommand).toHaveBeenCalledWith("command 1") 51 | expect(newTerminal.runCommand).toHaveBeenCalledWith("command 2") 52 | }) 53 | 54 | it("runs commands in active terminal", async () => { 55 | atom.config.set("atomic-terminal.runInActive", true) 56 | await terminal.runCommands(commands) 57 | 58 | expect(terminal.open).not.toHaveBeenCalled() 59 | expect(activeTerminal.runCommand).toHaveBeenCalledWith("command 1") 60 | expect(activeTerminal.runCommand).toHaveBeenCalledWith("command 2") 61 | }) 62 | 63 | it("runs commands in new terminal if none active", async () => { 64 | terminal.getActiveTerminal.and.returnValue() 65 | atom.config.set("atomic-terminal.runInActive", true) 66 | await terminal.runCommands(commands) 67 | 68 | expect(terminal.getActiveTerminal).toHaveBeenCalled() 69 | expect(newTerminal.runCommand).toHaveBeenCalledWith("command 1") 70 | expect(newTerminal.runCommand).toHaveBeenCalledWith("command 2") 71 | }) 72 | }) 73 | 74 | describe("performOnTerminal", () => { 75 | let activeTerminal 76 | beforeEach(() => { 77 | activeTerminal = { 78 | element: { 79 | initializedPromise: Promise.resolve(), 80 | }, 81 | exit: jasmine.createSpy("activeTerminal.exit"), 82 | restartPtyProcess: jasmine.createSpy("activeTerminal.restartPtyProcess"), 83 | copyFromTerminal: jasmine.createSpy("activeTerminal.copy").and.returnValue("copied"), 84 | pasteToTerminal: jasmine.createSpy("activeTerminal.paste"), 85 | clear: jasmine.createSpy("activeTerminal.clear"), 86 | } 87 | spyOn(terminal, "getActiveTerminal").and.returnValue(activeTerminal) 88 | }) 89 | 90 | describe("close()", () => { 91 | it("closes terminal", async () => { 92 | await terminal.close() 93 | 94 | expect(activeTerminal.exit).toHaveBeenCalled() 95 | }) 96 | }) 97 | 98 | describe("restart()", () => { 99 | it("restarts terminal", async () => { 100 | await terminal.restart() 101 | 102 | expect(activeTerminal.restartPtyProcess).toHaveBeenCalled() 103 | }) 104 | }) 105 | 106 | describe("copy()", () => { 107 | it("copys terminal", async () => { 108 | spyOn(atom.clipboard, "write") 109 | await terminal.copy() 110 | 111 | expect(atom.clipboard.write).toHaveBeenCalledWith("copied") 112 | }) 113 | }) 114 | 115 | describe("paste()", () => { 116 | it("pastes terminal", async () => { 117 | spyOn(atom.clipboard, "read").and.returnValue("copied") 118 | await terminal.paste() 119 | 120 | expect(activeTerminal.pasteToTerminal).toHaveBeenCalledWith("copied") 121 | }) 122 | }) 123 | 124 | describe("clear()", () => { 125 | it("clears terminal", async () => { 126 | await terminal.clear() 127 | 128 | expect(activeTerminal.clear).toHaveBeenCalled() 129 | }) 130 | }) 131 | }) 132 | describe("open()", () => { 133 | let uri 134 | beforeEach(() => { 135 | uri = terminal.generateNewUri() 136 | spyOn(atom.workspace, "open") 137 | }) 138 | 139 | it("simple", async () => { 140 | await terminal.open(uri) 141 | 142 | expect(atom.workspace.open).toHaveBeenCalledWith(uri, {}) 143 | }) 144 | 145 | it("target to cwd", async () => { 146 | const testPath = "/test/path" 147 | spyOn(terminal, "getPath").and.returnValue(testPath) 148 | await terminal.open(uri, { target: true }) 149 | 150 | const url = new URL(atom.workspace.open.calls.mostRecent().args[0]) 151 | 152 | expect(url.searchParams.get("cwd")).toBe(testPath) 153 | }) 154 | }) 155 | }) 156 | -------------------------------------------------------------------------------- /src/themes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Get xTerm.js theme based on settings 3 | * 4 | * @returns {object} XTerm.js theme object 5 | */ 6 | export function getTheme(): Record { 7 | const { theme, ...colors } = atom.config.get("atomic-terminal.colors") 8 | // themes modified from https://github.com/bus-stop/terminus/tree/master/styles/themes 9 | switch (theme) { 10 | case "Atom Dark": 11 | colors.background = "#1d1f21" 12 | colors.foreground = "#c5c8c6" 13 | colors.selection = "#999999" 14 | colors.cursor = "#ffffff" 15 | break 16 | case "Atom Light": 17 | colors.background = "#ffffff" 18 | colors.foreground = "#555555" 19 | colors.selection = "#afc4da" 20 | colors.cursor = "#000000" 21 | break 22 | case "Base16 Tomorrow Dark": 23 | colors.background = "#1d1f21" 24 | colors.foreground = "#c5c8c6" 25 | colors.selection = "#b4b7b4" 26 | // colors.selectionForeground = '#e0e0e0' 27 | colors.cursor = "#ffffff" 28 | break 29 | case "Base16 Tomorrow Light": 30 | colors.background = "#ffffff" 31 | colors.foreground = "#1d1f21" 32 | colors.selection = "#282a2e" 33 | // colors.selectionForeground = '#e0e0e0' 34 | colors.cursor = "#1d1f21" 35 | break 36 | case "Christmas": 37 | colors.background = "#0c0047" 38 | colors.foreground = "#f81705" 39 | colors.selection = "#298f16" 40 | colors.cursor = "#009f59" 41 | break 42 | case "City Lights": 43 | colors.background = "#181d23" 44 | colors.foreground = "#666d81" 45 | colors.selection = "#2a2f38" 46 | // colors.selectionForeground = '#b7c5d3' 47 | colors.cursor = "#528bff" 48 | break 49 | case "Dracula": 50 | colors.background = "#1e1f29" 51 | colors.foreground = "white" 52 | colors.selection = "#44475a" 53 | colors.cursor = "#999999" 54 | break 55 | case "Grass": 56 | colors.background = "rgb(19, 119, 61)" 57 | colors.foreground = "rgb(255, 240, 165)" 58 | colors.selection = "rgba(182, 73, 38, .99)" 59 | colors.cursor = "rgb(142, 40, 0)" 60 | break 61 | case "Homebrew": 62 | colors.background = "#000000" 63 | colors.foreground = "rgb(41, 254, 20)" 64 | colors.selection = "rgba(7, 30, 155, .99)" 65 | colors.cursor = "rgb(55, 254, 38)" 66 | break 67 | case "Inverse": 68 | colors.background = "#ffffff" 69 | colors.foreground = "#000000" 70 | colors.selection = "rgba(178, 215, 255, .99)" 71 | colors.cursor = "rgb(146, 146, 146)" 72 | break 73 | case "Linux": 74 | colors.background = "#000000" 75 | colors.foreground = "rgb(230, 230, 230)" 76 | colors.selection = "rgba(155, 30, 7, .99)" 77 | colors.cursor = "rgb(200, 20, 25)" 78 | break 79 | case "Man Page": 80 | colors.background = "rgb(254, 244, 156)" 81 | colors.foreground = "black" 82 | colors.selection = "rgba(178, 215, 255, .99)" 83 | colors.cursor = "rgb(146, 146, 146)" 84 | break 85 | case "Novel": 86 | colors.background = "rgb(223, 219, 196)" 87 | colors.foreground = "rgb(77, 47, 46)" 88 | colors.selection = "rgba(155, 153, 122, .99)" 89 | colors.cursor = "rgb(115, 99, 89)" 90 | break 91 | case "Ocean": 92 | colors.background = "rgb(44, 102, 201)" 93 | colors.foreground = "white" 94 | colors.selection = "rgba(41, 134, 255, .99)" 95 | colors.cursor = "rgb(146, 146, 146)" 96 | break 97 | case "One Dark": 98 | colors.background = "#282c34" 99 | colors.foreground = "#abb2bf" 100 | colors.selection = "#9196a1" 101 | colors.cursor = "#528bff" 102 | break 103 | case "One Light": 104 | colors.background = "hsl(230, 1%, 98%)" 105 | colors.foreground = "hsl(230, 8%, 24%)" 106 | colors.selection = "hsl(230, 1%, 90%)" 107 | colors.cursor = "hsl(230, 100%, 66%)" 108 | break 109 | case "Predawn": 110 | colors.background = "#282828" 111 | colors.foreground = "#f1f1f1" 112 | colors.selection = "rgba(255,255,255,0.25)" 113 | colors.cursor = "#f18260" 114 | break 115 | case "Pro": 116 | colors.background = "#000000" 117 | colors.foreground = "rgb(244, 244, 244)" 118 | colors.selection = "rgba(82, 82, 82, .99)" 119 | colors.cursor = "rgb(96, 96, 96)" 120 | break 121 | case "Red Sands": 122 | colors.background = "rgb(143, 53, 39)" 123 | colors.foreground = "rgb(215, 201, 167)" 124 | colors.selection = "rgba(60, 25, 22, .99)" 125 | colors.cursor = "white" 126 | break 127 | case "Red": 128 | colors.background = "#000000" 129 | colors.foreground = "rgb(255, 38, 14)" 130 | colors.selection = "rgba(7, 30, 155, .99)" 131 | colors.cursor = "rgb(255, 38, 14)" 132 | break 133 | case "Silver Aerogel": 134 | colors.background = "rgb(146, 146, 146)" 135 | colors.foreground = "#000000" 136 | colors.selection = "rgba(120, 123, 156, .99)" 137 | colors.cursor = "rgb(224, 224, 224)" 138 | break 139 | case "Solarized Dark": 140 | colors.background = "#042029" 141 | colors.foreground = "#708284" 142 | colors.selection = "#839496" 143 | colors.cursor = "#819090" 144 | break 145 | case "Solarized Light": 146 | colors.background = "#fdf6e3" 147 | colors.foreground = "#657a81" 148 | colors.selection = "#ece7d5" 149 | colors.cursor = "#586e75" 150 | break 151 | case "Solid Colors": 152 | colors.background = "rgb(120, 132, 151)" 153 | colors.foreground = "#000000" 154 | colors.selection = "rgba(178, 215, 255, .99)" 155 | colors.cursor = "#ffffff" 156 | break 157 | case "Standard": { 158 | const root = getComputedStyle(document.documentElement) 159 | colors.background = root.getPropertyValue("--standard-app-background-color") 160 | colors.foreground = root.getPropertyValue("--standard-text-color") 161 | colors.selection = root.getPropertyValue("--standard-background-color-selected") 162 | colors.cursor = root.getPropertyValue("--standard-text-color-highlight") 163 | 164 | colors.black = root.getPropertyValue("--standard-color-black") 165 | colors.red = root.getPropertyValue("--standard-color-red") 166 | colors.green = root.getPropertyValue("--standard-color-green") 167 | colors.yellow = root.getPropertyValue("--standard-color-yellow") 168 | colors.blue = root.getPropertyValue("--standard-color-blue") 169 | colors.magenta = root.getPropertyValue("--standard-color-magenta") 170 | colors.cyan = root.getPropertyValue("--standard-color-cyan") 171 | colors.white = root.getPropertyValue("--standard-color-white") 172 | colors.brightBlack = root.getPropertyValue("--standard-color-bright-black") 173 | colors.brightRed = root.getPropertyValue("--standard-color-bright-red") 174 | colors.brightGreen = root.getPropertyValue("--standard-color-bright-green") 175 | colors.brightYellow = root.getPropertyValue("--standard-color-bright-yellow") 176 | colors.brightBlue = root.getPropertyValue("--standard-color-bright-blue") 177 | colors.brightMagenta = root.getPropertyValue("--standard-color-bright-magenta") 178 | colors.brightCyan = root.getPropertyValue("--standard-color-bright-cyan") 179 | colors.brightWhite = root.getPropertyValue("--standard-color-bright-white") 180 | break 181 | } 182 | default: 183 | // do nothing 184 | } 185 | 186 | return colors 187 | } 188 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terminal 2 | 3 | ## Demo 4 | 5 | ![Terminal demo](https://cdn.statically.io/gh/bus-stop/x-terminal/master/resources/x-terminal-demo.gif) 6 | 7 | ## Theme variables 8 | 9 | The following theme variables are available to change the colors of the standard theme: 10 | 11 | ```less 12 | @app-background-color: #000000; 13 | @text-color: #ffffff; 14 | @background-color-selected: #4d4d4d; 15 | @text-color-highlight: #ffffff; 16 | @terminal-color-black: #2e3436; 17 | @terminal-color-red: #cc0000; 18 | @terminal-color-green: #4e9a06; 19 | @terminal-color-yellow: #c4a000; 20 | @terminal-color-blue: #3465a4; 21 | @terminal-color-magenta: #75507b; 22 | @terminal-color-cyan: #06989a; 23 | @terminal-color-white: #d3d7cf; 24 | @terminal-color-bright-black: #555753; 25 | @terminal-color-bright-red: #ef2929; 26 | @terminal-color-bright-green: #8ae234; 27 | @terminal-color-bright-yellow: #fce94f; 28 | @terminal-color-bright-blue: #729fcf; 29 | @terminal-color-bright-magenta: #ad7fa8; 30 | @terminal-color-bright-cyan: #34e2e2; 31 | @terminal-color-bright-white: #eeeeec; 32 | ``` 33 | 34 | ## Active Terminal 35 | 36 | The active terminal is the terminal that will be used when sending commands to 37 | the terminal. 38 | 39 | The active terminal will always have an astrix (`*`) in front of the title. 40 | By default when a terminal is hidden it becomes inactive and the last used 41 | visible terminal will become active. If there are no visible terminals none are 42 | active. 43 | 44 | The `Allow Hidden Terminal To Stay Active` setting will change the 45 | default behavior and keep a terminal that is hidden active until another 46 | terminal is focused. 47 | 48 | ## Services 49 | 50 | For plugin writers, the `terminal` package supports two services, `terminal` and `platformioIDETerminal`, which 51 | can be used to easily open terminals. These methods are provided using Atom's [services](http://flight-manual.atom.io/behind-atom/sections/interacting-with-other-packages-via-services/) 52 | API. 53 | 54 | To use a service, add a consumer method to consume the service, or 55 | rather a JavaScript object that provides methods to open terminals and run commands. 56 | 57 | ### 'terminal' service v1.0.0 58 | 59 | The `terminal` service provides an [object](https://github.com/atom-community/terminal/blob/29b0751250cb9262fb609db8cae87d87fb383c64/src/terminal.js#L291) with `updateProcessEnv`, `run`, `getTerminalViews`, and `open` methods. 60 | 61 | As an example on how to use the provided `run()` method, your 62 | `package.json` should have the following. 63 | 64 | ```json 65 | { 66 | "consumedServices": { 67 | "terminal": { 68 | "versions": { 69 | "^1.1.0": "consumePlatformioIDETerminalService" 70 | } 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | Your package's main module should then define a `consumePlatformioIDETerminalService` 77 | method, for example. 78 | 79 | ```js 80 | import { Disposable } from "atom" 81 | 82 | export default { 83 | terminalService: null, 84 | 85 | consumePlatformioIDETerminalService(terminalService) { 86 | this.terminalService = terminalService 87 | return new Disposable(() => { 88 | this.terminalService = null 89 | }) 90 | }, 91 | 92 | // . . . 93 | } 94 | ``` 95 | 96 | Once the service is consumed, use the `run()` method that is provided 97 | by the service, for example. 98 | 99 | ```js 100 | // Launch `somecommand --foo --bar --baz` in a terminal. 101 | this.terminalService.run(["somecommand --foo --bar --baz"]) 102 | ``` 103 | 104 | # Development 105 | 106 | Want to help develop terminal? Here's how to quickly get setup. 107 | 108 | First use the [apm](https://github.com/atom/apm) command to clone the 109 | [terminal repo](https://github.com/atom-community/terminal). 110 | 111 | ```sh 112 | apm develop terminal 113 | ``` 114 | 115 | This should clone the terminal package into the `$HOME/github/terminal` 116 | directory. Go into this directory and install its dependencies. 117 | 118 | ```sh 119 | cd $HOME/github/terminal 120 | npm install 121 | ``` 122 | 123 | You shouldn't need to rebuild any [node-pty](https://github.com/Tyriar/node-pty) 124 | since they are pre-compiled, however in the event they aren't available, 125 | you can rebuild them with: 126 | 127 | ```sh 128 | apm rebuild 129 | ``` 130 | 131 | Finally, open this directory in Atom's dev mode and hack away. 132 | 133 | ```sh 134 | atom --dev 135 | ``` 136 | 137 | There's a test suite available for automated testing of the terminal package. 138 | Simply go to `View > Developer > Run Package Specs` in Atom's main menu or 139 | use the hotkey. You can run the full test suite (which includes running lint 140 | tools) via command-line by running `npm run test` inside the terminal 141 | directory. 142 | 143 | Various lint tools are being used to keep the code "beautified". To run only 144 | the lint tools, simply run `npm run lint`. 145 | 146 | ## Pull Requests 147 | 148 | Whenever you're ready to submit a pull request, be sure to submit it 149 | against a fork of the main [terminal repo](https://github.com/atom-community/terminal) 150 | master branch that you'll own. Fork the repo using Github and make note of the 151 | new `git` URL. Set this new git URL as the URL for the `origin` remote in your 152 | already cloned git repo is follows. 153 | 154 | ```sh 155 | git remote set-url origin ${NEW_GIT_URL} 156 | ``` 157 | 158 | Ensure your new changes passes the test suite by running `npm run test`. 159 | Afterwards, push your changes to your repo and then use Github to submit a new 160 | pull request. 161 | 162 | ## [xterm.js](https://github.com/xtermjs/xterm.js) 163 | 164 | The terminals that users interact with in this package is made possible with 165 | major help from the [xterm.js](https://github.com/xtermjs/xterm.js) library. As 166 | such, often times it's necessary to make changes to xterm.js in order to fix 167 | some bug or implement new features. 168 | 169 | If you want to work on xterm.js for the benefit of a bug fix or feature to be 170 | supported in terminal, here's how you can quickly get setup. 171 | 172 | First make a fork of [xterm.js](https://github.com/xtermjs/xterm.js). Next, 173 | clone your newly created fork as follows. 174 | 175 | ```sh 176 | git clone ${YOUR_XTERMJS_FORK} ${HOME}/github/xterm.js 177 | ``` 178 | 179 | Go into your newly cloned repo for xterm.js. 180 | 181 | ```sh 182 | cd ${HOME}/github/xterm.js 183 | ``` 184 | 185 | Install all needed dependencies. 186 | 187 | ```sh 188 | npm install 189 | ``` 190 | 191 | Build xterm.js. 192 | 193 | ```sh 194 | npm run build 195 | ``` 196 | 197 | Ensure the test suite passes. 198 | 199 | ```sh 200 | npm run test 201 | npm run lint 202 | ``` 203 | 204 | Add a global link for xterm.js to your system. 205 | 206 | ```sh 207 | npm link 208 | ``` 209 | 210 | Inside your terminal directory, link against the global `xterm` link. 211 | 212 | ```sh 213 | cd ${HOME}/github/terminal 214 | npm link xterm 215 | ``` 216 | 217 | Finally, perform a rebuild with the [apm](https://github.com/atom/apm) program 218 | inside the terminal directory. 219 | 220 | ```sh 221 | apm rebuild 222 | ``` 223 | 224 | You're all set for developing xterm.js. Hack away in your xterm.js directory, 225 | run `npm run build`, then reload your Atom window to see the changes to your 226 | terminals. 227 | 228 | # Credits and Legal 229 | 230 | Click for copyright and license info about this package. 231 | 232 | [![LICENSE and © INFO](https://img.shields.io/badge/©%20&%20LICENSE-MIT-blue.svg?longCache=true&=flat-square)](LICENSE) 233 | 234 | # Feedback 235 | 236 | Need to submit a bug report? Have a new feature you want to see implemented in 237 | _terminal_? Please feel free to submit them through the appropriate 238 | [issue template](https://github.com/atom-community/terminal/issues/new/choose). 239 | 240 | For bug reports, please provide images or demos showing your issues if you can. 241 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | import { Emitter, Pane, Dock } from "atom" 2 | 3 | import * as fs from "fs-extra" 4 | import * as path from "path" 5 | import * as os from "os" 6 | 7 | import { AtomTerminal } from "./element" 8 | 9 | import { URL } from "url" 10 | 11 | const DEFAULT_TITLE = "Terminal" 12 | 13 | /** 14 | * The main terminal model, or rather item, displayed in the Atom workspace. 15 | * 16 | * @class 17 | */ 18 | export class TerminalModel { 19 | // TODO: maybe private? 20 | public uri: string 21 | public sessionId: string 22 | public title: string 23 | public modified: boolean 24 | public isInitialized: boolean 25 | public initializedPromise: Promise 26 | public fontSize: number 27 | public terminalsSet: Set 28 | public activeIndex: number 29 | public element?: AtomTerminal 30 | public pane?: Pane 31 | public dock?: Dock 32 | public emitter: Emitter 33 | public cwd?: string 34 | 35 | constructor({ uri, terminalsSet }: { uri: string; terminalsSet: Set }) { 36 | this.uri = uri 37 | const url = new URL(this.uri) 38 | this.sessionId = url.host 39 | this.cwd = url.searchParams.get("cwd") || undefined 40 | this.terminalsSet = terminalsSet 41 | this.activeIndex = this.terminalsSet.size 42 | this.title = DEFAULT_TITLE 43 | this.fontSize = atom.config.get("atomic-terminal.fontSize") 44 | this.modified = false 45 | this.emitter = new Emitter() 46 | this.terminalsSet.add(this) 47 | 48 | // Determine appropriate initial working directory based on previous 49 | // active item. Since this involves async operations on the file 50 | // system, a Promise will be used to indicate when initialization is 51 | // done. 52 | this.isInitialized = false 53 | this.initializedPromise = this.initialize().then(() => { 54 | this.isInitialized = true 55 | }) 56 | } 57 | 58 | async initialize() { 59 | this.cwd = await this.getInitialCwd() 60 | } 61 | 62 | async getInitialCwd(): Promise { 63 | let cwd 64 | if (this.cwd) { 65 | cwd = this.cwd 66 | } else { 67 | const previousActiveItem = atom.workspace.getActivePaneItem() 68 | // @ts-ignore 69 | cwd = previousActiveItem?.getPath?.() ?? previousActiveItem?.selectedPath 70 | if (cwd) { 71 | const dir = atom.project.relativizePath(cwd)[0] 72 | if (dir) { 73 | // Use project paths whenever they are available by default. 74 | return dir 75 | } 76 | } 77 | } 78 | 79 | try { 80 | if (cwd) { 81 | // Otherwise, if the path exists on the local file system, use the 82 | // path or parent directory as appropriate. 83 | const stats = await fs.stat(cwd) 84 | if (stats.isDirectory()) { 85 | return cwd 86 | } 87 | 88 | cwd = path.dirname(cwd) 89 | const dirStats = await fs.stat(cwd) 90 | if (dirStats.isDirectory()) { 91 | return cwd 92 | } 93 | } 94 | } catch { 95 | //failt silently 96 | } 97 | 98 | cwd = atom.project.getPaths()[0] 99 | // no project paths 100 | return cwd 101 | } 102 | 103 | serialize() { 104 | return { 105 | deserializer: "TerminalModel", 106 | version: "1.0.0", 107 | uri: this.uri, 108 | } 109 | } 110 | 111 | destroy() { 112 | if (this.element) { 113 | this.element.destroy() 114 | } 115 | this.terminalsSet.delete(this) 116 | } 117 | 118 | getTitle() { 119 | return (this.isActiveTerminal() ? "* " : "") + this.title 120 | } 121 | 122 | getElement() { 123 | return this.element 124 | } 125 | 126 | getURI() { 127 | return this.uri 128 | } 129 | 130 | getLongTitle() { 131 | if (this.title === DEFAULT_TITLE) { 132 | return DEFAULT_TITLE 133 | } 134 | return `${DEFAULT_TITLE} (${this.title})` 135 | } 136 | 137 | onDidChangeTitle(callback: (value?: string) => void) { 138 | return this.emitter.on("did-change-title", callback) 139 | } 140 | 141 | getIconName() { 142 | return "terminal" 143 | } 144 | 145 | getPath() { 146 | return this.cwd 147 | } 148 | 149 | isModified() { 150 | return this.modified 151 | } 152 | 153 | onDidChangeModified(callback: (value?: boolean) => void) { 154 | return this.emitter.on("did-change-modified", callback) 155 | } 156 | 157 | handleNewDataArrival() { 158 | if (!this.pane) { 159 | this.pane = atom.workspace.paneForItem(this) 160 | } 161 | const oldIsModified = this.modified 162 | let item 163 | if (this.pane) { 164 | item = this.pane.getActiveItem() 165 | } 166 | if (item === this) { 167 | this.modified = false 168 | } else { 169 | this.modified = true 170 | } 171 | if (oldIsModified !== this.modified) { 172 | this.emitter.emit("did-change-modified", this.modified) 173 | } 174 | } 175 | 176 | getSessionId() { 177 | return this.sessionId 178 | } 179 | 180 | refitTerminal() { 181 | // Only refit if there's a DOM element attached to the model. 182 | if (this.element) { 183 | this.element.refitTerminal() 184 | } 185 | } 186 | 187 | focusOnTerminal(double = false) { 188 | if (this.element) { 189 | if (this.pane) { 190 | this.pane.activateItem(this) 191 | } 192 | this.element.focusOnTerminal(double) 193 | const oldIsModified = this.modified 194 | this.modified = false 195 | if (oldIsModified !== this.modified) { 196 | this.emitter.emit("did-change-modified", this.modified) 197 | } 198 | } 199 | } 200 | 201 | exit() { 202 | if (this.pane) { 203 | this.pane.destroyItem(this, true) 204 | } 205 | } 206 | 207 | restartPtyProcess() { 208 | if (this.element) { 209 | this.element.restartPtyProcess() 210 | } 211 | } 212 | 213 | copyFromTerminal() { 214 | if (this.element && this.element.terminal) { 215 | return this.element.terminal.getSelection() 216 | } 217 | return 218 | } 219 | 220 | runCommand(cmd: string) { 221 | this.pasteToTerminal(cmd + os.EOL.charAt(0)) 222 | } 223 | 224 | pasteToTerminal(text: string) { 225 | if (this.element && this.element.ptyProcess) { 226 | this.element.ptyProcess.write(text) 227 | } 228 | } 229 | 230 | clear() { 231 | if (this.element) { 232 | return this.element.clear() 233 | } 234 | } 235 | 236 | setActive() { 237 | TerminalModel.recalculateActive(this.terminalsSet, this) 238 | } 239 | 240 | isVisible() { 241 | return this.pane && this.pane.getActiveItem() === this && (!this.dock || this.dock.isVisible()) 242 | } 243 | 244 | isActiveTerminal() { 245 | return this.activeIndex === 0 && (atom.config.get("atomic-terminal.allowHiddenToStayActive") || this.isVisible()) 246 | } 247 | 248 | updateTheme() { 249 | if (this.element) { 250 | return this.element.updateTheme() 251 | } 252 | } 253 | 254 | setNewPane(pane: Pane) { 255 | this.pane = pane 256 | // @ts-ignore 257 | const location = this.pane.getContainer().getLocation() 258 | switch (location) { 259 | case "left": 260 | this.dock = atom.workspace.getLeftDock() 261 | break 262 | case "right": 263 | this.dock = atom.workspace.getRightDock() 264 | break 265 | case "bottom": 266 | this.dock = atom.workspace.getBottomDock() 267 | break 268 | default: 269 | this.dock = undefined 270 | } 271 | } 272 | 273 | static recalculateActive(terminalsSet: Set, active?: TerminalModel) { 274 | const allowHidden = atom.config.get("atomic-terminal.allowHiddenToStayActive") 275 | const terminals = [...terminalsSet] 276 | terminals.sort((a, b) => { 277 | // active before other 278 | if (active && a === active) { 279 | return -1 280 | } 281 | if (active && b === active) { 282 | return 1 283 | } 284 | if (!allowHidden) { 285 | // visible before hidden 286 | if (a.isVisible() && !b.isVisible()) { 287 | return -1 288 | } 289 | if (!a.isVisible() && b.isVisible()) { 290 | return 1 291 | } 292 | } 293 | // lower activeIndex before higher activeIndex 294 | return a.activeIndex - b.activeIndex 295 | }) 296 | terminals.forEach((t, i) => { 297 | t.activeIndex = i 298 | t.emitter.emit("did-change-title", t.title) 299 | }) 300 | } 301 | 302 | static isTerminalModel(item: unknown) { 303 | return item instanceof TerminalModel 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.1.8](https://github.com/atom-community/terminal/compare/v1.1.7...v1.1.8) (2022-11-04) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * get correct cwd when treeview is selected ([#102](https://github.com/atom-community/terminal/issues/102)) ([e462b00](https://github.com/atom-community/terminal/commit/e462b00d01cf84060bcb25f976d56ed5d82f1da2)) 7 | 8 | ## [1.1.7](https://github.com/atom-community/terminal/compare/v1.1.6...v1.1.7) (2021-07-25) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * enable scope hoisting of Parcel - reduce the size of app ([00d8d96](https://github.com/atom-community/terminal/commit/00d8d96a7369f783553146b9e4bb092424ba0345)) 14 | * require at least Atom 1.52 - electron 6 ([8fc0612](https://github.com/atom-community/terminal/commit/8fc0612973e2f99e74107988853d9c7f97d5734f)) 15 | * use terser-config-atomic - reduce the size of the app ([c17c299](https://github.com/atom-community/terminal/commit/c17c29934b80670b8f7a091d2be579c4d52ad770)) 16 | 17 | ## [1.1.6](https://github.com/atom-community/terminal/compare/v1.1.5...v1.1.6) (2021-07-13) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * use padding variable ([81cf0c9](https://github.com/atom-community/terminal/commit/81cf0c96c54bf8cec4ca58f2e8689d867a04c46c)) 23 | 24 | ## [1.1.5](https://github.com/atom-community/terminal/compare/v1.1.4...v1.1.5) (2021-07-12) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * add padding to the terminal ([14144a5](https://github.com/atom-community/terminal/commit/14144a53e6055c18b9d0855a3d14b4d20101c4d7)) 30 | * add terminal syntax variables to standard theme ([bb27e56](https://github.com/atom-community/terminal/commit/bb27e56689e271b9216ef0039ab1aeccbcc52eaf)) 31 | * default to Standard theme ([512e285](https://github.com/atom-community/terminal/commit/512e28597aa4bd5d72214604e8b053ace3cb067f)) 32 | * update Dependencies ([a9e6be8](https://github.com/atom-community/terminal/commit/a9e6be82fef424d68686dd56189dbf156996c1f4)) 33 | * update theme without restarting terminal ([b38d4d1](https://github.com/atom-community/terminal/commit/b38d4d18b4ae5ce87598c11eeb51ec31dbcc0cd5)) 34 | * use 1rem of padding ([834a710](https://github.com/atom-community/terminal/commit/834a710537884fb0d92d40d059ef514bbbd1798d)) 35 | 36 | ## [1.1.4](https://github.com/atom-community/terminal/compare/v1.1.3...v1.1.4) (2021-06-13) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * update Dependencies ([d1f3d5d](https://github.com/atom-community/terminal/commit/d1f3d5d1570337f1a60f5b05245979511db3b627)) 42 | 43 | ## [1.1.3](https://github.com/atom-community/terminal/compare/v1.1.2...v1.1.3) (2021-05-13) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * update dependencies ([dcae454](https://github.com/atom-community/terminal/commit/dcae45439510fcb62d88044fa65450f13f63a842)) 49 | 50 | ## [1.1.2](https://github.com/atom-community/terminal/compare/v1.1.1...v1.1.2) (2021-05-08) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * use customElements ([bcf29f0](https://github.com/atom-community/terminal/commit/bcf29f0118f8a7d81a1a34b582a91063314aed62)) 56 | 57 | ## [1.1.1](https://github.com/atom-community/terminal/compare/v1.1.0...v1.1.1) (2021-05-07) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * toolbar config was wrong ([08625fb](https://github.com/atom-community/terminal/commit/08625fb3c83c586ec384ca2e9f2535c4958c03c4)) 63 | * use node url instead of whatwg-url ([71de4dc](https://github.com/atom-community/terminal/commit/71de4dc495f244257de8887fd860f5df9929f2ee)) 64 | 65 | # [1.1.0](https://github.com/atom-community/terminal/compare/v1.0.1...v1.1.0) (2021-05-03) 66 | 67 | 68 | ### Features 69 | 70 | * add setting to remove toolbar button ([6b66057](https://github.com/atom-community/terminal/commit/6b66057c5a0ea4590d5d595a9f03f46fbc484e32)) 71 | 72 | ## [1.0.1](https://github.com/atom-community/terminal/compare/v1.0.0...v1.0.1) (2021-05-03) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * activate pane on focus ([cee1511](https://github.com/atom-community/terminal/commit/cee1511f594467a1601f96c43fd1645bb4216f57)) 78 | 79 | # 1.0.0 (2021-04-29) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * allow showing the terminal when intersectionRatio is not 1 ([7a05451](https://github.com/atom-community/terminal/commit/7a0545149fc5d5917a322bd06f9f9af7a5308717)) 85 | * bump deps ([4de3aea](https://github.com/atom-community/terminal/commit/4de3aea97b0a0495b583c6cf90a63762032bdb7f)) 86 | * close on error opening shell ([11225e9](https://github.com/atom-community/terminal/commit/11225e9d4f5d72cb4c28338bed3f88434e5f9d73)) 87 | * enable ligatures ([10e64ce](https://github.com/atom-community/terminal/commit/10e64ceba8f13eadc96299da526a59d16d1e341d)) 88 | * export getDefaultShell and getFallbackShell for testing ([f8fa49b](https://github.com/atom-community/terminal/commit/f8fa49ba7dec189f93f24055974f642e25a09e19)) 89 | * Fix context menu on terminal ([#38](https://github.com/atom-community/terminal/issues/38)) ([f9a528e](https://github.com/atom-community/terminal/commit/f9a528e07b6b2a0d084f9544d4206a05bcbcf864)) 90 | * move watcher disposable code ([d8b4fc9](https://github.com/atom-community/terminal/commit/d8b4fc9bb5fab14ed77776ff5d0533ce704fc331)), closes [/github.com/atom-community/terminal/pull/24#discussion_r499099903](https://github.com//github.com/atom-community/terminal/pull/24/issues/discussion_r499099903) 91 | * set autoShell only for the first time ([977e6dc](https://github.com/atom-community/terminal/commit/977e6dc528e557d785587fc5dd9e2d7a0930f482)) 92 | * set shell to pwsh or powershell if exists ([60b28f4](https://github.com/atom-community/terminal/commit/60b28f414471aa6a2753ca7b8bc1072c29f10eda)) 93 | * typo defaultShellCache ([151d6b2](https://github.com/atom-community/terminal/commit/151d6b2640a291dff0f498c2ef65f3976b31dda8)) 94 | * update deps ([3944561](https://github.com/atom-community/terminal/commit/3944561566f501d759b3457d26534a12e8a4474b)) 95 | * update deps ([ec03e67](https://github.com/atom-community/terminal/commit/ec03e673b6f001a955b2d6e2f2ea7021dcd6c64c)) 96 | * upgrade whatwg-url from 8.3.0 to 8.4.0 ([a488877](https://github.com/atom-community/terminal/commit/a488877a2a2d1faaf86c7034e998feee9cc81d8e)) 97 | * use async exists ([02ad971](https://github.com/atom-community/terminal/commit/02ad9714d15ffe09c13b5a4e982946da262cefc8)) 98 | * use fallback in case of an error ([c168765](https://github.com/atom-community/terminal/commit/c168765a9a0190332414992c6edc73bb3703b89f)) 99 | * use localStorage to store autoShell ([b49dd6a](https://github.com/atom-community/terminal/commit/b49dd6aa27378323a008fcd78ec7beb2732df381)), closes [/github.com/atom-community/terminal/pull/24#discussion_r499099514](https://github.com//github.com/atom-community/terminal/pull/24/issues/discussion_r499099514) 100 | * use onDidChange instead of observe ([a3b6708](https://github.com/atom-community/terminal/commit/a3b67085261f747c42a591c6d153560636ea9f95)) 101 | * **deps:** update deps ([54734e4](https://github.com/atom-community/terminal/commit/54734e403efdc876170e148a226d626da7bed8fa)) 102 | 103 | 104 | ### Features 105 | 106 | * add defaultSellCache to improve performance ([16e51aa](https://github.com/atom-community/terminal/commit/16e51aa619c44f35b09672d5d563f48d9d02ac1c)) 107 | * add focus command ([fef8c6a](https://github.com/atom-community/terminal/commit/fef8c6a003be0ee7ce9bf22381c0a34a7b712a2a)) 108 | * add ligatures config (default to true) ([6a5c437](https://github.com/atom-community/terminal/commit/6a5c437661f0e45e2cbfbb36858af20cdf41a52f)) 109 | * add terminal:clear command ([5be215a](https://github.com/atom-community/terminal/commit/5be215af5ab3b8b171526d64ad23df64f687de64)) 110 | * add tool-bar button ([248825d](https://github.com/atom-community/terminal/commit/248825dbb2608d42fe4d1d0412c50fbb507c61ea)) 111 | * add tree view context menu ([941868f](https://github.com/atom-community/terminal/commit/941868f157e71a3c8d2ed41efdfcfab772e88104)) 112 | * autoShell config ([769abfe](https://github.com/atom-community/terminal/commit/769abfe9c66ec71e7537a7ec07387071ff79ccef)) 113 | * enable copy on select by default ([3766adb](https://github.com/atom-community/terminal/commit/3766adb12ee0888d2f2ca1fec45b00097a623469)) 114 | * getDefaultShell finds the default shell start commmand ([4c9aebe](https://github.com/atom-community/terminal/commit/4c9aebe6dfdbb182a57b3c2137ef48923b0c1542)) 115 | * getFallbackShell ([8755e11](https://github.com/atom-community/terminal/commit/8755e11762a40fbe754778631828c163ca5608ec)) 116 | * import webgl and ligatures using dynamic import ([c1311b6](https://github.com/atom-community/terminal/commit/c1311b6dcb8279de02fe75efd3688a723eb925e3)) 117 | * import WebLinksAddon using dynamic import ([eb3bfc3](https://github.com/atom-community/terminal/commit/eb3bfc31d72f062cc5dc4e84817ede4d08fa7f49)) 118 | * install which to find programs ([d108320](https://github.com/atom-community/terminal/commit/d1083206f4741c54c4ac7b89d3f8a5566ebb43cf)) 119 | * open in the bottom dock by default ([348c619](https://github.com/atom-community/terminal/commit/348c61979177c0dba80988afa587d492258f79b1)) 120 | * rename to atomic-terminal ([bc5929c](https://github.com/atom-community/terminal/commit/bc5929cc258a42f62974c88748ba3ccfed3c779c)) 121 | * set start command asyncronously inside activate ([68da18c](https://github.com/atom-community/terminal/commit/68da18c2934709737cecb5fde3f92745b7f32801)) 122 | * setShellStartCommand: set start command asyncronously ([2140047](https://github.com/atom-community/terminal/commit/21400478a5c4e10d5219403d0f7d5c8c57b8e97f)) 123 | * use getFallbackShell as the default value for shell ([ea5a014](https://github.com/atom-community/terminal/commit/ea5a01467e56e2856c8342dc62c5fa5ea59a525f)) 124 | 125 | 126 | ### Reverts 127 | 128 | * Revert "fix: use isIntersecting" ([b4795cc](https://github.com/atom-community/terminal/commit/b4795cc55c1853560065e29b8313f3f3b180f92b)) 129 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | type configObjects = Record 2 | 3 | type configObject = { 4 | title: string 5 | description: string 6 | default?: string | boolean | number 7 | enum?: (string | number)[] 8 | minimum?: number 9 | maximum?: number 10 | order?: number 11 | collapsed?: boolean 12 | type: string 13 | properties?: configObjects 14 | } 15 | 16 | function configOrder(obj: configObjects): configObjects { 17 | let order = 1 18 | for (const name in obj) { 19 | obj[name].order = order++ 20 | if (obj[name].type === "object" && "properties" in obj[name]) { 21 | configOrder(obj[name].properties) 22 | } 23 | } 24 | return obj 25 | } 26 | 27 | export function getDefaultShell(): string { 28 | return process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : process.env.SHELL || "/bin/sh" 29 | } 30 | 31 | export const config = configOrder({ 32 | shell: { 33 | title: "Shell", 34 | description: "Path to the shell command.", 35 | type: "string", 36 | default: getDefaultShell(), 37 | }, 38 | encoding: { 39 | title: "Character Encoding", 40 | description: "Character encoding to use in spawned terminal.", 41 | type: "string", 42 | default: "", 43 | }, 44 | webgl: { 45 | title: "WebGL Renderer", 46 | description: "Enable the WebGL-based renderer.", 47 | type: "boolean", 48 | default: false, 49 | }, 50 | webLinks: { 51 | title: "Web Links", 52 | description: "Enable clickable web links.", 53 | type: "boolean", 54 | default: true, 55 | }, 56 | ligatures: { 57 | title: "Ligatures", 58 | description: "Enables ligatures support.", 59 | type: "boolean", 60 | default: true, 61 | }, 62 | fontFamily: { 63 | title: "Font Family", 64 | description: "Font family used in terminal emulator.", 65 | type: "string", 66 | default: atom.config.get("editor.fontFamily") || "monospace", 67 | }, 68 | fontSize: { 69 | title: "Font Size", 70 | description: "Font size used in terminal emulator.", 71 | type: "integer", 72 | default: atom.config.get("editor.fontSize") || 14, 73 | minimum: 8, 74 | maximum: 100, 75 | }, 76 | defaultOpenPosition: { 77 | title: "Default Open Position", 78 | description: "Position to open terminal through service API or terminal:open.", 79 | type: "string", 80 | enum: ["Center", "Split Up", "Split Down", "Split Left", "Split Right", "Bottom Dock", "Left Dock", "Right Dock"], 81 | default: "Bottom Dock", 82 | }, 83 | allowHiddenToStayActive: { 84 | title: "Allow Hidden Terminal To Stay Active", 85 | description: "When an active terminal is hidden keep it active until another terminal is focused.", 86 | type: "boolean", 87 | default: false, 88 | }, 89 | runInActive: { 90 | title: "Run in Active Terminal", 91 | description: "Whether to run commands from the service API in the active terminal or in a new terminal.", 92 | type: "boolean", 93 | default: true, 94 | }, 95 | allowRelaunchingTerminalsOnStartup: { 96 | title: "Allow relaunching terminals on startup", 97 | description: "Whether to allow relaunching terminals on startup.", 98 | type: "boolean", 99 | default: false, 100 | }, 101 | copyOnSelect: { 102 | title: "Copy On Select", 103 | description: "Copy text to clipboard on selection.", 104 | type: "boolean", 105 | default: true, 106 | }, 107 | toolbarButton: { 108 | title: "Add Toolbar Button", 109 | description: "Add button to open a terminal from the toolbar.", 110 | type: "boolean", 111 | default: true, 112 | }, 113 | colors: { 114 | title: "Colors", 115 | description: "Settings for the terminal colors.", 116 | type: "object", 117 | collapsed: true, 118 | properties: { 119 | theme: { 120 | title: "Theme", 121 | description: "Theme used in terminal emulator.", 122 | type: "string", 123 | enum: [ 124 | "Custom", 125 | "Atom Dark", 126 | "Atom Light", 127 | "Base16 Tomorrow Dark", 128 | "Base16 Tomorrow Light", 129 | "Christmas", 130 | "City Lights", 131 | "Dracula", 132 | "Grass", 133 | "Homebrew", 134 | "Inverse", 135 | "Linux", 136 | "Man Page", 137 | "Novel", 138 | "Ocean", 139 | "One Dark", 140 | "One Light", 141 | "Predawn", 142 | "Pro", 143 | "Red Sands", 144 | "Red", 145 | "Silver Aerogel", 146 | "Solarized Dark", 147 | "Solarized Light", 148 | "Solid Colors", 149 | "Standard", 150 | ], 151 | default: "Standard", 152 | }, 153 | // TODO: add transparency settings? 154 | foreground: { 155 | title: "Text Color", 156 | description: "This will be overridden if the theme is not 'Custom'.", 157 | type: "color", 158 | default: "#ffffff", 159 | }, 160 | background: { 161 | title: "Background Color", 162 | description: "This will be overridden if the theme is not 'Custom'.", 163 | type: "color", 164 | default: "#000000", 165 | }, 166 | cursor: { 167 | title: "Cursor Color", 168 | description: "Can be transparent. This will be overridden if the theme is not 'Custom'.", 169 | type: "color", 170 | default: "#ffffff", 171 | }, 172 | cursorAccent: { 173 | title: "Cursor Text Color", 174 | description: "Can be transparent. This will be overridden if the theme is not 'Custom'.", 175 | type: "color", 176 | default: "#000000", 177 | }, 178 | selection: { 179 | title: "Selection Background Color", 180 | description: "Can be transparent. This will be overridden if the theme is not 'Custom'.", 181 | type: "color", 182 | default: "#4d4d4d", 183 | }, 184 | black: { 185 | title: "ANSI Black", 186 | description: "`\\x1b[30m`", 187 | type: "color", 188 | default: "#2e3436", 189 | }, 190 | red: { 191 | title: "ANSI Red", 192 | description: "`\\x1b[31m`", 193 | type: "color", 194 | default: "#cc0000", 195 | }, 196 | green: { 197 | title: "ANSI Green", 198 | description: "`\\x1b[32m`", 199 | type: "color", 200 | default: "#4e9a06", 201 | }, 202 | yellow: { 203 | title: "ANSI Yellow", 204 | description: "`\\x1b[33m`", 205 | type: "color", 206 | default: "#c4a000", 207 | }, 208 | blue: { 209 | title: "ANSI Blue", 210 | description: "`\\x1b[34m`", 211 | type: "color", 212 | default: "#3465a4", 213 | }, 214 | magenta: { 215 | title: "ANSI Magenta", 216 | description: "`\\x1b[35m`", 217 | type: "color", 218 | default: "#75507b", 219 | }, 220 | cyan: { 221 | title: "ANSI Cyan", 222 | description: "`\\x1b[36m`", 223 | type: "color", 224 | default: "#06989a", 225 | }, 226 | white: { 227 | title: "ANSI White", 228 | description: "`\\x1b[37m`", 229 | type: "color", 230 | default: "#d3d7cf", 231 | }, 232 | brightBlack: { 233 | title: "ANSI Bright Black", 234 | description: "`\\x1b[1;30m`", 235 | type: "color", 236 | default: "#555753", 237 | }, 238 | brightRed: { 239 | title: "ANSI Bright Red", 240 | description: "`\\x1b[1;31m`", 241 | type: "color", 242 | default: "#ef2929", 243 | }, 244 | brightGreen: { 245 | title: "ANSI Bright Green", 246 | description: "`\\x1b[1;32m`", 247 | type: "color", 248 | default: "#8ae234", 249 | }, 250 | brightYellow: { 251 | title: "ANSI Bright Yellow", 252 | description: "`\\x1b[1;33m`", 253 | type: "color", 254 | default: "#fce94f", 255 | }, 256 | brightBlue: { 257 | title: "ANSI Bright Blue", 258 | description: "`\\x1b[1;34m`", 259 | type: "color", 260 | default: "#729fcf", 261 | }, 262 | brightMagenta: { 263 | title: "ANSI Bright Magenta", 264 | description: "`\\x1b[1;35m`", 265 | type: "color", 266 | default: "#ad7fa8", 267 | }, 268 | brightCyan: { 269 | title: "ANSI Bright Cyan", 270 | description: "`\\x1b[1;36m`", 271 | type: "color", 272 | default: "#34e2e2", 273 | }, 274 | brightWhite: { 275 | title: "ANSI Bright White", 276 | description: "`\\x1b[1;37m`", 277 | type: "color", 278 | default: "#eeeeec", 279 | }, 280 | }, 281 | }, 282 | }) 283 | 284 | // finds the default shell start commmand 285 | export async function setAutoShell(whichFun: typeof which): Promise { 286 | let shellStartCommand 287 | if (process.platform === "win32") { 288 | // Windows 289 | try { 290 | shellStartCommand = await whichFun("pwsh.exe") 291 | } catch (e1) { 292 | try { 293 | shellStartCommand = await whichFun("powershell.exe") 294 | } catch (e2) { 295 | // keep default 296 | } 297 | } 298 | } 299 | 300 | if (shellStartCommand && atom.config.get("atomic-terminal.shell") === getDefaultShell()) { 301 | atom.config.set("atomic-terminal.shell", shellStartCommand) 302 | } 303 | } 304 | 305 | import which from "which" 306 | 307 | // set shell command automatically on first install 308 | if (localStorage.getItem("atomic-terminal.autoShellSet") === null) { 309 | localStorage.setItem("atomic-terminal.autoShellSet", "true") 310 | setAutoShell(which) 311 | } 312 | -------------------------------------------------------------------------------- /src/element.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Disposable } from "atom" 2 | import { spawn as spawnPty, IPty, IPtyForkOptions } from "node-pty-prebuilt-multiarch" 3 | import { Terminal } from "xterm" 4 | import { FitAddon } from "xterm-addon-fit" 5 | import { shell } from "electron" 6 | 7 | import { config } from "./config" 8 | import { getTheme } from "./themes" 9 | import { TerminalModel } from "./model" 10 | 11 | import * as fs from "fs-extra" 12 | 13 | export class AtomTerminal extends HTMLElement { 14 | public model?: TerminalModel 15 | public disposables?: CompositeDisposable 16 | public contentRect?: { width: number; height: number } 17 | public initiallyVisible?: boolean 18 | public isInitialized?: boolean 19 | public initializedPromise?: Promise 20 | public terminal?: Terminal 21 | public fitAddon?: FitAddon 22 | public ptyProcess?: IPty 23 | public ptyProcessRunning?: boolean 24 | public ptyProcessCols?: number 25 | public ptyProcessRows?: number 26 | public ptyProcessCommand?: string 27 | public ptyProcessOptions?: IPtyForkOptions 28 | 29 | async initialize(model: TerminalModel) { 30 | this.model = model 31 | this.model.element = this 32 | this.disposables = new CompositeDisposable() 33 | this.initiallyVisible = false 34 | this.isInitialized = false 35 | let resolveInit: (value?: void | PromiseLike) => void, rejectInit: (reason?: any) => void 36 | this.initializedPromise = new Promise((resolve, reject) => { 37 | resolveInit = resolve 38 | rejectInit = reject 39 | }) 40 | try { 41 | // Always wait for the model to finish initializing before proceeding. 42 | await this.model.initializedPromise 43 | this.setAttribute("session-id", this.model.getSessionId()) 44 | // An element resize detector is used to check when this element is 45 | // resized due to the pane resizing or due to the entire window 46 | // resizing. 47 | const resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[]) => { 48 | const lastEntry = entries[entries.length - 1] 49 | this.contentRect = lastEntry.contentRect 50 | this.refitTerminal() 51 | }) 52 | resizeObserver.observe(this) 53 | this.disposables.add( 54 | new Disposable(() => { 55 | resizeObserver.disconnect() 56 | }) 57 | ) 58 | // Add an IntersectionObserver in order to apply new options and 59 | // refit as soon as the terminal is visible. 60 | const intersectionObserver = new IntersectionObserver( 61 | async (entries) => { 62 | const lastEntry = entries[entries.length - 1] 63 | if (lastEntry && lastEntry.intersectionRatio > 0.0) { 64 | this.initiallyVisible = true 65 | try { 66 | await this.createTerminal() 67 | // https://github.com/atom-community/terminal/issues/12 68 | setTimeout(() => { 69 | this.refitTerminal() 70 | }, 0) 71 | resolveInit() 72 | } catch (ex) { 73 | rejectInit(ex) 74 | } 75 | // Remove observer once visible 76 | intersectionObserver.disconnect() 77 | } 78 | }, 79 | { 80 | root: atom.views.getView(atom.workspace), 81 | threshold: 1.0, 82 | } 83 | ) 84 | intersectionObserver.observe(this) 85 | this.disposables.add( 86 | new Disposable(() => { 87 | intersectionObserver.disconnect() 88 | }) 89 | ) 90 | // Add event handler for increasing/decreasing the font when 91 | // holding 'ctrl' and moving the mouse wheel up or down. 92 | this.addEventListener( 93 | "wheel", 94 | (wheelEvent) => { 95 | if (!this.model) { 96 | return 97 | } 98 | 99 | if (!wheelEvent.ctrlKey || !atom.config.get("editor.zoomFontWhenCtrlScrolling")) { 100 | return 101 | } 102 | 103 | let fontSize = this.model.fontSize + (wheelEvent.deltaY < 0 ? 1 : -1) 104 | if (fontSize < config.fontSize.minimum) { 105 | fontSize = config.fontSize.minimum 106 | } else if (fontSize > config.fontSize.maximum) { 107 | fontSize = config.fontSize.maximum 108 | } 109 | this.model.fontSize = fontSize 110 | if (this.terminal) { 111 | this.terminal.setOption("fontSize", fontSize) 112 | } 113 | wheelEvent.stopPropagation() 114 | }, 115 | { capture: true } 116 | ) 117 | } catch (ex) { 118 | // @ts-ignore 119 | rejectInit(ex) 120 | throw ex 121 | } 122 | this.isInitialized = true 123 | } 124 | 125 | destroy() { 126 | if (this.ptyProcess) { 127 | this.ptyProcess.kill() 128 | } 129 | if (this.terminal) { 130 | this.terminal.dispose() 131 | } 132 | if (this.disposables) { 133 | this.disposables.dispose() 134 | } 135 | } 136 | 137 | async checkPathIsDirectory(path: string) { 138 | if (path) { 139 | try { 140 | const stats = await fs.stat(path) 141 | if (stats && stats.isDirectory()) { 142 | return true 143 | } 144 | } catch (err) {} 145 | } 146 | 147 | return false 148 | } 149 | 150 | async getCwd() { 151 | if (!this.model) { 152 | return 153 | } 154 | 155 | const cwd = this.model.cwd 156 | if (cwd && (await this.checkPathIsDirectory(cwd))) { 157 | return cwd 158 | } 159 | 160 | // If the cwd from the model was invalid, reset it to null. 161 | this.model.cwd = undefined 162 | 163 | return 164 | } 165 | 166 | getEnv(): Record { 167 | const env = { ...process.env } 168 | delete env.NODE_ENV 169 | return >env 170 | } 171 | 172 | getXtermOptions() { 173 | const xtermOptions = { 174 | cursorBlink: true, 175 | fontSize: 14, 176 | fontFamily: "monospace", 177 | theme: {}, 178 | } 179 | xtermOptions.fontSize = atom.config.get("atomic-terminal.fontSize") 180 | xtermOptions.fontFamily = atom.config.get("atomic-terminal.fontFamily") 181 | xtermOptions.theme = getTheme() 182 | return xtermOptions 183 | } 184 | 185 | setMainBackgroundColor() { 186 | const theme = getTheme() 187 | if (theme && theme.background) { 188 | this.style.backgroundColor = theme.background 189 | } else { 190 | this.style.backgroundColor = "#000000" 191 | } 192 | } 193 | 194 | async loadAddons() { 195 | if (!this.terminal) { 196 | return 197 | } 198 | this.fitAddon = new FitAddon() 199 | this.terminal.loadAddon(this.fitAddon) 200 | if (atom.config.get("atomic-terminal.webLinks")) { 201 | const { WebLinksAddon } = await import("xterm-addon-web-links") 202 | this.terminal.loadAddon( 203 | new WebLinksAddon((_e, uri) => { 204 | shell.openExternal(uri) 205 | }) 206 | ) 207 | } 208 | this.terminal.open(this) 209 | if (atom.config.get("atomic-terminal.webgl")) { 210 | const { WebglAddon } = await import("xterm-addon-webgl") 211 | this.terminal.loadAddon(new WebglAddon()) 212 | } 213 | if (atom.config.get("atomic-terminal.ligatures")) { 214 | const { LigaturesAddon } = await import("xterm-addon-ligatures") 215 | this.terminal.loadAddon(new LigaturesAddon()) 216 | } 217 | } 218 | 219 | async createTerminal() { 220 | if (!this.disposables) { 221 | return 222 | } 223 | // Attach terminal emulator to this element and refit. 224 | this.setMainBackgroundColor() 225 | this.terminal = new Terminal(this.getXtermOptions()) 226 | await this.loadAddons() 227 | this.terminal.focus() 228 | this.ptyProcessCols = 80 229 | this.ptyProcessRows = 25 230 | this.refitTerminal() 231 | this.ptyProcess = undefined 232 | this.ptyProcessRunning = false 233 | this.disposables.add( 234 | this.terminal.onData((data) => { 235 | if (this.ptyProcess && this.ptyProcessRunning) { 236 | this.ptyProcess.write(data) 237 | } 238 | }) 239 | ) 240 | this.disposables.add( 241 | this.terminal.onSelectionChange(() => { 242 | if (this.terminal && atom.config.get("atomic-terminal.copyOnSelect")) { 243 | let text = this.terminal.getSelection() 244 | if (text) { 245 | const rawLines = text.split(/\r?\n/g) 246 | const lines = rawLines.map((line) => line.replace(/\s/g, " ").trimRight()) 247 | text = lines.join("\n") 248 | atom.clipboard.write(text) 249 | } 250 | } 251 | }) 252 | ) 253 | await this.restartPtyProcess() 254 | } 255 | 256 | async restartPtyProcess() { 257 | if (this.ptyProcess && this.ptyProcessRunning) { 258 | // @ts-ignore 259 | this.ptyProcess.removeAllListeners("exit") 260 | this.ptyProcess.kill() 261 | } 262 | // Reset the terminal. 263 | if (this.terminal) { 264 | this.terminal.reset() 265 | } 266 | 267 | // Setup pty process. 268 | this.ptyProcessCommand = atom.config.get("atomic-terminal.shell") 269 | const encoding = atom.config.get("atomic-terminal.encoding") 270 | 271 | // Attach pty process to terminal. 272 | // NOTE: This must be done after the terminal is attached to the 273 | // parent element and refitted. 274 | this.ptyProcessOptions = { 275 | cwd: await this.getCwd(), 276 | env: this.getEnv(), 277 | } 278 | if (encoding) { 279 | // There's some issue if 'encoding=null' is passed in the options, 280 | // therefore, only set it if there's an actual encoding to set. 281 | this.ptyProcessOptions.encoding = encoding 282 | } 283 | 284 | this.ptyProcessOptions.cols = this.ptyProcessCols 285 | this.ptyProcessOptions.rows = this.ptyProcessRows 286 | this.ptyProcess = undefined 287 | this.ptyProcessRunning = false 288 | try { 289 | this.ptyProcess = spawnPty(this.ptyProcessCommand, [], this.ptyProcessOptions) 290 | 291 | if (this.ptyProcess) { 292 | this.ptyProcessRunning = true 293 | this.ptyProcess.on("data", (data) => { 294 | if (!this.model || !this.terminal || !this.ptyProcess) { 295 | return 296 | } 297 | const oldTitle = this.model.title 298 | if (process.platform !== "win32") { 299 | this.model.title = this.ptyProcess.process 300 | } 301 | if (oldTitle !== this.model.title) { 302 | this.model.emitter.emit("did-change-title", this.model.title) 303 | } 304 | this.terminal.write(data) 305 | this.model.handleNewDataArrival() 306 | }) 307 | this.ptyProcess.on("exit", () => { 308 | this.ptyProcessRunning = false 309 | if (this.model) { 310 | this.model.exit() 311 | } 312 | }) 313 | } 314 | } catch (err) { 315 | let message = `Launching '${this.ptyProcessCommand}' raised the following error: ${err.message}` 316 | if (err.message.startsWith("File not found:")) { 317 | message = `Could not open shell '${this.ptyProcessCommand}'.` 318 | } 319 | atom.notifications.addError("Terminal Error", { detail: message }) 320 | if (this.model) { 321 | this.model.exit() 322 | } 323 | } 324 | } 325 | 326 | refitTerminal() { 327 | // Only refit the terminal when it is completely visible. 328 | if ( 329 | this.fitAddon && 330 | this.initiallyVisible && 331 | this.contentRect && 332 | this.contentRect.width > 0 && 333 | this.contentRect.height > 0 334 | ) { 335 | this.fitAddon.fit() 336 | const geometry = this.fitAddon.proposeDimensions() 337 | if (geometry && this.ptyProcess && this.ptyProcessRunning) { 338 | // Resize pty process 339 | if (this.ptyProcessCols !== geometry.cols || this.ptyProcessRows !== geometry.rows) { 340 | this.ptyProcess.resize(geometry.cols, geometry.rows) 341 | this.ptyProcessCols = geometry.cols 342 | this.ptyProcessRows = geometry.rows 343 | } 344 | } 345 | } 346 | } 347 | 348 | focusOnTerminal(double = false) { 349 | if (this.terminal && this.model) { 350 | this.model.setActive() 351 | this.terminal.focus() 352 | if (double) { 353 | // second focus will send command to pty 354 | this.terminal.focus() 355 | } 356 | } 357 | } 358 | 359 | clear() { 360 | if (this.terminal) { 361 | return this.terminal.clear() 362 | } 363 | } 364 | 365 | updateTheme() { 366 | if (this.terminal) { 367 | this.setMainBackgroundColor() 368 | this.terminal.setOption("theme", getTheme()) 369 | } 370 | } 371 | } 372 | 373 | if (!window.customElements.get("atomic-terminal")) { 374 | window.customElements.define("atomic-terminal", AtomTerminal) 375 | } 376 | 377 | export function createTerminalElement() { 378 | return document.createElement("atomic-terminal") as AtomTerminal 379 | } 380 | -------------------------------------------------------------------------------- /dist/xterm-addon-web-links.3abc7089.js.map: -------------------------------------------------------------------------------- 1 | {"mappings":"uMAA2CA,EAAMC,EAAND,EASxCE,OAT8CD,EAS9CC,W,oBCLQC,EAAoBC,GAG5B,GAAGC,EAAiBD,UACZC,EAAiBD,GAAUE,QAGnC,IAAIC,EAASF,EAAiBD,IAC7BI,EAAGJ,EACHK,KACAH,S,UAIDI,EAAQN,GAAUO,KAAKJ,EAAOD,QAASC,EAAQA,EAAOD,QAASH,GAG/DI,EAAOE,KAGAF,EAAOD,QAvBf,IAAID,G,SA4BJF,EAAoBS,EAAIF,EAGxBP,EAAoBU,EAAIR,EAGxBF,EAAoBW,GAAaR,EAASS,EAAMC,KAC3Cb,EAAoBc,EAAEX,EAASS,IAClCG,OAAOC,eAAeb,EAASS,GAAQK,cAAkBC,IAAKL,KAKhEb,EAAoBmB,EAAahB,I,oBACtBiB,QAA0BA,OAAOC,aAC1CN,OAAOC,eAAeb,EAASiB,OAAOC,aAAeC,iBAEtDP,OAAOC,eAAeb,eAAS,CAAgBmB,UAAO,EAQvDtB,EAAoBuB,GAAaD,EAAOE,KAEvC,GADU,EAAPA,IAAUF,EAAQtB,EAAoBsB,IAC/B,EAAPE,SAAiBF,EACpB,GAAW,EAAPE,oBAAoBF,GAAsBA,GAASA,EAAMG,kBAAmBH,EAChF,IAAII,EAAKX,OAAOY,OAAO,MAGvB,GAFA3B,EAAoBmB,EAAEO,GACtBX,OAAOC,eAAeU,YAAI,CAAaT,cAAkBK,MAAOA,IACtD,EAAPE,oBAAmBF,MAAuB,IAAIM,KAAON,EAAOtB,EAAoBW,EAAEe,EAAIE,GAAcA,GAAcN,EAAMM,IAAQC,KAAK,KAAMD,I,OACvIF,GAIR1B,EAAoB8B,EAAa1B,IAChC,IAAIS,EAAST,GAAUA,EAAOqB,eACErB,EAAgB2B,QAAA,IACV3B,E,OACtCJ,EAAoBW,EAAEE,MAAaA,GAC5BA,GAIRb,EAAoBc,GAAakB,EAAQC,KAAmBlB,CAAOmB,EAAUC,eAAe3B,KAAKwB,EAAQC,IAGzGjC,EAAoBoC,KAIbpC,EAAoBA,EAAoBqC,EAAI,E,0CCtD5CC,EAAWC,EAAmBC,GACrC,IAAMC,EAAY1C,OAAO2C,OACrBD,GACFA,EAAUE,OAAS,KACnBF,EAAUG,SAASC,KAAOL,GAE1BM,QAAQC,2DAAK,C,wEA5BjB,IAAA5B,EAAAW,EAAA,GAoBMkB,EAAqBC,sTAY3BC,EAAA,W,SAKEC,EACUC,EACAC,EACAC,Q,IAFAF,MAAAtC,QAAA,IACAuC,O,YACAC,UAAAC,KAFAH,gBACAC,gBACAC,wBAEHD,SAASG,WAAa,E,OAGtBL,EAAAjB,UAAAuB,kBAASC,G,KACTC,UAAYD,OAERJ,2CAAoBC,KAA+BI,eACrDC,mBAAqBD,UAAUE,qBAAqB,IAAI1C,EAAA2C,qBAAqBH,UAAWX,OAAqBI,gBAG7GW,oBAAuBJ,UAAuBK,oBAAoBhB,OAAqBI,cAAeC,WAIxGF,EAAAjB,UAAA+B,mB,eACuBC,KAAnBH,yBAAmDG,KAAdP,gBACvCA,UAAUQ,2BAA2BJ,gBAG1B,QAAlBK,EAAAb,KAAKK,yBAAaQ,KAAEH,WAExBd,CAAA,CA/BA,GAAa5B,EAAA8C,iB,sHC/Bb,IAAAlD,EAAA,W,SAEEgC,EACmBQ,EACAW,EACAlB,G,KAFAO,iBACAW,cACAlB,W,OAKZD,EAAAjB,UAAAqC,sBAAaC,EAAWC,GAC7BA,EAASC,EAAaC,YAAYH,OAAQF,YAAaX,eAAgBP,YAE3ED,CAAA,CAbA,GAAa5B,EAAAuC,kBAeb,IAAAzD,EAAA,M,SAAA8C,IAAA,C,OACgBA,EAAAwB,aAAYH,EAAWI,EAAelB,EAAoBmB,K,IACtE,IAIIC,EAJEC,EAAU9B,OAAO2B,EAAMI,QAASJ,EAAMK,gBAEtC5C,EAAyBqC,EAAaQ,qCAAqCV,EAAI,KAAUd,GAAxFyB,EAAI9C,EAAA,GAAE+C,EAAc/C,EAAA,GAGvBgD,KACEC,KAE8B,QAA5BR,EAAQC,EAAIQ,KAAKJ,KAAiB,CACxC,IAAMK,EAAOV,EAAM,GACnB,IAAKU,EAAM,CAGT1C,QAAQ2C,oD,KACR,CASF,GAFAJ,EAAcF,EAAKO,QAAQF,EAAMH,EAAc,GAC/CN,EAAIY,UAAYN,EAAcG,EAAKI,OACjB,EAAdP,EAAc,M,IAKlB,IAAIQ,EAAOR,EAAcG,EAAKI,OAC1BE,EAAOV,EAAiB,EAErBS,EAAOnC,EAASqC,MACrBF,GAAQnC,EAASqC,KACjBD,IAcFR,EAAOU,MAAOC,MAXRA,CACJC,OACEC,EAAGd,EAAc,EACjBb,EAAGY,EAAiB,GAEtBgB,KACED,EAAGN,EACHrB,EAAGsB,IAIcN,KAAIa,EAAE5C,SAAUoB,I,OAGhCS,GASMnC,EAAA+B,sCAAqCoB,EAAmBC,EAAoB7C,KACzF,IACI8C,EACAC,EAFAC,K,EAID,CAED,KADMvB,EAAOzB,EAASiD,OAAOC,OAAOC,QAAQP,UAKxCnB,EAAK2B,WACPR,IAGFG,EAAkBtB,EAAK2B,gBAChBL,GAET,IAAMrB,EAAiBkB,E,EAEpB,CACD,IAEMnB,EAFA4B,EAAWrD,EAASiD,OAAOC,OAAOC,QAAQP,EAAY,GAG5D,GAFAE,IAAkBO,GAAWA,EAASD,YAChC3B,EAAOzB,EAASiD,OAAOC,OAAOC,QAAQP,UAI5CI,GAAcvB,EAAK6B,mBAAmBR,GAAmBD,GAAWU,UAAU,EAAGvD,EAASqC,MAC1FO,UACOE,G,OAEDE,EAAYtB,IAExBjC,CAAA,EA9FA,GAAa5B,EAAAmD,aHtBbrE,CAAA,K,iBACWF,UACTC,UAAiBN,uBACHoH,QAAyBA,OAAOC,IAC9CD,UAAWpH,oBACGK,UACdA,UAAuBkE,cAAIvE,IAE3BD,EAAoBwE,cAAIvE,G","sources":["node_modules/xterm-addon-web-links/lib/webpack:/WebLinksAddon/webpack/universalModuleDefinition","node_modules/xterm-addon-web-links/lib/webpack:/WebLinksAddon/webpack/bootstrap","node_modules/xterm-addon-web-links/lib/webpack:/WebLinksAddon/src/WebLinksAddon.ts","node_modules/xterm-addon-web-links/lib/webpack:/WebLinksAddon/src/WebLinkProvider.ts"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"WebLinksAddon\"] = factory();\n\telse\n\t\troot[\"WebLinksAddon\"] = factory();\n})(window, function() {\nreturn "," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm';\nimport { WebLinkProvider } from './WebLinkProvider';\n\nconst protocolClause = '(https?:\\\\/\\\\/)';\nconst domainCharacterSet = '[\\\\da-z\\\\.-]+';\nconst negatedDomainCharacterSet = '[^\\\\da-z\\\\.-]+';\nconst domainBodyClause = '(' + domainCharacterSet + ')';\nconst tldClause = '([a-z\\\\.]{2,6})';\nconst ipClause = '((\\\\d{1,3}\\\\.){3}\\\\d{1,3})';\nconst localHostClause = '(localhost)';\nconst portClause = '(:\\\\d{1,5})';\nconst hostClause = '((' + domainBodyClause + '\\\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';\nconst pathCharacterSet = '(\\\\/[\\\\/\\\\w\\\\.\\\\-%~:+@]*)*([^:\"\\'\\\\s])';\nconst pathClause = '(' + pathCharacterSet + ')?';\nconst queryStringHashFragmentCharacterSet = '[0-9\\\\w\\\\[\\\\]\\\\(\\\\)\\\\/\\\\?\\\\!#@$%&\\'*+,:;~\\\\=\\\\.\\\\-]*';\nconst queryStringClause = '(\\\\?' + queryStringHashFragmentCharacterSet + ')?';\nconst hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';\nconst negatedPathCharacterSet = '[^\\\\/\\\\w\\\\.\\\\-%]+';\nconst bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;\nconst start = '(?:^|' + negatedDomainCharacterSet + ')(';\nconst end = ')($|' + negatedPathCharacterSet + ')';\nconst strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);\n\nfunction handleLink(event: MouseEvent, uri: string): void {\n const newWindow = window.open();\n if (newWindow) {\n newWindow.opener = null;\n newWindow.location.href = uri;\n } else {\n console.warn('Opening link blocked as opener could not be cleared');\n }\n}\n\nexport class WebLinksAddon implements ITerminalAddon {\n private _linkMatcherId: number | undefined;\n private _terminal: Terminal | undefined;\n private _linkProvider: IDisposable | undefined;\n\n constructor(\n private _handler: (event: MouseEvent, uri: string) => void = handleLink,\n private _options: ILinkMatcherOptions = {},\n private _useLinkProvider: boolean = false\n ) {\n this._options.matchIndex = 1;\n }\n\n public activate(terminal: Terminal): void {\n this._terminal = terminal;\n\n if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) {\n this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler));\n } else {\n // TODO: This should be removed eventually\n this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, this._options);\n }\n }\n\n public dispose(): void {\n if (this._linkMatcherId !== undefined && this._terminal !== undefined) {\n this._terminal.deregisterLinkMatcher(this._linkMatcherId);\n }\n\n this._linkProvider?.dispose();\n }\n}\n","/**\n * Copyright (c) 2019 The xterm.js authors. All rights reserved.\n * @license MIT\n */\n\nimport { ILinkProvider, IBufferCellPosition, ILink, Terminal } from 'xterm';\n\nexport class WebLinkProvider implements ILinkProvider {\n\n constructor(\n private readonly _terminal: Terminal,\n private readonly _regex: RegExp,\n private readonly _handler: (event: MouseEvent, uri: string) => void\n ) {\n\n }\n\n public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {\n callback(LinkComputer.computeLink(y, this._regex, this._terminal, this._handler));\n }\n}\n\nexport class LinkComputer {\n public static computeLink(y: number, regex: RegExp, terminal: Terminal, handler: (event: MouseEvent, uri: string) => void): ILink[] {\n const rex = new RegExp(regex.source, (regex.flags || '') + 'g');\n\n const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);\n\n let match;\n let stringIndex = -1;\n const result: ILink[] = [];\n\n while ((match = rex.exec(line)) !== null) {\n const text = match[1];\n if (!text) {\n // something matched but does not comply with the given matchIndex\n // since this is most likely a bug the regex itself we simply do nothing here\n console.log('match found without corresponding matchIndex');\n break;\n }\n\n // Get index, match.index is for the outer match which includes negated chars\n // therefore we cannot use match.index directly, instead we search the position\n // of the match group in text again\n // also correct regex and string search offsets for the next loop run\n stringIndex = line.indexOf(text, stringIndex + 1);\n rex.lastIndex = stringIndex + text.length;\n if (stringIndex < 0) {\n // invalid stringIndex (should not have happened)\n break;\n }\n\n let endX = stringIndex + text.length;\n let endY = startLineIndex + 1;\n\n while (endX > terminal.cols) {\n endX -= terminal.cols;\n endY++;\n }\n\n const range = {\n start: {\n x: stringIndex + 1,\n y: startLineIndex + 1\n },\n end: {\n x: endX,\n y: endY\n }\n };\n\n result.push({ range, text, activate: handler });\n }\n\n return result;\n }\n\n /**\n * Gets the entire line for the buffer line\n * @param line The line being translated.\n * @param trimRight Whether to trim whitespace to the right.\n * @param terminal The terminal\n */\n private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [string, number] {\n let lineString = '';\n let lineWrapsToNext: boolean;\n let prevLinesToWrap: boolean;\n\n do {\n const line = terminal.buffer.active.getLine(lineIndex);\n if (!line) {\n break;\n }\n\n if (line.isWrapped) {\n lineIndex--;\n }\n\n prevLinesToWrap = line.isWrapped;\n } while (prevLinesToWrap);\n\n const startLineIndex = lineIndex;\n\n do {\n const nextLine = terminal.buffer.active.getLine(lineIndex + 1);\n lineWrapsToNext = nextLine ? nextLine.isWrapped : false;\n const line = terminal.buffer.active.getLine(lineIndex);\n if (!line) {\n break;\n }\n lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols);\n lineIndex++;\n } while (lineWrapsToNext);\n\n return [lineString, startLineIndex];\n }\n}\n"],"names":["root","factory","window","__webpack_require__","moduleId","installedModules","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","default","object","property","prototype","hasOwnProperty","p","s","handleLink","event","uri","newWindow","open","opener","location","href","console","warn","strictUrlRegex","RegExp","a","e1","_handler","_options","_useLinkProvider","this","matchIndex","activate","terminal","_terminal","_linkProvider","registerLinkProvider","WebLinkProvider","_linkMatcherId","registerLinkMatcher","dispose","undefined","deregisterLinkMatcher","e2","WebLinksAddon","_regex","provideLinks","y","callback","LinkComputer","computeLink","regex","handler","match","rex","source","flags","_translateBufferLineToStringWithWrap","line","startLineIndex","stringIndex","result","exec","text","log","indexOf","lastIndex","length","endX","endY","cols","push","range","start","x","end","f","lineIndex","trimRight","lineWrapsToNext","prevLinesToWrap","lineString","buffer","active","getLine","isWrapped","nextLine","translateToString","substring","define","amd"],"version":3,"file":"xterm-addon-web-links.3abc7089.js.map"} -------------------------------------------------------------------------------- /src/terminal.ts: -------------------------------------------------------------------------------- 1 | import { CompositeDisposable, Workspace, Dock, Pane, WorkspaceOpenOptions } from "atom" 2 | 3 | import { createTerminalElement } from "./element" 4 | import { TerminalModel } from "./model" 5 | import { addToolbarButton, removeToolbarButton } from "./button" 6 | export { consumeToolBar } from "./button" 7 | 8 | import { v4 as uuidv4 } from "uuid" 9 | 10 | interface OpenOptions extends WorkspaceOpenOptions { 11 | target?: EventTarget | null 12 | pane?: Pane 13 | } 14 | 15 | function debounce(callback: (...args: unknown[]) => void, wait: number = 300) { 16 | let timeoutId: NodeJS.Timeout 17 | return (...args: unknown[]) => { 18 | clearTimeout(timeoutId) 19 | timeoutId = setTimeout(() => { 20 | callback(...args) 21 | }, wait) 22 | } 23 | } 24 | 25 | const TERMINAL_BASE_URI = "atomic-terminal://" 26 | 27 | class Terminal { 28 | // TODO: maybe private? 29 | public disposables: CompositeDisposable 30 | public terminalsSet: Set 31 | 32 | constructor() { 33 | // Disposables for this plugin. 34 | this.disposables = new CompositeDisposable() 35 | 36 | // Set holding all terminals available at any moment. 37 | this.terminalsSet = new Set() 38 | 39 | const debounceUpdateTheme = debounce(async (style) => { 40 | // @ts-ignore 41 | if (style.sourcePath.includes("atomic-terminal")) { 42 | return 43 | } 44 | 45 | // @ts-ignore 46 | await atom.packages.getLoadedPackage("atomic-terminal").reloadStylesheets() 47 | this.updateTheme() 48 | }, 1) 49 | 50 | this.disposables.add( 51 | // Register view provider for terminal emulator item. 52 | atom.views.addViewProvider(TerminalModel, (terminalModel) => { 53 | const terminalElement = createTerminalElement() 54 | terminalElement.initialize(terminalModel) 55 | return terminalElement 56 | }), 57 | 58 | // Add opener for terminal emulator item. 59 | atom.workspace.addOpener((uri) => { 60 | if (uri.startsWith(TERMINAL_BASE_URI)) { 61 | const item = new TerminalModel({ 62 | uri, 63 | terminalsSet: this.terminalsSet, 64 | }) 65 | return item 66 | } 67 | return 68 | }), 69 | 70 | // Set callback to run on current and future panes. 71 | atom.workspace.observePanes((pane) => { 72 | // In callback, set another callback to run on current and future items. 73 | this.disposables.add( 74 | pane.observeItems((item) => { 75 | // In callback, set current pane for terminal items. 76 | if (TerminalModel.isTerminalModel(item)) { 77 | ;(item).setNewPane(pane) 78 | } 79 | TerminalModel.recalculateActive(this.terminalsSet) 80 | }) 81 | ) 82 | TerminalModel.recalculateActive(this.terminalsSet) 83 | }), 84 | 85 | // Add callbacks to run for current and future active items on active panes. 86 | atom.workspace.observeActivePaneItem((item) => { 87 | // In callback, focus specifically on terminal when item is terminal item. 88 | if (TerminalModel.isTerminalModel(item)) { 89 | ;(item).focusOnTerminal() 90 | } 91 | TerminalModel.recalculateActive(this.terminalsSet) 92 | }), 93 | 94 | atom.workspace.getRightDock().observeVisible((visible) => { 95 | if (visible) { 96 | const item = atom.workspace.getRightDock().getActivePaneItem() 97 | if (TerminalModel.isTerminalModel(item)) { 98 | ;(item).focusOnTerminal() 99 | } 100 | } 101 | TerminalModel.recalculateActive(this.terminalsSet) 102 | }), 103 | 104 | atom.workspace.getLeftDock().observeVisible((visible) => { 105 | if (visible) { 106 | const item = atom.workspace.getLeftDock().getActivePaneItem() 107 | if (TerminalModel.isTerminalModel(item)) { 108 | ;(item).focusOnTerminal() 109 | } 110 | } 111 | TerminalModel.recalculateActive(this.terminalsSet) 112 | }), 113 | 114 | atom.workspace.getBottomDock().observeVisible((visible) => { 115 | if (visible) { 116 | const item = atom.workspace.getBottomDock().getActivePaneItem() 117 | if (TerminalModel.isTerminalModel(item)) { 118 | ;(item).focusOnTerminal() 119 | } 120 | } 121 | TerminalModel.recalculateActive(this.terminalsSet) 122 | }), 123 | 124 | atom.config.onDidChange("atomic-terminal.toolbarButton", ({ newValue }: { newValue: boolean }) => { 125 | if (newValue) { 126 | addToolbarButton() 127 | } else { 128 | removeToolbarButton() 129 | } 130 | }), 131 | 132 | // Theme changes 133 | atom.config.onDidChange("atomic-terminal.colors", () => { 134 | // not debounced to update immediately 135 | this.updateTheme() 136 | }), 137 | atom.themes.onDidChangeActiveThemes(debounceUpdateTheme), 138 | atom.styles.onDidUpdateStyleElement(debounceUpdateTheme), 139 | atom.styles.onDidAddStyleElement(debounceUpdateTheme), 140 | atom.styles.onDidRemoveStyleElement(debounceUpdateTheme), 141 | 142 | // Add commands. 143 | // @ts-ignore 144 | atom.commands.add("atom-workspace", { 145 | "atomic-terminal:open": () => this.open(this.generateNewUri(), this.addDefaultPosition()), 146 | "atomic-terminal:open-center": () => this.openInCenterOrDock(atom.workspace), 147 | "atomic-terminal:open-up": () => this.open(this.generateNewUri(), { split: "up" }), 148 | "atomic-terminal:open-down": () => this.open(this.generateNewUri(), { split: "down" }), 149 | "atomic-terminal:open-left": () => this.open(this.generateNewUri(), { split: "left" }), 150 | "atomic-terminal:open-right": () => this.open(this.generateNewUri(), { split: "right" }), 151 | "atomic-terminal:open-bottom-dock": () => this.openInCenterOrDock(atom.workspace.getBottomDock()), 152 | "atomic-terminal:open-left-dock": () => this.openInCenterOrDock(atom.workspace.getLeftDock()), 153 | "atomic-terminal:open-right-dock": () => this.openInCenterOrDock(atom.workspace.getRightDock()), 154 | "atomic-terminal:close-all": () => this.exitAllTerminals(), 155 | "atomic-terminal:focus": () => this.focus(), 156 | }), 157 | // @ts-ignore 158 | atom.commands.add("atom-text-editor, .tree-view, .tab-bar", { 159 | "atomic-terminal:open-context-menu": { 160 | hiddenInCommandPalette: true, 161 | didDispatch: ({ target }) => this.open(this.generateNewUri(), this.addDefaultPosition({ target })), 162 | }, 163 | "atomic-terminal:open-center-context-menu": { 164 | hiddenInCommandPalette: true, 165 | didDispatch: ({ target }) => this.openInCenterOrDock(atom.workspace, { target }), 166 | }, 167 | "atomic-terminal:open-split-up-context-menu": { 168 | hiddenInCommandPalette: true, 169 | didDispatch: ({ target }) => this.open(this.generateNewUri(), { split: "up", target }), 170 | }, 171 | "atomic-terminal:open-split-down-context-menu": { 172 | hiddenInCommandPalette: true, 173 | didDispatch: ({ target }) => this.open(this.generateNewUri(), { split: "down", target }), 174 | }, 175 | "atomic-terminal:open-split-left-context-menu": { 176 | hiddenInCommandPalette: true, 177 | didDispatch: ({ target }) => this.open(this.generateNewUri(), { split: "left", target }), 178 | }, 179 | "atomic-terminal:open-split-right-context-menu": { 180 | hiddenInCommandPalette: true, 181 | didDispatch: ({ target }) => this.open(this.generateNewUri(), { split: "right", target }), 182 | }, 183 | "atomic-terminal:open-split-bottom-dock-context-menu": { 184 | hiddenInCommandPalette: true, 185 | didDispatch: ({ target }) => this.openInCenterOrDock(atom.workspace.getBottomDock(), { target }), 186 | }, 187 | "atomic-terminal:open-split-left-dock-context-menu": { 188 | hiddenInCommandPalette: true, 189 | didDispatch: ({ target }) => this.openInCenterOrDock(atom.workspace.getLeftDock(), { target }), 190 | }, 191 | "atomic-terminal:open-split-right-dock-context-menu": { 192 | hiddenInCommandPalette: true, 193 | didDispatch: ({ target }) => this.openInCenterOrDock(atom.workspace.getRightDock(), { target }), 194 | }, 195 | }), 196 | atom.commands.add("atomic-terminal", { 197 | "atomic-terminal:close": () => this.close(), 198 | "atomic-terminal:restart": () => this.restart(), 199 | "atomic-terminal:copy": () => this.copy(), 200 | "atomic-terminal:paste": () => this.paste(), 201 | "atomic-terminal:unfocus": () => this.unfocus(), 202 | "atomic-terminal:clear": () => this.clear(), 203 | }) 204 | ) 205 | } 206 | 207 | // eslint-disable-next-line 208 | activate() {} 209 | 210 | destroy() { 211 | this.exitAllTerminals() 212 | removeToolbarButton() 213 | this.disposables.dispose() 214 | } 215 | 216 | deserializeTerminalModel(serializedModel: TerminalModel) { 217 | if (atom.config.get("atomic-terminal.allowRelaunchingTerminalsOnStartup")) { 218 | return new TerminalModel({ 219 | uri: serializedModel.uri, 220 | terminalsSet: this.terminalsSet, 221 | }) 222 | } 223 | return 224 | } 225 | 226 | openInCenterOrDock(centerOrDock: Workspace | Dock, options: OpenOptions = {}) { 227 | const pane = centerOrDock.getActivePane() 228 | if (pane) { 229 | options.pane = pane 230 | } 231 | return this.open(this.generateNewUri(), options) 232 | } 233 | 234 | exitAllTerminals() { 235 | for (const terminal of this.terminalsSet) { 236 | terminal.exit() 237 | } 238 | } 239 | 240 | updateTheme() { 241 | for (const terminal of this.terminalsSet) { 242 | terminal.updateTheme() 243 | } 244 | } 245 | 246 | getActiveTerminal() { 247 | const terminals = [...this.terminalsSet] 248 | return terminals.find((t) => t.isActiveTerminal()) 249 | } 250 | 251 | async open(uri: string, options: OpenOptions = {}): Promise { 252 | // TODO: should we check uri for TERMINAL_BASE_URI? 253 | // if (!uri.startsWith(TERMINAL_BASE_URI)) { 254 | // return null 255 | // } 256 | 257 | const url = new URL(uri) 258 | if (options.target) { 259 | const target = this.getPath(options.target) 260 | if (target) { 261 | url.searchParams.set("cwd", target) 262 | } 263 | } 264 | return >atom.workspace.open(url.href, options) 265 | } 266 | 267 | generateNewUri() { 268 | return `${TERMINAL_BASE_URI + uuidv4()}/` 269 | } 270 | 271 | /** 272 | * Service function which is a wrapper around 'atom.workspace.open()'. 273 | * 274 | * @async 275 | * @function 276 | * @param {Object} options Options to pass to call to 'atom.workspace.open()'. 277 | * @returns {TerminalModel} Instance of TerminalModel. 278 | */ 279 | async openTerminal(options: OpenOptions = {}): Promise { 280 | options = this.addDefaultPosition(options) 281 | return this.open(this.generateNewUri(), options) 282 | } 283 | 284 | /** 285 | * Service function which opens a terminal and runs the commands. 286 | * 287 | * @async 288 | * @function 289 | * @param {string[]} commands Commands to run in the terminal. 290 | * @returns {TerminalModel} Instance of TerminalModel. 291 | */ 292 | async runCommands(commands: string[]): Promise { 293 | let terminal 294 | if (atom.config.get("atomic-terminal.runInActive")) { 295 | terminal = this.getActiveTerminal() 296 | } 297 | 298 | if (!terminal) { 299 | const options = this.addDefaultPosition() 300 | terminal = await this.open(this.generateNewUri(), options) 301 | } 302 | 303 | if (terminal.element) { 304 | await terminal.element.initializedPromise 305 | for (const command of commands) { 306 | terminal.runCommand(command) 307 | } 308 | } 309 | 310 | return terminal 311 | } 312 | 313 | addDefaultPosition(options: OpenOptions = {}): OpenOptions { 314 | const position = atom.config.get("atomic-terminal.defaultOpenPosition") 315 | switch (position) { 316 | case "Center": { 317 | const pane = atom.workspace.getActivePane() 318 | if (pane && !("pane" in options)) { 319 | options.pane = pane 320 | } 321 | break 322 | } 323 | case "Split Up": 324 | if (!("split" in options)) { 325 | options.split = "up" 326 | } 327 | break 328 | case "Split Down": 329 | if (!("split" in options)) { 330 | options.split = "down" 331 | } 332 | break 333 | case "Split Left": 334 | if (!("split" in options)) { 335 | options.split = "left" 336 | } 337 | break 338 | case "Split Right": 339 | if (!("split" in options)) { 340 | options.split = "right" 341 | } 342 | break 343 | case "Bottom Dock": { 344 | const pane = atom.workspace.getBottomDock().getActivePane() 345 | if (pane && !("pane" in options)) { 346 | options.pane = pane 347 | } 348 | break 349 | } 350 | case "Left Dock": { 351 | const pane = atom.workspace.getLeftDock().getActivePane() 352 | if (pane && !("pane" in options)) { 353 | options.pane = pane 354 | } 355 | break 356 | } 357 | case "Right Dock": { 358 | const pane = atom.workspace.getRightDock().getActivePane() 359 | if (pane && !("pane" in options)) { 360 | options.pane = pane 361 | } 362 | break 363 | } 364 | } 365 | return options 366 | } 367 | 368 | getPath(target: EventTarget | null): string | null | undefined { 369 | if (!target || !(target instanceof HTMLElement)) { 370 | const paths = atom.project.getPaths() 371 | if (paths && paths.length > 0) { 372 | return paths[0] 373 | } 374 | return null 375 | } 376 | 377 | const treeView = target.closest(".tree-view") 378 | if (treeView) { 379 | // called from treeview 380 | const selected = treeView.querySelector(".selected > .list-item > .name, .selected > .name") as HTMLElement 381 | if (selected) { 382 | return selected.dataset.path 383 | } 384 | return null 385 | } 386 | 387 | const tab = target.closest(".tab-bar > .tab") 388 | if (tab) { 389 | // called from tab 390 | const title = tab.querySelector(".title") as HTMLElement 391 | if (title && title.dataset.path) { 392 | return title.dataset.path 393 | } 394 | return null 395 | } 396 | 397 | const textEditor = target.closest("atom-text-editor") 398 | if (textEditor && typeof textEditor.getModel === "function") { 399 | // called from atom-text-editor 400 | const model = textEditor.getModel() 401 | if (model && typeof model.getPath === "function") { 402 | const modelPath = model.getPath() 403 | if (modelPath) { 404 | return modelPath 405 | } 406 | } 407 | return null 408 | } 409 | 410 | return null 411 | } 412 | 413 | /** Function providing service functions offered by 'terminal' service. */ 414 | provideTerminalService() { 415 | // TODO: provide other service functions 416 | return providePlatformIOIDEService() 417 | } 418 | 419 | /** Function providing service functions offered by 'platformioIDETerminal' service. */ 420 | providePlatformIOIDEService() { 421 | return { 422 | updateProcessEnv(vars: Record): void { 423 | for (const name in vars) { 424 | process.env[name] = vars[name] 425 | } 426 | }, 427 | run: (commands: string[]): Promise => { 428 | return this.runCommands(commands) 429 | }, 430 | getTerminalViews: (): TerminalModel[] => { 431 | return [...this.terminalsSet] 432 | }, 433 | open: (): Promise => { 434 | return this.openTerminal() 435 | }, 436 | } 437 | } 438 | 439 | close() { 440 | const terminal = this.getActiveTerminal() 441 | if (terminal) { 442 | terminal.exit() 443 | } 444 | } 445 | 446 | restart() { 447 | const terminal = this.getActiveTerminal() 448 | if (terminal) { 449 | terminal.restartPtyProcess() 450 | } 451 | } 452 | 453 | copy() { 454 | const terminal = this.getActiveTerminal() 455 | if (terminal) { 456 | const text = terminal.copyFromTerminal() 457 | if (text) { 458 | atom.clipboard.write(text) 459 | } 460 | } 461 | } 462 | 463 | paste() { 464 | const terminal = this.getActiveTerminal() 465 | if (terminal) { 466 | terminal.pasteToTerminal(atom.clipboard.read()) 467 | } 468 | } 469 | 470 | async focus() { 471 | const terminal = [...this.terminalsSet].find((t) => t.activeIndex === 0) 472 | if (terminal) { 473 | terminal.focusOnTerminal(true) 474 | } else { 475 | const options = this.addDefaultPosition() 476 | await this.open(this.generateNewUri(), options) 477 | } 478 | } 479 | 480 | unfocus() { 481 | atom.views.getView(atom.workspace).focus() 482 | } 483 | 484 | clear() { 485 | const terminal = this.getActiveTerminal() 486 | if (terminal) { 487 | terminal.clear() 488 | } 489 | } 490 | } 491 | 492 | let terminalInstance: Terminal | null = null 493 | 494 | export { config } from "./config" 495 | 496 | export function getInstance(): Terminal { 497 | if (!terminalInstance) { 498 | terminalInstance = new Terminal() 499 | } 500 | return terminalInstance 501 | } 502 | 503 | export function activate(): void { 504 | return getInstance().activate() 505 | } 506 | 507 | export function deactivate(): void { 508 | if (terminalInstance) { 509 | terminalInstance.destroy() 510 | terminalInstance = null 511 | } 512 | } 513 | 514 | export function deserializeTerminalModel(serializedModel: TerminalModel) { 515 | return getInstance().deserializeTerminalModel(serializedModel) 516 | } 517 | 518 | export function provideTerminalService() { 519 | return getInstance().provideTerminalService() 520 | } 521 | 522 | export function providePlatformIOIDEService() { 523 | return getInstance().providePlatformIOIDEService() 524 | } 525 | -------------------------------------------------------------------------------- /spec/model-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { TerminalModel } from "../dist/model" 4 | 5 | import fs from "fs-extra" 6 | import path from "path" 7 | 8 | import temp from "temp" 9 | 10 | temp.track() 11 | 12 | describe("TerminalModel", () => { 13 | let model, pane, element, tmpdir, uri, terminalsSet 14 | 15 | beforeEach(async () => { 16 | uri = "atomic-terminal://somesessionid/" 17 | terminalsSet = new Set() 18 | model = new TerminalModel({ uri, terminalsSet }) 19 | await model.initializedPromise 20 | pane = jasmine.createSpyObj("pane", ["destroyItem", "getActiveItem", "activateItem"]) 21 | element = jasmine.createSpyObj("element", [ 22 | "destroy", 23 | "refitTerminal", 24 | "focusOnTerminal", 25 | "clickOnCurrentAnchor", 26 | "getCurrentAnchorHref", 27 | "restartPtyProcess", 28 | ]) 29 | element.terminal = jasmine.createSpyObj("atomic-terminal", ["getSelection"]) 30 | element.ptyProcess = jasmine.createSpyObj("ptyProcess", ["write"]) 31 | tmpdir = await temp.mkdir() 32 | }) 33 | 34 | afterEach(async () => { 35 | await temp.cleanup() 36 | }) 37 | 38 | it("constructor with previous active item that has no getPath() method", async () => { 39 | atom.project.setPaths([tmpdir]) 40 | spyOn(atom.workspace, "getActivePaneItem").and.returnValue({}) 41 | const newModel = new TerminalModel({ 42 | uri, 43 | terminalsSet, 44 | }) 45 | await newModel.initializedPromise 46 | expect(newModel.getPath()).toBe(tmpdir) 47 | }) 48 | 49 | it("constructor with previous active item that has getPath() method", async () => { 50 | const previousActiveItem = jasmine.createSpyObj("somemodel", ["getPath"]) 51 | previousActiveItem.getPath.and.returnValue(tmpdir) 52 | spyOn(atom.workspace, "getActivePaneItem").and.returnValue(previousActiveItem) 53 | const newModel = new TerminalModel({ 54 | uri, 55 | terminalsSet, 56 | }) 57 | await newModel.initializedPromise 58 | expect(newModel.getPath()).toBe(tmpdir) 59 | }) 60 | 61 | it("constructor with previous active item that has selectedPath property", async () => { 62 | const previousActiveItem = jasmine.createSpyObj("somemodel", {}, { ["selectedPath"]: "" }) 63 | Object.getOwnPropertyDescriptor(previousActiveItem, "selectedPath").get.and.returnValue(tmpdir) 64 | spyOn(atom.workspace, "getActivePaneItem").and.returnValue(previousActiveItem) 65 | const newModel = new TerminalModel({ 66 | uri, 67 | terminalsSet, 68 | }) 69 | await newModel.initializedPromise 70 | expect(newModel.getPath()).toBe(tmpdir) 71 | }) 72 | 73 | it("constructor with previous active item that has getPath() method returns file path", async () => { 74 | const previousActiveItem = jasmine.createSpyObj("somemodel", ["getPath"]) 75 | const filePath = path.join(tmpdir, "somefile") 76 | await fs.writeFile(filePath, "") 77 | previousActiveItem.getPath.and.returnValue(filePath) 78 | spyOn(atom.workspace, "getActivePaneItem").and.returnValue(previousActiveItem) 79 | const newModel = new TerminalModel({ 80 | uri, 81 | terminalsSet, 82 | }) 83 | await newModel.initializedPromise 84 | expect(newModel.getPath()).toBe(tmpdir) 85 | }) 86 | 87 | it("constructor with previous active item that has selectedPath() property that returns file path", async () => { 88 | const previousActiveItem = jasmine.createSpyObj("somemodel", {}, { ["selectedPath"]: "" }) 89 | const filePath = path.join(tmpdir, "somefile") 90 | await fs.writeFile(filePath, "") 91 | Object.getOwnPropertyDescriptor(previousActiveItem, "selectedPath").get.and.returnValue(filePath) 92 | spyOn(atom.workspace, "getActivePaneItem").and.returnValue(previousActiveItem) 93 | const newModel = new TerminalModel({ 94 | uri, 95 | terminalsSet, 96 | }) 97 | await newModel.initializedPromise 98 | expect(newModel.getPath()).toBe(tmpdir) 99 | }) 100 | 101 | it("constructor with previous active item that has getPath() returning invalid path", async () => { 102 | const dirPath = path.join(tmpdir, "dir") 103 | await fs.mkdir(dirPath) 104 | atom.project.setPaths([dirPath]) 105 | const previousActiveItem = jasmine.createSpyObj("somemodel", ["getPath"]) 106 | previousActiveItem.getPath.and.returnValue(path.join(tmpdir, "non-existent-dir")) 107 | spyOn(atom.workspace, "getActivePaneItem").and.returnValue(previousActiveItem) 108 | const newModel = new TerminalModel({ 109 | uri, 110 | terminalsSet, 111 | }) 112 | await newModel.initializedPromise 113 | expect(newModel.getPath()).toBe(dirPath) 114 | }) 115 | 116 | it("constructor with previous active item that has selectedPath returning invalid path", async () => { 117 | const dirPath = path.join(tmpdir, "dir") 118 | await fs.mkdir(dirPath) 119 | atom.project.setPaths([dirPath]) 120 | const previousActiveItem = jasmine.createSpyObj("somemodel", {}, { ["selectedPath"]: "" }) 121 | Object.getOwnPropertyDescriptor(previousActiveItem, "selectedPath").get.and.returnValue( 122 | path.join(tmpdir, "non-existent-dir") 123 | ) 124 | spyOn(atom.workspace, "getActivePaneItem").and.returnValue(previousActiveItem) 125 | const newModel = new TerminalModel({ 126 | uri, 127 | terminalsSet, 128 | }) 129 | await newModel.initializedPromise 130 | expect(newModel.getPath()).toBe(dirPath) 131 | }) 132 | 133 | it("constructor with previous active item which exists in project path and has getPath", async () => { 134 | const previousActiveItem = jasmine.createSpyObj("somemodel", ["getPath"]) 135 | previousActiveItem.getPath.and.returnValue("/some/dir/file") 136 | spyOn(atom.workspace, "getActivePaneItem").and.returnValue(previousActiveItem) 137 | const expected = ["/some/dir", null] 138 | spyOn(atom.project, "relativizePath").and.returnValue(expected) 139 | const newModel = new TerminalModel({ 140 | uri, 141 | terminalsSet, 142 | }) 143 | await newModel.initializedPromise 144 | expect(newModel.getPath()).toBe(expected[0]) 145 | }) 146 | 147 | it("constructor with previous active item which exists in project path and has selectedPath", async () => { 148 | const previousActiveItem = jasmine.createSpyObj("somemodel", {}, { ["selectedPath"]: "" }) 149 | Object.getOwnPropertyDescriptor(previousActiveItem, "selectedPath").get.and.returnValue("/some/dir/file") 150 | spyOn(atom.workspace, "getActivePaneItem").and.returnValue(previousActiveItem) 151 | const expected = ["/some/dir", null] 152 | spyOn(atom.project, "relativizePath").and.returnValue(expected) 153 | const newModel = new TerminalModel({ 154 | uri, 155 | terminalsSet, 156 | }) 157 | await newModel.initializedPromise 158 | expect(newModel.getPath()).toBe(expected[0]) 159 | }) 160 | 161 | it("constructor with target cwd", async () => { 162 | const expected = __dirname 163 | const url = new URL(uri) 164 | url.searchParams.set("cwd", __filename) 165 | const newModel = new TerminalModel({ 166 | uri: url.href, 167 | terminalsSet, 168 | }) 169 | await newModel.initializedPromise 170 | expect(newModel.getPath()).toBe(expected) 171 | }) 172 | 173 | it("serialize()", async () => { 174 | expect(model.serialize()).toEqual({ 175 | deserializer: "TerminalModel", 176 | version: "1.0.0", 177 | uri, 178 | }) 179 | }) 180 | 181 | it("destroy() check element is destroyed when set", () => { 182 | model.element = element 183 | model.destroy() 184 | expect(model.element.destroy).toHaveBeenCalled() 185 | }) 186 | 187 | it("destroy() check model removed from terminalsSet", () => { 188 | expect(terminalsSet.has(model)).toBe(true) 189 | model.destroy() 190 | expect(terminalsSet.has(model)).toBe(false) 191 | }) 192 | 193 | it("getTitle() with default title", () => { 194 | expect(model.getTitle()).toBe("Terminal") 195 | }) 196 | 197 | it("getTitle() with new title", () => { 198 | const expected = "some new title" 199 | model.title = expected 200 | expect(model.getTitle()).toBe(expected) 201 | }) 202 | 203 | it("getTitle() when active", () => { 204 | spyOn(model, "isActiveTerminal").and.returnValue(true) 205 | expect(model.getTitle()).toBe("* Terminal") 206 | }) 207 | 208 | it("getElement()", () => { 209 | const expected = { somekey: "somevalue" } 210 | model.element = expected 211 | expect(model.getElement()).toBe(expected) 212 | }) 213 | 214 | it("getURI()", async () => { 215 | expect(model.getURI()).toBe(uri) 216 | }) 217 | 218 | it("getLongTitle() with default title", () => { 219 | expect(model.getLongTitle()).toBe("Terminal") 220 | }) 221 | 222 | it("getLongTitle() with new title", () => { 223 | const expected = "Terminal (some new title)" 224 | model.title = "some new title" 225 | expect(model.getLongTitle()).toBe(expected) 226 | }) 227 | 228 | it("onDidChangeTitle()", () => { 229 | const spy = jasmine.createSpy("spy") 230 | const disposable = model.onDidChangeTitle(spy) 231 | const expected = "new title" 232 | model.emitter.emit("did-change-title", expected) 233 | expect(spy).toHaveBeenCalledWith(expected) 234 | disposable.dispose() 235 | }) 236 | 237 | it("getIconName()", () => { 238 | expect(model.getIconName()).toBe("terminal") 239 | }) 240 | 241 | it("isModified()", () => { 242 | expect(model.isModified()).toBe(false) 243 | }) 244 | 245 | it("isModified() modified attribute set to true", () => { 246 | model.modified = true 247 | expect(model.isModified()).toBe(true) 248 | }) 249 | 250 | it("getPath() cwd set", () => { 251 | const expected = "/some/dir" 252 | model.cwd = expected 253 | expect(model.getPath()).toBe(expected) 254 | }) 255 | 256 | it("onDidChangeModified()", () => { 257 | const spy = jasmine.createSpy("spy") 258 | const disposable = model.onDidChangeModified(spy) 259 | const expected = true 260 | model.emitter.emit("did-change-modified", expected) 261 | expect(spy).toHaveBeenCalledWith(expected) 262 | disposable.dispose() 263 | }) 264 | 265 | it("handleNewDataArrival() current item is active item", () => { 266 | pane.getActiveItem.and.returnValue(model) 267 | model.pane = pane 268 | model.handleNewDataArrival() 269 | expect(model.modified).toBe(false) 270 | }) 271 | 272 | it("handleNewDataArrival() current item is not active item", () => { 273 | pane.getActiveItem.and.returnValue({}) 274 | model.pane = pane 275 | model.handleNewDataArrival() 276 | expect(model.modified).toBe(true) 277 | }) 278 | 279 | it("handleNewDataArrival() current item is not in any pane", () => { 280 | model.pane = null 281 | model.handleNewDataArrival() 282 | expect(model.modified).toBe(true) 283 | }) 284 | 285 | it("handleNewDataArrival() model initially has no pane set", () => { 286 | pane.getActiveItem.and.returnValue({}) 287 | spyOn(atom.workspace, "paneForItem").and.returnValue(pane) 288 | model.handleNewDataArrival() 289 | expect(atom.workspace.paneForItem).toHaveBeenCalled() 290 | }) 291 | 292 | it("handleNewDataArrival() modified value of false not changed", () => { 293 | pane.getActiveItem.and.returnValue(model) 294 | model.pane = pane 295 | spyOn(model.emitter, "emit") 296 | model.handleNewDataArrival() 297 | expect(model.emitter.emit).toHaveBeenCalledTimes(0) 298 | }) 299 | 300 | it("handleNewDataArrival() modified value of true not changed", () => { 301 | pane.getActiveItem.and.returnValue({}) 302 | model.pane = pane 303 | model.modified = true 304 | spyOn(model.emitter, "emit") 305 | model.handleNewDataArrival() 306 | expect(model.emitter.emit).toHaveBeenCalledTimes(0) 307 | }) 308 | 309 | it("handleNewDataArrival() modified value changed", () => { 310 | pane.getActiveItem.and.returnValue({}) 311 | model.pane = pane 312 | spyOn(model.emitter, "emit") 313 | model.handleNewDataArrival() 314 | expect(model.emitter.emit).toHaveBeenCalled() 315 | }) 316 | 317 | it("getSessionId()", async () => { 318 | expect(model.getSessionId()).toBe("somesessionid") 319 | }) 320 | 321 | it("refitTerminal() without element set", () => { 322 | // Should just work. 323 | model.refitTerminal() 324 | }) 325 | 326 | it("refitTerminal() with element set", () => { 327 | model.element = element 328 | model.refitTerminal() 329 | expect(model.element.refitTerminal).toHaveBeenCalled() 330 | }) 331 | 332 | it("focusOnTerminal()", () => { 333 | model.element = element 334 | model.focusOnTerminal() 335 | expect(model.element.focusOnTerminal).toHaveBeenCalled() 336 | }) 337 | 338 | it("focusOnTerminal() reset modified value old modified value was false", () => { 339 | model.element = element 340 | model.focusOnTerminal() 341 | expect(model.modified).toBe(false) 342 | }) 343 | 344 | it("focusOnTerminal() reset modified value old modified value was true", () => { 345 | model.element = element 346 | model.modified = true 347 | model.focusOnTerminal() 348 | expect(model.modified).toBe(false) 349 | }) 350 | 351 | it("focusOnTerminal() no event emitted old modified value was false", () => { 352 | model.element = element 353 | spyOn(model.emitter, "emit") 354 | model.focusOnTerminal() 355 | expect(model.emitter.emit).toHaveBeenCalledTimes(0) 356 | }) 357 | 358 | it("focusOnTerminal() event emitted old modified value was true", () => { 359 | model.element = element 360 | model.modified = true 361 | spyOn(model.emitter, "emit") 362 | model.focusOnTerminal() 363 | expect(model.emitter.emit).toHaveBeenCalled() 364 | }) 365 | 366 | it("focusOnTerminal() activates the pane item", () => { 367 | model.element = element 368 | model.pane = pane 369 | model.focusOnTerminal() 370 | expect(model.pane.activateItem).toHaveBeenCalledWith(model) 371 | }) 372 | 373 | it("focusOnTerminal() passes along double", () => { 374 | model.element = element 375 | model.focusOnTerminal(true) 376 | expect(model.element.focusOnTerminal).toHaveBeenCalledWith(true) 377 | }) 378 | 379 | it("exit()", () => { 380 | model.pane = pane 381 | model.exit() 382 | expect(model.pane.destroyItem.calls.allArgs()).toEqual([[model, true]]) 383 | }) 384 | 385 | it("restartPtyProcess() no element set", () => { 386 | model.restartPtyProcess() 387 | expect(element.restartPtyProcess).not.toHaveBeenCalled() 388 | }) 389 | 390 | it("restartPtyProcess() element set", () => { 391 | model.element = element 392 | model.restartPtyProcess() 393 | expect(model.element.restartPtyProcess).toHaveBeenCalled() 394 | }) 395 | 396 | it("copyFromTerminal()", () => { 397 | model.element = element 398 | model.copyFromTerminal() 399 | expect(model.element.terminal.getSelection).toHaveBeenCalled() 400 | }) 401 | 402 | it("runCommand(cmd)", () => { 403 | model.element = element 404 | const expectedText = "some text" 405 | model.runCommand(expectedText) 406 | expect(model.element.ptyProcess.write.calls.allArgs()).toEqual([ 407 | [expectedText + (process.platform === "win32" ? "\r" : "\n")], 408 | ]) 409 | }) 410 | 411 | it("pasteToTerminal(text)", () => { 412 | model.element = element 413 | const expectedText = "some text" 414 | model.pasteToTerminal(expectedText) 415 | expect(model.element.ptyProcess.write.calls.allArgs()).toEqual([[expectedText]]) 416 | }) 417 | 418 | it("setActive()", async function () { 419 | const activePane = atom.workspace.getCenter().getActivePane() 420 | const newTerminalsSet = new Set() 421 | const model1 = new TerminalModel({ 422 | uri, 423 | terminalsSet: newTerminalsSet, 424 | }) 425 | await model1.initializedPromise 426 | activePane.addItem(model1) 427 | model1.setNewPane(activePane) 428 | const model2 = new TerminalModel({ 429 | uri, 430 | terminalsSet: newTerminalsSet, 431 | }) 432 | await model2.initializedPromise 433 | activePane.addItem(model2) 434 | model2.setNewPane(activePane) 435 | expect(model1.activeIndex).toBe(0) 436 | expect(model2.activeIndex).toBe(1) 437 | model2.setActive() 438 | expect(model1.activeIndex).toBe(1) 439 | expect(model2.activeIndex).toBe(0) 440 | }) 441 | 442 | describe("setNewPane", () => { 443 | it("(mock)", async () => { 444 | const expected = { getContainer: () => ({ getLocation: () => {} }) } 445 | model.setNewPane(expected) 446 | expect(model.pane).toBe(expected) 447 | expect(model.dock).toBe(undefined) 448 | }) 449 | 450 | it("(center)", async () => { 451 | const activePane = atom.workspace.getCenter().getActivePane() 452 | model.setNewPane(activePane) 453 | expect(model.pane).toBe(activePane) 454 | expect(model.dock).toBe(undefined) 455 | }) 456 | 457 | it("(left)", async () => { 458 | const dock = atom.workspace.getLeftDock() 459 | const activePane = dock.getActivePane() 460 | model.setNewPane(activePane) 461 | expect(model.pane).toBe(activePane) 462 | expect(model.dock).toBe(dock) 463 | }) 464 | 465 | it("(right)", async () => { 466 | const dock = atom.workspace.getRightDock() 467 | const activePane = dock.getActivePane() 468 | model.setNewPane(activePane) 469 | expect(model.pane).toBe(activePane) 470 | expect(model.dock).toBe(dock) 471 | }) 472 | 473 | it("(bottom)", async () => { 474 | const dock = atom.workspace.getBottomDock() 475 | const activePane = dock.getActivePane() 476 | model.setNewPane(activePane) 477 | expect(model.pane).toBe(activePane) 478 | expect(model.dock).toBe(dock) 479 | }) 480 | }) 481 | 482 | it("isVisible() in pane", () => { 483 | const activePane = atom.workspace.getCenter().getActivePane() 484 | model.setNewPane(activePane) 485 | expect(model.isVisible()).toBe(false) 486 | activePane.setActiveItem(model) 487 | expect(model.isVisible()).toBe(true) 488 | }) 489 | 490 | it("isVisible() in dock", () => { 491 | const dock = atom.workspace.getBottomDock() 492 | const activePane = dock.getActivePane() 493 | model.setNewPane(activePane) 494 | activePane.setActiveItem(model) 495 | expect(model.isVisible()).toBe(false) 496 | dock.show() 497 | expect(model.isVisible()).toBe(true) 498 | }) 499 | 500 | it("isActiveTerminal() visible and active", () => { 501 | model.activeIndex = 0 502 | spyOn(model, "isVisible").and.returnValue(true) 503 | expect(model.isActiveTerminal()).toBe(true) 504 | }) 505 | 506 | it("isActiveTerminal() visible and not active", () => { 507 | model.activeIndex = 1 508 | spyOn(model, "isVisible").and.returnValue(true) 509 | expect(model.isActiveTerminal()).toBe(false) 510 | }) 511 | 512 | it("isActiveTerminal() invisible and active", () => { 513 | model.activeIndex = 0 514 | spyOn(model, "isVisible").and.returnValue(false) 515 | expect(model.isActiveTerminal()).toBe(false) 516 | }) 517 | 518 | it("isActiveTerminal() allowHiddenToStayActive", () => { 519 | atom.config.set("atomic-terminal.allowHiddenToStayActive", true) 520 | model.activeIndex = 0 521 | spyOn(model, "isVisible").and.returnValue(false) 522 | expect(model.isActiveTerminal()).toBe(true) 523 | }) 524 | 525 | it("isTerminalModel() item is TerminalModel", () => { 526 | expect(TerminalModel.isTerminalModel(model)).toBe(true) 527 | }) 528 | 529 | it("isTerminalModel() item is not TerminalModel", () => { 530 | const item = document.createElement("div") 531 | expect(TerminalModel.isTerminalModel(item)).toBe(false) 532 | }) 533 | 534 | describe("recalculateActive()", () => { 535 | const createTerminals = (num = 1) => { 536 | const terminals = [] 537 | for (let i = 0; i < num; i++) { 538 | terminals.push({ 539 | activeIndex: i, 540 | isVisible() {}, 541 | emitter: { 542 | emit() {}, 543 | }, 544 | title: `title ${i}`, 545 | }) 546 | } 547 | return terminals 548 | } 549 | 550 | it("active first", () => { 551 | const terminals = createTerminals(2) 552 | TerminalModel.recalculateActive(new Set(terminals), terminals[1]) 553 | expect(terminals[0].activeIndex).toBe(1) 554 | expect(terminals[1].activeIndex).toBe(0) 555 | }) 556 | 557 | it("visible before hidden", () => { 558 | const terminals = createTerminals(2) 559 | spyOn(terminals[1], "isVisible").and.returnValue(true) 560 | TerminalModel.recalculateActive(new Set(terminals)) 561 | expect(terminals[0].activeIndex).toBe(1) 562 | expect(terminals[1].activeIndex).toBe(0) 563 | }) 564 | 565 | it("allowHiddenToStayActive", () => { 566 | atom.config.set("atomic-terminal.allowHiddenToStayActive", true) 567 | const terminals = createTerminals(2) 568 | spyOn(terminals[1], "isVisible").and.returnValue(true) 569 | TerminalModel.recalculateActive(new Set(terminals)) 570 | expect(terminals[0].activeIndex).toBe(0) 571 | expect(terminals[1].activeIndex).toBe(1) 572 | }) 573 | 574 | it("lower active index first", () => { 575 | const terminals = createTerminals(2) 576 | terminals[0].activeIndex = 1 577 | terminals[1].activeIndex = 0 578 | TerminalModel.recalculateActive(new Set(terminals)) 579 | expect(terminals[0].activeIndex).toBe(1) 580 | expect(terminals[1].activeIndex).toBe(0) 581 | }) 582 | 583 | it("emit did-change-title", () => { 584 | const terminals = createTerminals(2) 585 | spyOn(terminals[0].emitter, "emit") 586 | spyOn(terminals[1].emitter, "emit") 587 | TerminalModel.recalculateActive(new Set(terminals)) 588 | expect(terminals[0].emitter.emit).toHaveBeenCalledWith("did-change-title", "title 0") 589 | expect(terminals[1].emitter.emit).toHaveBeenCalledWith("did-change-title", "title 1") 590 | }) 591 | }) 592 | }) 593 | -------------------------------------------------------------------------------- /spec/themes-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import { getTheme } from "../dist/themes" 4 | 5 | describe("themes", () => { 6 | describe("getTheme()", () => { 7 | it("Custom", () => { 8 | atom.config.set("atomic-terminal.colors.theme", "Custom") 9 | expect(getTheme()).toEqual({ 10 | background: "#000000", 11 | foreground: "#ffffff", 12 | selection: "#4d4d4d", 13 | cursor: "#ffffff", 14 | cursorAccent: "#000000", 15 | black: "#2e3436", 16 | red: "#cc0000", 17 | green: "#4e9a06", 18 | yellow: "#c4a000", 19 | blue: "#3465a4", 20 | magenta: "#75507b", 21 | cyan: "#06989a", 22 | white: "#d3d7cf", 23 | brightBlack: "#555753", 24 | brightRed: "#ef2929", 25 | brightGreen: "#8ae234", 26 | brightYellow: "#fce94f", 27 | brightBlue: "#729fcf", 28 | brightMagenta: "#ad7fa8", 29 | brightCyan: "#34e2e2", 30 | brightWhite: "#eeeeec", 31 | }) 32 | }) 33 | 34 | it("Atom Dark", () => { 35 | atom.config.set("atomic-terminal.colors.theme", "Atom Dark") 36 | expect(getTheme()).toEqual({ 37 | background: "#1d1f21", 38 | foreground: "#c5c8c6", 39 | selection: "#999999", 40 | cursor: "#ffffff", 41 | cursorAccent: "#000000", 42 | black: "#2e3436", 43 | red: "#cc0000", 44 | green: "#4e9a06", 45 | yellow: "#c4a000", 46 | blue: "#3465a4", 47 | magenta: "#75507b", 48 | cyan: "#06989a", 49 | white: "#d3d7cf", 50 | brightBlack: "#555753", 51 | brightRed: "#ef2929", 52 | brightGreen: "#8ae234", 53 | brightYellow: "#fce94f", 54 | brightBlue: "#729fcf", 55 | brightMagenta: "#ad7fa8", 56 | brightCyan: "#34e2e2", 57 | brightWhite: "#eeeeec", 58 | }) 59 | }) 60 | 61 | it("Atom Light", () => { 62 | atom.config.set("atomic-terminal.colors.theme", "Atom Light") 63 | expect(getTheme()).toEqual({ 64 | background: "#ffffff", 65 | foreground: "#555555", 66 | selection: "#afc4da", 67 | cursor: "#000000", 68 | cursorAccent: "#000000", 69 | black: "#2e3436", 70 | red: "#cc0000", 71 | green: "#4e9a06", 72 | yellow: "#c4a000", 73 | blue: "#3465a4", 74 | magenta: "#75507b", 75 | cyan: "#06989a", 76 | white: "#d3d7cf", 77 | brightBlack: "#555753", 78 | brightRed: "#ef2929", 79 | brightGreen: "#8ae234", 80 | brightYellow: "#fce94f", 81 | brightBlue: "#729fcf", 82 | brightMagenta: "#ad7fa8", 83 | brightCyan: "#34e2e2", 84 | brightWhite: "#eeeeec", 85 | }) 86 | }) 87 | 88 | it("Base16 Tomorrow Dark", () => { 89 | atom.config.set("atomic-terminal.colors.theme", "Base16 Tomorrow Dark") 90 | expect(getTheme()).toEqual({ 91 | background: "#1d1f21", 92 | foreground: "#c5c8c6", 93 | selection: "#b4b7b4", 94 | // selectionForeground: '#e0e0e0', 95 | cursor: "#ffffff", 96 | cursorAccent: "#000000", 97 | black: "#2e3436", 98 | red: "#cc0000", 99 | green: "#4e9a06", 100 | yellow: "#c4a000", 101 | blue: "#3465a4", 102 | magenta: "#75507b", 103 | cyan: "#06989a", 104 | white: "#d3d7cf", 105 | brightBlack: "#555753", 106 | brightRed: "#ef2929", 107 | brightGreen: "#8ae234", 108 | brightYellow: "#fce94f", 109 | brightBlue: "#729fcf", 110 | brightMagenta: "#ad7fa8", 111 | brightCyan: "#34e2e2", 112 | brightWhite: "#eeeeec", 113 | }) 114 | }) 115 | 116 | it("Base16 Tomorrow Light", () => { 117 | atom.config.set("atomic-terminal.colors.theme", "Base16 Tomorrow Light") 118 | expect(getTheme()).toEqual({ 119 | background: "#ffffff", 120 | foreground: "#1d1f21", 121 | selection: "#282a2e", 122 | // selectionForeground: '#e0e0e0', 123 | cursor: "#1d1f21", 124 | cursorAccent: "#000000", 125 | black: "#2e3436", 126 | red: "#cc0000", 127 | green: "#4e9a06", 128 | yellow: "#c4a000", 129 | blue: "#3465a4", 130 | magenta: "#75507b", 131 | cyan: "#06989a", 132 | white: "#d3d7cf", 133 | brightBlack: "#555753", 134 | brightRed: "#ef2929", 135 | brightGreen: "#8ae234", 136 | brightYellow: "#fce94f", 137 | brightBlue: "#729fcf", 138 | brightMagenta: "#ad7fa8", 139 | brightCyan: "#34e2e2", 140 | brightWhite: "#eeeeec", 141 | }) 142 | }) 143 | 144 | it("Christmas", () => { 145 | atom.config.set("atomic-terminal.colors.theme", "Christmas") 146 | expect(getTheme()).toEqual({ 147 | background: "#0c0047", 148 | foreground: "#f81705", 149 | selection: "#298f16", 150 | cursor: "#009f59", 151 | cursorAccent: "#000000", 152 | black: "#2e3436", 153 | red: "#cc0000", 154 | green: "#4e9a06", 155 | yellow: "#c4a000", 156 | blue: "#3465a4", 157 | magenta: "#75507b", 158 | cyan: "#06989a", 159 | white: "#d3d7cf", 160 | brightBlack: "#555753", 161 | brightRed: "#ef2929", 162 | brightGreen: "#8ae234", 163 | brightYellow: "#fce94f", 164 | brightBlue: "#729fcf", 165 | brightMagenta: "#ad7fa8", 166 | brightCyan: "#34e2e2", 167 | brightWhite: "#eeeeec", 168 | }) 169 | }) 170 | 171 | it("City Lights", () => { 172 | atom.config.set("atomic-terminal.colors.theme", "City Lights") 173 | expect(getTheme()).toEqual({ 174 | background: "#181d23", 175 | foreground: "#666d81", 176 | selection: "#2a2f38", 177 | // selectionForeground: '#b7c5d3', 178 | cursor: "#528bff", 179 | cursorAccent: "#000000", 180 | black: "#2e3436", 181 | red: "#cc0000", 182 | green: "#4e9a06", 183 | yellow: "#c4a000", 184 | blue: "#3465a4", 185 | magenta: "#75507b", 186 | cyan: "#06989a", 187 | white: "#d3d7cf", 188 | brightBlack: "#555753", 189 | brightRed: "#ef2929", 190 | brightGreen: "#8ae234", 191 | brightYellow: "#fce94f", 192 | brightBlue: "#729fcf", 193 | brightMagenta: "#ad7fa8", 194 | brightCyan: "#34e2e2", 195 | brightWhite: "#eeeeec", 196 | }) 197 | }) 198 | 199 | it("Dracula", () => { 200 | atom.config.set("atomic-terminal.colors.theme", "Dracula") 201 | expect(getTheme()).toEqual({ 202 | background: "#1e1f29", 203 | foreground: "white", 204 | selection: "#44475a", 205 | cursor: "#999999", 206 | cursorAccent: "#000000", 207 | black: "#2e3436", 208 | red: "#cc0000", 209 | green: "#4e9a06", 210 | yellow: "#c4a000", 211 | blue: "#3465a4", 212 | magenta: "#75507b", 213 | cyan: "#06989a", 214 | white: "#d3d7cf", 215 | brightBlack: "#555753", 216 | brightRed: "#ef2929", 217 | brightGreen: "#8ae234", 218 | brightYellow: "#fce94f", 219 | brightBlue: "#729fcf", 220 | brightMagenta: "#ad7fa8", 221 | brightCyan: "#34e2e2", 222 | brightWhite: "#eeeeec", 223 | }) 224 | }) 225 | 226 | it("Grass", () => { 227 | atom.config.set("atomic-terminal.colors.theme", "Grass") 228 | expect(getTheme()).toEqual({ 229 | background: "rgb(19, 119, 61)", 230 | foreground: "rgb(255, 240, 165)", 231 | selection: "rgba(182, 73, 38, .99)", 232 | cursor: "rgb(142, 40, 0)", 233 | cursorAccent: "#000000", 234 | black: "#2e3436", 235 | red: "#cc0000", 236 | green: "#4e9a06", 237 | yellow: "#c4a000", 238 | blue: "#3465a4", 239 | magenta: "#75507b", 240 | cyan: "#06989a", 241 | white: "#d3d7cf", 242 | brightBlack: "#555753", 243 | brightRed: "#ef2929", 244 | brightGreen: "#8ae234", 245 | brightYellow: "#fce94f", 246 | brightBlue: "#729fcf", 247 | brightMagenta: "#ad7fa8", 248 | brightCyan: "#34e2e2", 249 | brightWhite: "#eeeeec", 250 | }) 251 | }) 252 | 253 | it("Homebrew", () => { 254 | atom.config.set("atomic-terminal.colors.theme", "Homebrew") 255 | expect(getTheme()).toEqual({ 256 | background: "#000000", 257 | foreground: "rgb(41, 254, 20)", 258 | selection: "rgba(7, 30, 155, .99)", 259 | cursor: "rgb(55, 254, 38)", 260 | cursorAccent: "#000000", 261 | black: "#2e3436", 262 | red: "#cc0000", 263 | green: "#4e9a06", 264 | yellow: "#c4a000", 265 | blue: "#3465a4", 266 | magenta: "#75507b", 267 | cyan: "#06989a", 268 | white: "#d3d7cf", 269 | brightBlack: "#555753", 270 | brightRed: "#ef2929", 271 | brightGreen: "#8ae234", 272 | brightYellow: "#fce94f", 273 | brightBlue: "#729fcf", 274 | brightMagenta: "#ad7fa8", 275 | brightCyan: "#34e2e2", 276 | brightWhite: "#eeeeec", 277 | }) 278 | }) 279 | 280 | it("Inverse", () => { 281 | atom.config.set("atomic-terminal.colors.theme", "Inverse") 282 | expect(getTheme()).toEqual({ 283 | background: "#ffffff", 284 | foreground: "#000000", 285 | selection: "rgba(178, 215, 255, .99)", 286 | cursor: "rgb(146, 146, 146)", 287 | cursorAccent: "#000000", 288 | black: "#2e3436", 289 | red: "#cc0000", 290 | green: "#4e9a06", 291 | yellow: "#c4a000", 292 | blue: "#3465a4", 293 | magenta: "#75507b", 294 | cyan: "#06989a", 295 | white: "#d3d7cf", 296 | brightBlack: "#555753", 297 | brightRed: "#ef2929", 298 | brightGreen: "#8ae234", 299 | brightYellow: "#fce94f", 300 | brightBlue: "#729fcf", 301 | brightMagenta: "#ad7fa8", 302 | brightCyan: "#34e2e2", 303 | brightWhite: "#eeeeec", 304 | }) 305 | }) 306 | 307 | it("Linux", () => { 308 | atom.config.set("atomic-terminal.colors.theme", "Linux") 309 | expect(getTheme()).toEqual({ 310 | background: "#000000", 311 | foreground: "rgb(230, 230, 230)", 312 | selection: "rgba(155, 30, 7, .99)", 313 | cursor: "rgb(200, 20, 25)", 314 | cursorAccent: "#000000", 315 | black: "#2e3436", 316 | red: "#cc0000", 317 | green: "#4e9a06", 318 | yellow: "#c4a000", 319 | blue: "#3465a4", 320 | magenta: "#75507b", 321 | cyan: "#06989a", 322 | white: "#d3d7cf", 323 | brightBlack: "#555753", 324 | brightRed: "#ef2929", 325 | brightGreen: "#8ae234", 326 | brightYellow: "#fce94f", 327 | brightBlue: "#729fcf", 328 | brightMagenta: "#ad7fa8", 329 | brightCyan: "#34e2e2", 330 | brightWhite: "#eeeeec", 331 | }) 332 | }) 333 | 334 | it("Man Page", () => { 335 | atom.config.set("atomic-terminal.colors.theme", "Man Page") 336 | expect(getTheme()).toEqual({ 337 | background: "rgb(254, 244, 156)", 338 | foreground: "black", 339 | selection: "rgba(178, 215, 255, .99)", 340 | cursor: "rgb(146, 146, 146)", 341 | cursorAccent: "#000000", 342 | black: "#2e3436", 343 | red: "#cc0000", 344 | green: "#4e9a06", 345 | yellow: "#c4a000", 346 | blue: "#3465a4", 347 | magenta: "#75507b", 348 | cyan: "#06989a", 349 | white: "#d3d7cf", 350 | brightBlack: "#555753", 351 | brightRed: "#ef2929", 352 | brightGreen: "#8ae234", 353 | brightYellow: "#fce94f", 354 | brightBlue: "#729fcf", 355 | brightMagenta: "#ad7fa8", 356 | brightCyan: "#34e2e2", 357 | brightWhite: "#eeeeec", 358 | }) 359 | }) 360 | 361 | it("Novel", () => { 362 | atom.config.set("atomic-terminal.colors.theme", "Novel") 363 | expect(getTheme()).toEqual({ 364 | background: "rgb(223, 219, 196)", 365 | foreground: "rgb(77, 47, 46)", 366 | selection: "rgba(155, 153, 122, .99)", 367 | cursor: "rgb(115, 99, 89)", 368 | cursorAccent: "#000000", 369 | black: "#2e3436", 370 | red: "#cc0000", 371 | green: "#4e9a06", 372 | yellow: "#c4a000", 373 | blue: "#3465a4", 374 | magenta: "#75507b", 375 | cyan: "#06989a", 376 | white: "#d3d7cf", 377 | brightBlack: "#555753", 378 | brightRed: "#ef2929", 379 | brightGreen: "#8ae234", 380 | brightYellow: "#fce94f", 381 | brightBlue: "#729fcf", 382 | brightMagenta: "#ad7fa8", 383 | brightCyan: "#34e2e2", 384 | brightWhite: "#eeeeec", 385 | }) 386 | }) 387 | 388 | it("Ocean", () => { 389 | atom.config.set("atomic-terminal.colors.theme", "Ocean") 390 | expect(getTheme()).toEqual({ 391 | background: "rgb(44, 102, 201)", 392 | foreground: "white", 393 | selection: "rgba(41, 134, 255, .99)", 394 | cursor: "rgb(146, 146, 146)", 395 | cursorAccent: "#000000", 396 | black: "#2e3436", 397 | red: "#cc0000", 398 | green: "#4e9a06", 399 | yellow: "#c4a000", 400 | blue: "#3465a4", 401 | magenta: "#75507b", 402 | cyan: "#06989a", 403 | white: "#d3d7cf", 404 | brightBlack: "#555753", 405 | brightRed: "#ef2929", 406 | brightGreen: "#8ae234", 407 | brightYellow: "#fce94f", 408 | brightBlue: "#729fcf", 409 | brightMagenta: "#ad7fa8", 410 | brightCyan: "#34e2e2", 411 | brightWhite: "#eeeeec", 412 | }) 413 | }) 414 | 415 | it("One Dark", () => { 416 | atom.config.set("atomic-terminal.colors.theme", "One Dark") 417 | expect(getTheme()).toEqual({ 418 | background: "#282c34", 419 | foreground: "#abb2bf", 420 | selection: "#9196a1", 421 | cursor: "#528bff", 422 | cursorAccent: "#000000", 423 | black: "#2e3436", 424 | red: "#cc0000", 425 | green: "#4e9a06", 426 | yellow: "#c4a000", 427 | blue: "#3465a4", 428 | magenta: "#75507b", 429 | cyan: "#06989a", 430 | white: "#d3d7cf", 431 | brightBlack: "#555753", 432 | brightRed: "#ef2929", 433 | brightGreen: "#8ae234", 434 | brightYellow: "#fce94f", 435 | brightBlue: "#729fcf", 436 | brightMagenta: "#ad7fa8", 437 | brightCyan: "#34e2e2", 438 | brightWhite: "#eeeeec", 439 | }) 440 | }) 441 | 442 | it("One Light", () => { 443 | atom.config.set("atomic-terminal.colors.theme", "One Light") 444 | expect(getTheme()).toEqual({ 445 | background: "hsl(230, 1%, 98%)", 446 | foreground: "hsl(230, 8%, 24%)", 447 | selection: "hsl(230, 1%, 90%)", 448 | cursor: "hsl(230, 100%, 66%)", 449 | cursorAccent: "#000000", 450 | black: "#2e3436", 451 | red: "#cc0000", 452 | green: "#4e9a06", 453 | yellow: "#c4a000", 454 | blue: "#3465a4", 455 | magenta: "#75507b", 456 | cyan: "#06989a", 457 | white: "#d3d7cf", 458 | brightBlack: "#555753", 459 | brightRed: "#ef2929", 460 | brightGreen: "#8ae234", 461 | brightYellow: "#fce94f", 462 | brightBlue: "#729fcf", 463 | brightMagenta: "#ad7fa8", 464 | brightCyan: "#34e2e2", 465 | brightWhite: "#eeeeec", 466 | }) 467 | }) 468 | 469 | it("Predawn", () => { 470 | atom.config.set("atomic-terminal.colors.theme", "Predawn") 471 | expect(getTheme()).toEqual({ 472 | background: "#282828", 473 | foreground: "#f1f1f1", 474 | selection: "rgba(255,255,255,0.25)", 475 | cursor: "#f18260", 476 | cursorAccent: "#000000", 477 | black: "#2e3436", 478 | red: "#cc0000", 479 | green: "#4e9a06", 480 | yellow: "#c4a000", 481 | blue: "#3465a4", 482 | magenta: "#75507b", 483 | cyan: "#06989a", 484 | white: "#d3d7cf", 485 | brightBlack: "#555753", 486 | brightRed: "#ef2929", 487 | brightGreen: "#8ae234", 488 | brightYellow: "#fce94f", 489 | brightBlue: "#729fcf", 490 | brightMagenta: "#ad7fa8", 491 | brightCyan: "#34e2e2", 492 | brightWhite: "#eeeeec", 493 | }) 494 | }) 495 | 496 | it("Pro", () => { 497 | atom.config.set("atomic-terminal.colors.theme", "Pro") 498 | expect(getTheme()).toEqual({ 499 | background: "#000000", 500 | foreground: "rgb(244, 244, 244)", 501 | selection: "rgba(82, 82, 82, .99)", 502 | cursor: "rgb(96, 96, 96)", 503 | cursorAccent: "#000000", 504 | black: "#2e3436", 505 | red: "#cc0000", 506 | green: "#4e9a06", 507 | yellow: "#c4a000", 508 | blue: "#3465a4", 509 | magenta: "#75507b", 510 | cyan: "#06989a", 511 | white: "#d3d7cf", 512 | brightBlack: "#555753", 513 | brightRed: "#ef2929", 514 | brightGreen: "#8ae234", 515 | brightYellow: "#fce94f", 516 | brightBlue: "#729fcf", 517 | brightMagenta: "#ad7fa8", 518 | brightCyan: "#34e2e2", 519 | brightWhite: "#eeeeec", 520 | }) 521 | }) 522 | 523 | it("Red Sands", () => { 524 | atom.config.set("atomic-terminal.colors.theme", "Red Sands") 525 | expect(getTheme()).toEqual({ 526 | background: "rgb(143, 53, 39)", 527 | foreground: "rgb(215, 201, 167)", 528 | selection: "rgba(60, 25, 22, .99)", 529 | cursor: "white", 530 | cursorAccent: "#000000", 531 | black: "#2e3436", 532 | red: "#cc0000", 533 | green: "#4e9a06", 534 | yellow: "#c4a000", 535 | blue: "#3465a4", 536 | magenta: "#75507b", 537 | cyan: "#06989a", 538 | white: "#d3d7cf", 539 | brightBlack: "#555753", 540 | brightRed: "#ef2929", 541 | brightGreen: "#8ae234", 542 | brightYellow: "#fce94f", 543 | brightBlue: "#729fcf", 544 | brightMagenta: "#ad7fa8", 545 | brightCyan: "#34e2e2", 546 | brightWhite: "#eeeeec", 547 | }) 548 | }) 549 | 550 | it("Red", () => { 551 | atom.config.set("atomic-terminal.colors.theme", "Red") 552 | expect(getTheme()).toEqual({ 553 | background: "#000000", 554 | foreground: "rgb(255, 38, 14)", 555 | selection: "rgba(7, 30, 155, .99)", 556 | cursor: "rgb(255, 38, 14)", 557 | cursorAccent: "#000000", 558 | black: "#2e3436", 559 | red: "#cc0000", 560 | green: "#4e9a06", 561 | yellow: "#c4a000", 562 | blue: "#3465a4", 563 | magenta: "#75507b", 564 | cyan: "#06989a", 565 | white: "#d3d7cf", 566 | brightBlack: "#555753", 567 | brightRed: "#ef2929", 568 | brightGreen: "#8ae234", 569 | brightYellow: "#fce94f", 570 | brightBlue: "#729fcf", 571 | brightMagenta: "#ad7fa8", 572 | brightCyan: "#34e2e2", 573 | brightWhite: "#eeeeec", 574 | }) 575 | }) 576 | 577 | it("Silver Aerogel", () => { 578 | atom.config.set("atomic-terminal.colors.theme", "Silver Aerogel") 579 | expect(getTheme()).toEqual({ 580 | background: "rgb(146, 146, 146)", 581 | foreground: "#000000", 582 | selection: "rgba(120, 123, 156, .99)", 583 | cursor: "rgb(224, 224, 224)", 584 | cursorAccent: "#000000", 585 | black: "#2e3436", 586 | red: "#cc0000", 587 | green: "#4e9a06", 588 | yellow: "#c4a000", 589 | blue: "#3465a4", 590 | magenta: "#75507b", 591 | cyan: "#06989a", 592 | white: "#d3d7cf", 593 | brightBlack: "#555753", 594 | brightRed: "#ef2929", 595 | brightGreen: "#8ae234", 596 | brightYellow: "#fce94f", 597 | brightBlue: "#729fcf", 598 | brightMagenta: "#ad7fa8", 599 | brightCyan: "#34e2e2", 600 | brightWhite: "#eeeeec", 601 | }) 602 | }) 603 | 604 | it("Solarized Dark", () => { 605 | atom.config.set("atomic-terminal.colors.theme", "Solarized Dark") 606 | expect(getTheme()).toEqual({ 607 | background: "#042029", 608 | foreground: "#708284", 609 | selection: "#839496", 610 | cursor: "#819090", 611 | cursorAccent: "#000000", 612 | black: "#2e3436", 613 | red: "#cc0000", 614 | green: "#4e9a06", 615 | yellow: "#c4a000", 616 | blue: "#3465a4", 617 | magenta: "#75507b", 618 | cyan: "#06989a", 619 | white: "#d3d7cf", 620 | brightBlack: "#555753", 621 | brightRed: "#ef2929", 622 | brightGreen: "#8ae234", 623 | brightYellow: "#fce94f", 624 | brightBlue: "#729fcf", 625 | brightMagenta: "#ad7fa8", 626 | brightCyan: "#34e2e2", 627 | brightWhite: "#eeeeec", 628 | }) 629 | }) 630 | 631 | it("Solarized Light", () => { 632 | atom.config.set("atomic-terminal.colors.theme", "Solarized Light") 633 | expect(getTheme()).toEqual({ 634 | background: "#fdf6e3", 635 | foreground: "#657a81", 636 | selection: "#ece7d5", 637 | cursor: "#586e75", 638 | cursorAccent: "#000000", 639 | black: "#2e3436", 640 | red: "#cc0000", 641 | green: "#4e9a06", 642 | yellow: "#c4a000", 643 | blue: "#3465a4", 644 | magenta: "#75507b", 645 | cyan: "#06989a", 646 | white: "#d3d7cf", 647 | brightBlack: "#555753", 648 | brightRed: "#ef2929", 649 | brightGreen: "#8ae234", 650 | brightYellow: "#fce94f", 651 | brightBlue: "#729fcf", 652 | brightMagenta: "#ad7fa8", 653 | brightCyan: "#34e2e2", 654 | brightWhite: "#eeeeec", 655 | }) 656 | }) 657 | 658 | it("Solid Colors", () => { 659 | atom.config.set("atomic-terminal.colors.theme", "Solid Colors") 660 | expect(getTheme()).toEqual({ 661 | background: "rgb(120, 132, 151)", 662 | foreground: "#000000", 663 | selection: "rgba(178, 215, 255, .99)", 664 | cursor: "#ffffff", 665 | cursorAccent: "#000000", 666 | black: "#2e3436", 667 | red: "#cc0000", 668 | green: "#4e9a06", 669 | yellow: "#c4a000", 670 | blue: "#3465a4", 671 | magenta: "#75507b", 672 | cyan: "#06989a", 673 | white: "#d3d7cf", 674 | brightBlack: "#555753", 675 | brightRed: "#ef2929", 676 | brightGreen: "#8ae234", 677 | brightYellow: "#fce94f", 678 | brightBlue: "#729fcf", 679 | brightMagenta: "#ad7fa8", 680 | brightCyan: "#34e2e2", 681 | brightWhite: "#eeeeec", 682 | }) 683 | }) 684 | 685 | it("Standard", () => { 686 | atom.config.set("atomic-terminal.colors.theme", "Standard") 687 | const { style } = document.documentElement 688 | style.setProperty("--standard-app-background-color", "#000000") 689 | style.setProperty("--standard-text-color", "#ffffff") 690 | style.setProperty("--standard-background-color-selected", "#4d4d4d") 691 | style.setProperty("--standard-text-color-highlight", "#ffffff") 692 | style.setProperty("--standard-color-black", "#2e3436") 693 | style.setProperty("--standard-color-red", "#cc0000") 694 | style.setProperty("--standard-color-green", "#4e9a06") 695 | style.setProperty("--standard-color-yellow", "#c4a000") 696 | style.setProperty("--standard-color-blue", "#3465a4") 697 | style.setProperty("--standard-color-magenta", "#75507b") 698 | style.setProperty("--standard-color-cyan", "#06989a") 699 | style.setProperty("--standard-color-white", "#d3d7cf") 700 | style.setProperty("--standard-color-bright-black", "#555753") 701 | style.setProperty("--standard-color-bright-red", "#ef2929") 702 | style.setProperty("--standard-color-bright-green", "#8ae234") 703 | style.setProperty("--standard-color-bright-yellow", "#fce94f") 704 | style.setProperty("--standard-color-bright-blue", "#729fcf") 705 | style.setProperty("--standard-color-bright-magenta", "#ad7fa8") 706 | style.setProperty("--standard-color-bright-cyan", "#34e2e2") 707 | style.setProperty("--standard-color-bright-white", "#eeeeec") 708 | 709 | expect(getTheme()).toEqual({ 710 | background: "#000000", 711 | foreground: "#ffffff", 712 | selection: "#4d4d4d", 713 | cursor: "#ffffff", 714 | cursorAccent: "#000000", 715 | black: "#2e3436", 716 | red: "#cc0000", 717 | green: "#4e9a06", 718 | yellow: "#c4a000", 719 | blue: "#3465a4", 720 | magenta: "#75507b", 721 | cyan: "#06989a", 722 | white: "#d3d7cf", 723 | brightBlack: "#555753", 724 | brightRed: "#ef2929", 725 | brightGreen: "#8ae234", 726 | brightYellow: "#fce94f", 727 | brightBlue: "#729fcf", 728 | brightMagenta: "#ad7fa8", 729 | brightCyan: "#34e2e2", 730 | brightWhite: "#eeeeec", 731 | }) 732 | }) 733 | }) 734 | }) 735 | -------------------------------------------------------------------------------- /spec/element-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import * as nodePty from "node-pty-prebuilt-multiarch" 4 | import { shell } from "electron" 5 | 6 | import { config } from "../dist/config" 7 | import { getTheme } from "../dist/themes" 8 | import { createTerminalElement } from "../dist/element" 9 | import { TerminalModel } from "../dist/model" 10 | 11 | import path from "path" 12 | import temp from "temp" 13 | 14 | temp.track() 15 | 16 | describe("TerminalElement", () => { 17 | const savedPlatform = process.platform 18 | let element, tmpdir 19 | 20 | const createNewElement = async (uri = "atomic-terminal://somesessionid/") => { 21 | const terminalsSet = new Set() 22 | const model = new TerminalModel({ 23 | uri, 24 | terminalsSet, 25 | }) 26 | await model.initializedPromise 27 | model.pane = jasmine.createSpyObj("pane", ["removeItem", "getActiveItem", "destroyItem"]) 28 | const terminalElement = createTerminalElement() 29 | await terminalElement.initialize(model) 30 | await terminalElement.createTerminal() 31 | return terminalElement 32 | } 33 | 34 | beforeEach(async () => { 35 | const ptyProcess = jasmine.createSpyObj("ptyProcess", ["kill", "write", "resize", "on", "removeAllListeners"]) 36 | ptyProcess.process = jasmine.createSpy("process").and.returnValue("sometestprocess") 37 | spyOn(nodePty, "spawn").and.returnValue(ptyProcess) 38 | spyOn(shell, "openExternal") 39 | element = await createNewElement() 40 | tmpdir = await temp.mkdir() 41 | }) 42 | 43 | afterEach(async () => { 44 | element.destroy() 45 | Object.defineProperty(process, "platform", { 46 | value: savedPlatform, 47 | }) 48 | await temp.cleanup() 49 | }) 50 | 51 | it("initialize(model)", () => { 52 | // Simply test if the terminal has been created. 53 | expect(element.terminal).toBeTruthy() 54 | }) 55 | 56 | it("initialize(model) check session-id", () => { 57 | expect(element.getAttribute("session-id")).toBe("somesessionid") 58 | }) 59 | 60 | it("destroy() check ptyProcess killed", () => { 61 | element.destroy() 62 | expect(element.ptyProcess.kill).toHaveBeenCalled() 63 | }) 64 | 65 | it("destroy() check terminal destroyed", () => { 66 | spyOn(element.terminal, "dispose").and.callThrough() 67 | element.destroy() 68 | expect(element.terminal.dispose).toHaveBeenCalled() 69 | }) 70 | 71 | it("destroy() check disposables disposed", () => { 72 | spyOn(element.disposables, "dispose").and.callThrough() 73 | element.destroy() 74 | expect(element.disposables.dispose).toHaveBeenCalled() 75 | }) 76 | 77 | it("checkPathIsDirectory() no path given", async () => { 78 | const isDirectory = await element.checkPathIsDirectory() 79 | expect(isDirectory).toBe(false) 80 | }) 81 | 82 | it("checkPathIsDirectory() path set to undefined", async () => { 83 | const isDirectory = await element.checkPathIsDirectory(undefined) 84 | expect(isDirectory).toBe(false) 85 | }) 86 | 87 | it("checkPathIsDirectory() path set to null", async () => { 88 | const isDirectory = await element.checkPathIsDirectory(null) 89 | expect(isDirectory).toBe(false) 90 | }) 91 | 92 | it("checkPathIsDirectory() path set to tmpdir", async () => { 93 | const isDirectory = await element.checkPathIsDirectory(tmpdir) 94 | expect(isDirectory).toBe(true) 95 | }) 96 | 97 | it("checkPathIsDirectory() path set to non-existent dir", async () => { 98 | const isDirectory = await element.checkPathIsDirectory(path.join(tmpdir, "non-existent-dir")) 99 | expect(isDirectory).toBe(false) 100 | }) 101 | 102 | it("getCwd()", async () => { 103 | element.model.cwd = tmpdir 104 | const cwd = await element.getCwd() 105 | expect(cwd).toBe(element.model.cwd) 106 | }) 107 | 108 | it("createTerminal() check terminal object", () => { 109 | expect(element.terminal).toBeTruthy() 110 | }) 111 | 112 | it("createTerminal() check ptyProcess object", () => { 113 | expect(element.ptyProcess).toBeTruthy() 114 | }) 115 | 116 | describe("loaded addons", () => { 117 | const { Terminal } = require("xterm") 118 | const { WebLinksAddon } = require("xterm-addon-web-links") 119 | const { WebglAddon } = require("xterm-addon-webgl") 120 | 121 | beforeEach(() => { 122 | spyOn(Terminal.prototype, "loadAddon").and.callThrough() 123 | }) 124 | 125 | it("createTerminal() enable web-link addon", async () => { 126 | atom.config.set("atomic-terminal.webLinks", true) 127 | await createNewElement() 128 | const wasAdded = Terminal.prototype.loadAddon.calls.all().some((call) => { 129 | return call.args[0] instanceof WebLinksAddon 130 | }) 131 | expect(wasAdded).toBe(true) 132 | }) 133 | 134 | it("createTerminal() disable web-link addon", async () => { 135 | atom.config.set("atomic-terminal.webLinks", false) 136 | await createNewElement() 137 | const wasAdded = Terminal.prototype.loadAddon.calls.all().some((call) => { 138 | return call.args[0] instanceof WebLinksAddon 139 | }) 140 | expect(wasAdded).toBe(false) 141 | }) 142 | 143 | if (process.platform !== "linux") { 144 | it("createTerminal() enable webgl addon", async () => { 145 | atom.config.set("atomic-terminal.webgl", true) 146 | await createNewElement() 147 | const wasAdded = Terminal.prototype.loadAddon.calls.all().some((call) => { 148 | return call.args[0] instanceof WebglAddon 149 | }) 150 | expect(wasAdded).toBe(true) 151 | }) 152 | } 153 | 154 | it("createTerminal() disable webgl addon", async () => { 155 | atom.config.set("atomic-terminal.webgl", false) 156 | await createNewElement() 157 | const wasAdded = Terminal.prototype.loadAddon.calls.all().some((call) => { 158 | return call.args[0] instanceof WebglAddon 159 | }) 160 | expect(wasAdded).toBe(false) 161 | }) 162 | }) 163 | 164 | it("restartPtyProcess() check new pty process created", async () => { 165 | const oldPtyProcess = element.ptyProcess 166 | const newPtyProcess = jasmine.createSpyObj("ptyProcess", ["kill", "write", "resize", "on", "removeAllListeners"]) 167 | newPtyProcess.process = jasmine.createSpy("process").and.returnValue("sometestprocess") 168 | nodePty.spawn.and.returnValue(newPtyProcess) 169 | await element.restartPtyProcess() 170 | expect(element.ptyProcess).toBe(newPtyProcess) 171 | expect(oldPtyProcess).not.toBe(element.ptyProcess) 172 | }) 173 | 174 | it("restartPtyProcess() check ptyProcessRunning set to true", async () => { 175 | const newPtyProcess = jasmine.createSpyObj("ptyProcess", ["kill", "write", "resize", "on", "removeAllListeners"]) 176 | newPtyProcess.process = jasmine.createSpy("process").and.returnValue("sometestprocess") 177 | nodePty.spawn.and.returnValue(newPtyProcess) 178 | await element.restartPtyProcess() 179 | expect(element.ptyProcessRunning).toBe(true) 180 | }) 181 | 182 | it("restartPtyProcess() command not found", async () => { 183 | spyOn(atom.notifications, "addError") 184 | atom.config.set("atomic-terminal.shell", "somecommand") 185 | const fakeCall = () => { 186 | throw Error("File not found: somecommand") 187 | } 188 | nodePty.spawn.and.callFake(fakeCall) 189 | await element.restartPtyProcess() 190 | expect(element.ptyProcess).toBe(undefined) 191 | expect(element.ptyProcessRunning).toBe(false) 192 | expect(atom.notifications.addError).toHaveBeenCalledWith("Terminal Error", { 193 | detail: "Could not open shell 'somecommand'.", 194 | }) 195 | }) 196 | 197 | it("restartPtyProcess() some other error thrown", async () => { 198 | spyOn(atom.notifications, "addError") 199 | atom.config.set("atomic-terminal.shell", "somecommand") 200 | const fakeCall = () => { 201 | throw Error("Something went wrong") 202 | } 203 | nodePty.spawn.and.callFake(fakeCall) 204 | await element.restartPtyProcess() 205 | expect(element.ptyProcess).toBe(undefined) 206 | expect(element.ptyProcessRunning).toBe(false) 207 | expect(atom.notifications.addError).toHaveBeenCalledWith("Terminal Error", { 208 | detail: "Launching 'somecommand' raised the following error: Something went wrong", 209 | }) 210 | }) 211 | 212 | it("ptyProcess exit handler set ptyProcessRunning to false", () => { 213 | let exitHandler 214 | for (const arg of element.ptyProcess.on.calls.allArgs()) { 215 | if (arg[0] === "exit") { 216 | exitHandler = arg[1] 217 | break 218 | } 219 | } 220 | spyOn(element.model, "exit") 221 | exitHandler(0) 222 | expect(element.ptyProcessRunning).toBe(false) 223 | }) 224 | 225 | it("ptyProcess exit handler code 0 don't leave open", () => { 226 | let exitHandler 227 | for (const arg of element.ptyProcess.on.calls.allArgs()) { 228 | if (arg[0] === "exit") { 229 | exitHandler = arg[1] 230 | break 231 | } 232 | } 233 | spyOn(element.model, "exit") 234 | exitHandler(0) 235 | expect(element.model.exit).toHaveBeenCalled() 236 | }) 237 | 238 | it("ptyProcess exit handler code 1 don't leave open", () => { 239 | let exitHandler 240 | for (const arg of element.ptyProcess.on.calls.allArgs()) { 241 | if (arg[0] === "exit") { 242 | exitHandler = arg[1] 243 | break 244 | } 245 | } 246 | spyOn(element.model, "exit") 247 | exitHandler(1) 248 | expect(element.model.exit).toHaveBeenCalled() 249 | }) 250 | 251 | it("refitTerminal() initial state", () => { 252 | spyOn(element.fitAddon, "proposeDimensions") 253 | element.refitTerminal() 254 | expect(element.fitAddon.proposeDimensions).not.toHaveBeenCalled() 255 | }) 256 | 257 | it("refitTerminal() terminal not visible", () => { 258 | spyOn(element.fitAddon, "proposeDimensions") 259 | element.contentRect = { width: 1, height: 1 } 260 | element.initiallyVisible = false 261 | element.refitTerminal() 262 | expect(element.fitAddon.proposeDimensions).not.toHaveBeenCalled() 263 | }) 264 | 265 | it("refitTerminal() terminal no width", () => { 266 | spyOn(element.fitAddon, "proposeDimensions") 267 | element.contentRect = { width: 0, height: 1 } 268 | element.initiallyVisible = true 269 | element.refitTerminal() 270 | expect(element.fitAddon.proposeDimensions).not.toHaveBeenCalled() 271 | }) 272 | 273 | it("refitTerminal() terminal no height", () => { 274 | spyOn(element.fitAddon, "proposeDimensions") 275 | element.contentRect = { width: 1, height: 0 } 276 | element.initiallyVisible = true 277 | element.refitTerminal() 278 | expect(element.fitAddon.proposeDimensions).not.toHaveBeenCalled() 279 | }) 280 | 281 | it("refitTerminal() terminal completely visible", () => { 282 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue(null) 283 | element.contentRect = { width: 1, height: 1 } 284 | element.initiallyVisible = true 285 | element.refitTerminal() 286 | expect(element.fitAddon.proposeDimensions).toHaveBeenCalled() 287 | }) 288 | 289 | it("refitTerminal() terminal size not changed", () => { 290 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 291 | cols: element.terminal.cols, 292 | rows: element.terminal.rows, 293 | }) 294 | spyOn(element.terminal, "resize") 295 | element.contentRect = { width: 1, height: 1 } 296 | element.initiallyVisible = true 297 | element.ptyProcessRunning = false 298 | element.refitTerminal() 299 | expect(element.terminal.resize).not.toHaveBeenCalled() 300 | }) 301 | 302 | it("refitTerminal() terminal size cols increased", () => { 303 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 304 | cols: element.terminal.cols + 1, 305 | rows: element.terminal.rows, 306 | }) 307 | spyOn(element.terminal, "resize") 308 | element.contentRect = { width: 1, height: 1 } 309 | element.initiallyVisible = true 310 | element.ptyProcessRunning = false 311 | element.refitTerminal() 312 | expect(element.terminal.resize).toHaveBeenCalled() 313 | }) 314 | 315 | it("refitTerminal() terminal size rows increased", () => { 316 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 317 | cols: element.terminal.cols, 318 | rows: element.terminal.rows + 1, 319 | }) 320 | spyOn(element.terminal, "resize") 321 | element.contentRect = { width: 1, height: 1 } 322 | element.initiallyVisible = true 323 | element.ptyProcessRunning = false 324 | element.refitTerminal() 325 | expect(element.terminal.resize).toHaveBeenCalled() 326 | }) 327 | 328 | it("refitTerminal() terminal size cols and rows increased", () => { 329 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 330 | cols: element.terminal.cols + 1, 331 | rows: element.terminal.rows + 1, 332 | }) 333 | spyOn(element.terminal, "resize") 334 | element.contentRect = { width: 1, height: 1 } 335 | element.initiallyVisible = true 336 | element.ptyProcessRunning = false 337 | element.refitTerminal() 338 | expect(element.terminal.resize).toHaveBeenCalled() 339 | }) 340 | 341 | it("refitTerminal() terminal size rows decreased", () => { 342 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 343 | cols: element.terminal.cols, 344 | rows: element.terminal.rows - 1, 345 | }) 346 | spyOn(element.terminal, "resize") 347 | element.contentRect = { width: 1, height: 1 } 348 | element.initiallyVisible = true 349 | element.ptyProcessRunning = false 350 | element.refitTerminal() 351 | expect(element.terminal.resize).toHaveBeenCalled() 352 | }) 353 | 354 | it("refitTerminal() terminal size cols and rows decreased", () => { 355 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 356 | cols: element.terminal.cols - 1, 357 | rows: element.terminal.rows - 1, 358 | }) 359 | spyOn(element.terminal, "resize") 360 | element.contentRect = { width: 1, height: 1 } 361 | element.initiallyVisible = true 362 | element.ptyProcessRunning = false 363 | element.refitTerminal() 364 | expect(element.terminal.resize).toHaveBeenCalled() 365 | }) 366 | 367 | it("refitTerminal() pty process size not changed ptyProcess running", () => { 368 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 369 | cols: element.ptyProcessCols, 370 | rows: element.ptyProcessRows, 371 | }) 372 | spyOn(element.terminal, "resize") 373 | element.contentRect = { width: 1, height: 1 } 374 | element.initiallyVisible = true 375 | element.ptyProcessRunning = true 376 | element.refitTerminal() 377 | expect(element.ptyProcess.resize).not.toHaveBeenCalled() 378 | }) 379 | 380 | it("refitTerminal() pty process size cols increased ptyProcess running", () => { 381 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 382 | cols: element.ptyProcessCols + 1, 383 | rows: element.ptyProcessRows, 384 | }) 385 | spyOn(element.terminal, "resize") 386 | element.contentRect = { width: 1, height: 1 } 387 | element.initiallyVisible = true 388 | element.ptyProcessRunning = true 389 | element.refitTerminal() 390 | expect(element.ptyProcess.resize).toHaveBeenCalled() 391 | }) 392 | 393 | it("refitTerminal() pty process size rows increased ptyProcess running", () => { 394 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 395 | cols: element.ptyProcessCols, 396 | rows: element.ptyProcessRows + 1, 397 | }) 398 | spyOn(element.terminal, "resize") 399 | element.contentRect = { width: 1, height: 1 } 400 | element.initiallyVisible = true 401 | element.ptyProcessRunning = true 402 | element.refitTerminal() 403 | expect(element.ptyProcess.resize).toHaveBeenCalled() 404 | }) 405 | 406 | it("refitTerminal() pty process size cols and rows increased ptyProcess running", () => { 407 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 408 | cols: element.ptyProcessCols + 1, 409 | rows: element.ptyProcessRows + 1, 410 | }) 411 | spyOn(element.terminal, "resize") 412 | element.contentRect = { width: 1, height: 1 } 413 | element.initiallyVisible = true 414 | element.ptyProcessRunning = true 415 | element.refitTerminal() 416 | expect(element.ptyProcess.resize).toHaveBeenCalled() 417 | }) 418 | 419 | it("refitTerminal() pty process size cols decreased ptyProcess running", () => { 420 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 421 | cols: element.ptyProcessCols - 1, 422 | rows: element.ptyProcessRows, 423 | }) 424 | spyOn(element.terminal, "resize") 425 | element.contentRect = { width: 1, height: 1 } 426 | element.initiallyVisible = true 427 | element.ptyProcessRunning = true 428 | element.refitTerminal() 429 | expect(element.ptyProcess.resize).toHaveBeenCalled() 430 | }) 431 | 432 | it("refitTerminal() pty process size rows decreased ptyProcess running", () => { 433 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 434 | cols: element.ptyProcessCols, 435 | rows: element.ptyProcessRows - 1, 436 | }) 437 | spyOn(element.terminal, "resize") 438 | element.contentRect = { width: 1, height: 1 } 439 | element.initiallyVisible = true 440 | element.ptyProcessRunning = true 441 | element.refitTerminal() 442 | expect(element.ptyProcess.resize).toHaveBeenCalled() 443 | }) 444 | 445 | it("refitTerminal() pty process size cols and rows decreased ptyProcess running", () => { 446 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue({ 447 | cols: element.ptyProcessCols - 1, 448 | rows: element.ptyProcessRows - 1, 449 | }) 450 | spyOn(element.terminal, "resize") 451 | element.contentRect = { width: 1, height: 1 } 452 | element.initiallyVisible = true 453 | element.ptyProcessRunning = true 454 | element.refitTerminal() 455 | expect(element.ptyProcess.resize).toHaveBeenCalled() 456 | }) 457 | 458 | it("refitTerminal() pty process size cols increased ptyProcess running check call args", () => { 459 | const expected = { 460 | cols: element.ptyProcessCols + 1, 461 | rows: element.ptyProcessRows, 462 | } 463 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue(expected) 464 | spyOn(element.terminal, "resize") 465 | element.contentRect = { width: 1, height: 1 } 466 | element.initiallyVisible = true 467 | element.ptyProcessRunning = true 468 | element.refitTerminal() 469 | expect(element.ptyProcess.resize).toHaveBeenCalledWith(expected.cols, expected.rows) 470 | }) 471 | 472 | it("refitTerminal() pty process size rows increased ptyProcess running check call args", () => { 473 | const expected = { 474 | cols: element.ptyProcessCols, 475 | rows: element.ptyProcessRows + 1, 476 | } 477 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue(expected) 478 | spyOn(element.terminal, "resize") 479 | element.contentRect = { width: 1, height: 1 } 480 | element.initiallyVisible = true 481 | element.ptyProcessRunning = true 482 | element.refitTerminal() 483 | expect(element.ptyProcess.resize).toHaveBeenCalledWith(expected.cols, expected.rows) 484 | }) 485 | 486 | it("refitTerminal() pty process size cols and rows increased ptyProcess running check call args", () => { 487 | const expected = { 488 | cols: element.ptyProcessCols + 1, 489 | rows: element.ptyProcessRows + 1, 490 | } 491 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue(expected) 492 | spyOn(element.terminal, "resize") 493 | element.contentRect = { width: 1, height: 1 } 494 | element.initiallyVisible = true 495 | element.ptyProcessRunning = true 496 | element.refitTerminal() 497 | expect(element.ptyProcess.resize).toHaveBeenCalledWith(expected.cols, expected.rows) 498 | }) 499 | 500 | it("refitTerminal() pty process size cols decreased ptyProcess running check call args", () => { 501 | const expected = { 502 | cols: element.ptyProcessCols - 1, 503 | rows: element.ptyProcessRows, 504 | } 505 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue(expected) 506 | spyOn(element.terminal, "resize") 507 | element.contentRect = { width: 1, height: 1 } 508 | element.initiallyVisible = true 509 | element.ptyProcessRunning = true 510 | element.refitTerminal() 511 | expect(element.ptyProcess.resize).toHaveBeenCalledWith(expected.cols, expected.rows) 512 | }) 513 | 514 | it("refitTerminal() pty process size rows decreased ptyProcess running check call args", () => { 515 | const expected = { 516 | cols: element.ptyProcessCols, 517 | rows: element.ptyProcessRows - 1, 518 | } 519 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue(expected) 520 | spyOn(element.terminal, "resize") 521 | element.contentRect = { width: 1, height: 1 } 522 | element.initiallyVisible = true 523 | element.ptyProcessRunning = true 524 | element.refitTerminal() 525 | expect(element.ptyProcess.resize).toHaveBeenCalledWith(expected.cols, expected.rows) 526 | }) 527 | 528 | it("refitTerminal() pty process size cols and rows decreased ptyProcess running check call args", () => { 529 | const expected = { 530 | cols: element.ptyProcessCols - 1, 531 | rows: element.ptyProcessRows - 1, 532 | } 533 | spyOn(element.fitAddon, "proposeDimensions").and.returnValue(expected) 534 | spyOn(element.terminal, "resize") 535 | element.contentRect = { width: 1, height: 1 } 536 | element.initiallyVisible = true 537 | element.ptyProcessRunning = true 538 | element.refitTerminal() 539 | expect(element.ptyProcess.resize).toHaveBeenCalledWith(expected.cols, expected.rows) 540 | }) 541 | 542 | it("focusOnTerminal()", () => { 543 | spyOn(element.terminal, "focus") 544 | spyOn(element.model, "setActive") 545 | element.focusOnTerminal() 546 | expect(element.model.setActive).toHaveBeenCalled() 547 | expect(element.terminal.focus).toHaveBeenCalledTimes(1) 548 | }) 549 | 550 | it("focusOnTerminal() terminal not set", () => { 551 | element.terminal = null 552 | element.focusOnTerminal() 553 | }) 554 | 555 | it("focusOnTerminal(true) calls focus twice", () => { 556 | spyOn(element.terminal, "focus") 557 | element.focusOnTerminal(true) 558 | expect(element.terminal.focus).toHaveBeenCalledTimes(2) 559 | }) 560 | 561 | it("on 'data' handler no custom title on win32 platform", async () => { 562 | Object.defineProperty(process, "platform", { 563 | value: "win32", 564 | }) 565 | const newPtyProcess = jasmine.createSpyObj("ptyProcess", ["kill", "write", "resize", "on", "removeAllListeners"]) 566 | newPtyProcess.process = "sometestprocess" 567 | nodePty.spawn.and.returnValue(newPtyProcess) 568 | await element.restartPtyProcess() 569 | const args = element.ptyProcess.on.calls.argsFor(0) 570 | const onDataCallback = args[1] 571 | onDataCallback("") 572 | expect(element.model.title).toBe("Terminal") 573 | }) 574 | 575 | it("on 'data' handler no custom title on linux platform", async () => { 576 | Object.defineProperty(process, "platform", { 577 | value: "linux", 578 | }) 579 | const newPtyProcess = jasmine.createSpyObj("ptyProcess", ["kill", "write", "resize", "on", "removeAllListeners"]) 580 | newPtyProcess.process = "sometestprocess" 581 | nodePty.spawn.and.returnValue(newPtyProcess) 582 | await element.restartPtyProcess() 583 | const args = element.ptyProcess.on.calls.argsFor(0) 584 | const onDataCallback = args[1] 585 | onDataCallback("") 586 | expect(element.model.title).toBe("sometestprocess") 587 | }) 588 | 589 | it("on 'data' handler custom title on win32 platform", async () => { 590 | Object.defineProperty(process, "platform", { 591 | value: "win32", 592 | }) 593 | const newPtyProcess = jasmine.createSpyObj("ptyProcess", ["kill", "write", "resize", "on", "removeAllListeners"]) 594 | newPtyProcess.process = "sometestprocess" 595 | nodePty.spawn.and.returnValue(newPtyProcess) 596 | element.model.title = "foo" 597 | await element.restartPtyProcess() 598 | const args = element.ptyProcess.on.calls.argsFor(0) 599 | const onDataCallback = args[1] 600 | onDataCallback("") 601 | expect(element.model.title).toBe("foo") 602 | }) 603 | 604 | it("on 'data' handler custom title on linux platform", async () => { 605 | Object.defineProperty(process, "platform", { 606 | value: "linux", 607 | }) 608 | const newPtyProcess = jasmine.createSpyObj("ptyProcess", ["kill", "write", "resize", "on", "removeAllListeners"]) 609 | newPtyProcess.process = "sometestprocess" 610 | nodePty.spawn.and.returnValue(newPtyProcess) 611 | element.model.title = "foo" 612 | await element.restartPtyProcess() 613 | const args = element.ptyProcess.on.calls.argsFor(0) 614 | const onDataCallback = args[1] 615 | onDataCallback("") 616 | expect(element.model.title).toBe("sometestprocess") 617 | }) 618 | 619 | it("on 'exit' handler", async () => { 620 | const newPtyProcess = jasmine.createSpyObj("ptyProcess", ["kill", "write", "resize", "on", "removeAllListeners"]) 621 | newPtyProcess.process = "sometestprocess" 622 | nodePty.spawn.and.returnValue(newPtyProcess) 623 | element.model.title = "foo" 624 | await element.restartPtyProcess() 625 | const args = element.ptyProcess.on.calls.argsFor(1) 626 | const onExitCallback = args[1] 627 | spyOn(element.model, "exit") 628 | onExitCallback(1) 629 | expect(element.model.exit).toHaveBeenCalled() 630 | }) 631 | 632 | it("use wheelScrollUp on terminal container", () => { 633 | const wheelEvent = new WheelEvent("wheel", { 634 | deltaY: -150, 635 | }) 636 | element.dispatchEvent(wheelEvent) 637 | expect(element.model.fontSize).toBe(14) 638 | }) 639 | 640 | it("use wheelScrollDown on terminal container", () => { 641 | const wheelEvent = new WheelEvent("wheel", { 642 | deltaY: 150, 643 | }) 644 | element.dispatchEvent(wheelEvent) 645 | expect(element.model.fontSize).toBe(14) 646 | }) 647 | 648 | it("use ctrl+wheelScrollUp on terminal container, editor.zoomFontWhenCtrlScrolling = true", () => { 649 | atom.config.set("editor.zoomFontWhenCtrlScrolling", true) 650 | const wheelEvent = new WheelEvent("wheel", { 651 | deltaY: -150, 652 | ctrlKey: true, 653 | }) 654 | element.dispatchEvent(wheelEvent) 655 | expect(element.model.fontSize).toBe(15) 656 | }) 657 | 658 | it("use ctrl+wheelScrollDown on terminal container, editor.zoomFontWhenCtrlScrolling = true", () => { 659 | atom.config.set("editor.zoomFontWhenCtrlScrolling", true) 660 | const wheelEvent = new WheelEvent("wheel", { 661 | deltaY: 150, 662 | ctrlKey: true, 663 | }) 664 | element.dispatchEvent(wheelEvent) 665 | expect(element.model.fontSize).toBe(13) 666 | }) 667 | 668 | it("use ctrl+wheelScrollUp on terminal container, editor.zoomFontWhenCtrlScrolling = false", () => { 669 | atom.config.set("editor.zoomFontWhenCtrlScrolling", false) 670 | const wheelEvent = new WheelEvent("wheel", { 671 | deltaY: -150, 672 | ctrlKey: true, 673 | }) 674 | element.dispatchEvent(wheelEvent) 675 | expect(element.model.fontSize).toBe(14) 676 | }) 677 | 678 | it("use ctrl+wheelScrollDown on terminal container, editor.zoomFontWhenCtrlScrolling = false", () => { 679 | atom.config.set("editor.zoomFontWhenCtrlScrolling", false) 680 | const wheelEvent = new WheelEvent("wheel", { 681 | deltaY: 150, 682 | ctrlKey: true, 683 | }) 684 | element.dispatchEvent(wheelEvent) 685 | expect(element.model.fontSize).toBe(14) 686 | }) 687 | 688 | it("use ctrl+wheelScrollUp font already at maximum", () => { 689 | element.model.fontSize = config.fontSize.maximum 690 | const wheelEvent = new WheelEvent("wheel", { 691 | deltaY: -150, 692 | ctrlKey: true, 693 | }) 694 | element.dispatchEvent(wheelEvent) 695 | expect(element.model.fontSize).toBe(config.fontSize.maximum) 696 | }) 697 | 698 | it("use ctrl+wheelScrollDown font already at minimum", () => { 699 | element.model.fontSize = config.fontSize.minimum 700 | const wheelEvent = new WheelEvent("wheel", { 701 | deltaY: 150, 702 | ctrlKey: true, 703 | }) 704 | element.dispatchEvent(wheelEvent) 705 | expect(element.model.fontSize).toBe(config.fontSize.minimum) 706 | }) 707 | 708 | it("copy on select", async () => { 709 | spyOn(atom.clipboard, "write") 710 | atom.config.set("atomic-terminal.copyOnSelect", true) 711 | await new Promise((resolve) => element.terminal.write("test", resolve)) 712 | element.terminal.selectLines(0, 0) 713 | const selection = element.terminal.getSelection() 714 | expect(atom.clipboard.write).toHaveBeenCalledWith(selection) 715 | }) 716 | 717 | it("does not copy on clear selection", async () => { 718 | spyOn(atom.clipboard, "write") 719 | atom.config.set("atomic-terminal.copyOnSelect", true) 720 | await new Promise((resolve) => element.terminal.write("test", resolve)) 721 | element.terminal.selectLines(0, 0) 722 | atom.clipboard.write.calls.reset() 723 | element.terminal.clearSelection() 724 | expect(atom.clipboard.write).not.toHaveBeenCalled() 725 | }) 726 | 727 | it("does not copy if copyOnSelect is false", async () => { 728 | spyOn(atom.clipboard, "write") 729 | atom.config.set("atomic-terminal.copyOnSelect", false) 730 | await new Promise((resolve) => element.terminal.write("test", resolve)) 731 | element.terminal.selectLines(0, 0) 732 | expect(atom.clipboard.write).not.toHaveBeenCalled() 733 | }) 734 | 735 | it("getXtermOptions() default options", () => { 736 | const expected = { 737 | cursorBlink: true, 738 | fontSize: atom.config.get("editor.fontSize"), 739 | fontFamily: atom.config.get("editor.fontFamily"), 740 | theme: getTheme(), 741 | } 742 | expect(element.getXtermOptions()).toEqual(expected) 743 | }) 744 | 745 | it("getXtermOptions() fontSize changed", () => { 746 | atom.config.set("atomic-terminal.fontSize", 8) 747 | const expected = { 748 | cursorBlink: true, 749 | fontSize: 8, 750 | fontFamily: atom.config.get("editor.fontFamily"), 751 | theme: getTheme(), 752 | } 753 | expect(element.getXtermOptions()).toEqual(expected) 754 | }) 755 | 756 | it("getXtermOptions() fontFamily changed", () => { 757 | atom.config.set("atomic-terminal.fontFamily", "serif") 758 | const expected = { 759 | cursorBlink: true, 760 | fontSize: atom.config.get("editor.fontSize"), 761 | fontFamily: "serif", 762 | theme: getTheme(), 763 | } 764 | expect(element.getXtermOptions()).toEqual(expected) 765 | }) 766 | 767 | it("getXtermOptions() theme changed", () => { 768 | atom.config.set("atomic-terminal.colors.theme", "Red") 769 | const expected = { 770 | cursorBlink: true, 771 | fontSize: atom.config.get("editor.fontSize"), 772 | fontFamily: atom.config.get("editor.fontFamily"), 773 | theme: getTheme(), 774 | } 775 | expect(element.getXtermOptions()).toEqual(expected) 776 | }) 777 | 778 | it("clear terminal", () => { 779 | spyOn(element.terminal, "clear") 780 | element.clear() 781 | expect(element.terminal.clear).toHaveBeenCalled() 782 | }) 783 | }) 784 | --------------------------------------------------------------------------------