dnavigate

menu tree tool for dmenu
git clone git://git.girlpoison.org/dnavigate
Log | Files | Refs | README | LICENSE

commit 626af0929929dabfc688656860859ae48e761c08
parent 946499c72bebf5010c758651610f28d8429f7eae
Author: Giygas <mvk@girlpoison.org>
Date:   Fri, 21 Jun 2024 01:35:46 +0200

Large overhaul

Diffstat:
AMakefile | 15+++++++++++++++
MREADME | 80+++++++------------------------------------------------------------------------
Mdnavigate | 107+++++++++++++++++++++++++++++++++++++------------------------------------------
Adnavigate.1 | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dexamples/README | 1-
Mexamples/abducate | 66++++++++++++++++++++++++++++++------------------------------------
Mexamples/abduco | 49++++++++++++++++++++++++-------------------------
Aexamples/dialer | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 259 insertions(+), 192 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,15 @@ +DESTDIR := ~ +PREFIX := .local + +install: + mkdir -p $(DESTDIR)/$(PREFIX)/bin/dnavigate.d + install -m 0700 dnavigate $(DESTDIR)/$(PREFIX)/bin/dnavigate + install -m 0700 examples/* $(DESTDIR)/$(PREFIX)/bin/dnavigate.d + mkdir -p $(DESTDIR)/$(PREFIX)/share/man/man1 + install -m 0700 dnavigate.1 $(DESTDIR)/$(PREFIX)/share/man/man1 + + +uninstall: + rm -rf $(DESTDIR)/$(PREFIX)/bin/dnavigate $(DESTDIR)/$(PREFIX)/bin/dnavigate.d/* $(DESTDIR)/$(PREFIX)/share/man/man1/dnavigate.1 + rmdir $(DESTDIR)/$(PREFIX)/bin/dnavigate.d + diff --git a/README b/README @@ -1,78 +1,12 @@ Intro ----------- -dnavigate is a wrapper for dmenu designed to accomodate the easy creation of menu trees. It does not depend on any patches, though noinput is recommended. The author enjoys mousesupport, too. +-------------- +dnavigate is a dmenu wrapper designed to accomodate easy creation of dynamic menu trees. See dnavigate(1) for more details. +dnavigate pairs well with the noinput and mousesupport patches. -dnavigate renders a menu based on a given tree and branch. A tree corresponds to a shell script located in DNAVIGATE_DIR ('$HOME/dnavigate.d' by default), which is executed with branch as its sole argument. The selected branch defines several variables, which dnavigate uses to construct a menu and execute a command corresponding to the selected option. - -An option may run the custom child command, which recursively calls dnavigate on the same tree, with a new branch. The dmenu prompt is updated accordingly, inheriting the prompt of the branch that spawned the child. Canceling out of a child will re-execute dnavigate again for the parent branch. +Included are several examples for abduco & abducate session management and ssh(fs) host dialing. Installation ----------- -Not really, no. - -Usage ----------- -dnavigate [-p parents] tree [branch] - -OPTIONS - -p parents - Specify a space-delimited list of parent branches, from oldest to newest. - dnavigate will build a prompt from every specified branch, and execute - itself with the most recent parent if the user cancels out of dmenu. - -TREES AND BRANCHES - When invoked, dnavigate executes a shell script in DNAVIGATE_DIR whose name - corresponds to tree, passing branch as its only argument. If no branch was - specified on the command line, dnavigate will assume 'main' as the branch. - - The tree script initialises a number of shell variables that define menu items - and options. These variables are LEAVES, ACTIONS, PROMPT, OPTIONS, LINES and - INPUT. - - LEAVES defines a newline-delimited list of menu items to be displayed by dmenu. - - ACTIONS defines a newline-delimited list of commands, corresponding one-to-one - with the items listed in LEAVES. The two must have the same number of items. - When the user selects one of the items in LEAVES, the corresponding command - is executed. If INPUT is set, this variable will be ignored. - - (Optional) PROMPT defines the dmenu prompt. It may be left empty. You must - set this variable to allow dnavigate to build up a prompt from a stack of - parents; manually specifying dmenu -p breaks this behaviour. - - (Optional) INPUT defines a command that dnavigate will pass the given input - to. If set, LEAVES will still be drawn but ACTIONS will be ignored. - - (Optional) LINES defines the number of rows drawn by dmenu -l. - dnavigate will automatically pad the LEAVES and ACTIONS strings with extra - newlines and no-ops if necessary. - - (Optional) OPTIONS defines any further options for dmenu. - -CHILD, RUN, BACK & CANCEL - dnavigate provides three special functions that can be included in the - ACTIONS list: run, child and back - - By default, dnavigate will block until the selected command has finished - executing. The run command will execute its arguments as a background - process and dnavigate will exit promptly. - - The child command executes dnavigate -p with the same tree, passing its - only argument along as the new branch, replicating the behaviour of a - submenu. - - The back command executes dnavigate on the direct parent. If no parents - are left, dnavigate will run the cancel function defined in the tree file, - if it exists. The back command is executed automatically if the user cancels - out of dmenu. - -ENVIRONMENT VARIABLES - DNAVIGATE_DIR - Defines the directory that dnavigate searches for trees - ('$HOME/.dnavigate.d' by default) - -EXAMPLES - The examples/ directory contains several example trees that provide a - starting-off point for creating your own. Among these are dmenu - interfaces for abduco & abducate session management. +-------------- +Run 'make'. +To uninstall, run `make uninstall`. diff --git a/dnavigate b/dnavigate @@ -1,76 +1,69 @@ #!/bin/sh PROGNAME="$(basename $0)" -FOREST="${DNAVIGATE_DIR=$HOME/.$PROGNAME.d}" +FOREST="${DNAVIGATE_DIR=$HOME/.local/bin/$PROGNAME.d}" -err() { echo "$PROGNAME: "$* >&2; } -usage() { err "usage: $0 [-p parent stack] menu [dmenu options]"; } -child() { $0 -p "$PARENTS $BRANCH" $TREE $1; } +panic() { echo "$PROGNAME: "$* >&2; exit 1; } +usage() { panic "usage: $0 [-d dmenu options... --] tree [branch [args...]]"; } run() { $SHELL -c "$*" & } +child() { + unset T B + [ "$1" ] || return -buildprompt() { - for B in $*; do - source "$FOREST/$TREE" $B - PPROMPT="$PPROMPT${PROMPT+"$PROMPT => "}" - unset PROMPT - done - unset LEAVES ACTIONS INPUT OPTIONS LINES RETURN + tchild $TREE "$@" } +tchild() { + unset T B + [ "$1" ] || return + T="$1"; shift + [ "$1" ] && B="$1" && shift -runparent() { - CENN="$(awk '{print $NF}' <<<"$PARENTS")" - GRANDCENNS="$(awk '{$NF=""; print}' <<<"$PARENTS")" - $0 -p "$GRANDCENNS" $TREE $CENN + export PPROMPT="$PROMPT" + $0 -d $DMENU_OPTS -- $T ${B:-main} "$@" } -back() { - if [ "$PARENTS" ]; then runparent - else - typeset -f cancel >&- && cancel - exit 0 - fi -} - -while getopts "p:" opt; do - case "${opt}" in - p) - PARENTS="${OPTARG}" - shift 2;; - *) - usage - exit 1;; +while getopts "d:" OPT; do + case ${OPT} in + d) + shift; # eat -d + while [ "$1" != "--" ]; do + [ "$1" ] || usage + DMENU_OPTS="$DMENU_OPTS$1 "; shift; + done + shift;; # eat -- + *) usage;; esac done -TREE=$1; -BRANCH=$2; [ $BRANCH ] && shift -if [ ! "$TREE" ]; then usage; exit 1; fi -buildprompt "$PARENTS" +[ "$1" ] || usage +TREE="$1"; shift +[ "$1" ] && BRANCH="$1" && shift -source $FOREST/$TREE "${BRANCH:=main}" || exit 1 +. $FOREST/$TREE "${BRANCH:=main}" "$@" || exit 1 -NLEAVES=$(wc -l <<<"$LEAVES") -NACTIONS=$(wc -l <<<"$ACTIONS") -if [ ! "$INPUT" ]; then - [ $NLEAVES -ne $NACTIONS ] && err "leaf and action count mismatch for '$TREE/$BRANCH'" && exit 1 -fi +[ ! "$INPUT" ] && [ ${#LEAVES[*]} -ne ${#ACTIONS[*]} ] && panic "leaf and action count mismatch for '$TREE/$BRANCH'" -if [ $NLEAVES -lt ${LINES=$NLEAVES} ]; then - LEAVES=$(awk -v l=$LINES '1;END{for(;NR<l;NR++)print" "}' <<<"$LEAVES") - ACTIONS=$(awk -v l=$LINES '1;END{for(;NR<l;NR++)print"back"}' <<<"$ACTIONS") +if [ ${#LEAVES[*]} -lt ${NLINES=${#LEAVES[*]}} ]; then + for I in $(seq ${#LEAVES[*]} $NLINES); do + LEAVES[$I]="" + ACTIONS[$I]="" + done fi -SELECTED="$(echo -n "$LEAVES" | dmenu -p "$PPROMPT$PROMPT" -l $LINES $OPTIONS)" -if [ ! "$SELECTED" ]; then back; exit; fi +[ "$PPROMPT" ] && PROMPT="$PPROMPT => $PROMPT" -if [ "$INPUT" ]; then - $INPUT $SELECTED -else - LINE="$(awk -F. -v s="$SELECTED" '$0==s{print NR;exit}' <<<"$LEAVES")" - ACTION="$(awk -v l="$LINE" 'NR==l' <<<"$ACTIONS")" - - - export PARENTSEL="$SELECTED" - $ACTION -fi +menu() { + SEL="$(printf "%s\n" "${LEAVES[@]}" | dmenu -l $NLINES -p "$PROMPT" $DMENU_OPTS $OPTIONS)" + if [ $? -ne 0 ]; then + return 1; + elif [ "$INPUT" ]; then + $INPUT $SEL || menu + else + for I in $(seq 0 "${#LEAVES[*]}"); do + [ "${LEAVES[$I]}" = "$SEL" ] && break; + done + + eval "${ACTIONS[$I]:-return 1}" || menu + fi +} -if [ "$RETURN" ]; then back; exit; fi +menu diff --git a/dnavigate.1 b/dnavigate.1 @@ -0,0 +1,58 @@ +.TH DNAVIGATE 1 +.sh NAME +dnavigate \- render menu tree from shell script +.SH SYNOPSIS +.B dnavigate +.IR tree +[\fIbranch\fR [\fIargs...\fR]] +.PP +.B dnavigate +\fB-d\fR \fIdmenu options...\fR -- +.IR tree +[\fIbranch\fR [\fIargs...\fR]] +.PP +.B child +.IR branch +[\fIargs...\fR] +.PP +.B tchild +.IR tree +.IR branch +[\fIargs...\fR] +.PP +.B run +.IR exec... +.SH DESCRIPTION +\fBdnavigate\fR executes shell script \fItree\fR, passing \fIbranch\fR and \fIargs\fR. +The script in turn sets several variables that define a menu layout, described below. +If no \fIbranch\fR was specified on the command line, dnavigate assumes 'main'. +.PP The \fBchild\fR function will execute \fBdnavigate\fR on the same tree, passing along its arguments. \fBtchild\fR does the same, but executes a different tree. +.PP After selecting an item, \fBdnavigate\fR will execute the corresponding command. +\fBdnavigate\fR will re-show the menu if the command returned a non-zero exit status. +\fBdnavigate\fR itself will return non-zero if the user cancels out of dmenu, or selects a blank option. +Combined with the \fBchild\fR command, this replicates the behaviour of a menu tree. +.PP By default, \fBdnavigate\fR will block until the selected command has finished executing. +The \fBrun\fR command will execute its arguments as a background process and \fBdnavigate\fR will exit promptly. +.PP All arguments to the \fB-d\fR option are passed along to dmenu. +.SH MENU DEFINITIONS +The \fItree\fR script sets several variables that define the look, layout and content of the menu to be rendered: +.PP +\fBLEAVES\fR defines an array of menu items to be displayed by dmenu. +.PP +\fBACTIONS\fR defines an array of commands, corresponding one-to-one with the items in \fBLEAVES\fR. +The two must have the same number of items. +When an item from \fBLEAVES\fR is selected, the corresponding \fBACTIONS\fR item is executed as a command. +This variable will be ignored if \fBINPUT\fR is set. +.PP +(Optional) \fBINPUT\fR defines a command that dnavigate will pass the user's input to. +If set, \fBLEAVES\fR will still be drawn but \fBACTIONS\fR will be ignored. +.PP +(Optional) \fBPROMPT\fR defines the dmenu prompt. +You must set this variable to allow dnavigate to build a prompt from a stack of parents; specifying \fB-p\fR as a dmenu option breaks this behaviour. +.PP +(Optional) \fBLINES\fR defines the number of rows drawn by dmenu. +dnavigate will pad the \fBLEAVES\fR and \fBACTIONS\fR arrays with empty elements if necessary. +.PP +(Optional) \fBOPTIONS\fR contains the list of dmenu options passed during invocation, and may be modified before the menu is drawn. +.SH ENVIRONMENT VARIABLES +\fBDNAVIGATE_DIR\fR defines the directory that \fBdnavigate\fR searches for trees ($HOME/.dnavigate.d by default) diff --git a/examples/README b/examples/README @@ -1 +0,0 @@ -Example trees. Put in your .dnavigate.d directory and execute via dnavigate. diff --git a/examples/abducate b/examples/abducate @@ -1,47 +1,41 @@ #!/bin/bash -OPTIONS="-noi" +OPTIONS="$OPTIONS -noi" TERMEXEC="urxvt -e" -SERVICES=\ -"picom picom -sxhkd sxhkd -mousejail xpointerbarrier 0 0 50 0" +SERVICES=( + picom # name of the service is also the command + sxhkd + plumber + "lemonbar panel" # name is lemonbar, command is panel + "mousejail xpointerbarrier 0 0 50 0" +) case $1 in main) - unset LEAVES ACTIONS PROMPT="$(abducate list | sed 1q)" - SESSIONS="$(abducate list | sed -n '2,$s/\t/ /gp')" - while read -r S; do - read LABEL _ <<<"$S" - RUNNING=$(awk -v label=$LABEL '$NF==label{print}' <<<"$SESSIONS") - if [ "$RUNNING" ]; then - LEAVES="$LEAVES$RUNNING -" - else - LEAVES="$LEAVES ... ................... ..... $LABEL -" - fi - done <<<"$SERVICES" - LEAVES=$(awk '$0' <<<"$LEAVES") - ACTIONS=$(awk '$0{print "child manage"}' <<<"$LEAVES");; + SESSIONS="$(abducate list | sed -n '2,$s/\t/ /gp')" + for S in "${SERVICES[@]}"; do + NAME="$(echo $S | awk '$0=$1')" + RUNNING=$(echo "$SESSIONS" | awk -v name=$NAME '$NF==name{print}') + if [ "$RUNNING" ]; then LEAVES[${#LEAVES[@]}]="$RUNNING" + else LEAVES[${#LEAVES[@]}]=" ... ................... ..... $NAME"; fi + ACTIONS[${#ACTIONS[@]}]="child manage $NAME" + done;; manage) - SESSION=$(awk '$0=$NF' <<<"$PARENTSEL") - while read -r S; do - read LABEL COMMAND <<<"$S" - [ "$LABEL" = "$SESSION" ] && break - done <<<"$SERVICES" + NAME="$2" + for S in "${SERVICES[@]}"; do + N="$(echo "$S" | awk '$0=$1')" + [ "$N" = "$NAME" ] && break; + done + + COMMAND="$(echo "$S" | awk '$1="";1')" + [ "$COMMAND" ] || COMMAND=$NAME - PROMPT="Manage service: $SESSION" - LEAVES=\ -"on -off -toggle -attach" - ACTIONS=\ -"abducate -nl $LABEL start $COMMAND -abducate -nl $LABEL stop -abducate -nl $LABEL toggle $COMMAND -$TERMEXEC abducate -l $LABEL attach";; + PROMPT="Manage service: $NAME" + LEAVES=("on" "off" "toggle" "attach") + ACTIONS=("abducate -l $NAME start $COMMAND" + "abducate -l $NAME stop" + "abducate -l $NAME toggle $COMMAND" + "$TERMEXEC abducate -l $NAME attach");; esac diff --git a/examples/abduco b/examples/abduco @@ -1,40 +1,39 @@ #!/bin/bash -# A simple abduco session management menu -OPTIONS="-noi" +OPTIONS="$OPTIONS -noi" TERMEXEC="urxvt -e" -# Suggestions for sessions -SUGGESTIONS="" - -case $1 in +B="$1"; shift +case $B in main) PROMPT="$(abduco | sed 1q)" - LEAVES="+ new session..." - ACTIONS="child new" - SESSIONS=$(abduco | sed -n '2,$s/\t/ /gp' | awk 'NF{print}') - if [ "$SESSIONS" ]; then - LEAVES="$SESSIONS -$LEAVES" - ACTIONS="$(awk '{print "child manage"}' <<<"$SESSIONS") -$ACTIONS" - fi;; + while read -r S; do + [ "$S" ] || continue + LEAVES[${#LEAVES[*]}]="$S" + ACTIONS[${#ACTIONS[*]}]="child manage '$S'" + done <<EOF +$(abduco | sed -n '2,$s/\t/ /gp' | awk 'NF{print}') +EOF + + LEAVES[${#LEAVES[*]}]="+ new session..." + ACTIONS[${#ACTIONS[*]}]="child new" + LEAVES[${#LEAVES[*]}]="+ dial a host..." + ACTIONS[${#ACTIONS[*]}]="tchild dialer" + ;; manage) - PARENTSEL="$(sed 's/^[*+] //' <<<"$PARENTSEL" | tr -s "[:blank:]" " ")" - SESSION=$(awk '$1=$2=$3=$4="";1' <<<"$PARENTSEL" | sed 's/^ *//') - PID=$(awk '{print $4}' <<<"$PARENTSEL") - PROMPT="Manage session: $SESSION" - LEAVES="attach -kill" - ACTIONS="run $TERMEXEC abduco -a '$SESSION' -kill $PID";; + PSEL="$(echo "$*" | sed 's/^[*+] //' | tr -s "[:blank:]" " ")" + S=$(echo "$PSEL" | awk '$1=$2=$3=$4="";1' | sed 's/^ *//') + PID=$(echo "$PSEL" | awk '{print $4}') + PROMPT="Manage session: $S" + LEAVES=("attach" "kill") + ACTIONS=("run $TERMEXEC abduco -a '$S'" "kill $PID");; new) unset OPTIONS - LEAVES="$SUGGESTIONS" INPUT="create" PROMPT="new session: ";; esac create() { - $TERMEXEC abduco -c "${*:=$SHELL}.$$" $* & + NAME="$(basename "$1")" + $TERMEXEC abduco -c "${NAME:-$(basename $SHELL)}.$$" ${*:-$SHELL} & } diff --git a/examples/dialer b/examples/dialer @@ -0,0 +1,75 @@ +#!/bin/bash +OPTIONS="$OPTIONS -noi" +TERMEXEC="urxvt -e" + +DIAL_HOSTS=("host.example + alice:/home/alice/:$HOME/net/host.example/home + git:srv:$HOME/net/host.example/git + root" + "host2.example + bob") +SSHFS_OPTS="-o idmap=user" +XCLIP=1 + +B=$1; shift; +case $B in +main) + PROMPT="dial" + + for H in "${DIAL_HOSTS[@]}"; do + HOST=$(echo $H | awk '$0=$1') + USERS=$(echo $H | awk '$1="";1') + + LEAVES[${#LEAVES[*]}]="$HOST" + ACTIONS[${#ACTIONS[*]}]="run $TERMEXEC abduco -c '$HOST.$$' ssh $HOST" + for U in $USERS; do + USER="$(echo "$U" | cut -d: -f1)" + if echo "$U" | grep ':' >/dev/null; then + MOUNT="$(echo "$U" | cut -d: -f2-)" + if [ "$USER" = "$PREV" ]; then + P=$((${#ACTIONS[*]}-1)) + ACTIONS[$P]="${ACTIONS[$P]} '$MOUNT'" + else + LEAVES[${#LEAVES[*]}]=" user: $USER..."; + ACTIONS[${#ACTIONS[*]}]="child connect $USER $HOST '$MOUNT'" + fi + else + LEAVES[${#LEAVES[*]}]=" user: $USER"; + ACTIONS[${#ACTIONS[*]}]="run $TERMEXEC abduco -c '$HOST.$$' ssh $USER@$HOST" + fi + PREV="$USER" + done + done;; +connect) + USER="$1"; HOST="$2"; shift 2 + PROMPT="$USER@$HOST" + + LEAVES=("connect") + ACTIONS=("run $TERMEXEC abduco -c '$HOST.$$' ssh $HOST") + + [ "$*" ] || break; + + LEAVES[${#LEAVES[*]}]="" + ACTIONS[${#ACTIONS[*]}]="back" + while [ "$*" ]; do + REMOTE="$(echo $1 | cut -d: -f1)" + LOCAL="$(echo $1 | cut -d: -f2-)" + [ "$LOCAL" ] || panic "$TREE/$BRANCH: missing LOCAL in mount declaration for '$USER@$HOST'" + MOUNTED="$(awk -v d="$USER@$HOST:$REMOTE" '$1==d && $3=="fuse.sshfs" {print $2}' </etc/mtab)" + if [ "$MOUNTED" ]; then + LEAVES[${#LEAVES[*]}]="unmount: $REMOTE" + ACTIONS[${#ACTIONS[*]}]="fusermount3 -u $LOCAL" + else + LEAVES[${#LEAVES[*]}]="mount: ${REMOTE:-~}" + ACTIONS[${#ACTIONS[*]}]="mount $USER $HOST '$REMOTE' '$LOCAL'" + fi + shift 1; + done + ;; +esac + +mount() { + [ -d "$LOCAL" ] || mkdir -p "$LOCAL" + sshfs "$1@$2:$3" "$4" $SSHFS_OPTS || exit 1; + [ "$XCLIP" ] && echo "$4" | xclip -r +}