-
-
Notifications
You must be signed in to change notification settings - Fork 211
/
config.rs
367 lines (328 loc) · 12.1 KB
/
config.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
use std::collections::HashMap;
use std::error::Error;
use std::path::PathBuf;
use std::sync::{RwLock, RwLockReadGuard};
use std::{fs, process};
use cursive::theme::Theme;
use log::{debug, error};
use ncspot::{CONFIGURATION_FILE_NAME, USER_STATE_FILE_NAME};
use platform_dirs::AppDirs;
use crate::command::{SortDirection, SortKey};
use crate::model::playable::Playable;
use crate::queue;
use crate::serialization::{Serializer, CBOR, TOML};
pub const CLIENT_ID: &str = "d420a117a32841c2b3474932e49fb54b";
pub const CACHE_VERSION: u16 = 1;
pub const DEFAULT_COMMAND_KEY: char = ':';
/// The playback state when ncspot is started.
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum PlaybackState {
Playing,
Paused,
Stopped,
Default,
}
/// The focussed library tab when ncspot is started.
#[derive(Clone, Serialize, Deserialize, Debug, Hash, strum_macros::EnumIter)]
#[serde(rename_all = "lowercase")]
pub enum LibraryTab {
Tracks,
Albums,
Artists,
Playlists,
Podcasts,
Browse,
}
/// The format used to represent tracks in a list.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct TrackFormat {
pub left: Option<String>,
pub center: Option<String>,
pub right: Option<String>,
}
impl TrackFormat {
pub fn default() -> Self {
Self {
left: Some(String::from("%artists - %title")),
center: Some(String::from("%album")),
right: Some(String::from("%saved %duration")),
}
}
}
/// The format used when sending desktop notifications about playback status.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct NotificationFormat {
pub title: Option<String>,
pub body: Option<String>,
}
impl NotificationFormat {
pub fn default() -> Self {
Self {
title: Some(String::from("%title")),
body: Some(String::from("%artists")),
}
}
}
/// The configuration of ncspot.
#[derive(Clone, Serialize, Deserialize, Debug, Default)]
pub struct ConfigValues {
pub command_key: Option<char>,
pub initial_screen: Option<String>,
pub default_keybindings: Option<bool>,
pub keybindings: Option<HashMap<String, String>>,
pub theme: Option<ConfigTheme>,
pub use_nerdfont: Option<bool>,
pub flip_status_indicators: Option<bool>,
pub audio_cache: Option<bool>,
pub audio_cache_size: Option<u32>,
pub backend: Option<String>,
pub backend_device: Option<String>,
pub volnorm: Option<bool>,
pub volnorm_pregain: Option<f64>,
pub notify: Option<bool>,
pub bitrate: Option<u32>,
pub gapless: Option<bool>,
pub shuffle: Option<bool>,
pub repeat: Option<queue::RepeatSetting>,
pub cover_max_scale: Option<f32>,
pub playback_state: Option<PlaybackState>,
pub track_format: Option<TrackFormat>,
pub notification_format: Option<NotificationFormat>,
pub statusbar_format: Option<String>,
pub library_tabs: Option<Vec<LibraryTab>>,
pub hide_display_names: Option<bool>,
pub credentials: Option<Credentials>,
pub ap_port: Option<u16>,
}
/// Commands used to obtain user credentials automatically.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct Credentials {
pub username_cmd: Option<String>,
pub password_cmd: Option<String>,
}
/// The ncspot theme.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ConfigTheme {
pub background: Option<String>,
pub primary: Option<String>,
pub secondary: Option<String>,
pub title: Option<String>,
pub playing: Option<String>,
pub playing_selected: Option<String>,
pub playing_bg: Option<String>,
pub highlight: Option<String>,
pub highlight_bg: Option<String>,
pub highlight_inactive_bg: Option<String>,
pub error: Option<String>,
pub error_bg: Option<String>,
pub statusbar_progress: Option<String>,
pub statusbar_progress_bg: Option<String>,
pub statusbar: Option<String>,
pub statusbar_bg: Option<String>,
pub cmdline: Option<String>,
pub cmdline_bg: Option<String>,
pub search_match: Option<String>,
}
/// The ordering that is used when representing a playlist.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SortingOrder {
pub key: SortKey,
pub direction: SortDirection,
}
/// The runtime state of the music queue.
#[derive(Serialize, Default, Deserialize, Debug, Clone)]
pub struct QueueState {
pub current_track: Option<usize>,
pub random_order: Option<Vec<usize>>,
pub track_progress: std::time::Duration,
pub queue: Vec<Playable>,
}
/// Runtime state that should be persisted accross sessions.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserState {
pub volume: u16,
pub shuffle: bool,
pub repeat: queue::RepeatSetting,
pub queuestate: QueueState,
pub playlist_orders: HashMap<String, SortingOrder>,
pub cache_version: u16,
pub playback_state: PlaybackState,
}
impl Default for UserState {
fn default() -> Self {
Self {
volume: u16::MAX,
shuffle: false,
repeat: queue::RepeatSetting::None,
queuestate: QueueState::default(),
playlist_orders: HashMap::new(),
cache_version: 0,
playback_state: PlaybackState::Default,
}
}
}
/// Configuration files are read/written relative to this directory.
static BASE_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
/// The complete configuration (state + user configuration) of ncspot.
pub struct Config {
/// The configuration file path.
filename: String,
/// Configuration set by the user, read only.
values: RwLock<ConfigValues>,
/// Runtime state which can't be edited by the user, read/write.
state: RwLock<UserState>,
}
impl Config {
/// Generate the configuration from the user configuration file and the runtime state file.
/// `filename` can be used to look for a differently named configuration file.
pub fn new(filename: Option<String>) -> Self {
let filename = filename.unwrap_or(CONFIGURATION_FILE_NAME.to_owned());
let values = load(&filename).unwrap_or_else(|e| {
eprint!(
"There is an error in your configuration file at {}:\n\n{e}",
user_configuration_directory()
.map(|ref mut path| {
path.push(CONFIGURATION_FILE_NAME);
path.to_string_lossy().to_string()
})
.expect("configuration directory expected but not found")
);
process::exit(1);
});
let mut userstate = {
let path = config_path(USER_STATE_FILE_NAME);
CBOR.load_or_generate_default(path, || Ok(UserState::default()), true)
.expect("could not load user state")
};
if let Some(shuffle) = values.shuffle {
userstate.shuffle = shuffle;
}
if let Some(repeat) = values.repeat {
userstate.repeat = repeat;
}
if let Some(playback_state) = values.playback_state.clone() {
userstate.playback_state = playback_state;
}
Self {
filename,
values: RwLock::new(values),
state: RwLock::new(userstate),
}
}
/// Get the user configuration values.
pub fn values(&self) -> RwLockReadGuard<ConfigValues> {
self.values.read().unwrap()
}
/// Get the runtime user state values.
pub fn state(&self) -> RwLockReadGuard<UserState> {
self.state.read().unwrap()
}
/// Modify the internal user state through a shared reference using a closure.
pub fn with_state_mut<F>(&self, cb: F)
where
F: Fn(&mut UserState),
{
let mut state_guard = self.state.write().unwrap();
cb(&mut state_guard);
}
/// Update the version number of the runtime user state. This should be done before saving it to
/// disk.
fn update_state_cache_version(&self) {
self.with_state_mut(|state| state.cache_version = CACHE_VERSION);
}
/// Save runtime state to the user configuration directory.
pub fn save_state(&self) {
self.update_state_cache_version();
let path = config_path(USER_STATE_FILE_NAME);
debug!("saving user state to {}", path.display());
if let Err(e) = CBOR.write(path, &*self.state()) {
error!("Could not save user state: {}", e);
}
}
/// Create a [Theme] from the user supplied theme in the configuration file.
pub fn build_theme(&self) -> Theme {
crate::theme::load(&self.values().theme)
}
/// Attempt to reload the configuration from the configuration file.
///
/// This only updates the values stored in memory but doesn't perform any additional actions
/// like updating active keybindings.
pub fn reload(&self) -> Result<(), Box<dyn Error>> {
let cfg = load(&self.filename)?;
*self.values.write().unwrap() = cfg;
Ok(())
}
}
/// Parse the configuration file with name `filename` at the configuration base path.
fn load(filename: &str) -> Result<ConfigValues, String> {
let path = config_path(filename);
TOML.load_or_generate_default(path, || Ok(ConfigValues::default()), false)
}
/// Returns the plaform app directories for ncspot if they could be determined,
/// or an error otherwise.
pub fn try_proj_dirs() -> Result<AppDirs, String> {
match *BASE_PATH
.read()
.map_err(|_| String::from("Poisoned RWLock"))?
{
Some(ref basepath) => Ok(AppDirs {
cache_dir: basepath.join(".cache"),
config_dir: basepath.join(".config"),
data_dir: basepath.join(".local/share"),
state_dir: basepath.join(".local/state"),
}),
None => AppDirs::new(Some("ncspot"), true)
.ok_or_else(|| String::from("Couldn't determine platform standard directories")),
}
}
/// Return the path to the current user's configuration directory, or None if it couldn't be found.
/// This function does not guarantee correct permissions or ownership of the directory!
pub fn user_configuration_directory() -> Option<PathBuf> {
let project_directories = try_proj_dirs().ok()?;
Some(project_directories.config_dir)
}
/// Return the path to the current user's cache directory, or None if one couldn't be found. This
/// function does not guarantee correct permissions or ownership of the directory!
pub fn user_cache_directory() -> Option<PathBuf> {
let project_directories = try_proj_dirs().ok()?;
Some(project_directories.cache_dir)
}
/// Force create the configuration directory at the default project location, removing anything that
/// isn't a directory but has the same name. Return the path to the configuration file inside the
/// directory.
///
/// This doesn't create the file, only the containing directory.
pub fn config_path(file: &str) -> PathBuf {
let cfg_dir = user_configuration_directory().unwrap();
if cfg_dir.exists() && !cfg_dir.is_dir() {
fs::remove_file(&cfg_dir).expect("unable to remove old config file");
}
if !cfg_dir.exists() {
fs::create_dir_all(&cfg_dir).expect("can't create config folder");
}
let mut cfg = cfg_dir.to_path_buf();
cfg.push(file);
cfg
}
/// Create the cache directory at the default project location, preserving it if it already exists,
/// and return the path to the cache file inside the directory.
///
/// This doesn't create the file, only the containing directory.
pub fn cache_path(file: &str) -> PathBuf {
let cache_dir = user_cache_directory().unwrap();
if !cache_dir.exists() {
fs::create_dir_all(&cache_dir).expect("can't create cache folder");
}
let mut pb = cache_dir.to_path_buf();
pb.push(file);
pb
}
/// Set the configuration base path. All configuration files are read/written relative to this path.
pub fn set_configuration_base_path(base_path: Option<PathBuf>) {
if let Some(basepath) = base_path {
if !basepath.exists() {
fs::create_dir_all(&basepath).expect("could not create basepath directory");
}
*BASE_PATH.write().unwrap() = Some(basepath);
}
}