Skip to content

Commit

Permalink
misc: update location output, add custom area printer (#96)
Browse files Browse the repository at this point in the history
* misc: update location output, add custom area printer

* update AreaClear
radulucut authored Mar 19, 2024
1 parent 52b7a42 commit 2c844ae
Showing 19 changed files with 356 additions and 513 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: ">=1.21.3"
go-version: ">=1.22"
cache: true

- uses: goreleaser/goreleaser-action@v4
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go: ["1.21"]
go: ["1.22"]
os: [ubuntu-latest, macOS-latest, windows-latest]
name: ${{ matrix.os }} Go ${{ matrix.go }} Tests
steps:
42 changes: 21 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -119,7 +119,7 @@ For example, if you want to run ping from a probe in Seattle that is also part o

```bash
globalping ping google.com from Comcast+Seattle
> NA, US, (WA), Seattle, ASN:7922, Comcast Cable Communications, LLC
> Seattle (WA), US, NA, Comcast Cable Communications, LLC (AS33650)
PING (142.250.217.78) 56(84) bytes of data.
64 bytes from sea09s29-in-f14.1e100.net (142.250.217.78): icmp_seq=1 ttl=58 time=14.0 ms
64 bytes from sea09s29-in-f14.1e100.net (142.250.217.78): icmp_seq=2 ttl=58 time=14.5 ms
@@ -141,22 +141,22 @@ With the following command, we execute four ping commands at four different loca

```bash
globalping ping google.com from Amazon,Germany,USA,Dallas --limit 4 --latency
> AS, KR, Seoul, ASN:16509, Amazon.com, Inc. (aws-ap-northeast-2)
> Seoul, KR, AS, Amazon.com, Inc. (AS16509) (aws-ap-northeast-2)
Min: 33.163 ms
Max: 33.256 ms
Avg: 33.22 ms

> EU, DE, Frankfurt, ASN:16276, OVH SAS
> Frankfurt, DE, EU, DE, OVH SAS (AS16276)
Min: 1.221 ms
Max: 1.291 ms
Avg: 1.264 ms

> NA, US, (IL), Chicago, ASN:174, Cogent Communications
> Chicago (IL), US, NA, Cogent Communications (AS174)
Min: 112.405 ms
Max: 112.686 ms
Avg: 112.528 ms

> NA, US, (TX), Dallas, ASN:393336, Catalyst Host LLC
> Dallas (TX), US, NA, Catalyst Host LLC (AS393336)
Min: 1.579 ms
Max: 1.588 ms
Avg: 1.584 ms
@@ -179,7 +179,7 @@ Include a link at the bottom of your results using the `--share` flag to view an
```bash
globalping dns google.com from gcp-asia-south1 --share
> AS, IN, Mumbai, ASN:396982, Google LLC (gcp-asia-south1)
> Mumbai, IN, AS, Google LLC (AS396982) (gcp-asia-south1)
; <<>> DiG 9.16.37-Debian <<>> -t A google.com -p 53 -4 +timeout=3 +tries=2 +nocookie +nsid
;; global options: +cmd
;; Got answer:
@@ -207,7 +207,7 @@ You can select the same probes used in a previous measurement by passing the mea
```bash
globalping dns google.com from rvasVvKnj48cxNjC
> AS, IN, Mumbai, ASN:396982, Google LLC (gcp-asia-south1)
> Mumbai, IN, AS, Google LLC (AS396982) (gcp-asia-south1)
; <<>> DiG 9.16.42-Debian <<>> -t A google.com -p 53 -4 +timeout=3 +tries=2 +nocookie +nosplit +nsid
;; global options: +cmd
;; Got answer:
@@ -234,43 +234,43 @@ Use `[@1 | first, @2 ... @-2, @-1 | last | previous]` to select the probes from
```bash
globalping ping google.com from USA --latency
> NA, US, (VA), Ashburn, ASN:213230, Hetzner Online GmbH
> Ashburn (VA), US, NA, Hetzner Online GmbH (AS213230)
Min: 7.314 ms
Max: 7.413 ms
Avg: 7.359 ms
globalping ping google.com from Germany --latency
> EU, DE, Falkenstein, ASN:24940, Hetzner Online GmbH
> Falkenstein, DE, EU, Hetzner Online GmbH (AS24940)
Min: 4.87 ms
Max: 4.936 ms
Avg: 4.911 ms
globalping ping google.com from previous --latency
> EU, DE, Falkenstein, ASN:24940, Hetzner Online GmbH
> Falkenstein, DE, EU, Hetzner Online GmbH (AS24940)
Min: 4.87 ms
Max: 4.936 ms
Avg: 4.911 ms
globalping ping google.com from @-1 --latency
> EU, DE, Falkenstein, ASN:24940, Hetzner Online GmbH
> Falkenstein, DE, EU, Hetzner Online GmbH (AS24940)
Min: 4.87 ms
Max: 4.936 ms
Avg: 4.911 ms
globalping ping google.com from @-2 --latency
> NA, US, (VA), Ashburn, ASN:213230, Hetzner Online GmbH
> Ashburn (VA), US, NA, Hetzner Online GmbH (AS213230)
Min: 7.314 ms
Max: 7.413 ms
Avg: 7.359 ms
globalping ping google.com from first --latency
> NA, US, (VA), Ashburn, ASN:213230, Hetzner Online GmbH
> Ashburn (VA), US, NA, Hetzner Online GmbH (AS213230)
Min: 7.314 ms
Max: 7.413 ms
Avg: 7.359 ms
globalping ping google.com from @1 --latency
> NA, US, (VA), Ashburn, ASN:213230, Hetzner Online GmbH
> Ashburn (VA), US, NA, Hetzner Online GmbH (AS213230)
Min: 7.314 ms
Max: 7.413 ms
Avg: 7.359 ms
@@ -287,7 +287,7 @@ This means that eventually you will run out of credits and the test will stop.
```bash
globalping ping cdn.jsdelivr.net from Europe --infinite
> EU, GB, London, ASN:40676, Psychz Networks
> London, GB, EU, Psychz Networks (AS40676)
PING cdn.jsdelivr.net (151.101.1.229) 56(84) bytes of data.
64 bytes from 151.101.1.229 (151.101.1.229): icmp_seq=1 ttl=59 time=0.54 ms
64 bytes from 151.101.1.229 (151.101.1.229): icmp_seq=2 ttl=59 time=0.42 ms
@@ -298,12 +298,12 @@ If you select multiple probes when using `--infinite` the output will change to
```bash
globalping ping cdn.jsdelivr.net from Europe --limit 5 --infinite
Location | Sent | Loss | Last | Min | Avg | Max
EU, GB, London, ASN:16276, OVH SAS | 22 | 0.00% | 3.33 ms | 3.07 ms | 3.20 ms | 3.33 ms
EU, DE, Falkenstein, ASN:24940, Hetzner Online GmbH | 22 | 0.00% | 5.41 ms | 5.30 ms | 5.78 ms | 13.1 ms
EU, AT, Vienna, ASN:57169, EDIS GmbH | 22 | 0.00% | 0.47 ms | 0.46 ms | 0.56 ms | 0.88 ms
EU, SE, Stockholm, ASN:20473, The Constant Company, LLC | 22 | 0.00% | 1.03 ms | 0.83 ms | 1.15 ms | 4.66 ms
EU, ES, Madrid, ASN:47787, EDGOO NETWORKS LLC | 22 | 0.00% | 0.24 ms | 0.13 ms | 0.26 ms | 0.42 ms
Location | Sent | Loss | Last | Min | Avg | Max
London, GB, EU, OVH SAS (AS16276) | 22 | 0.00% | 3.33 ms | 3.07 ms | 3.20 ms | 3.33 ms
Falkenstein, DE, EU, Hetzner Online GmbH (AS24940) | 22 | 0.00% | 5.41 ms | 5.30 ms | 5.78 ms | 13.1 ms
Vienna, AT, EU, EDIS GmbH (AS57169) | 22 | 0.00% | 0.47 ms | 0.46 ms | 0.56 ms | 0.88 ms
Stockholm, SE, EU, The Constant Company, LLC (AS20473) | 22 | 0.00% | 1.03 ms | 0.83 ms | 1.15 ms | 4.66 ms
Madrid, ES, EU, EDGOO NETWORKS LLC (AS47787) | 22 | 0.00% | 0.24 ms | 0.13 ms | 0.26 ms | 0.42 ms
^C
```
32 changes: 7 additions & 25 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,49 +1,31 @@
module github.com/jsdelivr/globalping-cli

go 1.21

toolchain go1.21.3
go 1.22

require (
github.com/andybalholm/brotli v1.1.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/icza/backscanner v0.0.0-20230330133933-bf6beb754c70
github.com/icza/backscanner v0.0.0-20240221180818-f23e3ba0e79f
github.com/mattn/go-runewidth v0.0.15
github.com/pkg/errors v0.9.1
github.com/pterm/pterm v0.12.78
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.9.0
go.uber.org/mock v0.4.0
golang.org/x/term v0.18.0
)

require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/sys v0.18.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
137 changes: 12 additions & 125 deletions go.sum

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions view/context.go
Original file line number Diff line number Diff line change
@@ -3,8 +3,6 @@ package view
import (
"math"
"time"

"github.com/pterm/pterm"
)

type Context struct {
@@ -35,7 +33,6 @@ type Context struct {

RecordToSession bool // Record measurement to session history

Area *pterm.AreaPrinter
Hostname string
IsHeaderPrinted bool
AggregatedStats []*MeasurementStats
4 changes: 2 additions & 2 deletions view/default.go
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ func (v *viewer) outputDefault(id string, data *globalping.Measurement, m *globa
}

// Output slightly different format if state is available
v.printer.Println(generateProbeInfo(result, !v.ctx.CIMode))
v.printer.Println(v.getProbeInfo(result))

if v.isBodyOnlyHttpGet(m) {
v.printer.Println(strings.TrimSpace(result.Result.RawBody))
@@ -26,6 +26,6 @@ func (v *viewer) outputDefault(id string, data *globalping.Measurement, m *globa
}

if v.ctx.Share {
v.printer.Println(formatWithLeadingArrow(shareMessage(id), !v.ctx.CIMode))
v.printer.Println(v.getShareMessage(id))
}
}
20 changes: 10 additions & 10 deletions view/default_test.go
Original file line number Diff line number Diff line change
@@ -69,10 +69,10 @@ func Test_Output_Default_HTTP_Get(t *testing.T) {

viewer.Output(measurementID1, m)

assert.Equal(t, `> EU, DE, Berlin, ASN:123, Network 1
assert.Equal(t, `> Berlin, DE, EU, Network 1 (AS123)
Body 1
> NA, US, (NY), New York, ASN:567, Network 2
> New York (NY), US, NA, Network 2 (AS567)
Body 2
`, w.String())
}
@@ -135,10 +135,10 @@ func Test_Output_Default_HTTP_Get_Share(t *testing.T) {

viewer.Output(measurementID1, m)

assert.Equal(t, fmt.Sprintf(`> EU, DE, Berlin, ASN:123, Network 1
assert.Equal(t, fmt.Sprintf(`> Berlin, DE, EU, Network 1 (AS123)
Body 1
> NA, US, (NY), New York, ASN:567, Network 2
> New York (NY), US, NA, Network 2 (AS567)
Body 2
> View the results online: https://www.jsdelivr.com/globalping?measurement=%s
`, measurementID1), w.String())
@@ -201,11 +201,11 @@ func Test_Output_Default_HTTP_Get_Full(t *testing.T) {

viewer.Output(measurementID1, m)

assert.Equal(t, `> EU, DE, Berlin, ASN:123, Network 1
assert.Equal(t, `> Berlin, DE, EU, Network 1 (AS123)
Headers 1
Body 1
> NA, US, (NY), New York, ASN:567, Network 2
> New York (NY), US, NA, Network 2 (AS567)
Headers 2
Body 2
`, w.String())
@@ -266,10 +266,10 @@ func Test_Output_Default_HTTP_Head(t *testing.T) {

viewer.Output(measurementID1, m)

assert.Equal(t, `> EU, DE, Berlin, ASN:123, Network 1
assert.Equal(t, `> Berlin, DE, EU, Network 1 (AS123)
Headers 1
> NA, US, (NY), New York, ASN:567, Network 2
> New York (NY), US, NA, Network 2 (AS567)
Headers 2
`, w.String())
}
@@ -321,10 +321,10 @@ func Test_Output_Default_Ping(t *testing.T) {

viewer.Output(measurementID1, m)

assert.Equal(t, `> EU, DE, Berlin, ASN:123, Network 1
assert.Equal(t, `> Berlin, DE, EU, Network 1 (AS123)
Ping Results 1
> NA, US, (NY), New York, ASN:567, Network 2
> New York (NY), US, NA, Network 2 (AS567)
Ping Results 2
`, w.String())
}
47 changes: 16 additions & 31 deletions view/infinite.go
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@ import (

"github.com/jsdelivr/globalping-cli/globalping"
"github.com/mattn/go-runewidth"
"github.com/pterm/pterm"
)

// Table defaults
@@ -54,7 +53,7 @@ func (v *viewer) outputStreamingPackets(m *globalping.Measurement) error {
hm.Stats[0] = parsedOutput.Stats
if !v.ctx.IsHeaderPrinted {
v.ctx.Hostname = parsedOutput.Hostname
v.printer.Println(generateProbeInfo(probeMeasurement, !v.ctx.CIMode))
v.printer.Println(v.getProbeInfo(probeMeasurement))
v.printer.Printf("PING %s (%s) %s bytes of data.\n",
parsedOutput.Hostname,
parsedOutput.Address,
@@ -74,23 +73,18 @@ func (v *viewer) outputStreamingPackets(m *globalping.Measurement) error {
}

func (v *viewer) outputTableView(m *globalping.Measurement) error {
var err error
if len(v.ctx.AggregatedStats) == 0 {
// Initialize state
v.ctx.AggregatedStats = make([]*MeasurementStats, len(m.Results))
for i := range m.Results {
v.ctx.AggregatedStats[i] = NewMeasurementStats()
}
// Create new writer
v.ctx.Area, err = pterm.DefaultArea.Start()
if err != nil {
return errors.New("failed to start writer: " + err.Error())
}
}
hm := v.ctx.History.Find(m.ID)
o, newStats, newAggregatedStats := v.generateTable(hm, m, pterm.GetTerminalWidth()-2)
width, _ := v.printer.GetSize()
o, newStats, newAggregatedStats := v.generateTable(hm, m, width-2)
hm.Stats = newStats
v.ctx.Area.Update(*o)
v.printer.AreaUpdate(o)
if m.Status != globalping.StatusInProgress {
v.ctx.AggregatedStats = newAggregatedStats
}
@@ -99,7 +93,7 @@ func (v *viewer) outputTableView(m *globalping.Measurement) error {

func (v *viewer) outputFailSummary(m *globalping.Measurement) error {
for i := range m.Results {
v.printer.Println(generateProbeInfo(&m.Results[i], !v.ctx.CIMode))
v.printer.Println(v.getProbeInfo(&m.Results[i]))
v.printer.Println(m.Results[i].Result.RawOutput)
}
return errors.New("all probes failed")
@@ -158,9 +152,9 @@ func (v *viewer) generateTable(hm *HistoryItem, m *globalping.Measurement, areaW
for i := range table {
table[i][0] = strings.ReplaceAll(table[i][0], "\t", " ") // Replace tabs with spaces
lines := strings.Split(table[i][0], "\n") // Split first column into lines
color := pterm.Reset // No color
color := ColorNone // No color
if i == 0 && !v.ctx.CIMode {
color = pterm.FgLightCyan
color = ColorLightCyan
}
for k := range lines {
width := runewidth.StringWidth(lines[k])
@@ -172,14 +166,19 @@ func (v *viewer) generateTable(hm *HistoryItem, m *globalping.Measurement, areaW
} else if colMax[0] > width {
lines[k] = runewidth.FillRight(lines[k], colMax[0])
}
if color != 0 {
lines[k] = pterm.NewStyle(color).Sprint(lines[k])
if color != "" {
lines[k] = v.printer.Color(lines[k], color)
}
}
for j := 1; j < len(table[i]); j++ {
lines[0] += colSeparator + formatValue(table[i][j], color, colMax[j], j != 0)
lines[0] += colSeparator
if j == 0 {
lines[0] += v.printer.FillRightAndColor(table[i][j], colMax[j], color)
} else {
lines[0] += v.printer.FillLeftAndColor(table[i][j], colMax[j], color)
}
for k := 1; k < len(lines); k++ {
lines[k] += colSeparator + formatValue("", 0, colMax[j], false)
lines[k] += colSeparator + v.printer.FillLeft("", colMax[j])
}
}
for j := 0; j < len(lines); j++ {
@@ -255,20 +254,6 @@ func getRowValues(stats *MeasurementStats) [7]string {
}
}

func formatValue(v string, color pterm.Color, width int, toRight bool) string {
for len(v) < width {
if toRight {
v = " " + v
} else {
v = v + " "
}
}
if color != 0 {
v = pterm.NewStyle(color).Sprint(v)
}
return v
}

type ParsedPingOutput struct {
Hostname string
Address string
257 changes: 103 additions & 154 deletions view/infinite_test.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion view/json.go
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ func (v *viewer) OutputJson(id string) error {
v.printer.Println(string(output))

if v.ctx.Share {
v.printer.Println(formatWithLeadingArrow(shareMessage(id), !v.ctx.CIMode))
v.printer.Println(v.getShareMessage(id))
}
v.printer.Println()

1 change: 1 addition & 0 deletions view/json_test.go
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ func Test_Output_Json(t *testing.T) {
&Context{
ToJSON: true,
Share: true,
CIMode: true,
},
NewPrinter(nil, w, w),
nil,
31 changes: 15 additions & 16 deletions view/latency.go
Original file line number Diff line number Diff line change
@@ -16,52 +16,51 @@ func (v *viewer) OutputLatency(id string, data *globalping.Measurement) error {
v.printer.Println()
}

v.printer.Println(generateProbeInfo(&result, !v.ctx.CIMode))
v.printer.Println(v.getProbeInfo(&result))

switch v.ctx.Cmd {
case "ping":
stats, err := globalping.DecodePingStats(result.Result.StatsRaw)
if err != nil {
return err
}
v.printer.Println(v.latencyStatHeader("Min", v.ctx.CIMode) + fmt.Sprintf("%.2f ms", stats.Min))
v.printer.Println(v.latencyStatHeader("Max", v.ctx.CIMode) + fmt.Sprintf("%.2f ms", stats.Max))
v.printer.Println(v.latencyStatHeader("Avg", v.ctx.CIMode) + fmt.Sprintf("%.2f ms", stats.Avg))
v.printer.Println(v.latencyStatHeader("Min") + fmt.Sprintf("%.2f ms", stats.Min))
v.printer.Println(v.latencyStatHeader("Max") + fmt.Sprintf("%.2f ms", stats.Max))
v.printer.Println(v.latencyStatHeader("Avg") + fmt.Sprintf("%.2f ms", stats.Avg))
case "dns":
timings, err := globalping.DecodeDNSTimings(result.Result.TimingsRaw)
if err != nil {
return err
}
v.printer.Println(v.latencyStatHeader("Total", v.ctx.CIMode) + fmt.Sprintf("%v ms", timings.Total))
v.printer.Println(v.latencyStatHeader("Total") + fmt.Sprintf("%v ms", timings.Total))
case "http":
timings, err := globalping.DecodeHTTPTimings(result.Result.TimingsRaw)
if err != nil {
return err
}
v.printer.Println(v.latencyStatHeader("Total", v.ctx.CIMode) + fmt.Sprintf("%v ms", timings.Total))
v.printer.Println(v.latencyStatHeader("Download", v.ctx.CIMode) + fmt.Sprintf("%v ms", timings.Download))
v.printer.Println(v.latencyStatHeader("First byte", v.ctx.CIMode) + fmt.Sprintf("%v ms", timings.FirstByte))
v.printer.Println(v.latencyStatHeader("DNS", v.ctx.CIMode) + fmt.Sprintf("%v ms", timings.DNS))
v.printer.Println(v.latencyStatHeader("TLS", v.ctx.CIMode) + fmt.Sprintf("%v ms", timings.TLS))
v.printer.Println(v.latencyStatHeader("TCP", v.ctx.CIMode) + fmt.Sprintf("%v ms", timings.TCP))
v.printer.Println(v.latencyStatHeader("Total") + fmt.Sprintf("%v ms", timings.Total))
v.printer.Println(v.latencyStatHeader("Download") + fmt.Sprintf("%v ms", timings.Download))
v.printer.Println(v.latencyStatHeader("First byte") + fmt.Sprintf("%v ms", timings.FirstByte))
v.printer.Println(v.latencyStatHeader("DNS") + fmt.Sprintf("%v ms", timings.DNS))
v.printer.Println(v.latencyStatHeader("TLS") + fmt.Sprintf("%v ms", timings.TLS))
v.printer.Println(v.latencyStatHeader("TCP") + fmt.Sprintf("%v ms", timings.TCP))
default:
return errors.New("unexpected command for latency output: " + v.ctx.Cmd)
}
}

if v.ctx.Share {
v.printer.Println(formatWithLeadingArrow(shareMessage(id), !v.ctx.CIMode))
v.printer.Println(v.getShareMessage(id))
}
v.printer.Println()

return nil
}

func (v *viewer) latencyStatHeader(title string, ci bool) string {
func (v *viewer) latencyStatHeader(title string) string {
text := fmt.Sprintf("%s: ", title)
if ci {
if v.ctx.CIMode {
return text
} else {
return terminalLayoutBold.Render(text)
}
return v.printer.Bold(text)
}
47 changes: 20 additions & 27 deletions view/latency_test.go
Original file line number Diff line number Diff line change
@@ -65,17 +65,14 @@ func Test_Output_Latency_Ping_Not_CI(t *testing.T) {
err := viewer.Output(measurementID1, &globalping.MeasurementCreate{})
assert.NoError(t, err)

assert.Equal(t, `> Continent, Country, (State), City, ASN:12345, Network (tag-1)
Min: 8.00 ms
Max: 20.00 ms
Avg: 12.00 ms
> Continent B, Country B, (State B), City B, ASN:12349, Network B
Min: 9.00 ms
Max: 22.00 ms
Avg: 15.00 ms
`, w.String())
assert.Equal(t, "\033[1;38;2;23;212;167m> City (State), Country, Continent, Network (AS12345) (tag-1)\033[0m\n"+
"\033[1mMin: \033[0m8.00 ms\n"+
"\033[1mMax: \033[0m20.00 ms\n"+
"\033[1mAvg: \033[0m12.00 ms\n\n"+
"\033[1;38;2;23;212;167m> City B (State B), Country B, Continent B, Network B (AS12349)\033[0m\n"+
"\033[1mMin: \033[0m9.00 ms\n"+
"\033[1mMax: \033[0m22.00 ms\n"+
"\033[1mAvg: \033[0m15.00 ms\n\n", w.String())
}

func Test_Output_Latency_Ping_CI(t *testing.T) {
@@ -119,7 +116,7 @@ func Test_Output_Latency_Ping_CI(t *testing.T) {
err := viewer.Output(measurementID1, &globalping.MeasurementCreate{})
assert.NoError(t, err)

assert.Equal(t, `> Continent, Country, (State), City, ASN:12345, Network
assert.Equal(t, `> City (State), Country, Continent, Network (AS12345)
Min: 8.00 ms
Max: 20.00 ms
Avg: 12.00 ms
@@ -167,10 +164,8 @@ func Test_Output_Latency_DNS_Not_CI(t *testing.T) {
err := viewer.Output(measurementID1, &globalping.MeasurementCreate{})
assert.NoError(t, err)

assert.Equal(t, `> Continent, Country, (State), City, ASN:12345, Network
Total: 44 ms
`, w.String())
assert.Equal(t, "\033[1;38;2;23;212;167m> City (State), Country, Continent, Network (AS12345)\033[0m\n"+
"\033[1mTotal: \033[0m44 ms\n\n", w.String())
}

func Test_Output_Latency_DNS_CI(t *testing.T) {
@@ -214,7 +209,7 @@ func Test_Output_Latency_DNS_CI(t *testing.T) {
err := viewer.Output(measurementID1, &globalping.MeasurementCreate{})
assert.NoError(t, err)

assert.Equal(t, `> Continent, Country, (State), City, ASN:12345, Network
assert.Equal(t, `> City (State), Country, Continent, Network (AS12345)
Total: 44 ms
`, w.String())
@@ -260,15 +255,13 @@ func Test_Output_Latency_Http_Not_CI(t *testing.T) {
err := viewer.Output(measurementID1, &globalping.MeasurementCreate{})
assert.NoError(t, err)

assert.Equal(t, `> Continent, Country, (State), City, ASN:12345, Network
Total: 44 ms
Download: 11 ms
First byte: 20 ms
DNS: 5 ms
TLS: 2 ms
TCP: 4 ms
`, w.String())
assert.Equal(t, "\033[1;38;2;23;212;167m> City (State), Country, Continent, Network (AS12345)\033[0m\n"+
"\033[1mTotal: \033[0m44 ms\n"+
"\033[1mDownload: \033[0m11 ms\n"+
"\033[1mFirst byte: \033[0m20 ms\n"+
"\033[1mDNS: \033[0m5 ms\n"+
"\033[1mTLS: \033[0m2 ms\n"+
"\033[1mTCP: \033[0m4 ms\n\n", w.String())
}

func Test_Output_Latency_Http_CI(t *testing.T) {
@@ -312,7 +305,7 @@ func Test_Output_Latency_Http_CI(t *testing.T) {
err := viewer.Output(measurementID1, &globalping.MeasurementCreate{})
assert.NoError(t, err)

assert.Equal(t, `> Continent, Country, (State), City, ASN:12345, Network
assert.Equal(t, `> City (State), Country, Continent, Network (AS12345)
Total: 44 ms
Download: 11 ms
First byte: 20 ms
97 changes: 25 additions & 72 deletions view/output.go
Original file line number Diff line number Diff line change
@@ -6,20 +6,8 @@ import (
"strings"
"time"

"github.com/charmbracelet/lipgloss"
"github.com/jsdelivr/globalping-cli/globalping"
"github.com/mattn/go-runewidth"
"github.com/pterm/pterm"
)

var (
// UI styles
terminalLayoutHighlight = lipgloss.NewStyle().
Bold(true).Foreground(lipgloss.Color("#17D4A7"))

terminalLayoutArrow = lipgloss.NewStyle().SetString(">").Bold(true).Foreground(lipgloss.Color("#17D4A7")).PaddingRight(1).String()

terminalLayoutBold = lipgloss.NewStyle().Bold(true)
)

func (v *viewer) Output(id string, m *globalping.MeasurementCreate) error {
@@ -67,28 +55,9 @@ func (v *viewer) Output(id string, m *globalping.MeasurementCreate) error {
func (v *viewer) liveView(id string, data *globalping.Measurement, m *globalping.MeasurementCreate) error {
var err error

// Create new writer
areaPrinter, err := pterm.DefaultArea.Start()
if err != nil {
return fmt.Errorf("failed to start writer: %v", err)
}
areaPrinter.RemoveWhenDone = true
w, h := v.printer.GetSize()

defer func() {
// Stop area printer and clear area if not already done
err := areaPrinter.Stop()
if err != nil {
v.printer.Printf("failed to stop writer: %v", err)
}
}()

w, h, err := pterm.GetTerminalSize()
if err != nil {
return fmt.Errorf("failed to get terminal size: %v", err)
}

// String builder for output
var output strings.Builder
output := &strings.Builder{}

// Poll API until the measurement is complete
for data.Status == globalping.StatusInProgress {
@@ -98,14 +67,13 @@ func (v *viewer) liveView(id string, data *globalping.Measurement, m *globalping
return fmt.Errorf("failed to get data: %v", err)
}

// Reset string builder
output.Reset()

// Output every result in case of multiple probes
for i := range data.Results {
result := &data.Results[i]
// Output slightly different format if state is available
output.WriteString(generateProbeInfo(result, !v.ctx.CIMode) + "\n")
output.WriteString(v.getProbeInfo(result) + "\n")

if v.isBodyOnlyHttpGet(m) {
output.WriteString(strings.TrimSpace(result.Result.RawBody) + "\n\n")
@@ -114,31 +82,23 @@ func (v *viewer) liveView(id string, data *globalping.Measurement, m *globalping
}
}

areaPrinter.Update(trimOutput(output.String(), w, h))
}

// Stop area printer and clear area
err = areaPrinter.Stop()
if err != nil {
return fmt.Errorf("failed to stop writer: %v", err)
v.printer.AreaUpdate(trimOutput(output, w, h))
}
v.printer.AreaClear()

v.outputDefault(id, data, m)
return nil
}

// Used to trim the output to fit the terminal in live view
func trimOutput(output string, terminalW, terminalH int) string {
func trimOutput(output *strings.Builder, terminalW, terminalH int) *string {
maxW := terminalW - 4 // 4 extra chars to be safe from overflow
maxH := terminalH - 4 // 4 extra lines to be safe from overflow

if maxW <= 0 || maxH <= 0 {
panic("terminal width / height too limited to display results")
}

text := strings.ReplaceAll(output, "\t", " ")

// Split output into lines
text := strings.ReplaceAll(output.String(), "\t", " ")
lines := strings.Split(text, "\n")

if len(lines) > maxH {
@@ -155,19 +115,14 @@ func trimOutput(output string, terminalW, terminalH int) string {
}
}

// Join lines back into a string
txt := strings.Join(lines, "\n")

return txt
return &txt
}

// Also checks if the probe has a state in it in the form %s, %s, (%s), %s, ASN:%d
func generateProbeInfo(result *globalping.ProbeMeasurement, useStyling bool) string {
func (v *viewer) getProbeInfo(result *globalping.ProbeMeasurement) string {
var output strings.Builder

// Continent + Country + (State) + City + ASN + Network + (Region Tag)
output.WriteString("> ")
output.WriteString(getLocationText(result))

// Check tags to see if there's a region code
if len(result.Probe.Tags) > 0 {
for _, tag := range result.Probe.Tags {
@@ -178,34 +133,32 @@ func generateProbeInfo(result *globalping.ProbeMeasurement, useStyling bool) str
}
}
}

headerWithFormat := formatWithLeadingArrow(output.String(), useStyling)
return headerWithFormat
if v.ctx.CIMode {
return output.String()
}
return v.printer.BoldWithColor(output.String(), ColorHighlight)
}

func formatWithLeadingArrow(text string, useStyling bool) string {
if useStyling {
return terminalLayoutArrow + terminalLayoutHighlight.Render(text)
func (v *viewer) getShareMessage(id string) string {
m := fmt.Sprintf("> View the results online: https://www.jsdelivr.com/globalping?measurement=%s", id)
if v.ctx.CIMode {
return m
}
return "> " + text
return v.printer.BoldWithColor(m, ColorHighlight)
}

func (v *viewer) isBodyOnlyHttpGet(m *globalping.MeasurementCreate) bool {
return v.ctx.Cmd == "http" && m.Options != nil && m.Options.Request != nil && m.Options.Request.Method == "GET" && !v.ctx.Full
}

func shareMessage(id string) string {
return fmt.Sprintf("View the results online: https://www.jsdelivr.com/globalping?measurement=%s", id)
}

func getLocationText(m *globalping.ProbeMeasurement) string {
state := ""
if m.Probe.State != "" {
state = "(" + m.Probe.State + "), "
state = " (" + m.Probe.State + ")"
}
return m.Probe.Continent +
", " + m.Probe.Country +
", " + state + m.Probe.City +
", ASN:" + fmt.Sprint(m.Probe.ASN) +
", " + m.Probe.Network
return m.Probe.City + state + ", " +
m.Probe.Country + ", " +
m.Probe.Continent + ", " +
m.Probe.Network + " " +
"(AS" + fmt.Sprint(m.Probe.ASN) + ")"
}
38 changes: 23 additions & 15 deletions view/output_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package view

import (
"strings"
"testing"

"github.com/jsdelivr/globalping-cli/globalping"
"github.com/stretchr/testify/assert"
)

var (
testContext = Context{
From: "New York",
Target: "1.1.1.1",
CIMode: true,
}
testResult = globalping.ProbeMeasurement{
Probe: globalping.ProbeDetails{
Continent: "Continent",
@@ -27,21 +23,32 @@ var (
)

func Test_HeadersBase(t *testing.T) {
assert.Equal(t, "> Continent, Country, (State), City, ASN:12345, Network", generateProbeInfo(&testResult, !testContext.CIMode))
v := viewer{
ctx: &Context{
CIMode: false,
},
}
assert.Equal(t, "\033[1;38;2;23;212;167m> City (State), Country, Continent, Network (AS12345)\033[0m", v.getProbeInfo(&testResult))
}

func Test_HeadersTags(t *testing.T) {
newResult := testResult
newResult.Probe.Tags = []string{"tag1", "tag2"}
v := viewer{
ctx: &Context{
CIMode: true,
},
}

assert.Equal(t, "> Continent, Country, (State), City, ASN:12345, Network (tag1)", generateProbeInfo(&newResult, !testContext.CIMode))
assert.Equal(t, "> City (State), Country, Continent, Network (AS12345) (tag1)", v.getProbeInfo(&newResult))

newResult.Probe.Tags = []string{"tag", "tag2"}
assert.Equal(t, "> Continent, Country, (State), City, ASN:12345, Network (tag2)", generateProbeInfo(&newResult, !testContext.CIMode))
assert.Equal(t, "> City (State), Country, Continent, Network (AS12345) (tag2)", v.getProbeInfo(&newResult))
}

func Test_TrimOutput(t *testing.T) {
output := `> EU, GB, London, ASN:12345
output := &strings.Builder{}
output.WriteString(`> London, GB, EU, Network (AS12345)
TEST CONTENT
ABCD
EDF
@@ -52,7 +59,7 @@ IOPU
GHJKL
IOPU
GHJKL
LOREM IPSUM LOREM IPSUM LOREM IPSUM`
LOREM IPSUM LOREM IPSUM LOREM IPSUM`)

res := trimOutput(output, 84, 11)

@@ -64,23 +71,24 @@ IOPU
GHJKL
LOREM IPSUM LOREM IPSUM LOREM IPSUM`

assert.Equal(t, expectedRes, res)
assert.Equal(t, expectedRes, *res)
}

func Test_TrimOutput_CN(t *testing.T) {
output := `> EU, GB, London, ASN:12345
output := &strings.Builder{}
output.WriteString(`> London, GB, EU, Network (AS12345)
some text a
中文互联文互联网高质量的问答社区和创 作者聚集的原创内容平台于201 1年1月正式上线让人们更 好的分享 知识经验和见解到自己的解答」中文互联网高质量的问答社区和创作者聚集的原创内容平台中文互联网高质量的问答社区和创作者聚集的原创内容平台于2011年1月正式上线让人们更好的分享知识经验和见解到自己的解答」中文互联网高质量的问答社区和创作者聚集的原创内容平台于
some text e
some text f`
some text f`)

res := trimOutput(output, 84, 10)

expectedRes := `> EU, GB, London, ASN:12345
expectedRes := `> London, GB, EU, Network (AS12345)
some text a
中文互联文互联网高质量的问答社区和创 作者聚集的原创内容平台于201 1年1月正式上线让人们更 好的分享 知识经验和见解到自己的解答」中文互联网高质量的问答社区和创作者聚集的原
some text e
some text f`

assert.Equal(t, expectedRes, res)
assert.Equal(t, expectedRes, *res)
}
96 changes: 91 additions & 5 deletions view/printer.go
Original file line number Diff line number Diff line change
@@ -3,22 +3,33 @@ package view
import (
"fmt"
"io"
"math"
"os"
"strings"

"github.com/pterm/pterm"
"golang.org/x/term"
)

type Color string

const (
ColorNone Color = ""
ColorLightCyan Color = "96"
ColorHighlight Color = "38;2;23;212;167"
)

type Printer struct {
InReader io.Reader
OutWriter io.Writer
ErrWriter io.Writer
InReader io.Reader
OutWriter io.Writer
ErrWriter io.Writer
areaHeight int
}

func NewPrinter(
inReader io.Reader,
outWriter io.Writer,
errWriter io.Writer,
) *Printer {
pterm.SetDefaultOutput(outWriter) // TODO: Set writer for AreaPrinter
return &Printer{
InReader: inReader,
OutWriter: outWriter,
@@ -37,3 +48,78 @@ func (p *Printer) Println(a ...any) {
func (p *Printer) Printf(format string, a ...any) {
fmt.Fprintf(p.OutWriter, format, a...)
}

func (p *Printer) FillLeft(s string, w int) string {
if len(s) >= w {
return s
}
return strings.Repeat(" ", w-len(s)) + s
}

func (p *Printer) FillRight(s string, w int) string {
if len(s) >= w {
return s
}
return s + strings.Repeat(" ", w-len(s))
}

func (p *Printer) FillLeftAndColor(s string, w int, color Color) string {
if len(s) < w {
s = strings.Repeat(" ", w-len(s)) + s
}
if color == ColorNone {
return s
}
return p.Color(s, color)
}

func (p *Printer) FillRightAndColor(s string, w int, color Color) string {
if len(s) < w {
s += strings.Repeat(" ", w-len(s))
}
if color == ColorNone {
return s
}
return p.Color(s, color)
}

func (p *Printer) Color(s string, color Color) string {
return fmt.Sprintf("\033[%sm%s\033[0m", color, s)
}

func (p *Printer) Bold(s string) string {
return fmt.Sprintf("\033[1m%s\033[0m", s)
}

func (p *Printer) BoldWithColor(s string, color Color) string {
return fmt.Sprintf("\033[1;%sm%s\033[0m", color, s)
}

func (p *Printer) GetSize() (width, height int) {
f, ok := p.OutWriter.(*os.File)
if !ok {
return math.MaxInt, math.MaxInt
}
w, h, _ := term.GetSize(int(f.Fd()))
if w <= 0 {
w = math.MaxInt
}
if h <= 0 {
h = math.MaxInt
}
return w, h
}

func (p *Printer) AreaUpdate(content *string) {
p.AreaClear()
p.areaHeight = strings.Count(*content, "\n")
fmt.Fprint(p.OutWriter, *content)
}

func (p *Printer) AreaClear() {
if p.areaHeight == 0 {
return
}
fmt.Fprintf(p.OutWriter, "\033[%dA\033[0J", p.areaHeight)
p.areaHeight = 0
}
2 changes: 1 addition & 1 deletion view/summary.go
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ func (v *viewer) OutputSummary() {
}
ids := v.ctx.History.ToString("+")
if ids != "" {
v.printer.Println(formatWithLeadingArrow(shareMessage(ids), !v.ctx.CIMode))
v.printer.Println(v.getShareMessage(ids))
}
if v.ctx.MeasurementsCreated > v.ctx.History.Capacity() {
v.printer.Printf("For long-running continuous mode measurements, only the last %d packets are shared.\n",
9 changes: 6 additions & 3 deletions view/summary_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package view

import (
"bytes"
"fmt"
"math"
"testing"

@@ -76,7 +77,7 @@ rtt min/avg/max/mdev = 0.770/0.770/0.770/0.000 ms
--- ping statistics ---
1 packets transmitted, 0 received, 100.00% packet loss, time 0ms
rtt min/avg/max/mdev = -/-/-/- ms
` + formatWithLeadingArrow(shareMessage(measurementID1), true) + "\n"
` + fmt.Sprintf("\033[1;38;2;23;212;167m> View the results online: https://www.jsdelivr.com/globalping?measurement=%s\033[0m\n", measurementID1)

assert.Equal(t, expectedOutput, w.String())
})
@@ -89,11 +90,12 @@ rtt min/avg/max/mdev = -/-/-/- ms
}
ctx.History.Push(&HistoryItem{Id: measurementID2})
ctx.Share = true
ctx.CIMode = true
w := new(bytes.Buffer)
viewer := NewViewer(ctx, NewPrinter(nil, w, w), nil, nil)
viewer.OutputSummary()

expectedOutput := "\n" + formatWithLeadingArrow(shareMessage(measurementID1+"+"+measurementID2), true) + "\n"
expectedOutput := fmt.Sprintf("\n> View the results online: https://www.jsdelivr.com/globalping?measurement=%s+%s\n", measurementID1, measurementID2)
assert.Equal(t, expectedOutput, w.String())
})

@@ -107,14 +109,15 @@ rtt min/avg/max/mdev = -/-/-/- ms
},
History: history,
Share: true,
CIMode: true,
MeasurementsCreated: 2,
Packets: 16,
}
w := new(bytes.Buffer)
viewer := NewViewer(ctx, NewPrinter(nil, w, w), nil, nil)
viewer.OutputSummary()

expectedOutput := "\n" + formatWithLeadingArrow(shareMessage(measurementID2), true) +
expectedOutput := fmt.Sprintf("\n> View the results online: https://www.jsdelivr.com/globalping?measurement=%s", measurementID2) +
"\nFor long-running continuous mode measurements, only the last 16 packets are shared.\n"
assert.Equal(t, expectedOutput, w.String())
})

0 comments on commit 2c844ae

Please sign in to comment.