Skip to content

Commit

Permalink
Merge pull request #141 from sergey-trotsyuk/example/focus-app-and-wi…
Browse files Browse the repository at this point in the history
…ndow-with-history-server

Server and window switchers those do not pay attention on workspaces
  • Loading branch information
Tony Crisci authored Dec 29, 2019
2 parents 13d6e3c + da940ef commit 260235a
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 0 deletions.
30 changes: 30 additions & 0 deletions examples/i3-focus/focus-app.py
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)
29 changes: 29 additions & 0 deletions examples/i3-focus/focus-current-app-window.py
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)
92 changes: 92 additions & 0 deletions examples/i3-focus/history-server.py
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()
4 changes: 4 additions & 0 deletions examples/i3-focus/tools/__init__.py
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
18 changes: 18 additions & 0 deletions examples/i3-focus/tools/app.py
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")
37 changes: 37 additions & 0 deletions examples/i3-focus/tools/lists.py
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
31 changes: 31 additions & 0 deletions examples/i3-focus/tools/menu.py
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');
13 changes: 13 additions & 0 deletions examples/i3-focus/tools/sockets.py
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)

0 comments on commit 260235a

Please sign in to comment.