├── .github └── FUNDING.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── github ├── lib ├── api │ ├── login.js │ ├── profile.js │ ├── stream.js │ ├── user-issues.js │ └── user-list.js ├── conf │ └── index.js ├── frame-handlers.js ├── ui │ ├── profile.js │ ├── splash-screen.js │ ├── stream.js │ ├── user-issues.js │ └── user-list.js └── utils.js ├── package.json ├── resources ├── github.png └── octocat.png └── screenshots ├── create-repo.png ├── issues.png ├── news-feed.png ├── profile.png ├── pull-requests.png └── splashscreen.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ionicabizau 2 | patreon: ionicabizau 3 | open_collective: ionicabizau 4 | custom: https://www.buymeacoffee.com/h96wwchmy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *~ 4 | *.log 5 | node_modules 6 | *.env 7 | .DS_Store 8 | package-lock.json 9 | .bloggify/* 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🌟 Contributing 2 | 3 | Want to contribute to this project? Great! Please read these quick steps to streamline the process and avoid unnecessary tasks. ✨ 4 | 5 | ## 💬 Discuss Changes 6 | Start by opening an issue in the repository using the [bug tracker][1]. Describe your proposed contribution or the bug you've found. If relevant, include platform info and screenshots. 🖼️ 7 | 8 | Wait for feedback before proceeding unless the fix is straightforward, like a typo. 📝 9 | 10 | ## 🔧 Fixing Issues 11 | 12 | Fork the project and create a branch for your fix, naming it `some-great-feature` or `some-issue-fix`. Commit changes while following the [code style][2]. If the project has tests, add one. ✅ 13 | 14 | If a `package.json` or `bower.json` exists, add yourself to the `contributors` array; create it if it doesn't. 🙌 15 | 16 | ```json 17 | { 18 | "contributors": [ 19 | "Your Name (http://your.website)" 20 | ] 21 | } 22 | ``` 23 | 24 | ## 📬 Creating a Pull Request 25 | Open a pull request and reference the initial issue (e.g., *fixes #*). Provide a clear title and consider adding visual aids for clarity. 📊 26 | 27 | ## ⏳ Wait for Feedback 28 | Your contributions will be reviewed. If feedback is given, update your branch as needed, and the pull request will auto-update. 🔄 29 | 30 | ## 🎉 Everyone Is Happy! 31 | Your contributions will be merged, and everyone will appreciate your effort! 😄❤️ 32 | 33 | Thanks! 🤩 34 | 35 | [1]: /issues 36 | [2]: https://github.com/IonicaBizau/code-style -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-25 Ionică Bizău (https://ionicabizau.net) 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ``` 16 | CLI GitHub 17 | A fancy GitHub client for command line. 18 | 19 | .. ..::.. .. 20 | .. ,,LLCCCCCCLL,, .. 21 | ,,CCCCCCCCCCCCCC,, 22 | LLCC,,iiiiii,,CCLL 23 | ,,CCCC CCCC,, 24 | ::CCLL ...... ffCC:: 25 | ,,CCCC .. CCCC,, 26 | CCCCLLii ;;LLCCCC 27 | ;;CCffii ttCCCC;; 28 | .. ;;CC11 11CC;; .. 29 | .. .... .... .. 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | :1ffftti, .:::. ,::: :::, 38 | tCCCCCCCCCL fCCf .... . 1CCC1 LCCC, ,CCCL . 39 | LCCCCLtttfL; .CCCC. ,CCCf . 1CCCt .. CCCC, ,CCCL 40 | 1CCCC. ;ff; ;CCCL,; 1CCC1 LCCC..;:;. .;:;.,CCCL,i11; 41 | LCCC; ;iiiii, CCCC.CCCCCCCC:iCCCCLLLLCCCC tCCCi tCCCi.CCCCCCCCCC; 42 | LCCC: CCCCCCf CCCC fLCCCCff.1CCCCCCCCCCCC tCCCi tCCCi.CCCC1itCCCC 43 | LCCC; tfLCCCt CCCC ,CCCL . 1CCCf:;;:CCCC tCCCi tCCCi.CCCL . LCCC: 44 | iCCCC. iCCCt CCCC :CCCL . 1CCC1 LCCC tCCCi 1CCCi.CCCL . LCCC: 45 | LCCCCLffLCCCf CCCC .CCCCi1 1CCC1 . CCCC tCCCLi1LCCCi.CCCC1itCCCC 46 | 1CCCCCCCCCC1 CCCC tCCCCC;iCCC1 LCCC..CCCCCCCCCCi.CCCCCCCCCC: 47 | .;1tt1i: :;;: ,i11i..;;;. :;;; :1t1i,.;;. ;;:.;111; 48 | ``` 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | # `$ cli-github` 57 | 58 | [![Support me on Patreon][badge_patreon]][patreon] [![Buy me a book][badge_amazon]][amazon] [![PayPal][badge_paypal_donate]][paypal-donations] [![Ask me anything](https://img.shields.io/badge/ask%20me-anything-1abc9c.svg)](https://github.com/IonicaBizau/ama) [![Version](https://img.shields.io/npm/v/cli-github.svg)](https://www.npmjs.com/package/cli-github) [![Downloads](https://img.shields.io/npm/dt/cli-github.svg)](https://www.npmjs.com/package/cli-github) [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/@johnnyb?utm_source=github&utm_medium=button&utm_term=johnnyb&utm_campaign=github) 59 | 60 | Buy Me A Coffee 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | > A fancy GitHub client for command line. 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | ## Prerequisites 91 | 92 | 93 | - [NodeJS](http://nodejs.org/) 94 | - [GraphicsMagick](http://www.graphicsmagick.org/) 95 | 96 | ```sh 97 | # Ubuntu 98 | $ sudo apt-get install graphicsmagick 99 | # Fedora 100 | $ sudo dnf install GraphicsMagick 101 | # Mac OS X 102 | $ brew install graphicsmagick 103 | ``` 104 | 105 | 106 | ## Installation 107 | 108 | ``` 109 | $ npm i -g cli-github 110 | ``` 111 | 112 | ## Usage 113 | 114 | ``` 115 | $ github 116 | ``` 117 | 118 | 119 | Use the following key shortcuts to access different GitHub resources: 120 | 121 | ### News Feed 122 | 123 | 124 | - SHIFT + C: Create a new repository on GitHub. 125 | - SHIFT + P: Visit GitHub profiles (default: your GitHub profile). 126 | - SHIFT + I: View the open issues that are assigned to you. 127 | - SHIFT + R: View the open pull requests that created by you. 128 | - SHIFT + : Go back in history 129 | - SHIFT + : Go forth in history 130 | 131 | ### Profile 132 | 133 | 134 | - SHIFT + R: Fetch user's followers. 135 | - SHIFT + N: Fetch user's following. 136 | - SHIFT + M: Fetch the organization members. 137 | 138 | ### User List 139 | 140 | 141 | - SHIFT + P: Visit GitHub profiles (default: your GitHub profile). 142 | 143 | ## Screenshots 144 | 145 | ### Splashscreen 146 | 147 | ![](/screenshots/splashscreen.png "") 148 | 149 | ### News Feed 150 | 151 | ![](/screenshots/news-feed.png "") 152 | 153 | ### Create repository 154 | 155 | ![](/screenshots/create-repo.png "") 156 | 157 | ### Profile 158 | 159 | ![](/screenshots/profile.png "") 160 | 161 | ### Issues 162 | 163 | ![](/screenshots/issues.png "") 164 | 165 | ### Pull Requests 166 | 167 | ![](/screenshots/pull-requests.png "") 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | ## :question: Get Help 187 | 188 | There are few ways to get help: 189 | 190 | 191 | 192 | 1. Please [post questions on Stack Overflow](https://stackoverflow.com/questions/ask). You can open issues with questions, as long you add a link to your Stack Overflow question. 193 | 2. For bug reports and feature requests, open issues. :bug: 194 | 3. For direct and quick help, you can [use Codementor](https://www.codementor.io/johnnyb). :rocket: 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | ## :yum: How to contribute 214 | Have an idea? Found a bug? See [how to contribute][contributing]. 215 | 216 | 217 | ## :sparkling_heart: Support my projects 218 | I open-source almost everything I can, and I try to reply to everyone needing help using these projects. Obviously, 219 | this takes time. You can integrate and use these projects in your applications *for free*! You can even change the source code and redistribute (even resell it). 220 | 221 | However, if you get some profit from this or just want to encourage me to continue creating stuff, there are few ways you can do it: 222 | 223 | 224 | - Starring and sharing the projects you like :rocket: 225 | - [![Buy me a book][badge_amazon]][amazon]—I love books! I will remember you after years if you buy me one. :grin: :book: 226 | - [![PayPal][badge_paypal]][paypal-donations]—You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea: 227 | - [![Support me on Patreon][badge_patreon]][patreon]—Set up a recurring monthly donation and you will get interesting news about what I'm doing (things that I don't share with everyone). 228 | - **Bitcoin**—You can send me bitcoins at this address (or scanning the code below): `1P9BRsmazNQcuyTxEqveUsnf5CERdq35V6` 229 | 230 | ![](https://i.imgur.com/z6OQI95.png) 231 | 232 | 233 | Thanks! :heart: 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | ## :scroll: License 259 | 260 | [MIT][license] © [Ionică Bizău][website] 261 | 262 | 263 | 264 | 265 | 266 | 267 | [license]: /LICENSE 268 | [website]: https://ionicabizau.net 269 | [contributing]: /CONTRIBUTING.md 270 | [docs]: /DOCUMENTATION.md 271 | [badge_patreon]: https://ionicabizau.github.io/badges/patreon.svg 272 | [badge_amazon]: https://ionicabizau.github.io/badges/amazon.svg 273 | [badge_paypal]: https://ionicabizau.github.io/badges/paypal.svg 274 | [badge_paypal_donate]: https://ionicabizau.github.io/badges/paypal_donate.svg 275 | [patreon]: https://www.patreon.com/ionicabizau 276 | [amazon]: http://amzn.eu/hRo9sIZ 277 | [paypal-donations]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RVXDDLKKLQRJW 278 | -------------------------------------------------------------------------------- /bin/github: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Dependencies 3 | var Box = require("cli-box") 4 | , GitHub = new (require("github"))({ 5 | version: "3.0.0" 6 | }) 7 | , Login = require("../lib/api/login") 8 | , Keypress = require("keypress") 9 | , CliUpdate = require("cli-update") 10 | , Prompt = require("promptify") 11 | , Path = require("path") 12 | , Config = require("../lib/conf") 13 | ; 14 | 15 | // Help output 16 | if (~process.argv.indexOf("-h") || ~process.argv.indexOf("--help")) { 17 | return console.log("Usage: github"); 18 | } 19 | 20 | // Conigure prompt 21 | Config.prompt = Prompt; 22 | Config.coloredImages = !!~process.argv.indexOf("--colored-images"); 23 | Keypress(process.stdin); 24 | 25 | // Frame handlers 26 | Config.frameHandlers = require("../lib/frame-handlers"); 27 | 28 | // Attach GitHub object to Config 29 | Config._github = GitHub; 30 | 31 | // Configure CLI Update 32 | Config.cli.update = CliUpdate; 33 | CliUpdate.navigation = function (data) { 34 | data && data.currentFrame && (Config.currentFrame = data.currentFrame); 35 | }; 36 | 37 | CliUpdate.changed = function (output) { 38 | Config.cache._currentScreen = output; 39 | }; 40 | 41 | 42 | // Set background box 43 | Config.background = Box({ 44 | fullscreen: true 45 | , marks: { 46 | nw: "╔" 47 | , n: "═" 48 | , ne: "╗" 49 | , e: "║" 50 | , se: "╝" 51 | , s: "═" 52 | , sw: "╚" 53 | , w: "║" 54 | , b: " " 55 | } 56 | }); 57 | 58 | try { 59 | var conf = require(Config.CONFIG_PATH); 60 | } catch (e) { 61 | conf = {}; 62 | } 63 | for (var key in conf) { 64 | Config[key] = conf[key]; 65 | } 66 | 67 | // Initialize UI 68 | var SplashScreen = require("../lib/ui/splash-screen") 69 | , MainStream = require("../lib/ui/stream") 70 | ; 71 | 72 | // Show splashscreen 73 | SplashScreen.show(function (err, output) { 74 | if (err) { throw err; } 75 | SplashScreen.updateMessage("Logging in ..."); 76 | 77 | // Login user 78 | Login(function (err, user) { 79 | if (err) { 80 | try { 81 | var error = JSON.parse(err.message).message; 82 | } catch (e) { 83 | error = err.toString(); 84 | } 85 | SplashScreen.updateMessage("Failed to login in: " + error); 86 | return setTimeout(function () { 87 | process.exit(); 88 | }, 1000); 89 | } 90 | Config.user = user; 91 | SplashScreen.updateMessage("Logged in as: " + user.login + ". Fetching News Feed ..."); 92 | MainStream(err => { 93 | if (err) { 94 | console.error(err) 95 | } 96 | }); 97 | }); 98 | }); 99 | 100 | // Listen for the "keypress" event 101 | process.stdin.on("keypress", function (ch, key) { 102 | if (Config.promptRunning) { 103 | return; 104 | } 105 | if (key && key.name === "c" && key.ctrl) { 106 | process.exit(); 107 | } 108 | var handlers = Config.frameHandlers[Config.currentFrame] || {}; 109 | if (key && key.shift && typeof handlers[key.name.toUpperCase()] === "function") { 110 | handlers[key.name.toUpperCase()](); 111 | } 112 | 113 | if (key && key.shift && key.name === "left") { 114 | Config.cli.update.back(); 115 | } 116 | 117 | if (key && key.shift && key.name === "right") { 118 | Config.cli.update.next(); 119 | } 120 | }); 121 | 122 | try { 123 | process.stdin.setRawMode(true); 124 | process.stdin.resume(); 125 | } catch (e) {} 126 | -------------------------------------------------------------------------------- /lib/api/login.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , Fs = require("fs") 4 | , idy = require("idy") 5 | ; 6 | 7 | // 2FA OTP Scheme 8 | var otpScheme = { 9 | properties: { 10 | otp: { 11 | required: true 12 | , message: "Invalid 2FA authentication code." 13 | , description: "" 14 | , type: "number" 15 | } 16 | } 17 | }; 18 | 19 | /** 20 | * Login 21 | * Asks the user for username and password. Then authenticates the user. 22 | * 23 | * @name Login 24 | * @function 25 | * @param {Function} callback The callback function. 26 | * @return {undefined} 27 | */ 28 | var login = module.exports = function (callback) { 29 | 30 | if (!Config.username) { 31 | Config.username = Config.prompt("Enter your GitHub username"); 32 | Config.password = Config.prompt("Enter your GitHub password", { char: "*" }); 33 | return login(callback); 34 | } 35 | 36 | if (Config.token) { 37 | Config._github.authenticate({ 38 | type: "oauth" 39 | , token: Config.token 40 | }); 41 | } else { 42 | if (!Config.username || !Config.password) { 43 | Config.username = null; 44 | Config.password = null; 45 | return login(callback); 46 | } 47 | 48 | Config._github.authenticate({ 49 | type: "basic" 50 | , username: Config.username 51 | , password: Config.password 52 | }); 53 | 54 | var opts = { 55 | note: "CLI GitHub :: " + idy() 56 | , note_url: "https://github.com/IonicaBizau/cli-github" 57 | , scopes: [ 58 | "user", "public_repo", "repo" 59 | , "repo_deployment", "repo:status" 60 | , "delete_repo", "notifications" 61 | , "read:org", "write:org", "admin:org" 62 | ] 63 | }; 64 | 65 | if (Config.otp) { 66 | opts.headers = { 67 | "X-GitHub-OTP": Config.otp 68 | }; 69 | } 70 | 71 | // Create the authorization 72 | return Config._github.authorization.create(opts, function (err, res) { 73 | 74 | if (Config.otp && err) { 75 | return console.log(err); 76 | } 77 | // Handle successfull creation 78 | if (!err) { 79 | Config.token = res.token; 80 | if (JSON.stringify(res.scopes) !== JSON.stringify(opts.scopes)) { 81 | Config._github.authorization.delete({id: res.id}, function (err) { 82 | if (err) { return callback(err); } 83 | Config._github.authorization.create(opts, function (err, res) { 84 | if (err) return callback(err); 85 | Config.token = res.token; 86 | login(callback); 87 | }); 88 | }); 89 | return; 90 | } 91 | return login(callback); 92 | } 93 | 94 | var error; 95 | try { 96 | error = JSON.parse(err.message).message; 97 | } catch (e) { 98 | error = err.toString(); 99 | } 100 | 101 | 102 | if (error.indexOf("Must specify two-factor authentication OTP code.") !== -1 && !Config.otp) { 103 | while (!(Config.otp = Config.prompt("2FA Authentication Code"))) {} 104 | login(callback); 105 | } else if (error || err) { 106 | return callback(error || err); 107 | } 108 | }); 109 | } 110 | 111 | Config._github.users.get({}, function (err, user) { 112 | if (err) { 113 | delete Config.username; 114 | delete Config.password; 115 | delete Config.otp; 116 | delete Config.token; 117 | return callback(err); 118 | } 119 | 120 | Fs.writeFileSync(Config.CONFIG_PATH, JSON.stringify({ 121 | username: Config.username 122 | , token: Config.token 123 | })); 124 | 125 | callback(null, user); 126 | }); 127 | }; 128 | -------------------------------------------------------------------------------- /lib/api/profile.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , Request = require("request") 4 | ; 5 | 6 | /** 7 | * Profile 8 | * Gets user profile data. 9 | * 10 | * @name Profile 11 | * @function 12 | * @param {Object} options Options object 13 | * @param {Function} progress The progress function. 14 | * @param {Function} callback The callback function. 15 | * @return {undefined} 16 | */ 17 | module.exports = function (options, progress, callback) { 18 | // Config._github.user.get({ 19 | // user: options.username || options 20 | // }, function (err, user) { 21 | // if (err) { return callback(err); } 22 | // callback(null, user); 23 | // }); 24 | // TODO use github library 25 | var user = options.username || options; 26 | Request.get({ 27 | headers: { 28 | "user-agent": "GitHub - CLI" 29 | , "Authorization": "token " + Config.token 30 | } 31 | , url: "https://api.github.com/users/" + user 32 | , json: true 33 | }, function (err, status, body) { 34 | if (err || body.error) { return callback(err || body.error); } 35 | progress(null, "Fetching repositories ..."); 36 | Config._github.repos.getForUser({ 37 | username: user 38 | , sort: "updated" 39 | }, function (err, repos) { 40 | if (err) { return callback(err); } 41 | body.repos = repos; 42 | progress(null, "Fetching organizations ..."); 43 | Config._github.orgs.getForUser({ 44 | username: user 45 | }, function (err, orgs) { 46 | if (err) { return callback(err); } 47 | body.orgs = orgs; 48 | callback(null, body); 49 | }); 50 | }); 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /lib/api/stream.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , Request = require("request") 4 | , Couleurs = require("couleurs") 5 | , Moment = require("moment") 6 | , ParseXML = require('xml2js').parseString 7 | ; 8 | 9 | // Icons! :-) 10 | var icons = { 11 | "CreateEvent": "⊕" 12 | , "WatchEvent": "★" 13 | , "PushEvent": "─●─" 14 | , "ForkEvent": "⑂" 15 | , "PullRequestEvent": "⇑" 16 | , "DeleteEvent": "⊗" 17 | , "ReleaseEvent": "⌗" 18 | , "IssuesEvent": "⚠" 19 | }; 20 | 21 | /** 22 | * NewsFeed 23 | * Fetches the latest events from news feed. 24 | * 25 | * @name NewsFeed 26 | * @function 27 | * @param {Function} callback The callback function. 28 | * @return {undefined} 29 | */ 30 | module.exports = function (callback) { 31 | callback = callback || function () {}; 32 | Request.get({ 33 | headers: { 34 | "user-agent": "GitHub - CLI" 35 | } 36 | , auth: { 37 | username: Config.username 38 | , password: Config.token 39 | } 40 | , url: "https://github.com/" + Config.username + ".private.atom" 41 | }, function (err, status, body) { 42 | if (err || body.error) { return callback(err || body.error); } 43 | // TODO Body is ' ' 44 | ParseXML(body, function (err, result) { 45 | if (err) { return callback(err); } 46 | var streamData = []; 47 | for (var i = 0; i < result.feed.entry.length; ++i) { 48 | var cEv = result.feed.entry[i]; 49 | streamData.push({ 50 | icon: icons[cEv.id[0].split(/:|\//)[2]] || " " 51 | , description: cEv.title[0]._ 52 | , time: Moment(cEv.updated[0]).fromNow() 53 | }); 54 | } 55 | callback(null, streamData); 56 | }); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /lib/api/user-issues.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , Request = require("request") 4 | , QueryString = require("querystring") 5 | ; 6 | 7 | /*! 8 | * getAll 9 | * Fetches **all** objects of some type. 10 | * 11 | * @name getAll 12 | * @function 13 | * @param {String} url The url that doesn't contain the page parameter. 14 | * @param {Function} callback The callback function. 15 | * @param {Function} progress The progress function. 16 | * @param {Number|undefined} i The current page number (default: 1). 17 | * @param {Array} data Older data objects (fetched already). 18 | * @return {undefined} 19 | */ 20 | function getAll(url, callback, progress, i, data) { 21 | data = data || []; 22 | i = i || 1; 23 | Request.get({ 24 | headers: { 25 | "user-agent": "GitHub - CLI" 26 | , "Authorization": "token " + Config.token 27 | } 28 | , json: true 29 | , url: url + "&page=" + i 30 | }, function (err, status, body) { 31 | if (err || body.error) { return callback(err || body.error); } 32 | data = data.concat(body.items); 33 | if (!body.items.length) { 34 | return callback(null, data); 35 | } 36 | 37 | progress("Fetched page: " + i); 38 | getAll(url, callback, progress, i + 1, data); 39 | }); 40 | } 41 | 42 | /** 43 | * UserIssues 44 | * Fetches the user issues or pull requests. 45 | * 46 | * @name UserIssues 47 | * @function 48 | * @param {Object} options An object containing the following fields: 49 | * 50 | * - `ops` (Object): the querystring parameters as object 51 | * 52 | * @param {Function} progress The progress function. 53 | * @param {Function} callback The callback function. 54 | * @return {undefined} 55 | */ 56 | module.exports = function (options, progress, callback) { 57 | options = Object(options); 58 | getAll("https://api.github.com/search/issues?" + QueryString.stringify(options.ops), function (err, res) { 59 | if (err) { return callback(err); } 60 | callback(null, res); 61 | }, progress); 62 | }; 63 | -------------------------------------------------------------------------------- /lib/api/user-list.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , Request = require("request") 4 | , QueryString = require("querystring") 5 | ; 6 | 7 | /** 8 | * UserIssues 9 | * Fetches the user issues or pull requests. 10 | * 11 | * @name UserIssues 12 | * @function 13 | * @param {Object} options An object containing the following fields: 14 | * 15 | * - `ops` (Object): the querystring parameters as object 16 | * 17 | * @param {Function} progress The progress function. 18 | * @param {Function} callback The callback function. 19 | * @return {undefined} 20 | */ 21 | module.exports = function (options, callback) { 22 | if (options.type === "followers") { 23 | return Config._github.users.getFollowersForUser({ 24 | username: options.user 25 | , per_page: 100 26 | }, callback); 27 | } else if (options.type === "following") { 28 | return Config._github.users.getFollowingForUser({ 29 | username: options.user 30 | , per_page: 100 31 | }, callback); 32 | } else if (options.type === "members") { 33 | return Config._github.orgs.getMembers({ 34 | org: options.user 35 | , per_page: 100 36 | }, callback); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /lib/conf/index.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Path = require("path"); 3 | 4 | // Constants 5 | const HOME_DIRECTORY = process.env[ 6 | process.platform == "win32" ? "USERPROFILE" : "HOME" 7 | ]; 8 | 9 | process.stdout.columns = process.stdout.columns || 212; 10 | process.stdout.rows = process.stdout.rows || 56; 11 | 12 | // Configuration 13 | module.exports = { 14 | root: Path.resolve(__dirname, "../..") 15 | , cli: { 16 | w: process.stdout.columns 17 | , h: process.stdout.rows 18 | } 19 | , cache: { 20 | avatars: {} 21 | } 22 | , promptRunning: false 23 | , HOMDE_DIR: HOME_DIRECTORY 24 | , CONFIG_PATH: HOME_DIRECTORY + "/.github-config.json" 25 | , title: "CLI GitHub - A fancy GitHub client." 26 | , description: " with <3 by @IonicaBizau" 27 | }; 28 | -------------------------------------------------------------------------------- /lib/frame-handlers.js: -------------------------------------------------------------------------------- 1 | var Config = require("./conf") 2 | , Profile = require(Config.root + "/lib/ui/profile") 3 | , Overlap = require("overlap") 4 | , Keypress = require("keypress") 5 | , Box = require("cli-box") 6 | ; 7 | 8 | var Handlers = module.exports = {}; 9 | 10 | var Dialog = { 11 | show: function (message) { 12 | var old = Config.cache._currentScreen; 13 | if (!message) { 14 | return Config.cli.update.render("\n" + old, false, {}, false); 15 | } 16 | 17 | var box = Box({ 18 | w: 50 19 | , h: 5 20 | , marks: { 21 | nw: "╔" 22 | , n: "═" 23 | , ne: "╗" 24 | , e: "║" 25 | , se: "╝" 26 | , s: "═" 27 | , sw: "╚" 28 | , w: "║" 29 | , b: " " 30 | } 31 | }, { 32 | text: message 33 | , stretch: true 34 | , autoEOL: true 35 | , hAlign: "center" 36 | , vAlign: "middle" 37 | }); 38 | 39 | var newS = Overlap({ 40 | who: old 41 | , with: box 42 | , where: { 43 | x: Math.floor(old.split("\n")[0].length / 2 - box.split("\n")[0].length / 2 - 1) 44 | , y: Math.floor(old.split("\n").length / 2 - box.split("\n").length / 2) 45 | } 46 | }).trim(); 47 | Config.cli.update.render(newS, false, {}, false); 48 | } 49 | }; 50 | 51 | 52 | Handlers["news-feed"] = { 53 | // Create repository 54 | "C": function () { 55 | Dialog.show("Waiting for user input ..."); 56 | Config.promptRunning = true; 57 | var result = {}; 58 | while (!(result.name = Config.prompt("Repository name"))) {} 59 | result.description = Config.prompt("Repository description (optional)"); 60 | result.homepage = Config.prompt("Repository homepage (optional)"); 61 | while (true) { 62 | result.private = Config.prompt("Private repository (y/N)"); 63 | 64 | if (!result.private || result.private.toLowerCase() === "n") { 65 | result.private = false; 66 | break; 67 | } 68 | 69 | if (result.private.toLowerCase() === "y") { 70 | result.private = true; 71 | break; 72 | } 73 | } 74 | 75 | Config.promptRunning = false; 76 | if (!result) { return Dialog.show(); } 77 | Dialog.show("Loading..."); 78 | Config._github.repos.create(result, function (err, data) { 79 | if (err) { return Dialog.show("Cannot create repository: " + JSON.parse(err.message).message); } 80 | Dialog.show("Repository created at: " + data.ssh_url); 81 | }); 82 | } 83 | // Profile 84 | , "P": function () { 85 | Dialog.show("Waiting for user input ..."); 86 | Config.promptRunning = true; 87 | var username = Config.prompt("Username"); 88 | Config.promptRunning = false; 89 | if (!username) { return Dialog.show(); } 90 | Dialog.show("Loading..."); 91 | Profile(username, function (err, progressMsg) { 92 | Dialog.show(err || progressMsg); 93 | }, function (err) { 94 | if (err) { 95 | return Dialog.show(err.message); 96 | } 97 | }); 98 | } 99 | // Show issues 100 | , "I": function () { 101 | Dialog.show("Fetching issues..."); 102 | require("./ui/user-issues")({ 103 | type: "issues" 104 | , ops: { 105 | sort: "updated" 106 | , q: "is:open is:issue assignee:" + Config.username 107 | } 108 | }, function (msg) { 109 | Dialog.show(msg); 110 | }); 111 | } 112 | // Show pull requests 113 | , "R": function () { 114 | Dialog.show("Fetching pull requests..."); 115 | require("./ui/user-issues")({ 116 | type: "pullrequests" 117 | , ops: { 118 | filter: "created" 119 | , sort: "updated" 120 | , state: "open" 121 | } 122 | , ops: { 123 | sort: "updated" 124 | , q: "is:open is:pr author:" + Config.username 125 | } 126 | }, function (msg) { 127 | Dialog.show(msg); 128 | }); 129 | } 130 | }; 131 | 132 | Handlers["profile"] = { 133 | // Show followers 134 | "R": function () { 135 | if (Config.cFrameData.type !== "User") { return; } 136 | Dialog.show("Fetching followers..."); 137 | require("./ui/user-list")({ 138 | type: "followers" 139 | , user: Config.cFrameData.login 140 | }, function (msg) { 141 | Dialog.show(msg); 142 | }); 143 | } 144 | 145 | // Show following 146 | , "N": function () { 147 | if (Config.cFrameData.type !== "User") { return; } 148 | Dialog.show("Fetching following..."); 149 | require("./ui/user-list")({ 150 | type: "following" 151 | , user: Config.cFrameData.login 152 | }, function (msg) { 153 | Dialog.show(msg); 154 | }); 155 | } 156 | 157 | // Show members 158 | , "M": function () { 159 | if (Config.cFrameData.type !== "Organization") { return; } 160 | Dialog.show("Fetching members..."); 161 | require("./ui/user-list")({ 162 | type: "members" 163 | , user: Config.cFrameData.login 164 | }, function (msg) { 165 | Dialog.show(msg); 166 | }); 167 | } 168 | }; 169 | 170 | Handlers["user-list"] = { 171 | "P": Handlers["news-feed"].P 172 | }; 173 | -------------------------------------------------------------------------------- /lib/ui/profile.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , Table = require("le-table") 4 | , Overlap = require("overlap") 5 | , Profile = require("../api/profile") 6 | , Utils = require(Config.root + "/lib/utils") 7 | , ImageToAscii = require("image-to-ascii") 8 | , Moment = require("moment") 9 | ; 10 | 11 | /*! 12 | * getAvatar 13 | * Gets the avatar in ascii format. 14 | * 15 | * @name getAvatar 16 | * @function 17 | * @param {Object} userObj User resource object 18 | * @param {Function} callback The callback function 19 | * @return {undefined} 20 | */ 21 | function getAvatar(userObj, callback) { 22 | var fCache = Config.cache.avatars[userObj.login]; 23 | if (fCache) { 24 | return callback(null, fCache); 25 | } 26 | 27 | ImageToAscii(userObj.avatar_url, { 28 | size: { height: "70%" } 29 | , colored: Config.coloredImages 30 | }, function (err, cAvatar) { 31 | if (err) { return callback(err); } 32 | callback(null, Config.cache.avatars[userObj.login] = cAvatar); 33 | }); 34 | } 35 | 36 | /** 37 | * Profile 38 | * Shows the profile view. 39 | * 40 | * @name Profile 41 | * @function 42 | * @param {Object} options The options object. 43 | * @param {Function} progress The progress function. 44 | * @param {Function} callback The callback function 45 | * @return {undefined} 46 | */ 47 | module.exports = function (options, progress, callback) { 48 | Profile(options, progress, function (err, data) { 49 | if (err) { return callback(err); } 50 | getAvatar(data, function (err, cAvatar) { 51 | if (err) { 52 | return callback(err); 53 | } 54 | 55 | // Profile 56 | var profileScreen = Overlap({ 57 | who: Config.background.toString() 58 | , with: cAvatar 59 | , where: { 60 | x: 3 61 | , y: 2 62 | } 63 | }); 64 | var info = [] 65 | , name = data.name + " (@" + data.login + ")" 66 | , separator = Array(name.length).join("-") 67 | ; 68 | 69 | info.push(separator); 70 | info.push(name) 71 | info.push(separator); 72 | if (data.company) { info.push("♟ " + data.company); } 73 | if (data.location) { info.push("༐ " + data.location); } 74 | if (data.email) { info.push("✉ " + data.email); } 75 | if (data.blog) { info.push("⊜ " + data.blog); } 76 | if (data.created_at) { info.push("⇴ Joined on " + Moment(data.created_at).format("LL")); } 77 | info.push(separator); 78 | if (data.type === "User") { 79 | info.push("Followe(R)s: " + data.followers); 80 | info.push("Followi(N)g: " + data.following); 81 | } else if (data.type === "Organization") { 82 | info.push("Press SHIFT + M to see the organization members."); 83 | } 84 | 85 | profileScreen = Utils.overlap(profileScreen, info, cAvatar.split("\n").length + 2, 3); 86 | 87 | // Repositories 88 | var repos = new Table(); 89 | repos.addRow(["#", "Type", "Name", "SSH URL"]); 90 | var show = Math.floor((Config.cli.h - 3) / 2) - 3; 91 | if (show > data.repos.length) { 92 | show = data.repos.length; 93 | } 94 | 95 | for (var i = 0; i < show; ++i) { 96 | var cRepo = data.repos[i] 97 | , type = "source" 98 | ; 99 | 100 | if (cRepo.fork) { type = "fork"; } 101 | if (cRepo.private) { type = "private"; } 102 | 103 | repos.addRow([i + 1, type, cRepo.name, cRepo.ssh_url]); 104 | } 105 | 106 | var cRepos = repos.stringify().trim(); 107 | profileScreen = Utils.overlap(profileScreen, [ 108 | "Repositories" 109 | , cRepos 110 | ], 2, cAvatar.split("\n")[0].length + 5); 111 | 112 | // Organizations 113 | var orgs = new Table(); 114 | orgs.addRow(["#", "Name"]); 115 | 116 | for (var i = 0; i < data.orgs.length; ++i) { 117 | var cOrg = data.orgs[i]; 118 | orgs.addRow([i + 1, cOrg.login]); 119 | } 120 | 121 | if (data.orgs.length) { 122 | profileScreen = Utils.overlap(profileScreen, [ 123 | "Organizations" 124 | , orgs.stringify().trim() 125 | ], 2, cAvatar.split("\n")[0].length + 5 + cRepos.split("\n")[0].length + 3); 126 | } 127 | 128 | Config.currentFrame = "profile"; 129 | Config.cFrameData = data; 130 | 131 | Config.cli.update.render(profileScreen.trim(), true, { 132 | currentFrame: Config.currentFrame 133 | }); 134 | }); 135 | }); 136 | }; 137 | -------------------------------------------------------------------------------- /lib/ui/splash-screen.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , ImageToAscii = require("image-to-ascii") 4 | , Overlap = require("overlap") 5 | , Utils = require(Config.root + "/lib/utils") 6 | ; 7 | 8 | /** 9 | * SplashScreen.show 10 | * Shows the splash screen. 11 | * 12 | * @name SplashScreen.show 13 | * @function 14 | * @param {Function} callback The callback function. 15 | * @return {undefined} 16 | */ 17 | var bOut = null; 18 | exports.show = function(callback) { 19 | 20 | Config.currentFrame = "splashscreen"; 21 | ImageToAscii(Config.root + "/resources/github.png", { 22 | size: { 23 | height: "25%" 24 | } 25 | , pxWidth: 1 26 | , reverse: true 27 | , colored: false 28 | }, function(err, cGithub) { 29 | if (err) { callback(err); } 30 | 31 | ImageToAscii(Config.root + "/resources/octocat.png", { 32 | size: { 33 | height: "30%" 34 | } 35 | , colored: false 36 | , pixels: "@O!. " 37 | , pxWidth: 1 38 | }, function(err, cOctocat) { 39 | if (err) { callback(err); } 40 | var output = Overlap({ 41 | who: Config.background.toString() 42 | , with: cGithub 43 | , where: { 44 | x: Config.cli.w / 2 - cGithub.split("\n")[0].length / 2 45 | , y: Config.cli.h - cGithub.split("\n").length - 4 46 | } 47 | }); 48 | bOut = output = Overlap({ 49 | who: output 50 | , with: cOctocat 51 | , where: { 52 | x: Config.cli.w / 2 - cOctocat.split("\n")[0].length / 2 53 | , y: Config.cli.h - cGithub.split("\n").length - cGithub.split("\n").length - 10 54 | } 55 | }).trim(); 56 | Config.cli.update.render(output, true, { 57 | currentFrame: Config.currentFrame 58 | }); 59 | callback(null, output); 60 | }); 61 | }); 62 | }; 63 | 64 | /** 65 | * SplashScreen.updateMessage 66 | * Updates the splash screen message. 67 | * 68 | * @name SplashScreen 69 | * @function 70 | * @param {String} message The message to show. 71 | * @return {undefined} 72 | */ 73 | exports.updateMessage = function (message) { 74 | Config.cli.update.render(Utils.overlap(bOut, message, bOut.split("\n").length - 2, "center").trim(), false); 75 | }; 76 | -------------------------------------------------------------------------------- /lib/ui/stream.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , Table = require("le-table") 4 | , Couleurs = require("couleurs") 5 | , Overlap = require("overlap") 6 | , Stream = require("../api/stream") 7 | , Utils = require(Config.root + "/lib/utils") 8 | ; 9 | 10 | // Set default marks 11 | Table.defaults.marks = { 12 | nw: "┌" 13 | , n: "─" 14 | , ne: "┐" 15 | , e: "│" 16 | , se: "┘" 17 | , s: "─" 18 | , sw: "└" 19 | , w: "│" 20 | , b: " " 21 | , mt: "┬" 22 | , ml: "├" 23 | , mr: "┤" 24 | , mb: "┴" 25 | , mm: "┼" 26 | }; 27 | 28 | /** 29 | * NewsFeed 30 | * Fetches the news feed and shows the table. 31 | * 32 | * @name NewsFeed 33 | * @function 34 | * @param {Function} callback The callback function. 35 | * @return {undefined} 36 | */ 37 | module.exports = function (callback) { 38 | callback = callback || function () {}; 39 | Stream(function (err, data) { 40 | if (err) { data = [] } 41 | Config.currentFrame = "news-feed"; 42 | 43 | var timeline = new Table(); 44 | timeline.addRow(["#", { 45 | text: "Type" 46 | , data: { 47 | width: 8 48 | , hAlign: "center" 49 | } 50 | }, "Description", { 51 | text: "Time" 52 | , data: { 53 | width: 18 54 | } 55 | }]); 56 | 57 | var offsetTop = 10 58 | , show = Math.floor((Config.cli.h - offsetTop - 3) / 2) - 1 59 | ; 60 | 61 | if (show > data.length) { 62 | show = data.length; 63 | } 64 | 65 | for (var i = 0; i < show; ++i) { 66 | var cEv = data[i]; 67 | 68 | timeline.addRow([{ 69 | text: i + 1 70 | , data: { 71 | vAlign: "middle" 72 | , width: 6 73 | } 74 | }, { 75 | text: cEv.icon 76 | , data: { 77 | hAlign: "center" 78 | , vAlign: "middle" 79 | } 80 | }, { 81 | text: cEv.description 82 | , data: { 83 | vAlign: "middle" 84 | } 85 | }, { 86 | text: cEv.time 87 | , data: { 88 | vAlign: "middle" 89 | , width: 18 90 | } 91 | }]); 92 | } 93 | 94 | var cTimeline = timeline.stringify() 95 | , output = Overlap({ 96 | who: Config.background.toString() 97 | , with: cTimeline 98 | , where: { 99 | x: Config.cli.w / 2 - cTimeline.split("\n")[0].length / 2 100 | , y: 10 101 | } 102 | }) 103 | ; 104 | 105 | output = Utils.overlap(output, [ 106 | Config.title 107 | , " - - - " 108 | , Config.description 109 | , "" 110 | , "-----------------------------------------------------------------" 111 | , "(C)reate new repository | (P)rofile | (I)ssues | Pull (R)equests" 112 | , "-----------------------------------------------------------------" 113 | ], 2, "center"); 114 | Config.cli.update.render(output.trim(), true, { 115 | currentFrame: Config.currentFrame 116 | }); 117 | }); 118 | }; 119 | -------------------------------------------------------------------------------- /lib/ui/user-issues.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , Table = require("le-table") 4 | , ImageToAscii = require("image-to-ascii") 5 | , Overlap = require("overlap") 6 | , UserIssues = require("../api/user-issues") 7 | , Utils = require(Config.root + "/lib/utils") 8 | ; 9 | 10 | /** 11 | * UserIssues 12 | * Shows the user issues. 13 | * 14 | * @name UserIssues 15 | * @function 16 | * @param {Object} options The options object. 17 | * @param {Function} progress The progress function. 18 | * @param {Function} callback The callback function. 19 | * @return {undefined} 20 | */ 21 | module.exports = function (options, progress, callback) { 22 | callback = callback || function () { }; 23 | UserIssues(options, progress, function (err, data) { 24 | if (err) { return callback(err); } 25 | Config.currentFrame = "user-issues"; 26 | 27 | var userIssues = new Table(); 28 | userIssues.addRow(["#", { 29 | text: "Repo" 30 | , data: { 31 | hAlign: "center" 32 | } 33 | }, "Title", { 34 | text: "Comments" 35 | , data: { 36 | width: 10 37 | } 38 | }]); 39 | 40 | var offsetTop = 10 41 | , show = Math.floor((Config.cli.h - offsetTop - 3) / 2) - 1 42 | ; 43 | 44 | if (show > data.length) { 45 | show = data.length; 46 | } 47 | 48 | for (var i = 0; i < show; ++i) { 49 | var cEv = data[i]; 50 | 51 | userIssues.addRow([{ 52 | text: i + 1 53 | , data: { 54 | vAlign: "middle" 55 | , width: 6 56 | } 57 | }, { 58 | text: cEv.url.match(/\/repos\/(.*)\/(.*)\/.*/)[1] 59 | , data: { 60 | hAlign: "center" 61 | , vAlign: "middle" 62 | } 63 | }, cEv.title 64 | , { 65 | text: cEv.comments.toString() 66 | , data: { 67 | width: 10 68 | } 69 | }]); 70 | } 71 | 72 | var cuserIssues = userIssues.stringify() 73 | , output = Overlap({ 74 | who: Config.background 75 | , with: cuserIssues 76 | , where: { 77 | x: Config.cli.w / 2 - cuserIssues.split("\n")[0].length / 2 78 | , y: 10 79 | } 80 | }) 81 | ; 82 | 83 | 84 | 85 | output = Utils.overlap(output, [ 86 | Config.title 87 | , " - - - " 88 | , Config.description 89 | , "" 90 | , "-----------------------------------------------------------------" 91 | , ["Issues assigned to you.", "Pull requests created by you"][({"issues": 0, "pullrequests": 1})[options.type]] + " (@" + Config.username + ")" 92 | , "-----------------------------------------------------------------" 93 | ], 2, "center"); 94 | 95 | Config.cli.update.render(output.trim(), true, { 96 | currentFrame: Config.currentFrame 97 | }); 98 | }); 99 | }; 100 | -------------------------------------------------------------------------------- /lib/ui/user-list.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | var Config = require("../conf") 3 | , Table = require("le-table") 4 | , Overlap = require("overlap") 5 | , UserList = require("../api/user-list") 6 | , Utils = require(Config.root + "/lib/utils") 7 | ; 8 | 9 | 10 | /** 11 | * NewsFeed 12 | * Fetches the news feed and shows the table. 13 | * 14 | * @name NewsFeed 15 | * @function 16 | * @param {Function} callback The callback function. 17 | * @return {undefined} 18 | */ 19 | module.exports = function (options, callback) { 20 | UserList(options, function (err, data) { 21 | if (err) { return callback(err); } 22 | Config.currentFrame = "user-list"; 23 | 24 | var offsetTop = 10 25 | , show = Math.floor((Config.cli.h - offsetTop - 3) / 2) - 1 26 | , yLimit = show 27 | , xLimit = Math.floor(Config.cli.w / 32) 28 | ; 29 | 30 | if (yLimit > data.length) { 31 | yLimit = data.length; 32 | } 33 | 34 | var tables = []; 35 | outer_loop: 36 | for (var x = 0; x < xLimit; ++x) { 37 | if (!data[x * yLimit]) { 38 | break; 39 | } 40 | var users = new Table(); 41 | tables.push(users); 42 | users.addRow(["#", { 43 | text: "Login" 44 | , data: { 45 | width: 23 46 | , hAlign: "center" 47 | } 48 | }]); 49 | for (var y = 0; y < yLimit; ++y) { 50 | var cUser = data[x * yLimit + y]; 51 | if (!cUser) { 52 | break outer_loop; 53 | } 54 | users.addRow([{ 55 | text: x * yLimit + y + 1 56 | , data: { 57 | width: 6 58 | } 59 | }, { 60 | text: "@" + cUser.login 61 | , data: { 62 | width: 23 63 | } 64 | }]); 65 | } 66 | } 67 | 68 | var allTables = null; 69 | if (!tables[0]) { 70 | allTables = "No users fetched."; 71 | } else { 72 | allTables = tables[0].stringify(); 73 | for (var i = 1; i < tables.length; ++i) { 74 | allTables = Overlap({ 75 | who: allTables 76 | , with: tables[i].stringify() 77 | , where: { 78 | x: i * 29 79 | , y: 0 80 | } 81 | }); 82 | } 83 | } 84 | 85 | var output = Overlap({ 86 | who: Config.background 87 | , with: allTables 88 | , where: { 89 | x: Config.cli.w / 2 - allTables.split("\n")[0].length / 2 90 | , y: 10 91 | } 92 | }) 93 | , desc = { 94 | followers: "@" + options.user + "'s followers" 95 | , following: "@" + options.user + "'s following" 96 | , members: "@" + options.user + "'s members" 97 | } 98 | ; 99 | 100 | output = Utils.overlap(output, [ 101 | Config.title 102 | , " - - - " 103 | , Config.description 104 | , "" 105 | , "-----------------------------------------------------------------" 106 | , desc[options.type] 107 | , "-----------------------------------------------------------------" 108 | ], 2, "center"); 109 | Config.cli.update.render(output.trim(), true, { 110 | currentFrame: Config.currentFrame 111 | }); 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var Overlap = require("overlap"); 2 | 3 | exports.overlap = function overlapCenter(background, text, y, x) { 4 | if (typeof text === "string") { 5 | return Overlap({ 6 | who: background 7 | , with: text 8 | , where: { 9 | x: x === "center" ? background.split("\n")[0].length / 2 - text.length / 2 - 1 : x 10 | , y: y 11 | } 12 | }); 13 | } 14 | 15 | if (text.constructor === Array) { 16 | for (var i = 0; i < text.length; ++i) { 17 | background = overlapCenter(background, text[i], Number(y) + i, x); 18 | } 19 | return background; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cli-github", 3 | "version": "1.8.19", 4 | "description": "A fancy GitHub client for command line.", 5 | "main": "lib/index.js", 6 | "preferGlobal": "true", 7 | "bin": { 8 | "github": "./bin/github" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "Ionică Bizău (https://ionicabizau.net)", 14 | "contributors": [ 15 | "Zach Bruggeman " 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "cli-box": "^6.0.5", 20 | "cli-update": "^3.2.2", 21 | "couleurs": "^6.0.6", 22 | "github": "^7.1.0", 23 | "idy": "^1.2.5", 24 | "image-to-ascii": "^3.0.5", 25 | "keypress": "0.2.1", 26 | "le-table": "^6.1.4", 27 | "moment": "^2.17.1", 28 | "overlap": "^2.2.5", 29 | "promptify": "^1.0.1", 30 | "request": "^2.79.0", 31 | "xml2js": "^0.4.17" 32 | }, 33 | "devDependencies": {}, 34 | "repository": { 35 | "type": "git", 36 | "url": "git@github.com:IonicaBizau/cli-github.git" 37 | }, 38 | "keywords": [ 39 | "github", 40 | "command", 41 | "line", 42 | "geek" 43 | ], 44 | "bugs": { 45 | "url": "https://github.com/IonicaBizau/cli-github/issues" 46 | }, 47 | "homepage": "https://github.com/IonicaBizau/cli-github", 48 | "blah": { 49 | "before_title": [ 50 | { 51 | "code": { 52 | "content": [ 53 | " CLI GitHub", 54 | " A fancy GitHub client for command line.", 55 | "", 56 | " .. ..::.. ..", 57 | " .. ,,LLCCCCCCLL,, ..", 58 | " ,,CCCCCCCCCCCCCC,,", 59 | " LLCC,,iiiiii,,CCLL", 60 | " ,,CCCC CCCC,,", 61 | " ::CCLL ...... ffCC::", 62 | " ,,CCCC .. CCCC,,", 63 | " CCCCLLii ;;LLCCCC", 64 | " ;;CCffii ttCCCC;;", 65 | " .. ;;CC11 11CC;; ..", 66 | " .. .... .... ..", 67 | "", 68 | "", 69 | "", 70 | "", 71 | "", 72 | "", 73 | "", 74 | " :1ffftti, .:::. ,::: :::,", 75 | " tCCCCCCCCCL fCCf .... . 1CCC1 LCCC, ,CCCL .", 76 | " LCCCCLtttfL; .CCCC. ,CCCf . 1CCCt .. CCCC, ,CCCL", 77 | " 1CCCC. ;ff; ;CCCL,; 1CCC1 LCCC..;:;. .;:;.,CCCL,i11;", 78 | " LCCC; ;iiiii, CCCC.CCCCCCCC:iCCCCLLLLCCCC tCCCi tCCCi.CCCCCCCCCC;", 79 | " LCCC: CCCCCCf CCCC fLCCCCff.1CCCCCCCCCCCC tCCCi tCCCi.CCCC1itCCCC", 80 | " LCCC; tfLCCCt CCCC ,CCCL . 1CCCf:;;:CCCC tCCCi tCCCi.CCCL . LCCC:", 81 | " iCCCC. iCCCt CCCC :CCCL . 1CCC1 LCCC tCCCi 1CCCi.CCCL . LCCC:", 82 | " LCCCCLffLCCCf CCCC .CCCCi1 1CCC1 . CCCC tCCCLi1LCCCi.CCCC1itCCCC", 83 | " 1CCCCCCCCCC1 CCCC tCCCCC;iCCC1 LCCC..CCCCCCCCCCi.CCCCCCCCCC:", 84 | " .;1tt1i: :;;: ,i11i..;;;. :;;; :1t1i,.;;. ;;:.;111;" 85 | ] 86 | } 87 | } 88 | ], 89 | "installation": [ 90 | { 91 | "h2": "Prerequisites" 92 | }, 93 | { 94 | "ul": [ 95 | "[NodeJS](http://nodejs.org/)", 96 | [ 97 | "[GraphicsMagick](http://www.graphicsmagick.org/)", 98 | "", 99 | { 100 | "code": { 101 | "language": "sh", 102 | "content": [ 103 | "# Ubuntu", 104 | "$ sudo apt-get install graphicsmagick", 105 | "# Fedora", 106 | "$ sudo dnf install GraphicsMagick", 107 | "# Mac OS X", 108 | "$ brew install graphicsmagick" 109 | ] 110 | } 111 | } 112 | ] 113 | ] 114 | }, 115 | { 116 | "h2": "Installation" 117 | }, 118 | { 119 | "code": { 120 | "content": "$ npm i -g cli-github" 121 | } 122 | }, 123 | { 124 | "h2": "Usage" 125 | }, 126 | { 127 | "code": { 128 | "content": "$ github" 129 | } 130 | }, 131 | { 132 | "p": "Use the following key shortcuts to access different GitHub resources:" 133 | }, 134 | { 135 | "h3": "News Feed" 136 | }, 137 | { 138 | "ul": [ 139 | "SHIFT + C: Create a new repository on GitHub.", 140 | "SHIFT + P: Visit GitHub profiles (default: your GitHub profile).", 141 | "SHIFT + I: View the open issues that are assigned to you.", 142 | "SHIFT + R: View the open pull requests that created by you.", 143 | "SHIFT + : Go back in history", 144 | "SHIFT + : Go forth in history" 145 | ] 146 | }, 147 | { 148 | "h3": "Profile" 149 | }, 150 | { 151 | "ul": [ 152 | "SHIFT + R: Fetch user's followers.", 153 | "SHIFT + N: Fetch user's following.", 154 | "SHIFT + M: Fetch the organization members." 155 | ] 156 | }, 157 | { 158 | "h3": "User List" 159 | }, 160 | { 161 | "ul": [ 162 | "SHIFT + P: Visit GitHub profiles (default: your GitHub profile)." 163 | ] 164 | }, 165 | { 166 | "h2": "Screenshots" 167 | }, 168 | { 169 | "h3": "Splashscreen" 170 | }, 171 | { 172 | "img": { 173 | "source": "/screenshots/splashscreen.png", 174 | "title": "" 175 | } 176 | }, 177 | { 178 | "h3": "News Feed" 179 | }, 180 | { 181 | "img": { 182 | "source": "/screenshots/news-feed.png", 183 | "title": "" 184 | } 185 | }, 186 | { 187 | "h3": "Create repository" 188 | }, 189 | { 190 | "img": { 191 | "source": "/screenshots/create-repo.png", 192 | "title": "" 193 | } 194 | }, 195 | { 196 | "h3": "Profile" 197 | }, 198 | { 199 | "img": { 200 | "source": "/screenshots/profile.png", 201 | "title": "" 202 | } 203 | }, 204 | { 205 | "h3": "Issues" 206 | }, 207 | { 208 | "img": { 209 | "source": "/screenshots/issues.png", 210 | "title": "" 211 | } 212 | }, 213 | { 214 | "h3": "Pull Requests" 215 | }, 216 | { 217 | "img": { 218 | "source": "/screenshots/pull-requests.png", 219 | "title": "" 220 | } 221 | } 222 | ], 223 | "license_year": "2014\n" 224 | }, 225 | "files": [ 226 | "bin/", 227 | "app/", 228 | "lib/", 229 | "dist/", 230 | "src/", 231 | "scripts/", 232 | "resources/", 233 | "menu/", 234 | "cli.js", 235 | "index.js", 236 | "index.d.ts", 237 | "package-lock.json", 238 | "bloggify.js", 239 | "bloggify.json", 240 | "bloggify/" 241 | ] 242 | } -------------------------------------------------------------------------------- /resources/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IonicaBizau/cli-github/a8d3d72c4d6aa95c04a2f6cdb74dcb1b34eae110/resources/github.png -------------------------------------------------------------------------------- /resources/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IonicaBizau/cli-github/a8d3d72c4d6aa95c04a2f6cdb74dcb1b34eae110/resources/octocat.png -------------------------------------------------------------------------------- /screenshots/create-repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IonicaBizau/cli-github/a8d3d72c4d6aa95c04a2f6cdb74dcb1b34eae110/screenshots/create-repo.png -------------------------------------------------------------------------------- /screenshots/issues.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IonicaBizau/cli-github/a8d3d72c4d6aa95c04a2f6cdb74dcb1b34eae110/screenshots/issues.png -------------------------------------------------------------------------------- /screenshots/news-feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IonicaBizau/cli-github/a8d3d72c4d6aa95c04a2f6cdb74dcb1b34eae110/screenshots/news-feed.png -------------------------------------------------------------------------------- /screenshots/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IonicaBizau/cli-github/a8d3d72c4d6aa95c04a2f6cdb74dcb1b34eae110/screenshots/profile.png -------------------------------------------------------------------------------- /screenshots/pull-requests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IonicaBizau/cli-github/a8d3d72c4d6aa95c04a2f6cdb74dcb1b34eae110/screenshots/pull-requests.png -------------------------------------------------------------------------------- /screenshots/splashscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IonicaBizau/cli-github/a8d3d72c4d6aa95c04a2f6cdb74dcb1b34eae110/screenshots/splashscreen.png --------------------------------------------------------------------------------