initialised with COSMIC app template

This commit is contained in:
2026-03-10 17:55:38 +10:00
committed by professor
commit b9c1315974
13 changed files with 717 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
.cargo/
*.pdb
**/*.rs.bk
debug/
target/
vendor/
vendor.tar
debian/*
!debian/changelog
!debian/control
!debian/copyright
!debian/install
!debian/rules
!debian/source
+48
View File
@@ -0,0 +1,48 @@
[package]
name = "planit"
version = "0.1.0"
edition = "2024"
license = "MPL-2.0"
description = "A COSMIC todo.txt application"
repository = "https://github.com/professorwaltwood/planit.git"
[dependencies]
futures-util = "0.3.31"
i18n-embed = { version = "0.16", features = [
"fluent-system",
"desktop-requester",
] }
i18n-embed-fl = "0.10"
open = "5.3.2"
rust-embed = "8.8.0"
tokio = { version = "1.48.0", features = ["full"] }
[dependencies.libcosmic]
git = "https://github.com/pop-os/libcosmic.git"
# See https://github.com/pop-os/libcosmic/blob/master/Cargo.toml for available features.
features = [
# Accessibility support
"a11y",
# About widget for the app
"about",
# Uses cosmic-settings-daemon to watch for config file changes
"dbus-config",
# Support creating additional application windows.
"multi-window",
# On app startup, focuses an existing instance if the app is already open
"single-instance",
# Uses tokio as the executor for the runtime
"tokio",
# Windowing support for X11, Windows, Mac, & Redox
"winit",
# Add Wayland support to winit
"wayland",
# GPU-accelerated rendering
"wgpu",
]
# Uncomment to test a locally-cloned libcosmic
# [patch.'https://github.com/pop-os/libcosmic']
# libcosmic = { path = "../libcosmic" }
# cosmic-config = { path = "../libcosmic/cosmic-config" }
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
+44
View File
@@ -0,0 +1,44 @@
# Planit
A COSMIC todo.txt application
## Installation
A [justfile](./justfile) is included by default for the [casey/just][just] command runner.
- `just` builds the application with the default `just build-release` recipe
- `just run` builds and runs the application
- `just install` installs the project into the system
- `just vendor` creates a vendored tarball
- `just build-vendored` compiles with vendored dependencies from that tarball
- `just check` runs clippy on the project to check for linter warnings
- `just check-json` can be used by IDEs that support LSP
## Translators
[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.
## Packaging
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.
```sh
just vendor
just build-vendored
just rootdir=debian/planit prefix=/usr install
```
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.
## Developers
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.
[fluent]: https://projectfluent.org/
[fluent-guide]: https://projectfluent.org/fluent/guide/hello.html
[iso-codes]: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
[just]: https://github.com/casey/just
[rustup]: https://rustup.rs/
[rust-analyzer]: https://rust-analyzer.github.io/
[mold]: https://github.com/rui314/mold
[sccache]: https://github.com/mozilla/sccache
+4
View File
@@ -0,0 +1,4 @@
fallback_language = "en"
[fluent]
assets_dir = "i18n"
+7
View File
@@ -0,0 +1,7 @@
app-title = Planit
about = About
repository = Repository
view = View
welcome = Welcome to COSMIC! ✨
page-id = Page { $num }
git-description = Git commit {$hash} on {$date}
+96
View File
@@ -0,0 +1,96 @@
# Name of the application's binary.
name := 'planit'
# The unique ID of the application.
appid := 'dev.waltwood.planit'
# Path to root file system, which defaults to `/`.
rootdir := ''
# The prefix for the `/usr` directory.
prefix := '/usr'
# The location of the cargo target directory.
cargo-target-dir := env('CARGO_TARGET_DIR', 'target')
# Application's appstream metadata
appdata := appid + '.metainfo.xml'
# Application's desktop entry
desktop := appid + '.desktop'
# Application's icon.
icon-svg := appid + '.svg'
# Install destinations
base-dir := absolute_path(clean(rootdir / prefix))
appdata-dst := base-dir / 'share' / 'appdata' / appdata
bin-dst := base-dir / 'bin' / name
desktop-dst := base-dir / 'share' / 'applications' / desktop
icons-dst := base-dir / 'share' / 'icons' / 'hicolor'
icon-svg-dst := icons-dst / 'scalable' / 'apps'
# Default recipe which runs `just build-release`
default: build-release
# Runs `cargo clean`
clean:
cargo clean
# Removes vendored dependencies
clean-vendor:
rm -rf .cargo vendor vendor.tar
# `cargo clean` and removes vendored dependencies
clean-dist: clean clean-vendor
# Compiles with debug profile
build-debug *args:
cargo build {{args}}
# Compiles with release profile
build-release *args: (build-debug '--release' args)
# Compiles release profile with vendored dependencies
build-vendored *args: vendor-extract (build-release '--frozen --offline' args)
# Runs a clippy check
check *args:
cargo clippy --all-features {{args}} -- -W clippy::pedantic
# Runs a clippy check with JSON message format
check-json: (check '--message-format=json')
# Run the application for testing purposes
run *args:
env RUST_BACKTRACE=full cargo run --release {{args}}
# Installs files
install:
install -Dm0755 {{ cargo-target-dir / 'release' / name }} {{bin-dst}}
install -Dm0644 {{ 'resources' / desktop }} {{desktop-dst}}
install -Dm0644 {{ 'resources' / appdata }} {{appdata-dst}}
install -Dm0644 {{ 'resources' / 'icons' / 'hicolor' / 'scalable' / 'apps' / 'icon.svg' }} {{icon-svg-dst}}
# Uninstalls installed files
uninstall:
rm {{bin-dst}} {{desktop-dst}} {{icon-svg-dst}}
# Vendor dependencies locally
vendor:
mkdir -p .cargo
cargo vendor | head -n -1 > .cargo/config.toml
echo 'directory = "vendor"' >> .cargo/config.toml
tar pcf vendor.tar vendor
rm -rf vendor
# Extracts vendored dependencies
vendor-extract:
rm -rf vendor
tar pxf vendor.tar
# Bump cargo version, create git commit, and create tag
tag version:
find -type f -name Cargo.toml -exec sed -i '0,/^version/s/^version.*/version = "{{version}}"/' '{}' \; -exec git add '{}' \;
cargo check
cargo clean
git add Cargo.lock
git commit -m 'release: {{version}}'
git commit --amend
git tag -a {{version}} -m ''
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Name=Planit
Comment=A COSMIC todo.txt application
Type=Application
Icon=dev.waltwood.planit
Exec=planit %F
Terminal=false
StartupNotify=true
Categories=COSMIC
Keywords=COSMIC
MimeType=
+35
View File
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>dev.waltwood.planit</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MPL-2.0</project_license>
<name>Planit</name>
<summary>A COSMIC todo.txt application</summary>
<icon type="remote" width="64" height="64" scale="1">
https://github.com/professorwaltwood/planit.git/raw/main/resources/icons/hicolor/scalable/apps/icon.svg
</icon>
<url type="vcs-browser">https://github.com/professorwaltwood/planit.git</url>
<launchable type="desktop-id">dev.waltwood.planit.desktop</launchable>
<provides>
<id>dev.waltwood.planit</id>
<binaries>
<binary>planit</binary>
</binaries>
</provides>
<requires>
<display_length compare="ge">360</display_length>
</requires>
<supports>
<control>keyboard</control>
<control>pointing</control>
<control>touch</control>
</supports>
<categories>
<category>COSMIC</category>
</categories>
<keywords>
<keyword>COSMIC</keyword>
</keywords>
<content_rating type="oars-1.1" />
</component>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg"/>

