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

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions docs/netsuke-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -2043,10 +2043,14 @@ Real-time stage reporting now uses a six-stage model in `src/status.rs` backed
by `indicatif::MultiProgress` for standard terminals. The reporter keeps one
persistent summary line per stage and updates each line through localized state
labels (`pending`, `in progress`, `done`, `failed`) plus localized stage text.
When stderr is not a TTY, the same reporter falls back to emitting localized
summary lines so non-interactive runs still surface deterministic stage state.
Accessible output remains text-first and static; it does not animate. The
standard reporter is configurable through OrthoConfig layering via
During Stage 6, Netsuke parses Ninja status lines of the form
`[current/total] ...` and emits localized task progress updates. Parsed updates
are monotonic: malformed lines, regressive counts, and total-mismatch lines are
ignored to avoid noisy or inconsistent progress state. Task updates fall back
to textual output when stdout is not a teletype terminal (TTY), ensuring
deterministic continuous integration (CI) logs; accessible mode always uses
textual output. Accessible output remains text-first and static; it does not
animate. The standard reporter is configurable through OrthoConfig layering via
`progress: Option<bool>` (`--progress`, `NETSUKE_PROGRESS`, or config file),
with accessible mode taking precedence when enabled.

Expand Down
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ library, and CLI ergonomics.
- [x] 3.9.1. Integrate `indicatif::MultiProgress`.
- [x] Surface the six pipeline stages with persistent summaries.
- [x] Apply localization-aware labelling.
- [ ] 3.9.2. Parse Ninja status lines to drive task progress.
- [ ] Emit fallback textual updates when stdout is not a TTY or accessible
- [x] 3.9.2. Parse Ninja status lines to drive task progress.
- [x] Emit fallback textual updates when stdout is not a TTY or accessible
mode is active.
- [ ] 3.9.3. Capture per-stage timing metrics in verbose mode.
- [ ] Include metrics in completion summary.
Expand Down
34 changes: 24 additions & 10 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,8 @@ For information on contributing translations, see the

### Accessible output mode

Netsuke supports an accessible output mode that replaces animated progress
indicators with static, labelled status lines suitable for screen readers and
dumb terminals.
Netsuke supports an accessible output mode that uses static, labelled status
lines suitable for screen readers and dumb terminals.

Accessible mode is auto-enabled when:

Expand All @@ -592,16 +591,31 @@ When accessible mode is active, each pipeline stage produces a labelled status
line on stderr:

```text
Stage 1/5: Configuring network policy
Stage 2/5: Loading manifest
Stage 3/5: Building dependency graph
Stage 4/5: Generating Ninja file
Stage 5/5: Executing Build
Stage 1/6: Reading manifest file
Stage 2/6: Parsing YAML document
Stage 3/6: Expanding template directives
Stage 4/6: Deserializing and rendering manifest values
Stage 5/6: Building and validating dependency graph
Stage 6/6: Synthesizing Ninja plan and executing Build
Task 1/2: cc -c src/a.c
Task 2/2: cc -c src/b.c
Build complete.
```

In standard mode, no status lines are emitted. Future versions may add animated
progress indicators for standard mode terminals.
In standard mode, Netsuke uses `indicatif` stage summaries when progress is
enabled. During Stage 6, Netsuke parses Ninja status lines (`[current/total]`)
and emits task progress updates. When stdout is not a teletype terminal (TTY),
task progress automatically falls back to textual updates, so continuous
integration (CI) and redirected logs remain readable.

Progress output can be controlled via OrthoConfig layering:

- CLI flag: `--progress true` or `--progress false`
- Environment variable: `NETSUKE_PROGRESS=true|false`
- Configuration file: `progress = true|false`

When progress is disabled, Netsuke suppresses stage and task progress output in
both standard and accessible modes.

### Emoji and accessibility preferences

