├── .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 | [](https://gitter.im/capablemonkey/sleep?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 |
5 | 
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 |
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 |
--------------------------------------------------------------------------------