Skip to content

Commit d2d165d

Browse files
author
FreeSynergy
committed
feat(phase1): capability detection + install wizard
Implements Phase 1 of the build plan (1.1 + 1.2 + 1.3): Design patterns: - Strategy: BootstrapStrategy (GuiBootstrap / TuiBootstrap / HeadlessBootstrap) selected based on fs-info capability detection - State Machine: WizardMachine drives 7 sequential steps with back-navigation New modules: - capability.rs BootstrapCapability, BootstrapMode, DisplayEnv, ContainerRuntime - error.rs FsInitError - keys.rs FTL key constants with hardcoded English fallback - store_clone.rs clone_store() + default_store_dir() (extracted from main) - strategy/ BootstrapStrategy trait + 3 impls - wizard/ WizardStep trait + WizardMachine + 7 step structs Wizard steps: Welcome → Capability → Engine → Bundle → Confirm → Progress → Done Store path migrated: ~/.local/share/fsn/store → ~/.local/share/freesynergy/store CLI: --clone-only flag added for non-interactive use i18n: all user-facing strings as FTL key constants (keys.rs)
1 parent c49eb43 commit d2d165d

19 files changed

Lines changed: 2355 additions & 103 deletions

Cargo.lock

Lines changed: 1167 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ name = "fs-init"
99
path = "src/main.rs"
1010

1111
[dependencies]
12+
fs-info = { path = "../fs-info" }
1213
gix = { version = "0.80", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "worktree-mutation"] }
1314
clap = { version = "4", features = ["derive"] }

src/capability.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//! Bootstrap capability detection via fs-info.
2+
//!
3+
//! Determines which display environment and terminal is available,
4+
//! and selects the appropriate `BootstrapMode` for the current system.
5+
6+
use fs_info::{DetectedFeatures, Feature, FeatureDetect, OsInfo};
7+
8+
// ── Display environment ───────────────────────────────────────────────────────
9+
10+
/// Which graphical display server (if any) is active.
11+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12+
pub enum DisplayEnv {
13+
/// Wayland compositor is running.
14+
Wayland,
15+
/// X11 display server is running.
16+
X11,
17+
/// No display server — headless or SSH-only session.
18+
None,
19+
}
20+
21+
impl DisplayEnv {
22+
fn from_features(features: &DetectedFeatures) -> Self {
23+
if features.has(Feature::WaylandDisplay) {
24+
DisplayEnv::Wayland
25+
} else if features.has(Feature::X11Display) {
26+
DisplayEnv::X11
27+
} else {
28+
DisplayEnv::None
29+
}
30+
}
31+
32+
/// Human-readable label for this display environment.
33+
pub fn label(self) -> &'static str {
34+
match self {
35+
DisplayEnv::Wayland => "Wayland",
36+
DisplayEnv::X11 => "X11",
37+
DisplayEnv::None => "none (headless / SSH)",
38+
}
39+
}
40+
}
41+
42+
// ── Bootstrap mode ────────────────────────────────────────────────────────────
43+
44+
/// The bootstrap mode determines which UI the wizard runs in.
45+
///
46+
/// All modes currently use the same CLI-based wizard (plain text output +
47+
/// stdin input) because the render engine has not been installed yet.
48+
/// The mode controls which bundles are pre-selected and what is launched
49+
/// after installation completes.
50+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51+
pub enum BootstrapMode {
52+
/// Display server is available — GUI engine will be installed.
53+
Gui,
54+
/// No display server, but an interactive terminal is present — TUI mode.
55+
Tui,
56+
/// No display server and no interactive terminal — API + CLI only.
57+
Headless,
58+
}
59+
60+
impl BootstrapMode {
61+
/// Human-readable label.
62+
pub fn label(self) -> &'static str {
63+
match self {
64+
BootstrapMode::Gui => "GUI",
65+
BootstrapMode::Tui => "TUI",
66+
BootstrapMode::Headless => "Headless (API + CLI only)",
67+
}
68+
}
69+
}
70+
71+
// ── Container runtime ─────────────────────────────────────────────────────────
72+
73+
/// Which container runtime is available.
74+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75+
pub enum ContainerRuntime {
76+
Podman,
77+
Docker,
78+
None,
79+
}
80+
81+
impl ContainerRuntime {
82+
fn from_features(features: &DetectedFeatures) -> Self {
83+
if features.has(Feature::Podman) {
84+
ContainerRuntime::Podman
85+
} else if features.has(Feature::Docker) {
86+
ContainerRuntime::Docker
87+
} else {
88+
ContainerRuntime::None
89+
}
90+
}
91+
92+
/// Human-readable label.
93+
pub fn label(self) -> &'static str {
94+
match self {
95+
ContainerRuntime::Podman => "Podman",
96+
ContainerRuntime::Docker => "Docker",
97+
ContainerRuntime::None => "none detected",
98+
}
99+
}
100+
}
101+
102+
// ── Bootstrap capability ──────────────────────────────────────────────────────
103+
104+
/// All system capabilities relevant to bootstrapping.
105+
pub struct BootstrapCapability {
106+
/// OS information (name, version, arch, hostname).
107+
pub os: OsInfo,
108+
/// Active display environment.
109+
pub display: DisplayEnv,
110+
/// Whether stdin is an interactive terminal.
111+
pub has_terminal: bool,
112+
/// Available container runtime.
113+
pub container: ContainerRuntime,
114+
/// Derived bootstrap mode.
115+
pub mode: BootstrapMode,
116+
}
117+
118+
impl BootstrapCapability {
119+
/// Detect all capabilities from the live system.
120+
pub fn detect() -> Self {
121+
let features = FeatureDetect::run();
122+
let os = OsInfo::detect();
123+
let display = DisplayEnv::from_features(&features);
124+
let has_terminal = features.has_terminal();
125+
let container = ContainerRuntime::from_features(&features);
126+
let mode = derive_mode(display, has_terminal);
127+
128+
BootstrapCapability {
129+
os,
130+
display,
131+
has_terminal,
132+
container,
133+
mode,
134+
}
135+
}
136+
}
137+
138+
fn derive_mode(display: DisplayEnv, has_terminal: bool) -> BootstrapMode {
139+
if display != DisplayEnv::None {
140+
BootstrapMode::Gui
141+
} else if has_terminal {
142+
BootstrapMode::Tui
143+
} else {
144+
BootstrapMode::Headless
145+
}
146+
}

