about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-07-10 16:41:05 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-07-10 16:41:05 +0200
commit97537059b44f5ed336a915a1ba805be215cf6566 (patch)
tree51c008494c5126942d2f227837e2b6433e62327c
parentrefactor(crates/yt_dlp): Port to `pyo3` again (diff)
downloadyt-97537059b44f5ed336a915a1ba805be215cf6566.zip
refactor(crates/yt/config): Use a macro to generate the config parsing code
This makes adding new config values easier and makes it harder to
introduce slight bugs (with the old config system the cli `--db-path`
flag did only take effect, after the value in the config file).
-rw-r--r--crates/yt/src/config/default.rs110
-rw-r--r--crates/yt/src/config/definitions.rs67
-rw-r--r--crates/yt/src/config/file_system.rs120
-rw-r--r--crates/yt/src/config/mod.rs134
-rw-r--r--crates/yt/src/config/paths.rs50
-rw-r--r--crates/yt/src/config/support.rs151
-rw-r--r--crates/yt/src/main.rs8
7 files changed, 274 insertions, 366 deletions
diff --git a/crates/yt/src/config/default.rs b/crates/yt/src/config/default.rs
deleted file mode 100644
index 4ed643b..0000000
--- a/crates/yt/src/config/default.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::path::PathBuf;
-
-use anyhow::{Context, Result};
-
-fn get_runtime_path(name: &'static str) -> Result<PathBuf> {
-    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX);
-    xdg_dirs
-        .place_runtime_file(name)
-        .with_context(|| format!("Failed to place runtime file: '{name}'"))
-}
-fn get_data_path(name: &'static str) -> Result<PathBuf> {
-    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX);
-    xdg_dirs
-        .place_data_file(name)
-        .with_context(|| format!("Failed to place data file: '{name}'"))
-}
-fn get_config_path(name: &'static str) -> Result<PathBuf> {
-    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX);
-    xdg_dirs
-        .place_config_file(name)
-        .with_context(|| format!("Failed to place config file: '{name}'"))
-}
-
-pub(super) fn create_path(path: PathBuf) -> Result<PathBuf> {
-    if !path.exists() {
-        if let Some(parent) = path.parent() {
-            std::fs::create_dir_all(parent)
-                .with_context(|| format!("Failed to create the '{}' directory", path.display()))?;
-        }
-    }
-
-    Ok(path)
-}
-
-pub(crate) const PREFIX: &str = "yt";
-
-pub(crate) mod global {
-    pub(crate) fn display_colors() -> bool {
-        // TODO: This should probably check if the output is a tty and otherwise return `false` <2025-02-14>
-        true
-    }
-}
-
-pub(crate) mod select {
-    pub(crate) fn playback_speed() -> f64 {
-        2.7
-    }
-    pub(crate) fn subtitle_langs() -> &'static str {
-        ""
-    }
-}
-
-pub(crate) mod watch {
-    pub(crate) fn local_displays_length() -> usize {
-        1000
-    }
-}
-
-pub(crate) mod update {
-    pub(crate) fn max_backlog() -> usize {
-        20
-    }
-}
-
-pub(crate) mod paths {
-    use std::{env::temp_dir, path::PathBuf};
-
-    use anyhow::Result;
-
-    use super::{PREFIX, create_path, get_config_path, get_data_path, get_runtime_path};
-
-    // We download to the temp dir to avoid taxing the disk
-    pub(crate) fn download_dir() -> Result<PathBuf> {
-        let temp_dir = temp_dir();
-
-        create_path(temp_dir.join(PREFIX))
-    }
-    pub(crate) fn mpv_config_path() -> Result<PathBuf> {
-        get_config_path("mpv.conf")
-    }
-    pub(crate) fn mpv_input_path() -> Result<PathBuf> {
-        get_config_path("mpv.input.conf")
-    }
-    pub(crate) fn database_path() -> Result<PathBuf> {
-        get_data_path("videos.sqlite")
-    }
-    pub(crate) fn config_path() -> Result<PathBuf> {
-        get_config_path("config.toml")
-    }
-    pub(crate) fn last_selection_path() -> Result<PathBuf> {
-        get_runtime_path("selected.yts")
-    }
-}
-
-pub(crate) mod download {
-    pub(crate) fn max_cache_size() -> &'static str {
-        "3 GiB"
-    }
-}
diff --git a/crates/yt/src/config/definitions.rs b/crates/yt/src/config/definitions.rs
deleted file mode 100644
index ce8c0d4..0000000
--- a/crates/yt/src/config/definitions.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::path::PathBuf;
-
-use serde::Deserialize;
-
-#[derive(Debug, Deserialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct ConfigFile {
-    pub global: Option<GlobalConfig>,
-    pub select: Option<SelectConfig>,
-    pub watch: Option<WatchConfig>,
-    pub paths: Option<PathsConfig>,
-    pub download: Option<DownloadConfig>,
-    pub update: Option<UpdateConfig>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct GlobalConfig {
-    pub display_colors: Option<bool>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct UpdateConfig {
-    pub max_backlog: Option<usize>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct DownloadConfig {
-    /// This will then be converted to an u64
-    pub max_cache_size: Option<String>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct SelectConfig {
-    pub playback_speed: Option<f64>,
-    pub subtitle_langs: Option<String>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct WatchConfig {
-    pub local_displays_length: Option<usize>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct PathsConfig {
-    pub download_dir: Option<PathBuf>,
-    pub mpv_config_path: Option<PathBuf>,
-    pub mpv_input_path: Option<PathBuf>,
-    pub database_path: Option<PathBuf>,
-    pub last_selection_path: Option<PathBuf>,
-}
diff --git a/crates/yt/src/config/file_system.rs b/crates/yt/src/config/file_system.rs
deleted file mode 100644
index 2463e9d..0000000
--- a/crates/yt/src/config/file_system.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use crate::config::{DownloadConfig, PathsConfig, SelectConfig, WatchConfig};
-
-use super::{
-    Config, GlobalConfig, UpdateConfig,
-    default::{create_path, download, global, paths, select, update, watch},
-};
-
-use std::{fs::read_to_string, path::PathBuf};
-
-use anyhow::{Context, Result};
-use bytes::Bytes;
-
-macro_rules! get {
-    ($default:path, $config:expr, $key_one:ident, $($keys:ident),*) => {
-        {
-            let maybe_value = get!{@option $config, $key_one, $($keys),*};
-            if let Some(value) = maybe_value {
-                value
-            } else {
-                $default().to_owned()
-            }
-        }
-    };
-
-    (@option $config:expr, $key_one:ident, $($keys:ident),*) => {
-        if let Some(key) = $config.$key_one.clone() {
-            get!{@option key, $($keys),*}
-        } else {
-            None
-        }
-    };
-    (@option $config:expr, $key_one:ident) => {
-        $config.$key_one
-    };
-
-    (@path_if_none $config:expr, $option_default:expr, $default:path, $key_one:ident, $($keys:ident),*) => {
-        {
-            let maybe_download_dir: Option<PathBuf> =
-                get! {@option $config, $key_one, $($keys),*};
-
-            let down_dir = if let Some(dir) = maybe_download_dir {
-                PathBuf::from(dir)
-            } else {
-                if let Some(path) = $option_default {
-                    path
-                } else {
-                    $default()
-                        .with_context(|| format!("Failed to get default path for: '{}.{}'", stringify!($key_one), stringify!($($keys),*)))?
-                }
-            };
-            create_path(down_dir)?
-        }
-    };
-    (@path $config:expr, $default:path, $key_one:ident, $($keys:ident),*) => {
-        get! {@path_if_none $config, None, $default, $key_one, $($keys),*}
-    };
-}
-
-impl Config {
-    pub fn from_config_file(
-        db_path: Option<PathBuf>,
-        config_path: Option<PathBuf>,
-        display_colors: Option<bool>,
-    ) -> Result<Self> {
-        let config_file_path =
-            config_path.map_or_else(|| -> Result<_> { paths::config_path() }, Ok)?;
-
-        let config: super::definitions::ConfigFile =
-            toml::from_str(&read_to_string(config_file_path).unwrap_or(String::new()))
-                .context("Failed to parse the config file as toml")?;
-
-        Ok(Self {
-            global: GlobalConfig {
-                display_colors: {
-                    let config_value: Option<bool> = get! {@option config, global, display_colors};
-
-                    display_colors.unwrap_or(config_value.unwrap_or_else(global::display_colors))
-                },
-            },
-            select: SelectConfig {
-                playback_speed: get! {select::playback_speed, config, select, playback_speed},
-                subtitle_langs: get! {select::subtitle_langs, config, select, subtitle_langs},
-            },
-            watch: WatchConfig {
-                local_displays_length: get! {watch::local_displays_length, config, watch, local_displays_length},
-            },
-            update: UpdateConfig {
-                max_backlog: get! {update::max_backlog, config, update, max_backlog},
-            },
-            paths: PathsConfig {
-                download_dir: get! {@path config, paths::download_dir, paths, download_dir},
-                mpv_config_path: get! {@path config, paths::mpv_config_path, paths, mpv_config_path},
-                mpv_input_path: get! {@path config, paths::mpv_input_path, paths, mpv_input_path},
-                database_path: get! {@path_if_none config, db_path, paths::database_path, paths, database_path},
-                last_selection_path: get! {@path config, paths::last_selection_path, paths, last_selection_path},
-            },
-            download: DownloadConfig {
-                max_cache_size: {
-                    let bytes_str: String =
-                        get! {download::max_cache_size, config, download, max_cache_size};
-                    let number: Bytes = bytes_str
-                        .parse()
-                        .context("Failed to parse max_cache_size")?;
-                    number
-                },
-            },
-        })
-    }
-}
diff --git a/crates/yt/src/config/mod.rs b/crates/yt/src/config/mod.rs
index a10f7c2..154a109 100644
--- a/crates/yt/src/config/mod.rs
+++ b/crates/yt/src/config/mod.rs
@@ -1,76 +1,74 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+use crate::config::support::mk_config;
 
-#![allow(clippy::module_name_repetitions)]
+mod paths;
+mod support;
 
-use std::path::PathBuf;
+mk_config! {
+    use std::path::PathBuf;
+    use std::io::IsTerminal;
 
-use bytes::Bytes;
-use serde::Serialize;
+    use crate::shared::bytes::Bytes;
 
-mod default;
-mod definitions;
-pub mod file_system;
+    use super::paths::ensure_parent_dir;
 
-#[derive(Serialize, Debug)]
-pub struct Config {
-    pub global: GlobalConfig,
-    pub select: SelectConfig,
-    pub watch: WatchConfig,
-    pub paths: PathsConfig,
-    pub download: DownloadConfig,
-    pub update: UpdateConfig,
-}
-// These structures could get non-copy fields in the future.
+    struct Config {
+        global: GlobalConfig = {
+            /// Whether to display colors.
+            display_colors: bool where display_color: Option<bool> =! {|config_value: Option<bool>|
+                Ok::<_, anyhow::Error>(
+                    display_color
+                    .unwrap_or(
+                        config_value
+                        .unwrap_or_else(|| std::io::stderr().is_terminal())
+                    )
+                )
+            },
+        },
+        select: SelectConfig = {
+            /// The playback speed to use, when it is not overridden.
+            playback_speed: f64 =: 2.7,
 
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct GlobalConfig {
-    pub display_colors: bool,
-}
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct UpdateConfig {
-    pub max_backlog: usize,
-}
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct DownloadConfig {
-    pub max_cache_size: Bytes,
-}
-#[derive(Serialize, Debug)]
-pub struct SelectConfig {
-    pub playback_speed: f64,
-    pub subtitle_langs: String,
-}
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct WatchConfig {
-    pub local_displays_length: usize,
-}
-#[derive(Serialize, Debug)]
-pub struct PathsConfig {
-    pub download_dir: PathBuf,
-    pub mpv_config_path: PathBuf,
-    pub mpv_input_path: PathBuf,
-    pub database_path: PathBuf,
-    pub last_selection_path: PathBuf,
-}
+            /// The subtitle langs to download, when it is not overridden.
+            subtitle_langs: String =: String::new(),
+        },
+        watch: WatchConfig = {
+            /// How many chars to display at most, when displaying information on mpv's local on screen
+            /// display.
+            local_displays_length: usize =: 1000,
+        },
+        paths: PathsConfig = {
+            /// Where to store downloaded files.
+            download_dir: PathBuf =: {
+                // We download to the temp dir to avoid taxing the disk
+                let temp_dir = std::env::temp_dir();
+
+                temp_dir.join(super::paths::PREFIX)
+            } => ensure_parent_dir,
+
+            /// Path to the mpv configuration file.
+            mpv_config_path: PathBuf =? super::paths::get_config_path("mpv.conf") => ensure_parent_dir,
 
-// pub fn status_path() -> anyhow::Result<PathBuf> {
-//     const STATUS_PATH: &str = "running.info.json";
-//     get_runtime_path(STATUS_PATH)
-// }
+            /// Path to the mpv input configuration file.
+            mpv_input_path: PathBuf =? super::paths::get_config_path("mpv.input.conf") => ensure_parent_dir,
 
-// pub fn subscriptions() -> anyhow::Result<PathBuf> {
-//     const SUBSCRIPTIONS: &str = "subscriptions.json";
-//     get_data_path(SUBSCRIPTIONS)
-// }
+            /// Which path to use for mpv ipc socket creation.
+            mpv_ipc_socket_path: PathBuf =? super::paths::get_runtime_path("mpv.ipc.socket") => ensure_parent_dir,
+
+            /// Path to the video database.
+            database_path: PathBuf where db_path: Option<PathBuf> =! {|config_value: Option<PathBuf>| {
+                db_path.map_or_else(|| config_value.map_or_else(|| super::paths::get_data_path("videos.sqlite"), Ok), Ok)
+            }} => ensure_parent_dir,
+
+            /// Where to store the selection file before applying it.
+            last_selection_path: PathBuf =? super::paths::get_runtime_path("selected.yts") => ensure_parent_dir,
+        },
+        download: DownloadConfig = {
+            /// The maximum cache size.
+            max_cache_size: Bytes =? "3 GiB".parse(),
+        },
+        update: UpdateConfig = {
+            /// How many videos to download, when checking for new ones.
+            max_backlog: usize =: 20,
+        },
+    }
+}
diff --git a/crates/yt/src/config/paths.rs b/crates/yt/src/config/paths.rs
new file mode 100644
index 0000000..224891b
--- /dev/null
+++ b/crates/yt/src/config/paths.rs
@@ -0,0 +1,50 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::path::{Path, PathBuf};
+
+use anyhow::{Context, Result};
+
+pub(super) fn get_runtime_path(name: &'static str) -> Result<PathBuf> {
+    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX);
+    xdg_dirs
+        .place_runtime_file(name)
+        .with_context(|| format!("Failed to place runtime file: '{name}'"))
+}
+pub(super) fn get_data_path(name: &'static str) -> Result<PathBuf> {
+    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX);
+    xdg_dirs
+        .place_data_file(name)
+        .with_context(|| format!("Failed to place data file: '{name}'"))
+}
+pub(super) fn get_config_path(name: &'static str) -> Result<PathBuf> {
+    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX);
+    xdg_dirs
+        .place_config_file(name)
+        .with_context(|| format!("Failed to place config file: '{name}'"))
+}
+
+pub(super) fn ensure_parent_dir(path: &Path) -> Result<()> {
+    if !path.exists() {
+        if let Some(parent) = path.parent() {
+            std::fs::create_dir_all(parent)
+                .with_context(|| format!("Failed to create the '{}' directory", path.display()))?;
+        }
+    }
+
+    Ok(())
+}
+
+pub(super) fn config_path() -> Result<PathBuf> {
+    get_config_path("config.toml")
+}
+
+pub(crate) const PREFIX: &str = "yt";
diff --git a/crates/yt/src/config/support.rs b/crates/yt/src/config/support.rs
new file mode 100644
index 0000000..880eba7
--- /dev/null
+++ b/crates/yt/src/config/support.rs
@@ -0,0 +1,151 @@
+macro_rules! mk_config {
+    (
+        $(use $usage_path:path;)*
+
+        struct $name:ident {
+            $(
+              $(#[$attr0:meta])*
+              $subconfig_name:ident : $subconfig_type:ident = {
+                $(
+                    $(#[$attr1:meta])*
+                    $field_name:ident : $field_type:ty $(
+                        where $extra_input:ident: $extra_input_type:ty
+                    ),* = $errors:tt  $default:expr $(=> $finalizer:ident)?
+                ),*
+                $(,)?
+              }
+            ),*
+            $(,)?
+        }
+    ) => {
+        mod _inner {
+            #![allow(non_snake_case)]
+
+            $(use $usage_path;)*
+
+            #[derive(serde::Serialize, Debug)]
+            pub(crate) struct $name {
+                $(
+                    $(#[$attr0])*
+                    pub(crate) $subconfig_name: $subconfig_type
+                ),*
+            }
+
+            #[derive(Debug, serde::Deserialize, PartialEq)]
+            #[serde(deny_unknown_fields)]
+            #[allow(non_camel_case_types)]
+            struct config {
+                $(
+                    $subconfig_name: Option<$subconfig_name>
+                ),*
+            }
+
+            impl $name {
+                pub(crate) fn from_config_file(
+                    config_file_path: Option<std::path::PathBuf>,
+                    $(
+                        $(
+                            $(
+                                $extra_input: $extra_input_type,
+                            )*
+                        )*
+                    )*
+                ) -> anyhow::Result<Self> {
+                    use anyhow::Context;
+
+                    let config_file_path =
+                        config_file_path.map_or_else(|| -> anyhow::Result<_> { super::paths::config_path() }, Ok)?;
+
+                    let config: config =
+                        toml::from_str(&std::fs::read_to_string(config_file_path).unwrap_or(String::new()))
+                            .context("Failed to parse the config file as toml")?;
+
+                    Ok(Self {
+                        $(
+                            $subconfig_name: {
+                                let toplevel = config.$subconfig_name.unwrap_or_default();
+                                $subconfig_type {
+                                    $(
+                                        $field_name: $field_name(toplevel.$field_name, $($extra_input),*)?
+                                    ),*
+                                }
+                            }
+                        ),*
+                    })
+                }
+
+                pub(crate) fn run_finalizers(&self) -> anyhow::Result<()> {
+                    #[allow(unused_imports)]
+                    use anyhow::Context;
+
+                    $(
+                        $(
+                            $(
+                                $finalizer(&self.$subconfig_name.$field_name)
+                                  .context(
+                                        concat!(
+                                            "While running the finalizer for config value '",
+                                            stringify!($subconfig_name),
+                                            ".",
+                                            stringify!($field_name),
+                                            "'"
+                                        )
+                                  )?;
+                            )?
+                        )*
+                    )*
+
+                    Ok(())
+                }
+            }
+
+            $(
+                #[derive(serde::Serialize, Debug)]
+                pub(crate) struct $subconfig_type {
+                    $(
+                        $(#[$attr1])*
+                        pub(crate) $field_name: $field_type
+                    ),*
+                }
+
+                #[derive(Debug, Default, serde::Deserialize, PartialEq)]
+                #[serde(deny_unknown_fields)]
+                #[allow(non_camel_case_types)]
+                struct $subconfig_name {
+                    $(
+                        $field_name: Option<$field_type>
+                    ),*
+                }
+
+                $(
+                    fn $field_name(
+                        config_value: Option<$field_type>,
+                        $($extra_input: $extra_input_type),*
+                    ) -> anyhow::Result<$field_type> {
+                        use anyhow::Context;
+
+                        let expr = $crate::config::support::maybe_wrap_type!($field_type =$errors $default)(config_value);
+
+                        expr.context(concat!("Failed to generate default config value for '", stringify!($field_name),"'"))
+                    }
+                )*
+            )*
+        }
+        pub(crate) use self::_inner::*;
+    };
+}
+
+macro_rules! maybe_wrap_type {
+    ($ty:ty =! $val:expr) => {
+        (|config_value: Option<$ty>| $val(config_value))
+    };
+    ($ty:ty =? $val:expr) => {
+        (|config_value: Option<$ty>| config_value.map_or_else(|| $val, Ok))
+    };
+    ($ty:ty =: $val:expr) => {
+        (|config_value: Option<$ty>| Ok::<_, anyhow::Error>(config_value.unwrap_or_else(|| $val)))
+    };
+}
+
+pub(crate) use maybe_wrap_type;
+pub(crate) use mk_config;
diff --git a/crates/yt/src/main.rs b/crates/yt/src/main.rs
index f6f18be..faee401 100644
--- a/crates/yt/src/main.rs
+++ b/crates/yt/src/main.rs
@@ -87,12 +87,18 @@ async fn main() -> Result<()> {
         }
     });
 
-    let config = Config::from_config_file(args.db_path, args.config_path, args.color)?;
+    let config = Config::from_config_file(args.config_path, args.color, args.db_path)?;
     if args.version {
         version::show(&config).await?;
         return Ok(());
     }
 
+    // Perform config finalization _after_ checking for the version
+    // so that version always works.
+    config
+        .run_finalizers()
+        .context("Failed to finalize config for usage")?;
+
     let app = App::new(config, !args.no_migrate_db).await?;
 
     match args.command.unwrap_or(Command::default()) {