├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── lib ├── editor-stats-view.coffee ├── editor-stats.coffee └── stats-tracker.coffee ├── menus └── editor-stats.cson ├── package.json ├── spec └── editor-stats-spec.coffee └── styles └── editor-stats.less /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: 5 | on_success: never 6 | on_failure: change 7 | 8 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [Atom contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md) 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Atom and all repositories under Atom will be archived on December 15, 2022. Learn more in our [official announcement](https://github.blog/2022-06-08-sunsetting-atom/) 2 | # Editor Stats package [![Build Status](https://travis-ci.org/atom/editor-stats.svg?branch=master)](https://travis-ci.org/atom/editor-stats) 3 | 4 | View a graph of your mouse and keyboard activity for the last 6 hours. 5 | 6 | The blue bar indicates the time of greatest activity. 7 | 8 | You can toggle it using the `Editor Stats: Toggle` command from the command palette. 9 | 10 | ![](https://f.cloud.github.com/assets/671378/2262223/843b1172-9e57-11e3-9c60-8d28d542f39c.png) 11 | -------------------------------------------------------------------------------- /lib/editor-stats-view.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore-plus' 2 | {$, ScrollView} = require 'atom-space-pen-views' 3 | d3 = require 'd3-browserify' 4 | 5 | module.exports = 6 | class EditorStatsView extends ScrollView 7 | @activate: -> 8 | new EditorStatsView 9 | 10 | @content: -> 11 | @div class: 'editor-stats-wrapper', tabindex: -1, => 12 | @div class: 'editor-stats', outlet: 'editorStats' 13 | 14 | pt: 15 15 | pl: 10 16 | pb: 3 17 | pr: 25 18 | 19 | initialize: -> 20 | super 21 | 22 | resizer = => 23 | return unless @isOnDom() 24 | @draw() 25 | @update() 26 | $(window).on 'resize', _.debounce(resizer, 300) 27 | 28 | draw: -> 29 | @editorStats.empty() 30 | @x ?= d3.scale.ordinal().domain d3.range(@stats.hours * 60) 31 | @y ?= d3.scale.linear() 32 | w = $(atom.views.getView(atom.workspace)).find('.vertical').width() 33 | h = @height() 34 | data = d3.entries @stats.eventLog 35 | max = d3.max data, (d) -> d.value 36 | 37 | @x.rangeBands [0, w - @pl - @pr], 0.2 38 | @y.domain([0, max]).range [h - @pt - @pb, 0] 39 | 40 | @xaxis ?= d3.svg.axis().scale(@x).orient('top') 41 | .tickSize(-h + @pt + @pb) 42 | .tickFormat (d) => 43 | d = new Date(@stats.startDate.getTime() + (d * 6e4)) 44 | mins = d.getMinutes() 45 | mins = "0#{mins}" if mins <= 9 46 | "#{d.getHours()}:#{mins}" 47 | 48 | vis = d3.select(@editorStats.get(0)).append('svg') 49 | .attr('width', w) 50 | .attr('height', h) 51 | .append('g') 52 | .attr('transform', "translate(#{@pl},#{@pt})") 53 | 54 | vis.append('g') 55 | .attr('class', 'x axis') 56 | .call(@xaxis) 57 | .selectAll('g') 58 | .classed('minor', (d, i) -> i % 5 == 0 && i % 15 != 0) 59 | .style 'display', (d, i) -> 60 | if i % 15 == 0 || i % 5 == 0 || i == data.length - 1 61 | 'block' 62 | else 63 | 'none' 64 | 65 | @bars = vis.selectAll('rect.bar') 66 | .data(data) 67 | .enter().append('rect') 68 | .attr('x', (d, i) => @x i) 69 | .attr('height', (d, i) => h - @y(d.value) - @pt - @pb) 70 | .attr('y', (d) => @y(d.value)) 71 | .attr('width', @x.rangeBand()) 72 | .attr('class', 'bar') 73 | 74 | clearInterval(@updateInterval) 75 | updater = => @update() if @isOnDom() 76 | setTimeout(updater, 100) 77 | @updateInterval = setInterval(updater, 5000) 78 | 79 | update: -> 80 | newData = d3.entries @stats.eventLog 81 | max = d3.max newData, (d) -> d.value 82 | @y.domain [0, max] 83 | h = @height() 84 | @bars.data(newData).transition() 85 | .attr('height', (d, i) => h - @y(d.value) - @pt - @pb) 86 | .attr('y', (d, i) => @y(d.value)) 87 | @bars.classed('max', (d, i) -> d.value == max) 88 | 89 | toggle: (@stats) -> 90 | if @panel?.isVisible() 91 | @detach() 92 | else 93 | @attach() 94 | 95 | attach: -> 96 | @panel ?= atom.workspace.addBottomPanel(item: this) 97 | @panel.show() 98 | @draw() 99 | 100 | detach: -> 101 | @panel?.hide() 102 | 103 | clearInterval(@updateInterval) 104 | $(atom.views.getView(atom.workspace)).focus() 105 | -------------------------------------------------------------------------------- /lib/editor-stats.coffee: -------------------------------------------------------------------------------- 1 | StatsTracker = require './stats-tracker' 2 | 3 | module.exports = 4 | activate: -> 5 | @stats = new StatsTracker() 6 | atom.commands.add 'atom-workspace', 'editor-stats:toggle', => 7 | @createView().toggle(@stats) 8 | 9 | deactivate: -> 10 | @editorStatsView = null 11 | @stats = null 12 | 13 | createView: -> 14 | unless @editorStatsView 15 | EditorStatsView = require './editor-stats-view' 16 | @editorStatsView = new EditorStatsView() 17 | @editorStatsView 18 | -------------------------------------------------------------------------------- /lib/stats-tracker.coffee: -------------------------------------------------------------------------------- 1 | {$} = require 'atom-space-pen-views' 2 | 3 | module.exports = 4 | class StatsTracker 5 | startDate: new Date 6 | hours: 6 7 | eventLog: {} 8 | 9 | constructor: -> 10 | date = new Date(@startDate) 11 | future = new Date(date.getTime() + (36e5 * @hours)) 12 | @eventLog[@time(date)] = 0 13 | 14 | while date < future 15 | @eventLog[@time(date)] = 0 16 | 17 | workspaceView = atom.views.getView(atom.workspace) 18 | $(workspaceView).on 'keydown', => @track() 19 | $(workspaceView).on 'mouseup', => @track() 20 | 21 | clear: -> 22 | @eventLog = {} 23 | 24 | track: -> 25 | date = new Date 26 | times = @time date 27 | @eventLog[times] ?= 0 28 | @eventLog[times] += 1 29 | @eventLog.shift() if @eventLog.length > (@hours * 60) 30 | 31 | time: (date) -> 32 | date.setTime(date.getTime() + 6e4) 33 | hour = date.getHours() 34 | minute = date.getMinutes() 35 | "#{hour}:#{minute}" 36 | -------------------------------------------------------------------------------- /menus/editor-stats.cson: -------------------------------------------------------------------------------- 1 | 'menu': [ 2 | 'label': 'Packages' 3 | 'submenu': [ 4 | 'label': 'Editor Stats' 5 | 'submenu': [ 6 | { 'label': 'Toggle', 'command': 'editor-stats:toggle' } 7 | ] 8 | ] 9 | ] 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "editor-stats", 3 | "version": "0.17.0", 4 | "main": "./lib/editor-stats", 5 | "description": "Display a graph of keyboard and mouse usage for the last 6 hours.", 6 | "license": "MIT", 7 | "dependencies": { 8 | "d3-browserify": "3.3.13", 9 | "underscore-plus": "1.x", 10 | "atom-space-pen-views": "^2.0.3" 11 | }, 12 | "repository": "https://github.com/atom/editor-stats", 13 | "engines": { 14 | "atom": "*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /spec/editor-stats-spec.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'underscore-plus' 2 | {$} = require 'atom-space-pen-views' 3 | 4 | describe "EditorStats", -> 5 | editorStats = null 6 | workspaceElement = null 7 | 8 | simulateKeyUp = (key) -> 9 | e = $.Event "keydown", keyCode: key.charCodeAt(0) 10 | $(workspaceElement).trigger e 11 | 12 | simulateClick = -> 13 | e = $.Event "mouseup" 14 | $(workspaceElement).trigger e 15 | 16 | beforeEach -> 17 | workspaceElement = atom.views.getView(atom.workspace) 18 | 19 | waitsForPromise -> 20 | atom.workspace.open('sample.js') 21 | 22 | waitsForPromise -> 23 | atom.packages.activatePackage('editor-stats').then (pack) -> 24 | editorStats = pack.mainModule.stats 25 | 26 | describe "when a keyup event is triggered", -> 27 | beforeEach -> 28 | expect(_.values(editorStats.eventLog)).not.toContain 1 29 | expect(_.values(editorStats.eventLog)).not.toContain 2 30 | 31 | it "records the number of times a keyup is triggered", -> 32 | simulateKeyUp('a') 33 | expect(_.values(editorStats.eventLog)).toContain 1 34 | simulateKeyUp('b') 35 | expect(_.values(editorStats.eventLog)).toContain 2 36 | 37 | describe "when a mouseup event is triggered", -> 38 | it "records the number of times a mouseup is triggered", -> 39 | simulateClick() 40 | expect(_.values(editorStats.eventLog)).toContain 1 41 | simulateClick() 42 | expect(_.values(editorStats.eventLog)).toContain 2 43 | -------------------------------------------------------------------------------- /styles/editor-stats.less: -------------------------------------------------------------------------------- 1 | .editor-stats-wrapper { 2 | padding: 5px; 3 | box-sizing: border-box; 4 | } 5 | 6 | .editor-stats { 7 | height: 50px; 8 | width: 100%; 9 | background: #1d1f21; 10 | overflow: hidden; 11 | background: #1a2226; 12 | border: 1px solid #171e22; 13 | border-radius: 3px; 14 | 15 | .bar { 16 | fill: rgba(255, 255, 255, 0.2); 17 | shape-rendering: crispedges; 18 | 19 | &.max { 20 | fill: rgba(0, 163, 255, 1); 21 | } 22 | } 23 | 24 | text { 25 | font-size: 10px; 26 | fill: rgba(255, 255, 255, 0.2); 27 | font-family: Courier; 28 | -webkit-user-select: none; 29 | cursor: default; 30 | } 31 | 32 | .minor text { 33 | display: none; 34 | } 35 | 36 | line { 37 | stroke: #ccc; 38 | stroke-opacity: 0.05; 39 | stroke-width: 1px; 40 | shape-rendering: crispedges; 41 | } 42 | 43 | path.domain { 44 | display: none; 45 | } 46 | } 47 | --------------------------------------------------------------------------------