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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
steps:
- name: Install build dependencies
run: |
dnf install -y git gcc clang gtk4-devel libadwaita-devel libgweather-devel \
dnf install -y git gcc clang gtk4-devel libadwaita-devel \
gnome-desktop4-devel fontconfig-devel lcms2-devel libseccomp-devel \
gettext glibc-all-langpacks pkgconf-pkg-config rust cargo clippy rustfmt

Expand Down Expand Up @@ -49,7 +49,7 @@ jobs:
steps:
- name: Install build dependencies
run: |
dnf install -y git gcc clang gtk4-devel libadwaita-devel libgweather-devel \
dnf install -y git gcc clang gtk4-devel libadwaita-devel \
gnome-desktop4-devel fontconfig-devel lcms2-devel libseccomp-devel \
gettext glibc-all-langpacks pkgconf-pkg-config rust cargo rpm-build

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Install system dependencies on Fedora or inside the project toolbox:
```sh
sudo dnf install -y \
rust cargo pkgconf-pkg-config \
gtk4-devel libadwaita-devel libgweather-devel gnome-desktop4-devel gettext \
gtk4-devel libadwaita-devel gnome-desktop4-devel gettext \
lcms2-devel fontconfig-devel libseccomp-devel glycin-loaders bubblewrap \
glibc-all-langpacks
```
Expand Down
26 changes: 0 additions & 26 deletions Cargo.lock

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

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ lsblk = "0.6.1"
uuid = { version = "1", features = ["v4"] }
zbus = "5"
zvariant = "5"
libgweather = "4"
# SVG decoding for the timezone map; driven on a dedicated thread through
# async-io (glycin's native runtime), never on the glib main context. Pinned
# to 3.x because its loader protocol (COMPAT_VERSION 2) matches the
Expand Down
10 changes: 10 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ warn = ["secure_boot", "network", "virt"]
min_ram_gib = 2
```

Optional terminal launcher, also in `/etc/sirius/sirius.toml`. The
Ctrl+Shift+P shortcut always works; the header-bar button stays hidden unless
`show_button` is enabled:

```toml
[terminal]
command = "ptyxis"
show_button = false
```

The canonical page id for disk selection and automatic/manual partitioning is
`storage`. Older configurations that list `disk`, `partition`, or
`manual_partition` are migrated in memory to one `storage` page. The `network`
Expand Down
1 change: 0 additions & 1 deletion crates/sirius-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ sirius-diag = { workspace = true }
relm4 = { workspace = true }
gettext-rs = { workspace = true }
adw = { workspace = true }
libgweather = { workspace = true }
glycin = { workspace = true }
async-io = { workspace = true }
futures-channel = "0.3"
Expand Down
118 changes: 110 additions & 8 deletions crates/sirius-app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub struct AppModel {
carousel: Option<adw::Carousel>,
page_widgets: std::collections::HashMap<String, gtk::Widget>,
window: Option<adw::ApplicationWindow>,
/// Command line for the terminal launcher (button + Ctrl+Shift+P).
terminal_command: String,
}

#[derive(Debug)]
Expand Down Expand Up @@ -50,9 +52,11 @@ impl SimpleComponent for AppModel {
add_top_bar = &adw::HeaderBar {
set_show_end_title_buttons: false,

#[name = "terminal_button"]
pack_start = &gtk::Button {
set_icon_name: "utilities-terminal-symbolic",
add_css_class: "flat",
set_visible: false,
#[watch]
set_tooltip_text: Some(gettext("Open terminal").as_str()),
connect_clicked => AppMsg::OpenTerminal,
Expand Down Expand Up @@ -136,9 +140,32 @@ impl SimpleComponent for AppModel {
carousel: None,
page_widgets: std::collections::HashMap::new(),
window: None,
terminal_command: bootstrap.terminal.command.clone(),
};

let widgets = view_output!();
widgets
.terminal_button
.set_visible(bootstrap.terminal.show_button);

// Ctrl+Shift+P opens the configured terminal even with the
// header-bar button hidden (the default).
{
let command = bootstrap.terminal.command.clone();
let keys = gtk::EventControllerKey::new();
keys.connect_key_pressed(move |_, key, _, mods| {
let wanted = matches!(key, gtk::gdk::Key::P | gtk::gdk::Key::p)
&& mods.contains(
gtk::gdk::ModifierType::CONTROL_MASK | gtk::gdk::ModifierType::SHIFT_MASK,
);
if wanted {
open_terminal(&command);
return gtk::glib::Propagation::Stop;
}
gtk::glib::Propagation::Proceed
});
root.add_controller(keys);
}

for id in &pages_order {
if let Some(w) = model.pages.widget(id) {
Expand All @@ -152,8 +179,13 @@ impl SimpleComponent for AppModel {
// Keep page content clear of the overlay navigation arrows and
// visually centered at every step of the carousel. The progress
// page shows neither arrow (Back hides once the install starts,
// Next is hidden on it), so the margins would be dead space.
let side_margin = if id == "progress" { 0 } else { 72 };
// Next is hidden on it) and the welcome page's banner runs
// edge-to-edge, so the margins would be dead space on both.
let side_margin = if matches!(id.as_str(), "progress" | "welcome") {
0
} else {
72
};
w.set_margin_start(side_margin);
w.set_margin_end(side_margin);
widgets.carousel.append(&w);
Expand Down Expand Up @@ -199,7 +231,7 @@ impl SimpleComponent for AppModel {
match msg {
AppMsg::Page(PageOutput::RequestInstall) => self.confirm_install(&sender),
AppMsg::Page(out) => self.apply_page_output(out),
AppMsg::OpenTerminal => Self::open_terminal(),
AppMsg::OpenTerminal => open_terminal(&self.terminal_command),
AppMsg::Next => {
// Leaving the summary erases the disk: require explicit confirmation.
if self.state.current_page() == "summary" {
Expand Down Expand Up @@ -256,13 +288,19 @@ impl SimpleComponent for AppModel {
}
}

impl AppModel {
fn open_terminal() {
if let Err(err) = std::process::Command::new("ptyxis").spawn() {
tracing::error!(?err, "failed to launch Ptyxis");
}
/// Launch the configured terminal command (program plus arguments, split on
/// whitespace — no shell quoting).
fn open_terminal(command: &str) {
let mut parts = command.split_whitespace();
let Some(program) = parts.next() else {
return;
};
if let Err(err) = std::process::Command::new(program).args(parts).spawn() {
tracing::error!(?err, command, "failed to launch the configured terminal");
}
}

impl AppModel {
/// Modal "this will erase the disk" gate before leaving the summary page.
fn confirm_install(&self, sender: &ComponentSender<Self>) {
let config = self.state.config();
Expand Down Expand Up @@ -328,3 +366,67 @@ impl AppModel {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

// Interactive: the overlay Back/Next arrows must keep their icons
// centered inside the 44px circle.
#[test]
fn navigation_arrow_icons_stay_centered() {
if std::env::var_os("WAYLAND_DISPLAY").is_none() && std::env::var_os("DISPLAY").is_none() {
eprintln!("skipping interactive test: no display available");
return;
}
crate::pages::testutil::run_on_gtk_thread(navigation_arrows_interactive);
}

fn navigation_arrows_interactive() {
crate::style::load();

let back = gtk::Button::from_icon_name("go-previous-symbolic");
back.add_css_class("navigation-arrow");
back.set_halign(gtk::Align::Start);
back.set_valign(gtk::Align::Center);
let next = gtk::Button::from_icon_name("go-next-symbolic");
next.add_css_class("navigation-arrow");
next.add_css_class("suggested-action");
next.set_halign(gtk::Align::End);
next.set_valign(gtk::Align::Center);
let row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
row.append(&back);
row.append(&next);
let window = adw::Window::new();
window.set_content(Some(&row));
window.present();

let context = gtk::glib::MainContext::default();
let deadline = std::time::Instant::now() + std::time::Duration::from_millis(800);
while std::time::Instant::now() < deadline {
while context.pending() {
context.iteration(false);
}
std::thread::sleep(std::time::Duration::from_millis(5));
}

for button in [&back, &next] {
assert_eq!(
(button.width(), button.height()),
(44, 44),
"navigation-arrow must render as a 44px circle"
);
let image = button.first_child().expect("icon child");
let bounds = image
.compute_bounds(button)
.expect("icon bounds inside the button");
let center_x = f64::from(bounds.x()) + f64::from(bounds.width()) / 2.0;
let center_y = f64::from(bounds.y()) + f64::from(bounds.height()) / 2.0;
assert!(
(center_x - 22.0).abs() < 1.5 && (center_y - 22.0).abs() < 1.5,
"icon must be centered in the 44px button, got center ({center_x:.1}, {center_y:.1})"
);
}
window.close();
}
}
2 changes: 2 additions & 0 deletions crates/sirius-app/src/app/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub(super) struct Bootstrap {
pub uefi: bool,
pub bentos: Vec<Bento>,
pub branding: Branding,
pub terminal: sirius_diag::config::TerminalConfig,
}

pub(super) fn load() -> Bootstrap {
Expand Down Expand Up @@ -50,5 +51,6 @@ pub(super) fn load() -> Bootstrap {
uefi: Path::new("/sys/firmware/efi").exists(),
bentos,
branding,
terminal: config.terminal,
}
}
43 changes: 40 additions & 3 deletions crates/sirius-app/src/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,51 @@ unsafe extern "C" {

/// Select the language used by subsequent GNU gettext lookups.
///
/// GNU gettext caches the last loaded catalog. Updating `LANGUAGE` alone can
/// therefore leave the previous translation active; advancing its catalog
/// generation makes the new preference visible immediately.
/// Two things are needed for a switch to take effect everywhere:
///
/// - `LANGUAGE` plus a catalog-generation bump: GNU gettext caches the last
/// loaded catalog, so advancing the generation makes the new preference
/// visible immediately.
/// - `setlocale(LC_ALL, <locale>)`: when the installer starts with an empty
/// environment (the live session's systemd service), the process sits in
/// the C locale and glibc gettext ignores `LANGUAGE` entirely, always
/// returning the English msgids. Pointing `LC_ALL` at the chosen locale
/// makes the switch work from any starting point — it requires the locale
/// to be installed on the system (e.g. `glibc-all-langpacks`).
pub fn set_ui_language(locale: &str) {
// SAFETY: Sirius changes the process-wide gettext language only from the
// single-threaded GTK main loop, before installation workers are started.
unsafe {
std::env::set_var("LANGUAGE", locale);
GETTEXT_CATALOG_GENERATION = GETTEXT_CATALOG_GENERATION.wrapping_add(1);
}
gettextrs::setlocale(gettextrs::LocaleCategory::LcAll, with_utf8_codeset(locale));
}

/// `pt_BR` → `pt_BR.UTF-8`, keeping an existing codeset or `@modifier` in
/// place (`sr_RS@latin` → `sr_RS.UTF-8@latin`).
fn with_utf8_codeset(locale: &str) -> String {
let (before_modifier, modifier) = locale.split_once('@').unwrap_or((locale, ""));
let base = before_modifier.split('.').next().unwrap_or(before_modifier);
if modifier.is_empty() {
format!("{base}.UTF-8")
} else {
format!("{base}.UTF-8@{modifier}")
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn adds_a_utf8_codeset_without_losing_the_modifier() {
assert_eq!(with_utf8_codeset("pt_BR"), "pt_BR.UTF-8");
assert_eq!(with_utf8_codeset("pt_BR.UTF-8"), "pt_BR.UTF-8");
assert_eq!(with_utf8_codeset("sr_RS@latin"), "sr_RS.UTF-8@latin");
assert_eq!(
with_utf8_codeset("ca_ES.UTF-8@valencia"),
"ca_ES.UTF-8@valencia"
);
}
}
Loading
Loading