From ff52806a673c6ea8483af1876b5b69e68eadab40 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Tue, 28 Jul 2026 20:21:15 -0400 Subject: [PATCH] auth: render with lipgloss --- go.mod | 22 ++++--- go.sum | 48 +++++++++----- internal/cli/auth_list.go | 2 +- internal/cli/auth_login.go | 10 ++- internal/cli/auth_logout.go | 17 +++-- internal/cli/auth_status.go | 80 ++++++++++++++++++++---- internal/cli/auth_status_test.go | 103 +++++++++++++++++++++++++++++++ internal/cli/auth_switch.go | 12 +++- internal/cli/detail.go | 11 ++-- internal/cli/rows.go | 4 +- internal/cli/rows_test.go | 2 +- nix/tg.nix | 2 +- 12 files changed, 258 insertions(+), 55 deletions(-) create mode 100644 internal/cli/auth_status_test.go diff --git a/go.mod b/go.mod index 51f26ef..247a32c 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module github.com/alyraffauf/tg go 1.26.4 require ( + charm.land/lipgloss/v2 v2.0.5 github.com/bluesky-social/indigo v0.0.0-20260629160527-dfe5578fd537 github.com/charmbracelet/glamour v1.0.0 - github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 - github.com/charmbracelet/x/term v0.2.1 + github.com/charmbracelet/x/term v0.2.2 github.com/ipfs/go-cid v0.6.2 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 @@ -21,10 +21,16 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/x/ansi v0.10.2 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect @@ -38,12 +44,13 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/klauspost/cpuid/v2 v2.4.0 // 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.22 // indirect - github.com/mattn/go-runewidth v0.0.17 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mr-tron/base58 v1.3.0 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect @@ -74,6 +81,7 @@ require ( golang.org/x/crypto v0.54.0 // indirect golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/term v0.45.0 // indirect golang.org/x/text v0.40.0 // indirect diff --git a/go.sum b/go.sum index 50b0193..e917eb9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= +charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= @@ -6,8 +8,8 @@ github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -16,22 +18,32 @@ github.com/bluesky-social/indigo v0.0.0-20260629160527-dfe5578fd537 h1:rHaND0arg github.com/bluesky-social/indigo v0.0.0-20260629160527-dfe5578fd537/go.mod h1:JqQkz8lrOI6YZivP38GHmtVOTtzsNToITKj1gMpU5Jo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +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/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= -github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= +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/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +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/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI= github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -74,19 +86,21 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -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.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= -github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= @@ -170,6 +184,8 @@ golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzH golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= diff --git a/internal/cli/auth_list.go b/internal/cli/auth_list.go index f450f4a..1133306 100644 --- a/internal/cli/auth_list.go +++ b/internal/cli/auth_list.go @@ -29,7 +29,7 @@ func renderAuthAccountList(writer io.Writer, items []app.AuthAccountResult) { for _, item := range items { active := "" if item.Active { - active = "*" + active = "✓" } rows = append(rows, []string{active, item.Handle, item.DID, item.Method}) } diff --git a/internal/cli/auth_login.go b/internal/cli/auth_login.go index fa95d6f..35f8b0e 100644 --- a/internal/cli/auth_login.go +++ b/internal/cli/auth_login.go @@ -42,7 +42,7 @@ func newAuthLoginCommand(service *app.Service) *cobra.Command { fmt.Fprintln(cmd.ErrOrStderr(), "Login completed but session could not be confirmed.") return err } - fmt.Fprintf(cmd.OutOrStdout(), "Logged in as %s\n", did) + renderAuthLogin(cmd.OutOrStdout(), did) return nil } @@ -75,7 +75,7 @@ func newAuthLoginCommand(service *app.Service) *cobra.Command { fmt.Fprintln(cmd.ErrOrStderr(), "Login completed but session could not be confirmed.") return err } - fmt.Fprintf(cmd.OutOrStdout(), "Logged in as %s\n", did) + renderAuthLogin(cmd.OutOrStdout(), did) return nil case <-ctx.Done(): return ctx.Err() @@ -87,6 +87,12 @@ func newAuthLoginCommand(service *app.Service) *cobra.Command { return command } +func renderAuthLogin(writer io.Writer, did string) { + styles := newAuthOutputStyles(isTerminal(writer)) + writeAuthHeading(writer, styles.active, "✓", "Logged in") + writeAuthDetail(writer, styles, "DID", styles.dim.Render(did)) +} + func loginPassword(args []string, fromStdin bool, stdin io.Reader) (string, bool, error) { if !fromStdin { if len(args) < 2 { diff --git a/internal/cli/auth_logout.go b/internal/cli/auth_logout.go index 58e2192..edbbda5 100644 --- a/internal/cli/auth_logout.go +++ b/internal/cli/auth_logout.go @@ -1,7 +1,7 @@ package cli import ( - "fmt" + "io" "github.com/alyraffauf/tg/internal/app" "github.com/spf13/cobra" @@ -20,14 +20,19 @@ func newAuthLogoutCommand(service *app.Service) *cobra.Command { return err } return output(cmd, result, func(r *app.AuthLogoutResult) { - if r.WasLoggedIn { - fmt.Fprintln(cmd.OutOrStdout(), "Logged out.") - } else { - fmt.Fprintln(cmd.OutOrStdout(), "Not logged in.") - } + renderAuthLogout(cmd.OutOrStdout(), r) }) }, } command.Flags().BoolVar(&logoutAll, "all", false, "Log out all accounts") return command } + +func renderAuthLogout(writer io.Writer, result *app.AuthLogoutResult) { + styles := newAuthOutputStyles(isTerminal(writer)) + if result.WasLoggedIn { + writeAuthHeading(writer, styles.active, "✓", "Logged out") + return + } + writeAuthHeading(writer, styles.inactive, "○", "Not logged in") +} diff --git a/internal/cli/auth_status.go b/internal/cli/auth_status.go index 54d00b5..4ca3070 100644 --- a/internal/cli/auth_status.go +++ b/internal/cli/auth_status.go @@ -2,7 +2,9 @@ package cli import ( "fmt" + "io" + "charm.land/lipgloss/v2" "github.com/alyraffauf/tg/internal/app" "github.com/spf13/cobra" ) @@ -17,19 +19,73 @@ func newAuthStatusCommand(service *app.Service) *cobra.Command { return err } return output(cmd, result, func(r *app.AuthStatusResult) { - if !r.Authenticated { - fmt.Fprintln(cmd.OutOrStdout(), "Not logged in.") - return - } - switch r.Status { - case app.SessionStatusActive: - fmt.Fprintf(cmd.OutOrStdout(), "Logged in as %s\n", r.Handle) - case app.SessionStatusExpired: - fmt.Fprintln(cmd.OutOrStdout(), "Session expired. Run \"tg auth login\" to re-authenticate.") - case app.SessionStatusUnknown: - fmt.Fprintln(cmd.OutOrStdout(), "Unable to verify session (network error).") - } + renderAuthStatus(cmd.OutOrStdout(), r) }) }, } } + +func renderAuthStatus(writer io.Writer, result *app.AuthStatusResult) { + styles := newAuthOutputStyles(isTerminal(writer)) + if !result.Authenticated { + writeAuthHeading(writer, styles.inactive, "○", "Not logged in") + writeAuthDetail(writer, styles, "Action", "tg auth login ") + return + } + + switch result.Status { + case app.SessionStatusActive: + writeAuthHeading(writer, styles.active, "✓", "Authenticated") + writeAuthIdentity(writer, styles, result) + writeAuthDetail(writer, styles, "Session", "Active") + case app.SessionStatusExpired: + writeAuthHeading(writer, styles.warning, "!", "Session expired") + writeAuthIdentity(writer, styles, result) + writeAuthDetail(writer, styles, "Action", "tg auth login "+result.Handle) + case app.SessionStatusUnknown: + writeAuthHeading(writer, styles.warning, "?", "Could not verify session") + writeAuthIdentity(writer, styles, result) + writeAuthDetail(writer, styles, "Reason", "Network error") + writeAuthDetail(writer, styles, "Retry", "tg auth status") + } +} + +type authOutputStyles struct { + active lipgloss.Style + inactive lipgloss.Style + warning lipgloss.Style + label lipgloss.Style + value lipgloss.Style + dim lipgloss.Style +} + +func newAuthOutputStyles(colorEnabled bool) authOutputStyles { + styles := authOutputStyles{} + if !colorEnabled { + return styles + } + styles.active = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Green) + styles.inactive = lipgloss.NewStyle().Faint(true) + styles.warning = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Yellow) + styles.label = lipgloss.NewStyle().Faint(true) + styles.value = lipgloss.NewStyle().Bold(true) + styles.dim = lipgloss.NewStyle().Faint(true) + return styles +} + +func writeAuthHeading(writer io.Writer, style lipgloss.Style, icon, text string) { + fmt.Fprintf(writer, "%s %s\n", style.Render(icon), style.Render(text)) +} + +func writeAuthIdentity(writer io.Writer, styles authOutputStyles, result *app.AuthStatusResult) { + if result.Handle != "" { + writeAuthDetail(writer, styles, "Account", styles.value.Render(result.Handle)) + } + if result.DID != "" { + writeAuthDetail(writer, styles, "DID", styles.dim.Render(result.DID)) + } +} + +func writeAuthDetail(writer io.Writer, styles authOutputStyles, label, value string) { + fmt.Fprintf(writer, " %s %s\n", styles.label.Render(label), value) +} diff --git a/internal/cli/auth_status_test.go b/internal/cli/auth_status_test.go new file mode 100644 index 0000000..ecc0e5c --- /dev/null +++ b/internal/cli/auth_status_test.go @@ -0,0 +1,103 @@ +package cli + +import ( + "bytes" + "testing" + + "github.com/alyraffauf/tg/internal/app" +) + +func TestRenderAuthStatus(t *testing.T) { + tests := []struct { + name string + result app.AuthStatusResult + want string + }{ + { + name: "not logged in", + want: "○ Not logged in\n Action tg auth login \n", + }, + { + name: "active", + result: app.AuthStatusResult{ + Authenticated: true, + Status: app.SessionStatusActive, + Handle: "aly.codes", + DID: "did:plc:abc123", + }, + want: "✓ Authenticated\n Account aly.codes\n DID did:plc:abc123\n Session Active\n", + }, + { + name: "expired", + result: app.AuthStatusResult{ + Authenticated: true, + Status: app.SessionStatusExpired, + Handle: "aly.codes", + }, + want: "! Session expired\n Account aly.codes\n Action tg auth login aly.codes\n", + }, + { + name: "unknown", + result: app.AuthStatusResult{ + Authenticated: true, + Status: app.SessionStatusUnknown, + Handle: "aly.codes", + }, + want: "? Could not verify session\n Account aly.codes\n Reason Network error\n Retry tg auth status\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var output bytes.Buffer + renderAuthStatus(&output, &tt.result) + if got := output.String(); got != tt.want { + t.Fatalf("renderAuthStatus() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestRenderAuthMessages(t *testing.T) { + tests := []struct { + name string + render func(*bytes.Buffer) + want string + }{ + { + name: "login", + render: func(output *bytes.Buffer) { + renderAuthLogin(output, "did:plc:abc123") + }, + want: "✓ Logged in\n DID did:plc:abc123\n", + }, + { + name: "logout", + render: func(output *bytes.Buffer) { + renderAuthLogout(output, &app.AuthLogoutResult{WasLoggedIn: true}) + }, + want: "✓ Logged out\n", + }, + { + name: "switch", + render: func(output *bytes.Buffer) { + renderAuthSwitch(output, &app.AuthAccountResult{ + Handle: "aly.codes", + DID: "did:plc:abc123", + Method: "oauth", + }) + }, + want: "✓ Active account changed\n Account aly.codes\n DID did:plc:abc123\n Method oauth\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var output bytes.Buffer + tt.render(&output) + if got := output.String(); got != tt.want { + t.Fatalf("render() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/internal/cli/auth_switch.go b/internal/cli/auth_switch.go index 4067df5..d77f9e1 100644 --- a/internal/cli/auth_switch.go +++ b/internal/cli/auth_switch.go @@ -1,7 +1,7 @@ package cli import ( - "fmt" + "io" "github.com/alyraffauf/tg/internal/app" "github.com/spf13/cobra" @@ -18,8 +18,16 @@ func newAuthSwitchCommand(service *app.Service) *cobra.Command { return err } return output(cmd, result, func(item *app.AuthAccountResult) { - fmt.Fprintf(cmd.OutOrStdout(), "Switched to %s\n", item.Handle) + renderAuthSwitch(cmd.OutOrStdout(), item) }) }, } } + +func renderAuthSwitch(writer io.Writer, account *app.AuthAccountResult) { + styles := newAuthOutputStyles(isTerminal(writer)) + writeAuthHeading(writer, styles.active, "✓", "Active account changed") + writeAuthDetail(writer, styles, "Account", styles.value.Render(account.Handle)) + writeAuthDetail(writer, styles, "DID", styles.dim.Render(account.DID)) + writeAuthDetail(writer, styles, "Method", account.Method) +} diff --git a/internal/cli/detail.go b/internal/cli/detail.go index d2711e2..3306a8b 100644 --- a/internal/cli/detail.go +++ b/internal/cli/detail.go @@ -5,8 +5,8 @@ import ( "io" "strings" + "charm.land/lipgloss/v2" "github.com/charmbracelet/glamour" - "github.com/charmbracelet/lipgloss" ) // defaultTerminalWidth is used when the terminal width can't be detected. @@ -18,11 +18,12 @@ type detailField struct { } // renderDetail writes aligned label/value pairs plus an optional markdown -// body. A writer-aware lipgloss renderer keeps TTY and piped output in one -// code path; the body renders as markdown on a terminal, verbatim otherwise. +// body. The body renders as markdown on a terminal, verbatim otherwise. func renderDetail(writer io.Writer, fields []detailField, body string) { - renderer := lipgloss.NewRenderer(writer) - labelStyle := renderer.NewStyle().Bold(true).Width(labelColumnWidth(fields)) + labelStyle := lipgloss.NewStyle().Width(labelColumnWidth(fields)) + if isTerminal(writer) { + labelStyle = labelStyle.Bold(true) + } for _, field := range fields { fmt.Fprintf(writer, "%s%s\n", labelStyle.Render(field.label+":"), field.value) diff --git a/internal/cli/rows.go b/internal/cli/rows.go index 8ce1e0a..291e942 100644 --- a/internal/cli/rows.go +++ b/internal/cli/rows.go @@ -7,9 +7,9 @@ import ( "strings" "text/tabwriter" + "charm.land/lipgloss/v2" + "charm.land/lipgloss/v2/table" "github.com/alyraffauf/tg/internal/app" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/table" xterm "github.com/charmbracelet/x/term" ) diff --git a/internal/cli/rows_test.go b/internal/cli/rows_test.go index 39fe1f8..feda58e 100644 --- a/internal/cli/rows_test.go +++ b/internal/cli/rows_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/charmbracelet/lipgloss" + "charm.land/lipgloss/v2" ) func TestShortDate(t *testing.T) { diff --git a/nix/tg.nix b/nix/tg.nix index a619ed1..e74ebcc 100644 --- a/nix/tg.nix +++ b/nix/tg.nix @@ -10,7 +10,7 @@ buildGoModule { version = "dev"; src = ../.; proxyVendor = true; - vendorHash = "sha256-Vh4uhJOXGMYoX7haUD6r3zti1KSb/uO5+TOtn0NrcCk="; + vendorHash = "sha256-SxmM+vW2E1i0E1ojUvv0B6Scr8A2bWbdu8Nf6Tt9vRg="; subPackages = ["cmd/tg"]; nativeBuildInputs = [installShellFiles]; -- 2.51.2