cargo-bubba 0.1.0

cargo subcommand for the Bubba mobile framework
//! `cargo bubba new` — scaffold a new Bubba project.

use anyhow::{bail, Context, Result};
use colored::Colorize;
use std::path::PathBuf;

pub fn run(name: &str, path: Option<&str>) -> Result<()> {
    let dir = PathBuf::from(path.unwrap_or(name));

    // The package name is always the final directory component, never a full path.
    // `cargo bubba new my_app`          → package name "my_app"
    // `cargo bubba new /tmp/my_app`     → package name "my_app"
    // `cargo bubba new ./projects/foo`  → package name "foo"
    let pkg_name = dir
        .file_name()
        .and_then(|n| n.to_str())
        .ok_or_else(|| anyhow::anyhow!("Could not determine package name from path `{}`", dir.display()))?
        .to_string();

    // Validate: Cargo package names must be valid Rust identifiers.
    if pkg_name.is_empty() || pkg_name.starts_with(|c: char| !c.is_alphabetic() && c != '_') {
        bail!(
            "Invalid package name `{}`. Package names must start with a letter or underscore.",
            pkg_name
        );
    }

    if dir.exists() {
        bail!("Directory `{}` already exists. Choose a different name or path.", dir.display());
    }

    println!("{} Scaffolding `{}`…", "".cyan(), pkg_name.bold());

    create_dir_structure(&dir, &pkg_name)?;
    write_cargo_toml(&dir, &pkg_name)?;
    write_main(&dir)?;
    write_screens(&dir)?;
    write_assets(&dir)?;
    write_android_manifest(&dir, &pkg_name)?;
    write_gitignore(&dir)?;

    println!("\n{} Created `{}` successfully!\n", "".green().bold(), pkg_name.bold());
    println!("  {} cd {}", "".dimmed(), dir.display());
    println!("  {} cargo bubba doctor   # verify your environment", "".dimmed());
    println!("  {} cargo bubba build    # compile your first .apk\n", "".dimmed());

    Ok(())
}

fn create_dir_structure(base: &PathBuf, _name: &str) -> Result<()> {
    for sub in &[
        "src/screens",
        "assets",
        "android/app/src/main",
    ] {
        std::fs::create_dir_all(base.join(sub))
            .with_context(|| format!("Failed to create {}", base.join(sub).display()))?;
    }
    Ok(())
}

fn write_cargo_toml(base: &PathBuf, name: &str) -> Result<()> {
    let content = format!(r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "{name}"
path = "src/main.rs"

[dependencies]
bubba = {{ version = "0.1", features = ["android"] }}

[profile.release]
opt-level = "z"        # minimise binary size for mobile
lto = true
codegen-units = 1
panic = "abort"        # removes unwinding overhead on Android
strip = true

[package.metadata.bubba]
app-name = "{name}"
package  = "rs.bubba.{name}"
min-sdk  = 21
target-sdk = 34
"#, name = name);
    write(base.join("Cargo.toml"), &content)
}

fn write_main(base: &PathBuf) -> Result<()> {
    let content = r#"use bubba::prelude::*;

mod screens;
use screens::home::Home;

fn main() {
    // Boot the Bubba runtime and launch the Home screen.
    bubba::launch!(Home);
}
"#;
    write(base.join("src/main.rs"), content)
}

fn write_screens(base: &PathBuf) -> Result<()> {
    // src/screens/mod.rs
    write(base.join("src/screens/mod.rs"), r#"pub mod home;
pub mod profile;
"#)?;

    // home.rs
    write(base.join("src/screens/home.rs"), r#"use bubba::prelude::*;
use crate::screens::profile::Profile;

pub fn Home() -> Screen {
    view! {
        <h1 class="title">"Welcome to Bubba"</h1>

        <button class="primary-btn" onclick=alert("You tapped the button!")>
            "Tap me"
        </button>

        <button class="link-btn" onclick=navigate(Profile)>
            "Go to Profile"
        </button>

        <input class="text-input"
               oninput=log("Typing...")
               onkeypress=log("Key pressed") />
    }
}
"#)?;

    // profile.rs
    write(base.join("src/screens/profile.rs"), r#"use bubba::prelude::*;
use crate::screens::home::Home;

pub fn Profile() -> Screen {
    view! {
        <img src="avatar.png" class="avatar" />

        <p class="label">"Username: Alice"</p>

        <button class="danger-btn" onclick=navigate(Home)>
            "Log out"
        </button>
    }
}
"#)?;

    Ok(())
}

fn write_assets(base: &PathBuf) -> Result<()> {
    write(base.join("assets/main.css"), r#"/* ── Bubba Starter Stylesheet ─────────────────────── */

/* Layout */
.screen {
    flex-direction: column;
    align-items: center;
    padding: 24px;
    background: #f8f9fa;
}

/* Typography */
.title {
    font-size: 28px;
    font-weight: bold;
    color: #1a1a2e;
    margin-bottom: 20px;
    text-align: center;
}

.label {
    font-size: 16px;
    color: #555;
    margin-bottom: 8px;
}

/* Buttons */
.primary-btn {
    background: #4CAF50;
    color: white;
    padding: 14px 28px;
    border-radius: 10px;
    font-size: 16px;
    font-weight: 600;
    margin-top: 16px;
    shadow: 0 2px 8px rgba(76,175,80,0.3);
}

.link-btn {
    background: transparent;
    color: #4CAF50;
    padding: 12px 24px;
    border-radius: 10px;
    font-size: 15px;
    margin-top: 10px;
    border: 1.5px solid #4CAF50;
}

.danger-btn {
    background: #e53935;
    color: white;
    padding: 14px 28px;
    border-radius: 10px;
    font-size: 16px;
    font-weight: 600;
    margin-top: 20px;
}

/* Inputs */
.text-input {
    background: white;
    border: 1.5px solid #ddd;
    border-radius: 10px;
    padding: 14px 16px;
    font-size: 16px;
    width: 100%;
    margin-top: 20px;
}

.text-input:focus {
    border-color: #4CAF50;
    shadow: 0 0 0 3px rgba(76,175,80,0.15);
}

/* Avatar */
.avatar {
    width: 88px;
    height: 88px;
    border-radius: 44px;
    margin-bottom: 16px;
    border: 3px solid #4CAF50;
}
"#)
}

fn write_android_manifest(base: &PathBuf, name: &str) -> Result<()> {
    let content = format!(r#"<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="rs.bubba.{name}">

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="34" />

    <application
        android:label="{name}"
        android:allowBackup="true"
        android:supportsRtl="true">

        <activity
            android:name="rs.bubba.BubbaActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>

</manifest>
"#, name = name);
    write(base.join("android/app/src/main/AndroidManifest.xml"), &content)
}

fn write_gitignore(base: &PathBuf) -> Result<()> {
    write(base.join(".gitignore"), r#"/target
/dist
*.apk
.env
"#)
}

fn write(path: PathBuf, content: &str) -> Result<()> {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    std::fs::write(&path, content)
        .with_context(|| format!("Failed to write {}", path.display()))?;
    println!("  {} {}", "create".green(), path.display());
    Ok(())
}