@@ -19,6 +19,15 @@ namespace wsl::windows::wslc::services {
1919
2020using wsl::windows::common::string::MultiByteToWide;
2121
22+ namespace {
23+ constexpr std::wstring_view c_escapeMoveCursorUpAndClear = L" \033 [{}A\033 [J" ;
24+ constexpr std::wstring_view c_escapeBrightGreen = L" \033 [92m" ;
25+ constexpr std::wstring_view c_escapeResetAttributes = L" \033 [0m" ;
26+ constexpr std::wstring_view c_escapeHideCursorDim = L" \033 [?25l\033 [2m" ;
27+ constexpr std::wstring_view c_escapeClearLineAndNewline = L" \033 [K\n " ;
28+ constexpr std::wstring_view c_escapeUndimShowCursor = L" \033 [22m\033 [?25h" ;
29+ } // namespace
30+
2231BuildImageCallback::~BuildImageCallback ()
2332try
2433{
@@ -58,15 +67,26 @@ void BuildImageCallback::CollapseWindow()
5867{
5968 if (m_displayedLines > 0 )
6069 {
61- WriteTerminal (std::format (L" \033 [{}A \033 [J " , m_displayedLines));
70+ WriteTerminal (std::format (c_escapeMoveCursorUpAndClear , m_displayedLines));
6271 m_displayedLines = 0 ;
6372 }
6473
6574 m_lines.clear ();
6675 m_pendingLine.clear ();
76+ m_pullLines.clear ();
6777}
6878
69- HRESULT BuildImageCallback::OnProgress (LPCSTR status, LPCSTR id, ULONGLONG /* current*/ , ULONGLONG /* total*/ )
79+ void BuildImageCallback::RedrawIfNeeded ()
80+ {
81+ auto now = std::chrono::steady_clock::now ();
82+ if (now - m_lastRedraw >= c_redrawInterval)
83+ {
84+ Redraw ();
85+ m_lastRedraw = now;
86+ }
87+ }
88+
89+ HRESULT BuildImageCallback::OnProgress (LPCSTR status, LPCSTR id, ULONGLONG current, ULONGLONG total)
7090try
7191{
7292 if (status == nullptr || *status == ' \0 ' )
81101 return S_OK ;
82102 }
83103
104+ const std::string_view idView = (id != nullptr ) ? id : std::string_view{};
105+ const bool isLog = (idView == " log" );
106+ const bool isPullProgress = (!idView.empty () && total > 0 && !isLog);
107+
84108 if (m_verbose || !m_isConsole)
85109 {
86- wprintf (L" %hs" , status);
110+ // Skip pull progress updates when output is redirected, show only major steps
111+ if (!isPullProgress)
112+ {
113+ wprintf (L" %hs" , status);
114+ }
87115 return S_OK ;
88116 }
89117
90- // Match the specific "log" sentinel sent by WSLCSession::BuildImage rather than
91- // accepting any non-empty id, so future or unrelated id usage defaults to permanent.
92- const bool isLog = (id != nullptr && std::string_view{id} == " log" );
93-
94- if (!isLog)
118+ // Pull/download progress: update the per-entry map so Redraw can show each entry
119+ // on a single line that updates in place.
120+ if (isPullProgress)
95121 {
96- // Permanent line: collapse the scrolling window then print directly.
97- CollapseWindow ();
98- WriteTerminal ( MultiByteToWide (status));
122+ m_pullLines[id] = status;
123+ RedrawIfNeeded ();
124+
99125 return S_OK ;
100126 }
101127
102- // Log line: add to the scrolling window.
103- for (const char * p = status; *p != ' \0 ' ; ++p)
128+ if (isLog)
104129 {
105- if (*p == ' \n ' )
130+ // Log line: add to the scrolling window.
131+ for (const char * p = status; *p != ' \0 ' ; ++p)
106132 {
107- // Store with the trailing newline so the byte count matches what is replayed.
108- // Cap retained log output to avoid unbounded growth on very long builds.
109- m_allLines.push_back (m_pendingLine + ' \n ' );
110- m_allLinesBytes += m_allLines.back ().size ();
111- while (m_allLinesBytes > c_maxAllLinesBytes && !m_allLines.empty ())
133+ if (*p == ' \n ' )
112134 {
113- m_allLinesBytes -= m_allLines.front ().size ();
114- m_allLines.pop_front ();
115- }
135+ // Store with the trailing newline so the byte count matches what is replayed.
136+ // Cap retained log output to avoid unbounded growth on very long builds.
137+ m_allLines.push_back (m_pendingLine + ' \n ' );
138+ m_allLinesBytes += m_allLines.back ().size ();
139+ while (m_allLinesBytes > c_maxAllLinesBytes && !m_allLines.empty ())
140+ {
141+ m_allLinesBytes -= m_allLines.front ().size ();
142+ m_allLines.pop_front ();
143+ }
116144
117- m_lines.push_back (std::move (m_pendingLine));
118- m_pendingLine.clear ();
119- if (m_lines.size () > c_maxDisplayLines)
120- {
121- m_lines.pop_front ();
145+ m_lines.push_back (std::move (m_pendingLine));
146+ m_pendingLine.clear ();
147+ if (m_lines.size () > c_maxDisplayLines)
148+ {
149+ m_lines.pop_front ();
150+ }
122151 }
123- }
124- else if (*p == ' \r ' )
125- {
126- // \r\n is a line ending; standalone \r overwrites the current line.
127- if (*(p + 1 ) != ' \n ' )
152+ else if (*p == ' \r ' )
128153 {
129- // Flush a throttled redraw before clearing so \r-based progress
130- // updates are visible even when batched in a single OnProgress call.
131- auto now = std::chrono::steady_clock::now ();
132- if (!m_pendingLine.empty () && now - m_lastRedraw >= c_redrawInterval)
154+ // \r\n is a line ending; standalone \r overwrites the current line.
155+ if (*(p + 1 ) != ' \n ' )
133156 {
134- Redraw ();
135- m_lastRedraw = now;
157+ // Flush a throttled redraw before clearing so \r-based progress
158+ // updates are visible even when batched in a single OnProgress call.
159+ if (!m_pendingLine.empty ())
160+ {
161+ RedrawIfNeeded ();
162+ }
163+ m_pendingLine.clear ();
136164 }
137- m_pendingLine.clear ();
165+ }
166+ else
167+ {
168+ m_pendingLine += *p;
138169 }
139170 }
140- else
141- {
142- m_pendingLine += *p;
143- }
144- }
145171
146- // Throttle redraws to avoid blocking the server's IO loop with console writes
147- // during rapid output. Lines accumulate in the deque immediately; the display
148- // catches up at ~20fps.
149- auto now = std::chrono::steady_clock::now ();
150- if (now - m_lastRedraw >= c_redrawInterval)
151- {
152- Redraw ();
153- m_lastRedraw = now;
172+ // Throttle redraws to avoid blocking the server's IO loop with console writes
173+ // during rapid output. Lines accumulate in the deque immediately; the display
174+ // catches up at ~20fps.
175+ RedrawIfNeeded ();
176+
177+ return S_OK ;
154178 }
155179
180+ // Else is a build step
181+ CollapseWindow ();
182+ auto wide = MultiByteToWide (status);
183+ const auto bodyLength = wide.find_last_not_of (L" \r\n " ) + 1 ;
184+ const auto newlines = wide.substr (bodyLength);
185+ wide.resize (bodyLength);
186+
187+ WriteTerminal (std::format (L" {}{}{}{}" , c_escapeBrightGreen, wide, c_escapeResetAttributes, newlines));
156188 return S_OK ;
157189}
158190CATCH_RETURN ();
@@ -167,27 +199,29 @@ void BuildImageCallback::Redraw()
167199 // to std::wstring::resize).
168200 const SHORT consoleWidth = std::max<SHORT >(0 , info.srWindow .Right - info.srWindow .Left );
169201
170- // Determine how many completed lines to show, leaving room for the pending line.
202+ // Determine how many completed lines to show, leaving room for the pending line and pull progress .
171203 const bool showPending = !m_pendingLine.empty ();
204+ const SHORT pullCount = static_cast <SHORT >(m_pullLines.size ());
172205 SHORT completedCount = static_cast <SHORT >(m_lines.size ());
173- if (showPending && completedCount >= c_maxDisplayLines)
206+ const SHORT reservedLines = (showPending ? 1 : 0 ) + pullCount;
207+ if (completedCount + reservedLines > c_maxDisplayLines)
174208 {
175- completedCount = c_maxDisplayLines - 1 ;
209+ completedCount = std::max< SHORT >( 0 , c_maxDisplayLines - reservedLines) ;
176210 }
177- const SHORT displayCount = completedCount + (showPending ? 1 : 0 ) ;
211+ const SHORT displayCount = completedCount + reservedLines ;
178212
179213 // Build the entire frame in one buffer to minimize console writes. Hide the cursor
180214 // during the redraw so the user doesn't see it bouncing through the cursor movement,
181215 // then show it again at the final position. The dim attribute (\033[2m) renders the
182216 // scrolling lines de-emphasized regardless of the user's theme.
183- std::wstring buffer = L" \033 [?25l \033 [2m " ;
217+ std::wstring buffer{c_escapeHideCursorDim} ;
184218
185219 // Move cursor to the start of the display area and erase from there to the end of
186220 // the screen. \033[J handles the case where the new display is shorter than the
187221 // previous one (e.g. when \r clears the pending line without a replacement).
188222 if (m_displayedLines > 0 )
189223 {
190- buffer += std::format (L" \033 [{}A \033 [J " , m_displayedLines);
224+ buffer += std::format (c_escapeMoveCursorUpAndClear , m_displayedLines);
191225 }
192226
193227 auto appendLine = [&](const std::string& line) {
@@ -197,7 +231,7 @@ void BuildImageCallback::Redraw()
197231 wline.resize (consoleWidth);
198232 }
199233 buffer += wline;
200- buffer += L" \033 [K \n " ;
234+ buffer += c_escapeClearLineAndNewline ;
201235 };
202236
203237 // Print completed lines (skip older ones if we need room for the pending line).
@@ -217,7 +251,13 @@ void BuildImageCallback::Redraw()
217251 appendLine (m_pendingLine);
218252 }
219253
220- buffer += L" \033 [22m\033 [?25h" ;
254+ // Render per-entry pull progress (each entry updates in place via the map).
255+ for (const auto & [key, line] : m_pullLines)
256+ {
257+ appendLine (line);
258+ }
259+
260+ buffer += c_escapeUndimShowCursor;
221261
222262 WriteTerminal (buffer);
223263 m_displayedLines = displayCount;
0 commit comments