github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/columns/formatter/textcolumns/scaler.go (about) 1 // Copyright 2022 The Inspektor Gadget authors 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 package textcolumns 16 17 import ( 18 "fmt" 19 "math" 20 "os" 21 "reflect" 22 "strconv" 23 24 "golang.org/x/term" 25 ) 26 27 // RecalculateWidths sets the screen width and automatically scales columns to fit (if enabled in options) 28 // If force is true, fixed widths will also be adjusted. 29 func (tf *TextColumnsFormatter[T]) RecalculateWidths(maxWidth int, force bool) { 30 if tf.currentMaxWidth == maxWidth { 31 // No need to recalculate 32 return 33 } 34 35 // set for caching (to avoid recalculation) 36 tf.currentMaxWidth = maxWidth 37 38 if len(tf.showColumns) == 0 { 39 return 40 } 41 42 // Keep count of occurrences (needed when redistributing leftover space) 43 occurrences := make(map[string]int) 44 45 // width of all dividers between the columns 46 dividerWidth := (len(tf.showColumns) - 1) * len([]rune(tf.options.ColumnDivider)) 47 48 // calculate the minimum required length (that is: length of dividers plus width (in case it's fixed or MinWidth is 49 // set) or one character (if no width was specified)) - else we could get negative values on auto-scaling 50 requiredWidth := dividerWidth 51 52 // totalWidthNotFixed will contain all columns that haven't been set to "fixed" 53 totalWidthNotFixed := 0 54 55 // totalWidthFixed will contain dividers and all columns that have either been set to "fixed" (or in a later pass 56 // have minWidth or maxWidth constraints) 57 totalWidthFixed := dividerWidth 58 59 for _, column := range tf.showColumns { 60 // Reset temporary values 61 column.treatAsFixed = false 62 63 occurrences[column.col.Name]++ 64 65 if column.col.FixedWidth && !force { 66 requiredWidth += column.col.Width 67 totalWidthFixed += column.col.Width 68 continue 69 } 70 71 totalWidthNotFixed += column.col.Width 72 73 if column.col.MinWidth > 0 && !force { 74 requiredWidth += column.col.MinWidth 75 continue 76 } 77 78 // at least account one character per column 79 requiredWidth++ 80 } 81 82 // if force is set, we only account one character per column plus dividers 83 if force { 84 requiredWidth = dividerWidth + len(tf.showColumns) 85 } 86 87 // enforce at least having requiredWidth (we need to ignore maxWidth in this case) 88 if requiredWidth > maxWidth { 89 maxWidth = requiredWidth 90 } 91 92 // totalAdjustedWidthNotFixed stores the combined widths of all fields that have been scaled 93 var totalAdjustedWidthNotFixed int 94 95 // we might need to do several passes to satisfy the constraints 96 for { 97 satisfied := true 98 99 // collect deltas when moving columns from nonFixed to fixed because of exceeding their constraints 100 addToFixed := 0 101 removeFromNotFixed := 0 102 103 totalAdjustedWidthNotFixed = 0 104 for _, column := range tf.showColumns { 105 if (column.col.FixedWidth || column.treatAsFixed) && !force { 106 if column.col.FixedWidth { 107 column.calculatedWidth = column.col.Width 108 } 109 continue 110 } 111 112 // set calculatedWidth based on the "weight" (relative width to other columns) of this column 113 column.calculatedWidth = int(math.Floor(float64(column.col.Width) / float64(totalWidthNotFixed) * float64(maxWidth-totalWidthFixed))) 114 115 // honor min/max widths; they'll now be treated as fixed width, afterwards we'll need another pass 116 if !force { 117 if column.col.MaxWidth > 0 && column.calculatedWidth > column.col.MaxWidth { 118 column.calculatedWidth = column.col.MaxWidth 119 column.treatAsFixed = true 120 satisfied = false 121 122 addToFixed += column.calculatedWidth 123 removeFromNotFixed += column.col.Width 124 continue 125 } 126 if column.col.MinWidth > 0 && column.calculatedWidth < column.col.MinWidth { 127 column.calculatedWidth = column.col.MinWidth 128 column.treatAsFixed = true 129 satisfied = false 130 131 addToFixed += column.calculatedWidth 132 removeFromNotFixed += column.col.Width 133 continue 134 } 135 } 136 totalAdjustedWidthNotFixed += column.calculatedWidth 137 } 138 139 if satisfied { 140 break 141 } 142 totalWidthFixed += addToFixed 143 totalWidthNotFixed -= removeFromNotFixed 144 } 145 146 // Handle leftover space (gets distributed amongst non-fixed columns) 147 leftover := maxWidth - (totalAdjustedWidthNotFixed + totalWidthFixed) 148 149 distributeLeftover: 150 for leftover > 0 { 151 // keep track whether we actually got to distribute space (e.g. can't do that if all columns are fixed) 152 spent := false 153 154 alreadySpent := make(map[string]struct{}) 155 156 // distribute one to each remaining candidate 157 for _, column := range tf.showColumns { 158 if (column.col.FixedWidth || column.treatAsFixed) && !force { 159 continue 160 } 161 162 // we can only distribute to columns that are used more than once if we have leftover space for all 163 // occurrences 164 if occ := occurrences[column.col.Name]; occ > 1 { 165 if _, ok := alreadySpent[column.col.Name]; ok { 166 // we already distributed to this column in this pass (on another occurrence) 167 continue 168 } 169 if occ <= leftover { 170 column.calculatedWidth += 1 171 leftover -= occ 172 spent = true 173 174 if leftover == 0 { 175 break distributeLeftover 176 } 177 178 alreadySpent[column.col.Name] = struct{}{} 179 continue 180 } 181 // cannot redistribute here, since it would be used more than just once 182 continue 183 } 184 185 column.calculatedWidth += 1 186 leftover-- 187 spent = true 188 if leftover == 0 { 189 break distributeLeftover 190 } 191 } 192 if !spent { 193 // in case there are no columns to be resized found 194 break 195 } 196 } 197 198 tf.buildFillString() 199 } 200 201 // GetTerminalWidth returns the width of the terminal (if one is in use) or 0 otherwise 202 func GetTerminalWidth() int { 203 if !term.IsTerminal(int(os.Stdout.Fd())) { 204 return 0 205 } 206 terminalWidth, _, err := term.GetSize(0) 207 if err != nil { 208 return 0 209 } 210 return terminalWidth 211 } 212 213 // AdjustWidthsToScreen will try to get the width of the screen buffer and, if successful, call RecalculateWidths with 214 // that value 215 func (tf *TextColumnsFormatter[T]) AdjustWidthsToScreen() { 216 if !tf.options.AutoScale { 217 return 218 } 219 220 terminalWidth := GetTerminalWidth() 221 if terminalWidth == 0 { 222 return 223 } 224 225 tf.RecalculateWidths(terminalWidth, false) 226 } 227 228 // AdjustWidthsToContent will calculate widths of columns by getting the maximum length found for each column 229 // in the input array. If considerHeaders is true, header lengths will also be considered when calculating. 230 // If maxWidth > 0, space will be reduced to accordingly to match the given width. 231 // If force is true, fixed widths will be ignored and scaled as well in the case that maxWidths is exceeded. 232 func (tf *TextColumnsFormatter[T]) AdjustWidthsToContent(entries []*T, considerHeaders bool, maxWidth int, force bool) { 233 columnWidths := make([]int, len(tf.showColumns)) 234 for columnIndex, column := range tf.showColumns { 235 // Get info on fixed columns first 236 if column.col.FixedWidth { 237 columnWidths[columnIndex] = column.calculatedWidth 238 } 239 } 240 for _, entry := range entries { 241 if entry == nil { 242 continue 243 } 244 entryValue := reflect.ValueOf(entry) 245 for columnIndex, column := range tf.showColumns { 246 if column.col.FixedWidth { 247 continue 248 } 249 250 field := column.col.GetRef(entryValue) 251 252 flen := 0 253 switch column.col.Kind() { 254 case reflect.Int, 255 reflect.Int8, 256 reflect.Int16, 257 reflect.Int32, 258 reflect.Int64: 259 flen = len([]rune((strconv.FormatInt(field.Int(), 10)))) 260 case reflect.Uint, 261 reflect.Uint8, 262 reflect.Uint16, 263 reflect.Uint32, 264 reflect.Uint64: 265 flen = len([]rune((strconv.FormatUint(field.Uint(), 10)))) 266 case reflect.Float32, 267 reflect.Float64: 268 flen = len([]rune(strconv.FormatFloat(field.Float(), 'f', column.col.Precision, 64))) 269 case reflect.String: 270 flen = len([]rune(field.String())) 271 default: 272 flen = len([]rune(fmt.Sprintf("%v", field.Interface()))) 273 } 274 275 if columnWidths[columnIndex] < flen { 276 columnWidths[columnIndex] = flen 277 } 278 } 279 } 280 281 if considerHeaders { 282 for columnIndex, column := range tf.showColumns { 283 if column.col.FixedWidth { 284 continue 285 } 286 headerLen := len([]rune(column.col.Name)) 287 if headerLen > columnWidths[columnIndex] { 288 columnWidths[columnIndex] = headerLen 289 } 290 } 291 } 292 293 // Now set calculated widths accordingly 294 totalWidth := 0 295 for columnIndex, column := range tf.showColumns { 296 column.calculatedWidth = columnWidths[columnIndex] 297 totalWidth += column.calculatedWidth 298 } 299 300 tf.buildFillString() 301 302 // Last but not least, add column dividers 303 totalWidth += len([]rune(tf.options.ColumnDivider)) * (len(tf.showColumns) - 1) 304 305 if maxWidth == 0 || totalWidth <= maxWidth { 306 // Yay, it fits! (or user doesn't care) 307 return 308 } 309 310 // Force RecalculateWidths() to run 311 tf.currentMaxWidth = -1 312 313 // We did our best, but let's resize to fit to maxWidth 314 tf.RecalculateWidths(maxWidth, force) 315 }