#!/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 "$@"