-
Notifications
You must be signed in to change notification settings - Fork 110
/
Copy pathi3-cycle-focus.py
executable file
·189 lines (159 loc) · 7.07 KB
/
i3-cycle-focus.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/env python3
#
# provides alt+tab functionality between windows, switching
# between n windows; example i3 conf to use:
# exec_always --no-startup-id i3-cycle-focus.py --history 2
# bindsym $mod1+Tab exec --no-startup-id i3-cycle-focus.py --switch
import os
import asyncio
from argparse import ArgumentParser
import logging
from i3ipc.aio import Connection
SOCKET_FILE = '/tmp/.i3-cycle-focus.sock'
MAX_WIN_HISTORY = 16
UPDATE_DELAY = 2.0
def on_shutdown(i3_conn, e):
os._exit(0)
class FocusWatcher:
def __init__(self):
self.i3 = None
self.window_list = []
self.update_task = None
self.window_index = 1
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')
if window_id in self.window_list:
self.window_list.remove(window_id)
self.window_list.insert(0, window_id)
if len(self.window_list) > MAX_WIN_HISTORY:
del self.window_list[MAX_WIN_HISTORY:]
self.window_index = 1
logging.info('new window list: {}'.format(self.window_list))
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')
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))
async def run(self):
async def handle_switch(reader, writer):
data = await reader.read(1024)
logging.info('received data: {}'.format(data))
if data == b'switch':
logging.info('switching window')
windows = await self.get_valid_windows()
logging.info('valid windows = {}'.format(windows))
for window_id in self.window_list[self.window_index:]:
if window_id not in windows:
self.window_list.remove(window_id)
else:
if self.window_index < (len(self.window_list) - 1):
self.window_index += 1
else:
self.window_index = 0
logging.info('focusing window id={}'.format(window_id))
await self.i3.command('[con_id={}] focus'.format(window_id))
break
server = await asyncio.start_unix_server(handle_switch, SOCKET_FILE)
await server.serve_forever()
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('--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.switch:
asyncio.run(send_switch())
else:
asyncio.run(run_server())