├── .gitignore ├── chart.js ├── cli.js ├── index.js ├── package.json ├── readme.md ├── screen.js ├── screenshot.png └── theme.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /chart.js: -------------------------------------------------------------------------------- 1 | var Drawille = require('drawille') 2 | var prettybytes = require('pretty-bytes') 3 | 4 | module.exports.create = function(box) { 5 | var width = (box.width - 3) * 2 6 | var height = (box.height - 2) * 4 7 | var canvas = new Drawille(width, height) 8 | var values = [] 9 | var chart = { 10 | chart: canvas, 11 | values: values, 12 | width: width, 13 | height: height, 14 | ready: false, 15 | min: 0, 16 | max: 0 17 | } 18 | 19 | return chart 20 | } 21 | 22 | module.exports.resize = function(chart, box) { 23 | var width = (box.width - 3) * 2 24 | var height = (box.height - 2) * 4 25 | chart.chart = new Drawille(width, height) 26 | chart.width = width 27 | chart.height = height 28 | } 29 | 30 | module.exports.draw = function(chart, position) { 31 | var c = chart.chart 32 | c.clear() 33 | 34 | if (!chart.ready) { 35 | return false 36 | } 37 | 38 | var dataPointsToKeep = 1000 39 | 40 | chart.values[position] = chart.value 41 | 42 | if (position > dataPointsToKeep) { 43 | delete chart.values[position - dataPointsToKeep] 44 | } 45 | 46 | chart.min = 0 47 | chart.max = 0 // 1kb 48 | chart.average = 0 49 | 50 | var avgCount = 0 51 | // do first pass to determine min/max 52 | for (var i = 0; i < chart.width; i++) { 53 | var rawval = chart.values[chart.values.length - i] 54 | if (rawval > 0) { 55 | avgCount++ 56 | chart.average += rawval 57 | if (rawval < chart.min) chart.min = rawval 58 | if (rawval > chart.max) chart.max = rawval 59 | } 60 | } 61 | 62 | chart.average = ~~(chart.average / avgCount) 63 | 64 | for (var pos in chart.values) { 65 | var p = parseInt(pos, 10) + (chart.width - chart.values.length) 66 | var pval = computeValue(chart.values[pos]) 67 | 68 | if (p > 0 && chart.values[pos] > 0) { 69 | c.set(p, chart.height - 1) 70 | } 71 | 72 | for (var y = 0; y < pval; y++) { 73 | c.set(p, chart.height - y) 74 | } 75 | } 76 | 77 | // Add percentage to top right of the chart by splicing it into the braille data 78 | var textOutput = c.frame().split("\n") 79 | 80 | var currentVal = prettybytes(chart.values[chart.values.length - 1]) 81 | var msg = "current: " + currentVal + ", avg: " + prettybytes(chart.average) + ', max: ' + prettybytes(chart.max) 82 | textOutput[0] = textOutput[0].slice(0, textOutput[0].length - msg.length) + msg 83 | 84 | return textOutput.join("\n") 85 | 86 | function computeValue(input) { 87 | var max = chart.max 88 | if (max < 1000) max = 1000 89 | return ~~scale(input, chart.min, max, 0, chart.height) 90 | } 91 | } 92 | 93 | function scale( x, fromLow, fromHigh, toLow, toHigh ) { 94 | return ( x - fromLow ) * ( toHigh - toLow ) / ( fromHigh - fromLow ) + toLow 95 | } 96 | 97 | function stringRepeat(string, num) { 98 | if (num < 0) return '' 99 | return new Array(num + 1).join(string) 100 | } 101 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var Datop = require('./') 4 | var theme = require('./theme.json') 5 | var host = process.argv[2] 6 | 7 | var datop = Datop(host, theme) 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var createScreen = require('./screen.js') 2 | var request = require('request') 3 | var ldj = require('ldjson-stream') 4 | var through = require('through2') 5 | 6 | module.exports = function(host, theme) { 7 | if (!host) host = "http://localhost:6461" 8 | else host = normalizeURL(host) 9 | 10 | var api = host + '/api/stats' 11 | var req = request(api) 12 | 13 | req.on('error', function(e) { 14 | console.error('Connection error!', api, e.message) 15 | }) 16 | 17 | req.on('response', function() { 18 | var screen = createScreen(host, theme) 19 | 20 | var read = screen.createChart({ 21 | height: "48%", 22 | width: "100%", 23 | title: "Read" 24 | }) 25 | 26 | var written = screen.createChart({ 27 | height: "49%", 28 | width: "100%", 29 | top: '52%', 30 | title: "Written" 31 | }) 32 | 33 | var parser = ldj.parse() 34 | 35 | // number of initial stats to skip (heisenberg - requesting stats causes stats to change) 36 | var skip = 2 37 | 38 | var filter = through.obj(function(obj, enc, next) { 39 | if (skip !== 0) { 40 | skip-- 41 | return next() 42 | } 43 | written.write(obj.http.written + obj.level.written + obj.blobs.written) 44 | read.write(obj.http.read + obj.level.read + obj.blobs.read) 45 | next() 46 | }) 47 | req.pipe(parser).pipe(filter) 48 | 49 | }) 50 | } 51 | 52 | function normalizeURL(urlString) { 53 | // strip trailing / 54 | if (urlString[urlString.length - 1] === '/') urlString = urlString.slice(0, urlString.length - 1) 55 | 56 | if (!urlString.match(/^http:\/\//)) urlString = 'http://' + urlString 57 | 58 | return urlString 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datop", 3 | "version": "1.0.4", 4 | "bin": { 5 | "datop": "cli.js" 6 | }, 7 | "main": "index.js", 8 | "description": "top for dat - cli performance dashboard", 9 | "author": "max ogden", 10 | "license": "BSD", 11 | "dependencies": { 12 | "blessed": "0.0.37", 13 | "drawille": "^0.1.1", 14 | "ldjson-stream": "^1.1.0", 15 | "pretty-bytes": "^0.1.2", 16 | "request": "^2.38.0", 17 | "through2": "^0.5.1", 18 | "xtend": "^3.0.0" 19 | }, 20 | "devDependencies": {}, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/maxogden/datop.git" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/maxogden/datop/issues" 27 | }, 28 | "homepage": "https://github.com/maxogden/datop" 29 | } 30 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # datop 2 | 3 | `top` for [dat](https://github.com/maxogden/dat) 4 | 5 | ![screenshot](screenshot.png) 6 | 7 | [![NPM](https://nodei.co/npm/datop.png?global=true)](https://nodei.co/npm/datop/) 8 | 9 | ``` 10 | npm install datop -g 11 | datop http://mydat 12 | ``` 13 | 14 | if you just run `datop` it will try and connect to `localhost:6461` (the default dat port) 15 | 16 | idea/design inspired by [vtop](https://www.npmjs.org/package/vtop) 17 | -------------------------------------------------------------------------------- /screen.js: -------------------------------------------------------------------------------- 1 | var blessed = require('blessed') 2 | var through = require('through2') 3 | var xtend = require('xtend') 4 | var dataChart = require('./chart.js') 5 | 6 | module.exports = Screen 7 | 8 | function Screen(host, theme) { 9 | if (!(this instanceof Screen)) return new Screen(host, theme) 10 | var self = this 11 | var opts = {} 12 | if (process.env.DEBUG) opts.log = './datop.log' 13 | var program = blessed.program(opts) 14 | 15 | process.on('SIGINT', function() { 16 | self.kill() 17 | }) 18 | 19 | this.program = program 20 | this.host = host 21 | this.screen = blessed.screen() 22 | 23 | this.headerText = ' {bold}datop{/bold} - ' + host 24 | var header = blessed.text({ 25 | top: 'top', 26 | left: 'left', 27 | width: this.headerText.length + 10, 28 | height: '1', 29 | fg: theme.title.fg, 30 | content: this.headerText, 31 | tags: true 32 | }) 33 | 34 | this.screen.append(header) 35 | 36 | this.screen.on('resize', function() { 37 | for (var i = 0; i < self.renderList.length; i++) { 38 | var item = self.renderList[i] 39 | if (item.chart) { 40 | dataChart.resize(item.chart, item.box) 41 | updateHeader(item.chart) 42 | } 43 | } 44 | }) 45 | 46 | this.theme = theme 47 | this.renderList = [] 48 | 49 | setInterval(draw, 1000) 50 | 51 | function draw() { 52 | if (self.renderList.length === 0) return 53 | var updatedHeader = self.headerText 54 | for (var i = 0; i < self.renderList.length; i++) { 55 | var item = self.renderList[i] 56 | item.render() 57 | if (item.chart) updateHeader(item.chart) 58 | } 59 | header.content = updatedHeader 60 | self.screen.render() 61 | } 62 | 63 | function updateHeader(chart) { 64 | var min = ~~(chart.width / 60) 65 | var sec = chart.width % 60 66 | self.headerText = ' {bold}datop{/bold} - ' + self.host + ' - showing ' + min + 'm' + sec + 's' 67 | } 68 | } 69 | 70 | Screen.prototype.kill = function() { 71 | this.program.clear() 72 | this.program.disableMouse() 73 | this.program.showCursor() 74 | this.program.normalBuffer() 75 | this.process.exit(0) 76 | } 77 | 78 | Screen.prototype.createBox = function(opts) { 79 | var theme = this.theme 80 | var screen = this.screen 81 | if (!opts) opts = {} 82 | 83 | var defaults = { 84 | top: 1, 85 | left: 'left', 86 | width: '100%', 87 | height: '99%', 88 | content: '', 89 | fg: theme.chart.fg, 90 | tags: true, 91 | border: theme.chart.border 92 | } 93 | 94 | var box = blessed.box(xtend(defaults, opts)) 95 | 96 | screen.append(box) 97 | screen.render() 98 | 99 | if (opts.title) box.setLabel(opts.title) 100 | 101 | return box 102 | } 103 | 104 | Screen.prototype.createChart = function(opts) { 105 | var self = this 106 | var box = this.createBox(opts) 107 | var chart = dataChart.create(box) 108 | var position = 0 109 | 110 | var stream = through.obj( 111 | function write(obj, enc, next) { 112 | chart.ready = true 113 | chart.value = obj 114 | next() 115 | }, 116 | function end() { 117 | chart.ready = false 118 | } 119 | ) 120 | 121 | stream.chart = chart 122 | stream.box = box 123 | 124 | stream.render = function() { 125 | if (process.env.DEBUG) self.program.log([chart.min, chart.max, chart.average, chart.values[chart.values.length - 1]]) 126 | position++ 127 | box.setContent(dataChart.draw(chart, position)) 128 | } 129 | 130 | this.renderList.push(stream) 131 | 132 | return stream 133 | } 134 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max-mapper/datop/98b9eebef32ca4bf074d090db49aa21a71237881/screenshot.png -------------------------------------------------------------------------------- /theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Becca", 3 | "author": "James Hall", 4 | "description": "In memory of Becca #663399. This is as close as we can get to that color in xterm", 5 | "title": { 6 | "fg": "#800080" 7 | }, 8 | "chart": { 9 | "fg": "#800080", 10 | "border": { 11 | "type": "line", 12 | "fg": "#800080" 13 | } 14 | }, 15 | "table": { 16 | "fg": "white", 17 | "items": { 18 | "selected": { 19 | "bg": "#800080", 20 | "fg": "bg" 21 | }, 22 | "item": { 23 | "fg": "fg", 24 | "bg": "bg" 25 | } 26 | }, 27 | "border": { 28 | "type": "line", 29 | "fg": "#800080" 30 | } 31 | }, 32 | "footer": { 33 | "fg": "fg" 34 | } 35 | } --------------------------------------------------------------------------------