├── .gitignore ├── spec ├── .eslintrc.js ├── custom-runner.js ├── save-profile-element-spec.js ├── delete-profile-element-spec.js ├── overwrite-profile-element-spec.js ├── profile-menu-model-spec.js ├── delete-profile-model-spec.js ├── utils-spec.js ├── overwrite-profile-model-spec.js ├── save-profile-model-spec.js ├── profile-menu-element-spec.js ├── x-terminal-spec.js ├── config-spec.js └── model-spec.js ├── release.config.js ├── resources ├── x-terminal-demo.gif ├── x-terminal-exit-failure.png ├── x-terminal-exit-success.png ├── x-terminal-packages-menu.png ├── x-terminal-profiles-demo.gif ├── x-terminal-activity-notification.gif └── x-terminal-moving-terminals-demo.gif ├── renovate.json ├── stylelint.config.mjs ├── LICENSE ├── eslint.config.mjs ├── src ├── profile-menu-model.js ├── save-profile-element.js ├── utils.js ├── delete-profile-model.js ├── overwrite-profile-model.js ├── delete-profile-element.js ├── overwrite-profile-element.js ├── save-profile-model.js ├── profiles.js ├── model.js └── profile-menu-element.js ├── package.json ├── keymaps └── x-terminal.json ├── .github └── workflows │ └── main.yml ├── menus └── x-terminal.json ├── styles └── x-terminal.less └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | docs 5 | -------------------------------------------------------------------------------- /spec/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jasmine: true, 4 | atomtest: true, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '@semantic-release/apm-config', 3 | branches: 'main', 4 | } 5 | -------------------------------------------------------------------------------- /resources/x-terminal-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spiker985/x-terminal-reloaded/HEAD/resources/x-terminal-demo.gif -------------------------------------------------------------------------------- /resources/x-terminal-exit-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spiker985/x-terminal-reloaded/HEAD/resources/x-terminal-exit-failure.png -------------------------------------------------------------------------------- /resources/x-terminal-exit-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spiker985/x-terminal-reloaded/HEAD/resources/x-terminal-exit-success.png -------------------------------------------------------------------------------- /resources/x-terminal-packages-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spiker985/x-terminal-reloaded/HEAD/resources/x-terminal-packages-menu.png -------------------------------------------------------------------------------- /resources/x-terminal-profiles-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spiker985/x-terminal-reloaded/HEAD/resources/x-terminal-profiles-demo.gif -------------------------------------------------------------------------------- /resources/x-terminal-activity-notification.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spiker985/x-terminal-reloaded/HEAD/resources/x-terminal-activity-notification.gif -------------------------------------------------------------------------------- /resources/x-terminal-moving-terminals-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Spiker985/x-terminal-reloaded/HEAD/resources/x-terminal-moving-terminals-demo.gif -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:recommended" 4 | ], 5 | "packageRules": [ 6 | { 7 | "groupName": "xterm", 8 | "matchPackageNames": [ 9 | "/^xterm/" 10 | ] 11 | }, 12 | { 13 | "groupName": "eslint", 14 | "matchPackageNames": [ 15 | "/^eslint/" 16 | ] 17 | }, 18 | { 19 | "groupName": "stylelint", 20 | "matchPackageNames": [ 21 | "/^stylelint/" 22 | ] 23 | }, 24 | { 25 | "matchDepTypes": [ 26 | "devDependencies" 27 | ], 28 | "automerge": true, 29 | "commitMessageTopic": "devDependency {{depName}}" 30 | } 31 | ], 32 | "rangeStrategy": "bump" 33 | } 34 | -------------------------------------------------------------------------------- /stylelint.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: "stylelint-config-standard", 3 | plugins: [ 4 | "@stylistic/stylelint-plugin", 5 | ], 6 | customSyntax: 'postcss-less', 7 | rules: { 8 | 'declaration-empty-line-before': null, 9 | 'declaration-block-no-redundant-longhand-properties': null, 10 | 'no-invalid-position-at-import-rule': null, 11 | 'selector-pseudo-element-colon-notation': 'double', 12 | '@stylistic/no-extra-semicolons': true, 13 | 'function-no-unknown': null, 14 | '@stylistic/indentation': 'tab', 15 | 'selector-type-no-unknown': [ 16 | true, { 17 | ignoreTypes: [ 18 | 'x-terminal-reloaded', 19 | 'x-terminal-reloaded-profile', 20 | ], 21 | }, 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | x-terminal-reloaded 3 | Copyright (c) 2022 Spiker985. 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 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import js from "@eslint/js"; 3 | import json from "@eslint/json"; 4 | import jasmine from "eslint-plugin-jasmine"; 5 | import stylisticJs from '@stylistic/eslint-plugin-js'; 6 | import { defineConfig, globalIgnores } from "eslint/config"; 7 | 8 | 9 | export default defineConfig([ 10 | // Globally ignore package lock files 11 | globalIgnores(["**/package-lock.json"]), 12 | // Define config for default js files 13 | { 14 | files: ["**/*.{js,mjs,cjs}"], 15 | ignores: ["spec/*.js"], 16 | plugins: { 17 | js, 18 | '@stylistic/js': stylisticJs, 19 | }, 20 | extends: ["js/recommended"], 21 | languageOptions: { 22 | globals: { 23 | ...globals.node, 24 | ...globals.browser, 25 | atom: "readonly", 26 | }, 27 | }, 28 | rules: { 29 | "@stylistic/js/comma-dangle": ["error", "always-multiline"], 30 | "@stylistic/js/indent": ["error", "tab", { SwitchCase: 1 }], 31 | "@stylistic/js/no-tabs": ["error", { allowIndentationTabs: true }], 32 | "no-console": "warn", 33 | "no-unused-vars": "warn", 34 | }, 35 | }, 36 | //Define config for spec files 37 | { 38 | files: ["spec/*-spec.js"], 39 | plugins: { jasmine }, 40 | extends: ["jasmine/recommended"], 41 | languageOptions: { 42 | globals: { 43 | ...globals.jasmine, 44 | ...globals.atomtest, 45 | }, 46 | }, 47 | }, 48 | // Define config for json files 49 | { 50 | files: ["**/*.json"], 51 | plugins: { json }, 52 | language: "json/json", 53 | extends: ["json/recommended"], 54 | }, 55 | ]); 56 | -------------------------------------------------------------------------------- /spec/custom-runner.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { createRunner } from 'atom-jasmine3-test-runner' 23 | 24 | module.exports = createRunner({ 25 | specHelper: { 26 | attachToDom: true, 27 | customMatchers: true, 28 | ci: true, 29 | }, 30 | }, () => { 31 | 32 | const warn = console.warn.bind(console) 33 | beforeEach(() => { 34 | spyOn(console, 'warn').and.callFake((...args) => { 35 | if (args[0].includes('not attached to the DOM')) { 36 | return 37 | } 38 | warn(...args) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/profile-menu-model.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | class XTerminalProfileMenuModel { 23 | constructor (atomXtermModel) { 24 | this.atomXtermModel = atomXtermModel 25 | this.element = null 26 | } 27 | 28 | destroy () { 29 | if (this.element) { 30 | this.element.destroy() 31 | } 32 | } 33 | 34 | getTitle () { 35 | return 'X-Terminal-Reloaded Profile Menu' 36 | } 37 | 38 | getElement () { 39 | return this.element 40 | } 41 | 42 | setElement (element) { 43 | this.element = element 44 | } 45 | 46 | getXTerminalModelElement () { 47 | return this.atomXtermModel.getElement() 48 | } 49 | 50 | getXTerminalModel () { 51 | return this.atomXtermModel 52 | } 53 | } 54 | 55 | export { 56 | XTerminalProfileMenuModel, 57 | } 58 | -------------------------------------------------------------------------------- /spec/save-profile-element-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalSaveProfileElementImpl } from '../src/save-profile-element' 23 | 24 | describe('XTerminalSaveProfileElement', () => { 25 | let model 26 | 27 | beforeEach(() => { 28 | model = jasmine.createSpyObj('model', ['setElement']) 29 | }) 30 | 31 | it('initialize()', () => { 32 | const element = new XTerminalSaveProfileElementImpl() 33 | element.initialize(model) 34 | 35 | expect(element.messageDiv.textContent).toBe('Enter new profile name') 36 | }) 37 | 38 | it('setNewTextbox()', () => { 39 | const element = new XTerminalSaveProfileElementImpl() 40 | element.initialize(model) 41 | const textbox = jasmine.createSpyObj('textbox', ['getElement']) 42 | textbox.getElement.and.returnValue(document.createElement('div')) 43 | element.setNewTextbox(textbox) 44 | 45 | expect(textbox.getElement).toHaveBeenCalled() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /spec/delete-profile-element-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalDeleteProfileElementImpl } from '../src/delete-profile-element' 23 | 24 | describe('XTerminalDeleteProfileElement', () => { 25 | let model 26 | 27 | beforeEach(() => { 28 | model = jasmine.createSpyObj('model', ['setElement']) 29 | }) 30 | 31 | it('initialize()', () => { 32 | const element = new XTerminalDeleteProfileElementImpl() 33 | element.initialize(model) 34 | 35 | expect(element.promptButtonsDiv.childElementCount).toBe(0) 36 | }) 37 | 38 | it('setNewPrompt()', () => { 39 | const element = new XTerminalDeleteProfileElementImpl() 40 | element.initialize(model) 41 | const profileName = 'foo' 42 | const confirmHandler = () => {} 43 | const cancelHandler = () => {} 44 | element.setNewPrompt(profileName, confirmHandler, cancelHandler) 45 | 46 | expect(element.messageDiv.textContent).toBe('Delete existing profile \'foo\'?') 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /spec/overwrite-profile-element-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalOverwriteProfileElementImpl } from '../src/overwrite-profile-element' 23 | 24 | describe('XTerminalOverwriteProfileElement', () => { 25 | let model 26 | 27 | beforeEach(() => { 28 | model = jasmine.createSpyObj('model', ['setElement']) 29 | }) 30 | 31 | it('initialize()', () => { 32 | const element = new XTerminalOverwriteProfileElementImpl() 33 | element.initialize(model) 34 | 35 | expect(element.promptButtonsDiv.childElementCount).toBe(0) 36 | }) 37 | 38 | it('setNewPrompt()', () => { 39 | const element = new XTerminalOverwriteProfileElementImpl() 40 | element.initialize(model) 41 | const profileName = 'foo' 42 | const confirmHandler = () => {} 43 | const cancelHandler = () => {} 44 | element.setNewPrompt(profileName, confirmHandler, cancelHandler) 45 | 46 | expect(element.messageDiv.textContent).toBe('Overwrite existing profile \'foo\'?') 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/save-profile-element.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { clearDiv } from './utils' 23 | 24 | class XTerminalSaveProfileElementImpl extends HTMLElement { 25 | initialize (model) { 26 | this.model = model 27 | this.model.setElement(this) 28 | this.textboxDiv = document.createElement('div') 29 | this.textboxDiv.classList.add('x-terminal-reloaded-save-profile-textbox') 30 | this.appendChild(this.textboxDiv) 31 | this.messageDiv = document.createElement('div') 32 | this.messageDiv.classList.add('x-terminal-reloaded-modal-message') 33 | this.messageDiv.appendChild(document.createTextNode('Enter new profile name')) 34 | this.appendChild(this.messageDiv) 35 | } 36 | 37 | setNewTextbox (textbox) { 38 | clearDiv(this.textboxDiv) 39 | this.textboxDiv.appendChild(textbox.getElement()) 40 | } 41 | } 42 | 43 | customElements.define('x-terminal-reloaded-save-profile', XTerminalSaveProfileElementImpl) 44 | 45 | export { 46 | XTerminalSaveProfileElementImpl, 47 | } 48 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | export function clearDiv (div) { 23 | while (div.firstChild) { 24 | div.removeChild(div.firstChild) 25 | } 26 | } 27 | 28 | export function createHorizontalLine () { 29 | const hLine = document.createElement('div') 30 | hLine.classList.add('x-terminal-reloaded-profile-menu-element-hline') 31 | hLine.appendChild(document.createTextNode('.')) 32 | return hLine 33 | } 34 | 35 | export function recalculateActive (terminalsSet, active) { 36 | const allowHidden = atom.config.get('x-terminal-reloaded.terminalSettings.allowHiddenToStayActive') 37 | const terminals = [...terminalsSet] 38 | terminals.sort((a, b) => { 39 | // active before other 40 | if (active && a === active) { 41 | return -1 42 | } 43 | if (active && b === active) { 44 | return 1 45 | } 46 | if (!allowHidden) { 47 | // visible before hidden 48 | if (a.isVisible() && !b.isVisible()) { 49 | return -1 50 | } 51 | if (!a.isVisible() && b.isVisible()) { 52 | return 1 53 | } 54 | } 55 | // lower activeIndex before higher activeIndex 56 | return a.activeIndex - b.activeIndex 57 | }) 58 | terminals.forEach((t, i) => { 59 | t.activeIndex = i 60 | t.emitter.emit('did-change-title') 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /src/delete-profile-model.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalProfilesSingleton } from './profiles' 23 | 24 | class XTerminalDeleteProfileModel { 25 | constructor (atomXtermProfileMenuElement) { 26 | this.atomXtermProfileMenuElement = atomXtermProfileMenuElement 27 | this.profilesSingleton = XTerminalProfilesSingleton.instance 28 | this.element = null 29 | this.panel = atom.workspace.addModalPanel({ 30 | item: this, 31 | visible: false, 32 | }) 33 | } 34 | 35 | getTitle () { 36 | return 'X-Terminal-Reloaded Delete Profile Model' 37 | } 38 | 39 | getElement () { 40 | return this.element 41 | } 42 | 43 | setElement (element) { 44 | this.element = element 45 | } 46 | 47 | close () { 48 | if (!this.panel.isVisible()) { 49 | return 50 | } 51 | this.panel.hide() 52 | } 53 | 54 | promptDelete (profileName) { 55 | this.panel.show() 56 | const confirmHandler = async (event) => { 57 | await this.profilesSingleton.deleteProfile(profileName) 58 | this.profilesSingleton.reloadProfiles() 59 | await this.profilesSingleton.profilesLoadPromise 60 | this.close() 61 | } 62 | const cancelHandler = (event) => { 63 | this.close() 64 | } 65 | this.getElement().setNewPrompt( 66 | profileName, 67 | confirmHandler, 68 | cancelHandler, 69 | ) 70 | } 71 | } 72 | 73 | export { 74 | XTerminalDeleteProfileModel, 75 | } 76 | -------------------------------------------------------------------------------- /src/overwrite-profile-model.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalProfilesSingleton } from './profiles' 23 | 24 | class XTerminalOverwriteProfileModel { 25 | constructor (atomXtermSaveProfileModel) { 26 | this.atomXtermSaveProfileModel = atomXtermSaveProfileModel 27 | this.atomXtermProfileMenuElement = this.atomXtermSaveProfileModel.atomXtermProfileMenuElement 28 | this.profilesSingleton = XTerminalProfilesSingleton.instance 29 | this.element = null 30 | this.panel = atom.workspace.addModalPanel({ 31 | item: this, 32 | visible: false, 33 | }) 34 | } 35 | 36 | getTitle () { 37 | return 'X-Terminal-Reloaded Overwrite Profile Model' 38 | } 39 | 40 | getElement () { 41 | return this.element 42 | } 43 | 44 | setElement (element) { 45 | this.element = element 46 | } 47 | 48 | close (newProfile, profileChanges, rePrompt = false) { 49 | if (!this.panel.isVisible()) { 50 | return 51 | } 52 | this.panel.hide() 53 | if (rePrompt) { 54 | this.atomXtermSaveProfileModel.promptForNewProfileName(newProfile, profileChanges) 55 | } 56 | } 57 | 58 | promptOverwrite (profileName, newProfile, profileChanges) { 59 | this.panel.show() 60 | const confirmHandler = async (event) => { 61 | await this.profilesSingleton.setProfile(profileName, newProfile) 62 | this.profilesSingleton.reloadProfiles() 63 | await this.profilesSingleton.profilesLoadPromise 64 | this.close(newProfile, profileChanges) 65 | this.atomXtermProfileMenuElement.applyProfileChanges(profileChanges) 66 | } 67 | const cancelHandler = (event) => { 68 | this.close(newProfile, profileChanges, true) 69 | } 70 | this.getElement().setNewPrompt( 71 | profileName, 72 | confirmHandler, 73 | cancelHandler, 74 | ) 75 | } 76 | } 77 | 78 | export { 79 | XTerminalOverwriteProfileModel, 80 | } 81 | -------------------------------------------------------------------------------- /spec/profile-menu-model-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalProfileMenuModel } from '../src/profile-menu-model' 23 | 24 | describe('XTerminalProfileMenuModel', () => { 25 | let atomXtermModel 26 | 27 | beforeEach(() => { 28 | atomXtermModel = jasmine.createSpyObj('atomXtermModel', ['getElement']) 29 | }) 30 | 31 | it('constructor()', () => { 32 | const model = new XTerminalProfileMenuModel(atomXtermModel) 33 | 34 | expect(model).not.toBeUndefined() 35 | }) 36 | 37 | it('destroy() no element set', () => { 38 | const model = new XTerminalProfileMenuModel(atomXtermModel) 39 | model.destroy() 40 | }) 41 | 42 | it('destroy() element set', () => { 43 | const model = new XTerminalProfileMenuModel(atomXtermModel) 44 | model.element = jasmine.createSpyObj('element', ['destroy']) 45 | model.destroy() 46 | 47 | expect(model.element.destroy).toHaveBeenCalled() 48 | }) 49 | 50 | it('getTitle()', () => { 51 | const model = new XTerminalProfileMenuModel(atomXtermModel) 52 | 53 | expect(model.getTitle()).toBe('X-Terminal-Reloaded Profile Menu') 54 | }) 55 | 56 | it('getElement()', () => { 57 | const model = new XTerminalProfileMenuModel(atomXtermModel) 58 | 59 | expect(model.getElement()).toBeNull() 60 | }) 61 | 62 | it('setElement()', () => { 63 | const model = new XTerminalProfileMenuModel(atomXtermModel) 64 | const mock = jasmine.createSpy('element') 65 | model.setElement(mock) 66 | 67 | expect(model.getElement()).toBe(mock) 68 | }) 69 | 70 | it('getXTerminalModelElement()', () => { 71 | const model = new XTerminalProfileMenuModel(atomXtermModel) 72 | model.getXTerminalModelElement() 73 | 74 | expect(model.atomXtermModel.getElement).toHaveBeenCalled() 75 | }) 76 | 77 | it('getXTerminalModel()', () => { 78 | const model = new XTerminalProfileMenuModel(atomXtermModel) 79 | 80 | expect(model.getXTerminalModel()).toBe(atomXtermModel) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/delete-profile-element.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { clearDiv, createHorizontalLine } from './utils' 23 | 24 | class XTerminalDeleteProfileElementImpl extends HTMLElement { 25 | initialize (model) { 26 | this.model = model 27 | this.model.setElement(this) 28 | this.messageDiv = document.createElement('div') 29 | this.messageDiv.classList.add('x-terminal-reloaded-modal-message') 30 | this.appendChild(this.messageDiv) 31 | this.appendChild(createHorizontalLine()) 32 | this.promptButtonsDiv = document.createElement('div') 33 | this.promptButtonsDiv.classList.add('x-terminal-reloaded-modal-buttons-div') 34 | this.appendChild(this.promptButtonsDiv) 35 | } 36 | 37 | setNewPrompt (profileName, confirmHandler, cancelHandler) { 38 | clearDiv(this.messageDiv) 39 | clearDiv(this.promptButtonsDiv) 40 | const text = 'Delete existing profile \'' + profileName + '\'?' 41 | this.messageDiv.appendChild(document.createTextNode(text)) 42 | const confirmButton = document.createElement('button') 43 | confirmButton.classList.add('x-terminal-reloaded-modal-button') 44 | confirmButton.classList.add('btn-primary') 45 | confirmButton.classList.add('btn-error') 46 | confirmButton.appendChild(document.createTextNode('Confirm')) 47 | confirmButton.addEventListener('click', confirmHandler, { passive: true }) 48 | this.promptButtonsDiv.appendChild(confirmButton) 49 | const cancelButton = document.createElement('button') 50 | cancelButton.classList.add('x-terminal-reloaded-modal-button') 51 | cancelButton.classList.add('btn-primary') 52 | cancelButton.appendChild(document.createTextNode('Cancel')) 53 | cancelButton.addEventListener('click', cancelHandler, { passive: true }) 54 | this.promptButtonsDiv.appendChild(cancelButton) 55 | } 56 | } 57 | 58 | customElements.define('x-terminal-reloaded-delete-profile', XTerminalDeleteProfileElementImpl) 59 | 60 | export { 61 | XTerminalDeleteProfileElementImpl, 62 | } 63 | -------------------------------------------------------------------------------- /src/overwrite-profile-element.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { clearDiv, createHorizontalLine } from './utils' 23 | 24 | class XTerminalOverwriteProfileElementImpl extends HTMLElement { 25 | initialize (model) { 26 | this.model = model 27 | this.model.setElement(this) 28 | this.messageDiv = document.createElement('div') 29 | this.messageDiv.classList.add('x-terminal-reloaded-modal-message') 30 | this.appendChild(this.messageDiv) 31 | this.appendChild(createHorizontalLine()) 32 | this.promptButtonsDiv = document.createElement('div') 33 | this.promptButtonsDiv.classList.add('x-terminal-reloaded-modal-buttons-div') 34 | this.appendChild(this.promptButtonsDiv) 35 | } 36 | 37 | setNewPrompt (profileName, confirmHandler, cancelHandler) { 38 | clearDiv(this.messageDiv) 39 | clearDiv(this.promptButtonsDiv) 40 | const text = 'Overwrite existing profile \'' + profileName + '\'?' 41 | this.messageDiv.appendChild(document.createTextNode(text)) 42 | const confirmButton = document.createElement('button') 43 | confirmButton.classList.add('x-terminal-reloaded-modal-button') 44 | confirmButton.classList.add('btn-primary') 45 | confirmButton.classList.add('btn-error') 46 | confirmButton.appendChild(document.createTextNode('Confirm')) 47 | confirmButton.addEventListener('click', confirmHandler, { passive: true }) 48 | this.promptButtonsDiv.appendChild(confirmButton) 49 | const cancelButton = document.createElement('button') 50 | cancelButton.classList.add('x-terminal-reloaded-modal-button') 51 | cancelButton.classList.add('btn-primary') 52 | cancelButton.appendChild(document.createTextNode('Cancel')) 53 | cancelButton.addEventListener('click', cancelHandler, { passive: true }) 54 | this.promptButtonsDiv.appendChild(cancelButton) 55 | } 56 | } 57 | 58 | customElements.define('x-terminal-reloaded-overwrite-profile', XTerminalOverwriteProfileElementImpl) 59 | 60 | export { 61 | XTerminalOverwriteProfileElementImpl, 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "x-terminal-reloaded", 3 | "title": "X-Terminal-reloaded", 4 | "main": "./src/x-terminal", 5 | "module": "./src/x-terminal", 6 | "bugs": { 7 | "url": "https://github.com/Spiker985/x-terminal-reloaded/issues/new/choose" 8 | }, 9 | "version": "14.4.2", 10 | "description": "An xterm based plugin for providing terminals inside your workspace. Originally for Atom, updated for Pulsar. A fork of atom-xterm and x-terminal", 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 | "repository": "https://github.com/Spiker985/x-terminal-reloaded", 32 | "license": "MIT", 33 | "engines": { 34 | "atom": ">=1.41.0 <2.0.0" 35 | }, 36 | "providedServices": { 37 | "atom-xterm": { 38 | "description": "An x-terminal service for providing terminals inside your Atom workspace.", 39 | "versions": { 40 | "2.0.0": "provideAtomXtermService" 41 | } 42 | }, 43 | "platformioIDETerminal": { 44 | "description": "Run commands and open terminals.", 45 | "versions": { 46 | "1.1.0": "providePlatformIOIDEService" 47 | } 48 | }, 49 | "terminal": { 50 | "description": "Run commands and open terminals.", 51 | "versions": { 52 | "1.0.0": "provideTerminalService" 53 | } 54 | } 55 | }, 56 | "dependencies": { 57 | "@xterm/addon-fit": "0.9.0", 58 | "@xterm/addon-ligatures": "0.8.0", 59 | "@xterm/addon-web-links": "0.10.0", 60 | "@xterm/addon-webgl": "0.17.0", 61 | "@xterm/xterm": "5.4.0", 62 | "array.prototype.at": "^1.1.3", 63 | "deep-object-diff": "^1.1.9", 64 | "fs-extra": "^11.3.0", 65 | "marked": "^15.0.12", 66 | "node-pty": "https://github.com/pulsar-edit/node-pty.git", 67 | "uuid": "^11.1.0", 68 | "whatwg-url": "^14.2.0", 69 | "which": "^5.0.0" 70 | }, 71 | "devDependencies": { 72 | "@eslint/eslintrc": "^3.3.3", 73 | "@eslint/js": "^9.39.2", 74 | "@eslint/json": "^0.14.0", 75 | "@semantic-release/apm-config": "^9.0.1", 76 | "@stylistic/eslint-plugin-js": "^4.4.1", 77 | "@stylistic/stylelint-plugin": "^4.0.0", 78 | "atom-jasmine3-test-runner": "^5.2.13", 79 | "eslint": "^9.39.2", 80 | "eslint-plugin-jasmine": "^4.2.2", 81 | "globals": "^16.5.0", 82 | "postcss-less": "^6.0.0", 83 | "semantic-release": "^25.0.2", 84 | "stylelint": "^16.26.1", 85 | "stylelint-config-standard": "^39.0.1", 86 | "temp": "^0.9.4" 87 | }, 88 | "scripts": { 89 | "eslint": "eslint . --ext .json,.js", 90 | "stylelint": "stylelint styles/*.less", 91 | "lint": "npm run eslint && npm run stylelint", 92 | "lintfix": "npm run eslint -- --fix && npm run stylelint -- --fix", 93 | "editor-test": "pulsar --test spec", 94 | "test": "npm run editor-test && npm run lint" 95 | }, 96 | "deserializers": { 97 | "XTerminalModel": "deserializeXTerminalModel" 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /keymaps/x-terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | ".platform-linux atom-workspace": { 3 | "ctrl-`": "x-terminal-reloaded:open", 4 | "ctrl-~": "x-terminal-reloaded:open", 5 | "ctrl-shift-t": "x-terminal-reloaded:open", 6 | "ctrl-alt-shift-t": "x-terminal-reloaded:open", 7 | "ctrl-alt-shift-up": "x-terminal-reloaded:open-split-up", 8 | "ctrl-alt-shift-down": "x-terminal-reloaded:open-split-down", 9 | "ctrl-alt-shift-left": "x-terminal-reloaded:open-split-left", 10 | "ctrl-alt-shift-right": "x-terminal-reloaded:open-split-right", 11 | "ctrl-alt-shift-i": "x-terminal-reloaded:open-split-bottom-dock", 12 | "ctrl-alt-shift-u": "x-terminal-reloaded:open-split-left-dock", 13 | "ctrl-alt-shift-o": "x-terminal-reloaded:open-split-right-dock", 14 | "ctrl-alt-r": "x-terminal-reloaded:run-selected-text", 15 | "ctrl-alt-i": "x-terminal-reloaded:insert-selected-text" 16 | }, 17 | ".platform-win32 atom-workspace": { 18 | "ctrl-`": "x-terminal-reloaded:open", 19 | "ctrl-~": "x-terminal-reloaded:open", 20 | "ctrl-shift-t": "x-terminal-reloaded:open", 21 | "ctrl-alt-shift-t": "x-terminal-reloaded:open", 22 | "ctrl-alt-shift-up": "x-terminal-reloaded:open-split-up", 23 | "ctrl-alt-shift-down": "x-terminal-reloaded:open-split-down", 24 | "ctrl-alt-shift-left": "x-terminal-reloaded:open-split-left", 25 | "ctrl-alt-shift-right": "x-terminal-reloaded:open-split-right", 26 | "ctrl-alt-shift-i": "x-terminal-reloaded:open-split-bottom-dock", 27 | "ctrl-alt-shift-u": "x-terminal-reloaded:open-split-left-dock", 28 | "ctrl-alt-shift-o": "x-terminal-reloaded:open-split-right-dock", 29 | "ctrl-alt-r": "x-terminal-reloaded:run-selected-text", 30 | "ctrl-alt-i": "x-terminal-reloaded:insert-selected-text" 31 | }, 32 | ".platform-darwin atom-workspace": { 33 | "cmd-`": "x-terminal-reloaded:open", 34 | "cmd-~": "x-terminal-reloaded:open", 35 | "cmd-t": "x-terminal-reloaded:open", 36 | "cmd-alt-shift-t": "x-terminal-reloaded:open", 37 | "cmd-alt-shift-up": "x-terminal-reloaded:open-split-up", 38 | "cmd-alt-shift-down": "x-terminal-reloaded:open-split-down", 39 | "cmd-alt-shift-left": "x-terminal-reloaded:open-split-left", 40 | "cmd-alt-shift-right": "x-terminal-reloaded:open-split-right", 41 | "cmd-alt-shift-i": "x-terminal-reloaded:open-split-bottom-dock", 42 | "cmd-alt-shift-u": "x-terminal-reloaded:open-split-left-dock", 43 | "cmd-alt-shift-o": "x-terminal-reloaded:open-split-right-dock", 44 | "cmd-alt-r": "x-terminal-reloaded:run-selected-text", 45 | "cmd-alt-i": "x-terminal-reloaded:insert-selected-text" 46 | }, 47 | ".platform-linux x-terminal-reloaded": { 48 | "ctrl-insert": "x-terminal-reloaded:copy", 49 | "ctrl-shift-c": "x-terminal-reloaded:copy", 50 | "shift-insert": "x-terminal-reloaded:paste", 51 | "ctrl-shift-v": "x-terminal-reloaded:paste", 52 | "ctrl-shift-u": "x-terminal-reloaded:unfocus", 53 | "ctrl-l": "x-terminal-reloaded:clear" 54 | }, 55 | ".platform-win32 x-terminal-reloaded": { 56 | "ctrl-insert": "x-terminal-reloaded:copy", 57 | "ctrl-shift-c": "x-terminal-reloaded:copy", 58 | "shift-insert": "x-terminal-reloaded:paste", 59 | "ctrl-shift-v": "x-terminal-reloaded:paste", 60 | "ctrl-shift-u": "x-terminal-reloaded:unfocus", 61 | "ctrl-l": "x-terminal-reloaded:clear" 62 | }, 63 | ".platform-darwin x-terminal-reloaded": { 64 | "ctrl-shift-c": "x-terminal-reloaded:copy", 65 | "ctrl-insert": "x-terminal-reloaded:copy", 66 | "ctrl-shift-v": "x-terminal-reloaded:paste", 67 | "shift-insert": "x-terminal-reloaded:paste", 68 | "cmd-c": "x-terminal-reloaded:copy", 69 | "cmd-v": "x-terminal-reloaded:paste", 70 | "ctrl-shift-u": "x-terminal-reloaded:unfocus", 71 | "ctrl-l": "x-terminal-reloaded:clear" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /spec/delete-profile-model-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalDeleteProfileModel } from '../src/delete-profile-model' 23 | 24 | describe('XTerminalDeleteProfileModel', () => { 25 | let atomXtermProfileMenuElement 26 | 27 | beforeEach(() => { 28 | atomXtermProfileMenuElement = jasmine.createSpy( 29 | 'atomXtermProfileMenuElement', 30 | ) 31 | }) 32 | 33 | it('constructor()', () => { 34 | const model = new XTerminalDeleteProfileModel(atomXtermProfileMenuElement) 35 | 36 | expect(model).not.toBeNull() 37 | }) 38 | 39 | it('getTitle()', () => { 40 | const model = new XTerminalDeleteProfileModel(atomXtermProfileMenuElement) 41 | 42 | expect(model.getTitle()).toBe('X-Terminal-Reloaded Delete Profile Model') 43 | }) 44 | 45 | it('getElement()', () => { 46 | const model = new XTerminalDeleteProfileModel(atomXtermProfileMenuElement) 47 | 48 | expect(model.getElement()).toBeNull() 49 | }) 50 | 51 | it('setElement()', () => { 52 | const model = new XTerminalDeleteProfileModel(atomXtermProfileMenuElement) 53 | const element = jasmine.createSpy('atomXtermDeleteProfileElement') 54 | model.setElement(element) 55 | 56 | expect(model.getElement()).toBe(element) 57 | }) 58 | 59 | it('close() panel is not visible', () => { 60 | const model = new XTerminalDeleteProfileModel(atomXtermProfileMenuElement) 61 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 62 | model.panel.isVisible.and.returnValue(false) 63 | model.close() 64 | 65 | expect(model.panel.hide).not.toHaveBeenCalled() 66 | }) 67 | 68 | it('close() panel is visible', () => { 69 | const model = new XTerminalDeleteProfileModel(atomXtermProfileMenuElement) 70 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 71 | model.panel.isVisible.and.returnValue(true) 72 | model.close() 73 | 74 | expect(model.panel.hide).toHaveBeenCalled() 75 | }) 76 | 77 | it('promptDelete() panel is shown', () => { 78 | const model = new XTerminalDeleteProfileModel(atomXtermProfileMenuElement) 79 | model.panel = jasmine.createSpyObj('panel', ['show', 'isVisible', 'hide']) 80 | model.panel.isVisible.and.returnValue(true) 81 | model.element = jasmine.createSpyObj('atomXtermDeleteProfileElement', ['setNewPrompt']) 82 | model.promptDelete('foo') 83 | 84 | expect(model.panel.show).toHaveBeenCalled() 85 | }) 86 | 87 | it('promptDelete() new prompt is set', () => { 88 | const model = new XTerminalDeleteProfileModel(atomXtermProfileMenuElement) 89 | model.panel = jasmine.createSpyObj('panel', ['show', 'isVisible', 'hide']) 90 | model.panel.isVisible.and.returnValue(true) 91 | model.element = jasmine.createSpyObj('atomXtermDeleteProfileElement', ['setNewPrompt']) 92 | model.promptDelete('foo') 93 | 94 | expect(model.element.setNewPrompt).toHaveBeenCalled() 95 | }) 96 | }) 97 | -------------------------------------------------------------------------------- /src/save-profile-model.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { TextEditor } from 'atom' 23 | 24 | import { XTerminalProfilesSingleton } from './profiles' 25 | import { XTerminalOverwriteProfileModel } from './overwrite-profile-model' 26 | import { currentItemIsXTerminalModel } from './model' 27 | 28 | class XTerminalSaveProfileModel { 29 | constructor (atomXtermProfileMenuElement) { 30 | this.atomXtermProfileMenuElement = atomXtermProfileMenuElement 31 | this.profilesSingleton = XTerminalProfilesSingleton.instance 32 | this.element = null 33 | this.panel = atom.workspace.addModalPanel({ 34 | item: this, 35 | visible: false, 36 | }) 37 | this.overwriteProfileModel = new XTerminalOverwriteProfileModel(this) 38 | } 39 | 40 | getTitle () { 41 | return 'X-Terminal-Reloaded Save Profile Model' 42 | } 43 | 44 | getElement () { 45 | return this.element 46 | } 47 | 48 | setElement (element) { 49 | this.element = element 50 | } 51 | 52 | getTextbox () { 53 | return this.textbox 54 | } 55 | 56 | async updateProfile (profileName, newProfile, profileChanges) { 57 | await this.profilesSingleton.setProfile(profileName, newProfile) 58 | this.profilesSingleton.reloadProfiles() 59 | await this.profilesSingleton.profilesLoadPromise 60 | this.close() 61 | this.atomXtermProfileMenuElement.applyProfileChanges(profileChanges) 62 | } 63 | 64 | async confirm (newProfile, profileChanges) { 65 | const profileName = this.textbox.getText() 66 | if (!profileName) { 67 | // Simply do nothing. 68 | return 69 | } 70 | const exists = await this.profilesSingleton.isProfileExists(profileName) 71 | if (exists) { 72 | this.close(false) 73 | this.overwriteProfileModel.promptOverwrite(profileName, newProfile, profileChanges) 74 | } else { 75 | this.updateProfile(profileName, newProfile, profileChanges) 76 | } 77 | } 78 | 79 | close (focusMenuElement = true) { 80 | if (!this.panel.isVisible()) { 81 | return 82 | } 83 | this.textbox.setText('') 84 | this.panel.hide() 85 | if (this.atomXtermProfileMenuElement.isVisible() && focusMenuElement) { 86 | this.atomXtermProfileMenuElement.focus() 87 | } 88 | } 89 | 90 | promptForNewProfileName (newProfile, profileChanges) { 91 | // TODO: Is it possible for the active item to change while the 92 | // modal is displayed. 93 | if (this.panel.isVisible() || !currentItemIsXTerminalModel()) { 94 | return 95 | } 96 | this.textbox = new TextEditor({ mini: true }) 97 | this.textbox.getElement().addEventListener('blur', (event) => { 98 | this.close() 99 | }, { passive: true }) 100 | atom.commands.add(this.textbox.getElement(), 'core:confirm', () => { 101 | this.confirm(newProfile, profileChanges) 102 | }) 103 | atom.commands.add(this.textbox.getElement(), 'core:cancel', () => { 104 | this.close() 105 | }) 106 | this.element.setNewTextbox(this.textbox) 107 | this.panel.show() 108 | this.textbox.getElement().focus() 109 | } 110 | } 111 | 112 | export { 113 | XTerminalSaveProfileModel, 114 | } 115 | -------------------------------------------------------------------------------- /spec/utils-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import * as utils from '../src/utils' 23 | 24 | describe('Utilities', () => { 25 | it('clearDiv()', () => { 26 | const div = document.createElement('div') 27 | for (let i = 0; i < 10; i++) { 28 | div.appendChild(document.createElement('div')) 29 | } 30 | utils.clearDiv(div) 31 | 32 | expect(div.childElementCount).toBe(0) 33 | }) 34 | 35 | it('clearDiv() empty div', () => { 36 | const div = document.createElement('div') 37 | utils.clearDiv(div) 38 | 39 | expect(div.childElementCount).toBe(0) 40 | }) 41 | 42 | it('createHorizontalLine()', () => { 43 | const hLine = utils.createHorizontalLine() 44 | 45 | expect(hLine.tagName).toBe('DIV') 46 | expect(hLine.classList.contains('x-terminal-reloaded-profile-menu-element-hline')).toBe(true) 47 | expect(hLine.textContent).toBe('.') 48 | }) 49 | 50 | describe('recalculateActive()', () => { 51 | const createTerminals = (num = 1) => { 52 | const terminals = [] 53 | for (let i = 0; i < num; i++) { 54 | terminals.push({ 55 | activeIndex: i, 56 | isVisible () {}, 57 | emitter: { 58 | emit () {}, 59 | }, 60 | }) 61 | } 62 | return terminals 63 | } 64 | 65 | it('active first', () => { 66 | const terminals = createTerminals(2) 67 | const terminalsSet = new Set(terminals) 68 | utils.recalculateActive(terminalsSet, terminals[1]) 69 | 70 | expect(terminals[0].activeIndex).toBe(1) 71 | expect(terminals[1].activeIndex).toBe(0) 72 | }) 73 | 74 | it('visible before hidden', () => { 75 | const terminals = createTerminals(2) 76 | const terminalsSet = new Set(terminals) 77 | spyOn(terminals[1], 'isVisible').and.returnValue(true) 78 | utils.recalculateActive(terminalsSet) 79 | 80 | expect(terminals[0].activeIndex).toBe(1) 81 | expect(terminals[1].activeIndex).toBe(0) 82 | }) 83 | 84 | it('allowHiddenToStayActive', () => { 85 | atom.config.set('x-terminal-reloaded.terminalSettings.allowHiddenToStayActive', true) 86 | const terminals = createTerminals(2) 87 | const terminalsSet = new Set(terminals) 88 | spyOn(terminals[1], 'isVisible').and.returnValue(true) 89 | utils.recalculateActive(terminalsSet) 90 | 91 | expect(terminals[0].activeIndex).toBe(0) 92 | expect(terminals[1].activeIndex).toBe(1) 93 | }) 94 | 95 | it('lower active index first', () => { 96 | const terminals = createTerminals(2) 97 | const terminalsSet = new Set(terminals) 98 | terminals[0].activeIndex = 1 99 | terminals[1].activeIndex = 0 100 | utils.recalculateActive(terminalsSet) 101 | 102 | expect(terminals[0].activeIndex).toBe(1) 103 | expect(terminals[1].activeIndex).toBe(0) 104 | }) 105 | 106 | it('emit did-change-title', () => { 107 | const terminals = createTerminals(2) 108 | const terminalsSet = new Set(terminals) 109 | spyOn(terminals[0].emitter, 'emit') 110 | spyOn(terminals[1].emitter, 'emit') 111 | utils.recalculateActive(terminalsSet) 112 | 113 | expect(terminals[0].emitter.emit).toHaveBeenCalledWith('did-change-title') 114 | expect(terminals[1].emitter.emit).toHaveBeenCalledWith('did-change-title') 115 | }) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: "CI" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | - 'feature/*' 8 | 9 | jobs: 10 | Test-GA: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | # Should be the static versions of the current "-latest" runners 15 | os: [ubuntu-24.04, windows-2022] 16 | # os: [ubuntu-24.04, macos-14, windows-2022] 17 | runs-on: ${{ matrix.os }} 18 | timeout-minutes: 90 19 | steps: 20 | - name: Checkout repo 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Node 24 | uses: actions/setup-node@v4.4.0 25 | with: 26 | node-version: 'lts/*' 27 | 28 | - name: Install dependencies 29 | run: npm install --verbose 30 | 31 | - name: Install Pulsar 32 | uses: pulsar-edit/action-pulsar-dependency@v3.4 33 | 34 | - name: Rebuild dependencies (Windows) 35 | if: ${{ runner.os == 'Windows' }} 36 | # Currently the Pulsar process starts, but unlike *nix doesn't wait for ppm to finish, probably because pulsar.cmd needs updated 37 | # As of Pulsar version 1.108.0, a ppm binary has been included (which is a clone of apm) with the intention of fully moving away from the apm binary 38 | run: ppm rebuild --verbose 39 | 40 | - name: Rebuild dependencies (*nix) 41 | if: ${{ runner.os != 'Windows' }} 42 | run: pulsar --package install --verbose 43 | 44 | - name: Run the headless Pulsar Tests 45 | uses: coactions/setup-xvfb@v1.0.1 46 | with: 47 | run: pulsar --test spec 48 | 49 | Test-RC: 50 | needs: [Test-GA] 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | # Should be the "-latest" runners to test before a new runner is promoted 55 | os: [ubuntu-latest, macos-latest, windows-latest] 56 | runs-on: ${{ matrix.os }} 57 | timeout-minutes: 90 58 | steps: 59 | - uses: actions/checkout@v4 60 | - name: Checkout repo 61 | uses: actions/checkout@v4 62 | 63 | - name: Setup Node 64 | uses: actions/setup-node@v4.4.0 65 | with: 66 | node-version: 'lts/*' 67 | 68 | - name: Install dependencies 69 | run: npm install --verbose 70 | 71 | - name: Install Pulsar 72 | uses: pulsar-edit/action-pulsar-dependency@v3.4 73 | 74 | - name: Rebuild dependencies (Windows) 75 | if: ${{ runner.os == 'Windows' }} 76 | # Currently the Pulsar process starts, but unlike *nix doesn't wait for ppm to finish, probably because pulsar.cmd needs updated 77 | # As of Pulsar version 1.108.0, a ppm binary has been included (which is a clone of apm) with the intention of fully moving away from the apm binary 78 | run: ppm rebuild --verbose 79 | 80 | - name: Rebuild dependencies (*nix) 81 | if: ${{ runner.os != 'Windows' }} 82 | run: pulsar --package install --verbose 83 | 84 | - name: Run the headless Pulsar Tests 85 | uses: coactions/setup-xvfb@v1.0.1 86 | with: 87 | run: pulsar --test spec 88 | 89 | Lint: 90 | runs-on: ubuntu-latest 91 | steps: 92 | - name: Checkout repot 93 | uses: actions/checkout@v4 94 | 95 | - name: Setup Node 96 | uses: actions/setup-node@v4.4.0 97 | with: 98 | node-version: 'lts/*' 99 | 100 | - name: Install package 101 | run: npm install --legacy-peer-deps 102 | 103 | - name: Lint ✨ 104 | run: npm run lint 105 | 106 | Release: 107 | needs: [Test-GA, Lint] 108 | if: | 109 | github.ref == 'refs/heads/main' 110 | runs-on: ubuntu-latest 111 | steps: 112 | - uses: actions/checkout@v4 113 | 114 | - uses: pulsar-edit/action-pulsar-dependency@v3.4 115 | 116 | - name: Setup Node 117 | uses: actions/setup-node@v4.4.0 118 | with: 119 | node-version: 'lts/*' 120 | 121 | - name: NPM install 122 | run: npm install 123 | 124 | - name: Shim apm call 125 | run: | 126 | cd /usr/bin/ 127 | # Only neccessary because of attempting to use the apm semantic-release config 128 | sudo ln --symbolic --force /usr/bin/ppm apm 129 | 130 | - name: Release 🎉 131 | env: 132 | #Used to allow the CI to push a new release automatically. Intrinsic to GitHub CI. 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | #Used by ppm to auth for publishing of packages 135 | ATOM_ACCESS_TOKEN: ${{ secrets.PULSAR_ACCESS_TOKEN }} 136 | run: npx semantic-release 137 | -------------------------------------------------------------------------------- /spec/overwrite-profile-model-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalOverwriteProfileModel } from '../src/overwrite-profile-model' 23 | 24 | describe('XTerminalOverwriteProfileModel', () => { 25 | let atomXtermSaveProfileModel 26 | 27 | beforeEach(() => { 28 | atomXtermSaveProfileModel = jasmine.createSpyObj( 29 | 'atomXtermSaveProfileModel', 30 | [ 31 | 'promptForNewProfileName', 32 | ], 33 | ) 34 | }) 35 | 36 | it('constructor()', () => { 37 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 38 | 39 | expect(model).not.toBeNull() 40 | }) 41 | 42 | it('getTitle()', () => { 43 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 44 | 45 | expect(model.getTitle()).toBe('X-Terminal-Reloaded Overwrite Profile Model') 46 | }) 47 | 48 | it('getElement()', () => { 49 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 50 | 51 | expect(model.getElement()).toBeNull() 52 | }) 53 | 54 | it('setElement()', () => { 55 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 56 | const element = jasmine.createSpy('atomXtermOverwriteProfileElement') 57 | model.setElement(element) 58 | 59 | expect(model.getElement()).toBe(element) 60 | }) 61 | 62 | it('close() panel is not visible', () => { 63 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 64 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 65 | model.panel.isVisible.and.returnValue(false) 66 | model.close('foo', 'bar') 67 | 68 | expect(model.panel.hide).not.toHaveBeenCalled() 69 | expect(model.atomXtermSaveProfileModel.promptForNewProfileName).not.toHaveBeenCalled() 70 | }) 71 | 72 | it('close() panel is visible', () => { 73 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 74 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 75 | model.panel.isVisible.and.returnValue(true) 76 | model.close('foo', 'bar') 77 | 78 | expect(model.panel.hide).toHaveBeenCalled() 79 | expect(model.atomXtermSaveProfileModel.promptForNewProfileName).not.toHaveBeenCalled() 80 | }) 81 | 82 | it('close() reprompt panel is not visible', () => { 83 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 84 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 85 | model.panel.isVisible.and.returnValue(false) 86 | model.close('foo', 'bar', true) 87 | 88 | expect(model.panel.hide).not.toHaveBeenCalled() 89 | expect(model.atomXtermSaveProfileModel.promptForNewProfileName).not.toHaveBeenCalled() 90 | }) 91 | 92 | it('close() reprompt panel is visible', () => { 93 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 94 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 95 | model.panel.isVisible.and.returnValue(true) 96 | model.close('foo', 'bar', true) 97 | 98 | expect(model.panel.hide).toHaveBeenCalled() 99 | expect(model.atomXtermSaveProfileModel.promptForNewProfileName).toHaveBeenCalled() 100 | }) 101 | 102 | it('promptOverwrite() panel is shown', () => { 103 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 104 | model.panel = jasmine.createSpyObj('panel', ['show', 'isVisible', 'hide']) 105 | model.panel.isVisible.and.returnValue(true) 106 | model.element = jasmine.createSpyObj('atomXtermDeleteProfileElement', ['setNewPrompt']) 107 | model.promptOverwrite('foo', 'bar', 'baz') 108 | 109 | expect(model.panel.show).toHaveBeenCalled() 110 | }) 111 | 112 | it('promptOverwrite() new prompt is set', () => { 113 | const model = new XTerminalOverwriteProfileModel(atomXtermSaveProfileModel) 114 | model.panel = jasmine.createSpyObj('panel', ['show', 'isVisible', 'hide']) 115 | model.panel.isVisible.and.returnValue(true) 116 | model.element = jasmine.createSpyObj('atomXtermDeleteProfileElement', ['setNewPrompt']) 117 | model.promptOverwrite('foo', 'bar', 'baz') 118 | 119 | expect(model.element.setNewPrompt).toHaveBeenCalled() 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /src/profiles.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { Emitter } from 'atom' 23 | 24 | import { configDefaults, CONFIG_DATA } from './config' 25 | 26 | import fs from 'fs-extra' 27 | import path from 'path' 28 | 29 | import { v4 as uuidv4 } from 'uuid' 30 | import { URL } from 'whatwg-url' 31 | import { detailedDiff } from 'deep-object-diff' 32 | 33 | const X_TERMINAL_BASE_URI = 'x-terminal-reloaded://' 34 | 35 | const XTerminalProfilesSingletonSymbol = Symbol('XTerminalProfilesSingleton sentinel') 36 | 37 | class XTerminalProfilesSingleton { 38 | constructor (symbolCheck) { 39 | if (XTerminalProfilesSingletonSymbol !== symbolCheck) { 40 | throw new Error('XTerminalProfilesSingleton cannot be instantiated directly.') 41 | } 42 | this.emitter = new Emitter() 43 | this.profilesConfigPath = path.join(configDefaults.userDataPath, 'profiles.json') 44 | this.profiles = {} 45 | this.previousBaseProfile = null 46 | this.baseProfile = this.getDefaultProfile() 47 | this.resetBaseProfile() 48 | this.profilesLoadPromise = null 49 | this.reloadProfiles() 50 | } 51 | 52 | static get instance () { 53 | if (!this[XTerminalProfilesSingletonSymbol]) { 54 | this[XTerminalProfilesSingletonSymbol] = new XTerminalProfilesSingleton(XTerminalProfilesSingletonSymbol) 55 | } 56 | return this[XTerminalProfilesSingletonSymbol] 57 | } 58 | 59 | sortProfiles (profiles) { 60 | const orderedProfiles = {} 61 | Object.keys(profiles).sort().forEach((key) => { 62 | orderedProfiles[key] = profiles[key] 63 | }) 64 | return orderedProfiles 65 | } 66 | 67 | async reloadProfiles () { 68 | let resolveLoad 69 | this.profilesLoadPromise = new Promise((resolve) => { 70 | resolveLoad = resolve 71 | }) 72 | try { 73 | const data = await fs.readJson(this.profilesConfigPath) 74 | this.profiles = this.sortProfiles(data) 75 | this.emitter.emit('did-reload-profiles', this.getSanitizedProfilesData()) 76 | resolveLoad() 77 | } catch (err) { 78 | // Create the profiles file. 79 | await this.updateProfiles({}) 80 | this.emitter.emit('did-reload-profiles', this.getSanitizedProfilesData()) 81 | resolveLoad() 82 | } 83 | } 84 | 85 | onDidReloadProfiles (callback) { 86 | return this.emitter.on('did-reload-profiles', callback) 87 | } 88 | 89 | onDidResetBaseProfile (callback) { 90 | return this.emitter.on('did-reset-base-profile', callback) 91 | } 92 | 93 | async updateProfiles (newProfilesConfigData) { 94 | await fs.ensureDir(path.dirname(this.profilesConfigPath)) 95 | newProfilesConfigData = this.sortProfiles(newProfilesConfigData) 96 | await fs.writeJson(this.profilesConfigPath, newProfilesConfigData) 97 | this.profiles = newProfilesConfigData 98 | } 99 | 100 | deepClone (data) { 101 | return JSON.parse(JSON.stringify(data)) 102 | } 103 | 104 | diffProfiles (oldProfile, newProfile) { 105 | // This method will return added or modified entries. 106 | const diff = detailedDiff(oldProfile, newProfile) 107 | return { 108 | ...diff.added, 109 | ...diff.updated, 110 | } 111 | } 112 | 113 | getDefaultProfile () { 114 | const defaultProfile = {} 115 | for (const data of CONFIG_DATA) { 116 | if (!data.profileKey) continue 117 | defaultProfile[data.profileKey] = data.defaultProfile 118 | } 119 | return defaultProfile 120 | } 121 | 122 | getBaseProfile () { 123 | return this.deepClone(this.baseProfile) 124 | } 125 | 126 | resetBaseProfile () { 127 | this.previousBaseProfile = this.deepClone(this.baseProfile) 128 | this.baseProfile = {} 129 | for (const data of CONFIG_DATA) { 130 | if (!data.profileKey) continue 131 | this.baseProfile[data.profileKey] = data.toBaseProfile(this.previousBaseProfile[data.profileKey]) 132 | } 133 | this.emitter.emit('did-reset-base-profile', this.getBaseProfile()) 134 | } 135 | 136 | sanitizeData (unsanitizedData) { 137 | if (!unsanitizedData) { 138 | return {} 139 | } 140 | const sanitizedData = {} 141 | for (const data of CONFIG_DATA) { 142 | if (!data.profileKey) continue 143 | if (data.profileKey in unsanitizedData) { 144 | sanitizedData[data.profileKey] = unsanitizedData[data.profileKey] 145 | } 146 | } 147 | 148 | return this.deepClone(sanitizedData) 149 | } 150 | 151 | getSanitizedProfilesData () { 152 | const retval = {} 153 | for (const key in this.profiles) { 154 | retval[key] = this.sanitizeData(this.profiles[key]) 155 | } 156 | return retval 157 | } 158 | 159 | async getProfiles () { 160 | await this.profilesLoadPromise 161 | return this.getSanitizedProfilesData() 162 | } 163 | 164 | async getProfile (profileName) { 165 | await this.profilesLoadPromise 166 | return { 167 | ...this.deepClone(this.baseProfile), 168 | ...this.sanitizeData(this.profiles[profileName] || {}), 169 | } 170 | } 171 | 172 | async isProfileExists (profileName) { 173 | await this.profilesLoadPromise 174 | return profileName in this.profiles 175 | } 176 | 177 | async setProfile (profileName, data) { 178 | await this.profilesLoadPromise 179 | const profileData = { 180 | ...this.deepClone(this.baseProfile), 181 | ...this.sanitizeData(data), 182 | } 183 | const newProfilesConfigData = { 184 | ...this.deepClone(this.profiles), 185 | } 186 | newProfilesConfigData[profileName] = profileData 187 | await this.updateProfiles(newProfilesConfigData) 188 | } 189 | 190 | async deleteProfile (profileName) { 191 | await this.profilesLoadPromise 192 | const newProfilesConfigData = { 193 | ...this.deepClone(this.profiles), 194 | } 195 | delete newProfilesConfigData[profileName] 196 | await this.updateProfiles(newProfilesConfigData) 197 | } 198 | 199 | generateNewUri () { 200 | return X_TERMINAL_BASE_URI + uuidv4() + '/' 201 | } 202 | 203 | generateNewUrlFromProfileData (profileData) { 204 | profileData = this.sanitizeData(profileData) 205 | const url = new URL(this.generateNewUri()) 206 | for (const data of CONFIG_DATA) { 207 | if (!data.profileKey) continue 208 | if (data.profileKey in profileData) url.searchParams.set(data.profileKey, data.toUrlParam(profileData[data.profileKey])) 209 | } 210 | return url 211 | } 212 | 213 | createProfileDataFromUri (uri) { 214 | const url = new URL(uri) 215 | const baseProfile = this.getBaseProfile() 216 | const profileData = {} 217 | for (const data of CONFIG_DATA) { 218 | if (!data.profileKey) continue 219 | const param = url.searchParams.get(data.profileKey) 220 | if (param) { 221 | profileData[data.profileKey] = data.fromUrlParam(param) 222 | } 223 | if (!param || !data.checkUrlParam(profileData[data.profileKey])) { 224 | profileData[data.profileKey] = baseProfile[data.profileKey] 225 | } 226 | } 227 | return profileData 228 | } 229 | } 230 | 231 | export { 232 | X_TERMINAL_BASE_URI, 233 | XTerminalProfilesSingleton, 234 | } 235 | -------------------------------------------------------------------------------- /spec/save-profile-model-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalSaveProfileModel } from '../src/save-profile-model' 23 | import * as atomXtermModelModule from '../src/model' 24 | 25 | describe('XTerminalSaveProfileModel', () => { 26 | let atomXtermProfileMenuElement 27 | 28 | beforeEach(() => { 29 | atomXtermProfileMenuElement = jasmine.createSpyObj( 30 | 'atomXtermProfileMenuElement', 31 | [ 32 | 'applyProfileChanges', 33 | 'restartTerminal', 34 | 'isVisible', 35 | 'focus', 36 | ], 37 | ) 38 | }) 39 | 40 | it('constructor()', () => { 41 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 42 | 43 | expect(model).not.toBeNull() 44 | }) 45 | 46 | it('getTitle()', () => { 47 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 48 | 49 | expect(model.getTitle()).toBe('X-Terminal-Reloaded Save Profile Model') 50 | }) 51 | 52 | it('getElement()', () => { 53 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 54 | 55 | expect(model.getElement()).toBeNull() 56 | }) 57 | 58 | it('setElement()', () => { 59 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 60 | const element = jasmine.createSpy('atomXtermSaveProfileElement') 61 | model.setElement(element) 62 | 63 | expect(model.getElement()).toBe(element) 64 | }) 65 | 66 | it('getTextbox()', () => { 67 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 68 | const mock = jasmine.createSpy('textbox') 69 | model.textbox = mock 70 | 71 | expect(model.getTextbox()).toBe(mock) 72 | }) 73 | 74 | it('updateProfile()', (done) => { 75 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 76 | spyOn(model.profilesSingleton, 'setProfile').and.resolveTo() 77 | model.atomXtermProfileMenuElement.applyProfileChanges.and.callFake((profileChanges) => { 78 | expect(profileChanges).toBe('baz') 79 | done() 80 | }) 81 | model.updateProfile('foo', {}, 'baz') 82 | }) 83 | 84 | it('confirm() no name given', () => { 85 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 86 | model.textbox = jasmine.createSpyObj('textbox', ['getText']) 87 | model.textbox.getText.and.returnValue('') 88 | spyOn(model.profilesSingleton, 'isProfileExists').and.resolveTo(false) 89 | model.confirm({}) 90 | 91 | expect(model.profilesSingleton.isProfileExists).not.toHaveBeenCalled() 92 | }) 93 | 94 | it('confirm() name given does not exist', (done) => { 95 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 96 | model.textbox = jasmine.createSpyObj('textbox', ['getText']) 97 | model.textbox.getText.and.returnValue('foo') 98 | spyOn(model.profilesSingleton, 'isProfileExists').and.resolveTo(false) 99 | spyOn(model, 'updateProfile').and.callFake((profileName, newProfile, profileChanges) => { 100 | expect(profileName).toBe('foo') 101 | expect(newProfile).toEqual({}) 102 | expect(profileChanges).toBe('baz') 103 | done() 104 | }) 105 | model.confirm({}, 'baz') 106 | }) 107 | 108 | it('confirm() name given exists', (done) => { 109 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 110 | model.textbox = jasmine.createSpyObj('textbox', ['getText']) 111 | model.textbox.getText.and.returnValue('foo') 112 | spyOn(model.profilesSingleton, 'isProfileExists').and.resolveTo(true) 113 | spyOn(model, 'close') 114 | spyOn(model.overwriteProfileModel, 'promptOverwrite').and.callFake((profileChanges) => { 115 | expect(model.close).toHaveBeenCalledWith(false) 116 | done() 117 | }) 118 | model.confirm({}, 'baz') 119 | }) 120 | 121 | it('close() panel is not visible', () => { 122 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 123 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 124 | model.panel.isVisible.and.returnValue(false) 125 | model.textbox = jasmine.createSpyObj('textbox', ['setText']) 126 | model.atomXtermProfileMenuElement.isVisible.and.returnValue(false) 127 | model.close() 128 | 129 | expect(model.panel.hide).not.toHaveBeenCalled() 130 | }) 131 | 132 | it('close() panel is visible profile menu element is not visible', () => { 133 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 134 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 135 | model.panel.isVisible.and.returnValue(true) 136 | model.textbox = jasmine.createSpyObj('textbox', ['setText']) 137 | model.atomXtermProfileMenuElement.isVisible.and.returnValue(false) 138 | model.close() 139 | 140 | expect(model.panel.hide).toHaveBeenCalled() 141 | expect(model.atomXtermProfileMenuElement.focus).not.toHaveBeenCalled() 142 | }) 143 | 144 | it('close() panel is visible profile menu element is visible', () => { 145 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 146 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 147 | model.panel.isVisible.and.returnValue(true) 148 | model.textbox = jasmine.createSpyObj('textbox', ['setText']) 149 | model.atomXtermProfileMenuElement.isVisible.and.returnValue(true) 150 | model.close() 151 | 152 | expect(model.panel.hide).toHaveBeenCalled() 153 | expect(model.atomXtermProfileMenuElement.focus).toHaveBeenCalled() 154 | }) 155 | 156 | it('close() panel is visible profile menu element is visible focusMenuElement = false', () => { 157 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 158 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'hide']) 159 | model.panel.isVisible.and.returnValue(true) 160 | model.textbox = jasmine.createSpyObj('textbox', ['setText']) 161 | model.atomXtermProfileMenuElement.isVisible.and.returnValue(true) 162 | model.close(false) 163 | 164 | expect(model.panel.hide).toHaveBeenCalled() 165 | expect(model.atomXtermProfileMenuElement.focus).not.toHaveBeenCalled() 166 | }) 167 | 168 | it('promptForNewProfileName() modal is not visible current item is not XTerminalModel', () => { 169 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 170 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'show']) 171 | model.panel.isVisible.and.returnValue(false) 172 | model.element = jasmine.createSpyObj('element', ['setNewTextbox']) 173 | spyOn(atomXtermModelModule, 'currentItemIsXTerminalModel').and.returnValue(false) 174 | model.promptForNewProfileName({}, 'baz') 175 | 176 | expect(model.panel.show).not.toHaveBeenCalled() 177 | }) 178 | 179 | it('promptForNewProfileName() modal is not visible current item is XTerminalModel', () => { 180 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 181 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'show']) 182 | model.panel.isVisible.and.returnValue(false) 183 | model.element = jasmine.createSpyObj('element', ['setNewTextbox']) 184 | spyOn(atomXtermModelModule, 'currentItemIsXTerminalModel').and.returnValue(true) 185 | model.promptForNewProfileName({}, 'baz') 186 | 187 | expect(model.panel.show).toHaveBeenCalled() 188 | }) 189 | 190 | it('promptForNewProfileName() modal is visible current item is not XTerminalModel', () => { 191 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 192 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'show']) 193 | model.panel.isVisible.and.returnValue(true) 194 | model.element = jasmine.createSpyObj('element', ['setNewTextbox']) 195 | spyOn(atomXtermModelModule, 'currentItemIsXTerminalModel').and.returnValue(false) 196 | model.promptForNewProfileName({}, 'baz') 197 | 198 | expect(model.panel.show).not.toHaveBeenCalled() 199 | }) 200 | 201 | it('promptForNewProfileName() modal is visible current item is XTerminalModel', () => { 202 | const model = new XTerminalSaveProfileModel(atomXtermProfileMenuElement) 203 | model.panel = jasmine.createSpyObj('panel', ['isVisible', 'show']) 204 | model.panel.isVisible.and.returnValue(true) 205 | model.element = jasmine.createSpyObj('element', ['setNewTextbox']) 206 | spyOn(atomXtermModelModule, 'currentItemIsXTerminalModel').and.returnValue(true) 207 | model.promptForNewProfileName({}, 'baz') 208 | 209 | expect(model.panel.show).not.toHaveBeenCalled() 210 | }) 211 | }) 212 | -------------------------------------------------------------------------------- /src/model.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { Emitter } from 'atom' 23 | 24 | import { recalculateActive } from './utils' 25 | import { XTerminalProfilesSingleton } from './profiles' 26 | 27 | import fs from 'fs-extra' 28 | import path from 'path' 29 | import os from 'os' 30 | 31 | import { URL } from 'whatwg-url' 32 | 33 | const DEFAULT_TITLE = 'X-Terminal-Reloaded' 34 | 35 | /** 36 | * The main terminal model, or rather item, displayed in the Atom workspace. 37 | * 38 | * @class 39 | */ 40 | class XTerminalModel { 41 | // NOTE: Though the class is publically accessible, all methods except for the 42 | // ones defined at the very bottom of the class should be considered private 43 | // and subject to change at any time. 44 | constructor (options) { 45 | this.options = options 46 | this.uri = this.options.uri 47 | const url = new URL(this.uri) 48 | this.sessionId = url.host 49 | this.uriCwd = url.searchParams.get('cwd') 50 | this.profilesSingleton = XTerminalProfilesSingleton.instance 51 | this.profile = this.profilesSingleton.createProfileDataFromUri(this.uri) 52 | this.debug = this.profile.debug 53 | this.terminals_set = this.options.terminals_set 54 | this.activeIndex = this.terminals_set.size 55 | this.element = null 56 | this.pane = null 57 | this.title = DEFAULT_TITLE 58 | if (this.profile.title !== null) { 59 | this.title = this.profile.title 60 | } 61 | this.modified = false 62 | this.emitter = new Emitter() 63 | this.terminals_set.add(this) 64 | 65 | // Determine appropriate initial working directory based on previous 66 | // active item. Since this involves async operations on the file 67 | // system, a Promise will be used to indicate when initialization is 68 | // done. 69 | this.isInitialized = false 70 | this.initializedPromise = this.initialize().then(() => { 71 | this.isInitialized = true 72 | }) 73 | } 74 | 75 | async initialize () { 76 | let cwd 77 | 78 | if (this.uriCwd) { 79 | cwd = this.uriCwd 80 | } else if (this.profile.projectCwd) { 81 | const previousActiveItem = atom.workspace.getActivePaneItem() 82 | if (typeof previousActiveItem !== 'undefined' && typeof previousActiveItem.getPath === 'function') { 83 | cwd = previousActiveItem.getPath() 84 | const dir = atom.project.relativizePath(cwd)[0] 85 | if (dir) { 86 | this.profile.cwd = dir 87 | return 88 | } 89 | } else if (typeof previousActiveItem !== 'undefined' && typeof previousActiveItem.selectedPath === 'string') { 90 | cwd = previousActiveItem.selectedPath 91 | const dir = atom.project.relativizePath(cwd)[0] 92 | if (dir) { 93 | this.profile.cwd = dir 94 | return 95 | } 96 | } else { 97 | cwd = atom.project.getPaths()[0] 98 | } 99 | } else { 100 | cwd = this.profile.cwd 101 | } 102 | 103 | const baseProfile = this.profilesSingleton.getBaseProfile() 104 | if (!cwd) { 105 | this.profile.cwd = baseProfile.cwd 106 | return 107 | } 108 | const exists = await fs.exists(cwd) 109 | if (!exists) { 110 | this.profile.cwd = baseProfile.cwd 111 | return 112 | } 113 | 114 | // Otherwise, if the path exists on the local file system, use the 115 | // path or parent directory as appropriate. 116 | const stats = await fs.stat(cwd) 117 | if (stats.isDirectory()) { 118 | this.profile.cwd = cwd 119 | return 120 | } 121 | 122 | cwd = path.dirname(cwd) 123 | const dirStats = await fs.stat(cwd) 124 | if (dirStats.isDirectory) { 125 | this.profile.cwd = cwd 126 | return 127 | } 128 | 129 | this.profile.cwd = baseProfile.cwd 130 | } 131 | 132 | serialize () { 133 | return { 134 | deserializer: 'XTerminalModel', 135 | version: '2017-09-17', 136 | uri: this.profilesSingleton.generateNewUrlFromProfileData(this.profile).href, 137 | } 138 | } 139 | 140 | destroy () { 141 | if (this.element) { 142 | this.element.destroy() 143 | } 144 | this.terminals_set.delete(this) 145 | } 146 | 147 | getTitle () { 148 | return (this.isActiveTerminal() ? this.profile.activeIndicator + ' ' : '') + this.title 149 | // return this.activeIndex + '|' + this.title 150 | } 151 | 152 | getElement () { 153 | return this.element 154 | } 155 | 156 | getURI () { 157 | return this.uri 158 | } 159 | 160 | getLongTitle () { 161 | if (this.title === DEFAULT_TITLE) { 162 | return DEFAULT_TITLE 163 | } 164 | return DEFAULT_TITLE + ' (' + this.title + ')' 165 | } 166 | 167 | onDidChangeTitle (callback) { 168 | return this.emitter.on('did-change-title', callback) 169 | } 170 | 171 | getIconName () { 172 | return 'terminal' 173 | } 174 | 175 | getPath () { 176 | return this.profile.cwd 177 | } 178 | 179 | isModified () { 180 | return this.modified 181 | } 182 | 183 | onDidChangeModified (callback) { 184 | return this.emitter.on('did-change-modified', callback) 185 | } 186 | 187 | handleNewDataArrival () { 188 | if (!this.pane) { 189 | this.pane = atom.workspace.paneForItem(this) 190 | } 191 | const oldIsModified = this.modified 192 | let item 193 | if (this.pane) { 194 | item = this.pane.getActiveItem() 195 | } 196 | if (item === this) { 197 | this.modified = false 198 | } else { 199 | this.modified = true 200 | } 201 | if (oldIsModified !== this.modified) { 202 | this.emitter.emit('did-change-modified', this.modified) 203 | } 204 | } 205 | 206 | getSessionId () { 207 | return this.sessionId 208 | } 209 | 210 | getSessionParameters () { 211 | const url = this.profilesSingleton.generateNewUrlFromProfileData(this.profile) 212 | url.searchParams.sort() 213 | return url.searchParams.toString() 214 | } 215 | 216 | refitTerminal () { 217 | // Only refit if there's a DOM element attached to the model. 218 | if (this.element) { 219 | this.element.refitTerminal() 220 | } 221 | } 222 | 223 | focusOnTerminal (double) { 224 | if (this.pane) { 225 | this.pane.activateItem(this) 226 | } 227 | this.element.focusOnTerminal(double) 228 | if (this.modified) { 229 | this.modified = false 230 | this.emitter.emit('did-change-modified', this.modified) 231 | } 232 | } 233 | 234 | exit () { 235 | this.pane.destroyItem(this, true) 236 | } 237 | 238 | restartPtyProcess () { 239 | if (this.element) { 240 | this.element.restartPtyProcess() 241 | } 242 | } 243 | 244 | copyFromTerminal () { 245 | return this.element.terminal.getSelection() 246 | } 247 | 248 | runCommand (cmd) { 249 | this.pasteToTerminal(cmd + os.EOL.charAt(0)) 250 | } 251 | 252 | pasteToTerminal (text) { 253 | this.element.ptyProcess.write(text) 254 | } 255 | 256 | clear () { 257 | if (this.element) { 258 | return this.element.clear() 259 | } 260 | } 261 | 262 | setActive () { 263 | recalculateActive(this.terminals_set, this) 264 | } 265 | 266 | isVisible () { 267 | return this.pane && this.pane.getActiveItem() === this && (!this.dock || this.dock.isVisible()) 268 | } 269 | 270 | isActiveTerminal () { 271 | return this.activeIndex === 0 && (atom.config.get('x-terminal-reloaded.terminalSettings.allowHiddenToStayActive') || this.isVisible()) 272 | } 273 | 274 | setNewPane (pane) { 275 | this.pane = pane 276 | const location = this.pane.getContainer().getLocation() 277 | switch (location) { 278 | case 'left': 279 | this.dock = atom.workspace.getLeftDock() 280 | break 281 | case 'right': 282 | this.dock = atom.workspace.getRightDock() 283 | break 284 | case 'bottom': 285 | this.dock = atom.workspace.getBottomDock() 286 | break 287 | default: 288 | this.dock = null 289 | } 290 | } 291 | 292 | toggleProfileMenu () { 293 | this.element.toggleProfileMenu() 294 | } 295 | 296 | /* Public methods are defined below this line. */ 297 | 298 | /** 299 | * Retrieve profile for this {@link XTerminalModel} instance. 300 | * 301 | * @function 302 | * @return {Object} Profile for {@link XTerminalModel} instance. 303 | */ 304 | getProfile () { 305 | return this.profile 306 | } 307 | 308 | /** 309 | * Apply profile changes to {@link XTerminalModel} instance. 310 | * 311 | * @function 312 | * @param {Object} profileChanges Profile changes to apply. 313 | */ 314 | applyProfileChanges (profileChanges) { 315 | profileChanges = this.profilesSingleton.sanitizeData(profileChanges) 316 | this.profile = this.profilesSingleton.deepClone({ 317 | ...this.profile, 318 | ...profileChanges, 319 | }) 320 | this.element.queueNewProfileChanges(profileChanges) 321 | } 322 | } 323 | 324 | function isXTerminalModel (item) { 325 | return (item instanceof XTerminalModel) 326 | } 327 | 328 | function currentItemIsXTerminalModel () { 329 | return isXTerminalModel(atom.workspace.getActivePaneItem()) 330 | } 331 | 332 | export { 333 | XTerminalModel, 334 | isXTerminalModel, 335 | currentItemIsXTerminalModel, 336 | } 337 | -------------------------------------------------------------------------------- /menus/x-terminal.json: -------------------------------------------------------------------------------- 1 | { 2 | "context-menu": { 3 | "x-terminal-reloaded .terminal": [ 4 | { 5 | "label": "Focus On Terminal", 6 | "command": "x-terminal-reloaded:focus" 7 | }, 8 | { 9 | "label": "Open New Terminal (Default)", 10 | "command": "x-terminal-reloaded:open" 11 | }, 12 | { 13 | "label": "Reorganize Terminal Tabs", 14 | "submenu": [ 15 | { 16 | "label": "Current Pane", 17 | "command": "x-terminal-reloaded:reorganize" 18 | }, 19 | { 20 | "label": "Topmost Pane", 21 | "command": "x-terminal-reloaded:reorganize-top" 22 | }, 23 | { 24 | "label": "Bottommost Pane", 25 | "command": "x-terminal-reloaded:reorganize-bottom" 26 | }, 27 | { 28 | "label": "Leftmost Pane", 29 | "command": "x-terminal-reloaded:reorganize-left" 30 | }, 31 | { 32 | "label": "Rightmost Pane", 33 | "command": "x-terminal-reloaded:reorganize-right" 34 | }, 35 | { 36 | "label": "Bottom Dock", 37 | "command": "x-terminal-reloaded:reorganize-bottom-dock" 38 | }, 39 | { 40 | "label": "Left Dock", 41 | "command": "x-terminal-reloaded:reorganize-left-dock" 42 | }, 43 | { 44 | "label": "Right Dock", 45 | "command": "x-terminal-reloaded:reorganize-right-dock" 46 | } 47 | ] 48 | }, 49 | { 50 | "label": "Close All Terminals", 51 | "command": "x-terminal-reloaded:close-all" 52 | }, 53 | { "type": "separator" }, 54 | { 55 | "label": "Toggle Profile Menu", 56 | "command": "x-terminal-reloaded:toggle-profile-menu" 57 | }, 58 | { "label": "Close", "command": "x-terminal-reloaded:close" }, 59 | { "label": "Restart", "command": "x-terminal-reloaded:restart" }, 60 | { "type": "separator" }, 61 | { "label": "Clear Terminal", "command": "x-terminal-reloaded:clear" }, 62 | { "label": "Copy from Terminal", "command": "x-terminal-reloaded:copy" }, 63 | { "label": "Paste to Terminal", "command": "x-terminal-reloaded:paste" }, 64 | { "label": "Reveal in Tree View", "visible": false }, 65 | { "label": "Split Up", "visible": false }, 66 | { "label": "Split Down", "visible": false }, 67 | { "label": "Split Left", "visible": false }, 68 | { "label": "Split Right", "visible": false } 69 | ], 70 | "x-terminal-reloaded-profile": [ 71 | { "label": "Undo", "command": "core:undo" }, 72 | { "label": "Redo", "command": "core:redo" }, 73 | { "type": "separator" }, 74 | { "label": "Cut", "command": "core:cut" }, 75 | { "label": "Copy", "command": "core:copy" }, 76 | { "label": "Paste", "command": "core:paste" }, 77 | { "label": "Delete", "command": "core:delete" }, 78 | { "label": "Reveal in Tree View", "visible": false }, 79 | { "label": "Split Up", "visible": false }, 80 | { "label": "Split Down", "visible": false }, 81 | { "label": "Split Left", "visible": false }, 82 | { "label": "Split Right", "visible": false } 83 | ], 84 | "atom-text-editor, .tree-view, .tab-bar": [ 85 | { 86 | "label": "X-Terminal-Reloaded", 87 | "submenu": [ 88 | { 89 | "label": "Focus On Terminal", 90 | "command": "x-terminal-reloaded:focus" 91 | }, 92 | { 93 | "label": "Open New Terminal (Default)", 94 | "command": "x-terminal-reloaded:open-context-menu" 95 | }, 96 | { 97 | "label": "Open New Terminal (cont.)", 98 | "submenu": [ 99 | { 100 | "label": "Split Up", 101 | "command": "x-terminal-reloaded:open-split-up-context-menu" 102 | }, 103 | { 104 | "label": "Split Down", 105 | "command": "x-terminal-reloaded:open-split-down-context-menu" 106 | }, 107 | { 108 | "label": "Split Left", 109 | "command": "x-terminal-reloaded:open-split-left-context-menu" 110 | }, 111 | { 112 | "label": "Split Right", 113 | "command": "x-terminal-reloaded:open-split-right-context-menu" 114 | }, 115 | { 116 | "label": "Bottom Dock", 117 | "command": "x-terminal-reloaded:open-split-bottom-dock-context-menu" 118 | }, 119 | { 120 | "label": "Left Dock", 121 | "command": "x-terminal-reloaded:open-split-left-dock-context-menu" 122 | }, 123 | { 124 | "label": "Right Dock", 125 | "command": "x-terminal-reloaded:open-split-right-dock-context-menu" 126 | } 127 | ] 128 | }, 129 | { 130 | "label": "Reorganize Terminal Tabs", 131 | "submenu": [ 132 | { 133 | "label": "Current Pane", 134 | "command": "x-terminal-reloaded:reorganize" 135 | }, 136 | { 137 | "label": "Topmost Pane", 138 | "command": "x-terminal-reloaded:reorganize-top" 139 | }, 140 | { 141 | "label": "Bottommost Pane", 142 | "command": "x-terminal-reloaded:reorganize-bottom" 143 | }, 144 | { 145 | "label": "Leftmost Pane", 146 | "command": "x-terminal-reloaded:reorganize-left" 147 | }, 148 | { 149 | "label": "Rightmost Pane", 150 | "command": "x-terminal-reloaded:reorganize-right" 151 | }, 152 | { 153 | "label": "Bottom Dock", 154 | "command": "x-terminal-reloaded:reorganize-bottom-dock" 155 | }, 156 | { 157 | "label": "Left Dock", 158 | "command": "x-terminal-reloaded:reorganize-left-dock" 159 | }, 160 | { 161 | "label": "Right Dock", 162 | "command": "x-terminal-reloaded:reorganize-right-dock" 163 | } 164 | ] 165 | }, 166 | { 167 | "label": "Close All Terminals", 168 | "command": "x-terminal-reloaded:close-all" 169 | } 170 | ] 171 | } 172 | ] 173 | }, 174 | "menu": [ 175 | { 176 | "label": "Packages", 177 | "submenu": [ 178 | { 179 | "label": "X-Terminal-Reloaded", 180 | "submenu": [ 181 | { 182 | "label": "Focus On Terminal", 183 | "command": "x-terminal-reloaded:focus" 184 | }, 185 | { 186 | "label": "Open New Terminal (Default)", 187 | "command": "x-terminal-reloaded:open" 188 | }, 189 | { 190 | "label": "Open New Terminal (cont.)", 191 | "submenu": [ 192 | { 193 | "label": "Split Up", 194 | "command": "x-terminal-reloaded:open-split-up-context-menu" 195 | }, 196 | { 197 | "label": "Split Down", 198 | "command": "x-terminal-reloaded:open-split-down-context-menu" 199 | }, 200 | { 201 | "label": "Split Left", 202 | "command": "x-terminal-reloaded:open-split-left-context-menu" 203 | }, 204 | { 205 | "label": "Split Right", 206 | "command": "x-terminal-reloaded:open-split-right-context-menu" 207 | }, 208 | { 209 | "label": "Bottom Dock", 210 | "command": "x-terminal-reloaded:open-split-bottom-dock-context-menu" 211 | }, 212 | { 213 | "label": "Left Dock", 214 | "command": "x-terminal-reloaded:open-split-left-dock-context-menu" 215 | }, 216 | { 217 | "label": "Right Dock", 218 | "command": "x-terminal-reloaded:open-split-right-dock-context-menu" 219 | } 220 | ] 221 | }, 222 | { 223 | "label": "Reorganize Terminal Tabs", 224 | "submenu": [ 225 | { 226 | "label": "Current Pane", 227 | "command": "x-terminal-reloaded:reorganize" 228 | }, 229 | { 230 | "label": "Topmost Pane", 231 | "command": "x-terminal-reloaded:reorganize-top" 232 | }, 233 | { 234 | "label": "Bottommost Pane", 235 | "command": "x-terminal-reloaded:reorganize-bottom" 236 | }, 237 | { 238 | "label": "Leftmost Pane", 239 | "command": "x-terminal-reloaded:reorganize-left" 240 | }, 241 | { 242 | "label": "Rightmost Pane", 243 | "command": "x-terminal-reloaded:reorganize-right" 244 | }, 245 | { 246 | "label": "Bottom Dock", 247 | "command": "x-terminal-reloaded:reorganize-bottom-dock" 248 | }, 249 | { 250 | "label": "Left Dock", 251 | "command": "x-terminal-reloaded:reorganize-left-dock" 252 | }, 253 | { 254 | "label": "Right Dock", 255 | "command": "x-terminal-reloaded:reorganize-right-dock" 256 | } 257 | ] 258 | }, 259 | { 260 | "label": "Close All Terminals", 261 | "command": "x-terminal-reloaded:close-all" 262 | } 263 | ] 264 | } 265 | ] 266 | } 267 | ] 268 | } 269 | -------------------------------------------------------------------------------- /spec/profile-menu-element-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { XTerminalProfileMenuElementImpl } from '../src/profile-menu-element' 23 | 24 | describe('XTerminalProfileMenuElement', () => { 25 | let element 26 | 27 | beforeEach(async () => { 28 | const model = jasmine.createSpyObj( 29 | 'atomXtermProfileMenuModel', 30 | [ 31 | 'setElement', 32 | 'getXTerminalModel', 33 | 'getXTerminalModelElement', 34 | ], 35 | ) 36 | model.atomXtermModel = jasmine.createSpyObj( 37 | 'atomXtermModel', 38 | [ 39 | 'getProfile', 40 | 'applyProfileChanges', 41 | ], 42 | ) 43 | model.atomXtermModel.getProfile.and.returnValue({}) 44 | model.atomXtermModel.profile = {} 45 | const mock = jasmine.createSpyObj( 46 | 'atomXtermElement', 47 | [ 48 | 'restartPtyProcess', 49 | 'hideTerminal', 50 | 'showTerminal', 51 | 'focusOnTerminal', 52 | ], 53 | ) 54 | model.getXTerminalModel.and.returnValue(model.atomXtermModel) 55 | model.getXTerminalModelElement.and.returnValue(mock) 56 | element = new XTerminalProfileMenuElementImpl() 57 | element.initialize(model) 58 | await element.initializedPromise 59 | }) 60 | 61 | it('initialize()', async () => { 62 | await element.initializedPromise 63 | }) 64 | 65 | it('destroy() disposables not set', () => { 66 | element.disposables = null 67 | element.destroy() 68 | }) 69 | 70 | it('destroy() disposables is set', () => { 71 | element.disposables = jasmine.createSpyObj( 72 | 'disposables', 73 | [ 74 | 'dispose', 75 | ], 76 | ) 77 | element.destroy() 78 | 79 | expect(element.disposables.dispose).toHaveBeenCalled() 80 | }) 81 | 82 | it('getModelProfile()', () => { 83 | const mock = jasmine.createSpy('mock') 84 | element.model.atomXtermModel.profile = mock 85 | 86 | expect(element.getModelProfile()).toBe(mock) 87 | }) 88 | 89 | it('getMenuElements()', () => { 90 | expect(element.getMenuElements()).toBeTruthy() 91 | }) 92 | 93 | it('getProfileMenuSettings()', () => { 94 | const expected = element.profilesSingleton.getBaseProfile() 95 | const actual = element.getProfileMenuSettings() 96 | 97 | expect(actual).toEqual(expected) 98 | }) 99 | 100 | it('applyProfileChanges()', () => { 101 | element.applyProfileChanges('foo') 102 | 103 | expect(element.model.getXTerminalModel().applyProfileChanges).toHaveBeenCalledWith('foo') 104 | }) 105 | 106 | it('applyProfileChanges() profile menu hidden', () => { 107 | spyOn(element, 'hideProfileMenu') 108 | element.applyProfileChanges('foo') 109 | 110 | expect(element.hideProfileMenu).toHaveBeenCalled() 111 | }) 112 | 113 | it('restartTerminal()', () => { 114 | element.restartTerminal() 115 | 116 | expect(element.model.getXTerminalModelElement().restartPtyProcess).toHaveBeenCalled() 117 | }) 118 | 119 | it('restartTerminal() profile menu hidden', () => { 120 | spyOn(element, 'hideProfileMenu') 121 | element.restartTerminal() 122 | 123 | expect(element.hideProfileMenu).toHaveBeenCalled() 124 | }) 125 | 126 | it('createMenuItemContainer() check id', () => { 127 | const container = element.createMenuItemContainer('foo', 'bar', 'baz') 128 | 129 | expect(container.getAttribute('id')).toBe('foo') 130 | }) 131 | 132 | it('createMenuItemContainer() check title', () => { 133 | const container = element.createMenuItemContainer('foo', 'bar', 'baz') 134 | const titleDiv = container.querySelector('.x-terminal-reloaded-profile-menu-item-title') 135 | 136 | expect(titleDiv.textContent).toBe('bar') 137 | }) 138 | 139 | it('createMenuItemContainer() check description', () => { 140 | const container = element.createMenuItemContainer('foo', 'bar', 'baz') 141 | const descriptionDiv = container.querySelector('.x-terminal-reloaded-profile-menu-item-description') 142 | 143 | expect(descriptionDiv.innerHTML).toBe('

