sway: add sway and waybar

This commit is contained in:
2024-07-30 10:35:42 +02:00
parent a25cd67069
commit 3689d53e4b
7 changed files with 1560 additions and 1 deletions

View File

@@ -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

View File

@@ -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 <i>{filepath}</i>.",
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()

View File

@@ -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"