Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,17 @@ cargo install --git https://github.com/LeadcodeDev/rustmotion
```

Either command installs the `rustmotion` CLI. Add the `studio` feature to get
the live preview window (`rustmotion-studio`) alongside it:
the live preview window alongside it:

```bash
cargo install --git https://github.com/LeadcodeDev/rustmotion --features studio
```

That build answers to `rustmotion studio -f scenario.json`, and also installs
the same window as a standalone `rustmotion-studio` binary. A CLI built without
the feature hides `studio` from `--help` and, when asked for it anyway, prints
the `cargo install` line above.

The studio is not in the default build because it pulls gpui and a native GUI
toolchain, which do not build everywhere the CLI builds — a headless server
being the obvious case.
Expand Down Expand Up @@ -2190,7 +2195,7 @@ crates/
│ └── *.rs # one file per component (Painter implementation)
└── rustmotion/src/
├── cli/ # the `rustmotion` binary (clap subcommands)
├── studio/ # the `rustmotion-studio` binary (feature `studio`)
├── studio/ # `rustmotion studio` and the `rustmotion-studio` binary (feature `studio`)
├── encode/ # video/audio encoders and muxing
└── loader.rs # JSON/HTML → ResolvedScenario
```
Expand Down
31 changes: 17 additions & 14 deletions crates/rustmotion/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,26 +262,29 @@ crates/
└── loader.rs # Chargement JSON/HTML → ResolvedScenario
```

> Les deux binaires vivent dans la crate publiée `rustmotion` : une crate qui
> n'a qu'une `[lib]` n'installe rien d'exécutable via `cargo install`, et
> `cargo install --git <url>` **refuse** un dépôt où plus d'un paquet déclare un
> `[[bin]]` (« multiple packages with binaries found » — ni `default-members` ni
> `required-features` ne changent ce décompte). C'est pour ça que le studio est
> un module de cette crate et non un paquet à part : en paquet, il dépendait de
> `loader`/`encode`, donc en faire une dépendance de `rustmotion` était un cycle,
> que cargo refuse même optionnel.
> Both binaries live in the published `rustmotion` crate: a crate with only a
> `[lib]` installs nothing executable through `cargo install`, and `cargo install
> --git <url>` **refuses** a repository where more than one package declares a
> `[[bin]]` ("multiple packages with binaries found" — neither `default-members`
> nor `required-features` changes that count). That is why the studio is a module
> of this crate rather than a package of its own: as a package it depended on
> `loader`/`encode`, so making it a dependency of `rustmotion` was a cycle, which
> cargo refuses even when optional.
>
> ```bash
> cargo install --git https://github.com/LeadcodeDev/rustmotion # CLI
> cargo install --git https://github.com/LeadcodeDev/rustmotion --features studio # CLI + studio
> ```
>
> `studio` est hors du build par défaut : il tire gpui et une toolchain GUI, qui
> ne compilent pas partout où le CLI compile. Il n'y a pas de **sous-commande**
> `studio` non plus — elle devrait disparaître du `--help` selon le feature.
> `--workspace` seul ne compile plus le studio : CI passe
> `--features rustmotion/studio` à clippy et aux tests, sans quoi 11 600 lignes
> cessent d'être vérifiées en restant vertes.
> `studio` is out of the default build: it pulls gpui and a GUI toolchain, which
> do not compile everywhere the CLI does. The `rustmotion studio` subcommand
> opens the same studio as the `rustmotion-studio` binary, with the same `-f` /
> `-d`. Without the feature the subcommand still exists but stays out of
> `--help`, and running it answers with the `cargo install` line that grants it
> instead of "unrecognized subcommand".
> `--workspace` alone no longer compiles the studio: CI passes
> `--features rustmotion/studio` to clippy and to the tests, without which 11,600
> lines stop being checked while staying green.

### Aucun commentaire, nulle part

Expand Down
24 changes: 24 additions & 0 deletions crates/rustmotion/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,13 +401,31 @@ enum Commands {
action: SkillsAction,
},

/// Open the live preview studio on a scenario
#[cfg_attr(not(feature = "studio"), command(hide = true))]
Studio {
/// Path to a JSON scenario to open directly in the editor
#[arg(short, long)]
file: Option<PathBuf>,

/// Workspace directory to scan for scenarios (default: current directory)
#[arg(short, long)]
dir: Option<PathBuf>,
},

/// Generate or install shell completions
Completions {
#[command(subcommand)]
action: CompletionsAction,
},
}

#[cfg(not(feature = "studio"))]
const STUDIO_NOT_BUILT: &str = "\
this build of rustmotion has no studio: it is behind the `studio` feature, which is \
off by default because it pulls gpui and a native GUI toolchain. Reinstall with it on:
cargo install --git https://github.com/LeadcodeDev/rustmotion --features studio";

const CAPTIONS_EXAMPLES: &str = "\
Examples:
# Transcribe audio into word-level timings (requires whisper.cpp)
Expand Down Expand Up @@ -963,6 +981,12 @@ pub fn run() -> Result<()> {
}
SkillsAction::Show { name } => skills::show(&name),
},
#[cfg(feature = "studio")]
Commands::Studio { file, dir } => rustmotion::studio::run_with(file, dir),

#[cfg(not(feature = "studio"))]
Commands::Studio { .. } => Err(RustmotionError::Generic(STUDIO_NOT_BUILT.to_string())),

Commands::Completions { action } => match action {
CompletionsAction::Generate { shell } => {
clap_complete::generate(
Expand Down
17 changes: 8 additions & 9 deletions crates/rustmotion/src/studio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub use app::{run_preview, run_preview_with_error};

use std::path::PathBuf;

use clap::{CommandFactory, Parser};
use clap::Parser;
use rustmotion::error::Result;
use rustmotion::loader::load_scenario;

Expand All @@ -29,16 +29,15 @@ pub struct Cli {
dir: Option<PathBuf>,
}

pub fn command() -> clap::Command {
Cli::command()
}

pub fn run() -> Result<()> {
let cli = Cli::parse();
let workspace = cli
.dir
.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
match cli.file {
run_with(cli.file, cli.dir)
}

pub fn run_with(file: Option<PathBuf>, dir: Option<PathBuf>) -> Result<()> {
let workspace =
dir.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
match file {
Some(f) => match load_scenario(&f) {
Ok(scenario) => app::run_preview_root(scenario, None, Some(f), workspace, true, true),
Err(e) => app::run_preview_root(
Expand Down
68 changes: 68 additions & 0 deletions crates/rustmotion/tests/studio_subcommand.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::process::Command;

fn rustmotion() -> Command {
Command::new(env!("CARGO_BIN_EXE_rustmotion"))
}

fn top_level_help() -> String {
let out = rustmotion().arg("--help").output().expect("run --help");
assert!(out.status.success(), "`rustmotion --help` failed");
String::from_utf8_lossy(&out.stdout).into_owned()
}

fn lists_studio(help: &str) -> bool {
help.lines()
.any(|line| line.trim_start().starts_with("studio "))
}

#[cfg(feature = "studio")]
#[test]
fn studio_is_listed_when_the_feature_is_on() {
let help = top_level_help();
assert!(
lists_studio(&help),
"`studio` is missing from the subcommand list:\n{help}"
);
}

#[cfg(feature = "studio")]
#[test]
fn studio_takes_the_same_flags_as_the_standalone_binary() {
let out = rustmotion()
.args(["studio", "--help"])
.output()
.expect("run studio --help");
assert!(out.status.success(), "`rustmotion studio --help` failed");
let help = String::from_utf8_lossy(&out.stdout);
assert!(help.contains("--file"), "--file missing:\n{help}");
assert!(help.contains("--dir"), "--dir missing:\n{help}");
}

#[cfg(not(feature = "studio"))]
#[test]
fn studio_stays_out_of_help_when_the_feature_is_off() {
let help = top_level_help();
assert!(
!lists_studio(&help),
"`studio` should not be advertised by a build that has no studio:\n{help}"
);
}

#[cfg(not(feature = "studio"))]
#[test]
fn studio_names_the_feature_when_the_feature_is_off() {
let out = rustmotion().arg("studio").output().expect("run studio");
assert!(
!out.status.success(),
"`rustmotion studio` should fail in a build with no studio"
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("--features studio"),
"the error should say how to get the studio:\n{err}"
);
assert!(
!err.contains("unrecognized subcommand"),
"the subcommand should exist and explain itself:\n{err}"
);
}
Loading