8 Commits

Author SHA1 Message Date
552f6c8e4f Merge pull request #41 from alex-xr555/refactor
refactor: Simplify use blocks
2026-09-20 18:28:22 +08:00
alex-xr555
da84916e10 Simplified use blocks 2026-09-19 18:11:28 +03:00
cb6ae3dcc7 Merge pull request #40 from alex-xr555/log 2026-09-19 09:28:18 +08:00
alex-xr555
10123ddedb Translation of comments into English, minor changes for completeness. 2026-09-15 18:34:54 +03:00
alex-xr555
0a140c75d8 Clarified the use block 2026-09-15 18:34:54 +03:00
alex-xr555
aa1d4ba807 The logging system has been brought to a fully functional level. 2026-09-15 18:34:54 +03:00
alex-xr555
991d1c20af Absolutely all logs have been replaced with the new system on log. 2026-09-15 18:34:54 +03:00
alex-xr555
df674defe1 Start of the logging system replacement 2026-09-15 18:34:54 +03:00
34 changed files with 1040 additions and 1769 deletions

View File

@@ -1,37 +1,5 @@
# Changelog # Changelog
## [v0.9.5] - 2026-10-21 "Структурированный вывод CLI / Structured CLI Output"
### RU
**Добавлено:**
- Человекочитаемый, plain- и JSON-режимы вывода в стиле Cargo с общими хелперами вывода.
- Явная обработка `--color auto|always|never`, `NO_COLOR`, `--quiet` и `--verbose`.
- Структурированные события жизненного цикла сборки и JSON-диагностика компилятора.
- Сценарии CLI output sandbox для help, жизненного цикла, диагностики, ошибок и broken pipe.
**Исправлено:**
- Единообразная маршрутизация stdout/stderr и обработка цветов во всех основных командах.
- Глобальный парсер опций теперь останавливается на subcommand и сохраняет аргументы после `--`.
- Перенаправленный вывод подпроцессов теперь захватывается конкурентно в output lab.
### EN
**Added:**
- Cargo-like human, plain, and JSON output modes with shared output helpers.
- Explicit `--color auto|always|never`, `NO_COLOR`, `--quiet`, and `--verbose` handling.
- Structured build lifecycle events and JSON compiler diagnostics.
- CLI output sandbox scenarios for help, lifecycle, diagnostics, errors, and broken pipes.
**Fixed:**
- Consistent stdout/stderr routing and color handling across the main commands.
- Global option parsing now stops at the subcommand and preserves arguments after `--`.
- Redirected subprocess output is captured concurrently in the output lab.
## [v0.9.1] - 2026-09-13 "Системные pkg-config-зависимости / System pkg-config Dependencies" ## [v0.9.1] - 2026-09-13 "Системные pkg-config-зависимости / System pkg-config Dependencies"
### RU ### RU

48
Cargo.lock generated
View File

