@@ -243,6 +243,11 @@ func Edit(app common.AppController, v *view.ResourceView) {
243243 }
244244 }
245245
246+ if strings .HasPrefix (currentEndpoint , "ssh://" ) {
247+ editSSH (app , id , currentDesc , currentEndpoint )
248+ return
249+ }
250+
246251 fields := []dialogs.FormField {
247252 {Name : "description" , Label : "Description" , Type : dialogs .FieldTypeInput , Default : currentDesc },
248253 {Name : "host" , Label : "Docker Host" , Type : dialogs .FieldTypeInput , Default : currentEndpoint },
@@ -256,13 +261,143 @@ func Edit(app common.AppController, v *view.ResourceView) {
256261
257262 app .RunInBackground (func () {
258263 err := app .GetDocker ().UpdateContext (id , description , host )
264+ isCurrent := app .GetDocker ().ContextName == id
259265 app .GetTviewApp ().QueueUpdateDraw (func () {
260266 if err != nil {
261267 app .AppendFlashError (fmt .Sprintf ("failed to update context: %v" , err ))
268+ app .RefreshCurrentView ()
269+ return
270+ }
271+ app .AppendFlashSuccess (fmt .Sprintf ("context %s updated" , id ))
272+ if isCurrent {
273+ // Rebuild the client so the new endpoint applies immediately
274+ app .ReloadContext (id )
262275 } else {
263- app .AppendFlashSuccess (fmt .Sprintf ("context %s updated" , id ))
276+ app .RefreshCurrentView ()
277+ }
278+ })
279+ })
280+ })
281+ }
282+
283+ // parseSSHURL splits "ssh://user@ip[:port][/socket/path]" into the host
284+ // part and the optional remote socket path.
285+ func parseSSHURL (endpoint string ) (host , socket string ) {
286+ rest := strings .TrimPrefix (endpoint , "ssh://" )
287+ if idx := strings .Index (rest , "/" ); idx >= 0 {
288+ return rest [:idx ], rest [idx :]
289+ }
290+ return rest , ""
291+ }
292+
293+ func editSSH (app common.AppController , id , currentDesc , currentEndpoint string ) {
294+ existing , _ := secrets .Load (id )
295+
296+ currentAuth := secrets .AuthTypeKey
297+ if existing != nil && existing .AuthType != "" {
298+ currentAuth = existing .AuthType
299+ }
300+
301+ items := []dialogs.PickerItem {
302+ {Label : "SSH Key" , Description : "authenticate with a private key" , Value : secrets .AuthTypeKey },
303+ {Label : "Password" , Description : "authenticate with a password" , Value : secrets .AuthTypePassword },
304+ }
305+ for i := range items {
306+ if items [i ].Value == currentAuth {
307+ items [i ].Description += " (current)"
308+ }
309+ }
310+
311+ dialogs .ShowPicker (app , "Authentication Method" , items , func (authType string ) {
312+ showSSHEditForm (app , id , currentDesc , currentEndpoint , authType , existing )
313+ })
314+ }
315+
316+ func showSSHEditForm (app common.AppController , id , currentDesc , currentEndpoint , authType string , existing * secrets.SSHCredentials ) {
317+ currentHost , currentSocket := parseSSHURL (currentEndpoint )
318+ if currentSocket == "" {
319+ currentSocket = "/var/run/docker.sock"
320+ }
321+
322+ // Pre-fill credentials only when the auth method is unchanged
323+ var defaultKey , defaultPassphrase , defaultPassword string
324+ if existing != nil && existing .AuthType == authType {
325+ defaultKey = existing .KeyPath
326+ defaultPassphrase = existing .Passphrase
327+ defaultPassword = existing .Password
328+ }
329+
330+ fields := []dialogs.FormField {
331+ {Name : "description" , Label : "Description" , Type : dialogs .FieldTypeInput , Default : currentDesc },
332+ {Name : "host" , Label : "Host (user@ip)" , Type : dialogs .FieldTypeInput , Default : currentHost },
333+ }
334+
335+ if authType == secrets .AuthTypeKey {
336+ fields = append (fields ,
337+ dialogs.FormField {Name : "key" , Label : "SSH Key" , Type : dialogs .FieldTypeInput , Default : defaultKey , Placeholder : "~/.ssh/id_ed25519" },
338+ dialogs.FormField {Name : "passphrase" , Label : "Passphrase (optional)" , Type : dialogs .FieldTypeInput , Default : defaultPassphrase , Secret : true },
339+ )
340+ } else {
341+ fields = append (fields ,
342+ dialogs.FormField {Name : "password" , Label : "Password" , Type : dialogs .FieldTypeInput , Default : defaultPassword , Secret : true },
343+ )
344+ }
345+
346+ fields = append (fields ,
347+ dialogs.FormField {Name : "socket" , Label : "Docker Socket" , Type : dialogs .FieldTypeInput , Default : currentSocket },
348+ )
349+
350+ dialogs .ShowFormWithDescription (app , fmt .Sprintf ("Edit Context: %s" , id ), "Updates the SSH context and its credentials" , fields , func (result dialogs.FormResult ) {
351+ description := result ["description" ]
352+ host := strings .TrimSpace (result ["host" ])
353+ socket := strings .TrimSpace (result ["socket" ])
354+
355+ if host == "" {
356+ app .SetFlashError ("host is required" )
357+ return
358+ }
359+ if authType == secrets .AuthTypePassword && result ["password" ] == "" {
360+ app .SetFlashError ("password is required" )
361+ return
362+ }
363+
364+ creds := secrets.SSHCredentials {
365+ AuthType : authType ,
366+ KeyPath : expandHome (strings .TrimSpace (result ["key" ])),
367+ Passphrase : result ["passphrase" ],
368+ Password : result ["password" ],
369+ }
370+
371+ sshURL := fmt .Sprintf ("ssh://%s" , host )
372+ if socket != "" && socket != "/var/run/docker.sock" {
373+ sshURL = fmt .Sprintf ("ssh://%s%s" , host , socket )
374+ }
375+
376+ app .AppendFlashPending (fmt .Sprintf ("updating context %s..." , id ))
377+
378+ app .RunInBackground (func () {
379+ err := app .GetDocker ().UpdateContext (id , description , sshURL )
380+ if err == nil {
381+ if kerr := secrets .Save (id , creds ); kerr != nil {
382+ app .GetTviewApp ().QueueUpdateDraw (func () {
383+ app .AppendFlashError (fmt .Sprintf ("context updated but credentials not saved: %v" , kerr ))
384+ })
385+ }
386+ }
387+ isCurrent := app .GetDocker ().ContextName == id
388+ app .GetTviewApp ().QueueUpdateDraw (func () {
389+ if err != nil {
390+ app .AppendFlashError (fmt .Sprintf ("failed to update context: %v" , err ))
391+ app .RefreshCurrentView ()
392+ return
393+ }
394+ app .AppendFlashSuccess (fmt .Sprintf ("SSH context '%s' updated" , id ))
395+ if isCurrent {
396+ // Rebuild the client so the new endpoint/credentials apply immediately
397+ app .ReloadContext (id )
398+ } else {
399+ app .RefreshCurrentView ()
264400 }
265- app .RefreshCurrentView ()
266401 })
267402 })
268403 })
0 commit comments