-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #141 from sergey-trotsyuk/example/focus-app-and-wi…
…ndow-with-history-server Server and window switchers those do not pay attention on workspaces
- Loading branch information
Showing
8 changed files
with
254 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import re | ||
from argparse import ArgumentParser | ||
from functools import reduce | ||
import i3ipc | ||
from tools import App, Lists, Menu, Sockets | ||
|
||
|
||
parser = ArgumentParser(prog='i3-app-focus.py', | ||
description=''' | ||
i3-app-focus.py is dmenu-based script for creating dynamic app switcher. | ||
''', | ||
epilog=''' | ||
Additional arguments found after "--" will be passed to dmenu. | ||
''') | ||
parser.add_argument('--menu', default='dmenu', help='The menu command to run (ex: --menu=rofi)') | ||
parser.add_argument('--socket-file', default='/tmp/i3-app-focus.socket', help='Socket file path') | ||
(args, menu_args) = parser.parse_known_args() | ||
|
||
|
||
sockets = Sockets(args.socket_file) | ||
containers_info = sockets.get_containers_history() | ||
|
||
apps = list(map(App, containers_info)) | ||
apps_uniq = reduce(Lists.accum_uniq_apps, apps, []) | ||
|
||
i3 = i3ipc.Connection() | ||
menu = Menu(i3, args.menu, menu_args) | ||
menu.show_menu_app(apps_uniq) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import re | ||
from argparse import ArgumentParser | ||
from functools import reduce | ||
import i3ipc | ||
from tools import App, Lists, Menu, Sockets | ||
|
||
|
||
parser = ArgumentParser(prog='i3-app-focus.py', | ||
description=''' | ||
i3-app-focus.py is dmenu-based script for creating dynamic window switcher for current app. | ||
''', | ||
epilog=''' | ||
Additional arguments found after "--" will be passed to dmenu. | ||
''') | ||
parser.add_argument('--menu', default='dmenu', help='The menu command to run (ex: --menu=rofi)') | ||
parser.add_argument('--socket-file', default='/tmp/i3-app-focus.socket', help='Socket file path') | ||
(args, menu_args) = parser.parse_known_args() | ||
|
||
|
||
sockets = Sockets(args.socket_file) | ||
containers_info = sockets.get_containers_history() | ||
|
||
containers_info_by_focused_app = Lists.find_all_by_focused_app(containers_info) | ||
|
||
i3 = i3ipc.Connection() | ||
menu = Menu(i3, args.menu, menu_args) | ||
menu.show_menu_container_info(containers_info_by_focused_app) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
#!/usr/bin/env python3 | ||
|
||
# i3 get_tree does not contain information about focus throw all workspaces, | ||
# so scripts those use it can sort windows only with workspace groupping. | ||
# This server accumulates window focus history and does not pay attention on workspaces. | ||
|
||
import os | ||
import socket | ||
import selectors | ||
import threading | ||
import json | ||
from argparse import ArgumentParser | ||
import i3ipc | ||
|
||
MAX_WIN_HISTORY = 15 | ||
|
||
parser = ArgumentParser(prog='i3-app-focus.py', | ||
description='''''', | ||
epilog='''''') | ||
parser.add_argument('--socket-file', default='/tmp/i3-app-focus.socket', help='Socket file path') | ||
(args, other) = parser.parse_known_args() | ||
|
||
class FocusWatcher: | ||
def __init__(self): | ||
self.i3 = i3ipc.Connection() | ||
self.i3.on('window::focus', self._on_window_focus) | ||
self.listening_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
if os.path.exists(args.socket_file): | ||
os.remove(args.socket_file) | ||
self.listening_socket.bind(args.socket_file) | ||
self.listening_socket.listen(2) | ||
self.window_list = [] | ||
self.window_list_lock = threading.RLock() | ||
|
||
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() | ||
|
||
def _on_window_focus(self, i3conn, event): | ||
if not self._is_window(event.container): | ||
return | ||
|
||
with self.window_list_lock: | ||
window_id = event.container.id | ||
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:] | ||
|
||
def _launch_i3(self): | ||
self.i3.main() | ||
|
||
def _launch_server(self): | ||
selector = selectors.DefaultSelector() | ||
|
||
def accept(sock): | ||
conn, addr = sock.accept() | ||
tree = self.i3.get_tree() | ||
info = [] | ||
with self.window_list_lock: | ||
for window_id in self.window_list: | ||
con = tree.find_by_id(window_id) | ||
if con: | ||
info.append({ | ||
"id": con.id, | ||
"window": con.window, | ||
"window_title": con.window_title, | ||
"window_class": con.window_class, | ||
"focused": con.focused | ||
}) | ||
|
||
conn.send(json.dumps(info).encode()); | ||
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) | ||
|
||
|
||
@staticmethod | ||
def _is_window(con): | ||
return not con.nodes and con.type == "con" and (con.parent and con.parent.type != "dockarea" or True) | ||
|
||
|
||
focus_watcher = FocusWatcher() | ||
focus_watcher.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from .app import App | ||
from .lists import Lists | ||
from .menu import Menu | ||
from .sockets import Sockets |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import re | ||
import i3ipc | ||
|
||
class App: | ||
def __init__(self, container_info): | ||
self._container_info = container_info | ||
|
||
def get_con_id(self): | ||
return self._container_info["id"] | ||
|
||
def get_window_class(self): | ||
return self._container_info["window_class"] | ||
|
||
def get_title(self): | ||
# i3 = i3ipc.Connection() | ||
# print("\n\n") | ||
# print(vars(i3.get_tree().find_by_id(self._container_info["id"]))) | ||
return re.match(r"^.*?\s*(?P<title>[^-—]+)$", self._container_info["window_title"]).group("title") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
from . import App | ||
|
||
class Lists: | ||
@staticmethod | ||
def accum_uniq_apps(result, app): | ||
exists = False | ||
for a in result: | ||
if a.get_title() == app.get_title(): | ||
exists = True | ||
|
||
if not exists: | ||
result.append(app) | ||
|
||
return result | ||
|
||
@staticmethod | ||
def find_all_by_focused_app(infos): | ||
for i in infos: | ||
if i["focused"]: | ||
focused_info = i | ||
|
||
focused_app = App(focused_info) | ||
|
||
focused_app_windows_by_class = list(filter(lambda i: i["window_class"] == focused_app.get_window_class(), infos)) | ||
return focused_app_windows_by_class | ||
|
||
@staticmethod | ||
def find_app_by_title(title, apps): | ||
for a in apps: | ||
if a.get_title() == title: | ||
return a | ||
|
||
@staticmethod | ||
def find_container_info_by_title(title, infos): | ||
for i in infos: | ||
if i["window_title"] == title: | ||
return i |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from collections import deque | ||
from subprocess import check_output | ||
from . import Lists | ||
|
||
class Menu: | ||
def __init__(self, i3, menu, menu_args): | ||
self._i3 = i3 | ||
self._menu = menu | ||
self._menu_args = menu_args | ||
|
||
def show_menu(self, items): | ||
menu_input = bytes(str.join('\n', items), 'UTF-8') | ||
menu_cmd = [self._menu] + ['-l', str(len(items))] + self._menu_args | ||
menu_result = check_output(menu_cmd, input=menu_input) | ||
return menu_result.decode().strip() | ||
|
||
def show_menu_app(self, apps): | ||
titles = list(map(lambda a: a.get_title(), apps)) | ||
selected_title = self.show_menu(titles) | ||
selected_app = Lists.find_app_by_title(selected_title, apps) | ||
tree = self._i3.get_tree() | ||
con = tree.find_by_id(selected_app.get_con_id()) | ||
con.command('focus'); | ||
|
||
def show_menu_container_info(self, containers_info): | ||
titles = list(map(lambda i: i["window_title"], containers_info)) | ||
selected_title = self.show_menu(titles) | ||
selected_info = Lists.find_container_info_by_title(selected_title, containers_info) | ||
tree = self._i3.get_tree() | ||
con = tree.find_by_id(selected_info["id"]) | ||
con.command('focus'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import socket | ||
import json | ||
|
||
class Sockets: | ||
def __init__(self, socket_file): | ||
self._socket_file = socket_file | ||
self._client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
|
||
def get_containers_history(self): | ||
self._client.connect(self._socket_file) | ||
history_json = self._client.recv(4096).decode() | ||
self._client.close() | ||
return json.loads(history_json) |