Electron: From Beginner to Pro
-
Upload
chris-griffith -
Category
Technology
-
view
69 -
download
3
Transcript of Electron: From Beginner to Pro
![Page 1: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/1.jpg)
ELECTRON:BEGINNER TO PRO
BUILD CROSS PLATFORM DESKTOP APPS USINGGITHUB’S ELECTRON
@chrisgriffith
![Page 2: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/2.jpg)
![Page 3: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/3.jpg)
WHAT IS ELECTRON?Released in July 2013 by Cheng ZhaoFoundation for GitHub Atom Editor
![Page 4: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/4.jpg)
![Page 5: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/5.jpg)
![Page 7: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/7.jpg)
ELECTRON'S FEATURES
Automatic updatesNative menus & notificationsCrash reportingDebugging & profilingWindows installersjust a partial list…
![Page 8: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/8.jpg)
PLATFORM SUPPORT
![Page 9: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/9.jpg)
HOW DOES ELECTRON WORK?
![Page 10: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/10.jpg)
UI?
![Page 11: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/11.jpg)
INSTALLING ELECTRONnpm install -g electron
![Page 12: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/12.jpg)
QUICK STARTgit clone https://github.com/electron/electron-quick-start quick-start
cd quick-start
git init
npm start
![Page 13: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/13.jpg)
![Page 14: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/14.jpg)
DEFAULT FILE STRUTURE index.html LICENSE.md main.js package.json README.md renderer.js
![Page 15: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/15.jpg)
REAL WORLD FILE STRUTURE � app main.js splash.html splash.png � www � build � dist � node_modules package-lock.json package.json
![Page 16: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/16.jpg)
MAIN.JS const electron = require('electron') // Module to control application life. const app = electron.app // Module to create native browser window. const BrowserWindow = electron.BrowserWindow const path = require('path') const url = require('url')
let mainWindow
![Page 17: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/17.jpg)
CREATEWINDOW function createWindow () { // Create the browser window. mainWindow = new BrowserWindow({width: 800, height: 600}) // and load the index.html of the app. mainWindow.loadURL(url.format({ pathname: path.join(__dirname, 'index.html'), protocol: 'file:', slashes: true })) …
![Page 18: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/18.jpg)
CREATEWINDOW … // Open the DevTools. mainWindow.webContents.openDevTools()
// Emitted when the window is closed. mainWindow.on('closed', function () { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. mainWindow = null }) }
![Page 19: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/19.jpg)
MAIN.JS // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on('ready', createWindow)
![Page 20: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/20.jpg)
INDEX.HTML… <h1>Hello World!</h1> <!-- All of the Node.js APIs are available in this renderer process. --> We are using Node.js <script>document.write(process.versions.node) < /script>, Chromium <script>document.write(process.versions.chrome) < /script>, and Electron <script>document.write(process.versions.electron) < /script>. …
![Page 21: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/21.jpg)
INDEX.HTML… <script> // You can also require other files to run in this process require('./renderer.js') < /script>
![Page 22: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/22.jpg)
WINDOWS
![Page 23: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/23.jpg)
BROWSERWINDOW mainWindow = new BrowserWindow({ show: false, backgroundColor: "#FFF", width: 800, height: 600, minWidth: 800, maxWidth: 1024, minHeight: 600, maxHeight: 768, resizable: true, movable: true })
![Page 24: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/24.jpg)
BROWSERWINDOWADDTIONAL PROPERTIES
mainWindow = new BrowserWindow({ show: false, backgroundColor: "#FFF", width: 800, height: 600, minWidth: 800, maxWidth: 1024, minHeight: 600, maxHeight: 768, resizable: true, movable: true, alwaysOnTop: false, title: "Goodbye, Moon?" })
![Page 25: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/25.jpg)
FRAMELESS WINDOWS mainWindow = new BrowserWindow({ show: false, backgroundColor: '#FFF', transparent: true, width: 800, height: 600, minWidth: 800, maxWidth: 1024, minHeight: 600, maxHeight: 768, resizable: true, movable: true, // frame: false, titleBarStyle: 'hidden' //hidden-inset // title: "Goodbye, Moon?", })
![Page 26: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/26.jpg)
TRANSPARENT WINDOWS mainWindow = new BrowserWindow({ show: false, // backgroundColor: '#FFF', transparent: true, width: 800, height: 600, minWidth: 800, maxWidth: 1024, minHeight: 600, maxHeight: 768, resizable: true, movable: true, frame: false, transparent: true })
![Page 27: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/27.jpg)
APPLICATION MENU
![Page 28: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/28.jpg)
MENUS const Menu = electron.Menu
app.on('ready', function () { const menu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(menu) createWindow() })
![Page 29: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/29.jpg)
MENU TEMPLATES let template = [{ label: 'Menu 1', submenu: [{ label: 'Menu item 1' }] }, { label: 'Menu 2', submenu: [{ label: 'Another Menu item' }, { label: 'One More Menu Item' }] }]
![Page 30: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/30.jpg)
MENU ACCELERATORS & ROLES let template = [{ label: 'Edit App', submenu: [{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, { type: 'separator' }, { ...
![Page 31: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/31.jpg)
![Page 32: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/32.jpg)
![Page 33: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/33.jpg)
CONTEXTUAL MENUS
![Page 34: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/34.jpg)
CONTEXTUAL MENU -RENDERER.JS
const { remote } = require('electron') const { Menu } = remote const myContextMenu = Menu.buildFromTemplate ([ { label: 'Cut', role: 'cut' }, { label: 'Copy', role: 'copy' }, { label: 'Paste', role: 'paste' }, { label: 'Select All', role: 'selectall' }, { type: 'separator' }, { label: 'Custom', click() { console.log('Custom Menu') } } ])
window.addEventListener('contextmenu', (event) => { event.preventDefault() myContextMenu.popup() })
![Page 35: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/35.jpg)
INTER-PROCESSCOMMUNICATION
![Page 36: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/36.jpg)
INTER-PROCESS COMMUNICATION
![Page 37: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/37.jpg)
IPC SYNC const ipc = require('electron').ipcRenderer syncMsgBtn.addEventListener('click', function () { const reply = ipc.sendSync('synchronous-message', 'Mr. Watson, come here })
const ipc = electron.ipcMain ipc.on('synchronous-message', function (event, arg) { event.returnValue = 'I heard you!' })
![Page 38: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/38.jpg)
IPC ASYNC const ipc = require('electron').ipcRenderer asyncMsgBtn.addEventListener('click', function () { ipc.send('asynchronous-message', ''That's one small step for man') }) ipc.on('asynchronous-reply', function (event, arg) { const message = `Asynchronous message reply: ${arg}` document.getElementById('asyncReply').innerHTML = message })
const ipc = electron.ipcMain ipc.on('asynchronous-message', function (event, arg) { if (arg === 'That’s one small step for man') { event.sender.send('asynchronous-reply', ', one giant leap for mankind. } })
![Page 39: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/39.jpg)
NATIVE DIALOGS
![Page 40: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/40.jpg)
DIALOG TYPESFile OpenFile SaveMessage BoxError Box
![Page 41: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/41.jpg)
FILE OPEN const dialog = electron.dialog
ipc.on('open-directory-dialog', function (event) { dialog.showOpenDialog({ properties: ['openDirectory'] }, function (files) { if (files) event.sender.send('selectedItem, files) }) })
![Page 42: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/42.jpg)
![Page 43: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/43.jpg)
![Page 44: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/44.jpg)
DIALOG PROPERTIESopenFileopenDirectorymultiSelectionscreateDirectoryshowHiddenFilespromptToCreate (Windows Only)
![Page 45: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/45.jpg)
![Page 46: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/46.jpg)
![Page 47: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/47.jpg)
MESSAGE BOXES
![Page 48: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/48.jpg)
MESSAGE BOX dialog.showMessageBox({ type: info, buttons: ['Save', 'Cancel', 'Don\'t Save'], defaultId: 0, cancelId: 1, title: 'Save Score', message: 'Backup your score file?', detail: 'Message detail' })
![Page 49: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/49.jpg)
DIALOG TYPESinfoerrorquestionnone
![Page 50: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/50.jpg)
![Page 51: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/51.jpg)
CUSTOM ICONS const nativeImage = electron.nativeImage
let warningIcon= nativeImage.createFromPath('images/warning.png')
dialog.showMessageBox({ type: info, buttons: ['Save', 'Cancel', 'Don\'t Save'], defaultId: 0, cancelId: 1, title: 'Save Score', message: 'Backup your score file?', detail: 'Message detail', icon: warningIcon })
![Page 52: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/52.jpg)
WEBCONTENTS EVENTSbefore-input-eventcertificate-errorcontext-menucrashedcursor-changeddestroyeddevtools-closeddevtools-focuseddevtools-openeddevtools-reload-pagedid-change-theme-colordid-fail-loaddid-finish-loaddid-frame-finish-loaddid-get-response-detailsdid-get-redirect-requestdid-navigatedid-navigate-in-page
did-start-loadingdid-stop-loadingdom-readyfound-in-pageloginmedia-started-playingmedia-pausednew-windowpage-favicon-updatedpaintplugin-crashedselect-client-certificateselect-bluetooth-deviceupdate-target-urlwill-attach-webviewwill-navigatewill-prevent-unload
![Page 53: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/53.jpg)
DEBUGGING YOUR ELECTRONAPPLICATION
Chromium’s Dev Tools
![Page 54: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/54.jpg)
DEBUGGING RENDERERPROCESS
Chromium’s Dev Tools
![Page 55: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/55.jpg)
DEBUGGING MAIN PROCESSVS Code's Toolsnode-inspector
![Page 56: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/56.jpg)
DEVTRONAn Electron DevTools extension to help you inspect,
monitor, and debug your app.
![Page 57: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/57.jpg)
TESTING WITH SPECTRON
![Page 58: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/58.jpg)
INSTALLATION npm install --save-dev spectron
![Page 59: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/59.jpg)
TEST SCRIPT npm install electron-builder --save-dev
![Page 60: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/60.jpg)
BUILDING YOUR APPLICATION
![Page 61: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/61.jpg)
INSTALLATION npm install electron-builder --save-dev
![Page 62: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/62.jpg)
ADJUSTING YOUR BUILD DIRECTORIES
![Page 63: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/63.jpg)
Build Platforms Descriptions--mac, -m, -o, --macos Build for macOS--win, -w, --windows Build for Windows--linux, -l Build for LinuxBuild Architectures Descriptions--x64 Build for x64--ia32 Build for ia32
![Page 64: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/64.jpg)
UPDATING THE PACKAGE.JSONFILE
"scripts": { "start": "electron .", "dist": "build -mwl --x64 --ia32" }
![Page 65: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/65.jpg)
UPDATING THE PACKAGE.JSONFILE
"build": { "appId": "com.your-company.electron-app-name", "copyright": "Copyright © 2017 YOUR-NAME", "productName": "My Electron App", "electronVersion": "1.4.1", "mac": { "category": "public.app-category.developer-tools" }, "win": { "target": [ "nsis" ] }, "linux": { "target": [ "AppImage", "deb"] } }
![Page 66: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/66.jpg)
UPDATING THE PACKAGE.JSONFILE
"main": "./app/main.js"
![Page 67: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/67.jpg)
APP ICONS16px32px128px256px (OS X 10.5+)512px (OS X 10.5+)1024px (OS X 10.7+)
![Page 68: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/68.jpg)
CONFIGURING THE MACOS DMG
![Page 69: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/69.jpg)
CONFIGURING THE WINDOWS INSTALLER
![Page 70: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/70.jpg)
AUTO-UPDATING
![Page 71: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/71.jpg)
BUILT-INPlatform Update MethodmacOS Squirrel.MacWindows SquirrelLinux None
![Page 72: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/72.jpg)
ELECTRON-UPDATERInstall electron-updater as an app dependency.
Use autoUpdater from electron-updater instead ofelectron
npm i electron-updater
import { autoUpdater } from "electron-updater"
![Page 73: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/73.jpg)
UPDATE EVENTS autoUpdater.on('checking-for-update', () => { })
autoUpdater.on('update-available', () => { })
autoUpdater.on('update-not-available', () => {})
autoUpdater.on('update-downloaded', () => {})
autoUpdater.on('error', (event, error) => {})
![Page 74: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/74.jpg)
ELECTRON FORGEA complete tool for building modern Electron
applications.
https://github.com/electron-userland/electron-forge
![Page 75: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/75.jpg)
![Page 76: Electron: From Beginner to Pro](https://reader035.fdocuments.in/reader035/viewer/2022062302/5a647da77f8b9a2c568b4d97/html5/thumbnails/76.jpg)
THANK YOU!Chris Griffith
San Diego, CA
@chrisgriffith
http://chrisgriffith.wordpress.com
https://github.com/chrisgriffith