Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Binaries
/ftm
/ftm-*
*.old
*.exe
bin/

Expand Down
49 changes: 26 additions & 23 deletions cmd/ftm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,28 @@ import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"

"github.com/sthbryan/ftm/internal/app"
"github.com/sthbryan/ftm/internal/cli"
"github.com/sthbryan/ftm/internal/i18n"
"github.com/sthbryan/ftm/internal/version"
)

var BuildVersion string

func doUninstall() {
binaryPath, err := exec.LookPath("ftm")
if err != nil {
fmt.Println(i18n.T("uninstall_not_found"))
os.Exit(1)
}

absPath, err := filepath.EvalSymlinks(binaryPath)
if err != nil {
absPath = binaryPath
}

fmt.Println(i18n.TF("uninstall_removing", absPath))
if err := os.Remove(absPath); err != nil {
fmt.Fprintf(os.Stderr, i18n.TF("uninstall_error", err.Error())+"\n")
func main() {
if err := cli.Init(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

fmt.Println(i18n.T("uninstall_success"))
}

func main() {
var (
Comment thread
sthbryan marked this conversation as resolved.
webOnly = flag.Bool("web", false, "Start web dashboard and open browser")
server = flag.Bool("server", false, "Start web dashboard only (no browser)")
port = flag.Int("port", 0, "Web server port (auto-detect if not specified)")
showVersion = flag.Bool("version", false, "Show version")
uninstall = flag.Bool("uninstall", false, "Uninstall ftm")
update = flag.Bool("update", false, "Update ftm to the latest release")
checkOnly = flag.Bool("check", false, "Check for updates without installing")
)
flag.Parse()

Expand All @@ -51,7 +35,26 @@ func main() {
}

if *uninstall {
doUninstall()
if err := cli.Uninstall(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
os.Exit(0)
}

if *checkOnly {
if err := cli.Update(true); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
os.Exit(0)
}

if *update {
if err := cli.Update(false); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
os.Exit(0)
}

Expand Down
11 changes: 11 additions & 0 deletions internal/app/keyhandlers_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ func (m *Model) handleListKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {

case key.Matches(msg, m.Keys.Delete):
return m.handleListDelete()

case key.Matches(msg, m.Keys.Update):
return m.handleListUpdate()
}

return m, nil
Expand Down Expand Up @@ -158,6 +161,14 @@ func (m *Model) openConfigDir() {
m.showMessage(i18n.T("config_opened"))
}

func (m *Model) handleListUpdate() (tea.Model, tea.Cmd) {
if m.UpdateAvailable == nil {
return m, nil
}
m.showMessage(i18n.T("update_applying"))
return m, applyUpdateCmd(m.UpdateAvailable)
}

func (m *Model) playBeep() {
if m.App.Config.NotificationSound {
fmt.Fprint(os.Stdout, Bell)
Expand Down
1 change: 1 addition & 0 deletions internal/app/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func (m *Model) Init() tea.Cmd {
return tea.Batch(
tickCmd(),
m.checkDownloadProgress(),
checkUpdateCmd(),
)
}

Expand Down
7 changes: 7 additions & 0 deletions internal/app/model_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/sthbryan/ftm/internal/config"
"github.com/sthbryan/ftm/internal/i18n"
"github.com/sthbryan/ftm/internal/providers"
"github.com/sthbryan/ftm/internal/updater"
)

type viewState int
Expand Down Expand Up @@ -53,6 +54,7 @@ type KeyMap struct {
Back key.Binding
Quit key.Binding
Help key.Binding
Update key.Binding
}

var DefaultKeys = KeyMap{
Expand Down Expand Up @@ -125,6 +127,10 @@ var DefaultKeys = KeyMap{
key.WithKeys("?"),
key.WithHelp("?", "help"),
),
Update: key.NewBinding(
key.WithKeys("u"),
key.WithHelp("u", "update"),
),
}

type Model struct {
Expand All @@ -148,6 +154,7 @@ type Model struct {
PendingTunnel *config.TunnelConfig
ProgressBar progress.Model
SettingsView *views.SettingsView
UpdateAvailable *updater.Info
}

type FormData struct {
Expand Down
19 changes: 17 additions & 2 deletions internal/app/ui/views/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type ListView struct {
Dashboard string
Sessions int
TwoColumnLimit int
UpdateBadge string
}

func NewListView() *ListView {
Expand Down Expand Up @@ -69,7 +70,14 @@ func (l *ListView) twoColumn() string {
b.WriteString(title)
b.WriteString(strings.Repeat(" ", l.Width-lipgloss.Width(title)-lipgloss.Width(versionStr)-ui.HeaderMargin))
b.WriteString(versionStr)
b.WriteString("\n\n")
b.WriteString("\n")

if l.UpdateBadge != "" {
badgeStyle := lipgloss.NewStyle().Foreground(ui.ThemeDefault.Gold).Bold(true)
b.WriteString(badgeStyle.Render(l.UpdateBadge))
b.WriteString("\n")
}
b.WriteString("\n")

leftWidth := int(float64(l.Width) * 0.4)
rightWidth := l.Width - leftWidth - 3
Expand Down Expand Up @@ -152,7 +160,14 @@ func (l *ListView) singleColumn() string {
b.WriteString(title)
b.WriteString(strings.Repeat(" ", l.Width-lipgloss.Width(title)-lipgloss.Width(versionStr)-ui.HeaderMargin))
b.WriteString(versionStr)
b.WriteString("\n\n")
b.WriteString("\n")

if l.UpdateBadge != "" {
badgeStyle := lipgloss.NewStyle().Foreground(ui.ThemeDefault.Gold).Bold(true)
b.WriteString(badgeStyle.Render(l.UpdateBadge))
b.WriteString("\n")
}
b.WriteString("\n")

if l.Dashboard != "" {
urlStyle := lipgloss.NewStyle().Foreground(gold)
Expand Down
31 changes: 31 additions & 0 deletions internal/app/update.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package app

import (
"os"
"time"

tea "github.com/charmbracelet/bubbletea"

"github.com/sthbryan/ftm/internal/i18n"
Expand Down Expand Up @@ -30,6 +33,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

case statusUpdateMsg:
return m.handleStatusUpdate(msg)

case updateCheckMsg:
return m.handleUpdateCheck(msg)

case updateApplyMsg:
return m.handleUpdateApply(msg)
}

return m, nil
Expand Down Expand Up @@ -74,3 +83,25 @@ func (m *Model) handleStatusUpdate(msg statusUpdateMsg) (tea.Model, tea.Cmd) {
}
return m, nil
}

func (m *Model) handleUpdateCheck(msg updateCheckMsg) (tea.Model, tea.Cmd) {
if msg.err == nil && msg.info != nil && msg.info.HasUpdate {
m.UpdateAvailable = msg.info
}
return m, scheduleUpdateRecheck()
}

func (m *Model) handleUpdateApply(msg updateApplyMsg) (tea.Model, tea.Cmd) {
if msg.err != nil {
m.showMessage(i18n.TF("update_apply_failed", msg.err.Error()))
return m, nil
}
os.Exit(0)
return m, nil
}

func scheduleUpdateRecheck() tea.Cmd {
return tea.Tick(6*time.Hour, func(time.Time) tea.Msg {
return checkUpdateCmd()()
})
}
33 changes: 33 additions & 0 deletions internal/app/update_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package app

import (
tea "github.com/charmbracelet/bubbletea"

"github.com/sthbryan/ftm/internal/updater"
"github.com/sthbryan/ftm/internal/version"
)

const updateRepo = "sthbryan/ftm"

type updateCheckMsg struct {
info *updater.Info
err error
}

type updateApplyMsg struct {
err error
}

func checkUpdateCmd() tea.Cmd {
return func() tea.Msg {
info, err := updater.New(updateRepo).Check(version.Version)
return updateCheckMsg{info: info, err: err}
}
}

func applyUpdateCmd(info *updater.Info) tea.Cmd {
return func() tea.Msg {
err := updater.New(updateRepo).Apply(info)
return updateApplyMsg{err: err}
}
}
3 changes: 3 additions & 0 deletions internal/app/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ func (m *Model) viewList() string {
view.Dashboard = m.App.WebServer.URL()
view.Sessions = m.App.WebServer.ClientCount()
view.TwoColumnLimit = TwoColumnThreshold
if m.UpdateAvailable != nil {
view.UpdateBadge = i18n.TF("update_tui_badge", m.UpdateAvailable.LatestVersion)
}

return view.Render()
}
Expand Down
7 changes: 7 additions & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package cli

import "github.com/sthbryan/ftm/internal/i18n"

func Init() error {
return i18n.Load()
}
30 changes: 30 additions & 0 deletions internal/cli/uninstall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cli

import (
"fmt"
"os"
"os/exec"
"path/filepath"

"github.com/sthbryan/ftm/internal/i18n"
)

func Uninstall() error {
binaryPath, err := exec.LookPath("ftm")
if err != nil {
return fmt.Errorf("%s", i18n.T("uninstall_not_found"))
}

absPath, err := filepath.EvalSymlinks(binaryPath)
if err != nil {
absPath = binaryPath
}

fmt.Println(i18n.TF("uninstall_removing", absPath))
if err := os.Remove(absPath); err != nil {
return fmt.Errorf("%s", i18n.TF("uninstall_error", err.Error()))
}

fmt.Println(i18n.T("uninstall_success"))
return nil
}
40 changes: 40 additions & 0 deletions internal/cli/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cli

import (
"fmt"

"github.com/sthbryan/ftm/internal/i18n"
"github.com/sthbryan/ftm/internal/updater"
"github.com/sthbryan/ftm/internal/version"
)

const defaultRepo = "sthbryan/ftm"

func Update(checkOnly bool) error {
u := updater.New(defaultRepo)
info, err := u.Check(version.Version)
if err != nil {
return fmt.Errorf("%s", i18n.TF("update_check_failed", err.Error()))
}

fmt.Println(i18n.TF("update_current_version", info.CurrentVersion))
fmt.Println(i18n.TF("update_latest_version", info.LatestVersion))

if !info.HasUpdate {
fmt.Println(i18n.T("update_up_to_date"))
return nil
}

fmt.Println(i18n.TF("update_available", info.Tag))
fmt.Println(i18n.TF("update_release_url", info.ReleaseURL))
if checkOnly {
return nil
}

fmt.Println(i18n.TF("update_downloading", info.AssetName))
if err := u.Apply(info); err != nil {
return fmt.Errorf("%s", i18n.TF("update_apply_failed", err.Error()))
}
fmt.Println(i18n.TF("update_success", info.LatestVersion))
return nil
}
18 changes: 17 additions & 1 deletion internal/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,20 @@ uninstall_success: "✓ ftm has been uninstalled"
version_output: "Foundry Tunnel Manager v{0}"
dashboard_url: "🌐 Dashboard running at: {0}"
press_ctrl_c: "\nPress Ctrl+C to stop"
tui_hint: "Press 'w' in the TUI to open the dashboard"
tui_hint: "Press 'w' in the TUI to open the dashboard"

# Update
update_check_failed: "Failed to check for updates: {0}"
update_current_version: "Current version: {0}"
update_latest_version: "Latest version: {0}"
update_up_to_date: "✓ You are already on the latest version"
update_available: "Update available: {0}"
update_release_url: "Release notes: {0}"
update_downloading: "Downloading {0}..."
update_apply_failed: "Update failed: {0}"
update_success: "✓ Updated to v{0}. Restart ftm to use the new version."
update_tui_badge: "↑ Update available: v{0} — press 'u' to install"
update_applying: "Downloading and installing update... ftm will restart."
update_web_banner: "Update available: v{latest}"
update_web_button: "Update now"
update_web_notes: "Release notes"
Loading
Loading