You are not logged in.
Hi,
pmenu is a dynamic terminal-based menu inspired by dmenu written in Python without dependencies with an optional MRU ordering which could also be used as an application launcher and CtrlP alternative.
The script pmenu-run is an example of an application launcher built with pmenu similar to dmenu_run, gmrun and bashrun. It builds the menu from system *.desktop files and launches the selected item in the current terminal or detached from it depending on the application type.
Usage:
usage: pipe newline-separated menu items to stdin and/or pass them as positional arguments
positional arguments:
item the menu item text
optional arguments:
-h, --help show this help message and exit
-c COMMAND, --command COMMAND
the shell command which output will populate the menu
items on every keystroke ({} will be replaced by the
current input text)
-n NAME, --name NAME the cache file name with the most recently used items
-p PROMPT, --prompt PROMPT
the prompt text
-v, --version show program's version number and exit
Display some menu items:
echo -e "foo\nbar\nbaz" | pmenu
pmenu foo bar baz
echo -e "foo\nbar" | pmenu baz qux
Pick some file from the current directory:
command ls /usr/bin/ | pmenu
find -maxdepth 3 -type f ! -path "./.git/*" ! -path "./.svn/*" -printf '%P\n' | LC_COLLATE=C sort | pmenu
Pick some file from the current directory for editing in VIM using Ctrl-P shortcut (a la the CtrlP plugin):
function! Pmenu()
let item_command = "find -maxdepth 3 -type f -regextype posix-egrep ! -regex '.*/(__pycache__|\.git|\.svn|node_modules)/.*' -printf '%P\\n'"
if isdirectory("./.git")
let item_command = "git ls-files"
endif
let cache_name = fnamemodify(getcwd(), ":t")
let items = sort(systemlist(item_command))
let current_item = expand("%:.")
if !empty(current_item)
let items = filter(copy(items), "v:val != " . shellescape(current_item))
endif
let selected_items = systemlist("pmenu -n " . shellescape(cache_name), items)
if !empty(selected_items)
execute "edit " . fnameescape(selected_items[0])
endif
redraw!
endfunction
nnoremap <silent> <C-P> :call Pmenu()<CR>
vnoremap <silent> <C-P> :call Pmenu()<CR>
Pick a title from the markdown file and jump to it:
function! PmenuMarkdownTitle()
let titles = filter(getline(1, '$'), "v:val =~ '^#\\+\\s'")
let selected_paths = systemlist('pmenu', titles)
if !empty(selected_paths)
call search('^#\+\s' . selected_paths[0])
endif
redraw!
endfunction
nnoremap <silent> <C-T> :call PmenuMarkdownTitle()<CR>
Pick and show a definition from the WordNet dictionary on the dict server (dict.org by default) using either the curl or dict command:
pmenu -c "m={} && curl -s \"dict://dict.org/m:\${m:-a}:wn:prefix\" | grep -oP '(?<=\").+(?=\")' | sort -f | uniq" | xargs -I '{}' curl -s "dict://dict.org/d:{}:wn" | grep -vP "^(\d+ |\.)" | less
pmenu -c "dict -fm -d wn -s prefix -- {} | grep -oP '(?<=\t)[^\t]+$' | sort -f | uniq" | xargs -I '{}' curl -s "dict://dict.org/d:{}:wn" | grep -vP "^(\d+ |\.)" | less
Home page: https://github.com/sgtpep/pmenu
List of alternatives: https://github.com/sgtpep/pmenu#alternatives
AUR: https://aur.archlinux.org/packages/pmenu/
Last edited by sgtpep (2016-02-21 01:36:28)
Offline
This sounds pretty good. Two questions. First, why would I use it over dmenu or xboomx (which I'm using now)? Second, is there a pkgbuild in the AUR?
EDIT: words. Also, not meant to be mean questions. Just curious to see what the advantages are.
Last edited by runical (2015-08-28 12:55:13)
Offline
@runical, thank you for your questions. The pkgbuild is on the way, thanks for mentioning it, I'll update the post with the link to it. Right now you could place pmenu (and pmenu-run) somewhere in $PATH, it's the single scripts with no dependencies besides Python 3, which is almost always is installed.
This script comes from my personal preference of doing all work from terminal, be it terminal emulator, tty, remote shell, etc. So it has no dependency on X11, like dmenu. And personally I don't like bitmap fonts, so I need to patch and recompile dmenu to add xft support to it. I like my desktop to be easily reprodusible: only installing some packages and throwing in dotfiles/scripts, no need for unnesessary compilations. I just store this pmenu script it in my dotfiles along with .vimrc.
Also I was looking for Pmenu replacement for Vim (quick picking files from current directory), as it's no longer maintained. I use vim from terminal, so dmenu would be at least unnatural fit for this task.
And last, but not least, it's simple and hackable (only 225 LOC).
It was inspired by selecta https://github.com/garybernhardt/selecta written in Ruby. Also I try to maintain the list of alternatives https://gitlab.com/sgtpep/pmenu#alternatives, so everyone could pick the best one for his/her task and environment.
Last edited by sgtpep (2015-08-28 13:00:02)
Offline
Sorry for the late reply, but thanks for the reply. It is a bit clearer now why you created the software.
How is the pkgbuild coming along?
Offline
And here it is: https://aur.archlinux.org/packages/pmenu/.
Offline
Lighthouse (C + anything, X11) is an alternative that you don't seem to have listed. Rather than being a filter, it is bidirectional (lighthouse runs script, lighthouse tells script what the user is inputting, script responds with candidates the user can select from). Slower to set up, but more powerful.
I have tried pmenu; once it supports deleting characters, I'll probably replace percol -- which I'm constantly forgetting the name of -- with pmenu. For the moment, this missing feature makes it impractical for me.
Offline
@likytau Thank you for suggesting Lighthouse! Added. Yeah, Lighthouse approach looks more powerful but adds complexity as a tradeoff.
What do you mean by deleting characters: deleting with Backspace/Ctrl-H or navigating between characters and deleting with Delete?
Offline
I wish this would be a complete rofi clone instead of dmenu
He hoped and prayed that there wasn't an afterlife. Then he realized there was a contradiction involved here and merely hoped that there wasn't an afterlife.
Douglas Adams
Offline
What do you mean by deleting characters: deleting with Backspace/Ctrl-H or navigating between characters and deleting with Delete?
Backspace. Deleting characters from end of string is essential, to me. Deleting characters in the middle of the string is more of a optional nicety, IMO.
Offline
@likytau It should be fixed now as I've added the support for alternative Backspace keycode that your terminal could use. If there are any problems left feel free to open an issue https://gitlab.com/sgtpep/pmenu/issues.
Offline
Oh, okay. If you'd told me you already had some Backspace support I would have mentioned which terminal (Sakura) I was using. In any case, yes, it works now, thanks
BTW, your AUR package should probably be called pmenu-git, since it pulls the latest git rather than a specific release.
You might also like to grab the dynamic versioning code from a -git package, so that the user is not wrongly told 'pmenu-0.1.0 is out of date -- reinstalling' when upgrading.
Something like this should work (adapted from https://aur.archlinux.org/cgit/aur.git/ … e-soup-git )
pkgver() {
cd $srcdir/pmenu
git describe | sed 's#-#.#g'
}
(note that this is defined in -addition- to the usual static value at the top of the PKGBUILD.)
Offline
@likytau Thank you for your suggestions. I had some doubts about package naming conventions and whether it sould be pulled from git. I decided to update the package for using the specific version with checksum matching by now.
Offline
This is a magnicient piece of software! I'm using it as a part of a pacman frontend to select packages to (un)install/downgrade (pacli). Thank you for this awesome tool!
The difference between reality and fiction is that fiction has to make sense.
Offline
@Chrysostomus Thank you for your appreciation! I'm excited that you've found it useful and used it in your project. BTW, package selection is a curious applicaton of pmenu.
Offline
It works very well for it, because it combines browsing for package with choosing it. The only thing I miss is option to choose multiple items. Though that could probably be implemented in the script by looping pmenu until certain item is chosen and gathering results in an array. Sadly that is for the moment beyond my meager scripting skills. Have you considered adding multiselect option to pmenu?
The difference between reality and fiction is that fiction has to make sense.
Offline
Multiselect mode would be a nice addition to it. I wonder how it should be implemented in dmenu-ish way without much additional interface and shortcuts. I need to explore some existing implementations for inspiration. I have only these examples of multiselection in my mind at the moment: ranger and fzf. But it may also turn out that the real minimalistic/dmenu-ish/unix-ish way for this task is to write the wrapper script for it.
Your suggestions will be highly appreciated.
Offline
Hi sgtpep, I was looking at pmenu again for a selector script for my x-session and I stumbled upon the -n switch. Unfortunately, I can't find any documentation for it. Can you explain what it does?
Last edited by runical (2015-10-30 10:59:21)
Offline
Hi sgtpep, I was looking at pmenu again for a selector script for my x-session and I stumbled upon the -n switch. Unfortunately, I can't find any documentation for it. Can you explain what it does?
Yes, I admit that this option is not clearly decumented at the moment:
-n NAME, --name NAME name of the usage cache
If you pass a name with the -n/--name option, pmenu will create and use the history file located at ~/.cache/<name>. It will add the selected option values at the end of this files avoiding duplicates (like .bash_history with HISTCONTROL=ignoredups). Also it changes the default prompt value which you could override explicitly passing the -p/--prompt option. The options with the values from the ~/.cache/<name> file has a higher priority in all menues with the same corresponding -n/--name option value. You could see the on top of your menu list. Also it could be compared to the MRU (most recently used) lists on some programs.
For example, the provided pmenu-run application launcher has a static option value: -n run. In vim I pass the top level directory name as a value to the -n option.
Offline
That sounds pretty useful. I'll give it a shot then.
Thanks for explaining.
EDIT: Works like a charm! Thanks again.
Last edited by runical (2015-11-04 15:01:18)
Offline
This script is great! I started using it instead of percol. If it's interesting, I added some basic status line functionality, matches highlighting, pg_up and pg_down (not tested thouroughly, however).
#!/usr/bin/env python3
import argparse
import curses
import curses.ascii
import fileinput
import io
import os
import re
import shlex
import subprocess
import sys
__version__ = '0.3.0'
required_version = (3, 3)
if sys.version_info < required_version:
sys.exit("Python {}.{} or newer is required.".format(*required_version))
def get_args():
parser = argparse.ArgumentParser(usage="pipe menu items to stdin or pass with as positional arguments")
parser.add_argument('item', nargs='*', help="menu item text")
parser.add_argument('-c', '--command', help="populate menu items from the shell command output ({} will be replaced by the input text)")
parser.add_argument('-n', '--name', help="name of the usage cache")
parser.add_argument('-p', '--prompt', help="prompt text")
parser.add_argument('-v', '--version', action='version', version="%(prog)s " + __version__)
args = parser.parse_args()
if args.prompt is None:
args.prompt = "> "
if args.name:
args.prompt = args.name + args.prompt
return args
def get_mru_path():
if not args.name:
return
cache_dir = os.environ.get('XDG_CACHE_HOME', os.path.join(os.path.expanduser('~'), '.cache'))
mru_dir = os.path.join(cache_dir, 'pmenu')
os.makedirs(mru_dir, exist_ok=True)
return os.path.join(mru_dir, args.name)
def get_input_items():
input_items = []
if not sys.stdin.isatty():
stdin = io.TextIOWrapper(sys.stdin.buffer, 'utf8', 'replace')
input_items += stdin.read().splitlines()
input_items += args.item
return input_items
def get_command_items():
if not args.command:
return []
command_argument = shlex.quote(query_text)
command = args.command.replace('{}', command_argument)
command_output = subprocess.check_output(command, shell=True, stderr=subprocess.DEVNULL)
command_output = command_output.decode('utf8', 'replace')
if not query_text:
command_items = command_output.splitlines()
else:
command_items = [(x,list()) for x in command_output.splitlines()]
return command_items
def get_mru_items(mru_path, input_items):
if not mru_path or not os.path.exists(mru_path):
return []
input_items += get_command_items()
mru_file = open(mru_path, encoding='utf8', errors='replace')
mru_items = mru_file.read().splitlines()
mru_items = [i for i in mru_items if i in input_items]
mru_items.reverse()
input_items[:] = [i for i in input_items if i not in mru_items]
return mru_items
def redirect_stdio(func):
try:
prev_stdin = os.dup(0)
prev_stdout = os.dup(1)
stdin = open("/dev/tty")
stdout = open("/dev/tty", 'w')
os.dup2(stdin.fileno(), 0)
os.dup2(stdout.fileno(), 1)
return func()
finally:
os.dup2(prev_stdin, 0)
os.dup2(prev_stdout, 1)
def curses_wrapper(func):
if 'ESCDELAY' not in os.environ:
os.environ['ESCDELAY'] = '0'
is_vim = os.environ.get('VIM')
if is_vim:
sys.stdout.write("\033[m")
sys.stdout.flush()
try:
screen = curses.initscr()
curses.noecho()
curses.cbreak()
screen.keypad(1)
try:
curses.start_color()
except:
pass
else:
curses.use_default_colors()
if is_vim:
curses.curs_set(1)
return func(screen)
finally:
if 'screen' in locals():
screen.keypad(0)
curses.echo()
try:
curses.nocbreak()
except curses.error:
pass
if not is_vim:
curses.endwin()
def get_filtered_items():
if not query_text:
filtered_items = mru_items + input_items
else:
filtered_items = []
word_regexes = [re.escape(i) for i in re.split(r"\s+", query_text.strip()) if i]
exact_match_regexes = [re.compile(r'\b' + i + r'\b', re.I) for i in word_regexes]
prefix_match_regexes = [re.compile(r'\b' + i, re.I) for i in word_regexes]
substring_match_regexes = [re.compile(i, re.I) for i in word_regexes]
for items in (mru_items, input_items):
exact_matched_items = []
prefix_matched_items = []
substring_matched_items = []
for item in items:
ms = [re.search(i, item) for i in substring_match_regexes]
is_substring_match = all(ms)
if not is_substring_match:
continue
intervals = []
for m in ms:
st, en = m.span(0)
if not intervals:
intervals = [st, en]
continue
i = 0
while True:
if i % 2 == 0:
if st < intervals[i]:
if en < intervals[i]: # out before
intervals = intervals[:i] + [st, en] + intervals[i:]
break
elif en <= intervals[i+1]: # overlap from left
intervals = intervals[:i] + [st] + intervals[i+1]
break
else: # includes
intervals = intervals[:i] + intervals[i+2:]
else:
if st < intervals[i]:
if en <= intervals[i]: # within
break
else: # overlap from right
st = intervals[i-1]
intervals = intervals[:i] + intervals[i+2:]
else:
intervals = intervals + [st, en]
i += 1
is_exact_match = all(re.search(i, item) for i in exact_match_regexes)
if is_exact_match:
exact_matched_items.append((item,intervals))
else:
is_prefix_match = all(re.search(i, item) for i in prefix_match_regexes)
if is_prefix_match:
prefix_matched_items.append((item,intervals))
else:
substring_matched_items.append((item,intervals))
filtered_items.extend(exact_matched_items + prefix_matched_items + substring_matched_items)
filtered_items += get_command_items()
return filtered_items
def redraw(screen):
screen.erase()
items = filtered_items
start_pos = int( (selection_index +1) / curses.LINES ) * curses.LINES
end_pos = start_pos + curses.LINES - 1
if end_pos > itemslen:
end_pos = itemslen
#items = items[start_pos:(start_pos + curses.LINES - 1)]
#for i, item in enumerate(items):
curses.init_pair(100, curses.COLOR_RED, -1)
for i in range(start_pos, end_pos):
item_attr_ = curses.A_REVERSE if i == selection_index else curses.A_NORMAL
mark = False
curpos = 0
if query_text:
for k in items[i][1]:
if k > curses.COLS - 1:
k = curses.COLS - 1
s = items[i][0][curpos:k]
item_attr = (curses.color_pair(100) | curses.A_BOLD) if mark else item_attr_
mark = not mark
if s:
screen.insstr(i - start_pos + 1, curpos, s, item_attr )
curpos = k
s = items[i][0][curpos:len(items[i][0])]
else:
s = items[i]
item_attr = item_attr_
if s:
screen.insstr(i - start_pos + 1, curpos, s, item_attr_ )
status = "{0}/{1}".format( selection_index, itemslen )
top_line_text = args.prompt + query_text
top_line_offset = len(top_line_text) - (curses.COLS - 1) - len(status) - 1
top_line_text = top_line_text + " "*(curses.COLS - len(top_line_text) - len(status)) + status
if top_line_offset < 0:
top_line_offset = 0
screen.addstr(0, 0, top_line_text[top_line_offset:])
screen.refresh()
def main(screen):
global selection_index, filtered_items, query_text, start_pos, itemslen
selection_index = start_pos = 0
read_items = True
while True:
if read_items:
filtered_items = get_filtered_items()
itemslen = len(filtered_items)
redraw(screen)
read_items = False
try:
char = screen.get_wch()
except KeyboardInterrupt:
return
char_code = isinstance(char, str) and ord(char)
if char == curses.KEY_RESIZE:
selection_index = 0
curses.resizeterm(*screen.getmaxyx())
redraw(screen)
continue
# see https://en.wikipedia.org/wiki/C0_and_C1_control_codes
# ^H, Backspace
elif char_code in (curses.ascii.BS, curses.ascii.DEL) or char == curses.KEY_BACKSPACE:
query_text = query_text[:-1]
read_items = True
# ^N, Down
elif char_code == curses.ascii.SO or char == curses.KEY_DOWN:
if selection_index < len(filtered_items) - 1:
selection_index += 1
redraw(screen)
continue
# ^P, Up
elif char_code == curses.ascii.DLE or char == curses.KEY_UP:
if selection_index > 0:
selection_index -= 1
redraw(screen)
continue
# ^[, ^G
elif char_code in (curses.ascii.ESC, curses.ascii.BEL):
return
# ^U
elif char_code == curses.ascii.NAK:
query_text = ''
read_items = True
# ^W
elif char_code == curses.ascii.ETB:
query_text = re.sub(r"\w*[^\w]*$", '', query_text)
read_items = True
# ^J, ^M, Enter
elif char_code == curses.ascii.NL:
break
# ^I, Tab
elif char_code == curses.ascii.TAB:
if filtered_items:
query_text = filtered_items[selection_index]
read_items = True
elif char == curses.KEY_PPAGE:
selection_index = ( selection_index - min(itemslen, curses.LINES) ) % itemslen
redraw(screen)
continue
elif char == curses.KEY_NPAGE:
selection_index = ( selection_index + min(itemslen, curses.LINES) ) % itemslen
redraw(screen)
continue
elif isinstance(char, str) and not curses.ascii.isctrl(char):
query_text += char
read_items = True
selection_index = 0
if filtered_items:
return True, filtered_items[selection_index][0]
else:
return False, query_text
def add_mru_text(mru_path, mru_text):
if not mru_path:
return
if os.path.exists(mru_path):
with fileinput.input(mru_path, inplace=True) as mru_file:
for mru_line in mru_file:
mru_line_text = mru_line.rstrip("\n\r")
if mru_line_text != mru_text:
print(mru_line_text)
with open(mru_path, 'a') as mru_file:
mru_file.write(mru_text)
if __name__ == '__main__':
args = get_args()
query_text = ''
input_items = get_input_items()
mru_path = get_mru_path()
mru_items = get_mru_items(mru_path, input_items)
result = redirect_stdio(lambda: curses_wrapper(main))
if not result:
sys.exit(130)
is_existing_item, selection_text = result
if is_existing_item:
add_mru_text(mru_path, selection_text)
print(selection_text)
Last edited by nbd (2015-11-25 13:51:26)
bing different
Offline
Thank you for pmenu, this is really great and nearly perfect for my needs.
Would you consider adding a keybinding to return the query text? (In dmenu this is shift-enter; in rofi this is control-enter.) I notice pmenu does this automatically if the query does not match any item but sometimes I want to output the query even if it does match an item.
Example,
$ echo -e 'hi\nhello\nhey' | pmenu
hell
I made a patch for myself (see below) implementing the feature. The key used is ^D. (I could not think of a key to use and meskarune suggested that.)
diff -Naur old/pmenu new/pmenu
--- old/pmenu 2015-11-28 16:35:27.139953784 -0500
+++ new/pmenu 2015-11-28 19:56:04.130087865 -0500
@@ -184,6 +184,8 @@
selection_index = 0
+ use_query = False
+
while True:
filtered_items = get_filtered_items()
redraw(screen)
@@ -238,6 +240,11 @@
elif char_code == curses.ascii.NL:
break
+ # ^D
+ elif char_code == curses.ascii.EOT:
+ use_query = True
+ break
+
# ^I, Tab
elif char_code == curses.ascii.TAB:
if filtered_items:
@@ -248,7 +255,7 @@
selection_index = 0
- if filtered_items:
+ if filtered_items and not use_query:
return True, filtered_items[selection_index]
else:
return False, query_text
aur S & M :: forum rules :: Community Ethos
Resources for Women, POC, LGBT*, and allies
Offline
This script is great! I started using it instead of percol. If it's interesting, I added some basic status line functionality, matches highlighting, pg_up and pg_down (not tested thouroughly, however).
Thank you for suggestions and sharing your version the script! Let me speak on every improvement and their applicability to pmenu:
I was not able to start your script so I couldn't see the status line in action. Personally I'd like to keep the interface as minimal and distraction-free as possible. I'm not sure whether the status line stats adds a lot of value.
Matches highlighting is a nice thing to have. I'll try to backport your implementation or implement something similar as soon as I get time.
PgUp/PgDn is a nice addition. But right now it's impossible to select and scroll to the items that are outside of the screen. If I'll fix that than PgUp/PgDn would make more sense (and Home/End).
Would you consider adding a keybinding to return the query text? (In dmenu this is shift-enter; in rofi this is control-enter.) I notice pmenu does this automatically if the query does not match any item but sometimes I want to output the query even if it does match an item.
Yeah, that makes sense. And the ^D shortcut is a good compromise since it's not possible to to catch the shortcut modifiers like Ctrl and Shift in curses. It's in the master branch https://github.com/sgtpep/pmenu/blob/master/pmenu. If it works for you than I'll publish the AUR package with the new version.
Offline
fsckd wrote:Would you consider adding a keybinding to return the query text? (In dmenu this is shift-enter; in rofi this is control-enter.) I notice pmenu does this automatically if the query does not match any item but sometimes I want to output the query even if it does match an item.
Yeah, that makes sense. And the ^D shortcut is a good compromise since it's not possible to to catch the shortcut modifiers like Ctrl and Shift in curses. It's in the master branch https://github.com/sgtpep/pmenu/blob/master/pmenu. If it works for you than I'll publish the AUR package with the new version.
Thank you, it works excellently.
I just noticed if i resize the terminal (I am running pmenu in tmux so I am actually resizing the pane) pmenu is no longer responsive and has to be killed.
aur S & M :: forum rules :: Community Ethos
Resources for Women, POC, LGBT*, and allies
Offline
I just noticed if i resize the terminal (I am running pmenu in tmux so I am actually resizing the pane) pmenu is no longer responsive and has to be killed.
Confirmed. Turns out it's not that easy to synchronize a terminal resize detection and a full redraw using python's curses library only. Reimplemented with the SIGWINCH signal handler. You can try it from the git master branch.
Offline
You are fast. This is excellent. Thank you.
aur S & M :: forum rules :: Community Ethos
Resources for Women, POC, LGBT*, and allies
Offline