Ninja
elide_middle.cc
Go to the documentation of this file.
1// Copyright 2024 Google Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#include "elide_middle.h"
16
17#include <assert.h>
18#include <string.h>
19
20// Convenience class used to iterate over the ANSI color sequences
21// of an input string. Note that this ignores non-color related
22// ANSI sequences. Usage is:
23//
24// - Create instance, passing the input string to the constructor.
25// - Loop over each sequence with:
26//
27// AnsiColorSequenceIterator iter;
28// while (iter.HasSequence()) {
29// .. use iter.SequenceStart() and iter.SequenceEnd()
30// iter.NextSequence();
31// }
32//
34 // Constructor takes input string .
35 AnsiColorSequenceIterator(const std::string& input)
36 : input_(input.data()), input_end_(input_ + input.size()) {
38 }
39
40 // Return true if an ANSI sequence was found.
41 bool HasSequence() const { return cur_end_ != 0; }
42
43 // Start of the current sequence.
44 size_t SequenceStart() const { return cur_start_; }
45
46 // End of the current sequence (index of the first character
47 // following the sequence).
48 size_t SequenceEnd() const { return cur_end_; }
49
50 // Size of the current sequence in characters.
51 size_t SequenceSize() const { return cur_end_ - cur_start_; }
52
53 // Returns true if |input_index| belongs to the current sequence.
54 bool SequenceContains(size_t input_index) const {
55 return (input_index >= cur_start_ && input_index < cur_end_);
56 }
57
58 // Find the next sequence, if any, from the input.
59 // Returns false is there is no more sequence.
60 bool NextSequence() {
62 return true;
63
64 cur_start_ = 0;
65 cur_end_ = 0;
66 return false;
67 }
68
69 // Reset iterator to start of input.
70 void Reset() {
71 cur_start_ = cur_end_ = 0;
73 }
74
75 private:
76 // Find the next sequence from the input, |from| being the starting position
77 // for the search, and must be in the [input_, input_end_] interval. On
78 // success, returns true after setting cur_start_ and cur_end_, on failure,
79 // return false.
80 bool FindNextSequenceFrom(const char* from) {
81 assert(from >= input_ && from <= input_end_);
82 auto* seq =
83 static_cast<const char*>(::memchr(from, '\x1b', input_end_ - from));
84 if (!seq)
85 return false;
86
87 // The smallest possible color sequence if '\x1c[0m` and has four
88 // characters.
89 if (seq + 4 > input_end_)
90 return false;
91
92 if (seq[1] != '[')
93 return FindNextSequenceFrom(seq + 1);
94
95 // Skip parameters (digits + ; separator)
96 auto is_parameter_char = [](char ch) -> bool {
97 return (ch >= '0' && ch <= '9') || ch == ';';
98 };
99
100 const char* end = seq + 2;
101 while (is_parameter_char(end[0])) {
102 if (++end == input_end_)
103 return false; // Incomplete sequence (no command).
104 }
105
106 if (*end++ != 'm') {
107 // Not a color sequence. Restart the search after the first
108 // character following the [, in case this was a 3-char ANSI
109 // sequence (which is ignored here).
110 return FindNextSequenceFrom(seq + 3);
111 }
112
113 // Found it!
114 cur_start_ = seq - input_;
115 cur_end_ = end - input_;
116 return true;
117 }
118
119 size_t cur_start_ = 0;
120 size_t cur_end_ = 0;
121 const char* input_;
122 const char* input_end_;
123};
124
125// A class used to iterate over all characters of an input string,
126// and return its visible position in the terminal, and whether that
127// specific character is visible (or otherwise part of an ANSI color sequence).
128//
129// Example sequence and iterations, where 'ANSI' represents an ANSI Color
130// sequence, and | is used to express concatenation
131//
132// |abcd|ANSI|efgh|ANSI|ijk| input string
133//
134// 11 1111 111
135// 0123 4567 8901 2345 678 input indices
136//
137// 1
138// 0123 4444 4567 8888 890 visible positions
139//
140// TTTT FFFF TTTT FFFF TTT is_visible
141//
142// Usage is:
143//
144// VisibleInputCharsIterator iter(input);
145// while (iter.HasChar()) {
146// ... use iter.InputIndex() to get input index of current char.
147// ... use iter.VisiblePosition() to get its visible position.
148// ... use iter.IsVisible() to check whether the current char is visible.
149//
150// NextChar();
151// }
152//
154 VisibleInputCharsIterator(const std::string& input)
155 : input_size_(input.size()), ansi_iter_(input) {}
156
157 // Return true if there is a character in the sequence.
158 bool HasChar() const { return input_index_ < input_size_; }
159
160 // Return current input index.
161 size_t InputIndex() const { return input_index_; }
162
163 // Return current visible position.
164 size_t VisiblePosition() const { return visible_pos_; }
165
166 // Return true if the current input character is visible
167 // (i.e. not part of an ANSI color sequence).
168 bool IsVisible() const { return !ansi_iter_.SequenceContains(input_index_); }
169
170 // Find next character from the input.
171 void NextChar() {
173 if (++input_index_ == ansi_iter_.SequenceEnd()) {
174 ansi_iter_.NextSequence();
175 }
176 }
177
178 private:
180 size_t input_index_ = 0;
181 size_t visible_pos_ = 0;
183};
184
185void ElideMiddleInPlace(std::string& str, size_t max_width) {
186 if (str.size() <= max_width) {
187 return;
188 }
189 // Look for an ESC character. If there is none, use a fast path
190 // that avoids any intermediate allocations.
191 if (str.find('\x1b') == std::string::npos) {
192 const int ellipsis_width = 3; // Space for "...".
193
194 // If max width is too small, do not keep anything from the input.
195 if (max_width <= ellipsis_width) {
196 str.assign("...", max_width);
197 return;
198 }
199
200 // Keep only |max_width - ellipsis_size| visible characters from the input
201 // which will be split into two spans separated by "...".
202 const size_t remaining_size = max_width - ellipsis_width;
203 const size_t left_span_size = remaining_size / 2;
204 const size_t right_span_size = remaining_size - left_span_size;
205
206 // Replace the gap in the input between the spans with "..."
207 const size_t gap_start = left_span_size;
208 const size_t gap_end = str.size() - right_span_size;
209 str.replace(gap_start, gap_end - gap_start, "...");
210 return;
211 }
212
213 // Compute visible width.
214 size_t visible_width = str.size();
215 for (AnsiColorSequenceIterator ansi(str); ansi.HasSequence();
216 ansi.NextSequence()) {
217 visible_width -= ansi.SequenceSize();
218 }
219
220 if (visible_width <= max_width)
221 return;
222
223 // Compute the widths of the ellipsis, left span and right span
224 // visible space.
225 const size_t ellipsis_width = max_width < 3 ? max_width : 3;
226 const size_t visible_left_span_size = (max_width - ellipsis_width) / 2;
227 const size_t visible_right_span_size =
228 (max_width - ellipsis_width) - visible_left_span_size;
229
230 // Compute the gap of visible characters that will be replaced by
231 // the ellipsis in visible space.
232 const size_t visible_gap_start = visible_left_span_size;
233 const size_t visible_gap_end = visible_width - visible_right_span_size;
234
235 std::string result;
236 result.reserve(str.size());
237
238 // Parse the input chars info to:
239 //
240 // 1) Append any characters belonging to the left span (visible or not).
241 //
242 // 2) Add the ellipsis ("..." truncated to ellipsis_width).
243 // Note that its color is inherited from the left span chars
244 // which will never end with an ANSI sequence.
245 //
246 // 3) Append any ANSI sequence that appears inside the gap. This
247 // ensures the characters after the ellipsis appear with
248 // the right color,
249 //
250 // 4) Append any remaining characters (visible or not) to the result.
251 //
253
254 // Step 1 - determine left span length in input chars.
255 for (; iter.HasChar(); iter.NextChar()) {
256 if (iter.VisiblePosition() == visible_gap_start)
257 break;
258 }
259 result.append(str.begin(), str.begin() + iter.InputIndex());
260
261 // Step 2 - Append the possibly-truncated ellipsis.
262 result.append("...", ellipsis_width);
263
264 // Step 3 - Append elided ANSI sequences to the result.
265 for (; iter.HasChar(); iter.NextChar()) {
266 if (iter.VisiblePosition() == visible_gap_end)
267 break;
268 if (!iter.IsVisible())
269 result.push_back(str[iter.InputIndex()]);
270 }
271
272 // Step 4 - Append anything else.
273 result.append(str.begin() + iter.InputIndex(), str.end());
274
275 str = std::move(result);
276}
void ElideMiddleInPlace(std::string &str, size_t max_width)
Elide the given string str with '...' in the middle if the length exceeds max_width.
AnsiColorSequenceIterator(const std::string &input)
size_t SequenceSize() const
size_t SequenceStart() const
bool SequenceContains(size_t input_index) const
bool FindNextSequenceFrom(const char *from)
VisibleInputCharsIterator(const std::string &input)
AnsiColorSequenceIterator ansi_iter_
size_t VisiblePosition() const