pax_global_header00006660000000000000000000000064145527757230014533gustar00rootroot0000000000000052 comment=b7825b218e677c65f6849be061b93bd5654991bf sway-contrib-1.9-contrib.0/000077500000000000000000000000001455277572300156215ustar00rootroot00000000000000sway-contrib-1.9-contrib.0/.github/000077500000000000000000000000001455277572300171615ustar00rootroot00000000000000sway-contrib-1.9-contrib.0/.github/workflows/000077500000000000000000000000001455277572300212165ustar00rootroot00000000000000sway-contrib-1.9-contrib.0/.github/workflows/ci.yaml000066400000000000000000000006741455277572300225040ustar00rootroot00000000000000name: CI on: pull_request jobs: linux: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: '3.10' cache: 'pip' - run: pip install -U -r requirements.txt -r requirements-dev.txt - name: Lint Python run: ruff check --output-format=github **.py - name: Check Python type annotations run: mypy **.py sway-contrib-1.9-contrib.0/LICENSE000066400000000000000000000020411455277572300166230ustar00rootroot00000000000000Copyright (c) 2023 Sungjoon Moon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sway-contrib-1.9-contrib.0/README.md000066400000000000000000000043041455277572300171010ustar00rootroot00000000000000# Sway-Contrib [![CI](https://github.com/OctopusET/sway-contrib/actions/workflows/ci.yaml/badge.svg)](https://github.com/OctopusET/sway-contrib/actions/workflows/ci.yaml) > This repository is a collection of user contributions for the Sway window manager. [Sway](https://github.com/swaywm/sway/) is an i3-compatible tiling window manager for Wayland, offering a lightweight, efficient, and customizable environment. Sway-Contrib is a community-driven effort to share and showcase various user-created configurations, scripts, themes, and other resources that enhance and help the Sway experience. ## Tools | Name | Description | | :---: | :---: | | autoname-workspaces.py | Adds icons to the workspace name for each open window | | firefox-focus-monitor.py | Utility to selectively disable keypresses to specific windows | | grimshot | A helper for screenshots within sway | | inactive-windows-transparency.py | Makes inactive windows transparent | | layout-per-window.py | A script keeps track of the active layout for each window | | switch-top-level.py | A script allows you to define two new bindings | ## Contributing We encourage everyone to contribute to Sway-Contrib! Whether you have a new script, a theme, a configuration file, or any other enhancement for Sway, your contributions are valuable to the community. To contribute, follow these steps: 1. Fork the repository to your GitHub account. 2. Create a new branch for your changes. 3. Make your changes and commit them. 4. Push the changes to your forked repository. 5. Open a pull request to the main Sway-Contrib repository. ## Resources - [Sway Website](https://swaywm.org/): Official website for the Sway window manager. - [Sway Wiki](https://github.com/swaywm/sway/wiki): Official wiki for Sway. ## Support and Issues If you encounter any issues with Sway-Contrib or have questions, feel free to open an issue on the [issue tracker](https://github.com/OctopusET/sway-contrib/issues). ## See also [Sway-Contrib Wiki](https://github.com/OctopusET/sway-contrib/wiki) ## License The Sway-Contrib repository is licensed under the [MIT License](LICENSE). By contributing to this project, you agree that your contributions will be licensed under the same license. sway-contrib-1.9-contrib.0/autoname-workspaces.py000077500000000000000000000067071455277572300222000ustar00rootroot00000000000000#!/usr/bin/python # This script requires i3ipc-python package (install it from a system package manager # or pip). # It adds icons to the workspace name for each open window. # Set your keybindings like this: set $workspace1 workspace number 1 # Add your icons to WINDOW_ICONS. # Based on https://github.com/maximbaz/dotfiles/blob/master/bin/i3-autoname-workspaces import argparse import logging import re import signal import sys import i3ipc WINDOW_ICONS = { "firefox": "", } DEFAULT_ICON = "󰀏" def icon_for_window(window): name = None if window.app_id is not None and len(window.app_id) > 0: name = window.app_id.lower() elif window.window_class is not None and len(window.window_class) > 0: name = window.window_class.lower() if name in WINDOW_ICONS: return WINDOW_ICONS[name] logging.info("No icon available for window with name: %s" % str(name)) return DEFAULT_ICON def rename_workspaces(ipc): for workspace in ipc.get_tree().workspaces(): name_parts = parse_workspace_name(workspace.name) icon_tuple = () for w in workspace: if w.app_id is not None or w.window_class is not None: icon = icon_for_window(w) if not ARGUMENTS.duplicates and icon in icon_tuple: continue icon_tuple += (icon,) name_parts["icons"] = " ".join(icon_tuple) + " " new_name = construct_workspace_name(name_parts) ipc.command('rename workspace "%s" to "%s"' % (workspace.name, new_name)) def undo_window_renaming(ipc): for workspace in ipc.get_tree().workspaces(): name_parts = parse_workspace_name(workspace.name) name_parts["icons"] = None new_name = construct_workspace_name(name_parts) ipc.command('rename workspace "%s" to "%s"' % (workspace.name, new_name)) ipc.main_quit() sys.exit(0) def parse_workspace_name(name): return re.match( "(?P[0-9]+):?(?P\w+)? ?(?P.+)?", name ).groupdict() def construct_workspace_name(parts): new_name = str(parts["num"]) if parts["shortname"] or parts["icons"]: new_name += ":" if parts["shortname"]: new_name += parts["shortname"] if parts["icons"]: new_name += " " + parts["icons"] return new_name if __name__ == "__main__": parser = argparse.ArgumentParser( description="This script automatically changes the workspace name in sway depending on your open applications." ) parser.add_argument( "--duplicates", "-d", action="store_true", help="Set it when you want an icon for each instance of the same application per workspace.", ) parser.add_argument( "--logfile", "-l", type=str, default="/tmp/sway-autoname-workspaces.log", help="Path for the logfile.", ) args = parser.parse_args() global ARGUMENTS ARGUMENTS = args logging.basicConfig( level=logging.INFO, filename=ARGUMENTS.logfile, filemode="w", format="%(message)s", ) ipc = i3ipc.Connection() for sig in [signal.SIGINT, signal.SIGTERM]: signal.signal(sig, lambda signal, frame: undo_window_renaming(ipc)) def window_event_handler(ipc, e): if e.change in ["new", "close", "move"]: rename_workspaces(ipc) ipc.on("window", window_event_handler) rename_workspaces(ipc) ipc.main() sway-contrib-1.9-contrib.0/firefox-focus-monitor.py000066400000000000000000000077201455277572300224450ustar00rootroot00000000000000""" Utility to selectively disable keypresses to specific windows. This program was written due to Firefox's pop-out video player closing when the Escape key is pressed. I use a modal text editor (Helix) that encourages regularly pressing that key and it had a habit of going to the wrong window. The easiest way I could find to make this window specific key-binding change was via this code. Specifically it watches focus changes until the "right" windowds is focused, and then causes Sway to bind the Escape key before Firefox can see it. It continues to watch focus changes so that this binding can be disabled when another window is selected. This feels like a potentially useful pattern, please let us know: https://github.com/OctopusET/sway-contrib of any other programs that this functionality would benefit. """ import argparse import logging import signal from dataclasses import dataclass from typing import Any import i3ipc logger = logging.getLogger(__name__) @dataclass(slots=True) class Watch: container_props: dict[str,str] binds: set[str] class Monitor: _bound: set[str] watched: list[Watch] def __init__(self) -> None: self.ipc = i3ipc.Connection() self.ipc.on("window::focus", self.on_window_event) # firefox creates PIP window without title, so need to watch for changes self.ipc.on("window::title", self.on_window_event) self._bound = set() self.watched = [] def bind(self, *binds: str, **props: str) -> None: self.watched.append(Watch(props, set(binds))) def run(self) -> None: "run main i3ipc event loop" ipc = self.ipc def sighandler(signum: int, frame: Any) -> None: logger.debug("exit signal received, stopping event loop") ipc.main_quit() # stop event loop when we get one of these for sig in signal.SIGINT, signal.SIGTERM: signal.signal(sig, sighandler) try: ipc.main() finally: # clean up self.bound = set() def on_window_event(self, ipc: i3ipc.Connection, event: i3ipc.WindowEvent) -> None: "respond to window events" container = event.container if not container.focused: return data = container.ipc_data logger.debug("window event %s", data) binds = set() for watch in self.watched: if all(data.get(k) == v for k, v in watch.container_props.items()): binds.update(watch.binds) self.bound = binds @property def bound(self) -> set[str]: return self._bound @bound.setter def bound(self, binds: set[str]) -> None: if binds == self._bound: return to_del = self._bound - binds to_add = binds - self._bound if to_del: logger.info(f"removing binds {', '.join(to_del)}") if to_add: logger.info(f"adding binds {', '.join(to_add)}") for bind in to_del: self.ipc.command(f"unbindsym {bind}") for bind in to_add: msg = f"{bind} ignored due to focus monitor" self.ipc.command(f"bindsym {bind} exec echo '{msg}'") self._bound = binds def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="track active window to disable Esc key in Firefox popout media player", ) parser.add_argument( "--verbose", "-v", help="Increase verbosity", action="store_true" ) args = parser.parse_args() args.loglevel = logging.DEBUG if args.verbose else logging.INFO return args KEY_ESCAPE = "Escape" def main() -> None: args = parse_args() logging.basicConfig( level=args.loglevel, format="%(asctime)s %(levelname)s %(message)s", ) mon = Monitor() # block Escape key from reaching Firefox's popout window mon.bind(KEY_ESCAPE, app_id="firefox", name="Picture-in-Picture") mon.run() if __name__ == "__main__": main() sway-contrib-1.9-contrib.0/grimshot000077500000000000000000000124111455277572300174020ustar00rootroot00000000000000#!/bin/sh ## Grimshot: a helper for screenshots within sway ## Requirements: ## - `grim`: screenshot utility for wayland ## - `slurp`: to select an area ## - `swaymsg`: to read properties of current window ## - `wl-copy`: clipboard utility ## - `jq`: json utility to parse swaymsg output ## - `notify-send`: to show notifications ## Those are needed to be installed, if unsure, run `grimshot check` ## ## See `man 1 grimshot` or `grimshot usage` for further details. getTargetDirectory() { test -f "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" && \ . "${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs" echo "${XDG_SCREENSHOTS_DIR:-${XDG_PICTURES_DIR:-$HOME}}" } NOTIFY=no CURSOR= WAIT=no while [ $# -gt 0 ]; do key="$1" case $key in -n|--notify) NOTIFY=yes shift # past argument ;; -c|--cursor) CURSOR=yes shift # past argument ;; -w|--wait) shift WAIT="$1" if echo "$WAIT" | grep "[^0-9]" -q; then echo "invalid value for wait '$WAIT'" >&2 exit 3 fi shift ;; *) # unknown option break # done with parsing --flags ;; esac done ACTION=${1:-usage} SUBJECT=${2:-screen} FILE=${3:-$(getTargetDirectory)/$(date -Ins).png} if [ "$ACTION" != "save" ] && [ "$ACTION" != "copy" ] && [ "$ACTION" != "savecopy" ] && [ "$ACTION" != "check" ]; then echo "Usage:" echo " grimshot [--notify] [--cursor] [--wait N] (copy|save) [active|screen|output|area|window|anything] [FILE|-]" echo " grimshot check" echo " grimshot usage" echo "" echo "Commands:" echo " copy: Copy the screenshot data into the clipboard." echo " save: Save the screenshot to a regular file or '-' to pipe to STDOUT." echo " savecopy: Save the screenshot to a regular file and copy the data into the clipboard." echo " check: Verify if required tools are installed and exit." echo " usage: Show this message and exit." echo "" echo "Targets:" echo " active: Currently active window." echo " screen: All visible outputs." echo " output: Currently active output." echo " area: Manually select a region." echo " window: Manually select a window." echo " anything: Manually select an area, window, or output." exit fi notify() { notify-send -t 3000 -a grimshot "$@" } notifyOk() { [ "$NOTIFY" = "no" ] && return TITLE=${2:-"Screenshot"} MESSAGE=${1:-"OK"} notify "$TITLE" "$MESSAGE" } notifyError() { if [ $NOTIFY = "yes" ]; then TITLE=${2:-"Screenshot"} MESSAGE=${1:-"Error taking screenshot with grim"} notify -u critical "$TITLE" "$MESSAGE" else echo "$1" fi } die() { MSG=${1:-Bye} notifyError "Error: $MSG" exit 2 } check() { COMMAND=$1 if command -v "$COMMAND" > /dev/null 2>&1; then RESULT="OK" else RESULT="NOT FOUND" fi echo " $COMMAND: $RESULT" } takeScreenshot() { FILE=$1 GEOM=$2 OUTPUT=$3 if [ -n "$OUTPUT" ]; then grim ${CURSOR:+-c} -o "$OUTPUT" "$FILE" || die "Unable to invoke grim" elif [ -z "$GEOM" ]; then grim ${CURSOR:+-c} "$FILE" || die "Unable to invoke grim" else grim ${CURSOR:+-c} -g "$GEOM" "$FILE" || die "Unable to invoke grim" fi } if [ "$ACTION" = "check" ] ; then echo "Checking if required tools are installed. If something is missing, install it to your system and make it available in PATH..." check grim check slurp check swaymsg check wl-copy check jq check notify-send exit elif [ "$SUBJECT" = "area" ] ; then GEOM=$(slurp -d) # Check if user exited slurp without selecting the area if [ -z "$GEOM" ]; then exit 1 fi WHAT="Area" elif [ "$SUBJECT" = "active" ] ; then FOCUSED=$(swaymsg -t get_tree | jq -r 'recurse(.nodes[]?, .floating_nodes[]?) | select(.focused)') GEOM=$(echo "$FOCUSED" | jq -r '.rect | "\(.x),\(.y) \(.width)x\(.height)"') APP_ID=$(echo "$FOCUSED" | jq -r '.app_id') WHAT="$APP_ID window" elif [ "$SUBJECT" = "screen" ] ; then GEOM="" WHAT="Screen" elif [ "$SUBJECT" = "output" ] ; then GEOM="" OUTPUT=$(swaymsg -t get_outputs | jq -r '.[] | select(.focused)' | jq -r '.name') WHAT="$OUTPUT" elif [ "$SUBJECT" = "window" ] ; then GEOM=$(swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"' | slurp -r) # Check if user exited slurp without selecting the area if [ -z "$GEOM" ]; then exit 1 fi WHAT="Window" elif [ "$SUBJECT" = "anything" ] ; then GEOM=$(swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"' | slurp -o) # Check if user exited slurp without selecting the area if [ -z "$GEOM" ]; then exit fi WHAT="Selection" else die "Unknown subject to take a screen shot from" "$SUBJECT" fi if [ "$WAIT" != "no" ]; then sleep "$WAIT" fi if [ "$ACTION" = "copy" ] ; then takeScreenshot - "$GEOM" "$OUTPUT" | wl-copy --type image/png || die "Clipboard error" notifyOk "$WHAT copied to buffer" else if takeScreenshot "$FILE" "$GEOM" "$OUTPUT"; then TITLE="Screenshot of $SUBJECT" MESSAGE=$(basename "$FILE") notifyOk "$MESSAGE" "$TITLE" echo "$FILE" if [ "$ACTION" = "savecopy" ]; then wl-copy --type image/png < "$FILE" || die "Clipboard error" notifyOk "$WHAT copied to buffer" fi else notifyError "Error taking screenshot with grim" fi fi sway-contrib-1.9-contrib.0/grimshot.1000066400000000000000000000055601455277572300175450ustar00rootroot00000000000000.\" Generated by scdoc 1.11.2 .\" Complete documentation for this program is not available as a GNU info page .ie \n(.g .ds Aq \(aq .el .ds Aq ' .nh .ad l .\" Begin generated content: .TH "grimshot" "1" "2024-01-01" .P .SH NAME .P grimshot - a helper for screenshots within sway .P .SH SYNOPSIS .P \fBgrimshot\fR [--notify] [--cursor] [--wait N] (copy|save) [TARGET] [FILE] .br \fBgrimshot\fR check .br \fBgrimshot\fR usage .P .SH OPTIONS .P \fB--notify\fR .RS 4 Show notifications to the user that a screenshot has been taken.\& .P .RE \fB--cursor\fR .RS 4 Include cursors in the screenshot.\& .P .RE \fB--wait N\fR .RS 4 Wait for N seconds before taking a screenshot.\& Waits after any manual selection is made.\& Recommended to combine with --notify in order to know when the screenshot has been taken.\& .P .RE \fBsave\fR .RS 4 Save the screenshot into a regular file.\& Grimshot will write image files to \fBXDG_SCREENSHOTS_DIR\fR if this is set (or defined in \fBuser-dirs.\&dir\fR), or otherwise fall back to \fBXDG_PICTURES_DIR\fR.\& Set FILE to '\&-'\& to pipe the output to STDOUT.\& .P .RE \fBcopy\fR .RS 4 Copy the screenshot data (as image/png) into the clipboard.\& .P .RE \fB\fRsavecopy\fB\fR .RS 4 Save the screenshot into a regular file (see \fIsave\fR documentation) and copy the screenshot data into the clipboard (see \fIcopy\fR documentation).\& .P .RE .SH DESCRIPTION .P Grimshot is an easy-to-use screenshot utility for sway.\& It provides a convenient interface over grim, slurp and jq, and supports storing the screenshot either directly to the clipboard using wl-copy or to a file.\& .P .SH EXAMPLES .P An example usage pattern is to add these bindings to your sway config: .P .nf .RS 4 # Screenshots: # Super+P: Current window # Super+Shift+p: Select area # Super+Alt+p Current output # Super+Ctrl+p Select a window bindsym Mod4+p exec grimshot save active bindsym Mod4+Shift+p exec grimshot save area bindsym Mod4+Mod1+p exec grimshot save output bindsym Mod4+Ctrl+p exec grimshot save window .fi .RE .P .SH TARGETS .P grimshot can capture the following named targets: .P \fIactive\fR .RS 4 Captures the currently active window.\& .P .RE \fIscreen\fR .RS 4 Captures the entire screen.\& This includes all visible outputs.\& .P .RE \fIarea\fR .RS 4 Allows manually selecting a rectangular region, and captures that.\& .P .RE \fIwindow\fR .RS 4 Allows manually selecting a single window (by clicking on it), and captures it.\& .P .RE \fIoutput\fR .RS 4 Captures the currently active output.\& .P .RE \fIanything\fR .RS 4 Allows manually selecting a single window (by clicking on it), an output (by clicking outside of all windows, e.\&g.\& on the status bar), or an area (by using click and drag).\& .P .RE .SH OUTPUT .P Grimshot will print the filename of the captured screenshot to stdout if called with the \fIsave\fR or \fIsavecopy\fR subcommands.\& .P .SH SEE ALSO .P \fBgrim\fR(1) sway-contrib-1.9-contrib.0/grimshot.1.scd000066400000000000000000000045231455277572300203130ustar00rootroot00000000000000grimshot(1) # NAME grimshot - a helper for screenshots within sway # SYNOPSIS *grimshot* [--notify] [--cursor] [--wait N] (copy|save) [TARGET] [FILE]++ *grimshot* check++ *grimshot* usage # OPTIONS *--notify* Show notifications to the user that a screenshot has been taken. *--cursor* Include cursors in the screenshot. *--wait N* Wait for N seconds before taking a screenshot. Waits after any manual selection is made. Recommended to combine with --notify in order to know when the screenshot has been taken. *save* Save the screenshot into a regular file. Grimshot will write image files to *XDG_SCREENSHOTS_DIR* if this is set (or defined in *user-dirs.dir*), or otherwise fall back to *XDG_PICTURES_DIR*. Set FILE to '-' to pipe the output to STDOUT. *copy* Copy the screenshot data (as image/png) into the clipboard. **savecopy** Save the screenshot into a regular file (see _save_ documentation) and copy the screenshot data into the clipboard (see _copy_ documentation). # DESCRIPTION Grimshot is an easy-to-use screenshot utility for sway. It provides a convenient interface over grim, slurp and jq, and supports storing the screenshot either directly to the clipboard using wl-copy or to a file. # EXAMPLES An example usage pattern is to add these bindings to your sway config: ``` # Screenshots: # Super+P: Current window # Super+Shift+p: Select area # Super+Alt+p Current output # Super+Ctrl+p Select a window bindsym Mod4+p exec grimshot save active bindsym Mod4+Shift+p exec grimshot save area bindsym Mod4+Mod1+p exec grimshot save output bindsym Mod4+Ctrl+p exec grimshot save window ``` # TARGETS grimshot can capture the following named targets: _active_ Captures the currently active window. _screen_ Captures the entire screen. This includes all visible outputs. _area_ Allows manually selecting a rectangular region, and captures that. _window_ Allows manually selecting a single window (by clicking on it), and captures it. _output_ Captures the currently active output. _anything_ Allows manually selecting a single window (by clicking on it), an output (by clicking outside of all windows, e.g. on the status bar), or an area (by using click and drag). # OUTPUT Grimshot will print the filename of the captured screenshot to stdout if called with the _save_ or _savecopy_ subcommands. # SEE ALSO *grim*(1) sway-contrib-1.9-contrib.0/inactive-windows-transparency.py000077500000000000000000000037301455277572300242020ustar00rootroot00000000000000#!/usr/bin/python # This script requires i3ipc-python package (install it from a system package manager # or pip). # It makes inactive windows transparent. Use `transparency_val` variable to control # transparency strength in range of 0…1 or use the command line argument -o. import argparse import signal import sys from functools import partial import i3ipc def on_window_focus(inactive_opacity, ipc, event): global prev_focused global prev_workspace focused_workspace = ipc.get_tree().find_focused() if focused_workspace is None: return focused = event.container workspace = focused_workspace.workspace().num if focused.id != prev_focused.id: # https://github.com/swaywm/sway/issues/2859 focused.command("opacity 1") if workspace == prev_workspace: prev_focused.command("opacity " + inactive_opacity) prev_focused = focused prev_workspace = workspace def remove_opacity(ipc): for workspace in ipc.get_tree().workspaces(): for w in workspace: w.command("opacity 1") ipc.main_quit() sys.exit(0) if __name__ == "__main__": transparency_val = "0.80" parser = argparse.ArgumentParser( description="This script allows you to set the transparency of unfocused windows in sway." ) parser.add_argument( "--opacity", "-o", type=str, default=transparency_val, help="set opacity value in range 0...1", ) args = parser.parse_args() ipc = i3ipc.Connection() prev_focused = None prev_workspace = ipc.get_tree().find_focused().workspace().num for window in ipc.get_tree(): if window.focused: prev_focused = window else: window.command("opacity " + args.opacity) for sig in [signal.SIGINT, signal.SIGTERM]: signal.signal(sig, lambda signal, frame: remove_opacity(ipc)) ipc.on("window::focus", partial(on_window_focus, args.opacity)) ipc.main()sway-contrib-1.9-contrib.0/layout-per-window.py000077500000000000000000000027651455277572300216160ustar00rootroot00000000000000#!/usr/bin/env python # This script keeps track of the active layout for each window. # # This script requires i3ipc-python package (install it from a system package # manager or pip). import i3ipc def on_window_focus(ipc: i3ipc.connection.Connection, event: i3ipc.events.WindowEvent): global windows, prev_focused # Save current layout layouts = {input.identifier : input.xkb_active_layout_index for input in ipc.get_inputs()} windows[prev_focused] = layouts # Restore layout of the newly focused window if event.container.id in windows: for (kdb_id, layout_index) in windows[event.container.id].items(): if layout_index != layouts[kdb_id]: ipc.command(f'input "{kdb_id}" xkb_switch_layout {layout_index}') break prev_focused = event.container.id def on_window_close(ipc: i3ipc.connection.Connection, event: i3ipc.events.WindowEvent): global windows if event.container.id in windows: del(windows[event.container.id]) def on_window(ipc: i3ipc.connection.Connection, event: i3ipc.events.WindowEvent): if event.change == "focus": on_window_focus(ipc, event) elif event.change == "close": on_window_close(ipc, event) if __name__ == "__main__": ipc = i3ipc.Connection() focused = ipc.get_tree().find_focused() if focused: prev_focused = focused.id else: prev_focused = None windows = {} # type: dict ipc.on("window", on_window) ipc.main() sway-contrib-1.9-contrib.0/pyproject.toml000066400000000000000000000002421455277572300205330ustar00rootroot00000000000000[tool.ruff] select = ["E", "F", "B", "Q", "I"] target-version = "py310" line-length = 120 [[tool.mypy.overrides]] module = "i3ipc" ignore_missing_imports = true sway-contrib-1.9-contrib.0/requirements-dev.txt000066400000000000000000000000111455277572300216510ustar00rootroot00000000000000ruff mypysway-contrib-1.9-contrib.0/requirements.txt000066400000000000000000000000061455277572300211010ustar00rootroot00000000000000i3ipc sway-contrib-1.9-contrib.0/sway-session.target000066400000000000000000000002541455277572300214760ustar00rootroot00000000000000[Unit] Description=Sway session Documentation=man:systemd.special(7) BindsTo=graphical-session.target Wants=graphical-session-pre.target After=graphical-session-pre.target sway-contrib-1.9-contrib.0/switch-top-level.py000077500000000000000000000060701455277572300214070ustar00rootroot00000000000000#!/usr/bin/env python3 import i3ipc # # This script requires i3ipc-python package (install it from a system package manager # or pip). # # The scripts allows you to define two new bindings: # bindsym $mod+bracketright nop top_next # bindsym $mod+bracketleft nop top_prev # # The purpose of it is to switch between top-level containers (windows) in a workspace. # One possible usecase is having a workspace with two (or more on large displays) # columns of tabs: one on left and one on right. In such setup, "move left" and # "move right" will only switch tabs inside the column. # # You can add a systemd user service to run this script on startup: # # ~> cat .config/systemd/user/switch-top-level.service # [Install] # WantedBy=graphical-session.target # [Service] # ExecStart=path/to/switch-top-level.py # Restart=on-failure # RestartSec=1 # [Unit] # Requires=graphical-session.target class TopLevelSwitcher: def __init__(self): self.top_to_selected = {} # top i3ipc.Con -> selected container id self.con_to_top = {} # container id -> top i3ipc.Con self.prev = None # previously focused container id self.i3 = i3ipc.Connection() self.i3.on("window::focus", self.on_window_focus) self.i3.on(i3ipc.Event.BINDING, self.on_binding) self.update_top_level() self.i3.main() def top_level(self, node): if len(node.nodes) == 1: return self.top_level(node.nodes[0]) return node.nodes def update_top_level(self): tree = self.i3.get_tree() for ws in tree.workspaces(): for con in self.top_level(ws): self.update_top_level_rec(con, con.id) def update_top_level_rec(self, con: i3ipc.Con, top: i3ipc.Con): self.con_to_top[con.id] = top for child in con.nodes: self.update_top_level_rec(child, top) if len(con.nodes) == 0 and top not in self.top_to_selected: self.top_to_selected[top] = con.id def save_prev(self): if not self.prev: return prev_top = self.con_to_top.get(self.prev) if not prev_top: return self.top_to_selected[prev_top] = self.prev def on_window_focus(self, _i3, event): self.update_top_level() self.save_prev() self.prev = event.container.id def on_top(self, _i3, _event, diff: int): root = self.i3.get_tree() if not self.prev: return top = self.con_to_top[self.prev] ws = [top.id for top in self.top_level(root.find_focused().workspace())] top_idx = ws.index(top) top_idx = (top_idx + diff + len(ws)) % len(ws) next_top = ws[top_idx] next_window = self.top_to_selected.get(next_top) self.i3.command("[con_id=%s] focus" % next_window) def on_binding(self, i3, event): if event.binding.command.startswith("nop top_next"): self.on_top(i3, event, 1) elif event.binding.command.startswith("nop top_prev"): self.on_top(i3, event, -1) if __name__ == "__main__": TopLevelSwitcher()