src/error.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//! Error type for fs-init.
2+
3+
use std::fmt;
4+
5+
/// All errors that can occur during bootstrap.
6+
#[derive(Debug)]
7+
pub enum FsInitError {
8+
/// Store clone failed.
9+
StoreClone(String),
10+
/// I/O error (stdin/stdout).
11+
Io(std::io::Error),
12+
/// User aborted the wizard.
13+
Aborted,
14+
}
15+
16+
impl fmt::Display for FsInitError {
17+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18+
match self {
19+
FsInitError::StoreClone(msg) => write!(f, "Store clone failed: {msg}"),
20+
FsInitError::Io(e) => write!(f, "I/O error: {e}"),
21+
FsInitError::Aborted => write!(f, "Aborted by user"),
22+
}
23+
}
24+
}
25+
26+
impl std::error::Error for FsInitError {
27+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
28+
match self {
29+
FsInitError::Io(e) => Some(e),
30+
_ => None,
31+
}
32+
}
33+
}
34+
35+
impl From<std::io::Error> for FsInitError {
36+
fn from(e: std::io::Error) -> Self {
37+
FsInitError::Io(e)
38+
}
39+
}

src/keys.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//! FTL key constants with hardcoded English fallback values.
2+
//!
3+
//! Every user-facing string must be declared here.
4+
//! The FTL key name is documented in the comment above each constant.
5+
//! The value is the English fallback embedded in the binary.
6+
//! When fs-i18n is available at runtime, the FTL key is used instead.
7+
8+
// ── General ───────────────────────────────────────────────────────────────────
9+
10+
/// FTL key: `init-title`
11+
pub const INIT_TITLE: &str = "FreeSynergy Init";
12+
13+
/// FTL key: `init-version`
14+
pub const INIT_DIVIDER: &str = "────────────────────────────────────────────────";
15+
16+
/// FTL key: `init-abort-hint`
17+
pub const INIT_ABORT_HINT: &str = "Press Ctrl+C at any time to abort.";
18+
19+
/// FTL key: `init-prompt-continue`
20+
pub const INIT_PROMPT_CONTINUE: &str = "Press Enter to continue…";
21+
22+
/// FTL key: `init-prompt-choice`
23+
pub const INIT_PROMPT_CHOICE: &str = "Enter number: ";
24+
25+
/// FTL key: `init-invalid-choice`
26+
pub const INIT_INVALID_CHOICE: &str = "Invalid choice. Please try again.";
27+
28+
// ── Capability detection ──────────────────────────────────────────────────────
29+
30+
/// FTL key: `init-detecting-capabilities`
31+
pub const INIT_DETECTING_CAPABILITIES: &str = "Detecting system capabilities…";
32+
33+
/// FTL key: `init-capability-os`
34+
pub const INIT_CAPABILITY_OS: &str = " OS: ";
35+
36+
/// FTL key: `init-capability-arch`
37+
pub const INIT_CAPABILITY_ARCH: &str = " Architecture: ";
38+
39+
// ── Wizard steps ──────────────────────────────────────────────────────────────
40+
41+
/// FTL key: `init-step-welcome-title`
42+
pub const INIT_STEP_WELCOME_TITLE: &str = "Welcome";
43+
44+
/// FTL key: `init-step-welcome-body`
45+
pub const INIT_STEP_WELCOME_BODY: &str =
46+
"This wizard will guide you through installing FreeSynergy on this node.\n\
47+
It will clone the official store and help you choose what to install.";
48+
49+
/// FTL key: `init-step-capability-title`
50+
pub const INIT_STEP_CAPABILITY_TITLE: &str = "System Capabilities";
51+
52+
/// FTL key: `init-step-engine-title`
53+
pub const INIT_STEP_ENGINE_TITLE: &str = "Render Engine";
54+
55+
/// FTL key: `init-step-engine-prompt`
56+
pub const INIT_STEP_ENGINE_PROMPT: &str = "Choose the render engine for the desktop UI.\n\
57+
(Only relevant if you install a bundle with a desktop.)";
58+
59+
/// FTL key: `init-step-bundle-title`
60+
pub const INIT_STEP_BUNDLE_TITLE: &str = "Bundle Selection";
61+
62+
/// FTL key: `init-step-bundle-prompt`
63+
pub const INIT_STEP_BUNDLE_PROMPT: &str = "Choose a bundle to install:";
64+
65+
/// FTL key: `init-step-confirm-title`
66+
pub const INIT_STEP_CONFIRM_TITLE: &str = "Confirm Installation";
67+
68+
/// FTL key: `init-step-confirm-bundle`
69+
pub const INIT_STEP_CONFIRM_BUNDLE: &str = " Bundle: ";
70+
71+
/// FTL key: `init-step-confirm-engine`
72+
pub const INIT_STEP_CONFIRM_ENGINE: &str = " Render engine: ";
73+
74+
/// FTL key: `init-step-confirm-target`
75+
pub const INIT_STEP_CONFIRM_TARGET: &str = " Install target: ";
76+
77+
/// FTL key: `init-step-confirm-question`
78+
pub const INIT_STEP_CONFIRM_QUESTION: &str = "Proceed? [y/N]: ";
79+
80+
/// FTL key: `init-step-progress-title`
81+
pub const INIT_STEP_PROGRESS_TITLE: &str = "Installing";
82+
83+
/// FTL key: `init-step-progress-cloning-store`
84+
pub const INIT_STEP_PROGRESS_CLONING_STORE: &str = " Cloning store catalog…";
85+
86+
/// FTL key: `init-step-progress-clone-ok`
87+
pub const INIT_STEP_PROGRESS_CLONE_OK: &str = " Store ready.";
88+
89+
/// FTL key: `init-step-progress-clone-exists`
90+
pub const INIT_STEP_PROGRESS_CLONE_EXISTS: &str = " Store already present — skipping clone.";
91+
92+
/// FTL key: `init-step-progress-install-pending`
93+
pub const INIT_STEP_PROGRESS_INSTALL_PENDING: &str =
94+
" Package install pipeline not yet available (Phase 2).";
95+
96+
/// FTL key: `init-step-done-title`
97+
pub const INIT_STEP_DONE_TITLE: &str = "Done";
98+
99+
/// FTL key: `init-step-done-body`
100+
pub const INIT_STEP_DONE_BODY: &str = "FreeSynergy has been bootstrapped.\n\
101+
The store is available locally. Start the Store service to continue.";
102+
103+
/// FTL key: `init-step-done-store-path`
104+
pub const INIT_STEP_DONE_STORE_PATH: &str = " Store path: ";
105+
106+
// ── Target ────────────────────────────────────────────────────────────────────
107+
108+
/// FTL key: `init-target-container`
109+
pub const INIT_TARGET_CONTAINER: &str = "Container (Podman / Docker)";
110+
111+
/// FTL key: `init-target-rpm`
112+
pub const INIT_TARGET_RPM: &str = "RPM package";
113+
114+
/// FTL key: `init-target-deb`
115+
pub const INIT_TARGET_DEB: &str = "DEB package";
116+
117+
/// FTL key: `init-target-appimage`
118+
pub const INIT_TARGET_APPIMAGE: &str = "AppImage";

0 commit comments

Comments
 (0)