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).
169 
170  // Find next character from the input.
171  void NextChar() {
172  visible_pos_ += IsVisible();
173  if (++input_index_ == ansi_iter_.SequenceEnd()) {
175  }
176  }
177 
178  private:
179  size_t input_size_;
180  size_t input_index_ = 0;
181  size_t visible_pos_ = 0;
183 };
184 
185 void 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  //
252  VisibleInputCharsIterator iter(str);
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)
Definition: elide_middle.cc:35
size_t SequenceSize() const
Definition: elide_middle.cc:51
size_t SequenceStart() const
Definition: elide_middle.cc:44
size_t SequenceEnd() const
Definition: elide_middle.cc:48
bool SequenceContains(size_t input_index) const
Definition: elide_middle.cc:54
bool FindNextSequenceFrom(const char *from)
Definition: elide_middle.cc:80
VisibleInputCharsIterator(const std::string &input)
AnsiColorSequenceIterator ansi_iter_
size_t VisiblePosition() const