Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ A polished Go TUI with a startup menu, one-shot speed tests, a live bandwidth mo
[![terminal](https://img.shields.io/badge/terminal-TUI-39d0d8?style=flat-square)](https://github.com/Foxemsx/riptide)
[![Go](https://img.shields.io/badge/Go-1.23+-00ADD8?style=flat-square&logo=go&logoColor=white)](https://go.dev)
[![Linux](https://img.shields.io/badge/Linux-supported-2ea44f?style=flat-square&logo=linux&logoColor=white)](https://github.com/Foxemsx/riptide)
[![macOS](https://img.shields.io/badge/macOS-supported-A2AAAD?style=flat-square&logo=apple&logoColor=white)](https://github.com/Foxemsx/riptide)
[![Windows](https://img.shields.io/badge/Windows-supported-0078D6?style=flat-square&logo=windows&logoColor=white)](https://github.com/Foxemsx/riptide)
[![License](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)

Expand All @@ -22,7 +23,7 @@ A polished Go TUI with a startup menu, one-shot speed tests, a live bandwidth mo
|:---|:---|
| **Speed Test** | One-shot download, upload, and ping. Parallel connections, peak rates, timed phases. Auto-saves runs; compare the latest 10. |
| **Bandwidth Monitor** | Live view of *real* PC traffic (OS counters only — no test load). Peaks, uptime, pause. |
| **Settings** | Searchable settings: 11 color themes, database reset, uninstall instructions. |
| **Settings** | Searchable settings: 14 color themes, database reset, uninstall instructions. |

---

Expand Down Expand Up @@ -101,6 +102,24 @@ curl -fsSL https://raw.githubusercontent.com/Foxemsx/riptide/main/uninstall.sh |

You can also open **Settings → Uninstall** inside the app for the same instructions.

### macOS

With Go 1.23+ installed:

```sh
go build -o /usr/local/bin/riptide github.com/Foxemsx/riptide/cmd/riptide@main
riptide
```

Or from source (same as the [From source](#from-source-any-os-with-go-123) section below).

> **Known issue — transparent terminals (Ghostty, iTerm2)**
> On macOS, some terminal emulators render ~30% of the background as
> transparent due to a Go/Lipgloss interaction with ANSI reset codes.
> The core features (speed test, bandwidth monitor, themes, history)
> all work normally. Use **Settings → Terminal BG** (key `4`) to
> toggle full transparency mode as a workaround.

### Windows

The bash installer does **not** run on Windows. Pick one of:
Expand Down Expand Up @@ -184,6 +203,9 @@ riptide --theme ocean # start with a palette (also saved as preference)
| `cyber` | Neon green · hot magenta |
| `ember` | Charcoal fire · molten gold |
| `arctic` | Ice blue · clean slate |
| `mono` | True black · arctic white |
| `signal` | True black · rose red |
| `ink` | True black · cold blue |

Theme preference is stored in the local database (overridden by `--theme` for that launch).

Expand Down
105 changes: 105 additions & 0 deletions internal/engine/monitor_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//go:build darwin

package engine

import (
"bytes"
"context"
"os/exec"
"strconv"
"strings"
"time"
)

func RunMonitor(ctx context.Context, p *Progress, sampleInterval time.Duration) {
p.ServerName = discoverAdaptersDarwin()
sendPhase(p, PhaseConnected)

prev := map[string]counters{}

ticker := time.NewTicker(sampleInterval)
defer ticker.Stop()

var last time.Time
for {
select {
case <-ctx.Done():
return
case t := <-ticker.C:
if last.IsZero() {
_, _ = readCountersDarwin(prev)
last = t
continue
}
elapsed := t.Sub(last).Seconds()
last = t
if elapsed <= 0 {
continue
}

total, names := readCountersDarwin(nil)
if len(names) > 0 {
p.ServerName = strings.Join(names, ", ")
}

var rx, tx uint64
for iface, c := range total {
base, ok := prev[iface]
if !ok {
prev[iface] = c
continue
}
rx += safeDelta(base.rx, c.rx)
tx += safeDelta(base.tx, c.tx)
prev[iface] = c
}

_ = sendSample(p, Sample{Phase: PhaseDownload, Rate: float64(rx) / elapsed, At: t})
_ = sendSample(p, Sample{Phase: PhaseUpload, Rate: float64(tx) / elapsed, At: t})
}
}
}

func readCountersDarwin(seen map[string]counters) (map[string]counters, []string) {
total := map[string]counters{}
var names []string

out, err := exec.Command("netstat", "-ib", "-n").Output()
if err != nil {
return total, names
}

for _, line := range bytes.Split(out, []byte("\n")) {
s := string(bytes.TrimSpace(line))
if s == "" || !strings.Contains(s, "<Link#") {
continue
}
fields := strings.Fields(s)
if len(fields) < 10 {
continue
}
iface := fields[0]
if iface == "lo0" || strings.HasSuffix(iface, "*") {
continue
}

ibytes, _ := strconv.ParseUint(fields[5], 10, 64)
obytes, _ := strconv.ParseUint(fields[8], 10, 64)

c := counters{rx: ibytes, tx: obytes}
total[iface] = c
if seen != nil {
seen[iface] = c
}
names = append(names, iface)
}
return total, names
}

func discoverAdaptersDarwin() string {
_, names := readCountersDarwin(nil)
if len(names) == 0 {
return "no active interfaces"
}
return strings.Join(names, ", ")
}
Loading