ApplicationContext.cpp 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934
  1. #include "ApplicationContext.h"
  2. #include "Steam/FileSystem/FileSystem.h"
  3. #include "WallpaperEngine/Data/JSON.h"
  4. #include "WallpaperEngine/Logging/Log.h"
  5. #include "WallpaperEngine/Render/CFBO.h"
  6. #include <algorithm>
  7. #include <charconv>
  8. #include <cstdlib>
  9. #include <cstring>
  10. #include <fstream>
  11. #include <iostream>
  12. #include <optional>
  13. #include <sstream>
  14. #include <stdexcept>
  15. #include <string_view>
  16. #include <argparse/argparse.hpp>
  17. #define WORKSHOP_APP_ID 431960
  18. #define APP_DIRECTORY "wallpaper_engine"
  19. using namespace WallpaperEngine::Application;
  20. using WallpaperEngine::Data::JSON::JSON;
  21. std::filesystem::path ApplicationContext::resolvePlaylistItemPath (const std::string& raw) const {
  22. if (raw.empty ()) {
  23. return {};
  24. }
  25. std::string cleaned = raw;
  26. constexpr std::string_view windowsPrefix = "\\\\?\\";
  27. if (cleaned.rfind (windowsPrefix, 0) == 0) {
  28. cleaned = cleaned.substr (windowsPrefix.length ());
  29. }
  30. std::replace (cleaned.begin (), cleaned.end (), '\\', '/');
  31. if (cleaned.size () > 1 && cleaned[1] == ':') {
  32. cleaned = cleaned.substr (2);
  33. }
  34. if (!cleaned.empty () && cleaned.front () != '/') {
  35. cleaned.insert (cleaned.begin (), '/');
  36. }
  37. std::filesystem::path path = std::filesystem::path (cleaned).lexically_normal ();
  38. if (std::filesystem::is_regular_file (path)) {
  39. path = path.parent_path ();
  40. }
  41. return path;
  42. }
  43. std::filesystem::path ApplicationContext::configFilePath () const {
  44. try {
  45. return Steam::FileSystem::appDirectory (APP_DIRECTORY, "") / "config.json";
  46. } catch (std::runtime_error&) {
  47. sLog.exception ("Cannot locate wallpaper engine installation to read config.json");
  48. return {};
  49. }
  50. }
  51. std::optional<JSON> ApplicationContext::parseConfigJson (const std::filesystem::path& path) const {
  52. if (path.empty ()) {
  53. return std::nullopt;
  54. }
  55. std::ifstream configFile (path);
  56. if (!configFile.is_open ()) {
  57. sLog.exception ("Cannot open wallpaper engine config file at ", path);
  58. return std::nullopt;
  59. }
  60. try {
  61. return JSON::parse (configFile);
  62. } catch (const std::exception& e) {
  63. sLog.error ("Failed parsing wallpaper engine config.json: ", e.what ());
  64. return std::nullopt;
  65. }
  66. }
  67. std::optional<ApplicationContext::PlaylistDefinition>
  68. ApplicationContext::buildPlaylistDefinition (const JSON& playlistJson, const std::string& fallbackName) const {
  69. PlaylistDefinition definition;
  70. definition.name = playlistJson.optional<std::string> ("name", fallbackName);
  71. definition.settings = this->parsePlaylistSettings (playlistJson);
  72. definition.items
  73. = this->collectPlaylistItems (playlistJson, definition.name.empty () ? fallbackName : definition.name);
  74. if (definition.items.empty ()) {
  75. return std::nullopt;
  76. }
  77. if (definition.name.empty ()) {
  78. if (fallbackName.empty ()) {
  79. sLog.error ("Skipping playlist with no name");
  80. return std::nullopt;
  81. }
  82. definition.name = fallbackName;
  83. }
  84. return definition;
  85. }
  86. ApplicationContext::PlaylistSettings ApplicationContext::parsePlaylistSettings (const JSON& playlistJson) const {
  87. PlaylistSettings settings;
  88. const auto settingsJson = playlistJson.optional ("settings");
  89. settings.delayMinutes = settingsJson ? settingsJson->optional<uint32_t> ("delay", 60) : 60;
  90. settings.mode = settingsJson ? settingsJson->optional<std::string> ("mode", "timer") : "timer";
  91. settings.order = settingsJson ? settingsJson->optional<std::string> ("order", "sequential") : "sequential";
  92. settings.updateOnPause = settingsJson ? settingsJson->optional<bool> ("updateonpause", false) : false;
  93. settings.videoSequence = settingsJson ? settingsJson->optional<bool> ("videosequence", false) : false;
  94. return settings;
  95. }
  96. std::vector<std::filesystem::path>
  97. ApplicationContext::collectPlaylistItems (const JSON& playlistJson, const std::string& name) const {
  98. std::vector<std::filesystem::path> items;
  99. const auto jsonItems = playlistJson.optional ("items");
  100. if (!jsonItems.has_value () || !jsonItems->is_array ()) {
  101. sLog.error ("Skipping playlist ", name, ": missing items");
  102. return items;
  103. }
  104. for (const auto& rawItem : *jsonItems) {
  105. if (!rawItem.is_string ()) {
  106. continue;
  107. }
  108. auto resolvedPath = this->resolvePlaylistItemPath (rawItem.get<std::string> ());
  109. if (resolvedPath.empty ()) {
  110. continue;
  111. }
  112. if (!std::filesystem::exists (resolvedPath)) {
  113. sLog.error ("Skipping playlist item not found: ", resolvedPath.string ());
  114. continue;
  115. }
  116. items.push_back (resolvedPath);
  117. }
  118. if (items.empty ()) {
  119. sLog.error ("Skipping playlist ", name, ": no usable items found");
  120. }
  121. return items;
  122. }
  123. void ApplicationContext::registerPlaylist (PlaylistDefinition&& definition) {
  124. this->m_configPlaylists.insert_or_assign (definition.name, std::move (definition));
  125. }
  126. void ApplicationContext::loadPlaylistsFromConfig () {
  127. if (this->m_loadedConfigPlaylists) {
  128. return;
  129. }
  130. this->m_loadedConfigPlaylists = true;
  131. const auto configPath = this->configFilePath ();
  132. const auto root = this->parseConfigJson (configPath);
  133. if (!root.has_value ()) {
  134. return;
  135. }
  136. const auto steamUser = root->optional ("steamuser");
  137. if (!steamUser.has_value ()) {
  138. sLog.exception ("Cannot find steamuser section in config.json");
  139. }
  140. auto addPlaylist = [this] (const JSON& playlistJson, const std::string& fallbackName) {
  141. auto definition = this->buildPlaylistDefinition (playlistJson, fallbackName);
  142. if (!definition) {
  143. return;
  144. }
  145. this->registerPlaylist (std::move (*definition));
  146. };
  147. if (const auto general = steamUser->optional ("general")) {
  148. if (const auto playlists = general->optional ("playlists")) {
  149. for (const auto& playlist : *playlists) {
  150. try {
  151. addPlaylist (playlist, playlist.optional<std::string> ("name", ""));
  152. } catch (const std::exception& e) {
  153. sLog.error ("Failed parsing playlist: ", e.what ());
  154. }
  155. }
  156. }
  157. }
  158. if (const auto wallpaperConfig = steamUser->optional ("wallpaperconfig")) {
  159. if (const auto selected = wallpaperConfig->optional ("selectedwallpapers")) {
  160. for (const auto& entry : selected->items ()) {
  161. const auto playlist = entry.value ().optional ("playlist");
  162. if (!playlist.has_value ()) {
  163. continue;
  164. }
  165. try {
  166. addPlaylist (*playlist, entry.key ());
  167. } catch (const std::exception& e) {
  168. sLog.error ("Failed parsing playlist for ", entry.key (), ": ", e.what ());
  169. }
  170. }
  171. }
  172. }
  173. }
  174. const ApplicationContext::PlaylistDefinition& ApplicationContext::getPlaylistFromConfig (const std::string& name) {
  175. if (!this->m_loadedConfigPlaylists) {
  176. this->loadPlaylistsFromConfig ();
  177. }
  178. const auto cur = this->m_configPlaylists.find (name);
  179. if (cur == this->m_configPlaylists.end ()) {
  180. std::string available;
  181. for (auto it = this->m_configPlaylists.begin (); it != this->m_configPlaylists.end (); ++it) {
  182. available += it->first;
  183. if (std::next (it) != this->m_configPlaylists.end ()) {
  184. available += ", ";
  185. }
  186. }
  187. const std::string availableText = available.empty () ? "" : std::string (". Available: ") + available;
  188. sLog.exception ("Playlist not found in config.json: ", name, availableText);
  189. throw std::runtime_error ("Playlist not found: " + name);
  190. }
  191. return cur->second;
  192. }
  193. ApplicationContext::ApplicationContext (int argc, char* argv[]) : m_argc (argc), m_argv (argv) { }
  194. namespace {
  195. bool matchesObjectToken (const std::string& token, int id, const std::string& name) {
  196. if (token == name) {
  197. return true;
  198. }
  199. int parsed = 0;
  200. const auto* end = token.data () + token.size ();
  201. const auto result = std::from_chars (token.data (), end, parsed);
  202. return result.ec == std::errc () && result.ptr == end && parsed == id;
  203. }
  204. } // namespace
  205. std::optional<bool> ApplicationContext::resolveObjectVisibility (int id, const std::string& name) const {
  206. for (const auto& token : this->settings.general.disabledObjects) {
  207. if (matchesObjectToken (token, id, name)) {
  208. return false;
  209. }
  210. }
  211. for (const auto& token : this->settings.general.enabledObjects) {
  212. if (matchesObjectToken (token, id, name)) {
  213. return true;
  214. }
  215. }
  216. return std::nullopt;
  217. }
  218. void ApplicationContext::loadSettingsFromArgv () {
  219. std::string lastScreen;
  220. argparse::ArgumentParser program ("linux-wallpaperengine", "0.0", argparse::default_arguments::help);
  221. auto& backgroundGroup = program.add_group ("Background options");
  222. auto& backgroundMode = backgroundGroup.add_mutually_exclusive_group (false);
  223. backgroundGroup.add_argument ("background id")
  224. .help ("The background to use as default for screens with no background specified")
  225. .default_value ("")
  226. .action ([this] (const std::string& value) -> void {
  227. if (!value.empty ()) {
  228. this->settings.general.defaultBackground = translateBackground (value);
  229. }
  230. });
  231. // Internal, not advertised in --help. Appended to CEF subprocess re-execs so they see the
  232. // *current* (possibly hotswapped) background instead of the "background id" positional's
  233. // launch-time value. Registered last so it takes precedence.
  234. backgroundGroup.add_argument ("--current-background")
  235. .default_value ("")
  236. .hidden ()
  237. .action ([this] (const std::string& value) -> void {
  238. if (!value.empty ()) {
  239. this->settings.general.defaultBackground = translateBackground (value);
  240. }
  241. });
  242. // Internal, not advertised in --help: marks a self-re-exec as a disposable CEF host for one
  243. // Web wallpaper.
  244. backgroundGroup.add_argument ("--web-host")
  245. .flag ()
  246. .hidden ()
  247. .store_into (this->settings.general.webHost);
  248. backgroundGroup.add_argument ("--web-host-shm")
  249. .default_value ("")
  250. .hidden ()
  251. .store_into (this->settings.general.webHostShm);
  252. backgroundGroup.add_argument ("--web-host-width")
  253. .default_value (0)
  254. .hidden ()
  255. .action ([this] (const std::string& value) -> void {
  256. try {
  257. this->settings.general.webHostWidth = static_cast<uint32_t> (std::stoul (value));
  258. } catch (const std::exception&) {
  259. sLog.exception ("Invalid --web-host-width value: ", value);
  260. }
  261. });
  262. backgroundGroup.add_argument ("--web-host-height")
  263. .default_value (0)
  264. .hidden ()
  265. .action ([this] (const std::string& value) -> void {
  266. try {
  267. this->settings.general.webHostHeight = static_cast<uint32_t> (std::stoul (value));
  268. } catch (const std::exception&) {
  269. sLog.exception ("Invalid --web-host-height value: ", value);
  270. }
  271. });
  272. backgroundMode.add_argument ("-w", "--window")
  273. .help ("Window geometry to use for the given screen")
  274. .action ([this] (const std::string& value) -> void {
  275. if (this->settings.render.mode == DESKTOP_BACKGROUND) {
  276. sLog.exception ("Cannot run in both background and window mode");
  277. }
  278. if (this->settings.render.mode == EXPLICIT_WINDOW) {
  279. sLog.exception ("Only one window at a time can be specified in explicit window mode");
  280. }
  281. this->settings.render.mode = EXPLICIT_WINDOW;
  282. if (value.empty ()) {
  283. sLog.exception ("Window geometry cannot be empty");
  284. }
  285. const char* str = value.c_str ();
  286. const char* delim1 = strchr (str, 'x');
  287. const char* delim2 = delim1 ? strchr (delim1 + 1, 'x') : nullptr;
  288. const char* delim3 = delim2 ? strchr (delim2 + 1, 'x') : nullptr;
  289. if (delim1 == nullptr || delim2 == nullptr || delim3 == nullptr) {
  290. sLog.exception ("Window geometry must be in the format: XxYxWxH");
  291. }
  292. this->settings.render.window.geometry.x = strtol (str, nullptr, 10);
  293. this->settings.render.window.geometry.y = strtol (delim1 + 1, nullptr, 10);
  294. this->settings.render.window.geometry.z = strtol (delim2 + 1, nullptr, 10);
  295. this->settings.render.window.geometry.w = strtol (delim3 + 1, nullptr, 10);
  296. })
  297. .append ();
  298. backgroundMode.add_argument ("-r", "--screen-root")
  299. .help ("The screen the following settings will have an effect on")
  300. .action ([this, &lastScreen] (const std::string& value) -> void {
  301. if (this->settings.general.screenBackgrounds.find (value)
  302. != this->settings.general.screenBackgrounds.end ()) {
  303. sLog.exception ("Cannot specify the same screen more than once: ", value);
  304. }
  305. for (const auto& group : this->settings.general.spanGroups) {
  306. if (std::find (group.screens.begin (), group.screens.end (), value) != group.screens.end ()) {
  307. sLog.exception ("--screen-root: screen '", value, "' already belongs to a span group");
  308. }
  309. }
  310. if (this->settings.render.mode == EXPLICIT_WINDOW) {
  311. sLog.exception ("Cannot run in both background and window mode");
  312. }
  313. this->settings.render.mode = DESKTOP_BACKGROUND;
  314. lastScreen = value;
  315. this->settings.general.screenBackgrounds[lastScreen] = "";
  316. this->settings.general.screenScalings[lastScreen] = this->settings.render.window.scalingMode;
  317. this->settings.general.screenClamps[lastScreen] = this->settings.render.window.clamp;
  318. })
  319. .append ();
  320. backgroundGroup.add_argument ("--screen-span")
  321. .help ("Comma-separated list of screens to span a single wallpaper across")
  322. .action ([this, &lastScreen] (const std::string& value) -> void {
  323. if (this->settings.render.mode == EXPLICIT_WINDOW) {
  324. sLog.exception ("Cannot run in both background and window mode");
  325. }
  326. this->settings.render.mode = DESKTOP_BACKGROUND;
  327. SpanGroup group;
  328. std::string screen;
  329. std::istringstream ss (value);
  330. while (std::getline (ss, screen, ',')) {
  331. if (screen.empty ()) {
  332. continue;
  333. }
  334. if (this->settings.general.screenBackgrounds.find (screen)
  335. != this->settings.general.screenBackgrounds.end ()) {
  336. sLog.exception ("--screen-span: screen '", screen, "' is already configured individually");
  337. }
  338. // reject duplicates within this group
  339. if (std::find (group.screens.begin (), group.screens.end (), screen) != group.screens.end ()) {
  340. sLog.exception ("--screen-span: duplicate screen name '", screen, "'");
  341. }
  342. // reject screens already claimed by another span group
  343. for (const auto& existing : this->settings.general.spanGroups) {
  344. if (std::find (existing.screens.begin (), existing.screens.end (), screen)
  345. != existing.screens.end ()) {
  346. sLog.exception ("--screen-span: screen '", screen, "' already belongs to another span group");
  347. }
  348. }
  349. group.screens.push_back (screen);
  350. }
  351. if (group.screens.size () < 2) {
  352. sLog.exception ("--screen-span requires at least two comma-separated screen names");
  353. }
  354. group.scaling = this->settings.render.window.scalingMode;
  355. group.clamp = this->settings.render.window.clamp;
  356. this->settings.general.spanGroups.push_back (std::move (group));
  357. // set lastScreen to a synthetic name so --bg/--scaling/--clamp can target this group
  358. lastScreen = "span:" + value;
  359. // register the synthetic name in screenBackgrounds so the rest of the pipeline sees it
  360. this->settings.general.screenBackgrounds[lastScreen] = "";
  361. })
  362. .append ();
  363. backgroundGroup.add_argument ("-b", "--bg")
  364. .help ("After --screen-root or --screen-span, specifies the background to use")
  365. .action ([this, &lastScreen] (const std::string& value) -> void {
  366. this->settings.general.screenBackgrounds[lastScreen] = translateBackground (value);
  367. // set the default background to the last one used
  368. this->settings.general.defaultBackground = translateBackground (value);
  369. // if this targets a span group, update the group's background too
  370. if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) {
  371. this->settings.general.spanGroups.back ().background = translateBackground (value);
  372. }
  373. })
  374. .append ();
  375. backgroundGroup.add_argument ("--playlist")
  376. .help (
  377. "Uses a playlist from wallpaper engine's config.json. If used after --screen-root it is applied to that "
  378. "screen, otherwise it is used in window mode."
  379. )
  380. .action ([this, &lastScreen] (const std::string& value) -> void {
  381. const auto& playlist = this->getPlaylistFromConfig (value);
  382. if (lastScreen.empty ()) {
  383. this->settings.general.defaultPlaylist = playlist;
  384. if (this->settings.general.defaultBackground.empty () && !playlist.items.empty ()) {
  385. this->settings.general.defaultBackground = playlist.items.front ();
  386. }
  387. } else {
  388. this->settings.general.screenPlaylists[lastScreen] = playlist;
  389. if (!playlist.items.empty ()) {
  390. this->settings.general.screenBackgrounds[lastScreen] = playlist.items.front ();
  391. }
  392. if (this->settings.general.defaultBackground.empty () && !playlist.items.empty ()) {
  393. this->settings.general.defaultBackground = playlist.items.front ();
  394. }
  395. }
  396. })
  397. .append ();
  398. backgroundGroup.add_argument ("--scaling")
  399. .help (
  400. "Scaling mode to use when rendering the background, this applies to the previous --window, --screen-root, "
  401. "or --screen-span output, or the default background if no other background is specified"
  402. )
  403. .choices ("stretch", "fit", "fill", "center", "default")
  404. .action ([this, &lastScreen] (const std::string& value) -> void {
  405. const auto parsed = WallpaperEngine::Render::WallpaperState::parseScalingMode (value);
  406. if (!parsed.has_value ()) {
  407. sLog.exception ("Invalid scaling mode: ", value);
  408. }
  409. const auto mode = *parsed;
  410. if (this->settings.render.mode == DESKTOP_BACKGROUND) {
  411. this->settings.general.screenScalings[lastScreen] = mode;
  412. // also update span group if targeting one
  413. if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) {
  414. this->settings.general.spanGroups.back ().scaling = mode;
  415. }
  416. } else {
  417. this->settings.render.window.scalingMode = mode;
  418. }
  419. })
  420. .append ();
  421. backgroundGroup.add_argument ("--clamp")
  422. .help (
  423. "Clamp mode to use when rendering the background, this applies to the previous --window, --screen-root, "
  424. "or --screen-span output, or the default background if no other background is specified"
  425. )
  426. .choices ("clamp", "border", "repeat")
  427. .action ([this, &lastScreen] (const std::string& value) -> void {
  428. TextureFlags flags;
  429. if (value == "clamp") {
  430. flags = TextureFlags_ClampUVs;
  431. } else if (value == "border") {
  432. flags = TextureFlags_ClampUVsBorder;
  433. } else if (value == "repeat") {
  434. flags = TextureFlags_NoFlags;
  435. } else {
  436. sLog.exception ("Invalid clamp mode: ", value);
  437. }
  438. if (this->settings.render.mode == DESKTOP_BACKGROUND) {
  439. this->settings.general.screenClamps[lastScreen] = flags;
  440. // also update span group if targeting one
  441. if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) {
  442. this->settings.general.spanGroups.back ().clamp = flags;
  443. }
  444. } else {
  445. this->settings.render.window.clamp = flags;
  446. }
  447. })
  448. .append ();
  449. backgroundGroup.add_argument ("--zoom")
  450. .help (
  451. "Manual zoom factor layered on top of --scaling (e.g. 1.5 zooms in 50%, 0.5 zooms out to half size), "
  452. "this applies to the previous --window, --screen-root, or --screen-span output, or the default "
  453. "background if no other background is specified"
  454. )
  455. .action ([this, &lastScreen] (const std::string& value) -> void {
  456. float zoom;
  457. try {
  458. zoom = std::stof (value);
  459. } catch (const std::exception&) {
  460. sLog.exception ("Invalid zoom value: ", value);
  461. }
  462. if (this->settings.render.mode == DESKTOP_BACKGROUND) {
  463. this->settings.general.screenZooms[lastScreen] = zoom;
  464. // also update span group if targeting one
  465. if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) {
  466. this->settings.general.spanGroups.back ().zoom = zoom;
  467. }
  468. } else {
  469. this->settings.render.window.zoom = zoom;
  470. }
  471. })
  472. .append ();
  473. backgroundGroup.add_argument ("--corner-color")
  474. .help (
  475. "Color to show outside the wallpaper's bounds (Center/Fit letterboxing, zoomed-out scaling) when "
  476. "--clamp is border, as a hex RGB or RGBA value (e.g. \"000000\" or \"#1a1a1aff\"). Default: opaque "
  477. "black. This applies to the previous --window, --screen-root, or --screen-span output, or the "
  478. "default background if no other background is specified"
  479. )
  480. .action ([this, &lastScreen] (const std::string& value) -> void {
  481. const auto color = WallpaperEngine::Render::CFBO::parseColor (value);
  482. if (!color.has_value ()) {
  483. sLog.exception ("Invalid corner color: ", value);
  484. }
  485. if (this->settings.render.mode == DESKTOP_BACKGROUND) {
  486. this->settings.general.screenCornerColors[lastScreen] = *color;
  487. // also update span group if targeting one
  488. if (lastScreen.rfind ("span:", 0) == 0 && !this->settings.general.spanGroups.empty ()) {
  489. this->settings.general.spanGroups.back ().cornerColor = *color;
  490. }
  491. } else {
  492. this->settings.render.window.cornerColor = *color;
  493. }
  494. })
  495. .append ();
  496. backgroundGroup.add_argument ("--layer")
  497. .help (
  498. "Wayland-only: which wlr-layer-shell layer to anchor the wallpaper to "
  499. "(background, bottom, top, overlay). Default: background on KDE, bottom elsewhere. "
  500. "KDE defaults to 'background' because KWin's 'show desktop' (Super+D) hides 'bottom' surfaces. "
  501. "Use 'background' on niri to pair with the `place-within-backdrop` layer-rule, "
  502. "otherwise the wallpaper will be cloned to every workspace in the overview."
  503. )
  504. .choices ("background", "bottom", "top", "overlay")
  505. .default_value (std::string ("bottom"))
  506. .action ([this] (const std::string& value) -> void {
  507. if (value == "background") {
  508. this->settings.render.wayland.layer = WAYLAND_LAYER_BACKGROUND;
  509. } else if (value == "bottom") {
  510. this->settings.render.wayland.layer = WAYLAND_LAYER_BOTTOM;
  511. } else if (value == "top") {
  512. this->settings.render.wayland.layer = WAYLAND_LAYER_TOP;
  513. } else if (value == "overlay") {
  514. this->settings.render.wayland.layer = WAYLAND_LAYER_OVERLAY;
  515. } else {
  516. sLog.exception ("Invalid wlr-layer-shell layer: ", value);
  517. }
  518. });
  519. auto& performanceGroup = program.add_group ("Performance options");
  520. performanceGroup.add_argument ("-f", "--fps")
  521. .help ("Limits the FPS to the given number, useful to keep battery consumption low")
  522. .default_value (60)
  523. .store_into (this->settings.render.maximumFPS);
  524. performanceGroup.add_argument ("--speed")
  525. .help ("Global playback speed multiplier for animations, particles and effects (e.g. 0.5 for half speed, "
  526. "2.0 for double speed). Useful to calm down backgrounds with overly fast particle effects")
  527. .action ([this] (const std::string& value) -> void {
  528. float speed;
  529. try {
  530. speed = std::stof (value);
  531. } catch (const std::exception&) {
  532. sLog.exception ("Invalid speed value: ", value);
  533. }
  534. if (speed <= 0.0f) {
  535. sLog.exception ("--speed must be a positive number");
  536. }
  537. this->settings.render.playbackSpeed = speed;
  538. });
  539. performanceGroup.add_argument ("--no-fullscreen-pause")
  540. .help ("Prevents the background pausing when an app is fullscreen")
  541. .flag ()
  542. .action ([this] (const std::string& value) -> void { this->settings.render.pauseOnFullscreen = false; });
  543. performanceGroup.add_argument ("--fullscreen-pause-only-active")
  544. .help ("Wayland only: pause only when a fullscreen window is active (activated)")
  545. .flag ()
  546. .action ([this] (const std::string& value) -> void {
  547. this->settings.render.pauseOnFullscreenOnlyWhenActive = true;
  548. });
  549. performanceGroup.add_argument ("--fullscreen-pause-ignore-appid")
  550. .help ("Wayland only: ignore fullscreen windows whose app_id contains this value (repeatable)")
  551. .action ([this] (const std::string& value) -> void {
  552. if (!value.empty ()) {
  553. this->settings.render.fullscreenPauseIgnoreAppIds.push_back (value);
  554. }
  555. })
  556. .append ();
  557. auto& audioGroup = program.add_group ("Sound settings");
  558. auto& audioSettingsGroup = audioGroup.add_mutually_exclusive_group (false);
  559. audioSettingsGroup.add_argument ("-v", "--volume")
  560. .help ("Volume for all the sounds in the background")
  561. .default_value (15)
  562. .store_into (this->settings.audio.volume);
  563. audioSettingsGroup.add_argument ("-s", "--silent")
  564. .help ("Mutes all the sound the wallpaper might produce")
  565. .flag ()
  566. .action ([this] (const std::string& value) -> void { this->settings.audio.enabled = false; });
  567. audioGroup.add_argument ("--automute")
  568. .help ("Mutes the wallpaper's audio when another app is playing sound")
  569. .flag ()
  570. .action ([this] (const std::string& value) -> void { this->settings.audio.automute = true; });
  571. audioGroup.add_argument ("--noautomute")
  572. .help ("Disables the automute when an app is playing sound (default)")
  573. .flag ()
  574. .action ([this] (const std::string& value) -> void { this->settings.audio.automute = false; });
  575. audioGroup.add_argument ("--no-audio-processing")
  576. .help ("Disables audio processing for backgrounds")
  577. .flag ()
  578. .action ([this] (const std::string& value) -> void { this->settings.audio.audioprocessing = false; });
  579. auto& screenshotGroup = program.add_group ("Screenshot options");
  580. screenshotGroup.add_argument ("--screenshot")
  581. .help ("Takes a screenshot of the background for it's use with tools like PyWAL")
  582. .default_value ("")
  583. .action ([this] (const std::string& value) -> void {
  584. this->settings.screenshot.take = true;
  585. this->settings.screenshot.path = value;
  586. });
  587. screenshotGroup.add_argument ("--screenshot-delay")
  588. .help ("Frames to wait before taking the screenshot")
  589. .default_value<uint32_t> (5)
  590. .store_into (this->settings.screenshot.delay);
  591. auto& contentGroup = program.add_group ("Content options");
  592. contentGroup.add_argument ("--assets-dir")
  593. .help ("Folder where the assets are stored")
  594. .default_value ("")
  595. .action ([this] (const std::string& value) -> void { this->settings.general.assets = value; });
  596. auto& configurationGroup = program.add_group ("Wallpaper configuration options");
  597. configurationGroup.add_argument ("--disable-particles")
  598. .help ("Disables particles for the backgrounds")
  599. .flag ()
  600. .action ([this] (const std::string& value) -> void { this->settings.general.disableParticles = true; });
  601. configurationGroup.add_argument ("--disable-mouse")
  602. .help ("Disables mouse interaction with the backgrounds")
  603. .flag ()
  604. .action ([this] (const std::string& value) -> void { this->settings.mouse.enabled = false; });
  605. configurationGroup.add_argument ("--disable-parallax")
  606. .help ("Disables parallax effect for the backgrounds")
  607. .flag ()
  608. .action ([this] (const std::string& value) -> void { this->settings.mouse.disableparallax = true; });
  609. configurationGroup.add_argument ("-l", "--list-properties")
  610. .help ("List all the available properties and their configuration")
  611. .flag ()
  612. .store_into (this->settings.general.onlyListProperties);
  613. configurationGroup.add_argument ("--set-property", "--property")
  614. .help ("Overrides the default value of the given property")
  615. .action ([this] (const std::string& value) -> void {
  616. const std::string::size_type equals = value.find ('=');
  617. // properties without value are treated as booleans for now
  618. if (equals == std::string::npos) {
  619. this->settings.general.properties[value] = "1";
  620. } else {
  621. this->settings.general.properties[value.substr (0, equals)] = value.substr (equals + 1);
  622. }
  623. })
  624. .append ();
  625. configurationGroup.add_argument ("--list-objects")
  626. .help ("List all the objects/layers a background has, with their id, name and type")
  627. .flag ()
  628. .store_into (this->settings.general.onlyListObjects);
  629. configurationGroup.add_argument ("--disable-object")
  630. .help ("Hides an object/layer (parallax layer, clock, particles, etc), matched by id or name. Can be repeated")
  631. .action ([this] (const std::string& value) -> void { this->settings.general.disabledObjects.push_back (value); }
  632. )
  633. .append ();
  634. configurationGroup.add_argument ("--enable-object")
  635. .help ("Forces an object/layer to show even if the background hides it by default, matched by id or name. "
  636. "Can be repeated")
  637. .action ([this] (const std::string& value) -> void { this->settings.general.enabledObjects.push_back (value); }
  638. )
  639. .append ();
  640. auto& debuggingGroup = program.add_group ("Debugging options");
  641. debuggingGroup.add_argument ("-z", "--dump-structure")
  642. .help ("Dumps the structure of the backgrounds")
  643. .flag ()
  644. .store_into (this->settings.general.dumpStructure);
  645. debuggingGroup.add_argument ("--render-debug")
  646. .help (
  647. "Scene render debug mode: base-only, no-solid-final, pass-log, object=<id>, skip-object=<id>, or "
  648. "skip-effect=<id>. Can be repeated."
  649. )
  650. .action ([this] (const std::string& value) -> void {
  651. const auto parseDebugId = [&value] (const std::string& prefix) -> std::optional<int> {
  652. try {
  653. return std::stoi (value.substr (prefix.length ()));
  654. } catch (const std::invalid_argument&) {
  655. sLog.exception ("Invalid numeric value for --render-debug ", value);
  656. } catch (const std::out_of_range&) {
  657. sLog.exception ("Out-of-range numeric value for --render-debug ", value);
  658. }
  659. return std::nullopt;
  660. };
  661. if (value == "base-only") {
  662. this->settings.render.debug.baseOnly = true;
  663. } else if (value == "no-solid-final") {
  664. this->settings.render.debug.noSolidFinal = true;
  665. } else if (value == "pass-log") {
  666. this->settings.render.debug.passLog = true;
  667. } else if (value.rfind ("object=", 0) == 0) {
  668. this->settings.render.debug.objectFilter = parseDebugId ("object=");
  669. } else if (value.rfind ("skip-object=", 0) == 0) {
  670. if (const auto id = parseDebugId ("skip-object="); id.has_value ()) {
  671. this->settings.render.debug.skipObjects.emplace_back (*id);
  672. }
  673. } else if (value.rfind ("skip-effect=", 0) == 0) {
  674. if (const auto id = parseDebugId ("skip-effect="); id.has_value ()) {
  675. this->settings.render.debug.skipEffects.emplace_back (*id);
  676. }
  677. } else {
  678. sLog.exception ("Invalid render debug mode: ", value);
  679. }
  680. })
  681. .append ();
  682. program.add_epilog (
  683. "Usage examples:\n"
  684. " linux-wallpaperengine --screen-root HDMI-1 --bg 2317494988 --scaling fill --clamp border\n"
  685. " Runs the background 2317494988 on screen HDMI-1, scaling it to fill the screen and clamping the UVs to "
  686. "the border\n\n"
  687. " linux-wallpaperengine 2317494988\n"
  688. " Previews the background 2317494988 on a window\n\n"
  689. " linux-wallpaperengine --screen-root HDMI-1 --bg 2317494988 --screen-root HDMI-2 --bg 1108150151\n"
  690. " Runs two backgrounds on two screens, one on HDMI-1 and the other on HDMI-2\n\n"
  691. " linux-wallpaperengine --screen-root HDMI-1 --screen-root HDMI-2 2317494988\n"
  692. " Runs the background 2317494988 on two screens, one on HDMI-1 and the other on HDMI-2\n\n"
  693. " linux-wallpaperengine --screen-span HDMI-1,HDMI-2 --bg 2317494988 --scaling fill\n"
  694. " Spans the background 2317494988 across HDMI-1 and HDMI-2 as a single stretched wallpaper\n\n"
  695. );
  696. try {
  697. program.parse_known_args (this->m_argc, this->m_argv);
  698. if (!program.is_used ("--layer")) {
  699. const auto* desktopSession = std::getenv ("DESKTOP_SESSION");
  700. const auto* xdgDesktop = std::getenv ("XDG_SESSION_DESKTOP");
  701. const auto* kdeSession = std::getenv ("KDE_FULL_SESSION");
  702. bool onKDE = false;
  703. if (desktopSession) {
  704. std::string_view s (desktopSession);
  705. onKDE = s.find ("kde") != std::string_view::npos || s.find ("plasma") != std::string_view::npos;
  706. }
  707. if (!onKDE && xdgDesktop && std::string_view (xdgDesktop) == "KDE") {
  708. onKDE = true;
  709. }
  710. if (!onKDE && kdeSession && std::string_view (kdeSession) == "true") {
  711. onKDE = true;
  712. }
  713. if (onKDE) {
  714. this->settings.render.wayland.layer = WAYLAND_LAYER_BACKGROUND;
  715. sLog.out ("KDE detected: using --layer background to prevent 'show desktop' from hiding the wallpaper (use --layer bottom to override)");
  716. }
  717. }
  718. if (this->settings.general.defaultBackground.empty ()) {
  719. throw std::runtime_error ("At least one background ID must be specified");
  720. }
  721. this->settings.audio.volume = std::max (0, std::min (this->settings.audio.volume, 128));
  722. this->settings.screenshot.delay
  723. = std::max<uint32_t> (0, std::min<uint32_t> (this->settings.screenshot.delay, 5));
  724. // use std::cout on this in case logging is disabled, this way it's easy to look at what is running
  725. std::stringbuf buffer;
  726. std::ostream bufferStream (&buffer);
  727. bufferStream << "Running with: ";
  728. for (int i = 0; i < this->m_argc; i++) {
  729. bufferStream << this->m_argv[i];
  730. bufferStream << " ";
  731. }
  732. std::cout << buffer.str () << std::endl;
  733. this->validateAssets ();
  734. this->validateScreenshot ();
  735. this->state.general.keepRunning = true;
  736. this->state.audio.enabled = this->settings.audio.enabled;
  737. this->state.audio.volume = this->settings.audio.volume;
  738. this->state.mouse.enabled = this->settings.mouse.enabled;
  739. #if DEMOMODE
  740. sLog.error ("WARNING: RUNNING IN DEMO MODE WILL STOP WALLPAPERS AFTER 5 SECONDS SO VIDEO CAN BE RECORDED");
  741. this->settings.render.maximumFPS = 30;
  742. this->settings.screenshot.take = false;
  743. this->settings.render.pauseOnFullscreen = false;
  744. #endif /* DEMOMODE */
  745. } catch (const std::runtime_error& e) {
  746. throw std::runtime_error (
  747. std::string (e.what ()) + ". Use " + std::string (this->m_argv[0]) + " --help for more information"
  748. );
  749. }
  750. }
  751. int ApplicationContext::getArgc () const { return this->m_argc; }
  752. char** ApplicationContext::getArgv () const { return this->m_argv; }
  753. std::filesystem::path ApplicationContext::translateBackground (const std::string& bgIdOrPath) {
  754. if (bgIdOrPath.find ('/') == std::string::npos) {
  755. return Steam::FileSystem::workshopDirectory (WORKSHOP_APP_ID, bgIdOrPath);
  756. }
  757. return bgIdOrPath;
  758. }
  759. void ApplicationContext::validateAssets () {
  760. if (!this->settings.general.assets.empty ()) {
  761. sLog.out (
  762. "Using wallpaper engine's assets at ", this->settings.general.assets, " based on --assets-dir parameter"
  763. );
  764. return;
  765. }
  766. try {
  767. this->settings.general.assets = Steam::FileSystem::appDirectory (APP_DIRECTORY, "assets");
  768. } catch (std::runtime_error&) {
  769. this->settings.general.assets = std::filesystem::canonical ("/proc/self/exe").parent_path () / "assets";
  770. }
  771. }
  772. void ApplicationContext::validateScreenshot () const {
  773. if (!this->settings.screenshot.take) {
  774. return;
  775. }
  776. if (!this->settings.screenshot.path.has_extension ()) {
  777. sLog.exception ("Cannot determine screenshot format");
  778. }
  779. const std::string extension = this->settings.screenshot.path.extension ();
  780. if (extension != ".bmp" && extension != ".png" && extension != ".jpeg" && extension != ".jpg") {
  781. sLog.exception ("Cannot determine screenshot format, unknown extension ", extension);
  782. }
  783. }