├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json ├── screenshot.gif ├── utils.js └── widgets ├── ConfirmModal.js ├── ExecCommandDialog.js ├── HelpDialog.js ├── PackageInfo.js ├── PackageList.js ├── StatusLine.js ├── UIEvents.js └── oo-sample.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": false, 7 | "node": true 8 | }, 9 | "parserOptions": { 10 | "ecmaFeatures": { 11 | "jsx": false 12 | }, 13 | "sourceType": "module" 14 | }, 15 | "globals": { 16 | "Promise": true 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "quotes": ["error", "double"], 21 | "space-infix-ops": "error", 22 | "space-unary-ops": "error", 23 | "block-spacing": ["error", "always"], 24 | "brace-style": "error", 25 | "comma-dangle": "error", 26 | "comma-spacing": "error", 27 | "computed-property-spacing": "error", 28 | "indent": ["warn", 2, {"SwitchCase": 1}], 29 | "max-depth": ["error", 3], 30 | "max-len": "error" 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 张垚 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 | # termux-pkg-viewer 2 | 3 | **NEW:** To make it easier to use, I wrote a VIM script to do the same thing. 4 | ```vim 5 | let s:currentPkgName = "" 6 | let s:lastJob = 0 7 | let s:isJobPending = v:false 8 | let s:bufferId = 0 9 | 10 | function! s:UpdateInfo(text) 11 | if s:bufferId == 0 12 | rightbelow vnew 13 | let s:bufferId = winbufnr(".") 14 | setlocal buftype=nofile 15 | setlocal bufhidden=hide 16 | setlocal noswapfile 17 | endif 18 | wincmd l 19 | call deletebufline(s:bufferId, 1, "$") 20 | call appendbufline(s:bufferId, 0, a:text) 21 | wincmd h 22 | endfunction 23 | 24 | 25 | function! s:ReadChannelData(channel) 26 | let l:buffer = [] 27 | while ch_status(a:channel, {'part': 'out'}) == 'buffered' 28 | call add(l:buffer, ch_read(a:channel)) 29 | endwhile 30 | call s:UpdateInfo(l:buffer) 31 | let s:isJobPending = v:false 32 | endfunction 33 | 34 | 35 | function! s:ShowPkgInfo() 36 | let l:text = getline(".") 37 | let l:parts = split(l:text, "/") 38 | if len(l:parts) < 2 39 | return 40 | endif 41 | let l:name = l:parts[0] 42 | if l:name == s:currentPkgName 43 | return 44 | endif 45 | let s:currentPkgName = l:name 46 | if s:isJobPending 47 | call job_stop(s:lastJob) 48 | endif 49 | let s:lastJob = job_start(["apt", "show", l:name], {"close_cb": function("ReadChannelData")}) 50 | let s:isJobPending = v:true 51 | endfunction 52 | " usage: 53 | " pkg list-all > termux.pkg 54 | " vim termux.pkg 55 | autocmd CursorMoved *.pkg call s:ShowPkgInfo() 56 | 57 | ``` 58 | 59 | Don't know termux yet? See [here](https://termux.com/) and [here](https://github.com/termux/termux-app) and [here](https://github.com/termux/termux-packages) 60 | 61 | I wrote this tool to make it more convenient for myself to play termux. I used nodejs and [blessed](https://github.com/chjj/blessed) to develop this tool. The following is a screenshot. Welcome to use this tool, and if you have a better idea, just open a new issue😀. 62 | 63 | ![screenshot](./screenshot.gif) 64 | 65 | ## Features 66 | 67 | * Arbitrary install, uninstall termux package 68 | 69 | * easy to see the instructions for each package 70 | 71 | ## Installation 72 | 73 | 1. install `nodejs` 74 | ```bash 75 | pkg install nodejs 76 | ``` 77 | 78 | 2. clone this project 79 | ```bash 80 | git clone https://github.com/mystorp/termux-pkg-viewer.git 81 | ``` 82 | 83 | 3. run 84 | ``` 85 | cd termux-pkg-viewer 86 | npm start 87 | ``` 88 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var chalk = require("chalk"); 2 | var blessed = require("blessed"); 3 | 4 | var PackageList = require("./widgets/PackageList"); 5 | var PackageInfo = require("./widgets/PackageInfo"); 6 | var StatusLine = require("./widgets/StatusLine"); 7 | 8 | if(module === require.main) { 9 | createUI(); 10 | } 11 | 12 | function createUI() { 13 | var screen = blessed.screen({ 14 | smartCSR: true, 15 | tabSize: 4, 16 | cursor: { 17 | shape: "block", 18 | blink: true 19 | }, 20 | fullUnicode: true, 21 | title: "termux package viewer" 22 | }); 23 | screen.key("S-q", function(ch, keys){ 24 | process.exit(0); 25 | }); 26 | var leftui = new PackageList({ 27 | top: 0, 28 | left: 0, 29 | bottom: 1, 30 | width: 30, 31 | items: [], 32 | focused: true, 33 | scrollable: true, 34 | clickable: true, 35 | label: chalk.bold.magenta(" packages "), 36 | border: {type: "line"}, 37 | style: { 38 | border: {fg: "white"}, 39 | item: {fg: "green"}, 40 | selected: {fg: "white", bg: "magenta"} 41 | } 42 | }); 43 | var rightui = new PackageInfo({ 44 | top: 0, 45 | left: 30, 46 | bottom: 1, 47 | right: 0, 48 | clickable: false, 49 | label: chalk.bold.magenta(" description "), 50 | border: {type: "line"}, 51 | style: { 52 | border: {fg: "white"} 53 | } 54 | }); 55 | var bottomui = new StatusLine({ 56 | top: '100%-1', 57 | left: 0, 58 | right: 0, 59 | bottom: 0, 60 | style: { 61 | bg: "blue", 62 | fg: "white", 63 | bold: true 64 | } 65 | }); 66 | screen.append(leftui); 67 | screen.append(rightui); 68 | screen.append(bottomui); 69 | screen.render(); 70 | } 71 | // vim: set sw=2 ts=2: 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pkg-viewer", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "ubuntudev": "NODE_ENV=ubuntu node index.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "blessed": "^0.1.81", 15 | "chalk": "^2.3.0", 16 | "lodash": "^4.17.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mystorp/termux-pkg-viewer/a53be8be63232671171fc2f358dee5158d7ca4c2/screenshot.gif -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | var child_process = require("child_process"); 2 | 3 | exports.listPackages = listPackages; 4 | exports.getPackageInfo = getPackageInfo; 5 | 6 | 7 | function listPackages(){ 8 | var cmd = "apt list"; 9 | var options ={}; 10 | // DEBUG 11 | if(process.env.NODE_ENV === "ubuntu") { 12 | cmd = "apt list z*"; 13 | } 14 | return getCommandOutput(cmd, options).then(function(buf){ 15 | var pkgs = {}; 16 | buf.split(/\n/g).forEach(function(line){ 17 | var pkg = parsePackageInfo(line); 18 | if(pkg.name && pkg.version) { 19 | pkgs[pkg.name] = pkg; 20 | } 21 | }); 22 | return pkgs; 23 | }); 24 | } 25 | 26 | function getPackageInfo(name) { 27 | var cmd = "apt show " + name; 28 | var options = {}; 29 | return getCommandOutput(cmd, options); 30 | } 31 | 32 | function getCommandOutput(cmd, options) { 33 | return new Promise(function(resolve, reject) { 34 | child_process.exec(cmd, options, function(err, stdout, stderr) { 35 | if(err) { 36 | reject(err); 37 | } else { 38 | resolve(stdout.toString()); 39 | } 40 | }); 41 | }); 42 | } 43 | 44 | function parsePackageInfo(info) { 45 | var parts = info.split(" "); 46 | var name = parts[0].split("/").shift(); 47 | var version = parts[1]; 48 | var arch = parts[2]; 49 | var props = parts[3]; 50 | if(props) { 51 | props = props.substring(1, props.length - 1).split(","); 52 | } else { 53 | props = []; 54 | } 55 | return { 56 | name: name, 57 | version: version, 58 | arch: arch, 59 | installed: props.indexOf("installed") > -1 60 | }; 61 | } 62 | // vim: set ts=2 sw=2: 63 | -------------------------------------------------------------------------------- /widgets/ConfirmModal.js: -------------------------------------------------------------------------------- 1 | var blessed = require("blessed"); 2 | var Node = blessed.Node; 3 | var Question = blessed.Question; 4 | var UIEvents = require("./UIEvents"); 5 | var lodash = require("lodash"); 6 | 7 | function ConfirmModal(options) { 8 | if (!(this instanceof Node)) { 9 | return new ConfirmModal(options); 10 | } 11 | options = lodash.assign({ 12 | shrink: true, 13 | top: "center", 14 | left: "center", 15 | border: {type: "line"}, 16 | padding: 1, 17 | label: " Confirm ", 18 | style: { 19 | shadow: true, 20 | fg: "yellow", 21 | bg: "black", 22 | border: { 23 | fg: "yellow" 24 | } 25 | } 26 | }, options || {}); 27 | Question.call(this, options); 28 | this._patch(); 29 | this.bindEvents(); 30 | this.setIndex(-1); 31 | } 32 | 33 | ConfirmModal.prototype._patch = function(){ 34 | var self = this; 35 | var okBtn = self._.okay; 36 | var cancelBtn = self._.cancel; 37 | var screen = self.screen; 38 | okBtn.position.left = "50%-8"; 39 | okBtn.position.top += 1; 40 | okBtn.style.fg = "black"; 41 | okBtn.style.bg = "white"; 42 | okBtn.content = "Yes"; 43 | screen.setEffects(okBtn, okBtn, "focus", "blur", { 44 | fg: "white", 45 | bg: "blue", 46 | bold: true 47 | }, "focusEffects", "_ftemp"); 48 | 49 | cancelBtn.align = "center"; 50 | cancelBtn.content = " No"; 51 | cancelBtn.position.left = "50%"; 52 | cancelBtn.position.top += 1; 53 | cancelBtn.style.fg = "black"; 54 | cancelBtn.style.bg = "white"; 55 | screen.setEffects(cancelBtn, cancelBtn, "focus", "blur", { 56 | fg: "white", 57 | bg: "red", 58 | bold: true 59 | }, "focusEffects", "_ftemp"); 60 | }; 61 | 62 | ConfirmModal.prototype.bindEvents = function(){ 63 | var self = this; 64 | var okBtn = self._.okay; 65 | var cancelBtn = self._.cancel; 66 | self.on("attach", function(){ 67 | okBtn.key("tab", onTabKey); 68 | cancelBtn.key("tab", onTabKey); 69 | }); 70 | self.on("detach", function(){ 71 | okBtn.unkey("tab", onTabKey); 72 | cancelBtn.unkey("tab", onTabKey); 73 | }); 74 | function onTabKey(){ 75 | if(this === okBtn) { 76 | cancelBtn.focus(); 77 | } else { 78 | okBtn.focus(); 79 | } 80 | self.screen.render(); 81 | } 82 | }; 83 | 84 | ConfirmModal.prototype.ask = function(msg, callback){ 85 | var superAsk = Question.prototype.ask; 86 | superAsk.call(this, msg, callback); 87 | this.focus(); 88 | this._.okay.focus(); 89 | this.setIndex(-1); 90 | UIEvents.emit("show-modal"); 91 | this.screen.render(); 92 | }; 93 | 94 | ConfirmModal.prototype.hide = function(){ 95 | var superHide = Question.prototype.hide; 96 | UIEvents.emit("hide-modal"); 97 | superHide.call(this); 98 | }; 99 | 100 | ConfirmModal.prototype.__proto__ = Question.prototype; 101 | 102 | ConfirmModal.prototype.type = "confirmmodal"; 103 | 104 | module.exports = ConfirmModal; 105 | 106 | // vim: set ts=2 sw=2: 107 | -------------------------------------------------------------------------------- /widgets/ExecCommandDialog.js: -------------------------------------------------------------------------------- 1 | var child_process = require("child_process"); 2 | var blessed = require("blessed"); 3 | var lodash = require("lodash"); 4 | var Node = blessed.Node; 5 | var Element = blessed.Element; 6 | var UIEvents = require("./UIEvents"); 7 | 8 | 9 | function ExecCommandDialog(options) { 10 | if (!(this instanceof Node)) { 11 | return new ExecCommandDialog(options); 12 | } 13 | options = lodash.assign({ 14 | top: "center", 15 | left: "center", 16 | width: "80%", 17 | height: "80%", 18 | border: {type: "line"}, 19 | style: { 20 | bg: "black" 21 | } 22 | }, options || {}); 23 | Element.call(this, options); 24 | // child components 25 | this._.quit = blessed.Button(lodash.assign({ 26 | parent: this, 27 | top: 1, 28 | right: 2, 29 | shrink: true, 30 | content: " quit", 31 | width: 8, 32 | style: { 33 | focus: { 34 | fg: "white", 35 | bg: "blue", 36 | bold: true 37 | } 38 | } 39 | }, options.buttonOptions || {})); 40 | this._.body = blessed.Text({ 41 | parent: this, 42 | top: 3, 43 | right: 2, 44 | bottom: 1, 45 | left: 2, 46 | style: { 47 | bg: "black", 48 | fg: "white" 49 | }, 50 | scrollable: true 51 | }); 52 | this.bindEvents(); 53 | } 54 | 55 | ExecCommandDialog.prototype.bindEvents = function(){ 56 | var self = this; 57 | var quitBtn = self._.quit; 58 | self.on("attach", function(){ 59 | self.key("tab", onTab); 60 | quitBtn.on("press", onQuit); 61 | }); 62 | self.on("detach", function(){ 63 | self.unkey("tab", onTab); 64 | quitBtn.un("press", onQuit); 65 | }); 66 | function onTab(){ 67 | self._.quit.focus(); 68 | } 69 | function onQuit(){ 70 | self.hide(); 71 | UIEvents.emit("hide-cmddialog"); 72 | self.screen.render(); 73 | } 74 | }; 75 | 76 | ExecCommandDialog.prototype.exec = function(cmd, args, options){ 77 | var self = this; 78 | var quitElement = self._.quit; 79 | var bodyElement = self._.body; 80 | var p = child_process.spawn(cmd, args, options); 81 | var output = ""; 82 | bodyElement.setContent(output); 83 | p.on("close", function(){ 84 | quitElement.focus(); 85 | UIEvents.emit("pclose-cmddialog"); 86 | self.screen.render(); 87 | }); 88 | p.stdout.on("data", onData); 89 | p.stderr.on("data", onData); 90 | self.focus(); 91 | self.screen.render(); 92 | function onData(buf){ 93 | output += buf.toString(); 94 | bodyElement.setContent(output); 95 | bodyElement.setScrollPerc(100); 96 | self.screen.render(); 97 | } 98 | }; 99 | 100 | ExecCommandDialog.prototype.show = function(){ 101 | var superShow = Element.prototype.show; 102 | UIEvents.emit("show-cmddialog"); 103 | superShow.call(this); 104 | }; 105 | 106 | ExecCommandDialog.prototype.__proto__ = Element.prototype; 107 | 108 | ExecCommandDialog.prototype.type = "execcommanddialog"; 109 | 110 | module.exports = ExecCommandDialog; 111 | // vim: set ts=2 sw=2: 112 | -------------------------------------------------------------------------------- /widgets/HelpDialog.js: -------------------------------------------------------------------------------- 1 | var blessed = require("blessed"); 2 | var Node = blessed.Node; 3 | var Box = blessed.Box; 4 | 5 | function HelpDialog(options) { 6 | if (!(this instanceof Node)) { 7 | return new HelpDialog(options); 8 | } 9 | options = options || {}; 10 | options.hidden = true; 11 | Box.call(this, options); 12 | var helpText = [ 13 | "{cyan-fg}", 14 | " (a-z)", 15 | " jump to first package whose", 16 | " name starts with \n", 17 | " tab or space:", 18 | " jump to next package\n", 19 | " shift + tab or shift + space:", 20 | " jump to prev package\n", 21 | " enter", 22 | " install selected package\n", 23 | " backspace", 24 | " remove selected package\n", 25 | " shift + q", 26 | " exit program", 27 | "{/cyan-fg}" 28 | ].join("\n"); 29 | blessed.Text({ 30 | parent: this, 31 | top: "center", 32 | left: "center", 33 | shrink: true, 34 | tags: true, 35 | wrap: false, 36 | content: helpText 37 | }); 38 | this.bindEvents(); 39 | } 40 | 41 | HelpDialog.prototype.help = function(){ 42 | this.show(); 43 | this.focus(); 44 | this.setIndex(-1); 45 | this.screen.render(); 46 | }; 47 | 48 | HelpDialog.prototype.bindEvents = function(){ 49 | this.on("attach", function(){ 50 | this.key(["escape", "?"], onESC); 51 | }); 52 | this.on("detach", function(){ 53 | this.unkey(["escape", "?"], onESC); 54 | }); 55 | function onESC(){ 56 | if(this.hidden) { 57 | return; 58 | } 59 | this.hide(); 60 | this.screen.render(); 61 | } 62 | }; 63 | 64 | HelpDialog.prototype.__proto__ = Box.prototype; 65 | 66 | HelpDialog.prototype.type = "helpdialog"; 67 | 68 | module.exports = HelpDialog; 69 | // vim: set sw=2 ts=2: 70 | -------------------------------------------------------------------------------- /widgets/PackageInfo.js: -------------------------------------------------------------------------------- 1 | var blessed = require("blessed"); 2 | var Node = blessed.Node; 3 | var Element = blessed.Element; 4 | var UIEvents = require("./UIEvents"); 5 | 6 | var utils = require("../utils"); 7 | 8 | function PackageInfo(options) { 9 | if (!(this instanceof Node)) { 10 | return new PackageInfo(options); 11 | } 12 | options = options || {}; 13 | options.tags = true; 14 | Element.call(this, options); 15 | this.bindEvents(); 16 | } 17 | 18 | PackageInfo.prototype.showPackage = function(pkg){ 19 | var self = this; 20 | // TODO: show pkg files 21 | utils.getPackageInfo(pkg.name).then(function(info){ 22 | info = "Installed: " + !!pkg.installed + "\n" + info; 23 | self.setContent(self.formatInfo(info)); 24 | self.screen.render(); 25 | }); 26 | }; 27 | 28 | PackageInfo.prototype.formatInfo = function(info){ 29 | var lines = info.trim().split(/\r?\n/g); 30 | return lines.map(function(line){ 31 | var pos = line.indexOf(": "); 32 | if(pos === -1) { 33 | return line; 34 | } 35 | var field = line.substring(0, pos); 36 | var value = line.substring(pos + 2); 37 | return "{bold}" + field + "{/bold}: " + value; 38 | }).join("\n"); 39 | }; 40 | 41 | PackageInfo.prototype.bindEvents = function(){ 42 | var onSelectPackage = this.showPackage.bind(this); 43 | this.on("attach", function(){ 44 | UIEvents.on("select-package", onSelectPackage); 45 | }); 46 | this.on("detach", function(){ 47 | UIEvents.removeListener("select-package", onSelectPackage); 48 | }); 49 | }; 50 | 51 | PackageInfo.prototype.__proto__ = Element.prototype; 52 | 53 | PackageInfo.prototype.type = "packageinfo"; 54 | 55 | module.exports = PackageInfo; 56 | 57 | // vim: ts=2 sw=2: 58 | -------------------------------------------------------------------------------- /widgets/PackageList.js: -------------------------------------------------------------------------------- 1 | var blessed = require("blessed"); 2 | var Node = blessed.Node; 3 | var List = blessed.List; 4 | var ConfirmModal = require("./ConfirmModal"); 5 | var ExecCommandDialog = require("./ExecCommandDialog"); 6 | var HelpDialog = require("./HelpDialog"); 7 | var UIEvents = require("./UIEvents"); 8 | 9 | var utils = require("../utils"); 10 | 11 | function PackageList(options) { 12 | if (!(this instanceof Node)) { 13 | return new PackageList(options); 14 | } 15 | options = options || {}; 16 | List.call(this, options); 17 | this._initConfirmModal(); 18 | this._initCommandDialog(); 19 | this._initHelp(); 20 | this.bindEvents(); 21 | this._readPackages(); 22 | } 23 | 24 | PackageList.prototype.bindEvents = function(){ 25 | this.on("attach", function(){ 26 | // bind navigate keys 27 | this.key("abcdefghijklmnopqrstuvwxyz".split(""), onLetterKey); 28 | this.key(["home", "end", "left", "up", "right", "down"], onDirectionKey); 29 | this.key(["tab", "space"], onDownKey); 30 | this.key(["S-tab", "S-space"], onUpKey); 31 | this.key("backspace", onRemoveKey); 32 | this.key("enter", onInstallKey); 33 | this.key("?", onHelp); 34 | }); 35 | this.on("detach", function(){ 36 | // unbind navigate keys 37 | this.unkey("abcdefghijklmnopqrstuvwxyz".split(""), onLetterKey); 38 | this.unkey(["home", "end", "left", "up", "right", "down"], onDirectionKey); 39 | this.unkey(["tab", "space"], onDownKey); 40 | this.unkey(["S-tab", "S-space"], onUpKey); 41 | this.unkey("backspace", onRemoveKey); 42 | this.unkey("enter", onInstallKey); 43 | this.unkey("?", onHelp); 44 | }); 45 | this.on("select item", function(item){ 46 | var pkg = this.allPackages[item.content]; 47 | UIEvents.emit("select-package", pkg); 48 | }); 49 | 50 | function onLetterKey(ch){ 51 | var hit = false; 52 | var self = this; 53 | self.ritems.forEach(function(text, index){ 54 | if(hit) { 55 | return; 56 | } 57 | if(text.indexOf(ch) === 0) { 58 | hit = true; 59 | self.select(index); 60 | } 61 | }); 62 | } 63 | function onDirectionKey(ch, key){ 64 | switch(key.name) { 65 | case "home": 66 | this.select(0); 67 | break; 68 | case "end": 69 | this.select(this.ritems.length > 0 ? this.ritems.length - 1 : 0); 70 | break; 71 | case "left": 72 | case "up": 73 | this.up(); 74 | break; 75 | case "right": 76 | case "down": 77 | this.down(); 78 | break; 79 | } 80 | } 81 | function onDownKey(){ 82 | this.down(); 83 | } 84 | function onUpKey(){ 85 | this.up() 86 | } 87 | // remove 88 | function onRemoveKey(){ 89 | var self = this; 90 | var name = self.getItem(self.selected).content; 91 | var msg = "Are you sure you want to remove `" + name + "` ?"; 92 | self.confirm(msg).then(function(result){ 93 | self.focus(); 94 | if(result) { 95 | self.removePackage(name); 96 | } 97 | }, function(){}); 98 | } 99 | // install 100 | function onInstallKey(){ 101 | var self = this; 102 | var name = self.getItem(self.selected).content; 103 | var msg = "Are you sure you want to install `" + name + "` ?"; 104 | self.confirm(msg).then(function(result){ 105 | self.focus(); 106 | if(result) { 107 | self.installPackage(name); 108 | } 109 | }, function(){}); 110 | } 111 | // show help 112 | function onHelp(){ 113 | this._.helpDialog.help(); 114 | } 115 | }; 116 | 117 | PackageList.prototype.installPackage = function(name){ 118 | var title = "installing package: " + name; 119 | this.syscall(title, "apt", ["install", name, "-y"], {}); 120 | }; 121 | 122 | PackageList.prototype.removePackage = function(name){ 123 | var title = "removing package: " + name 124 | this.syscall(title, "apt", ["remove", name, "-y"], {}); 125 | }; 126 | 127 | PackageList.prototype.confirm = function(msg){ 128 | var self = this; 129 | return new Promise(function(resolve, reject){ 130 | self._.modal.ask(msg, function(err, result){ 131 | if(err) { 132 | reject(err); 133 | } else { 134 | resolve(result); 135 | } 136 | }); 137 | self.screen.render(); 138 | }); 139 | }; 140 | 141 | PackageList.prototype.syscall = function(title, cmd, args, options){ 142 | var dialog = this._.cmdDialog; 143 | dialog.setLabel(title); 144 | dialog.exec(cmd, args, options); 145 | dialog.show(); 146 | dialog.setIndex(-1); 147 | this.screen.render(); 148 | }; 149 | 150 | PackageList.prototype._initConfirmModal = function(){ 151 | this._.modal = new ConfirmModal(); 152 | this.screen.append(this._.modal); 153 | }; 154 | 155 | PackageList.prototype._initCommandDialog = function(){ 156 | var dialog = this._.cmdDialog = new ExecCommandDialog({ 157 | hidden: true 158 | }); 159 | this.screen.append(dialog); 160 | dialog.on("hide", function(){ 161 | this.focus(); 162 | }.bind(this)); 163 | }; 164 | 165 | PackageList.prototype._initHelp = function(){ 166 | var dialog = this._.helpDialog = new HelpDialog(); 167 | this.screen.append(dialog); 168 | dialog.on("hide", function(){ 169 | this.focus(); 170 | }.bind(this)); 171 | }; 172 | 173 | PackageList.prototype._readPackages = function(){ 174 | var self = this; 175 | utils.listPackages().then(function(pkgs){ 176 | var items = Object.keys(pkgs); 177 | self.allPackages = pkgs; 178 | self.setItems(items); 179 | self.screen.render(); 180 | }); 181 | }; 182 | 183 | PackageList.prototype.__proto__ = List.prototype; 184 | 185 | PackageList.prototype.type = "packagelist"; 186 | 187 | module.exports = PackageList; 188 | // vim: set ts=2 sw=2: 189 | -------------------------------------------------------------------------------- /widgets/StatusLine.js: -------------------------------------------------------------------------------- 1 | var blessed = require("blessed"); 2 | var Node = blessed.Node; 3 | var Element = blessed.Element; 4 | var UIEvents = require("./UIEvents"); 5 | 6 | function StatusLine(options) { 7 | if (!(this instanceof Node)) { 8 | return new StatusLine(options); 9 | } 10 | options = options || {}; 11 | options.tags = true; 12 | Element.call(this, options); 13 | this.bindEvents(); 14 | this.setPackage(null); 15 | } 16 | 17 | StatusLine.prototype.setPackage = function(pkg){ 18 | var text = ""; 19 | if(pkg) { 20 | text += pkg.name + (pkg.installed ? "[installed]" : ""); 21 | } 22 | text += "{|}press ? for help"; 23 | this.setContent(text); 24 | this.set("lastPackage", pkg); 25 | }; 26 | 27 | StatusLine.prototype.onShowModal = function(){ 28 | this.setContent("{right}tab: switch button, space: click button, esc: hide{/right}"); 29 | }; 30 | StatusLine.prototype.onHideModal = function(){ 31 | this.setPackage(this.get("lastPackage")); 32 | }; 33 | 34 | StatusLine.prototype.onShowCommandDialog = function(){ 35 | this.setContent("{right}please wait while \"quit\" is not blue{/right}"); 36 | }; 37 | StatusLine.prototype.onHideCommandDialog = function(){ 38 | this.setPackage(this.get("lastPackage")); 39 | }; 40 | StatusLine.prototype.onOperationDone = function(){ 41 | this.setContent("{right}press enter to close dialog.{/right}"); 42 | }; 43 | 44 | StatusLine.prototype.bindEvents = function(){ 45 | var onSelectPackage = this.setPackage.bind(this); 46 | var onShowModal = this.onShowModal.bind(this); 47 | var onHideModal = this.onHideModal.bind(this); 48 | var onShowCommandDialog = this.onShowCommandDialog.bind(this); 49 | var onHideCommandDialog = this.onHideCommandDialog.bind(this); 50 | var onOperationDone = this.onOperationDone.bind(this); 51 | this.on("attach", function(){ 52 | UIEvents.on("select-package", onSelectPackage); 53 | UIEvents.on("show-modal", onShowModal); 54 | UIEvents.on("hide-modal", onHideModal); 55 | UIEvents.on("show-cmddialog", onShowCommandDialog); 56 | UIEvents.on("hide-cmddialog", onHideCommandDialog); 57 | UIEvents.on("pclose-cmddialog", onOperationDone); 58 | }); 59 | this.on("detach", function(){ 60 | UIEvents.removeListener("select-package", onSelectPackage); 61 | UIEvents.removeListener("show-modal", onShowModal); 62 | UIEvents.removeListener("hide-modal", onHideModal); 63 | UIEvents.removeListener("show-cmddialog", onShowCommandDialog); 64 | UIEvents.removeListener("hide-cmddialog", onHideCommandDialog); 65 | UIEvents.removeListener("pclose-cmddialog", onOperationDone); 66 | }); 67 | }; 68 | 69 | StatusLine.prototype.__proto__ = Element.prototype; 70 | 71 | StatusLine.prototype.type = "statusline"; 72 | 73 | module.exports = StatusLine; 74 | // vim: set sw=2 ts=2: 75 | -------------------------------------------------------------------------------- /widgets/UIEvents.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require("events").EventEmitter; 2 | 3 | /** 4 | * shared events object for widgets in same directory 5 | * 6 | * events: 7 | * 8 | * select-package 9 | * show-modal 10 | * hide-modal 11 | * show-cmddialog 12 | * hide-cmddialog 13 | */ 14 | module.exports = new EventEmitter(); 15 | -------------------------------------------------------------------------------- /widgets/oo-sample.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 这个仅仅作为示例文件 3 | * 它提供了了 blessed 风格的面向对象类的写法 4 | * 有任何新增组件,都基于这个风格修改 5 | */ 6 | var blessed = require("blessed"); 7 | var Node = blessed.Node; 8 | var Element = blessed.Element; 9 | 10 | function Sample(options) { 11 | if (!(this instanceof Node)) { 12 | return new Sample(options); 13 | } 14 | options = options || {}; 15 | Element.call(this, options); 16 | } 17 | 18 | Sample.prototype.__proto__ = Element.prototype; 19 | 20 | Sample.prototype.type = "sample"; 21 | 22 | module.exports = Sample; 23 | --------------------------------------------------------------------------------