Skip to content

Commit 254140b

Browse files
committed
cli(run): unify runtime error rendering and fix extra blank line
- Render runtime 'error:/tip:' blocks using Vix styled ✖ / ➜ format - Deduplicate raw runtime output (stdout/stderr overlap) - Suppress libstdc++ uncaught-exception noise before styling - Track real runtime output separately from spinner activity - Prevent trailing empty line when only styled errors are printed - Align script runtime errors with global Vix error UX
1 parent 1be34a6 commit 254140b

4 files changed

Lines changed: 184 additions & 18 deletions

File tree

src/commands/RunCommand.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,8 +477,6 @@ namespace vix::commands::RunCommand
477477

478478
if (buildExit != 0)
479479
{
480-
return buildExit != 0 ? buildExit : 2;
481-
482480
if (!log.empty())
483481
{
484482
vix::cli::ErrorHandler::printBuildErrors(
@@ -498,6 +496,7 @@ namespace vix::commands::RunCommand
498496

499497
return buildExit != 0 ? buildExit : 2;
500498
}
499+
501500
#endif
502501
}
503502

src/commands/run/RunProcess.cpp

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,36 @@ namespace vix::commands::RunCommand::detail
8787
}
8888
}
8989

90+
static inline bool is_vix_error_tip_line(std::string_view line) noexcept
91+
{
92+
while (!line.empty() && (line.front() == ' ' || line.front() == '\t'))
93+
line.remove_prefix(1);
94+
95+
return (line.rfind("error:", 0) == 0) || (line.rfind("tip:", 0) == 0);
96+
}
97+
98+
static inline std::string drop_vix_error_tip_lines(const std::string &chunk)
99+
{
100+
std::string out;
101+
out.reserve(chunk.size());
102+
103+
std::size_t start = 0;
104+
while (start < chunk.size())
105+
{
106+
const std::size_t nl = chunk.find('\n', start);
107+
const std::size_t end = (nl == std::string::npos) ? chunk.size() : (nl + 1);
108+
109+
std::string_view line(&chunk[start], end - start);
110+
111+
if (!is_vix_error_tip_line(line))
112+
out.append(line.data(), line.size());
113+
114+
start = end;
115+
}
116+
117+
return out;
118+
}
119+
90120
static inline std::size_t utf8_safe_prefix_len(const std::string &s, std::size_t want)
91121
{
92122
if (want >= s.size())
@@ -787,13 +817,10 @@ namespace vix::commands::RunCommand::detail
787817
if (is_noise_line(line))
788818
continue;
789819

790-
// drop empty/whitespace-only lines created by filtering
791820
if (is_whitespace_only(line))
792821
continue;
793822

794823
out.append(line.data(), line.size());
795-
796-
out.append(line.data(), line.size());
797824
}
798825

799826
return out;
@@ -805,6 +832,7 @@ namespace vix::commands::RunCommand::detail
805832

806833
bool running = true;
807834
bool printedSomething = false;
835+
bool printedRealOutput = false;
808836
char lastPrintedChar = '\n';
809837

810838
int finalStatus = 0;
@@ -1016,11 +1044,14 @@ namespace vix::commands::RunCommand::detail
10161044

10171045
if (!filtered.empty())
10181046
{
1019-
if (!captureOnly)
1047+
std::string toPrint = drop_vix_error_tip_lines(filtered);
1048+
1049+
if (!toPrint.empty() && !captureOnly)
10201050
{
1021-
write_all(STDOUT_FILENO, filtered.data(), filtered.size());
1051+
write_all(STDOUT_FILENO, toPrint.data(), toPrint.size());
10221052
printedSomething = true;
1023-
lastPrintedChar = filtered.back();
1053+
printedRealOutput = true; //
1054+
lastPrintedChar = toPrint.back();
10241055
}
10251056
}
10261057
}
@@ -1069,7 +1100,7 @@ namespace vix::commands::RunCommand::detail
10691100
result.exitCode = haveStatus ? normalize_exit_code(finalStatus) : 1;
10701101

