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; +}