Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 19 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,30 @@ jobs:
run: cargo fmt --all --check

# Every row in the window carries text that came off the network — a host, a path, a process name — and
# libadwaita parses a row's title and subtitle as Pango markup unless told otherwise. A single `&` in a
# query string then renders as nothing at all, and `<span>` in one would render as markup.
# libadwaita reads a row's title as Pango markup unless it is told otherwise *before* the text is set.
# A single `&` in a query string then renders as nothing at all, and a `<span>` in one would render as
# markup. Found by opening the window on a real desktop and seeing a request row with no request in it.
#
# Found by opening the window on a real desktop and seeing a request row with no request in it.
- name: Data in the window is not markup
# Two checks, because the first version of this was one check and it was the wrong one. Counting
# declarations said yes while the window showed nothing; only running the thing says anything.
- name: Every row in the window is built the one way that works
run: |
set -euo pipefail
rows=$(grep -c 'adw::ActionRow::builder()' crates/flowlight-gui/src/main.rs)
literal=$(grep -A 1 'adw::ActionRow::builder()' crates/flowlight-gui/src/main.rs \
| grep -c 'use_markup(false)')
echo "$literal of $rows rows say their text is not markup"
if [ "$rows" != "$literal" ]; then
echo "a row carries data and does not say it is not markup"
# The structural half: no row is built by a builder, because a builder cannot set the property
# before the text whatever order it lists them in.
if grep -n 'adw::ActionRow::builder()' crates/flowlight-gui/src/main.rs; then
echo "a row is built with a builder, which sets its title before it is told the title is text"
exit 1
fi
grep -q 'use_markup(false)' <(grep -A 3 'adw::Banner::builder()' crates/flowlight-gui/src/main.rs) \
|| { echo "the banner carries error text and does not say it is not markup"; exit 1; }
echo "every row goes through the helper"

- name: A row shows the text it was given
run: |
set -euo pipefail
# The behavioural half, under a display, catching what GTK logs rather than what a property says.
# Proven to fail against the construction it replaced before it was trusted.
sudo apt-get install -y -qq xvfb
xvfb-run -a cargo run -q -p flowlight-gui --example markup

# `flowlight-ebpf` is excluded from the host-target commands and checked by being compiled for the BPF
# target as part of the daemon's build script. Linting it here would mean linting it for x86_64, which
Expand Down
28 changes: 14 additions & 14 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 @@ -36,7 +36,7 @@ default-members = [
]

[workspace.package]
version = "0.5.12"
version = "0.5.13"
edition = "2024"
license = "GPL-3.0-only"
repository = "https://github.com/xinbetween/flowlight-linux"
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ including an installation from it with a throwaway key, and goes live once a sig
[`packaging/apt/README.md`](packaging/apt/README.md) says why that key is not something this code can create for
you. Until it is, the commands above have nothing to answer them.

**What the window shows is text, not markup.** Every row carries something that came off the network — a
host, a path, a process name — and libadwaita reads a row's title as Pango markup unless it is told otherwise
first. Until v0.5.13 a request with two query parameters rendered as an empty row, and a path containing
`<span>` would have been rendered rather than shown. Rows are now built by one helper that sets the property
before the text, which is the only order that works, and an example under a virtual display proves it on every
push.

**If the window opens blank, your machine has no working GL.** GTK 4 renders with the GPU, and a virtual
machine without a working driver draws the header bar and nothing else — Mesa says so on the way past
(`failed to choose pdev`, `DRI3 error`). `GSK_RENDERER=cairo flowlight` renders in software and shows
Expand Down
4 changes: 4 additions & 0 deletions crates/flowlight-gui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ repository.workspace = true
rust-version = "1.92"
publish = false

[lib]
name = "flowlight_gui"
path = "src/lib.rs"

