├── keymaps └── dark-mode.json ├── lib ├── sensors │ ├── index.js │ ├── system-theme.js │ ├── ambient-light.js │ ├── sun.js │ └── sensor.js ├── config.js └── dark-mode.js ├── menus └── dark-mode.json ├── package.json ├── LICENSE.md └── README.md /keymaps/dark-mode.json: -------------------------------------------------------------------------------- 1 | { 2 | "atom-workspace": { 3 | "ctrl-`": "dark-mode:toggle" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/sensors/index.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import Sun from './sun'; 4 | import SystemTheme from './system-theme'; 5 | import AmbientLight from './ambient-light'; 6 | 7 | export default [ 8 | SystemTheme, 9 | Sun, 10 | AmbientLight 11 | ]; 12 | -------------------------------------------------------------------------------- /menus/dark-mode.json: -------------------------------------------------------------------------------- 1 | { 2 | "context-menu": { 3 | "atom-text-editor": [ 4 | { 5 | "label": "Toggle dark-mode", 6 | "command": "dark-mode:toggle" 7 | } 8 | ] 9 | }, 10 | "menu": [ 11 | { 12 | "label": "Packages", 13 | "submenu": [ 14 | { 15 | "label": "dark-mode", 16 | "submenu": [ 17 | { 18 | "label": "Toggle", 19 | "command": "dark-mode:toggle" 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dark-mode", 3 | "main": "./lib/dark-mode", 4 | "version": "4.0.2", 5 | "author": "Exelord", 6 | "description": "Automatically switch between dark mode and light mode thanks to your light sensor or just change your mood switching between your favorites themes.", 7 | "keywords": [ 8 | "dark mode", 9 | "themes", 10 | "switch", 11 | "atom" 12 | ], 13 | "activationCommands": {}, 14 | "repository": "https://github.com/exelord/dark-mode", 15 | "license": "MIT", 16 | "engines": { 17 | "atom": ">=1.0.0 <2.0.0" 18 | }, 19 | "dependencies": { 20 | "lodash.throttle": "^4.0.8", 21 | "node-schedule": "^1.3.1", 22 | "suncalc": "^1.8.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/sensors/system-theme.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import Sensor from './sensor'; 4 | import { systemPreferences } from 'electron'; 5 | 6 | export default class extends Sensor { 7 | get sensorOptionName() { 8 | return 'systemThemeSensor'; 9 | } 10 | 11 | activate() { 12 | if (systemPreferences) { 13 | this.setCurrentSystemTheme(); 14 | this.subscriptionId = systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => { 15 | this.setCurrentSystemTheme(); 16 | }); 17 | } else { 18 | this.log('Sensor is not supported on this system!', 'warn'); 19 | } 20 | } 21 | 22 | setCurrentSystemTheme() { 23 | if (systemPreferences) { 24 | systemPreferences.isDarkMode() ? this.switchToDarkMode() : this.switchToLightMode(); 25 | } 26 | } 27 | 28 | deactivate() { 29 | if (this.subscriptionId && systemPreferences) { 30 | systemPreferences.unsubscribeNotification(this.subscriptionId); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/sensors/ambient-light.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import Sensor from './sensor'; 4 | 5 | import throttle from 'lodash.throttle'; 6 | 7 | const loopInterval = 3000; 8 | 9 | export default class extends Sensor { 10 | get sensorOptionName() { 11 | return 'ambientLightSensor'; 12 | } 13 | 14 | get thresholdOption() { 15 | return atom.config.get('dark-mode.ambientLightThreshold'); 16 | } 17 | 18 | activate() { 19 | this._initializeSensor(); 20 | 21 | if (this.sensor) { 22 | this.sensor.start(); 23 | } 24 | } 25 | 26 | deactivate() { 27 | if (this.sensor) { 28 | this.sensor.stop(); 29 | } 30 | } 31 | 32 | _initializeSensor() { 33 | this.sensor = new window.AmbientLightSensor(); 34 | this.sensor.onreading = throttle(this._onLightChange.bind(this), loopInterval); 35 | this.sensor.onerror = (event) => this.log(event.error.message, 'warn'); 36 | 37 | } 38 | 39 | _onLightChange({ value }) { 40 | value > this.thresholdOption ? this.switchToLightMode() : this.switchToDarkMode(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Exelord 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 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | export default { 4 | darkProfile: { 5 | order: 1, 6 | description: 'Specify ui and syntax theme in the right order', 7 | type: 'string', 8 | default: 'one-dark-ui one-dark-syntax' 9 | }, 10 | 11 | lightProfile: { 12 | order: 2, 13 | description: 'Specify ui and syntax theme in the right order', 14 | type: 'string', 15 | default: 'one-light-ui one-light-syntax' 16 | }, 17 | 18 | autoMode: { 19 | order: 3, 20 | description: 'Determine if themes should be automatically changed by selected sensors', 21 | type: 'boolean', 22 | default: true 23 | }, 24 | 25 | ambientLightSensor: { 26 | order: 4, 27 | description: 'Determine if themes should be automatically changed by your ambient light sensor', 28 | type: 'boolean', 29 | default: false 30 | }, 31 | 32 | ambientLightThreshold: { 33 | order: 5, 34 | description: 'Determine threshold of Ambient Light Sensor (lower is darker)', 35 | type: 'integer', 36 | default: '10' 37 | }, 38 | 39 | sunSensor: { 40 | order: 6, 41 | description: 'Determine if themes should be automatically changed based on your sunset time', 42 | type: 'boolean', 43 | default: true 44 | }, 45 | 46 | systemThemeSensor: { 47 | order: 7, 48 | description: 'Determine if themes should be automatically changed when the system theme changes', 49 | type: 'boolean', 50 | default: true 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dark Mode 2 | 3 | Package for Atom Editor which allow you to switch to `dark mode` and `light mode` theme automatically thanks to your Mac light sensor. 4 | 5 | ## Installation 6 | Run: `apm install dark-mode` or use Atom Package Manager in Atom settings. 7 | 8 | ** After installation remember to restart Atom or run `Window: Reload` in command palette.** 9 | 10 | ## Manual Theme switcher 11 | ![Dark Mode](https://raw.githubusercontent.com/Exelord/dark-mode/master/DarkMode640.gif) 12 | 13 | To change theme manually use: 14 | > ctrl + \` 15 | 16 | or in command palette choose `Dark Mode: Toggle` 17 | 18 | ## Auto mode 19 | By activating auto mode you will take an advantage of implemented sensors to switch the theme automatically. 20 | 21 | To disable/enable auto mode choose in command palette: 22 | `Dark Mode: Turn On Auto mode` or `Dark Mode: Turn Off Auto Mode` 23 | 24 | ### Sun Sensor 25 | Based on DAY or NIGHT atom will change theme automatically. 26 | 27 | ### System Theme Sensor 28 | **Not working until Atom will upgrade to Electron 3.0 but we are already all set :)** 29 | Based on your system's theme atom will adjust the theme automatically. 30 | 31 | ### Ambient Light Sensor 32 | **Not working until Atom will enable chrome's sensors.api by default** 33 | 34 | It will change your theme based on your ambient light sensor of your computer (if present and supported) 35 | 36 | You can setup the interval of refreshing and threshold of darkness level in the package settings. 37 | 38 | ## Customization 39 | Go to the package config in Atom settings. 40 | 41 | You can specify your own custom theme for each mode and use it as a fast theme switcher. 42 | -------------------------------------------------------------------------------- /lib/sensors/sun.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import Sensor from './sensor'; 4 | import SunCalc from 'suncalc'; 5 | import schedule from 'node-schedule'; 6 | 7 | export default class extends Sensor { 8 | constructor() { 9 | super(...arguments); 10 | } 11 | 12 | get sensorOptionName() { 13 | return 'sunSensor'; 14 | } 15 | 16 | get currentPhase() { 17 | let now = new Date(); 18 | let { nightAt, dayAt } = this; 19 | 20 | return now >= dayAt && now < nightAt ? 'day' : 'night'; 21 | } 22 | 23 | async activate() { 24 | await this._fetchSunTimes(); 25 | 26 | if (this.dayAt && this.nightAt) { 27 | let changeAt = this.currentPhase == 'day' ? this.nightAt : this.dayAt; 28 | 29 | this._setCurrentPhaseTheme(); 30 | this.scheduler = this._callAt(changeAt, () => this.activate()); 31 | } else { 32 | this.log('Could not calculate sun positions', 'warn'); 33 | } 34 | } 35 | 36 | deactivate() { 37 | this.scheduler.cancel(); 38 | } 39 | 40 | _fetchSunTimes() { 41 | return fetch('http://ip-api.com/json?fields=lat,lon') 42 | .then((response) => response.json()) 43 | .then(({ lat, lon }) => { 44 | let day = 1000 * 60 * 60 * 24; 45 | let now = new Date(); 46 | let tomorrow = new Date(now.getTime() + day); 47 | let yesterday = new Date(now.getTime() - day); 48 | 49 | let todayTimes = this._calculateTimes(now, lat, lon); 50 | 51 | this.dayAt = now >= todayTimes.nightAt ? this._calculateTimes(tomorrow, lat, lon).dayAt : todayTimes.dayAt; 52 | this.nightAt = now < todayTimes.dayAt ? this._calculateTimes(yesterday, lat, lon).nightAt : todayTimes.nightAt; 53 | }); 54 | } 55 | 56 | _calculateTimes(date, lat, long) { 57 | let times = SunCalc.getTimes(date, lat, long); 58 | 59 | return { 60 | nightAt: times.dusk, 61 | dayAt: times.sunriseEnd 62 | }; 63 | } 64 | 65 | _setCurrentPhaseTheme() { 66 | this.currentPhase == 'day' ? this.switchToLightMode() : this.switchToDarkMode(); 67 | } 68 | 69 | _callAt(date, callback = () => {}) { 70 | let now = new Date(); 71 | 72 | if (date && date > now) { 73 | this.log(`Theme change has been schedule at: ${date}`); 74 | return schedule.scheduleJob(date, callback); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/dark-mode.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | import Config from './config'; 4 | import { CompositeDisposable } from 'atom'; 5 | import sensors from './sensors'; 6 | 7 | const notificationsOptions = { icon: 'light-bulb' }; 8 | 9 | export default { 10 | config: Config, 11 | subscriptions: null, 12 | 13 | get sensors() { 14 | return this._sensors || this._initializeSensors(); 15 | }, 16 | 17 | get lightTheme() { 18 | return atom.config.get('dark-mode.lightProfile'); 19 | }, 20 | 21 | get darkTheme() { 22 | return atom.config.get('dark-mode.darkProfile'); 23 | }, 24 | 25 | get currentTheme() { 26 | return atom.config.get('core.themes').join(' '); 27 | }, 28 | 29 | get contrastTheme() { 30 | return (this.currentTheme == this.darkTheme ? this.lightTheme : this.darkTheme); 31 | }, 32 | 33 | get autoModeOption() { 34 | return atom.config.get('dark-mode.autoMode'); 35 | }, 36 | 37 | activate() { 38 | this.subscriptions = new CompositeDisposable(); 39 | 40 | this._registerCommands(); 41 | this._registerCallbacks(); 42 | this._setSensorsState(this.autoModeOption); 43 | this._startupNotification(); 44 | }, 45 | 46 | deactivate() { 47 | this._setSensorsState(false); 48 | this._disposeCallbacks(); 49 | }, 50 | 51 | switchToLightMode() { 52 | if (this.currentTheme != this.lightTheme) { 53 | this._changeTheme(this.lightTheme); 54 | atom.notifications.addSuccess('Dark Mode: Theme has been changed automatically', notificationsOptions); 55 | } 56 | }, 57 | 58 | switchToDarkMode() { 59 | if (this.currentTheme != this.darkTheme) { 60 | this._changeTheme(this.darkTheme); 61 | atom.notifications.addSuccess('Dark Mode: Theme has been changed automatically', notificationsOptions); 62 | } 63 | }, 64 | 65 | _registerCommands() { 66 | this.subscriptions.add(atom.commands.add('atom-workspace', { 67 | 'dark-mode:toggle': () => this._toggle(), 68 | 'dark-mode:Turn On Auto Mode': () => atom.config.set('dark-mode.autoMode', true), 69 | 'dark-mode:Turn Off Auto Mode': () => atom.config.set('dark-mode.autoMode', false) 70 | })); 71 | }, 72 | 73 | _registerCallbacks() { 74 | this.subscriptions.add(atom.config.onDidChange('dark-mode.autoMode', ({ newValue }) => { 75 | this._setSensorsState(newValue); 76 | atom.notifications.addInfo(`Dark Mode: Automatic mode is ${newValue ? 'ON' : 'OFF'}`, notificationsOptions); 77 | })); 78 | }, 79 | 80 | _initializeSensors() { 81 | return this._sensors = sensors.map((Sensor) => new Sensor(this)); 82 | }, 83 | 84 | _setSensorsState(state) { 85 | this.sensors.forEach((sensor) => state ? sensor.activateSensor() : sensor.deactivateSensor()); 86 | }, 87 | 88 | _disposeCallbacks() { 89 | this.sensors.forEach((sensor) => sensor.disposeCallbacks()); 90 | this.subscriptions.dispose(); 91 | }, 92 | 93 | _toggle() { 94 | atom.config.set('dark-mode.autoMode', false); 95 | return this._changeTheme(this.contrastTheme); 96 | }, 97 | 98 | _changeTheme(theme = '') { 99 | atom.config.set('core.themes', theme.split(' ')); 100 | }, 101 | 102 | _startupNotification() { 103 | atom.notifications.addInfo(`Dark Mode: Automatic mode is ${this.autoModeOption ? 'ON' : 'OFF'}`, { 104 | icon: 'light-bulb', 105 | buttons: [{ 106 | text: this.autoModeOption ? 'Disable' : 'Enable', 107 | onDidClick() { 108 | atom.config.set('dark-mode.autoMode', !this.autoModeOption); 109 | } 110 | }] 111 | }); 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /lib/sensors/sensor.js: -------------------------------------------------------------------------------- 1 | 'use babel'; 2 | 3 | /** 4 | * To define a new sensor use following template and create it in `sensors` folder: 5 | * 6 | * ```js 7 | * import Sensor from './sensor'; 8 | * 9 | * export default class extends Sensor { 10 | * get sensorOptionName() { 11 | * return 'yourSensorConfigOptionName'; 12 | * } 13 | * 14 | * activate() { 15 | * // activate the sensor 16 | * } 17 | * 18 | * deactivate() { 19 | * // deactivate the sensor 20 | * } 21 | * } 22 | * ``` 23 | * 24 | * Next go to `dark-mode/sensors/index.js` and export a new sensor: 25 | * ```js 26 | * import MySuperSensor from './my-super-sensor' 27 | * 28 | * export [ 29 | * AmbientLightSensor, 30 | * MySuperSensor 31 | * ] 32 | * 33 | * ``` 34 | */ 35 | 36 | import { CompositeDisposable } from 'atom'; 37 | 38 | export default class { 39 | constructor(darkMode) { 40 | this.darkMode = darkMode; 41 | this.subscriptions = new CompositeDisposable(); 42 | this._registerCallbacks(); 43 | } 44 | 45 | /** 46 | * @public 47 | * @override 48 | * Sensor option name 49 | */ 50 | get sensorOptionName() { 51 | return 'autoMode'; 52 | } 53 | 54 | /** 55 | * @public 56 | * @protected 57 | * Returns a config status if a sensor is enabled or not 58 | * @return {Boolean} sensor config status 59 | */ 60 | get isEnabled() { 61 | return atom.config.get(`dark-mode.${this.sensorOptionName}`); 62 | } 63 | 64 | /** 65 | * @public 66 | * @protected 67 | * @return {Boolean} auto mode config status 68 | */ 69 | get isAutoMode() { 70 | return atom.config.get('dark-mode.autoMode'); 71 | } 72 | 73 | /** 74 | * @public 75 | * @protected 76 | * Status if the sensor is currently activated or not 77 | * @return {Boolean} sensor status 78 | */ 79 | get isActive() { 80 | return this._isActive || false; 81 | } 82 | 83 | /** 84 | * @public 85 | * @override 86 | * This method should activate the sensor (turn on) 87 | */ 88 | activate() {} 89 | 90 | /** 91 | * @public 92 | * @override 93 | * This method should deactivate the sensor (turn off) 94 | */ 95 | deactivate() {} 96 | 97 | /** 98 | * @public 99 | */ 100 | switchToLightMode() { 101 | this.darkMode.switchToLightMode(); 102 | } 103 | 104 | /** 105 | * @public 106 | */ 107 | switchToDarkMode() { 108 | this.darkMode.switchToDarkMode(); 109 | } 110 | 111 | /** 112 | * @public 113 | */ 114 | log(message, severity = 'info') { 115 | return console[severity](`DarkMode: [${this.sensorOptionName}] | ${message}`); 116 | } 117 | 118 | /** 119 | * @private 120 | * @override 121 | */ 122 | activateSensor() { 123 | if (this.isAutoMode && this.isEnabled && !this.isActive) { 124 | this.activate(); 125 | this._isActive = true; 126 | this.log('Sensor has been activated'); 127 | } 128 | 129 | return this.isActive; 130 | } 131 | 132 | /** 133 | * @private 134 | */ 135 | deactivateSensor() { 136 | if (this.isActive) { 137 | this.deactivate(); 138 | this._isActive = false; 139 | this.log('Sensor has been deactivated'); 140 | } 141 | 142 | return !this.isActive; 143 | } 144 | 145 | /** 146 | * @private 147 | */ 148 | disposeCallbacks() { 149 | this.subscriptions.dispose(); 150 | } 151 | 152 | _registerCallbacks() { 153 | this.subscriptions.add(atom.config.onDidChange(`dark-mode.${this.sensorOptionName}`, ({ newValue }) => { 154 | newValue ? this.activateSensor() : this.deactivateSensor(); 155 | })); 156 | } 157 | } 158 | --------------------------------------------------------------------------------