|
#!/usr/bin/env python3 |
|
import sys, tty, termios |
|
import subprocess |
|
from fuzzywuzzy import fuzz |
|
import re |
|
import curses |
|
from curses import wrapper |
|
|
|
def getch(): |
|
fd = sys.stdin.fileno() |
|
old_settings = termios.tcgetattr(fd) |
|
try: |
|
tty.setraw(sys.stdin.fileno()) |
|
ch = sys.stdin.read(1) |
|
finally: |
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) |
|
return ch |
|
|
|
def get_windows(): |
|
current = subprocess.Popen("xdotool getwindowfocus getwindowname".split(), stdout=subprocess.PIPE) |
|
current = current.stdout.readlines()[0].decode("utf-8")[:-1] |
|
p = subprocess.Popen("wmctrl -l".split(), stdout=subprocess.PIPE) |
|
windows = {} |
|
for line in p.stdout.readlines(): |
|
line = line.decode("utf-8") |
|
match = re.match(r"(\S*)\s*\S*\s*\S*\s*(.*)", line) |
|
id, name = match.groups() |
|
# exclude Desktop which would just defocus all windows |
|
# exclude the currently active window |
|
if name not in ["Desktop", current]: |
|
windows[name] = id |
|
return windows |
|
|
|
def similarity(name, query): |
|
return fuzz.token_set_ratio(name, query) |
|
|
|
def closest_sorted(names, query): |
|
return sorted(names, reverse=True, key=lambda k: similarity(k, query)) |
|
|
|
def closest_counts(names, query): |
|
names = closest_sorted(names, query) |
|
return list(map(lambda k: (k, similarity(k, query)), names)) |
|
|
|
class Window: |
|
def __init__(self, window): |
|
self.window = window |
|
self.line = 2 |
|
|
|
def add_line(self, str, *args, **kwargs): |
|
self.window.addstr(self.line, 0, str, *args, **kwargs) |
|
self.line += 1 |
|
|
|
def clear(self): |
|
self.window.clear() |
|
self.line = 2 |
|
|
|
def __getattr__(self, attr): |
|
return getattr(self.window, attr) |
|
|
|
windows_map = get_windows() |
|
windows = get_windows().keys() |
|
|
|
def open_window(name): |
|
id = windows_map[name] |
|
subprocess.Popen(["wmctrl", "-a", id, "-i"]) |
|
|
|
# globals, yay |
|
shown_entries = [] |
|
query = "" |
|
selected_entry = None |
|
entry_count = 1 |
|
|
|
def main(stdscr): |
|
scr = Window(stdscr) |
|
|
|
def update_query(_query): |
|
global shown_entries, query, selected_entry |
|
query = _query |
|
shown_entries = closest_sorted(windows, query)[:entry_count] |
|
selected_entry = shown_entries[0] |
|
|
|
def update_entry(delta=0, set=None): |
|
global shown_entries, selected_entry |
|
for i, entry in enumerate(shown_entries): |
|
if entry == selected_entry: |
|
pos = i + delta |
|
if set is not None: |
|
pos = set |
|
if pos < 0: |
|
pos = 0 |
|
if pos >= entry_count: |
|
pos = entry_count - 1 |
|
selected_entry = shown_entries[pos] |
|
break |
|
|
|
while True: |
|
height, width = scr.getmaxyx() |
|
entry_count = min(height - 2, len(windows)) |
|
update_entry(0) |
|
key = scr.getkey() |
|
if key == chr(27): |
|
break |
|
elif key in ["KEY_RIGHT", "KEY_DOWN"]: |
|
update_entry(1) |
|
elif key in ["KEY_LEFT", "KEY_UP"]: |
|
update_entry(-1) |
|
elif key in ["KEY_PPAGE", "KEY_HOME"]: |
|
update_entry(set=0) |
|
elif key in ["KEY_NPAGE", "KEY_END"]: |
|
update_entry(set=entry_count-1) |
|
elif key in ["KEY_BACKSPACE", "KEY_DC"]: |
|
update_query(query[:-1]) |
|
elif key in ["KEY_RESIZE", "KEY_IC"]: |
|
# ignore |
|
pass |
|
elif key == "\n": |
|
open_window(selected_entry) |
|
else: |
|
update_query(query + key) |
|
scr.clear() |
|
|
|
for name in shown_entries: |
|
try: |
|
if name == selected_entry: |
|
scr.add_line(name, curses.A_STANDOUT) |
|
else: |
|
scr.add_line(name) |
|
except: |
|
pass |
|
|
|
scr.line = 0 |
|
scr.add_line(query, curses.A_BOLD) |
|
|
|
scr.refresh() |
|
scr.refresh() |
|
|
|
wrapper(main) |