1use std::collections::{HashMap, HashSet};
2use std::path::PathBuf;
3use std::time::Duration;
4
5#[cfg(test)]
6use iced::Theme;
7use iced::window;
8use smallvec::SmallVec;
9
10use modde_core::filter::{FilterCriterion, FilterKind, FilterMode};
11use modde_core::manifest::collection::CollectionManifest;
12use modde_core::profile::ProfileManager;
13#[cfg(test)]
14use modde_core::resolver::GameId;
15use modde_core::save::SaveSnapshot;
16use modde_core::settings::AppSettings;
17
18mod fomod_wizard_state;
19mod install_ops;
20mod model;
21mod state;
22mod tool_ops;
23mod tool_settings;
24mod update;
25mod view;
26
27pub use self::fomod_wizard_state::FOMODWizardState;
28pub(crate) use self::state::format_lock_reason;
29pub use self::state::{
30 AddCustomGameDraft, AddCustomGameDraftField, AddCustomGameState, ExecutableDraft,
31 ExecutableDraftField, ExecutableUiEntry, ReorderDirection, SidebarGroup, ToolApplyResult,
32 ToolHistoryUiEntry, ToolLoadSnapshot, ToolReleaseSupport, ToolRevertResult, ToolState,
33 ToolUiEntry, View, WabbajackInstallerState, WabbajackTab,
34};
35pub use self::tool_ops::parse_executable_environment;
36#[cfg(test)]
37use self::tool_ops::{apply_tool_for_game, validate_optiscaler_apply};
38#[cfg(test)]
39use self::tool_settings::{
40 get_tool_setting_value, normalize_tool_settings_for_specs, set_nested_tool_setting,
41 tool_apply_is_pending, tool_apply_signature,
42};
43
44pub type ToolOptionCatalog = HashMap<String, Vec<String>>;
45
46const BUTTON_HOVER_TOAST_DELAY: Duration = Duration::from_secs(2);
47#[derive(Debug, Clone, Default)]
52pub struct SettingsState {
53 pub nexus_api_key_draft: String,
54 pub nexus_api_key_visible: bool,
55 pub nexus_api_key_source: Option<modde_sources::nexus::auth::ApiKeySource>,
56 pub nexus_config_key_exists: bool,
57 pub game_install_paths: Vec<SettingsGameInstall>,
58 pub download_dir: Option<PathBuf>,
59 pub effective_download_dir: PathBuf,
60 pub has_stock_snapshot: bool,
61 pub theme_name: String,
62 pub nexus_status: Option<NexusAuthStatus>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct SettingsGameInstall {
67 pub game_id: String,
68 pub display_name: String,
69 pub source: String,
70 pub path: PathBuf,
71}
72
73#[derive(Debug, Clone)]
74pub enum NexusAuthStatus {
75 Checking,
76 Valid { username: String, is_premium: bool },
77 Invalid(String),
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct ButtonHoverToast {
82 pub id: u64,
83 pub description: &'static str,
84}
85
86#[derive(Debug, Clone, Default, PartialEq, Eq)]
87pub struct ButtonHoverToastState {
88 pub pending: Option<ButtonHoverToast>,
89 pub visible: Option<ButtonHoverToast>,
90}
91
92#[allow(clippy::struct_excessive_bools)]
94pub struct Modde {
95 pub active_view: View,
96 pub active_profile: Option<String>,
97 pub profiles: Vec<modde_core::profile::ProfileSummary>,
98 pub status_message: String,
99 pub button_hover_toast: ButtonHoverToastState,
100 pub pending_tools_load_status_message: Option<String>,
101 pub settings: AppSettings,
102 pub collection_search: String,
103 pub collections: Vec<CollectionManifest>,
104 pub fomod_installer: Option<FOMODWizardState>,
105 pub fomod_visible_step_indices: SmallVec<[usize; 16]>,
106 pub fomod_wizard_pos: usize,
107 pub fomod_source_dir: Option<PathBuf>,
108 pub fomod_dest_dir: Option<PathBuf>,
109 pub fomod_conflicts: SmallVec<[String; 4]>,
110 pub fomod_can_undo: bool,
111 pub fomod_selections: HashMap<(usize, usize), Vec<usize>>,
112 pub selected_mod_index: Option<usize>,
113 pub selected_mod_details: Option<crate::views::mod_details::ModDetailsState>,
118 pub mod_filter: String,
119 pub mod_id_filter_keys: Vec<String>,
120 pub theme_name: String,
121 pub wabbajack_manifest: Option<modde_core::WabbajackManifest>,
122 pub active_downloads: Vec<crate::views::collections::CollectionDownload>,
123 pub download_queue: modde_sources::queue::DownloadQueue,
124 pub download_lookup: HashMap<String, usize>,
125 pub loaded_profile: Option<modde_core::Profile>,
127 pub save_snapshots: Vec<SaveSnapshot>,
128 pub current_fingerprint: Option<modde_core::save::SaveFingerprint>,
129 pub selected_save_details: Option<crate::views::save_details::SaveDetailsState>,
130 pub experiment_depth: usize,
131 pub nexus_status: Option<NexusAuthStatus>,
132 pub nexus_api_key_draft: String,
133 pub nexus_api_key_visible: bool,
134 pub nexus_api_key_source: Option<modde_sources::nexus::auth::ApiKeySource>,
135 pub nexus_config_key_exists: bool,
136 pub new_profile_name: String,
137 pub new_profile_dialog_open: bool,
138 pub game_path_dialog_open: bool,
139 pub add_custom_game_dialog_open: bool,
140 pub manage_custom_games_dialog_open: bool,
141 pub pending_game_path_game_id: Option<String>,
142 pub previous_game_before_path_dialog: Option<String>,
143 pub game_path_dialog_error: Option<String>,
144 pub add_custom_game: AddCustomGameState,
145 pub available_games: SmallVec<[(String, String); 8]>,
146 pub detected_games: HashSet<String>,
147 pub selected_game: Option<String>,
148 pub stock_snapshot_exists: bool,
149 pub window_id: window::Id,
150 pub collapsed_categories: HashSet<Option<i64>>,
153 pub mod_categories: Vec<(Option<i64>, String)>,
155 pub data_tab_state: crate::views::data_tab::DataTabState,
156 pub data_tab_conflicts: Vec<(String, Vec<String>)>,
157 pub browse_nexus: crate::views::browse_nexus::NexusBrowseState,
159 pub diagnostics_state: crate::views::diagnostics::DiagnosticsState,
160 pub tool_state: ToolState,
161 pub filter_mode: FilterMode,
163 pub filter_criteria: Vec<FilterCriterion>,
165 pub compact_mod_list: bool,
167 pub collapsed_sidebar_groups: HashSet<SidebarGroup>,
169 pub update_available: Option<modde_core::update_check::UpdateInfo>,
170}
171
172fn load_hidden_files(
173 pm: &ProfileManager,
174 profile: &modde_core::Profile,
175) -> HashSet<(String, String)> {
176 profile
177 .id
178 .and_then(|profile_id| pm.db().list_hidden_files(profile_id).ok())
179 .map(|rows| {
180 rows.into_iter()
181 .map(|row| (row.mod_id, row.rel_path))
182 .collect()
183 })
184 .unwrap_or_default()
185}
186
187fn load_active_plugins(pm: &ProfileManager, profile: &modde_core::Profile) -> Vec<String> {
188 let mut plugins = profile
189 .id
190 .and_then(|profile_id| pm.db().get_plugin_order(profile_id).ok())
191 .unwrap_or_default();
192
193 if plugins.is_empty() {
194 plugins =
195 modde_games::read_native_plugin_order(profile.game_id.as_str()).unwrap_or_default();
196 if let Some(profile_id) = profile.id {
197 let _ = pm.db().set_plugin_order(profile_id, &plugins);
198 }
199 }
200
201 plugins
202 .into_iter()
203 .filter(|plugin| plugin.enabled)
204 .map(|plugin| plugin.plugin_name)
205 .collect()
206}
207
208fn detected_game_ids(
209 settings: &AppSettings,
210 available_games: &[(String, String)],
211) -> HashSet<String> {
212 let mut detected: HashSet<String> = settings
213 .game_paths
214 .iter()
215 .filter(|game_path| game_path.path.is_dir())
216 .map(|game_path| game_path.game_id.to_string())
217 .collect();
218
219 detected.extend(
220 modde_games::scan_installed_games()
221 .into_iter()
222 .map(|game| game.game_id.to_string()),
223 );
224
225 for (game_id, _) in available_games {
226 if !detected.contains(game_id)
227 && modde_games::resolve_game_plugin(game_id)
228 .and_then(modde_games::GamePlugin::detect_install)
229 .is_some()
230 {
231 detected.insert(game_id.clone());
232 }
233 }
234
235 detected
236}
237
238fn settings_game_install_paths(
239 settings: &AppSettings,
240 detected_games: Vec<modde_games::detection::DetectedGame>,
241) -> Vec<SettingsGameInstall> {
242 let mut seen = HashSet::new();
243 let mut installs = Vec::new();
244
245 for detected in detected_games {
246 let game_id = detected.game_id.to_string();
247 let path = detected.install_path;
248 if !seen.insert((game_id.clone(), path.clone())) {
249 continue;
250 }
251 installs.push(SettingsGameInstall {
252 game_id,
253 display_name: detected.display_name.to_string(),
254 source: detected.source.to_string(),
255 path,
256 });
257 }
258
259 for game_path in &settings.game_paths {
260 if !game_path.path.is_dir() {
261 continue;
262 }
263 let game_id = game_path.game_id.to_string();
264 let path = game_path.path.clone();
265 if !seen.insert((game_id.clone(), path.clone())) {
266 continue;
267 }
268 let display_name = modde_games::resolve_game_plugin(&game_id)
269 .map(|plugin| plugin.display_name().to_string())
270 .unwrap_or_else(|| game_id.clone());
271 installs.push(SettingsGameInstall {
272 game_id,
273 display_name,
274 source: "Configured".to_string(),
275 path,
276 });
277 }
278
279 installs.sort_by(|a, b| {
280 a.display_name
281 .cmp(&b.display_name)
282 .then_with(|| a.path.cmp(&b.path))
283 .then_with(|| a.source.cmp(&b.source))
284 });
285 installs
286}
287
288fn build_conflict_rows(
289 analysis: &modde_core::diagnostics::ProfileAnalysis,
290 hidden: &HashSet<(String, String)>,
291) -> Vec<(String, Vec<String>)> {
292 let mut rows: Vec<(String, Vec<String>)> = analysis
293 .conflict_map
294 .resolved_conflicts(&analysis.resolved_order, hidden)
295 .into_iter()
296 .filter(|(_, providers, _)| providers.len() > 1)
297 .map(|(path, providers, winner)| {
298 let mut provider_list: Vec<String> = providers
299 .iter()
300 .map(|provider| {
301 if winner.as_ref() == Some(provider) {
302 format!("{provider} (winner)")
303 } else {
304 provider.to_string()
305 }
306 })
307 .collect();
308 provider_list.sort();
309 (path.to_string(), provider_list)
310 })
311 .collect();
312 rows.sort_by(|a, b| a.0.cmp(&b.0));
313 rows
314}
315
316fn format_diagnostic_entry(
317 diagnostic: &modde_core::diagnostics::Diagnostic,
318) -> crate::views::diagnostics::DiagnosticEntry {
319 let severity = match diagnostic.severity {
320 modde_core::diagnostics::Severity::Info => {
321 crate::views::diagnostics::DiagnosticSeverity::Info
322 }
323 modde_core::diagnostics::Severity::Warning => {
324 crate::views::diagnostics::DiagnosticSeverity::Warning
325 }
326 modde_core::diagnostics::Severity::Error => {
327 crate::views::diagnostics::DiagnosticSeverity::Error
328 }
329 };
330
331 let mut message = diagnostic.title.clone();
332 if !diagnostic.detail.is_empty() {
333 message.push_str(": ");
334 message.push_str(&diagnostic.detail);
335 }
336 if let Some(mod_id) = &diagnostic.affected_mod {
337 message.push_str(&format!(" [mod: {mod_id}]"));
338 }
339
340 crate::views::diagnostics::DiagnosticEntry { severity, message }
341}
342
343fn build_default_download_meta(id: &str, name: &str) -> modde_sources::meta::DownloadMeta {
344 modde_sources::meta::DownloadMeta {
345 url: id.to_string(),
346 expected_hash: None,
347 bytes_downloaded: 0,
348 total_bytes: None,
349 nexus_mod_id: None,
350 nexus_file_id: None,
351 game_domain: None,
352 mod_name: Some(name.to_string()),
353 version: None,
354 status: "queued".to_string(),
355 }
356}
357
358#[derive(Debug, Clone)]
361pub enum Message {
362 ExternalRefresh,
365
366 SwitchView(View),
368 ToggleSidebarGroup(SidebarGroup),
369 SwitchProfile(String),
370 CreateProfile {
371 name: String,
372 game_id: String,
373 },
374 DeleteProfile(String),
375 ForkProfile {
376 source: String,
377 new_name: String,
378 },
379
380 OpenNewProfileDialog,
382 NewProfileNameChanged(String),
383 CancelNewProfileDialog,
384 SubmitNewProfileDialog,
385
386 SelectGame(String),
388 GamePathDialogBrowse,
389 GamePathDialogPathSelected {
390 game_id: String,
391 path: PathBuf,
392 },
393 CancelGamePathDialog,
394 OpenAddCustomGame,
395 BrowseAddCustomGameInstallPath,
396 AddCustomGameFieldChanged {
397 field: AddCustomGameDraftField,
398 value: String,
399 },
400 AddCustomGameInstallPathPicked(PathBuf),
401 AddCustomGameSubmit,
402 AddCustomGameCancel,
403 OpenManageCustomGames,
404 CloseManageCustomGames,
405 RemoveCustomGame(String),
406
407 GotWindowId(Option<window::Id>),
409 TitleBarDrag,
410 WindowMinimize,
411 WindowToggleMaximize,
412 WindowClose,
413
414 ToggleMod {
416 mod_id: String,
417 enabled: bool,
418 },
419 FilterChanged(String),
420 AddMod,
421 AddModFromPath(PathBuf),
422 RemoveMod(usize),
423 SelectMod(usize),
424 ModDetailsLoaded {
428 nexus_mod_id: modde_core::NexusModId,
429 result: Result<modde_sources::nexus::api::NexusMod, String>,
430 },
431 ModGalleryLoaded {
433 nexus_mod_id: modde_core::NexusModId,
434 urls: Vec<String>,
435 },
436 ModThumbnailLoaded {
440 nexus_mod_id: modde_core::NexusModId,
441 gallery_index: usize,
442 bytes: Vec<u8>,
443 },
444 ModGalleryNext,
446 OpenModPage,
448 Deploy,
449 DeployComplete(Result<String, String>),
450
451 ReorderMod {
458 mod_id: String,
459 direction: ReorderDirection,
460 },
461 LockMod {
463 mod_id: String,
464 },
465 UnlockMod {
467 mod_id: String,
468 },
469
470 SearchCollections(String),
472 InstallCollection {
473 slug: String,
474 version: String,
475 },
476
477 BrowseTabSwitched(crate::views::browse_nexus::BrowseTab),
481 BrowseGameChanged(Option<String>),
483 BrowseSearchChanged(String),
485 BrowseSearchSubmit,
488 BrowseModsLoaded(Result<Vec<modde_sources::nexus::graphql::GqlModTile>, String>),
490 BrowseCollectionsLoaded(Result<Vec<modde_sources::nexus::graphql::GqlCollectionTile>, String>),
492 BrowseInstallMod {
495 game_domain: String,
496 mod_id: modde_core::NexusModId,
497 },
498 BrowseInstallResult {
501 download_key: String,
502 result: Result<String, String>,
503 },
504
505 LoadWabbajackCatalog,
507 WabbajackCatalogLoaded(
508 Result<Vec<modde_sources::wabbajack::catalog::WabbajackCatalogEntry>, String>,
509 ),
510 WabbajackTabChanged(WabbajackTab),
511 WabbajackSearchChanged(String),
512 WabbajackGameFilterChanged(Option<String>),
513 WabbajackToggleOfficialOnly(bool),
514 WabbajackToggleNsfw(bool),
515 WabbajackToggleDown(bool),
516 WabbajackSelectEntry(usize),
517 WabbajackManualSourceChanged(String),
518 WabbajackHmProfileChanged(String),
519 WabbajackHmGameChanged(String),
520 WabbajackHmGameDirChanged(String),
521 WabbajackDownloadSelected,
522 WabbajackDownloadComplete(Result<PathBuf, String>),
523 WabbajackGenerateHmSnippet,
524 WabbajackHmSnippetGenerated(Result<String, String>),
525 WabbajackCopyHmSnippet,
526 WabbajackSaveHmSnippet,
527 WabbajackHmSnippetSaved(Result<PathBuf, String>),
528 WabbajackOpenUrl(String),
529 OpenWabbajackFile,
530 WabbajackFileSelected(PathBuf),
531 WabbajackProgress(f32),
532 WabbajackStartInstall,
533 WabbajackInstallComplete(Result<(String, Vec<String>), String>),
534 WabbajackLog(String),
535
536 StartFOMOD {
538 mod_path: PathBuf,
539 dest_path: PathBuf,
540 },
541 FOMODChoice {
542 step: usize,
543 group: usize,
544 option: usize,
545 selected: bool,
546 },
547 FOMODNext,
548 FOMODBack,
549 FOMODCancel,
550 FOMODUndo,
551 FOMODInstallComplete(Result<(), String>),
552
553 DownloadProgress {
555 id: String,
556 bytes: u64,
557 total: u64,
558 },
559 DownloadComplete {
560 id: String,
561 },
562 DownloadFailed {
563 id: String,
564 error: String,
565 },
566
567 SetNexusApiKeyDraft(String),
569 ToggleNexusApiKeyVisibility,
570 ReplaceNexusApiKey,
571 RemoveNexusConfigKey,
572 SetGamePath {
573 game_id: String,
574 path: PathBuf,
575 },
576 SetDownloadDir(PathBuf),
577 BrowseGamePath,
578 BrowseDownloadDir,
579 SetTheme(String),
580 ValidateNexusKey,
581 NexusKeyValidated(Result<(String, bool), String>),
582
583 CreateStockSnapshot,
585 StockSnapshotCreated(Result<String, String>),
586 VerifyStockSnapshot,
587 StockVerifyResult(Result<String, String>),
588
589 TryProfile,
591 RollbackExperiment,
592 CommitExperiment,
593
594 LoadSaveHistory,
596 RestoreSaveSnapshot(String),
597 SelectSaveSnapshot(String),
598
599 ToggleSeparator(Option<i64>),
601
602 DataTabFilterChanged(String),
604 DataTabToggleConflicts(bool),
605
606 RunDiagnostics,
608
609 LoadTools,
611 ToolsLoaded {
612 generation: u64,
613 result: Result<ToolLoadSnapshot, String>,
614 },
615 RefreshTools,
616 LoadExecutables,
617 RefreshExecutables,
618 ExecutablesLoaded {
619 generation: u64,
620 result: Result<Vec<ExecutableUiEntry>, String>,
621 },
622 SelectToolTab(String),
623 UpdateToolSetting {
624 tool_id: String,
625 key: String,
626 value: serde_json::Value,
627 },
628 ToggleTool {
629 tool_id: String,
630 enabled: bool,
631 },
632 ToggleToolAdvancedSettings,
633 ApplyTool(String),
634 RevertTool(String),
635 ActivateOptiScaler,
636 DeactivateOptiScaler,
637 AdoptOptiScaler,
638 RestoreOptiScalerBackup,
639 ResetOptiScalerConfig,
640 RestoreToolSettings {
641 tool_id: String,
642 node_id: String,
643 },
644 ToolSettingsRestored {
645 tool_id: String,
646 result: Result<String, String>,
647 },
648 RefreshOptiScalerReleases,
649 OptiScalerReleasesLoaded(Result<Vec<modde_games::tools::ToolReleaseSummary>, String>),
650 InstallOptiScalerRelease,
651 OptiScalerReleaseInstalled(Result<String, String>),
652 RefreshProtonVersions,
653 ProtonVersionsLoaded(Result<Vec<String>, String>),
654 InstallProtonVersion,
655 ProtonVersionInstalled(Result<String, String>),
656 ToolApplied {
657 tool_id: String,
658 result: Result<ToolApplyResult, String>,
659 },
660 ToolReverted {
661 tool_id: String,
662 result: Result<ToolRevertResult, String>,
663 },
664 UpdateExecutableDraft {
665 field: ExecutableDraftField,
666 value: String,
667 },
668 OpenExecutableEditor,
669 ClearExecutableDraft,
670 EditExecutable(String),
671 SaveExecutable,
672 ExecutableSaved(Result<String, String>),
673 RemoveExecutable(String),
674 ExecutableRemoved {
675 name: String,
676 result: Result<String, String>,
677 },
678 RunExecutable(String),
679 ExecutableRunComplete {
680 name: String,
681 result: Result<String, String>,
682 },
683 BrowseExecutablePath,
684 ExecutablePathSelected(Option<PathBuf>),
685 BrowseExecutableWorkingDir,
686 ExecutableWorkingDirSelected(Option<PathBuf>),
687
688 PauseDownload(usize),
690 ResumeDownload(usize),
691 CancelDownload(usize),
692
693 ModEndorseToggle,
696 ModEndorseResult {
700 nexus_mod_id: modde_core::NexusModId,
701 new_status: String,
702 result: Result<(), String>,
703 },
704 ModTrackToggle,
706 ModTrackResult {
708 nexus_mod_id: modde_core::NexusModId,
709 new_tracked: bool,
710 result: Result<(), String>,
711 },
712 ModTrackedSetLoaded {
715 nexus_mod_id: modde_core::NexusModId,
716 is_tracked: bool,
717 },
718
719 ClearOverwrite,
721 MoveOverwriteToMod(String),
722
723 ToggleFilterMode,
725 CycleFilter(FilterKind),
726 ClearFilters,
727 ToggleCompactModList,
728
729 ButtonHoverStarted {
731 id: u64,
732 description: &'static str,
733 },
734 ButtonHoverElapsed {
735 id: u64,
736 },
737 ButtonHoverEnded {
738 id: u64,
739 },
740
741 Noop,
743 UpdateCheckLoaded(Result<Option<modde_core::update_check::UpdateInfo>, String>),
744 OpenUpdateReleasePage,
745 DismissUpdateBanner,
746}
747
748fn external_refresh_stream() -> impl iced::futures::Stream<Item = Message> {
751 use iced::futures::SinkExt as _;
752 use tokio::io::AsyncReadExt as _;
753
754 iced::stream::channel(8, async move |mut output| {
755 let path = modde_core::ipc::gui_socket_path();
760 let _ = std::fs::remove_file(&path);
765
766 let listener = match tokio::net::UnixListener::bind(&path) {
767 Ok(l) => l,
768 Err(e) => {
769 tracing::warn!(
770 error = %e,
771 socket = %path.display(),
772 "could not bind refresh socket; CLI → GUI live updates disabled \
773 for this window"
774 );
775 return;
776 }
777 };
778
779 let _guard = SocketGuard::new(path.clone());
786
787 tracing::info!(socket = %path.display(), "listening for CLI refresh signals");
788
789 loop {
790 let (mut stream, _addr) = match listener.accept().await {
791 Ok(p) => p,
792 Err(e) => {
793 tracing::warn!(error = %e, "accept failed; restarting listen loop");
794 continue;
795 }
796 };
797 let mut buf = [0u8; 64];
801 let _ = stream.read(&mut buf).await;
802 if output.send(Message::ExternalRefresh).await.is_err() {
803 break;
805 }
806 }
807 })
808}
809
810struct SocketGuard {
812 path: std::path::PathBuf,
813}
814
815impl SocketGuard {
816 fn new(path: std::path::PathBuf) -> Self {
817 Self { path }
818 }
819}
820
821impl Drop for SocketGuard {
822 fn drop(&mut self) {
823 modde_core::ipc::cleanup_socket(&self.path);
824 }
825}
826
827const THUMB_MAX_W: u32 = 340;
832const THUMB_MAX_H: u32 = 192;
833
834fn resize_thumbnail_bytes(raw: &[u8]) -> iced::widget::image::Handle {
835 let Ok(img) = image::load_from_memory(raw) else {
836 return iced::widget::image::Handle::from_bytes(raw.to_vec());
838 };
839
840 let resized = img.resize(
841 THUMB_MAX_W,
842 THUMB_MAX_H,
843 image::imageops::FilterType::Lanczos3,
844 );
845 let rgba = resized.to_rgba8();
846 let (w, h) = rgba.dimensions();
847 iced::widget::image::Handle::from_rgba(w, h, rgba.into_raw())
848}
849
850pub(crate) fn shortcut_action_to_message(action: &str) -> Option<Message> {
856 match action {
857 "deploy" => Some(Message::Deploy),
858 "dismiss_modal" => Some(Message::CancelNewProfileDialog),
859 _ => None,
860 }
861}
862
863pub fn run() -> iced::Result {
865 iced::application(Modde::new, Modde::update, Modde::view)
866 .title(Modde::title)
867 .theme(Modde::theme)
868 .subscription(Modde::subscription)
869 .decorations(false)
870 .run()
871}
872
873#[cfg(test)]
874mod tests;