Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d851906
rename notification list modules
locainin Jul 1, 2026
4025135
cover notification list lifecycle
locainin Jul 1, 2026
e0adf07
cover notification list blocks
locainin Jul 1, 2026
c5af4f7
cover notification list row identity
locainin Jul 1, 2026
f483caa
cover list widget plumbing
locainin Jul 1, 2026
af143cf
cover notification list updates
locainin Jul 1, 2026
bd707aa
cover notification grouping rules
locainin Jul 1, 2026
c3f2a2a
cover notification list mutations
locainin Jul 1, 2026
22f9f55
cover list row widgets
locainin Jul 1, 2026
0883d1c
split notification row label tests
locainin Jul 1, 2026
bdd6e82
cover notification row metadata
locainin Jul 1, 2026
3587c3d
cover notification row thumbnails
locainin Jul 1, 2026
d6e756d
cover notification row state
locainin Jul 1, 2026
c242b00
cover notification row actions
locainin Jul 1, 2026
ebc262a
sanitize notification markup in views
locainin Jul 1, 2026
ba90310
cover notification text sanitizing
locainin Jul 1, 2026
b5a1b3b
bump workspace release version
locainin Jul 3, 2026
d6b3033
discover installer release archives
locainin Jul 3, 2026
6dcff72
read release archive binary manifests
locainin Jul 3, 2026
25005c8
verify bundled release binaries
locainin Jul 3, 2026
ce4a12e
copy binaries from release archives
locainin Jul 3, 2026
cadb65a
relax cargo checks for release archives
locainin Jul 3, 2026
c7b9f2e
show installer release status
locainin Jul 3, 2026
a0eb395
add manual release package script
locainin Jul 3, 2026
b5c3f32
add manual release package workflow
locainin Jul 3, 2026
37c8f62
document v1 release process
locainin Jul 3, 2026
372f246
keep systemd reinstall scoped
locainin Jul 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ jobs:
run: |
set -euo pipefail
shellcheck \
scripts/package-release.sh \
crates/unixnotis-core/assets/scripts/unixnotis-blue-light-lib \
crates/unixnotis-core/assets/scripts/unixnotis-blue-light-state \
crates/unixnotis-core/assets/scripts/unixnotis-blue-light-on \
Expand Down
80 changes: 80 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Release Package

on:
# Release packaging is manual so maintainers can write the public changelog by hand
workflow_dispatch:
inputs:
tag:
description: Release tag to package, such as v1.0.0
required: true
type: string

permissions:
# The workflow builds archives only; publishing a GitHub Release stays a manual action
contents: read

concurrency:
# A release tag should produce one archive set, so do not cancel a run already packaging it
group: release-package-${{ github.ref }}-${{ inputs.tag }}
cancel-in-progress: false

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

defaults:
run:
shell: bash

jobs:
package:
name: Build release tarball
runs-on: ubuntu-24.04
container: debian:trixie-slim
timeout-minutes: 45

steps:
- name: Install system dependencies
run: |
set -euo pipefail
apt-get update
apt-get install -y --no-install-recommends \
bash \
build-essential \
ca-certificates \
curl \
git \
libgtk-4-dev \
libgtk4-layer-shell-dev \
pkg-config \
shellcheck \
xz-utils \
zstd

- name: Check out repository
uses: actions/checkout@v5

- name: Install Rust toolchain
run: |
set -euo pipefail
curl --proto '=https' --tlsv1.2 -fsS https://sh.rustup.rs \
| sh -s -- -y --profile minimal
echo "${HOME}/.cargo/bin" >> "${GITHUB_PATH}"
source "${HOME}/.cargo/env"
rustup default stable

- name: Check packaging script
run: shellcheck scripts/package-release.sh

- name: Build package archive
# The script checks that Cargo's version matches the requested release tag
run: scripts/package-release.sh "${{ inputs.tag }}"

