package main import ( "fmt" "os/exec" "sort" "github.com/atotto/clipboard" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) // handleWindowSize updates the model dimensions and adjusts the list size accordingly. func (m model) handleWindowSize(msg tea.WindowSizeMsg) (model, tea.Cmd) { m.width = msg.Width m.height = msg.Height if !m.loading && !m.promptingEmail { m.list.SetWidth(msg.Width) m.list.SetHeight(msg.Height - listHeightOffset) } return m, nil } // handleAuthRequired switches the model to email prompt mode. func (m model) handleAuthRequired() (model, tea.Cmd) { m.promptingEmail = true m.loading = false m.emailInput, m.rememberEmail = loadEmailIntoInput(m.emailInput) return m, textinput.Blink } // handleLogin processes the login result and saves the email if requested. func (m model) handleLogin(msg loginMsg) (model, tea.Cmd) { m.loggingIn = false if msg.err != nil { m.err = fmt.Sprintf("Login failed: %v", msg.err) return m, nil } if m.rememberEmail { email := m.emailInput.Value() if email != "" { saveConfig(email) } } m.promptingEmail = false m.loading = true return m, fetchStatus } // handleStatus processes the status message and updates the list with connections. // Connections are sorted with connected items first, then by name. func (m model) handleStatus(msg statusMsg) (model, tea.Cmd) { if msg.err != nil { m.loading = false m.err = fmt.Sprintf("Failed to fetch status: %v", msg.err) return m, nil } // Sort connections: connected items first, then by name connections := make([]connection, len(msg.connections)) copy(connections, msg.connections) sort.Slice(connections, func(i, j int) bool { if connections[i].Connected != connections[j].Connected { // Connected items come first return connections[i].Connected } // If both have same connection status, sort by name return connections[i].Name < connections[j].Name }) items := make([]list.Item, len(connections)) for i, conn := range connections { items[i] = item{ address: getAddress(conn), name: conn.Name, connected: conn.Connected, connType: conn.Type, } } m.list = newList(items, m.width, m.height-listHeightOffset) m.loading = false return m, nil } // handleConnection initiates a connection to the selected item. func (m model) handleConnection() (model, tea.Cmd) { i, ok := m.list.SelectedItem().(item) if !ok { return m, nil } m.loading = true name := i.name address := i.address return m, tea.Batch( func() tea.Msg { return connectSDM(name, address) }, m.spinner.Tick, ) } // handleConnectResult processes the result of a connect operation. func (m model) handleConnectResult(msg connectMsg) (model, tea.Cmd) { m.loading = false if msg.err != nil { m.err = fmt.Sprintf("Error connecting to %s: %v", msg.name, msg.err) return m, nil } clipboard.WriteAll(msg.address) sendNotification("SDM Connected", fmt.Sprintf("Connected to %s", msg.name)) return m, tea.Quit } // handleKillConnections disconnects all connections. func (m model) handleKillConnections() (model, tea.Cmd) { m.loading = true return m, tea.Batch( func() tea.Msg { cmd := exec.Command("sdm", "disconnect", "--all") output, err := cmd.CombinedOutput() if err != nil { return disconnectMsg{err: fmt.Errorf("disconnecting: %v: %s", err, string(output))} } return disconnectMsg{} }, m.spinner.Tick, ) } // handleDisconnectSelected disconnects the currently selected connection. func (m model) handleDisconnectSelected() (model, tea.Cmd) { i, ok := m.list.SelectedItem().(item) if !ok || !i.connected { return m, nil } m.loading = true name := i.name return m, tea.Batch( func() tea.Msg { return disconnectSDM(name) }, m.spinner.Tick, ) } // handleDisconnect processes the result of a disconnect operation. func (m model) handleDisconnect(msg disconnectMsg) (model, tea.Cmd) { m.loading = false if msg.err != nil { m.err = fmt.Sprintf("Error disconnecting: %v", msg.err) return m, nil } name := "all connections" if msg.name != "" { name = msg.name } sendNotification("SDM Disconnected", fmt.Sprintf("Disconnected from %s", name)) m.loading = true return m, tea.Batch(fetchStatus, m.spinner.Tick) } // handleKeyInput processes keyboard input based on the current application state. func (m model) handleKeyInput(msg tea.KeyMsg) (model, tea.Cmd) { if m.promptingEmail { switch msg.String() { case "enter": email := m.emailInput.Value() if email != "" && !m.loggingIn { m.loggingIn = true return m, tea.Batch( func() tea.Msg { return loginSDM(email) }, m.spinner.Tick, ) } case " ": if !m.loggingIn { m.rememberEmail = !m.rememberEmail } return m, nil } if isQuitKey(msg.String()) { return m, tea.Quit } if !m.loggingIn { var cmd tea.Cmd m.emailInput, cmd = m.emailInput.Update(msg) return m, cmd } return m, nil } if m.err != "" { switch msg.String() { case "r": m.err = "" m.loading = true m.promptingEmail = false m.loggingIn = false return m, tea.Batch( func() tea.Msg { if err := checkSDMBinary(); err != nil { return statusMsg{err: err} } return fetchStatus() }, m.spinner.Tick, ) } if isQuitKey(msg.String()) { return m, tea.Quit } return m, nil } if m.loading { return m, nil } if m.list.FilterState() == list.Filtering { var cmd tea.Cmd m.list, cmd = m.list.Update(msg) return m, cmd } switch msg.String() { case tea.KeyEnter.String(): return m.handleConnection() case tea.KeyCtrlX.String(): return m.handleKillConnections() case "d": return m.handleDisconnectSelected() case "r": m.loading = true return m, tea.Batch(fetchStatus, m.spinner.Tick) } if isQuitKey(msg.String()) { return m, tea.Quit } var cmd tea.Cmd m.list, cmd = m.list.Update(msg) return m, cmd }