diff --git a/README.md b/README.md index 0820f80..15ed914 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,12 @@ The configuration is stored in `$XDG_CONFIG_HOME/gotz/config.json` (usually `~/. // Indicates whether to colorize the blocks "hours12": false, // Indicates whether to use 12-hour format - "live": false + "live": false, + // Selects the sorting of the timezones + // (one of 'name' - lexicographically, 'offset' - TZ offset, 'none' - user defined) + "sorting": "name", + // Indicates whether to keep the local timezone on top when using sorting + "sort_local_top": true } ``` diff --git a/core/args.go b/core/args.go index da05ccc..da8dbf6 100644 --- a/core/args.go +++ b/core/args.go @@ -16,7 +16,7 @@ func ParseFlags(startConfig Config, appVersion string) (Config, time.Time, bool, // Check for any changes var changed bool // Define configuration flags - var timezones, symbols, tics, stretch, inline, colorize, hours12, live string + var timezones, symbols, tics, stretch, inline, colorize, hours12, live, sorting, sortLocalTop string flag.StringVar( &timezones, "timezones", @@ -69,6 +69,21 @@ func ParseFlags(startConfig Config, appVersion string) (Config, time.Time, bool, "", "indicates whether to display time live (quit via 'q' or 'Ctrl+C') (one of: true, false)", ) + flag.StringVar( + &sorting, + "sorting", + SortingModeDefault, + "indicates how to sort the timezones (one of: "+ + SortingModeNone+", "+ + SortingModeOffset+", "+ + SortingModeName+")", + ) + flag.StringVar( + &sortLocalTop, + "sort-local-top", + "", + "indicates whether to keep the local timezone at the top (one of: true, false)", + ) // Define direct flags var requestTime string @@ -166,6 +181,23 @@ func ParseFlags(startConfig Config, appVersion string) (Config, time.Time, bool, return startConfig, rt, changed, fmt.Errorf("invalid value for live: %s", live) } } + if sorting != "" { + changed = true + if !isValidSortingMode(sorting) { + return startConfig, rt, changed, fmt.Errorf("invalid sorting mode: %s", sorting) + } + startConfig.Sorting = sorting + } + if sortLocalTop != "" { + changed = true + if strings.ToLower(sortLocalTop) == "true" { + startConfig.SortLocalTop = true + } else if strings.ToLower(sortLocalTop) == "false" { + startConfig.SortLocalTop = false + } else { + return startConfig, rt, changed, fmt.Errorf("invalid value for sort-local-top: %s", sortLocalTop) + } + } // Handle direct flags if requestTime != "" { diff --git a/core/auxiliary.go b/core/auxiliary.go index e87791a..0572fd6 100644 --- a/core/auxiliary.go +++ b/core/auxiliary.go @@ -10,17 +10,6 @@ import ( "golang.org/x/term" ) -// maxStringLength returns the length of the longest string in the given slice. -func maxStringLength(s []string) int { - length := 0 - for _, str := range s { - if len(str) > length { - length = len(str) - } - } - return length -} - // getTerminalWidth returns the width of the terminal. func getTerminalWidth() int { width, _, err := term.GetSize(0) diff --git a/core/configuration.go b/core/configuration.go index 5047cfd..df86da5 100644 --- a/core/configuration.go +++ b/core/configuration.go @@ -38,6 +38,12 @@ type Config struct { // Indicates whether to continuously update. Live bool `json:"live"` + + // Defines the mode for sorting the timezones. + Sorting string `json:"sorting"` + // SortLocalTop indicates whether the local timezone should be kept at the + // top (independent of the sorting mode). + SortLocalTop bool `json:"sort_local_top"` } // Location describes a timezone the user wants to display. diff --git a/core/plot.go b/core/plot.go index 4abd1b5..9e60f7c 100644 --- a/core/plot.go +++ b/core/plot.go @@ -329,36 +329,58 @@ func PlotTime(plt Plotter, cfg Config, t time.Time) error { } // createTimeInfos creates the time info strings for all locations. -func createTimeInfos(cfg Config, t time.Time) (timeInfos []string, times []*time.Location, err error) { - // Init - timeInfos = make([]string, len(cfg.Timezones)+1) - - // Prepare timeZones to plot - timeZones := make([]*time.Location, len(cfg.Timezones)+1) - descriptions := make([]string, len(cfg.Timezones)+1) - timeZones[0] = time.Local - descriptions[0] = "Local" +func createTimeInfos(cfg Config, t time.Time) (timeInfos []string, timeZones []*time.Location, err error) { + // Prepare timezones for plotting + locations := make([]locationContainer, len(cfg.Timezones)+1) + now := time.Now() + _, localOffset := now.In(time.Local).Zone() + locations[0] = locationContainer{ + location: time.Local, + description: "Local", + offset: localOffset, + } for i, tz := range cfg.Timezones { // Get timezone loc, err := time.LoadLocation(tz.TZ) if err != nil { return nil, nil, fmt.Errorf("error loading timezone %s: %s", tz.TZ, err) } + _, offset := now.In(loc).Zone() // Store timezone - timeZones[i+1] = loc - descriptions[i+1] = tz.Name + locations[i+1] = locationContainer{ + location: loc, + description: tz.Name, + offset: offset, + } + } + + // Sort timezones and convert + if cfg.Sorting != SortingModeNone { + sortLocations(locations, cfg.Sorting, cfg.SortLocalTop) } - descriptionLength := maxStringLength(descriptions) - for i := range timeZones { + // Determine max description length + descriptionLength := 0 + for _, location := range locations { + if len(location.description) > descriptionLength { + descriptionLength = len(location.description) + } + } + + timeInfos = make([]string, len(cfg.Timezones)+1) + timeZones = make([]*time.Location, len(cfg.Timezones)+1) + for i, location := range locations { // Prepare location and time infos - timeInfo := fmt.Sprintf("%-*s", descriptionLength, descriptions[i]) + timeInfo := fmt.Sprintf("%-*s", descriptionLength, location.description) timeInfo = fmt.Sprintf( "%s: %s %s", timeInfo, - formatDay(cfg.Hours12, t.In(timeZones[i])), - formatTime(cfg.Hours12, true, t.In(timeZones[i]))) + formatDay(cfg.Hours12, t.In(location.location)), + formatTime(cfg.Hours12, true, t.In(location.location)), + ) + // Store time info and timezone timeInfos[i] = timeInfo + timeZones[i] = location.location } return timeInfos, timeZones, nil diff --git a/core/sorting.go b/core/sorting.go new file mode 100644 index 0000000..f51a761 --- /dev/null +++ b/core/sorting.go @@ -0,0 +1,59 @@ +package core + +import ( + "sort" + "time" +) + +// Define sorting modes +const ( + // SortingModeNone keeps the order of the timezones as they are defined. + SortingModeNone = "none" + // SortingModeOffset sorts the timezones by their offset. + SortingModeOffset = "offset" + // SortingModeName sorts the timezones by their name. + SortingModeName = "name" + // SortingModeDefault is the default sorting mode. + SortingModeDefault = SortingModeNone +) + +// isValidSortingMode checks if the given sorting mode is defined and valid. +func isValidSortingMode(mode string) bool { + switch mode { + case SortingModeNone, SortingModeOffset, SortingModeName: + return true + default: + return false + } +} + +// locationContainer is a container for a location with additional information. +type locationContainer struct { + location *time.Location + description string + offset int +} + +// sortLocations sorts the given locations based on the given sorting mode. +func sortLocations(locations []locationContainer, sortingMode string, localTop bool) { + sort.Slice(locations, func(i, j int) bool { + // If the local timezone should be kept at the top, check if one of the + // locations is the local timezone. + if localTop { + if locations[i].location == time.Local { + return true + } else if locations[j].location == time.Local { + return false + } + } + // Sort based on the sorting mode + switch sortingMode { + case SortingModeOffset: + return locations[i].offset < locations[j].offset + case SortingModeName: + return locations[i].description < locations[j].description + default: + return i < j + } + }) +} diff --git a/go.mod b/go.mod index dc18caf..42bc678 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/merschformann/gotz -go 1.17 +go 1.23.0 require ( github.com/adrg/xdg v0.4.0 diff --git a/tests/testdata/static_sort.golden b/tests/testdata/static_sort.golden new file mode 100644 index 0000000..641ce86 --- /dev/null +++ b/tests/testdata/static_sort.golden @@ -0,0 +1,11 @@ + now v 16:00 +Local : Sat 24 Aug 1985 14:00 | + ▒▒▒▒▒▒██████████████████|███████████▒▒▒▒▒▒▒▒▒▒▒▒ +Berlin : Sat 24 Aug 1985 16:00 | + ▒▒▒▒▒▒████████████████████████|█████▒▒▒▒▒▒▒▒▒▒▒▒ +New York: Sat 24 Aug 1985 10:00 | + ▒▒▒▒▒▒██████|███████████████████████▒▒▒▒▒▒▒▒▒▒▒▒ +Shanghai: Sat 24 Aug 1985 22:00 | +████████████████████████▒▒▒▒▒▒▒▒▒▒▒▒| ▒▒▒▒▒▒██████ +Sydney : Sun 25 Aug 1985 00:00 | +██████████████████▒▒▒▒▒▒▒▒▒▒▒▒ | ▒▒▒▒▒▒████████████ diff --git a/tests/testdata/static_sort.json b/tests/testdata/static_sort.json new file mode 100644 index 0000000..d0fa69a --- /dev/null +++ b/tests/testdata/static_sort.json @@ -0,0 +1,50 @@ +{ + "config_version": "1.0", + "timezones": [ + { + "Name": "New York", + "TZ": "America/New_York" + }, + { + "Name": "Berlin", + "TZ": "Europe/Berlin" + }, + { + "Name": "Shanghai", + "TZ": "Asia/Shanghai" + }, + { + "Name": "Sydney", + "TZ": "Australia/Sydney" + } + ], + "style": { + "symbols": "rectangles", + "colorize": false, + "day_segments": { + "morning": 6, + "day": 8, + "evening": 18, + "night": 22 + }, + "coloring": { + "StaticColorMorning": "red", + "StaticColorDay": "yellow", + "StaticColorEvening": "red", + "StaticColorNight": "blue", + "StaticColorForeground": "", + "DynamicColorMorning": "red", + "DynamicColorDay": "yellow", + "DynamicColorEvening": "red", + "DynamicColorNight": "blue", + "DynamicColorForeground": "", + "DynamicColorBackground": "" + } + }, + "tics": false, + "stretch": true, + "hours12": false, + "live": false, + "sorting": "name", + "sort_local_top": true +}