@@ -219,7 +219,7 @@ dependencies = [
[[package]] [[package]]
name = "dcr" name = "dcr"
version = "0.9.5" version = "0.9.1"
dependencies = [ dependencies = [
"ctrlc", "ctrlc",
"env_logger", "env_logger",
@@ -452,12 +452,6 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.65" version = "0.1.65"
@@ -603,23 +597,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "is-terminal"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "is_ci"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.2" version = "1.70.2"
@@ -774,10 +751,6 @@ name = "owo-colors"
version = "4.4.0" version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8" checksum = "13c45bb4a6ae1280ec0803b1ef9d3455eb50f01efbbe1447ab020f1d54fba9d8"
dependencies = [
"supports-color 2.1.0",
"supports-color 3.0.2",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
@@ -1064,25 +1037,6 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "supports-color"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
dependencies = [
"is-terminal",
"is_ci",
]
[[package]]
name = "supports-color"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
dependencies = [
"is_ci",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dcr" name = "dcr"
version = "0.9.5" version = "0.9.1"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"
description = "DCR is a utility for managing C/C++ projects in a Cargo-like style." description = "DCR is a utility for managing C/C++ projects in a Cargo-like style."
@@ -78,7 +78,7 @@ fatfs = { version = "0.3", optional = true }
# Logging # Logging
log = "0.4.34" log = "0.4.34"
env_logger = "0.11.11" env_logger = "0.11.11"
owo-colors = { version = "4", features = ["supports-colors"] } owo-colors = "4"
[profile.release] [profile.release]
opt-level = "z" opt-level = "z"

13
dcrup.cmd Executable file
View File

@@ -0,0 +1,13 @@
@echo off
setlocal
set "SCRIPT_DIR=%~dp0"
if exist "%SCRIPT_DIR%dcrup.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%dcrup.ps1" %*
exit /b %ERRORLEVEL%
)
if exist "%USERPROFILE%\.dcr\bin\dcrup.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass -File "%USERPROFILE%\.dcr\bin\dcrup.ps1" %*
exit /b %ERRORLEVEL%
)
echo [error] dcrup.ps1 not found near dcrup.cmd >&2
exit /b 1

View File

@@ -1,4 +1,4 @@
#Requires -Version 5.1 #Requires -Version 5.1
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$DcrupHome = if ($env:DCRUP_HOME) { $env:DCRUP_HOME } else { Join-Path $env:USERPROFILE '.dcr' } $DcrupHome = if ($env:DCRUP_HOME) { $env:DCRUP_HOME } else { Join-Path $env:USERPROFILE '.dcr' }
@@ -6,7 +6,6 @@ $DcrupBin = if ($env:DCRUP_BIN) { $env:DCRUP_BIN } else { Join-Path $DcrupHom
$DcrupTc = Join-Path $DcrupHome 'toolchains' $DcrupTc = Join-Path $DcrupHome 'toolchains'
$DcrupMeta = Join-Path $DcrupHome 'meta' $DcrupMeta = Join-Path $DcrupHome 'meta'
$RepoUrl = if ($env:DCRUP_REPO) { $env:DCRUP_REPO } else { 'https://github.com/dexoron/dcr' } $RepoUrl = if ($env:DCRUP_REPO) { $env:DCRUP_REPO } else { 'https://github.com/dexoron/dcr' }
$InstallUrl = if ($env:DCRUP_INSTALL_URL) { $env:DCRUP_INSTALL_URL } else { 'https://ps1.dcr-tool.ru' }
$DcrupMirror = if ($env:DCRUP_MIRROR) { $env:DCRUP_MIRROR } else { 'auto' } $DcrupMirror = if ($env:DCRUP_MIRROR) { $env:DCRUP_MIRROR } else { 'auto' }
$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest' $ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest'
$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases' $ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases'
@@ -134,18 +133,6 @@ function Link-Default([string]$id) {
Ok "default → $id ($dst)" Ok "default → $id ($dst)"
} }
function Resolve-InstalledId([string]$spec) {
Parse-Spec $spec
if ($script:SpecChannel -eq 'night' -or $script:SpecFloating) {
$item = Get-ChildItem $DcrupTc -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "$($script:SpecChannel)-*" } |
Sort-Object Name | Select-Object -Last 1
if ($item) { return $item.Name }
return $null
}
return (Toolchain-Id)
}
function Set-Mirror([string]$value) { function Set-Mirror([string]$value) {
if ($value -notin @('auto', 'cf', 'gh', 'sf', 'ya')) { if ($value -notin @('auto', 'cf', 'gh', 'sf', 'ya')) {
Die "invalid --mirror: $value (use auto|cf|gh|sf|ya)" Die "invalid --mirror: $value (use auto|cf|gh|sf|ya)"
@@ -392,19 +379,7 @@ function Cmd-Show {
function Cmd-SelfInstall { function Cmd-SelfInstall {
Ensure-Dirs Ensure-Dirs
if ([string]::IsNullOrWhiteSpace($PSCommandPath) -or -not (Test-Path $PSCommandPath)) { Copy-Item $PSCommandPath (Join-Path $DcrupBin 'dcrup.ps1') -Force
# With `irm URL | iex`, PowerShell evaluates text and there is no
# source path. Download the canonical installer for self-install.
$tmp = Join-Path $env:TEMP ("dcrup-" + [guid]::NewGuid().ToString('N') + '.ps1')
try {
Invoke-WebRequest -Uri $InstallUrl -OutFile $tmp -UseBasicParsing -UserAgent 'dcrup'
Copy-Item $tmp (Join-Path $DcrupBin 'dcrup.ps1') -Force
} finally {
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
}
} else {
Copy-Item $PSCommandPath (Join-Path $DcrupBin 'dcrup.ps1') -Force
}
Install-CmdShim Install-CmdShim
Ensure-UserPath Ensure-UserPath
Ok "dcrup installed to $DcrupBin (use: dcrup …)" Ok "dcrup installed to $DcrupBin (use: dcrup …)"
@@ -442,7 +417,7 @@ switch ($cmd) {
if ($a -like 'night-*') { Cmd-Install @('night', '--build', '--force') } if ($a -like 'night-*') { Cmd-Install @('night', '--build', '--force') }
elseif ($a -like 'stable-*') { Cmd-Install @('stable', '--force') } elseif ($a -like 'stable-*') { Cmd-Install @('stable', '--force') }
elseif ($a -like 'dev-*') { Cmd-Install @('dev', '--force') } elseif ($a -like 'dev-*') { Cmd-Install @('dev', '--force') }
else { Ok "pinned $a - install a newer version explicitly" } else { Ok "pinned $a — install a newer version explicitly" }
} }
'uninstall' { 'uninstall' {
if ($rest -contains '--all') { if ($rest -contains '--all') {
@@ -451,18 +426,7 @@ switch ($cmd) {
Remove-Item (Active-Path) -ErrorAction SilentlyContinue Remove-Item (Active-Path) -ErrorAction SilentlyContinue
Ok 'removed all' Ok 'removed all'
} else { } else {
if ($rest.Count -ne 1) { Die 'usage: dcrup uninstall <spec>|--all' } Die 'use: dcrup uninstall --all (or install a replacement first)'
$id = Resolve-InstalledId $rest[0]
if (-not $id) { Die "not installed: $($rest[0])" }
$dir = Join-Path $DcrupTc $id
if (-not (Test-Path $dir)) { Die "not installed: $($rest[0])" }
Remove-Item -Recurse -Force $dir
if ((Read-Active) -eq $id) {
Remove-Item (Join-Path $DcrupBin 'dcr.exe') -ErrorAction SilentlyContinue
Remove-Item (Active-Path) -ErrorAction SilentlyContinue
Warn 'removed active toolchain; install or select another default'
}
Ok "removed $id"
} }
} }
'self-install' { Cmd-SelfInstall } 'self-install' { Cmd-SelfInstall }

996
dcrup.sh Normal file → Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,6 @@ sidebar_label: Changelog
# Changelog # Changelog
## 0.9.5 (2026-10-21)
- **Structured CLI output** — shared helpers now provide Cargo-like human, plain, and JSON output modes.
- **Color and diagnostics policy** — `--color`, `NO_COLOR`, `--quiet`, and `--verbose` are applied consistently across commands.
- **Build reporting** — lifecycle events and compiler diagnostics are available as structured JSON events.
- **Output lab** — sandbox scenarios cover help, lifecycle commands, compiler warnings/errors, stream capture, and broken pipes.
## 0.9.1 (2026-09-13) ## 0.9.1 (2026-09-13)
- **`pkg-config` dependencies** — declare `fmt = { pkg-config = "fmt" }` or multiple package names in `[dependencies]`; DCR applies `.pc` compiler/linker flags and records the resolved package in `dcr.lock`. - **`pkg-config` dependencies** — declare `fmt = { pkg-config = "fmt" }` or multiple package names in `[dependencies]`; DCR applies `.pc` compiler/linker flags and records the resolved package in `dcr.lock`.

View File

@@ -11,7 +11,7 @@ Install and switch DCR versions (stable / dev / night, pin `0.8.2`, optional bui
### Linux / macOS / BSD / Windows (bash) ### Linux / macOS / BSD / Windows (bash)
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- self-install curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install
export PATH="$HOME/.dcr/bin:$PATH" export PATH="$HOME/.dcr/bin:$PATH"
dcrup install stable dcrup install stable
``` ```
@@ -19,19 +19,28 @@ dcrup install stable
Non-interactive install of DCR in one shot: Non-interactive install of DCR in one shot:
```sh ```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.dcr-tool.ru | sh -s -- install stable curl -fsSL https://sh.dcr-tool.ru | sh -s -- install stable
export PATH="$HOME/.dcr/bin:$PATH" export PATH="$HOME/.dcr/bin:$PATH"
``` ```
### Windows (PowerShell) ### Windows (PowerShell)
```powershell ```powershell
& ([scriptblock]::Create((irm https://ps1.dcr-tool.ru))) self-install irm https://ps1.dcr-tool.ru | iex
# or download then run:
# irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1
# powershell -File .\dcrup.ps1 self-install
# $env:Path += ";$env:USERPROFILE\.dcr\bin" # $env:Path += ";$env:USERPROFILE\.dcr\bin"
dcrup install stable dcrup install stable
``` ```
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`) and is installed in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows). Optional cmd bootstrap (if you prefer `curl` of the cmd shim):
```bat
curl -fsSL -o dcrup.cmd https://cmd.dcr-tool.ru
```
After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`): shims live in `~/.dcr/bin` (Unix) or `%USERPROFILE%\.dcr\bin` (Windows `dcrup.cmd`).
### Common dcrup commands ### Common dcrup commands
@@ -49,8 +58,6 @@ dcrup update
dcrup list dcrup list
dcrup show dcrup show
dcrup which dcrup which
dcrup uninstall 0.8.2 # remove one installed version
dcrup uninstall --all # remove all installed versions
``` ```
Layout: `~/.dcr/toolchains/<id>/dcr` and `~/.dcr/bin/dcr` → active version. Layout: `~/.dcr/toolchains/<id>/dcr` and `~/.dcr/bin/dcr` → active version.

View File

@@ -1,34 +0,0 @@
---
sidebar_label: CLI output
---
# CLI output (experimental)
The structured output implementation is being developed incrementally.
Currently these global options apply to diagnostics emitted through the logger:
```sh
dcr --output plain build
dcr --output json build
dcr --color never build
dcr --quiet build
dcr --verbose build
```
`--output` accepts `human` (default), `plain`, or `json`.
Diagnostics are written to stderr. JSON diagnostics are newline-delimited objects
with `schema_version: 1`, `event`, and `message` fields. Command statuses use the
shared output helpers, build reporting emits structured lifecycle events, and
compiler output is represented as JSON events with its original stream and text.
The protocol is still experimental: some raw command data and subprocess stdout
remain unstructured.
`--color` accepts `auto`, `always`, or `never`. Automatic coloring requires a
terminal and respects `NO_COLOR`. Plain and JSON diagnostics never add colors.
Quiet mode preserves warnings, errors, and compiler diagnostics; verbose mode
enables debug and trace logging. Arguments following `--` are passed through
unchanged.
Remaining work includes migrating the remaining raw command data and subprocess
stdout, improving progress rendering and contextual errors, and expanding
integration coverage for stdout/stderr and closed pipes.

View File

@@ -1,4 +1,4 @@
.TH DCR 1 "2026-10-21" "dcr 0.9.5" "DCR Manual" .TH DCR 1 "2026-08-31" "dcr 0.9.1" "DCR Manual"
.SH NAME .SH NAME
dcr \- Cargo\-like build tool and package manager for C/C++ projects dcr \- Cargo\-like build tool and package manager for C/C++ projects
.SH SYNOPSIS .SH SYNOPSIS

View File

@@ -19,6 +19,7 @@ use crate::prelude::*;
use crate::core::{build_config::Config, deps::register}; use crate::core::{build_config::Config, deps::register};
use crate::utils::fs::find_project_root; use crate::utils::fs::find_project_root;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use toml::{Value, map::Map}; use toml::{Value, map::Map};
/// Parsed arguments for the `dcr add` command. /// Parsed arguments for the `dcr add` command.
@@ -57,22 +58,27 @@ pub struct AddArgs {
/// Process exit code: `0` on success, non-zero on usage or I/O errors. /// Process exit code: `0` on success, non-zero on usage or I/O errors.
pub fn add(args: &[String]) -> i32 { pub fn add(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr add"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE") printc(
.example("dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]"); " dcr add <name> <source> [--branch <b>] [--tag <t>] [--rev <r>]",
help.description("Adds a dependency to the project."); BOLD_CYAN,
help.section("SOURCES") );
.item("github:user/repo", "GitHub repository") println!();
.item("gitlab:user/repo", "GitLab repository") printc("DESCRIPTION:", BOLD_GREEN);
.item("git:host.com/user/repo", "Generic git repository") println!(" Adds a dependency to the project.");
.item("path:./path/to/lib", "Local path dependency") println!();
.item("pkg-config:fmt", "System pkg-config dependency") printc("SOURCES:", BOLD_GREEN);
.item("<version>", "Version from registry"); println!(" github:user/repo GitHub repository");
help.section("OPTIONS") println!(" gitlab:user/repo GitLab repository");
.item("--branch <b>", "Use a specific branch") println!(" git:host.com/user/repo Generic git repository");
.item("--tag <t>", "Use a specific tag") println!(" path:./path/to/lib Local path dependency");
.item("--rev <r>", "Use a specific commit hash"); println!(" pkg-config:fmt System pkg-config dependency");
help.render(); println!(" <version> Version from registry");
println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --branch <b> Use a specific branch");
println!(" --tag <t> Use a specific tag");
println!(" --rev <r> Use a specific commit hash");
return 0; return 0;
} }
@@ -157,7 +163,11 @@ pub fn add(args: &[String]) -> i32 {
return 1; return 1;
} }
crate::utils::output::success(format!("Added dependency `{}` to dcr.toml", add_args.name)); println!(
" {} dependency `{}` to dcr.toml",
colored("Added", BOLD_GREEN),
add_args.name
);
0 0
} }

View File

@@ -29,16 +29,33 @@ use crate::utils::build::{
normalize_kind, resolve_artifact_target_dir, resolve_compiler, normalize_kind, resolve_artifact_target_dir, resolve_compiler,
}; };
use crate::utils::fs::{canonicalize_path, find_project_root}; use crate::utils::fs::{canonicalize_path, find_project_root};
use crate::utils::output; use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use std::io::IsTerminal; use std::io::IsTerminal;
use std::path::Path; use std::path::Path;
use std::sync::{Arc, atomic::AtomicBool}; use std::sync::{Arc, atomic::AtomicBool};
pub use crate::core::build::get_build_string_with_profile; pub use crate::core::build::get_build_string_with_profile;
/// Formats a left-aligned status verb with the given style for CLI output.
fn status(verb: &str, style: &str) -> String {
colored(&format!("{verb:<9}"), style)
}
/// CLI build reporter that prints human-readable progress to stderr/stdout. /// CLI build reporter that prints human-readable progress to stderr/stdout.
struct CliReporter; struct CliReporter;
impl CliReporter {
/// Prints a single status line with a styled verb and free-form rest text.
///
/// # Parameters
/// - `verb`: Short status label (e.g. `"target"`, `"dep"`).
/// - `style`: Color/style constant for the verb.
/// - `rest`: Remaining message text after the verb.
fn line(&self, verb: &str, style: &str, rest: &str) {
eprintln!(" {} {}", status(verb, style), rest);
}
}
impl BuildReporter for CliReporter { impl BuildReporter for CliReporter {
/// Handles build events by printing progress, status lines, and compiler output. /// Handles build events by printing progress, status lines, and compiler output.
/// ///
@@ -52,13 +69,7 @@ impl BuildReporter for CliReporter {
target, target,
} => { } => {
common::finish_progress_line(); common::finish_progress_line();
output::emit_event( self.line("target", BOLD_CYAN, &format!("{index}/{total} {target}"));
"target_started",
serde_json::json!({
"index": index, "total": total, "target": target
}),
&format!("target {index}/{total} {target}"),
);
} }
BuildEvent::ProjectStart { BuildEvent::ProjectStart {
name, name,
@@ -66,23 +77,15 @@ impl BuildReporter for CliReporter {
target, target,
} => { } => {
common::finish_progress_line(); common::finish_progress_line();
output::emit_event( self.line(
"project_started", "project",
serde_json::json!({ BOLD_CYAN,
"name": name, "profile": profile, "target": target &format!("{name} ({profile}, {target})"),
}),
&format!("project {name} ({profile}, {target})"),
); );
} }
BuildEvent::DepBuilding { name, version } => { BuildEvent::DepBuilding { name, version } => {
common::finish_progress_line(); common::finish_progress_line();
output::emit_event( self.line("dep", BOLD_YELLOW, &format!("{name} v{version}"));
"dependency_building",
serde_json::json!({
"name": name, "version": version
}),
&format!("dep {name} v{version}"),
);
} }
BuildEvent::DepReady { BuildEvent::DepReady {
name, name,
@@ -92,55 +95,34 @@ impl BuildReporter for CliReporter {
common::finish_progress_line(); common::finish_progress_line();
// Only announce deps that were actually rebuilt this run. // Only announce deps that were actually rebuilt this run.
if rebuilt { if rebuilt {
output::emit_event( self.line("ready", BOLD_GREEN, &format!("{name} v{version}"));
"dependency_ready",
serde_json::json!({
"name": name, "version": version, "rebuilt": rebuilt
}),
&format!("ready {name} v{version}"),
);
} }
} }
BuildEvent::Compiling { name, version } => { BuildEvent::Compiling { name, version } => {
common::finish_progress_line(); common::finish_progress_line();
let label = format!( let label = format!(" {} {} v{}", status("compile", BOLD_GREEN), name, version);
" {}",
output::progress_label("compile", &format!("{name} v{version}"))
);
// Terminals use an in-place progress line; non-TTY gets a plain log line. // Terminals use an in-place progress line; non-TTY gets a plain log line.
if output::mode() == output::Mode::Human { common::set_progress_label(Some(label.clone()));
common::set_progress_label(Some(label.clone())); if !std::io::stderr().is_terminal() {
} eprintln!("{label}");
if output::mode() != output::Mode::Human || !std::io::stderr().is_terminal() {
output::emit_event(
"compiling",
serde_json::json!({
"name": name, "version": version
}),
&format!("compile {name} v{version}"),
);
} }
} }
BuildEvent::Packing { path } => { BuildEvent::Packing { path } => {
common::finish_progress_line(); common::finish_progress_line();
output::emit_event( self.line("pack", BOLD_CYAN, path);
"packing",
serde_json::json!({ "path": path }),
&format!("pack {path}"),
);
} }
BuildEvent::Finished { secs } => { BuildEvent::Finished { secs } => {
common::finish_progress_line(); common::finish_progress_line();
output::emit_event( self.line("done", BOLD_GREEN, &format!("in {secs}s"));
"finished",
serde_json::json!({ "seconds": secs }),
&format!("done in {secs}s"),
);
} }
BuildEvent::CompilerOutput { stream, text } => { BuildEvent::CompilerOutput { stream, text } => {
// Pause the progress spinner so compiler text is not overwritten. // Pause the progress spinner so compiler text is not overwritten.
common::interrupt_progress_for_output(); common::interrupt_progress_for_output();
output::compiler_output(stream, text); if stream == "stderr" {
eprint!("{text}");
} else {
print!("{text}");
}
} }
} }
} }
@@ -159,22 +141,26 @@ impl BuildReporter for CliReporter {
/// Process exit code: `0` on success or help, non-zero on failure. /// Process exit code: `0` on success or help, non-zero on failure.
pub fn build(args: &[String]) -> i32 { pub fn build(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr build"); printc("USAGE:", BOLD_GREEN);
help.description("Compiles the project. Default profile is --debug."); printc(
help.section("USAGE").example("dcr build [options]"); " dcr build [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [--print-artifact-path]",
help.section("OPTIONS") BOLD_CYAN,
.item("--debug", "Build with debug profile (default)") );
.item("--release", "Build with release profile") println!();
.item("--target <triple>", "Cross-compile for the given target") printc("DESCRIPTION:", BOLD_GREEN);
.item("--force", "Force a full rebuild") println!(" Compiles the project. Default profile is --debug.");
.item("--clean", "Clean before building") println!();
.item("--verbose", "Print detailed build output") printc("OPTIONS:", BOLD_GREEN);
.item("--workspace <name>", "Build a specific workspace member") println!(" --debug Build with debug profile (default)");
.item( println!(" --release Build with release profile");
"--print-artifact-path", println!(" --target <triple> Cross-compile for the given target");
"Print the absolute main artifact path", println!(" --force Force a full rebuild");
); println!(" --clean Clean before building");
help.render(); println!(" --verbose Print detailed build output");
println!(" --workspace <name> Build a specific workspace member");
println!(
" --print-artifact-path Print absolute main artifact path on stdout after success"
);
return 0; return 0;
} }
@@ -245,21 +231,13 @@ pub fn build(args: &[String]) -> i32 {
if flags.print_artifact_path if flags.print_artifact_path
&& let Some(ref path) = info.artifact_path && let Some(ref path) = info.artifact_path
{ {
if crate::utils::output::mode() == crate::utils::output::Mode::Json { println!("{path}");
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
} else {
crate::say!("{path}");
}
} }
} else if flags.print_artifact_path } else if flags.print_artifact_path
&& let Some(path) = && let Some(path) =
fallback_artifact_path(&root, &flags.profile, flags.target.as_deref()) fallback_artifact_path(&root, &flags.profile, flags.target.as_deref())
{ {
if crate::utils::output::mode() == crate::utils::output::Mode::Json { println!("{path}");
crate::utils::output::write_stdout_raw(&format!("{path}\n"));
} else {
crate::say!("{path}");
}
} }
0 0
} }

View File

@@ -21,6 +21,7 @@ use crate::core::build_config::Config;
use crate::core::workspace::parse_workspace; use crate::core::workspace::parse_workspace;
use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info}; use crate::utils::build::{default_profile_flags, default_target_triple, parse_version_info};
use crate::utils::fs::{check_dir, find_project_root, with_dir}; use crate::utils::fs::{check_dir, find_project_root, with_dir};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use glob::glob; use glob::glob;
use std::{fs, path::Path}; use std::{fs, path::Path};
@@ -35,16 +36,20 @@ use std::{fs, path::Path};
/// Process exit code: `0` on success, `1` on failure. /// Process exit code: `0` on success, `1` on failure.
pub fn clean(args: &[String]) -> i32 { pub fn clean(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr clean"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE") printc(
.example("dcr clean [--debug | --release] [--target <triple>] [--all]"); " dcr clean [--debug | --release] [--target <triple>] [--all]",
help.description("Removes build artifacts from the target directory."); BOLD_CYAN,
help.section("OPTIONS") );
.item("--debug", "Clean debug artifacts (default)") println!();
.item("--release", "Clean release artifacts") printc("DESCRIPTION:", BOLD_GREEN);
.item("--target <triple>", "Clean artifacts for a specific target") println!(" Removes build artifacts from the target directory.");
.item("--all", "Clean all workspace members"); println!();
help.render(); printc("OPTIONS:", BOLD_GREEN);
println!(" --debug Clean debug artifacts (default)");
println!(" --release Clean release artifacts");
println!(" --target <triple> Clean artifacts for a specific target");
println!(" --all Clean all workspace members");
return 0; return 0;
} }
@@ -205,7 +210,10 @@ fn clean_project_at(
if !items.contains(&"dcr.toml".to_string()) { if !items.contains(&"dcr.toml".to_string()) {
return Err("dcr.toml file not found".to_string()); return Err("dcr.toml file not found".to_string());
} }
crate::utils::output::status("Cleaning", format!("project `{project_name}`")); println!(
" Cleaning project `{}`",
colored(&project_name, BOLD_GREEN)
);
if let Some(profile) = profile { if let Some(profile) = profile {
let target_dir = if let Some(t) = target { let target_dir = if let Some(t) = target {
format!("target/{t}/{profile}") format!("target/{t}/{profile}")
@@ -223,12 +231,16 @@ fn clean_project_at(
if !dir_exists { if !dir_exists {
warn!("Directory target/{target_dir} not found"); warn!("Directory target/{target_dir} not found");
} else { } else {
crate::utils::output::status("Profile", profile); println!(" Profile: {}", colored(profile, BOLD_GREEN));
if let Some(t) = target { if let Some(t) = target {
crate::utils::output::status("Target", t); println!(" Target: {}", colored(t, BOLD_GREEN));
} }
let _ = fs::remove_dir_all(&target_dir); let _ = fs::remove_dir_all(&target_dir);
crate::utils::output::removed(format!("directory {target_dir}")); println!(
"{} Removed directory {}",
colored("\n ✔", BOLD_GREEN),
target_dir
);
} }
clean_custom_paths(&config, profile)?; clean_custom_paths(&config, profile)?;
return Ok(()); return Ok(());
@@ -236,7 +248,10 @@ fn clean_project_at(
if items.contains(&"target".to_string()) { if items.contains(&"target".to_string()) {
let _ = fs::remove_dir_all("target"); let _ = fs::remove_dir_all("target");
crate::utils::output::removed("directory target"); println!(
"{} Removed directory target",
colored("\n ✔", BOLD_GREEN)
);
} else { } else {
warn!("Directory target not found"); warn!("Directory target not found");
} }

View File

@@ -17,6 +17,7 @@
use crate::prelude::*; use crate::prelude::*;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use serde::Deserialize; use serde::Deserialize;
use std::fs; use std::fs;
use std::io::Read; use std::io::Read;
@@ -55,10 +56,11 @@ struct ReleaseAsset {
/// `0` on success / already up to date / pacman-managed refuse; `1` on error. /// `0` on success / already up to date / pacman-managed refuse; `1` on error.
pub fn flag_update(args: &[String]) -> i32 { pub fn flag_update(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr --update"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE").example("dcr --update"); printc(" dcr --update", BOLD_CYAN);
help.description("Updates DCR to the latest version from GitHub Releases."); println!();
help.render(); printc("DESCRIPTION:", BOLD_GREEN);
println!(" Updates DCR to the latest version from GitHub Releases.");
return 0; return 0;
} }
@@ -78,9 +80,8 @@ pub fn flag_update(args: &[String]) -> i32 {
// Refuse self-update when the binary is owned by pacman/AUR. // Refuse self-update when the binary is owned by pacman/AUR.
if let Some(package_name) = pacman_owned_package(&current_exe) { if let Some(package_name) = pacman_owned_package(&current_exe) {
warn!("This dcr binary is managed by pacman/AUR"); warn!("This dcr binary is managed by pacman/AUR");
crate::utils::output::status( println!(
"Update via package manager:", "Update via package manager: yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"
format!("yay/paru -Syu {package_name} or sudo pacman -Syu {package_name}"),
); );
return 0; return 0;
} }
@@ -99,7 +100,7 @@ pub fn flag_update(args: &[String]) -> i32 {
// Normalize the tag by stripping a leading `v` before comparison. // Normalize the tag by stripping a leading `v` before comparison.
let latest_version = release.tag_name.trim_start_matches('v'); let latest_version = release.tag_name.trim_start_matches('v');
if latest_version == current_version { if latest_version == current_version {
crate::utils::output::status("Up to date", current_version); println!("Latest version is already installed: {current_version}");
return 0; return 0;
} }
@@ -137,9 +138,7 @@ pub fn flag_update(args: &[String]) -> i32 {
} }
let _ = fs::remove_file(&temp_path); let _ = fs::remove_file(&temp_path);
crate::utils::output::success(format!( println!("Update completed: {current_version} -> {latest_version}");
"Updated DCR: {current_version} -> {latest_version}"
));
0 0
} }

View File

@@ -17,6 +17,7 @@
use crate::prelude::*; use crate::prelude::*;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use glob::glob; use glob::glob;
use std::process::Command; use std::process::Command;
@@ -28,11 +29,12 @@ use std::process::Command;
/// If the first argument is `--help`, prints usage information and returns 0. /// If the first argument is `--help`, prints usage information and returns 0.
pub fn fmt(args: &[String]) -> i32 { pub fn fmt(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr fmt"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE").example("dcr fmt"); printc(" dcr fmt", BOLD_CYAN);
help.description("Formats all C/C++ source files using clang-format.") println!();
.note("Scans src/ and tests/ directories recursively."); printc("DESCRIPTION:", BOLD_GREEN);
help.render(); println!(" Formats all C/C++ source files using clang-format.");
println!(" Scans src/ and tests/ directories recursively.");
return 0; return 0;
} }
@@ -68,18 +70,22 @@ pub fn fmt(args: &[String]) -> i32 {
} }
if files.is_empty() { if files.is_empty() {
crate::utils::output::status("Format", "no files to format"); println!(" {} No files to format", colored("Format", BOLD_GREEN));
return 0; return 0;
} }
// Format the collected files // Format the collected files
crate::utils::output::status("Formatting", format!("{} files", files.len())); println!(
" {} {} files",
colored("Formatting", BOLD_GREEN),
files.len()
);
let status = Command::new("clang-format").arg("-i").args(&files).status(); let status = Command::new("clang-format").arg("-i").args(&files).status();
match status { match status {
Ok(s) if s.success() => { Ok(s) if s.success() => {
crate::utils::output::success("Format successful"); println!(" {} successful", colored("Format", BOLD_GREEN));
0 0
} }
Ok(s) => { Ok(s) => {

View File

@@ -31,6 +31,7 @@ use crate::utils::build::{
use crate::utils::fs::{ use crate::utils::fs::{
absolute_join, atomic_write, canonicalize_path, ensure_dcr_dir, find_project_root, absolute_join, atomic_write, canonicalize_path, ensure_dcr_dir, find_project_root,
}; };
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@@ -251,30 +252,33 @@ pub struct ProjectInfo {
pub fn r#gen(args: &[String]) -> i32 { pub fn r#gen(args: &[String]) -> i32 {
let subcommand = match args.first() { let subcommand = match args.first() {
Some(s) if s == "--help" => { Some(s) if s == "--help" => {
let mut help = crate::utils::help::HelpBuilder::new("dcr gen"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE").example("dcr gen <subcommand>"); printc(" dcr gen <subcommand>", BOLD_CYAN);
help.description("Generates IDE and tooling integration files."); println!();
help.section("SUBCOMMANDS") printc("DESCRIPTION:", BOLD_GREEN);
.item("project-info", "Print project metadata as JSON") println!(" Generates IDE and tooling integration files.");
.item("compile-commands", "Generate .dcr/compile_commands.json") println!();
.item("vscode", "Generate .vscode/ integration files") printc("SUBCOMMANDS:", BOLD_GREEN);
.item("clion", "Generate .idea/ integration files"); println!(" project-info Print project metadata as JSON");
help.section("OPTIONS") println!(" compile-commands Generate .dcr/compile_commands.json");
.item("--debug | --release", "Profile (default: debug)") println!(" vscode Generate .vscode/ integration files");
.item("--quiet | -q", "Suppress success messages on stdout"); println!(" clion Generate .idea/ integration files");
help.render(); println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --debug | --release Profile (default: debug)");
println!(" --quiet | -q Suppress success messages on stdout");
return 0; return 0;
} }
Some(s) => s.as_str(), Some(s) => s.as_str(),
None => { None => {
let mut help = crate::utils::help::HelpBuilder::new("dcr gen"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE").example("dcr gen <subcommand>"); printc(" dcr gen <subcommand>", BOLD_CYAN);
help.section("SUBCOMMANDS") println!();
.item("project-info", "Print project metadata as JSON") printc("SUBCOMMANDS:", BOLD_GREEN);
.item("compile-commands", "Generate .dcr/compile_commands.json") println!(" project-info Print project metadata as JSON");
.item("vscode", "Generate .vscode/ integration files") println!(" compile-commands Generate .dcr/compile_commands.json");
.item("clion", "Generate .idea/ integration files"); println!(" vscode Generate .vscode/ integration files");
help.render(); println!(" clion Generate .idea/ integration files");
return 1; return 1;
} }
}; };
@@ -773,8 +777,8 @@ fn ensure_clangd(root: &Path, quiet: bool) {
} }
Ok(_) => { Ok(_) => {
if !quiet { if !quiet {
crate::utils::output::warning!( eprintln!(
".clangd already exists and is not managed by dcr; \ "Warning: .clangd already exists and is not managed by dcr; \
set CompilationDatabase to .dcr manually if needed" set CompilationDatabase to .dcr manually if needed"
); );
} }
@@ -813,8 +817,8 @@ fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
let member_path = canonicalize_path(&member.path); let member_path = canonicalize_path(&member.path);
match collect_project_info(&member_path, profile, ws_root) { match collect_project_info(&member_path, profile, ws_root) {
Ok(info) => all.push(info), Ok(info) => all.push(info),
Err(e) => crate::utils::output::warning!( Err(e) => eprintln!(
"skipping workspace member {}: {e}", "Warning: skipping workspace member {}: {e}",
member_path.display() member_path.display()
), ),
} }
@@ -825,7 +829,7 @@ fn collect_all(root: &Path, profile: &str) -> Result<Vec<ProjectInfo>, String> {
match collect_project_info(&root, profile, ws_root) { match collect_project_info(&root, profile, ws_root) {
Ok(info) => all.push(info), Ok(info) => all.push(info),
Err(e) if is_workspace => { Err(e) if is_workspace => {
crate::utils::output::warning!("skipping workspace root package: {e}"); eprintln!("Warning: skipping workspace root package: {e}");
} }
Err(e) => return Err(e), Err(e) => return Err(e),
} }
@@ -859,12 +863,20 @@ fn gen_project_info(args: &[String]) -> i32 {
&& let Err(e) = write_dcr_metadata(&root, info) && let Err(e) = write_dcr_metadata(&root, info)
&& !quiet && !quiet
{ {
crate::utils::output::warning!("{e}"); eprintln!("Warning: {e}");
} }
ensure_clangd(&root, quiet); ensure_clangd(&root, quiet);
let entries = all.iter().map(project_info_to_json).collect::<Vec<_>>(); print!("[");
crate::utils::output::write_stdout_raw(&format!("[\n{}\n]\n", entries.join(",\n"))); for (i, info) in all.iter().enumerate() {
if i > 0 {
print!(",");
}
println!();
print!("{}", project_info_to_json(info));
}
println!();
println!("]");
0 0
} }
@@ -989,11 +1001,11 @@ fn gen_compile_commands_inner(root: &Path, profile: &str, all: &[ProjectInfo], q
&& let Err(e) = write_dcr_metadata(root, info) && let Err(e) = write_dcr_metadata(root, info)
&& !quiet && !quiet
{ {
crate::utils::output::warning!("{e}"); eprintln!("Warning: {e}");
} }
ensure_clangd(root, quiet); ensure_clangd(root, quiet);
if !quiet { if !quiet {
crate::utils::output::created(format!("{}", out_path.display())); println!("Generated {}", out_path.display());
} }
0 0
} }
@@ -1174,7 +1186,7 @@ fn gen_vscode(args: &[String]) -> i32 {
error!("Failed to write tasks.json: {e}"); error!("Failed to write tasks.json: {e}");
return 1; return 1;
} }
crate::utils::output::created(format!("{}", vscode_dir.join("tasks.json").display())); println!("Generated {}", vscode_dir.join("tasks.json").display());
// launch.json — one entry per binary target // launch.json — one entry per binary target
let launch = gen_launch_json(&all, &root); let launch = gen_launch_json(&all, &root);
@@ -1182,7 +1194,7 @@ fn gen_vscode(args: &[String]) -> i32 {
error!("Failed to write launch.json: {e}"); error!("Failed to write launch.json: {e}");
return 1; return 1;
} }
crate::utils::output::created(format!("{}", vscode_dir.join("launch.json").display())); println!("Generated {}", vscode_dir.join("launch.json").display());
// settings.json (clangd compile-commands-dir) // settings.json (clangd compile-commands-dir)
let settings = gen_settings_json(&root); let settings = gen_settings_json(&root);
@@ -1190,14 +1202,14 @@ fn gen_vscode(args: &[String]) -> i32 {
error!("Failed to write settings.json: {e}"); error!("Failed to write settings.json: {e}");
return 1; return 1;
} }
crate::utils::output::created(format!("{}", vscode_dir.join("settings.json").display())); println!("Generated {}", vscode_dir.join("settings.json").display());
// extensions.json — disable cpptools, recommend clangd // extensions.json — disable cpptools, recommend clangd
if let Err(e) = std::fs::write(vscode_dir.join("extensions.json"), gen_extensions_json()) { if let Err(e) = std::fs::write(vscode_dir.join("extensions.json"), gen_extensions_json()) {
error!("Failed to write extensions.json: {e}"); error!("Failed to write extensions.json: {e}");
return 1; return 1;
} }
crate::utils::output::created(format!("{}", vscode_dir.join("extensions.json").display())); println!("Generated {}", vscode_dir.join("extensions.json").display());
0 0
} }
@@ -1470,7 +1482,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write externalTools.xml: {e}"); error!("Failed to write externalTools.xml: {e}");
return 1; return 1;
} }
crate::utils::output::created(format!("{}", idea_dir.join("externalTools.xml").display())); println!("Generated {}", idea_dir.join("externalTools.xml").display());
// customTargets.xml // customTargets.xml
let targets = gen_clion_custom_targets(); let targets = gen_clion_custom_targets();
@@ -1478,7 +1490,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write customTargets.xml: {e}"); error!("Failed to write customTargets.xml: {e}");
return 1; return 1;
} }
crate::utils::output::created(format!("{}", idea_dir.join("customTargets.xml").display())); println!("Generated {}", idea_dir.join("customTargets.xml").display());
// misc.xml — point CLion at compile_commands.json // misc.xml — point CLion at compile_commands.json
let misc = gen_clion_misc_xml(&root); let misc = gen_clion_misc_xml(&root);
@@ -1486,7 +1498,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write misc.xml: {e}"); error!("Failed to write misc.xml: {e}");
return 1; return 1;
} }
crate::utils::output::created(format!("{}", idea_dir.join("misc.xml").display())); println!("Generated {}", idea_dir.join("misc.xml").display());
// .idea/.gitignore // .idea/.gitignore
let gitignore = "# CLion generated files\nworkspace.xml\n*.iml\n"; let gitignore = "# CLion generated files\nworkspace.xml\n*.iml\n";
@@ -1494,7 +1506,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write .idea/.gitignore: {e}"); error!("Failed to write .idea/.gitignore: {e}");
return 1; return 1;
} }
crate::utils::output::created(format!("{}", idea_dir.join(".gitignore").display())); println!("Generated {}", idea_dir.join(".gitignore").display());
// runConfigurations/<name>.xml — one per binary // runConfigurations/<name>.xml — one per binary
for info in &all { for info in &all {
@@ -1508,7 +1520,7 @@ fn gen_clion(args: &[String]) -> i32 {
error!("Failed to write runConfigurations/{fname}: {e}"); error!("Failed to write runConfigurations/{fname}: {e}");
return 1; return 1;
} }
crate::utils::output::created(format!("{}", path.display())); println!("Generated {}", path.display());
} }
0 0

View File

@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
/// Displays the help information for the DCR CLI tool. /// Displays the help information for the DCR CLI tool.
/// ///
/// Prints usage, commands, flags, options, examples, and a tip. /// Prints usage, commands, flags, options, examples, and a tip.
@@ -22,40 +24,43 @@
/// # Returns /// # Returns
/// Always `0`. /// Always `0`.
pub fn help() -> i32 { pub fn help() -> i32 {
use crate::utils::help::HelpBuilder; println!("DCR (Dexoron Cargo Realization)");
let mut help = HelpBuilder::new("DCR (Dexoron Cargo Realization)"); println!("C project manager inspired by Cargo.");
help.description("C/C++ project manager inspired by Cargo."); println!();
help.section("USAGE").example("dcr <command> [options]"); printc("USAGE:", BOLD_GREEN);
help.section("COMMANDS") printc(" dcr <command> [options]", BOLD_CYAN);
.item("new <name>", "Create a new project") println!();
.item("init", "Initialize the current directory as a project") printc("COMMANDS:", BOLD_GREEN);
.item("build", "Build the project (default: --debug)") println!(" new <name> Create a new project");
.item("run", "Build and run the project") println!(" init Initialize the current directory as a project");
.item("add <name> ...", "Add a dependency to dcr.toml") println!(" build [--profile] Build the project (default: --debug)");
.item("tree", "Display the dependency tree") println!(" run [--profile] Build and run the project (default: --debug)");
.item("test", "Run project tests (alias: tests)") println!(" add <name> ... Add a dependency to dcr.toml");
.item("fmt", "Format C/C++ sources using clang-format") println!(" tree Display the dependency tree");
.item("lint", "Run clang-tidy on C/C++ sources") println!(" test Run project tests (alias: tests)");
.item("setup", "Show configured package registries") println!(" fmt Format all C/C++ source files using clang-format");
.item("clean", "Remove build artifacts") println!(" lint Run clang-tidy on C/C++ sources");
.item("gen <subcommand>", "Generate IDE integration files"); println!(" setup Show configured package registries");
help.section("FLAGS") println!(" clean Remove the target directory");
.item("--help", "Show command help") println!(" gen <subcommand> Generate IDE integration files");
.item("--update", "Update DCR") printc("FLAGS:", BOLD_GREEN);
.item("--version", "Show DCR version"); println!(" --help Show command help");
help.section("OPTIONS") println!(" --update Update dcr to the latest version");
.item("--output human|plain|json", "Select output format") println!(" --version Show dcr version");
.item("--color auto|always|never", "Select color policy") println!();
.item("--quiet", "Suppress ordinary diagnostics") printc("OPTIONS:", BOLD_GREEN);
.item("--verbose", "Enable detailed diagnostics") println!(" --debug Use debug profile");
.item("--debug / --release", "Select build profile") println!(" --release Use release profile");
.item("--force", "Force rebuild (build/run)") println!(" --force Force rebuild (build/run)");
.item("--clean", "Clean before build (build/run)") println!(" --clean Clean before build (build/run)");
.item("--all", "Clean all workspace members"); println!(" --all Clean all workspace members (clean)");
help.example("dcr new hello") println!();
.example("dcr build --release") printc("EXAMPLES:", BOLD_GREEN);
.example("dcr run --debug"); printc(" dcr new hello", BOLD_CYAN);
help.tip("Run 'dcr <command> --help' for command-specific help."); printc(" dcr build --release", BOLD_CYAN);
help.render(); printc(" dcr run --debug", BOLD_CYAN);
println!();
printc("TIP:", BOLD_GREEN);
println!(" Run 'dcr <command> --help' for command-specific help.");
0 0
} }

View File

@@ -21,6 +21,7 @@ use crate::config::FILE_MAIN_C;
use crate::core::build_config::{Config, validate_package_name}; use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind; use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::{fs, io::Write}; use std::{fs, io::Write};
/// Initializes the current working directory as a new DCR project. /// Initializes the current working directory as a new DCR project.
@@ -29,11 +30,12 @@ use std::{fs, io::Write};
/// `dcr.toml` and `src/main.c`, and configures version control (Git) integration if applicable. /// `dcr.toml` and `src/main.c`, and configures version control (Git) integration if applicable.
pub fn init(args: &[String]) -> i32 { pub fn init(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr init"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE").example("dcr init [--vcs <git|none>]"); printc(" dcr init [--vcs <git|none>]", BOLD_CYAN);
help.description("Initializes the current directory as a DCR project.") println!();
.note("The directory must be empty."); printc("DESCRIPTION:", BOLD_GREEN);
help.render(); println!(" Initializes the current directory as a DCR project.");
println!(" The directory must be empty.");
return 0; return 0;
} }
@@ -74,21 +76,28 @@ pub fn init(args: &[String]) -> i32 {
} }
if let Err(e) = validate_package_name(&project_name) { if let Err(e) = validate_package_name(&project_name) {
error!("Invalid project name `{}`: {e}", project_name,); error!(
"Invalid project name `{}`: {e}",
colored(&project_name, BOLD_CYAN),
);
return 1; return 1;
} }
let cwd = std::env::current_dir() let cwd = std::env::current_dir()
.map(|v| v.to_string_lossy().to_string()) .map(|v| v.to_string_lossy().to_string())
.unwrap_or_else(|_| ".".to_string()); .unwrap_or_else(|_| ".".to_string());
crate::utils::output::status("Initializing", format!("project in {cwd}")); println!("Initializing the project in {cwd}");
// Create and initialize default manifest (dcr.toml). // Create and initialize default manifest (dcr.toml).
if Config::create("./dcr.toml", Some(&project_name)).is_err() { if Config::create("./dcr.toml", Some(&project_name)).is_err() {
error!("Failed to create dcr.toml"); error!("Failed to create dcr.toml");
return 1; return 1;
} }
crate::utils::output::created("dcr.toml"); println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
);
// Create src directory and write template main.c file. // Create src directory and write template main.c file.
if fs::create_dir("src").is_err() { if fs::create_dir("src").is_err() {
@@ -106,7 +115,11 @@ pub fn init(args: &[String]) -> i32 {
error!("Failed to write src/main.c"); error!("Failed to write src/main.c");
return 1; return 1;
} }
crate::utils::output::created("src/main.c"); println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
);
// Resolve target VCS provider or detect existing repositories. // Resolve target VCS provider or detect existing repositories.
let mut vcs_kind = VcsKind::Git; let mut vcs_kind = VcsKind::Git;
@@ -130,7 +143,10 @@ pub fn init(args: &[String]) -> i32 {
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) { if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn!("Failed to initialize git repository: {e}"); warn!("Failed to initialize git repository: {e}");
} else { } else {
crate::utils::output::success("Initialized git repository"); println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
} }
} else { } else {
warn!( warn!(
@@ -143,7 +159,11 @@ pub fn init(args: &[String]) -> i32 {
warn!("Failed to update .gitignore: {e}"); warn!("Failed to update .gitignore: {e}");
} }
crate::utils::output::success(format!("Initialized project `{project_name}`")); println!(
crate::utils::output::info("Next step:\n dcr run"); "Project `{}` successfully created\n",
colored(project_name.as_str(), BOLD_GREEN)
);
printc("Next step:", BOLD_GREEN);
printc(" dcr run", BOLD_CYAN);
0 0
} }

