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));
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();
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<()> {
write(base.join("src/screens/mod.rs"), r#"pub mod home;
pub mod profile;
"#)?;
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") />
}
}
"#)?;
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(())
}