Expand Down
5 changes: 4 additions & 1 deletion locales/en-US/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ cli.flag.fetch_allow_host.help = Hostnames that are permitted when default deny
cli.flag.fetch_block_host.help = Hostnames that are always blocked, even when allowed elsewhere.
cli.flag.fetch_default_deny.help = Deny all hosts by default; only allow the declared allowlist.
cli.flag.accessible.help = Force accessible output mode on or off.
cli.flag.progress.help = Force standard progress summaries on or off.
cli.flag.progress.help = Force standard stage and task progress summaries on or off.

# Subcommand descriptions.
cli.subcommand.build.about = Build targets defined in the manifest (default).
Expand Down Expand Up @@ -325,6 +325,9 @@ status.state.done = done
status.state.failed = failed
status.stage.label = Stage { $current }/{ $total }: { $description }
status.stage.summary = [{ $state }] { $label }
status.stage.summary_with_task = [{ $state }] { $label } ({ $task_progress })
status.task.progress_label = Task { $current }/{ $total }
status.task.progress_update = { $task }: { $description }
status.stage.manifest_ingestion = Reading manifest file
status.stage.initial_yaml_parsing = Parsing YAML document
status.stage.template_expansion = Expanding template directives
Expand Down
5 changes: 4 additions & 1 deletion locales/es-ES/messages.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ cli.flag.fetch_allow_host.help = Nombres de host permitidos cuando la denegació
cli.flag.fetch_block_host.help = Nombres de host siempre bloqueados, incluso cuando están permitidos.
cli.flag.fetch_default_deny.help = Denegar todos los hosts por defecto; solo permitir la lista de permitidos.
cli.flag.accessible.help = Forzar el modo de salida accesible (activado o desactivado).
cli.flag.progress.help = Forzar los resúmenes de progreso estándar (activados o desactivados).
cli.flag.progress.help = Forzar los resúmenes de progreso estándar de etapas y tareas (activados o desactivados).

