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  }