File: i3-cycle-focus.py

package info (click to toggle)
python-i3ipc 2.2.1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 580 kB
  • sloc: python: 2,968; makefile: 222; sh: 4
file content (184 lines) | stat: -rw-r--r-- 7,282 bytes parent folder | download | duplicates (2)
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
#!/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 socket
import selectors
import threading
from argparse import ArgumentParser
import i3ipc

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 = i3ipc.Connection()
        self.i3.on('window::focus', self.on_window_focus)
        self.i3.on('shutdown', on_shutdown)
        self.listening_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        if os.path.exists(SOCKET_FILE):
            os.remove(SOCKET_FILE)
        self.listening_socket.bind(SOCKET_FILE)
        self.listening_socket.listen(1)
        self.window_list = []
        self.window_list_lock = threading.RLock()
        self.focus_timer = None
        self.window_index = 1

    def update_windowlist(self, window_id):
        with self.window_list_lock:
            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

    def get_valid_windows(self):
        tree = 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()
            for item in self.i3.get_outputs():
                ws_list.append(item.current_workspace)
            for ws in tree.workspaces():
                if str(ws.num) 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())

    def on_window_focus(self, i3conn, event):
        if args.ignore_float and (event.container.floating == "user_on"
                                  or event.container.floating == "auto_on"):
            return
        if UPDATE_DELAY != 0.0:
            if self.focus_timer is not None:
                self.focus_timer.cancel()
            self.focus_timer = threading.Timer(UPDATE_DELAY, self.update_windowlist,
                                               [event.container.id])
            self.focus_timer.start()
        else:
            self.update_windowlist(event.container.id)

    def launch_i3(self):
        self.i3.main()

    def launch_server(self):
        selector = selectors.DefaultSelector()

        def accept(sock):
            conn, addr = sock.accept()
            selector.register(conn, selectors.EVENT_READ, read)

        def read(conn):
            data = conn.recv(1024)
            if data == b'switch':
                with self.window_list_lock:
                    windows = self.get_valid_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
                            self.i3.command('[con_id=%s] focus' % window_id)
                            break
            elif not data:
                selector.unregister(conn)
                conn.close()

        selector.register(self.listening_socket, selectors.EVENT_READ, accept)

        while True:
            for key, event in selector.select():
                callback = key.data
                callback(key.fileobj)

    def run(self):
        t_i3 = threading.Thread(target=self.launch_i3)
        t_server = threading.Thread(target=self.launch_server)
        for t in (t_i3, t_server):
            t.start()


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)
    args = parser.parse_args()

    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 not args.switch:
        focus_watcher = FocusWatcher()
        focus_watcher.run()
    else:
        client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        client_socket.connect(SOCKET_FILE)
        client_socket.send(b'switch')
        client_socket.close()