├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config.lua ├── example.lua ├── promptua.lua ├── providers ├── git.lua └── init.lua ├── searchpath.lua └── themes └── myeline.lua /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 📡 Promptua 2 | 9 | ## Providers 10 | Promptua doesn't have that much providers, but you can change that. A good way to 11 | contribute is to add a provider function. 12 | 13 | To do so, edit the [provider source file](../provider.lua). 14 | The `Providers` table there have all premade provider functions, which are in 15 | nested table. This is actually for better organization. 16 | 17 | To add, for example, a `dir.basename` provider you would have a basename table in 18 | the dir table. 19 | ```lua 20 | Providers = { 21 | dir = { 22 | path = function() 23 | -- ... 24 | end, 25 | -- our basename provider here 26 | basename = function() 27 | -- ... code here 28 | end 29 | } 30 | } 31 | ``` 32 | 33 | ## Bug Reports 34 | For bug reports, include the version of Promptua (`require 'promptua'.version`) 35 | and/or git commit hash if you just pulled the latest git commit. 36 | Also include ways to reproduce the bug. 37 | 38 | ## Code Contribution 39 | A contribution of code is simple: just make a pull request to the master branch. 40 | You *must* use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). 41 | If any commit doesn't follow this standard, the pull request will not be considered. 42 | If you made any breaking changes, be sure to document them. 43 | 44 | ### Code Style 45 | - Prefer omitting parens if a function takes a single string argument (like `print 'hi'`) 46 | - Use tabs 47 | - camelCase for function names 48 | 49 | ### Finding ways to contribute code 50 | Check out the [help wanted](https://github.com/TorchedSammy/Promptua/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22+) 51 | label to see anything Promptua needs help working on. 52 | 53 | The [up for grabs](https://github.com/TorchedSammy/Promptua/issues?q=is%3Aissue+is%3Aopen+label%3A%22up+for+grabs%22+) 54 | label is also available for easy issues. Have fun! 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 TorchedSammy 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 | # Promptua 2 | > 📡 A customizable prompt "engine" for Hilbish. 3 | 4 | Promptua is a custom prompt builder/engine for Hilbish. It allows you to easily 5 | create good looking prompts. For its use, it takes inspiration from 6 | [oh-my-posh](https://ohmyposh.dev/) and Galaxyline for Neovim. 7 | 8 | # Install 9 | > Promptua requires Hilbish v2.0+ 10 | 11 | Clone to a Hilbish library directory: 12 | 13 | ``` 14 | git clone --depth 1 https://github.com/TorchedSammy/Promptua ~/.local/share/hilbish/libs/promptua 15 | ``` 16 | 17 | # Usage 18 | To make a Promptua prompt, you have to make a proper theme. 19 | A theme is a table of [segments](#segments). 20 | 21 | ## Example 22 | ```lua 23 | local promptua = require 'promptua' 24 | 25 | local theme = { 26 | { 27 | provider = 'dir.path', 28 | style = 'blue', 29 | }, 30 | { 31 | provider = 'prompt.icon', 32 | style = 'green' 33 | } 34 | } 35 | 36 | promptua.setTheme(theme) 37 | promptua.init() 38 | ``` 39 | 40 | ## Segments 41 | A segment is a table which has at least a `provider` key, which shows the info in the segment. 42 | It can consist of the following keys: 43 | 44 | ```lua 45 | { 46 | provider = '', 47 | separator = ' ', 48 | condition = function() end, 49 | icon = '', 50 | style = '', 51 | format = '@style@icon@info' 52 | } 53 | ``` 54 | 55 | - `provider`: A function or string which *provides* the info for that segment. A string 56 | provider will use one of the [premade providers](#premade-providers). A function 57 | provider is passed the segment itself to set default values. If you are creating a theme, 58 | it is preferred to set `segment.defaults` for the default values in a segment. 59 | 60 | - `condition`: A function which will determine whether to show the segment in the prompt. 61 | If it returns false, the provider will not be run and the segment will be skipped, 62 | 63 | - `style`: The color or general style of the info in the segment. 64 | It is space separated and follows the naming of Lunacolors, which you can 65 | find more info on by running `doc lunacolors`. The following styles are 66 | available: 67 | - Colors: black, red, green, blue, yellow, magenta, cyan, white. 68 | - Modifiers: dim, italic, underline, invert. 69 | 70 | ### Premade Providers 71 | - `dir.path` - Path of current directory 72 | - `git.branch` - Git branch 73 | - `git.dirty` - Icon if local git has unpushed changes 74 | - `prompt.icon` - Main prompt icon 75 | - `prompt.failSuccess` - Prompt icon based on exit code of command 76 | - `command.execTime` - Time it took to run a command (hidden if 0s) 77 | - `user.name` - Username 78 | - `user.hostname` - Hostname of machine 79 | 80 | ## Using Premade Themes 81 | If you do not want to create a prompt on your own, you can look at the 82 | [premade themes](themes/) included in this repository. These themes 83 | can be used by just passing the name to the setTheme function like: 84 | `promptua.setTheme 'myeline'` 85 | 86 | ## Config 87 | If needed, you can change the default separator or format for segments, 88 | or have configuration for certain providers via the config. 89 | 90 | Promptua has a default which looks like: 91 | 92 | ```lua 93 | { 94 | format = '@style@icon@text', 95 | separator = ' ', 96 | prompt = { 97 | icon = '%', 98 | fail = '!', 99 | success = '%' 100 | } 101 | } 102 | ``` 103 | 104 | # Contributing 105 | If you want to contribute, read the [CONTRIBUTING.md](CONTRIBUTING.md) file to find 106 | our guidelines, and easy ways to contribute. 107 | 108 | # License 109 | MIT 110 | 111 | -------------------------------------------------------------------------------- /config.lua: -------------------------------------------------------------------------------- 1 | local defaults = { 2 | format = '@style@icon@info', 3 | separator = ' ', 4 | prompt = { 5 | icon = '%', 6 | } 7 | } 8 | local conf = {} 9 | local M = { 10 | set = function(c) 11 | conf = c 12 | end 13 | } 14 | 15 | return setmetatable(M, { 16 | __index = function(_, k) 17 | return conf[k] or defaults[k] 18 | end 19 | }) 20 | -------------------------------------------------------------------------------- /example.lua: -------------------------------------------------------------------------------- 1 | local promptua = require 'promptua' 2 | 3 | local promptTheme = { 4 | { 5 | provider = 'dir.path', 6 | style = 'blue', 7 | }, 8 | { 9 | provider = 'git.branch', 10 | style = 'gray', 11 | separator = '' 12 | }, 13 | { 14 | provider = 'git.dirty', 15 | style = 'yellow' 16 | }, 17 | { 18 | provider = 'user.time', 19 | style = 'gray' 20 | }, 21 | { 22 | provider = 'command.execTime' 23 | }, 24 | { 25 | provider = 'prompt.failSuccess', 26 | separator = ' ' 27 | }, 28 | } 29 | 30 | promptua.setTheme(promptTheme) 31 | promptua.init() 32 | 33 | -------------------------------------------------------------------------------- /promptua.lua: -------------------------------------------------------------------------------- 1 | local bait = require 'bait' 2 | local lunacolors = require 'lunacolors' 3 | local providers = require 'promptua.providers' -- get Providers tables 4 | local config = require 'promptua.config' 5 | local searchpath = require 'promptua.searchpath' 6 | 7 | local M = { 8 | version = 'v0.4.0' 9 | } 10 | setmetatable(M, { 11 | __index = function(_, k) 12 | if k == 'config' then return config end 13 | end 14 | }) 15 | 16 | local function initProviders() 17 | M.providers = {} 18 | for k, v in pairs(providers) do 19 | for kk, vv in pairs(v) do 20 | M.providers[k .. '.' .. kk] = vv 21 | end 22 | end 23 | end 24 | 25 | local function callDefaultProvider(providerstr, segment) 26 | return M.providers[providerstr](segment) 27 | end 28 | 29 | local function loadTheme(thm) 30 | -- take a table (thm) and set M.prompt 31 | M.prompt = thm 32 | end 33 | 34 | -- fmt takes a string with format verbs and a style and returns a formatted string, 35 | -- with style applied 36 | local function fmt(formatstr, style, verbs) 37 | local formatted = formatstr:gsub('@%a+', function(v) 38 | -- if its @style use our style 39 | if v:sub(2) == 'style' then 40 | return style:gsub('%a+', function(key) 41 | if not lunacolors.formatColors[key] then 42 | return '' 43 | end 44 | return lunacolors.formatColors[key] 45 | end):gsub('%s+', '') 46 | end 47 | return verbs[v:sub(2)] or v 48 | end) 49 | 50 | return formatted 51 | end 52 | 53 | function M.setConfig(c) 54 | config.set(c) 55 | end 56 | 57 | -- Sets a Promptua theme. 58 | function M.setTheme(theme) 59 | if type(theme) == 'string' then 60 | local themeName = theme 61 | 62 | local themeFile = string.format('%s/promptua/themes/%s/theme.lua', hilbish.userDir.config, themeName) 63 | 64 | local ok, res = pcall(dofile, themeFile) 65 | theme = res 66 | if not ok then 67 | ok, theme = pcall(require, 'promptua.themes.' .. themeName) 68 | if not ok then 69 | error(string.format('promptua: error loading %s theme\n first error: %s', themeName, res)) 70 | return 71 | end 72 | end 73 | end 74 | 75 | loadTheme(theme) 76 | end 77 | 78 | local function handleCond(cond) 79 | if type(cond) == 'function' then 80 | if cond() then 81 | return true 82 | end 83 | elseif type(cond) == 'nil' then 84 | return true 85 | else 86 | error('promptua: invalid condition') 87 | end 88 | end 89 | 90 | function M.handlePrompt(code) 91 | if not code then code = 0 end 92 | local promptStr = '' 93 | for _, segment in pairs(M.prompt) do 94 | local cond = segment.condition 95 | local function handleSegment() 96 | local provider = segment.provider 97 | local info = '' 98 | 99 | if type(provider) == 'function' then 100 | info = provider(segment) 101 | elseif type(provider) == 'string' then 102 | info = callDefaultProvider(provider, segment) 103 | elseif provider == nil then 104 | info = '' 105 | else 106 | error('promptua: invalid provider') 107 | end 108 | local style = segment.style or segment.defaults.style 109 | local icon = segment.icon or segment.defaults.icon or '' 110 | local format = segment.format or segment.defaults.format or M.config.format 111 | local separator = segment.separator or segment.defaults.separator or M.config.separator 112 | local condition = segment.condition or segment.defaults.condition 113 | if not handleCond(condition) then return end 114 | 115 | if style then 116 | -- reason for info or is because some segments only set icon and no info 117 | local fmtbl = {info = info or '', icon = icon} 118 | if type(style) == 'string' then 119 | info = fmt(format, style, fmtbl) 120 | elseif type(style) == 'function' then 121 | info = fmt(format, style(segment), fmtbl) 122 | end 123 | end 124 | 125 | promptStr = promptStr .. info .. separator .. lunacolors.formatColors.reset 126 | end 127 | 128 | if handleCond(cond) then handleSegment() end 129 | end 130 | hilbish.prompt(promptStr) 131 | end 132 | 133 | function M.init() 134 | if not M.prompt then error 'promptua: no theme set' end 135 | -- add functions to segments in M.prompt 136 | for _, segment in pairs(M.prompt) do 137 | if type(segment.provider) == 'string' and not M.providers[segment.provider] then 138 | error(string.format('promptua: unknown provider %s', segment.provider)) 139 | end 140 | segment.defaults = {} 141 | end 142 | 143 | M.handlePrompt() 144 | bait.catch('command.exit', M.handlePrompt) 145 | end 146 | 147 | initProviders() 148 | M.setConfig {} 149 | return M 150 | -------------------------------------------------------------------------------- /providers/git.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.isRepo() 4 | -- just run rev-parse to see if it's a git repo 5 | local gitRepo = io.popen 'git rev-parse --git-dir' 6 | local getHeadfile = gitRepo:read('*a'):gsub('\n', '') 7 | gitRepo:close() 8 | local isRepo = not getHeadfile:match 'fatal: not a git repository' or false 9 | 10 | if isRepo then 11 | return true, getHeadfile 12 | else 13 | return false, '' 14 | end 15 | end 16 | 17 | function M.getBranch() 18 | local isRepo, getHeadfile = M.isRepo() 19 | if not isRepo then 20 | return nil 21 | end 22 | 23 | local headfile = io.open(getHeadfile .. '/HEAD') 24 | if not headfile then 25 | return nil 26 | end 27 | 28 | local inf = headfile:read '*a' 29 | headfile:close() 30 | -- there is a format like 'ref: refs/heads/master' 31 | -- so get just the branch name 32 | -- and handle detached head case 33 | local branch = inf:match 'ref: refs/heads/(.+)' or inf:match 'ref: (.+)' or inf:match '(.+)' 34 | 35 | -- remove newline 36 | if branch then branch = branch:gsub('\n', '') end 37 | 38 | -- return nil if no branch found 39 | -- its falsy if branch is empty string which is why the or works 40 | return branch or nil 41 | end 42 | 43 | function M.isDirty() 44 | local res = io.popen 'git status --porcelain | wc -l' 45 | local dirt = res:read():gsub('\n', '') 46 | res:close() 47 | 48 | return (dirt ~= '0') 49 | end 50 | 51 | -- check if local is ahead of remote 52 | -- will return nil if not a repo 53 | function M.aheadRemote() 54 | if not M.isRepo() then 55 | return nil 56 | end 57 | 58 | local res = io.popen 'git rev-list @..@{u}' 59 | local ahead = res:read():gsub('\n', '') 60 | res:close() 61 | 62 | return (ahead ~= '0') 63 | end 64 | 65 | -- check if local is behind remote 66 | -- will return nil if not a repo 67 | function M.behindRemote() 68 | if not M.isRepo() then 69 | return nil 70 | end 71 | 72 | local res = io.popen 'git rev-list @{u}..@' 73 | local behind = res:read():gsub('\n', '') 74 | res:close() 75 | 76 | return (behind ~= '0') 77 | end 78 | 79 | return M 80 | 81 | -------------------------------------------------------------------------------- /providers/init.lua: -------------------------------------------------------------------------------- 1 | local bait = require 'bait' 2 | local config = require 'promptua.config' 3 | local git = require 'promptua.providers.git' 4 | local execTime = nil 5 | 6 | return { 7 | dir = { 8 | path = function() 9 | return '%d' 10 | end, 11 | }, 12 | user = { 13 | name = function() 14 | return '%u' 15 | end, 16 | hostname = function() 17 | return '%h' 18 | end, 19 | time = function(segment) 20 | segment.defaults.icon = '🕙 ' 21 | -- get the time with lua's os.time() 22 | -- and convert it to a string 23 | return os.date('%I:%M:%S %p', os.time()) 24 | end, 25 | }, 26 | prompt = { 27 | icon = function () 28 | return config.prompt.icon 29 | end, 30 | failSuccess = function(segment) 31 | if hilbish.exitCode == 0 then 32 | -- defaults for success prompt 33 | segment.defaults = { 34 | style = 'green', 35 | icon = config.prompt.success or config.prompt.icon 36 | } 37 | else 38 | segment.defaults = { 39 | style = 'bold red', 40 | icon = config.prompt.fail or config.prompt.icon 41 | } 42 | end 43 | end 44 | }, 45 | git = { 46 | branch = function(segment) 47 | segment.defaults.condition = git.isRepo 48 | local branch = git.getBranch() 49 | if not branch then 50 | return '' 51 | end 52 | 53 | return branch 54 | end, 55 | dirty = function(segment) 56 | segment.defaults = { 57 | condition = function() 58 | return git.isRepo() and git.isDirty() 59 | end, 60 | style = 'gray', 61 | icon = '*' 62 | } 63 | end 64 | }, 65 | command = { 66 | execTime = function(segment) 67 | if not execTime then 68 | execTime = {stamp = os.time()} 69 | bait.catch('command.preexec', function() 70 | execTime.stamp = os.time() 71 | end) 72 | end 73 | 74 | local execTime = os.time() - execTime.stamp 75 | segment.defaults.condition = function() 76 | return execTime > 1 77 | end 78 | 79 | if execTime > 60 then 80 | return string.format('%dm %ds', math.floor(execTime / 60), execTime % 60) 81 | else 82 | return string.format('%ds', execTime) 83 | end 84 | end 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /searchpath.lua: -------------------------------------------------------------------------------- 1 | local fs = require 'fs' 2 | 3 | -- package.searchpath function (hilbish is 5.1 so we don't have it) 4 | return function(name, path, sep, rep) 5 | if type(name) ~= 'string' then 6 | error(('bad argument #1 to \'searchpath\' (string expected, got %s)'):format(type(path)), 2) 7 | end 8 | if type(path) ~= 'string' then 9 | error(('bad argument #2 to \'searchpath\' (string expected, got %s)'):format(type(path)), 2) 10 | end 11 | if sep ~= nil and type(sep) ~= 'string' then 12 | error(('bad argument #3 to \'searchpath\' (string expected, got %s)'):format(type(path)), 2) 13 | end 14 | if rep ~= nil and type(rep) ~= 'string' then 15 | error(('bad argument #4 to \'searchpath\' (string expected, got %s)'):format(type(path)), 2) 16 | end 17 | sep = sep or '.' 18 | rep = rep or '/' 19 | do 20 | local s, e = name:find(sep, nil, true) 21 | while s do 22 | name = name:sub(1, s - 1) .. rep .. name:sub(e + 1, -1) 23 | s, e = name:find(sep, s + #rep + 1, true) 24 | end 25 | end 26 | local tried = {} 27 | for m in path:gmatch('[^;]+') do 28 | local nm = m:gsub('?', name) 29 | tried[#tried + 1] = nm 30 | local ok = pcall(fs.stat, nm) 31 | if ok then 32 | return nm 33 | end 34 | end 35 | return nil 36 | end 37 | -------------------------------------------------------------------------------- /themes/myeline.lua: -------------------------------------------------------------------------------- 1 | local bait = require 'bait' 2 | local promptua = require 'promptua' 3 | 4 | local conf = { 5 | prompt = { 6 | icon = 'ﴨ', 7 | success = '✓', 8 | fail = '✕' 9 | } 10 | } 11 | promptua.setConfig(conf) 12 | 13 | return { 14 | { 15 | provider = 'prompt.failSuccess', 16 | style = function(info) 17 | if hilbish.exitCode ~= 0 then 18 | return 'bold red' 19 | else 20 | return 'bold green' 21 | end 22 | end 23 | }, 24 | { 25 | provider = 'user.name', 26 | style = 'bold yellow' 27 | }, 28 | { 29 | provider = 'user.hostname', 30 | style = 'bold blue' 31 | }, 32 | { 33 | provider = function() 34 | -- the current directory 35 | local cwd = hilbish.cwd() 36 | local splitwd = string.split(cwd, '/') 37 | -- truncate to last 2 directories 38 | -- if we are at root, just show the root 39 | if #splitwd == 1 then 40 | return splitwd[1] 41 | else 42 | -- if in home, show ~ 43 | if cwd == hilbish.home then return '~' end 44 | return splitwd[#splitwd - 1] .. '/' .. splitwd[#splitwd] 45 | end 46 | end, 47 | style = 'bold magenta' 48 | }, 49 | { 50 | provider = 'git.branch', 51 | style = 'bold cyan' 52 | }, 53 | { 54 | provider = 'command.execTime', 55 | style = 'bold blue' 56 | }, 57 | { 58 | provider = function() 59 | return '\n' 60 | end, 61 | separator = '' 62 | }, 63 | { 64 | provider = 'prompt.icon', 65 | style = 'bold blue' 66 | } 67 | } 68 | --------------------------------------------------------------------------------