View File

@@ -21,6 +21,7 @@ use crate::core::build::common;
use crate::core::build_config::Config; use crate::core::build_config::Config;
use crate::utils::build::{get_bool_with_profile, get_list_with_profile}; use crate::utils::build::{get_bool_with_profile, get_list_with_profile};
use crate::utils::fs::find_project_root; use crate::utils::fs::find_project_root;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_YELLOW, colored, printc};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
@@ -29,13 +30,15 @@ use std::process::Command;
/// Scans source roots (configurable via build.roots) and tests/ directory. /// Scans source roots (configurable via build.roots) and tests/ directory.
pub fn lint(args: &[String]) -> i32 { pub fn lint(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr lint"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE").example("dcr lint [--fix]"); printc(" dcr lint [--fix]", BOLD_CYAN);
help.description("Runs clang-tidy on all C/C++ source files.") println!();
.note("Scans source roots (configurable via build.roots) and tests/ directory."); printc("DESCRIPTION:", BOLD_GREEN);
help.section("OPTIONS") println!(" Runs clang-tidy on all C/C++ source files.");
.item("--fix", "Apply clang-tidy suggestions automatically"); println!(" Scans source roots (configurable via build.roots) and tests/ directory.");
help.render(); println!();
printc("OPTIONS:", BOLD_GREEN);
println!(" --fix Apply clang-tidy suggestions automatically");
return 0; return 0;
} }
@@ -114,17 +117,15 @@ pub fn lint(args: &[String]) -> i32 {
files.sort(); files.sort();
if files.is_empty() { if files.is_empty() {
crate::utils::output::status("Lint", "no files to lint"); println!(" {} No files to lint", colored("Lint", BOLD_GREEN));
return 0; return 0;
} }
crate::utils::output::status( println!(
"Linting", " {} {} files {}",
format!( colored("Linting", BOLD_GREEN),
"{} files {}", files.len(),
files.len(), if do_fix { "(with --fix)" } else { "" }
if do_fix { "(with --fix)" } else { "" }
),
); );
let mut total_issues = 0u32; let mut total_issues = 0u32;
@@ -160,12 +161,14 @@ pub fn lint(args: &[String]) -> i32 {
if issue_count > 0 { if issue_count > 0 {
total_issues += issue_count as u32; total_issues += issue_count as u32;
failed += 1; failed += 1;
crate::utils::output::warning(format!("{file} ({issue_count} issues)")); print!(" {} ", colored("×", BOLD_YELLOW));
println!("{} ({} issues)", file, issue_count);
for issue in &issues { for issue in &issues {
crate::utils::output::compiler_output("stderr", issue); println!(" {}", issue);
} }
} else { } else {
crate::utils::output::status("Checked", file); print!(" {} ", colored("✓", BOLD_GREEN));
println!("{}", file);
} }
} }
@@ -173,11 +176,19 @@ pub fn lint(args: &[String]) -> i32 {
let passed = total - failed as usize; let passed = total - failed as usize;
if failed == 0 { if failed == 0 {
crate::utils::output::success(format!("Finished checking {total} files; no issues")); println!(
" {} {} files, no issues",
colored("Lint", BOLD_GREEN),
total
);
} else { } else {
crate::utils::output::warning(format!( println!(
"Finished checking {total} files; {passed} passed, {total_issues} issues found" " {} {}/{} files passed, {} issues found",
)); colored("Lint", BOLD_YELLOW),
passed,
total,
total_issues
);
} }
0 0

View File

@@ -21,6 +21,7 @@ use crate::config::FILE_MAIN_C;
use crate::core::build_config::{Config, validate_package_name}; use crate::core::build_config::{Config, validate_package_name};
use crate::core::vcs::VcsKind; use crate::core::vcs::VcsKind;
use crate::utils::fs::check_dir; use crate::utils::fs::check_dir;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::{fs, io::Write}; use std::{fs, io::Write};
/// Executes the `dcr new` command to scaffold a new C/C++ project directory structure. /// Executes the `dcr new` command to scaffold a new C/C++ project directory structure.
@@ -29,12 +30,12 @@ use std::{fs, io::Write};
/// template source file generation (`src/main.c`), and initial VCS setup. /// template source file generation (`src/main.c`), and initial VCS setup.
pub fn new(args: &[String]) -> i32 { pub fn new(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr new"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE") printc(" dcr new <name> [--vcs <git|none>]", BOLD_CYAN);
.example("dcr new <name> [--vcs <git|none>]"); println!();
help.description("Creates a new C/C++ project with the given name.") printc("DESCRIPTION:", BOLD_GREEN);
.note("The name may only contain ASCII letters, digits, '_' and '-'."); println!(" Creates a new C/C++ project with the given name.");
help.render(); println!(" The name may only contain ASCII letters, digits, '_' and '-'.");
return 0; return 0;
} }
@@ -69,27 +70,41 @@ pub fn new(args: &[String]) -> i32 {
} }
let project_name = &clean_args[0]; let project_name = &clean_args[0];
println!(
"Creating a Project `{}`...",
colored(project_name, BOLD_CYAN)
);
if let Err(e) = validate_package_name(project_name) { if let Err(e) = validate_package_name(project_name) {
error!("Invalid project name `{}`: {e}", project_name,); error!(
"Invalid project name `{}`: {e}",
colored(project_name, BOLD_CYAN),
);
return 1; return 1;
} }
if items.contains(project_name) { if items.contains(project_name) {
error!("Directory `{}` already exists", project_name); error!(
crate::utils::output::info(format!( "Directory `{}` already exists\n",
colored(project_name, BOLD_CYAN)
);
printc("Hint:", BOLD_CYAN);
println!(
" Use `{}` to initialize an existing project\n or specify a different project name", " Use `{}` to initialize an existing project\n or specify a different project name",
"dcr init" colored("dcr init", BOLD_CYAN)
)); );
return 1; return 1;
} }
crate::utils::output::status("Creating", format!("project `{project_name}`"));
if fs::create_dir(project_name).is_err() { if fs::create_dir(project_name).is_err() {
error!("Failed to create directory"); error!("Failed to create directory");
return 1; return 1;
} }
crate::utils::output::created(format!("directory `{project_name}`")); println!(
" {} Directory created {}",
colored("✔", BOLD_GREEN),
project_name
);
// Initialize and write project configuration manifest (dcr.toml). // Initialize and write project configuration manifest (dcr.toml).
let toml_path = format!("./{project_name}/dcr.toml"); let toml_path = format!("./{project_name}/dcr.toml");
@@ -97,7 +112,11 @@ pub fn new(args: &[String]) -> i32 {
error!("Failed to create dcr.toml"); error!("Failed to create dcr.toml");
return 1; return 1;
} }
crate::utils::output::created("dcr.toml"); println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("dcr.toml", BOLD_CYAN)
);
// Generate src directory and populate initial main.c file. // Generate src directory and populate initial main.c file.
if fs::create_dir_all(format!("./{project_name}/src")).is_err() { if fs::create_dir_all(format!("./{project_name}/src")).is_err() {
@@ -116,7 +135,11 @@ pub fn new(args: &[String]) -> i32 {
error!("Failed to write src/main.c"); error!("Failed to write src/main.c");
return 1; return 1;
} }
crate::utils::output::created("src/main.c"); println!(
" {} Created file {}",
colored("✔", BOLD_GREEN),
colored("src/main.c", BOLD_CYAN)
);
// Resolve target VCS mode based on CLI flags or parent directory state. // Resolve target VCS mode based on CLI flags or parent directory state.
let mut vcs_kind = VcsKind::Git; let mut vcs_kind = VcsKind::Git;
@@ -141,7 +164,10 @@ pub fn new(args: &[String]) -> i32 {
if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) { if let Err(e) = crate::core::vcs::init_vcs(vcs_kind, project_path) {
warn!("Failed to initialize git repository: {e}"); warn!("Failed to initialize git repository: {e}");
} else { } else {
crate::utils::output::success("Initialized git repository"); println!(
" {} Initialized git repository",
colored("✔", BOLD_GREEN)
);
} }
} else { } else {
warn!( warn!(
@@ -155,7 +181,11 @@ pub fn new(args: &[String]) -> i32 {
warn!("Failed to update .gitignore: {e}"); warn!("Failed to update .gitignore: {e}");
} }
crate::utils::output::success(format!("Created project `{project_name}`")); println!(
crate::utils::output::info(format!("Next steps:\n cd {project_name}\n dcr run")); "Project `{}` successfully created\n",
colored(project_name, BOLD_GREEN)
);
printc("Next step:", BOLD_GREEN);
printc(&format!(" cd {}\n dcr run", project_name), BOLD_CYAN);
0 0
} }

View File

@@ -23,6 +23,7 @@ use crate::core::build_config::Config;
use crate::core::runner::run_binary; use crate::core::runner::run_binary;
use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars}; use crate::utils::build::{normalize_target_os, parse_version_info, substitute_vars};
use crate::utils::fs::{find_project_root, with_dir}; use crate::utils::fs::{find_project_root, with_dir};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, colored, printc};
use std::{path::Path, process::Command}; use std::{path::Path, process::Command};
/// Retrieves the run command from the config, preferring target-specific, /// Retrieves the run command from the config, preferring target-specific,
@@ -65,19 +66,24 @@ fn get_run_cmd(
/// Process exit code from the built binary / `run.cmd`, or non-zero on build/setup failure. /// Process exit code from the built binary / `run.cmd`, or non-zero on build/setup failure.
pub fn run(args: &[String]) -> i32 { pub fn run(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr run"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE").example("dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]"); printc(
help.description("Builds and runs the project. Only available for kind = \"bin\".") " dcr run [--debug | --release] [--target <triple>] [--force] [--clean] [--verbose] [-- <args>...]",
.note("Arguments after `--` are passed to the built binary (cargo-style)."); BOLD_CYAN,
help.section("OPTIONS") );
.item("--debug", "Run with debug profile (default)") println!();
.item("--release", "Run with release profile") printc("DESCRIPTION:", BOLD_GREEN);
.item("--target <triple>", "Cross-compile for the given target") println!(" Builds and runs the project. Only available for kind = \"bin\".");
.item("--force", "Force a full rebuild") println!(" Arguments after `--` are passed to the built binary (cargo-style).");
.item("--clean", "Clean before building") println!();
.item("--verbose", "Print detailed build output") printc("OPTIONS:", BOLD_GREEN);
.item("-- <args>...", "Arguments forwarded to the binary"); println!(" --debug Run with debug profile (default)");
help.render(); println!(" --release Run with release profile");
println!(" --target <triple> Cross-compile for the given target");
println!(" --force Force a full rebuild");
println!(" --clean Clean before building");
println!(" --verbose Print detailed build output");
println!(" -- <args>... Arguments forwarded to the binary");
return 0; return 0;
} }
@@ -119,7 +125,11 @@ pub fn run(args: &[String]) -> i32 {
let build_status = build(&args_for_build(&flags)); let build_status = build(&args_for_build(&flags));
if build_status == 0 { if build_status == 0 {
let display = display_run_cmd(&cmd, &flags.bin_args); let display = display_run_cmd(&cmd, &flags.bin_args);
crate::utils::output::status("run", display); println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
return run_shell_with_args(&cmd, &flags.bin_args); return run_shell_with_args(&cmd, &flags.bin_args);
} }
return build_status; return build_status;
@@ -255,11 +265,19 @@ fn run_project(
if build_status == 0 { if build_status == 0 {
if let Some(cmd) = run_cmd { if let Some(cmd) = run_cmd {
let display = display_run_cmd(&cmd, &flags.bin_args); let display = display_run_cmd(&cmd, &flags.bin_args);
crate::utils::output::status("run", display); println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
return Ok(run_shell_with_args(&cmd, &flags.bin_args)); return Ok(run_shell_with_args(&cmd, &flags.bin_args));
} }
let display = display_bin_run(&bin_path, &flags.bin_args); let display = display_bin_run(&bin_path, &flags.bin_args);
crate::utils::output::status("run", display); println!(
" {} {}",
colored(&format!("{:<9}", "run"), BOLD_GREEN),
display
);
return Ok(run_binary( return Ok(run_binary(
project_name, project_name,
&flags.profile, &flags.profile,

View File

@@ -15,31 +15,32 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
/// Sets up DCR registries by loading the registry manager and printing the list of registries. /// Sets up DCR registries by loading the registry manager and printing the list of registries.
pub fn setup(args: &[String]) -> i32 { pub fn setup(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr setup"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE").example("dcr setup"); printc(" dcr setup", BOLD_CYAN);
help.description("Show configured package registries and their priorities."); println!();
help.render(); printc("DESCRIPTION:", BOLD_GREEN);
println!(" Sets up DCR registries. Downloads and indexes");
println!(" package registries for dependency resolution.");
return 0; return 0;
} }
println!("Setting up DCR registries...");
// Load and process the registries, returning 0 on success or 1 on error // Load and process the registries, returning 0 on success or 1 on error
match crate::core::registry::RegistryManager::load() { match crate::core::registry::RegistryManager::load() {
Ok(manager) => { Ok(manager) => {
let mut lines = vec![format!( println!("Loaded {} registries.", manager.config.registry.len());
"Loaded {} registries (configured)",
manager.config.registry.len()
)];
for (name, reg) in manager.config.registry { for (name, reg) in manager.config.registry {
lines.push(format!(" {name}: {} (priority {})", reg.url, reg.priority)); println!("- {}: {} (priority {})", name, reg.url, reg.priority);
} }
crate::say!("{}", lines.join("\n"));
0 0
} }
Err(e) => { Err(e) => {
crate::utils::output::error!("{e}"); println!("Error: {}", e);
1 1
} }
} }

View File

@@ -25,29 +25,32 @@ use crate::utils::build::{
get_string_with_profile, resolve_compiler, resolve_pkg_config_flags, get_string_with_profile, resolve_compiler, resolve_pkg_config_flags,
}; };
use crate::utils::fs::{find_project_root, with_dir}; use crate::utils::fs::{find_project_root, with_dir};
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, BOLD_RED, RESET, colored, printc};
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::Command; use std::process::Command;
const BOLD_BLUE: &str = "\x1b[1m\x1b[94m";
/// Runs project tests and prints a unified testsuite report. /// Runs project tests and prints a unified testsuite report.
pub fn test(args: &[String]) -> i32 { pub fn test(args: &[String]) -> i32 {
let mut init_header = false; let mut init_header = false;
let mut profile = "debug"; let mut profile = "debug";
for arg in args { for arg in args {
if arg == "--help" { if arg == "--help" {
let mut help = crate::utils::help::HelpBuilder::new("dcr test"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE") printc(" dcr test [--init] [--debug | --release]", BOLD_CYAN);
.example("dcr test [--init] [--debug | --release]"); println!();
help.section("ALIASES").example("dcr tests"); printc("ALIASES:", BOLD_GREEN);
help.description("Runs project tests and prints a unified testsuite report."); println!(" dcr tests");
help.section("OPTIONS") println!();
.item("--init", "Create tests/dcr_test.h in current project") printc("DESCRIPTION:", BOLD_GREEN);
.item( println!(" Runs project tests and prints a unified testsuite report.");
"--debug", println!();
"Build and run tests with debug profile (default)", printc("OPTIONS:", BOLD_GREEN);
) println!(" --init Create tests/dcr_test.h in current project");
.item("--release", "Build and run tests with release profile"); println!(" --debug Build and run tests with debug profile (default)");
help.render(); println!(" --release Build and run tests with release profile");
return 0; return 0;
} }
if arg == "--init" { if arg == "--init" {
@@ -177,7 +180,7 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
stdout.push_str(&String::from_utf8_lossy(&out.stdout)); stdout.push_str(&String::from_utf8_lossy(&out.stdout));
let stderr = String::from_utf8_lossy(&out.stderr); let stderr = String::from_utf8_lossy(&out.stderr);
if !stderr.trim().is_empty() { if !stderr.trim().is_empty() {
crate::utils::output::compiler_output("stderr", &stderr); eprint!("{}", stderr);
} }
if !out.status.success() { if !out.status.success() {
suite_success = false; suite_success = false;
@@ -192,19 +195,19 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
for line in stdout.lines() { for line in stdout.lines() {
let line = line.trim(); let line = line.trim();
if let Some(name) = line.strip_prefix("[PASS] ") { if let Some(name) = line.strip_prefix("[PASS] ") {
crate::utils::output::success(format!("Passed {name}")); println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
pass += 1; pass += 1;
parsed_any = true; parsed_any = true;
continue; continue;
} }
if let Some(name) = line.strip_prefix("[SKIP] ") { if let Some(name) = line.strip_prefix("[SKIP] ") {
crate::utils::output::status("Skipped", name); println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
skip += 1; skip += 1;
parsed_any = true; parsed_any = true;
continue; continue;
} }
if let Some(name) = line.strip_prefix("[FAIL] ") { if let Some(name) = line.strip_prefix("[FAIL] ") {
crate::utils::output::error(format!("Failed {name}")); println!("{} {}", colored("[FAIL]", BOLD_RED), name);
fail += 1; fail += 1;
parsed_any = true; parsed_any = true;
continue; continue;
@@ -212,17 +215,17 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
if let Some((status, name)) = line.split_once('\t') { if let Some((status, name)) = line.split_once('\t') {
match status { match status {
"PASS" => { "PASS" => {
crate::utils::output::success(format!("Passed {name}")); println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
pass += 1; pass += 1;
parsed_any = true; parsed_any = true;
} }
"SKIP" => { "SKIP" => {
crate::utils::output::status("Skipped", name); println!("{} {}", colored("[SKIP]", BOLD_BLUE), name);
skip += 1; skip += 1;
parsed_any = true; parsed_any = true;
} }
"FAIL" => { "FAIL" => {
crate::utils::output::error(format!("Failed {name}")); println!("{} {}", colored("[FAIL]", BOLD_RED), name);
fail += 1; fail += 1;
parsed_any = true; parsed_any = true;
} }
@@ -234,11 +237,11 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
if !parsed_any { if !parsed_any {
if suite_success { if suite_success {
for name in &declared { for name in &declared {
crate::utils::output::success(format!("Passed {name}")); println!("{} {}", colored("[PASS]", BOLD_GREEN), name);
} }
pass = declared.len() as i32; pass = declared.len() as i32;
} else { } else {
crate::utils::output::error("testsuite failed"); println!("{} testsuite", colored("[FAIL]", BOLD_RED));
fail = 1; fail = 1;
} }
} }
@@ -249,10 +252,15 @@ fn run_testsuite(profile: &str) -> Result<i32, String> {
declared.len() as i32 declared.len() as i32
}; };
crate::utils::output::status( println!();
"test result:", println!("{}", colored("=====================", BOLD_GREEN));
format!("{pass} passed; {fail} failed; {skip} skipped; {total} total"), println!("{}", colored(" Testsuite summary ", BOLD_GREEN));
); println!("{}", colored("=====================", BOLD_GREEN));
println!("TOTAL: {}", total);
print_field("PASS", pass, BOLD_GREEN);
print_field("SKIP", skip, BOLD_BLUE);
print_field("FAIL", fail, BOLD_RED);
println!("{}", colored("=====================", BOLD_GREEN));
if fail > 0 || !suite_success { if fail > 0 || !suite_success {
return Ok(1); return Ok(1);
@@ -350,10 +358,10 @@ fn link_test_binary(
/// Prints the stdout and stderr from a command execution. /// Prints the stdout and stderr from a command execution.
fn print_command_output(output: &std::process::Output) { fn print_command_output(output: &std::process::Output) {
if !output.stdout.is_empty() { if !output.stdout.is_empty() {
crate::utils::output::write_stdout(&format!("{}", String::from_utf8_lossy(&output.stdout))); print!("{}", String::from_utf8_lossy(&output.stdout));
} }
if !output.stderr.is_empty() { if !output.stderr.is_empty() {
crate::utils::output::compiler_output("stderr", &String::from_utf8_lossy(&output.stderr)); eprint!("{}", String::from_utf8_lossy(&output.stderr));
} }
} }
@@ -362,10 +370,10 @@ fn ensure_test_header() -> Result<(), String> {
fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?; fs::create_dir_all("./tests").map_err(|_| "Failed to create tests/".to_string())?;
fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H) fs::write("./tests/dcr_test.h", FILE_DCR_TEST_H)
.map_err(|_| "Failed to write tests/dcr_test.h".to_string())?; .map_err(|_| "Failed to write tests/dcr_test.h".to_string())?;
crate::utils::output::created("tests/dcr_test.h"); println!("Created tests/dcr_test.h");
fs::write("./tests/test.c", FILE_TEST_C) fs::write("./tests/test.c", FILE_TEST_C)
.map_err(|_| "Failed to write tests/test.c".to_string())?; .map_err(|_| "Failed to write tests/test.c".to_string())?;
crate::utils::output::created("tests/test.c"); println!("Created tests/test.c");
Ok(()) Ok(())
} }
@@ -385,3 +393,12 @@ fn extract_test_names_path(path: &Path) -> Vec<String> {
} }
names names
} }
/// Prints a test result field with color coding.
fn print_field(label: &str, value: i32, color: &str) {
if value == 0 {
println!("{}: {}", label, value);
} else {
println!("{}{}: {}{}", color, label, value, RESET);
}
}

View File

@@ -18,6 +18,7 @@
use crate::prelude::*; use crate::prelude::*;
use crate::core::build_config::Config; use crate::core::build_config::Config;
use crate::utils::text::{BOLD_CYAN, BOLD_GREEN, printc};
use std::collections::HashSet; use std::collections::HashSet;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use toml::Value; use toml::Value;
@@ -28,10 +29,11 @@ use toml::Value;
/// Otherwise, it parses the dcr.toml configuration and prints the dependency tree. /// Otherwise, it parses the dcr.toml configuration and prints the dependency tree.
pub fn tree(args: &[String]) -> i32 { pub fn tree(args: &[String]) -> i32 {
if args.first().is_some_and(|a| a == "--help") { if args.first().is_some_and(|a| a == "--help") {
let mut help = crate::utils::help::HelpBuilder::new("dcr tree"); printc("USAGE:", BOLD_GREEN);
help.section("USAGE").example("dcr tree"); printc(" dcr tree", BOLD_CYAN);
help.description("Displays the dependency tree of the current project."); println!();
help.render(); printc("DESCRIPTION:", BOLD_GREEN);
println!(" Displays the dependency tree of the current project.");
return 0; return 0;
} }
@@ -53,7 +55,7 @@ pub fn tree(args: &[String]) -> i32 {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("0.0.0"); .unwrap_or("0.0.0");
crate::utils::output::write_stdout_raw(&format!("{} v{}\n", name, version)); printc(&format!("{} v{}", name, version), BOLD_CYAN);
let mut seen = HashSet::new(); let mut seen = HashSet::new();
seen.insert(name.to_string()); seen.insert(name.to_string());
@@ -135,10 +137,7 @@ fn print_deps(
}; };
} }
crate::utils::output::write_stdout_raw(&format!( println!("{}{}{}{}", prefix, connector, name, version);
"{}{}{}{}\n",
prefix, connector, name, version
));
// Recurse only if sub-dependencies exist and this dep hasn't been seen // Recurse only if sub-dependencies exist and this dep hasn't been seen
if let Some(s_deps) = sub_deps if let Some(s_deps) = sub_deps

View File

@@ -609,14 +609,10 @@ fn build_project_at_inner(
tc_as.as_deref(), tc_as.as_deref(),
); );
let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref()); let resolved_linker = resolve_tool("DCR_LD", tc_ld.as_deref());
// The host triple is still used for native target-specific configuration and prepend_clang_target_flag(&mut build_cflags, build_target, &resolved_compiler);
// artifact paths, but native Clang must select the platform SDK implicitly.
// Pass --target only when the user/config explicitly requested a target.
let compiler_target = if explicit_target { build_target } else { None };
prepend_clang_target_flag(&mut build_cflags, compiler_target, &resolved_compiler);
prepend_clang_target_flag( prepend_clang_target_flag(
&mut build_ldflags, &mut build_ldflags,
compiler_target, build_target,
resolved_linker.as_deref().unwrap_or(&resolved_compiler), resolved_linker.as_deref().unwrap_or(&resolved_compiler),
); );
let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref()); let resolved_archiver = resolve_tool("DCR_AR", tc_ar.as_deref());

View File

@@ -29,15 +29,10 @@ use prelude::*;
/// appropriate subcommand handler. With fewer than two arguments, shows help and /// appropriate subcommand handler. With fewer than two arguments, shows help and
/// exits with code 0; otherwise exits with the handler's exit code. /// exits with code 0; otherwise exits with the handler's exit code.
fn main() { fn main() {
let args = match utils::output::configure(std::env::args().collect()) {
Ok(args) => args,
Err(message) => {
utils::output::emit("error", &message);
std::process::exit(2);
}
};
utils::log::init(); utils::log::init();
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 { if args.len() < 2 {
std::process::exit(cli::help::help()); std::process::exit(cli::help::help());
} }

View File

@@ -58,11 +58,11 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
macos::bin_path(profile, name, target_dir) return macos::bin_path(profile, name, target_dir);
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
windows::bin_path(profile, name, target_dir) return windows::bin_path(profile, name, target_dir);
} }
#[cfg(any( #[cfg(any(
target_os = "freebsd", target_os = "freebsd",
@@ -71,7 +71,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly" target_os = "dragonfly"
))] ))]
{ {
bsd::bin_path(profile, name, target_dir) return bsd::bin_path(profile, name, target_dir);
} }
#[cfg(not(any( #[cfg(not(any(
target_os = "linux", target_os = "linux",
@@ -83,7 +83,7 @@ pub fn bin_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly" target_os = "dragonfly"
)))] )))]
{ {
posix::bin_path(profile, name, target_dir) return posix::bin_path(profile, name, target_dir);
} }
} }
@@ -97,11 +97,11 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
macos::elf_path(profile, name, target_dir) return macos::elf_path(profile, name, target_dir);
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
windows::elf_path(profile, name, target_dir) return windows::elf_path(profile, name, target_dir);
} }
#[cfg(any( #[cfg(any(
target_os = "freebsd", target_os = "freebsd",
@@ -110,7 +110,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly" target_os = "dragonfly"
))] ))]
{ {
bsd::elf_path(profile, name, target_dir) return bsd::elf_path(profile, name, target_dir);
} }
#[cfg(not(any( #[cfg(not(any(
target_os = "linux", target_os = "linux",
@@ -122,7 +122,7 @@ pub fn elf_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly" target_os = "dragonfly"
)))] )))]
{ {
posix::elf_path(profile, name, target_dir) return posix::elf_path(profile, name, target_dir);
} }
} }
@@ -136,11 +136,11 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
macos::efi_path(profile, name, target_dir) return macos::efi_path(profile, name, target_dir);
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
windows::efi_path(profile, name, target_dir) return windows::efi_path(profile, name, target_dir);
} }
#[cfg(any( #[cfg(any(
target_os = "freebsd", target_os = "freebsd",
@@ -149,7 +149,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly" target_os = "dragonfly"
))] ))]
{ {
bsd::efi_path(profile, name, target_dir) return bsd::efi_path(profile, name, target_dir);
} }
#[cfg(not(any( #[cfg(not(any(
target_os = "linux", target_os = "linux",
@@ -161,7 +161,7 @@ pub fn efi_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly" target_os = "dragonfly"
)))] )))]
{ {
posix::efi_path(profile, name, target_dir) return posix::efi_path(profile, name, target_dir);
} }
} }
@@ -175,11 +175,11 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
macos::lib_path(profile, name, target_dir) return macos::lib_path(profile, name, target_dir);
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
windows::lib_path(profile, name, target_dir) return windows::lib_path(profile, name, target_dir);
} }
#[cfg(any( #[cfg(any(
target_os = "freebsd", target_os = "freebsd",
@@ -188,7 +188,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly" target_os = "dragonfly"
))] ))]
{ {
bsd::lib_path(profile, name, target_dir) return bsd::lib_path(profile, name, target_dir);
} }
#[cfg(not(any( #[cfg(not(any(
target_os = "linux", target_os = "linux",
@@ -200,7 +200,7 @@ pub fn lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> String {
target_os = "dragonfly" target_os = "dragonfly"
)))] )))]
{ {
posix::lib_path(profile, name, target_dir) return posix::lib_path(profile, name, target_dir);
} }
} }
@@ -214,11 +214,11 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
macos::shared_lib_path(profile, name, target_dir) return macos::shared_lib_path(profile, name, target_dir);
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
windows::shared_lib_path(profile, name, target_dir) return windows::shared_lib_path(profile, name, target_dir);
} }
#[cfg(any( #[cfg(any(
target_os = "freebsd", target_os = "freebsd",
@@ -227,7 +227,7 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
target_os = "dragonfly" target_os = "dragonfly"
))] ))]
{ {
bsd::shared_lib_path(profile, name, target_dir) return bsd::shared_lib_path(profile, name, target_dir);
} }
#[cfg(not(any( #[cfg(not(any(
target_os = "linux", target_os = "linux",
@@ -239,6 +239,6 @@ pub fn shared_lib_path(profile: &str, name: &str, target_dir: Option<&str>) -> S
target_os = "dragonfly" target_os = "dragonfly"
)))] )))]
{ {
posix::shared_lib_path(profile, name, target_dir) return posix::shared_lib_path(profile, name, target_dir);
} }
} }

