handlers.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package main
  2. import (
  3. "fmt"
  4. "os/exec"
  5. "sort"
  6. "github.com/atotto/clipboard"
  7. "github.com/charmbracelet/bubbles/list"
  8. "github.com/charmbracelet/bubbles/textinput"
  9. tea "github.com/charmbracelet/bubbletea"
  10. )
  11. // handleWindowSize updates the model dimensions and adjusts the list size accordingly.
  12. func (m model) handleWindowSize(msg tea.WindowSizeMsg) (model, tea.Cmd) {
  13. m.width = msg.Width
  14. m.height = msg.Height
  15. if !m.loading && !m.promptingEmail {
  16. m.list.SetWidth(msg.Width)
  17. m.list.SetHeight(msg.Height - listHeightOffset)
  18. }
  19. return m, nil
  20. }
  21. // handleAuthRequired switches the model to email prompt mode.
  22. func (m model) handleAuthRequired() (model, tea.Cmd) {
  23. m.promptingEmail = true
  24. m.loading = false
  25. m.emailInput, m.rememberEmail = loadEmailIntoInput(m.emailInput)
  26. return m, textinput.Blink
  27. }
  28. // handleLogin processes the login result and saves the email if requested.
  29. func (m model) handleLogin(msg loginMsg) (model, tea.Cmd) {
  30. m.loggingIn = false
  31. if msg.err != nil {
  32. m.err = fmt.Sprintf("Login failed: %v", msg.err)
  33. return m, nil
  34. }
  35. if m.rememberEmail {
  36. email := m.emailInput.Value()
  37. if email != "" {
  38. saveConfig(email)
  39. }
  40. }
  41. m.promptingEmail = false
  42. m.loading = true
  43. return m, fetchStatus
  44. }
  45. // handleStatus processes the status message and updates the list with connections.
  46. // Connections are sorted with connected items first, then by name.
  47. func (m model) handleStatus(msg statusMsg) (model, tea.Cmd) {
  48. if msg.err != nil {
  49. m.loading = false
  50. m.err = fmt.Sprintf("Failed to fetch status: %v", msg.err)
  51. return m, nil
  52. }
  53. // Sort connections: connected items first, then by name
  54. connections := make([]connection, len(msg.connections))
  55. copy(connections, msg.connections)
  56. sort.Slice(connections, func(i, j int) bool {
  57. if connections[i].Connected != connections[j].Connected {
  58. // Connected items come first
  59. return connections[i].Connected
  60. }
  61. // If both have same connection status, sort by name
  62. return connections[i].Name < connections[j].Name
  63. })
  64. items := make([]list.Item, len(connections))
  65. for i, conn := range connections {
  66. items[i] = item{
  67. address: getAddress(conn),
  68. name: conn.Name,
  69. connected: conn.Connected,
  70. connType: conn.Type,
  71. }
  72. }
  73. m.list = newList(items, m.width, m.height-listHeightOffset)
  74. m.loading = false
  75. return m, nil
  76. }
  77. // handleConnection initiates a connection to the selected item.
  78. func (m model) handleConnection() (model, tea.Cmd) {
  79. i, ok := m.list.SelectedItem().(item)
  80. if !ok {
  81. return m, nil
  82. }
  83. m.loading = true
  84. name := i.name
  85. address := i.address
  86. return m, tea.Batch(
  87. func() tea.Msg {
  88. return connectSDM(name, address)
  89. },
  90. m.spinner.Tick,
  91. )
  92. }
  93. // handleConnectResult processes the result of a connect operation.
  94. func (m model) handleConnectResult(msg connectMsg) (model, tea.Cmd) {
  95. m.loading = false
  96. if msg.err != nil {
  97. m.err = fmt.Sprintf("Error connecting to %s: %v", msg.name, msg.err)
  98. return m, nil
  99. }
  100. clipboard.WriteAll(msg.address)
  101. sendNotification("SDM Connected", fmt.Sprintf("Connected to %s", msg.name))
  102. return m, tea.Quit
  103. }
  104. // handleKillConnections disconnects all connections.
  105. func (m model) handleKillConnections() (model, tea.Cmd) {
  106. m.loading = true
  107. return m, tea.Batch(
  108. func() tea.Msg {
  109. cmd := exec.Command("sdm", "disconnect", "--all")
  110. output, err := cmd.CombinedOutput()
  111. if err != nil {
  112. return disconnectMsg{err: fmt.Errorf("disconnecting: %v: %s", err, string(output))}
  113. }
  114. return disconnectMsg{}
  115. },
  116. m.spinner.Tick,
  117. )
  118. }
  119. // handleDisconnectSelected disconnects the currently selected connection.
  120. func (m model) handleDisconnectSelected() (model, tea.Cmd) {
  121. i, ok := m.list.SelectedItem().(item)
  122. if !ok || !i.connected {
  123. return m, nil
  124. }
  125. m.loading = true
  126. name := i.name
  127. return m, tea.Batch(
  128. func() tea.Msg {
  129. return disconnectSDM(name)
  130. },
  131. m.spinner.Tick,
  132. )
  133. }
  134. // handleDisconnect processes the result of a disconnect operation.
  135. func (m model) handleDisconnect(msg disconnectMsg) (model, tea.Cmd) {
  136. m.loading = false
  137. if msg.err != nil {
  138. m.err = fmt.Sprintf("Error disconnecting: %v", msg.err)
  139. return m, nil
  140. }
  141. name := "all connections"
  142. if msg.name != "" {
  143. name = msg.name
  144. }
  145. sendNotification("SDM Disconnected", fmt.Sprintf("Disconnected from %s", name))
  146. m.loading = true
  147. return m, tea.Batch(fetchStatus, m.spinner.Tick)
  148. }
  149. // handleKeyInput processes keyboard input based on the current application state.
  150. func (m model) handleKeyInput(msg tea.KeyMsg) (model, tea.Cmd) {
  151. if m.promptingEmail {
  152. switch msg.String() {
  153. case "enter":
  154. email := m.emailInput.Value()
  155. if email != "" && !m.loggingIn {
  156. m.loggingIn = true
  157. return m, tea.Batch(
  158. func() tea.Msg {
  159. return loginSDM(email)
  160. },
  161. m.spinner.Tick,
  162. )
  163. }
  164. case " ":
  165. if !m.loggingIn {
  166. m.rememberEmail = !m.rememberEmail
  167. }
  168. return m, nil
  169. }
  170. if isQuitKey(msg.String()) {
  171. return m, tea.Quit
  172. }
  173. if !m.loggingIn {
  174. var cmd tea.Cmd
  175. m.emailInput, cmd = m.emailInput.Update(msg)
  176. return m, cmd
  177. }
  178. return m, nil
  179. }
  180. if m.err != "" {
  181. switch msg.String() {
  182. case "r":
  183. m.err = ""
  184. m.loading = true
  185. m.promptingEmail = false
  186. m.loggingIn = false
  187. return m, tea.Batch(
  188. func() tea.Msg {
  189. if err := checkSDMBinary(); err != nil {
  190. return statusMsg{err: err}
  191. }
  192. return fetchStatus()
  193. },
  194. m.spinner.Tick,
  195. )
  196. }
  197. if isQuitKey(msg.String()) {
  198. return m, tea.Quit
  199. }
  200. return m, nil
  201. }
  202. if m.loading {
  203. return m, nil
  204. }
  205. if m.list.FilterState() == list.Filtering {
  206. var cmd tea.Cmd
  207. m.list, cmd = m.list.Update(msg)
  208. return m, cmd
  209. }
  210. switch msg.String() {
  211. case tea.KeyEnter.String():
  212. return m.handleConnection()
  213. case tea.KeyCtrlX.String():
  214. return m.handleKillConnections()
  215. case "d":
  216. return m.handleDisconnectSelected()
  217. case "r":
  218. m.loading = true
  219. return m, tea.Batch(fetchStatus, m.spinner.Tick)
  220. }
  221. if isQuitKey(msg.String()) {
  222. return m, tea.Quit
  223. }
  224. var cmd tea.Cmd
  225. m.list, cmd = m.list.Update(msg)
  226. return m, cmd
  227. }