mirror of
https://github.com/Gu1llaum-3/sshm.git
synced 2026-03-14 03:41:27 +01:00
feat: add automatic version update checking and notifications
- Add internal/version module for GitHub release checking - Integrate async version check in Bubble Tea UI - Display update notification in main interface - Add version check to --version/-v command output - Include comprehensive version comparison and error handling - Add unit tests for version parsing and comparison logic
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
||||
"github.com/Gu1llaum-3/sshm/internal/history"
|
||||
"github.com/Gu1llaum-3/sshm/internal/version"
|
||||
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
@@ -78,6 +79,10 @@ type Model struct {
|
||||
sortMode SortMode
|
||||
configFile string // Path to the SSH config file
|
||||
|
||||
// Version update information
|
||||
updateInfo *version.UpdateInfo
|
||||
currentVersion string
|
||||
|
||||
// View management
|
||||
viewMode ViewMode
|
||||
addForm *addFormModel
|
||||
|
||||
@@ -178,12 +178,13 @@ func (m *Model) updateTableHeight() {
|
||||
// Calculate dynamic table height based on terminal size
|
||||
// Layout breakdown:
|
||||
// - ASCII title: 5 lines (1 empty + 4 text lines)
|
||||
// - Update banner : 1 line (if present)
|
||||
// - Search bar: 1 line
|
||||
// - Help text: 1 line
|
||||
// - App margins/spacing: 3 lines
|
||||
// - Safety margin: 3 lines (to ensure UI elements are always visible)
|
||||
// Total reserved: 13 lines minimum to preserve essential UI elements
|
||||
reservedHeight := 13
|
||||
// Total reserved: 14 lines minimum to preserve essential UI elements
|
||||
reservedHeight := 14
|
||||
availableHeight := m.height - reservedHeight
|
||||
hostCount := len(m.table.Rows())
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// NewModel creates a new TUI model with the given SSH hosts
|
||||
func NewModel(hosts []config.SSHHost, configFile string) Model {
|
||||
func NewModel(hosts []config.SSHHost, configFile, currentVersion string) Model {
|
||||
// Initialize the history manager
|
||||
historyManager, err := history.NewHistoryManager()
|
||||
if err != nil {
|
||||
@@ -38,6 +38,7 @@ func NewModel(hosts []config.SSHHost, configFile string) Model {
|
||||
pingManager: pingManager,
|
||||
sortMode: SortByName,
|
||||
configFile: configFile,
|
||||
currentVersion: currentVersion,
|
||||
styles: styles,
|
||||
width: 80,
|
||||
height: 24,
|
||||
@@ -136,8 +137,8 @@ func NewModel(hosts []config.SSHHost, configFile string) Model {
|
||||
}
|
||||
|
||||
// RunInteractiveMode starts the interactive TUI interface
|
||||
func RunInteractiveMode(hosts []config.SSHHost, configFile string) error {
|
||||
m := NewModel(hosts, configFile)
|
||||
func RunInteractiveMode(hosts []config.SSHHost, configFile, currentVersion string) error {
|
||||
m := NewModel(hosts, configFile, currentVersion)
|
||||
|
||||
// Start the application in alt screen mode for clean output
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
|
||||
@@ -8,14 +8,17 @@ import (
|
||||
|
||||
"github.com/Gu1llaum-3/sshm/internal/config"
|
||||
"github.com/Gu1llaum-3/sshm/internal/connectivity"
|
||||
"github.com/Gu1llaum-3/sshm/internal/version"
|
||||
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// Messages for SSH ping functionality
|
||||
// Messages for SSH ping functionality and version checking
|
||||
type (
|
||||
pingResultMsg *connectivity.HostPingResult
|
||||
pingResultMsg *connectivity.HostPingResult
|
||||
versionCheckMsg *version.UpdateInfo
|
||||
versionErrorMsg error
|
||||
)
|
||||
|
||||
// startPingAllCmd creates a command to ping all hosts concurrently
|
||||
@@ -49,12 +52,33 @@ func pingSingleHostCmd(pingManager *connectivity.PingManager, host config.SSHHos
|
||||
}
|
||||
}
|
||||
|
||||
// checkVersionCmd creates a command to check for version updates
|
||||
func checkVersionCmd(currentVersion string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
updateInfo, err := version.CheckForUpdates(ctx, currentVersion)
|
||||
if err != nil {
|
||||
return versionErrorMsg(err)
|
||||
}
|
||||
return versionCheckMsg(updateInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the model
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
textinput.Blink,
|
||||
// Ping is now optional - use 'p' key to start ping
|
||||
)
|
||||
var cmds []tea.Cmd
|
||||
|
||||
// Basic initialization commands
|
||||
cmds = append(cmds, textinput.Blink)
|
||||
|
||||
// Check for version updates if we have a current version
|
||||
if m.currentVersion != "" {
|
||||
cmds = append(cmds, checkVersionCmd(m.currentVersion))
|
||||
}
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// Update handles model updates
|
||||
@@ -115,6 +139,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case versionCheckMsg:
|
||||
// Handle version check result
|
||||
if msg != nil {
|
||||
m.updateInfo = msg
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case versionErrorMsg:
|
||||
// Handle version check error (silently - not critical)
|
||||
// We don't want to show error messages for version checks
|
||||
// as it might disrupt the user experience
|
||||
return m, nil
|
||||
|
||||
case addFormSubmitMsg:
|
||||
if msg.err != nil {
|
||||
// Show error in form
|
||||
|
||||
@@ -54,6 +54,20 @@ func (m Model) renderListView() string {
|
||||
// Add the ASCII title
|
||||
components = append(components, m.styles.Header.Render(asciiTitle))
|
||||
|
||||
// Add update notification if available (between title and search)
|
||||
if m.updateInfo != nil && m.updateInfo.Available {
|
||||
updateText := fmt.Sprintf("🚀 Update available: %s → %s",
|
||||
m.updateInfo.CurrentVer,
|
||||
m.updateInfo.LatestVer)
|
||||
|
||||
updateStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("10")). // Green color
|
||||
Bold(true).
|
||||
Align(lipgloss.Center) // Center the notification
|
||||
|
||||
components = append(components, updateStyle.Render(updateText))
|
||||
}
|
||||
|
||||
// Add the search bar with the appropriate style based on focus
|
||||
searchPrompt := "Search (/ to focus): "
|
||||
if m.searchMode {
|
||||
@@ -157,3 +171,30 @@ func (m Model) renderDeleteConfirmation() string {
|
||||
|
||||
return box.Render(raw)
|
||||
}
|
||||
|
||||
// renderUpdateNotification renders the update notification banner
|
||||
func (m Model) renderUpdateNotification() string {
|
||||
if m.updateInfo == nil || !m.updateInfo.Available {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Create the notification message
|
||||
message := fmt.Sprintf("🚀 Update available: %s → %s",
|
||||
m.updateInfo.CurrentVer,
|
||||
m.updateInfo.LatestVer)
|
||||
|
||||
// Add release URL if available
|
||||
if m.updateInfo.ReleaseURL != "" {
|
||||
message += fmt.Sprintf(" • View release: %s", m.updateInfo.ReleaseURL)
|
||||
}
|
||||
|
||||
// Style the notification with a bright color to make it stand out
|
||||
notificationStyle := lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00FF00")). // Bright green
|
||||
Bold(true).
|
||||
Padding(0, 1).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("#00AA00")) // Darker green border
|
||||
|
||||
return notificationStyle.Render(message)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user