Compare commits

...

7 Commits

Author SHA1 Message Date
Tom Alexander
477637ae62
Add a script to test fetching PGP keys from a Web Key Directory (WKD). 2025-01-12 18:29:48 -05:00
Tom Alexander
5146a114eb
Introduce a variable for sway includes and disable relatime on the zfs legacy mounts. 2025-01-12 15:39:46 -05:00
Tom Alexander
a817464b38
Preserve steam directories. 2025-01-11 22:36:09 -05:00
Tom Alexander
1acf889c68
Instll steam and the zfs_clone_send / zfs_clone_recv scripts. 2025-01-11 13:48:46 -05:00
Tom Alexander
af07d43c18
Add asian fonts. 2025-01-11 12:50:13 -05:00
Tom Alexander
33f13d898d
Switch to ares instead of bsnes. 2025-01-11 12:09:02 -05:00
Tom Alexander
47d9e203f3
Add media role. 2025-01-10 22:54:32 -05:00
21 changed files with 662 additions and 132 deletions

View File

@ -10,6 +10,7 @@
{
imports = [
./roles/reset
./util/unfree_polyfill
./roles/iso
./hosts/odo
"${
@ -36,12 +37,14 @@
./roles/waybar
./roles/qemu
./roles/wireguard
./roles/bsnes
./roles/ares
./roles/ssh
./roles/python
./roles/docker
./roles/kubernetes
./roles/rust
./roles/media
./roles/steam
];
nix.settings.experimental-features = [
@ -131,6 +134,7 @@
tcpdump
git-crypt
nix-index-unwrapped
gnumake
];
services.openssh = {

View File

@ -27,4 +27,7 @@
environment.systemPackages = with pkgs; [
fw-ectool
];
me.graphical = true;
me.graphicsCardType = "amd";
}

View File

@ -119,6 +119,27 @@ lib.mkIf (!config.me.buildingIso) {
fileSystems."/state".neededForBoot = true;
fileSystems."/home".neededForBoot = true;
fileSystems."/".options = [
"noatime"
"norelatime"
];
fileSystems."/nix".options = [
"noatime"
"norelatime"
];
fileSystems."/persist".options = [
"noatime"
"norelatime"
];
fileSystems."/state".options = [
"noatime"
"norelatime"
];
fileSystems."/home".options = [
"noatime"
"norelatime"
];
# Only attempt to decrypt the main pool. Otherwise it attempts to decrypt pools that aren't even used.
boot.zfs.requestEncryptionCredentials = [ "zroot/linux/nix" ];
}

View File

@ -9,6 +9,6 @@
imports = [ ];
environment.systemPackages = with pkgs; [
bsnes-hd
ares
];
}

View File

@ -14,13 +14,11 @@
(chromium.override { enableWideVine = true; })
];
nixpkgs.config.allowUnfreePredicate =
pkg:
builtins.elem (lib.getName pkg) [
"chromium"
"chromium-unwrapped"
"widevine-cdm"
];
allowedUnfree = [
"chromium"
"chromium-unwrapped"
"widevine-cdm"
];
environment.persistence."/persist" = lib.mkIf (!config.me.buildingIso) {
hideMounts = true;

View File

@ -13,6 +13,9 @@
enable = true;
setSocketVariable = true;
};
environment.systemPackages = with pkgs; [
docker-buildx
];
environment.persistence."/state" = lib.mkIf (!config.me.buildingIso) {
hideMounts = true;

View File

@ -14,6 +14,8 @@
cascadia-code
source-sans-pro
source-serif-pro
noto-fonts-cjk-sans
noto-fonts-cjk-serif
noto-fonts-color-emoji
];

View File

@ -6,6 +6,14 @@
...
}:
let
gpg_test_wkd =
(pkgs.writeScriptBin "gpg_test_wkd" (builtins.readFile ./files/gpg_test_wkd.bash)).overrideAttrs
(old: {
buildCommand = "${old.buildCommand}\n patchShebangs $out";
});
in
{
imports = [ ];
@ -139,6 +147,7 @@
glibcLocales
ccid
libusb-compat-0_1
gpg_test_wkd
];
# nixpkgs.overlays = [

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
#
# Test that we can retrieve a PGP key using Web Key Directory (WKD)
set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
gpg --no-default-keyring --keyring /tmp/gpg-$$ --auto-key-locate clear,wkd --locate-keys "${@}"

