Skip to content

Instantly share code, notes, and snippets.

@nejni-marji
Last active August 12, 2019 01:17
Show Gist options
  • Save nejni-marji/53be7b2f69719c8af48e687f88fd04fc to your computer and use it in GitHub Desktop.
Save nejni-marji/53be7b2f69719c8af48e687f88fd04fc to your computer and use it in GitHub Desktop.
tool to swap between splits and tabs/stacks in i3wm
#!/bin/zsh
#
# flipsplit.zsh
#
# {{{ help text
show_help() {
>&2 <<-EOM
Usage: $0 STYLE [WORKSPACE]
STYLE can be either 'split' or 'merge'.
If WORKSPACE is not specified, default to the currently focused workspace.
-h, --help show this message and exit
EOM
exit 0
}
if [[ $1 =~ '^(-?h|(--)?help)$' ]] ; then
show_help
fi
# }}}
# {{{ validate arguments
oldStyle="$1"
wsNum="$2"
{
! [[ $oldStyle =~ '^(split|merge)$' ]] \
||
! [[ $wsNum =~ '^[[:digit:]]*$' ]]
} && show_help # this runs exit()
[[ -z $wsNum ]] && wsNum="$(i3-msg -t get_workspaces | jq '.[] | select(.focused) | .num')"
# }}}
# functions for fs_enable()
# {{{ helper json data
STYLES='{
"splith": "split",
"splitv": "split",
"stacked": "merge",
"tabbed": "merge"
}'
FLIPS='{
"splith": "tabbed",
"splitv": "stacked",
"tabbed": "splith",
"stacked": "splitv"
}'
# }}}
# {{{ fs_fix_ws_node(wsNum, oldStyle)
# Returns as a string a command which fixes a bad workspace object.
# If the workspace has more than 1 child, then we must individually set its
# layout. The workspace object is weird when it comes to its layout, and
# this is required to make it usable for the rest of the program.
# Uses pseudo-constant $STYLES and $FLIPS.
# }}}
fs_fix_ws_node() { # {{{
i3-msg -t get_tree \
| jq \
--argjson STYLES "$STYLES" \
--argjson FLIPS "$FLIPS" \
--argjson wsNum "$1" \
--arg oldStyle "$2" \
'
..
| select(.num?==$wsNum)
| if
(.nodes | length)>=2
and
($STYLES[.layout]==$oldStyle)
and
(any(
# the gen and cond should be changed later.
# dupe_fs_excl
.nodes[].marks?[]? ;
startswith("fs_excl")
or
startswith("FS_EXCL")
) | not)
then
"[con_id=\(.id)] layout \(.layout)"
# "[con_id=\(.id)] layout \($FLIPS[.layout])"
# + " ; "
# + "[con_id=\(.id)] layout \(.layout)"
else
null
end
'
}
# }}}
# {{{ fs_get_nodes_to_flip(wsNum, oldStyle)
# Returns node objects that should be flipped, not in an array.
# This ignores the workspace object itself, which is checked separately.
# Uses pseudo-constant $STYLES.
# }}}
fs_get_nodes_to_flip() { # {{{
i3-msg -t get_tree \
| jq \
--argjson STYLES "$STYLES" \
--argjson wsNum "$1" \
--arg oldStyle "$2" \
'
..
| select(.num?==$wsNum)
| .nodes[]
| ..
| select(
has("id")?
and
(.nodes | length)>=2
and
($STYLES[.layout]==$oldStyle)
and
(any(
# the gen and cond should be changed later.
# dupe_fs_excl
.nodes[].marks?[]? ;
startswith("fs_excl")
or
startswith("FS_EXCL")
) | not)
)
'
}
# }}}
# {{{ fs_flip_nodes()
# Input is a sequence of object records, not an array.
# Input is given by STDIN.
# Returns string along the lines of:
# "[con_id=94322622271600] layout tabbed, mark --add --toggle fs_used_ID"
# Uses pseudo-constant $FLIPS
# (This should be chained directly after fs_get_nodes_to_flip.)
# We need two statements. One for the parent, to set mark, and one for the
# child, to set layout.
# }}}
fs_flip_nodes() { # {{{
jq \
--argjson FLIPS "$FLIPS" \
'
"[con_id=\(.nodes[0].id)] layout \($FLIPS[.layout])"
+ " ; "
+ "[con_id=\(.id)] mark --add \"fs_used_\(.id)\""
'
}
# }}}
# {{{ helpers for enable
fs_cmd_fix() {
fs_fix_ws_node \
"$wsNum" "$oldStyle" \
| jq -re
}
fs_cmd_flip() {
fs_get_nodes_to_flip \
"$wsNum" "$oldStyle" \
| fs_flip_nodes \
| jq -sre 'join(" ; ")'
}
# }}}
fs_enable() { # {{{
# fs_fix_ws_node "$wsNum" "$oldStyle"
# fs_get_nodes_to_flip "$wsNum" "$oldStyle" | fs_flip_nodes
# If we want to change the workspace node, we have to force it to have only one
# child node before doing anything else. This is because the workspace node
# refuses to have a merge style under normal conditions.
if cmd_fix_ws_node="$(fs_cmd_fix)" ; then
# notify-send -a flipsplit 'fixing ws node'
print "${(q)cmd_fix_ws_node}"
i3-msg "$cmd_fix_ws_node"
# sleep 0.25
fi
if cmd_flip_nodes="$(fs_cmd_flip)" ; then
# notify-send -a flipsplit 'flipping nodes'
print "${(q)cmd_flip_nodes}"
i3-msg "$cmd_flip_nodes"
fi
} # }}}
# functions for fs_enable()
# {{{ fs_get_nodes_to_unflip(wsNum, oldStyle)
# Returns node objects that should be unflipped, not in an array.
# Uses pseudo-constant $STYLES.
# }}}
fs_get_nodes_to_unflip() { # {{{
i3-msg -t get_tree \
| jq \
--argjson STYLES "$STYLES" \
--argjson wsNum "$1" \
--arg oldStyle "$2" \
'
..
| select(.num?==$wsNum)
| .nodes[]
| ..
| select(
has("id")?
and
(any(
.marks[]
;
startswith("fs_used")
))
)
'
}
# }}}
# {{{ fs_flip_nodes()
# }}}
fs_unflip_nodes() { # {{{
jq \
--argjson FLIPS "$FLIPS" \
'
"[con_id=\(.nodes[0].id)] layout \($FLIPS[.layout])"
+ " ; "
+ "[con_id=\(.id)] unmark \"fs_used_\(.id)\""
'
}
# }}}
# {{{ helpers for disable
fs_cmd_unflip() {
fs_get_nodes_to_unflip \
"$wsNum" "$oldStyle" \
| fs_unflip_nodes \
| jq -sre 'join(" ; ")'
}
# }}}
fs_disable() { # {{{
# fs_get_nodes_to_unflip "$wsNum" "$oldStyle" | fs_unflip_nodes
if cmd_unflip_nodes="$(fs_cmd_unflip)" ; then
# notify-send -a flipsplit 'unflipping nodes'
print "${(q)cmd_unflip_nodes}"
i3-msg "$cmd_unflip_nodes"
fi
}
# }}}
# {{{ fs_check_presence()
# Return table:
# true fs_used marks are present, use fs_disable
# false no such marks are present, use fs_enable
# }}}
fs_check_presence() { # {{{
i3-msg -t get_tree \
| jq \
-e \
--argjson wsNum "$wsNum" \
--arg oldStyle "$oldStyle" \
'
..
| select(.num?==$wsNum)
| any(
.. | .marks?[]?
;
startswith("fs_used_")
)
# | if . then "fs marks present" else "no marks present" end
# | if . then "disable" else "enable" end
'
}
# }}}
# Run final code?
if fs_check_presence ; then
# fs_used is present
fs_disable
else
# fs_used is not present
fs_enable
fi
# vim: syn=hash fdm=marker
# :syn match Comment /#.*/
I'm not sure what I'm expecting from posting this. If you find it interesting, I
guess just let me know what you think?
# Description
The basic idea for this is a script that toggles windows in i3:
splith<->tabbed
splitv<->stacked
Why? Sometimes I want to see more of one window, but fullscreen doesn't quite do
it. I set my display resolution lower sometimes when I'm using a TV instead of a
monitor, so stuff is easier to read, but at the same time, I have to change
splits to tabs so that windows aren't completely cramped. This is a response to
that specific problem, but I'm curious to see if anyone else might find it
useful.
This tool will also do the inverse, changing tabs to splits, purely because it's
a symmetric idea in the first place. However, if you happen to have some nested
tabs/stacks, briefly turning them all into splits might be a nice way to quickly
see how they're laid out?
# {{{ Terminology
FlipSplit -> to flip between split and merge.
split -> parent containers with layout splith or splitv
merge -> parent containers with layout tabbed or stacked
style -> to refer to whether something is split or merge.
FS_USED -> mark for containers whose layouts have been changed by
FlipSplit.
FS_EXCL -> mark, to be applied to parent containers or leaf containers, to
prevent that container or any of its parents from having its layout
modified by FlipSplit.
FSOLD -> The type of layouts (split or merge) that we intend to change.
[note 2]
FSNEW -> The type of layouts (merge or split) that we intend to apply.
# I might use ThisCase, thisCase, or this_case, in various places.
# I'm... bad at deciding. Accept it.
wsNum -> workspace number on which to operate
oldStyle -> FSOLD
newStyle -> FSNEW
nodesToFlip -> nodes to apply FS to
nodesToUndo -> nodes with FS_USED to undo FS
wmid -> node .id property (not .window). This is the ID that i3wm uses, not the
X11 window id.
# }}}
# {{{ Program flow:
Assign FSOLD to either 'split' or 'merge'.
Assign FSNEW to the opposite value.
Assign FSWS to the workspace number being used. [note 6]
Get entire workspace.
Grab all relevant parent nodes. [note 5, get_cons_by_ws]
Create an empty array to store i3-msg commands. (We don't want to run them at
execution, we want to build all of them and then run them joined by commas.
Race conditions suck, and I want to avoid any weirdness by doing multiple IPC
calls. This is how I always handle this sort of thing.)
(remember to pass these to any jq function, do not hardcode in testing)
VARIABLES:
FSWS
FSOLD
FSNEW
Do any of the nodes have FS_USED marks? [note 3]
If not FS_USED:
For each node:
Does it or any children have FS_EXCL marks? # done
If so, skip this node. # done
Is the node's layout style == FSOLD? # done
If not, skip this node. # done
.
Set the node's layout style to FSNEW.
Add the mark (--add) FS_USED.
Else if FS_USED:
For all nodes with the FS_USED mark:
[note 4]
Toggle the node's layout style.
Remove (careful with --add?) the FS_USED mark.
Finally, print out the IPC command array.
Once the main code flow is done, then add code to try to run those commands.
# }}}
# {{{ Notes
[1] ->
All marks being used will use some form of randomness to prevent
collisions, so that marks don't eat other marks. I've used UUIDs in the
past, but only for hidden marks. I might want these marks to be visible, and
UUIDs are long, so I might need a different kind of solution.
[2] ->
The FS_USED mark may also contain data for what FSOLD was.
[3] ->
If we only apply this mark to the parents, we only need to check the
containers we would be searching initially anyway. However, this might
presume that no containers are moved, which might not be true. I'll need to
consider this more later on.
[4] ->
For undoing FlipSplit, there's a change a FS_USED node may have a FS_EXCL
as a child. What should be done about this?
I propose creating a rigid form for this rather than trying to predict what
the user might want from the situation. This is for a few reasons:
1) Generally, we should expect that no marks are being created outside
of the script. This is in no way guaranteed, but if a user does it
manually, it's their own fault, and it's no longer my responsibility.
Such is the way of scripts.
2) I prefer my tools to have rigidly defined functional cases, and to
be fairly agnostic outside of those cases. If necessary, I am willing to
implement some corner cases to prevent wasteful behavior (infinite
loops) and to handle for very basic situations where the main logic
clearly does something nobody would want, but I prefer to avoid such
things.
Secondly, there may be containers with FS_USED that each have different
styles. This is to say, FS_USED on a split, and FS_USED on a merge.
This script could be invoked purely with undoing FS_USED in mind, in which
case we remain agnostic of it.
However, the invocation of this tool asks for an FSOLD value, and if a
value is provided, should we ignore containers with FSOLD, with FSNEW, be
agnostic and operate on both, or reject the operation entirely?
[5] ->
get_cons_by_ws is a function defined in my test scripts. It grabs all
windows on a particular workspace which have at least 2 child nodes.
[6] ->
Non-numerical workspaces are getting the shaft. Maybe after the thing works,
I'll add that, but for now, .num is the best identifier for workspace
objects.
# }}}
# {{{ 1st draft notes from .zsh file
# read
# flip_by_id "$1"
# Here's the design idea.
# From a plain default state:
# flipping a window adds a _fsed marker.
# unflipping can be performed automatically on only _fsed markers
# there should be a marker that goes on a leaf or parent, which excludes it
# from being flipped
# #
# the method to flip an entire workspace should either only flip tab/stack, or
# only flip splith/splitv, but not mix the two groups.
# that's to make sure that when we already have tabs in a workspace, to enter
# tabbed mode we don't make a split, or when entering splits mode, to not
# convert existing splits into tabs.
# #
# besides, for the most part, this is designed to condense splits into tabs,
# not the other way around. doing the reverse seems kinda silly to me, but
# i'll support it anyway since it's easier to include that to not.
# #
# lastly, what are our states?
# if any _fsed mark exists, assume we're in a fsed state. any at all means
# this.
# #
# Hmm...
# Relevant pieces of info for any given non-leaf node:
# does it or any children have _fs_no mark?
#
#
#
#
#
# }}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment