From c3de2224a8376c3a406a10c44af6b2a49437daa3 Mon Sep 17 00:00:00 2001 From: ranfdev Date: Sun, 23 Jul 2023 18:15:23 +0200 Subject: [PATCH] Init with GTK Rust Template --- .editorconfig | 21 +++ .github/workflows/ci.yml | 39 ++++++ .gitignore | 13 ++ .gitlab-ci.yml | 43 ++++++ Cargo.toml | 16 +++ README.md | 76 +++++++++++ build-aux/com.ranfdev.Notify.Devel.json | 53 ++++++++ build-aux/dist-vendor.sh | 10 ++ data/com.ranfdev.Notify.desktop.in.in | 12 ++ data/com.ranfdev.Notify.gschema.xml.in | 17 +++ data/com.ranfdev.Notify.metainfo.xml.in.in | 37 +++++ data/icons/com.ranfdev.Notify-symbolic.svg | 59 ++++++++ data/icons/com.ranfdev.Notify.Devel.svg | 147 ++++++++++++++++++++ data/icons/com.ranfdev.Notify.svg | 60 ++++++++ data/icons/meson.build | 10 ++ data/meson.build | 76 +++++++++++ data/resources/meson.build | 20 +++ data/resources/resources.gresource.xml | 9 ++ data/resources/screenshots/screenshot1.png | Bin 0 -> 22719 bytes data/resources/style.css | 4 + data/resources/ui/shortcuts.blp | 25 ++++ data/resources/ui/window.blp | 79 +++++++++++ hooks/pre-commit.hook | 57 ++++++++ meson.build | 71 ++++++++++ meson_options.txt | 10 ++ po/LINGUAS | 0 po/POTFILES.in | 6 + po/meson.build | 1 + src/application.rs | 151 +++++++++++++++++++++ src/config.rs.in | 7 + src/main.rs | 28 ++++ src/meson.build | 52 +++++++ src/window.rs | 118 ++++++++++++++++ subprojects/blueprint-compiler.wrap | 8 ++ 34 files changed, 1335 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 build-aux/com.ranfdev.Notify.Devel.json create mode 100644 build-aux/dist-vendor.sh create mode 100644 data/com.ranfdev.Notify.desktop.in.in create mode 100644 data/com.ranfdev.Notify.gschema.xml.in create mode 100644 data/com.ranfdev.Notify.metainfo.xml.in.in create mode 100644 data/icons/com.ranfdev.Notify-symbolic.svg create mode 100644 data/icons/com.ranfdev.Notify.Devel.svg create mode 100644 data/icons/com.ranfdev.Notify.svg create mode 100644 data/icons/meson.build create mode 100644 data/meson.build create mode 100644 data/resources/meson.build create mode 100644 data/resources/resources.gresource.xml create mode 100644 data/resources/screenshots/screenshot1.png create mode 100644 data/resources/style.css create mode 100644 data/resources/ui/shortcuts.blp create mode 100644 data/resources/ui/window.blp create mode 100755 hooks/pre-commit.hook create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 po/LINGUAS create mode 100644 po/POTFILES.in create mode 100644 po/meson.build create mode 100644 src/application.rs create mode 100644 src/config.rs.in create mode 100644 src/main.rs create mode 100644 src/meson.build create mode 100644 src/window.rs create mode 100644 subprojects/blueprint-compiler.wrap diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5600faa --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +root = true +[*] +indent_style = space +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 + +[*.{build,css,doap,scss,ui,xml,xml.in,xml.in.in,yaml,yml}] +indent_size = 2 + +[*.{json,py,rs}] +indent_size = 4 + +[*.{c,h,h.in}] +indent_size = 2 +max_line_length = 80 + +[NEWS] +indent_size = 2 +max_line_length = 72 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ff00044 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +on: + push: + branches: [main] + pull_request: + +name: CI + +jobs: + rustfmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt + - name: Create blank versions of configured file + run: echo -e "" >> src/config.rs + - name: Run cargo fmt + run: cargo fmt --all -- --check + + flatpak: + name: Flatpak + runs-on: ubuntu-latest + container: + image: bilelmoussaoui/flatpak-github-actions:gnome-nightly + options: --privileged + steps: + - uses: actions/checkout@v3 + - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: notify.flatpak + manifest-path: build-aux/com.ranfdev.Notify.Devel.json + repository-name: "flathub-beta" + run-tests: true + cache-key: flatpak-builder-${{ github.sha }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..416830f --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +/target/ +/build/ +/_build/ +/builddir/ +/build-aux/app +/build-aux/.flatpak-builder/ +/src/config.rs +*.ui.in~ +*.ui~ +/.flatpak/ +/vendor +/.vscode +/subprojects/blueprint-compiler diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..1d2a109 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,43 @@ +stages: + - check + - test + +flatpak: + image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-43' + stage: test + tags: + - flatpak + variables: + BUNDLE: "notify-nightly.flatpak" + MANIFEST_PATH: "build-aux/com.ranfdev.Notify.Devel.json" + FLATPAK_MODULE: "notify" + APP_ID: "com.ranfdev.Notify.Devel" + RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo" + script: + - flatpak install --user --noninteractive org.freedesktop.Sdk.Extension.llvm14//21.08 + - > + xvfb-run -a -s "-screen 0 1024x768x24" + flatpak-builder --keep-build-dirs --user --disable-rofiles-fuse flatpak_app --repo=repo ${BRANCH:+--default-branch=$BRANCH} ${MANIFEST_PATH} + - flatpak build-bundle repo ${BUNDLE} --runtime-repo=${RUNTIME_REPO} ${APP_ID} ${BRANCH} + artifacts: + name: 'Flatpak artifacts' + expose_as: 'Get Flatpak bundle here' + when: 'always' + paths: + - "${BUNDLE}" + - '.flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/meson-log.txt' + - '.flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/testlog.txt' + expire_in: 14 days + +# Configure and run rustfmt +# Exits and builds fails if on bad format +rustfmt: + image: "rust:slim" + script: + - rustup component add rustfmt + # Create blank versions of our configured files + # so rustfmt does not yell about non-existent files or completely empty files + - echo -e "" >> src/config.rs + - rustc -Vv && cargo -Vv + - cargo fmt --version + - cargo fmt --all -- --color=always --check diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8a3f4bd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "notify" +version = "0.1.0" +authors = ["ranfdev "] +edition = "2021" + +[profile.release] +lto = true + +[dependencies] +gettext-rs = { version = "0.7", features = ["gettext-system"] } +gtk = { version = "0.6", package = "gtk4", features = ["v4_8"] } +once_cell = "1.14" +tracing = "0.1.37" +tracing-subscriber = "0.3" +adw = { version = "0.4", package = "libadwaita", features = ["v1_4"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..d26c289 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# GTK + Libadwaita + Blueprint + Rust + Meson + Flatpak = <3 + +A boilerplate template to get started with GTK, Libadwaita, Blueprint, Rust, Meson, Flatpak made for GNOME. + +![Main window](data/resources/screenshots/screenshot1.png "Main window") + +## What does it contains? + +- A simple window with a headerbar +- Bunch of useful files that you SHOULD ship with your application on Linux: + - Metainfo: describe your application for the different application stores out there; + - Desktop: the application launcher; + - Icons: This repo contains three icons, a normal, a nightly & monochromatic icon (symbolic) per the GNOME HIG, exported using [App Icon Preview](https://flathub.org/apps/details/org.gnome.design.AppIconPreview). +- Flatpak Manifest for nightly builds +- Dual installation support +- Uses Meson for building the application +- Bundles the UI files & the CSS using gresources +- A pre-commit hook to run rustfmt on your code +- Tests to validate your Metainfo, Schemas & Desktop files +- Gsettings to store the window state, more settings could be added +- Gitlab CI to produce flatpak nightlies +- i18n support + +## How to init a project ? + +The template ships a simple python script to init a project easily. It asks you a few questions and replaces & renames all the necessary files. + +The script requires having `git` installed on your system. + +You can run it with, + +```shell +python3 create-project.py +``` + +```shell +➜ python3 create-project.py +Welcome to GTK Rust Template +Name: Contrast +Project Name: contrast +Application ID (e.g. org.domain.MyAwesomeApp, see: https://developer.gnome.org/ChooseApplicationID/): org.gnome.design.Contrast +Author: Bilal Elmoussaoui +Email: bil.elmoussaoui@gmail.com +``` + +A new directory named `contrast` containing the generated project + +## Building the project + +Make sure you have `flatpak` and `flatpak-builder` installed. Then run the commands below. Replace `` with the value you entered during project creation. Please note that these commands are just for demonstration purposes. Normally this would be handled by your IDE, such as GNOME Builder or VS Code with the Flatpak extension. + +``` +flatpak install --user org.gnome.Sdk//master org.freedesktop.Sdk.Extension.rust-stable org.gnome.Platform//master org.freedesktop.Sdk.Extension.llvm16 +flatpak-builder --user flatpak_app build-aux/.Devel.json +``` + +## Running the project + +Once the project is build, run the command below. Replace Replace `` and `` with the values you entered during project creation. Please note that these commands are just for demonstration purposes. Normally this would be handled by your IDE, such as GNOME Builder or VS Code with the Flatpak extension. + +``` +flatpak-builder --run flatpak_app build-aux/.Devel.json +``` + +## Community + +Join the GNOME and gtk-rs community! +- [Matrix chat](https://matrix.to/#/#rust:gnome.org): chat with other developers using gtk-rs +- [Discourse forum](https://discourse.gnome.org/tag/rust): topics tagged with `rust` on the GNOME forum. +- [GNOME circle](https://circle.gnome.org/): take inspiration from applications and libraries already extending the GNOME ecosystem. + +## Credits + +- [Podcasts](https://gitlab.gnome.org/World/podcasts) +- [Shortwave](https://gitlab.gnome.org/World/Shortwave) +- [gtk-rust-template without libadwaita and blueprint](https://gitlab.gnome.org/World/Rust/gtk-rust-template) diff --git a/build-aux/com.ranfdev.Notify.Devel.json b/build-aux/com.ranfdev.Notify.Devel.json new file mode 100644 index 0000000..a28651f --- /dev/null +++ b/build-aux/com.ranfdev.Notify.Devel.json @@ -0,0 +1,53 @@ +{ + "id": "com.ranfdev.Notify.Devel", + "runtime": "org.gnome.Platform", + "runtime-version": "master", + "sdk": "org.gnome.Sdk", + "sdk-extensions": [ + "org.freedesktop.Sdk.Extension.rust-stable", + "org.freedesktop.Sdk.Extension.llvm16" + ], + "command": "notify", + "finish-args": [ + "--share=ipc", + "--socket=fallback-x11", + "--socket=wayland", + "--device=dri", + "--env=RUST_LOG=notify=debug", + "--env=G_MESSAGES_DEBUG=none", + "--env=RUST_BACKTRACE=1" + ], + "build-options": { + "append-path": "/usr/lib/sdk/rust-stable/bin:/usr/lib/sdk/llvm16/bin", + "build-args": [ + "--share=network" + ], + "env": { + "CARGO_REGISTRIES_CRATES_IO_PROTOCOL": "sparse", + "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER": "clang", + "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS": "-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold", + "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER": "clang", + "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS": "-C link-arg=-fuse-ld=/usr/lib/sdk/rust-stable/bin/mold" + }, + "test-args": [ + "--socket=x11", + "--share=network" + ] + }, + "modules": [ + { + "name": "notify", + "buildsystem": "meson", + "run-tests": true, + "config-opts": [ + "-Dprofile=development" + ], + "sources": [ + { + "type": "dir", + "path": "../" + } + ] + } + ] +} diff --git a/build-aux/dist-vendor.sh b/build-aux/dist-vendor.sh new file mode 100644 index 0000000..be73278 --- /dev/null +++ b/build-aux/dist-vendor.sh @@ -0,0 +1,10 @@ +#!/bin/bash +export DIST="$1" +export SOURCE_ROOT="$2" + +cd "$SOURCE_ROOT" +mkdir "$DIST"/.cargo +cargo vendor | sed 's/^directory = ".*"/directory = "vendor"/g' > $DIST/.cargo/config +# Move vendor into dist tarball directory +mv vendor "$DIST" + diff --git a/data/com.ranfdev.Notify.desktop.in.in b/data/com.ranfdev.Notify.desktop.in.in new file mode 100644 index 0000000..28de99c --- /dev/null +++ b/data/com.ranfdev.Notify.desktop.in.in @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Notify +Comment=Write a GTK + Rust application +Type=Application +Exec=notify +Terminal=false +Categories=GNOME;GTK; +# Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! +Keywords=Gnome;GTK; +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=@icon@ +StartupNotify=true diff --git a/data/com.ranfdev.Notify.gschema.xml.in b/data/com.ranfdev.Notify.gschema.xml.in new file mode 100644 index 0000000..cda49ef --- /dev/null +++ b/data/com.ranfdev.Notify.gschema.xml.in @@ -0,0 +1,17 @@ + + + + + 600 + Window width + + + 400 + Window height + + + false + Window maximized state + + + diff --git a/data/com.ranfdev.Notify.metainfo.xml.in.in b/data/com.ranfdev.Notify.metainfo.xml.in.in new file mode 100644 index 0000000..2c1f4c3 --- /dev/null +++ b/data/com.ranfdev.Notify.metainfo.xml.in.in @@ -0,0 +1,37 @@ + + + + @app-id@ + CC0 + + + Notify + Write a GTK + Rust application + +

A boilerplate template for GTK + Rust. It uses Meson as a build system and has flatpak support by default.

+
+ + + https://gitlab.gnome.org/bilelmoussaoui/notify/raw/master/data/resources/screenshots/screenshot1.png + Main window + + + https://gitlab.gnome.org/bilelmoussaoui/notify + https://gitlab.gnome.org/bilelmoussaoui/notify/issues + + + + + + + ModernToolkit + HiDpiIcon + + ranfdev + ranfdev@gmail.com + @gettext-package@ + @app-id@.desktop +
diff --git a/data/icons/com.ranfdev.Notify-symbolic.svg b/data/icons/com.ranfdev.Notify-symbolic.svg new file mode 100644 index 0000000..fc4d934 --- /dev/null +++ b/data/icons/com.ranfdev.Notify-symbolic.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/com.ranfdev.Notify.Devel.svg b/data/icons/com.ranfdev.Notify.Devel.svg new file mode 100644 index 0000000..92533ae --- /dev/null +++ b/data/icons/com.ranfdev.Notify.Devel.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/com.ranfdev.Notify.svg b/data/icons/com.ranfdev.Notify.svg new file mode 100644 index 0000000..c2bd5b1 --- /dev/null +++ b/data/icons/com.ranfdev.Notify.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/meson.build b/data/icons/meson.build new file mode 100644 index 0000000..2ab86e9 --- /dev/null +++ b/data/icons/meson.build @@ -0,0 +1,10 @@ +install_data( + '@0@.svg'.format(application_id), + install_dir: iconsdir / 'hicolor' / 'scalable' / 'apps' +) + +install_data( + '@0@-symbolic.svg'.format(base_id), + install_dir: iconsdir / 'hicolor' / 'symbolic' / 'apps', + rename: '@0@-symbolic.svg'.format(application_id) +) diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..5643b60 --- /dev/null +++ b/data/meson.build @@ -0,0 +1,76 @@ +subdir('icons') +subdir('resources') +# Desktop file +desktop_conf = configuration_data() +desktop_conf.set('icon', application_id) +desktop_file = i18n.merge_file( + type: 'desktop', + input: configure_file( + input: '@0@.desktop.in.in'.format(base_id), + output: '@BASENAME@', + configuration: desktop_conf + ), + output: '@0@.desktop'.format(application_id), + po_dir: podir, + install: true, + install_dir: datadir / 'applications' +) +# Validate Desktop file +if desktop_file_validate.found() + test( + 'validate-desktop', + desktop_file_validate, + args: [ + desktop_file.full_path() + ], + depends: desktop_file, + ) +endif + +# Appdata +appdata_conf = configuration_data() +appdata_conf.set('app-id', application_id) +appdata_conf.set('gettext-package', gettext_package) +appdata_file = i18n.merge_file( + input: configure_file( + input: '@0@.metainfo.xml.in.in'.format(base_id), + output: '@BASENAME@', + configuration: appdata_conf + ), + output: '@0@.metainfo.xml'.format(application_id), + po_dir: podir, + install: true, + install_dir: datadir / 'metainfo' +) +# Validate Appdata +if appstream_util.found() + test( + 'validate-appdata', appstream_util, + args: [ + 'validate', '--nonet', appdata_file.full_path() + ], + depends: appdata_file, + ) +endif + +# GSchema +gschema_conf = configuration_data() +gschema_conf.set('app-id', application_id) +gschema_conf.set('gettext-package', gettext_package) +configure_file( + input: '@0@.gschema.xml.in'.format(base_id), + output: '@0@.gschema.xml'.format(application_id), + configuration: gschema_conf, + install: true, + install_dir: datadir / 'glib-2.0' / 'schemas' +) + +# Validata GSchema +if glib_compile_schemas.found() + test( + 'validate-gschema', glib_compile_schemas, + args: [ + '--strict', '--dry-run', meson.current_build_dir() + ], + ) +endif diff --git a/data/resources/meson.build b/data/resources/meson.build new file mode 100644 index 0000000..e55b4ae --- /dev/null +++ b/data/resources/meson.build @@ -0,0 +1,20 @@ +# Resources + +blueprints = custom_target('blueprints', + input: files( + 'ui/window.blp', + 'ui/shortcuts.blp', + ), + output: '.', + command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], +) + +resources = gnome.compile_resources( + 'resources', + 'resources.gresource.xml', + gresource_bundle: true, + source_dir: meson.current_build_dir(), + install: true, + install_dir: pkgdatadir, + dependencies: blueprints, +) diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml new file mode 100644 index 0000000..d62cd5d --- /dev/null +++ b/data/resources/resources.gresource.xml @@ -0,0 +1,9 @@ + + + + + ui/shortcuts.ui + ui/window.ui + style.css + + diff --git a/data/resources/screenshots/screenshot1.png b/data/resources/screenshots/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..dd6d75910d05eada41f34beac34a97102c71379f GIT binary patch literal 22719 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 literal 0 HcmV?d00001 diff --git a/data/resources/style.css b/data/resources/style.css new file mode 100644 index 0000000..3c4bd47 --- /dev/null +++ b/data/resources/style.css @@ -0,0 +1,4 @@ +.title-header{ + font-size: 36px; + font-weight: bold; +} diff --git a/data/resources/ui/shortcuts.blp b/data/resources/ui/shortcuts.blp new file mode 100644 index 0000000..69798b0 --- /dev/null +++ b/data/resources/ui/shortcuts.blp @@ -0,0 +1,25 @@ +using Gtk 4.0; + +ShortcutsWindow help_overlay { + modal: true; + + ShortcutsSection { + section-name: "shortcuts"; + max-height: 10; + + ShortcutsGroup { + title: C_("shortcut window", "General"); + + ShortcutsShortcut { + title: C_("shortcut window", "Show Shortcuts"); + action-name: "win.show-help-overlay"; + } + + ShortcutsShortcut { + title: C_("shortcut window", "Quit"); + action-name: "app.quit"; + } + } + } +} + diff --git a/data/resources/ui/window.blp b/data/resources/ui/window.blp new file mode 100644 index 0000000..6360934 --- /dev/null +++ b/data/resources/ui/window.blp @@ -0,0 +1,79 @@ +using Gtk 4.0; +using Adw 1; + +menu primary_menu { + section { + item { + label: _("_Preferences"); + action: "app.preferences"; + } + + item { + label: _("_Keyboard Shortcuts"); + action: "win.show-help-overlay"; + } + + item { + label: _("_About Notify"); + action: "app.about"; + } + } +} + +template $ExampleApplicationWindow : Adw.ApplicationWindow { + width-request: 240; + height-request: 480; + Adw.Breakpoint { + condition ("max-width: 400sp") + setters { + split_view.collapsed: true; + show_hello_btn.visible: true; + } + } + Adw.NavigationSplitView split_view { + sidebar: Adw.NavigationPage { + title: "Sidebar"; + child: Adw.ToolbarView { + [top] + Adw.HeaderBar { + [end] + MenuButton appmenu_button { + icon-name: "open-menu-symbolic"; + menu-model: primary_menu; + primary: true; + tooltip-text: _("Main Menu"); + } + } + + content: Adw.StatusPage { + title: "Sidebar"; + child: Gtk.Button show_hello_btn { + label: "Show Hello Page"; + visible: false; + styles [ + "pill" + ] + action-name: "navigation.push"; + action-target: "'hello'"; + }; + }; + }; + }; + content: Adw.NavigationPage { + title: "Hello Page"; + tag: "hello"; + child: Adw.ToolbarView { + [top] + Adw.HeaderBar headerbar { + // this will have a title label inherited from the Adw.NavigationPage + } + + content: Adw.StatusPage { + title: "Hello World!"; + description: "Hello to everyone!"; + }; + }; + }; + } +} + diff --git a/hooks/pre-commit.hook b/hooks/pre-commit.hook new file mode 100755 index 0000000..464590e --- /dev/null +++ b/hooks/pre-commit.hook @@ -0,0 +1,57 @@ +#!/bin/sh +# Source: https://gitlab.gnome.org/GNOME/fractal/blob/master/hooks/pre-commit.hook + +install_rustfmt() { + if ! which rustup >/dev/null 2>&1; then + curl https://sh.rustup.rs -sSf | sh -s -- -y + export PATH=$PATH:$HOME/.cargo/bin + if ! which rustup >/dev/null 2>&1; then + echo "Failed to install rustup. Performing the commit without style checking." + exit 0 + fi + fi + + if ! rustup component list|grep rustfmt >/dev/null 2>&1; then + echo "Installing rustfmt…" + rustup component add rustfmt + fi +} + +if ! which cargo >/dev/null 2>&1 || ! cargo fmt --help >/dev/null 2>&1; then + echo "Unable to check the project’s code style, because rustfmt could not be run." + + if [ ! -t 1 ]; then + # No input is possible + echo "Performing commit." + exit 0 + fi + + echo "" + echo "y: Install rustfmt via rustup" + echo "n: Don't install rustfmt and perform the commit" + echo "Q: Don't install rustfmt and abort the commit" + + echo "" + while true + do + printf "%s" "Install rustfmt via rustup? [y/n/Q]: "; read yn < /dev/tty + case $yn in + [Yy]* ) install_rustfmt; break;; + [Nn]* ) echo "Performing commit."; exit 0;; + [Qq]* | "" ) echo "Aborting commit."; exit 1 >/dev/null 2>&1;; + * ) echo "Invalid input";; + esac + done + +fi + +echo "--Checking style--" +cargo fmt --all -- --check +if test $? != 0; then + echo "--Checking style fail--" + echo "Please fix the above issues, either manually or by running: cargo fmt --all" + + exit 1 +else + echo "--Checking style pass--" +fi diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..66b3455 --- /dev/null +++ b/meson.build @@ -0,0 +1,71 @@ +project( + 'notify', + 'rust', + version: '0.1.0', + meson_version: '>= 0.59', + # license: 'MIT', +) + +i18n = import('i18n') +gnome = import('gnome') + +base_id = 'com.ranfdev.Notify' + +dependency('glib-2.0', version: '>= 2.66') +dependency('gio-2.0', version: '>= 2.66') +dependency('gtk4', version: '>= 4.0.0') + +glib_compile_resources = find_program('glib-compile-resources', required: true) +glib_compile_schemas = find_program('glib-compile-schemas', required: true) +desktop_file_validate = find_program('desktop-file-validate', required: false) +appstream_util = find_program('appstream-util', required: false) +cargo = find_program('cargo', required: true) + +version = meson.project_version() + +prefix = get_option('prefix') +bindir = prefix / get_option('bindir') +localedir = prefix / get_option('localedir') + +datadir = prefix / get_option('datadir') +pkgdatadir = datadir / meson.project_name() +iconsdir = datadir / 'icons' +podir = meson.project_source_root() / 'po' +gettext_package = meson.project_name() + +if get_option('profile') == 'development' + profile = 'Devel' + vcs_tag = run_command('git', 'rev-parse', '--short', 'HEAD', check: false).stdout().strip() + if vcs_tag == '' + version_suffix = '-devel' + else + version_suffix = '-@0@'.format(vcs_tag) + endif + application_id = '@0@.@1@'.format(base_id, profile) +else + profile = '' + version_suffix = '' + application_id = base_id +endif + +meson.add_dist_script( + 'build-aux/dist-vendor.sh', + meson.project_build_root() / 'meson-dist' / meson.project_name() + '-' + version, + meson.project_source_root() +) + +if get_option('profile') == 'development' + # Setup pre-commit hook for ensuring coding style is always consistent + message('Setting up git pre-commit hook..') + run_command('cp', '-f', 'hooks/pre-commit.hook', '.git/hooks/pre-commit', check: false) +endif + +subdir('data') +subdir('po') +subdir('src') + +gnome.post_install( + gtk_update_icon_cache: true, + glib_compile_schemas: true, + update_desktop_database: true, +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..4dbc18a --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,10 @@ +option( + 'profile', + type: 'combo', + choices: [ + 'default', + 'development' + ], + value: 'default', + description: 'The build profile for Notify. One of "default" or "development".' +) diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 0000000..e69de29 diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..e5c7dae --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,6 @@ +data/com.ranfdev.Notify.desktop.in.in +data/com.ranfdev.Notify.gschema.xml.in +data/com.ranfdev.Notify.metainfo.xml.in.in +data/resources/ui/shortcuts.ui +data/resources/ui/window.ui +src/application.rs diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 0000000..57d1266 --- /dev/null +++ b/po/meson.build @@ -0,0 +1 @@ +i18n.gettext(gettext_package, preset: 'glib') diff --git a/src/application.rs b/src/application.rs new file mode 100644 index 0000000..335e246 --- /dev/null +++ b/src/application.rs @@ -0,0 +1,151 @@ +use gettextrs::gettext; +use tracing::{debug, info}; + +use adw::subclass::prelude::*; +use gtk::prelude::*; +use gtk::{gdk, gio, glib}; + +use crate::config::{APP_ID, PKGDATADIR, PROFILE, VERSION}; +use crate::window::ExampleApplicationWindow; + +mod imp { + use super::*; + use glib::WeakRef; + use once_cell::sync::OnceCell; + + #[derive(Debug, Default)] + pub struct ExampleApplication { + pub window: OnceCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for ExampleApplication { + const NAME: &'static str = "ExampleApplication"; + type Type = super::ExampleApplication; + type ParentType = adw::Application; + } + + impl ObjectImpl for ExampleApplication {} + + impl ApplicationImpl for ExampleApplication { + fn activate(&self) { + debug!("AdwApplication::activate"); + self.parent_activate(); + let app = self.obj(); + + if let Some(window) = self.window.get() { + let window = window.upgrade().unwrap(); + window.present(); + return; + } + + let window = ExampleApplicationWindow::new(&app); + self.window + .set(window.downgrade()) + .expect("Window already set."); + + app.main_window().present(); + } + + fn startup(&self) { + debug!("AdwApplication::startup"); + self.parent_startup(); + let app = self.obj(); + + // Set icons for shell + gtk::Window::set_default_icon_name(APP_ID); + + app.setup_css(); + app.setup_gactions(); + app.setup_accels(); + } + } + + impl GtkApplicationImpl for ExampleApplication {} + impl AdwApplicationImpl for ExampleApplication {} +} + +glib::wrapper! { + pub struct ExampleApplication(ObjectSubclass) + @extends gio::Application, gtk::Application, + @implements gio::ActionMap, gio::ActionGroup; +} + +impl ExampleApplication { + fn main_window(&self) -> ExampleApplicationWindow { + self.imp().window.get().unwrap().upgrade().unwrap() + } + + fn setup_gactions(&self) { + // Quit + let action_quit = gio::ActionEntry::builder("quit") + .activate(move |app: &Self, _, _| { + // This is needed to trigger the delete event and saving the window state + app.main_window().close(); + app.quit(); + }) + .build(); + + // About + let action_about = gio::ActionEntry::builder("about") + .activate(|app: &Self, _, _| { + app.show_about_dialog(); + }) + .build(); + self.add_action_entries([action_quit, action_about]); + } + + // Sets up keyboard shortcuts + fn setup_accels(&self) { + self.set_accels_for_action("app.quit", &["q"]); + self.set_accels_for_action("window.close", &["w"]); + } + + fn setup_css(&self) { + let provider = gtk::CssProvider::new(); + provider.load_from_resource("/com/ranfdev/Notify/style.css"); + if let Some(display) = gdk::Display::default() { + gtk::StyleContext::add_provider_for_display( + &display, + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + } + } + + fn show_about_dialog(&self) { + let dialog = adw::AboutWindow::builder() + .application_icon(APP_ID) + .application_name("Notify") + // Insert your license of choice here + // .license_type(gtk::License::MitX11) + // Insert your website here + // .website("https://gitlab.gnome.org/bilelmoussaoui/notify/") + .version(VERSION) + .transient_for(&self.main_window()) + .translator_credits(gettext("translator-credits")) + .modal(true) + .developers(vec!["ranfdev"]) + .artists(vec!["ranfdev"]) + .build(); + + dialog.present(); + } + + pub fn run(&self) -> glib::ExitCode { + info!("Notify ({})", APP_ID); + info!("Version: {} ({})", VERSION, PROFILE); + info!("Datadir: {}", PKGDATADIR); + + ApplicationExtManual::run(self) + } +} + +impl Default for ExampleApplication { + fn default() -> Self { + glib::Object::builder() + .property("application-id", APP_ID) + .property("resource-base-path", "/com/ranfdev/Notify/") + .build() + } +} diff --git a/src/config.rs.in b/src/config.rs.in new file mode 100644 index 0000000..699897f --- /dev/null +++ b/src/config.rs.in @@ -0,0 +1,7 @@ +pub const APP_ID: &str = @APP_ID@; +pub const GETTEXT_PACKAGE: &str = @GETTEXT_PACKAGE@; +pub const LOCALEDIR: &str = @LOCALEDIR@; +pub const PKGDATADIR: &str = @PKGDATADIR@; +pub const PROFILE: &str = @PROFILE@; +pub const RESOURCES_FILE: &str = concat!(@PKGDATADIR@, "/resources.gresource"); +pub const VERSION: &str = @VERSION@; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4b10cfa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,28 @@ +mod application; +#[rustfmt::skip] +mod config; +mod window; + +use gettextrs::{gettext, LocaleCategory}; +use gtk::{gio, glib}; + +use self::application::ExampleApplication; +use self::config::{GETTEXT_PACKAGE, LOCALEDIR, RESOURCES_FILE}; + +fn main() -> glib::ExitCode { + // Initialize logger + tracing_subscriber::fmt::init(); + + // Prepare i18n + gettextrs::setlocale(LocaleCategory::LcAll, ""); + gettextrs::bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR).expect("Unable to bind the text domain"); + gettextrs::textdomain(GETTEXT_PACKAGE).expect("Unable to switch to the text domain"); + + glib::set_application_name(&gettext("Notify")); + + let res = gio::Resource::load(RESOURCES_FILE).expect("Could not load gresource file"); + gio::resources_register(&res); + + let app = ExampleApplication::default(); + app.run() +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..d99036d --- /dev/null +++ b/src/meson.build @@ -0,0 +1,52 @@ +global_conf = configuration_data() +global_conf.set_quoted('APP_ID', application_id) +global_conf.set_quoted('PKGDATADIR', pkgdatadir) +global_conf.set_quoted('PROFILE', profile) +global_conf.set_quoted('VERSION', version + version_suffix) +global_conf.set_quoted('GETTEXT_PACKAGE', gettext_package) +global_conf.set_quoted('LOCALEDIR', localedir) +config = configure_file( + input: 'config.rs.in', + output: 'config.rs', + configuration: global_conf +) +# Copy the config.rs output to the source directory. +run_command( + 'cp', + meson.project_build_root() / 'src' / 'config.rs', + meson.project_source_root() / 'src' / 'config.rs', + check: true +) + +cargo_options = [ '--manifest-path', meson.project_source_root() / 'Cargo.toml' ] +cargo_options += [ '--target-dir', meson.project_build_root() / 'src' ] + +if get_option('profile') == 'default' + cargo_options += [ '--release' ] + rust_target = 'release' + message('Building in release mode') +else + rust_target = 'debug' + message('Building in debug mode') +endif + +cargo_env = [ 'CARGO_HOME=' + meson.project_build_root() / 'cargo-home' ] + +cargo_build = custom_target( + 'cargo-build', + build_by_default: true, + build_always_stale: true, + output: meson.project_name(), + console: true, + install: true, + install_dir: bindir, + depends: resources, + command: [ + 'env', + cargo_env, + cargo, 'build', + cargo_options, + '&&', + 'cp', 'src' / rust_target / meson.project_name(), '@OUTPUT@', + ] +) diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..e17713c --- /dev/null +++ b/src/window.rs @@ -0,0 +1,118 @@ +use adw::subclass::prelude::*; +use gtk::prelude::*; +use gtk::{gio, glib}; + +use crate::application::ExampleApplication; +use crate::config::{APP_ID, PROFILE}; + +mod imp { + use super::*; + + #[derive(Debug, gtk::CompositeTemplate)] + #[template(resource = "/com/ranfdev/Notify/ui/window.ui")] + pub struct ExampleApplicationWindow { + #[template_child] + pub headerbar: TemplateChild, + pub settings: gio::Settings, + } + + impl Default for ExampleApplicationWindow { + fn default() -> Self { + Self { + headerbar: TemplateChild::default(), + settings: gio::Settings::new(APP_ID), + } + } + } + + #[glib::object_subclass] + impl ObjectSubclass for ExampleApplicationWindow { + const NAME: &'static str = "ExampleApplicationWindow"; + type Type = super::ExampleApplicationWindow; + type ParentType = adw::ApplicationWindow; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + } + + // You must call `Widget`'s `init_template()` within `instance_init()`. + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for ExampleApplicationWindow { + fn constructed(&self) { + self.parent_constructed(); + let obj = self.obj(); + + // Devel Profile + if PROFILE == "Devel" { + obj.add_css_class("devel"); + } + + // Load latest window state + obj.load_window_size(); + } + + fn dispose(&self) { + self.dispose_template(); + } + } + + impl WidgetImpl for ExampleApplicationWindow {} + impl WindowImpl for ExampleApplicationWindow { + // Save window state on delete event + fn close_request(&self) -> gtk::Inhibit { + if let Err(err) = self.obj().save_window_size() { + tracing::warn!("Failed to save window state, {}", &err); + } + + // Pass close request on to the parent + self.parent_close_request() + } + } + + impl ApplicationWindowImpl for ExampleApplicationWindow {} + impl AdwApplicationWindowImpl for ExampleApplicationWindow {} +} + +glib::wrapper! { + pub struct ExampleApplicationWindow(ObjectSubclass) + @extends gtk::Widget, gtk::Window, adw::Window, adw::ApplicationWindow, + @implements gio::ActionMap, gio::ActionGroup, gtk::Root; +} + +impl ExampleApplicationWindow { + pub fn new(app: &ExampleApplication) -> Self { + glib::Object::builder().property("application", app).build() + } + + fn save_window_size(&self) -> Result<(), glib::BoolError> { + let imp = self.imp(); + + let (width, height) = self.default_size(); + + imp.settings.set_int("window-width", width)?; + imp.settings.set_int("window-height", height)?; + + imp.settings + .set_boolean("is-maximized", self.is_maximized())?; + + Ok(()) + } + + fn load_window_size(&self) { + let imp = self.imp(); + + let width = imp.settings.int("window-width"); + let height = imp.settings.int("window-height"); + let is_maximized = imp.settings.boolean("is-maximized"); + + self.set_default_size(width, height); + + if is_maximized { + self.maximize(); + } + } +} diff --git a/subprojects/blueprint-compiler.wrap b/subprojects/blueprint-compiler.wrap new file mode 100644 index 0000000..5c978d6 --- /dev/null +++ b/subprojects/blueprint-compiler.wrap @@ -0,0 +1,8 @@ +[wrap-git] +directory = blueprint-compiler +url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +revision = main +depth = 1 + +[provide] +program_names = blueprint-compiler