- name: Upload package archive
uses: actions/upload-artifact@v4
with:
name: unixnotis-${{ inputs.tag }}-release
path: |
dist/*.tar.zst
dist/*.sha256
if-no-files-found: error
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
/tools/
/.ruff_cache
/testing/
/dist/
32 changes: 32 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,38 @@ Please try to keep pull requests easy to review.

If docs live in the Wiki and are not updated in the PR, note what needs to be updated.

## Releases

UnixNotis releases are cut from `master` with tags such as `v1.0.0`.

Before tagging a release:

- Confirm `dev` has been reviewed and merged into `master`
- Confirm config compatibility is documented, especially for renamed keys or changed defaults
- Confirm installer upgrade behavior is covered for existing config, service files, shell startup
entries, and bundled binary replacement
- Confirm release notes cover user-visible changes, known limitations, and any manual migration
- Confirm the workspace version in `Cargo.toml` matches the tag without the `v` prefix

Build the release archive manually before creating the GitHub release:

```sh
scripts/package-release.sh v1.0.0
```

The same packaging step can be run from the manual **Release Package** GitHub Actions workflow. It
only builds and uploads workflow artifacts; it does not create tags, publish GitHub Releases, or
write release notes.

Upload the generated `.tar.zst` and `.sha256` files from `dist/` or from the workflow artifact to
the GitHub release by hand. The tarball contains `unixnotis-installer`,
`unixnotis-release.json`, and the bundled runtime binaries under `bin/`. Release archives are
intentionally not distro packages and do not produce AppImage artifacts.

The manually written GitHub release body is the changelog for that tag. Keep it focused on
user-visible changes, compatibility notes, and any upgrade steps that generated PR summaries would
miss.

## Bug reports and fixes

If fixing a bug:
Expand Down
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ members = [
resolver = "2"

[workspace.package]
version = "0.1.0"
version = "1.0.0"
edition = "2021"
license = "MIT"

Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,25 @@ git clone https://github.com/locainin/UnixNotis.wiki.git
- A supported user service manager for installer-managed startup.
- Default: `systemd --user`.
- Alternatives: `dinit --user`, user runit, or user s6-rc.
- Rust toolchain for builds and the installer.
- Rust toolchain for source builds.
- `curl` for the installer's best-effort update check.

## Quick start

![Installer CLI](assets/images/InstallerCLI.png)

Download the current release tarball from
[GitHub Releases](https://github.com/locainin/UnixNotis/releases), extract it, then run:

```sh
./unixnotis-installer
```

Release archives include the installer and the bundled runtime binaries, so they do not require a
Rust toolchain on the target system.

Source checkouts can still run the installer directly:

```sh
git clone https://github.com/locainin/UnixNotis
cd UnixNotis
Expand All @@ -64,6 +77,16 @@ The installer builds the release binaries, installs them under `$HOME/.local/bin
default config/theme files, syncs the live Wayland session environment, and enables the selected
user service.

When launched from a downloaded release, the installer verifies the bundled binaries and then copies
them into `$HOME/.local/bin` instead of building from source. The TUI shows the installed version and
reports when a newer GitHub release is available.

Maintainers can build a local release archive manually:

```sh
scripts/package-release.sh v1.0.0
```

## Development

```sh
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::rc::Rc;
use gtk::glib;
use gtk::glib::object::Cast;

use super::list_item::{RowData, RowPresentation};
use super::item::{RowData, RowPresentation};
use super::types::{NotificationList, RowKey};
use super::RowItem;

Expand Down Expand Up @@ -142,19 +142,24 @@ fn collapsed_stack_depth(count: usize, expanded: bool) -> u8 {

pub(super) fn common_prefix_suffix(current: &[RowKey], next: &[RowKey]) -> (usize, usize) {
// Compute shared prefix/suffix so list-store splices only touch the minimal changed window.
let mut prefix = 0;
let min_len = current.len().min(next.len());
while prefix < min_len && current[prefix] == next[prefix] {
prefix += 1;
}

let mut suffix = 0;
while suffix < current.len().saturating_sub(prefix)
&& suffix < next.len().saturating_sub(prefix)
&& current[current.len() - 1 - suffix] == next[next.len() - 1 - suffix]
{
suffix += 1;
}
let prefix = current
.iter()
.zip(next.iter())
.take_while(|(left, right)| left == right)
.count();

let suffix_limit = current.len().min(next.len()).saturating_sub(prefix);
let suffix = current
.iter()
.rev()
.zip(next.iter().rev())
.take(suffix_limit)
.take_while(|(left, right)| left == right)
.count();

(prefix, suffix)
}

#[cfg(test)]
#[path = "tests/blocks.rs"]
mod tests;
32 changes: 17 additions & 15 deletions crates/unixnotis-center/src/ui/list/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,10 @@ use tokio::sync::mpsc;
use crate::dbus::{UiCommand, UiEvent};

use super::super::icons::IconResolver;
use super::list_item::RowKind;
use super::list_row::empty::{build_empty_row, update_empty_row};
use super::list_widgets::{
bind_row, ensure_row_widgets, get_row_widgets, set_row_widgets, RowWidgets,
};
use super::item::RowKind;
use super::row::empty::{build_empty_row, update_empty_row};
use super::types::{NotificationList, NotificationListConfig};
use super::widgets::{bind_row, ensure_row_widgets, get_row_widgets, set_row_widgets, RowWidgets};

impl NotificationList {
pub fn new(
Expand All @@ -28,7 +26,7 @@ impl NotificationList {
icon_resolver: Rc<IconResolver>,
config: NotificationListConfig,
) -> Self {
let store = gio::ListStore::new::<super::list_item::RowItem>();
let store = gio::ListStore::new::<super::item::RowItem>();
let selection = gtk::NoSelection::new(Some(store.clone()));
let factory = gtk::SignalListItemFactory::new();

Expand All @@ -55,35 +53,35 @@ impl NotificationList {

let command_tx_clone = command_tx.clone();
let event_tx_clone = event_tx.clone();
factory.connect_setup(move |_, list_item| {
factory.connect_setup(move |_, gtk_item| {
let widgets = RowWidgets::new(
RowKind::Notification,
command_tx_clone.clone(),
event_tx_clone.clone(),
);
set_row_widgets(list_item, Rc::new(widgets));
set_row_widgets(gtk_item, Rc::new(widgets));
});

let command_tx_clone = command_tx.clone();
let event_tx_clone = event_tx.clone();
let icon_resolver_clone = icon_resolver.clone();
factory.connect_bind(move |_, list_item| {
let Some(item) = list_item.item().and_downcast::<super::list_item::RowItem>() else {
factory.connect_bind(move |_, gtk_item| {
let Some(row_item) = gtk_item.item().and_downcast::<super::item::RowItem>() else {
return;
};
let data = item.data();
let data = row_item.data();
let widgets = ensure_row_widgets(
list_item,
gtk_item,
data.kind,
command_tx_clone.clone(),
event_tx_clone.clone(),
);

bind_row(widgets, &item, &data, icon_resolver_clone.clone());
bind_row(widgets, &row_item, &data, icon_resolver_clone.clone());
});

factory.connect_unbind(move |_, list_item| {
if let Some(widgets) = get_row_widgets(list_item) {
factory.connect_unbind(move |_, gtk_item| {
if let Some(widgets) = get_row_widgets(gtk_item) {
widgets.unbind();
}
// Keep RowWidgets attached so GTK can recycle rows without rebuilding
Expand Down Expand Up @@ -166,3 +164,7 @@ impl NotificationList {
}
}
}

#[cfg(test)]
#[path = "tests/build.rs"]
mod tests;
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,7 @@ fn remove_from_group_bucket(map: &mut HashMap<Rc<str>, VecDeque<u32>>, key: &Rc<
}
}
}

#[cfg(test)]
#[path = "tests/index.rs"]
mod tests;
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,7 @@ impl RowItem {
self.imp().data.borrow().clone()
}
}

#[cfg(test)]
#[path = "tests/item.rs"]
mod tests;
Loading
Loading