2424#include < cstring>
2525#include < string>
2626#include < string_view>
27+ #include < iomanip>
28+ #include < sstream>
2729
2830#ifndef _WIN32
2931#include < errno.h>
3032#include < signal.h>
33+ #include < sys/ioctl.h>
3134#include < sys/resource.h>
3235#include < sys/select.h>
3336#include < sys/types.h>
@@ -277,6 +280,42 @@ namespace vix::commands::RunCommand::detail
277280 }
278281 }
279282
283+ std::size_t terminal_width () noexcept
284+ {
285+ struct winsize ws{};
286+
287+ if (::ioctl (STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0 )
288+ return static_cast <std::size_t >(ws.ws_col );
289+
290+ return 120 ;
291+ }
292+
293+ std::string format_seconds (long long milliseconds)
294+ {
295+ const double seconds = static_cast <double >(milliseconds) / 1000.0 ;
296+
297+ std::ostringstream out;
298+ out.setf (std::ios::fixed);
299+ out.precision (seconds >= 10.0 ? 1 : 2 );
300+ out << seconds << " s" ;
301+
302+ return out.str ();
303+ }
304+
305+ std::string truncate_terminal_line (const std::string &line, std::size_t width)
306+ {
307+ if (width == 0 )
308+ return " " ;
309+
310+ if (line.size () <= width)
311+ return line;
312+
313+ if (width <= 3 )
314+ return std::string (width, ' .' );
315+
316+ return line.substr (0 , width - 3 ) + " ..." ;
317+ }
318+
280319 std::size_t utf8_safe_prefix_len (const std::string &s, std::size_t want)
281320 {
282321 if (want >= s.size ())
@@ -385,13 +424,37 @@ namespace vix::commands::RunCommand::detail
385424
386425 bool is_cmake_configure_cmd (const std::string &cmd) noexcept
387426 {
388- const bool isCmake = (cmd.find (" cmake" ) != std::string::npos);
389- const bool isBuild = (cmd.find (" --build" ) != std::string::npos);
390- const bool isPreset = (cmd.find (" --preset" ) != std::string::npos);
391- const bool isDotDot = (cmd.find (" cmake .." ) != std::string::npos) ||
392- (cmd.find (" cmake .." ) != std::string::npos);
427+ const bool hasCmake =
428+ cmd.find (" cmake" ) != std::string::npos;
429+
430+ if (!hasCmake)
431+ return false ;
432+
433+ if (cmd.find (" --build" ) != std::string::npos)
434+ return false ;
435+
436+ if (cmd.find (" --install" ) != std::string::npos)
437+ return false ;
438+
439+ if (cmd.find (" --preset" ) != std::string::npos)
440+ return true ;
441+
442+ if (cmd.find (" cmake .." ) != std::string::npos ||
443+ cmd.find (" cmake .." ) != std::string::npos)
444+ return true ;
445+
446+ const bool hasSource =
447+ cmd.find (" -S " ) != std::string::npos ||
448+ cmd.find (" -S." ) != std::string::npos ||
449+ cmd.find (" -S\" " ) != std::string::npos ||
450+ cmd.find (" -S'" ) != std::string::npos;
393451
394- return isCmake && !isBuild && (isPreset || isDotDot);
452+ const bool hasBuild =
453+ cmd.find (" -B " ) != std::string::npos ||
454+ cmd.find (" -B\" " ) != std::string::npos ||
455+ cmd.find (" -B'" ) != std::string::npos;
456+
457+ return hasSource && hasBuild;
395458 }
396459
397460 bool looks_like_error_or_warning (std::string_view line) noexcept
@@ -683,6 +746,7 @@ namespace vix::commands::RunCommand::detail
683746 while (true )
684747 {
685748 const std::size_t nl = data.find (' \n ' , start);
749+
686750 if (nl == std::string::npos)
687751 {
688752 carry = data.substr (start);
@@ -692,22 +756,12 @@ namespace vix::commands::RunCommand::detail
692756 std::string_view line (&data[start], (nl - start) + 1 );
693757 start = nl + 1 ;
694758
759+ /*
760+ * During CMake configure, Vix must never dump normal CMake traces.
761+ * Only real errors and warnings are allowed to reach the terminal.
762+ */
695763 if (looks_like_error_or_warning (line))
696- {
697764 out.append (line.data (), line.size ());
698- continue ;
699- }
700-
701- if (line.rfind (" -- " , 0 ) == 0 )
702- continue ;
703-
704- if (line.find (" Preset CMake variables:" ) != std::string_view::npos)
705- continue ;
706-
707- if (line.rfind (" CMAKE_" , 0 ) == 0 )
708- continue ;
709-
710- out.append (line.data (), line.size ());
711765 }
712766
713767 return out;
@@ -1134,8 +1188,23 @@ namespace vix::commands::RunCommand::detail
11341188 std::string printable = chunk;
11351189
11361190 if (cmakeConfigure)
1191+ {
11371192 printable = cmakeNoise.filter (printable);
11381193
1194+ if (printable.empty ())
1195+ return ;
1196+
1197+ if (captureOnly || useSan)
1198+ return ;
1199+
1200+ write_all (STDOUT_FILENO, printable.data (), printable.size ());
1201+ printedSomething = true ;
1202+ printedRealOutput = true ;
1203+ result.printed_live = true ;
1204+ lastPrintedChar = printable.back ();
1205+ return ;
1206+ }
1207+
11391208 if (printable.empty ())
11401209 return ;
11411210
@@ -1263,6 +1332,87 @@ namespace vix::commands::RunCommand::detail
12631332 const auto startTime = std::chrono::steady_clock::now ();
12641333 bool didTimeout = false ;
12651334
1335+ auto lastOutputTime = startTime;
1336+ auto lastHeartbeatTime = startTime;
1337+ bool heartbeatVisible = false ;
1338+
1339+ auto heartbeat_env_enabled = [&]() -> bool
1340+ {
1341+ if (captureOnly)
1342+ return false ;
1343+
1344+ if (passthroughRuntime)
1345+ return false ;
1346+
1347+ if (!cmakeConfigure)
1348+ return false ;
1349+
1350+ const char *runValue = vix::utils::vix_getenv (" VIX_RUN_HEARTBEAT" );
1351+ const char *buildValue = vix::utils::vix_getenv (" VIX_BUILD_HEARTBEAT" );
1352+
1353+ const char *value = runValue && *runValue ? runValue : buildValue;
1354+
1355+ if (!value || !*value)
1356+ return true ;
1357+
1358+ std::string s (value);
1359+ for (char &c : s)
1360+ c = static_cast <char >(std::tolower (static_cast <unsigned char >(c)));
1361+
1362+ if (s == " 0" || s == " false" || s == " no" || s == " off" )
1363+ return false ;
1364+
1365+ return true ;
1366+ }();
1367+
1368+ auto clear_heartbeat_line = [&]() -> void
1369+ {
1370+ if (!heartbeatVisible)
1371+ return ;
1372+
1373+ const std::size_t width = terminal_width ();
1374+
1375+ std::string clear;
1376+ clear += " \r " ;
1377+ clear.append (width, ' ' );
1378+ clear += " \r " ;
1379+
1380+ write_all (STDOUT_FILENO, clear.data (), clear.size ());
1381+ heartbeatVisible = false ;
1382+ };
1383+
1384+ auto render_heartbeat_line = [&](long long elapsedMs) -> void
1385+ {
1386+ const std::size_t width = terminal_width ();
1387+
1388+ std::string line;
1389+ line += " " ;
1390+ line += CYAN;
1391+ line += " configure" ;
1392+ line += RESET;
1393+ line += " still running... " ;
1394+ line += GRAY;
1395+ line += " (" + format_seconds (elapsedMs) + " , checking/downloading dependencies)" ;
1396+ line += RESET;
1397+
1398+ std::string out;
1399+ out += " \r " ;
1400+ out.append (width, ' ' );
1401+ out += " \r " ;
1402+ out += truncate_terminal_line (line, width);
1403+
1404+ write_all (STDOUT_FILENO, out.data (), out.size ());
1405+ heartbeatVisible = true ;
1406+ };
1407+
1408+ auto finish_heartbeat_line = [&]() -> void
1409+ {
1410+ if (!heartbeatVisible)
1411+ return ;
1412+
1413+ clear_heartbeat_line ();
1414+ };
1415+
12661416 bool sentInt = false ;
12671417 bool sentTerm = false ;
12681418 bool sentKill = false ;
@@ -1362,7 +1512,7 @@ namespace vix::commands::RunCommand::detail
13621512 struct timeval tv;
13631513 struct timeval *tv_ptr = nullptr ;
13641514
1365- if (spinnerActive || enableTimeout)
1515+ if (spinnerActive || enableTimeout || heartbeat_env_enabled )
13661516 {
13671517 tv.tv_sec = 0 ;
13681518 tv.tv_usec = 100000 ;
@@ -1413,6 +1563,17 @@ namespace vix::commands::RunCommand::detail
14131563 {
14141564 if (!chunk.empty ())
14151565 {
1566+ const bool outputMayBePrinted =
1567+ !cmakeConfigure || looks_like_error_or_warning (std::string_view (chunk));
1568+
1569+ if (outputMayBePrinted)
1570+ {
1571+ lastOutputTime = std::chrono::steady_clock::now ();
1572+
1573+ if (heartbeatVisible && !captureOnly)
1574+ clear_heartbeat_line ();
1575+ }
1576+
14161577 result.stdoutText += chunk;
14171578
14181579 if (replayCapture)
@@ -1447,6 +1608,33 @@ namespace vix::commands::RunCommand::detail
14471608 }
14481609 }
14491610
1611+ if (heartbeat_env_enabled)
1612+ {
1613+ const auto now = std::chrono::steady_clock::now ();
1614+
1615+ const auto silenceMs =
1616+ std::chrono::duration_cast<std::chrono::milliseconds>(
1617+ now - lastOutputTime)
1618+ .count ();
1619+
1620+ const auto heartbeatMs =
1621+ std::chrono::duration_cast<std::chrono::milliseconds>(
1622+ now - lastHeartbeatTime)
1623+ .count ();
1624+
1625+ if (silenceMs >= 2000 && heartbeatMs >= 1000 )
1626+ {
1627+ lastHeartbeatTime = now;
1628+
1629+ const auto elapsedMs =
1630+ std::chrono::duration_cast<std::chrono::milliseconds>(
1631+ now - startTime)
1632+ .count ();
1633+
1634+ render_heartbeat_line (elapsedMs);
1635+ }
1636+ }
1637+
14501638 int status = 0 ;
14511639 const pid_t r = ::waitpid (pid, &status, WNOHANG);
14521640 if (r == pid)
@@ -1476,6 +1664,9 @@ namespace vix::commands::RunCommand::detail
14761664
14771665 if (didTimeout)
14781666 {
1667+ if (!captureOnly)
1668+ finish_heartbeat_line ();
1669+
14791670 result.exitCode = 124 ;
14801671 return result;
14811672 }
@@ -1501,6 +1692,9 @@ namespace vix::commands::RunCommand::detail
15011692 write_all (STDOUT_FILENO, &nl, 1 );
15021693 }
15031694
1695+ if (!captureOnly)
1696+ finish_heartbeat_line ();
1697+
15041698 if (userInterrupted)
15051699 result.exitCode = 130 ;
15061700
0 commit comments