def get_current_workspace( conn: i3ipc.Connection) -> Tuple[i3ipc.WorkspaceReply, object]: """ Get the focused workspace. Returns ------- A tuple in form (workspace, con), with the focused workspace and the container object for the focused workspace, respectively. """ ws = [w for w in conn.get_workspaces() if w['focused']][0] tree = conn.get_tree() ws_tree = [c for c in tree.descendents() if c.type == 'workspace' and c.name == ws['name']][0] return ws, ws_tree
def render_apps(i3: i3ipc.Connection, ws: Optional[int]): tree = i3.get_tree() wss = i3.get_workspaces() visible_ws = [ws.name for ws in wss if ws.visible] apps = tree.leaves() apps = [app for app in apps if app.workspace().name in visible_ws] if ws is not None: apps = [ app for app in apps if (int(app.workspace().name) - 1) % 3 == ws ] apps.sort(key=lambda app: app.workspace().name) out = '%{O12}'.join(format_entry(app) for app in apps) print(out, flush=True)
#!/usr/bin/python import sys from i3ipc import Connection, Event i3 = Connection() next_num = next(i for i in range(1, 100) if not [ws for ws in i3.get_workspaces() if int(ws.name) == i]) i3.command(f'workspace number {next_num};')
#!/usr/bin/env python3 # # Promotes the focused window by swapping it with the largest window. from i3ipc import Connection, Event def find_biggest_window(container): max_leaf = None max_area = 0 for leaf in container.leaves(): rect = leaf.rect area = rect.width * rect.height if not leaf.focused and area > max_area: max_area = area max_leaf = leaf return max_leaf i3 = Connection() for reply in i3.get_workspaces(): if reply.focused: workspace = i3.get_tree().find_by_id(reply.ipc_data["id"]) master = find_biggest_window(workspace) i3.command("swap container with con_id %s" % master.id)
#!/usr/bin/env python3 from i3ipc import Connection # Lil util to flatten 2d list flatten = lambda t: [item for sublist in t for item in sublist] i3 = Connection() visible_workspaces = list(filter(lambda ws: ws.visible, i3.get_workspaces())) focused_workspace = list(filter(lambda ws: ws.focused, visible_workspaces))[0] named_workspace = i3.get_tree().find_named(focused_workspace.name) workspace_index = list(map(lambda ws: ws.name, named_workspace)).index(focused_workspace.name) floating_windows = list( filter(lambda window: window.floating == 'user_on', named_workspace[workspace_index].leaves())) floating_index = list(map(lambda win: win.focused, floating_windows)).index(True) next_win = floating_windows[(floating_index + 1) % len(floating_windows)] next_win.command('focus')
#!/usr/bin/env python3 from i3ipc import Connection i3 = Connection() scratch = i3.get_tree().scratchpad().leaves() # If there is something in the scratchpad # Lists are true if not empty if scratch: #switch to workspace 42 and making the windows stack i3.command("workspace 42") i3.command("layout stacking") #remove each window from scratchpad and defloat it for window in scratch: window.command("scratchpad show") window.command("floating toggle") else: #make list of used workspaces workspaces = i3.get_workspaces() wsinuse = [] for i in workspaces: wsinuse.append(i.num) #if workspace 42 is already in use, switch to it if 42 in wsinuse: i3.command("workspace 42")
class WorkSpacer: def __init__(self, args): self.i3 = None self.args = args self.workspaces_on_outputs = {} self.workspaces = None self.outputs = None self.config = None self.mouse = pynput.mouse.Controller() self.mouse_position = None self.current_output_name = None def _connect(self): self.print_if_debug('All available outputs on device') self.print_if_debug(names_for_outputs) try: self.i3 = Connection() self.config = self.i3.get_config().__dict__['config'] config_outputs = {} for matchNo, match in enumerate( re.finditer(r'set (\$[a-zA-Z0-9]+) (' + names_for_outputs + ')', self.config, re.MULTILINE), start=1 ): config_outputs[match.group(1)] = match.group(2) self.print_if_debug('All outputs listed in the config, matched on available configs') self.print_if_debug(config_outputs) config_workspace_names = {} for matchNum, match in enumerate( re.finditer(r'set (\$.*) (\d.*)', self.config, re.MULTILINE) ): config_workspace_names[match.group(1)] = match.group(2) self.print_if_debug('All config_workspaces_names') self.print_if_debug(config_workspace_names) for matchNum, match in enumerate( re.finditer(r'workspace (\$.*) output (\$.*)', self.config, re.MULTILINE) ): if not config_outputs.keys().__contains__(match.group(2)): continue # Not an active display, skip it if not self.workspaces_on_outputs.keys().__contains__(config_outputs[match.group(2)]): self.workspaces_on_outputs[config_outputs[match.group(2)]] = [] self.workspaces_on_outputs[config_outputs[match.group(2)]].append( config_workspace_names[match.group(1)]) self.print_if_debug("All workspaces with outputs") self.print_if_debug(self.workspaces_on_outputs) except Exception as exc: if self.args.debug: raise exc sys.exit(1) self.workspaces = [workspaces for workspaces in self.i3.get_workspaces()] outputs = self.i3.get_outputs() self.outputs = [output for output in outputs if output.__dict__["active"] is True] def run(self): self._connect() self.mouse_position = self.mouse.position self.current_output_name = self._get_workspace_from_courser_position() if self.args.shift: self.i3.command( f'move container to workspace {self.workspaces_on_outputs[self.current_output_name][self.args.index - 1]}') if not self.args.keep_with_it: return self.i3.command(f'workspace {self.workspaces_on_outputs[self.current_output_name][self.args.index - 1]}') def _get_workspace_from_courser_position(self): for output in self.outputs: width = output.__dict__["rect"].__dict__["width"] height = output.__dict__["rect"].__dict__["height"] x_offset = output.__dict__["rect"].__dict__["x"] y_offset = output.__dict__["rect"].__dict__["y"] if x_offset == 0 and y_offset == 0: if x_offset <= self.mouse_position[0] <= x_offset + width and y_offset <= \ self.mouse_position[1] <= y_offset + height: return output.__dict__["name"] elif x_offset == 0: if x_offset <= self.mouse_position[0] <= x_offset + width and y_offset < \ self.mouse_position[1] <= y_offset + height: return output.__dict__["name"] elif y_offset == 0: if x_offset < self.mouse_position[0] <= x_offset + width and y_offset <= \ self.mouse_position[1] <= y_offset + height: return output.__dict__["name"] else: if x_offset < self.mouse_position[0] <= x_offset + width and y_offset < \ self.mouse_position[1] <= y_offset + height: return output.__dict__["name"] def _get_workspaces_for_output(self, output): return [workspace for workspace in self.workspaces if workspace.__dict__['output'] == output] def print_if_debug(self, to_print): if self.args.debug: pprint.pprint(to_print)
import i3ipc.events as events import time, os, subprocess sway = Connection() def show_notification(text, timeout=1000): proc = subprocess.Popen(['notify-send', text, '-t', str(timeout)]) proc.wait(2) def on_ws_init(_, e: events.WorkspaceEvent): print('ws init', e.current.name) # Start VS Code on workspace 2 if e.current.name == '2' and os.system('pgrep code') != 0: show_notification('Launching VS Code...', 2000) sway.command('exec code') # Start Firefox on workspace 3 if e.current.name == '3' and os.system('pgrep firefox') != 0: show_notification('Launching Firefox...', 3000) sway.command('exec firefox') sway.on(Event.WORKSPACE_INIT, on_ws_init) sway.get_workspaces() sway.main()
class Heimdall(QObject): """Heimdall service This service maintains a Raspberry Pi or similar device as an automatic display that works without user interaction. Connection timeline: 0. _setup_reestablish_tunnel - Set up a timer to try to reestablish the tunnel. This step is only performed if the tunnel was previously active and failed, and exist to provide a reconnection delay. 1. start_tunnel - Run SSH to the Raspberry pi 2. try_connect - Attempt to use the SSH connection to contact i3/Sway. If this fails, an automatic retry after a timeout is done. 3. setup - Take over the i3/Sway setup """ def __init__(self, parent=None): super().__init__(parent) self.reconnect_timer = QTimer() self.ssh_proc = QProcess() self.bus = QtDBus.QDBusConnection.sessionBus() self.dbus_adaptor = DBusAdaptor(self) self.contextual_executor = ContextualExecutor(self) if not self.bus.isConnected(): raise Exception("Failed to connect to dbus!") self.bus.registerObject("/heimdall", self) self.bus.registerService("com.troshchinskiy.Heimdall") self.homedir = os.environ['HOME'] + "/.heimdall" self.read_config() self.start_tunnel() def echo(self, text): return text def version(self): return "0.1" def connect(self): self.ssh = Popen(["ssh"], stdout=PIPE) def read_config(self): filename = self.homedir + '/config.json' print("Loading config file {}...\n".format(filename)) with open(filename, 'r') as conf_h: self.config = json.load(conf_h) def start_tunnel(self): if self.ssh_proc and self.ssh_proc.isOpen(): print("Tunnel already running") return print("Starting tunnel...\n") sway_pid = self._run_remote(["pidof", "sway"]) if sway_pid is None: raise Exception('Sway is not running!') home_dir = self._run_remote(["echo", '$HOME']) uid = self._run_remote(["echo", '$UID']) self.remote_socket = "/run/user/" + uid + "/sway-ipc." + uid + "." + sway_pid + ".sock" self.local_socket = self.homedir + "/sway.sock" print("Sway pid: '{}'".format(sway_pid)) print("Home dir: '{}'".format(home_dir)) print("UID : '{}'".format(uid)) print("Socket : '{}'".format(self.remote_socket)) if os.path.exists(self.local_socket): os.remove(self.local_socket) r = self.config['remote'] command_args = [ "-i", r['ssh-key'], "-p", r['port'], "-l", r['user'], "-R", r['backwards-port'] + ":127.0.0.1:" + r['local-ssh-port'], "-L", self.local_socket + ':' + self.remote_socket, r['server'] ] print("Running command: ssh {}".format(command_args)) self.ssh_proc.started.connect(self._ssh_process_started) self.ssh_proc.errorOccurred.connect(self._ssh_process_error) self.ssh_proc.finished.connect(self._ssh_process_finished) self.ssh_proc.start(self.config['commands']['ssh'], command_args) def try_connect(self): """Try to connect to i3/Sway. SSH takes a while to perform the port forwarding, so we may do this several times, until it starts working. """ print("Trying to connect to Sway/i3 at socket {}...".format( self.local_socket)) try: self.i3 = Connection(socket_path=self.local_socket) except ConnectionRefusedError: print("Not connected yet!") return except FileNotFoundError: print("Socket doesn't exist yet!!") return self.connect_timer.stop() self.setup() def setup(self): try: print("Setting up Sway/i3...") self.wm_version = self.i3.get_version() print("Connected to Sway/i3 version {}".format(self.wm_version)) print("Resetting workspace...") for workspace in self.i3.get_workspaces(): print("Deleting workspace {}".format(workspace.name)) self.i3.command('[workspace="{}"] kill'.format(workspace.name)) print("Executing commands...") for cmd in self.config['startup']['remote-run']: print("\tExecuting: {}".format(cmd)) self._run_remote(cmd) print("Setting up workspaces...") wsnum = 0 for wsconf in self.config['startup']['workspaces']: wsnum += 1 self.i3.command("workspace {}".format(wsnum)) self.i3.command('rename workspace "{}" to "{}"'.format( wsnum, wsconf['name'])) for wscmd in wsconf['commands']: self.i3_command(wscmd) except (ConnectionRefusedError, FileNotFoundError): self._setup_reestablish_tunnel() def i3_command(self, command): command = command.replace('$TERM_EXEC_KEEP', self.config['remote']['terminal-exec-keep']) command = command.replace('$TERM_EXEC', self.config['remote']['terminal-exec']) command = command.replace('$TERM', self.config['remote']['terminal']) command = command.replace( '$SSH_TO_HOST', self.config['commands']['ssh'] + " -p " + self.config['remote']['backwards-port'] + " -t " + os.environ['USER'] + '@localhost ') print("Executing command: " + command) self.i3.command(command) def contextual_action(self, environment, path, command): self.contextual_executor.execute(environment, path, command) def stop_tunnel(self): """Stop the tunnel, if it's running""" if self.ssh_proc and self.ssh_proc.isOpen(): print("Stopping ssh\n") self.ssh_proc.kill() self.ssh_proc.close() if os.path.exists(self.local_socket): os.remove(self.local_socket) def _setup_reestablish_tunnel(self): """Re-establish the SSH tunnel and begin again the process of syncing up""" self.stop_tunnel() self.reconnect_timer.timeout.connect(self.start_tunnel()) self.reconnect_timer.singleShot(True) self.reconnect_timer.start(100) def _ssh_process_started(self): print("SSH process started!") self.connect_timer = QTimer() self.connect_timer.timeout.connect(self.try_connect) self.connect_timer.start(50) def _ssh_process_error(self, error): print("SSH process failed with error {}!".format(error)) def _ssh_process_finished(self, exit_code, exit_status): print("SSH process exited with code {}, status {}!".format( exit_code, exit_status)) def _run_remote(self, command): r = self.config['remote'] ssh_command = [ self.config['commands']['ssh'], "-i", r['ssh-key'], "-p", r['port'], "-l", r['user'], r['server'] ] ssh_command += command print("Running: {}".format(ssh_command)) result_raw = subprocess.run(ssh_command, stdout=subprocess.PIPE) result = result_raw.stdout.decode('utf-8').strip() return result