View File

@@ -782,13 +782,6 @@ mod tests {
assert_eq!(flags, vec!["-O2".to_string()]); assert_eq!(flags, vec!["-O2".to_string()]);
} }
#[test]
fn native_clang_target_is_not_added_without_explicit_target() {
let mut flags = vec!["-O3".to_string()];
prepend_clang_target_flag(&mut flags, None, "clang");
assert_eq!(flags, vec!["-O3".to_string()]);
}
/// Test for parse_version_parts function. /// Test for parse_version_parts function.
#[test] #[test]
fn parse_version_parts() { fn parse_version_parts() {

View File

@@ -1,124 +0,0 @@
use super::cli_styles::{BOLD_CYAN, BOLD_GREEN, Colorize};
use super::output;
pub struct HelpBuilder {
title: String,
description: Option<String>,
sections: Vec<Section>,
tip: Option<String>,
}
pub struct HelpSection<'a> {
help: &'a mut HelpBuilder,
index: usize,
}
struct Section {
title: String,
entries: Vec<(String, String)>,
}
impl HelpBuilder {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
description: None,
sections: Vec::new(),
tip: None,
}
}
pub fn description(&mut self, text: impl Into<String>) -> &mut Self {
self.description = Some(text.into());
self
}
/// Add an example to a dedicated `EXAMPLES` section.
pub fn example(&mut self, text: impl Into<String>) -> &mut Self {
let text = text.into();
if let Some(section) = self.sections.iter_mut().find(|s| s.title == "EXAMPLES") {
section.entries.push((text, String::new()));
} else {
self.sections.push(Section {
title: "EXAMPLES".to_owned(),
entries: vec![(text, String::new())],
});
}
self
}
pub fn note(&mut self, text: impl Into<String>) -> &mut Self {
let text = text.into();
match &mut self.description {
Some(description) if !description.is_empty() => {
description.push('\n');
description.push_str(&text);
}
Some(description) => description.push_str(&text),
None => self.description = Some(text),
}
self
}
pub fn section(&mut self, title: impl Into<String>) -> HelpSection<'_> {
self.sections.push(Section {
title: title.into(),
entries: Vec::new(),
});
HelpSection {
index: self.sections.len() - 1,
help: self,
}
}
pub fn tip(&mut self, text: impl Into<String>) -> &mut Self {
self.tip = Some(text.into());
self
}
pub fn render(&self) {
if output::mode() == output::Mode::Json {
let sections: Vec<_> = self.sections.iter().map(|s| serde_json::json!({"title": s.title, "items": s.entries.iter().map(|(n,d)| serde_json::json!({"name":n,"description":d})).collect::<Vec<_>>() })).collect();
let event = serde_json::json!({
"schema_version": 1,
"event": "help",
"title": self.title,
"description": self.description,
"sections": sections,
"tip": self.tip
});
output::write_stdout_raw(&format!("{event}\n"));
return;
}
let mut rendered = String::new();
rendered.push_str(&format!("{}\n", self.title.style(BOLD_GREEN)));
if let Some(d) = &self.description {
rendered.push_str(&format!("{}\n\n", d));
}
for s in &self.sections {
rendered.push_str(&format!("{}:\n", s.title.style(BOLD_GREEN)));
let width = s.entries.iter().map(|(n, _)| n.len()).max().unwrap_or(0);
for (n, d) in &s.entries {
if d.is_empty() {
rendered.push_str(&format!(" {}\n", n.style(BOLD_CYAN)));
} else {
rendered.push_str(&format!(
" {} {d}\n",
format!("{n:<width$}").style(BOLD_CYAN)
));
}
}
rendered.push('\n');
}
if let Some(t) = &self.tip {
rendered.push_str(&format!("{}:\n {t}\n", "TIP".style(BOLD_GREEN)));
}
output::write_stdout(&rendered);
}
}
impl HelpSection<'_> {
pub fn item(self, name: impl Into<String>, description: impl Into<String>) -> Self {
self.help.sections[self.index]
.entries
.push((name.into(), description.into()));
self
}
pub fn example(self, command: impl Into<String>) -> Self {
self.item(command, "")
}
}