10711102
if (!captureOnly &&
1072-
printedSomething &&
1103+
printedRealOutput &&
10731104
lastPrintedChar != '\n' &&
10741105
::isatty(STDOUT_FILENO) != 0)
10751106
{

src/commands/run/RunScript.cpp

Lines changed: 132 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,100 @@ namespace vix::commands::RunCommand::detail
6060
log.find("interrupted by user") != std::string::npos;
6161
}
6262

63+
static inline std::string trim_copy(std::string s)
64+
{
65+
while (!s.empty() && (s.back() == '\n' || s.back() == '\r' || s.back() == ' ' || s.back() == '\t'))
66+
s.pop_back();
67+
68+
size_t i = 0;
69+
while (i < s.size() && (s[i] == '\n' || s[i] == '\r' || s[i] == ' ' || s[i] == '\t'))
70+
++i;
71+
72+
s.erase(0, i);
73+
return s;
74+
}
75+
76+
static bool handle_error_tip_block(const std::string &log)
77+
{
78+
const auto epos = log.find("error:");
79+
if (epos == std::string::npos)
80+
return false;
81+
82+
auto line_end = [&](size_t p) -> size_t
83+
{
84+
size_t n = log.find('\n', p);
85+
return (n == std::string::npos) ? log.size() : n;
86+
};
87+
88+
const size_t eend = line_end(epos);
89+
std::string eLine = log.substr(epos, eend - epos); // "error: ..."
90+
91+
std::string tipLine;
92+
const auto tpos = log.find("tip:", eend);
93+
if (tpos != std::string::npos)
94+
{
95+
const size_t tend = line_end(tpos);
96+
tipLine = log.substr(tpos, tend - tpos); // "tip: ..."
97+
}
98+
99+
auto strip_prefix = [](std::string s, const char *pref) -> std::string
100+
{
101+
if (s.rfind(pref, 0) == 0)
102+
s.erase(0, std::strlen(pref));
103+
while (!s.empty() && (s.front() == ' ' || s.front() == '\t'))
104+
s.erase(0, 1);
105+
return s;
106+
};
107+
108+
vix::cli::style::error(strip_prefix(eLine, "error:"));
109+
if (!tipLine.empty())
110+
vix::cli::style::hint(strip_prefix(tipLine, "tip:"));
111+
return true;
112+
}
113+
114+
static bool handle_error_tip_block_vix(const std::string &log)
115+
{
116+
const auto epos = log.find("error:");
117+
if (epos == std::string::npos)
118+
return false;
119+
120+
auto line_end = [&](size_t p) -> size_t
121+
{
122+
size_t n = log.find('\n', p);
123+
return (n == std::string::npos) ? log.size() : n;
124+
};
125+
126+
auto strip_prefix = [](std::string s, const char *pref) -> std::string
127+
{
128+
if (s.rfind(pref, 0) == 0)
129+
s.erase(0, std::strlen(pref));
130+
while (!s.empty() && (s.front() == ' ' || s.front() == '\t'))
131+
s.erase(0, 1);
132+
return s;
133+
};
134+
135+
const size_t eend = line_end(epos);
136+
std::string eLine = log.substr(epos, eend - epos); // "error: ..."
137+
138+
std::string tipLine;
139+
const auto tpos = log.find("tip:", eend);
140+
if (tpos != std::string::npos)
141+
{
142+
const size_t tend = line_end(tpos);
143+
tipLine = log.substr(tpos, tend - tpos); // "tip: ..."
144+
}
145+
146+
const std::string msg = strip_prefix(eLine, "error:");
147+
const std::string tip = tipLine.empty() ? "" : strip_prefix(tipLine, "tip:");
148+
149+
// ✅ Style Vix (comme tes autres erreurs)
150+
std::cerr << " " << RED << "" << RESET << " " << msg << "\n";
151+
if (!tip.empty())
152+
std::cerr << " " << GRAY << "" << RESET << " " << tip << "\n";
153+
154+
return true;
155+
}
156+
63157
int run_single_cpp(const Options &opt)
64158
{
65159
using namespace std;
@@ -236,37 +330,66 @@ namespace vix::commands::RunCommand::detail
236330
#ifndef _WIN32
237331
apply_sanitizer_env_if_needed(opt.enableSanitizers, opt.enableUbsanOnly);
238332

239-
auto rr = run_cmd_live_filtered_capture(cmdRun, "Running script", true, opt.timeoutSec);
333+
auto rr = run_cmd_live_filtered_capture(cmdRun, "Running script", false, opt.timeoutSec);
240334
runCode = normalize_exit_code(rr.exitCode);
241335

242336
if (runCode != 0)
243337
{
338+
std::string out = rr.stdoutText;
339+
std::string err = rr.stderrText;
340+
341+
const std::string outT = trim_copy(out);
342+
const std::string errT = trim_copy(err);
343+
344+
// Dédup: même contenu → on garde un seul
345+
if (!outT.empty() && outT == errT)
346+
{
347+
err.clear();
348+
}
349+
else if (!outT.empty() && !errT.empty())
350+
{
351+
// Si l’un contient l’autre, garde le plus “riche”
352+
if (outT.find(errT) != std::string::npos)
353+
err.clear();
354+
else if (errT.find(outT) != std::string::npos)
355+
out.clear();
356+
}
357+
244358
std::string runtimeLog;
245-
runtimeLog.reserve(rr.stdoutText.size() + rr.stderrText.size() + 1);
359+
runtimeLog.reserve(out.size() + err.size() + 1);
246360

247-
if (!rr.stdoutText.empty())
248-
runtimeLog += rr.stdoutText;
361+
if (!out.empty())
362+
runtimeLog += out;
249363

250-
if (!rr.stderrText.empty())
364+
if (!err.empty())
251365
{
252366
if (!runtimeLog.empty() && runtimeLog.back() != '\n')
253367
runtimeLog.push_back('\n');
254-
runtimeLog += rr.stderrText;
368+
runtimeLog += err;
255369
}
256370

257371
bool handled = false;
258372

259373
if (!runtimeLog.empty())
260374
{
261-
handled = vix::cli::errors::RawLogDetectors::handleRuntimeCrash(
262-
runtimeLog, script, "Script execution failed");
375+
handled = handle_error_tip_block_vix(runtimeLog);
376+
377+
if (!handled)
378+
{
379+
handled = vix::cli::errors::RawLogDetectors::handleRuntimeCrash(
380+
runtimeLog, script, "Script execution failed");
381+
}
263382

264383
if (!handled &&
265384
vix::cli::errors::RawLogDetectors::handleKnownRunFailure(runtimeLog, script))
385+
{
266386
handled = true;
387+
}
267388

268389
if (!handled)
269-
std::cout << runtimeLog << "\n";
390+
{
391+
std::cerr << runtimeLog << "\n";
392+
}
270393
}
271394

272395
handle_runtime_exit_code(runCode, "Script execution failed", /*alreadyHandled=*/handled);

src/errors/rules/UncaughtExceptionRule.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
/**
2+
*
3+
* @file UncaughtExceptionRule.cpp
4+
* @author Gaspard Kirira
5+
*
6+
* Copyright 2025, Gaspard Kirira. All rights reserved.
7+
* https://github.com/vixcpp/vix
8+
* Use of this source code is governed by a MIT license
9+
* that can be found in the License file.
10+
*
11+
* Vix.cpp
12+
*
13+
*/
114
#include <vix/cli/errors/rules/UncaughtExceptionRule.hpp>
215

316
#include <vix/cli/errors/CodeFrame.hpp>

0 commit comments

Comments
 (0)