Last active
August 12, 2019 01:17
-
-
Save nejni-marji/53be7b2f69719c8af48e687f88fd04fc to your computer and use it in GitHub Desktop.
tool to swap between splits and tabs/stacks in i3wm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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