baz

\n') 144 | }) 145 | 146 | it('createProfilesDropDownSelectItem() check id', async () => { 147 | const select = await element.createProfilesDropDownSelectItem() 148 | 149 | expect(select.getAttribute('id')).toBe('profiles-dropdown') 150 | }) 151 | 152 | it('createProfilesDropDownSelectItem() check classList', async () => { 153 | const select = await element.createProfilesDropDownSelectItem() 154 | 155 | expect(select.classList.contains('x-terminal-reloaded-profile-menu-item-select')).toBe(true) 156 | }) 157 | 158 | it('createProfilesDropDown()', async () => { 159 | const menuItemContainer = await element.createProfilesDropDown() 160 | 161 | expect(menuItemContainer.getAttribute('id')).toBe('profiles-selection') 162 | }) 163 | 164 | it('createProfileMenuButtons()', () => { 165 | const buttonsContainer = element.createProfileMenuButtons() 166 | 167 | expect(buttonsContainer.classList.contains('x-terminal-reloaded-profile-menu-buttons-div')).toBe(true) 168 | }) 169 | 170 | it('createButton()', () => { 171 | const button = element.createButton() 172 | 173 | expect(button.classList.contains('x-terminal-reloaded-profile-menu-button')).toBe(true) 174 | }) 175 | 176 | it('createTextbox()', () => { 177 | const menuItemContainer = element.createTextbox('foo', 'bar', 'baz', 'cat', 'dog') 178 | 179 | expect(menuItemContainer.getAttribute('id')).toBe('foo') 180 | }) 181 | 182 | it('createCheckbox()', () => { 183 | const menuItemContainer = element.createCheckbox('foo', 'bar', 'baz', true, false) 184 | 185 | expect(menuItemContainer.getAttribute('id')).toBe('foo') 186 | }) 187 | 188 | it('isVisible() initial value', () => { 189 | expect(element.isVisible()).toBe(false) 190 | }) 191 | 192 | it('hideProfileMenu()', () => { 193 | element.hideProfileMenu() 194 | 195 | expect(element.style.visibility).toBe('hidden') 196 | }) 197 | 198 | it('hideProfileMenu() terminal shown', () => { 199 | element.hideProfileMenu() 200 | 201 | expect(element.model.getXTerminalModelElement().showTerminal).toHaveBeenCalled() 202 | }) 203 | 204 | it('hideProfileMenu() terminal focused', () => { 205 | element.hideProfileMenu() 206 | 207 | expect(element.model.getXTerminalModelElement().focusOnTerminal).toHaveBeenCalled() 208 | }) 209 | 210 | it('showProfileMenu()', () => { 211 | element.showProfileMenu() 212 | 213 | expect(element.style.visibility).toBe('visible') 214 | }) 215 | 216 | it('showProfileMenu() terminal hidden', () => { 217 | element.showProfileMenu() 218 | 219 | expect(element.model.getXTerminalModelElement().hideTerminal).toHaveBeenCalled() 220 | }) 221 | 222 | it('toggleProfileMenu() currently hidden', () => { 223 | spyOn(element, 'isVisible').and.returnValue(false) 224 | spyOn(element, 'showProfileMenu') 225 | element.toggleProfileMenu() 226 | 227 | expect(element.showProfileMenu).toHaveBeenCalled() 228 | }) 229 | 230 | it('toggleProfileMenu() currently visible', () => { 231 | spyOn(element, 'isVisible').and.returnValue(true) 232 | spyOn(element, 'hideProfileMenu') 233 | element.toggleProfileMenu() 234 | 235 | expect(element.hideProfileMenu).toHaveBeenCalled() 236 | }) 237 | 238 | it('getNewProfileAndChanges()', () => { 239 | spyOn(element, 'getProfileMenuSettings').and.returnValue({ 240 | args: [ 241 | '--foo', 242 | '--bar', 243 | '--baz', 244 | ], 245 | }) 246 | element.model.atomXtermModel.getProfile.and.returnValue({ 247 | command: 'somecommand', 248 | }) 249 | const expected = { 250 | newProfile: { 251 | args: [ 252 | '--foo', 253 | '--bar', 254 | '--baz', 255 | ], 256 | }, 257 | profileChanges: { 258 | args: [ 259 | '--foo', 260 | '--bar', 261 | '--baz', 262 | ], 263 | }, 264 | } 265 | const actual = element.getNewProfileAndChanges() 266 | 267 | expect(actual).toEqual(expected) 268 | }) 269 | 270 | it('loadProfile()', () => { 271 | spyOn(element, 'applyProfileChanges') 272 | element.loadProfile() 273 | 274 | expect(element.applyProfileChanges).toHaveBeenCalled() 275 | }) 276 | 277 | it('saveProfile()', () => { 278 | spyOn(element, 'promptForNewProfileName') 279 | element.saveProfile() 280 | 281 | expect(element.promptForNewProfileName).toHaveBeenCalled() 282 | }) 283 | 284 | it('deleteProfile() nothing selected', () => { 285 | spyOn(element, 'promptDelete') 286 | element.deleteProfile() 287 | 288 | expect(element.promptDelete).not.toHaveBeenCalled() 289 | }) 290 | 291 | it('deleteProfile() option selected', () => { 292 | spyOn(element, 'promptDelete') 293 | const mock = jasmine.createSpy('mock') 294 | mock.options = [{ text: 'foo' }] 295 | mock.selectedIndex = 0 296 | spyOn(element.mainDiv, 'querySelector').and.returnValue(mock) 297 | element.deleteProfile() 298 | 299 | expect(element.promptDelete).toHaveBeenCalledWith('foo') 300 | }) 301 | 302 | it('promptDelete()', (done) => { 303 | spyOn(element.deleteProfileModel, 'promptDelete').and.callFake((newProfile) => { 304 | expect(newProfile).toBe('foo') 305 | done() 306 | }) 307 | element.promptDelete('foo') 308 | }) 309 | 310 | it('promptForNewProfileName()', (done) => { 311 | spyOn(element.saveProfileModel, 'promptForNewProfileName').and.callFake((newProfile, profileChanges) => { 312 | expect(newProfile).toBe('foo') 313 | expect(profileChanges).toBe('bar') 314 | done() 315 | }) 316 | element.promptForNewProfileName('foo', 'bar') 317 | }) 318 | 319 | it('setNewMenuSettings()', () => { 320 | element.setNewMenuSettings(element.profilesSingleton.getBaseProfile()) 321 | }) 322 | 323 | it('setNewMenuSettings() clear = true', () => { 324 | element.setNewMenuSettings(element.profilesSingleton.getBaseProfile(), true) 325 | }) 326 | }) 327 | -------------------------------------------------------------------------------- /styles/x-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 | // The ui-variables file is provided by whatever theme is selected by the 21 | // user at runtime. See also 22 | // https://flight-manual.atom.io/hacking-atom/sections/creating-a-theme/#creating-a-ui-theme . 23 | @import url("ui-variables"); 24 | @import url("octicon-utf-codes"); 25 | @import url("octicon-mixins"); 26 | @import url("syntax-variables"); 27 | 28 | @profile-menu-background-color: lighten(@app-background-color, 2%); 29 | @component-size: @component-icon-size; // use for text-less controls like radio, checkboxes etc. 30 | @component-background-color: mix(@text-color, @base-background-color, 20%); 31 | @btn-border: 1px solid @button-border-color; 32 | @ui-size: 1em; 33 | @btn-padding: 0 @ui-size/1.25; 34 | @accent-luma: luma( hsl(@ui-hue, 50%, 50%) ); // get lightness of current hue 35 | @accent-color: mix( hsv( @ui-hue, 100%, 66%), hsl( @ui-hue, 100%, 70%), @accent-luma ); 36 | @ui-syntax-color: @syntax-background-color; 37 | @ui-s-h: hue(@ui-syntax-color); 38 | .ui-hue() when (@ui-s-h = 0) { @ui-hue: 220; } // Use blue hue when no saturation 39 | .ui-hue() when (@ui-s-h > 0) { @ui-hue: @ui-s-h; } 40 | .ui-hue(); 41 | 42 | :root { 43 | --standard-app-background-color: @app-background-color; 44 | --standard-text-color: @text-color; 45 | --standard-background-color-selected: @background-color-selected; 46 | --standard-text-color-highlight: @text-color-highlight; 47 | } 48 | 49 | x-terminal-reloaded { 50 | display: flex; 51 | flex-direction: column; 52 | } 53 | 54 | x-terminal-reloaded-profile { 55 | width: 100%; 56 | height: 100%; 57 | position: absolute; 58 | top: 0; 59 | left: 0; 60 | flex: auto; 61 | color: @text-color; 62 | background-color: @app-background-color; 63 | overflow: auto; 64 | visibility: hidden; 65 | display: grid; 66 | grid-template-columns: (@component-padding * 5) auto (@component-padding * 5); /* stylelint-disable-line declaration-property-value-no-unknown */ 67 | grid-template-rows: (@component-padding * 5) auto (@component-padding * 5); /* stylelint-disable-line declaration-property-value-no-unknown */ 68 | grid-template-areas: "top top top" "left main right" "bottom bottom bottom"; 69 | } 70 | 71 | .x-terminal-reloaded-restart-btn { 72 | margin-left: @component-padding; 73 | margin-right: @component-padding; 74 | } 75 | 76 | .x-terminal-reloaded-main-div { 77 | height: 95%; 78 | width: auto; 79 | flex: auto; 80 | } 81 | 82 | .x-terminal-reloaded-profile-menu-element-top-div { 83 | grid-area: top; 84 | } 85 | 86 | .x-terminal-reloaded-profile-menu-element-bottom-div { 87 | grid-area: bottom; 88 | } 89 | 90 | .x-terminal-reloaded-profile-menu-element-left-div { 91 | grid-area: left; 92 | } 93 | 94 | .x-terminal-reloaded-profile-menu-element-right-div { 95 | grid-area: right; 96 | } 97 | 98 | .x-terminal-reloaded-profile-menu-element-main-div { 99 | grid-area: main; 100 | padding: @component-padding * 5; 101 | border-radius: @component-border-radius * 2; 102 | border: @btn-border; 103 | background-color: @profile-menu-background-color; 104 | margin: -35px; 105 | } 106 | 107 | .x-terminal-reloaded-profile-menu-element-hline { 108 | width: 100%; 109 | color: @base-border-color; 110 | line-height: 2px; 111 | background-color: @base-border-color; 112 | margin-top: 20px; 113 | margin-bottom: 10px; 114 | } 115 | 116 | .x-terminal-reloaded-profile-menu-item { 117 | padding: 0.8em 0; 118 | overflow: auto; 119 | } 120 | 121 | .x-terminal-reloaded-profile-menu-item-label { 122 | user-select: none; 123 | cursor: default; 124 | display: block; 125 | max-width: 100%; 126 | margin-bottom: 5px; 127 | background-color: @profile-menu-background-color; 128 | margin-left: 2px; 129 | } 130 | 131 | .x-terminal-reloaded-profile-menu-item-label-checkbox { 132 | padding-left: 2.2em; 133 | margin-top: -20px; 134 | 135 | .x-terminal-reloaded-profile-menu-item-checkbox { 136 | margin-left: -2.2em; 137 | vertical-align: bottom; 138 | position: relative; 139 | top: 20px; 140 | } 141 | } 142 | 143 | .x-terminal-reloaded-profile-menu-item-description-checkbox { 144 | padding-left: 2.25em; 145 | margin-left: 2px; 146 | margin-top: -0.5em; 147 | } 148 | 149 | .x-terminal-reloaded-profile-menu-item-title { 150 | font-size: @font-size + 4px; 151 | user-select: none; 152 | color: @text-color; 153 | } 154 | 155 | .x-terminal-reloaded-profile-menu-item-description { 156 | color: @text-color-subtle; 157 | user-select: none; 158 | cursor: default; 159 | 160 | &:empty { 161 | display: none; 162 | } 163 | 164 | p { 165 | margin-bottom: 0; 166 | } 167 | 168 | a { 169 | color: @accent-color; 170 | } 171 | } 172 | 173 | .x-terminal-reloaded-profile-menu-item-select { 174 | color: @text-color; 175 | border-color: @button-border-color; 176 | border-radius: @component-border-radius; 177 | background-color: @button-background-color; 178 | height: 2em; 179 | width: 100%; 180 | font-size: 1.25em; 181 | padding-top: 0; 182 | padding-bottom: 0; 183 | padding-left: @component-padding; 184 | 185 | &:focus, 186 | &:hover { 187 | box-shadow: none; 188 | background-color: @button-background-color-hover; 189 | } 190 | } 191 | 192 | .x-terminal-reloaded-profile-menu-item-label-color { 193 | padding-left: 5em; 194 | margin-top: -20px; 195 | 196 | .x-terminal-reloaded-profile-menu-item-color { 197 | border-color: @button-border-color; 198 | background-color: @button-background-color; 199 | width: 4em; 200 | height: 1.6em; 201 | margin-left: -5em; 202 | padding: 0; 203 | vertical-align: bottom; 204 | position: relative; 205 | left: -2px; 206 | top: 22px; 207 | 208 | &::-webkit-color-swatch-wrapper { 209 | padding: 2px; 210 | margin: 0; 211 | border-radius: inherit; 212 | } 213 | 214 | &::-webkit-color-swatch { 215 | border-radius: @component-border-radius; 216 | border: 1px solid hsla(0, 0%, 0%, 0.1); /* stylelint-disable-line */ 217 | } 218 | 219 | &:hover { 220 | box-shadow: none; 221 | background-color: @button-background-color-hover; 222 | } 223 | 224 | &:focus { 225 | background-color: @button-background-color-hover; 226 | border-color: @accent-color; 227 | border-radius: @component-border-radius; 228 | } 229 | } 230 | } 231 | 232 | .x-terminal-reloaded-profile-menu-item-description-color { 233 | padding-left: 5em; 234 | margin-left: 2px; 235 | margin-top: -0.5em; 236 | } 237 | 238 | .x-terminal-reloaded-profile-menu-buttons-div { 239 | padding-top: 1em; 240 | } 241 | 242 | .x-terminal-reloaded-modal-button, 243 | .x-terminal-reloaded-profile-menu-button { 244 | height: @component-line-height + 2px; 245 | padding: @btn-padding; 246 | font-size: @font-size + 2px; 247 | border: @btn-border; 248 | border-radius: @component-border-radius; 249 | background-color: @button-background-color; 250 | 251 | &.btn-load { 252 | &::before { 253 | content: @settings; 254 | .octicon-font(); 255 | 256 | margin-right: @component-padding; 257 | } 258 | } 259 | 260 | &.btn-save { 261 | &::before { 262 | content: @file; 263 | .octicon-font(); 264 | 265 | margin-right: @component-padding; 266 | } 267 | } 268 | 269 | &.btn-delete { 270 | &::before { 271 | content: @trashcan; 272 | .octicon-font(); 273 | 274 | margin-right: @component-padding; 275 | } 276 | } 277 | 278 | &.btn-restart { 279 | &::before { 280 | content: @terminal; 281 | .octicon-font(); 282 | 283 | margin-right: @component-padding; 284 | } 285 | } 286 | 287 | &.btn-hide { 288 | &::before { 289 | content: @sign-out; 290 | .octicon-font(); 291 | 292 | margin-right: @component-padding; 293 | } 294 | 295 | &:hover { 296 | background-color: @button-background-color-hover; 297 | color: lighten(@text-color, 20%); /* stylelint-disable-line declaration-property-value-no-unknown */ 298 | } 299 | 300 | &:focus { 301 | background-color: @button-background-color-hover; 302 | color: lighten(@text-color, 20%); /* stylelint-disable-line declaration-property-value-no-unknown */ 303 | border-color: @accent-color; 304 | } 305 | } 306 | } 307 | 308 | .x-terminal-reloaded-modal-message { 309 | font-size: @font-size + 4px; 310 | margin-top: 0.6em; 311 | 312 | &::before { 313 | content: @arrow-right; 314 | .octicon-font(); 315 | 316 | margin-right: @component-padding; 317 | } 318 | } 319 | 320 | .x-terminal-reloaded-term-container { 321 | height: 100%; 322 | width: 100%; 323 | flex: auto; 324 | } 325 | 326 | .x-terminal-reloaded-notice-type(@tx; @bg) { 327 | @x-terminal-reloaded-notice-background-color: mix(@bg, @base-background-color, 10%); 328 | 329 | color: contrast(@x-terminal-reloaded-notice-background-color, darken(@tx, 20%), lighten(@tx, 20%)); /* stylelint-disable-line declaration-property-value-no-unknown */ 330 | border-color: lighten(@x-terminal-reloaded-notice-background-color, 10%); /* stylelint-disable-line declaration-property-value-no-unknown */ 331 | background-color: @x-terminal-reloaded-notice-background-color; 332 | border-style: solid; 333 | font-size: @input-font-size; 334 | height: 100%; 335 | width: 100%; 336 | flex: none; 337 | 338 | .x-terminal-reloaded-link { 339 | color: @accent-color; 340 | text-decoration: underline; 341 | } 342 | } 343 | 344 | .x-terminal-reloaded-notice-info { 345 | .x-terminal-reloaded-notice-type(@text-color-info; @background-color-info); 346 | } 347 | 348 | .x-terminal-reloaded-notice-success { 349 | .x-terminal-reloaded-notice-type(@text-color-success; @background-color-success); 350 | } 351 | 352 | .x-terminal-reloaded-notice-warning { 353 | .x-terminal-reloaded-notice-type(@text-color-warning; @background-color-warning); 354 | } 355 | 356 | .x-terminal-reloaded-notice-error { 357 | .x-terminal-reloaded-notice-type(@text-color-error; @background-color-error); 358 | } 359 | 360 | // Load the static styles here. 361 | /* stylelint-disable-next-line */ 362 | @import (less) "../node_modules/@xterm/xterm/css/xterm.css"; 363 | -------------------------------------------------------------------------------- /spec/x-terminal-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | import * as xTerminal from '../src/x-terminal' 4 | 5 | const xTerminalInstance = xTerminal.getInstance() 6 | 7 | describe('x-terminal-reloaded', () => { 8 | beforeEach(async () => { 9 | await xTerminalInstance.activate() 10 | }) 11 | 12 | afterEach(async () => { 13 | await xTerminalInstance.deactivate() 14 | }) 15 | 16 | describe('getSelectedText()', () => { 17 | it('returns selection', () => { 18 | spyOn(atom.workspace, 'getActiveTextEditor').and.returnValue({ 19 | getSelectedText () { 20 | return 'selection' 21 | }, 22 | }) 23 | const selection = xTerminalInstance.getSelectedText() 24 | 25 | expect(selection).toBe('selection') 26 | }) 27 | 28 | it('returns removes newlines at the end', () => { 29 | spyOn(atom.workspace, 'getActiveTextEditor').and.returnValue({ 30 | getSelectedText () { 31 | return 'line1\r\nline2\r\n' 32 | }, 33 | }) 34 | const selection = xTerminalInstance.getSelectedText() 35 | 36 | expect(selection).toBe('line1\r\nline2') 37 | }) 38 | 39 | it('returns entire line if nothing selected and moves down', () => { 40 | const moveDown = jasmine.createSpy('moveDown') 41 | spyOn(atom.workspace, 'getActiveTextEditor').and.returnValue({ 42 | getSelectedText () { 43 | return '' 44 | }, 45 | getCursorBufferPosition () { 46 | return { row: 1, column: 1 } 47 | }, 48 | lineTextForBufferRow (row) { 49 | return `line${row}` 50 | }, 51 | moveDown, 52 | }) 53 | const selection = xTerminalInstance.getSelectedText() 54 | 55 | expect(selection).toBe('line1') 56 | expect(moveDown).toHaveBeenCalledWith(1) 57 | }) 58 | }) 59 | 60 | describe('unfocus()', () => { 61 | it('focuses atom-workspace', async () => { 62 | jasmine.attachToDOM(atom.views.getView(atom.workspace)) 63 | const model = await xTerminalInstance.openInCenterOrDock(atom.workspace) 64 | await model.initializedPromise 65 | await model.element.createTerminal() 66 | 67 | expect(model.element).toHaveFocus() 68 | xTerminalInstance.unfocus() 69 | 70 | expect(model.element).not.toHaveFocus() 71 | }) 72 | }) 73 | 74 | describe('focus()', () => { 75 | it('opens new terminal', async () => { 76 | const workspace = atom.views.getView(atom.workspace) 77 | jasmine.attachToDOM(workspace) 78 | workspace.focus() 79 | spyOn(xTerminalInstance, 'open') 80 | 81 | expect(xTerminalInstance.open).not.toHaveBeenCalled() 82 | xTerminalInstance.focus() 83 | 84 | expect(xTerminalInstance.open).toHaveBeenCalledTimes(1) 85 | }) 86 | 87 | it('focuses terminal', async () => { 88 | const workspace = atom.views.getView(atom.workspace) 89 | jasmine.attachToDOM(workspace) 90 | const model = await xTerminalInstance.openInCenterOrDock(atom.workspace) 91 | await model.initializedPromise 92 | await model.element.createTerminal() 93 | workspace.focus() 94 | 95 | expect(model.element).not.toHaveFocus() 96 | xTerminalInstance.focus() 97 | 98 | expect(model.element).toHaveFocus() 99 | }) 100 | }) 101 | 102 | describe('focus-next', () => { 103 | it('opens new terminal', async () => { 104 | const workspace = atom.views.getView(atom.workspace) 105 | jasmine.attachToDOM(workspace) 106 | workspace.focus() 107 | spyOn(xTerminalInstance, 'open') 108 | 109 | expect(xTerminalInstance.open).not.toHaveBeenCalled() 110 | xTerminalInstance.focus(1) 111 | 112 | expect(xTerminalInstance.open).toHaveBeenCalledTimes(1) 113 | }) 114 | 115 | it('focuses next terminal', async () => { 116 | const workspace = atom.views.getView(atom.workspace) 117 | jasmine.attachToDOM(workspace) 118 | const terminals = [] 119 | for (let i = 0; i < 3; i++) { 120 | terminals[i] = await xTerminalInstance.openInCenterOrDock(atom.workspace) 121 | await terminals[i].initializedPromise 122 | await terminals[i].element.createTerminal() 123 | } 124 | 125 | expect(terminals[2].element).toHaveFocus() 126 | xTerminalInstance.focusNext() 127 | 128 | expect(terminals[0].element).toHaveFocus() 129 | xTerminalInstance.focusNext() 130 | 131 | expect(terminals[1].element).toHaveFocus() 132 | xTerminalInstance.focusNext() 133 | 134 | expect(terminals[2].element).toHaveFocus() 135 | xTerminalInstance.focusNext() 136 | 137 | expect(terminals[0].element).toHaveFocus() 138 | }) 139 | }) 140 | 141 | describe('focus-previous', () => { 142 | it('opens new terminal', async () => { 143 | const workspace = atom.views.getView(atom.workspace) 144 | jasmine.attachToDOM(workspace) 145 | workspace.focus() 146 | spyOn(xTerminalInstance, 'open') 147 | 148 | expect(xTerminalInstance.open).not.toHaveBeenCalled() 149 | xTerminalInstance.focus(-1) 150 | 151 | expect(xTerminalInstance.open).toHaveBeenCalledTimes(1) 152 | }) 153 | 154 | it('focuses prev terminal', async () => { 155 | const workspace = atom.views.getView(atom.workspace) 156 | jasmine.attachToDOM(workspace) 157 | const terminals = [] 158 | for (let i = 0; i < 3; i++) { 159 | terminals[i] = await xTerminalInstance.openInCenterOrDock(atom.workspace) 160 | await terminals[i].initializedPromise 161 | await terminals[i].element.createTerminal() 162 | } 163 | 164 | expect(terminals[2].element).toHaveFocus() 165 | xTerminalInstance.focusPrev() 166 | 167 | expect(terminals[1].element).toHaveFocus() 168 | xTerminalInstance.focusPrev() 169 | 170 | expect(terminals[0].element).toHaveFocus() 171 | xTerminalInstance.focusPrev() 172 | 173 | expect(terminals[2].element).toHaveFocus() 174 | xTerminalInstance.focusPrev() 175 | 176 | expect(terminals[1].element).toHaveFocus() 177 | }) 178 | }) 179 | 180 | describe('runCommands()', () => { 181 | let activeTerminal, newTerminal, commands 182 | beforeEach(() => { 183 | activeTerminal = { 184 | element: { 185 | initializedPromise: Promise.resolve(), 186 | }, 187 | runCommand: jasmine.createSpy('activeTerminal.runCommand'), 188 | } 189 | newTerminal = { 190 | element: { 191 | initializedPromise: Promise.resolve(), 192 | }, 193 | runCommand: jasmine.createSpy('newTerminal.runCommand'), 194 | } 195 | commands = [ 196 | 'command 1', 197 | 'command 2', 198 | ] 199 | spyOn(xTerminalInstance, 'getActiveTerminal').and.returnValue(activeTerminal) 200 | spyOn(xTerminalInstance, 'open').and.returnValue(newTerminal) 201 | }) 202 | 203 | it('runs commands in new terminal', async () => { 204 | await xTerminalInstance.runCommands(commands) 205 | 206 | expect(xTerminalInstance.getActiveTerminal).not.toHaveBeenCalled() 207 | expect(newTerminal.runCommand).toHaveBeenCalledWith('command 1') 208 | expect(newTerminal.runCommand).toHaveBeenCalledWith('command 2') 209 | }) 210 | 211 | it('runs commands in active terminal', async () => { 212 | atom.config.set('x-terminal-reloaded.terminalSettings.runInActive', true) 213 | await xTerminalInstance.runCommands(commands) 214 | 215 | expect(xTerminalInstance.open).not.toHaveBeenCalled() 216 | expect(activeTerminal.runCommand).toHaveBeenCalledWith('command 1') 217 | expect(activeTerminal.runCommand).toHaveBeenCalledWith('command 2') 218 | }) 219 | 220 | it('runs commands in new terminal if none active', async () => { 221 | xTerminalInstance.getActiveTerminal.and.returnValue() 222 | atom.config.set('x-terminal-reloaded.terminalSettings.runInActive', true) 223 | await xTerminalInstance.runCommands(commands) 224 | 225 | expect(xTerminalInstance.getActiveTerminal).toHaveBeenCalled() 226 | expect(newTerminal.runCommand).toHaveBeenCalledWith('command 1') 227 | expect(newTerminal.runCommand).toHaveBeenCalledWith('command 2') 228 | }) 229 | }) 230 | 231 | describe('close()', () => { 232 | let activeTerminal 233 | beforeEach(() => { 234 | activeTerminal = { 235 | element: { 236 | initializedPromise: Promise.resolve(), 237 | }, 238 | exit: jasmine.createSpy('activeTerminal.exit'), 239 | } 240 | spyOn(xTerminalInstance, 'getActiveTerminal').and.returnValue(activeTerminal) 241 | }) 242 | 243 | it('closes terminal', async () => { 244 | await xTerminalInstance.close() 245 | 246 | expect(activeTerminal.exit).toHaveBeenCalled() 247 | }) 248 | }) 249 | 250 | describe('restart()', () => { 251 | let activeTerminal 252 | beforeEach(() => { 253 | activeTerminal = { 254 | element: { 255 | initializedPromise: Promise.resolve(), 256 | }, 257 | restartPtyProcess: jasmine.createSpy('activeTerminal.restartPtyProcess'), 258 | } 259 | spyOn(xTerminalInstance, 'getActiveTerminal').and.returnValue(activeTerminal) 260 | }) 261 | 262 | it('restarts terminal', async () => { 263 | await xTerminalInstance.restart() 264 | 265 | expect(activeTerminal.restartPtyProcess).toHaveBeenCalled() 266 | }) 267 | }) 268 | 269 | describe('copy()', () => { 270 | let activeTerminal 271 | beforeEach(() => { 272 | activeTerminal = { 273 | element: { 274 | initializedPromise: Promise.resolve(), 275 | }, 276 | copyFromTerminal: jasmine.createSpy('activeTerminal.copy').and.returnValue('copied'), 277 | } 278 | spyOn(xTerminalInstance, 'getActiveTerminal').and.returnValue(activeTerminal) 279 | spyOn(atom.clipboard, 'write') 280 | }) 281 | 282 | it('copys terminal', async () => { 283 | await xTerminalInstance.copy() 284 | 285 | expect(atom.clipboard.write).toHaveBeenCalledWith('copied') 286 | }) 287 | }) 288 | 289 | describe('paste()', () => { 290 | let activeTerminal 291 | beforeEach(() => { 292 | activeTerminal = { 293 | element: { 294 | initializedPromise: Promise.resolve(), 295 | }, 296 | pasteToTerminal: jasmine.createSpy('activeTerminal.paste'), 297 | } 298 | spyOn(xTerminalInstance, 'getActiveTerminal').and.returnValue(activeTerminal) 299 | spyOn(atom.clipboard, 'read').and.returnValue('copied') 300 | }) 301 | 302 | it('pastes terminal', async () => { 303 | await xTerminalInstance.paste() 304 | 305 | expect(activeTerminal.pasteToTerminal).toHaveBeenCalledWith('copied') 306 | }) 307 | }) 308 | 309 | describe('clear()', () => { 310 | let activeTerminal 311 | beforeEach(() => { 312 | activeTerminal = { 313 | element: { 314 | initializedPromise: Promise.resolve(), 315 | }, 316 | clear: jasmine.createSpy('activeTerminal.clear'), 317 | } 318 | spyOn(xTerminalInstance, 'getActiveTerminal').and.returnValue(activeTerminal) 319 | }) 320 | 321 | it('clears terminal', async () => { 322 | await xTerminalInstance.clear() 323 | 324 | expect(activeTerminal.clear).toHaveBeenCalled() 325 | }) 326 | }) 327 | 328 | describe('open()', () => { 329 | let uri 330 | beforeEach(() => { 331 | uri = xTerminalInstance.profilesSingleton.generateNewUri() 332 | spyOn(atom.workspace, 'open') 333 | }) 334 | 335 | it('simple', async () => { 336 | await xTerminalInstance.open(uri) 337 | 338 | expect(atom.workspace.open).toHaveBeenCalledWith(uri, {}) 339 | }) 340 | 341 | it('target to cwd', async () => { 342 | const testPath = '/test/path' 343 | spyOn(xTerminalInstance, 'getPath').and.returnValue(testPath) 344 | await xTerminalInstance.open( 345 | uri, 346 | { target: true }, 347 | ) 348 | 349 | const url = new URL(atom.workspace.open.calls.mostRecent().args[0]) 350 | 351 | expect(url.searchParams.get('cwd')).toBe(testPath) 352 | }) 353 | }) 354 | }) 355 | 356 | describe('x-terminal-reloaded services', () => { 357 | beforeEach(async () => { 358 | atom.packages.triggerDeferredActivationHooks() 359 | atom.packages.triggerActivationHook('core:loaded-shell-environment') 360 | await atom.packages.activatePackage('x-terminal-reloaded') 361 | }) 362 | 363 | it('terminal.run', async () => { 364 | spyOn(xTerminalInstance, 'runCommands') 365 | const service = await new Promise(resolve => { 366 | atom.packages.serviceHub.consume('terminal', '^1.0.0', resolve) 367 | }) 368 | service.run(['test']) 369 | 370 | expect(xTerminalInstance.runCommands).toHaveBeenCalledWith(['test']) 371 | }) 372 | 373 | it('platformioIDETerminal.run', async () => { 374 | spyOn(xTerminalInstance, 'runCommands') 375 | const service = await new Promise(resolve => { 376 | atom.packages.serviceHub.consume('platformioIDETerminal', '^1.1.0', resolve) 377 | }) 378 | service.run(['test']) 379 | 380 | expect(xTerminalInstance.runCommands).toHaveBeenCalledWith(['test']) 381 | }) 382 | 383 | it('atom-xterm.openTerminal', async () => { 384 | spyOn(xTerminalInstance, 'openTerminal') 385 | const service = await new Promise(resolve => { 386 | atom.packages.serviceHub.consume('atom-xterm', '^2.0.0', resolve) 387 | }) 388 | service.openTerminal({}) 389 | 390 | expect(xTerminalInstance.openTerminal).toHaveBeenCalledWith({}) 391 | }) 392 | }) 393 | -------------------------------------------------------------------------------- /spec/config-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 bus-stop All Rights Reserved. 6 | * Copyright (c) 2020 UziTech All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { configDefaults, resetConfigDefaults, setInitialCommand } from '../src/config' 23 | 24 | import os from 'os' 25 | import path from 'path' 26 | 27 | describe('config', () => { 28 | describe('debug', () => { 29 | it('return false', () => { 30 | expect(configDefaults.debug).toBe(false) 31 | }) 32 | }) 33 | 34 | describe('shellCommand', () => { 35 | const savedPlatform = process.platform 36 | const savedEnv = JSON.parse(JSON.stringify(process.env)) 37 | 38 | afterEach(() => { 39 | process.env = savedEnv 40 | Object.defineProperty(process, 'platform', { 41 | value: savedPlatform, 42 | }) 43 | }) 44 | 45 | it('on win32 without COMSPEC set', () => { 46 | Object.defineProperty(process, 'platform', { 47 | value: 'win32', 48 | }) 49 | if (process.env.COMSPEC) { 50 | delete process.env.COMSPEC 51 | } 52 | 53 | expect(resetConfigDefaults().command).toBe('cmd.exe') 54 | }) 55 | 56 | it('on win32 with COMSPEC set', () => { 57 | Object.defineProperty(process, 'platform', { 58 | value: 'win32', 59 | }) 60 | const expected = 'somecommand.exe' 61 | process.env.COMSPEC = expected 62 | 63 | expect(resetConfigDefaults().command).toBe(expected) 64 | }) 65 | 66 | it('on linux without SHELL set', () => { 67 | Object.defineProperty(process, 'platform', { 68 | value: 'linux', 69 | }) 70 | if (process.env.SHELL) { 71 | delete process.env.SHELL 72 | } 73 | 74 | expect(resetConfigDefaults().command).toBe('/bin/sh') 75 | }) 76 | 77 | it('on linux with SHELL set', () => { 78 | Object.defineProperty(process, 'platform', { 79 | value: 'linux', 80 | }) 81 | const expected = 'somecommand' 82 | process.env.SHELL = expected 83 | 84 | expect(resetConfigDefaults().command).toBe(expected) 85 | }) 86 | }) 87 | 88 | describe('activeIndicator', () => { 89 | it('return *', () => { 90 | expect(configDefaults.activeIndicator).toBe('*') 91 | }) 92 | }) 93 | 94 | describe('args', () => { 95 | it('return []', () => { 96 | expect(configDefaults.args).toBe('[]') 97 | }) 98 | }) 99 | 100 | describe('termType', () => { 101 | let savedEnv 102 | 103 | beforeEach(() => { 104 | savedEnv = JSON.parse(JSON.stringify(process.env)) 105 | }) 106 | 107 | afterEach(() => { 108 | process.env = savedEnv 109 | }) 110 | 111 | it('without TERM set', () => { 112 | if (process.env.TERM) { 113 | delete process.env.TERM 114 | } 115 | 116 | expect(resetConfigDefaults().termType).toBe('xterm-256color') 117 | }) 118 | 119 | it('with TERM set', () => { 120 | const expected = 'sometermtype' 121 | process.env.TERM = expected 122 | 123 | expect(resetConfigDefaults().termType).toBe(expected) 124 | }) 125 | }) 126 | 127 | describe('cwd', () => { 128 | const savedPlatform = process.platform 129 | let savedEnv 130 | 131 | beforeEach(() => { 132 | savedEnv = JSON.parse(JSON.stringify(process.env)) 133 | }) 134 | 135 | afterEach(() => { 136 | process.env = savedEnv 137 | Object.defineProperty(process, 'platform', { 138 | value: savedPlatform, 139 | }) 140 | }) 141 | 142 | it('on win32', () => { 143 | Object.defineProperty(process, 'platform', { 144 | value: 'win32', 145 | }) 146 | const expected = 'C:\\some\\dir' 147 | process.env.USERPROFILE = expected 148 | 149 | expect(resetConfigDefaults().cwd).toBe(expected) 150 | }) 151 | 152 | it('on linux', () => { 153 | Object.defineProperty(process, 'platform', { 154 | value: 'linux', 155 | }) 156 | const expected = '/some/dir' 157 | process.env.HOME = expected 158 | 159 | expect(resetConfigDefaults().cwd).toBe(expected) 160 | }) 161 | }) 162 | 163 | describe('env', () => { 164 | it('return \'\'', () => { 165 | expect(configDefaults.env).toBe('') 166 | }) 167 | }) 168 | 169 | describe('setEnv', () => { 170 | it('return {}', () => { 171 | expect(configDefaults.setEnv).toBe('{}') 172 | }) 173 | }) 174 | 175 | describe('deleteEnv', () => { 176 | it('return []', () => { 177 | expect(configDefaults.deleteEnv).toBe('["NODE_ENV"]') 178 | }) 179 | }) 180 | 181 | describe('encoding', () => { 182 | it('return \'\'', () => { 183 | expect(configDefaults.encoding).toBe('') 184 | }) 185 | }) 186 | 187 | describe('fontSize', () => { 188 | it('return 14', () => { 189 | expect(configDefaults.fontSize).toBe(14) 190 | }) 191 | }) 192 | 193 | describe('Call to minimumFontSize', () => { 194 | it('return 8', () => { 195 | expect(configDefaults.minimumFontSize).toBe(8) 196 | }) 197 | }) 198 | 199 | describe('Call to maximumFontSize', () => { 200 | it('return 100', () => { 201 | expect(configDefaults.maximumFontSize).toBe(100) 202 | }) 203 | }) 204 | 205 | describe('useEditorFont', () => { 206 | it('return true', () => { 207 | expect(configDefaults.useEditorFont).toBe(true) 208 | }) 209 | }) 210 | 211 | describe('fontFamily', () => { 212 | it('uses editor\'s font', () => { 213 | atom.config.set('editor.fontFamily', 'Cascadia Code PL') 214 | 215 | expect(resetConfigDefaults().fontFamily).toBe('Cascadia Code PL') 216 | }) 217 | 218 | it('uses \'monospace\' when the editor font is not set', () => { 219 | atom.config.set('editor.fontFamily', '') 220 | 221 | expect(resetConfigDefaults().fontFamily).toBe('monospace') 222 | }) 223 | }) 224 | 225 | describe('theme', () => { 226 | it('return \'Custom\'', () => { 227 | expect(configDefaults.theme).toBe('Custom') 228 | }) 229 | }) 230 | 231 | describe('colorForeground', () => { 232 | it('return \'#ffffff\'', () => { 233 | expect(configDefaults.colorForeground).toBe('#ffffff') 234 | }) 235 | }) 236 | 237 | describe('colorBackground', () => { 238 | it('return \'#000000\'', () => { 239 | expect(configDefaults.colorBackground).toBe('#000000') 240 | }) 241 | }) 242 | 243 | describe('colorCursor', () => { 244 | it('return \'#ffffff\'', () => { 245 | expect(configDefaults.colorCursor).toBe('#ffffff') 246 | }) 247 | }) 248 | 249 | describe('colorCursorAccent', () => { 250 | it('return \'#000000\'', () => { 251 | expect(configDefaults.colorCursorAccent).toBe('#000000') 252 | }) 253 | }) 254 | 255 | describe('colorSelectionBackground', () => { 256 | it('return \'#4d4d4d\'', () => { 257 | expect(configDefaults.colorSelectionBackground).toBe('#4d4d4d') 258 | }) 259 | }) 260 | 261 | describe('colorBlack', () => { 262 | it('return \'#2e3436\'', () => { 263 | expect(configDefaults.colorBlack).toBe('#2e3436') 264 | }) 265 | }) 266 | 267 | describe('colorRed', () => { 268 | it('return \'#cc0000\'', () => { 269 | expect(configDefaults.colorRed).toBe('#cc0000') 270 | }) 271 | }) 272 | 273 | describe('colorGreen', () => { 274 | it('return \'#4e9a06\'', () => { 275 | expect(configDefaults.colorGreen).toBe('#4e9a06') 276 | }) 277 | }) 278 | 279 | describe('colorYellow', () => { 280 | it('return \'#c4a000\'', () => { 281 | expect(configDefaults.colorYellow).toBe('#c4a000') 282 | }) 283 | }) 284 | 285 | describe('colorBlue', () => { 286 | it('return \'#3465a4\'', () => { 287 | expect(configDefaults.colorBlue).toBe('#3465a4') 288 | }) 289 | }) 290 | 291 | describe('colorMagenta', () => { 292 | it('return \'#75507b\'', () => { 293 | expect(configDefaults.colorMagenta).toBe('#75507b') 294 | }) 295 | }) 296 | 297 | describe('colorCyan', () => { 298 | it('return \'#06989a\'', () => { 299 | expect(configDefaults.colorCyan).toBe('#06989a') 300 | }) 301 | }) 302 | 303 | describe('colorWhite', () => { 304 | it('return \'#d3d7cf\'', () => { 305 | expect(configDefaults.colorWhite).toBe('#d3d7cf') 306 | }) 307 | }) 308 | 309 | describe('colorBrightBlack', () => { 310 | it('return \'#555753\'', () => { 311 | expect(configDefaults.colorBrightBlack).toBe('#555753') 312 | }) 313 | }) 314 | 315 | describe('colorBrightRed', () => { 316 | it('return \'#ef2929\'', () => { 317 | expect(configDefaults.colorBrightRed).toBe('#ef2929') 318 | }) 319 | }) 320 | 321 | describe('colorBrightGreen', () => { 322 | it('return \'#8ae234\'', () => { 323 | expect(configDefaults.colorBrightGreen).toBe('#8ae234') 324 | }) 325 | }) 326 | 327 | describe('colorBrightYellow', () => { 328 | it('return \'#fce94f\'', () => { 329 | expect(configDefaults.colorBrightYellow).toBe('#fce94f') 330 | }) 331 | }) 332 | 333 | describe('colorBrightBlue', () => { 334 | it('return \'#729fcf\'', () => { 335 | expect(configDefaults.colorBrightBlue).toBe('#729fcf') 336 | }) 337 | }) 338 | 339 | describe('colorBrightMagenta', () => { 340 | it('return \'#ad7fa8\'', () => { 341 | expect(configDefaults.colorBrightMagenta).toBe('#ad7fa8') 342 | }) 343 | }) 344 | 345 | describe('colorBrightCyan', () => { 346 | it('return \'#34e2e2\'', () => { 347 | expect(configDefaults.colorBrightCyan).toBe('#34e2e2') 348 | }) 349 | }) 350 | 351 | describe('colorBrightWhite', () => { 352 | it('return \'#eeeeec\'', () => { 353 | expect(configDefaults.colorBrightWhite).toBe('#eeeeec') 354 | }) 355 | }) 356 | 357 | describe('allowHiddenToStayActive', () => { 358 | it('return false', () => { 359 | expect(configDefaults.allowHiddenToStayActive).toBe(false) 360 | }) 361 | }) 362 | 363 | describe('runInActive', () => { 364 | it('return false', () => { 365 | expect(configDefaults.runInActive).toBe(false) 366 | }) 367 | }) 368 | 369 | describe('leaveOpenAfterExit', () => { 370 | it('return true', () => { 371 | expect(configDefaults.leaveOpenAfterExit).toBe(true) 372 | }) 373 | }) 374 | 375 | describe('allowRelaunchingTerminalsOnStartup', () => { 376 | it('return true', () => { 377 | expect(configDefaults.allowRelaunchingTerminalsOnStartup).toBe(true) 378 | }) 379 | }) 380 | 381 | describe('relaunchTerminalOnStartup', () => { 382 | it('return true', () => { 383 | expect(configDefaults.relaunchTerminalOnStartup).toBe(true) 384 | }) 385 | }) 386 | 387 | describe('xtermOptions', () => { 388 | it('return {}', () => { 389 | expect(configDefaults.xtermOptions).toBe('{}') 390 | }) 391 | }) 392 | 393 | describe('userDataPath', () => { 394 | const savedPlatform = process.platform 395 | let savedEnv 396 | 397 | beforeEach(() => { 398 | savedEnv = JSON.parse(JSON.stringify(process.env)) 399 | }) 400 | 401 | afterEach(() => { 402 | process.env = savedEnv 403 | Object.defineProperty(process, 'platform', { 404 | value: savedPlatform, 405 | }) 406 | }) 407 | 408 | it('on win32 without APPDATA set', () => { 409 | Object.defineProperty(process, 'platform', { 410 | value: 'win32', 411 | }) 412 | if (process.env.APPDATA) { 413 | delete process.env.APPDATA 414 | } 415 | const expected = path.join(os.homedir(), 'AppData', 'Roaming', 'x-terminal-reloaded') 416 | 417 | expect(resetConfigDefaults().userDataPath).toBe(expected) 418 | }) 419 | 420 | it('on win32 with APPDATA set', () => { 421 | Object.defineProperty(process, 'platform', { 422 | value: 'win32', 423 | }) 424 | process.env.APPDATA = path.join('/some', 'dir') 425 | const expected = path.join(process.env.APPDATA, 'x-terminal-reloaded') 426 | 427 | expect(resetConfigDefaults().userDataPath).toBe(expected) 428 | }) 429 | 430 | it('on darwin', () => { 431 | Object.defineProperty(process, 'platform', { 432 | value: 'darwin', 433 | }) 434 | const expected = path.join(os.homedir(), 'Library', 'Application Support', 'x-terminal-reloaded') 435 | 436 | expect(resetConfigDefaults().userDataPath).toBe(expected) 437 | }) 438 | 439 | it('on linux without XDG_CONFIG_HOME set', () => { 440 | Object.defineProperty(process, 'platform', { 441 | value: 'linux', 442 | }) 443 | if (process.env.XDG_CONFIG_HOME) { 444 | delete process.env.XDG_CONFIG_HOME 445 | } 446 | const expected = path.join(os.homedir(), '.config', 'x-terminal-reloaded') 447 | 448 | expect(resetConfigDefaults().userDataPath).toBe(expected) 449 | }) 450 | 451 | it('on linux with XDG_CONFIG_HOME set', () => { 452 | Object.defineProperty(process, 'platform', { 453 | value: 'linux', 454 | }) 455 | process.env.XDG_CONFIG_HOME = path.join('/some', 'dir') 456 | const expected = path.join(process.env.XDG_CONFIG_HOME, 'x-terminal-reloaded') 457 | 458 | expect(resetConfigDefaults().userDataPath).toBe(expected) 459 | }) 460 | }) 461 | 462 | describe('title', () => { 463 | it('return \'\'', () => { 464 | expect(configDefaults.title).toBe('') 465 | }) 466 | }) 467 | 468 | describe('promptToStartup', () => { 469 | it('return false', () => { 470 | expect(configDefaults.promptToStartup).toBe(false) 471 | }) 472 | }) 473 | 474 | describe('apiOpenPosition', () => { 475 | it('return \'Center\'', () => { 476 | expect(configDefaults.apiOpenPosition).toBe('Center') 477 | }) 478 | }) 479 | 480 | describe('setInitialCommand()', () => { 481 | const savedPlatform = process.platform 482 | 483 | beforeEach(() => { 484 | Object.defineProperty(process, 'platform', { 485 | value: 'win32', 486 | }) 487 | atom.config.set('x-terminal-reloaded.spawnPtySettings.command', configDefaults.command) 488 | }) 489 | 490 | afterEach(() => { 491 | Object.defineProperty(process, 'platform', { 492 | value: savedPlatform, 493 | }) 494 | }) 495 | 496 | it('should set x-terminal-reloaded.command to pwsh', async () => { 497 | const shell = 'path/to/pwsh.exe' 498 | await setInitialCommand(async (file) => { 499 | if (file === 'pwsh.exe') { 500 | return shell 501 | } 502 | throw new Error('ENOENT') 503 | }) 504 | 505 | expect(atom.config.get('x-terminal-reloaded.spawnPtySettings.command')).toBe(shell) 506 | }) 507 | 508 | it('should set x-terminal-reloaded.command to powershell', async () => { 509 | const shell = 'path/to/powershell.exe' 510 | await setInitialCommand(async (file) => { 511 | if (file === 'powershell.exe') { 512 | return shell 513 | } 514 | throw new Error('ENOENT') 515 | }) 516 | 517 | expect(atom.config.get('x-terminal-reloaded.spawnPtySettings.command')).toBe(shell) 518 | }) 519 | 520 | it('should set x-terminal-reloaded.command to powershell', async () => { 521 | const shell = configDefaults.command 522 | await setInitialCommand(async () => { 523 | throw new Error('ENOENT') 524 | }) 525 | 526 | expect(atom.config.get('x-terminal-reloaded.spawnPtySettings.command')).toBe(shell) 527 | }) 528 | }) 529 | }) 530 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ██╗ ██╗ ████████╗███████╗██████╗ ███╗ ███╗██╗███╗ ██╗ █████╗ ██╗ 2 | ╚██╗██╔╝ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║██║████╗ ██║██╔══██╗██║ 3 | ╚███╔╝█████╗██║ █████╗ ██████╔╝██╔████╔██║██║██╔██╗ ██║███████║██║ 4 | ██╔██╗╚════╝██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║██║╚██╗██║██╔══██║██║ 5 | ██╔╝ ██╗ ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║██║ ╚████║██║ ██║███████╗ 6 | ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ 7 | ██████╗ ███████╗██╗ ██████╗ █████╗ ██████╗ ███████╗██████╗ 8 | ██╔══██╗██╔════╝██║ ██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗ 9 | ██████╔╝█████╗ ██║ ██║ ██║███████║██║ ██║█████╗ ██║ ██║ 10 | ██╔══██╗██╔══╝ ██║ ██║ ██║██╔══██║██║ ██║██╔══╝ ██║ ██║ 11 | ██║ ██║███████╗███████╗╚██████╔╝██║ ██║██████╔╝███████╗██████╔╝ 12 | ╚═╝ ╚═╝╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═════╝ 13 | 14 | 15 | 16 |
17 |

