Skip to content

Commit 792b09f

Browse files
committed
ci: add async update notifier
1 parent 2a1f251 commit 792b09f

File tree

4 files changed

+137
-4
lines changed

4 files changed

+137
-4
lines changed

internal/ui/model.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ type Model struct {
3636
width int
3737
height int
3838
sshOnExit string
39+
updateInfo internal.UpdateInfo
3940
}
4041

4142
type TickMsg time.Time
4243

44+
type UpdateCheckMsg internal.UpdateInfo
45+
4346
type SystemInfoMsg struct {
4447
hostName string
4548
info *internal.SystemInfo
@@ -72,6 +75,11 @@ func (h hostItem) Description() string {
7275
return ""
7376
}
7477

78+
func checkForUpdates() tea.Msg {
79+
updateInfo := internal.CheckForUpdates()
80+
return UpdateCheckMsg(updateInfo)
81+
}
82+
7583
func censorHostname(hostname string) string {
7684
if hostname == "" {
7785
return ""
@@ -201,7 +209,7 @@ func (m *Model) updateListSelection() {
201209

202210
func (m Model) Init() tea.Cmd {
203211
if m.screen == ScreenConnecting && len(m.selectedHosts) > 0 {
204-
return tea.Batch(m.spinner.Tick, m.connectToHosts())
212+
return tea.Batch(m.spinner.Tick, m.connectToHosts(), checkForUpdates)
205213
}
206-
return m.spinner.Tick
214+
return tea.Batch(m.spinner.Tick, checkForUpdates)
207215
}

internal/ui/update.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ui
33
import (
44
"time"
55

6+
"github.com/alpindale/ssh-dashboard/internal"
67
tea "github.com/charmbracelet/bubbletea"
78
)
89

@@ -144,6 +145,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
144145
}
145146
}
146147

148+
case UpdateCheckMsg:
149+
m.updateInfo = internal.UpdateInfo(msg)
150+
147151
case TickMsg:
148152
// update every 10 seconds
149153
return m, tea.Batch(m.gatherAllSysInfo(), m.tick())

internal/ui/view.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,24 @@ import (
88
"github.com/charmbracelet/lipgloss"
99
)
1010

11+
func (m Model) renderUpdateNotification() string {
12+
if !m.updateInfo.Available {
13+
return ""
14+
}
15+
16+
updateStyle := lipgloss.NewStyle().
17+
Foreground(lipgloss.Color("226")).
18+
Bold(true)
19+
20+
currentVer := m.updateInfo.CurrentVersion
21+
if !strings.HasPrefix(currentVer, "v") {
22+
currentVer = "v" + currentVer
23+
}
24+
25+
return updateStyle.Render(fmt.Sprintf("\n\n⬆ Update available! %s → %s",
26+
currentVer, m.updateInfo.LatestVersion))
27+
}
28+
1129
func (m Model) View() string {
1230
switch m.screen {
1331
case ScreenHostList:
@@ -30,6 +48,7 @@ func (m Model) View() string {
3048
}
3149
versionFooter := fmt.Sprintf("\nv%s", internal.ShortVersion())
3250
listView += lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(versionFooter)
51+
listView += m.renderUpdateNotification()
3352
return listView
3453

3554
case ScreenConnecting:
@@ -50,12 +69,14 @@ func (m Model) View() string {
5069
if len(m.selectedHosts) > 1 {
5170
hostIndicator = fmt.Sprintf(" [%d/%d]", m.currentHostIdx+1, len(m.selectedHosts))
5271
}
53-
return renderDashboard(currentHost.Name+hostIndicator, sysInfo, m.updateInterval, lastUpdate, m.width, m.height, len(m.selectedHosts) > 1)
72+
dashboardView := renderDashboard(currentHost.Name+hostIndicator, sysInfo, m.updateInterval, lastUpdate, m.width, m.height, len(m.selectedHosts) > 1)
73+
return dashboardView + m.renderUpdateNotification()
5474
}
5575
return m.renderConnectingProgress()
5676

5777
case ScreenOverview:
58-
return m.renderOverview()
78+
overviewView := m.renderOverview()
79+
return overviewView + m.renderUpdateNotification()
5980
}
6081

6182
return ""

internal/updater.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package internal
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"net/http"
7+
"strings"
8+
"time"
9+
)
10+
11+
const (
12+
githubAPIURL = "https://api.github.com/repos/alpindale/ssh-dashboard/releases/latest"
13+
updateCheckTimeout = 3 * time.Second
14+
)
15+
16+
type UpdateInfo struct {
17+
Available bool
18+
LatestVersion string
19+
CurrentVersion string
20+
}
21+
22+
type githubRelease struct {
23+
TagName string `json:"tag_name"`
24+
HTMLURL string `json:"html_url"`
25+
}
26+
27+
func CheckForUpdates() UpdateInfo {
28+
currentVersion := Version
29+
if currentVersion == "dev" {
30+
return UpdateInfo{Available: false, CurrentVersion: currentVersion}
31+
}
32+
33+
latestVersion, err := fetchLatestVersion()
34+
if err != nil {
35+
return UpdateInfo{Available: false, CurrentVersion: currentVersion}
36+
}
37+
38+
needsUpdate := compareVersions(currentVersion, latestVersion)
39+
40+
return UpdateInfo{
41+
Available: needsUpdate,
42+
LatestVersion: latestVersion,
43+
CurrentVersion: currentVersion,
44+
}
45+
}
46+
47+
func fetchLatestVersion() (string, error) {
48+
client := &http.Client{
49+
Timeout: updateCheckTimeout,
50+
}
51+
52+
resp, err := client.Get(githubAPIURL)
53+
if err != nil {
54+
return "", err
55+
}
56+
defer resp.Body.Close()
57+
58+
if resp.StatusCode != http.StatusOK {
59+
return "", fmt.Errorf("github api returned status %d", resp.StatusCode)
60+
}
61+
62+
var release githubRelease
63+
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
64+
return "", err
65+
}
66+
67+
return release.TagName, nil
68+
}
69+
70+
func compareVersions(current, latest string) bool {
71+
current = strings.TrimPrefix(current, "v")
72+
latest = strings.TrimPrefix(latest, "v")
73+
74+
currentBase := strings.Split(strings.Split(current, "-")[0], "+")[0]
75+
latestBase := strings.Split(strings.Split(latest, "-")[0], "+")[0]
76+
77+
currentParts := strings.Split(currentBase, ".")
78+
latestParts := strings.Split(latestBase, ".")
79+
80+
for len(currentParts) < 3 {
81+
currentParts = append(currentParts, "0")
82+
}
83+
for len(latestParts) < 3 {
84+
latestParts = append(latestParts, "0")
85+
}
86+
87+
for i := 0; i < 3; i++ {
88+
var currentNum, latestNum int
89+
fmt.Sscanf(currentParts[i], "%d", &currentNum)
90+
fmt.Sscanf(latestParts[i], "%d", &latestNum)
91+
92+
if latestNum > currentNum {
93+
return true
94+
} else if latestNum < currentNum {
95+
return false
96+
}
97+
}
98+
99+
return false
100+
}

0 commit comments

Comments
 (0)