├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── schedule.xml /.gitignore: -------------------------------------------------------------------------------- 1 | schedule.tmp 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 'lts/*' 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Thomas Watson Steen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 34c3 2 | 3 | Chaos Communication Congress 2017 Schedule on the Command Line 4 | 5 | [![Build status](https://travis-ci.org/watson/34c3.svg?branch=master)](https://travis-ci.org/watson/34c3) 6 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 7 | 8 | ![34c3-10fps](https://user-images.githubusercontent.com/10602/34446547-36ef5ed2-ecdc-11e7-8947-0fc3c41ea64b.gif) 9 | 10 | ## Install 11 | 12 | If you're running Node.js 8, you don't need to install anything as this 13 | tool can simply be run using: 14 | 15 | ``` 16 | npx 34c3 17 | ``` 18 | 19 | But if you wish, you can install it globally like in the old days: 20 | 21 | ``` 22 | npm install 34c3 -g 23 | ``` 24 | 25 | ## Usage 26 | 27 | Show the next talk today: 28 | 29 | ``` 30 | 34c3 31 | ``` 32 | 33 | Get help using the `--help` option: 34 | 35 | ``` 36 | Usage: 34c3 [options] 37 | 38 | --help, -h Show this help 39 | --version, -v Show version 40 | --update, -u Update schedule with new changes 41 | ``` 42 | 43 | ## License 44 | 45 | MIT 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict' 3 | 4 | process.title = require('./package').name 5 | 6 | var os = require('os') 7 | var fs = require('fs') 8 | var path = require('path') 9 | var download = require('download-to-file') 10 | var xml2js = require('xml2js') 11 | var nearest = require('nearest-date') 12 | var diffy = require('diffy')({fullscreen: true}) 13 | var input = require('diffy/input')() 14 | var trim = require('diffy/trim') 15 | var Grid = require('virtual-grid') 16 | var scrollable = require('scrollable-string') 17 | var Menu = require('menu-string') 18 | var wrap = require('wrap-ansi') 19 | var pad = require('fixed-width-string') 20 | var chalk = require('chalk') 21 | var argv = require('minimist')(process.argv.slice(2)) 22 | 23 | var URL = 'https://events.ccc.de/congress/2017/Fahrplan/schedule.xml' 24 | var CACHE = path.join(os.homedir(), '.34c3', 'schedule.xml') 25 | var activeCol = 0 26 | var grid, talk 27 | 28 | if (argv.help || argv.h) help() 29 | else if (argv.version || argv.v) version() 30 | else if (argv.update || argv.u) update() 31 | else run() 32 | 33 | function help () { 34 | console.log('Usage: 34c3 [options]') 35 | console.log() 36 | console.log('Options:') 37 | console.log(' --help, -h Show this help') 38 | console.log(' --version, -v Show version') 39 | console.log(' --update, -u Update schedule with new changes') 40 | } 41 | 42 | function version () { 43 | console.log(require('./package').version) 44 | } 45 | 46 | function update () { 47 | console.log('Downloading schedule to %s...', CACHE) 48 | download(URL, CACHE, function (err) { 49 | if (err) throw err 50 | run() 51 | }) 52 | } 53 | 54 | function run () { 55 | load(function (err, schedule) { 56 | if (err) throw err 57 | initUI(schedule) 58 | updateTopBar() 59 | }) 60 | } 61 | 62 | function load (cb) { 63 | fs.stat(CACHE, function (err) { 64 | var filepath = err ? path.join(__dirname, 'schedule.xml') : CACHE 65 | console.log('Schedule cache:', filepath) 66 | fs.readFile(filepath, function (err, xml) { 67 | if (err) return cb(err) 68 | // Unfortunately error handling is very bad in xml2js, so it will throw 69 | // if the xml is malformed instead of passing on the error to the 70 | // callback. Bug report: 71 | // https://github.com/Leonidas-from-XIV/node-xml2js/issues/408 72 | try { 73 | xml2js.parseString(xml, function (err, result) { 74 | if (err) return cb(err) 75 | cb(null, result.schedule) 76 | }) 77 | } catch (e) { 78 | console.error('Could not parse conference schedule - malformed XML!') 79 | console.error('Run "34c3 --update" to re-download the schedule') 80 | process.exit(1) 81 | } 82 | }) 83 | }) 84 | } 85 | 86 | function initUI (schedule) { 87 | // setup virtual grid 88 | grid = new Grid([ 89 | [{height: 2, wrap: false, padding: [0, 1, 0, 0]}, {height: 2, wrap: false, padding: [0, 0, 0, 1]}], 90 | [{padding: [0, 1, 0, 0], wrap: false}, {padding: [0, 0, 0, 1], wrap: false}] 91 | ]) 92 | 93 | grid.on('update', function () { 94 | diffy.render() 95 | }) 96 | 97 | // setup screen 98 | diffy.on('resize', function () { 99 | grid.resize(diffy.width, diffy.height) 100 | }) 101 | 102 | diffy.render(function () { 103 | return grid.toString() 104 | }) 105 | 106 | // generate menu 107 | var menu = initMenu(schedule) 108 | 109 | menu.on('update', function () { 110 | grid.update(1, 0, menu.toString()) 111 | }) 112 | 113 | menu.select(nearest(menu.items.map(function (item) { 114 | return item.date 115 | }))) 116 | 117 | // listen for keybord input 118 | input.on('keypress', function (ch, key) { 119 | if (ch === 'k') goUp() 120 | if (ch === 'j') goDown() 121 | if (ch === 'q') process.exit() 122 | }) 123 | input.on('up', goUp) 124 | input.on('down', goDown) 125 | 126 | input.on('left', function () { 127 | activeCol = 0 128 | updateTopBar() 129 | }) 130 | 131 | input.on('right', function () { 132 | activeCol = 1 133 | updateTopBar() 134 | }) 135 | 136 | input.on('tab', function () { 137 | activeCol = activeCol === 0 ? 1 : 0 138 | updateTopBar() 139 | }) 140 | 141 | input.on('enter', function () { 142 | var item = menu.selected() 143 | talk = scrollable(renderTalk(item.event), { 144 | maxHeight: grid.cellAt(1, 1).height 145 | }) 146 | talk.on('update', updateTalk) 147 | updateTalk() 148 | }) 149 | 150 | function updateTalk () { 151 | updateTopBar() 152 | grid.update(1, 1, talk.toString()) 153 | } 154 | 155 | function goUp () { 156 | if (activeCol === 0) menu.up() 157 | else if (talk) talk.up() 158 | } 159 | 160 | function goDown () { 161 | if (activeCol === 0) menu.down() 162 | else if (talk) talk.down() 163 | } 164 | } 165 | 166 | function initMenu (schedule) { 167 | var items = [] 168 | 169 | schedule.day.forEach(function (day, index) { 170 | items.push({text: 'Day ' + (index + 1), separator: true}) 171 | 172 | var events = [] 173 | 174 | day.room.forEach(function (room, roomIndex) { 175 | if (!room.event) return 176 | room.event.forEach(function (event, index) { 177 | events.push({ 178 | text: ` ${event.start}: ${event.title[0]} (${event.room}, ${event.language[0].toUpperCase()})`, 179 | event: event, 180 | date: (new Date(event.date[0])).getTime() 181 | }) 182 | }) 183 | }) 184 | 185 | items = items.concat(events.sort(function (a, b) { 186 | return a.date - b.date 187 | })) 188 | }) 189 | 190 | var maxWidth = items.reduce(function (max, item) { 191 | return item.text.length > max ? item.text.length : max 192 | }, 0) 193 | var height = grid.cellAt(1, 0).height 194 | 195 | var menu = new Menu({ 196 | items: items, 197 | render: function (item, selected) { 198 | return selected ? chalk.inverse(pad(item.text, maxWidth)) : item.text 199 | }, 200 | height: height 201 | }) 202 | 203 | return menu 204 | } 205 | 206 | function renderTopBar (text, active) { 207 | return active 208 | ? chalk.black.bgGreen(pad(text, process.stdout.columns)) 209 | : text 210 | } 211 | 212 | function updateTopBar () { 213 | grid.update(0, 0, renderTopBar(` 34c3 schedule - ${chalk.bold('enter:')} select, ${chalk.bold('tab:')} switch column`, activeCol === 0)) 214 | grid.update(0, 1, renderTopBar(talk ? `Scroll: ${Math.round(talk.pct() * 100)}%` : '', activeCol === 1)) 215 | } 216 | 217 | function renderTalk (talk) { 218 | var cell = grid.cellAt(1, 1) 219 | var width = cell.width - cell.padding[1] - cell.padding[3] 220 | 221 | var body = trim(` 222 | Room: ${talk.room[0]} 223 | Start: ${talk.start[0]} 224 | Duration: ${talk.duration[0]} 225 | 226 | ${chalk.black.bgYellow('** Title **')} 227 | ${talk.title[0]} 228 | `) 229 | 230 | if (talk.subtitle[0]) { 231 | body = trim(` 232 | ${body} 233 | ${chalk.black.bgYellow('** Subtitle **')} 234 | ${talk.subtitle[0]} 235 | `) 236 | } 237 | 238 | body = trim(` 239 | ${body} 240 | ${chalk.black.bgYellow('** Abstract **')} 241 | ${talk.abstract[0]} 242 | `) 243 | 244 | if (talk.description[0]) { 245 | body = trim(` 246 | ${body} 247 | ${chalk.black.bgYellow('** Description **')} 248 | ${talk.description[0]} 249 | `) 250 | } 251 | 252 | return wrap(body, width) 253 | } 254 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "34c3", 3 | "version": "2.1.2", 4 | "description": "Chaos Communication Congress 2017 Schedule on the Command Line", 5 | "bin": "index.js", 6 | "scripts": { 7 | "test": "standard", 8 | "update": "curl https://events.ccc.de/congress/2017/Fahrplan/schedule.xml > schedule.tmp && mv schedule.tmp schedule.xml" 9 | }, 10 | "keywords": [ 11 | "ccc", 12 | "34c3", 13 | "program", 14 | "schedule", 15 | "event", 16 | "events", 17 | "talks", 18 | "conf", 19 | "conference" 20 | ], 21 | "author": "Thomas Watson (https://twitter.com/wa7son)", 22 | "license": "MIT", 23 | "dependencies": { 24 | "chalk": "^2.3.0", 25 | "diffy": "^1.3.0", 26 | "download-to-file": "^2.1.0", 27 | "fixed-width-string": "^1.0.0", 28 | "inquirer": "^2.0.0", 29 | "menu-string": "^1.2.0", 30 | "minimist": "^1.2.0", 31 | "nearest-date": "^1.0.1", 32 | "scrollable-string": "^1.4.0", 33 | "virtual-grid": "^2.0.0", 34 | "wrap-ansi": "^2.1.0", 35 | "xml2js": "^0.4.19" 36 | }, 37 | "devDependencies": { 38 | "standard": "^10.0.3" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/watson/34c3.git" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/watson/34c3/issues" 46 | }, 47 | "homepage": "https://github.com/watson/34c3#readme", 48 | "coordinates": [ 49 | 51.3978265, 50 | 12.3975969 51 | ] 52 | } 53 | --------------------------------------------------------------------------------