Repository: eythaann/Seelen-UI Branch: master Commit: c00d75dcbb8f Files: 1904 Total size: 3.8 MB Directory structure: gitextract_5t012xim/ ├── .cargo/ │ └── config.toml ├── .cert/ │ ├── Seelen.cer │ ├── Seelen.pfx │ ├── Seelen.pfx.pwd │ └── readme.md ├── .commitlintrc.yml ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── actions/ │ │ ├── generate-update-manifest/ │ │ │ └── action.yml │ │ └── setup/ │ │ └── action.yml │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── dependabot-automerge.yml │ ├── discord-notify.yml │ ├── msix.yml │ ├── nightly.yml │ ├── publish-core.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── AGENTS.md ├── CLA.md ├── CODE_OF_CONDUCT ├── CONTRIBUTING ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── changelog.md ├── crowdin.yml ├── deno.json ├── lefthook.yml ├── libs/ │ ├── core/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── deno.json │ │ ├── mocks/ │ │ │ └── themes/ │ │ │ ├── v2.3.0.yml │ │ │ └── v2.3.12.yml │ │ ├── mod.ts │ │ ├── readme.md │ │ ├── scripts/ │ │ │ ├── build_npm.ts │ │ │ └── rust_bindings.ts │ │ └── src/ │ │ ├── constants/ │ │ │ ├── mod.rs │ │ │ └── mod.ts │ │ ├── error.rs │ │ ├── handlers/ │ │ │ ├── commands.rs │ │ │ ├── commands.ts │ │ │ ├── events.rs │ │ │ ├── events.ts │ │ │ ├── mod.rs │ │ │ └── mod.ts │ │ ├── lib.rs │ │ ├── lib.test.ts │ │ ├── lib.ts │ │ ├── re-exports/ │ │ │ └── tauri.ts │ │ ├── rect.rs │ │ ├── resource/ │ │ │ ├── file.rs │ │ │ ├── interface.rs │ │ │ ├── metadata.rs │ │ │ ├── mod.rs │ │ │ ├── mod.ts │ │ │ ├── resource_id.rs │ │ │ └── yaml_ext.rs │ │ ├── state/ │ │ │ ├── icon_pack.rs │ │ │ ├── icon_pack.test.ts │ │ │ ├── icon_pack.ts │ │ │ ├── mod.rs │ │ │ ├── mod.ts │ │ │ ├── placeholder.rs │ │ │ ├── plugin/ │ │ │ │ ├── mod.rs │ │ │ │ ├── mod.ts │ │ │ │ └── value.rs │ │ │ ├── popups/ │ │ │ │ └── mod.rs │ │ │ ├── settings/ │ │ │ │ ├── by_monitor.rs │ │ │ │ ├── by_theme.rs │ │ │ │ ├── by_wallpaper.rs │ │ │ │ ├── by_widget.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── mod.ts │ │ │ │ ├── settings_by_app.rs │ │ │ │ └── shortcuts.rs │ │ │ ├── theme/ │ │ │ │ ├── config.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── mod.ts │ │ │ │ ├── tests.rs │ │ │ │ └── theming.ts │ │ │ ├── wallpaper/ │ │ │ │ ├── mod.rs │ │ │ │ └── mod.ts │ │ │ ├── weg_items.rs │ │ │ ├── widget/ │ │ │ │ ├── context_menu.rs │ │ │ │ ├── declaration.rs │ │ │ │ ├── interfaces.ts │ │ │ │ ├── mod.rs │ │ │ │ ├── mod.ts │ │ │ │ ├── performance.ts │ │ │ │ ├── positioning.ts │ │ │ │ └── sizing.ts │ │ │ ├── wm_layout.rs │ │ │ └── workspaces/ │ │ │ └── mod.rs │ │ ├── system_state/ │ │ │ ├── bluetooth/ │ │ │ │ ├── appearance_values.yml │ │ │ │ ├── build_low_energy_enums.rs │ │ │ │ ├── enums.rs │ │ │ │ ├── low_energy_enums.rs │ │ │ │ ├── mod.rs │ │ │ │ └── mod.ts │ │ │ ├── components.rs │ │ │ ├── language.rs │ │ │ ├── language.ts │ │ │ ├── media.rs │ │ │ ├── mod.rs │ │ │ ├── mod.ts │ │ │ ├── monitors.rs │ │ │ ├── monitors.ts │ │ │ ├── network/ │ │ │ │ └── mod.rs │ │ │ ├── notification.rs │ │ │ ├── power.rs │ │ │ ├── radios/ │ │ │ │ └── mod.rs │ │ │ ├── trash_bin.rs │ │ │ ├── tray.rs │ │ │ ├── ui_colors.rs │ │ │ ├── ui_colors.ts │ │ │ ├── user.rs │ │ │ ├── user.ts │ │ │ ├── user_apps/ │ │ │ │ └── mod.rs │ │ │ └── win_explorer.rs │ │ └── utils/ │ │ ├── DOM.ts │ │ ├── List.ts │ │ ├── State.ts │ │ ├── async.ts │ │ ├── mod.rs │ │ └── mod.ts │ ├── positioning/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── api/ │ │ │ ├── mod.rs │ │ │ └── windows.rs │ │ ├── easings.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── minimization.rs │ │ └── rect.rs │ ├── slu-ipc/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── app.rs │ │ ├── common.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── messages.rs │ │ └── service.rs │ ├── ui/ │ │ ├── icons.ts │ │ ├── react/ │ │ │ ├── components/ │ │ │ │ ├── BackgroundByLayers/ │ │ │ │ │ ├── infra.module.css │ │ │ │ │ └── infra.tsx │ │ │ │ ├── Icon/ │ │ │ │ │ ├── FileIcon.tsx │ │ │ │ │ ├── MissingIcon.tsx │ │ │ │ │ ├── SpecificIcon.tsx │ │ │ │ │ ├── common.ts │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── InlineSvg/ │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── ResourceText/ │ │ │ │ │ └── index.tsx │ │ │ │ └── Wallpaper/ │ │ │ │ ├── components/ │ │ │ │ │ ├── ImageWallpaper.tsx │ │ │ │ │ ├── ThemedWallpaper.tsx │ │ │ │ │ └── VideoWallpaper.tsx │ │ │ │ ├── index.module.css │ │ │ │ ├── index.tsx │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ └── utils/ │ │ │ ├── DndKit/ │ │ │ │ └── utils.ts │ │ │ ├── LazySignal.ts │ │ │ ├── hooks.ts │ │ │ ├── index.ts │ │ │ ├── layered.ts │ │ │ ├── signals.ts │ │ │ └── styling.ts │ │ ├── svelte/ │ │ │ ├── components/ │ │ │ │ ├── BackgroundByLayers/ │ │ │ │ │ ├── BackgroundByLayers.svelte │ │ │ │ │ └── index.ts │ │ │ │ ├── Icon/ │ │ │ │ │ ├── FileIcon.svelte │ │ │ │ │ ├── Icon.svelte │ │ │ │ │ ├── InlineSVG.svelte │ │ │ │ │ ├── InlineSVGState.svelte.ts │ │ │ │ │ ├── MissingIcon.svelte │ │ │ │ │ ├── SpecificIcon.svelte │ │ │ │ │ ├── common.svelte.ts │ │ │ │ │ └── index.ts │ │ │ │ └── Wallpaper/ │ │ │ │ ├── Wallpaper.svelte │ │ │ │ ├── components/ │ │ │ │ │ ├── ImageWallpaper.svelte │ │ │ │ │ ├── ThemedWallpaper.svelte │ │ │ │ │ └── VideoWallpaper.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── runes/ │ │ │ │ └── DarkMode.svelte.ts │ │ │ └── utils/ │ │ │ ├── LazyRune.svelte.ts │ │ │ ├── PersistentRune.svelte.ts │ │ │ ├── hooks.svelte.ts │ │ │ ├── i18n.ts │ │ │ └── index.ts │ │ └── utils.ts │ ├── utils/ │ │ ├── Cargo.toml │ │ └── src/ │ │ ├── checksums.rs │ │ ├── debounce.rs │ │ ├── lib.rs │ │ ├── signature.rs │ │ └── throttle.rs │ └── widgets-shared/ │ └── styles/ │ ├── RichText.css │ ├── colors.css │ ├── reset.css │ └── spacings.css ├── package.json ├── rust-toolchain.toml ├── scripts/ │ ├── PalletteGenerator.ts │ ├── SetFixedRuntime.ps1 │ ├── SubmitToStore.ps1 │ ├── UpdateTauri.ts │ ├── build/ │ │ ├── README.md │ │ ├── builders/ │ │ │ ├── react.ts │ │ │ ├── svelte.ts │ │ │ └── vanilla.ts │ │ ├── config.ts │ │ ├── plugins/ │ │ │ └── index.ts │ │ ├── server.ts │ │ ├── steps/ │ │ │ ├── cleanup.ts │ │ │ ├── discover.ts │ │ │ └── icons.ts │ │ └── types.ts │ ├── build.ts │ ├── bundle.msix.ts │ ├── clean.ps1 │ ├── submission.json │ ├── translate/ │ │ ├── mod.ps1 │ │ └── mod.ts │ └── versionish.ts ├── src/ │ ├── Cargo.toml │ ├── background/ │ │ ├── app.rs │ │ ├── app_instance.rs │ │ ├── cli/ │ │ │ ├── application/ │ │ │ │ ├── art.rs │ │ │ │ ├── debugger.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── uri.rs │ │ │ │ └── win32.rs │ │ │ ├── infrastructure.rs │ │ │ ├── mod.rs │ │ │ ├── self_pipe.rs │ │ │ └── svc_pipe.rs │ │ ├── error.rs │ │ ├── exposed.rs │ │ ├── hook.rs │ │ ├── i18n/ │ │ │ ├── af.yml │ │ │ ├── am.yml │ │ │ ├── ar.yml │ │ │ ├── az.yml │ │ │ ├── bg.yml │ │ │ ├── bn.yml │ │ │ ├── bs.yml │ │ │ ├── ca.yml │ │ │ ├── cs.yml │ │ │ ├── cy.yml │ │ │ ├── da.yml │ │ │ ├── de.yml │ │ │ ├── el.yml │ │ │ ├── en.yml │ │ │ ├── es.yml │ │ │ ├── et.yml │ │ │ ├── eu.yml │ │ │ ├── fa.yml │ │ │ ├── fi.yml │ │ │ ├── fr.yml │ │ │ ├── gu.yml │ │ │ ├── he.yml │ │ │ ├── hi.yml │ │ │ ├── hr.yml │ │ │ ├── hu.yml │ │ │ ├── hy.yml │ │ │ ├── id.yml │ │ │ ├── is.yml │ │ │ ├── it.yml │ │ │ ├── ja.yml │ │ │ ├── ka.yml │ │ │ ├── km.yml │ │ │ ├── ko.yml │ │ │ ├── ku.yml │ │ │ ├── lb.yml │ │ │ ├── lo.yml │ │ │ ├── lt.yml │ │ │ ├── lv.yml │ │ │ ├── mk.yml │ │ │ ├── mn.yml │ │ │ ├── ms.yml │ │ │ ├── mt.yml │ │ │ ├── ne.yml │ │ │ ├── nl.yml │ │ │ ├── no.yml │ │ │ ├── pa.yml │ │ │ ├── pl.yml │ │ │ ├── ps.yml │ │ │ ├── pt-BR.yml │ │ │ ├── pt-PT.yml │ │ │ ├── ro.yml │ │ │ ├── ru.yml │ │ │ ├── si.yml │ │ │ ├── sk.yml │ │ │ ├── so.yml │ │ │ ├── sr.yml │ │ │ ├── sv.yml │ │ │ ├── sw.yml │ │ │ ├── ta.yml │ │ │ ├── te.yml │ │ │ ├── tg.yml │ │ │ ├── th.yml │ │ │ ├── tl.yml │ │ │ ├── tr.yml │ │ │ ├── uk.yml │ │ │ ├── ur.yml │ │ │ ├── uz.yml │ │ │ ├── vi.yml │ │ │ ├── yo.yml │ │ │ ├── zh-CN.yml │ │ │ ├── zh-TW.yml │ │ │ └── zu.yml │ │ ├── logger.rs │ │ ├── main.rs │ │ ├── migrations.rs │ │ ├── modules/ │ │ │ ├── apps/ │ │ │ │ ├── application/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── msix.rs │ │ │ │ │ ├── msix_manifest.rs │ │ │ │ │ ├── previews.rs │ │ │ │ │ └── windows.rs │ │ │ │ ├── infrastructure.rs │ │ │ │ └── mod.rs │ │ │ ├── media/ │ │ │ │ ├── devices/ │ │ │ │ │ ├── application.rs │ │ │ │ │ ├── domain.rs │ │ │ │ │ ├── infrastructure.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── players/ │ │ │ │ │ ├── application.rs │ │ │ │ │ ├── domain.rs │ │ │ │ │ ├── infrastructure.rs │ │ │ │ │ └── mod.rs │ │ │ │ └── readme.md │ │ │ ├── mod.rs │ │ │ ├── monitors/ │ │ │ │ ├── application.rs │ │ │ │ ├── brightness/ │ │ │ │ │ ├── application.rs │ │ │ │ │ ├── domain.rs │ │ │ │ │ ├── infrastructure.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── domain.rs │ │ │ │ ├── infrastructure.rs │ │ │ │ └── mod.rs │ │ │ ├── network/ │ │ │ │ ├── application/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── passwordless_profile.template.xml │ │ │ │ │ ├── profile.template.xml │ │ │ │ │ ├── profiles.ps1 │ │ │ │ │ ├── scanner.rs │ │ │ │ │ └── v2.rs │ │ │ │ ├── domain/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ └── types.rs │ │ │ │ ├── infrastructure.rs │ │ │ │ └── mod.rs │ │ │ ├── notifications/ │ │ │ │ ├── application.rs │ │ │ │ ├── domain.rs │ │ │ │ ├── infrastructure.rs │ │ │ │ └── mod.rs │ │ │ ├── power/ │ │ │ │ ├── application.rs │ │ │ │ ├── domain.rs │ │ │ │ ├── infrastructure.rs │ │ │ │ └── mod.rs │ │ │ ├── radios/ │ │ │ │ ├── bluetooth/ │ │ │ │ │ ├── classic.rs │ │ │ │ │ ├── handlers.rs │ │ │ │ │ ├── low_energy.rs │ │ │ │ │ ├── manager.rs │ │ │ │ │ └── mod.rs │ │ │ │ ├── device.rs │ │ │ │ ├── handlers.rs │ │ │ │ ├── manager.rs │ │ │ │ ├── mod.rs │ │ │ │ └── wifi/ │ │ │ │ └── mod.rs │ │ │ ├── start/ │ │ │ │ ├── application.rs │ │ │ │ ├── domain.rs │ │ │ │ ├── infrastructure.rs │ │ │ │ └── mod.rs │ │ │ ├── system/ │ │ │ │ ├── mod.rs │ │ │ │ └── tauri.rs │ │ │ ├── system_settings/ │ │ │ │ ├── application.rs │ │ │ │ ├── domain.rs │ │ │ │ ├── infrastructure.rs │ │ │ │ ├── language/ │ │ │ │ │ ├── application.rs │ │ │ │ │ ├── domain.rs │ │ │ │ │ ├── infrastructure.rs │ │ │ │ │ └── mod.rs │ │ │ │ └── mod.rs │ │ │ ├── system_tray/ │ │ │ │ ├── application/ │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── tray_hook_loader.rs │ │ │ │ │ ├── tray_icon.rs │ │ │ │ │ └── util.rs │ │ │ │ ├── domain.rs │ │ │ │ ├── infrastructure.rs │ │ │ │ └── mod.rs │ │ │ ├── trash_bin/ │ │ │ │ ├── application.rs │ │ │ │ ├── infrastructure.rs │ │ │ │ └── mod.rs │ │ │ └── user/ │ │ │ ├── application.rs │ │ │ ├── domain.rs │ │ │ ├── infrastructure.rs │ │ │ └── mod.rs │ │ ├── resources/ │ │ │ ├── cli.rs │ │ │ ├── commands.rs │ │ │ ├── emitters.rs │ │ │ ├── mod.rs │ │ │ └── system_icon_pack.rs │ │ ├── state/ │ │ │ ├── application/ │ │ │ │ ├── apps_config.rs │ │ │ │ ├── icons.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── performance.rs │ │ │ │ ├── settings.rs │ │ │ │ ├── toolbar_items.rs │ │ │ │ └── weg_items.rs │ │ │ ├── domain/ │ │ │ │ └── mod.rs │ │ │ ├── infrastructure.rs │ │ │ └── mod.rs │ │ ├── tauri_context.rs │ │ ├── tauri_plugins.rs │ │ ├── telemetry.rs │ │ ├── utils/ │ │ │ ├── constants.rs │ │ │ ├── discord.rs │ │ │ ├── icon_extractor/ │ │ │ │ ├── mod.rs │ │ │ │ └── queue.rs │ │ │ ├── integrity/ │ │ │ │ ├── checksums.rs │ │ │ │ ├── mod.rs │ │ │ │ └── webview.rs │ │ │ ├── lock_free/ │ │ │ │ ├── mod.rs │ │ │ │ ├── sync_hash_map.rs │ │ │ │ ├── sync_vec.rs │ │ │ │ └── traced_mutex.rs │ │ │ ├── mod.rs │ │ │ ├── pwsh.rs │ │ │ ├── updater.rs │ │ │ ├── virtual_desktop.rs │ │ │ └── winver.rs │ │ ├── virtual_desktops/ │ │ │ ├── cli.rs │ │ │ ├── events.rs │ │ │ ├── handlers.rs │ │ │ ├── mod.rs │ │ │ └── wallpapers.rs │ │ ├── widgets/ │ │ │ ├── cli.rs │ │ │ ├── loader.rs │ │ │ ├── manager.rs │ │ │ ├── mod.rs │ │ │ ├── permissions.rs │ │ │ ├── popups/ │ │ │ │ ├── cli.rs │ │ │ │ ├── handlers.rs │ │ │ │ ├── mod.rs │ │ │ │ └── shortcut_registering.rs │ │ │ ├── task_switcher/ │ │ │ │ ├── cli.rs │ │ │ │ └── mod.rs │ │ │ ├── toolbar/ │ │ │ │ ├── hook.rs │ │ │ │ └── mod.rs │ │ │ ├── wallpaper_manager/ │ │ │ │ ├── cli.rs │ │ │ │ ├── hook.rs │ │ │ │ └── mod.rs │ │ │ ├── webview.rs │ │ │ ├── weg/ │ │ │ │ ├── cli.rs │ │ │ │ ├── handler.rs │ │ │ │ ├── hook.rs │ │ │ │ ├── instance.rs │ │ │ │ ├── mod.rs │ │ │ │ └── weg_items_impl.rs │ │ │ └── window_manager/ │ │ │ ├── cli.rs │ │ │ ├── handler.rs │ │ │ ├── hook.rs │ │ │ ├── instance.rs │ │ │ ├── mod.rs │ │ │ └── state/ │ │ │ ├── mod.rs │ │ │ └── node_ext.rs │ │ └── windows_api/ │ │ ├── app_bar.rs │ │ ├── com.rs │ │ ├── devices.rs │ │ ├── event_window.rs │ │ ├── hdc.rs │ │ ├── input.rs │ │ ├── iterator.rs │ │ ├── mod.rs │ │ ├── monitor/ │ │ │ ├── brightness.rs │ │ │ └── mod.rs │ │ ├── process.rs │ │ ├── string_utils.rs │ │ ├── types.rs │ │ ├── undocumented/ │ │ │ ├── audio_policy_config.rs │ │ │ └── mod.rs │ │ └── window/ │ │ ├── cache.rs │ │ ├── event.rs │ │ └── mod.rs │ ├── build.rs │ ├── capabilities/ │ │ ├── general.json │ │ ├── general_window.json │ │ └── settings_widget.json │ ├── hook_dll/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── service/ │ │ ├── app_management.rs │ │ ├── cli/ │ │ │ ├── mod.rs │ │ │ └── processing.rs │ │ ├── enviroment.rs │ │ ├── error.rs │ │ ├── hotkeys.rs │ │ ├── logger.rs │ │ ├── main.rs │ │ ├── shutdown.rs │ │ ├── string_utils.rs │ │ ├── task_scheduler.rs │ │ └── windows_api/ │ │ ├── app_bar.rs │ │ ├── com.rs │ │ ├── iterator.rs │ │ └── mod.rs │ ├── static/ │ │ ├── apps_templates/ │ │ │ ├── adobe.yml │ │ │ ├── browser.yml │ │ │ ├── core.yml │ │ │ ├── development.yml │ │ │ ├── gaming.yml │ │ │ ├── password_managers.yml │ │ │ ├── system.yml │ │ │ └── video-and-streaming.yml │ │ ├── plugins/ │ │ │ ├── tb_cpu_usage/ │ │ │ │ ├── i18n/ │ │ │ │ │ └── display_name.yml │ │ │ │ ├── metadata.yml │ │ │ │ └── plugin/ │ │ │ │ └── template.js │ │ │ ├── tb_default_focused_app.yml │ │ │ ├── tb_default_focused_app_title.yml │ │ │ ├── tb_default_power/ │ │ │ │ ├── i18n/ │ │ │ │ │ ├── description.yml │ │ │ │ │ └── display_name.yml │ │ │ │ ├── mod.yml │ │ │ │ └── plugin/ │ │ │ │ ├── template.js │ │ │ │ └── tooltip.js │ │ │ ├── tb_disk_usage/ │ │ │ │ ├── i18n/ │ │ │ │ │ └── display_name.yml │ │ │ │ ├── metadata.yml │ │ │ │ └── plugin/ │ │ │ │ ├── template.js │ │ │ │ └── tooltip.js │ │ │ ├── tb_memory_usage/ │ │ │ │ ├── i18n/ │ │ │ │ │ └── display_name.yml │ │ │ │ ├── metadata.yml │ │ │ │ └── plugin/ │ │ │ │ └── template.js │ │ │ ├── tb_network_usage/ │ │ │ │ ├── i18n/ │ │ │ │ │ └── display_name.yml │ │ │ │ ├── metadata.yml │ │ │ │ └── plugin/ │ │ │ │ └── template.js │ │ │ ├── tb_workspaces_dotted/ │ │ │ │ ├── i18n/ │ │ │ │ │ └── display_name.yml │ │ │ │ ├── metadata.yml │ │ │ │ └── plugin/ │ │ │ │ └── template.js │ │ │ ├── tb_workspaces_named/ │ │ │ │ ├── i18n/ │ │ │ │ │ └── display_name.yml │ │ │ │ ├── metadata.yml │ │ │ │ └── plugin/ │ │ │ │ └── template.js │ │ │ ├── tb_workspaces_numbered/ │ │ │ │ ├── i18n/ │ │ │ │ │ └── display_name.yml │ │ │ │ ├── metadata.yml │ │ │ │ └── plugin/ │ │ │ │ └── template.js │ │ │ ├── wm_bsp.yml │ │ │ ├── wm_grid.yml │ │ │ ├── wm_tall.yml │ │ │ └── wm_wide.yml │ │ ├── readme │ │ ├── themes/ │ │ │ ├── animated-start-icon/ │ │ │ │ ├── metadata.yml │ │ │ │ └── seelen/ │ │ │ │ └── weg.scss │ │ │ ├── bubbles/ │ │ │ │ ├── i18n/ │ │ │ │ │ ├── description.yml │ │ │ │ │ └── display_name.yml │ │ │ │ ├── mod.yml │ │ │ │ └── styles/ │ │ │ │ └── toolbar.css │ │ │ └── default/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ ├── shared/ │ │ │ │ ├── buttons.scss │ │ │ │ ├── index.scss │ │ │ │ └── inputs.scss │ │ │ └── styles/ │ │ │ ├── apps-menu.scss │ │ │ ├── bluetooth-popup.scss │ │ │ ├── calendar-popup.scss │ │ │ ├── context-menu.scss │ │ │ ├── fancy-toolbar.scss │ │ │ ├── flyouts.css │ │ │ ├── keyboard-selector.scss │ │ │ ├── media-popup.scss │ │ │ ├── network-popup.scss │ │ │ ├── notifications.scss │ │ │ ├── power-menu.scss │ │ │ ├── quick-settings.scss │ │ │ ├── task-switcher.scss │ │ │ ├── tray-menu.scss │ │ │ ├── user-menu.scss │ │ │ ├── wallpaper-manager.css │ │ │ ├── weg.css │ │ │ ├── window-manager.css │ │ │ └── workspaces-viewer.scss │ │ └── widgets/ │ │ ├── apps-menu/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ ├── bluetooth-popup/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── calendar-popup/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── context-menu/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ ├── flyouts/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ ├── keyboard-selector/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── media-popup/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── network-popup/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── notifications/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── popup/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ ├── power-menu/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── quick-settings/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── settings/ │ │ │ ├── i18n/ │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ ├── system-tray/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── task-switcher/ │ │ │ ├── i18n/ │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ ├── toolbar/ │ │ │ ├── i18n/ │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ ├── user/ │ │ │ ├── i18n/ │ │ │ │ ├── description.yml │ │ │ │ └── display_name.yml │ │ │ ├── metadata.yml │ │ │ └── toolbar-plugin.yml │ │ ├── wallpaper-manager/ │ │ │ ├── i18n/ │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ ├── weg/ │ │ │ ├── i18n/ │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ ├── window-manager/ │ │ │ ├── i18n/ │ │ │ │ └── display_name.yml │ │ │ └── metadata.yml │ │ └── workspaces-viewer/ │ │ ├── i18n/ │ │ │ └── display_name.yml │ │ └── metadata.yml │ ├── tauri.conf.json │ ├── templates/ │ │ ├── AppxManifest.xml │ │ ├── installer-hooks.nsh │ │ └── installer.nsi │ └── ui/ │ ├── globals.d.ts │ ├── react/ │ │ ├── popup/ │ │ │ ├── app.tsx │ │ │ ├── global.css │ │ │ ├── index.tsx │ │ │ └── public/ │ │ │ └── index.html │ │ ├── settings/ │ │ │ ├── app.tsx │ │ │ ├── components/ │ │ │ │ ├── SettingsBox/ │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── SortableSelector/ │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── ThumbnailGeneratorModal/ │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── thumbnailGenerator.ts │ │ │ │ │ └── videoThumbnail.ts │ │ │ │ ├── WelcomeModal/ │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── infra.tsx │ │ │ │ ├── header/ │ │ │ │ │ ├── ExtraInfo.tsx │ │ │ │ │ ├── UpdateButton.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── layout/ │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── monitor/ │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ └── navigation/ │ │ │ │ ├── index.module.css │ │ │ │ ├── index.tsx │ │ │ │ └── routes.tsx │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.tsx │ │ │ ├── modules/ │ │ │ │ ├── ByMonitor/ │ │ │ │ │ └── infra/ │ │ │ │ │ ├── WallpaperSettingsModal.tsx │ │ │ │ │ ├── WidgetSettingsModal.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── Home/ │ │ │ │ │ ├── MiniStore.module.css │ │ │ │ │ ├── MiniStore.tsx │ │ │ │ │ ├── News.module.css │ │ │ │ │ ├── News.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── IconPackEditor/ │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── Wall/ │ │ │ │ │ ├── WallpaperList.tsx │ │ │ │ │ ├── application.ts │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── infra.tsx │ │ │ │ ├── WindowManager/ │ │ │ │ │ ├── application.ts │ │ │ │ │ ├── border/ │ │ │ │ │ │ ├── application.ts │ │ │ │ │ │ └── infra.tsx │ │ │ │ │ └── main/ │ │ │ │ │ └── infra/ │ │ │ │ │ ├── Animations.tsx │ │ │ │ │ ├── GlobalPaddings.tsx │ │ │ │ │ ├── Others.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── appsConfigurations/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── default.ts │ │ │ │ │ │ ├── filters.ts │ │ │ │ │ │ └── reducer.ts │ │ │ │ │ ├── domain.ts │ │ │ │ │ └── infra/ │ │ │ │ │ ├── EditModal.tsx │ │ │ │ │ ├── Identifier.module.css │ │ │ │ │ ├── Identifier.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── infra.tsx │ │ │ │ ├── developer/ │ │ │ │ │ ├── application.ts │ │ │ │ │ └── infra.tsx │ │ │ │ ├── extras/ │ │ │ │ │ ├── application.ts │ │ │ │ │ ├── infra.module.css │ │ │ │ │ └── infrastructure.tsx │ │ │ │ ├── fancyToolbar/ │ │ │ │ │ ├── application.ts │ │ │ │ │ └── infra.tsx │ │ │ │ ├── general/ │ │ │ │ │ ├── application.ts │ │ │ │ │ └── infra/ │ │ │ │ │ ├── Colors.tsx │ │ │ │ │ ├── Performance.tsx │ │ │ │ │ ├── index.module.css │ │ │ │ │ └── index.tsx │ │ │ │ ├── resources/ │ │ │ │ │ ├── IconPacks.tsx │ │ │ │ │ ├── Plugins.tsx │ │ │ │ │ ├── ResourceCard.tsx │ │ │ │ │ ├── SoundPacks.tsx │ │ │ │ │ ├── Theme/ │ │ │ │ │ │ ├── AllView.tsx │ │ │ │ │ │ ├── View.tsx │ │ │ │ │ │ ├── application.ts │ │ │ │ │ │ ├── components/ │ │ │ │ │ │ │ ├── ThemeConfigDefinition.tsx │ │ │ │ │ │ │ └── ThemeSetting.tsx │ │ │ │ │ │ ├── constants.ts │ │ │ │ │ │ └── hooks/ │ │ │ │ │ │ └── useThemeVariable.ts │ │ │ │ │ ├── Wallpapers/ │ │ │ │ │ │ ├── AllView.tsx │ │ │ │ │ │ ├── View.module.css │ │ │ │ │ │ ├── View.tsx │ │ │ │ │ │ └── application.ts │ │ │ │ │ ├── Widget/ │ │ │ │ │ │ ├── AllView.tsx │ │ │ │ │ │ ├── ConfigRenderer.tsx │ │ │ │ │ │ ├── InstanceSelector.tsx │ │ │ │ │ │ ├── View.tsx │ │ │ │ │ │ └── application.ts │ │ │ │ │ ├── infra.module.css │ │ │ │ │ └── infra.tsx │ │ │ │ ├── seelenweg/ │ │ │ │ │ ├── application.ts │ │ │ │ │ └── infra.tsx │ │ │ │ ├── shared/ │ │ │ │ │ ├── config/ │ │ │ │ │ │ └── infra.ts │ │ │ │ │ ├── signals.ts │ │ │ │ │ ├── tauri/ │ │ │ │ │ │ └── infra.ts │ │ │ │ │ └── utils/ │ │ │ │ │ ├── app.ts │ │ │ │ │ └── domain.ts │ │ │ │ └── shortcuts/ │ │ │ │ ├── application.ts │ │ │ │ └── infrastructure.tsx │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ ├── router.tsx │ │ │ ├── state/ │ │ │ │ ├── mod.ts │ │ │ │ ├── resources.ts │ │ │ │ └── system.ts │ │ │ └── styles/ │ │ │ ├── global.css │ │ │ └── variables.css │ │ ├── toolbar/ │ │ │ ├── app.tsx │ │ │ ├── components/ │ │ │ │ └── Error/ │ │ │ │ ├── index.module.css │ │ │ │ └── index.tsx │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.tsx │ │ │ ├── modules/ │ │ │ │ ├── item/ │ │ │ │ │ ├── app/ │ │ │ │ │ │ ├── actionEvaluator.ts │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ ├── scope.ts │ │ │ │ │ │ │ ├── useItemScope.ts │ │ │ │ │ │ │ └── useRemoteData.ts │ │ │ │ │ │ └── services/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── widgetTrigger.ts │ │ │ │ │ ├── domain/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── types.ts │ │ │ │ │ └── infra/ │ │ │ │ │ ├── ContextMenu.tsx │ │ │ │ │ ├── EvaluatedComponents.tsx │ │ │ │ │ └── infra.tsx │ │ │ │ ├── main/ │ │ │ │ │ ├── ContextMenu.tsx │ │ │ │ │ ├── CornerAction.tsx │ │ │ │ │ ├── ItemsContainer.tsx │ │ │ │ │ └── Toolbar.tsx │ │ │ │ └── shared/ │ │ │ │ ├── state/ │ │ │ │ │ ├── default.ts │ │ │ │ │ ├── items.ts │ │ │ │ │ ├── lazy.ts │ │ │ │ │ ├── mod.ts │ │ │ │ │ ├── system.ts │ │ │ │ │ └── windows.ts │ │ │ │ └── utils.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── styles/ │ │ │ ├── global.css │ │ │ └── variables.css │ │ └── weg/ │ │ ├── app.tsx │ │ ├── components/ │ │ │ └── Error/ │ │ │ └── index.tsx │ │ ├── i18n/ │ │ │ ├── index.ts │ │ │ └── translations/ │ │ │ ├── af.yml │ │ │ ├── am.yml │ │ │ ├── ar.yml │ │ │ ├── az.yml │ │ │ ├── bg.yml │ │ │ ├── bn.yml │ │ │ ├── bs.yml │ │ │ ├── ca.yml │ │ │ ├── cs.yml │ │ │ ├── cy.yml │ │ │ ├── da.yml │ │ │ ├── de.yml │ │ │ ├── el.yml │ │ │ ├── en.yml │ │ │ ├── es.yml │ │ │ ├── et.yml │ │ │ ├── eu.yml │ │ │ ├── fa.yml │ │ │ ├── fi.yml │ │ │ ├── fr.yml │ │ │ ├── gu.yml │ │ │ ├── he.yml │ │ │ ├── hi.yml │ │ │ ├── hr.yml │ │ │ ├── hu.yml │ │ │ ├── hy.yml │ │ │ ├── id.yml │ │ │ ├── is.yml │ │ │ ├── it.yml │ │ │ ├── ja.yml │ │ │ ├── ka.yml │ │ │ ├── km.yml │ │ │ ├── ko.yml │ │ │ ├── ku.yml │ │ │ ├── lb.yml │ │ │ ├── lo.yml │ │ │ ├── lt.yml │ │ │ ├── lv.yml │ │ │ ├── mk.yml │ │ │ ├── mn.yml │ │ │ ├── ms.yml │ │ │ ├── mt.yml │ │ │ ├── ne.yml │ │ │ ├── nl.yml │ │ │ ├── no.yml │ │ │ ├── pa.yml │ │ │ ├── pl.yml │ │ │ ├── ps.yml │ │ │ ├── pt-BR.yml │ │ │ ├── pt-PT.yml │ │ │ ├── ro.yml │ │ │ ├── ru.yml │ │ │ ├── si.yml │ │ │ ├── sk.yml │ │ │ ├── so.yml │ │ │ ├── sr.yml │ │ │ ├── sv.yml │ │ │ ├── sw.yml │ │ │ ├── ta.yml │ │ │ ├── te.yml │ │ │ ├── tg.yml │ │ │ ├── th.yml │ │ │ ├── tl.yml │ │ │ ├── tr.yml │ │ │ ├── uk.yml │ │ │ ├── ur.yml │ │ │ ├── uz.yml │ │ │ ├── vi.yml │ │ │ ├── yo.yml │ │ │ ├── zh-CN.yml │ │ │ ├── zh-TW.yml │ │ │ └── zu.yml │ │ ├── index.tsx │ │ ├── modules/ │ │ │ ├── bar/ │ │ │ │ ├── DockMenu.tsx │ │ │ │ ├── DraggableItem.tsx │ │ │ │ ├── ItemReordableList.tsx │ │ │ │ └── index.tsx │ │ │ ├── item/ │ │ │ │ ├── application.ts │ │ │ │ └── infra/ │ │ │ │ ├── GeneralMenu.tsx │ │ │ │ ├── MediaSession.css │ │ │ │ ├── MediaSession.tsx │ │ │ │ ├── RecycleBin.tsx │ │ │ │ ├── Separator.tsx │ │ │ │ ├── ShowDesktop.tsx │ │ │ │ ├── StartMenu.tsx │ │ │ │ ├── UserApplication.tsx │ │ │ │ ├── UserApplicationContextMenu.tsx │ │ │ │ └── UserApplicationPreview.tsx │ │ │ └── shared/ │ │ │ ├── state/ │ │ │ │ ├── hidden.ts │ │ │ │ ├── items.ts │ │ │ │ ├── mod.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── system.ts │ │ │ │ └── windows.ts │ │ │ └── types.ts │ │ ├── public/ │ │ │ └── index.html │ │ └── styles/ │ │ ├── global.css │ │ └── variables.css │ ├── reduxRootState.ts │ ├── svelte/ │ │ ├── apps-menu/ │ │ │ ├── App.svelte │ │ │ ├── components/ │ │ │ │ ├── AllAppsView.svelte │ │ │ │ ├── AppItem.svelte │ │ │ │ ├── FolderItem.svelte │ │ │ │ ├── FolderModal.svelte │ │ │ │ ├── PinnedView.svelte │ │ │ │ └── StartMenuBody.svelte │ │ │ ├── constants.ts │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.ts │ │ │ ├── keyboard-navigation.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ ├── state/ │ │ │ │ ├── config.svelte.ts │ │ │ │ ├── knownFolders.svelte.ts │ │ │ │ ├── mod.svelte.ts │ │ │ │ └── positioning.svelte.ts │ │ │ └── utils.ts │ │ ├── bluetooth-popup/ │ │ │ ├── app.svelte │ │ │ ├── components/ │ │ │ │ └── BluetoothDevice.svelte │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── icons.ts │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── calendar-popup/ │ │ │ ├── app.svelte │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── context-menu/ │ │ │ ├── MenuItem.svelte │ │ │ ├── Submenu.svelte │ │ │ ├── app.svelte │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── flyouts/ │ │ │ ├── app/ │ │ │ │ ├── Brightness.svelte │ │ │ │ ├── MediaDevices.svelte │ │ │ │ ├── MediaPlaying.svelte │ │ │ │ └── Workspace.svelte │ │ │ ├── app.svelte │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state/ │ │ │ ├── config.svelte.ts │ │ │ ├── mod.svelte.ts │ │ │ └── placement.svelte.ts │ │ ├── keyboard-selector/ │ │ │ ├── app.svelte │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── media-popup/ │ │ │ ├── app.svelte │ │ │ ├── components/ │ │ │ │ ├── DeviceView.svelte │ │ │ │ ├── MainView.svelte │ │ │ │ ├── MediaDevice.svelte │ │ │ │ ├── MediaPlayer.svelte │ │ │ │ └── VolumeControl.svelte │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── network-popup/ │ │ │ ├── app.svelte │ │ │ ├── components/ │ │ │ │ └── WlanEntry.svelte │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── notifications/ │ │ │ ├── app.svelte │ │ │ ├── components/ │ │ │ │ └── Notification.svelte │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── power-menu/ │ │ │ ├── app.svelte │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.ts │ │ │ ├── options.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── quick-settings/ │ │ │ ├── app.svelte │ │ │ ├── components/ │ │ │ │ ├── BrightnessControl.svelte │ │ │ │ ├── MediaDevices.svelte │ │ │ │ └── RadioButtons.svelte │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── system-tray/ │ │ │ ├── app.svelte │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── task-switcher/ │ │ │ ├── App.svelte │ │ │ ├── components/ │ │ │ │ └── TaskItem.svelte │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── user-menu/ │ │ │ ├── app.svelte │ │ │ ├── components/ │ │ │ │ ├── EmptyList.svelte │ │ │ │ ├── FilePreview.svelte │ │ │ │ ├── UserFolder.svelte │ │ │ │ └── UserProfile.svelte │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.ts │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state/ │ │ │ ├── knownFolders.svelte.ts │ │ │ └── mod.svelte.ts │ │ ├── wallpaper_manager/ │ │ │ ├── app.svelte │ │ │ ├── i18n/ │ │ │ │ ├── index.ts │ │ │ │ └── translations/ │ │ │ │ ├── af.yml │ │ │ │ ├── am.yml │ │ │ │ ├── ar.yml │ │ │ │ ├── az.yml │ │ │ │ ├── bg.yml │ │ │ │ ├── bn.yml │ │ │ │ ├── bs.yml │ │ │ │ ├── ca.yml │ │ │ │ ├── cs.yml │ │ │ │ ├── cy.yml │ │ │ │ ├── da.yml │ │ │ │ ├── de.yml │ │ │ │ ├── el.yml │ │ │ │ ├── en.yml │ │ │ │ ├── es.yml │ │ │ │ ├── et.yml │ │ │ │ ├── eu.yml │ │ │ │ ├── fa.yml │ │ │ │ ├── fi.yml │ │ │ │ ├── fr.yml │ │ │ │ ├── gu.yml │ │ │ │ ├── he.yml │ │ │ │ ├── hi.yml │ │ │ │ ├── hr.yml │ │ │ │ ├── hu.yml │ │ │ │ ├── hy.yml │ │ │ │ ├── id.yml │ │ │ │ ├── is.yml │ │ │ │ ├── it.yml │ │ │ │ ├── ja.yml │ │ │ │ ├── ka.yml │ │ │ │ ├── km.yml │ │ │ │ ├── ko.yml │ │ │ │ ├── ku.yml │ │ │ │ ├── lb.yml │ │ │ │ ├── lo.yml │ │ │ │ ├── lt.yml │ │ │ │ ├── lv.yml │ │ │ │ ├── mk.yml │ │ │ │ ├── mn.yml │ │ │ │ ├── ms.yml │ │ │ │ ├── mt.yml │ │ │ │ ├── ne.yml │ │ │ │ ├── nl.yml │ │ │ │ ├── no.yml │ │ │ │ ├── pa.yml │ │ │ │ ├── pl.yml │ │ │ │ ├── ps.yml │ │ │ │ ├── pt-BR.yml │ │ │ │ ├── pt-PT.yml │ │ │ │ ├── ro.yml │ │ │ │ ├── ru.yml │ │ │ │ ├── si.yml │ │ │ │ ├── sk.yml │ │ │ │ ├── so.yml │ │ │ │ ├── sr.yml │ │ │ │ ├── sv.yml │ │ │ │ ├── sw.yml │ │ │ │ ├── ta.yml │ │ │ │ ├── te.yml │ │ │ │ ├── tg.yml │ │ │ │ ├── th.yml │ │ │ │ ├── tl.yml │ │ │ │ ├── tr.yml │ │ │ │ ├── uk.yml │ │ │ │ ├── ur.yml │ │ │ │ ├── uz.yml │ │ │ │ ├── vi.yml │ │ │ │ ├── yo.yml │ │ │ │ ├── zh-CN.yml │ │ │ │ ├── zh-TW.yml │ │ │ │ └── zu.yml │ │ │ ├── index.ts │ │ │ ├── modules/ │ │ │ │ └── Monitor/ │ │ │ │ ├── Monitor.svelte │ │ │ │ └── infra.svelte │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ └── state.svelte.ts │ │ ├── window_manager/ │ │ │ ├── App.svelte │ │ │ ├── index.ts │ │ │ ├── layout/ │ │ │ │ ├── application.ts │ │ │ │ ├── domain.ts │ │ │ │ └── infra/ │ │ │ │ ├── Container.svelte │ │ │ │ ├── Layout.svelte │ │ │ │ ├── containers/ │ │ │ │ │ ├── Leaf.svelte │ │ │ │ │ ├── Reserved.svelte │ │ │ │ │ └── Stack.svelte │ │ │ │ └── index.css │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ ├── state.svelte.ts │ │ │ ├── styles/ │ │ │ │ └── global.css │ │ │ └── utils.ts │ │ └── workspaces-viewer/ │ │ ├── app/ │ │ │ ├── Monitor.svelte │ │ │ ├── Window.svelte │ │ │ └── Workspace.svelte │ │ ├── app.svelte │ │ ├── index.ts │ │ ├── public/ │ │ │ └── index.html │ │ └── state.svelte.ts │ └── vanilla/ │ ├── entry-point/ │ │ ├── ConsoleWrapper.ts │ │ ├── _ConsoleWrapper.ts │ │ ├── _tauri.ts │ │ ├── index.ts │ │ └── setup.ts │ ├── integrity/ │ │ ├── index.ts │ │ └── public/ │ │ └── index.html │ └── third_party/ │ ├── index.ts │ ├── public/ │ │ └── index.html │ └── reset.css └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [env] SLU_SERVICE_CONNECTION_TOKEN = "__local__" TS_RS_EXPORT_DIR = "./gen/types" TS_RS_IMPORT_EXTENSION = "ts" TS_RS_LARGE_INT = "number" ================================================ FILE: .cert/Seelen.pfx.pwd ================================================ Seelen ================================================ FILE: .cert/readme.md ================================================ # Self Signed Certificates ## Creation ```pwsh msixherocli.exe newcert --directory .\.cert --name Seelen --password Seelen --subject CN=7E60225C-94CB-4B2E-B17F-0159A11074CB --validUntil "14/12/2026 6:31:10 pm" ``` ## Usage In this directory, you will find the self-signed certificates used in the development environment. You can add it to your system to trust nighly msix packages. 1. Download Seelen.pfx 2. Open a powershell terminal as administrator 3. Go to the directory where you downloaded the file 4. Run the following command ```pwsh $password = ConvertTo-SecureString -String Seelen -Force -AsPlainText Import-PfxCertificate -FilePath .\Seelen.pfx -CertStoreLocation Cert:\LocalMachine\root -Password $password ``` > [!NOTE] > These files expire each year and should be replaced with new ones. ================================================ FILE: .commitlintrc.yml ================================================ # commitlint.config.yml extends: - "@commitlint/config-conventional" rules: # Basic rules header-max-length: [2, "always", 72] body-max-line-length: [2, "always", 100] # Commit type type-enum: - 2 - "always" - [ "feat", # New feature "enh", # Enhancement of an existing feature "fix", # Bug fix "docs", # Documentation changes "style", # Code formatting, white spaces, etc. "refactor", # Code refactoring "perf", # Performance improvement "test", # Adding or fixing tests "build", # Changes affecting the build system or external dependencies "ci", # Changes to CI configuration files and scripts "chore", # Other changes that don't modify src or test files "delete", # Deleting unused files "revert", # Reverting to a previous commit ] scope-empty: [2, "never"] subject-empty: [2, "never"] ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/FUNDING.yml ================================================ github: [eythaann] ko_fi: eythaann patreon: eythaann custom: - https://www.paypal.me/eythaann ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report 🐛 about: Create a report to help us improve title: "[BUG] Short description of the issue" labels: bug assignees: "" --- ### Description of the issue A clear and concise description of what's wrong. ### To reproduce Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll to '....' 4. See error ### Expected behavior Explain what you expected to happen. ### Evidence - **Screenshots/Videos**: If applicable, add visual proof - **Log File**: Please attach the latest log file from: `%LocalAppdata%\com.seelen.seelen-ui\logs` (This helps us diagnose the issue) ### Additional context Add any other relevant information here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: "[FEAT] Include a pandora box" labels: enhancement, feature request assignees: "" --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/actions/generate-update-manifest/action.yml ================================================ name: Generate Update Manifest description: Generates and uploads latest.json for Tauri updater inputs: release-id: description: Release ID to get the release from required: true version: description: Version to use in the manifest required: true github-token: description: GitHub token for API access required: true runs: using: composite steps: - name: Generate and Upload latest.json uses: actions/github-script@v7 with: github-token: ${{ inputs.github-token }} script: | const releaseId = '${{ inputs.release-id }}'; const version = '${{ inputs.version }}'.replaceAll('"', ''); core.info(`Using version: ${version}`); // Get the release const release = await github.rest.repos.getRelease({ owner: context.repo.owner, repo: context.repo.repo, release_id: releaseId, }); core.info(`Found release: ${release.data.name} (ID: ${release.data.id})`); // Get all assets from the release const { data: assets } = await github.rest.repos.listReleaseAssets({ owner: context.repo.owner, repo: context.repo.repo, release_id: release.data.id, }); core.info(`Found ${assets.length} assets`); // Filter relevant assets (exe and sig files) const relevantAssets = assets.filter(asset => asset.name.endsWith('.exe') || asset.name.endsWith('.sig') ); if (relevantAssets.length === 0) { throw new Error('No relevant assets found in release'); } // Build the update manifest const update = { version, notes: `Seelen UI build ${version}`, pub_date: new Date().toISOString(), platforms: { "windows-x86_64": { signature: "", url: "" }, "windows-aarch64": { signature: "", url: "" }, }, }; // Map assets to platforms for (const asset of relevantAssets) { const platform = asset.name.includes('x64') ? 'windows-x86_64' : asset.name.includes('arm64') ? 'windows-aarch64' : null; if (!platform) { core.warning(`Skipping asset with unknown platform: ${asset.name}`); continue; } if (asset.name.endsWith('.sig')) { // Download signature content const sigResponse = await github.request('GET /repos/{owner}/{repo}/releases/assets/{asset_id}', { owner: context.repo.owner, repo: context.repo.repo, asset_id: asset.id, headers: { accept: 'application/octet-stream' }, }); update.platforms[platform].signature = Buffer.from(sigResponse.data).toString('utf-8'); core.info(`Added signature for ${platform}`); } if (asset.name.endsWith('.exe')) { update.platforms[platform].url = asset.browser_download_url; core.info(`Added URL for ${platform}: ${asset.browser_download_url}`); } } // Validate and remove incomplete platforms for (const [platform, data] of Object.entries(update.platforms)) { if (!data.signature || !data.url) { core.warning(`Platform ${platform} is incomplete - removing`); delete update.platforms[platform]; } } if (Object.keys(update.platforms).length === 0) { throw new Error('No valid platforms found in assets'); } const manifestJson = JSON.stringify(update, null, 2); core.info('Generated update manifest:'); core.info(manifestJson); // Delete existing latest.json if it exists const existingManifest = assets.find(a => a.name === 'latest.json'); if (existingManifest) { core.info('Deleting existing latest.json'); await github.rest.repos.deleteReleaseAsset({ owner: context.repo.owner, repo: context.repo.repo, asset_id: existingManifest.id, }); } // Upload the new manifest core.info('Uploading latest.json to release'); const manifestBuffer = Buffer.from(manifestJson, 'utf-8'); await github.rest.repos.uploadReleaseAsset({ owner: context.repo.owner, repo: context.repo.repo, release_id: release.data.id, name: 'latest.json', data: manifestBuffer, headers: { 'content-type': 'application/json', 'content-length': manifestBuffer.length, }, }); core.info('✅ Successfully generated and uploaded latest.json'); // Delete .sig files from release assets core.info('Deleting .sig files from release assets'); const sigAssets = assets.filter(a => a.name.endsWith('.sig')); for (const sigAsset of sigAssets) { core.info(`Deleting ${sigAsset.name}`); await github.rest.repos.deleteReleaseAsset({ owner: context.repo.owner, repo: context.repo.repo, asset_id: sigAsset.id, }); } core.info(`✅ Deleted ${sigAssets.length} .sig file(s) from release`); ================================================ FILE: .github/actions/setup/action.yml ================================================ name: Setup Development Environment description: Sets up Node.js, Deno, and Rust toolchain with caching inputs: rust-components: description: Additional Rust components to install (comma-separated) required: false default: "" rust-targets: description: Additional Rust targets to install (comma-separated) required: false default: "" cache-key-prefix: description: Prefix for cache keys required: false default: "rust" runs: using: composite steps: - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 24 cache: npm - name: Setup Deno uses: denoland/setup-deno@v2 with: deno-version: v2.x - name: Setup Rust uses: dtolnay/rust-toolchain@master with: toolchain: nightly-2025-09-12 components: ${{ inputs.rust-components }} targets: ${{ inputs.rust-targets }} - name: Add Rust targets if: inputs.rust-targets != '' shell: bash run: | IFS=',' read -ra TARGETS <<< "${{ inputs.rust-targets }}" for target in "${TARGETS[@]}"; do rustup target add "$target" done - name: Cache Rust dependencies uses: actions/cache@v4 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} restore-keys: | ${{ inputs.cache-key-prefix }}-${{ runner.os }}- ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: / schedule: interval: weekly - package-ecosystem: cargo directory: / schedule: interval: weekly ================================================ FILE: .github/workflows/ci.yml ================================================ name: Continuous Integration on: pull_request: branches: - master workflow_dispatch: workflow_call: jobs: js-test-and-linter: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup with: rust-components: rustfmt - run: npm install - run: deno fmt --check - run: deno lint - run: npm run type-check - run: npm run test rust-linter: runs-on: windows-2025 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup - name: Format run: |- rustup component add rustfmt cargo fmt -- --check - name: Linter run: |- rustup component add clippy cargo clippy --locked --all-targets -- -D warnings rust-test: runs-on: windows-2025 steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup with: cache-key-prefix: rust-test - run: cargo test --locked --verbose ================================================ FILE: .github/workflows/dependabot-automerge.yml ================================================ name: Dependabot auto-merge on: pull_request_target permissions: contents: write pull-requests: write jobs: automerge: if: github.actor == 'dependabot[bot]' && github.event.pull_request.head.repo.fork == false runs-on: ubuntu-latest steps: - name: Fetch Dependabot metadata id: metadata uses: dependabot/fetch-metadata@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Approve PR run: gh pr review --approve "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Enable auto-merge (squash) run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/discord-notify.yml ================================================ name: "Discord Notify" on: workflow_dispatch: inputs: tag: description: "Release tag to announce (e.g. v2.5.1)" required: true workflow_call: inputs: tag: description: "Release tag to announce" type: string required: true jobs: notify: name: Send Announcement To Discord Server runs-on: ubuntu-latest steps: - name: Discord notification uses: actions/github-script@v7 env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} with: script: | const tag = '${{ inputs.tag }}'; const { data: release } = await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag: tag, }); let body = release.body || ''; // Strip HTML comments body = body.replace(//g, ''); // Reduce heading levels (avoid h1/h2 dominating the embed) body = body.replace(/^### /gm, '#### ').replace(/^## /gm, '### ').replace(/^# /gm, '## '); // Normalise whitespace body = body.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim(); // Enforce Discord embed description limit if (body.length > 4096) body = body.slice(0, 4093) + '...'; const payload = { username: 'Seelen', avatar_url: 'https://raw.githubusercontent.com/eythaann/Seelen-UI/master/documentation/images/logo_with_margins.png', embeds: [{ title: release.name || tag, description: body, url: release.html_url, color: 5814783, footer: { text: 'Seelen Inc.' }, }], }; const response = await fetch(process.env.DISCORD_WEBHOOK, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!response.ok) { const text = await response.text(); core.setFailed(`Discord webhook failed (${response.status}): ${text}`); } ================================================ FILE: .github/workflows/msix.yml ================================================ name: Pull MSIX from Store permissions: contents: write on: schedule: - cron: "0 0 * * *" # Run the action every day workflow_dispatch: # Manually run the action jobs: check-msix: runs-on: ubuntu-latest outputs: MSIX_ALREADY_EXIST: ${{ steps.check_msix.outputs.result }} steps: - name: Check for MSIX file in latest release id: check_msix uses: actions/github-script@v7 with: script: | const { owner, repo } = context.repo; const latestRelease = await github.rest.repos.getLatestRelease({ owner, repo }); // Check if .msix file exists in the release assets const msixAsset = latestRelease.data.assets.find(asset => asset.name.toLowerCase().endsWith('.msix')); return msixAsset ? 'true' : 'false'; result-encoding: string - name: Debug output run: echo "MSIX_ALREADY_EXIST value is ${{ steps.check_msix.outputs.result }}" upload-store-msix-to-release: name: Upload Signed MSIX to release needs: check-msix if: ${{ needs.check-msix.outputs.MSIX_ALREADY_EXIST == 'false' }} runs-on: ubuntu-latest steps: - name: Upload store MSIX to release uses: JasonWei512/Upload-Microsoft-Store-MSIX-Package-to-GitHub-Release@v1 with: store-id: 9P67C2D4T9FB token: ${{ secrets.GITHUB_TOKEN }} asset-name-pattern: Seelen.UI_{version}_{arch} msix-to-winget: name: MSIX to Winget needs: upload-store-msix-to-release runs-on: windows-2025 steps: - name: Get Latest Release id: get-version uses: actions/github-script@v7 with: script: |- const { owner, repo } = context.repo; const latestRelease = await github.rest.repos.getLatestRelease({ owner, repo }); const tag = latestRelease.data.tag_name; const version = tag.replace("v", "") + ".0"; core.setOutput('version', version); core.setOutput('release-tag', tag); console.info("Release tag: ", tag, " Version: ", version); - uses: vedantmgoyal9/winget-releaser@main with: identifier: Seelen.SeelenUI version: ${{ steps.get-version.outputs.version }} release-tag: ${{ steps.get-version.outputs.release-tag }} installers-regex: '\.msix$' fork-user: eythaann token: ${{ secrets.WINGET_TOKEN }} ================================================ FILE: .github/workflows/nightly.yml ================================================ name: Nightly on: push: branches: - master paths-ignore: - "**.md" - "documentation/**" - ".github/**" - "crowdin.yml" workflow_dispatch: permissions: contents: write concurrency: group: ${{ github.workflow }} cancel-in-progress: true jobs: continuous-integration: uses: ./.github/workflows/ci.yml update-tag: needs: continuous-integration runs-on: ubuntu-latest outputs: version: ${{ steps.gen-version.outputs.result }} steps: - uses: actions/checkout@v4 - run: git fetch --tags --prune - name: Create or update 'nightly' tag (force overwrite) run: | git tag -f nightly git push origin --force --tags - name: Generate Version id: gen-version uses: actions/github-script@v7 with: script: | const fs = require('fs'); const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); const currentVersion = packageJson.version; const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(2, 12); const nightlyVersion = `${currentVersion}-nightly.${timestamp}`; return nightlyVersion; build-binaries: needs: update-tag strategy: fail-fast: false matrix: include: - platform: windows-2025 target: x86_64-pc-windows-msvc - platform: windows-2025 target: aarch64-pc-windows-msvc runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup with: rust-targets: ${{ matrix.target }} cache-key-prefix: rust-build-${{ matrix.target }} - name: Install frontend dependencies run: npm install - name: Set Version run: | npx tsx scripts/versionish.ts ci ${{ needs.update-tag.outputs.version }} - name: Build Hook DLL run: cargo build --release --target ${{ matrix.target }} -p sluhk - name: Build env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: npx tauri build --ci --verbose --no-bundle --target ${{ matrix.target }} - name: Upload binaries to artifacts id: upload-binaries uses: actions/upload-artifact@v4 with: name: binaries-${{ matrix.target }} path: | target/${{ matrix.target }}/release/static/**/* target/${{ matrix.target }}/release/*.exe target/${{ matrix.target }}/release/*.dll target/${{ matrix.target }}/release/SHA256SUMS target/${{ matrix.target }}/release/SHA256SUMS.sig target/${{ matrix.target }}/release/seelen_ui.pdb merge-binaries: needs: build-binaries runs-on: ubuntu-latest outputs: artifact-id: ${{ steps.merge-binaries.outputs.artifact-id }} steps: - name: Merge binaries artifacts id: merge-binaries uses: actions/upload-artifact/merge@v4 with: name: binaries pattern: binaries-* separate-directories: true delete-merged: true sign-binaries: needs: merge-binaries runs-on: ubuntu-latest steps: - name: Sign binaries with SignPath uses: signpath/github-action-submit-signing-request@v1 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: 1a9e9b37-229a-4540-a639-137deebee4e1 project-slug: seelen-ui signing-policy-slug: test-signing artifact-configuration-slug: binaries github-artifact-id: ${{ needs.merge-binaries.outputs.artifact-id }} output-artifact-directory: signed-binaries wait-for-completion: true - name: Upload signed binaries by target uses: actions/upload-artifact@v4 with: name: signed-binaries-x86_64-pc-windows-msvc path: signed-binaries/binaries-x86_64-pc-windows-msvc/**/* - name: Upload signed binaries by target uses: actions/upload-artifact@v4 with: name: signed-binaries-aarch64-pc-windows-msvc path: signed-binaries/binaries-aarch64-pc-windows-msvc/**/* bundle: needs: - update-tag - sign-binaries strategy: fail-fast: false matrix: include: - platform: windows-2025 target: x86_64-pc-windows-msvc - platform: windows-2025 target: aarch64-pc-windows-msvc runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup with: rust-targets: ${{ matrix.target }} cache-key-prefix: rust-bundle-${{ matrix.target }} - name: Install frontend dependencies run: npm install - name: Install MSIX dependencies shell: pwsh run: | winget upgrade winget --accept-package-agreements --accept-source-agreements --disable-interactivity --force || Write-Output "Ignoring winget update failure" winget install --id Microsoft.DotNet.AspNetCore.8 --accept-package-agreements --accept-source-agreements --force winget install --id Microsoft.DotNet.DesktopRuntime.8 --accept-package-agreements --accept-source-agreements --force winget install --id MarcinOtorowski.MSIXHero --accept-package-agreements --accept-source-agreements --force - name: Set Version run: | npx tsx scripts/versionish.ts ci ${{ needs.update-tag.outputs.version }} - name: Download signed binaries uses: actions/download-artifact@v4 with: name: signed-binaries-${{ matrix.target }} path: target/${{ matrix.target }}/release - name: Clean bundle folder from cache shell: pwsh run: | $bundlePath = "target/${{ matrix.target }}/release/bundle" if (Test-Path $bundlePath) { Remove-Item -Path $bundlePath -Recurse -Force Write-Output "Old bundles files deleted: $bundlePath" } - name: Bundle run: npx tauri bundle --ci --verbose --target ${{ matrix.target }} --no-sign - name: Bundle MSIX run: npx tsx scripts/bundle.msix.ts --target ${{ matrix.target }} - name: Upload bundles to artifacts uses: actions/upload-artifact@v4 with: name: bundles-${{ matrix.target }} path: target/${{ matrix.target }}/release/bundle merge-bundles: needs: bundle runs-on: ubuntu-latest outputs: artifact-id: ${{ steps.upload-merged.outputs.artifact-id }} steps: - name: Merge artifacts id: upload-merged uses: actions/upload-artifact/merge@v4 with: name: bundles pattern: bundles-* delete-merged: true sign-bundles: needs: - update-tag - merge-bundles runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Clean existing assets uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const tagName = 'nightly'; const release = await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag: tagName, }); const { data: assets } = await github.rest.repos.listReleaseAssets({ owner: context.repo.owner, repo: context.repo.repo, release_id: release.data.id, }); core.info(`Found ${assets.length} existing assets to clean`); const deletions = assets.map(asset => { core.info(`Deleting ${asset.name}`); return github.rest.repos.deleteReleaseAsset({ owner: context.repo.owner, repo: context.repo.repo, asset_id: asset.id, }); }); await Promise.all(deletions); core.info('✅ All existing assets cleaned'); - name: Submit to SignPath uses: signpath/github-action-submit-signing-request@v1 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: 1a9e9b37-229a-4540-a639-137deebee4e1 project-slug: seelen-ui signing-policy-slug: test-signing artifact-configuration-slug: nightly-bundles github-artifact-id: ${{ needs.merge-bundles.outputs.artifact-id }} output-artifact-directory: bundles - name: File Tree run: |- tree bundles - name: Tauri Updater Signature env: TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | npm install -g @tauri-apps/cli echo "Removing existing .sig files to regenerate with Tauri updater keys..." find ./bundles -name "*.sig" -type f -delete echo "Existing signatures removed" VERSION=${{ needs.update-tag.outputs.version }} PATH1="bundles/nsis/Seelen UI_${VERSION}_arm64-setup.exe" PATH2="bundles/nsis/Seelen UI_${VERSION}_x64-setup.exe" echo "Signing ${PATH1}..." tauri signer sign --verbose "$PATH1" echo "Signing ${PATH2}..." tauri signer sign --verbose "$PATH2" - name: Upload Signed Installers to release uses: svenstaro/upload-release-action@v2 with: tag: nightly file: bundles/**/* file_glob: true generate-update-file: needs: - update-tag - sign-bundles runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Get Release ID id: get-release uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const release = await github.rest.repos.getReleaseByTag({ owner: context.repo.owner, repo: context.repo.repo, tag: 'nightly', }); return release.data.id; - uses: ./.github/actions/generate-update-manifest with: release-id: ${{ steps.get-release.outputs.result }} version: ${{ needs.update-tag.outputs.version }} github-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/publish-core.yml ================================================ name: "Publish Core Library" on: workflow_call: workflow_dispatch: permissions: contents: read id-token: write jobs: publish-core: runs-on: ubuntu-latest defaults: run: working-directory: ./libs/core steps: - uses: actions/checkout@v4 - uses: denoland/setup-deno@v2 with: deno-version: v2.x - uses: actions/setup-node@v4 with: node-version: 24 registry-url: "https://registry.npmjs.org" - name: Setup Rust run: rustup --version - name: Build Rust Bindings (ts-rs) run: deno task build:rs - name: Publish to JSR run: deno publish --allow-dirty - name: Build NPM package run: deno task build:npm - name: Publish to NPM run: | cd ./npm npm --verbose publish --tag latest ================================================ FILE: .github/workflows/release.yml ================================================ name: "Release" on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" workflow_dispatch: permissions: contents: write id-token: write jobs: continuous-integration: uses: ./.github/workflows/ci.yml get-version: needs: continuous-integration runs-on: ubuntu-latest outputs: version: ${{ steps.get-version.outputs.result }} steps: - uses: actions/checkout@v4 - name: Get version id: get-version uses: actions/github-script@v7 with: result-encoding: string script: | const fs = require('fs'); const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')); return packageJson.version; create-release: needs: get-version runs-on: ubuntu-latest outputs: release_id: ${{ steps.create-release.outputs.result }} steps: - uses: actions/checkout@v4 - name: Create release (draft) id: create-release uses: actions/github-script@v7 with: script: | const fs = require('fs'); const version = "${{ needs.get-version.outputs.version }}"; const changelogContent = fs.readFileSync('changelog.md', 'utf-8'); const regex = new RegExp(`(?<=\\[${version}\\]\\s)([\\s\\S]*?)(?=\\s## \\[)`, 'g'); const releaseNotes = changelogContent.match(regex)?.[0].trim() || ''; const { data } = await github.rest.repos.createRelease({ owner: context.repo.owner, repo: context.repo.repo, tag_name: `v${version}`, name: `Seelen UI v${version}`, body: releaseNotes, generate_release_notes: true, draft: true, prerelease: false, }); return data.id; build-binaries: needs: create-release strategy: fail-fast: false matrix: include: - platform: windows-2025 target: x86_64-pc-windows-msvc - platform: windows-2025 target: aarch64-pc-windows-msvc runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup with: rust-targets: ${{ matrix.target }} cache-key-prefix: rust-build-${{ matrix.target }} - name: Install frontend dependencies run: npm install - name: Build Hook DLL run: cargo build --release --target ${{ matrix.target }} -p sluhk - name: Build (no bundle) env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: npx tauri build --ci --verbose --no-bundle --target ${{ matrix.target }} - name: Upload binaries to artifacts uses: actions/upload-artifact@v4 with: name: binaries-${{ matrix.target }} path: | target/${{ matrix.target }}/release/static/**/* target/${{ matrix.target }}/release/*.exe target/${{ matrix.target }}/release/*.dll target/${{ matrix.target }}/release/SHA256SUMS target/${{ matrix.target }}/release/SHA256SUMS.sig target/${{ matrix.target }}/release/seelen_ui.pdb merge-binaries: needs: build-binaries runs-on: ubuntu-latest outputs: artifact-id: ${{ steps.merge-binaries.outputs.artifact-id }} steps: - name: Merge binaries artifacts id: merge-binaries uses: actions/upload-artifact/merge@v4 with: name: binaries pattern: binaries-* separate-directories: true delete-merged: true sign-binaries: needs: merge-binaries runs-on: ubuntu-latest steps: - name: Sign binaries with SignPath uses: signpath/github-action-submit-signing-request@v1 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: 1a9e9b37-229a-4540-a639-137deebee4e1 project-slug: seelen-ui signing-policy-slug: release-signing artifact-configuration-slug: binaries github-artifact-id: ${{ needs.merge-binaries.outputs.artifact-id }} output-artifact-directory: signed-binaries wait-for-completion: true - name: Upload signed binaries by target uses: actions/upload-artifact@v4 with: name: signed-binaries-x86_64-pc-windows-msvc path: signed-binaries/binaries-x86_64-pc-windows-msvc/**/* - name: Upload signed binaries by target uses: actions/upload-artifact@v4 with: name: signed-binaries-aarch64-pc-windows-msvc path: signed-binaries/binaries-aarch64-pc-windows-msvc/**/* bundle: needs: - get-version - sign-binaries strategy: fail-fast: false matrix: include: - platform: windows-2025 target: x86_64-pc-windows-msvc - platform: windows-2025 target: aarch64-pc-windows-msvc runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup with: rust-targets: ${{ matrix.target }} cache-key-prefix: rust-bundle-${{ matrix.target }} - name: Install frontend dependencies run: npm install - name: Install MSIX dependencies shell: pwsh run: | winget upgrade winget --accept-package-agreements --accept-source-agreements --disable-interactivity --force || Write-Output "Ignoring winget update failure" winget install --id Microsoft.DotNet.AspNetCore.8 --accept-package-agreements --accept-source-agreements --force winget install --id Microsoft.DotNet.DesktopRuntime.8 --accept-package-agreements --accept-source-agreements --force winget install --id MarcinOtorowski.MSIXHero --accept-package-agreements --accept-source-agreements --force - name: Download signed binaries uses: actions/download-artifact@v4 with: name: signed-binaries-${{ matrix.target }} path: target/${{ matrix.target }}/release - name: Clean bundle folder from cache shell: pwsh run: | $bundlePath = "target/${{ matrix.target }}/release/bundle" if (Test-Path $bundlePath) { Remove-Item -Path $bundlePath -Recurse -Force Write-Output "Old bundles files deleted: $bundlePath" } - name: Bundle run: npx tauri bundle --ci --verbose --target ${{ matrix.target }} --no-sign - name: Rename normal bundle to temp shell: pwsh run: | $arch = "${{ matrix.target }}".Contains("aarch64") ? "arm64" : "x64" $version = "${{ needs.get-version.outputs.version }}" $bundlePath = "target/${{ matrix.target }}/release/bundle/nsis" $setupFile = "$bundlePath/Seelen UI_${version}_${arch}-setup.exe" $tempFile = "$bundlePath/Seelen UI_${version}_${arch}-setup_temp.exe" if (Test-Path $setupFile) { Move-Item -Path $setupFile -Destination $tempFile Write-Output "Renamed $setupFile to $tempFile" } else { Write-Error "Setup file not found: $setupFile" exit 1 } - name: Set Fixed Runtime shell: pwsh run: | $arch = "${{ matrix.target }}".Contains("aarch64") ? "arm64" : "x64" ./scripts/SetFixedRuntime.ps1 -Architecture $arch - name: Copy Runtime to target directory shell: pwsh run: | $runtimeSource = Get-ChildItem -Path "src/runtime" -Directory | Where-Object { $_.Name -match '^\d+\.\d+\.\d+\.\d+$' } | Select-Object -First 1 if ($runtimeSource) { $targetRuntimeDir = "target/${{ matrix.target }}/release/runtime" New-Item -ItemType Directory -Force -Path $targetRuntimeDir | Out-Null Copy-Item -Path $runtimeSource.FullName -Destination "$targetRuntimeDir/$($runtimeSource.Name)" -Recurse -Force Write-Output "Copied runtime from $($runtimeSource.FullName) to $targetRuntimeDir/$($runtimeSource.Name)" } else { Write-Error "Runtime version directory not found in src/runtime" exit 1 } - name: Bundle with Fixed Runtime run: npx tauri bundle --ci --verbose --target ${{ matrix.target }} --no-sign - name: Rename bundles shell: pwsh run: | $arch = "${{ matrix.target }}".Contains("aarch64") ? "arm64" : "x64" $version = "${{ needs.get-version.outputs.version }}" $bundlePath = "target/${{ matrix.target }}/release/bundle/nsis" $tempFile = "$bundlePath/Seelen UI_${version}_${arch}-setup_temp.exe" $normalFile = "$bundlePath/Seelen UI_${version}_${arch}-setup.exe" $fixedFile = "$bundlePath/Seelen UI_${version}_${arch}-setup-fixed.exe" Write-Output "Checking files in $bundlePath" Get-ChildItem -Path $bundlePath -Filter "*.exe" | ForEach-Object { Write-Output " Found: $($_.Name)" } if (Test-Path $normalFile) { Move-Item -Path $normalFile -Destination $fixedFile -Force Write-Output "Renamed fixed runtime bundle to $fixedFile" } else { Write-Output "Warning: $normalFile does not exist" } if (Test-Path $tempFile) { Move-Item -Path $tempFile -Destination $normalFile -Force Write-Output "Renamed normal bundle back to $normalFile" } else { Write-Output "Warning: $tempFile does not exist" } - name: Bundle MSIX run: npx tsx scripts/bundle.msix.ts --target ${{ matrix.target }} - name: Upload bundles to artifacts uses: actions/upload-artifact@v4 with: name: bundles-${{ matrix.target }} path: target/${{ matrix.target }}/release/bundle merge-bundles: needs: bundle runs-on: ubuntu-latest outputs: artifact-id: ${{ steps.upload-merged.outputs.artifact-id }} steps: - name: Merge artifacts id: upload-merged uses: actions/upload-artifact/merge@v4 with: name: bundles pattern: bundles-* delete-merged: true sign-bundles: needs: - get-version - create-release - merge-bundles runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Submit to SignPath uses: signpath/github-action-submit-signing-request@v1 with: api-token: ${{ secrets.SIGNPATH_API_TOKEN }} organization-id: 1a9e9b37-229a-4540-a639-137deebee4e1 project-slug: seelen-ui signing-policy-slug: release-signing artifact-configuration-slug: bundles github-artifact-id: ${{ needs.merge-bundles.outputs.artifact-id }} output-artifact-directory: bundles - name: File Tree run: |- tree bundles - name: Tauri Updater Signature env: TAURI_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} run: | npm install -g @tauri-apps/cli echo "Removing existing .sig files to regenerate with Tauri updater keys..." find ./bundles -name "*.sig" -type f -delete echo "Existing signatures removed" VERSION="${{ needs.get-version.outputs.version }}" PATH1="bundles/nsis/Seelen UI_${VERSION}_arm64-setup.exe" PATH2="bundles/nsis/Seelen UI_${VERSION}_x64-setup.exe" echo "Signing ${PATH1}..." tauri signer sign --verbose "$PATH1" echo "Signing ${PATH2}..." tauri signer sign --verbose "$PATH2" - name: Remove self-signed MSIX files (store-signed handled elsewhere) run: | echo "Removing self-signed .msix files from bundles..." find ./bundles -name "*.msix" -type f -delete || true echo "Self-signed .msix files removed (Store-signed MSIX will be added later by msix.yml workflow)" - name: Upload Signed Installers to release uses: svenstaro/upload-release-action@v2 with: release_id: ${{ needs.create-release.outputs.release_id }} file: bundles/**/* file_glob: true publish-release: needs: [create-release, sign-bundles] runs-on: ubuntu-latest steps: - name: Publish release (un-draft) uses: actions/github-script@v7 env: releaseId: ${{ needs.create-release.outputs.release_id }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, release_id: process.env.releaseId, draft: false, }); microsoft-store-submission: needs: [publish-release, merge-bundles] runs-on: windows-2025 steps: - uses: actions/checkout@v4 - name: Download merged bundles (unsigned) uses: actions/download-artifact@v4 with: name: bundles path: target/release/bundle # Use Store Broker to publish to Microsoft Store - name: Submit to Partner Center (aka DevCenter) shell: pwsh run: | ./scripts/SubmitToStore.ps1 env: PartnerCenterStoreId: ${{ secrets.MS_PRODUCT_ID }} PartnerCenterTenantId: ${{ secrets.MS_TENANT_ID }} PartnerCenterClientId: ${{ secrets.MS_CLIENT_ID }} PartnerCenterClientSecret: ${{ secrets.MS_CLIENT_SECRET }} SBDisableTelemetry: true generate-update-file: needs: [get-version, create-release, publish-release] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: ./.github/actions/generate-update-manifest with: release-id: ${{ needs.create-release.outputs.release_id }} version: ${{ needs.get-version.outputs.version }} github-token: ${{ secrets.GITHUB_TOKEN }} discord: needs: [get-version, publish-release] uses: ./.github/workflows/discord-notify.yml with: tag: v${{ needs.get-version.outputs.version }} secrets: inherit publish-core-library: needs: publish-release runs-on: ubuntu-latest permissions: actions: write steps: - name: Trigger publish-core workflow uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | await github.rest.actions.createWorkflowDispatch({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'publish-core.yml', ref: context.ref, }); ================================================ FILE: .gitignore ================================================ node_modules .vscode .env dist target gen src/runtime # Submission Files /scripts/SBTemp # rust-analyzer rustc-ice-*.txt CLAUDE.md GEMINI.md .claude ================================================ FILE: .npmrc ================================================ message = "chore(release): v%s" ================================================ FILE: AGENTS.md ================================================ # AGENTS.md Guidance for AI agents working in this repository. Seelen UI is a customizable Windows desktop environment built with: - Rust + Tauri (backend) - TypeScript + React/Preact (frontend) - A monorepo layout with shared libs under `libs/` ## Read First (Non-Negotiable) Build speed / safety: - DO NOT use `cargo build --release` for testing, type-checking, or local iteration. - Prefer `cargo check` for fast Rust validation. - Use `cargo build` (debug) only when you need a binary. Translations: - DO NOT run `npm run translate` during active development. - Add translations manually while iterating; run the translate command only right before a final commit. Rust locking order (avoid deadlocks): 1. CLI locks 2. DATA locks 3. EVENT locks Backend architecture rules: - System modules in `src/background/modules/` MUST follow the modern pattern (lazy init + lazy tauri registration). - Business logic must NOT call `emit_to_webviews` directly. WinRT / COM safety: - For WinRT objects with event subscriptions, use wrapper structs with `Drop` for automatic unregistration. - Windows-rs clones `TypedEventHandler` internally: store tokens, not handlers. ## Common Commands Initial setup: ```bash npm install && npm run dev ``` Dev / build: - `npm run dev` - Frontend dev workflow - `npm run build:ui` - Build UI bundles - `npm run tauri dev` - Run Tauri in dev mode - `cargo check` - Fast Rust type check - `cargo build` - Debug build Quality (Deno-based): - `deno lint` - `deno fmt` - `npm run type-check` - `npm test` Core library (`libs/core`): - `deno task build` - `deno task build:rs` - Regenerate Rust -> TypeScript bindings - `deno task build:npm` ## Repo Map (Where Things Live) Shared libraries: - `libs/core/` - Core library + Rust-generated TypeScript bindings - `libs/widgets-shared/` - Cross-widget state utilities (includes LazySignal) - `libs/slu-ipc/`, `libs/positioning/`, `libs/widgets-integrity/` Main app: - `src/background/` - Rust backend (modules, native integrations) - `src/service/` - System service components - `src/ui/` - Frontend apps (each subdirectory is an independent app) - examples: `src/ui/settings/`, `src/ui/toolbar/`, `src/ui/launcher/`, `src/ui/window_manager/` ## Frontend Conventions App architecture: - UI apps use a hexagonal-ish layering: `infra/`, `app/`, `domain/`, `shared/`. - Keep boundaries clean: `domain/` is pure logic; `infra/` is UI + integration. Styling: - CSS Modules are the default. - Naming: kebab-case for CSS, camelCase for TS. Internationalization: - All user-visible strings must be i18n. - Translation files live under `i18n/translations/` (YAML). ## Backend: System Modules (Modern Pattern) All modules in `src/background/modules/` follow this pattern: - `application.rs` owns the singleton manager and emits internal events. - `infrastructure.rs` (or `handlers.rs`) owns Tauri commands and bridges internal events -> webviews. - Tauri event registration happens lazily on first command access (via `Once`). Suggested layout: ``` src/background/modules// mod.rs application.rs infrastructure.rs # or handlers.rs domain.rs # optional ``` Minimal pattern (infrastructure side): ```rust use std::sync::Once; use seelen_core::handlers::SeelenEvent; use crate::{app::emit_to_webviews, error::Result}; use super::{YourEvent, YourManager}; fn get_manager() -> &'static YourManager { static REGISTER: Once = Once::new(); REGISTER.call_once(|| { YourManager::subscribe(|_event: YourEvent| { // Keep this small and side-effect focused. if let Ok(data) = get_your_data() { emit_to_webviews(SeelenEvent::YourDataChanged, data); } }); }); YourManager::instance() } #[tauri::command(async)] pub fn get_your_data() -> Result> { let manager = get_manager(); Ok(manager.get_data()) } ``` Minimal pattern (application side): ```rust use std::sync::LazyLock; pub struct YourManager { // fields } #[derive(Debug, Clone)] pub enum YourEvent { DataChanged, } event_manager!(YourManager, YourEvent); impl YourManager { fn new() -> Self { Self { /* init */ } } pub fn instance() -> &'static Self { static MANAGER: LazyLock = LazyLock::new(|| { let mut m = YourManager::new(); m.init().log_error(); m }); &MANAGER } fn init(&mut self) -> Result<()> { self.setup_listeners()?; Ok(()) } fn setup_listeners(&mut self) -> Result<()> { // Listen to OS signals; emit internal YourEvent::* (not webview events) Ok(()) } pub fn get_data(&self) -> Vec { // return data vec![] } } ``` When adding a new backend feature exposed to the UI, update `libs/core`: 1. `libs/core/src/handlers/commands.rs` ```rust slu_commands_declaration! { GetYourData = get_your_data() -> Vec, } ``` 2. `libs/core/src/handlers/events.rs` ```rust slu_events_declaration! { YourDataChanged(Vec) as "your-module::data-changed", } ``` 3. Regenerate bindings: `cd libs/core && deno task build:rs` ## WinRT Wrapper Pattern (Automatic Cleanup) Use wrappers for WinRT objects that register events. Rules: - Store event tokens (WinRT tokens are often `i64`). - Do NOT store `TypedEventHandler` values in struct fields. - Implement `Drop` to unregister events. Example: ```rust pub struct WinRtWrapper { pub object: SomeWinRtObject, token: i64, } impl WinRtWrapper { pub fn create(object: SomeWinRtObject) -> Result { let token = object.SomeEvent(&TypedEventHandler::new(Self::on_event))?; Ok(Self { object, token }) } fn on_event( _sender: &Option, _args: &Option, ) -> windows_core::Result<()> { Ok(()) } } impl Drop for WinRtWrapper { fn drop(&mut self) { self.object.RemoveSomeEvent(self.token).log_error(); } } ``` ## Shared State: LazySignal (Cross-Widget) Use `LazySignal` (in `libs/widgets-shared/`) when state is: - fetched asynchronously (invoke/system APIs) - updated by async events - shared across widgets/webviews Critical usage pattern: 1. Create lazy signal with async initializer. 2. Register event listeners first (they may fire immediately). 3. Call `.init()` last; it must not overwrite a value set by an event. Example: ```ts import { lazySignal } from "libs/widgets-shared/LazySignal"; import { invoke, SeelenCommand, SeelenEvent, subscribe } from "@seelen-ui/lib"; const $data = lazySignal(async () => { return await invoke(SeelenCommand.GetYourData); }); subscribe(SeelenEvent.YourDataChanged, (event) => { $data.value = event.payload; }); await $data.init(); ``` ## Creating Svelte Widgets (High-Level) Seelen UI supports standalone Svelte widgets. Prefer following existing widget patterns; do not invent new build plumbing. Typical pieces: 1. Static widget definition: `src/static/widgets//` 2. Svelte app: `src/ui/svelte//` 3. Theme styles: `src/static/themes/default/styles/.scss` 4. i18n: translations (and keep the translation workflow rule) 5. Optional Rust backend integration (use the modern module pattern) Widget checklist: - Static metadata and HTML exist under `src/static/widgets//` - Svelte entry mounts into `#root` and calls `Widget.getCurrent().init(...)` - Shared, event-driven state uses LazySignal - Styling uses existing CSS variables; avoid global class conflicts Shared styling for widgets: - Use `data-skin` attributes for common control styling (buttons, inputs) to avoid class collisions. ## Rust Types: Tagged Enums (Serde) Avoid tuple variants for internally tagged enums. Bad: ```rust #[serde(tag = "type")] pub enum Action { WithData(String), } ``` Good: ```rust #[serde(tag = "type")] pub enum Action { WithData { data: String }, } ``` ## Testing Expectations - Prefer quick feedback loops (`cargo check`, `npm run type-check`, `deno lint`). - Keep changes scoped; add tests when behavior changes. ================================================ FILE: CLA.md ================================================ Seelen UI - Individual Contributor License Agreement (CLA) Thank you for your interest in contributing to the Seelen UI project. By submitting a contribution to the Project, you agree to the following terms and conditions: 1. Grant of License: By submitting a contribution to the Project, you grant the Project Owner a non-exclusive, perpetual, irrevocable, worldwide license to use, copy, modify, distribute, and sublicense your contribution, as well as any derivative works thereof, under the terms of the open-source license under which the Project is distributed. 2. Copyright: You represent that you are the legitimate author of the submitted contribution or that you have the necessary rights to grant the license described in point 1 above. 3. Warranties: You acknowledge that your contribution is provided "as is" and that you waive any warranties, express or implied, including, but not limited to, the warranties of merchantability, fitness for a particular purpose, and non-infringement. 4. Representations and Warranties: You warrant that the contribution you submit does not infringe any third-party intellectual property rights, and in the event of any third-party claim arising out of or related to your contribution, you agree to indemnify and hold harmless the Project Owner and its affiliates, agents, licensees, and sublicensees. 5. Acceptance: By submitting your contribution to the Project, you agree to abide by these terms and conditions. ================================================ FILE: CODE_OF_CONDUCT ================================================ # Seelen UI Code of Conduct ## 1. Our Pledge We as members, contributors, and maintainers pledge to make participation in the Seelen UI community a harassment-free experience for everyone. We welcome all regardless of: - Age, body size, disability, ethnicity, gender identity/expression - Level of experience, nationality, personal appearance, race, religion - Sexual identity/orientation, or any other characteristic ## 2. Expected Behavior All community members must: - Use welcoming and inclusive language - Respect differing viewpoints and experiences - Gracefully accept constructive criticism - Focus on what's best for the community - Show empathy toward other members ## 3. Unacceptable Behavior Unacceptable behaviors include: - Sexualized language/imagery and unwelcome advances - Trolling, insulting/derogatory comments, personal attacks - Public/private harassment - Publishing others' private information without permission - Other conduct reasonably considered inappropriate ## 4. Enforcement Responsibilities Project maintainers will: - Remove/edit inappropriate content - Warn or ban offenders for inappropriate behavior - Interpret these standards appropriately ## 5. Scope Applies to all community spaces including: - GitHub repositories and discussions - Discord/Slack channels - Social media accounts - In-person events ## 6. Enforcement Instances of abusive behavior may be reported to: - Email: [support@seelen.io] - Discord: [Modmail/DM contact] - GitHub: [@eythaann] All complaints will be reviewed and investigated promptly. ## 7. Attribution This Code is adapted from the [Contributor Covenant][homepage], version 2.1, available at: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [homepage]: https://www.contributor-covenant.org ================================================ FILE: CONTRIBUTING ================================================ # Contributing to Seelen UI Thank you for your interest in contributing to this project! We welcome contributions from everyone. By participating in this project, you agree to abide by the following guidelines and terms. ## How to Contribute 1. Fork the repository and clone it to your local machine. 2. Read the [Project Documentation](documentation/project.md) to understand the project structure and how to use it. 3. Create a new branch for your contribution: `git checkout -b feature/new-feature`. 4. Make your changes and ensure they are well-tested. 5. Commit your changes: - Ensure that your commits are signed. You can sign commits with `git commit -S -m 'Add new feature'`. - Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) syntax for commit messages, e.g., `feat: add new feature`, `fix(wm): resolve #567 issue`. 6. Push to the branch: `git push origin feature/new-feature`. 7. Submit a pull request. ## Contributor License Agreement (CLA) By submitting code as an individual, you agree to the [Contributor License Agreement](CLA.md). ## Code of Conduct This project adheres to the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT). By participating, you are expected to uphold this code. Please report any unacceptable behavior to [Maintainer's Email]. ## Licensing All contributions to this project are subject to the terms of the [GNU Affero General Public License v3.0](LICENSE). By contributing, you agree that your contributions will be licensed under the terms of this license. ## Contact If you have any questions or need further clarification, please feel free to join our [Discord server](https://discord.gg/ABfASx5ZAJ). ================================================ FILE: Cargo.toml ================================================ cargo-features = ["profile-rustflags", "codegen-backend"] [profile.dev] debug = "full" strip = false opt-level = 0 incremental = true rustflags = ["-Z", "threads=8", "-C", "link-arg=-fuse-ld=lld"] [profile.rust-analyzer] inherits = "dev" debug = false [profile.release] debug = "limited" strip = true opt-level = "z" lto = true codegen-units = 1 rustflags = ["-Z", "threads=8", "-C", "link-arg=-fuse-ld=lld"] [workspace] members = [ "libs/core", "libs/positioning", "libs/slu-ipc", "libs/utils", "src", "src/hook_dll", ] resolver = "3" lints = {} [workspace.dependencies] tokio = "1.49.0" thiserror = "2.0.18" serde = "1.0" serde_json = "1.0" serde_yaml = "0.9.34" log = "0.4" arc-swap = "1.8.1" backtrace = "0.3.76" base64 = "0.22.1" battery = "0.7.8" clap = "4.5.58" crossbeam-channel = "0.5.15" discord-rich-presence = "0.2.5" encoding_rs = "0.8.35" evalexpr = "11.3.1" fern = "0.7.1" futures = "0.3.31" image = "0.25.9" interprocess = "2.3.1" itertools = "0.14.0" lazy_static = "1.5.0" notify-debouncer-full = "0.3.2" minisign = "0.8.0" os_info = "3.14.0" owo-colors = "4.2.3" parking_lot = "0.12.5" phf = "0.11.3" quick-xml = "0.38.4" rand = "0.9.2" regex = "1.12.3" reqwest = "0.12.28" rust-i18n = "3.1.5" sha2 = "0.10.9" seelen-core = { path = "libs/core" } slu-ipc = { path = "libs/slu-ipc" } slu-utils = { path = "libs/utils" } positioning = { path = "libs/positioning" } sysinfo = "0.38.3" tauri = "2.10.3" tauri-build = "2.5.6" tauri-plugin-deep-link = "2.4.7" tauri-plugin-dialog = "2.6.0" tauri-plugin-fs = "2.4.5" tauri-plugin-http = "2.5.7" tauri-plugin-log = "2.8.0" tauri-plugin-process = "2.3.1" tauri-plugin-shell = "2.3.5" tauri-plugin-updater = "2.10.0" translators = "0.1.5" url = "2.5.8" urlencoding = "2.1.3" uuid = "1.20.0" walkdir = "2.5.0" widestring = "1.2.1" win-hotkeys = { git = "https://github.com/Seelen-Inc/windows-keyboard-hook.git", features = ["serde"] } win-screenshot = "4.0.14" windows = "0.62.2" windows-future = "0.3.2" wmi = "0.18.1" windows-core = "0.62.2" winreg = "0.55.0" time = "0.3.47" scc = "2.4.0" ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: README.md ================================================

Seelen UI Logo Seelen UI

Fully Customizable Desktop Environment for Windows
Available in 70+ Languages

[![Contributors](https://img.shields.io/github/contributors/eythaann/seelen-ui.svg)](https://github.com/eythaann/seelen-ui/graphs/contributors) [![Last Commit](https://img.shields.io/github/last-commit/eythaann/seelen-ui.svg)](https://github.com/eythaann/seelen-ui/commits/main) [![Version](https://img.shields.io/github/v/release/eythaann/seelen-ui.svg)](https://github.com/eythaann/seelen-ui/releases) [![Downloads](https://img.shields.io/github/downloads/eythaann/seelen-ui/total.svg)](https://github.com/eythaann/seelen-ui/releases)
Screenshot of Seelen UI desktop showing a customized desktop environment
Download Seelen UI from Microsoft Store Join the Seelen UI Discord community DigitalOcean Referral Badge
## Overview [Seelen UI](https://seelen.io/apps/seelen-ui) is a tool designed to enhance your Windows desktop experience with a focus on customization and productivity. It integrates smoothly into your system, providing a range of features that allow you to personalize your desktop and optimize your workflow. - **Be Creative**: Seelen UI lets you tailor your desktop to fit your style and needs. You can adjust menus, widgets, icons, and other elements to create a personalized and visually appealing desktop environment. ![Seelen UI Custom Theme](./documentation/images/theme_preview.png)
- **Enhance Your Productivity**: Seelen UI helps you organize your desktop efficiently. With a Tiling Windows Manager, windows automatically arrange themselves to support multitasking, making your work more streamlined. ![Seelen UI Tiling Window Manager](./documentation/images/twm_preview.png)
- **Enjoy your music**: With an integrated media module that's compatible with most music players, Seelen UI allows you to enjoy your music seamlessly. You can pause, resume, and skip tracks at any time without the need to open additional windows. ![Seelen UI Media Module](./documentation/images/media_module_preview.png)
- **Be faster!**: With an app launcher inspired by Rofi, Seelen UI provides a simple and intuitive way to quickly access your applications and execute commands. ![Seelen UI App Launcher](./documentation/images/app_launcher_preview.png)
- **User-Friendly Configuration**: Seelen UI offers an intuitive interface for easy customization. Adjust settings such as themes, taskbar layouts, icons, etc. With just a few clicks. ![Seelen UI Settings](./documentation/images/settings_preview.png)
## Installation > [!CAUTION] > Seelen UI requires the WebView runtime to be installed. On Windows 11, it comes pre-installed with the system. > However, on Windows 10, the WebView runtime is included with the `setup.exe` installer. Additionally, Microsoft Edge > is necessary to function correctly. Some users may have modified their system and removed Edge, so please ensure both > Edge and the WebView runtime are installed on your system. > [!NOTE] > On fresh installations of Windows, the app might show a white or dark screen. You only need to update your Windows > through Windows Update and restart your PC. You can choose from different installation options based on your preference: ### Microsoft Store (recommended) Download the latest version from the [Store](https://www.microsoft.com/store/productId/9P67C2D4T9FB?ocid=pdpshare) page. This is the recommended option because you will receive updates and a secure version of the program. _**Note**_: It may take around 1 to 3 business days for changes to be reflected in the Microsoft Store, as updates are approved by real people in the store. ### Winget Install the latest version using: ```pwsh winget install --id Seelen.SeelenUI ``` This option also uses the signed `.msix` package and ensures you have the latest secure version. Similar to the Microsoft Store, it may take around 1 to 3 business days for changes to be reflected in Winget, as updates are approved by real people in the `winget-pkg` project. ### .msix Installer Download the `.msix` installer from the [Releases](https://github.com/eythaann/seelen-ui/releases) page. This package is signed, ensuring a secure installation. This is the same option as the Microsoft Store but is a portable installer. ### .exe Installer Download the latest version from the [Releases](https://github.com/eythaann/seelen-ui/releases) page and run the `setup.exe` installer. This option is less recommended as the installer is not signed, which may cause it to be flagged as a potential threat by some antivirus programs. The `setup.exe` is updated more quickly than the Microsoft Store or Winget versions and also it receives notifications updates on new release. ## Usage Once installed or extracted, simply open the program. The easy-to-use and intuitive GUI will guide you through the configuration process. Customize your desktop environment effortlessly. ## Upcoming Features I’m excited to share some upcoming features for Seelen UI! Here’s a glimpse of what’s planned for the future: ### ~~App Launcher~~ ✅ I’m planning to develop an app launcher inspired by [Rofi](https://github.com/davatorium/rofi) on Linux. This feature will provide a sleek and highly customizable way to quickly access your applications. ![App Launcher Preview](https://raw.githubusercontent.com/adi1090x/files/master/rofi/previews/colorful/main.gif) _Image courtesy of [rofi-themes](https://github.com/dctxmei/rofi-themes)_ ### Customizable Popup Widgets I aim to introduce a set of fully customizable popup widgets, similar to the features available in [EWW](https://github.com/elkowar/eww). These widgets will be highly configurable and adaptable to your needs, providing an enhanced and interactive way to manage your desktop environment. ![Customizable Widgets Preview](https://raw.githubusercontent.com/adi1090x/widgets/main/previews/dashboard.png) _Image courtesy of [adi1090x](https://github.com/adi1090x/widgets)_ ### Custom Alt + Tab (Task Switching) An upgraded Alt + Tab system for task switching is on the horizon. This will offer a more visually appealing and functional experience, allowing for smoother transitions between open applications and windows. ### Custom Virtual Desktops Viewer and Animations I’m also working on a custom virtual desktops viewer and dynamic animations to improve navigation between different workspaces. This will provide a more intuitive and immersive multitasking experience. Stay tuned for more updates as I develop these features. I appreciate your support and enthusiasm! Happy customizing! The Seelen UI Team ## Contributing We welcome contributions! - Read the [Contribution Guidelines](CONTRIBUTING) to get started with terms. ## License See the [LICENSE](LICENSE) file for details. ## Contact For inquiries and support, please contact me on [Discord](https://discord.gg/ABfASx5ZAJ). ## Sponsors We're grateful for the support of our sponsors who help make Seelen UI possible. | Sponsor | Description | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------- | | [![DigitalOcean](https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%201.svg)](https://www.digitalocean.com/?refcode=955c7335abf5&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) | **DigitalOcean** provides cloud infrastructure services that power our development and testing environments. | | [![SignPath](https://avatars.githubusercontent.com/u/34448643?s=60)](https://signpath.io/) | **SignPath** provides free code signing certificates, ensuring secure and trusted releases for our users. | ## See you later ``` . .& _,x&"`` & . &' ;.&&' &. . &.& .0&&&;&""` . '& &.&&& .&&&&&' .& ;&&& &&&&&' && &&&&&&&& &&& 0& . &&&&&&&&"" && .0 &&&&&&& 0&& .&' &&&&&& :&&&&& . &&&&& 0&&&& & &&&&& &&&&' &&&&&&& .&&&x& &&&& :&&&&&0.&' , .&&&&&&&&&&;. &&&&. &&&&&&&& .&&&&&&&&&&' . 0&&&& &&&&&&& ,&&&&&&&&&&&& & :&&&&; &&&&&0 ,;&&&&&&&&&&& ; .0 0&&&&&&&&&&0 ,;&&&&&&&&&&&&& & &; 0&&&&&&&&&&0 :',;".&&&&&&".& && &0 0&&&&&&&&&0 ',;',&&&&&" ,&' &&&&0 0&&&&&&&&&0 ,x&&&&" .&&& &&&&0 0&&&&&& .&&&&"'''"&&"&& &&&&&0 0&& .&&;`` `&: :& &&&&&&0 &"' &&&&&&&& &"& &"& &&&&&&&&0 0&&&&&&&&&&&&&&&&&&&&&&&&&0 0&&&&&&&&&&&&&&&&&&&0 Seelen 0&&&&&&&&&0 ``` --- 📌 **Official Website**: [https://seelen.io](https://seelen.io) Seelen Inc © 2026 - All rights reserved ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Supported Versions At Seelen UI, we always recommend using the latest available version, as all versions are kept secure and up to date. | Version | Support | | ------- | ------------------ | | Latest | :white_check_mark: | ## Reporting a Vulnerability If you discover a vulnerability in Seelen UI, we appreciate you reporting it promptly. ### How to Report - **Email:** Send an email to [support@seelen.io](mailto:support@seelen.io). - **Discord:** Send a message to the project staff on our official Discord server. ### What to Expect - **Acknowledgment:** You will receive a confirmation of receipt within 48 hours. - **Updates:** We will keep you informed about the status of the investigation and any actions taken. - **Resolution:** If the report is accepted, we will work on a solution and implement it in the next update. Thank you for helping us keep Seelen UI secure for all users. ================================================ FILE: changelog.md ================================================ # Changelog ## [2.5.7-dev] ### features - allow split multiple windows into separated items on dock. - trash bin module for dock. - allow change polling interval for system resources (cpu, ram, network usage, etc). ### fix - not displaying tooltips on settings. - apps menu folder creation being skipped. - apps menu folder items not scrollable when overflowing. - some msix apps using resources mapping via 'pri' files not having icons (ex: iCloud). - PWA edge apps not extracting icons. - power menu and workspace viewer not scaling properly when users set text scale factor. - icons not refreshing on new installed apps. - not clearing icon extraction failure list on cache clear. - toolbar and calendar language not changing. - flyout widget being shown on media player close as a dot in the screen. ## [2.5.6] ### fix - dock pinned apps not being rendered correctly. ## [2.5.5] ### features - disable all css animations on performance mode extreme. - add more position options for flyouts. - show custom browser profile icons instead program one. - add 'pin to dock' option on apps menu widget. ### refactor - dock state reimplemented. ### fix - slow down on performace while dragging apps. - prevent win32 handle leaks. - user menu slow render. - calendar language not being changed. - media player stucked state. - some lnk files (ex: docker) not appearing on apps menu. - app not starting on corrupted settings files. ## [2.5.4] ### features - new configurations for toolbar customization. ### enhancements - log system. - icon pack extractor and cache system. ### fix - toolbar and dock state resetting on widget reload. ## [2.5.3] ### fix - can't open apps using apps menu. - app crashing for new users. ## [2.5.2] ### features - add new widget security layer for exposed commands. - apps menu optinal acrylic effect. - allow users decide if use or not hardware acceleration. ### enhancements - widgets loading time. - settings resources lists. ### refactors - remake wallpaper manager from react to svelte. ### fix - infinity switching of items on toolbar. - app not starting correctly from service. - power menu clossing instantly. - settings by apps not working after saving new state. - context menu on toolbar bottom position. - external links being opened on webview instead external browser. - apps menu widget freeze on search. - change wallpaper shortcuts not working. - apps menu drag and drop. ## [2.5.1] ### enhancements - add liveness prove for widgets. ### fix - app failing opening on MSIX version. ## [2.5.0] ### features - add corner actions on toolbar. - add toggle desktop module on dock. - allow layout sorting by windows dragging on the tiling window manager. - workspaces viewer gui. - wallpapers per monitor and per workspace. - wallpaper collections. - allow set start of week on calendars. - extended wallpapers across all monitors. - new start menu widget. - customizable flyouts (volume, brightness, workspaces, notification, etc) - resources usage plugins for toolbar. ### enhancements - improve display changes events. - tiling window manager now swap windows by center point on drag end. - improve windows preview system. - improvements on wallpaper manager syncronization and changes. - settings UX improvements. - extraction on local video wallpapers thumbnails. - avoid reload the UI while playing as this can cause fps drops. - reduce memory usage! - copy button for icon list on settings. ### refactor - toolbar items structure (breaking change). ### fix - window manager moving windows being currently dragged by the user. - power menu on multi-monitor setups. - showing paused message on no video wallpapers. - app not working on local accounts. - app and service not running if already running on another session. - high cpu usage while user session inactive. - fix stale battery percentage on sleep wake up. - click on keyboard layout no changing the layout on the system. - window labels not being shown on dock. - tiling window manager not pausing on maximized/fullscreen apps. - run as admin not working on dock/taskbar. - single key shortcuts not taking in care holded keys. - wallpaper sometimes not showing on windows 11. - no emitting brightness events. - monitors missmatch. ## [2.4.11] ### fix - tray not working on MSIX installation. - shortcuts not being enabled by default but widgets yes. ## [2.4.10] ### refactor - entire rewrite of the bluetooth module. - internal widgets loader. - internal generated icon pack logic. ### enhancements - expose complementary colors of the system accent to css. - change theme config url from input to local file selector. - lazy widgets initialization. - auto closing of popup widgets to save resources. - show paused message on wallpaper manager. ### fix - no updating data attribute on toolbar for maximized windows on background. - ghost items on dock and window manager. - missing messages to the service on startup. - options field on theme settings rendering an input instead of selector. - missing events on app windows hook. - wasted space reserved for tiling window manager when dock/toolbar are disabled on monitor. - fix missing variables values on default percentages for theme variables. - #1290 dock and toolbar showing on other monitors when mouse is at edge of screen. ## [2.4.9] ### features - implement wallpaper downloads. - add new wallpaper type "layered" it allows create unique wallpaper via css. - new css variables added related to the date (hour, minute, day, month, year). ## [2.4.8] ### enhancements - toolbar overlapped and hidden state. - addition of pdb files on nightly builds. - interactable windows matching system. ### fix - crash caused by wallpaper manager. - toolbar being shown while using fullscreen apps. - dock being shown while using fullscreen apps. ## [2.4.7] ### features - add new cli command to tranlate resource texts. ### enhancements - improve window manager matching system. ## [2.4.6] ### features - add new cli command to bundle resources. - allow close window using middle click on dock item preview. ### refactor - remove export resource button from settings. - remake wm ui from react to svelte (this reduced 10mb of ram usage). - remove native shell window hook, instead use win32 hooks via dlls. ### fix - infity switching loop on workspaces. - memory leak on long sessions. - external links not working. - wallpaper manager corner barder radius. - windows overlaping the toolbar and dock when autohide is disabled. - Power Menu not showing corrently on multiple monitors (#1266). ## [2.4.5] ### features - new power menu widget. - widgets now can declare plugins to be loaded. - new fields on widgets metadata. - new system tray widget. ### enhancements - disable individual shortcuts if attached widget is not enabled. - only show enabled themes as quick access in settings window. ### refactor - refactor on widget declarations for bundled widgets. ### fix - app cli was not working. - (#1258) missing translations on background process. - apps being filtering wrong. - stucked video wallpapers. - deadlocks on media manager. ## [2.4.4] ### features - add emergency shortcut to stop seelen process (Ctrl + Win + Alt + K). ### enhancements - improve development ecosystem of resources via embedding of yaml files. - allow open dev tools on release builds. - optimize window movement animations. - don't show version warning if target version is not present. ### fix - not considering multiple batteries on setups. - app not forcing restart on pc sleep/resume. - app/service ipc messages corrupted on encode/decode. - window manager animations enable logic. - window manager not forcing size of windows on tiling. - wallpaper paused while using Alt + Tab. - stucked menus on extreme performance mode. - stucked inline items after being dragged on toolbar. ### refactor - move core library to main repository. - remove app tray icon registration. - remove windows tray icons related code. - discord RPC now is disabled by default. ## [2.4.3] ### fix - notifications not showing or working as expected. - autohide not working properly on some screen resolutions. - managing incorrect app windows on restart the app. ## [2.4.2] ### features - tiling window manager animations. - performance mode for seelen. ### enhancements - improvement on power events. - reduce memory usage (RAM). ### fix - third party widgets sometimes not starting. - can not save settings on empty wallpaper list. - not updating UI on remotion of resources. - button to update downloaded resources not working properly. - crash on max integer caused by media players. - media player popup not working properly. - app not working when system installed on different drive unit. - twm not considerationg the dock size. ## [2.4.1] ### features - add a button to delete resources. - add a button to update downloaded resources. ### enhancements - cleanup of old service logs on start. - add time to service logs. ### fix - toolbar not showing when user deleted basic system folders. - hotkeys stopping working after lock screen. - update app button not showing correctly on settings. ## [2.4.0] ### features - wallpaper resources. - filter for wallpapers. - tranfomation for wallpapers. - wallpaper animations on in/out. - unique wallpaper slider per monitor. - new shortcuts to create/delete and change virtual desktops. - add force restart of gui, via shortcut `Ctrl + Win + Alt + R`. - now workspaces are per monitor. - now workspaces persist over restarts. - on window manager windows now can be float or tiled. - implement focus change via keyboard hotkeys on tiling window manager. - implement container swapping via keyboard hotkeys on tiling window manager. - swap twm containers on window drag event. ### refactor - monitors ids. - wallpaper manager. - virtual desktops. - tiling window manager. ### fix - drag of text toolbar items not working. - live wallpapers blocking screensaver and lock screen. - sleep/wake up events not working. ## [2.3.12] ### enhancements - add new ways of customization for theme settings ### fix - volume changed popup wrong placement. ## [2.3.11] ### features - allow add shared styles on themes to be applied to all the widgets. ### enhancements - dock and toolbar autohide behaviors. - animations on popups and start of the own component library. ### refactor - start migrating to preact signals. ### fix - (#1019) maximized windows being detected as fullscreen. - icorrect resource id regex. - sorting of items on dock and toolbar. ## [2.3.10] ### hotfix - missing react icons. ## [2.3.9] ### features - discord rich presence. ### refactor - icon packs implementation. - changed react to preact. ### fix - app crashing on start by missing user folder (desktop, pictures, etc). ## [2.3.8] ### enhancements - show a warning to users with monitors drivers disabled. - allow change workspace using wheel on toolbar. ### fix - (#897) stucked arrival notifications. - showing native notifications preview with the seelen ones (now only seelen arrival will be shown). - (#934) invisible edge window on dock. - (#925) invisible spotify widget on dock. - (#938) listing 6ghz networks as 5g networks. - (#962) JetBrains software isn't showing in dock. - (#923) Can't change media device on MSIX version. ## [2.3.7] ### enhancements - expose timeline on media players. - reduce bundled size of js code. - reduce verbosity on logs comming from widgets. ## [2.3.6] ### features - widgets implementation. - allow load/unload widgets via command line client. ### refactor - improve internal code quality related to app console client. ### fix - app not starting on start-menu cache corruption. - widgets being reloaded on ctrl + r. ## [2.3.5] ### fix - settings by monitor not working correctly. ## [2.3.4] ### enhancements - sort windows on dock by activation order. ### fix - duplicated themes on settings GUI. - bad toolbar color on multiple maximized windows. ## [2.3.3] ### features - add open window label on dock for app items (configurable). - add input to write custom text items on the toolbar. - add export resource button on developer tools. - allow settings by theme. ### enhancements - Settings UI refactor to follow the new resources ecosystem. - (#838) Improve dynamic color behavior on toolbar. ### fix - not removing old icons mask on icon pack change. - styles for dock media item. - not scrollable dock on overflow (many items). - media player not being correctly updated on player close event. - (#636) input experience and another background process appearing on dock. ## [2.3.2] ### enhancements - mini performance improve on dock. ### fix - resources not being updated correctly. ## [2.3.1] ### breaking changes - rename scope variables for toolbar plugins. ### refactor - improve inner code quality on toolbar plugins. ### enhancements - improve robustness on toolbar items to avoid blue screen. ## [2.3.0] ### breaking changes - remove mathjs eval by an more accurated eval for js code in toolbar plugins. This will break any plugin created before v2.2.10. ### features - add resources endpoint to home tab on settings. - add customizable and reusable popups implementation. - improvements on toolbar plugins system. - allow set buttons with custom actions on toolbar, via toolbar plugins. - add restore to default button for toolbar structure. - allow fetching remote data on toolbar plugins. ### enhancements - reduce CPU usage on slu-service process. - improve ui on toolbar modules. ### fix - media player styles on toolbar. - steam pin item on dock not working properly. - plugins not being updated on toolbar. ## [2.2.9] ### enhancements - store service logs in a file to help debugging. - wait for native shell on startup before start seelen ui. ## [2.2.7] ### fix - dock items not opening correctly. ## [2.2.6] ### feature - icons on icon packs now can declare a mask that could be used by themes. ### enhancements - add custom icons to bluetooth devices. - allow set different icons by color scheme (light or dark) on icon packs. ### fix - no dragable dock files and folders. - focusing widgets on creation. - not opening settings window when starting the app with an instance already running. ## [2.2.4] ### enhancements - add suspend/resume logic. ### fix - not restoring native taskbar on close/crash. - clear all notifications button not updating UI. - not translated date on chinese and norwegian. ## [2.2.3] ### enhancements - wrap webview console as soon as posible to avoid missing errors on logs. - wait some seconds before remove media players to avoid shifting on chrome. ### refactor - remove minified classnames and add do-not-use prefix to be clear to users. ### fix - panic on media module when loading initial devices. - discord window without umid (for now umid was hardcoded). - focused app not updating on title change. - not considerating accesibility text scale factor on toolbar and dock. - wheel not changing volume level. - clear all notifications button not working correctly. ## [2.2.2] ### enhancements - add option to disable dynamic colors on toolbar. - reduce notification arrival time on screen from 10 seconds to 5 seconds. ### fix - not showing some notifications. - crash when disabling a monitor on windows native settings. - unsyncronized clock on toolbar. ## [2.2.1] ### enhancements - add new bundled theme as example of animated icons with css. ## [2.2.0] ### features - add option to disable app thumbnail generation (dock). - allow lock the dock/toolbar items. - show instances counter of the same app on dock. - allow set the toolbar on different positions. - allow set custom start menu icon. - language selector for toolbar. - add dynamic color by focused app on toolbar. - add hibernate button on toolbar power menu. - add media volume mixer by apps and by device. - add clickable notifications, images, and more. ### enhancements - settings shown each time on startup. - expand power module with power plan. ### refactor - update windows-rs crate to 0.59.0. ### fix - showing domain on username for local accounts. - showing application frame host instead real app name. - incorrect event order on win events. ## [2.1.9] ### fix - shortcuts not working on MSIX. - empty username for local accounts. ## [2.1.8] ### features - add button to clear the cached icons on settings. ### enhancements - allow custom icons by extension on icon packs. ### fix - error on file icons. - fix blue screen on toolbar when errors on template evaluation. ## [2.1.7] ### fix - power module not clickable on toolbar. - pinned items not working correctly for some apps. - electron apps without aumid. ## [2.1.6] ### fix - missing translations. - fullscreen match not beeing removed. ## [2.1.5] ### fix - devices and battery not clickables on toolbar. - missing icon on dock media module while not playing. - msix store not showing admin prompt. - links on settings not opening. ## [2.1.4] ### fix - crash on user module. - slow loading of toolbar. - no icons on PWA from edge browser. - icon packs not modifying icons on toolbar/dock media modules. - icon packs bad ordering, now the priority order is (umid > full-path > filename > extension). - bad dock execution path on apps with property store umid but no shortcut on start menu. ## [2.1.3] ### fix - bad user infomation on user module. ## [2.1.2] ### fix - style issue on toolbar user module. - showing unhandable tray icons (ex: nvidia old control panel). - installer being frozen on update. ## [2.1.0] ### features - allow custom animations on popups/dropdowns. - show open new window buttons on dock app items context menu. - toggle dock items using win + number. - notifications count on dock app items. - add brightness slider to quick settings on toolbar. - add user module on toolbar. ### refactor - create separated system service to handle elevated actions. ### fix - ghost windows caused by a refactor donde on v2.0.13. - not showing save button after icon packs change. - app failing when powershell is not part of the $PATH enviroment. - dock items no updatings paths on store updates. - unremovable workspace module on toolbar. - missing system tray icons. - date not being inmediately updated on settings change. - ghost notification hitbox preventing mouse events on windows. ## [2.0.14] ### hotfix - not creating the default (system) icon pack. ## [2.0.13] ### features - add kill process option on context menu for dock items. - multi-language calendar and date module on toolbar. - add icon packs selector on settings > general. - allow show only windows on monitor owner of Dock/Taskbar. ### enhancements - allow search languages by their english label. ### refactor - move dock state to the background. - remove pin sub-menu on dock. ### fix - slu-service was being closed on exit code 1. - logging errors on monitor changes. - duplicated items on dock after drag items. ## [2.0.12] ### fix - msix version crashing on start. ## [2.0.11] ### features - add new setting on dock and toolbar to maintain overlap state by monitor. - add service to restart the seelen-ui app on crash. ### enhancements - force run the app as an APPX if it was installed using msix. ### refactor - custom http server to serve files instead bundle it in the executable on development (local). ### fix - remove ghost settings window from dock. ## [2.0.10] ### fixes - fix settings `cancel` button not working correctly. - fix settings `save` button not saving the monitor settings correctly. ## [2.0.9] ### enhancements - add `XboxGameBarWidgets.exe` to the bundled apps settings list. ### refactor - themes now use widget ids instead hard-coded keys. - improvements on events propagation. - improvements on settings by monitor implementation. ### fix - window manager not working properly. - resolution, scale changes not refreshing the UI. ## [2.0.8] ### enhancements - add task manager shortcut on toolbar and dock context menu. - improve default no media style on dock. - add icons to context menus. ### fix - randomized wallpaper slice freeze. - bad behavior on context menus and popups. ## [2.0.7] ### fix - crashing on fresh installations. ## [2.0.6] ### feature - add bases for future plugins and widgets sytems. - change wallpaper randomly. ### enhancements - some UI/UX improvements on seelen wallpaper manager. - UI/UX improvements on wi-fi toolbar module. - seelen-ui added to user PATH enviroment variable. ### fix - popups and context menus fast flashing. - media player app not appearing on media module (weg & toolbar). - touch looking the cursor. - improve start up failure behavior. ## [2.0.5] ### features - allow change default output volume using mouse wheel on media module items. - add mini calendar to date module. ### enhancements - add new settings to delay the show and hide of dock and toolbar. ### fix - dock and toolbar animations - fix update notifications for release and nightly channels. ## [2.0.4] ### fix - app crashing when changing settings on app launcher. - app previews on wrong position on dock. ## [2.0.3] ### fix - apps being runned as admin instead normal. ## [2.0.2] ### fix - infinite render loop on settings home page, fetching news. ## [2.0.1] ### refactor - unification of SeelenWeg pinned files, folder and apps in a single structure. ### enhancements - improve open_file function to allow arguments. - allow to users select update channel between release, beta and nightly. ### fix - not getting icons directly from .lnk files. - users not recieving update notification on settings header. - start-menu item on dock not closing native start menu. - default theme wallpaper showing cut on ultra-wide monitors. ## [2.0.0] ### breaking changes - Window Manager Layout Conditions was reimplemented, old conditions (v1) will fail. ### refactor - refactors, more and more refactors, refactors for everyone. - reimplementation of Tiling Window Manager. - remove Update modal at startup by an update button on settings. ### features - make the dock/taskbar solid when hide mode is `never`. - add app launcher (rofi for windows). - add seelen wall (rain-meter and wallpaper engine alternatives). - expose function to pin items into the dock. - settings by monitor. - window manager multimonitor support. - allow users change date format directly on UI settings. - add context menu to toolbar items. ### enhancements - improve quality icons from all app/files items. - improve init loading performance. - improve fullscreen matching system. - reduce UI total size from 355mb (v1) to 121mb (v2-beta4) to 93mb (v2-beta8). - reduce Installer size from 75mb (v1) to 40mb (v2-beta4) to 28.8mb (v2-beta8). - allow drop files, apps and folders into the dock to pin them. - now Virtual Desktop shortcuts doesn't require Tiling WM be enabled to work. - now Themes are wrapped in a CSS layer, making easier the override theming. - allow change size of Window Manager Layouts via window resizing with the mouse. - allow close windows by middle clicking on dock items. - show icon of app in media players that are not uwp/msix. - show pwa apps like a separeted app from browser on dock. ### fix - missing icons for files with a different extension than `exe`. - losing cursor events on clicking a dock item. - app allowing be closed via Alt + F4. - native taskbar being hidden regardless of whether the program starts successfully or not. - app continuing running when the program fails to start (case: WebView2 Runtime not installed). - no stoping correctly secondary processes/threads on app close. - showing unmanageable windows on dock. - restart seelen-ui button not working properly. - tray icons not working on others language than english. - edge tabs open in file explorer. ## [1.10.6] ### fix - tray module only working when the system language is english. ## [1.10.5] ### fix - app crashing on IMMDevice disconnection. ## [1.10.4] ### enhancements - clean weg items on load to remove duped items and apps/files that don't exist. - remove 1/2px thickness border on window manager border. - remove 1/2px black border on some previews of apps. ### fix - can not restore settings window. - taskbar not been restored when changing weg enabled state. - taskbar been restored always as not autohide, now it will restored as initial state. ## [1.10.3] ### features - add beta channel ### enhancements - add debugger cli toggles to tracing more info on logs. - media modules now exports the app related to the media player. ### fix - saving ahk lib in wrong location. ## [1.10.2] ### fix - app crashing on enumerating many monitors or on large load. ## [1.10.1] ### fix - app crashing if uwp package has missing path. - app no working fine on multiple monitors. ## [1.10.0] ### features - add volume changed popup. - new custom virtual desktop implementation. - shortcut to toggle debug mode on hitboxes (Control + Win + Alt + H). ### enhancements - remove black borders of windows previews on dock. - improve uwp app manage on dock/taskbar. ### refactor - add strategy pattern to virtual desktops. ### fix - topbar hitbox rect on auto hide equals to on-overlap. - bad matching fullscreen apps. - suspended process (ex: Settings) been shown on dock. - uwp icons not loading correctly. - bad focused app matching strategy. ## [1.9.11] ### features - add a option to hide apps from the dock/taskbar, requested on #5. - update tray labels when tray icons module are open. - add auto-hide option to the toolbar. ### fix - route no maintaining on cancel changes on settings window. - cancel button no working correctly after save the settings multiple times. - tray module no forcing tray overflow creating on startup. - native taskbar not been restored on close. ## [1.9.10] ### features - add `getIcon` fn to the scopes of toolbar placeholders. ### refactored - improve interfaces and documentation. ### fix - styles of media module when dock is on left side. - opened apps been removing on weg items file change. - app crashing on update if language prop was null in the settings.json file. ## [1.9.9] ### refactored - internal interfaces to improve documentation and development. ### enhancements - add language selector to the nsis installer. - allow search on lang selector on Seelen UI Settings. ### fix - app no opening to new users. ## [1.9.8] ### enhancements - avoid recreate already existing folders. - separate lib and app in two crates. - improve click behavior on seelen weg item to make it more intuitive. ### fix - can no disable run on startup. - text been cut on toolbar. - app crashing on wallpaper change on win11 24h2 ## [1.9.7] ### enhancements - made all invoke handlers async ### fix - crash on registering network event ## [1.9.6] ### fix - app crashing on 24h2 ## [1.9.5] ### fix - app crashing by tray icon module ## [1.9.4] ### fix - app crashing for new users ## [1.9.3] ### performance - reduce load time from ~7s to ~4s ### features - .slu and uri now are loaded correctly on seelen ui. - allow change wallpaper from seelen settings. ### enhancements - add file associations for .slu files - add uri associations for seelen-ui:uri - improve settings editor experience by adding live reload feature. ### fix - cli no working on production ## [1.9.1] ### fix - no listening window moving of virtual desktop events. - no closing or starting widgets on settings changes. - no listening monitors changes. - no loading toolbar modules on wake up ## [1.9.0] ### features - allow custom images on toolbar by `imgFromUrl`, `imgFromPath` and `imgFromExe` functions. - add notifications module to toolbar. - add exe path to window in generic module for toolbar. - add focused window icon to default toolbar layouts. ### enhancements - icons now are recreated from exe path if icon was deleted. - uwp icons now are loaded from background. - improvements on themes selector. - improvements on system color detection and expose more system colors based in accent gamma. - improve theme creation experience by adding live reload feature. - improve toolbar layouts (placeholders) creation experience by adding live reload feature. - improve weg items editor experience by adding live reload feature. ### refactor - deprecate `onClick` and add new `onClickV2` on toolbar modules. ### fix - bad translations keys. - no restoring dock on closing fullscreened app. ## [1.8.12] ### fix - app installed by msix no opening. ## [1.8.11] ### fix - remove unnecessary 1px padding on toolbar. ## [1.8.10] ### enhancements - remove unnecessary loop on taskbar hiding function. ### fix - no loading translations correctly on update modal. ## [1.8.9] ### enhancements - add translation to the rest of apps (dock, toolbar, and update modal). ### fix - not hiding the taskbar at start. - opening multiple instances of the app. ## [1.8.8] ### fix - app not running on startup ## [1.8.7] ### fix - no updating themes on changes saved. ## [1.8.6] ### features - Add multi-language support! 🥳. - Add default media input/output selectors to media module in fancy toolbar. - Add start module to dock/taskbar (opens start menu). ### enhancements - Flat default themes to allow easier overrides. ### fix - Fix zorder on hovering on weg and toolbar respectively to wm borders. - Applying bad themes on apps. - Not hiding the taskbar at start. ## [1.8.5] ### fix - no executing seelen after update installation ## [1.8.4] ## [1.8.3] ### refactor - migrate settings files from `$USER/.config/seelen` to `$APPDATA/com.seelen.seelen-ui` - load uwp apps info asynchronously ### fix - crash on move toolbar item - can not remove media module ## [1.8.2] ### features - fancy toolbar items now can be dragged of position. - using fancy toolbar's layout now can be modified and saved as custom.yml. ## [1.8.1] ### features - styles can be specified in fancy toolbar placeholder item. - fancy toolbar item now will have an unique id, this can be specified in the placeholder file. ### enhancements - replace "bluetooth" for "devices" on bundled fancy toolbar placeholders. ## [1.8.0] ### features - Media module added to the toolbar. - Media module added to SeelenWeg. ![Media Module Example](documentation/images/media_module_preview.png) - SeelenWeg now has a context menu (Right Click Menu). ### enhancements - enhancements on fullscreen events. ### refactor - remove Default Wave animation on seelenweg (users will be able to add their own animations). ### fix - no updating colors correctly on change light or dark mode on windows settings. - window manager enabled by default for new users. - showing tray icons with empty name. - no focusing seelen settings if it was minimized. ## [1.7.7] ### fix - no registering system events (battery/network/etc) ## [1.7.6] ### enhancements - improve logging on dev mode and fix missing target on production logged errors. - improve fullscreen matching. ### fix - network icon showing incorrect interface icon (lan instead wifi). - no updating adapters list and using adapter on network changes. ## [1.7.5] ## [1.7.4] ### enhancements - improvements on workflows to auto upload artifacts to the store. ## [1.7.3] ### enhancements - improvements on fullscreen events. ## [1.7.2] ### enhancements - disable tiling window manager on windows 10 from UI (can be forced on settings.json file) ### fix - app crashing on windows 10 - empty tray icons list on windows 10 ## [1.7.1] ### enhancements - separate `information` and `developer tools` tabs in the settings. - add a option to open the install path in explorer. - focus settings window if already exist. - better performance on canceling changes in settings. - avoid loading innecesary files in modules that are not used. - update pinned apps path by filename on open (some apps change of path on updates so this will fix that). - show empty message on toolbar when no wlan networks are found. ### fix - ahk error on save. ## [1.7.0] ### features - add Network toolbar module. - add WLAN selector to the Network toolbar module. - add css variable (--config-accent-color-rgb) to be used with css functions like `rgb` or `rgba`. ### enhancements - now placeholders, layouts and themes can be loaded from data users folder (`AppData\Roaming\com.seelen.seelen-ui`) - now buttons and others components will use the user accent color. ### fix - no max size on System Tray List module. ## [1.6.4] ### fix - xbox games showing missing icons on SeelenWeg. ### enhancements - follow user accent color for tray list and windows borders ### fix - no showing promoted (pinned on taskbar) tray icons. - toolbar no initialized correctly sometimes, now will retry if fails. - battery no updating level. - battery showing as always charging on default toolbar templates. - tray overflow module no working on different languages. ### refactor - refactor on window_api and AppBar structures. ## [1.6.3] ### enhancements - only show a progress bar on update and not the complete installer GUI. ### fix - main app no running if the forced creation of tray overflow fails. ## [1.6.2] ### features - now `batteries` and `battery` (same as: `batteries[0]`) are available on the scope of power toolbar module. ### enhancements - add battery crate to handle batteries info directly from their drivers. - show if is smart charging. - now battery module wont be shown if batteries are not found. ### fix - battery showing 255%. ## [1.6.1] ### fix - tray icons not showing on startup - hidden trays if icon was not found (now will show a missing icon) ## [1.6.0] ### features - add "Run as admin" option at context menu on Seelenweg. - allow receive commands using TCP connections. - Add System Tray Icons module, (incomplete, devices like usb or windows antivirus trays are still not supported). ### enhancements - improve power (battery) events performance. - Window manager disabled by default to new users. ### refactor - remove tauri single instance plugin by TCP connection. ## [1.5.0] ### features - new placeholder added to the bundle as alternative to default. - new workspace item available to be used in placeholders. ### enhancements - support fullscreen apps (will hide the toolbar and the weg on fullscreen an app). ### fix - showing incorrect format on dates at start of the app. - complex text with icons on toolbar items cause wraps. - missing icons on some uwp apps. ### refactor - refactor on window event manager to allow synthetic events. ## [1.4.1] ### fix - no truncating text on toolbar items overflow. - rendering empty items on toolbar when empty placeholder is declared. ## [1.4.0] ### features - Modular Themes - Themes now allow tags to be categorized. - Allow add, organize, combine multiple themes as cascade layers. - Themes now allow folder structure to improve developers experience. ### refactor - Now themes will use .yml files instead json to improve developers experience. - Themes schema updated, no backwards compatibility with json themes. (.json in themes folder will be ignored) ### fix - No hiding the taskbar correctly. ## [1.3.4] ### enhancements - Add splash screen to Settings window. - Add discord link on Information Section. ### refactor - Use TaskScheduler for autostart Seelen with priority and admin privileges. ### fix - bad zorder on Weg and Toolbar under the WM borders ## [1.3.3] ### features - Multi-monitor support for Fancy Toolbar. - Multi-monitor support for Seelenweg. ## [1.3.2] ### enhancements - Remove unnecessary tooltip collision on toolbar items. ### fix - Crash on restoring app in other virtual desktop using Weg. - Touch events not working on Toolbar and Weg. ## [1.3.1] ### fix - disable binding monitors and monitors on apps configurations for now. ## [1.3.0] ### features - Allow pin apps on Open using Apps Configurations. - Allow changes Shortcuts using UI. - Allow Binary Conditions in Apps Configurations Identifiers. - Allow change the Auto hide behavior for Seelenweg. ### enhancements - Close AHK by itself if app is crashed or forcedly closed. - Configurations by apps are enabled again. - Allow open settings file from Extras/Information - Add opacity to toolbar (theme: default) ### fix - Ahk not closing on app close or when user change options. ## [1.2.4] ### enhancements - Automatic MSIX bundle. - Add Github Actions for Releases. - Add Github Actions for Web Page. ## [1.2.3] ### features - Allow customize Fancy Toolbar modules using placeholders yaml files. - Add fast settings for toolbar allowing to adjust volume, brightness, etc. ## [1.2.2] ### enhancements - if app on weg is cloak, change of virtual desktop instead minimize/restore ### fix - no closing AHK instances - floating size on fallback - reservation not working properly - ignore top most windows by default (normally these are tools or widgets) - minimization on weg not working properly if window manager is activated - change focus using commands not working with conditional layouts - randomly frozen app on start - no tiling UWP apps ## [1.2.1] ### enhancements - Allow quit from settings - Using Box-Content style in the position of windows instead outlined for a better user experience ### fix - Managing windows without caption (Title bar) - can't update border configurations - hiding dock on switching virtual desktops ## [1.2.0] ### fix - Taskbar showing instead be always hidden ## [1.1.1] ### fix - Bad download URL in Update Notification - Showing update notification on installations by Windows Store ## [1.1.0] ### features - Add Smart Auto Hide for Seelenweg. - Add visible Separators Option - Enable animations for items into LEFT, TOP, RIGHT positions ### enhancements - Now the copy handles option will return hexadecimal handles instead decimal (good for faster debug in tools like spy++). ### fix - duped handles - inconsistencies in separators width ## [1.0.1] ### fix - App downloaded form Microsoft Store was not running without admin. ## [1.0.0] ### refactored - Update notifications always enabled for nsis installer - Update notifications will not appear if app is installed using msix (Microsoft Store). ### enhancements - Now by default if user is Admin, UAC will be triggered on run the app to allow a better integrated experience in SeelenWeg and Komorebi Tiling Manager. ## [1.0.0-prerelease.14] ### features - add indicator to know opens and focused apps on SeelenWeg - allow set the position of seelenweg (left, top, right, bottom) 🎉 ### enhancements - only creates app icons the first time they are loaded ### refactor - change themes implementation to allow customs css files ### fix - incorrect icon for UWP (was using store icon instead taskbar icon) - replacing icons on each load - showing logs of opened apps on development - offset margins working like windows RECT instead like one side margins ## [1.0.0-prerelease.13] ### features - add Themes Feature 🎉 (incomplete only for Seelenweg for now) - add SeelenWeg (a Dock or Taskbar) beta feature - add SeelenWeg in to Settings - add ContextMenu for apps in SeelenWeg - allow move apps in the Weg 😄 - add Grouped Apps in one item - live reload of Apps on events like change of title - UWP apps support ### enhancements - move readme blob to documentation/images ## [1.0.0-prerelease.12] ### enhancements - add some traces on functions to save logs ### fix - clean installation of komorebi no working ## [1.0.0-prerelease.11] ### refactor - little improvements on background code ### fix - initial users can not save the settings ## [1.0.0-prerelease.10] ### features - add a update tab to allow users decide if will receive notifications for updates ## [1.0.0-prerelease.9] ## [1.0.0-prerelease.8] - add functionality to pause btn on tray menu ## [1.0.0-prerelease.6] ### added - Enable Updater 🎉 ## [1.0.0-prerelease.3] ### fix - icon not showing on tray - poor icon quality on task bar - StartUp running bad exe file ## [1.0.0-prerelease.2] ## [1.0.0-prerelease.1] ### added - implement tray icon ### refactored - Migrate all app background from Electron ⚡ to Tauri 🦀 - reimplement startup to use native system startup - reimplement included shortcuts with ahk - reimplement komorebi autostart - reimplement installer to use NSIS - refactor folder structure to isolate front-end apps ## [1.0.0-beta.13] ### enhancements - improve maximized windows experience ### fixed - fix resize not working (now works like master) ## [1.0.0-beta.12] ### added - show current used versions on information - add grid layout preview - add win + k to open komorebi settings ### refactored - update komorebi to 0.1.22 ### removed - remove invisible borders feature ## [1.0.0-beta.11] ### fixed - missing property on schema - white screen on start app ## [1.0.0-beta.10] ### added - add a new way to match applications by path ### fixed - searching feature on apps - no focusing windows on change workspace - autostacking not working properly - workspaces rules not working ## [1.0.0-beta.9] ### added - add popups on actions 🦀 - now can switch from installed and packaged and should work as the same ### fixed - fix no removing old path - lag on many applications ## [1.0.0-beta.8] ### added - add more templates ## [1.0.0-beta.7] ### fixed - fix first install ## [1.0.0-beta.6] ### added - delete old paths on update ### fixed - fix not saving templates - fix toggle ahk shortcuts does not run or stop the instance - running ahk when disabled - not updating the path of installation folder on update for windows tasks ## [1.0.0-beta.5] ### added - new searching option for applications - templates feature ### fixed - including ghost apps on migration ## [1.0.0-beta.4] ### added - new feature of invisible borders per app - new easy way to hard restart the services and AHK ### changed - delete border overflow and changed for invisible borders per app ### fixed - components was not triggering dark mode correctly ## [1.0.0-beta.3] ### added - new apps templates - add AHK as a dependency to show to new users - add AHK settings ## [1.0.0-beta.2] ### added - export option for apps ### fixed - delete bound monitor and workspace on an application - bad installation on setup ================================================ FILE: crowdin.yml ================================================ base_path: "." base_url: "https://api.crowdin.com" preserve_hierarchy: true files: - source: i18n/**/en.yml translation: /%original_path%/%two_letters_code%.yml ================================================ FILE: deno.json ================================================ { "lint": { "rules": { "include": [ "verbatim-module-syntax" ], "exclude": [ "prefer-const", "jsx-button-has-type", "no-explicit-any", "ban-types", "no-window", "no-window-prefix", "no-sloppy-imports" ] } }, "fmt": { "lineWidth": 120, "exclude": [ "**/i18n/translations/**" ] } } ================================================ FILE: lefthook.yml ================================================ commit-msg: commands: lint-commit-msg: run: npx commitlint --edit pre-commit: commands: file-format: priority: 1 run: deno fmt --check rust-format: priority: 2 glob: "**/*.rs" run: cargo fmt -- --check ts-type-check: priority: 3 glob: "**/*.{js,jsx,ts,tsx}" run: npm run type-check js-linter: priority: 5 glob: "**/*.{js,jsx,ts,tsx}" run: deno lint rust-linter: priority: 6 glob: "**/*.rs" run: cargo clippy --all-targets -- -D warnings pre-push: commands: js-test: priority: 1 glob: "**/*.{js,jsx,ts,tsx}" run: npm run test rust-test: priority: 3 glob: "**/*.rs" run: cargo test ================================================ FILE: libs/core/.gitignore ================================================ npm ================================================ FILE: libs/core/Cargo.toml ================================================ [package] name = "seelen-core" version = "2.5.7" edition = "2021" rust-version = "1.89" [lints] workspace = true [dependencies] serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_yaml = { workspace = true } serde_alias = "0.0.2" schemars = { version = "1.2.1", features = ["url2", "uuid1", "chrono04"] } regex = { workspace = true } sys-locale = "0.3.2" uuid = { workspace = true, features = ["v4", "serde"] } ts-rs = { version = "12.0.0", features = [ "no-serde-warnings", "serde-json-impl", "url-impl", "uuid-impl", "chrono-impl", ] } base64 = { workspace = true } url = { workspace = true, features = ["serde"] } grass = { version = "0.13.4", default-features = false, features = ['random'] } num_enum = "0.7.5" chrono = { version = "0.4.43", features = ["serde"] } paste = "1.0.15" [features] gen-binds = [] ================================================ FILE: libs/core/deno.json ================================================ { "name": "@seelen-ui/lib", "version": "2.5.7", "description": "Seelen UI Library for Widgets", "license": "AGPL-3.0-only", "exports": { ".": "./src/lib.ts", "./types": "./gen/types/mod.ts", "./tauri": "./src/re-exports/tauri.ts" }, "tasks": { "build:npm": "deno run -A ./scripts/build_npm.ts", "build:rs": "deno run -A ./scripts/rust_bindings.ts", "build": "deno task build:rs && deno task build:npm" }, "test": { "include": [ "src/**/*.test.ts" ] }, "imports": { "@deno/dnt": "jsr:@deno/dnt@0.42.3", "@seelen-ui/types": "./gen/types/mod.ts", "@std/assert": "jsr:@std/assert@^1.0.19", "@std/encoding": "jsr:@std/encoding@^1.0.10", "@tauri-apps/api": "npm:@tauri-apps/api@^2.10.1", "@tauri-apps/plugin-dialog": "npm:@tauri-apps/plugin-dialog@^2.6.0", "@tauri-apps/plugin-fs": "npm:@tauri-apps/plugin-fs@^2.4.5", "@tauri-apps/plugin-log": "npm:@tauri-apps/plugin-log@^2.8.0", "@tauri-apps/plugin-process": "npm:@tauri-apps/plugin-process@^2.3.1" }, "compilerOptions": { "lib": [ "dom", "dom.iterable", "ES2023" ] }, "fmt": { "lineWidth": 120 }, "lint": { "rules": { "include": [ "explicit-function-return-type" ], "exclude": [ "no-slow-types" ] } }, "publish": { "include": [ "src", "gen", "readme.md" ], "exclude": [ "src/**/*.test.ts" ] }, "exclude": [ "npm" ] } ================================================ FILE: libs/core/mocks/themes/v2.3.0.yml ================================================ id: "@eythaann/variables-test" metadata: displayName: Variables Test description: this theme is just for testing appTargetVersion: [2, 3, 0] settings: - syntax: name: --testing-settings1-1 label: Testing Color Setting 1 initialValue: "#f00" - syntax: name: --testing-settings1-2 label: Testing Color Setting 2 initialValue: "#00f" - syntax: name: --testing-sizes label: Testing Length Setting 3 initialValue: 20 initialValueUnit: px - syntax: name: --testing-number label: Testing Number Setting initialValue: 60 - syntax: name: --testing-string label: Testing User Input initialValue: "hello world" - syntax: name: --testing-url label: Testing User URL Input initialValue: https://google.com ================================================ FILE: libs/core/mocks/themes/v2.3.12.yml ================================================ id: "@eythaann/variables-test" metadata: displayName: Variables Test description: this theme is just for testing appTargetVersion: [2, 3, 12] settings: - syntax: name: --testing-settings label: Testing Color Setting1-1 initialValue: "#f00" - syntax: name: --testing-settings1-2 label: Testing Color Setting 2 initialValue: "#00f" - syntax: name: --testing-sizes label: Testing Length Setting 3 initialValue: 20 initialValueUnit: px - syntax: name: --testing-number label: Testing Number Setting initialValue: 60 - syntax: name: --testing-string label: Testing User Input initialValue: "hello world" - syntax: name: --testing-url label: Testing User URL Input initialValue: https://google.com - group: header: This is a group header items: - syntax: name: --testing-settings2-1 label: Testing Color Setting description: This is a description to test log text in the settings UI initialValue: "#f00" - syntax: name: --testing-settings2-2 label: Testing Color Setting 2 initialValue: "#00f" - syntax: name: --testing-sizes2 label: Testing Length Setting 3 description: This is a description to test log text in the settings UI initialValue: 20 initialValueUnit: px - syntax: name: --testing-number2 label: Testing Number Setting initialValue: 60 - syntax: name: --testing-string2 label: Testing User Input initialValue: "hello world" - syntax: name: --testing-url2 label: Testing User URL Input initialValue: https://google.com - group: header: This is a sub group header items: - syntax: name: --testing-settings2-1 label: Testing Color Setting initialValue: "#f00" - syntax: name: --testing-settings2-2 label: Testing Color Setting 2 initialValue: "#00f" - syntax: name: --testing-sizes2 label: Testing Length Setting 3 tip: This is a tooltip to test, log text in the settings UI here as well as the description below it as well as the label of the setting description: Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua initialValue: 20 initialValueUnit: px - syntax: name: --testing-number2 label: Testing Number Setting description: This is a description to test log text in the settings UI initialValue: 60 options: - 60 - 90 - 120 - 300 - syntax: name: --testing-string2 label: Testing User Input initialValue: "hello world" - syntax: name: --testing-url2 label: Testing User URL Input initialValue: https://google.com ================================================ FILE: libs/core/mod.ts ================================================ // this re-export file is needed as a workaround for a bug in @deno/dnt // // remember usage `madge --circular .\mod.ts` to find circular dependencies export * from "./src/lib.ts"; ================================================ FILE: libs/core/readme.md ================================================

Seelen UI Logo Seelen UI Library

The **Seelen UI Library** is the core library for [Seelen UI](https://github.com/eythaann/seelen-ui), a highly customizable desktop UI. This library provides the necessary tools and types to create and manage widgets, plugins, and themes for the Seelen UI application. It's a hybrid library with a Rust core and TypeScript/Deno bindings, designed for performance and type safety. ## Installation ### TypeScript/Deno You can use the library from JSR or npm. **JSR:** ```sh deno add @seelen-ui/lib ``` **NPM:** ```sh npm install @seelen-ui/lib ``` ## Contributing Contributions are welcome! Please read our [contributing guidelines](contributing.md) to get started. ## License This project is licensed under the AGPL-3.0 License. See the [LICENSE](LICENSE) file for details. ## Links - [GitHub Repository](https://github.com/Seelen-Inc/slu-lib) - [NPM Package](https://npmjs.com/package/@seelen-ui/lib) - [JSR Package](https://jsr.io/@seelen-ui/lib) ================================================ FILE: libs/core/scripts/build_npm.ts ================================================ /// import { build, type BuildOptions, emptyDir } from "@deno/dnt"; import denoJson from "../deno.json" with { type: "json" }; const { name, description, version, license } = denoJson; const packageJson: BuildOptions["package"] = { name, description, version, license, repository: { type: "git", url: "git+https://github.com/eythaann/Seelen-UI.git", }, bugs: { url: "https://github.com/eythaann/Seelen-UI/issues", }, }; await emptyDir("./npm"); // clear previous build await build({ compilerOptions: { lib: ["DOM", "DOM.Iterable", "ESNext"], target: "ES2023", }, test: false, // this is performed by CI typeCheck: false, // this is performed by CI entryPoints: [{ name: ".", path: "./mod.ts", }, { name: "./types", path: "./gen/types/mod.ts", }, { name: "./tauri", path: "./src/re-exports/tauri.ts", }], outDir: "./npm", shims: {}, importMap: "deno.json", package: packageJson, postBuild(): void { Deno.copyFileSync("../../LICENSE", "npm/LICENSE"); Deno.copyFileSync("readme.md", "npm/readme.md"); Deno.removeSync("npm/src", { recursive: true }); }, }); ================================================ FILE: libs/core/scripts/rust_bindings.ts ================================================ /// await Deno.mkdir("./gen/types", { recursive: true }); // await Deno.mkdir('./src/validators', { recursive: true }); const GenTypesPath = await Deno.realPath("./gen/types"); // const GenJsonSchemasPath = await Deno.realPath('./gen/schemas'); // const GenZodSchemasPath = await Deno.realPath('./src/validators'); const libPath = await Deno.realPath("./src/lib.ts"); console.log("[Task] Removing old bindings..."); await Deno.remove(GenTypesPath, { recursive: true }); // await Deno.remove(GenZodSchemasPath, { recursive: true }); // recreate await Deno.mkdir(GenTypesPath, { recursive: true }); // await Deno.mkdir(GenZodSchemasPath, { recursive: true }); { console.log("[Task] Generating Typescript Bindings and JSON Schemas..."); // yeah cargo test generates the typescript bindings, why? ask to @aleph-alpha/ts-rs xd // btw internally we also decided to use tests to avoid having a binary. // also this gill generate the json schemas await new Deno.Command("cargo", { args: ["test", "--features", "gen-binds"], stderr: "inherit", stdout: "inherit", }).output(); } /* { console.log('[Task] Converting JSON Schemas to Zod Schemas...'); for (const file of Deno.readDirSync(GenJsonSchemasPath)) { if (file.isFile && file.name.endsWith('.schema.json')) { const schema = JSON.parse(await Deno.readTextFile(`${GenJsonSchemasPath}/${file.name}`)); const { resolved } = await resolveRefs(schema); const zodCode = jsonSchemaToZod(resolved, { module: 'esm' }); await Deno.writeTextFile(`${GenZodSchemasPath}/${file.name.replace('.schema.json', '.ts')}`, zodCode); } } } */ { console.log("[Task] Creating entry points..."); /* const zodMod = await Deno.open(`${GenZodSchemasPath}/mod.ts`, { create: true, append: true, }); for (const file of Deno.readDirSync(GenZodSchemasPath)) { if (file.isFile && file.name.endsWith('.ts') && file.name !== 'mod.ts') { await zodMod.write( new TextEncoder().encode(`export { default as ${file.name.replace('.ts', '')} } from './${file.name}';\n`), ); } } */ const typesMod = await Deno.open(`${GenTypesPath}/mod.ts`, { create: true, append: true, }); for (const entry of Deno.readDirSync(GenTypesPath)) { if (entry.isFile && entry.name.endsWith(".ts") && entry.name !== "mod.ts") { await typesMod.write( new TextEncoder().encode(`export * from './${entry.name}';\n`), ); } } } { console.log("[Task] Extracting Types Definitions..."); const doc = await new Deno.Command("deno", { args: ["doc", "--json", "--private", `${GenTypesPath}/mod.ts`], stderr: "inherit", stdout: "piped", }).output(); const docJson = JSON.parse(new TextDecoder().decode(doc.stdout)); await Deno.writeTextFile( "./gen/doc-types.json", JSON.stringify(docJson, null, 2), ); } { console.log("[Task] Extracting Library Definitions..."); const doc2 = await new Deno.Command("deno", { args: ["doc", "--json", "--private", libPath], stderr: "inherit", stdout: "piped", }).output(); const docJson2 = JSON.parse(new TextDecoder().decode(doc2.stdout)); await Deno.writeTextFile( "./gen/doc-lib.json", JSON.stringify(docJson2, null, 2), ); } console.log("[Task] Formatting..."); await new Deno.Command("cargo", { args: ["fmt"], stderr: "inherit", stdout: "inherit", }).output(); await new Deno.Command("deno", { args: ["fmt", "--quiet"], stderr: "inherit", stdout: "inherit", }).output(); console.log("[Task] Done!"); ================================================ FILE: libs/core/src/constants/mod.rs ================================================ pub const SUPPORTED_LANGUAGES: &[SupportedLanguage] = &[ lang("Afrikaans", "Afrikaans", "af"), lang("አማርኛ", "Amharic", "am"), lang("العربية", "Arabic", "ar"), lang("Azərbaycan", "Azerbaijani", "az"), lang("Български", "Bulgarian", "bg"), lang("বাংলা", "Bengali", "bn"), lang("Bosanski", "Bosnian", "bs"), lang("Català", "Catalan", "ca"), lang("Čeština", "Czech", "cs"), lang("Cymraeg", "Welsh", "cy"), lang("Dansk", "Danish", "da"), lang("Deutsch", "German", "de"), lang("Ελληνικά", "Greek", "el"), lang("English", "English", "en"), lang("Español", "Spanish", "es"), lang("Eesti", "Estonian", "et"), lang("Euskara", "Basque", "eu"), lang("فارسی", "Persian", "fa"), lang("Suomi", "Finnish", "fi"), lang("Français", "French", "fr"), lang("ગુજરાતી", "Gujarati", "gu"), lang("עברית", "Hebrew", "he"), lang("हिन्दी", "Hindi", "hi"), lang("Hrvatski", "Croatian", "hr"), lang("Magyar", "Hungarian", "hu"), lang("Հայերեն", "Armenian", "hy"), lang("Indonesia", "Indonesian", "id"), lang("Íslenska", "Icelandic", "is"), lang("Italiano", "Italian", "it"), lang("日本語", "Japanese", "ja"), lang("ქართული", "Georgian", "ka"), lang("ភាសាខ្មែរ", "Khmer", "km"), lang("한국어", "Korean", "ko"), lang("Kurdî", "Kurdish", "ku"), lang("Lëtzebuergesch", "Luxembourgish", "lb"), lang("ລາວ", "Lao", "lo"), lang("Lietuvių", "Lithuanian", "lt"), lang("Latviešu", "Latvian", "lv"), lang("Македонски", "Macedonian", "mk"), lang("Монгол", "Mongolian", "mn"), lang("Malay", "Malay", "ms"), lang("Malti", "Maltese", "mt"), lang("नेपाली", "Nepali", "ne"), lang("Nederlands", "Dutch", "nl"), lang("Norsk", "Norwegian", "no"), lang("ਪੰਜਾਬੀ", "Punjabi", "pa"), lang("Polski", "Polish", "pl"), lang("پښتو", "Pashto", "ps"), lang("Português (Brasil)", "Portuguese (Brazil)", "pt-BR"), lang("Português (Portugal)", "Portuguese (Portugal)", "pt-PT"), lang("Română", "Romanian", "ro"), lang("Русский", "Russian", "ru"), lang("සිංහල", "Sinhala", "si"), lang("Slovenský", "Slovak", "sk"), lang("Soomaali", "Somali", "so"), lang("Српски", "Serbian", "sr"), lang("Svenska", "Swedish", "sv"), lang("Kiswahili", "Swahili", "sw"), lang("தமிழ்", "Tamil", "ta"), lang("తెలుగు", "Telugu", "te"), lang("Тоҷикӣ", "Tajik", "tg"), lang("ไทย", "Thai", "th"), lang("Tagalog", "Filipino", "tl"), lang("Türkçe", "Turkish", "tr"), lang("Українська", "Ukrainian", "uk"), lang("اردو", "Urdu", "ur"), lang("Oʻzbek", "Uzbek", "uz"), lang("Tiếng Việt", "Vietnamese", "vi"), lang("Yorùbá", "Yoruba", "yo"), lang("中文 (简体)", "Chinese (Simplified)", "zh-CN"), lang("中文 (繁體)", "Chinese (Traditional)", "zh-TW"), lang("isiZulu", "Zulu", "zu"), ]; pub struct SupportedLanguage { pub label: &'static str, pub en_label: &'static str, pub value: &'static str, } const fn lang( label: &'static str, en_label: &'static str, value: &'static str, ) -> SupportedLanguage { SupportedLanguage { label, en_label, value, } } ================================================ FILE: libs/core/src/constants/mod.ts ================================================ export type SupportedLanguagesCode = (typeof SupportedLanguages)[number]["value"]; export interface SupportedLanguage { label: string; enLabel: string; /** language code @example 'de' 'es' 'zh' 'en-US' 'en-UK' */ value: string; } export const SupportedLanguages = [ { label: "Afrikaans", enLabel: "Afrikaans", value: "af" }, { label: "አማርኛ", enLabel: "Amharic", value: "am" }, { label: "العربية", enLabel: "Arabic", value: "ar" }, { label: "Azərbaycan", enLabel: "Azerbaijani", value: "az" }, { label: "Български", enLabel: "Bulgarian", value: "bg" }, { label: "বাংলা", enLabel: "Bengali", value: "bn" }, { label: "Bosanski", enLabel: "Bosnian", value: "bs" }, { label: "Català", enLabel: "Catalan", value: "ca" }, { label: "Čeština", enLabel: "Czech", value: "cs" }, { label: "Cymraeg", enLabel: "Welsh", value: "cy" }, { label: "Dansk", enLabel: "Danish", value: "da" }, { label: "Deutsch", enLabel: "German", value: "de" }, { label: "Ελληνικά", enLabel: "Greek", value: "el" }, { label: "English", enLabel: "English", value: "en" }, { label: "Español", enLabel: "Spanish", value: "es" }, { label: "Eesti", enLabel: "Estonian", value: "et" }, { label: "Euskara", enLabel: "Basque", value: "eu" }, { label: "فارسی", enLabel: "Persian", value: "fa" }, { label: "Suomi", enLabel: "Finnish", value: "fi" }, { label: "Français", enLabel: "French", value: "fr" }, { label: "ગુજરાતી", enLabel: "Gujarati", value: "gu" }, { label: "עברית", enLabel: "Hebrew", value: "he" }, { label: "हिन्दी", enLabel: "Hindi", value: "hi" }, { label: "Hrvatski", enLabel: "Croatian", value: "hr" }, { label: "Magyar", enLabel: "Hungarian", value: "hu" }, { label: "Հայերեն", enLabel: "Armenian", value: "hy" }, { label: "Indonesia", enLabel: "Indonesian", value: "id" }, { label: "Íslenska", enLabel: "Icelandic", value: "is" }, { label: "Italiano", enLabel: "Italian", value: "it" }, { label: "日本語", enLabel: "Japanese", value: "ja" }, { label: "ქართული", enLabel: "Georgian", value: "ka" }, { label: "ភាសាខ្មែរ", enLabel: "Khmer", value: "km" }, { label: "한국어", enLabel: "Korean", value: "ko" }, { label: "Kurdî", enLabel: "Kurdish", value: "ku" }, { label: "Lëtzebuergesch", enLabel: "Luxembourgish", value: "lb" }, { label: "ລາວ", enLabel: "Lao", value: "lo" }, { label: "Lietuvių", enLabel: "Lithuanian", value: "lt" }, { label: "Latviešu", enLabel: "Latvian", value: "lv" }, { label: "Македонски", enLabel: "Macedonian", value: "mk" }, { label: "Монгол", enLabel: "Mongolian", value: "mn" }, { label: "Malay", enLabel: "Malay", value: "ms" }, { label: "Malti", enLabel: "Maltese", value: "mt" }, { label: "नेपाली", enLabel: "Nepali", value: "ne" }, { label: "Nederlands", enLabel: "Dutch", value: "nl" }, { label: "Norsk", enLabel: "Norwegian", value: "no" }, { label: "ਪੰਜਾਬੀ", enLabel: "Punjabi", value: "pa" }, { label: "Polski", enLabel: "Polish", value: "pl" }, { label: "پښتو", enLabel: "Pashto", value: "ps" }, { label: "Português (Brasil)", enLabel: "Portuguese (Brazil)", value: "pt-BR" }, { label: "Português (Portugal)", enLabel: "Portuguese (Portugal)", value: "pt-PT" }, { label: "Română", enLabel: "Romanian", value: "ro" }, { label: "Русский", enLabel: "Russian", value: "ru" }, { label: "සිංහල", enLabel: "Sinhala", value: "si" }, { label: "Slovenský", enLabel: "Slovak", value: "sk" }, { label: "Soomaali", enLabel: "Somali", value: "so" }, { label: "Српски", enLabel: "Serbian", value: "sr" }, { label: "Svenska", enLabel: "Swedish", value: "sv" }, { label: "Kiswahili", enLabel: "Swahili", value: "sw" }, { label: "தமிழ்", enLabel: "Tamil", value: "ta" }, { label: "తెలుగు", enLabel: "Telugu", value: "te" }, { label: "Тоҷикӣ", enLabel: "Tajik", value: "tg" }, { label: "ไทย", enLabel: "Thai", value: "th" }, { label: "Tagalog", enLabel: "Filipino", value: "tl" }, { label: "Türkçe", enLabel: "Turkish", value: "tr" }, { label: "Українська", enLabel: "Ukrainian", value: "uk" }, { label: "اردو", enLabel: "Urdu", value: "ur" }, { label: "Oʻzbek", enLabel: "Uzbek", value: "uz" }, { label: "Tiếng Việt", enLabel: "Vietnamese", value: "vi" }, { label: "Yorùbá", enLabel: "Yoruba", value: "yo" }, { label: "中文 (简体)", enLabel: "Chinese (Simplified)", value: "zh-CN" }, { label: "中文 (繁體)", enLabel: "Chinese (Traditional)", value: "zh-TW" }, { label: "isiZulu", enLabel: "Zulu", value: "zu" }, ] as const; ================================================ FILE: libs/core/src/error.rs ================================================ macro_rules! define_app_errors { ($( $variant:ident($error_type:ty); )*) => { #[derive(Debug)] pub enum SeelenLibError { $( $variant($error_type), )* } $( impl From<$error_type> for SeelenLibError { fn from(err: $error_type) -> Self { SeelenLibError::$variant(err) } } )* }; } define_app_errors!( Custom(String); Io(std::io::Error); SerdeJson(serde_json::Error); SerdeYaml(serde_yaml::Error); Base64Decode(base64::DecodeError); Grass(Box); ); impl From<&str> for SeelenLibError { fn from(err: &str) -> Self { err.to_owned().into() } } impl std::fmt::Display for SeelenLibError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self:?}") } } pub type Result = std::result::Result; ================================================ FILE: libs/core/src/handlers/commands.rs ================================================ #[cfg(test)] use crate::{ rect::Rect, resource::*, state::by_monitor::MonitorConfiguration, state::by_wallpaper::WallpaperInstanceSettings, state::context_menu::*, state::*, system_state::*, }; #[cfg(test)] use std::{collections::HashMap, path::PathBuf}; macro_rules! slu_commands_declaration { ($($key:ident = $fn_name:ident($($args:tt)*) $(-> $return_type:ty)?,)*) => { #[cfg(test)] pub struct SeelenCommand; #[cfg(test)] impl SeelenCommand { #[cfg(feature = "gen-binds")] pub(crate) fn generate_ts_file(path: &str) { let mut content: Vec = std::vec::Vec::new(); content.push("// This file was generated via rust macros. Don't modify manually.".to_owned()); content.push("export enum SeelenCommand {".to_owned()); $( content.push(format!(" {} = '{}',", stringify!($key), stringify!($fn_name))); )* content.push("}\n".to_owned()); std::fs::write(path, content.join("\n")).unwrap(); } } paste::paste! { $( $crate::__switch! { if { $($args)* } do { #[cfg(test)] #[derive(Deserialize, TS)] #[serde(rename_all = "camelCase")] #[allow(dead_code)] struct [] { $($args)* } } else {} } )* /// Internal used as mapping of commands to their arguments #[cfg(test)] #[allow(non_camel_case_types, dead_code)] #[derive(Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] enum SeelenCommandArgument { $( #[allow(non_snake_case)] $fn_name(Box<$crate::__switch! { if { $($args)* } do { [] } else { () } }>), )* } } /// Internal used as mapping of commands to their return types #[derive(Serialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] #[allow(non_camel_case_types, dead_code)] #[cfg(test)] enum SeelenCommandReturn { $( $fn_name(Box<$crate::__switch! { if { $($return_type)? } do { $($return_type)? } else { () } }>), )* } #[macro_export] macro_rules! command_handler_list { () => { tauri::generate_handler![ $( $fn_name, )* ] }; } pub use command_handler_list; }; } slu_commands_declaration! { // virtual desktops StateGetVirtualDesktops = get_virtual_desktops() -> VirtualDesktops, SwitchWorkspace = switch_workspace(workspace_id: WorkspaceId), CreateWorkspace = create_workspace(monitor_id: MonitorId) -> WorkspaceId, DestroyWorkspace = destroy_workspace(workspace_id: WorkspaceId), RenameWorkspace = rename_workspace(workspace_id: WorkspaceId, name: Option), // wallpaper WallpaperNext = wallpaper_next(), WallpaperPrev = wallpaper_prev(), WallpaperSaveThumbnail = wallpaper_save_thumbnail(wallpaper_id: ResourceId, thumbnail_bytes: Vec), // Logging LogFromWebview = log_from_webview(level: u8, message: String, location: String), // General OpenFile = open_file(path: PathBuf), SelectFileOnExplorer = select_file_on_explorer(path: PathBuf), Run = run(program: PathBuf, args: Option, working_dir: Option, elevated: bool), SimulatePerm = simulate_perm(widget_id: String, perm: String), IsDevMode = is_dev_mode() -> bool, IsAppxPackage = is_appx_package() -> bool, HasFixedRuntime = has_fixed_runtime() -> bool, GetFocusedApp = get_focused_app() -> FocusedApp, GetMousePosition = get_mouse_position() -> [i32; 2], GetKeyState = get_key_state(key: String) -> bool, GetUserEnvs = get_user_envs() -> HashMap, ShowStartMenu = show_start_menu(), GetIcon = get_icon( #[ts(optional = nullable)] path: Option, #[ts(optional = nullable)] umid: Option ), ShowDesktop = show_desktop(), RequestToUserInputShortcut = request_to_user_input_shortcut(callback_event: String), CheckForUpdates = check_for_updates() -> bool, // Restart the app after install the update so it returns a promise resolved with `never` InstallLastAvailableUpdate = install_last_available_update(), // System SystemGetForegroundWindowColor = get_foreground_window_color() -> Color, SystemGetMonitors = get_connected_monitors() -> Vec, SystemGetColors = get_system_colors() -> UIColors, SystemGetLanguages = get_system_languages() -> Vec, SystemSetKeyboardLayout = set_system_keyboard_layout(id: String, handle: String), // Seelen Settings StateGetDefaultSettings = state_get_default_settings() -> Settings, StateGetDefaultMonitorSettings = state_get_default_monitor_settings() -> MonitorConfiguration, StateGetDefaultWallpaperSettings = state_get_default_wallpaper_settings() -> WallpaperInstanceSettings, SetAutoStart = set_auto_start(enabled: bool), GetAutoStartStatus = get_auto_start_status() -> bool, RemoveResource = remove_resource(id: ResourceId, kind: ResourceKind), StateGetThemes = state_get_themes() -> Vec, StateGetWegItems = state_get_weg_items() -> WegItems, StateWriteWegItems = state_write_weg_items(items: WegItems), StateGetToolbarItems = state_get_toolbar_items() -> ToolbarState, StateWriteToolbarItems = state_write_toolbar_items(items: ToolbarState), StateGetSettings = state_get_settings(path: Option) -> Settings, StateWriteSettings = state_write_settings(settings: Settings), StateGetSettingsByApp = state_get_settings_by_app() -> Vec , StateGetPlugins = state_get_plugins() -> Vec, StateGetWidgets = state_get_widgets() -> Vec, StateGetIconPacks = state_get_icon_packs() -> Vec, StateGetWallpapers = state_get_wallpapers() -> Vec, StateSetCustomIconPack = state_add_icon_to_custom_icon_pack(icon: IconPackEntry), StateDeleteCachedIcons = state_delete_cached_icons(), StateRequestWallpaperAddition = state_request_wallpaper_addition(), StateGetPerformanceMode = state_get_performance_mode() -> PerformanceMode, // Widgets TriggerWidget = trigger_widget(payload: WidgetTriggerPayload), TriggerContextMenu = trigger_context_menu(menu: ContextMenu, forward_to: Option), SetCurrentWidgetStatus = set_current_widget_status(status: WidgetStatus), GetSelfWindowId = get_self_window_handle() -> isize, SetSelfPosition = set_self_position(rect: Rect), BringSelfToTop = bring_self_to_top(), WriteFile = write_data_file(filename: String, content: String), ReadFile = read_data_file(filename: String) -> String, // Shell GetNativeShellWallpaper = get_native_shell_wallpaper() -> PathBuf, SetNativeShellWallpaper = set_native_shell_wallpaper(path: PathBuf), // User GetUser = get_user() -> User, GetUserFolderContent = get_user_folder_content(folder_type: FolderType) -> Vec, GetUserAppWindows = get_user_app_windows() -> Vec, GetUserAppWindowsPreviews = get_user_app_windows_previews() -> HashMap, // Media GetMediaDevices = get_media_devices() -> [Vec; 2], GetMediaSessions = get_media_sessions() -> Vec, MediaPrev = media_prev(id: String), MediaTogglePlayPause = media_toggle_play_pause(id: String), MediaNext = media_next(id: String), SetVolumeLevel = set_volume_level(device_id: String, session_id: Option, level: f32), MediaToggleMute = media_toggle_mute(device_id: String, session_id: Option), MediaSetDefaultDevice = media_set_default_device(id: String, role: String), // Brightness - Multi-monitor support GetAllMonitorsBrightness = get_all_monitors_brightness() -> Vec, SetMonitorBrightness = set_monitor_brightness(instance_name: String, level: u8), // Power GetPowerStatus = get_power_status() -> PowerStatus, GetPowerMode = get_power_mode() -> PowerMode, GetBatteries = get_batteries() -> Vec, LogOut = log_out(), Suspend = suspend(), Hibernate = hibernate(), Restart = restart(), Shutdown = shutdown(), Lock = lock(), // SeelenWeg WegCloseApp = weg_close_app(hwnd: isize), WegKillApp = weg_kill_app(hwnd: isize), WegToggleWindowState = weg_toggle_window_state(hwnd: isize, was_focused: bool), WegPinItem = weg_pin_item(path: PathBuf), // Windows Manager WmGetRenderTree = wm_get_render_tree() -> WmRenderTree, SetAppWindowsPositions = set_app_windows_positions(positions: HashMap), RequestFocus = request_focus(hwnd: isize), // Slu Popups CreatePopup = create_popup(config: SluPopupConfig) -> uuid::Uuid, UpdatePopup = update_popup(instance_id: uuid::Uuid, config: SluPopupConfig), ClosePopup = close_popup(instance_id: uuid::Uuid), GetPopupConfig = get_popup_config(instance_id: uuid::Uuid) -> SluPopupConfig, // Network WlanGetProfiles = wlan_get_profiles() -> Vec, WlanStartScanning = wlan_start_scanning(), WlanStopScanning = wlan_stop_scanning(), WlanConnect = wlan_connect(ssid: String, password: Option, hidden: bool) -> bool, WlanDisconnect = wlan_disconnect(), GetNetworkDefaultLocalIp = get_network_default_local_ip() -> String, GetNetworkAdapters = get_network_adapters() -> Vec, GetNetworkInternetConnection = get_network_internet_connection() -> bool, // system tray GetSystemTrayIcons = get_system_tray_icons() -> Vec, SendSystemTrayIconAction = send_system_tray_icon_action(id: SysTrayIconId, action: SystrayIconAction), // Notifications GetNotifications = get_notifications() -> Vec, NotificationsClose = notifications_close(id: u32), NotificationsCloseAll = notifications_close_all(), ActivateNotification = activate_notification( umid: String, args: String, input_data: HashMap, ), // Radios GetRadios = get_radios() -> Vec, SetRadioState = set_radios_state(kind: RadioDeviceKind, enabled: bool), // System Info GetSystemDisks = get_system_disks() -> Vec, GetSystemNetwork = get_system_network() -> Vec, GetSystemMemory = get_system_memory() -> Memory, GetSystemCores = get_system_cores() -> Vec, // Bluetooth GetBluetoothDevices = get_bluetooth_devices() -> Vec, StartBluetoothScanning = start_bluetooth_scanning(), StopBluetoothScanning = stop_bluetooth_scanning(), RequestPairBluetoothDevice = request_pair_bluetooth_device(id: String) -> DevicePairingNeededAction, ConfirmBluetoothDevicePairing = confirm_bluetooth_device_pairing(id: String, answer: DevicePairingAnswer), DisconnectBluetoothDevice = disconnect_bluetooth_device(id: String), ForgetBluetoothDevice = forget_bluetooth_device(id: String), // Start Menu GetStartMenuItems = get_start_menu_items() -> Vec, GetNativeStartMenu = get_native_start_menu() -> StartMenuLayout, // Trash Bin GetTrashBinInfo = get_trash_bin_info() -> TrashBinInfo, TrashBinEmpty = trash_bin_empty(), } ================================================ FILE: libs/core/src/handlers/commands.ts ================================================ // This file was generated via rust macros. Don't modify manually. export enum SeelenCommand { StateGetVirtualDesktops = "get_virtual_desktops", SwitchWorkspace = "switch_workspace", CreateWorkspace = "create_workspace", DestroyWorkspace = "destroy_workspace", RenameWorkspace = "rename_workspace", WallpaperNext = "wallpaper_next", WallpaperPrev = "wallpaper_prev", WallpaperSaveThumbnail = "wallpaper_save_thumbnail", LogFromWebview = "log_from_webview", OpenFile = "open_file", SelectFileOnExplorer = "select_file_on_explorer", Run = "run", SimulatePerm = "simulate_perm", IsDevMode = "is_dev_mode", IsAppxPackage = "is_appx_package", HasFixedRuntime = "has_fixed_runtime", GetFocusedApp = "get_focused_app", GetMousePosition = "get_mouse_position", GetKeyState = "get_key_state", GetUserEnvs = "get_user_envs", ShowStartMenu = "show_start_menu", GetIcon = "get_icon", ShowDesktop = "show_desktop", RequestToUserInputShortcut = "request_to_user_input_shortcut", CheckForUpdates = "check_for_updates", InstallLastAvailableUpdate = "install_last_available_update", SystemGetForegroundWindowColor = "get_foreground_window_color", SystemGetMonitors = "get_connected_monitors", SystemGetColors = "get_system_colors", SystemGetLanguages = "get_system_languages", SystemSetKeyboardLayout = "set_system_keyboard_layout", StateGetDefaultSettings = "state_get_default_settings", StateGetDefaultMonitorSettings = "state_get_default_monitor_settings", StateGetDefaultWallpaperSettings = "state_get_default_wallpaper_settings", SetAutoStart = "set_auto_start", GetAutoStartStatus = "get_auto_start_status", RemoveResource = "remove_resource", StateGetThemes = "state_get_themes", StateGetWegItems = "state_get_weg_items", StateWriteWegItems = "state_write_weg_items", StateGetToolbarItems = "state_get_toolbar_items", StateWriteToolbarItems = "state_write_toolbar_items", StateGetSettings = "state_get_settings", StateWriteSettings = "state_write_settings", StateGetSettingsByApp = "state_get_settings_by_app", StateGetPlugins = "state_get_plugins", StateGetWidgets = "state_get_widgets", StateGetIconPacks = "state_get_icon_packs", StateGetWallpapers = "state_get_wallpapers", StateSetCustomIconPack = "state_add_icon_to_custom_icon_pack", StateDeleteCachedIcons = "state_delete_cached_icons", StateRequestWallpaperAddition = "state_request_wallpaper_addition", StateGetPerformanceMode = "state_get_performance_mode", TriggerWidget = "trigger_widget", TriggerContextMenu = "trigger_context_menu", SetCurrentWidgetStatus = "set_current_widget_status", GetSelfWindowId = "get_self_window_handle", SetSelfPosition = "set_self_position", BringSelfToTop = "bring_self_to_top", WriteFile = "write_data_file", ReadFile = "read_data_file", GetNativeShellWallpaper = "get_native_shell_wallpaper", SetNativeShellWallpaper = "set_native_shell_wallpaper", GetUser = "get_user", GetUserFolderContent = "get_user_folder_content", GetUserAppWindows = "get_user_app_windows", GetUserAppWindowsPreviews = "get_user_app_windows_previews", GetMediaDevices = "get_media_devices", GetMediaSessions = "get_media_sessions", MediaPrev = "media_prev", MediaTogglePlayPause = "media_toggle_play_pause", MediaNext = "media_next", SetVolumeLevel = "set_volume_level", MediaToggleMute = "media_toggle_mute", MediaSetDefaultDevice = "media_set_default_device", GetAllMonitorsBrightness = "get_all_monitors_brightness", SetMonitorBrightness = "set_monitor_brightness", GetPowerStatus = "get_power_status", GetPowerMode = "get_power_mode", GetBatteries = "get_batteries", LogOut = "log_out", Suspend = "suspend", Hibernate = "hibernate", Restart = "restart", Shutdown = "shutdown", Lock = "lock", WegCloseApp = "weg_close_app", WegKillApp = "weg_kill_app", WegToggleWindowState = "weg_toggle_window_state", WegPinItem = "weg_pin_item", WmGetRenderTree = "wm_get_render_tree", SetAppWindowsPositions = "set_app_windows_positions", RequestFocus = "request_focus", CreatePopup = "create_popup", UpdatePopup = "update_popup", ClosePopup = "close_popup", GetPopupConfig = "get_popup_config", WlanGetProfiles = "wlan_get_profiles", WlanStartScanning = "wlan_start_scanning", WlanStopScanning = "wlan_stop_scanning", WlanConnect = "wlan_connect", WlanDisconnect = "wlan_disconnect", GetNetworkDefaultLocalIp = "get_network_default_local_ip", GetNetworkAdapters = "get_network_adapters", GetNetworkInternetConnection = "get_network_internet_connection", GetSystemTrayIcons = "get_system_tray_icons", SendSystemTrayIconAction = "send_system_tray_icon_action", GetNotifications = "get_notifications", NotificationsClose = "notifications_close", NotificationsCloseAll = "notifications_close_all", ActivateNotification = "activate_notification", GetRadios = "get_radios", SetRadioState = "set_radios_state", GetSystemDisks = "get_system_disks", GetSystemNetwork = "get_system_network", GetSystemMemory = "get_system_memory", GetSystemCores = "get_system_cores", GetBluetoothDevices = "get_bluetooth_devices", StartBluetoothScanning = "start_bluetooth_scanning", StopBluetoothScanning = "stop_bluetooth_scanning", RequestPairBluetoothDevice = "request_pair_bluetooth_device", ConfirmBluetoothDevicePairing = "confirm_bluetooth_device_pairing", DisconnectBluetoothDevice = "disconnect_bluetooth_device", ForgetBluetoothDevice = "forget_bluetooth_device", GetStartMenuItems = "get_start_menu_items", GetNativeStartMenu = "get_native_start_menu", GetTrashBinInfo = "get_trash_bin_info", TrashBinEmpty = "trash_bin_empty", } ================================================ FILE: libs/core/src/handlers/events.rs ================================================ use std::collections::HashMap; use crate::state::*; use crate::system_state::*; macro_rules! slu_events_declaration { ($($name:ident$(($payload:ty))? as $value:literal,)*) => { pub struct SeelenEvent; #[allow(non_upper_case_globals)] impl SeelenEvent { $( pub const $name: &'static str = $value; )* #[allow(dead_code)] pub(crate) fn generate_ts_file(path: &str) { let content: Vec = vec![ "// This file was generated via rust macros. Don't modify manually.".to_owned(), "export enum SeelenEvent {".to_owned(), $( format!(" {} = '{}',", stringify!($name), Self::$name), )* "}\n".to_owned(), ]; std::fs::write(path, content.join("\n")).unwrap(); } } #[derive(Serialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub enum SeelenEventPayload { $( #[serde(rename = $value)] $name($crate::__switch! { if { $($payload)? } do { Box<$($payload)?> } else { () } }), )* } }; } slu_events_declaration! { VirtualDesktopsChanged(VirtualDesktops) as "virtual-desktops::changed", GlobalFocusChanged(FocusedApp) as "global-focus-changed", GlobalMouseMove([i32; 2]) as "global-mouse-move", HandleLayeredHitboxes(bool) as "handle-layered", SystemMonitorsChanged(Vec) as "system::monitors-changed", SystemLanguagesChanged(Vec) as "system::languages-changed", SystemMonitorsBrightnessChanged(Vec) as "system::monitors-brightness-changed", UserChanged(User) as "user-changed", UserFolderChanged(FolderChangedArgs) as "user::known-folder-changed", UserAppWindowsChanged(Vec) as "user::windows-changed", UserAppWindowsPreviewsChanged(HashMap) as "user::windows-previews-changed", MediaSessions(Vec) as "media-sessions", MediaDevices([Vec; 2]) as "media::devices", MediaInputs(Vec) as "media-inputs", MediaOutputs(Vec) as "media-outputs", NetworkDefaultLocalIp(String) as "network-default-local-ip", NetworkAdapters(Vec) as "network-adapters", NetworkInternetConnection(bool) as "network-internet-connection", NetworkWlanScanned(Vec) as "wlan-scanned", Notifications(Vec) as "notifications", PowerStatus(PowerStatus) as "power-status", PowerMode(PowerMode) as "power-mode", BatteriesStatus(Vec) as "batteries-status", ColorsChanged(UIColors) as "colors-changed", WMSetReservation as "wm::set-reservation", WMForceRetiling as "wm::force-retiling", WMTreeChanged(WmRenderTree) as "wm::tree-changed", PopupContentChanged(SluPopupConfig) as "popup-content-changed", StateSettingsChanged(Settings) as "settings-changed", StateSettingsByAppChanged(Vec) as "settings-by-app", StateThemesChanged(Vec) as "themes", StateIconPacksChanged(Vec) as "icon-packs", StatePluginsChanged(Vec) as "plugins-changed", StateWidgetsChanged(Vec) as "widgets-changed", StateWallpapersChanged(Vec) as "UserResources::wallpapers-changed", // system tray SystemTrayChanged(Vec) as "system-tray::changed", StatePerformanceModeChanged(PerformanceMode) as "state::performance-mode-changed", WidgetTriggered(WidgetTriggerPayload) as "widget::triggered", // Radios RadiosChanged(Vec) as "radio::changed", // System Info SystemDisksChanged(Vec) as "system::disks-changed", SystemNetworkChanged(Vec) as "system::network-changed", SystemMemoryChanged(Memory) as "system::memory-changed", SystemCoresChanged(Vec) as "system::cores-changed", BluetoothDevicesChanged(Vec) as "bluetooth-devices-changed", // Start Menu StartMenuItemsChanged(Vec) as "start-menu::items-changed", // SeelenWeg WegAddItem(WegItemData) as "weg::add-item", // Trash Bin TrashBinChanged(TrashBinInfo) as "trash-bin::changed", } ================================================ FILE: libs/core/src/handlers/events.ts ================================================ // This file was generated via rust macros. Don't modify manually. export enum SeelenEvent { VirtualDesktopsChanged = "virtual-desktops::changed", GlobalFocusChanged = "global-focus-changed", GlobalMouseMove = "global-mouse-move", HandleLayeredHitboxes = "handle-layered", SystemMonitorsChanged = "system::monitors-changed", SystemLanguagesChanged = "system::languages-changed", SystemMonitorsBrightnessChanged = "system::monitors-brightness-changed", UserChanged = "user-changed", UserFolderChanged = "user::known-folder-changed", UserAppWindowsChanged = "user::windows-changed", UserAppWindowsPreviewsChanged = "user::windows-previews-changed", MediaSessions = "media-sessions", MediaDevices = "media::devices", MediaInputs = "media-inputs", MediaOutputs = "media-outputs", NetworkDefaultLocalIp = "network-default-local-ip", NetworkAdapters = "network-adapters", NetworkInternetConnection = "network-internet-connection", NetworkWlanScanned = "wlan-scanned", Notifications = "notifications", PowerStatus = "power-status", PowerMode = "power-mode", BatteriesStatus = "batteries-status", ColorsChanged = "colors-changed", WMSetReservation = "wm::set-reservation", WMForceRetiling = "wm::force-retiling", WMTreeChanged = "wm::tree-changed", PopupContentChanged = "popup-content-changed", StateSettingsChanged = "settings-changed", StateSettingsByAppChanged = "settings-by-app", StateThemesChanged = "themes", StateIconPacksChanged = "icon-packs", StatePluginsChanged = "plugins-changed", StateWidgetsChanged = "widgets-changed", StateWallpapersChanged = "UserResources::wallpapers-changed", SystemTrayChanged = "system-tray::changed", StatePerformanceModeChanged = "state::performance-mode-changed", WidgetTriggered = "widget::triggered", RadiosChanged = "radio::changed", SystemDisksChanged = "system::disks-changed", SystemNetworkChanged = "system::network-changed", SystemMemoryChanged = "system::memory-changed", SystemCoresChanged = "system::cores-changed", BluetoothDevicesChanged = "bluetooth-devices-changed", StartMenuItemsChanged = "start-menu::items-changed", WegAddItem = "weg::add-item", TrashBinChanged = "trash-bin::changed", } ================================================ FILE: libs/core/src/handlers/mod.rs ================================================ mod commands; mod events; pub use commands::*; pub use events::*; ================================================ FILE: libs/core/src/handlers/mod.ts ================================================ import type { SeelenCommandArgument, SeelenCommandReturn, SeelenEventPayload } from "@seelen-ui/types"; import { invoke as tauriInvoke, type InvokeOptions } from "@tauri-apps/api/core"; import { type EventCallback, listen, type Options as ListenerOptions } from "@tauri-apps/api/event"; import type { SeelenCommand } from "./commands.ts"; import type { SeelenEvent } from "./events.ts"; type $keyof = [Type] extends [never] ? keyof Type : Type extends Type ? keyof Type : never; type UnionToIntersection = { [Key in $keyof]: Extract< Type, { [key in Key]?: unknown; } >[Key]; }; type MapNullToVoid = { [K in keyof Obj]: [Obj[K]] extends [null] ? void : Obj[K]; }; type MapNullToUndefined = { [K in keyof Obj]: [Obj[K]] extends [null] ? undefined : Obj[K]; }; export type AllSeelenCommandArguments = MapNullToUndefined< UnionToIntersection >; export type AllSeelenCommandReturns = MapNullToVoid< UnionToIntersection >; export type AllSeelenEventPayloads = UnionToIntersection; /** * Will call to the background process * @args Command to be called * @args Command arguments * @return Result of the command */ export function invoke( ...args: [AllSeelenCommandArguments[T]] extends [undefined] ? [ command: T, args?: undefined, options?: InvokeOptions, ] : [ command: T, args: AllSeelenCommandArguments[T], options?: InvokeOptions, ] ): Promise { const [command, commandArgs, options] = args; return tauriInvoke(command, commandArgs, options); } export type UnSubscriber = () => void; export function subscribe( event: T, cb: EventCallback, options?: ListenerOptions, ): Promise { return listen(event, cb, options); } export * from "./events.ts"; export * from "./commands.ts"; ================================================ FILE: libs/core/src/lib.rs ================================================ pub mod constants; pub mod error; pub mod handlers; pub mod rect; pub mod resource; pub mod state; pub mod system_state; pub mod utils; pub use chrono; pub use error::SeelenLibError; pub use rect::*; #[macro_use(Serialize, Deserialize)] extern crate serde; #[macro_use(JsonSchema)] extern crate schemars; #[macro_use(TS)] extern crate ts_rs; #[macro_use(FromPrimitive, IntoPrimitive)] extern crate num_enum; #[cfg(feature = "gen-binds")] #[test] fn generate_schemas() { use state::{AppConfig, IconPack, Plugin, Settings, Theme, Widget}; fn write_schema(path: &str) where T: schemars::JsonSchema, { let schema = schemars::schema_for!(T); std::fs::write(path, serde_json::to_string_pretty(&schema).unwrap()).unwrap(); } std::fs::create_dir_all("./gen/schemas").unwrap(); write_schema::("./gen/schemas/settings.schema.json"); write_schema::>("./gen/schemas/settings_by_app.schema.json"); write_schema::("./gen/schemas/theme.schema.json"); write_schema::("./gen/schemas/plugin.schema.json"); write_schema::("./gen/schemas/widget.schema.json"); write_schema::("./gen/schemas/icon_pack.schema.json"); handlers::SeelenEvent::generate_ts_file("./src/handlers/events.ts"); handlers::SeelenCommand::generate_ts_file("./src/handlers/commands.ts"); } ================================================ FILE: libs/core/src/lib.test.ts ================================================ import { assert } from "@std/assert/assert"; import * as SluLib from "./lib.ts"; Deno.test("Library is importable on background (no window)", () => { assert(!!SluLib); }); ================================================ FILE: libs/core/src/lib.ts ================================================ export * from "./constants/mod.ts"; export * from "./state/mod.ts"; export * from "./system_state/mod.ts"; export * from "./utils/mod.ts"; export * from "./resource/mod.ts"; export { type AllSeelenCommandReturns, invoke, SeelenCommand, SeelenEvent, subscribe } from "./handlers/mod.ts"; ================================================ FILE: libs/core/src/re-exports/tauri.ts ================================================ export * from "@tauri-apps/api"; export * from "@tauri-apps/api/dpi"; export * as dialog from "@tauri-apps/plugin-dialog"; export * as fs from "@tauri-apps/plugin-fs"; export * as logger from "@tauri-apps/plugin-log"; export * as process from "@tauri-apps/plugin-process"; ================================================ FILE: libs/core/src/rect.rs ================================================ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ts_rs::TS; #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Rect { pub left: i32, pub top: i32, /// The right edge (exclusive). Pixels at x >= right are outside the rectangle. pub right: i32, /// The bottom edge (exclusive). Pixels at y >= bottom are outside the rectangle. pub bottom: i32, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Frame { pub x: i32, pub y: i32, pub width: u32, pub height: u32, } impl Rect { pub fn as_frame(&self) -> Frame { Frame { x: self.left, y: self.top, width: (self.right - self.left) as u32, height: (self.bottom - self.top) as u32, } } pub fn width(&self) -> i32 { self.right - self.left } pub fn height(&self) -> i32 { self.bottom - self.top } pub fn center(&self) -> Point { Point::new(self.left + self.width() / 2, self.top + self.height() / 2) } pub fn corners(&self) -> [Point; 4] { [ Point::new(self.left, self.top), Point::new(self.right, self.top), Point::new(self.right, self.bottom), Point::new(self.left, self.bottom), ] } pub fn intersection(&self, other: &Rect) -> Option { let left = self.left.max(other.left); let top = self.top.max(other.top); let right = self.right.min(other.right); let bottom = self.bottom.min(other.bottom); if left >= right || top >= bottom { return None; } Some(Rect { left, top, right, bottom, }) } pub fn contains(&self, point: &Point) -> bool { point.x >= self.left && point.x < self.right && point.y >= self.top && point.y < self.bottom } } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Point { pub x: i32, pub y: i32, } impl Point { pub fn new(x: i32, y: i32) -> Self { Self { x, y } } pub fn distance_squared(&self, other: &Point) -> i32 { let dx = self.x.saturating_sub(other.x); let dy = self.y.saturating_sub(other.y); dx.saturating_pow(2).saturating_add(dy.saturating_pow(2)) } pub fn distance(&self, other: &Point) -> f64 { (self.distance_squared(other) as f64).sqrt() } } ================================================ FILE: libs/core/src/resource/file.rs ================================================ use std::{ fs::File, io::{Read, Seek, SeekFrom, Write}, path::Path, }; use base64::Engine; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use ts_rs::TS; use crate::{error::Result, utils::TsUnknown}; use super::Resource; /// A container for Seelen UI resources. /// /// This struct contains all the necessary data that a resource needs. /// It uses a custom `.slu` file extension format that can change over time /// with new versions. #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct SluResourceFile { pub version: u32, /// information about the downloaded resource pub resource: Resource, /// real resource data to be deserialized on load pub data: TsUnknown, } impl SluResourceFile { pub fn decode(mut reader: R) -> Result { let mut version = [0u8; 1]; reader.read_exact(&mut version)?; match version[0] { 1 => { reader.seek(SeekFrom::Current(3))?; // SLU mime type } 2 => { reader.seek(SeekFrom::Current(3))?; // SLU mime type reader.seek(SeekFrom::Current(4))?; // 32 bits reserved } // todo for version 3, use zip crate _ => { return Err("unsupported slu file version".into()); } } // read the rest of the body as content let mut buffer = Vec::new(); reader.read_to_end(&mut buffer)?; let decoded = base64::engine::general_purpose::STANDARD.decode(&buffer)?; Ok(serde_yaml::from_slice(&decoded)?) } pub fn encode(&self, mut writer: W) -> Result<()> { let data = serde_yaml::to_string(self)?; let encoded = base64::engine::general_purpose::STANDARD.encode(data); writer.write_all(&[2])?; // version writer.write_all("SLU".as_bytes())?; // SLU mime type writer.write_all(&[0u8; 4])?; // 32 bits reserved writer.write_all(encoded.as_bytes())?; Ok(()) } pub fn load(path: &Path) -> Result { let file = File::open(path)?; let decoded = Self::decode(&file)?; decoded.resource.verify()?; Ok(decoded) } pub fn store(&self, path: &Path) -> Result<()> { let mut file = File::create(path)?; self.encode(&mut file) } pub fn try_parse_into(&self) -> Result where T: DeserializeOwned, { let mut obj = serde_json::value::Map::new(); obj.insert( "id".to_string(), serde_json::Value::String(self.resource.id.to_string()), ); obj.insert( "metadata".to_string(), serde_json::to_value(&self.resource.metadata)?, ); let data = self.data.0.as_object().ok_or("invalid data")?; obj.append(&mut data.clone()); Ok(serde_json::from_value(obj.into())?) } } ================================================ FILE: libs/core/src/resource/interface.rs ================================================ use std::{fs::File, path::Path}; use serde::{de::DeserializeOwned, Serialize}; use crate::{ error::Result, resource::{deserialize_extended_yaml, ResourceKind, SluResourceFile}, utils::search_resource_entrypoint, }; use super::ResourceMetadata; pub trait SluResource: Sized + Serialize + DeserializeOwned { const KIND: ResourceKind; fn metadata(&self) -> &ResourceMetadata; fn metadata_mut(&mut self) -> &mut ResourceMetadata; /// Try to load the resource from a file.\ /// This won't run post loading processing, please use `load` instead. fn load_from_file(path: &Path) -> Result { let ext = path .extension() .ok_or("Invalid file extension")? .to_ascii_lowercase(); let resource: Self = match ext.to_string_lossy().as_ref() { "yml" | "yaml" => deserialize_extended_yaml(path)?, "json" | "jsonc" => { let file = File::open(path)?; file.lock_shared()?; serde_json::from_reader(file)? } "slu" => { let file = SluResourceFile::load(path)?; if Self::KIND != file.resource.kind { return Err(format!( "Resource file is not of expected kind: {:?} instead is {:?}", Self::KIND, file.resource.kind ) .into()); } let mut parsed: Self = file.try_parse_into()?; parsed.metadata_mut().internal.remote = Some(Box::new(file.resource.clone())); parsed } _ => return Err("Invalid file extension".into()), }; Ok(resource) } /// Try to load the resource from a folder.\ /// This won't run post loading processing, please use `load` instead. fn load_from_folder(path: &Path) -> Result { let file = search_resource_entrypoint(path).ok_or("No metadata file found")?; Self::load_from_file(&file) } /// Try to load the resource from a file or directory.\ /// After deserialization, this will run post loading processing like `sanitize` and `validate`, /// Also will set the internal metadata needed to handle the resource fn load(path: &Path) -> Result { let mut resource = if path.is_dir() { Self::load_from_folder(path)? } else { Self::load_from_file(path)? }; let meta = resource.metadata_mut(); meta.internal.path = path.to_path_buf(); meta.internal.filename = path .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); meta.internal.written_at = path.metadata()?.modified()?.into(); resource.sanitize(); resource.validate()?; Ok(resource) } /// Sanitize the resource data fn sanitize(&mut self) {} /// Validates the resource after sanitization fn validate(&self) -> Result<()> { Ok(()) } /// Saves the resource in same path as it was loaded fn save(&self) -> Result<()> { let mut save_path = self.metadata().internal.path.to_path_buf(); if save_path.is_dir() { std::fs::create_dir_all(&save_path)?; save_path = search_resource_entrypoint(&save_path) .unwrap_or_else(|| save_path.join("metadata.yml")); } let extension = save_path .extension() .ok_or("Invalid path extension")? .to_string_lossy() .to_lowercase(); match extension.as_str() { "slu" => { let mut slu_file = SluResourceFile::load(&save_path)?; slu_file.data = serde_json::to_value(self)?.into(); slu_file.store(&save_path)?; } "yml" | "yaml" => { let file = File::create(save_path)?; serde_yaml::to_writer(file, self)?; } "json" | "jsonc" => { let file = File::create(save_path)?; serde_json::to_writer_pretty(file, self)?; } _ => { return Err("Unsupported path extension".into()); } } Ok(()) } fn delete(&self) -> Result<()> { let path = self.metadata().internal.path.to_path_buf(); if path.is_dir() { std::fs::remove_dir_all(path)?; } else { std::fs::remove_file(path)?; } Ok(()) } } ================================================ FILE: libs/core/src/resource/metadata.rs ================================================ use std::{collections::HashMap, path::PathBuf}; use chrono::{DateTime, Utc}; use url::Url; use crate::{ error::Result, resource::{Resource, ResourceText}, }; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct ResourceMetadata { /// Map of language code to display name. Could be a string, mapped to `en`. pub display_name: ResourceText, /// Map of language code to description. Could be a string, mapped to `en`. pub description: ResourceText, /// Portrait image with aspect ratio of 1/1 pub portrait: Option, /// Banner image with aspect ratio of 21/9, this is used when promoting the resource. pub banner: Option, /// Screenshots should use aspect ratio of 16/9 pub screenshots: Vec, /// tags are keywords to be used for searching and indexing pub tags: Vec, /// App target version that this resource is compatible with.\ /// Developers are responsible to update the resource so when resource does not /// match the current app version, the resource will be shown with a warning message pub app_target_version: Option<(u32, u32, u32)>, /// Extra metadata for the resource pub extras: HashMap, #[serde(flatten, skip_deserializing)] pub internal: InternalResourceMetadata, } impl ResourceMetadata { /// Returns the directory of where the resource is stored pub fn directory(&self) -> Result { Ok(if self.internal.path.is_dir() { self.internal.path.clone() } else { self.internal .path .parent() .ok_or("No Parent")? .to_path_buf() }) } } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct InternalResourceMetadata { pub path: PathBuf, pub filename: String, pub bundled: bool, /// Last date when the metadata file was written pub written_at: DateTime, /// only present for remote/downloaded resources pub remote: Option>, } impl Default for ResourceMetadata { fn default() -> Self { Self { display_name: ResourceText::Localized(HashMap::new()), description: ResourceText::Localized(HashMap::new()), portrait: None, banner: None, screenshots: Vec::new(), tags: Vec::new(), extras: HashMap::new(), app_target_version: None, internal: InternalResourceMetadata::default(), } } } ================================================ FILE: libs/core/src/resource/mod.rs ================================================ mod file; mod interface; mod metadata; mod resource_id; mod yaml_ext; pub use file::*; pub use interface::*; pub use metadata::*; pub use resource_id::*; pub use yaml_ext::*; use std::{ collections::{HashMap, HashSet}, hash::Hash, }; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ts_rs::TS; use uuid::Uuid; use crate::error::Result; // ============================================================================= #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[serde(untagged)] /// Map of language code as key an translated values. Could be a string, mapped to `en`. pub enum ResourceText { En(String), Localized(HashMap), } impl Default for ResourceText { fn default() -> Self { Self::En(String::new()) } } impl ResourceText { const MISSING_TEXT: &'static str = "!?"; /// Returns true if the text exists for the given lang pub fn has(&self, lang: &str) -> bool { match self { ResourceText::En(_) => lang == "en", ResourceText::Localized(map) => map.get(lang).is_some_and(|t| !t.is_empty()), } } /// Returns the text by lang, uses `en` as fallback. /// If no text fallback found will return `!?` pub fn get(&self, lang: &str) -> &str { match self { ResourceText::En(value) => value, ResourceText::Localized(map) => match map.get(lang) { Some(value) => value, None => match map.get("en") { Some(value) => value, None => Self::MISSING_TEXT, }, }, } } pub fn set(&mut self, lang: impl Into, value: impl Into) { if let ResourceText::En(v) = self { let mut dict = HashMap::new(); dict.insert("en".to_string(), v.to_string()); *self = ResourceText::Localized(dict); } if let ResourceText::Localized(dict) = self { dict.insert(lang.into(), value.into()); } } } // ============================================================================= #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum ResourceKind { Theme, IconPack, Widget, Plugin, Wallpaper, SoundPack, } // ============================================================================= #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum ResourceStatus { /// Initial state Draft, /// Waiting for review Reviewing, /// review done and rejected Rejected, /// review done and approved Published, /// soft delete by user Deleted, } // ============================================================================= #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum ResourceAttribute { /// this resource is not working NotWorking, /// this resource is recommended by the staff StaffLiked, } // ============================================================================= /// Represents a resource in the cloud, uploaded by a user #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Resource { /// unique id pub id: Uuid, /// id of the document containing the resource pub data_id: Uuid, /// user id who created the resource pub creator_id: Uuid, /// Visual id composed of creator username and resource name.\ /// Warning: as username and resource name could be changed, this id is not stable.\ /// Use it for display purposes only pub friendly_id: ResourceId, pub kind: ResourceKind, pub metadata: ResourceMetadata, pub created_at: DateTime, pub updated_at: DateTime, /// current status pub status: ResourceStatus, /// if status == ResourceStatus::Rejected, this is the reason for rejection pub rejected_reason: Option, /// date when the resource was reviewed pub reviewed_at: Option>, /// user id who reviewed the resource pub reviewed_by: Option, /// should be filled if `status == ResourceStatus::Deleted` pub deleted_at: Option>, /// resource attributes #[serde(default)] pub attributes: HashSet, /// resource version (increased every time the resource is updated) pub version: u32, /// number of stars pub stars: u32, /// number of downloads pub downloads: u32, } impl Resource { pub fn verify(&self) -> Result<()> { if let ResourceText::Localized(map) = &self.metadata.display_name { if map.get("en").is_none() { return Err("missing mandatory english display name".into()); } } if let ResourceText::Localized(map) = &self.metadata.description { if map.get("en").is_none() { return Err("missing mandatory english description".into()); } } Ok(()) } } ================================================ FILE: libs/core/src/resource/mod.ts ================================================ export {}; ================================================ FILE: libs/core/src/resource/resource_id.rs ================================================ use std::sync::LazyLock; use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; use crate::error::Result; /// For local resources this will be an visual id composed of the creator username and the resource name. e.g. `@username/resource-name` /// For downloaded resources this will be an UUID. #[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema, TS)] #[ts(type = "string & { __brand: 'ResourceId' }")] pub enum ResourceId { Local(String), Remote(uuid::Uuid), } impl ResourceId { fn regex() -> &'static regex::Regex { static REGEX: LazyLock = LazyLock::new(|| { regex::Regex::new(r"^@[a-zA-Z][\w\-]{1,30}[a-zA-Z0-9]\/[a-zA-Z][\w\-]+[a-zA-Z0-9]$") .unwrap() }); ®EX } pub fn is_valid(&self) -> bool { match self { ResourceId::Local(id) => Self::regex().is_match(id), ResourceId::Remote(_) => true, // UUIDs are always valid } } pub fn validate(&self) -> Result<(), String> { if !self.is_valid() { let id_str = match self { ResourceId::Local(id) => id.as_str(), ResourceId::Remote(_) => return Ok(()), // UUIDs are always valid }; return Err(format!( "Invalid resource id ({}), should follow the regex: {}", id_str, Self::regex() )); } Ok(()) } pub fn as_str(&self) -> &str { match self { ResourceId::Local(id) => id.as_str(), ResourceId::Remote(_) => "", } } pub fn starts_with(&self, prefix: &str) -> bool { match self { ResourceId::Local(id) => id.starts_with(prefix), ResourceId::Remote(_) => false, } } /// Creator username of the resource, will be none for remote resources pub fn creator(&self) -> Option { match self { ResourceId::Local(id) => Some( id.split('/') .next() .unwrap() .trim_start_matches('@') .to_string(), ), ResourceId::Remote(_) => None, } } /// Name of the resource, will be none for remote resources pub fn resource_name(&self) -> Option { match self { ResourceId::Local(id) => Some(id.split('/').nth(1).unwrap().to_string()), ResourceId::Remote(_) => None, } } } impl Default for ResourceId { fn default() -> Self { Self::Local("@unknown/unknown".to_owned()) } } impl From<&str> for ResourceId { fn from(value: &str) -> Self { Self::from(value.to_string()) } } impl From for ResourceId { fn from(value: String) -> Self { // Try to parse as UUID first, otherwise treat as local ID if let Ok(uuid) = uuid::Uuid::try_parse(&value) { ResourceId::Remote(uuid) } else { ResourceId::Local(value) } } } impl From for ResourceId { fn from(value: uuid::Uuid) -> Self { ResourceId::Remote(value) } } impl std::fmt::Display for ResourceId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ResourceId::Local(id) => write!(f, "{}", id), ResourceId::Remote(uuid) => write!(f, "{}", uuid), } } } impl Serialize for ResourceId { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { match self { ResourceId::Local(id) => serializer.serialize_str(id), ResourceId::Remote(id) => serializer.serialize_str(&id.to_string()), } } } impl<'de> Deserialize<'de> for ResourceId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct ResourceIdVisitor; impl<'de> Visitor<'de> for ResourceIdVisitor { type Value = ResourceId; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { write!( formatter, "a string matching the resource ID pattern: {}", ResourceId::regex().as_str() ) } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { // this step is to allow deserialize old ids used on older schemas (themes) let id = match value { "toolbar" => WidgetId::known_toolbar().0, "weg" => WidgetId::known_weg().0, "wm" => WidgetId::known_wm().0, "wall" => WidgetId::known_wall().0, "settings" => WidgetId::known_settings().0, "launcher" => "@deprecated/launcher".into(), "popup" => WidgetId::known_popup().0, _ => { // Try to parse as UUID first, otherwise treat as local ID if let Ok(uuid) = uuid::Uuid::try_parse(value) { ResourceId::Remote(uuid) } else { ResourceId::Local(value.to_string()) } } }; id.validate().map_err(serde::de::Error::custom)?; Ok(id) } } deserializer.deserialize_str(ResourceIdVisitor) } } macro_rules! resource_id_variant { ($name:ident) => { /// Visual id composed of the creator username and the resource name. e.g. `@username/resource-name` #[derive( Debug, Clone, Hash, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS, )] pub struct $name(ResourceId); crate::identifier_impl!($name, ResourceId); impl From for $name { fn from(value: ResourceId) -> Self { Self(value) } } }; } resource_id_variant!(PluginId); resource_id_variant!(IconPackId); resource_id_variant!(ThemeId); resource_id_variant!(WidgetId); resource_id_variant!(WallpaperId); impl WidgetId { pub fn known_settings() -> Self { "@seelen/settings".into() } pub fn known_weg() -> Self { "@seelen/weg".into() } pub fn known_toolbar() -> Self { "@seelen/fancy-toolbar".into() } pub fn known_wm() -> Self { "@seelen/window-manager".into() } pub fn known_wall() -> Self { "@seelen/wallpaper-manager".into() } pub fn known_popup() -> Self { "@seelen/popup".into() } } ================================================ FILE: libs/core/src/resource/yaml_ext.rs ================================================ // the idea with this module is improve YAML with extensibility, via custom keywords use std::{fs::File, path::Path}; use serde_yaml::{Mapping, Value}; use crate::error::Result; /// Will deserialize a YAML file and parse the custom extended syntax pub fn deserialize_extended_yaml(path: &Path) -> Result { let value = read_and_parse_yml(path)?; Ok(serde_yaml::from_value(value)?) } fn read_and_parse_yml(path: &Path) -> Result { let file = File::open(path)?; file.lock_shared()?; let base = path.parent().ok_or("No parent directory")?; let value: Value = serde_yaml::from_reader(file)?; parse_yaml(base, value) } fn parse_yaml(base: &Path, value: Value) -> Result { match value { Value::Mapping(map) => { let mut new_map = Mapping::new(); for (key, value) in map { let value = parse_yaml(base, value)?; new_map.insert(key, value); } Ok(Value::Mapping(new_map)) } Value::Sequence(seq) => { let mut new_seq = Vec::new(); for value in seq { let value = parse_yaml(base, value)?; new_seq.push(value); } Ok(Value::Sequence(new_seq)) } Value::Tagged(tag) => { if tag.tag == "!include" { if let Value::String(relative_path) = tag.value { let to_include = base.join(relative_path); let text = if to_include .extension() .is_some_and(|ext| ext == "scss" || ext == "sass") { grass::from_path(&to_include, &grass::Options::default())? } else { std::fs::read_to_string(&to_include)? }; return Ok(Value::String(text)); } } if tag.tag == "!extend" { if let Value::String(relative_path) = tag.value { let value = read_and_parse_yml(&base.join(relative_path))?; return Ok(value); } } Ok(Value::Tagged(tag)) } _ => Ok(value), } } ================================================ FILE: libs/core/src/state/icon_pack.rs ================================================ use std::path::PathBuf; use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::resource::{IconPackId, ResourceKind, ResourceMetadata, SluResource}; #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct IconPack { pub id: IconPackId, #[serde(alias = "info")] pub metadata: ResourceMetadata, /// Special icon used when some other icon is not found pub missing: Option, /// Icons defined in this icon pack pub entries: Vec, /// This lists will be downloaded and stored locally pub remote_entries: Vec, /// Indicates if the icon pack icons was downloaded from `remote_entries` pub downloaded: bool, } impl SluResource for IconPack { const KIND: ResourceKind = ResourceKind::IconPack; fn metadata(&self) -> &ResourceMetadata { &self.metadata } fn metadata_mut(&mut self) -> &mut ResourceMetadata { &mut self.metadata } fn sanitize(&mut self) { self.missing = self.missing.take().filter(|e| e.is_valid()); self.entries.retain(|e| match e { IconPackEntry::Unique(e) => match &e.icon { Some(icon) => icon.is_valid(), None => e.redirect.is_some(), }, IconPackEntry::Shared(e) => e.icon.is_valid(), IconPackEntry::Custom(e) => e.icon.is_valid(), }) } } impl IconPack { /// replace existing entry if found, otherwise add it. pub fn add_entry(&mut self, entry: IconPackEntry) { if let Some(found) = self.find_similar_mut(&entry) { *found = entry; } else { self.entries.push(entry); } } /// search for same entry ignoring the icon. pub fn find_similar_mut(&mut self, entry: &IconPackEntry) -> Option<&mut IconPackEntry> { self.entries .iter_mut() .find(|existing| existing.matches(entry)) } /// search for same entry ignoring the icon. pub fn find_similar(&self, entry: &IconPackEntry) -> Option<&IconPackEntry> { self.entries.iter().find(|existing| existing.matches(entry)) } /// search for same entry ignoring the icon. pub fn contains_similar(&self, entry: &IconPackEntry) -> bool { self.find_similar(entry).is_some() } } /// Key can be user model id, filename or a full path. /// In case of path this should be an executable or a lnk file or any other file that can /// have an unique/individual icon as are the applications, otherwise use `shared`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct UniqueIconPackEntry { /// Application user model id #[serde(skip_serializing_if = "Option::is_none")] pub umid: Option, /// Path or filename of the application, mostly this should be present, /// but cases like PWAs on Edge can have no path and be only an UMID. pub path: Option, /// In case of path be a lnk file this can be set to a different location to use the icon from. /// If present, icon on this entry will be ignored #[serde(skip_serializing_if = "Option::is_none")] pub redirect: Option, #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option, /// Source file modification time for cache invalidation #[serde(skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] pub source_mtime: Option>, } /// Intended to store file icons by extension #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct SharedIconPackEntry { /// File extension without the dot, e.g. "txt" pub extension: String, pub icon: Icon, } /// Here specific/custom icons for widgets can be stored. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct CustomIconPackEntry { /// we recomend following the widget id + icon name to avoid collisions /// e.g. "@username/widgetid::iconname" but you can use whatever you want pub key: String, /// Value is the path to the icon relative to the icon pack folder. pub icon: Icon, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] pub enum IconPackEntry { Unique(UniqueIconPackEntry), Shared(SharedIconPackEntry), Custom(CustomIconPackEntry), } impl IconPackEntry { pub fn matches(&self, entry: &IconPackEntry) -> bool { match (self, entry) { (IconPackEntry::Unique(self_unique), IconPackEntry::Unique(other_unique)) => { self_unique.umid == other_unique.umid && self_unique.path == other_unique.path && self_unique.redirect == other_unique.redirect } (IconPackEntry::Shared(self_shared), IconPackEntry::Shared(other_shared)) => { self_shared.extension == other_shared.extension } (IconPackEntry::Custom(self_custom), IconPackEntry::Custom(other_custom)) => { self_custom.key == other_custom.key } _ => false, } } } /// The icon paths in this structure are relative to the icon pack folder. #[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct Icon { /// Icon to use if no light or dark icon is specified, if both light and dark are specified this can be omitted #[serde(skip_serializing_if = "Option::is_none")] pub base: Option, /// Alternative icon to use when system theme is light #[serde(skip_serializing_if = "Option::is_none")] pub light: Option, /// Alternative icon to use when system theme is dark #[serde(skip_serializing_if = "Option::is_none")] pub dark: Option, /// Mask to be applied over the icon, themes can use this to apply custom colors over the icon. #[serde(skip_serializing_if = "Option::is_none")] pub mask: Option, /// Whether the icon is a square or not #[serde(skip_serializing_if = "is_false")] pub is_aproximately_square: bool, } impl Icon { pub fn is_valid(&self) -> bool { self.base.is_some() || (self.light.is_some() && self.dark.is_some()) } } fn is_false(b: &bool) -> bool { !b } ================================================ FILE: libs/core/src/state/icon_pack.test.ts ================================================ import type { Icon, IconPack, IconPackId, ResourceMetadata } from "@seelen-ui/types"; import { IconPackManager } from "./icon_pack.ts"; import { assertEquals } from "@std/assert"; // Custom assertion for null values function assertNull(value: unknown): void { return assertEquals(value, null); } function onlyBase(x: string): Icon { return { base: x, light: null, dark: null, mask: null, isAproximatelySquare: false, }; } // Constants for test icons const GOT_BY_PATH = "GOT_BY_PATH"; const GOT_BY_FILENAME = "GOT_BY_FILENAME"; const GOT_BY_EXTENSION = "GOT_BY_EXTENSION"; const GOT_BY_UMID = "GOT_BY_UMID"; const A_PATH = "path\\to\\a"; const B_PATH = "path\\to\\b"; const C_PATH = "path\\to\\c"; // Deep clone helper to ensure test isolation const cloneIconPack = (pack: IconPack): IconPack => JSON.parse(JSON.stringify(pack)); // Factory function for mock icon packs const createMockIconPacks = (): { packA: IconPack; packB: IconPack; packC: IconPack; } => ({ packA: { id: "a" as IconPackId, metadata: { path: A_PATH } as ResourceMetadata, missing: onlyBase("MissingIconA.png"), entries: [ // Unique entries (apps) { type: "unique", umid: "MSEdge", redirect: null, path: "C:\\Program Files (x86)\\Microsoft\\Edge\\msedge.exe", icon: onlyBase(GOT_BY_UMID), }, { type: "unique", umid: null, redirect: null, path: "C:\\Windows\\explorer.exe", icon: onlyBase(GOT_BY_PATH), }, { type: "unique", umid: null, redirect: null, path: "C:\\Program Files (x86)\\Some\\App\\filenameApp.exe", icon: onlyBase(GOT_BY_PATH), }, // Shared entries (file extensions) { type: "shared", extension: "txt", icon: onlyBase(GOT_BY_EXTENSION), }, { type: "shared", extension: "png", icon: onlyBase(GOT_BY_EXTENSION), }, { type: "shared", extension: "jpg", icon: onlyBase(GOT_BY_EXTENSION), }, // Custom entries { type: "custom", key: "my-custom-icon", icon: onlyBase("CustomA.png"), }, ], remoteEntries: [], downloaded: false, }, packB: { id: "b" as IconPackId, metadata: { path: B_PATH } as ResourceMetadata, missing: onlyBase("MissingIconB.png"), entries: [ // Unique entries (apps) { type: "unique", umid: null, redirect: null, path: "C:\\Windows\\explorer.exe", icon: onlyBase(GOT_BY_PATH), }, { type: "unique", umid: null, redirect: null, path: "filenameApp.exe", icon: onlyBase(GOT_BY_FILENAME), }, // Shared entries (file extensions) { type: "shared", extension: "txt", icon: onlyBase(GOT_BY_EXTENSION), }, // Custom entries { type: "custom", key: "my-custom-icon", icon: onlyBase("CustomB.png"), }, ], remoteEntries: [], downloaded: false, }, packC: { id: "c" as IconPackId, metadata: { path: C_PATH } as ResourceMetadata, missing: null, entries: [ // Unique entries (apps) { type: "unique", umid: null, path: "C:\\folder\\app1.exe", icon: onlyBase(GOT_BY_PATH), redirect: null, }, ], remoteEntries: [], downloaded: false, }, }); // Test context manager for cleaner test setup class IconPackManagerTestContext { private manager: IconPackManagerMock; // Default active packs: ['b', 'a'] (note 'a' has higher priority as it's last) constructor(initialActives: string[] = ["b", "a"]) { const mocks = createMockIconPacks(); this.manager = new IconPackManagerMock( [ cloneIconPack(mocks.packA), cloneIconPack(mocks.packB), cloneIconPack(mocks.packC), ], initialActives, ); } get instance(): IconPackManagerMock { return this.manager; } // Fluent interface for changing active packs withActives(actives: string[]): this { this.manager.setActives(actives); return this; } } // Mock implementation of IconPackManager for testing class IconPackManagerMock extends IconPackManager { constructor(packs: IconPack[], activeKeys: string[]) { super(packs, activeKeys); this.resolveAvailableIcons(); this.cacheActiveIconPacks(); } public setActives(actives: string[]): void { this._activeIconPackIds = actives; this.cacheActiveIconPacks(); } } Deno.test("IconPackManager", async (t) => { await t.step("Icon lookup functionality", async (t) => { await t.step("should return null for non-existent paths or UMIDs", () => { const ctx = new IconPackManagerTestContext(); // Non-existent path assertNull( ctx.instance.getIconPath({ path: "C:\\nonexistent\\path.exe" }), ); // Non-existent UMID assertNull(ctx.instance.getIconPath({ umid: "NonexistentUMID" })); }); await t.step("should ignore inactive icon packs", () => { // Only 'a' and 'b' are active by default const ctx = new IconPackManagerTestContext(); // This path only exists in packC which is inactive assertNull(ctx.instance.getIconPath({ path: "C:\\folder\\app1.exe" })); }); await t.step( "should respect cascading priority order (last has highest priority)", () => { // Default order is ['b', 'a'] so 'a' has higher priority const ctx = new IconPackManagerTestContext(["b", "a"]); // 'a' should take priority for explorer.exe (last in active list) assertEquals( ctx.instance.getIconPath({ path: "C:\\Windows\\explorer.exe" }), onlyBase(`${A_PATH}\\${GOT_BY_PATH}`), ); // After changing priority to ['a', 'b'], 'b' should now have priority assertEquals( ctx.withActives(["a", "b"]).instance.getIconPath({ path: "C:\\Windows\\explorer.exe", }), onlyBase(`${B_PATH}\\${GOT_BY_PATH}`), ); }, ); await t.step("should prioritize UMID over path matching", () => { const ctx = new IconPackManagerTestContext(["b", "a"]); // 'a' has priority // Should match UMID in packA even though path exists in both packs assertEquals( ctx.instance.getIconPath({ path: "C:\\Program Files (x86)\\Microsoft\\Edge\\msedge.exe", umid: "MSEdge", }), onlyBase(`${A_PATH}\\${GOT_BY_UMID}`), ); }); await t.step( "should match by filename when higher priority pack has filename match", () => { // With ['b', 'a'] order, packB has filename match that should take priority const ctx = new IconPackManagerTestContext(["a", "b"]); assertEquals( ctx.instance.getIconPath({ path: "C:\\Program Files (x86)\\Some\\App\\filenameApp.exe", }), onlyBase(`${B_PATH}\\${GOT_BY_FILENAME}`), ); }, ); await t.step("should match files by extension with priority order", () => { const ctx = new IconPackManagerTestContext(["b", "a"]); // 'a' has priority // .txt exists in both packs - should use 'a' (higher priority) assertEquals( ctx.instance.getIconPath({ path: "C:\\Some\\App\\someFile.txt" }), onlyBase(`${A_PATH}\\${GOT_BY_EXTENSION}`), ); // .png only exists in packA assertEquals( ctx.instance.getIconPath({ path: "C:\\Some\\App\\someFile.png" }), onlyBase(`${A_PATH}\\${GOT_BY_EXTENSION}`), ); // When we change priority to ['a', 'b'], 'b' should have priority for .txt assertEquals( ctx.withActives(["a", "b"]).instance.getIconPath({ path: "C:\\Some\\App\\someFile.txt", }), onlyBase(`${B_PATH}\\${GOT_BY_EXTENSION}`), ); }); }); await t.step("Missing icon functionality", async (t) => { await t.step( "should return missing icon from highest priority pack (last in active list)", () => { // With ['b', 'a'], 'a' has priority const ctx = new IconPackManagerTestContext(["b", "a"]); assertEquals( ctx.instance.getMissingIconPath(), onlyBase(`${A_PATH}\\MissingIconA.png`), ); }, ); await t.step( "should fallback when higher priority pack has no missing icon", () => { // packC has no missing icon, should fallback to packB const ctx = new IconPackManagerTestContext(["c", "b"]); assertEquals( ctx.instance.getMissingIconPath(), onlyBase(`${B_PATH}\\MissingIconB.png`), ); }, ); await t.step( "should return null when no active packs have missing icons", () => { const ctx = new IconPackManagerTestContext(["c"]); // packC has no missing icon assertNull(ctx.instance.getMissingIconPath()); }, ); }); await t.step("Custom icon functionality", async (t) => { await t.step("should return custom icon from highest priority pack", () => { // With ['b', 'a'], 'a' has priority const ctx = new IconPackManagerTestContext(["b", "a"]); assertEquals( ctx.instance.getCustomIconPath("my-custom-icon"), onlyBase(`${A_PATH}\\CustomA.png`), ); }); await t.step( "should fallback when custom icon not found in higher priority pack", () => { // packC has no custom icons, should fallback to packA const ctx = new IconPackManagerTestContext(["c", "a"]); assertEquals( ctx.instance.getCustomIconPath("my-custom-icon"), onlyBase(`${A_PATH}\\CustomA.png`), ); }, ); await t.step( "should return null when custom icon not found in any active pack", () => { const ctx = new IconPackManagerTestContext(["c"]); assertNull(ctx.instance.getCustomIconPath("non-existent-icon")); }, ); }); await t.step("Redirect functionality", async (t) => { await t.step( "should follow redirect and ignore icon when redirect is present", () => { const ctx = new IconPackManagerTestContext(["a", "b"]); // Add entry with both redirect and icon (icon should be ignored) ctx.instance.iconPacks[0].entries.push({ type: "unique", umid: "RedirectTest", path: "C:\\redirect\\source.exe", redirect: "C:\\redirect\\target.exe", icon: onlyBase(A_PATH + "\\ThisShouldBeIgnored.png"), }); // Add target entry to packB ctx.instance.iconPacks[1].entries.push({ type: "unique", umid: null, path: "C:\\redirect\\target.exe", redirect: null, icon: onlyBase(B_PATH + "\\RedirectTargetIcon.png"), }); assertEquals( ctx.instance.getIconPath({ umid: "RedirectTest" }), onlyBase(B_PATH + "\\RedirectTargetIcon.png"), ); }, ); await t.step( "should not use icon when redirect points to non-existent path", () => { const ctx = new IconPackManagerTestContext(["a"]); // Add entry with redirect to non-existent path and icon ctx.instance.iconPacks[0].entries.push({ type: "unique", umid: "BadRedirect", path: "C:\\redirect\\source.exe", redirect: "C:\\nonexistent\\path.exe", icon: onlyBase(A_PATH + "\\ShouldNotUseThis.png"), // <-- should be ignored inclusively if redirect points to non-existent path }); assertNull(ctx.instance.getIconPath({ umid: "BadRedirect" })); }, ); await t.step( "should follow redirect chain until icon is found or no more redirects", () => { const ctx = new IconPackManagerTestContext(["a", "b", "c"]); // First redirect (icon should be ignored) ctx.instance.iconPacks[0].entries.push({ type: "unique", umid: "ChainRedirect", path: "C:\\redirect\\start.exe", redirect: "C:\\redirect\\middle.exe", icon: onlyBase(A_PATH + "\\IgnoreThis.png"), }); // Second redirect (no icon) ctx.instance.iconPacks[1].entries.push({ type: "unique", umid: null, path: "C:\\redirect\\middle.exe", redirect: "C:\\redirect\\final.exe", icon: null, }); // Final target with icon ctx.instance.iconPacks[2].entries.push({ type: "unique", umid: null, path: "C:\\redirect\\final.exe", redirect: null, icon: onlyBase(C_PATH + "\\FinalIcon.png"), }); assertEquals( ctx.instance.getIconPath({ umid: "ChainRedirect" }), onlyBase(C_PATH + "\\FinalIcon.png"), ); }, ); await t.step( "should return null if redirect chain ends without finding an icon", () => { const ctx = new IconPackManagerTestContext(["a", "b"]); // First redirect ctx.instance.iconPacks[0].entries.push({ type: "unique", umid: "BrokenChain", path: "C:\\redirect\\start.exe", redirect: "C:\\redirect\\missing.exe", icon: onlyBase(A_PATH + "\\IgnoreMe.png"), }); // No matching entry for the redirect target assertNull(ctx.instance.getIconPath({ umid: "BrokenChain" })); }, ); await t.step("should handle redirect to extension match", () => { const ctx = new IconPackManagerTestContext(["a", "b"]); // Redirect to a file with specific extension ctx.instance.iconPacks[0].entries.push({ type: "unique", umid: "RedirectToExtension", path: "C:\\some\\app.exe", redirect: "C:\\some\\file.txt", // <-- redirect to txt file icon: onlyBase(A_PATH + "\\WrongIcon.png"), // <-- should be ignored }); assertEquals( ctx.instance.getIconPath({ umid: "RedirectToExtension" }), onlyBase(B_PATH + "\\GOT_BY_EXTENSION"), ); }); }); await t.step("should handle circular references and return null", () => { const ctx = new IconPackManagerTestContext(["a", "b"]); ctx.instance.iconPacks[0].entries.push({ type: "unique", umid: "CircularRef1", path: "C:\\circle\\app1.exe", redirect: "C:\\circle\\app2.exe", icon: onlyBase("IgnoredIcon1.png"), }); ctx.instance.iconPacks[1].entries.push({ type: "unique", umid: "CircularRef2", path: "C:\\circle\\app2.exe", redirect: "C:\\circle\\app1.exe", icon: onlyBase("IgnoredIcon2.png"), }); assertNull(ctx.instance.getIconPath({ umid: "CircularRef1" })); assertNull(ctx.instance.getIconPath({ path: "C:\\circle\\app1.exe" })); assertNull(ctx.instance.getIconPath({ path: "C:\\circle\\app2.exe" })); }); await t.step("should detect self-references and return null", () => { const ctx = new IconPackManagerTestContext(["a"]); // Configurar autorreferencia ctx.instance.iconPacks[0].entries.push({ type: "unique", umid: "SelfRef", path: "C:\\circle\\self.exe", redirect: "C:\\circle\\self.exe", // Se redirige a sí mismo icon: onlyBase("IgnoredIcon.png"), }); assertNull(ctx.instance.getIconPath({ umid: "SelfRef" })); }); await t.step( "should detect longer circular references and return null", () => { const ctx = new IconPackManagerTestContext(["a", "b", "c"]); // Configurar referencia circular más larga (A -> B -> C -> A) ctx.instance.iconPacks[0].entries.push({ type: "unique", umid: null, path: "C:\\circle\\a.exe", redirect: "C:\\circle\\b.exe", icon: onlyBase("IgnoredA.png"), }); ctx.instance.iconPacks[1].entries.push({ type: "unique", umid: null, path: "C:\\circle\\b.exe", redirect: "C:\\circle\\c.exe", icon: onlyBase("IgnoredB.png"), }); ctx.instance.iconPacks[2].entries.push({ type: "unique", umid: null, path: "C:\\circle\\c.exe", redirect: "C:\\circle\\a.exe", icon: onlyBase("IgnoredC.png"), }); assertNull(ctx.instance.getIconPath({ path: "C:\\circle\\a.exe" })); }, ); }); ================================================ FILE: libs/core/src/state/icon_pack.ts ================================================ import type { Icon as IIcon, IconPack, IconPack as IIconPack, SeelenCommandGetIconArgs, UniqueIconPackEntry, } from "@seelen-ui/types"; import { List } from "../utils/List.ts"; import { newFromInvoke, newOnEvent } from "../utils/State.ts"; import { invoke, SeelenCommand, SeelenEvent, type UnSubscriber } from "../handlers/mod.ts"; import { Settings } from "./settings/mod.ts"; import type { UnlistenFn } from "@tauri-apps/api/event"; import { convertFileSrc } from "@tauri-apps/api/core"; export class IconPackList extends List { static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.StateGetIconPacks); } static onChange(cb: (payload: IconPackList) => void): Promise { return newOnEvent(cb, this, SeelenEvent.StateIconPacksChanged); } } /** * Class helper to allow easy use of icon packs */ export class IconPackManager { private callbacks: Set<() => void> = new Set(); private unlisteners: UnlistenFn[] = []; private isListeningForChanges = false; /// list of active icon packs and fully resolved paths private activeIconPacks: IconPack[] = []; protected constructor( protected _availableIconPacks: IconPack[], protected _activeIconPackIds: string[], ) {} get iconPacks(): IconPack[] { return this._availableIconPacks; } get activeIconPackIds(): string[] { return this._activeIconPackIds; } protected resolveAvailableIcons(): void { for (const pack of this._availableIconPacks) { const path = `${pack.metadata.path}`; if (pack.missing) { pack.missing = resolveIcon(path, pack.missing); } for (const entry of pack.entries) { if (entry.type === "unique") { entry.path = entry.path?.toLowerCase() || null; } if (entry.type === "shared") { entry.extension = entry.extension.toLowerCase(); } if (entry.icon) { entry.icon = resolveIcon(path, entry.icon); } } } } protected cacheActiveIconPacks(): void { this.activeIconPacks = []; for (const key of this._activeIconPackIds.toReversed()) { const pack = this._availableIconPacks.find((p) => p.id === key); if (pack) { this.activeIconPacks.push(pack); } } } /** * Creates an instance of IconPackManager. This intance will be updated when * the list of icon packs or the settings changes, so just having one global instance is enough. * * @returns A new instance of IconPackManager */ public static async create(): Promise { const instance = new IconPackManager( (await IconPackList.getAsync()).asArray(), (await Settings.getAsync()).inner.activeIconPacks, ); instance.resolveAvailableIcons(); instance.cacheActiveIconPacks(); return instance; } /** * Registers a callback to be invoked when the list of active icon packs changes. * This method also sets up listeners to detect changes in the icon pack list and * the active icon packs settings. If no callbacks are registered beforehand, the * listeners are initialized. When no callbacks remain registered, the listeners are stopped. * * @param {() => void} cb - The callback to be invoked when the list of active icon packs changes. * This callback takes no arguments and returns no value. * @returns {Promise} A promise that resolves to an `UnlistenFn` function. When invoked, * this function unregisters the callback and stops listening for changes * if no other callbacks are registered. * * @example * const manager = await IconPackManager.create(); * const unlisten = await manager.onChange(() => { * console.log("Icon packs changed: ", manager.actives); * }); * * // Later, to stop listening for changes: * unlisten(); * * @remarks * - The `this` context inside the callback refers to the `IconPackManager` instance, provided the callback * is not rebound to another context (e.g., using `bind`, `call`, or `apply`). * - If the callback is defined as an arrow function, `this` will be lexically bound to the surrounding context. * - If the callback is a regular function, ensure it is bound correctly to avoid `this` being `undefined` (in strict mode) * or the global object (in non-strict mode). * * @see {@link IconPackManager} for the class this method belongs to. * @see {@link UnlistenFn} for the type of the function returned by this method. */ public async onChange(cb: () => void): Promise { this.callbacks.add(cb); if (!this.isListeningForChanges) { this.isListeningForChanges = true; const unlistenerIcons = await IconPackList.onChange((list) => { this._availableIconPacks = JSON.parse(JSON.stringify(list.all())); this.resolveAvailableIcons(); this.cacheActiveIconPacks(); this.callbacks.forEach((cb) => cb()); }); const unlistenerSettings = await Settings.onChange((settings) => { this._activeIconPackIds = settings.inner.activeIconPacks; this.cacheActiveIconPacks(); this.callbacks.forEach((cb) => cb()); }); this.unlisteners = [unlistenerIcons, unlistenerSettings]; } return () => { this.callbacks.delete(cb); if (this.callbacks.size === 0) { this.unlisteners.forEach((unlisten) => unlisten()); this.unlisteners = []; this.isListeningForChanges = false; } }; } /** * Returns the icon path for an app or file. If no icon is available, returns `null`. * * The search for icons follows this priority order: * 1. UMID (App User Model Id) * 2. Full path * 3. Filename (this is only used to match executable files like .exe) * 4. Extension * * @param {Object} args - Arguments for retrieving the icon path. * @param {string} [args.path] - The full path to the app or file. * @param {string} [args.umid] - The UMID of the app. * @returns {string | null} - The path to the icon, or `null` if no icon is found. * * @example * // Example 1: Get icon by full path * const iconPath = instance.getIconPath({ * path: "C:\\Program Files\\Steam\\steam.exe" * }); * * // Example 2: Get icon by UMID * const iconPath = instance.getIconPath({ * umid: "Seelen.SeelenUI_p6yyn03m1894e!App" * }); */ public getIconPath(args: SeelenCommandGetIconArgs): IIcon | null { const { path, umid, __seen = new Set() } = args as & SeelenCommandGetIconArgs & { __seen?: Set }; // If neither path nor UMID is provided, return null if (!path && !umid) { return null; } const lowerPath = path?.toLowerCase(); // Extract extension only when there's an actual dot after the last path separator, // to avoid treating directory names without dots as extensions. const lastDot = lowerPath?.lastIndexOf("."); const lastSlash = lowerPath?.lastIndexOf("\\") ?? -1; const extension = (lastDot !== undefined && lastDot > lastSlash) ? lowerPath!.slice(lastDot + 1) : undefined; // Add the starting path to __seen so that direct A→B→A cycles are caught in 2 hops. if (lowerPath) { __seen.add(lowerPath); } for (const pack of this.activeIconPacks) { let entry: UniqueIconPackEntry | undefined; if (umid) { entry = pack.entries.find((e) => e.type === "unique" && !!e.umid && e.umid === umid) as UniqueIconPackEntry; } if (!entry && lowerPath) { entry = pack.entries.find((e) => { if (e.type !== "unique" || !e.path) { return false; } if (e.path === lowerPath) { return true; } // only search for filename in case of executable files if (extension === "exe") { const filename = lowerPath.split("\\").pop(); // Use separator-aware check to avoid partial name matches (e.g. "mysteam.exe" != "steam.exe") if (filename && (e.path === filename || e.path.endsWith("\\" + filename))) { return true; } } return false; }) as UniqueIconPackEntry; } if (entry) { if (entry.redirect) { // break circular references if (__seen.has(entry.redirect)) { return null; } __seen.add(entry.redirect); return this.getIconPath( { path: entry.redirect, __seen } as SeelenCommandGetIconArgs, ); } if (entry.icon) { return entry.icon; } } } // search by file extension if (!extension) { return null; } for (const pack of this.activeIconPacks) { const icon = pack.entries.find((e) => { return e.type === "shared" && e.extension === extension; }); if (icon) { return icon.icon; } } // If no icon is found in any icon pack, return null return null; } public getIcon({ path, umid }: SeelenCommandGetIconArgs): IIcon | null { const iconPath = this.getIconPath({ path, umid }); return iconPath ? resolveAsSrc(iconPath) : null; } /** * Will return the special missing icon path from the highest priority icon pack. * If no icon pack haves a missing icon, will return null. */ public getMissingIconPath(): IIcon | null { for (const pack of this.activeIconPacks) { if (pack.missing) { return pack.missing; } } return null; } /** * Will return the special missing icon SRC from the highest priority icon pack. * If no icon pack haves a missing icon, will return null. */ public getMissingIcon(): IIcon | null { const iconPath = this.getMissingIconPath(); return iconPath ? resolveAsSrc(iconPath) : null; } /** * Will return the specific icon path from the highest priority icon pack. * If no icon pack haves the searched icon, will return null. */ public getCustomIconPath(name: string): IIcon | null { for (const pack of this.activeIconPacks) { const entry = pack.entries.find((e) => e.type === "custom" && e.key === name); if (entry) { return entry.icon; } } return null; } /** * Will return the specific icon SRC from the highest priority icon pack. * If no icon pack haves the searched icon, will return null. */ public getCustomIcon(name: string): IIcon | null { const iconPath = this.getCustomIconPath(name); return iconPath ? resolveAsSrc(iconPath) : null; } /** * This method doesn't take in care icon packs, just extracts the inherited icon into system's icon pack * if it's not already there. * * @param filePath The path to the app could be umid o full path * @example * const iconPath = instance.extractIcon({ * path: "C:\\Program Files\\Steam\\steam.exe" * }); * const iconPath = instance.extractIcon({ * umid: "Seelen.SeelenUI_p6yyn03m1894e!App" * }); */ public static requestIconExtraction( obj: SeelenCommandGetIconArgs, ): Promise { return invoke(SeelenCommand.GetIcon, obj); } /** * This will delete all stored icons on the system icon pack.\ * All icons should be regenerated after calling this method. */ public static clearCachedIcons(): Promise { return invoke(SeelenCommand.StateDeleteCachedIcons); } } function resolveIcon(parent: string, icon: IIcon): IIcon { return { base: icon.base ? `${parent}\\${icon.base}` : null, light: icon.light ? `${parent}\\${icon.light}` : null, dark: icon.dark ? `${parent}\\${icon.dark}` : null, mask: icon.mask ? `${parent}\\${icon.mask}` : null, isAproximatelySquare: icon.isAproximatelySquare, }; } function resolveAsSrc(icon: IIcon): IIcon { return { base: icon.base ? convertFileSrc(icon.base) : null, light: icon.light ? convertFileSrc(icon.light) : null, dark: icon.dark ? convertFileSrc(icon.dark) : null, mask: icon.mask ? convertFileSrc(icon.mask) : null, isAproximatelySquare: icon.isAproximatelySquare, }; } ================================================ FILE: libs/core/src/state/mod.rs ================================================ mod icon_pack; mod placeholder; mod plugin; mod popups; mod settings; mod theme; mod wallpaper; mod weg_items; mod widget; mod wm_layout; mod workspaces; pub use icon_pack::*; pub use placeholder::*; pub use plugin::*; pub use popups::*; pub use settings::*; pub use theme::*; pub use wallpaper::*; pub use weg_items::*; pub use widget::*; pub use wm_layout::*; pub use workspaces::*; ================================================ FILE: libs/core/src/state/mod.ts ================================================ export * from "./theme/mod.ts"; export * from "./settings/mod.ts"; export * from "./icon_pack.ts"; export * from "./plugin/mod.ts"; export * from "./widget/mod.ts"; export * from "./wallpaper/mod.ts"; ================================================ FILE: libs/core/src/state/placeholder.rs ================================================ use std::collections::{HashMap, HashSet}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ts_rs::TS; use url::Url; use crate::{resource::PluginId, utils::TsUnknown}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum ToolbarJsScope { Date, Notifications, Media, Network, Keyboard, User, Bluetooth, Power, FocusedApp, Workspaces, Disk, NetworkStatistics, Memory, Cpu, } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct ToolbarItem { /// Id to identify the item, should be unique. Preferably a uuid. pub id: String, /// List of scopes to be loaded in the item js execution scope. pub scopes: HashSet, /// JS function definition for content to display in the item. pub template: String, /// JS function definition for content to display in tooltip of the item. pub tooltip: Option, /// JS function definition badge content, will be displayed over the item, useful as notifications. pub badge: Option, /// JS function definition that will be executed when the item is clicked. #[serde(alias = "onClickV2")] pub on_click: Option, /// Styles to be added to the item. This follow the same interface of React's `style` prop. pub style: HashMap>, /// Remote data to be added to the item scope. pub remote_data: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct RemoteDataDeclaration { url: Url, request_init: Option, update_interval_seconds: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(untagged)] pub enum StyleValue { String(String), Number(serde_json::Number), } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(repr(enum = name))] pub enum WorkspaceToolbarItemMode { #[default] Dotted, Named, Numbered, } impl ToolbarItem { pub fn id(&self) -> String { self.id.clone() } pub fn set_id(&mut self, id: String) { self.id = id; } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(untagged)] pub enum ToolbarItem2 { Plugin(PluginId), Inline(Box), } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct ToolbarState { /// Whether the reordering possible on the toolbar pub is_reorder_disabled: bool, /// Items to be displayed in the toolbar pub left: Vec, /// Items to be displayed in the toolbar pub center: Vec, /// Items to be displayed in the toolbar pub right: Vec, } impl ToolbarState { fn migrate_plugin_id(id: PluginId) -> PluginId { match id.as_str() { "@default/system-tray" => "@seelen/tb-system-tray".into(), "@default/quick-settings" => "@seelen/tb-quick-settings".into(), "@default/bluetooth" => "@seelen/tb-bluetooth-popup".into(), "@default/keyboard" => "@seelen/tb-keyboard-selector".into(), "@default/user" => "@seelen/tb-user-menu".into(), "@default/network" => "@seelen/tb-network-popup".into(), "@default/date" => "@seelen/tb-calendar-popup".into(), "@default/media" => "@seelen/tb-media-popup".into(), "@default/notifications" => "@seelen/tb-notifications".into(), _ => id, } } fn sanitize_items(dict: &mut HashSet, items: Vec) -> Vec { let mut result = Vec::new(); for item in items { match item { ToolbarItem2::Plugin(id) => { let id = Self::migrate_plugin_id(id); let str_id = id.to_string(); if !dict.contains(&str_id) && id.is_valid() { dict.insert(str_id); result.push(ToolbarItem2::Plugin(id)); } } ToolbarItem2::Inline(mut item) => { // migration step for old default separator before v2.5 if item.template.contains("window") && item.scopes.is_empty() { item.scopes.insert(ToolbarJsScope::FocusedApp); item.template = item.template.replace("window", "focusedApp"); } if item.id().is_empty() { item.set_id(uuid::Uuid::new_v4().to_string()); } if !dict.contains(&item.id()) { dict.insert(item.id()); result.push(ToolbarItem2::Inline(item)); } } } } result } pub fn sanitize(&mut self) { let mut dict = HashSet::new(); self.left = Self::sanitize_items(&mut dict, std::mem::take(&mut self.left)); self.center = Self::sanitize_items(&mut dict, std::mem::take(&mut self.center)); self.right = Self::sanitize_items(&mut dict, std::mem::take(&mut self.right)); } } ================================================ FILE: libs/core/src/state/plugin/mod.rs ================================================ pub mod value; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ts_rs::TS; use value::PluginValue; use crate::resource::{PluginId, ResourceKind, ResourceMetadata, SluResource}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Plugin { pub id: PluginId, #[serde(default)] pub metadata: ResourceMetadata, /// Optional icon to be used on settings. This have to be a valid react icon name.\ /// You can find all icons here: https://react-icons.github.io/react-icons/. #[serde(default = "Plugin::default_icon")] pub icon: String, #[serde(flatten)] pub plugin: PluginValue, } impl SluResource for Plugin { const KIND: ResourceKind = ResourceKind::Plugin; fn metadata(&self) -> &ResourceMetadata { &self.metadata } fn metadata_mut(&mut self) -> &mut ResourceMetadata { &mut self.metadata } } impl Plugin { pub fn default_icon() -> String { "PiPuzzlePieceDuotone".to_string() } } ================================================ FILE: libs/core/src/state/plugin/mod.ts ================================================ import { SeelenCommand, SeelenEvent, type UnSubscriber } from "../../handlers/mod.ts"; import { List } from "../../utils/List.ts"; import { newFromInvoke, newOnEvent } from "../../utils/State.ts"; import type { Plugin } from "@seelen-ui/types"; import { Widget } from "../widget/mod.ts"; export class PluginList extends List { static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.StateGetPlugins); } static onChange(cb: (payload: PluginList) => void): Promise { return newOnEvent(cb, this, SeelenEvent.StatePluginsChanged); } forCurrentWidget(): Plugin[] { const target = Widget.self.id; return this.inner.filter((plugin) => plugin.target === target); } } ================================================ FILE: libs/core/src/state/plugin/value.rs ================================================ use schemars::JsonSchema; use crate::{ resource::WidgetId, state::{ToolbarItem, WindowManagerLayout}, utils::TsUnknown, }; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(tag = "target", content = "plugin")] pub enum KnownPlugin { #[serde(rename = "@seelen/fancy-toolbar")] FacyToolbar(ToolbarItem), #[serde(rename = "@seelen/window-manager")] WManager(WindowManagerLayout), } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] pub struct ThirdPartyPlugin { /// The friendly id of the widget that will use this plugin /// example: `@username/widget-name` target: WidgetId, /// The plugin data, this can be anything and depends on the widget using this plugin /// to handle it, parse it and use it. plugin: TsUnknown, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(untagged)] pub enum PluginValue { Known(Box), Any(ThirdPartyPlugin), } ================================================ FILE: libs/core/src/state/popups/mod.rs ================================================ use std::collections::HashMap; use url::Url; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct SluPopupConfig { pub width: f64, pub height: f64, pub title: Vec, pub content: Vec, pub footer: Vec, } impl Default for SluPopupConfig { fn default() -> Self { Self { width: 400.0, height: 200.0, title: vec![], content: vec![], footer: vec![], } } } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde( tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase" )] pub enum SluPopupContent { Text { value: String, styles: Option, }, Icon { /// react icon name. ex: `FaGithub` name: String, styles: Option, }, Image { href: Url, styles: Option, }, Button { inner: Vec, styles: Option, /// event name to be emitted on click ex: `test::clicked` on_click: String, }, Group { items: Vec, styles: Option, }, } impl SluPopupContent { pub fn set_styles(&mut self, new_styles: CssStyles) { match self { SluPopupContent::Text { styles, .. } => *styles = Some(new_styles), SluPopupContent::Icon { styles, .. } => *styles = Some(new_styles), SluPopupContent::Image { styles, .. } => *styles = Some(new_styles), SluPopupContent::Button { styles, .. } => *styles = Some(new_styles), SluPopupContent::Group { styles, .. } => *styles = Some(new_styles), } } } #[derive(Debug, Default, Clone, Serialize, Deserialize, TS)] pub struct CssStyles(HashMap); impl CssStyles { pub fn new() -> Self { Self::default() } pub fn add(mut self, key: &str, value: &str) -> Self { self.0.insert(key.to_string(), value.to_string()); self } } ================================================ FILE: libs/core/src/state/settings/by_monitor.rs ================================================ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use ts_rs::TS; use crate::{ resource::WidgetId, state::{by_widget::ThirdPartyWidgetSettings, WorkspaceId}, }; #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] pub struct MonitorSettingsByWidget(HashMap); impl MonitorSettingsByWidget { pub fn is_widget_enabled(&self, widget_id: &WidgetId) -> bool { self.0 .get(widget_id) .is_none_or(|settings| settings.enabled) } pub fn remove(&mut self, widget_id: &WidgetId) -> Option { self.0.remove(widget_id) } pub fn insert(&mut self, widget_id: WidgetId, settings: ThirdPartyWidgetSettings) { self.0.insert(widget_id, settings); } } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct MonitorConfiguration { /// dictionary of settings by widget pub by_widget: MonitorSettingsByWidget, /// Id of the wallpaper collection to use in this monitor.\ /// If not set, the default wallpaper collection will be used. pub wallpaper_collection: Option, /// dictionary of settings by workspace on this monitor pub by_workspace: HashMap, } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WorkspaceConfiguration { /// Id of the wallpaper collection to use in this workspace.\ /// If not set, the monitor's wallpaper collection will be used. pub wallpaper_collection: Option, } ================================================ FILE: libs/core/src/state/settings/by_theme.rs ================================================ use std::collections::HashMap; use crate::state::config::CssVariableName; #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] pub struct ThemeSettings(HashMap); ================================================ FILE: libs/core/src/state/settings/by_wallpaper.rs ================================================ #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WallpaperInstanceSettings { /// playback speed for video backgrounds pub playback_speed: PlaybackSpeed, /// will flip the image/video vertically pub flip_vertical: bool, /// will flip the image/video horizontally pub flip_horizontal: bool, /// blur factor to apply to the image pub blur: u32, /// method to fill the monitor background pub object_fit: ObjectFit, /// position of the background pub object_position: ObjectPosition, /// number between 0 and 2 pub saturation: f32, /// number between 0 and 2 pub contrast: f32, /// will overlay the image/video with a color filter pub with_overlay: bool, pub overlay_mix_blend_mode: MixBlendMode, pub overlay_color: String, /// mute video backgrounds pub muted: bool, } impl Default for WallpaperInstanceSettings { fn default() -> Self { Self { playback_speed: PlaybackSpeed::default(), flip_vertical: false, flip_horizontal: false, blur: 0, object_fit: ObjectFit::default(), object_position: ObjectPosition::default(), saturation: 1.0, contrast: 1.0, with_overlay: false, overlay_mix_blend_mode: MixBlendMode::default(), overlay_color: "#ff0000".to_string(), muted: true, } } } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(repr(enum = name))] pub enum ObjectFit { Fill, Contain, #[default] Cover, } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(repr(enum = name))] pub enum ObjectPosition { Top, #[default] Center, Bottom, Left, Right, } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "kebab-case")] #[ts(repr(enum = name))] pub enum MixBlendMode { Normal, #[default] Multiply, Screen, Overlay, Darken, Lighten, ColorDodge, ColorBurn, HardLight, SoftLight, Difference, Exclusion, Hue, Saturation, Color, Luminosity, PlusDarker, PlusLighter, } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(repr(enum = name))] pub enum PlaybackSpeed { XDot25, XDot5, XDot75, #[default] X1, X1Dot25, X1Dot5, X1Dot75, X2, } ================================================ FILE: libs/core/src/state/settings/by_widget.rs ================================================ use std::collections::HashMap; use schemars::JsonSchema; use uuid::Uuid; use crate::{resource::WidgetId, utils::TsUnknown}; use super::{FancyToolbarSettings, SeelenWallSettings, SeelenWegSettings, WindowManagerSettings}; #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default)] pub struct SettingsByWidget { #[serde(rename = "@seelen/weg")] pub weg: SeelenWegSettings, #[serde(rename = "@seelen/fancy-toolbar")] pub fancy_toolbar: FancyToolbarSettings, #[serde(rename = "@seelen/window-manager")] pub wm: WindowManagerSettings, #[serde(rename = "@seelen/wallpaper-manager")] pub wall: SeelenWallSettings, #[serde(flatten)] pub others: HashMap, } impl SettingsByWidget { pub fn is_enabled(&self, widget_id: &WidgetId) -> bool { match widget_id.as_str() { "@seelen/weg" => self.weg.enabled, "@seelen/fancy-toolbar" => self.fancy_toolbar.enabled, "@seelen/window-manager" => self.wm.enabled, "@seelen/wallpaper-manager" => self.wall.enabled, _ => match self.others.get(widget_id) { Some(settings) => settings.enabled, // only official widgets are enabled by default None => widget_id.starts_with("@seelen"), }, } } pub fn set_enabled(&mut self, widget_id: &WidgetId, enabled: bool) { match widget_id.as_str() { "@seelen/weg" => self.weg.enabled = enabled, "@seelen/fancy-toolbar" => self.fancy_toolbar.enabled = enabled, "@seelen/window-manager" => self.wm.enabled = enabled, "@seelen/wallpaper-manager" => self.wall.enabled = enabled, _ => match self.others.entry(widget_id.clone()) { std::collections::hash_map::Entry::Occupied(mut o) => { o.get_mut().enabled = enabled; } std::collections::hash_map::Entry::Vacant(v) => { v.insert(ThirdPartyWidgetSettings { enabled, ..Default::default() }); } }, } } } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default)] pub struct ThirdPartyWidgetSettings { /// Enable or disable the widget pub enabled: bool, /// By intance will be used to store settings in case of multiple instances allowed on widget.\ /// The map values will be merged with the root object and default values on settings declaration. #[serde(rename = "$instances")] #[serde(skip_serializing_if = "Option::is_none")] #[ts(optional = nullable)] pub instances: Option>>, #[serde(flatten)] pub rest: HashMap, } ================================================ FILE: libs/core/src/state/settings/mod.rs ================================================ /* In this file we use #[serde_alias(SnakeCase)] as backward compatibility from versions below v1.9.8 */ pub mod by_monitor; pub mod by_theme; pub mod by_wallpaper; pub mod by_widget; pub mod settings_by_app; pub mod shortcuts; pub use settings_by_app::*; use std::collections::{HashMap, HashSet}; use std::fs::File; use std::io::Write; use std::path::Path; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_alias::serde_alias; use ts_rs::TS; use crate::resource::WidgetId; use crate::state::WallpaperCollection; use crate::system_state::MonitorId; use crate::{ error::Result, rect::Rect, resource::{IconPackId, PluginId, ThemeId, WallpaperId}, state::{ by_monitor::MonitorConfiguration, by_theme::ThemeSettings, by_wallpaper::WallpaperInstanceSettings, by_widget::SettingsByWidget, shortcuts::SluShortcutsSettings, }, }; // ============== Fancy Toolbar Settings ============== #[serde_alias(SnakeCase)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct FancyToolbarSettings { /// enable or disable the fancy toolbar pub enabled: bool, /// item size in px pub item_size: u32, /// Toolbar margin in px pub margin: u32, /// Toolbar padding in px pub padding: u32, /// position of the toolbar pub position: FancyToolbarSide, /// hide mode pub hide_mode: HideMode, /// delay to show the toolbar on Mouse Hover in milliseconds pub delay_to_show: u32, /// delay to hide the toolbar on Mouse Leave in milliseconds pub delay_to_hide: u32, } impl Default for FancyToolbarSettings { fn default() -> Self { Self { enabled: true, item_size: 16, padding: 8, margin: 0, position: FancyToolbarSide::Top, hide_mode: HideMode::Never, delay_to_show: 100, delay_to_hide: 800, } } } impl FancyToolbarSettings { /// total height of the toolbar pub fn total_size(&self) -> u32 { self.item_size + (self.padding * 2) + (self.margin * 2) } } #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum FancyToolbarSide { Top, Bottom, } // ============== SeelenWeg Settings ============== #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum SeelenWegMode { #[serde(alias = "Full-Width")] FullWidth, #[serde(alias = "Min-Content")] MinContent, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WegTemporalItemsVisibility { All, OnMonitor, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WegPinnedItemsVisibility { Always, WhenPrimary, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum HideMode { /// never hide Never, /// auto-hide always on Always, /// auto-hide only if is overlaped by the focused window #[serde(alias = "On-Overlap")] OnOverlap, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum SeelenWegSide { Left, Right, Top, Bottom, } #[serde_alias(SnakeCase)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct SeelenWegSettings { /// enable or disable the seelenweg pub enabled: bool, /// Dock/Taskbar mode pub mode: SeelenWegMode, /// When to hide the dock pub hide_mode: HideMode, /// Split windows into separated items instead of grouped. pub split_windows: bool, /// Which temporal items to show on the dock instance (this can be overridden per monitor) pub temporal_items_visibility: WegTemporalItemsVisibility, /// Determines is the pinned item should be shown or not (this can be overridden per monitor). pub pinned_items_visibility: WegPinnedItemsVisibility, /// Dock position pub position: SeelenWegSide, /// enable or disable the instance counter visibility on weg instance pub show_instance_counter: bool, /// enable or disable the window title visibility for opened apps pub show_window_title: bool, /// enable or disable separators visibility pub visible_separators: bool, /// item size in px pub size: u32, /// zoomed item size in px pub zoom_size: u32, /// Dock/Taskbar margin in px pub margin: u32, /// Dock/Taskbar padding in px pub padding: u32, /// space between items in px pub space_between_items: u32, /// delay to show the toolbar on Mouse Hover in milliseconds pub delay_to_show: u32, /// delay to hide the toolbar on Mouse Leave in milliseconds pub delay_to_hide: u32, /// show end task button on context menu (needs developer mode enabled) pub show_end_task: bool, } impl Default for SeelenWegSettings { fn default() -> Self { Self { enabled: true, mode: SeelenWegMode::MinContent, hide_mode: HideMode::OnOverlap, position: SeelenWegSide::Bottom, visible_separators: true, show_instance_counter: true, show_window_title: false, temporal_items_visibility: WegTemporalItemsVisibility::All, pinned_items_visibility: WegPinnedItemsVisibility::Always, size: 40, zoom_size: 70, margin: 8, padding: 8, space_between_items: 8, delay_to_show: 100, delay_to_hide: 800, show_end_task: false, split_windows: false, } } } impl SeelenWegSettings { /// total height or width of the dock, depending on the Position pub fn total_size(&self) -> u32 { self.size + (self.padding * 2) + (self.margin * 2) } } // ============== Window Manager Settings ============== #[serde_alias(SnakeCase)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WindowManagerSettings { /// enable or disable the tiling window manager pub enabled: bool, /// enable or disable auto stacking by category pub auto_stacking_by_category: bool, /// window manager border pub border: Border, /// the resize size in % to be used when resizing via cli pub resize_delta: f32, /// default gap between containers pub workspace_gap: u32, /// default workspace padding pub workspace_padding: u32, /// default workspace margin pub workspace_margin: Rect, /// floating window settings pub floating: FloatingWindowSettings, /// default layout pub default_layout: PluginId, /// window manager animations pub animations: WmAnimations, /// window manager drag behavior pub drag_behavior: WmDragBehavior, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WmDragBehavior { /// While dragging the windows on the layout will be sorted. Sort, /// On drag end the dragged and the overlaped will be swapped. Swap, } #[serde_alias(SnakeCase)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct Border { pub enabled: bool, pub width: f64, pub offset: f64, } #[serde_alias(SnakeCase)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct FloatingWindowSettings { pub width: f64, pub height: f64, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WmAnimations { pub enabled: bool, pub duration_ms: u64, pub ease_function: String, } impl Default for WmAnimations { fn default() -> Self { Self { enabled: true, duration_ms: 200, ease_function: "EaseOut".into(), } } } impl Default for Border { fn default() -> Self { Self { enabled: true, offset: 0.0, width: 3.0, } } } impl Default for FloatingWindowSettings { fn default() -> Self { Self { width: 800.0, height: 500.0, } } } impl Default for WindowManagerSettings { fn default() -> Self { Self { enabled: false, auto_stacking_by_category: true, border: Border::default(), resize_delta: 10.0, workspace_gap: 10, workspace_padding: 10, workspace_margin: Rect::default(), floating: FloatingWindowSettings::default(), default_layout: "@default/wm-bspwm".into(), animations: WmAnimations::default(), drag_behavior: WmDragBehavior::Sort, } } } // ================= Seelen Wall ================ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum MultimonitorBehaviour { /// Each monitor has its own wallpaper PerMonitor, /// Single wallpaper extended across all monitors Extend, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct SeelenWallSettings { pub enabled: bool, /// update interval in seconds pub interval: u32, /// randomize order pub randomize: bool, /// collection id, if none default wallpaper will be used pub default_collection: Option, /// multimonitor behaviour pub multimonitor_behaviour: MultimonitorBehaviour, /// deprecated, this field will be removed on v3 #[serde(alias = "backgroundsV2")] pub deprecated_bgs: Option>, } impl Default for SeelenWallSettings { fn default() -> Self { Self { enabled: true, interval: 300, // 5min randomize: false, default_collection: None, multimonitor_behaviour: MultimonitorBehaviour::PerMonitor, deprecated_bgs: None, } } } // ========================== Seelen Updates ============================== #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum UpdateChannel { Release, Beta, Nightly, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct UpdaterSettings { pub channel: UpdateChannel, } impl Default for UpdaterSettings { fn default() -> Self { Self { channel: UpdateChannel::Release, } } } // ========================== Start of Week ============================== #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] #[derive(Default)] pub enum StartOfWeek { #[default] Monday, Sunday, Saturday, } // ======================== Final Settings Struct =============================== #[serde_alias(SnakeCase)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Settings { pub by_app: AppsConfigurationList, /// list of monitors and their configurations pub monitors_v3: HashMap, /// app shortcuts settings pub shortcuts: SluShortcutsSettings, /// list of selected themes as filename as backguard compatibility for versions before v2.3.8, will be removed in v3 #[serde(alias = "selectedThemes")] pub old_active_themes: Vec, /// list of selected themes pub active_themes: Vec, /// list of selected icon packs pub active_icon_packs: Vec, /// enable or disable dev tools tab in settings pub dev_tools: bool, /// discord rich presence pub drpc: bool, /// language to use, if null the system locale is used pub language: Option, /// MomentJS date format pub date_format: String, /// Start of week for calendar pub start_of_week: StartOfWeek, /// Updater Settings pub updater: UpdaterSettings, /// Custom settings for widgets pub by_widget: SettingsByWidget, /// Custom variables for themes by theme id /// ### example /// ```json /// { /// "@username/themeName": { /// "--css-variable-name": "123px", /// "--css-variable-name2": "#aabbccaa", /// } /// } /// ``` pub by_theme: HashMap, /// settings for each background pub by_wallpaper: HashMap, /// list of wallpaper collections pub wallpaper_collections: Vec, /// Performance options pub performance_mode: PerformanceModeSettings, /// enable or disable hardware acceleration pub hardware_acceleration: bool, /// interval to poll for system resources like cpu, memory, network usage, etc, in seconds. pub polling_interval: u64, } impl Default for Settings { fn default() -> Self { Self { by_app: AppsConfigurationList::default(), performance_mode: PerformanceModeSettings::default(), shortcuts: SluShortcutsSettings::default(), drpc: false, old_active_themes: Vec::new(), active_themes: vec!["@default/theme".into()], active_icon_packs: vec!["@system/icon-pack".into()], monitors_v3: HashMap::new(), dev_tools: false, language: Some(Self::get_system_language()), date_format: "ddd D MMM, hh:mm A".to_owned(), start_of_week: StartOfWeek::default(), updater: UpdaterSettings::default(), by_widget: SettingsByWidget::default(), by_theme: HashMap::new(), by_wallpaper: HashMap::new(), wallpaper_collections: Vec::new(), hardware_acceleration: true, polling_interval: 3, } } } impl Settings { pub fn get_locale() -> Option { sys_locale::get_locale() } pub fn get_system_language() -> String { match sys_locale::get_locale() { Some(l) => l.split('-').next().unwrap_or("en").to_string(), None => "en".to_string(), } } pub fn migrate(&mut self) -> Result<()> { // Migrate step for v2.5 if let Some(backgrounds) = self.by_widget.wall.deprecated_bgs.take() { if !backgrounds.is_empty() { let collection = WallpaperCollection { id: uuid::Uuid::new_v4(), name: "Migrated".to_string(), wallpapers: backgrounds, }; // Set as default collection if no default is set if self.by_widget.wall.default_collection.is_none() { self.by_widget.wall.default_collection = Some(collection.id); } self.wallpaper_collections.push(collection); } } Ok(()) } pub fn dedup_themes(&mut self) { let mut seen = HashSet::new(); self.active_themes.retain(|x| seen.insert(x.clone())); // dedup } pub fn dedup_icon_packs(&mut self) { let mut seen = HashSet::new(); self.active_icon_packs.retain(|x| seen.insert(x.clone())); // dedup } pub fn sanitize(&mut self) -> Result<()> { if self.language.is_none() { self.language = Some(Self::get_system_language()); } // ensure base is always selected self.active_themes.insert(0, "@default/theme".into()); self.dedup_themes(); // ensure base is always selected self.active_icon_packs.insert(0, "@system/icon-pack".into()); self.dedup_icon_packs(); self.shortcuts.sanitize(); self.by_app.prepare(); self.polling_interval = self.polling_interval.max(1); Ok(()) } pub fn load(path: impl AsRef) -> Result { let path = path.as_ref(); let mut settings: Self = { let file = File::open(path)?; file.lock_shared()?; serde_json::from_reader(&file)? }; // Load shortcuts from sibling file if it exists if let (Some(parent), Some(stem)) = (path.parent(), path.file_stem()) { let path = parent.join(format!("{}_shortcuts.json", stem.to_string_lossy())); if path.exists() { let file = File::open(&path)?; file.lock_shared()?; settings.shortcuts = serde_json::from_reader(&file)?; } let path = parent.join(format!("{}_by_app.yml", stem.to_string_lossy())); if path.exists() { let file = File::open(&path)?; file.lock_shared()?; settings.by_app = serde_yaml::from_reader(&file)?; } } settings.migrate()?; settings.sanitize()?; Ok(settings) } pub fn save(&self, path: impl AsRef) -> Result<()> { let path = path.as_ref(); { // Create a copy without splitted fields let mut settings_copy = serde_json::to_value(self)?; let obj = settings_copy.as_object_mut().unwrap(); obj.remove("shortcuts"); obj.remove("byApp"); let mut file = File::create(path)?; file.lock()?; serde_json::to_writer_pretty(&file, &settings_copy)?; file.flush()?; } // Save shortcuts to sibling file if let (Some(parent), Some(stem)) = (path.parent(), path.file_stem()) { let shortcuts_path = parent.join(format!("{}_shortcuts.json", stem.to_string_lossy())); let mut shortcuts_file = File::create(&shortcuts_path)?; shortcuts_file.lock()?; serde_json::to_writer_pretty(&shortcuts_file, &self.shortcuts)?; shortcuts_file.flush()?; let by_app_path = parent.join(format!("{}_by_app.yml", stem.to_string_lossy())); let mut by_app_file = File::create(&by_app_path)?; by_app_file.lock()?; serde_yaml::to_writer(&by_app_file, &self.by_app)?; by_app_file.flush()?; } Ok(()) } /// This indicates if the widget is enabled on general, doesn't take in care multi-instances pub fn is_widget_enabled(&self, widget_id: &WidgetId) -> bool { self.by_widget.is_enabled(widget_id) } pub fn set_widget_enabled(&mut self, widget_id: &WidgetId, enabled: bool) { self.by_widget.set_enabled(widget_id, enabled); } pub fn is_widget_enabled_on_monitor( &self, widget_id: &WidgetId, monitor_id: &MonitorId, ) -> bool { if !self.is_widget_enabled(widget_id) { return false; } // default to true as new connected monitors should be enabled self.monitors_v3 .get(monitor_id) .is_none_or(|monitor_config| monitor_config.by_widget.is_widget_enabled(widget_id)) } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct PerformanceModeSettings { pub default: PerformanceMode, pub on_battery: PerformanceMode, pub on_energy_saver: PerformanceMode, } impl Default for PerformanceModeSettings { fn default() -> Self { Self { default: PerformanceMode::Disabled, on_battery: PerformanceMode::Minimal, on_energy_saver: PerformanceMode::Extreme, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum PerformanceMode { /// Does nothing, all animations are enabled. Disabled, /// Disables windows animations and other heavy effects. Minimal, /// Disables all the animations. Extreme, } ================================================ FILE: libs/core/src/state/settings/mod.ts ================================================ import { SeelenCommand, SeelenEvent, type UnSubscriber } from "../../handlers/mod.ts"; import type { Settings as ISettings, ThirdPartyWidgetSettings } from "@seelen-ui/types"; import { newFromInvoke, newOnEvent } from "../../utils/State.ts"; import { invoke } from "../../handlers/mod.ts"; import { Widget } from "../widget/mod.ts"; export interface Settings extends ISettings {} export class Settings { constructor(public inner: ISettings) { Object.assign(this, this.inner); } static default(): Promise { return newFromInvoke(this, SeelenCommand.StateGetDefaultSettings); } static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.StateGetSettings); } static onChange(cb: (payload: Settings) => void): Promise { return newOnEvent(cb, this, SeelenEvent.StateSettingsChanged); } static loadCustom(path: string): Promise { return newFromInvoke(this, SeelenCommand.StateGetSettings, { path }); } /** * Returns the settings for the current widget, taking in care of the replicas * the returned object will be a merge of: * - the default settings set on the widget definition * - the stored user settings * - the instance patch settings (if apply) * - the monitor patch settings (if apply) */ getCurrentWidgetConfig(): ThirdPartyWidgetSettings { const currentWidget = Widget.getCurrent(); const widgetId = currentWidget.id; const { monitorId, instanceId } = currentWidget.decoded; const root = this.inner.byWidget[widgetId]; const instance = instanceId ? root?.$instances?.[instanceId] : undefined; const monitor = monitorId ? this.inner.monitorsV3[monitorId]?.byWidget[widgetId] : undefined; return { ...currentWidget.getDefaultConfig(), ...(root || {}), ...(instance || {}), ...(monitor || {}), }; } /** Will store the settings on disk */ save(): Promise { return invoke(SeelenCommand.StateWriteSettings, { settings: this.inner }); } } ================================================ FILE: libs/core/src/state/settings/settings_by_app.rs ================================================ use regex::Regex; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_alias::serde_alias; use ts_rs::TS; #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum AppExtraFlag { /// Mark this app as non interactive window. #[serde(alias = "no-interactive")] NoInteractive, /// Start the app in the center of the screen as floating in the wm. #[serde(alias = "float", alias = "wm-float")] WmFloat, /// Forces the management of this app in the wm. (only if it is interactable and not pinned) #[serde(alias = "force", alias = "wm-force")] WmForce, /// Unmanage this app in the wm. #[serde(alias = "unmanage", alias = "wm-unmanage")] WmUnmanage, /// Pin this app in all the virtual desktops in the wm. #[serde(alias = "pinned", alias = "vd-pinned")] VdPinned, #[serde(other)] Unknown, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum AppIdentifierType { #[serde(alias = "exe")] Exe, #[serde(alias = "class")] Class, #[serde(alias = "title")] Title, #[serde(alias = "path")] Path, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum MatchingStrategy { #[serde(alias = "equals", alias = "legacy", alias = "Legacy")] Equals, #[serde(alias = "startsWith")] StartsWith, #[serde(alias = "endsWith")] EndsWith, #[serde(alias = "contains")] Contains, #[serde(alias = "regex")] Regex, } #[serde_alias(SnakeCase)] #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct AppIdentifier { /// Depending of the kind this can be case sensitive or not. /// - `class` and `title` are case sensitive /// - `exe` and `path` are case insensitive pub id: String, /// the way to match the application pub kind: AppIdentifierType, /// the strategy to use to determine if id matches with the application pub matching_strategy: MatchingStrategy, #[serde(default)] pub negation: bool, #[serde(default)] pub and: Vec, #[serde(default)] pub or: Vec, #[serde(skip)] cache: AppIdentifierCache, } /// this struct is intented to improve performance #[derive(Debug, Default, Clone)] pub struct AppIdentifierCache { pub regex: Option, pub lower_id: Option, } impl AppIdentifier { fn prepare(&mut self) { if matches!(self.matching_strategy, MatchingStrategy::Regex) { let result = Regex::new(&self.id); if let Ok(re) = result { self.cache.regex = Some(re); } } if matches!(self.kind, AppIdentifierType::Path | AppIdentifierType::Exe) { // Normalize path separators to backslash and uppercase for Windows paths let normalized = self.id.replace('\\', "/"); self.cache.lower_id = Some(normalized.to_lowercase()); } self.and.iter_mut().for_each(|i| i.prepare()); self.or.iter_mut().for_each(|i| i.prepare()); } fn lower_id(&self) -> &str { self.cache.lower_id.as_deref().unwrap() } /// path and filenames on Windows System should be uppercased before be passed to this function /// Safety: will panic if cache was not performed before fn validate(&self, title: &str, class: &str, exe: &str, path: &str) -> bool { let mut self_result = match self.matching_strategy { MatchingStrategy::Equals => match self.kind { AppIdentifierType::Title => title.eq(&self.id), AppIdentifierType::Class => class.eq(&self.id), AppIdentifierType::Exe => exe.eq(self.lower_id()), AppIdentifierType::Path => path.eq(self.lower_id()), }, MatchingStrategy::StartsWith => match self.kind { AppIdentifierType::Title => title.starts_with(&self.id), AppIdentifierType::Class => class.starts_with(&self.id), AppIdentifierType::Exe => exe.starts_with(self.lower_id()), AppIdentifierType::Path => path.starts_with(self.lower_id()), }, MatchingStrategy::EndsWith => match self.kind { AppIdentifierType::Title => title.ends_with(&self.id), AppIdentifierType::Class => class.ends_with(&self.id), AppIdentifierType::Exe => exe.ends_with(self.lower_id()), AppIdentifierType::Path => path.ends_with(self.lower_id()), }, MatchingStrategy::Contains => match self.kind { AppIdentifierType::Title => title.contains(&self.id), AppIdentifierType::Class => class.contains(&self.id), AppIdentifierType::Exe => exe.contains(self.lower_id()), AppIdentifierType::Path => path.contains(self.lower_id()), }, MatchingStrategy::Regex => match &self.cache.regex { Some(regex) => match self.kind { AppIdentifierType::Title => regex.is_match(title), AppIdentifierType::Class => regex.is_match(class), AppIdentifierType::Exe => regex.is_match(exe), AppIdentifierType::Path => regex.is_match(path), }, None => false, }, }; if self.negation { self_result = !self_result; } (self_result && { self.and .iter() .all(|and| and.validate(title, class, exe, path)) }) || { self.or .iter() .any(|or| or.validate(title, class, exe, path)) } } } #[serde_alias(SnakeCase)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct AppConfig { /// name of the app pub name: String, /// category to group the app under pub category: Option, /// monitor index that the app should be bound to pub bound_monitor: Option, /// workspace index that the app should be bound to pub bound_workspace: Option, /// app identifier pub identifier: AppIdentifier, /// extra specific options/settings for the app #[serde(default)] pub options: Vec, /// is this config bundled with seelen ui. #[serde(skip_deserializing, skip_serializing_if = "AppConfig::is_false")] pub is_bundled: bool, } impl AppConfig { pub fn prepare(&mut self) { self.identifier.prepare(); } fn is_false(b: &bool) -> bool { !b } } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] pub struct AppsConfigurationList(Vec); impl AppsConfigurationList { pub fn prepare(&mut self) { self.0.iter_mut().for_each(|config| config.prepare()); } pub fn search(&self, title: &str, class: &str, exe: &str, path: &str) -> Option<&AppConfig> { let normalized_path = path.to_lowercase().replace("\\", "/"); let normalized_exe = exe.to_lowercase(); self.0.iter().find(|&config| { config .identifier .validate(title, class, &normalized_exe, &normalized_path) }) } pub fn iter(&self) -> impl Iterator { self.0.iter() } pub fn clear(&mut self) { self.0.clear(); } pub fn len(&self) -> usize { self.0.len() } pub fn is_empty(&self) -> bool { self.0.is_empty() } pub fn extend(&mut self, configs: Vec) { self.0.extend(configs); } pub fn as_slice(&self) -> &[AppConfig] { &self.0 } } #[cfg(test)] mod tests { use super::*; #[test] fn test_system_apps_path_contains_matching() { // Test the specific case: ShellExperienceHost.exe with SystemApps path matching let mut identifier = AppIdentifier { id: "Windows\\SystemApps".to_string(), kind: AppIdentifierType::Path, matching_strategy: MatchingStrategy::Contains, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }; // Prepare the identifier (this normalizes to lowercase with forward slashes) identifier.prepare(); // Test path (will be normalized by search() to lowercase with forward slashes) let path = "C:\\WINDOWS\\SYSTEMAPPS\\SHELLEXPERIENCEHOST_CW5N1H2TXYEWY\\SHELLEXPERIENCEHOST.EXE"; let title = ""; let class = ""; let exe = "SHELLEXPERIENCEHOST.EXE"; // Normalize path and exe as search() does let normalized_path = path.to_lowercase().replace("\\", "/"); let normalized_exe = exe.to_lowercase(); // Should match because normalized path contains "windows/systemapps" assert!( identifier.validate(title, class, &normalized_exe, &normalized_path), "Path should match with Contains strategy" ); } #[test] fn test_system_apps_full_config() { // Test a full AppConfig with the System Background Apps configuration let mut config = AppConfig { name: "System Background Apps".to_string(), category: None, bound_monitor: None, bound_workspace: None, identifier: AppIdentifier { id: "Windows\\SystemApps".to_string(), kind: AppIdentifierType::Path, matching_strategy: MatchingStrategy::Contains, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }, options: vec![AppExtraFlag::NoInteractive], is_bundled: false, }; config.prepare(); // Test that ShellExperienceHost.exe matches let path = "C:\\WINDOWS\\SYSTEMAPPS\\SHELLEXPERIENCEHOST_CW5N1H2TXYEWY\\SHELLEXPERIENCEHOST.EXE"; let title = ""; let class = ""; let exe = "SHELLEXPERIENCEHOST.EXE"; // Normalize as search() does let normalized_path = path.to_lowercase().replace("\\", "/"); let normalized_exe = exe.to_lowercase(); assert!( config .identifier .validate(title, class, &normalized_exe, &normalized_path), "ShellExperienceHost.exe should match System Background Apps config" ); // Verify options assert_eq!(config.options.len(), 1); assert_eq!(config.options[0], AppExtraFlag::NoInteractive); } #[test] fn test_apps_configuration_list_search() { // Test searching in AppsConfigurationList let mut list = AppsConfigurationList(vec![AppConfig { name: "System Background Apps".to_string(), category: None, bound_monitor: None, bound_workspace: None, identifier: AppIdentifier { id: "Windows\\SystemApps".to_string(), kind: AppIdentifierType::Path, matching_strategy: MatchingStrategy::Contains, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }, options: vec![AppExtraFlag::NoInteractive], is_bundled: true, }]); list.prepare(); // Search for ShellExperienceHost.exe let path = "C:\\WINDOWS\\SYSTEMAPPS\\SHELLEXPERIENCEHOST_CW5N1H2TXYEWY\\SHELLEXPERIENCEHOST.EXE"; let title = ""; let class = ""; let exe = "SHELLEXPERIENCEHOST.EXE"; let result = list.search(title, class, exe, path); assert!(result.is_some(), "Should find matching config"); let found_config = result.unwrap(); assert_eq!(found_config.name, "System Background Apps"); assert!(found_config.options.contains(&AppExtraFlag::NoInteractive)); } #[test] fn test_path_contains_non_matching() { // Test that non-SystemApps paths don't match let mut identifier = AppIdentifier { id: "Windows\\SystemApps".to_string(), kind: AppIdentifierType::Path, matching_strategy: MatchingStrategy::Contains, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }; identifier.prepare(); // Test with a regular Program Files path let path = "C:\\PROGRAM FILES\\SOME APP\\APP.EXE"; let title = ""; let class = ""; let exe = "APP.EXE"; // Normalize as search() does let normalized_path = path.to_lowercase().replace("\\", "/"); let normalized_exe = exe.to_lowercase(); assert!( !identifier.validate(title, class, &normalized_exe, &normalized_path), "Non-SystemApps path should not match" ); } #[test] fn test_path_case_insensitivity() { // Test that path matching is case insensitive let mut identifier = AppIdentifier { id: "windows\\systemapps".to_string(), // lowercase kind: AppIdentifierType::Path, matching_strategy: MatchingStrategy::Contains, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }; identifier.prepare(); // Test with uppercase path (will be normalized to lowercase) let path = "C:\\WINDOWS\\SYSTEMAPPS\\SHELLEXPERIENCEHOST_CW5N1H2TXYEWY\\SHELLEXPERIENCEHOST.EXE"; let title = ""; let class = ""; let exe = "SHELLEXPERIENCEHOST.EXE"; // Normalize as search() does let normalized_path = path.to_lowercase().replace("\\", "/"); let normalized_exe = exe.to_lowercase(); assert!( identifier.validate(title, class, &normalized_exe, &normalized_path), "Path matching should be case insensitive" ); } #[test] fn test_multiple_system_apps() { // Test that the same config matches multiple SystemApps executables let mut identifier = AppIdentifier { id: "Windows\\SystemApps".to_string(), kind: AppIdentifierType::Path, matching_strategy: MatchingStrategy::Contains, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }; identifier.prepare(); // Test different SystemApps let test_cases = vec![ "C:\\WINDOWS\\SYSTEMAPPS\\SHELLEXPERIENCEHOST_CW5N1H2TXYEWY\\SHELLEXPERIENCEHOST.EXE", "C:\\WINDOWS\\SYSTEMAPPS\\MICROSOFT.WINDOWS.STARTMENUEXPERIENCEHOST_CW5N1H2TXYEWY\\STARTMENUEXPERIENCEHOST.EXE", "C:\\WINDOWS\\SYSTEMAPPS\\MICROSOFT.WINDOWS.SEARCH_CW5N1H2TXYEWY\\SEARCHAPP.EXE", ]; for path in test_cases { // Normalize as search() does let normalized_path = path.to_lowercase().replace("\\", "/"); assert!( identifier.validate("", "", "", &normalized_path), "Path {} should match SystemApps pattern", path ); } } #[test] fn test_app_extra_flag_deserialization() { // Test that "no-interactive" deserializes correctly let json = r#"{"name":"Test","identifier":{"id":"test","kind":"exe","matchingStrategy":"equals"},"options":["no-interactive"]}"#; let config: AppConfig = serde_json::from_str(json).expect("Should deserialize"); assert_eq!(config.options.len(), 1); assert_eq!(config.options[0], AppExtraFlag::NoInteractive); } #[test] fn test_matching_strategy_deserialization() { // Test that "contains" deserializes correctly let json = r#"{"name":"Test","identifier":{"id":"test","kind":"path","matchingStrategy":"contains"},"options":[]}"#; let config: AppConfig = serde_json::from_str(json).expect("Should deserialize"); assert!(matches!( config.identifier.matching_strategy, MatchingStrategy::Contains )); } #[test] fn test_path_separator_normalization_forward_slash() { // Test that both forward and backslashes work (both normalized to forward slash) let mut identifier = AppIdentifier { id: "Windows/SystemApps".to_string(), // Using forward slash kind: AppIdentifierType::Path, matching_strategy: MatchingStrategy::Contains, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }; identifier.prepare(); // Verify the cached id is normalized to lowercase with forward slashes assert_eq!(identifier.lower_id(), "windows/systemapps"); // Should match when path uses backslashes (normalized by search()) let path = "C:\\WINDOWS\\SYSTEMAPPS\\SHELLEXPERIENCEHOST_CW5N1H2TXYEWY\\SHELLEXPERIENCEHOST.EXE"; let title = ""; let class = ""; let exe = "SHELLEXPERIENCEHOST.EXE"; // Normalize as search() does let normalized_path = path.to_lowercase().replace("\\", "/"); let normalized_exe = exe.to_lowercase(); assert!( identifier.validate(title, class, &normalized_exe, &normalized_path), "Both forward and backslashes should be normalized to forward slash" ); } #[test] fn test_path_separator_mixed() { // Test with mixed separators in the identifier let mut identifier = AppIdentifier { id: "Windows\\SystemApps/Microsoft.Windows".to_string(), // Mixed separators kind: AppIdentifierType::Path, matching_strategy: MatchingStrategy::Contains, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }; identifier.prepare(); // Verify all backslashes are normalized to forward slashes assert_eq!( identifier.lower_id(), "windows/systemapps/microsoft.windows" ); // Path with backslashes should match (normalized by search()) let path = "C:\\WINDOWS\\SYSTEMAPPS\\MICROSOFT.WINDOWS.SEARCH_CW5N1H2TXYEWY\\SEARCHAPP.EXE"; let title = ""; let class = ""; let exe = "SEARCHAPP.EXE"; // Normalize as search() does let normalized_path = path.to_lowercase().replace("\\", "/"); let normalized_exe = exe.to_lowercase(); assert!( identifier.validate(title, class, &normalized_exe, &normalized_path), "Mixed separators should be normalized to forward slashes" ); } #[test] fn test_exe_separator_normalization() { // Test that exe type also normalizes separators (though less common) let mut identifier = AppIdentifier { id: "app.EXE".to_string(), kind: AppIdentifierType::Exe, matching_strategy: MatchingStrategy::Contains, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }; identifier.prepare(); // Verify the cached value has backslashes assert_eq!(identifier.lower_id(), "app.exe"); } #[test] fn test_title_and_class_no_normalization() { // Test that Title and Class types don't normalize slashes let mut title_identifier = AppIdentifier { id: "Some/Title".to_string(), kind: AppIdentifierType::Title, matching_strategy: MatchingStrategy::Equals, negation: false, and: vec![], or: vec![], cache: AppIdentifierCache::default(), }; title_identifier.prepare(); // Title with forward slash should match exactly (no normalization) assert!( title_identifier.validate("Some/Title", "", "", ""), "Title should not normalize separators" ); // Should NOT match with backslash assert!( !title_identifier.validate("Some\\Title", "", "", ""), "Title should preserve forward slash" ); } } ================================================ FILE: libs/core/src/state/settings/shortcuts.rs ================================================ use std::collections::HashSet; use uuid::Uuid; use crate::resource::WidgetId; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] #[serde(tag = "name", rename_all = "snake_case")] pub enum SluHotkeyAction { ToggleAppsMenu, ToggleWorkspacesView, // ========================== TaskNext { select_on_key_up: bool, }, TaskPrev { select_on_key_up: bool, }, // ========================== PauseTiling, ToggleFloat, ToggleMonocle, CycleStackNext, CycleStackPrev, ReserveTop, ReserveBottom, ReserveLeft, ReserveRight, ReserveFloat, ReserveStack, FocusTop, FocusBottom, FocusLeft, FocusRight, IncreaseWidth, DecreaseWidth, IncreaseHeight, DecreaseHeight, RestoreSizes, MoveWindowUp, MoveWindowDown, MoveWindowLeft, MoveWindowRight, // ========================== StartWegApp { #[serde(alias = "arg")] index: usize, }, // ========================== SwitchWorkspace { #[serde(alias = "arg")] index: usize, }, MoveToWorkspace { #[serde(alias = "arg")] index: usize, }, SendToWorkspace { #[serde(alias = "arg")] index: usize, }, SwitchToNextWorkspace, SwitchToPreviousWorkspace, CreateNewWorkspace, DestroyCurrentWorkspace, // ========================== CycleWallpaperNext, CycleWallpaperPrev, // ========================== MiscOpenSettings, MiscForceRestart, MiscForceQuit, MiscToggleLockTracing, MiscToggleWinEventTracing, #[serde(other)] Unknown, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] pub struct SluHotkey { pub id: Uuid, pub action: SluHotkeyAction, pub keys: Vec, #[serde(default)] pub readonly: bool, /// This will be true for hotkeys intended to override system hotkeys. #[serde(default)] pub system: bool, /// If present this shortcut will be only available if the widget is enabled. #[serde(default)] pub attached_to: Option, } impl SluHotkey { pub fn new<'a, T, I>(action: SluHotkeyAction, keys: I) -> Self where T: AsRef + 'a, I: IntoIterator, { Self { id: Uuid::new_v4(), action, keys: keys.into_iter().map(|k| k.as_ref().to_string()).collect(), readonly: false, system: false, attached_to: None, } } pub fn system(mut self) -> Self { self.system = true; self.readonly = true; self } pub fn readonly(mut self) -> Self { self.readonly = true; self } pub fn attached_to(mut self, widget_id: impl Into) -> Self { self.attached_to = Some(widget_id.into()); self } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct SluShortcutsSettings { pub enabled: bool, pub app_commands: Vec, } impl Default for SluShortcutsSettings { fn default() -> Self { Self { enabled: true, app_commands: Vec::new(), } } } impl SluShortcutsSettings { pub fn contains_action(&self, action: SluHotkeyAction) -> bool { self.app_commands.iter().any(|h| h.action == action) } pub fn sanitize(&mut self) { let defaults = Self::default_shortcuts(); for hotkey in defaults.app_commands { // add missing hotkeys from defaults if !self.contains_action(hotkey.action) { self.app_commands.push(hotkey); } } let mut seen_ids = HashSet::new(); self.app_commands.retain(|h| { seen_ids.insert(h.id) && !h.keys.is_empty() && h.action != SluHotkeyAction::Unknown }); } pub fn get_mut(&mut self, action: SluHotkeyAction) -> Option<&mut SluHotkey> { self.app_commands.iter_mut().find(|h| h.action == action) } pub fn default_shortcuts() -> Self { let mut shorcuts = Self::_default_shortcuts(); for index in 0..10 { let digit_key = if index == 9 { String::from("0") } else { format!("{}", index + 1) }; shorcuts.push( SluHotkey::new( SluHotkeyAction::StartWegApp { index }, ["Win", digit_key.as_str()], ) .system() .attached_to("@seelen/weg"), ); shorcuts.push(SluHotkey::new( SluHotkeyAction::SwitchWorkspace { index }, ["Alt", digit_key.as_str()], )); shorcuts.push(SluHotkey::new( SluHotkeyAction::MoveToWorkspace { index }, ["Alt", "Shift", digit_key.as_str()], )); shorcuts.push(SluHotkey::new( SluHotkeyAction::SendToWorkspace { index }, ["Win", "Shift", digit_key.as_str()], )); } Self { enabled: true, app_commands: shorcuts, } } fn _default_shortcuts() -> Vec { use SluHotkeyAction::*; let wm = "@seelen/window-manager"; vec![ SluHotkey::new(ToggleAppsMenu, ["Win"]) .system() .attached_to("@seelen/apps-menu"), // Task switching and viewer SluHotkey::new( TaskNext { select_on_key_up: true, }, ["Alt", "Tab"], ) .system() .attached_to("@seelen/task-switcher"), SluHotkey::new( TaskPrev { select_on_key_up: true, }, ["Alt", "Shift", "Tab"], ) .system() .attached_to("@seelen/task-switcher"), SluHotkey::new( TaskNext { select_on_key_up: false, }, ["Alt", "Ctrl", "Tab"], ) .system() .attached_to("@seelen/task-switcher"), SluHotkey::new( TaskPrev { select_on_key_up: false, }, ["Alt", "Ctrl", "Shift", "Tab"], ) .system() .attached_to("@seelen/task-switcher"), // tiling window manager SluHotkey::new(PauseTiling, ["Win", "P"]).attached_to(wm), SluHotkey::new(ToggleFloat, ["Win", "F"]).attached_to(wm), SluHotkey::new(ToggleMonocle, ["Win", "M"]).attached_to(wm), // SluHotkey::new(CycleStackNext, ["Win", "Alt", "Right"]).attached_to(wm), SluHotkey::new(CycleStackPrev, ["Win", "Alt", "Left"]).attached_to(wm), // SluHotkey::new(ReserveTop, ["Win", "Shift", "I"]).attached_to(wm), SluHotkey::new(ReserveBottom, ["Win", "Shift", "K"]).attached_to(wm), SluHotkey::new(ReserveLeft, ["Win", "Shift", "J"]).attached_to(wm), SluHotkey::new(ReserveRight, ["Win", "Shift", "L"]).attached_to(wm), SluHotkey::new(ReserveFloat, ["Win", "Shift", "U"]).attached_to(wm), SluHotkey::new(ReserveStack, ["Win", "Shift", "O"]).attached_to(wm), // SluHotkey::new(FocusTop, ["Alt", "I"]).attached_to(wm), SluHotkey::new(FocusBottom, ["Alt", "K"]).attached_to(wm), SluHotkey::new(FocusLeft, ["Alt", "J"]).attached_to(wm), SluHotkey::new(FocusRight, ["Alt", "L"]).attached_to(wm), // SluHotkey::new(IncreaseWidth, ["Win", "Alt", "="]).attached_to(wm), SluHotkey::new(DecreaseWidth, ["Win", "Alt", "-"]).attached_to(wm), SluHotkey::new(IncreaseHeight, ["Win", "Ctrl", "="]).attached_to(wm), SluHotkey::new(DecreaseHeight, ["Win", "Ctrl", "-"]).attached_to(wm), SluHotkey::new(RestoreSizes, ["Win", "Alt", "0"]).attached_to(wm), // SluHotkey::new(MoveWindowUp, ["Shift", "Alt", "I"]).attached_to(wm), SluHotkey::new(MoveWindowDown, ["Shift", "Alt", "K"]).attached_to(wm), SluHotkey::new(MoveWindowLeft, ["Shift", "Alt", "J"]).attached_to(wm), SluHotkey::new(MoveWindowRight, ["Shift", "Alt", "L"]).attached_to(wm), // virtual desktop SluHotkey::new(SwitchToNextWorkspace, ["Ctrl", "Win", "Right"]).system(), SluHotkey::new(SwitchToPreviousWorkspace, ["Ctrl", "Win", "Left"]).system(), SluHotkey::new(CreateNewWorkspace, ["Ctrl", "Win", "D"]).system(), SluHotkey::new(DestroyCurrentWorkspace, ["Ctrl", "Win", "F4"]).system(), SluHotkey::new(ToggleWorkspacesView, ["Win", "Tab"]) .system() .attached_to("@seelen/workspaces-viewer"), // wallpaper manager SluHotkey::new(CycleWallpaperNext, ["Ctrl", "Win", "Up"]), SluHotkey::new(CycleWallpaperPrev, ["Ctrl", "Win", "Down"]), // misc SluHotkey::new(MiscOpenSettings, ["Win", "K"]), SluHotkey::new(MiscForceRestart, ["Ctrl", "Win", "Alt", "R"]).readonly(), SluHotkey::new(MiscForceQuit, ["Ctrl", "Win", "Alt", "K"]).readonly(), ] } } ================================================ FILE: libs/core/src/state/theme/config.rs ================================================ use std::sync::LazyLock; use schemars::JsonSchema; use serde::{de::Visitor, Deserialize, Deserializer}; use crate::{error::Result, resource::ResourceText}; #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] pub struct ThemeSettingsDefinition(Vec); #[derive(Debug, Clone, Serialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub enum ThemeConfigDefinition { Group(ThemeConfigGroup), #[serde(untagged)] Item(Box), } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ThemeConfigGroup { header: ResourceText, items: Vec, } impl<'de> Deserialize<'de> for ThemeConfigDefinition { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] struct GroupVariant { group: ThemeConfigGroup, } let value = serde_json::Value::deserialize(deserializer).map_err(serde::de::Error::custom)?; if let Ok(parsed) = GroupVariant::deserialize(value.clone()) { return Ok(ThemeConfigDefinition::Group(parsed.group)); } Ok(ThemeConfigDefinition::Item(Box::new( serde_json::from_value(value).map_err(serde::de::Error::custom)?, ))) } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(tag = "syntax")] pub enum ThemeVariableDefinition { /// This config definition will allow to users write any string.\ /// Css syntax: https://developer.mozilla.org/en-US/docs/Web/CSS/string \ /// ### example: /// ```css /// --var-name: "user input" /// ``` #[serde(rename = "")] String(ThemeVariable), /// This config definition will allow to users select a color and /// will be stored as a hex value, opacity is always allowed via UI.\ /// Css syntax: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value \ /// ### example: /// ```css /// --var-name: #ff22ee /// --var-name: #ff22ee /// ``` #[serde(rename = "")] Color(ThemeVariable), /// This will allow to the user set any lenght in any unit. (px, %, vw, etc). /// If you need force a specific unit, use Number instead lenght and on theme code makes the conversion.\ /// Css syntax: https://developer.mozilla.org/en-US/docs/Web/CSS/length \ /// ### example: /// ```css /// --var-name: 10px /// --var-name: 10% /// --var-name: 10vw /// ``` #[serde( rename = "", alias = "", alias = "" )] Length(ThemeVariableWithUnit), /// This will allow to users to set any number, without units. /// Css syntax: https://developer.mozilla.org/en-US/docs/Web/CSS/number \ /// ### example: /// ```css /// --var-name: 10 /// ``` #[serde(rename = "")] Number(ThemeVariable), /// This will allow to users to set any url.\ /// Css syntax: https://developer.mozilla.org/en-US/docs/Web/CSS/url_value \ /// ### example: /// ```css /// --var-name: url("https://example.com/image.png") /// ``` /// This will be rendered as a file input on select file the url version of the path will be stored. /// Initial value will be ignored. #[serde(rename = "")] Url(ThemeVariable), } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ThemeVariable { /// Css variable name, example: `--my-css-variable` pub name: CssVariableName, /// Label to show to the user on Settings. pub label: ResourceText, /// Extra details to show to the user under the label on Settings. pub description: Option, /// Will be rendered as a icon with a tooltip side the label. pub tip: Option, /// Initial variable value, if not manually set by the user. pub initial_value: T, /// syntax = min length of the input.\ /// syntax = min value of the input. pub min: Option, /// syntax = max length of the input.\ /// syntax = max value of the input. pub max: Option, /// Only used if syntax is ``, setting this will make the input a slider pub step: Option, /// If present, this will be rendered as a selector of options instead of an input. /// `initial_value` should be present in this list. pub options: Option>, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ThemeVariableWithUnit { #[serde(flatten)] pub _extends: ThemeVariable, pub initial_value_unit: String, } /// Valid CSS variable name that starts with `--` and follows CSS naming conventions #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, JsonSchema, TS)] pub struct CssVariableName(String); static CSS_VAR_REGEX: LazyLock = LazyLock::new(|| regex::Regex::new(r"^--[a-zA-Z_][\w-]*$").unwrap()); impl CssVariableName { /// Creates a new CssVariableName after validation pub fn from_string(name: &str) -> Result { if !CSS_VAR_REGEX.is_match(name) { return Err(format!( "Invalid CSS variable name '{name}'. Must start with '--' and follow CSS naming rules" ) .into()); } Ok(Self(name.to_string())) } } impl std::fmt::Display for CssVariableName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl<'de> Deserialize<'de> for CssVariableName { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { struct CssVariableNameVisitor; impl<'de> Visitor<'de> for CssVariableNameVisitor { type Value = CssVariableName; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { write!( formatter, "a valid CSS variable name starting with '--' and following CSS naming rules" ) } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { CssVariableName::from_string(value).map_err(serde::de::Error::custom) } } deserializer.deserialize_str(CssVariableNameVisitor) } } ================================================ FILE: libs/core/src/state/theme/mod.rs ================================================ #[cfg(test)] mod tests; pub mod config; use std::{collections::HashMap, path::Path}; use config::ThemeSettingsDefinition; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::{ error::Result, resource::{ResourceKind, ResourceMetadata, SluResource, ThemeId, WidgetId}, utils::search_resource_entrypoint, }; pub static ALLOWED_STYLE_EXTENSIONS: &[&str] = &["css", "scss", "sass"]; #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Theme { pub id: ThemeId, /// Metadata about the theme #[serde(alias = "info")] // for backwards compatibility before v2.0 pub metadata: ResourceMetadata, pub settings: ThemeSettingsDefinition, /// Css Styles of the theme pub styles: HashMap, /// Shared css styles for all widgets, commonly used to set styles /// for the components library pub shared_styles: String, } impl SluResource for Theme { const KIND: ResourceKind = ResourceKind::Theme; fn metadata(&self) -> &ResourceMetadata { &self.metadata } fn metadata_mut(&mut self) -> &mut ResourceMetadata { &mut self.metadata } fn load_from_folder(path: &Path) -> Result { let mut theme = Self::load_old_folder_schema(path)?; 'outer: for entry in path.read_dir()?.flatten() { let outer_path = entry.path(); if !outer_path.is_dir() { let (Some(file_stem), Some(ext)) = (outer_path.file_stem(), outer_path.extension()) else { continue 'outer; }; if file_stem == "shared" && ALLOWED_STYLE_EXTENSIONS.iter().any(|e| *e == ext) { let css = if ext == "scss" || ext == "sass" { grass::from_path(&outer_path, &grass::Options::default())? } else { std::fs::read_to_string(&outer_path)? }; theme.shared_styles = css; } continue 'outer; } let creator_username = entry.file_name(); 'inner: for entry in outer_path.read_dir()?.flatten() { let path = entry.path(); if !path.is_file() { continue 'inner; } let (Some(resource_name), Some(ext)) = (path.file_stem(), path.extension()) else { continue 'inner; }; if ALLOWED_STYLE_EXTENSIONS.iter().any(|e| *e == ext) { let css = if ext == "scss" || ext == "sass" { grass::from_path(&path, &grass::Options::default())? } else { std::fs::read_to_string(&path)? }; theme.styles.insert( WidgetId::from( format!( "@{}/{}", creator_username.to_string_lossy(), resource_name.to_string_lossy() ) .as_str(), ), css, ); } } } Ok(theme) } } impl Theme { /// Load theme from a folder using old deprecated paths since v2.1.0 will be removed in v3 fn load_old_folder_schema(path: &Path) -> Result { let file = search_resource_entrypoint(path).unwrap_or_else(|| { path.join("theme.yml") // backward compatibility to be removed in v3 }); let mut theme = Self::load_from_file(&file)?; if path.join("theme.weg.css").exists() { theme.styles.insert( WidgetId::known_weg(), std::fs::read_to_string(path.join("theme.weg.css"))?, ); } if path.join("theme.toolbar.css").exists() { theme.styles.insert( WidgetId::known_toolbar(), std::fs::read_to_string(path.join("theme.toolbar.css"))?, ); } if path.join("theme.wm.css").exists() { theme.styles.insert( WidgetId::known_wm(), std::fs::read_to_string(path.join("theme.wm.css"))?, ); } if path.join("theme.wall.css").exists() { theme.styles.insert( WidgetId::known_wall(), std::fs::read_to_string(path.join("theme.wall.css"))?, ); }; Ok(theme) } } ================================================ FILE: libs/core/src/state/theme/mod.ts ================================================ import type { ResourceId, Settings as ISettings, Theme as ITheme, ThemeConfigDefinition, ThemeId, ThemeVariableDefinition, } from "@seelen-ui/types"; import { SeelenCommand, SeelenEvent, type UnSubscriber } from "../../handlers/mod.ts"; import { List } from "../../utils/List.ts"; import { newFromInvoke, newOnEvent } from "../../utils/State.ts"; import { Widget } from "../widget/mod.ts"; export class ThemeList extends List { static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.StateGetThemes); } static onChange(cb: (payload: ThemeList) => void): Promise { return newOnEvent(cb, this, SeelenEvent.StateThemesChanged); } applyToDocument(activeIds: ThemeId[], variables: ISettings["byTheme"]): void { const enabledThemes: Theme[] = []; for (const theme of this.asArray()) { if (activeIds.includes(theme.id)) { enabledThemes.push(new Theme(theme)); } } // sort by user order enabledThemes.sort((a, b) => activeIds.indexOf(a.id) - activeIds.indexOf(b.id)); removeAllThemeStyles(); for (const theme of enabledThemes) { theme.applyToDocument(variables[theme.id]); } } } export interface Theme extends ITheme {} export class Theme { constructor(plain: ITheme) { Object.assign(this, plain); } forEachVariableDefinition(cb: (def: ThemeVariableDefinition) => void): void { iterateVariableDefinitions(this.settings, cb); } /** Will add the styles targeting the current widget id */ applyToDocument(varValues: ISettings["byTheme"][ResourceId] = {}): void { const widgetId = Widget.self.id; let styles = ``; this.forEachVariableDefinition((def) => { if (!isValidCssVariableName(def.name)) { return; } styles += ` @property ${def.name} { syntax: "${def.syntax}"; inherits: true; initial-value: ${def.initialValue}${"initialValueUnit" in def ? def.initialValueUnit : ""}; } `; }); const layerName = "theme-" + this.id .toLowerCase() .replaceAll("@", "") .replaceAll(/[^a-zA-Z0-9\-\_]/g, "_"); styles += `@layer ${layerName}-shared {\n${this.sharedStyles}\n}\n`; const variablesContent = Object.entries(varValues) .filter(([name, _value]) => isValidCssVariableName(name)) .map(([name, value]) => `${name}: ${value || ""};`) .join("\n"); styles += `@layer ${layerName} {\n:root {${variablesContent}}\n${this.styles[widgetId] ?? ""}\n}\n`; this.removeFromDocument(); // remove old styles const styleElement = document.createElement("style"); styleElement.id = this.id; styleElement.textContent = styles; styleElement.setAttribute("data-source", "theme"); document.head.appendChild(styleElement); } removeFromDocument(): void { document.getElementById(this.id)?.remove(); } } function isValidCssVariableName(name: string): boolean { return /^--[\w\d-]*$/.test(name); } function iterateVariableDefinitions( defs: ThemeConfigDefinition[], cb: (def: ThemeVariableDefinition) => void, ): void { for (const def of defs) { if ("group" in def) { iterateVariableDefinitions(def.group.items, cb); } else { cb(def); } } } function removeAllThemeStyles(): void { const elements = document.querySelectorAll(`style[data-source="theme"]`); for (const element of elements) { if (element instanceof HTMLStyleElement) { element.remove(); } } } ================================================ FILE: libs/core/src/state/theme/tests.rs ================================================ use std::path::PathBuf; use crate::{error::Result, resource::SluResource, state::Theme}; #[test] fn test_compatibility_with_older_schemas() -> Result<()> { Theme::load(&PathBuf::from("./mocks/themes/v2.3.0.yml")).map_err(|e| format!("v2.3.0: {e}"))?; Theme::load(&PathBuf::from("./mocks/themes/v2.3.12.yml")) .map_err(|e| format!("v2.3.12: {e}"))?; Ok(()) } ================================================ FILE: libs/core/src/state/theme/theming.ts ================================================ import { UIColors } from "../../system_state/ui_colors.ts"; import { RuntimeStyleSheet } from "../../utils/DOM.ts"; import { Settings } from "../settings/mod.ts"; import { ThemeList } from "./mod.ts"; /** * This will apply the active themes for this widget, and automatically update * when the themes or settings change. Also will add the systehm ui colors to the document. */ export async function startThemingTool(): Promise { let settings = await Settings.getAsync(); let themes = await ThemeList.getAsync(); await ThemeList.onChange((newThemes) => { themes = newThemes; themes.applyToDocument(settings.activeThemes, settings.byTheme); }); await Settings.onChange((newSettings) => { settings = newSettings; themes.applyToDocument(settings.activeThemes, settings.byTheme); }); (await UIColors.getAsync()).setAsCssVariables(); await UIColors.onChange((colors) => colors.setAsCssVariables()); startDateCssVariables(); themes.applyToDocument(settings.activeThemes, settings.byTheme); } export function startDateCssVariables(): void { // Set initial values immediately updateDateCssVariables(); // Update every minute (60000ms) to avoid overhead from seconds setInterval(updateDateCssVariables, 60000); } function updateDateCssVariables(): void { const now = new Date(); const locale = navigator.language; // Time values const hour = now.getHours(); // 0-23 const minute = now.getMinutes(); // 0-59 // Date name values using Intl API const dayName = new Intl.DateTimeFormat(locale, { weekday: "long" }).format(now); const monthName = new Intl.DateTimeFormat(locale, { month: "long" }).format(now); // Date numeric values const dayOfMonth = now.getDate(); // 1-31 const monthNumber = now.getMonth() + 1; // 1-12 const year = now.getFullYear(); // 2025, etc. const styleSheet = new RuntimeStyleSheet("@runtime/date-variables"); // Time variables styleSheet.addVariable("--date-hour", String(hour)); styleSheet.addVariable("--date-minute", String(minute)); // Date name variables (localized) styleSheet.addVariable("--date-day-name", dayName); styleSheet.addVariable("--date-month-name", monthName); // Date numeric variables styleSheet.addVariable("--date-day", String(dayOfMonth)); styleSheet.addVariable("--date-month", String(monthNumber)); styleSheet.addVariable("--date-year", String(year)); styleSheet.applyToDocument(); } ================================================ FILE: libs/core/src/state/wallpaper/mod.rs ================================================ use std::path::Path; use url::Url; use uuid::Uuid; use crate::{ error::Result, resource::{ InternalResourceMetadata, ResourceKind, ResourceMetadata, ResourceText, SluResource, WallpaperId, }, }; #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Wallpaper { pub id: WallpaperId, pub metadata: ResourceMetadata, pub r#type: WallpaperKind, pub url: Option, pub filename: Option, pub thumbnail_url: Option, #[serde(alias = "thumbnail_filename")] pub thumbnail_filename: Option, /// Only used if the wallpaper type is `Layered`.\ /// Custom css that will be applied only on this wallpaper. pub css: Option, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WallpaperKind { #[serde(alias = "image")] Image, #[serde(alias = "video")] Video, #[serde(alias = "layered")] Layered, /// used for wallpapers created before v2.4.9, will be changed on sanitization #[default] Unsupported, } impl SluResource for Wallpaper { const KIND: ResourceKind = ResourceKind::Wallpaper; fn metadata(&self) -> &ResourceMetadata { &self.metadata } fn metadata_mut(&mut self) -> &mut ResourceMetadata { &mut self.metadata } fn sanitize(&mut self) { // migration step for old wallpapers if WallpaperKind::Unsupported == self.r#type { if let Some(filename) = &self.filename { if Self::SUPPORTED_VIDEOS .iter() .any(|ext| filename.ends_with(ext)) { self.r#type = WallpaperKind::Video; } if Self::SUPPORTED_IMAGES .iter() .any(|ext| filename.ends_with(ext)) { self.r#type = WallpaperKind::Image; } } } // remove thumbnail if doesn't exist if let Some(filename) = &self.thumbnail_filename { let thumbnail_path = self.metadata.internal.path.join(filename); if !thumbnail_path.exists() { self.thumbnail_filename = None; } } } fn validate(&self) -> Result<()> { if self.r#type == WallpaperKind::Unsupported { return Err("Unsupported wallpaper extension".into()); } Ok(()) } } impl Wallpaper { /// https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types pub const SUPPORTED_IMAGES: [&str; 11] = [ "apng", "avif", "gif", "jpg", "jpeg", "png", "svg", "webp", "bmp", "ico", "tiff", ]; /// https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Containers pub const SUPPORTED_VIDEOS: [&str; 7] = ["mp4", "webm", "ogg", "avi", "mov", "mkv", "mpeg"]; /// path should be the path to the wallpaper image or video to be moved or copied to the wallpaper folder pub fn create_from_file(path: &Path, folder_to_store: &Path, copy: bool) -> Result { if !path.exists() || path.is_dir() { return Err("File does not exist".into()); } let (Some(filename), Some(ext)) = (path.file_name(), path.extension()) else { return Err("Invalid file name or extension".into()); }; let filename = filename.to_string_lossy().to_string(); let ext = ext.to_string_lossy().to_string(); // as uuids can start with numbers and resources names can't start with numbers // we prefix the uuid with an 'x' let resource_name = uuid::Uuid::new_v4(); let id = format!("@user/x{}", resource_name.as_simple()).into(); let metadata = ResourceMetadata { display_name: ResourceText::En(filename.clone()), internal: InternalResourceMetadata { path: folder_to_store.join("metadata.yml"), ..Default::default() }, ..Default::default() }; std::fs::create_dir_all(folder_to_store)?; if copy { std::fs::copy(path, folder_to_store.join(&filename))?; } else { std::fs::rename(path, folder_to_store.join(&filename))?; } let r#type = if Self::SUPPORTED_IMAGES.contains(&ext.as_str()) { WallpaperKind::Image } else if Self::SUPPORTED_VIDEOS.contains(&ext.as_str()) { WallpaperKind::Video } else { WallpaperKind::Unsupported }; let wallpaper = Self { id, metadata, r#type, filename: Some(filename.clone()), thumbnail_filename: if Self::SUPPORTED_IMAGES.contains(&ext.as_str()) { Some(filename) } else { None }, ..Default::default() }; wallpaper.save()?; Ok(wallpaper) } } #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct WallpaperCollection { pub id: Uuid, pub name: String, pub wallpapers: Vec, } ================================================ FILE: libs/core/src/state/wallpaper/mod.ts ================================================ import type { Wallpaper as IWallpaper, WallpaperInstanceSettings } from "@seelen-ui/types"; import { SeelenCommand, SeelenEvent, type UnSubscriber } from "../../handlers/mod.ts"; import { List } from "../../utils/List.ts"; import { newFromInvoke, newOnEvent } from "../../utils/State.ts"; export const SUPPORTED_IMAGE_WALLPAPER_EXTENSIONS = [ "apng", "avif", "gif", "jpg", "jpeg", "png", "svg", "webp", "bmp", "ico", "tiff", ]; export const SUPPORTED_VIDEO_WALLPAPER_EXTENSIONS = [ "mp4", "webm", "ogg", "avi", "mov", "mkv", "mpeg", ]; export class WallpaperList extends List { static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.StateGetWallpapers); } static onChange(cb: (payload: WallpaperList) => void): Promise { return newOnEvent(cb, this, SeelenEvent.StateWallpapersChanged); } } export interface WallpaperConfiguration extends WallpaperInstanceSettings {} export class WallpaperConfiguration { constructor(plain: WallpaperInstanceSettings) { Object.assign(this, plain); } static default(): Promise { return newFromInvoke(this, SeelenCommand.StateGetDefaultWallpaperSettings); } } ================================================ FILE: libs/core/src/state/weg_items.rs ================================================ use std::{collections::HashSet, path::PathBuf}; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::system_state::{Relaunch, RelaunchArguments}; #[derive(Debug, Default, Clone, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] pub struct WegItemData { /// internal UUID to differentiate items pub id: uuid::Uuid, /// display name of the item pub display_name: String, /// Application user model id. pub umid: Option, /// path to file or program. pub path: PathBuf, /// the item will persist after all windows are closed pub pinned: bool, /// this item should not be pinnable pub prevent_pinning: bool, /// custom information to relaunch this app, if none, UMID or path should be used pub relaunch: Option, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(tag = "type")] pub enum WegItem { #[serde(alias = "PinnedApp", alias = "Pinned")] DeprecatedOldPinned(OldWegItemData), AppOrFile(WegItemData), Separator { id: uuid::Uuid, }, Media { id: uuid::Uuid, }, StartMenu { id: uuid::Uuid, }, ShowDesktop { id: uuid::Uuid, }, TrashBin { id: uuid::Uuid, }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export, repr(enum = name)))] pub enum WegItemType { AppOrFile, Separator, Media, StartMenu, ShowDesktop, TrashBin, } impl WegItem { pub fn id(&self) -> &uuid::Uuid { match self { WegItem::DeprecatedOldPinned(data) => &data.id, WegItem::AppOrFile(data) => &data.id, WegItem::Separator { id } => id, WegItem::Media { id } => id, WegItem::StartMenu { id } => id, WegItem::ShowDesktop { id } => id, WegItem::TrashBin { id } => id, } } fn set_id(&mut self, identifier: uuid::Uuid) { match self { WegItem::DeprecatedOldPinned(data) => data.id = identifier, WegItem::AppOrFile(data) => data.id = identifier, WegItem::Separator { id } => *id = identifier, WegItem::Media { id } => *id = identifier, WegItem::StartMenu { id } => *id = identifier, WegItem::ShowDesktop { id } => *id = identifier, WegItem::TrashBin { id } => *id = identifier, } } } #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct WegItems { /// Whether the reordering possible on the weg pub is_reorder_disabled: bool, pub left: Vec, pub center: Vec, pub right: Vec, } impl WegItems { fn migrate_item(item: WegItem) -> Option { let WegItem::DeprecatedOldPinned(mut data) = item else { return Some(item); }; // migration step for items before v2.1.6 if data.subtype == OldWegItemSubtype::UnknownV2_1_6 { data.subtype = if data.relaunch_program.to_lowercase().contains(".exe") { OldWegItemSubtype::App } else if data.path.is_dir() { OldWegItemSubtype::Folder } else { OldWegItemSubtype::File }; } if data.subtype == OldWegItemSubtype::Folder { return None; } // migration of old scheme before v2.5 if let Some(args) = &data.relaunch_args { if data.relaunch_program.contains("explorer") && args.to_string().starts_with("shell:AppsFolder") { data.relaunch_program = args.to_string(); data.relaunch_args = None; } } if data.relaunch_program.is_empty() { data.relaunch_program = data.path.to_string_lossy().to_string(); } let relaunch = Some(Relaunch { command: data.relaunch_program, args: data.relaunch_args, working_dir: data.relaunch_in, icon: None, }); Some(WegItem::AppOrFile(WegItemData { id: data.id, display_name: data.display_name, umid: data.umid, path: data.path, pinned: true, prevent_pinning: data.pin_disabled, relaunch, })) } fn migrate_items(items: Vec) -> Vec { items.into_iter().filter_map(Self::migrate_item).collect() } pub fn migrate(&mut self) { self.left = Self::migrate_items(std::mem::take(&mut self.left)); self.center = Self::migrate_items(std::mem::take(&mut self.center)); self.right = Self::migrate_items(std::mem::take(&mut self.right)); } fn sanitize_items(dict: &mut HashSet, items: Vec) -> Vec { let mut result = Vec::new(); for mut item in items { if let WegItem::AppOrFile(data) = &item { let should_ensure_path = data.umid.is_none() || data.path.extension().is_some_and(|e| e == "lnk"); if should_ensure_path && !data.path.exists() { continue; } } if item.id().is_nil() { item.set_id(uuid::Uuid::new_v4()); } if !dict.contains(item.id()) { dict.insert(*item.id()); result.push(item); } } result } pub fn sanitize(&mut self) { self.migrate(); let mut dict = HashSet::new(); self.left = Self::sanitize_items(&mut dict, std::mem::take(&mut self.left)); self.center = Self::sanitize_items(&mut dict, std::mem::take(&mut self.center)); self.right = Self::sanitize_items(&mut dict, std::mem::take(&mut self.right)); } } // ===================== DEPRECATED STRUCTS ===================== #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum OldWegItemSubtype { File, Folder, App, #[default] UnknownV2_1_6, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] pub struct OldWegItemData { /// internal UUID to differentiate items pub id: uuid::Uuid, /// Subtype of the item (mandatory, but is optional for backward compatibility) pub subtype: OldWegItemSubtype, /// Application user model id. pub umid: Option, /// path to file, folder or program. pub path: PathBuf, /// program to be executed pub relaunch_program: String, /// arguments to be passed to the relaunch program pub relaunch_args: Option, /// path where ejecute the relaunch command pub relaunch_in: Option, /// display name of the item pub display_name: String, /// This intention is to prevent pinned state change, when this is neccesary #[serde(skip_deserializing)] pub pin_disabled: bool, } ================================================ FILE: libs/core/src/state/widget/context_menu.rs ================================================ use crate::{resource::ResourceText, state::Alignment}; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export, optional_fields = nullable))] pub struct ContextMenu { pub identifier: uuid::Uuid, pub items: Vec, /// Alignment of the context menu on the X axis relative to the trigger point. pub align_x: Option, /// Alignment of the context menu on the Y axis relative to the trigger point. pub align_y: Option, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(tag = "type", rename_all_fields = "camelCase")] pub enum ContextMenuItem { Separator, Item { key: String, icon: Option, label: String, /// event name to be emitted on click, `key` will be sent as payload callback_event: String, /// If not null, the item will display a checkbox. /// `checked` field will be send as payload. #[ts(optional = nullable)] checked: Option, #[ts(optional = nullable)] disabled: Option, }, Submenu { identifier: uuid::Uuid, icon: Option, label: ResourceText, items: Vec, }, } ================================================ FILE: libs/core/src/state/widget/declaration.rs ================================================ use std::collections::HashSet; use schemars::JsonSchema; use serde::{Deserialize, Deserializer, Serialize}; use ts_rs::TS; use crate::resource::ResourceText; /// The Widget Settings Declaration is a list of configuration definitions. /// Each definition can be either a group (with nested items) or a direct configuration item. /// /// This structure is used to render and store widget settings in a user-friendly way, /// matching the style of the settings window. With this approach, custom configuration /// windows for specific widgets are not needed. #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct WidgetSettingsDeclarationList(Vec); impl WidgetSettingsDeclarationList { /// Checks if there are duplicate keys in the settings declaration.\ /// Reserved keys like "enabled" and "$instances" are also checked. pub fn there_are_duplicates(&self) -> bool { let mut seen: HashSet<&str> = HashSet::new(); // Reserved keys that cannot be used seen.insert("enabled"); seen.insert("$instances"); for definition in &self.0 { if Self::collect_keys_recursive(definition, &mut seen) { return true; } } false } fn collect_keys_recursive<'a>( definition: &'a WidgetConfigDefinition, seen: &mut HashSet<&'a str>, ) -> bool { match definition { WidgetConfigDefinition::Group(group) => { for item in &group.items { if Self::collect_keys_recursive(item, seen) { return true; } } } WidgetConfigDefinition::Item(item) => { let key = item.get_key(); if seen.contains(key) { return true; } seen.insert(key); } } false } } /// A widget configuration definition that can be either a group container or a settings item #[derive(Debug, Clone, Serialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub enum WidgetConfigDefinition { /// A group that contains nested configuration items. /// Groups are used to organize related settings with headers. Group(WidgetConfigGroup), /// A direct configuration item (untagged variant for simpler JSON structure) #[serde(untagged)] Item(Box), } /// A group of widget configuration items with a label #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct WidgetConfigGroup { /// Label for this group, can use `t::` prefix for translation pub label: ResourceText, /// Optional description or tooltip for this group pub description: Option, /// List of items or nested groups in this group pub items: Vec, } impl<'de> Deserialize<'de> for WidgetConfigDefinition { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] struct GroupVariant { group: WidgetConfigGroup, } let value = serde_json::Value::deserialize(deserializer).map_err(serde::de::Error::custom)?; // Try to deserialize as a group first if let Ok(parsed) = GroupVariant::deserialize(value.clone()) { return Ok(WidgetConfigDefinition::Group(parsed.group)); } // Otherwise deserialize as an item Ok(WidgetConfigDefinition::Item(Box::new( serde_json::from_value(value).map_err(serde::de::Error::custom)?, ))) } } /// Individual widget setting item with type-specific configuration #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(tag = "type")] pub enum WidgetSettingItem { /// Toggle switch for boolean values.\ /// Renders as a switch/toggle component in the UI. #[serde(alias = "switch")] Switch(WidgetSettingSwitch), /// Selection from a list of options.\ /// Can be rendered as a dropdown list or inline buttons depending on subtype. #[serde(alias = "select")] Select(WidgetSettingSelect), /// Text input field.\ /// Supports both single-line and multiline input with optional validation. #[serde(alias = "text", alias = "input-text")] InputText(WidgetSettingInputText), /// Numeric input field.\ /// Renders as a number input with optional min/max/step constraints. #[serde(alias = "number", alias = "input-number")] InputNumber(WidgetSettingInputNumber), /// Slider/range input for numeric values.\ /// Renders as a visual slider component. #[serde(alias = "range")] Range(WidgetSettingRange), /// Color picker input.\ /// Allows users to select colors with optional alpha/transparency support. #[serde(alias = "color")] Color(WidgetSettingColor), } impl WidgetSettingItem { /// Returns the unique key identifying this setting item pub fn get_key(&self) -> &str { match self { WidgetSettingItem::Switch(item) => &item.base.key, WidgetSettingItem::Select(item) => &item.base.key, WidgetSettingItem::InputText(item) => &item.base.key, WidgetSettingItem::InputNumber(item) => &item.base.key, WidgetSettingItem::Range(item) => &item.base.key, WidgetSettingItem::Color(item) => &item.base.key, } } } /// Common fields shared across all widget setting items #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WidgetSettingBase { /// Unique key for this setting, used to identify it in the configuration.\ /// Must be unique within the widget. Duplicates will be ignored. pub key: String, /// Label to display to the user pub label: ResourceText, /// Optional detailed description shown under the label pub description: Option, /// Optional tooltip icon with extra information pub tip: Option, /// Whether this setting can be configured per monitor in monitor-specific settings pub allow_set_by_monitor: bool, /// Keys of settings that must be enabled for this item to be active.\ /// Uses JavaScript truthy logic (!!value) to determine if dependency is met. pub dependencies: Vec, } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WidgetSettingSwitch { #[serde(flatten)] pub base: WidgetSettingBase, /// Default value for this switch pub default_value: bool, } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WidgetSettingSelect { #[serde(flatten)] pub base: WidgetSettingBase, /// Default selected value (must match one of the option values) pub default_value: String, /// List of available options pub options: Vec, /// How to render the select options pub subtype: WidgetSelectSubtype, } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WidgetSettingInputText { #[serde(flatten)] pub base: WidgetSettingBase, /// Default text value pub default_value: String, /// Whether to render as a multiline textarea pub multiline: bool, /// Minimum text length validation pub min_length: Option, /// Maximum text length validation pub max_length: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WidgetSettingInputNumber { #[serde(flatten)] pub base: WidgetSettingBase, /// Default numeric value pub default_value: f64, /// Minimum allowed value pub min: Option, /// Maximum allowed value pub max: Option, /// Step increment for input controls pub step: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WidgetSettingRange { #[serde(flatten)] pub base: WidgetSettingBase, /// Default value for the range slider pub default_value: f64, /// Minimum value of the range pub min: Option, /// Maximum value of the range pub max: Option, /// Step increment for the slider pub step: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WidgetSettingColor { #[serde(flatten)] pub base: WidgetSettingBase, /// Default color value (hex format: #RRGGBB or #RRGGBBAA) pub default_value: String, /// Whether to allow alpha/transparency channel pub allow_alpha: bool, } /// An option in a select widget setting #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct WidgetSelectOption { /// Optional React icon name to display with this option pub icon: Option, /// Label to display for this option (can use `t::` prefix) pub label: ResourceText, /// Value to store when this option is selected (must be unique) pub value: String, } /// Visual style for rendering select options #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WidgetSelectSubtype { /// Render as a dropdown list (default) #[default] List, /// Render as inline buttons/tabs Inline, } ================================================ FILE: libs/core/src/state/widget/interfaces.ts ================================================ export interface WidgetInformation { /** decoded webview label */ label: string; /** Will be present if the widget replicas is set to by monitor */ monitorId: string | null; /** Will be present if the widget replicas is set to multiple */ instanceId: string | null; /** params present on the webview label */ params: { readonly [key in string]?: string }; } export interface InitWidgetOptions { /** * Will auto size the widget to the content size of the element * @example * autoSizeByContent: document.body, * autoSizeByContent: document.getElementById("root"), * @default undefined */ autoSizeByContent?: HTMLElement | null; /** * Will save the position and size of the widget on change. * This is intedeed to be used when the size and position of the widget is * allowed to be changed by the user, Normally used on desktop widgets. * * @default widget.preset === "Desktop" */ saveAndRestoreLastRect?: boolean; /** * Will disable the css animations on the widget when performace mode is set to Extreme * * @default true */ disableCssAnimations?: boolean; } export interface ReadyWidgetOptions { /** * If show the widget on Ready * * @default !widget.lazy */ show?: boolean; } ================================================ FILE: libs/core/src/state/widget/mod.rs ================================================ pub mod context_menu; pub mod declaration; use std::{collections::HashMap, path::Path}; use declaration::WidgetSettingsDeclarationList; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use ts_rs::TS; use crate::{ error::Result, resource::{ResourceKind, ResourceMetadata, SluResource, WidgetId}, state::Plugin, system_state::MonitorId, utils::{search_resource_entrypoint, TsUnknown}, Point, }; #[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Widget { /// Resource id ex: `@seelen/weg` pub id: WidgetId, /// Optional icon to be used on settings. This have to be a valid react icon name.\ /// You can find all icons here: https://react-icons.github.io/react-icons/. pub icon: Option, /// Widget metadata, as texts, tags, images, etc. pub metadata: ResourceMetadata, /// Widget settings declaration, this is esentially a struct to be used by an /// builder to create the widget settings UI on the Settings window. pub settings: WidgetSettingsDeclarationList, /// Widget configuration preset pub preset: WidgetPreset, /// If true, the widget webview won't be created until it is requested via trigger action. pub lazy: bool, /// How many instances are allowed of this widget. pub instances: WidgetInstanceMode, /// If true, the widget will not be shown on the Settings Navigation as a Tab, but it will /// still be available on the widgets full list. pub hidden: bool, /// If true, the memory leak of webview2 (https://github.com/tauri-apps/tauri/issues/4026) /// workaround, will be no applied for instances of this widget. pub no_memory_leak_workaround: bool, /// Way to load the widget pub loader: WidgetLoader, /// Optional widget js code pub js: Option, /// Optional widget css pub css: Option, /// Optional widget html pub html: Option, /// Optional list of plugins to be installed side the widget. /// Use this if your widget needs interaction with other widgets like /// adding a context menu item that shows this widget. pub plugins: Vec, } impl SluResource for Widget { const KIND: ResourceKind = ResourceKind::Widget; fn metadata(&self) -> &ResourceMetadata { &self.metadata } fn metadata_mut(&mut self) -> &mut ResourceMetadata { &mut self.metadata } fn load_from_folder(path: &Path) -> Result { let file = search_resource_entrypoint(path).ok_or("No metadata file found")?; let mut widget = Self::load_from_file(&file)?; for stem in ["index.js", "main.js", "mod.js"] { if path.join(stem).exists() { widget.js = Some(std::fs::read_to_string(path.join(stem))?); break; } } for stem in ["index.css", "main.css", "mod.css"] { if path.join(stem).exists() { widget.css = Some(std::fs::read_to_string(path.join(stem))?); break; } } for stem in ["index.html", "main.html", "mod.html"] { if path.join(stem).exists() { widget.html = Some(std::fs::read_to_string(path.join(stem))?); break; } } Ok(widget) } fn validate(&self) -> Result<()> { if self.settings.there_are_duplicates() { return Err("Widget settings declaration have duplicated keys".into()); } for plugin in &self.plugins { plugin.validate()? } Ok(()) } fn sanitize(&mut self) { for plugin in &mut self.plugins { plugin.sanitize() } } } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WidgetInstanceMode { /// Default behavior, only one instance of this widget is allowed. /// This is useful for widgets intended to work as custom config window. #[default] Single, /// The widget is allowed to have multiple instances.\ /// This allow to the user manually create more instances of this same widget. Multiple, /// Seelen UI will create an instance of this widget per each monitor connected.\ /// This can be configured by the user using per monitor settings.\ ReplicaByMonitor, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WidgetLoader { /// Used for old internal widgets, similar to `Internal`. Legacy, /// Used for internal bundled widgets, this will load the code from internal resources. InternalReact, /// Used for internal bundled widgets, this will load the code from internal resources. Internal, /// Used for third party widgets, this will load the code from the `js`, `css`, and `html` fields #[default] ThirdParty, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WidgetPreset { /// No special behavior, all should be manually configured #[default] None, /// Always on bottom, no title bar, etc. Resizable by default. Desktop, /// Always on top, no title bar, etc. Overlay, /// Same as overlay, but will be automatically closed on unfocus; /// Also this type of widgets can be manually open/closed/show/hide by other widgets or plugins. /// On show the widget will be at the specified position, could be custom one, or will take the mouse cursor position. /// /// If a widget is of this type, the enabled property won't determine the visibility of the widget, /// as this widget is only shown when explicitly requested. /// /// Widget instances mode will be ignored for this type of widgets, As popups should be always single instance. Popup, } /// Arguments that could be passed on the trigger widget function, widgets decides if use it or not. #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export, optional_fields = nullable))] pub struct WidgetTriggerPayload { pub id: WidgetId, pub monitor_id: Option, pub instance_id: Option, /// Desired position to show the widget pub desired_position: Option, /// This will be used to align the widget at the desired position /// - start will set the widget at the left of point, /// - center will set the widget at the center of point, /// - end will set the widget at the right of point pub align_x: Option, /// This will be used to align the widget at the desired position /// - start will set the widget at the top of point, /// - center will set the widget at the center of point, /// - end will set the widget at the bottom of point pub align_y: Option, /// Custom arguments to be used by the widget recieving the trigger. /// this can be anything, and depends on the widget to evaluate them. pub custom_args: Option>, } impl WidgetTriggerPayload { pub fn new(id: WidgetId) -> Self { Self { id, monitor_id: None, instance_id: None, desired_position: None, align_x: None, align_y: None, custom_args: None, } } pub fn add_custom_arg(&mut self, key: impl AsRef, value: impl Into) { if self.custom_args.is_none() { self.custom_args = Some(HashMap::new()); } self.custom_args .as_mut() .unwrap() .insert(key.as_ref().to_string(), value.into()); } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum Alignment { Start, Center, End, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export, repr(enum = name)))] pub enum WidgetStatus { /// Widget has been registered, but not yet loaded Pending, /// Webview window is being created Creating, /// Widget javascript, html and css is being loaded Mounting, /// Widget loaded and is ready Ready, /// Webview window failed to be created. CrashedOnCreation, } ================================================ FILE: libs/core/src/state/widget/mod.ts ================================================ import { type Frame, type Rect, type ThirdPartyWidgetSettings, type Widget as IWidget, type WidgetConfigDefinition, type WidgetId, WidgetPreset, type WidgetSettingItem, WidgetStatus, type WidgetTriggerPayload, } from "@seelen-ui/types"; import { invoke, SeelenCommand, SeelenEvent } from "../../handlers/mod.ts"; import { decodeBase64Url } from "@std/encoding"; import { debounce } from "../../utils/async.ts"; import { WidgetAutoSizer } from "./sizing.ts"; import { adjustPositionByPlacement, fitIntoMonitor, initMonitorsState } from "./positioning.ts"; import { startThemingTool } from "../theme/theming.ts"; import type { InitWidgetOptions, ReadyWidgetOptions, WidgetInformation } from "./interfaces.ts"; import { disableAnimationsOnPerformanceMode } from "./performance.ts"; import { getCurrentWebview, type Webview } from "@tauri-apps/api/webview"; import { getCurrentWindow, type Window } from "@tauri-apps/api/window"; interface WidgetInternalState { hwnd: number; initialized: boolean; ready: boolean; position: { x: number; y: number; }; size: { width: number; height: number; }; firstFocus: boolean; } /** * Represents the widget instance running in the current webview */ export class Widget { /** * Alternative accesor for the current running widget.\ * Will throw if the library is being used on a non Seelen UI environment */ static getCurrent(): Widget { const scope = globalThis as ExtendedGlobalThis; if (!scope.__SLU_WIDGET) { throw new Error("The library is being used on a non Seelen UI environment"); } return ( scope.__SLU_WIDGET_INSTANCE || (scope.__SLU_WIDGET_INSTANCE = new Widget(scope.__SLU_WIDGET)) ); } /** The current running widget */ static get self(): Widget { return Widget.getCurrent(); } /** widget id */ public readonly id: WidgetId; /** widget definition */ public readonly def: IWidget; /** decoded widget instance information */ public readonly decoded: WidgetInformation; /** current webview where the widget is running */ public readonly webview: Webview; /** current window where the widget is running */ public readonly window: Window; private autoSizer?: WidgetAutoSizer; private runtimeState: WidgetInternalState = { hwnd: 0, initialized: false, ready: false, position: { x: 0, y: 0, }, size: { width: 0, height: 0, }, firstFocus: true, }; private constructor(widget: IWidget) { this.def = widget; this.webview = getCurrentWebview(); this.window = getCurrentWindow(); const [id, query] = getDecodedWebviewLabel(); const params = new URLSearchParams(query); const paramsObj = Object.freeze(Object.fromEntries(params)); this.id = id as WidgetId; this.decoded = Object.freeze({ label: `${id}${query ? `?${query}` : ""}`, monitorId: paramsObj.monitorId || null, instanceId: paramsObj.instanceId || null, params: Object.freeze(Object.fromEntries(params)), }); } /** Returns the current window id of the widget */ get windowId(): number { return this.runtimeState.hwnd; } /** Returns the current position and size of the widget */ get frame(): Frame { return { x: this.runtimeState.position.x, y: this.runtimeState.position.y, width: this.runtimeState.size.width, height: this.runtimeState.size.height, }; } /** Returns the default config of the widget, declared on the widget definition */ public getDefaultConfig(): ThirdPartyWidgetSettings { const config: ThirdPartyWidgetSettings = { enabled: true }; for (const definition of this.def.settings) { Object.assign(config, getDefinitionDefaultValues(definition)); } return config; } private applyInvisiblePreset(): Array> { return [ this.window.setDecorations(false), // no title bar this.window.setShadow(false), // no shadows // hide from native shell this.window.setSkipTaskbar(true), // as a (desktop/overlay) widget we don't wanna allow nothing of these this.window.setMinimizable(false), this.window.setMaximizable(false), this.window.setClosable(false), ]; } /** Will apply the recommended settings for a desktop widget */ private async applyDesktopPreset(): Promise { await Promise.all([...this.applyInvisiblePreset(), this.window.setAlwaysOnBottom(true)]); } /** Will apply the recommended settings for an overlay widget */ private async applyOverlayPreset(): Promise { await Promise.all([...this.applyInvisiblePreset(), this.window.setAlwaysOnTop(true)]); } /** Will apply the recommended settings for a popup widget */ private async applyPopupPreset(): Promise { await Promise.all([...this.applyInvisiblePreset(), this.window.setAlwaysOnTop(true)]); const hideWidget = debounce(() => { this.hide(true); }, 100); this.window.onFocusChanged(({ payload: focused }) => { if (focused) { hideWidget.cancel(); } else { hideWidget(); } }); this.onTrigger(async ({ desiredPosition, alignX, alignY }) => { // avoid flickering when clicking a button that triggers the widget hideWidget.cancel(); if (this.autoSizer && alignX && alignY) { this.autoSizer.originX = alignX; this.autoSizer.originY = alignY; } if (desiredPosition) { const adjusted = adjustPositionByPlacement({ frame: { x: desiredPosition.x, y: desiredPosition.y, width: this.runtimeState.size.width, height: this.runtimeState.size.height, }, originX: alignX, originY: alignY, }); await this.setPosition({ left: adjusted.x, top: adjusted.y, right: adjusted.x + adjusted.width, bottom: adjusted.y + adjusted.height, }); } await this.show(); await this.focus(); }); } /** * Will restore the saved position and size of the widget on start, * after that will store the position and size of the widget on change. */ public async persistPositionAndSize(): Promise { const storage = globalThis.window.localStorage; const [x, y, width, height] = [`x`, `y`, `width`, `height`].map((k) => storage.getItem(`${k}`)); if (x && y && width && height) { const frame = { x: Number(x), y: Number(y), width: Number(width), height: Number(height), }; const safeFrame = fitIntoMonitor(frame); await this.setPosition({ left: safeFrame.x, top: safeFrame.y, right: safeFrame.x + safeFrame.width, bottom: safeFrame.y + safeFrame.height, }); } this.window.onMoved( debounce((e) => { const { x, y } = e.payload; storage.setItem(`x`, x.toString()); storage.setItem(`y`, y.toString()); console.info(`Widget position saved: ${x} ${y}`); }, 500), ); this.window.onResized( debounce((e) => { const { width, height } = e.payload; storage.setItem(`width`, width.toString()); storage.setItem(`height`, height.toString()); console.info(`Widget size saved: ${width} ${height}`); }, 500), ); } /** * Will initialize the widget based on the preset and mark it as `pending`, this function won't show the widget. * This should be called before any other action on the widget. After this you should call * `ready` to mark the widget as ready and show it. */ public async init(options: InitWidgetOptions = {}): Promise { if (this.runtimeState.initialized) { console.warn(`Widget already initialized`); return; } this.runtimeState.hwnd = await invoke(SeelenCommand.GetSelfWindowId); this.runtimeState.initialized = true; if (options.autoSizeByContent) { this.autoSizer = new WidgetAutoSizer(this, options.autoSizeByContent); } else if (options.saveAndRestoreLastRect ?? this.def.preset === WidgetPreset.Desktop) { await this.persistPositionAndSize(); } switch (this.def.preset) { case WidgetPreset.None: break; case WidgetPreset.Desktop: await this.applyDesktopPreset(); break; case WidgetPreset.Overlay: await this.applyOverlayPreset(); break; case WidgetPreset.Popup: await this.applyPopupPreset(); break; } await startThemingTool(); await initMonitorsState(); if (options.disableCssAnimations ?? true) { await disableAnimationsOnPerformanceMode(); } else { console.trace("Animations won't be disabled because widget configuration"); } // state initialization this.runtimeState.size = await this.window.outerSize(); this.runtimeState.position = await this.window.outerPosition(); this.window.onResized((e) => { this.runtimeState.size.width = e.payload.width; this.runtimeState.size.height = e.payload.height; }); this.window.onMoved((e) => { this.runtimeState.position.x = e.payload.x; this.runtimeState.position.y = e.payload.y; }); } /** * Will mark the widget as `ready` and pool pending triggers. * * If the widget is not lazy this will inmediately show the widget. * Lazy widget should be shown on trigger action. */ public async ready(options: ReadyWidgetOptions = {}): Promise { const { show = !this.def.lazy } = options; if (!this.runtimeState.initialized) { throw new Error(`Widget was not initialized before ready`); } if (this.runtimeState.ready) { console.warn(`Widget is already ready`); return; } this.runtimeState.ready = true; await this.autoSizer?.execute(); if (show && !(await this.window.isVisible())) { await this.show(); await this.focus(); } // this will mark the widget as ready, and send pending trigger event if exists await invoke(SeelenCommand.SetCurrentWidgetStatus, { status: WidgetStatus.Ready }); } public onTrigger(cb: (args: WidgetTriggerPayload) => void): void { this.webview.listen(SeelenEvent.WidgetTriggered, ({ payload }) => { cb(payload); }); } /** If animations are enabled this will animate the movement of the widget */ public setPosition(rect: Rect): Promise { this.runtimeState.position.x = rect.left; this.runtimeState.position.y = rect.top; this.runtimeState.size.width = rect.right - rect.left; this.runtimeState.size.height = rect.bottom - rect.top; return invoke(SeelenCommand.SetSelfPosition, { rect: { left: Math.round(rect.left), top: Math.round(rect.top), right: Math.round(rect.right), bottom: Math.round(rect.bottom), }, }); } public async show(): Promise { debouncedClose.cancel(); await this.window.show(); } /** Will force foreground the widget */ public async focus(): Promise { if (this.runtimeState.firstFocus) { await getCurrentWebview().setFocus(); this.runtimeState.firstFocus = false; } await invoke(SeelenCommand.RequestFocus, { hwnd: this.runtimeState.hwnd }).catch(() => { console.warn(`Failed to focus widget: ${this.decoded.label}`); }); } public hide(closeAfterInactivity?: boolean): void { this.window.hide(); if (closeAfterInactivity) { debouncedClose(); } } } const debouncedClose = debounce(() => { Widget.self.window.close(); }, 30_000); type ExtendedGlobalThis = typeof globalThis & { __SLU_WIDGET?: IWidget; __SLU_WIDGET_INSTANCE?: Widget; }; export const SeelenSettingsWidgetId: WidgetId = "@seelen/settings" as WidgetId; export const SeelenPopupWidgetId: WidgetId = "@seelen/popup" as WidgetId; export const SeelenWegWidgetId: WidgetId = "@seelen/weg" as WidgetId; export const SeelenToolbarWidgetId: WidgetId = "@seelen/fancy-toolbar" as WidgetId; export const SeelenWindowManagerWidgetId: WidgetId = "@seelen/window-manager" as WidgetId; export const SeelenWallWidgetId: WidgetId = "@seelen/wallpaper-manager" as WidgetId; function getDecodedWebviewLabel(): [WidgetId, string | undefined] { const encondedLabel = getCurrentWebview().label; const decodedLabel = new TextDecoder().decode(decodeBase64Url(encondedLabel)); const [id, query] = decodedLabel.split("?"); if (!id) { throw new Error("Missing widget id on webview label"); } return [id as WidgetId, query]; } function getDefinitionDefaultValues(definition: WidgetConfigDefinition): Record { const config: Record = {}; // Check if it's a group (has "group" property) if ("group" in definition) { // Recursively process all items in the group for (const item of definition.group.items) { Object.assign(config, getDefinitionDefaultValues(item)); } } else { // It's a setting item, extract key and defaultValue const item = definition as WidgetSettingItem; if ("key" in item && "defaultValue" in item) { config[item.key] = item.defaultValue; } } return config; } ================================================ FILE: libs/core/src/state/widget/performance.ts ================================================ import { PerformanceMode } from "@seelen-ui/types"; import { invoke, SeelenCommand, SeelenEvent, subscribe } from "../../handlers/mod.ts"; export async function disableAnimationsOnPerformanceMode(): Promise { const initial = await invoke(SeelenCommand.StateGetPerformanceMode); setDisableAnimations(initial); subscribe(SeelenEvent.StatePerformanceModeChanged, (e) => { setDisableAnimations(e.payload); }); } function setDisableAnimations(mode: PerformanceMode): void { const root = document.documentElement; if (mode === PerformanceMode.Extreme) { root.setAttribute("data-animations-off", ""); } else { root.removeAttribute("data-animations-off"); } } ================================================ FILE: libs/core/src/state/widget/positioning.ts ================================================ import { Alignment, type Frame, type PhysicalMonitor } from "@seelen-ui/types"; import { invoke, subscribe } from "../../handlers/mod.ts"; import { SeelenCommand } from "@seelen-ui/lib"; import { SeelenEvent } from "../../handlers/events.ts"; interface args { frame: Frame; originX?: Alignment | null; originY?: Alignment | null; } const monitors = { value: [] as PhysicalMonitor[], }; export async function initMonitorsState(): Promise { monitors.value = await invoke(SeelenCommand.SystemGetMonitors); subscribe(SeelenEvent.SystemMonitorsChanged, ({ payload }) => { monitors.value = payload; }); } function monitorFromPoint(x: number, y: number): PhysicalMonitor | undefined { return monitors.value.find( (m) => m.rect.left <= x && x < m.rect.right && m.rect.top <= y && y < m.rect.bottom, ); } function primaryMonitor(): PhysicalMonitor | undefined { return monitors.value.find((m) => m.isPrimary); } export function adjustPositionByPlacement({ frame: { x, y, width, height }, originX, originY, }: args): Frame { if (originX === Alignment.Center) { x -= width / 2; } else if (originX === Alignment.End) { x -= width; } if (originY === Alignment.Center) { y -= height / 2; } else if (originY === Alignment.End) { y -= height; } const newFrame = fitIntoMonitor({ x, y, width, height }); return { x: Math.round(newFrame.x), y: Math.round(newFrame.y), width: Math.round(newFrame.width), height: Math.round(newFrame.height), }; } export function fitIntoMonitor({ x, y, width, height }: Frame): Frame { const monitor = monitorFromPoint(Math.round(x), Math.round(y)) || primaryMonitor(); if (monitor) { width = Math.min(width, monitor.rect.right - monitor.rect.left); height = Math.min(height, monitor.rect.bottom - monitor.rect.top); const x2 = x + width; const y2 = y + height; // check left edge if (x < monitor.rect.left) { x = monitor.rect.left; } // check top edge if (y < monitor.rect.top) { y = monitor.rect.top; } // check right edge if (x2 > monitor.rect.right) { x = monitor.rect.right - width; } // check bottom edge if (y2 > monitor.rect.bottom) { y = monitor.rect.bottom - height; } } return { x, y, width, height, }; } ================================================ FILE: libs/core/src/state/widget/sizing.ts ================================================ import { PhysicalSize } from "@tauri-apps/api/dpi"; import type { Widget } from "./mod.ts"; import { Alignment } from "@seelen-ui/types"; export class WidgetAutoSizer { /** From which side the widget will grow */ originX: Alignment = Alignment.Start; /** From which side the widget will grow */ originY: Alignment = Alignment.Start; constructor( private widget: Widget, private element: HTMLElement, ) { this.execute = this.execute.bind(this); this.setup(); } private setup(): () => void { // Disable resizing by the user this.widget.window.setResizable(false); const observer = new ResizeObserver(this.execute); observer.observe(this.element, { box: "border-box", }); return () => { observer.disconnect(); }; } async execute(): Promise { const { x, y, width, height } = this.widget.frame; const frame = { x, y, width: Math.ceil(this.element.scrollWidth * globalThis.window.devicePixelRatio), height: Math.ceil(this.element.scrollHeight * globalThis.window.devicePixelRatio), }; const widthDiff = frame.width - width; const heightDiff = frame.height - height; // Only update if the difference is more than 1px (avoid infinite loops from decimal differences) if (widthDiff === 0 && heightDiff === 0) { return; } console.trace(`Auto resize from ${width}x${height} to ${frame.width}x${frame.height}`); if (this.originX === Alignment.Center) { frame.x -= widthDiff / 2; } else if (this.originX === Alignment.End) { frame.x -= widthDiff; } if (this.originY === Alignment.Center) { frame.y -= heightDiff / 2; } else if (this.originY === Alignment.End) { frame.y -= heightDiff; } // only update size no position on this case if (frame.x === x && frame.y === y) { await this.widget.window.setSize(new PhysicalSize(frame.width, frame.height)); return; } await this.widget.setPosition({ left: frame.x, top: frame.y, right: frame.x + frame.width, bottom: frame.y + frame.height, }); } } ================================================ FILE: libs/core/src/state/wm_layout.rs ================================================ use std::{cell::Cell, collections::HashMap}; use crate::system_state::MonitorId; #[derive(Debug, Serialize, Deserialize, JsonSchema, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct WmRenderTree(pub HashMap); #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct WmNode { /// Type determines the behavior of the node #[serde(rename = "type")] pub kind: WmNodeKind, /// Lifetime of the node pub lifetime: WmNodeLifetime, /// Order in how the tree will be traversed (1 = first, 2 = second, etc.) pub priority: u32, /// How much of the remaining space this node will take pub grow_factor: Cell, /// Math Condition for the node to be shown, e.g: n >= 3 pub condition: Option, /// Active window handle (HWND) in the node. #[serde(skip_deserializing)] pub active: Option, /// Window handles (HWND) in the node. #[serde(skip_deserializing)] pub windows: Vec, /// Child nodes, this field is ignored for leaf and stack nodes. pub children: Vec, /// Max amount of windows in the stack. Set it to `null` for unlimited stack.\ /// This field is ignored for non-stack nodes pub max_stack_size: Option, } unsafe impl Send for WmNode {} unsafe impl Sync for WmNode {} impl WmNode { pub fn len(&self) -> usize { match self.kind { WmNodeKind::Leaf | WmNodeKind::Stack => self.windows.len(), WmNodeKind::Vertical | WmNodeKind::Horizontal => { self.children.iter().map(|n| n.len()).sum() } } } pub fn capacity(&self) -> usize { match self.kind { WmNodeKind::Leaf => 1usize, WmNodeKind::Stack => self.max_stack_size.unwrap_or(usize::MAX), WmNodeKind::Vertical | WmNodeKind::Horizontal => { let mut total = 0usize; for n in &self.children { total = total.saturating_add(n.capacity()); } total } } } pub fn is_full(&self) -> bool { match self.kind { WmNodeKind::Leaf => !self.windows.is_empty(), WmNodeKind::Stack => self.max_stack_size.is_some_and(|max| self.len() >= max), WmNodeKind::Vertical | WmNodeKind::Horizontal => { self.children.iter().all(|n| n.is_full()) } } } pub fn is_empty(&self) -> bool { self.len() == 0 } } impl Default for WmNode { fn default() -> Self { Self { kind: WmNodeKind::Leaf, lifetime: WmNodeLifetime::Permanent, priority: 1, grow_factor: Cell::new(1.0), condition: None, active: None, windows: Vec::new(), children: Vec::new(), max_stack_size: Some(3), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WmNodeKind { /// node will not grow, this is the final node. Leaf, /// node will grow on z-axis Stack, /// node will grow on y-axis Vertical, /// node will grow on x-axis Horizontal, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(repr(enum = name))] pub enum WmNodeLifetime { Temporal, #[default] Permanent, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct WindowManagerLayout { pub structure: WmNode, #[serde(skip_deserializing)] pub floating_windows: Vec, } impl Default for WindowManagerLayout { fn default() -> Self { Self { structure: WmNode { kind: WmNodeKind::Stack, max_stack_size: None, ..Default::default() }, floating_windows: Vec::new(), } } } ================================================ FILE: libs/core/src/state/workspaces/mod.rs ================================================ use std::collections::{HashMap, HashSet}; use uuid::Uuid; use crate::{error::Result, identifier_impl, resource::WallpaperId, system_state::MonitorId}; #[derive(Debug, Default, Clone, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct VirtualDesktops { /// Workspaces per monitor pub monitors: HashMap, /// pinned windows will be not affected by switching workspaces pub pinned: Vec, } impl VirtualDesktops { pub fn sanitize(&mut self) { let mut seen = HashSet::new(); self.pinned.retain(|x| seen.insert(*x)); for monitor in self.monitors.values_mut() { monitor.sanitize(); for workspace in &mut monitor.workspaces { workspace.windows.retain(|x| seen.insert(*x)); } } } } #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct VirtualDesktopMonitor { pub workspaces: Vec, active_workspace: WorkspaceId, } impl VirtualDesktopMonitor { pub fn create() -> Self { let workspace = DesktopWorkspace::create(); let current_workspace = workspace.id.clone(); Self { workspaces: vec![workspace], active_workspace: current_workspace, } } pub fn sanitize(&mut self) { if self.workspaces.is_empty() { let workspace = DesktopWorkspace::create(); self.active_workspace = workspace.id.clone(); self.workspaces.push(workspace); } if !self .workspaces .iter() .any(|ws| ws.id == self.active_workspace) { self.active_workspace = self.workspaces[0].id.clone(); } } pub fn active_workspace_id(&self) -> &WorkspaceId { &self.active_workspace } pub fn active_workspace(&self) -> &DesktopWorkspace { self.workspaces .iter() .find(|w| w.id == self.active_workspace) .expect("current workspace not found") } pub fn active_workspace_mut(&mut self) -> &mut DesktopWorkspace { self.workspaces .iter_mut() .find(|w| w.id == self.active_workspace) .expect("current workspace not found") } /// Set the current workspace, return error if the workspace doesn't exist pub fn set_active_workspace(&mut self, workspace_id: &WorkspaceId) -> Result<()> { if self.workspaces.iter().any(|w| &w.id == workspace_id) { self.active_workspace = workspace_id.clone(); Ok(()) } else { Err("Invalid workspace id".into()) } } /// Add a new workspace and return its id pub fn add_workspace(&mut self) -> WorkspaceId { let workspace = DesktopWorkspace::create(); let workspace_id = workspace.id.clone(); self.workspaces.push(workspace); workspace_id } /// Remove a workspace by id /// - Does nothing if there's only 1 workspace (minimum required) /// - If the removed workspace was current, switches to the side one /// - Moves all windows from the removed workspace to the side one in the array pub fn remove_workspace(&mut self, workspace_id: &WorkspaceId) -> Result<()> { // Don't remove if it's the only workspace if self.workspaces.len() <= 1 { return Err("Cannot remove the last workspace".into()); } // Find the index of the workspace to remove let idx_to_delete = self .workspaces .iter() .position(|w| &w.id == workspace_id) .ok_or("Workspace not found")?; let idx_to_move = if idx_to_delete == 0 { 1 } else { idx_to_delete - 1 }; // If the removed workspace was current, switch to the previous one if &self.active_workspace == workspace_id { self.active_workspace = self.workspaces[idx_to_move].id.clone(); } // Move windows to the side workspace let windows = self.workspaces[idx_to_delete].windows.clone(); for window in windows { self.workspaces[idx_to_move].windows.push(window); } self.workspaces.remove(idx_to_delete); Ok(()) } /// Rename a workspace by id pub fn rename_workspace( &mut self, workspace_id: &WorkspaceId, name: Option, ) -> Result<()> { let workspace = self .workspaces .iter_mut() .find(|w| &w.id == workspace_id) .ok_or("Workspace not found")?; workspace.name = name; Ok(()) } } #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct DesktopWorkspace { pub id: WorkspaceId, pub name: Option, /// react-icon icon name pub icon: Option, pub wallpaper: Option, #[serde(default)] pub windows: Vec, } impl DesktopWorkspace { pub fn create() -> Self { Self { id: WorkspaceId(Uuid::new_v4().to_string()), name: None, icon: None, wallpaper: None, windows: Vec::new(), } } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] pub struct WorkspaceId(pub String); identifier_impl!(WorkspaceId, String); ================================================ FILE: libs/core/src/system_state/bluetooth/appearance_values.yml ================================================ # https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/core/appearance_values.yaml appearance_values: - category: 0x000 name: Unknown - category: 0x001 name: Phone - category: 0x002 name: Computer subcategory: - value: 0x01 name: Desktop Workstation - value: 0x02 name: Server-class Computer - value: 0x03 name: Laptop - value: 0x04 name: Handheld PC/PDA (clamshell) - value: 0x05 name: Palm-size PC/PDA - value: 0x06 name: Wearable computer (watch size) - value: 0x07 name: Tablet - value: 0x08 name: Docking Station - value: 0x09 name: All in One - value: 0x0A name: Blade Server - value: 0x0B name: Convertible - value: 0x0C name: Detachable - value: 0x0D name: IoT Gateway - value: 0x0E name: Mini PC - value: 0x0F name: Stick PC - category: 0x003 name: Watch subcategory: - value: 0x01 name: Sports Watch - value: 0x02 name: Smartwatch - category: 0x004 name: Clock - category: 0x005 name: Display - category: 0x006 name: Remote Control - category: 0x007 name: Eye-glasses - category: 0x008 name: Tag - category: 0x009 name: Keyring - category: 0x00A name: Media Player - category: 0x00B name: Barcode Scanner - category: 0x00C name: Thermometer subcategory: - value: 0x01 name: Ear Thermometer - category: 0x00D name: Heart Rate Sensor subcategory: - value: 0x01 name: Heart Rate Belt - category: 0x00E name: Blood Pressure subcategory: - value: 0x01 name: Arm Blood Pressure - value: 0x02 name: Wrist Blood Pressure - category: 0x00F name: Human Interface Device subcategory: - value: 0x01 name: Keyboard - value: 0x02 name: Mouse - value: 0x03 name: Joystick - value: 0x04 name: Gamepad - value: 0x05 name: Digitizer Tablet - value: 0x06 name: Card Reader - value: 0x07 name: Digital Pen - value: 0x08 name: Barcode Scanner - value: 0x09 name: Touchpad - value: 0x0A name: Presentation Remote - category: 0x010 name: Glucose Meter - category: 0x011 name: Running Walking Sensor subcategory: - value: 0x01 name: In-Shoe Running Walking Sensor - value: 0x02 name: On-Shoe Running Walking Sensor - value: 0x03 name: On-Hip Running Walking Sensor - category: 0x012 name: Cycling subcategory: - value: 0x01 name: Cycling Computer - value: 0x02 name: Speed Sensor - value: 0x03 name: Cadence Sensor - value: 0x04 name: Power Sensor - value: 0x05 name: Speed and Cadence Sensor - category: 0x013 name: Control Device subcategory: - value: 0x01 name: Switch - value: 0x02 name: Multi-switch - value: 0x03 name: Button - value: 0x04 name: Slider - value: 0x05 name: Rotary Switch - value: 0x06 name: Touch Panel - value: 0x07 name: Single Switch - value: 0x08 name: Double Switch - value: 0x09 name: Triple Switch - value: 0x0A name: Battery Switch - value: 0x0B name: Energy Harvesting Switch - value: 0x0C name: Push Button - value: 0x0D name: Dial - category: 0x014 name: Network Device subcategory: - value: 0x01 name: Access Point - value: 0x02 name: Mesh Device - value: 0x03 name: Mesh Network Proxy - category: 0x015 name: Sensor subcategory: - value: 0x01 name: Motion Sensor - value: 0x02 name: Air quality Sensor - value: 0x03 name: Temperature Sensor - value: 0x04 name: Humidity Sensor - value: 0x05 name: Leak Sensor - value: 0x06 name: Smoke Sensor - value: 0x07 name: Occupancy Sensor - value: 0x08 name: Contact Sensor - value: 0x09 name: Carbon Monoxide Sensor - value: 0x0A name: Carbon Dioxide Sensor - value: 0x0B name: Ambient Light Sensor - value: 0x0C name: Energy Sensor - value: 0x0D name: Color Light Sensor - value: 0x0E name: Rain Sensor - value: 0x0F name: Fire Sensor - value: 0x10 name: Wind Sensor - value: 0x11 name: Proximity Sensor - value: 0x12 name: Multi-Sensor - value: 0x13 name: Flush Mounted Sensor - value: 0x14 name: Ceiling Mounted Sensor - value: 0x15 name: Wall Mounted Sensor - value: 0x16 name: Multisensor - value: 0x17 name: Energy Meter - value: 0x18 name: Flame Detector - value: 0x19 name: Vehicle Tire Pressure Sensor - category: 0x016 name: Light Fixtures subcategory: - value: 0x01 name: Wall Light - value: 0x02 name: Ceiling Light - value: 0x03 name: Floor Light - value: 0x04 name: Cabinet Light - value: 0x05 name: Desk Light - value: 0x06 name: Troffer Light - value: 0x07 name: Pendant Light - value: 0x08 name: In-ground Light - value: 0x09 name: Flood Light - value: 0x0A name: Underwater Light - value: 0x0B name: Bollard with Light - value: 0x0C name: Pathway Light - value: 0x0D name: Garden Light - value: 0x0E name: Pole-top Light - value: 0x0F name: Spotlight - value: 0x10 name: Linear Light - value: 0x11 name: Street Light - value: 0x12 name: Shelves Light - value: 0x13 name: Bay Light - value: 0x14 name: Emergency Exit Light - value: 0x15 name: Light Controller - value: 0x16 name: Light Driver - value: 0x17 name: Bulb - value: 0x18 name: Low-bay Light - value: 0x19 name: High-bay Light - category: 0x017 name: Fan subcategory: - value: 0x01 name: Ceiling Fan - value: 0x02 name: Axial Fan - value: 0x03 name: Exhaust Fan - value: 0x04 name: Pedestal Fan - value: 0x05 name: Desk Fan - value: 0x06 name: Wall Fan - category: 0x018 name: HVAC subcategory: - value: 0x01 name: Thermostat - value: 0x02 name: Humidifier - value: 0x03 name: De-humidifier - value: 0x04 name: Heater - value: 0x05 name: Radiator - value: 0x06 name: Boiler - value: 0x07 name: Heat Pump - value: 0x08 name: Infrared Heater - value: 0x09 name: Radiant Panel Heater - value: 0x0A name: Fan Heater - value: 0x0B name: Air Curtain - category: 0x019 name: Air Conditioning - category: 0x01A name: Humidifier - category: 0x01B name: Heating subcategory: - value: 0x01 name: Radiator - value: 0x02 name: Boiler - value: 0x03 name: Heat Pump - value: 0x04 name: Infrared Heater - value: 0x05 name: Radiant Panel Heater - value: 0x06 name: Fan Heater - value: 0x07 name: Air Curtain - category: 0x01C name: Access Control subcategory: - value: 0x01 name: Access Door - value: 0x02 name: Garage Door - value: 0x03 name: Emergency Exit Door - value: 0x04 name: Access Lock - value: 0x05 name: Elevator - value: 0x06 name: Window - value: 0x07 name: Entrance Gate - value: 0x08 name: Door Lock - value: 0x09 name: Locker - category: 0x01D name: Motorized Device subcategory: - value: 0x01 name: Motorized Gate - value: 0x02 name: Awning - value: 0x03 name: Blinds or Shades - value: 0x04 name: Curtains - value: 0x05 name: Screen - category: 0x01E name: Power Device subcategory: - value: 0x01 name: Power Outlet - value: 0x02 name: Power Strip - value: 0x03 name: Plug - value: 0x04 name: Power Supply - value: 0x05 name: LED Driver - value: 0x06 name: Fluorescent Lamp Gear - value: 0x07 name: HID Lamp Gear - value: 0x08 name: Charge Case - value: 0x09 name: Power Bank - category: 0x01F name: Light Source subcategory: - value: 0x01 name: Incandescent Light Bulb - value: 0x02 name: LED Lamp - value: 0x03 name: HID Lamp - value: 0x04 name: Fluorescent Lamp - value: 0x05 name: LED Array - value: 0x06 name: Multi-Color LED Array - value: 0x07 name: Low voltage halogen - value: 0x08 name: Organic light emitting diode (OLED) - category: 0x020 name: Window Covering subcategory: - value: 0x01 name: Window Shades - value: 0x02 name: Window Blinds - value: 0x03 name: Window Awning - value: 0x04 name: Window Curtain - value: 0x05 name: Exterior Shutter - value: 0x06 name: Exterior Screen - category: 0x021 name: Audio Sink subcategory: - value: 0x01 name: Standalone Speaker - value: 0x02 name: Soundbar - value: 0x03 name: Bookshelf Speaker - value: 0x04 name: Standmounted Speaker - value: 0x05 name: Speakerphone - category: 0x022 name: Audio Source subcategory: - value: 0x01 name: Microphone - value: 0x02 name: Alarm - value: 0x03 name: Bell - value: 0x04 name: Horn - value: 0x05 name: Broadcasting Device - value: 0x06 name: Service Desk - value: 0x07 name: Kiosk - value: 0x08 name: Broadcasting Room - value: 0x09 name: Auditorium - category: 0x023 name: Motorized Vehicle subcategory: - value: 0x01 name: Car - value: 0x02 name: Large Goods Vehicle - value: 0x03 name: Vehicle 2-Wheels - value: 0x04 name: Motorbike - value: 0x05 name: Scooter - value: 0x06 name: Moped - value: 0x07 name: Vehicle 3-Wheels - value: 0x08 name: Light Vehicle - value: 0x09 name: Quad Bike - value: 0x0A name: Minibus - value: 0x0B name: Bus - value: 0x0C name: Trolley - value: 0x0D name: Agricultural Vehicle - value: 0x0E name: Camper / Caravan - value: 0x0F name: Recreational Vehicle / Motor Home - category: 0x024 name: Domestic Appliance subcategory: - value: 0x01 name: Refrigerator - value: 0x02 name: Freezer - value: 0x03 name: Oven - value: 0x04 name: Microwave - value: 0x05 name: Toaster - value: 0x06 name: Washing Machine - value: 0x07 name: Dryer - value: 0x08 name: Coffee maker - value: 0x09 name: Clothes iron - value: 0x0A name: Curling iron - value: 0x0B name: Hair dryer - value: 0x0C name: Vacuum cleaner - value: 0x0D name: Robotic vacuum cleaner - value: 0x0E name: Rice cooker - value: 0x0F name: Clothes steamer - category: 0x025 name: Wearable Audio Device subcategory: - value: 0x01 name: Earbud - value: 0x02 name: Headset - value: 0x03 name: Headphones - value: 0x04 name: Neck Band - category: 0x026 name: Aircraft subcategory: - value: 0x01 name: Light Aircraft - value: 0x02 name: Microlight - value: 0x03 name: Paraglider - value: 0x04 name: Large Passenger Aircraft - category: 0x027 name: AV Equipment subcategory: - value: 0x01 name: Amplifier - value: 0x02 name: Receiver - value: 0x03 name: Radio - value: 0x04 name: Tuner - value: 0x05 name: Turntable - value: 0x06 name: CD Player - value: 0x07 name: DVD Player - value: 0x08 name: Bluray Player - value: 0x09 name: Optical Disc Player - value: 0x0A name: Set-Top Box - category: 0x028 name: Display Equipment subcategory: - value: 0x01 name: Television - value: 0x02 name: Monitor - value: 0x03 name: Projector - category: 0x029 name: Hearing aid subcategory: - value: 0x01 name: In-ear hearing aid - value: 0x02 name: Behind-ear hearing aid - value: 0x03 name: Cochlear Implant - category: 0x02A name: Gaming subcategory: - value: 0x01 name: Home Video Game Console - value: 0x02 name: Portable handheld console - category: 0x02B name: Signage subcategory: - value: 0x01 name: Digital Signage - value: 0x02 name: Electronic Label - category: 0x031 name: Pulse Oximeter subcategory: - value: 0x01 name: Fingertip Pulse Oximeter - value: 0x02 name: Wrist Worn Pulse Oximeter - category: 0x032 name: Weight Scale - category: 0x033 name: Personal Mobility Device subcategory: - value: 0x01 name: Powered Wheelchair - value: 0x02 name: Mobility Scooter - category: 0x034 name: Continuous Glucose Monitor - category: 0x035 name: Insulin Pump subcategory: - value: 0x01 name: "Insulin Pump, durable pump" - value: 0x04 name: "Insulin Pump, patch pump" - value: 0x08 name: Insulin Pen - category: 0x036 name: Medication Delivery - category: 0x037 name: Spirometer subcategory: - value: 0x01 name: Handheld Spirometer - category: 0x051 name: Outdoor Sports Activity subcategory: - value: 0x01 name: Location Display - value: 0x02 name: Location and Navigation Display - value: 0x03 name: Location Pod - value: 0x04 name: Location and Navigation Pod - category: 0x052 name: Industrial Measurement Device subcategory: - value: 0x01 name: Torque Testing Device - value: 0x02 name: Caliper - value: 0x03 name: Dial Indicator - value: 0x04 name: Micrometer - value: 0x05 name: Height Gauge - value: 0x06 name: Force Gauge - category: 0x053 name: Industrial Tools subcategory: - value: 0x01 name: Machine Tool Holder - value: 0x02 name: Generic Clamping Device - value: 0x03 name: Clamping Jaws/Jaw Chuck - value: 0x04 name: Clamping (Collet) Chuck - value: 0x05 name: Clamping Mandrel - value: 0x06 name: Vise - value: 0x07 name: Zero-Point Clamping System - value: 0x08 name: Torque Wrench - value: 0x09 name: Torque Screwdriver ================================================ FILE: libs/core/src/system_state/bluetooth/build_low_energy_enums.rs ================================================ #[cfg(feature = "gen-binds")] mod tests { #[derive(Deserialize)] struct AppearanceDefinition { pub appearance_values: Vec, } #[derive(Deserialize)] struct AppearanceCategoryDefinition { pub category: u16, pub name: String, #[serde(default)] pub subcategory: Vec, } #[derive(Deserialize)] struct AppearanceSubcategoryDefinition { pub value: u16, pub name: String, } #[test] fn build_low_energy_enums() -> crate::error::Result<()> { use regex::Regex; use std::io::Write; let yaml_str = include_str!("appearance_values.yml"); let definition: AppearanceDefinition = serde_yaml::from_str(yaml_str)?; let regex = Regex::new(r"[^a-zA-Z0-9_]").unwrap(); let mut file = std::fs::File::create("./src/system_state/bluetooth/low_energy_enums.rs")?; file.write_all( "// This file was generated via rust macros. Don't modify manually.\n".as_bytes(), )?; file.write_all( "// all this structs are based on official docs https://www.bluetooth.com/specifications/assigned-numbers\n\n".as_bytes() )?; let mut category_enum = vec![ "#[repr(u16)]".to_string(), "#[derive(Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS)]".to_string(), "pub enum BLEAppearanceCategory {".to_string(), " #[default]".to_string(), ]; let mut appearance_enum = vec![ "#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, TS)]".to_string(), "#[serde(tag = \"category\", content = \"subcategory\")]".to_string(), "pub enum BLEAppearance {".to_string(), ]; let mut appearance_impl = vec![ "impl From for BLEAppearance {".to_string(), " fn from(value: u16) -> Self {".to_string(), " let category = BLEAppearanceCategory::from(value >> 6); // 10 bits" .to_string(), " let subcategory = value & 0b111111; // 6 bits\n".to_string(), " match category {".to_string(), ]; for category in definition.appearance_values { let name = regex.replace_all(&category.name, ""); let sub_enum_name = format!("BLEAppearance{name}SubCategory"); category_enum.push(format!(" {name} = 0x{:x},", category.category)); appearance_enum.push(format!(" {name}({sub_enum_name}),")); appearance_impl.push(format!(" BLEAppearanceCategory::{name} => BLEAppearance::{name}({sub_enum_name}::from(subcategory)),")); let mut subcategory_enum = vec![ "#[derive(Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS)]".to_string(), "#[cfg_attr(feature = \"gen-binds\", ts(export_to = \"BLEAppearanceSubCategory.ts\"))]".to_string(), "#[repr(u16)]".to_string(), format!("pub enum {sub_enum_name} {{"), ]; for subcategory in category.subcategory { let sub_name = regex.replace_all(&subcategory.name, ""); subcategory_enum.push(format!(" {} = 0x{:x},", sub_name, subcategory.value)); } subcategory_enum.push(" #[num_enum(catch_all)]".to_string()); subcategory_enum.push(" Reserved(u16),".to_string()); subcategory_enum.push("}\n\n".to_string()); file.write_all(subcategory_enum.join("\n").as_bytes())?; } category_enum.push("}\n\n".to_string()); file.write_all(category_enum.join("\n").as_bytes())?; appearance_enum.push("}\n\n".to_string()); file.write_all(appearance_enum.join("\n").as_bytes())?; appearance_impl.push(" }".to_string()); appearance_impl.push(" }".to_string()); appearance_impl.push("}".to_string()); file.write_all(appearance_impl.join("\n").as_bytes())?; Ok(()) } } ================================================ FILE: libs/core/src/system_state/bluetooth/enums.rs ================================================ #[repr(u32)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum BluetoothMajorServiceClass { LimitedDiscoverableMode = 0x1, LowEnergyAudio = 0x2, Reserved = 0x4, /// Location identification Positioning = 0x8, /// LAN, Ad hoc, ... Networking = 0x10, /// Printing, Speakers, ... Rendering = 0x20, /// Scanner, Microphone, ... Capturing = 0x40, /// v-Inbox, v-Folder, ... ObjectTransfer = 0x80, /// Speaker, Microphone, Headset service, ... Audio = 0x100, /// Cordless telephony, Modem, Headset service, ... Telephony = 0x200, /// WEB-server, WAP-server, ... Information = 0x400, } /// 5 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[ts(repr(enum = name))] pub enum BluetoothMajorClass { /// The ”Miscellaneous” Major Device Class is used where a more specific /// Major Device Class code is not suitable.\ /// A device that does not have a Major Class Code assigned can use the /// ”Uncategorized: device code not specified” code until classified. Miscellaneous = 0x0, /// Computer (desktop, notebook, PDA, organizer, ...) Computer = 0x1, /// Phone (cellular, cordless, payphone, modem, ...) Phone = 0x2, /// LAN/NetworkAccessPoint NetworkAccessPoint = 0x3, /// Audio/Video (headset, speaker, stereo, video display, VCR, ...) AudioVideo = 0x4, /// Peripheral (mouse, joystick, keyboard, ...) Peripheral = 0x5, /// Imaging (printer, scanner, camera, display, ...) Imaging = 0x6, Wearable = 0x7, Toy = 0x8, Health = 0x9, /// Device code not specified #[default] Uncategorized = 0x1F, } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, TS)] pub enum BluetoothMinorClass { Miscellaneous { unused: u8 }, Computer(BluetoothComputerMinor), Phone(BluetoothPhoneMinor), NetworkAccessPoint(BluetoothNetworkMinor, BluetoothNetworkSubMinor), AudioVideo(BluetoothAudioVideoMinor), Peripheral(BluetoothPeripheralMinor, BluetoothPeripheralSubMinor), Imaging(Vec, BluetoothImagingSubMinor), Wearable(BluetoothWearableMinor), Toy(BluetoothToyMinor), Health(BluetoothHealthMinor), Uncategorized { unused: u8 }, } /// 6 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BluetoothComputerMinor { /// Code for device not specified Uncategorized = 0x0, /// Desktop Workstation, PC Desktop = 0x1, /// Server-class Computer Server = 0x2, Laptop = 0x3, /// Handheld PC/PDA (e.g. clamshell) Handheld = 0x4, /// Palm-size PC/PDA PalmSize = 0x5, /// Wearable computer (e.g. watch) Wearable = 0x6, Tablet = 0x7, #[num_enum(catch_all)] Reserved(u8), } /// 6 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BluetoothPhoneMinor { /// Code for device not specified Uncategorized = 0x0, Cellular = 0x1, Cordless = 0x2, SmartPhone = 0x3, /// Wired Modem or Voice Gateway Wired = 0x4, /// Common ISDN access device Isdn = 0x5, #[num_enum(catch_all)] Reserved(u8), } /// 3 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[ts(repr(enum = name))] pub enum BluetoothNetworkMinor { FullyAvailable = 0x0, Used01To17Percent = 0x1, Used17To33Percent = 0x2, Used33To50Percent = 0x3, Used50To67Percent = 0x4, Used67To83Percent = 0x5, Used83To99Percent = 0x6, #[default] NoServiceAvailable = 0x7, } /// 3 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BluetoothNetworkSubMinor { Uncategorized = 0x0, #[num_enum(catch_all)] Reserved(u8), } /// 6 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BluetoothAudioVideoMinor { /// Code for device not specified Uncategorized = 0x0, /// Wearable Headset Headset = 0x1, /// Hands-free Headset HandsFree = 0x2, Microphone = 0x4, // 0x3 is reserved Loudspeaker = 0x5, Headphones = 0x6, PortableAudio = 0x7, CarAudio = 0x8, /// Set-top box SetTopBox = 0x9, HiFiAudioDevice = 0xA, Vcr = 0xB, VideoCamera = 0xC, CamCorder = 0xD, VideoMonitor = 0xE, VideoDisplayAndLoudspeaker = 0xF, VideoConferencing = 0x10, GamingToy = 0x12, // 0x11 is reserved #[num_enum(catch_all)] Reserved(u8), } /// 2 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[ts(repr(enum = name))] pub enum BluetoothPeripheralMinor { /// Code for device not specified #[default] Uncategorized = 0x0, Keyboard = 0x1, Pointing = 0x2, ComboKeyboardPointing = 0x3, } /// 4 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BluetoothPeripheralSubMinor { Uncategorized = 0x0, Joystick = 0x1, Gamepad = 0x2, RemoteControl = 0x3, Sensor = 0x4, DigitizerTablet = 0x5, CardReader = 0x6, DigitalPen = 0x7, /// Handheld scanner (e.g. barcodes, RFID) HandheldScanner = 0x8, /// Handheld Gestural Input Device (e.g., “wand” form factor) HandheldGestural = 0x9, #[num_enum(catch_all)] Reserved(u8), } /// 4 bits, flags that can be combined #[repr(u8)] #[derive(Debug, Copy, Clone, Eq, PartialEq, IntoPrimitive, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum BluetoothImagingMinor { Display = 0x1, Camera = 0x2, Scanner = 0x4, Printer = 0x8, } /// 2 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BluetoothImagingSubMinor { Uncategorized = 0x0, #[num_enum(catch_all)] Reserved(u8), } /// 6 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BluetoothWearableMinor { Wristwatch = 0x1, Pager = 0x2, Jacket = 0x3, Helmet = 0x4, Glasses = 0x5, /// Pin (e.g., lapel pin, broach, badge) Pin = 0x6, #[num_enum(catch_all)] Reserved(u8), } /// 6 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BluetoothToyMinor { Robot = 0x1, Vehicle = 0x2, /// Doll/Action Figure Doll = 0x3, Controller = 0x4, Game = 0x5, #[num_enum(catch_all)] Reserved(u8), } /// 6 bits #[repr(u8)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BluetoothHealthMinor { Undefined = 0x0, BloodPressureMonitor = 0x1, Thermometer = 0x2, WeighingScale = 0x3, GlucoseMeter = 0x4, PulseOximeter = 0x5, HeartPulseMonitor = 0x6, HealthDataDisplay = 0x7, StepCounter = 0x8, BodyCompositionMonitor = 0x9, PeakFlowMonitor = 0xA, MedicationMonitor = 0xB, KneeProsthesis = 0xC, AnkleProsthesis = 0xD, GenericHealthManager = 0xE, PersonalMobilityDevice = 0xF, #[num_enum(catch_all)] Reserved(u8), } ================================================ FILE: libs/core/src/system_state/bluetooth/low_energy_enums.rs ================================================ // This file was generated via rust macros. Don't modify manually. // all this structs are based on official docs https://www.bluetooth.com/specifications/assigned-numbers #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceUnknownSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearancePhoneSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceComputerSubCategory { DesktopWorkstation = 0x1, ServerclassComputer = 0x2, Laptop = 0x3, HandheldPCPDAclamshell = 0x4, PalmsizePCPDA = 0x5, Wearablecomputerwatchsize = 0x6, Tablet = 0x7, DockingStation = 0x8, AllinOne = 0x9, BladeServer = 0xa, Convertible = 0xb, Detachable = 0xc, IoTGateway = 0xd, MiniPC = 0xe, StickPC = 0xf, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceWatchSubCategory { SportsWatch = 0x1, Smartwatch = 0x2, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceClockSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceDisplaySubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceRemoteControlSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceEyeglassesSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceTagSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceKeyringSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceMediaPlayerSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceBarcodeScannerSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceThermometerSubCategory { EarThermometer = 0x1, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceHeartRateSensorSubCategory { HeartRateBelt = 0x1, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceBloodPressureSubCategory { ArmBloodPressure = 0x1, WristBloodPressure = 0x2, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceHumanInterfaceDeviceSubCategory { Keyboard = 0x1, Mouse = 0x2, Joystick = 0x3, Gamepad = 0x4, DigitizerTablet = 0x5, CardReader = 0x6, DigitalPen = 0x7, BarcodeScanner = 0x8, Touchpad = 0x9, PresentationRemote = 0xa, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceGlucoseMeterSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceRunningWalkingSensorSubCategory { InShoeRunningWalkingSensor = 0x1, OnShoeRunningWalkingSensor = 0x2, OnHipRunningWalkingSensor = 0x3, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceCyclingSubCategory { CyclingComputer = 0x1, SpeedSensor = 0x2, CadenceSensor = 0x3, PowerSensor = 0x4, SpeedandCadenceSensor = 0x5, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceControlDeviceSubCategory { Switch = 0x1, Multiswitch = 0x2, Button = 0x3, Slider = 0x4, RotarySwitch = 0x5, TouchPanel = 0x6, SingleSwitch = 0x7, DoubleSwitch = 0x8, TripleSwitch = 0x9, BatterySwitch = 0xa, EnergyHarvestingSwitch = 0xb, PushButton = 0xc, Dial = 0xd, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceNetworkDeviceSubCategory { AccessPoint = 0x1, MeshDevice = 0x2, MeshNetworkProxy = 0x3, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceSensorSubCategory { MotionSensor = 0x1, AirqualitySensor = 0x2, TemperatureSensor = 0x3, HumiditySensor = 0x4, LeakSensor = 0x5, SmokeSensor = 0x6, OccupancySensor = 0x7, ContactSensor = 0x8, CarbonMonoxideSensor = 0x9, CarbonDioxideSensor = 0xa, AmbientLightSensor = 0xb, EnergySensor = 0xc, ColorLightSensor = 0xd, RainSensor = 0xe, FireSensor = 0xf, WindSensor = 0x10, ProximitySensor = 0x11, MultiSensor = 0x12, FlushMountedSensor = 0x13, CeilingMountedSensor = 0x14, WallMountedSensor = 0x15, Multisensor = 0x16, EnergyMeter = 0x17, FlameDetector = 0x18, VehicleTirePressureSensor = 0x19, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceLightFixturesSubCategory { WallLight = 0x1, CeilingLight = 0x2, FloorLight = 0x3, CabinetLight = 0x4, DeskLight = 0x5, TrofferLight = 0x6, PendantLight = 0x7, IngroundLight = 0x8, FloodLight = 0x9, UnderwaterLight = 0xa, BollardwithLight = 0xb, PathwayLight = 0xc, GardenLight = 0xd, PoletopLight = 0xe, Spotlight = 0xf, LinearLight = 0x10, StreetLight = 0x11, ShelvesLight = 0x12, BayLight = 0x13, EmergencyExitLight = 0x14, LightController = 0x15, LightDriver = 0x16, Bulb = 0x17, LowbayLight = 0x18, HighbayLight = 0x19, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceFanSubCategory { CeilingFan = 0x1, AxialFan = 0x2, ExhaustFan = 0x3, PedestalFan = 0x4, DeskFan = 0x5, WallFan = 0x6, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceHVACSubCategory { Thermostat = 0x1, Humidifier = 0x2, Dehumidifier = 0x3, Heater = 0x4, Radiator = 0x5, Boiler = 0x6, HeatPump = 0x7, InfraredHeater = 0x8, RadiantPanelHeater = 0x9, FanHeater = 0xa, AirCurtain = 0xb, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceAirConditioningSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceHumidifierSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceHeatingSubCategory { Radiator = 0x1, Boiler = 0x2, HeatPump = 0x3, InfraredHeater = 0x4, RadiantPanelHeater = 0x5, FanHeater = 0x6, AirCurtain = 0x7, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceAccessControlSubCategory { AccessDoor = 0x1, GarageDoor = 0x2, EmergencyExitDoor = 0x3, AccessLock = 0x4, Elevator = 0x5, Window = 0x6, EntranceGate = 0x7, DoorLock = 0x8, Locker = 0x9, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceMotorizedDeviceSubCategory { MotorizedGate = 0x1, Awning = 0x2, BlindsorShades = 0x3, Curtains = 0x4, Screen = 0x5, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearancePowerDeviceSubCategory { PowerOutlet = 0x1, PowerStrip = 0x2, Plug = 0x3, PowerSupply = 0x4, LEDDriver = 0x5, FluorescentLampGear = 0x6, HIDLampGear = 0x7, ChargeCase = 0x8, PowerBank = 0x9, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceLightSourceSubCategory { IncandescentLightBulb = 0x1, LEDLamp = 0x2, HIDLamp = 0x3, FluorescentLamp = 0x4, LEDArray = 0x5, MultiColorLEDArray = 0x6, Lowvoltagehalogen = 0x7, OrganiclightemittingdiodeOLED = 0x8, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceWindowCoveringSubCategory { WindowShades = 0x1, WindowBlinds = 0x2, WindowAwning = 0x3, WindowCurtain = 0x4, ExteriorShutter = 0x5, ExteriorScreen = 0x6, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceAudioSinkSubCategory { StandaloneSpeaker = 0x1, Soundbar = 0x2, BookshelfSpeaker = 0x3, StandmountedSpeaker = 0x4, Speakerphone = 0x5, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceAudioSourceSubCategory { Microphone = 0x1, Alarm = 0x2, Bell = 0x3, Horn = 0x4, BroadcastingDevice = 0x5, ServiceDesk = 0x6, Kiosk = 0x7, BroadcastingRoom = 0x8, Auditorium = 0x9, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceMotorizedVehicleSubCategory { Car = 0x1, LargeGoodsVehicle = 0x2, Vehicle2Wheels = 0x3, Motorbike = 0x4, Scooter = 0x5, Moped = 0x6, Vehicle3Wheels = 0x7, LightVehicle = 0x8, QuadBike = 0x9, Minibus = 0xa, Bus = 0xb, Trolley = 0xc, AgriculturalVehicle = 0xd, CamperCaravan = 0xe, RecreationalVehicleMotorHome = 0xf, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceDomesticApplianceSubCategory { Refrigerator = 0x1, Freezer = 0x2, Oven = 0x3, Microwave = 0x4, Toaster = 0x5, WashingMachine = 0x6, Dryer = 0x7, Coffeemaker = 0x8, Clothesiron = 0x9, Curlingiron = 0xa, Hairdryer = 0xb, Vacuumcleaner = 0xc, Roboticvacuumcleaner = 0xd, Ricecooker = 0xe, Clothessteamer = 0xf, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceWearableAudioDeviceSubCategory { Earbud = 0x1, Headset = 0x2, Headphones = 0x3, NeckBand = 0x4, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceAircraftSubCategory { LightAircraft = 0x1, Microlight = 0x2, Paraglider = 0x3, LargePassengerAircraft = 0x4, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceAVEquipmentSubCategory { Amplifier = 0x1, Receiver = 0x2, Radio = 0x3, Tuner = 0x4, Turntable = 0x5, CDPlayer = 0x6, DVDPlayer = 0x7, BlurayPlayer = 0x8, OpticalDiscPlayer = 0x9, SetTopBox = 0xa, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceDisplayEquipmentSubCategory { Television = 0x1, Monitor = 0x2, Projector = 0x3, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceHearingaidSubCategory { Inearhearingaid = 0x1, Behindearhearingaid = 0x2, CochlearImplant = 0x3, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceGamingSubCategory { HomeVideoGameConsole = 0x1, Portablehandheldconsole = 0x2, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceSignageSubCategory { DigitalSignage = 0x1, ElectronicLabel = 0x2, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearancePulseOximeterSubCategory { FingertipPulseOximeter = 0x1, WristWornPulseOximeter = 0x2, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceWeightScaleSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearancePersonalMobilityDeviceSubCategory { PoweredWheelchair = 0x1, MobilityScooter = 0x2, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceContinuousGlucoseMonitorSubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceInsulinPumpSubCategory { InsulinPumpdurablepump = 0x1, InsulinPumppatchpump = 0x4, InsulinPen = 0x8, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceMedicationDeliverySubCategory { #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceSpirometerSubCategory { HandheldSpirometer = 0x1, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceOutdoorSportsActivitySubCategory { LocationDisplay = 0x1, LocationandNavigationDisplay = 0x2, LocationPod = 0x3, LocationandNavigationPod = 0x4, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceIndustrialMeasurementDeviceSubCategory { TorqueTestingDevice = 0x1, Caliper = 0x2, DialIndicator = 0x3, Micrometer = 0x4, HeightGauge = 0x5, ForceGauge = 0x6, #[num_enum(catch_all)] Reserved(u16), } #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] #[cfg_attr(feature = "gen-binds", ts(export_to = "BLEAppearanceSubCategory.ts"))] #[repr(u16)] pub enum BLEAppearanceIndustrialToolsSubCategory { MachineToolHolder = 0x1, GenericClampingDevice = 0x2, ClampingJawsJawChuck = 0x3, ClampingColletChuck = 0x4, ClampingMandrel = 0x5, Vise = 0x6, ZeroPointClampingSystem = 0x7, TorqueWrench = 0x8, TorqueScrewdriver = 0x9, #[num_enum(catch_all)] Reserved(u16), } #[repr(u16)] #[derive( Debug, Copy, Clone, Eq, PartialEq, FromPrimitive, IntoPrimitive, Serialize, Deserialize, TS, )] pub enum BLEAppearanceCategory { #[default] Unknown = 0x0, Phone = 0x1, Computer = 0x2, Watch = 0x3, Clock = 0x4, Display = 0x5, RemoteControl = 0x6, Eyeglasses = 0x7, Tag = 0x8, Keyring = 0x9, MediaPlayer = 0xa, BarcodeScanner = 0xb, Thermometer = 0xc, HeartRateSensor = 0xd, BloodPressure = 0xe, HumanInterfaceDevice = 0xf, GlucoseMeter = 0x10, RunningWalkingSensor = 0x11, Cycling = 0x12, ControlDevice = 0x13, NetworkDevice = 0x14, Sensor = 0x15, LightFixtures = 0x16, Fan = 0x17, HVAC = 0x18, AirConditioning = 0x19, Humidifier = 0x1a, Heating = 0x1b, AccessControl = 0x1c, MotorizedDevice = 0x1d, PowerDevice = 0x1e, LightSource = 0x1f, WindowCovering = 0x20, AudioSink = 0x21, AudioSource = 0x22, MotorizedVehicle = 0x23, DomesticAppliance = 0x24, WearableAudioDevice = 0x25, Aircraft = 0x26, AVEquipment = 0x27, DisplayEquipment = 0x28, Hearingaid = 0x29, Gaming = 0x2a, Signage = 0x2b, PulseOximeter = 0x31, WeightScale = 0x32, PersonalMobilityDevice = 0x33, ContinuousGlucoseMonitor = 0x34, InsulinPump = 0x35, MedicationDelivery = 0x36, Spirometer = 0x37, OutdoorSportsActivity = 0x51, IndustrialMeasurementDevice = 0x52, IndustrialTools = 0x53, } #[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(tag = "category", content = "subcategory")] pub enum BLEAppearance { Unknown(BLEAppearanceUnknownSubCategory), Phone(BLEAppearancePhoneSubCategory), Computer(BLEAppearanceComputerSubCategory), Watch(BLEAppearanceWatchSubCategory), Clock(BLEAppearanceClockSubCategory), Display(BLEAppearanceDisplaySubCategory), RemoteControl(BLEAppearanceRemoteControlSubCategory), Eyeglasses(BLEAppearanceEyeglassesSubCategory), Tag(BLEAppearanceTagSubCategory), Keyring(BLEAppearanceKeyringSubCategory), MediaPlayer(BLEAppearanceMediaPlayerSubCategory), BarcodeScanner(BLEAppearanceBarcodeScannerSubCategory), Thermometer(BLEAppearanceThermometerSubCategory), HeartRateSensor(BLEAppearanceHeartRateSensorSubCategory), BloodPressure(BLEAppearanceBloodPressureSubCategory), HumanInterfaceDevice(BLEAppearanceHumanInterfaceDeviceSubCategory), GlucoseMeter(BLEAppearanceGlucoseMeterSubCategory), RunningWalkingSensor(BLEAppearanceRunningWalkingSensorSubCategory), Cycling(BLEAppearanceCyclingSubCategory), ControlDevice(BLEAppearanceControlDeviceSubCategory), NetworkDevice(BLEAppearanceNetworkDeviceSubCategory), Sensor(BLEAppearanceSensorSubCategory), LightFixtures(BLEAppearanceLightFixturesSubCategory), Fan(BLEAppearanceFanSubCategory), HVAC(BLEAppearanceHVACSubCategory), AirConditioning(BLEAppearanceAirConditioningSubCategory), Humidifier(BLEAppearanceHumidifierSubCategory), Heating(BLEAppearanceHeatingSubCategory), AccessControl(BLEAppearanceAccessControlSubCategory), MotorizedDevice(BLEAppearanceMotorizedDeviceSubCategory), PowerDevice(BLEAppearancePowerDeviceSubCategory), LightSource(BLEAppearanceLightSourceSubCategory), WindowCovering(BLEAppearanceWindowCoveringSubCategory), AudioSink(BLEAppearanceAudioSinkSubCategory), AudioSource(BLEAppearanceAudioSourceSubCategory), MotorizedVehicle(BLEAppearanceMotorizedVehicleSubCategory), DomesticAppliance(BLEAppearanceDomesticApplianceSubCategory), WearableAudioDevice(BLEAppearanceWearableAudioDeviceSubCategory), Aircraft(BLEAppearanceAircraftSubCategory), AVEquipment(BLEAppearanceAVEquipmentSubCategory), DisplayEquipment(BLEAppearanceDisplayEquipmentSubCategory), Hearingaid(BLEAppearanceHearingaidSubCategory), Gaming(BLEAppearanceGamingSubCategory), Signage(BLEAppearanceSignageSubCategory), PulseOximeter(BLEAppearancePulseOximeterSubCategory), WeightScale(BLEAppearanceWeightScaleSubCategory), PersonalMobilityDevice(BLEAppearancePersonalMobilityDeviceSubCategory), ContinuousGlucoseMonitor(BLEAppearanceContinuousGlucoseMonitorSubCategory), InsulinPump(BLEAppearanceInsulinPumpSubCategory), MedicationDelivery(BLEAppearanceMedicationDeliverySubCategory), Spirometer(BLEAppearanceSpirometerSubCategory), OutdoorSportsActivity(BLEAppearanceOutdoorSportsActivitySubCategory), IndustrialMeasurementDevice(BLEAppearanceIndustrialMeasurementDeviceSubCategory), IndustrialTools(BLEAppearanceIndustrialToolsSubCategory), } impl From for BLEAppearance { fn from(value: u16) -> Self { let category = BLEAppearanceCategory::from(value >> 6); // 10 bits let subcategory = value & 0b111111; // 6 bits match category { BLEAppearanceCategory::Unknown => { BLEAppearance::Unknown(BLEAppearanceUnknownSubCategory::from(subcategory)) } BLEAppearanceCategory::Phone => { BLEAppearance::Phone(BLEAppearancePhoneSubCategory::from(subcategory)) } BLEAppearanceCategory::Computer => { BLEAppearance::Computer(BLEAppearanceComputerSubCategory::from(subcategory)) } BLEAppearanceCategory::Watch => { BLEAppearance::Watch(BLEAppearanceWatchSubCategory::from(subcategory)) } BLEAppearanceCategory::Clock => { BLEAppearance::Clock(BLEAppearanceClockSubCategory::from(subcategory)) } BLEAppearanceCategory::Display => { BLEAppearance::Display(BLEAppearanceDisplaySubCategory::from(subcategory)) } BLEAppearanceCategory::RemoteControl => BLEAppearance::RemoteControl( BLEAppearanceRemoteControlSubCategory::from(subcategory), ), BLEAppearanceCategory::Eyeglasses => { BLEAppearance::Eyeglasses(BLEAppearanceEyeglassesSubCategory::from(subcategory)) } BLEAppearanceCategory::Tag => { BLEAppearance::Tag(BLEAppearanceTagSubCategory::from(subcategory)) } BLEAppearanceCategory::Keyring => { BLEAppearance::Keyring(BLEAppearanceKeyringSubCategory::from(subcategory)) } BLEAppearanceCategory::MediaPlayer => { BLEAppearance::MediaPlayer(BLEAppearanceMediaPlayerSubCategory::from(subcategory)) } BLEAppearanceCategory::BarcodeScanner => BLEAppearance::BarcodeScanner( BLEAppearanceBarcodeScannerSubCategory::from(subcategory), ), BLEAppearanceCategory::Thermometer => { BLEAppearance::Thermometer(BLEAppearanceThermometerSubCategory::from(subcategory)) } BLEAppearanceCategory::HeartRateSensor => BLEAppearance::HeartRateSensor( BLEAppearanceHeartRateSensorSubCategory::from(subcategory), ), BLEAppearanceCategory::BloodPressure => BLEAppearance::BloodPressure( BLEAppearanceBloodPressureSubCategory::from(subcategory), ), BLEAppearanceCategory::HumanInterfaceDevice => BLEAppearance::HumanInterfaceDevice( BLEAppearanceHumanInterfaceDeviceSubCategory::from(subcategory), ), BLEAppearanceCategory::GlucoseMeter => { BLEAppearance::GlucoseMeter(BLEAppearanceGlucoseMeterSubCategory::from(subcategory)) } BLEAppearanceCategory::RunningWalkingSensor => BLEAppearance::RunningWalkingSensor( BLEAppearanceRunningWalkingSensorSubCategory::from(subcategory), ), BLEAppearanceCategory::Cycling => { BLEAppearance::Cycling(BLEAppearanceCyclingSubCategory::from(subcategory)) } BLEAppearanceCategory::ControlDevice => BLEAppearance::ControlDevice( BLEAppearanceControlDeviceSubCategory::from(subcategory), ), BLEAppearanceCategory::NetworkDevice => BLEAppearance::NetworkDevice( BLEAppearanceNetworkDeviceSubCategory::from(subcategory), ), BLEAppearanceCategory::Sensor => { BLEAppearance::Sensor(BLEAppearanceSensorSubCategory::from(subcategory)) } BLEAppearanceCategory::LightFixtures => BLEAppearance::LightFixtures( BLEAppearanceLightFixturesSubCategory::from(subcategory), ), BLEAppearanceCategory::Fan => { BLEAppearance::Fan(BLEAppearanceFanSubCategory::from(subcategory)) } BLEAppearanceCategory::HVAC => { BLEAppearance::HVAC(BLEAppearanceHVACSubCategory::from(subcategory)) } BLEAppearanceCategory::AirConditioning => BLEAppearance::AirConditioning( BLEAppearanceAirConditioningSubCategory::from(subcategory), ), BLEAppearanceCategory::Humidifier => { BLEAppearance::Humidifier(BLEAppearanceHumidifierSubCategory::from(subcategory)) } BLEAppearanceCategory::Heating => { BLEAppearance::Heating(BLEAppearanceHeatingSubCategory::from(subcategory)) } BLEAppearanceCategory::AccessControl => BLEAppearance::AccessControl( BLEAppearanceAccessControlSubCategory::from(subcategory), ), BLEAppearanceCategory::MotorizedDevice => BLEAppearance::MotorizedDevice( BLEAppearanceMotorizedDeviceSubCategory::from(subcategory), ), BLEAppearanceCategory::PowerDevice => { BLEAppearance::PowerDevice(BLEAppearancePowerDeviceSubCategory::from(subcategory)) } BLEAppearanceCategory::LightSource => { BLEAppearance::LightSource(BLEAppearanceLightSourceSubCategory::from(subcategory)) } BLEAppearanceCategory::WindowCovering => BLEAppearance::WindowCovering( BLEAppearanceWindowCoveringSubCategory::from(subcategory), ), BLEAppearanceCategory::AudioSink => { BLEAppearance::AudioSink(BLEAppearanceAudioSinkSubCategory::from(subcategory)) } BLEAppearanceCategory::AudioSource => { BLEAppearance::AudioSource(BLEAppearanceAudioSourceSubCategory::from(subcategory)) } BLEAppearanceCategory::MotorizedVehicle => BLEAppearance::MotorizedVehicle( BLEAppearanceMotorizedVehicleSubCategory::from(subcategory), ), BLEAppearanceCategory::DomesticAppliance => BLEAppearance::DomesticAppliance( BLEAppearanceDomesticApplianceSubCategory::from(subcategory), ), BLEAppearanceCategory::WearableAudioDevice => BLEAppearance::WearableAudioDevice( BLEAppearanceWearableAudioDeviceSubCategory::from(subcategory), ), BLEAppearanceCategory::Aircraft => { BLEAppearance::Aircraft(BLEAppearanceAircraftSubCategory::from(subcategory)) } BLEAppearanceCategory::AVEquipment => { BLEAppearance::AVEquipment(BLEAppearanceAVEquipmentSubCategory::from(subcategory)) } BLEAppearanceCategory::DisplayEquipment => BLEAppearance::DisplayEquipment( BLEAppearanceDisplayEquipmentSubCategory::from(subcategory), ), BLEAppearanceCategory::Hearingaid => { BLEAppearance::Hearingaid(BLEAppearanceHearingaidSubCategory::from(subcategory)) } BLEAppearanceCategory::Gaming => { BLEAppearance::Gaming(BLEAppearanceGamingSubCategory::from(subcategory)) } BLEAppearanceCategory::Signage => { BLEAppearance::Signage(BLEAppearanceSignageSubCategory::from(subcategory)) } BLEAppearanceCategory::PulseOximeter => BLEAppearance::PulseOximeter( BLEAppearancePulseOximeterSubCategory::from(subcategory), ), BLEAppearanceCategory::WeightScale => { BLEAppearance::WeightScale(BLEAppearanceWeightScaleSubCategory::from(subcategory)) } BLEAppearanceCategory::PersonalMobilityDevice => BLEAppearance::PersonalMobilityDevice( BLEAppearancePersonalMobilityDeviceSubCategory::from(subcategory), ), BLEAppearanceCategory::ContinuousGlucoseMonitor => { BLEAppearance::ContinuousGlucoseMonitor( BLEAppearanceContinuousGlucoseMonitorSubCategory::from(subcategory), ) } BLEAppearanceCategory::InsulinPump => { BLEAppearance::InsulinPump(BLEAppearanceInsulinPumpSubCategory::from(subcategory)) } BLEAppearanceCategory::MedicationDelivery => BLEAppearance::MedicationDelivery( BLEAppearanceMedicationDeliverySubCategory::from(subcategory), ), BLEAppearanceCategory::Spirometer => { BLEAppearance::Spirometer(BLEAppearanceSpirometerSubCategory::from(subcategory)) } BLEAppearanceCategory::OutdoorSportsActivity => BLEAppearance::OutdoorSportsActivity( BLEAppearanceOutdoorSportsActivitySubCategory::from(subcategory), ), BLEAppearanceCategory::IndustrialMeasurementDevice => { BLEAppearance::IndustrialMeasurementDevice( BLEAppearanceIndustrialMeasurementDeviceSubCategory::from(subcategory), ) } BLEAppearanceCategory::IndustrialTools => BLEAppearance::IndustrialTools( BLEAppearanceIndustrialToolsSubCategory::from(subcategory), ), } } } ================================================ FILE: libs/core/src/system_state/bluetooth/mod.rs ================================================ // all this structs are based on official docs https://www.bluetooth.com/specifications/assigned-numbers #[cfg(test)] mod build_low_energy_enums; pub mod enums; pub mod low_energy_enums; use enums::*; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct BluetoothDevice { pub id: String, pub name: String, pub address: u64, pub major_service_classes: Vec, pub major_class: BluetoothMajorClass, pub minor_class: BluetoothMinorClass, /// only available for low energy devices pub appearance: Option, pub connected: bool, pub paired: bool, pub can_pair: bool, pub can_disconnect: bool, pub is_low_energy: bool, } impl BluetoothDevice { fn map_services_classes(class: u32) -> Vec { use BluetoothMajorServiceClass::*; [ LimitedDiscoverableMode, LowEnergyAudio, Reserved, Positioning, Networking, Rendering, Capturing, ObjectTransfer, Audio, Telephony, Information, ] .into_iter() .filter(|&service| class & service as u32 != 0) .collect() } pub fn get_parts_of_class( class: u32, ) -> ( Vec, BluetoothMajorClass, BluetoothMinorClass, ) { let major_service_classes = class >> 13; let major_service_classes = Self::map_services_classes(major_service_classes); let major_class = (class >> 8) & 0b11111; // 5 bits let major_class = BluetoothMajorClass::from(major_class as u8); let minor_class = ((class >> 2) & 0b111111) as u8; // 6 bits let minor_class = match major_class { BluetoothMajorClass::Miscellaneous => BluetoothMinorClass::Miscellaneous { unused: minor_class, }, BluetoothMajorClass::Computer => { BluetoothMinorClass::Computer(BluetoothComputerMinor::from(minor_class)) } BluetoothMajorClass::Phone => { BluetoothMinorClass::Phone(BluetoothPhoneMinor::from(minor_class)) } BluetoothMajorClass::NetworkAccessPoint => { let minor_class = minor_class >> 3 & 0b111; // 3 bits let sub_minor_class = minor_class & 0b111; // 3 bits BluetoothMinorClass::NetworkAccessPoint( BluetoothNetworkMinor::from(minor_class), BluetoothNetworkSubMinor::from(sub_minor_class), ) } BluetoothMajorClass::AudioVideo => { BluetoothMinorClass::AudioVideo(BluetoothAudioVideoMinor::from(minor_class)) } BluetoothMajorClass::Peripheral => { let minor_class = minor_class >> 4 & 0b11; // 2 bits let sub_minor_class = minor_class & 0b1111; // 4 bits BluetoothMinorClass::Peripheral( BluetoothPeripheralMinor::from(minor_class), BluetoothPeripheralSubMinor::from(sub_minor_class), ) } BluetoothMajorClass::Imaging => { let minor_class = minor_class >> 2 & 0b1111; // 4 bits let sub_minor_class = minor_class & 0b11; // 2 bits use BluetoothImagingMinor::*; let flags: Vec = [Display, Camera, Scanner, Printer] .into_iter() .filter(|&flag| minor_class & flag as u8 != 0) .collect(); BluetoothMinorClass::Imaging(flags, BluetoothImagingSubMinor::from(sub_minor_class)) } BluetoothMajorClass::Wearable => { BluetoothMinorClass::Wearable(BluetoothWearableMinor::from(minor_class)) } BluetoothMajorClass::Toy => { BluetoothMinorClass::Toy(BluetoothToyMinor::from(minor_class)) } BluetoothMajorClass::Health => { BluetoothMinorClass::Health(BluetoothHealthMinor::from(minor_class)) } BluetoothMajorClass::Uncategorized => BluetoothMinorClass::Uncategorized { unused: minor_class, }, }; (major_service_classes, major_class, minor_class) } } #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct BluetoothDevicePairShowPinRequest { pub pin: String, pub confirmation_needed: bool, } #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(tag = "needs")] #[cfg_attr(feature = "gen-binds", ts(export))] pub enum DevicePairingNeededAction { /// No extra action is needed None, /// The user only needs to confirm the pairing ConfirmOnly, /// Should be displayed to the user to be inserted in the other device DisplayPin { pin: String }, /// An input pin should be provided ProvidePin, /// Pin should be displayed to the user and confirm that is the same as the other device ConfirmPinMatch { pin: String }, /// An input pin should be provided ProvidePasswordCredential, /// An input address should be provided ProvideAddress, } #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct DevicePairingAnswer { pub accept: bool, pub pin: Option, pub username: Option, pub password: Option, pub address: Option, } ================================================ FILE: libs/core/src/system_state/bluetooth/mod.ts ================================================ import { invoke, SeelenCommand, SeelenEvent, type UnSubscriber } from "../../handlers/mod.ts"; import { List } from "../../utils/List.ts"; import type { BluetoothDevice } from "@seelen-ui/types"; import { newFromInvoke, newOnEvent } from "../../utils/State.ts"; export class BluetoothDevices extends List { static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.GetBluetoothDevices); } static onChange(cb: (payload: BluetoothDevices) => void): Promise { return newOnEvent(cb, this, SeelenEvent.BluetoothDevicesChanged); } static async discover(): Promise { return await invoke(SeelenCommand.StartBluetoothScanning); } static async stopDiscovery(): Promise { return await invoke(SeelenCommand.StopBluetoothScanning); } static default(): BluetoothDevices { return new this([]); } } ================================================ FILE: libs/core/src/system_state/components.rs ================================================ use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] #[serde(rename_all = "camelCase")] pub struct Disk { pub name: String, pub file_system: String, pub total_space: u64, pub available_space: u64, pub mount_point: PathBuf, pub is_removable: bool, pub read_bytes: u64, pub written_bytes: u64, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] #[serde(rename_all = "camelCase")] pub struct NetworkStatistics { pub name: String, pub received: u64, pub transmitted: u64, pub packets_received: u64, pub packets_transmitted: u64, } #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] #[serde(rename_all = "camelCase")] pub struct Memory { pub total: u64, pub free: u64, pub swap_total: u64, pub swap_free: u64, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] #[serde(rename_all = "camelCase")] pub struct Core { pub name: String, pub brand: String, pub usage: f32, pub frequency: u64, } ================================================ FILE: libs/core/src/system_state/language.rs ================================================ #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct SystemLanguage { pub id: String, pub code: String, pub name: String, pub native_name: String, /// List of loaded keyboard layouts for this language pub keyboard_layouts: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub struct KeyboardLayout { /// KLID ex: "00000409" or "0000080a" or "00010409" pub id: String, /// HKL: locale input identifier pub handle: String, pub display_name: String, pub active: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct ImeStatus { pub conversion_mode: u32, pub sentence_mode: u32, } ================================================ FILE: libs/core/src/system_state/language.ts ================================================ import { SeelenCommand, SeelenEvent, type UnSubscriber } from "../handlers/mod.ts"; import { List } from "../utils/List.ts"; import { newFromInvoke, newOnEvent } from "../utils/State.ts"; import type { SystemLanguage } from "@seelen-ui/types"; export class LanguageList extends List { static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.SystemGetLanguages); } static onChange(cb: (payload: LanguageList) => void): Promise { return newOnEvent(cb, this, SeelenEvent.SystemLanguagesChanged); } } ================================================ FILE: libs/core/src/system_state/media.rs ================================================ use std::path::PathBuf; #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct MediaPlayerOwner { pub name: String, } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct MediaPlayerTimeline { /// The starting timestamp in nanoseconds (aparently it's always 0) pub start: i64, /// The total duration of the media item in nanoseconds pub end: i64, /// Current playback position in nanoseconds pub position: i64, /// The earliest timestamp at which the current media item can currently seek to. (in nanoseconds) pub min_seek: i64, /// The furthest timestamp at which the content can currently seek to. (in nanoseconds) pub max_seek: i64, pub last_updated_time: i64, } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct MediaPlayer { pub umid: String, pub title: String, pub author: String, pub thumbnail: Option, pub owner: MediaPlayerOwner, pub timeline: MediaPlayerTimeline, pub playing: bool, pub default: bool, } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct MediaDeviceSession { pub id: String, pub instance_id: String, pub process_id: u32, pub name: String, pub icon_path: Option, pub is_system: bool, pub volume: f32, pub muted: bool, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(repr(enum = name))] pub enum MediaDeviceType { Input, Output, } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct MediaDevice { pub id: String, pub name: String, pub r#type: MediaDeviceType, pub is_default_multimedia: bool, pub is_default_communications: bool, pub sessions: Vec, pub volume: f32, pub muted: bool, } ================================================ FILE: libs/core/src/system_state/mod.rs ================================================ mod bluetooth; mod components; mod language; mod media; mod monitors; mod network; mod notification; mod power; mod radios; mod trash_bin; mod tray; mod ui_colors; mod user; mod user_apps; mod win_explorer; pub use bluetooth::*; pub use components::*; pub use language::*; pub use media::*; pub use monitors::*; pub use network::*; pub use notification::*; pub use power::*; pub use radios::*; pub use trash_bin::*; pub use tray::*; pub use ui_colors::*; pub use user::*; pub use user_apps::*; pub use win_explorer::*; ================================================ FILE: libs/core/src/system_state/mod.ts ================================================ export * from "./monitors.ts"; export * from "./ui_colors.ts"; export * from "./language.ts"; export * from "./user.ts"; export * from "./bluetooth/mod.ts"; ================================================ FILE: libs/core/src/system_state/monitors.rs ================================================ use crate::{identifier_impl, rect::Rect}; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub struct PhysicalMonitor { pub id: MonitorId, pub name: String, pub rect: Rect, pub scale_factor: f64, pub is_primary: bool, } #[derive(Debug, Serialize, Deserialize, TS)] pub struct Brightness { pub min: u32, pub max: u32, pub current: u32, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub struct MonitorBrightness { pub instance_name: String, pub current_brightness: u8, pub levels: u32, pub available_levels: Vec, pub active: bool, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] pub struct MonitorId(pub String); identifier_impl!(MonitorId, String); impl Default for MonitorId { fn default() -> Self { Self("null".to_string()) } } ================================================ FILE: libs/core/src/system_state/monitors.ts ================================================ import { SeelenCommand, SeelenEvent, type UnSubscriber } from "../handlers/mod.ts"; import type { PhysicalMonitor } from "@seelen-ui/types"; import { List } from "../utils/List.ts"; import { newFromInvoke, newOnEvent } from "../utils/State.ts"; export class ConnectedMonitorList extends List { static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.SystemGetMonitors); } static onChange( cb: (payload: ConnectedMonitorList) => void, ): Promise { return newOnEvent(cb, this, SeelenEvent.SystemMonitorsChanged); } } ================================================ FILE: libs/core/src/system_state/network/mod.rs ================================================ use serde_alias::serde_alias; #[serde_alias(PascalCase)] // used by pwsh scripts #[derive(Debug, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub struct WlanProfile { pub profile_name: String, #[serde(alias = "SSID")] pub ssid: String, pub authentication: String, pub encryption: String, pub password: Option, } #[derive(Debug, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct WlanBssEntry { pub ssid: Option, pub bssid: String, pub channel_frequency: u32, pub signal: u32, /// true if the network is a saved profile pub known: bool, /// true if the network is encrypted like WEP, WPA, or WPA2 pub secured: bool, /// true if the interface is connected to this network pub connected: bool, /// true if the interface is connected to this network and is using this channel frequency pub connected_channel: bool, } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] #[ts(repr(enum = name))] pub enum AdapterStatus { Up, Down, } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct NetworkAdapter { // General information pub name: String, pub description: String, pub status: AdapterStatus, pub dns_suffix: String, #[serde(rename = "type")] pub interface_type: String, // Address information pub ipv6: Option, pub ipv4: Option, pub gateway: Option, pub mac: String, } ================================================ FILE: libs/core/src/system_state/notification.rs ================================================ // All this structs/interfaces are taken from https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/schema-root use serde::{Deserialize, Serialize}; use ts_rs::TS; #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct AppNotification { pub id: u32, pub app_umid: String, pub app_name: String, pub app_description: String, pub date: i64, pub content: Toast, } /// Base toast element, which contains at least a single visual element #[derive(Debug, Default, Clone, Serialize, Deserialize, TS)] #[serde(default)] pub struct Toast { pub header: Option, pub visual: ToastVisual, pub actions: Option, #[serde(rename = "@launch")] pub launch: Option, #[serde(rename = "@activationType")] pub activation_type: ToastActionActivationType, #[serde(rename = "@duration")] pub duration: ToastDuration, } #[derive(Debug, Default, Clone, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum ToastDuration { #[default] Short, Long, #[serde(other)] Unknown, } /// Specifies a custom header that groups multiple notifications together within Action Center. /// /// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-header #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ToastHeader { #[serde(rename = "@id")] pub id: String, #[serde(rename = "@title")] pub title: String, #[serde(rename = "@arguments")] pub arguments: String, #[serde(default, rename = "@activationType")] pub activation_type: ToastActionActivationType, } /// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-visual #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(default)] pub struct ToastVisual { pub binding: ToastBinding, #[serde(rename = "@baseUri")] pub base_uri: String, #[serde(rename = "@lang")] pub lang: String, #[serde(rename = "@version")] pub version: u32, #[serde(rename = "@addImageQuery")] pub add_image_query: bool, } impl Default for ToastVisual { fn default() -> Self { ToastVisual { binding: Default::default(), base_uri: "ms-appx:///".to_owned(), lang: "none".to_owned(), version: 1, add_image_query: false, } } } #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[serde(default)] pub struct ToastBinding { #[serde(rename = "@template")] pub template: ToastTemplateType, #[serde(rename = "$value")] pub children: Vec, } #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum ToastTemplateType { ToastImageAndText01, ToastImageAndText02, ToastImageAndText03, ToastImageAndText04, ToastText01, ToastText02, ToastText03, ToastText04, #[default] ToastGeneric, #[serde(other)] Unknown, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub enum ToastBindingChild { Text(ToastText), Image(ToastImage), Group(ToastGroup), Progress(ToastProgress), } #[derive(Debug, Default, Clone, Serialize, Deserialize, TS)] #[serde(default)] pub struct ToastText { #[serde(rename = "@id")] pub id: Option, #[serde(rename = "$value")] pub content: String, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ToastImage { #[serde(rename = "@id")] pub id: Option, #[serde(rename = "@src")] pub src: String, #[serde(rename = "@alt")] pub alt: Option, #[serde(default, rename = "@addImageQuery")] pub add_image_query: bool, #[serde(rename = "@placement")] pub placement: Option, #[serde(rename = "@hint-crop")] pub hint_crop: Option, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum ToastImageCropType { #[serde(alias = "circle")] Circle, #[serde(other)] Unknown, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub enum ToastImagePlacement { #[serde(alias = "appLogoOverride")] AppLogoOverride, #[serde(alias = "hero")] Hero, #[serde(other)] Unknown, } /// Semantically identifies that the content in the group must either be displayed as a whole, /// or not displayed if it cannot fit. Groups also allow creating multiple columns. /// /// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-group #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ToastGroup { pub subgroup: Vec, } /// Specifies vertical columns that can contain text and images. /// /// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-subgroup #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[serde(default)] pub struct ToastSubGroup { #[serde(rename = "$value")] pub children: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub enum ToastSubGroupChild { Text(ToastText), Image(ToastImage), } #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ToastProgress { #[serde(rename = "@title")] pub title: Option, #[serde(rename = "@status")] pub status: String, #[serde(rename = "@value")] pub value: String, #[serde(rename = "@valueStringOverride")] pub value_string_override: Option, } /// Container element for declaring up to five inputs and up to five button actions for the toast notification. /// /// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-actions #[derive(Debug, Default, Clone, Serialize, Deserialize, TS)] #[serde(default)] pub struct ToastActions { #[serde(rename = "$value")] pub children: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub enum ToastActionsChild { Input(ToastInput), Action(ToastAction), } /// Specifies an input, either text box or selection menu, shown in a toast notification. /// /// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-input #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ToastInput { /// The ID associated with the input #[serde(rename = "@id")] pub id: String, /// The type of input. #[serde(rename = "@type")] pub r#type: ToastInputType, /// The placeholder displayed for text input. #[serde(rename = "@placeHolderContent")] pub placeholder: Option, /// Text displayed as a label for the input. #[serde(rename = "@title")] pub title: Option, /// Options for the input if it is of type selection. #[serde(default)] pub selection: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum ToastInputType { #[serde(alias = "text")] Text, #[serde(alias = "selection")] Selection, #[serde(other)] Unknown, } /// Specifies the id and text of a selection item. /// /// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-selection #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ToastInputSelection { #[serde(rename = "@id")] pub id: String, #[serde(rename = "@content")] pub content: String, } /// Specifies a button shown in a toast. /// /// https://learn.microsoft.com/en-us/uwp/schemas/tiles/toastschema/element-action #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ToastAction { #[serde(rename = "@content")] pub content: String, #[serde(rename = "@arguments")] pub arguments: String, #[serde(default, rename = "@activationType")] pub activation_type: ToastActionActivationType, #[serde(default, rename = "@afterActivationBehavior")] pub after_activation_behavior: ToastActionAfterActivationBehavior, /// if set to "contextMenu" then the action will be added to_string the context menu intead of the toast #[serde(rename = "@placement")] pub placement: Option, /// this is used as button icon #[serde(rename = "@imageUri")] pub image_uri: Option, #[serde(rename = "@hint-inputid")] pub hint_inputid: Option, #[serde(rename = "@hint-buttonStyle")] pub hint_button_style: Option, /// button tooltip #[serde(rename = "@hint-toolTip")] pub hint_tooltip: Option, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum ToastActionButtonStyle { #[serde(alias = "success")] Sucess, #[serde(alias = "critical")] Critical, #[serde(other)] Unknown, } #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum ToastActionAfterActivationBehavior { #[default] #[serde(alias = "default")] Default, #[serde(alias = "pendingUpdate")] PendingUpdate, #[serde(other)] Unknown, } #[derive(Debug, Default, Clone, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum ToastActionActivationType { #[default] #[serde(alias = "foreground")] Foreground, #[serde(alias = "background")] Background, #[serde(alias = "protocol")] Protocol, #[serde(alias = "system")] System, #[serde(other)] Unknown, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum ToastActionPlacement { #[serde(alias = "contextMenu")] ContextMenu, #[serde(other)] Unknown, } ================================================ FILE: libs/core/src/system_state/power.rs ================================================ #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] #[allow(non_snake_case)] pub struct PowerStatus { pub ac_line_status: u8, pub battery_flag: u8, pub battery_life_percent: u8, pub system_status_flag: u8, pub battery_life_time: u32, pub battery_full_life_time: u32, } // https://learn.microsoft.com/en-us/windows/win32/api/powersetting/ne-powersetting-effective_power_mode #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, TS)] #[repr(i32)] #[ts(repr(enum = name))] pub enum PowerMode { BatterySaver, BetterBattery, Balanced, HighPerformance, MaxPerformance, GameMode, MixedReality, Unknown = i32::MAX, } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct Battery { // static info pub vendor: Option, pub model: Option, pub serial_number: Option, pub technology: String, // common information pub state: String, pub capacity: f32, pub temperature: Option, pub percentage: f32, pub cycle_count: Option, pub smart_charging: bool, // this is triggered by windows idk how but this is a simulation of that // energy stats pub energy: f32, pub energy_full: f32, pub energy_full_design: f32, pub energy_rate: f32, pub voltage: f32, // charge stats pub time_to_full: Option, pub time_to_empty: Option, } ================================================ FILE: libs/core/src/system_state/radios/mod.rs ================================================ /// Represents a radio device like a bluetooth - wifi etc. #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct RadioDevice { pub id: String, pub name: String, pub kind: RadioDeviceKind, /// True if the radio device is currently `On`. pub is_enabled: bool, } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, TS)] #[ts(repr(enum = name))] pub enum RadioDeviceKind { Other, WiFi, MobileBroadband, Bluetooth, FM, } ================================================ FILE: libs/core/src/system_state/trash_bin.rs ================================================ use serde::{Deserialize, Serialize}; use ts_rs::TS; #[derive(Debug, Default, Clone, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] #[serde(rename_all = "camelCase")] pub struct TrashBinInfo { /// Number of items currently in the recycle bin pub item_count: i64, /// Total size of all items in bytes pub size_in_bytes: i64, } ================================================ FILE: libs/core/src/system_state/tray.rs ================================================ use std::path::PathBuf; /// Identifier for a systray icon. /// /// A systray icon is either identified by a (window handle + uid) or /// its guid. Since a systray icon can be updated to also include a /// guid or window handle/uid later on, a stable ID is useful for /// consistently identifying an icon. #[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, TS)] pub enum SysTrayIconId { HandleUid(isize, u32), Guid(uuid::Uuid), } impl std::fmt::Display for SysTrayIconId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SysTrayIconId::HandleUid(handle, uid) => write!(f, "{:x}_{}", handle, uid), SysTrayIconId::Guid(guid) => write!(f, "{}", guid), } } } impl std::str::FromStr for SysTrayIconId { type Err = crate::error::SeelenLibError; fn from_str(s: &str) -> Result { // Try parsing as handle and uid (format: "handle:uid"). if let Some((handle_str, uid_str)) = s.split_once(':') { return Ok(SysTrayIconId::HandleUid( handle_str.parse().map_err(|_| "Invalid icon id")?, uid_str.parse().map_err(|_| "Invalid icon id")?, )); } // Try parsing as a guid. if let Ok(guid) = uuid::Uuid::parse_str(s) { return Ok(SysTrayIconId::Guid(guid)); } Err("Invalid icon id".into()) } } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, TS)] #[ts(export)] pub struct SysTrayIcon { /// Identifier for the icon. Will not change for the lifetime of the /// icon. /// /// The Windows shell uses either a (window handle + uid) or its guid /// to identify which icon to operate on. /// /// Read more: https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-notifyicondataw pub stable_id: SysTrayIconId, /// Application-defined identifier for the icon, used in combination /// with the window handle. /// /// The uid only has to be unique for the window handle. Multiple /// icons (across different window handles) can have the same uid. pub uid: Option, /// Handle to the window that contains the icon. Used in combination /// with a uid. /// /// Note that multiple icons can have the same window handle. pub window_handle: Option, /// GUID for the icon. /// /// Used as an alternate way to identify the icon (versus its window /// handle and uid). pub guid: Option, /// Tooltip to show for the icon on hover. pub tooltip: String, /// Handle to the icon bitmap. pub icon_handle: Option, /// Path to the icon image file. pub icon_path: Option, /// Hash of the icon image. /// /// Used to determine if the icon image has changed without having to /// compare the entire image. pub icon_image_hash: Option, /// Application-defined message identifier. /// /// Used to send messages to the window that contains the icon. pub callback_message: Option, /// Version of the icon. pub version: Option, /// Whether the icon is visible in the system tray. /// /// This is determined by the `NIS_HIDDEN` flag in the icon's state. pub is_visible: bool, } /// Actions that can be performed on a `SystrayIcon`. #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, TS)] #[ts(export, repr(enum = name))] pub enum SystrayIconAction { HoverEnter, HoverLeave, HoverMove, LeftClick, RightClick, MiddleClick, LeftDoubleClick, } ================================================ FILE: libs/core/src/system_state/ui_colors.rs ================================================ /// https://learn.microsoft.com/is-is/uwp/api/windows.ui.viewmanagement.uicolortype?view=winrt-19041 #[derive(Debug, Default, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct UIColors { pub background: String, pub foreground: String, pub accent_darkest: String, pub accent_darker: String, pub accent_dark: String, pub accent: String, pub accent_light: String, pub accent_lighter: String, pub accent_lightest: String, pub complement: Option, } /// since v2.2.0 this should be used to handle every used color in the app #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct Color { pub r: u8, pub g: u8, pub b: u8, pub a: u8, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ColorFormat { Rgba(u32), Rgb(u32), Bgra(u32), Bgr(u32), } impl Color { pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { Self { r, g, b, a } } pub fn parse(format: ColorFormat) -> Self { match format { ColorFormat::Rgba(rgba) => { let [r, g, b, a] = rgba.to_be_bytes(); Color::new(r, g, b, a) } ColorFormat::Rgb(rgb) => { let [_, r, g, b] = rgb.to_be_bytes(); Color::new(r, g, b, 0xFF) } ColorFormat::Bgra(bgra) => { let [b, g, r, a] = bgra.to_be_bytes(); Color::new(r, g, b, a) } ColorFormat::Bgr(bgr) => { let [_, b, g, r] = bgr.to_be_bytes(); Color::new(r, g, b, 0xFF) } } } } ================================================ FILE: libs/core/src/system_state/ui_colors.ts ================================================ import { SeelenCommand, SeelenEvent, type UnSubscriber } from "../handlers/mod.ts"; import { RuntimeStyleSheet } from "../utils/DOM.ts"; import { newFromInvoke, newOnEvent } from "../utils/State.ts"; import type { Color as IColor, UIColors as IUIColors } from "@seelen-ui/types"; export class UIColors { constructor(public inner: IUIColors) {} static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.SystemGetColors); } static onChange(cb: (payload: UIColors) => void): Promise { return newOnEvent(cb, this, SeelenEvent.ColorsChanged); } static default(): UIColors { return new this({ background: "#ffffff", foreground: "#000000", accent_darkest: "#990000", accent_darker: "#aa0000", accent_dark: "#bb0000", accent: "#cc0000", accent_light: "#dd0000", accent_lighter: "#ee0000", accent_lightest: "#ff0000", complement: null, }); } setAsCssVariables(): void { const oldStyles = new RuntimeStyleSheet("@deprecated/system-colors"); const newStyles = new RuntimeStyleSheet("@runtime/system-colors"); for (const [key, value] of Object.entries(this.inner)) { if (typeof value !== "string") { continue; } const color = Color.fromHex(value); const { r, g, b } = color; // replace rust snake case with kebab case const name = key.replace("_", "-"); // @deprecated old names oldStyles.addVariable(`--config-${name}-color`, value.slice(0, 7)); oldStyles.addVariable(`--config-${name}-color-rgb`, `${r}, ${g}, ${b}`); if (name.startsWith("accent")) { newStyles.addVariable(`--system-${name}-color`, value.slice(0, 7)); } } oldStyles.applyToDocument(); newStyles.applyToDocument(); } } export interface Color extends IColor {} export class Color { constructor(obj: IColor) { this.r = obj.r; this.g = obj.g; this.b = obj.b; this.a = obj.a; } /** generates a random solid color */ static random(): Color { return new Color({ r: Math.floor(Math.random() * 255), g: Math.floor(Math.random() * 255), b: Math.floor(Math.random() * 255), a: 255, }); } static fromHex(hex: string): Color { if (hex.startsWith("#")) { hex = hex.slice(1); } if (hex.length === 3) { hex = hex .split("") .map((char) => `${char}${char}`) .join(""); } if (hex.length === 6) { hex = hex.padStart(8, "f"); } const color = parseInt(hex.replace("#", ""), 16); return new Color({ r: (color >> 24) & 255, g: (color >> 16) & 255, b: (color >> 8) & 255, a: color & 255, }); } toHexString(): string { return ( "#" + this.r.toString(16).padStart(2, "0") + this.g.toString(16).padStart(2, "0") + this.b.toString(16).padStart(2, "0") + (this.a === 255 ? "" : this.a.toString(16).padStart(2, "0")) ); } private getRuntimeStyleSheet(): HTMLStyleElement { const styleId = "slu-lib-runtime-color-variables"; let styleElement = document.getElementById(styleId) as HTMLStyleElement; if (!styleElement) { styleElement = document.createElement("style"); styleElement.id = styleId; styleElement.textContent = ":root {\n}"; document.head.appendChild(styleElement); } return styleElement; } private insertIntoStyleSheet(obj: Record): void { const sheet = this.getRuntimeStyleSheet(); const lines = sheet.textContent!.split("\n"); lines.pop(); // remove the closing brace for (const [key, value] of Object.entries(obj)) { const old = lines.findIndex((line) => line.startsWith(key)); if (old !== -1) { lines[old] = `${key}: ${value};`; } else { lines.push(`${key}: ${value};`); } } lines.push("}"); sheet.textContent = lines.join("\n"); } /** * @param name the name of the color * the name will be parsed to lower kebab case and remove non-alphanumeric characters * this will create some css variables as:\ * `--color-{name}` -> #RRGGBBAA\ */ setAsCssVariable(name: string): void { const parsedName = name .replace("_", "-") .replace(/[^a-zA-Z0-9\-]/g, "") .toLowerCase(); this.insertIntoStyleSheet({ [`--color-${parsedName}`]: this.toHexString(), }); } /** * https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color * * @param accuracy if true will use an expensive but more accurate algorithm * @returns a number between 0 and 255 */ calcLuminance(accuracy?: boolean): number { if (accuracy) { // gamma correction const gR = this.r ** 2.2; const gG = this.g ** 2.2; const gB = this.b ** 2.2; return (0.299 * gR + 0.587 * gG + 0.114 * gB) ** (1 / 2.2); } // standard algorithm return 0.2126 * this.r + 0.7152 * this.g + 0.0722 * this.b; } complementary(): Color { return new Color({ r: 255 - this.r, g: 255 - this.g, b: 255 - this.b, a: this.a, }); } } ================================================ FILE: libs/core/src/system_state/user.rs ================================================ use std::path::PathBuf; use ts_rs::TS; #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export, repr(enum = name)))] pub enum FolderType { Recent, Desktop, Downloads, Documents, Music, Pictures, Videos, } static ALL_FOLDERS: [FolderType; 7] = [ FolderType::Recent, FolderType::Desktop, FolderType::Downloads, FolderType::Documents, FolderType::Music, FolderType::Pictures, FolderType::Videos, ]; impl FolderType { pub fn values() -> &'static [FolderType] { &ALL_FOLDERS } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct FolderChangedArgs { pub of_folder: FolderType, pub content: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, TS)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct User { pub name: String, pub domain: String, pub profile_home_path: PathBuf, pub email: Option, pub one_drive_path: Option, pub profile_picture_path: Option, pub xbox_gamertag: Option, } ================================================ FILE: libs/core/src/system_state/user.ts ================================================ import { SeelenCommand, SeelenEvent, type UnSubscriber } from "../handlers/mod.ts"; import { newFromInvoke, newOnEvent } from "../utils/State.ts"; import type { User } from "@seelen-ui/types"; export class UserDetails { constructor(public user: User) {} static getAsync(): Promise { return newFromInvoke(this, SeelenCommand.GetUser); } static onChange(cb: (payload: UserDetails) => void): Promise { return newOnEvent(cb, this, SeelenEvent.UserChanged); } } ================================================ FILE: libs/core/src/system_state/user_apps/mod.rs ================================================ use std::path::PathBuf; use crate::{rect::Rect, system_state::MonitorId}; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub struct FocusedApp { pub hwnd: isize, pub monitor: MonitorId, pub title: String, pub class: String, pub name: String, pub exe: Option, pub umid: Option, pub is_maximized: bool, pub is_fullscreened: bool, pub is_seelen_overlay: bool, /// this is the rect of the window, without the shadow. pub rect: Option, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub struct UserAppWindow { pub hwnd: isize, pub monitor: MonitorId, pub title: String, pub app_name: String, pub is_zoomed: bool, pub is_iconic: bool, pub is_fullscreen: bool, /// this can be from the window property store, or inherited from the process pub umid: Option, /// if the window is a frame, this information will be mapped to the process creator pub process: ProcessInformation, /// this app window can not be pinned pub prevent_pinning: bool, /// custom method to create start this application pub relaunch: Option, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] pub struct ProcessInformation { pub id: u32, pub path: Option, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub struct UserAppWindowPreview { pub hash: String, pub path: PathBuf, pub width: u32, pub height: u32, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[serde(default, rename_all = "camelCase")] pub struct Relaunch { /// program to be executed pub command: String, /// arguments to be passed to the relaunch program pub args: Option, /// path where ejecute the relaunch command pub working_dir: Option, /// custom relaunch/window icon pub icon: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[serde(untagged)] pub enum RelaunchArguments { Array(Vec), String(String), } impl std::fmt::Display for RelaunchArguments { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let args = match self { RelaunchArguments::String(args) => args.clone(), RelaunchArguments::Array(args) => args.join(" ").trim().to_owned(), }; write!(f, "{}", args) } } ================================================ FILE: libs/core/src/system_state/win_explorer.rs ================================================ use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] pub struct StartMenuItem { pub path: PathBuf, pub umid: Option, pub toast_activator: Option, /// Will be present if the item is a shortcut pub target: Option, /// Display name for the item pub display_name: String, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[cfg_attr(feature = "gen-binds", ts(export))] #[serde(rename_all = "camelCase")] pub struct StartMenuLayout { pub pinned_list: Vec, } #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] pub enum StartMenuLayoutItem { DestopAppId(String), PackagedAppId(String), DesktopAppLink(String), SecondaryTile(String), } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct TrayIcon { pub label: String, pub registry: RegistryNotifyIcon, } #[derive(Debug, Clone, Serialize, TS)] #[serde(rename_all = "camelCase")] pub struct RegistryNotifyIcon { /// can be used as a unique identifier of the registered tray icon pub key: String, pub executable_path: PathBuf, pub initial_tooltip: Option, /// PNG image of the cached icon pub icon_snapshot: Option>, pub icon_guid: Option, pub icon_uid: Option, pub is_promoted: bool, pub is_running: bool, } ================================================ FILE: libs/core/src/utils/DOM.ts ================================================ export class RuntimeStyleSheet { #element: HTMLStyleElement; #variables: Array<[string, string]> = []; #styles: Array = []; constructor(styleId: string) { let styleElement = document.getElementById(styleId) as HTMLStyleElement; if (!styleElement) { styleElement = document.createElement("style"); styleElement.id = styleId; document.head.appendChild(styleElement); } this.#element = styleElement; } addVariable(key: string, value: string): void { this.#variables.push([key, value]); } addStyle(style: string): void { this.#styles.push(style); } clear(): void { this.#variables = []; this.#styles = []; } applyToDocument(): void { const vars = this.#variables.map(([key, value]) => `${key}: ${value};`).join("\n"); this.#element.textContent = `:root {\n${vars}\n}\n\n`; this.#element.textContent += this.#styles.join("\n\n/* -=-=-=-=-=-=-=-=- */\n\n"); } } ================================================ FILE: libs/core/src/utils/List.ts ================================================ /** * A generic, abstract class for managing an array-like collection of items. * @template T The type of elements stored in the list. */ export abstract class List { /** * Constructor for the List class. * @param inner The internal array that stores the elements. * @throws Error if the provided array is not */ constructor(protected inner: T[]) { if (!inner) { throw new Error("The inner array cannot be null or undefined."); } if (!Array.isArray(inner)) { throw new Error("The inner array must be an array."); } } public [Symbol.iterator](): Iterable { return this.inner[Symbol.iterator](); } public get length(): number { return this.inner.length; } /** * Provides direct access to the internal array of items. * @returns A reference to the internal array of items. */ public asArray(): T[] { return this.inner; } /** * Returns a copy of the internal array of items. * This ensures the internal array remains immutable when accessed via this method. * @returns A new array containing the elements of the internal array. */ public all(): T[] { return [...this.inner]; } } ================================================ FILE: libs/core/src/utils/State.ts ================================================ import { invoke as tauriInvoke } from "@tauri-apps/api/core"; import { listen as tauriListen, type Options as ListenerOptions } from "@tauri-apps/api/event"; import type { AllSeelenCommandArguments, AllSeelenCommandReturns, AllSeelenEventPayloads, SeelenCommand, SeelenEvent, UnSubscriber, } from "../handlers/mod.ts"; // deno-lint-ignore no-explicit-any interface ConstructorWithSingleArg { // deno-lint-ignore no-explicit-any new (arg0: T): any; } export async function newFromInvoke< Command extends SeelenCommand, This extends ConstructorWithSingleArg, >( Class: This, command: Command, args?: NonNullable, ): Promise> { return new Class(await tauriInvoke(command, args)); } export function newOnEvent< Event extends SeelenEvent, This extends ConstructorWithSingleArg, >( cb: (instance: InstanceType) => void, Class: This, event: Event, options?: ListenerOptions, ): Promise { return tauriListen( event, (eventData) => { cb(new Class(eventData.payload as AllSeelenEventPayloads[Event])); }, options, ); } ================================================ FILE: libs/core/src/utils/async.ts ================================================ // deno-lint-ignore no-explicit-any export interface DebouncedFunction any> { (...args: Parameters): void; cancel(): void; flush(): void; pending(): boolean; } // deno-lint-ignore no-explicit-any export function debounce any>( fn: T, delay: number, ): DebouncedFunction { let timeout: ReturnType | null = null; let lastArgs: Parameters | null = null; const debounced = (...args: Parameters) => { lastArgs = args; if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { if (lastArgs) { fn(...lastArgs); lastArgs = null; } timeout = null; }, delay); }; debounced.cancel = () => { if (timeout) { clearTimeout(timeout); timeout = null; } lastArgs = null; }; debounced.flush = () => { if (timeout) { clearTimeout(timeout); timeout = null; } if (lastArgs) { fn(...lastArgs); lastArgs = null; } }; debounced.pending = () => { return timeout !== null; }; return debounced as DebouncedFunction; } ================================================ FILE: libs/core/src/utils/mod.rs ================================================ use std::path::{Path, PathBuf}; use schemars::JsonSchema; #[macro_export(local_inner_macros)] macro_rules! __switch { { if { $($if:tt)+ } do { $($do:tt)* } else { $($else:tt)* } } => { $($do)* }; { if { } do { $($do:tt)* } else { $($else:tt)* } } => { $($else)* }; } #[macro_export(local_inner_macros)] macro_rules! identifier_impl { ($type:ty, $inner:ty) => { impl std::ops::Deref for $type { type Target = $inner; fn deref(&self) -> &Self::Target { &self.0 } } impl std::ops::DerefMut for $type { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl From<&str> for $type { fn from(value: &str) -> Self { Self(<$inner>::from(value)) } } impl From for $type { fn from(value: String) -> Self { Self(<$inner>::from(value)) } } impl std::fmt::Display for $type { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { ::std::write!(f, "{}", self.0) } } }; } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)] #[ts(type = "unknown")] pub struct TsUnknown(pub serde_json::Value); impl> From for TsUnknown { fn from(value: T) -> Self { TsUnknown(value.into()) } } static ALLOWED_ROOT_FILESTEMS: &[&str] = &["metadata", "index", "mod", "main"]; static ALLOWED_ROOT_EXTENSIONS: &[&str] = &["yml", "yaml", "slu", "json"]; pub fn search_resource_entrypoint(folder: &Path) -> Option { if folder.is_file() { return None; } for filestem in ALLOWED_ROOT_FILESTEMS { for extension in ALLOWED_ROOT_EXTENSIONS { let path = folder.join(format!("{filestem}.{extension}")); if path.is_file() { return Some(path); } } } None } ================================================ FILE: libs/core/src/utils/mod.ts ================================================ export class Rect { left = 0; top = 0; right = 0; bottom = 0; } ================================================ FILE: libs/positioning/Cargo.toml ================================================ [package] name = "positioning" edition = "2024" [lints] workspace = true [dependencies] thiserror = { workspace = true } log = { workspace = true } keyframe = "1.1.1" scc = { workspace = true } windows = { workspace = true, features = ["Win32_Foundation", "Win32_Graphics_Gdi", "Win32_UI_WindowsAndMessaging"] } ================================================ FILE: libs/positioning/src/api/mod.rs ================================================ #[cfg(target_os = "windows")] mod windows; #[cfg(target_os = "windows")] pub use windows::*; ================================================ FILE: libs/positioning/src/api/windows.rs ================================================ use windows::Win32::{ Foundation::{HWND, RECT}, Graphics::Gdi::{ RDW_ALLCHILDREN, RDW_ERASE, RDW_FRAME, RDW_INVALIDATE, RDW_UPDATENOW, RedrawWindow, UpdateWindow, }, UI::WindowsAndMessaging::{ BeginDeferWindowPos, DeferWindowPos, EndDeferWindowPos, GetClassNameW, GetWindowRect, HDWP, MoveWindow, SWP_DEFERERASE, SWP_NOACTIVATE, SWP_NOCOPYBITS, SWP_NOOWNERZORDER, SWP_NOREDRAW, SWP_NOSENDCHANGING, SWP_NOSIZE, SWP_NOZORDER, SetWindowPos, }, }; use crate::{error::Result, rect::Rect}; pub fn get_window_rect(window_id: isize) -> Result { let mut rect = RECT::default(); unsafe { GetWindowRect(HWND(window_id as _), &mut rect)? }; Ok(rect.into()) } #[allow(dead_code)] pub fn start_defered_positioning(amount: i32) -> Result { let hdwp = unsafe { BeginDeferWindowPos(amount)? }; Ok(hdwp) } #[allow(dead_code)] pub fn move_window(hwnd: isize, rect: &Rect, redraw: bool) -> Result<()> { unsafe { MoveWindow( HWND(hwnd as _), rect.x, rect.y, rect.width, rect.height, redraw, )?; } Ok(()) } pub fn position_window(hwnd: isize, rect: &Rect, redraw: bool, no_size: bool) -> Result<()> { let mut flags = SWP_NOACTIVATE | SWP_NOSENDCHANGING | SWP_NOZORDER | SWP_NOOWNERZORDER; if !redraw { flags |= SWP_NOREDRAW | SWP_DEFERERASE /* | SWP_NOCOPYBITS */; } if no_size { flags |= SWP_NOSIZE; } unsafe { SetWindowPos( HWND(hwnd as _), None, rect.x, rect.y, rect.width, rect.height, flags, )?; } Ok(()) } #[allow(dead_code)] pub fn defer_window_position( hdwp: HDWP, window_id: isize, rect: &Rect, no_size: bool, ) -> Result { let mut flags = SWP_NOACTIVATE | SWP_NOREDRAW | SWP_NOCOPYBITS | SWP_NOOWNERZORDER | SWP_NOZORDER; if no_size { flags |= SWP_NOSIZE; } let hdwp = unsafe { DeferWindowPos( hdwp, HWND(window_id as _), None, rect.x, rect.y, rect.width, rect.height, flags, )? }; Ok(hdwp) } #[allow(dead_code)] pub fn finish_defered_positioning(hdwp: HDWP) -> Result<()> { unsafe { EndDeferWindowPos(hdwp)? }; Ok(()) } pub fn force_redraw_window(window_id: isize) -> Result<()> { unsafe { let hwnd = HWND(window_id as _); RedrawWindow( Some(hwnd), None, None, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ALLCHILDREN | RDW_FRAME | RDW_ERASE, ) .ok()?; UpdateWindow(hwnd).ok()?; } Ok(()) } pub fn get_class(hwnd: isize) -> Result { let mut text: [u16; 512] = [0; 512]; let len = unsafe { GetClassNameW(HWND(hwnd as _), &mut text) }; let length = usize::try_from(len).unwrap_or(0); Ok(String::from_utf16(&text[..length])?) } pub fn is_explorer(hwnd: isize) -> Result { let class = get_class(hwnd as _)?; Ok(class == "CabinetWClass" || class == "ExplorerWClass") } ================================================ FILE: libs/positioning/src/easings.rs ================================================ use keyframe::EasingFunction; /// Easing based on https://easings.net/# #[derive(Clone, Copy)] pub enum Easing { Linear, EaseIn, EaseOut, EaseInOut, EaseInQuad, EaseOutQuad, EaseInOutQuad, EaseInCubic, EaseOutCubic, EaseInOutCubic, EaseInQuart, EaseOutQuart, EaseInOutQuart, EaseInQuint, EaseOutQuint, EaseInOutQuint, EaseInExpo, EaseOutExpo, EaseInOutExpo, EaseInCirc, EaseOutCirc, EaseInOutCirc, EaseInBack, EaseOutBack, EaseInOutBack, EaseInElastic, EaseOutElastic, EaseInOutElastic, EaseInBounce, EaseOutBounce, EaseInOutBounce, } impl EasingFunction for Easing { fn y(&self, x: f64) -> f64 { use keyframe::functions::*; match self { Easing::Linear => Linear.y(x), Easing::EaseIn => EaseIn.y(x), Easing::EaseOut => EaseOut.y(x), Easing::EaseInOut => EaseInOut.y(x), Easing::EaseInQuad => EaseInQuad.y(x), Easing::EaseOutQuad => EaseOutQuad.y(x), Easing::EaseInOutQuad => EaseInOutQuad.y(x), Easing::EaseInCubic => EaseInCubic.y(x), Easing::EaseOutCubic => EaseOutCubic.y(x), Easing::EaseInOutCubic => EaseInOutCubic.y(x), Easing::EaseInQuart => EaseInQuart.y(x), Easing::EaseOutQuart => EaseOutQuart.y(x), Easing::EaseInOutQuart => EaseInOutQuart.y(x), Easing::EaseInQuint => EaseInQuint.y(x), Easing::EaseOutQuint => EaseOutQuint.y(x), Easing::EaseInOutQuint => EaseInOutQuint.y(x), Easing::EaseInExpo => Self::ease_in_expo(x), Easing::EaseOutExpo => Self::ease_out_expo(x), Easing::EaseInOutExpo => Self::ease_in_out_expo(x), Easing::EaseInCirc => Self::ease_in_circ(x), Easing::EaseOutCirc => Self::ease_out_circ(x), Easing::EaseInOutCirc => Self::ease_in_out_circ(x), Easing::EaseInBack => Self::ease_in_back(x), Easing::EaseOutBack => Self::ease_out_back(x), Easing::EaseInOutBack => Self::ease_in_out_back(x), Easing::EaseInElastic => Self::ease_in_elastic(x), Easing::EaseOutElastic => Self::ease_out_elastic(x), Easing::EaseInOutElastic => Self::ease_in_out_elastic(x), Easing::EaseInBounce => Self::ease_in_bounce(x), Easing::EaseOutBounce => Self::ease_out_bounce(x), Easing::EaseInOutBounce => Self::ease_in_out_bounce(x), } } } impl Easing { pub fn from_name(name: &str) -> Option { let name = name.to_lowercase(); let easing = match name.as_str() { "linear" => Easing::Linear, "easein" => Easing::EaseIn, "easeout" => Easing::EaseOut, "easeinout" => Easing::EaseInOut, "easeinquad" => Easing::EaseInQuad, "easeoutquad" => Easing::EaseOutQuad, "easeinoutquad" => Easing::EaseInOutQuad, "easeincubic" => Easing::EaseInCubic, "easeoutcubic" => Easing::EaseOutCubic, "easeinoutcubic" => Easing::EaseInOutCubic, "easeinquart" => Easing::EaseInQuart, "easeoutquart" => Easing::EaseOutQuart, "easeinoutquart" => Easing::EaseInOutQuart, "easeinquint" => Easing::EaseInQuint, "easeoutquint" => Easing::EaseOutQuint, "easeinoutquint" => Easing::EaseInOutQuint, "easeinexpo" => Easing::EaseInExpo, "easeoutexpo" => Easing::EaseOutExpo, "easeinoutexpo" => Easing::EaseInOutExpo, "easeincirc" => Easing::EaseInCirc, "easeoutcirc" => Easing::EaseOutCirc, "easeinoutcirc" => Easing::EaseInOutCirc, "easeinback" => Easing::EaseInBack, "easeoutback" => Easing::EaseOutBack, "easeinoutback" => Easing::EaseInOutBack, "easeinelastic" => Easing::EaseInElastic, "easeoutelastic" => Easing::EaseOutElastic, "easeinoutelastic" => Easing::EaseInOutElastic, "easeinbounce" => Easing::EaseInBounce, "easeoutbounce" => Easing::EaseOutBounce, "easeinoutbounce" => Easing::EaseInOutBounce, _ => { return None; } }; Some(easing) } #[inline] fn ease_in_expo(x: f64) -> f64 { if x == 0.0 { 0.0 } else { 2.0f64.powf(10.0 * (x - 1.0)) } } #[inline] fn ease_out_expo(x: f64) -> f64 { if x == 1.0 { 1.0 } else { 1.0 - 2.0f64.powf(-10.0 * x) } } #[inline] fn ease_in_out_expo(x: f64) -> f64 { if x == 0.0 { 0.0 } else if x == 1.0 { 1.0 } else if x < 0.5 { 2.0f64.powf(20.0 * x - 10.0) / 2.0 } else { (2.0 - 2.0f64.powf(-20.0 * x + 10.0)) / 2.0 } } #[inline] fn ease_in_circ(x: f64) -> f64 { 1.0 - f64::sqrt(1.0 - x * x) } #[inline] fn ease_out_circ(x: f64) -> f64 { f64::sqrt(1.0 - (x - 1.0).powi(2)) } #[inline] fn ease_in_out_circ(x: f64) -> f64 { if x < 0.5 { (1.0 - f64::sqrt(1.0 - (2.0 * x) * (2.0 * x))) / 2.0 } else { (f64::sqrt(1.0 - (-2.0 * x + 2.0).powi(2)) + 1.0) / 2.0 } } #[inline] fn ease_in_back(x: f64) -> f64 { let c1 = 1.70158; let c3 = c1 + 1.0; c3 * x * x * x - c1 * x * x } #[inline] fn ease_out_back(x: f64) -> f64 { let c1 = 1.70158; let c3 = c1 + 1.0; 1.0 + c3 * (x - 1.0).powi(3) + c1 * (x - 1.0).powi(2) } #[inline] fn ease_in_out_back(x: f64) -> f64 { let c1 = 1.70158; let c2 = c1 * 1.525; if x < 0.5 { ((2.0 * x).powi(2) * ((c2 + 1.0) * 2.0 * x - c2)) / 2.0 } else { ((2.0 * x - 2.0).powi(2) * ((c2 + 1.0) * (x * 2.0 - 2.0) + c2) + 2.0) / 2.0 } } #[inline] fn ease_in_elastic(x: f64) -> f64 { let c4 = 2.0 * std::f64::consts::FRAC_PI_3; if x == 0.0 { 0.0 } else if x == 1.0 { 1.0 } else { -2.0f64.powf(10.0 * x - 10.0) * f64::sin((x * 10.0 - 10.75) * c4) } } #[inline] fn ease_out_elastic(x: f64) -> f64 { let c4 = 2.0 * std::f64::consts::FRAC_PI_3; if x == 0.0 { 0.0 } else if x == 1.0 { 1.0 } else { 2.0f64.powf(-10.0 * x) * f64::sin((x * 10.0 - 0.75) * c4) + 1.0 } } #[inline] fn ease_in_out_elastic(x: f64) -> f64 { let c5 = (2.0 * std::f64::consts::PI) / 4.5; if x == 0.0 { 0.0 } else if x == 1.0 { 1.0 } else if x < 0.5 { -(2.0f64.powf(20.0 * x - 10.0) * f64::sin((20.0 * x - 11.125) * c5)) / 2.0 } else { (2.0f64.powf(-20.0 * x + 10.0) * f64::sin((20.0 * x - 11.125) * c5)) / 2.0 + 1.0 } } #[inline] fn ease_in_bounce(x: f64) -> f64 { 1.0 - Self::ease_out_bounce(1.0 - x) } #[inline] fn ease_out_bounce(x: f64) -> f64 { let n1 = 7.5625; let d1 = 2.75; if x < 1.0 / d1 { n1 * x * x } else if x < 2.0 / d1 { let x_adjusted = x - 1.5 / d1; n1 * x_adjusted * x_adjusted + 0.75 } else if x < 2.5 / d1 { let x_adjusted = x - 2.25 / d1; n1 * x_adjusted * x_adjusted + 0.9375 } else { let x_adjusted = x - 2.625 / d1; n1 * x_adjusted * x_adjusted + 0.984375 } } #[inline] fn ease_in_out_bounce(x: f64) -> f64 { if x < 0.5 { (1.0 - Self::ease_out_bounce(1.0 - 2.0 * x)) / 2.0 } else { (1.0 + Self::ease_out_bounce(2.0 * x - 1.0)) / 2.0 } } } ================================================ FILE: libs/positioning/src/error.rs ================================================ #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Windows: {0}")] Windows(#[from] windows::core::Error), #[error("Starting positioning failed")] StartingPositioningFailed, #[error("Positioning failed")] SetPositionFailed, #[error("Utf16: {0}")] Utf16(#[from] std::string::FromUtf16Error), } pub type Result = core::result::Result; ================================================ FILE: libs/positioning/src/lib.rs ================================================ mod api; pub mod easings; pub mod error; pub mod minimization; pub mod rect; use std::collections::HashMap; use std::sync::Arc; use crate::{ api::{force_redraw_window, get_window_rect, is_explorer, position_window}, easings::Easing, error::Result, rect::Rect, }; #[derive(Debug, Default)] pub struct PositionerBuilder { /// key-pair of window id and its desired position pub to_positioning: HashMap, } struct WinDataForAnimation { hwnd: isize, from: Rect, to: Rect, is_size_changing: bool, is_explorer: bool, } impl PositionerBuilder { pub fn new() -> Self { Self::default() } pub fn add(&mut self, window_id: isize, rect: Rect) { self.to_positioning.insert(window_id, rect); } pub fn remove(&mut self, window_id: isize) { self.to_positioning.remove(&window_id); } pub fn clear(&mut self) { self.to_positioning.clear(); } /// Place all windows to their desired position pub fn place(&self) -> Result<()> { for (window_id, rect) in self.to_positioning.iter() { position_window(*window_id, rect, true, false)?; } Ok(()) } /// Get the batch as a HashMap pub fn build(self) -> HashMap { self.to_positioning } } /// Manages the animation of a single window pub struct WindowAnimation { hwnd: isize, interrupt_signal: Option>, animation_thread: Option>, } impl WindowAnimation { fn new() -> Self { Self { hwnd: 0, interrupt_signal: None, animation_thread: None, } } /// Start animating this window. If already animating, interrupt and restart. fn start( &mut self, hwnd: isize, target_rect: Rect, easing: Easing, duration_ms: u64, on_end: Arc, ) -> Result<()> where F: Fn(Result) + Sync + Send + 'static, { // Interrupt any existing animation for this window self.interrupt(); self.wait(); self.hwnd = hwnd; // Get initial rect let initial_rect = get_window_rect(hwnd)?; let is_size_changing = initial_rect.width != target_rect.width || initial_rect.height != target_rect.height; let is_position_changing = initial_rect.x != target_rect.x || initial_rect.y != target_rect.y; // Skip if already in position if !is_size_changing && !is_position_changing { return Ok(()); } let data = WinDataForAnimation { hwnd, from: initial_rect, to: target_rect, is_size_changing, is_explorer: is_explorer(hwnd)?, }; let (tx, rx) = std::sync::mpsc::channel::<()>(); let animation_duration = std::time::Duration::from_millis(duration_ms); let thread = std::thread::spawn(move || { let result = Self::perform(&data, easing, animation_duration, rx); on_end(result); }); self.interrupt_signal = Some(tx); self.animation_thread = Some(thread); Ok(()) } /// Returns true if animation was interrupted/canceled fn perform( data: &WinDataForAnimation, easing: Easing, animation_duration: std::time::Duration, interrupt_rx: std::sync::mpsc::Receiver<()>, ) -> Result { let start_time = std::time::Instant::now(); let mut progress = 0.0; let mut interrupted = false; let mut frames = 0; let mut last_frame_time = start_time; let min_frame_duration = std::time::Duration::from_millis(7); // ~ 144 fps as limit while progress < 1.0 { if interrupt_rx.try_recv().is_ok() { interrupted = true; break; } let elapsed = start_time.elapsed(); progress = (elapsed.as_millis() as f64 / animation_duration.as_millis() as f64).min(1.0); let rect = keyframe::ease(easing, data.from, data.to, progress); position_window(data.hwnd, &rect, data.is_explorer, !data.is_size_changing)?; frames += 1; let elapsed = last_frame_time.elapsed(); if elapsed < min_frame_duration { std::thread::sleep(min_frame_duration - elapsed); } last_frame_time = std::time::Instant::now(); } if !interrupted { log::trace!("Animation({:?}) completed in {frames} frames", data.hwnd); let _ = force_redraw_window(data.hwnd); } Ok(interrupted) } pub fn is_running(&self) -> bool { self.animation_thread.is_some() } fn interrupt(&mut self) { if let Some(signal) = self.interrupt_signal.take() { let _ = signal.send(()); } } fn wait(&mut self) { if let Some(thread) = self.animation_thread.take() { let _ = thread.join(); } } } impl Drop for WindowAnimation { fn drop(&mut self) { self.interrupt(); self.wait(); } } /// Orchestrates animations for multiple windows, allowing per-window interruption pub struct AnimationOrchestrator { animations: scc::HashMap, } impl AnimationOrchestrator { pub fn new() -> Self { Self { animations: scc::HashMap::new(), } } /// Animate a batch of windows with the given duration and easing. /// If a window in the batch is already animating, it will be interrupted and restarted. /// Other windows not in the batch will continue animating uninterrupted. pub fn animate_batch( &self, batch: HashMap, duration_ms: u64, easing: Easing, on_end: F, ) -> Result<()> where F: Fn(Result) + Sync + Send + 'static, { let on_end = Arc::new(on_end); for (hwnd, rect) in batch { self.animate_window(hwnd, rect, duration_ms, easing, on_end.clone())?; } Ok(()) } fn animate_window( &self, hwnd: isize, target_rect: Rect, duration_ms: u64, easing: Easing, on_end: Arc, ) -> Result<()> where F: Fn(Result) + Sync + Send + 'static, { // Start animation (this will interrupt any existing animation for this window only) let mut animation = self .animations .entry(hwnd) .or_insert_with(WindowAnimation::new); animation.start(hwnd, target_rect, easing, duration_ms, on_end)?; Ok(()) } } impl Default for AnimationOrchestrator { fn default() -> Self { Self::new() } } ================================================ FILE: libs/positioning/src/minimization.rs ================================================ /* use windows::Win32::{ Foundation::{HWND, POINT}, UI::WindowsAndMessaging::{ AnimateWindow, GetWindowPlacement, IsIconic, SetWindowPlacement, ShowWindow, AW_ACTIVATE, AW_HIDE, AW_HOR_NEGATIVE, AW_HOR_POSITIVE, AW_SLIDE, SHOW_WINDOW_CMD, SW_FORCEMINIMIZE, SW_MINIMIZE, SW_SHOWMINNOACTIVE, WINDOWPLACEMENT, WPF_SETMINPOSITION }, }; use crate::error::Result; fn show_window(hwnd: HWND, cmd: SHOW_WINDOW_CMD) -> Result<()> { let _ = unsafe { ShowWindow(hwnd, cmd) }; Ok(()) } fn get_window_placement(hwnd: HWND) -> Result { let mut placement = WINDOWPLACEMENT { length: std::mem::size_of::() as u32, ..Default::default() }; unsafe { GetWindowPlacement(hwnd, &mut placement)? }; Ok(placement) } fn set_window_placement(hwnd: HWND, placement: &WINDOWPLACEMENT) -> Result<()> { unsafe { SetWindowPlacement(hwnd, placement)? }; Ok(()) } pub fn minimize_to_position(hwnd: HWND, x: i32, y: i32) -> Result<()> { if unsafe { IsIconic(hwnd).as_bool() } { return Ok(()); } /* let mut placement = get_window_placement(hwnd)?; println!("PLACEMENT: {placement:?}"); placement.flags = placement.flags | WPF_SETMINPOSITION; placement.ptMinPosition = POINT { x, y }; println!("PLACEMENT: {placement:?}"); set_window_placement(hwnd, &placement)?; let placement2 = get_window_placement(hwnd)?; println!("PLACEMENT: {placement2:?}"); */ unsafe { AnimateWindow(hwnd, 1000, AW_SLIDE | AW_HOR_NEGATIVE | AW_HIDE)? }; std::thread::sleep(std::time::Duration::from_millis(1000)); // show_window(hwnd, SW_MINIMIZE)?; Ok(()) } */ ================================================ FILE: libs/positioning/src/rect.rs ================================================ use keyframe::CanTween; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct Rect { pub x: i32, pub y: i32, pub width: i32, pub height: i32, } impl CanTween for Rect { fn ease(from: Self, to: Self, time: impl keyframe::num_traits::Float) -> Self { #[inline(always)] fn ease_field(from: i32, to: i32, time: impl keyframe::num_traits::Float) -> i32 { if from == to { to } else { f64::ease(from as f64, to as f64, time).ceil() as i32 } } Self { x: ease_field(from.x, to.x, time), y: ease_field(from.y, to.y, time), width: ease_field(from.width, to.width, time), height: ease_field(from.height, to.height, time), } } } impl From for Rect { fn from(rect: windows::Win32::Foundation::RECT) -> Self { Self { x: rect.left, y: rect.top, width: rect.right - rect.left, height: rect.bottom - rect.top, } } } impl From for windows::Win32::Foundation::RECT { fn from(rect: Rect) -> Self { Self { left: rect.x, top: rect.y, right: rect.x + rect.width, bottom: rect.y + rect.height, } } } ================================================ FILE: libs/slu-ipc/Cargo.toml ================================================ [package] name = "slu-ipc" version = "0.1.0" edition = "2024" [lints] workspace = true [dependencies] tokio = { workspace = true } thiserror = { workspace = true } log = { workspace = true } interprocess = { workspace = true, features = ["tokio"] } seelen-core = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } uuid = { workspace = true, features = ["serde"] } windows = { workspace = true, features = [ "Win32_System_RemoteDesktop", "Win32_System_Pipes", ] } ================================================ FILE: libs/slu-ipc/src/app.rs ================================================ use std::sync::Arc; use interprocess::os::windows::named_pipe::{ DuplexPipeStream, PipeListenerOptions, pipe_mode::Bytes, tokio::DuplexPipeStream as AsyncDuplexPipeStream, }; use windows::Win32::System::RemoteDesktop::{ProcessIdToSessionId, WTSGetActiveConsoleSessionId}; use crate::{ common::{ IPC, create_security_descriptor, read_from_ipc_stream, send_to_ipc_stream, send_to_ipc_stream_blocking, write_to_ipc_stream, }, error::Result, messages::{AppMessage, IpcResponse}, }; pub struct AppIpc { _priv: (), } impl IPC for AppIpc { fn path() -> String { let session_id = current_session_id().unwrap_or(0); Self::path_with_session(session_id) } } impl AppIpc { /// Constructs the pipe path for a specific session ID pub fn path_with_session(session_id: u32) -> String { format!(r"\\.\pipe\seelen-ui-{}", session_id) } pub fn start(cb: F) -> Result<()> where F: Fn(AppMessage) -> IpcResponse + Send + Sync + 'static, { let sd = create_security_descriptor()?; let listener = PipeListenerOptions::new() .path(Self::path()) .security_descriptor(Some(sd)) .create_tokio_duplex::()?; tokio::spawn(async move { let callback = Arc::new(cb); while let Ok(stream) = listener.accept().await { let callback = callback.clone(); tokio::spawn(async move { if let Err(err) = Self::process_connection(&stream, callback).await && let Err(send_err) = Self::response_to_client(&stream, IpcResponse::Err(err.to_string())) .await { log::error!( "Failed to send error response: {send_err} || Original error: {err}" ); } }); } }); Ok(()) } async fn process_connection(stream: &AsyncDuplexPipeStream, cb: Arc) -> Result<()> where F: Fn(AppMessage) -> IpcResponse, { let data = read_from_ipc_stream(stream).await?; if data.is_empty() { return Self::response_to_client(stream, IpcResponse::Success).await; } let message = AppMessage::from_bytes(&data)?; log::trace!("IPC command received: {message:?}"); Self::response_to_client(stream, cb(message)).await?; Ok(()) } async fn response_to_client( stream: &AsyncDuplexPipeStream, res: IpcResponse, ) -> Result<()> { write_to_ipc_stream(stream, &res.to_bytes()?).await } /// Sends a message to the current session asynchronously pub async fn send(message: AppMessage) -> Result<()> { let stream = AsyncDuplexPipeStream::connect_by_path(Self::path()).await?; send_to_ipc_stream(&stream, &message.to_bytes()?) .await? .ok() } /// Sends a message to the current session synchronously pub fn send_sync(message: &AppMessage) -> Result<()> { let stream = DuplexPipeStream::connect_by_path(Self::path())?; let data = message.to_bytes()?; send_to_ipc_stream_blocking(&stream, &data)?; Ok(()) } } /// Gets the current session ID of the process pub fn current_session_id() -> Result { let process_id = std::process::id(); let mut session_id = 0; unsafe { ProcessIdToSessionId(process_id, &mut session_id)? }; Ok(session_id) } /// Gets the current interactive session, if any pub fn current_interactive_session_id() -> Option { let session_id = unsafe { WTSGetActiveConsoleSessionId() }; if session_id == u32::MAX { None } else { Some(session_id) } } ================================================ FILE: libs/slu-ipc/src/common.rs ================================================ use std::{ io::{BufRead, Write}, time::Duration, }; use interprocess::os::windows::{ named_pipe::{ DuplexPipeStream, pipe_mode::Bytes, tokio::DuplexPipeStream as AsyncDuplexPipeStream, }, security_descriptor::{AsSecurityDescriptorMutExt, SecurityDescriptor}, }; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}; use crate::{error::Result, messages::IpcResponse}; /// https://learn.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-control pub static SE_DACL_PROTECTED: u16 = 4096u16; /// End of transmission block marker for IPC messages pub const END_OF_TRANSMISSION_BLOCK: u8 = 0x17; /// Timeout for IPC operations pub const IPC_TIMEOUT: Duration = Duration::from_secs(3); /// Maximum number of retries for failed IPC operations pub const MAX_RETRIES: u32 = 3; /// IPC trait for common connection operations pub trait IPC { fn path() -> String; #[allow(async_fn_in_trait)] async fn server_process_id() -> Result { let stream = AsyncDuplexPipeStream::connect_by_path(Self::path()).await?; let pid = stream.server_process_id()?; write_to_ipc_stream(&stream, &[]).await?; Ok(pid) } fn test_connection() -> Result<()> { let stream = DuplexPipeStream::connect_by_path(Self::path())?; let response = send_to_ipc_stream_blocking(&stream, &[])?; response.ok() } fn can_stablish_connection() -> bool { Self::test_connection().is_ok() } } /// Creates a security descriptor for IPC pipes pub fn create_security_descriptor() -> Result { let mut sd = SecurityDescriptor::new()?; unsafe { sd.set_dacl(std::ptr::null_mut(), false)? }; sd.set_control(SE_DACL_PROTECTED, SE_DACL_PROTECTED)?; Ok(sd) } /// Reads data from an async IPC stream with timeout pub async fn read_from_ipc_stream(stream: &AsyncDuplexPipeStream) -> Result> { let mut reader = BufReader::new(stream); let mut buf = Vec::new(); tokio::time::timeout(IPC_TIMEOUT, async { reader.read_until(END_OF_TRANSMISSION_BLOCK, &mut buf).await }) .await .map_err(|_| crate::error::Error::Timeout("Failed to read from IPC stream".to_string()))??; buf.pop(); Ok(buf) } /// Writes data to an async IPC stream with timeout pub async fn write_to_ipc_stream(stream: &AsyncDuplexPipeStream, buf: &[u8]) -> Result<()> { let mut writter = BufWriter::new(stream); tokio::time::timeout(IPC_TIMEOUT, async { writter.write_all(buf).await?; writter.write_all(&[END_OF_TRANSMISSION_BLOCK]).await?; writter.flush().await?; Ok::<(), std::io::Error>(()) }) .await .map_err(|_| crate::error::Error::Timeout("Failed to write to IPC stream".to_string()))??; Ok(()) } /// Sends data and receives response from an async IPC stream pub async fn send_to_ipc_stream( stream: &AsyncDuplexPipeStream, buf: &[u8], ) -> Result { write_to_ipc_stream(stream, buf).await?; let buf = read_from_ipc_stream(stream).await?; IpcResponse::from_bytes(&buf) } /// Blocking version to test connections without needed of tokio runtime pub fn send_to_ipc_stream_blocking( stream: &DuplexPipeStream, buf: &[u8], ) -> Result { let mut writter = std::io::BufWriter::new(stream); writter.write_all(buf)?; writter.write_all(&[END_OF_TRANSMISSION_BLOCK])?; writter.flush()?; let mut reader = std::io::BufReader::new(stream); let mut buf = Vec::new(); reader.read_until(END_OF_TRANSMISSION_BLOCK, &mut buf)?; buf.pop(); IpcResponse::from_bytes(&buf) } /// Sends data with retry logic and exponential backoff pub async fn send_with_retry(send_fn: F) -> Result<()> where F: Fn() -> Fut, Fut: std::future::Future>, { let mut last_error = None; for attempt in 0..MAX_RETRIES { match send_fn().await { Ok(response) => return response.ok(), Err(err) => { last_error = Some(err); if attempt < MAX_RETRIES - 1 { // Exponential backoff: 100ms, 200ms, 400ms let delay = Duration::from_millis(100 * 2u64.pow(attempt)); log::debug!( "IPC send failed (attempt {}/{}), retrying in {}ms", attempt + 1, MAX_RETRIES, delay.as_millis() ); tokio::time::sleep(delay).await; } } } } Err(last_error.unwrap_or_else(|| { crate::error::Error::Timeout("Unknown error during IPC send".to_string()) })) } ================================================ FILE: libs/slu-ipc/src/error.rs ================================================ use thiserror::Error; #[derive(Error, Debug)] pub enum Error { #[error("IO Error: {0}")] Io(#[from] std::io::Error), #[error("Service Error: {0}")] IpcResponseError(String), #[error("Serde Json Error: {0}")] SerdeJson(#[from] serde_json::Error), #[error("IPC Timeout: {0}")] Timeout(String), #[error("Windows: {0}")] Windows(#[from] windows::core::Error), } pub type Result = core::result::Result; ================================================ FILE: libs/slu-ipc/src/lib.rs ================================================ pub mod app; pub mod common; pub mod error; pub mod messages; pub mod service; // Re-export main types for convenience pub use app::AppIpc; pub use common::IPC; pub use service::ServiceIpc; ================================================ FILE: libs/slu-ipc/src/messages.rs ================================================ use std::collections::HashMap; use seelen_core::{rect::Rect, state::Settings}; use serde::{Deserialize, Serialize}; use crate::error::{Error, Result}; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum IpcResponse { Success, Err(String), } impl IpcResponse { pub fn ok(self) -> Result<()> { match self { IpcResponse::Success => Ok(()), IpcResponse::Err(err) => Err(Error::IpcResponseError(err)), } } pub fn from_bytes(bytes: &[u8]) -> Result { Ok(serde_json::from_slice(bytes)?) } pub fn to_bytes(&self) -> Result> { Ok(serde_json::to_vec(self)?) } } // ============================================== #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum AppMessage { /// Command-line messages Cli(Vec), /// System tray change event TrayChanged(Win32TrayEvent), /// Debug message for logging and diagnostics Debug(String), } impl AppMessage { pub fn from_bytes(bytes: &[u8]) -> Result { Ok(serde_json::from_slice(bytes)?) } pub fn to_bytes(&self) -> Result> { Ok(serde_json::to_vec(self)?) } } // ============================================== /// Seelen UI Service Actions #[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum SvcAction { Stop, SetStartup(bool), SetSettings(Box), ShowWindow { hwnd: isize, command: i32, }, ShowWindowAsync { hwnd: isize, command: i32, }, SetWindowPosition { hwnd: isize, rect: Rect, flags: u32, }, DeferWindowPositions { list: HashMap, animated: bool, animation_duration: u64, easing: String, }, SetForeground(isize), StartShortcutRegistration, StopShortcutRegistration, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SvcMessage { pub token: String, pub action: SvcAction, } impl SvcMessage { pub fn signature() -> &'static str { std::env!("SLU_SERVICE_CONNECTION_TOKEN") } pub fn is_signature_valid(&self) -> bool { self.token == SvcMessage::signature() } pub fn from_bytes(bytes: &[u8]) -> Result { Ok(serde_json::from_slice(bytes)?) } pub fn to_bytes(&self) -> Result> { Ok(serde_json::to_vec(self)?) } } // ========== Launcher ========== #[derive(Debug, Clone, Serialize, Deserialize)] pub enum LauncherMessage { GuiStarted, Quit, } impl LauncherMessage { pub fn from_bytes(bytes: &[u8]) -> Result { Ok(serde_json::from_slice(bytes)?) } pub fn to_bytes(&self) -> Result> { Ok(serde_json::to_vec(self)?) } } // ========== Tray ========== /// System tray icon data #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct IconEventData { pub uid: Option, pub window_handle: Option, pub guid: Option, pub tooltip: Option, pub icon_handle: Option, pub callback_message: Option, pub version: Option, pub is_visible: bool, } /// System tray events captured by the hook #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(tag = "type")] pub enum Win32TrayEvent { IconAdd { data: IconEventData }, IconUpdate { data: IconEventData }, IconRemove { data: IconEventData }, } ================================================ FILE: libs/slu-ipc/src/service.rs ================================================ use std::{future::Future, sync::Arc}; use interprocess::os::windows::named_pipe::{ PipeListenerOptions, pipe_mode::Bytes, tokio::DuplexPipeStream as AsyncDuplexPipeStream, }; use crate::{ app::current_session_id, common::{ IPC, create_security_descriptor, read_from_ipc_stream, send_to_ipc_stream, send_with_retry, write_to_ipc_stream, }, error::Result, messages::{IpcResponse, SvcAction, SvcMessage}, }; pub struct ServiceIpc { _priv: (), } impl IPC for ServiceIpc { fn path() -> String { let session_id = current_session_id().unwrap_or(0); Self::path_with_session(session_id) } } impl ServiceIpc { /// Constructs the pipe path for a specific session ID pub fn path_with_session(session_id: u32) -> String { format!(r"\\.\pipe\seelen-ui-service-{}", session_id) } } impl ServiceIpc { pub fn start(cb: F) -> Result<()> where R: Future + Send + Sync, F: Fn(SvcAction) -> R + Send + Sync + 'static, { let sd = create_security_descriptor()?; let listener = PipeListenerOptions::new() .path(Self::path()) .security_descriptor(Some(sd)) .create_tokio_duplex::()?; tokio::spawn(async move { let callback = Arc::new(cb); while let Ok(stream) = listener.accept().await { let callback = callback.clone(); tokio::spawn(async move { if let Err(err) = Self::process_connection(&stream, callback).await && let Err(send_err) = Self::response_to_client(&stream, IpcResponse::Err(err.to_string())) .await { log::error!( "Failed to send error response: {send_err} || Original error: {err}" ); } }); } }); Ok(()) } async fn process_connection( stream: &AsyncDuplexPipeStream, cb: Arc, ) -> Result<()> where R: Future + Send + Sync, F: Fn(SvcAction) -> R + Send + Sync + 'static, { let data = read_from_ipc_stream(stream).await?; if data.is_empty() { return Self::response_to_client(stream, IpcResponse::Success).await; } let message = SvcMessage::from_bytes(&data)?; if !message.is_signature_valid() { Self::response_to_client( stream, IpcResponse::Err("Unauthorized connection".to_owned()), ) .await?; return Ok(()); } log::trace!("IPC command received: {:?}", message.action); Self::response_to_client(stream, cb(message.action).await).await?; Ok(()) } async fn response_to_client( stream: &AsyncDuplexPipeStream, res: IpcResponse, ) -> Result<()> { write_to_ipc_stream(stream, &res.to_bytes()?).await } pub async fn send(message: SvcAction) -> Result<()> { let data = SvcMessage { token: SvcMessage::signature().to_string(), action: message, } .to_bytes()?; send_with_retry(|| Self::try_send(&data)).await } async fn try_send(data: &[u8]) -> Result { let stream = AsyncDuplexPipeStream::connect_by_path(Self::path()).await?; send_to_ipc_stream(&stream, data).await } } ================================================ FILE: libs/ui/icons.ts ================================================ // This file is generated on build, do not edit. export type IconName = | keyof typeof import("react-icons/ai") | keyof typeof import("react-icons/bi") | keyof typeof import("react-icons/bs") | keyof typeof import("react-icons/cg") | keyof typeof import("react-icons/ci") | keyof typeof import("react-icons/di") | keyof typeof import("react-icons/fa") | keyof typeof import("react-icons/fa6") | keyof typeof import("react-icons/fc") | keyof typeof import("react-icons/fi") | keyof typeof import("react-icons/gi") | keyof typeof import("react-icons/go") | keyof typeof import("react-icons/gr") | keyof typeof import("react-icons/hi") | keyof typeof import("react-icons/hi2") | keyof typeof import("react-icons/im") | keyof typeof import("react-icons/io") | keyof typeof import("react-icons/io5") | keyof typeof import("react-icons/lia") | keyof typeof import("react-icons/lu") | keyof typeof import("react-icons/md") | keyof typeof import("react-icons/pi") | keyof typeof import("react-icons/ri") | keyof typeof import("react-icons/rx") | keyof typeof import("react-icons/si") | keyof typeof import("react-icons/sl") | keyof typeof import("react-icons/tb") | keyof typeof import("react-icons/tfi") | keyof typeof import("react-icons/ti") | keyof typeof import("react-icons/vsc") | keyof typeof import("react-icons/wi"); ================================================ FILE: libs/ui/react/components/BackgroundByLayers/infra.module.css ================================================ .background { position: absolute; top: 0; right: 0; bottom: 0; left: 0; .layer { width: 100%; height: 100%; position: absolute; } } .container { position: relative; } ================================================ FILE: libs/ui/react/components/BackgroundByLayers/infra.tsx ================================================ import { cx } from "libs/ui/react/utils/styling"; import type { HTMLAttributes } from "react"; import cs from "./infra.module.css"; interface PropsV2 extends HTMLAttributes { className?: string; /** for backward compatibility */ prefix?: string; } export function BackgroundByLayersV2({ children, className, prefix, ...divProps }: PropsV2) { let background = (
{Array.from({ length: 10 }, (_, index) => (
))}
); if (!children) { /** for backward compatibility with V1 */ return background; } return (
{background} {children}
); } ================================================ FILE: libs/ui/react/components/Icon/FileIcon.tsx ================================================ import { IconPackManager } from "@seelen-ui/lib"; import type { SeelenCommandGetIconArgs } from "@seelen-ui/lib/types"; import { cx } from "libs/ui/react/utils/styling.ts"; import { useComputed, useSignal, useSignalEffect } from "@preact/signals"; import { useEffect, useRef } from "react"; import type { ImgHTMLAttributes } from "react"; import { darkMode, iconPackManager } from "./common.ts"; import { MissingIcon } from "./MissingIcon.tsx"; import cs from "./index.module.css"; interface FileIconProps extends SeelenCommandGetIconArgs, Omit, "src"> { /** if true, no missing icon will be rendered in case no icon found */ noFallback?: boolean; } export function FileIcon({ path, umid, noFallback, ...imgProps }: FileIconProps) { const $path = useSignal(path); const $umid = useSignal(umid); $path.value = path; $umid.value = umid; const icon = useComputed(() => { const found = iconPackManager.value.value.getIcon({ path: $path.value, umid: $umid.value }); if (found) { return { src: (darkMode.value ? found.dark : found.light) || found.base, mask: found.mask, isAproximatelySquare: found.isAproximatelySquare, }; } return { src: null as string | null, mask: null as string | null, isAproximatelySquare: false }; }); const prevSrcRef = useRef(null); // On mount: always request icon extraction useEffect(() => { IconPackManager.requestIconExtraction({ path, umid }); }, []); // When icon changes: if src went from non-null to null, re-request extraction useSignalEffect(() => { const src = icon.value.src; if (prevSrcRef.current !== null && src === null) { IconPackManager.requestIconExtraction({ path, umid }); } prevSrcRef.current = src; }); const { src, mask, isAproximatelySquare } = icon.value; const { ref: _ref, ...figureProps } = imgProps; const dataProps = Object.entries(figureProps as Record) .filter(([k]) => k.startsWith("data-")) .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {} as Record); if (src) { return (
{mask && (
)}
); } if (noFallback) { return null; } return ; } ================================================ FILE: libs/ui/react/components/Icon/MissingIcon.tsx ================================================ import { cx } from "libs/ui/react/utils/styling.ts"; import { useComputed } from "@preact/signals"; import type { ImgHTMLAttributes } from "react"; import { darkMode, iconPackManager } from "./common.ts"; import cs from "./index.module.css"; interface MissingIconProps extends Omit, "src"> {} export function MissingIcon({ ref: _ref, ...props }: MissingIconProps) { const icon = useComputed(() => { const found = iconPackManager.value.value.getMissingIcon(); if (found) { return { src: (darkMode.value ? found.dark : found.light) || found.base, mask: found.mask, }; } return { src: null as string | null, mask: null as string | null }; }); const { src, mask } = icon.value; const dataProps = Object.entries(props as Record) .filter(([k]) => k.startsWith("data-")) .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {} as Record); return (
{mask && (
)}
); } ================================================ FILE: libs/ui/react/components/Icon/SpecificIcon.tsx ================================================ import { cx } from "libs/ui/react/utils/styling.ts"; import { useComputed, useSignal } from "@preact/signals"; import type { ImgHTMLAttributes } from "react"; import { darkMode, iconPackManager } from "./common.ts"; import cs from "./index.module.css"; interface SpecificIconProps extends Omit, "src"> { name: string; } export function SpecificIcon({ name, ref: _ref, ...imgProps }: SpecificIconProps) { const $name = useSignal(name); $name.value = name; const icon = useComputed(() => { const found = iconPackManager.value.value.getCustomIcon($name.value); if (found) { return { src: (darkMode.value ? found.dark : found.light) || found.base, mask: found.mask, isAproximatelySquare: found.isAproximatelySquare, }; } return { src: null as string | null, mask: null as string | null, isAproximatelySquare: false }; }); const { src, mask, isAproximatelySquare } = icon.value; if (!src) { return null; } return (
{mask && (
)}
); } ================================================ FILE: libs/ui/react/components/Icon/common.ts ================================================ import { IconPackManager } from "@seelen-ui/lib"; import { signal } from "@preact/signals"; const manager = await IconPackManager.create(); export const iconPackManager = signal({ _version: 0, value: manager }); manager.onChange(() => { iconPackManager.value = { _version: iconPackManager.value._version + 1, value: manager }; }); const darkModeQuery = globalThis.matchMedia("(prefers-color-scheme: dark)"); export const darkMode = signal(darkModeQuery.matches); darkModeQuery.addEventListener("change", () => { darkMode.value = darkModeQuery.matches; }); ================================================ FILE: libs/ui/react/components/Icon/index.module.css ================================================ .reactIcon { height: 1rem; width: max-content; min-width: max-content; display: inline-block; > svg { vertical-align: middle; } } /* the layer is to allow these styles to be overridden by themes */ @layer IconBaseStyle { .outer { position: relative; img { height: 100%; object-fit: contain; } .mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; mask-repeat: no-repeat; mask-size: contain; mask-position: center; mask-mode: luminance; background-color: var(--system-accent-light-color); } } } ================================================ FILE: libs/ui/react/components/Icon/index.tsx ================================================ import { forwardRef, type HTMLAttributes } from "preact/compat"; import { cx } from "../../utils/styling.ts"; import InlineSVG from "../InlineSvg/index.tsx"; import cs from "./index.module.css"; import type { IconName } from "libs/ui/icons.ts"; interface ReactIconProps extends HTMLAttributes { iconName: IconName; size?: string | number; color?: string; style?: React.CSSProperties; } /** React Icons */ export const Icon = forwardRef((props, ref) => { const { iconName, size, color, className, style, ...rest } = props; return ( ); }); export * from "./FileIcon.tsx"; export * from "./MissingIcon.tsx"; export * from "./SpecificIcon.tsx"; ================================================ FILE: libs/ui/react/components/InlineSvg/index.module.css ================================================ .inlineSvg { > svg { width: 100%; height: 100%; } } ================================================ FILE: libs/ui/react/components/InlineSvg/index.tsx ================================================ import { cx } from "libs/ui/react/utils/styling"; import { forwardRef, type HTMLAttributes, useEffect, useState } from "react"; import cs from "./index.module.css"; interface Props extends HTMLAttributes { src: string; } const InlineSVG = forwardRef(({ src, className, ...rest }, ref) => { const [svgContent, setSvgContent] = useState(null); useEffect(() => { const fetchSVG = async () => { try { const response = await fetch(src); if (!response.ok) { throw new Error(`Failed to fetch SVG: ${response.statusText}`); } const svgText = await response.text(); setSvgContent(svgText); } catch (err: any) { console.error(err); } }; fetchSVG(); }, [src]); return ( ); }); export default InlineSVG; ================================================ FILE: libs/ui/react/components/ResourceText/index.tsx ================================================ import type { ResourceText as IResourceText } from "@seelen-ui/lib/types"; import { invoke, SeelenCommand } from "@seelen-ui/lib"; import { useTranslation } from "react-i18next"; import { unified } from "unified"; import remarkParse from "remark-parse"; import remarkGfm from "remark-gfm"; import remarkRehype from "remark-rehype"; import rehypeStringify from "rehype-stringify"; import { useEffect } from "preact/hooks"; import { useSignal } from "@preact/signals"; interface Props { className?: string; text?: IResourceText; } export function ResourceText({ text, className }: Props) { const { i18n: { language }, } = useTranslation(); if (!text) { return null; } if (typeof text === "string") { return {text}; } const text2 = text[language] || text["en"]; if (!text2) { return null; } return {text2}; } interface MarkdownViewerProps { text: IResourceText; } export function ResourceTextAsMarkdown({ text }: MarkdownViewerProps) { const html = useSignal(""); const { i18n: { language }, } = useTranslation(); useEffect(() => { let input = typeof text === "string" ? text : text[language] || text["en"]; if (!input) { html.value = ""; return; } safeMarkdownToHtml(input).then((content) => (html.value = content)); }, [text, language]); if (!html.value) { return null; } return (
{ const target = e.target as HTMLElement; const anchor = target.closest("a"); if (anchor?.href) { // force links on markdown being opened on browser e.preventDefault(); invoke(SeelenCommand.OpenFile, { path: anchor.href }); } }} /> ); } /** this can be used on untrusted markdown, ex user inputs */ export async function safeMarkdownToHtml(markdown: string): Promise { const result = await unified() .use(remarkParse) .use(remarkGfm) // enable GitHub Flavored Markdown .use(remarkRehype) // allow conversion of markdown to html .use(rehypeStringify) // convert html to string .process(markdown); return result.toString(); } ================================================ FILE: libs/ui/react/components/Wallpaper/components/ImageWallpaper.tsx ================================================ import { cx } from "libs/ui/react/utils/styling.ts"; import { convertFileSrc } from "@tauri-apps/api/core"; import { useMemo } from "preact/hooks"; import type { DefinedWallProps } from "../types"; import { getWallpaperStyles } from "../utils.ts"; import cs from "../index.module.css"; export function ImageWallpaper({ definition, config, onLoad }: DefinedWallProps) { const imageSrc = useMemo( () => convertFileSrc(definition.metadata.path + "\\" + definition.filename!), [definition.metadata.path, definition.filename], ); const handleError = (e: Event) => { const target = e.target as HTMLImageElement; console.error("Image failed to load:", { src: imageSrc, naturalWidth: target.naturalWidth, naturalHeight: target.naturalHeight, }); }; return ( ); } ================================================ FILE: libs/ui/react/components/Wallpaper/components/ThemedWallpaper.tsx ================================================ import { cx } from "libs/ui/react/utils/styling.ts"; import { useEffect } from "preact/hooks"; import { BackgroundByLayersV2 } from "libs/ui/react/components/BackgroundByLayers/infra.tsx"; import type { BaseProps } from "../types"; import { getWallpaperStyles } from "../utils.ts"; import cs from "../index.module.css"; export function ThemedWallpaper({ definition, config, onLoad, }: Pick) { useEffect(() => { onLoad?.(); }, []); if (!definition || !config) { return (
); } return (
); } ================================================ FILE: libs/ui/react/components/Wallpaper/components/VideoWallpaper.tsx ================================================ import { cx } from "libs/ui/react/utils/styling.ts"; import { convertFileSrc } from "@tauri-apps/api/core"; import { useEffect, useMemo, useRef } from "preact/hooks"; import type { DefinedWallProps } from "../types"; import { getPlaybackRate, getWallpaperStyles } from "../utils.ts"; import cs from "../index.module.css"; const MAX_RETRIES = 3; const WAITING_TIMEOUT_MS = 3000; const STALL_CHECK_INTERVAL_MS = 5000; export function VideoWallpaper({ definition, config, muted, paused, onLoad }: DefinedWallProps) { const ref = useRef(null); const waitingTimeoutRef = useRef>(); const retryCountRef = useRef(0); const lastTimeUpdateRef = useRef(0); const isLoadedRef = useRef(false); const videoSrc = useMemo( () => convertFileSrc(definition.metadata.path + "\\" + definition.filename!), [definition.metadata.path, definition.filename], ); // Monitor for stalls by checking timeupdate useEffect(() => { const checkInterval = setInterval(() => { if (ref.current && !paused && isLoadedRef.current) { const currentTime = ref.current.currentTime; // If time hasn't changed and video should be playing, it's stalled if ( currentTime === lastTimeUpdateRef.current && ref.current.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && retryCountRef.current < MAX_RETRIES ) { console.debug("Video appears stalled, attempting recovery"); retryCountRef.current++; ref.current.load(); ref.current.currentTime = currentTime; ref.current.play().catch((err) => { console.error("Failed to resume video after stall:", err); }); } lastTimeUpdateRef.current = currentTime; } }, STALL_CHECK_INTERVAL_MS); return () => clearInterval(checkInterval); }, [paused]); // Cleanup on unmount to prevent memory leaks useEffect(() => { // https://github.com/facebook/react/issues/15583 // this is a workaround for a bug in js that causes memory leak on video elements return () => { if (waitingTimeoutRef.current) { clearTimeout(waitingTimeoutRef.current); } if (ref.current) { ref.current.pause(); ref.current.removeAttribute("src"); ref.current.load(); if (globalThis.gc) { setTimeout(() => globalThis.gc?.(), 100); } } }; }, []); // Handle pause/play state changes useEffect(() => { if (ref.current && paused !== undefined) { if (paused) { ref.current.pause(); } else if (ref.current.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) { ref.current.play().catch((err) => { console.error("Failed to play video:", err); }); } } }, [paused]); const handleWaiting = () => { // Clear any existing timeout if (waitingTimeoutRef.current) { clearTimeout(waitingTimeoutRef.current); } // Set a timeout to detect if truly stuck waitingTimeoutRef.current = setTimeout(() => { if (ref.current && retryCountRef.current < MAX_RETRIES) { console.debug( `Video stuck in waiting state, retry ${retryCountRef.current + 1}/${MAX_RETRIES}`, ); retryCountRef.current++; const currentTime = ref.current.currentTime; // Full reload to recover from stuck state ref.current.load(); // Restore position if it wasn't at the beginning if (currentTime > 0) { ref.current.currentTime = currentTime; } if (!paused) { ref.current.play().catch((err) => { console.error("Failed to resume video after waiting timeout:", err); }); } } }, WAITING_TIMEOUT_MS); }; const handlePlaying = () => { // Clear timeout when playing successfully if (waitingTimeoutRef.current) { clearTimeout(waitingTimeoutRef.current); waitingTimeoutRef.current = undefined; } // Reset retry count on successful playback retryCountRef.current = 0; }; const handleStalled = () => { // Stalled = browser thinks it can play but isn't fetching data if (ref.current && retryCountRef.current < MAX_RETRIES) { console.debug("Video network stalled, forcing reload"); retryCountRef.current++; const currentTime = ref.current.currentTime; ref.current.load(); ref.current.currentTime = currentTime; if (!paused) { ref.current.play().catch((err) => { console.error("Failed to resume video after stall:", err); }); } } }; const handleCanPlay = () => { if (ref.current && !paused) { ref.current.play().catch((err) => { console.error("Failed to play video on canplay:", err); }); } }; const handleLoadedMetadata = () => { isLoadedRef.current = true; onLoad?.(); }; const handleError = (e: Event) => { const target = e.target as HTMLVideoElement; const error = target.error; if (error) { console.error("Video error:", { code: error.code, message: error.message, src: videoSrc, }); // Attempt recovery on certain errors if (error.code === MediaError.MEDIA_ERR_NETWORK && retryCountRef.current < MAX_RETRIES) { console.debug("Network error, attempting recovery"); retryCountRef.current++; setTimeout(() => { if (ref.current) { ref.current.load(); if (!paused) { ref.current.play().catch((err) => { console.error("Failed to recover from network error:", err); }); } } }, 1000); } } }; const handleTimeUpdate = () => { if (ref.current) { lastTimeUpdateRef.current = ref.current.currentTime; } }; return (