#include "ApplicationContext.h" #include "Steam/FileSystem/FileSystem.h" #include "WallpaperEngine/Data/JSON.h" #include "WallpaperEngine/Logging/Log.h" #include "WallpaperEngine/Render/CFBO.h" #include #include #include #include #include #include #include #include #include #include #include #define WORKSHOP_APP_ID 431960 #define APP_DIRECTORY "wallpaper_engine" using namespace WallpaperEngine::Application; using WallpaperEngine::Data::JSON::JSON; std::filesystem::path ApplicationContext::resolvePlaylistItemPath (const std::string& raw) const { if (raw.empty ()) { return {}; } std::string cleaned = raw; constexpr std::string_view windowsPrefix = "\\\\?\\"; if (cleaned.rfind (windowsPrefix, 0) == 0) { cleaned = cleaned.substr (windowsPrefix.length ()); } std::replace (cleaned.begin (), cleaned.end (), '\\', '/'); if (cleaned.size () > 1 && cleaned[1] == ':') { cleaned = cleaned.substr (2); } if (!cleaned.empty () && cleaned.front () != '/') { cleaned.insert (cleaned.begin (), '/'); } std::filesystem::path path = std::filesystem::path (cleaned).lexically_normal (); if (std::filesystem::is_regular_file (path)) { path = path.parent_path (); } return path; } std::filesystem::path ApplicationContext::configFilePath () const { try { return Steam::FileSystem::appDirectory (APP_DIRECTORY, "") / "config.json"; } catch (std::runtime_error&) { sLog.exception ("Cannot locate wallpaper engine installation to read config.json"); return {}; } } std::optional ApplicationContext::parseConfigJson (const std::filesystem::path& path) const { if (path.empty ()) { return std::nullopt; } std::ifstream configFile (path); if (!configFile.is_open ()) { sLog.exception ("Cannot open wallpaper engine config file at ", path); return std::nullopt; } try { return JSON::parse (configFile); } catch (const std::exception& e) { sLog.error ("Failed parsing wallpaper engine config.json: ", e.what ()); return std::nullopt; } } std::optional ApplicationContext::buildPlaylistDefinition (const JSON& playlistJson, const std::string& fallbackName) const { PlaylistDefinition definition; definition.name = playlistJson.optional ("name", fallbackName); definition.settings = this->parsePlaylistSettings (playlistJson); definition.items = this->collectPlaylistItems (playlistJson, definition.name.empty () ? fallbackName : definition.name); if (definition.items.empty ()) { return std::nullopt; } if (definition.name.empty ()) { if (fallbackName.empty ()) { sLog.error ("Skipping playlist with no name"); return std::nullopt; } definition.name = fallbackName; } return definition; } ApplicationContext::PlaylistSettings ApplicationContext::parsePlaylistSettings (const JSON& playlistJson) const { PlaylistSettings settings; const auto settingsJson = playlistJson.optional ("settings"); settings.delayMinutes = settingsJson ? settingsJson->optional ("delay", 60) : 60; settings.mode = settingsJson ? settingsJson->optional ("mode", "timer") : "timer"; settings.order = settingsJson ? settingsJson->optional ("order", "sequential") : "sequential"; settings.updateOnPause = settingsJson ? settingsJson->optional ("updateonpause", false) : false; settings.videoSequence = settingsJson ? settingsJson->optional ("videosequence", false) : false; return settings; } std::vector ApplicationContext::collectPlaylistItems (const JSON& playlistJson, const std::string& name) const { std::vector items; const auto jsonItems = playlistJson.optional ("items"); if (!jsonItems.has_value () || !jsonItems->is_array ()) { sLog.error ("Skipping playlist ", name, ": missing items"); return items; } for (const auto& rawItem : *jsonItems) { if (!rawItem.is_string ()) { continue; } auto resolvedPath = this->resolvePlaylistItemPath (rawItem.get ()); if (resolvedPath.empty ()) { continue; } if (!std::filesystem::exists (resolvedPath)) { sLog.error ("Skipping playlist item not found: ", resolvedPath.string ()); continue; } items.push_back (resolvedPath); } if (items.empty ()) { sLog.error ("Skipping playlist ", name, ": no usable items found"); } return items; } void ApplicationContext::registerPlaylist (PlaylistDefinition&& definition) { this->m_configPlaylists.insert_or_assign (definition.name, std::move (definition)); } void ApplicationContext::loadPlaylistsFromConfig () { if (this->m_loadedConfigPlaylists) { return; } this->m_loadedConfigPlaylists = true; const auto configPath = this->configFilePath (); const auto root = this->parseConfigJson (configPath); if (!root.has_value ()) { return; } const auto steamUser = root->optional ("steamuser"); if (!steamUser.has_value ()) { sLog.exception ("Cannot find steamuser section in config.json"); } auto addPlaylist = [this] (const JSON& playlistJson, const std::string& fallbackName) { auto definition = this->buildPlaylistDefinition (playlistJson, fallbackName); if (!definition) { return; } this->registerPlaylist (std::move (*definition)); }; if (const auto general = steamUser->optional ("general")) { if (const auto playlists = general->optional ("playlists")) { for (const auto& playlist : *playlists) { try { addPlaylist (playlist, playlist.optional ("name", "")); } catch (const std::exception& e) { sLog.error ("Failed parsing playlist: ", e.what ()); } } } } if (const auto wallpaperConfig = steamUser->optional ("wallpaperconfig")) { if (const auto selected = wallpaperConfig->optional ("selectedwallpapers")) { for (const auto& entry : selected->items ()) { const auto playlist = entry.value ().optional ("playlist"); if (!playlist.has_value ()) { continue; } try { addPlaylist (*playlist, entry.key ()); } catch (const std::exception& e) { sLog.error ("Failed parsing playlist for ", entry.key (), ": ", e.what ()); } } } } } const ApplicationContext::PlaylistDefinition& ApplicationContext::getPlaylistFromConfig (const std::string& name) { if (!this->m_loadedConfigPlaylists) { this->loadPlaylistsFromConfig (); } const auto cur = this->m_configPlaylists.find (name); if (cur == this->m_configPlaylists.end ()) { std::string available; for (auto it = this->m_configPlaylists.begin (); it != this->m_configPlaylists.end (); ++it) { available += it->first; if (std::next (it) != this->m_configPlaylists.end ()) { available += ", "; } } const std::string availableText = available.empty () ? "" : std::string (". Available: ") + available; sLog.exception ("Playlist not found in config.json: ", name, availableText); throw std::runtime_error ("Playlist not found: " + name); } return cur->second; } ApplicationContext::ApplicationContext (int argc, char* argv[]) : m_argc (argc), m_argv (argv) { } namespace { bool matchesObjectToken (const std::string& token, int id, const std::string& name) { if (token == name) { return true; } int parsed = 0; const auto* end = token.data () + token.size (); const auto result = std::from_chars (token.data (), end, parsed); return result.ec == std::errc () && result.ptr == end && parsed == id; } } // namespace std::optional ApplicationContext::resolveObjectVisibility (int id, const std::string& name) const { for (const auto& token : this->settings.general.disabledObjects) { if (matchesObjectToken (token, id, name)) { return false; } } for (const auto& token : this->settings.general.enabledObjects) { if (matchesObjectToken (token, id, name)) { return true; } } return std::nullopt; } void ApplicationContext::loadSettingsFromArgv () { std::string lastScreen; argparse::ArgumentParser program ("linux-wallpaperengine", "0.0", argparse::default_arguments::help); auto& backgroundGroup = program.add_group ("Background options"); auto& backgroundMode = backgroundGroup.add_mutually_exclusive_group (false); backgroundGroup.add_argument ("background id") .help ("The background to use as default for screens with no background specified") .default_value ("") .action ([this] (const std::string& value) -> void { if (!value.empty ()) { this->settings.general.defaultBackground = translateBackground (value); } }); // Internal, not advertised in --help. Appended to CEF subprocess re-execs so they see the // *current* (possibly hotswapped) background instead of the "background id" positional's // launch-time value. Registered last so it takes precedence. backgroundGroup.add_argument ("--current-background") .default_value ("") .hidden () .action ([this] (const std::string& value) -> void { if (!value.empty ()) { this->settings.general.defaultBackground = translateBackground (value); } }); // Internal, not advertised in --help: marks a self-re-exec as a disposable CEF host for one // Web wallpaper. backgroundGroup.add_argument ("--web-host") .flag () .hidden () .store_into (this->settings.general.webHost); backgroundGroup.add_argument ("--web-host-shm") .default_value ("") .hidden () .store_into (this->settings.general.webHostShm); backgroundGroup.add_argument ("--web-host-width") .default_value (0) .hidden () .action ([this] (const std::string& value) -> void { try { this->settings.general.webHostWidth = static_cast (std::stoul (value)); } catch (const std::exception&) { sLog.exception ("Invalid --web-host-width value: ", value); } }); backgroundGroup.add_argument ("--web-host-height") .default_value (0) .hidden () .action ([this] (const std::string& value) -> void { try { this->settings.general.webHostHeight = static_cast (std::stoul (value)); } catch (const std::exception&) { sLog.exception ("Invalid --web-host-height value: ", value); } }); backgroundMode.add_argument ("-w", "--window") .help ("Window geometry to use for the given screen") .action ([this] (const std::string& value) -> void { if (this->settings.render.mode == DESKTOP_BACKGROUND) { sLog.exception ("Cannot run in both background and window mode"); } if (this->settings.render.mode == EXPLICIT_WINDOW) { sLog.exception ("Only one window at a time can be specified in explicit window mode"); } this->settings.render.mode = EXPLICIT_WINDOW; if (value.empty ()) { sLog.exception ("Window geometry cannot be empty"); } const char* str = value.c_str (); const char* delim1 = strchr (str, 'x'); const char* delim2 = delim1 ? strchr (delim1 + 1, 'x') : nullptr; const char* delim3 = delim2 ? strchr (delim2 + 1, 'x') : nullptr; if (delim1 == nullptr || delim2 == nullptr || delim3 == nullptr) { sLog.exception ("Window geometry must be in the format: XxYxWxH"); } this->settings.render.window.geometry.x = strtol (str, nullptr, 10); this->settings.render.window.geometry.y = strtol (delim1 + 1, nullptr, 10); this->settings.render.window.geometry.z = strtol (delim2 + 1, nullptr, 10); this->settings.render.window.geometry.w = strtol (delim3 + 1, nullptr, 10); }) .append (); backgroundMode.add_argument ("-r", "--screen-root") .help ("The screen the following settings will have an effect on") .action ([this, &lastScreen] (const std::string& value) -> void { if (this->settings.general.screenBackgrounds.find (value) != this->settings.general.screenBackgrounds.end ()) { sLog.exception ("Cannot specify the same screen more than once: ", value); } for (const auto& group : this->settings.general.spanGroups) { if (std::find (group.screens.begin (), group.screens.end (), value) != group.screens.end ()) { sLog.exception ("--screen-root: screen '", value, "' already belongs to a span group"); } } if (this->settings.render.mode == EXPLICIT_WINDOW) { sLog.exception ("Cannot run in both background and window mode"); } this->settings.render.mode = DESKTOP_BACKGROUND; lastScreen = value; this->settings.general.screenBackgrounds[lastScreen] = ""; this->settings.general.screenScalings[lastScreen] = this->settings.render.window.scalingMode; this->settings.general.screenClamps[lastScreen] = this->settings.render.window.clamp; }) .append (); backgroundGroup.add_argument ("--screen-span") .help ("Comma-separated list of screens to span a single wallpaper across") .action ([this, &lastScreen] (const std::string& value) -> void { if (this->settings.render.mode == EXPLICIT_WINDOW) { sLog.exception ("Cannot run in both background and window mode"); } this->settings.render.mode = DESKTOP_BACKGROUND; SpanGroup group; std::string screen; std::istringstream ss (value); while (std::getline (ss, screen, ',')) { if (screen.empty ()) { continue; } if (this->settings.general.screenBackgrounds.find (screen) != this->settings.general.screenBackgrounds.end ()) { sLog.exception ("--screen-span: screen '", screen, "' is already configured individually"); } // reject duplicates within this group if (std::find (group.screens.begin (), group.screens.end (), screen) != group.screens.end ()) { sLog.exception ("--screen-span: duplicate screen name '", screen, "'"); } // reject screens already claimed by another span group for (const auto& existing : this->settings.general.spanGroups) { if (std::find (existing.screens.begin (), existing.screens.end (), screen) != existing.screens.end ()) { sLog.exception ("--screen-span: screen '", screen, "' already belongs to another span group"); } } group.screens.push_back (screen); } if (group.screens.size () < 2) { sLog.exception ("--screen-span requires at least two comma-separated screen names"); } group.scaling = this->settings.render.window.scalingMode; group.clamp = this->settings.render.window.clamp; this->settings.general.spanGroups.push_back (std::move (group)); // set lastScreen to a synthetic name so --bg/--scaling/--clamp can target this group lastScreen = "span:" + value; // register the synthetic name in screenBackgrounds so the rest of the pipeline sees it this->settings.general.screenBackgrounds[lastScreen] = ""; }) .append (); backgroundGroup.add_argument ("-b", "--bg") .help ("After --screen-root or --screen-span, specifies the background to use") .action ([this, &lastScreen] (const std::string& value) -> void { this->settings.general.screenBackgrounds[lastScreen] = translateBackground (value); // set the default background to the last one used this->settings.general.defaultBackground = translateBackground (value); // if this targets a span group, update the group's background too if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) { this->settings.general.spanGroups.back ().background = translateBackground (value); } }) .append (); backgroundGroup.add_argument ("--playlist") .help ( "Uses a playlist from wallpaper engine's config.json. If used after --screen-root it is applied to that " "screen, otherwise it is used in window mode." ) .action ([this, &lastScreen] (const std::string& value) -> void { const auto& playlist = this->getPlaylistFromConfig (value); if (lastScreen.empty ()) { this->settings.general.defaultPlaylist = playlist; if (this->settings.general.defaultBackground.empty () && !playlist.items.empty ()) { this->settings.general.defaultBackground = playlist.items.front (); } } else { this->settings.general.screenPlaylists[lastScreen] = playlist; if (!playlist.items.empty ()) { this->settings.general.screenBackgrounds[lastScreen] = playlist.items.front (); } if (this->settings.general.defaultBackground.empty () && !playlist.items.empty ()) { this->settings.general.defaultBackground = playlist.items.front (); } } }) .append (); backgroundGroup.add_argument ("--scaling") .help ( "Scaling mode to use when rendering the background, this applies to the previous --window, --screen-root, " "or --screen-span output, or the default background if no other background is specified" ) .choices ("stretch", "fit", "fill", "center", "default") .action ([this, &lastScreen] (const std::string& value) -> void { const auto parsed = WallpaperEngine::Render::WallpaperState::parseScalingMode (value); if (!parsed.has_value ()) { sLog.exception ("Invalid scaling mode: ", value); } const auto mode = *parsed; if (this->settings.render.mode == DESKTOP_BACKGROUND) { this->settings.general.screenScalings[lastScreen] = mode; // also update span group if targeting one if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) { this->settings.general.spanGroups.back ().scaling = mode; } } else { this->settings.render.window.scalingMode = mode; } }) .append (); backgroundGroup.add_argument ("--clamp") .help ( "Clamp mode to use when rendering the background, this applies to the previous --window, --screen-root, " "or --screen-span output, or the default background if no other background is specified" ) .choices ("clamp", "border", "repeat") .action ([this, &lastScreen] (const std::string& value) -> void { TextureFlags flags; if (value == "clamp") { flags = TextureFlags_ClampUVs; } else if (value == "border") { flags = TextureFlags_ClampUVsBorder; } else if (value == "repeat") { flags = TextureFlags_NoFlags; } else { sLog.exception ("Invalid clamp mode: ", value); } if (this->settings.render.mode == DESKTOP_BACKGROUND) { this->settings.general.screenClamps[lastScreen] = flags; // also update span group if targeting one if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) { this->settings.general.spanGroups.back ().clamp = flags; } } else { this->settings.render.window.clamp = flags; } }) .append (); backgroundGroup.add_argument ("--zoom") .help ( "Manual zoom factor layered on top of --scaling (e.g. 1.5 zooms in 50%, 0.5 zooms out to half size), " "this applies to the previous --window, --screen-root, or --screen-span output, or the default " "background if no other background is specified" ) .action ([this, &lastScreen] (const std::string& value) -> void { float zoom; try { zoom = std::stof (value); } catch (const std::exception&) { sLog.exception ("Invalid zoom value: ", value); } if (this->settings.render.mode == DESKTOP_BACKGROUND) { this->settings.general.screenZooms[lastScreen] = zoom; // also update span group if targeting one if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) { this->settings.general.spanGroups.back ().zoom = zoom; } } else { this->settings.render.window.zoom = zoom; } }) .append (); backgroundGroup.add_argument ("--corner-color") .help ( "Color to show outside the wallpaper's bounds (Center/Fit letterboxing, zoomed-out scaling) when " "--clamp is border, as a hex RGB or RGBA value (e.g. \"000000\" or \"#1a1a1aff\"). Default: opaque " "black. This applies to the previous --window, --screen-root, or --screen-span output, or the " "default background if no other background is specified" ) .action ([this, &lastScreen] (const std::string& value) -> void { const auto color = WallpaperEngine::Render::CFBO::parseColor (value); if (!color.has_value ()) { sLog.exception ("Invalid corner color: ", value); } if (this->settings.render.mode == DESKTOP_BACKGROUND) { this->settings.general.screenCornerColors[lastScreen] = *color; // also update span group if targeting one if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) { this->settings.general.spanGroups.back ().cornerColor = *color; } } else { this->settings.render.window.cornerColor = *color; } }) .append (); backgroundGroup.add_argument ("--layer") .help ( "Wayland-only: which wlr-layer-shell layer to anchor the wallpaper to " "(background, bottom, top, overlay). Default: background on KDE, bottom elsewhere. " "KDE defaults to 'background' because KWin's 'show desktop' (Super+D) hides 'bottom' surfaces. " "Use 'background' on niri to pair with the `place-within-backdrop` layer-rule, " "otherwise the wallpaper will be cloned to every workspace in the overview." ) .choices ("background", "bottom", "top", "overlay") .default_value (std::string ("bottom")) .action ([this] (const std::string& value) -> void { if (value == "background") { this->settings.render.wayland.layer = WAYLAND_LAYER_BACKGROUND; } else if (value == "bottom") { this->settings.render.wayland.layer = WAYLAND_LAYER_BOTTOM; } else if (value == "top") { this->settings.render.wayland.layer = WAYLAND_LAYER_TOP; } else if (value == "overlay") { this->settings.render.wayland.layer = WAYLAND_LAYER_OVERLAY; } else { sLog.exception ("Invalid wlr-layer-shell layer: ", value); } }); auto& performanceGroup = program.add_group ("Performance options"); performanceGroup.add_argument ("-f", "--fps") .help ("Limits the FPS to the given number, useful to keep battery consumption low") .default_value (60) .store_into (this->settings.render.maximumFPS); performanceGroup.add_argument ("--speed") .help ("Global playback speed multiplier for animations, particles and effects (e.g. 0.5 for half speed, " "2.0 for double speed). Useful to calm down backgrounds with overly fast particle effects") .action ([this] (const std::string& value) -> void { float speed; try { speed = std::stof (value); } catch (const std::exception&) { sLog.exception ("Invalid speed value: ", value); } if (speed <= 0.0f) { sLog.exception ("--speed must be a positive number"); } this->settings.render.playbackSpeed = speed; }); performanceGroup.add_argument ("--no-fullscreen-pause") .help ("Prevents the background pausing when an app is fullscreen") .flag () .action ([this] (const std::string& value) -> void { this->settings.render.pauseOnFullscreen = false; }); performanceGroup.add_argument ("--fullscreen-pause-only-active") .help ("Wayland only: pause only when a fullscreen window is active (activated)") .flag () .action ([this] (const std::string& value) -> void { this->settings.render.pauseOnFullscreenOnlyWhenActive = true; }); performanceGroup.add_argument ("--fullscreen-pause-ignore-appid") .help ("Wayland only: ignore fullscreen windows whose app_id contains this value (repeatable)") .action ([this] (const std::string& value) -> void { if (!value.empty ()) { this->settings.render.fullscreenPauseIgnoreAppIds.push_back (value); } }) .append (); auto& audioGroup = program.add_group ("Sound settings"); auto& audioSettingsGroup = audioGroup.add_mutually_exclusive_group (false); audioSettingsGroup.add_argument ("-v", "--volume") .help ("Volume for all the sounds in the background") .default_value (15) .store_into (this->settings.audio.volume); audioSettingsGroup.add_argument ("-s", "--silent") .help ("Mutes all the sound the wallpaper might produce") .flag () .action ([this] (const std::string& value) -> void { this->settings.audio.enabled = false; }); audioGroup.add_argument ("--automute") .help ("Mutes the wallpaper's audio when another app is playing sound") .flag () .action ([this] (const std::string& value) -> void { this->settings.audio.automute = true; }); audioGroup.add_argument ("--noautomute") .help ("Disables the automute when an app is playing sound (default)") .flag () .action ([this] (const std::string& value) -> void { this->settings.audio.automute = false; }); audioGroup.add_argument ("--no-audio-processing") .help ("Disables audio processing for backgrounds") .flag () .action ([this] (const std::string& value) -> void { this->settings.audio.audioprocessing = false; }); auto& screenshotGroup = program.add_group ("Screenshot options"); screenshotGroup.add_argument ("--screenshot") .help ("Takes a screenshot of the background for it's use with tools like PyWAL") .default_value ("") .action ([this] (const std::string& value) -> void { this->settings.screenshot.take = true; this->settings.screenshot.path = value; }); screenshotGroup.add_argument ("--screenshot-delay") .help ("Frames to wait before taking the screenshot") .default_value (5) .store_into (this->settings.screenshot.delay); auto& contentGroup = program.add_group ("Content options"); contentGroup.add_argument ("--assets-dir") .help ("Folder where the assets are stored") .default_value ("") .action ([this] (const std::string& value) -> void { this->settings.general.assets = value; }); auto& configurationGroup = program.add_group ("Wallpaper configuration options"); configurationGroup.add_argument ("--disable-particles") .help ("Disables particles for the backgrounds") .flag () .action ([this] (const std::string& value) -> void { this->settings.general.disableParticles = true; }); configurationGroup.add_argument ("--disable-mouse") .help ("Disables mouse interaction with the backgrounds") .flag () .action ([this] (const std::string& value) -> void { this->settings.mouse.enabled = false; }); configurationGroup.add_argument ("--disable-parallax") .help ("Disables parallax effect for the backgrounds") .flag () .action ([this] (const std::string& value) -> void { this->settings.mouse.disableparallax = true; }); configurationGroup.add_argument ("-l", "--list-properties") .help ("List all the available properties and their configuration") .flag () .store_into (this->settings.general.onlyListProperties); configurationGroup.add_argument ("--set-property", "--property") .help ("Overrides the default value of the given property") .action ([this] (const std::string& value) -> void { const std::string::size_type equals = value.find ('='); // properties without value are treated as booleans for now if (equals == std::string::npos) { this->settings.general.properties[value] = "1"; } else { this->settings.general.properties[value.substr (0, equals)] = value.substr (equals + 1); } }) .append (); configurationGroup.add_argument ("--list-objects") .help ("List all the objects/layers a background has, with their id, name and type") .flag () .store_into (this->settings.general.onlyListObjects); configurationGroup.add_argument ("--disable-object") .help ("Hides an object/layer (parallax layer, clock, particles, etc), matched by id or name. Can be repeated") .action ([this] (const std::string& value) -> void { this->settings.general.disabledObjects.push_back (value); } ) .append (); configurationGroup.add_argument ("--enable-object") .help ("Forces an object/layer to show even if the background hides it by default, matched by id or name. " "Can be repeated") .action ([this] (const std::string& value) -> void { this->settings.general.enabledObjects.push_back (value); } ) .append (); auto& debuggingGroup = program.add_group ("Debugging options"); debuggingGroup.add_argument ("-z", "--dump-structure") .help ("Dumps the structure of the backgrounds") .flag () .store_into (this->settings.general.dumpStructure); debuggingGroup.add_argument ("--render-debug") .help ( "Scene render debug mode: base-only, no-solid-final, pass-log, object=, skip-object=, or " "skip-effect=. Can be repeated." ) .action ([this] (const std::string& value) -> void { const auto parseDebugId = [&value] (const std::string& prefix) -> std::optional { try { return std::stoi (value.substr (prefix.length ())); } catch (const std::invalid_argument&) { sLog.exception ("Invalid numeric value for --render-debug ", value); } catch (const std::out_of_range&) { sLog.exception ("Out-of-range numeric value for --render-debug ", value); } return std::nullopt; }; if (value == "base-only") { this->settings.render.debug.baseOnly = true; } else if (value == "no-solid-final") { this->settings.render.debug.noSolidFinal = true; } else if (value == "pass-log") { this->settings.render.debug.passLog = true; } else if (value.rfind ("object=", 0) == 0) { this->settings.render.debug.objectFilter = parseDebugId ("object="); } else if (value.rfind ("skip-object=", 0) == 0) { if (const auto id = parseDebugId ("skip-object="); id.has_value ()) { this->settings.render.debug.skipObjects.emplace_back (*id); } } else if (value.rfind ("skip-effect=", 0) == 0) { if (const auto id = parseDebugId ("skip-effect="); id.has_value ()) { this->settings.render.debug.skipEffects.emplace_back (*id); } } else { sLog.exception ("Invalid render debug mode: ", value); } }) .append (); program.add_epilog ( "Usage examples:\n" " linux-wallpaperengine --screen-root HDMI-1 --bg 2317494988 --scaling fill --clamp border\n" " Runs the background 2317494988 on screen HDMI-1, scaling it to fill the screen and clamping the UVs to " "the border\n\n" " linux-wallpaperengine 2317494988\n" " Previews the background 2317494988 on a window\n\n" " linux-wallpaperengine --screen-root HDMI-1 --bg 2317494988 --screen-root HDMI-2 --bg 1108150151\n" " Runs two backgrounds on two screens, one on HDMI-1 and the other on HDMI-2\n\n" " linux-wallpaperengine --screen-root HDMI-1 --screen-root HDMI-2 2317494988\n" " Runs the background 2317494988 on two screens, one on HDMI-1 and the other on HDMI-2\n\n" " linux-wallpaperengine --screen-span HDMI-1,HDMI-2 --bg 2317494988 --scaling fill\n" " Spans the background 2317494988 across HDMI-1 and HDMI-2 as a single stretched wallpaper\n\n" ); try { program.parse_known_args (this->m_argc, this->m_argv); if (!program.is_used ("--layer")) { const auto* desktopSession = std::getenv ("DESKTOP_SESSION"); const auto* xdgDesktop = std::getenv ("XDG_SESSION_DESKTOP"); const auto* kdeSession = std::getenv ("KDE_FULL_SESSION"); bool onKDE = false; if (desktopSession) { std::string_view s (desktopSession); onKDE = s.find ("kde") != std::string_view::npos || s.find ("plasma") != std::string_view::npos; } if (!onKDE && xdgDesktop && std::string_view (xdgDesktop) == "KDE") { onKDE = true; } if (!onKDE && kdeSession && std::string_view (kdeSession) == "true") { onKDE = true; } if (onKDE) { this->settings.render.wayland.layer = WAYLAND_LAYER_BACKGROUND; sLog.out ("KDE detected: using --layer background to prevent 'show desktop' from hiding the wallpaper (use --layer bottom to override)"); } } if (this->settings.general.defaultBackground.empty ()) { throw std::runtime_error ("At least one background ID must be specified"); } this->settings.audio.volume = std::max (0, std::min (this->settings.audio.volume, 128)); this->settings.screenshot.delay = std::max (0, std::min (this->settings.screenshot.delay, 5)); // use std::cout on this in case logging is disabled, this way it's easy to look at what is running std::stringbuf buffer; std::ostream bufferStream (&buffer); bufferStream << "Running with: "; for (int i = 0; i < this->m_argc; i++) { bufferStream << this->m_argv[i]; bufferStream << " "; } std::cout << buffer.str () << std::endl; this->validateAssets (); this->validateScreenshot (); this->state.general.keepRunning = true; this->state.audio.enabled = this->settings.audio.enabled; this->state.audio.volume = this->settings.audio.volume; this->state.mouse.enabled = this->settings.mouse.enabled; #if DEMOMODE sLog.error ("WARNING: RUNNING IN DEMO MODE WILL STOP WALLPAPERS AFTER 5 SECONDS SO VIDEO CAN BE RECORDED"); this->settings.render.maximumFPS = 30; this->settings.screenshot.take = false; this->settings.render.pauseOnFullscreen = false; #endif /* DEMOMODE */ } catch (const std::runtime_error& e) { throw std::runtime_error ( std::string (e.what ()) + ". Use " + std::string (this->m_argv[0]) + " --help for more information" ); } } int ApplicationContext::getArgc () const { return this->m_argc; } char** ApplicationContext::getArgv () const { return this->m_argv; } std::filesystem::path ApplicationContext::translateBackground (const std::string& bgIdOrPath) { if (bgIdOrPath.find ('/') == std::string::npos) { return Steam::FileSystem::workshopDirectory (WORKSHOP_APP_ID, bgIdOrPath); } return bgIdOrPath; } void ApplicationContext::validateAssets () { if (!this->settings.general.assets.empty ()) { sLog.out ( "Using wallpaper engine's assets at ", this->settings.general.assets, " based on --assets-dir parameter" ); return; } try { this->settings.general.assets = Steam::FileSystem::appDirectory (APP_DIRECTORY, "assets"); } catch (std::runtime_error&) { this->settings.general.assets = std::filesystem::canonical ("/proc/self/exe").parent_path () / "assets"; } } void ApplicationContext::validateScreenshot () const { if (!this->settings.screenshot.take) { return; } if (!this->settings.screenshot.path.has_extension ()) { sLog.exception ("Cannot determine screenshot format"); } const std::string extension = this->settings.screenshot.path.extension (); if (extension != ".bmp" && extension != ".png" && extension != ".jpeg" && extension != ".jpg") { sLog.exception ("Cannot determine screenshot format, unknown extension ", extension); } }