View File

@ -5,6 +5,14 @@
...
}:
let
exec_kanshi = pkgs.writeTextFile {
name = "exec_kanshi.conf";
text = ''
exec kanshi
'';
};
in
{
imports = [ ];
@ -12,6 +20,10 @@
kanshi
];
me.swayIncludes = [
exec_kanshi
];
home-manager.users.talexander =
{ pkgs, ... }:
{

View File

@ -0,0 +1,74 @@
{
config,
lib,
pkgs,
...
}:
let
cast_file_vaapi =
(pkgs.writeScriptBin "cast_file" (builtins.readFile ./files/cast_file_vaapi)).overrideAttrs
(old: {
buildCommand = "${old.buildCommand}\n patchShebangs $out";
});
cast_file_nvidia =
(pkgs.writeScriptBin "cast_file" (builtins.readFile ./files/cast_file_nvidia)).overrideAttrs
(old: {
buildCommand = "${old.buildCommand}\n patchShebangs $out";
});
in
{
imports = [ ];
options.me.graphicsCardType = lib.mkOption {
type = lib.types.nullOr (
lib.types.enum [
"amd"
"intel"
"nvidia"
]
);
default = null;
example = "amd";
description = "What graphics card type is in the computer.";
};
options.me.graphical = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = "Whether we want to install graphical programs.";
};
config = (
lib.mkMerge [
{
environment.systemPackages = with pkgs; [
ffmpeg
];
home-manager.users.talexander =
{ pkgs, ... }:
{
home.file.".config/mpv/mpv.conf" = {
source = ./files/mpv.conf;
};
};
}
(lib.mkIf config.me.graphical {
environment.systemPackages = with pkgs; [
mpv
evince
gimp
imv
];
})
(lib.mkIf (config.me.graphicsCardType == "amd" || config.me.graphicsCardType == "intel") {
environment.systemPackages = with pkgs; [
cast_file_vaapi
];
})
]
);
}

View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
#
ffmpeg -re -i "$1" -vcodec h264_nvenc -r 30 -g 30 -loop -1 -c:a aac -b:a 160k -ar 44100 -strict -2 -f flv rtmp:172.16.16.44/live/test &
ffmpegpid=$!
sleep 1
castnow --exit 'https://broadcast.fizz.buzz/hls/hls/test.m3u8'
wait "$ffmpegpid"
sleep 10

View File

