From 6ce280bb9c2df926a50826232c2ac910b08c43ae Mon Sep 17 00:00:00 2001 From: djairoh Date: Tue, 23 Apr 2024 23:59:52 +0200 Subject: [PATCH] feat: mangled cfg/data structs so the player_prefixes field now accepts strings as well, allowing for more customization --- src/main.rs | 131 +++++++------- src/print_text.rs | 2 +- src/structs/config.rs | 384 +++++++++++++++++++++++------------------- src/structs/data.rs | 24 +-- src/update_message.rs | 4 + src/update_players.rs | 18 +- 6 files changed, 304 insertions(+), 259 deletions(-) diff --git a/src/main.rs b/src/main.rs index 090be4c..e875f00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,56 +1,54 @@ //! This file contains all driver code for the program. -use core::time; -use std::ffi::OsString; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::thread; +use crate::print_players::print_players; +use crate::print_text::print_text; +use crate::update_message::update_message; +use crate::update_players::update_players; use clap::Parser; +use core::time; use log::{error, info, warn}; use mpris::PlayerFinder; +use std::ffi::OsString; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::thread; use structs::cli::Cli; use structs::{config::Config, data::Data}; -use crate::update_players::update_players; -use crate::update_message::update_message; -use crate::print_text::print_text; -use crate::print_players::print_players; -mod update_players; -mod update_message; +mod print_players; mod print_text; mod structs; -mod print_players; +mod update_message; +mod update_players; /// This function deals with an incoming (USR1) signal. /// It is hard-coded to play/pause the active player. -/// +/// /// input: /// data: Data struct for active configuration. fn handle_signal(data: &Data) { - if let Some(p) = &data.current_player { - match p.checked_play_pause() { - Ok(b) => { - match b { - true => info!("Player play/paused succesfully!"), - false => warn!("Failed to send play/pause signal!"), + if let Some(p) = &data.current_player { + match p.checked_play_pause() { + Ok(b) => match b { + true => info!("Player play/paused succesfully!"), + false => warn!("Failed to send play/pause signal!"), + }, + Err(e) => error!("{e}"), } - }, - Err(e) => error!("{e}"), } - } } /// This function contains the default maim loop body of the program. /// It updates the active player, updates the output strings based on this, and finally formats and outputs these strings to stdout. -/// +/// /// input: /// pf: PlayerFinder instance for the program /// cfg: Configuration of the program /// data: mutable Data struct, active state of the program /// r: pre-computed rating strings fn default_loop(pf: &PlayerFinder, cfg: &Config, data: &mut Data, r: &Vec) { - update_players(pf, cfg, data); - update_message(cfg, data, r); - print_text(cfg, data); + update_players(pf, cfg, data); + update_message(cfg, data, r); + print_text(cfg, data); } /// Main function. Mostly concerned with initialisation. @@ -61,48 +59,55 @@ fn main() { // logging initialisation std::env::set_var::<&str, OsString>("RUST_LOG", cli.log_level.into()); if let Err(e) = env_logger::init() { - error!("{e}"); - return + error!("{e}"); + return; } // Config, Data, and PlayerFinder initialisation match confy::load::("polybar-now-playing", cli.config_file.as_str()) { - Ok(cfg) => { - let mut data: Data = Data::default(); - let rating_strings = cfg.build_rating_strings(); - - let pf: PlayerFinder; - match PlayerFinder::new() { - Ok(finder) => pf = finder, - Err(e) => { - error!("{e}"); - return - }, - } + Ok(mut cfg) => { + if let None = cfg.player_prefixes.get("default") { + cfg.player_prefixes + .insert("default".to_owned(), ">".to_owned()); + } - // signal interception initialisation - let term = Arc::new(AtomicBool::new(false)); - if let Err(e) = signal_hook::flag::register(signal_hook::consts::SIGUSR1, Arc::clone(&term)) { - error!("{e}"); - return - } + let mut data: Data = Data::default(); + let rating_strings = cfg.build_rating_strings(); - // main body loop - loop { - thread::sleep(time::Duration::from_millis(cfg.update_delay)); - match cli.list { - true => print_players(&pf), - false => default_loop(&pf, &cfg, &mut data, &rating_strings), - } - - if term.load(Ordering::Relaxed) { - handle_signal(&data); - term.swap(false, Ordering::Relaxed); - }; + let pf: PlayerFinder; + match PlayerFinder::new() { + Ok(finder) => pf = finder, + Err(e) => { + error!("{e}"); + return; + } + } + + // signal interception initialisation + let term = Arc::new(AtomicBool::new(false)); + if let Err(e) = + signal_hook::flag::register(signal_hook::consts::SIGUSR1, Arc::clone(&term)) + { + error!("{e}"); + return; + } + + // main body loop + loop { + thread::sleep(time::Duration::from_millis(cfg.update_delay)); + match cli.list { + true => print_players(&pf), + false => default_loop(&pf, &cfg, &mut data, &rating_strings), + } + + if term.load(Ordering::Relaxed) { + handle_signal(&data); + term.swap(false, Ordering::Relaxed); + }; + } } - }, - Err(e) => { - error!("{e}"); - }, - }; + Err(e) => { + error!("{e}"); + } + }; } diff --git a/src/print_text.rs b/src/print_text.rs index a80e54e..9016efb 100644 --- a/src/print_text.rs +++ b/src/print_text.rs @@ -72,7 +72,7 @@ fn cutoff( /// b: mutable String builder to append to. /// data: Data struct containing the current prefix character. fn append_prefix(b: &mut Builder, data: &Data) { - b.append(data.prefix); + b.append(data.prefix.to_owned()); b.append(" "); } diff --git a/src/structs/config.rs b/src/structs/config.rs index 25c3285..411baf9 100644 --- a/src/structs/config.rs +++ b/src/structs/config.rs @@ -1,101 +1,139 @@ //! This file contains structs and functions concerning themselves with the configuration of the program. +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use serde::{Serialize, Deserialize}; /// This struct represents one metadata field to be rendered, as well as the maximum length of its' output. /// There is also support for custom formatting. #[derive(Serialize, Deserialize)] pub struct Field { - /// The name of the metadata field. - pub field: String, - /// The maximum length of the metadata field's output. - pub num_chars: u32, - /// Formatting to apply. (the value "{}" is substituted with the actual string) - pub format: String + /// The name of the metadata field. + pub field: String, + /// The maximum length of the metadata field's output. + pub num_chars: u32, + /// Formatting to apply. (the value "{}" is substituted with the actual string) + pub format: String, } impl Field { - /// Create a new field from given values. - /// input: - /// field: name of the field - /// num_chars: maximum length of the field - /// format: what formatting to apply to the field - /// - /// returns: - /// a new Field with the given parameters. - fn new(field: String, num_chars: u32, format: String) -> Self { - Field { - field, - num_chars, - format + /// Create a new field from given values. + /// input: + /// field: name of the field + /// num_chars: maximum length of the field + /// format: what formatting to apply to the field + /// + /// returns: + /// a new Field with the given parameters. + fn new(field: String, num_chars: u32, format: String) -> Self { + Field { + field, + num_chars, + format, + } } - } - /// Create a new field from given values. - /// input: - /// field: name of the field - /// num_chars: maximum length of the field - /// format: (optional), formatting to apply. - /// - /// returns: - /// a new Field with the given parameters. - pub fn constructor(field: &str, num_chars: u32, format: Option) -> Self { - if let Some(format) = format { - Self::new(field.to_owned(), num_chars, format) - } else { - Self::new(field.to_owned(), num_chars, "{}".to_owned()) + /// Create a new field from given values. + /// input: + /// field: name of the field + /// num_chars: maximum length of the field + /// format: (optional), formatting to apply. + /// + /// returns: + /// a new Field with the given parameters. + pub fn constructor(field: &str, num_chars: u32, format: Option) -> Self { + if let Some(format) = format { + Self::new(field.to_owned(), num_chars, format) + } else { + Self::new(field.to_owned(), num_chars, "{}".to_owned()) + } } - } } - /// This struct contains the 3 symbols used to represent a given userRating in a media field. #[derive(Serialize, Deserialize)] pub struct Rating { - /// character for an empty token - pub nil: char, - /// character for a half token - pub half: char, - /// character for a full token - pub full: char + /// character for an empty token + pub nil: char, + /// character for a half token + pub half: char, + /// character for a full token + pub full: char, } impl Rating { - /// This function repeats a given character n times, interspersing each occurence with a space. - /// It's kinda unwieldy here, but this is the least odd place to put it. - /// - /// input: - /// c: character to repeat - /// n: number of times to repeat the character - /// - /// returns: - /// string of the form '\ '{n} - fn repeat(c: char, n: usize) -> String { - let mut s = c.to_string(); - s.push(' '); - s.repeat(n) - } + /// This function repeats a given character n times, interspersing each occurence with a space. + /// It's kinda unwieldy here, but this is the least odd place to put it. + /// + /// input: + /// c: character to repeat + /// n: number of times to repeat the character + /// + /// returns: + /// string of the form '\ '{n} + fn repeat(c: char, n: usize) -> String { + let mut s = c.to_string(); + s.push(' '); + s.repeat(n) + } - /// As there are only a small, run-time defined variances on possible ratings (from 5 empty tokens to 5 full ones), - /// this function computes all these strings during initialization. This saves a near negligble amount of operations during run-time. - /// - /// output: - /// Vec of Strings representing all possible rating configurations - fn build_rating_strings(&self) -> Vec { - let mut out = Vec::new(); - out.push(Self::repeat(self.nil, 5)); - out.push(format!("{}{}", Self::repeat(self.half, 1), Self::repeat(self.nil, 4))); - out.push(format!("{}{}", Self::repeat(self.full, 1), Self::repeat(self.nil, 4))); - out.push(format!("{}{}{}", Self::repeat(self.full, 1), Self::repeat(self.half, 1), Self::repeat(self.nil, 3))); - out.push(format!("{}{}", Self::repeat(self.full, 2), Self::repeat(self.nil, 3))); - out.push(format!("{}{}{}", Self::repeat(self.full, 2), Self::repeat(self.half, 1), Self::repeat(self.nil, 2))); - out.push(format!("{}{}", Self::repeat(self.full, 3), Self::repeat(self.nil, 2))); - out.push(format!("{}{}{}", Self::repeat(self.full, 3), Self::repeat(self.half, 1), Self::repeat(self.nil, 1))); - out.push(format!("{}{}", Self::repeat(self.full, 4), Self::repeat(self.nil, 1))); - out.push(format!("{}{}", Self::repeat(self.full, 4), Self::repeat(self.half, 1))); - out.push(Self::repeat(self.full, 5)); - out - } + /// As there are only a small, run-time defined variances on possible ratings (from 5 empty tokens to 5 full ones), + /// this function computes all these strings during initialization. This saves a near negligble amount of operations during run-time. + /// + /// output: + /// Vec of Strings representing all possible rating configurations + fn build_rating_strings(&self) -> Vec { + let mut out = Vec::new(); + out.push(Self::repeat(self.nil, 5)); + out.push(format!( + "{}{}", + Self::repeat(self.half, 1), + Self::repeat(self.nil, 4) + )); + out.push(format!( + "{}{}", + Self::repeat(self.full, 1), + Self::repeat(self.nil, 4) + )); + out.push(format!( + "{}{}{}", + Self::repeat(self.full, 1), + Self::repeat(self.half, 1), + Self::repeat(self.nil, 3) + )); + out.push(format!( + "{}{}", + Self::repeat(self.full, 2), + Self::repeat(self.nil, 3) + )); + out.push(format!( + "{}{}{}", + Self::repeat(self.full, 2), + Self::repeat(self.half, 1), + Self::repeat(self.nil, 2) + )); + out.push(format!( + "{}{}", + Self::repeat(self.full, 3), + Self::repeat(self.nil, 2) + )); + out.push(format!( + "{}{}{}", + Self::repeat(self.full, 3), + Self::repeat(self.half, 1), + Self::repeat(self.nil, 1) + )); + out.push(format!( + "{}{}", + Self::repeat(self.full, 4), + Self::repeat(self.nil, 1) + )); + out.push(format!( + "{}{}", + Self::repeat(self.full, 4), + Self::repeat(self.half, 1) + )); + out.push(Self::repeat(self.full, 5)); + out + } } /// Defaults for Rating struct. @@ -103,126 +141,124 @@ impl Rating { impl Default for Rating { fn default() -> Self { Self { - nil: '-', - half: '/', - full: '+' + nil: '-', + half: '/', + full: '+', } } } - -/// This struct contains all possible configuration fields. +/// This struct contains all possible configuration fields. /// It should not be used as mutable; all data in this struct should effectively be treated as read-only. #[derive(Serialize, Deserialize)] pub struct Config { - /// Whether to hide the last output if there are currently no accepted players. - pub hide_output: bool, - /// Whether to apply 'fuzzy' cutoff to strings exceeding their maximum lenght. - pub fuzzy: bool, - /// Whether to render the prefix at all. - pub render_prefix: bool, - /// Time in milliseconds to wait between loops of the program. - pub update_delay: u64, - /// String to insert between different metadata fields. - pub metadata_separator: String, - /// Character to insert between Array values (used when a MetadataVaue is of type Vec (ie multiple artists on one track)). - pub array_separator: char, - /// Character to insert when a string is truncated. None implies no cut off character is inserted and the strings are truncated as is. - pub break_character: Option, - /// Hashmap of mpris identities, describing what players are considered acceptable. - pub player_priorities: HashMap, - /// Characters to use for the xesam:userRating field. - /// If None, default values are used ('-', '/', '+'). - pub rating_icons: Option, - /// Vec of Fields. Each field represents one metadata_string to be shown in output, as well as the maximum number of characters for this field. - /// Output is shown based on Vec index (vec\[0\] first, vec\[1\] second, etc). - pub metadata_fields: Vec, - /// Hashmap which maps Player Identities (strings; key) to prefixes (char; value). - /// If left blank all players will use the default prefix character ('>'). - pub player_prefixes: HashMap, - /// Boolean which tells the program to escape special characters or not. - /// This is useful for some bar implementations (i.e. waybar needs to escape the '&' character). - /// Currently only escapes '&', i will be adding more as i run into them. - pub escape_chars: bool, + /// Whether to hide the last output if there are currently no accepted players. + pub hide_output: bool, + /// Whether to apply 'fuzzy' cutoff to strings exceeding their maximum lenght. + pub fuzzy: bool, + /// Whether to render the prefix at all. + pub render_prefix: bool, + /// Time in milliseconds to wait between loops of the program. + pub update_delay: u64, + /// String to insert between different metadata fields. + pub metadata_separator: String, + /// Character to insert between Array values (used when a MetadataVaue is of type Vec (ie multiple artists on one track)). + pub array_separator: char, + /// Character to insert when a string is truncated. None implies no cut off character is inserted and the strings are truncated as is. + pub break_character: Option, + /// Hashmap of mpris identities, describing what players are considered acceptable. + pub player_priorities: HashMap, + /// Characters to use for the xesam:userRating field. + /// If None, default values are used ('-', '/', '+'). + pub rating_icons: Option, + /// Vec of Fields. Each field represents one metadata_string to be shown in output, as well as the maximum number of characters for this field. + /// Output is shown based on Vec index (vec\[0\] first, vec\[1\] second, etc). + pub metadata_fields: Vec, + /// Hashmap which maps Player Identities (strings; key) to prefixes (char; value). + /// If left blank all players will use the default prefix character ('>'). + pub player_prefixes: HashMap, + /// Boolean which tells the program to escape special characters or not. + /// This is useful for some bar implementations (i.e. waybar needs to escape the '&' character). + /// Currently only escapes '&', i will be adding more as i run into them. + pub escape_chars: bool, } /// Defaults for the Config struct. /// This is generated when a non-existant config file is specified in the command line. impl Default for Config { - fn default() -> Self { - Config { - hide_output: true, - fuzzy: false, - render_prefix: true, - update_delay: 300_u64, - metadata_separator: " | ".to_owned(), - array_separator: '+', - break_character: Some('-'), - player_priorities: Config::default_player_priorities(), - rating_icons: Some(Rating::default()), - metadata_fields: Config::default_metadata_fields(), - player_prefixes: Config::default_player_prefixes(), - escape_chars: false, - } - } + fn default() -> Self { + Config { + hide_output: true, + fuzzy: false, + render_prefix: true, + update_delay: 300_u64, + metadata_separator: " | ".to_owned(), + array_separator: '+', + break_character: Some('-'), + player_priorities: Config::default_player_priorities(), + rating_icons: Some(Rating::default()), + metadata_fields: Config::default_metadata_fields(), + player_prefixes: Config::default_player_prefixes(), + escape_chars: false, + } + } } impl Config { - /// This function returns the index of a given player identity in the player_priorities hashmap. - /// If the given identity is not in the map, the value of i32::MAX is returned instead. - pub fn find_player_priorities_idx(&self, name: &str) -> u8 { - match self.player_priorities.get(name) { - Some(val) => *val, - None => u8::MAX, + /// This function returns the index of a given player identity in the player_priorities hashmap. + /// If the given identity is not in the map, the value of i32::MAX is returned instead. + pub fn find_player_priorities_idx(&self, name: &str) -> u8 { + match self.player_priorities.get(name) { + Some(val) => *val, + None => u8::MAX, + } } - } - /// This function builds the pre-computed rating strings for a given Rating_icons field. - pub fn build_rating_strings(&self) -> Vec { - match self.rating_icons.as_ref() { - Some(r) => r.build_rating_strings(), - None => Rating::default().build_rating_strings(), + /// This function builds the pre-computed rating strings for a given Rating_icons field. + pub fn build_rating_strings(&self) -> Vec { + match self.rating_icons.as_ref() { + Some(r) => r.build_rating_strings(), + None => Rating::default().build_rating_strings(), + } } - } - /// This function returns the default player_priorities, used when a non-existent config file is requested. - /// The values of these are based on nothing but my own experience; in fact I'm not even sure if the Spotify app's identity is correct. - fn default_player_priorities() -> HashMap< String, u8> { - let mut out = HashMap::new(); + /// This function returns the default player_priorities, used when a non-existent config file is requested. + /// The values of these are based on nothing but my own experience; in fact I'm not even sure if the Spotify app's identity is correct. + fn default_player_priorities() -> HashMap { + let mut out = HashMap::new(); - out.insert("Clementine".to_owned(), 1); - out.insert("Spotify".to_owned(), 2); - out.insert("mpv".to_owned(), 3); - out.insert("VLC Media Player".to_owned(), 4); - out.insert("Firefox".to_owned(), 5); - out.insert("Chromium".to_owned(), 6); - - out - } + out.insert("Clementine".to_owned(), 1); + out.insert("Spotify".to_owned(), 2); + out.insert("mpv".to_owned(), 3); + out.insert("VLC Media Player".to_owned(), 4); + out.insert("Firefox".to_owned(), 5); + out.insert("Chromium".to_owned(), 6); - /// This function returns the default metadata fields, used when a non-existent config file is requested. - /// It contains the "title" and "artist" fields, with 40 and 20 maximum characters respectively. - fn default_metadata_fields() -> Vec { - vec![ - Field::constructor("xesam:title", 40, None), - Field::constructor("xesam:artist", 20, None) - ] - } + out + } - /// This function returns the default prefixes, used when a non-existent config file is requested. - /// Like the player priorities function, this is mostly just based on my own experience. - fn default_player_prefixes() -> HashMap { - let mut out: HashMap = HashMap::new(); + /// This function returns the default metadata fields, used when a non-existent config file is requested. + /// It contains the "title" and "artist" fields, with 40 and 20 maximum characters respectively. + fn default_metadata_fields() -> Vec { + vec![ + Field::constructor("xesam:title", 40, None), + Field::constructor("xesam:artist", 20, None), + ] + } - out.insert("chromium".to_owned(), 'g'); - out.insert("Clementine".to_owned(), 'c'); - out.insert("default".to_owned(), '>'); - out.insert("Firefox".to_owned(), 'f'); - out.insert("mpv".to_owned(), 'm'); - out.insert("Spotify".to_owned(), 's'); - out.insert("VLC Media Player".to_owned(), 'v'); + /// This function returns the default prefixes, used when a non-existent config file is requested. + /// Like the player priorities function, this is mostly just based on my own experience. + fn default_player_prefixes() -> HashMap { + let mut out: HashMap = HashMap::new(); - out - } + out.insert("chromium".to_owned(), "g".to_owned()); + out.insert("Clementine".to_owned(), "c".to_owned()); + out.insert("default".to_owned(), ">".to_owned()); + out.insert("Firefox".to_owned(), "f".to_owned()); + out.insert("mpv".to_owned(), "m".to_owned()); + out.insert("Spotify".to_owned(), "s".to_owned()); + out.insert("VLC Media Player".to_owned(), "v".to_owned()); + + out + } } - diff --git a/src/structs/data.rs b/src/structs/data.rs index d208661..8dfdccb 100644 --- a/src/structs/data.rs +++ b/src/structs/data.rs @@ -6,23 +6,23 @@ use mpris::Player; /// This struct concerns itself with the current state of the program. pub struct Data { - /// Represents the media player marked as active. - /// Should be None when no (accepted) players are active. - pub current_player: Option, - /// HashMap representing the current output strings for each configured field. - pub field_text: HashMap, - /// What character to render as prefix. - pub prefix: char, + /// Represents the media player marked as active. + /// Should be None when no (accepted) players are active. + pub current_player: Option, + /// HashMap representing the current output strings for each configured field. + pub field_text: HashMap, + /// What character to render as prefix. + pub prefix: String, } /// Defaults for Data struct. /// Generates an empty hashmap, and prefix, as well as None for current_player. impl Default for Data { fn default() -> Self { - Self { - current_player: None, - field_text: HashMap::new(), - prefix: ' ', + Self { + current_player: None, + field_text: HashMap::new(), + prefix: "".to_owned(), } } -} \ No newline at end of file +} diff --git a/src/update_message.rs b/src/update_message.rs index 3306d50..9259608 100644 --- a/src/update_message.rs +++ b/src/update_message.rs @@ -114,6 +114,10 @@ pub fn update_message(cfg: &Config, data: &mut Data, ratings: &Vec) { } None => { trace!("update_messages: field {} has no value!", key); + if field.field.eq("bs:isFavorite") { + data.field_text.remove(key); + continue; + } data.field_text.insert( key.to_owned(), format!("No {}", key.trim_start_matches("xesam:")), diff --git a/src/update_players.rs b/src/update_players.rs index 37c1b4a..7b76169 100644 --- a/src/update_players.rs +++ b/src/update_players.rs @@ -13,13 +13,13 @@ use mpris::PlayerFinder; /// cfg: Config struct for the program, containing the hashmap of prefixes. /// data: mutable char containing the active prefix. /// name: name of active player, to fetch the appropriate prefix from cfg. -fn update_prefix(cfg: &Config, data: &mut char, name: &str) { +fn update_prefix(cfg: &Config, data: &mut Data, name: &str) { if let Some(char) = cfg.player_prefixes.get(name) { - *data = *char; - trace!("updated prefix to {}", data); + data.prefix = char.to_owned(); + trace!("updated prefix to {}", data.prefix); } else { - *data = *cfg.player_prefixes.get("default").unwrap_or(&'>'); - trace!("set prefix to default ({})", data); + data.prefix = cfg.player_prefixes.get("default").unwrap().to_owned(); + trace!("set prefix to default ({})", data.prefix); } } @@ -38,14 +38,14 @@ pub fn update_players(pf: &PlayerFinder, cfg: &Config, data: &mut Data) { data.current_player = None; debug!("update_players: no players found!") } else { - let mut trees = vec![BTreeMap::new(), BTreeMap::new(), BTreeMap::new()]; + let mut trees = vec![BTreeMap::new(), BTreeMap::new()]; for player in players { if let Ok(status) = player.get_playback_status() { let idx = cfg.find_player_priorities_idx(player.identity()); match status { mpris::PlaybackStatus::Playing => trees[0].insert(idx, player), - mpris::PlaybackStatus::Paused => trees[1].insert(idx, player), - mpris::PlaybackStatus::Stopped => trees[2].insert(idx, player), + mpris::PlaybackStatus::Paused => trees[0].insert(idx, player), + mpris::PlaybackStatus::Stopped => trees[1].insert(idx, player), }; } } @@ -53,7 +53,7 @@ pub fn update_players(pf: &PlayerFinder, cfg: &Config, data: &mut Data) { // select the player with the highest priority. for mut tree in trees { if let Some((_, player)) = tree.pop_first() { - update_prefix(cfg, &mut data.prefix, player.identity()); + update_prefix(cfg, data, player.identity()); debug!("update_players: updated player to {}!", player.identity()); data.current_player = Some(player); break;