2018-08-12 11:31:27 +02:00
|
|
|
package utils
|
|
|
|
|
|
|
|
import (
|
2018-12-04 10:50:11 +02:00
|
|
|
"encoding/json"
|
2018-08-12 11:31:27 +02:00
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2018-09-17 13:02:30 +02:00
|
|
|
"reflect"
|
2019-02-24 08:05:17 +02:00
|
|
|
"regexp"
|
2018-08-12 11:31:27 +02:00
|
|
|
"strings"
|
2018-08-25 07:55:49 +02:00
|
|
|
"time"
|
2018-08-12 11:31:27 +02:00
|
|
|
|
2019-02-11 12:30:27 +02:00
|
|
|
"github.com/go-errors/errors"
|
|
|
|
|
2018-08-12 11:31:27 +02:00
|
|
|
"github.com/fatih/color"
|
|
|
|
)
|
|
|
|
|
2018-08-12 13:04:47 +02:00
|
|
|
// SplitLines takes a multiline string and splits it on newlines
|
|
|
|
// currently we are also stripping \r's which may have adverse effects for
|
|
|
|
// windows users (but no issues have been raised yet)
|
|
|
|
func SplitLines(multilineString string) []string {
|
2018-08-12 11:31:27 +02:00
|
|
|
multilineString = strings.Replace(multilineString, "\r", "", -1)
|
|
|
|
if multilineString == "" || multilineString == "\n" {
|
|
|
|
return make([]string, 0)
|
|
|
|
}
|
|
|
|
lines := strings.Split(multilineString, "\n")
|
|
|
|
if lines[len(lines)-1] == "" {
|
|
|
|
return lines[:len(lines)-1]
|
|
|
|
}
|
|
|
|
return lines
|
|
|
|
}
|
|
|
|
|
2018-08-12 13:04:47 +02:00
|
|
|
// WithPadding pads a string as much as you want
|
|
|
|
func WithPadding(str string, padding int) string {
|
2019-05-18 13:59:33 +02:00
|
|
|
uncoloredStr := Decolorise(str)
|
|
|
|
if padding < len(uncoloredStr) {
|
2018-08-12 11:31:27 +02:00
|
|
|
return str
|
|
|
|
}
|
2019-05-18 13:59:33 +02:00
|
|
|
return str + strings.Repeat(" ", padding-len(uncoloredStr))
|
2018-08-12 11:31:27 +02:00
|
|
|
}
|
|
|
|
|
2018-08-12 13:04:47 +02:00
|
|
|
// ColoredString takes a string and a colour attribute and returns a colored
|
|
|
|
// string with that attribute
|
|
|
|
func ColoredString(str string, colorAttribute color.Attribute) string {
|
2018-08-12 11:31:27 +02:00
|
|
|
colour := color.New(colorAttribute)
|
2018-08-12 13:04:47 +02:00
|
|
|
return ColoredStringDirect(str, colour)
|
2018-08-12 11:31:27 +02:00
|
|
|
}
|
|
|
|
|
2018-08-12 13:04:47 +02:00
|
|
|
// ColoredStringDirect used for aggregating a few color attributes rather than
|
|
|
|
// just sending a single one
|
|
|
|
func ColoredStringDirect(str string, colour *color.Color) string {
|
2018-08-12 11:31:27 +02:00
|
|
|
return colour.SprintFunc()(fmt.Sprint(str))
|
|
|
|
}
|
|
|
|
|
2018-08-13 13:16:21 +02:00
|
|
|
// GetCurrentRepoName gets the repo's base name
|
|
|
|
func GetCurrentRepoName() string {
|
2018-08-12 11:31:27 +02:00
|
|
|
pwd, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalln(err.Error())
|
|
|
|
}
|
|
|
|
return filepath.Base(pwd)
|
|
|
|
}
|
2018-08-13 12:26:02 +02:00
|
|
|
|
|
|
|
// TrimTrailingNewline - Trims the trailing newline
|
|
|
|
// TODO: replace with `chomp` after refactor
|
|
|
|
func TrimTrailingNewline(str string) string {
|
|
|
|
if strings.HasSuffix(str, "\n") {
|
|
|
|
return str[:len(str)-1]
|
|
|
|
}
|
|
|
|
return str
|
|
|
|
}
|
2018-08-19 13:20:50 +02:00
|
|
|
|
2018-08-19 14:48:03 +02:00
|
|
|
// NormalizeLinefeeds - Removes all Windows and Mac style line feeds
|
2018-08-19 13:20:50 +02:00
|
|
|
func NormalizeLinefeeds(str string) string {
|
2018-08-20 15:16:35 +02:00
|
|
|
str = strings.Replace(str, "\r\n", "\n", -1)
|
2018-08-19 14:48:03 +02:00
|
|
|
str = strings.Replace(str, "\r", "", -1)
|
2018-08-19 13:20:50 +02:00
|
|
|
return str
|
|
|
|
}
|
2018-08-19 13:22:48 +02:00
|
|
|
|
2018-08-19 06:48:39 +02:00
|
|
|
// GetProjectRoot returns the path to the root of the project. Only to be used
|
|
|
|
// in testing contexts, as with binaries it's unlikely this path will exist on
|
|
|
|
// the machine
|
|
|
|
func GetProjectRoot() string {
|
2018-08-19 07:05:36 +02:00
|
|
|
dir, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return strings.Split(dir, "lazygit")[0] + "lazygit"
|
2018-08-19 06:48:39 +02:00
|
|
|
}
|
2018-08-25 07:55:49 +02:00
|
|
|
|
2018-08-29 13:31:27 +02:00
|
|
|
// Loader dumps a string to be displayed as a loader
|
2018-08-25 07:55:49 +02:00
|
|
|
func Loader() string {
|
|
|
|
characters := "|/-\\"
|
|
|
|
now := time.Now()
|
|
|
|
nanos := now.UnixNano()
|
|
|
|
index := nanos / 50000000 % int64(len(characters))
|
|
|
|
return characters[index : index+1]
|
|
|
|
}
|
2018-08-31 10:43:54 +02:00
|
|
|
|
|
|
|
// ResolvePlaceholderString populates a template with values
|
|
|
|
func ResolvePlaceholderString(str string, arguments map[string]string) string {
|
|
|
|
for key, value := range arguments {
|
|
|
|
str = strings.Replace(str, "{{"+key+"}}", value, -1)
|
|
|
|
}
|
|
|
|
return str
|
|
|
|
}
|
2018-09-10 12:17:39 +02:00
|
|
|
|
|
|
|
// Min returns the minimum of two integers
|
|
|
|
func Min(x, y int) int {
|
|
|
|
if x < y {
|
|
|
|
return x
|
|
|
|
}
|
|
|
|
return y
|
|
|
|
}
|
2018-09-17 13:02:30 +02:00
|
|
|
|
|
|
|
type Displayable interface {
|
2019-02-16 06:17:44 +02:00
|
|
|
GetDisplayStrings(bool) []string
|
2018-09-17 13:02:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// RenderList takes a slice of items, confirms they implement the Displayable
|
|
|
|
// interface, then generates a list of their displaystrings to write to a panel's
|
|
|
|
// buffer
|
2019-02-16 06:17:44 +02:00
|
|
|
func RenderList(slice interface{}, isFocused bool) (string, error) {
|
2018-09-17 13:02:30 +02:00
|
|
|
s := reflect.ValueOf(slice)
|
|
|
|
if s.Kind() != reflect.Slice {
|
|
|
|
return "", errors.New("RenderList given a non-slice type")
|
|
|
|
}
|
|
|
|
|
|
|
|
displayables := make([]Displayable, s.Len())
|
|
|
|
|
|
|
|
for i := 0; i < s.Len(); i++ {
|
|
|
|
value, ok := s.Index(i).Interface().(Displayable)
|
|
|
|
if !ok {
|
|
|
|
return "", errors.New("item does not implement the Displayable interface")
|
|
|
|
}
|
|
|
|
displayables[i] = value
|
|
|
|
}
|
|
|
|
|
2019-02-16 06:17:44 +02:00
|
|
|
return renderDisplayableList(displayables, isFocused)
|
2018-09-17 13:02:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// renderDisplayableList takes a list of displayable items, obtains their display
|
|
|
|
// strings via GetDisplayStrings() and then returns a single string containing
|
|
|
|
// each item's string representation on its own line, with appropriate horizontal
|
|
|
|
// padding between the item's own strings
|
2019-02-16 06:17:44 +02:00
|
|
|
func renderDisplayableList(items []Displayable, isFocused bool) (string, error) {
|
2018-09-17 13:11:47 +02:00
|
|
|
if len(items) == 0 {
|
|
|
|
return "", nil
|
2018-09-17 13:02:30 +02:00
|
|
|
}
|
|
|
|
|
2019-02-16 06:17:44 +02:00
|
|
|
stringArrays := getDisplayStringArrays(items, isFocused)
|
2018-09-17 13:11:47 +02:00
|
|
|
|
|
|
|
if !displayArraysAligned(stringArrays) {
|
|
|
|
return "", errors.New("Each item must return the same number of strings to display")
|
2018-09-17 13:02:30 +02:00
|
|
|
}
|
|
|
|
|
2018-09-17 13:11:47 +02:00
|
|
|
padWidths := getPadWidths(stringArrays)
|
|
|
|
paddedDisplayStrings := getPaddedDisplayStrings(stringArrays, padWidths)
|
|
|
|
|
|
|
|
return strings.Join(paddedDisplayStrings, "\n"), nil
|
|
|
|
}
|
|
|
|
|
2019-02-25 13:11:35 +02:00
|
|
|
// Decolorise strips a string of color
|
|
|
|
func Decolorise(str string) string {
|
2019-02-24 08:05:17 +02:00
|
|
|
re := regexp.MustCompile(`\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]`)
|
|
|
|
return re.ReplaceAllString(str, "")
|
|
|
|
}
|
|
|
|
|
2018-09-17 13:11:47 +02:00
|
|
|
func getPadWidths(stringArrays [][]string) []int {
|
2018-09-19 10:03:44 +02:00
|
|
|
if len(stringArrays[0]) <= 1 {
|
|
|
|
return []int{}
|
|
|
|
}
|
|
|
|
padWidths := make([]int, len(stringArrays[0])-1)
|
|
|
|
for i := range padWidths {
|
2018-09-17 13:11:47 +02:00
|
|
|
for _, strings := range stringArrays {
|
2019-02-25 13:11:35 +02:00
|
|
|
uncoloredString := Decolorise(strings[i])
|
2019-02-24 08:05:17 +02:00
|
|
|
if len(uncoloredString) > padWidths[i] {
|
|
|
|
padWidths[i] = len(uncoloredString)
|
2018-09-17 13:02:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-09-17 13:11:47 +02:00
|
|
|
return padWidths
|
|
|
|
}
|
2018-09-17 13:02:30 +02:00
|
|
|
|
2018-09-17 13:11:47 +02:00
|
|
|
func getPaddedDisplayStrings(stringArrays [][]string, padWidths []int) []string {
|
|
|
|
paddedDisplayStrings := make([]string, len(stringArrays))
|
|
|
|
for i, stringArray := range stringArrays {
|
2018-09-19 10:03:44 +02:00
|
|
|
if len(stringArray) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2018-09-17 13:02:30 +02:00
|
|
|
for j, padWidth := range padWidths {
|
2018-09-17 13:11:47 +02:00
|
|
|
paddedDisplayStrings[i] += WithPadding(stringArray[j], padWidth) + " "
|
2018-09-17 13:02:30 +02:00
|
|
|
}
|
2018-09-19 10:03:44 +02:00
|
|
|
paddedDisplayStrings[i] += stringArray[len(padWidths)]
|
2018-09-17 13:02:30 +02:00
|
|
|
}
|
2018-09-17 13:11:47 +02:00
|
|
|
return paddedDisplayStrings
|
|
|
|
}
|
2018-09-17 13:02:30 +02:00
|
|
|
|
2018-09-17 13:11:47 +02:00
|
|
|
// displayArraysAligned returns true if every string array returned from our
|
|
|
|
// list of displayables has the same length
|
|
|
|
func displayArraysAligned(stringArrays [][]string) bool {
|
|
|
|
for _, strings := range stringArrays {
|
|
|
|
if len(strings) != len(stringArrays[0]) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2019-02-16 06:17:44 +02:00
|
|
|
func getDisplayStringArrays(displayables []Displayable, isFocused bool) [][]string {
|
2018-09-17 13:11:47 +02:00
|
|
|
stringArrays := make([][]string, len(displayables))
|
|
|
|
for i, item := range displayables {
|
2019-02-16 06:17:44 +02:00
|
|
|
stringArrays[i] = item.GetDisplayStrings(isFocused)
|
2018-09-17 13:11:47 +02:00
|
|
|
}
|
|
|
|
return stringArrays
|
2018-09-17 13:02:30 +02:00
|
|
|
}
|
2018-09-19 12:36:40 +02:00
|
|
|
|
|
|
|
// IncludesString if the list contains the string
|
|
|
|
func IncludesString(list []string, a string) bool {
|
|
|
|
for _, b := range list {
|
|
|
|
if b == a {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
2018-12-05 10:33:46 +02:00
|
|
|
|
|
|
|
// NextIndex returns the index of the element that comes after the given number
|
|
|
|
func NextIndex(numbers []int, currentNumber int) int {
|
|
|
|
for index, number := range numbers {
|
|
|
|
if number > currentNumber {
|
|
|
|
return index
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
// PrevIndex returns the index that comes before the given number, cycling if we reach the end
|
|
|
|
func PrevIndex(numbers []int, currentNumber int) int {
|
|
|
|
end := len(numbers) - 1
|
|
|
|
for i := end; i >= 0; i -= 1 {
|
|
|
|
if numbers[i] < currentNumber {
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return end
|
|
|
|
}
|
2018-12-04 10:50:11 +02:00
|
|
|
|
|
|
|
func AsJson(i interface{}) string {
|
|
|
|
bytes, _ := json.MarshalIndent(i, "", " ")
|
|
|
|
return string(bytes)
|
|
|
|
}
|