@ -0,0 +1,237 @@
#!/usr/bin/env bash
#
set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
: ${VIDEO_BITRATE:="1M"} # Only for encoding modes targeting bitrate
: ${AUDIO_BITRATE:="192k"}
############## Setup #########################
function die {
local status_code="$1"
shift
(>&2 echo "${@}")
exit "$status_code"
}
function log {
(>&2 echo "${@}")
}
############## Program #########################
function main {
local cmd
cmd=$1
shift
if [ "$cmd" = "copy" ]; then
copy "${@}"
elif [ "$cmd" = "convert" ]; then
convert "${@}"
elif [ "$cmd" = "stream" ]; then
stream "${@}"
elif [ "$cmd" = "webcam" ]; then
webcam "${@}"
elif [ "$cmd" = "encode_webcam" ]; then
encode_webcam "${@}"
else
(>&2 echo "Unknown command: $cmd")
exit 1
fi
}
function copy {
local file_to_cast
file_to_cast="$3"
local USERNAME PASSWORD
USERNAME="$1"
PASSWORD="$2"
set -x
</dev/null exec ffmpeg \
-re \
-stream_loop -1 \
-i "$file_to_cast" \
-c copy \
-strict experimental \
-f rtsp \
-rtsp_transport tcp \
"rtsp://$USERNAME:$PASSWORD@172.16.16.251:8554/fetch"
}
function convert {
local args=()
local acceleration_type="$1" # "software" or "hardware"
local codec="$2" # "h264" or "av1"
local file_to_cast="$3"
local file_to_save="$4"
# Verify parameters
if [ "$acceleration_type" == "software" ]; then
true
elif [ "$acceleration_type" == "hardware" ]; then
true
else
die 1 "Unknown acceleration type: $acceleration_type"
fi
if [ "$codec" == "h264" ]; then
true
elif [ "$codec" == "av1" ]; then
true
else
die 1 "Unknown codec: $codec"
fi
# Build command
if [ "$acceleration_type" == "software" ]; then
true
elif [ "$acceleration_type" == "hardware" ]; then
args+=(-vaapi_device /dev/dri/renderD128)
fi
args+=(-i "$file_to_cast")
if [ "$codec" == "h264" ]; then
if [ "$acceleration_type" == "software" ]; then
args+=(-c:v h264)
args+=(-profile:v high)
args+=(-b:v "$VIDEO_BITRATE")
elif [ "$acceleration_type" == "hardware" ]; then
args+=(-vf 'format=nv12|vaapi,hwupload')
args+=(-c:v h264_vaapi)
args+=(-profile:v high)
args+=(-b:v "$VIDEO_BITRATE")
fi
elif [ "$codec" == "av1" ]; then
if [ "$acceleration_type" == "software" ]; then
args+=(-c:v libsvtav1)
args+=(-preset 4) # [0-13] default 10, lower = higher quality / slower encode
args+=(-crf 20) # [0-63] default 35, lower = higher quality / larger file
# Parameters: https://gitlab.com/AOMediaCodec/SVT-AV1/-/blob/master/Docs/Parameters.md
# fast-decode [0-2] default 0 (off), higher = faster decode
# tune [0-2] default 1, Specifies whether to use PSNR or VQ as the tuning metric [0 = VQ, 1 = PSNR, 2 = SSIM]
# film-grain-denoise, setting to 0 uses the original frames instead of denoising the film grain
args+=(-svtav1-params "fast-decode=1:film-grain-denoise=0")
elif [ "$acceleration_type" == "hardware" ]; then
# -c:v av1_amf -quality quality
args+=(-vf 'format=nv12|vaapi,hwupload')
args+=(-c:v av1_vaapi)
args+=(-b:v "$VIDEO_BITRATE")
fi
fi
# -bf 0 :: Disable b-frames because webrtc doesn't support h264 streams with b-frames.
args+=(-bf 0)
args+=(-strict -2)
args+=(-c:a opus)
args+=(-ac 2)
args+=(-b:a "$AUDIO_BITRATE")
args+=(-ar 48000)
args+=("$file_to_save")
set -x
</dev/null exec ffmpeg "${args[@]}"
}
function stream {
local args=()
local acceleration_type="$1" # "software" or "hardware"
local codec="$2" # "h264" or "av1"
local USERNAME="$3"
local PASSWORD="$4"
local file_to_cast="$5"
args+=(-re -stream_loop -1)
args+=(-f rtsp)
args+=(-rtsp_transport tcp)
args+=("rtsp://$USERNAME:$PASSWORD@172.16.16.251:8554/fetch")
}
function webcam {
# Uses on-webcam h264 encoding.
local USERNAME PASSWORD
USERNAME="$1"
PASSWORD="$2"
set -x
</dev/null exec ffmpeg \
-re \
-input_format h264 \
-video_size 1920x1080 \
-i /dev/video0 \
-c:v copy \
-an \
-f rtsp \
-rtsp_transport tcp \
"rtsp://$USERNAME:$PASSWORD@172.16.16.251:8554/fetch"
}
function encode_webcam {
# Uses hardware accelerated gpu-based encoding.
local USERNAME PASSWORD
USERNAME="$1"
PASSWORD="$2"
set -x
</dev/null exec ffmpeg \
-re \
-vaapi_device /dev/dri/renderD128 \
-i /dev/video0 \
-vf 'format=nv12,hwupload' \
-c:v h264_vaapi \
-an \
-f rtsp \
-rtsp_transport tcp \
"rtsp://$USERNAME:$PASSWORD@172.16.16.251:8554/fetch"
}
function speed_up_preprocess_vp8 {
local file_to_cast file_to_save
file_to_cast="$1"
file_to_save="$2"
set -x
# -bf 0 :: Disable b-frames because webrtc doesn't support h264 streams with b-frames.
# -strict -2 :: Enable support for experimental codecs like opus.
# -b:v 2M :: Target 2 megabit/s
# -crf 10 :: Target a quality level and adjust bitrate accordingly. This should be preferred, but ideally both should be used.
# Could also use -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]"
</dev/null exec ffmpeg \
-i "$file_to_cast" \
-filter:v "setpts=0.66666666*PTS" \
-filter:a "atempo=1.5" \
-c:v vp8 \
-b:v 2M \
-crf 10 \
-bf 0 \
-c:a opus \
-b:a 320k \
-ar 48000 \
-strict -2 \
"$file_to_save"
}
main "${@}"

