//! `shturman-shell` (lib) — первый Slint-кадр (срезы C03/C04/C05/C07/C02) + headless //! software-render кадра в PNG (спека §6). Bin (`main.rs`) — тонкая обёртка над этим API: //! интерактивный `run` (dev: weston/нативно) либо `--screenshot ` (E2E/CI, без композитора). //! v0: одноразовое чтение `ui.theme`/Power при старте (best-effort; без шины — дефолты, #4). mod theme; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; use slint::ComponentHandle; slint::slint! { import { VerticalBox, HorizontalBox } from "std-widgets.slint"; export component AppWindow inherits Window { in property is-night: false; in property clock: "--:--"; in property network: "unknown"; in property ignition: "unknown"; in property <[string]> tiles: ["Навигация", "Музыка", "Телефон", "Ассистент", "Машина", "Настройки"]; title: "Штурман"; width: 1024px; height: 600px; background: root.is-night ? #0e1014 : #f4f5f7; VerticalBox { padding: 16px; spacing: 16px; HorizontalBox { height: 44px; Text { text: root.clock; font-size: 22px; color: root.is-night ? #f0f0f0 : #1a1a1a; vertical-alignment: center; } Rectangle { } Text { text: "сеть: " + root.network; color: root.is-night ? #9aa0a6 : #5f6368; vertical-alignment: center; } Text { text: "зажигание: " + root.ignition; color: root.is-night ? #9aa0a6 : #5f6368; vertical-alignment: center; } } HorizontalBox { spacing: 16px; for tile in root.tiles : Rectangle { background: root.is-night ? #1b1e24 : #ffffff; border-radius: 16px; Text { text: tile; font-size: 18px; color: root.is-night ? #e8eaed : #202124; horizontal-alignment: center; vertical-alignment: center; } } } } } } /// Размер первого кадра (логические пиксели = физические при scale 1.0). const FRAME_W: u32 = 1024; const FRAME_H: u32 = 600; /// Начальное состояние кадра, прочитанное с шины (best-effort). Без шины — дефолты (#4). #[derive(Debug, Clone)] pub struct Initial { pub theme: String, pub ignition: String, pub network: String, } impl Default for Initial { fn default() -> Self { Self { theme: "auto".into(), ignition: "unknown".into(), network: "unknown".into(), } } } /// Одноразовое чтение состояния с шины (best-effort). Без шины/сервисов — дефолты (#4). pub fn read_initial() -> Initial { // current-thread рантайм: одно best-effort чтение на холодном старте, без пула потоков (#11). let rt = match tokio::runtime::Builder::new_current_thread() .enable_all() .build() { Ok(rt) => rt, Err(_) => return Initial::default(), }; rt.block_on(async { match connect_and_read().await { Ok(i) => i, Err(e) => { tracing::warn!(error = %e, "нет шины/сервисов — дефолты кадра"); Initial::default() } } }) } async fn connect_and_read() -> anyhow::Result { let def = Initial::default(); // единый источник дефолтов (без дублей литералов) let conn = shturman_sdk::connect().await?; let settings = shturman_sdk::SettingsClient::new(&conn).await?; let theme = settings .get("ui.theme") .await .ok() .and_then(|v| String::try_from(v).ok()) .unwrap_or(def.theme); let power = shturman_sdk::PowerClient::new(&conn).await?; let ignition = power .ignition_state() .await .map(|i| i.as_str().to_string()) .unwrap_or(def.ignition); Ok(Initial { theme, ignition, network: def.network, }) } /// Часы UTC `HH:MM` без tz-зависимостей (локальная tz — позже, a-base §7). Возвращает (час, строка). pub fn utc_hh_mm() -> (u8, String) { let secs = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); let h = ((secs / 3600) % 24) as u8; let m = ((secs / 60) % 60) as u8; (h, format!("{h:02}:{m:02}")) } /// Создать и наполнить окно (общий код интерактива и скриншота). fn build_ui(initial: &Initial, hour: u8, clock: &str) -> anyhow::Result { let ui = AppWindow::new()?; ui.set_is_night(theme::resolve_night(&initial.theme, hour)); ui.set_clock(clock.into()); ui.set_network(initial.network.as_str().into()); ui.set_ignition(initial.ignition.as_str().into()); Ok(ui) } /// Интерактивный запуск (dev: weston в VM / нативно на хосте). Блокирующий event-loop. pub fn run_interactive(initial: &Initial, hour: u8, clock: &str) -> anyhow::Result<()> { let ui = build_ui(initial, hour, clock)?; tracing::info!("первый Slint-кадр (интерактивно)"); ui.run()?; Ok(()) } // --- headless software-render первого кадра (спека §6) — через общий хелпер shturman-render --- /// Headless software-render первого кадра в PNG (спека §6). Без дисплей-сервера/композитора. /// `hour` задаёт тему для `auto` (тест — детерминированно); `clock` берётся из текущего времени. pub fn render_screenshot(initial: &Initial, hour: u8, path: &Path) -> anyhow::Result<()> { let (_, clock) = utc_hh_mm(); shturman_render::render_to_png(|| build_ui(initial, hour, &clock), FRAME_W, FRAME_H, path)?; tracing::info!(path = %path.display(), "кадр записан (software-render)"); Ok(()) }