tangled
alpha
login
or
join now
mariuskimmina.eurosky.social
/
recard
0
fork
atom
this repo has no description
0
fork
atom
overview
issues
pulls
1
pipelines
Initial boilerplate
Marius Kimmina
2 months ago
34741c1f
+597
13 changed files
expand all
collapse all
unified
split
.gitignore
appview
config
config.go
oauth
oauth.go
pages
pages.go
templates
index.html
login.html
notice.html
state
login.go
state.go
cmd
appview
main.go
go.mod
go.sum
log
log.go
+32
.gitignore
···
1
1
+
*.exe
2
2
+
*.exe~
3
3
+
*.dll
4
4
+
*.so
5
5
+
*.dylib
6
6
+
7
7
+
# Test binary, built with `go test -c`
8
8
+
*.test
9
9
+
10
10
+
# Code coverage profiles and other test artifacts
11
11
+
*.out
12
12
+
coverage.*
13
13
+
*.coverprofile
14
14
+
profile.cov
15
15
+
16
16
+
# Dependency directories (remove the comment below to include it)
17
17
+
# vendor/
18
18
+
19
19
+
# Go workspace file
20
20
+
go.work
21
21
+
go.work.sum
22
22
+
23
23
+
# env file
24
24
+
.env
25
25
+
26
26
+
# Editor/IDE
27
27
+
# .idea/
28
28
+
# .vscode/
29
29
+
30
30
+
# Application-specific
31
31
+
*.db
32
32
+
/cmd/appview/appview
+53
appview/config/config.go
···
1
1
+
package config
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
6
6
+
"github.com/sethvargo/go-envconfig"
7
7
+
)
8
8
+
9
9
+
type CoreConfig struct {
10
10
+
CookieSecret string `env:"COOKIE_SECRET, default=00000000000000000000000000000000"`
11
11
+
DbPath string `env:"DB_PATH, default=appview.db"`
12
12
+
ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:3000"`
13
13
+
AppviewHost string `env:"APPVIEW_HOST, default=https://recard.blue"`
14
14
+
AppviewName string `env:"APPVIEW_Name, default=Recard"`
15
15
+
Dev bool `env:"DEV, default=false"`
16
16
+
DisallowedNicknamesFile string `env:"DISALLOWED_NICKNAMES_FILE"`
17
17
+
}
18
18
+
19
19
+
type OAuthConfig struct {
20
20
+
ClientSecret string `env:"CLIENT_SECRET"`
21
21
+
ClientKid string `env:"CLIENT_KID"`
22
22
+
}
23
23
+
24
24
+
type PlcConfig struct {
25
25
+
PLCURL string `env:"URL, default=https://plc.directory"`
26
26
+
}
27
27
+
28
28
+
type JetstreamConfig struct {
29
29
+
Endpoint string `env:"ENDPOINT, default=wss://jetstream1.us-east.bsky.network/subscribe"`
30
30
+
}
31
31
+
32
32
+
type PdsConfig struct {
33
33
+
Host string `env:"HOST, default=https://tngl.sh"`
34
34
+
AdminSecret string `env:"ADMIN_SECRET"`
35
35
+
}
36
36
+
37
37
+
type Config struct {
38
38
+
Core CoreConfig `env:",prefix=RECARD_"`
39
39
+
Jetstream JetstreamConfig `env:",prefix=RECARD_JETSTREAM_"`
40
40
+
OAuth OAuthConfig `env:",prefix=RECARD_OAUTH_"`
41
41
+
Plc PlcConfig `env:",prefix=RECARD_PLC_"`
42
42
+
Pds PdsConfig `env:",prefix=RECARD_PDS_"`
43
43
+
}
44
44
+
45
45
+
func LoadConfig(ctx context.Context) (*Config, error) {
46
46
+
var cfg Config
47
47
+
err := envconfig.Process(ctx, &cfg)
48
48
+
if err != nil {
49
49
+
return nil, err
50
50
+
}
51
51
+
52
52
+
return &cfg, nil
53
53
+
}
+105
appview/oauth/oauth.go
···
1
1
+
package oauth
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"crypto/rand"
6
6
+
"encoding/base64"
7
7
+
"fmt"
8
8
+
"net/http"
9
9
+
"sync"
10
10
+
11
11
+
"recard.blue/appview/config"
12
12
+
)
13
13
+
14
14
+
type OAuth struct {
15
15
+
config *config.Config
16
16
+
ClientApp *ClientApp
17
17
+
sessions map[string]*Session
18
18
+
mu sync.RWMutex
19
19
+
}
20
20
+
21
21
+
type ClientApp struct {
22
22
+
oauth *OAuth
23
23
+
}
24
24
+
25
25
+
type Session struct {
26
26
+
Handle string
27
27
+
State string
28
28
+
}
29
29
+
30
30
+
func New(ctx context.Context, c *config.Config) (*OAuth, error) {
31
31
+
o := &OAuth{
32
32
+
config: c,
33
33
+
sessions: make(map[string]*Session),
34
34
+
}
35
35
+
o.ClientApp = &ClientApp{oauth: o}
36
36
+
return o, nil
37
37
+
}
38
38
+
39
39
+
func (o *OAuth) Close() error {
40
40
+
return nil
41
41
+
}
42
42
+
43
43
+
func (ca *ClientApp) StartAuthFlow(ctx context.Context, handle string) (string, error) {
44
44
+
state := generateState()
45
45
+
46
46
+
ca.oauth.mu.Lock()
47
47
+
ca.oauth.sessions[state] = &Session{
48
48
+
Handle: handle,
49
49
+
State: state,
50
50
+
}
51
51
+
ca.oauth.mu.Unlock()
52
52
+
53
53
+
authURL := fmt.Sprintf("%s/oauth/callback?state=%s&handle=%s",
54
54
+
ca.oauth.config.Core.AppviewHost, state, handle)
55
55
+
56
56
+
return authURL, nil
57
57
+
}
58
58
+
59
59
+
func (o *OAuth) DeleteSession(w http.ResponseWriter, r *http.Request) error {
60
60
+
cookie, err := r.Cookie("session")
61
61
+
if err == nil {
62
62
+
o.mu.Lock()
63
63
+
delete(o.sessions, cookie.Value)
64
64
+
o.mu.Unlock()
65
65
+
}
66
66
+
67
67
+
http.SetCookie(w, &http.Cookie{
68
68
+
Name: "session",
69
69
+
Value: "",
70
70
+
Path: "/",
71
71
+
MaxAge: -1,
72
72
+
})
73
73
+
74
74
+
return nil
75
75
+
}
76
76
+
77
77
+
func (o *OAuth) Callback(w http.ResponseWriter, r *http.Request) {
78
78
+
state := r.URL.Query().Get("state")
79
79
+
80
80
+
o.mu.RLock()
81
81
+
_, exists := o.sessions[state]
82
82
+
o.mu.RUnlock()
83
83
+
84
84
+
if !exists {
85
85
+
http.Error(w, "Invalid session", http.StatusBadRequest)
86
86
+
return
87
87
+
}
88
88
+
89
89
+
http.SetCookie(w, &http.Cookie{
90
90
+
Name: "session",
91
91
+
Value: state,
92
92
+
Path: "/",
93
93
+
HttpOnly: true,
94
94
+
Secure: !o.config.Core.Dev,
95
95
+
SameSite: http.SameSiteLaxMode,
96
96
+
})
97
97
+
98
98
+
http.Redirect(w, r, "/", http.StatusFound)
99
99
+
}
100
100
+
101
101
+
func generateState() string {
102
102
+
b := make([]byte, 32)
103
103
+
rand.Read(b)
104
104
+
return base64.URLEncoding.EncodeToString(b)
105
105
+
}
+66
appview/pages/pages.go
···
1
1
+
package pages
2
2
+
3
3
+
import (
4
4
+
"embed"
5
5
+
"html/template"
6
6
+
"net/http"
7
7
+
)
8
8
+
9
9
+
//go:embed templates/*.html
10
10
+
var templatesFS embed.FS
11
11
+
12
12
+
type Pages struct {
13
13
+
templates *template.Template
14
14
+
}
15
15
+
16
16
+
type LoginParams struct {
17
17
+
ReturnUrl string
18
18
+
ErrorCode string
19
19
+
}
20
20
+
21
21
+
type loginData struct {
22
22
+
ErrorMsg string
23
23
+
}
24
24
+
25
25
+
type noticeData struct {
26
26
+
ID string
27
27
+
Message string
28
28
+
}
29
29
+
30
30
+
func New() *Pages {
31
31
+
tmpl := template.Must(template.ParseFS(templatesFS, "templates/*.html"))
32
32
+
return &Pages{
33
33
+
templates: tmpl,
34
34
+
}
35
35
+
}
36
36
+
37
37
+
func (p *Pages) Index(w http.ResponseWriter) {
38
38
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
39
39
+
p.templates.ExecuteTemplate(w, "index.html", nil)
40
40
+
}
41
41
+
42
42
+
func (p *Pages) Login(w http.ResponseWriter, params LoginParams) {
43
43
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
44
44
+
45
45
+
data := loginData{
46
46
+
ErrorMsg: params.ErrorCode,
47
47
+
}
48
48
+
49
49
+
p.templates.ExecuteTemplate(w, "login.html", data)
50
50
+
}
51
51
+
52
52
+
func (p *Pages) Notice(w http.ResponseWriter, id string, message string) {
53
53
+
w.Header().Set("Content-Type", "text/html; charset=utf-8")
54
54
+
55
55
+
data := noticeData{
56
56
+
ID: id,
57
57
+
Message: message,
58
58
+
}
59
59
+
60
60
+
p.templates.ExecuteTemplate(w, "notice.html", data)
61
61
+
}
62
62
+
63
63
+
func (p *Pages) HxRedirect(w http.ResponseWriter, url string) {
64
64
+
w.Header().Set("HX-Redirect", url)
65
65
+
w.WriteHeader(http.StatusOK)
66
66
+
}
+11
appview/pages/templates/index.html
···
1
1
+
<!DOCTYPE html>
2
2
+
<html>
3
3
+
<head>
4
4
+
<title>Recard</title>
5
5
+
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
6
6
+
</head>
7
7
+
<body>
8
8
+
<h1>Welcome to Recard</h1>
9
9
+
<p><a href="/login">Login</a></p>
10
10
+
</body>
11
11
+
</html>
+18
appview/pages/templates/login.html
···
1
1
+
<!DOCTYPE html>
2
2
+
<html>
3
3
+
<head>
4
4
+
<title>Login - Recard</title>
5
5
+
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
6
6
+
</head>
7
7
+
<body>
8
8
+
<h1>Login</h1>
9
9
+
{{if .ErrorMsg}}
10
10
+
<div id="login-msg" style="color: red;">Error: {{.ErrorMsg}}</div>
11
11
+
{{end}}
12
12
+
<form hx-post="/login" hx-target="body">
13
13
+
<label for="handle">Handle:</label>
14
14
+
<input type="text" id="handle" name="handle" placeholder="user.bsky.social" required>
15
15
+
<button type="submit">Login</button>
16
16
+
</form>
17
17
+
</body>
18
18
+
</html>
+1
appview/pages/templates/notice.html
···
1
1
+
<div id="{{.ID}}" style="color: red;">{{.Message}}</div>
+69
appview/state/login.go
···
1
1
+
package state
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
"net/http"
6
6
+
"strings"
7
7
+
8
8
+
"recard.blue/appview/pages"
9
9
+
)
10
10
+
11
11
+
func (s *State) Login(w http.ResponseWriter, r *http.Request) {
12
12
+
l := s.logger.With("handler", "Login")
13
13
+
14
14
+
switch r.Method {
15
15
+
case http.MethodGet:
16
16
+
returnURL := r.URL.Query().Get("return_url")
17
17
+
errorCode := r.URL.Query().Get("error")
18
18
+
s.pages.Login(w, pages.LoginParams{
19
19
+
ReturnUrl: returnURL,
20
20
+
ErrorCode: errorCode,
21
21
+
})
22
22
+
case http.MethodPost:
23
23
+
handle := r.FormValue("handle")
24
24
+
25
25
+
// when users copy their handle from bsky.app, it tends to have these characters around it:
26
26
+
//
27
27
+
// @nelind.dk:
28
28
+
// \u202a ensures that the handle is always rendered left to right and
29
29
+
// \u202c reverts that so the rest of the page renders however it should
30
30
+
handle = strings.TrimPrefix(handle, "\u202a")
31
31
+
handle = strings.TrimSuffix(handle, "\u202c")
32
32
+
33
33
+
// `@` is harmless
34
34
+
handle = strings.TrimPrefix(handle, "@")
35
35
+
36
36
+
// basic handle validation
37
37
+
if !strings.Contains(handle, ".") {
38
38
+
l.Error("invalid handle format", "raw", handle)
39
39
+
s.pages.Notice(
40
40
+
w,
41
41
+
"login-msg",
42
42
+
fmt.Sprintf("\"%s\" is an invalid handle. Did you mean %s.bsky.social or %s.tngl.sh?", handle, handle, handle),
43
43
+
)
44
44
+
return
45
45
+
}
46
46
+
47
47
+
redirectURL, err := s.oauth.ClientApp.StartAuthFlow(r.Context(), handle)
48
48
+
if err != nil {
49
49
+
l.Error("failed to start auth", "err", err)
50
50
+
http.Error(w, err.Error(), http.StatusInternalServerError)
51
51
+
return
52
52
+
}
53
53
+
54
54
+
s.pages.HxRedirect(w, redirectURL)
55
55
+
}
56
56
+
}
57
57
+
58
58
+
func (s *State) Logout(w http.ResponseWriter, r *http.Request) {
59
59
+
l := s.logger.With("handler", "Logout")
60
60
+
61
61
+
err := s.oauth.DeleteSession(w, r)
62
62
+
if err != nil {
63
63
+
l.Error("failed to logout", "err", err)
64
64
+
} else {
65
65
+
l.Info("logged out successfully")
66
66
+
}
67
67
+
68
68
+
s.pages.HxRedirect(w, "/login")
69
69
+
}
+64
appview/state/state.go
···
1
1
+
package state
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"log/slog"
6
6
+
"net/http"
7
7
+
8
8
+
"recard.blue/appview/config"
9
9
+
"recard.blue/appview/oauth"
10
10
+
"recard.blue/appview/pages"
11
11
+
tlog "recard.blue/log"
12
12
+
)
13
13
+
14
14
+
type State struct {
15
15
+
logger *slog.Logger
16
16
+
config *config.Config
17
17
+
pages *pages.Pages
18
18
+
oauth *oauth.OAuth
19
19
+
}
20
20
+
21
21
+
func Make(ctx context.Context, c *config.Config) (*State, error) {
22
22
+
logger := tlog.FromContext(ctx)
23
23
+
24
24
+
oauthHandler, err := oauth.New(ctx, c)
25
25
+
if err != nil {
26
26
+
return nil, err
27
27
+
}
28
28
+
29
29
+
pagesHandler := pages.New()
30
30
+
31
31
+
s := &State{
32
32
+
logger: logger,
33
33
+
config: c,
34
34
+
pages: pagesHandler,
35
35
+
oauth: oauthHandler,
36
36
+
}
37
37
+
38
38
+
return s, nil
39
39
+
}
40
40
+
41
41
+
func (s *State) Close() error {
42
42
+
if s.oauth != nil {
43
43
+
return s.oauth.Close()
44
44
+
}
45
45
+
return nil
46
46
+
}
47
47
+
48
48
+
func (s *State) Router() http.Handler {
49
49
+
mux := http.NewServeMux()
50
50
+
51
51
+
mux.HandleFunc("/login", s.Login)
52
52
+
mux.HandleFunc("/logout", s.Logout)
53
53
+
mux.HandleFunc("/oauth/callback", s.oauth.Callback)
54
54
+
55
55
+
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
56
56
+
if r.URL.Path != "/" {
57
57
+
http.NotFound(w, r)
58
58
+
return
59
59
+
}
60
60
+
s.pages.Index(w)
61
61
+
})
62
62
+
63
63
+
return mux
64
64
+
}
+41
cmd/appview/main.go
···
1
1
+
package main
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"net/http"
6
6
+
"os"
7
7
+
8
8
+
"recard.blue/appview/config"
9
9
+
"recard.blue/appview/state"
10
10
+
tlog "recard.blue/log"
11
11
+
)
12
12
+
13
13
+
func main() {
14
14
+
ctx := context.Background()
15
15
+
logger := tlog.New("appview")
16
16
+
ctx = tlog.IntoContext(ctx, logger)
17
17
+
18
18
+
c, err := config.LoadConfig(ctx)
19
19
+
if err != nil {
20
20
+
logger.Error("failed to load config", "error", err)
21
21
+
return
22
22
+
}
23
23
+
24
24
+
state, err := state.Make(ctx, c)
25
25
+
defer func() {
26
26
+
if err := state.Close(); err != nil {
27
27
+
logger.Error("failed to close state", "err", err)
28
28
+
}
29
29
+
}()
30
30
+
31
31
+
if err != nil {
32
32
+
logger.Error("failed to start appview", "err", err)
33
33
+
os.Exit(-1)
34
34
+
}
35
35
+
36
36
+
logger.Info("starting server", "address", c.Core.ListenAddr)
37
37
+
38
38
+
if err := http.ListenAndServe(c.Core.ListenAddr, state.Router()); err != nil {
39
39
+
logger.Error("failed to start appview", "err", err)
40
40
+
}
41
41
+
}
+26
go.mod
···
1
1
+
module recard.blue
2
2
+
3
3
+
go 1.25.5
4
4
+
5
5
+
require (
6
6
+
github.com/charmbracelet/log v0.4.2
7
7
+
github.com/sethvargo/go-envconfig v1.3.0
8
8
+
)
9
9
+
10
10
+
require (
11
11
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
12
12
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
13
13
+
github.com/charmbracelet/lipgloss v1.1.0 // indirect
14
14
+
github.com/charmbracelet/x/ansi v0.8.0 // indirect
15
15
+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
16
16
+
github.com/charmbracelet/x/term v0.2.1 // indirect
17
17
+
github.com/go-logfmt/logfmt v0.6.0 // indirect
18
18
+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
19
19
+
github.com/mattn/go-isatty v0.0.20 // indirect
20
20
+
github.com/mattn/go-runewidth v0.0.16 // indirect
21
21
+
github.com/muesli/termenv v0.16.0 // indirect
22
22
+
github.com/rivo/uniseg v0.4.7 // indirect
23
23
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
24
24
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
25
25
+
golang.org/x/sys v0.30.0 // indirect
26
26
+
)
+46
go.sum
···
1
1
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
2
2
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
3
3
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
4
4
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
5
5
+
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
6
6
+
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
7
7
+
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
8
8
+
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
9
9
+
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
10
10
+
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
11
11
+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
12
12
+
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
13
13
+
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
14
14
+
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
15
15
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
16
16
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17
17
+
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
18
18
+
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
19
19
+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
20
20
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
21
21
+
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
22
22
+
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
23
23
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
24
24
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
25
25
+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
26
26
+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
27
27
+
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
28
28
+
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
29
29
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
30
30
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
31
31
+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
32
32
+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
33
33
+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
34
34
+
github.com/sethvargo/go-envconfig v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U=
35
35
+
github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
36
36
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
37
37
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
38
38
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
39
39
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
40
40
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
41
41
+
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
42
42
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
43
43
+
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
44
44
+
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
45
45
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
46
46
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+65
log/log.go
···
1
1
+
package log
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"log/slog"
6
6
+
"os"
7
7
+
8
8
+
"github.com/charmbracelet/log"
9
9
+
)
10
10
+
11
11
+
func NewHandler(name string) slog.Handler {
12
12
+
return log.NewWithOptions(os.Stderr, log.Options{
13
13
+
ReportTimestamp: true,
14
14
+
Prefix: name,
15
15
+
Level: log.DebugLevel,
16
16
+
})
17
17
+
}
18
18
+
19
19
+
func New(name string) *slog.Logger {
20
20
+
return slog.New(NewHandler(name))
21
21
+
}
22
22
+
23
23
+
func NewContext(ctx context.Context, name string) context.Context {
24
24
+
return IntoContext(ctx, New(name))
25
25
+
}
26
26
+
27
27
+
type ctxKey struct{}
28
28
+
29
29
+
// IntoContext adds a logger to a context. Use FromContext to
30
30
+
// pull the logger out.
31
31
+
func IntoContext(ctx context.Context, l *slog.Logger) context.Context {
32
32
+
return context.WithValue(ctx, ctxKey{}, l)
33
33
+
}
34
34
+
35
35
+
// FromContext returns a logger from a context.Context;
36
36
+
// if the passed context is nil, we return the default slog
37
37
+
// logger.
38
38
+
func FromContext(ctx context.Context) *slog.Logger {
39
39
+
if ctx != nil {
40
40
+
v := ctx.Value(ctxKey{})
41
41
+
if v == nil {
42
42
+
return slog.Default()
43
43
+
}
44
44
+
return v.(*slog.Logger)
45
45
+
}
46
46
+
47
47
+
return slog.Default()
48
48
+
}
49
49
+
50
50
+
// sublogger derives a new logger from an existing one by appending a suffix to its prefix.
51
51
+
func SubLogger(base *slog.Logger, suffix string) *slog.Logger {
52
52
+
// try to get the underlying charmbracelet logger
53
53
+
if cl, ok := base.Handler().(*log.Logger); ok {
54
54
+
prefix := cl.GetPrefix()
55
55
+
if prefix != "" {
56
56
+
prefix = prefix + "/" + suffix
57
57
+
} else {
58
58
+
prefix = suffix
59
59
+
}
60
60
+
return slog.New(NewHandler(prefix))
61
61
+
}
62
62
+
63
63
+
// Fallback: no known handler type
64
64
+
return slog.New(NewHandler(suffix))
65
65
+
}