View File

@ -0,0 +1,25 @@
# To debug hardware video acceleration:
# mpv --hwdec=auto --msg-level=vd=v,vo=v,vo/gpu/vaapi-egl=trace
# GPU Decoding
hwdec=auto
# Allow CPU processing via filters:
#hwdec=auto-copy
# Use higher quality gpu rendering
profile=gpu-hq
scale=ewa_lanczossharp
cscale=ewa_lanczossharp
# Instead of dropping frames, re-sample audio which may cause a slight pitch change
# ISSUE: caused frame stutter on Louie S01E03
# video-sync=display-resample
# Make motion smoother when video frame rate != monitor refresh rate
interpolation
tscale=oversample
# Load a lot of the file into memory
# cache=yes
# demuxer-max-bytes=123400KiB
# demuxer-readahead-secs=20

View File

@ -0,0 +1,48 @@
{
config,
lib,
pkgs,
...
}:
{
imports = [ ];
options.me.games = lib.mkOption {
type = lib.types.bool;
default = config.me.graphical;
example = true;
description = "Whether we want to install games.";
};
config = (
lib.mkMerge [
(lib.mkIf config.me.games {
allowedUnfree = [
"steam"
"steam-original"
"steam-unwrapped"
"steam-run"
];
programs.steam = {
enable = true;
remotePlay.openFirewall = true; # Open ports in the firewall for Steam Remote Play
# dedicatedServer.openFirewall = true; # Open ports in the firewall for Source Dedicated Server
localNetworkGameTransfers.openFirewall = true; # Open ports in the firewall for Steam Local Network Game Transfers
};
environment.persistence."/persist" = lib.mkIf (!config.me.buildingIso) {
hideMounts = true;
users.talexander = {
directories = [
".local/share/Steam"
".steam"
];
};
};
})
]
);
}

View File

