magbo system

i3 keeping mouse pointer after workspace switching

I use i3 window manager. If you switch to another workspace, mouse pointer will move to the center of the active window. It doesn’t matter for my work but sometimes (mainly on workspace where I have web browser) the centered mouse pointer obstructs the view and I’m forced give it away.

I created small utility which moves the mouse pointer to the last position for each workspace. It works with changing focus too.

Here is the part of my ~/.config/i3/config:

bindsym $mod+h exec "~/bin/i3-change.py focus --due left"
bindsym $mod+j exec "~/bin/i3-change.py focus --due down"
bindsym $mod+k exec "~/bin/i3-change.py focus --due up"
bindsym $mod+l exec "~/bin/i3-change.py focus --due right"

bindsym $mod+1 exec "~/bin/i3-change.py workspace 1"
bindsym $mod+2 exec "~/bin/i3-change.py workspace 2"
bindsym $mod+3 exec "~/bin/i3-change.py workspace 3"
bindsym $mod+4 exec "~/bin/i3-change.py workspace 4"
bindsym $mod+5 exec "~/bin/i3-change.py workspace 5"
bindsym $mod+6 exec "~/bin/i3-change.py workspace 6"
bindsym $mod+7 exec "~/bin/i3-change.py workspace 7"
bindsym $mod+8 exec "~/bin/i3-change.py workspace 8"
bindsym $mod+9 exec "~/bin/i3-change.py workspace 9"
bindsym $mod+0 exec "~/bin/i3-change.py workspace 10"

Here is the i3-change.py utility:

#!/usr/bin/python3


import click
import json
import logging
import os
import re
import subprocess


log = logging.getLogger(__name__)


def get_current_workspace():
    process = subprocess.run([
        'i3-msg',
        '-t',
        'get_workspaces',
    ], check=True, stdout=subprocess.PIPE)

    return [workspace['name'] for workspace in json.loads(process.stdout) if workspace['focused']][0]  # noqa: E501


def get_current_window_id():
    ps = subprocess.Popen([
        'i3-msg',
        '-t',
        'get_tree',
    ], stdout=subprocess.PIPE)

    process = subprocess.run([
        'jq',
        '-r',
        '.. | select(.focused? and .focused == true).id'
    ], check=True, stdin=ps.stdout, stdout=subprocess.PIPE)

    ps.wait()
    return process.stdout.decode('utf-8').strip()


def get_current_window_id2():
    process = subprocess.run([
        'i3-msg',
        '-t',
        'get_tree',
    ], check=True, stdout=subprocess.PIPE)

    def find(element, results=[]):
        if 'focused' in element and element['focused']:
            results.append(element)
        if 'nodes' in element:
            for node in element['nodes']:
                find(node, results)
        return results

    return find(json.loads(process.stdout))[0]['id']


def get_current_mouse():
    process = subprocess.run([
        'xdotool',
        'getmouselocation',
    ], check=True, stdout=subprocess.PIPE)

    match = re.match('x:(\\d+) y:(\\d+) screen:(\\d+) window:(\\d+)', process.stdout.decode('utf-8'))  # noqa: E501
    return match[1], match[2]


def communicate(data: str):
    fifo = '/tmp/in-memory-storage'
    if not os.path.exists(fifo):
        return None

    with open(fifo, 'w') as f:
        f.write(data)
    with open(fifo, 'r') as f:
        return f.read()


@click.group()
def cli():
    pass


@cli.command()
@click.option('--due', type=click.Choice(['left', 'up', 'down', 'right']))
def focus(due):
    old = get_current_workspace()
    mouse_x, mouse_y = get_current_mouse()
    communicate(f'i3-workspace-{old}={mouse_x} {mouse_y}')

    subprocess.run([
        'i3-msg',
        'focus',
        due,
    ], check=True)

    new = get_current_workspace()
    if old == new:
        return

    match = re.match('(\\d+) (\\d+)', communicate(f'i3-workspace-{new}'))
    if match:
        window_id = get_current_window_id2()

        subprocess.run([
            'xdotool',
            'mousemove',
            match[1],
            match[2],
        ], check=True)

        subprocess.run([
            'i3-msg',
            f'[con_id="{window_id}"] focus',
        ], check=True, stdout=subprocess.PIPE)


@cli.command()
@click.argument('name')
def workspace(name):
    mouse_x, mouse_y = get_current_mouse()
    communicate(f'i3-workspace-{get_current_workspace()}={mouse_x} {mouse_y}')

    subprocess.run([
        'i3-msg',
        'workspace',
        name,
    ], check=True)

    match = re.match('(\\d+) (\\d+)', communicate(f'i3-workspace-{name}'))
    if match:
        subprocess.run([
            'xdotool',
            'mousemove',
            match[1],
            match[2],
        ], check=True)


if __name__ == '__main__':
    cli()

It will be published on my Github repo https://github.com/petrposvic/scripts.

Leave a Reply

Your email address will not be published. Required fields are marked *