├── .github ├── no-response.yml └── workflows │ └── ci.yml ├── .gitignore ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── lib ├── background-tips-view.js ├── background-tips.js └── tips.js ├── package.json ├── spec ├── async-spec-helpers.js └── background-tips-spec.js └── styles └── background-tips.less /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an issue is closed for lack of response 4 | daysUntilClose: 28 5 | 6 | # Label requiring a response 7 | responseRequiredLabel: more-information-needed 8 | 9 | # Comment to post when closing an issue for lack of response. Set to `false` to disable. 10 | closeComment: > 11 | This issue has been automatically closed because there has been no response 12 | to our request for more information from the original author. With only the 13 | information that is currently in the issue, we don't have enough information 14 | to take action. Please reach out if you have or find the answers we need so 15 | that we can investigate further. 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | env: 6 | CI: true 7 | 8 | jobs: 9 | Test: 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | channel: [stable, beta] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - uses: actions/checkout@v1 17 | - uses: UziTech/action-setup-atom@v2 18 | with: 19 | version: ${{ matrix.channel }} 20 | - name: Install dependencies 21 | run: apm install 22 | - name: Run tests 23 | run: atom --test spec 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [Atom contributing guide](https://atom.io/docs/latest/contributing) 2 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Prerequisites 10 | 11 | * [ ] Put an X between the brackets on this line if you have done all of the following: 12 | * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode 13 | * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ 14 | * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq 15 | * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom 16 | * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages 17 | 18 | ### Description 19 | 20 | [Description of the issue] 21 | 22 | ### Steps to Reproduce 23 | 24 | 1. [First Step] 25 | 2. [Second Step] 26 | 3. [and so on...] 27 | 28 | **Expected behavior:** [What you expect to happen] 29 | 30 | **Actual behavior:** [What actually happens] 31 | 32 | **Reproduces how often:** [What percentage of the time does it reproduce?] 33 | 34 | ### Versions 35 | 36 | You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running. 37 | 38 | ### Additional Information 39 | 40 | Any additional information, configuration or data that might be necessary to reproduce the issue. 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Requirements 2 | 3 | * Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. 4 | * All new code requires tests to ensure against regressions 5 | 6 | ### Description of the Change 7 | 8 | 13 | 14 | ### Alternate Designs 15 | 16 | 17 | 18 | ### Benefits 19 | 20 | 21 | 22 | ### Possible Drawbacks 23 | 24 | 25 | 26 | ### Applicable Issues 27 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | ## Background Tips package 3 | [![OS X Build Status](https://travis-ci.org/atom/background-tips.svg?branch=master)](https://travis-ci.org/atom/background-tips) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/2utcugietl5vjc7w/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/background-tips/branch/master) [![Dependency Status](https://david-dm.org/atom/background-tips.svg)](https://david-dm.org/atom/background-tips) 4 | 5 | Displays tips about Atom in the background when there are no open editors. 6 | 7 | ![Screen shot](https://f.cloud.github.com/assets/69169/1796267/c3de038c-6a60-11e3-8bf8-36f45684902c.png) 8 | -------------------------------------------------------------------------------- /lib/background-tips-view.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore-plus') 2 | const {CompositeDisposable, Disposable} = require('atom') 3 | const Tips = require('./tips') 4 | 5 | const TEMPLATE = `\ 6 | \ 9 | ` 10 | 11 | module.exports = 12 | class BackgroundTipsElement { 13 | constructor () { 14 | this.element = document.createElement('background-tips') 15 | this.index = -1 16 | this.workspaceCenter = atom.workspace.getCenter() 17 | 18 | this.startDelay = 1000 19 | this.displayDuration = 10000 20 | this.fadeDuration = 300 21 | 22 | this.disposables = new CompositeDisposable() 23 | 24 | const visibilityCallback = () => this.updateVisibility() 25 | 26 | this.disposables.add(this.workspaceCenter.onDidAddPane(visibilityCallback)) 27 | this.disposables.add(this.workspaceCenter.onDidDestroyPane(visibilityCallback)) 28 | this.disposables.add(this.workspaceCenter.onDidChangeActivePaneItem(visibilityCallback)) 29 | 30 | atom.getCurrentWindow().on('blur', visibilityCallback) 31 | atom.getCurrentWindow().on('focus', visibilityCallback) 32 | 33 | this.disposables.add(new Disposable(() => atom.getCurrentWindow().removeListener('blur', visibilityCallback))) 34 | this.disposables.add(new Disposable(() => atom.getCurrentWindow().removeListener('focus', visibilityCallback))) 35 | 36 | this.startTimeout = setTimeout(() => this.start(), this.startDelay) 37 | } 38 | 39 | destroy () { 40 | this.stop() 41 | this.disposables.dispose() 42 | } 43 | 44 | attach () { 45 | this.element.innerHTML = TEMPLATE 46 | this.message = this.element.querySelector('.message') 47 | 48 | const paneView = atom.views.getView(this.workspaceCenter.getActivePane()) 49 | const itemViews = paneView.querySelector('.item-views') 50 | let top = 0 51 | if (itemViews && itemViews.offsetTop) { 52 | top = itemViews.offsetTop 53 | } 54 | 55 | this.element.style.top = top + 'px' 56 | paneView.appendChild(this.element) 57 | } 58 | 59 | detach () { 60 | this.element.remove() 61 | } 62 | 63 | updateVisibility () { 64 | if (this.shouldBeAttached()) { 65 | this.start() 66 | } else { 67 | this.stop() 68 | } 69 | } 70 | 71 | shouldBeAttached () { 72 | return this.workspaceCenter.getPanes().length === 1 && 73 | this.workspaceCenter.getActivePaneItem() == null && 74 | atom.getCurrentWindow().isFocused() 75 | } 76 | 77 | start () { 78 | if (!this.shouldBeAttached() || this.interval != null) return 79 | this.renderTips() 80 | this.randomizeIndex() 81 | this.attach() 82 | this.showNextTip() 83 | this.interval = setInterval(() => this.showNextTip(), this.displayDuration) 84 | } 85 | 86 | stop () { 87 | this.element.remove() 88 | if (this.interval != null) { 89 | clearInterval(this.interval) 90 | } 91 | 92 | clearTimeout(this.startTimeout) 93 | clearTimeout(this.nextTipTimeout) 94 | this.interval = null 95 | } 96 | 97 | randomizeIndex () { 98 | const len = Tips.length 99 | this.index = Math.round(Math.random() * len) % len 100 | } 101 | 102 | showNextTip () { 103 | this.index = ++this.index % Tips.length 104 | this.message.classList.remove('fade-in') 105 | this.nextTipTimeout = setTimeout(() => { 106 | this.message.innerHTML = Tips[this.index] 107 | this.message.classList.add('fade-in') 108 | }, this.fadeDuration) 109 | } 110 | 111 | renderTips () { 112 | if (this.tipsRendered) return 113 | for (let i = 0; i < Tips.length; i++) { 114 | const tip = Tips[i] 115 | Tips[i] = this.renderTip(tip) 116 | } 117 | this.tipsRendered = true 118 | } 119 | 120 | renderTip (str) { 121 | str = str.replace(/\{(.+)\}/g, (match, command) => { 122 | let binding, scope 123 | const scopeAndCommand = command.split('>') 124 | if (scopeAndCommand.length > 1) { 125 | [scope, command] = scopeAndCommand 126 | } 127 | const bindings = atom.keymaps.findKeyBindings({command: command.trim()}) 128 | 129 | if (scope) { 130 | for (binding of bindings) { 131 | if (binding.selector === scope) break 132 | } 133 | } else { 134 | binding = this.getKeyBindingForCurrentPlatform(bindings) 135 | } 136 | 137 | if (binding && binding.keystrokes) { 138 | const keystrokeLabel = _.humanizeKeystroke(binding.keystrokes).replace(/\s+/g, ' ') 139 | return `${keystrokeLabel}` 140 | } else { 141 | return command 142 | } 143 | }) 144 | return str 145 | } 146 | 147 | getKeyBindingForCurrentPlatform (bindings) { 148 | if (!bindings || !bindings.length) return 149 | for (let binding of bindings) { 150 | if (binding.selector.indexOf(process.platform) !== -1) { 151 | return binding 152 | } 153 | } 154 | return bindings[0] 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/background-tips.js: -------------------------------------------------------------------------------- 1 | const BackgroundTipsView = require('./background-tips-view') 2 | 3 | module.exports = { 4 | activate () { 5 | this.backgroundTipsView = new BackgroundTipsView() 6 | }, 7 | 8 | deactivate () { 9 | this.backgroundTipsView.destroy() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/tips.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'Close panels like find and replace with {body>core:cancel}', 3 | 'Everything Atom can do is in the Command Palette. See it by using {command-palette:toggle}', 4 | 'You can quickly open files with the Fuzzy Finder. Try it by using {fuzzy-finder:toggle-file-finder}', 5 | 'You can toggle the Tree View with {tree-view:toggle}', 6 | 'You can focus the Tree View with {tree-view:toggle-focus}', 7 | 'You can toggle the Git tab with {github:toggle-git-tab}', 8 | 'You can focus the Git tab with {github:toggle-git-tab-focus}', 9 | 'You can toggle the GitHub tab with {github:toggle-github-tab}', 10 | 'You can focus the GitHub tab with {github:toggle-github-tab-focus}', 11 | 'You can split a pane with {pane:split-right-and-copy-active-item}', 12 | 'You can jump to a method in the editor using {symbols-view:toggle-file-symbols}', 13 | 'You can install packages and themes from the Settings View {settings-view:open}' 14 | ] 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "background-tips", 3 | "main": "./lib/background-tips", 4 | "version": "0.28.0", 5 | "private": true, 6 | "description": "Displays tips about Atom in the background when there are no editors open.", 7 | "repository": "https://github.com/atom/background-tips", 8 | "license": "MIT", 9 | "engines": { 10 | "atom": ">0.42.0" 11 | }, 12 | "dependencies": { 13 | "underscore-plus": "1.x" 14 | }, 15 | "devDependencies": { 16 | "standard": "^10.0.3" 17 | }, 18 | "standard": { 19 | "env": { 20 | "atomtest": true, 21 | "browser": true, 22 | "jasmine": true, 23 | "node": true 24 | }, 25 | "globals": [ 26 | "atom" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spec/async-spec-helpers.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | 3 | export function beforeEach (fn) { 4 | global.beforeEach(function () { 5 | const result = fn() 6 | if (result instanceof Promise) { 7 | waitsForPromise(() => result) 8 | } 9 | }) 10 | } 11 | 12 | export function afterEach (fn) { 13 | global.afterEach(function () { 14 | const result = fn() 15 | if (result instanceof Promise) { 16 | waitsForPromise(() => result) 17 | } 18 | }) 19 | } 20 | 21 | ['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { 22 | module.exports[name] = function (description, fn) { 23 | if (fn === undefined) { 24 | global[name](description) 25 | return 26 | } 27 | 28 | global[name](description, function () { 29 | const result = fn() 30 | if (result instanceof Promise) { 31 | waitsForPromise(() => result) 32 | } 33 | }) 34 | } 35 | }) 36 | 37 | export async function conditionPromise (condition, description = 'anonymous condition') { 38 | const startTime = Date.now() 39 | 40 | while (true) { 41 | await timeoutPromise(100) 42 | 43 | if (await condition()) { 44 | return 45 | } 46 | 47 | if (Date.now() - startTime > 5000) { 48 | throw new Error('Timed out waiting on ' + description) 49 | } 50 | } 51 | } 52 | 53 | export function timeoutPromise (timeout) { 54 | return new Promise(function (resolve) { 55 | global.setTimeout(resolve, timeout) 56 | }) 57 | } 58 | 59 | function waitsForPromise (fn) { 60 | const promise = fn() 61 | global.waitsFor('spec promise to resolve', function (done) { 62 | promise.then(done, function (error) { 63 | jasmine.getEnv().currentSpec.fail(error) 64 | done() 65 | }) 66 | }) 67 | } 68 | 69 | export function emitterEventPromise (emitter, event, timeout = 15000) { 70 | return new Promise((resolve, reject) => { 71 | const timeoutHandle = setTimeout(() => { 72 | reject(new Error(`Timed out waiting for '${event}' event`)) 73 | }, timeout) 74 | emitter.once(event, () => { 75 | clearTimeout(timeoutHandle) 76 | resolve() 77 | }) 78 | }) 79 | } 80 | 81 | export function promisify (original) { 82 | return function (...args) { 83 | return new Promise((resolve, reject) => { 84 | args.push((err, ...results) => { 85 | if (err) { 86 | reject(err) 87 | } else { 88 | resolve(...results) 89 | } 90 | }) 91 | 92 | return original(...args) 93 | }) 94 | } 95 | } 96 | 97 | export function promisifySome (obj, fnNames) { 98 | const result = {} 99 | for (const fnName of fnNames) { 100 | result[fnName] = promisify(obj[fnName]) 101 | } 102 | return result 103 | } 104 | -------------------------------------------------------------------------------- /spec/background-tips-spec.js: -------------------------------------------------------------------------------- 1 | const {it, fit, ffit, afterEach, beforeEach, emitterEventPromise} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars 2 | 3 | describe('BackgroundTips', () => { 4 | let workspaceElement 5 | 6 | const activatePackage = async () => { 7 | const {mainModule} = await atom.packages.activatePackage('background-tips') 8 | return mainModule.backgroundTipsView 9 | } 10 | 11 | beforeEach(() => { 12 | workspaceElement = atom.views.getView(atom.workspace) 13 | jasmine.attachToDOM(workspaceElement) 14 | jasmine.useMockClock() 15 | spyOn(atom.getCurrentWindow(), 'isFocused').andReturn(true) 16 | }) 17 | 18 | describe('when the package is activated when there is only one pane', () => { 19 | beforeEach(() => { 20 | expect(atom.workspace.getCenter().getPanes().length).toBe(1) 21 | }) 22 | 23 | describe('when the pane is empty', () => { 24 | it('attaches the view after a delay', async () => { 25 | expect(atom.workspace.getActivePane().getItems().length).toBe(0) 26 | 27 | const backgroundTipsView = await activatePackage() 28 | expect(backgroundTipsView.element.parentNode).toBeFalsy() 29 | advanceClock(backgroundTipsView.startDelay + 1) 30 | expect(backgroundTipsView.element.parentNode).toBeTruthy() 31 | }) 32 | }) 33 | 34 | describe('when the pane is not empty', () => { 35 | it('does not attach the view', async () => { 36 | await atom.workspace.open() 37 | 38 | const backgroundTipsView = await activatePackage() 39 | advanceClock(backgroundTipsView.startDelay + 1) 40 | expect(backgroundTipsView.element.parentNode).toBeFalsy() 41 | }) 42 | }) 43 | 44 | describe('when a second pane is created', () => { 45 | it('detaches the view', async () => { 46 | const backgroundTipsView = await activatePackage() 47 | advanceClock(backgroundTipsView.startDelay + 1) 48 | expect(backgroundTipsView.element.parentNode).toBeTruthy() 49 | 50 | atom.workspace.getActivePane().splitRight() 51 | expect(backgroundTipsView.element.parentNode).toBeFalsy() 52 | }) 53 | }) 54 | }) 55 | 56 | describe('when the package is activated when there are multiple panes', () => { 57 | beforeEach(() => { 58 | atom.workspace.getActivePane().splitRight() 59 | expect(atom.workspace.getCenter().getPanes().length).toBe(2) 60 | }) 61 | 62 | it('does not attach the view', async () => { 63 | const backgroundTipsView = await activatePackage() 64 | advanceClock(backgroundTipsView.startDelay + 1) 65 | expect(backgroundTipsView.element.parentNode).toBeFalsy() 66 | }) 67 | 68 | describe('when all but the last pane is destroyed', () => { 69 | it('attaches the view', async () => { 70 | const backgroundTipsView = await activatePackage() 71 | atom.workspace.getActivePane().destroy() 72 | advanceClock(backgroundTipsView.startDelay + 1) 73 | expect(backgroundTipsView.element.parentNode).toBeTruthy() 74 | 75 | atom.workspace.getActivePane().splitRight() 76 | expect(backgroundTipsView.element.parentNode).toBeFalsy() 77 | 78 | atom.workspace.getActivePane().destroy() 79 | expect(backgroundTipsView.element.parentNode).toBeTruthy() 80 | }) 81 | }) 82 | }) 83 | 84 | describe('when the view is attached', () => { 85 | let backgroundTipsView 86 | 87 | beforeEach(async () => { 88 | expect(atom.workspace.getCenter().getPanes().length).toBe(1) 89 | 90 | backgroundTipsView = await activatePackage() 91 | advanceClock(backgroundTipsView.startDelay) 92 | advanceClock(backgroundTipsView.fadeDuration) 93 | }) 94 | 95 | it('has text in the message', () => { 96 | expect(backgroundTipsView.element.parentNode).toBeTruthy() 97 | expect(backgroundTipsView.message.textContent).toBeTruthy() 98 | }) 99 | 100 | it('changes text in the message', async () => { 101 | const oldText = backgroundTipsView.message.textContent 102 | advanceClock(backgroundTipsView.displayDuration) 103 | advanceClock(backgroundTipsView.fadeDuration) 104 | expect(backgroundTipsView.message.textContent).not.toEqual(oldText) 105 | }) 106 | }) 107 | 108 | describe('when Atom is not focused but all other requirements are satisfied', () => { 109 | beforeEach(() => { 110 | jasmine.unspy(atom.getCurrentWindow(), 'isFocused') 111 | spyOn(atom.getCurrentWindow(), 'isFocused').andReturn(false) 112 | }) 113 | 114 | it('does not display the background tips', async () => { 115 | expect(atom.workspace.getActivePane().getItems().length).toBe(0) 116 | 117 | const backgroundTipsView = await activatePackage() 118 | expect(backgroundTipsView.element.parentNode).toBeFalsy() 119 | advanceClock(backgroundTipsView.startDelay + 1) 120 | expect(backgroundTipsView.element.parentNode).toBeFalsy() 121 | }) 122 | 123 | it('reactivates the background tips if the focus event is received', async () => { 124 | expect(atom.workspace.getActivePane().getItems().length).toBe(0) 125 | 126 | const backgroundTipsView = await activatePackage() 127 | advanceClock(backgroundTipsView.startDelay + 1) 128 | expect(backgroundTipsView.element.parentNode).toBeFalsy() 129 | 130 | jasmine.unspy(atom.getCurrentWindow(), 'isFocused') 131 | spyOn(atom.getCurrentWindow(), 'isFocused').andReturn(true) 132 | 133 | const focusEvent = emitterEventPromise(atom.getCurrentWindow(), 'focus') 134 | atom.getCurrentWindow().emit('focus') // Manually emit to prevent actually blurring + refocusing the window 135 | 136 | await focusEvent 137 | 138 | advanceClock(backgroundTipsView.startDelay + 1) 139 | expect(backgroundTipsView.element.parentNode).toBeTruthy() 140 | }) 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /styles/background-tips.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | background-tips { 8 | display: block; 9 | pointer-events: none; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | bottom: 0; 15 | overflow: hidden; 16 | 17 | .background-message { 18 | user-select: none; 19 | cursor: default; 20 | 21 | .message { 22 | padding: 0 @component-padding*3; 23 | opacity: 0; 24 | transition: opacity .3s ease-in-out; 25 | &.fade-in { 26 | opacity: 1; 27 | } 28 | 29 | .keystroke { 30 | border: 2px solid; 31 | padding: 0 @component-padding/2; 32 | border-radius: @component-border-radius * 3; 33 | font-family: "Helvetica Neue", Arial, sans-serif; 34 | } 35 | } 36 | } 37 | } 38 | --------------------------------------------------------------------------------