@ -34,19 +34,11 @@ let
# Do not show a title bar on windows
default_border pixel 2
hide_edge_borders smart_no_gaps
bindsym $mod+grave exec $term
include ${base-hotkeys}
include ${display-configs}
include ${window-management}
include ${movement}
include ${disable-focus-follows-mouse}
include ${background}
include ${touchpad_input}
include ${waybar}
include ${announce_sway_start}
include ${exec_kanshi}
${lib.concatMapStringsSep "\n" (item: "include ${item}") config.me.swayIncludes}
'';
};
base-hotkeys = pkgs.writeTextFile {
@ -232,27 +224,6 @@ let
}
'';
};
waybar = pkgs.writeTextFile {
name = "waybar.conf";
text = ''
#
# Status Bar:
#
# Read `man 5 sway-bar` for more information about this section.
bar {
position top
font pango:Cascadia Mono, FontAwesome 10
swaybar_command waybar
colors {
statusline #ffffff
background #323232
inactive_workspace #32323200 #32323200 #5c5c5c
}
}
'';
};
announce_sway_start = pkgs.writeTextFile {
name = "announce_sway_start.conf";
text = ''
@ -262,13 +233,6 @@ let
'';
};
exec_kanshi = pkgs.writeTextFile {
name = "exec_kanshi.conf";
text = ''
exec kanshi
'';
};
start_screen_share = pkgs.writeShellScriptBin "start_screen_share" ''
# Disable displaying notifications. This is useful for video conference screen sharing.
set -euo pipefail
@ -295,101 +259,132 @@ in
../kanshi
];
environment.systemPackages = with pkgs; [
alacritty
pcmanfm
start_screen_share
stop_screen_share
];
options.me.swayIncludes = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
example = lib.literalExpression ''
[ (pkgs.writeTextFile {
name = "launch-kanshi.conf";
text = "exec kanshi";
}) ]'';
description = "List of packages to import as sway configs.";
};
# Probably would be cleaner to use environment.sessionVariables but programs.sway.extraSessionCommands is sway-specific.
programs.sway.extraSessionCommands =
if config.me.buildingIso then
''
export WLR_RENDERER_ALLOW_SOFTWARE=1
export NIXOS_OZONE_WL=1 # Wayland support for chromium and electron
export QT_QPA_PLATFORMTHEME=gtk3 # Use gtk theme in Qt applications
''
else
''
export WLR_RENDERER=vulkan
export NIXOS_OZONE_WL=1 # Wayland support for chromium and electron
export QT_QPA_PLATFORMTHEME=gtk3 # Use gtk theme in Qt applications
'';
config = {
environment.systemPackages = with pkgs; [
alacritty
pcmanfm
start_screen_share
stop_screen_share
];
programs.sway = {
enable = true;
wrapperFeatures.gtk = true;
extraOptions =
me.swayIncludes = [
base-hotkeys
display-configs
window-management
movement
disable-focus-follows-mouse
background
touchpad_input
announce_sway_start
];
# Probably would be cleaner to use environment.sessionVariables but programs.sway.extraSessionCommands is sway-specific.
programs.sway.extraSessionCommands =
if config.me.buildingIso then
[
"--debug"
"--config"
"${sway-config}"
"--unsupported-gpu"
]
''
export WLR_RENDERER_ALLOW_SOFTWARE=1
export NIXOS_OZONE_WL=1 # Wayland support for chromium and electron
export QT_QPA_PLATFORMTHEME=gtk3 # Use gtk theme in Qt applications
''
else
[
"--debug"
"--config"
"${sway-config}"
];
};
''
export WLR_RENDERER=vulkan
export NIXOS_OZONE_WL=1 # Wayland support for chromium and electron
export QT_QPA_PLATFORMTHEME=gtk3 # Use gtk theme in Qt applications
'';
environment.persistence."/state" = lib.mkIf (!config.me.buildingIso) {
hideMounts = true;
users.talexander = {
files = [
".cache/wofi-drun" # Execution history for wofi to sort results
];
};
};
xdg = {
portal = {
programs.sway = {
enable = true;
extraPortals = with pkgs; [
xdg-desktop-portal-wlr
xdg-desktop-portal-gtk
];
wlr = {
wrapperFeatures.gtk = true;
extraOptions =
if config.me.buildingIso then
[
"--debug"
"--config"
"${sway-config}"
"--unsupported-gpu"
]
else
[
"--debug"
"--config"
"${sway-config}"
];
};
environment.persistence."/state" = lib.mkIf (!config.me.buildingIso) {
hideMounts = true;
users.talexander = {
files = [
".cache/wofi-drun" # Execution history for wofi to sort results
];
};
};
xdg = {
portal = {
enable = true;
settings = {
# uninteresting for this problem, for completeness only
screencast = {
# output_name = "eDP-1";
max_fps = 30;
exec_before = "${start_screen_share}";
exec_after = "${stop_screen_share}";
chooser_type = "simple";
chooser_cmd = "${pkgs.slurp}/bin/slurp -f %o -or";
extraPortals = with pkgs; [
xdg-desktop-portal-wlr
xdg-desktop-portal-gtk
];
wlr = {
enable = true;
settings = {
# uninteresting for this problem, for completeness only
screencast = {
# output_name = "eDP-1";
max_fps = 30;
exec_before = "${start_screen_share}";
exec_after = "${stop_screen_share}";
chooser_type = "simple";
chooser_cmd = "${pkgs.slurp}/bin/slurp -f %o -or";
};
};
};
};
};
home-manager.users.talexander =
{ pkgs, ... }:
{
home.file = {
# Configure default programs (for example, default browser)
".config/mimeapps.list" = {
source = ./files/mimeapps.list;
};
};
home.file = {
".config/gtk-3.0/settings.ini" = {
source = ./files/settings.ini;
};
};
home.file = {
".icons/default" = {
source = "${pkgs.adwaita-icon-theme}/share/icons/Adwaita";
};
};
};
# For mounting drives in pcmanfm
services.gvfs.enable = true;
# Auto-launch sway
environment.loginShellInit = ''
# TODO: This shouldn't be shoe-horned into the sway config
doas iw dev wlan0 set power_save off
[[ "$(tty)" = "/dev/tty1" ]] && exec sway
'';
};
home-manager.users.talexander =
{ pkgs, ... }:
{
home.file = {
# Configure default programs (for example, default browser)
".config/mimeapps.list" = {
source = ./files/mimeapps.list;
};
};
home.file = {
".config/gtk-3.0/settings.ini" = {
source = ./files/settings.ini;
};
};
home.file = {
".icons/default" = {
source = "${pkgs.adwaita-icon-theme}/share/icons/Adwaita";
};
};
};
# For mounting drives in pcmanfm
services.gvfs.enable = true;
}

View File

@ -5,6 +5,28 @@
...
}:
let
waybar_sway_config = pkgs.writeTextFile {
name = "waybar.conf";
text = ''
#
# Status Bar:
#
# Read `man 5 sway-bar` for more information about this section.
bar {
position top
font pango:Cascadia Mono, FontAwesome 10
swaybar_command waybar
colors {
statusline #ffffff
background #323232
inactive_workspace #32323200 #32323200 #5c5c5c
}
}
'';
};
waybar_available_memory =
(pkgs.writeScriptBin "waybar_custom_available_memory" (
builtins.readFile ./files/waybar_scripts/waybar_available_memory_linux.bash
@ -72,6 +94,10 @@ in
wlsunset # for night mode
];
me.swayIncludes = [
waybar_sway_config
];
services.upower.enable = true; # for battery
home-manager.users.talexander =

View File

@ -5,6 +5,20 @@
...
}:
let
zfs_clone_send =
(pkgs.writeScriptBin "zfs_clone_send" (builtins.readFile ./files/zfs_clone_send.bash)).overrideAttrs
(old: {
buildCommand = "${old.buildCommand}\n patchShebangs $out";
});
zfs_clone_recv =
(pkgs.writeScriptBin "zfs_clone_recv" (builtins.readFile ./files/zfs_clone_recv.bash)).overrideAttrs
(old: {
buildCommand = "${old.buildCommand}\n patchShebangs $out";
});
in
{
imports = [ ];
@ -18,4 +32,8 @@
trim.enable = true;
};
environment.systemPackages = with pkgs; [
zfs_clone_send
zfs_clone_recv
];
}

View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
#
# A zfs-send alias that creates a perfect clone with good defaults.
set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# -s if the stream is interrupted, save the partial stream. The stream can then be resumed by doing a zfs send -t token where token is the receive_resume_token prop on the dataset we received into.
# -u Do not mount the filesystem we are receiving. We can always mount afterwards but this avoids issues with streams with mountpoints to places like /
# Can optionally add -F to destroy the dataset in the recv location.
exec zfs recv -s -u "${@}"
# To delete an interrupted recv, run `zfs receive -A dataset`

View File

@ -0,0 +1,8 @@
#!/usr/bin/env bash
#
# A zfs-send alias that creates a perfect clone with good defaults.
set -euo pipefail
IFS=$'\n\t'
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
exec zfs send --compressed --replicate --large-block --embed --verbose --raw "${@}"

View File

@ -0,0 +1,15 @@
{ config, lib, ... }:
let
inherit (builtins) elem;
inherit (lib) getName mkOption;
inherit (lib.types) listOf str;
in
{
# Pending https://github.com/NixOS/nixpkgs/issues/55674
options.allowedUnfree = mkOption {
type = listOf str;
default = [ ];
};
config.nixpkgs.config.allowUnfreePredicate = p: elem (getName p) config.allowedUnfree;
}