After

Width:  |  Height:  |  Size: 102 B

+372
View File
@@ -0,0 +1,372 @@
// SPDX-License-Identifier: MPL-2.0
use crate::config::Config;
use crate::fl;
use cosmic::app::context_drawer;
use cosmic::cosmic_config::{self, CosmicConfigEntry};
use cosmic::iced::alignment::{Horizontal, Vertical};
use cosmic::iced::{Alignment, Length, Subscription};
use cosmic::widget::{self, about::About, icon, menu, nav_bar};
use cosmic::{iced_futures, prelude::*};
use futures_util::SinkExt;
use std::collections::HashMap;
use std::time::Duration;
const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
const APP_ICON: &[u8] = include_bytes!("../resources/icons/hicolor/scalable/apps/icon.svg");
/// The application model stores app-specific state used to describe its interface and
/// drive its logic.
pub struct AppModel {
/// Application state which is managed by the COSMIC runtime.
core: cosmic::Core,
/// Display a context drawer with the designated page if defined.
context_page: ContextPage,
/// The about page for this app.
about: About,
/// Contains items assigned to the nav bar panel.
nav: nav_bar::Model,
/// Key bindings for the application's menu bar.
key_binds: HashMap<menu::KeyBind, MenuAction>,
/// Configuration data that persists between application runs.
config: Config,
/// Time active
time: u32,
/// Toggle the watch subscription
watch_is_active: bool,
}
/// Messages emitted by the application and its widgets.
#[derive(Debug, Clone)]
pub enum Message {
LaunchUrl(String),
ToggleContextPage(ContextPage),
ToggleWatch,
UpdateConfig(Config),
WatchTick(u32),
}
/// Create a COSMIC application from the app model
impl cosmic::Application for AppModel {
/// The async executor that will be used to run your application's commands.
type Executor = cosmic::executor::Default;
/// Data that your application receives to its init method.
type Flags = ();
/// Messages which the application and its widgets will emit.
type Message = Message;
/// Unique identifier in RDNN (reverse domain name notation) format.
const APP_ID: &'static str = "dev.mmurphy.Test";
fn core(&self) -> &cosmic::Core {
&self.core
}
fn core_mut(&mut self) -> &mut cosmic::Core {
&mut self.core
}
/// Initializes the application with any given flags and startup commands.
fn init(
core: cosmic::Core,
_flags: Self::Flags,
) -> (Self, Task<cosmic::Action<Self::Message>>) {
// Create a nav bar with three page items.
let mut nav = nav_bar::Model::default();
nav.insert()
.text(fl!("page-id", num = 1))
.data::<Page>(Page::Page1)
.icon(icon::from_name("applications-science-symbolic"))
.activate();
nav.insert()
.text(fl!("page-id", num = 2))
.data::<Page>(Page::Page2)
.icon(icon::from_name("applications-system-symbolic"));
nav.insert()
.text(fl!("page-id", num = 3))
.data::<Page>(Page::Page3)
.icon(icon::from_name("applications-games-symbolic"));
// Create the about widget
let about = About::default()
.name(fl!("app-title"))
.icon(widget::icon::from_svg_bytes(APP_ICON))
.version(env!("CARGO_PKG_VERSION"))
.links([(fl!("repository"), REPOSITORY)])
.license(env!("CARGO_PKG_LICENSE"));
// Construct the app model with the runtime's core.
let mut app = AppModel {
core,
context_page: ContextPage::default(),
about,
nav,
key_binds: HashMap::new(),
// Optional configuration file for an application.
config: cosmic_config::Config::new(Self::APP_ID, Config::VERSION)
.map(|context| match Config::get_entry(&context) {
Ok(config) => config,
Err((_errors, config)) => {
// for why in errors {
// tracing::error!(%why, "error loading app config");
// }
config
}
})
.unwrap_or_default(),
time: 0,
watch_is_active: false,
};
// Create a startup command that sets the window title.
let command = app.update_title();
(app, command)
}
/// Elements to pack at the start of the header bar.
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
let menu_bar = menu::bar(vec![menu::Tree::with_children(
menu::root(fl!("view")).apply(Element::from),
menu::items(
&self.key_binds,
vec![menu::Item::Button(fl!("about"), None, MenuAction::About)],
),
)]);
vec![menu_bar.into()]
}
/// Enables the COSMIC application to create a nav bar with this model.
fn nav_model(&self) -> Option<&nav_bar::Model> {
Some(&self.nav)
}
/// Display a context drawer if the context page is requested.
fn context_drawer(&self) -> Option<context_drawer::ContextDrawer<'_, Self::Message>> {
if !self.core.window.show_context {
return None;
}
Some(match self.context_page {
ContextPage::About => context_drawer::about(
&self.about,
|url| Message::LaunchUrl(url.to_string()),
Message::ToggleContextPage(ContextPage::About),
),
})
}
/// Describes the interface based on the current state of the application model.
///
/// Application events will be processed through the view. Any messages emitted by
/// events received by widgets will be passed to the update method.
fn view(&self) -> Element<'_, Self::Message> {
let space_s = cosmic::theme::spacing().space_s;
let content: Element<_> = match self.nav.active_data::<Page>().unwrap() {
Page::Page1 => {
let header = widget::row::with_capacity(2)
.push(widget::text::title1(fl!("welcome")))
.push(widget::text::title3(fl!("page-id", num = 1)))
.align_y(Alignment::End)
.spacing(space_s);
let counter_label = ["Watch: ", self.time.to_string().as_str()].concat();
let section = cosmic::widget::settings::section().add(
cosmic::widget::settings::item::builder(counter_label).control(
widget::button::text(if self.watch_is_active {
"Stop"
} else {
"Start"
})
.on_press(Message::ToggleWatch),
),
);
widget::column::with_capacity(2)
.push(header)
.push(section)
.spacing(space_s)
.height(Length::Fill)
.into()
}
Page::Page2 => {
let header = widget::row::with_capacity(2)
.push(widget::text::title1(fl!("welcome")))
.push(widget::text::title3(fl!("page-id", num = 2)))
.align_y(Alignment::End)
.spacing(space_s);
widget::column::with_capacity(1)
.push(header)
.spacing(space_s)
.height(Length::Fill)
.into()
}
Page::Page3 => {
let header = widget::row::with_capacity(2)
.push(widget::text::title1(fl!("welcome")))
.push(widget::text::title3(fl!("page-id", num = 3)))
.align_y(Alignment::End)
.spacing(space_s);
widget::column::with_capacity(1)
.push(header)
.spacing(space_s)
.height(Length::Fill)
.into()
}
};
widget::container(content)
.width(600)
.height(Length::Fill)
.apply(widget::container)
.width(Length::Fill)
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.into()
}
/// Register subscriptions for this application.
///
/// Subscriptions are long-running async tasks running in the background which
/// emit messages to the application through a channel. They can be dynamically
/// stopped and started conditionally based on application state, or persist
/// indefinitely.
fn subscription(&self) -> Subscription<Self::Message> {
// Add subscriptions which are always active.
let mut subscriptions = vec![
// Watch for application configuration changes.
self.core()
.watch_config::<Config>(Self::APP_ID)
.map(|update| {
// for why in update.errors {
// tracing::error!(?why, "app config error");
// }
Message::UpdateConfig(update.config)
}),
];
// Conditionally enables a timer that emits a message every second.
if self.watch_is_active {
subscriptions.push(Subscription::run(|| {
iced_futures::stream::channel(1, |mut emitter| async move {
let mut time = 1;
let mut interval = tokio::time::interval(Duration::from_secs(1));
loop {
interval.tick().await;
_ = emitter.send(Message::WatchTick(time)).await;
time += 1;
}
})
}));
}
Subscription::batch(subscriptions)
}
/// Handles messages emitted by the application and its widgets.
///
/// Tasks may be returned for asynchronous execution of code in the background
/// on the application's async runtime.
fn update(&mut self, message: Self::Message) -> Task<cosmic::Action<Self::Message>> {
match message {
Message::WatchTick(time) => {
self.time = time;
}
Message::ToggleWatch => {
self.watch_is_active = !self.watch_is_active;
}
Message::ToggleContextPage(context_page) => {
if self.context_page == context_page {
// Close the context drawer if the toggled context page is the same.
self.core.window.show_context = !self.core.window.show_context;
} else {
// Open the context drawer to display the requested context page.
self.context_page = context_page;
self.core.window.show_context = true;
}
}
Message::UpdateConfig(config) => {
self.config = config;
}
Message::LaunchUrl(url) => match open::that_detached(&url) {
Ok(()) => {}
Err(err) => {
eprintln!("failed to open {url:?}: {err}");
}
},
}
Task::none()
}
/// Called when a nav item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<cosmic::Action<Self::Message>> {
// Activate the page in the model.
self.nav.activate(id);
self.update_title()
}
}
impl AppModel {
/// Updates the header and window titles.
pub fn update_title(&mut self) -> Task<cosmic::Action<Message>> {
let mut window_title = fl!("app-title");
if let Some(page) = self.nav.text(self.nav.active()) {
window_title.push_str(" — ");
window_title.push_str(page);
}
if let Some(id) = self.core.main_window_id() {
self.set_window_title(window_title, id)
} else {
Task::none()
}
}
}
/// The page to display in the application.
pub enum Page {
Page1,
Page2,
Page3,
}
/// The context page to display in the context drawer.
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum ContextPage {
#[default]
About,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MenuAction {
About,
}
impl menu::action::MenuAction for MenuAction {
type Message = Message;
fn message(&self) -> Self::Message {
match self {
MenuAction::About => Message::ToggleContextPage(ContextPage::About),
}
}
}
+9
View File
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: MPL-2.0
use cosmic::cosmic_config::{self, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry};
#[derive(Debug, Default, Clone, CosmicConfigEntry, Eq, PartialEq)]
#[version = 1]
pub struct Config {
demo: String,
}
+52
View File
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: MPL-2.0
//! Provides localization support for this crate.
use i18n_embed::{
DefaultLocalizer, LanguageLoader, Localizer,
fluent::{FluentLanguageLoader, fluent_language_loader},
unic_langid::LanguageIdentifier,
};
use rust_embed::RustEmbed;
use std::sync::LazyLock;
/// Applies the requested language(s) to requested translations from the `fl!()` macro.
pub fn init(requested_languages: &[LanguageIdentifier]) {
if let Err(why) = localizer().select(requested_languages) {
eprintln!("error while loading fluent localizations: {why}");
}
}
// Get the `Localizer` to be used for localizing this library.
#[must_use]
pub fn localizer() -> Box<dyn Localizer> {
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
}
#[derive(RustEmbed)]
#[folder = "i18n/"]
struct Localizations;
pub static LANGUAGE_LOADER: LazyLock<FluentLanguageLoader> = LazyLock::new(|| {
let loader: FluentLanguageLoader = fluent_language_loader!();
loader
.load_fallback_language(&Localizations)
.expect("Error while loading fallback language");
loader
});
/// Request a localized string by ID from the i18n/ directory.
#[macro_export]
macro_rules! fl {
($message_id:literal) => {{
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id)
}};
($message_id:literal, $($args:expr),*) => {{
i18n_embed_fl::fl!($crate::i18n::LANGUAGE_LOADER, $message_id, $($args), *)
}};
}
+23
View File
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MPL-2.0
mod app;
mod config;
mod i18n;
fn main() -> cosmic::iced::Result {
// Get the system's preferred languages.
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
// Enable localizations to be applied.
i18n::init(&requested_languages);
// Settings for configuring the application window and iced runtime.
let settings = cosmic::app::Settings::default().size_limits(
cosmic::iced::Limits::NONE
.min_width(360.0)
.min_height(180.0),
);
// Starts the application's event loop with `()` as the application's flags.
cosmic::app::run::<app::AppModel>(settings, ())
}