==> menu_mods/gnome.py <== """ Change gtk / icons themes and another gnome settings """ import os import configparser import subprocess import glob import path from misc import Misc class gnome(): """ Change gtk / icons themes and another gnome settings using gsd-xsettings. """ def __init__(self, menu): self.menu = menu self.gtk_config = configparser.ConfigParser() self.gnome_settings_script = os.path.expanduser( '~/bin/scripts/gnome_settings' ) def menu_params(self, length, prompt): """ Set menu params """ return { 'cnum': length / 2, 'lnum': 2, 'width': int(self.menu.screen_width * 0.55), 'prompt': f'{self.menu.wrap_str(prompt)} {self.menu.conf("prompt")}' } def apply_settings(self, selection, *cmd_opts): """ Apply selected gnome settings """ ret = "" if selection is not None: ret = selection.decode('UTF-8').strip() if ret is not None and ret != '': try: subprocess.call([ self.gnome_settings_script, *cmd_opts, ret ], check=True) except subprocess.CalledProcessError as proc_err: Misc.print_run_exception_info(proc_err) def change_icon_theme(self): """ Changes icon theme with help of gsd-xsettings """ icon_dirs = [] icons_path = path.Path('~/.icons').expanduser() for icon in glob.glob(icons_path + '/*'): if icon: icon_dirs += [path.Path(icon).name] menu_params = self.menu_params(len(icon_dirs), 'icon theme') try: selection = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(icon_dirs), 'UTF-8'), check=True ).stdout except subprocess.CalledProcessError as proc_err: Misc.print_run_exception_info(proc_err) self.apply_settings(selection, '-i') def change_gtk_theme(self): """ Changes gtk theme with help of gsd-xsettings """ theme_dirs = [] gtk_theme_path = path.Path('~/.themes').expanduser() for theme in glob.glob(gtk_theme_path + '/*/*/gtk.css'): if theme: theme_dirs += [path.Path(theme).dirname().dirname().name] menu_params = self.menu_params(len(theme_dirs), 'gtk theme') try: selection = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(theme_dirs), 'UTF-8'), check=True ).stdout except subprocess.CalledProcessError as proc_err: Misc.print_run_exception_info(proc_err) self.apply_settings(selection, '-a') ==> menu_mods/i3menu.py <== import sys import json import re import subprocess from typing import List class i3menu(): def __init__(self, menu): self.menu = menu def i3_cmds(self) -> List[str]: """ Return the list of i3 commands with magic_pie hack autocompletion. """ try: out = subprocess.run( [self.menu.i3cmd, 'magic_pie'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False ).stdout except Exception: return [] lst = [ t.replace("'", '') for t in re.split('\\s*,\\s*', json.loads( out.decode('UTF-8') )[0]['error'])[2:] ] lst.remove('nop') lst.extend(['splitv', 'splith']) lst.sort() return lst def i3_cmd_args(self, cmd: str) -> List[str]: try: out = subprocess.run( [self.menu.i3cmd, cmd], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False ).stdout if out is not None: ret = [ t.replace("'", '') for t in re.split('\\s*, \\s*', json.loads( out.decode('UTF-8') )[0]['error'])[1:] ] return ret except Exception: return [""] def cmd_menu(self) -> int: """ Menu for i3 commands with hackish autocompletion. """ # set default menu args for supported menus cmd = '' try: menu = subprocess.run( self.menu.args({}), stdout=subprocess.PIPE, input=bytes('\n'.join(self.i3_cmds()), 'UTF-8'), check=True ).stdout if menu is not None and menu: cmd = menu.decode('UTF-8').strip() except subprocess.CalledProcessError as call_e: sys.exit(call_e.returncode) if not cmd: # nothing to do return 0 debug, ok, notify_msg = False, False, "" args, prev_args = None, None menu_params = { 'prompt': f"{self.menu.wrap_str('i3cmd')} \ {self.menu.conf('prompt')} " + cmd, } while not (ok or args == [''] or args == []): if debug: print(f"evaluated cmd=[{cmd}] args=[{self.i3_cmd_args(cmd)}]") out = subprocess.run( (f"{self.menu.i3cmd} " + cmd).split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False ).stdout if out is not None and out: ret = json.loads(out.decode('UTF-8').strip())[0] result, err = ret.get('success', ''), ret.get('error', '') ok = True if not result: ok = False notify_msg = ['notify-send', 'i3-cmd error', err] try: args = self.i3_cmd_args(cmd) if args == prev_args: return 0 cmd_rerun = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(args), 'UTF-8'), check=False ).stdout cmd += ' ' + cmd_rerun.decode('UTF-8').strip() prev_args = args except subprocess.CalledProcessError as call_e: return call_e.returncode if not ok: subprocess.run(notify_msg, check=False) ==> menu_mods/props.py <== import subprocess import re import socket from typing import List class props(): def __init__(self, menu): self.menu = menu # Magic delimiter used by add_prop / del_prop routines. self.delim = "@" # default echo server host self.host = self.menu.conf("host") # default echo server port self.port = int(self.menu.conf("port")) # create echo server socket self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) # negi3mods which allows add / delete property. # For example this feature can be used to move / delete window # to / from named scratchpad. self.possible_mods = ['ns', 'circle'] # Window properties used by i3 to match windows. self.i3rules_xprop = set(self.menu.conf("rules_xprop")) def tag_name(self, mod: str, lst: List[str]) -> str: """ Returns tag name, selected by menu. Args: mod (str): module name string. lst (List[str]): list of menu input. """ menu_params = { 'cnum': len(lst), 'width': int(self.menu.screen_width * 0.75), 'prompt': f'{self.menu.wrap_str(mod)} {self.menu.conf("prompt")}', } menu_tag = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(lst), 'UTF-8'), check=False ).stdout if menu_tag is not None and menu_tag: return menu_tag.decode('UTF-8').strip() return "" def autoprop(self) -> None: """ Start autoprop menu to move current module to smth. """ mod = self.get_mod() if mod is None or not mod: return aprop_str = self.get_autoprop_as_str(with_title=False) lst = self.mod_data_list(mod) tag_name = self.tag_name(mod, lst) if tag_name is not None and tag_name: for mod in self.possible_mods: cmdl = [ f'{self.menu.i3_path}send', f'{mod}', 'add_prop', f'{tag_name}', f'{aprop_str}' ] subprocess.run(cmdl, check=False) else: print(f'No tag name specified for props [{aprop_str}]') def get_mod(self) -> str: """ Select negi3mod for add_prop by menu. """ menu_params = { 'cnum': len(self.possible_mods), 'lnum': 1, 'width': int(self.menu.screen_width * 0.75), 'prompt': f'{self.menu.wrap_str("selmod")} \ {self.menu.conf("prompt")}' } mod = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(self.possible_mods), 'UTF-8'), check=False ).stdout if mod is not None and mod: return mod.decode('UTF-8').strip() return "" def show_props(self) -> None: """ Send notify-osd message about current properties. """ aprop_str = self.get_autoprop_as_str(with_title=False) notify_msg = ['notify-send', 'X11 prop', aprop_str] subprocess.run(notify_msg, check=False) def get_autoprop_as_str(self, with_title: bool = False, with_role: bool = False) -> str: """ Convert xprops list to i3 commands format. Args: with_title (bool): add WM_NAME attribute, to the list, optional. with_role (bool): add WM_WINDOW_ROLE attribute to the list, optional. """ xprops = [] win = self.menu.i3ipc.get_tree().find_focused() xprop = subprocess.run( ['xprop', '-id', str(win.window)] + self.menu.xprops_list, stdout=subprocess.PIPE, check=False ).stdout if xprop is not None: xprop = xprop.decode('UTF-8').split('\n') ret = [] for attr in self.i3rules_xprop: for xattr in xprop: xprops.append(xattr) if attr in xattr and 'not found' not in xattr: founded_attr = re.search("[A-Z]+(.*) = ", xattr).group(0) xattr = re.sub("[A-Z]+(.*) = ", '', xattr).split(', ') if "WM_CLASS" in founded_attr: if xattr[0] is not None and xattr[0]: ret.append(f'instance={xattr[0]}{self.delim}') if xattr[1] is not None and xattr[1]: ret.append(f'class={xattr[1]}{self.delim}') if with_role and "WM_WINDOW_ROLE" in founded_attr: ret.append(f'window_role={xattr[0]}{self.delim}') if with_title and "WM_NAME" in founded_attr: ret.append(f'title={xattr[0]}{self.delim}') return "[" + ''.join(sorted(ret)) + "]" def mod_data_list(self, mod: str) -> List[str]: """ Extract list of module tags. Used by add_prop menus. Args: mod (str): negi3mod name. """ self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) self.sock.connect((self.host, self.port)) self.sock.send(bytes(f'{mod}_list\n', 'UTF-8')) out = self.sock.recv(1024) self.sock.shutdown(1) self.sock.close() lst = [] if out is not None: lst = out.decode('UTF-8').strip()[1:-1].split(', ') lst = [t.replace("'", '') for t in lst] return lst ==> menu_mods/pulse_menu.py <== import subprocess import pulsectl class pulse_menu(): def __init__(self, menu): self.menu = menu self.pulse = pulsectl.Pulse('neg-pulse-selector') self.pulse_data = {} def pulseaudio_output(self): self.pulse_data = { "app_list": [], "sink_output_list": [], "app_props": {}, "pulse_sink_list": self.pulse.sink_list(), "pulse_app_list": self.pulse.sink_input_list(), } for app in self.pulse_data["pulse_app_list"]: app_name = app.proplist["media.name"] + ' -- ' + \ app.proplist["application.name"] self.pulse_data["app_list"] += [app_name] self.pulse_data["app_props"][app_name] = app if self.pulse_data["app_list"]: app_ret = self.pulseaudio_select_app() if self.pulse_data["sink_output_list"]: self.pulseaudio_select_output(app_ret) def pulseaudio_input(self): pass def pulseaudio_select_app(self): menu_params = { 'cnum': 1, 'lnum': len(self.pulse_data["app_list"]), 'auto_selection': '-auto-select', 'width': int(self.menu.screen_width * 0.55), 'prompt': f'{self.menu.wrap_str("pulse app")} \ {self.menu.conf("prompt")}', } menu_app_sel = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(self.pulse_data["app_list"]), 'UTF-8'), check=False ).stdout if menu_app_sel is not None: app_ret = menu_app_sel.decode('UTF-8').strip() exclude_device_name = "" sel_app_props = \ self.pulse_data["app_props"][app_ret].proplist for stream in self.pulse.stream_restore_list(): if stream is not None: if stream.device is not None: if stream.name == sel_app_props['module-stream-restore.id']: exclude_device_name = stream.device for _, sink in enumerate(self.pulse_data["pulse_sink_list"]): if sink.proplist.get('udev.id', ''): if sink.proplist['udev.id'].split('.')[0] == \ exclude_device_name.split('.')[1]: continue if sink.proplist.get('device.profile.name', ''): if sink.proplist['device.profile.name'] == \ exclude_device_name.split('.')[-1]: continue self.pulse_data["sink_output_list"] += \ [str(sink.index) + ' -- ' + sink.description] return app_ret def pulseaudio_select_output(self, app_ret) -> None: """ Create params for pulseaudio selector """ menu_params = { 'cnum': 1, 'lnum': len(self.pulse_data["sink_output_list"]), 'auto_selection': '-auto-select', 'width': int(self.menu.screen_width * 0.55), 'prompt': f'{self.menu.wrap_str("pulse output")} \ {self.menu.conf("prompt")}' } menu_output_sel = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes( '\n'.join(self.pulse_data["sink_output_list"]), 'UTF-8' ), check=False ).stdout if menu_output_sel is not None: out_ret = menu_output_sel.decode('UTF-8').strip() target_idx = out_ret.split('--')[0].strip() if int(self.pulse_data["app_props"][app_ret].index) is not None \ and int(target_idx) is not None: self.pulse.sink_input_move( int(self.pulse_data["app_props"][app_ret].index), int(target_idx), ) ==> menu_mods/winact.py <== import subprocess from functools import partial from typing import Callable class winact(): def __init__(self, menu): self.menu = menu self.workspaces = menu.conf("workspaces") def win_act_simple(self, cmd: str, prompt: str) -> None: """ Run simple and fast selection dialog for window with given action. Args: cmd (str): action for window to run. prompt (str): custom prompt for menu. """ leaves = self.menu.i3ipc.get_tree().leaves() winlist = [win.name for win in leaves] winlist_len = len(winlist) menu_params = { 'cnum': winlist_len, 'width': int(self.menu.screen_width * 0.75), 'prompt': f"{prompt} {self.menu.conf('prompt')}" } if winlist and winlist_len > 1: win_name = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(winlist), 'UTF-8'), check=False ).stdout elif winlist_len: win_name = winlist[0].encode() if win_name is not None and win_name: win_name = win_name.decode('UTF-8').strip() for win in leaves: if win.name == win_name: win.command(cmd) def goto_win(self) -> None: """ Run menu goto selection dialog """ self.win_act_simple('focus', self.menu.wrap_str('go')) def attach_win(self) -> None: """ Attach window to the current workspace. """ self.win_act_simple( 'move window to workspace current', self.menu.wrap_str('attach') ) def select_ws(self, use_wslist: bool) -> str: """ Apply target function to workspace. """ if use_wslist: wslist = self.workspaces else: wslist = [ws.name for ws in self.menu.i3ipc.get_workspaces()] + \ ["[empty]"] menu_params = { 'cnum': len(wslist), 'width': int(self.menu.screen_width * 0.66), 'prompt': f'{self.menu.wrap_str("ws")} {self.menu.conf("prompt")}', } workspace_name = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(wslist), 'UTF-8'), check=False ).stdout selected_ws = workspace_name.decode('UTF-8').strip() return str(wslist.index(selected_ws) + 1) + ' :: ' + selected_ws @staticmethod def apply_to_ws(ws_func: Callable) -> None: """ Partial apply function to workspace. """ ws_func() def goto_ws(self, use_wslist: bool = True) -> None: """ Go to workspace menu. """ workspace_name = self.select_ws(use_wslist) if workspace_name is not None and workspace_name: self.apply_to_ws( partial(self.menu.i3ipc.command, f'workspace {workspace_name}') ) def move_to_ws(self, use_wslist: bool = True) -> None: """ Move current window to the selected workspace """ workspace_name = self.select_ws(use_wslist) if workspace_name is not None and workspace_name: self.apply_to_ws( partial(self.menu.i3ipc.command, f'[con_id=__focused__] \ move to workspace {workspace_name}') ) ==> menu_mods/xprop.py <== import subprocess from misc import Misc class xprop(): """ Setup screen resolution """ def __init__(self, menu): self.menu = menu def xprop(self) -> None: """ Menu to show X11 atom attributes for current window. """ xprops = [] target_win = self.menu.i3ipc.get_tree().find_focused() try: xprop_ret = subprocess.run( ['xprop', '-id', str(target_win.window)] + self.menu.xprops_list, stdout=subprocess.PIPE, check=True ).stdout if xprop_ret is not None: xprop_ret = xprop_ret.decode().split('\n') for line in xprop_ret: if 'not found' not in line: xprops.append(line) except subprocess.CalledProcessError as proc_err: Misc.print_run_exception_info(proc_err) menu_params = { 'cnum': 1, 'lnum': len(xprops), 'width': int(self.menu.screen_width * 0.75), 'prompt': f'{self.menu.wrap_str("xprop")} {self.menu.conf("prompt")}' } ret = "" try: xprop_sel = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(xprops), 'UTF-8'), check=True ).stdout if xprop_sel is not None: ret = xprop_sel.decode('UTF-8').strip() except subprocess.CalledProcessError as proc_err: Misc.print_run_exception_info(proc_err) # Copy to the clipboard if ret is not None and ret != '': try: subprocess.run( ['xsel', '-i'], input=bytes(ret.strip(), 'UTF-8'), check=True ) except subprocess.CalledProcessError as proc_err: Misc.print_run_exception_info(proc_err) ==> menu_mods/xrandr.py <== import subprocess class xrandr(): def __init__(self, menu): self.menu = menu def change_resolution_xrandr(self): from display import Display xrandr_data = Display.xrandr_resolution_list() menu_params = { 'cnum': 8, 'width': int(Display.get_screen_resolution()["width"] * 0.55), 'prompt': f'{self.menu.wrap_str("gtk_theme")} \ {self.menu.conf("prompt")}', } resolution_sel = subprocess.run( self.menu.args(menu_params), stdout=subprocess.PIPE, input=bytes('\n'.join(xrandr_data), 'UTF-8'), check=False ).stdout if resolution_sel is not None: ret = resolution_sel.decode('UTF-8').strip() ret_list = [] if ret and 'x' in ret: size_pair = ret.split(':') size_id = size_pair[0] res_str = size_pair[1:][0].strip() ret_list = res_str.split('x') width, height = ret_list[0].strip(), ret_list[1].strip() print(f'Set size to {width}x{height}') Display.set_screen_size(size_id)