View File

@@ -15,37 +15,30 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use log::{LevelFilter, Log, Metadata, Record}; use env_logger::{Builder, WriteStyle};
use log::{Level, LevelFilter};
struct CliLogger; use owo_colors::OwoColorize;
use std::io::Write;
impl Log for CliLogger {
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
metadata.level().to_level_filter() <= log::max_level()
}
fn log(&self, record: &Record<'_>) {
if !self.enabled(record.metadata()) {
return;
}
let kind = match record.level() {
log::Level::Warn => "warning",
log::Level::Error => "error",
log::Level::Info => "info",
log::Level::Debug => "debug",
log::Level::Trace => "trace",
};
crate::utils::output::emit(kind, &record.args().to_string());
}
fn flush(&self) {}
}
// A function for initializing the logging lib with basic settings. // A function for initializing the logging lib with basic settings.
pub fn init() { pub fn init() {
static LOGGER: CliLogger = CliLogger; Builder::new()
log::set_logger(&LOGGER).expect("logger already initialized"); .filter_level(LevelFilter::Warn)
log::set_max_level(if crate::utils::output::verbose() { .write_style(WriteStyle::Auto)
LevelFilter::Debug .format(|buf, record| {
} else { let colored_lv = match record.level() {
LevelFilter::Info Level::Error => record.level().red().to_string(),
}); Level::Warn => record.level().yellow().to_string(),
Level::Info => record.level().default_color().to_string(),
_ => record.level().purple().italic().to_string(),
};
writeln!(
buf,
"{}: {}",
colored_lv.to_lowercase().bold(),
record.args(),
)
})
.init();
} }

