diff --git a/files/environment/.config/environment.d/envvars.conf b/files/environment/.config/environment.d/envvars.conf
index 92a6827..395d330 100644
--- a/files/environment/.config/environment.d/envvars.conf
+++ b/files/environment/.config/environment.d/envvars.conf
@@ -2,8 +2,13 @@ PATH=$HOME/.dots/scripts:$HOME/.cargo/bin:$HOME/.ghcup/bin:$HOME/.local/bin:$HOM
EDITOR=hx
VISUAL=hx
+SSH_AUTH_SOCK=/run/user/1000/keyring/ssh
+
# XXX: render shadow on wayland
-KITTY_DISABLE_WAYLAND=1
+# KITTY_DISABLE_WAYLAND=1
+# KITTY_ENABLE_WAYLAND=1
+
+_JAVA_AWT_WM_NONREPARENTING=1
# support jp input in gnome
GTK_IM_MODULE=ibus
diff --git a/files/sway/.config/sway/config b/files/sway/.config/sway/config
new file mode 100644
index 0000000..f4e7ccb
--- /dev/null
+++ b/files/sway/.config/sway/config
@@ -0,0 +1,146 @@
+### Variables
+ set $mod Mod4
+ floating_modifier $mod normal
+
+### Output configuration (swaymsg -t get_output)
+ output * bg ~/cloud/images/wallpaper/wallpaper.png fill
+ # output eDP-1 scale 2
+
+ # disable laptop when closed
+ set $laptop eDP-1
+ bindswitch --reload --locked lid:on output $laptop disable
+ bindswitch --reload --locked lid:off output $laptop enable
+
+ workspace_layout tabbed
+ smart_borders on
+ # default_border none
+
+ exec swayidle -w \
+ timeout 300 'swaylock -i ~/cloud/images/wallpaper/lock.png -s center -u' \
+ timeout 600 'systemctl suspend' \
+ before-sleep 'swaylock -i ~/cloud/images/wallpaper/lock.png -s center -u'
+
+### Input configuration
+
+input "type:keyboard" {
+ xkb_layout eu
+ xkb_options caps:swapescape
+ repeat_delay 200
+ repeat_rate 100
+}
+
+bindgesture swipe:right workspace prev
+bindgesture swipe:left workspace next
+
+### Key bindings
+
+# System
+ bindsym --locked XF86MonBrightnessDown exec brightnessctl set 5%-
+ bindsym --locked XF86MonBrightnessUp exec brightnessctl set 5%+
+
+ bindsym --locked XF86AudioMute exec wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
+ bindsym --locked XF86AudioLowerVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- -l 1.2
+ bindsym --locked XF86AudioRaiseVolume exec wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ -l 1.2
+
+ bindsym $mod+Control+BackSpace exec swaylock -i ~/cloud/images/wallpaper/lock.png -s center -u
+ bindsym $mod+Control+Shift+BackSpace exec systemctl suspend
+
+ bindsym $mod+Shift+s exec ~/.config/sway/screenshot -s region
+
+ bindsym $mod+Shift+c reload
+ bindsym $mod+Shift+e exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -B 'Yes, exit sway' 'swaymsg exit'
+ bindsym $mod+Shift+q kill
+
+# Run applications
+ bindsym $mod+Return exec kitty
+ bindsym $mod+d exec wofi --show drun -I -i -a
+ bindsym $mod+Shift+d exec wofi --show run -i -a
+ bindsym $mod+e exec nautilus
+
+# Moving around
+ set $left h
+ set $down j
+ set $up k
+ set $right l
+ # Move your focus around
+ bindsym $mod+$left focus left
+ bindsym $mod+$down focus down
+ bindsym $mod+$up focus up
+ bindsym $mod+$right focus right
+
+ # Move the focused window with the same, but add Shift
+ bindsym $mod+Shift+$left move left
+ bindsym $mod+Shift+$down move down
+ bindsym $mod+Shift+$up move up
+ bindsym $mod+Shift+$right move right
+
+# Workspaces
+ # Switch to workspace
+ bindsym $mod+1 workspace number 1
+ bindsym $mod+2 workspace number 2
+ bindsym $mod+3 workspace number 3
+ bindsym $mod+4 workspace number 4
+ bindsym $mod+5 workspace number 5
+ bindsym $mod+6 workspace number 6
+ bindsym $mod+7 workspace number 7
+ bindsym $mod+8 workspace number 8
+ bindsym $mod+9 workspace number 9
+ bindsym $mod+0 workspace number 10
+ # Move focused container to workspace
+ bindsym $mod+Shift+1 move container to workspace number 1
+ bindsym $mod+Shift+2 move container to workspace number 2
+ bindsym $mod+Shift+3 move container to workspace number 3
+ bindsym $mod+Shift+4 move container to workspace number 4
+ bindsym $mod+Shift+5 move container to workspace number 5
+ bindsym $mod+Shift+6 move container to workspace number 6
+ bindsym $mod+Shift+7 move container to workspace number 7
+ bindsym $mod+Shift+8 move container to workspace number 8
+ bindsym $mod+Shift+9 move container to workspace number 9
+ bindsym $mod+Shift+0 move container to workspace number 10
+
+# Layout stuff
+ bindsym $mod+f fullscreen
+ bindsym $mod+Shift+space floating toggle
+ bindsym $mod+space focus mode_toggle
+ bindsym $mod+a focus parent
+
+# Scratchpad
+ bindsym $mod+Shift+minus move scratchpad
+ bindsym $mod+minus scratchpad show
+
+# Theming
+font pango: JetBrainsMono Nerd Font Mono 10
+bar {
+ position top
+ mode dock
+ output *
+
+ # status_command while ~/.config/sway/status.sh; do sleep 1; done
+ swaybar_command waybar
+
+ colors {
+ statusline #5c6a72
+ background #fdf6e3
+ inactive_workspace #fdf6e3 #fdf6e3 #5c6a72
+ focused_workspace #8da101 #8da101 #fdf6e3
+ }
+}
+
+client.focused #A7C080 #A7C080 #2D353B #A7C080 #A7C080
+client.focused_inactive #3D484D #2D353B #859289 #3D484D #3D484D
+client.unfocused #3D484D #2D353B #859289 #3D484D #3D484D
+client.urgent #E67E80 #E67E80 #2D353B #E67E80 #E67E80
+client.placeholder #2D353B #2D353B #859289 #2D353B #2D353B
+client.background #2D353B
+
+include /etc/sway/config.d/*
+
+# Autostart
+exec --no-startup-id netbird-ui
+exec --no-startup-id nm-applet
+exec --no-startup-id keepassxc
+exec --no-startup-id nextcloud
+
+# SSH
+exec eval $(gnome-keyring-daemon --start)
+exec export SSH_AUTH_SOCK
diff --git a/files/sway/.config/sway/screenshot b/files/sway/.config/sway/screenshot
new file mode 100755
index 0000000..c58ae0c
--- /dev/null
+++ b/files/sway/.config/sway/screenshot
@@ -0,0 +1,923 @@
+#!/usr/bin/env python3
+
+import abc
+import argparse
+import os
+import json
+import signal
+import sys
+import subprocess
+from datetime import datetime
+from functools import partial
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ NamedTuple,
+ Union,
+ Optional,
+ Set,
+ Tuple,
+ Type,
+)
+from pathlib import Path
+
+try:
+ import tomllib
+except ModuleNotFoundError:
+ import tomli as tomllib
+
+
+class ValidationError(NamedTuple):
+ path: str
+ error: str
+
+
+CONFIG_VALIDATOR = Callable[[str, Any], Iterator[ValidationError]]
+
+
+def config_dict_validator(
+ field_validators: Dict[str, CONFIG_VALIDATOR]
+) -> CONFIG_VALIDATOR:
+ def validate(path: str, value: Any) -> Iterator[ValidationError]:
+ if not isinstance(value, Dict):
+ yield ValidationError(path, "Should be a dictionary.")
+ return
+
+ unknown_fields = set(value).difference(field_validators)
+ for unknown_field in unknown_fields:
+ yield ValidationError(f"{path}.{unknown_field}", "Field does not exist.")
+
+ for field_name, field_value in value.items():
+ if field_name not in field_validators:
+ continue
+
+ yield from field_validators[field_name](f"{path}.{field_name}", field_value)
+
+ return validate
+
+
+def config_type_validator(*types: Type) -> CONFIG_VALIDATOR:
+ def validate(path: str, value: Any) -> Iterator[ValidationError]:
+ if not isinstance(value, types):
+ yield ValidationError(
+ path, f"Type {type(value)} is not one of {', '.join(types)}."
+ )
+
+ return validate
+
+
+def config_enum_validator(values: Set[Any]) -> CONFIG_VALIDATOR:
+ def validate(path: str, value: Any) -> Iterator[ValidationError]:
+ if value not in values:
+ yield ValidationError(path, f"Value '{value}' not one of {values}.")
+
+ return validate
+
+def config_list_validator(*types: Type) -> CONFIG_VALIDATOR:
+ def validate(path: str, value: Any) -> Iterator[ValidationError]:
+ if not isinstance(value, list):
+ yield ValidationError(path, f"Expected a list")
+ return
+
+ for i, element in enumerate(value):
+ if not isinstance(element, types):
+ yield ValidationError(
+ f"{path}[{i}]", f"Type {type(value)} is not one of {', '.join(types)}."
+ )
+
+ return validate
+
+
+class ConfigError(Exception):
+ def __init__(self, errors: List[ValidationError]) -> None:
+ super().__init__(
+ "Config errors: \n"
+ + "\n".join(f" {error.path}: {error.error}" for error in errors)
+ )
+ self.errors = errors
+
+
+class Config:
+ CONFIG_DEFAULT = {
+ "screenshot": {
+ "prompt": "📷> ",
+ "file_name": "screenshots/screenshot_%Y-%m-%dT%H:%M:%S.png",
+ },
+ "screencast": {
+ "prompt": "📹> ",
+ "pid_file": "${XDG_RUNTIME_DIR}/sway-interactive-screenshot.${WAYLAND_DISPLAY}.video.pid",
+ "file_name": "screencast_%Y-%m-%dT%H:%M:%S.mkv",
+ "audio": "ask",
+ },
+ "notification_actions": {
+ "dragon": {
+ "command": "dragon-drop",
+ }
+ },
+ "ask" : {
+ "command" : "rofi",
+ "args": [],
+ }
+ }
+
+ _validate = staticmethod(
+ config_dict_validator(
+ {
+ "screenshot": config_dict_validator(
+ {
+ "prompt": config_type_validator(str),
+ "save_dir": config_type_validator(str),
+ "file_name": config_type_validator(str),
+ "type": config_enum_validator({"png", "jpeg", "ppm"}),
+ "jpeg_quality": config_type_validator(int),
+ "png_level": config_type_validator(int),
+ "cursor": config_type_validator(bool),
+ }
+ ),
+ "screencast": config_dict_validator(
+ {
+ "prompt": config_type_validator(str),
+ "save_dir": config_type_validator(str),
+ "pid_file": config_type_validator(str),
+ "file_name": config_type_validator(str),
+ "audio": config_enum_validator({"yes", "no", "ask"}),
+ }
+ ),
+ "notification_actions": config_dict_validator(
+ {
+ "dragon": config_dict_validator(
+ {
+ "command": config_type_validator(str),
+ }
+ )
+ }
+ ),
+ "ask" : config_dict_validator(
+ {
+ "command" : config_type_validator(str),
+ "args" : config_list_validator(str),
+ }
+ ),
+ }
+ )
+ )
+
+ config: Dict[str, Any]
+
+ def __init__(self, filepath: Union[Path, str, None]) -> None:
+ self._load(filepath)
+ errors = list(self._validate("", self.config))
+ if errors:
+ raise ConfigError(errors)
+
+ def get(self, *path: str) -> Any:
+ value: Any = self.config
+ for item in path:
+ if item not in value:
+ default: Any = self.CONFIG_DEFAULT
+ for item in path:
+ default = default.get(item)
+ if default is None:
+ return None
+
+ return default
+
+ value = value.get(item)
+
+ return value
+
+ def _load(self, filepath: Union[Path, str, None]) -> None:
+ if filepath is None:
+ xdg_config_home = os.getenv("XDG_CONFIG_HOME", "~/.config")
+ filepath = Path(xdg_config_home) / "sway-interactive-screenshot/config.toml"
+
+ filepath = Path(filepath).expanduser()
+
+ if filepath.exists():
+ with open(filepath, "rb") as f:
+ self.config = tomllib.load(f)
+
+ else:
+ self.config = {}
+
+
+class CanceledError(Exception):
+ def __init__(self, msg: Optional[str] = None) -> None:
+ super().__init__(msg)
+ self.msg = msg
+
+
+class Area(NamedTuple):
+ output: Optional[str] = None
+ geometry: Optional[str] = None
+
+
+class Window(NamedTuple):
+ name: str
+ x: int
+ y: int
+ width: int
+ height: int
+ focused: bool
+
+ def __str__(self) -> str:
+ return f"window: {self.name}"
+
+ def get_geometry_str(self) -> str:
+ return f"{self.x},{self.y} {self.width}x{self.height}"
+
+ def get_area(self) -> Area:
+ return Area(geometry=self.get_geometry_str())
+
+
+class AllOutputs:
+ selection_name = "all-outputs"
+
+ def __str__(self) -> str:
+ return "all outputs"
+
+ def get_area(self) -> Area:
+ return Area()
+
+
+class Region:
+ selection_name = "region"
+
+ def __str__(self) -> str:
+ return "region"
+
+ def get_area(self) -> Area:
+ geometry = ask_geometry()
+ if geometry is None:
+ raise CanceledError("No region selected.")
+
+ return Area(geometry=geometry)
+
+
+class FocusedWindow:
+ selection_name = "focused-window"
+
+ def __str__(self) -> str:
+ return "focused window"
+
+ def get_area(self) -> Area:
+ window = next(
+ (window for window in get_windows() if window.focused),
+ None,
+ )
+ if window is None:
+ raise CanceledError("Could not find any focused window.")
+
+ return window.get_area()
+
+
+class SelectWindow:
+ selection_name = "select-window"
+
+ def __str__(self) -> str:
+ return "select window"
+
+ def get_area(self) -> Area:
+ windows = list(get_windows())
+ if not windows:
+ raise CanceledError("No visible window found.")
+
+ geometry = ask_geometry(window.get_geometry_str() for window in windows)
+ if geometry is None:
+ raise CanceledError("No window selected.")
+
+ return Area(geometry=geometry)
+
+
+class FocusedOutput:
+ selection_name = "focused-output"
+
+ def __str__(self) -> str:
+ return "focused output"
+
+ def get_area(self) -> Area:
+ output = next(
+ (output for output in get_outputs() if output.focused),
+ None,
+ )
+ if output is None:
+ raise CanceledError("Could not find any focused output.")
+
+ return output.get_area()
+
+
+class Output(NamedTuple):
+ name: str
+ model: Optional[str]
+ focused: bool
+
+ def __str__(self) -> str:
+ model = f" ({self.model})" if self.model else ""
+ focused = " (focused)" if self.focused else ""
+ return f"output: {self.name}{model}{focused}"
+
+ def get_area(self) -> Area:
+ return Area(output=self.name)
+
+
+class SelectOutput:
+ selection_name = "select-output"
+
+ def __str__(self) -> str:
+ return "select output"
+
+ def get_area(self) -> Area:
+ output = ask_output()
+ if output is None:
+ raise CanceledError("No output selected.")
+
+ return Area(output=output)
+
+
+def get_windows() -> Iterator[Window]:
+ def walk(node: Dict[str, Any]) -> Iterator[Window]:
+ sub_nodes = node.get("floating_nodes")
+ if sub_nodes:
+ for sub_node in sub_nodes:
+ yield from walk(sub_node)
+ sub_nodes = node.get("nodes")
+ if sub_nodes:
+ for sub_node in sub_nodes:
+ yield from walk(sub_node)
+ elif node.get("visible") and node.get("pid"):
+ rect = node["rect"]
+ yield Window(
+ name=node["name"],
+ x=rect["x"],
+ y=rect["y"],
+ width=rect["width"],
+ height=rect["height"],
+ focused=node["focused"],
+ )
+
+ process = subprocess.run(
+ ["swaymsg", "-t", "get_tree"],
+ capture_output=True,
+ check=True,
+ )
+ tree = json.loads(process.stdout.decode())
+ return walk(tree)
+
+
+def get_outputs() -> Iterator[Output]:
+ process = subprocess.run(
+ ["swaymsg", "-t", "get_outputs"],
+ capture_output=True,
+ check=True,
+ )
+ for output in json.loads(process.stdout.decode()):
+ if output["active"]:
+ yield Output(
+ name=output["name"],
+ model=output["model"],
+ focused=output["focused"],
+ )
+
+
+def ask_rofi(
+ choices: List[Any],
+ *,
+ prompt: Optional[str] = None,
+ lines: Optional[int] = None,
+ index: bool = False,
+ additional_args: Optional[List[str]] = None
+) -> Union[int, str, None]:
+ args = ["rofi", "-dmenu"]
+
+ if index:
+ args.extend(("-format","i"))
+
+ if prompt is not None:
+ args.extend(("-p", prompt))
+
+ if lines is not None:
+ args.extend(("-l", str(lines)))
+
+ if additional_args:
+ args.extend(additional_args)
+
+ process = subprocess.run(
+ args,
+ input=b"\n".join(str(choice).encode() for choice in choices),
+ capture_output=True,
+ check=False,
+ )
+
+ if process.returncode != 0:
+ return None
+
+ stdout = process.stdout.decode()
+
+ breakpoint()
+
+ return int(stdout) if index else stdout.strip()
+
+
+def ask_fuzzel(
+ choices: List[Any],
+ *,
+ prompt: Optional[str] = None,
+ lines: Optional[int] = None,
+ index: bool = False,
+ additional_args: Optional[List[str]] = None
+) -> Union[int, str, None]:
+ args = ["fuzzel", "--dmenu"]
+
+ if index:
+ args.append("--index")
+
+ if prompt is not None:
+ args.extend(("--prompt", prompt))
+
+ if lines is not None:
+ args.extend(("--lines", str(lines)))
+
+ if additional_args:
+ args.extend(additional_args)
+
+ process = subprocess.run(
+ args,
+ input=b"\n".join(str(choice).encode() for choice in choices),
+ capture_output=True,
+ check=False,
+ )
+
+ if process.returncode != 0:
+ return None
+
+ stdout = process.stdout.decode()
+
+ return int(stdout) if index else stdout.strip()
+
+
+ASK_FNS = {
+ "rofi" : ask_rofi,
+ "fuzzel" : ask_fuzzel
+}
+
+
+ask = ask_fuzzel
+
+def notify(
+ title: str,
+ summary: Optional[str] = None,
+ *,
+ icon: Optional[str] = None,
+ actions: Optional[List[Tuple[str, str]]] = None,
+) -> Optional[str]:
+ args = ["notify-send"]
+
+ if icon is not None:
+ args.extend(("--icon", icon))
+
+ for action_name, action_desc in actions or ():
+ args.extend(("-A", f"{action_name}={action_desc}"))
+
+ args.extend(("--", title))
+ if summary:
+ args.append(summary)
+
+ process = subprocess.run(args, capture_output=True)
+ if process.returncode != 0:
+ print("Error while sending notification.", file=sys.stderr)
+ print(process.stderr.decode(), file=sys.stderr)
+
+ return process.stdout.decode().strip() or None
+
+
+def take_screenshot(
+ *,
+ filepath: str,
+ geometry: Optional[str] = None,
+ output: Optional[str] = None,
+ cursor: Optional[bool] = None,
+ type_: Optional[str] = None,
+ jpeg_quality: Optional[int] = None,
+ png_level: Optional[int] = None,
+) -> None:
+ args = ["grim"]
+
+ if geometry is not None:
+ args.extend(("-g", geometry))
+
+ if output is not None:
+ args.extend(("-o", output))
+
+ if cursor:
+ args.append("-c")
+
+ if type_ is not None:
+ args.extend(("-t", type_))
+
+ if png_level is not None and (type == "png" or filepath.endswith(".png")):
+ args.extend(("-l", str(png_level)))
+
+ if jpeg_quality is not None and (
+ type == "jpeg" or filepath.endswith(".jpg") or filepath.endswith(".jpeg")
+ ):
+ args.extend(("-q", str(jpeg_quality)))
+
+ args.append(filepath)
+ subprocess.run(args, check=True)
+
+
+def take_screencast(
+ *,
+ filepath: str,
+ geometry: Optional[str] = None,
+ output: Optional[str] = None,
+ audio: Union[bool, str, None] = None,
+ pid_file: Optional[Path] = None,
+) -> None:
+ args = ["wf-recorder", "-f", filepath]
+
+ if geometry is not None:
+ args.extend(("-g", geometry))
+
+ if output is not None:
+ args.extend(("-o", output))
+
+ if audio is not None:
+ if audio is True:
+ args.append("-a")
+ else:
+ args.extend(("-a", str(audio)))
+
+ with subprocess.Popen(args) as process:
+ try:
+ if pid_file:
+ pid_file.write_text(str(process.pid))
+ process.wait()
+ if process.returncode != 0:
+ raise Exception(
+ f"wf-recorder exited with status code {process.returncode}"
+ )
+ finally:
+ if pid_file:
+ pid_file.unlink(missing_ok=True)
+
+
+def ask_geometry(geometries: Optional[Iterable[str]] = None) -> Optional[str]:
+ if geometries is not None:
+ geomerties_input = b"\n".join(geometry.encode() for geometry in geometries)
+ else:
+ geomerties_input = None
+
+ process = subprocess.run(
+ ["slurp"],
+ capture_output=True,
+ input=geomerties_input,
+ check=False,
+ )
+ if process.returncode != 0:
+ return None
+
+ return process.stdout.decode().strip()
+
+
+def ask_output() -> Optional[str]:
+ process = subprocess.run(
+ ["slurp", "-o", "-f", "%o"], capture_output=True, check=False
+ )
+ if process.returncode != 0:
+ return None
+
+ return process.stdout.decode().strip()
+
+
+def edit_capture(filepath: str) -> None:
+ subprocess.run(["swappy", "-f", filepath, "-o", filepath], check=False)
+
+
+def copy_file_to_clipboard(filepath: str) -> None:
+ with open(filepath, "rb") as file:
+ with subprocess.Popen("wl-copy", stdin=file) as process:
+ process.wait()
+
+
+class NotificationAction(abc.ABC):
+ name: str
+ description: str
+
+ @classmethod
+ @abc.abstractmethod
+ def run(cls, *, filepath: Path, config_getter: Callable[[str], Any]) -> None:
+ pass
+
+
+class EditNotificationAction(NotificationAction):
+ name = "default"
+ description = "Edit"
+
+ @classmethod
+ def run(cls, *, filepath: Path, config_getter: Callable[[str], Any]) -> None:
+ filepath_str = str(filepath.expanduser())
+ edit_capture(filepath_str)
+ copy_file_to_clipboard(filepath_str)
+
+
+class DeleteNotificationAction(NotificationAction):
+ name = "delete"
+ description = "Delete"
+
+ @classmethod
+ def run(cls, *, filepath: Path, config_getter: Callable[[str], Any]) -> None:
+ filepath.expanduser().unlink()
+ notify("Deleted")
+
+
+class DragonNotificationAction(NotificationAction):
+ name = "dragon"
+ description = "Drag and drop"
+
+ @classmethod
+ def run(cls, *, filepath: Path, config_getter: Callable[[str], Any]) -> None:
+ subprocess.run([config_getter("command"), filepath.expanduser()], check=False)
+
+
+class OpenNotificationAction(NotificationAction):
+ name = "open"
+ description = "Open"
+
+ @classmethod
+ def run(cls, *, filepath: Path, config_getter: Callable[[str], Any]) -> None:
+ subprocess.run(["xdg-open", filepath.expanduser()], check=False)
+
+
+def parse_arguments():
+ argparser = argparse.ArgumentParser(
+ prog="sway-interactive-screenshot",
+ description="Interactively take screenshots with Sway.",
+ )
+ selections = (
+ Region,
+ FocusedOutput,
+ AllOutputs,
+ SelectOutput,
+ FocusedWindow,
+ SelectWindow,
+ )
+ argparser.add_argument(
+ "-s",
+ "--selection",
+ choices=[selection.selection_name for selection in selections],
+ help="Selection mode.",
+ )
+ argparser.add_argument(
+ "--save-dir",
+ help="Directory where screenshots are saved.",
+ )
+ argparser.add_argument(
+ "-o",
+ "--output",
+ help="Output file name. If set, --save-dir is ignored.",
+ )
+ argparser.add_argument(
+ "--video",
+ help="Make a screen video recording.",
+ action="store_true",
+ )
+ argparser.add_argument(
+ "-c",
+ "--config",
+ help="Config file.",
+ )
+
+ args = argparser.parse_args()
+ if args.selection is not None:
+ args.selection = next(
+ (
+ selection
+ for selection in selections
+ if selection.selection_name == args.selection
+ ),
+ None,
+ )
+ return args
+
+
+class CaptureMode(abc.ABC):
+ def __init__(self, config_getter: Callable[[str], Any]) -> None:
+ self.config_getter = config_getter
+
+ def get_prompt(self) -> str:
+ return self.config_getter("prompt")
+
+ def get_filename(self) -> str:
+ return datetime.now().strftime(self.config_getter("file_name"))
+
+ def get_save_dir(self) -> str:
+ return self.config_getter("save_dir")
+
+ @abc.abstractmethod
+ def get_display_name(self) -> str:
+ pass
+
+ def early_exit(self) -> bool:
+ return False
+
+ @abc.abstractmethod
+ def capture(self, *, area: Area, filepath: str) -> None:
+ pass
+
+ @abc.abstractmethod
+ def get_notification_actions(self) -> Iterable[Type[NotificationAction]]:
+ pass
+
+ def get_notification_icon(self, filepath: str) -> Optional[str]:
+ return None
+
+
+class Screenshot(CaptureMode):
+ def get_display_name(self) -> str:
+ return "Screenshot"
+
+ def capture(self, *, area: Area, filepath: str) -> None:
+ take_screenshot(
+ filepath=filepath,
+ geometry=area.geometry,
+ output=area.output,
+ cursor=self.config_getter("cursor"),
+ type_=self.config_getter("type"),
+ jpeg_quality=self.config_getter("jpeg_quality"),
+ png_level=self.config_getter("png_level"),
+ )
+ copy_file_to_clipboard(filepath)
+
+ def get_notification_actions(self) -> Iterable[Type[NotificationAction]]:
+ return (
+ EditNotificationAction,
+ DeleteNotificationAction,
+ DragonNotificationAction,
+ OpenNotificationAction,
+ )
+
+ def get_notification_icon(self, filepath: str) -> Optional[str]:
+ return filepath
+
+
+class Screencast(CaptureMode):
+ def get_display_name(self) -> str:
+ return "Screencast"
+
+ def get_pid_filepath(self) -> Path:
+ uid = os.getuid()
+ xdg_runtime_dir = os.getenv("XDG_RUNTIME_DIR", f"/run/user/{uid}")
+ wayland_display = os.getenv("WAYLAND_DISPLAY", "wayland-1")
+
+ return Path(
+ self.config_getter("pid_file")
+ .replace("${XDG_RUNTIME_DIR}", xdg_runtime_dir)
+ .replace("${WAYLAND_DISPLAY}", wayland_display)
+ .replace("${UID}", str(uid))
+ )
+
+ def early_exit(self) -> bool:
+ pid_filepath = self.get_pid_filepath()
+ if pid_filepath.exists():
+ pid = int(pid_filepath.read_text())
+ print(
+ f"Sending kill signal to {pid} to stop video recording.",
+ file=sys.stderr,
+ )
+ os.kill(pid, signal.SIGINT)
+ notify(self.get_display_name(), "Stopping recording.")
+ return True
+
+ return False
+
+ def capture(self, *, area: Area, filepath: str) -> None:
+ audio_raw_input = self.config_getter("audio")
+ if audio_raw_input == "ask":
+ audio_raw_input = ask(["yes", "no"], prompt=self.get_prompt() + "Audio? ")
+ if audio_raw_input is None:
+ raise CanceledError("No audio preference selected.")
+
+ start_recording = ask(
+ ["yes", "cancel"], prompt=self.get_prompt() + "Start recording? "
+ )
+ if start_recording != "yes":
+ raise CanceledError()
+
+ take_screencast(
+ filepath=filepath,
+ geometry=area.geometry,
+ output=area.output,
+ audio=audio_raw_input == "yes",
+ pid_file=self.get_pid_filepath(),
+ )
+
+ def get_notification_actions(self) -> Iterable[Type[NotificationAction]]:
+ return (
+ DeleteNotificationAction,
+ DragonNotificationAction,
+ OpenNotificationAction,
+ )
+
+
+def main():
+ mode: Optional[CaptureMode] = None
+ try:
+ args = parse_arguments()
+ config = Config(args.config)
+ mode: CaptureMode = (
+ Screenshot(partial(config.get, "screenshot"))
+ if not args.video
+ else Screencast(partial(config.get, "screencast"))
+ )
+ if mode.early_exit():
+ return
+
+ global ask
+ ask = partial(
+ ASK_FNS[config.get("ask","command")],
+ additional_args=config.get("ask","args")
+ )
+
+ save_dir = args.save_dir
+ if save_dir is None:
+ save_dir = mode.get_save_dir()
+ if save_dir is None:
+ save_dir = os.getenv("SWAY_INTERACTIVE_SCREENSHOT_SAVEDIR", "~")
+
+ save_dir = Path(save_dir)
+ save_dir.expanduser().mkdir(parents=True, exist_ok=True)
+ filepath = Path(args.output) if args.output else None
+
+ if args.selection is None:
+ choices = [
+ Region(),
+ FocusedOutput(),
+ *((AllOutputs(),) if isinstance(mode, Screenshot) else ()),
+ SelectOutput(),
+ *get_outputs(),
+ FocusedWindow(),
+ SelectWindow(),
+ *get_windows(),
+ ]
+
+ choice_idx = ask(choices, prompt=mode.get_prompt(), index=True)
+ if choice_idx is None:
+ return
+ if choice_idx == -1:
+ raise CanceledError("No option selected.")
+
+ choice = choices[choice_idx]
+ else:
+ choice = args.selection()
+
+ area = choice.get_area()
+ if not filepath:
+ filepath = save_dir / mode.get_filename()
+
+ mode.capture(filepath=str(filepath.expanduser()), area=area)
+
+ action_name = notify(
+ mode.get_display_name(),
+ summary=f"File saved as {filepath}.",
+ icon=mode.get_notification_icon(filepath.expanduser()),
+ actions=[
+ (action.name, action.description)
+ for action in mode.get_notification_actions()
+ ],
+ )
+ action = next(
+ (
+ action
+ for action in mode.get_notification_actions()
+ if action.name == action_name
+ ),
+ None,
+ )
+
+ if action is not None:
+ action.run(filepath=filepath,
+ config_getter=partial(config.get,
+ "notification_actions",
+ action.name)
+ )
+
+ except Exception as err: # pylint: disable=broad-except
+ display_name = (
+ mode.get_display_name()
+ if mode is not None
+ else "sway-interactive-screenshot"
+ )
+ if isinstance(err, CanceledError):
+ notify(f"{display_name} canceled", summary=err.msg)
+ else:
+ notify(f"{display_name} error", str(err))
+ raise
+
+
+if __name__ == "__main__":
+ main()
diff --git a/files/sway/.config/sway/status.sh b/files/sway/.config/sway/status.sh
new file mode 100755
index 0000000..c883d25
--- /dev/null
+++ b/files/sway/.config/sway/status.sh
@@ -0,0 +1,64 @@
+# Change this according to your device
+################
+# Variables
+################
+
+# Keyboard input name
+# keyboard_input_name="1:2:AT_Raw_Set_2_keyboard"
+
+# Date and time
+date_and_week=$(date "+%a %d.%b (KW%-V)")
+current_time=$(date "+%H:%M")
+
+#############
+# Commands
+#############
+
+# Battery or charger
+battery_charge=$(upower --show-info $(upower --enumerate | grep 'BAT') | egrep "percentage" | awk '{print $2}')
+battery_status=$(upower --show-info $(upower --enumerate | grep 'BAT') | egrep "state" | awk '{print $2}')
+
+# Audio and multimedia
+audio_volume=$(pamixer --sink `pactl list sinks short | grep RUNNING | awk '{print $1}'` --get-volume)
+audio_is_muted=$(pamixer --sink `pactl list sinks short | grep RUNNING | awk '{print $1}'` --get-mute)
+loadavg_5min=$(cat /proc/loadavg | awk -F ' ' '{print $2}')
+
+# Brightness
+brightness=$(printf %.0f $(light))
+
+# Removed weather because we are requesting it too many times to have a proper
+# refresh on the bar
+#weather=$(curl -Ss 'https://wttr.in/Pontevedra?0&T&Q&format=1')
+
+if [ $battery_status = "discharging" ];
+then
+ battery_pluggedin='⚠'
+else
+ battery_pluggedin='⚡'
+fi
+
+if ! [ $network ]
+then
+ network_active="⛔"
+else
+ network_active="⇆"
+fi
+
+if [ $player_status = "Playing" ]
+then
+ song_status='▶'
+elif [ $player_status = "Paused" ]
+then
+ song_status='⏸'
+else
+ song_status='⏹'
+fi
+
+if [ $audio_is_muted = "true" ]
+then
+ audio_active='🔇'
+else
+ audio_active='🔊'
+fi
+
+echo "🏋 $loadavg_5min | $audio_active $audio_volume% | ☀️ $brightness | $battery_pluggedin $battery_charge | $date_and_week 🕘 $current_time"
diff --git a/files/waybar/.config/waybar/config b/files/waybar/.config/waybar/config
new file mode 100755
index 0000000..432bc60
--- /dev/null
+++ b/files/waybar/.config/waybar/config
@@ -0,0 +1,184 @@
+// =============================================================================
+//
+// Waybar configuration
+//
+// Configuration reference: https://github.com/Alexays/Waybar/wiki/Configuration
+//
+// =============================================================================
+
+{
+ // -------------------------------------------------------------------------
+ // Global configuration
+ // -------------------------------------------------------------------------
+
+ "layer": "top",
+
+ "position": "top",
+
+ // If height property would be not present, it'd be calculated dynamically
+ "height": 30,
+
+ "modules-left": [
+ "sway/workspaces",
+ // "sway/mode",
+ "sway/window"
+ ],
+ "modules-center": [
+ ],
+ "modules-right": [
+ // "network",
+ "pulseaudio",
+ // "memory",
+ // "cpu",
+ // "temperature",
+ "backlight",
+ // "custom/keyboard-layout",
+ "battery",
+ "clock#date",
+ "clock#time",
+ "tray"
+ ],
+
+
+ // -------------------------------------------------------------------------
+ // Modules
+ // -------------------------------------------------------------------------
+
+ "battery": {
+ "interval": 10,
+ "states": {
+ "warning": 30,
+ "critical": 15
+ },
+ // Connected to AC
+ "format": " {icon} {capacity}%", // Icon: bolt
+ // Not connected to AC
+ // "format-discharging": "{icon} {capacity}%",
+ "format-discharging": "{capacity}%",
+ "format-icons": [
+ "", // Icon: battery-full
+ "", // Icon: battery-three-quarters
+ "", // Icon: battery-half
+ "", // Icon: battery-quarter
+ "" // Icon: battery-empty
+ ],
+ "tooltip": true
+ },
+
+ "backlight": {
+ "device": "intel_backlight",
+ // "format": "{icon} {percent}%",
+ "format": "{percent}%",
+ "format-icons": ["", ""]
+ },
+
+
+ "clock#time": {
+ "interval": 1,
+ "format": "{:%H:%M}",
+ "tooltip": false,
+ "on-click": "gnome-clocks"
+ },
+
+ "clock#date": {
+ "interval": 10,
+ // "format": " {:%e %b %Y}", // Icon: calendar-alt
+ "format": "{:%e %b %Y}", // Icon: calendar-alt
+ "tooltip-format": "{:%e %B %Y}",
+ "on-click": "gnome-calendar"
+ },
+
+ "cpu": {
+ "interval": 5,
+ "format": " {usage}% ({load})", // Icon: microchip
+ "states": {
+ "warning": 70,
+ "critical": 90
+ }
+ },
+
+ "custom/keyboard-layout": {
+ "exec": "swaymsg -t get_inputs | grep -m1 'xkb_active_layout_name' | cut -d '\"' -f4",
+ // Interval set only as a fallback, as the value is updated by signal
+ "interval": 30,
+ "format": " {}", // Icon: keyboard
+ // Signal sent by Sway key binding (~/.config/sway/key-bindings)
+ "signal": 1, // SIGHUP
+ "tooltip": false
+ },
+
+ "memory": {
+ "interval": 5,
+ "format": " {}%", // Icon: memory
+ "states": {
+ "warning": 70,
+ "critical": 90
+ }
+ },
+
+ "network": {
+ "interval": 5,
+ "format-wifi": " {essid} ({signalStrength}%)", // Icon: wifi
+ "format-ethernet": " {ifname}: {ipaddr}/{cidr}", // Icon: ethernet
+ "format-disconnected": "⚠ Disconnected",
+ "tooltip-format": "{ifname}: {ipaddr}"
+ },
+
+ "sway/mode": {
+ "format": " {}", // Icon: expand-arrows-alt
+ "tooltip": false
+ },
+
+ "sway/window": {
+ "format": "{}",
+ "max-length": 120
+ },
+
+ "sway/workspaces": {
+ "all-outputs": false,
+ "disable-scroll": true,
+ "format": "{icon} {name}",
+ "format-icons": {
+ "urgent": "",
+ "focused": "",
+ "default": ""
+ }
+ },
+
+ "pulseaudio": {
+ //"scroll-step": 1,
+ "format": "{volume}%",
+ "format-bluetooth": "{volume}%",
+ "format-muted": "Mute",
+ "format-icons": {
+ "headphones": "",
+ "handsfree": "",
+ "headset": "",
+ "phone": "",
+ "portable": "",
+ "car": "",
+ "default": ["", ""]
+ },
+ "on-click": "pavucontrol"
+ },
+
+ "temperature": {
+ "critical-threshold": 80,
+ "interval": 5,
+ "format": "{icon} {temperatureC}°C",
+ "format-icons": [
+ "", // Icon: temperature-empty
+ "", // Icon: temperature-quarter
+ "", // Icon: temperature-half
+ "", // Icon: temperature-three-quarters
+ "" // Icon: temperature-full
+ ],
+ "tooltip": true
+ },
+
+ "tray": {
+ "icon-size": 15,
+ "spacing": 10
+ }
+
+}
diff --git a/files/waybar/.config/waybar/style.css b/files/waybar/.config/waybar/style.css
new file mode 100755
index 0000000..f3869a5
--- /dev/null
+++ b/files/waybar/.config/waybar/style.css
@@ -0,0 +1,197 @@
+/* =============================================================================
+ *
+ * Waybar configuration
+ *
+ * Configuration reference: https://github.com/Alexays/Waybar/wiki/Configuration
+ *
+ * =========================================================================== */
+
+/* -----------------------------------------------------------------------------
+ * Keyframes
+ * -------------------------------------------------------------------------- */
+
+@keyframes blink-warning {
+ 70% {
+ color: white;
+ }
+
+ to {
+ color: white;
+ background-color: orange;
+ }
+}
+
+@keyframes blink-critical {
+ 70% {
+ color: white;
+ }
+
+ to {
+ color: white;
+ background-color: red;
+ }
+}
+
+
+/* -----------------------------------------------------------------------------
+ * Base styles
+ * -------------------------------------------------------------------------- */
+
+/* Reset all styles */
+* {
+ border: none;
+ border-radius: 0;
+ min-height: 0;
+ margin: 0;
+ padding: 0;
+}
+
+/* The whole bar */
+#waybar {
+ background: #2D353B;
+ color: white;
+ /* font-family: Cantarell, Noto Sans, sans-serif; */
+ font-family: JetBrainsMono Nerd Font Mono;
+ font-size: 12px;
+}
+
+/* Each module */
+#battery,
+#backlight,
+#clock,
+#cpu,
+#custom-keyboard-layout,
+#memory,
+#mode,
+#network,
+#pulseaudio,
+#temperature,
+#tray {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+
+/* -----------------------------------------------------------------------------
+ * Module styles
+ * -------------------------------------------------------------------------- */
+
+#battery {
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+}
+
+#battery.warning {
+ color: orange;
+}
+
+#battery.critical {
+ color: red;
+}
+
+#battery.warning.discharging {
+ animation-name: blink-warning;
+ animation-duration: 3s;
+}
+
+#battery.critical.discharging {
+ animation-name: blink-critical;
+ animation-duration: 2s;
+}
+
+#clock {
+ font-weight: bold;
+}
+
+#cpu {
+ /* No styles */
+}
+
+#cpu.warning {
+ color: orange;
+}
+
+#cpu.critical {
+ color: red;
+}
+
+#memory {
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+ animation-direction: alternate;
+}
+
+#memory.warning {
+ color: orange;
+}
+
+#memory.critical {
+ color: red;
+ animation-name: blink-critical;
+ animation-duration: 2s;
+}
+
+#mode {
+ background: #64727D;
+ border-top: 2px solid white;
+ /* To compensate for the top border and still have vertical centering */
+ padding-bottom: 2px;
+}
+
+#network {
+ /* No styles */
+}
+
+#network.disconnected {
+ color: orange;
+}
+
+#pulseaudio {
+ /* No styles */
+}
+
+#pulseaudio.muted {
+ /* No styles */
+}
+
+#custom-spotify {
+ color: rgb(102, 220, 105);
+}
+
+#temperature {
+ /* No styles */
+}
+
+#temperature.critical {
+ color: red;
+}
+
+#tray {
+ /* No styles */
+}
+
+#window {
+ font-weight: bold;
+ padding-left: 10px;
+}
+
+#workspaces button {
+ border-top: 2px solid transparent;
+ /* To compensate for the top border and still have vertical centering */
+ padding-bottom: 2px;
+ padding-left: 10px;
+ padding-right: 10px;
+ color: #888888;
+}
+
+#workspaces button.focused {
+ border-color: #A7C080;
+ /* color: white; */
+ color: #A7C080;
+}
+
+#workspaces button.urgent {
+ border-color: #c9545d;
+ color: #c9545d;
+}
\ No newline at end of file
diff --git a/files/wofi/.config/wofi/style.css b/files/wofi/.config/wofi/style.css
new file mode 100644
index 0000000..7150aac
--- /dev/null
+++ b/files/wofi/.config/wofi/style.css
@@ -0,0 +1,40 @@
+window {
+margin: 0px;
+border: 2px solid #7A8478;
+background-color: #1E2326;
+border-radius: 10px;
+}
+
+#input {
+margin: 5px;
+border: none;
+color: #D3C6AA;
+background-color: #272E33;
+}
+
+#inner-box {
+margin: 5px;
+border: none;
+background-color: #1E2326;
+}
+
+#outer-box {
+margin: 5px;
+border: none;
+background-color: #1E2326;
+}
+
+#scroll {
+margin: 0px;
+border: none;
+}
+
+#text {
+margin: 5px;
+border: none;
+color: #D3C6AA;
+}
+
+#entry:selected {
+background-color: #272E33;
+}