TableOutput.h (6854B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #pragma once 4 #include "ExecutionReporter.h" 5 #include "Resources.h" 6 7 #include <array> 8 #include <ostream> 9 #include <string> 10 #include <vector> 11 12 13 namespace AppInstaller::CLI::Execution 14 { 15 // Enables output data in a table format. 16 // TODO: Improve for use with sparse data. 17 template <size_t FieldCount> 18 struct TableOutput 19 { 20 using header_t = std::array<Resource::LocString, FieldCount>; 21 using line_t = std::array<std::string, FieldCount>; 22 23 TableOutput(Reporter& reporter, header_t&& header, size_t sizingBuffer = 50) : 24 m_reporter(reporter), m_sizingBuffer(sizingBuffer) 25 { 26 for (size_t i = 0; i < FieldCount; ++i) 27 { 28 m_columns[i].Name = std::move(header[i]); 29 m_columns[i].MinLength = Utility::UTF8ColumnWidth(m_columns[i].Name.get()); 30 m_columns[i].MaxLength = 0; 31 } 32 } 33 34 void OutputLine(line_t&& line) 35 { 36 m_empty = false; 37 38 if (m_buffer.size() < m_sizingBuffer) 39 { 40 m_buffer.emplace_back(std::move(line)); 41 } 42 else 43 { 44 EvaluateAndFlushBuffer(); 45 OutputLineToStream(line); 46 } 47 } 48 49 void Complete() 50 { 51 if (!m_empty) 52 { 53 EvaluateAndFlushBuffer(); 54 } 55 } 56 57 bool IsEmpty() 58 { 59 return m_empty; 60 } 61 62 private: 63 // A column in the table. 64 struct Column 65 { 66 Resource::LocString Name; 67 size_t MinLength = 0; 68 size_t MaxLength = 0; 69 bool SpaceAfter = true; 70 }; 71 72 Reporter& m_reporter; 73 std::array<Column, FieldCount> m_columns; 74 size_t m_sizingBuffer; 75 std::vector<line_t> m_buffer; 76 bool m_bufferEvaluated = false; 77 bool m_empty = true; 78 79 void EvaluateAndFlushBuffer() 80 { 81 if (m_bufferEvaluated) 82 { 83 return; 84 } 85 86 // Determine the maximum length for all columns 87 for (const auto& line : m_buffer) 88 { 89 for (size_t i = 0; i < FieldCount; ++i) 90 { 91 m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, Utility::UTF8ColumnWidth(line[i])); 92 } 93 } 94 95 // If there are actually columns with data, then also bring in the minimum size 96 for (size_t i = 0; i < FieldCount; ++i) 97 { 98 if (m_columns[i].MaxLength) 99 { 100 m_columns[i].MaxLength = std::max(m_columns[i].MaxLength, m_columns[i].MinLength); 101 } 102 } 103 104 // Only output the extra space if: 105 // 1. Not the last field 106 m_columns[FieldCount - 1].SpaceAfter = false; 107 108 // 2. Not empty (taken care of by not doing anything if empty) 109 // 3. There are non-empty fields after 110 for (size_t i = FieldCount - 1; i > 0; --i) 111 { 112 if (m_columns[i].MaxLength) 113 { 114 break; 115 } 116 else 117 { 118 m_columns[i - 1].SpaceAfter = false; 119 } 120 } 121 122 // Determine the total width required to not truncate any columns 123 size_t totalRequired = 0; 124 125 for (size_t i = 0; i < FieldCount; ++i) 126 { 127 totalRequired += m_columns[i].MaxLength + (m_columns[i].SpaceAfter ? 1 : 0); 128 } 129 130 size_t consoleWidth = GetConsoleWidth(); 131 132 // If the total space would be too big, shrink them. 133 // We don't want to use the last column, lest we auto-wrap 134 if (totalRequired >= consoleWidth) 135 { 136 size_t extra = (totalRequired - consoleWidth) + 1; 137 138 while (extra) 139 { 140 size_t targetIndex = 0; 141 size_t targetVal = m_columns[0].MaxLength; 142 for (size_t j = 1; j < FieldCount; ++j) 143 { 144 if (m_columns[j].MaxLength > targetVal) 145 { 146 targetIndex = j; 147 targetVal = m_columns[j].MaxLength; 148 } 149 } 150 m_columns[targetIndex].MaxLength -= 1; 151 extra -= 1; 152 } 153 154 totalRequired = consoleWidth - 1; 155 } 156 157 // Header line 158 line_t headerLine; 159 160 for (size_t i = 0; i < FieldCount; ++i) 161 { 162 headerLine[i] = m_columns[i].Name.get(); 163 } 164 165 OutputLineToStream(headerLine); 166 167 m_reporter.Info() << std::string(totalRequired, '-') << std::endl; 168 169 for (const auto& line : m_buffer) 170 { 171 OutputLineToStream(line); 172 } 173 174 m_bufferEvaluated = true; 175 } 176 177 void OutputLineToStream(const line_t& line) 178 { 179 auto out = m_reporter.Info(); 180 181 for (size_t i = 0; i < FieldCount; ++i) 182 { 183 const auto& col = m_columns[i]; 184 185 if (col.MaxLength) 186 { 187 size_t valueLength = Utility::UTF8ColumnWidth(line[i]); 188 189 if (valueLength > col.MaxLength) 190 { 191 size_t actualWidth; 192 out << Utility::UTF8TrimRightToColumnWidth(line[i], col.MaxLength - 1, actualWidth) << "\xE2\x80\xA6"; // UTF8 encoding of ellipsis (…) character 193 194 // Some characters take 2 unit space, the trimmed string length might be 1 less than the expected length. 195 if (actualWidth != col.MaxLength - 1) 196 { 197 out << ' '; 198 } 199 200 if (col.SpaceAfter) 201 { 202 out << ' '; 203 } 204 } 205 else 206 { 207 out << line[i]; 208 209 if (col.SpaceAfter) 210 { 211 out << std::string(col.MaxLength - valueLength + 1, ' '); 212 } 213 } 214 } 215 } 216 217 out << std::endl; 218 } 219 }; 220 }