View File

@@ -29,6 +29,4 @@ pub mod git;
pub mod log; pub mod log;
/// Text processing and manipulation helpers. /// Text processing and manipulation helpers.
pub mod cli_styles; pub mod text;
pub mod help;
pub mod output;

View File

@@ -1,535 +0,0 @@
//! Central rendering policy for user-facing CLI events.
use std::io::{self, IsTerminal, Write};
use std::sync::OnceLock;
#[derive(Clone, Copy, Default, PartialEq)]
pub enum Mode {
#[default]
Human,
Plain,
Json,
}
#[derive(Default)]
pub struct Options {
pub mode: Mode,
pub quiet: bool,
pub verbose: bool,
pub color: Option<bool>,
}
static OPTIONS: OnceLock<Options> = OnceLock::new();
/// Consume global options without touching arguments after `--`.
pub fn configure(args: Vec<String>) -> Result<Vec<String>, String> {
let mut options = Options::default();
let mut remaining = Vec::new();
let mut args = args.into_iter();
if let Some(program) = args.next() {
remaining.push(program);
}
while let Some(arg) = args.next() {
if arg == "--" {
remaining.push(arg);
remaining.extend(args);
break;
}
match arg.as_str() {
"--quiet" => options.quiet = true,
"--verbose" => options.verbose = true,
"--output" | "--color" => {
let value = args
.next()
.ok_or_else(|| format!("{arg} requires a value"))?;
set_option(&mut options, &arg, &value)?;
}
_ if arg.starts_with("--output=") || arg.starts_with("--color=") => {
let (key, value) = arg.split_once('=').unwrap();
set_option(&mut options, key, value)?;
}
_ => {
remaining.push(arg);
remaining.extend(args);
break;
}
}
}
OPTIONS
.set(options)
.map_err(|_| "output already configured".to_owned())?;
// Help is styled before it reaches the output writer. Keep owo-colors'
// global decision in sync with DCR's explicit color policy; otherwise
// `--color always` can color the DCR banner but leave help sections plain.
let configured = OPTIONS.get().expect("output options were just configured");
let force_color = configured.color.or_else(|| {
if std::env::var_os("NO_COLOR").is_some() {
Some(false)
} else {
None
}
});
if let Some(enabled) = force_color {
owo_colors::set_override(enabled);
}
Ok(remaining)
}
fn set_option(options: &mut Options, key: &str, value: &str) -> Result<(), String> {
match (key, value) {
("--output", "human") => options.mode = Mode::Human,
("--output", "plain") => options.mode = Mode::Plain,
("--output", "json") => options.mode = Mode::Json,
("--color", "auto") => options.color = None,
("--color", "always") => options.color = Some(true),
("--color", "never") => options.color = Some(false),
_ => return Err(format!("invalid value `{value}` for {key}")),
}
Ok(())
}
pub fn emit(kind: &str, message: &str) {
let defaults = Options::default();
let options = OPTIONS.get().unwrap_or(&defaults);
if options.quiet && !is_quiet_visible(kind) {
return;
}
if matches!(kind, "debug" | "trace") && !options.verbose {
return;
}
let mut stderr = io::stderr().lock();
let _ = render(
&mut stderr,
options,
kind,
message,
io::stderr().is_terminal(),
);
}
/// Emit a structured event while retaining the human-readable diagnostic form.
pub fn emit_event(event: &str, fields: serde_json::Value, human: &str) {
let defaults = Options::default();
let options = OPTIONS.get().unwrap_or(&defaults);
if options.quiet && !is_quiet_visible(event) {
return;
}
if options.mode == Mode::Json {
let mut object = match fields {
serde_json::Value::Object(object) => object,
_ => serde_json::Map::new(),
};
object.insert("schema_version".into(), serde_json::json!(1));
object.insert("event".into(), serde_json::json!(event));
let mut stderr = io::stderr().lock();
let _ = writeln!(stderr, "{}", serde_json::Value::Object(object));
} else if event == "compiler_output" {
emit("compiler_output", human);
} else {
emit("build", human);
}
}
fn is_quiet_visible(kind: &str) -> bool {
matches!(kind, "error" | "warning" | "compiler_output")
}
pub fn mode() -> Mode {
OPTIONS.get().map_or(Mode::Human, |options| options.mode)
}
pub fn verbose() -> bool {
OPTIONS.get().is_some_and(|options| options.verbose)
}
pub fn colors_enabled(terminal: bool) -> bool {
let defaults = Options::default();
let options = OPTIONS.get().unwrap_or(&defaults);
colors_enabled_for(options, terminal)
}
fn colors_enabled_for(options: &Options, terminal: bool) -> bool {
options.mode == Mode::Human
&& options
.color
.unwrap_or(terminal && std::env::var_os("NO_COLOR").is_none())
}
pub fn progress_label(action: &str, subject: &str) -> String {
let text = format!("{action:<12} {subject}");
if !colors_enabled(true) {
return text;
}
let code = match action.to_ascii_lowercase().as_str() {
"project" | "target" | "dep" | "pack" | "ready" | "profile" => 36,
_ => 32,
};
format!("\x1b[1;{code}m{action:>12}\x1b[0m {subject}")
}
/// A compact fluent writer for user-facing status output.
///
/// Commands can build a complete message without repeating the output macro:
/// `output::ui().status("Compiling", name).detail("release").finish();`
#[allow(dead_code)]
pub struct Ui {
lines: Vec<String>,
}
#[allow(dead_code)]
pub fn ui() -> Ui {
Ui { lines: Vec::new() }
}
pub fn say(message: impl AsRef<str>) {
write_stdout(&format!("{}\n", message.as_ref()));
}
pub fn status(action: impl AsRef<str>, subject: impl AsRef<str>) {
emit(
"status",
&format!("{} {}", action.as_ref(), subject.as_ref()),
);
}
pub fn success(message: impl AsRef<str>) {
emit("success", message.as_ref());
}
pub fn warning(message: impl AsRef<str>) {
emit("warning", message.as_ref());
}
pub fn info(message: impl AsRef<str>) {
emit("info", message.as_ref());
}
pub fn error(message: impl AsRef<str>) {
emit("error", message.as_ref());
}
pub fn created(subject: impl AsRef<str>) {
emit("success", &format!("Created {}", subject.as_ref()));
}
pub fn removed(subject: impl AsRef<str>) {
emit("success", &format!("Removed {}", subject.as_ref()));
}
pub fn compiler_output(stream: &str, text: &str) {
if mode() == Mode::Json {
emit_event(
"compiler_output",
serde_json::json!({"stream": stream, "text": text}),
"",
);
} else if stream == "stderr" {
emit("compiler_output", text);
} else {
write_stdout(text);
}
}
#[macro_export]
macro_rules! say { () => { $crate::utils::output::say("") }; ($($arg:tt)*) => { $crate::utils::output::say(format!($($arg)*)) }; }
#[macro_export]
macro_rules! status { ($action:expr, $($arg:tt)*) => { $crate::utils::output::status($action, format!($($arg)*)) }; }
#[macro_export]
macro_rules! success { ($($arg:tt)*) => { $crate::utils::output::success(format!($($arg)*)) }; }
impl Ui {
#[allow(dead_code)]
pub fn status(mut self, action: impl Into<String>, subject: impl Into<String>) -> Self {
self.lines
.push(format!("{} {}", action.into(), subject.into()));
self
}
#[allow(dead_code)]
pub fn detail(mut self, detail: impl Into<String>) -> Self {
if let Some(line) = self.lines.last_mut() {
line.push_str(&format!(" ({})", detail.into()));
}
self
}
#[allow(dead_code)]
pub fn item(mut self, item: impl Into<String>) -> Self {
self.lines.push(format!(" {}", item.into()));
self
}
#[allow(dead_code)]
pub fn finish(self) {
emit("status", &self.lines.join("\n"));
}
}
#[macro_export]
macro_rules! output_warning {
($($arg:tt)*) => { $crate::utils::output::emit("warning", &format!($($arg)*)) };
}
#[macro_export]
macro_rules! output_error {
($($arg:tt)*) => { $crate::utils::output::emit("error", &format!($($arg)*)) };
}
#[macro_export]
macro_rules! output_info {
($($arg:tt)*) => { $crate::utils::output::emit("info", &format!($($arg)*)) };
}
#[macro_export]
macro_rules! output_success {
($($arg:tt)*) => { $crate::utils::output::emit("success", &format!($($arg)*)) };
}
// Namespaced aliases avoid collisions with the technical `log` macros.
#[allow(unused_imports)]
pub use crate::{
output_error as error, output_info as info, output_success as success,
output_warning as warning,
};
/// User-facing stdout macros. Data/help output belongs on stdout; diagnostics
/// and build status use `emit` on stderr.
#[macro_export]
macro_rules! output_println {
() => { $crate::utils::output::write_stdout("\n") };
($($arg:tt)*) => { $crate::utils::output::write_stdout(&format!("{}\n", format_args!($($arg)*))) };
}
#[macro_export]
macro_rules! output_print {
($($arg:tt)*) => { $crate::utils::output::write_stdout(&format!($($arg)*)) };
}
pub fn write_stdout(text: &str) {
let defaults = Options::default();
let options = OPTIONS.get().unwrap_or(&defaults);
let colored = colors_enabled(io::stdout().is_terminal());
let has_ansi = text.contains('\x1b');
let rendered = if colored || !has_ansi {
text.to_owned()
} else {
strip_ansi(text)
};
let mut stdout = io::stdout().lock();
let result = if options.mode == Mode::Json {
writeln!(
stdout,
"{}",
serde_json::json!({"schema_version": 1, "event": "output", "text": rendered})
)
} else {
stdout.write_all(rendered.as_bytes())
};
if let Err(error) = result
&& error.kind() == io::ErrorKind::BrokenPipe
{
std::process::exit(0);
}
}
pub fn write_stdout_raw(text: &str) {
let mut stdout = io::stdout().lock();
if let Err(error) = stdout.write_all(text.as_bytes())
&& error.kind() == io::ErrorKind::BrokenPipe
{
std::process::exit(0);
}
}
fn strip_ansi(text: &str) -> String {
let mut chars = text.chars().peekable();
let mut clean = String::new();
while let Some(ch) = chars.next() {
if ch == '\x1b' && chars.peek() == Some(&'[') {
chars.next();
for ch in chars.by_ref() {
if ('@'..='~').contains(&ch) {
break;
}
}
} else {
clean.push(ch);
}
}
clean
}
#[macro_export]
macro_rules! output_sprintln {
($style:expr, $($arg:tt)*) => {{
use $crate::utils::cli_styles::Colorize as _;
$crate::output_println!("{}", format!($($arg)*).style($style));
}};
}
/// Render a help heading through the same output policy as the rest of the CLI.
/// Keeping this separate from ordinary styled output makes help formatting easy
/// to change globally without touching every command.
#[macro_export]
macro_rules! output_help_heading {
($style:expr, $($arg:tt)*) => {{
use $crate::utils::cli_styles::Colorize as _;
$crate::utils::output::write_stdout(&format!("{}\n", format!($($arg)*).style($style)));
}};
}
#[macro_export]
macro_rules! output_sprint {
($style:expr, $($arg:tt)*) => {{
use $crate::utils::cli_styles::Colorize as _;
$crate::output_print!("{}", format!($($arg)*).style($style));
}};
}
fn render(
writer: &mut impl Write,
options: &Options,
kind: &str,
message: &str,
terminal: bool,
) -> io::Result<()> {
if options.mode == Mode::Json {
return writeln!(
writer,
"{}",
serde_json::json!({
"schema_version": 1, "event": kind, "message": message
})
);
}
let color = colors_enabled_for(options, terminal);
if kind == "compiler_output" {
let message = strip_ansi(message);
for line in message.lines() {
let code = if line.contains("error:") || line.contains("fatal error") {
31
} else if line.contains("warning:") {
33
} else if line.contains("note:") {
36
} else {
37
};
if color {
writeln!(writer, "\x1b[{code}m{line}\x1b[0m")?;
} else {
writeln!(writer, "{line}")?;
}
}
return Ok(());
}
if matches!(kind, "status" | "success" | "build") {
let message = strip_ansi(message);
let (action, subject) = message.split_once(' ').unwrap_or((&message, ""));
if color {
let code = match action.to_ascii_lowercase().as_str() {
"project" | "target" | "dep" | "pack" | "ready" | "profile" => 36,
"compile" | "compiling" | "created" | "creating" | "finished" | "done"
| "removed" | "initialized" | "added" | "checked" | "skipped" | "format"
| "formatting" | "lint" | "linting" | "run" | "up" | "loaded" => 32,
_ if kind == "success" => 32,
_ => 36,
};
return writeln!(writer, "\x1b[1;{code}m{action:>12}\x1b[0m {subject}");
}
return writeln!(writer, "{action:>12} {subject}");
}
if color {
let code = match kind {
"error" => 31,
"warning" => 33,
_ => 36,
};
writeln!(writer, "\x1b[1;{code}m{kind}:\x1b[0m {message}")
} else {
writeln!(writer, "{kind}: {}", strip_ansi(message))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn human_colors_status_action_and_diagnostics() {
let options = Options {
color: Some(true),
..Options::default()
};
for (kind, message, expected) in [
(
"status",
"Compiling hello",
"\x1b[1;32m Compiling\x1b[0m hello\n",
),
(
"success",
"Created project",
"\x1b[1;32m Created\x1b[0m project\n",
),
("warning", "notice", "\x1b[1;33mwarning:\x1b[0m notice\n"),
("error", "failed", "\x1b[1;31merror:\x1b[0m failed\n"),
] {
let mut bytes = Vec::new();
render(&mut bytes, &options, kind, message, false).unwrap();
assert_eq!(String::from_utf8(bytes).unwrap(), expected);
}
}
#[test]
fn disabled_color_strips_existing_styles() {
let options = Options {
color: Some(false),
..Options::default()
};
let mut bytes = Vec::new();
render(
&mut bytes,
&options,
"warning",
"\x1b[32mnotice\x1b[0m",
true,
)
.unwrap();
assert_eq!(String::from_utf8(bytes).unwrap(), "warning: notice\n");
}
#[test]
fn human_colors_compiler_diagnostics_by_severity() {
let options = Options {
color: Some(true),
..Options::default()
};
let mut bytes = Vec::new();
render(
&mut bytes,
&options,
"compiler_output",
"file.c:1:1: warning: unused value\nfile.c:2:1: error: invalid token",
true,
)
.unwrap();
let output = String::from_utf8(bytes).unwrap();
assert!(output.contains("\x1b[33mfile.c:1:1: warning: unused value\x1b[0m"));
assert!(output.contains("\x1b[31mfile.c:2:1: error: invalid token\x1b[0m"));
}
#[test]
fn plain_never_contains_ansi() {
let mut bytes = Vec::new();
let options = Options {
mode: Mode::Plain,
color: Some(true),
..Options::default()
};
render(&mut bytes, &options, "error", "compiler missing", true).unwrap();
assert_eq!(
String::from_utf8(bytes).unwrap(),
"error: compiler missing\n"
);
}
#[test]
fn json_is_versioned_and_escapes_multiline_messages() {
let mut bytes = Vec::new();
let options = Options {
mode: Mode::Json,
..Options::default()
};
render(&mut bytes, &options, "warning", "first\nsecond", false).unwrap();
let event: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert_eq!(event["schema_version"], 1);
assert_eq!(event["message"], "first\nsecond");
}
}

View File

@@ -15,33 +15,39 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
pub use owo_colors::{OwoColorize as Colorize, Style}; #[allow(dead_code)]
pub const RESET: &str = "\x1b[0m";
// Сli theme styles
// Main
#[allow(dead_code)] #[allow(dead_code)]
pub const BOLD_RED: Style = Style::new().bright_red().bold(); pub const BOLD: &str = "\x1b[1m";
#[allow(dead_code)]
pub const BOLD_GREEN: Style = Style::new().bright_green().bold();
#[allow(dead_code)]
pub const BOLD_YELLOW: Style = Style::new().bright_yellow().bold();
#[allow(dead_code)]
pub const BOLD_CYAN: Style = Style::new().bright_cyan().bold();
#[allow(dead_code)]
pub const BOLD_BLUE: Style = Style::new().bright_blue().bold();
// Help
#[allow(dead_code)] #[allow(dead_code)]
pub const HELP_SECTION_TITLE_ST: Style = BOLD_GREEN; pub const BRIGHT_RED: &str = "\x1b[91m";
#[allow(dead_code)] #[allow(dead_code)]
pub const HELP_EXAMPLES_ST: Style = BOLD_CYAN; pub const BRIGHT_GREEN: &str = "\x1b[92m";
#[allow(dead_code)]
pub const BRIGHT_YELLOW: &str = "\x1b[93m";
#[allow(dead_code)]
pub const BRIGHT_CYAN: &str = "\x1b[96m";
#[allow(dead_code)]
pub const BOLD_RED: &str = "\x1b[1m\x1b[91m";
#[allow(dead_code)]
pub const BOLD_GREEN: &str = "\x1b[1m\x1b[92m";
#[allow(dead_code)]
pub const BOLD_YELLOW: &str = "\x1b[1m\x1b[93m";
#[allow(dead_code)]
pub const BOLD_CYAN: &str = "\x1b[1m\x1b[96m";
// Other /// Applies ANSI escape codes to format the message with the given style.
///
/// This is a utility for colored output in terminal applications.
#[allow(dead_code)] #[allow(dead_code)]
pub const ERROR_ST: Style = BOLD_RED; pub fn colored(msg: &str, style: &str) -> String {
format!("{style}{msg}{RESET}")
}
/// Prints `msg` to stdout with the given ANSI `style`.
#[allow(dead_code)] #[allow(dead_code)]
pub const SUCCESS_ST: Style = BOLD_GREEN; pub fn printc(msg: &str, style: &str) {
#[allow(dead_code)] println!("{style}{msg}{RESET}");
pub const ALERT_ST: Style = BOLD_YELLOW; }
#[allow(dead_code)]
pub const SKIP_ST: Style = BOLD_BLUE;

View File

@@ -20,15 +20,10 @@ fn dcr_test_runs_without_sandbox_dependency() {
assert!(out.status.success(), "dcr test should succeed"); assert!(out.status.success(), "dcr test should succeed");
let stdout = String::from_utf8_lossy(&out.stdout); let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr); let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stdout.contains("TOTAL: 1"), "TOTAL summary line missing");
assert!( assert!(
stderr.contains("test result: 1 passed; 0 failed; 0 skipped; 1 total"), stdout.contains("PASS: 1"),
"test result summary line missing\nstdout:\n{}\nstderr:\n{}", "PASS summary line missing\nstdout:\n{}\nstderr:\n{}",
stdout,
stderr
);
assert!(
stdout.contains("PASS") || stderr.contains("Passed"),
"test pass output missing\nstdout:\n{}\nstderr:\n{}",
stdout, stdout,
stderr stderr
); );