Parch

source: handbook ch. 53, 56

Rust apps

Rust is the dominant first-party language on Parch: parch-welcome (the first-run assistant, Rust + libadwaita with a relm4-style structure), Pastor (the Parch Store), gamehub, mirrorman, and pordle are all Rust. This tutorial builds Parch Greet, a small libadwaita window with persistence, then packages it the way Rust packages on Arch are meant to be packaged: locked dependencies, no network surprises at build time.

Prerequisites

  • A Parch or Arch-based system (x86_64), any edition.
  • Working directory: ~/devel, user privileges except where sudo is shown.
$ sudo pacman -S --needed rust gtk4 libadwaita
$ cargo --version && rustc --version

1. Create the project

$ cargo new parch-greet
$ cd parch-greet
$ mkdir data

Layout after this tutorial:

parch-greet/
├── Cargo.toml
├── Cargo.lock          # generated, commit it
├── src/
│   └── main.rs
└── data/
    └── parch-greet.desktop

Cargo.toml:

[package]
name = "parch-greet"
version = "0.1.0"
edition = "2021"

[dependencies]
adw = { package = "libadwaita", version = "0.7", features = ["v1_4"] }
gtk = { package = "gtk4", version = "0.9", features = ["v4_12"] }

[profile.release]
lto = true
strip = true

The gtk4-rs and libadwaita crates release in lockstep, so pick the pair that matches on crates.io. The v1_4 feature unlocks Adw.ToolbarView and friends, and the [profile.release] block buys link-time optimization and a stripped binary for distribution.

2. Write the application

src/main.rs:

use std::fs;
use std::path::PathBuf;

use adw::prelude::*;
use adw::{Application, ApplicationWindow, HeaderBar, Toast, ToastOverlay, ToolbarView};
use gtk::glib;
use gtk::{Button, Entry, Label, Orientation};

const APP_ID: &str = "ir.parchlinux.Greet";
const STATE_FILE: &str = "parch-greet/last-greeting.txt";

fn state_path() -> PathBuf {
    glib::user_config_dir().join(STATE_FILE)
}

fn read_saved_greeting() -> Option<String> {
    fs::read_to_string(state_path())
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty())
}

fn save_greeting(name: &str) -> std::io::Result<()> {
    let path = state_path();
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(path, name)
}

fn main() -> glib::ExitCode {
    let app = Application::builder().application_id(APP_ID).build();
    app.connect_activate(build_ui);
    app.run()
}

fn build_ui(app: &Application) {
    let window = ApplicationWindow::builder()
        .application(app)
        .title("Parch Greet")
        .default_width(480)
        .default_height(220)
        .build();

    let entry = Entry::new();
    entry.set_placeholder_text(Some("Your name"));

    let label = Label::new(None);
    label.add_css_class("title-2");

    let button = Button::with_label("Greet");

    let toast_overlay = ToastOverlay::new();
    let content = gtk::Box::new(Orientation::Vertical, 12);
    content.set_margin_top(12);
    content.set_margin_bottom(12);
    content.set_margin_start(12);
    content.set_margin_end(12);
    content.append(&entry);
    content.append(&button);
    content.append(&label);
    toast_overlay.set_child(Some(&content));

    let header = HeaderBar::new();
    let toolbar = ToolbarView::new();
    toolbar.set_top_bar(&header);
    toolbar.set_content(Some(&toast_overlay));
    window.set_content(Some(&toolbar));

    match read_saved_greeting() {
        Some(name) => {
            label.set_text(&format!("Hello again, {name}"));
            entry.set_text(&name);
        }
        None => label.set_text("Hello, Parch"),
    }

    button.connect_clicked(glib::clone!(@weak entry, @weak label, @weak toast_overlay => move |_| {
        let name = entry.text().trim().to_string();
        if name.is_empty() {
            toast_overlay.add_toast(&Toast::builder().title("Nothing to greet").timeout(3).build());
            return;
        }
        if save_greeting(&name).is_ok() {
            label.set_text(&format!("Hello, {name}"));
            toast_overlay.add_toast(&Toast::builder().title("Greeting saved").timeout(3).build());
        } else {
            toast_overlay.add_toast(&Toast::builder().title("Could not save").timeout(3).build());
        }
    }));

    window.present();
}

Idioms worth copying into any Rust app: the application id is the same reverse-DNS string used in the desktop file and the Wayland app id (see desktop integration), state goes under glib::user_config_dir() so it follows XDG rules, and glib::clone! borrows widgets weakly so closures never keep destroyed widgets alive.

3. Build and run

From the project root, user privileges:

$ cargo run

Expected result: a libadwaita window with a header bar. Typing a name and pressing Greet shows a toast, saves the name, and the label greets you. The name survives a restart because it is read from ~/.config/parch-greet/last-greeting.txt.

For the binary you will ship:

$ cargo build --release
$ ./target/release/parch-greet

4. Desktop entry

data/parch-greet.desktop:

[Desktop Entry]
Type=Application
Name=Parch Greet
Name[fa]=درود پارچ
Comment=Say hello to Parch
Comment[fa]=به پارچ سلام کن
Exec=parch-greet
Icon=parch-greet
Terminal=false
Categories=Utility;
Keywords=greet;درود;

Validate with desktop-file-validate data/parch-greet.desktop. Icon naming and MIME rules are on HIG desktop files.

5. Package it

Rust packages build from a source tarball that includes Cargo.lock, fetch the locked dependency set in prepare(), and compile with --frozen so the build can never resolve a version you did not test.

PKGBUILD:

# Maintainer: Your Name <you@example.com>
pkgname=parch-greet
pkgver=0.1.0
pkgrel=1
pkgdesc="A small greeting app for Parch"
arch=('x86_64')
url="https://github.com/parchlinux/parch-greet"
license=('GPL-3.0-or-later')
depends=('glibc' 'gtk4' 'libadwaita')
makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://github.com/parchlinux/parch-greet/archive/refs/tags/v$pkgver.tar.gz")
sha256sums=('SKIP')  # fill with updpkgsums before any real submission

prepare() {
    cd "$pkgname-$pkgver"
    export RUSTUP_TOOLCHAIN=stable
    cargo fetch --locked --target "$CARCH-unknown-linux-gnu"
}

build() {
    cd "$pkgname-$pkgver"
    export RUSTUP_TOOLCHAIN=stable
    export CARGO_TARGET_DIR=target
    cargo build --frozen --release --all-features
}

package() {
    cd "$pkgname-$pkgver"
    install -Dm755 target/release/parch-greet -t "$pkgdir/usr/bin/"
    install -Dm644 data/parch-greet.desktop -t "$pkgdir/usr/share/applications/"
}

The SKIP checksum above exists only because the tarball URL is hypothetical; real submissions run updpkgsums and pass packaging review.

6. Validate the package

$ makepkg -f
$ namcap parch-greet-0.1.0-1-x86_64.pkg.tar.zst
$ sudo pacman -U parch-greet-0.1.0-1-x86_64.pkg.tar.zst
$ pacman -Ql parch-greet

namcap should report no missing-dependency warnings: the runtime depends list carries only glibc, gtk4, and libadwaita, because cargo crates are compiled in, not shipped.