commit be1d5b0fb3304ed86dbe6b79e3177275c9d40f5d Author: Tine Jozelj Date: Thu Dec 21 10:38:30 2023 +0100 Init with GTK Rust Template 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 0000000..173b6be Binary files /dev/null and b/data/resources/screenshots/screenshot1.png differ 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(); + } + } +}