diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index 41c3a6eaa3..ffc9857543 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -76,6 +76,7 @@ jobs: pam-devel python-langtable-data timezone + xkeyboard-config - name: Install Rust toolchains run: rustup toolchain install stable @@ -92,10 +93,12 @@ jobs: tool: cargo-binstall - name: Install Tarpaulin (for code coverage) - run: cargo-binstall --no-confirm cargo-tarpaulin + run: | + echo "$PWD/share/bin" >> $GITHUB_PATH + cargo-binstall --no-confirm cargo-tarpaulin - name: Run the tests - run: cargo tarpaulin --out xml + run: cargo tarpaulin --out xml -- --nocapture # send the code coverage for the Rust part to the coveralls.io - name: Coveralls GitHub Action diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d79e816f4c..8bef766f82 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -66,6 +66,7 @@ dependencies = [ "regex", "serde", "serde_json", + "serde_with", "serde_yaml", "simplelog", "systemd-journal-logger", @@ -158,6 +159,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -250,6 +266,19 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compression" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +dependencies = [ + "brotli", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-io" version = "1.13.0" @@ -572,6 +601,27 @@ dependencies = [ "tracing", ] +[[package]] +name = "brotli" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bumpalo" version = "3.15.0" @@ -629,6 +679,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", + "serde", "windows-targets 0.52.0", ] @@ -692,7 +743,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.0", "terminal_size", ] @@ -870,6 +921,41 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 2.0.48", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.48", +] + [[package]] name = "data-encoding" version = "2.5.0" @@ -883,6 +969,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1220,13 +1307,19 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", "tracing", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.13.2" @@ -1394,6 +1487,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1404,6 +1503,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.1.0" @@ -2526,13 +2636,43 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "serde_yaml" version = "0.9.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" dependencies = [ - "indexmap", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -2661,6 +2801,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "strsim" version = "0.11.0" @@ -2928,7 +3074,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap", + "indexmap 2.1.0", "toml_datetime", "winnow 0.5.28", ] @@ -2939,7 +3085,7 @@ version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" dependencies = [ - "indexmap", + "indexmap 2.1.0", "serde", "serde_spanned", "toml_datetime", @@ -2968,12 +3114,16 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0da193277a4e2c33e59e09b5861580c33dd0a637c3883d0fa74ba40c0374af2e" dependencies = [ + "async-compression", "bitflags 2.4.1", "bytes", + "futures-core", "http", "http-body", "http-body-util", "pin-project-lite", + "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -3186,7 +3336,7 @@ version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "272ebdfbc99111033031d2f10e018836056e4d2c8e2acda76450ec7974269fa7" dependencies = [ - "indexmap", + "indexmap 2.1.0", "serde", "serde_json", "utoipa-gen", diff --git a/rust/agama-dbus-server/Cargo.toml b/rust/agama-dbus-server/Cargo.toml index 85f8753a9e..25cc8ccba5 100644 --- a/rust/agama-dbus-server/Cargo.toml +++ b/rust/agama-dbus-server/Cargo.toml @@ -29,7 +29,7 @@ macaddr = "1.0" async-trait = "0.1.75" axum = { version = "0.7.4", features = ["ws"] } serde_json = "1.0.113" -tower-http = { version = "0.5.1", features = ["trace"] } +tower-http = { version = "0.5.1", features = ["compression-br", "trace"] } tracing-subscriber = "0.3.18" tracing-journald = "0.3.0" tracing = "0.1.40" @@ -47,6 +47,7 @@ chrono = { version = "0.4.34", default-features = false, features = [ "clock", ] } pam = "0.8.0" +serde_with = "3.6.1" [[bin]] name = "agama-dbus-server" diff --git a/rust/agama-dbus-server/src/agama-web-server.rs b/rust/agama-dbus-server/src/agama-web-server.rs index 9df6dfac0c..1398f3f1c7 100644 --- a/rust/agama-dbus-server/src/agama-web-server.rs +++ b/rust/agama-dbus-server/src/agama-web-server.rs @@ -1,6 +1,11 @@ -use agama_dbus_server::web; -use agama_lib::connection; +use std::process::{ExitCode, Termination}; + +use agama_dbus_server::{ + l10n::helpers, + web::{self, run_monitor}, +}; use clap::{Parser, Subcommand}; +use tokio::sync::broadcast::channel; use tracing_subscriber::prelude::*; use utoipa::OpenApi; @@ -27,7 +32,7 @@ struct Cli { } /// Start serving the API. -async fn serve_command(address: &str) { +async fn serve_command(address: &str) -> anyhow::Result<()> { let journald = tracing_journald::layer().expect("could not connect to journald"); tracing_subscriber::registry().with(journald).init(); @@ -35,25 +40,53 @@ async fn serve_command(address: &str) { .await .unwrap_or_else(|_| panic!("could not listen on {}", address)); - let dbus_connection = connection().await.unwrap(); + let (tx, _) = channel(16); + run_monitor(tx.clone()).await?; + let config = web::ServiceConfig::load().unwrap(); - let service = web::service(config, dbus_connection); + let service = web::service(config, tx); axum::serve(listener, service) .await .expect("could not mount app on listener"); + Ok(()) } /// Display the API documentation in OpenAPI format. -fn openapi_command() { +fn openapi_command() -> anyhow::Result<()> { println!("{}", web::ApiDoc::openapi().to_pretty_json().unwrap()); + Ok(()) } -#[tokio::main] -async fn main() { - let cli = Cli::parse(); - +async fn run_command(cli: Cli) -> anyhow::Result<()> { match cli.command { Commands::Serve { address } => serve_command(&address).await, Commands::Openapi => openapi_command(), } } + +/// Represents the result of execution. +pub enum CliResult { + /// Successful execution. + Ok = 0, + /// Something went wrong. + Error = 1, +} + +impl Termination for CliResult { + fn report(self) -> ExitCode { + ExitCode::from(self as u8) + } +} + +#[tokio::main] +async fn main() -> CliResult { + let cli = Cli::parse(); + _ = helpers::init_locale(); + + if let Err(error) = run_command(cli).await { + eprintln!("{:?}", error); + return CliResult::Error; + } + + CliResult::Ok +} diff --git a/rust/agama-dbus-server/src/l10n.rs b/rust/agama-dbus-server/src/l10n.rs index fcf6ec3b1c..a5ebb08fe9 100644 --- a/rust/agama-dbus-server/src/l10n.rs +++ b/rust/agama-dbus-server/src/l10n.rs @@ -2,13 +2,17 @@ pub mod helpers; mod keyboard; mod locale; mod timezone; +pub mod web; use crate::error::Error; use agama_locale_data::{KeymapId, LocaleCode}; use anyhow::Context; +pub use keyboard::Keymap; use keyboard::KeymapsDatabase; +pub use locale::LocaleEntry; use locale::LocalesDatabase; use std::process::Command; +pub use timezone::TimezoneEntry; use timezone::TimezonesDatabase; use zbus::{dbus_interface, Connection}; @@ -16,7 +20,7 @@ pub struct Locale { timezone: String, timezones_db: TimezonesDatabase, locales: Vec, - locales_db: LocalesDatabase, + pub locales_db: LocalesDatabase, keymap: KeymapId, keymaps_db: KeymapsDatabase, ui_locale: LocaleCode, @@ -190,18 +194,18 @@ impl Locale { let mut locales_db = LocalesDatabase::new(); locales_db.read(&locale)?; - let default_locale = if locales_db.exists(locale.as_str()) { - ui_locale.to_string() - } else { + let mut default_locale = ui_locale.to_string(); + if !locales_db.exists(locale.as_str()) { // TODO: handle the case where the database is empty (not expected!) - locales_db.entries().get(0).unwrap().code.to_string() + default_locale = locales_db.entries().first().unwrap().code.to_string(); }; let mut timezones_db = TimezonesDatabase::new(); timezones_db.read(&ui_locale.language)?; + let mut default_timezone = DEFAULT_TIMEZONE.to_string(); if !timezones_db.exists(&default_timezone) { - default_timezone = timezones_db.entries().get(0).unwrap().code.to_string(); + default_timezone = timezones_db.entries().first().unwrap().code.to_string(); }; let mut keymaps_db = KeymapsDatabase::new(); diff --git a/rust/agama-dbus-server/src/l10n/keyboard.rs b/rust/agama-dbus-server/src/l10n/keyboard.rs index 8a286bf4f6..aec68657e6 100644 --- a/rust/agama-dbus-server/src/l10n/keyboard.rs +++ b/rust/agama-dbus-server/src/l10n/keyboard.rs @@ -1,10 +1,14 @@ use agama_locale_data::{get_localectl_keymaps, keyboard::XkbConfigRegistry, KeymapId}; use gettextrs::*; +use serde::Serialize; use std::collections::HashMap; // Minimal representation of a keymap +#[derive(Clone, Debug, Serialize, utoipa::ToSchema)] pub struct Keymap { + /// Keymap identifier (e.g., "us") pub id: KeymapId, + /// Keymap description description: String, } diff --git a/rust/agama-dbus-server/src/l10n/locale.rs b/rust/agama-dbus-server/src/l10n/locale.rs index a56919ea0a..630c844144 100644 --- a/rust/agama-dbus-server/src/l10n/locale.rs +++ b/rust/agama-dbus-server/src/l10n/locale.rs @@ -3,12 +3,16 @@ use crate::error::Error; use agama_locale_data::{InvalidLocaleCode, LocaleCode}; use anyhow::Context; +use serde::Serialize; +use serde_with::{serde_as, DisplayFromStr}; use std::process::Command; /// Represents a locale, including the localized language and territory. -#[derive(Debug)] +#[serde_as] +#[derive(Debug, Serialize, Clone, utoipa::ToSchema)] pub struct LocaleEntry { /// The locale code (e.g., "es_ES.UTF-8"). + #[serde_as(as = "DisplayFromStr")] pub code: LocaleCode, /// Localized language name (e.g., "Spanish", "EspaƱol", etc.) pub language: String, @@ -35,7 +39,7 @@ impl LocalesDatabase { /// /// * `ui_language`: language to translate the descriptions (e.g., "en"). pub fn read(&mut self, ui_language: &str) -> Result<(), Error> { - let result = Command::new("/usr/bin/localectl") + let result = Command::new("localectl") .args(["list-locales"]) .output() .context("Failed to get the list of locales")?; diff --git a/rust/agama-dbus-server/src/l10n/timezone.rs b/rust/agama-dbus-server/src/l10n/timezone.rs index bfa7ddae8f..f1547903f7 100644 --- a/rust/agama-dbus-server/src/l10n/timezone.rs +++ b/rust/agama-dbus-server/src/l10n/timezone.rs @@ -3,10 +3,11 @@ use crate::error::Error; use agama_locale_data::territory::Territories; use agama_locale_data::timezone_part::TimezoneIdParts; +use serde::Serialize; use std::collections::HashMap; /// Represents a timezone, including each part as localized. -#[derive(Debug)] +#[derive(Clone, Debug, Serialize, utoipa::ToSchema)] pub struct TimezoneEntry { /// Timezone identifier (e.g. "Atlantic/Canary"). pub code: String, @@ -114,7 +115,6 @@ mod tests { let mut db = TimezonesDatabase::new(); db.read("es").unwrap(); let found_timezones = db.entries(); - dbg!(&found_timezones); let found = found_timezones .iter() .find(|tz| tz.code == "Europe/Berlin") diff --git a/rust/agama-dbus-server/src/l10n/web.rs b/rust/agama-dbus-server/src/l10n/web.rs new file mode 100644 index 0000000000..1f84f4d437 --- /dev/null +++ b/rust/agama-dbus-server/src/l10n/web.rs @@ -0,0 +1,207 @@ +//! This module implements the web API for the localization module. + +use super::{keyboard::Keymap, locale::LocaleEntry, timezone::TimezoneEntry, Locale}; +use crate::{ + error::Error, + l10n::helpers, + web::{Event, EventsSender}, +}; +use agama_locale_data::{InvalidKeymap, LocaleCode}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, put}, + Json, Router, +}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::sync::{Arc, RwLock}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum LocaleError { + #[error("Unknown locale code: {0}")] + UnknownLocale(String), + #[error("Unknown timezone: {0}")] + UnknownTimezone(String), + #[error("Invalid keymap: {0}")] + InvalidKeymap(#[from] InvalidKeymap), + #[error("Cannot translate: {0}")] + OtherError(#[from] Error), +} + +impl IntoResponse for LocaleError { + fn into_response(self) -> Response { + let body = json!({ + "error": self.to_string() + }); + (StatusCode::BAD_REQUEST, Json(body)).into_response() + } +} + +#[derive(Clone)] +struct LocaleState { + locale: Arc>, + events: EventsSender, +} + +/// Sets up and returns the axum service for the localization module. +/// +/// * `events`: channel to send the events to the main service. +pub fn l10n_service(events: EventsSender) -> Router { + let code = LocaleCode::default(); + let locale = Locale::new_with_locale(&code).unwrap(); + let state = LocaleState { + locale: Arc::new(RwLock::new(locale)), + events, + }; + + Router::new() + .route("/keymaps", get(keymaps)) + .route("/locales", get(locales)) + .route("/timezones", get(timezones)) + .route("/config", put(set_config).get(get_config)) + .with_state(state) +} + +#[utoipa::path(get, path = "/l10n/locales", responses( + (status = 200, description = "List of known locales", body = Vec) +))] +async fn locales(State(state): State) -> Json> { + let data = state.locale.read().unwrap(); + let locales = data.locales_db.entries().to_vec(); + Json(locales) +} + +#[derive(Serialize, Deserialize, utoipa::ToSchema)] +pub struct LocaleConfig { + /// Locales to install in the target system + locales: Option>, + /// Keymap for the target system + keymap: Option, + /// Timezone for the target system + timezone: Option, + /// User-interface locale. It is actually not related to the `locales` property. + ui_locale: Option, +} + +#[utoipa::path(get, path = "/l10n/timezones", responses( + (status = 200, description = "List of known timezones") +))] +async fn timezones(State(state): State) -> Json> { + let data = state.locale.read().unwrap(); + let timezones = data.timezones_db.entries().to_vec(); + Json(timezones) +} + +#[utoipa::path(get, path = "/l10n/keymaps", responses( + (status = 200, description = "List of known keymaps", body = Vec) +))] +async fn keymaps(State(state): State) -> Json> { + let data = state.locale.read().unwrap(); + let keymaps = data.keymaps_db.entries().to_vec(); + Json(keymaps) +} + +#[utoipa::path(put, path = "/l10n/config", responses( + (status = 200, description = "Set the locale configuration", body = LocaleConfig) +))] +async fn set_config( + State(state): State, + Json(value): Json, +) -> Result, LocaleError> { + let mut data = state.locale.write().unwrap(); + + if let Some(locales) = &value.locales { + for loc in locales { + if !data.locales_db.exists(loc.as_str()) { + return Err(LocaleError::UnknownLocale(loc.to_string())); + } + } + data.locales = locales.clone(); + } + + if let Some(timezone) = &value.timezone { + if !data.timezones_db.exists(timezone) { + return Err(LocaleError::UnknownTimezone(timezone.to_string())); + } + data.timezone = timezone.to_owned(); + } + + if let Some(keymap_id) = &value.keymap { + data.keymap = keymap_id.parse()?; + } + + if let Some(ui_locale) = &value.ui_locale { + let locale: LocaleCode = ui_locale + .as_str() + .try_into() + .map_err(|_e| LocaleError::UnknownLocale(ui_locale.to_string()))?; + + helpers::set_service_locale(&locale); + data.translate(&locale)?; + _ = state.events.send(Event::LocaleChanged { + locale: locale.to_string(), + }); + } + + Ok(Json(())) +} + +#[utoipa::path(get, path = "/l10n/config", responses( + (status = 200, description = "Localization configuration", body = LocaleConfig) +))] +async fn get_config(State(state): State) -> Json { + let data = state.locale.read().unwrap(); + Json(LocaleConfig { + locales: Some(data.locales.clone()), + keymap: Some(data.keymap()), + timezone: Some(data.timezone().to_string()), + ui_locale: Some(data.ui_locale().to_string()), + }) +} + +#[cfg(test)] +mod tests { + use crate::l10n::{web::LocaleState, Locale}; + use agama_locale_data::{KeymapId, LocaleCode}; + use std::sync::{Arc, RwLock}; + use tokio::{sync::broadcast::channel, test}; + + fn build_state() -> LocaleState { + let (tx, _) = channel(16); + let default_code = LocaleCode::default(); + let locale = Locale::new_with_locale(&default_code).unwrap(); + LocaleState { + locale: Arc::new(RwLock::new(locale)), + events: tx, + } + } + + #[test] + async fn test_locales() { + let state = build_state(); + let response = super::locales(axum::extract::State(state)).await; + let default = LocaleCode::default(); + let found = response.iter().find(|l| l.code == default); + assert!(found.is_some()); + } + + #[test] + async fn test_keymaps() { + let state = build_state(); + let response = super::keymaps(axum::extract::State(state)).await; + let english: KeymapId = "us".parse().unwrap(); + let found = response.iter().find(|k| k.id == english); + assert!(found.is_some()); + } + + #[test] + async fn test_timezones() { + let state = build_state(); + let response = super::timezones(axum::extract::State(state)).await; + let found = response.iter().find(|t| t.code == "Atlantic/Canary"); + assert!(found.is_some()); + } +} diff --git a/rust/agama-dbus-server/src/network/model.rs b/rust/agama-dbus-server/src/network/model.rs index 4a963b07d0..551c353496 100644 --- a/rust/agama-dbus-server/src/network/model.rs +++ b/rust/agama-dbus-server/src/network/model.rs @@ -345,7 +345,6 @@ mod tests { let error = state .set_ports(&bond0, vec!["eth0".to_string()]) .unwrap_err(); - dbg!(&error); assert!(matches!(error, NetworkStateError::UnknownConnection(_))); } diff --git a/rust/agama-dbus-server/src/web.rs b/rust/agama-dbus-server/src/web.rs index b3e1490f9b..7cbb1ec6b8 100644 --- a/rust/agama-dbus-server/src/web.rs +++ b/rust/agama-dbus-server/src/web.rs @@ -7,12 +7,49 @@ mod auth; mod config; mod docs; +mod event; mod http; +mod progress; mod service; mod state; mod ws; +use agama_lib::{connection, error::ServiceError, progress::ProgressMonitor}; pub use auth::generate_token; pub use config::ServiceConfig; pub use docs::ApiDoc; -pub use service::service; +pub use event::{Event, EventsReceiver, EventsSender}; + +use crate::l10n::web::l10n_service; +use axum::Router; +pub use service::MainServiceBuilder; + +use self::progress::EventsProgressPresenter; + +/// Returns a service that implements the web-based Agama API. +/// +/// * `config`: service configuration. +/// * `events`: D-Bus connection. +pub fn service(config: ServiceConfig, events: EventsSender) -> Router { + MainServiceBuilder::new(events.clone()) + .add_service("/l10n", l10n_service(events)) + .with_config(config) + .build() +} + +/// Starts monitoring the D-Bus service progress. +/// +/// The events are sent to the `events` channel. +/// +/// * `events`: channel to send the events to. +pub async fn run_monitor(events: EventsSender) -> Result<(), ServiceError> { + let presenter = EventsProgressPresenter::new(events); + let connection = connection().await?; + let mut monitor = ProgressMonitor::new(connection).await?; + tokio::spawn(async move { + if let Err(error) = monitor.run(presenter).await { + eprintln!("Could not monitor the D-Bus server: {}", error); + } + }); + Ok(()) +} diff --git a/rust/agama-dbus-server/src/web/docs.rs b/rust/agama-dbus-server/src/web/docs.rs index a45465aa44..58a1de6aed 100644 --- a/rust/agama-dbus-server/src/web/docs.rs +++ b/rust/agama-dbus-server/src/web/docs.rs @@ -3,7 +3,13 @@ use utoipa::OpenApi; #[derive(OpenApi)] #[openapi( info(description = "Agama web API description"), - paths(super::http::ping), - components(schemas(super::http::PingResponse)) + paths(super::http::ping, crate::l10n::web::locales), + components( + schemas(super::http::PingResponse), + schemas(crate::l10n::LocaleEntry), + schemas(crate::l10n::web::LocaleConfig), + schemas(crate::l10n::Keymap), + schemas(crate::l10n::TimezoneEntry) + ) )] pub struct ApiDoc; diff --git a/rust/agama-dbus-server/src/web/event.rs b/rust/agama-dbus-server/src/web/event.rs new file mode 100644 index 0000000000..76db326bc4 --- /dev/null +++ b/rust/agama-dbus-server/src/web/event.rs @@ -0,0 +1,13 @@ +use agama_lib::progress::Progress; +use serde::Serialize; +use tokio::sync::broadcast::{Receiver, Sender}; + +#[derive(Clone, Serialize)] +#[serde(tag = "type")] +pub enum Event { + LocaleChanged { locale: String }, + Progress(Progress), +} + +pub type EventsSender = Sender; +pub type EventsReceiver = Receiver; diff --git a/rust/agama-dbus-server/src/web/http.rs b/rust/agama-dbus-server/src/web/http.rs index f6131044db..54da54a3ae 100644 --- a/rust/agama-dbus-server/src/web/http.rs +++ b/rust/agama-dbus-server/src/web/http.rs @@ -24,11 +24,6 @@ pub async fn ping() -> Json { }) } -// TODO: remove this route (as it is just for teting) as soon as we have a legit protected one -pub async fn protected() -> String { - "OK".to_string() -} - #[derive(Serialize)] pub struct AuthResponse { /// Bearer token to use on subsequent calls diff --git a/rust/agama-dbus-server/src/web/progress.rs b/rust/agama-dbus-server/src/web/progress.rs new file mode 100644 index 0000000000..c892edd8ed --- /dev/null +++ b/rust/agama-dbus-server/src/web/progress.rs @@ -0,0 +1,40 @@ +//! Implements a mechanism to monitor track service progress. + +use super::event::{Event, EventsSender}; +use agama_lib::progress::{Progress, ProgressPresenter}; +use async_trait::async_trait; + +// let presenter = EventsProgressPresenter::new(socket); +// let mut monitor = ProgressMonitor::new(connection).await.unwrap(); +// _ = monitor.run(presenter).await; + +/// Experimental ProgressPresenter to emit progress events over a Events. +pub struct EventsProgressPresenter(EventsSender); + +impl EventsProgressPresenter { + pub fn new(events: EventsSender) -> Self { + Self(events) + } + + pub async fn report_progress(&mut self, progress: &Progress) { + _ = self.0.send(Event::Progress(progress.clone())) + // _ = self.events.send(Message::Text(payload)).await; + } +} + +#[async_trait] +impl ProgressPresenter for EventsProgressPresenter { + async fn start(&mut self, progress: &Progress) { + self.report_progress(progress).await; + } + + async fn update_main(&mut self, progress: &Progress) { + self.report_progress(progress).await; + } + + async fn update_detail(&mut self, progress: &Progress) { + self.report_progress(progress).await; + } + + async fn finish(&mut self) {} +} diff --git a/rust/agama-dbus-server/src/web/service.rs b/rust/agama-dbus-server/src/web/service.rs index 2c30d24212..d2590bc688 100644 --- a/rust/agama-dbus-server/src/web/service.rs +++ b/rust/agama-dbus-server/src/web/service.rs @@ -1,25 +1,62 @@ -use super::{auth::TokenClaims, config::ServiceConfig, state::ServiceState}; +use super::{auth::TokenClaims, config::ServiceConfig, state::ServiceState, EventsSender}; use axum::{ + extract::Request, middleware, + response::IntoResponse, routing::{get, post}, Router, }; -use tower_http::trace::TraceLayer; +use std::convert::Infallible; +use tower::Service; +use tower_http::{compression::CompressionLayer, trace::TraceLayer}; -/// Returns a service that implements the web-based Agama API. -pub fn service(config: ServiceConfig, dbus_connection: zbus::Connection) -> Router { - let state = ServiceState { - config, - dbus_connection, - }; - Router::new() - .route("/protected", get(super::http::protected)) - .route("/ws", get(super::ws::ws_handler)) - .route_layer(middleware::from_extractor_with_state::( - state.clone(), - )) - .route("/ping", get(super::http::ping)) - .route("/authenticate", post(super::http::authenticate)) - .layer(TraceLayer::new_for_http()) - .with_state(state) +pub struct MainServiceBuilder { + config: ServiceConfig, + events: EventsSender, + router: Router, +} + +impl MainServiceBuilder { + pub fn new(events: EventsSender) -> Self { + let router = Router::new().route("/ws", get(super::ws::ws_handler)); + let config = ServiceConfig::default(); + + Self { + events, + router, + config, + } + } + + pub fn with_config(self, config: ServiceConfig) -> Self { + Self { config, ..self } + } + + pub fn add_service(self, path: &str, service: T) -> Self + where + T: Service + Clone + Send + 'static, + T::Response: IntoResponse, + T::Future: Send + 'static, + { + Self { + router: self.router.nest_service(path, service), + ..self + } + } + + pub fn build(self) -> Router { + let state = ServiceState { + config: self.config, + events: self.events, + }; + self.router + .route_layer(middleware::from_extractor_with_state::( + state.clone(), + )) + .route("/ping", get(super::http::ping)) + .route("/authenticate", post(super::http::authenticate)) + .layer(TraceLayer::new_for_http()) + .layer(CompressionLayer::new().br(true)) + .with_state(state) + } } diff --git a/rust/agama-dbus-server/src/web/state.rs b/rust/agama-dbus-server/src/web/state.rs index 49e16c4e22..c35592b8c5 100644 --- a/rust/agama-dbus-server/src/web/state.rs +++ b/rust/agama-dbus-server/src/web/state.rs @@ -1,12 +1,12 @@ //! Implements the web service state. -use super::config::ServiceConfig; +use super::{config::ServiceConfig, EventsSender}; /// Web service state. /// -/// It holds the service configuration and the current D-Bus connection. +/// It holds the service configuration, the current D-Bus connection and a channel to send events. #[derive(Clone)] pub struct ServiceState { pub config: ServiceConfig, - pub dbus_connection: zbus::Connection, + pub events: EventsSender, } diff --git a/rust/agama-dbus-server/src/web/ws.rs b/rust/agama-dbus-server/src/web/ws.rs index 3cfd9061c1..e7c348ddb8 100644 --- a/rust/agama-dbus-server/src/web/ws.rs +++ b/rust/agama-dbus-server/src/web/ws.rs @@ -1,8 +1,6 @@ //! Implements the websocket handling. -use super::state::ServiceState; -use agama_lib::progress::{Progress, ProgressMonitor, ProgressPresenter}; -use async_trait::async_trait; +use super::{state::ServiceState, EventsSender}; use axum::{ extract::{ ws::{Message, WebSocket}, @@ -15,42 +13,14 @@ pub async fn ws_handler( State(state): State, ws: WebSocketUpgrade, ) -> impl IntoResponse { - ws.on_upgrade(move |socket| handle_socket(socket, state.dbus_connection)) + ws.on_upgrade(move |socket| handle_socket(socket, state.events)) } -async fn handle_socket(socket: WebSocket, connection: zbus::Connection) { - let presenter = WebSocketProgressPresenter::new(socket); - let mut monitor = ProgressMonitor::new(connection).await.unwrap(); - _ = monitor.run(presenter).await; -} - -/// Experimental ProgressPresenter to emit progress events over a WebSocket. -struct WebSocketProgressPresenter(WebSocket); - -impl WebSocketProgressPresenter { - pub fn new(socket: WebSocket) -> Self { - Self(socket) +async fn handle_socket(mut socket: WebSocket, events: EventsSender) { + let mut rx = events.subscribe(); + while let Ok(msg) = rx.recv().await { + if let Ok(json) = serde_json::to_string(&msg) { + _ = socket.send(Message::Text(json)).await; + } } - - pub async fn report_progress(&mut self, progress: &Progress) { - let payload = serde_json::to_string(&progress).unwrap(); - _ = self.0.send(Message::Text(payload)).await; - } -} - -#[async_trait] -impl ProgressPresenter for WebSocketProgressPresenter { - async fn start(&mut self, progress: &Progress) { - self.report_progress(progress).await; - } - - async fn update_main(&mut self, progress: &Progress) { - self.report_progress(progress).await; - } - - async fn update_detail(&mut self, progress: &Progress) { - self.report_progress(progress).await; - } - - async fn finish(&mut self) {} } diff --git a/rust/agama-dbus-server/tests/common/mod.rs b/rust/agama-dbus-server/tests/common/mod.rs index 009d95bfb1..cd77539774 100644 --- a/rust/agama-dbus-server/tests/common/mod.rs +++ b/rust/agama-dbus-server/tests/common/mod.rs @@ -1,4 +1,5 @@ use agama_lib::error::ServiceError; +use axum::body::{to_bytes, Body}; use std::{ error::Error, future::Future, @@ -144,3 +145,8 @@ where } } } + +pub async fn body_to_string(body: Body) -> String { + let bytes = to_bytes(body, usize::MAX).await.unwrap(); + String::from_utf8(bytes.to_vec()).unwrap() +} diff --git a/rust/agama-dbus-server/tests/l10n.rs b/rust/agama-dbus-server/tests/l10n.rs new file mode 100644 index 0000000000..6ad7e352a0 --- /dev/null +++ b/rust/agama-dbus-server/tests/l10n.rs @@ -0,0 +1,66 @@ +mod common; + +use agama_dbus_server::l10n::web::l10n_service; +use axum::{ + body::Body, + http::{Request, StatusCode}, + Router, +}; +use common::body_to_string; +use tokio::{sync::broadcast::channel, test}; +use tower::ServiceExt; + +fn build_service() -> Router { + let (tx, _) = channel(16); + l10n_service(tx) +} + +#[test] +async fn test_get_config() { + let service = build_service(); + let request = Request::builder() + .uri("/config") + .body(Body::empty()) + .unwrap(); + let response = service.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); +} + +#[test] +async fn test_locales() { + let service = build_service(); + let request = Request::builder() + .uri("/locales") + .body(Body::empty()) + .unwrap(); + let response = service.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = body_to_string(response.into_body()).await; + assert!(body.contains(r#""language":"English""#)); +} + +#[test] +async fn test_keymaps() { + let service = build_service(); + let request = Request::builder() + .uri("/keymaps") + .body(Body::empty()) + .unwrap(); + let response = service.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = body_to_string(response.into_body()).await; + assert!(body.contains(r#""layout":"us""#)); +} + +#[test] +async fn test_timezones() { + let service = build_service(); + let request = Request::builder() + .uri("/timezones") + .body(Body::empty()) + .unwrap(); + let response = service.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + let body = body_to_string(response.into_body()).await; + assert!(body.contains(r#""code":"Atlantic/Canary""#)); +} diff --git a/rust/agama-dbus-server/tests/service.rs b/rust/agama-dbus-server/tests/service.rs index c825fbbe43..aac1b2922b 100644 --- a/rust/agama-dbus-server/tests/service.rs +++ b/rust/agama-dbus-server/tests/service.rs @@ -1,29 +1,32 @@ mod common; -use self::common::DBusServer; -use agama_dbus_server::{service, web::generate_token, web::ServiceConfig}; +use agama_dbus_server::{ + service, + web::{generate_token, MainServiceBuilder, ServiceConfig}, +}; use axum::{ body::Body, http::{Method, Request, StatusCode}, response::Response, + routing::get, + Router, }; -use http_body_util::BodyExt; +use common::body_to_string; use std::error::Error; -use tokio::test; +use tokio::{sync::broadcast::channel, test}; use tower::ServiceExt; -async fn body_to_string(body: Body) -> String { - let bytes = body.collect().await.unwrap().to_bytes(); - String::from_utf8(bytes.to_vec()).unwrap() +fn build_service() -> Router { + let (tx, _) = channel(16); + service(ServiceConfig::default(), tx) } #[test] async fn test_ping() -> Result<(), Box> { - let dbus_server = DBusServer::new().start().await?; - let web_server = service(ServiceConfig::default(), dbus_server.connection()); + let web_service = build_service(); let request = Request::builder().uri("/ping").body(Body::empty()).unwrap(); - let response = web_server.oneshot(request).await.unwrap(); + let response = web_service.oneshot(request).await.unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body_to_string(response.into_body()).await; @@ -31,12 +34,20 @@ async fn test_ping() -> Result<(), Box> { Ok(()) } +async fn protected() -> String { + "OK".to_string() +} + async fn access_protected_route(token: &str, jwt_secret: &str) -> Response { - let dbus_server = DBusServer::new().start().await.unwrap(); let config = ServiceConfig { jwt_secret: jwt_secret.to_string(), }; - let web_server = service(config, dbus_server.connection()); + let (tx, _) = channel(16); + let web_service = MainServiceBuilder::new(tx) + .add_service("/protected", get(protected)) + .with_config(config) + .build(); + let request = Request::builder() .uri("/protected") .method(Method::GET) @@ -44,12 +55,10 @@ async fn access_protected_route(token: &str, jwt_secret: &str) -> Response { .body(Body::empty()) .unwrap(); - web_server.oneshot(request).await.unwrap() + web_service.oneshot(request).await.unwrap() } -// TODO: The following test should belong to `auth.rs`. However, we need a working -// D-Bus connection which is not available on containers. Let's keep the test -// here until by now. +// TODO: The following test should belong to `auth.rs` #[test] async fn test_access_protected_route() -> Result<(), Box> { let token = generate_token("nots3cr3t"); @@ -60,10 +69,8 @@ async fn test_access_protected_route() -> Result<(), Box> { assert_eq!(body, "OK"); Ok(()) } - -// TODO: The following test should belong to `auth.rs`. However, we need a working -// D-Bus connection which is not available on containers. Let's keep the test -// here until by now. +// +// TODO: The following test should belong to `auth.rs`. #[test] async fn test_access_protected_route_failed() -> Result<(), Box> { let token = generate_token("nots3cr3t"); diff --git a/rust/agama-lib/src/progress.rs b/rust/agama-lib/src/progress.rs index 0e3b1a7237..b79db61195 100644 --- a/rust/agama-lib/src/progress.rs +++ b/rust/agama-lib/src/progress.rs @@ -53,7 +53,7 @@ use tokio_stream::{StreamExt, StreamMap}; use zbus::Connection; /// Represents the progress for an Agama service. -#[derive(Default, Debug, Serialize)] +#[derive(Clone, Default, Debug, Serialize)] pub struct Progress { /// Current step pub current_step: u32, @@ -112,22 +112,21 @@ impl<'a> ProgressMonitor<'a> { /// Runs the monitor until the current operation finishes. pub async fn run(&mut self, mut presenter: impl ProgressPresenter) -> Result<(), ServiceError> { - presenter.start(&self.main_progress().await).await; + presenter.start(&self.main_progress().await?).await; let mut changes = self.build_stream().await; while let Some(stream) = changes.next().await { match stream { ("/org/opensuse/Agama/Manager1", _) => { - let progress = self.main_progress().await; + let progress = self.main_progress().await?; if progress.finished { presenter.finish().await; return Ok(()); - } else { - presenter.update_main(&progress).await; } + presenter.update_main(&progress).await; } ("/org/opensuse/Agama/Software1", _) => { - let progress = &self.detail_progress().await; + let progress = &self.detail_progress().await?; presenter.update_detail(progress).await; } _ => eprintln!("Unknown"), @@ -138,13 +137,13 @@ impl<'a> ProgressMonitor<'a> { } /// Proxy that reports the progress. - async fn main_progress(&self) -> Progress { - Progress::from_proxy(&self.manager_proxy).await.unwrap() + async fn main_progress(&self) -> Result { + Ok(Progress::from_proxy(&self.manager_proxy).await?) } /// Proxy that reports the progress detail. - async fn detail_progress(&self) -> Progress { - Progress::from_proxy(&self.software_proxy).await.unwrap() + async fn detail_progress(&self) -> Result { + Ok(Progress::from_proxy(&self.software_proxy).await?) } /// Builds an stream of progress changes. diff --git a/rust/agama-locale-data/src/lib.rs b/rust/agama-locale-data/src/lib.rs index 71d6c1654c..fad38cbe70 100644 --- a/rust/agama-locale-data/src/lib.rs +++ b/rust/agama-locale-data/src/lib.rs @@ -19,7 +19,7 @@ pub mod timezone_part; use keyboard::xkeyboard; -pub use locale::{InvalidLocaleCode, KeymapId, LocaleCode}; +pub use locale::{InvalidKeymap, InvalidLocaleCode, KeymapId, LocaleCode}; fn file_reader(file_path: &str) -> anyhow::Result { let file = File::open(file_path) @@ -51,8 +51,7 @@ pub fn get_xkeyboards() -> anyhow::Result { /// assert!(key_maps.contains(&us)); /// ``` pub fn get_localectl_keymaps() -> anyhow::Result> { - const BINARY: &str = "/usr/bin/localectl"; - let output = Command::new(BINARY) + let output = Command::new("localectl") .arg("list-keymaps") .output() .context("failed to execute localectl list-maps")? diff --git a/rust/agama-locale-data/src/locale.rs b/rust/agama-locale-data/src/locale.rs index 43fa6ce5f7..f46f5501fd 100644 --- a/rust/agama-locale-data/src/locale.rs +++ b/rust/agama-locale-data/src/locale.rs @@ -1,11 +1,12 @@ //! Defines useful types to deal with localization values use regex::Regex; +use serde::Serialize; use std::sync::OnceLock; use std::{fmt::Display, str::FromStr}; use thiserror::Error; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct LocaleCode { // ISO-639 pub language: String, @@ -79,7 +80,7 @@ static KEYMAP_ID_REGEX: OnceLock = OnceLock::new(); /// let id_with_dashes: KeymapId = "es-ast".parse().unwrap(); /// assert_eq!(id, id_with_dashes); /// ``` -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct KeymapId { pub layout: String, pub variant: Option, diff --git a/rust/share/bin/README.md b/rust/share/bin/README.md new file mode 100644 index 0000000000..7c6ea621db --- /dev/null +++ b/rust/share/bin/README.md @@ -0,0 +1,8 @@ +This directory contains commands that replaces real ones during CI testing. The reason is that these +commands might not work in the CI environment (e.g., systemd related commands). + +To use these "binaries" in the tests, just set the right PATH: + +``` +PATH=$PWD/share/bin:$PATH cargo test +``` diff --git a/rust/share/bin/localectl b/rust/share/bin/localectl new file mode 100755 index 0000000000..21f4ce9dbe --- /dev/null +++ b/rust/share/bin/localectl @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +SCRIPT=$(readlink -f "$0") +DATADIR=$(dirname "$SCRIPT")/.. +cat $DATADIR/localectl-$1.txt diff --git a/rust/share/localectl-list-keymaps.txt b/rust/share/localectl-list-keymaps.txt new file mode 100644 index 0000000000..a68870b4ea --- /dev/null +++ b/rust/share/localectl-list-keymaps.txt @@ -0,0 +1,612 @@ +3l +ANSI-dvorak +Pl02 +adnw +al +al-plisi +amiga-de +amiga-us +apple-a1048-sv +apple-a1243-sv +apple-a1243-sv-fn-reverse +apple-internal-0x0253-sv +apple-internal-0x0253-sv-fn-reverse +applkey +ara +at +at-mac +at-nodeadkeys +atari-de +atari-se +atari-uk-falcon +atari-us +az +azerty +ba +ba-alternatequotes +ba-unicode +ba-unicodeus +ba-us +backspace +bashkir +be +be-iso-alternate +be-latin1 +be-nodeadkeys +be-oss +be-oss_latin9 +be-wang +bg-cp1251 +bg-cp855 +bg_bds-cp1251 +bg_bds-utf8 +bg_pho-cp1251 +bg_pho-utf8 +bone +br +br-abnt +br-abnt-alt +br-abnt2 +br-abnt2-old +br-dvorak +br-latin1-abnt2 +br-latin1-us +br-nativo +br-nativo-epo +br-nativo-us +br-nodeadkeys +br-thinkpad +by +by-cp1251 +by-latin +bywin-cp1251 +ca +ca-eng +ca-fr-dvorak +ca-fr-legacy +ca-multix +carpalx +carpalx-full +cf +ch +ch-de_mac +ch-de_nodeadkeys +ch-fr +ch-fr_mac +ch-fr_nodeadkeys +ch-legacy +chinese +cm +cm-azerty +cm-dvorak +cm-french +cm-mmuock +cm-qwerty +cn +cn-altgr-pinyin +cn-latin1 +croat +ctrl +cz +cz-bksl +cz-cp1250 +cz-dvorak-ucw +cz-lat2 +cz-lat2-prog +cz-lat2-us +cz-qwerty +cz-qwerty-mac +cz-qwerty_bksl +cz-rus +cz-us-qwertz +cz-winkeys +cz-winkeys-qwerty +de +de-T3 +de-deadacute +de-deadgraveacute +de-deadtilde +de-dsb +de-dsb_qwertz +de-dvorak +de-e1 +de-e2 +de-latin1 +de-latin1-nodeadkeys +de-mac +de-mac_nodeadkeys +de-mobii +de-neo +de-nodeadkeys +de-qwerty +de-ro +de-ro_nodeadkeys +de-tr +de-us +de_CH-latin1 +de_alt_UTF-8 +defkeymap +defkeymap_V1.0 +dk +dk-dvorak +dk-latin1 +dk-mac +dk-mac_nodeadkeys +dk-nodeadkeys +dk-winkeys +dvorak +dvorak-ca-fr +dvorak-de +dvorak-es +dvorak-fr +dvorak-l +dvorak-la +dvorak-no +dvorak-programmer +dvorak-r +dvorak-ru +dvorak-sv-a1 +dvorak-sv-a5 +dvorak-uk +dvorak-ukp +dz +dz-azerty-deadkeys +dz-qwerty-gb-deadkeys +dz-qwerty-us-deadkeys +ee +ee-dvorak +ee-nodeadkeys +ee-us +emacs +emacs2 +en +en-latin9 +epo +epo-legacy +es +es-ast +es-cat +es-cp850 +es-deadtilde +es-dvorak +es-nodeadkeys +es-olpc +es-winkeys +et +et-nodeadkeys +euro +euro1 +euro2 +fa +fi +fi-classic +fi-kotoistus +fi-mac +fi-nodeadkeys +fi-smi +fi-winkeys +fo +fo-nodeadkeys +fr +fr-afnor +fr-azerty +fr-bepo +fr-bepo-latin9 +fr-bepo_afnor +fr-bepo_latin9 +fr-bre +fr-dvorak +fr-latin1 +fr-latin9 +fr-latin9_nodeadkeys +fr-mac +fr-nodeadkeys +fr-oci +fr-oss +fr-oss_latin9 +fr-oss_nodeadkeys +fr-pc +fr-us +fr_CH +fr_CH-latin1 +gb +gb-colemak +gb-colemak_dh +gb-dvorak +gb-dvorakukp +gb-extd +gb-gla +gb-intl +gb-mac +gb-mac_intl +gb-pl +ge +ge-ergonomic +ge-mess +ge-ru +gh +gh-akan +gh-avn +gh-ewe +gh-fula +gh-ga +gh-generic +gh-gillbt +gh-hausa +gr +gr-pc +hr +hr-alternatequotes +hr-unicode +hr-unicodeus +hr-us +hu +hu-101_qwerty_comma_dead +hu-101_qwerty_comma_nodead +hu-101_qwerty_dot_dead +hu-101_qwerty_dot_nodead +hu-101_qwertz_comma_dead +hu-101_qwertz_comma_nodead +hu-101_qwertz_dot_dead +hu-101_qwertz_dot_nodead +hu-102_qwerty_comma_dead +hu-102_qwerty_comma_nodead +hu-102_qwerty_dot_dead +hu-102_qwerty_dot_nodead +hu-102_qwertz_comma_dead +hu-102_qwertz_comma_nodead +hu-102_qwertz_dot_dead +hu-102_qwertz_dot_nodead +hu-nodeadkeys +hu-qwerty +hu-standard +hu101 +id +ie +ie-CloGaelach +ie-UnicodeExpert +ie-ogam_is434 +il +il-heb +il-phonetic +il-si2 +in-eng +in-iipa +iq-ku +iq-ku_alt +iq-ku_ara +iq-ku_f +ir +ir-ku +ir-ku_alt +ir-ku_ara +ir-ku_f +is +is-dvorak +is-latin1 +is-latin1-us +is-mac +is-mac_legacy +it +it-fur +it-geo +it-ibm +it-intl +it-mac +it-nodeadkeys +it-scn +it-us +it-winkeys +it2 +jp +jp-OADG109A +jp-dvorak +jp-kana86 +jp106 +kazakh +ke +ke-kik +keypad +khmer +koy +kr +kr-kr104 +ky_alt_sh-UTF-8 +kyrgyz +kz-latin +la-latin1 +latam +latam-colemak +latam-deadtilde +latam-dvorak +latam-nodeadkeys +lk-us +lt +lt-ibm +lt-lekp +lt-lekpa +lt-ratise +lt-sgs +lt-std +lt-us +lt.baltic +lt.l4 +lt.std +lv +lv-adapted +lv-apostrophe +lv-ergonomic +lv-fkey +lv-modern +lv-tilde +ma-french +ma-rif +mac-Pl02 +mac-be +mac-br-abnt2 +mac-cz-us-qwertz +mac-de-latin1 +mac-de-latin1-nodeadkeys +mac-de_CH +mac-dk-latin1 +mac-dvorak +mac-es +mac-euro +mac-euro2 +mac-fi-latin1 +mac-fr +mac-fr-legacy +mac-fr_CH-latin1 +mac-gr +mac-hu +mac-it +mac-jp106 +mac-no-latin1 +mac-pl +mac-pt-latin1 +mac-ru1 +mac-se +mac-template +mac-uk +mac-us +md +md-gag +me +me-latinalternatequotes +me-latinunicode +me-latinunicodeyz +me-latinyz +mk +mk-cp1251 +mk-utf +mk0 +ml +ml-fr-oss +ml-us-intl +ml-us-mac +mm +mm-mnw +mm-shn +mod-dh-ansi-us +mod-dh-ansi-us-awing +mod-dh-ansi-us-fatz +mod-dh-ansi-us-fatz-wide +mod-dh-ansi-us-wide +mod-dh-iso-uk +mod-dh-iso-uk-wide +mod-dh-iso-us +mod-dh-iso-us-wide +mod-dh-matrix-us +mt +mt-alt-gb +mt-alt-us +mt-us +neo +neoqwertz +ng +ng-hausa +ng-igbo +ng-yoruba +nl +nl-mac +nl-std +nl-us +nl2 +no +no-colemak +no-colemak_dh +no-colemak_dh_wide +no-dvorak +no-latin1 +no-mac +no-mac_nodeadkeys +no-nodeadkeys +no-smi +no-smi_nodeadkeys +no-winkeys +nz +nz-mao +pc110 +ph +ph-capewell-dvorak +ph-capewell-qwerf2k6 +ph-colemak +ph-dvorak +pl +pl-csb +pl-dvorak +pl-dvorak_altquotes +pl-dvorak_quotes +pl-dvp +pl-legacy +pl-qwertz +pl-szl +pl1 +pl2 +pl3 +pl4 +pt +pt-latin1 +pt-latin9 +pt-mac +pt-mac_nodeadkeys +pt-nativo +pt-nativo-epo +pt-nativo-us +pt-nodeadkeys +ro +ro-latin2 +ro-std +ro-winkeys +ro_std +ro_win +rs-latin +rs-latinalternatequotes +rs-latinunicode +rs-latinunicodeyz +rs-latinyz +ru +ru-cp1251 +ru-cv_latin +ru-ms +ru-ruchey_en +ru-yawerty +ru1 +ru1_win-utf +ru2 +ru3 +ru4 +ru_win +ruwin_alt-CP1251 +ruwin_alt-KOI8-R +ruwin_alt-UTF-8 +ruwin_alt_sh-UTF-8 +ruwin_cplk-CP1251 +ruwin_cplk-KOI8-R +ruwin_cplk-UTF-8 +ruwin_ct_sh-CP1251 +ruwin_ct_sh-KOI8-R +ruwin_ct_sh-UTF-8 +ruwin_ctrl-CP1251 +ruwin_ctrl-KOI8-R +ruwin_ctrl-UTF-8 +se +se-dvorak +se-fi-ir209 +se-fi-lat6 +se-ir209 +se-lat6 +se-latin1 +se-mac +se-nodeadkeys +se-smi +se-svdvorak +se-us +se-us_dvorak +sg +sg-latin1 +sg-latin1-lk450 +si +si-alternatequotes +si-us +sk +sk-bksl +sk-prog-qwerty +sk-prog-qwertz +sk-qwerty +sk-qwerty_bksl +sk-qwertz +slovene +sr-cy +sr-latin +sun-pl +sun-pl-altgraph +sundvorak +sunkeymap +sunt4-es +sunt4-fi-latin1 +sunt4-no-latin1 +sunt5-cz-us +sunt5-de-latin1 +sunt5-es +sunt5-fi-latin1 +sunt5-fr-latin1 +sunt5-ru +sunt5-uk +sunt5-us-cz +sunt6-uk +sv-latin1 +sy-ku +sy-ku_alt +sy-ku_f +taiwanese +tj_alt-UTF8 +tm +tm-alt +tr +tr-alt +tr-e +tr-f +tr-intl +tr-ku +tr-ku_alt +tr-ku_f +tr_f-latin5 +tr_q-latin5 +tralt +trf +trq +ttwin_alt-UTF-8 +ttwin_cplk-UTF-8 +ttwin_ct_sh-UTF-8 +ttwin_ctrl-UTF-8 +tw +tw-indigenous +tw-saisiyat +ua +ua-cp1251 +ua-crh +ua-crh_alt +ua-crh_f +ua-utf +ua-utf-ws +ua-ws +uk +unicode +us +us-acentos +us-acentos-old +us-alt-intl +us-altgr-intl +us-colemak +us-colemak_dh +us-colemak_dh_iso +us-colemak_dh_ortho +us-colemak_dh_wide +us-colemak_dh_wide_iso +us-dvorak +us-dvorak-alt-intl +us-dvorak-classic +us-dvorak-intl +us-dvorak-l +us-dvorak-mac +us-dvorak-r +us-dvp +us-euro +us-haw +us-hbs +us-intl +us-mac +us-norman +us-olpc2 +us-symbolic +us-workman +us-workman-intl +us1 +uz-latin +vn +vn-fr +vn-us +wangbe +wangbe2 +windowkeys diff --git a/rust/share/localectl-list-locale.txt b/rust/share/localectl-list-locale.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rust/share/localectl-list-locales.txt b/rust/share/localectl-list-locales.txt new file mode 100644 index 0000000000..a3da6481d4 --- /dev/null +++ b/rust/share/localectl-list-locales.txt @@ -0,0 +1,151 @@ +C.UTF-8 +aa_DJ.UTF-8 +af_ZA.UTF-8 +an_ES.UTF-8 +ar_AE.UTF-8 +ar_BH.UTF-8 +ar_DZ.UTF-8 +ar_EG.UTF-8 +ar_IQ.UTF-8 +ar_JO.UTF-8 +ar_KW.UTF-8 +ar_LB.UTF-8 +ar_LY.UTF-8 +ar_MA.UTF-8 +ar_OM.UTF-8 +ar_QA.UTF-8 +ar_SA.UTF-8 +ar_SD.UTF-8 +ar_SY.UTF-8 +ar_TN.UTF-8 +ar_YE.UTF-8 +ast_ES.UTF-8 +be_BY.UTF-8 +bg_BG.UTF-8 +bhb_IN.UTF-8 +br_FR.UTF-8 +bs_BA.UTF-8 +ca_AD.UTF-8 +ca_ES.UTF-8 +ca_FR.UTF-8 +ca_IT.UTF-8 +cs_CZ.UTF-8 +cy_GB.UTF-8 +da_DK.UTF-8 +de_AT.UTF-8 +de_BE.UTF-8 +de_CH.UTF-8 +de_DE.UTF-8 +de_IT.UTF-8 +de_LI.UTF-8 +de_LU.UTF-8 +el_CY.UTF-8 +el_GR.UTF-8 +en_AU.UTF-8 +en_BW.UTF-8 +en_CA.UTF-8 +en_DK.UTF-8 +en_GB.UTF-8 +en_HK.UTF-8 +en_IE.UTF-8 +en_NZ.UTF-8 +en_PH.UTF-8 +en_SC.UTF-8 +en_SG.UTF-8 +en_US.UTF-8 +en_ZA.UTF-8 +en_ZW.UTF-8 +es_AR.UTF-8 +es_BO.UTF-8 +es_CL.UTF-8 +es_CO.UTF-8 +es_CR.UTF-8 +es_DO.UTF-8 +es_EC.UTF-8 +es_ES.UTF-8 +es_GT.UTF-8 +es_HN.UTF-8 +es_MX.UTF-8 +es_NI.UTF-8 +es_PA.UTF-8 +es_PE.UTF-8 +es_PR.UTF-8 +es_PY.UTF-8 +es_SV.UTF-8 +es_US.UTF-8 +es_UY.UTF-8 +es_VE.UTF-8 +et_EE.UTF-8 +eu_ES.UTF-8 +fi_FI.UTF-8 +fo_FO.UTF-8 +fr_BE.UTF-8 +fr_CA.UTF-8 +fr_CH.UTF-8 +fr_FR.UTF-8 +fr_LU.UTF-8 +ga_IE.UTF-8 +gd_GB.UTF-8 +gl_ES.UTF-8 +gv_GB.UTF-8 +he_IL.UTF-8 +hr_HR.UTF-8 +hsb_DE.UTF-8 +hu_HU.UTF-8 +id_ID.UTF-8 +is_IS.UTF-8 +it_CH.UTF-8 +it_IT.UTF-8 +ja_JP.UTF-8 +ka_GE.UTF-8 +kk_KZ.UTF-8 +kl_GL.UTF-8 +ko_KR.UTF-8 +ku_TR.UTF-8 +kw_GB.UTF-8 +lg_UG.UTF-8 +lt_LT.UTF-8 +lv_LV.UTF-8 +mg_MG.UTF-8 +mi_NZ.UTF-8 +mk_MK.UTF-8 +ms_MY.UTF-8 +mt_MT.UTF-8 +nb_NO.UTF-8 +nl_BE.UTF-8 +nl_NL.UTF-8 +nn_NO.UTF-8 +no_NO.UTF-8 +oc_FR.UTF-8 +om_KE.UTF-8 +pl_PL.UTF-8 +pt_BR.UTF-8 +pt_PT.UTF-8 +ro_RO.UTF-8 +ru_RU.UTF-8 +ru_UA.UTF-8 +sk_SK.UTF-8 +sl_SI.UTF-8 +so_DJ.UTF-8 +so_KE.UTF-8 +so_SO.UTF-8 +sq_AL.UTF-8 +st_ZA.UTF-8 +sv_FI.UTF-8 +sv_SE.UTF-8 +tcy_IN.UTF-8 +tg_TJ.UTF-8 +th_TH.UTF-8 +tl_PH.UTF-8 +tr_CY.UTF-8 +tr_TR.UTF-8 +uk_UA.UTF-8 +uz_UZ.UTF-8 +wa_BE.UTF-8 +xh_ZA.UTF-8 +yi_US.UTF-8 +zh_CN.UTF-8 +zh_HK.UTF-8 +zh_SG.UTF-8 +zh_TW.UTF-8 +zu_ZA.UTF-8