Skip to content

Commit aa89829

Browse files
author
Tony Crisci
authored
Merge pull request #78 from okraits/master
i3-cycle-focus.py: cycle through all previously focused windows
2 parents f132047 + 59274a2 commit aa89829

File tree

1 file changed

+166
-0
lines changed

1 file changed

+166
-0
lines changed

examples/i3-cycle-focus.py

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env python3
2+
3+
import os
4+
import socket
5+
import selectors
6+
import threading
7+
from argparse import ArgumentParser
8+
import i3ipc
9+
10+
SOCKET_FILE = '/tmp/i3-cycle-focus'
11+
MAX_WIN_HISTORY = 16
12+
UPDATE_DELAY = 2.0
13+
14+
15+
class FocusWatcher:
16+
17+
def __init__(self):
18+
self.i3 = i3ipc.Connection()
19+
self.i3.on('window::focus', self.on_window_focus)
20+
self.listening_socket = socket.socket(socket.AF_UNIX,
21+
socket.SOCK_STREAM)
22+
if os.path.exists(SOCKET_FILE):
23+
os.remove(SOCKET_FILE)
24+
self.listening_socket.bind(SOCKET_FILE)
25+
self.listening_socket.listen(1)
26+
self.window_list = []
27+
self.window_list_lock = threading.RLock()
28+
self.focus_timer = None
29+
self.window_index = 1
30+
31+
def update_windowlist(self, window_id):
32+
with self.window_list_lock:
33+
if window_id in self.window_list:
34+
self.window_list.remove(window_id)
35+
self.window_list.insert(0, window_id)
36+
if len(self.window_list) > MAX_WIN_HISTORY:
37+
del self.window_list[MAX_WIN_HISTORY:]
38+
self.window_index = 1
39+
40+
def get_valid_windows(self):
41+
tree = self.i3.get_tree()
42+
if args.active_workspace:
43+
return set(w.id for w in tree.find_focused().workspace().leaves())
44+
elif args.visible_workspaces:
45+
ws_list = []
46+
w_set = set()
47+
for item in self.i3.get_outputs():
48+
ws_list.append(item["current_workspace"])
49+
for ws in tree.workspaces():
50+
if str(ws.num) in ws_list:
51+
for w in ws.leaves():
52+
w_set.add(w.id)
53+
return w_set
54+
else:
55+
return set(w.id for w in tree.leaves())
56+
57+
def on_window_focus(self, i3conn, event):
58+
if args.ignore_float and (event.container.props.floating == "user_on" or
59+
event.container.props.floating == "auto_on"):
60+
return
61+
if UPDATE_DELAY != 0.0:
62+
if self.focus_timer is not None:
63+
self.focus_timer.cancel()
64+
self.focus_timer = threading.Timer(UPDATE_DELAY,
65+
self.update_windowlist,
66+
[event.container.props.id])
67+
self.focus_timer.start()
68+
else:
69+
self.update_windowlist(event.container.props.id)
70+
71+
def launch_i3(self):
72+
self.i3.main()
73+
74+
def launch_server(self):
75+
selector = selectors.DefaultSelector()
76+
77+
def accept(sock):
78+
conn, addr = sock.accept()
79+
selector.register(conn, selectors.EVENT_READ, read)
80+
81+
def read(conn):
82+
data = conn.recv(1024)
83+
if data == b'switch':
84+
with self.window_list_lock:
85+
windows = self.get_valid_windows()
86+
for window_id in self.window_list[self.window_index:]:
87+
if window_id not in windows:
88+
self.window_list.remove(window_id)
89+
else:
90+
if self.window_index < (len(self.window_list) - 1):
91+
self.window_index += 1
92+
else:
93+
self.window_index = 0
94+
self.i3.command('[con_id=%s] focus' % window_id)
95+
break
96+
elif not data:
97+
selector.unregister(conn)
98+
conn.close()
99+
100+
selector.register(self.listening_socket, selectors.EVENT_READ, accept)
101+
102+
while True:
103+
for key, event in selector.select():
104+
callback = key.data
105+
callback(key.fileobj)
106+
107+
def run(self):
108+
t_i3 = threading.Thread(target=self.launch_i3)
109+
t_server = threading.Thread(target=self.launch_server)
110+
for t in (t_i3, t_server):
111+
t.start()
112+
113+
114+
if __name__ == '__main__':
115+
parser = ArgumentParser(prog='i3-cycle-focus.py',
116+
description="""
117+
Cycle backwards through the history of focused windows (aka Alt-Tab).
118+
This script should be launched from ~/.xsession or ~/.xinitrc.
119+
Use the `--history` option to set the maximum number of windows to be
120+
stored in the focus history (Default 16 windows).
121+
Use the `--delay` option to set the delay between focusing the
122+
selected window and updating the focus history (Default 2.0 seconds).
123+
Use a value of 0.0 seconds to toggle focus only between the current
124+
and the previously focused window. Use the `--ignore-floating` option
125+
to exclude all floating windows when cycling and updating the focus
126+
history. Use the `--visible-workspaces` option to include windows on
127+
visible workspaces only when cycling the focus history. Use the
128+
`--active-workspace` option to include windows on the active workspace
129+
only when cycling the focus history.
130+
131+
To trigger focus switching, execute the script from a keybinding with
132+
the `--switch` option.""")
133+
parser.add_argument('--history', dest='history',
134+
help='Maximum number of windows in the focus history',
135+
type=int)
136+
parser.add_argument('--delay', dest='delay',
137+
help='Delay before updating focus history',
138+
type=float)
139+
parser.add_argument('--ignore-floating', dest='ignore_float',
140+
action='store_true', help='Ignore floating windows '
141+
'when cycling and updating the focus history')
142+
parser.add_argument('--visible-workspaces', dest='visible_workspaces',
143+
action='store_true', help='Include windows on visible '
144+
'workspaces only when cycling the focus history')
145+
parser.add_argument('--active-workspace', dest='active_workspace',
146+
action='store_true', help='Include windows on the '
147+
'active workspace only when cycling the focus history')
148+
parser.add_argument('--switch', dest='switch', action='store_true',
149+
help='Switch to the previous window', default=False)
150+
args = parser.parse_args()
151+
152+
if args.history:
153+
MAX_WIN_HISTORY = args.history
154+
if args.delay:
155+
UPDATE_DELAY = args.delay
156+
else:
157+
if args.delay == 0.0:
158+
UPDATE_DELAY = args.delay
159+
if not args.switch:
160+
focus_watcher = FocusWatcher()
161+
focus_watcher.run()
162+
else:
163+
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
164+
client_socket.connect(SOCKET_FILE)
165+
client_socket.send(b'switch')
166+
client_socket.close()

0 commit comments

Comments
 (0)