Skip to content

Instantly share code, notes, and snippets.

@wwalker
Created July 8, 2021 16:03
Show Gist options
  • Save wwalker/48409bba154ff0cba253255dbdc00bc2 to your computer and use it in GitHub Desktop.
Save wwalker/48409bba154ff0cba253255dbdc00bc2 to your computer and use it in GitHub Desktop.
my i3 alt-tab functionality - work in progress
#!/usr/bin/env python3
#
# modified by wwalker (wwalker@solid-constructs.com) from example script
# https://github.com/altdesktop/i3ipc-python/blob/master/examples/i3-cycle-focus.py
#
# provides alt+tab functionality between windows, switching
# between n windows; example i3 conf to use:
# exec_always --no-startup-id i3-alt-tab-ww
# bindsym Shift+$alt+Tab exec --no-startup-id i3-select-recent-window
# bindsym $alt+Tab exec --no-startup-id i3-alt-tab-ww --switch
import os
import time
import asyncio
from argparse import ArgumentParser
import logging
import pprint
from i3ipc.aio import Connection
SOCKET_FILE = '/tmp/.i3-cycle-focus.sock'
MAX_WIN_HISTORY = 16
UPDATE_DELAY = 0.05
def on_shutdown(i3_conn, e):
os._exit(0)
class FocusWatcher:
def __init__(self):
self.i3 = None
self.window_list = {}
self.update_task = None
async def connect(self):
self.i3 = await Connection().connect()
self.i3.on('window::focus', self.on_window_focus)
self.i3.on('shutdown', on_shutdown)
async def update_window_list(self, window_id):
if UPDATE_DELAY != 0.0:
await asyncio.sleep(UPDATE_DELAY)
logging.info('updating window list')
self.window_list[window_id] = time.time()
logging.info('new window list: {}'.format(self.window_list))
logging.info('new window list: {}'.format(self.ids()))
async def get_all_windows(self):
tree = await self.i3.get_tree()
return tree.leaves()
async def get_valid_windows(self):
tree = await self.i3.get_tree()
if args.active_workspace:
return set(w.id for w in tree.find_focused().workspace().leaves())
elif args.visible_workspaces:
ws_list = []
w_set = set()
outputs = await self.i3.get_outputs()
for item in outputs:
if item.active:
ws_list.append(item.current_workspace)
for ws in tree.workspaces():
if str(ws.name) in ws_list:
for w in ws.leaves():
w_set.add(w.id)
return w_set
else:
return set(w.id for w in tree.leaves())
async def on_window_focus(self, i3conn, event):
logging.info('got window focus event:')
# pprint.pprint(event.ipc_data)
if args.ignore_float and (event.container.floating == "user_on"
or event.container.floating == "auto_on"):
logging.info('not handling this floating window')
return
if self.update_task is not None:
self.update_task.cancel()
logging.info('scheduling task to update window list')
self.update_task = asyncio.create_task(self.update_window_list(event.container.id))
def ids(self):
tmp = sorted(self.window_list, key=self.window_list.get)
tmp.reverse()
return tmp
async def run(self):
async def handle_requests(reader, writer):
data = await reader.read(1024)
logging.info('received data: {}'.format(data))
if data == b'list':
id_list = list( map(lambda x: str(x), self.ids()) )
print('Starting with {} windows in the list.'.format(len(id_list)))
windows = await self.get_all_windows()
pprint.pprint(windows[0])
window_ids = list( map(lambda x: x.id, windows) )
print(window_ids)
def window_description(id):
print('describing id: {}'.format(id))
id = int(id)
l = list( filter(lambda x: x.id == id, windows) )
if len(l) == 1:
w = l[0]
pprint.pprint(w)
return '\t'.join( [ str(w.id), str(w.window), w.window_class, w.window_instance, w.window_title ] )
return False
# for id in id_list:
# if id not in window_ids:
# print('removing {}'.format(id))
# id_list.remove(id)
print('I have {} windows listed now.'.format(len(id_list)))
window_descs = '\n'.join( filter(lambda x: x, map(window_description, id_list) ) )
writer.write(bytes(window_descs, 'utf-8'))
if data == b'switch':
logging.info('switching window')
windows = await self.get_valid_windows()
logging.info('valid windows = {}'.format(windows))
ids = self.ids()
logging.info('ids is of type {} and value {}'.format(type(ids), ids))
current_focused_window = ids[0]
ids.pop(0)
for window_id in ids:
if window_id not in windows:
logging.info('window_id is of type {} and value {}'.format(type(window_id), window_id))
del self.window_list[ window_id ]
else:
logging.info('focusing window id={}'.format(window_id))
# # Change the "last focused time" of the most recently focused window to now
# self.window_list[current_focused_window] = time.time
await self.i3.command('[con_id={}] focus'.format(window_id))
break
server = await asyncio.start_unix_server(handle_requests, SOCKET_FILE)
await server.serve_forever()
async def send_list():
reader, writer = await asyncio.open_unix_connection(SOCKET_FILE)
logging.info('sending list message')
writer.write('list'.encode())
await writer.drain()
data = await reader.read(1024)
print(data.decode("utf-8"))
logging.info('closing the connection')
writer.close()
await writer.wait_closed()
async def send_switch():
reader, writer = await asyncio.open_unix_connection(SOCKET_FILE)
logging.info('sending switch message')
writer.write('switch'.encode())
await writer.drain()
logging.info('closing the connection')
writer.close()
await writer.wait_closed()
async def run_server():
focus_watcher = FocusWatcher()
await focus_watcher.connect()
await focus_watcher.run()
if __name__ == '__main__':
parser = ArgumentParser(prog='i3-cycle-focus.py',
description="""
Cycle backwards through the history of focused windows (aka Alt-Tab).
This script should be launched from ~/.xsession or ~/.xinitrc.
Use the `--history` option to set the maximum number of windows to be
stored in the focus history (Default 16 windows).
Use the `--delay` option to set the delay between focusing the
selected window and updating the focus history (Default 2.0 seconds).
Use a value of 0.0 seconds to toggle focus only between the current
and the previously focused window. Use the `--ignore-floating` option
to exclude all floating windows when cycling and updating the focus
history. Use the `--visible-workspaces` option to include windows on
visible workspaces only when cycling the focus history. Use the
`--active-workspace` option to include windows on the active workspace
only when cycling the focus history.
To trigger focus switching, execute the script from a keybinding with
the `--switch` option.""")
parser.add_argument('--history',
dest='history',
help='Maximum number of windows in the focus history',
type=int)
parser.add_argument('--delay',
dest='delay',
help='Delay before updating focus history',
type=float)
parser.add_argument('--ignore-floating',
dest='ignore_float',
action='store_true',
help='Ignore floating windows '
'when cycling and updating the focus history')
parser.add_argument('--visible-workspaces',
dest='visible_workspaces',
action='store_true',
help='Include windows on visible '
'workspaces only when cycling the focus history')
parser.add_argument('--active-workspace',
dest='active_workspace',
action='store_true',
help='Include windows on the '
'active workspace only when cycling the focus history')
parser.add_argument('--list',
dest='list',
action='store_true',
help='Output the window focus list',
default=False)
parser.add_argument('--switch',
dest='switch',
action='store_true',
help='Switch to the previous window',
default=False)
parser.add_argument('--debug', dest='debug', action='store_true', help='Turn on debug logging')
args = parser.parse_args()
if args.debug:
logging.basicConfig(level=logging.DEBUG)
if args.history:
MAX_WIN_HISTORY = args.history
if args.delay:
UPDATE_DELAY = args.delay
else:
if args.delay == 0.0:
UPDATE_DELAY = args.delay
if args.list:
asyncio.run(send_list())
else:
if args.switch:
asyncio.run(send_switch())
else:
asyncio.run(run_server())
exec_always --no-startup-id i3-alt-tab-ww
bindsym Shift+$alt+Tab exec --no-startup-id i3-select-recent-window
bindsym $alt+Tab exec --no-startup-id i3-alt-tab-ww --switch
#!/bin/bash
ids=$(i3-alt-tab-ww --list)
[[ -z "$ids" ]] && exit 1
id=$( printf "%s" "$ids" | rofi -dmenu -selected-row 1 | awk '{print $1}' )
[[ -z "$id" ]] && exit
i3-msg "[con_id=$id] focus"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment