feat: add info command line subcommand (#1330)

* feat: add `info` command line subcommand

Adding an info command allows the documentation to refer to it when
mentioning platform specific information. This gives users a nicer
experience since they don't need to think about how `ncspot` will behave
on their system, but can run `ncspot info` to get that information.

* fix: info command don't create runtime directory

* fix: don't print runtime path on Windows

Windows doesn't use the runtime path so it shouldn't be printed there.

* fix: make `info` command easier to parse

* docs: add back the default configuration directory
This commit is contained in:
Thomas Frans
2023-11-27 08:43:55 +01:00
committed by GitHub
parent 0c9be11357
commit 0cee99ba4c
10 changed files with 110 additions and 54 deletions

View File

@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Special color for unavailable items - Special color for unavailable items
- Changelog with all the relevant user-facing changes to the project - Changelog with all the relevant user-facing changes to the project
- `info` command line subcommand to show platform specific information
### Changed ### Changed

View File

@@ -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). BSD's. Detailed installation instructions for each platform can be found [here](/doc/users.md).
## Configuration ## 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). configuration information can be found [here](/doc/users.md#configuration).
## Building ## Building

View File

@@ -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 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. 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 ## Compiling
Compile and install the latest release with `cargo-install`: Compile and install the latest release with `cargo-install`:

View File

@@ -181,14 +181,12 @@ Note: \<FOO\> - mandatory arg; [BAR] - optional arg
## Remote control (IPC) ## Remote control (IPC)
Apart from MPRIS, ncspot will also create a domain socket on UNIX platforms (Linux, macOS, *BSD). 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 The socket will be created in the platform's runtime directory. Run `ncspot info` to show the
be created under `$XDG_RUNTIME_DIR/ncspot`. If XDG_RUNTIME_DIR isn't set, it will be created under location of this directory on your platform. Applications or scripts can connect to this socket to
`/run/user/<uid>` for Linux if it exists. In all other cases, it will be created under send commands or be notified of the currently playing track, i.e. with `netcat`:
`/tmp/ncspot-<uid>`. 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 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}} {"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 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. 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}} {"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: 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'" "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" "Hideki Naganuma"
``` ```
## Configuration ## Configuration
Configuration is saved to `~/.config/ncspot/config.toml` (or Configuration is saved to the `config.toml` file in the platform's standard configuration directory.
`%AppData%\ncspot\config.toml` on Windows). To reload the configuration during Run `ncspot info` to show the location of this directory on your platform. To reload the
runtime use the `reload` command. configuration during runtime use the `reload` command.
Possible configuration values are: 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 `ncspot` prompts for a Spotify username and password on first launch, uses this
to generate an OAuth token, and stores it to disk. to generate an OAuth token, and stores it to disk.
The credentials are stored in `~/.cache/ncspot/librespot/credentials.json` The credentials are stored in `librespot/credentials.json` in the user's cache directory. Run
(unless the base path has been changed with the `--basepath` option). `ncspot info` to show the location of this directory.
The `logout` command can be used to remove cached credentials. See The `logout` command can be used to remove cached credentials. See
[Vim-Like Commands](#vim-like-commands). [Vim-Like Commands](#vim-like-commands).

35
src/cli.rs Normal file
View File

@@ -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(())
}

View File

@@ -5,6 +5,7 @@ use std::{fs, process};
use cursive::theme::Theme; use cursive::theme::Theme;
use log::{debug, error}; use log::{debug, error};
use ncspot::CONFIGURATION_FILE_NAME;
use platform_dirs::AppDirs; use platform_dirs::AppDirs;
use crate::command::{SortDirection, SortKey}; use crate::command::{SortDirection, SortKey};
@@ -192,7 +193,7 @@ impl Config {
/// Generate the configuration from the user configuration file and the runtime state file. /// 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. /// `filename` can be used to look for a differently named configuration file.
pub fn new(filename: Option<String>) -> Self { pub fn new(filename: Option<String>) -> 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| { let values = load(&filename).unwrap_or_else(|e| {
eprintln!("could not load config: {e}"); eprintln!("could not load config: {e}");
process::exit(1); process::exit(1);
@@ -268,16 +269,6 @@ fn load(filename: &str) -> Result<ConfigValues, String> {
TOML.load_or_generate_default(path, || Ok(ConfigValues::default()), false) 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, /// Returns the plaform app directories for ncspot if they could be determined,
/// or an error otherwise. /// or an error otherwise.
pub fn try_proj_dirs() -> Result<AppDirs, String> { pub fn try_proj_dirs() -> Result<AppDirs, String> {
@@ -296,19 +287,32 @@ pub fn try_proj_dirs() -> Result<AppDirs, String> {
} }
} }
/// 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 /// 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 /// isn't a directory but has the same name. Return the path to the configuration file inside the
/// directory. /// directory.
/// ///
/// This doesn't create the file, only the containing directory. /// This doesn't create the file, only the containing directory.
pub fn config_path(file: &str) -> PathBuf { pub fn config_path(file: &str) -> PathBuf {
let proj_dirs = proj_dirs(); let cfg_dir = user_configuration_directory().unwrap();
let cfg_dir = &proj_dirs.config_dir;
if cfg_dir.exists() && !cfg_dir.is_dir() { 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() { 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(); let mut cfg = cfg_dir.to_path_buf();
cfg.push(file); cfg.push(file);
@@ -320,10 +324,9 @@ pub fn config_path(file: &str) -> PathBuf {
/// ///
/// This doesn't create the file, only the containing directory. /// This doesn't create the file, only the containing directory.
pub fn cache_path(file: &str) -> PathBuf { pub fn cache_path(file: &str) -> PathBuf {
let proj_dirs = proj_dirs(); let cache_dir = user_cache_directory().unwrap();
let cache_dir = &proj_dirs.cache_dir;
if !cache_dir.exists() { 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(); let mut pb = cache_dir.to_path_buf();
pb.push(file); pb.push(file);

View File

@@ -3,6 +3,7 @@ use librespot_playback::audio_backend;
pub const AUTHOR: &str = "Henrik Friedrichsen <henrik@affekt.org> and contributors"; pub const AUTHOR: &str = "Henrik Friedrichsen <henrik@affekt.org> and contributors";
pub const BIN_NAME: &str = "ncspot"; 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 /// 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 /// 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") .long("config")
.value_name("FILE") .value_name("FILE")
.help("Filename of config file in basepath") .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")])
} }

View File

@@ -13,6 +13,7 @@ use ncspot::program_arguments;
mod application; mod application;
mod authentication; mod authentication;
mod cli;
mod command; mod command;
mod commands; mod commands;
mod config; mod config;
@@ -56,9 +57,15 @@ fn main() -> Result<(), String> {
// path. // path.
set_configuration_base_path(matches.get_one::<PathBuf>("basepath").cloned()); set_configuration_base_path(matches.get_one::<PathBuf>("basepath").cloned());
// Create the application. match matches.subcommand() {
let mut application = Application::new(matches.get_one::<String>("config").cloned())?; Some(("info", _subcommand_matches)) => cli::info(),
Some((_, _)) => unreachable!(),
None => {
// Create the application.
let mut application = Application::new(matches.get_one::<String>("config").cloned())?;
// Start the application event loop. // Start the application event loop.
application.run() application.run()
}
}
} }

View File

@@ -5,6 +5,7 @@ use cursive::utils::markup::StyledString;
use cursive::view::ViewWrapper; use cursive::view::ViewWrapper;
use cursive::views::{ScrollView, TextView}; use cursive::views::{ScrollView, TextView};
use cursive::Cursive; use cursive::Cursive;
use ncspot::CONFIGURATION_FILE_NAME;
use crate::command::{Command, MoveAmount, MoveMode}; use crate::command::{Command, MoveAmount, MoveMode};
use crate::commands::CommandResult; use crate::commands::CommandResult;
@@ -22,7 +23,9 @@ impl HelpView {
let note = format!( let note = format!(
"Custom bindings can be set in {} within the [keybindings] section.\n\n", "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)); text.append(StyledString::styled(note, Effect::Italic));

View File

@@ -72,20 +72,7 @@ pub fn create_runtime_directory() -> Result<PathBuf, Box<dyn std::error::Error>>
os::unix::prelude::PermissionsExt, os::unix::prelude::PermissionsExt,
}; };
let linux_runtime_directory = let user_runtime_directory = user_runtime_directory().ok_or("no runtime directory found")?;
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 creation_result = fs::create_dir(&user_runtime_directory); let creation_result = fs::create_dir(&user_runtime_directory);
@@ -106,6 +93,25 @@ pub fn create_runtime_directory() -> Result<PathBuf, Box<dyn std::error::Error>>
} }
} }
/// 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<PathBuf> {
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)] #[cfg(unix)]
fn xdg_runtime_directory() -> Option<PathBuf> { fn xdg_runtime_directory() -> Option<PathBuf> {
std::env::var("XDG_RUNTIME_DIR").ok().map(Into::into) std::env::var("XDG_RUNTIME_DIR").ok().map(Into::into)