Files
niri/src/config.rs
T

735 lines
21 KiB
Rust
Raw Normal View History

2023-09-05 12:58:51 +04:00
use std::path::PathBuf;
use std::str::FromStr;
use bitflags::bitflags;
use directories::ProjectDirs;
use miette::{miette, Context, IntoDiagnostic};
2023-09-24 11:04:30 +04:00
use smithay::input::keyboard::keysyms::KEY_NoSymbol;
use smithay::input::keyboard::xkb::{keysym_from_name, KEYSYM_CASE_INSENSITIVE};
2023-09-05 12:58:51 +04:00
use smithay::input::keyboard::Keysym;
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Config {
#[knuffel(child, default)]
pub input: Input,
#[knuffel(children(name = "output"))]
pub outputs: Vec<Output>,
2023-09-21 19:58:03 +04:00
#[knuffel(children(name = "spawn-at-startup"))]
pub spawn_at_startup: Vec<SpawnAtStartup>,
2023-09-05 12:58:51 +04:00
#[knuffel(child, default)]
2023-09-26 13:09:33 +04:00
pub focus_ring: FocusRing,
#[knuffel(child, default)]
2023-09-26 13:44:37 +04:00
pub prefer_no_csd: bool,
#[knuffel(child, default)]
2023-10-01 17:42:56 +04:00
pub cursor: Cursor,
#[knuffel(child, unwrap(children), default)]
pub preset_column_widths: Vec<PresetWidth>,
2023-10-07 17:45:55 +04:00
#[knuffel(child, unwrap(argument), default = 16)]
pub gaps: u16,
2023-10-31 08:57:44 +04:00
#[knuffel(child, unwrap(argument), default = Some(PathBuf::from("~/Pictures/Screenshots")))]
pub screenshot_path: Option<PathBuf>,
2023-10-01 17:42:56 +04:00
#[knuffel(child, default)]
2023-09-05 12:58:51 +04:00
pub binds: Binds,
2023-09-06 15:49:46 +04:00
#[knuffel(child, default)]
pub debug: DebugConfig,
2023-09-05 12:58:51 +04:00
}
// FIXME: Add other devices.
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Input {
#[knuffel(child, default)]
pub keyboard: Keyboard,
#[knuffel(child, default)]
pub touchpad: Touchpad,
2023-10-03 17:02:07 +04:00
#[knuffel(child, default)]
pub tablet: Tablet,
2023-09-05 12:58:51 +04:00
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
pub struct Keyboard {
#[knuffel(child, default)]
pub xkb: Xkb,
2023-09-16 20:01:52 +04:00
// The defaults were chosen to match wlroots and sway.
#[knuffel(child, unwrap(argument), default = 600)]
pub repeat_delay: u16,
#[knuffel(child, unwrap(argument), default = 25)]
pub repeat_rate: u8,
2023-09-05 12:58:51 +04:00
}
#[derive(knuffel::Decode, Debug, Default, PartialEq, Eq)]
pub struct Xkb {
#[knuffel(child, unwrap(argument), default)]
pub rules: String,
#[knuffel(child, unwrap(argument), default)]
pub model: String,
#[knuffel(child, unwrap(argument))]
pub layout: Option<String>,
#[knuffel(child, unwrap(argument), default)]
pub variant: String,
#[knuffel(child, unwrap(argument))]
pub options: Option<String>,
}
// FIXME: Add the rest of the settings.
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Touchpad {
#[knuffel(child)]
pub tap: bool,
#[knuffel(child)]
pub natural_scroll: bool,
#[knuffel(child, unwrap(argument), default)]
pub accel_speed: f64,
}
2023-10-03 17:02:07 +04:00
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
pub struct Tablet {
#[knuffel(child, unwrap(argument))]
pub map_to_output: Option<String>,
}
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
pub struct Output {
#[knuffel(argument)]
pub name: String,
#[knuffel(child, unwrap(argument), default = 1.)]
pub scale: f64,
2023-09-30 11:33:02 +04:00
#[knuffel(child)]
pub position: Option<Position>,
2023-10-03 08:35:24 +04:00
#[knuffel(child, unwrap(argument, str))]
pub mode: Option<Mode>,
}
impl Default for Output {
fn default() -> Self {
Self {
name: String::new(),
scale: 1.,
2023-09-30 11:33:02 +04:00
position: None,
2023-10-03 08:35:24 +04:00
mode: None,
}
}
}
2023-09-30 11:33:02 +04:00
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct Position {
#[knuffel(property)]
pub x: i32,
#[knuffel(property)]
pub y: i32,
}
2023-10-03 08:35:24 +04:00
#[derive(Debug, Clone, PartialEq)]
pub struct Mode {
pub width: u16,
pub height: u16,
pub refresh: Option<f64>,
}
2023-09-21 19:58:03 +04:00
#[derive(knuffel::Decode, Debug, Clone, PartialEq, Eq)]
pub struct SpawnAtStartup {
#[knuffel(arguments)]
pub command: Vec<String>,
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
2023-09-26 13:09:33 +04:00
pub struct FocusRing {
#[knuffel(child)]
pub off: bool,
#[knuffel(child, unwrap(argument), default = 4)]
pub width: u16,
#[knuffel(child, default = Color::new(127, 200, 255, 255))]
2023-09-26 13:09:33 +04:00
pub active_color: Color,
#[knuffel(child, default = Color::new(80, 80, 80, 255))]
2023-09-26 13:09:33 +04:00
pub inactive_color: Color,
}
impl Default for FocusRing {
fn default() -> Self {
Self {
off: false,
width: 4,
active_color: Color::new(127, 200, 255, 255),
inactive_color: Color::new(80, 80, 80, 255),
2023-09-26 13:09:33 +04:00
}
}
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq, Eq)]
2023-09-26 13:09:33 +04:00
pub struct Color {
#[knuffel(argument)]
pub r: u8,
2023-09-26 13:09:33 +04:00
#[knuffel(argument)]
pub g: u8,
2023-09-26 13:09:33 +04:00
#[knuffel(argument)]
pub b: u8,
2023-09-26 13:09:33 +04:00
#[knuffel(argument)]
pub a: u8,
2023-09-26 13:09:33 +04:00
}
impl Color {
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
2023-09-26 13:09:33 +04:00
Self { r, g, b, a }
}
}
impl From<Color> for [f32; 4] {
fn from(c: Color) -> Self {
[c.r, c.g, c.b, c.a].map(|x| x as f32 / 255.)
2023-09-26 13:09:33 +04:00
}
}
2023-10-01 17:42:56 +04:00
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct Cursor {
#[knuffel(child, unwrap(argument), default = String::from("default"))]
pub xcursor_theme: String,
#[knuffel(child, unwrap(argument), default = 24)]
pub xcursor_size: u8,
}
impl Default for Cursor {
fn default() -> Self {
Self {
xcursor_theme: String::from("default"),
xcursor_size: 24,
}
}
}
#[derive(knuffel::Decode, Debug, Clone, Copy, PartialEq)]
pub enum PresetWidth {
Proportion(#[knuffel(argument)] f64),
Fixed(#[knuffel(argument)] i32),
}
2023-10-03 11:38:42 +04:00
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
2023-09-05 12:58:51 +04:00
pub struct Binds(#[knuffel(children)] pub Vec<Bind>);
2023-10-03 11:38:42 +04:00
#[derive(knuffel::Decode, Debug, PartialEq)]
2023-09-05 12:58:51 +04:00
pub struct Bind {
#[knuffel(node_name)]
pub key: Key,
#[knuffel(children)]
pub actions: Vec<Action>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Key {
pub keysym: Keysym,
pub modifiers: Modifiers,
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Modifiers : u8 {
const CTRL = 1;
const SHIFT = 2;
const ALT = 4;
const SUPER = 8;
const COMPOSITOR = 16;
}
}
2023-10-03 11:38:42 +04:00
#[derive(knuffel::Decode, Debug, Clone, PartialEq)]
2023-09-05 12:58:51 +04:00
pub enum Action {
Quit,
#[knuffel(skip)]
ChangeVt(i32),
Suspend,
2023-10-09 18:37:43 +04:00
PowerOffMonitors,
2023-09-05 12:58:51 +04:00
ToggleDebugTint,
Spawn(#[knuffel(arguments)] Vec<String>),
2023-10-30 20:29:03 +04:00
#[knuffel(skip)]
ConfirmScreenshot,
#[knuffel(skip)]
CancelScreenshot,
Screenshot,
2023-10-26 16:47:59 +04:00
ScreenshotScreen,
2023-10-10 12:42:24 +04:00
ScreenshotWindow,
2023-09-05 12:58:51 +04:00
CloseWindow,
FullscreenWindow,
FocusColumnLeft,
FocusColumnRight,
FocusWindowDown,
FocusWindowUp,
MoveColumnLeft,
MoveColumnRight,
MoveWindowDown,
MoveWindowUp,
ConsumeWindowIntoColumn,
ExpelWindowFromColumn,
FocusWorkspaceDown,
FocusWorkspaceUp,
2023-09-16 12:14:02 +04:00
FocusWorkspace(#[knuffel(argument)] u8),
2023-09-05 12:58:51 +04:00
MoveWindowToWorkspaceDown,
MoveWindowToWorkspaceUp,
2023-09-16 12:14:02 +04:00
MoveWindowToWorkspace(#[knuffel(argument)] u8),
2023-10-14 20:42:10 +04:00
MoveWorkspaceDown,
MoveWorkspaceUp,
2023-09-05 12:58:51 +04:00
FocusMonitorLeft,
FocusMonitorRight,
FocusMonitorDown,
FocusMonitorUp,
MoveWindowToMonitorLeft,
MoveWindowToMonitorRight,
MoveWindowToMonitorDown,
MoveWindowToMonitorUp,
SwitchPresetColumnWidth,
MaximizeColumn,
2023-10-03 11:38:42 +04:00
SetColumnWidth(#[knuffel(argument, str)] SizeChange),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SizeChange {
SetFixed(i32),
SetProportion(f64),
AdjustFixed(i32),
AdjustProportion(f64),
2023-09-05 12:58:51 +04:00
}
2023-09-06 15:49:46 +04:00
#[derive(knuffel::Decode, Debug, PartialEq)]
pub struct DebugConfig {
#[knuffel(child, unwrap(argument), default = 1.)]
pub animation_slowdown: f64,
2023-09-08 17:54:02 +04:00
#[knuffel(child)]
pub dbus_interfaces_in_non_session_instances: bool,
2023-09-14 09:33:42 +04:00
#[knuffel(child)]
pub wait_for_frame_completion_before_queueing: bool,
#[knuffel(child)]
pub enable_color_transformations_capability: bool,
2023-09-14 22:43:12 +04:00
#[knuffel(child)]
pub enable_overlay_planes: bool,
2023-09-06 15:49:46 +04:00
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
animation_slowdown: 1.,
dbus_interfaces_in_non_session_instances: false,
2023-09-14 09:33:42 +04:00
wait_for_frame_completion_before_queueing: false,
enable_color_transformations_capability: false,
2023-09-14 22:43:12 +04:00
enable_overlay_planes: false,
2023-09-06 15:49:46 +04:00
}
}
}
2023-09-05 12:58:51 +04:00
impl Config {
2023-09-26 19:24:50 +04:00
pub fn load(path: Option<PathBuf>) -> miette::Result<(Self, PathBuf)> {
2023-09-05 12:58:51 +04:00
let path = if let Some(path) = path {
path
} else {
let mut path = ProjectDirs::from("", "", "niri")
.ok_or_else(|| miette!("error retrieving home directory"))?
.config_dir()
.to_owned();
path.push("config.kdl");
path
};
let contents = std::fs::read_to_string(&path)
.into_diagnostic()
.with_context(|| format!("error reading {path:?}"))?;
let config = Self::parse("config.kdl", &contents).context("error parsing")?;
debug!("loaded config from {path:?}");
2023-09-26 19:24:50 +04:00
Ok((config, path))
2023-09-05 12:58:51 +04:00
}
pub fn parse(filename: &str, text: &str) -> Result<Self, knuffel::Error> {
knuffel::parse(filename, text)
}
}
impl Default for Config {
fn default() -> Self {
Config::parse(
"default-config.kdl",
include_str!("../resources/default-config.kdl"),
)
.unwrap()
}
}
2023-10-03 08:35:24 +04:00
impl FromStr for Mode {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((width, rest)) = s.split_once('x') else {
return Err(miette!("no 'x' separator found"));
};
let (height, refresh) = match rest.split_once('@') {
Some((height, refresh)) => (height, Some(refresh)),
None => (rest, None),
};
let width = width
.parse()
.into_diagnostic()
.context("error parsing width")?;
let height = height
.parse()
.into_diagnostic()
.context("error parsing height")?;
let refresh = refresh
.map(str::parse)
.transpose()
.into_diagnostic()
.context("error parsing refresh rate")?;
Ok(Self {
width,
height,
refresh,
})
}
}
2023-09-05 12:58:51 +04:00
impl FromStr for Key {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut modifiers = Modifiers::empty();
let mut split = s.split('+');
let key = split.next_back().unwrap();
for part in split {
let part = part.trim();
if part.eq_ignore_ascii_case("mod") {
modifiers |= Modifiers::COMPOSITOR
} else if part.eq_ignore_ascii_case("ctrl") || part.eq_ignore_ascii_case("control") {
modifiers |= Modifiers::CTRL;
} else if part.eq_ignore_ascii_case("shift") {
modifiers |= Modifiers::SHIFT;
} else if part.eq_ignore_ascii_case("alt") {
modifiers |= Modifiers::ALT;
} else if part.eq_ignore_ascii_case("super") || part.eq_ignore_ascii_case("win") {
modifiers |= Modifiers::SUPER;
} else {
return Err(miette!("invalid modifier: {part}"));
}
}
let keysym = keysym_from_name(key, KEYSYM_CASE_INSENSITIVE);
2023-09-24 11:04:30 +04:00
if keysym.raw() == KEY_NoSymbol {
2023-09-05 12:58:51 +04:00
return Err(miette!("invalid key: {key}"));
}
Ok(Key { keysym, modifiers })
}
}
2023-10-03 11:38:42 +04:00
impl FromStr for SizeChange {
type Err = miette::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.split_once('%') {
Some((value, empty)) => {
if !empty.is_empty() {
return Err(miette!("trailing characters after '%' are not allowed"));
}
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::AdjustProportion(value))
}
Some(_) => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::SetProportion(value))
}
None => Err(miette!("value is missing")),
}
}
None => {
let value = s;
match value.bytes().next() {
Some(b'-' | b'+') => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::AdjustFixed(value))
}
Some(_) => {
let value = value
.parse()
.into_diagnostic()
.context("error parsing value")?;
Ok(Self::SetFixed(value))
}
None => Err(miette!("value is missing")),
}
}
}
}
}
2023-09-05 12:58:51 +04:00
#[cfg(test)]
mod tests {
2023-10-03 07:41:05 +04:00
use miette::NarratableReportHandler;
2023-09-05 12:58:51 +04:00
use super::*;
#[track_caller]
fn check(text: &str, expected: Config) {
2023-10-03 07:41:05 +04:00
let _ = miette::set_hook(Box::new(|_| Box::new(NarratableReportHandler::new())));
2023-09-05 12:58:51 +04:00
let parsed = Config::parse("test.kdl", text)
.map_err(miette::Report::new)
.unwrap();
assert_eq!(parsed, expected);
}
#[test]
fn parse() {
check(
r#"
input {
keyboard {
2023-09-16 20:01:52 +04:00
repeat-delay 600
repeat-rate 25
2023-09-05 12:58:51 +04:00
xkb {
layout "us,ru"
options "grp:win_space_toggle"
}
}
touchpad {
tap
accel-speed 0.2
}
2023-10-03 17:02:07 +04:00
tablet {
map-to-output "eDP-1"
}
2023-09-05 12:58:51 +04:00
}
output "eDP-1" {
scale 2.0
2023-09-30 11:33:02 +04:00
position x=10 y=20
2023-10-03 08:35:24 +04:00
mode "1920x1080@144"
}
2023-09-21 19:58:03 +04:00
spawn-at-startup "alacritty" "-e" "fish"
2023-09-26 13:09:33 +04:00
focus-ring {
width 5
active-color 0 100 200 255
inactive-color 255 200 100 0
2023-09-26 13:09:33 +04:00
}
2023-09-26 13:44:37 +04:00
prefer-no-csd
2023-10-01 17:42:56 +04:00
cursor {
xcursor-theme "breeze_cursors"
xcursor-size 16
}
preset-column-widths {
proportion 0.25
proportion 0.5
fixed 960
fixed 1280
}
2023-10-07 17:45:55 +04:00
gaps 8
2023-10-31 08:57:44 +04:00
screenshot-path "~/Screenshots"
2023-09-05 12:58:51 +04:00
binds {
Mod+T { spawn "alacritty"; }
Mod+Q { close-window; }
Mod+Shift+H { focus-monitor-left; }
Mod+Ctrl+Shift+L { move-window-to-monitor-right; }
Mod+Comma { consume-window-into-column; }
2023-09-16 12:14:02 +04:00
Mod+1 { focus-workspace 1;}
2023-09-05 12:58:51 +04:00
}
2023-09-06 15:49:46 +04:00
debug {
animation-slowdown 2.0
}
2023-09-05 12:58:51 +04:00
"#,
Config {
input: Input {
keyboard: Keyboard {
xkb: Xkb {
layout: Some("us,ru".to_owned()),
options: Some("grp:win_space_toggle".to_owned()),
..Default::default()
},
2023-09-16 20:01:52 +04:00
repeat_delay: 600,
repeat_rate: 25,
2023-09-05 12:58:51 +04:00
},
touchpad: Touchpad {
tap: true,
natural_scroll: false,
accel_speed: 0.2,
},
2023-10-03 17:02:07 +04:00
tablet: Tablet {
map_to_output: Some("eDP-1".to_owned()),
},
2023-09-05 12:58:51 +04:00
},
outputs: vec![Output {
name: "eDP-1".to_owned(),
scale: 2.,
2023-09-30 11:33:02 +04:00
position: Some(Position { x: 10, y: 20 }),
2023-10-03 08:35:24 +04:00
mode: Some(Mode {
width: 1920,
height: 1080,
refresh: Some(144.),
}),
}],
2023-09-21 19:58:03 +04:00
spawn_at_startup: vec![SpawnAtStartup {
command: vec!["alacritty".to_owned(), "-e".to_owned(), "fish".to_owned()],
}],
2023-09-26 13:09:33 +04:00
focus_ring: FocusRing {
off: false,
width: 5,
active_color: Color {
r: 0,
g: 100,
b: 200,
a: 255,
2023-09-26 13:09:33 +04:00
},
inactive_color: Color {
r: 255,
g: 200,
b: 100,
a: 0,
2023-09-26 13:09:33 +04:00
},
},
2023-09-26 13:44:37 +04:00
prefer_no_csd: true,
2023-10-01 17:42:56 +04:00
cursor: Cursor {
xcursor_theme: String::from("breeze_cursors"),
xcursor_size: 16,
},
preset_column_widths: vec![
PresetWidth::Proportion(0.25),
PresetWidth::Proportion(0.5),
PresetWidth::Fixed(960),
PresetWidth::Fixed(1280),
],
2023-10-07 17:45:55 +04:00
gaps: 8,
2023-10-31 08:57:44 +04:00
screenshot_path: Some(PathBuf::from("~/Screenshots")),
2023-09-05 12:58:51 +04:00
binds: Binds(vec![
Bind {
key: Key {
2023-09-24 11:04:30 +04:00
keysym: Keysym::t,
2023-09-05 12:58:51 +04:00
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::Spawn(vec!["alacritty".to_owned()])],
},
Bind {
key: Key {
2023-09-24 11:04:30 +04:00
keysym: Keysym::q,
2023-09-05 12:58:51 +04:00
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::CloseWindow],
},
Bind {
key: Key {
2023-09-24 11:04:30 +04:00
keysym: Keysym::h,
2023-09-05 12:58:51 +04:00
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT,
},
actions: vec![Action::FocusMonitorLeft],
},
Bind {
key: Key {
2023-09-24 11:04:30 +04:00
keysym: Keysym::l,
2023-09-05 12:58:51 +04:00
modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT | Modifiers::CTRL,
},
actions: vec![Action::MoveWindowToMonitorRight],
},
Bind {
key: Key {
2023-09-24 11:04:30 +04:00
keysym: Keysym::comma,
2023-09-05 12:58:51 +04:00
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::ConsumeWindowIntoColumn],
},
2023-09-16 12:14:02 +04:00
Bind {
key: Key {
2023-09-24 11:04:30 +04:00
keysym: Keysym::_1,
2023-09-16 12:14:02 +04:00
modifiers: Modifiers::COMPOSITOR,
},
actions: vec![Action::FocusWorkspace(1)],
},
2023-09-05 12:58:51 +04:00
]),
2023-09-06 15:49:46 +04:00
debug: DebugConfig {
animation_slowdown: 2.,
2023-09-08 17:54:02 +04:00
..Default::default()
2023-09-06 15:49:46 +04:00
},
2023-09-05 12:58:51 +04:00
},
);
}
#[test]
fn can_create_default_config() {
let _ = Config::default();
}
2023-10-03 08:35:24 +04:00
#[test]
fn parse_mode() {
assert_eq!(
"2560x1600@165.004".parse::<Mode>().unwrap(),
Mode {
width: 2560,
height: 1600,
refresh: Some(165.004),
},
);
assert_eq!(
"1920x1080".parse::<Mode>().unwrap(),
Mode {
width: 1920,
height: 1080,
refresh: None,
},
);
assert!("1920".parse::<Mode>().is_err());
assert!("1920x".parse::<Mode>().is_err());
assert!("1920x1080@".parse::<Mode>().is_err());
assert!("1920x1080@60Hz".parse::<Mode>().is_err());
}
2023-10-03 11:38:42 +04:00
#[test]
fn parse_size_change() {
assert_eq!(
"10".parse::<SizeChange>().unwrap(),
SizeChange::SetFixed(10),
);
assert_eq!(
"+10".parse::<SizeChange>().unwrap(),
SizeChange::AdjustFixed(10),
);
assert_eq!(
"-10".parse::<SizeChange>().unwrap(),
SizeChange::AdjustFixed(-10),
);
assert_eq!(
"10%".parse::<SizeChange>().unwrap(),
SizeChange::SetProportion(10.),
);
assert_eq!(
"+10%".parse::<SizeChange>().unwrap(),
SizeChange::AdjustProportion(10.),
);
assert_eq!(
"-10%".parse::<SizeChange>().unwrap(),
SizeChange::AdjustProportion(-10.),
);
assert!("-".parse::<SizeChange>().is_err());
assert!("10% ".parse::<SizeChange>().is_err());
}
2023-09-05 12:58:51 +04:00
}