pax_global_header00006660000000000000000000000064147506332630014523gustar00rootroot0000000000000052 comment=cf0e86b8f66f8b287c900b9331e7681e952042be sway-contrib-1.10.1/000077500000000000000000000000001475063326300142245ustar00rootroot00000000000000sway-contrib-1.10.1/.github/000077500000000000000000000000001475063326300155645ustar00rootroot00000000000000sway-contrib-1.10.1/.github/workflows/000077500000000000000000000000001475063326300176215ustar00rootroot00000000000000sway-contrib-1.10.1/.github/workflows/ci.yaml000066400000000000000000000006741475063326300211070ustar00rootroot00000000000000name: 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.10.1/LICENSE000066400000000000000000000020411475063326300152260ustar00rootroot00000000000000Copyright (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.10.1/README.md000066400000000000000000000046251475063326300155120ustar00rootroot00000000000000# 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 | | grimpicker | A simple color picker for wlroots | | 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. ## Packaging status [![Packaging status](https://repology.org/badge/vertical-allrepos/sway-contrib.svg)](https://repology.org/project/sway-contrib/versions) sway-contrib-1.10.1/autoname-workspaces.py000077500000000000000000000067071475063326300206030ustar00rootroot00000000000000#!/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.10.1/firefox-focus-monitor.py000066400000000000000000000077201475063326300210500ustar00rootroot00000000000000""" 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.10.1/float-window-manager/000077500000000000000000000000001475063326300202465ustar00rootroot00000000000000sway-contrib-1.10.1/float-window-manager/float-window-managerd-readme.txt000066400000000000000000000076331475063326300264410ustar00rootroot00000000000000FloatWindowManager is designed to remember where you put floating windows, so that they appear there the next time they appear - thus they are not always appearing dead center in the screen. This is accomplished by subscribing to swaymsg's window events, and storing all window events that are new, close, or float. When a float window is opened, its position is stored. When it closes, if the position is different, the window's position as a percentage is stored in a file, the filename being the window title. Percentages in move commands started in Sway version 1.6, so this script will not work with Sway versions before that. When floatwindowmanager starts up, it looks through all files in a specified directory ("$HOME/.config/sway/float_window_store/"), and creates "for_window" rules for each one instructing sway to move the window to x and y percentages of the output's width/height. Thus Sway itself moves the windows. FloatWindowManager also remembers where you place windows that HAD BEEN tiling, but that you have converted to floating. Since a window that changes from tiling to floating (or from floating to tiling) is not a new window, Sway will not automatically move the windows; the script does that "manually". If a window was moved accidentally, that you would rather just leave centered, you can delete the file from the above directory, or leave it there and erase the percentages within. This script uses 4 commands (at least) that must be present in order to run: swaymsg, inotifywait, jq, and notify-send. If you're using Sway, then you'll have swaymsg, but the others may need to be installed. On Debian, inotifywait is available in the package inotify-tools, jq is from the identically-named package jq, and notify-send is from libnotify-bin (for notifications, I use emersion's mako, from the mako-notifier package on Debian) How to use: Move/copy the script to somewhere on your path. Add a line to the config file similar to the following, which I use: exec float-window-managerd.sh > $LOGS/$(date +"%Y%m%d:%H%M%S_")float-window-managerd.log 2>&1 (I don't know if there would be any advantage with using exec_always - I don't know if Sway throws out all existing for_window rules for a config-reload.) If you wish to look at the log file, then use the re-directs, and define $LOGS to be where you want to find logs, or use an already defined location. Presumably, you'll want to have the line earlier in the config than when where you call the first app that has a floating window that you want to move... (cannot rule out possible race conditions, of course...), 'though after Mako (or similar notification app). I have NOT included a command to turn FloatWindowManager off. While I could call the same process-killing code that is part of the program, Sway's for_window rules would still be in play. I don't know of any way to remove for_window rules, and while I could issue new for_window rules with "move position center", the old rules will still be there. If there are a lot, it could be slow, maybe ending up with the window moving twice. Turning it off can be done with commenting-out/deleting the config-file line, and logging out/in again. How it works: FloatWindowManager is a daemon (I believe how it works qualifies for that term) that sits and waits for a certain things to happen. inotifywait is called to notify when a certain file changes. A request is made to subscribe to Sway's window events, and only new/close/floating events are added to a file - the file that inotifywait is monitoring. This file is '/tmp/sway_win_events.txt', and (being in /tmp) will be deleted when rebooting; On startup, if the file is found, it is reset to empty. Those window events are read and handled, and then a different inotifywait call is used to wait until the first inotifywait notices again that are new events. Then the new events are handled, etc. This system ensures that no events are missed while earlier events are being handled. sway-contrib-1.10.1/float-window-manager/float-window-managerd.sh000077500000000000000000000430021475063326300247720ustar00rootroot00000000000000#!/usr/bin/env bash # $HOME/.local/bin/float-window-managerd.sh # This version will attempt to also handle windows that were tiling, but are now floating. # The type of window event is: "change": "floating" # This is the same whether changing to floating, or changing from floating to tiling. # The "container" "type" is what the window is changing TO: "floating_con" if changing to floating, "con" if changing to tiling # Since this does not involve a window's creation, one cannot use a for_window to automatically move the window. # The event's container's rect.x and rect.y appear to be the position of the floating window, whether it is just becoming floating # or just becoming tiling. This means we DO have the ability to see the original position when floating, and the last position # before tiling. cmds_all_found=true for cmd in swaymsg inotifywait jq notify-send; do output=$(command -v "$cmd") if [[ "$output" =~ ^alias ]]; then echo cmd \'"$cmd"\' found as an alias. else if [[ "$output" == "" ]]; then echo cmd \'"$cmd"\' not found. cmds_all_found=false else if ! [[ -x "$output" ]]; then echo cmd \'"$cmd"\' found, but is not executable. cmds_all_found=false else echo cmd \'"$cmd"\' found and is executable. fi fi fi done if ! "$cmds_all_found"; then echo Not all commands were found. notify-send "Not all required commands for float-window-manager were found." exit 1 else echo All commands were found. fi version=$(swaymsg -t get_version | jq -r '.human_readable') if [[ "$version" < "1.6" ]]; then echo This version of Sway is earlier than supports moving windows with percentages. echo Version is \'"$version"\'. Version needed: \'1.6\'. exit 1 fi winpath="$HOME/.config/sway/float_window_store/" [ -d "$winpath" ] || mkdir -p -v "$winpath" # " -p no error if existing, make parent directories as needed" # https://stackoverflow.com/questions/15783701/which-characters-need-to-be-escaped-when-using-bash doublequote=\" singlequote=\' # I noticed that the GIMP main window ends with '– GIMP', where the dash is not '-'. but '–', so I have added it to "special", in case it's a problem for regexp special=$'`!@#$%^&*()-–_+={}|[]\\;\:,.<>?/ '$doublequote$singlequote # featherpad's syntax highlighting seems to get messed up with these actual quotes in the special string... backslash=$'\\' escape_bad_chars_in_winname() { local winname="$1" winname_escape="" for ((i=0; i<"${#winname}"; i++)); do c="${winname:i:1}" if [[ $special =~ "$c" ]]; then # if not in quotes, some are missed, like '*' # if [[ $special == *"$c"* ]]; then # another way to do this (like with case statements) c1="$backslash$c" else c1="$c" fi winname_escape+="$c1" done echo "$winname_escape" } unset_arrays() { unset already_processed["$con_id"] unset was_tiling["$con_id"] unset outputwidth["$con_id"] unset outputheight["$con_id"] unset orig_win_x["$con_id"] unset orig_win_y["$con_id"] unset ws_x["$con_id"] unset ws_y["$con_id"] unset win_deco_height["$con_id"] unset ignore_me["$con_id"] echo Arrays unset... } # Make a Sway for_window rule for each file in $winpath, using the filename as title, and the contents as x and y percentages for file in $winpath/*; do filename=$(basename "$file") winname="$filename" read -r xperc yperc < "$file" > /dev/null 2>&1 result=$? if (( "$result" != 0 )); then echo file \'"$file"\' could not be read... continue fi echo winname= \'"$winname"\' xperc= \'"$xperc"\' yperc= \'"$yperc"\' if [[ ("$xperc" == "") && ("$yperc" == "") ]]; then # "ignore me" echo winname= \'"$winname"\' --\> \"Ignore Me\" continue fi if [[ ( ("$xperc" != "") && ("$yperc" != "") ) && ( ! ("$xperc" =~ ^[+-]?[0-9]{1,2}$) && ("$yperc" =~ ^[+-]?[0-9]{1,2}$) ) ]]; then # sign (or not) plus 1-2 digits echo Window \'"$winname"\' has invalid percentage[s]. Ignoring. continue fi if [[ ( "$xperc" -lt 0 ) || ( "$xperc" -gt 100 ) || ( "$yperc" -lt 0 ) || ( "$yperc" -gt 100 ) ]]; then echo Window \'"$winname"\' has percentage[s] outside 0-100. Ignoring. continue fi winname="$(escape_bad_chars_in_winname "$winname")" swaymsg_output=$(swaymsg -- for_window \[title="$doublequote$winname$doublequote"\] "move position "$xperc" ppt "$yperc" ppt") result=$? # "swaymsg_output" is always non-blank, as a jq result with { "success": true } is printed if we are successful. if [[ ! "$result" == 0 ]]; then echo Window \'"$winname"\': for_window fails with message: \'"$swaymsg_output"\' fi done declare -i con_id opw oph declare -a PIDarray we_array outputwidth outputheight orig_win_x orig_win_y ignore_me declare -a already_processed was_tiling ws_x ws_y win_deco_height we_file='/tmp/sway_win_events.txt' if [[ -s "$we_file" ]]; then # ...if we find "$we_file", and it has non-zero size... truncate -s 0 "$we_file" else touch "$we_file" fi echo echo echo '=============================================================' echo Now killing any other window-placement processes... echo BASHPID= \'"$BASHPID"\' BASH_SID=$(ps -p "$BASHPID" -o sid=) echo BASH_SID= \'"$BASH_SID"\' #PS=$(pgrep -f -d ' ' "float-window-placement[0-9]*d.sh") #instead of \n, -d elimit with a space. PS=$(pgrep -f -d ' ' "float-window-managerd.sh") #instead of \n, -d elimit with a space. # -f "match against the ...full command line, not just the process name." echo PS= \'"$PS"\' IFS=" " read -r -a PIDarray <<< "$PS" for PID in "${PIDarray[@]}" do echo PID= \'"$PID"\' SID=$(ps -p "$PID" -o sid=) if [[ $SID == "" ]]; then #...already killed this one... echo ...must already have killed the SID for PID= \'"$PID"\' continue fi if [[ "$SID" != "$BASH_SID" ]]; then echo killing session ID \'$SID\' pkill -s $SID #not quoted, as $SID as generated by PS has a space at the beginning... else echo oops! That\'s OUR SID! \($BASH_SID\) Not killing... fi done echo '=============================================================' echo echo notify-send "...starting Float Window Manager..." # We will constantly monitor (-m) we_file, and when it changes, write stuff (we don't care what) to inw_file # Later, instead of constantly looping to see if we_file has had more lines added to it (and thus use 20-25% CPU), # we will call inotifywait again (but NOT with -m) and have it wake us when something changes. inw_file='/tmp/sway_win_events_inotifywait.txt' inotifywait -m -e modify "$we_file" > "$inw_file" 2>&1 & swaymsg -t subscribe -m '[ "window" ]' | grep --line-buffered -E '"change": "new"|"change": "close"|"change": "floating"' >> "$we_file" & echo now starting to read window new and close events from file # We will read events from we_file until we cannot, and then inotifywait until there are events again i=1 while true; do while true; do IFS=$'\n' read -d '\n' -a we_array < <(tail -n +"$i" "$we_file" ) # read from line $i to [current] EOF num_events="${#we_array[@]}" if [[ "$num_events" == 0 ]]; then break; # while true - inner loop - go back to waiting in outer 'while true' loop fi echo echo i=$i read $num_events events for we in "${we_array[@]}"; do echo '==================================================================================' echo \'"$we"\' echo # $con.name is sometimes null, and messes up the read; we change null to bogus name here, and will deal with it later... IFS=$'\t' read -r event_type con_id event_win_name event_win_type event_win_x event_win_y \ < <(echo "$we" | jq -r '. as $root | $root.container as $con | [$root.change, $con.id, $con.name // "NuLlnUlLNuLlnUlL", $con.type, $con.rect.x, $con.rect.y] | @tsv ') event_win_name=${event_win_name//NuLlnUlLNuLlnUlL/} if [[ "$event_type" == "floating" ]]; then if [[ "$event_win_type" == "floating_con" ]]; then event_type="to_floating" echo event_type "floating" ---> "to_floating" else event_type="to_tiling" echo event_type "floating" ---> "to_tiling" fi else # handle a closed originally-tiling window as if it reverted to tiling if [[ ("$event_type" == "close") && ("${was_tiling["$con_id"]}" == "yes") ]]; then event_type="to_tiling" echo event_type "close" ---> "to_tiling" fi fi echo \'"$event_type"\' \'"$con_id"\' \'"$event_win_type"\' \'"$event_win_name"\' echo Window position: "$event_win_x" , "$event_win_y" case "$event_type" in *"new"* | *"to_floating"*) if [[ "$event_type" == "new" ]]; then sleep 1 # delay long enough for name to get into win_name, and win_type to be set to "floating_con" (or not) else # "to_floating" if [[ -v already_processed["$con_id"] ]]; then echo We already processed a \'new\' event for window \'"$con_id"\' echo continue # for we in "${we_array[@]}" fi fi # $win.name is sometimes null, and messes up the read; we change null to bogus name here, and will deal with it later... IFS=$'\t' read -r win_name win_type win_x win_y win_deco_height ws_name ws_x ws_y output_width output_height < <(swaymsg -t get_tree | \ jq -r --arg CONID "$con_id" '. as $root | $root.nodes[] as $output | $output.nodes[] as $ws | $ws.floating_nodes[] as $win | select ( $win.id == ($CONID | tonumber) ) | [$win.name // "NuLlnUlLNuLlnUlL", $win.type, $win.rect.x, $win.rect.y, $win.deco_rect.height, $ws.name, $ws.rect.x, $ws.rect.y, $output.rect.width, $output.rect.height] | @tsv') result=$? if [[ "$result" != 0 ]]; then echo result=\'"$result"\' ...apparently window matching con_id=\'"$con_id"\' is not floating - aborting this event... unset_arrays continue # for we in "${we_array[@]}" fi win_name=${win_name//NuLlnUlLNuLlnUlL/} # change bogus "name" back into empty string echo \'$event_type\' \'$con_id\' \'$win_name\' echo \'"$win_name"\' type=\'"$win_type"\' x:y $win_x : $win_y win_deco_height=$win_deco_height ws:$ws_name x:$ws_x y:$ws_y $output_width x $output_height already_processed["$con_id"]="yes" # this will never be "no" - we just test for set/unset ... if [[ "$event_type" == "new" ]]; then was_tiling["$con_id"]="no" else was_tiling["$con_id"]="yes" fi outputwidth["$con_id"]="$output_width" outputheight["$con_id"]="$output_height" orig_win_x["$con_id"]="$win_x" orig_win_y["$con_id"]="$win_y" ws_x["$con_id"]="$ws_x" ws_y["$con_id"]="$ws_y" win_deco_height["$con_id"]="$win_deco_height" ignore_me["$con_id"]="no" if [[ "$win_name" != "" ]]; then # $win_name often 'null'-->"" for 'to_floating' wn="$win_name" else wn="$event_win_name" fi file="$winpath""$wn" read -r xperc yperc < "$file" > /dev/null 2>&1 result=$? echo file-read result: \'$result\' if (( "$result" == 0 )); then echo -n 'xperc:yperc' \'"$xperc"\' : \'"$yperc"\' # echo line finished with the below '--->' \"Ignore me\", or with other text. if [[ ("$xperc" == "") && ("$yperc" == "") ]]; then ignore_me["$con_id"]="yes" notify-send "Position of window \"$wn\" ignored." echo '--->' \"Ignore me\" else if [[ ( ("$xperc" != "") && ("$yperc" != "") ) && ( ("$xperc" =~ ^[+-]?[0-9]{1,2}$) && ("$yperc" =~ ^[+-]?[0-9]{1,2}$) ) ]]; then # sign (or not) plus 1-2 digits if [[ ( "$xperc" -ge 0 ) || ( "$xperc" -le 100 ) || ( "$yperc" -ge 0 ) || ( "$yperc" -le 100 ) ]]; then echo '---> percentages present and valid.' if [[ "$event_type" == "to_floating" ]]; then # we have to move the window ourselves. swaymsg_output=$(swaymsg -- \[con_id=$con_id\] "move position "$xperc" ppt "$yperc" ppt") result=$? # "swaymsg_output" is always non-blank, as a jq result with { "success": true } is printed if we are successful. if [[ "$result" != 0 ]]; then echo Window \'"$wn"\': \"move position\" fails with message: \'"$swaymsg_output"\' unset_arrays continue # for we in "${we_array[@]}" fi # Now, we'll have to get the window from swaymsg, as the position changed, and we will need to know later for "close"/"to_tiling # if its position later changed from the saved position. IFS=$'\t' read -r win_x win_y < <(swaymsg -t get_tree | \ jq -r --arg CONID "$con_id" '. as $root | $root.nodes[] as $output | $output.nodes[] as $ws | $ws.floating_nodes[] as $win | select ( $win.id == ($CONID | tonumber) ) | [$win.rect.x, $win.rect.y] | @tsv') result=$? if [[ "$result" != 0 ]]; then echo result=\'"$result"\' ...apparently window matching con_id=\'"$con_id"\' is not floating - aborting this event... unset_arrays continue # for we in "${we_array[@]}" fi orig_win_x["$con_id"]="$win_x" orig_win_y["$con_id"]="$win_y" echo window moved to "$win_x" "$win_y" echo window moved to $xperc % $yperc % notify-send "Position of window \"$wn\" moved." fi # "$event_type" == "to_floating" else echo '---> percentage[s] out of bounds. Will fix later, if moved.' fi # xperc/yperc present and correctly formed, but out of bounds ? else echo '---> missing/malformed percentage[s]. Will fix later, if moved.' fi # xperc/yperc present and valid ? fi # xperc AND yperc present ? ? fi # file read successful ? ;; *"close"* | *"to_tiling"*) echo \'"$event_type"\' \'"$con_id"\' \'"$event_win_name"\' if [[ ("$event_type" == "close") && ("$event_win_type" != "floating_con") ]]; then echo 'Not a floating-con...' continue # for we in "${we_array[@]}" fi opw=${outputwidth["$con_id"]} oph=${outputheight["$con_id"]} echo \(i.m.: ${ignore_me["$con_id"]}\) \(x: new:$event_win_x : old:${orig_win_x["$con_id"]}\) \(y: new:$event_win_y : old:${orig_win_y["$con_id"]}\) echo outputwidth of "$con_id" is: $opw outputheight of "$con_id" is: $oph if [[ ($event_win_x -ge 0 ) ]]; then echo event-win-x IS GE 0; else echo event-win-x IS NOT GE 0; fi if [[ ($event_win_y -ge 0 ) ]]; then echo event-win-y IS GE 0; else echo event-win-y IS NOT GE 0; fi if [[ ($event_win_x -lt $opw) ]]; then echo event-win-x IS LT outputwidth; else echo event-win-x IS NOT LT outputwidth; fi if [[ ($event_win_y -lt $oph) ]]; then echo event-win-y IS LT outputheight; else echo event-win-y IS NOT LT outputheight; fi if [[ (${ignore_me["$con_id"]} == "no") && (($event_win_x != ${orig_win_x["$con_id"]}) || ($event_win_y != ${orig_win_y["$con_id"]})) && (($event_win_x -ge 0 ) && ($event_win_y -ge 0 )) && (($event_win_x -lt $opw) && ($event_win_y -lt $oph)) ]]; then #NOTE: We now store percentage as 0-100 so we multiply by 100, and round to the nearest pixel by adding .5 and truncating ("scale=0") # We have to do scale=4, and then take the result and do scale=0 to truncate, as bc won't do it all in one, for some reason... # (and won't do scale=0 if no division in calc, so " / 1") # sway does not use the whole output width/height when it performs its percentage window placement, but offsets by the workspace x/y # and deco-rect height. Currently, only ws_y and win-deco-height seem not to be non-zero, but ... opw_m_wsx=$(echo "$opw-${ws_x["$con_id"]}" | bc -l) xperc=$(echo "scale=4; $event_win_x / $opw_m_wsx * 100 + .5" | bc -l) xperc=$(echo "scale=0; $xperc / 1" | bc -l) oph_m_wsy_m_wdh=$(echo "($oph-${ws_y["$con_id"]})-${win_deco_height["$con_id"]}" | bc -l) yperc=$(echo "scale=4; $event_win_y / $oph_m_wsy_m_wdh * 100 + .5" | bc -l) yperc=$(echo "scale=0; $yperc / 1" | bc -l) echo xperc: $xperc, yperc: $yperc file="$winpath""$event_win_name" echo $xperc $yperc > "$file" result=$? if [[ "$result" != 0 ]]; then notify-send "Could not save window \"$event_win_name\" position."; else notify-send "Window \"$event_win_name\" position saved." if [[ "$event_type" == "close" ]]; then echo window \"$event_win_name\" - position saved. echo Making new/changed "for_window" for \'"$event_win_name"\' ... echo wn before: \'"$event_win_name"\' xperc= \'"$xperc"\' yperc= \'"$yperc"\' event_win_name="$(escape_bad_chars_in_winname "$event_win_name")" echo wn after: \'"$event_win_name"\' swaymsg_output=$(swaymsg -- for_window \[title="$doublequote$event_win_name$doublequote"\] "move position "$xperc" ppt "$yperc" ppt") result=$? # "swaymsg_output" is always non-blank, as a jq result with { "success": true } is printed if we are successful. if [[ "$result" != 0 ]]; then echo Window \'"$event_win_name"\': for_window fails with message: \'"$swaymsg_output"\' fi else echo originally-tiled window \"$event_win_name\" - position saved. fi fi # file write successful ? else notify-send "Window \"$event_win_name\" ignored (or not moved)." echo Nothing changed here for \'"$event_win_name"\', or we are ignoring it, or it is offscreen. fi # ignore?/unchanged?/off-screen? unset_arrays ;; *) ;; esac # "$event_type" echo done # for we in "${we_array[@]}" ((i+="$num_events")) done # while true - inner loop - read from we_file until num_events == 0 echo nothing to read, so we go to sleep with inotifywait inotifywait -e modify "$inw_file" done # while true - inotifywait exit 0 # probably never reached, but ... sway-contrib-1.10.1/grimpicker/000077500000000000000000000000001475063326300163605ustar00rootroot00000000000000sway-contrib-1.10.1/grimpicker/Makefile000066400000000000000000000011131475063326300200140ustar00rootroot00000000000000PKGNAME = "grimpicker" DESTDIR ?= "" PREFIX ?= "/usr" .PHONY: build install build: scdoc <"${PKGNAME}.1.scd" >"${PKGNAME}.1" install: # Not installing zsh completion here as its destination depends on the distribution install -D -m 755 "${PKGNAME}" "${DESTDIR}${PREFIX}/bin/${PKGNAME}" install -D -m 644 "completion.bash" "${DESTDIR}${PREFIX}/share/bash-completion/completions/${PKGNAME}" install -D -m 644 "completion.fish" "${DESTDIR}${PREFIX}/share/fish/vendor_completions.d/${PKGNAME}.fish" install -D -m 644 "${PKGNAME}.1" "${DESTDIR}${PREFIX}/share/man/man1/${PKGNAME}.1" sway-contrib-1.10.1/grimpicker/completion.bash000066400000000000000000000005771475063326300214010ustar00rootroot00000000000000_grimpicker() { local cur="${COMP_WORDS[COMP_CWORD]}" short=(-p -d -e -c -n -h -v) long=(--print --draw --escape --copy --notify --help --version) if [[ $cur == --* ]]; then COMPREPLY=($(compgen -W "${long[*]}" -- "$cur")) else COMPREPLY=($(compgen -W "${short[*]}" -- "$cur")) COMPREPLY+=($(compgen -W "${long[*]}" -- "$cur")) fi } complete -F _grimpicker grimpicker sway-contrib-1.10.1/grimpicker/completion.fish000066400000000000000000000007411475063326300214060ustar00rootroot00000000000000complete -c grimpicker -f complete -c grimpicker -s p -l print -d "Print to stdout" complete -c grimpicker -s d -l draw -d "Draw a colored block" complete -c grimpicker -s e -l escape -d "Print shell escape sequences" complete -c grimpicker -s c -l copy -d "Copy to clipboard" complete -c grimpicker -s n -l notify -d "Send a notification" complete -c grimpicker -s h -l help -d "Show help message and quit" complete -c grimpicker -s v -l version -d "Show version number and quit" sway-contrib-1.10.1/grimpicker/completion.zsh000066400000000000000000000005131475063326300212560ustar00rootroot00000000000000#compdef grimpicker _arguments -s \ {-d,--print}'[Print to stdout]' \ {-d,--draw}'[Draw a colored block]' \ {-e,--escape}'[Print shell escape sequences]' \ {-c,--copy}'[Copy to clipboard]' \ {-n,--notify}'[Send a notification]' \ {-h,--help}'[Show help message and quit]' \ {-v,--version}'[Show version number and exit]' \ sway-contrib-1.10.1/grimpicker/grimpicker000077500000000000000000000110161475063326300204410ustar00rootroot00000000000000#!/usr/bin/env python3 ''' grimpicker: a simple color picker for wlroots Dependencies: `slurp`: utility to select a region `grim`: utility to make screenshots Recommendations: `wl-copy`: clipboard utility `notify-send`: desktop notifications sender ''' __pkgname__ = 'grimpicker' __version__ = '1.0.0' import argparse import subprocess import sys class Color: escape = b'\x1b' reset_fg = escape + b'[39m' reset_bg = escape + b'[49m' reset_all = escape + b'[0m' escape_str = '\\e' reset_fg_str = escape_str + '[39m' reset_bg_str = escape_str + '[49m' reset_all_str = escape_str + '[0m' def __init__(self, r: int, g: int, b: int): (self.r, self.g, self.b) = (r, g, b) @classmethod def decode_ppm_pixel(cls, ppm: bytes): ppm_lines = ppm.splitlines() scales = ppm_lines[1].split(b' ') scale = int(scales[0]) if not scale == int(scales[1]): raise ValueError("Unknown output scaling used") if not (len(ppm_lines) == 4 and ppm_lines[0] == b'P6' and ppm_lines[2] == b'255' and len(ppm_lines[3]) == scale * scale * 3): raise ValueError('only 1x1 pixel ppm P6 format without comments is supported, no HDR') #if we are dealing with multiple pixels, average them. r = 0 g = 0 b = 0 for s in range(0, scale * scale): r += ppm_lines[3][s * 3 + 0] g += ppm_lines[3][s * 3 + 1] b += ppm_lines[3][s * 3 + 2] r /= scale * scale g /= scale * scale b /= scale * scale return cls(int(r), int(g), int(b)) def to_hex(self) -> str: return '#{:0>2X}{:0>2X}{:0>2X}'.format(self.r, self.g, self.b) def to_escape_fg(self) -> bytes: return b'%b[38;2;%d;%d;%dm' % (self.escape, self.r, self.g, self.b) def to_escape_bg(self) -> bytes: return b'%b[48;2;%d;%d;%dm' % (self.escape, self.r, self.g, self.b) def to_escape_fg_str(self) -> str: return '{}[38;2;{};{};{}m'.format(self.escape_str, self.r, self.g, self.b) def to_escape_bg_str(self) -> str: return '{}[48;2;{};{};{}m'.format(self.escape_str, self.r, self.g, self.b) def run(args) -> None: slurp = subprocess.check_output(('slurp', '-p')) grim = subprocess.check_output(('grim', '-g', '-', '-t', 'ppm', '-'), input=slurp) color = Color.decode_ppm_pixel(grim) if not (args.print or args.draw or args.escape or args.copy or args.notify): args.print = True args.draw = True if args.print: print(color.to_hex()) if args.draw: sys.stdout.buffer.write(color.to_escape_bg() + b' ' * 7 + color.reset_bg + b'\n') if args.escape: sys.stdout.buffer.write( b'Truecolor terminal shell escape sequences:\n' + b'%bTo change foreground:%b ' % (color.to_escape_fg(), color.reset_fg) + b'echo -e "%b", to reset: ' % color.to_escape_fg_str().encode() + b'echo -e "%b"\n' % color.reset_fg_str.encode() + b'%bTo change background:%b ' % (color.to_escape_bg(), color.reset_bg) + b'echo -e "%b", to reset: ' % color.to_escape_bg_str().encode() + b'echo -e "%b"\n' % color.reset_bg_str.encode() + b'To reset all attributes: echo -e "%b"\n' % color.reset_all_str.encode() ) if args.copy: subprocess.run(('wl-copy', color.to_hex()), check=True) if args.notify: subprocess.run(('notify-send', color.to_hex()), check=True) def parse_args() -> argparse.Namespace: usage = '{} [OPTIONS]'.format(__pkgname__) version = '{} {}'.format(__pkgname__, __version__) epilog = 'See `man 1 grimpicker` for further details' parser = argparse.ArgumentParser(usage=usage, add_help=False, epilog=epilog) parser.add_argument('-p', '--print', dest='print', action='store_true', help='Print to stdout') parser.add_argument('-d', '--draw', dest='draw', action='store_true', help='Draw a colored block') parser.add_argument('-e', '--escape', dest='escape', action='store_true', help='Print shell escape sequences') parser.add_argument('-c', '--copy', dest='copy', action='store_true', help='Copy to clipboard') parser.add_argument('-n', '--notify', dest='notify', action='store_true', help='Send a notification') parser.add_argument('-h', '--help', action='help', help='Show help message and quit') parser.add_argument('-v', '--version', action='version', version=version, help='Show version number and quit') return parser.parse_args() if __name__ == '__main__': run(parse_args()) sway-contrib-1.10.1/grimpicker/grimpicker.1.scd000066400000000000000000000024641475063326300213540ustar00rootroot00000000000000GRIMPICKER(1) # NAME grimpicker - a simple color picker for wlroots # SYNOPSIS *grimpicker* [_OPTIONS_] # OPTIONS *-p*, *--print* Print to stdout *-d*, *--draw* Draw a colored block *-e*, *--escape* Print shell escape sequences *-c*, *--copy* Copy to clipboard *-n*, *--notify* Send a notification *-h*, *--help* Show help message and quit *-v*, *--version* Show version number and quit # DESCRIPTION *grimpicker* is a color picker that uses *slurp* and *grim*. These programs rely on _zwlr_layer_shell_v1_ and _wlr-screencopy-unstable-v1_ (maybe be replaced with _ext-image-capture-source-v1_ and _ext-image-copy-capture-v1_ in the future) wayland protocols (implemented in wlroots-based compositors, e.g. *sway*). It has several output options, they can be combined. _--copy_ needs *wl-clipboard* to be installed. _--draw_ and _--escape_ need a terminal with truecolor support (e.g. *foot*). _--notify_ needs *libnotify* to be installed and a notification daemon (e.g. *mako* or *fnott*) to be running. _--print_ and _--draw_ are selected by default if no arguments are provided. # EXAMPLES An example usage pattern is to add this binding to your sway config: ``` # Super+Print: color picker bindsym --to-code $mod+Print exec grimpicker --notify ``` # SEE ALSO *slurp*(1), *grim*(1), *grimshot*(1) sway-contrib-1.10.1/grimshot/000077500000000000000000000000001475063326300160605ustar00rootroot00000000000000sway-contrib-1.10.1/grimshot/functional-helpers000077500000000000000000000007711475063326300216150ustar00rootroot00000000000000#! /bin/sh when() { condition=$1 action=$2 if eval "$condition"; then eval "$action" fi } whenOtherwise() { condition=$1 true_action=$2 false_action=$3 if eval "$condition"; then eval "$true_action" else eval "$false_action" fi } any() { for tuple in "$@"; do condition=$(echo "$tuple" | cut -d: -f1) action=$(echo "$tuple" | cut -d: -f2-) if eval "$condition"; then eval "$action" return 0 fi done return 1 # No conditions matched } sway-contrib-1.10.1/grimshot/grimshot000077500000000000000000000165671475063326300176610ustar00rootroot00000000000000#!/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. when() { condition=$1 action=$2 if eval "$condition"; then eval "$action" fi } whenOtherwise() { condition=$1 true_action=$2 false_action=$3 if eval "$condition"; then eval "$true_action" else eval "$false_action" fi } any() { for tuple in "$@"; do condition=$(echo "$tuple" | cut -d: -f1) action=$(echo "$tuple" | cut -d: -f2-) if eval "$condition"; then eval "$action" return 0 fi done return 1 # No conditions matched } NOTIFY=no CURSOR= WAIT=no 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}}" } parseArgs() { POSITIONAL_ARGS="" while [ $# -gt 0 ]; do case "$1" in -n | --notify) NOTIFY=yes shift ;; -c | --cursor) CURSOR=yes shift ;; -w | --wait) shift WAIT="$1" if echo "$WAIT" | grep "[^0-9]" -q; then echo "invalid value for wait '$WAIT'" >&2 exit 3 fi shift ;; *) # Treat anything else as a positional argument POSITIONAL_ARGS="$POSITIONAL_ARGS $1" # Add positional argument to the string shift ;; esac done set -- $POSITIONAL_ARGS # Re-assign positional arguments ACTION=${1:-usage} SUBJECT=${2:-screen} FILE=${3:-$(getTargetDirectory)/$(date -Ins).png} } printUsageMsg() { 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 } notify() { notify-send -t 3000 -a grimshot "$@" } notifyOk() { notify_disabled='[ "$NOTIFY" = "no" ]' action_involves_saving='[ "$ACTION" = "save" ] || [ "$ACTION" = "savecopy" ]' if eval $notify_disabled; then return fi TITLE=${2:-"Screenshot"} MESSAGE=${1:-"OK"} whenOtherwise "$action_involves_saving" \ 'notify "$TITLE" "$MESSAGE" -i "$FILE"' \ 'notify "$TITLE" "$MESSAGE"' } notifyError() { notify_enabled='[ "$NOTIFY" = "yes" ]' TITLE=${2:-"Screenshot"} errorMssg=$1 MESSAGE=${errorMssg:-"Error taking screenshot with grim"} whenOtherwise "$notify_enabled" \ 'notify "$TITLE" "$MESSAGE" -u critical' \ 'echo "$errorMssg"' } die() { MSG=${1:-Bye} notifyError "Error: $MSG" exit 2 } check() { COMMAND=$1 command_exists='command -v "$COMMAND" > /dev/null 2>&1' whenOtherwise "$command_exists" \ 'RESULT="OK"' \ 'RESULT="NOT FOUND"' echo " $COMMAND: $RESULT" } takeScreenshot() { FILE=$1 GEOM=$2 OUTPUT=$3 output_provided='[ -n "$OUTPUT" ]' geom_not_provided='[ -z "$GEOM" ]' output_action='grim ${CURSOR:+-c} -o "$OUTPUT" "$FILE" || die "Unable to invoke grim"' full_screenshot_action='grim ${CURSOR:+-c} "$FILE" || die "Unable to invoke grim"' geometry_screenshot_action='grim ${CURSOR:+-c} -g "$GEOM" "$FILE" || die "Unable to invoke grim"' any \ "$output_provided:$output_action" \ "$geom_not_provided:$full_screenshot_action" \ "true:$geometry_screenshot_action" } checkRequiredTools() { 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 } selectArea() { GEOM=$(slurp -d) geomIsEmpty='[ -z "$GEOM" ]' when "$geomIsEmpty" "exit 1" WHAT="Area" } selectActiveWindow() { 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" } selectScreen() { GEOM="" WHAT="Screen" } selectOutput() { GEOM="" OUTPUT=$(swaymsg -t get_outputs | jq -r '.[] | select(.focused)' | jq -r '.name') WHAT="$OUTPUT" } selectWindow() { GEOM=$(swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"' | slurp -r) geomIsEmpty='[ -z "$GEOM" ]' when "$geomIsEmpty" "exit 1" WHAT="Window" } selectAnything() { GEOM=$(swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"' | slurp -o) geomIsEmpty='[ -z "$GEOM" ]' when "$geomIsEmpty" "exit 1" WHAT="Selection" } handleSaveCopy() { wl-copy --type image/png <"$FILE" || die "Clipboard error" MESSAGE="$MESSAGE and clipboard" } handleScreenshotSuccess() { TITLE="Screenshot of $SUBJECT" MESSAGE=$(basename "$FILE") isSaveCopy='[ "$ACTION" = "savecopy" ]' when "$isSaveCopy" "handleSaveCopy" notifyOk "$MESSAGE" "$TITLE" echo "$FILE" } handleScreenshotFailure() { notifyError "Error taking screenshot with grim" } handleCopy() { takeScreenshot - "$GEOM" "$OUTPUT" | wl-copy --type image/png || die "Clipboard error" notifyOk "$WHAT copied to clipboard" } handleSave() { screenshotTaken="takeScreenshot \"$FILE\" \"$GEOM\" \"$OUTPUT\"" whenOtherwise "$screenshotTaken" \ "handleScreenshotSuccess" \ "handleScreenshotFailure" } handleUnknownSubject() { die "Unknown subject to take a screenshot from" "$SUBJECT" } handleScreenshot() { actionIsInvalid='[ "$ACTION" != "save" ] && [ "$ACTION" != "copy" ] && [ "$ACTION" != "savecopy" ] && [ "$ACTION" != "check" ]' actionIsCheck='[ "$ACTION" = "check" ]' subjectIsArea='[ "$SUBJECT" = "area" ]' subjectIsActiveWindow='[ "$SUBJECT" = "active" ]' subjectIsScreen='[ "$SUBJECT" = "screen" ]' subjectIsOutput='[ "$SUBJECT" = "output" ]' subjectIsWindow='[ "$SUBJECT" = "window" ]' subjectIsAnything='[ "$SUBJECT" = "anything" ]' subjectIsUnknown=true any \ "$actionIsInvalid:printUsageMsg" \ "$actionIsCheck:checkRequiredTools" \ "$subjectIsArea:selectArea" \ "$subjectIsActiveWindow:selectActiveWindow" \ "$subjectIsScreen:selectScreen" \ "$subjectIsOutput:selectOutput" \ "$subjectIsWindow:selectWindow" \ "$subjectIsAnything:selectAnything" \ "$subjectIsUnknown:handleUnknownSubject" wait='[ "$WAIT" != "no" ]' when "$wait" "sleep $WAIT" actionIsCopy='[ "$ACTION" = "copy" ]' whenOtherwise "$actionIsCopy" \ "handleCopy" \ "handleSave" } parseArgs "$@" handleScreenshot sway-contrib-1.10.1/grimshot/grimshot-completion.bash000066400000000000000000000020301475063326300227150ustar00rootroot00000000000000#!/bin/bash # To make use of this script simply add: source path/to/grimshot-completion.bash # to your .bashrc. _grimshot_bash_comp() { local req_target="copy save savecopy" local first_char=$(cut -c -1 <<< "${COMP_WORDS[1]}") local target_pos=3 local cmd_index=1 local cmd_pos=2 if [[ $first_char == "-" ]]; then target_pos=4 cmd_index=2 cmd_pos=3 fi # Complete options if [[ $first_char == "-" && ${#COMP_WORDS[@]} -eq 2 ]]; then COMPREPLY=( $(compgen -W "--notify --cursor --wait" -- "${COMP_WORDS[COMP_CWORD]}") ) # Complete commands elif [[ ${#COMP_WORDS[@]} -eq $cmd_pos ]]; then COMPREPLY=( $(compgen -W "check usage $req_target" -- "${COMP_WORDS[COMP_CWORD]}") ) # Complete targets elif [[ $req_target =~ "${COMP_WORDS[$cmd_index]}" && ${#COMP_WORDS[@]} -eq $target_pos ]]; then COMPREPLY=( $(compgen -W "active screen output area window anything" -- "${COMP_WORDS[COMP_CWORD]}") ) fi } complete -F _grimshot_bash_comp grimshot sway-contrib-1.10.1/grimshot/grimshot.1000066400000000000000000000055601475063326300200040ustar00rootroot00000000000000.\" 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.10.1/grimshot/grimshot.1.scd000066400000000000000000000045231475063326300205520ustar00rootroot00000000000000grimshot(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.10.1/inactive-windows-transparency.py000077500000000000000000000047261475063326300226130ustar00rootroot00000000000000#!/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(args, ipc, event): global focused_set # To get the workspace for a container, we need to have received its # parents, so fetch the whole tree tree = ipc.get_tree() focused = tree.find_focused() focused_workspace = focused.workspace() focused.command("opacity " + args.focused) focused_set.add(focused.id) to_remove = set() for window_id in focused_set: if window_id == focused.id: continue window = tree.find_by_id(window_id) if window is None: to_remove.add(window_id) elif args.global_focus or window.workspace() == focused_workspace: window.command("opacity " + args.opacity) to_remove.add(window_id) focused_set -= to_remove def remove_opacity(ipc, focused_opacity): for workspace in ipc.get_tree().workspaces(): for w in workspace: w.command("opacity " + focused_opacity) ipc.main_quit() sys.exit(0) if __name__ == "__main__": 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="0.80", help="set inactive opacity value in range 0...1", ) parser.add_argument( "--focused", "-f", type=str, default="1.0", help="set focused opacity value in range 0...1", ) parser.add_argument( "--global-focus", "-g", action="store_true", help="only have one opaque window across all workspaces", ) args = parser.parse_args() ipc = i3ipc.Connection() focused_set = set() for window in ipc.get_tree(): if window.focused: focused_set.add(window.id) window.command("opacity " + args.focused) else: window.command("opacity " + args.opacity) for sig in [signal.SIGINT, signal.SIGTERM]: signal.signal(sig, lambda signal, frame: remove_opacity(ipc, args.focused)) ipc.on("window", partial(on_window, args)) ipc.main() sway-contrib-1.10.1/layout-per-window.py000077500000000000000000000045221475063326300202120ustar00rootroot00000000000000#!/usr/bin/env python # This script keeps track of the active layout for each window. # Optional argument defines numeric layout index for new windows (counted from 0) # # This script requires i3ipc-python package (install it from a system package # manager or pip). import sys from typing import Optional import i3ipc def on_window_focus(ipc: i3ipc.connection.Connection, event: i3ipc.events.WindowEvent): global windows, prev_focused, default_layout # Get current layouts layouts = { input.identifier: input.xkb_active_layout_index for input in ipc.get_inputs() } # Save layouts for previous window windows[prev_focused] = layouts # Restore layout of the newly focused known 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 # Set default layout for a fresh window elif default_layout is not None: for kdb_id, layout_index in layouts.items(): if layout_index is not None and layout_index != default_layout: ipc.command(f'input "{kdb_id}" xkb_switch_layout {default_layout}') 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__": default_layout: Optional[int] = None if len(sys.argv) == 2: if sys.argv[1].isnumeric(): default_layout = int(sys.argv[1]) else: print(f"Expected an integer, got: {sys.argv[1]}", file=sys.stderr) sys.exit(2) elif len(sys.argv) > 2: print("Too many arguments", file=sys.stderr) sys.exit(2) ipc = i3ipc.Connection() focused = ipc.get_tree().find_focused() if focused: prev_focused = focused.id else: prev_focused = None windows: dict = {} ipc.on("window", on_window) ipc.main() sway-contrib-1.10.1/pyproject.toml000066400000000000000000000002421475063326300171360ustar00rootroot00000000000000[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.10.1/requirements-dev.txt000066400000000000000000000000111475063326300202540ustar00rootroot00000000000000ruff mypysway-contrib-1.10.1/requirements.txt000066400000000000000000000000061475063326300175040ustar00rootroot00000000000000i3ipc sway-contrib-1.10.1/sway-session.target000066400000000000000000000002541475063326300201010ustar00rootroot00000000000000[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.10.1/switch-top-level.py000077500000000000000000000060701475063326300200120ustar00rootroot00000000000000#!/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()