#!/bin/zsh setopt no_unset extended_glob warn_create_global typeset -g -A item_title item_to_id id_to_item container_exec_linked typeset -g -a items dmenu_cmd links item_props typeset -g delim _0 RS current_item next_item_id delim=' » ' _0=$'\0' RS=$'\036' next_item_id=1 if (($+commands[dmenu-custom])); then dmenu_cmd=( dmenu-custom ) else dmenu_cmd=( dmenu ) fi dmenu_cmd+=( -i -l 30 ) ### Utility functions {{{1 typeset -g hl_fatal hl_reset # die messages if (( $terminfo[colors] >= 8 )); then hl_success='%F{green}%B'; hl_success=${(%)hl_success} hl_fatal='%F{red}%B'; hl_fatal=${(%)hl_fatal} hl_warn='%F{yellow}%B'; hl_warn=${(%)hl_warn} hl_reset='%b%f'; hl_reset=${(%)hl_reset} fi err_msg() { local first=$1 shift printf >&2 '%s%s%s%s\n' "$hl_fatal" "$first" "$hl_reset" "$*" } warn_msg() { local first=$1 shift printf >&2 '%s%s%s%s\n' "$hl_warn" "$first" "$hl_reset" "$*" } # helper that prints out stack, error message and exits die_ret() { set +x local ret n ret=$1 shift print -r - >&2 "${hl_fatal}Fatal$hl_reset error occurend in:" for n in {${#funcfiletrace}..1}; do printf >&2 '%d> %s (%s)\n' $n "$funcfiletrace[$n]" "$functrace[$n]" done printf >&2 '%s\n' "${hl_fatal}*$hl_reset $^@" exit $ret } die() { set +x die_ret 1 "$@" } die100() { # 100: wrong usage set +x die_ret 100 "$@" } die111() { # 111: system call failed set +x die_ret 111 "$@" } -() { # Run command and die on nonzero exitcode "$@" || die_ret $? "command failed with exitcode $?: ${(j: :)${(q)@}}" } ::() { : "$@" } typeset -f -t :: ### main code { item_make_id() { (($# == 1)) || die "Must specify packed item name" (($+item_to_id[$1])) && return item_to_id[$1]=$next_item_id id_to_item[$next_item_id]=$1 : $(( next_item_id++ )) } item_start() { (($#)) || die "Must specify item name" if [[ -n $current_item ]]; then die "Previous item not closed: ${(qqq)current_item}" fi current_item=${(pj:\0:)*} local x for x in "$@"; do [[ $x == ?*=?* ]] || die "Malformed item name ${(j: :)${(qqq)*}}" done (($+item_to_id[$current_item])) && die "Duplicate item: ${(qqq)current_item}" item_make_id $current_item } #typeset -f -t item_start item_end() { [[ -z $current_item ]] && die "No opened item" (($#)) && item_title "$@" items+=( $current_item ) current_item= } item_title() { [[ -z $current_item ]] && die "No opened item" item_title[$current_item]=$* } item_prop() { [[ -z $current_item ]] && die "No opened item" (($# == 2)) || die "Must specify prop key and value" item_props+=( $current_item $1 $2 ) } item_link() { [[ -z $current_item ]] && die "No opened item" (($#)) || die "Must specify item name" links+=( $current_item ${(pj:\0:)*} ) } item_backlink() { [[ -z $current_item ]] && die "No opened item" (($#)) || die "Must specify item name" links+=( ${(pj:\0:)*} $current_item ) } load() { # load_vim load_x load_xpra load_tmux process_items } #typeset -f -t load c_exec() { local container=$1 display=${DISPLAY#:} shift if ! (( $+container_exec_linked[$container] )); then - s6-sudo /run/cmd.s link $container xsession.$display.$USER run/exec/exec 2>/dev/null # die_ret $? "Failed to link run/exec/exec from ${(qqq)container}" container_exec_linked[$container]=1 fi s6-sudo /run/inbox/$container/run/exec/exec "$@" } #load_vim() { # local s b f # vim --serverlist | while read s; do # vim --servername $s --remote-expr "ccx#PrintBuffers()" | \ # while read b f; do # items+=($s $b $f) # done # done #} load_x() { local line wid w_host w_class w_instance w_title term_container term_pts local -a l #local MATCH MBEGIN MEND # used by =~ i3-msg -t get_tree | awk -f $HOME/i3_list_windows.awk -f $HOME/bin/JSON.bbawk -v STREAM=0 - | while IFS= read -r line; do l=( "${(@ps:\t:)line}" ) wid=$l[1] w_host=${(Q)l[2]} w_class=${(Q)l[3]} w_instance=${(Q)l[4]} w_title=${(Q)l[5]} item_start X=$wid item_title $w_title if [[ $w_host == $HOST && $w_class == URxvt ]]; then # local urxvt if [[ $w_instance == *:/dev/pts/* ]]; then term_container=${${w_instance%%:/dev/pts/*}//:/.} term_pts=${w_instance#*:/dev/pts/} item_link C=$term_container PTS=$term_pts else term_container= term_pts= fi # if [[ -n $term_container && $w_title == *tmux::*:$USER@${term_container%%.*} ]]; then # links[X$_0$wid]=C/$term_container/TMUX/${${w_title##*tmux::}%%:*} # elif [[ $w_title =~ 'VIM/[0-9]+$' ]]; then # links[X$_0$wid]=$MATCH # fi elif [[ $w_host == xpra && $w_title == *'«s=xpra c'* ]]; then local x= local -A xpra_info=( ) for x in "${(@s: :)${w_title##*«s=xpra }}"; do [[ $x == ?=* ]] || die "Malformed xpra info: ${(qqq)x}" xpra_info[${x%%=*}]=${x#?=} done item_link XPRA=${xpra_info[d]} C=${xpra_info[c]}.$USER X=${xpra_info[w]} fi item_end done } #typeset -f -t load_x load_xpra() { local disp container wid w_desktop w_pid w_host w_title c_exec xpra.$USER wmctrl-all | while IFS=$'\t' read -r disp container wid w_desktop w_pid w_host w_title; do item_start XPRA=$disp C=$container X=$wid item_title $w_title item_prop pid $w_pid item_prop desktop $w_desktop item_prop host $w_host item_end done } #typeset -f -t load_xpra load_container_tmux() { local container line has_tmux=0 n local -a tmux_vars tmux_fmt l local -A info #local MATCH MBEGIN MEND # used by =~ container=$1 tmux_vars=( session_name session_id window_index window_active pane_id pane_active pane_title pane_current_command ) tmux_fmt=( "#{"$^tmux_vars"}" ) c_exec 2>/dev/null $container tmux list-panes -a -F ${(pj:\t:)tmux_fmt} | \ while IFS= read line; do l=( "${(@ps:\t:)line}" ) info=( ) for n in {1..$#tmux_vars}; do info[$tmux_vars[$n]]=${l[$n]} done item_start C=$container TMUX=$info[session_name] PANE=$info[pane_id] item_title "$info[pane_title] [$info[pane_current_command]]" item_prop pane_window $info[window_index] item_backlink C=$container TMUX=$info[session_name] if [[ $info[window_active] == 1 ]]; then item_props+=( C=$container${_0}TMUX=$info[session_name] active_window $info[window_index] ) if [[ $info[pane_active] == 1 ]]; then item_props+=( C=$container${_0}TMUX=$info[session_name] active_pane $info[pane_id] ) item_props+=( C=$container${_0}TMUX=$info[session_name] session_id $info[session_id] ) item_title "*$info[pane_title] [$info[pane_current_command]]" fi fi # if [[ $pane_title =~ 'VIM/[0-9]+$' ]]; then # links[C/$container/TMUX/$session_name$_0$window_index/$pane_id]=$MATCH # fi item_end has_tmux=1 done ((has_tmux)) || return tmux_vars=( client_tty session_name window_index pane_id #client_flags #client_termfeatures #client_activity #client_created #client_readonly ) tmux_fmt=( "#{"$^tmux_vars"}" ) c_exec $container tmux list-clients -F ${(pj:\t:)tmux_fmt} | \ while IFS= read line; do l=( "${(@ps:\t:)line}" ) info=( ) for n in {1..$#tmux_vars}; do info[$tmux_vars[$n]]=$l[$n] done [[ $info[client_tty] == /dev/pts/* ]] || continue item_start C=$container PTS=${info[client_tty]:t} item_link C=$container TMUX=$info[session_name] item_prop tmux_window_index $info[window_index] item_prop tmux_pane_id $info[pane_id] item_end done } #typeset -f -t load_container_tmux load_tmux() { local -A service_up local sv container service_up=( $(s6-sudo /run/cmd.s services -u) ) for sv in ${(k)service_up[(R)true]}; do [[ $sv == /run/service/container.*.$USER ]] || continue container=${sv##*/container.} load_container_tmux $container done return local session_name window_index pane_id pane_title #local MATCH MBEGIN MEND # used by =~ tmux list-panes -a -F '#{session_name}|#{window_index}|#{pane_id}|#{pane_title} | #{pane_current_command}' | \ while IFS='|' read session_name window_index pane_id pane_title; do item_start TMUX=$session_name PANE=$pane_id item_title $pane_title item_prop pane_window $window_index item_backlink TMUX=$session_name # if [[ $pane_title =~ 'VIM/[0-9]+$' ]]; then # links[C/$container/TMUX/$session_name$_0$window_index/$pane_id]=$MATCH # fi item_end done } process_items() { local id oid name other k v typeset -gA id_links id_backlinks id_props for name other in $links; do item_make_id $name id=$item_to_id[$name] item_make_id $other oid=$item_to_id[$other] (($+id_links[$id])) && id_links[$id]+=$_0 id_links[$id]+=$oid (($+id_backlinks[$oid])) && id_backlinks[$oid]+=$_0 id_backlinks[$oid]+=$id #printf '%s => %s [%d]\n' $id $oid $#id_links[$id] #printf '%s => %s\n' "${(j: :)${(0)name}}" "${(j: :)${(0)other}}" done for name k v in "$item_props[@]"; do id=$item_to_id[$name] (($+id_props[$id/$k])) && [[ $id_props[$id/$k] != $v ]] && die "Duplicate prop ${(qqq)k} in item ${(qqq)name}" id_props[$id/$k]=$v done for id in ${(k)id_to_item}; do typeset -ga "item_${id}_links" if (($+id_links[$id])); then set -A "item_${id}_links" $id_links[$id] else fi typeset -ga "item_${id}_backlinks" if (($+id_backlinks[$id])); then set -A "item_${id}_backlinks" $id_backlinks[$id] else fi typeset -gA item_${id}_props local -a kv=( ) for k v in "${(kv@)id_props[(I)$id/*]}"; do kv+=( $k $v ) done set -A item_${id}_props "$kv[@]" done typeset -gA id_can_show id_can_omit for id name in ${(kv)id_to_item}; do id_can_show[$id]=$(( $+item_title[$name] )) done for id in {1..$((next_item_id - 1))}; do (( $+id_links[$id] || ! $id_can_show[$id] )) && continue (( $+id_backlinks[$id] )) || continue # keep this id, try omiting it's parents id_can_omit[$id]=0 for oid in ${(0)id_backlinks[$id]}; do process_items_try_omit $oid done done for id in {1..$((next_item_id - 1))}; do id_can_omit[$id]=${id_can_omit[$id]:-0} done } #typeset -f -t process_items process_items_try_omit() { local n parent local -a children n=$1 (( $+id_links[$n] )) && children=( ${(0)id_links[$n]} ) #local -a parents #debug #(( $+id_backlinks[$n] )) && parents=( ${(0)id_backlinks[$n]} ) #printf >&2 '[%d] ↓ %s ↑ %s\n' $n ${(j:,:)children:--} ${(j:,:)parents:--} [[ $#children == 1 ]] || return id_can_omit[$n]=1 (( $+id_backlinks[$n] )) || return for parent in ${(0)id_backlinks[$n]}; do process_items_try_omit $parent done } #typeset -f -t process_items_try_omit _item_color() { local n n=$1 if (($+id_selected && n == id_selected)); then - printf '%s' $hl_success elif ((!$id_can_show[$n])); then - printf '%s' $hl_fatal elif (($id_can_omit[$n])); then - printf '%s' $hl_warn fi } _item_addr() { local n item n=$1 item=$id_to_item[$n] - printf '%s\n' "$(_item_color $n)${(j: » :)${(0)item}}$hl_reset" } print_tree_recurse() { local n indent inum itxt subindent item title n=$1 indent=${2:-} for inum in ${(s::)indent}; do itxt+=$print_tree_indent_style[$inum] done item=$id_to_item[$n] title=${item_title[$item]:-} subindent=${${indent//3/2}//4/1} printf '%s- %s « %s « %s\n' "$itxt" "$(_item_addr $n)" ${(qqq)title} $n #printf '%s- %s « %s « %s\n' "$itxt" "$(_item_color $n)${(j: » :)${(0)item}}$hl_reset" ${(qqq)title} $n if (( $+id_links[$n] )); then local -a ls=( ${(0)id_links[$n]} ) local i l for i in {1..$#ls}; do l=$ls[i] if (( $i < $#ls )); then - print_tree_recurse $l ${subindent}3 else - print_tree_recurse $l ${subindent}4 fi done fi } #typeset -f -t print_tree_recurse print_subtree_recurse() { local n parent n=$1 if (( $+id_backlinks[$n] )); then for parent in ${(0)id_backlinks[$n]}; do print_subtree_recurse $parent done else print_tree_recurse $n fi } typeset -ga print_tree_indent_style=( ' ' # 1 ' | ' # 2 ' |-' # 3 ' `-' # 4 ) print_tree() { local -a stack ls local n item title l for n in {1..$((next_item_id - 1))}; do (( $+id_backlinks[$n] )) && continue - print_tree_recurse $n done } print_options() { local n item title addr for n in {1..$((next_item_id - 1))}; do item=$id_to_item[$n] addr=${(j: » :)${(0)item}} #addr=$(_item_addr $n) if ((!$id_can_show[$n])); then continue elif (($id_can_omit[$n])); then continue fi title=${item_title[$item]:-N/A} printf '%s\n' "$n$delim$title | $addr" done } choose() { local result local -a selected print_options | $dmenu_cmd | read result if [[ $? == 0 && -n $result ]]; then selected=( "${(@s: » :)result}" ) printf 'selected: %s\n' "$(_item_addr $selected[1])" activate $selected[1] fi } test_activation() { local id item addr local -a options local -g id_selected options=( ${${(f)"$(print_options)"}%% *} ) for id in $options; do id_selected=$id typeset -ga plan_steps=( ) plan_activation $id print_subtree_recurse $id pretend_activate echo >&2 done } activate_x_window() { i3-msg "[id=$1] focus" } add_to_plan() { plan_steps+=( "${(pj:\0:)@}" ) } plan_activation() { local id item x structure k v p local -a parents full props_kv struct_values local -A props id=$1 item=$id_to_item[$id] full=( ${(0)item} ) (( $+id_backlinks[$id] )) && parents=( ${(0)id_backlinks[$id]} ) for x in ${(0)${id_to_item[$id]}}; do (($#structure)) && structure+=' ' structure+=${x%%=*} struct_values+=( ${x#*=} ) done #for k v in "${(kv@)id_props[(I)$id/*]}"; do # k=${k%*/} # props[$k]=$v #done case $structure in (X) add_to_plan i3-msg "[id=${struct_values[1]}] focus" ;; (XPRA C X) if (($#parents)); then plan_activation $parents[1] else # TODO restore layout / activate waiting container? add_to_plan run-in-container-xpra $struct_values[2] true fi ;; (C PTS) (($#parents == 1)) || die "urxvt without window" plan_activation $parents[1] ;; (C TMUX PANE) (($#parents == 1)) || die "tmux pane without session" p=$parents[1] if [[ $struct_values[3] != ${id_props[$p/active_pane]:-missing} ]]; then add_to_plan c.exec $struct_values[1] \ tmux select-pane $struct_values[3] \; \ select-window -t $struct_values[2]:${id_props[$id/pane_window]} fi plan_activation $p ;; (C TMUX) if (($#parents)); then plan_activation $parents[1] else add_to_plan c.tmux $struct_values[1] -s $struct_values[2] fi ;; (*) set +x err_msg "Could not resolve ${id_selected}" print_subtree_recurse $id_selected die "Unhandled structure: ${(qqq)structure}" ;; #(VIM/*) vim --servername $1 --remote-expr "ccx#SwitchBuffer($2)";; esac } #typeset -f -t plan_activation pretend_activate() { local i if ! (($#plan_steps)); then warn_msg "Nothing to do" return fi for i in {$#plan_steps..1}; do :: "${(@0)plan_steps[$i]}" done } do_activate() { local i if ! (($#plan_steps)); then warn_msg "Nothing to do" return fi for i in {$#plan_steps..1}; do "${(@0)plan_steps[$i]}" done } activate() { local id id=$1 typeset -ga plan_steps - plan_activation $id do_activate &! } #typeset -f -t activate main() { - load #- print_tree >&2 #echo #- print_options #- test_activation - choose } main "$@"