11//
2- // Copyright 2024 The Chainloop Authors.
2+ // Copyright 2024-2026 The Chainloop Authors.
33//
44// Licensed under the Apache License, Version 2.0 (the "License");
55// you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import (
2323 "net"
2424 "net/http"
2525 "net/url"
26+ "os"
2627 "os/exec"
2728 "runtime"
2829 "time"
@@ -32,6 +33,7 @@ import (
3233 jwt "github.com/golang-jwt/jwt/v4"
3334 "github.com/spf13/cobra"
3435 "github.com/spf13/viper"
36+ "golang.org/x/term"
3537)
3638
3739type app struct {
@@ -87,11 +89,25 @@ func interactiveAuth(forceHeadless bool) error {
8789
8890 a .serverChan = make (chan error )
8991
90- // Run server in background
92+ loginURLStr := serverLoginURL .String ()
93+ fmt .Println ()
94+ fmt .Println (" Browser didn't open? Use the URL below to sign in (press c to copy):" )
95+ fmt .Println ()
96+ fmt .Printf (" %s\n " , loginURLStr )
97+ fmt .Println ()
98+ fmt .Println (" Waiting for authentication to complete..." )
99+
100+ // Save terminal state before raw mode so we can restore it reliably.
101+ // The keypress goroutine may be blocked on stdin.Read when auth completes,
102+ // so its deferred restore won't run until the process exits.
103+ fd := int (os .Stdin .Fd ())
104+ termState , _ := term .GetState (fd )
105+
106+ done := make (chan struct {})
107+ go listenForCopyKeypress (loginURLStr , done )
108+
91109 http .HandleFunc (callbackURL .Path , a .handleCallback )
92110 go func () {
93- logger .Info ().Msg ("waiting for the authentication to be completed, please check your browser" )
94-
95111 server := & http.Server {ReadHeaderTimeout : time .Second }
96112
97113 err := server .Serve (listener )
@@ -101,6 +117,10 @@ func interactiveAuth(forceHeadless bool) error {
101117 }()
102118
103119 err = <- a .serverChan
120+ close (done )
121+ if termState != nil {
122+ term .Restore (fd , termState )
123+ }
104124 if err != nil {
105125 return err
106126 }
@@ -206,3 +226,47 @@ func localListenerAndCallbackURL() (net.Listener, *url.URL, error) {
206226 callbackURL := & url.URL {Scheme : "http" , Host : listener .Addr ().String (), Path : "/auth/callback" }
207227 return listener , callbackURL , nil
208228}
229+
230+ const ctrlC = 3
231+
232+ // listenForCopyKeypress puts the terminal in raw mode and listens for
233+ // 'c' keypress to copy the URL to the system clipboard.
234+ func listenForCopyKeypress (urlStr string , done <- chan struct {}) {
235+ fd := int (os .Stdin .Fd ())
236+
237+ oldState , err := term .MakeRaw (fd )
238+ if err != nil {
239+ return
240+ }
241+ defer term .Restore (fd , oldState )
242+
243+ buf := make ([]byte , 1 )
244+ for {
245+ select {
246+ case <- done :
247+ return
248+ default :
249+ }
250+
251+ n , err := os .Stdin .Read (buf )
252+ if err != nil || n == 0 {
253+ return
254+ }
255+
256+ switch buf [0 ] {
257+ case 'c' , 'C' :
258+ term .Restore (fd , oldState )
259+ if err := copyToClipboard (urlStr ); err != nil {
260+ fmt .Println (" Could not copy to clipboard. Please copy the URL manually." )
261+ } else {
262+ fmt .Println (" Copied to clipboard!" )
263+ }
264+ oldState , err = term .MakeRaw (fd )
265+ if err != nil {
266+ return
267+ }
268+ case ctrlC :
269+ return
270+ }
271+ }
272+ }
0 commit comments