diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index d28f417..e389254 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -17,6 +17,8 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: Install prerequisites
run: |
+ cargo install cargo-workspaces
+
sudo apt-get update
sudo apt-get install -y \
libudev-dev \
diff --git a/.gitignore b/.gitignore
index dc5e6fd..c09504c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,11 +19,11 @@ tarpaulin-report.html
# Jetbrain
.idea/
-/docs/handbook/.obsidian/app.json
-/docs/handbook/.obsidian/appearance.json
-/docs/handbook/.obsidian/hotkeys.json
-/docs/handbook/.obsidian/workspace.json
-/docs/handbook/.obsidian/plugins
-!/docs/handbook/.obsidian/plugins/obsidian-git/data.json
-!/docs/handbook/.obsidian/plugins/obsidian-linter/data.json
+.obsidian/app.json
+.obsidian/appearance.json
+.obsidian/hotkeys.json
+.obsidian/workspace.json
+.obsidian/plugins
+!.obsidian/plugins/obsidian-git/data.json
+!.obsidian/plugins/obsidian-linter/data.json
diff --git a/docs/handbook/.obsidian/community-plugins.json b/.obsidian/community-plugins.json
similarity index 100%
rename from docs/handbook/.obsidian/community-plugins.json
rename to .obsidian/community-plugins.json
diff --git a/docs/handbook/.obsidian/core-plugins.json b/.obsidian/core-plugins.json
similarity index 100%
rename from docs/handbook/.obsidian/core-plugins.json
rename to .obsidian/core-plugins.json
diff --git a/docs/handbook/.obsidian/graph.json b/.obsidian/graph.json
similarity index 100%
rename from docs/handbook/.obsidian/graph.json
rename to .obsidian/graph.json
diff --git a/docs/handbook/.obsidian/themes/GitHub theme/manifest.json b/.obsidian/themes/GitHub theme/manifest.json
similarity index 100%
rename from docs/handbook/.obsidian/themes/GitHub theme/manifest.json
rename to .obsidian/themes/GitHub theme/manifest.json
diff --git a/docs/handbook/.obsidian/themes/GitHub theme/theme.css b/.obsidian/themes/GitHub theme/theme.css
similarity index 100%
rename from docs/handbook/.obsidian/themes/GitHub theme/theme.css
rename to .obsidian/themes/GitHub theme/theme.css
diff --git a/docs/handbook/.obsidian/themes/Minimal/manifest.json b/.obsidian/themes/Minimal/manifest.json
similarity index 100%
rename from docs/handbook/.obsidian/themes/Minimal/manifest.json
rename to .obsidian/themes/Minimal/manifest.json
diff --git a/docs/handbook/.obsidian/themes/Minimal/theme.css b/.obsidian/themes/Minimal/theme.css
similarity index 100%
rename from docs/handbook/.obsidian/themes/Minimal/theme.css
rename to .obsidian/themes/Minimal/theme.css
diff --git a/Cargo.lock b/Cargo.lock
index a8391ef..952e0de 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -852,6 +852,31 @@ dependencies = [
"os_str_bytes",
]
+[[package]]
+name = "cmd_lib"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ba0f413777386d37f85afa5242f277a7b461905254c1af3c339d4af06800f62"
+dependencies = [
+ "cmd_lib_macros",
+ "faccess",
+ "lazy_static",
+ "log",
+ "os_pipe",
+]
+
+[[package]]
+name = "cmd_lib_macros"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e66605092ff6c6e37e0246601ae6c3f62dc1880e0599359b5f303497c112dc0"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "cocoa"
version = "0.24.0"
@@ -1261,6 +1286,7 @@ dependencies = [
"clap",
"config",
"dioxus",
+ "dip_bundle",
"dip_cli",
"dip_core",
"dip_desktop",
@@ -1272,6 +1298,22 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "dip_bundle"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bevy",
+ "cmd_lib",
+ "convert_case 0.5.0",
+ "dip_core",
+ "dirs",
+ "pathdiff",
+ "reqwest",
+ "tempfile",
+ "walkdir",
+]
+
[[package]]
name = "dip_cli"
version = "0.1.0"
@@ -1451,6 +1493,17 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+[[package]]
+name = "faccess"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5"
+dependencies = [
+ "bitflags",
+ "libc",
+ "winapi",
+]
+
[[package]]
name = "fastrand"
version = "1.8.0"
@@ -2826,6 +2879,16 @@ dependencies = [
"hashbrown",
]
+[[package]]
+name = "os_pipe"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb233f06c2307e1f5ce2ecad9f8121cffbbee2c95428f44ea85222e460d0d213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
[[package]]
name = "os_str_bytes"
version = "6.3.0"
diff --git a/Cargo.toml b/Cargo.toml
index 63c6683..6828727 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,6 @@
[workspace]
members = [
+ "plugins/bundle",
"plugins/core",
"plugins/macro",
"plugins/task",
@@ -23,18 +24,24 @@ keywords = ["declarative-ui", "ecs", "bevy", "dioxus", "cross-platform"]
anyhow = "1.0"
bevy = { version = "0.8", default-features = false }
bevy_ecs = "0.8"
+cmd_lib = "1"
config = "0.13"
+convert_case = "0.5"
dioxus = { version = "0.2", features = ["fermi"] }
dip = { version = "0.1", path = ".", features = ["desktop"] }
+dip_bundle = { version = "0.1", path = "./plugins/bundle", features = ["full"] }
dip_core = { version = "0.1", path = "./plugins/core" }
dip_macro = { version = "0.1", path = "./plugins/macro" }
dip_task = { version = "0.1", path = "./plugins/task" }
dirs = "4.0"
+pathdiff = "0.2"
reqwest = { version = "0.11", features = ["json", "blocking"] }
tokio = { version = "1.18", features = ["rt-multi-thread", "sync", "macros", "fs"], default-features = false }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
+tempfile = "3"
+walkdir = "2"
[package]
name = "dip"
@@ -57,6 +64,7 @@ bevy.workspace = true
clap = { version = "3.2", features = ["derive"], optional = true }
config.workspace = true
dioxus.workspace = true
+dip_bundle.workspace = true
dip_cli = { version = "0.1", path = "./plugins/ui/cli", optional = true }
dip_core = { version = "0.1", path = "./plugins/core" }
dip_desktop = { version = "0.1", path = "./plugins/ui/desktop", optional = true }
diff --git a/README.md b/README.md
index cc4bb06..683270c 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+
dip
diff --git a/docs/handbook/.obsidian/plugins/obsidian-git/data.json b/docs/handbook/.obsidian/plugins/obsidian-git/data.json
deleted file mode 100644
index 9ce6101..0000000
--- a/docs/handbook/.obsidian/plugins/obsidian-git/data.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "commitMessage": "vault backup: {{date}}",
- "autoCommitMessage": "vault backup: {{date}}",
- "commitDateFormat": "YYYY-MM-DD HH:mm:ss",
- "autoSaveInterval": 0,
- "autoPushInterval": 0,
- "autoPullInterval": 0,
- "autoPullOnBoot": false,
- "disablePush": false,
- "pullBeforePush": true,
- "disablePopups": false,
- "listChangedFilesInMessageBody": false,
- "showStatusBar": true,
- "updateSubmodules": true,
- "syncMethod": "merge",
- "customMessageOnAutoBackup": false,
- "autoBackupAfterFileChange": false,
- "treeStructure": false,
- "refreshSourceControl": true,
- "basePath": "../..",
- "differentIntervalCommitAndPush": false,
- "changedFilesInStatusBar": false,
- "username": "",
- "showedMobileNotice": true,
- "refreshSourceControlTimer": 7000,
- "showBranchStatusBar": true
-}
diff --git a/docs/handbook/Internal b/docs/handbook/Internal
index 8a1510f..3f793af 160000
--- a/docs/handbook/Internal
+++ b/docs/handbook/Internal
@@ -1 +1 @@
-Subproject commit 8a1510f3cab5829051f2c859779909995cf7768a
+Subproject commit 3f793af89d53c67b626443be7f16d98efb89b256
diff --git a/docs/handbook/Product/Framework/CLI/Installation.md b/docs/handbook/Product/Framework/CLI/Installation.md
index 973e9cd..4c236c6 100644
--- a/docs/handbook/Product/Framework/CLI/Installation.md
+++ b/docs/handbook/Product/Framework/CLI/Installation.md
@@ -1,3 +1,4 @@
+
## Installation
### From Crates.io
diff --git a/plugins/bundle/Cargo.toml b/plugins/bundle/Cargo.toml
new file mode 100644
index 0000000..13fb180
--- /dev/null
+++ b/plugins/bundle/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "dip_bundle"
+version.workspace = true
+edition.workspace = true
+
+[dependencies]
+anyhow.workspace = true
+bevy.workspace = true
+dip_core.workspace = true
+dirs.workspace = true
+cmd_lib.workspace = true
+convert_case.workspace = true
+pathdiff.workspace = true
+reqwest.workspace = true
+tempfile.workspace = true
+walkdir.workspace = true
+
+[features]
+# default = ["full"]
+full = ["dotfiles", "brew", "tailwind"]
+dotfiles = []
+brew = []
+tailwind = []
diff --git a/plugins/bundle/src/config.rs b/plugins/bundle/src/config.rs
new file mode 100644
index 0000000..1b364a9
--- /dev/null
+++ b/plugins/bundle/src/config.rs
@@ -0,0 +1,43 @@
+use bevy::app::{App, Plugin};
+use std::{fs, path::PathBuf};
+
+pub struct BundleConfigPlugin;
+
+impl Plugin for BundleConfigPlugin {
+ fn build(&self, app: &mut App) {
+ app.init_resource::();
+ }
+}
+
+pub struct BundleConfig {
+ app_path: PathBuf,
+}
+
+impl Default for BundleConfig {
+ fn default() -> Self {
+ Self {
+ app_path: dirs::home_dir().unwrap().join(".dip"),
+ }
+ }
+}
+
+impl BundleConfig {
+ pub fn app_path(&self) -> PathBuf {
+ Self::ensure_dir(&self.app_path);
+
+ self.app_path.clone()
+ }
+
+ pub fn install_path(&self) -> PathBuf {
+ let p = self.app_path().join("installs");
+ Self::ensure_dir(&p);
+
+ p
+ }
+
+ fn ensure_dir(p: &PathBuf) {
+ if !&p.is_dir() {
+ fs::create_dir_all(&p).unwrap();
+ }
+ }
+}
diff --git a/plugins/bundle/src/lib.rs b/plugins/bundle/src/lib.rs
new file mode 100644
index 0000000..7c652be
--- /dev/null
+++ b/plugins/bundle/src/lib.rs
@@ -0,0 +1,48 @@
+// mod config;
+mod schedule;
+mod tool;
+
+use bevy::{
+ app::{App, Plugin},
+ ecs::event::{EventReader, EventWriter},
+};
+// pub use config::BundleConfigPlugin;
+pub use schedule::{BundleSchedulePlugin, BundleStage};
+use std::path::PathBuf;
+use tool::{InstallTools, ToolPlugin};
+
+pub struct BundlePlugin;
+
+impl Plugin for BundlePlugin {
+ fn build(&self, app: &mut App) {
+ app.add_plugin(BundleSchedulePlugin)
+ .add_event::()
+ .add_event::()
+ // .add_plugin(BundleConfigPlugin)
+ .add_plugin(ToolPlugin)
+ .add_system_to_stage(BundleStage::First, apply_bundle);
+ }
+}
+
+// Events
+
+#[derive(Clone)]
+pub struct ApplyBundle {
+ pub path: PathBuf,
+}
+
+#[derive(Clone)]
+pub struct CleanBundle {
+ pub path: PathBuf,
+}
+
+fn apply_bundle(
+ mut events: EventReader,
+ mut install_tools: EventWriter,
+) {
+ events.iter().for_each(|e| {
+ install_tools.send(InstallTools {
+ path: e.path.clone(),
+ });
+ });
+}
diff --git a/plugins/bundle/src/schedule.rs b/plugins/bundle/src/schedule.rs
new file mode 100644
index 0000000..c92ee93
--- /dev/null
+++ b/plugins/bundle/src/schedule.rs
@@ -0,0 +1,46 @@
+use bevy::{
+ app::{App, Plugin},
+ ecs::schedule::{StageLabel, SystemStage},
+};
+use dip_core::schedule::DipStage;
+
+pub struct BundleSchedulePlugin;
+
+impl Plugin for BundleSchedulePlugin {
+ fn build(&self, app: &mut App) {
+ app.add_stage_after(
+ DipStage::Render,
+ BundleStage::First,
+ SystemStage::parallel(),
+ )
+ .add_stage_after(
+ BundleStage::First,
+ BundleStage::Clean,
+ SystemStage::parallel(),
+ )
+ .add_stage_after(
+ BundleStage::Clean,
+ BundleStage::Install,
+ SystemStage::parallel(),
+ )
+ .add_stage_after(
+ BundleStage::Install,
+ BundleStage::Apply,
+ SystemStage::parallel(),
+ )
+ .add_stage_after(
+ BundleStage::Apply,
+ BundleStage::Last,
+ SystemStage::parallel(),
+ );
+ }
+}
+
+#[derive(Debug, Hash, PartialEq, Eq, Clone, StageLabel)]
+pub enum BundleStage {
+ First,
+ Clean,
+ Install,
+ Apply,
+ Last,
+}
diff --git a/plugins/bundle/src/tool.rs b/plugins/bundle/src/tool.rs
new file mode 100644
index 0000000..fc460b4
--- /dev/null
+++ b/plugins/bundle/src/tool.rs
@@ -0,0 +1,35 @@
+mod dotfiles;
+mod homebrew;
+mod script;
+mod tailwind;
+mod unix;
+
+pub use self::unix::UnixToolPlugin;
+use bevy::{
+ app::{App, Plugin},
+ ecs::component::Component,
+};
+use std::path::PathBuf;
+
+pub struct ToolPlugin;
+
+impl Plugin for ToolPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::();
+
+ #[cfg(target_family = "unix")]
+ app.add_plugin(UnixToolPlugin);
+ }
+}
+
+// Events
+
+#[derive(Clone)]
+pub struct InstallTools {
+ pub path: PathBuf,
+}
+
+// Commponents
+
+#[derive(Component)]
+pub struct Tool;
diff --git a/plugins/bundle/src/tool/dotfiles.rs b/plugins/bundle/src/tool/dotfiles.rs
new file mode 100644
index 0000000..44d0989
--- /dev/null
+++ b/plugins/bundle/src/tool/dotfiles.rs
@@ -0,0 +1,171 @@
+use crate::{ApplyBundle, BundleStage, CleanBundle};
+use bevy::{
+ app::{App, Plugin},
+ ecs::event::EventReader,
+};
+use pathdiff::diff_paths;
+use std::{
+ fs::{self, DirEntry},
+ os,
+ path::PathBuf,
+};
+use walkdir::WalkDir;
+
+// Plugin
+
+pub struct DotfilesPlugin;
+
+impl Plugin for DotfilesPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_system_to_stage(BundleStage::Apply, apply)
+ .add_system_to_stage(BundleStage::Clean, clean);
+ }
+}
+
+// Systems
+
+fn apply(mut events: EventReader) {
+ events.iter().for_each(|e| {
+ let dotfiles = Dotfiles::from(e.clone());
+
+ if dotfiles.bundle_exists() {
+ println!("📌 Apply dotfiles");
+
+ dotfiles.symlinks().for_each(|sym| sym.apply());
+ } else {
+ println!("🟡 Skip: Apply dotfiles");
+ println!("bundle/dotfiles directory is empty",);
+ }
+
+ println!("✅ Apply dotfiles");
+ });
+}
+
+fn clean(mut events: EventReader) {
+ events.iter().for_each(|e| {
+ let dotfiles = Dotfiles::from(e.clone());
+
+ if dotfiles.bundle_exists() {
+ println!("📌 Clean dotfiles");
+
+ dotfiles.symlinks().for_each(|sym| sym.clean());
+ } else {
+ println!("🟡 Skip: Clean dotfiles");
+ println!("bundle/dotfiles directory is empty",);
+ }
+
+ println!("✅ Clean dotfiles");
+ });
+}
+
+struct Dotfiles {
+ pub path: PathBuf,
+}
+
+impl Dotfiles {
+ fn bundle_path(&self) -> PathBuf {
+ self.path.join("bundle/dotfiles")
+ }
+
+ fn bundle_exists(&self) -> bool {
+ self.bundle_path().is_dir()
+ }
+
+ fn symlinks(&self) -> std::boxed::Box + '_> {
+ Box::new(
+ self.packages()
+ .flat_map(|dir| WalkDir::new(&dir.path().into_iter()))
+ .filter_map(Result::ok)
+ .filter_map(|dir| {
+ let original = dir.path().to_path_buf().canonicalize().unwrap();
+ let diff = diff_paths(dir.path(), &self.bundle_path()).unwrap();
+ let dotfile_bundle_name = diff.iter().next().unwrap();
+ let stripped = diff.strip_prefix(dotfile_bundle_name).unwrap();
+ let link = dirs::home_dir().unwrap().join(stripped);
+
+ if dir.file_type().is_dir() {
+ fs::create_dir_all(link).unwrap();
+ None
+ } else {
+ Some(Symlink { original, link })
+ }
+ }),
+ )
+ }
+
+ fn packages(&self) -> std::boxed::Box + '_> {
+ let dir = fs::read_dir(&self.bundle_path())
+ .unwrap()
+ .filter_map(Result::ok);
+
+ Box::new(dir)
+ }
+}
+
+impl From for Dotfiles {
+ fn from(ApplyBundle { path }: ApplyBundle) -> Self {
+ Self { path }
+ }
+}
+
+impl From for Dotfiles {
+ fn from(CleanBundle { path }: CleanBundle) -> Self {
+ Self { path }
+ }
+}
+
+struct Symlink {
+ original: PathBuf,
+ link: PathBuf,
+}
+
+impl Symlink {
+ fn apply(&self) {
+ if self.link.is_symlink() {
+ // println!(
+ // "{}",
+ // &self.format("🟡 Skip: File is already symlinked")
+ // );
+ } else if self.link.is_file() {
+ // println!("{}", &self.format("🟡 Skip: File already exists"));
+ } else {
+ #[cfg(target_family = "unix")]
+ let res = os::unix::fs::symlink(&self.original, &self.link);
+
+ #[cfg(target_family = "windows")]
+ let res = os::windows::fs::symlink(&self.original, &self.link);
+
+ match res {
+ Ok(_) => {
+ println!("{}", &self.format("Symlink created"));
+ }
+ Err(e) => {
+ eprintln!("{}", &self.format(&e.to_string()));
+ }
+ }
+ }
+ }
+
+ fn clean(&self) {
+ if self.link.is_symlink() {
+ match fs::remove_file(&self.link) {
+ Ok(_) => {
+ println!("{}", &self.format("Symlink removed"));
+ }
+ Err(e) => {
+ eprintln!("{}", &self.format(&e.to_string()));
+ }
+ }
+ }
+ }
+
+ fn format<'a>(&self, message: &'a str) -> String {
+ format!(
+ "----------------------------------------------------------\n\
+ {message}\n\
+ original : {:?}\n\
+ link : {:?}",
+ &self.original, &self.link,
+ )
+ }
+}
diff --git a/plugins/bundle/src/tool/homebrew.rs b/plugins/bundle/src/tool/homebrew.rs
new file mode 100644
index 0000000..42c2464
--- /dev/null
+++ b/plugins/bundle/src/tool/homebrew.rs
@@ -0,0 +1,154 @@
+use crate::{tool::InstallTools, ApplyBundle, BundleStage};
+use bevy::{
+ app::{App, Plugin},
+ ecs::event::EventReader,
+};
+use cmd_lib::spawn_with_output;
+use std::{
+ fs::File,
+ io::{self, BufRead, BufReader, Write},
+ path::{Path, PathBuf},
+};
+use tempfile::tempdir;
+
+// Plugin
+
+pub struct HomebrewPlugin;
+
+impl Plugin for HomebrewPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_system_to_stage(BundleStage::Install, install)
+ .add_system_to_stage(BundleStage::Apply, apply);
+ }
+}
+
+// Systems
+
+fn install(mut events: EventReader) {
+ events.iter().for_each(|e| {
+ let brew = Homebrew::from(e.clone());
+
+ match &brew.brewfile_path() {
+ Ok(_brewfile_path) => {
+ if brew.installed() {
+ println!("🟡 Skip: Install Homebrew");
+ println!("brew is already installed");
+ } else {
+ println!("📌 Install Homebrew bundle");
+
+ if let Err(e) = brew.install() {
+ println!("Failed to run brew install.");
+ eprintln!("{e}");
+ } else {
+ println!("✅ Install Homebrew");
+ }
+ }
+ }
+ Err(_e) => {
+ println!("🟡 Skip: Install Homebrew");
+ println!("bundle/homebrew/Brewfile does not exists.",);
+ }
+ }
+ });
+}
+
+fn apply(mut events: EventReader) {
+ events.iter().for_each(|e| {
+ let brew = Homebrew::from(e.clone());
+
+ match &brew.brewfile_path() {
+ Ok(brewfile_path) => {
+ if brew.installed() {
+ println!("📌 Apply Homebrew bundle");
+
+ if let Err(e) = brew.apply(&brewfile_path) {
+ println!("Failed to run brew bundle.");
+ eprintln!("{e}");
+ } else {
+ println!("✅ Apply Homebrew bundle");
+ }
+ } else {
+ eprintln!("Could not find homebrew binary.");
+ }
+ }
+ Err(_e) => {
+ println!("🟡 Skip: Apply Homebrew bundle");
+ println!("bundle/homebrew/Brewfile does not exists.");
+ }
+ }
+ });
+}
+
+struct Homebrew {
+ pub path: PathBuf,
+}
+
+impl Homebrew {
+ fn homebrew_path() -> &'static str {
+ "/opt/homebrew/bin/brew"
+ }
+
+ fn bundle_path(&self) -> PathBuf {
+ self.path.join("bundle/homebrew")
+ }
+
+ fn brewfile_path(&self) -> io::Result {
+ self.bundle_path().join("Brewfile").canonicalize()
+ }
+
+ fn installed(&self) -> bool {
+ Path::new(Self::homebrew_path()).exists()
+ }
+
+ fn install(&self) -> anyhow::Result<()> {
+ let install_sh = reqwest::blocking::get(
+ "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh",
+ )?
+ .text()?;
+
+ let dir = tempdir()?;
+ let file_path = dir.path().join("brew-install.sh");
+ let file_path_str = file_path.display().to_string();
+ let mut file = File::create(file_path)?;
+ file.write_all(install_sh.as_bytes())
+ .expect("Unable to write file");
+
+ let mut install_brew = spawn_with_output!(NONINTERACTIVE=1 /bin/bash -C $file_path_str)?;
+
+ let result = install_brew.wait_with_pipe(&mut |pipe| {
+ BufReader::new(pipe)
+ .lines()
+ .filter_map(|line| line.ok())
+ .for_each(|f| println!("{f}"));
+ })?;
+
+ Ok(result)
+ }
+
+ fn apply(&self, brewfile_path: &PathBuf) -> anyhow::Result<()> {
+ let brewfile_path_str = &brewfile_path.display();
+
+ let mut brew_bundle = spawn_with_output!(brew bundle --cleanup --file $brewfile_path_str)?;
+
+ let result = brew_bundle.wait_with_pipe(&mut |pipe| {
+ BufReader::new(pipe)
+ .lines()
+ .filter_map(|line| line.ok())
+ .for_each(|line| println!("{:?}", line));
+ })?;
+
+ Ok(result)
+ }
+}
+
+impl From for Homebrew {
+ fn from(InstallTools { path }: InstallTools) -> Self {
+ Self { path }
+ }
+}
+
+impl From for Homebrew {
+ fn from(ApplyBundle { path }: ApplyBundle) -> Self {
+ Self { path }
+ }
+}
diff --git a/plugins/bundle/src/tool/script.rs b/plugins/bundle/src/tool/script.rs
new file mode 100644
index 0000000..4fabe02
--- /dev/null
+++ b/plugins/bundle/src/tool/script.rs
@@ -0,0 +1,117 @@
+use bevy::{
+ app::{App, Plugin},
+ ecs::event::EventReader,
+};
+use cmd_lib::spawn_with_output;
+use convert_case::{Case, Casing};
+use std::{
+ io::{self, BufRead, BufReader},
+ path::PathBuf,
+};
+
+use crate::{ApplyBundle, BundleStage};
+
+pub struct ScriptPlugin;
+
+impl Plugin for ScriptPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_system_to_stage(BundleStage::First, pre_script)
+ .add_system_to_stage(BundleStage::Last, post_script);
+ }
+}
+
+fn pre_script(mut events: EventReader) {
+ events.iter().for_each(|e| Script::pre(e.clone()).run());
+}
+
+fn post_script(mut events: EventReader) {
+ events.iter().for_each(|e| Script::post(e.clone()).run());
+}
+
+struct Script {
+ event: ApplyBundle,
+ schedule: ScriptSchedule,
+}
+
+impl Script {
+ fn pre(event: ApplyBundle) -> Self {
+ Self {
+ event,
+ schedule: ScriptSchedule::Pre,
+ }
+ }
+
+ fn post(event: ApplyBundle) -> Self {
+ Self {
+ event,
+ schedule: ScriptSchedule::Post,
+ }
+ }
+
+ fn run(&self) {
+ match self.find_file() {
+ Ok(file_path) => {
+ println!("📌 {} script", self.schedule.to_upper_camel());
+
+ let file_path_str = file_path.display();
+ let mut script = spawn_with_output!(/bin/bash -C $file_path_str).unwrap();
+
+ let result = script.wait_with_pipe(&mut |pipe| {
+ BufReader::new(pipe)
+ .lines()
+ .filter_map(|line| line.ok())
+ .for_each(|f| println!("{f}"));
+ });
+
+ if let Err(e) = result {
+ println!("Failed to run {} script.", self.schedule.to_string());
+ eprintln!("{e}");
+ } else {
+ println!("✅ {} script", self.schedule.to_upper_camel());
+ }
+ }
+ Err(_e) => {
+ self.skip();
+ }
+ }
+ }
+
+ fn skip(&self) {
+ println!("🟡 Skip: {} script", &self.schedule.to_upper_camel());
+ println!("{} does not exists.", &self.file_path().display());
+ }
+
+ fn find_file(&self) -> io::Result {
+ self.file_path().canonicalize()
+ }
+
+ fn file_path(&self) -> PathBuf {
+ self.event
+ .path
+ .join(format!("bundle/scripts/{}", &self.file_name()))
+ }
+
+ fn file_name(&self) -> String {
+ format!("{}.sh", self.schedule.to_string())
+ }
+}
+
+enum ScriptSchedule {
+ Pre,
+ Post,
+}
+
+impl ScriptSchedule {
+ fn to_upper_camel(&self) -> String {
+ self.to_string().to_case(Case::UpperCamel)
+ }
+}
+
+impl ToString for ScriptSchedule {
+ fn to_string(&self) -> String {
+ match self {
+ ScriptSchedule::Pre => "pre".into(),
+ ScriptSchedule::Post => "post".into(),
+ }
+ }
+}
diff --git a/plugins/bundle/src/tool/tailwind.rs b/plugins/bundle/src/tool/tailwind.rs
new file mode 100644
index 0000000..3206487
--- /dev/null
+++ b/plugins/bundle/src/tool/tailwind.rs
@@ -0,0 +1,27 @@
+use crate::tool::InstallTools;
+use bevy::{
+ app::{App, Plugin},
+ ecs::event::{EventReader, EventWriter},
+ log,
+};
+
+// Plugin
+pub struct TailwindPlugin;
+
+impl Plugin for TailwindPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_event::().add_system(install);
+ }
+}
+
+fn install(mut events: EventReader, mut installed: EventWriter) {
+ for _e in events.iter() {
+ log::warn!("TODO: Install Tool");
+
+ installed.send(TailwindInstalled);
+ }
+}
+
+// Events
+
+pub struct TailwindInstalled;
diff --git a/plugins/bundle/src/tool/unix.rs b/plugins/bundle/src/tool/unix.rs
new file mode 100644
index 0000000..c42b110
--- /dev/null
+++ b/plugins/bundle/src/tool/unix.rs
@@ -0,0 +1,22 @@
+pub use crate::tool::{
+ dotfiles::DotfilesPlugin, homebrew::HomebrewPlugin, script::ScriptPlugin,
+ tailwind::TailwindPlugin,
+};
+use bevy::app::{App, Plugin};
+
+pub struct UnixToolPlugin;
+
+impl Plugin for UnixToolPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_plugin(ScriptPlugin);
+
+ #[cfg(feature = "dotfiles")]
+ app.add_plugin(DotfilesPlugin);
+
+ #[cfg(feature = "brew")]
+ app.add_plugin(HomebrewPlugin);
+
+ #[cfg(feature = "tailwind")]
+ app.add_plugin(TailwindPlugin);
+ }
+}
diff --git a/plugins/core/src/schedule.rs b/plugins/core/src/schedule.rs
index 863b137..7f9e223 100644
--- a/plugins/core/src/schedule.rs
+++ b/plugins/core/src/schedule.rs
@@ -44,6 +44,6 @@ impl Plugin for UiSchedulePlugin {
SystemStage::parallel(),
)
.add_stage_after(DipStage::Prepare, DipStage::Apply, SystemStage::parallel())
- .add_stage_after(DipStage::Prepare, DipStage::Render, SystemStage::parallel());
+ .add_stage_after(DipStage::Apply, DipStage::Render, SystemStage::parallel());
}
}
diff --git a/plugins/macro/Cargo.toml b/plugins/macro/Cargo.toml
index c02ae57..5a60532 100644
--- a/plugins/macro/Cargo.toml
+++ b/plugins/macro/Cargo.toml
@@ -13,7 +13,7 @@ keywords.workspace = true
proc-macro = true
[dependencies]
-convert_case = "0.5"
+convert_case.workspace = true
dirs.workspace = true
quote = "1.0"
proc-macro2 = "1.0"
diff --git a/plugins/macro/src/cli.rs b/plugins/macro/src/cli.rs
index 13b8097..15714d4 100644
--- a/plugins/macro/src/cli.rs
+++ b/plugins/macro/src/cli.rs
@@ -40,6 +40,7 @@ impl CliParser {
&format!("handle_{}", subcommand_name),
)
.unwrap();
+
token.add_subcommand_handler = quote! {
.add_startup_system_to_stage(
::dip::core::schedule::DipStartupStage::Action,
diff --git a/plugins/macro/src/subcommand.rs b/plugins/macro/src/subcommand.rs
index bba96ca..c23ae3d 100644
--- a/plugins/macro/src/subcommand.rs
+++ b/plugins/macro/src/subcommand.rs
@@ -43,7 +43,30 @@ impl SubcommandParser {
}
fn add_system(&self) -> TokenStream2 {
- let mut subsubcommand_handler_names = vec![];
+ let handler_with_system_order = self.handler_with_system_order();
+
+ let gen = quote! {
+ .add_startup_system_to_stage(::dip::core::schedule::DipStartupStage::Action, #handler_with_system_order);
+ };
+
+ gen
+ }
+
+ fn handler_with_system_order(&self) -> TokenStream2 {
+ let gen_str = self
+ .child_names()
+ .iter()
+ .map(|name| format!("handle_{}", name.to_case(Case::Snake)))
+ .fold(self.handler_name().to_string(), |acc, name| {
+ format!("{acc}.before({})", name)
+ });
+ let gen = TokenStream2::from_str(&gen_str).unwrap();
+
+ gen
+ }
+
+ fn child_names(&self) -> Vec {
+ let mut child_names = vec![];
for v in self.commands_enum.variants.iter() {
for a in v.attrs.iter() {
for t in a.tokens.clone().into_iter() {
@@ -54,15 +77,13 @@ impl SubcommandParser {
TokenTree::Ident(ident) => {
if ident.to_string() == "subcommand" {
if let syn::Fields::Unnamed(f) = &v.fields {
- let subsubcommand_ty = &f.unnamed[0].ty;
- let subsubcommand_ty_quote =
- quote! { #subsubcommand_ty };
- let subsubcommand_name = &subsubcommand_ty_quote
+ let child_ty = &f.unnamed[0].ty;
+ let child_ty_quote = quote! { #child_ty };
+ let child_name = child_ty_quote
.to_string()
- .to_case(Case::Snake);
+ .to_case(Case::UpperCamel);
- subsubcommand_handler_names
- .push(format!("handle_{}", subsubcommand_name));
+ child_names.push(child_name);
}
}
}
@@ -76,16 +97,7 @@ impl SubcommandParser {
}
}
- let mut handler = self.handler_name().to_string();
- for n in subsubcommand_handler_names {
- handler = format!("{}.before({})", handler, n);
- }
-
- let handler_token = TokenStream2::from_str(&handler).unwrap();
-
- quote! {
- .add_startup_system_to_stage(::dip::core::schedule::DipStartupStage::Action, #handler_token);
- }
+ child_names
}
fn subcommand_ty_name(&self) -> TokenStream2 {
diff --git a/src/cli.rs b/src/cli.rs
new file mode 100644
index 0000000..201a382
--- /dev/null
+++ b/src/cli.rs
@@ -0,0 +1,46 @@
+mod action;
+
+use crate::cli::action::{
+ ActionPlugin, ApplyBundleAction, BundleActionPlugin, CleanBundleAction, CliPlugin,
+};
+use dip::{
+ bevy::{
+ app::{App, Plugin},
+ ecs::event::{EventReader, EventWriter},
+ },
+ bundle::{ApplyBundle, BundlePlugin, CleanBundle},
+ core::task::NoAsyncAction,
+};
+use std::path::PathBuf;
+
+pub struct DipCliPlugin;
+
+impl Plugin for DipCliPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_plugin(CliPlugin::::oneshot())
+ .add_plugin(ActionPlugin)
+ .add_plugin(BundleActionPlugin)
+ .add_plugin(BundlePlugin)
+ .add_system(install_bundle)
+ .add_system(clean_bundle);
+ }
+}
+
+fn install_bundle(
+ mut actions: EventReader,
+ mut apply: EventWriter,
+) {
+ actions.iter().for_each(|a| {
+ apply.send(ApplyBundle {
+ path: PathBuf::from(&a.path),
+ });
+ });
+}
+
+fn clean_bundle(mut actions: EventReader, mut clean: EventWriter) {
+ actions.iter().for_each(|a| {
+ clean.send(CleanBundle {
+ path: PathBuf::from(&a.path),
+ })
+ });
+}
diff --git a/src/plugin/cli.rs b/src/cli/action.rs
similarity index 65%
rename from src/plugin/cli.rs
rename to src/cli/action.rs
index 9489719..2cb1922 100644
--- a/src/plugin/cli.rs
+++ b/src/cli/action.rs
@@ -1,3 +1,7 @@
+// mod build;
+
+// pub use build::*;
+
use dip::cli::{CliPlugin, SubcommandPlugin};
#[derive(CliPlugin, clap::Parser)]
@@ -12,7 +16,7 @@ pub enum Action {
Build(BuildArgs),
#[clap(subcommand)]
- Tool(ToolAction),
+ Bundle(BundleAction),
}
#[derive(clap::Args, Clone, Debug)]
@@ -34,7 +38,19 @@ pub struct BuildArgs {
}
#[derive(SubcommandPlugin, clap::Subcommand, Clone, Debug)]
-pub enum ToolAction {
- List,
- Add { name: String },
+pub enum BundleAction {
+ Apply(ApplyBundleArgs),
+ Clean(CleanBundleArgs),
+}
+
+#[derive(clap::Args, Clone, Debug)]
+pub struct ApplyBundleArgs {
+ #[clap(short, long, default_value_t = String::from("."))]
+ pub path: String,
+}
+
+#[derive(clap::Args, Clone, Debug)]
+pub struct CleanBundleArgs {
+ #[clap(short, long, default_value_t = String::from("."))]
+ pub path: String,
}
diff --git a/src/plugin/handler.rs b/src/cli/action/build.rs
similarity index 89%
rename from src/plugin/handler.rs
rename to src/cli/action/build.rs
index b9e45bc..ee7adfb 100644
--- a/src/plugin/handler.rs
+++ b/src/cli/action/build.rs
@@ -1,25 +1,21 @@
use crate::{
- plugin::{AsyncAction, BuildAction},
+ cli::{action::BuildAction, async_action::AsyncAction},
resource::tool::Tool,
};
use dip::{
bevy::{
- app::AppExit,
- ecs::{
- event::{EventReader, EventWriter},
- schedule::ParallelSystemDescriptorCoercion,
- system::Res,
- },
+ app::{App, AppExit, Plugin},
+ ecs::prelude::*,
log,
},
- prelude::{AsyncActionPool, Plugin},
+ core::task::AsyncActionPool,
};
use std::{fs, path::PathBuf, process::Command};
-pub struct HandlerPlugin;
+pub struct BuildActionPlugin;
-impl Plugin for HandlerPlugin {
- fn build(&self, app: &mut dip::prelude::App) {
+impl Plugin for BuildActionPlugin {
+ fn build(&self, app: &mut App) {
app.add_event::()
.add_event::()
.add_system(handle_build)
@@ -27,7 +23,6 @@ impl Plugin for HandlerPlugin {
.add_system(build_app.after(handle_build).after(compile_css));
}
}
-
fn handle_build(
mut actions: EventReader,
mut build_app: EventWriter,
@@ -56,6 +51,38 @@ fn handle_build(
}
}
+fn build_app(mut events: EventReader, mut app_exit: EventWriter) {
+ for BuildApp { action } in events.iter() {
+ let mut cmd = Command::new("cargo");
+
+ cmd.current_dir(fs::canonicalize(&action.path).unwrap())
+ .args(["build"]);
+
+ let output = cmd.output().expect("Could not execute cargo build");
+ log::trace!("{output:?}");
+
+ if output.status.success() {
+ println!("Build finished");
+ } else {
+ println!("Failed to build project");
+ println!("{}", String::from_utf8(output.stderr).unwrap());
+ }
+
+ app_exit.send(AppExit);
+ }
+}
+
+#[derive(Clone, Debug)]
+struct BuildApp {
+ pub action: BuildAction,
+}
+
+#[derive(Clone, Debug)]
+struct CompileCss {
+ pub action: BuildAction,
+ pub tool: Tool,
+}
+
fn compile_css(
mut events: EventReader,
mut build_app: EventWriter,
@@ -97,35 +124,3 @@ fn compile_css(
}
}
}
-
-fn build_app(mut events: EventReader, mut app_exit: EventWriter) {
- for BuildApp { action } in events.iter() {
- let mut cmd = Command::new("cargo");
-
- cmd.current_dir(fs::canonicalize(&action.path).unwrap())
- .args(["build"]);
-
- let output = cmd.output().expect("Could not execute cargo build");
- log::trace!("{output:?}");
-
- if output.status.success() {
- println!("Build finished");
- } else {
- println!("Failed to build project");
- println!("{}", String::from_utf8(output.stderr).unwrap());
- }
-
- app_exit.send(AppExit);
- }
-}
-
-#[derive(Clone, Debug)]
-struct BuildApp {
- pub action: BuildAction,
-}
-
-#[derive(Clone, Debug)]
-struct CompileCss {
- pub action: BuildAction,
- pub tool: Tool,
-}
diff --git a/src/lib.rs b/src/lib.rs
index c37415a..1b6d5b3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -9,6 +9,8 @@ pub use dip_cli as cli;
#[cfg(feature = "desktop")]
pub use dip_desktop as desktop;
+pub use dip_bundle as bundle;
+
pub use bevy;
#[cfg(feature = "desktop")]
diff --git a/src/main.rs b/src/main.rs
index 2be3737..ca12c8d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,26 +1,10 @@
-mod plugin;
-mod resource;
+mod cli;
-use crate::plugin::DipCliPlugin;
-use dip::bevy::{
- app::App,
- log::{LogPlugin, LogSettings},
-};
+use crate::cli::DipCliPlugin;
+use dip::bevy::app::App;
fn main() {
let mut app = App::new();
- #[cfg(debug_assertions)]
- app.insert_resource(LogSettings {
- filter: "info,dip=debug".into(),
- level: bevy::log::Level::DEBUG,
- });
-
- #[cfg(not(debug_assertions))]
- app.insert_resource(LogSettings {
- filter: "warn".into(),
- level: bevy::log::Level::WARN,
- });
-
- app.add_plugin(DipCliPlugin).add_plugin(LogPlugin).run();
+ app.add_plugin(DipCliPlugin).run();
}
diff --git a/src/plugin.rs b/src/plugin.rs
deleted file mode 100644
index f77f849..0000000
--- a/src/plugin.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-mod async_action;
-mod cli;
-mod handler;
-mod tool;
-
-use crate::plugin::{async_action::*, cli::*, handler::*, tool::*};
-use dip::bevy::app::{App, Plugin};
-
-pub struct DipCliPlugin;
-
-impl Plugin for DipCliPlugin {
- fn build(&self, app: &mut App) {
- app.add_plugin(CliPlugin::::application())
- .add_plugin(ActionPlugin)
- .add_plugin(AsyncActionPlugin)
- .add_plugin(ToolPlugin)
- .add_plugin(HandlerPlugin);
- }
-}
diff --git a/src/plugin/async_action.rs b/src/plugin/async_action.rs
deleted file mode 100644
index 70ab215..0000000
--- a/src/plugin/async_action.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-use crate::{
- plugin::BuildAction,
- resource::tool::{Tool, ToolResult},
-};
-use dip::{bevy::ecs::event::EventReader, core::task::async_action};
-
-#[async_action]
-impl AsyncActionCreator {
- async fn install(tool: Tool) -> ToolResult {
- tool.install().await?;
-
- Ok(Install)
- }
-
- async fn install_and_build(tool: &Tool, action: BuildAction) -> BuildAction {
- tool.install().await.unwrap();
-
- action
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Install;
diff --git a/src/plugin/tool.rs b/src/plugin/tool.rs
deleted file mode 100644
index c961806..0000000
--- a/src/plugin/tool.rs
+++ /dev/null
@@ -1,60 +0,0 @@
-use crate::{
- plugin::{async_action::*, cli::*},
- resource::tool::{Tool, ToolResult},
-};
-use dip::{
- bevy::{
- app::{App, AppExit, Plugin},
- ecs::{
- event::{EventReader, EventWriter},
- system::Res,
- },
- },
- core::task::AsyncActionPool,
-};
-
-pub struct ToolPlugin;
-
-impl Plugin for ToolPlugin {
- fn build(&self, app: &mut App) {
- app.add_plugin(ToolActionPlugin)
- .add_system(list_tool)
- .add_system(add_tool)
- .add_system(install_result);
- }
-}
-
-fn list_tool(mut events: EventReader, mut app_exit: EventWriter) {
- for _ in events.iter() {
- for t in Tool::list().iter() {
- println!("- {t}");
- }
- app_exit.send(AppExit);
- }
-}
-
-fn add_tool(
- mut events: EventReader,
- async_action: Res>,
-) {
- for e in events.iter() {
- let name = e.name.as_str();
- let tool = Tool::from_str(name).expect(&format!("Could not find tool: {name}"));
-
- match tool {
- Tool::Tailwind => async_action.send(AsyncAction::install(tool)),
- }
- }
-}
-
-fn install_result(
- mut events: EventReader>,
- mut app_exit: EventWriter,
-) {
- for e in events.iter() {
- if let Err(e) = e {
- println!("{:?}", e.error);
- }
- app_exit.send(AppExit);
- }
-}