diff --git a/cmd/lard-client/main.go b/cmd/lard-client/main.go index 7f723e8..1dfc7ee 100644 --- a/cmd/lard-client/main.go +++ b/cmd/lard-client/main.go @@ -22,6 +22,7 @@ import ( "github.com/taciturnaxolotl/lard/internal/dotenv" "github.com/taciturnaxolotl/lard/internal/service" "github.com/taciturnaxolotl/lard/internal/setup" + "github.com/taciturnaxolotl/lard/internal/ui" ) // version is overridden at build time via -ldflags. @@ -102,8 +103,8 @@ only has to re-authenticate.`, } func printConnected(cfg *client.Config) { - fmt.Printf("Connected to %s via %s.\n", cfg.URL, cfg.AuthMode()) - fmt.Printf("Saved %s\n\nNext: lard-client backfill\n", client.DefaultConfigPath()) + fmt.Println() + fmt.Printf("Connected to %s via %s.\n", ui.Link(cfg.URL, "lard-server"), cfg.AuthMode()) } // --- collection --- diff --git a/go.mod b/go.mod index 8531295..82ccc23 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( charm.land/fang/v2 v2.0.1 charm.land/fantasy v0.38.1 charm.land/huh/v2 v2.0.3 + charm.land/lipgloss/v2 v2.0.5 + github.com/charmbracelet/x/exp/charmtone v0.0.0-20260726004341-482a56510f1b github.com/google/uuid v1.6.0 github.com/modelcontextprotocol/go-sdk v1.6.1 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c @@ -18,13 +20,11 @@ require ( require ( charm.land/bubbles/v2 v2.0.0 // indirect charm.land/bubbletea/v2 v2.0.2 // indirect - charm.land/lipgloss/v2 v2.0.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect - github.com/charmbracelet/x/ansi v0.11.6 // indirect - github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 // indirect github.com/charmbracelet/x/exp/strings v0.1.0 // indirect @@ -41,9 +41,9 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kaptinlin/jsonpointer v0.4.27 // indirect github.com/kaptinlin/jsonschema v0.9.3 // indirect - github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/mango v0.1.0 // indirect diff --git a/go.sum b/go.sum index d30be3d..df41f78 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ charm.land/fantasy v0.38.1 h1:U1HNOVQaCK/3zwDbwUYZoiTXSZsoxn0JpnrVfqC4O5k= charm.land/fantasy v0.38.1/go.mod h1:4iPCl1cQYjWQHdN/L9n0sjWpWYuMY/ci0SWYcy5bONc= charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= -charm.land/lipgloss/v2 v2.0.1 h1:6Xzrn49+Py1Um5q/wZG1gWgER2+7dUyZ9XMEufqPSys= -charm.land/lipgloss/v2 v2.0.1/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= +charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= +charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -18,18 +18,18 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= -github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= -github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= -github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs= github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260726004341-482a56510f1b h1:KXyKTg/2+ktNqnc/UZ6hpwfhjWK3EIPjenZNhmCK4WY= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260726004341-482a56510f1b/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= @@ -81,12 +81,12 @@ github.com/kaptinlin/jsonpointer v0.4.27 h1:5FOnhlkqQ4/lvHudaAWS8HJCXjN4yAHSIGl7 github.com/kaptinlin/jsonpointer v0.4.27/go.mod h1:dfub/n58cWS32Dyf3AZsnKblSAgrz9PyOU76GDPpx8Q= github.com/kaptinlin/jsonschema v0.9.3 h1:uDVd3w4aXwO0tbycblKYvFofhl3hVuE31vOl5JkDXI0= github.com/kaptinlin/jsonschema v0.9.3/go.mod h1:LvtQ/mO0E1e/3c3DiOWTj05LTeot8cTv7DyQq7eJMQg= -github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= -github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= -github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= diff --git a/internal/client/device.go b/internal/client/device.go index c43fd6e..ca663e3 100644 --- a/internal/client/device.go +++ b/internal/client/device.go @@ -12,6 +12,7 @@ import ( "time" "github.com/pkg/browser" + "github.com/taciturnaxolotl/lard/internal/ui" "golang.org/x/oauth2" ) @@ -81,7 +82,6 @@ func LoginDevice(ctx context.Context, serverURL, clientID string, scopes []strin } switch code { case "": - fmt.Println("Connected.") return tok, nil case errAuthorizationPending: // Still waiting on the human. @@ -129,6 +129,9 @@ func startDevice(ctx context.Context, endpoint, clientID string, scopes []string return nil, fmt.Errorf("starting authorization: %w", err) } defer resp.Body.Close() + if resp.StatusCode == http.StatusTooManyRequests { + return nil, errors.New("authorization server is rate-limited; wait a moment and try again") + } var auth DeviceAuth if err := json.NewDecoder(resp.Body).Decode(&auth); err != nil { return nil, fmt.Errorf("starting authorization: unreadable response (status %d)", resp.StatusCode) @@ -212,16 +215,16 @@ func printDevicePrompt(auth *DeviceAuth, openBrowser bool) { opened = browser.OpenURL(target) == nil } if opened { - fmt.Println("Opened your browser to authorize. If it opened on the wrong") - fmt.Println("machine, use this URL from any browser instead:") + fmt.Println("Opened your browser to authorize.") + fmt.Println("If it opened on the wrong machine, use this link instead:") } else { - fmt.Println("Open this URL in any browser to authorize:") + fmt.Println("Open this link in any browser to authorize:") } - // Bare and unstyled on its own line: terminals linkify a lone URL, and - // decoration breaks copy and paste. - fmt.Printf("\n%s\n\n", target) + fmt.Println() + fmt.Println(ui.Link(target, "lard-device")) + fmt.Println() if auth.UserCode != "" { - fmt.Printf("Your code: %s\n", auth.UserCode) + fmt.Printf("Your code: %s\n", ui.Success(auth.UserCode)) } - fmt.Println("Waiting for you to authorize...") + fmt.Println(ui.Subtle("Waiting for you to authorize...")) } diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 94bca96..7fcb50b 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -18,6 +18,7 @@ import ( "golang.org/x/term" "github.com/taciturnaxolotl/lard/internal/client" + "github.com/taciturnaxolotl/lard/internal/ui" ) // Interactive reports whether we can prompt: a TTY on both ends, and no CI @@ -110,7 +111,7 @@ func Logout(ctx context.Context) error { if revoked { fmt.Println("Revoked the refresh token and removed local credentials.") } else { - fmt.Println("Removed local credentials.") + fmt.Println(ui.Subtle("Removed local credentials.")) } return nil } diff --git a/internal/ui/styles.go b/internal/ui/styles.go new file mode 100644 index 0000000..5b76dff --- /dev/null +++ b/internal/ui/styles.go @@ -0,0 +1,28 @@ +// Package ui provides terminal styling for lard-client output. +package ui + +import ( + "charm.land/lipgloss/v2" + "github.com/charmbracelet/x/exp/charmtone" +) + +// Link renders a URL as an OSC 8 hyperlink with a subtle green underline, +// matching the charm/crush convention. The id parameter is optional; pass "" +// if you don't need a stable anchor. +func Link(url, id string) string { + return lipgloss.NewStyle(). + Foreground(charmtone.Guac). + Underline(true). + Hyperlink(url, id). + Render(url) +} + +// Success formats text in the same green used by crush for positive status. +func Success(s string) string { + return lipgloss.NewStyle().Foreground(charmtone.Julep).Render(s) +} + +// Subtle formats text in a muted tone for secondary information. +func Subtle(s string) string { + return lipgloss.NewStyle().Foreground(charmtone.Smoke).Render(s) +}