baskerville / bspwm

A tiling window manager based on binary space partitioning
BSD 2-Clause "Simplified" License
7.61k stars 420 forks source link

Wondering where I might be able to share a tool I've created, `bspcd` (d for dump), `bspcq` (q for query) #1354

Open aidenlangley opened 2 years ago

aidenlangley commented 2 years ago

It's not trivial to try to find the className, or 'flags', on a node, or other relevant information about any bsp domain. I wrote some code that makes life a little easier, and I'm wondering if it'll be of any use to a wider audience, and if so, where might it go?

This is the kind of information it produces - the idea is it's for humans, but it shouldn't be all that difficult to be consumed by other applications. If it's useful, it can of course be enhanced in a number of ways.

It's in Python, with a sh wrapper for invocation via bspcd n for node info, bspcd d/m for desktop/monitor, yada yada. I'm adding a --fancy flag so a very human readable format can be output instead.

Edit: Removed previews, more up to date images below.

emanuele6 commented 2 years ago

my jq clone:

def camel_to_snake: gsub("(?<x>[[:upper:]])"; "_\(.x|ascii_downcase)");
def dump_object: to_entries[] | "\(.key|camel_to_snake)\t-> \(.value)";

.monitors[] | (
    { monitorName: .name, windowGap, borderWidth, padding } |
    dump_object
), "", (
    .desktops[] | (
        { desktopName: .name, splitRatio } |
        dump_object
    ), "", (
        .root | [ .. | .client? | values ] | select(length != 0) | (
            .[] |
            { className, instanceName } |
            dump_object
        ), ""
    )
)

invoke with:

bspc wm -d | jq -rf ./bspcd.jq

EDIT: the desktops' split ratios are all null because bspwm is not including desktop split ratios in wm -d's output apparently :D

aidenlangley commented 2 years ago

Nice, yeah, I am going for something user friendly and easy on the eyes with a rich output.

aidenlangley commented 2 years ago

Fair warning, there's a lot code...

Here's the wrapper, and a snippet of the meat & potatoes:

#! /usr/bin/env sh

# This is the default behaviour of the script, calling me without any arguments
# will invoke the same command with an argument provided.
if [ -z "$1" ]; then
    bspcd node
    exit 0
fi

case "$1" in
    # These arguments are valid, append to this list to support more.
    monitor | m | desktop | d | node | n | all | a)
        for mon_id in $(bspc query -M); do
            case "$1" in
                monitor | m)
                    bspcq "$(bspc query -T -m $mon_id)" -d monitor
                    ;;
                desktop | d)
                    bspcq "$(bspc query -T -m $mon_id)" -d desktop
                    ;;
                node | n)
                    bspcq "$(bspc query -T -m $mon_id)" -d node
                    ;;
                all | a)
                    bspcq "$(bspc query -T -m $mon_id)" -d node -a
                    ;;
            esac
        done
        ;;
    *)
        echo "Unrecognised argument: $1"
        exit 1
        ;;
esac
def get_node_info(bsp_tree: dict[str, Any]) -> dict[str, Any]:
    """
    Curated list of `node` properties that will be returned by our
    application.
    """

    # This is a system call to `xtitle`, given the ID of the node, and output is
    # sent to /dev/null and read back from stdout later.
    xtitle_proc = subprocess.run([
        'xtitle', str(bsp_tree['id']), '&>', '/dev/null'
    ], capture_output=True)

    node_info = {
        'id': bsp_tree['id'],
        'class_name': bsp_tree['client']['className'],
        'instance_name': bsp_tree['client']['instanceName'],
        'xtitle': xtitle_proc.stdout.decode('utf-8').rstrip()
    }

    return node_info

def recurse_nodes(
    bsp_tree: dict[str, Any],
    nodes: list[dict[str, Any]],
    all: bool,
    properties: list[str]
) -> list[dict[str, Any]]:
    """`bspwm` is simple, but it isn't easy. -zyk

    Probably the most complex piece of the puzzle - we repeatedly
    `get_node_info` here, since in theory we can have an infinite number of
    `node`s.
    """

    # Absence of `firstChild` means there is only a single active `node` on the
    # `desktop`, so we don't have to go fishing for children.
    if bsp_tree['firstChild'] is None:
        nodes.append(get_info('node', bsp_tree, all, properties))

    else:
        if bsp_tree['firstChild'].get('client'):
            nodes.append(get_info('node', bsp_tree['firstChild'], all, properties))

            # In the event that there is a `firstChild`, there will be a
            # `secondChild`. A `secondChild` can have a `firstChild`, and so on.
            recurse_nodes(bsp_tree['secondChild'], nodes, all, properties)

        # A `firstChild` without a `client` is a branch without leaves - no
        # `node`s but there are more branches...
        else:
            recurse_nodes(bsp_tree['firstChild'], nodes, all, properties)

    return nodes

Edit:

Then when you're like, hey, this window ought go to desktop 3, what's it called? What's the bspc syntax again? man this & man that, but instead you can run bspcd and:

M: 4194308 HDMI-A-0
└── D: 4194310 1
    ├── N: 33554476 Firefox: Inbox | Fastmail — Mozilla Firefox
    └── N: 33554508 Firefox: Wondering where I might be able to share a tool I've created,
        `bspcd` (d for dump), `bspcq` (q for query) · Issue #1354 · baskerville/bspwm — Mozilla
        Firefox
M: 4194306 DisplayPort-0
├── D: 4194311 2
│   ├── N: 44040193 Code: bspcq - bspcq - Visual Studio Code
│   ├── N: 94371842 Alacritty: bspcq -s ~
│   └── N: 104857602 Alacritty: ~/Code/bspcq
├── D: 4194312 3
│   └── N: 44040227 Code: bspcq - bin - Visual Studio Code
├── D: 4194313 4
├── D: 4194314 5
│   ├── N: 58720259 Google-chrome: Inbox - aiden@wowhub.co.nz - wowhub Mail - Google Chrome
│   └── N: 58720271 Google-chrome: Aiden Langley Service Agreement - Google Docs - Google Chrome
└── D: 4194315 6
aidenlangley commented 2 years ago

Here's an update: https://github.com/aidenlangley/bspcq