From be1d5b0fb3304ed86dbe6b79e3177275c9d40f5d Mon Sep 17 00:00:00 2001 From: Tine Jozelj Date: Thu, 21 Dec 2023 10:38:30 +0100 Subject: [PATCH] Init with GTK Rust Template --- .editorconfig | 21 +++ .github/workflows/ci.yml | 36 +++++ .gitignore | 11 ++ .gitlab-ci.yml | 40 ++++++ Cargo.toml | 14 ++ README.md | 77 ++++++++++ build-aux/dev.mnts.ModManager.Devel.json | 54 +++++++ build-aux/dist-vendor.sh | 16 +++ data/dev.mnts.ModManager.desktop.in.in | 12 ++ data/dev.mnts.ModManager.gschema.xml.in | 17 +++ data/dev.mnts.ModManager.metainfo.xml.in.in | 37 +++++ data/icons/dev.mnts.ModManager-symbolic.svg | 59 ++++++++ data/icons/dev.mnts.ModManager.Devel.svg | 147 +++++++++++++++++++ data/icons/dev.mnts.ModManager.svg | 60 ++++++++ data/icons/meson.build | 10 ++ data/meson.build | 76 ++++++++++ data/resources/meson.build | 9 ++ data/resources/resources.gresource.xml | 9 ++ data/resources/screenshots/screenshot1.png | Bin 0 -> 19037 bytes data/resources/style.css | 4 + data/resources/ui/shortcuts.ui | 29 ++++ data/resources/ui/window.ui | 41 ++++++ 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 | 149 ++++++++++++++++++++ src/config.rs.in | 7 + src/main.rs | 32 +++++ src/meson.build | 48 +++++++ src/window.rs | 117 +++++++++++++++ 33 files changed, 1277 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/dev.mnts.ModManager.Devel.json create mode 100644 build-aux/dist-vendor.sh create mode 100644 data/dev.mnts.ModManager.desktop.in.in create mode 100644 data/dev.mnts.ModManager.gschema.xml.in create mode 100644 data/dev.mnts.ModManager.metainfo.xml.in.in create mode 100644 data/icons/dev.mnts.ModManager-symbolic.svg create mode 100644 data/icons/dev.mnts.ModManager.Devel.svg create mode 100644 data/icons/dev.mnts.ModManager.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.ui create mode 100644 data/resources/ui/window.ui 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 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..4141a32 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +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: Run cargo fmt + run: cargo fmt --all -- --check + + flatpak: + name: Flatpak + runs-on: ubuntu-latest + container: + image: bilelmoussaoui/flatpak-github-actions:gnome-44 + options: --privileged + steps: + - uses: actions/checkout@v3 + - uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: mod-manager.flatpak + manifest-path: build-aux/dev.mnts.ModManager.Devel.json + run-tests: true + cache-key: flatpak-builder-${{ github.sha }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d8ad9a --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/target/ +/build/ +/_build/ +/builddir/ +/build-aux/app +/build-aux/.flatpak-builder/ +*.ui.in~ +*.ui~ +/.flatpak/ +/vendor +/.vscode diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..a5fa8db --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,40 @@ +stages: + - check + - test + +flatpak: + image: 'quay.io/gnome_infrastructure/gnome-runtime-images:gnome-44' + stage: test + tags: + - flatpak + variables: + BUNDLE: "mod-manager-nightly.flatpak" + MANIFEST_PATH: "build-aux/dev.mnts.ModManager.Devel.json" + FLATPAK_MODULE: "mod-manager" + APP_ID: "dev.mnts.ModManager.Devel" + RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo" + script: + - flatpak install --user --noninteractive flathub org.freedesktop.Sdk.Extension.llvm16//22.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 + - 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..803dc9b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "mod-manager" +version = "0.1.0" +authors = ["Tine Jozelj "] +edition = "2021" + +[profile.release] +lto = true + +[dependencies] +gettext-rs = { version = "0.7", features = ["gettext-system"] } +gtk = { version = "0.7", package = "gtk4", features = ["v4_8"] } +tracing = "0.1.37" +tracing-subscriber = "0.3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb7cea9 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# GTK + Rust + Meson + Flatpak = <3 + +A boilerplate template to get started with GTK, Rust, Meson, Flatpak made for GNOME. It can be adapted for other desktop environments like elementary. + +
+![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//43 org.freedesktop.Sdk.Extension.rust-stable//22.08 org.gnome.Platform//43 org.freedesktop.Sdk.Extension.llvm14//22.08 +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) diff --git a/build-aux/dev.mnts.ModManager.Devel.json b/build-aux/dev.mnts.ModManager.Devel.json new file mode 100644 index 0000000..3540f1b --- /dev/null +++ b/build-aux/dev.mnts.ModManager.Devel.json @@ -0,0 +1,54 @@ +{ + "id": "dev.mnts.ModManager.Devel", + "runtime": "org.gnome.Platform", + "runtime-version": "44", + "sdk": "org.gnome.Sdk", + "sdk-extensions": [ + "org.freedesktop.Sdk.Extension.rust-stable", + "org.freedesktop.Sdk.Extension.llvm16" + ], + "command": "mod-manager", + "finish-args": [ + "--share=ipc", + "--socket=fallback-x11", + "--socket=wayland", + "--device=dri", + "--env=RUST_LOG=mod_manager=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": "mod-manager", + "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..ad6a6f0 --- /dev/null +++ b/build-aux/dist-vendor.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Since Meson invokes this script as +# "/bin/sh .../dist-vendor.sh DIST SOURCE_ROOT" we can't rely on bash features +set -eu +export DIST="$1" +export SOURCE_ROOT="$2" + +cd "$SOURCE_ROOT" +mkdir "$DIST"/.cargo +cargo vendor > "$DIST/.cargo/config" +# Don't combine the previous and this line with a pipe because we can't catch +# errors with "set -o pipefail" +sed -i 's/^directory = ".*"/directory = "vendor"/g' "$DIST/.cargo/config" +# Move vendor into dist tarball directory +mv vendor "$DIST" + diff --git a/data/dev.mnts.ModManager.desktop.in.in b/data/dev.mnts.ModManager.desktop.in.in new file mode 100644 index 0000000..fe18611 --- /dev/null +++ b/data/dev.mnts.ModManager.desktop.in.in @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Mod Manager +Comment=Write a GTK + Rust application +Type=Application +Exec=mod-manager +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/dev.mnts.ModManager.gschema.xml.in b/data/dev.mnts.ModManager.gschema.xml.in new file mode 100644 index 0000000..7c09a08 --- /dev/null +++ b/data/dev.mnts.ModManager.gschema.xml.in @@ -0,0 +1,17 @@ + + + + + 600 + Window width + + + 400 + Window height + + + false + Window maximized state + + + diff --git a/data/dev.mnts.ModManager.metainfo.xml.in.in b/data/dev.mnts.ModManager.metainfo.xml.in.in new file mode 100644 index 0000000..2ca3e26 --- /dev/null +++ b/data/dev.mnts.ModManager.metainfo.xml.in.in @@ -0,0 +1,37 @@ + + + + @app-id@ + CC0 + + + Mod Manager + 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/mod-manager/raw/master/data/resources/screenshots/screenshot1.png + Main window + + + https://gitlab.gnome.org/bilelmoussaoui/mod-manager + https://gitlab.gnome.org/bilelmoussaoui/mod-manager/issues + + + + + + + ModernToolkit + HiDpiIcon + + Tine Jozelj + tine@tjo.space + @gettext-package@ + @app-id@.desktop +
diff --git a/data/icons/dev.mnts.ModManager-symbolic.svg b/data/icons/dev.mnts.ModManager-symbolic.svg new file mode 100644 index 0000000..fc4d934 --- /dev/null +++ b/data/icons/dev.mnts.ModManager-symbolic.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/dev.mnts.ModManager.Devel.svg b/data/icons/dev.mnts.ModManager.Devel.svg new file mode 100644 index 0000000..92533ae --- /dev/null +++ b/data/icons/dev.mnts.ModManager.Devel.svg @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/dev.mnts.ModManager.svg b/data/icons/dev.mnts.ModManager.svg new file mode 100644 index 0000000..c2bd5b1 --- /dev/null +++ b/data/icons/dev.mnts.ModManager.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..604e1b2 --- /dev/null +++ b/data/resources/meson.build @@ -0,0 +1,9 @@ +# Resources +resources = gnome.compile_resources( + 'resources', + 'resources.gresource.xml', + gresource_bundle: true, + source_dir: meson.current_build_dir(), + install: true, + install_dir: pkgdatadir, +) diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml new file mode 100644 index 0000000..74c30a6 --- /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..173b6beebdbdd81021d0048036f9ba38476cd887 GIT binary patch literal 19037 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 z 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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.ui b/data/resources/ui/shortcuts.ui new file mode 100644 index 0000000..ef12f02 --- /dev/null +++ b/data/resources/ui/shortcuts.ui @@ -0,0 +1,29 @@ + + + + True + + + shortcuts + 10 + + + General + + + Show Shortcuts + win.show-help-overlay + + + + + Quit + app.quit + + + + + + + + diff --git a/data/resources/ui/window.ui b/data/resources/ui/window.ui new file mode 100644 index 0000000..0b1f360 --- /dev/null +++ b/data/resources/ui/window.ui @@ -0,0 +1,41 @@ + + + +
+ + _Preferences + app.preferences + + + _Keyboard Shortcuts + win.show-help-overlay + + + _About Mod Manager + app.about + +
+
+ +
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..a1f5f0d --- /dev/null +++ b/meson.build @@ -0,0 +1,71 @@ +project( + 'mod-manager', + 'rust', + version: '0.1.0', + meson_version: '>= 0.59', + # license: 'MIT', +) + +i18n = import('i18n') +gnome = import('gnome') + +base_id = 'dev.mnts.ModManager' + +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..a921096 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,10 @@ +option( + 'profile', + type: 'combo', + choices: [ + 'default', + 'development' + ], + value: 'default', + description: 'The build profile for Mod Manager. 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..fdee44f --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,6 @@ +data/dev.mnts.ModManager.desktop.in.in +data/dev.mnts.ModManager.gschema.xml.in +data/dev.mnts.ModManager.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..a59bd35 --- /dev/null +++ b/src/application.rs @@ -0,0 +1,149 @@ +use gettextrs::gettext; +use tracing::{debug, info}; + +use gtk::prelude::*; +use gtk::subclass::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 std::cell::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 = gtk::Application; + } + + impl ObjectImpl for ExampleApplication {} + + impl ApplicationImpl for ExampleApplication { + fn activate(&self) { + debug!("GtkApplication::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!("GtkApplication::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 {} +} + +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("/dev/mnts/ModManager/style.css"); + if let Some(display) = gdk::Display::default() { + gtk::style_context_add_provider_for_display( + &display, + &provider, + gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); + } + } + + fn show_about_dialog(&self) { + let dialog = gtk::AboutDialog::builder() + .logo_icon_name(APP_ID) + // Insert your license of choice here + // .license_type(gtk::License::MitX11) + // Insert your website here + // .website("https://gitlab.gnome.org/bilelmoussaoui/mod-manager/") + .version(VERSION) + .transient_for(&self.main_window()) + .translator_credits(gettext("translator-credits")) + .modal(true) + .authors(vec!["Tine Jozelj"]) + .artists(vec!["Tine Jozelj"]) + .build(); + + dialog.present(); + } + + pub fn run(&self) -> glib::ExitCode { + info!("Mod Manager ({})", 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", "/dev/mnts/ModManager/") + .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..2281216 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,32 @@ +mod config { + #![allow(dead_code)] + + include!(concat!(env!("CODEGEN_BUILD_DIR"), "/config.rs")); +} + +mod application; +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("Mod Manager")); + + 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..75c7a75 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,48 @@ +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 +) + +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', + 'CODEGEN_BUILD_DIR=' + meson.current_build_dir() +] + +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..3fa99fa --- /dev/null +++ b/src/window.rs @@ -0,0 +1,117 @@ +use gtk::prelude::*; +use gtk::subclass::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 = "/dev/mnts/ModManager/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 = gtk::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) -> glib::Propagation { + 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 {} +} + +glib::wrapper! { + pub struct ExampleApplicationWindow(ObjectSubclass) + @extends gtk::Widget, gtk::Window, gtk::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(); + } + } +}