├── .gitignore ├── LICENSE.md ├── README.md ├── bin └── git-commander ├── config ├── index.js └── key │ ├── mc.json │ └── vi.json ├── controller ├── branch.js ├── diff.js ├── editor.js ├── log.js └── main.js ├── doc ├── git-commander.gif └── git-commander@2x.gif ├── model └── git.js ├── package.json └── view ├── branch.js ├── diff.js ├── editor.js ├── log.js ├── main.js └── style ├── branch.js ├── diff.js ├── editor.js ├── log.js └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | # npm modules 2 | node_modules 3 | 4 | # OS junk files 5 | [Tt]humbs.db 6 | *.DS_Store 7 | 8 | # WebStorm 9 | .idea 10 | 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2015 Jin Kim 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git commander 2 | 3 | A git tool with an easy terminal interface. 4 | 5 | ![ScreenShot](https://raw.githubusercontent.com/golbin/git-commander/master/doc/git-commander@2x.gif) 6 | 7 | # Features 8 | 9 | - [x] git status 10 | - [x] git add [files] 11 | - [x] git reset -- [files] 12 | - [x] git commit [files] 13 | - [x] git log 14 | - [x] git reset 15 | - [x] git diff file 16 | - [x] git branch 17 | - [ ] git merge (handling conflict) 18 | - [ ] git pull/push 19 | 20 | # Install & Usage 21 | 22 | ```bash 23 | $ npm install -g git-commander 24 | $ git-commander 25 | ``` 26 | 27 | # Requirements 28 | 29 | - **git** >= 2.4 30 | - **nodejs** >= 0.12 31 | - **blessed** >= 0.1.7 32 | - **lodash** >= 3.0 33 | 34 | # Key Configuration 35 | 36 | We have two key sets _vi_ and _mc_ preconfigured. The default one is _vi_. 37 | 38 | ##### Using the mc key set 39 | 40 | You will need to place a file at `~/.config/git-commander/config.json` with the following content: 41 | 42 | ``` 43 | { 44 | "keySet": "mc" 45 | } 46 | ``` 47 | 48 | ##### Redefining keys one by one 49 | 50 | You also can redefine keys one by one if you would like. You'll need to extend your `~/.config/git-commander/config.json` file with a key called `keys` and put all your key definitions there. For example if you would like to use the _mc_ key set and make key _x_ quit the application, you'll need to add the following: 51 | 52 | ``` 53 | { 54 | "keySet": "mc", 55 | "keys": { 56 | "common": { 57 | "quit": [ 58 | "x" 59 | ] 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | You can find [default settings here](https://github.com/golbin/git-commander/tree/master/config/key). 66 | 67 | # Troubleshootings 68 | 69 | ## ANSI color codes are displayed 70 | 71 | ANSI color codes are being displayed if you set "always" for color settings in your **.gitconfig**. For fixing this, set "auto" for color settings like below. 72 | 73 | ``` 74 | [color] 75 | # diff = always 76 | diff = auto 77 | status = auto 78 | ui = auto 79 | branch = auto 80 | ``` 81 | 82 | ## Non-ascii character problem 83 | 84 | If you use non-ascii character for source files, You need to disable 85 | the **core.quotepath** option using following command: 86 | 87 | ```bash 88 | $ git config --global core.quotepath false 89 | ``` 90 | 91 | # License 92 | 93 | MIT 94 | -------------------------------------------------------------------------------- /bin/git-commander: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../controller/main').reload(); 4 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var defaultConfig = { 4 | keySet: 'vi' 5 | }; 6 | 7 | var config = (function() { 8 | var fs = require('fs'); 9 | 10 | var retval = defaultConfig; 11 | var userHome = process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']; 12 | var configFileName = userHome + '/.config/git-commander/config.json'; 13 | 14 | if (configFileName && fs.existsSync(configFileName)) { 15 | retval = _.extend({}, defaultConfig, JSON.parse(fs.readFileSync(configFileName))); 16 | } 17 | 18 | return retval; 19 | }()); 20 | 21 | 22 | var keyConfig = require('./key/' + config.keySet + '.json'); 23 | 24 | module.exports = { 25 | keys: config.keys ? _.merge({}, keyConfig, config.keys) : keyConfig 26 | }; 27 | -------------------------------------------------------------------------------- /config/key/mc.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "quit": [ 4 | "escape", 5 | "q" 6 | ], 7 | "pageUp": [ 8 | "pageup" 9 | ], 10 | "pageDown": [ 11 | "pagedown" 12 | ] 13 | }, 14 | "main": { 15 | "select": [ 16 | "space" 17 | ], 18 | "selectAll": [ 19 | "enter" 20 | ], 21 | "add": [ 22 | "a" 23 | ], 24 | "reset": [ 25 | "r" 26 | ], 27 | "commit": [ 28 | "c" 29 | ], 30 | "log": [ 31 | "l" 32 | ], 33 | "diff": [ 34 | "d" 35 | ], 36 | "showBranch": [ 37 | "b" 38 | ], 39 | "togglePanes": [ 40 | "tab" 41 | ], 42 | "leftPane": [ 43 | "left" 44 | ], 45 | "rightPane": [ 46 | "right" 47 | ] 48 | }, 49 | "editor": { 50 | "save": [ 51 | "C-s" 52 | ] 53 | }, 54 | "branch": { 55 | "checkOut": [ 56 | "enter" 57 | ], 58 | "delete": [ 59 | "d" 60 | ], 61 | "add": [ 62 | "a" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /config/key/vi.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "quit": [ 4 | "escape", 5 | "q" 6 | ], 7 | "pageUp": [ 8 | "C-b" 9 | ], 10 | "pageDown": [ 11 | "C-f" 12 | ] 13 | }, 14 | "main": { 15 | "select": [ 16 | "space" 17 | ], 18 | "selectAll": [ 19 | "enter" 20 | ], 21 | "add": [ 22 | "C-a" 23 | ], 24 | "reset": [ 25 | "C-r" 26 | ], 27 | "commit": [ 28 | "C-c" 29 | ], 30 | "log": [ 31 | "C-l" 32 | ], 33 | "diff": [ 34 | "C-d" 35 | ], 36 | "showBranch": [ 37 | "C-b" 38 | ], 39 | "togglePanes": [ 40 | "tab" 41 | ], 42 | "leftPane": [ 43 | "left" 44 | ], 45 | "rightPane": [ 46 | "right" 47 | ] 48 | }, 49 | "editor": { 50 | "save": [ 51 | "C-s" 52 | ] 53 | }, 54 | "branch": { 55 | "checkOut": [ 56 | "enter" 57 | ], 58 | "delete": [ 59 | "C-d" 60 | ], 61 | "add": [ 62 | "C-a" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /controller/branch.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var BranchView = require('../view/branch'); 4 | var config = require('../config'); 5 | 6 | var parent = null, 7 | view = null; 8 | 9 | var branch = { 10 | loadBranches: function () { 11 | return parent.git.loadBranches() 12 | .map(function (branchName, index) { 13 | if (index === parent.git.currentBranchIndex) { 14 | return " * " + branchName; 15 | } else { 16 | return " " + branchName; 17 | } 18 | }); 19 | }, 20 | 21 | show: function () { 22 | view.list.setItems(branch.loadBranches()); 23 | view.list.select(parent.git.currentBranchIndex); 24 | 25 | view.layout.show(); 26 | view.list.focus(); 27 | view.layout.parent.render(); 28 | }, 29 | 30 | hide: function (reload) { 31 | view.layout.hide(); 32 | parent.show(reload); 33 | }, 34 | 35 | init: function (delegate) { 36 | parent = delegate; 37 | 38 | view = BranchView(parent.screen); 39 | 40 | view.list.key(config.keys.branch.checkOut, function () { 41 | try { 42 | parent.git.checkout(this.selected); 43 | branch.hide(true); 44 | } catch (e) { 45 | branch.hide(true); 46 | parent.showPopup(_.trim(e.stderr.toString()), 3); 47 | } 48 | }); 49 | 50 | view.list.key(config.keys.branch.delete, function () { 51 | try { 52 | parent.git.delBranch(this.selected); 53 | branch.show(); 54 | } catch (e) { 55 | branch.hide(true); 56 | parent.showPopup(_.trim(e.stderr.toString()), 3); 57 | } 58 | }); 59 | 60 | view.list.key(config.keys.branch.add, function () { 61 | view.prompt.input('Input the new branch name', '', function (err, value) { 62 | try { 63 | parent.git.addBranch(value); 64 | branch.show(); 65 | } catch (e) { 66 | branch.hide(true); 67 | parent.showPopup(_.trim(e.stderr.toString()), 3); 68 | } 69 | }); 70 | }); 71 | 72 | view.list.key(config.keys.common.quit, function () { 73 | branch.hide(); 74 | }); 75 | } 76 | }; 77 | 78 | module.exports = branch; 79 | 80 | -------------------------------------------------------------------------------- /controller/diff.js: -------------------------------------------------------------------------------- 1 | var DiffView = require('../view/diff'); 2 | var config = require('../config'); 3 | 4 | var parent = null, 5 | view = null; 6 | 7 | var diff = { 8 | colorFormat: function (diffText) { 9 | return diffText 10 | .replace(/(^\-.*$)/gm, "{red-fg}$1{/red-fg}") 11 | .replace(/(^\+.*$)/gm, "{green-fg}$1{/green-fg}") 12 | .replace(/(^@@.*$)/gm, "{cyan-fg}$1{/cyan-fg}"); 13 | }, 14 | show: function () { 15 | var diffText = parent.git.diff( 16 | parent.prevFocused.name, 17 | parent.prevFocused.selected 18 | ); 19 | 20 | view.textarea.setContent(diff.colorFormat(diffText)); 21 | 22 | view.layout.show(); 23 | view.textarea.focus(); 24 | view.layout.parent.render(); 25 | }, 26 | 27 | hide: function (reload) { 28 | view.layout.hide(); 29 | view.textarea.setContent(""); 30 | parent.show(reload); 31 | }, 32 | 33 | init: function (delegate) { 34 | parent = delegate; 35 | 36 | view = DiffView(parent.screen); 37 | 38 | view.textarea.key(config.keys.common.quit, function () { 39 | diff.hide(); 40 | }); 41 | 42 | view.textarea.key(config.keys.common.pageUp, function () { 43 | view.textarea.scroll(-view.textarea.height || -1); 44 | redraw(); 45 | }); 46 | 47 | view.textarea.key(config.keys.common.pageDown, function () { 48 | view.textarea.scroll(view.textarea.height || 1); 49 | redraw(); 50 | }); 51 | } 52 | }; 53 | 54 | module.exports = diff; 55 | 56 | -------------------------------------------------------------------------------- /controller/editor.js: -------------------------------------------------------------------------------- 1 | var EditorView = require('../view/editor'); 2 | var config = require('../config'); 3 | 4 | var parent = null, 5 | view = null; 6 | 7 | var editor = { 8 | show: function () { 9 | view.layout.show(); 10 | view.textarea.focus(); 11 | view.textarea.readInput(); 12 | view.layout.parent.render(); 13 | }, 14 | 15 | hide: function (reload) { 16 | view.layout.hide(); 17 | view.textarea.clearValue(); 18 | parent.show(reload); 19 | }, 20 | 21 | init: function (delegate) { 22 | parent = delegate; 23 | 24 | view = EditorView(parent.screen); 25 | 26 | view.textarea.key(config.keys.editor.save, function () { 27 | var message = view.textarea.getValue(); 28 | 29 | parent.git.commit(message); 30 | 31 | editor.hide(true); 32 | }); 33 | 34 | view.textarea.key(config.keys.common.quit, function () { 35 | editor.hide(); 36 | }); 37 | } 38 | }; 39 | 40 | 41 | module.exports = editor; 42 | 43 | -------------------------------------------------------------------------------- /controller/log.js: -------------------------------------------------------------------------------- 1 | var LogView = require('../view/log'); 2 | var config = require('../config'); 3 | 4 | var parent = null, 5 | view = null; 6 | 7 | var SECONDS_OF_MINUTES = 60, 8 | SECONDS_OF_HOUR = 3600, 9 | SECONDS_OF_DAY = 86400, 10 | SECONDS_OF_WEEK = 604800, 11 | SECONDS_OF_MONTH = 2592000; 12 | 13 | var logItems = []; 14 | 15 | var log = { 16 | diffDate: function (date1, date2) { 17 | var unit = "", 18 | number = Math.floor((date1 - date2) / 1000); 19 | 20 | if (number < SECONDS_OF_MINUTES) { 21 | unit = "sec"; 22 | } else if (number < SECONDS_OF_HOUR) { 23 | unit = "min"; 24 | number = Math.floor(number / SECONDS_OF_MINUTES); 25 | } else if (number < SECONDS_OF_DAY) { 26 | unit = "hour"; 27 | number = Math.floor(number / SECONDS_OF_HOUR); 28 | } else if (number < SECONDS_OF_WEEK) { 29 | unit = "day"; 30 | number = Math.floor(number / SECONDS_OF_DAY); 31 | } else if (number < SECONDS_OF_MONTH) { 32 | unit = "week"; 33 | number = Math.floor(number / SECONDS_OF_WEEK); 34 | } else { 35 | unit = "month"; 36 | number = Math.floor(number / SECONDS_OF_MONTH); 37 | } 38 | 39 | return number + " " + unit + (number > 1 ? "s" : ""); 40 | }, 41 | 42 | getLogItems: function () { 43 | var now = new Date(); 44 | 45 | logItems = parent.git.log(); 46 | 47 | return logItems.reduce(function (formatted, item) { 48 | var line = " {red-fg}" + item.id.slice(0, 7) + "{/red-fg}"; 49 | 50 | line = line + " - "; 51 | 52 | if (item.messages && item.messages[0]) { 53 | line = line + item.messages[0]; 54 | } else { 55 | line = line + "No message"; 56 | } 57 | 58 | line = line + " {green-fg}(" + log.diffDate(now, item.date) + "){/green-fg}"; 59 | 60 | line = line + " {cyan-fg}<" + item.author.name + ">{/cyan-fg}"; 61 | 62 | formatted.push(line); 63 | 64 | return formatted; 65 | }, []); 66 | }, 67 | 68 | show: function () { 69 | view.list.setItems(log.getLogItems()); 70 | 71 | view.layout.show(); 72 | view.list.focus(); 73 | view.layout.parent.render(); 74 | }, 75 | 76 | clear: function () { 77 | logItems = []; 78 | view.list.setItems(logItems); 79 | }, 80 | 81 | hide: function (reload) { 82 | view.layout.hide(); 83 | log.clear(); 84 | parent.show(reload); 85 | }, 86 | 87 | init: function (delegate) { 88 | parent = delegate; 89 | 90 | view = LogView(parent.screen); 91 | 92 | view.list.key(config.keys.main.reset, function () { 93 | var item = logItems[view.list.selected]; 94 | 95 | view.confirm.ask("Are you sure to reset? (Y/N)\n", function (err, value) { 96 | if (value === true) { 97 | parent.git.resetCommit(item.id); 98 | 99 | log.hide(true); 100 | } else { 101 | redraw(); 102 | } 103 | }); 104 | }); 105 | 106 | view.list.key(config.keys.common.quit, function () { 107 | log.hide(); 108 | }); 109 | 110 | view.list.key(config.keys.common.pageUp, function () { 111 | view.list.scroll(-view.list.height || -1); 112 | redraw(); 113 | }); 114 | 115 | view.list.key(config.keys.common.pageDown, function () { 116 | view.list.scroll(view.list.height || 1); 117 | redraw(); 118 | }); 119 | 120 | view.confirm.key(config.keys.common.quit, function () { 121 | view.confirm.hide(); 122 | }); 123 | 124 | } 125 | }; 126 | 127 | 128 | module.exports = log; 129 | 130 | -------------------------------------------------------------------------------- /controller/main.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | var Git = require('../model/git'), 4 | view = require('../view/main'); 5 | 6 | var editor = require('./editor'), 7 | branch = require('./branch'), 8 | log = require('./log'), 9 | diff = require('./diff'); 10 | 11 | var config = require('../config'); 12 | 13 | // model control 14 | var git = new Git(process.cwd()); 15 | 16 | // for convenience. 17 | // use just call redraw() anywhere if need screen.render() 18 | global.redraw = function () { 19 | view.screen.render(); 20 | }; 21 | 22 | var main = { 23 | git : git, 24 | screen: view.screen, 25 | 26 | moveToStaged: function (index) { 27 | view.list.staged.interactive = true; 28 | view.list.unstaged.interactive = false; 29 | view.list.staged.select(index !== undefined ? index : view.list.unstaged.selected); 30 | view.list.staged.focus(); 31 | redraw(); 32 | }, 33 | 34 | moveToUnstaged: function (index) { 35 | view.list.staged.interactive = false; 36 | view.list.unstaged.interactive = true; 37 | view.list.unstaged.select(index !== undefined ? index : view.list.staged.selected); 38 | view.list.unstaged.focus(); 39 | redraw(); 40 | }, 41 | 42 | next: function () { 43 | this.move(1); 44 | redraw(); 45 | }, 46 | 47 | // TODO: Fix crash if there is no item 48 | toggle: function () { 49 | var selected = git.isSelected(this.name, this.selected); 50 | 51 | if (selected) { 52 | git.deselect(this.name, this.selected); 53 | main.unmark.call(this); 54 | } else { 55 | git.select(this.name, this.selected); 56 | main.mark.call(this); 57 | } 58 | 59 | redraw(); 60 | }, 61 | 62 | selectAll: function () { 63 | var type = view.list.staged.focused ? view.list.staged.name : view.list.unstaged.name; 64 | 65 | var selected = git.isSelected(type); 66 | 67 | if (selected) { 68 | git.deselect(type); 69 | main.setItems(git); 70 | } else { 71 | git.select(type); 72 | main.setItems(git); 73 | } 74 | 75 | redraw(); 76 | }, 77 | 78 | lockScreen: function () { 79 | main.prevFocused = view.screen.focused; 80 | }, 81 | 82 | unlockScreen: function () { 83 | if (main.prevFocused) { 84 | main.prevFocused.focus(); 85 | } 86 | }, 87 | 88 | show: function (controller) { 89 | if (_.isBoolean(controller) && controller === true) { 90 | main.reload(); 91 | redraw(); 92 | } else if (_.isObject(controller)) { 93 | main.lockScreen(); 94 | controller.show(); 95 | } else { 96 | main.unlockScreen(); 97 | redraw(); 98 | } 99 | }, 100 | 101 | setBranchLine: function () { 102 | var branchLine = 103 | "{blue-fg}" + git.getCurrentProjectName() + ":{/blue-fg}" + 104 | "{yellow-fg}" + git.getCurrentBranchName() + "{/yellow-fg}"; 105 | 106 | view.branchbox.setContent(branchLine); 107 | }, 108 | 109 | // initialize 110 | reload: function () { 111 | git.status(); 112 | 113 | main.setBranchLine(); 114 | 115 | main.setItems(); 116 | 117 | main.moveToUnstaged(0); 118 | 119 | view.loading.stop(); 120 | 121 | redraw(); 122 | }, 123 | 124 | // utility functions 125 | formatItem: function (selected, symbol, file) { 126 | var prefix = selected ? " * " : " "; 127 | return prefix + symbol + " " + file; 128 | }, 129 | 130 | buildItemListByType: function (type) { 131 | return _.reduce(git.files[type], function (result, val, index) { 132 | result.push(main.formatItem(git.selected[type][index], git.symbols[type][index], val)); 133 | return result; 134 | }, []); 135 | }, 136 | 137 | setItems: function () { 138 | view.list.staged.setItems(main.buildItemListByType('staged')); 139 | view.list.unstaged.setItems(main.buildItemListByType('unstaged')); 140 | }, 141 | 142 | mark: function () { 143 | var selectedItem = this.getItem(this.selected); 144 | if (selectedItem === undefined) { 145 | return; 146 | } 147 | 148 | var content = selectedItem.content; 149 | this.setItem(this.selected, " * " + content.substr(3)); 150 | }, 151 | 152 | unmark: function () { 153 | var selectedItem = this.getItem(this.selected); 154 | if (selectedItem === undefined) { 155 | return; 156 | } 157 | 158 | var content = this.getItem(this.selected).content; 159 | this.setItem(this.selected, " " + content.substr(3)); 160 | }, 161 | 162 | showPopup: function (msg, hideAfter) { 163 | view.popup.content = msg; 164 | view.popup.hidden = false; 165 | redraw(); 166 | 167 | setTimeout(main.hidePopup, hideAfter ? hideAfter : 1000); 168 | }, 169 | 170 | hidePopup: function () { 171 | view.popup.hidden = true; 172 | redraw(); 173 | } 174 | }; 175 | 176 | // bind editor 177 | editor.init(main); 178 | 179 | // bind log viewer 180 | log.init(main); 181 | 182 | // bind diff viewer 183 | diff.init(main); 184 | 185 | // bind branch viewer 186 | branch.init(main); 187 | 188 | // bind keys 189 | // TODO: Need to refactor 190 | _.each(view.list, function (elem) { 191 | elem.key(config.keys.main.select, function () { 192 | main.toggle.call(this); 193 | main.next.call(this); 194 | }); 195 | 196 | elem.key(config.keys.main.selectAll, function () { 197 | main.selectAll(); 198 | }); 199 | 200 | elem.key(config.keys.common.quit, function () { 201 | return process.exit(0); 202 | }); 203 | 204 | elem.key(config.keys.common.pageUp, function () { 205 | elem.move(-(elem.height - elem.iheight)); 206 | redraw(); 207 | }); 208 | 209 | elem.key(config.keys.common.pageDown, function () { 210 | elem.move(elem.height - elem.iheight); 211 | redraw(); 212 | }); 213 | 214 | // git commands 215 | elem.key(config.keys.main.add, function () { 216 | if (!view.list.unstaged.focused) { 217 | main.showPopup("It cannot work on Staged list"); 218 | return; 219 | } 220 | 221 | if (git.selectedFiles('unstaged').length === 0) { 222 | main.showPopup("Please select a file or files"); 223 | return; 224 | } 225 | 226 | git.add(); 227 | main.reload(); 228 | }); 229 | 230 | elem.key(config.keys.main.reset, function () { 231 | git.reset(); 232 | main.reload(); 233 | }); 234 | 235 | elem.key(config.keys.main.commit, function () { 236 | if (git.getStagedFiles().length < 1) { 237 | main.showPopup("You need to stage files first"); 238 | return; 239 | } 240 | 241 | main.show(editor); 242 | }); 243 | 244 | elem.key(config.keys.main.log, function () { 245 | main.show(log); 246 | }); 247 | 248 | elem.key(config.keys.main.diff, function () { 249 | if (this.getItem(this.selected) === undefined) { 250 | main.showPopup("There is no diff file you selected"); 251 | return; 252 | } 253 | 254 | main.show(diff); 255 | }); 256 | 257 | elem.key(config.keys.main.showBranch, function () { 258 | main.show(branch); 259 | }); 260 | 261 | elem.key(config.keys.main.togglePanes, function () { 262 | if (view.list.staged.interactive) { 263 | main.moveToUnstaged(); 264 | } else { 265 | main.moveToStaged(); 266 | } 267 | }); 268 | 269 | elem.key(config.keys.main.leftPane, function () { 270 | main.moveToStaged(); 271 | }); 272 | 273 | elem.key(config.keys.main.leftPane, function () { 274 | main.moveToUnstaged(); 275 | }); 276 | }); 277 | 278 | module.exports = main; 279 | -------------------------------------------------------------------------------- /doc/git-commander.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golbin/git-commander/c091ad4ff64694f6112202bce4b067293ac21e5a/doc/git-commander.gif -------------------------------------------------------------------------------- /doc/git-commander@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/golbin/git-commander/c091ad4ff64694f6112202bce4b067293ac21e5a/doc/git-commander@2x.gif -------------------------------------------------------------------------------- /model/git.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | path = require('path'), 3 | execSync = require('child_process').execSync; 4 | 5 | // TODO: get git command from shell 6 | var gitExec = 'git'; 7 | 8 | function Git (repoPath) { 9 | this.repoPath = repoPath; 10 | 11 | this.projectName = repoPath.split(path.sep).pop(); 12 | 13 | this.loadBranches(); 14 | } 15 | 16 | Git.prototype.clear = function () { 17 | this.files = { 18 | staged : [], 19 | unstaged: [] 20 | }; 21 | 22 | this.selected = { 23 | staged : [], 24 | unstaged: [] 25 | }; 26 | 27 | this.symbols = { 28 | staged : [], 29 | unstaged: [] 30 | }; 31 | }; 32 | 33 | Git.prototype.setStatus = function (code, filePath) { 34 | if (code.charAt(0) !== '?' && code.charAt(0) !== ' ') { 35 | this.files.staged.push(filePath); 36 | this.selected.staged.push(false); 37 | this.symbols.staged.push(code.charAt(0)); 38 | } 39 | 40 | if (code.charAt(1) !== ' ') { 41 | this.files.unstaged.push(filePath); 42 | this.selected.unstaged.push(false); 43 | this.symbols.unstaged.push(code.charAt(1)); 44 | } 45 | }; 46 | 47 | Git.prototype.parseStatus = function (stdout) { 48 | return stdout.split('\n') 49 | .reduce(function (result, row) { 50 | if (row !== '') { 51 | result.push({ 52 | code: row.substr(0, 2), 53 | path: row.substr(3) 54 | }); 55 | } 56 | 57 | return result; 58 | }, []); 59 | }; 60 | 61 | Git.prototype.convertStatus = function (stdout) { 62 | var self = this, 63 | status = this.parseStatus(stdout); 64 | 65 | _.each(status, function (item) { 66 | self.setStatus(item.code, item.path); 67 | }); 68 | }; 69 | 70 | Git.prototype.status = function () { 71 | var self = this; 72 | 73 | this.clear(); 74 | 75 | var gitCommand = gitExec + ' status -su'; 76 | 77 | var stdout = execSync(gitCommand); 78 | 79 | return self.convertStatus(stdout.toString()); 80 | }; 81 | 82 | Git.prototype.isSelected = function (type, index) { 83 | if (index !== undefined) { 84 | return this.selected[type][index]; 85 | } else { 86 | return _.every(this.selected[type], function (selected) { 87 | return selected; 88 | }); 89 | } 90 | }; 91 | 92 | Git.prototype.select = function (type, index) { 93 | if (index !== undefined) { 94 | this.selected[type][index] = true; 95 | } else { 96 | for (var i = 0; i < this.selected[type].length; i++) { 97 | this.selected[type][i] = true; 98 | } 99 | } 100 | }; 101 | 102 | Git.prototype.deselect = function (type, index) { 103 | if (index !== undefined) { 104 | this.selected[type][index] = false; 105 | } else { 106 | for (var i = 0; i < this.selected[type].length; i++) { 107 | this.selected[type][i] = false; 108 | } 109 | } 110 | }; 111 | 112 | Git.prototype.getStagedFiles = function () { 113 | return this.files.staged; 114 | }; 115 | 116 | Git.prototype.selectedFiles = function (type) { 117 | var selectedFiles = []; 118 | 119 | for (var i = 0; i < this.selected[type].length; i++) { 120 | if (this.selected[type][i] === true) { 121 | selectedFiles.push('"' + this.files[type][i] + '"'); 122 | } 123 | } 124 | 125 | return selectedFiles.join(' '); 126 | }; 127 | 128 | Git.prototype.commandWithFiles = function (command, type) { 129 | var files = this.selectedFiles(type); 130 | 131 | var gitCommand = gitExec + ' ' + command + ' ' + files; 132 | 133 | var stdout = execSync(gitCommand); 134 | 135 | return stdout.toString(); 136 | }; 137 | 138 | Git.prototype.add = function () { 139 | return this.commandWithFiles('add', 'unstaged'); 140 | }; 141 | 142 | Git.prototype.reset = function () { 143 | return this.commandWithFiles('reset --', 'staged'); 144 | }; 145 | 146 | Git.prototype.commit = function (message) { 147 | message.replace('"', '\\"'); 148 | 149 | var gitCommand = gitExec + ' commit -m "' + message + '"'; 150 | 151 | var stdout = execSync(gitCommand); 152 | 153 | return stdout.toString(); 154 | }; 155 | 156 | var isNotEmpty = function (value) { 157 | return !_.isEmpty(value); 158 | }; 159 | 160 | var parseLogContent = function (logs, line) { 161 | if (line.slice(0, 6) === 'commit') { 162 | logs.push({ 163 | id: line.slice(7) 164 | }); 165 | } else if (line.slice(0, 6) === 'Author') { 166 | var emailIndex = line.indexOf('<'); 167 | 168 | if (emailIndex > 0) { 169 | _.last(logs).author = { 170 | name : line.slice(8, emailIndex - 1), 171 | email: line.slice(emailIndex + 1, -1) 172 | }; 173 | } else { 174 | _.last(logs).author = { 175 | name: line 176 | }; 177 | } 178 | } else if (line.slice(0, 4) === 'Date') { 179 | _.last(logs).date = new Date(line.slice(5)); 180 | } else { 181 | if (!_.last(logs).messages) { 182 | _.last(logs).messages = [_.trim(line)]; 183 | } else { 184 | _.last(logs).messages.push(_.trim(line)); 185 | } 186 | } 187 | 188 | return logs; 189 | }; 190 | 191 | Git.prototype.log = function () { 192 | var gitCommand = gitExec + ' log'; 193 | 194 | var stdout = execSync(gitCommand); 195 | 196 | return _(stdout.toString().split('\n')) 197 | .filter(isNotEmpty) 198 | .reduce(parseLogContent, []); 199 | }; 200 | 201 | Git.prototype.resetCommit = function (commitId) { 202 | var gitCommand = gitExec + ' reset ' + commitId; 203 | 204 | var stdout = execSync(gitCommand); 205 | 206 | return stdout.toString(); 207 | }; 208 | 209 | Git.prototype.diff = function (type, index) { 210 | var gitCommand = gitExec + ' diff "' + this.files[type][index] + '"'; 211 | 212 | var stdout = execSync(gitCommand); 213 | 214 | return stdout.toString(); 215 | }; 216 | 217 | Git.prototype.loadBranches = function () { 218 | var self = this; 219 | 220 | var gitCommand = gitExec + ' branch --list'; 221 | 222 | var stdout = execSync(gitCommand); 223 | 224 | this.branches = _.trim(stdout.toString()).split('\n') 225 | .map(function (line, index) { 226 | if (line.charAt(0) === '*') { 227 | self.currentBranchIndex = index; 228 | return _.trim(line.slice(2)); 229 | } else { 230 | return _.trim(line); 231 | } 232 | }); 233 | 234 | return this.branches; 235 | }; 236 | 237 | Git.prototype.getCurrentBranchName = function () { 238 | return this.branches[this.currentBranchIndex]; 239 | }; 240 | 241 | Git.prototype.getCurrentProjectName = function () { 242 | return this.projectName; 243 | }; 244 | 245 | Git.prototype.checkout = function (branchIndex) { 246 | var gitCommand = gitExec + ' checkout ' + this.branches[branchIndex]; 247 | 248 | var stdout = execSync(gitCommand, {stdio: [0, 'pipe', 'pipe']}); 249 | 250 | this.currentBranchIndex = branchIndex; 251 | 252 | return this.currentBranchIndex; 253 | }; 254 | 255 | Git.prototype.delBranch = function (branchIndex) { 256 | var gitCommand = gitExec + ' branch -d ' + this.branches[branchIndex]; 257 | 258 | var stdout = execSync(gitCommand, {stdio: [0, 'pipe', 'pipe']}); 259 | 260 | this.currentBranchIndex = branchIndex; 261 | 262 | return this.currentBranchIndex; 263 | }; 264 | 265 | Git.prototype.addBranch = function (branchName) { 266 | var gitCommand = gitExec + ' branch ' + _.trim(branchName); 267 | 268 | var stdout = execSync(gitCommand, {stdio: [0, 'pipe', 'pipe']}); 269 | 270 | return branchName; 271 | }; 272 | 273 | module.exports = Git; 274 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-commander", 3 | "version": "0.0.14", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/golbin/git-commander.git" 8 | }, 9 | "homepage": "https://github.com/golbin/git-commander", 10 | "bugs": { 11 | "url": "https://github.com/golbin/git-commander/issues" 12 | }, 13 | "dependencies": { 14 | "blessed": "^0.1.7", 15 | "lodash": "^3" 16 | }, 17 | "devDependencies": {}, 18 | "engines": { 19 | "node": "^0.12" 20 | }, 21 | "main": "controller/main.js", 22 | "bin": { 23 | "git-commander": "./bin/git-commander" 24 | }, 25 | "keywords": [ 26 | "git", 27 | "terminal" 28 | ], 29 | "directories": { 30 | "doc": "doc" 31 | }, 32 | "scripts": { 33 | "test": "echo \"Error: no test specified\" && exit 1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /view/branch.js: -------------------------------------------------------------------------------- 1 | var blessed = require('blessed'), 2 | config = require('../config'), 3 | styles = require('./style/branch')(config); 4 | 5 | var layout = null, 6 | list = null, 7 | menubar = null, 8 | prompt = null; 9 | 10 | var init = function (screen) { 11 | styles.layout.parent = screen; 12 | 13 | layout = blessed.layout(styles.layout); 14 | 15 | styles.list.parent = layout; 16 | styles.menubar.parent = layout; 17 | 18 | list = blessed.list(styles.list); 19 | menubar = blessed.listbar(styles.menubar); 20 | 21 | styles.prompt.parent = list; 22 | prompt = blessed.prompt(styles.prompt); 23 | 24 | return { 25 | layout : layout, 26 | list : list, 27 | menubar: menubar, 28 | prompt : prompt 29 | }; 30 | }; 31 | 32 | module.exports = init; 33 | -------------------------------------------------------------------------------- /view/diff.js: -------------------------------------------------------------------------------- 1 | var blessed = require('blessed'), 2 | config = require('../config'), 3 | styles = require('./style/diff')(config); 4 | 5 | var layout = null, 6 | textarea = null, 7 | menubar = null; 8 | 9 | var init = function (screen) { 10 | styles.layout.parent = screen; 11 | 12 | layout = blessed.layout(styles.layout); 13 | 14 | styles.textarea.parent = layout; 15 | styles.menubar.parent = layout; 16 | 17 | textarea = blessed.text(styles.textarea); 18 | menubar = blessed.listbar(styles.menubar); 19 | 20 | return { 21 | layout : layout, 22 | textarea: textarea, 23 | menubar : menubar 24 | }; 25 | }; 26 | 27 | module.exports = init; 28 | -------------------------------------------------------------------------------- /view/editor.js: -------------------------------------------------------------------------------- 1 | var blessed = require('blessed'), 2 | config = require('../config'), 3 | styles = require('./style/editor')(config); 4 | 5 | var layout = null, 6 | textarea = null, 7 | menubar = null; 8 | 9 | var init = function (screen) { 10 | styles.layout.parent = screen; 11 | 12 | layout = blessed.layout(styles.layout); 13 | 14 | styles.textarea.parent = layout; 15 | styles.menubar.parent = layout; 16 | 17 | textarea = blessed.textarea(styles.textarea); 18 | menubar = blessed.listbar(styles.menubar); 19 | 20 | return { 21 | layout : layout, 22 | textarea: textarea, 23 | menubar : menubar 24 | }; 25 | }; 26 | 27 | module.exports = init; 28 | -------------------------------------------------------------------------------- /view/log.js: -------------------------------------------------------------------------------- 1 | var blessed = require('blessed'), 2 | config = require('../config'), 3 | styles = require('./style/log')(config); 4 | 5 | var layout = null, 6 | list = null, 7 | confirm = null, 8 | menubar = null; 9 | 10 | var init = function (screen) { 11 | styles.layout.parent = screen; 12 | 13 | layout = blessed.layout(styles.layout); 14 | 15 | styles.list.parent = layout; 16 | styles.menubar.parent = layout; 17 | 18 | list = blessed.list(styles.list); 19 | menubar = blessed.listbar(styles.menubar); 20 | 21 | styles.confirm.parent = screen; 22 | 23 | confirm = blessed.question(styles.confirm); 24 | 25 | return { 26 | layout : layout, 27 | list : list, 28 | confirm: confirm, 29 | menubar: menubar 30 | }; 31 | }; 32 | 33 | module.exports = init; 34 | 35 | -------------------------------------------------------------------------------- /view/main.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'), 2 | blessed = require('blessed'), 3 | config = require('../config'), 4 | styles = require('./style/main')(config); 5 | 6 | // build layout 7 | var screen = blessed.screen(styles.screen); 8 | 9 | // bind screen to child elems 10 | _.merge(styles, { 11 | title : { 12 | staged : {parent: screen}, 13 | unstaged: {parent: screen} 14 | }, 15 | list : { 16 | staged : {parent: screen}, 17 | unstaged: {parent: screen} 18 | }, 19 | branchbox: { 20 | parent: screen 21 | }, 22 | menubar1 : { 23 | parent: screen 24 | }, 25 | menubar2 : { 26 | parent: screen 27 | }, 28 | loading : { 29 | parent: screen 30 | }, 31 | popup : { 32 | parent: screen 33 | } 34 | }); 35 | 36 | var title = { 37 | staged : blessed.box(styles.title.staged), 38 | unstaged: blessed.box(styles.title.unstaged) 39 | }; 40 | 41 | var list = { 42 | staged : blessed.list(styles.list.staged), 43 | unstaged: blessed.list(styles.list.unstaged) 44 | }; 45 | 46 | list.staged.name = "staged"; 47 | list.unstaged.name = "unstaged"; 48 | 49 | var branchbox = blessed.box(styles.branchbox); 50 | 51 | blessed.listbar(styles.menubar1); 52 | blessed.listbar(styles.menubar2); 53 | 54 | var loading = blessed.loading(styles.loading); 55 | 56 | var popup = blessed.text(styles.popup); 57 | 58 | module.exports = { 59 | screen : screen, 60 | title : title, 61 | list : list, 62 | branchbox: branchbox, 63 | loading : loading, 64 | popup : popup 65 | }; 66 | -------------------------------------------------------------------------------- /view/style/branch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (config) { 4 | return { 5 | "layout": { 6 | "hidden": true, 7 | "top": "center", 8 | "left": "center", 9 | "width": "50%", 10 | "height": "50%" 11 | }, 12 | "list": { 13 | "top": "top", 14 | "left": "left", 15 | "width": "100%", 16 | "height": "100%-4", 17 | "data": null, 18 | "border": "line", 19 | "align": "left", 20 | "vi": true, 21 | "keys": true, 22 | "style": { 23 | "border": { 24 | "fg": "white" 25 | }, 26 | "selected": { 27 | "bg": "blue" 28 | } 29 | } 30 | }, 31 | "menubar": { 32 | "align": "center", 33 | "bottom": 0, 34 | "width": "100%", 35 | "height": 3, 36 | "border": "line", 37 | "mouse": true, 38 | "vi": true, 39 | "keys": true, 40 | "style": { 41 | "prefix": { 42 | "fg": "white" 43 | }, 44 | "item": { 45 | "fg": "cyan" 46 | }, 47 | "selected": { 48 | "fg": "cyan" 49 | } 50 | }, 51 | "commands": { 52 | "CHECKOUT": { 53 | "keys": config.keys.branch.checkOut 54 | }, 55 | "ADD": { 56 | "keys": config.keys.branch.add 57 | }, 58 | "DEL": { 59 | "keys": config.keys.branch.delete 60 | } 61 | } 62 | }, 63 | "prompt": { 64 | "top": "center", 65 | "left": "center", 66 | "width": "80%", 67 | "height": "shrink", 68 | "border": "line", 69 | "align": "left", 70 | "vi": true, 71 | "keys": true 72 | } 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /view/style/diff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (config) { 4 | return { 5 | "layout": { 6 | "hidden": true, 7 | "top": "center", 8 | "left": "center", 9 | "width": "100%", 10 | "height": "100%" 11 | }, 12 | "textarea": { 13 | "top": "top", 14 | "left": "left", 15 | "width": "100%", 16 | "height": "100%-3", 17 | "border": "line", 18 | "tags": true, 19 | "scrollable": true, 20 | "mouse": true, 21 | "keys": true, 22 | "padding": { 23 | "left": 1, 24 | "right": 1 25 | }, 26 | "scrollbar": { 27 | "ch": " " 28 | }, 29 | "style": { 30 | "scrollbar": { 31 | "inverse": true 32 | } 33 | } 34 | }, 35 | "menubar": { 36 | "align": "center", 37 | "bottom": 0, 38 | "width": "100%", 39 | "height": 3, 40 | "mouse": true, 41 | "border": "line", 42 | "keys": true, 43 | "style": { 44 | "prefix": { 45 | "fg": "white" 46 | }, 47 | "item": { 48 | "fg": "cyan" 49 | }, 50 | "selected": { 51 | "fg": "cyan" 52 | } 53 | }, 54 | "commands": { 55 | "EXIT": { 56 | "keys": config.keys.common.quit 57 | }, 58 | "PAGE DOWN": { 59 | "keys": config.keys.common.pageDown 60 | }, 61 | "PAGE UP": { 62 | "keys": config.keys.common.pageUp 63 | } 64 | } 65 | } 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /view/style/editor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (config) { 4 | return { 5 | "layout": { 6 | "hidden": true, 7 | "top": "center", 8 | "left": "center", 9 | "width": "80%", 10 | "height": "70%" 11 | }, 12 | "textarea": { 13 | "top": "top", 14 | "left": "left", 15 | "width": "100%", 16 | "height": "100%-3", 17 | "border": "line", 18 | "mouse": true, 19 | "vi": true, 20 | "keys": true, 21 | "padding": { 22 | "left": 1, 23 | "right": 1 24 | }, 25 | "style": { 26 | "bg": "blue" 27 | } 28 | }, 29 | "menubar": { 30 | "align": "center", 31 | "bottom": 0, 32 | "width": "100%", 33 | "height": 3, 34 | "mouse": true, 35 | "border": "line", 36 | "vi": true, 37 | "keys": true, 38 | "style": { 39 | "prefix": { 40 | "fg": "white" 41 | }, 42 | "item": { 43 | "fg": "cyan" 44 | }, 45 | "selected": { 46 | "fg": "cyan" 47 | } 48 | }, 49 | "commands": { 50 | "SAVE & COMMIT": { 51 | "keys": config.keys.editor.save 52 | }, 53 | "CANCEL": { 54 | "keys": config.keys.common.quit 55 | } 56 | } 57 | } 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /view/style/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (config) { 4 | return { 5 | "layout": { 6 | "hidden": true, 7 | "top": "center", 8 | "left": "center", 9 | "width": "100%", 10 | "height": "100%" 11 | }, 12 | "list": { 13 | "top": "top", 14 | "left": "left", 15 | "data": null, 16 | "border": "line", 17 | "align": "left", 18 | "tags": true, 19 | "width": "100%", 20 | "height": "100%-3", 21 | "mouse": true, 22 | "vi": true, 23 | "keys": true, 24 | "style": { 25 | "border": { 26 | "fg": "white" 27 | }, 28 | "selected": { 29 | "bg": "blue" 30 | } 31 | } 32 | }, 33 | "confirm": { 34 | "border": "line", 35 | "height": "shrink", 36 | "width": "half", 37 | "top": "center", 38 | "left": "center", 39 | "keys": true, 40 | "vi": true 41 | }, 42 | "menubar": { 43 | "align": "center", 44 | "bottom": 0, 45 | "width": "100%", 46 | "height": 3, 47 | "mouse": true, 48 | "border": "line", 49 | "vi": true, 50 | "keys": true, 51 | "style": { 52 | "prefix": { 53 | "fg": "white" 54 | }, 55 | "item": { 56 | "fg": "cyan" 57 | }, 58 | "selected": { 59 | "fg": "cyan" 60 | } 61 | }, 62 | "commands": { 63 | "RESET COMMIT": { 64 | "keys": config.keys.main.reset 65 | }, 66 | "CANCEL": { 67 | "keys": config.keys.common.quit 68 | }, 69 | "PAGE DOWN": { 70 | "keys": config.keys.common.pageDown 71 | }, 72 | "PAGE UP": { 73 | "keys": config.keys.common.pageUp 74 | } 75 | } 76 | } 77 | }; 78 | }; 79 | -------------------------------------------------------------------------------- /view/style/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (config) { 4 | return { 5 | "screen": { 6 | "autoPadding": false, 7 | "fullUnicode": true 8 | }, 9 | "title": { 10 | "staged": { 11 | "top": "top", 12 | "left": "left", 13 | "width": "50%", 14 | "height": 3, 15 | "align": "center", 16 | "content": "Staged Files", 17 | "tags": true, 18 | "border": { 19 | "type": "none" 20 | }, 21 | "style": { 22 | "fg": "green" 23 | } 24 | }, 25 | "unstaged": { 26 | "top": "top", 27 | "left": "50%", 28 | "width": "50%", 29 | "height": 3, 30 | "align": "center", 31 | "content": "Not Staged Files", 32 | "tags": true, 33 | "border": { 34 | "type": "none" 35 | }, 36 | "style": { 37 | "fg": "red" 38 | } 39 | } 40 | }, 41 | "list": { 42 | "staged": { 43 | "top": "top+2", 44 | "left": "left", 45 | "data": null, 46 | "border": "line", 47 | "align": "left", 48 | "tags": true, 49 | "width": "50%", 50 | "height": "100%-6", 51 | "interactive": false, 52 | "keys": true, 53 | "style": { 54 | "border": { 55 | "fg": "green" 56 | }, 57 | "selected": { 58 | "bg": "blue" 59 | } 60 | } 61 | }, 62 | "unstaged": { 63 | "top": "top+2", 64 | "left": "50%", 65 | "data": null, 66 | "border": "line", 67 | "align": "left", 68 | "tags": true, 69 | "width": "50%", 70 | "height": "100%-6", 71 | "interactive": true, 72 | "keys": true, 73 | "style": { 74 | "border": { 75 | "fg": "red" 76 | }, 77 | "selected": { 78 | "bg": "blue" 79 | } 80 | } 81 | } 82 | }, 83 | "branchbox": { 84 | "bottom": 3, 85 | "height": 1, 86 | "width": "100%", 87 | "align": "right", 88 | "tags": true, 89 | "padding": { 90 | "right": 2 91 | } 92 | }, 93 | "menubar1": { 94 | "align": "center", 95 | "bottom": 1, 96 | "height": 2, 97 | "mouse": true, 98 | "vi": true, 99 | "keys": true, 100 | "style": { 101 | "prefix": { 102 | "fg": "white" 103 | }, 104 | "item": { 105 | "fg": "cyan" 106 | }, 107 | "selected": { 108 | "fg": "cyan" 109 | } 110 | }, 111 | "commands": { 112 | "SELECT": { 113 | "keys": config.keys.main.select 114 | }, 115 | "ADD": { 116 | "keys": config.keys.main.add 117 | }, 118 | "RESET": { 119 | "keys": config.keys.main.reset 120 | }, 121 | "COMMIT": { 122 | "keys": config.keys.main.commit 123 | } 124 | } 125 | }, 126 | "menubar2": { 127 | "align": "center", 128 | "bottom": 1, 129 | "height": 1, 130 | "mouse": true, 131 | "vi": true, 132 | "keys": true, 133 | "style": { 134 | "prefix": { 135 | "fg": "white" 136 | }, 137 | "item": { 138 | "fg": "cyan" 139 | }, 140 | "selected": { 141 | "fg": "cyan" 142 | } 143 | }, 144 | "commands": { 145 | "ALL ": { 146 | "keys": config.keys.main.selectAll 147 | }, 148 | "LOG": { 149 | "keys": config.keys.main.log 150 | }, 151 | "DIFF ": { 152 | "keys": config.keys.main.diff 153 | }, 154 | "BRANCH": { 155 | "keys": config.keys.main.showBranch 156 | } 157 | } 158 | }, 159 | "loading": { 160 | "top": "center", 161 | "left": "center", 162 | "align": "center", 163 | "height": 5, 164 | "width": "50%", 165 | "tags": true, 166 | "hidden": true, 167 | "border": "line" 168 | }, 169 | "popup": { 170 | "top": "center", 171 | "left": "center", 172 | "align": "left", 173 | "height": "shrink", 174 | "width": "shrink", 175 | "tags": true, 176 | "hidden": true, 177 | "border": "line", 178 | "padding": { 179 | "top": 1, 180 | "bottom": 1, 181 | "left": 3, 182 | "right": 3 183 | }, 184 | "style": { 185 | "border": { 186 | "fg": "red" 187 | } 188 | } 189 | } 190 | }; 191 | }; 192 | --------------------------------------------------------------------------------