UNPKG

@nasc/termtools

Version:

Easy to customize, uses the power of both JavaScript and Bash to add a bunch of _aliases_ and extra funcionality for your bash profile.

561 lines (501 loc) 18.7 kB
#!/usr/bin/env node /** * This script is triggered on PROMPT_COMMAND every time PS1 is about to be * rendered. We will output here, what will be shown in PS1 * * This is a mess! */ // let's import some useful dependencies // besides chalk, used for colors, everything else is native from node const path = require('path') const fs = require('fs') const execSync = require('child_process').execSync let colors = require('./colors') // we will have to enforce chalk to use colors // as it is running in a second level command, it will turn them off by default colors.enabled = true colors.level = 3 // we will also need to parse/escape the unprintable characters // otherwise, the PS1 will act really weird with long lines and command history colors.wrapper = { pre: '\\[', post: '\\]', } // also, let's defined some constants const DEFAULT_ELLIPSIS_SIZE = 4 const DEFAULT_MAX_PATH_LENGTH = 40 // sadly enough...the only way we could get the arguments from bash was from // args passed to the node process :/ const IS_DEBUGGING = process.argv[1] === 'inspect' const ARG_POSITION = IS_DEBUGGING ? 1 : 0 // the path where node we find the node executable file const NODE_BIN = process.argv[0] // the profiler js file (index.js in this project) const PROFILER_JS_PATH = process.argv[ARG_POSITION + 2] // the profiler js file (index.sh in this project) const PROFILER_SH_PATH = process.argv[ARG_POSITION + 3] // we will use this to find files from within this project const DIR_NAME = path.dirname(PROFILER_JS_PATH) // is it running under a TTY environment? const IS_TTY = process.argv[ARG_POSITION + 4] == '1' ? true : false // the session type const SESSION_TYPE = process.argv[ARG_POSITION + 7] // let's look for some of the arguments, from the other way around const ARGVLength = process.argv.length // the current time itself const TIME = process.argv[ARGVLength - 3] // the real user name const USER = process.argv[ARGVLength - 2] // and if the current user is root const IS_ROOT = USER == 'root' // we will also use some useful info on the current battery status // updated every 10 seconds const BATTERY = parseInt(process.argv[ARGVLength - 4], 10) const IS_CHARGING = true //parseInt(process.argv[ARGVLength - 5], 10) // if user is root, we will not be able to update it from line to line (unless // we had changed the root profile as well, what would require sudo permission // and I was not much up to do so) const IS_WRITABLE = IS_ROOT ? 1 : parseInt(process.argv[ARGVLength - 6], 10) /** * This parses the gitinfo into git branch, status and symbol * * The status may be from -2 to 5, meaning: * -2: COMMITS DIVERGED * -1: COMMITS BEHIND * 0: NO CHANGES * 1: COMMITS AHEAD * 2: UNTRACKED CHANGES * 3: CHANGES TO BE COMMITTED * 4: LOCAL AND UNTRACKED CHANGES * 5: LOCAL CHANGES * * Symbols can be: * "-": COMMITS BEHIND * "+": COMMITS AHEAD * "!": COMMITS DIVERGED * "*": UNTRACKED * "": Anything else */ let GIT_INFO = IS_ROOT ? [] : process.argv[ARGVLength - 7].split('@@@') const GIT_BRANCH = GIT_INFO[0] || '' const GIT_STATUS = parseInt(GIT_INFO[1], 10) || '' const GIT_SYMBOL = GIT_INFO[2] || '' // this will be 1 when there are imported user customizations const USE_CUSTOM_SETTINGS = process.argv[ARGVLength - 1] == 1 // the machine/host name const HOST_NAME = process.argv[ARG_POSITION + 5].replace(/\.[^\.]+$/, '') // this is the current user's home path const HOME = process.argv[ARG_POSITION + 6] // in case we will show the machine ip const IP = process.argv[ARG_POSITION + 7] // this is the real string we will use ahead, to show the hostname const MACHINE = `${IS_ROOT ? '\\h' : HOST_NAME}` // we will separate the basename from the rest of the path, and show them as // two distinct sections // but, again, as we will not be able to update this during sudo navigation // we will use only one section, the path, with the full address let BASENAME = IS_ROOT ? '' : path.basename(process.cwd()).toString() const PATH = IS_ROOT ? ['\\w '] : path.dirname(process.cwd().replace(HOME, ' ~')).split(path.sep) // node returns `path.dirname('~')` as "." instead of "~" if (!IS_ROOT && PATH.join('') === '.') { BASENAME = '~' } // this is the resulting context for the current state const context = { IS_TTY, IS_ROOT, IP, BATTERY, GIT_STATUS, GIT_BRANCH, GIT_SYMBOL, IS_WRITABLE, IS_CHARGING, colors, storage: getStorage() } // time to start preparing the PS1 itself let SETTINGS = {} // if the user has custom settings... // (we have set this flag only once, so we can avoid looking for this file // everytime we have to update the PS1, unless the user has customizations) if (USE_CUSTOM_SETTINGS) { try { // users may export a function, or a straight object // let custom = require('./custom-user-settings.js') let custom = require(path.resolve(HOME, '.bash_profile.js')) if (typeof custom == 'function') { custom = custom(context) } SETTINGS = custom } catch (e) { if (e.message.indexOf('Cannot find module') < 0) { console.log(colors.red('[x] ') + 'Failed importing settings!\n' + e.message) } } } // here, we will require the default settings we have and will run it (as it // is a function) using the current state (with everything we parsed so far) let theme = SETTINGS.extends || 'default' try { theme = require(`./themes/${theme}.js`, 'utf8')(context) } catch (e) { console.error('Invalid theme!\nPlease, select one of the following:') let availableThemes = execSync(`ls ${DIR_NAME}/themes/`) .toString() .split('\n') .map(theme => theme.replace(/\.js$/, '')) .filter(theme => theme.length) console.log('\n - ' + availableThemes.join('\n - ') + '\n') console.log('Loading default theme instead') theme = require(`./themes/default.js`, 'utf8')(context) } // then, we will have to merge the current default options with // the user's customization SETTINGS.ps1 = SETTINGS.ps1 || {} SETTINGS.ps1.parts = Object.assign({}, theme.ps1.parts, SETTINGS.ps1.parts || {}) SETTINGS.decorators = Object.assign({}, theme.decorators, SETTINGS.decorators || {}) SETTINGS.ps1.effects = Object.assign({}, theme.ps1.effects, SETTINGS.ps1.effects || {}) const sectionSeparator = SETTINGS.decorators.section // these are the variables available to be used as parts of the PS1 string // we will use the parts from SETTINGS, only if they exist here const VARS = { string: '', time: getWrapper('time', IS_ROOT ? ' \\t ' : ` ${TIME} `), machine: getWrapper('machine', `${MACHINE}`), basename: getWrapper('basename', `${BASENAME || (IS_ROOT ? '': ' / ')}`), path: getPath, os: getOS, weather: getWeather, entry: getWrapper('entry', ''), readOnly: getWrapper('readOnly', IS_WRITABLE ? '' : SETTINGS.decorators.readOnly || 'R'), // 🔒🔐👁 separator: sectionSeparator, git: getWrapper('git', `${SETTINGS.decorators.git}${GIT_BRANCH}${GIT_SYMBOL}`), // ⑂ᛘ⎇ gitStatus: GIT_STATUS, battery: getWrapper('battery', `${IS_CHARGING ? '⚡ ' : '◒ '}${BATTERY}`), userName: getWrapper('userName', USER) } // let's start by having the PS1Parts list empty let PS1Parts = [] // this map will help us quickly find the effects for each part of PS1 const effectsMap = new Map() // SETTINGS is now the merge between the default settings and the user's customizations for (let partName in SETTINGS.ps1.parts) { let tmp = '' // remember that? We will not use the basename for sudos if (IS_ROOT && partName == 'basename') { continue } // the part itself, with its options let part = SETTINGS.ps1.parts[partName] part.name = partName // obviously... if (!part.enabled) { continue } // in case the current part is "string" or "entry", we will output that part // using their respective effects, adding their contents if (partName === 'string' || partName === 'entry' || !VARS[partName]) { tmp += applyEffects(part, getWrapper(partName, part.content)) } else { // for any other kind of part, we check if they exist in our valid list if (VARS[partName]) { // and, if so, // that may be either a string of a function that will return a string let value = VARS[partName] if (typeof value === 'function') { value = getWrapper(partName, value(part)) } // finally, we add the effects to it tmp += applyEffects(part, value) } } // if there is something to add... if (tmp) { // we set it in our map, for later use if (SETTINGS.ps1.effects[part.name]) { effectsMap.set(tmp, { fx: SETTINGS.ps1.effects[part.name], part }) } PS1Parts.push(tmp) } } /** * Time to think about the separators! * * The hard part here is their logic. * For example: * the separator "" need to have the TEXT COLOR of the current section BGCOLOR, * and the BGCOLOR of the NEXT section's BGCOLOR. */ PS1Parts = PS1Parts.map((part, i) => { // let curPart = PS1Parts[i] let nextPart = PS1Parts[i + 1] || null let curFx = effectsMap.get(part) let nextFx = effectsMap.get(nextPart) let sep = sectionSeparator if (!nextPart && curFx && curFx.fx && curFx.fx.bgColor) { // this is the end of the PS1 return part + colorNameParser(colors, curFx.fx.bgColor)(sep) } if (!nextFx || !isSection(nextFx.part)) { return part } // if the current section does not have any effects, we will // not use any separator // if separator was set to FALSE in that part, we will not show it either if (curFx && curFx.fx.separator !== false) { if (curFx.fx && curFx.fx.bgColor) { sep = colorNameParser(colors, curFx.fx.bgColor)(sep) } else { return part } } else { // for separator to work, we need at least a background return part } if (nextFx) { if (nextFx.fx && nextFx.fx.bgColor) { sep = colorNameParser(colors, nextFx.fx.bgColor, 'bg')(sep) } } return part + sep }).join('') /****************************************************************************** * HERE IS WHERE THE MAGIC HAPPENS! * not like that...just prints the result for the current context and * PS1 will use our output *****************************************************************************/ process.stdout.write(PS1Parts + colors.reset()) /****************************************************************************** * Bellow here, we can find the functions we used above * long live hoisting! *****************************************************************************/ /** * Gets the configured wrapper for the content */ function getWrapper (partName, content) { let part = SETTINGS.ps1.parts[partName] if (part) { if (part.wrapper) { return part.wrapper.replace(/\$1/, content.toString()) } } return content.toString() } // we can use nonSections to avoid section separators function isSection (part) { const nonSections = new Set([/*'basename'/*, 'entry'*/]) return !nonSections.has(part.name) } /** * Get the current path with separators. * * This function will treat the path parts and add separators and * effects as specified in the current context settings. * * @param {object} opts The options for how to decorate the path */ function getPath (opts = {}) { let str = '' let thePATH = Array.from(PATH) let sep = SETTINGS.decorators.pathSeparator || path.sep if (thePATH[0] === '.' || thePATH[0] === '~') { thePATH[0] = '' } if (thePATH[0] === '') { if (thePATH[1]) { thePATH[1] = path.sep + thePATH[1] thePATH.shift() } } if (thePATH.join('') === '') { return ' ' } if (opts.ellipsis && !opts.cut) { let ellipsisSize = opts.ellipsis === true ? DEFAULT_ELLIPSIS_SIZE : opts.ellipsis let last = thePATH.length - 1 thePATH = thePATH.map((dir, i) => { // if last part of the path, we will not ellipse it if (!i || i === last) { return dir } if (dir.length > ellipsisSize + 1) { dir = dir.substr(0, ellipsisSize) + '…' } return dir }) } thePath = thePATH.join(sep) thePath = thePath === sep ? '' : thePath let l = opts.maxLength || DEFAULT_MAX_PATH_LENGTH if (opts.cut && thePath.length > l - 6) { if (opts.cut === 'center') { l = l / 2 -3 thePath = thePath.slice(0, l) + '...' + thePath.slice(-1 * l) } if (opts.cut === 'left') { l = l - 4 thePath = ' …' + thePath.slice(-1 * l) } if (opts.cut === 'right') { l = l - 4 thePath = thePath.slice(0, l) + '… ' } } return thePath.length ? thePath + '' : '' } /** * This function parses the colors * * It will not only parse the colors themselves, but will also parse the effect * name. For example, "redBright", when used with the prefix "bg", becomes "bgRedBright". * Noticed the camel case? * Also, just for fun, there is a mix with gray, grey and blackBright when used with * or without "bg". * * @param {Chalk} applier An instance of chalk. May already have effects applied to it * @param {String} color The color itself. May be a valid color name of an RGB code in hex starting with "#" * @param {String} prefix Optionally, adds a prefix to the effect function. Mainly used for prefixing colors with "bg" */ function colorNameParser (applier, color, prefix) { if (color.startsWith('#')) { if (prefix) { return applier[prefix + 'Hex'](color) } else { return applier.hex(color) } } else { if (prefix) { if (color === 'gray' || color === 'grey') { color = 'blackBright' } color = prefix + color[0].toUpperCase() + color.substr(1) } else if (color === 'blackBright') { color = 'gray' } if (applier[color]) { return applier[color] } return arg => arg } } /** * This function will run through the parts of PS1 and apply their respective effects * dealing also with their separators. */ function applyEffects (part, str) { let fx = SETTINGS.ps1.effects[part.name] let applier = colors if (fx) { if (fx.bgColor) { str = colorNameParser(applier, fx.bgColor, 'bg')(str) } if (fx.color) { str = colorNameParser(applier, fx.color)(str) } if (fx.bold) { str = colors.bold(str) } if (fx.italic) { str = colors.italic(str) } if (fx.dim) { str = colors.dim(str) } if (fx.underline) { str = colors.underline(str) } return str } return str } function getOS (opts = {}) { const osType = require('os').type().toLowerCase() const OS_TYPE = osType == 'linux' ? '\ue712' : osType == 'darwin' ? '\ue711' : '\ue70f' const ret = { name: osType, symbol: OS_TYPE, toString: function () { return this.symbol }} return ret } function getWeather (opts = {}) { if (!opts.city) { return '' } try { let lastWeather = context.storage.getItem('termtools-last-weather') // we will check for weather changes only once every 3 hours if (!lastWeather || lastWeather.lastCheck < (new Date).getTime() - 10800000) { const city = encodeURIComponent(opts.city.toLowerCase()) const WEATHER_SERVICE = `http://samples.openweathermap.org/data/2.5/forecast?q=${city}&appid=e52979c4fe80f8dbc823fad77212e0c9` const fetch = require('node-fetch') fetch(WEATHER_SERVICE).then((response) => { response.json().then(json => { let main = json.list[0].weather[0].main console.log(2, main) context.storage.setItem('termtools-last-weather', JSON.stringify({ lastCheck: (new Date).getTime(), main })) }) }).catch(err => { console.log(colors.red('[x] ' + 'Failed looking for weather data\n', err)) }) return '' } if (lastWeather) { if (typeof lastWeather !== 'object') { lastWeather = JSON.parse(lastWeather) } return getWeatherSymbol(lastWeather.main.toLowerCase()) } } catch (error) { console.log(colors.red('[x] ') + error) } return '' } function getWeatherSymbol (type) { const list = new Map() list.set(/thunderstorm(.+)heavy/, '⛈') list.set(/thunderstorm(.+)(rain|drizzle)/, '⛈') list.set(/heavy(.+)thunderstorm/, '⛈') list.set(/thunderstorm/, '🌩') list.set(/heavy(.+)?drizzle/, '☔') list.set(/drizzle/, '🌂') list.set(/(heavy|extreme|freezing|ragged)(.+)rain/, '🌧') list.set(/rain/, '🌦') list.set(/sleet/, '❅') list.set(/heavy(.+)snow/, '❆') list.set(/snow|hail/, '❄') list.set(/mist|fog|dust|sand|haze|ash/, '☁') list.set(/windy/, '🌬') list.set(/tornado|hurricane/, '🌪') list.set(/clouds/, '⛅') list.set(/clear/, '🌞') list.set(/sunny/, '🌞') let main = '⛅' iterator = list.entries() let item console.log(type) while(item = iterator.next()) { if (type.match(item.value[0])) { main = item.value[1] break } if (item.done) { break } } return main + ' ' } function getStorage () { if (typeof localStorage === "undefined" || localStorage === null) { const LocalStorage = require('node-localstorage').LocalStorage; localStorage = new LocalStorage(path.resolve(DIR_NAME, 'scratch')); return localStorage } return localStorage } // not sure will ever be used again...but once it worked on TTYs... // if we have any issue related to that again, we may come here and use it! // function fixTerminalColors (str) { // return unescape(escape(str).replace(/\%1B/i, '%1B')) // }