# Descripciones de subcomandos.
cli.subcommand.build.about = Compila objetivos definidos en el manifiesto (predeterminado).
Expand Down Expand Up @@ -325,6 +325,9 @@ status.state.done = completada
status.state.failed = fallida
status.stage.label = Etapa { $current }/{ $total }: { $description }
status.stage.summary = [{ $state }] { $label }
status.stage.summary_with_task = [{ $state }] { $label } ({ $task_progress })
status.task.progress_label = Tarea { $current }/{ $total }
status.task.progress_update = { $task }: { $description }
status.stage.manifest_ingestion = Leyendo el archivo del manifiesto
status.stage.initial_yaml_parsing = Analizando el documento YAML
status.stage.template_expansion = Expandiendo directivas de plantilla
Expand Down
3 changes: 3 additions & 0 deletions src/localization/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,9 @@ define_keys! {
STATUS_STATE_FAILED => "status.state.failed",
STATUS_STAGE_LABEL => "status.stage.label",
STATUS_STAGE_SUMMARY => "status.stage.summary",
STATUS_STAGE_SUMMARY_WITH_TASK => "status.stage.summary_with_task",
STATUS_TASK_PROGRESS_LABEL => "status.task.progress_label",
STATUS_TASK_PROGRESS_UPDATE => "status.task.progress_update",
STATUS_STAGE_MANIFEST_INGESTION => "status.stage.manifest_ingestion",
STATUS_STAGE_INITIAL_YAML_PARSING => "status.stage.initial_yaml_parsing",
STATUS_STAGE_TEMPLATE_EXPANSION => "status.stage.template_expansion",
Expand Down
135 changes: 96 additions & 39 deletions src/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{ir::BuildGraph, manifest, ninja_gen};
use anyhow::{Context, Result};
use camino::Utf8PathBuf;
use std::borrow::Cow;
use std::io::IsTerminal;
use std::path::Path;
use tempfile::NamedTempFile;
use tracing::{debug, info};
Expand Down Expand Up @@ -97,12 +98,22 @@ fn make_reporter(
mode: OutputMode,
progress_enabled: bool,
prefs: OutputPrefs,
stdout_is_tty: bool,
) -> Box<dyn StatusReporter> {
match (mode, progress_enabled) {
(OutputMode::Accessible, _) => Box::new(AccessibleReporter::new(prefs)),
(OutputMode::Standard, true) => Box::new(IndicatifReporter::new()),
(OutputMode::Standard, false) => Box::new(SilentReporter),
if !progress_enabled {
return Box::new(SilentReporter);
}
let force_text_task_updates = should_force_text_task_updates(mode, stdout_is_tty);
match mode {
OutputMode::Accessible => Box::new(AccessibleReporter::new(prefs)),
OutputMode::Standard => Box::new(IndicatifReporter::with_force_text_task_updates(
force_text_task_updates,
)),
}
}

const fn should_force_text_task_updates(mode: OutputMode, stdout_is_tty: bool) -> bool {
mode.is_accessible() || !stdout_is_tty
}

/// Execute the parsed [`Cli`] commands with the given output preferences.
Expand All @@ -112,14 +123,16 @@ fn make_reporter(
/// Returns an error if manifest generation or the Ninja process fails.
pub fn run(cli: &Cli, prefs: OutputPrefs) -> Result<()> {
let mode = output_mode::resolve(cli.accessible);
let reporter = make_reporter(mode, cli.progress.unwrap_or(true), prefs);
let progress_enabled = cli.progress.unwrap_or(true);
let stdout_is_tty = std::io::stdout().is_terminal();
let reporter = make_reporter(mode, progress_enabled, prefs, stdout_is_tty);

let command = cli.command.clone().unwrap_or(Commands::Build(BuildArgs {
emit: None,
targets: Vec::new(),
}));
match command {
Commands::Build(args) => handle_build(cli, &args, reporter.as_ref()),
Commands::Build(args) => handle_build(cli, &args, reporter.as_ref(), progress_enabled),
Commands::Manifest { file } => {
let ninja = generate_ninja(cli, reporter.as_ref(), None)?;
if process::is_stdout_path(file.as_path()) {
Expand All @@ -131,8 +144,30 @@ pub fn run(cli: &Cli, prefs: OutputPrefs) -> Result<()> {
reporter.report_complete(keys::STATUS_TOOL_MANIFEST.into());
Ok(())
}
Commands::Clean => handle_clean(cli, reporter.as_ref()),
Commands::Graph => handle_graph(cli, reporter.as_ref()),
Commands::Clean => handle_ninja_tool(
cli,
NinjaToolSpec {
name: "clean",
key: keys::STATUS_TOOL_CLEAN.into(),
},
reporter.as_ref(),
progress_enabled,
),
Commands::Graph => handle_ninja_tool(
cli,
NinjaToolSpec {
name: "graph",
key: keys::STATUS_TOOL_GRAPH.into(),
},
reporter.as_ref(),
progress_enabled,
),
}
}

fn on_task_progress_callback(reporter: &dyn StatusReporter) -> impl FnMut(u32, u32, &str) + '_ {
move |current: u32, total: u32, description: &str| {
reporter.report_task_progress(current, total, description);
}
}

Expand All @@ -141,17 +176,12 @@ pub fn run(cli: &Cli, prefs: OutputPrefs) -> Result<()> {
/// # Errors
///
/// Returns an error if manifest generation or Ninja execution fails.
///
/// # Examples
/// ```ignore
/// use netsuke::cli::{BuildArgs, Cli};
/// use netsuke::runner::handle_build;
/// use netsuke::status::SilentReporter;
/// let cli = Cli::default();
/// let args = BuildArgs { emit: None, targets: vec![] };
/// handle_build(&cli, &args, &SilentReporter).unwrap();
/// ```
fn handle_build(cli: &Cli, args: &BuildArgs, reporter: &dyn StatusReporter) -> Result<()> {
fn handle_build(
cli: &Cli,
args: &BuildArgs,
reporter: &dyn StatusReporter,
progress_enabled: bool,
) -> Result<()> {
let ninja = generate_ninja(cli, reporter, Some(keys::STATUS_TOOL_BUILD.into()))?;
let targets = BuildTargets::new(&args.targets);

Expand All @@ -172,17 +202,39 @@ fn handle_build(cli: &Cli, args: &BuildArgs, reporter: &dyn StatusReporter) -> R
}

let program = process::resolve_ninja_program();
run_ninja(program.as_path(), cli, build_path.as_ref(), &targets).with_context(|| {
let ctx = || {
format!(
"running {} with build file {}",
program.display(),
build_path.display()
)
})?;
};
if progress_enabled {
let mut on_task_progress = on_task_progress_callback(reporter);
process::run_ninja_with_status(
process::NinjaBuildRequest {
program: program.as_path(),
cli,
build_file: build_path.as_ref(),
targets: &targets,
},
&mut on_task_progress,
)
.with_context(ctx)?;
} else {
run_ninja(program.as_path(), cli, build_path.as_ref(), &targets).with_context(ctx)?;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
reporter.report_complete(keys::STATUS_TOOL_BUILD.into());
Ok(())
}

/// Specification for a Ninja tool invocation: name and localization key.
#[derive(Clone, Copy)]
struct NinjaToolSpec<'a> {
name: &'a str,
key: LocalizationKey,
}

/// Execute a Ninja tool (e.g., `ninja -t clean`) using a temporary build file.
///
/// Generates the Ninja manifest to a temporary file, then invokes Ninja with
Expand All @@ -194,43 +246,48 @@ fn handle_build(cli: &Cli, args: &BuildArgs, reporter: &dyn StatusReporter) -> R
/// Returns an error if manifest generation or Ninja execution fails.
fn handle_ninja_tool(
cli: &Cli,
tool: &str,
tool_key: LocalizationKey,
tool: NinjaToolSpec<'_>,
reporter: &dyn StatusReporter,
progress_enabled: bool,
) -> Result<()> {
info!(
target: "netsuke::subcommand",
subcommand = tool,
subcommand = tool.name,
"Preparing Ninja tool invocation"
);
let ninja = generate_ninja(cli, reporter, Some(tool_key))?;
let ninja = generate_ninja(cli, reporter, Some(tool.key))?;

let tmp = process::create_temp_ninja_file(&ninja)?;
let build_path = tmp.path();

let program = process::resolve_ninja_program();
run_ninja_tool(program.as_path(), cli, build_path, tool).with_context(|| {
let ctx = || {
format!(
"running {} -t {} with build file {}",
program.display(),
tool,
tool.name,
build_path.display()
)
})?;
reporter.report_complete(tool_key);
};
if progress_enabled {
let mut on_task_progress = on_task_progress_callback(reporter);
process::run_ninja_tool_with_status(
process::NinjaToolRequest {
program: program.as_path(),
cli,
build_file: build_path,
tool: tool.name,
},
&mut on_task_progress,
)
.with_context(ctx)?;
} else {
run_ninja_tool(program.as_path(), cli, build_path, tool.name).with_context(ctx)?;
}
reporter.report_complete(tool.key);
Ok(())
}

/// Remove build artefacts by invoking `ninja -t clean`.
fn handle_clean(cli: &Cli, reporter: &dyn StatusReporter) -> Result<()> {
handle_ninja_tool(cli, "clean", keys::STATUS_TOOL_CLEAN.into(), reporter)
}

/// Display build dependency graph by invoking `ninja -t graph`.
fn handle_graph(cli: &Cli, reporter: &dyn StatusReporter) -> Result<()> {
handle_ninja_tool(cli, "graph", keys::STATUS_TOOL_GRAPH.into(), reporter)
}

/// Generate the Ninja manifest string from the Netsuke manifest referenced by `cli`.
///
/// Reports manifest and graph/synthesis pipeline stages via the provided
Expand Down
Loading
Loading