diff --git a/CHANGELOG.md b/CHANGELOG.md index 70474af..0f5a6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Special color for unavailable items - Changelog with all the relevant user-facing changes to the project +- `info` command line subcommand to show platform specific information ### Changed diff --git a/README.md b/README.md index aa72816..1cfb8d4 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ ncspot is available on macOS (Homebrew), Windows (Scoop), Linux (native package BSD's. Detailed installation instructions for each platform can be found [here](/doc/users.md). ## Configuration -A configuration file can be provided at `$XDG_CONFIG_HOME/ncspot/config.toml`. Detailed +A configuration file can be provided. The default location is `~/.config/ncspot`. Detailed configuration information can be found [here](/doc/users.md#configuration). ## Building diff --git a/doc/developers.md b/doc/developers.md index 5ae0977..5587f94 100644 --- a/doc/developers.md +++ b/doc/developers.md @@ -36,7 +36,8 @@ and attach a debugger. On Linux this can be achieved with `gdb` or `lldb`. It is work. To disable it, execute `echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope`. This will allow any process to inspect the memory of another process. It is automatically re-enabled after a reboot. -If ncspot has crashed you can find the latest backtrace at `~/.cache/ncspot/backtrace.log`. +If ncspot has crashed you can find the latest backtrace at `$NCSPOT_CACHE_DIRECTORY/backtrace.log`. +The cache directory can be shown by running `ncspot info`. ## Compiling Compile and install the latest release with `cargo-install`: diff --git a/doc/users.md b/doc/users.md index 2d2d5e6..0fc8a65 100644 --- a/doc/users.md +++ b/doc/users.md @@ -181,14 +181,12 @@ Note: \ - mandatory arg; [BAR] - optional arg ## Remote control (IPC) Apart from MPRIS, ncspot will also create a domain socket on UNIX platforms (Linux, macOS, *BSD). -The socket will be created in the platform's runtime directory. If XDG_RUNTIME_DIR is set, it will -be created under `$XDG_RUNTIME_DIR/ncspot`. If XDG_RUNTIME_DIR isn't set, it will be created under -`/run/user/` for Linux if it exists. In all other cases, it will be created under -`/tmp/ncspot-`. Applications or scripts can connect to this socket to send commands or be -notified of the currently playing track, i.e. with `netcat`: +The socket will be created in the platform's runtime directory. Run `ncspot info` to show the +location of this directory on your platform. Applications or scripts can connect to this socket to +send commands or be notified of the currently playing track, i.e. with `netcat`: ``` -% nc -U ~/.cache/ncspot/ncspot.sock +% nc -U $NCSPOT_CACHE_DIRECTORY/ncspot.sock play {"mode":{"Playing":{"secs_since_epoch":1672249086,"nanos_since_epoch":547517730}},"playable":{"type":"Track","id":"2wcrQZ7ZJolYEfIaPP9yL4","uri":"spotify:track:2wcrQZ7ZJolYEfIaPP9yL4","title":"Hit Me Where It Hurts","track_number":4,"disc_number":1,"duration":184132,"artists":["Caroline Polachek"],"artist_ids":["4Ge8xMJNwt6EEXOzVXju9a"],"album":"Pang","album_id":"4ClyeVlAKJJViIyfVW0yQD","album_artists":["Caroline Polachek"],"cover_url":"https://i.scdn.co/image/ab67616d0000b2737d983e7bf67c2806218c2759","url":"https://open.spotify.com/track/2wcrQZ7ZJolYEfIaPP9yL4","added_at":"2022-12-19T22:41:05Z","list_index":0}} playpause @@ -212,7 +210,7 @@ as they typically tend to keep the connection to the socket open. OpenBSD's specific number of packets have been received. ``` -% nc -W 1 -U ~/.cache/ncspot/ncspot.sock +% nc -W 1 -U $NCSPOT_CACHE_DIRECTORY/ncspot.sock {"mode":{"Playing":{"secs_since_epoch":1675188934,"nanos_since_epoch":50913345}},"playable":{"type":"Track","id":"5Cp6a1h2VnuOtsh1Nqxfv6","uri":"spotify:track:5Cp6a1h2VnuOtsh1Nqxfv6","title":"New Track","track_number":1,"disc_number":1,"duration":498358,"artists":["Francis Bebey"],"artist_ids":["0mdmrbu5UZ32uRcRp2z6mr"],"album":"African Electronic Music (1975-1982)","album_id":"7w99Aae1tYSTSb1OiDnxYY","album_artists":["Francis Bebey"],"cover_url":"https://i.scdn.co/image/ab67616d0000b2736ab57cedf27177fae1eaed87","url":"https://open.spotify.com/track/5Cp6a1h2VnuOtsh1Nqxfv6","added_at":"2020-12-22T09:57:17Z","list_index":0}} ``` @@ -221,17 +219,17 @@ For example, you can get the currently playing artist and title in your terminal as follows: ``` -% nc -W 1 -U ~/.cache/ncspot/ncspot.sock | jq '.playable.title' +% nc -W 1 -U $NCSPOT_CACHE_DIRECTORY/ncspot.sock | jq '.playable.title' "PUMPIN' JUMPIN'" -% nc -W 1 -U ~/.cache/ncspot/ncspot.sock | jq '.playable.artists[0]' +% nc -W 1 -U $NCSPOT_CACHE_DIRECTORY/ncspot.sock | jq '.playable.artists[0]' "Hideki Naganuma" ``` ## Configuration -Configuration is saved to `~/.config/ncspot/config.toml` (or -`%AppData%\ncspot\config.toml` on Windows). To reload the configuration during -runtime use the `reload` command. +Configuration is saved to the `config.toml` file in the platform's standard configuration directory. +Run `ncspot info` to show the location of this directory on your platform. To reload the +configuration during runtime use the `reload` command. Possible configuration values are: @@ -448,8 +446,8 @@ cover_max_scale = 2 `ncspot` prompts for a Spotify username and password on first launch, uses this to generate an OAuth token, and stores it to disk. -The credentials are stored in `~/.cache/ncspot/librespot/credentials.json` -(unless the base path has been changed with the `--basepath` option). +The credentials are stored in `librespot/credentials.json` in the user's cache directory. Run +`ncspot info` to show the location of this directory. The `logout` command can be used to remove cached credentials. See [Vim-Like Commands](#vim-like-commands). diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..15eb0d1 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,35 @@ +use crate::config::{user_cache_directory, user_configuration_directory}; + +/// Print platform info like which platform directories will be used. +pub fn info() -> Result<(), String> { + let user_configuration_directory = user_configuration_directory(); + let user_cache_directory = user_cache_directory(); + + println!( + "USER_CONFIGURATION_PATH {}", + user_configuration_directory + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or("not found".into()) + ); + println!( + "USER_CACHE_PATH {}", + user_cache_directory + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or("not found".into()) + ); + + #[cfg(unix)] + { + use crate::utils::user_runtime_directory; + + let user_runtime_directory = user_runtime_directory(); + println!( + "USER_RUNTIME_PATH {}", + user_runtime_directory + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or("not found".into()) + ); + } + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 8e53335..f2f8ef1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use std::{fs, process}; use cursive::theme::Theme; use log::{debug, error}; +use ncspot::CONFIGURATION_FILE_NAME; use platform_dirs::AppDirs; use crate::command::{SortDirection, SortKey}; @@ -192,7 +193,7 @@ 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) -> Self { - let filename = filename.unwrap_or("config.toml".to_owned()); + let filename = filename.unwrap_or(CONFIGURATION_FILE_NAME.to_owned()); let values = load(&filename).unwrap_or_else(|e| { eprintln!("could not load config: {e}"); process::exit(1); @@ -268,16 +269,6 @@ fn load(filename: &str) -> Result { TOML.load_or_generate_default(path, || Ok(ConfigValues::default()), false) } -/// Returns the platform app directories for ncspot. -/// -/// # Panics -/// -/// This panics if the project directories could not be determined. Use `try_proj_dirs` for a -/// non-panicking version. -fn proj_dirs() -> AppDirs { - try_proj_dirs().unwrap() -} - /// Returns the plaform app directories for ncspot if they could be determined, /// or an error otherwise. pub fn try_proj_dirs() -> Result { @@ -296,19 +287,32 @@ pub fn try_proj_dirs() -> Result { } } +/// 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 { + 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 { + 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 proj_dirs = proj_dirs(); - let cfg_dir = &proj_dirs.config_dir; + 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"); + 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"); + fs::create_dir_all(&cfg_dir).expect("can't create config folder"); } let mut cfg = cfg_dir.to_path_buf(); cfg.push(file); @@ -320,10 +324,9 @@ pub fn config_path(file: &str) -> PathBuf { /// /// This doesn't create the file, only the containing directory. pub fn cache_path(file: &str) -> PathBuf { - let proj_dirs = proj_dirs(); - let cache_dir = &proj_dirs.cache_dir; + let cache_dir = user_cache_directory().unwrap(); if !cache_dir.exists() { - fs::create_dir_all(cache_dir).expect("can't create cache folder"); + fs::create_dir_all(&cache_dir).expect("can't create cache folder"); } let mut pb = cache_dir.to_path_buf(); pb.push(file); diff --git a/src/lib.rs b/src/lib.rs index c2c349a..565bbf5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ use librespot_playback::audio_backend; pub const AUTHOR: &str = "Henrik Friedrichsen and contributors"; pub const BIN_NAME: &str = "ncspot"; +pub const CONFIGURATION_FILE_NAME: &str = "config.toml"; /// Return the [Command](clap::Command) that models the program's command line arguments. The /// command can be used to parse the actual arguments passed to the program, or to automatically @@ -40,6 +41,7 @@ pub fn program_arguments() -> clap::Command { .long("config") .value_name("FILE") .help("Filename of config file in basepath") - .default_value("config.toml"), + .default_value(CONFIGURATION_FILE_NAME), ) + .subcommands([clap::Command::new("info").about("Print platform information like paths")]) } diff --git a/src/main.rs b/src/main.rs index 2aad71a..11e55e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use ncspot::program_arguments; mod application; mod authentication; +mod cli; mod command; mod commands; mod config; @@ -56,9 +57,15 @@ fn main() -> Result<(), String> { // path. set_configuration_base_path(matches.get_one::("basepath").cloned()); - // Create the application. - let mut application = Application::new(matches.get_one::("config").cloned())?; + match matches.subcommand() { + Some(("info", _subcommand_matches)) => cli::info(), + Some((_, _)) => unreachable!(), + None => { + // Create the application. + let mut application = Application::new(matches.get_one::("config").cloned())?; - // Start the application event loop. - application.run() + // Start the application event loop. + application.run() + } + } } diff --git a/src/ui/help.rs b/src/ui/help.rs index c401ef0..02aa80a 100644 --- a/src/ui/help.rs +++ b/src/ui/help.rs @@ -5,6 +5,7 @@ use cursive::utils::markup::StyledString; use cursive::view::ViewWrapper; use cursive::views::{ScrollView, TextView}; use cursive::Cursive; +use ncspot::CONFIGURATION_FILE_NAME; use crate::command::{Command, MoveAmount, MoveMode}; use crate::commands::CommandResult; @@ -22,7 +23,9 @@ impl HelpView { let note = format!( "Custom bindings can be set in {} within the [keybindings] section.\n\n", - config_path("config.toml").to_str().unwrap_or_default() + config_path(CONFIGURATION_FILE_NAME) + .to_str() + .unwrap_or_default() ); text.append(StyledString::styled(note, Effect::Italic)); diff --git a/src/utils.rs b/src/utils.rs index bacc963..ce42bf4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -72,20 +72,7 @@ pub fn create_runtime_directory() -> Result> os::unix::prelude::PermissionsExt, }; - let linux_runtime_directory = - PathBuf::from(format!("/run/user/{}/", unsafe { libc::getuid() })); - let unix_runtime_directory = PathBuf::from("/tmp/"); - - let user_runtime_directory = if let Some(xdg_runtime_directory) = xdg_runtime_directory() { - Some(xdg_runtime_directory.join("ncspot")) - } else if cfg!(linux) && linux_runtime_directory.exists() { - Some(linux_runtime_directory.join("ncspot")) - } else if unix_runtime_directory.exists() { - Some(unix_runtime_directory.join(format!("ncspot-{}", unsafe { libc::getuid() }))) - } else { - None - } - .ok_or("no runtime directory found")?; + let user_runtime_directory = user_runtime_directory().ok_or("no runtime directory found")?; let creation_result = fs::create_dir(&user_runtime_directory); @@ -106,6 +93,25 @@ pub fn create_runtime_directory() -> Result> } } +/// Return the path to the current user's runtime directory, or None if it couldn't be found. +/// This function does not guarantee correct ownership or permissions of the directory. +#[cfg(unix)] +pub fn user_runtime_directory() -> Option { + let linux_runtime_directory = + PathBuf::from(format!("/run/user/{}/", unsafe { libc::getuid() })); + let unix_runtime_directory = PathBuf::from("/tmp/"); + + if let Some(xdg_runtime_directory) = xdg_runtime_directory() { + Some(xdg_runtime_directory.join("ncspot")) + } else if cfg!(linux) && linux_runtime_directory.exists() { + Some(linux_runtime_directory.join("ncspot")) + } else if unix_runtime_directory.exists() { + Some(unix_runtime_directory.join(format!("ncspot-{}", unsafe { libc::getuid() }))) + } else { + None + } +} + #[cfg(unix)] fn xdg_runtime_directory() -> Option { std::env::var("XDG_RUNTIME_DIR").ok().map(Into::into)