├── .gitignore ├── Cargo.toml ├── README.md ├── build.rs ├── cargo-generate.toml ├── generate ├── cleanup.rhai ├── rename.rhai └── template-README.md ├── i18n.toml ├── i18n └── en │ └── cosmic_app_template.ftl ├── justfile ├── resources ├── app.desktop ├── app.metainfo.xml └── icons │ └── hicolor │ └── scalable │ └── apps │ └── icon.svg └── src ├── app.rs ├── config.rs ├── i18n.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | .cargo/ 2 | *.pdb 3 | **/*.rs.bk 4 | debug/ 5 | target/ 6 | vendor/ 7 | vendor.tar 8 | debian/* 9 | !debian/changelog 10 | !debian/control 11 | !debian/copyright 12 | !debian/install 13 | !debian/rules 14 | !debian/source -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "{{ project-name }}" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "{{ license }}" 6 | description = "{{ description }}" 7 | repository = "{{ repository-url }}" 8 | 9 | [build-dependencies] 10 | vergen = { version = "8", features = ["git", "gitcl"] } 11 | 12 | [dependencies] 13 | futures-util = "0.3.31" 14 | i18n-embed-fl = "0.9.2" 15 | open = "5.3.0" 16 | rust-embed = "8.5.0" 17 | tokio = { version = "1.41.0", features = ["full"] } 18 | 19 | [dependencies.i18n-embed] 20 | version = "0.15" 21 | features = ["fluent-system", "desktop-requester"] 22 | 23 | [dependencies.libcosmic] 24 | git = "https://github.com/pop-os/libcosmic.git" 25 | # See https://github.com/pop-os/libcosmic/blob/master/Cargo.toml for available features. 26 | features = [ 27 | # Accessibility support 28 | "a11y", 29 | # Uses cosmic-settings-daemon to watch for config file changes 30 | "dbus-config", 31 | # Support creating additional application windows. 32 | "multi-window", 33 | # On app startup, focuses an existing instance if the app is already open 34 | "single-instance", 35 | # Uses tokio as the executor for the runtime 36 | "tokio", 37 | # Windowing support for X11, Windows, Mac, & Redox 38 | "winit", 39 | # Add Wayland support to winit 40 | "wayland", 41 | # GPU-accelerated rendering 42 | "wgpu", 43 | ] 44 | 45 | # Uncomment to test a locally-cloned libcosmic 46 | # [patch.'https://github.com/pop-os/libcosmic'] 47 | # libcosmic = { path = "../libcosmic" } 48 | # cosmic-config = { path = "../libcosmic/cosmic-config" } 49 | # cosmic-theme = { path = "../libcosmic/cosmic-theme" } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COSMIC Application Template 2 | 3 | A template for developing applications for the COSMIC™ desktop environment using [libcosmic][libcosmic]. 4 | 5 | ## Getting Started 6 | 7 | To create an application with this template, [install `cargo generate`][cargo-generate] and run: 8 | 9 | ```sh 10 | cargo generate gh:pop-os/cosmic-app-template 11 | ``` 12 | 13 | A [justfile](./justfile) is included by default with common recipes used by other COSMIC projects. Install from [casey/just][just] 14 | 15 | - `just` builds the application with the default `just build-release` recipe 16 | - `just run` builds and runs the application 17 | - `just install` installs the project into the system 18 | - `just vendor` creates a vendored tarball 19 | - `just build-vendored` compiles with vendored dependencies from that tarball 20 | - `just check` runs clippy on the project to check for linter warnings 21 | - `just check-json` can be used by IDEs that support LSP 22 | 23 | ## Documentation 24 | 25 | Refer to the [libcosmic API documentation][api-docs] and [book][book] for help with building applications with [libcosmic][libcosmic]. 26 | 27 | [api-docs]: https://pop-os.github.io/libcosmic/cosmic/ 28 | [book]: https://pop-os.github.io/libcosmic-book/ 29 | [cargo-generate]: https://cargo-generate.github.io/cargo-generate/installation.html 30 | [libcosmic]: https://github.com/pop-os/libcosmic/ 31 | [just]: https://github.com/casey/just 32 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | // Rebuild if i18n files change 3 | println!("cargo:rerun-if-changed=i18n"); 4 | 5 | // Emit version information (if not cached by just vendor) 6 | let mut vergen = vergen::EmitBuilder::builder(); 7 | 8 | println!("cargo:rerun-if-env-changed=VERGEN_GIT_COMMIT_DATE"); 9 | if std::env::var_os("VERGEN_GIT_COMMIT_DATE").is_none() { 10 | vergen.git_commit_date(); 11 | } 12 | 13 | println!("cargo:rerun-if-env-changed=VERGEN_GIT_SHA"); 14 | if std::env::var_os("VERGEN_GIT_SHA").is_none() { 15 | vergen.git_sha(false); 16 | } 17 | vergen.fail_on_error().emit()?; 18 | Ok(()) 19 | } 20 | -------------------------------------------------------------------------------- /cargo-generate.toml: -------------------------------------------------------------------------------- 1 | [template] 2 | cargo_generate_version = ">=0.22" 3 | 4 | [placeholders.appid] 5 | type = "string" 6 | prompt = "RDNN App ID (com.github.username.ProjectName)" 7 | regex = "^[a-zA-Z0-9]+\\.[a-zA-Z0-9]+\\.[a-zA-Z0-9-_]+(\\.[a-zA-Z0-9-_]+)?$" 8 | default = "com.github.pop-os.cosmic-app-template" 9 | 10 | [placeholders.description] 11 | type = "string" 12 | prompt = "Description of application" 13 | default = "An application for the COSMIC™ desktop" 14 | 15 | [placeholders.license] 16 | type = "string" 17 | prompt = "SPDX license identifier (https://spdx.org/licenses/)" 18 | default = "MPL-2.0" 19 | 20 | [placeholders.repository-url] 21 | type = "string" 22 | prompt = "Repository URL (https://*)" 23 | default = "https://github.com/pop-os/cosmic-app-template" 24 | regex = "^https?:\\/\\/(www\\.)?[\\w\\-]+(\\.[\\w\\-]+)+[/#?]?.*$" 25 | 26 | [hooks] 27 | pre = ["generate/rename.rhai"] 28 | post = ["generate/cleanup.rhai"] 29 | -------------------------------------------------------------------------------- /generate/cleanup.rhai: -------------------------------------------------------------------------------- 1 | file::delete("generate"); 2 | -------------------------------------------------------------------------------- /generate/rename.rhai: -------------------------------------------------------------------------------- 1 | let project_name = variable::get("project-name").to_snake_case(); 2 | file::rename("i18n/en/cosmic_app_template.ftl", `i18n/en/${project_name}.ftl`); 3 | file::rename("generate/template-README.md", "README.md"); 4 | -------------------------------------------------------------------------------- /generate/template-README.md: -------------------------------------------------------------------------------- 1 | # {{ project-name | title_case }} 2 | 3 | {{ description }} 4 | 5 | ## Installation 6 | 7 | A [justfile](./justfile) is included by default for the [casey/just][just] command runner. 8 | 9 | - `just` builds the application with the default `just build-release` recipe 10 | - `just run` builds and runs the application 11 | - `just install` installs the project into the system 12 | - `just vendor` creates a vendored tarball 13 | - `just build-vendored` compiles with vendored dependencies from that tarball 14 | - `just check` runs clippy on the project to check for linter warnings 15 | - `just check-json` can be used by IDEs that support LSP 16 | 17 | ## Translators 18 | 19 | [Fluent][fluent] is used for localization of the software. Fluent's translation files are found in the [i18n directory](./i18n). New translations may copy the [English (en) localization](./i18n/en) of the project, rename `en` to the desired [ISO 639-1 language code][iso-codes], and then translations can be provided for each [message identifier][fluent-guide]. If no translation is necessary, the message may be omitted. 20 | 21 | ## Packaging 22 | 23 | If packaging for a Linux distribution, vendor dependencies locally with the `vendor` rule, and build with the vendored sources using the `build-vendored` rule. When installing files, use the `rootdir` and `prefix` variables to change installation paths. 24 | 25 | ```sh 26 | just vendor 27 | just build-vendored 28 | just rootdir=debian/{{ project-name }} prefix=/usr install 29 | ``` 30 | 31 | It is recommended to build a source tarball with the vendored dependencies, which can typically be done by running `just vendor` on the host system before it enters the build environment. 32 | 33 | ## Developers 34 | 35 | Developers should install [rustup][rustup] and configure their editor to use [rust-analyzer][rust-analyzer]. To improve compilation times, disable LTO in the release profile, install the [mold][mold] linker, and configure [sccache][sccache] for use with Rust. The [mold][mold] linker will only improve link times if LTO is disabled. 36 | 37 | [fluent]: https://projectfluent.org/ 38 | [fluent-guide]: https://projectfluent.org/fluent/guide/hello.html 39 | [iso-codes]: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes 40 | [just]: https://github.com/casey/just 41 | [rustup]: https://rustup.rs/ 42 | [rust-analyzer]: https://rust-analyzer.github.io/ 43 | [mold]: https://github.com/rui314/mold 44 | [sccache]: https://github.com/mozilla/sccache 45 | -------------------------------------------------------------------------------- /i18n.toml: -------------------------------------------------------------------------------- 1 | fallback_language = "en" 2 | 3 | [fluent] 4 | assets_dir = "i18n" -------------------------------------------------------------------------------- /i18n/en/cosmic_app_template.ftl: -------------------------------------------------------------------------------- 1 | app-title = {{ project-name | title_case }} 2 | about = About 3 | view = View 4 | welcome = Welcome to COSMIC! ✨ 5 | page-id = Page { $num } 6 | git-description = Git commit {$hash} on {$date} 7 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | name := '{{ project-name }}' 2 | appid := '{{ appid }}' 3 | {% raw %} 4 | rootdir := '' 5 | prefix := '/usr' 6 | 7 | base-dir := absolute_path(clean(rootdir / prefix)) 8 | 9 | bin-src := 'target' / 'release' / name 10 | bin-dst := base-dir / 'bin' / name 11 | 12 | desktop := appid + '.desktop' 13 | desktop-src := 'resources' / desktop 14 | desktop-dst := clean(rootdir / prefix) / 'share' / 'applications' / desktop 15 | 16 | appdata := appid + '.metainfo.xml' 17 | appdata-src := 'resources' / appdata 18 | appdata-dst := clean(rootdir / prefix) / 'share' / 'appdata' / appdata 19 | 20 | icons-src := 'resources' / 'icons' / 'hicolor' 21 | icons-dst := clean(rootdir / prefix) / 'share' / 'icons' / 'hicolor' 22 | 23 | icon-svg-src := icons-src / 'scalable' / 'apps' / 'icon.svg' 24 | icon-svg-dst := icons-dst / 'scalable' / 'apps' / appid + '.svg' 25 | 26 | # Default recipe which runs `just build-release` 27 | default: build-release 28 | 29 | # Runs `cargo clean` 30 | clean: 31 | cargo clean 32 | 33 | # Removes vendored dependencies 34 | clean-vendor: 35 | rm -rf .cargo vendor vendor.tar 36 | 37 | # `cargo clean` and removes vendored dependencies 38 | clean-dist: clean clean-vendor 39 | 40 | # Compiles with debug profile 41 | build-debug *args: 42 | cargo build {{args}} 43 | 44 | # Compiles with release profile 45 | build-release *args: (build-debug '--release' args) 46 | 47 | # Compiles release profile with vendored dependencies 48 | build-vendored *args: vendor-extract (build-release '--frozen --offline' args) 49 | 50 | # Runs a clippy check 51 | check *args: 52 | cargo clippy --all-features {{args}} -- -W clippy::pedantic 53 | 54 | # Runs a clippy check with JSON message format 55 | check-json: (check '--message-format=json') 56 | 57 | # Run the application for testing purposes 58 | run *args: 59 | env RUST_BACKTRACE=full cargo run --release {{args}} 60 | 61 | # Installs files 62 | install: 63 | install -Dm0755 {{bin-src}} {{bin-dst}} 64 | install -Dm0644 resources/app.desktop {{desktop-dst}} 65 | install -Dm0644 resources/app.metainfo.xml {{appdata-dst}} 66 | install -Dm0644 {{icon-svg-src}} {{icon-svg-dst}} 67 | 68 | # Uninstalls installed files 69 | uninstall: 70 | rm {{bin-dst}} {{desktop-dst}} {{icon-svg-dst}} 71 | 72 | # Vendor dependencies locally 73 | vendor: 74 | #!/usr/bin/env bash 75 | mkdir -p .cargo 76 | cargo vendor --sync Cargo.toml | head -n -1 > .cargo/config.toml 77 | echo 'directory = "vendor"' >> .cargo/config.toml 78 | echo >> .cargo/config.toml 79 | echo '[env]' >> .cargo/config.toml 80 | if [ -n "${SOURCE_DATE_EPOCH}" ] 81 | then 82 | source_date="$(date -d "@${SOURCE_DATE_EPOCH}" "+%Y-%m-%d")" 83 | echo "VERGEN_GIT_COMMIT_DATE = \"${source_date}\"" >> .cargo/config.toml 84 | fi 85 | if [ -n "${SOURCE_GIT_HASH}" ] 86 | then 87 | echo "VERGEN_GIT_SHA = \"${SOURCE_GIT_HASH}\"" >> .cargo/config.toml 88 | fi 89 | tar pcf vendor.tar .cargo vendor 90 | rm -rf .cargo vendor 91 | 92 | # Extracts vendored dependencies 93 | vendor-extract: 94 | rm -rf vendor 95 | tar pxf vendor.tar 96 | {% endraw %} 97 | -------------------------------------------------------------------------------- /resources/app.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name={{ project-name | title_case }} 3 | Comment={{ description }} 4 | Type=Application 5 | Icon={{ appid }} 6 | Exec={{ project-name }} %F 7 | Terminal=false 8 | StartupNotify=true 9 | Categories=COSMIC 10 | Keywords=COSMIC 11 | MimeType= 12 | -------------------------------------------------------------------------------- /resources/app.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ appid }} 4 | CC0-1.0 5 | {{ license }} 6 | {{ project-name | title_case }} 7 | {{ description }} 8 | 9 | {{ repository-url }}/raw/main/resources/icons/hicolor/scalable/apps/icon.svg 10 | 11 | {{ repository-url }} 12 | {{ appid }}.desktop 13 | 14 | {{ appid }} 15 | 16 | {{ project-name }} 17 | 18 | 19 | 20 | 360 21 | 22 | 23 | keyboard 24 | pointing 25 | touch 26 | 27 | 28 | COSMIC 29 | 30 | 31 | COSMIC 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /resources/icons/hicolor/scalable/apps/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: {{ license }} 2 | 3 | use crate::config::Config; 4 | use crate::fl; 5 | use cosmic::app::context_drawer; 6 | use cosmic::cosmic_config::{self, CosmicConfigEntry}; 7 | use cosmic::iced::alignment::{Horizontal, Vertical}; 8 | use cosmic::iced::{Alignment, Length, Subscription}; 9 | use cosmic::prelude::*; 10 | use cosmic::widget::{self, icon, menu, nav_bar}; 11 | use cosmic::{cosmic_theme, theme}; 12 | use futures_util::SinkExt; 13 | use std::collections::HashMap; 14 | 15 | const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY"); 16 | const APP_ICON: &[u8] = include_bytes!("../resources/icons/hicolor/scalable/apps/icon.svg"); 17 | 18 | /// The application model stores app-specific state used to describe its interface and 19 | /// drive its logic. 20 | pub struct AppModel { 21 | /// Application state which is managed by the COSMIC runtime. 22 | core: cosmic::Core, 23 | /// Display a context drawer with the designated page if defined. 24 | context_page: ContextPage, 25 | /// Contains items assigned to the nav bar panel. 26 | nav: nav_bar::Model, 27 | /// Key bindings for the application's menu bar. 28 | key_binds: HashMap, 29 | // Configuration data that persists between application runs. 30 | config: Config, 31 | } 32 | 33 | /// Messages emitted by the application and its widgets. 34 | #[derive(Debug, Clone)] 35 | pub enum Message { 36 | OpenRepositoryUrl, 37 | SubscriptionChannel, 38 | ToggleContextPage(ContextPage), 39 | UpdateConfig(Config), 40 | LaunchUrl(String), 41 | } 42 | 43 | /// Create a COSMIC application from the app model 44 | impl cosmic::Application for AppModel { 45 | /// The async executor that will be used to run your application's commands. 46 | type Executor = cosmic::executor::Default; 47 | 48 | /// Data that your application receives to its init method. 49 | type Flags = (); 50 | 51 | /// Messages which the application and its widgets will emit. 52 | type Message = Message; 53 | 54 | /// Unique identifier in RDNN (reverse domain name notation) format. 55 | const APP_ID: &'static str = "{{ appid }}"; 56 | 57 | fn core(&self) -> &cosmic::Core { 58 | &self.core 59 | } 60 | 61 | fn core_mut(&mut self) -> &mut cosmic::Core { 62 | &mut self.core 63 | } 64 | 65 | /// Initializes the application with any given flags and startup commands. 66 | fn init( 67 | core: cosmic::Core, 68 | _flags: Self::Flags, 69 | ) -> (Self, Task>) { 70 | // Create a nav bar with three page items. 71 | let mut nav = nav_bar::Model::default(); 72 | 73 | nav.insert() 74 | .text(fl!("page-id", num = 1)) 75 | .data::(Page::Page1) 76 | .icon(icon::from_name("applications-science-symbolic")) 77 | .activate(); 78 | 79 | nav.insert() 80 | .text(fl!("page-id", num = 2)) 81 | .data::(Page::Page2) 82 | .icon(icon::from_name("applications-system-symbolic")); 83 | 84 | nav.insert() 85 | .text(fl!("page-id", num = 3)) 86 | .data::(Page::Page3) 87 | .icon(icon::from_name("applications-games-symbolic")); 88 | 89 | // Construct the app model with the runtime's core. 90 | let mut app = AppModel { 91 | core, 92 | context_page: ContextPage::default(), 93 | nav, 94 | key_binds: HashMap::new(), 95 | // Optional configuration file for an application. 96 | config: cosmic_config::Config::new(Self::APP_ID, Config::VERSION) 97 | .map(|context| match Config::get_entry(&context) { 98 | Ok(config) => config, 99 | Err((_errors, config)) => { 100 | // for why in errors { 101 | // tracing::error!(%why, "error loading app config"); 102 | // } 103 | 104 | config 105 | } 106 | }) 107 | .unwrap_or_default(), 108 | }; 109 | 110 | // Create a startup command that sets the window title. 111 | let command = app.update_title(); 112 | 113 | (app, command) 114 | } 115 | 116 | /// Elements to pack at the start of the header bar. 117 | fn header_start(&self) -> Vec> { 118 | let menu_bar = menu::bar(vec![menu::Tree::with_children( 119 | menu::root(fl!("view")), 120 | menu::items( 121 | &self.key_binds, 122 | vec![menu::Item::Button(fl!("about"), None, MenuAction::About)], 123 | ), 124 | )]); 125 | 126 | vec![menu_bar.into()] 127 | } 128 | 129 | /// Enables the COSMIC application to create a nav bar with this model. 130 | fn nav_model(&self) -> Option<&nav_bar::Model> { 131 | Some(&self.nav) 132 | } 133 | 134 | /// Display a context drawer if the context page is requested. 135 | fn context_drawer(&self) -> Option> { 136 | if !self.core.window.show_context { 137 | return None; 138 | } 139 | 140 | Some(match self.context_page { 141 | ContextPage::About => context_drawer::context_drawer( 142 | self.about(), 143 | Message::ToggleContextPage(ContextPage::About), 144 | ) 145 | .title(fl!("about")), 146 | }) 147 | } 148 | 149 | /// Describes the interface based on the current state of the application model. 150 | /// 151 | /// Application events will be processed through the view. Any messages emitted by 152 | /// events received by widgets will be passed to the update method. 153 | fn view(&self) -> Element { 154 | widget::text::title1(fl!("welcome")) 155 | .apply(widget::container) 156 | .width(Length::Fill) 157 | .height(Length::Fill) 158 | .align_x(Horizontal::Center) 159 | .align_y(Vertical::Center) 160 | .into() 161 | } 162 | 163 | /// Register subscriptions for this application. 164 | /// 165 | /// Subscriptions are long-running async tasks running in the background which 166 | /// emit messages to the application through a channel. They are started at the 167 | /// beginning of the application, and persist through its lifetime. 168 | fn subscription(&self) -> Subscription { 169 | struct MySubscription; 170 | 171 | Subscription::batch(vec![ 172 | // Create a subscription which emits updates through a channel. 173 | Subscription::run_with_id( 174 | std::any::TypeId::of::(), 175 | cosmic::iced::stream::channel(4, move |mut channel| async move { 176 | _ = channel.send(Message::SubscriptionChannel).await; 177 | 178 | futures_util::future::pending().await 179 | }), 180 | ), 181 | // Watch for application configuration changes. 182 | self.core() 183 | .watch_config::(Self::APP_ID) 184 | .map(|update| { 185 | // for why in update.errors { 186 | // tracing::error!(?why, "app config error"); 187 | // } 188 | 189 | Message::UpdateConfig(update.config) 190 | }), 191 | ]) 192 | } 193 | 194 | /// Handles messages emitted by the application and its widgets. 195 | /// 196 | /// Tasks may be returned for asynchronous execution of code in the background 197 | /// on the application's async runtime. 198 | fn update(&mut self, message: Self::Message) -> Task> { 199 | match message { 200 | Message::OpenRepositoryUrl => { 201 | _ = open::that_detached(REPOSITORY); 202 | } 203 | 204 | Message::SubscriptionChannel => { 205 | // For example purposes only. 206 | } 207 | 208 | Message::ToggleContextPage(context_page) => { 209 | if self.context_page == context_page { 210 | // Close the context drawer if the toggled context page is the same. 211 | self.core.window.show_context = !self.core.window.show_context; 212 | } else { 213 | // Open the context drawer to display the requested context page. 214 | self.context_page = context_page; 215 | self.core.window.show_context = true; 216 | } 217 | } 218 | 219 | Message::UpdateConfig(config) => { 220 | self.config = config; 221 | } 222 | 223 | Message::LaunchUrl(url) => match open::that_detached(&url) { 224 | Ok(()) => {} 225 | Err(err) => { 226 | eprintln!("failed to open {url:?}: {err}"); 227 | } 228 | }, 229 | } 230 | Task::none() 231 | } 232 | 233 | /// Called when a nav item is selected. 234 | fn on_nav_select(&mut self, id: nav_bar::Id) -> Task> { 235 | // Activate the page in the model. 236 | self.nav.activate(id); 237 | 238 | self.update_title() 239 | } 240 | } 241 | 242 | impl AppModel { 243 | /// The about page for this app. 244 | pub fn about(&self) -> Element { 245 | let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing; 246 | 247 | let icon = widget::svg(widget::svg::Handle::from_memory(APP_ICON)); 248 | 249 | let title = widget::text::title3(fl!("app-title")); 250 | 251 | let hash = env!("VERGEN_GIT_SHA"); 252 | let short_hash: String = hash.chars().take(7).collect(); 253 | let date = env!("VERGEN_GIT_COMMIT_DATE"); 254 | 255 | let link = widget::button::link(REPOSITORY) 256 | .on_press(Message::OpenRepositoryUrl) 257 | .padding(0); 258 | 259 | widget::column() 260 | .push(icon) 261 | .push(title) 262 | .push(link) 263 | .push( 264 | widget::button::link(fl!( 265 | "git-description", 266 | hash = short_hash.as_str(), 267 | date = date 268 | )) 269 | .on_press(Message::LaunchUrl(format!("{REPOSITORY}/commits/{hash}"))) 270 | .padding(0), 271 | ) 272 | .align_x(Alignment::Center) 273 | .spacing(space_xxs) 274 | .into() 275 | } 276 | 277 | /// Updates the header and window titles. 278 | pub fn update_title(&mut self) -> Task> { 279 | let mut window_title = fl!("app-title"); 280 | 281 | if let Some(page) = self.nav.text(self.nav.active()) { 282 | window_title.push_str(" — "); 283 | window_title.push_str(page); 284 | } 285 | 286 | if let Some(id) = self.core.main_window_id() { 287 | self.set_window_title(window_title, id) 288 | } else { 289 | Task::none() 290 | } 291 | } 292 | } 293 | 294 | /// The page to display in the application. 295 | pub enum Page { 296 | Page1, 297 | Page2, 298 | Page3, 299 | } 300 | 301 | /// The context page to display in the context drawer. 302 | #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] 303 | pub enum ContextPage { 304 | #[default] 305 | About, 306 | } 307 | 308 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 309 | pub enum MenuAction { 310 | About, 311 | } 312 | 313 | impl menu::action::MenuAction for MenuAction { 314 | type Message = Message; 315 | 316 | fn message(&self) -> Self::Message { 317 | match self { 318 | MenuAction::About => Message::ToggleContextPage(ContextPage::About), 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: {{ license }} 2 | 3 | use cosmic::cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry}; 4 | 5 | #[derive(Debug, Default, Clone, CosmicConfigEntry, Eq, PartialEq)] 6 | #[version = 1] 7 | pub struct Config { 8 | demo: String, 9 | } 10 | -------------------------------------------------------------------------------- /src/i18n.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: {{ license }} 2 | 3 | //! Provides localization support for this crate. 4 | 5 | use std::sync::LazyLock; 6 | 7 | use i18n_embed::{ 8 | fluent::{fluent_language_loader, FluentLanguageLoader}, 9 | unic_langid::LanguageIdentifier, 10 | DefaultLocalizer, LanguageLoader, Localizer, 11 | }; 12 | use rust_embed::RustEmbed; 13 | 14 | /// Applies the requested language(s) to requested translations from the `fl!()` macro. 15 | pub fn init(requested_languages: &[LanguageIdentifier]) { 16 | if let Err(why) = localizer().select(requested_languages) { 17 | eprintln!("error while loading fluent localizations: {why}"); 18 | } 19 | } 20 | 21 | // Get the `Localizer` to be used for localizing this library. 22 | #[must_use] 23 | pub fn localizer() -> Box { 24 | Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) 25 | } 26 | 27 | #[derive(RustEmbed)] 28 | #[folder = "i18n/"] 29 | struct Localizations; 30 | 31 | pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { 32 | let loader: FluentLanguageLoader = fluent_language_loader!(); 33 | 34 | loader 35 | .load_fallback_language(&Localizations) 36 | .expect("Error while loading fallback language"); 37 | 38 | loader 39 | }); 40 | 41 | {% raw %} 42 | /// Request a localized string by ID from the i18n/ directory. 43 | #[macro_export] 44 | macro_rules! fl { 45 | ($message_id:literal) => {{ 46 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id) 47 | }}; 48 | 49 | ($message_id:literal, $($args:expr),*) => {{ 50 | i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *) 51 | }}; 52 | } 53 | {% endraw %} 54 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: {{ license }} 2 | 3 | mod app; 4 | mod config; 5 | mod i18n; 6 | 7 | fn main() -> cosmic::iced::Result { 8 | // Get the system's preferred languages. 9 | let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); 10 | 11 | // Enable localizations to be applied. 12 | i18n::init(&requested_languages); 13 | 14 | // Settings for configuring the application window and iced runtime. 15 | let settings = cosmic::app::Settings::default().size_limits( 16 | cosmic::iced::Limits::NONE 17 | .min_width(360.0) 18 | .min_height(180.0), 19 | ); 20 | 21 | // Starts the application's event loop with `()` as the application's flags. 22 | cosmic::app::run::(settings, ()) 23 | } 24 | --------------------------------------------------------------------------------