├── .gitignore ├── LICENSE ├── README.md ├── components ├── consolebox.js └── index.js ├── dccc.gif ├── index.js ├── linq.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paul Taylor 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 | # docker-compose-command-center 2 | A blessed UI for docker-compose output 3 | 4 | ![docker-compose-command-center](https://raw.githubusercontent.com/trxcllnt/docker-compose-command-center/master/dccc.gif) 5 | 6 | ## Prerequisites 7 | Requires node >= v10.0.0. 8 | 9 | Use the [node version manager](https://github.com/creationix/nvm#install-script) to easily install and switch between multiple versions of node. 10 | 11 | ## Installation (optional) 12 | ```sh 13 | # use -g to install globally 14 | npm install docker-compose-command-center 15 | ``` 16 | 17 | ## Use 18 | ```sh 19 | # If installed globally 20 | $ docker-compose up | dccc 21 | # If installed locally 22 | $ docker-compose up | npx dccc 23 | # If using without installing first 24 | $ docker-compose up | npx docker-compose-command-center 25 | ``` 26 | 27 | ### Notes 28 | - Press `Q` or `Ctrl-C` to kill `dccc`. Press `Ctrl-C` again to terminate `docker-compose` 29 | - Terminal dependent: hold `shift` to select text with the mouse (e.g. in Ubuntu's default terminal) 30 | - Refer to the [`blessed`](https://github.com/chjj/blessed) package for general usability questions 31 | -------------------------------------------------------------------------------- /components/consolebox.js: -------------------------------------------------------------------------------- 1 | const { Subject } = require('../linq'); 2 | const Box = require('blessed/lib/widgets/box'); 3 | 4 | const defaultProps = { 5 | label: 'console', 6 | border: 'line', dockBorders: true, padding: 0, 7 | key: true, mouse: true, scrollable: true, alwaysScroll: true, 8 | scrollbar: { ch: ' ', track: { bg: '#666' }, style: { inverse: true } } 9 | }; 10 | 11 | class ConsoleBox extends Box { 12 | constructor({ idx, key, ...props } = {}) { 13 | super(Object.assign({}, defaultProps, props)); 14 | this.set('idx', idx); 15 | this.set('key', key); 16 | this.set('autoScroll', true); 17 | this.on('scroll', () => this.set('autoScroll', this.getScrollPerc() >= 100)); 18 | 19 | const renderTriggers = new Subject(); 20 | this.set('renderTriggers', renderTriggers); 21 | this.set('renderSubscription', renderTriggers 22 | .groupBy(({ trigger }) => trigger) 23 | .flatMap((xs) => xs.auditTime(25)) 24 | .subscribe(({ fn }) => fn(this))); 25 | } 26 | pushLine(...xs) { 27 | super.pushLine(...xs); 28 | if (this.get('autoScroll')) { 29 | this.get('renderTriggers').next({ 30 | trigger: 'pushLine', 31 | fn: () => { 32 | this.screen.render(); 33 | this.setScrollPerc(100); 34 | } 35 | }); 36 | } 37 | } 38 | } 39 | 40 | module.exports = ConsoleBox; 41 | -------------------------------------------------------------------------------- /components/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ConsoleBox: require('./consolebox') 3 | }; 4 | -------------------------------------------------------------------------------- /dccc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trxcllnt/docker-compose-command-center/aa3cfae56f5a39de1f37cfb236af1c16c4224858/dccc.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const ttys = require('ttys'); 4 | const blessed = require('blessed'); 5 | const { ConsoleBox } = require('./components'); 6 | const { Subject, Observable } = require('./linq'); 7 | const { splitAsAsyncIterable } = require('./linq'); 8 | const { asyncIteratorToObservable } = require('./linq') 9 | 10 | start(splitAsAsyncIterable(process.stdin)[Symbol.asyncIterator]()) 11 | .then(() => {}, exit); 12 | 13 | const stdinSymbol = '__stdin__'; 14 | 15 | const keyFromLine = firstCaptureGroup(/(\[.*?\|)/i); 16 | const labelFromLine = firstCaptureGroup(/(\[.*?)\s*\|/i); 17 | const contentFromLine = firstCaptureGroup(/\|(\[.*)/i); 18 | 19 | async function start(lines) { 20 | 21 | const firstLine = await lines.next(); 22 | if (firstLine.done) return exit(0); 23 | 24 | // parse out the container names from the first line. 25 | // unused for now, but might be useful use for the groupBy keys 26 | const names = getContainerNamesFromFirstLine(firstLine.value || ''); 27 | 28 | const screen = blessedScreen(); 29 | const grid = containerGrid({ parent: screen, top: '10%', left: '0%', width: '100%', height: '90%' }); 30 | const stdin = new ConsoleBox({ parent: screen, top: '90%', left: '0%', width: '100%', height: '10%', idx: -1, key: stdinSymbol }); 31 | const { menu, addMenuItem, selections } = containerMenu({ parent: screen, top: '0%', left: '0%', width: '100%', height: '10%' }); 32 | 33 | // Quit on Escape, q, or Control-C. 34 | screen.key(['escape', 'q', 'C-c'], () => exit()); 35 | 36 | const mapLines = (lines) => lines.key === stdinSymbol ? lines : lines.map(contentFromLine); 37 | const aggregateBoxes = ({ boxes }, group, idx) => ({ 38 | boxes, lines: mapLines(group), 39 | box: group.key === stdinSymbol ? stdin : boxes.set(group.key, 40 | addMenuItem(group.key) || new ConsoleBox({ 41 | parent: grid, 42 | idx, key: group.key, 43 | width: `50%`, height: '50%', 44 | label: labelFromLine(group.key), 45 | }) 46 | ).get(group.key) 47 | }); 48 | 49 | const updates = asyncIteratorToObservable(lines) 50 | .groupBy((line) => keyFromLine(line) || stdinSymbol) 51 | .scan(aggregateBoxes, { boxes: new Map() }) 52 | .multicast(() => new Subject(), (shared) => Observable.merge( 53 | 54 | shared.flatMap(({ box, lines }) => lines.do((line) => box.pushLine(line))), 55 | 56 | shared.switchMap(({ boxes }) => selections 57 | .startWith([...boxes.keys()].slice(0, 4)) 58 | .scan((xs, x) => [x, ...xs.filter(y => x !== y)].slice(0, 4)) 59 | .do((keys) => { 60 | grid.children.sort((a, b) => { 61 | if (a === b) return 0; 62 | let a2 = keys.indexOf(a.get('key')); 63 | let b2 = keys.indexOf(b.get('key')); 64 | if (~a2) return ~b2 ? a2 - b2 : -1; 65 | if (~b2) return ~a2 ? a2 - b2 : 1; 66 | return a.get('idx') - b.get('idx'); 67 | }).forEach((x) => x.hide()); 68 | 69 | keys.map((k) => boxes.get(k)).forEach((x) => x && x.show()); 70 | })) 71 | )); 72 | 73 | updates.startWith(null).subscribe(() => screen.render()); 74 | } 75 | 76 | const blessedScreen = (props = {}) => blessed.screen({ 77 | debug: true, 78 | key: true, mouse: true, sendFocus: true, 79 | scrollable: false, smartCSR: true, fullUnicode: true, 80 | dockBorders: true, autoPadding: true, ignoreDockContrast: true, 81 | input: ttys.stdin, output: ttys.stdout, cursor: { shape: 'line', color: 'white' }, 82 | ...props, 83 | }); 84 | 85 | const containerGrid = (props = {}) => blessed.layout({ 86 | layout: 'grid', padding: 0, 87 | top: '5%', left: '0%', width: '100%', height: `90%`, 88 | key: true, mouse: true, scrollable: false, alwaysScroll: false, 89 | ...props 90 | }); 91 | 92 | const containerMenu = (props = {}) => { 93 | 94 | let selections = new Subject(); 95 | let menu = blessed.listbar({ 96 | border: 'line', padding: 0, dockBorders: true, 97 | key: true, mouse: true, autoCommandKeys: true, 98 | ...props, 99 | }); 100 | 101 | let addMenuItem = (key) => menu.add({ 102 | key, 103 | text: labelFromLine(key), 104 | callback: () => selections.next(key) 105 | }); 106 | 107 | return { menu, addMenuItem, selections }; 108 | } 109 | 110 | // Parses the initial "Attaching to (dir_label_inst, ...)" line to get the container names 111 | function getContainerNamesFromFirstLine(line = '') { 112 | return line 113 | .substring(line.indexOf('Attaching to ') + 1).split(', ') 114 | // skip past the `dir_` prefix 115 | .map((name) => name.substring(name.indexOf('_') + 1)).filter(Boolean); 116 | } 117 | 118 | function exit(err) { (err && console.error(err)) || process.exit(err ? 1 : 0); } 119 | function firstCaptureGroup(exp) { let cap; return (line) => (cap = exp.exec(line)) && cap[1] || ''; }; 120 | -------------------------------------------------------------------------------- /linq.js: -------------------------------------------------------------------------------- 1 | const es = require('event-stream'); 2 | const { Subject } = require('rxjs'); 3 | const { Observable } = require('rxjs'); 4 | const { AsyncIterable } = require('ix'); 5 | 6 | require('rxjs/add/observable/from'); 7 | require('rxjs/add/observable/merge'); 8 | 9 | require('rxjs/add/operator/do'); 10 | require('rxjs/add/operator/map'); 11 | require('rxjs/add/operator/scan'); 12 | require('rxjs/add/operator/skip'); 13 | require('rxjs/add/operator/filter'); 14 | require('rxjs/add/operator/groupBy'); 15 | require('rxjs/add/operator/mergeMap'); 16 | require('rxjs/add/operator/auditTime'); 17 | require('rxjs/add/operator/switchMap'); 18 | require('rxjs/add/operator/multicast'); 19 | require('rxjs/add/operator/startWith'); 20 | require('rxjs/add/operator/combineLatest'); 21 | 22 | const splitAsAsyncIterable = (({ Readable }) => 23 | (source, ...xs) => AsyncIterable.from(new Readable({ 24 | objectMode: true, highWaterMark: 1 25 | }).wrap(source.pipe(es.split(...xs)))) 26 | )(require('stream')); 27 | 28 | const asyncIteratorToObservable = ((sym, fn) => (x) => { 29 | const itObs = AsyncIterable.from(x).toObservable(); 30 | return Observable.from((itObs[sym] = fn) && itObs); 31 | })(require('rxjs').observable, function() { return this; }); 32 | 33 | module.exports = { 34 | splitAsAsyncIterable, 35 | asyncIteratorToObservable, 36 | Subject, Observable, AsyncIterable, 37 | }; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docker-compose-command-center", 3 | "version": "0.0.1", 4 | "description": "A blessed UI for docker-compose output", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=10.0.0" 8 | }, 9 | "bin": { 10 | "dccc": "./index.js", 11 | "docker-compose-command-center": "./index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/trxcllnt/docker-compose-command-center.git" 16 | }, 17 | "files": [ 18 | "linq.js", 19 | "index.js", 20 | "LICENSE", 21 | "README.md", 22 | "components" 23 | ], 24 | "keywords": ["blessed", "docker", "docker-compose"], 25 | "author": "Paul Taylor ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/trxcllnt/docker-compose-command-center/issues" 29 | }, 30 | "homepage": "https://github.com/trxcllnt/docker-compose-command-center#readme", 31 | "dependencies": { 32 | "blessed": "0.1.81", 33 | "event-stream": "3.3.5", 34 | "ix": "2.3.5", 35 | "rxjs": "6.3.2", 36 | "rxjs-compat": "6.3.2", 37 | "ttys": "0.0.3" 38 | } 39 | } 40 | --------------------------------------------------------------------------------