feat(shell): первый Slint-кадр на SDK (срезы C03/04/05/07/02) + slint GPL exception

theme::resolve_night (TDD); slint! AppWindow (статус-бар часы+сеть + грид тайлов + тема день/ночь);
main — best-effort чтение Settings/Power через sdk (без шины — дефолты, #4); часы UTC (локаль tz — позже).
deny.toml: GPL-3.0 exceptions для slint-крейтов (вариант A, финал к v4) + BSL-1.0 (error-code).
Slint тянет zbus5/thiserror2 — дубли версий (bans=warn). Реальный screenshot кадра — План 5 E2E.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
2026-06-24 13:03:22 +03:00
parent 75a7132864
commit ca763116d8
6 changed files with 4607 additions and 40 deletions
Generated
+4391 -38
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -9,6 +9,7 @@ members = [
"crates/core/shturman-firstboot",
"crates/core/shturman-settings",
"crates/core/shturman-power",
"crates/apps/shturman-shell",
]
[workspace.package]
@@ -28,6 +29,7 @@ tracing-journald = "0.3"
anyhow = "1"
thiserror = "1"
clap = { version = "4", features = ["derive"] }
slint = { version = "1", default-features = false, features = ["compat-1-2", "std", "backend-winit", "renderer-software"] }
# dev
tempfile = "3"
futures-util = "0.3"
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "shturman-shell"
version = "0.0.0"
edition.workspace = true
license.workspace = true
[dependencies]
shturman-sdk = { path = "../../shturman-sdk" }
shturman-common = { path = "../../shturman-common" }
tokio.workspace = true
anyhow.workspace = true
tracing.workspace = true
slint.workspace = true
+147
View File
@@ -0,0 +1,147 @@
//! `shturman-shell` — первый Slint-кадр (срезы C03/C04/C05/C07/C02). На SDK (architecture §1).
//! v0: одноразовое чтение `ui.theme`/Power при старте (best-effort; без шины — дефолты, #4); рендер.
//! Live-обновления (Changed/AccChanged) и локальная tz часов — позже (v0.5 / a-base §7).
mod theme;
use std::time::{SystemTime, UNIX_EPOCH};
slint::slint! {
import { VerticalBox, HorizontalBox } from "std-widgets.slint";
export component AppWindow inherits Window {
in property <bool> is-night: false;
in property <string> clock: "--:--";
in property <string> network: "unknown";
in property <string> 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;
}
}
}
}
}
}
#[derive(Debug)]
struct Initial {
theme: String,
ignition: String,
network: String,
}
impl Default for Initial {
fn default() -> Self {
Self {
theme: "auto".into(),
ignition: "unknown".into(),
network: "unknown".into(),
}
}
}
/// Одноразовое чтение состояния с шины (best-effort). Без шины/сервисов — дефолты (#4).
fn read_initial() -> Initial {
let rt = match tokio::runtime::Runtime::new() {
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<Initial> {
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_else(|| "auto".into());
let power = shturman_sdk::PowerClient::new(&conn).await?;
let ignition = power
.ignition_state()
.await
.map(|i| i.as_str().to_string())
.unwrap_or_else(|_| "unknown".into());
Ok(Initial {
theme,
ignition,
network: "unknown".into(),
})
}
/// Часы UTC `HH:MM` без tz-зависимостей (локальная tz — позже, a-base §7). Возвращает (час, строка).
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 main() -> anyhow::Result<()> {
shturman_common::init_tracing("shturman-shell");
let initial = read_initial();
let (hour, clock) = utc_hh_mm();
let ui = AppWindow::new()?;
ui.set_is_night(theme::resolve_night(&initial.theme, hour));
ui.set_clock(clock.into());
ui.set_network(initial.network.into());
ui.set_ignition(initial.ignition.into());
tracing::info!("первый Slint-кадр");
ui.run()?;
Ok(())
}
+35
View File
@@ -0,0 +1,35 @@
//! Тема день/ночь (C §6). v0: по часу (UTC); локальная tz и GPS-восход — позже (a-base §7 / домен K).
/// `day`→день, `night`→ночь, `auto`/неизвестное → ночь если `hour < 7 || hour >= 20` (🟡 пороги).
pub fn resolve_night(theme: &str, hour_utc: u8) -> bool {
match theme {
"day" => false,
"night" => true,
_ => !(7..20).contains(&hour_utc),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_day_night() {
assert!(!resolve_night("day", 3));
assert!(resolve_night("night", 12));
}
#[test]
fn auto_by_hour() {
assert!(resolve_night("auto", 2)); // ночь
assert!(resolve_night("auto", 23)); // ночь
assert!(!resolve_night("auto", 12)); // день
assert!(!resolve_night("auto", 7)); // день (нижняя граница)
}
#[test]
fn unknown_falls_back_to_auto() {
assert_eq!(resolve_night("bogus", 2), resolve_night("auto", 2));
assert_eq!(resolve_night("bogus", 12), resolve_night("auto", 12));
}
}
+19 -2
View File
@@ -1,6 +1,10 @@
# Лицензионная гигиена (#12) + advisories.
# Заражающий дистрибуцию копилефт (GPL/AGPL) — НЕ в allow. LGPL — гранулярно (точечно при появлении).
# slint GPL-3.0-exception добавится в Плане 4 (когда slint войдёт в граф зависимостей).
# Заражающий дистрибуцию копилефт (GPL/AGPL) — НЕ в общем allow. LGPL — гранулярно (точечно при появлении).
#
# Slint лицензирован "GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0".
# Для embedded royalty-free неприменим → берём GPL-3.0 (вариант A; финал по UI-тулкиту/лицензии — к v4,
# см. docs/specs/v0.1-v0.6-foundation.md §12). Шипимый UI-бинарь прод-образа (v4) будет копилефтным.
# Точечные exceptions для slint-крейтов ниже — НЕ глобальный allow GPL.
[advisories]
version = 2
@@ -16,8 +20,21 @@ allow = [
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
"BSL-1.0",
]
confidence-threshold = 0.9
exceptions = [
{ name = "slint", allow = ["GPL-3.0-only"] },
{ name = "slint-macros", allow = ["GPL-3.0-only"] },
{ name = "i-slint-core", allow = ["GPL-3.0-only"] },
{ name = "i-slint-core-macros", allow = ["GPL-3.0-only"] },
{ name = "i-slint-common", allow = ["GPL-3.0-only"] },
{ name = "i-slint-compiler", allow = ["GPL-3.0-only"] },
{ name = "i-slint-backend-selector", allow = ["GPL-3.0-only"] },
{ name = "i-slint-backend-winit", allow = ["GPL-3.0-only"] },
{ name = "i-slint-renderer-software", allow = ["GPL-3.0-only"] },
{ name = "i-slint-renderer-skia", allow = ["GPL-3.0-only"] },
]
[bans]
multiple-versions = "warn"