master
h 203 lines 6.54 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 Terminal.h
8
9 Abstract:
10
11 Level-filtered, std::format-style user-facing output for the WSLC CLI, plus
12 line-oriented user input (prompts). Sequence arguments are stripped when VT is
13 off; color Sequences are also stripped when color is disabled, while cursor-move
14 Sequences still pass through.
15
16 --*/
17 #pragma once
18
19 #include "InputChannel.h"
20 #include "OutputChannel.h"
21 #include "VTSupport.h"
22
23 #include <cstdio>
24 #include <format>
25 #include <optional>
26 #include <string>
27 #include <string_view>
28 #include <tuple>
29 #include <type_traits>
30 #include <utility>
31
32 namespace wsl::windows::wslc {
33
34 // Fallback width for progress displays when the console width can't be queried. This
35 // value already includes the autowrap guard (visible width minus one) so a wrapped line
36 // can't corrupt cursor-based rendering.
37 inline constexpr int c_fallbackConsoleWidth = 79;
38
39 namespace terminal_detail {
40
41 // SFINAE: excludes Sequence-derived types so the overload below wins for them.
42 template <typename T, typename = std::enable_if_t<!std::is_base_of_v<wsl::windows::common::vt::Sequence, std::remove_cvref_t<T>>>>
43 constexpr T&& StripIfDisabled(T&& value, bool /*vtEnabled*/, bool /*colorEnabled*/) noexcept
44 {
45 return std::forward<T>(value);
46 }
47
48 // Returns VT bytes when permitted, empty when stripped. The returned view borrows
49 // from the caller's argument, which outlives the Write call.
50 inline std::wstring_view StripIfDisabled(const wsl::windows::common::vt::Sequence& sequence, bool vtEnabled, bool colorEnabled)
51 {
52 if (!vtEnabled)
53 {
54 return {};
55 }
56 if (!colorEnabled && sequence.IsColor())
57 {
58 return {};
59 }
60 return sequence.Get();
61 }
62
63 } // namespace terminal_detail
64
65 struct Terminal
66 {
67 enum class Level
68 {
69 Output,
70 Info,
71 Warning,
72 Error,
73 };
74
75 Terminal();
76 Terminal(FILE* outFile, bool outVtEnabled, FILE* errFile, bool errVtEnabled, FILE* inFile = nullptr, bool inInteractive = false);
77
78 NON_COPYABLE(Terminal);
79 NON_MOVABLE(Terminal);
80
81 ~Terminal() = default;
82
83 // std::format-style write API.
84 template <typename... Args>
85 void Write(Level level, std::wformat_string<Args...> fmt, Args&&... args)
86 {
87 EmitFormatted(level, std::move(fmt), std::forward<Args>(args)...);
88 }
89
90 template <typename... Args>
91 void Output(std::wformat_string<Args...> fmt, Args&&... args)
92 {
93 EmitFormatted(Level::Output, std::move(fmt), std::forward<Args>(args)...);
94 }
95 template <typename... Args>
96 void Info(std::wformat_string<Args...> fmt, Args&&... args)
97 {
98 EmitFormatted(Level::Info, std::move(fmt), std::forward<Args>(args)...);
99 }
100 template <typename... Args>
101 void Warn(std::wformat_string<Args...> fmt, Args&&... args)
102 {
103 EmitFormatted(Level::Warning, std::move(fmt), std::forward<Args>(args)...);
104 }
105 template <typename... Args>
106 void Error(std::wformat_string<Args...> fmt, Args&&... args)
107 {
108 EmitFormatted(Level::Error, std::move(fmt), std::forward<Args>(args)...);
109 }
110
111 // True when user input is attached to an interactive console (a prompt can be
112 // shown and echo can be masked); false when input is redirected from a file or pipe.
113 bool IsInputInteractive() const noexcept
114 {
115 return m_in.IsInteractive();
116 }
117
118 // Reads a single line of user input, stripping the trailing CR and/or LF. Returns
119 // nullopt at end of input with nothing read. When mask is true and input is an
120 // interactive console, echo is disabled for the duration of the read.
121 std::optional<std::wstring> ReadLine(bool mask = false)
122 {
123 return m_in.ReadLine(mask);
124 }
125
126 // Writes label (no trailing newline) at the given level, then reads a line of input.
127 // When mask is true and input is interactive, echo is disabled during the read and a
128 // trailing newline is emitted afterward (the un-echoed Enter). Returns the line, or an
129 // empty string at end of input.
130 std::wstring PromptForLine(Level level, std::wstring_view label, bool mask);
131
132 // Convenience overload that defaults prompts to stdout (Level::Output) to align with the
133 // container CLI ecosystem: Docker (cli.Out()), containerd/nerdctl (cmd.OutOrStdout()), and
134 // Apple container (Swift print) all prompt on stdout. This diverges from general Unix tools
135 // (sudo, ssh, git, gh) that prompt on stderr/tty to keep stdout pipeable, but WSLC follows
136 // Docker's CLI semantics.
137 std::wstring PromptForLine(std::wstring_view label, bool mask = false)
138 {
139 return PromptForLine(Level::Output, label, mask);
140 }
141
142 bool IsVTEnabled(Level level) const noexcept;
143
144 bool IsColorEnabled(Level level) const noexcept;
145
146 bool IsNoColor() const noexcept
147 {
148 return m_noColor;
149 }
150
151 void SetNoColor(bool noColor) noexcept
152 {
153 m_noColor = noColor;
154 }
155
156 // Console write width minus one (autowrap guard), or nullopt when redirected.
157 std::optional<int> GetConsoleWidth(Level level) const;
158
159 private:
160 const OutputChannel& ChannelFor(Level level) const noexcept
161 {
162 return (level == Level::Output) ? m_out : m_err;
163 }
164
165 // Per-level SGR prefix (empty when color is off).
166 std::wstring_view LevelPrefix(Level level) const noexcept;
167
168 template <typename... Args>
169 void EmitFormatted(Level level, std::wformat_string<Args...> fmt, Args&&... args)
170 {
171 const OutputChannel& channel = ChannelFor(level);
172 const bool vtEnabled = channel.IsVTEnabled();
173 const bool colorEnabled = vtEnabled && !m_noColor;
174
175 // Materialize stripped args into stable storage for vformat.
176 auto stripped = std::tuple{terminal_detail::StripIfDisabled(std::forward<Args>(args), vtEnabled, colorEnabled)...};
177
178 std::wstring body = std::apply(
179 [&fmt](auto&... values) { return std::vformat(std::wstring_view{fmt.get()}, std::make_wformat_args(values...)); }, stripped);
180
181 const auto prefix = LevelPrefix(level);
182 if (prefix.empty())
183 {
184 channel.WriteString(body);
185 return;
186 }
187
188 const auto reset = wsl::windows::common::vt::Format::Default.Get();
189 std::wstring out;
190 out.reserve(prefix.size() + body.size() + reset.size());
191 out.append(prefix);
192 out.append(body);
193 out.append(reset);
194 channel.WriteString(out);
195 }
196
197 OutputChannel m_out;
198 OutputChannel m_err;
199 InputChannel m_in;
200 bool m_noColor = false;
201 };
202
203 } // namespace wsl::windows::wslc