├── .editorconfig ├── .gitignore ├── .jshintrc ├── Gruntfile.js ├── README.md ├── app ├── assets │ ├── icon1.png │ └── icon2.png ├── com.capablemonkey.sleepApp.plist ├── css │ └── main.css ├── js │ └── index.js ├── package.json └── views │ └── index.html ├── assets ├── dmgBackground.png ├── dmgBackground.psd ├── sleepicon.icns └── sleepicon.sketch ├── bower.json ├── dmgConfig.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | .DS_Store 4 | npm-debug.log 5 | app/npm-debug.log 6 | dist/ 7 | webkitbuilds/ 8 | cache/ -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 4, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "white": true 21 | } 22 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jshint camelcase: false*/ 2 | 3 | module.exports = function (grunt) { 4 | 'use strict'; 5 | 6 | grunt.loadNpmTasks('grunt-node-webkit-builder'); 7 | 8 | grunt.initConfig({ 9 | nodewebkit: { 10 | options: { 11 | platforms: ['osx'], 12 | buildDir: './webkitbuilds', // Where the build version of my node-webkit app is saved 13 | macIcns: './assets/sleepicon.icns' 14 | }, 15 | src: ['./app/**/*'] // Your node-webkit app 16 | }, 17 | }) 18 | }; 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sleep 2 | 3 | [![Join the chat at https://gitter.im/capablemonkey/sleep](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/capablemonkey/sleep?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | ![screenshot](https://cloud.githubusercontent.com/assets/1661310/6768116/df5ce18e-d02e-11e4-9332-99717bd20294.png) 6 | 7 | A little [node-webkit / nw.js](https://github.com/nwjs/nw.js/) application that attempts to answer the infamous question of "Whoa... when did I fall asleep?" by telling you when you last closed your Macbook lid or when your Mac last fell asleep after being idle for a while. 8 | 9 | Actually, I lied. It's not that little. Because it relies on the nw.js runtime, it's like 98MB with the runtime bundled. Sigh. 10 | 11 | Sleep has its own [project page](http://capablemonkey.github.io/sleep/)! 12 | 13 | ## Download it 14 | 15 | [Download the Mac OS X DMG](https://github.com/capablemonkey/sleep/raw/build/webkitbuilds/sleep.dmg). Only available for 64-bit Macs. 16 | 17 | ## Playing with the source 18 | 19 | ### Installing dependencies 20 | You'll need to make sure you have nw.js installed. 21 | 22 | `npm install nw -g` 23 | 24 | The actual nw.js / node-webkit app is located in the `/app` directory. The root directory encapsulates the `/app` directory to provide build tools to actually compile the app. 25 | 26 | You'll want to make sure you do `npm install` in both the root directory and the `/app` directory. 27 | 28 | ### Running 29 | From the root directory: 30 | 31 | `nw app` 32 | 33 | ### Building 34 | To build, do from the *root* directory of the repo: 35 | 36 | `grunt nodewebkit` 37 | 38 | It'll build sleep.app, targeting OS X 32-bit and 64-bit. You'll find the resulting .app files in `/webkitbuilds/sleep/`. 39 | 40 | #### Building the DMG 41 | Once the app's been packaged, we'll want to build the DMG. Make sure you have `appdmg` installed: 42 | 43 | `npm install -g appdmg` 44 | 45 | Then, do: 46 | 47 | `appdmg dmgConfig.json webkitbuilds/sleep.dmg` 48 | 49 | And your DMG will end up in webkitbuilds/. In the future, consider adding this as a grunt task in the gruntfile. 50 | 51 | ## todo 52 | 53 | - Right now it'll refresh every 5 minutes. But, it'd be nicer if it could detect when the machine comes out of sleep and refresh 54 | -------------------------------------------------------------------------------- /app/assets/icon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capablemonkey/sleep/fb7d3c93c9579136a4f8f909aaec8ad9bc71c157/app/assets/icon1.png -------------------------------------------------------------------------------- /app/assets/icon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capablemonkey/sleep/fb7d3c93c9579136a4f8f909aaec8ad9bc71c157/app/assets/icon2.png -------------------------------------------------------------------------------- /app/com.capablemonkey.sleepApp.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | com.capablemonkey.sleepApp 7 | ProgramArguments 8 | 9 | /usr/bin/open 10 | -W 11 | /Applications/sleep.app 12 | 13 | RunAtLoad 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Courier; 3 | } 4 | 5 | #close { 6 | font-size: 1.5em; 7 | float:right; 8 | } -------------------------------------------------------------------------------- /app/js/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | var moment = require('moment'); 5 | var exec = require('child_process').exec; 6 | var util = require('util'); 7 | 8 | var sleepEvents = []; 9 | var wakeEvents = []; 10 | 11 | // gui elements 12 | var gui = require('nw.gui'); 13 | var tray; 14 | var trayMenu; 15 | var waitingTrayMenu; 16 | var trayMenuItems = {}; 17 | 18 | function main() { 19 | // Set up UI elements 20 | initTray(); 21 | refreshMenu(); 22 | 23 | // refresh every 5 mins 24 | // TODO: only refresh once we come out of sleep... 25 | setInterval(refreshMenu, 5 * 60 * 1000); 26 | } 27 | 28 | // TODO: try instead: defaults write loginwindow AutoLaunchedApplicationDictionary -array-add '{Path="/Applications/sleep.app";}' 29 | 30 | function setWaitingMenu(callback) { 31 | tray.menu = waitingTrayMenu; 32 | callback(); 33 | } 34 | 35 | function refreshMenu() { 36 | // once data is collected, populate the main window with results: 37 | async.parallel([setWaitingMenu, getSleepEvents, getWakeEvents], function() { 38 | // get rid of last event, which is always invalid: 39 | sleepEvents.pop(); 40 | wakeEvents.pop(); 41 | 42 | // 'Maintenance Sleep' events are useless... get rid of them: 43 | sleepEvents = sleepEvents.filter(function(k) { 44 | if (/Maintenance/.test(k.description)) { return false; } 45 | return true; 46 | }); 47 | 48 | var lastSlept = sleepEvents[sleepEvents.length - 1]; 49 | var lastWake = wakeEvents[wakeEvents.length - 1]; 50 | 51 | /* 52 | reasons: 53 | 54 | Entering Sleep state due to 'Software Sleep pid=68': Using AC (Charge:100%) 55 | */ 56 | 57 | // figure out reason for sleep: 58 | 59 | var reasons = { 60 | 'you closed the lid': /Clamshell Sleep/, 61 | 'your Mac was idle for a while': /Idle Sleep/, 62 | 'you made it sleep': /Software Sleep/ 63 | }; 64 | 65 | var reason = "uh, cause it was tired?"; 66 | 67 | Object.keys(reasons).forEach(function(r) { 68 | if (reasons[r].test(lastSlept.description)) { 69 | reason = r; 70 | } 71 | }); 72 | 73 | trayMenuItems.lastSlept.label = util.format("fell asleep: %s", lastSlept.timestamp.format('LT')); 74 | trayMenuItems.lastWoke.label = util.format("awoke: %s", lastWake.timestamp.format('LT')); 75 | trayMenuItems.duration.label = util.format("slept %s mins.", lastWake.timestamp.diff(lastSlept.timestamp, 'minutes')); 76 | trayMenuItems.reason.label = reason; 77 | 78 | tray.menu = trayMenu; 79 | }); 80 | } 81 | 82 | // initializes main UI 83 | function initTray() { 84 | // TODO: maybe have a giant display for the time that spans 5 menu items; make the time more prominent 85 | 86 | // add tray icon to statusbar: 87 | tray = new gui.Tray({ icon: 'assets/icon2.png'}); 88 | 89 | /* 90 | * create the main tray menu and its items 91 | */ 92 | 93 | trayMenu = new gui.Menu(); 94 | trayMenuItems.lastSlept = new gui.MenuItem({ type: 'normal', label: 'ugh', enabled: false }); 95 | trayMenuItems.lastWoke = new gui.MenuItem({ type: 'normal', label: 'ugh', enabled: false }); 96 | trayMenuItems.duration = new gui.MenuItem({ type: 'normal', label: 'ugh', enabled: false }); 97 | trayMenuItems.sep1 = new gui.MenuItem({type: 'separator'}); 98 | trayMenuItems.reasonLabel = new gui.MenuItem({type: 'normal', label: 'reason for sleep:', enabled: false}); 99 | trayMenuItems.reason = new gui.MenuItem({ type: 'normal', label: 'ugh', enabled: false }); 100 | trayMenuItems.sep2 = new gui.MenuItem({type: 'separator'}); 101 | trayMenuItems.startup = new gui.MenuItem({type: 'checkbox', label: 'run on login?', checked: false}); 102 | trayMenuItems.refresh = new gui.MenuItem({type: 'normal', label: 'refresh'}); 103 | trayMenuItems.about = new gui.MenuItem({type: 'normal', label: 'about'}); 104 | trayMenuItems.quit = new gui.MenuItem({ type: 'normal', label: 'quit', enabled: true }); 105 | 106 | /* 107 | * Logic + Event handling for MenuItems 108 | */ 109 | 110 | trayMenuItems.about.click = function() { 111 | var win = gui.Window.get(); 112 | win.show(); 113 | win.focus(); 114 | }; 115 | 116 | trayMenuItems.quit.click = function() { gui.App.quit(); } 117 | 118 | // check if runonlogin enabled set startup checkbox: 119 | checkIfRunOnLoginEnabled(function(err, enabled) { 120 | if (err) { console.error('exec error, ', err); } 121 | trayMenuItems.startup.checked = enabled; 122 | }); 123 | 124 | trayMenuItems.startup.click = function() { 125 | // TODO: tell user to move the app to /Applications if they want it to run on login 126 | // or, modify the plist file to point to the pwd 127 | checkIfRunOnLoginEnabled(function(error, enabled) { 128 | if (enabled) { 129 | disableRunOnLogin(function(err) { 130 | if (err === null) { trayMenuItems.startup.checked = false; } 131 | }); 132 | } else { 133 | enableRunOnLogin(function(err) { 134 | if (err === null) { trayMenuItems.startup.checked = true; } 135 | }); 136 | } 137 | }); 138 | }; 139 | 140 | trayMenuItems.refresh.click = function() { refreshMenu(); } 141 | 142 | // add all MenuItems to the trayMenu 143 | Object.keys(trayMenuItems).forEach(function(key) { 144 | trayMenu.append(trayMenuItems[key]); 145 | }); 146 | 147 | /* 148 | * Create Waiting Tray menu, which is displayed as we fetch data 149 | */ 150 | 151 | waitingTrayMenu = new gui.Menu(); 152 | waitingTrayMenu.append(new gui.MenuItem({type: 'normal', label: 'fetching...', enabled: false})); 153 | 154 | var quit = new gui.MenuItem({ type: 'normal', label: 'quit', enabled: true }); 155 | quit.click = function() { gui.App.quit(); } 156 | waitingTrayMenu.append(quit); 157 | 158 | // initial menu is waiting menu: 159 | tray.menu = waitingTrayMenu; 160 | } 161 | 162 | /* 163 | * pmset methods; used to fetch sleep data 164 | */ 165 | 166 | // TODO: wrap exec call to reduce code re-use involved with handling stderr, etc 167 | 168 | function getSleepEvents(callback) { 169 | exec('pmset -g log | grep "Entering Sleep"', 170 | function (error, stdout, stderr) { 171 | if (stderr) { console.error('stderr' + stderr); } 172 | if (error !== null) { console.error('exec error: ' + error); } 173 | 174 | sleepEvents = parsePMSETOutput(stdout); 175 | callback(); 176 | }); 177 | } 178 | 179 | function getWakeEvents(callback) { 180 | exec('pmset -g log | grep "Wake .* due to"', 181 | function(error, stdout, stderr) { 182 | if (stderr) { console.error('stderr' + stderr); } 183 | if (error !== null) { console.error('exec error: ' + error); } 184 | 185 | wakeEvents = parsePMSETOutput(stdout); 186 | callback(); 187 | }); 188 | } 189 | 190 | // utility function used to parse output from pmset 191 | function parsePMSETOutput(stdout) { 192 | var lineBuffer; 193 | var timestampBuffer; 194 | return stdout.split('\n').map(function(line) { 195 | lineBuffer = line.split('\t'); 196 | timestampBuffer = line.split(' '); 197 | return { 198 | timestamp: moment(new Date(timestampBuffer.slice(0, 3).join(' '))), 199 | description: lineBuffer[1], 200 | timeToSleep: lineBuffer[2] 201 | }; 202 | }); 203 | } 204 | 205 | /* 206 | * Launch at login logic: 207 | */ 208 | 209 | function checkIfRunOnLoginEnabled(callback) { 210 | // launchctl list | grep com.capablemonkey.sleepApp 211 | exec('launchctl list | grep com.capablemonkey.sleepApp', 212 | function(error, stdout, stderr) { 213 | if (stderr) { callback(stderr); } 214 | if (error !== null) { 215 | // if grep returns return code 1, our launchd job is unloaded 216 | if (error.code === 1) { return callback(null, false); } 217 | else { return callback(error); } 218 | } 219 | 220 | // if stdout not empty, launchd job is loaded; else it's unloaded 221 | return callback(null, stdout.length !== 0); 222 | } 223 | ); 224 | } 225 | 226 | function enableRunOnLogin(callback) { 227 | // cp com.capablemonkey.sleepApp.plist ~/Library/LaunchAgents/ 228 | // launchctl load ~/Library/LaunchAgents/com.capablemonkey.sleepApp.plist 229 | async.waterfall([ 230 | function(callback) { 231 | exec('cp ./com.capablemonkey.sleepApp.plist ~/Library/LaunchAgents/', 232 | function(error, stdout, stderr) { 233 | if (stderr) { callback(stderr); } 234 | if (error !== null) { callback(error); } 235 | 236 | return callback(null); 237 | } 238 | ); 239 | }, 240 | function(callback) { 241 | exec('launchctl load ~/Library/LaunchAgents/com.capablemonkey.sleepApp.plist', 242 | function(error, stdout, stderr) { 243 | if (stderr) { callback(stderr); } 244 | if (error !== null) { callback(error); } 245 | 246 | return callback(null); 247 | } 248 | ); 249 | } 250 | 251 | ], function(err, result) { 252 | if (err) { 253 | console.error("Exec error", err); 254 | return callback(err); 255 | } 256 | 257 | return callback(null); 258 | }); 259 | } 260 | 261 | function disableRunOnLogin(callback) { 262 | // launchctl unload ~/Library/LaunchAgents/com.capablemonkey.sleepApp 263 | exec('launchctl unload ~/Library/LaunchAgents/com.capablemonkey.sleepApp.plist', 264 | function(error, stdout, stderr) { 265 | if (stderr) { callback(stderr); } 266 | if (error !== null) { callback(error); } 267 | 268 | // if stdout empty, successfully unloaded 269 | return callback(stdout.length === 0 ? null : true); 270 | } 271 | ); 272 | } 273 | 274 | // used by about page to close/hide itself: 275 | function hideWindow() { 276 | var win = gui.Window.get(); 277 | win.hide(); 278 | } 279 | 280 | main(); 281 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sleep", 3 | "main": "views/index.html", 4 | "version": "0.0.1", 5 | "single-instance": true, 6 | "window": { 7 | "title": "sleep", 8 | "width": 500, 9 | "height": 200, 10 | "min_width": 500, 11 | "min_height": 200, 12 | "toolbar": false, 13 | "frame": false, 14 | "show_in_taskbar": false, 15 | "show": false 16 | }, 17 | "chromium-args": "--child-clean-exit", 18 | "dependencies": { 19 | "async": "^0.9.0", 20 | "moment": "^2.8.3", 21 | "moment-duration-format": "^1.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sleep 5 | 6 | 7 | 8 | 9 |

sleep

10 |

do you remember when you fell asleep?

11 | 12 |

see source on github

13 |

written by @capable_monkey

14 | 15 |
16 | close 17 |
18 | 19 | -------------------------------------------------------------------------------- /assets/dmgBackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capablemonkey/sleep/fb7d3c93c9579136a4f8f909aaec8ad9bc71c157/assets/dmgBackground.png -------------------------------------------------------------------------------- /assets/dmgBackground.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capablemonkey/sleep/fb7d3c93c9579136a4f8f909aaec8ad9bc71c157/assets/dmgBackground.psd -------------------------------------------------------------------------------- /assets/sleepicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capablemonkey/sleep/fb7d3c93c9579136a4f8f909aaec8ad9bc71c157/assets/sleepicon.icns -------------------------------------------------------------------------------- /assets/sleepicon.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/capablemonkey/sleep/fb7d3c93c9579136a4f8f909aaec8ad9bc71c157/assets/sleepicon.sketch -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package", 3 | "version": "0.0.0", 4 | "dependencies": {} 5 | } 6 | 7 | -------------------------------------------------------------------------------- /dmgConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "sleep", 3 | "icon": "./assets/sleepicon.icns", 4 | "background": "./assets/dmgBackground.png", 5 | "icon-size": 80, 6 | "contents": [ 7 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" }, 8 | { "x": 192, "y": 344, "type": "file", "path": "./webkitbuilds/sleep/osx64/sleep.app" } 9 | ] 10 | 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sleep", 3 | "version": "0.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "homepage": "https://github.com/capablemonkey/sleep", 7 | "bugs": "https://github.com/capablemonkey/sleep/issues", 8 | "author": { 9 | "name": "Gordon Zheng", 10 | "email": "", 11 | "url": "https://github.com/capablemonkey" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/capablemonkey/sleep.git" 16 | }, 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "appdmg": "^0.3.0", 20 | "grunt": "~0.4.5", 21 | "grunt-node-webkit-builder": "^1.0.2" 22 | }, 23 | "engines": { 24 | "node": ">=0.8.0" 25 | } 26 | } 27 | --------------------------------------------------------------------------------