[[bin]]
name = "flowlight"
path = "src/main.rs"
Expand Down
75 changes: 75 additions & 0 deletions crates/flowlight-gui/examples/markup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! Does a row show the text it was given?
//!
//! Not a question about a property — a question about what GTK does while the row is built. The first
//! attempt at this fix set `use-markup` on the builder, which leaves the property reading `false` and still
//! parses the title as markup, so a check that asked the property said yes while the window showed nothing.
//!
//! GTK 4 logs through `g_log_structured`, which goes to the writer function rather than to a log handler —
//! so the complaint is caught there and counted. `G_DEBUG=fatal-warnings` was tried first and is too blunt:
//! it also aborts on an unrelated missing gsettings schema, which says nothing about this.
//!
//! Needs a display. In CI that is `xvfb-run`; on a desktop it is the desktop.
use adw::prelude::*;
use flowlight_gui::{literal_row, literal_row_with};
use gtk::glib;
use std::sync::atomic::{AtomicUsize, Ordering};

/// How many times GTK said it could not set a row's text.
static COMPLAINTS: AtomicUsize = AtomicUsize::new(0);

/// A path with everything in it that markup would read as markup — an ampersand from a query string, and a
/// span a hostile one could put there. Both arrive in this window from the network.
const AWKWARD: &str = "GET example.com/api/v1/suggest?q=&providers=weather&region=CA <span foreground=\"white\">gone</span>";

fn main() {
glib::log_set_writer_func(|_level, fields| {
for field in fields {
if field.key() == "MESSAGE"
&& let Some(message) = field.value_str()
&& (message.contains("from markup") || message.contains("Failed to set text"))
{
COMPLAINTS.fetch_add(1, Ordering::SeqCst);
println!(" GTK could not set a row's text: {message}");
}
}
glib::LogWriterOutput::Handled
});

// Without a display there is nothing to ask, and saying so beats a panic with a backtrace in it.
if let Err(err) = adw::init() {
println!(
"libadwaita will not start here: {err}. This needs a display — `xvfb-run` is one."
);
std::process::exit(1);
}

// The rows the window makes, made the way the window makes them. Any markup complaint from here is fatal
// because of `G_DEBUG`, so reaching the end is the assertion.
let row = literal_row(AWKWARD);
let with = literal_row_with(AWKWARD, AWKWARD);

assert_eq!(
row.title(),
AWKWARD,
"the row changed the text it was given"
);
assert_eq!(with.subtitle().unwrap_or_default(), AWKWARD);
assert!(
!row.uses_markup(),
"the row still treats its text as markup"
);

println!("the row shows: {}", row.title());

// The assertion the whole example exists for. The property reading `false` is not the same as the text
// having been set as text, which is the mistake this is here to stop being made twice.
let complaints = COMPLAINTS.load(Ordering::SeqCst);
if complaints != 0 {
println!(
"FAIL: GTK refused to set a row's text {complaints} time(s). The property has to be set \
before the text, not beside it."
);
std::process::exit(1);
}
println!("OK: a row shows the text it was given, ampersands and angle brackets and all");
}
33 changes: 33 additions & 0 deletions crates/flowlight-gui/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//! The parts of the window that can be built and checked without opening it.
//!
//! A library beside the binary so that `examples/markup.rs` exercises the same code the window does, rather
//! than a copy of it that can drift. There is one thing in here and it is the one thing that was wrong.

use adw::prelude::*;

/// A row whose title says what it says, rather than being read as markup.
///
/// The property has to be set **before** the text and not beside it. An `AdwActionRow`'s title is applied as
/// the object is constructed and `use-markup` afterwards, whatever order a builder lists them in — so a
/// builder carrying both still parses the title as markup, and a path with an `&` in it renders as nothing
/// at all.
///
/// That is not a guess. A row built the other way logs `Failed to set text … from markup` and then reports
/// `uses_markup: false`, which is exactly how the first attempt at this fix passed a test that counted
/// declarations instead of watching behaviour. `examples/markup.rs` is that measurement, kept.
///
/// Every row in this window carries text that came off the network — a host, a path, a process name — so
/// every row goes through here.
pub fn literal_row(title: impl AsRef<str>) -> adw::ActionRow {
let row = adw::ActionRow::new();
row.set_use_markup(false);
row.set_title(title.as_ref());
row
}

/// The same, with the line underneath — which is data as often as the title is.
pub fn literal_row_with(title: impl AsRef<str>, subtitle: impl AsRef<str>) -> adw::ActionRow {
let row = literal_row(title);
row.set_subtitle(subtitle.as_ref());
row
}
Loading
Loading