==> display.py <== """ Handle X11 screen tasks with randr extension """ import subprocess from Xlib import display from Xlib.ext import randr from misc import Misc class Display(): d = display.Display() s = d.screen() window = s.root.create_window(0, 0, 1, 1, 1, s.root_depth) xrandr_cache = randr.get_screen_info(window)._data resolution_list = [] @classmethod def get_screen_resolution(cls) -> dict: size_id = cls.xrandr_cache['size_id'] resolution = cls.xrandr_cache['sizes'][size_id] return { 'width': int(resolution['width_in_pixels']), 'height': int(resolution['height_in_pixels']) } @classmethod def get_screen_resolution_data(cls) -> dict: return cls.xrandr_cache['sizes'] @classmethod def xrandr_resolution_list(cls) -> dict: if not cls.resolution_list: delimiter = 'x' resolution_data = cls.get_screen_resolution_data() for size_id, res in enumerate(resolution_data): if res is not None and res: cls.resolution_list.append( str(size_id) + ': ' + str(res['width_in_pixels']) + delimiter + str(res['height_in_pixels']) ) return cls.resolution_list @classmethod def set_screen_size(cls, size_id=0) -> None: try: subprocess.run(['xrandr', '-s', str(size_id)], check=True) except subprocess.CalledProcessError as proc_err: Misc.print_run_exception_info(proc_err) ==> executor.py <== """ Tmux Manager. Give simple and consistent way for user to create tmux sessions on dedicated sockets. Also it can run simply run applications without Tmux. The main advantage is dynamic config reloading and simplicity of adding or modifing of various parameters, also it works is faster then dedicated scripts, because there is no parsing / translation phase here in runtime. """ import subprocess import os import errno import shlex import shutil import threading import multiprocessing import yaml import yamlloader from negi3mod import negi3mod from typing import List from os.path import expanduser from cfg import cfg from misc import Misc class env(): """ Environment class. It is a helper for tmux manager to store info about currently selected application. This class rules over parameters and settings of application, like used terminal enumator, fonts, all path settings, etc. Parents: config: configuration manager to autosave/autoload TOML-configutation with inotify """ def __init__(self, name: str, config: dict) -> None: self.name = name self.tmux_socket_dir = expanduser('/dev/shm/tmux_sockets') self.alacritty_cfg_dir = expanduser('/dev/shm/alacritty_cfg') self.sockpath = expanduser(f'{self.tmux_socket_dir}/{name}.socket') self.default_alacritty_cfg_path = "~/.config/alacritty/alacritty.yml" Misc.create_dir(self.tmux_socket_dir) Misc.create_dir(self.alacritty_cfg_dir) try: os.makedirs(self.tmux_socket_dir) except OSError as dir_not_created: if dir_not_created.errno != errno.EEXIST: raise try: os.makedirs(self.alacritty_cfg_dir) except OSError as dir_not_created: if dir_not_created.errno != errno.EEXIST: raise # get terminal from config, use Alacritty by default self.term = config.get(name, {}).get("term", "alacritty").lower() self.wclass = config.get(name, {}).get("class", self.term) self.title = config.get(name, {}).get("title", self.wclass) self.font = config.get("default_font", "") if not self.font: self.font = config.get(name, {}).get("font", "Iosevka Term") self.font_size = config.get("default_font_size", "") if not self.font_size: self.font_size = config.get(name, {}).get("font_size", "18") use_one_fontstyle = config.get("use_one_fontstyle", False) self.font_style = config.get("default_font_style", "") if not self.font_style: self.font_style = config.get(name, {}).get("font_style", "Regular") if use_one_fontstyle: self.font_style_normal = config.get(name, {})\ .get("font_style_normal", self.font_style) self.font_style_bold = config.get(name, {})\ .get("font_style_bold", self.font_style) self.font_style_italic = config.get(name, {})\ .get("font_style_italic", self.font_style) else: self.font_style_normal = config.get(name, {})\ .get("font_style_normal", 'Regular') self.font_style_bold = config.get(name, {})\ .get("font_style_bold", 'Bold') self.font_style_italic = config.get(name, {})\ .get("font_style_italic", 'Italic') self.tmux_session_attach = \ f"tmux -S {self.sockpath} a -t {name}" self.tmux_new_session = \ f"tmux -S {self.sockpath} new-session -s {name}" colorscheme = config.get("colorscheme", "") if not colorscheme: colorscheme = config.get(name, {}).get("colorscheme", 'dark3') self.set_colorscheme = \ f"{expanduser('~/bin/dynamic-colors')} switch {colorscheme};" self.postfix = config.get(name, {}).get("postfix", '') if self.postfix and self.postfix[0] != '-': self.postfix = '\\; ' + self.postfix self.run_tmux = int(config.get(name, {}).get("run_tmux", 1)) if not self.run_tmux: prog_to_dtach = config.get(name, {}).get('prog_detach', '') if prog_to_dtach: self.prog = \ f'dtach -A ~/1st_level/{name}.session {prog_to_dtach}' else: self.prog = config.get(name, {}).get('prog', 'true') self.set_wm_class = config.get(name, {}).get('set_wm_class', '') self.set_instance = config.get(name, {}).get('set_instance', '') self.x_pad = config.get(name, {}).get('x_padding', '2') self.y_pad = config.get(name, {}).get('y_padding', '2') self.create_term_params(config, name) def join_processes(): for prc in multiprocessing.active_children(): prc.join() threading.Thread(target=join_processes, args=(), daemon=True).start() @staticmethod def generate_alacritty_config( alacritty_cfg_dir, config: dict, name: str) -> str: """ Config generator for alacritty. We need it because of alacritty cannot bypass most of user parameters with command line now. Args: alacritty_cfg_dir: alacritty config dir config: config dirtionary name(str): name of config to generate Return: cfgname(str): configname """ app_name = config.get(name, {}).get('app_name', {}) if not app_name: app_name = config.get(name, {}).get('class') app_name = expanduser(app_name + '.yml') cfgname = expanduser(f'{alacritty_cfg_dir}/{app_name}') if not os.path.exists(cfgname): shutil.copyfile( expanduser("~/.config/alacritty/alacritty.yml"), cfgname ) return cfgname def yaml_config_create(self, custom_config: str) -> None: """ Create config for alacritty Args: custom_config(str): config name to create """ with open(custom_config, "r") as cfg_file: try: conf = yaml.load( cfg_file, Loader=yamlloader.ordereddict.CSafeLoader) if conf is not None: conf["font"]["normal"]["family"] = self.font conf["font"]["bold"]["family"] = self.font conf["font"]["italic"]["family"] = self.font conf["font"]["normal"]["style"] = self.font_style_normal conf["font"]["bold"]["style"] = self.font_style_bold conf["font"]["italic"]["style"] = self.font_style_italic conf["font"]["size"] = self.font_size conf["window"]["padding"]['x'] = int(self.x_pad) conf["window"]["padding"]['y'] = int(self.y_pad) except yaml.YAMLError as yamlerror: print(yamlerror) with open(custom_config, 'w', encoding='utf8') as outfile: try: yaml.dump( conf, outfile, default_flow_style=False, allow_unicode=True, canonical=False, explicit_start=True, Dumper=yamlloader.ordereddict.CDumper ) except yaml.YAMLError as yamlerror: print(yamlerror) def create_term_params(self, config: dict, name: str) -> None: """ This function fill self.term_opts for settings.abs Args: config(dict): config dictionary which should be adopted to commandline options or settings. """ if self.term == "alacritty": custom_config = self.generate_alacritty_config( self.alacritty_cfg_dir, config, name ) multiprocessing.Process( target=self.yaml_config_create, args=(custom_config,), daemon=True ).start() self.term_opts = [ "alacritty", '-qq', "--live-config-reload", "--config-file", expanduser(custom_config) ] + [ "--class", self.wclass, "-t", self.title, "-e", "dash", "-c" ] elif self.term == "st": self.term_opts = ["st"] + [ "-c", self.wclass, "-f", self.font + ":size=" + str(self.font_size), "-e", "dash", "-c", ] elif self.term == "urxvt": self.term_opts = ["urxvt"] + [ "-name", self.wclass, "-fn", "xft:" + self.font + ":size=" + str(self.font_size), "-e", "dash", "-c", ] elif self.term == "xterm": self.term_opts = ["xterm"] + [ '-class', self.wclass, '-fa', "xft:" + self.font + ":size=" + str(self.font_size), "-e", "dash", "-c", ] elif self.term == "cool-retro-term": self.term_opts = ["cool-retro-term"] + [ "-e", "dash", "-c", ] class executor(negi3mod, cfg): """ Tmux Manager class. Easy and consistent way to create tmux sessions on dedicated sockets. Also it can run simply run applications without Tmux. The main advantage is dynamic config reloading and simplicity of adding or modifing of various parameters. Parents: cfg: configuration manager to autosave/autoload TOML-configutation with inotify """ def __init__(self, i3, loop=None) -> None: """ Init function. Arguments for this constructor used only for compatibility. Args: i3: i3ipc connection(not used). loop: asyncio loop. It's need to be given as parameter because of you need to bypass asyncio-loop to the thread(not used). """ cfg.__init__(self, i3, convert_me=False) self.envs = {} for app in self.cfg: self.envs[app] = env(app, self.cfg) self.bindings = { "run": self.run, "reload": self.reload_config, } def __exit__(self, exc_type, exc_value, traceback) -> None: self.envs.clear() def run_app(self, args: List) -> None: """ Wrapper to run selected application in background. Args: args (List): arguments list. """ if not self.env.set_wm_class: subprocess.Popen(args) else: if not self.env.set_instance: self.env.set_instance = self.env.set_wm_class subprocess.Popen( [ './wm_class', '--run', self.env.set_wm_class, self.env.set_instance, ] + args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) @staticmethod def detect_session_bind(sockpath, name) -> str: """ Find target session for given socket. """ session_list = subprocess.run( shlex.split(f"tmux -S {sockpath} list-sessions"), stdout=subprocess.PIPE, check=False ).stdout return subprocess.run( shlex.split(f"awk -F ':' '/{name}/ {{print $1}}'"), stdout=subprocess.PIPE, input=(session_list), check=False ).stdout.decode() def attach_to_session(self) -> None: """ Run tmux to attach to given socket. """ self.run_app( self.env.term_opts + [f"{self.env.set_colorscheme} {self.env.tmux_session_attach}"] ) def search_classname(self) -> bytes: """ Search for selected window class. """ return subprocess.run( shlex.split(f"xdotool search --classname {self.env.wclass}"), stdout=subprocess.PIPE, check=False ).stdout def create_new_session(self) -> None: """ Run tmux to create the new session on given socket. """ self.run_app( self.env.term_opts + [f"{self.env.set_colorscheme} \ {self.env.tmux_new_session} {self.env.postfix} && \ {self.env.tmux_session_attach}"] ) def run(self, name: str) -> None: """ Entry point, run application with Tmux on dedicated socket(in most cases), or without tmux, if config value run_tmux=0. Args: name (str): target application name, with configuration taken from TOML. """ self.env = self.envs[name] if self.env.run_tmux: if self.env.name in self.detect_session_bind( self.env.sockpath, self.env.name): wid = self.search_classname() try: if int(wid.decode()): pass except ValueError: self.attach_to_session() else: self.create_new_session() else: self.run_app( self.env.term_opts + [ self.env.set_colorscheme + self.env.prog ] ) ==> fs.py <== #!/usr/bin/pypy3 """ Module to set / unset dpms while fullscreen is toggled on. I am simply use xset here. There is better solution possible, for example wayland-friendly. """ import subprocess from negi3mod import negi3mod from cfg import cfg class fs(negi3mod, cfg): def __init__(self, i3conn, loop=None): # i3ipc connection, bypassed by negi3mods runner self.i3ipc = i3conn self.panel_should_be_restored = False # Initialize modcfg. cfg.__init__(self, i3conn, convert_me=False) # default panel classes self.panel_classes = self.cfg.get("panel_classes", []) # fullscreened workspaces self.ws_fullscreen = self.cfg.get("ws_fullscreen", []) # for which windows we shoudn't show panel self.classes_to_hide_panel = self.cfg.get( "classes_to_hide_panel", [] ) self.show_panel_on_close = False self.bindings = { "reload": self.reload_config, "fullscreen": self.hide, } self.i3ipc.on('window::close', self.on_window_close) self.i3ipc.on('workspace::focus', self.on_workspace_focus) def on_workspace_focus(self, _, event): """ Hide panel if it is fullscreen workspace, show panel otherwise """ for tgt_ws in self.ws_fullscreen: if event.current.name.endswith(tgt_ws): self.panel_action('hide', restore=False) return self.panel_action('show', restore=False) def panel_action(self, action: str, restore: bool): """ Helper to do show/hide with panel or another action Args: action (str): action to do. restore(bool): shows should the panel state be restored or not. """ # should be empty ret = subprocess.Popen( ['xdo', action, '-N', 'Polybar'], stdout=subprocess.PIPE ).communicate()[0] if not ret and restore is not None: self.panel_should_be_restored = restore def on_fullscreen_mode(self, _, event): """ Disable panel if it was in fullscreen mode and then goes to windowed mode. Args: _: i3ipc connection. event: i3ipc event. We can extract window from it using event.container. """ if event.container.window_class in self.panel_classes: return self.hide() def hide(self): """ Hide panel for this workspace """ i3_tree = self.i3ipc.get_tree() fullscreens = i3_tree.find_fullscreen() focused_ws = i3_tree.find_focused().workspace().name if not fullscreens: return for win in fullscreens: for tgt_class in self.classes_to_hide_panel: if win.window_class == tgt_class: for tgt_ws in self.ws_fullscreen: if focused_ws.endswith(tgt_ws): self.panel_action('hide', restore=False) break def on_window_close(self, i3conn, event): """ If there are no fullscreen windows then show panel closing window. Args: i3: i3ipc connection. event: i3ipc event. We can extract window from it using event.container. """ if event.container.window_class in self.panel_classes: return if self.show_panel_on_close: if not i3conn.get_tree().find_fullscreen(): self.panel_action('show', restore=True) ==> geom.py <== """ Module to convert from 16:10 1080p geometry to target screen geometry. This module contains geometry converter and also i3-rules generator. Also in this module geometry is parsed from config X11 internal format to the i3 commands. """ import re from typing import List from display import Display class geom(): def __init__(self, cfg: dict) -> None: """ Init function Args: cfg: config bypassed from target module, nsd for example. """ # generated command list for i3 config self.cmd_list = [] # geometry in the i3-commands format. self.parsed_geom = {} # set current screen resolution self.current_resolution = Display.get_screen_resolution() # external config self.cfg = cfg # fill self.parsed_geom with self.parse_geom function. for tag in self.cfg: self.parsed_geom[tag] = self.parse_geom(tag) @staticmethod def scratchpad_hide_cmd(hide: bool) -> str: """ Returns cmd needed to hide scratchpad. Args: hide (bool): to hide target or not. """ ret = "" if hide: ret = ", [con_id=__focused__] scratchpad show" return ret @staticmethod def ch(lst: List, ch: str) -> str: """ Return char is list is not empty to prevent stupid commands. """ ret = '' if len(lst) > 1: ret = ch return ret def ret_info(self, tag: str, attr: str, target_attr: str, dprefix: str, hide: str) -> str: """ Create rule in i3 commands format Args: tag (str): target tag. attr (str): tag attrubutes. target_attr (str): attribute to fill. dprefix (str): rule prefix. hide (str): to hide target or not. """ if target_attr in attr: lst = [item for item in self.cfg[tag][target_attr] if item != ''] if lst != []: pref = dprefix+"[" + '{}="'.format(attr) + \ self.ch(self.cfg[tag][attr], '^') for_win_cmd = pref + self.parse_attr(self.cfg[tag][attr]) + \ "move scratchpad, " + self.get_geom(tag) \ + self.scratchpad_hide_cmd(hide) return for_win_cmd return '' @staticmethod def parse_attr(attrib_list: List) -> str: """ Create attribute matching string. Args: tag (str): target tag. attr (str): target attrubute. """ ret = '' if len(attrib_list) > 1: ret += '(' for iter, item in enumerate(attrib_list): ret += item if iter+1 < len(attrib_list): ret += '|' if len(attrib_list) > 1: ret += ')$' ret += '"] ' return ret def create_i3_match_rules(self, hide: bool = True, dprefix: str = "for_window ") -> None: """ Create i3 match rules for all tags. Args: hide (bool): to hide target or not, optional. dprefix (str): i3-cmd prefix is "for_window " by default, optional. """ cmd_list = [] for tag in self.cfg: for attr in self.cfg[tag]: cmd_list.append(self.ret_info( tag, attr, 'class', dprefix, hide) ) cmd_list.append(self.ret_info( tag, attr, 'instance', dprefix, hide) ) self.cmd_list = filter(lambda str: str != '', cmd_list) # nsd need this function def get_geom(self, tag: str) -> str: """ External function used by nsd """ return self.parsed_geom[tag] def parse_geom(self, tag: str) -> str: """ Convert geometry from self.cfg format to i3 commands. Args: tag (str): target self.cfg tag """ rd = {'width': 1920, 'height': 1200} # resolution_default cr = self.current_resolution # current resolution g = re.split(r'[x+]', self.cfg[tag]["geom"]) cg = [] # converted_geom cg.append(int(int(g[0])*cr['width'] / rd['width'])) cg.append(int(int(g[1])*cr['height'] / rd['height'])) cg.append(int(int(g[2])*cr['width'] / rd['width'])) cg.append(int(int(g[3])*cr['height'] / rd['height'])) return "move absolute position {2} {3}, resize set {0} {1}".format(*cg) ==> __init__.py <== import os import sys sys.path.append(os.getenv("XDG_CONFIG_HOME") + "/i3/lib") ==> locker.py <== """ Create a pid lock with abstract socket. Taken from [https://stackoverflow.com/questions/788411/check-to-see-if-python-script-is-running] """ import sys import socket def get_lock(process_name: str) -> None: """ Without holding a reference to our socket somewhere it gets garbage collected when the function exits Args: process_name (str): process name to bind. """ get_lock._lock_socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) try: get_lock._lock_socket.bind('\0' + process_name) print('locking successful') except socket.error: print('lock exists') sys.exit() ==> matcher.py <== """ Matcher module In this class to check that window can be tagged with given tag by WM_CLASS, WM_INSTANCE, regexes, etc. It can be used by named scrachpad, circle run-or-raise, etc. """ import sys import re from typing import List, Iterator class Matcher(): """ Generic matcher class Used by several classes. It can match windows by several criteria, which I am calling "factor", including: - by class, by class regex - by instance, by instance regex - by role, by role regex - by name regex Of course this list can by expanded. It uses sys.intern hack for better performance and simple caching. One of the most resource intensive part of negi3mods. """ factors = [ sys.intern("class"), sys.intern("instance"), sys.intern("role"), sys.intern("class_r"), sys.intern("instance_r"), sys.intern("name_r"), sys.intern("role_r"), sys.intern('match_all') ] def __init__(self): self.matched_list = [] self.match_dict = { sys.intern("class"): lambda: self.win.window_class in self.matched_list, sys.intern("instance"): lambda: self.win.window_instance in self.matched_list, sys.intern("role"): lambda: self.win.window_role in self.matched_list, sys.intern("class_r"): self.class_r, sys.intern("instance_r"): self.instance_r, sys.intern("role_r"): self.role_r, sys.intern("name_r"): self.name_r, sys.intern("match_all"): Matcher.match_all } @staticmethod def find_classed(win: List, pattern: str) -> Iterator: """ Returns iterator to find by window class """ return (c for c in win if c.window_class and re.search(pattern, c.window_class)) @staticmethod def find_instanced(win: List, pattern: str) -> Iterator: """ Returns iterator to find by window instance """ return (c for c in win if c.window_instance and re.search(pattern, c.window_instance)) @staticmethod def find_by_role(win: List, pattern: str) -> Iterator: """ Returns iterator to find by window role """ return (c for c in win if c.window_role and re.search(pattern, c.window_role)) @staticmethod def find_named(win: List, pattern: str) -> Iterator: """ Returns iterator to find by window name """ return (c for c in win if c.name and re.search(pattern, c.name)) def class_r(self) -> bool: """ Check window by class with regex """ for pattern in self.matched_list: cls_by_regex = Matcher.find_classed([self.win], pattern) if cls_by_regex: for class_regex in cls_by_regex: if self.win.window_class == class_regex.window_class: return True return False def instance_r(self) -> bool: """ Check window by instance with regex """ for pattern in self.matched_list: inst_by_regex = Matcher.find_instanced([self.win], pattern) if inst_by_regex: for inst_regex in inst_by_regex: if self.win.window_instance == inst_regex.window_instance: return True return False def role_r(self) -> bool: """ Check window by role with regex """ for pattern in self.matched_list: role_by_regex = Matcher.find_by_role([self.win], pattern) if role_by_regex: for role_regex in role_by_regex: if self.win.window_role == role_regex.window_role: return True return False def name_r(self) -> bool: """ Check window by name with regex """ for pattern in self.matched_list: name_by_regex = Matcher.find_named([self.win], pattern) if name_by_regex: for name_regex in name_by_regex: if self.win.name == name_regex.name: return True return False @staticmethod def match_all() -> bool: """ Match every possible window """ return True def match(self, win, tag: str) -> bool: """ Check that window matches to the config rules """ self.win = win for f in Matcher.factors: self.matched_list = self.cfg.get(tag, {}).get(f, {}) if self.matched_list and self.match_dict[f](): return True return False ==> menu.py <== """ Menu manager module. This module is about creating various menu. For now it contains following menus: - Goto workspace. - Attach to workspace. - Add window to named scratchpad or circle group. - xprop menu to get X11-atom parameters of selected window. - i3-cmd menu with autocompletion. """ import importlib from typing import List from cfg import cfg from misc import Misc from negi3mod import negi3mod class menu(negi3mod, cfg): """ Base class for menu module """ def __init__(self, i3ipc, loop=None) -> None: # Initialize cfg. cfg.__init__(self, i3ipc) # i3ipc connection, bypassed by negi3mods runner self.i3ipc = i3ipc # i3 path used to get "send" binary path self.i3_path = Misc.i3path() # i3-msg application name self.i3cmd = self.conf("i3cmd") # Window properties shown by xprop menu. self.xprops_list = self.conf("xprops_list") # cache screen width if not self.conf("use_default_width"): from display import Display self.screen_width = Display.get_screen_resolution()["width"] else: self.screen_width = int(self.conf('use_default_width')) for mod in self.cfg['modules']: module = importlib.import_module('menu_mods.' + mod) setattr(self, mod, getattr(module, mod)(self)) self.bindings = { "cmd_menu": self.i3menu.cmd_menu, "xprop": self.xprop.xprop, "autoprop": self.props.autoprop, "show_props": self.props.show_props, "pulse_output": self.pulse_menu.pulseaudio_output, "pulse_input": self.pulse_menu.pulseaudio_input, "ws": self.winact.goto_ws, "goto_win": self.winact.goto_win, "attach": self.winact.attach_win, "movews": self.winact.move_to_ws, "gtk_theme": self.gnome.change_gtk_theme, "icon_theme": self.gnome.change_icon_theme, "xrandr_resolution": self.xrandr.change_resolution_xrandr, "reload": self.reload_config, } def args(self, params: dict) -> List[str]: """ Create run parameters to spawn rofi process from dict Args: params(dict): parameters for rofi Return: List(str) to do rofi subprocessing """ prompt = self.conf("prompt") params['width'] = params.get('width', int(self.screen_width * 0.85)) params['prompt'] = params.get('prompt', prompt) params['cnum'] = params.get('cnum', 16) params['lnum'] = params.get('lnum', 2) params['markup_rows'] = params.get('markup_rows', '-no-markup-rows') params['auto_selection'] = \ params.get('auto_selection', "-no-auto-selection") launcher_font = self.conf("font") + " " + \ str(self.conf("font_size")) location = self.conf("location") anchor = self.conf("anchor") matching = self.conf("matching") gap = self.conf("gap") return [ 'rofi', '-show', '-dmenu', '-columns', str(params['cnum']), '-lines', str(params['lnum']), '-disable-history', params['auto_selection'], params['markup_rows'], '-p', params['prompt'], '-i', '-matching', f'{matching}', '-theme-str', f'* {{ font: "{launcher_font}"; }}', '-theme-str', f'#window {{ width:{params["width"]}; y-offset: -{gap}; \ location: {location}; \ anchor: {anchor}; }}', ] def wrap_str(self, string: str) -> str: """ String wrapper to make it beautiful """ return self.conf('left_bracket') + string + self.conf('right_bracket') ==> misc.py <== """ Various helper functions Class for this is created for the more well defined namespacing and more simple import. """ import os import subprocess import errno class Misc(): """ Implements various helper functions """ @staticmethod def create_dir(dirname): """ Helper function to create directory Args: dirname(str): directory name to create """ try: os.makedirs(dirname) except OSError as oserr: if oserr.errno != errno.EEXIST: raise @staticmethod def i3path() -> str: """ Easy way to return i3 config path. """ return os.environ.get("XDG_CONFIG_HOME") + "/i3/" @staticmethod def extract_xrdb_value(field: str) -> str: """ Extracts field from xrdb executable. """ try: out = subprocess.run( f"xrescat '{field}'", shell=True, stdout=subprocess.PIPE, check=True ).stdout if out is not None and out: ret = out.decode('UTF-8').split()[0] return ret except subprocess.CalledProcessError as proc_err: Misc.print_run_exception_info(proc_err) return "" @classmethod def notify_msg(cls, msg: str, prefix: str = " "): """ Send messages via notify-osd based notifications. Args: msg: message string. prefix: optional prefix for message string. """ def get_pids(process): try: pidlist = map( int, subprocess.check_output(["pidof", process]).split() ) except subprocess.CalledProcessError: pidlist = [] return pidlist if get_pids('dunst'): foreground_color = cls.extract_xrdb_value('\\*.foreground') notify_msg = [ 'dunstify', '', f"" + prefix + msg + "" ] subprocess.Popen(notify_msg) @classmethod def notify_off(cls, _dummy_msg: str, _dummy_prefix: str = " "): """ Do nothing """ return @staticmethod def echo_on(*args, **kwargs): """ print info """ print(*args, **kwargs) @staticmethod def echo_off(*_dummy_args, **_dummy_kwargs): """ do not print info """ return @staticmethod def print_run_exception_info(proc_err): print(f'returncode={proc_err.returncode}, \ cmd={proc_err.cmd}, \ output={proc_err.output}')