commit 626af0929929dabfc688656860859ae48e761c08
parent 946499c72bebf5010c758651610f28d8429f7eae
Author: Giygas <mvk@girlpoison.org>
Date: Fri, 21 Jun 2024 01:35:46 +0200
Large overhaul
Diffstat:
A | Makefile | | | 15 | +++++++++++++++ |
M | README | | | 80 | +++++++------------------------------------------------------------------------ |
M | dnavigate | | | 107 | +++++++++++++++++++++++++++++++++++++------------------------------------------ |
A | dnavigate.1 | | | 58 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
D | examples/README | | | 1 | - |
M | examples/abducate | | | 66 | ++++++++++++++++++++++++++++++------------------------------------ |
M | examples/abduco | | | 49 | ++++++++++++++++++++++++------------------------- |
A | examples/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
+}