18 | 19 | CI status 20 | 21 | 22 | Latest Version 23 | 24 | 25 | Pulsar package repository link 26 | 27 | 28 | GitHub Stars 29 | 30 | 31 | GitHub Forks 32 | 33 | 34 | Pulsar download page direct to rolling download 35 | 36 | 37 | Pulsar download page direct to regular download 38 | 39 |

40 |

41 | An xterm based for providing terminals inside your workspace! ❤️ 42 |

43 |
A fork of 44 | atom-xterm 45 | and 46 | x-terminal 47 |
48 |
49 | 50 | ![X-Terminal demo](https://cdn.statically.io/gh/Spiker985/x-terminal-reloaded/master/resources/x-terminal-demo.gif) 51 | 52 | ## Pulsar Built-in Terminal 53 | 54 | Eventually, I'd like to transition this over to the [Pulsar repo](https://github.com/pulsar-edit/pulsar), however as of right now I'll maintain it separately because there's a lot on our plates over there. 55 | 56 | # Installation 57 | 58 | There are 3 ways to install this package: 59 | 1. Navigate to https://web.pulsar-edit.dev/packages/x-terminal-reloaded and click install. 60 | 2. Open Pulsar, open Settings (Ctrl + Shift + Comma), click on Install, search for `x-terminal-reloaded` 61 | 3. Install via CLI. `pulsar --package install x-terminal-reloaded` or `pulsar -p install x-terminal-reloaded` 62 | 63 | #### Note: 64 | Due to native module requirements, you may need the C++ toolchain ([Windows](https://visualstudio.microsoft.com/vs/features/cplusplus/), [mac](https://www.cs.rhodes.edu/~kirlinp/courses/cs2/s17/installing-clion/xcode.html), [linux](https://www.google.com/search?q=%3Cdistro%3E+c%252B%252B+toolchain)) and [Python 3.10](https://www.python.org/downloads/release/python-3100/). 65 | 66 | Node-pty, which is used for terminal emulation, is a native module and may need to be built for your specific system. Provided you have the tooling listed above, Pulsar will do this for you automatically. 67 | 68 | ## Opening Terminals 69 | 70 | To open terminals, you can open them through the menu or through the available key bindings. 71 | 72 | ![X-Terminal menu](https://cdn.statically.io/gh/Spiker985/x-terminal-reloaded/master/resources/x-terminal-packages-menu.png) 73 | 74 | See [the available key bindings](https://github.com/Spiker985/x-terminal-reloaded/blob/master/keymaps/x-terminal.json) for the x-terminal package. 75 | 76 | There's also menu items available for opening terminals via right clicking on a 77 | text editor or on a terminal. 78 | 79 | Finally, terminal tabs are automatically reopened at the spot you placed them 80 | when you last exited Pulsar. 81 | 82 | ## Active Terminal 83 | 84 | The active terminal is the terminal that will be used when sending commands to 85 | the terminal with commands like `x-terminal:insert-selected-text` and 86 | `x-terminal:run-selected-text` 87 | 88 | The active terminal will, by default, have an asterisk (`*`) in front of the title. 89 | By default when a terminal is hidden it becomes inactive and the last used 90 | visible terminal will become active. If there are no visible terminals none are 91 | active. 92 | 93 | The `Allow Hidden Terminal To Stay Active` setting will change the 94 | default behavior and keep a terminal that is hidden active until another 95 | terminal is focused. 96 | 97 | ## Organizing Terminals 98 | 99 | To quickly organize your terminal tabs, simply use the main menu. You can also 100 | find menu items by right-clicking on a terminal to organize your terminals. 101 | 102 | And of course, there's the old fashion way of just moving the tabs where you 103 | want them. Feel free to place your terminal tabs anywhere in your workspace to 104 | include any of the docks. 105 | 106 | ![X-Terminal moving terminals demo](https://cdn.statically.io/gh/Spiker985/x-terminal-reloaded/master/resources/x-terminal-moving-terminals-demo.gif) 107 | 108 | ## Profiles 109 | 110 | The x-terminal package supports saving and loading profiles. What this allows 111 | you to do is save commonly used commands and settings for later use. 112 | 113 | ![X-Terminal profiles demo](https://cdn.statically.io/gh/Spiker985/x-terminal-reloaded/master/resources/x-terminal-profiles-demo.gif) 114 | 115 | ## Notifications 116 | 117 | The x-terminal package provides notifications about terminal process exit 118 | successes and failures. Notifications will appear in Pulsar's own notification 119 | manager as well as on the terminal tab triggering the notification. 120 | 121 | Success 122 | 123 | ![X-Terminal exit success](https://cdn.statically.io/gh/Spiker985/x-terminal-reloaded/master/resources/x-terminal-exit-success.png) 124 | 125 | Failure 126 | 127 | ![X-Terminal exit failure](https://cdn.statically.io/gh/Spiker985/x-terminal-reloaded/master/resources/x-terminal-exit-failure.png) 128 | 129 | There are also activity notifications for terminal tabs not in focus. 130 | 131 | ![X-Terminal activity notification](https://cdn.statically.io/gh/Spiker985/x-terminal-reloaded/master/resources/x-terminal-activity-notification.gif) 132 | 133 | ## Services 134 | 135 | For package writers, the `x-terminal` package supports three services, `terminal`, `atom-xterm`, and `platformioIDETerminal`, which 136 | can be used to easily open terminals. These methods are provided using Pulsar's [services](https://pulsar-edit.dev/docs/atom-archive/behind-atom/#interacting-with-other-packages-via-services) 137 | API. 138 | 139 | To use a service, add a consumer method to consume the service, or 140 | rather a JavaScript object that provides methods to open terminals and run commands. 141 | 142 | ### 'terminal' service v1.0.0 143 | 144 | The `terminal` service provides an object with `updateProcessEnv`, `run`, `getTerminalViews`, and `open` methods. 145 | 146 | As an example on how to use the provided `run()` method, your 147 | `package.json` should have the following. 148 | 149 | ```json 150 | { 151 | "consumedServices": { 152 | "terminal": { 153 | "versions": { 154 | "^1.0.0": "consumeTerminalService" 155 | } 156 | } 157 | } 158 | } 159 | ``` 160 | 161 | Your package's main module should then define a `consumeTerminalService` 162 | method, for example. 163 | 164 | ```js 165 | import { Disposable } from 'atom' 166 | 167 | export default { 168 | terminalService: null, 169 | 170 | consumeTerminalService (terminalService) { 171 | this.terminalService = terminalService 172 | return new Disposable(() => { 173 | this.terminalService = null 174 | }) 175 | }, 176 | 177 | // . . . 178 | } 179 | ``` 180 | 181 | Once the service is consumed, use the `run()` method that is provided 182 | by the service, for example. 183 | 184 | ```js 185 | // Launch `somecommand --foo --bar --baz` in a terminal. 186 | this.terminalService.run([ 187 | 'somecommand --foo --bar --baz' 188 | ]) 189 | ``` 190 | 191 | ### 'atom-xterm' service v2.0.0 192 | 193 | The `atom-xterm` service provides the 194 | [openTerminal()](https://github.com/Spiker985/x-terminal-reloaded/blob/465d2909ea1e8457151bf5ddf9f57fc8404e10fe/src/x-terminal.js#L468) method. The `openTerminal()` method behaves just like Pulsar's 195 | [open()](https://github.com/pulsar-edit/pulsar/blob/7d933c561f6f43def1dbb0408df9575e265f6e7d/src/workspace.js#L1077) 196 | method except that the first argument must be a JSON object describing the 197 | terminal profile that should be opened. Docs about this JSON object can be 198 | found [here](https://github.com/Spiker985/x-terminal-reloaded/blob/465d2909ea1e8457151bf5ddf9f57fc8404e10fe/src/config.js#L26). 199 | 200 | As an example on how to use the provided `openTerminal()` method, your 201 | `package.json` should have the following. 202 | 203 | ```json 204 | { 205 | "consumedServices": { 206 | "atom-xterm": { 207 | "versions": { 208 | "^2.0.0": "consumeAtomXtermService" 209 | } 210 | } 211 | } 212 | } 213 | ``` 214 | 215 | Your package's main module should then define a `consumeAtomXtermService` 216 | method, for example. 217 | 218 | ```js 219 | import { Disposable } from 'atom' 220 | 221 | export default { 222 | atomXtermService: null, 223 | 224 | consumeAtomXtermService (atomXtermService) { 225 | this.atomXtermService = atomXtermService 226 | return new Disposable(() => { 227 | this.atomXtermService = null 228 | }) 229 | }, 230 | 231 | // . . . 232 | } 233 | ``` 234 | 235 | Once the service is consumed, use the `openTerminal()` method that is provided 236 | by the service, for example. 237 | 238 | ```js 239 | // Launch `somecommand --foo --bar --baz` in a terminal. 240 | this.atomXtermService.openTerminal({ 241 | command: 'somecommand', 242 | args: [ 243 | '--foo', 244 | '--bar', 245 | '--baz' 246 | ] 247 | }) 248 | ``` 249 | 250 | ### 'platformioIDETerminal' service v1.1.0 251 | 252 | The `platformioIDETerminal` service provides an [object](https://github.com/Spiker985/x-terminal-reloaded/blob/465d2909ea1e8457151bf5ddf9f57fc8404e10fe/src/x-terminal.js#L579) with `updateProcessEnv`, `run`, `getTerminalViews`, and `open` methods. 253 | 254 | As an example on how to use the provided `run()` method, your 255 | `package.json` should have the following. 256 | 257 | ```json 258 | { 259 | "consumedServices": { 260 | "platformioIDETerminal": { 261 | "versions": { 262 | "^1.1.0": "consumePlatformioIDETerminalService" 263 | } 264 | } 265 | } 266 | } 267 | ``` 268 | 269 | Your package's main module should then define a `consumePlatformioIDETerminalService` 270 | method, for example. 271 | 272 | ```js 273 | import { Disposable } from 'atom' 274 | 275 | export default { 276 | platformioIDETerminalService: null, 277 | 278 | consumePlatformioIDETerminalService (platformioIDETerminalService) { 279 | this.platformioIDETerminalService = platformioIDETerminalService 280 | return new Disposable(() => { 281 | this.platformioIDETerminalService = null 282 | }) 283 | }, 284 | 285 | // . . . 286 | } 287 | ``` 288 | 289 | Once the service is consumed, use the `run()` method that is provided 290 | by the service, for example. 291 | 292 | ```js 293 | // Launch `somecommand --foo --bar --baz` in a terminal. 294 | this.platformioIDETerminalService.run([ 295 | 'somecommand --foo --bar --baz' 296 | ]) 297 | ``` 298 | 299 | # Development 300 | 301 | Want to help develop x-terminal? Here's how to quickly get setup. 302 | 303 | First clone the [x-terminal-reloaded repo](https://github.com/Spiker985/x-terminal-reloaded.git). This step does _not_ need to be done with `pulsar --package`, if you already have an existing clone. 304 | 305 | ```sh 306 | pulsar --package develop x-terminal 307 | ``` 308 | 309 | This should clone the x-terminal-reloaded package into the `$HOME/github/x-terminal` 310 | directory. Go into this directory (or your pre-existing directory) and install its dependencies. 311 | 312 | ```sh 313 | cd $HOME/github/x-terminal 314 | npm install 315 | ``` 316 | 317 | ~You shouldn't need to rebuild any [node-pty](https://github.com/Tyriar/node-pty) 318 | since they are pre-compiled, however in the event they aren't available, 319 | you can rebuild them with:~ 320 | 321 | You will probably need to rebuild `node-pty` as Pulsar's current electron version is no longer LTS. Please do so with the following: 322 | 323 | ```sh 324 | pulsar --package rebuild 325 | ``` 326 | 327 | Finally, open this directory in Pulsar's dev mode and hack away. 328 | 329 | ```sh 330 | pulsar --dev 331 | ``` 332 | 333 | There's a test suite available for automated testing of the x-terminal package. 334 | Simply go to `View > Developer > Run Package Specs` in Pulsar's main menu or 335 | use the hotkey. You can run the full test suite (which includes running lint 336 | tools) via command-line by running `npm run test` inside the x-terminal 337 | directory. 338 | 339 | Various lint tools are being used to keep the code "beautified". To run only 340 | the lint tools, simply run `npm run lint`. 341 | 342 | ## Pull Requests 343 | 344 | Whenever you're ready to submit a pull request, be sure to submit it 345 | against a fork of the main [x-terminal repo](https://github.com/Spiker985/x-terminal-reloaded) 346 | master branch that you'll own. Fork the repo using Github and make note of the 347 | new `git` URL. Set this new git URL as the URL for the `origin` remote in your 348 | already cloned git repo is follows. You can also validate it with `git remote --verbose` 349 | 350 | ```sh 351 | git remote set-url upstream "https://github.com/Spiker985/x-terminal-reloaded.git" 352 | git remote set-url origin ${NEW_GIT_URL} 353 | ``` 354 | 355 | Ensure your new changes passes the test suite by running `npm run test`. 356 | Afterwards, push your changes to your repo and then use Github to submit a new 357 | pull request. 358 | 359 | ## [xterm.js](https://github.com/xtermjs/xterm.js) 360 | 361 | The terminals that users interact with in this package is made possible with 362 | major help from the [xterm.js](https://github.com/xtermjs/xterm.js) library. As 363 | such, often times it's necessary to make changes to xterm.js in order to fix 364 | some bug or implement new features. 365 | 366 | If you want to work on xterm.js for the benefit of a bug fix or feature to be 367 | supported in x-terminal, here's how you can quickly get setup. 368 | 369 | First make a fork of [xterm.js](https://github.com/xtermjs/xterm.js). Next, 370 | clone your newly created fork as follows. 371 | 372 | ```sh 373 | git clone ${YOUR_XTERMJS_FORK} ${HOME}/github/xterm.js 374 | ``` 375 | 376 | Go into your newly cloned repo for xterm.js. 377 | 378 | ```sh 379 | cd ${HOME}/github/xterm.js 380 | ``` 381 | 382 | Install all needed dependencies. 383 | 384 | ```sh 385 | npm install 386 | ``` 387 | 388 | Build xterm.js. 389 | 390 | ```sh 391 | npm run build 392 | ``` 393 | 394 | Ensure the test suite passes. 395 | 396 | ```sh 397 | npm run test 398 | npm run lint 399 | ``` 400 | 401 | Add a global link for xterm.js to your system. 402 | 403 | ```sh 404 | npm link 405 | ``` 406 | 407 | Inside your x-terminal directory, link against the global `xterm` link. 408 | 409 | ```sh 410 | cd ${HOME}/github/x-terminal 411 | npm link xterm 412 | ``` 413 | 414 | Finally, perform a rebuild using pulsar inside the x-terminal directory. 415 | 416 | ```sh 417 | pulsar --package rebuild 418 | ``` 419 | 420 | You're all set for developing xterm.js. Hack away in your xterm.js directory, 421 | run `npm run build`, then reload your Pulsar window to see the changes to your 422 | terminals. 423 | 424 | # Credits and Legal 425 | 426 | Click for copyright and license info about this package. 427 | 428 | [![LICENSE and © INFO](https://img.shields.io/badge/©%20&%20LICENSE-MIT-blue.svg?longCache=true&style=flat-square)](LICENSE) 429 | 430 | # Feedback 431 | 432 | Need to submit a bug report? Have a new feature you want to see implemented in 433 | *x-terminal*? Please feel free to submit them through the appropriate 434 | [issue template](https://github.com/Spiker985/x-terminal-reloaded/issues/new/choose). 435 | 436 | For bug reports, please provide images or demos showing your issues if you can. 437 | -------------------------------------------------------------------------------- /src/profile-menu-element.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { CompositeDisposable, TextEditor } from 'atom' 23 | import { marked } from 'marked' 24 | 25 | import { XTerminalProfilesSingleton } from './profiles' 26 | import { XTerminalDeleteProfileModel } from './delete-profile-model' 27 | import { XTerminalSaveProfileModel } from './save-profile-model' 28 | import { createHorizontalLine } from './utils' 29 | import { CONFIG_DATA } from './config.js' 30 | 31 | class XTerminalProfileMenuElementImpl extends HTMLElement { 32 | async initialize (model) { 33 | this.model = model 34 | this.model.setElement(this) 35 | this.profilesSingleton = XTerminalProfilesSingleton.instance 36 | const topDiv = document.createElement('div') 37 | topDiv.classList.add('x-terminal-reloaded-profile-menu-element-top-div') 38 | this.appendChild(topDiv) 39 | const leftDiv = document.createElement('div') 40 | leftDiv.classList.add('x-terminal-reloaded-profile-menu-element-left-div') 41 | this.appendChild(leftDiv) 42 | this.mainDiv = document.createElement('div') 43 | this.mainDiv.classList.add('x-terminal-reloaded-profile-menu-element-main-div') 44 | this.appendChild(this.mainDiv) 45 | const rightDiv = document.createElement('div') 46 | rightDiv.classList.add('x-terminal-reloaded-profile-menu-element-right-div') 47 | this.appendChild(rightDiv) 48 | const bottomDiv = document.createElement('div') 49 | bottomDiv.classList.add('x-terminal-reloaded-profile-menu-element-bottom-div') 50 | this.appendChild(bottomDiv) 51 | this.disposables = new CompositeDisposable() 52 | let resolveInit 53 | this.initializedPromise = new Promise((resolve, reject) => { 54 | resolveInit = resolve 55 | }) 56 | 57 | const profilesDiv = await this.createProfilesDropDown() 58 | const modelProfile = this.getModelProfile() 59 | const baseProfile = this.profilesSingleton.getBaseProfile() 60 | // Profiles 61 | this.mainDiv.appendChild(profilesDiv) 62 | 63 | // Buttons div 64 | this.mainDiv.appendChild(this.createProfileMenuButtons()) 65 | 66 | // Horizontal line. 67 | this.mainDiv.appendChild(createHorizontalLine()) 68 | 69 | this.createFromConfig(baseProfile, modelProfile) 70 | 71 | this.deleteProfileModel = new XTerminalDeleteProfileModel(this) 72 | this.saveProfileModel = new XTerminalSaveProfileModel(this) 73 | 74 | this.disposables.add(this.profilesSingleton.onDidReloadProfiles(async (profiles) => { 75 | const select = await this.createProfilesDropDownSelectItem() 76 | const menuItemContainer = this.mainDiv.querySelector('#profiles-selection') 77 | while (menuItemContainer.firstChild) { 78 | menuItemContainer.removeChild(menuItemContainer.firstChild) 79 | } 80 | menuItemContainer.appendChild(select) 81 | })) 82 | resolveInit() 83 | } 84 | 85 | createFromConfig (baseProfile, modelProfile) { 86 | for (const data of CONFIG_DATA) { 87 | if (!data.profileKey) continue 88 | const title = data.title || data.profileKey.charAt(0).toUpperCase() + data.profileKey.substring(1).replace(/[A-Z]/g, ' $&') 89 | const description = data.description || '' 90 | if (data.enum) { 91 | this.mainDiv.appendChild(this.createSelect( 92 | `${data.profileKey.toLowerCase()}-select`, 93 | title, 94 | description, 95 | baseProfile[data.profileKey], 96 | modelProfile[data.profileKey], 97 | data.enum, 98 | )) 99 | } else if (data.type === 'color') { 100 | this.mainDiv.appendChild(this.createColor( 101 | `${data.profileKey.toLowerCase()}-color`, 102 | title, 103 | description, 104 | baseProfile[data.profileKey], 105 | modelProfile[data.profileKey], 106 | )) 107 | } else if (data.type === 'boolean') { 108 | this.mainDiv.appendChild(this.createCheckbox( 109 | `${data.profileKey.toLowerCase()}-checkbox`, 110 | title, 111 | description, 112 | baseProfile[data.profileKey], 113 | modelProfile[data.profileKey], 114 | )) 115 | } else { 116 | this.mainDiv.appendChild(this.createTextbox( 117 | `${data.profileKey.toLowerCase()}-textbox`, 118 | title, 119 | description, 120 | baseProfile[data.profileKey], 121 | modelProfile[data.profileKey], 122 | )) 123 | } 124 | } 125 | } 126 | 127 | destroy () { 128 | if (this.disposables) { 129 | this.disposables.dispose() 130 | } 131 | } 132 | 133 | getModelProfile () { 134 | return this.model.atomXtermModel.profile 135 | } 136 | 137 | getMenuElements () { 138 | const menuElements = {} 139 | for (const data of CONFIG_DATA) { 140 | if (!data.profileKey) continue 141 | 142 | let type = 'textbox > atom-text-editor' 143 | if (data.enum) { 144 | type = 'select select' 145 | } else if (data.type === 'color') { 146 | type = 'color input' 147 | } else if (data.type === 'boolean') { 148 | type = 'checkbox input' 149 | } 150 | menuElements[data.profileKey] = this.mainDiv.querySelector(`#${data.profileKey.toLowerCase()}-${type}`) 151 | } 152 | return menuElements 153 | } 154 | 155 | getProfileMenuSettings () { 156 | const newProfile = {} 157 | const baseProfile = this.profilesSingleton.getBaseProfile() 158 | const menuElements = this.getMenuElements() 159 | for (const data of CONFIG_DATA) { 160 | if (!data.profileKey) continue 161 | newProfile[data.profileKey] = data.fromMenuSetting(menuElements[data.profileKey], baseProfile[data.profileKey]) 162 | } 163 | return newProfile 164 | } 165 | 166 | applyProfileChanges (profileChanges) { 167 | this.hideProfileMenu() 168 | this.model.getXTerminalModel().applyProfileChanges(profileChanges) 169 | } 170 | 171 | restartTerminal () { 172 | this.hideProfileMenu() 173 | this.model.getXTerminalModelElement().restartPtyProcess() 174 | } 175 | 176 | createMenuItemContainer (id, labelTitle, labelDescription) { 177 | const menuItemContainer = document.createElement('div') 178 | menuItemContainer.classList.add('x-terminal-reloaded-profile-menu-item') 179 | menuItemContainer.setAttribute('id', id) 180 | const menuItemLabel = document.createElement('label') 181 | menuItemLabel.classList.add('x-terminal-reloaded-profile-menu-item-label') 182 | const titleDiv = document.createElement('div') 183 | titleDiv.classList.add('x-terminal-reloaded-profile-menu-item-title') 184 | titleDiv.appendChild(document.createTextNode(labelTitle)) 185 | menuItemLabel.appendChild(titleDiv) 186 | const descriptionDiv = document.createElement('div') 187 | descriptionDiv.classList.add('x-terminal-reloaded-profile-menu-item-description') 188 | descriptionDiv.innerHTML = marked(labelDescription) 189 | menuItemLabel.appendChild(descriptionDiv) 190 | menuItemContainer.appendChild(menuItemLabel) 191 | return menuItemContainer 192 | } 193 | 194 | async createProfilesDropDownSelectItem () { 195 | const profiles = await this.profilesSingleton.getProfiles() 196 | const select = document.createElement('select') 197 | select.setAttribute('id', 'profiles-dropdown') 198 | select.classList.add('x-terminal-reloaded-profile-menu-item-select') 199 | let option = document.createElement('option') 200 | let text = document.createTextNode('') 201 | option.setAttribute('value', text) 202 | option.appendChild(text) 203 | select.appendChild(option) 204 | for (const profile in profiles) { 205 | option = document.createElement('option') 206 | text = document.createTextNode(profile) 207 | option.setAttribute('value', text.textContent) 208 | option.appendChild(text) 209 | select.appendChild(option) 210 | } 211 | select.addEventListener('change', async (event) => { 212 | if (event.target.value) { 213 | const profile = await this.profilesSingleton.getProfile(event.target.value) 214 | this.setNewMenuSettings(profile) 215 | } else { 216 | const profile = this.profilesSingleton.getBaseProfile() 217 | this.setNewMenuSettings(profile, true) 218 | } 219 | }, { passive: true }) 220 | return select 221 | } 222 | 223 | async createProfilesDropDown () { 224 | const menuItemContainer = this.createMenuItemContainer( 225 | 'profiles-selection', 226 | 'Profiles', 227 | 'Available profiles', 228 | ) 229 | const select = await this.createProfilesDropDownSelectItem() 230 | menuItemContainer.appendChild(select) 231 | return menuItemContainer 232 | } 233 | 234 | createProfileMenuButtons () { 235 | const buttonsContainer = document.createElement('div') 236 | buttonsContainer.classList.add('x-terminal-reloaded-profile-menu-buttons-div') 237 | let button = this.createButton() 238 | button.appendChild(document.createTextNode('Load Settings')) 239 | button.classList.add('btn-load') 240 | button.addEventListener('click', (event) => { 241 | this.loadProfile() 242 | }, { passive: true }) 243 | buttonsContainer.appendChild(button) 244 | button = this.createButton() 245 | button.appendChild(document.createTextNode('Save Settings')) 246 | button.classList.add('btn-save') 247 | button.addEventListener('click', (event) => { 248 | this.saveProfile() 249 | }, { passive: true }) 250 | buttonsContainer.appendChild(button) 251 | button = this.createButton() 252 | button.appendChild(document.createTextNode('Delete Settings')) 253 | button.classList.add('btn-delete') 254 | button.addEventListener('click', (event) => { 255 | this.deleteProfile() 256 | }, { passive: true }) 257 | buttonsContainer.appendChild(button) 258 | button = this.createButton() 259 | button.appendChild(document.createTextNode('Restart')) 260 | button.classList.add('btn-restart') 261 | button.addEventListener('click', (event) => { 262 | this.restartTerminal() 263 | }, { passive: true }) 264 | buttonsContainer.appendChild(button) 265 | button = this.createButton() 266 | button.appendChild(document.createTextNode('Hide Menu')) 267 | button.classList.add('btn-hide') 268 | button.addEventListener('click', (event) => { 269 | this.hideProfileMenu() 270 | }, { passive: true }) 271 | buttonsContainer.appendChild(button) 272 | return buttonsContainer 273 | } 274 | 275 | createButton () { 276 | const button = document.createElement('button') 277 | button.classList.add('x-terminal-reloaded-profile-menu-button') 278 | button.classList.add('btn') 279 | button.classList.add('inline-block-tight') 280 | return button 281 | } 282 | 283 | createTextbox (id, labelTitle, labelDescription, defaultValue, initialValue) { 284 | const menuItemContainer = this.createMenuItemContainer( 285 | id, 286 | labelTitle, 287 | labelDescription, 288 | ) 289 | const textbox = new TextEditor({ 290 | mini: true, 291 | placeholderText: defaultValue, 292 | }) 293 | if (initialValue) { 294 | if (initialValue.constructor === Array || initialValue.constructor === Object) { 295 | textbox.setText(JSON.stringify(initialValue)) 296 | } else { 297 | textbox.setText(initialValue.toString()) 298 | } 299 | } 300 | menuItemContainer.appendChild(textbox.getElement()) 301 | return menuItemContainer 302 | } 303 | 304 | toHex (color) { 305 | color = color.replace(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*[\d.]+\s*\)/, 'rgb($1, $2, $3)').trim() 306 | const ctx = document.createElement('canvas').getContext('2d') 307 | ctx.fillStyle = color 308 | return ctx.fillStyle 309 | } 310 | 311 | createColor (id, labelTitle, labelDescription, defaultValue, initialValue) { 312 | const menuItemContainer = document.createElement('div') 313 | menuItemContainer.classList.add('x-terminal-reloaded-profile-menu-item') 314 | menuItemContainer.setAttribute('id', id) 315 | const menuItemLabel = document.createElement('label') 316 | menuItemLabel.classList.add('x-terminal-reloaded-profile-menu-item-label') 317 | menuItemLabel.classList.add('x-terminal-reloaded-profile-menu-item-label-color') 318 | const color = document.createElement('input') 319 | color.setAttribute('type', 'color') 320 | color.classList.add('x-terminal-reloaded-profile-menu-item-color') 321 | color.value = this.toHex(defaultValue) 322 | if (initialValue !== undefined) { 323 | color.value = this.toHex(initialValue) 324 | } 325 | menuItemLabel.appendChild(color) 326 | const titleDiv = document.createElement('div') 327 | titleDiv.classList.add('x-terminal-reloaded-profile-menu-item-title') 328 | titleDiv.appendChild(document.createTextNode(labelTitle)) 329 | menuItemLabel.appendChild(titleDiv) 330 | menuItemContainer.appendChild(menuItemLabel) 331 | const descriptionDiv = document.createElement('div') 332 | descriptionDiv.classList.add('x-terminal-reloaded-profile-menu-item-description') 333 | descriptionDiv.classList.add('x-terminal-reloaded-profile-menu-item-description-color') 334 | descriptionDiv.innerHTML = marked(labelDescription) 335 | menuItemContainer.appendChild(descriptionDiv) 336 | return menuItemContainer 337 | } 338 | 339 | createSelect (id, labelTitle, labelDescription, defaultValue, initialValue, possibleValues) { 340 | const menuItemContainer = this.createMenuItemContainer( 341 | id, 342 | labelTitle, 343 | labelDescription, 344 | ) 345 | const select = document.createElement('select') 346 | select.setAttribute('type', 'select') 347 | select.classList.add('x-terminal-reloaded-profile-menu-item-select') 348 | select.classList.add('settings-view') 349 | for (let optionValue of possibleValues) { 350 | if (typeof optionValue !== 'object') { 351 | optionValue = { 352 | value: optionValue, 353 | description: optionValue, 354 | } 355 | } 356 | const option = document.createElement('option') 357 | option.setAttribute('value', optionValue.value) 358 | option.textContent = optionValue.description 359 | select.appendChild(option) 360 | } 361 | select.value = defaultValue 362 | if (initialValue !== undefined) { 363 | select.value = initialValue 364 | } 365 | menuItemContainer.appendChild(select) 366 | return menuItemContainer 367 | } 368 | 369 | createCheckbox (id, labelTitle, labelDescription, defaultValue, initialValue) { 370 | const menuItemContainer = document.createElement('div') 371 | menuItemContainer.classList.add('x-terminal-reloaded-profile-menu-item') 372 | menuItemContainer.setAttribute('id', id) 373 | const menuItemLabel = document.createElement('label') 374 | menuItemLabel.classList.add('x-terminal-reloaded-profile-menu-item-label') 375 | menuItemLabel.classList.add('x-terminal-reloaded-profile-menu-item-label-checkbox') 376 | const checkbox = document.createElement('input') 377 | checkbox.setAttribute('type', 'checkbox') 378 | checkbox.classList.add('x-terminal-reloaded-profile-menu-item-checkbox') 379 | checkbox.classList.add('input-checkbox') 380 | checkbox.checked = defaultValue 381 | if (initialValue !== undefined) { 382 | checkbox.checked = initialValue 383 | } 384 | menuItemLabel.appendChild(checkbox) 385 | const titleDiv = document.createElement('div') 386 | titleDiv.classList.add('x-terminal-reloaded-profile-menu-item-title') 387 | titleDiv.appendChild(document.createTextNode(labelTitle)) 388 | menuItemLabel.appendChild(titleDiv) 389 | menuItemContainer.appendChild(menuItemLabel) 390 | const descriptionDiv = document.createElement('div') 391 | descriptionDiv.classList.add('x-terminal-reloaded-profile-menu-item-description') 392 | descriptionDiv.classList.add('x-terminal-reloaded-profile-menu-item-description-checkbox') 393 | descriptionDiv.innerHTML = marked(labelDescription) 394 | menuItemContainer.appendChild(descriptionDiv) 395 | return menuItemContainer 396 | } 397 | 398 | isVisible () { 399 | const style = window.getComputedStyle(this, null) 400 | return (style.visibility === 'visible') 401 | } 402 | 403 | hideProfileMenu () { 404 | this.style.visibility = 'hidden' 405 | const e = this.model.getXTerminalModelElement() 406 | e.showTerminal() 407 | e.focusOnTerminal() 408 | } 409 | 410 | showProfileMenu () { 411 | this.model.getXTerminalModelElement().hideTerminal() 412 | this.style.visibility = 'visible' 413 | } 414 | 415 | toggleProfileMenu () { 416 | if (!this.isVisible()) { 417 | this.showProfileMenu() 418 | } else { 419 | this.hideProfileMenu() 420 | } 421 | } 422 | 423 | getNewProfileAndChanges () { 424 | const newProfile = this.getProfileMenuSettings() 425 | const profileChanges = this.profilesSingleton.diffProfiles( 426 | this.model.getXTerminalModel().getProfile(), 427 | newProfile, 428 | ) 429 | return { 430 | newProfile, 431 | profileChanges, 432 | } 433 | } 434 | 435 | loadProfile () { 436 | const newProfileAndChanges = this.getNewProfileAndChanges() 437 | this.applyProfileChanges(newProfileAndChanges.profileChanges) 438 | } 439 | 440 | saveProfile () { 441 | // Get the current profile settings before entering the promise. 442 | const newProfileAndChanges = this.getNewProfileAndChanges() 443 | this.promptForNewProfileName( 444 | newProfileAndChanges.newProfile, 445 | newProfileAndChanges.profileChanges, 446 | ) 447 | } 448 | 449 | deleteProfile () { 450 | const e = this.mainDiv.querySelector('#profiles-dropdown') 451 | const profileName = e.options[e.selectedIndex].text 452 | if (!profileName) { 453 | atom.notifications.addWarning('Profile must be selected in order to delete it.') 454 | return 455 | } 456 | this.promptDelete(profileName) 457 | } 458 | 459 | async promptDelete (newProfile) { 460 | this.deleteProfileModel.promptDelete(newProfile) 461 | } 462 | 463 | async promptForNewProfileName (newProfile, profileChanges) { 464 | this.saveProfileModel.promptForNewProfileName(newProfile, profileChanges) 465 | } 466 | 467 | setNewMenuSettings (profile, clear = false) { 468 | for (const data of CONFIG_DATA) { 469 | if (!data.profileKey) continue 470 | 471 | if (data.enum) { 472 | const selector = `#${data.profileKey.toLowerCase()}-select select` 473 | const input = this.querySelector(selector) 474 | input.value = data.toMenuSetting(profile[data.profileKey]) 475 | } else if (data.type === 'color') { 476 | const selector = `#${data.profileKey.toLowerCase()}-color input` 477 | const input = this.querySelector(selector) 478 | input.value = data.toMenuSetting(profile[data.profileKey]) 479 | } else if (data.type === 'boolean') { 480 | const selector = `#${data.profileKey.toLowerCase()}-checkbox input` 481 | const checkbox = this.querySelector(selector) 482 | checkbox.checked = data.toMenuSetting(profile[data.profileKey]) 483 | } else { 484 | const selector = `#${data.profileKey.toLowerCase()}-textbox > atom-text-editor` 485 | const model = this.querySelector(selector).getModel() 486 | if (!clear) { 487 | model.setText(data.toMenuSetting(profile[data.profileKey])) 488 | } else { 489 | model.setText('') 490 | } 491 | } 492 | } 493 | } 494 | } 495 | 496 | customElements.define('x-terminal-reloaded-profile', XTerminalProfileMenuElementImpl) 497 | 498 | export { 499 | XTerminalProfileMenuElementImpl, 500 | } 501 | -------------------------------------------------------------------------------- /spec/model-spec.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /* 3 | * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * Copyright 2017-2018 Andres Mejia . All Rights Reserved. 5 | * Copyright (c) 2020 UziTech All Rights Reserved. 6 | * Copyright (c) 2020 bus-stop All Rights Reserved. 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 9 | * software and associated documentation files (the "Software"), to deal in the Software 10 | * without restriction, including without limitation the rights to use, copy, modify, 11 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 16 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 17 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 18 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 19 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | */ 21 | 22 | import { configDefaults } from '../src/config' 23 | import { XTerminalModel, isXTerminalModel, currentItemIsXTerminalModel } from '../src/model' 24 | import { XTerminalProfilesSingleton } from '../src/profiles' 25 | 26 | import fs from 'fs-extra' 27 | import path from 'path' 28 | 29 | import temp from 'temp' 30 | 31 | temp.track() 32 | 33 | async function createNewModel (params, options = {}) { 34 | let uri = 'x-terminal-reloaded://somesessionid/' 35 | if (params) { 36 | const searchParams = new URLSearchParams(params) 37 | const url = new URL('x-terminal-reloaded://?' + searchParams.toString()) 38 | uri = url.href 39 | } 40 | const model = new XTerminalModel({ 41 | uri, 42 | terminals_set: new Set(), 43 | ...options, 44 | }) 45 | await model.initializedPromise 46 | 47 | return model 48 | } 49 | 50 | describe('XTerminalModel', () => { 51 | let model, pane, element, tmpdir 52 | 53 | beforeEach(async () => { 54 | const uri = 'x-terminal-reloaded://somesessionid/' 55 | spyOn(XTerminalProfilesSingleton.instance, 'generateNewUri').and.returnValue(uri) 56 | model = await createNewModel() 57 | pane = jasmine.createSpyObj('pane', 58 | ['destroyItem', 'getActiveItem', 'activateItem']) 59 | element = jasmine.createSpyObj('element', 60 | ['destroy', 'refitTerminal', 'focusOnTerminal', 'clickOnCurrentAnchor', 'getCurrentAnchorHref', 'restartPtyProcess', 'clear']) 61 | element.terminal = jasmine.createSpyObj('terminal', 62 | ['getSelection']) 63 | element.ptyProcess = jasmine.createSpyObj('ptyProcess', 64 | ['write']) 65 | tmpdir = await temp.mkdir() 66 | }) 67 | 68 | afterEach(async () => { 69 | await temp.cleanup() 70 | }) 71 | 72 | it('constructor with previous active item that has no getPath() method', async () => { 73 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue({}) 74 | const model = await createNewModel() 75 | 76 | expect(model.getPath()).toBe(configDefaults.cwd) 77 | }) 78 | 79 | it('constructor with valid cwd passed in uri', async () => { 80 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue({}) 81 | const model = await createNewModel({ cwd: tmpdir }) 82 | 83 | expect(model.getPath()).toBe(tmpdir) 84 | }) 85 | 86 | it('use projectCwd with valid cwd passed in uri', async () => { 87 | atom.config.set('x-terminal-reloaded.spawnPtySettings.cwd', tmpdir) 88 | const expected = await temp.mkdir('projectCwd') 89 | spyOn(atom.project, 'getPaths').and.returnValue([expected]) 90 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue({}) 91 | const model = await createNewModel({ projectCwd: true }) 92 | 93 | expect(model.getPath()).toBe(expected) 94 | }) 95 | 96 | it('constructor with invalid cwd passed in uri', async () => { 97 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue({}) 98 | const model = await createNewModel({ cwd: path.join(tmpdir, 'non-existent-dir') }) 99 | 100 | expect(model.getPath()).toBe(configDefaults.cwd) 101 | }) 102 | 103 | it('constructor with previous active item that has getPath() method', async () => { 104 | const previousActiveItem = jasmine.createSpyObj('somemodel', ['getPath']) 105 | previousActiveItem.getPath.and.returnValue(tmpdir) 106 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(previousActiveItem) 107 | const model = await createNewModel({ projectCwd: true }) 108 | 109 | expect(model.getPath()).toBe(tmpdir) 110 | }) 111 | 112 | it('constructor with previous active item that has selectedPath property', async () => { 113 | const previousActiveItem = jasmine.createSpyObj('somemodel', {}, { selectedPath: '' }) 114 | Object.getOwnPropertyDescriptor(previousActiveItem, 'selectedPath').get.and.returnValue(tmpdir) 115 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(previousActiveItem) 116 | const model = await createNewModel({ projectCwd: true }) 117 | 118 | expect(model.getPath()).toBe(tmpdir) 119 | }) 120 | 121 | it('constructor with previous active item that has getPath() method that returns file path', async () => { 122 | const previousActiveItem = jasmine.createSpyObj('somemodel', ['getPath']) 123 | const filePath = path.join(tmpdir, 'somefile') 124 | await fs.writeFile(filePath, '') 125 | previousActiveItem.getPath.and.returnValue(filePath) 126 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(previousActiveItem) 127 | const model = await createNewModel({ projectCwd: true }) 128 | 129 | expect(model.getPath()).toBe(tmpdir) 130 | }) 131 | 132 | it('constructor with previous active item that has selectedPath() property that returns file path', async () => { 133 | const previousActiveItem = jasmine.createSpyObj('somemodel', {}, { selectedPath: '' }) 134 | const filePath = path.join(tmpdir, 'somefile') 135 | await fs.writeFile(filePath, '') 136 | Object.getOwnPropertyDescriptor(previousActiveItem, 'selectedPath').get.and.returnValue(filePath) 137 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(previousActiveItem) 138 | const model = await createNewModel({ projectCwd: true }) 139 | 140 | expect(model.getPath()).toBe(tmpdir) 141 | }) 142 | 143 | it('constructor with previous active item that has getPath() returning invalid path', async () => { 144 | const previousActiveItem = jasmine.createSpyObj('somemodel', ['getPath']) 145 | previousActiveItem.getPath.and.returnValue(path.join(tmpdir, 'non-existent-dir')) 146 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(previousActiveItem) 147 | const model = await createNewModel({ projectCwd: true }) 148 | 149 | expect(model.getPath()).toBe(configDefaults.cwd) 150 | }) 151 | 152 | it('constructor with previous active item that has selectedPath returning invalid path', async () => { 153 | const previousActiveItem = jasmine.createSpyObj('somemodel', {}, { selectedPath: '' }) 154 | Object.getOwnPropertyDescriptor(previousActiveItem, 'selectedPath').get.and.returnValue(path.join(tmpdir, 'non-existent-dir')) 155 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(previousActiveItem) 156 | const model = await createNewModel({ projectCwd: true }) 157 | 158 | expect(model.getPath()).toBe(configDefaults.cwd) 159 | }) 160 | 161 | it('constructor with previous active item which exists in project path and calls getPath', async () => { 162 | const previousActiveItem = jasmine.createSpyObj('somemodel', ['getPath']) 163 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(previousActiveItem) 164 | spyOn(atom.project, 'relativizePath').and.returnValue(['/some/dir', null]) 165 | const model = await createNewModel({ projectCwd: true }) 166 | 167 | expect(model.getPath()).toBe('/some/dir') 168 | }) 169 | 170 | it('constructor with previous active item which exists in project path and calls selectedPath', async () => { 171 | const previousActiveItem = jasmine.createSpyObj('somemodel', {}, { selectedPath: '' }) 172 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(previousActiveItem) 173 | spyOn(atom.project, 'relativizePath').and.returnValue(['/some/dir', null]) 174 | const model = await createNewModel({ projectCwd: true }) 175 | 176 | expect(model.getPath()).toBe('/some/dir') 177 | }) 178 | 179 | it('constructor with custom title', async () => { 180 | const model = await createNewModel({ title: 'foo' }) 181 | 182 | expect(model.title).toBe('foo') 183 | }) 184 | 185 | it('serialize() no cwd set', async () => { 186 | const model = await createNewModel() 187 | const url = XTerminalProfilesSingleton.instance.generateNewUrlFromProfileData(model.profile) 188 | const expected = { 189 | deserializer: 'XTerminalModel', 190 | version: '2017-09-17', 191 | uri: url.href, 192 | } 193 | 194 | expect(model.serialize()).toEqual(expected) 195 | }) 196 | 197 | it('serialize() cwd set in model', async () => { 198 | const model = await createNewModel() 199 | model.profile.cwd = '/some/dir' 200 | const url = XTerminalProfilesSingleton.instance.generateNewUrlFromProfileData(model.profile) 201 | const expected = { 202 | deserializer: 'XTerminalModel', 203 | version: '2017-09-17', 204 | uri: url.href, 205 | } 206 | 207 | expect(model.serialize()).toEqual(expected) 208 | }) 209 | 210 | it('serialize() cwd set in uri', async () => { 211 | const model = await createNewModel({ cwd: tmpdir }) 212 | const url2 = XTerminalProfilesSingleton.instance.generateNewUrlFromProfileData(model.profile) 213 | const expected = { 214 | deserializer: 'XTerminalModel', 215 | version: '2017-09-17', 216 | uri: url2.href, 217 | } 218 | 219 | expect(url2.searchParams.get('cwd')).toEqual(tmpdir) 220 | expect(model.serialize()).toEqual(expected) 221 | }) 222 | 223 | it('destroy() check element is destroyed when set', () => { 224 | model.element = element 225 | model.destroy() 226 | 227 | expect(model.element.destroy).toHaveBeenCalled() 228 | }) 229 | 230 | it('destroy() check model removed from terminals_set', () => { 231 | spyOn(model.terminals_set, 'delete').and.callThrough() 232 | model.destroy() 233 | 234 | expect(model.terminals_set.delete.calls.allArgs()).toEqual([[model]]) 235 | }) 236 | 237 | it('getTitle() with default title', () => { 238 | expect(model.getTitle()).toBe('X-Terminal-Reloaded') 239 | }) 240 | 241 | it('getTitle() with new title', () => { 242 | const expected = 'some new title' 243 | model.title = expected 244 | 245 | expect(model.getTitle()).toBe(expected) 246 | }) 247 | 248 | it('getTitle() when active', () => { 249 | spyOn(model, 'isActiveTerminal').and.returnValue(true) 250 | 251 | expect(model.getTitle()).toBe(configDefaults.activeIndicator + ' X-Terminal-Reloaded') 252 | }) 253 | 254 | it('getElement()', () => { 255 | const expected = { somekey: 'somevalue' } 256 | model.element = expected 257 | 258 | expect(model.getElement()).toBe(expected) 259 | }) 260 | 261 | it('getURI()', async () => { 262 | const uri = 'x-terminal-reloaded://somesessionid/' 263 | const model = await createNewModel() 264 | 265 | expect(model.getURI()).toBe(uri) 266 | }) 267 | 268 | it('getLongTitle() with default title', () => { 269 | expect(model.getLongTitle()).toBe('X-Terminal-Reloaded') 270 | }) 271 | 272 | it('getLongTitle() with new title', () => { 273 | const expected = 'X-Terminal-Reloaded (some new title)' 274 | model.title = 'some new title' 275 | 276 | expect(model.getLongTitle()).toBe(expected) 277 | }) 278 | 279 | it('onDidChangeTitle()', () => { 280 | let callbackCalled = false 281 | const disposable = model.onDidChangeTitle(() => { 282 | callbackCalled = true 283 | }) 284 | model.emitter.emit('did-change-title') 285 | 286 | expect(callbackCalled).toBe(true) 287 | disposable.dispose() 288 | }) 289 | 290 | it('getIconName()', () => { 291 | expect(model.getIconName()).toBe('terminal') 292 | }) 293 | 294 | it('isModified()', () => { 295 | expect(model.isModified()).toBe(false) 296 | }) 297 | 298 | it('isModified() modified attribute set to true', () => { 299 | model.modified = true 300 | 301 | expect(model.isModified()).toBe(true) 302 | }) 303 | 304 | it('getPath()', () => { 305 | expect(model.getPath()).toBe(configDefaults.cwd) 306 | }) 307 | 308 | it('getPath() cwd set', () => { 309 | const expected = '/some/dir' 310 | model.profile.cwd = expected 311 | 312 | expect(model.getPath()).toBe(expected) 313 | }) 314 | 315 | it('onDidChangeModified()', () => { 316 | let callbackCalled = false 317 | const disposable = model.onDidChangeModified(() => { 318 | callbackCalled = true 319 | }) 320 | model.emitter.emit('did-change-modified') 321 | 322 | expect(callbackCalled).toBe(true) 323 | disposable.dispose() 324 | }) 325 | 326 | it('handleNewDataArrival() current item is active item', () => { 327 | pane.getActiveItem.and.returnValue(model) 328 | model.pane = pane 329 | model.handleNewDataArrival() 330 | 331 | expect(model.modified).toBe(false) 332 | }) 333 | 334 | it('handleNewDataArrival() current item is not active item', () => { 335 | pane.getActiveItem.and.returnValue({}) 336 | model.pane = pane 337 | model.handleNewDataArrival() 338 | 339 | expect(model.modified).toBe(true) 340 | }) 341 | 342 | it('handleNewDataArrival() current item is not in any pane', () => { 343 | model.pane = null 344 | model.handleNewDataArrival() 345 | 346 | expect(model.modified).toBe(true) 347 | }) 348 | 349 | it('handleNewDataArrival() model initially has no pane set', () => { 350 | pane.getActiveItem.and.returnValue({}) 351 | spyOn(atom.workspace, 'paneForItem').and.returnValue(pane) 352 | model.handleNewDataArrival() 353 | 354 | expect(atom.workspace.paneForItem).toHaveBeenCalled() 355 | }) 356 | 357 | it('handleNewDataArrival() modified value of false not changed', () => { 358 | pane.getActiveItem.and.returnValue(model) 359 | model.pane = pane 360 | spyOn(model.emitter, 'emit') 361 | model.handleNewDataArrival() 362 | 363 | expect(model.emitter.emit).toHaveBeenCalledTimes(0) 364 | }) 365 | 366 | it('handleNewDataArrival() modified value of true not changed', () => { 367 | pane.getActiveItem.and.returnValue({}) 368 | model.pane = pane 369 | model.modified = true 370 | spyOn(model.emitter, 'emit') 371 | model.handleNewDataArrival() 372 | 373 | expect(model.emitter.emit).toHaveBeenCalledTimes(0) 374 | }) 375 | 376 | it('handleNewDataArrival() modified value changed', () => { 377 | pane.getActiveItem.and.returnValue({}) 378 | model.pane = pane 379 | spyOn(model.emitter, 'emit') 380 | model.handleNewDataArrival() 381 | 382 | expect(model.emitter.emit).toHaveBeenCalled() 383 | }) 384 | 385 | it('getSessionId()', async () => { 386 | const expected = 'somesessionid' 387 | const model = await createNewModel() 388 | 389 | expect(model.getSessionId()).toBe(expected) 390 | }) 391 | 392 | it('getSessionParameters() when no parameters set', async () => { 393 | const model = await createNewModel() 394 | const url = XTerminalProfilesSingleton.instance.generateNewUrlFromProfileData(model.profile) 395 | url.searchParams.sort() 396 | 397 | expect(model.getSessionParameters()).toBe(url.searchParams.toString()) 398 | }) 399 | 400 | it('refitTerminal() without element set', () => { 401 | // Should just work. 402 | model.refitTerminal() 403 | }) 404 | 405 | it('refitTerminal() with element set', () => { 406 | model.element = element 407 | model.refitTerminal() 408 | 409 | expect(model.element.refitTerminal).toHaveBeenCalled() 410 | }) 411 | 412 | it('focusOnTerminal()', () => { 413 | model.element = element 414 | model.focusOnTerminal() 415 | 416 | expect(model.element.focusOnTerminal).toHaveBeenCalled() 417 | }) 418 | 419 | it('focusOnTerminal() reset modified value old modified value was false', () => { 420 | model.element = element 421 | model.focusOnTerminal() 422 | 423 | expect(model.modified).toBe(false) 424 | }) 425 | 426 | it('focusOnTerminal() reset modified value old modified value was true', () => { 427 | model.element = element 428 | model.modified = true 429 | model.focusOnTerminal() 430 | 431 | expect(model.modified).toBe(false) 432 | }) 433 | 434 | it('focusOnTerminal() no event emitted old modified value was false', () => { 435 | model.element = element 436 | spyOn(model.emitter, 'emit') 437 | model.focusOnTerminal() 438 | 439 | expect(model.emitter.emit).toHaveBeenCalledTimes(0) 440 | }) 441 | 442 | it('focusOnTerminal() event emitted old modified value was true', () => { 443 | model.element = element 444 | model.modified = true 445 | spyOn(model.emitter, 'emit') 446 | model.focusOnTerminal() 447 | 448 | expect(model.emitter.emit).toHaveBeenCalled() 449 | }) 450 | 451 | it('focusOnTerminal() activate pane', () => { 452 | model.element = element 453 | model.pane = pane 454 | model.focusOnTerminal() 455 | 456 | expect(model.pane.activateItem).toHaveBeenCalled() 457 | }) 458 | 459 | it('exit()', () => { 460 | model.pane = pane 461 | model.exit() 462 | 463 | expect(model.pane.destroyItem.calls.allArgs()).toEqual([[model, true]]) 464 | }) 465 | 466 | it('restartPtyProcess() no element set', () => { 467 | model.restartPtyProcess() 468 | 469 | expect(element.restartPtyProcess).not.toHaveBeenCalled() 470 | }) 471 | 472 | it('restartPtyProcess() element set', () => { 473 | model.element = element 474 | model.restartPtyProcess() 475 | 476 | expect(model.element.restartPtyProcess).toHaveBeenCalled() 477 | }) 478 | 479 | it('copyFromTerminal()', () => { 480 | model.element = element 481 | model.copyFromTerminal() 482 | 483 | expect(model.element.terminal.getSelection).toHaveBeenCalled() 484 | }) 485 | 486 | it('runCommand(cmd)', () => { 487 | model.element = element 488 | const expectedText = 'some text' 489 | model.runCommand(expectedText) 490 | 491 | expect(model.element.ptyProcess.write.calls.allArgs()).toEqual([[expectedText + (process.platform === 'win32' ? '\r' : '\n')]]) 492 | }) 493 | 494 | it('pasteToTerminal(text)', () => { 495 | model.element = element 496 | const expectedText = 'some text' 497 | model.pasteToTerminal(expectedText) 498 | 499 | expect(model.element.ptyProcess.write.calls.allArgs()).toEqual([[expectedText]]) 500 | }) 501 | 502 | it('clear()', () => { 503 | model.element = element 504 | model.clear() 505 | 506 | expect(model.element.clear).toHaveBeenCalled() 507 | }) 508 | 509 | it('setActive()', async () => { 510 | const pane = atom.workspace.getCenter().getActivePane() 511 | const terminalsSet = new Set() 512 | const model1 = await createNewModel(null, { terminals_set: terminalsSet }) 513 | pane.addItem(model1) 514 | model1.setNewPane(pane) 515 | const model2 = await createNewModel(null, { terminals_set: terminalsSet }) 516 | pane.addItem(model2) 517 | model2.setNewPane(pane) 518 | 519 | expect(model1.activeIndex).toBe(0) 520 | expect(model2.activeIndex).toBe(1) 521 | model2.setActive() 522 | 523 | expect(model1.activeIndex).toBe(1) 524 | expect(model2.activeIndex).toBe(0) 525 | }) 526 | 527 | describe('setNewPane', () => { 528 | it('(mock)', async () => { 529 | const expected = { getContainer: () => ({ getLocation: () => {} }) } 530 | model.setNewPane(expected) 531 | 532 | expect(model.pane).toBe(expected) 533 | expect(model.dock).toBe(null) 534 | }) 535 | 536 | it('(center)', async () => { 537 | const pane = atom.workspace.getCenter().getActivePane() 538 | model.setNewPane(pane) 539 | 540 | expect(model.pane).toBe(pane) 541 | expect(model.dock).toBe(null) 542 | }) 543 | 544 | it('(left)', async () => { 545 | const dock = atom.workspace.getLeftDock() 546 | const pane = dock.getActivePane() 547 | model.setNewPane(pane) 548 | 549 | expect(model.pane).toBe(pane) 550 | expect(model.dock).toBe(dock) 551 | }) 552 | 553 | it('(right)', async () => { 554 | const dock = atom.workspace.getRightDock() 555 | const pane = dock.getActivePane() 556 | model.setNewPane(pane) 557 | 558 | expect(model.pane).toBe(pane) 559 | expect(model.dock).toBe(dock) 560 | }) 561 | 562 | it('(bottom)', async () => { 563 | const dock = atom.workspace.getBottomDock() 564 | const pane = dock.getActivePane() 565 | model.setNewPane(pane) 566 | 567 | expect(model.pane).toBe(pane) 568 | expect(model.dock).toBe(dock) 569 | }) 570 | }) 571 | 572 | it('isVisible() in pane', () => { 573 | const pane = atom.workspace.getCenter().getActivePane() 574 | model.setNewPane(pane) 575 | 576 | expect(model.isVisible()).toBe(false) 577 | pane.setActiveItem(model) 578 | 579 | expect(model.isVisible()).toBe(true) 580 | }) 581 | 582 | it('isVisible() in dock', () => { 583 | const dock = atom.workspace.getBottomDock() 584 | const pane = dock.getActivePane() 585 | model.setNewPane(pane) 586 | pane.setActiveItem(model) 587 | 588 | expect(model.isVisible()).toBe(false) 589 | dock.show() 590 | 591 | expect(model.isVisible()).toBe(true) 592 | }) 593 | 594 | it('isActiveTerminal() visible and active', () => { 595 | model.activeIndex = 0 596 | spyOn(model, 'isVisible').and.returnValue(true) 597 | 598 | expect(model.isActiveTerminal()).toBe(true) 599 | }) 600 | 601 | it('isActiveTerminal() visible and not active', () => { 602 | model.activeIndex = 1 603 | spyOn(model, 'isVisible').and.returnValue(true) 604 | 605 | expect(model.isActiveTerminal()).toBe(false) 606 | }) 607 | 608 | it('isActiveTerminal() invisible and active', () => { 609 | model.activeIndex = 0 610 | spyOn(model, 'isVisible').and.returnValue(false) 611 | 612 | expect(model.isActiveTerminal()).toBe(false) 613 | }) 614 | 615 | it('isActiveTerminal() allowHiddenToStayActive', () => { 616 | atom.config.set('x-terminal-reloaded.terminalSettings.allowHiddenToStayActive', true) 617 | model.activeIndex = 0 618 | spyOn(model, 'isVisible').and.returnValue(false) 619 | 620 | expect(model.isActiveTerminal()).toBe(true) 621 | }) 622 | 623 | it('toggleProfileMenu()', () => { 624 | model.element = jasmine.createSpyObj('element', ['toggleProfileMenu']) 625 | model.toggleProfileMenu() 626 | 627 | expect(model.element.toggleProfileMenu).toHaveBeenCalled() 628 | }) 629 | 630 | it('getProfile()', () => { 631 | const mock = jasmine.createSpy('mock') 632 | model.profile = mock 633 | 634 | expect(model.getProfile()).toBe(mock) 635 | }) 636 | 637 | it('applyProfileChanges() element queueNewProfileChanges() called', () => { 638 | model.element = jasmine.createSpyObj('element', ['queueNewProfileChanges']) 639 | model.applyProfileChanges({}) 640 | 641 | expect(model.element.queueNewProfileChanges).toHaveBeenCalled() 642 | }) 643 | 644 | it('applyProfileChanges() profileChanges = {}', () => { 645 | model.element = jasmine.createSpyObj('element', ['queueNewProfileChanges']) 646 | const expected = model.profilesSingleton.deepClone(model.profile) 647 | model.applyProfileChanges({}) 648 | 649 | expect(model.profile).toEqual(expected) 650 | }) 651 | 652 | it('applyProfileChanges() profileChanges = {fontSize: 24}', () => { 653 | model.element = jasmine.createSpyObj('element', ['queueNewProfileChanges']) 654 | const expected = model.profilesSingleton.deepClone(model.profile) 655 | expected.fontSize = 24 656 | model.applyProfileChanges({ fontSize: 24 }) 657 | 658 | expect(model.profile).toEqual(expected) 659 | }) 660 | }) 661 | 662 | describe('XTerminalModel utilities', () => { 663 | it('isXTerminalModel() item is not XTerminalModel', () => { 664 | const item = document.createElement('div') 665 | 666 | expect(isXTerminalModel(item)).toBe(false) 667 | }) 668 | 669 | it('isXTerminalModel() item is XTerminalModel', async () => { 670 | const item = await createNewModel() 671 | 672 | expect(isXTerminalModel(item)).toBe(true) 673 | }) 674 | 675 | it('currentItemIsXTerminalModel() item is not XTerminalModel', () => { 676 | const item = document.createElement('div') 677 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(item) 678 | 679 | expect(currentItemIsXTerminalModel()).toBe(false) 680 | }) 681 | 682 | it('currentItemIsXTerminalModel() item is XTerminalModel', async () => { 683 | const item = await createNewModel() 684 | spyOn(atom.workspace, 'getActivePaneItem').and.returnValue(item) 685 | 686 | expect(currentItemIsXTerminalModel()).toBe(true) 687 | }) 688 | }) 689 | --------------------------------------------------------------------------------