From ce547fe086f962aafdc13f8e2f3254e5f152e5b3 Mon Sep 17 00:00:00 2001 From: Antoine Grondin Date: Fri, 12 Apr 2024 09:24:22 +0900 Subject: [PATCH 1/2] wip make log queryable and uploadable --- .github/workflows/go.yml | 10 +- .github/workflows/release-dev.yml | 37 + .github/workflows/release.yml | 6 +- .gitignore | 2 + .goreleaser-dev.yaml | 44 + goreleaser.yaml => .goreleaser.yaml | 9 +- .vscode/launch.json | 17 + README.md | 2 +- cmd/humanlog/account.go | 445 + cmd/humanlog/auth.go | 123 + cmd/humanlog/genny.go | 403 + cmd/humanlog/helper.go | 374 + cmd/humanlog/ingest.go | 159 + cmd/humanlog/localhost.go | 161 + cmd/humanlog/log.go | 2 +- cmd/humanlog/machine.go | 79 + cmd/humanlog/main.go | 234 +- cmd/humanlog/organization.go | 355 + cmd/humanlog/query.go | 267 + cmd/humanlog/versions.go | 37 +- docker_compose_handler.go | 10 +- go.mod | 59 +- go.sum | 144 +- internal/localstorage/memory.go | 199 + internal/localstorage/memory_test.go | 202 + internal/localstorage/queryable.go | 26 + internal/localsvc/svc.go | 320 + internal/localsvc/svc_test.go | 152 + internal/pkg/config/config.go | 7 + internal/pkg/iterapi/iter.go | 71 + internal/pkg/model/types.go | 20 - internal/pkg/state/state.go | 20 +- json_handler.go | 11 +- json_handler_test.go | 10 +- logfmt_handler.go | 11 +- pkg/auth/interceptors.go | 146 + pkg/auth/token_source.go | 100 + pkg/retry/retry.go | 114 + pkg/sink/bufsink/sized.go | 22 +- pkg/sink/logsvcsink/bidistream_sink.go | 189 + pkg/sink/logsvcsink/stream_sink.go | 222 + pkg/sink/logsvcsink/unary_sink.go | 206 + pkg/sink/sink.go | 8 +- pkg/sink/stdiosink/stdio.go | 28 +- pkg/sink/teesink/tee.go | 134 + pkg/tui/accounts.go | 204 + pkg/tui/components/keyhandler/keyhandler.go | 43 + pkg/tui/components/keyhandler/main.go | 77 + pkg/tui/components/modal/main.go | 91 + pkg/tui/components/modal/mode_selector.go | 111 + pkg/tui/components/querybar/main.go | 56 + pkg/tui/components/querybar/querybar.go | 74 + pkg/tui/machines.go | 196 + pkg/tui/organizations.go | 203 + pkg/tui/querier.go | 310 + pkg/tui/tui.go | 99 + scanner.go | 14 +- scanner_test.go | 6 +- script/create_version_artifacts.sh | 9 +- time_parse.go | 4 +- vendor/connectrpc.com/connect/.gitignore | 5 + .../connect}/.golangci.yml | 55 +- .../connect}/LICENSE | 2 +- vendor/connectrpc.com/connect/MAINTAINERS.md | 12 + vendor/connectrpc.com/connect/Makefile | 117 + .../connect}/README.md | 89 +- vendor/connectrpc.com/connect/RELEASE.md | 44 + vendor/connectrpc.com/connect/SECURITY.md | 5 + .../connect}/buf.gen.yaml | 2 +- .../connect}/buf.work.yaml | 0 .../connect}/buffer_pool.go | 2 +- .../connect}/client.go | 82 +- .../connect}/client_stream.go | 28 +- .../connect}/code.go | 4 +- vendor/connectrpc.com/connect/codec.go | 259 + .../connect}/compression.go | 15 +- .../connect}/connect.go | 195 +- .../connect/duplex_http_call.go | 468 + vendor/connectrpc.com/connect/envelope.go | 374 + .../connect}/error.go | 143 +- vendor/connectrpc.com/connect/error_writer.go | 179 + .../connect}/handler.go | 147 +- .../connect}/handler_stream.go | 31 +- vendor/connectrpc.com/connect/header.go | 126 + .../connect/idempotency_level.go | 68 + .../connect}/interceptor.go | 2 +- .../connectext/grpc/status/v1/status.pb.go | 29 +- .../connect}/option.go | 211 +- .../connect}/protobuf_util.go | 6 +- .../connect}/protocol.go | 172 +- .../connect}/protocol_connect.go | 661 +- .../connect}/protocol_grpc.go | 603 +- .../connect}/recover.go | 2 +- vendor/connectrpc.com/cors/.gitignore | 4 + vendor/connectrpc.com/cors/.golangci.yml | 54 + vendor/connectrpc.com/cors/LICENSE | 201 + vendor/connectrpc.com/cors/MAINTAINERS.md | 7 + vendor/connectrpc.com/cors/Makefile | 73 + vendor/connectrpc.com/cors/README.md | 85 + vendor/connectrpc.com/cors/SECURITY.md | 5 + vendor/connectrpc.com/cors/cors.go | 66 + .../99designs/go-keychain/.gitignore | 24 + .../99designs/go-keychain/.golangci.yml | 11 + .../99designs/go-keychain/.travis.yml | 20 + .../github.com/99designs/go-keychain/LICENSE | 22 + .../99designs/go-keychain/README.md | 159 + .../99designs/go-keychain/corefoundation.go | 359 + .../99designs/go-keychain/datetime.go | 68 + .../github.com/99designs/go-keychain/ios.go | 22 + .../99designs/go-keychain/keychain.go | 531 + .../github.com/99designs/go-keychain/macos.go | 272 + .../github.com/99designs/go-keychain/util.go | 31 + .../99designs/keyring/.gitattributes | 1 + .../github.com/99designs/keyring/.gitignore | 1 + .../99designs/keyring/.golangci.yml | 29 + vendor/github.com/99designs/keyring/LICENSE | 22 + vendor/github.com/99designs/keyring/README.md | 67 + .../github.com/99designs/keyring/Vagrantfile | 85 + vendor/github.com/99designs/keyring/array.go | 54 + vendor/github.com/99designs/keyring/config.go | 58 + .../99designs/keyring/docker-compose.yml | 7 + vendor/github.com/99designs/keyring/file.go | 180 + .../github.com/99designs/keyring/keychain.go | 301 + vendor/github.com/99designs/keyring/keyctl.go | 327 + .../github.com/99designs/keyring/keyring.go | 134 + .../github.com/99designs/keyring/kwallet.go | 237 + vendor/github.com/99designs/keyring/pass.go | 166 + vendor/github.com/99designs/keyring/prompt.go | 27 + .../99designs/keyring/secretservice.go | 293 + vendor/github.com/99designs/keyring/tilde.go | 22 + .../github.com/99designs/keyring/wincred.go | 98 + .../NimbleMarkets/ntcharts/LICENSE.txt | 21 + .../ntcharts/canvas/buffer/buffer.go | 342 + .../NimbleMarkets/ntcharts/canvas/canvas.go | 514 + .../ntcharts/canvas/graph/graph.go | 737 ++ .../NimbleMarkets/ntcharts/canvas/options.go | 86 + .../ntcharts/canvas/runes/runes.go | 457 + .../ntcharts/canvas/updatehandler.go | 155 + .../ntcharts/linechart/linechart.go | 733 ++ .../ntcharts/linechart/options.go | 103 + .../linechart/timeserieslinechart/options.go | 139 + .../timeserieslinechart.go | 450 + .../timeserieslinechart/updatehandler.go | 77 + .../ntcharts/linechart/updatehandler.go | 361 + .../github.com/atotto/clipboard/.travis.yml | 22 + vendor/github.com/atotto/clipboard/LICENSE | 27 + vendor/github.com/atotto/clipboard/README.md | 48 + .../github.com/atotto/clipboard/clipboard.go | 20 + .../atotto/clipboard/clipboard_darwin.go | 52 + .../atotto/clipboard/clipboard_plan9.go | 42 + .../atotto/clipboard/clipboard_unix.go | 149 + .../atotto/clipboard/clipboard_windows.go | 157 + .../aymanbagabas/go-osc52/v2/LICENSE | 21 + .../aymanbagabas/go-osc52/v2/README.md | 83 + .../aymanbagabas/go-osc52/v2/osc52.go | 305 + .../github.com/bufbuild/connect-go/.gitignore | 5 - .../github.com/bufbuild/connect-go/Makefile | 102 - .../github.com/bufbuild/connect-go/codec.go | 147 - .../bufbuild/connect-go/duplex_http_call.go | 278 - .../bufbuild/connect-go/envelope.go | 273 - .../bufbuild/connect-go/error_writer.go | 168 - .../github.com/bufbuild/connect-go/header.go | 52 - .../bufbuild/connect-go/maxbytes.go | 32 - .../bufbuild/connect-go/maxbytes_go118.go | 32 - vendor/github.com/catppuccin/go/.editorconfig | 34 + vendor/github.com/catppuccin/go/.gitignore | 8 + .../github.com/catppuccin/go/.goreleaser.yaml | 9 + vendor/github.com/catppuccin/go/LICENSE | 21 + vendor/github.com/catppuccin/go/README.md | 43 + vendor/github.com/catppuccin/go/frappe.go | 217 + vendor/github.com/catppuccin/go/latte.go | 217 + vendor/github.com/catppuccin/go/macchiato.go | 217 + vendor/github.com/catppuccin/go/main.go | 69 + vendor/github.com/catppuccin/go/mocha.go | 216 + .../github.com/charmbracelet/bubbles/LICENSE | 21 + .../charmbracelet/bubbles/cursor/cursor.go | 207 + .../bubbles/filepicker/filepicker.go | 523 + .../bubbles/filepicker/hidden_unix.go | 11 + .../bubbles/filepicker/hidden_windows.go | 21 + .../charmbracelet/bubbles/help/help.go | 233 + .../charmbracelet/bubbles/key/key.go | 142 + .../bubbles/paginator/paginator.go | 198 + .../bubbles/runeutil/runeutil.go | 102 + .../charmbracelet/bubbles/table/table.go | 427 + .../textarea/memoization/memoization.go | 123 + .../bubbles/textarea/textarea.go | 1390 +++ .../bubbles/textinput/textinput.go | 882 ++ .../charmbracelet/bubbles/viewport/keymap.go | 48 + .../bubbles/viewport/viewport.go | 405 + .../charmbracelet/bubbletea/.gitattributes | 1 + .../charmbracelet/bubbletea/.gitignore | 22 + .../bubbletea/.golangci-soft.yml | 46 + .../charmbracelet/bubbletea/.golangci.yml | 30 + .../charmbracelet/bubbletea/.goreleaser.yml | 6 + .../charmbracelet/bubbletea/CONTRIBUTING.md | 13 + .../charmbracelet/bubbletea/LICENSE | 21 + .../charmbracelet/bubbletea/README.md | 387 + .../charmbracelet/bubbletea/commands.go | 216 + .../charmbracelet/bubbletea/exec.go | 129 + .../charmbracelet/bubbletea/focus.go | 9 + .../bubbletea/inputreader_other.go | 14 + .../bubbletea/inputreader_windows.go | 107 + .../github.com/charmbracelet/bubbletea/key.go | 715 ++ .../charmbracelet/bubbletea/key_other.go | 13 + .../charmbracelet/bubbletea/key_sequences.go | 131 + .../charmbracelet/bubbletea/key_windows.go | 351 + .../charmbracelet/bubbletea/logging.go | 53 + .../charmbracelet/bubbletea/mouse.go | 308 + .../charmbracelet/bubbletea/nil_renderer.go | 28 + .../charmbracelet/bubbletea/options.go | 249 + .../charmbracelet/bubbletea/renderer.go | 85 + .../charmbracelet/bubbletea/screen.go | 248 + .../charmbracelet/bubbletea/signals_unix.go | 33 + .../bubbletea/signals_windows.go | 10 + .../bubbletea/standard_renderer.go | 755 ++ .../github.com/charmbracelet/bubbletea/tea.go | 788 ++ .../charmbracelet/bubbletea/tea_init.go | 22 + .../github.com/charmbracelet/bubbletea/tty.go | 136 + .../charmbracelet/bubbletea/tty_unix.go | 49 + .../charmbracelet/bubbletea/tty_windows.go | 68 + .../charmbracelet/huh/.gitattributes | 2 + .../github.com/charmbracelet/huh/.gitignore | 21 + .../charmbracelet/huh/.golangci-soft.yml | 47 + .../charmbracelet/huh/.golangci.yml | 29 + .../charmbracelet/huh/CONTRIBUTING.md | 3 + vendor/github.com/charmbracelet/huh/LICENSE | 21 + vendor/github.com/charmbracelet/huh/Makefile | 17 + vendor/github.com/charmbracelet/huh/README.md | 431 + .../github.com/charmbracelet/huh/SECURITY.md | 5 + .../huh/accessibility/accessibility.go | 94 + vendor/github.com/charmbracelet/huh/clamp.go | 22 + .../charmbracelet/huh/field_confirm.go | 285 + .../charmbracelet/huh/field_filepicker.go | 407 + .../charmbracelet/huh/field_input.go | 356 + .../charmbracelet/huh/field_multiselect.go | 565 + .../charmbracelet/huh/field_note.go | 274 + .../charmbracelet/huh/field_select.go | 544 + .../charmbracelet/huh/field_text.go | 383 + vendor/github.com/charmbracelet/huh/form.go | 589 + vendor/github.com/charmbracelet/huh/group.go | 304 + vendor/github.com/charmbracelet/huh/keymap.go | 178 + vendor/github.com/charmbracelet/huh/option.go | 38 + vendor/github.com/charmbracelet/huh/run.go | 8 + vendor/github.com/charmbracelet/huh/theme.go | 296 + .../github.com/charmbracelet/huh/validate.go | 61 + .../charmbracelet/lipgloss/.gitignore | 1 + .../charmbracelet/lipgloss/.golangci-soft.yml | 46 + .../charmbracelet/lipgloss/.golangci.yml | 30 + .../charmbracelet/lipgloss/.goreleaser.yml | 5 + .../github.com/charmbracelet/lipgloss/LICENSE | 21 + .../charmbracelet/lipgloss/README.md | 751 ++ .../charmbracelet/lipgloss/align.go | 83 + .../charmbracelet/lipgloss/ansi_unix.go | 7 + .../charmbracelet/lipgloss/ansi_windows.go | 22 + .../charmbracelet/lipgloss/borders.go | 443 + .../charmbracelet/lipgloss/color.go | 172 + .../github.com/charmbracelet/lipgloss/get.go | 542 + .../github.com/charmbracelet/lipgloss/join.go | 175 + .../charmbracelet/lipgloss/position.go | 154 + .../charmbracelet/lipgloss/renderer.go | 181 + .../charmbracelet/lipgloss/runes.go | 43 + .../github.com/charmbracelet/lipgloss/set.go | 799 ++ .../github.com/charmbracelet/lipgloss/size.go | 41 + .../charmbracelet/lipgloss/style.go | 585 + .../charmbracelet/lipgloss/tree/children.go | 98 + .../charmbracelet/lipgloss/tree/enumerator.go | 74 + .../charmbracelet/lipgloss/tree/renderer.go | 138 + .../charmbracelet/lipgloss/tree/tree.go | 338 + .../charmbracelet/lipgloss/unset.go | 331 + .../charmbracelet/lipgloss/whitespace.go | 83 + .../github.com/charmbracelet/x/ansi/LICENSE | 21 + .../github.com/charmbracelet/x/ansi/ansi.go | 11 + .../github.com/charmbracelet/x/ansi/ascii.go | 8 + .../charmbracelet/x/ansi/background.go | 61 + vendor/github.com/charmbracelet/x/ansi/c0.go | 72 + vendor/github.com/charmbracelet/x/ansi/c1.go | 72 + .../charmbracelet/x/ansi/clipboard.go | 75 + .../github.com/charmbracelet/x/ansi/color.go | 196 + vendor/github.com/charmbracelet/x/ansi/csi.go | 141 + .../github.com/charmbracelet/x/ansi/ctrl.go | 17 + .../github.com/charmbracelet/x/ansi/cursor.go | 235 + vendor/github.com/charmbracelet/x/ansi/dcs.go | 148 + vendor/github.com/charmbracelet/x/ansi/doc.go | 7 + .../charmbracelet/x/ansi/hyperlink.go | 28 + .../github.com/charmbracelet/x/ansi/kitty.go | 90 + .../github.com/charmbracelet/x/ansi/mode.go | 179 + vendor/github.com/charmbracelet/x/ansi/osc.go | 69 + .../github.com/charmbracelet/x/ansi/params.go | 45 + .../github.com/charmbracelet/x/ansi/parser.go | 363 + .../charmbracelet/x/ansi/parser/const.go | 78 + .../charmbracelet/x/ansi/parser/seq.go | 136 + .../x/ansi/parser/transition_table.go | 273 + .../charmbracelet/x/ansi/parser_decode.go | 423 + .../charmbracelet/x/ansi/parser_sync.go | 26 + .../charmbracelet/x/ansi/passthrough.go | 63 + .../github.com/charmbracelet/x/ansi/screen.go | 126 + .../charmbracelet/x/ansi/sequence.go | 199 + .../github.com/charmbracelet/x/ansi/style.go | 496 + .../charmbracelet/x/ansi/termcap.go | 31 + .../github.com/charmbracelet/x/ansi/title.go | 32 + .../charmbracelet/x/ansi/truncate.go | 107 + .../github.com/charmbracelet/x/ansi/util.go | 29 + .../github.com/charmbracelet/x/ansi/width.go | 95 + .../github.com/charmbracelet/x/ansi/wrap.go | 395 + .../github.com/charmbracelet/x/ansi/xterm.go | 50 + .../charmbracelet/x/exp/strings/LICENSE | 21 + .../charmbracelet/x/exp/strings/join.go | 133 + .../github.com/charmbracelet/x/term/LICENSE | 21 + .../github.com/charmbracelet/x/term/term.go | 49 + .../charmbracelet/x/term/term_other.go | 39 + .../charmbracelet/x/term/term_unix.go | 96 + .../charmbracelet/x/term/term_unix_bsd.go | 11 + .../charmbracelet/x/term/term_unix_other.go | 11 + .../charmbracelet/x/term/term_windows.go | 86 + .../charmbracelet/x/term/terminal.go | 12 + .../github.com/charmbracelet/x/term/util.go | 47 + .../danieljoos/wincred/.gitattributes | 1 + .../github.com/danieljoos/wincred/.gitignore | 25 + vendor/github.com/danieljoos/wincred/LICENSE | 21 + .../github.com/danieljoos/wincred/README.md | 145 + .../danieljoos/wincred/conversion.go | 116 + .../wincred/conversion_unsupported.go | 11 + vendor/github.com/danieljoos/wincred/sys.go | 147 + .../danieljoos/wincred/sys_unsupported.go | 36 + vendor/github.com/danieljoos/wincred/types.go | 69 + .../github.com/danieljoos/wincred/wincred.go | 111 + .../github.com/dustin/go-humanize/.travis.yml | 21 + vendor/github.com/dustin/go-humanize/LICENSE | 21 + .../dustin/go-humanize/README.markdown | 124 + vendor/github.com/dustin/go-humanize/big.go | 31 + .../github.com/dustin/go-humanize/bigbytes.go | 189 + vendor/github.com/dustin/go-humanize/bytes.go | 143 + vendor/github.com/dustin/go-humanize/comma.go | 116 + .../github.com/dustin/go-humanize/commaf.go | 41 + vendor/github.com/dustin/go-humanize/ftoa.go | 49 + .../github.com/dustin/go-humanize/humanize.go | 8 + .../github.com/dustin/go-humanize/number.go | 192 + .../github.com/dustin/go-humanize/ordinals.go | 25 + vendor/github.com/dustin/go-humanize/si.go | 127 + vendor/github.com/dustin/go-humanize/times.go | 117 + .../dvsekhvalnov/jose2go/.gitignore | 23 + .../github.com/dvsekhvalnov/jose2go/LICENSE | 21 + .../github.com/dvsekhvalnov/jose2go/README.md | 938 ++ .../dvsekhvalnov/jose2go/aes/ecb.go | 68 + .../dvsekhvalnov/jose2go/aes/key_wrap.go | 113 + .../dvsekhvalnov/jose2go/aes_cbc_hmac.go | 112 + .../dvsekhvalnov/jose2go/aes_gcm.go | 98 + .../dvsekhvalnov/jose2go/aes_gcm_kw.go | 128 + .../github.com/dvsekhvalnov/jose2go/aeskw.go | 64 + .../dvsekhvalnov/jose2go/arrays/arrays.go | 116 + .../jose2go/base64url/base64url.go | 31 + .../dvsekhvalnov/jose2go/compact/compact.go | 33 + .../dvsekhvalnov/jose2go/deflate.go | 39 + .../github.com/dvsekhvalnov/jose2go/direct.go | 39 + .../github.com/dvsekhvalnov/jose2go/ecdh.go | 157 + .../dvsekhvalnov/jose2go/ecdh_aeskw.go | 42 + .../dvsekhvalnov/jose2go/ecdsa_using_sha.go | 76 + .../github.com/dvsekhvalnov/jose2go/hmac.go | 13 + .../dvsekhvalnov/jose2go/hmac_using_sha.go | 46 + .../github.com/dvsekhvalnov/jose2go/jose.go | 424 + .../jose2go/kdf/nist_sp800_56a.go | 43 + .../dvsekhvalnov/jose2go/kdf/pbkdf2.go | 63 + .../dvsekhvalnov/jose2go/keys/ecc/ec_cert.pem | 14 + .../jose2go/keys/ecc/ec_private.key | 5 + .../jose2go/keys/ecc/ec_private.pem | 5 + .../jose2go/keys/ecc/ec_public.key | 4 + .../dvsekhvalnov/jose2go/keys/ecc/ecc.go | 88 + .../dvsekhvalnov/jose2go/padding/align.go | 23 + .../dvsekhvalnov/jose2go/padding/pkcs7.go | 38 + .../dvsekhvalnov/jose2go/pbse2_hmac_aeskw.go | 103 + .../dvsekhvalnov/jose2go/plaintext.go | 38 + .../dvsekhvalnov/jose2go/rsa_oaep.go | 57 + .../dvsekhvalnov/jose2go/rsa_pkcs1v15.go | 41 + .../dvsekhvalnov/jose2go/rsa_using_sha.go | 50 + .../dvsekhvalnov/jose2go/rsapss_using_sha.go | 43 + vendor/github.com/dvsekhvalnov/jose2go/sha.go | 24 + .../github.com/erikgeiser/coninput/.gitignore | 15 + .../erikgeiser/coninput/.golangci.yml | 24 + vendor/github.com/erikgeiser/coninput/LICENSE | 21 + .../github.com/erikgeiser/coninput/README.md | 2 + .../erikgeiser/coninput/keycodes.go | 205 + vendor/github.com/erikgeiser/coninput/mode.go | 82 + vendor/github.com/erikgeiser/coninput/read.go | 154 + .../github.com/erikgeiser/coninput/records.go | 486 + vendor/github.com/fatih/color/README.md | 14 +- vendor/github.com/fatih/color/color.go | 98 +- .../github.com/fatih/color/color_windows.go | 19 + vendor/github.com/fatih/color/doc.go | 137 +- vendor/github.com/godbus/dbus/.travis.yml | 46 + vendor/github.com/godbus/dbus/CONTRIBUTING.md | 50 + vendor/github.com/godbus/dbus/LICENSE | 25 + vendor/github.com/godbus/dbus/MAINTAINERS | 3 + vendor/github.com/godbus/dbus/README.markdown | 44 + vendor/github.com/godbus/dbus/auth.go | 252 + .../github.com/godbus/dbus/auth_anonymous.go | 16 + .../github.com/godbus/dbus/auth_external.go | 26 + vendor/github.com/godbus/dbus/auth_sha1.go | 102 + vendor/github.com/godbus/dbus/call.go | 60 + vendor/github.com/godbus/dbus/conn.go | 865 ++ vendor/github.com/godbus/dbus/conn_darwin.go | 37 + vendor/github.com/godbus/dbus/conn_other.go | 93 + vendor/github.com/godbus/dbus/conn_unix.go | 17 + vendor/github.com/godbus/dbus/conn_windows.go | 15 + vendor/github.com/godbus/dbus/dbus.go | 427 + vendor/github.com/godbus/dbus/decoder.go | 286 + .../github.com/godbus/dbus/default_handler.go | 328 + vendor/github.com/godbus/dbus/doc.go | 69 + vendor/github.com/godbus/dbus/encoder.go | 210 + vendor/github.com/godbus/dbus/export.go | 412 + vendor/github.com/godbus/dbus/homedir.go | 28 + .../github.com/godbus/dbus/homedir_dynamic.go | 15 + .../github.com/godbus/dbus/homedir_static.go | 45 + vendor/github.com/godbus/dbus/match.go | 62 + vendor/github.com/godbus/dbus/message.go | 353 + vendor/github.com/godbus/dbus/object.go | 211 + .../godbus/dbus/server_interfaces.go | 107 + vendor/github.com/godbus/dbus/sig.go | 259 + .../godbus/dbus/transport_darwin.go | 6 + .../godbus/dbus/transport_generic.go | 50 + .../godbus/dbus/transport_nonce_tcp.go | 39 + .../github.com/godbus/dbus/transport_tcp.go | 41 + .../github.com/godbus/dbus/transport_unix.go | 214 + .../dbus/transport_unixcred_dragonfly.go | 95 + .../godbus/dbus/transport_unixcred_freebsd.go | 91 + .../godbus/dbus/transport_unixcred_linux.go | 25 + .../godbus/dbus/transport_unixcred_openbsd.go | 14 + vendor/github.com/godbus/dbus/variant.go | 144 + .../github.com/godbus/dbus/variant_lexer.go | 284 + .../github.com/godbus/dbus/variant_parser.go | 817 ++ vendor/github.com/google/uuid/CHANGELOG.md | 41 + vendor/github.com/google/uuid/CONTRIBUTING.md | 26 + vendor/github.com/google/uuid/CONTRIBUTORS | 9 + vendor/github.com/google/uuid/LICENSE | 27 + vendor/github.com/google/uuid/README.md | 21 + vendor/github.com/google/uuid/dce.go | 80 + vendor/github.com/google/uuid/doc.go | 12 + vendor/github.com/google/uuid/hash.go | 59 + vendor/github.com/google/uuid/marshal.go | 38 + vendor/github.com/google/uuid/node.go | 90 + vendor/github.com/google/uuid/node_js.go | 12 + vendor/github.com/google/uuid/node_net.go | 33 + vendor/github.com/google/uuid/null.go | 118 + vendor/github.com/google/uuid/sql.go | 59 + vendor/github.com/google/uuid/time.go | 134 + vendor/github.com/google/uuid/util.go | 43 + vendor/github.com/google/uuid/uuid.go | 365 + vendor/github.com/google/uuid/version1.go | 44 + vendor/github.com/google/uuid/version4.go | 76 + vendor/github.com/google/uuid/version6.go | 56 + vendor/github.com/google/uuid/version7.go | 104 + .../github.com/gsterjov/go-libsecret/LICENSE | 22 + .../gsterjov/go-libsecret/collection.go | 124 + .../github.com/gsterjov/go-libsecret/item.go | 77 + .../gsterjov/go-libsecret/prompt.go | 55 + .../gsterjov/go-libsecret/secret.go | 21 + .../gsterjov/go-libsecret/service.go | 141 + .../gsterjov/go-libsecret/session.go | 22 + .../humanlogio/api/go/pkg/lang/parse.go | 7 + .../v1/accountv1connect/service.connect.go | 113 + .../api/go/svc/account/v1/service.pb.go | 290 + .../auth/v1/authv1connect/service.connect.go | 172 + .../api/go/svc/auth/v1/service.pb.go | 676 ++ .../v1/cliupdatev1connect/service.connect.go | 65 +- .../api/go/svc/cliupdate/v1/service.pb.go | 72 +- .../v1/ingestv1connect/service.connect.go | 202 + .../api/go/svc/ingest/v1/service.pb.go | 651 ++ .../v1/localhostv1connect/service.connect.go | 112 + .../api/go/svc/localhost/v1/service.pb.go | 226 + .../organizationv1connect/service.connect.go | 234 + .../api/go/svc/organization/v1/service.pb.go | 823 ++ .../v1/queryv1connect/service.connect.go | 142 + .../api/go/svc/query/v1/service.pb.go | 445 + .../api/go/svc/token/v1/service.pb.go | 656 ++ .../v1/tokenv1connect/service.connect.go | 203 + .../api/go/svc/user/v1/service.pb.go | 596 ++ .../user/v1/userv1connect/service.connect.go | 202 + .../humanlogio/api/go/types/v1/account.pb.go | 28 +- .../api/go/types/v1/account_token.pb.go | 262 + .../humanlogio/api/go/types/v1/cursor.pb.go | 28 +- .../humanlogio/api/go/types/v1/logevent.pb.go | 372 + .../humanlogio/api/go/types/v1/logquery.pb.go | 153 + .../humanlogio/api/go/types/v1/machine.pb.go | 28 +- .../humanlogio/api/go/types/v1/meta.pb.go | 50 +- .../api/go/types/v1/organization.pb.go | 161 + .../api/go/types/v1/release_channel.pb.go | 28 +- .../humanlogio/api/go/types/v1/session.pb.go | 206 + .../humanlogio/api/go/types/v1/user.pb.go | 201 + .../api/go/types/v1/user_token.pb.go | 147 + .../humanlogio/api/go/types/v1/version.pb.go | 28 +- .../api/go/types/v1/version_artifact.pb.go | 28 +- .../lrstanley/bubblezone/.editorconfig | 53 + .../lrstanley/bubblezone/.gitignore | 12 + .../lrstanley/bubblezone/.golangci.yml | 228 + .../github.com/lrstanley/bubblezone/LICENSE | 21 + .../github.com/lrstanley/bubblezone/Makefile | 24 + .../github.com/lrstanley/bubblezone/README.md | 350 + .../lrstanley/bubblezone/manager.go | 248 + .../lrstanley/bubblezone/manager_global.go | 156 + .../lrstanley/bubblezone/messages.go | 70 + .../lrstanley/bubblezone/scanner.go | 160 + .../lrstanley/bubblezone/zoneinfo.go | 63 + .../lucasb-eyer/go-colorful/.gitignore | 101 + .../lucasb-eyer/go-colorful/CHANGELOG.md | 42 + .../lucasb-eyer/go-colorful/LICENSE | 7 + .../lucasb-eyer/go-colorful/README.md | 482 + .../lucasb-eyer/go-colorful/colorgens.go | 55 + .../lucasb-eyer/go-colorful/colors.go | 979 ++ .../go-colorful/happy_palettegen.go | 25 + .../lucasb-eyer/go-colorful/hexcolor.go | 67 + .../go-colorful/hsluv-snapshot-rev4.json | 1 + .../lucasb-eyer/go-colorful/hsluv.go | 207 + .../go-colorful/soft_palettegen.go | 185 + .../go-colorful/warm_palettegen.go | 25 + .../github.com/matoous/go-nanoid/.gitignore | 1 + .../matoous/go-nanoid/.goreleaser.yml | 28 + vendor/github.com/matoous/go-nanoid/LICENSE | 21 + vendor/github.com/matoous/go-nanoid/Makefile | 16 + vendor/github.com/matoous/go-nanoid/README.md | 61 + .../github.com/matoous/go-nanoid/gonanoid.go | 131 + .../github.com/mattn/go-isatty/isatty_bsd.go | 5 +- .../mattn/go-isatty/isatty_others.go | 5 +- .../mattn/go-isatty/isatty_tcgets.go | 3 +- .../mattn/go-localereader/README.md | 23 + .../mattn/go-localereader/localereader.go | 19 + .../go-localereader/localereader_unix.go | 11 + .../go-localereader/localereader_windows.go | 85 + vendor/github.com/mattn/go-runewidth/LICENSE | 21 + .../github.com/mattn/go-runewidth/README.md | 27 + .../mattn/go-runewidth/runewidth.go | 358 + .../mattn/go-runewidth/runewidth_appengine.go | 9 + .../mattn/go-runewidth/runewidth_js.go | 9 + .../mattn/go-runewidth/runewidth_posix.go | 81 + .../mattn/go-runewidth/runewidth_table.go | 450 + .../mattn/go-runewidth/runewidth_windows.go | 28 + vendor/github.com/mtibben/percent/LICENSE | 21 + vendor/github.com/mtibben/percent/README.md | 5 + vendor/github.com/mtibben/percent/percent.go | 64 + vendor/github.com/muesli/ansi/.gitignore | 15 + vendor/github.com/muesli/ansi/.golangci.yml | 27 + vendor/github.com/muesli/ansi/LICENSE | 21 + vendor/github.com/muesli/ansi/README.md | 31 + vendor/github.com/muesli/ansi/ansi.go | 7 + vendor/github.com/muesli/ansi/buffer.go | 40 + .../muesli/ansi/compressor/writer.go | 132 + vendor/github.com/muesli/ansi/writer.go | 76 + .../github.com/muesli/cancelreader/.gitignore | 15 + .../muesli/cancelreader/.golangci-soft.yml | 47 + .../muesli/cancelreader/.golangci.yml | 29 + vendor/github.com/muesli/cancelreader/LICENSE | 21 + .../github.com/muesli/cancelreader/README.md | 64 + .../muesli/cancelreader/cancelreader.go | 93 + .../muesli/cancelreader/cancelreader_bsd.go | 146 + .../cancelreader/cancelreader_default.go | 12 + .../muesli/cancelreader/cancelreader_linux.go | 154 + .../cancelreader/cancelreader_select.go | 136 + .../muesli/cancelreader/cancelreader_unix.go | 18 + .../cancelreader/cancelreader_windows.go | 244 + vendor/github.com/muesli/termenv/.gitignore | 15 + .../muesli/termenv/.golangci-soft.yml | 47 + .../github.com/muesli/termenv/.golangci.yml | 29 + vendor/github.com/muesli/termenv/LICENSE | 21 + vendor/github.com/muesli/termenv/README.md | 431 + .../github.com/muesli/termenv/ansi_compat.md | 65 + .../github.com/muesli/termenv/ansicolors.go | 281 + vendor/github.com/muesli/termenv/color.go | 204 + .../muesli/termenv/constants_linux.go | 8 + .../muesli/termenv/constants_solaris.go | 8 + .../muesli/termenv/constants_unix.go | 13 + vendor/github.com/muesli/termenv/copy.go | 37 + vendor/github.com/muesli/termenv/hyperlink.go | 11 + .../github.com/muesli/termenv/notification.go | 11 + vendor/github.com/muesli/termenv/output.go | 197 + vendor/github.com/muesli/termenv/profile.go | 97 + vendor/github.com/muesli/termenv/screen.go | 590 + vendor/github.com/muesli/termenv/style.go | 126 + .../muesli/termenv/templatehelper.go | 86 + vendor/github.com/muesli/termenv/termenv.go | 114 + .../muesli/termenv/termenv_other.go | 30 + .../muesli/termenv/termenv_posix.go | 17 + .../muesli/termenv/termenv_solaris.go | 22 + .../github.com/muesli/termenv/termenv_unix.go | 293 + .../muesli/termenv/termenv_windows.go | 139 + vendor/github.com/pkg/browser/LICENSE | 23 + vendor/github.com/pkg/browser/README.md | 55 + vendor/github.com/pkg/browser/browser.go | 57 + .../github.com/pkg/browser/browser_darwin.go | 5 + .../github.com/pkg/browser/browser_freebsd.go | 14 + .../github.com/pkg/browser/browser_linux.go | 21 + .../github.com/pkg/browser/browser_netbsd.go | 14 + .../github.com/pkg/browser/browser_openbsd.go | 14 + .../pkg/browser/browser_unsupported.go | 12 + .../github.com/pkg/browser/browser_windows.go | 7 + vendor/github.com/rivo/uniseg/LICENSE.txt | 21 + vendor/github.com/rivo/uniseg/README.md | 137 + vendor/github.com/rivo/uniseg/doc.go | 108 + .../github.com/rivo/uniseg/eastasianwidth.go | 2588 +++++ .../rivo/uniseg/emojipresentation.go | 295 + .../github.com/rivo/uniseg/gen_breaktest.go | 215 + .../github.com/rivo/uniseg/gen_properties.go | 261 + vendor/github.com/rivo/uniseg/grapheme.go | 331 + .../rivo/uniseg/graphemeproperties.go | 1915 ++++ .../github.com/rivo/uniseg/graphemerules.go | 176 + vendor/github.com/rivo/uniseg/line.go | 134 + .../github.com/rivo/uniseg/lineproperties.go | 3554 +++++++ vendor/github.com/rivo/uniseg/linerules.go | 626 ++ vendor/github.com/rivo/uniseg/properties.go | 208 + vendor/github.com/rivo/uniseg/sentence.go | 90 + .../rivo/uniseg/sentenceproperties.go | 2845 +++++ .../github.com/rivo/uniseg/sentencerules.go | 276 + vendor/github.com/rivo/uniseg/step.go | 242 + vendor/github.com/rivo/uniseg/width.go | 61 + vendor/github.com/rivo/uniseg/word.go | 89 + .../github.com/rivo/uniseg/wordproperties.go | 1883 ++++ vendor/github.com/rivo/uniseg/wordrules.go | 282 + vendor/github.com/rs/cors/LICENSE | 19 + vendor/github.com/rs/cors/README.md | 125 + vendor/github.com/rs/cors/cors.go | 502 + .../github.com/rs/cors/internal/sortedset.go | 113 + vendor/github.com/rs/cors/utils.go | 34 + .../testify/assert/assertion_compare.go | 36 +- .../testify/assert/assertion_format.go | 216 +- .../testify/assert/assertion_forward.go | 432 +- .../testify/assert/assertion_order.go | 24 +- .../stretchr/testify/assert/assertions.go | 384 +- .../github.com/stretchr/testify/assert/doc.go | 43 +- .../testify/assert/http_assertions.go | 12 +- .../stretchr/testify/require/doc.go | 23 +- .../stretchr/testify/require/require.go | 444 +- .../testify/require/require_forward.go | 432 +- vendor/github.com/urfave/cli/.gitignore | 2 + vendor/github.com/urfave/cli/LICENSE | 2 +- vendor/github.com/urfave/cli/README.md | 49 +- vendor/github.com/urfave/cli/appveyor.yml | 28 - vendor/github.com/urfave/cli/command.go | 8 +- vendor/golang.org/x/exp/LICENSE | 27 + vendor/golang.org/x/exp/PATENTS | 22 + vendor/golang.org/x/exp/rand/exp.go | 221 + vendor/golang.org/x/exp/rand/normal.go | 156 + vendor/golang.org/x/exp/rand/rand.go | 372 + vendor/golang.org/x/exp/rand/rng.go | 91 + vendor/golang.org/x/exp/rand/zipf.go | 77 + vendor/golang.org/x/net/LICENSE | 27 + vendor/golang.org/x/net/PATENTS | 22 + vendor/golang.org/x/net/http/httpguts/guts.go | 50 + .../golang.org/x/net/http/httpguts/httplex.go | 352 + vendor/golang.org/x/net/http2/.gitignore | 2 + vendor/golang.org/x/net/http2/ascii.go | 53 + vendor/golang.org/x/net/http2/ciphers.go | 641 ++ .../x/net/http2/client_conn_pool.go | 311 + vendor/golang.org/x/net/http2/databuffer.go | 149 + vendor/golang.org/x/net/http2/errors.go | 145 + vendor/golang.org/x/net/http2/flow.go | 120 + vendor/golang.org/x/net/http2/frame.go | 1688 +++ vendor/golang.org/x/net/http2/gotrack.go | 170 + vendor/golang.org/x/net/http2/h2c/h2c.go | 240 + vendor/golang.org/x/net/http2/headermap.go | 105 + vendor/golang.org/x/net/http2/hpack/encode.go | 245 + vendor/golang.org/x/net/http2/hpack/hpack.go | 523 + .../golang.org/x/net/http2/hpack/huffman.go | 226 + .../x/net/http2/hpack/static_table.go | 188 + vendor/golang.org/x/net/http2/hpack/tables.go | 403 + vendor/golang.org/x/net/http2/http2.go | 385 + vendor/golang.org/x/net/http2/pipe.go | 184 + vendor/golang.org/x/net/http2/server.go | 3281 ++++++ vendor/golang.org/x/net/http2/testsync.go | 331 + vendor/golang.org/x/net/http2/transport.go | 3419 ++++++ vendor/golang.org/x/net/http2/write.go | 370 + vendor/golang.org/x/net/http2/writesched.go | 251 + .../x/net/http2/writesched_priority.go | 451 + .../x/net/http2/writesched_random.go | 77 + .../x/net/http2/writesched_roundrobin.go | 119 + vendor/golang.org/x/net/idna/go118.go | 13 + vendor/golang.org/x/net/idna/idna10.0.0.go | 769 ++ vendor/golang.org/x/net/idna/idna9.0.0.go | 717 ++ vendor/golang.org/x/net/idna/pre_go118.go | 11 + vendor/golang.org/x/net/idna/punycode.go | 217 + vendor/golang.org/x/net/idna/tables10.0.0.go | 4559 ++++++++ vendor/golang.org/x/net/idna/tables11.0.0.go | 4653 ++++++++ vendor/golang.org/x/net/idna/tables12.0.0.go | 4733 ++++++++ vendor/golang.org/x/net/idna/tables13.0.0.go | 4959 +++++++++ vendor/golang.org/x/net/idna/tables15.0.0.go | 5144 +++++++++ vendor/golang.org/x/net/idna/tables9.0.0.go | 4486 ++++++++ vendor/golang.org/x/net/idna/trie.go | 51 + vendor/golang.org/x/net/idna/trie12.0.0.go | 30 + vendor/golang.org/x/net/idna/trie13.0.0.go | 30 + vendor/golang.org/x/net/idna/trieval.go | 119 + vendor/golang.org/x/sync/LICENSE | 27 + vendor/golang.org/x/sync/PATENTS | 22 + vendor/golang.org/x/sync/errgroup/errgroup.go | 135 + vendor/golang.org/x/sync/errgroup/go120.go | 13 + .../golang.org/x/sync/errgroup/pre_go120.go | 14 + vendor/golang.org/x/sys/LICENSE | 4 +- vendor/golang.org/x/sys/plan9/asm.s | 8 + vendor/golang.org/x/sys/plan9/asm_plan9_386.s | 30 + .../golang.org/x/sys/plan9/asm_plan9_amd64.s | 30 + vendor/golang.org/x/sys/plan9/asm_plan9_arm.s | 25 + vendor/golang.org/x/sys/plan9/const_plan9.go | 70 + vendor/golang.org/x/sys/plan9/dir_plan9.go | 212 + vendor/golang.org/x/sys/plan9/env_plan9.go | 31 + vendor/golang.org/x/sys/plan9/errors_plan9.go | 50 + vendor/golang.org/x/sys/plan9/mkall.sh | 150 + vendor/golang.org/x/sys/plan9/mkerrors.sh | 246 + .../golang.org/x/sys/plan9/mksysnum_plan9.sh | 23 + .../golang.org/x/sys/plan9/pwd_go15_plan9.go | 21 + vendor/golang.org/x/sys/plan9/pwd_plan9.go | 23 + vendor/golang.org/x/sys/plan9/race.go | 30 + vendor/golang.org/x/sys/plan9/race0.go | 25 + vendor/golang.org/x/sys/plan9/str.go | 22 + vendor/golang.org/x/sys/plan9/syscall.go | 109 + .../golang.org/x/sys/plan9/syscall_plan9.go | 361 + .../x/sys/plan9/zsyscall_plan9_386.go | 284 + .../x/sys/plan9/zsyscall_plan9_amd64.go | 284 + .../x/sys/plan9/zsyscall_plan9_arm.go | 284 + .../golang.org/x/sys/plan9/zsysnum_plan9.go | 49 + vendor/golang.org/x/sys/unix/aliases.go | 4 +- vendor/golang.org/x/sys/unix/asm_aix_ppc64.s | 1 - vendor/golang.org/x/sys/unix/asm_bsd_386.s | 2 - vendor/golang.org/x/sys/unix/asm_bsd_amd64.s | 2 - vendor/golang.org/x/sys/unix/asm_bsd_arm.s | 2 - vendor/golang.org/x/sys/unix/asm_bsd_arm64.s | 2 - vendor/golang.org/x/sys/unix/asm_bsd_ppc64.s | 2 - .../golang.org/x/sys/unix/asm_bsd_riscv64.s | 2 - vendor/golang.org/x/sys/unix/asm_linux_386.s | 1 - .../golang.org/x/sys/unix/asm_linux_amd64.s | 1 - vendor/golang.org/x/sys/unix/asm_linux_arm.s | 1 - .../golang.org/x/sys/unix/asm_linux_arm64.s | 3 - .../golang.org/x/sys/unix/asm_linux_loong64.s | 3 - .../golang.org/x/sys/unix/asm_linux_mips64x.s | 3 - .../golang.org/x/sys/unix/asm_linux_mipsx.s | 3 - .../golang.org/x/sys/unix/asm_linux_ppc64x.s | 3 - .../golang.org/x/sys/unix/asm_linux_riscv64.s | 2 - .../golang.org/x/sys/unix/asm_linux_s390x.s | 3 - .../x/sys/unix/asm_openbsd_mips64.s | 1 - .../golang.org/x/sys/unix/asm_solaris_amd64.s | 1 - vendor/golang.org/x/sys/unix/asm_zos_s390x.s | 668 +- vendor/golang.org/x/sys/unix/bpxsvc_zos.go | 657 ++ vendor/golang.org/x/sys/unix/bpxsvc_zos.s | 192 + vendor/golang.org/x/sys/unix/cap_freebsd.go | 1 - vendor/golang.org/x/sys/unix/constants.go | 1 - vendor/golang.org/x/sys/unix/dev_aix_ppc.go | 1 - vendor/golang.org/x/sys/unix/dev_aix_ppc64.go | 1 - vendor/golang.org/x/sys/unix/dev_zos.go | 1 - vendor/golang.org/x/sys/unix/dirent.go | 1 - vendor/golang.org/x/sys/unix/endian_big.go | 1 - vendor/golang.org/x/sys/unix/endian_little.go | 1 - vendor/golang.org/x/sys/unix/env_unix.go | 1 - vendor/golang.org/x/sys/unix/epoll_zos.go | 221 - vendor/golang.org/x/sys/unix/fcntl.go | 3 +- .../x/sys/unix/fcntl_linux_32bit.go | 1 - vendor/golang.org/x/sys/unix/fdset.go | 1 - vendor/golang.org/x/sys/unix/fstatfs_zos.go | 164 - vendor/golang.org/x/sys/unix/gccgo.go | 3 +- vendor/golang.org/x/sys/unix/gccgo_c.c | 3 +- .../x/sys/unix/gccgo_linux_amd64.go | 1 - vendor/golang.org/x/sys/unix/ifreq_linux.go | 1 - vendor/golang.org/x/sys/unix/ioctl_linux.go | 5 + vendor/golang.org/x/sys/unix/ioctl_signed.go | 69 + .../sys/unix/{ioctl.go => ioctl_unsigned.go} | 20 +- vendor/golang.org/x/sys/unix/ioctl_zos.go | 21 +- vendor/golang.org/x/sys/unix/mkall.sh | 6 +- vendor/golang.org/x/sys/unix/mkerrors.sh | 61 +- vendor/golang.org/x/sys/unix/mmap_nomremap.go | 13 + vendor/golang.org/x/sys/unix/mremap.go | 57 + vendor/golang.org/x/sys/unix/pagesize_unix.go | 3 +- .../golang.org/x/sys/unix/pledge_openbsd.go | 92 +- vendor/golang.org/x/sys/unix/ptrace_darwin.go | 1 - vendor/golang.org/x/sys/unix/ptrace_ios.go | 1 - vendor/golang.org/x/sys/unix/race.go | 1 - vendor/golang.org/x/sys/unix/race0.go | 1 - .../x/sys/unix/readdirent_getdents.go | 1 - .../x/sys/unix/readdirent_getdirentries.go | 3 +- vendor/golang.org/x/sys/unix/sockcmsg_unix.go | 15 +- .../x/sys/unix/sockcmsg_unix_other.go | 1 - vendor/golang.org/x/sys/unix/sockcmsg_zos.go | 58 + .../golang.org/x/sys/unix/symaddr_zos_s390x.s | 75 + vendor/golang.org/x/sys/unix/syscall.go | 1 - vendor/golang.org/x/sys/unix/syscall_aix.go | 28 +- .../golang.org/x/sys/unix/syscall_aix_ppc.go | 2 - .../x/sys/unix/syscall_aix_ppc64.go | 2 - vendor/golang.org/x/sys/unix/syscall_bsd.go | 20 +- .../golang.org/x/sys/unix/syscall_darwin.go | 305 +- .../x/sys/unix/syscall_darwin_amd64.go | 1 - .../x/sys/unix/syscall_darwin_arm64.go | 1 - .../x/sys/unix/syscall_darwin_libSystem.go | 3 +- .../x/sys/unix/syscall_dragonfly.go | 201 +- .../x/sys/unix/syscall_dragonfly_amd64.go | 1 - .../golang.org/x/sys/unix/syscall_freebsd.go | 249 +- .../x/sys/unix/syscall_freebsd_386.go | 13 +- .../x/sys/unix/syscall_freebsd_amd64.go | 13 +- .../x/sys/unix/syscall_freebsd_arm.go | 11 +- .../x/sys/unix/syscall_freebsd_arm64.go | 11 +- .../x/sys/unix/syscall_freebsd_riscv64.go | 11 +- vendor/golang.org/x/sys/unix/syscall_hurd.go | 30 + .../golang.org/x/sys/unix/syscall_hurd_386.go | 28 + .../golang.org/x/sys/unix/syscall_illumos.go | 1 - vendor/golang.org/x/sys/unix/syscall_linux.go | 451 +- .../x/sys/unix/syscall_linux_386.go | 28 - .../x/sys/unix/syscall_linux_alarm.go | 2 - .../x/sys/unix/syscall_linux_amd64.go | 4 +- .../x/sys/unix/syscall_linux_amd64_gc.go | 1 - .../x/sys/unix/syscall_linux_arm.go | 28 - .../x/sys/unix/syscall_linux_arm64.go | 13 +- .../golang.org/x/sys/unix/syscall_linux_gc.go | 1 - .../x/sys/unix/syscall_linux_gc_386.go | 1 - .../x/sys/unix/syscall_linux_gc_arm.go | 1 - .../x/sys/unix/syscall_linux_gccgo_386.go | 1 - .../x/sys/unix/syscall_linux_gccgo_arm.go | 1 - .../x/sys/unix/syscall_linux_loong64.go | 8 +- .../x/sys/unix/syscall_linux_mips64x.go | 5 +- .../x/sys/unix/syscall_linux_mipsx.go | 29 - .../x/sys/unix/syscall_linux_ppc.go | 28 - .../x/sys/unix/syscall_linux_ppc64x.go | 3 - .../x/sys/unix/syscall_linux_riscv64.go | 15 +- .../x/sys/unix/syscall_linux_s390x.go | 2 - .../x/sys/unix/syscall_linux_sparc64.go | 2 - .../golang.org/x/sys/unix/syscall_netbsd.go | 294 +- .../x/sys/unix/syscall_netbsd_386.go | 1 - .../x/sys/unix/syscall_netbsd_amd64.go | 1 - .../x/sys/unix/syscall_netbsd_arm.go | 1 - .../x/sys/unix/syscall_netbsd_arm64.go | 1 - .../golang.org/x/sys/unix/syscall_openbsd.go | 123 +- .../x/sys/unix/syscall_openbsd_386.go | 1 - .../x/sys/unix/syscall_openbsd_amd64.go | 1 - .../x/sys/unix/syscall_openbsd_arm.go | 1 - .../x/sys/unix/syscall_openbsd_arm64.go | 1 - .../x/sys/unix/syscall_openbsd_libc.go | 3 +- .../x/sys/unix/syscall_openbsd_ppc64.go | 1 - .../x/sys/unix/syscall_openbsd_riscv64.go | 1 - .../golang.org/x/sys/unix/syscall_solaris.go | 74 +- .../x/sys/unix/syscall_solaris_amd64.go | 1 - vendor/golang.org/x/sys/unix/syscall_unix.go | 85 +- .../golang.org/x/sys/unix/syscall_unix_gc.go | 2 - .../x/sys/unix/syscall_unix_gc_ppc64x.go | 3 - .../x/sys/unix/syscall_zos_s390x.go | 1517 ++- vendor/golang.org/x/sys/unix/sysvshm_linux.go | 1 - vendor/golang.org/x/sys/unix/sysvshm_unix.go | 3 +- .../x/sys/unix/sysvshm_unix_other.go | 3 +- vendor/golang.org/x/sys/unix/timestruct.go | 3 +- .../golang.org/x/sys/unix/unveil_openbsd.go | 41 +- vendor/golang.org/x/sys/unix/xattr_bsd.go | 10 +- .../golang.org/x/sys/unix/zerrors_aix_ppc.go | 1 - .../x/sys/unix/zerrors_aix_ppc64.go | 1 - .../x/sys/unix/zerrors_darwin_amd64.go | 32 +- .../x/sys/unix/zerrors_darwin_arm64.go | 32 +- .../x/sys/unix/zerrors_dragonfly_amd64.go | 1 - .../x/sys/unix/zerrors_freebsd_386.go | 1 - .../x/sys/unix/zerrors_freebsd_amd64.go | 1 - .../x/sys/unix/zerrors_freebsd_arm.go | 1 - .../x/sys/unix/zerrors_freebsd_arm64.go | 1 - .../x/sys/unix/zerrors_freebsd_riscv64.go | 1 - vendor/golang.org/x/sys/unix/zerrors_linux.go | 271 +- .../x/sys/unix/zerrors_linux_386.go | 19 +- .../x/sys/unix/zerrors_linux_amd64.go | 19 +- .../x/sys/unix/zerrors_linux_arm.go | 18 +- .../x/sys/unix/zerrors_linux_arm64.go | 21 +- .../x/sys/unix/zerrors_linux_loong64.go | 21 +- .../x/sys/unix/zerrors_linux_mips.go | 18 +- .../x/sys/unix/zerrors_linux_mips64.go | 18 +- .../x/sys/unix/zerrors_linux_mips64le.go | 18 +- .../x/sys/unix/zerrors_linux_mipsle.go | 18 +- .../x/sys/unix/zerrors_linux_ppc.go | 18 +- .../x/sys/unix/zerrors_linux_ppc64.go | 18 +- .../x/sys/unix/zerrors_linux_ppc64le.go | 18 +- .../x/sys/unix/zerrors_linux_riscv64.go | 21 +- .../x/sys/unix/zerrors_linux_s390x.go | 18 +- .../x/sys/unix/zerrors_linux_sparc64.go | 66 +- .../x/sys/unix/zerrors_netbsd_386.go | 1 - .../x/sys/unix/zerrors_netbsd_amd64.go | 1 - .../x/sys/unix/zerrors_netbsd_arm.go | 1 - .../x/sys/unix/zerrors_netbsd_arm64.go | 1 - .../x/sys/unix/zerrors_openbsd_386.go | 357 +- .../x/sys/unix/zerrors_openbsd_amd64.go | 190 +- .../x/sys/unix/zerrors_openbsd_arm.go | 349 +- .../x/sys/unix/zerrors_openbsd_arm64.go | 161 +- .../x/sys/unix/zerrors_openbsd_mips64.go | 96 +- .../x/sys/unix/zerrors_openbsd_ppc64.go | 1 - .../x/sys/unix/zerrors_openbsd_riscv64.go | 1 - .../x/sys/unix/zerrors_solaris_amd64.go | 1 - .../x/sys/unix/zerrors_zos_s390x.go | 236 +- .../x/sys/unix/zptrace_armnn_linux.go | 10 +- .../x/sys/unix/zptrace_linux_arm64.go | 4 +- .../x/sys/unix/zptrace_mipsnn_linux.go | 10 +- .../x/sys/unix/zptrace_mipsnnle_linux.go | 10 +- .../x/sys/unix/zptrace_x86_linux.go | 10 +- .../x/sys/unix/zsymaddr_zos_s390x.s | 364 + .../golang.org/x/sys/unix/zsyscall_aix_ppc.go | 46 +- .../x/sys/unix/zsyscall_aix_ppc64.go | 47 +- .../x/sys/unix/zsyscall_aix_ppc64_gc.go | 18 +- .../x/sys/unix/zsyscall_aix_ppc64_gccgo.go | 19 +- .../x/sys/unix/zsyscall_darwin_amd64.go | 173 +- .../x/sys/unix/zsyscall_darwin_amd64.s | 185 +- .../x/sys/unix/zsyscall_darwin_arm64.go | 173 +- .../x/sys/unix/zsyscall_darwin_arm64.s | 185 +- .../x/sys/unix/zsyscall_dragonfly_amd64.go | 53 +- .../x/sys/unix/zsyscall_freebsd_386.go | 63 +- .../x/sys/unix/zsyscall_freebsd_amd64.go | 63 +- .../x/sys/unix/zsyscall_freebsd_arm.go | 63 +- .../x/sys/unix/zsyscall_freebsd_arm64.go | 63 +- .../x/sys/unix/zsyscall_freebsd_riscv64.go | 63 +- .../x/sys/unix/zsyscall_illumos_amd64.go | 11 +- .../golang.org/x/sys/unix/zsyscall_linux.go | 152 +- .../x/sys/unix/zsyscall_linux_386.go | 11 - .../x/sys/unix/zsyscall_linux_amd64.go | 11 - .../x/sys/unix/zsyscall_linux_arm.go | 11 - .../x/sys/unix/zsyscall_linux_arm64.go | 11 - .../x/sys/unix/zsyscall_linux_loong64.go | 1 - .../x/sys/unix/zsyscall_linux_mips.go | 11 - .../x/sys/unix/zsyscall_linux_mips64.go | 11 - .../x/sys/unix/zsyscall_linux_mips64le.go | 11 - .../x/sys/unix/zsyscall_linux_mipsle.go | 11 - .../x/sys/unix/zsyscall_linux_ppc.go | 11 - .../x/sys/unix/zsyscall_linux_ppc64.go | 11 - .../x/sys/unix/zsyscall_linux_ppc64le.go | 11 - .../x/sys/unix/zsyscall_linux_riscv64.go | 27 +- .../x/sys/unix/zsyscall_linux_s390x.go | 11 - .../x/sys/unix/zsyscall_linux_sparc64.go | 11 - .../x/sys/unix/zsyscall_netbsd_386.go | 60 +- .../x/sys/unix/zsyscall_netbsd_amd64.go | 60 +- .../x/sys/unix/zsyscall_netbsd_arm.go | 60 +- .../x/sys/unix/zsyscall_netbsd_arm64.go | 60 +- .../x/sys/unix/zsyscall_openbsd_386.go | 154 +- .../x/sys/unix/zsyscall_openbsd_386.s | 177 +- .../x/sys/unix/zsyscall_openbsd_amd64.go | 154 +- .../x/sys/unix/zsyscall_openbsd_amd64.s | 177 +- .../x/sys/unix/zsyscall_openbsd_arm.go | 154 +- .../x/sys/unix/zsyscall_openbsd_arm.s | 177 +- .../x/sys/unix/zsyscall_openbsd_arm64.go | 154 +- .../x/sys/unix/zsyscall_openbsd_arm64.s | 177 +- .../x/sys/unix/zsyscall_openbsd_mips64.go | 932 +- .../x/sys/unix/zsyscall_openbsd_mips64.s | 699 ++ .../x/sys/unix/zsyscall_openbsd_ppc64.go | 154 +- .../x/sys/unix/zsyscall_openbsd_ppc64.s | 54 +- .../x/sys/unix/zsyscall_openbsd_riscv64.go | 154 +- .../x/sys/unix/zsyscall_openbsd_riscv64.s | 177 +- .../x/sys/unix/zsyscall_solaris_amd64.go | 292 +- .../x/sys/unix/zsyscall_zos_s390x.go | 3113 +++++- .../x/sys/unix/zsysctl_openbsd_386.go | 52 +- .../x/sys/unix/zsysctl_openbsd_amd64.go | 18 +- .../x/sys/unix/zsysctl_openbsd_arm.go | 52 +- .../x/sys/unix/zsysctl_openbsd_arm64.go | 12 +- .../x/sys/unix/zsysctl_openbsd_mips64.go | 4 +- .../x/sys/unix/zsysctl_openbsd_ppc64.go | 1 - .../x/sys/unix/zsysctl_openbsd_riscv64.go | 1 - .../x/sys/unix/zsysnum_darwin_amd64.go | 1 - .../x/sys/unix/zsysnum_darwin_arm64.go | 1 - .../x/sys/unix/zsysnum_dragonfly_amd64.go | 1 - .../x/sys/unix/zsysnum_freebsd_386.go | 1 - .../x/sys/unix/zsysnum_freebsd_amd64.go | 1 - .../x/sys/unix/zsysnum_freebsd_arm.go | 1 - .../x/sys/unix/zsysnum_freebsd_arm64.go | 1 - .../x/sys/unix/zsysnum_freebsd_riscv64.go | 1 - .../x/sys/unix/zsysnum_linux_386.go | 13 +- .../x/sys/unix/zsysnum_linux_amd64.go | 13 +- .../x/sys/unix/zsysnum_linux_arm.go | 13 +- .../x/sys/unix/zsysnum_linux_arm64.go | 13 +- .../x/sys/unix/zsysnum_linux_loong64.go | 13 +- .../x/sys/unix/zsysnum_linux_mips.go | 13 +- .../x/sys/unix/zsysnum_linux_mips64.go | 13 +- .../x/sys/unix/zsysnum_linux_mips64le.go | 13 +- .../x/sys/unix/zsysnum_linux_mipsle.go | 13 +- .../x/sys/unix/zsysnum_linux_ppc.go | 13 +- .../x/sys/unix/zsysnum_linux_ppc64.go | 13 +- .../x/sys/unix/zsysnum_linux_ppc64le.go | 13 +- .../x/sys/unix/zsysnum_linux_riscv64.go | 15 +- .../x/sys/unix/zsysnum_linux_s390x.go | 14 +- .../x/sys/unix/zsysnum_linux_sparc64.go | 13 +- .../x/sys/unix/zsysnum_netbsd_386.go | 1 - .../x/sys/unix/zsysnum_netbsd_amd64.go | 1 - .../x/sys/unix/zsysnum_netbsd_arm.go | 1 - .../x/sys/unix/zsysnum_netbsd_arm64.go | 1 - .../x/sys/unix/zsysnum_openbsd_386.go | 1 - .../x/sys/unix/zsysnum_openbsd_amd64.go | 1 - .../x/sys/unix/zsysnum_openbsd_arm.go | 1 - .../x/sys/unix/zsysnum_openbsd_arm64.go | 1 - .../x/sys/unix/zsysnum_openbsd_mips64.go | 2 +- .../x/sys/unix/zsysnum_openbsd_ppc64.go | 1 - .../x/sys/unix/zsysnum_openbsd_riscv64.go | 1 - .../x/sys/unix/zsysnum_zos_s390x.go | 5508 +++++----- .../golang.org/x/sys/unix/ztypes_aix_ppc.go | 1 - .../golang.org/x/sys/unix/ztypes_aix_ppc64.go | 1 - .../x/sys/unix/ztypes_darwin_amd64.go | 25 +- .../x/sys/unix/ztypes_darwin_arm64.go | 25 +- .../x/sys/unix/ztypes_dragonfly_amd64.go | 1 - .../x/sys/unix/ztypes_freebsd_386.go | 4 +- .../x/sys/unix/ztypes_freebsd_amd64.go | 4 +- .../x/sys/unix/ztypes_freebsd_arm.go | 4 +- .../x/sys/unix/ztypes_freebsd_arm64.go | 4 +- .../x/sys/unix/ztypes_freebsd_riscv64.go | 4 +- vendor/golang.org/x/sys/unix/ztypes_linux.go | 601 +- .../golang.org/x/sys/unix/ztypes_linux_386.go | 13 +- .../x/sys/unix/ztypes_linux_amd64.go | 14 +- .../golang.org/x/sys/unix/ztypes_linux_arm.go | 14 +- .../x/sys/unix/ztypes_linux_arm64.go | 14 +- .../x/sys/unix/ztypes_linux_loong64.go | 14 +- .../x/sys/unix/ztypes_linux_mips.go | 14 +- .../x/sys/unix/ztypes_linux_mips64.go | 14 +- .../x/sys/unix/ztypes_linux_mips64le.go | 14 +- .../x/sys/unix/ztypes_linux_mipsle.go | 14 +- .../golang.org/x/sys/unix/ztypes_linux_ppc.go | 14 +- .../x/sys/unix/ztypes_linux_ppc64.go | 14 +- .../x/sys/unix/ztypes_linux_ppc64le.go | 14 +- .../x/sys/unix/ztypes_linux_riscv64.go | 74 +- .../x/sys/unix/ztypes_linux_s390x.go | 14 +- .../x/sys/unix/ztypes_linux_sparc64.go | 14 +- .../x/sys/unix/ztypes_netbsd_386.go | 85 +- .../x/sys/unix/ztypes_netbsd_amd64.go | 85 +- .../x/sys/unix/ztypes_netbsd_arm.go | 85 +- .../x/sys/unix/ztypes_netbsd_arm64.go | 85 +- .../x/sys/unix/ztypes_openbsd_386.go | 98 +- .../x/sys/unix/ztypes_openbsd_amd64.go | 34 +- .../x/sys/unix/ztypes_openbsd_arm.go | 10 +- .../x/sys/unix/ztypes_openbsd_arm64.go | 10 +- .../x/sys/unix/ztypes_openbsd_mips64.go | 10 +- .../x/sys/unix/ztypes_openbsd_ppc64.go | 1 - .../x/sys/unix/ztypes_openbsd_riscv64.go | 1 - .../x/sys/unix/ztypes_solaris_amd64.go | 1 - .../golang.org/x/sys/unix/ztypes_zos_s390x.go | 147 +- vendor/golang.org/x/sys/windows/aliases.go | 12 + .../golang.org/x/sys/windows/dll_windows.go | 416 + .../golang.org/x/sys/windows/env_windows.go | 57 + vendor/golang.org/x/sys/windows/eventlog.go | 20 + .../golang.org/x/sys/windows/exec_windows.go | 248 + .../x/sys/windows/memory_windows.go | 48 + vendor/golang.org/x/sys/windows/mkerrors.bash | 70 + .../x/sys/windows/mkknownfolderids.bash | 27 + vendor/golang.org/x/sys/windows/mksyscall.go | 9 + vendor/golang.org/x/sys/windows/race.go | 30 + vendor/golang.org/x/sys/windows/race0.go | 25 + .../x/sys/windows/security_windows.go | 1458 +++ vendor/golang.org/x/sys/windows/service.go | 257 + .../x/sys/windows/setupapi_windows.go | 1425 +++ vendor/golang.org/x/sys/windows/str.go | 22 + vendor/golang.org/x/sys/windows/syscall.go | 104 + .../x/sys/windows/syscall_windows.go | 1930 ++++ .../golang.org/x/sys/windows/types_windows.go | 3476 ++++++ .../x/sys/windows/types_windows_386.go | 35 + .../x/sys/windows/types_windows_amd64.go | 34 + .../x/sys/windows/types_windows_arm.go | 35 + .../x/sys/windows/types_windows_arm64.go | 34 + .../x/sys/windows/zerrors_windows.go | 9468 +++++++++++++++++ .../x/sys/windows/zknownfolderids_windows.go | 149 + .../x/sys/windows/zsyscall_windows.go | 4615 ++++++++ vendor/golang.org/x/term/CONTRIBUTING.md | 26 + vendor/golang.org/x/term/LICENSE | 27 + vendor/golang.org/x/term/PATENTS | 22 + vendor/golang.org/x/term/README.md | 19 + vendor/golang.org/x/term/codereview.cfg | 1 + vendor/golang.org/x/term/term.go | 60 + vendor/golang.org/x/term/term_plan9.go | 42 + vendor/golang.org/x/term/term_unix.go | 91 + vendor/golang.org/x/term/term_unix_bsd.go | 12 + vendor/golang.org/x/term/term_unix_other.go | 12 + vendor/golang.org/x/term/term_unsupported.go | 38 + vendor/golang.org/x/term/term_windows.go | 79 + vendor/golang.org/x/term/terminal.go | 986 ++ vendor/golang.org/x/text/LICENSE | 27 + vendor/golang.org/x/text/PATENTS | 22 + .../x/text/secure/bidirule/bidirule.go | 336 + .../x/text/secure/bidirule/bidirule10.0.0.go | 11 + .../x/text/secure/bidirule/bidirule9.0.0.go | 14 + .../golang.org/x/text/transform/transform.go | 709 ++ vendor/golang.org/x/text/unicode/bidi/bidi.go | 359 + .../golang.org/x/text/unicode/bidi/bracket.go | 335 + vendor/golang.org/x/text/unicode/bidi/core.go | 1071 ++ vendor/golang.org/x/text/unicode/bidi/prop.go | 206 + .../x/text/unicode/bidi/tables10.0.0.go | 1815 ++++ .../x/text/unicode/bidi/tables11.0.0.go | 1887 ++++ .../x/text/unicode/bidi/tables12.0.0.go | 1923 ++++ .../x/text/unicode/bidi/tables13.0.0.go | 1955 ++++ .../x/text/unicode/bidi/tables15.0.0.go | 2042 ++++ .../x/text/unicode/bidi/tables9.0.0.go | 1781 ++++ .../golang.org/x/text/unicode/bidi/trieval.go | 48 + .../x/text/unicode/norm/composition.go | 512 + .../x/text/unicode/norm/forminfo.go | 279 + .../golang.org/x/text/unicode/norm/input.go | 109 + vendor/golang.org/x/text/unicode/norm/iter.go | 458 + .../x/text/unicode/norm/normalize.go | 610 ++ .../x/text/unicode/norm/readwriter.go | 125 + .../x/text/unicode/norm/tables10.0.0.go | 7657 +++++++++++++ .../x/text/unicode/norm/tables11.0.0.go | 7693 ++++++++++++++ .../x/text/unicode/norm/tables12.0.0.go | 7710 ++++++++++++++ .../x/text/unicode/norm/tables13.0.0.go | 7760 ++++++++++++++ .../x/text/unicode/norm/tables15.0.0.go | 7907 ++++++++++++++ .../x/text/unicode/norm/tables9.0.0.go | 7637 +++++++++++++ .../x/text/unicode/norm/transform.go | 88 + vendor/golang.org/x/text/unicode/norm/trie.go | 54 + vendor/gonum.org/v1/gonum/AUTHORS | 137 + vendor/gonum.org/v1/gonum/CONTRIBUTORS | 140 + vendor/gonum.org/v1/gonum/LICENSE | 23 + vendor/gonum.org/v1/gonum/blas/README.md | 51 + vendor/gonum.org/v1/gonum/blas/blas.go | 283 + .../gonum.org/v1/gonum/blas/blas64/blas64.go | 533 + vendor/gonum.org/v1/gonum/blas/blas64/conv.go | 263 + .../v1/gonum/blas/blas64/conv_symmetric.go | 153 + vendor/gonum.org/v1/gonum/blas/blas64/doc.go | 6 + .../v1/gonum/blas/cblas128/cblas128.go | 600 ++ .../gonum.org/v1/gonum/blas/cblas128/conv.go | 265 + .../v1/gonum/blas/cblas128/conv_hermitian.go | 155 + .../v1/gonum/blas/cblas128/conv_symmetric.go | 155 + .../gonum.org/v1/gonum/blas/cblas128/doc.go | 6 + .../gonum.org/v1/gonum/blas/conversions.bash | 159 + vendor/gonum.org/v1/gonum/blas/doc.go | 108 + vendor/gonum.org/v1/gonum/blas/gonum/dgemm.go | 297 + vendor/gonum.org/v1/gonum/blas/gonum/doc.go | 99 + .../gonum.org/v1/gonum/blas/gonum/errors.go | 35 + vendor/gonum.org/v1/gonum/blas/gonum/gonum.go | 38 + .../v1/gonum/blas/gonum/level1cmplx128.go | 454 + .../v1/gonum/blas/gonum/level1cmplx64.go | 476 + .../v1/gonum/blas/gonum/level1float32.go | 653 ++ .../gonum/blas/gonum/level1float32_dsdot.go | 54 + .../v1/gonum/blas/gonum/level1float32_sdot.go | 54 + .../gonum/blas/gonum/level1float32_sdsdot.go | 54 + .../v1/gonum/blas/gonum/level1float64.go | 629 ++ .../v1/gonum/blas/gonum/level1float64_ddot.go | 50 + .../v1/gonum/blas/gonum/level2cmplx128.go | 2940 +++++ .../v1/gonum/blas/gonum/level2cmplx64.go | 2976 ++++++ .../v1/gonum/blas/gonum/level2float32.go | 2400 +++++ .../v1/gonum/blas/gonum/level2float64.go | 2366 ++++ .../v1/gonum/blas/gonum/level3cmplx128.go | 1751 +++ .../v1/gonum/blas/gonum/level3cmplx64.go | 1771 +++ .../v1/gonum/blas/gonum/level3float32.go | 925 ++ .../v1/gonum/blas/gonum/level3float64.go | 913 ++ vendor/gonum.org/v1/gonum/blas/gonum/sgemm.go | 301 + .../v1/gonum/blas/gonum/single_precision.bash | 224 + vendor/gonum.org/v1/gonum/floats/README.md | 7 + vendor/gonum.org/v1/gonum/floats/doc.go | 11 + vendor/gonum.org/v1/gonum/floats/floats.go | 808 ++ .../gonum.org/v1/gonum/floats/scalar/doc.go | 6 + .../v1/gonum/floats/scalar/scalar.go | 171 + .../gonum/internal/asm/c128/axpyinc_amd64.s | 134 + .../gonum/internal/asm/c128/axpyincto_amd64.s | 141 + .../internal/asm/c128/axpyunitary_amd64.s | 122 + .../internal/asm/c128/axpyunitaryto_amd64.s | 123 + .../v1/gonum/internal/asm/c128/doc.go | 6 + .../gonum/internal/asm/c128/dotcinc_amd64.s | 153 + .../internal/asm/c128/dotcunitary_amd64.s | 143 + .../gonum/internal/asm/c128/dotuinc_amd64.s | 141 + .../internal/asm/c128/dotuunitary_amd64.s | 130 + .../gonum/internal/asm/c128/dscalinc_amd64.s | 69 + .../internal/asm/c128/dscalunitary_amd64.s | 66 + .../v1/gonum/internal/asm/c128/scal.go | 33 + .../internal/asm/c128/scalUnitary_amd64.s | 116 + .../gonum/internal/asm/c128/scalinc_amd64.s | 121 + .../v1/gonum/internal/asm/c128/stubs.go | 180 + .../v1/gonum/internal/asm/c128/stubs_amd64.go | 109 + .../v1/gonum/internal/asm/c128/stubs_noasm.go | 176 + .../v1/gonum/internal/asm/c64/axpyinc_amd64.s | 151 + .../gonum/internal/asm/c64/axpyincto_amd64.s | 156 + .../internal/asm/c64/axpyunitary_amd64.s | 160 + .../internal/asm/c64/axpyunitaryto_amd64.s | 157 + .../v1/gonum/internal/asm/c64/conj.go | 7 + .../v1/gonum/internal/asm/c64/doc.go | 6 + .../v1/gonum/internal/asm/c64/dotcinc_amd64.s | 160 + .../internal/asm/c64/dotcunitary_amd64.s | 208 + .../v1/gonum/internal/asm/c64/dotuinc_amd64.s | 148 + .../internal/asm/c64/dotuunitary_amd64.s | 197 + .../v1/gonum/internal/asm/c64/scal.go | 85 + .../v1/gonum/internal/asm/c64/stubs.go | 180 + .../v1/gonum/internal/asm/c64/stubs_amd64.go | 77 + .../v1/gonum/internal/asm/c64/stubs_noasm.go | 122 + .../v1/gonum/internal/asm/f32/axpyinc_amd64.s | 73 + .../gonum/internal/asm/f32/axpyincto_amd64.s | 78 + .../internal/asm/f32/axpyunitary_amd64.s | 97 + .../internal/asm/f32/axpyunitaryto_amd64.s | 98 + .../v1/gonum/internal/asm/f32/ddotinc_amd64.s | 91 + .../internal/asm/f32/ddotunitary_amd64.s | 110 + .../v1/gonum/internal/asm/f32/doc.go | 6 + .../v1/gonum/internal/asm/f32/dotinc_amd64.s | 85 + .../gonum/internal/asm/f32/dotunitary_amd64.s | 106 + .../v1/gonum/internal/asm/f32/ge_amd64.go | 18 + .../v1/gonum/internal/asm/f32/ge_amd64.s | 757 ++ .../v1/gonum/internal/asm/f32/ge_noasm.go | 39 + .../v1/gonum/internal/asm/f32/gemv.go | 92 + .../v1/gonum/internal/asm/f32/l2norm.go | 90 + .../v1/gonum/internal/asm/f32/scal.go | 59 + .../v1/gonum/internal/asm/f32/stubs_amd64.go | 86 + .../v1/gonum/internal/asm/f32/stubs_noasm.go | 137 + .../v1/gonum/internal/asm/f32/sum_amd64.s | 100 + .../v1/gonum/internal/asm/f64/abssum_amd64.s | 82 + .../gonum/internal/asm/f64/abssuminc_amd64.s | 90 + .../v1/gonum/internal/asm/f64/add_amd64.s | 66 + .../gonum/internal/asm/f64/addconst_amd64.s | 53 + .../v1/gonum/internal/asm/f64/axpy.go | 62 + .../v1/gonum/internal/asm/f64/axpyinc_amd64.s | 142 + .../gonum/internal/asm/f64/axpyincto_amd64.s | 148 + .../internal/asm/f64/axpyunitary_amd64.s | 134 + .../internal/asm/f64/axpyunitaryto_amd64.s | 140 + .../v1/gonum/internal/asm/f64/cumprod_amd64.s | 71 + .../v1/gonum/internal/asm/f64/cumsum_amd64.s | 64 + .../v1/gonum/internal/asm/f64/div_amd64.s | 67 + .../v1/gonum/internal/asm/f64/divto_amd64.s | 73 + .../v1/gonum/internal/asm/f64/doc.go | 6 + .../v1/gonum/internal/asm/f64/dot.go | 38 + .../v1/gonum/internal/asm/f64/dot_amd64.s | 145 + .../v1/gonum/internal/asm/f64/ge_amd64.go | 29 + .../v1/gonum/internal/asm/f64/ge_noasm.go | 125 + .../v1/gonum/internal/asm/f64/gemvN_amd64.s | 685 ++ .../v1/gonum/internal/asm/f64/gemvT_amd64.s | 745 ++ .../v1/gonum/internal/asm/f64/ger_amd64.s | 591 + .../v1/gonum/internal/asm/f64/l1norm_amd64.s | 58 + .../v1/gonum/internal/asm/f64/l2norm_amd64.s | 109 + .../v1/gonum/internal/asm/f64/l2norm_noasm.go | 93 + .../gonum/internal/asm/f64/l2normdist_amd64.s | 115 + .../gonum/internal/asm/f64/l2norminc_amd64.s | 110 + .../gonum/internal/asm/f64/linfnorm_amd64.s | 57 + .../v1/gonum/internal/asm/f64/scal.go | 62 + .../v1/gonum/internal/asm/f64/scalinc_amd64.s | 113 + .../gonum/internal/asm/f64/scalincto_amd64.s | 122 + .../internal/asm/f64/scalunitary_amd64.s | 112 + .../internal/asm/f64/scalunitaryto_amd64.s | 113 + .../v1/gonum/internal/asm/f64/stubs_amd64.go | 277 + .../v1/gonum/internal/asm/f64/stubs_noasm.go | 182 + .../v1/gonum/internal/asm/f64/sum_amd64.s | 99 + .../v1/gonum/internal/cmplx64/abs.go | 14 + .../v1/gonum/internal/cmplx64/conj.go | 12 + .../v1/gonum/internal/cmplx64/doc.go | 7 + .../v1/gonum/internal/cmplx64/isinf.go | 25 + .../v1/gonum/internal/cmplx64/isnan.go | 29 + .../v1/gonum/internal/cmplx64/sqrt.go | 108 + .../gonum.org/v1/gonum/internal/math32/doc.go | 7 + .../v1/gonum/internal/math32/math.go | 166 + .../v1/gonum/internal/math32/signbit.go | 16 + .../v1/gonum/internal/math32/sqrt.go | 26 + .../v1/gonum/internal/math32/sqrt_amd64.go | 22 + .../v1/gonum/internal/math32/sqrt_amd64.s | 17 + .../v1/gonum/internal/math32/sqrt_arm64.go | 22 + .../v1/gonum/internal/math32/sqrt_arm64.s | 18 + vendor/gonum.org/v1/gonum/lapack/.gitignore | 0 vendor/gonum.org/v1/gonum/lapack/README.md | 29 + vendor/gonum.org/v1/gonum/lapack/doc.go | 6 + .../gonum.org/v1/gonum/lapack/gonum/dbdsqr.go | 506 + .../gonum.org/v1/gonum/lapack/gonum/dgebak.go | 91 + .../gonum.org/v1/gonum/lapack/gonum/dgebal.go | 248 + .../gonum.org/v1/gonum/lapack/gonum/dgebd2.go | 88 + .../gonum.org/v1/gonum/lapack/gonum/dgebrd.go | 169 + .../gonum.org/v1/gonum/lapack/gonum/dgecon.go | 106 + .../gonum.org/v1/gonum/lapack/gonum/dgeev.go | 287 + .../gonum.org/v1/gonum/lapack/gonum/dgehd2.go | 105 + .../gonum.org/v1/gonum/lapack/gonum/dgehrd.go | 202 + .../gonum.org/v1/gonum/lapack/gonum/dgelq2.go | 65 + .../gonum.org/v1/gonum/lapack/gonum/dgelqf.go | 97 + .../gonum.org/v1/gonum/lapack/gonum/dgels.go | 220 + .../gonum.org/v1/gonum/lapack/gonum/dgeql2.go | 67 + .../gonum.org/v1/gonum/lapack/gonum/dgeqp3.go | 195 + .../gonum.org/v1/gonum/lapack/gonum/dgeqr2.go | 78 + .../gonum.org/v1/gonum/lapack/gonum/dgeqrf.go | 108 + .../gonum.org/v1/gonum/lapack/gonum/dgerq2.go | 74 + .../gonum.org/v1/gonum/lapack/gonum/dgerqf.go | 135 + .../gonum.org/v1/gonum/lapack/gonum/dgesc2.go | 93 + .../gonum.org/v1/gonum/lapack/gonum/dgesv.go | 60 + .../gonum.org/v1/gonum/lapack/gonum/dgesvd.go | 1378 +++ .../gonum.org/v1/gonum/lapack/gonum/dgetc2.go | 125 + .../gonum.org/v1/gonum/lapack/gonum/dgetf2.go | 90 + .../gonum.org/v1/gonum/lapack/gonum/dgetrf.go | 89 + .../gonum.org/v1/gonum/lapack/gonum/dgetri.go | 116 + .../gonum.org/v1/gonum/lapack/gonum/dgetrs.go | 74 + .../gonum.org/v1/gonum/lapack/gonum/dgghrd.go | 125 + .../v1/gonum/lapack/gonum/dggsvd3.go | 258 + .../v1/gonum/lapack/gonum/dggsvp3.go | 286 + .../gonum.org/v1/gonum/lapack/gonum/dgtsv.go | 101 + .../gonum.org/v1/gonum/lapack/gonum/dhseqr.go | 272 + .../gonum.org/v1/gonum/lapack/gonum/dlabrd.go | 183 + .../gonum.org/v1/gonum/lapack/gonum/dlacn2.go | 136 + .../gonum.org/v1/gonum/lapack/gonum/dlacpy.go | 59 + .../gonum.org/v1/gonum/lapack/gonum/dlae2.go | 51 + .../gonum.org/v1/gonum/lapack/gonum/dlaev2.go | 85 + .../gonum.org/v1/gonum/lapack/gonum/dlaexc.go | 269 + .../gonum.org/v1/gonum/lapack/gonum/dlag2.go | 237 + .../gonum.org/v1/gonum/lapack/gonum/dlags2.go | 186 + .../gonum.org/v1/gonum/lapack/gonum/dlagtm.go | 111 + .../gonum.org/v1/gonum/lapack/gonum/dlahqr.go | 449 + .../gonum.org/v1/gonum/lapack/gonum/dlahr2.go | 202 + .../gonum.org/v1/gonum/lapack/gonum/dlaln2.go | 407 + .../gonum.org/v1/gonum/lapack/gonum/dlangb.go | 87 + .../gonum.org/v1/gonum/lapack/gonum/dlange.go | 89 + .../gonum.org/v1/gonum/lapack/gonum/dlangt.go | 115 + .../gonum.org/v1/gonum/lapack/gonum/dlanhs.go | 78 + .../gonum.org/v1/gonum/lapack/gonum/dlansb.go | 131 + .../gonum.org/v1/gonum/lapack/gonum/dlanst.go | 75 + .../gonum.org/v1/gonum/lapack/gonum/dlansy.go | 125 + .../gonum.org/v1/gonum/lapack/gonum/dlantb.go | 209 + .../gonum.org/v1/gonum/lapack/gonum/dlantr.go | 252 + .../gonum.org/v1/gonum/lapack/gonum/dlanv2.go | 151 + .../gonum.org/v1/gonum/lapack/gonum/dlapll.go | 55 + .../gonum.org/v1/gonum/lapack/gonum/dlapmr.go | 88 + .../gonum.org/v1/gonum/lapack/gonum/dlapmt.go | 89 + .../gonum.org/v1/gonum/lapack/gonum/dlapy2.go | 14 + .../gonum.org/v1/gonum/lapack/gonum/dlaqp2.go | 127 + .../gonum.org/v1/gonum/lapack/gonum/dlaqps.go | 244 + .../v1/gonum/lapack/gonum/dlaqr04.go | 493 + .../gonum.org/v1/gonum/lapack/gonum/dlaqr1.go | 61 + .../v1/gonum/lapack/gonum/dlaqr23.go | 423 + .../gonum.org/v1/gonum/lapack/gonum/dlaqr5.go | 560 + .../gonum.org/v1/gonum/lapack/gonum/dlarf.go | 102 + .../gonum.org/v1/gonum/lapack/gonum/dlarfb.go | 461 + .../gonum.org/v1/gonum/lapack/gonum/dlarfg.go | 75 + .../gonum.org/v1/gonum/lapack/gonum/dlarft.go | 169 + .../gonum.org/v1/gonum/lapack/gonum/dlarfx.go | 552 + .../gonum.org/v1/gonum/lapack/gonum/dlartg.go | 73 + .../gonum.org/v1/gonum/lapack/gonum/dlas2.go | 45 + .../gonum.org/v1/gonum/lapack/gonum/dlascl.go | 111 + .../gonum.org/v1/gonum/lapack/gonum/dlaset.go | 58 + .../gonum.org/v1/gonum/lapack/gonum/dlasq1.go | 100 + .../gonum.org/v1/gonum/lapack/gonum/dlasq2.go | 370 + .../gonum.org/v1/gonum/lapack/gonum/dlasq3.go | 172 + .../gonum.org/v1/gonum/lapack/gonum/dlasq4.go | 249 + .../gonum.org/v1/gonum/lapack/gonum/dlasq5.go | 140 + .../gonum.org/v1/gonum/lapack/gonum/dlasq6.go | 118 + .../gonum.org/v1/gonum/lapack/gonum/dlasr.go | 287 + .../gonum.org/v1/gonum/lapack/gonum/dlasrt.go | 36 + .../gonum.org/v1/gonum/lapack/gonum/dlassq.go | 131 + .../gonum.org/v1/gonum/lapack/gonum/dlasv2.go | 117 + .../gonum.org/v1/gonum/lapack/gonum/dlaswp.go | 58 + .../gonum.org/v1/gonum/lapack/gonum/dlasy2.go | 292 + .../gonum.org/v1/gonum/lapack/gonum/dlatbs.go | 454 + .../gonum.org/v1/gonum/lapack/gonum/dlatdf.go | 175 + .../gonum.org/v1/gonum/lapack/gonum/dlatrd.go | 176 + .../gonum.org/v1/gonum/lapack/gonum/dlatrs.go | 410 + .../gonum.org/v1/gonum/lapack/gonum/dlauu2.go | 66 + .../gonum.org/v1/gonum/lapack/gonum/dlauum.go | 83 + vendor/gonum.org/v1/gonum/lapack/gonum/doc.go | 28 + .../gonum.org/v1/gonum/lapack/gonum/dorg2l.go | 78 + .../gonum.org/v1/gonum/lapack/gonum/dorg2r.go | 77 + .../gonum.org/v1/gonum/lapack/gonum/dorgbr.go | 138 + .../gonum.org/v1/gonum/lapack/gonum/dorghr.go | 103 + .../gonum.org/v1/gonum/lapack/gonum/dorgl2.go | 79 + .../gonum.org/v1/gonum/lapack/gonum/dorglq.go | 125 + .../gonum.org/v1/gonum/lapack/gonum/dorgql.go | 139 + .../gonum.org/v1/gonum/lapack/gonum/dorgqr.go | 136 + .../gonum.org/v1/gonum/lapack/gonum/dorgr2.go | 83 + .../gonum.org/v1/gonum/lapack/gonum/dorgtr.go | 106 + .../gonum.org/v1/gonum/lapack/gonum/dorm2r.go | 103 + .../gonum.org/v1/gonum/lapack/gonum/dormbr.go | 180 + .../gonum.org/v1/gonum/lapack/gonum/dormhr.go | 134 + .../gonum.org/v1/gonum/lapack/gonum/dorml2.go | 104 + .../gonum.org/v1/gonum/lapack/gonum/dormlq.go | 176 + .../gonum.org/v1/gonum/lapack/gonum/dormqr.go | 180 + .../gonum.org/v1/gonum/lapack/gonum/dormr2.go | 105 + .../gonum.org/v1/gonum/lapack/gonum/dpbcon.go | 111 + .../gonum.org/v1/gonum/lapack/gonum/dpbtf2.go | 114 + .../gonum.org/v1/gonum/lapack/gonum/dpbtrf.go | 216 + .../gonum.org/v1/gonum/lapack/gonum/dpbtrs.go | 69 + .../gonum.org/v1/gonum/lapack/gonum/dpocon.go | 90 + .../gonum.org/v1/gonum/lapack/gonum/dpotf2.go | 82 + .../gonum.org/v1/gonum/lapack/gonum/dpotrf.go | 81 + .../gonum.org/v1/gonum/lapack/gonum/dpotri.go | 44 + .../gonum.org/v1/gonum/lapack/gonum/dpotrs.go | 64 + .../gonum.org/v1/gonum/lapack/gonum/dpstf2.go | 202 + .../gonum.org/v1/gonum/lapack/gonum/dpstrf.go | 233 + .../gonum.org/v1/gonum/lapack/gonum/dptcon.go | 99 + .../gonum.org/v1/gonum/lapack/gonum/dptsv.go | 49 + .../gonum.org/v1/gonum/lapack/gonum/dpttrf.go | 80 + .../gonum.org/v1/gonum/lapack/gonum/dpttrs.go | 51 + .../gonum.org/v1/gonum/lapack/gonum/dptts2.go | 39 + .../gonum.org/v1/gonum/lapack/gonum/drscl.go | 63 + .../gonum.org/v1/gonum/lapack/gonum/dsteqr.go | 376 + .../gonum.org/v1/gonum/lapack/gonum/dsterf.go | 285 + .../gonum.org/v1/gonum/lapack/gonum/dsyev.go | 130 + .../gonum.org/v1/gonum/lapack/gonum/dsytd2.go | 147 + .../gonum.org/v1/gonum/lapack/gonum/dsytrd.go | 184 + .../gonum.org/v1/gonum/lapack/gonum/dtbtrs.go | 77 + .../gonum.org/v1/gonum/lapack/gonum/dtgsja.go | 389 + .../gonum.org/v1/gonum/lapack/gonum/dtrcon.go | 90 + .../v1/gonum/lapack/gonum/dtrevc3.go | 894 ++ .../gonum.org/v1/gonum/lapack/gonum/dtrexc.go | 230 + .../gonum.org/v1/gonum/lapack/gonum/dtrti2.go | 69 + .../gonum.org/v1/gonum/lapack/gonum/dtrtri.go | 72 + .../gonum.org/v1/gonum/lapack/gonum/dtrtrs.go | 55 + .../gonum.org/v1/gonum/lapack/gonum/errors.go | 183 + .../gonum.org/v1/gonum/lapack/gonum/iladlc.go | 45 + .../gonum.org/v1/gonum/lapack/gonum/iladlr.go | 41 + .../gonum.org/v1/gonum/lapack/gonum/ilaenv.go | 395 + .../gonum.org/v1/gonum/lapack/gonum/iparmq.go | 117 + .../gonum.org/v1/gonum/lapack/gonum/lapack.go | 64 + vendor/gonum.org/v1/gonum/lapack/lapack.go | 240 + .../gonum.org/v1/gonum/lapack/lapack64/doc.go | 20 + .../v1/gonum/lapack/lapack64/lapack64.go | 908 ++ vendor/gonum.org/v1/gonum/mat/README.md | 6 + vendor/gonum.org/v1/gonum/mat/band.go | 368 + vendor/gonum.org/v1/gonum/mat/cdense.go | 368 + vendor/gonum.org/v1/gonum/mat/cholesky.go | 1203 +++ vendor/gonum.org/v1/gonum/mat/cmatrix.go | 314 + vendor/gonum.org/v1/gonum/mat/consts.go | 15 + vendor/gonum.org/v1/gonum/mat/dense.go | 670 ++ .../v1/gonum/mat/dense_arithmetic.go | 877 ++ vendor/gonum.org/v1/gonum/mat/diagonal.go | 342 + vendor/gonum.org/v1/gonum/mat/doc.go | 200 + vendor/gonum.org/v1/gonum/mat/eigen.go | 450 + vendor/gonum.org/v1/gonum/mat/errors.go | 154 + vendor/gonum.org/v1/gonum/mat/format.go | 516 + vendor/gonum.org/v1/gonum/mat/gsvd.go | 436 + vendor/gonum.org/v1/gonum/mat/hogsvd.go | 239 + .../v1/gonum/mat/index_bound_checks.go | 398 + .../v1/gonum/mat/index_no_bound_checks.go | 400 + vendor/gonum.org/v1/gonum/mat/inner.go | 126 + vendor/gonum.org/v1/gonum/mat/io.go | 495 + vendor/gonum.org/v1/gonum/mat/lq.go | 305 + vendor/gonum.org/v1/gonum/mat/lu.go | 487 + vendor/gonum.org/v1/gonum/mat/matrix.go | 1000 ++ vendor/gonum.org/v1/gonum/mat/offset.go | 32 + .../v1/gonum/mat/offset_appengine.go | 40 + vendor/gonum.org/v1/gonum/mat/pool.go | 260 + vendor/gonum.org/v1/gonum/mat/product.go | 193 + vendor/gonum.org/v1/gonum/mat/qr.go | 349 + vendor/gonum.org/v1/gonum/mat/shadow.go | 243 + .../gonum.org/v1/gonum/mat/shadow_common.go | 54 + .../gonum.org/v1/gonum/mat/shadow_complex.go | 72 + vendor/gonum.org/v1/gonum/mat/solve.go | 124 + vendor/gonum.org/v1/gonum/mat/svd.go | 425 + vendor/gonum.org/v1/gonum/mat/symband.go | 312 + vendor/gonum.org/v1/gonum/mat/symmetric.go | 698 ++ vendor/gonum.org/v1/gonum/mat/triangular.go | 832 ++ vendor/gonum.org/v1/gonum/mat/triband.go | 694 ++ vendor/gonum.org/v1/gonum/mat/tridiag.go | 417 + vendor/gonum.org/v1/gonum/mat/vector.go | 855 ++ vendor/gonum.org/v1/gonum/mathext/README.md | 6 + vendor/gonum.org/v1/gonum/mathext/airy.go | 41 + vendor/gonum.org/v1/gonum/mathext/beta.go | 40 + vendor/gonum.org/v1/gonum/mathext/betainc.go | 33 + vendor/gonum.org/v1/gonum/mathext/digamma.go | 45 + vendor/gonum.org/v1/gonum/mathext/doc.go | 7 + .../gonum.org/v1/gonum/mathext/ell_carlson.go | 168 + .../v1/gonum/mathext/ell_complete.go | 355 + vendor/gonum.org/v1/gonum/mathext/erf.go | 91 + .../gonum.org/v1/gonum/mathext/gamma_inc.go | 58 + .../v1/gonum/mathext/gamma_inc_inv.go | 58 + .../v1/gonum/mathext/internal/amos/amos.go | 2136 ++++ .../v1/gonum/mathext/internal/amos/doc.go | 6 + .../mathext/internal/amos/staticcheck.conf | 1 + .../gonum/mathext/internal/cephes/cephes.go | 28 + .../v1/gonum/mathext/internal/cephes/doc.go | 6 + .../v1/gonum/mathext/internal/cephes/igam.go | 320 + .../v1/gonum/mathext/internal/cephes/igami.go | 155 + .../gonum/mathext/internal/cephes/incbeta.go | 312 + .../v1/gonum/mathext/internal/cephes/incbi.go | 247 + .../gonum/mathext/internal/cephes/lanczos.go | 153 + .../v1/gonum/mathext/internal/cephes/ndtri.go | 150 + .../gonum/mathext/internal/cephes/polevl.go | 84 + .../mathext/internal/cephes/staticcheck.conf | 1 + .../v1/gonum/mathext/internal/cephes/unity.go | 184 + .../v1/gonum/mathext/internal/cephes/zeta.go | 117 + .../v1/gonum/mathext/internal/gonum/beta.go | 58 + .../v1/gonum/mathext/internal/gonum/doc.go | 7 + .../v1/gonum/mathext/internal/gonum/gonum.go | 5 + vendor/gonum.org/v1/gonum/mathext/mvgamma.go | 32 + vendor/gonum.org/v1/gonum/mathext/roots.go | 181 + vendor/gonum.org/v1/gonum/mathext/zeta.go | 22 + vendor/gonum.org/v1/gonum/stat/README.md | 6 + .../gonum.org/v1/gonum/stat/combin/combin.go | 683 ++ vendor/gonum.org/v1/gonum/stat/combin/doc.go | 7 + .../v1/gonum/stat/distuv/alphastable.go | 113 + .../v1/gonum/stat/distuv/bernoulli.go | 141 + vendor/gonum.org/v1/gonum/stat/distuv/beta.go | 152 + .../v1/gonum/stat/distuv/binomial.go | 190 + .../v1/gonum/stat/distuv/categorical.go | 185 + vendor/gonum.org/v1/gonum/stat/distuv/chi.go | 125 + .../v1/gonum/stat/distuv/chisquared.go | 102 + .../v1/gonum/stat/distuv/constants.go | 28 + vendor/gonum.org/v1/gonum/stat/distuv/doc.go | 6 + .../v1/gonum/stat/distuv/exponential.go | 267 + vendor/gonum.org/v1/gonum/stat/distuv/f.go | 135 + .../gonum.org/v1/gonum/stat/distuv/gamma.go | 204 + .../gonum.org/v1/gonum/stat/distuv/general.go | 24 + .../gonum.org/v1/gonum/stat/distuv/gumbel.go | 119 + .../v1/gonum/stat/distuv/interfaces.go | 32 + .../v1/gonum/stat/distuv/inversegamma.go | 124 + .../gonum.org/v1/gonum/stat/distuv/laplace.go | 267 + .../v1/gonum/stat/distuv/logistic.go | 98 + .../v1/gonum/stat/distuv/lognormal.go | 114 + vendor/gonum.org/v1/gonum/stat/distuv/norm.go | 264 + .../gonum.org/v1/gonum/stat/distuv/pareto.go | 131 + .../gonum.org/v1/gonum/stat/distuv/poisson.go | 145 + .../v1/gonum/stat/distuv/statdist.go | 142 + .../v1/gonum/stat/distuv/studentst.go | 162 + .../v1/gonum/stat/distuv/triangle.go | 279 + .../gonum.org/v1/gonum/stat/distuv/uniform.go | 211 + .../gonum.org/v1/gonum/stat/distuv/weibull.go | 232 + vendor/gonum.org/v1/gonum/stat/doc.go | 6 + vendor/gonum.org/v1/gonum/stat/pca_cca.go | 317 + vendor/gonum.org/v1/gonum/stat/roc.go | 198 + vendor/gonum.org/v1/gonum/stat/stat.go | 1400 +++ vendor/gonum.org/v1/gonum/stat/statmat.go | 142 + .../protobuf/encoding/protojson/decode.go | 38 +- .../protobuf/encoding/protojson/doc.go | 4 +- .../protobuf/encoding/protojson/encode.go | 53 +- .../encoding/protojson/well_known_types.go | 71 +- .../protobuf/encoding/prototext/decode.go | 8 +- .../protobuf/encoding/prototext/encode.go | 18 +- .../protobuf/encoding/protowire/wire.go | 36 +- .../protobuf/internal/descfmt/stringer.go | 183 +- .../internal/editiondefaults/defaults.go | 12 + .../editiondefaults/editions_defaults.binpb | 4 + .../protobuf/internal/encoding/json/decode.go | 4 +- .../protobuf/internal/encoding/json/encode.go | 10 +- .../protobuf/internal/encoding/text/decode.go | 5 +- .../internal/encoding/text/decode_number.go | 43 +- .../protobuf/internal/encoding/text/encode.go | 10 +- .../protobuf/internal/filedesc/desc.go | 102 +- .../protobuf/internal/filedesc/desc_init.go | 52 + .../protobuf/internal/filedesc/desc_lazy.go | 28 + .../protobuf/internal/filedesc/editions.go | 142 + .../protobuf/internal/genid/descriptor_gen.go | 458 +- .../internal/genid/go_features_gen.go | 31 + .../protobuf/internal/genid/struct_gen.go | 5 + .../protobuf/internal/genid/type_gen.go | 44 + .../protobuf/internal/impl/codec_extension.go | 22 +- .../protobuf/internal/impl/codec_gen.go | 113 +- .../protobuf/internal/impl/codec_tables.go | 2 +- .../protobuf/internal/impl/convert.go | 1 - .../protobuf/internal/impl/legacy_message.go | 19 +- .../protobuf/internal/impl/message.go | 17 +- .../internal/impl/message_reflect_field.go | 2 +- .../protobuf/internal/impl/pointer_reflect.go | 36 + .../protobuf/internal/impl/pointer_unsafe.go | 40 + .../protobuf/internal/order/order.go | 2 +- .../protobuf/internal/strs/strings.go | 2 +- ...ings_unsafe.go => strings_unsafe_go120.go} | 6 +- .../internal/strs/strings_unsafe_go121.go | 74 + .../protobuf/internal/version/version.go | 4 +- .../protobuf/proto/decode.go | 2 +- .../google.golang.org/protobuf/proto/doc.go | 67 +- .../protobuf/proto/encode.go | 2 +- .../google.golang.org/protobuf/proto/equal.go | 172 +- .../protobuf/proto/extension.go | 2 +- .../google.golang.org/protobuf/proto/merge.go | 2 +- .../google.golang.org/protobuf/proto/proto.go | 18 +- .../google.golang.org/protobuf/proto/size.go | 10 +- .../protobuf/reflect/protoreflect/proto.go | 85 +- .../reflect/protoreflect/source_gen.go | 95 +- .../protobuf/reflect/protoreflect/type.go | 44 +- .../protobuf/reflect/protoreflect/value.go | 26 +- .../reflect/protoreflect/value_equal.go | 168 + .../reflect/protoreflect/value_union.go | 48 +- ...{value_unsafe.go => value_unsafe_go120.go} | 4 +- .../protoreflect/value_unsafe_go121.go | 87 + .../reflect/protoregistry/registry.go | 26 +- .../protobuf/types/known/anypb/any.pb.go | 136 +- .../types/known/durationpb/duration.pb.go | 374 + .../types/known/timestamppb/timestamp.pb.go | 383 + vendor/modules.txt | 224 +- zap_development_handler.go | 23 +- zap_development_handler_test.go | 26 +- 1542 files changed, 338685 insertions(+), 12468 deletions(-) create mode 100644 .github/workflows/release-dev.yml create mode 100644 .goreleaser-dev.yaml rename goreleaser.yaml => .goreleaser.yaml (90%) create mode 100644 .vscode/launch.json create mode 100644 cmd/humanlog/account.go create mode 100644 cmd/humanlog/auth.go create mode 100644 cmd/humanlog/genny.go create mode 100644 cmd/humanlog/helper.go create mode 100644 cmd/humanlog/ingest.go create mode 100644 cmd/humanlog/localhost.go create mode 100644 cmd/humanlog/machine.go create mode 100644 cmd/humanlog/organization.go create mode 100644 cmd/humanlog/query.go create mode 100644 internal/localstorage/memory.go create mode 100644 internal/localstorage/memory_test.go create mode 100644 internal/localstorage/queryable.go create mode 100644 internal/localsvc/svc.go create mode 100644 internal/localsvc/svc_test.go create mode 100644 internal/pkg/iterapi/iter.go delete mode 100644 internal/pkg/model/types.go create mode 100644 pkg/auth/interceptors.go create mode 100644 pkg/auth/token_source.go create mode 100644 pkg/retry/retry.go create mode 100644 pkg/sink/logsvcsink/bidistream_sink.go create mode 100644 pkg/sink/logsvcsink/stream_sink.go create mode 100644 pkg/sink/logsvcsink/unary_sink.go create mode 100644 pkg/sink/teesink/tee.go create mode 100644 pkg/tui/accounts.go create mode 100644 pkg/tui/components/keyhandler/keyhandler.go create mode 100644 pkg/tui/components/keyhandler/main.go create mode 100644 pkg/tui/components/modal/main.go create mode 100644 pkg/tui/components/modal/mode_selector.go create mode 100644 pkg/tui/components/querybar/main.go create mode 100644 pkg/tui/components/querybar/querybar.go create mode 100644 pkg/tui/machines.go create mode 100644 pkg/tui/organizations.go create mode 100644 pkg/tui/querier.go create mode 100644 pkg/tui/tui.go create mode 100644 vendor/connectrpc.com/connect/.gitignore rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/.golangci.yml (63%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/LICENSE (99%) create mode 100644 vendor/connectrpc.com/connect/MAINTAINERS.md create mode 100644 vendor/connectrpc.com/connect/Makefile rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/README.md (57%) create mode 100644 vendor/connectrpc.com/connect/RELEASE.md create mode 100644 vendor/connectrpc.com/connect/SECURITY.md rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/buf.gen.yaml (78%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/buf.work.yaml (100%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/buffer_pool.go (96%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/client.go (77%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/client_stream.go (90%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/code.go (98%) create mode 100644 vendor/connectrpc.com/connect/codec.go rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/compression.go (94%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/connect.go (57%) create mode 100644 vendor/connectrpc.com/connect/duplex_http_call.go create mode 100644 vendor/connectrpc.com/connect/envelope.go rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/error.go (69%) create mode 100644 vendor/connectrpc.com/connect/error_writer.go rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/handler.go (71%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/handler_stream.go (84%) create mode 100644 vendor/connectrpc.com/connect/header.go create mode 100644 vendor/connectrpc.com/connect/idempotency_level.go rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/interceptor.go (98%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/internal/gen/connectext/grpc/status/v1/status.pb.go (85%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/option.go (67%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/protobuf_util.go (90%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/protocol.go (69%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/protocol_connect.go (58%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/protocol_grpc.go (60%) rename vendor/{github.com/bufbuild/connect-go => connectrpc.com/connect}/recover.go (98%) create mode 100644 vendor/connectrpc.com/cors/.gitignore create mode 100644 vendor/connectrpc.com/cors/.golangci.yml create mode 100644 vendor/connectrpc.com/cors/LICENSE create mode 100644 vendor/connectrpc.com/cors/MAINTAINERS.md create mode 100644 vendor/connectrpc.com/cors/Makefile create mode 100644 vendor/connectrpc.com/cors/README.md create mode 100644 vendor/connectrpc.com/cors/SECURITY.md create mode 100644 vendor/connectrpc.com/cors/cors.go create mode 100644 vendor/github.com/99designs/go-keychain/.gitignore create mode 100644 vendor/github.com/99designs/go-keychain/.golangci.yml create mode 100644 vendor/github.com/99designs/go-keychain/.travis.yml create mode 100644 vendor/github.com/99designs/go-keychain/LICENSE create mode 100644 vendor/github.com/99designs/go-keychain/README.md create mode 100644 vendor/github.com/99designs/go-keychain/corefoundation.go create mode 100644 vendor/github.com/99designs/go-keychain/datetime.go create mode 100644 vendor/github.com/99designs/go-keychain/ios.go create mode 100644 vendor/github.com/99designs/go-keychain/keychain.go create mode 100644 vendor/github.com/99designs/go-keychain/macos.go create mode 100644 vendor/github.com/99designs/go-keychain/util.go create mode 100644 vendor/github.com/99designs/keyring/.gitattributes create mode 100644 vendor/github.com/99designs/keyring/.gitignore create mode 100644 vendor/github.com/99designs/keyring/.golangci.yml create mode 100644 vendor/github.com/99designs/keyring/LICENSE create mode 100644 vendor/github.com/99designs/keyring/README.md create mode 100644 vendor/github.com/99designs/keyring/Vagrantfile create mode 100644 vendor/github.com/99designs/keyring/array.go create mode 100644 vendor/github.com/99designs/keyring/config.go create mode 100644 vendor/github.com/99designs/keyring/docker-compose.yml create mode 100644 vendor/github.com/99designs/keyring/file.go create mode 100644 vendor/github.com/99designs/keyring/keychain.go create mode 100644 vendor/github.com/99designs/keyring/keyctl.go create mode 100644 vendor/github.com/99designs/keyring/keyring.go create mode 100644 vendor/github.com/99designs/keyring/kwallet.go create mode 100644 vendor/github.com/99designs/keyring/pass.go create mode 100644 vendor/github.com/99designs/keyring/prompt.go create mode 100644 vendor/github.com/99designs/keyring/secretservice.go create mode 100644 vendor/github.com/99designs/keyring/tilde.go create mode 100644 vendor/github.com/99designs/keyring/wincred.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/LICENSE.txt create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/canvas/buffer/buffer.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/canvas/canvas.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/canvas/graph/graph.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/canvas/options.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/canvas/runes/runes.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/canvas/updatehandler.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/linechart/linechart.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/linechart/options.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/options.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/timeserieslinechart.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/updatehandler.go create mode 100644 vendor/github.com/NimbleMarkets/ntcharts/linechart/updatehandler.go create mode 100644 vendor/github.com/atotto/clipboard/.travis.yml create mode 100644 vendor/github.com/atotto/clipboard/LICENSE create mode 100644 vendor/github.com/atotto/clipboard/README.md create mode 100644 vendor/github.com/atotto/clipboard/clipboard.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_darwin.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_plan9.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_unix.go create mode 100644 vendor/github.com/atotto/clipboard/clipboard_windows.go create mode 100644 vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE create mode 100644 vendor/github.com/aymanbagabas/go-osc52/v2/README.md create mode 100644 vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go delete mode 100644 vendor/github.com/bufbuild/connect-go/.gitignore delete mode 100644 vendor/github.com/bufbuild/connect-go/Makefile delete mode 100644 vendor/github.com/bufbuild/connect-go/codec.go delete mode 100644 vendor/github.com/bufbuild/connect-go/duplex_http_call.go delete mode 100644 vendor/github.com/bufbuild/connect-go/envelope.go delete mode 100644 vendor/github.com/bufbuild/connect-go/error_writer.go delete mode 100644 vendor/github.com/bufbuild/connect-go/header.go delete mode 100644 vendor/github.com/bufbuild/connect-go/maxbytes.go delete mode 100644 vendor/github.com/bufbuild/connect-go/maxbytes_go118.go create mode 100644 vendor/github.com/catppuccin/go/.editorconfig create mode 100644 vendor/github.com/catppuccin/go/.gitignore create mode 100644 vendor/github.com/catppuccin/go/.goreleaser.yaml create mode 100644 vendor/github.com/catppuccin/go/LICENSE create mode 100644 vendor/github.com/catppuccin/go/README.md create mode 100644 vendor/github.com/catppuccin/go/frappe.go create mode 100644 vendor/github.com/catppuccin/go/latte.go create mode 100644 vendor/github.com/catppuccin/go/macchiato.go create mode 100644 vendor/github.com/catppuccin/go/main.go create mode 100644 vendor/github.com/catppuccin/go/mocha.go create mode 100644 vendor/github.com/charmbracelet/bubbles/LICENSE create mode 100644 vendor/github.com/charmbracelet/bubbles/cursor/cursor.go create mode 100644 vendor/github.com/charmbracelet/bubbles/filepicker/filepicker.go create mode 100644 vendor/github.com/charmbracelet/bubbles/filepicker/hidden_unix.go create mode 100644 vendor/github.com/charmbracelet/bubbles/filepicker/hidden_windows.go create mode 100644 vendor/github.com/charmbracelet/bubbles/help/help.go create mode 100644 vendor/github.com/charmbracelet/bubbles/key/key.go create mode 100644 vendor/github.com/charmbracelet/bubbles/paginator/paginator.go create mode 100644 vendor/github.com/charmbracelet/bubbles/runeutil/runeutil.go create mode 100644 vendor/github.com/charmbracelet/bubbles/table/table.go create mode 100644 vendor/github.com/charmbracelet/bubbles/textarea/memoization/memoization.go create mode 100644 vendor/github.com/charmbracelet/bubbles/textarea/textarea.go create mode 100644 vendor/github.com/charmbracelet/bubbles/textinput/textinput.go create mode 100644 vendor/github.com/charmbracelet/bubbles/viewport/keymap.go create mode 100644 vendor/github.com/charmbracelet/bubbles/viewport/viewport.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/.gitattributes create mode 100644 vendor/github.com/charmbracelet/bubbletea/.gitignore create mode 100644 vendor/github.com/charmbracelet/bubbletea/.golangci-soft.yml create mode 100644 vendor/github.com/charmbracelet/bubbletea/.golangci.yml create mode 100644 vendor/github.com/charmbracelet/bubbletea/.goreleaser.yml create mode 100644 vendor/github.com/charmbracelet/bubbletea/CONTRIBUTING.md create mode 100644 vendor/github.com/charmbracelet/bubbletea/LICENSE create mode 100644 vendor/github.com/charmbracelet/bubbletea/README.md create mode 100644 vendor/github.com/charmbracelet/bubbletea/commands.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/exec.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/focus.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/inputreader_other.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/key.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/key_other.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/key_sequences.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/key_windows.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/logging.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/mouse.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/nil_renderer.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/options.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/renderer.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/screen.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/signals_unix.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/signals_windows.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/standard_renderer.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/tea.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/tea_init.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/tty.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/tty_unix.go create mode 100644 vendor/github.com/charmbracelet/bubbletea/tty_windows.go create mode 100644 vendor/github.com/charmbracelet/huh/.gitattributes create mode 100644 vendor/github.com/charmbracelet/huh/.gitignore create mode 100644 vendor/github.com/charmbracelet/huh/.golangci-soft.yml create mode 100644 vendor/github.com/charmbracelet/huh/.golangci.yml create mode 100644 vendor/github.com/charmbracelet/huh/CONTRIBUTING.md create mode 100644 vendor/github.com/charmbracelet/huh/LICENSE create mode 100644 vendor/github.com/charmbracelet/huh/Makefile create mode 100644 vendor/github.com/charmbracelet/huh/README.md create mode 100644 vendor/github.com/charmbracelet/huh/SECURITY.md create mode 100644 vendor/github.com/charmbracelet/huh/accessibility/accessibility.go create mode 100644 vendor/github.com/charmbracelet/huh/clamp.go create mode 100644 vendor/github.com/charmbracelet/huh/field_confirm.go create mode 100644 vendor/github.com/charmbracelet/huh/field_filepicker.go create mode 100644 vendor/github.com/charmbracelet/huh/field_input.go create mode 100644 vendor/github.com/charmbracelet/huh/field_multiselect.go create mode 100644 vendor/github.com/charmbracelet/huh/field_note.go create mode 100644 vendor/github.com/charmbracelet/huh/field_select.go create mode 100644 vendor/github.com/charmbracelet/huh/field_text.go create mode 100644 vendor/github.com/charmbracelet/huh/form.go create mode 100644 vendor/github.com/charmbracelet/huh/group.go create mode 100644 vendor/github.com/charmbracelet/huh/keymap.go create mode 100644 vendor/github.com/charmbracelet/huh/option.go create mode 100644 vendor/github.com/charmbracelet/huh/run.go create mode 100644 vendor/github.com/charmbracelet/huh/theme.go create mode 100644 vendor/github.com/charmbracelet/huh/validate.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/.gitignore create mode 100644 vendor/github.com/charmbracelet/lipgloss/.golangci-soft.yml create mode 100644 vendor/github.com/charmbracelet/lipgloss/.golangci.yml create mode 100644 vendor/github.com/charmbracelet/lipgloss/.goreleaser.yml create mode 100644 vendor/github.com/charmbracelet/lipgloss/LICENSE create mode 100644 vendor/github.com/charmbracelet/lipgloss/README.md create mode 100644 vendor/github.com/charmbracelet/lipgloss/align.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/ansi_unix.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/ansi_windows.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/borders.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/color.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/get.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/join.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/position.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/renderer.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/runes.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/set.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/size.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/style.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/tree/children.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/tree/enumerator.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/tree/renderer.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/tree/tree.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/unset.go create mode 100644 vendor/github.com/charmbracelet/lipgloss/whitespace.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/LICENSE create mode 100644 vendor/github.com/charmbracelet/x/ansi/ansi.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/ascii.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/background.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/c0.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/c1.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/clipboard.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/color.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/csi.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/ctrl.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/cursor.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/dcs.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/doc.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/hyperlink.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/kitty.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/mode.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/osc.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/params.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/parser.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/parser/const.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/parser/seq.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/parser_decode.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/parser_sync.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/passthrough.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/screen.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/sequence.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/style.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/termcap.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/title.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/truncate.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/util.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/width.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/wrap.go create mode 100644 vendor/github.com/charmbracelet/x/ansi/xterm.go create mode 100644 vendor/github.com/charmbracelet/x/exp/strings/LICENSE create mode 100644 vendor/github.com/charmbracelet/x/exp/strings/join.go create mode 100644 vendor/github.com/charmbracelet/x/term/LICENSE create mode 100644 vendor/github.com/charmbracelet/x/term/term.go create mode 100644 vendor/github.com/charmbracelet/x/term/term_other.go create mode 100644 vendor/github.com/charmbracelet/x/term/term_unix.go create mode 100644 vendor/github.com/charmbracelet/x/term/term_unix_bsd.go create mode 100644 vendor/github.com/charmbracelet/x/term/term_unix_other.go create mode 100644 vendor/github.com/charmbracelet/x/term/term_windows.go create mode 100644 vendor/github.com/charmbracelet/x/term/terminal.go create mode 100644 vendor/github.com/charmbracelet/x/term/util.go create mode 100644 vendor/github.com/danieljoos/wincred/.gitattributes create mode 100644 vendor/github.com/danieljoos/wincred/.gitignore create mode 100644 vendor/github.com/danieljoos/wincred/LICENSE create mode 100644 vendor/github.com/danieljoos/wincred/README.md create mode 100644 vendor/github.com/danieljoos/wincred/conversion.go create mode 100644 vendor/github.com/danieljoos/wincred/conversion_unsupported.go create mode 100644 vendor/github.com/danieljoos/wincred/sys.go create mode 100644 vendor/github.com/danieljoos/wincred/sys_unsupported.go create mode 100644 vendor/github.com/danieljoos/wincred/types.go create mode 100644 vendor/github.com/danieljoos/wincred/wincred.go create mode 100644 vendor/github.com/dustin/go-humanize/.travis.yml create mode 100644 vendor/github.com/dustin/go-humanize/LICENSE create mode 100644 vendor/github.com/dustin/go-humanize/README.markdown create mode 100644 vendor/github.com/dustin/go-humanize/big.go create mode 100644 vendor/github.com/dustin/go-humanize/bigbytes.go create mode 100644 vendor/github.com/dustin/go-humanize/bytes.go create mode 100644 vendor/github.com/dustin/go-humanize/comma.go create mode 100644 vendor/github.com/dustin/go-humanize/commaf.go create mode 100644 vendor/github.com/dustin/go-humanize/ftoa.go create mode 100644 vendor/github.com/dustin/go-humanize/humanize.go create mode 100644 vendor/github.com/dustin/go-humanize/number.go create mode 100644 vendor/github.com/dustin/go-humanize/ordinals.go create mode 100644 vendor/github.com/dustin/go-humanize/si.go create mode 100644 vendor/github.com/dustin/go-humanize/times.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/.gitignore create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/LICENSE create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/README.md create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/aes/ecb.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/aes/key_wrap.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/aes_cbc_hmac.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/aes_gcm.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/aes_gcm_kw.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/aeskw.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/arrays/arrays.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/base64url/base64url.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/compact/compact.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/deflate.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/direct.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/ecdh.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/ecdh_aeskw.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/ecdsa_using_sha.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/hmac.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/hmac_using_sha.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/jose.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/kdf/nist_sp800_56a.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/kdf/pbkdf2.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_cert.pem create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.key create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.pem create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_public.key create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ecc.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/padding/align.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/padding/pkcs7.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/pbse2_hmac_aeskw.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/plaintext.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/rsa_oaep.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/rsa_pkcs1v15.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/rsa_using_sha.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/rsapss_using_sha.go create mode 100644 vendor/github.com/dvsekhvalnov/jose2go/sha.go create mode 100644 vendor/github.com/erikgeiser/coninput/.gitignore create mode 100644 vendor/github.com/erikgeiser/coninput/.golangci.yml create mode 100644 vendor/github.com/erikgeiser/coninput/LICENSE create mode 100644 vendor/github.com/erikgeiser/coninput/README.md create mode 100644 vendor/github.com/erikgeiser/coninput/keycodes.go create mode 100644 vendor/github.com/erikgeiser/coninput/mode.go create mode 100644 vendor/github.com/erikgeiser/coninput/read.go create mode 100644 vendor/github.com/erikgeiser/coninput/records.go create mode 100644 vendor/github.com/fatih/color/color_windows.go create mode 100644 vendor/github.com/godbus/dbus/.travis.yml create mode 100644 vendor/github.com/godbus/dbus/CONTRIBUTING.md create mode 100644 vendor/github.com/godbus/dbus/LICENSE create mode 100644 vendor/github.com/godbus/dbus/MAINTAINERS create mode 100644 vendor/github.com/godbus/dbus/README.markdown create mode 100644 vendor/github.com/godbus/dbus/auth.go create mode 100644 vendor/github.com/godbus/dbus/auth_anonymous.go create mode 100644 vendor/github.com/godbus/dbus/auth_external.go create mode 100644 vendor/github.com/godbus/dbus/auth_sha1.go create mode 100644 vendor/github.com/godbus/dbus/call.go create mode 100644 vendor/github.com/godbus/dbus/conn.go create mode 100644 vendor/github.com/godbus/dbus/conn_darwin.go create mode 100644 vendor/github.com/godbus/dbus/conn_other.go create mode 100644 vendor/github.com/godbus/dbus/conn_unix.go create mode 100644 vendor/github.com/godbus/dbus/conn_windows.go create mode 100644 vendor/github.com/godbus/dbus/dbus.go create mode 100644 vendor/github.com/godbus/dbus/decoder.go create mode 100644 vendor/github.com/godbus/dbus/default_handler.go create mode 100644 vendor/github.com/godbus/dbus/doc.go create mode 100644 vendor/github.com/godbus/dbus/encoder.go create mode 100644 vendor/github.com/godbus/dbus/export.go create mode 100644 vendor/github.com/godbus/dbus/homedir.go create mode 100644 vendor/github.com/godbus/dbus/homedir_dynamic.go create mode 100644 vendor/github.com/godbus/dbus/homedir_static.go create mode 100644 vendor/github.com/godbus/dbus/match.go create mode 100644 vendor/github.com/godbus/dbus/message.go create mode 100644 vendor/github.com/godbus/dbus/object.go create mode 100644 vendor/github.com/godbus/dbus/server_interfaces.go create mode 100644 vendor/github.com/godbus/dbus/sig.go create mode 100644 vendor/github.com/godbus/dbus/transport_darwin.go create mode 100644 vendor/github.com/godbus/dbus/transport_generic.go create mode 100644 vendor/github.com/godbus/dbus/transport_nonce_tcp.go create mode 100644 vendor/github.com/godbus/dbus/transport_tcp.go create mode 100644 vendor/github.com/godbus/dbus/transport_unix.go create mode 100644 vendor/github.com/godbus/dbus/transport_unixcred_dragonfly.go create mode 100644 vendor/github.com/godbus/dbus/transport_unixcred_freebsd.go create mode 100644 vendor/github.com/godbus/dbus/transport_unixcred_linux.go create mode 100644 vendor/github.com/godbus/dbus/transport_unixcred_openbsd.go create mode 100644 vendor/github.com/godbus/dbus/variant.go create mode 100644 vendor/github.com/godbus/dbus/variant_lexer.go create mode 100644 vendor/github.com/godbus/dbus/variant_parser.go create mode 100644 vendor/github.com/google/uuid/CHANGELOG.md create mode 100644 vendor/github.com/google/uuid/CONTRIBUTING.md create mode 100644 vendor/github.com/google/uuid/CONTRIBUTORS create mode 100644 vendor/github.com/google/uuid/LICENSE create mode 100644 vendor/github.com/google/uuid/README.md create mode 100644 vendor/github.com/google/uuid/dce.go create mode 100644 vendor/github.com/google/uuid/doc.go create mode 100644 vendor/github.com/google/uuid/hash.go create mode 100644 vendor/github.com/google/uuid/marshal.go create mode 100644 vendor/github.com/google/uuid/node.go create mode 100644 vendor/github.com/google/uuid/node_js.go create mode 100644 vendor/github.com/google/uuid/node_net.go create mode 100644 vendor/github.com/google/uuid/null.go create mode 100644 vendor/github.com/google/uuid/sql.go create mode 100644 vendor/github.com/google/uuid/time.go create mode 100644 vendor/github.com/google/uuid/util.go create mode 100644 vendor/github.com/google/uuid/uuid.go create mode 100644 vendor/github.com/google/uuid/version1.go create mode 100644 vendor/github.com/google/uuid/version4.go create mode 100644 vendor/github.com/google/uuid/version6.go create mode 100644 vendor/github.com/google/uuid/version7.go create mode 100644 vendor/github.com/gsterjov/go-libsecret/LICENSE create mode 100644 vendor/github.com/gsterjov/go-libsecret/collection.go create mode 100644 vendor/github.com/gsterjov/go-libsecret/item.go create mode 100644 vendor/github.com/gsterjov/go-libsecret/prompt.go create mode 100644 vendor/github.com/gsterjov/go-libsecret/secret.go create mode 100644 vendor/github.com/gsterjov/go-libsecret/service.go create mode 100644 vendor/github.com/gsterjov/go-libsecret/session.go create mode 100644 vendor/github.com/humanlogio/api/go/pkg/lang/parse.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/account/v1/accountv1connect/service.connect.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/account/v1/service.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/auth/v1/authv1connect/service.connect.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/auth/v1/service.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/ingest/v1/ingestv1connect/service.connect.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/ingest/v1/service.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/localhost/v1/localhostv1connect/service.connect.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/localhost/v1/service.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/organization/v1/organizationv1connect/service.connect.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/organization/v1/service.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/query/v1/queryv1connect/service.connect.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/query/v1/service.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/token/v1/service.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/token/v1/tokenv1connect/service.connect.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/user/v1/service.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/svc/user/v1/userv1connect/service.connect.go create mode 100644 vendor/github.com/humanlogio/api/go/types/v1/account_token.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/types/v1/logevent.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/types/v1/logquery.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/types/v1/organization.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/types/v1/session.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/types/v1/user.pb.go create mode 100644 vendor/github.com/humanlogio/api/go/types/v1/user_token.pb.go create mode 100644 vendor/github.com/lrstanley/bubblezone/.editorconfig create mode 100644 vendor/github.com/lrstanley/bubblezone/.gitignore create mode 100644 vendor/github.com/lrstanley/bubblezone/.golangci.yml create mode 100644 vendor/github.com/lrstanley/bubblezone/LICENSE create mode 100644 vendor/github.com/lrstanley/bubblezone/Makefile create mode 100644 vendor/github.com/lrstanley/bubblezone/README.md create mode 100644 vendor/github.com/lrstanley/bubblezone/manager.go create mode 100644 vendor/github.com/lrstanley/bubblezone/manager_global.go create mode 100644 vendor/github.com/lrstanley/bubblezone/messages.go create mode 100644 vendor/github.com/lrstanley/bubblezone/scanner.go create mode 100644 vendor/github.com/lrstanley/bubblezone/zoneinfo.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/.gitignore create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/CHANGELOG.md create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/LICENSE create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/README.md create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/colorgens.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/colors.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/hsluv-snapshot-rev4.json create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/hsluv.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go create mode 100644 vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go create mode 100644 vendor/github.com/matoous/go-nanoid/.gitignore create mode 100644 vendor/github.com/matoous/go-nanoid/.goreleaser.yml create mode 100644 vendor/github.com/matoous/go-nanoid/LICENSE create mode 100644 vendor/github.com/matoous/go-nanoid/Makefile create mode 100644 vendor/github.com/matoous/go-nanoid/README.md create mode 100644 vendor/github.com/matoous/go-nanoid/gonanoid.go create mode 100644 vendor/github.com/mattn/go-localereader/README.md create mode 100644 vendor/github.com/mattn/go-localereader/localereader.go create mode 100644 vendor/github.com/mattn/go-localereader/localereader_unix.go create mode 100644 vendor/github.com/mattn/go-localereader/localereader_windows.go create mode 100644 vendor/github.com/mattn/go-runewidth/LICENSE create mode 100644 vendor/github.com/mattn/go-runewidth/README.md create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_appengine.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_js.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_posix.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_table.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_windows.go create mode 100644 vendor/github.com/mtibben/percent/LICENSE create mode 100644 vendor/github.com/mtibben/percent/README.md create mode 100644 vendor/github.com/mtibben/percent/percent.go create mode 100644 vendor/github.com/muesli/ansi/.gitignore create mode 100644 vendor/github.com/muesli/ansi/.golangci.yml create mode 100644 vendor/github.com/muesli/ansi/LICENSE create mode 100644 vendor/github.com/muesli/ansi/README.md create mode 100644 vendor/github.com/muesli/ansi/ansi.go create mode 100644 vendor/github.com/muesli/ansi/buffer.go create mode 100644 vendor/github.com/muesli/ansi/compressor/writer.go create mode 100644 vendor/github.com/muesli/ansi/writer.go create mode 100644 vendor/github.com/muesli/cancelreader/.gitignore create mode 100644 vendor/github.com/muesli/cancelreader/.golangci-soft.yml create mode 100644 vendor/github.com/muesli/cancelreader/.golangci.yml create mode 100644 vendor/github.com/muesli/cancelreader/LICENSE create mode 100644 vendor/github.com/muesli/cancelreader/README.md create mode 100644 vendor/github.com/muesli/cancelreader/cancelreader.go create mode 100644 vendor/github.com/muesli/cancelreader/cancelreader_bsd.go create mode 100644 vendor/github.com/muesli/cancelreader/cancelreader_default.go create mode 100644 vendor/github.com/muesli/cancelreader/cancelreader_linux.go create mode 100644 vendor/github.com/muesli/cancelreader/cancelreader_select.go create mode 100644 vendor/github.com/muesli/cancelreader/cancelreader_unix.go create mode 100644 vendor/github.com/muesli/cancelreader/cancelreader_windows.go create mode 100644 vendor/github.com/muesli/termenv/.gitignore create mode 100644 vendor/github.com/muesli/termenv/.golangci-soft.yml create mode 100644 vendor/github.com/muesli/termenv/.golangci.yml create mode 100644 vendor/github.com/muesli/termenv/LICENSE create mode 100644 vendor/github.com/muesli/termenv/README.md create mode 100644 vendor/github.com/muesli/termenv/ansi_compat.md create mode 100644 vendor/github.com/muesli/termenv/ansicolors.go create mode 100644 vendor/github.com/muesli/termenv/color.go create mode 100644 vendor/github.com/muesli/termenv/constants_linux.go create mode 100644 vendor/github.com/muesli/termenv/constants_solaris.go create mode 100644 vendor/github.com/muesli/termenv/constants_unix.go create mode 100644 vendor/github.com/muesli/termenv/copy.go create mode 100644 vendor/github.com/muesli/termenv/hyperlink.go create mode 100644 vendor/github.com/muesli/termenv/notification.go create mode 100644 vendor/github.com/muesli/termenv/output.go create mode 100644 vendor/github.com/muesli/termenv/profile.go create mode 100644 vendor/github.com/muesli/termenv/screen.go create mode 100644 vendor/github.com/muesli/termenv/style.go create mode 100644 vendor/github.com/muesli/termenv/templatehelper.go create mode 100644 vendor/github.com/muesli/termenv/termenv.go create mode 100644 vendor/github.com/muesli/termenv/termenv_other.go create mode 100644 vendor/github.com/muesli/termenv/termenv_posix.go create mode 100644 vendor/github.com/muesli/termenv/termenv_solaris.go create mode 100644 vendor/github.com/muesli/termenv/termenv_unix.go create mode 100644 vendor/github.com/muesli/termenv/termenv_windows.go create mode 100644 vendor/github.com/pkg/browser/LICENSE create mode 100644 vendor/github.com/pkg/browser/README.md create mode 100644 vendor/github.com/pkg/browser/browser.go create mode 100644 vendor/github.com/pkg/browser/browser_darwin.go create mode 100644 vendor/github.com/pkg/browser/browser_freebsd.go create mode 100644 vendor/github.com/pkg/browser/browser_linux.go create mode 100644 vendor/github.com/pkg/browser/browser_netbsd.go create mode 100644 vendor/github.com/pkg/browser/browser_openbsd.go create mode 100644 vendor/github.com/pkg/browser/browser_unsupported.go create mode 100644 vendor/github.com/pkg/browser/browser_windows.go create mode 100644 vendor/github.com/rivo/uniseg/LICENSE.txt create mode 100644 vendor/github.com/rivo/uniseg/README.md create mode 100644 vendor/github.com/rivo/uniseg/doc.go create mode 100644 vendor/github.com/rivo/uniseg/eastasianwidth.go create mode 100644 vendor/github.com/rivo/uniseg/emojipresentation.go create mode 100644 vendor/github.com/rivo/uniseg/gen_breaktest.go create mode 100644 vendor/github.com/rivo/uniseg/gen_properties.go create mode 100644 vendor/github.com/rivo/uniseg/grapheme.go create mode 100644 vendor/github.com/rivo/uniseg/graphemeproperties.go create mode 100644 vendor/github.com/rivo/uniseg/graphemerules.go create mode 100644 vendor/github.com/rivo/uniseg/line.go create mode 100644 vendor/github.com/rivo/uniseg/lineproperties.go create mode 100644 vendor/github.com/rivo/uniseg/linerules.go create mode 100644 vendor/github.com/rivo/uniseg/properties.go create mode 100644 vendor/github.com/rivo/uniseg/sentence.go create mode 100644 vendor/github.com/rivo/uniseg/sentenceproperties.go create mode 100644 vendor/github.com/rivo/uniseg/sentencerules.go create mode 100644 vendor/github.com/rivo/uniseg/step.go create mode 100644 vendor/github.com/rivo/uniseg/width.go create mode 100644 vendor/github.com/rivo/uniseg/word.go create mode 100644 vendor/github.com/rivo/uniseg/wordproperties.go create mode 100644 vendor/github.com/rivo/uniseg/wordrules.go create mode 100644 vendor/github.com/rs/cors/LICENSE create mode 100644 vendor/github.com/rs/cors/README.md create mode 100644 vendor/github.com/rs/cors/cors.go create mode 100644 vendor/github.com/rs/cors/internal/sortedset.go create mode 100644 vendor/github.com/rs/cors/utils.go delete mode 100644 vendor/github.com/urfave/cli/appveyor.yml create mode 100644 vendor/golang.org/x/exp/LICENSE create mode 100644 vendor/golang.org/x/exp/PATENTS create mode 100644 vendor/golang.org/x/exp/rand/exp.go create mode 100644 vendor/golang.org/x/exp/rand/normal.go create mode 100644 vendor/golang.org/x/exp/rand/rand.go create mode 100644 vendor/golang.org/x/exp/rand/rng.go create mode 100644 vendor/golang.org/x/exp/rand/zipf.go create mode 100644 vendor/golang.org/x/net/LICENSE create mode 100644 vendor/golang.org/x/net/PATENTS create mode 100644 vendor/golang.org/x/net/http/httpguts/guts.go create mode 100644 vendor/golang.org/x/net/http/httpguts/httplex.go create mode 100644 vendor/golang.org/x/net/http2/.gitignore create mode 100644 vendor/golang.org/x/net/http2/ascii.go create mode 100644 vendor/golang.org/x/net/http2/ciphers.go create mode 100644 vendor/golang.org/x/net/http2/client_conn_pool.go create mode 100644 vendor/golang.org/x/net/http2/databuffer.go create mode 100644 vendor/golang.org/x/net/http2/errors.go create mode 100644 vendor/golang.org/x/net/http2/flow.go create mode 100644 vendor/golang.org/x/net/http2/frame.go create mode 100644 vendor/golang.org/x/net/http2/gotrack.go create mode 100644 vendor/golang.org/x/net/http2/h2c/h2c.go create mode 100644 vendor/golang.org/x/net/http2/headermap.go create mode 100644 vendor/golang.org/x/net/http2/hpack/encode.go create mode 100644 vendor/golang.org/x/net/http2/hpack/hpack.go create mode 100644 vendor/golang.org/x/net/http2/hpack/huffman.go create mode 100644 vendor/golang.org/x/net/http2/hpack/static_table.go create mode 100644 vendor/golang.org/x/net/http2/hpack/tables.go create mode 100644 vendor/golang.org/x/net/http2/http2.go create mode 100644 vendor/golang.org/x/net/http2/pipe.go create mode 100644 vendor/golang.org/x/net/http2/server.go create mode 100644 vendor/golang.org/x/net/http2/testsync.go create mode 100644 vendor/golang.org/x/net/http2/transport.go create mode 100644 vendor/golang.org/x/net/http2/write.go create mode 100644 vendor/golang.org/x/net/http2/writesched.go create mode 100644 vendor/golang.org/x/net/http2/writesched_priority.go create mode 100644 vendor/golang.org/x/net/http2/writesched_random.go create mode 100644 vendor/golang.org/x/net/http2/writesched_roundrobin.go create mode 100644 vendor/golang.org/x/net/idna/go118.go create mode 100644 vendor/golang.org/x/net/idna/idna10.0.0.go create mode 100644 vendor/golang.org/x/net/idna/idna9.0.0.go create mode 100644 vendor/golang.org/x/net/idna/pre_go118.go create mode 100644 vendor/golang.org/x/net/idna/punycode.go create mode 100644 vendor/golang.org/x/net/idna/tables10.0.0.go create mode 100644 vendor/golang.org/x/net/idna/tables11.0.0.go create mode 100644 vendor/golang.org/x/net/idna/tables12.0.0.go create mode 100644 vendor/golang.org/x/net/idna/tables13.0.0.go create mode 100644 vendor/golang.org/x/net/idna/tables15.0.0.go create mode 100644 vendor/golang.org/x/net/idna/tables9.0.0.go create mode 100644 vendor/golang.org/x/net/idna/trie.go create mode 100644 vendor/golang.org/x/net/idna/trie12.0.0.go create mode 100644 vendor/golang.org/x/net/idna/trie13.0.0.go create mode 100644 vendor/golang.org/x/net/idna/trieval.go create mode 100644 vendor/golang.org/x/sync/LICENSE create mode 100644 vendor/golang.org/x/sync/PATENTS create mode 100644 vendor/golang.org/x/sync/errgroup/errgroup.go create mode 100644 vendor/golang.org/x/sync/errgroup/go120.go create mode 100644 vendor/golang.org/x/sync/errgroup/pre_go120.go create mode 100644 vendor/golang.org/x/sys/plan9/asm.s create mode 100644 vendor/golang.org/x/sys/plan9/asm_plan9_386.s create mode 100644 vendor/golang.org/x/sys/plan9/asm_plan9_amd64.s create mode 100644 vendor/golang.org/x/sys/plan9/asm_plan9_arm.s create mode 100644 vendor/golang.org/x/sys/plan9/const_plan9.go create mode 100644 vendor/golang.org/x/sys/plan9/dir_plan9.go create mode 100644 vendor/golang.org/x/sys/plan9/env_plan9.go create mode 100644 vendor/golang.org/x/sys/plan9/errors_plan9.go create mode 100644 vendor/golang.org/x/sys/plan9/mkall.sh create mode 100644 vendor/golang.org/x/sys/plan9/mkerrors.sh create mode 100644 vendor/golang.org/x/sys/plan9/mksysnum_plan9.sh create mode 100644 vendor/golang.org/x/sys/plan9/pwd_go15_plan9.go create mode 100644 vendor/golang.org/x/sys/plan9/pwd_plan9.go create mode 100644 vendor/golang.org/x/sys/plan9/race.go create mode 100644 vendor/golang.org/x/sys/plan9/race0.go create mode 100644 vendor/golang.org/x/sys/plan9/str.go create mode 100644 vendor/golang.org/x/sys/plan9/syscall.go create mode 100644 vendor/golang.org/x/sys/plan9/syscall_plan9.go create mode 100644 vendor/golang.org/x/sys/plan9/zsyscall_plan9_386.go create mode 100644 vendor/golang.org/x/sys/plan9/zsyscall_plan9_amd64.go create mode 100644 vendor/golang.org/x/sys/plan9/zsyscall_plan9_arm.go create mode 100644 vendor/golang.org/x/sys/plan9/zsysnum_plan9.go create mode 100644 vendor/golang.org/x/sys/unix/bpxsvc_zos.go create mode 100644 vendor/golang.org/x/sys/unix/bpxsvc_zos.s delete mode 100644 vendor/golang.org/x/sys/unix/epoll_zos.go delete mode 100644 vendor/golang.org/x/sys/unix/fstatfs_zos.go create mode 100644 vendor/golang.org/x/sys/unix/ioctl_signed.go rename vendor/golang.org/x/sys/unix/{ioctl.go => ioctl_unsigned.go} (77%) create mode 100644 vendor/golang.org/x/sys/unix/mmap_nomremap.go create mode 100644 vendor/golang.org/x/sys/unix/mremap.go create mode 100644 vendor/golang.org/x/sys/unix/sockcmsg_zos.go create mode 100644 vendor/golang.org/x/sys/unix/symaddr_zos_s390x.s create mode 100644 vendor/golang.org/x/sys/unix/syscall_hurd.go create mode 100644 vendor/golang.org/x/sys/unix/syscall_hurd_386.go create mode 100644 vendor/golang.org/x/sys/unix/zsymaddr_zos_s390x.s create mode 100644 vendor/golang.org/x/sys/unix/zsyscall_openbsd_mips64.s create mode 100644 vendor/golang.org/x/sys/windows/aliases.go create mode 100644 vendor/golang.org/x/sys/windows/dll_windows.go create mode 100644 vendor/golang.org/x/sys/windows/env_windows.go create mode 100644 vendor/golang.org/x/sys/windows/eventlog.go create mode 100644 vendor/golang.org/x/sys/windows/exec_windows.go create mode 100644 vendor/golang.org/x/sys/windows/memory_windows.go create mode 100644 vendor/golang.org/x/sys/windows/mkerrors.bash create mode 100644 vendor/golang.org/x/sys/windows/mkknownfolderids.bash create mode 100644 vendor/golang.org/x/sys/windows/mksyscall.go create mode 100644 vendor/golang.org/x/sys/windows/race.go create mode 100644 vendor/golang.org/x/sys/windows/race0.go create mode 100644 vendor/golang.org/x/sys/windows/security_windows.go create mode 100644 vendor/golang.org/x/sys/windows/service.go create mode 100644 vendor/golang.org/x/sys/windows/setupapi_windows.go create mode 100644 vendor/golang.org/x/sys/windows/str.go create mode 100644 vendor/golang.org/x/sys/windows/syscall.go create mode 100644 vendor/golang.org/x/sys/windows/syscall_windows.go create mode 100644 vendor/golang.org/x/sys/windows/types_windows.go create mode 100644 vendor/golang.org/x/sys/windows/types_windows_386.go create mode 100644 vendor/golang.org/x/sys/windows/types_windows_amd64.go create mode 100644 vendor/golang.org/x/sys/windows/types_windows_arm.go create mode 100644 vendor/golang.org/x/sys/windows/types_windows_arm64.go create mode 100644 vendor/golang.org/x/sys/windows/zerrors_windows.go create mode 100644 vendor/golang.org/x/sys/windows/zknownfolderids_windows.go create mode 100644 vendor/golang.org/x/sys/windows/zsyscall_windows.go create mode 100644 vendor/golang.org/x/term/CONTRIBUTING.md create mode 100644 vendor/golang.org/x/term/LICENSE create mode 100644 vendor/golang.org/x/term/PATENTS create mode 100644 vendor/golang.org/x/term/README.md create mode 100644 vendor/golang.org/x/term/codereview.cfg create mode 100644 vendor/golang.org/x/term/term.go create mode 100644 vendor/golang.org/x/term/term_plan9.go create mode 100644 vendor/golang.org/x/term/term_unix.go create mode 100644 vendor/golang.org/x/term/term_unix_bsd.go create mode 100644 vendor/golang.org/x/term/term_unix_other.go create mode 100644 vendor/golang.org/x/term/term_unsupported.go create mode 100644 vendor/golang.org/x/term/term_windows.go create mode 100644 vendor/golang.org/x/term/terminal.go create mode 100644 vendor/golang.org/x/text/LICENSE create mode 100644 vendor/golang.org/x/text/PATENTS create mode 100644 vendor/golang.org/x/text/secure/bidirule/bidirule.go create mode 100644 vendor/golang.org/x/text/secure/bidirule/bidirule10.0.0.go create mode 100644 vendor/golang.org/x/text/secure/bidirule/bidirule9.0.0.go create mode 100644 vendor/golang.org/x/text/transform/transform.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/bidi.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/bracket.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/core.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/prop.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/tables10.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/tables11.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/tables12.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/tables13.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/tables15.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/tables9.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/bidi/trieval.go create mode 100644 vendor/golang.org/x/text/unicode/norm/composition.go create mode 100644 vendor/golang.org/x/text/unicode/norm/forminfo.go create mode 100644 vendor/golang.org/x/text/unicode/norm/input.go create mode 100644 vendor/golang.org/x/text/unicode/norm/iter.go create mode 100644 vendor/golang.org/x/text/unicode/norm/normalize.go create mode 100644 vendor/golang.org/x/text/unicode/norm/readwriter.go create mode 100644 vendor/golang.org/x/text/unicode/norm/tables10.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/norm/tables11.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/norm/tables12.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/norm/tables13.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/norm/tables15.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/norm/tables9.0.0.go create mode 100644 vendor/golang.org/x/text/unicode/norm/transform.go create mode 100644 vendor/golang.org/x/text/unicode/norm/trie.go create mode 100644 vendor/gonum.org/v1/gonum/AUTHORS create mode 100644 vendor/gonum.org/v1/gonum/CONTRIBUTORS create mode 100644 vendor/gonum.org/v1/gonum/LICENSE create mode 100644 vendor/gonum.org/v1/gonum/blas/README.md create mode 100644 vendor/gonum.org/v1/gonum/blas/blas.go create mode 100644 vendor/gonum.org/v1/gonum/blas/blas64/blas64.go create mode 100644 vendor/gonum.org/v1/gonum/blas/blas64/conv.go create mode 100644 vendor/gonum.org/v1/gonum/blas/blas64/conv_symmetric.go create mode 100644 vendor/gonum.org/v1/gonum/blas/blas64/doc.go create mode 100644 vendor/gonum.org/v1/gonum/blas/cblas128/cblas128.go create mode 100644 vendor/gonum.org/v1/gonum/blas/cblas128/conv.go create mode 100644 vendor/gonum.org/v1/gonum/blas/cblas128/conv_hermitian.go create mode 100644 vendor/gonum.org/v1/gonum/blas/cblas128/conv_symmetric.go create mode 100644 vendor/gonum.org/v1/gonum/blas/cblas128/doc.go create mode 100644 vendor/gonum.org/v1/gonum/blas/conversions.bash create mode 100644 vendor/gonum.org/v1/gonum/blas/doc.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/dgemm.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/doc.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/errors.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/gonum.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level1cmplx128.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level1cmplx64.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level1float32.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level1float32_dsdot.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level1float32_sdot.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level1float32_sdsdot.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level1float64.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level1float64_ddot.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level2cmplx128.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level2cmplx64.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level2float32.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level2float64.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level3cmplx128.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level3cmplx64.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level3float32.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/level3float64.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/sgemm.go create mode 100644 vendor/gonum.org/v1/gonum/blas/gonum/single_precision.bash create mode 100644 vendor/gonum.org/v1/gonum/floats/README.md create mode 100644 vendor/gonum.org/v1/gonum/floats/doc.go create mode 100644 vendor/gonum.org/v1/gonum/floats/floats.go create mode 100644 vendor/gonum.org/v1/gonum/floats/scalar/doc.go create mode 100644 vendor/gonum.org/v1/gonum/floats/scalar/scalar.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/axpyinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/axpyincto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/axpyunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/axpyunitaryto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/doc.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/dotcinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/dotcunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/dotuinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/dotuunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/dscalinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/dscalunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/scal.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/scalUnitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/scalinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/stubs.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/stubs_amd64.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c128/stubs_noasm.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/axpyinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/axpyincto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/axpyunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/axpyunitaryto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/conj.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/doc.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/dotcinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/dotcunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/dotuinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/dotuunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/scal.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/stubs.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/stubs_amd64.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/c64/stubs_noasm.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/axpyinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/axpyincto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/axpyunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/axpyunitaryto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/ddotinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/ddotunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/doc.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/dotinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/dotunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/ge_amd64.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/ge_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/ge_noasm.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/gemv.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/l2norm.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/scal.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/stubs_amd64.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/stubs_noasm.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f32/sum_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/abssum_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/abssuminc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/add_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/addconst_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/axpy.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/axpyinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/axpyincto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/axpyunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/axpyunitaryto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/cumprod_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/cumsum_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/div_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/divto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/doc.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/dot.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/dot_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/ge_amd64.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/ge_noasm.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/gemvN_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/gemvT_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/ger_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/l1norm_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/l2norm_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/l2norm_noasm.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/l2normdist_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/l2norminc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/linfnorm_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/scal.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/scalinc_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/scalincto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/scalunitary_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/scalunitaryto_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/stubs_amd64.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/stubs_noasm.go create mode 100644 vendor/gonum.org/v1/gonum/internal/asm/f64/sum_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/cmplx64/abs.go create mode 100644 vendor/gonum.org/v1/gonum/internal/cmplx64/conj.go create mode 100644 vendor/gonum.org/v1/gonum/internal/cmplx64/doc.go create mode 100644 vendor/gonum.org/v1/gonum/internal/cmplx64/isinf.go create mode 100644 vendor/gonum.org/v1/gonum/internal/cmplx64/isnan.go create mode 100644 vendor/gonum.org/v1/gonum/internal/cmplx64/sqrt.go create mode 100644 vendor/gonum.org/v1/gonum/internal/math32/doc.go create mode 100644 vendor/gonum.org/v1/gonum/internal/math32/math.go create mode 100644 vendor/gonum.org/v1/gonum/internal/math32/signbit.go create mode 100644 vendor/gonum.org/v1/gonum/internal/math32/sqrt.go create mode 100644 vendor/gonum.org/v1/gonum/internal/math32/sqrt_amd64.go create mode 100644 vendor/gonum.org/v1/gonum/internal/math32/sqrt_amd64.s create mode 100644 vendor/gonum.org/v1/gonum/internal/math32/sqrt_arm64.go create mode 100644 vendor/gonum.org/v1/gonum/internal/math32/sqrt_arm64.s create mode 100644 vendor/gonum.org/v1/gonum/lapack/.gitignore create mode 100644 vendor/gonum.org/v1/gonum/lapack/README.md create mode 100644 vendor/gonum.org/v1/gonum/lapack/doc.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dbdsqr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgebak.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgebal.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgebd2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgebrd.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgecon.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgeev.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgehd2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgehrd.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgelq2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgelqf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgels.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgeql2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgeqp3.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgeqr2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgeqrf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgerq2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgerqf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgesc2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgesv.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgesvd.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgetc2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgetf2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgetrf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgetri.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgetrs.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgghrd.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dggsvd3.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dggsvp3.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dgtsv.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dhseqr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlabrd.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlacn2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlacpy.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlae2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaev2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaexc.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlag2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlags2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlagtm.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlahqr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlahr2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaln2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlangb.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlange.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlangt.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlanhs.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlansb.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlanst.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlansy.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlantb.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlantr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlanv2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlapll.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlapmr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlapmt.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlapy2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaqp2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaqps.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaqr04.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaqr1.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaqr23.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaqr5.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlarf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlarfb.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlarfg.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlarft.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlarfx.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlartg.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlas2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlascl.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaset.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasq1.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasq2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasq3.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasq4.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasq5.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasq6.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasrt.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlassq.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasv2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlaswp.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlasy2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlatbs.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlatdf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlatrd.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlatrs.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlauu2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dlauum.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/doc.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorg2l.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorg2r.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorgbr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorghr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorgl2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorglq.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorgql.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorgqr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorgr2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorgtr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorm2r.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dormbr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dormhr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dorml2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dormlq.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dormqr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dormr2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpbcon.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpbtf2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpbtrf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpbtrs.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpocon.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpotf2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpotrf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpotri.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpotrs.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpstf2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpstrf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dptcon.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dptsv.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpttrf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dpttrs.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dptts2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/drscl.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dsteqr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dsterf.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dsyev.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dsytd2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dsytrd.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dtbtrs.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dtgsja.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dtrcon.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dtrevc3.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dtrexc.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dtrti2.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dtrtri.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/dtrtrs.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/errors.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/iladlc.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/iladlr.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/ilaenv.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/iparmq.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/gonum/lapack.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/lapack.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/lapack64/doc.go create mode 100644 vendor/gonum.org/v1/gonum/lapack/lapack64/lapack64.go create mode 100644 vendor/gonum.org/v1/gonum/mat/README.md create mode 100644 vendor/gonum.org/v1/gonum/mat/band.go create mode 100644 vendor/gonum.org/v1/gonum/mat/cdense.go create mode 100644 vendor/gonum.org/v1/gonum/mat/cholesky.go create mode 100644 vendor/gonum.org/v1/gonum/mat/cmatrix.go create mode 100644 vendor/gonum.org/v1/gonum/mat/consts.go create mode 100644 vendor/gonum.org/v1/gonum/mat/dense.go create mode 100644 vendor/gonum.org/v1/gonum/mat/dense_arithmetic.go create mode 100644 vendor/gonum.org/v1/gonum/mat/diagonal.go create mode 100644 vendor/gonum.org/v1/gonum/mat/doc.go create mode 100644 vendor/gonum.org/v1/gonum/mat/eigen.go create mode 100644 vendor/gonum.org/v1/gonum/mat/errors.go create mode 100644 vendor/gonum.org/v1/gonum/mat/format.go create mode 100644 vendor/gonum.org/v1/gonum/mat/gsvd.go create mode 100644 vendor/gonum.org/v1/gonum/mat/hogsvd.go create mode 100644 vendor/gonum.org/v1/gonum/mat/index_bound_checks.go create mode 100644 vendor/gonum.org/v1/gonum/mat/index_no_bound_checks.go create mode 100644 vendor/gonum.org/v1/gonum/mat/inner.go create mode 100644 vendor/gonum.org/v1/gonum/mat/io.go create mode 100644 vendor/gonum.org/v1/gonum/mat/lq.go create mode 100644 vendor/gonum.org/v1/gonum/mat/lu.go create mode 100644 vendor/gonum.org/v1/gonum/mat/matrix.go create mode 100644 vendor/gonum.org/v1/gonum/mat/offset.go create mode 100644 vendor/gonum.org/v1/gonum/mat/offset_appengine.go create mode 100644 vendor/gonum.org/v1/gonum/mat/pool.go create mode 100644 vendor/gonum.org/v1/gonum/mat/product.go create mode 100644 vendor/gonum.org/v1/gonum/mat/qr.go create mode 100644 vendor/gonum.org/v1/gonum/mat/shadow.go create mode 100644 vendor/gonum.org/v1/gonum/mat/shadow_common.go create mode 100644 vendor/gonum.org/v1/gonum/mat/shadow_complex.go create mode 100644 vendor/gonum.org/v1/gonum/mat/solve.go create mode 100644 vendor/gonum.org/v1/gonum/mat/svd.go create mode 100644 vendor/gonum.org/v1/gonum/mat/symband.go create mode 100644 vendor/gonum.org/v1/gonum/mat/symmetric.go create mode 100644 vendor/gonum.org/v1/gonum/mat/triangular.go create mode 100644 vendor/gonum.org/v1/gonum/mat/triband.go create mode 100644 vendor/gonum.org/v1/gonum/mat/tridiag.go create mode 100644 vendor/gonum.org/v1/gonum/mat/vector.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/README.md create mode 100644 vendor/gonum.org/v1/gonum/mathext/airy.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/beta.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/betainc.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/digamma.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/doc.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/ell_carlson.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/ell_complete.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/erf.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/gamma_inc.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/gamma_inc_inv.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/amos/amos.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/amos/doc.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/amos/staticcheck.conf create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/cephes.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/doc.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/igam.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/igami.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/incbeta.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/incbi.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/lanczos.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/ndtri.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/polevl.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/staticcheck.conf create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/unity.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/cephes/zeta.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/gonum/beta.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/gonum/doc.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/internal/gonum/gonum.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/mvgamma.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/roots.go create mode 100644 vendor/gonum.org/v1/gonum/mathext/zeta.go create mode 100644 vendor/gonum.org/v1/gonum/stat/README.md create mode 100644 vendor/gonum.org/v1/gonum/stat/combin/combin.go create mode 100644 vendor/gonum.org/v1/gonum/stat/combin/doc.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/alphastable.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/bernoulli.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/beta.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/binomial.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/categorical.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/chi.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/chisquared.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/constants.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/doc.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/exponential.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/f.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/gamma.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/general.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/gumbel.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/interfaces.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/inversegamma.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/laplace.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/logistic.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/lognormal.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/norm.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/pareto.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/poisson.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/statdist.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/studentst.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/triangle.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/uniform.go create mode 100644 vendor/gonum.org/v1/gonum/stat/distuv/weibull.go create mode 100644 vendor/gonum.org/v1/gonum/stat/doc.go create mode 100644 vendor/gonum.org/v1/gonum/stat/pca_cca.go create mode 100644 vendor/gonum.org/v1/gonum/stat/roc.go create mode 100644 vendor/gonum.org/v1/gonum/stat/stat.go create mode 100644 vendor/gonum.org/v1/gonum/stat/statmat.go create mode 100644 vendor/google.golang.org/protobuf/internal/editiondefaults/defaults.go create mode 100644 vendor/google.golang.org/protobuf/internal/editiondefaults/editions_defaults.binpb create mode 100644 vendor/google.golang.org/protobuf/internal/filedesc/editions.go create mode 100644 vendor/google.golang.org/protobuf/internal/genid/go_features_gen.go rename vendor/google.golang.org/protobuf/internal/strs/{strings_unsafe.go => strings_unsafe_go120.go} (95%) create mode 100644 vendor/google.golang.org/protobuf/internal/strs/strings_unsafe_go121.go create mode 100644 vendor/google.golang.org/protobuf/reflect/protoreflect/value_equal.go rename vendor/google.golang.org/protobuf/reflect/protoreflect/{value_unsafe.go => value_unsafe_go120.go} (97%) create mode 100644 vendor/google.golang.org/protobuf/reflect/protoreflect/value_unsafe_go121.go create mode 100644 vendor/google.golang.org/protobuf/types/known/durationpb/duration.pb.go create mode 100644 vendor/google.golang.org/protobuf/types/known/timestamppb/timestamp.pb.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ac917b9e..d8ad98eb 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -6,14 +6,16 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.19 - uses: actions/setup-go@v1 + - name: Set up Go 1.23 + uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: 1.23 id: go - name: Check out code into the Go module directory - uses: actions/checkout@v1 + uses: actions/checkout@v3 + with: + fetch-depth: 0 - name: Test run: go test -mod=vendor -short ./... diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml new file mode 100644 index 00000000..c2ea4c44 --- /dev/null +++ b/.github/workflows/release-dev.yml @@ -0,0 +1,37 @@ +name: Create a dev release + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + create_a_dev_release: + name: create a dev release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - run: git fetch --force --tags + - uses: actions/setup-go@v3 + with: + go-version: ">=1.23" + cache: true + - uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: latest + args: release --clean --config ./goreleaser-dev.yaml + env: + GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} + - run: echo "${HOME}/.humanlog/bin" >> $GITHUB_PATH + - run: curl https://humanlog.dev/install_apictl.sh | bash + - run: ./script/create_version_artifacts.sh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HMAC_KEY_ID: ${{ secrets.HMAC_KEY_ID }} + HMAC_PRIVATE_KEY: ${{ secrets.HMAC_PRIVATE_KEY }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 33b64226..c127c7c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,13 +19,13 @@ jobs: - run: git fetch --force --tags - uses: actions/setup-go@v3 with: - go-version: ">=1.19.4" + go-version: ">=1.23" cache: true - - uses: goreleaser/goreleaser-action@v4 + - uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: latest - args: release --rm-dist + args: release --clean --config ./goreleaser.yaml env: GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} - run: echo "${HOME}/.humanlog/bin" >> $GITHUB_PATH diff --git a/.gitignore b/.gitignore index d81933f7..2f04af4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.ignore dist +debug.log +logdump.json diff --git a/.goreleaser-dev.yaml b/.goreleaser-dev.yaml new file mode 100644 index 00000000..428d04eb --- /dev/null +++ b/.goreleaser-dev.yaml @@ -0,0 +1,44 @@ +project_name: humanlog-dev +version: 2 +before: + hooks: + - go mod tidy + - go generate ./... +builds: + - main: ./cmd/humanlog/ + binary: humanlog + env: + - CGO_ENABLED=0 + ldflags: + - -s -w -X main.versionMajor={{.Major}} -X main.versionMinor={{.Minor}} -X main.versionPatch={{.Patch}} -X main.versionPrerelease={{.Prerelease}} -X main.versionBuild={{.ShortCommit}} -X main.defaultApiAddr="https://api.humanlog.dev" + goos: + # - windows + - darwin + - linux + goarch: + - amd64 + - arm64 +release: + github: + owner: humanlogio + name: humanlog + target_commitish: "{{ .Commit }}" + prerelease: true +archives: + - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + format_overrides: + - goos: windows + format: zip +checksum: + name_template: "checksums.txt" +snapshot: + version_template: "{{ incpatch .Version }}-next.{{ .Timestamp }}.{{ .ShortCommit }}" +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" +# modelines, feel free to remove those if you don't want/use them: +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/goreleaser.yaml b/.goreleaser.yaml similarity index 90% rename from goreleaser.yaml rename to .goreleaser.yaml index 9ea18c6c..3e88437b 100644 --- a/goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,4 +1,5 @@ project_name: humanlog +version: 2 before: hooks: - go mod tidy @@ -11,7 +12,7 @@ builds: ldflags: - -s -w -X main.versionMajor={{.Major}} -X main.versionMinor={{.Minor}} -X main.versionPatch={{.Patch}} -X main.versionPrerelease={{.Prerelease}} -X main.versionBuild={{.ShortCommit}} goos: - - windows + # - windows - darwin - linux goarch: @@ -27,11 +28,11 @@ archives: - goos: windows format: zip brews: - - tap: + - repository: owner: humanlogio name: homebrew-tap nfpms: - - maintainer: "antoinegrondin@gmail.com" + - maintainer: "hi@webscale.com" formats: - deb checksum: @@ -48,7 +49,7 @@ checksum: # ] # artifacts: all snapshot: - name_template: "{{ incpatch .Version }}-next" + version_template: "{{ incpatch .Version }}-next" changelog: sort: asc filters: diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..8b65161d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "humanlog --logsvc", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/cmd/humanlog/", + "cwd": "${workspaceFolder}", + "args": ["--logsvc", "http://127.0.0.1:8080"], + }, + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 8ce61ca2..e5e34165 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ brew install humanlog ## On linux (and macOS) ```bash -curl -L "https://humanlog.io/install.sh" | sh +curl -sSL "https://humanlog.io/install.sh" | sh ``` ## Otherwise diff --git a/cmd/humanlog/account.go b/cmd/humanlog/account.go new file mode 100644 index 00000000..0194367d --- /dev/null +++ b/cmd/humanlog/account.go @@ -0,0 +1,445 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "strconv" + + "connectrpc.com/connect" + organizationv1 "github.com/humanlogio/api/go/svc/organization/v1" + "github.com/humanlogio/api/go/svc/organization/v1/organizationv1connect" + tokenv1 "github.com/humanlogio/api/go/svc/token/v1" + "github.com/humanlogio/api/go/svc/token/v1/tokenv1connect" + "github.com/humanlogio/humanlog/internal/pkg/config" + "github.com/humanlogio/humanlog/internal/pkg/iterapi" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/auth" + "github.com/urfave/cli" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + accountCmdName = "account" +) + +func accountCmd( + getCtx func(cctx *cli.Context) context.Context, + getLogger func(cctx *cli.Context) *slog.Logger, + getCfg func(cctx *cli.Context) *config.Config, + getState func(cctx *cli.Context) *state.State, + getTokenSource func(cctx *cli.Context) *auth.UserRefreshableTokenSource, + getAPIUrl func(cctx *cli.Context) string, + getHTTPClient func(*cli.Context) *http.Client, +) cli.Command { + return cli.Command{ + Name: accountCmdName, + Usage: "Manage accounts for the current user or org.", + Before: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + return nil + }, + Subcommands: []cli.Command{ + { + Name: "set-current", + ArgsUsage: "", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + accountName := cctx.Args().First() + if accountName == "" { + logerror("missing argument: ") + return cli.ShowSubcommandHelp(cctx) + } + + // lookup `accountName` and set its ID in `state` + _ = ctx + _ = state + _ = tokenSource + _ = apiURL + _ = httpClient + _ = accountName + + return nil + }, + }, + { + Name: "get-current", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + orgID, err := ensureOrgSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + accountID, err := ensureAccountSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient, orgID) + if err != nil { + return err + } + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + orgClient := organizationv1connect.NewOrganizationServiceClient(httpClient, apiURL, clOpts) + iter := ListAccounts(ctx, orgID, orgClient) + a, ok, err := iterapi.Find(iter, func(el *organizationv1.ListAccountResponse_ListItem) bool { + return el.Account.Id == accountID + }) + if err != nil { + return err + } + if !ok { + logwarn("account with id %d doesn't exist anymore, select another one") + state.CurrentAccountID = nil + return state.WriteBack() + } + printFact("id", a.Account.Id) + printFact("name", a.Account.Name) + return nil + }, + }, + { + Name: "create", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + orgID, err := ensureOrgSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + orgClient := organizationv1connect.NewOrganizationServiceClient(httpClient, apiURL, clOpts) + + res, err := orgClient.CreateAccount(ctx, connect.NewRequest(&organizationv1.CreateAccountRequest{ + OrganizationId: orgID, + })) + if err != nil { + return err + } + account := res.Msg.Account + printFact("created id", account.Id) + printFact("created name", account.Name) + return nil + }, + }, + { + Name: "get", + ArgsUsage: "", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + accountName := cctx.Args().First() + if accountName == "" { + logerror("missing argument: ") + return cli.ShowSubcommandHelp(cctx) + } + + orgID, err := ensureOrgSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + orgClient := organizationv1connect.NewOrganizationServiceClient(httpClient, apiURL, clOpts) + + el, ok, err := iterapi.Find(ListAccounts(ctx, orgID, orgClient), func(el *organizationv1.ListAccountResponse_ListItem) bool { + return el.Account.Name == accountName + }) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("no account with name %q", accountName) + } + printFact("id", el.Account.Id) + printFact("name", el.Account.Name) + return nil + }, + }, + { + Name: "list", + Usage: "list the accounts for the current user or org", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + orgClient := organizationv1connect.NewOrganizationServiceClient(httpClient, apiURL, clOpts) + + orgID, err := ensureOrgSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + + iter := ListAccounts(ctx, orgID, orgClient) + + for iter.Next() { + li := iter.Current() + account := li.Account + printFact("account name", account.Name) + return nil + } + if err := iter.Err(); err != nil { + return err + } + return nil + }, + }, + { + Name: "generate-token", + Usage: "generate an API token for the current account", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + orgID, err := ensureOrgSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + accountID, err := ensureAccountSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient, orgID) + if err != nil { + return err + } + expiresAt, err := hubAskTokenExpiry("Creating an account token") + if err != nil { + return err + } + roles, err := hubAskTokenRoles("Creating an account token") + if err != nil { + return err + } + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + + tokenClient := tokenv1connect.NewTokenServiceClient(httpClient, apiURL, clOpts) + + res, err := tokenClient.GenerateAccountToken(ctx, connect.NewRequest(&tokenv1.GenerateAccountTokenRequest{ + AccountId: accountID, + ExpiresAt: timestamppb.New(expiresAt), + Roles: roles, + })) + if err != nil { + return fmt.Errorf("generating account token: %v", err) + } + token := res.Msg.Token + printFact("id", token.TokenId) + printFact("account id", token.AccountId) + printFact("expires at", token.ExpiresAt.AsTime()) + printFact("roles", token.Roles) + printFact("token (secret! do not lose)", token.Token) + return nil + }, + }, + { + Name: "revoke-token", + Usage: "revoke an API token for the current account", + ArgsUsage: "", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + orgID, err := ensureOrgSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + accountID, err := ensureAccountSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient, orgID) + if err != nil { + return err + } + + tokenIdStr := cctx.Args().First() + if tokenIdStr == "" { + logerror("missing argument: ") + return cli.ShowSubcommandHelp(cctx) + } + tokenID, err := strconv.ParseInt(tokenIdStr, 10, 64) + if err != nil { + logerror("invalid argument: ") + return err + } + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + + tokenClient := tokenv1connect.NewTokenServiceClient(httpClient, apiURL, clOpts) + + loginfo("revoking token %d on account %d", tokenID, accountID) + res, err := tokenClient.RevokeAccountToken(ctx, connect.NewRequest(&tokenv1.RevokeAccountTokenRequest{ + AccountId: accountID, + TokenId: tokenID, + })) + if err != nil { + return fmt.Errorf("revoking account token: %v", err) + } + _ = res + loginfo("token revoked") + return nil + }, + }, + { + Name: "view-token", + Usage: "view the details of an API token for the current account", + ArgsUsage: "", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + orgID, err := ensureOrgSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + accountID, err := ensureAccountSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient, orgID) + if err != nil { + return err + } + + tokenIdStr := cctx.Args().First() + if tokenIdStr == "" { + logerror("missing argument: ") + return cli.ShowSubcommandHelp(cctx) + } + tokenID, err := strconv.ParseInt(tokenIdStr, 10, 64) + if err != nil { + logerror("invalid argument: ") + return err + } + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + + tokenClient := tokenv1connect.NewTokenServiceClient(httpClient, apiURL, clOpts) + + res, err := tokenClient.GetAccountToken(ctx, connect.NewRequest(&tokenv1.GetAccountTokenRequest{ + AccountId: accountID, + TokenId: tokenID, + })) + if err != nil { + return fmt.Errorf("revoking account token: %v", err) + } + token := res.Msg.Token + printFact("id", token.TokenId) + printFact("account id", token.AccountId) + printFact("roles", token.Roles) + printFact("expires at", token.ExpiresAt.AsTime()) + if token.LastUsedAt != nil { + printFact("last used at", token.LastUsedAt.AsTime()) + } else { + printFact("last used at", "never") + } + if token.RevokedAt != nil { + printFact("revoked at", token.RevokedAt.AsTime()) + } else { + printFact("revoked at", "never") + } + return nil + }, + }, + { + Name: "list-tokens", + Usage: "list the API tokens for the current account", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + orgID, err := ensureOrgSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + accountID, err := ensureAccountSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient, orgID) + if err != nil { + return err + } + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + + tokenClient := tokenv1connect.NewTokenServiceClient(httpClient, apiURL, clOpts) + + hasAny := false + iter := ListAccountTokens(ctx, accountID, tokenClient) + for iter.Next() { + hasAny = true + token := iter.Current().Token + printFact("id", token.TokenId) + } + if err := iter.Err(); err != nil { + return fmt.Errorf("listing tokens: %v", err) + } + if !hasAny { + loginfo("no account token found") + } + + return nil + }, + }, + }, + } +} + +func promptCreateAccount(ctx context.Context, + ll *slog.Logger, + cctx *cli.Context, + state *state.State, + tokenSource *auth.UserRefreshableTokenSource, + apiURL string, + httpClient *http.Client, + orgID int64) (int64, error) { + panic("todo") +} diff --git a/cmd/humanlog/auth.go b/cmd/humanlog/auth.go new file mode 100644 index 00000000..078bde48 --- /dev/null +++ b/cmd/humanlog/auth.go @@ -0,0 +1,123 @@ +package main + +import ( + "context" + "fmt" + "log" + "log/slog" + "net/http" + "os" + + "connectrpc.com/connect" + "github.com/fatih/color" + "github.com/humanlogio/api/go/svc/auth/v1/authv1connect" + userpb "github.com/humanlogio/api/go/svc/user/v1" + "github.com/humanlogio/api/go/svc/user/v1/userv1connect" + "github.com/humanlogio/humanlog/internal/pkg/config" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/auth" + "github.com/pkg/browser" + "github.com/urfave/cli" +) + +const ( + authCmdName = "auth" +) + +func authCmd( + getCtx func(cctx *cli.Context) context.Context, + getLogger func(cctx *cli.Context) *slog.Logger, + getCfg func(cctx *cli.Context) *config.Config, + getState func(cctx *cli.Context) *state.State, + getTokenSource func(cctx *cli.Context) *auth.UserRefreshableTokenSource, + getAPIUrl func(cctx *cli.Context) string, + getHTTPClient func(cctx *cli.Context) *http.Client, +) cli.Command { + return cli.Command{ + Name: authCmdName, + Usage: "Authenticate with humanlog.io", + Subcommands: cli.Commands{ + { + Name: "login", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + authClient := authv1connect.NewAuthServiceClient(httpClient, apiURL) + _, err := performLoginFlow(ctx, state, authClient, tokenSource) + return err + }, + }, + { + Name: "whoami", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + apiURL := getAPIUrl(cctx) + state := getState(cctx) + httpClient := getHTTPClient(cctx) + tokenSource := getTokenSource(cctx) + userToken, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return fmt.Errorf("looking up local user state: %v", err) + } + + ll := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})) + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + + printFact("token user ID", userToken.UserId) + + userClient := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + res, err := userClient.Whoami(ctx, connect.NewRequest(&userpb.WhoamiRequest{})) + if err != nil { + return fmt.Errorf("looking up who you are: %v", err) + } + + printFact("email", res.Msg.User.Email) + printFact("verified", res.Msg.User.EmailVerified) + printFact("first name", res.Msg.User.FirstName) + printFact("last name", res.Msg.User.LastName) + printFact("registered since", res.Msg.User.CreatedAt.AsTime()) + printFact("logged into org", res.Msg.CurrentOrganization.Name) + + return nil + }, + }, + { + Name: "logout", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + tokenSource := getTokenSource(cctx) + + ll := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})) + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + userClient := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + res, err := userClient.GetLogoutURL(ctx, connect.NewRequest(&userpb.GetLogoutURLRequest{})) + if err != nil { + return fmt.Errorf("retrieving logout URL") + } + if err := browser.OpenURL(res.Msg.GetLogoutUrl()); err != nil { + return fmt.Errorf("opening logout URL") + } + return tokenSource.ClearToken(ctx) + }, + }, + }, + } +} + +func promptToLogin() { + log.Print( + color.YellowString("You are not logged in."), + ) + log.Print( + color.YellowString("Run `%s` to log in.", color.New(color.Bold).Sprint("humanlog auth login")), + ) +} diff --git a/cmd/humanlog/genny.go b/cmd/humanlog/genny.go new file mode 100644 index 00000000..8a1eddc0 --- /dev/null +++ b/cmd/humanlog/genny.go @@ -0,0 +1,403 @@ +package main + +import ( + "context" + "fmt" + "io" + "log/slog" + "os" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + gonanoid "github.com/matoous/go-nanoid" + "golang.org/x/exp/rand" + + "github.com/humanlogio/humanlog" + "github.com/humanlogio/humanlog/internal/pkg/config" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/urfave/cli" + + "gonum.org/v1/gonum/stat/distuv" +) + +// note: all the randomness clamping with % leads to not +// really random and it's all not correct and it's not meant +// to be, this is just written real fast to get it going. feel +// free to make this better and fancier! + +const ( + gennyCmdName = "genny" +) + +func gennyCmd( + getCtx func(cctx *cli.Context) context.Context, + getLogger func(cctx *cli.Context) *slog.Logger, + getCfg func(cctx *cli.Context) *config.Config, + getState func(cctx *cli.Context) *state.State, +) cli.Command { + + seedFlag := cli.Uint64Flag{ + Name: "seed", + Value: uint64(time.Now().UnixNano()), + } + startAtFlag := cli.StringFlag{ + Name: "start_at", + Value: time.Now().Format(time.RFC3339), + } + averagePerInternalFlag := cli.Float64Flag{ + Name: "logs_per_s", + Value: 50, + } + + return cli.Command{ + Name: gennyCmdName, + Usage: "Generate fake logs", + Hidden: true, + Flags: []cli.Flag{ + seedFlag, + startAtFlag, + averagePerInternalFlag, + }, + + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + seed := cctx.Uint64(seedFlag.Name) + start, err := time.Parse(time.RFC3339, cctx.String(startAtFlag.Name)) + if err != nil { + return fmt.Errorf("invalid start: %v", err) + } + averagePerInternal := cctx.Float64(averagePerInternalFlag.Name) + return genny(ctx, seed, start, time.Second, averagePerInternal, os.Stdout) + }, + } +} + +func genny( + ctx context.Context, + seed uint64, + start time.Time, + interval time.Duration, + averagePerInterval float64, + out io.Writer, +) error { + src := rand.NewSource(seed) + arrivalRateDist := distuv.Poisson{ + Src: src, + Lambda: averagePerInterval, + } + + now := start + for { + if err := emitMessage(out, now, src); err != nil { + return err + } + nextArrival := arrivalRateDist.Rand() + nextMsgIn := time.Duration(float64(interval) / nextArrival) + select { + case <-ctx.Done(): + return nil + case <-time.After(nextMsgIn): + now = now.Add(nextMsgIn) + } + } +} + +func emitMessage(out io.Writer, now time.Time, src rand.Source) error { + t := ts(now, src) + l := lvl(src) + m := msg(src) + k := kvs(src) + _, err := fmt.Fprintln(out, t+l+m+k) + return err +} + +var opts = humanlog.DefaultOptions() + +func ts(now time.Time, src rand.Source) string { + if src.Uint64()%20 == 0 { // 1/20 times, no timestamp + return "" + } + key := randel(src, opts.TimeFields) + format := randel(src, humanlog.TimeFormats) + return key + "=" + now.Format(format) +} + +func kvs(src rand.Source) string { + keyCount := src.Uint64() % 20 + if keyCount > 20 { + panic(keyCount) + } + if keyCount == 0 { + return "" + } + buf := strings.Builder{} + for range keyCount { + buf.WriteString(" ") + buf.WriteString(genKey(src)) + buf.WriteString("=") + buf.WriteString(genVal(src)) + } + return buf.String() +} + +func genKey(src rand.Source) string { + i := src.Uint64() + dice := int(i % 100) + switch { + case dice >= 0 && dice < 4: + return []string{ + "request_id", + "trace_id", + "RequestID", + "req.id", + }[dice] + case dice >= 4 && dice < 6: + return "user" + case dice >= 6 && dice < 8: + return "org" + case dice >= 8 && dice < 12: + keys := []string{ + "index", + "project", + "car", + "idk", + } + return keys[dice%len(keys)] + default: + return genString(src, false) + } +} + +var bases = []int{2, 8, 10, 16} +var fmtbytes = []byte{'b', 'e', 'E', 'f', 'g', 'G', 'x', 'X'} +var bools = []string{"true", "True", "false", "False"} +var bitsizes = []int{32, 64} + +func genVal(src rand.Source) string { + i := src.Uint64() + switch i % 4 { + case 0: + base := randel(src, bases) + return strconv.FormatUint(i, base) + case 1: + f := (distuv.Normal{ + Mu: float64(i), + Src: src, + }).Rand() + fmt := randel(src, fmtbytes) + prec := int(src.Uint64()) % 10 + if prec == 0 { + prec = -1 + } + bitsize := randel(src, bitsizes) + return strconv.FormatFloat(f, + fmt, + prec, + bitsize, + ) + case 2: + return randel(src, bools) + case 3: + return genString(src, true) + } + panic("missing case") +} + +func randel[T any](src rand.Source, sl []T) T { + i := src.Uint64() % uint64(len(sl)) + return sl[i] +} + +func lvl(src rand.Source) string { + key := " " + randel(src, opts.LevelFields) + switch src.Uint64() % 5 { + case 0: + return key + "=DEBUG" + case 1: + return key + "=INFO" + case 2: + return key + "=WARN" + case 3: + return key + "=ERROR" + case 4: + return "" + } + panic("missing case") +} + +func msg(src rand.Source) string { + words := (src.Uint64() % 10) + if words > 10 { + panic(words) + } + if words == 0 { + return "" + } + key := " " + randel(src, opts.MessageFields) + "=" + buf := strings.Builder{} + buf.WriteString(genString(src, false)) + for range words { + buf.WriteRune(' ') + buf.WriteString(genString(src, false)) + } + return key + strconv.Quote(buf.String()) +} + +func genString(src rand.Source, genIDs bool) string { + if !genIDs { + switch i := src.Uint64() % 3; i { + case 0, 1: + return randel(src, nouns) + case 2: + return randel(src, adjectives) + } + } + switch i := src.Uint64() % 4; i { + case 0, 1: + return randel(src, nouns) + case 2: + return randel(src, adjectives) + case 3: + return uuid.NewString() + case 4: + return gonanoid.MustID(int(src.Uint64() % 20)) + default: + panic(i) + } +} + +var adjectives = []string{ + "aged", + "ancient", + "billowing", + "black", + "blue", + "cold", + "cool", + "crimson", + "damp", + "dawn", + "delicate", + "divine", + "falling", + "floral", + "fragrant", + "frosty", + "green", + "holy", + "late", + "lingering", + "little", + "lively", + "long", + "morning", + "muddy", + "nameless", + "old", + "patient", + "polished", + "proud", + "purple", + "quiet", + "red", + "rough", + "shy", + "small", + "snowy", + "solitary", + "spring", + "still", + "throbbing", + "wandering", + "weathered", + "white", + "wild", + "winter", + "wispy", + "withered", + "bold", + "broken", + "icy", + "restless", + "sparkling", + "twilight", + "young", + "bitter", + "dark", + "dry", + "empty", + "hidden", + "misty", + "silent", + "summer", + "autumn", +} + +var nouns = []string{ + "bird", + "breeze", + "brook", + "bush", + "butterfly", + "cherry", + "cloud", + "darkness", + "dawn", + "dew", + "dream", + "dust", + "feather", + "field", + "fire", + "firefly", + "flower", + "fog", + "forest", + "frog", + "frost", + "glade", + "glitter", + "grass", + "haze", + "hill", + "lake", + "leaf", + "meadow", + "moon", + "morning", + "mountain", + "night", + "paper", + "pine", + "pond", + "rain", + "resonance", + "river", + "sea", + "shadow", + "shape", + "silence", + "sky", + "smoke", + "snow", + "snowflake", + "sound", + "star", + "sun", + "sun", + "sunset", + "surf", + "thunder", + "tree", + "violet", + "voice", + "water", + "water", + "waterfall", + "wave", + "wildflower", + "wind", + "wood", +} diff --git a/cmd/humanlog/helper.go b/cmd/humanlog/helper.go new file mode 100644 index 00000000..5e34a9ab --- /dev/null +++ b/cmd/humanlog/helper.go @@ -0,0 +1,374 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "log/slog" + "net/http" + "net/http/httputil" + "os" + "runtime" + "strings" + "time" + + "connectrpc.com/connect" + "github.com/charmbracelet/huh" + "github.com/fatih/color" + authv1 "github.com/humanlogio/api/go/svc/auth/v1" + "github.com/humanlogio/api/go/svc/auth/v1/authv1connect" + organizationv1 "github.com/humanlogio/api/go/svc/organization/v1" + "github.com/humanlogio/api/go/svc/organization/v1/organizationv1connect" + tokenv1 "github.com/humanlogio/api/go/svc/token/v1" + "github.com/humanlogio/api/go/svc/token/v1/tokenv1connect" + userv1 "github.com/humanlogio/api/go/svc/user/v1" + "github.com/humanlogio/api/go/svc/user/v1/userv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/internal/pkg/iterapi" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/auth" + "github.com/pkg/browser" + "github.com/urfave/cli" +) + +func ensureLoggedIn( + ctx context.Context, + cctx *cli.Context, + state *state.State, + tokenSource *auth.UserRefreshableTokenSource, + apiURL string, + httpClient *http.Client) (*typesv1.UserToken, error) { + userToken, err := tokenSource.GetUserToken(ctx) + if err != nil { + return nil, fmt.Errorf("looking up local user state: %v", err) + } + authClient := authv1connect.NewAuthServiceClient(httpClient, apiURL) + if userToken == nil { + confirms := false + err := huh.NewConfirm(). + Title("You're logged out. Would you like to login?"). + Affirmative("Yes!"). + Negative("No."). + Value(&confirms). + WithTheme(huh.ThemeCatppuccin()). + Run() + if err != nil { + return nil, err + } + if !confirms { + return nil, fmt.Errorf("aborting") + } + // no user auth, perform login flow + t, err := performLoginFlow(ctx, state, authClient, tokenSource) + if err != nil { + return nil, fmt.Errorf("performing login: %v", err) + } + userToken = t + } else { + // check that the token is valid + ll := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})) + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + userClient := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + cerr := new(connect.Error) + _, err := userClient.Whoami(ctx, connect.NewRequest(&userv1.WhoamiRequest{})) + if errors.As(err, &cerr) && cerr.Code() == connect.CodeUnauthenticated { + // token isn't valid anymore, login again + confirms := true + err := huh.NewConfirm(). + Title("Your session has expired. Would you like to login again?"). + Affirmative("Yes!"). + Negative("No."). + Value(&confirms). + WithTheme(huh.ThemeCatppuccin()). + Run() + if err != nil { + return nil, err + } + if !confirms { + return nil, fmt.Errorf("aborting") + } + t, err := performLoginFlow(ctx, state, authClient, tokenSource) + if err != nil { + return nil, fmt.Errorf("performing login: %v", err) + } + userToken = t + } else if err != nil { + return nil, fmt.Errorf("requesting whoami: %v", err) + } + } + return userToken, nil +} + +func performLoginFlow( + ctx context.Context, + state *state.State, + authClient authv1connect.AuthServiceClient, + tokenSource *auth.UserRefreshableTokenSource, +) (*typesv1.UserToken, error) { + res, err := authClient.BeginDeviceAuth(ctx, connect.NewRequest(&authv1.BeginDeviceAuthRequest{})) + if err != nil { + return nil, fmt.Errorf("requesting auth URL: %v", err) + } + + url := res.Msg.Url + deviceCode := res.Msg.DeviceCode + userCode := res.Msg.UserCode + pollUntil := res.Msg.ExpiresAt + pollInterval := res.Msg.PollInterval.AsDuration() + loginfo("open your browser at URL %q", url) + if err := browser.OpenURL(url); err != nil { + return nil, fmt.Errorf("opening browser: %v", err) + } + + ctx, cancel := context.WithDeadline(ctx, pollUntil.AsTime()) + defer cancel() + + ticker := time.NewTicker(pollInterval) + defer ticker.Stop() + + var ( + userToken *typesv1.UserToken + accountID int64 + machineID int64 + ) +poll_for_tokens: + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-ticker.C: + } + + res, err := authClient.CompleteDeviceAuth(ctx, connect.NewRequest(&authv1.CompleteDeviceAuthRequest{ + DeviceCode: deviceCode, + UserCode: userCode, + + ClaimAccountId: state.AccountID, + ClaimMachineId: state.MachineID, + Architecture: runtime.GOARCH, + OperatingSystem: runtime.GOOS, + })) + if err != nil { + if cerr, ok := err.(*connect.Error); ok { + switch cerr.Code() { + case connect.CodeFailedPrecondition: + continue poll_for_tokens + } + } + return nil, fmt.Errorf("waiting for user to be authenticated: %v", err) + } + userToken = res.Msg.Token + accountID = res.Msg.AccountId + machineID = res.Msg.MachineId + break poll_for_tokens + + } + + err = tokenSource.SetUserToken(ctx, userToken) + if err != nil { + return nil, fmt.Errorf("saving credentials to keyring: %v", err) + } + state.AccountID = &accountID + state.MachineID = &machineID + if err := state.WriteBack(); err != nil { + return nil, fmt.Errorf("saving state") + } + return userToken, nil +} + +func ensureOrgSelected( + ctx context.Context, + ll *slog.Logger, + cctx *cli.Context, + state *state.State, + tokenSource *auth.UserRefreshableTokenSource, + apiURL string, + httpClient *http.Client, +) (int64, error) { + if state.CurrentOrgID != nil { + return *state.CurrentOrgID, nil + } + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + + client := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + orgID, err := huhSelectOrganizations(ctx, client, "You belong to many orgs. Which one would you like to use?") + if err != nil { + return -1, err + } + state.CurrentOrgID = &orgID + return orgID, state.WriteBack() +} + +func huhSelectOrganizations(ctx context.Context, client userv1connect.UserServiceClient, title string) (int64, error) { + var options []huh.Option[*typesv1.Organization] + iter := ListOrganizations(ctx, client) + for iter.Next() { + org := iter.Current().Organization + options = append(options, huh.NewOption(org.Name, org)) + } + if err := iter.Err(); err != nil { + return -1, fmt.Errorf("no org selected and couldn't list user orgs: %v", err) + } + if len(options) == 0 { + return -1, fmt.Errorf("no org is attached to your user, this is a bug. please contact support at hi@humanlog.io") + } + if len(options) == 1 { + return options[0].Value.Id, nil + } + var selected *typesv1.Organization + err := huh.NewSelect[*typesv1.Organization](). + Title(title). + Options(options...). + Value(&selected). + WithTheme(huh.ThemeCatppuccin()). + Run() + if err != nil { + return -1, fmt.Errorf("prompting for org selection: %v", err) + } + return selected.Id, nil +} + +func ensureAccountSelected( + ctx context.Context, + ll *slog.Logger, + cctx *cli.Context, + state *state.State, + tokenSource *auth.UserRefreshableTokenSource, + apiURL string, + httpClient *http.Client, + orgID int64, +) (int64, error) { + if state.CurrentAccountID != nil { + return *state.CurrentAccountID, nil + } + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + + var options []huh.Option[*typesv1.Account] + client := organizationv1connect.NewOrganizationServiceClient(httpClient, apiURL, clOpts) + iter := ListAccounts(ctx, orgID, client) + for iter.Next() { + item := iter.Current().Account + options = append(options, huh.NewOption(item.Name, item)) + } + if err := iter.Err(); err != nil { + return -1, fmt.Errorf("no account selected and couldn't list user accounts: %v", err) + } + + if len(options) == 0 { + accountID, err := promptCreateAccount(ctx, ll, cctx, state, tokenSource, apiURL, httpClient, orgID) + if err != nil { + return -1, err + } + state.CurrentAccountID = &accountID + return accountID, state.WriteBack() + } + if len(options) == 1 { + state.CurrentAccountID = &options[0].Value.Id + return *state.CurrentAccountID, state.WriteBack() + } + + var ( + selected *typesv1.Account + ) + err := huh.NewSelect[*typesv1.Account](). + Title("You have access to multiple accounts. Which one would you like to use?"). + Options(options...). + Value(&selected). + WithTheme(huh.ThemeCatppuccin()). + Run() + if err != nil { + return -1, fmt.Errorf("prompting for account selection: %v", err) + } + + state.CurrentOrgID = &selected.Id + return *state.CurrentOrgID, state.WriteBack() +} + +func ListOrganizations(ctx context.Context, client userv1connect.UserServiceClient) *iterapi.Iter[*userv1.ListOrganizationResponse_ListItem] { + return iterapi.New(ctx, 100, func(ctx context.Context, cursor *typesv1.Cursor, limit int32) ([]*userv1.ListOrganizationResponse_ListItem, *typesv1.Cursor, error) { + list, err := client.ListOrganization(ctx, connect.NewRequest(&userv1.ListOrganizationRequest{ + Cursor: cursor, + Limit: limit, + })) + if err != nil { + return nil, nil, err + } + return list.Msg.Items, list.Msg.Next, nil + }) +} + +func ListOrgUser(ctx context.Context, orgID int64, client organizationv1connect.OrganizationServiceClient) *iterapi.Iter[*organizationv1.ListUserResponse_ListItem] { + return iterapi.New(ctx, 100, func(ctx context.Context, cursor *typesv1.Cursor, limit int32) ([]*organizationv1.ListUserResponse_ListItem, *typesv1.Cursor, error) { + list, err := client.ListUser(ctx, connect.NewRequest(&organizationv1.ListUserRequest{ + Cursor: cursor, + Limit: limit, + OrganizationId: orgID, + })) + if err != nil { + return nil, nil, err + } + return list.Msg.Items, list.Msg.Next, nil + }) +} + +func ListAccounts(ctx context.Context, orgID int64, client organizationv1connect.OrganizationServiceClient) *iterapi.Iter[*organizationv1.ListAccountResponse_ListItem] { + return iterapi.New(ctx, 100, func(ctx context.Context, cursor *typesv1.Cursor, limit int32) ([]*organizationv1.ListAccountResponse_ListItem, *typesv1.Cursor, error) { + list, err := client.ListAccount(ctx, connect.NewRequest(&organizationv1.ListAccountRequest{ + Cursor: cursor, + Limit: limit, + OrganizationId: orgID, + })) + if err != nil { + return nil, nil, err + } + return list.Msg.Items, list.Msg.Next, nil + }) +} + +func ListAccountTokens(ctx context.Context, accountID int64, client tokenv1connect.TokenServiceClient) *iterapi.Iter[*tokenv1.ListAccountTokenResponse_ListItem] { + return iterapi.New(ctx, 100, func(ctx context.Context, cursor *typesv1.Cursor, limit int32) ([]*tokenv1.ListAccountTokenResponse_ListItem, *typesv1.Cursor, error) { + list, err := client.ListAccountToken(ctx, connect.NewRequest(&tokenv1.ListAccountTokenRequest{ + Cursor: cursor, + Limit: limit, + AccountId: accountID, + })) + if err != nil { + return nil, nil, err + } + return list.Msg.Items, list.Msg.Next, nil + }) +} + +func printFact(key string, fact any) { + log.Printf( + "- %s: %s", + color.YellowString(key), + color.CyanString(fmt.Sprintf("%v", fact)), + ) +} + +type debugLogger struct { + rt http.RoundTripper +} + +func (dl *debugLogger) RoundTrip(req *http.Request) (*http.Response, error) { + var rt string + if b, err := httputil.DumpRequestOut(req, true); err == nil { + rt += strings.Repeat("->", 20) + "\n" + string(b) + "\n" + } + res, err := dl.rt.RoundTrip(req) + if err != nil { + rt += strings.Repeat("<=", 20) + "\n" + strings.Repeat("\terror", 5) + "\n" + err.Error() + "\n" + } else if b, err := httputil.DumpResponse(res, true); err == nil { + rt += strings.Repeat("<=", 20) + "\n" + string(b) + "\n" + } + log.Print(rt) + return res, err +} diff --git a/cmd/humanlog/ingest.go b/cmd/humanlog/ingest.go new file mode 100644 index 00000000..88382d28 --- /dev/null +++ b/cmd/humanlog/ingest.go @@ -0,0 +1,159 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + "time" + + "connectrpc.com/connect" + "github.com/charmbracelet/huh" + "github.com/humanlogio/api/go/svc/ingest/v1/ingestv1connect" + tokenv1 "github.com/humanlogio/api/go/svc/token/v1" + "github.com/humanlogio/api/go/svc/token/v1/tokenv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/internal/pkg/config" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/auth" + "github.com/humanlogio/humanlog/pkg/sink" + "github.com/humanlogio/humanlog/pkg/sink/logsvcsink" + "github.com/urfave/cli" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func ingest( + ctx context.Context, + ll *slog.Logger, + cctx *cli.Context, + apiAddr string, + getCfg func(*cli.Context) *config.Config, + getState func(*cli.Context) *state.State, + getTokenSource func(cctx *cli.Context) *auth.UserRefreshableTokenSource, + getHTTPClient func(*cli.Context) *http.Client, +) (sink.Sink, error) { + state := getState(cctx) + tokenSource := getTokenSource(cctx) + httpClient := getHTTPClient(cctx) + + if state.IngestionToken == nil || time.Now().After(state.IngestionToken.ExpiresAt.AsTime()) { + // we need to create an account token + accountToken, err := createIngestionToken(ctx, ll, cctx, state, tokenSource, apiAddr, httpClient) + if err != nil { + return nil, fmt.Errorf("no ingestion token configured, and couldn't generate one: %v", err) + } + state.IngestionToken = accountToken + if err := state.WriteBack(); err != nil { + return nil, fmt.Errorf("writing back generated ingestion token: %v", err) + } + } + + if state.MachineID == nil || *state.MachineID <= 0 { + //lint:ignore ST1005 "user facing call-to-action" + return nil, fmt.Errorf("It looks like this machine isn't associated with this account. Try to login again, or register with humanlog.io.") + } + + clOpts := []connect.ClientOption{ + connect.WithInterceptors( + auth.NewAccountAuthInterceptor(state.IngestionToken), + ), + connect.WithGRPC(), + } + + client := ingestv1connect.NewIngestServiceClient(httpClient, apiAddr, clOpts...) + var snk sink.Sink + switch sinkType := os.Getenv("SINK_TYPE"); sinkType { + case "unary": + snk = logsvcsink.StartUnarySink(ctx, client, "api", uint64(*state.MachineID), 1<<20, 100*time.Millisecond, true) + case "bidi": + snk = logsvcsink.StartBidiStreamSink(ctx, client, "api", uint64(*state.MachineID), 1<<20, 100*time.Millisecond, true) + case "stream": + fallthrough // use the stream sink as default, it's the best tradeoff for performance and compatibility + default: + snk = logsvcsink.StartStreamSink(ctx, client, "api", uint64(*state.MachineID), 1<<20, 100*time.Millisecond, true) + } + + return snk, nil +} + +func createIngestionToken( + ctx context.Context, + ll *slog.Logger, + cctx *cli.Context, + state *state.State, + tokenSource *auth.UserRefreshableTokenSource, + apiURL string, + httpClient *http.Client, +) (*typesv1.AccountToken, error) { + _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return nil, fmt.Errorf("ensuring you're logged in: %v", err) + } + if state.AccountID == nil { + //lint:ignore ST1005 "user facing call-to-action" + return nil, fmt.Errorf("It looks like no account is associated with this user. Try to login again, or register with humanlog.io.") + } + + // userToken is most likely valid and unexpired, use it + // to generate an account token with the right roles + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + tokenClient := tokenv1connect.NewTokenServiceClient(httpClient, apiURL, clOpts) + + expiresAt, err := hubAskTokenExpiry("Creating an ingestion token.") + if err != nil { + return nil, err + } + req := &tokenv1.GenerateAccountTokenRequest{ + AccountId: *state.AccountID, + ExpiresAt: timestamppb.New(expiresAt), + Roles: []typesv1.AccountRole{typesv1.AccountRole_AccountRole_Ingestor}, + } + res, err := tokenClient.GenerateAccountToken(ctx, connect.NewRequest(req)) + if err != nil { + return nil, fmt.Errorf("generating account token for ingestion: %v", err) + } + return res.Msg.Token, nil +} + +func hubAskTokenExpiry(title string) (time.Time, error) { + var ( + now = time.Now() + expiresAt time.Time + ) + err := huh.NewSelect[time.Time](). + Title(title). + Description("When should this token expire?"). + Options( + huh.NewOption("in 24h", now.AddDate(0, 0, 1)), + huh.NewOption("in a week", now.AddDate(0, 0, 7)), + huh.NewOption("in a month", now.AddDate(0, 1, 0)), + huh.NewOption("in 6 months", now.AddDate(0, 6, 0)), + huh.NewOption("in a year", now.AddDate(1, 0, 0)), + huh.NewOption("a thousand years from now =3", now.AddDate(1000, 0, 0)), + ). + Value(&expiresAt). + Run() + if err != nil { + return expiresAt, fmt.Errorf("prompting for expiry duration: %v", err) + } + return expiresAt, nil +} + +func hubAskTokenRoles(title string) ([]typesv1.AccountRole, error) { + var roles []typesv1.AccountRole + err := huh.NewMultiSelect[typesv1.AccountRole](). + Title(title). + Description("What roles should be granted to this token?"). + Options( + huh.NewOption("ingestor", typesv1.AccountRole_AccountRole_Ingestor), + ). + Value(&roles). + Run() + if err != nil { + return roles, fmt.Errorf("prompting for roles: %v", err) + } + return roles, nil +} diff --git a/cmd/humanlog/localhost.go b/cmd/humanlog/localhost.go new file mode 100644 index 00000000..5a83c79a --- /dev/null +++ b/cmd/humanlog/localhost.go @@ -0,0 +1,161 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "log/slog" + "net" + "net/http" + "net/url" + "os" + "strconv" + "sync" + "syscall" + "time" + + connectcors "connectrpc.com/cors" + + "github.com/humanlogio/api/go/svc/ingest/v1/ingestv1connect" + "github.com/humanlogio/api/go/svc/localhost/v1/localhostv1connect" + "github.com/humanlogio/api/go/svc/query/v1/queryv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/internal/localstorage" + "github.com/humanlogio/humanlog/internal/localsvc" + "github.com/humanlogio/humanlog/internal/pkg/config" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/sink" + "github.com/humanlogio/humanlog/pkg/sink/logsvcsink" + "github.com/rs/cors" + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "golang.org/x/sys/unix" +) + +func isEADDRINUSE(err error) bool { + nerr, ok := err.(*net.OpError) + if !ok { + return false + } + nserr, ok := nerr.Err.(*os.SyscallError) + if !ok { + return false + } + if nserr.Syscall != "bind" { + return false + } + nserrno, ok := nserr.Err.(syscall.Errno) + if !ok { + return false + } + return nserrno == unix.EADDRINUSE +} + +func startLocalhostServer( + ctx context.Context, + ll *slog.Logger, + cfg *config.Config, + state *state.State, + machineID uint64, + port int, + localhostHttpClient *http.Client, + ownVersion *typesv1.Version, +) (localsink sink.Sink, done func(context.Context) error, err error) { + localhostAddr := net.JoinHostPort("localhost", strconv.Itoa(port)) + + l, err := net.Listen("tcp", localhostAddr) + if err != nil && !isEADDRINUSE(err) { + return nil, nil, fmt.Errorf("listening on host/port: %v", err) + } + if isEADDRINUSE(err) { + // TODO(antoine): + // 1) log to localhost until it's gone + // 2) try to gain the socket, if fail; goto 1) + // 3) serve the localhost service + save local logs + forward them to remote + addr, err := url.Parse("http://" + localhostAddr) + if err != nil { + panic(err) + } + log.Printf("DEBUG: sending logs to localhost forwarder") + client := ingestv1connect.NewIngestServiceClient(localhostHttpClient, addr.String()) + localhostSink := logsvcsink.StartStreamSink(ctx, client, "local", machineID, 1<<20, 100*time.Millisecond, true) + return localhostSink, func(ctx context.Context) error { + return localhostSink.Flush(ctx) + }, nil + } + storage := localstorage.NewMemStorage() + ownSink, _, err := storage.SinkFor(int64(machineID), time.Now().UnixNano()) + if err != nil { + return nil, nil, fmt.Errorf("can't create own sink: %v", err) + } + + mux := http.NewServeMux() + + localhostsvc := localsvc.New(ll, state, ownVersion, storage) + mux.Handle(localhostv1connect.NewLocalhostServiceHandler(localhostsvc)) + mux.Handle(ingestv1connect.NewIngestServiceHandler(localhostsvc)) + mux.Handle(queryv1connect.NewQueryServiceHandler(localhostsvc)) + + hdl := h2c.NewHandler(mux, &http2.Server{}) + hdl = withCORS(hdl) + + srv := http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hdl.ServeHTTP(w, r) + })} + + go func() { + log.Printf("localhost service available on %s, visit `https://humanlog.io` so see your logs", l.Addr().String()) + if err := srv.Serve(l); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Printf("failed to serve localhost service, giving up: %v", err) + } + }() + + return ownSink, func(ctx context.Context) error { + errc := make(chan error, 2) + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + errc <- srv.Shutdown(ctx) + }() + wg.Add(1) + go func() { + defer wg.Done() + errc <- ownSink.Flush(ctx) + }() + wg.Wait() + close(errc) + l.Close() + var ferr error + for err := range errc { + if ferr == nil { + ferr = err + } else { + ll.ErrorContext(ctx, "multiple errors", slog.Any("err", err)) + } + } + return ferr + }, nil +} + +// withCORS adds CORS support to a Connect HTTP handler. +func withCORS(connectHandler http.Handler) http.Handler { + c := cors.New(cors.Options{ + // Debug: true, + AllowedOrigins: []string{ + "https://humanlog.io", + "https://humanlog.dev", + "https://app.humanlog.dev", + "https://app.humanlog.dev:3000", + "https://humanlog.sh", + "http://localhost:3000", + "https://humanlog.test:3000", + }, + AllowedMethods: connectcors.AllowedMethods(), + AllowedHeaders: connectcors.AllowedHeaders(), + ExposedHeaders: connectcors.ExposedHeaders(), + MaxAge: 7200, // 2 hours in seconds + }) + return c.Handler(connectHandler) +} diff --git a/cmd/humanlog/log.go b/cmd/humanlog/log.go index eee5fcf8..fbb2b6f4 100644 --- a/cmd/humanlog/log.go +++ b/cmd/humanlog/log.go @@ -25,7 +25,7 @@ var logLevel = func() loglevel { case "error": return errorlvl default: - return errorlvl + return infolvl } }() diff --git a/cmd/humanlog/machine.go b/cmd/humanlog/machine.go new file mode 100644 index 00000000..2d12c37a --- /dev/null +++ b/cmd/humanlog/machine.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "net/http" + + "github.com/humanlogio/humanlog/internal/pkg/config" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/auth" + "github.com/urfave/cli" +) + +const ( + machineCmdName = "machine" +) + +func machineCmd( + getCtx func(cctx *cli.Context) context.Context, + getLogger func(cctx *cli.Context) *slog.Logger, + getCfg func(cctx *cli.Context) *config.Config, + getState func(cctx *cli.Context) *state.State, + getTokenSource func(cctx *cli.Context) *auth.UserRefreshableTokenSource, + getAPIUrl func(cctx *cli.Context) string, + getHTTPClient func(*cli.Context) *http.Client, +) cli.Command { + return cli.Command{ + Name: machineCmdName, + Usage: "Manage machines in the current account.", + Before: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + return nil + }, + Subcommands: []cli.Command{ + { + Name: "register", + Usage: "register this machine to save logs in an account", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + accountToken, err := createIngestionToken(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return fmt.Errorf("ingestion token couldn't be generated: %v", err) + } + state.IngestionToken = accountToken + if err := state.WriteBack(); err != nil { + return fmt.Errorf("writing back generated ingestion token: %v", err) + } + return nil + }, + }, + { + Name: "deregister", + Usage: "deregister this machine from saving logs in an account", + Action: func(cctx *cli.Context) error { + state := getState(cctx) + state.IngestionToken = nil + if err := state.WriteBack(); err != nil { + return fmt.Errorf("writing back generated ingestion token: %v", err) + } + return nil + }, + }, + }, + } +} diff --git a/cmd/humanlog/main.go b/cmd/humanlog/main.go index fec97ff0..6d5bcafb 100644 --- a/cmd/humanlog/main.go +++ b/cmd/humanlog/main.go @@ -2,23 +2,33 @@ package main import ( "context" + "crypto/tls" "fmt" "log" + "log/slog" + "net" + "net/http" + "net/url" "os" "os/signal" - "runtime" "strconv" "strings" + "time" + "github.com/99designs/keyring" "github.com/aybabtme/rgbterm" "github.com/blang/semver" types "github.com/humanlogio/api/go/types/v1" "github.com/humanlogio/humanlog" "github.com/humanlogio/humanlog/internal/pkg/config" "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/auth" + "github.com/humanlogio/humanlog/pkg/sink" "github.com/humanlogio/humanlog/pkg/sink/stdiosink" + "github.com/humanlogio/humanlog/pkg/sink/teesink" "github.com/mattn/go-colorable" "github.com/urfave/cli" + "golang.org/x/net/http2" ) var ( @@ -47,6 +57,7 @@ var ( } return v }() + defaultApiAddr = "https://api.humanlog.io" ) func fatalf(c *cli.Context, format string, args ...interface{}) { @@ -157,20 +168,86 @@ func newApp() *cli.App { Value: &levelFields, } + apiServerAddr := cli.StringFlag{ + Name: "api", + Value: defaultApiAddr, + Usage: "address of the api server", + EnvVar: "HUMANLOG_API_URL", + Hidden: true, + } + app := cli.NewApp() - app.Author = "Antoine Grondin" - app.Email = "antoinegrondin@gmail.com" + app.Author = "humanlog.io" + app.Email = "hi@humanlog.io" app.Name = "humanlog" app.Version = semverVersion.String() app.Usage = "reads structured logs from stdin, makes them pretty on stdout!" + app.Description = `humanlog parses logs and makes them easier to read and search. + + When invoked with no argument, it consumes stdin and parses it, + attempts to make it prettier on stdout. It also allows searching + the logs that were parsed, both in a TUI by pressing "s" or in a + webapp by pressing "space". + + If registered to ingest logs via "humanlog machine register" logs + will be saved to humanlog.io for vizualization, searching and + analysis. +` var ( - ctx context.Context - cancel context.CancelFunc - cfg *config.Config - statefile *state.State + ctx context.Context + cancel context.CancelFunc + cfg *config.Config + statefile *state.State + httpClient = &http.Client{ + Transport: &http2.Transport{}, + } + localhostHttpClient = &http.Client{ + Transport: &http2.Transport{ + AllowHTTP: true, + DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + }, + } promptedToUpdate *semver.Version updateRes <-chan *checkForUpdateRes + apiURL = "" + keyringName = "humanlog" + + getCtx = func(*cli.Context) context.Context { return ctx } + getLogger = func(*cli.Context) *slog.Logger { return slog.New(slog.NewJSONHandler(os.Stderr, nil)) } + getCfg = func(*cli.Context) *config.Config { return cfg } + getState = func(*cli.Context) *state.State { return statefile } + getKeyring = func(cctx *cli.Context) (keyring.Keyring, error) { + stateDir, err := state.GetDefaultStateDirpath() + if err != nil { + return nil, err + } + return keyring.Open(keyring.Config{ + ServiceName: keyringName, + KeychainSynchronizable: true, + FileDir: stateDir, + }) + + } + getTokenSource = func(cctx *cli.Context) *auth.UserRefreshableTokenSource { + return auth.NewRefreshableTokenSource(func() (keyring.Keyring, error) { + return getKeyring(cctx) + }) + } + getAPIUrl = func(*cli.Context) string { logdebug("using api at %q", apiURL); return apiURL } + getHTTPClient = func(cctx *cli.Context) *http.Client { + u, _ := url.Parse(apiURL) + if host, _, _ := net.SplitHostPort(u.Host); host == "localhost" { + getLogger(cctx).Debug("using localhost client") + return localhostHttpClient + } + return httpClient + } + getLocalhostHTTPClient = func(*cli.Context) *http.Client { + return localhostHttpClient + } ) app.Before = func(c *cli.Context) error { @@ -195,6 +272,10 @@ func newApp() *cli.App { } cfg = cfgFromDir } + if c.String(apiServerAddr.Name) != "" { + apiURL = c.String(apiServerAddr.Name) + getLogger(c).Debug("contacting api at %q (due to flag") + } stateFilepath, err := state.GetDefaultStateFilepath() if err != nil { @@ -213,14 +294,11 @@ func newApp() *cli.App { promptToUpdate(semverVersion, *statefile.LatestKnownVersion) } } - - req := &checkForUpdateReq{ - arch: runtime.GOARCH, - os: runtime.GOOS, - current: version, - } - updateRes = asyncCheckForUpdate(ctx, req, cfg, statefile) + ll := getLogger(c) + tokenSource := getTokenSource(c) + updateRes = asyncCheckForUpdate(ctx, ll, cfg, statefile, apiURL, httpClient, tokenSource) } + return nil } app.After = func(c *cli.Context) error { @@ -242,56 +320,60 @@ func newApp() *cli.App { } return nil } - var ( - getCtx = func(*cli.Context) context.Context { return ctx } - getCfg = func(*cli.Context) *config.Config { return cfg } - getState = func(*cli.Context) *state.State { return statefile } + app.Commands = append( + app.Commands, + versionCmd(getCtx, getLogger, getCfg, getState, getTokenSource, getAPIUrl, getHTTPClient), + authCmd(getCtx, getLogger, getCfg, getState, getTokenSource, getAPIUrl, getHTTPClient), + organizationCmd(getCtx, getLogger, getCfg, getState, getTokenSource, getAPIUrl, getHTTPClient), + accountCmd(getCtx, getLogger, getCfg, getState, getTokenSource, getAPIUrl, getHTTPClient), + machineCmd(getCtx, getLogger, getCfg, getState, getTokenSource, getAPIUrl, getHTTPClient), + queryCmd(getCtx, getLogger, getCfg, getState, getTokenSource, getAPIUrl, getHTTPClient), + gennyCmd(getCtx, getLogger, getCfg, getState), ) - app.Commands = append(app.Commands, versionCmd(getCtx, getCfg, getState)) - app.Flags = []cli.Flag{configFlag, skipFlag, keepFlag, sortLongest, skipUnchanged, truncates, truncateLength, colorFlag, lightBg, timeFormat, ignoreInterrupts, messageFieldsFlag, timeFieldsFlag, levelFieldsFlag} - app.Action = func(c *cli.Context) error { + app.Flags = []cli.Flag{configFlag, skipFlag, keepFlag, sortLongest, skipUnchanged, truncates, truncateLength, colorFlag, lightBg, timeFormat, ignoreInterrupts, messageFieldsFlag, timeFieldsFlag, levelFieldsFlag, apiServerAddr} + app.Action = func(cctx *cli.Context) error { // flags overwrite config file - if c.IsSet(sortLongest.Name) { - cfg.SortLongest = ptr(c.BoolT(sortLongest.Name)) + if cctx.IsSet(sortLongest.Name) { + cfg.SortLongest = ptr(cctx.BoolT(sortLongest.Name)) } - if c.IsSet(skipUnchanged.Name) { - cfg.SkipUnchanged = ptr(c.BoolT(skipUnchanged.Name)) + if cctx.IsSet(skipUnchanged.Name) { + cfg.SkipUnchanged = ptr(cctx.BoolT(skipUnchanged.Name)) } - if c.IsSet(truncates.Name) { - cfg.Truncates = ptr(c.BoolT(truncates.Name)) + if cctx.IsSet(truncates.Name) { + cfg.Truncates = ptr(cctx.BoolT(truncates.Name)) } - if c.IsSet(truncateLength.Name) { - cfg.TruncateLength = ptr(c.Int(truncateLength.Name)) + if cctx.IsSet(truncateLength.Name) { + cfg.TruncateLength = ptr(cctx.Int(truncateLength.Name)) } - if c.IsSet(lightBg.Name) { - cfg.LightBg = ptr(c.Bool(lightBg.Name)) + if cctx.IsSet(lightBg.Name) { + cfg.LightBg = ptr(cctx.Bool(lightBg.Name)) } - if c.IsSet(timeFormat.Name) { - cfg.TimeFormat = ptr(c.String(timeFormat.Name)) + if cctx.IsSet(timeFormat.Name) { + cfg.TimeFormat = ptr(cctx.String(timeFormat.Name)) } - if c.IsSet(colorFlag.Name) { - cfg.ColorMode = ptr(c.String(colorFlag.Name)) + if cctx.IsSet(colorFlag.Name) { + cfg.ColorMode = ptr(cctx.String(colorFlag.Name)) } - if c.IsSet(skipFlag.Name) { + if cctx.IsSet(skipFlag.Name) { cfg.Skip = ptr([]string(skip)) } - if c.IsSet(keepFlag.Name) { + if cctx.IsSet(keepFlag.Name) { cfg.Keep = ptr([]string(keep)) } - if c.IsSet(strings.Split(messageFieldsFlag.Name, ",")[0]) { + if cctx.IsSet(strings.Split(messageFieldsFlag.Name, ",")[0]) { cfg.MessageFields = ptr([]string(messageFields)) } - if c.IsSet(strings.Split(timeFieldsFlag.Name, ",")[0]) { + if cctx.IsSet(strings.Split(timeFieldsFlag.Name, ",")[0]) { cfg.TimeFields = ptr([]string(timeFields)) } - if c.IsSet(strings.Split(levelFieldsFlag.Name, ",")[0]) { + if cctx.IsSet(strings.Split(levelFieldsFlag.Name, ",")[0]) { cfg.LevelFields = ptr([]string(levelFields)) } - if c.IsSet(strings.Split(ignoreInterrupts.Name, ",")[0]) { - cfg.Interrupt = ptr(c.Bool(strings.Split(ignoreInterrupts.Name, ",")[0])) + if cctx.IsSet(strings.Split(ignoreInterrupts.Name, ",")[0]) { + cfg.Interrupt = ptr(cctx.Bool(strings.Split(ignoreInterrupts.Name, ",")[0])) } // apply the config @@ -300,21 +382,79 @@ func newApp() *cli.App { } if len(*cfg.Skip) > 0 && len(*cfg.Keep) > 0 { - fatalf(c, "can only use one of %q and %q", skipFlag.Name, keepFlag.Name) + fatalf(cctx, "can only use one of %q and %q", skipFlag.Name, keepFlag.Name) } sinkOpts, errs := stdiosink.StdioOptsFrom(*cfg) if len(errs) > 0 { for _, err := range errs { - log.Printf("config error: %v", err) + logerror("config error: %v", err) } } - sink := stdiosink.NewStdio(colorable.NewColorableStdout(), sinkOpts) + var sink sink.Sink + sink = stdiosink.NewStdio(colorable.NewColorableStdout(), sinkOpts) handlerOpts := humanlog.HandlerOptionsFrom(*cfg) - log.Print("reading stdin...") + if cfg.ExperimentalFeatures != nil { + if cfg.ExperimentalFeatures.SendLogsToCloud != nil && *cfg.ExperimentalFeatures.SendLogsToCloud { + // TODO(antoine): remove this codepath, it's redundant with the localhost port path + ll := getLogger(cctx) + apiURL := getAPIUrl(cctx) + remotesink, err := ingest(ctx, ll, cctx, apiURL, getCfg, getState, getTokenSource, getHTTPClient) + if err != nil { + return fmt.Errorf("can't send logs: %v", err) + } + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + if err := remotesink.Flush(ctx); err != nil { + ll.ErrorContext(ctx, "couldn't flush buffered log", slog.Any("err", err)) + } else { + ll.InfoContext(ctx, "done sending all logs") + } + }() + loginfo("saving to %s", apiURL) + sink = teesink.NewTeeSink(sink, remotesink) + } + + if cfg.ExperimentalFeatures.ServeLocalhostOnPort != nil { + port := *cfg.ExperimentalFeatures.ServeLocalhostOnPort + state := getState(cctx) + // TODO(antoine): all logs to a single location, right now there's code logging + // randomly everywhere + ll := getLogger(cctx) + var machineID uint64 + for state.MachineID == nil { + // no machine ID assigned, ensure machine gets onboarded via the loggin flow + // TODO(antoine): if an account token exists, auto-onboard the machine. it's probably + // not an interactive session + _, err := ensureLoggedIn(ctx, cctx, state, getTokenSource(cctx), apiURL, getHTTPClient(cctx)) + if err != nil { + return fmt.Errorf("this feature requires a valid machine ID, which requires an account. failed to login: %v", err) + } + } + machineID = uint64(*state.MachineID) + localhostSink, done, err := startLocalhostServer(ctx, ll, cfg, state, machineID, port, getLocalhostHTTPClient(cctx), version) + if err != nil { + loginfo("starting experimental localhost service: %v", err) + } else { + sink = teesink.NewTeeSink(sink, localhostSink) + } + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + if err := done(ctx); err != nil { + ll.ErrorContext(ctx, "couldn't flush buffered log (localhost)", slog.Any("err", err)) + } else { + ll.InfoContext(ctx, "done sending all logs") + } + }() + } + } + + loginfo("reading stdin...") if err := humanlog.Scan(ctx, os.Stdin, sink, handlerOpts); err != nil { - log.Fatalf("scanning caught an error: %v", err) + logerror("scanning caught an error: %v", err) } return nil diff --git a/cmd/humanlog/organization.go b/cmd/humanlog/organization.go new file mode 100644 index 00000000..2fe955b8 --- /dev/null +++ b/cmd/humanlog/organization.go @@ -0,0 +1,355 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "net/http" + + "connectrpc.com/connect" + "github.com/charmbracelet/huh" + "github.com/humanlogio/api/go/svc/organization/v1/organizationv1connect" + userv1 "github.com/humanlogio/api/go/svc/user/v1" + "github.com/humanlogio/api/go/svc/user/v1/userv1connect" + "github.com/humanlogio/humanlog/internal/pkg/config" + "github.com/humanlogio/humanlog/internal/pkg/iterapi" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/auth" + "github.com/urfave/cli" +) + +const ( + organizationCmdName = "organization" +) + +func organizationCmd( + getCtx func(cctx *cli.Context) context.Context, + getLogger func(cctx *cli.Context) *slog.Logger, + getCfg func(cctx *cli.Context) *config.Config, + getState func(cctx *cli.Context) *state.State, + getTokenSource func(cctx *cli.Context) *auth.UserRefreshableTokenSource, + getAPIUrl func(cctx *cli.Context) string, + getHTTPClient func(*cli.Context) *http.Client, +) cli.Command { + + var ( + createOrgNameFlag = cli.StringFlag{ + Name: "name", + Usage: "name of the org to create", + } + ) + + return cli.Command{ + Name: organizationCmdName, + ShortName: "org", + Usage: "Manage organizations for the current user.", + Before: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + return nil + }, + Subcommands: []cli.Command{ + { + Name: "set-current", + Usage: "set the org currently configured in the CLI", + ArgsUsage: "", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + organizationName := cctx.Args().First() + if organizationName == "" { + logerror("missing argument: ") + return cli.ShowSubcommandHelp(cctx) + } + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + userClient := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + + iter := ListOrganizations(ctx, userClient) + + for iter.Next() { + li := iter.Current() + if li.Organization.Name != organizationName { + continue + } + + state.CurrentOrgID = &li.Organization.Id + return state.WriteBack() + } + if err := iter.Err(); err != nil { + return err + } + return fmt.Errorf("you're not part of any org with name %q", organizationName) + }, + }, + { + Name: "get-current", + Usage: "get the org currently configured in the CLI", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + if state.CurrentOrgID == nil { + return fmt.Errorf("no org is currently set") + } + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + userClient := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + + iter := ListOrganizations(ctx, userClient) + + for iter.Next() { + li := iter.Current() + if li.Organization.Id != *state.CurrentOrgID { + continue + } + org := li.Organization + + printFact("org id", org.Id) + printFact("org name", org.Name) + printFact("created on", org.CreatedAt.AsTime()) + return nil + } + if err := iter.Err(); err != nil { + return err + } + return fmt.Errorf("current org not found") + }, + }, + { + Name: "switch", + Usage: "switch to a different org. like `set-current` but with a prompt", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + userClient := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + orgID, err := huhSelectOrganizations(ctx, userClient, "Which org do you want to switch to?") + if err != nil { + return err + } + state.CurrentOrgID = &orgID + return state.WriteBack() + }, + }, + { + Name: "create", + Usage: "create an org", + Flags: []cli.Flag{createOrgNameFlag}, + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + userClient := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + + req := &userv1.CreateOrganizationRequest{ + Name: cctx.String(createOrgNameFlag.Name), + } + + if req.Name == "" { + err := huh.NewInput(). + Title("How should this org be named?"). + Value(&req.Name). + WithTheme(huh.ThemeCatppuccin()). + Run() + if err != nil { + return fmt.Errorf("requesting name from user: %v", err) + } + } + + res, err := userClient.CreateOrganization(ctx, connect.NewRequest(req)) + if err != nil { + return err + } + org := res.Msg.Organization + printFact("id", org.Id) + printFact("name", org.Name) + printFact("created at", org.CreatedAt.AsTime()) + return nil + }, + }, + { + Name: "get", + Usage: "get an org's details", + ArgsUsage: "", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + organizationName := cctx.Args().First() + if organizationName == "" { + logerror("missing argument: ") + return cli.ShowSubcommandHelp(cctx) + } + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + userClient := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + + el, ok, err := iterapi.Find(ListOrganizations(ctx, userClient), func(el *userv1.ListOrganizationResponse_ListItem) bool { + return el.Organization.Name == organizationName + }) + if err != nil { + return err + } + if !ok { + return fmt.Errorf("no org with name %q", organizationName) + } + printFact("id", el.Organization.Id) + printFact("name", el.Organization.Name) + printFact("created at", el.Organization.CreatedAt.AsTime()) + return nil + }, + }, + { + Name: "list", + Usage: "list the orgs you belong to", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + userClient := userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + + iter := ListOrganizations(ctx, userClient) + + for iter.Next() { + li := iter.Current() + org := li.Organization + printFact("name", org.Name) + return nil + } + if err := iter.Err(); err != nil { + return err + } + return nil + }, + }, + { + Name: "list-users", + Usage: "list the users in an org you belong to", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + orgID, err := ensureOrgSelected(ctx, ll, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + organizationClient := organizationv1connect.NewOrganizationServiceClient(httpClient, apiURL, clOpts) + iter := ListOrgUser(ctx, orgID, organizationClient) + for iter.Next() { + u := iter.Current().User + printFact("id", u.Id) + printFact("email", u.Email) + } + if err := iter.Err(); err != nil { + return err + } + + return nil + }, + }, + { + Name: "invite", + Usage: "invite someone to access an org", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + _ = ctx + _ = state + _ = tokenSource + _ = apiURL + _ = httpClient + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + organizationClient := organizationv1connect.NewOrganizationServiceClient(httpClient, apiURL, clOpts) + _ = organizationClient + return nil + }, + }, + { + Name: "revoke", + Usage: "revoke someone's access to an org", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + ll := getLogger(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + + _ = ctx + _ = state + _ = tokenSource + _ = apiURL + _ = httpClient + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + organizationClient := organizationv1connect.NewOrganizationServiceClient(httpClient, apiURL, clOpts) + _ = organizationClient + return nil + }, + }, + }, + } +} diff --git a/cmd/humanlog/query.go b/cmd/humanlog/query.go new file mode 100644 index 00000000..313dc527 --- /dev/null +++ b/cmd/humanlog/query.go @@ -0,0 +1,267 @@ +package main + +import ( + "context" + "fmt" + "log" + "log/slog" + "net/http" + "os" + "time" + + "connectrpc.com/connect" + "github.com/NimbleMarkets/ntcharts/canvas/runes" + "github.com/NimbleMarkets/ntcharts/linechart" + "github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart" + "github.com/charmbracelet/x/term" + "github.com/humanlogio/api/go/svc/account/v1/accountv1connect" + "github.com/humanlogio/api/go/svc/organization/v1/organizationv1connect" + queryv1 "github.com/humanlogio/api/go/svc/query/v1" + "github.com/humanlogio/api/go/svc/query/v1/queryv1connect" + "github.com/humanlogio/api/go/svc/user/v1/userv1connect" + "github.com/humanlogio/humanlog/internal/pkg/config" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/auth" + "github.com/humanlogio/humanlog/pkg/tui" + "github.com/urfave/cli" + "google.golang.org/protobuf/types/known/timestamppb" +) + +const ( + queryCmdName = "query" +) + +func queryCmd( + getCtx func(cctx *cli.Context) context.Context, + getLogger func(cctx *cli.Context) *slog.Logger, + getCfg func(cctx *cli.Context) *config.Config, + getState func(cctx *cli.Context) *state.State, + getTokenSource func(cctx *cli.Context) *auth.UserRefreshableTokenSource, + getAPIUrl func(cctx *cli.Context) string, + getHTTPClient func(*cli.Context) *http.Client, +) cli.Command { + return cli.Command{ + Name: queryCmdName, + Usage: "Query your logs", + + Subcommands: []cli.Command{ + { + Name: "api", + Subcommands: []cli.Command{ + queryApiSummarizeCmd( + getCtx, + getLogger, + getCfg, + getState, + getTokenSource, + getAPIUrl, + getHTTPClient, + ), + queryApiWatchCmd( + getCtx, + getLogger, + getCfg, + getState, + getTokenSource, + getAPIUrl, + getHTTPClient, + ), + }, + }, + }, + + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + ll := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})) + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + return query(ctx, state, apiURL, httpClient, clOpts) + }, + } +} + +func query( + ctx context.Context, + state *state.State, + apiURL string, + httpClient *http.Client, + clOpts connect.ClientOption, +) error { + var ( + userClient = userv1connect.NewUserServiceClient(httpClient, apiURL, clOpts) + organizationClient = organizationv1connect.NewOrganizationServiceClient(httpClient, apiURL, clOpts) + accountClient = accountv1connect.NewAccountServiceClient(httpClient, apiURL, clOpts) + queryClient = queryv1connect.NewQueryServiceClient(httpClient, apiURL, clOpts) + ) + return tui.RunTUI(ctx, state, userClient, organizationClient, accountClient, queryClient) +} + +func queryApiSummarizeCmd( + getCtx func(cctx *cli.Context) context.Context, + getLogger func(cctx *cli.Context) *slog.Logger, + getCfg func(cctx *cli.Context) *config.Config, + getState func(cctx *cli.Context) *state.State, + getTokenSource func(cctx *cli.Context) *auth.UserRefreshableTokenSource, + getAPIUrl func(cctx *cli.Context) string, + getHTTPClient func(*cli.Context) *http.Client, +) cli.Command { + fromFlag := cli.DurationFlag{Name: "since", Value: 365 * 24 * time.Hour} + toFlag := cli.DurationFlag{Name: "to", Value: 0} + return cli.Command{ + Name: "summarize", + Flags: []cli.Flag{fromFlag, toFlag}, + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + state := getState(cctx) + ll := getLogger(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + + termWidth, termHeight, err := term.GetSize(os.Stdout.Fd()) + if err != nil { + return fmt.Errorf("getting term size: %v", err) + } + now := time.Now() + from := now.Add(-cctx.Duration(fromFlag.Name)) + to := now.Add(-cctx.Duration(toFlag.Name)) + + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + queryClient := queryv1connect.NewQueryServiceClient(httpClient, apiURL, clOpts) + + res, err := queryClient.SummarizeEvents(ctx, connect.NewRequest(&queryv1.SummarizeEventsRequest{ + AccountId: *state.AccountID, + BucketCount: 20, + From: timestamppb.New(from), + To: timestamppb.New(to), + })) + if err != nil { + return fmt.Errorf("querying summary data: %v", err) + } + + buckets := res.Msg.Buckets + + firstTimeformat := "'06 01/02 15:04:05" + width := res.Msg.BucketWidth.AsDuration() + if width < time.Microsecond { + firstTimeformat = "'06 01/02 15:04:05.000000000" + } else if width < time.Millisecond { + firstTimeformat = "'06 01/02 15:04:05.000000" + } else if width < time.Second { + firstTimeformat = "'06 01/02 15:04:05.000" + } else if width > 24*time.Hour { + firstTimeformat = "'06 01/02" + } + lastTimeFormat := "'06 01/02 15:04:05" + window := to.Sub(from) + if window < time.Microsecond { + lastTimeFormat = ".000000000" + } else if window < time.Millisecond { + lastTimeFormat = ".000000" + } else if window < time.Second { + lastTimeFormat = ".000" + } else if window < time.Minute { + lastTimeFormat = "05s" + } else if window < time.Hour { + lastTimeFormat = "15:04:05" + } else if window < 24*time.Hour { + lastTimeFormat = "15:04" + } else if window > 24*time.Hour { + lastTimeFormat = "'06 01/02" + } + stepTimeFormat := "'06 01/02 15:04:05" + if width < time.Microsecond { + stepTimeFormat = ".000000000" + } else if width < time.Millisecond { + stepTimeFormat = ".000000" + } else if width < time.Second { + stepTimeFormat = ".000" + } else if width < time.Minute { + stepTimeFormat = "05s" + } else if width < time.Hour { + stepTimeFormat = "15:04:05" + } else if width < 24*time.Hour { + stepTimeFormat = "15:04" + } else if width > 24*time.Hour { + stepTimeFormat = "'06 01/02" + } + + tslc := timeserieslinechart.New(termWidth, termHeight-3, + timeserieslinechart.WithTimeRange(from, to), + ) + tslc.XLabelFormatter = linechart.LabelFormatter(func(i int, f float64) string { + t := time.Unix(int64(f), 0).UTC() + var ts string + if i == 0 { + ts = t.Format(firstTimeformat) + } else if i == len(buckets)-1 { + ts = t.Format(lastTimeFormat) + } else { + ts = t.Format(stepTimeFormat) + } + log.Printf("label: ts=%v", ts) + return ts + }) + for _, bucket := range buckets { + log.Printf("ts=%v ev=%d", bucket.Ts.AsTime().Format(time.RFC3339Nano), bucket.GetEventCount()) + tslc.Push(timeserieslinechart.TimePoint{ + Time: bucket.Ts.AsTime(), + Value: float64(bucket.GetEventCount()), + }) + } + tslc.SetLineStyle(runes.ThinLineStyle) + tslc.Draw() + + fmt.Fprint(os.Stdout, tslc.View()) + + return nil + }, + } +} + +func queryApiWatchCmd( + getCtx func(cctx *cli.Context) context.Context, + getLogger func(cctx *cli.Context) *slog.Logger, + getCfg func(cctx *cli.Context) *config.Config, + getState func(cctx *cli.Context) *state.State, + getTokenSource func(cctx *cli.Context) *auth.UserRefreshableTokenSource, + getAPIUrl func(cctx *cli.Context) string, + getHTTPClient func(*cli.Context) *http.Client, +) cli.Command { + return cli.Command{ + Name: "watch", + Action: func(cctx *cli.Context) error { + ctx := getCtx(cctx) + state := getState(cctx) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + _, err := ensureLoggedIn(ctx, cctx, state, tokenSource, apiURL, httpClient) + if err != nil { + return err + } + ll := getLogger(cctx) + clOpts := connect.WithInterceptors( + auth.Interceptors(ll, tokenSource)..., + ) + queryClient := queryv1connect.NewQueryServiceClient(httpClient, apiURL, clOpts) + _ = queryClient + return nil + }, + } +} diff --git a/cmd/humanlog/versions.go b/cmd/humanlog/versions.go index b000b489..69ce2bdf 100644 --- a/cmd/humanlog/versions.go +++ b/cmd/humanlog/versions.go @@ -5,13 +5,14 @@ import ( "errors" "fmt" "log" + "log/slog" "net/http" "os" "runtime" "time" + "connectrpc.com/connect" "github.com/blang/semver" - "github.com/bufbuild/connect-go" "github.com/fatih/color" cliupdatepb "github.com/humanlogio/api/go/svc/cliupdate/v1" "github.com/humanlogio/api/go/svc/cliupdate/v1/cliupdatev1connect" @@ -19,6 +20,7 @@ import ( "github.com/humanlogio/humanlog/internal/pkg/config" "github.com/humanlogio/humanlog/internal/pkg/selfupdate" "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/auth" "github.com/mattn/go-isatty" "github.com/urfave/cli" ) @@ -45,10 +47,6 @@ func shouldPromptAboutUpdate() bool { return true } -var httpClient = &http.Client{ - Transport: &http.Transport{}, -} - func reqMeta(st *state.State) *types.ReqMeta { req := new(types.ReqMeta) if st == nil { @@ -97,8 +95,12 @@ const versionCmdName = "version" func versionCmd( getCtx func(cctx *cli.Context) context.Context, + getLogger func(cctx *cli.Context) *slog.Logger, getCfg func(cctx *cli.Context) *config.Config, getState func(cctx *cli.Context) *state.State, + getTokenSource func(cctx *cli.Context) *auth.UserRefreshableTokenSource, + getAPIUrl func(cctx *cli.Context) string, + getHTTPClient func(*cli.Context) *http.Client, ) cli.Command { return cli.Command{ Name: versionCmdName, @@ -109,9 +111,13 @@ func versionCmd( Usage: "checks whether a newer version is available", Action: func(cctx *cli.Context) error { ctx := getCtx(cctx) + ll := getLogger(cctx) cfg := getCfg(cctx) state := getState(cctx) - nextVersion, nextArtifact, hasUpdate, err := checkForUpdate(ctx, cfg, state) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + nextVersion, nextArtifact, hasUpdate, err := checkForUpdate(ctx, ll, cfg, state, apiURL, httpClient, tokenSource) if err != nil { return err } @@ -135,9 +141,13 @@ func versionCmd( Usage: "self-update to the latest version", Action: func(cctx *cli.Context) error { ctx := getCtx(cctx) + ll := getLogger(cctx) cfg := getCfg(cctx) state := getState(cctx) - _, _, hasUpdate, err := checkForUpdate(ctx, cfg, state) + tokenSource := getTokenSource(cctx) + apiURL := getAPIUrl(cctx) + httpClient := getHTTPClient(cctx) + _, _, hasUpdate, err := checkForUpdate(ctx, ll, cfg, state, apiURL, httpClient, tokenSource) if err != nil { return err } @@ -152,8 +162,6 @@ func versionCmd( } } -const apiURL = "https://api.humanlog.io" - type checkForUpdateReq struct { arch string os string @@ -165,13 +173,14 @@ type checkForUpdateRes struct { hasUpdate bool } -func checkForUpdate(ctx context.Context, cfg *config.Config, state *state.State) (v *types.Version, a *types.VersionArtifact, hasUpdate bool, err error) { +func checkForUpdate(ctx context.Context, ll *slog.Logger, cfg *config.Config, state *state.State, apiURL string, httpClient *http.Client, tokenSource *auth.UserRefreshableTokenSource) (v *types.Version, a *types.VersionArtifact, hasUpdate bool, err error) { currentSV, err := version.AsSemver() if err != nil { return nil, nil, false, err } - - updateClient := cliupdatev1connect.NewUpdateServiceClient(httpClient, apiURL) + var clOpts []connect.ClientOption + clOpts = append(clOpts, connect.WithInterceptors(auth.NewRefreshedUserAuthInterceptor(ll, tokenSource))) + updateClient := cliupdatev1connect.NewUpdateServiceClient(httpClient, apiURL, clOpts...) res, err := updateClient.GetNextUpdate(ctx, connect.NewRequest(&cliupdatepb.GetNextUpdateRequest{ ProjectName: "humanlog", CurrentVersion: version, @@ -196,11 +205,11 @@ func checkForUpdate(ctx context.Context, cfg *config.Config, state *state.State) return msg.NextVersion, msg.NextArtifact, currentSV.LT(nextSV), nil } -func asyncCheckForUpdate(ctx context.Context, req *checkForUpdateReq, cfg *config.Config, state *state.State) <-chan *checkForUpdateRes { +func asyncCheckForUpdate(ctx context.Context, ll *slog.Logger, cfg *config.Config, state *state.State, apiURL string, httpClient *http.Client, tokenSource *auth.UserRefreshableTokenSource) <-chan *checkForUpdateRes { out := make(chan *checkForUpdateRes, 1) go func() { defer close(out) - nextVersion, _, hasUpdate, err := checkForUpdate(ctx, cfg, state) + nextVersion, _, hasUpdate, err := checkForUpdate(ctx, ll, cfg, state, apiURL, httpClient, tokenSource) if err != nil { if errors.Is(errors.Unwrap(err), context.Canceled) { return diff --git a/docker_compose_handler.go b/docker_compose_handler.go index 43bbfdc6..46c2b3ab 100644 --- a/docker_compose_handler.go +++ b/docker_compose_handler.go @@ -3,7 +3,7 @@ package humanlog import ( "regexp" - "github.com/humanlogio/humanlog/internal/pkg/model" + typesv1 "github.com/humanlogio/api/go/types/v1" ) // dcLogsPrefixRe parses out a prefix like 'web_1 | ' from docker-compose @@ -16,14 +16,14 @@ import ( var dcLogsPrefixRe = regexp.MustCompile("^(?:\x1b\\[\\d+m)?(?P[a-zA-Z0-9._-]+)\\s+\\|(?:\x1b\\[0m)? (?P.*)$") type handler interface { - TryHandle([]byte, *model.Structured) bool + TryHandle([]byte, *typesv1.StructuredLogEvent) bool } -func tryDockerComposePrefix(d []byte, ev *model.Structured, nextHandler handler) bool { +func tryDockerComposePrefix(d []byte, ev *typesv1.StructuredLogEvent, nextHandler handler) bool { matches := dcLogsPrefixRe.FindSubmatch(d) if matches != nil { if nextHandler.TryHandle(matches[2], ev) { - ev.KVs = append(ev.KVs, model.KV{ + ev.Kvs = append(ev.Kvs, &typesv1.KV{ Key: "service", Value: string(matches[1]), }) return true @@ -33,7 +33,7 @@ func tryDockerComposePrefix(d []byte, ev *model.Structured, nextHandler handler) switch h := nextHandler.(type) { case *JSONHandler: if tryZapDevDCPrefix(matches[2], ev, h) { - ev.KVs = append(ev.KVs, model.KV{ + ev.Kvs = append(ev.Kvs, &typesv1.KV{ Key: "service", Value: string(matches[1]), }) return true diff --git a/go.mod b/go.mod index 8d07ab69..dafe796b 100644 --- a/go.mod +++ b/go.mod @@ -1,28 +1,69 @@ module github.com/humanlogio/humanlog -go 1.19 +go 1.23 require ( + connectrpc.com/connect v1.16.2 + connectrpc.com/cors v0.1.0 + github.com/99designs/keyring v1.2.2 + github.com/NimbleMarkets/ntcharts v0.1.2 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 github.com/blang/semver v3.5.1+incompatible - github.com/bufbuild/connect-go v1.4.1 + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v1.1.1 + github.com/charmbracelet/huh v0.4.2 + github.com/charmbracelet/lipgloss v0.13.0 + github.com/charmbracelet/x/term v0.2.0 github.com/cli/safeexec v1.0.1 - github.com/fatih/color v1.13.0 + github.com/fatih/color v1.16.0 github.com/go-logfmt/logfmt v0.5.1 - github.com/humanlogio/api/go v0.0.0-20230114081617-9054c2994bcc + github.com/google/uuid v1.6.0 + github.com/humanlogio/api/go v0.0.0-20231208063410-e088b7d026d1 github.com/kr/logfmt v0.0.0-20210122060352-19f9bcb100e6 + github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e + github.com/matoous/go-nanoid v1.5.0 github.com/mattn/go-colorable v0.1.13 - github.com/mattn/go-isatty v0.0.16 - github.com/stretchr/testify v1.8.1 - github.com/urfave/cli v1.22.10 + github.com/mattn/go-isatty v0.0.20 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c + github.com/rs/cors v1.11.0 + github.com/stretchr/testify v1.8.4 + github.com/urfave/cli v1.22.14 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + golang.org/x/net v0.23.0 + golang.org/x/sync v0.8.0 + golang.org/x/sys v0.25.0 + gonum.org/v1/gonum v0.15.1 + google.golang.org/protobuf v1.33.0 ) require ( + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.2.0 // indirect + github.com/charmbracelet/x/ansi v0.3.1 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/sys v0.1.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.18.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +replace github.com/humanlogio/api/go => /Users/antoine/code/src/github.com/humanlogio/api/go/ diff --git a/go.sum b/go.sum index 23835e85..763fd3f2 100644 --- a/go.sum +++ b/go.sum @@ -1,64 +1,146 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +connectrpc.com/connect v1.16.2 h1:ybd6y+ls7GOlb7Bh5C8+ghA6SvCBajHwxssO2CGFjqE= +connectrpc.com/connect v1.16.2/go.mod h1:n2kgwskMHXC+lVqb18wngEpF95ldBHXjZYJussz5FRc= +connectrpc.com/cors v0.1.0 h1:f3gTXJyDZPrDIZCQ567jxfD9PAIpopHiRDnJRt3QuOQ= +connectrpc.com/cors v0.1.0/go.mod h1:v8SJZCPfHtGH1zsm+Ttajpozd4cYIUryl4dFB6QEpfg= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/NimbleMarkets/ntcharts v0.1.2 h1:iW1aiOif/Dm74sQd18opi10RMED5589cVhy9SGp98Tw= +github.com/NimbleMarkets/ntcharts v0.1.2/go.mod h1:WcHS7kc8oQctN1543DeV9a+gOrS4DDVfKp1N9RZFUqc= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= +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/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bufbuild/connect-go v1.4.1 h1:6usL3JGjKhxQpvDlizP7u8VfjAr1JkckcAUbrdcbgNY= -github.com/bufbuild/connect-go v1.4.1/go.mod h1:9iNvh/NOsfhNBUH5CtvXeVUskQO1xsrEviH7ZArwZ3I= +github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= +github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqKOCvppbPY= +github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= +github.com/charmbracelet/huh v0.4.2 h1:5wLkwrA58XDAfEZsJzNQlfJ+K8N9+wYwvR5FOM7jXFM= +github.com/charmbracelet/huh v0.4.2/go.mod h1:g9OXBgtY3zRV4ahnVih9bZE+1yGYN+y2C9Q6L2P+WM0= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= +github.com/charmbracelet/x/ansi v0.3.1 h1:CRO6lc/6HCx2/D6S/GZ87jDvRvk6GtPyFP+IljkNtqI= +github.com/charmbracelet/x/ansi v0.3.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +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/x/exp/strings v0.0.0-20240524151031-ff83003bf67a h1:lOpqe2UvPmlln41DGoii7wlSZ/q8qGIon5JJ8Biu46I= +github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE= +github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/humanlogio/api/go v0.0.0-20230114081617-9054c2994bcc h1:gYloPWoTui1LUo/n2FqcdyAosbCupP3N3vn9EJYickM= -github.com/humanlogio/api/go v0.0.0-20230114081617-9054c2994bcc/go.mod h1:DkpFdPohT0dBEJvo9lYEHJZ7qdGNQpsw89an+GXqMUU= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/kr/logfmt v0.0.0-20210122060352-19f9bcb100e6 h1:ZK1mH67KVyVW/zOLu0xLva+f6xJ8vt+LGrkQq5FJYLY= github.com/kr/logfmt v0.0.0-20210122060352-19f9bcb100e6/go.mod h1:JIiJcj9TX57tEvCXjm6eaHd2ce4pZZf9wzYuThq45u8= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e h1:OLwZ8xVaeVrru0xyeuOX+fne0gQTFEGlzfNjipCbxlU= +github.com/lrstanley/bubblezone v0.0.0-20240914071701-b48c55a5e78e/go.mod h1:NQ34EGeu8FAYGBMDzwhfNJL8YQYoWZP5xYJPRDAwN3E= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= +github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +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/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= -github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= +github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gonum.org/v1/gonum v0.15.1 h1:FNy7N6OUZVUaWG9pTiD+jlhdQ3lMP+/LcTpJ6+a8sQ0= +gonum.org/v1/gonum v0.15.1/go.mod h1:eZTZuRFrzu5pcyjN5wJhcIhnUdNijYxX1T2IcrOGY0o= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/localstorage/memory.go b/internal/localstorage/memory.go new file mode 100644 index 00000000..c4ef39d8 --- /dev/null +++ b/internal/localstorage/memory.go @@ -0,0 +1,199 @@ +package localstorage + +import ( + "context" + "fmt" + "slices" + "sync" + "time" + + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/pkg/sink" + "google.golang.org/protobuf/proto" +) + +var ( + _ Queryable = (*MemStorage)(nil) + _ Storage = (*MemStorage)(nil) + _ sink.Sink = (*MemStorageSink)(nil) + _ sink.BatchSink = (*MemStorageSink)(nil) +) + +type MemStorage struct { + heartbeat time.Duration + sinksMu sync.Mutex + sinks []*MemStorageSink +} + +func NewMemStorage() *MemStorage { + return &MemStorage{heartbeat: time.Hour} +} + +type SummarizedEvents struct { + BucketWidth time.Duration + Buckets []struct { + Time time.Time + EventCount int + } +} + +func (str *MemStorage) Query(ctx context.Context, q *typesv1.LogQuery) ([]Cursor, error) { + if q.From.AsTime().After(q.To.AsTime()) { + return nil, fmt.Errorf("invalid query, `to` is before `from`") + } + + str.sinksMu.Lock() + defer str.sinksMu.Unlock() + var cursors []Cursor + for _, snk := range str.sinks { + if idx, ok, err := snk.firstMatch(ctx, q); err != nil { + return nil, err + } else if ok { + cursors = append(cursors, &MemSinkCursor{q: q, cur: idx, next: idx, more: true, sink: snk}) + } + } + return cursors, nil +} + +func (str *MemStorage) Heartbeat(ctx context.Context, machineID, sessionID int64) (time.Duration, error) { + return str.heartbeat, nil +} + +func (str *MemStorage) SinkFor(machineID, sessionID int64) (sink.Sink, time.Duration, error) { + str.sinksMu.Lock() + defer str.sinksMu.Unlock() + id := SinkID{machineID: machineID, sessionID: sessionID} + loc, ok := slices.BinarySearchFunc(str.sinks, id, func(mss *MemStorageSink, si SinkID) int { + return mss.id.cmp(si) + }) + if ok { + return str.sinks[loc], time.Hour, nil + } + newsink := &MemStorageSink{id: id} + str.sinks = slices.Insert(str.sinks, loc, newsink) + return newsink, str.heartbeat, nil +} + +type MemSinkCursor struct { + q *typesv1.LogQuery + + cur int + next int + more bool + + sink *MemStorageSink + err error +} + +func (crs *MemSinkCursor) IDs() (machineID, sessionID int64) { + return crs.sink.id.machineID, crs.sink.id.sessionID +} + +func (crs *MemSinkCursor) Next(ctx context.Context) bool { + hasCurrent := crs.cur >= 0 && crs.more + if !hasCurrent { + return false + } + crs.cur = crs.next + crs.next = crs.next + 1 + crs.next, crs.more, crs.err = crs.sink.nextMatch(ctx, crs.q, crs.next) + return hasCurrent && crs.err == nil +} + +func (crs *MemSinkCursor) Event() *typesv1.LogEvent { + return crs.sink.evs[crs.cur] +} + +func (crs *MemSinkCursor) Err() error { + return crs.err +} + +type SinkID struct { + machineID int64 + sessionID int64 +} + +func (sid SinkID) cmp(other SinkID) int { + mid := sid.machineID - other.machineID + if mid != 0 { + return int(mid) + } + return int(sid.sessionID - other.sessionID) +} + +type MemStorageSink struct { + mu sync.RWMutex + id SinkID + evs []*typesv1.LogEvent +} + +func (snk *MemStorageSink) firstMatch(_ context.Context, q *typesv1.LogQuery) (index int, ok bool, err error) { + snk.mu.RLock() + defer snk.mu.RUnlock() + + for i, ev := range snk.evs { + if eventMatches(q, ev) { + return i, true, nil + } + } + return -1, false, nil +} + +func (snk *MemStorageSink) nextMatch(_ context.Context, q *typesv1.LogQuery, fromIndex int) (index int, ok bool, err error) { + snk.mu.RLock() + defer snk.mu.RUnlock() + if len(snk.evs) < fromIndex { + return -1, false, nil + } + for i, ev := range snk.evs[fromIndex:] { + if eventMatches(q, ev) { + return fromIndex + i, true, nil + } + } + return -1, false, nil +} + +func eventMatches(q *typesv1.LogQuery, ev *typesv1.LogEvent) bool { + ts := ev.ParsedAt.AsTime() + from := q.From.AsTime() + to := q.To.AsTime() + atOrAfter := ts.Equal(from) || ts.After(from) + before := ts.Before(to) + + // TODO(antoine): match more stuff on the query + + return atOrAfter && before +} + +func (snk *MemStorageSink) Receive(ctx context.Context, ev *typesv1.LogEvent) error { + snk.mu.Lock() + defer snk.mu.Unlock() + snk.receive(ev) + return nil +} + +func (snk *MemStorageSink) ReceiveBatch(ctx context.Context, evs []*typesv1.LogEvent) error { + snk.mu.Lock() + defer snk.mu.Unlock() + for _, ev := range evs { + snk.receive(ev) + } + return nil +} + +func (snk *MemStorageSink) receive(ev *typesv1.LogEvent) { + ev = proto.Clone(ev).(*typesv1.LogEvent) + if len(snk.evs) == 0 { + snk.evs = append(snk.evs, ev) + return + } + lastEv := snk.evs[len(snk.evs)-1] + if (lastEv.ParsedAt.Seconds > ev.ParsedAt.Seconds) || + (lastEv.ParsedAt.Seconds == ev.ParsedAt.Seconds && lastEv.ParsedAt.Nanos > ev.ParsedAt.Nanos) { + panic("out of order inserts within same sink?") + } + snk.evs = append(snk.evs, ev) + +} + +func (snk *MemStorageSink) Flush(ctx context.Context) error { return nil } diff --git a/internal/localstorage/memory_test.go b/internal/localstorage/memory_test.go new file mode 100644 index 00000000..5f37592d --- /dev/null +++ b/internal/localstorage/memory_test.go @@ -0,0 +1,202 @@ +package localstorage + +import ( + "context" + "testing" + "time" + + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestMemoryStorage(t *testing.T) { + tests := []struct { + name string + q *typesv1.LogQuery + input []*typesv1.LogEventGroup + want []*typesv1.LogEventGroup + }{ + { + name: "nothing", + q: &typesv1.LogQuery{ + To: timestamppb.New(musttime("2006-01-02T15:04:06.000")), + }, + input: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + want: []*typesv1.LogEventGroup{}, + }, + { + name: "all", + q: &typesv1.LogQuery{ + To: timestamppb.New(musttime("2006-01-02T15:04:06.005")), + }, + input: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + want: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + }, + { + name: "skip last", + q: &typesv1.LogQuery{ + To: timestamppb.New(musttime("2006-01-02T15:04:06.004")), + }, + input: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + want: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + }, + }, + }, + }, + { + name: "skip first", + q: &typesv1.LogQuery{ + From: timestamppb.New(musttime("2006-01-02T15:04:06.002")), + To: timestamppb.New(musttime("2006-01-02T15:04:06.005")), + }, + input: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + want: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + }, + { + name: "slice", + q: &typesv1.LogQuery{ + From: timestamppb.New(musttime("2006-01-02T15:04:06.002")), + To: timestamppb.New(musttime("2006-01-02T15:04:06.004")), + }, + input: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + want: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + }, + }, + }, + }, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + mem := NewMemStorage() + + for _, leg := range tt.input { + snk, _, err := mem.SinkFor(leg.MachineId, leg.SessionId) + require.NoError(t, err) + for _, ev := range leg.Logs { + err = snk.Receive(ctx, ev) + require.NoError(t, err) + } + err = snk.Flush(ctx) + require.NoError(t, err) + } + + cursors, err := mem.Query(ctx, tt.q) + require.NoError(t, err) + got := drainCursors(t, ctx, cursors) + + require.Len(t, got, len(tt.want)) + for i := range tt.want { + require.Equal(t, protojson.Format(tt.want[i]), protojson.Format(got[i])) + } + }) + } +} + +func drainCursors(t *testing.T, ctx context.Context, cursors []Cursor) []*typesv1.LogEventGroup { + out := make([]*typesv1.LogEventGroup, 0, len(cursors)) + for _, cursor := range cursors { + mid, sid := cursor.IDs() + leg := &typesv1.LogEventGroup{ + MachineId: mid, SessionId: sid, + } + for cursor.Next(ctx) { + leg.Logs = append(leg.Logs, cursor.Event()) + } + require.NoError(t, cursor.Err()) + out = append(out, leg) + } + return out +} + +func musttime(str string) time.Time { + t, err := time.Parse("2006-01-02T15:04:05.000", str) + if err != nil { + panic(err) + } + return t +} diff --git a/internal/localstorage/queryable.go b/internal/localstorage/queryable.go new file mode 100644 index 00000000..fc99c6f7 --- /dev/null +++ b/internal/localstorage/queryable.go @@ -0,0 +1,26 @@ +package localstorage + +import ( + "context" + "time" + + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/pkg/sink" +) + +type Storage interface { + Queryable + SinkFor(machineID, sessionID int64) (_ sink.Sink, heartbeatIn time.Duration, _ error) + Heartbeat(ctx context.Context, machineID, sessionID int64) (time.Duration, error) +} + +type Queryable interface { + Query(context.Context, *typesv1.LogQuery) ([]Cursor, error) +} + +type Cursor interface { + IDs() (machineID, sessionID int64) + Next(context.Context) bool + Event() *typesv1.LogEvent + Err() error +} diff --git a/internal/localsvc/svc.go b/internal/localsvc/svc.go new file mode 100644 index 00000000..08567315 --- /dev/null +++ b/internal/localsvc/svc.go @@ -0,0 +1,320 @@ +package localsvc + +import ( + "context" + "fmt" + "log/slog" + "slices" + "time" + + "connectrpc.com/connect" + igv1 "github.com/humanlogio/api/go/svc/ingest/v1" + igsvcpb "github.com/humanlogio/api/go/svc/ingest/v1/ingestv1connect" + lhv1 "github.com/humanlogio/api/go/svc/localhost/v1" + lhsvcpb "github.com/humanlogio/api/go/svc/localhost/v1/localhostv1connect" + qrv1 "github.com/humanlogio/api/go/svc/query/v1" + qrsvcpb "github.com/humanlogio/api/go/svc/query/v1/queryv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/internal/localstorage" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/sink" + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type Service struct { + ll *slog.Logger + state *state.State + ownVersion *typesv1.Version + storage localstorage.Storage +} + +func New(ll *slog.Logger, state *state.State, ownVersion *typesv1.Version, storage localstorage.Storage) *Service { + return &Service{ll: ll, state: state, ownVersion: ownVersion, storage: storage} +} + +var ( + _ lhsvcpb.LocalhostServiceHandler = (*Service)(nil) + _ igsvcpb.IngestServiceHandler = (*Service)(nil) + _ qrsvcpb.QueryServiceHandler = (*Service)(nil) +) + +func (svc *Service) Ping(ctx context.Context, req *connect.Request[lhv1.PingRequest]) (*connect.Response[lhv1.PingResponse], error) { + res := &lhv1.PingResponse{ + ClientVersion: svc.ownVersion, + Meta: &typesv1.ResMeta{}, + } + if svc.state.AccountID != nil && svc.state.MachineID != nil { + res.Meta = &typesv1.ResMeta{ + AccountId: *svc.state.AccountID, + MachineId: *svc.state.MachineID, + } + } + return connect.NewResponse(res), nil +} + +func (svc *Service) GetHeartbeat(ctx context.Context, req *connect.Request[igv1.GetHeartbeatRequest]) (*connect.Response[igv1.GetHeartbeatResponse], error) { + msg := req.Msg + if msg.MachineId == nil { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("no machine ID present, ensure you're logged in (or authorized) to obtain a machine ID")) + } + sessionID := int64(0) + if msg.SessionId != nil { + sessionID = int64(*msg.SessionId) + } + heartbeat, err := svc.storage.Heartbeat(ctx, int64(*msg.MachineId), sessionID) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + return connect.NewResponse(&igv1.GetHeartbeatResponse{ + HeartbeatIn: durationpb.New(heartbeat), + }), nil +} + +func (svc *Service) Ingest(ctx context.Context, req *connect.Request[igv1.IngestRequest]) (*connect.Response[igv1.IngestResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, fmt.Errorf("not available on localhost")) +} + +func (svc *Service) IngestStream(ctx context.Context, req *connect.ClientStream[igv1.IngestStreamRequest]) (*connect.Response[igv1.IngestStreamResponse], error) { + ll := svc.ll + + var ( + machineID int64 + sessionID int64 + ) + + // get the first message which has the metadata to start ingesting + if !req.Receive() { + msg := req.Msg() + machineID = int64(msg.MachineId) + sessionID = int64(msg.SessionId) + if sessionID == 0 { + sessionID = time.Now().UnixNano() + } + if machineID == 0 && svc.state.MachineID != nil { + machineID = int64(*svc.state.MachineID) + } + } + ll = ll.With( + slog.Int64("machine_id", machineID), + slog.Int64("session_id", sessionID), + ) + snk, heartbeatIn, err := svc.storage.SinkFor(machineID, sessionID) + if err != nil { + ll.ErrorContext(ctx, "obtaining sink for stream", slog.Any("err", err)) + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("obtaining sink for stream: %v", err)) + } + defer func() { + if ferr := snk.Flush(ctx); ferr != nil { + if err == nil { + err = ferr + } else { + ll.ErrorContext(ctx, "erroneous exit and also failed to flush", slog.Any("err", err)) + } + } + }() + if bsnk, ok := snk.(sink.BatchSink); ok { + for req.Receive() { + msg := req.Msg() + if err := bsnk.ReceiveBatch(ctx, msg.Events); err != nil { + ll.ErrorContext(ctx, "ingesting event batch", slog.Any("err", err)) + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ingesting event batch: %v", err)) + } + } + } else { + for req.Receive() { + msg := req.Msg() + for _, ev := range msg.Events { + if err := snk.Receive(ctx, ev); err != nil { + ll.ErrorContext(ctx, "ingesting event", slog.Any("err", err)) + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ingesting event: %v", err)) + } + } + } + } + if err := req.Err(); err != nil { + ll.ErrorContext(ctx, "ingesting localhost stream", slog.Any("err", err)) + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ingesting localhost stream: %v", err)) + } + res := &igv1.IngestStreamResponse{ + SessionId: uint64(sessionID), + HeartbeatIn: durationpb.New(heartbeatIn), + } + return connect.NewResponse(res), nil +} + +func (svc *Service) IngestBidiStream(ctx context.Context, req *connect.BidiStream[igv1.IngestBidiStreamRequest, igv1.IngestBidiStreamResponse]) error { + return connect.NewError(connect.CodeUnimplemented, fmt.Errorf("not available on localhost")) +} + +func (svc *Service) SummarizeEvents(ctx context.Context, req *connect.Request[qrv1.SummarizeEventsRequest]) (*connect.Response[qrv1.SummarizeEventsResponse], error) { + if req.Msg.From == nil { + req.Msg.From = timestamppb.New(time.Now().Add(-time.Minute)) + } + if req.Msg.To == nil { + req.Msg.To = timestamppb.Now() + } + ll := svc.ll.With( + slog.Time("from", req.Msg.From.AsTime()), + slog.Time("to", req.Msg.From.AsTime()), + slog.Int("bucket_count", int(req.Msg.BucketCount)), + slog.Int("account_id", int(req.Msg.AccountId)), + ) + + cursors, err := svc.storage.Query(ctx, &typesv1.LogQuery{ + From: req.Msg.From, + To: req.Msg.To, + }) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("summarizing local storage: %v", err)) + } + ll = ll.With(slog.Int("cursor_len", len(cursors))) + ll.DebugContext(ctx, "queried, got cursors") + + from := req.Msg.From.AsTime() + to := req.Msg.To.AsTime() + width := to.Sub(from) / time.Duration(req.Msg.BucketCount) + + type bucket struct { + ts time.Time + count int + } + var buckets []bucket + for now := from; now.Before(to) || now.Equal(to); now = now.Add(width) { + buckets = append(buckets, bucket{ts: now}) + } + ll = ll.With(slog.Duration("width", width)) + + for _, cursor := range cursors { + for cursor.Next(ctx) { + ts := cursor.Event().ParsedAt.AsTime().Truncate(width) + loc, _ := slices.BinarySearchFunc(buckets, ts, func(a bucket, t time.Time) int { + return a.ts.Compare(t) + }) + buckets[loc].count++ + } + if err := cursor.Err(); err != nil { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("counting summary: %v", err)) + } + } + ll.DebugContext(ctx, "iterated all cursors") + out := &qrv1.SummarizeEventsResponse{ + BucketWidth: durationpb.New(width), + } + for _, bucket := range buckets { + if bucket.count == 0 { + continue + } + out.Buckets = append(out.Buckets, &qrv1.SummarizeEventsResponse_Bucket{ + Ts: timestamppb.New(bucket.ts), + EventCount: uint64(bucket.count), + }) + } + ll.DebugContext(ctx, "non-zero buckets filled", slog.Int("buckets_len", len(out.Buckets))) + return connect.NewResponse(out), nil +} +func (svc *Service) WatchQuery(ctx context.Context, req *connect.Request[qrv1.WatchQueryRequest], stream *connect.ServerStream[qrv1.WatchQueryResponse]) error { + ll := svc.ll.With( + slog.Any("query", req.Msg.GetQuery().String()), + ) + cursors, err := svc.storage.Query(ctx, req.Msg.Query) + if err != nil { + return connect.NewError(connect.CodeInternal, fmt.Errorf("querying local storage: %v", err)) + } + + legc := make(chan *typesv1.LogEventGroup) + + iterateCursor := func(ctx context.Context, cursor localstorage.Cursor) error { + var ( + lastSend = time.Now() + machineID, sessionID = cursor.IDs() + evs []*typesv1.LogEvent + ) + for cursor.Next(ctx) { + evs = append(evs, cursor.Event()) + now := time.Now() + if now.Sub(lastSend) > 100*time.Millisecond { + lastSend = now + select { + case legc <- &typesv1.LogEventGroup{ + MachineId: machineID, + SessionId: sessionID, + Logs: evs, + }: + case <-ctx.Done(): + return nil + } + evs = evs[:0] + } + } + select { + case legc <- &typesv1.LogEventGroup{ + MachineId: machineID, + SessionId: sessionID, + Logs: evs, + }: + case <-ctx.Done(): + } + return nil + } + + cursorCtx, cancelCursors := context.WithCancel(ctx) + defer cancelCursors() + eg, cursorCtx := errgroup.WithContext(cursorCtx) + for _, cursor := range cursors { + cursor := cursor + eg.Go(func() error { return iterateCursor(cursorCtx, cursor) }) + } + + doneSending := make(chan struct{}) + go func() { + defer func() { + close(doneSending) + cancelCursors() + }() + var ( + sender = time.NewTicker(100 * time.Millisecond) + legs []*typesv1.LogEventGroup + ) + defer sender.Stop() + wait_for_more_leg: + for { + select { + case <-ctx.Done(): + return + case leg := <-legc: + // try to append to an existing LEG first + for _, eleg := range legs { + if eleg.MachineId == leg.MachineId && + eleg.SessionId == leg.SessionId { + eleg.Logs = append(eleg.Logs, leg.Logs...) + continue wait_for_more_leg + } + } + // didn't have an existing LEG for it, add it + legs = append(legs, leg) + case <-sender.C: + err := stream.Send(&qrv1.WatchQueryResponse{ + Events: legs, + }) + legs = legs[:0] + if err != nil { + ll.ErrorContext(ctx, "failed to send response", slog.Any("err", err)) + return + } + } + } + }() + + err = eg.Wait() + close(legc) + if err != nil { + return connect.NewError(connect.CodeInternal, fmt.Errorf("streaming localhost log for query: %v", err)) + } + select { + case <-ctx.Done(): + case <-doneSending: + } + return nil +} diff --git a/internal/localsvc/svc_test.go b/internal/localsvc/svc_test.go new file mode 100644 index 00000000..48c259d9 --- /dev/null +++ b/internal/localsvc/svc_test.go @@ -0,0 +1,152 @@ +package localsvc + +import ( + "context" + "log/slog" + "os" + "testing" + "time" + + "connectrpc.com/connect" + qrv1 "github.com/humanlogio/api/go/svc/query/v1" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/internal/localstorage" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestSummarize(t *testing.T) { + + tests := []struct { + name string + req *qrv1.SummarizeEventsRequest + input []*typesv1.LogEventGroup + want *qrv1.SummarizeEventsResponse + }{ + { + name: "all", + req: &qrv1.SummarizeEventsRequest{ + From: timestamppb.New(musttime("2006-01-02T15:04:06.001")), + To: timestamppb.New(musttime("2006-01-02T15:04:06.005")), + BucketCount: 100, + }, + input: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + want: &qrv1.SummarizeEventsResponse{ + BucketWidth: durationpb.New(40 * time.Microsecond), + Buckets: []*qrv1.SummarizeEventsResponse_Bucket{ + {Ts: timestamppb.New(musttime("2006-01-02T15:04:06.001")), EventCount: 1}, + {Ts: timestamppb.New(musttime("2006-01-02T15:04:06.002")), EventCount: 1}, + {Ts: timestamppb.New(musttime("2006-01-02T15:04:06.003")), EventCount: 1}, + {Ts: timestamppb.New(musttime("2006-01-02T15:04:06.004")), EventCount: 1}, + }, + }, + }, + { + name: "one hour long, all data in 1 bucket", + req: &qrv1.SummarizeEventsRequest{ + From: timestamppb.New(musttime("2006-01-02T14:04:06.005")), + To: timestamppb.New(musttime("2006-01-02T15:04:06.005")), + BucketCount: 60, + }, + input: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + want: &qrv1.SummarizeEventsResponse{ + BucketWidth: durationpb.New(60 * time.Second), + Buckets: []*qrv1.SummarizeEventsResponse_Bucket{ + {Ts: timestamppb.New(musttime("2006-01-02T15:04:06.005")), EventCount: 4}, + }, + }, + }, + { + name: "one hour long, two session data in 2 bucket", + req: &qrv1.SummarizeEventsRequest{ + From: timestamppb.New(musttime("2006-01-02T14:04:06.005")), + To: timestamppb.New(musttime("2006-01-02T15:04:06.005")), + BucketCount: 60, + }, + input: []*typesv1.LogEventGroup{ + { + MachineId: 1, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T15:04:06.004")), Raw: []byte("hello world 4")}, + }, + }, + { + MachineId: 4, SessionId: 2, + Logs: []*typesv1.LogEvent{ + {ParsedAt: timestamppb.New(musttime("2006-01-02T14:45:06.001")), Raw: []byte("hello world 1")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T14:45:06.002")), Raw: []byte("hello world 2")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T14:45:06.003")), Raw: []byte("hello world 3")}, + {ParsedAt: timestamppb.New(musttime("2006-01-02T14:45:06.004")), Raw: []byte("hello world 4")}, + }, + }, + }, + want: &qrv1.SummarizeEventsResponse{ + BucketWidth: durationpb.New(60 * time.Second), + Buckets: []*qrv1.SummarizeEventsResponse_Bucket{ + {Ts: timestamppb.New(musttime("2006-01-02T14:45:06.005")), EventCount: 4}, + {Ts: timestamppb.New(musttime("2006-01-02T15:04:06.005")), EventCount: 4}, + }, + }, + }, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ll := slog.New(slog.NewTextHandler(os.Stderr, nil)) + mem := localstorage.NewMemStorage() + + for _, leg := range tt.input { + snk, _, err := mem.SinkFor(leg.MachineId, leg.SessionId) + require.NoError(t, err) + for _, ev := range leg.Logs { + err = snk.Receive(ctx, ev) + require.NoError(t, err) + } + err = snk.Flush(ctx) + require.NoError(t, err) + } + + svc := New(ll, nil, nil, mem) + got, err := svc.SummarizeEvents(ctx, connect.NewRequest(tt.req)) + require.NoError(t, err) + require.Equal(t, protojson.Format(tt.want), protojson.Format(got.Msg)) + + }) + } + +} + +func musttime(str string) time.Time { + t, err := time.Parse("2006-01-02T15:04:05.000", str) + if err != nil { + panic(err) + } + return t +} diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 1d812ffa..2fc02d87 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -92,6 +92,13 @@ type Config struct { Palette *TextPalette `json:"palette"` Interrupt *bool `json:"interrupt"` SkipCheckForUpdates *bool `json:"skip_check_updates"` + + ExperimentalFeatures *Features `json:"experimental_features"` +} + +type Features struct { + SendLogsToCloud *bool `json:"send_logs_to_cloud"` + ServeLocalhostOnPort *int `json:"serve_localhost_on_port"` } func (cfg Config) populateEmpty(other *Config) *Config { diff --git a/internal/pkg/iterapi/iter.go b/internal/pkg/iterapi/iter.go new file mode 100644 index 00000000..0f34ad8d --- /dev/null +++ b/internal/pkg/iterapi/iter.go @@ -0,0 +1,71 @@ +package iterapi + +import ( + "context" + + typesv1 "github.com/humanlogio/api/go/types/v1" +) + +type Lister[Elem any] func(ctx context.Context, cursor *typesv1.Cursor, limit int32) ([]Elem, *typesv1.Cursor, error) + +type Iter[Elem any] struct { + ctx context.Context + lister Lister[Elem] + limit int32 + + i int + items []Elem + cursor *typesv1.Cursor + err error +} + +func New[Elem any](ctx context.Context, limit int32, lister Lister[Elem]) *Iter[Elem] { + return &Iter[Elem]{ + ctx: ctx, + lister: lister, + limit: limit, + } +} + +func (iter *Iter[Elem]) Next() bool { + + if iter.i == -1 || // first call + iter.i+1 >= len(iter.items) { // or reached last item + + if len(iter.items) > 0 && len(iter.items) < int(iter.limit) { + // last list call returned less than limit + return false + } + iter.items, iter.cursor, iter.err = iter.lister(iter.ctx, iter.cursor, iter.limit) + if iter.err != nil { + return false + } + if len(iter.items) == 0 { + return false + } + iter.i = 0 + } else { + iter.i++ + } + + return true +} + +func (iter *Iter[Elem]) Current() Elem { + return iter.items[iter.i] +} + +func (iter *Iter[Elem]) Err() error { + return iter.err +} + +func Find[Elem any](iter *Iter[Elem], lookup func(Elem) bool) (Elem, bool, error) { + for iter.Next() { + cur := iter.Current() + if lookup(cur) { + return cur, true, nil + } + } + var e Elem + return e, false, iter.Err() +} diff --git a/internal/pkg/model/types.go b/internal/pkg/model/types.go deleted file mode 100644 index 5e6367d5..00000000 --- a/internal/pkg/model/types.go +++ /dev/null @@ -1,20 +0,0 @@ -package model - -import "time" - -type KV struct { - Key string - Value string -} - -type Structured struct { - Time time.Time - Level string - Msg string - KVs []KV -} - -type Event struct { - Raw []byte - Structured *Structured -} diff --git a/internal/pkg/state/state.go b/internal/pkg/state/state.go index eadb6d47..466ad82c 100644 --- a/internal/pkg/state/state.go +++ b/internal/pkg/state/state.go @@ -9,18 +9,27 @@ import ( "time" "github.com/blang/semver" + typesv1 "github.com/humanlogio/api/go/types/v1" ) var DefaultState = State{ Version: 1, } -func GetDefaultStateFilepath() (string, error) { +func GetDefaultStateDirpath() (string, error) { home, err := os.UserHomeDir() if err != nil { - return "", fmt.Errorf("$HOME not set, can't determine a state file path") + return "", fmt.Errorf("$HOME not set, can't determine a state dir path") } stateDirpath := filepath.Join(home, ".state", "humanlog") + return stateDirpath, nil +} + +func GetDefaultStateFilepath() (string, error) { + stateDirpath, err := GetDefaultStateDirpath() + if err != nil { + return "", err + } stateFilepath := filepath.Join(stateDirpath, "state.json") dfi, err := os.Stat(stateDirpath) if err != nil && !errors.Is(err, os.ErrNotExist) { @@ -102,6 +111,13 @@ type State struct { LatestKnownVersion *semver.Version `json:"latest_known_version,omitempty"` LastestKnownVersionUpdatedAt *time.Time `json:"latest_known_version_updated_at"` + IngestionToken *typesv1.AccountToken `json:"ingestion_token,omitempty"` + + // preferences set in the CLI/TUI when querying + CurrentOrgID *int64 `json:"current_org_id,omitempty"` + CurrentAccountID *int64 `json:"current_account_id,omitempty"` + CurrentMachineID *int64 `json:"current_machine_id,omitempty"` + // unexported path string } diff --git a/json_handler.go b/json_handler.go index 1901e3c9..ca1e7c3f 100644 --- a/json_handler.go +++ b/json_handler.go @@ -7,7 +7,8 @@ import ( "strings" "time" - "github.com/humanlogio/humanlog/internal/pkg/model" + typesv1 "github.com/humanlogio/api/go/types/v1" + "google.golang.org/protobuf/types/known/timestamppb" ) // JSONHandler can handle logs emitted by logrus.TextFormatter loggers. @@ -66,16 +67,16 @@ func (h *JSONHandler) clear() { } // TryHandle tells if this line was handled by this handler. -func (h *JSONHandler) TryHandle(d []byte, out *model.Structured) bool { +func (h *JSONHandler) TryHandle(d []byte, out *typesv1.StructuredLogEvent) bool { h.clear() if !h.UnmarshalJSON(d) { return false } - out.Time = h.Time + out.Timestamp = timestamppb.New(h.Time) out.Msg = h.Message - out.Level = h.Level + out.Lvl = h.Level for k, v := range h.Fields { - out.KVs = append(out.KVs, model.KV{Key: k, Value: v}) + out.Kvs = append(out.Kvs, &typesv1.KV{Key: k, Value: v}) } return true } diff --git a/json_handler_test.go b/json_handler_test.go index fa999bcf..c03b5647 100644 --- a/json_handler_test.go +++ b/json_handler_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" + typesv1 "github.com/humanlogio/api/go/types/v1" "github.com/humanlogio/humanlog" - "github.com/humanlogio/humanlog/internal/pkg/model" ) func TestJSONHandler_UnmarshalJSON_ParsesFields(t *testing.T) { @@ -23,7 +23,7 @@ func TestJSONHandler_UnmarshalJSON_ParsesFields(t *testing.T) { opts := humanlog.DefaultOptions() h := humanlog.JSONHandler{Opts: opts} - ev := new(model.Structured) + ev := new(typesv1.StructuredLogEvent) if !h.TryHandle(raw, ev) { t.Fatalf("failed to parse log level") } @@ -59,7 +59,7 @@ func TestJSONHandler_UnmarshalJSON_ParsesCustomFields(t *testing.T) { h := humanlog.JSONHandler{Opts: opts} - ev := new(model.Structured) + ev := new(typesv1.StructuredLogEvent) if !h.TryHandle(raw, ev) { t.Fatalf("failed to parse log level") } @@ -93,7 +93,7 @@ func TestJSONHandler_UnmarshalJSON_ParsesCustomNestedFields(t *testing.T) { opts.TimeFields = []string{"data.time"} h := humanlog.JSONHandler{Opts: opts} - ev := new(model.Structured) + ev := new(typesv1.StructuredLogEvent) if !h.TryHandle(raw, ev) { t.Fatalf("failed to handle log") } @@ -136,7 +136,7 @@ func TestJSONHandler_UnmarshalJSON_ParsesCustomMultiNestedFields(t *testing.T) { opts.TimeFields = []string{"data.l2.time"} h := humanlog.JSONHandler{Opts: opts} - ev := new(model.Structured) + ev := new(typesv1.StructuredLogEvent) if !h.TryHandle(raw, ev) { t.Fatalf("failed to handle log") } diff --git a/logfmt_handler.go b/logfmt_handler.go index f0fdbc1c..262b4daa 100644 --- a/logfmt_handler.go +++ b/logfmt_handler.go @@ -6,7 +6,8 @@ import ( "time" "github.com/go-logfmt/logfmt" - "github.com/humanlogio/humanlog/internal/pkg/model" + typesv1 "github.com/humanlogio/api/go/types/v1" + "google.golang.org/protobuf/types/known/timestamppb" ) // LogfmtHandler can handle logs emmited by logrus.TextFormatter loggers. @@ -27,7 +28,7 @@ func (h *LogfmtHandler) clear() { } // CanHandle tells if this line can be handled by this handler. -func (h *LogfmtHandler) TryHandle(d []byte, out *model.Structured) bool { +func (h *LogfmtHandler) TryHandle(d []byte, out *typesv1.StructuredLogEvent) bool { if !bytes.ContainsRune(d, '=') { return false } @@ -35,11 +36,11 @@ func (h *LogfmtHandler) TryHandle(d []byte, out *model.Structured) bool { if !h.UnmarshalLogfmt(d) { return false } - out.Time = h.Time + out.Timestamp = timestamppb.New(h.Time) out.Msg = h.Message - out.Level = h.Level + out.Lvl = h.Level for k, v := range h.Fields { - out.KVs = append(out.KVs, model.KV{Key: k, Value: v}) + out.Kvs = append(out.Kvs, &typesv1.KV{Key: k, Value: v}) } return true } diff --git a/pkg/auth/interceptors.go b/pkg/auth/interceptors.go new file mode 100644 index 00000000..bf6a3892 --- /dev/null +++ b/pkg/auth/interceptors.go @@ -0,0 +1,146 @@ +package auth + +import ( + "context" + "log/slog" + + "connectrpc.com/connect" + typesv1 "github.com/humanlogio/api/go/types/v1" +) + +func Interceptors(ll *slog.Logger, tokenSource *UserRefreshableTokenSource) []connect.Interceptor { + return []connect.Interceptor{ + NewRefreshedUserAuthInterceptor(ll, tokenSource), + NewUserAuthInterceptor(tokenSource), + } +} + +func NewRefreshedUserAuthInterceptor(ll *slog.Logger, tokenSource *UserRefreshableTokenSource) connect.Interceptor { + return &refreshedTokenInterceptor{ll: ll, tokenSource: tokenSource} +} + +type refreshedTokenInterceptor struct { + ll *slog.Logger + tokenSource *UserRefreshableTokenSource +} + +func (rti *refreshedTokenInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + res, err := next(ctx, req) + if res != nil { + if refreshedToken := res.Header().Get("UseAuthorization"); refreshedToken != "" { + rti.ll.DebugContext(ctx, "received refreshed token") + if err := rti.tokenSource.RefreshUserToken(ctx, refreshedToken); err != nil { + rti.ll.ErrorContext(ctx, "refreshing user token", slog.Any("err", err)) + } + } + } + return res, err + } +} + +func (rti *refreshedTokenInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { + conn := next(ctx, spec) + + if refreshedToken := conn.ResponseHeader().Get("UseAuthorization"); refreshedToken != "" { + rti.ll.DebugContext(ctx, "received refreshed token") + if err := rti.tokenSource.RefreshUserToken(ctx, refreshedToken); err != nil { + rti.ll.ErrorContext(ctx, "refreshing user token", slog.Any("err", err)) + } + } + return conn + } +} + +func (rti *refreshedTokenInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return func(ctx context.Context, shc connect.StreamingHandlerConn) error { + if refreshedToken := shc.ResponseHeader().Get("UseAuthorization"); refreshedToken != "" { + rti.ll.DebugContext(ctx, "received refreshed token") + if err := rti.tokenSource.RefreshUserToken(ctx, refreshedToken); err != nil { + rti.ll.ErrorContext(ctx, "refreshing user token", slog.Any("err", err)) + } + } + err := next(ctx, shc) + if refreshedToken := shc.ResponseTrailer().Get("UseAuthorization"); refreshedToken != "" { + rti.ll.DebugContext(ctx, "received refreshed token") + if err := rti.tokenSource.RefreshUserToken(ctx, refreshedToken); err != nil { + rti.ll.ErrorContext(ctx, "refreshing user token", slog.Any("err", err)) + } + } + return err + } +} + +func NewUserAuthInterceptor(tokenSource *UserRefreshableTokenSource) connect.Interceptor { + return &userAuthInjector{tokenSource: tokenSource} +} + +type userAuthInjector struct { + tokenSource *UserRefreshableTokenSource +} + +func (uai *userAuthInjector) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + userToken, err := uai.tokenSource.GetUserToken(ctx) + if err != nil { + return nil, err + } + + req.Header().Set("Authorization", "Bearer "+userToken.Token) + return next(ctx, req) + } +} + +func (uai *userAuthInjector) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { + conn := next(ctx, spec) + userToken, err := uai.tokenSource.GetUserToken(ctx) + if err != nil { + panic(err) + } + conn.RequestHeader().Set("Authorization", "Bearer "+userToken.Token) + return conn + } +} + +func (uai *userAuthInjector) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return func(ctx context.Context, shc connect.StreamingHandlerConn) error { + userToken, err := uai.tokenSource.GetUserToken(ctx) + if err != nil { + return err + } + shc.RequestHeader().Set("Authorization", "Bearer "+userToken.Token) + return next(ctx, shc) + } +} + +func NewAccountAuthInterceptor(token *typesv1.AccountToken) connect.Interceptor { + return &accountAuthInjector{token: token} +} + +type accountAuthInjector struct { + token *typesv1.AccountToken +} + +func (aai *accountAuthInjector) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + req.Header().Set("Authorization", "Bearer "+aai.token.Token) + return next(ctx, req) + } +} + +func (aai *accountAuthInjector) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn { + conn := next(ctx, spec) + conn.RequestHeader().Set("Authorization", "Bearer "+aai.token.Token) + return conn + } +} + +func (aai *accountAuthInjector) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return func(ctx context.Context, shc connect.StreamingHandlerConn) error { + shc.RequestHeader().Set("Authorization", "Bearer "+aai.token.Token) + return next(ctx, shc) + } +} diff --git a/pkg/auth/token_source.go b/pkg/auth/token_source.go new file mode 100644 index 00000000..87cd201a --- /dev/null +++ b/pkg/auth/token_source.go @@ -0,0 +1,100 @@ +package auth + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/99designs/keyring" + typesv1 "github.com/humanlogio/api/go/types/v1" + "google.golang.org/protobuf/proto" +) + +const ( + UserTokenKeyringKey = "user-token" +) + +type UserRefreshableTokenSource struct { + utmu sync.Mutex + ut *typesv1.UserToken + + getKeyring func() (keyring.Keyring, error) +} + +func NewRefreshableTokenSource(getKeyring func() (keyring.Keyring, error)) *UserRefreshableTokenSource { + return &UserRefreshableTokenSource{getKeyring: getKeyring} +} + +func (rts *UserRefreshableTokenSource) ClearToken(ctx context.Context) error { + ring, err := rts.getKeyring() + if err != nil { + return fmt.Errorf("opening keyring: %v", err) + } + + rts.utmu.Lock() + defer rts.utmu.Unlock() + rts.ut = nil + if err := ring.Remove(UserTokenKeyringKey); err != nil { + return fmt.Errorf("removing credentials from keyring") + } + return nil +} + +func (rts *UserRefreshableTokenSource) GetUserToken(ctx context.Context) (*typesv1.UserToken, error) { + rts.utmu.Lock() + defer rts.utmu.Unlock() + if rts.ut != nil { + return rts.ut, nil + } + ring, err := rts.getKeyring() + if err != nil { + return nil, fmt.Errorf("opening keyring: %v", err) + } + data, err := ring.Get(UserTokenKeyringKey) + if errors.Is(err, keyring.ErrKeyNotFound) { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("retrieving user token from keyring: %v", err) + } + + var out typesv1.UserToken + err = proto.Unmarshal(data.Data, &out) + if err != nil { + return nil, fmt.Errorf("marshaling token to proto: %v", err) + } + rts.ut = &out + return &out, nil +} + +func (rts *UserRefreshableTokenSource) RefreshUserToken(ctx context.Context, newToken string) error { + old, err := rts.GetUserToken(ctx) + if err != nil { + return err + } + return rts.SetUserToken(ctx, &typesv1.UserToken{UserId: old.UserId, Token: newToken}) +} + +func (rts *UserRefreshableTokenSource) SetUserToken(ctx context.Context, userToken *typesv1.UserToken) error { + userTokenRaw, err := proto.Marshal(userToken) + if err != nil { + return fmt.Errorf("marshaling token to proto: %v", err) + } + ring, err := rts.getKeyring() + if err != nil { + return fmt.Errorf("opening keyring: %v", err) + } + rts.utmu.Lock() + defer rts.utmu.Unlock() + err = ring.Set(keyring.Item{ + Key: UserTokenKeyringKey, + Data: userTokenRaw, + Label: "humanlog.io user authentication", + Description: "humanlog wants to store session credentials in a secure location", + }) + if err != nil { + return fmt.Errorf("setting user token in keyring: %v", err) + } + rts.ut = userToken + return nil +} diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go new file mode 100644 index 00000000..3eafcf0e --- /dev/null +++ b/pkg/retry/retry.go @@ -0,0 +1,114 @@ +package retry + +import ( + "context" + "math" + "math/rand" + "time" +) + +// Defaults for the retries. +const ( + DefaultBaseSleep = 100 * time.Millisecond + DefaultCapSleep = 30 * time.Second + DefaultFactor = 2.0 +) + +type OnRetry func(attempt float64, err error) + +type retrier struct { + log OnRetry + ctx context.Context + r *rand.Rand + sleep time.Duration + cap time.Duration + factor float64 + sleepFn func(time.Duration) <-chan time.Time +} + +func newRetrier(ctx context.Context) *retrier { + return &retrier{ + log: func(_ float64, _ error) {}, + ctx: ctx, + r: rand.New(rand.NewSource(time.Now().UnixNano())), + sleep: DefaultBaseSleep, + cap: DefaultCapSleep, + factor: DefaultFactor, + sleepFn: time.After, + } +} + +// An Option changes the default behavior of a retrier. +type Option func(*retrier) + +// UseBaseSleep makes the retrier sleep at least `base`. +func UseBaseSleep(base time.Duration) Option { + return func(r *retrier) { r.sleep = base } +} + +// UseCapSleep makes the retrier sleep at most `cap`. +func UseCapSleep(cap time.Duration) Option { + return func(r *retrier) { r.cap = cap } +} + +// UseFactor makes the retrier grow its exponential backoff using factor +// as the base of the exponent. +func UseFactor(factor float64) Option { + return func(r *retrier) { r.factor = factor } +} + +// UseLog makes the retrier log retries to the given func. +func UseLog(log OnRetry) Option { + return func(r *retrier) { r.log = log } +} + +// UseRand makes the retrier use the *rand.Rand for randomization of retries. +func UseRand(rd *rand.Rand) Option { + return func(r *retrier) { r.r = rd } +} + +// useSleepFn makes the retrier use the sleep func for sleeps. +func useSleepFn(fn func(time.Duration) <-chan time.Time) Option { + return func(r *retrier) { r.sleepFn = fn } +} + +// Do an action with exponential randomized backoff. +func Do(ctx context.Context, fn func(context.Context) (bool, error), opts ...Option) error { + + retrier := newRetrier(ctx) + for _, opt := range opts { + opt(retrier) + } + + var ( + baseSeconds = retrier.sleep.Seconds() + capSecond = retrier.cap.Seconds() + factor = retrier.factor + r = retrier.r + sleepFn = retrier.sleepFn + attempt = 0.0 + ) + retryFunc := func() bool { + sleepSeconds := r.Float64() * math.Min(capSecond, baseSeconds*math.Pow(factor, attempt)) + sleep := time.Duration(sleepSeconds * float64(time.Second)) + select { + case <-sleepFn(sleep): + case <-ctx.Done(): + return false + } + attempt += 1.0 + return true + } + + for { + retry, err := fn(ctx) + if !retry { + return err + } + + retrier.log(attempt, err) + if !retryFunc() { + return err + } + } +} diff --git a/pkg/sink/bufsink/sized.go b/pkg/sink/bufsink/sized.go index 8ac1ddad..e035e142 100644 --- a/pkg/sink/bufsink/sized.go +++ b/pkg/sink/bufsink/sized.go @@ -3,12 +3,14 @@ package bufsink import ( "context" - "github.com/humanlogio/humanlog/internal/pkg/model" + typesv1 "github.com/humanlogio/api/go/types/v1" "github.com/humanlogio/humanlog/pkg/sink" + "google.golang.org/protobuf/proto" ) type SizedBuffer struct { - Buffered []model.Event + size int + Buffered []*typesv1.LogEvent flush sink.BatchSink } @@ -16,19 +18,25 @@ var _ sink.Sink = (*SizedBuffer)(nil) func NewSizedBufferedSink(size int, flush sink.BatchSink) *SizedBuffer { return &SizedBuffer{ - Buffered: make([]model.Event, 0, size), + size: size, + Buffered: make([]*typesv1.LogEvent, 0, size), flush: flush, } } -func (sn *SizedBuffer) Receive(ctx context.Context, ev *model.Event) error { - sn.Buffered = append(sn.Buffered, *ev) - if len(sn.Buffered) == cap(sn.Buffered) { +func (sn *SizedBuffer) Flush(ctx context.Context) error { + return nil +} + +func (sn *SizedBuffer) Receive(ctx context.Context, ev *typesv1.LogEvent) error { + cev := proto.Clone(ev).(*typesv1.LogEvent) + sn.Buffered = append(sn.Buffered, cev) + if len(sn.Buffered) == sn.size { if err := sn.flush.ReceiveBatch(ctx, sn.Buffered); err != nil { sn.Buffered = sn.Buffered[:len(sn.Buffered)-1] return err } - sn.Buffered = sn.Buffered[:0] + sn.Buffered = sn.Buffered[:0:sn.size] } return nil } diff --git a/pkg/sink/logsvcsink/bidistream_sink.go b/pkg/sink/logsvcsink/bidistream_sink.go new file mode 100644 index 00000000..cb954f2f --- /dev/null +++ b/pkg/sink/logsvcsink/bidistream_sink.go @@ -0,0 +1,189 @@ +package logsvcsink + +import ( + "context" + "fmt" + "io" + "log" + "time" + + "connectrpc.com/connect" + v1 "github.com/humanlogio/api/go/svc/ingest/v1" + "github.com/humanlogio/api/go/svc/ingest/v1/ingestv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/pkg/retry" + "github.com/humanlogio/humanlog/pkg/sink" + "google.golang.org/protobuf/proto" +) + +var ( + _ sink.Sink = (*ConnectBidiStreamSink)(nil) +) + +type ConnectBidiStreamSink struct { + name string + eventsc chan *typesv1.LogEvent + dropIfFull bool + doneFlushing chan struct{} +} + +func StartBidiStreamSink(ctx context.Context, client ingestv1connect.IngestServiceClient, name string, machineID uint64, bufferSize int, drainBufferFor time.Duration, dropIfFull bool) *ConnectBidiStreamSink { + snk := &ConnectBidiStreamSink{ + name: name, + eventsc: make(chan *typesv1.LogEvent, bufferSize), + dropIfFull: dropIfFull, + doneFlushing: make(chan struct{}), + } + + go func() { + var ( + buffered []*typesv1.LogEvent + resumeSessionID uint64 + err error + ) + for { + startedAt := time.Now() + buffered, resumeSessionID, err = snk.connectAndHandleBuffer(ctx, client, machineID, bufferSize, drainBufferFor, buffered, resumeSessionID) + if err == io.EOF { + close(snk.doneFlushing) + return + } + if err != nil { + log.Printf("failed to send logs: %v", err) + } + if time.Since(startedAt) < time.Second { + select { + case <-ctx.Done(): + return + case <-time.After(time.Second): + } + } else { + select { + case <-ctx.Done(): + return + default: + } + } + + } + }() + + return snk +} + +func (snk *ConnectBidiStreamSink) connectAndHandleBuffer( + ctx context.Context, + client ingestv1connect.IngestServiceClient, + machineID uint64, + bufferSize int, + drainBufferFor time.Duration, + buffered []*typesv1.LogEvent, + resumeSessionID uint64, +) (lastBuffer []*typesv1.LogEvent, _ uint64, _ error) { + log.Print("contacting log ingestor") + var stream *connect.BidiStreamForClient[v1.IngestBidiStreamRequest, v1.IngestBidiStreamResponse] + err := retry.Do(ctx, func(ctx context.Context) (bool, error) { + stream = client.IngestBidiStream(ctx) + firstReq := &v1.IngestBidiStreamRequest{Events: buffered, MachineId: machineID, ResumeSessionId: resumeSessionID} + if err := stream.Send(firstReq); err != nil { + return true, fmt.Errorf("creating ingestion stream: %w", err) + } + return false, nil + }, retry.UseCapSleep(time.Second), retry.UseLog(func(attempt float64, err error) { + log.Printf("can't reach humanlog.io, attempt %d: %v", int(attempt), err) + })) + if err != nil { + return buffered, resumeSessionID, fmt.Errorf("retry aborted: %w", err) + } + + log.Print("receiving log ingestor session") + res, err := stream.Receive() + if err != nil { + // nothing is buffered + return nil, resumeSessionID, fmt.Errorf("waiting for ingestion stream session ID: %w", err) + } + defer func() { + stream.CloseRequest() + stream.CloseResponse() + }() + + log.Print("ready to send logs") + resumeSessionID = res.SessionId + ticker := time.NewTicker(drainBufferFor) + ticker.Stop() + req := new(v1.IngestBidiStreamRequest) + flushing := false + for !flushing { + // wait for any event to come + select { + case ev, more := <-snk.eventsc: + if !more { + flushing = true + } + if ev != nil { + req.Events = append(req.Events, ev) + } + case <-ctx.Done(): + return req.Events, resumeSessionID, nil + } + ticker.Reset(drainBufferFor) + + drain_buffered_events_loop: + // try to drain the channel for 100ms + for len(req.Events) < bufferSize { + select { + case ev, more := <-snk.eventsc: + if !more { + flushing = true + } + if ev != nil { + req.Events = append(req.Events, ev) + } + case <-ticker.C: + ticker.Stop() + break drain_buffered_events_loop + } + } + // until it's empty, then send what we have + start := time.Now() + err := stream.Send(req) + dur := time.Since(start) + log.Printf("send: %s %v ms (err=%v) ev=%d", snk.name, dur.Milliseconds(), err, len(req.Events)) + if err != nil { + return req.Events, resumeSessionID, err + } + req.Events = req.Events[:0:len(req.Events)] + } + return nil, resumeSessionID, io.EOF +} + +func (snk *ConnectBidiStreamSink) Receive(ctx context.Context, ev *typesv1.LogEvent) error { + send := proto.Clone(ev).(*typesv1.LogEvent) + if snk.dropIfFull { + select { + case snk.eventsc <- send: + case <-ctx.Done(): + return ctx.Err() + default: + log.Print("dropping log event, buffer full") + } + } else { + select { + case snk.eventsc <- send: + case <-ctx.Done(): + return ctx.Err() + } + } + return nil +} + +// Flush can only be called once, calling it twice will panic. +func (snk *ConnectBidiStreamSink) Flush(ctx context.Context) error { + close(snk.eventsc) + select { + case <-snk.doneFlushing: + case <-ctx.Done(): + return ctx.Err() + } + return nil +} diff --git a/pkg/sink/logsvcsink/stream_sink.go b/pkg/sink/logsvcsink/stream_sink.go new file mode 100644 index 00000000..1ff981c9 --- /dev/null +++ b/pkg/sink/logsvcsink/stream_sink.go @@ -0,0 +1,222 @@ +package logsvcsink + +import ( + "context" + "fmt" + "io" + "log" + "time" + + "connectrpc.com/connect" + v1 "github.com/humanlogio/api/go/svc/ingest/v1" + "github.com/humanlogio/api/go/svc/ingest/v1/ingestv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/pkg/retry" + "github.com/humanlogio/humanlog/pkg/sink" + "google.golang.org/protobuf/proto" +) + +var ( + _ sink.Sink = (*ConnectStreamSink)(nil) +) + +type ConnectStreamSink struct { + name string + eventsc chan *typesv1.LogEvent + dropIfFull bool + doneFlushing chan struct{} +} + +func StartStreamSink( + ctx context.Context, + client ingestv1connect.IngestServiceClient, + name string, + machineID uint64, + bufferSize int, + drainBufferFor time.Duration, + dropIfFull bool, +) *ConnectStreamSink { + + snk := &ConnectStreamSink{ + name: name, + eventsc: make(chan *typesv1.LogEvent, bufferSize), + dropIfFull: dropIfFull, + doneFlushing: make(chan struct{}), + } + + go func() { + var ( + buffered []*typesv1.LogEvent + sessionID = uint64(time.Now().UnixNano()) + heartbeatEvery = 5 * time.Second + err error + ) + for { + startedAt := time.Now() + buffered, sessionID, heartbeatEvery, err = snk.connectAndHandleBuffer(ctx, client, machineID, bufferSize, drainBufferFor, buffered, sessionID, heartbeatEvery) + if err == io.EOF { + close(snk.doneFlushing) + return + } + if err != nil { + log.Printf("failed to send logs: %v", err) + } + if time.Since(startedAt) < time.Second { + select { + case <-ctx.Done(): + return + case <-time.After(time.Second): + } + } else { + select { + case <-ctx.Done(): + return + default: + } + } + + } + }() + + return snk +} + +func (snk *ConnectStreamSink) connectAndHandleBuffer( + ctx context.Context, + client ingestv1connect.IngestServiceClient, + machineID uint64, + bufferSize int, + drainBufferFor time.Duration, + buffered []*typesv1.LogEvent, + sessionID uint64, + heartbeatEvery time.Duration, +) (lastBuffer []*typesv1.LogEvent, _ uint64, _ time.Duration, _ error) { + log.Print("contacting log ingestor") + var stream *connect.ClientStreamForClient[v1.IngestStreamRequest, v1.IngestStreamResponse] + err := retry.Do(ctx, func(ctx context.Context) (bool, error) { + + hbRes, err := client.GetHeartbeat(ctx, connect.NewRequest(&v1.GetHeartbeatRequest{MachineId: &machineID})) + if err != nil { + return true, fmt.Errorf("requesting heartbeat config from ingestor: %v", err) + } + heartbeatEvery = hbRes.Msg.HeartbeatIn.AsDuration() + + stream = client.IngestStream(ctx) + firstReq := &v1.IngestStreamRequest{Events: buffered, MachineId: machineID, SessionId: sessionID} + if err := stream.Send(firstReq); err != nil { + return true, fmt.Errorf("creating ingestion stream: %w", err) + } + return false, nil + }, retry.UseCapSleep(time.Second), retry.UseLog(func(attempt float64, err error) { + log.Printf("can't reach humanlog.io, attempt %d: %v", int(attempt), err) + })) + if err != nil { + return buffered, sessionID, heartbeatEvery, fmt.Errorf("retry aborted: %w", err) + } + + defer func() { + res, err := stream.CloseAndReceive() + if err != nil { + log.Printf("closing and receiving response for log ingestor session: %v", err) + return + } + if res.Msg == nil { + return + } + if res.Msg.SessionId != 0 { + sessionID = res.Msg.SessionId + } + if res.Msg.HeartbeatIn != nil { + heartbeatEvery = res.Msg.HeartbeatIn.AsDuration() + } + }() + + log.Print("ready to send logs") + heartbeater := time.NewTicker(heartbeatEvery) + defer heartbeater.Stop() + ticker := time.NewTicker(drainBufferFor) + ticker.Stop() + req := new(v1.IngestStreamRequest) + flushing := false + heartbeat := false + for !flushing { + // wait for any event to come + select { + case ev, more := <-snk.eventsc: + if !more { + flushing = true + } + if ev != nil { + req.Events = append(req.Events, ev) + } + heartbeat = false + case <-heartbeater.C: + heartbeat = true + // send whatever is there + case <-ctx.Done(): + return req.Events, sessionID, heartbeatEvery, nil + } + ticker.Reset(drainBufferFor) + + if !heartbeat { + // unless we're just heartbeating, + // try to drain the channel for 100ms + drain_buffered_events_loop: + for len(req.Events) < bufferSize { + select { + case ev, more := <-snk.eventsc: + if !more { + flushing = true + } + if ev != nil { + req.Events = append(req.Events, ev) + } + case <-ticker.C: + ticker.Stop() + break drain_buffered_events_loop + } + } + // until it's empty, then send what we have + } + start := time.Now() + err := stream.Send(req) + dur := time.Since(start) + log.Printf("send: %q %v ms (err=%v) ev=%d", snk.name, dur.Milliseconds(), err, len(req.Events)) + if err != nil { + return req.Events, sessionID, heartbeatEvery, err + } + req.Events = req.Events[:0:len(req.Events)] + } + return nil, sessionID, heartbeatEvery, io.EOF +} + +func (snk *ConnectStreamSink) Receive(ctx context.Context, ev *typesv1.LogEvent) error { + send := proto.Clone(ev).(*typesv1.LogEvent) + if snk.dropIfFull { + select { + case snk.eventsc <- send: + case <-ctx.Done(): + return ctx.Err() + default: + log.Print("dropping log event, buffer full") + } + } else { + select { + case snk.eventsc <- send: + case <-ctx.Done(): + return ctx.Err() + } + } + return nil +} + +// Flush can only be called once, calling it twice will panic. +func (snk *ConnectStreamSink) Flush(ctx context.Context) error { + close(snk.eventsc) + select { + case <-snk.doneFlushing: + case <-ctx.Done(): + return ctx.Err() + } + return nil +} diff --git a/pkg/sink/logsvcsink/unary_sink.go b/pkg/sink/logsvcsink/unary_sink.go new file mode 100644 index 00000000..0dc22ae6 --- /dev/null +++ b/pkg/sink/logsvcsink/unary_sink.go @@ -0,0 +1,206 @@ +package logsvcsink + +import ( + "context" + "fmt" + "io" + "log" + "time" + + "connectrpc.com/connect" + v1 "github.com/humanlogio/api/go/svc/ingest/v1" + "github.com/humanlogio/api/go/svc/ingest/v1/ingestv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/pkg/retry" + "github.com/humanlogio/humanlog/pkg/sink" + "google.golang.org/protobuf/proto" +) + +var ( + _ sink.Sink = (*ConnectUnarySink)(nil) +) + +type ConnectUnarySink struct { + name string + eventsc chan *typesv1.LogEvent + dropIfFull bool + doneFlushing chan struct{} +} + +func StartUnarySink( + ctx context.Context, + client ingestv1connect.IngestServiceClient, + name string, + machineID uint64, + bufferSize int, + drainBufferFor time.Duration, + dropIfFull bool, +) *ConnectUnarySink { + snk := &ConnectUnarySink{ + name: name, + eventsc: make(chan *typesv1.LogEvent, bufferSize), + dropIfFull: dropIfFull, + doneFlushing: make(chan struct{}), + } + + go func() { + var ( + buffered []*typesv1.LogEvent + sessionID = uint64(time.Now().UnixNano()) + heartbeatEvery = 5 * time.Second + err error + ) + for { + startedAt := time.Now() + buffered, sessionID, heartbeatEvery, err = snk.connectAndHandleBuffer(ctx, client, machineID, bufferSize, drainBufferFor, buffered, sessionID, heartbeatEvery) + if err == io.EOF { + close(snk.doneFlushing) + return + } + if err != nil { + log.Printf("failed to send logs: %v", err) + } + if time.Since(startedAt) < time.Second { + select { + case <-ctx.Done(): + return + case <-time.After(time.Second): + } + } else { + select { + case <-ctx.Done(): + return + default: + } + } + + } + }() + + return snk +} + +func (snk *ConnectUnarySink) connectAndHandleBuffer( + ctx context.Context, + client ingestv1connect.IngestServiceClient, + machineID uint64, + bufferSize int, + drainBufferFor time.Duration, + buffered []*typesv1.LogEvent, + sessionID uint64, + heartbeatEvery time.Duration, +) (lastBuffer []*typesv1.LogEvent, _ uint64, _ time.Duration, _ error) { + log.Print("contacting log ingestor") + err := retry.Do(ctx, func(ctx context.Context) (bool, error) { + hbRes, err := client.GetHeartbeat(ctx, connect.NewRequest(&v1.GetHeartbeatRequest{MachineId: &machineID})) + if err != nil { + return true, fmt.Errorf("requesting heartbeat config from ingestor: %v", err) + } + heartbeatEvery = hbRes.Msg.HeartbeatIn.AsDuration() + return false, nil + }, retry.UseCapSleep(time.Second), retry.UseLog(func(attempt float64, err error) { + log.Printf("can't reach humanlog.io, attempt %d: %v", int(attempt), err) + })) + if err != nil { + return buffered, sessionID, heartbeatEvery, fmt.Errorf("retry aborted: %w", err) + } + + log.Print("ready to send logs") + heartbeater := time.NewTicker(heartbeatEvery) + defer heartbeater.Stop() + ticker := time.NewTicker(drainBufferFor) + ticker.Stop() + req := connect.NewRequest(&v1.IngestRequest{ + MachineId: machineID, + }) + flushing := false + heartbeat := false + for !flushing { + // wait for any event to come + select { + case ev, more := <-snk.eventsc: + if !more { + flushing = true + } + if ev != nil { + req.Msg.Events = append(req.Msg.Events, ev) + } + heartbeat = false + case <-heartbeater.C: + heartbeat = true + // send whatever is there + case <-ctx.Done(): + return req.Msg.Events, sessionID, heartbeatEvery, nil + } + ticker.Reset(drainBufferFor) + + if !heartbeat { + // unless we're just heartbeating, + // try to drain the channel for 100ms + drain_buffered_events_loop: + for len(req.Msg.Events) < bufferSize { + select { + case ev, more := <-snk.eventsc: + if !more { + flushing = true + } + if ev != nil { + req.Msg.Events = append(req.Msg.Events, ev) + } + case <-ticker.C: + ticker.Stop() + break drain_buffered_events_loop + } + } + // until it's empty, then send what we have + } + req.Msg.MachineId = machineID + req.Msg.SessionId = sessionID + start := time.Now() + res, err := client.Ingest(ctx, req) + dur := time.Since(start) + log.Printf("send: %s %v ms (err=%v) ev=%d", snk.name, dur.Milliseconds(), err, len(req.Msg.Events)) + if err != nil { + return req.Msg.Events, sessionID, heartbeatEvery, err + } + if res.Msg.SessionId != 0 { + sessionID = res.Msg.SessionId + } + if res.Msg.HeartbeatIn != nil { + heartbeatEvery = res.Msg.HeartbeatIn.AsDuration() + } + req.Msg.Events = req.Msg.Events[:0:len(req.Msg.Events)] + } + return nil, sessionID, heartbeatEvery, io.EOF +} + +func (snk *ConnectUnarySink) Receive(ctx context.Context, ev *typesv1.LogEvent) error { + send := proto.Clone(ev).(*typesv1.LogEvent) + if snk.dropIfFull { + select { + case snk.eventsc <- send: + case <-ctx.Done(): + return ctx.Err() + default: + log.Print("dropping log event, buffer full") + } + } else { + select { + case snk.eventsc <- send: + case <-ctx.Done(): + return ctx.Err() + } + } + return nil +} + +// Flush can only be called once, calling it twice will panic. +func (snk *ConnectUnarySink) Flush(ctx context.Context) error { + close(snk.eventsc) + select { + case <-snk.doneFlushing: + case <-ctx.Done(): + return ctx.Err() + } + return nil +} diff --git a/pkg/sink/sink.go b/pkg/sink/sink.go index e3d8c633..3d6f95e7 100644 --- a/pkg/sink/sink.go +++ b/pkg/sink/sink.go @@ -3,13 +3,15 @@ package sink import ( "context" - "github.com/humanlogio/humanlog/internal/pkg/model" + typesv1 "github.com/humanlogio/api/go/types/v1" ) type Sink interface { - Receive(ctx context.Context, ev *model.Event) error + Receive(ctx context.Context, ev *typesv1.LogEvent) error + Flush(ctx context.Context) error } type BatchSink interface { - ReceiveBatch(ctx context.Context, evs []model.Event) error + ReceiveBatch(ctx context.Context, evs []*typesv1.LogEvent) error + Flush(ctx context.Context) error } diff --git a/pkg/sink/stdiosink/stdio.go b/pkg/sink/stdiosink/stdio.go index 4a4a63a4..7dd4a76d 100644 --- a/pkg/sink/stdiosink/stdio.go +++ b/pkg/sink/stdiosink/stdio.go @@ -11,8 +11,8 @@ import ( "time" "github.com/fatih/color" + typesv1 "github.com/humanlogio/api/go/types/v1" "github.com/humanlogio/humanlog/internal/pkg/config" - "github.com/humanlogio/humanlog/internal/pkg/model" "github.com/humanlogio/humanlog/pkg/sink" ) @@ -118,7 +118,11 @@ func NewStdio(w io.Writer, opts StdioOpts) *Stdio { } } -func (std *Stdio) Receive(ctx context.Context, ev *model.Event) error { +func (std *Stdio) Flush(ctx context.Context) error { + return nil +} + +func (std *Stdio) Receive(ctx context.Context, ev *typesv1.LogEvent) error { if ev.Structured == nil { std.lastRaw = true std.lastLevel = "" @@ -154,9 +158,9 @@ func (std *Stdio) Receive(ctx context.Context, ev *model.Event) error { msg = msgColor.Sprint(data.Msg) } - lvl := strings.ToUpper(data.Level)[:imin(4, len(data.Level))] + lvl := strings.ToUpper(data.Lvl)[:imin(4, len(data.Lvl))] var level string - switch strings.ToLower(data.Level) { + switch strings.ToLower(data.Lvl) { case "debug": level = std.opts.Palette.DebugLevelColor.Sprint(lvl) case "info": @@ -179,7 +183,7 @@ func (std *Stdio) Receive(ctx context.Context, ev *model.Event) error { } _, _ = fmt.Fprintf(out, "%s |%s| %s\t %s", - timeColor.Sprint(data.Time.Format(std.opts.TimeFormat)), + timeColor.Sprint(data.Timestamp.AsTime().Format(std.opts.TimeFormat)), level, msg, strings.Join(std.joinKVs(data, "="), "\t "), @@ -195,22 +199,22 @@ func (std *Stdio) Receive(ctx context.Context, ev *model.Event) error { return err } - kvs := make(map[string]string, len(data.KVs)) - for _, kv := range data.KVs { + kvs := make(map[string]string, len(data.Kvs)) + for _, kv := range data.Kvs { kvs[kv.Key] = kv.Value } std.lastRaw = false - std.lastLevel = ev.Structured.Level + std.lastLevel = ev.Structured.Lvl std.lastKVs = kvs return nil } -func (std *Stdio) joinKVs(data *model.Structured, sep string) []string { - wasSameLevel := std.lastLevel == data.Level +func (std *Stdio) joinKVs(data *typesv1.StructuredLogEvent, sep string) []string { + wasSameLevel := std.lastLevel == data.Lvl skipUnchanged := !std.lastRaw && std.opts.SkipUnchanged && wasSameLevel - kv := make([]string, 0, len(data.KVs)) - for _, pair := range data.KVs { + kv := make([]string, 0, len(data.Kvs)) + for _, pair := range data.Kvs { k, v := pair.Key, pair.Value if !std.opts.shouldShowKey(k) { continue diff --git a/pkg/sink/teesink/tee.go b/pkg/sink/teesink/tee.go new file mode 100644 index 00000000..2f55fbc1 --- /dev/null +++ b/pkg/sink/teesink/tee.go @@ -0,0 +1,134 @@ +package teesink + +import ( + "context" + "fmt" + + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/pkg/sink" +) + +var _ sink.Sink = (*Tee)(nil) + +func NewTeeSink(sinks ...sink.Sink) sink.Sink { + var ( + nonbatchers []sink.Sink + batchers []sink.BatchSink + ) + for _, snk := range sinks { + if batcher, ok := snk.(sink.BatchSink); ok { + batchers = append(batchers, batcher) + } else { + nonbatchers = append(nonbatchers, snk) + } + } + if len(batchers) != 0 && len(nonbatchers) != 0 { + return &MixedBatchingTee{nonbatchers: nonbatchers, batchers: batchers} + } + if len(batchers) != 0 { + return &BatchingTee{batchers: batchers} + } + return &Tee{sinks: sinks} +} + +type Tee struct { + sinks []sink.Sink +} + +func (sn *Tee) Receive(ctx context.Context, ev *typesv1.LogEvent) error { + for i, sinks := range sn.sinks { + if err := sinks.Receive(ctx, ev); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + return nil +} + +func (sn *Tee) Flush(ctx context.Context) error { + for i, sinks := range sn.sinks { + if err := sinks.Flush(ctx); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + return nil +} + +type MixedBatchingTee struct { + nonbatchers []sink.Sink + batchers []sink.BatchSink +} + +func (sn *MixedBatchingTee) Receive(ctx context.Context, ev *typesv1.LogEvent) error { + for i, sinks := range sn.nonbatchers { + if err := sinks.Receive(ctx, ev); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + for i, sinks := range sn.batchers { + if err := sinks.ReceiveBatch(ctx, []*typesv1.LogEvent{ev}); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + return nil +} + +func (sn *MixedBatchingTee) ReceiveBatch(ctx context.Context, evs []*typesv1.LogEvent) error { + for i, sinks := range sn.nonbatchers { + for _, ev := range evs { + if err := sinks.Receive(ctx, ev); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + } + for i, sinks := range sn.batchers { + if err := sinks.ReceiveBatch(ctx, evs); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + return nil +} + +func (sn *MixedBatchingTee) Flush(ctx context.Context) error { + for i, sinks := range sn.nonbatchers { + if err := sinks.Flush(ctx); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + for i, sinks := range sn.batchers { + if err := sinks.Flush(ctx); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + return nil +} + +type BatchingTee struct { + batchers []sink.BatchSink +} + +func (sn *BatchingTee) Receive(ctx context.Context, ev *typesv1.LogEvent) error { + for i, sinks := range sn.batchers { + if err := sinks.ReceiveBatch(ctx, []*typesv1.LogEvent{ev}); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + return nil +} + +func (sn *BatchingTee) ReceiveBatch(ctx context.Context, evs []*typesv1.LogEvent) error { + for i, sinks := range sn.batchers { + if err := sinks.ReceiveBatch(ctx, evs); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + return nil +} + +func (sn *BatchingTee) Flush(ctx context.Context) error { + for i, sinks := range sn.batchers { + if err := sinks.Flush(ctx); err != nil { + return fmt.Errorf("tee sink %d: %w", i, err) + } + } + return nil +} diff --git a/pkg/tui/accounts.go b/pkg/tui/accounts.go new file mode 100644 index 00000000..f4f735bc --- /dev/null +++ b/pkg/tui/accounts.go @@ -0,0 +1,204 @@ +package tui + +import ( + "context" + "errors" + "log" + + "connectrpc.com/connect" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + organizationv1 "github.com/humanlogio/api/go/svc/organization/v1" + "github.com/humanlogio/api/go/svc/organization/v1/organizationv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/internal/pkg/state" +) + +type accountSelectorShell struct { + appStyle lipgloss.Style + ctx context.Context + organizationClient organizationv1connect.OrganizationServiceClient + state *state.State + + children tea.Model + + table table.Model + + accounts []*typesv1.Account + nextCursor *typesv1.Cursor + selected *typesv1.Account + + err error +} + +func WithAccountSelectorShell( + appStyle lipgloss.Style, + ctx context.Context, + state *state.State, + organizationClient organizationv1connect.OrganizationServiceClient, + children tea.Model, +) *accountSelectorShell { + + columns := []table.Column{ + {Title: "Name", Width: 10}, + } + t := table.New( + table.WithColumns(columns), + table.WithFocused(true), + table.WithHeight(5), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + return &accountSelectorShell{ + appStyle: appStyle, + ctx: ctx, + state: state, + children: children, + organizationClient: organizationClient, + table: t, + } +} + +func (m *accountSelectorShell) Init() tea.Cmd { + log.Printf("account: view") + return m.children.Init() +} + +func (m *accountSelectorShell) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Printf("account: update") + switch msg := msg.(type) { + + case *SelectedOrganizationMsg: + log.Printf("account: got org selected orgID=%d", msg.Organization.Id) + return m, listAccountsCmd(m.ctx, m.organizationClient, m.state) + + case listAccountMsg: + m.accounts = msg.accounts + m.nextCursor = msg.next + rows := make([]table.Row, 0, len(m.accounts)) + for _, acct := range m.accounts { + rows = append(rows, table.Row{ + acct.Name, + }) + if m.state.CurrentAccountID != nil && *m.state.CurrentAccountID == acct.Id { + m.selected = acct + break + } + } + if len(m.accounts) == 1 { + m.selected = m.accounts[0] + return m, writeSelectedAccountToState(m.state, m.selected) + } + m.table.SetRows(rows) + + case errMsg: + m.err = msg + return m, tea.Quit + + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + case "enter": + if m.selected == nil { + selectedName := string(m.table.SelectedRow()[0]) + for _, account := range m.accounts { + if account.Name == selectedName { + m.selected = account + break + } + } + return m, writeSelectedAccountToState(m.state, m.selected) + } + } + case tea.WindowSizeMsg: + var ccmd, tcmd tea.Cmd + m.children, ccmd = m.children.Update(msg) + m.table, tcmd = m.table.Update(msg) + return m, tea.Batch(ccmd, tcmd) + } + + var cmd tea.Cmd + if m.selected != nil { + m.children, cmd = m.children.Update(msg) + } else { + m.table, cmd = m.table.Update(msg) + } + return m, cmd +} + +func (m *accountSelectorShell) View() string { + log.Printf("account: view") + if m.accounts == nil { + return "Looking up accounts..." + } + if m.selected == nil { + return m.appStyle.Render( + "Select an account\n", + m.table.View(), + ) + } + return m.children.View() +} + +type listAccountMsg struct { + accounts []*typesv1.Account + next *typesv1.Cursor +} + +func listAccountsCmd( + ctx context.Context, + organizationClient organizationv1connect.OrganizationServiceClient, + state *state.State, +) func() tea.Msg { + return func() tea.Msg { + log.Printf("account: listAccounts") + res, err := organizationClient.ListAccount(ctx, connect.NewRequest(&organizationv1.ListAccountRequest{ + Cursor: nil, + Limit: 10, + OrganizationId: *state.CurrentOrgID, + })) + if err != nil { + cerr := new(connect.Error) + if errors.As(err, &cerr) { + log.Printf("account: listAccounts err=%v", cerr) + } + return errMsg{err} + } + out := listAccountMsg{ + accounts: make([]*typesv1.Account, 0, len(res.Msg.Items)), + } + for _, mc := range res.Msg.Items { + out.accounts = append(out.accounts, mc.Account) + } + log.Printf("account: got %d accounts", len(out.accounts)) + out.next = res.Msg.Next + return out + } +} + +type SelectedAccountMsg struct { + Account *typesv1.Account +} + +func writeSelectedAccountToState(state *state.State, selected *typesv1.Account) func() tea.Msg { + return func() tea.Msg { + log.Print("app: writeSelectedAccountToState") + state.CurrentAccountID = &selected.Id + if err := state.WriteBack(); err != nil { + return errMsg{err} + } + return &SelectedAccountMsg{Account: selected} + } +} diff --git a/pkg/tui/components/keyhandler/keyhandler.go b/pkg/tui/components/keyhandler/keyhandler.go new file mode 100644 index 00000000..f8e78a5e --- /dev/null +++ b/pkg/tui/components/keyhandler/keyhandler.go @@ -0,0 +1,43 @@ +package keyhandler + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +type Shell struct { + child tea.Model + key key.Binding + cmd tea.Cmd +} + +type State int + +var _ tea.Model = (*Shell)(nil) + +func Handle(key key.Binding, cmd tea.Cmd, child tea.Model) *Shell { + return &Shell{ + child: child, + key: key, + cmd: cmd, + } +} + +func (mdl *Shell) Init() tea.Cmd { + return mdl.child.Init() +} + +func (mdl *Shell) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if msg, ok := msg.(tea.KeyMsg); ok { + if key.Matches(msg, mdl.key) { + return mdl, mdl.cmd + } + } + var cmd tea.Cmd + mdl.child, cmd = mdl.child.Update(msg) + return mdl, cmd +} + +func (mdl *Shell) View() string { + return mdl.child.View() +} diff --git a/pkg/tui/components/keyhandler/main.go b/pkg/tui/components/keyhandler/main.go new file mode 100644 index 00000000..8f9cc313 --- /dev/null +++ b/pkg/tui/components/keyhandler/main.go @@ -0,0 +1,77 @@ +//go:build exclude + +package main + +import ( + "log" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/humanlogio/humanlog/pkg/tui/components/modal" +) + +func main() { + + style := lipgloss.NewStyle() + modes := []*modal.Mode{ + { + EnterModeKey: key.NewBinding( + key.WithKeys("q"), + ), + Name: "query", + Component: func() tea.Model { + ti := textinput.New() + ti.Prompt = "query" + return textInput{ti} + }(), + }, + { + EnterModeKey: key.NewBinding( + key.WithKeys("n"), + ), + Name: "navigate", + Component: func() tea.Model { + ti := textinput.New() + ti.Prompt = "fzf..." + return textInput{ti} + }(), + }, + { + EnterModeKey: key.NewBinding( + key.WithKeys("a"), + ), + Name: "auth", + Component: func() tea.Model { + ti := textinput.New() + ti.Prompt = "fzf..." + return textInput{ti} + }(), + }, + } + + exitKey := key.NewBinding(key.WithKeys("esc")) + + if _, err := tea.NewProgram(modal.NewModal(style, modes, exitKey)).Run(); err != nil { + log.Fatal(err) + } +} + +type textInput struct { + m textinput.Model +} + +func (m textInput) Init() tea.Cmd { + return nil +} + +func (m textInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + m.m, cmd = m.m.Update(msg) + return m, cmd +} + +func (m textInput) View() string { + return m.m.View() +} diff --git a/pkg/tui/components/modal/main.go b/pkg/tui/components/modal/main.go new file mode 100644 index 00000000..ac2f0e13 --- /dev/null +++ b/pkg/tui/components/modal/main.go @@ -0,0 +1,91 @@ +//go:build exclude + +package main + +import ( + "log" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/humanlogio/humanlog/pkg/tui/components/keyhandler" + "github.com/humanlogio/humanlog/pkg/tui/components/modal" +) + +func main() { + + baseStyle := lipgloss.NewStyle(). + PaddingLeft(1). + PaddingRight(1). + Foreground(lipgloss.Color("#282828")) + + modes := []*modal.Mode{ + { + EnterModeKey: key.NewBinding( + key.WithKeys("q"), + ), + Name: "query", + Component: func() tea.Model { + ti := textinput.New() + ti.Prompt = "query" + return textInput{ti} + }(), + }, + { + EnterModeKey: key.NewBinding( + key.WithKeys("n"), + ), + Name: "navigate", + Component: func() tea.Model { + ti := textinput.New() + ti.Prompt = "fzf..." + return textInput{ti} + }(), + }, + { + EnterModeKey: key.NewBinding( + key.WithKeys("a"), + ), + Name: "auth", + Component: func() tea.Model { + ti := textinput.New() + ti.Prompt = "fzf..." + return textInput{ti} + }(), + }, + } + + exitKey := key.NewBinding(key.WithKeys("esc")) + + app := keyhandler.Handle( + key.NewBinding(key.WithKeys("ctrl+d")), + tea.Batch( + tea.ClearScreen, + tea.Quit, + ), + modal.NewModal(baseStyle, modes, exitKey), + ) + + if _, err := tea.NewProgram(app).Run(); err != nil { + log.Fatal(err) + } +} + +type textInput struct { + m textinput.Model +} + +func (m textInput) Init() tea.Cmd { + return nil +} + +func (m textInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + m.m, cmd = m.m.Update(msg) + return m, cmd +} + +func (m textInput) View() string { + return m.m.View() +} diff --git a/pkg/tui/components/modal/mode_selector.go b/pkg/tui/components/modal/mode_selector.go new file mode 100644 index 00000000..a7164139 --- /dev/null +++ b/pkg/tui/components/modal/mode_selector.go @@ -0,0 +1,111 @@ +package modal + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/tree" +) + +const ( + SelectingMode State = iota + UsingMode +) + +type Mode struct { + EnterModeKey key.Binding + Name string + Component tea.Model +} + +type Modal struct { + modeStyle lipgloss.Style + current *Mode + modes []*Mode + + exitModeKey key.Binding +} + +type State int + +var _ tea.Model = (*Modal)(nil) + +func NewModal(baseStyle lipgloss.Style, modes []*Mode, exitModeKey key.Binding) *Modal { + modeStyle := baseStyle.Copy(). + Align(lipgloss.Center). + Bold(true). + Background(lipgloss.Color("#b8bb26")) + + return &Modal{ + modeStyle: modeStyle, + modes: modes, + exitModeKey: exitModeKey, + } +} + +func (mdl *Modal) Init() tea.Cmd { + var cmds []tea.Cmd + for _, mode := range mdl.modes { + cmds = append(cmds, mode.Component.Init()) + } + return tea.Batch(cmds...) +} + +func (mdl *Modal) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + + if mdl.current == nil { + // we're selecting a mode + if msg, ok := msg.(tea.KeyMsg); ok { + for _, mode := range mdl.modes { + if key.Matches(msg, mode.EnterModeKey) { + mdl.current = mode + var cmd tea.Cmd + mdl.current.Component, cmd = mdl.current.Component.Update(msg) + return mdl, cmd + } + } + } + return mdl, nil + } + // we're inside a mode + if msg, ok := msg.(tea.KeyMsg); ok { + if key.Matches(msg, mdl.exitModeKey) { + mdl.current = nil + return mdl, nil + } + } + var cmd tea.Cmd + mdl.current.Component, cmd = mdl.current.Component.Update(msg) + return mdl, cmd +} + +func (mdl *Modal) View() string { + if mdl.current == nil { + + rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1) + enumStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("10")).MarginRight(1) + + t := tree.New().Root( + mdl.modeStyle.Render("mode"), + ).ItemStyle(rootStyle).EnumeratorStyle(enumStyle) + for _, mode := range mdl.modes { + t.Child( + "(" + mode.EnterModeKey.Keys()[0] + ") ─> " + mode.Name, + ) + } + return t.String() + } + + footerStyle := lipgloss.NewStyle(). + Foreground(lipgloss.Color("#282828")). + Background(lipgloss.Color("#7c6f64")) + + modeFooter := mdl.modeStyle.Render(mdl.current.Name) + + footer := footerStyle.Render(modeFooter, " <- "+mdl.current.EnterModeKey.Keys()[0]) + + return lipgloss.JoinVertical(lipgloss.Left, + mdl.current.Component.View(), + footer, + ) +} diff --git a/pkg/tui/components/querybar/main.go b/pkg/tui/components/querybar/main.go new file mode 100644 index 00000000..eeb93ad5 --- /dev/null +++ b/pkg/tui/components/querybar/main.go @@ -0,0 +1,56 @@ +//go:build exclude + +package main + +import ( + "log" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/humanlogio/humanlog/pkg/tui/components/keyhandler" + "github.com/humanlogio/humanlog/pkg/tui/components/querybar" +) + +func main() { + + // baseStyle := lipgloss.NewStyle(). + // PaddingLeft(1). + // PaddingRight(1). + // Foreground(lipgloss.Color("#282828")) + + submitKey := key.NewBinding(key.WithKeys("enter")) + + app := keyhandler.Handle( + key.NewBinding(key.WithKeys("ctrl+d")), + tea.Batch( + tea.ClearScreen, + tea.Quit, + ), + querybar.NewQueryBar(submitKey, func(str string) []string { + return nil + }), + ) + + if _, err := tea.NewProgram(app).Run(); err != nil { + log.Fatal(err) + } +} + +type textInput struct { + m textinput.Model +} + +func (m textInput) Init() tea.Cmd { + return nil +} + +func (m textInput) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + m.m, cmd = m.m.Update(msg) + return m, cmd +} + +func (m textInput) View() string { + return m.m.View() +} diff --git a/pkg/tui/components/querybar/querybar.go b/pkg/tui/components/querybar/querybar.go new file mode 100644 index 00000000..c05e0f97 --- /dev/null +++ b/pkg/tui/components/querybar/querybar.go @@ -0,0 +1,74 @@ +package querybar + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textarea" + tea "github.com/charmbracelet/bubbletea" + "github.com/humanlogio/api/go/pkg/lang" + typesv1 "github.com/humanlogio/api/go/types/v1" +) + +type ValidatorFunc func(str string) []string + +type QueryBar struct { + textArea textarea.Model + submitQuery key.Binding + validator ValidatorFunc + + problems []string +} + +type SubmitQueryMsg struct { + Query *typesv1.LogQuery +} + +func NewQueryBar(submitQuery key.Binding, validator ValidatorFunc) *QueryBar { + ta := textarea.New() + ta.Prompt = "┃ " + ta.Placeholder = "Query..." + ta.Focus() + ta.SetHeight(1) + ta.ShowLineNumbers = false + ta.KeyMap.InsertNewline = key.NewBinding(key.WithDisabled()) + return &QueryBar{ + textArea: ta, + submitQuery: submitQuery, + validator: validator, + } +} + +func (m *QueryBar) Init() tea.Cmd { + return textarea.Blink +} + +func (m *QueryBar) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var ( + tiCmd tea.Cmd + ) + m.textArea, tiCmd = m.textArea.Update(msg) + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.textArea.SetWidth(msg.Width) + case tea.KeyMsg: + if key.Matches(msg, m.submitQuery) { + q := m.textArea.Value() + qq, err := lang.ParseLogQuery(q) + if err != nil { + m.problems = append(m.problems, err.Error()) + } else { + return m, func() tea.Msg { + return &SubmitQueryMsg{Query: qq} + } + } + } + } + + m.problems = m.validator(m.textArea.Value()) + + return m, tea.Batch(tiCmd) +} + +func (m *QueryBar) View() string { + return m.textArea.View() +} diff --git a/pkg/tui/machines.go b/pkg/tui/machines.go new file mode 100644 index 00000000..6c6dcd32 --- /dev/null +++ b/pkg/tui/machines.go @@ -0,0 +1,196 @@ +package tui + +import ( + "context" + "errors" + "log" + + "connectrpc.com/connect" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + accountv1 "github.com/humanlogio/api/go/svc/account/v1" + "github.com/humanlogio/api/go/svc/account/v1/accountv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/internal/pkg/state" +) + +type machineSelectorShell struct { + appStyle lipgloss.Style + ctx context.Context + state *state.State + accountClient accountv1connect.AccountServiceClient + + children tea.Model + + table table.Model + + nextCursor *typesv1.Cursor + machines []*typesv1.Machine + selected *typesv1.Machine + + err error +} + +func WithMachineSelectorShell( + appStyle lipgloss.Style, + ctx context.Context, + state *state.State, + accountClient accountv1connect.AccountServiceClient, + children tea.Model, +) *machineSelectorShell { + + columns := []table.Column{ + {Title: "Name", Width: 10}, + {Title: "OS", Width: 4}, + {Title: "Arch", Width: 4}, + {Title: "Hostname", Width: 10}, + } + t := table.New( + table.WithColumns(columns), + table.WithFocused(true), + table.WithHeight(7), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + return &machineSelectorShell{ + appStyle: appStyle, + ctx: ctx, + state: state, + children: children, + accountClient: accountClient, + table: t, + } +} + +func (m *machineSelectorShell) Init() tea.Cmd { + log.Printf("machine: init") + return m.children.Init() +} + +func (m *machineSelectorShell) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + + log.Printf("machine: update") + switch msg := msg.(type) { + + case *SelectedAccountMsg: + log.Printf("machine: got account selected orgID=%d", msg.Account.Id) + return m, listMachinesCmd(m.ctx, m.accountClient, m.state) + case listMachineMsg: + m.machines = msg.machines + m.nextCursor = msg.next + rows := make([]table.Row, 0, len(m.machines)) + for _, org := range m.machines { + rows = append(rows, table.Row{ + org.Name, + }) + } + m.table.SetRows(rows) + + case errMsg: + m.err = msg + return m, tea.Quit + + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + case "enter": + if m.selected == nil { + selectName := string(m.table.SelectedRow()[0]) + for _, item := range m.machines { + if item.Name == selectName { + m.selected = item + break + } + } + return m, writeSelectedMachineToState(m.state, m.selected) + } + } + case tea.WindowSizeMsg: + var ccmd, tcmd tea.Cmd + m.children, ccmd = m.children.Update(msg) + m.table, tcmd = m.table.Update(msg) + return m, tea.Batch(ccmd, tcmd) + } + + var cmd tea.Cmd + if m.selected != nil { + m.children, cmd = m.children.Update(msg) + } else { + m.table, cmd = m.table.Update(msg) + } + return m, cmd +} + +func (m *machineSelectorShell) View() string { + log.Printf("machine: view") + if m.selected == nil { + return m.appStyle.Render( + "Select a machine\n", + m.table.View(), + ) + } + return m.children.View() +} + +type listMachineMsg struct { + machines []*typesv1.Machine + next *typesv1.Cursor +} + +func listMachinesCmd( + ctx context.Context, + accountClient accountv1connect.AccountServiceClient, + state *state.State, +) func() tea.Msg { + return func() tea.Msg { + log.Printf("machine: listMachines") + res, err := accountClient.ListMachine(ctx, connect.NewRequest(&accountv1.ListMachineRequest{ + Cursor: nil, + Limit: 10, + AccountId: *state.CurrentAccountID, + })) + if err != nil { + cerr := new(connect.Error) + if errors.As(err, &cerr) { + log.Printf("machine: listMachines err=%v", cerr) + } + return errMsg{err} + } + out := listMachineMsg{ + machines: make([]*typesv1.Machine, 0, len(res.Msg.Items)), + } + for _, mc := range res.Msg.Items { + out.machines = append(out.machines, mc.Machine) + } + log.Printf("machine: got %d machines", len(out.machines)) + out.next = res.Msg.Next + return out + } +} + +type SelectedMachineMsg struct { + Machine *typesv1.Machine +} + +func writeSelectedMachineToState(state *state.State, selected *typesv1.Machine) func() tea.Msg { + return func() tea.Msg { + log.Print("app: writeSelectedMachineToState") + state.CurrentMachineID = &selected.Id + if err := state.WriteBack(); err != nil { + return errMsg{err} + } + return &SelectedMachineMsg{Machine: selected} + } +} diff --git a/pkg/tui/organizations.go b/pkg/tui/organizations.go new file mode 100644 index 00000000..9ab888a7 --- /dev/null +++ b/pkg/tui/organizations.go @@ -0,0 +1,203 @@ +package tui + +import ( + "context" + "errors" + "log" + + "connectrpc.com/connect" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + userv1 "github.com/humanlogio/api/go/svc/user/v1" + "github.com/humanlogio/api/go/svc/user/v1/userv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/internal/pkg/state" +) + +type orgSelectorShell struct { + appStyle lipgloss.Style + ctx context.Context + userClient userv1connect.UserServiceClient + state *state.State + + children tea.Model + + table table.Model + + organizations []*typesv1.Organization + nextCursor *typesv1.Cursor + selected *typesv1.Organization + + err error +} + +func WithOrgSelectorShell( + appStyle lipgloss.Style, + ctx context.Context, + state *state.State, + userClient userv1connect.UserServiceClient, + children tea.Model, +) *orgSelectorShell { + + columns := []table.Column{ + {Title: "Name", Width: 10}, + } + t := table.New( + table.WithColumns(columns), + table.WithFocused(true), + table.WithHeight(3), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")). + BorderBottom(true). + Bold(false) + s.Selected = s.Selected. + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("57")). + Bold(false) + t.SetStyles(s) + return &orgSelectorShell{ + appStyle: appStyle, + ctx: ctx, + state: state, + children: children, + userClient: userClient, + table: t, + } +} + +func (m *orgSelectorShell) Init() tea.Cmd { + log.Printf("org: init") + return tea.Batch( + listOrganizationsCmd(m.ctx, m.userClient, m.state), + m.children.Init(), + ) +} + +func (m *orgSelectorShell) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Printf("org: update") + switch msg := msg.(type) { + + case listOrganizationMsg: + m.organizations = msg.organizations + m.nextCursor = msg.next + rows := make([]table.Row, 0, len(m.organizations)) + for _, org := range m.organizations { + rows = append(rows, table.Row{ + org.Name, + }) + if m.state.CurrentOrgID != nil && *m.state.CurrentOrgID == org.Id { + m.selected = org + break + } + } + if len(m.organizations) == 1 { + m.selected = m.organizations[0] + return m, writeSelectedOrgToState(m.state, m.selected) + } + m.table.SetRows(rows) + + case errMsg: + m.err = msg + return m, tea.Quit + + case tea.KeyMsg: + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + case "enter": + if m.selected == nil { + selectName := string(m.table.SelectedRow()[0]) + for _, item := range m.organizations { + if item.Name == selectName { + m.selected = item + break + } + } + return m, writeSelectedOrgToState(m.state, m.selected) + } + } + case tea.WindowSizeMsg: + var ccmd, tcmd tea.Cmd + m.children, ccmd = m.children.Update(msg) + m.table, tcmd = m.table.Update(msg) + return m, tea.Batch(ccmd, tcmd) + } + + var cmd tea.Cmd + if m.selected != nil { + m.children, cmd = m.children.Update(msg) + } else { + m.table, cmd = m.table.Update(msg) + } + return m, cmd +} + +func (m *orgSelectorShell) View() string { + log.Printf("org: view") + if m.organizations == nil { + return "Looking up organizations..." + } + if m.selected == nil { + return m.appStyle.Render( + "Select an organization\n", + m.table.View(), + ) + } + return m.children.View() +} + +type listOrganizationMsg struct { + organizations []*typesv1.Organization + next *typesv1.Cursor +} + +func listOrganizationsCmd( + ctx context.Context, + userClient userv1connect.UserServiceClient, + state *state.State, +) func() tea.Msg { + return func() tea.Msg { + log.Printf("organization: listOrgs") + res, err := userClient.ListOrganization(ctx, connect.NewRequest(&userv1.ListOrganizationRequest{ + Cursor: nil, + Limit: 10, + })) + if err != nil { + cerr := new(connect.Error) + if errors.As(err, &cerr) { + log.Printf("organization: listOrgs err=%v", cerr) + } + return errMsg{err} + } + out := listOrganizationMsg{ + organizations: make([]*typesv1.Organization, 0, len(res.Msg.Items)), + } + for _, mc := range res.Msg.Items { + log.Printf("organization: got org %q", mc.Organization.Name) + out.organizations = append(out.organizations, mc.Organization) + } + log.Printf("organization: got %d orgs", len(out.organizations)) + out.next = res.Msg.Next + return out + } +} + +type SelectedOrganizationMsg struct { + Organization *typesv1.Organization +} + +func writeSelectedOrgToState(state *state.State, selected *typesv1.Organization) func() tea.Msg { + return func() tea.Msg { + log.Print("app: writeSelectedOrgToState") + state.CurrentOrgID = &selected.Id + if err := state.WriteBack(); err != nil { + return errMsg{err} + } + return &SelectedOrganizationMsg{Organization: selected} + } +} diff --git a/pkg/tui/querier.go b/pkg/tui/querier.go new file mode 100644 index 00000000..1c860264 --- /dev/null +++ b/pkg/tui/querier.go @@ -0,0 +1,310 @@ +package tui + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "time" + + "connectrpc.com/connect" + "github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + queryv1 "github.com/humanlogio/api/go/svc/query/v1" + "github.com/humanlogio/api/go/svc/query/v1/queryv1connect" + typesv1 "github.com/humanlogio/api/go/types/v1" + "github.com/humanlogio/humanlog/internal/pkg/state" + "github.com/humanlogio/humanlog/pkg/tui/components/querybar" + zone "github.com/lrstanley/bubblezone" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var axisStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("3")) // yellow + +var labelStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("6")) // cyan + +type QuerierModel struct { + appStyle lipgloss.Style + ctx context.Context + state *state.State + queryClient queryv1connect.QueryServiceClient + + from time.Time + to time.Time + + summary timeserieslinechart.Model + querybar *querybar.QueryBar + + stopQuery func() tea.Msg + sendMsg func(tea.Msg) + + height int + width int + inputs []*input +} + +type input struct { + logs *typesv1.LogEventGroup + viewport viewport.Model +} + +func NewQuerierModel( + appStyle lipgloss.Style, + ctx context.Context, + state *state.State, + queryClient queryv1connect.QueryServiceClient, + sendMsg func(tea.Msg), +) *QuerierModel { + + submitKey := key.NewBinding(key.WithKeys("enter")) + + qbar := querybar.NewQueryBar(submitKey, func(str string) []string { return nil }) + + return &QuerierModel{ + appStyle: appStyle, + ctx: ctx, + state: state, + queryClient: queryClient, + + from: time.Time{}, + to: time.Now(), + querybar: qbar, + + sendMsg: sendMsg, + } +} + +func (m *QuerierModel) Init() tea.Cmd { + return nil +} + +func (m *QuerierModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Printf("querier: update: %T -> %#v", msg, msg) + var cmds []tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.height = msg.Height + m.width = msg.Width + zoneManager := zone.New() + m.summary = timeserieslinechart.New( + min(msg.Height, 8), + msg.Width, + timeserieslinechart.WithZoneManager(zoneManager), + ) + m.summary.DrawXYAxisAndLabel() + + cmds = append(cmds, sendSummarizeCmd(m.ctx, m.queryClient, m.state, m.width, m.from, m.to)) + + case *querybar.SubmitQueryMsg: + // stop any existing running query + if m.stopQuery != nil { + cmds = append(cmds, m.stopQuery) + } + cmds = append(cmds, sendQueryCmd(m.ctx, m.queryClient, m.state, msg.Query, m.sendMsg)) + return m, tea.Batch(cmds...) + case *StartLogStreamMsg: + m.stopQuery = msg.StopFunc + for _, leg := range msg.Events { + m.addToInput(leg.MachineId, leg.SessionId, leg.Logs) + } + case *AppendLogsMsg: + for _, leg := range msg.Events { + m.addToInput(leg.MachineId, leg.SessionId, leg.Logs) + } + case *SummaryMsg: + m.summary.ClearAllData() + for _, bucket := range msg.Buckets { + tp := timeserieslinechart.TimePoint{ + Time: bucket.Ts.AsTime(), + Value: float64(bucket.EventCount), + } + m.summary.Push(tp) + } + m.summary.Draw() + + case errMsg: + log.Printf("querier error: %#v", msg.err) + return m, tea.Quit + } + qbar, qcmd := m.querybar.Update(msg) + m.querybar = qbar.(*querybar.QueryBar) + + if qcmd != nil { + cmds = append(cmds, qcmd) + } + + for _, input := range m.inputs { + vport, vcmd := input.viewport.Update(msg) + input.viewport = vport + if vcmd != nil { + cmds = append(cmds, vcmd) + } + } + return m, tea.Batch(cmds...) +} + +func (m *QuerierModel) addToInput(machineID, sessionID int64, logs []*typesv1.LogEvent) { + if m.inputs == nil { + m.inputs = make([]*input, 0) + } + found := false + for _, input := range m.inputs { + if input.logs.MachineId != machineID { + continue + } + if input.logs.SessionId != sessionID { + continue + } + input.logs.Logs = append(input.logs.Logs, logs...) + } + if !found { + m.inputs = append(m.inputs, &input{ + viewport: viewport.New(0, 0), // will be set to the proper w & h right away + logs: &typesv1.LogEventGroup{ + MachineId: machineID, + SessionId: sessionID, + Logs: logs, + }, + }) + // resize all inputs to the right w & h + m.resizeViewports() + } +} + +func (m *QuerierModel) resizeViewports() { + height := m.height - lipgloss.Height(m.querybar.View()) + width := m.width / len(m.inputs) + for _, sessions := range m.inputs { + sessions.viewport.Height = height + sessions.viewport.Width = width + } +} + +func sendSummarizeCmd( + ctx context.Context, + client queryv1connect.QueryServiceClient, + state *state.State, + width int, + from, to time.Time, +) func() tea.Msg { + return func() tea.Msg { + log.Printf("querier: send SummarizeEvents") + res, err := client.SummarizeEvents(ctx, connect.NewRequest(&queryv1.SummarizeEventsRequest{ + AccountId: *state.CurrentAccountID, + BucketCount: uint32(width - 5), + From: timestamppb.New(from), + To: timestamppb.New(to), + })) + if err != nil { + return errMsg{err: err} + } + return SummaryMsg{res.Msg} + } +} + +type SummaryMsg struct { + *queryv1.SummarizeEventsResponse +} + +func sendQueryCmd( + ctx context.Context, + client queryv1connect.QueryServiceClient, + state *state.State, + query *typesv1.LogQuery, + sendMsg func(tea.Msg), +) func() tea.Msg { + return func() tea.Msg { + log.Printf("querier: send WatchQuery") + res, err := client.WatchQuery(ctx, connect.NewRequest(&queryv1.WatchQueryRequest{ + AccountId: *state.CurrentAccountID, + Query: query, + })) + if err != nil { + return errMsg{err: err} + } + + if !res.Receive() { + return nil + } + + msg := res.Msg() + + f, err := os.Create("logdump.json") + if err != nil { + return errMsg{err: err} + } + enc := json.NewEncoder(f) + + run := true + go func() { + for res.Receive() && run { + msg := res.Msg() + + for _, leg := range msg.Events { + enc.Encode(map[string]int64{ + "machine": leg.MachineId, + "session": leg.SessionId, + }) + for _, ev := range leg.Logs { + enc.Encode(ev) + } + } + sendMsg(&AppendLogsMsg{ + Events: msg.GetEvents(), + }) + } + if err := res.Err(); err != nil { + sendMsg(errMsg{err: err}) + } + }() + + return &StartLogStreamMsg{ + Events: msg.GetEvents(), + StopFunc: func() tea.Msg { + f.Close() + err := res.Close() + run = false + if err != nil { + return errMsg{err: err} + } + return nil + }, + } + } +} + +type StartLogStreamMsg struct { + Events []*typesv1.LogEventGroup + StopFunc func() tea.Msg +} + +type AppendLogsMsg struct { + Events []*typesv1.LogEventGroup +} + +func (m *QuerierModel) View() string { + if m.width == 0 || m.height == 0 { + return "Initializing..." + } + var sessions []string + for _, input := range m.inputs { + input.viewport.SetContent(renderLogs(input.logs.Logs)) + sessions = append(sessions, input.viewport.View()) + } + return m.appStyle.Render(lipgloss.JoinVertical( + lipgloss.Left, + m.summary.View(), + m.querybar.View(), + lipgloss.JoinHorizontal(lipgloss.Top, sessions...), + )) +} + +func renderLogs(logs []*typesv1.LogEvent) string { + return fmt.Sprintf("%d log events", len(logs)) +} diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go new file mode 100644 index 00000000..ed696767 --- /dev/null +++ b/pkg/tui/tui.go @@ -0,0 +1,99 @@ +package tui + +import ( + "context" + "log" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/humanlogio/api/go/svc/account/v1/accountv1connect" + "github.com/humanlogio/api/go/svc/organization/v1/organizationv1connect" + "github.com/humanlogio/api/go/svc/query/v1/queryv1connect" + "github.com/humanlogio/api/go/svc/user/v1/userv1connect" + "github.com/humanlogio/humanlog/internal/pkg/state" +) + +func RunTUI( + ctx context.Context, + state *state.State, + userClient userv1connect.UserServiceClient, + organizationClient organizationv1connect.OrganizationServiceClient, + accountClient accountv1connect.AccountServiceClient, + queryClient queryv1connect.QueryServiceClient, +) error { + appStyle := lipgloss.NewStyle().Padding(1, 2) + + var p *tea.Program + app := &model{ + appStyle: appStyle, + state: state, + orgSelectorShell: WithOrgSelectorShell(appStyle, ctx, state, userClient, + WithAccountSelectorShell(appStyle, ctx, state, organizationClient, + // WithMachineSelectorShell(appStyle, ctx, state, accountClient, + NewQuerierModel(appStyle, ctx, state, queryClient, func(m tea.Msg) { + p.Send(m) + }), + // ), + ), + ), + } + p = tea.NewProgram(app) + + f, err := tea.LogToFile("debug.log", "humanlog") + if err != nil { + return err + } + defer f.Close() + + if _, err := p.Run(); err != nil { + log.Printf("program errored: %v", err) + return err + } + log.Printf("program exited") + return nil +} + +type model struct { + appStyle lipgloss.Style + + state *state.State + + err errMsg + + orgSelectorShell tea.Model +} + +func (m *model) Init() tea.Cmd { + log.Printf("app: init") + return m.orgSelectorShell.Init() +} + +func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + log.Printf("app: update: %T -> %#v", msg, msg) + switch msg := msg.(type) { + case errMsg: + m.err = msg + log.Printf("err=%v", msg.Error()) + return m, tea.Quit + case tea.KeyMsg: + log.Printf("app: key-press: %q", msg.String()) + switch msg.String() { + case "q", "ctrl+c": + return m, tea.Quit + } + } + var cmd tea.Cmd + m.orgSelectorShell, cmd = m.orgSelectorShell.Update(msg) + return m, cmd +} + +func (m *model) View() string { + log.Printf("app: view") + return m.appStyle.Render(m.orgSelectorShell.View()) +} + +type errMsg struct{ err error } + +// For messages that contain errors it's often handy to also implement the +// error interface on the message. +func (e errMsg) Error() string { return e.err.Error() } diff --git a/scanner.go b/scanner.go index 09402cfd..2ac56786 100644 --- a/scanner.go +++ b/scanner.go @@ -5,10 +5,10 @@ import ( "bytes" "context" "io" - "time" - "github.com/humanlogio/humanlog/internal/pkg/model" + typesv1 "github.com/humanlogio/api/go/types/v1" "github.com/humanlogio/humanlog/pkg/sink" + "google.golang.org/protobuf/types/known/timestamppb" ) // Scan reads JSON-structured lines from src and prettify them onto dst. If @@ -24,8 +24,8 @@ func Scan(ctx context.Context, src io.Reader, sink sink.Sink, opts *HandlerOptio logfmtEntry := LogfmtHandler{Opts: opts} jsonEntry := JSONHandler{Opts: opts} - ev := new(model.Event) - data := new(model.Structured) + ev := new(typesv1.LogEvent) + data := new(typesv1.StructuredLogEvent) ev.Structured = data for in.Scan() { @@ -36,11 +36,9 @@ func Scan(ctx context.Context, src io.Reader, sink sink.Sink, opts *HandlerOptio if ev.Structured == nil { ev.Structured = data } - data.Time = time.Time{} - data.Msg = "" - data.Level = "" - data.KVs = data.KVs[:0] + data.Reset() ev.Raw = lineData + ev.ParsedAt = timestamppb.Now() // remove that pesky syslog crap lineData = bytes.TrimPrefix(lineData, []byte("@cee: ")) diff --git a/scanner_test.go b/scanner_test.go index 075f77db..3b1755cc 100644 --- a/scanner_test.go +++ b/scanner_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/humanlogio/humanlog/internal/pkg/model" + typesv1 "github.com/humanlogio/api/go/types/v1" "github.com/humanlogio/humanlog/pkg/sink/bufsink" "github.com/stretchr/testify/require" ) @@ -14,8 +14,8 @@ func TestScannerLongLine(t *testing.T) { data := `{"msg":"` + strings.Repeat("a", 1023*1024) + `"}` ctx := context.Background() src := strings.NewReader(data) - want := []model.Event{ - {Raw: []byte(data), Structured: &model.Structured{Msg: strings.Repeat("a", 1023*1024)}}, + want := []*typesv1.LogEvent{ + {Raw: []byte(data), Structured: &typesv1.StructuredLogEvent{Msg: strings.Repeat("a", 1023*1024)}}, } sink := bufsink.NewSizedBufferedSink(100, nil) err := Scan(ctx, src, sink, DefaultOptions()) diff --git a/script/create_version_artifacts.sh b/script/create_version_artifacts.sh index 3efd03d9..d1af8f9c 100755 --- a/script/create_version_artifacts.sh +++ b/script/create_version_artifacts.sh @@ -76,17 +76,22 @@ function get_project_name() { jq < dist/metadata.json -r '.project_name' } +function get_channel() { + echo "latest" +} + function main() { owner=humanlogio project=$(get_project_name) tag=$(get_version) + channel=$(get_channel) list_archives | handle_archive apictl create published-version \ --project ${project} \ - --channel latest \ + --channel ${channel} \ --version $(get_version) } -main \ No newline at end of file +main diff --git a/time_parse.go b/time_parse.go index debd9801..320d46aa 100644 --- a/time_parse.go +++ b/time_parse.go @@ -4,7 +4,7 @@ import ( "time" ) -var formats = []string{ +var TimeFormats = []string{ "2006-01-02 15:04:05.999999999 -0700 MST", "2006-01-02 15:04:05", "2006-01-02T15:04:05-0700", @@ -48,7 +48,7 @@ func tryParseTime(value interface{}) (time.Time, bool) { var err error switch value.(type) { case string: - for _, layout := range formats { + for _, layout := range TimeFormats { t, err = time.Parse(layout, value.(string)) if err == nil { return t, true diff --git a/vendor/connectrpc.com/connect/.gitignore b/vendor/connectrpc.com/connect/.gitignore new file mode 100644 index 00000000..35531439 --- /dev/null +++ b/vendor/connectrpc.com/connect/.gitignore @@ -0,0 +1,5 @@ +/.tmp/ +*.pprof +*.svg +cover.out +connect.test diff --git a/vendor/github.com/bufbuild/connect-go/.golangci.yml b/vendor/connectrpc.com/connect/.golangci.yml similarity index 63% rename from vendor/github.com/bufbuild/connect-go/.golangci.yml rename to vendor/connectrpc.com/connect/.golangci.yml index e70e6773..fc627292 100644 --- a/vendor/github.com/bufbuild/connect-go/.golangci.yml +++ b/vendor/connectrpc.com/connect/.golangci.yml @@ -6,11 +6,7 @@ linters-settings: exhaustruct: include: # No zero values for param structs. - - 'github\.com/bufbuild/connect-go\..*[pP]arams' - # No zero values for ClientStream, ServerStream, and friends. - - 'github\.com/bufbuild/connect-go\.ClientStream.*' - - 'github\.com/bufbuild/connect-go\.ServerStream.*' - - 'github\.com/bufbuild/connect-go\.BidiStream.*' + - 'connectrpc\.com/connect\..*[pP]arams' forbidigo: forbid: - '^fmt\.Print' @@ -22,6 +18,13 @@ linters-settings: # TODO, OPT, etc. comments are fine to commit. Use FIXME comments for # temporary hacks, and use godox to prevent committing them. keywords: [FIXME] + importas: + no-unaliased: true + alias: + - pkg: connectrpc.com/connect + alias: connect + - pkg: connectrpc.com/connect/internal/gen/connect/ping/v1 + alias: pingv1 varnamelen: ignore-decls: - T any @@ -32,6 +35,7 @@ linters: disable: - cyclop # covered by gocyclo - deadcode # abandoned + - depguard # unnecessary for small libraries - exhaustivestruct # replaced by exhaustruct - funlen # rely on code review to limit function length - gocognit # dubious "cognitive overhead" quantification @@ -40,13 +44,16 @@ linters: - golint # deprecated by Go team - gomnd # some unnamed constants are okay - ifshort # deprecated by author + - inamedparam # convention is not followed - interfacer # deprecated by author - ireturn # "accept interfaces, return structs" isn't ironclad - lll # don't want hard limits for line length - maintidx # covered by gocyclo - maligned # readability trumps efficient struct packing - nlreturn # generous whitespace violates house style + - nonamedreturns # named returns are fine; it's *bare* returns that are bad - nosnakecase # deprecated in https://github.com/golangci/golangci-lint/pull/3065 + - protogetter # too many false positives - scopelint # deprecated by author - structcheck # abandoned - testpackage # internal tests are fine @@ -69,9 +76,12 @@ issues: # We need to init a global in-mem HTTP server for testable examples. - linters: [gochecknoinits, gochecknoglobals] path: example_init_test.go - # We need to initialize a global map from a slice. - - linters: [gochecknoinits, gochecknoglobals] + # We need to initialize default grpc User-Agent + - linters: [gochecknoglobals] path: protocol_grpc.go + # We need to initialize default connect User-Agent + - linters: [gochecknoglobals] + path: protocol_connect.go # We purposefully do an ineffectual assignment for an example. - linters: [ineffassign] path: client_example_test.go @@ -91,3 +101,34 @@ issues: # No output assertions needed for these examples. - linters: [testableexamples] path: error_writer_example_test.go + - linters: [testableexamples] + path: error_not_modified_example_test.go + - linters: [testableexamples] + path: error_example_test.go + # In examples, it's okay to use http.ListenAndServe. + - linters: [gosec] + path: error_not_modified_example_test.go + # There are many instances where we want to keep unused parameters + # as a matter of style or convention, for example when a context.Context + # is the first parameter, we choose to just globally ignore this. + - linters: [revive] + text: "^unused-parameter: " + # We want to return explicit nils in protocol_grpc.go + - linters: [revive] + text: "^if-return: " + path: protocol_grpc.go + # We want to return explicit nils in protocol_connect.go + - linters: [revive] + text: "^if-return: " + path: protocol_connect.go + # We want to return explicit nils in error_writer.go + - linters: [revive] + text: "^if-return: " + path: error_writer.go + # We want to set http.Server's logger + - linters: [forbidigo] + path: internal/memhttp + text: "use of `log.(New|Logger|Lshortfile)` forbidden by pattern .*" + # We want to show examples with http.Get + - linters: [noctx] + path: internal/memhttp/memhttp_test.go diff --git a/vendor/github.com/bufbuild/connect-go/LICENSE b/vendor/connectrpc.com/connect/LICENSE similarity index 99% rename from vendor/github.com/bufbuild/connect-go/LICENSE rename to vendor/connectrpc.com/connect/LICENSE index a037229e..8b55ffaf 100644 --- a/vendor/github.com/bufbuild/connect-go/LICENSE +++ b/vendor/connectrpc.com/connect/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2021-2022 Buf Technologies, Inc. + Copyright 2021-2024 The Connect Authors Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vendor/connectrpc.com/connect/MAINTAINERS.md b/vendor/connectrpc.com/connect/MAINTAINERS.md new file mode 100644 index 00000000..129a3c01 --- /dev/null +++ b/vendor/connectrpc.com/connect/MAINTAINERS.md @@ -0,0 +1,12 @@ +Maintainers +=========== + +## Current +* [Peter Edge](https://github.com/bufdev), [Buf](https://buf.build) +* [Akshay Shah](https://github.com/akshayjshah), [Buf](https://buf.build) +* [Josh Humphries](https://github.com/jhump), [Buf](https://buf.build) +* [Matt Robenolt](https://github.com/mattrobenolt), [PlanetScale](https://planetscale.com) +* [Edward McFarlane](https://github.com/emcfarlane), [Buf](https://buf.build) + +## Former +* [Alex McKinney](https://github.com/amckinney) diff --git a/vendor/connectrpc.com/connect/Makefile b/vendor/connectrpc.com/connect/Makefile new file mode 100644 index 00000000..cb07829b --- /dev/null +++ b/vendor/connectrpc.com/connect/Makefile @@ -0,0 +1,117 @@ +# See https://tech.davis-hansson.com/p/make/ +SHELL := bash +.DELETE_ON_ERROR: +.SHELLFLAGS := -eu -o pipefail -c +.DEFAULT_GOAL := all +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules +MAKEFLAGS += --no-print-directory +BIN := .tmp/bin +export PATH := $(BIN):$(PATH) +export GOBIN := $(abspath $(BIN)) +COPYRIGHT_YEARS := 2021-2024 +LICENSE_IGNORE := --ignore /testdata/ + +.PHONY: help +help: ## Describe useful make targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' + +.PHONY: all +all: ## Build, test, and lint (default) + $(MAKE) test + $(MAKE) lint + +.PHONY: clean +clean: ## Delete intermediate build artifacts + @# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs + git clean -Xdf + +.PHONY: test +test: shorttest slowtest + +.PHONY: shorttest +shorttest: build ## Run unit tests + go test -vet=off -race -cover -short ./... + +.PHONY: slowtest +# Runs all tests, including known long/slow ones. The +# race detector is not used for a few reasons: +# 1. Race coverage of the short tests should be +# adequate to catch race conditions. +# 2. It slows tests down, which is not good if we +# know these are already slow tests. +# 3. Some of the slow tests can't repro issues and +# find regressions as reliably with the race +# detector enabled. +slowtest: build + go test ./... + +.PHONY: runconformance +runconformance: build ## Run conformance test suite + cd internal/conformance && ./runconformance.sh + +.PHONY: bench +bench: BENCH ?= .* +bench: build ## Run benchmarks for root package + go test -vet=off -run '^$$' -bench '$(BENCH)' -benchmem -cpuprofile cpu.pprof -memprofile mem.pprof . + +.PHONY: build +build: generate ## Build all packages + go build ./... + +.PHONY: install +install: ## Install all binaries + go install ./... + +.PHONY: lint +lint: $(BIN)/golangci-lint $(BIN)/buf ## Lint Go and protobuf + go vet ./... + golangci-lint run --modules-download-mode=readonly --timeout=3m0s + buf lint + buf format -d --exit-code + +.PHONY: lintfix +lintfix: $(BIN)/golangci-lint $(BIN)/buf ## Automatically fix some lint errors + golangci-lint run --fix --modules-download-mode=readonly --timeout=3m0s + buf format -w + +.PHONY: generate +generate: $(BIN)/buf $(BIN)/protoc-gen-go $(BIN)/protoc-gen-connect-go $(BIN)/license-header ## Regenerate code and licenses + rm -rf internal/gen + PATH="$(abspath $(BIN))" buf generate + license-header \ + --license-type apache \ + --copyright-holder "The Connect Authors" \ + --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) + +.PHONY: upgrade +upgrade: ## Upgrade dependencies + go get -u -t ./... && go mod tidy -v + +.PHONY: checkgenerate +checkgenerate: + @# Used in CI to verify that `make generate` doesn't produce a diff. + test -z "$$(git status --porcelain | tee /dev/stderr)" + +.PHONY: $(BIN)/protoc-gen-connect-go +$(BIN)/protoc-gen-connect-go: + @mkdir -p $(@D) + go build -o $(@) ./cmd/protoc-gen-connect-go + +$(BIN)/buf: Makefile + @mkdir -p $(@D) + go install github.com/bufbuild/buf/cmd/buf@v1.27.2 + +$(BIN)/license-header: Makefile + @mkdir -p $(@D) + go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v1.27.2 + +$(BIN)/golangci-lint: Makefile + @mkdir -p $(@D) + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.55.2 + +$(BIN)/protoc-gen-go: Makefile go.mod + @mkdir -p $(@D) + @# The version of protoc-gen-go is determined by the version in go.mod + go install google.golang.org/protobuf/cmd/protoc-gen-go + diff --git a/vendor/github.com/bufbuild/connect-go/README.md b/vendor/connectrpc.com/connect/README.md similarity index 57% rename from vendor/github.com/bufbuild/connect-go/README.md rename to vendor/connectrpc.com/connect/README.md index 38d0dc5e..082e72af 100644 --- a/vendor/github.com/bufbuild/connect-go/README.md +++ b/vendor/connectrpc.com/connect/README.md @@ -1,9 +1,10 @@ Connect ======= -[![Build](https://github.com/bufbuild/connect-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/bufbuild/connect-go/actions/workflows/ci.yaml) -[![Report Card](https://goreportcard.com/badge/github.com/bufbuild/connect-go)](https://goreportcard.com/report/github.com/bufbuild/connect-go) -[![GoDoc](https://pkg.go.dev/badge/github.com/bufbuild/connect-go.svg)](https://pkg.go.dev/github.com/bufbuild/connect-go) +[![Build](https://github.com/connectrpc/connect-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/connectrpc/connect-go/actions/workflows/ci.yaml) +[![Report Card](https://goreportcard.com/badge/connectrpc.com/connect)](https://goreportcard.com/report/connectrpc.com/connect) +[![GoDoc](https://pkg.go.dev/badge/connectrpc.com/connect.svg)](https://pkg.go.dev/connectrpc.com/connect) +[![Slack](https://img.shields.io/badge/slack-buf-%23e01563)][slack] Connect is a slim library for building browser and gRPC-compatible HTTP APIs. You write a short [Protocol Buffer][protobuf] schema and implement your @@ -12,30 +13,29 @@ compression, and content type negotiation. It also generates an idiomatic, type-safe client. Handlers and clients support three protocols: gRPC, gRPC-Web, and Connect's own protocol. -The [Connect protocol][protocol] is a simple, POST-only protocol that works -over HTTP/1.1 or HTTP/2. It takes the best portions of gRPC and gRPC-Web, -including streaming, and packages them into a protocol that works equally well -in browsers, monoliths, and microservices. Calling a Connect API is as easy as +The [Connect protocol][protocol] is a simple protocol that works over HTTP/1.1 +or HTTP/2. It takes the best portions of gRPC and gRPC-Web, including +streaming, and packages them into a protocol that works equally well in +browsers, monoliths, and microservices. Calling a Connect API is as easy as using `curl`. Try it with our live demo: ``` curl \ --header "Content-Type: application/json" \ --data '{"sentence": "I feel happy."}' \ - https://demo.connect.build/buf.connect.demo.eliza.v1.ElizaService/Say + https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say ``` Handlers and clients also support the gRPC and gRPC-Web protocols, including streaming, headers, trailers, and error details. gRPC-compatible [server -reflection][] and [health checks][] are available as standalone packages. -Instead of cURL, we could call our API with `grpcurl`: +reflection][grpcreflect] and [health checks][grpchealth] are available as +standalone packages. Instead of cURL, we could call our API with a gRPC client: ``` -go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest -grpcurl \ - -d '{"sentence": "I feel happy."}' \ - demo.connect.build:443 \ - buf.connect.demo.eliza.v1.ElizaService/Say +go install github.com/bufbuild/buf/cmd/buf@latest +buf curl --protocol grpc \ + --data '{"sentence": "I feel happy."}' \ + https://demo.connectrpc.com/connectrpc.eliza.v1.ElizaService/Say ``` Under the hood, Connect is just [Protocol Buffers][protobuf] and the standard @@ -45,8 +45,8 @@ still applies, and any package that works with an `http.Server`, `http.Client`, or `http.Handler` also works with Connect. For more on Connect, see the [announcement blog post][blog], the documentation -on [connect.build][docs] (especially the [Getting Started] guide for Go), the -[demo service][demo], or the [protocol specification][protocol]. +on [connectrpc.com][docs] (especially the [Getting Started] guide for Go), the +[demo service][examples-go], or the [protocol specification][protocol]. ## A small example @@ -63,9 +63,9 @@ import ( "log" "net/http" - "github.com/bufbuild/connect-go" - pingv1 "github.com/bufbuild/connect-go/internal/gen/connect/ping/v1" - "github.com/bufbuild/connect-go/internal/gen/connect/ping/v1/pingv1connect" + "connectrpc.com/connect" + pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" + "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) @@ -106,7 +106,7 @@ func main() { ``` With that server running, you can make requests with any gRPC or Connect -client. To write a client using `connect-go`, +client. To write a client using Connect, ```go package main @@ -116,9 +116,9 @@ import ( "log" "net/http" - "github.com/bufbuild/connect-go" - pingv1 "github.com/bufbuild/connect-go/internal/gen/connect/ping/v1" - "github.com/bufbuild/connect-go/internal/gen/connect/ping/v1/pingv1connect" + "connectrpc.com/connect" + pingv1 "connectrpc.com/connect/internal/gen/connect/ping/v1" + "connectrpc.com/connect/internal/gen/connect/ping/v1/pingv1connect" ) func main() { @@ -145,21 +145,22 @@ configuring timeouts, connection pools, observability, and h2c. ## Ecosystem -* [connect-grpchealth-go]: gRPC-compatible health checks -* [connect-grpcreflect-go]: gRPC-compatible server reflection -* [connect-demo]: service powering demo.connect.build, including bidi streaming -* [connect-web]: TypeScript clients for web browsers +* [grpchealth]: gRPC-compatible health checks +* [grpcreflect]: gRPC-compatible server reflection +* [examples-go]: service powering demo.connectrpc.com, including bidi streaming +* [connect-es]: Type-safe APIs with Protobuf and TypeScript * [Buf Studio]: web UI for ad-hoc RPCs -* [connect-crosstest]: gRPC and gRPC-Web interoperability tests +* [conformance]: Connect, gRPC, and gRPC-Web interoperability tests ## Status: Stable This module is stable. It supports: -* The [two most recent major releases][go-support-policy] of Go. +* The three most recent major releases of Go. Keep in mind that [only the last + two releases receive security patches][go-support-policy]. * [APIv2] of Protocol Buffers in Go (`google.golang.org/protobuf`). -Within those parameters, `connect-go` follows semantic versioning. We will +Within those parameters, `connect` follows semantic versioning. We will _not_ make breaking changes in the 1.x series of releases. ## Legal @@ -167,20 +168,18 @@ _not_ make breaking changes in the 1.x series of releases. Offered under the [Apache 2 license][license]. [APIv2]: https://blog.golang.org/protobuf-apiv2 -[Buf Studio]: https://studio.buf.build/ -[Getting Started]: https://connect.build/docs/go/getting-started +[Buf Studio]: https://buf.build/studio +[Getting Started]: https://connectrpc.com/docs/go/getting-started [blog]: https://buf.build/blog/connect-a-better-grpc -[connect-crosstest]: https://github.com/bufbuild/connect-crosstest -[connect-demo]: https://github.com/bufbuild/connect-demo -[connect-grpchealth-go]: https://github.com/bufbuild/connect-grpchealth-go -[connect-grpcreflect-go]: https://github.com/bufbuild/connect-grpcreflect-go -[connect-web]: https://www.npmjs.com/package/@bufbuild/connect-web -[demo]: https://github.com/bufbuild/connect-demo -[docs-deployment]: https://connect.build/docs/go/deployment -[docs]: https://connect.build +[conformance]: https://github.com/connectrpc/conformance +[grpchealth]: https://github.com/connectrpc/grpchealth-go +[grpcreflect]: https://github.com/connectrpc/grpcreflect-go +[connect-es]: https://github.com/connectrpc/connect-es +[examples-go]: https://github.com/connectrpc/examples-go +[docs-deployment]: https://connectrpc.com/docs/go/deployment +[docs]: https://connectrpc.com [go-support-policy]: https://golang.org/doc/devel/release#policy -[health checks]: https://github.com/bufbuild/connect-grpchealth-go -[license]: https://github.com/bufbuild/connect-go/blob/main/LICENSE +[license]: https://github.com/connectrpc/connect-go/blob/main/LICENSE [protobuf]: https://developers.google.com/protocol-buffers -[protocol]: https://connect.build/docs/protocol -[server reflection]: https://github.com/bufbuild/connect-grpcreflect-go +[protocol]: https://connectrpc.com/docs/protocol +[slack]: https://buf.build/links/slack diff --git a/vendor/connectrpc.com/connect/RELEASE.md b/vendor/connectrpc.com/connect/RELEASE.md new file mode 100644 index 00000000..22364cd1 --- /dev/null +++ b/vendor/connectrpc.com/connect/RELEASE.md @@ -0,0 +1,44 @@ +# Releasing connect-go + +This document outlines how to create a release of connect-go. + +1. Clone the repo, ensuring you have the latest main. + +2. On a new branch, open [connect.go](connect.go) and change the `Version` constant to an appropriate [semantic version](https://semver.org/). To select the correct version, look at the version number of the [latest release] and the changes that are included in this new release. + * If there are only bug fixes and no new features, remove the `-dev` suffix, set MINOR number to be equal to the [latest release], and set the PATCH number to be 1 more than the PATCH number of the [latest release]. + * If there are features being released, remove the `-dev` suffix, set the MINOR number to be 1 more than the MINOR number of the [latest release], and set the PATCH number to `0`. In the common case, the diff here will just be to remove the `-dev` suffix. + + ```patch + -const Version = "1.14.0-dev" + +const Version = "1.14.0" + ``` + +3. Check for any changes in [cmd/protoc-gen-connect-go/main.go](cmd/protoc-gen-connect-go/main.go) that require a version restriction. A constant `IsAtLeastVersionX_Y_Z` should be defined in [connect.go](connect.go) if generated code has begun to use a new API. Make sure the generated code references this constant. If a new constant has been added since the last release, ensure that the name of the constant matches the version being released ([Example PR #496](https://github.com/connectrpc/connect-go/pull/496)). + +4. Open a PR titled "Prepare for vX.Y.Z" ([Example PR #661](https://github.com/connectrpc/connect-go/pull/661)) and a description tagging all current maintainers. Once it's reviewed and CI passes, merge it. + + *Make sure no new commits are merged until the release is complete.* + +5. Review all commits in the new release and for each PR check an appropriate label is used and edit the title to be meaninful to end users. This will help auto-generated release notes match the final notes as closely as possible. + +6. Using the Github UI, create a new release. + - Under “Choose a tagâ€, type in “vX.Y.Z†to create a new tag for the release upon publish. + - Target the main branch. + - Title the Release “vX.Y.Zâ€. + - Click “set as latest releaseâ€. + - Set the last version as the “Previous tagâ€. + - Click “Generate release notes†to autogenerate release notes. + - Edit the release notes. A summary and other sub categories may be added if required but should, in most cases, be left as ### Enhancements and ### Bugfixes. Feel free to collect multiple small changes to docs or Github config into one line, but try to tag every contributor. Make especially sure to credit new external contributors! + +7. Publish the release. + +8. On a new branch, open [connect.go](connect.go) and change the `Version` to increment the minor tag and append the `-dev` suffix. Use the next minor release - we never anticipate bugs and patch releases. + + ```patch + -const Version = "1.14.0" + +const Version = "1.15.0-dev" + ``` + +9. Open a PR titled "Back to development" ([Example PR #662](https://github.com/connectrpc/connect-go/pull/662)). Once it's reviewed and CI passes, merge it. + +[latest release]: https://github.com/connectrpc/connect-go/releases/latest diff --git a/vendor/connectrpc.com/connect/SECURITY.md b/vendor/connectrpc.com/connect/SECURITY.md new file mode 100644 index 00000000..04dcde52 --- /dev/null +++ b/vendor/connectrpc.com/connect/SECURITY.md @@ -0,0 +1,5 @@ +Security Policy +=============== + +This project follows the [Connect security policy and reporting +process](https://connectrpc.com/docs/governance/security). diff --git a/vendor/github.com/bufbuild/connect-go/buf.gen.yaml b/vendor/connectrpc.com/connect/buf.gen.yaml similarity index 78% rename from vendor/github.com/bufbuild/connect-go/buf.gen.yaml rename to vendor/connectrpc.com/connect/buf.gen.yaml index 757746fa..09894754 100644 --- a/vendor/github.com/bufbuild/connect-go/buf.gen.yaml +++ b/vendor/connectrpc.com/connect/buf.gen.yaml @@ -2,7 +2,7 @@ version: v1 managed: enabled: true go_package_prefix: - default: github.com/bufbuild/connect-go/internal/gen + default: connectrpc.com/connect/internal/gen plugins: - name: go out: internal/gen diff --git a/vendor/github.com/bufbuild/connect-go/buf.work.yaml b/vendor/connectrpc.com/connect/buf.work.yaml similarity index 100% rename from vendor/github.com/bufbuild/connect-go/buf.work.yaml rename to vendor/connectrpc.com/connect/buf.work.yaml diff --git a/vendor/github.com/bufbuild/connect-go/buffer_pool.go b/vendor/connectrpc.com/connect/buffer_pool.go similarity index 96% rename from vendor/github.com/bufbuild/connect-go/buffer_pool.go rename to vendor/connectrpc.com/connect/buffer_pool.go index 13f8a9e8..8ac68db2 100644 --- a/vendor/github.com/bufbuild/connect-go/buffer_pool.go +++ b/vendor/connectrpc.com/connect/buffer_pool.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/vendor/github.com/bufbuild/connect-go/client.go b/vendor/connectrpc.com/connect/client.go similarity index 77% rename from vendor/github.com/bufbuild/connect-go/client.go rename to vendor/connectrpc.com/connect/client.go index ad223040..1a7b1d64 100644 --- a/vendor/github.com/bufbuild/connect-go/client.go +++ b/vendor/connectrpc.com/connect/client.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,8 +17,11 @@ package connect import ( "context" "errors" + "fmt" "io" "net/http" + "net/url" + "strings" ) // Client is a reusable, concurrency-safe client for a single procedure. @@ -55,10 +58,13 @@ func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...Clien Protobuf: config.protobuf(), CompressMinBytes: config.CompressMinBytes, HTTPClient: httpClient, - URL: url, + URL: config.URL, BufferPool: config.BufferPool, ReadMaxBytes: config.ReadMaxBytes, SendMaxBytes: config.SendMaxBytes, + EnableGet: config.EnableGet, + GetURLMaxBytes: config.GetURLMaxBytes, + GetUseFallback: config.GetUseFallback, }, ) if protocolErr != nil { @@ -71,6 +77,9 @@ func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...Clien unarySpec := config.newSpec(StreamTypeUnary) unaryFunc := UnaryFunc(func(ctx context.Context, request AnyRequest) (AnyResponse, error) { conn := client.protocolClient.NewConn(ctx, unarySpec, request.Header()) + conn.onRequestSend(func(r *http.Request) { + request.setRequestMethod(r.Method) + }) // Send always returns an io.EOF unless the error is from the client-side. // We want the user to continue to call Receive in those cases to get the // full error from the server-side. @@ -83,7 +92,7 @@ func NewClient[Req, Res any](httpClient HTTPClient, url string, options ...Clien _ = conn.CloseResponse() return nil, err } - response, err := receiveUnaryResponse[Res](conn) + response, err := receiveUnaryResponse[Res](conn, config.Initializer) if err != nil { _ = conn.CloseResponse() return nil, err @@ -126,7 +135,10 @@ func (c *Client[Req, Res]) CallClientStream(ctx context.Context) *ClientStreamFo if c.err != nil { return &ClientStreamForClient[Req, Res]{err: c.err} } - return &ClientStreamForClient[Req, Res]{conn: c.newConn(ctx, StreamTypeClient)} + return &ClientStreamForClient[Req, Res]{ + conn: c.newConn(ctx, StreamTypeClient, nil), + initializer: c.config.Initializer, + } } // CallServerStream calls a server streaming procedure. @@ -134,7 +146,11 @@ func (c *Client[Req, Res]) CallServerStream(ctx context.Context, request *Reques if c.err != nil { return nil, c.err } - conn := c.newConn(ctx, StreamTypeServer) + conn := c.newConn(ctx, StreamTypeServer, func(r *http.Request) { + request.method = r.Method + }) + request.spec = conn.Spec() + request.peer = conn.Peer() mergeHeaders(conn.RequestHeader(), request.header) // Send always returns an io.EOF unless the error is from the client-side. // We want the user to continue to call Receive in those cases to get the @@ -147,7 +163,10 @@ func (c *Client[Req, Res]) CallServerStream(ctx context.Context, request *Reques if err := conn.CloseRequest(); err != nil { return nil, err } - return &ServerStreamForClient[Res]{conn: conn}, nil + return &ServerStreamForClient[Res]{ + conn: conn, + initializer: c.config.Initializer, + }, nil } // CallBidiStream calls a bidirectional streaming procedure. @@ -155,14 +174,19 @@ func (c *Client[Req, Res]) CallBidiStream(ctx context.Context) *BidiStreamForCli if c.err != nil { return &BidiStreamForClient[Req, Res]{err: c.err} } - return &BidiStreamForClient[Req, Res]{conn: c.newConn(ctx, StreamTypeBidi)} + return &BidiStreamForClient[Req, Res]{ + conn: c.newConn(ctx, StreamTypeBidi, nil), + initializer: c.config.Initializer, + } } -func (c *Client[Req, Res]) newConn(ctx context.Context, streamType StreamType) StreamingClientConn { +func (c *Client[Req, Res]) newConn(ctx context.Context, streamType StreamType, onRequestSend func(r *http.Request)) StreamingClientConn { newConn := func(ctx context.Context, spec Spec) StreamingClientConn { header := make(http.Header, 8) // arbitrary power of two, prevent immediate resizing c.protocolClient.WriteRequestHeader(streamType, header) - return c.protocolClient.NewConn(ctx, spec, header) + conn := c.protocolClient.NewConn(ctx, spec, header) + conn.onRequestSend(onRequestSend) + return conn } if interceptor := c.config.Interceptor; interceptor != nil { newConn = interceptor.WrapStreamingClient(newConn) @@ -171,8 +195,11 @@ func (c *Client[Req, Res]) newConn(ctx context.Context, streamType StreamType) S } type clientConfig struct { + URL *url.URL Protocol protocol Procedure string + Schema any + Initializer maybeInitializer CompressMinBytes int Interceptor Interceptor CompressionPools map[string]*compressionPool @@ -182,11 +209,20 @@ type clientConfig struct { BufferPool *bufferPool ReadMaxBytes int SendMaxBytes int + EnableGet bool + GetURLMaxBytes int + GetUseFallback bool + IdempotencyLevel IdempotencyLevel } -func newClientConfig(url string, options []ClientOption) (*clientConfig, *Error) { - protoPath := extractProtoPath(url) +func newClientConfig(rawURL string, options []ClientOption) (*clientConfig, *Error) { + url, err := parseRequestURL(rawURL) + if err != nil { + return nil, err + } + protoPath := extractProtoPath(url.Path) config := clientConfig{ + URL: url, Protocol: &protocolConnect{}, Procedure: protoPath, CompressionPools: make(map[string]*compressionPool), @@ -224,8 +260,26 @@ func (c *clientConfig) protobuf() Codec { func (c *clientConfig) newSpec(t StreamType) Spec { return Spec{ - StreamType: t, - Procedure: c.Procedure, - IsClient: true, + StreamType: t, + Procedure: c.Procedure, + Schema: c.Schema, + IsClient: true, + IdempotencyLevel: c.IdempotencyLevel, + } +} + +func parseRequestURL(rawURL string) (*url.URL, *Error) { + url, err := url.ParseRequestURI(rawURL) + if err == nil { + return url, nil + } + if !strings.Contains(rawURL, "://") { + // URL doesn't have a scheme, so the user is likely accustomed to + // grpc-go's APIs. + err = fmt.Errorf( + "URL %q missing scheme: use http:// or https:// (unlike grpc-go)", + rawURL, + ) } + return nil, NewError(CodeUnavailable, err) } diff --git a/vendor/github.com/bufbuild/connect-go/client_stream.go b/vendor/connectrpc.com/connect/client_stream.go similarity index 90% rename from vendor/github.com/bufbuild/connect-go/client_stream.go rename to vendor/connectrpc.com/connect/client_stream.go index c7557afa..886fe7c3 100644 --- a/vendor/github.com/bufbuild/connect-go/client_stream.go +++ b/vendor/connectrpc.com/connect/client_stream.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,7 +25,8 @@ import ( // It's returned from [Client].CallClientStream, but doesn't currently have an // exported constructor function. type ClientStreamForClient[Req, Res any] struct { - conn StreamingClientConn + conn StreamingClientConn + initializer maybeInitializer // Error from client construction. If non-nil, return for all calls. err error } @@ -42,6 +43,9 @@ func (c *ClientStreamForClient[_, _]) Peer() Peer { // RequestHeader returns the request headers. Headers are sent to the server with the // first call to Send. +// +// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. func (c *ClientStreamForClient[Req, Res]) RequestHeader() http.Header { if c.err != nil { return http.Header{} @@ -75,7 +79,7 @@ func (c *ClientStreamForClient[Req, Res]) CloseAndReceive() (*Response[Res], err _ = c.conn.CloseResponse() return nil, err } - response, err := receiveUnaryResponse[Res](c.conn) + response, err := receiveUnaryResponse[Res](c.conn, c.initializer) if err != nil { _ = c.conn.CloseResponse() return nil, err @@ -94,8 +98,9 @@ func (c *ClientStreamForClient[Req, Res]) Conn() (StreamingClientConn, error) { // It's returned from [Client].CallServerStream, but doesn't currently have an // exported constructor function. type ServerStreamForClient[Res any] struct { - conn StreamingClientConn - msg *Res + conn StreamingClientConn + initializer maybeInitializer + msg *Res // Error from client construction. If non-nil, return for all calls. constructErr error // Error from conn.Receive(). @@ -112,6 +117,10 @@ func (s *ServerStreamForClient[Res]) Receive() bool { return false } s.msg = new(Res) + if err := s.initializer.maybe(s.conn.Spec(), s.msg); err != nil { + s.receiveErr = err + return false + } s.receiveErr = s.conn.Receive(s.msg) return s.receiveErr == nil } @@ -172,7 +181,8 @@ func (s *ServerStreamForClient[Res]) Conn() (StreamingClientConn, error) { // It's returned from [Client].CallBidiStream, but doesn't currently have an // exported constructor function. type BidiStreamForClient[Req, Res any] struct { - conn StreamingClientConn + conn StreamingClientConn + initializer maybeInitializer // Error from client construction. If non-nil, return for all calls. err error } @@ -189,6 +199,9 @@ func (b *BidiStreamForClient[_, _]) Peer() Peer { // RequestHeader returns the request headers. Headers are sent with the first // call to Send. +// +// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. func (b *BidiStreamForClient[Req, Res]) RequestHeader() http.Header { if b.err != nil { return http.Header{} @@ -228,6 +241,9 @@ func (b *BidiStreamForClient[Req, Res]) Receive() (*Res, error) { return nil, b.err } var msg Res + if err := b.initializer.maybe(b.conn.Spec(), &msg); err != nil { + return nil, err + } if err := b.conn.Receive(&msg); err != nil { return nil, err } diff --git a/vendor/github.com/bufbuild/connect-go/code.go b/vendor/connectrpc.com/connect/code.go similarity index 98% rename from vendor/github.com/bufbuild/connect-go/code.go rename to vendor/connectrpc.com/connect/code.go index 557c3805..126fae43 100644 --- a/vendor/github.com/bufbuild/connect-go/code.go +++ b/vendor/connectrpc.com/connect/code.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ import ( // See the [Connect protocol specification] for detailed descriptions of each // code and example usage. // -// [Connect protocol specification]: https://connect.build/docs/protocol +// [Connect protocol specification]: https://connectrpc.com/docs/protocol type Code uint32 const ( diff --git a/vendor/connectrpc.com/connect/codec.go b/vendor/connectrpc.com/connect/codec.go new file mode 100644 index 00000000..4e7c3ba9 --- /dev/null +++ b/vendor/connectrpc.com/connect/codec.go @@ -0,0 +1,259 @@ +// Copyright 2021-2024 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/runtime/protoiface" +) + +const ( + codecNameProto = "proto" + codecNameJSON = "json" + codecNameJSONCharsetUTF8 = codecNameJSON + "; charset=utf-8" +) + +// Codec marshals structs (typically generated from a schema) to and from bytes. +type Codec interface { + // Name returns the name of the Codec. + // + // This may be used as part of the Content-Type within HTTP. For example, + // with gRPC this is the content subtype, so "application/grpc+proto" will + // map to the Codec with name "proto". + // + // Names must not be empty. + Name() string + // Marshal marshals the given message. + // + // Marshal may expect a specific type of message, and will error if this type + // is not given. + Marshal(any) ([]byte, error) + // Unmarshal unmarshals the given message. + // + // Unmarshal may expect a specific type of message, and will error if this + // type is not given. + Unmarshal([]byte, any) error +} + +// marshalAppender is an extension to Codec for appending to a byte slice. +type marshalAppender interface { + Codec + + // MarshalAppend marshals the given message and appends it to the given + // byte slice. + // + // MarshalAppend may expect a specific type of message, and will error if + // this type is not given. + MarshalAppend([]byte, any) ([]byte, error) +} + +// stableCodec is an extension to Codec for serializing with stable output. +type stableCodec interface { + Codec + + // MarshalStable marshals the given message with stable field ordering. + // + // MarshalStable should return the same output for a given input. Although + // it is not guaranteed to be canonicalized, the marshalling routine for + // MarshalStable will opt for the most normalized output available for a + // given serialization. + // + // For practical reasons, it is possible for MarshalStable to return two + // different results for two inputs considered to be "equal" in their own + // domain, and it may change in the future with codec updates, but for + // any given concrete value and any given version, it should return the + // same output. + MarshalStable(any) ([]byte, error) + + // IsBinary returns true if the marshalled data is binary for this codec. + // + // If this function returns false, the data returned from Marshal and + // MarshalStable are considered valid text and may be used in contexts + // where text is expected. + IsBinary() bool +} + +type protoBinaryCodec struct{} + +var _ Codec = (*protoBinaryCodec)(nil) + +func (c *protoBinaryCodec) Name() string { return codecNameProto } + +func (c *protoBinaryCodec) Marshal(message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + return proto.Marshal(protoMessage) +} + +func (c *protoBinaryCodec) MarshalAppend(dst []byte, message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + return proto.MarshalOptions{}.MarshalAppend(dst, protoMessage) +} + +func (c *protoBinaryCodec) Unmarshal(data []byte, message any) error { + protoMessage, ok := message.(proto.Message) + if !ok { + return errNotProto(message) + } + err := proto.Unmarshal(data, protoMessage) + if err != nil { + return fmt.Errorf("unmarshal into %T: %w", message, err) + } + return nil +} + +func (c *protoBinaryCodec) MarshalStable(message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + // protobuf does not offer a canonical output today, so this format is not + // guaranteed to match deterministic output from other protobuf libraries. + // In addition, unknown fields may cause inconsistent output for otherwise + // equal messages. + // https://github.com/golang/protobuf/issues/1121 + options := proto.MarshalOptions{Deterministic: true} + return options.Marshal(protoMessage) +} + +func (c *protoBinaryCodec) IsBinary() bool { + return true +} + +type protoJSONCodec struct { + name string +} + +var _ Codec = (*protoJSONCodec)(nil) + +func (c *protoJSONCodec) Name() string { return c.name } + +func (c *protoJSONCodec) Marshal(message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + return protojson.MarshalOptions{}.Marshal(protoMessage) +} + +func (c *protoJSONCodec) MarshalAppend(dst []byte, message any) ([]byte, error) { + protoMessage, ok := message.(proto.Message) + if !ok { + return nil, errNotProto(message) + } + return protojson.MarshalOptions{}.MarshalAppend(dst, protoMessage) +} + +func (c *protoJSONCodec) Unmarshal(binary []byte, message any) error { + protoMessage, ok := message.(proto.Message) + if !ok { + return errNotProto(message) + } + if len(binary) == 0 { + return errors.New("zero-length payload is not a valid JSON object") + } + // Discard unknown fields so clients and servers aren't forced to always use + // exactly the same version of the schema. + options := protojson.UnmarshalOptions{DiscardUnknown: true} + err := options.Unmarshal(binary, protoMessage) + if err != nil { + return fmt.Errorf("unmarshal into %T: %w", message, err) + } + return nil +} + +func (c *protoJSONCodec) MarshalStable(message any) ([]byte, error) { + // protojson does not offer a "deterministic" field ordering, but fields + // are still ordered consistently by their index. However, protojson can + // output inconsistent whitespace for some reason, therefore it is + // suggested to use a formatter to ensure consistent formatting. + // https://github.com/golang/protobuf/issues/1373 + messageJSON, err := c.Marshal(message) + if err != nil { + return nil, err + } + compactedJSON := bytes.NewBuffer(messageJSON[:0]) + if err = json.Compact(compactedJSON, messageJSON); err != nil { + return nil, err + } + return compactedJSON.Bytes(), nil +} + +func (c *protoJSONCodec) IsBinary() bool { + return false +} + +// readOnlyCodecs is a read-only interface to a map of named codecs. +type readOnlyCodecs interface { + // Get gets the Codec with the given name. + Get(string) Codec + // Protobuf gets the user-supplied protobuf codec, falling back to the default + // implementation if necessary. + // + // This is helpful in the gRPC protocol, where the wire protocol requires + // marshaling protobuf structs to binary even if the RPC procedures were + // generated from a different IDL. + Protobuf() Codec + // Names returns a copy of the registered codec names. The returned slice is + // safe for the caller to mutate. + Names() []string +} + +func newReadOnlyCodecs(nameToCodec map[string]Codec) readOnlyCodecs { + return &codecMap{ + nameToCodec: nameToCodec, + } +} + +type codecMap struct { + nameToCodec map[string]Codec +} + +func (m *codecMap) Get(name string) Codec { + return m.nameToCodec[name] +} + +func (m *codecMap) Protobuf() Codec { + if pb, ok := m.nameToCodec[codecNameProto]; ok { + return pb + } + return &protoBinaryCodec{} +} + +func (m *codecMap) Names() []string { + names := make([]string, 0, len(m.nameToCodec)) + for name := range m.nameToCodec { + names = append(names, name) + } + return names +} + +func errNotProto(message any) error { + if _, ok := message.(protoiface.MessageV1); ok { + return fmt.Errorf("%T uses github.com/golang/protobuf, but connect-go only supports google.golang.org/protobuf: see https://go.dev/blog/protobuf-apiv2", message) + } + return fmt.Errorf("%T doesn't implement proto.Message", message) +} diff --git a/vendor/github.com/bufbuild/connect-go/compression.go b/vendor/connectrpc.com/connect/compression.go similarity index 94% rename from vendor/github.com/bufbuild/connect-go/compression.go rename to vendor/connectrpc.com/connect/compression.go index 4e7c23a5..ee43412b 100644 --- a/vendor/github.com/bufbuild/connect-go/compression.go +++ b/vendor/connectrpc.com/connect/compression.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -65,6 +65,9 @@ func newCompressionPool( newDecompressor func() Decompressor, newCompressor func() Compressor, ) *compressionPool { + if newDecompressor == nil && newCompressor == nil { + return nil + } return &compressionPool{ decompressors: sync.Pool{ New: func() any { return newDecompressor() }, @@ -87,6 +90,10 @@ func (c *compressionPool) Decompress(dst *bytes.Buffer, src *bytes.Buffer, readM bytesRead, err := dst.ReadFrom(reader) if err != nil { _ = c.putDecompressor(decompressor) + err = wrapIfContextError(err) + if connectErr, ok := asError(err); ok { + return connectErr + } return errorf(CodeInvalidArgument, "decompress: %w", err) } if readMaxBytes > 0 && bytesRead > readMaxBytes { @@ -108,8 +115,12 @@ func (c *compressionPool) Compress(dst *bytes.Buffer, src *bytes.Buffer) *Error if err != nil { return errorf(CodeUnknown, "get compressor: %w", err) } - if _, err := io.Copy(compressor, src); err != nil { + if _, err := src.WriteTo(compressor); err != nil { _ = c.putCompressor(compressor) + err = wrapIfContextError(err) + if connectErr, ok := asError(err); ok { + return connectErr + } return errorf(CodeInternal, "compress: %w", err) } if err := c.putCompressor(compressor); err != nil { diff --git a/vendor/github.com/bufbuild/connect-go/connect.go b/vendor/connectrpc.com/connect/connect.go similarity index 57% rename from vendor/github.com/bufbuild/connect-go/connect.go rename to vendor/connectrpc.com/connect/connect.go index 6734cb28..ea3fef5b 100644 --- a/vendor/github.com/bufbuild/connect-go/connect.go +++ b/vendor/connectrpc.com/connect/connect.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,25 +20,28 @@ // isolation. Walkthroughs, FAQs, and other narrative docs are available on the // [Connect website], and there's a working [demonstration service] on Github. // -// [Connect website]: https://connect.build -// [demonstration service]: https://github.com/bufbuild/connect-demo +// [Connect website]: https://connectrpc.com +// [demonstration service]: https://github.com/connectrpc/examples-go package connect import ( "errors" + "fmt" "io" "net/http" "net/url" ) // Version is the semantic version of the connect module. -const Version = "1.4.1" +const Version = "1.16.2" // These constants are used in compile-time handshakes with connect's generated // code. const ( - IsAtLeastVersion0_0_1 = true - IsAtLeastVersion0_1_0 = true + IsAtLeastVersion0_0_1 = true + IsAtLeastVersion0_1_0 = true + IsAtLeastVersion1_7_0 = true + IsAtLeastVersion1_13_0 = true ) // StreamType describes whether the client, server, neither, or both is @@ -47,11 +50,25 @@ type StreamType uint8 const ( StreamTypeUnary StreamType = 0b00 - StreamTypeClient = 0b01 - StreamTypeServer = 0b10 + StreamTypeClient StreamType = 0b01 + StreamTypeServer StreamType = 0b10 StreamTypeBidi = StreamTypeClient | StreamTypeServer ) +func (s StreamType) String() string { + switch s { + case StreamTypeUnary: + return "unary" + case StreamTypeClient: + return "client" + case StreamTypeServer: + return "server" + case StreamTypeBidi: + return "bidi" + } + return fmt.Sprintf("stream_%d", s) +} + // StreamingHandlerConn is the server's view of a bidirectional message // exchange. Interceptors for streaming RPCs may wrap StreamingHandlerConns. // @@ -62,6 +79,10 @@ const ( // Receive returns an error wrapping [io.EOF]. Handlers should check for this // using the standard library's [errors.Is]. // +// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for +// use by the gRPC and Connect protocols: applications may read them but +// shouldn't write them. +// // StreamingHandlerConn implementations provided by this module guarantee that // all returned errors can be cast to [*Error] using the standard library's // [errors.As]. @@ -91,6 +112,10 @@ type StreamingHandlerConn interface { // return an error wrapping [io.EOF]; clients may then call Receive to unmarshal // the error. // +// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for +// use by the gRPC and Connect protocols: applications may read them but +// shouldn't write them. +// // StreamingClientConn implementations provided by this module guarantee that // all returned errors can be cast to [*Error] using the standard library's // [errors.As]. @@ -126,6 +151,7 @@ type Request[T any] struct { spec Spec peer Peer header http.Header + method string } // NewRequest wraps a generated request message. @@ -153,7 +179,9 @@ func (r *Request[_]) Peer() Peer { return r.peer } -// Header returns the HTTP headers for this request. +// Header returns the HTTP headers for this request. Headers beginning with +// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC +// protocols: applications may read them but shouldn't write them. func (r *Request[_]) Header() http.Header { if r.header == nil { r.header = make(http.Header) @@ -161,12 +189,35 @@ func (r *Request[_]) Header() http.Header { return r.header } +// HTTPMethod returns the HTTP method for this request. This is nearly always +// POST, but side-effect-free unary RPCs could be made via a GET. +// +// On a newly created request, via NewRequest, this will return the empty +// string until the actual request is actually sent and the HTTP method +// determined. This means that client interceptor functions will see the +// empty string until *after* they delegate to the handler they wrapped. It +// is even possible for this to return the empty string after such delegation, +// if the request was never actually sent to the server (and thus no +// determination ever made about the HTTP method). +func (r *Request[_]) HTTPMethod() string { + return r.method +} + // internalOnly implements AnyRequest. func (r *Request[_]) internalOnly() {} +// setRequestMethod sets the request method to the given value. +func (r *Request[_]) setRequestMethod(method string) { + r.method = method +} + // AnyRequest is the common method set of every [Request], regardless of type // parameter. It's used in unary interceptors. // +// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for +// use by the gRPC and Connect protocols: applications may read them but +// shouldn't write them. +// // To preserve our ability to add methods to this interface without breaking // backward compatibility, only types defined in this package can implement // AnyRequest. @@ -175,8 +226,10 @@ type AnyRequest interface { Spec() Spec Peer() Peer Header() http.Header + HTTPMethod() string internalOnly() + setRequestMethod(string) } // Response is a wrapper around a generated response message. It provides @@ -205,7 +258,9 @@ func (r *Response[_]) Any() any { return r.Msg } -// Header returns the HTTP headers for this response. +// Header returns the HTTP headers for this response. Headers beginning with +// "Connect-" and "Grpc-" are reserved for use by the Connect and gRPC +// protocols: applications may read them but shouldn't write them. func (r *Response[_]) Header() http.Header { if r.header == nil { r.header = make(http.Header) @@ -216,6 +271,10 @@ func (r *Response[_]) Header() http.Header { // Trailer returns the trailers for this response. Depending on the underlying // RPC protocol, trailers may be sent as HTTP trailers or a protocol-specific // block of in-body metadata. +// +// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols: applications may read them but shouldn't write +// them. func (r *Response[_]) Trailer() http.Header { if r.trailer == nil { r.trailer = make(http.Header) @@ -229,6 +288,10 @@ func (r *Response[_]) internalOnly() {} // AnyResponse is the common method set of every [Response], regardless of type // parameter. It's used in unary interceptors. // +// Headers and trailers beginning with "Connect-" and "Grpc-" are reserved for +// use by the gRPC and Connect protocols: applications may read them but +// shouldn't write them. +// // To preserve our ability to add methods to this interface without breaking // backward compatibility, only types defined in this package can implement // AnyResponse. @@ -247,10 +310,15 @@ type HTTPClient interface { } // Spec is a description of a client call or a handler invocation. +// +// If you're using Protobuf, protoc-gen-connect-go generates a constant for the +// fully-qualified Procedure corresponding to each RPC in your schema. type Spec struct { - StreamType StreamType - Procedure string // for example, "/acme.foo.v1.FooService/Bar" - IsClient bool // otherwise we're in a handler + StreamType StreamType + Schema any // for protobuf RPCs, a protoreflect.MethodDescriptor + Procedure string // for example, "/acme.foo.v1.FooService/Bar" + IsClient bool // otherwise we're in a handler + IdempotencyLevel IdempotencyLevel } // Peer describes the other party to an RPC. @@ -262,20 +330,23 @@ type Spec struct { // On both the client and the server, Protocol is the RPC protocol in use. // Currently, it's either [ProtocolConnect], [ProtocolGRPC], or // [ProtocolGRPCWeb], but additional protocols may be added in the future. +// +// Query contains the query parameters for the request. For the server, this +// will reflect the actual query parameters sent. For the client, it is unset. type Peer struct { Addr string Protocol string + Query url.Values // server-only } -func newPeerFromURL(urlString, protocol string) Peer { - peer := Peer{Protocol: protocol} - if u, err := url.Parse(urlString); err == nil { - peer.Addr = u.Host +func newPeerFromURL(url *url.URL, protocol string) Peer { + return Peer{ + Addr: url.Host, + Protocol: protocol, } - return peer } -// handlerConnCloser extends HandlerConn with a method for handlers to +// handlerConnCloser extends StreamingHandlerConn with a method for handlers to // terminate the message exchange (and optionally send an error to the client). type handlerConnCloser interface { StreamingHandlerConn @@ -283,26 +354,88 @@ type handlerConnCloser interface { Close(error) error } +// receiveConn represents the shared methods of both StreamingClientConn and StreamingHandlerConn +// that the below helper functions use for implementing the rules around a "unary" stream, that +// is expected to have exactly one message (or zero messages followed by a non-EOF error). +type receiveConn interface { + Spec() Spec + Receive(any) error +} + +// hasHTTPMethod is implemented by streaming connections that support HTTP methods other than +// POST. +type hasHTTPMethod interface { + getHTTPMethod() string +} + // receiveUnaryResponse unmarshals a message from a StreamingClientConn, then // envelopes the message and attaches headers and trailers. It attempts to // consume the response stream and isn't appropriate when receiving multiple // messages. -func receiveUnaryResponse[T any](conn StreamingClientConn) (*Response[T], error) { - var msg T - if err := conn.Receive(&msg); err != nil { +func receiveUnaryResponse[T any](conn StreamingClientConn, initializer maybeInitializer) (*Response[T], error) { + msg, err := receiveUnaryMessage[T](conn, initializer, "response") + if err != nil { return nil, err } - // In a well-formed stream, the response message may be followed by a block - // of in-stream trailers or HTTP trailers. To ensure that we receive the - // trailers, try to read another message from the stream. - if err := conn.Receive(new(T)); err == nil { - return nil, NewError(CodeUnknown, errors.New("unary stream has multiple messages")) - } else if err != nil && !errors.Is(err, io.EOF) { - return nil, NewError(CodeUnknown, err) - } return &Response[T]{ - Msg: &msg, + Msg: msg, header: conn.ResponseHeader(), trailer: conn.ResponseTrailer(), }, nil } + +// receiveUnaryRequest unmarshals a message from a StreamingClientConn, then +// envelopes the message and attaches headers and other request properties. It +// attempts to consume the request stream and isn't appropriate when receiving +// multiple messages. +func receiveUnaryRequest[T any](conn StreamingHandlerConn, initializer maybeInitializer) (*Request[T], error) { + msg, err := receiveUnaryMessage[T](conn, initializer, "request") + if err != nil { + return nil, err + } + method := http.MethodPost + if hasRequestMethod, ok := conn.(hasHTTPMethod); ok { + method = hasRequestMethod.getHTTPMethod() + } + return &Request[T]{ + Msg: msg, + spec: conn.Spec(), + peer: conn.Peer(), + header: conn.RequestHeader(), + method: method, + }, nil +} + +func receiveUnaryMessage[T any](conn receiveConn, initializer maybeInitializer, what string) (*T, error) { + var msg T + if err := initializer.maybe(conn.Spec(), &msg); err != nil { + return nil, err + } + // Possibly counter-intuitive, but the gRPC specs about error codes state that both clients + // and servers should return "unimplemented" when they encounter a cardinality violation: where + // the number of messages in the stream is wrong. Search for "cardinality violation" in the + // following docs: + // https://grpc.github.io/grpc/core/md_doc_statuscodes.html + if err := conn.Receive(&msg); err != nil { + if errors.Is(err, io.EOF) { + err = NewError(CodeUnimplemented, fmt.Errorf("unary %s has zero messages", what)) + } + return nil, err + } + // In a well-formed stream, the one message must be the only content in the body. + // To verify that it is well-formed, try to read another message from the stream. + // TODO: optimise this second receive: ideally do it w/out allocation, w/out + // fully reading next message (if one is present), and w/out trying to + // actually unmarshal the bytes) + var msg2 T + if err := initializer.maybe(conn.Spec(), &msg2); err != nil { + return nil, err + } + if err := conn.Receive(&msg2); !errors.Is(err, io.EOF) { + if err == nil { + err = NewError(CodeUnimplemented, fmt.Errorf("unary %s has multiple messages", what)) + } + return nil, err + } + return &msg, nil +} diff --git a/vendor/connectrpc.com/connect/duplex_http_call.go b/vendor/connectrpc.com/connect/duplex_http_call.go new file mode 100644 index 00000000..a1f8f36a --- /dev/null +++ b/vendor/connectrpc.com/connect/duplex_http_call.go @@ -0,0 +1,468 @@ +// Copyright 2021-2024 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "sync" + "sync/atomic" +) + +// duplexHTTPCall is a full-duplex stream between the client and server. The +// request body is the stream from client to server, and the response body is +// the reverse. +// +// Be warned: we need to use some lesser-known APIs to do this with net/http. +type duplexHTTPCall struct { + ctx context.Context + httpClient HTTPClient + streamType StreamType + onRequestSend func(*http.Request) + validateResponse func(*http.Response) *Error + + // io.Pipe is used to implement the request body for client streaming calls. + // If the request is unary, requestBodyWriter is nil. + requestBodyWriter *io.PipeWriter + + // requestSent ensures we only send the request once. + requestSent atomic.Bool + request *http.Request + + // responseReady is closed when the response is ready or when the request + // fails. Any error on request initialisation will be set on the + // responseErr. There's always a response if responseErr is nil. + responseReady chan struct{} + response *http.Response + responseErr error +} + +func newDuplexHTTPCall( + ctx context.Context, + httpClient HTTPClient, + url *url.URL, + spec Spec, + header http.Header, +) *duplexHTTPCall { + // ensure we make a copy of the url before we pass along to the + // Request. This ensures if a transport out of our control wants + // to mutate the req.URL, we don't feel the effects of it. + url = cloneURL(url) + + // This is mirroring what http.NewRequestContext did, but + // using an already parsed url.URL object, rather than a string + // and parsing it again. This is a bit funny with HTTP/1.1 + // explicitly, but this is logic copied over from + // NewRequestContext and doesn't effect the actual version + // being transmitted. + request := (&http.Request{ + Method: http.MethodPost, + URL: url, + Header: header, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Body: http.NoBody, + GetBody: getNoBody, + Host: url.Host, + }).WithContext(ctx) + return &duplexHTTPCall{ + ctx: ctx, + httpClient: httpClient, + streamType: spec.StreamType, + request: request, + responseReady: make(chan struct{}), + } +} + +// Send sends a message to the server. +func (d *duplexHTTPCall) Send(payload messagePayload) (int64, error) { + if d.streamType&StreamTypeClient == 0 { + return d.sendUnary(payload) + } + isFirst := d.requestSent.CompareAndSwap(false, true) + if isFirst { + // This is the first time we're sending a message to the server. + // We need to send the request headers and start the request. + pipeReader, pipeWriter := io.Pipe() + d.requestBodyWriter = pipeWriter + d.request.Body = pipeReader + d.request.GetBody = nil // GetBody not supported for client streaming + d.request.ContentLength = -1 + go d.makeRequest() // concurrent request + } + if err := d.ctx.Err(); err != nil { + return 0, wrapIfContextError(err) + } + if isFirst && payload.Len() == 0 { + // On first write a nil Send is used to send request headers. Avoid + // writing a zero-length payload to avoid superfluous errors with close. + return 0, nil + } + // It's safe to write to this side of the pipe while net/http concurrently + // reads from the other side. + bytesWritten, err := payload.WriteTo(d.requestBodyWriter) + if err != nil && errors.Is(err, io.ErrClosedPipe) { + // Signal that the stream is closed with the more-typical io.EOF instead of + // io.ErrClosedPipe. This makes it easier for protocol-specific wrappers to + // match grpc-go's behavior. + err = io.EOF + } + return bytesWritten, err +} + +func (d *duplexHTTPCall) sendUnary(payload messagePayload) (int64, error) { + // Unary messages are sent as a single HTTP request. We don't need to use a + // pipe for the request body and we don't need to send headers separately. + if !d.requestSent.CompareAndSwap(false, true) { + return 0, fmt.Errorf("request already sent") + } + payloadLength := int64(payload.Len()) + if payloadLength > 0 { + // Build the request body from the payload. + payloadBody := newPayloadCloser(payload) + d.request.Body = payloadBody + d.request.ContentLength = payloadLength + d.request.GetBody = func() (io.ReadCloser, error) { + if !payloadBody.Rewind() { + return nil, fmt.Errorf("payload cannot be retried") + } + return payloadBody, nil + } + // Release the payload ensuring that after Send returns the + // payload is safe to be reused. See [http.RoundTripper] for + // more details. + defer payloadBody.Release() + } + d.makeRequest() // synchronous request + if d.responseErr != nil { + // Check on response errors for context errors. Other errors are + // handled on read. + if err := d.ctx.Err(); err != nil { + return 0, wrapIfContextError(err) + } + } + return payloadLength, nil +} + +// CloseWrite closes the request body. Callers *must* call CloseWrite before Read when +// using HTTP/1.x. +func (d *duplexHTTPCall) CloseWrite() error { + // Even if Write was never called, we need to make an HTTP request. This + // ensures that we've sent any headers to the server and that we have an HTTP + // response to read from. + if d.requestSent.CompareAndSwap(false, true) { + go d.makeRequest() + return nil + } + // The user calls CloseWrite to indicate that they're done sending data. It's + // safe to close the write side of the pipe while net/http is reading from + // it. + // + // Because connect also supports some RPC types over HTTP/1.1, we need to be + // careful how we expose this method to users. HTTP/1.1 doesn't support + // bidirectional streaming - the write side of the stream (aka request body) + // must be closed before we start reading the response or we'll just block + // forever. To make sure users don't have to worry about this, the generated + // code for unary, client streaming, and server streaming RPCs must call + // CloseWrite automatically rather than requiring the user to do it. + if d.requestBodyWriter != nil { + return d.requestBodyWriter.Close() + } + return d.request.Body.Close() +} + +// Header returns the HTTP request headers. +func (d *duplexHTTPCall) Header() http.Header { + return d.request.Header +} + +// Trailer returns the HTTP request trailers. +func (d *duplexHTTPCall) Trailer() http.Header { + return d.request.Trailer +} + +// URL returns the URL for the request. +func (d *duplexHTTPCall) URL() *url.URL { + return d.request.URL +} + +// Method returns the HTTP method for the request (GET or POST). +func (d *duplexHTTPCall) Method() string { + return d.request.Method +} + +// SetMethod changes the method of the request before it is sent. +func (d *duplexHTTPCall) SetMethod(method string) { + d.request.Method = method +} + +// Read from the response body. Returns the first error passed to SetError. +func (d *duplexHTTPCall) Read(data []byte) (int, error) { + // First, we wait until we've gotten the response headers and established the + // server-to-client side of the stream. + if err := d.BlockUntilResponseReady(); err != nil { + // The stream is already closed or corrupted. + return 0, err + } + // Before we read, check if the context has been canceled. + if err := d.ctx.Err(); err != nil { + return 0, wrapIfContextError(err) + } + n, err := d.response.Body.Read(data) + if err != nil && !errors.Is(err, io.EOF) { + err = wrapIfContextDone(d.ctx, err) + err = wrapIfRSTError(err) + } + return n, err +} + +func (d *duplexHTTPCall) CloseRead() error { + _ = d.BlockUntilResponseReady() + if d.response == nil { + return nil + } + _, err := discard(d.response.Body) + closeErr := d.response.Body.Close() + if err == nil || + errors.Is(err, context.Canceled) || + errors.Is(err, context.DeadlineExceeded) { + err = closeErr + } + err = wrapIfContextDone(d.ctx, err) + return wrapIfRSTError(err) +} + +// ResponseStatusCode is the response's HTTP status code. +func (d *duplexHTTPCall) ResponseStatusCode() (int, error) { + if err := d.BlockUntilResponseReady(); err != nil { + return 0, err + } + return d.response.StatusCode, nil +} + +// ResponseHeader returns the response HTTP headers. +func (d *duplexHTTPCall) ResponseHeader() http.Header { + _ = d.BlockUntilResponseReady() + if d.response != nil { + return d.response.Header + } + return make(http.Header) +} + +// ResponseTrailer returns the response HTTP trailers. +func (d *duplexHTTPCall) ResponseTrailer() http.Header { + _ = d.BlockUntilResponseReady() + if d.response != nil { + return d.response.Trailer + } + return make(http.Header) +} + +// SetValidateResponse sets the response validation function. The function runs +// in a background goroutine. +func (d *duplexHTTPCall) SetValidateResponse(validate func(*http.Response) *Error) { + d.validateResponse = validate +} + +// BlockUntilResponseReady returns when the response is ready or reports an +// error from initializing the request. +func (d *duplexHTTPCall) BlockUntilResponseReady() error { + <-d.responseReady + return d.responseErr +} + +func (d *duplexHTTPCall) makeRequest() { + // This runs concurrently with Write and CloseWrite. Read and CloseRead wait + // on d.responseReady, so we can't race with them. + defer close(d.responseReady) + + // Promote the header Host to the request object. + if host := getHeaderCanonical(d.request.Header, headerHost); len(host) > 0 { + d.request.Host = host + } + if d.onRequestSend != nil { + d.onRequestSend(d.request) + } + // Once we send a message to the server, they send a message back and + // establish the receive side of the stream. + // On error, we close the request body using the Write side of the pipe. + // This ensures HTTP2 streams receive an io.EOF from the Read side of the + // pipe. Write's check for io.ErrClosedPipe and will convert this to io.EOF. + response, err := d.httpClient.Do(d.request) //nolint:bodyclose + if err != nil { + err = wrapIfContextError(err) + err = wrapIfLikelyH2CNotConfiguredError(d.request, err) + err = wrapIfLikelyWithGRPCNotUsedError(err) + err = wrapIfRSTError(err) + if _, ok := asError(err); !ok { + err = NewError(CodeUnavailable, err) + } + d.responseErr = err + _ = d.CloseWrite() + return + } + // We've got a response. We can now read from the response body. + // Closing the response body is delegated to the caller even on error. + d.response = response + if err := d.validateResponse(response); err != nil { + d.responseErr = err + _ = d.CloseWrite() + return + } + if (d.streamType&StreamTypeBidi) == StreamTypeBidi && response.ProtoMajor < 2 { + // If we somehow dialed an HTTP/1.x server, fail with an explicit message + // rather than returning a more cryptic error later on. + d.responseErr = errorf( + CodeUnimplemented, + "response from %v is HTTP/%d.%d: bidi streams require at least HTTP/2", + d.request.URL, + response.ProtoMajor, + response.ProtoMinor, + ) + _ = d.CloseWrite() + } +} + +// getNoBody is a GetBody function for http.NoBody. +func getNoBody() (io.ReadCloser, error) { + return http.NoBody, nil +} + +// messagePayload is a sized and seekable message payload. The interface is +// implemented by [*bytes.Reader] and *envelope. Reads must be non-blocking. +type messagePayload interface { + io.Reader + io.WriterTo + io.Seeker + Len() int +} + +// nopPayload is a message payload that does nothing. It's used to send headers +// to the server. +type nopPayload struct{} + +var _ messagePayload = nopPayload{} + +func (nopPayload) Read([]byte) (int, error) { + return 0, io.EOF +} +func (nopPayload) WriteTo(io.Writer) (int64, error) { + return 0, nil +} +func (nopPayload) Seek(int64, int) (int64, error) { + return 0, nil +} +func (nopPayload) Len() int { + return 0 +} + +// messageSender sends a message payload. The interface is implemented by +// [*duplexHTTPCall] and writeSender. +type messageSender interface { + Send(messagePayload) (int64, error) +} + +// writeSender is a sender that writes to an [io.Writer]. Useful for wrapping +// [http.ResponseWriter]. +type writeSender struct { + writer io.Writer +} + +var _ messageSender = writeSender{} + +func (w writeSender) Send(payload messagePayload) (int64, error) { + return payload.WriteTo(w.writer) +} + +// See: https://cs.opensource.google/go/go/+/refs/tags/go1.20.1:src/net/http/clone.go;l=22-33 +func cloneURL(oldURL *url.URL) *url.URL { + if oldURL == nil { + return nil + } + newURL := new(url.URL) + *newURL = *oldURL + if oldURL.User != nil { + newURL.User = new(url.Userinfo) + *newURL.User = *oldURL.User + } + return newURL +} + +// payloadCloser is an [io.ReadCloser] that wraps a messagePayload. It's used to +// implement the request body for unary calls. To safely reuse the buffer +// call Release after the response is received to ensure the payload is safe for +// reuse. +type payloadCloser struct { + mu sync.Mutex + payload messagePayload // nil after Release +} + +func newPayloadCloser(payload messagePayload) *payloadCloser { + return &payloadCloser{ + payload: payload, + } +} + +// Read implements [io.Reader]. +func (p *payloadCloser) Read(dst []byte) (readN int, err error) { + p.mu.Lock() + defer p.mu.Unlock() + if p.payload == nil { + return 0, io.EOF + } + return p.payload.Read(dst) +} + +// WriteTo implements [io.WriterTo]. +func (p *payloadCloser) WriteTo(dst io.Writer) (int64, error) { + p.mu.Lock() + defer p.mu.Unlock() + if p.payload == nil { + return 0, nil + } + return p.payload.WriteTo(dst) +} + +// Close implements [io.Closer]. +func (p *payloadCloser) Close() error { + return nil +} + +// Rewind rewinds the payload to the beginning. It returns false if the +// payload has been discarded from a previous call to Release. +func (p *payloadCloser) Rewind() bool { + p.mu.Lock() + defer p.mu.Unlock() + if p.payload == nil { + return false + } + if _, err := p.payload.Seek(0, io.SeekStart); err != nil { + return false + } + return true +} + +// Release discards the payload. After Release is called, the payload cannot be +// rewound and the payload is safe to reuse. +func (p *payloadCloser) Release() { + p.mu.Lock() + p.payload = nil + p.mu.Unlock() +} diff --git a/vendor/connectrpc.com/connect/envelope.go b/vendor/connectrpc.com/connect/envelope.go new file mode 100644 index 00000000..bc85c551 --- /dev/null +++ b/vendor/connectrpc.com/connect/envelope.go @@ -0,0 +1,374 @@ +// Copyright 2021-2024 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "bytes" + "context" + "encoding/binary" + "errors" + "io" +) + +// flagEnvelopeCompressed indicates that the data is compressed. It has the +// same meaning in the gRPC-Web, gRPC-HTTP2, and Connect protocols. +const flagEnvelopeCompressed = 0b00000001 + +var errSpecialEnvelope = errorf( + CodeUnknown, + "final message has protocol-specific flags: %w", + // User code checks for end of stream with errors.Is(err, io.EOF). + io.EOF, +) + +// envelope is a block of arbitrary bytes wrapped in gRPC and Connect's framing +// protocol. +// +// Each message is preceded by a 5-byte prefix. The first byte is a uint8 used +// as a set of bitwise flags, and the remainder is a uint32 indicating the +// message length. gRPC and Connect interpret the bitwise flags differently, so +// envelope leaves their interpretation up to the caller. +type envelope struct { + Data *bytes.Buffer + Flags uint8 + offset int64 +} + +var _ messagePayload = (*envelope)(nil) + +func (e *envelope) IsSet(flag uint8) bool { + return e.Flags&flag == flag +} + +// Read implements [io.Reader]. +func (e *envelope) Read(data []byte) (readN int, err error) { + if e.offset < 5 { + prefix := makeEnvelopePrefix(e.Flags, e.Data.Len()) + readN = copy(data, prefix[e.offset:]) + e.offset += int64(readN) + if e.offset < 5 { + return readN, nil + } + data = data[readN:] + } + n := copy(data, e.Data.Bytes()[e.offset-5:]) + e.offset += int64(n) + readN += n + if readN == 0 && e.offset == int64(e.Data.Len()+5) { + err = io.EOF + } + return readN, err +} + +// WriteTo implements [io.WriterTo]. +func (e *envelope) WriteTo(dst io.Writer) (wroteN int64, err error) { + if e.offset < 5 { + prefix := makeEnvelopePrefix(e.Flags, e.Data.Len()) + prefixN, err := dst.Write(prefix[e.offset:]) + e.offset += int64(prefixN) + wroteN += int64(prefixN) + if e.offset < 5 { + return wroteN, err + } + } + n, err := dst.Write(e.Data.Bytes()[e.offset-5:]) + e.offset += int64(n) + wroteN += int64(n) + return wroteN, err +} + +// Seek implements [io.Seeker]. Based on the implementation of [bytes.Reader]. +func (e *envelope) Seek(offset int64, whence int) (int64, error) { + var abs int64 + switch whence { + case io.SeekStart: + abs = offset + case io.SeekCurrent: + abs = e.offset + offset + case io.SeekEnd: + abs = int64(e.Data.Len()) + offset + default: + return 0, errors.New("connect.envelope.Seek: invalid whence") + } + if abs < 0 { + return 0, errors.New("connect.envelope.Seek: negative position") + } + e.offset = abs + return abs, nil +} + +// Len returns the number of bytes of the unread portion of the envelope. +func (e *envelope) Len() int { + if length := int(int64(e.Data.Len()) + 5 - e.offset); length > 0 { + return length + } + return 0 +} + +type envelopeWriter struct { + ctx context.Context //nolint:containedctx + sender messageSender + codec Codec + compressMinBytes int + compressionPool *compressionPool + bufferPool *bufferPool + sendMaxBytes int +} + +func (w *envelopeWriter) Marshal(message any) *Error { + if message == nil { + // Send no-op message to create the request and send headers. + payload := nopPayload{} + if _, err := w.sender.Send(payload); err != nil { + if connectErr, ok := asError(err); ok { + return connectErr + } + return NewError(CodeUnknown, err) + } + return nil + } + if appender, ok := w.codec.(marshalAppender); ok { + return w.marshalAppend(message, appender) + } + return w.marshal(message) +} + +// Write writes the enveloped message, compressing as necessary. It doesn't +// retain any references to the supplied envelope or its underlying data. +func (w *envelopeWriter) Write(env *envelope) *Error { + if env.IsSet(flagEnvelopeCompressed) || + w.compressionPool == nil || + env.Data.Len() < w.compressMinBytes { + if w.sendMaxBytes > 0 && env.Data.Len() > w.sendMaxBytes { + return errorf(CodeResourceExhausted, "message size %d exceeds sendMaxBytes %d", env.Data.Len(), w.sendMaxBytes) + } + return w.write(env) + } + data := w.bufferPool.Get() + defer w.bufferPool.Put(data) + if err := w.compressionPool.Compress(data, env.Data); err != nil { + return err + } + if w.sendMaxBytes > 0 && data.Len() > w.sendMaxBytes { + return errorf(CodeResourceExhausted, "compressed message size %d exceeds sendMaxBytes %d", data.Len(), w.sendMaxBytes) + } + return w.write(&envelope{ + Data: data, + Flags: env.Flags | flagEnvelopeCompressed, + }) +} + +func (w *envelopeWriter) marshalAppend(message any, codec marshalAppender) *Error { + // Codec supports MarshalAppend; try to re-use a []byte from the pool. + buffer := w.bufferPool.Get() + defer w.bufferPool.Put(buffer) + raw, err := codec.MarshalAppend(buffer.Bytes(), message) + if err != nil { + return errorf(CodeInternal, "marshal message: %w", err) + } + if cap(raw) > buffer.Cap() { + // The buffer from the pool was too small, so MarshalAppend grew the slice. + // Pessimistically assume that the too-small buffer is insufficient for the + // application workload, so there's no point in keeping it in the pool. + // Instead, replace it with the larger, newly-allocated slice. This + // allocates, but it's a small, constant-size allocation. + *buffer = *bytes.NewBuffer(raw) + } else { + // MarshalAppend didn't allocate, but we need to fix the internal state of + // the buffer. Compared to replacing the buffer (as above), buffer.Write + // copies but avoids allocating. + buffer.Write(raw) + } + envelope := &envelope{Data: buffer} + return w.Write(envelope) +} + +func (w *envelopeWriter) marshal(message any) *Error { + // Codec doesn't support MarshalAppend; let Marshal allocate a []byte. + raw, err := w.codec.Marshal(message) + if err != nil { + return errorf(CodeInternal, "marshal message: %w", err) + } + buffer := bytes.NewBuffer(raw) + // Put our new []byte into the pool for later reuse. + defer w.bufferPool.Put(buffer) + envelope := &envelope{Data: buffer} + return w.Write(envelope) +} + +func (w *envelopeWriter) write(env *envelope) *Error { + if _, err := w.sender.Send(env); err != nil { + err = wrapIfContextDone(w.ctx, err) + if connectErr, ok := asError(err); ok { + return connectErr + } + return errorf(CodeUnknown, "write envelope: %w", err) + } + return nil +} + +type envelopeReader struct { + ctx context.Context //nolint:containedctx + reader io.Reader + bytesRead int64 // detect trailers-only gRPC responses + codec Codec + last envelope + compressionPool *compressionPool + bufferPool *bufferPool + readMaxBytes int +} + +func (r *envelopeReader) Unmarshal(message any) *Error { + buffer := r.bufferPool.Get() + var dontRelease *bytes.Buffer + defer func() { + if buffer != dontRelease { + r.bufferPool.Put(buffer) + } + }() + + env := &envelope{Data: buffer} + err := r.Read(env) + switch { + case err == nil && env.IsSet(flagEnvelopeCompressed) && r.compressionPool == nil: + return errorf( + CodeInternal, + "protocol error: sent compressed message without compression support", + ) + case err == nil && + (env.Flags == 0 || env.Flags == flagEnvelopeCompressed) && + env.Data.Len() == 0: + // This is a standard message (because none of the top 7 bits are set) and + // there's no data, so the zero value of the message is correct. + return nil + case err != nil && errors.Is(err, io.EOF): + // The stream has ended. Propagate the EOF to the caller. + return err + case err != nil: + // Something's wrong. + return err + } + + data := env.Data + if data.Len() > 0 && env.IsSet(flagEnvelopeCompressed) { + decompressed := r.bufferPool.Get() + defer func() { + if decompressed != dontRelease { + r.bufferPool.Put(decompressed) + } + }() + if err := r.compressionPool.Decompress(decompressed, data, int64(r.readMaxBytes)); err != nil { + return err + } + data = decompressed + } + + if env.Flags != 0 && env.Flags != flagEnvelopeCompressed { + // Drain the rest of the stream to ensure there is no extra data. + numBytes, err := discard(r.reader) + r.bytesRead += numBytes + if err != nil { + err = wrapIfContextError(err) + if connErr, ok := asError(err); ok { + return connErr + } + return errorf(CodeInternal, "corrupt response: I/O error after end-stream message: %w", err) + } else if numBytes > 0 { + return errorf(CodeInternal, "corrupt response: %d extra bytes after end of stream", numBytes) + } + // One of the protocol-specific flags are set, so this is the end of the + // stream. Save the message for protocol-specific code to process and + // return a sentinel error. We alias the buffer with dontRelease as a + // way of marking it so above defers don't release it to the pool. + r.last = envelope{ + Data: data, + Flags: env.Flags, + } + dontRelease = data + return errSpecialEnvelope + } + + if err := r.codec.Unmarshal(data.Bytes(), message); err != nil { + return errorf(CodeInvalidArgument, "unmarshal message: %w", err) + } + return nil +} + +func (r *envelopeReader) Read(env *envelope) *Error { + prefixes := [5]byte{} + // io.ReadFull reads the number of bytes requested, or returns an error. + // io.EOF will only be returned if no bytes were read. + n, err := io.ReadFull(r.reader, prefixes[:]) + r.bytesRead += int64(n) + if err != nil { + if errors.Is(err, io.EOF) { + // The stream ended cleanly. That's expected, but we need to propagate an EOF + // to the user so that they know that the stream has ended. We shouldn't + // add any alarming text about protocol errors, though. + return NewError(CodeUnknown, err) + } + err = wrapIfMaxBytesError(err, "read 5 byte message prefix") + err = wrapIfContextDone(r.ctx, err) + if connectErr, ok := asError(err); ok { + return connectErr + } + // Something else has gone wrong - the stream didn't end cleanly. + return errorf( + CodeInvalidArgument, + "protocol error: incomplete envelope: %w", err, + ) + } + size := int64(binary.BigEndian.Uint32(prefixes[1:5])) + if r.readMaxBytes > 0 && size > int64(r.readMaxBytes) { + n, err := io.CopyN(io.Discard, r.reader, size) + r.bytesRead += n + if err != nil && !errors.Is(err, io.EOF) { + return errorf(CodeResourceExhausted, "message is larger than configured max %d - unable to determine message size: %w", r.readMaxBytes, err) + } + return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", size, r.readMaxBytes) + } + // We've read the prefix, so we know how many bytes to expect. + // CopyN will return an error if it doesn't read the requested + // number of bytes. + readN, err := io.CopyN(env.Data, r.reader, size) + r.bytesRead += readN + if err != nil { + if errors.Is(err, io.EOF) { + // We've gotten fewer bytes than we expected, so the stream has ended + // unexpectedly. + return errorf( + CodeInvalidArgument, + "protocol error: promised %d bytes in enveloped message, got %d bytes", + size, + readN, + ) + } + err = wrapIfMaxBytesError(err, "read %d byte message", size) + err = wrapIfContextDone(r.ctx, err) + if connectErr, ok := asError(err); ok { + return connectErr + } + return errorf(CodeUnknown, "read enveloped message: %w", err) + } + env.Flags = prefixes[0] + return nil +} + +func makeEnvelopePrefix(flags uint8, size int) [5]byte { + prefix := [5]byte{} + prefix[0] = flags + binary.BigEndian.PutUint32(prefix[1:5], uint32(size)) + return prefix +} diff --git a/vendor/github.com/bufbuild/connect-go/error.go b/vendor/connectrpc.com/connect/error.go similarity index 69% rename from vendor/github.com/bufbuild/connect-go/error.go rename to vendor/connectrpc.com/connect/error.go index a4cd72b9..f461d1bb 100644 --- a/vendor/github.com/bufbuild/connect-go/error.go +++ b/vendor/connectrpc.com/connect/error.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "net/url" + "os" "strings" "google.golang.org/protobuf/proto" @@ -27,10 +28,18 @@ import ( ) const ( - commonErrorsURL = "https://connect.build/docs/go/common-errors" + commonErrorsURL = "https://connectrpc.com/docs/go/common-errors" defaultAnyResolverPrefix = "type.googleapis.com/" ) +var ( + // errNotModified signals Connect-protocol responses to GET requests to use the + // 304 Not Modified HTTP error code. + errNotModified = errors.New("not modified") + // errNotModifiedClient wraps ErrNotModified for use client-side. + errNotModifiedClient = fmt.Errorf("HTTP 304: %w", errNotModified) +) + // An ErrorDetail is a self-describing Protobuf message attached to an [*Error]. // Error details are sent over the network to clients, which can then work with // strongly-typed data rather than trying to parse a complex error message. For @@ -40,8 +49,9 @@ const ( // The [google.golang.org/genproto/googleapis/rpc/errdetails] package contains a // variety of Protobuf messages commonly used as error details. type ErrorDetail struct { - pb *anypb.Any - wireJSON string // preserve human-readable JSON + pbAny *anypb.Any + pbInner proto.Message // if nil, must be extracted from pbAny + wireJSON string // preserve human-readable JSON } // NewErrorDetail constructs a new error detail. If msg is an *[anypb.Any] then @@ -50,13 +60,13 @@ type ErrorDetail struct { func NewErrorDetail(msg proto.Message) (*ErrorDetail, error) { // If it's already an Any, don't wrap it inside another. if pb, ok := msg.(*anypb.Any); ok { - return &ErrorDetail{pb: pb}, nil + return &ErrorDetail{pbAny: pb}, nil } pb, err := anypb.New(msg) if err != nil { return nil, err } - return &ErrorDetail{pb: pb}, nil + return &ErrorDetail{pbAny: pb, pbInner: msg}, nil } // Type is the fully-qualified name of the detail's Protobuf message (for @@ -66,17 +76,17 @@ func (d *ErrorDetail) Type() string { // than plain type names, but there aren't any descriptor registries // deployed. With the current state of the `Any` code, it's not possible to // build a useful type registry either. To hide this from users, we should - // trim the static hostname that `Any` adds to the type name. + // trim the URL prefix is added to the type name. // // If we ever want to support remote registries, we can add an explicit // `TypeURL` method. - return strings.TrimPrefix(d.pb.TypeUrl, defaultAnyResolverPrefix) + return typeNameFromURL(d.pbAny.GetTypeUrl()) } // Bytes returns a copy of the Protobuf-serialized detail. func (d *ErrorDetail) Bytes() []byte { - out := make([]byte, len(d.pb.Value)) - copy(out, d.pb.Value) + out := make([]byte, len(d.pbAny.GetValue())) + copy(out, d.pbAny.GetValue()) return out } @@ -84,7 +94,12 @@ func (d *ErrorDetail) Bytes() []byte { // Detail into a strongly-typed message. Typically, clients use Go type // assertions to cast from the proto.Message interface to concrete types. func (d *ErrorDetail) Value() (proto.Message, error) { - return d.pb.UnmarshalNew() + if d.pbInner != nil { + // We clone it so that if the caller mutates the returned value, + // they don't inadvertently corrupt this error detail value. + return proto.Clone(d.pbInner), nil + } + return d.pbAny.UnmarshalNew() } // An Error captures four key pieces of information: a [Code], an underlying Go @@ -104,7 +119,7 @@ func (d *ErrorDetail) Value() (proto.Message, error) { // They're a clearer and more performant alternative to HTTP header // microformats. See [the documentation on errors] for more details. // -// [the documentation on errors]: https://connect.build/docs/go/errors +// [the documentation on errors]: https://connectrpc.com/docs/go/errors type Error struct { code Code err error @@ -118,12 +133,35 @@ func NewError(c Code, underlying error) *Error { return &Error{code: c, err: underlying} } +// NewWireError is similar to [NewError], but the resulting *Error returns true +// when tested with [IsWireError]. +// +// This is useful for clients trying to propagate partial failures from +// streaming RPCs. Often, these RPCs include error information in their +// response messages (for example, [gRPC server reflection] and +// OpenTelemetry's [OTLP]). Clients propagating these errors up the stack +// should use NewWireError to clarify that the error code, message, and details +// (if any) were explicitly sent by the server rather than inferred from a +// lower-level networking error or timeout. +// +// [gRPC server reflection]: https://github.com/grpc/grpc/blob/v1.49.2/src/proto/grpc/reflection/v1alpha/reflection.proto#L132-L136 +// [OTLP]: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#partial-success +func NewWireError(c Code, underlying error) *Error { + err := NewError(c, underlying) + err.wireErr = true + return err +} + // IsWireError checks whether the error was returned by the server, as opposed // to being synthesized by the client. // // Clients may find this useful when deciding how to propagate errors. For // example, an RPC-to-HTTP proxy might expose a server-sent CodeUnknown as an // HTTP 500 but a client-synthesized CodeUnknown as a 503. +// +// Handlers will strip [Error.Meta] headers propagated from wire errors to avoid +// leaking response headers. To propagate headers recreate the error as a +// non-wire error. func IsWireError(err error) bool { se := new(Error) if !errors.As(err, &se) { @@ -132,6 +170,24 @@ func IsWireError(err error) bool { return se.wireErr } +// NewNotModifiedError indicates that the requested resource hasn't changed. It +// should be used only when handlers wish to respond to conditional HTTP GET +// requests with a 304 Not Modified. In all other circumstances, including all +// RPCs using the gRPC or gRPC-Web protocols, it's equivalent to sending an +// error with [CodeUnknown]. The supplied headers should include Etag, +// Cache-Control, or any other headers required by [RFC 9110 § 15.4.5]. +// +// Clients should check for this error using [IsNotModifiedError]. +// +// [RFC 9110 § 15.4.5]: https://httpwg.org/specs/rfc9110.html#status.304 +func NewNotModifiedError(headers http.Header) *Error { + err := NewError(CodeUnknown, errNotModified) + if headers != nil { + err.meta = headers + } + return err +} + func (e *Error) Error() string { message := e.Message() if message == "" { @@ -177,6 +233,11 @@ func (e *Error) AddDetail(d *ErrorDetail) { // or a block of in-body metadata, depending on the protocol in use and whether // or not the handler has already written messages to the stream. // +// Protocol-specific headers and trailers may be removed to avoid breaking +// protocol semantics. For example, Content-Length and Content-Type headers +// won't be propagated. See the documentation for each protocol for more +// datails. +// // When clients receive errors, the metadata contains the union of the HTTP // headers and the protocol-specific trailers (either HTTP trailers or in-body // metadata). @@ -190,11 +251,19 @@ func (e *Error) Meta() http.Header { func (e *Error) detailsAsAny() []*anypb.Any { anys := make([]*anypb.Any, 0, len(e.details)) for _, detail := range e.details { - anys = append(anys, detail.pb) + anys = append(anys, detail.pbAny) } return anys } +// IsNotModifiedError checks whether the supplied error indicates that the +// requested resource hasn't changed. It only returns true if the server used +// [NewNotModifiedError] in response to a Connect-protocol RPC made with an +// HTTP GET. +func IsNotModifiedError(err error) bool { + return errors.Is(err, errNotModified) +} + // errorf calls fmt.Errorf with the supplied template and arguments, then wraps // the resulting error. func errorf(c Code, template string, args ...any) *Error { @@ -239,10 +308,35 @@ func wrapIfContextError(err error) error { if errors.Is(err, context.DeadlineExceeded) { return NewError(CodeDeadlineExceeded, err) } + // Ick, some dial errors can be returned as os.ErrDeadlineExceeded + // instead of context.DeadlineExceeded :( + // https://github.com/golang/go/issues/64449 + if errors.Is(err, os.ErrDeadlineExceeded) { + return NewError(CodeDeadlineExceeded, err) + } return err } -// wrapIfLikelyWithGRPCNotUsedError adds a wrapping error that has a message +// wrapIfContextDone wraps errors with CodeCanceled or CodeDeadlineExceeded +// if the context is done. It leaves already-wrapped errors unchanged. +func wrapIfContextDone(ctx context.Context, err error) error { + if err == nil { + return nil + } + err = wrapIfContextError(err) + if _, ok := asError(err); ok { + return err + } + ctxErr := ctx.Err() + if errors.Is(ctxErr, context.Canceled) { + return NewError(CodeCanceled, err) + } else if errors.Is(ctxErr, context.DeadlineExceeded) { + return NewError(CodeDeadlineExceeded, err) + } + return err +} + +// wrapIfLikelyH2CNotConfiguredError adds a wrapping error that has a message // telling the caller that they likely need to use h2c but are using a raw http.Client{}. // // This happens when running a gRPC-only server. @@ -347,3 +441,24 @@ func wrapIfRSTError(err error) error { return err } } + +// wrapIfMaxBytesError wraps errors returned reading from a http.MaxBytesHandler +// whose limit has been exceeded. +func wrapIfMaxBytesError(err error, tmpl string, args ...any) error { + if err == nil { + return nil + } + if _, ok := asError(err); ok { + return err + } + var maxBytesErr *http.MaxBytesError + if ok := errors.As(err, &maxBytesErr); !ok { + return err + } + prefix := fmt.Sprintf(tmpl, args...) + return errorf(CodeResourceExhausted, "%s: exceeded %d byte http.MaxBytesReader limit", prefix, maxBytesErr.Limit) +} + +func typeNameFromURL(url string) string { + return url[strings.LastIndexByte(url, '/')+1:] +} diff --git a/vendor/connectrpc.com/connect/error_writer.go b/vendor/connectrpc.com/connect/error_writer.go new file mode 100644 index 00000000..58ce3c42 --- /dev/null +++ b/vendor/connectrpc.com/connect/error_writer.go @@ -0,0 +1,179 @@ +// Copyright 2021-2024 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// protocolType is one of the supported RPC protocols. +type protocolType uint8 + +const ( + unknownProtocol protocolType = iota + connectUnaryProtocol + connectStreamProtocol + grpcProtocol + grpcWebProtocol +) + +// An ErrorWriter writes errors to an [http.ResponseWriter] in the format +// expected by an RPC client. This is especially useful in server-side net/http +// middleware, where you may wish to handle requests from RPC and non-RPC +// clients with the same code. +// +// ErrorWriters are safe to use concurrently. +type ErrorWriter struct { + bufferPool *bufferPool + protobuf Codec + requireConnectProtocolHeader bool +} + +// NewErrorWriter constructs an ErrorWriter. Handler options may be passed to +// configure the error writer behaviour to match the handlers. +// [WithRequiredConnectProtocolHeader] will assert that Connect protocol +// requests include the version header allowing the error writer to correctly +// classify the request. +// Options supplied via [WithConditionalHandlerOptions] are ignored. +func NewErrorWriter(opts ...HandlerOption) *ErrorWriter { + config := newHandlerConfig("", StreamTypeUnary, opts) + codecs := newReadOnlyCodecs(config.Codecs) + return &ErrorWriter{ + bufferPool: config.BufferPool, + protobuf: codecs.Protobuf(), + requireConnectProtocolHeader: config.RequireConnectProtocolHeader, + } +} + +func (w *ErrorWriter) classifyRequest(request *http.Request) protocolType { + ctype := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType)) + isPost := request.Method == http.MethodPost + isGet := request.Method == http.MethodGet + switch { + case isPost && (ctype == grpcContentTypeDefault || strings.HasPrefix(ctype, grpcContentTypePrefix)): + return grpcProtocol + case isPost && (ctype == grpcWebContentTypeDefault || strings.HasPrefix(ctype, grpcWebContentTypePrefix)): + return grpcWebProtocol + case isPost && strings.HasPrefix(ctype, connectStreamingContentTypePrefix): + // Streaming ignores the requireConnectProtocolHeader option as the + // Content-Type is enough to determine the protocol. + if err := connectCheckProtocolVersion(request, false /* required */); err != nil { + return unknownProtocol + } + return connectStreamProtocol + case isPost && strings.HasPrefix(ctype, connectUnaryContentTypePrefix): + if err := connectCheckProtocolVersion(request, w.requireConnectProtocolHeader); err != nil { + return unknownProtocol + } + return connectUnaryProtocol + case isGet: + if err := connectCheckProtocolVersion(request, w.requireConnectProtocolHeader); err != nil { + return unknownProtocol + } + return connectUnaryProtocol + default: + return unknownProtocol + } +} + +// IsSupported checks whether a request is using one of the ErrorWriter's +// supported RPC protocols. +func (w *ErrorWriter) IsSupported(request *http.Request) bool { + return w.classifyRequest(request) != unknownProtocol +} + +// Write an error, using the format appropriate for the RPC protocol in use. +// Callers should first use IsSupported to verify that the request is using one +// of the ErrorWriter's supported RPC protocols. If the protocol is unknown, +// Write will send the error as unprefixed, Connect-formatted JSON. +// +// Write does not read or close the request body. +func (w *ErrorWriter) Write(response http.ResponseWriter, request *http.Request, err error) error { + ctype := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType)) + switch protocolType := w.classifyRequest(request); protocolType { + case connectStreamProtocol: + setHeaderCanonical(response.Header(), headerContentType, ctype) + return w.writeConnectStreaming(response, err) + case grpcProtocol: + setHeaderCanonical(response.Header(), headerContentType, ctype) + return w.writeGRPC(response, err) + case grpcWebProtocol: + setHeaderCanonical(response.Header(), headerContentType, ctype) + return w.writeGRPCWeb(response, err) + case unknownProtocol, connectUnaryProtocol: + fallthrough + default: + // Unary errors are always JSON. Unknown protocols are treated as unary + // because they are likely to be Connect clients and will still be able to + // parse the error as it's in a human-readable format. + setHeaderCanonical(response.Header(), headerContentType, connectUnaryContentTypeJSON) + return w.writeConnectUnary(response, err) + } +} + +func (w *ErrorWriter) writeConnectUnary(response http.ResponseWriter, err error) error { + if connectErr, ok := asError(err); ok && !connectErr.wireErr { + mergeMetadataHeaders(response.Header(), connectErr.meta) + } + response.WriteHeader(connectCodeToHTTP(CodeOf(err))) + data, marshalErr := json.Marshal(newConnectWireError(err)) + if marshalErr != nil { + return fmt.Errorf("marshal error: %w", marshalErr) + } + _, writeErr := response.Write(data) + return writeErr +} + +func (w *ErrorWriter) writeConnectStreaming(response http.ResponseWriter, err error) error { + response.WriteHeader(http.StatusOK) + marshaler := &connectStreamingMarshaler{ + envelopeWriter: envelopeWriter{ + sender: writeSender{writer: response}, + bufferPool: w.bufferPool, + }, + } + // MarshalEndStream returns *Error: check return value to avoid typed nils. + if marshalErr := marshaler.MarshalEndStream(err, make(http.Header)); marshalErr != nil { + return marshalErr + } + return nil +} + +func (w *ErrorWriter) writeGRPC(response http.ResponseWriter, err error) error { + trailers := make(http.Header, 2) // need space for at least code & message + grpcErrorToTrailer(trailers, w.protobuf, err) + // To make net/http reliably send trailers without a body, we must set the + // Trailers header rather than using http.TrailerPrefix. See + // https://github.com/golang/go/issues/54723. + keys := make([]string, 0, len(trailers)) + for k := range trailers { + keys = append(keys, k) + } + setHeaderCanonical(response.Header(), headerTrailer, strings.Join(keys, ",")) + response.WriteHeader(http.StatusOK) + mergeHeaders(response.Header(), trailers) + return nil +} + +func (w *ErrorWriter) writeGRPCWeb(response http.ResponseWriter, err error) error { + // This is a trailers-only response. To match the behavior of Envoy and + // protocol_grpc.go, put the trailers in the HTTP headers. + grpcErrorToTrailer(response.Header(), w.protobuf, err) + response.WriteHeader(http.StatusOK) + return nil +} diff --git a/vendor/github.com/bufbuild/connect-go/handler.go b/vendor/connectrpc.com/connect/handler.go similarity index 71% rename from vendor/github.com/bufbuild/connect-go/handler.go rename to vendor/connectrpc.com/connect/handler.go index 0560e933..1d573291 100644 --- a/vendor/github.com/bufbuild/connect-go/handler.go +++ b/vendor/connectrpc.com/connect/handler.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -29,8 +29,9 @@ import ( type Handler struct { spec Spec implementation StreamingHandlerFunc - protocolHandlers []protocolHandler - acceptPost string // Accept-Post header + protocolHandlers map[string][]protocolHandler // Method to protocol handlers + allowMethod string // Allow header + acceptPost string // Accept-Post header } // NewUnaryHandler constructs a [Handler] for a request-response procedure. @@ -56,22 +57,16 @@ func NewUnaryHandler[Req, Res any]( } return res, err }) - config := newHandlerConfig(procedure, options) + config := newHandlerConfig(procedure, StreamTypeUnary, options) if interceptor := config.Interceptor; interceptor != nil { untyped = interceptor.WrapUnary(untyped) } // Given a stream, how should we call the unary function? implementation := func(ctx context.Context, conn StreamingHandlerConn) error { - var msg Req - if err := conn.Receive(&msg); err != nil { + request, err := receiveUnaryRequest[Req](conn, config.Initializer) + if err != nil { return err } - request := &Request[Req]{ - Msg: &msg, - spec: conn.Spec(), - peer: conn.Peer(), - header: conn.RequestHeader(), - } response, err := untyped(ctx, request) if err != nil { return err @@ -81,11 +76,12 @@ func NewUnaryHandler[Req, Res any]( return conn.Send(response.Any()) } - protocolHandlers := config.newProtocolHandlers(StreamTypeUnary) + protocolHandlers := config.newProtocolHandlers() return &Handler{ - spec: config.newSpec(StreamTypeUnary), + spec: config.newSpec(), implementation: implementation, - protocolHandlers: protocolHandlers, + protocolHandlers: mappedMethodHandlers(protocolHandlers), + allowMethod: sortedAllowMethodValue(protocolHandlers), acceptPost: sortedAcceptPostValue(protocolHandlers), } } @@ -96,11 +92,14 @@ func NewClientStreamHandler[Req, Res any]( implementation func(context.Context, *ClientStream[Req]) (*Response[Res], error), options ...HandlerOption, ) *Handler { + config := newHandlerConfig(procedure, StreamTypeClient, options) return newStreamHandler( - procedure, - StreamTypeClient, + config, func(ctx context.Context, conn StreamingHandlerConn) error { - stream := &ClientStream[Req]{conn: conn} + stream := &ClientStream[Req]{ + conn: conn, + initializer: config.Initializer, + } res, err := implementation(ctx, stream) if err != nil { return err @@ -114,7 +113,6 @@ func NewClientStreamHandler[Req, Res any]( mergeHeaders(conn.ResponseTrailer(), res.trailer) return conn.Send(res.Msg) }, - options..., ) } @@ -124,26 +122,16 @@ func NewServerStreamHandler[Req, Res any]( implementation func(context.Context, *Request[Req], *ServerStream[Res]) error, options ...HandlerOption, ) *Handler { + config := newHandlerConfig(procedure, StreamTypeServer, options) return newStreamHandler( - procedure, - StreamTypeServer, + config, func(ctx context.Context, conn StreamingHandlerConn) error { - var msg Req - if err := conn.Receive(&msg); err != nil { + req, err := receiveUnaryRequest[Req](conn, config.Initializer) + if err != nil { return err } - return implementation( - ctx, - &Request[Req]{ - Msg: &msg, - spec: conn.Spec(), - peer: conn.Peer(), - header: conn.RequestHeader(), - }, - &ServerStream[Res]{conn: conn}, - ) + return implementation(ctx, req, &ServerStream[Res]{conn: conn}) }, - options..., ) } @@ -153,22 +141,24 @@ func NewBidiStreamHandler[Req, Res any]( implementation func(context.Context, *BidiStream[Req, Res]) error, options ...HandlerOption, ) *Handler { + config := newHandlerConfig(procedure, StreamTypeBidi, options) return newStreamHandler( - procedure, - StreamTypeBidi, + config, func(ctx context.Context, conn StreamingHandlerConn) error { return implementation( ctx, - &BidiStream[Req, Res]{conn: conn}, + &BidiStream[Req, Res]{ + conn: conn, + initializer: config.Initializer, + }, ) }, - options..., ) } // ServeHTTP implements [http.Handler]. func (h *Handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { - // We don't need to defer functions to close the request body or read to + // We don't need to defer functions to close the request body or read to // EOF: the stream we construct later on already does that, and we only // return early when dealing with misbehaving clients. In those cases, it's // okay if we can't re-use the connection. @@ -182,18 +172,19 @@ func (h *Handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Re return } - // The gRPC-HTTP2, gRPC-Web, and Connect protocols are all POST-only. - if request.Method != http.MethodPost { - responseWriter.Header().Set("Allow", http.MethodPost) + protocolHandlers := h.protocolHandlers[request.Method] + if len(protocolHandlers) == 0 { + responseWriter.Header().Set("Allow", h.allowMethod) responseWriter.WriteHeader(http.StatusMethodNotAllowed) return } + contentType := canonicalizeContentType(getHeaderCanonical(request.Header, headerContentType)) + // Find our implementation of the RPC protocol in use. - contentType := canonicalizeContentType(request.Header.Get("Content-Type")) var protocolHandler protocolHandler - for _, handler := range h.protocolHandlers { - if _, ok := handler.ContentTypes()[contentType]; ok { + for _, handler := range protocolHandlers { + if handler.CanHandlePayload(request, contentType) { protocolHandler = handler break } @@ -204,8 +195,26 @@ func (h *Handler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Re return } + if request.Method == http.MethodGet { + // A body must not be present. + hasBody := request.ContentLength > 0 + if request.ContentLength < 0 { + // No content-length header. + // Test if body is empty by trying to read a single byte. + var b [1]byte + n, _ := request.Body.Read(b[:]) + hasBody = n > 0 + } + if hasBody { + responseWriter.WriteHeader(http.StatusUnsupportedMediaType) + return + } + _ = request.Body.Close() + } + // Establish a stream and serve the RPC. - request.Header.Set("Content-Type", contentType) // prefer canonicalized value + setHeaderCanonical(request.Header, headerContentType, contentType) + setHeaderCanonical(request.Header, headerHost, request.Host) ctx, cancel, timeoutErr := protocolHandler.SetTimeout(request) //nolint: contextcheck if timeoutErr != nil { ctx = request.Context() @@ -236,23 +245,24 @@ type handlerConfig struct { CompressMinBytes int Interceptor Interceptor Procedure string - HandleGRPC bool - HandleGRPCWeb bool + Schema any + Initializer maybeInitializer RequireConnectProtocolHeader bool + IdempotencyLevel IdempotencyLevel BufferPool *bufferPool ReadMaxBytes int SendMaxBytes int + StreamType StreamType } -func newHandlerConfig(procedure string, options []HandlerOption) *handlerConfig { +func newHandlerConfig(procedure string, streamType StreamType, options []HandlerOption) *handlerConfig { protoPath := extractProtoPath(procedure) config := handlerConfig{ Procedure: protoPath, CompressionPools: make(map[string]*compressionPool), Codecs: make(map[string]Codec), - HandleGRPC: true, - HandleGRPCWeb: true, BufferPool: newBufferPool(), + StreamType: streamType, } withProtoBinaryCodec().applyToHandler(&config) withProtoJSONCodecs().applyToHandler(&config) @@ -263,20 +273,20 @@ func newHandlerConfig(procedure string, options []HandlerOption) *handlerConfig return &config } -func (c *handlerConfig) newSpec(streamType StreamType) Spec { +func (c *handlerConfig) newSpec() Spec { return Spec{ - Procedure: c.Procedure, - StreamType: streamType, + Procedure: c.Procedure, + Schema: c.Schema, + StreamType: c.StreamType, + IdempotencyLevel: c.IdempotencyLevel, } } -func (c *handlerConfig) newProtocolHandlers(streamType StreamType) []protocolHandler { - protocols := []protocol{&protocolConnect{}} - if c.HandleGRPC { - protocols = append(protocols, &protocolGRPC{web: false}) - } - if c.HandleGRPCWeb { - protocols = append(protocols, &protocolGRPC{web: true}) +func (c *handlerConfig) newProtocolHandlers() []protocolHandler { + protocols := []protocol{ + &protocolConnect{}, + &protocolGRPC{web: false}, + &protocolGRPC{web: true}, } handlers := make([]protocolHandler, 0, len(protocols)) codecs := newReadOnlyCodecs(c.Codecs) @@ -286,7 +296,7 @@ func (c *handlerConfig) newProtocolHandlers(streamType StreamType) []protocolHan ) for _, protocol := range protocols { handlers = append(handlers, protocol.NewHandler(&protocolHandlerParams{ - Spec: c.newSpec(streamType), + Spec: c.newSpec(), Codecs: codecs, CompressionPools: compressors, CompressMinBytes: c.CompressMinBytes, @@ -294,26 +304,25 @@ func (c *handlerConfig) newProtocolHandlers(streamType StreamType) []protocolHan ReadMaxBytes: c.ReadMaxBytes, SendMaxBytes: c.SendMaxBytes, RequireConnectProtocolHeader: c.RequireConnectProtocolHeader, + IdempotencyLevel: c.IdempotencyLevel, })) } return handlers } func newStreamHandler( - procedure string, - streamType StreamType, + config *handlerConfig, implementation StreamingHandlerFunc, - options ...HandlerOption, ) *Handler { - config := newHandlerConfig(procedure, options) if ic := config.Interceptor; ic != nil { implementation = ic.WrapStreamingHandler(implementation) } - protocolHandlers := config.newProtocolHandlers(streamType) + protocolHandlers := config.newProtocolHandlers() return &Handler{ - spec: config.newSpec(streamType), + spec: config.newSpec(), implementation: implementation, - protocolHandlers: protocolHandlers, + protocolHandlers: mappedMethodHandlers(protocolHandlers), + allowMethod: sortedAllowMethodValue(protocolHandlers), acceptPost: sortedAcceptPostValue(protocolHandlers), } } diff --git a/vendor/github.com/bufbuild/connect-go/handler_stream.go b/vendor/connectrpc.com/connect/handler_stream.go similarity index 84% rename from vendor/github.com/bufbuild/connect-go/handler_stream.go rename to vendor/connectrpc.com/connect/handler_stream.go index 5eb7a953..e946163d 100644 --- a/vendor/github.com/bufbuild/connect-go/handler_stream.go +++ b/vendor/connectrpc.com/connect/handler_stream.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,9 +25,10 @@ import ( // It's constructed as part of [Handler] invocation, but doesn't currently have // an exported constructor. type ClientStream[Req any] struct { - conn StreamingHandlerConn - msg *Req - err error + conn StreamingHandlerConn + initializer maybeInitializer + msg *Req + err error } // Spec returns the specification for the RPC. @@ -55,6 +56,10 @@ func (c *ClientStream[Req]) Receive() bool { return false } c.msg = new(Req) + if err := c.initializer.maybe(c.Spec(), c.msg); err != nil { + c.err = err + return false + } c.err = c.conn.Receive(c.msg) return c.err == nil } @@ -91,12 +96,18 @@ type ServerStream[Res any] struct { // ResponseHeader returns the response headers. Headers are sent with the first // call to Send. +// +// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. func (s *ServerStream[Res]) ResponseHeader() http.Header { return s.conn.ResponseHeader() } // ResponseTrailer returns the response trailers. Handlers may write to the // response trailers at any time before returning. +// +// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. func (s *ServerStream[Res]) ResponseTrailer() http.Header { return s.conn.ResponseTrailer() } @@ -121,7 +132,8 @@ func (s *ServerStream[Res]) Conn() StreamingHandlerConn { // It's constructed as part of [Handler] invocation, but doesn't currently have // an exported constructor. type BidiStream[Req, Res any] struct { - conn StreamingHandlerConn + conn StreamingHandlerConn + initializer maybeInitializer } // Spec returns the specification for the RPC. @@ -143,6 +155,9 @@ func (b *BidiStream[Req, Res]) RequestHeader() http.Header { // return an error that wraps [io.EOF]. func (b *BidiStream[Req, Res]) Receive() (*Req, error) { var req Req + if err := b.initializer.maybe(b.Spec(), &req); err != nil { + return nil, err + } if err := b.conn.Receive(&req); err != nil { return nil, err } @@ -151,12 +166,18 @@ func (b *BidiStream[Req, Res]) Receive() (*Req, error) { // ResponseHeader returns the response headers. Headers are sent with the first // call to Send. +// +// Headers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. func (b *BidiStream[Req, Res]) ResponseHeader() http.Header { return b.conn.ResponseHeader() } // ResponseTrailer returns the response trailers. Handlers may write to the // response trailers at any time before returning. +// +// Trailers beginning with "Connect-" and "Grpc-" are reserved for use by the +// Connect and gRPC protocols. Applications shouldn't write them. func (b *BidiStream[Req, Res]) ResponseTrailer() http.Header { return b.conn.ResponseTrailer() } diff --git a/vendor/connectrpc.com/connect/header.go b/vendor/connectrpc.com/connect/header.go new file mode 100644 index 00000000..f3c7cacd --- /dev/null +++ b/vendor/connectrpc.com/connect/header.go @@ -0,0 +1,126 @@ +// Copyright 2021-2024 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import ( + "encoding/base64" + "net/http" +) + +// EncodeBinaryHeader base64-encodes the data. It always emits unpadded values. +// +// In the Connect, gRPC, and gRPC-Web protocols, binary headers must have keys +// ending in "-Bin". +func EncodeBinaryHeader(data []byte) string { + // gRPC specification says that implementations should emit unpadded values. + return base64.RawStdEncoding.EncodeToString(data) +} + +// DecodeBinaryHeader base64-decodes the data. It can decode padded or unpadded +// values. Following usual HTTP semantics, multiple base64-encoded values may +// be joined with a comma. When receiving such comma-separated values, split +// them with [strings.Split] before calling DecodeBinaryHeader. +// +// Binary headers sent using the Connect, gRPC, and gRPC-Web protocols have +// keys ending in "-Bin". +func DecodeBinaryHeader(data string) ([]byte, error) { + if len(data)%4 != 0 { + // Data definitely isn't padded. + return base64.RawStdEncoding.DecodeString(data) + } + // Either the data was padded, or padding wasn't necessary. In both cases, + // the padding-aware decoder works. + return base64.StdEncoding.DecodeString(data) +} + +func mergeHeaders(into, from http.Header) { + for key, vals := range from { + if len(vals) == 0 { + // For response trailers, net/http will pre-populate entries + // with nil values based on the "Trailer" header. But if there + // are no actual values for those keys, we skip them. + continue + } + into[key] = append(into[key], vals...) + } +} + +// mergeMetdataHeaders merges the metadata headers from the "from" header into +// the "into" header. It skips over non metadata headers that should not be +// propagated from the server to the client. +func mergeMetadataHeaders(into, from http.Header) { + for key, vals := range from { + if len(vals) == 0 { + // For response trailers, net/http will pre-populate entries + // with nil values based on the "Trailer" header. But if there + // are no actual values for those keys, we skip them. + continue + } + switch http.CanonicalHeaderKey(key) { + case headerContentType, + headerContentLength, + headerContentEncoding, + headerHost, + headerUserAgent, + headerTrailer, + headerDate: + // HTTP headers. + case connectUnaryHeaderAcceptCompression, + connectUnaryTrailerPrefix, + connectStreamingHeaderCompression, + connectStreamingHeaderAcceptCompression, + connectHeaderTimeout, + connectHeaderProtocolVersion: + // Connect headers. + case grpcHeaderCompression, + grpcHeaderAcceptCompression, + grpcHeaderTimeout, + grpcHeaderStatus, + grpcHeaderMessage, + grpcHeaderDetails: + // gRPC headers. + default: + into[key] = append(into[key], vals...) + } + } +} + +// getHeaderCanonical is a shortcut for Header.Get() which +// bypasses the CanonicalMIMEHeaderKey operation when we +// know the key is already in canonical form. +func getHeaderCanonical(h http.Header, key string) string { + if h == nil { + return "" + } + v := h[key] + if len(v) == 0 { + return "" + } + return v[0] +} + +// setHeaderCanonical is a shortcut for Header.Set() which +// bypasses the CanonicalMIMEHeaderKey operation when we +// know the key is already in canonical form. +func setHeaderCanonical(h http.Header, key, value string) { + h[key] = []string{value} +} + +// delHeaderCanonical is a shortcut for Header.Del() which +// bypasses the CanonicalMIMEHeaderKey operation when we +// know the key is already in canonical form. +func delHeaderCanonical(h http.Header, key string) { + delete(h, key) +} diff --git a/vendor/connectrpc.com/connect/idempotency_level.go b/vendor/connectrpc.com/connect/idempotency_level.go new file mode 100644 index 00000000..46ad7467 --- /dev/null +++ b/vendor/connectrpc.com/connect/idempotency_level.go @@ -0,0 +1,68 @@ +// Copyright 2021-2024 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package connect + +import "fmt" + +// An IdempotencyLevel is a value that declares how "idempotent" an RPC is. This +// value can affect RPC behaviors, such as determining whether it is safe to +// retry a request, or what kinds of request modalities are allowed for a given +// procedure. +type IdempotencyLevel int + +// NOTE: For simplicity, these should be kept in sync with the values of the +// google.protobuf.MethodOptions.IdempotencyLevel enumeration. + +const ( + // IdempotencyUnknown is the default idempotency level. A procedure with + // this idempotency level may not be idempotent. This is appropriate for + // any kind of procedure. + IdempotencyUnknown IdempotencyLevel = 0 + + // IdempotencyNoSideEffects is the idempotency level that specifies that a + // given call has no side-effects. This is equivalent to [RFC 9110 § 9.2.1] + // "safe" methods in terms of semantics. This procedure should not mutate + // any state. This idempotency level is appropriate for queries, or anything + // that would be suitable for an HTTP GET request. In addition, due to the + // lack of side-effects, such a procedure would be suitable to retry and + // expect that the results will not be altered by preceding attempts. + // + // [RFC 9110 § 9.2.1]: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.1 + IdempotencyNoSideEffects IdempotencyLevel = 1 + + // IdempotencyIdempotent is the idempotency level that specifies that a + // given call is "idempotent", such that multiple instances of the same + // request to this procedure would have the same side-effects as a single + // request. This is equivalent to [RFC 9110 § 9.2.2] "idempotent" methods. + // This level is a subset of the previous level. This idempotency level is + // appropriate for any procedure that is safe to retry multiple times + // and be guaranteed that the response and side-effects will not be altered + // as a result of multiple attempts, for example, entity deletion requests. + // + // [RFC 9110 § 9.2.2]: https://www.rfc-editor.org/rfc/rfc9110.html#section-9.2.2 + IdempotencyIdempotent IdempotencyLevel = 2 +) + +func (i IdempotencyLevel) String() string { + switch i { + case IdempotencyUnknown: + return "idempotency_unknown" + case IdempotencyNoSideEffects: + return "no_side_effects" + case IdempotencyIdempotent: + return "idempotent" + } + return fmt.Sprintf("idempotency_%d", i) +} diff --git a/vendor/github.com/bufbuild/connect-go/interceptor.go b/vendor/connectrpc.com/connect/interceptor.go similarity index 98% rename from vendor/github.com/bufbuild/connect-go/interceptor.go rename to vendor/connectrpc.com/connect/interceptor.go index b9373683..8d0beac6 100644 --- a/vendor/github.com/bufbuild/connect-go/interceptor.go +++ b/vendor/connectrpc.com/connect/interceptor.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/vendor/github.com/bufbuild/connect-go/internal/gen/connectext/grpc/status/v1/status.pb.go b/vendor/connectrpc.com/connect/internal/gen/connectext/grpc/status/v1/status.pb.go similarity index 85% rename from vendor/github.com/bufbuild/connect-go/internal/gen/connectext/grpc/status/v1/status.pb.go rename to vendor/connectrpc.com/connect/internal/gen/connectext/grpc/status/v1/status.pb.go index 39708334..d43c9bd1 100644 --- a/vendor/github.com/bufbuild/connect-go/internal/gen/connectext/grpc/status/v1/status.pb.go +++ b/vendor/connectrpc.com/connect/internal/gen/connectext/grpc/status/v1/status.pb.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.28.1 +// protoc-gen-go v1.33.0 // protoc (unknown) // source: connectext/grpc/status/v1/status.proto @@ -119,21 +119,20 @@ var file_connectext_grpc_status_v1_status_proto_rawDesc = []byte{ 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, - 0x6e, 0x79, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x42, 0xcb, 0x01, 0x0a, 0x12, + 0x6e, 0x79, 0x52, 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x42, 0xc3, 0x01, 0x0a, 0x12, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x76, 0x31, 0x42, 0x0b, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, - 0x01, 0x5a, 0x4e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x75, - 0x66, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2d, 0x67, - 0x6f, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, - 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x76, - 0x31, 0xa2, 0x02, 0x03, 0x47, 0x53, 0x58, 0xaa, 0x02, 0x0e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0e, 0x47, 0x72, 0x70, 0x63, 0x5c, - 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1a, 0x47, 0x72, 0x70, 0x63, - 0x5c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x10, 0x47, 0x72, 0x70, 0x63, 0x3a, 0x3a, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x01, 0x5a, 0x46, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x65, 0x78, + 0x74, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2f, 0x76, 0x31, + 0x3b, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x47, 0x53, 0x58, 0xaa, + 0x02, 0x0e, 0x47, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x56, 0x31, + 0xca, 0x02, 0x0e, 0x47, 0x72, 0x70, 0x63, 0x5c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5c, 0x56, + 0x31, 0xe2, 0x02, 0x1a, 0x47, 0x72, 0x70, 0x63, 0x5c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5c, + 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, + 0x10, 0x47, 0x72, 0x70, 0x63, 0x3a, 0x3a, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3a, 0x3a, 0x56, + 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/vendor/github.com/bufbuild/connect-go/option.go b/vendor/connectrpc.com/connect/option.go similarity index 67% rename from vendor/github.com/bufbuild/connect-go/option.go rename to vendor/connectrpc.com/connect/option.go index 7fd2e8ad..9cc0c2ee 100644 --- a/vendor/github.com/bufbuild/connect-go/option.go +++ b/vendor/connectrpc.com/connect/option.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -43,14 +43,13 @@ type ClientOption interface { // Clients accept gzipped responses by default, using a compressor backed by the // standard library's [gzip] package with the default compression level. Use // [WithSendGzip] to compress requests with gzip. +// +// Calling WithAcceptCompression with an empty name is a no-op. func WithAcceptCompression( name string, newDecompressor func() Decompressor, newCompressor func() Compressor, ) ClientOption { - if newDecompressor == nil && newCompressor == nil { - return &compressionOption{Name: name} - } return &compressionOption{ Name: name, CompressionPool: newCompressionPool(newDecompressor, newCompressor), @@ -93,7 +92,7 @@ func WithSendCompression(name string) ClientOption { // WithSendGzip configures the client to gzip requests. Since clients have // access to a gzip compressor by default, WithSendGzip doesn't require -// [WithSendCompresion]. +// [WithSendCompression]. // // Some servers don't support gzip, so clients default to sending uncompressed // requests. @@ -116,9 +115,11 @@ type HandlerOption interface { // compressors and decompressors. // // By default, handlers support gzip using the standard library's -// [compress/gzip] package at the default compression level. +// [compress/gzip] package at the default compression level. To remove support for +// a previously-registered compression algorithm, use WithCompression with nil +// decompressor and compressor constructors. // -// Calling WithCompression with an empty name or nil constructors is a no-op. +// Calling WithCompression with an empty name is a no-op. func WithCompression( name string, newDecompressor func() Decompressor, @@ -158,13 +159,24 @@ func WithRecover(handle func(context.Context, Spec, http.Header, any) error) Han // header. This ensures that HTTP proxies and net/http middleware can easily // identify valid Connect requests, even if they use a common Content-Type like // application/json. However, it makes ad-hoc requests with tools like cURL -// more laborious. +// more laborious. Streaming requests are not affected by this option. // // This option has no effect if the client uses the gRPC or gRPC-Web protocols. func WithRequireConnectProtocolHeader() HandlerOption { return &requireConnectProtocolHeaderOption{} } +// WithConditionalHandlerOptions allows procedures in the same service to have +// different configurations: for example, one procedure may need a much larger +// WithReadMaxBytes setting than the others. +// +// WithConditionalHandlerOptions takes a function which may inspect each +// procedure's Spec before deciding which options to apply. Returning a nil +// slice is safe. +func WithConditionalHandlerOptions(conditional func(spec Spec) []HandlerOption) HandlerOption { + return &conditionalHandlerOptions{conditional: conditional} +} + // Option implements both [ClientOption] and [HandlerOption], so it can be // applied both client-side and server-side. type Option interface { @@ -172,6 +184,34 @@ type Option interface { HandlerOption } +// WithSchema provides a parsed representation of the schema for an RPC to a +// client or handler. The supplied schema is exposed as [Spec.Schema]. This +// option is typically added by generated code. +// +// For services using protobuf schemas, the supplied schema should be a +// [protoreflect.MethodDescriptor]. +func WithSchema(schema any) Option { + return &schemaOption{Schema: schema} +} + +// WithRequestInitializer provides a function that initializes a new message. +// It may be used to dynamically construct request messages. It is called on +// server receives to construct the message to be unmarshaled into. The message +// will be a non nil pointer to the type created by the handler. Use the Schema +// field of the [Spec] to determine the type of the message. +func WithRequestInitializer(initializer func(spec Spec, message any) error) HandlerOption { + return &initializerOption{Initializer: initializer} +} + +// WithResponseInitializer provides a function that initializes a new message. +// It may be used to dynamically construct response messages. It is called on +// client receives to construct the message to be unmarshaled into. The message +// will be a non nil pointer to the type created by the client. Use the Schema +// field of the [Spec] to determine the type of the message. +func WithResponseInitializer(initializer func(spec Spec, message any) error) ClientOption { + return &initializerOption{Initializer: initializer} +} + // WithCodec registers a serialization method with a client or handler. // Handlers may have multiple codecs registered, and use whichever the client // chooses. Clients may only have a single codec. @@ -230,6 +270,36 @@ func WithSendMaxBytes(max int) Option { return &sendMaxBytesOption{Max: max} } +// WithIdempotency declares the idempotency of the procedure. This can determine +// whether a procedure call can safely be retried, and may affect which request +// modalities are allowed for a given procedure call. +// +// In most cases, you should not need to manually set this. It is normally set +// by the code generator for your schema. For protobuf schemas, it can be set like this: +// +// rpc Ping(PingRequest) returns (PingResponse) { +// option idempotency_level = NO_SIDE_EFFECTS; +// } +func WithIdempotency(idempotencyLevel IdempotencyLevel) Option { + return &idempotencyOption{idempotencyLevel: idempotencyLevel} +} + +// WithHTTPGet allows Connect-protocol clients to use HTTP GET requests for +// side-effect free unary RPC calls. Typically, the service schema indicates +// which procedures are idempotent (see [WithIdempotency] for an example +// protobuf schema). The gRPC and gRPC-Web protocols are POST-only, so this +// option has no effect when combined with [WithGRPC] or [WithGRPCWeb]. +// +// Using HTTP GET requests makes it easier to take advantage of CDNs, caching +// reverse proxies, and browsers' built-in caching. Note, however, that servers +// don't automatically set any cache headers; you can set cache headers using +// interceptors or by adding headers in individual procedure implementations. +// +// By default, all requests are made as HTTP POSTs. +func WithHTTPGet() ClientOption { + return &enableGet{} +} + // WithInterceptors configures a client or handler's interceptor stack. Repeated // WithInterceptors options are applied in order, so // @@ -286,6 +356,41 @@ func WithOptions(options ...Option) Option { return &optionsOption{options} } +type schemaOption struct { + Schema any +} + +func (o *schemaOption) applyToClient(config *clientConfig) { + config.Schema = o.Schema +} + +func (o *schemaOption) applyToHandler(config *handlerConfig) { + config.Schema = o.Schema +} + +type initializerOption struct { + Initializer func(spec Spec, message any) error +} + +func (o *initializerOption) applyToHandler(config *handlerConfig) { + config.Initializer = maybeInitializer{initializer: o.Initializer} +} + +func (o *initializerOption) applyToClient(config *clientConfig) { + config.Initializer = maybeInitializer{initializer: o.Initializer} +} + +type maybeInitializer struct { + initializer func(spec Spec, message any) error +} + +func (o maybeInitializer) maybe(spec Spec, message any) error { + if o.initializer != nil { + return o.initializer(spec, message) + } + return nil +} + type clientOptionsOption struct { options []ClientOption } @@ -320,31 +425,31 @@ type compressionOption struct { } func (o *compressionOption) applyToClient(config *clientConfig) { + o.apply(&config.CompressionNames, config.CompressionPools) +} + +func (o *compressionOption) applyToHandler(config *handlerConfig) { + o.apply(&config.CompressionNames, config.CompressionPools) +} + +func (o *compressionOption) apply(configuredNames *[]string, configuredPools map[string]*compressionPool) { if o.Name == "" { return } if o.CompressionPool == nil { - delete(config.CompressionPools, o.Name) + delete(configuredPools, o.Name) var names []string - for _, name := range config.CompressionNames { + for _, name := range *configuredNames { if name == o.Name { continue } names = append(names, name) } - config.CompressionNames = names - return - } - config.CompressionPools[o.Name] = o.CompressionPool - config.CompressionNames = append(config.CompressionNames, o.Name) -} - -func (o *compressionOption) applyToHandler(config *handlerConfig) { - if o.Name == "" || o.CompressionPool == nil { + *configuredNames = names return } - config.CompressionPools[o.Name] = o.CompressionPool - config.CompressionNames = append(config.CompressionNames, o.Name) + configuredPools[o.Name] = o.CompressionPool + *configuredNames = append(*configuredNames, o.Name) } type compressMinBytesOption struct { @@ -399,6 +504,18 @@ func (o *requireConnectProtocolHeaderOption) applyToHandler(config *handlerConfi config.RequireConnectProtocolHeader = true } +type idempotencyOption struct { + idempotencyLevel IdempotencyLevel +} + +func (o *idempotencyOption) applyToClient(config *clientConfig) { + config.IdempotencyLevel = o.idempotencyLevel +} + +func (o *idempotencyOption) applyToHandler(config *handlerConfig) { + config.IdempotencyLevel = o.idempotencyLevel +} + type grpcOption struct { web bool } @@ -407,6 +524,44 @@ func (o *grpcOption) applyToClient(config *clientConfig) { config.Protocol = &protocolGRPC{web: o.web} } +type enableGet struct{} + +func (o *enableGet) applyToClient(config *clientConfig) { + config.EnableGet = true +} + +// WithHTTPGetMaxURLSize sets the maximum allowable URL length for GET requests +// made using the Connect protocol. It has no effect on gRPC or gRPC-Web +// clients, since those protocols are POST-only. +// +// Limiting the URL size is useful as most user agents, proxies, and servers +// have limits on the allowable length of a URL. For example, Apache and Nginx +// limit the size of a request line to around 8 KiB, meaning that maximum +// length of a URL is a bit smaller than this. If you run into URL size +// limitations imposed by your network infrastructure and don't know the +// maximum allowable size, or if you'd prefer to be cautious from the start, a +// 4096 byte (4 KiB) limit works with most common proxies and CDNs. +// +// If fallback is set to true and the URL would be longer than the configured +// maximum value, the request will be sent as an HTTP POST instead. If fallback +// is set to false, the request will fail with [CodeResourceExhausted]. +// +// By default, Connect-protocol clients with GET requests enabled may send a +// URL of any size. +func WithHTTPGetMaxURLSize(bytes int, fallback bool) ClientOption { + return &getURLMaxBytes{Max: bytes, Fallback: fallback} +} + +type getURLMaxBytes struct { + Max int + Fallback bool +} + +func (o *getURLMaxBytes) applyToClient(config *clientConfig) { + config.GetURLMaxBytes = o.Max + config.GetUseFallback = o.Fallback +} + type interceptorsOption struct { Interceptors []Interceptor } @@ -476,3 +631,17 @@ func withProtoJSONCodecs() HandlerOption { WithCodec(&protoJSONCodec{codecNameJSONCharsetUTF8}), ) } + +type conditionalHandlerOptions struct { + conditional func(spec Spec) []HandlerOption +} + +func (o *conditionalHandlerOptions) applyToHandler(config *handlerConfig) { + spec := config.newSpec() + if spec.Procedure == "" { + return // ignore empty specs + } + for _, option := range o.conditional(spec) { + option.applyToHandler(config) + } +} diff --git a/vendor/github.com/bufbuild/connect-go/protobuf_util.go b/vendor/connectrpc.com/connect/protobuf_util.go similarity index 90% rename from vendor/github.com/bufbuild/connect-go/protobuf_util.go rename to vendor/connectrpc.com/connect/protobuf_util.go index b98937b4..1c8dbdce 100644 --- a/vendor/github.com/bufbuild/connect-go/protobuf_util.go +++ b/vendor/connectrpc.com/connect/protobuf_util.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,8 +22,8 @@ import ( // corresponding to the Protobuf package, service, and method. It always starts // with a slash. Within connect, we use this as (1) Spec.Procedure and (2) the // path when mounting handlers on muxes. -func extractProtoPath(url string) string { - segments := strings.Split(url, "/") +func extractProtoPath(path string) string { + segments := strings.Split(path, "/") var pkg, method string if len(segments) > 0 { pkg = segments[0] diff --git a/vendor/github.com/bufbuild/connect-go/protocol.go b/vendor/connectrpc.com/connect/protocol.go similarity index 69% rename from vendor/github.com/bufbuild/connect-go/protocol.go rename to vendor/connectrpc.com/connect/protocol.go index 5d291809..9add614c 100644 --- a/vendor/github.com/bufbuild/connect-go/protocol.go +++ b/vendor/connectrpc.com/connect/protocol.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,8 +35,13 @@ const ( ) const ( - headerContentType = "Content-Type" - headerUserAgent = "User-Agent" + headerContentType = "Content-Type" + headerContentEncoding = "Content-Encoding" + headerContentLength = "Content-Length" + headerHost = "Host" + headerUserAgent = "User-Agent" + headerTrailer = "Trailer" + headerDate = "Date" discardLimit = 1024 * 1024 * 4 // 4MiB ) @@ -77,11 +82,15 @@ type protocolHandlerParams struct { ReadMaxBytes int SendMaxBytes int RequireConnectProtocolHeader bool + IdempotencyLevel IdempotencyLevel } // Handler is the server side of a protocol. HTTP handlers typically support // multiple protocols, codecs, and compressors. type protocolHandler interface { + // Methods is the set of HTTP methods the protocol can handle. + Methods() map[string]struct{} + // ContentTypes is the set of HTTP Content-Types that the protocol can // handle. ContentTypes() map[string]struct{} @@ -94,6 +103,11 @@ type protocolHandler interface { // request's context, a nil cancellation function, and a nil error. SetTimeout(*http.Request) (context.Context, context.CancelFunc, error) + // CanHandlePayload returns true if the protocol can handle an HTTP request. + // This is called after the request method is validated, so we only need to + // be concerned with the content type/payload specifically. + CanHandlePayload(*http.Request, string) bool + // NewConn constructs a HandlerConn for the message exchange. NewConn(http.ResponseWriter, *http.Request) (handlerConnCloser, bool) } @@ -108,10 +122,13 @@ type protocolClientParams struct { Codec Codec CompressMinBytes int HTTPClient HTTPClient - URL string + URL *url.URL BufferPool *bufferPool ReadMaxBytes int SendMaxBytes int + EnableGet bool + GetURLMaxBytes int + GetUseFallback bool // The gRPC family of protocols always needs access to a Protobuf codec to // marshal and unmarshal errors. Protobuf Codec @@ -132,7 +149,15 @@ type protocolClient interface { // been populated by WriteRequestHeader. When constructing a stream for a // unary call, implementations may assume that the Sender's Send and Close // methods return before the Receiver's Receive or Close methods are called. - NewConn(context.Context, Spec, http.Header) StreamingClientConn + NewConn(context.Context, Spec, http.Header) streamingClientConn +} + +// streamingClientConn extends StreamingClientConn with a method for registering +// a hook when the HTTP request is actually sent. +type streamingClientConn interface { + StreamingClientConn + + onRequestSend(fn func(*http.Request)) } // errorTranslatingHandlerConnCloser wraps a handlerConnCloser to ensure that @@ -160,30 +185,41 @@ func (hc *errorTranslatingHandlerConnCloser) Close(err error) error { return hc.fromWire(closeErr) } +func (hc *errorTranslatingHandlerConnCloser) getHTTPMethod() string { + if methoder, ok := hc.handlerConnCloser.(interface{ getHTTPMethod() string }); ok { + return methoder.getHTTPMethod() + } + return http.MethodPost +} + // errorTranslatingClientConn wraps a StreamingClientConn to make sure that we always // return coded errors from clients. // // It's used in protocol implementations. type errorTranslatingClientConn struct { - StreamingClientConn + streamingClientConn fromWire func(error) error } func (cc *errorTranslatingClientConn) Send(msg any) error { - return cc.fromWire(cc.StreamingClientConn.Send(msg)) + return cc.fromWire(cc.streamingClientConn.Send(msg)) } func (cc *errorTranslatingClientConn) Receive(msg any) error { - return cc.fromWire(cc.StreamingClientConn.Receive(msg)) + return cc.fromWire(cc.streamingClientConn.Receive(msg)) } func (cc *errorTranslatingClientConn) CloseRequest() error { - return cc.fromWire(cc.StreamingClientConn.CloseRequest()) + return cc.fromWire(cc.streamingClientConn.CloseRequest()) } func (cc *errorTranslatingClientConn) CloseResponse() error { - return cc.fromWire(cc.StreamingClientConn.CloseResponse()) + return cc.fromWire(cc.streamingClientConn.CloseResponse()) +} + +func (cc *errorTranslatingClientConn) onRequestSend(fn func(*http.Request)) { + cc.streamingClientConn.onRequestSend(fn) } // wrapHandlerConnWithCodedErrors ensures that we (1) automatically code @@ -199,13 +235,23 @@ func wrapHandlerConnWithCodedErrors(conn handlerConnCloser) handlerConnCloser { // wrapClientConnWithCodedErrors ensures that we always return *Errors from // public APIs. -func wrapClientConnWithCodedErrors(conn StreamingClientConn) StreamingClientConn { +func wrapClientConnWithCodedErrors(conn streamingClientConn) streamingClientConn { return &errorTranslatingClientConn{ - StreamingClientConn: conn, + streamingClientConn: conn, fromWire: wrapIfUncoded, } } +func mappedMethodHandlers(handlers []protocolHandler) map[string][]protocolHandler { + methodHandlers := make(map[string][]protocolHandler) + for _, handler := range handlers { + for method := range handler.Methods() { + methodHandlers[method] = append(methodHandlers[method], handler) + } + } + return methodHandlers +} + func sortedAcceptPostValue(handlers []protocolHandler) string { contentTypes := make(map[string]struct{}) for _, handler := range handlers { @@ -221,36 +267,33 @@ func sortedAcceptPostValue(handlers []protocolHandler) string { return strings.Join(accept, ", ") } +func sortedAllowMethodValue(handlers []protocolHandler) string { + methods := make(map[string]struct{}) + for _, handler := range handlers { + for method := range handler.Methods() { + methods[method] = struct{}{} + } + } + allow := make([]string, 0, len(methods)) + for ct := range methods { + allow = append(allow, ct) + } + sort.Strings(allow) + return strings.Join(allow, ", ") +} + func isCommaOrSpace(c rune) bool { return c == ',' || c == ' ' } -func discard(reader io.Reader) error { +func discard(reader io.Reader) (int64, error) { if lr, ok := reader.(*io.LimitedReader); ok { - _, err := io.Copy(io.Discard, lr) - return err + return io.Copy(io.Discard, lr) } // We don't want to get stuck throwing data away forever, so limit how much // we're willing to do here. lr := &io.LimitedReader{R: reader, N: discardLimit} - _, err := io.Copy(io.Discard, lr) - return err -} - -func validateRequestURL(uri string) *Error { - _, err := url.ParseRequestURI(uri) - if err == nil { - return nil - } - if !strings.Contains(uri, "://") { - // URL doesn't have a scheme, so the user is likely accustomed to - // grpc-go's APIs. - err = fmt.Errorf( - "URL %q missing scheme: use http:// or https:// (unlike grpc-go)", - uri, - ) - } - return NewError(CodeUnavailable, err) + return io.Copy(io.Discard, lr) } // negotiateCompression determines and validates the request compression and @@ -315,10 +358,67 @@ func flushResponseWriter(w http.ResponseWriter) { } } -func canonicalizeContentType(ct string) string { - base, params, err := mime.ParseMediaType(ct) +func canonicalizeContentType(contentType string) string { + // Typically, clients send Content-Type in canonical form, without + // parameters. In those cases, we'd like to avoid parsing and + // canonicalization overhead. + // + // See https://www.rfc-editor.org/rfc/rfc2045.html#section-5.1 for a full + // grammar. + var slashes int + for _, r := range contentType { + switch { + case r >= 'a' && r <= 'z': + case r == '.' || r == '+' || r == '-': + case r == '/': + slashes++ + default: + return canonicalizeContentTypeSlow(contentType) + } + } + if slashes == 1 { + return contentType + } + return canonicalizeContentTypeSlow(contentType) +} + +func canonicalizeContentTypeSlow(contentType string) string { + base, params, err := mime.ParseMediaType(contentType) if err != nil { - return ct + return contentType + } + // According to RFC 9110 Section 8.3.2, the charset parameter value should be treated as case-insensitive. + // mime.FormatMediaType canonicalizes parameter names, but not parameter values, + // because the case sensitivity of a parameter value depends on its semantics. + // Therefore, the charset parameter value should be canonicalized here. + // ref.) https://httpwg.org/specs/rfc9110.html#rfc.section.8.3.2 + if charset, ok := params["charset"]; ok { + params["charset"] = strings.ToLower(charset) } return mime.FormatMediaType(base, params) } + +func httpToCode(httpCode int) Code { + // https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md + // Note that this is NOT the inverse of the gRPC-to-HTTP or Connect-to-HTTP + // mappings. + + // Literals are easier to compare to the specification (vs named + // constants). + switch httpCode { + case 400: + return CodeInternal + case 401: + return CodeUnauthenticated + case 403: + return CodePermissionDenied + case 404: + return CodeUnimplemented + case 429: + return CodeUnavailable + case 502, 503, 504: + return CodeUnavailable + default: + return CodeUnknown + } +} diff --git a/vendor/github.com/bufbuild/connect-go/protocol_connect.go b/vendor/connectrpc.com/connect/protocol_connect.go similarity index 58% rename from vendor/github.com/bufbuild/connect-go/protocol_connect.go rename to vendor/connectrpc.com/connect/protocol_connect.go index 796354f0..e3c5e4a5 100644 --- a/vendor/github.com/bufbuild/connect-go/protocol_connect.go +++ b/vendor/connectrpc.com/connect/protocol_connect.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,11 +24,13 @@ import ( "io" "math" "net/http" + "net/url" "runtime" "strconv" "strings" "time" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" ) @@ -41,44 +43,69 @@ const ( connectHeaderTimeout = "Connect-Timeout-Ms" connectHeaderProtocolVersion = "Connect-Protocol-Version" connectProtocolVersion = "1" + headerVary = "Vary" connectFlagEnvelopeEndStream = 0b00000010 connectUnaryContentTypePrefix = "application/" - connectUnaryContentTypeJSON = connectUnaryContentTypePrefix + "json" + connectUnaryContentTypeJSON = connectUnaryContentTypePrefix + codecNameJSON connectStreamingContentTypePrefix = "application/connect+" + + connectUnaryEncodingQueryParameter = "encoding" + connectUnaryMessageQueryParameter = "message" + connectUnaryBase64QueryParameter = "base64" + connectUnaryCompressionQueryParameter = "compression" + connectUnaryConnectQueryParameter = "connect" + connectUnaryConnectQueryValue = "v" + connectProtocolVersion ) +// defaultConnectUserAgent returns a User-Agent string similar to those used in gRPC. +var defaultConnectUserAgent = fmt.Sprintf("connect-go/%s (%s)", Version, runtime.Version()) + type protocolConnect struct{} // NewHandler implements protocol, so it must return an interface. func (*protocolConnect) NewHandler(params *protocolHandlerParams) protocolHandler { + methods := make(map[string]struct{}) + methods[http.MethodPost] = struct{}{} + + if params.Spec.StreamType == StreamTypeUnary && params.IdempotencyLevel == IdempotencyNoSideEffects { + methods[http.MethodGet] = struct{}{} + } + contentTypes := make(map[string]struct{}) for _, name := range params.Codecs.Names() { if params.Spec.StreamType == StreamTypeUnary { - contentTypes[connectUnaryContentTypePrefix+name] = struct{}{} + contentTypes[canonicalizeContentType(connectUnaryContentTypePrefix+name)] = struct{}{} continue } - contentTypes[connectStreamingContentTypePrefix+name] = struct{}{} + contentTypes[canonicalizeContentType(connectStreamingContentTypePrefix+name)] = struct{}{} } + return &connectHandler{ protocolHandlerParams: *params, + methods: methods, accept: contentTypes, } } // NewClient implements protocol, so it must return an interface. func (*protocolConnect) NewClient(params *protocolClientParams) (protocolClient, error) { - if err := validateRequestURL(params.URL); err != nil { - return nil, err - } - return &connectClient{protocolClientParams: *params}, nil + return &connectClient{ + protocolClientParams: *params, + peer: newPeerFromURL(params.URL, ProtocolConnect), + }, nil } type connectHandler struct { protocolHandlerParams - accept map[string]struct{} + methods map[string]struct{} + accept map[string]struct{} +} + +func (h *connectHandler) Methods() map[string]struct{} { + return h.methods } func (h *connectHandler) ContentTypes() map[string]struct{} { @@ -86,7 +113,7 @@ func (h *connectHandler) ContentTypes() map[string]struct{} { } func (*connectHandler) SetTimeout(request *http.Request) (context.Context, context.CancelFunc, error) { - timeout := request.Header.Get(connectHeaderTimeout) + timeout := getHeaderCanonical(request.Header, connectHeaderTimeout) if timeout == "" { return request.Context(), nil, nil } @@ -104,19 +131,38 @@ func (*connectHandler) SetTimeout(request *http.Request) (context.Context, conte return ctx, cancel, nil } +func (h *connectHandler) CanHandlePayload(request *http.Request, contentType string) bool { + if request.Method == http.MethodGet { + query := request.URL.Query() + codecName := query.Get(connectUnaryEncodingQueryParameter) + contentType = connectContentTypeFromCodecName( + h.Spec.StreamType, + codecName, + ) + } + _, ok := h.accept[contentType] + return ok +} + func (h *connectHandler) NewConn( responseWriter http.ResponseWriter, request *http.Request, ) (handlerConnCloser, bool) { + ctx := request.Context() + query := request.URL.Query() // We need to parse metadata before entering the interceptor stack; we'll // send the error to the client later on. var contentEncoding, acceptEncoding string if h.Spec.StreamType == StreamTypeUnary { - contentEncoding = request.Header.Get(connectUnaryHeaderCompression) - acceptEncoding = request.Header.Get(connectUnaryHeaderAcceptCompression) + if request.Method == http.MethodGet { + contentEncoding = query.Get(connectUnaryCompressionQueryParameter) + } else { + contentEncoding = getHeaderCanonical(request.Header, connectUnaryHeaderCompression) + } + acceptEncoding = getHeaderCanonical(request.Header, connectUnaryHeaderAcceptCompression) } else { - contentEncoding = request.Header.Get(connectStreamingHeaderCompression) - acceptEncoding = request.Header.Get(connectStreamingHeaderAcceptCompression) + contentEncoding = getHeaderCanonical(request.Header, connectStreamingHeaderCompression) + acceptEncoding = getHeaderCanonical(request.Header, connectStreamingHeaderAcceptCompression) } requestCompression, responseCompression, failed := negotiateCompression( h.CompressionPools, @@ -127,12 +173,40 @@ func (h *connectHandler) NewConn( failed = checkServerStreamsCanFlush(h.Spec, responseWriter) } if failed == nil { - version := request.Header.Get(connectHeaderProtocolVersion) - if version == "" && h.RequireConnectProtocolHeader { - failed = errorf(CodeInvalidArgument, "missing required header: set %s to %q", connectHeaderProtocolVersion, connectProtocolVersion) - } else if version != "" && version != connectProtocolVersion { - failed = errorf(CodeInvalidArgument, "%s must be %q: got %q", connectHeaderProtocolVersion, connectProtocolVersion, version) + required := h.RequireConnectProtocolHeader && (h.Spec.StreamType == StreamTypeUnary) + failed = connectCheckProtocolVersion(request, required) + } + + var requestBody io.ReadCloser + var contentType, codecName string + if request.Method == http.MethodGet { + if failed == nil && !query.Has(connectUnaryEncodingQueryParameter) { + failed = errorf(CodeInvalidArgument, "missing %s parameter", connectUnaryEncodingQueryParameter) + } else if failed == nil && !query.Has(connectUnaryMessageQueryParameter) { + failed = errorf(CodeInvalidArgument, "missing %s parameter", connectUnaryMessageQueryParameter) } + msg := query.Get(connectUnaryMessageQueryParameter) + msgReader := queryValueReader(msg, query.Get(connectUnaryBase64QueryParameter) == "1") + requestBody = io.NopCloser(msgReader) + codecName = query.Get(connectUnaryEncodingQueryParameter) + contentType = connectContentTypeFromCodecName( + h.Spec.StreamType, + codecName, + ) + } else { + requestBody = request.Body + contentType = getHeaderCanonical(request.Header, headerContentType) + codecName = connectCodecFromContentType( + h.Spec.StreamType, + contentType, + ) + } + + codec := h.Codecs.Get(codecName) + // The codec can be nil in the GET request case; that's okay: when failed + // is non-nil, codec is never used. + if failed == nil && codec == nil { + failed = errorf(CodeInvalidArgument, "invalid message encoding: %q", codecName) } // Write any remaining headers here: @@ -143,7 +217,7 @@ func (h *connectHandler) NewConn( // Since we know that these header keys are already in canonical form, we can // skip the normalization in Header.Set. header := responseWriter.Header() - header[headerContentType] = []string{request.Header.Get(headerContentType)} + header[headerContentType] = []string{contentType} acceptCompressionHeader := connectUnaryHeaderAcceptCompression if h.Spec.StreamType != StreamTypeUnary { acceptCompressionHeader = connectStreamingHeaderAcceptCompression @@ -157,16 +231,11 @@ func (h *connectHandler) NewConn( } header[acceptCompressionHeader] = []string{h.CompressionPools.CommaSeparatedNames()} - codecName := connectCodecFromContentType( - h.Spec.StreamType, - request.Header.Get(headerContentType), - ) - codec := h.Codecs.Get(codecName) // handler.go guarantees this is not nil - var conn handlerConnCloser peer := Peer{ Addr: request.RemoteAddr, Protocol: ProtocolConnect, + Query: query, } if h.Spec.StreamType == StreamTypeUnary { conn = &connectUnaryHandlerConn{ @@ -175,7 +244,8 @@ func (h *connectHandler) NewConn( request: request, responseWriter: responseWriter, marshaler: connectUnaryMarshaler{ - writer: responseWriter, + ctx: ctx, + sender: writeSender{writer: responseWriter}, codec: codec, compressMinBytes: h.CompressMinBytes, compressionName: responseCompression, @@ -185,7 +255,8 @@ func (h *connectHandler) NewConn( sendMaxBytes: h.SendMaxBytes, }, unmarshaler: connectUnaryUnmarshaler{ - reader: request.Body, + ctx: ctx, + reader: requestBody, codec: codec, compressionPool: h.CompressionPools.Get(requestCompression), bufferPool: h.BufferPool, @@ -201,7 +272,8 @@ func (h *connectHandler) NewConn( responseWriter: responseWriter, marshaler: connectStreamingMarshaler{ envelopeWriter: envelopeWriter{ - writer: responseWriter, + ctx: ctx, + sender: writeSender{responseWriter}, codec: codec, compressMinBytes: h.CompressMinBytes, compressionPool: h.CompressionPools.Get(responseCompression), @@ -211,7 +283,8 @@ func (h *connectHandler) NewConn( }, unmarshaler: connectStreamingUnmarshaler{ envelopeReader: envelopeReader{ - reader: request.Body, + ctx: ctx, + reader: requestBody, codec: codec, compressionPool: h.CompressionPools.Get(requestCompression), bufferPool: h.BufferPool, @@ -233,17 +306,19 @@ func (h *connectHandler) NewConn( type connectClient struct { protocolClientParams + + peer Peer } func (c *connectClient) Peer() Peer { - return newPeerFromURL(c.URL, ProtocolConnect) + return c.peer } func (c *connectClient) WriteRequestHeader(streamType StreamType, header http.Header) { // We know these header keys are in canonical form, so we can bypass all the // checks in Header.Set. - if header.Get(headerUserAgent) == "" { - header[headerUserAgent] = []string{connectUserAgent()} + if getHeaderCanonical(header, headerUserAgent) == "" { + header[headerUserAgent] = []string{defaultConnectUserAgent} } header[connectHeaderProtocolVersion] = []string{connectProtocolVersion} header[headerContentType] = []string{ @@ -273,7 +348,7 @@ func (c *connectClient) NewConn( ctx context.Context, spec Spec, header http.Header, -) StreamingClientConn { +) streamingClientConn { if deadline, ok := ctx.Deadline(); ok { millis := int64(time.Until(deadline) / time.Millisecond) if millis > 0 { @@ -284,7 +359,7 @@ func (c *connectClient) NewConn( } } duplexCall := newDuplexHTTPCall(ctx, c.HTTPClient, c.URL, spec, header) - var conn StreamingClientConn + var conn streamingClientConn if spec.StreamType == StreamTypeUnary { unaryConn := &connectUnaryClientConn{ spec: spec, @@ -292,17 +367,21 @@ func (c *connectClient) NewConn( duplexCall: duplexCall, compressionPools: c.CompressionPools, bufferPool: c.BufferPool, - marshaler: connectUnaryMarshaler{ - writer: duplexCall, - codec: c.Codec, - compressMinBytes: c.CompressMinBytes, - compressionName: c.CompressionName, - compressionPool: c.CompressionPools.Get(c.CompressionName), - bufferPool: c.BufferPool, - header: duplexCall.Header(), - sendMaxBytes: c.SendMaxBytes, + marshaler: connectUnaryRequestMarshaler{ + connectUnaryMarshaler: connectUnaryMarshaler{ + ctx: ctx, + sender: duplexCall, + codec: c.Codec, + compressMinBytes: c.CompressMinBytes, + compressionName: c.CompressionName, + compressionPool: c.CompressionPools.Get(c.CompressionName), + bufferPool: c.BufferPool, + header: duplexCall.Header(), + sendMaxBytes: c.SendMaxBytes, + }, }, unmarshaler: connectUnaryUnmarshaler{ + ctx: ctx, reader: duplexCall, codec: c.Codec, bufferPool: c.BufferPool, @@ -311,6 +390,15 @@ func (c *connectClient) NewConn( responseHeader: make(http.Header), responseTrailer: make(http.Header), } + if spec.IdempotencyLevel == IdempotencyNoSideEffects { + unaryConn.marshaler.enableGet = c.EnableGet + unaryConn.marshaler.getURLMaxBytes = c.GetURLMaxBytes + unaryConn.marshaler.getUseFallback = c.GetUseFallback + unaryConn.marshaler.duplexCall = duplexCall + if stableCodec, ok := c.Codec.(stableCodec); ok { + unaryConn.marshaler.stableCodec = stableCodec + } + } conn = unaryConn duplexCall.SetValidateResponse(unaryConn.validateResponse) } else { @@ -323,7 +411,8 @@ func (c *connectClient) NewConn( codec: c.Codec, marshaler: connectStreamingMarshaler{ envelopeWriter: envelopeWriter{ - writer: duplexCall, + ctx: ctx, + sender: duplexCall, codec: c.Codec, compressMinBytes: c.CompressMinBytes, compressionPool: c.CompressionPools.Get(c.CompressionName), @@ -333,6 +422,7 @@ func (c *connectClient) NewConn( }, unmarshaler: connectStreamingUnmarshaler{ envelopeReader: envelopeReader{ + ctx: ctx, reader: duplexCall, codec: c.Codec, bufferPool: c.BufferPool, @@ -354,7 +444,7 @@ type connectUnaryClientConn struct { duplexCall *duplexHTTPCall compressionPools readOnlyCompressionPools bufferPool *bufferPool - marshaler connectUnaryMarshaler + marshaler connectUnaryRequestMarshaler unmarshaler connectUnaryUnmarshaler responseHeader http.Header responseTrailer http.Header @@ -384,7 +474,9 @@ func (cc *connectUnaryClientConn) CloseRequest() error { } func (cc *connectUnaryClientConn) Receive(msg any) error { - cc.duplexCall.BlockUntilResponseReady() + if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { + return err + } if err := cc.unmarshaler.Unmarshal(msg); err != nil { return err } @@ -392,12 +484,12 @@ func (cc *connectUnaryClientConn) Receive(msg any) error { } func (cc *connectUnaryClientConn) ResponseHeader() http.Header { - cc.duplexCall.BlockUntilResponseReady() + _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseHeader } func (cc *connectUnaryClientConn) ResponseTrailer() http.Header { - cc.duplexCall.BlockUntilResponseReady() + _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseTrailer } @@ -405,15 +497,33 @@ func (cc *connectUnaryClientConn) CloseResponse() error { return cc.duplexCall.CloseRead() } +func (cc *connectUnaryClientConn) onRequestSend(fn func(*http.Request)) { + cc.duplexCall.onRequestSend = fn +} + func (cc *connectUnaryClientConn) validateResponse(response *http.Response) *Error { for k, v := range response.Header { if !strings.HasPrefix(k, connectUnaryTrailerPrefix) { cc.responseHeader[k] = v continue } - cc.responseTrailer[strings.TrimPrefix(k, connectUnaryTrailerPrefix)] = v + cc.responseTrailer[k[len(connectUnaryTrailerPrefix):]] = v + } + if err := connectValidateUnaryResponseContentType( + cc.marshaler.codec.Name(), + cc.duplexCall.Method(), + response.StatusCode, + response.Status, + getHeaderCanonical(response.Header, headerContentType), + ); err != nil { + if IsNotModifiedError(err) { + // Allow access to response headers for this kind of error. + // RFC 9110 doesn't allow trailers on 304s, so we only need to include headers. + err.meta = cc.responseHeader.Clone() + } + return err } - compression := response.Header.Get(connectUnaryHeaderCompression) + compression := getHeaderCanonical(response.Header, connectUnaryHeaderCompression) if compression != "" && compression != compressionIdentity && !cc.compressionPools.Contains(compression) { @@ -424,19 +534,25 @@ func (cc *connectUnaryClientConn) validateResponse(response *http.Response) *Err cc.compressionPools.CommaSeparatedNames(), ) } + cc.unmarshaler.compressionPool = cc.compressionPools.Get(compression) if response.StatusCode != http.StatusOK { unmarshaler := connectUnaryUnmarshaler{ + ctx: cc.unmarshaler.ctx, reader: response.Body, - compressionPool: cc.compressionPools.Get(compression), + compressionPool: cc.unmarshaler.compressionPool, bufferPool: cc.bufferPool, } var wireErr connectWireError if err := unmarshaler.UnmarshalFunc(&wireErr, json.Unmarshal); err != nil { return NewError( - connectHTTPToCode(response.StatusCode), + httpToCode(response.StatusCode), errors.New(response.Status), ) } + if wireErr.Code == 0 { + // code not set? default to one implied by HTTP status + wireErr.Code = httpToCode(response.StatusCode) + } serverErr := wireErr.asError() if serverErr == nil { return nil @@ -445,7 +561,6 @@ func (cc *connectUnaryClientConn) validateResponse(response *http.Response) *Err mergeHeaders(serverErr.meta, cc.responseTrailer) return serverErr } - cc.unmarshaler.compressionPool = cc.compressionPools.Get(compression) return nil } @@ -486,7 +601,9 @@ func (cc *connectStreamingClientConn) CloseRequest() error { } func (cc *connectStreamingClientConn) Receive(msg any) error { - cc.duplexCall.BlockUntilResponseReady() + if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { + return err + } err := cc.unmarshaler.Unmarshal(msg) if err == nil { return nil @@ -500,24 +617,29 @@ func (cc *connectStreamingClientConn) Receive(msg any) error { // error. serverErr.meta = cc.responseHeader.Clone() mergeHeaders(serverErr.meta, cc.responseTrailer) - cc.duplexCall.SetError(serverErr) + _ = cc.duplexCall.CloseWrite() return serverErr } + // If the error is EOF but not from a last message, we want to return + // io.ErrUnexpectedEOF instead. + if errors.Is(err, io.EOF) && !errors.Is(err, errSpecialEnvelope) { + err = errorf(CodeInternal, "protocol error: %w", io.ErrUnexpectedEOF) + } // There's no error in the trailers, so this was probably an error // converting the bytes to a message, an error reading from the network, or // just an EOF. We're going to return it to the user, but we also want to - // setResponseError so Send errors out. - cc.duplexCall.SetError(err) + // close the writer so Send errors out. + _ = cc.duplexCall.CloseWrite() return err } func (cc *connectStreamingClientConn) ResponseHeader() http.Header { - cc.duplexCall.BlockUntilResponseReady() + _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseHeader } func (cc *connectStreamingClientConn) ResponseTrailer() http.Header { - cc.duplexCall.BlockUntilResponseReady() + _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseTrailer } @@ -525,11 +647,22 @@ func (cc *connectStreamingClientConn) CloseResponse() error { return cc.duplexCall.CloseRead() } +func (cc *connectStreamingClientConn) onRequestSend(fn func(*http.Request)) { + cc.duplexCall.onRequestSend = fn +} + func (cc *connectStreamingClientConn) validateResponse(response *http.Response) *Error { if response.StatusCode != http.StatusOK { - return errorf(connectHTTPToCode(response.StatusCode), "HTTP status %v", response.Status) + return errorf(httpToCode(response.StatusCode), "HTTP status %v", response.Status) + } + if err := connectValidateStreamResponseContentType( + cc.codec.Name(), + cc.spec.StreamType, + getHeaderCanonical(response.Header, headerContentType), + ); err != nil { + return err } - compression := response.Header.Get(connectStreamingHeaderCompression) + compression := getHeaderCanonical(response.Header, connectStreamingHeaderCompression) if compression != "" && compression != compressionIdentity && !cc.compressionPools.Contains(compression) { @@ -553,7 +686,6 @@ type connectUnaryHandlerConn struct { marshaler connectUnaryMarshaler unmarshaler connectUnaryUnmarshaler responseTrailer http.Header - wroteBody bool } func (hc *connectUnaryHandlerConn) Spec() Spec { @@ -576,8 +708,7 @@ func (hc *connectUnaryHandlerConn) RequestHeader() http.Header { } func (hc *connectUnaryHandlerConn) Send(msg any) error { - hc.wroteBody = true - hc.writeResponseHeader(nil /* error */) + hc.mergeResponseHeader(nil /* error */) if err := hc.marshaler.Marshal(msg); err != nil { return err } @@ -593,14 +724,20 @@ func (hc *connectUnaryHandlerConn) ResponseTrailer() http.Header { } func (hc *connectUnaryHandlerConn) Close(err error) error { - if !hc.wroteBody { - hc.writeResponseHeader(err) + if !hc.marshaler.wroteHeader { + hc.mergeResponseHeader(err) + // If the handler received a GET request and the resource hasn't changed, + // return a 304. + if len(hc.peer.Query) > 0 && IsNotModifiedError(err) { + hc.responseWriter.WriteHeader(http.StatusNotModified) + return hc.request.Body.Close() + } } - if err == nil { + if err == nil || hc.marshaler.wroteHeader { return hc.request.Body.Close() } // In unary Connect, errors always use application/json. - hc.responseWriter.Header().Set(headerContentType, connectUnaryContentTypeJSON) + setHeaderCanonical(hc.responseWriter.Header(), headerContentType, connectUnaryContentTypeJSON) hc.responseWriter.WriteHeader(connectCodeToHTTP(CodeOf(err))) data, marshalErr := json.Marshal(newConnectWireError(err)) if marshalErr != nil { @@ -614,11 +751,21 @@ func (hc *connectUnaryHandlerConn) Close(err error) error { return hc.request.Body.Close() } -func (hc *connectUnaryHandlerConn) writeResponseHeader(err error) { +func (hc *connectUnaryHandlerConn) getHTTPMethod() string { + return hc.request.Method +} + +func (hc *connectUnaryHandlerConn) mergeResponseHeader(err error) { header := hc.responseWriter.Header() + if hc.request.Method == http.MethodGet { + // The response content varies depending on the compression that the client + // requested (if any). GETs are potentially cacheable, so we should ensure + // that the Vary header includes at least Accept-Encoding (and not overwrite any values already set). + header[headerVary] = append(header[headerVary], connectUnaryHeaderAcceptCompression) + } if err != nil { - if connectErr, ok := asError(err); ok { - mergeHeaders(header, connectErr.meta) + if connectErr, ok := asError(err); ok && !connectErr.wireErr { + mergeMetadataHeaders(header, connectErr.meta) } } for k, v := range hc.responseTrailer { @@ -702,8 +849,8 @@ func (m *connectStreamingMarshaler) MarshalEndStream(err error, trailer http.Hea end := &connectEndStreamMessage{Trailer: trailer} if err != nil { end.Error = newConnectWireError(err) - if connectErr, ok := asError(err); ok { - mergeHeaders(end.Trailer, connectErr.meta) + if connectErr, ok := asError(err); ok && !connectErr.wireErr { + mergeMetadataHeaders(end.Trailer, connectErr.meta) } } data, marshalErr := json.Marshal(end) @@ -733,14 +880,24 @@ func (u *connectStreamingUnmarshaler) Unmarshal(message any) *Error { if !errors.Is(err, errSpecialEnvelope) { return err } - env := u.envelopeReader.last + env := u.last + data := env.Data + u.last.Data = nil // don't keep a reference to it + defer u.bufferPool.Put(data) if !env.IsSet(connectFlagEnvelopeEndStream) { return errorf(CodeInternal, "protocol error: invalid envelope flags %d", env.Flags) } var end connectEndStreamMessage - if err := json.Unmarshal(env.Data.Bytes(), &end); err != nil { + if err := json.Unmarshal(data.Bytes(), &end); err != nil { return errorf(CodeInternal, "unmarshal end stream message: %w", err) } + for name, value := range end.Trailer { + canonical := http.CanonicalHeaderKey(name) + if name != canonical { + delHeaderCanonical(end.Trailer, name) + end.Trailer[canonical] = append(end.Trailer[canonical], value...) + } + } u.trailer = end.Trailer u.endStreamErr = end.Error.asError() return errSpecialEnvelope @@ -755,7 +912,8 @@ func (u *connectStreamingUnmarshaler) EndStreamError() *Error { } type connectUnaryMarshaler struct { - writer io.Writer + ctx context.Context //nolint:containedctx + sender messageSender codec Codec compressMinBytes int compressionName string @@ -763,17 +921,24 @@ type connectUnaryMarshaler struct { bufferPool *bufferPool header http.Header sendMaxBytes int + wroteHeader bool } func (m *connectUnaryMarshaler) Marshal(message any) *Error { if message == nil { return m.write(nil) } - data, err := m.codec.Marshal(message) + var data []byte + var err error + if appender, ok := m.codec.(marshalAppender); ok { + data, err = appender.MarshalAppend(m.bufferPool.Get().Bytes(), message) + } else { + // Can't avoid allocating the slice, but we'll reuse it. + data, err = m.codec.Marshal(message) + } if err != nil { return errorf(CodeInternal, "marshal message: %w", err) } - // Can't avoid allocating the slice, but we can reuse it. uncompressed := bytes.NewBuffer(data) defer m.bufferPool.Put(uncompressed) if len(data) < m.compressMinBytes || m.compressionPool == nil { @@ -790,12 +955,15 @@ func (m *connectUnaryMarshaler) Marshal(message any) *Error { if m.sendMaxBytes > 0 && compressed.Len() > m.sendMaxBytes { return NewError(CodeResourceExhausted, fmt.Errorf("compressed message size %d exceeds sendMaxBytes %d", compressed.Len(), m.sendMaxBytes)) } - m.header.Set(connectUnaryHeaderCompression, m.compressionName) + setHeaderCanonical(m.header, connectUnaryHeaderCompression, m.compressionName) return m.write(compressed.Bytes()) } func (m *connectUnaryMarshaler) write(data []byte) *Error { - if _, err := m.writer.Write(data); err != nil { + m.wroteHeader = true + payload := bytes.NewReader(data) + if _, err := m.sender.Send(payload); err != nil { + err = wrapIfContextError(err) if connectErr, ok := asError(err); ok { return connectErr } @@ -804,7 +972,115 @@ func (m *connectUnaryMarshaler) write(data []byte) *Error { return nil } +type connectUnaryRequestMarshaler struct { + connectUnaryMarshaler + + enableGet bool + getURLMaxBytes int + getUseFallback bool + stableCodec stableCodec + duplexCall *duplexHTTPCall +} + +func (m *connectUnaryRequestMarshaler) Marshal(message any) *Error { + if m.enableGet { + if m.stableCodec == nil && !m.getUseFallback { + return errorf(CodeInternal, "codec %s doesn't support stable marshal; can't use get", m.codec.Name()) + } + if m.stableCodec != nil { + return m.marshalWithGet(message) + } + } + return m.connectUnaryMarshaler.Marshal(message) +} + +func (m *connectUnaryRequestMarshaler) marshalWithGet(message any) *Error { + // TODO(jchadwick-buf): This function is mostly a superset of + // connectUnaryMarshaler.Marshal. This should be reconciled at some point. + var data []byte + var err error + if message != nil { + data, err = m.stableCodec.MarshalStable(message) + if err != nil { + return errorf(CodeInternal, "marshal message stable: %w", err) + } + } + isTooBig := m.sendMaxBytes > 0 && len(data) > m.sendMaxBytes + if isTooBig && m.compressionPool == nil { + return NewError(CodeResourceExhausted, fmt.Errorf( + "message size %d exceeds sendMaxBytes %d: enabling request compression may help", + len(data), + m.sendMaxBytes, + )) + } + if !isTooBig { + url := m.buildGetURL(data, false /* compressed */) + if m.getURLMaxBytes <= 0 || len(url.String()) < m.getURLMaxBytes { + return m.writeWithGet(url) + } + if m.compressionPool == nil { + if m.getUseFallback { + return m.write(data) + } + return NewError(CodeResourceExhausted, fmt.Errorf( + "url size %d exceeds getURLMaxBytes %d: enabling request compression may help", + len(url.String()), + m.getURLMaxBytes, + )) + } + } + // Compress message to try to make it fit in the URL. + uncompressed := bytes.NewBuffer(data) + defer m.bufferPool.Put(uncompressed) + compressed := m.bufferPool.Get() + defer m.bufferPool.Put(compressed) + if err := m.compressionPool.Compress(compressed, uncompressed); err != nil { + return err + } + if m.sendMaxBytes > 0 && compressed.Len() > m.sendMaxBytes { + return NewError(CodeResourceExhausted, fmt.Errorf("compressed message size %d exceeds sendMaxBytes %d", compressed.Len(), m.sendMaxBytes)) + } + url := m.buildGetURL(compressed.Bytes(), true /* compressed */) + if m.getURLMaxBytes <= 0 || len(url.String()) < m.getURLMaxBytes { + return m.writeWithGet(url) + } + if m.getUseFallback { + setHeaderCanonical(m.header, connectUnaryHeaderCompression, m.compressionName) + return m.write(compressed.Bytes()) + } + return NewError(CodeResourceExhausted, fmt.Errorf("compressed url size %d exceeds getURLMaxBytes %d", len(url.String()), m.getURLMaxBytes)) +} + +func (m *connectUnaryRequestMarshaler) buildGetURL(data []byte, compressed bool) *url.URL { + url := *m.duplexCall.URL() + query := url.Query() + query.Set(connectUnaryConnectQueryParameter, connectUnaryConnectQueryValue) + query.Set(connectUnaryEncodingQueryParameter, m.codec.Name()) + if m.stableCodec.IsBinary() || compressed { + query.Set(connectUnaryMessageQueryParameter, encodeBinaryQueryValue(data)) + query.Set(connectUnaryBase64QueryParameter, "1") + } else { + query.Set(connectUnaryMessageQueryParameter, string(data)) + } + if compressed { + query.Set(connectUnaryCompressionQueryParameter, m.compressionName) + } + url.RawQuery = query.Encode() + return &url +} + +func (m *connectUnaryRequestMarshaler) writeWithGet(url *url.URL) *Error { + delHeaderCanonical(m.header, connectHeaderProtocolVersion) + delHeaderCanonical(m.header, headerContentType) + delHeaderCanonical(m.header, headerContentEncoding) + delHeaderCanonical(m.header, headerContentLength) + m.duplexCall.SetMethod(http.MethodGet) + *m.duplexCall.URL() = *url + return nil +} + type connectUnaryUnmarshaler struct { + ctx context.Context //nolint:containedctx reader io.Reader codec Codec compressionPool *compressionPool @@ -831,12 +1107,11 @@ func (u *connectUnaryUnmarshaler) UnmarshalFunc(message any, unmarshal func([]by // ReadFrom ignores io.EOF, so any error here is real. bytesRead, err := data.ReadFrom(reader) if err != nil { + err = wrapIfMaxBytesError(err, "read first %d bytes of message", bytesRead) + err = wrapIfContextDone(u.ctx, err) if connectErr, ok := asError(err); ok { return connectErr } - if readMaxBytesErr := asMaxBytesError(err, "read first %d bytes of message", bytesRead); readMaxBytesErr != nil { - return readMaxBytesErr - } return errorf(CodeUnknown, "read message: %w", err) } if u.readMaxBytes > 0 && bytesRead > int64(u.readMaxBytes) { @@ -856,7 +1131,7 @@ func (u *connectUnaryUnmarshaler) UnmarshalFunc(message any, unmarshal func([]by data = decompressed } if err := unmarshal(data.Bytes(), message); err != nil { - return errorf(CodeInvalidArgument, "unmarshal into %T: %w", message, err) + return errorf(CodeInvalidArgument, "unmarshal message: %w", err) } return nil } @@ -874,15 +1149,18 @@ func (d *connectWireDetail) MarshalJSON() ([]byte, error) { Value string `json:"value"` Debug json.RawMessage `json:"debug,omitempty"` }{ - Type: strings.TrimPrefix(d.pb.TypeUrl, defaultAnyResolverPrefix), - Value: base64.RawStdEncoding.EncodeToString(d.pb.Value), + Type: typeNameFromURL(d.pbAny.GetTypeUrl()), + Value: base64.RawStdEncoding.EncodeToString(d.pbAny.GetValue()), } // Try to produce debug info, but expect failure when we don't have // descriptors. - var codec protoJSONCodec - debug, err := codec.Marshal(d.pb) - if err == nil && len(debug) > 2 { // don't bother sending `{}` - wire.Debug = json.RawMessage(debug) + msg, err := d.getInner() + if err == nil { + var codec protoJSONCodec + debug, err := codec.Marshal(msg) + if err == nil { + wire.Debug = debug + } } return json.Marshal(wire) } @@ -903,7 +1181,7 @@ func (d *connectWireDetail) UnmarshalJSON(data []byte) error { return fmt.Errorf("decode base64: %w", err) } *d = connectWireDetail{ - pb: &anypb.Any{ + pbAny: &anypb.Any{ TypeUrl: wire.Type, Value: decoded, }, @@ -912,6 +1190,13 @@ func (d *connectWireDetail) UnmarshalJSON(data []byte) error { return nil } +func (d *connectWireDetail) getInner() (proto.Message, error) { + if d.pbInner != nil { + return d.pbInner, nil + } + return d.pbAny.UnmarshalNew() +} + type connectWireError struct { Code Code `json:"code"` Message string `json:"message,omitempty"` @@ -943,8 +1228,7 @@ func (e *connectWireError) asError() *Error { if e.Code < minCode || e.Code > maxCode { e.Code = CodeUnknown } - err := NewError(e.Code, errors.New(e.Message)) - err.wireErr = true + err := NewWireError(e.Code, errors.New(e.Message)) if len(e.Details) > 0 { err.details = make([]*ErrorDetail, len(e.Details)) for i, detail := range e.Details { @@ -954,6 +1238,26 @@ func (e *connectWireError) asError() *Error { return err } +func (e *connectWireError) UnmarshalJSON(data []byte) error { + // We want to be lenient if the JSON has an unrecognized or invalid code. + // So if that occurs, we leave the code unset but can still de-serialize + // the other fields from the input JSON. + var wireError struct { + Code string `json:"code"` + Message string `json:"message"` + Details []*connectWireDetail `json:"details"` + } + err := json.Unmarshal(data, &wireError) + if err != nil { + return err + } + e.Message = wireError.Message + e.Details = wireError.Details + // This will leave e.Code unset if we can't unmarshal the given string. + _ = e.Code.UnmarshalText([]byte(wireError.Code)) + return nil +} + type connectEndStreamMessage struct { Error *connectWireError `json:"error,omitempty"` Trailer http.Header `json:"metadata,omitempty"` @@ -964,13 +1268,13 @@ func connectCodeToHTTP(code Code) int { // it easier to compare this function to the Connect specification. switch code { case CodeCanceled: - return 408 + return 499 case CodeUnknown: return 500 case CodeInvalidArgument: return 400 case CodeDeadlineExceeded: - return 408 + return 504 case CodeNotFound: return 404 case CodeAlreadyExists: @@ -980,13 +1284,13 @@ func connectCodeToHTTP(code Code) int { case CodeResourceExhausted: return 429 case CodeFailedPrecondition: - return 412 + return 400 case CodeAborted: return 409 case CodeOutOfRange: return 400 case CodeUnimplemented: - return 404 + return 501 case CodeInternal: return 500 case CodeUnavailable: @@ -1000,40 +1304,6 @@ func connectCodeToHTTP(code Code) int { } } -func connectHTTPToCode(httpCode int) Code { - // As above, literals are easier to compare to the specificaton (vs named - // constants). - switch httpCode { - case 400: - return CodeInvalidArgument - case 401: - return CodeUnauthenticated - case 403: - return CodePermissionDenied - case 404: - return CodeUnimplemented - case 408: - return CodeDeadlineExceeded - case 412: - return CodeFailedPrecondition - case 413: - return CodeResourceExhausted - case 429: - return CodeUnavailable - case 431: - return CodeResourceExhausted - case 502, 503, 504: - return CodeUnavailable - default: - return CodeUnknown - } -} - -// connectUserAgent returns a User-Agent string similar to those used in gRPC. -func connectUserAgent() string { - return fmt.Sprintf("connect-go/%s (%s)", Version, runtime.Version()) -} - func connectCodecFromContentType(streamType StreamType, contentType string) string { if streamType == StreamTypeUnary { return strings.TrimPrefix(contentType, connectUnaryContentTypePrefix) @@ -1047,3 +1317,130 @@ func connectContentTypeFromCodecName(streamType StreamType, name string) string } return connectStreamingContentTypePrefix + name } + +// encodeBinaryQueryValue URL-safe base64-encodes data, without padding. +func encodeBinaryQueryValue(data []byte) string { + return base64.RawURLEncoding.EncodeToString(data) +} + +// binaryQueryValueReader creates a reader that can read either padded or +// unpadded URL-safe base64 from a string. +func binaryQueryValueReader(data string) io.Reader { + stringReader := strings.NewReader(data) + if len(data)%4 != 0 { + // Data definitely isn't padded. + return base64.NewDecoder(base64.RawURLEncoding, stringReader) + } + // Data is padded, or no padding was necessary. + return base64.NewDecoder(base64.URLEncoding, stringReader) +} + +// queryValueReader creates a reader for a string that may be URL-safe base64 +// encoded. +func queryValueReader(data string, base64Encoded bool) io.Reader { + if base64Encoded { + return binaryQueryValueReader(data) + } + return strings.NewReader(data) +} + +func connectValidateUnaryResponseContentType( + requestCodecName string, + httpMethod string, + statusCode int, + statusMsg string, + responseContentType string, +) *Error { + if statusCode != http.StatusOK { + if statusCode == http.StatusNotModified && httpMethod == http.MethodGet { + return NewWireError(CodeUnknown, errNotModifiedClient) + } + // Error responses must be JSON-encoded. + if responseContentType == connectUnaryContentTypePrefix+codecNameJSON || + responseContentType == connectUnaryContentTypePrefix+codecNameJSONCharsetUTF8 { + return nil + } + return NewError( + httpToCode(statusCode), + errors.New(statusMsg), + ) + } + // Normal responses must have valid content-type that indicates same codec as the request. + if !strings.HasPrefix(responseContentType, connectUnaryContentTypePrefix) { + // Doesn't even look like a Connect response? Use code "unknown". + return errorf( + CodeUnknown, + "invalid content-type: %q; expecting %q", + responseContentType, + connectUnaryContentTypePrefix+requestCodecName, + ) + } + responseCodecName := connectCodecFromContentType( + StreamTypeUnary, + responseContentType, + ) + if responseCodecName == requestCodecName { + return nil + } + // HACK: We likely want a better way to handle the optional "charset" parameter + // for application/json, instead of hard-coding. But this suffices for now. + if (responseCodecName == codecNameJSON && requestCodecName == codecNameJSONCharsetUTF8) || + (responseCodecName == codecNameJSONCharsetUTF8 && requestCodecName == codecNameJSON) { + // Both are JSON + return nil + } + return errorf( + CodeInternal, + "invalid content-type: %q; expecting %q", + responseContentType, + connectUnaryContentTypePrefix+requestCodecName, + ) +} + +func connectValidateStreamResponseContentType(requestCodecName string, streamType StreamType, responseContentType string) *Error { + // Responses must have valid content-type that indicates same codec as the request. + if !strings.HasPrefix(responseContentType, connectStreamingContentTypePrefix) { + // Doesn't even look like a Connect response? Use code "unknown". + return errorf( + CodeUnknown, + "invalid content-type: %q; expecting %q", + responseContentType, + connectUnaryContentTypePrefix+requestCodecName, + ) + } + responseCodecName := connectCodecFromContentType( + streamType, + responseContentType, + ) + if responseCodecName != requestCodecName { + return errorf( + CodeInternal, + "invalid content-type: %q; expecting %q", + responseContentType, + connectStreamingContentTypePrefix+requestCodecName, + ) + } + return nil +} + +func connectCheckProtocolVersion(request *http.Request, required bool) *Error { + switch request.Method { + case http.MethodGet: + version := request.URL.Query().Get(connectUnaryConnectQueryParameter) + if version == "" && required { + return errorf(CodeInvalidArgument, "missing required query parameter: set %s to %q", connectUnaryConnectQueryParameter, connectUnaryConnectQueryValue) + } else if version != "" && version != connectUnaryConnectQueryValue { + return errorf(CodeInvalidArgument, "%s must be %q: got %q", connectUnaryConnectQueryParameter, connectUnaryConnectQueryValue, version) + } + case http.MethodPost: + version := getHeaderCanonical(request.Header, connectHeaderProtocolVersion) + if version == "" && required { + return errorf(CodeInvalidArgument, "missing required header: set %s to %q", connectHeaderProtocolVersion, connectProtocolVersion) + } else if version != "" && version != connectProtocolVersion { + return errorf(CodeInvalidArgument, "%s must be %q: got %q", connectHeaderProtocolVersion, connectProtocolVersion, version) + } + default: + return errorf(CodeInvalidArgument, "unsupported method: %q", request.Method) + } + return nil +} diff --git a/vendor/github.com/bufbuild/connect-go/protocol_grpc.go b/vendor/connectrpc.com/connect/protocol_grpc.go similarity index 60% rename from vendor/github.com/bufbuild/connect-go/protocol_grpc.go rename to vendor/connectrpc.com/connect/protocol_grpc.go index 35235468..32b116f4 100644 --- a/vendor/github.com/bufbuild/connect-go/protocol_grpc.go +++ b/vendor/connectrpc.com/connect/protocol_grpc.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,9 +27,8 @@ import ( "strconv" "strings" "time" - "unicode/utf8" - statusv1 "github.com/bufbuild/connect-go/internal/gen/connectext/grpc/status/v1" + statusv1 "connectrpc.com/connect/internal/gen/connectext/grpc/status/v1" ) const ( @@ -42,36 +41,33 @@ const ( grpcFlagEnvelopeTrailer = 0b10000000 - grpcTimeoutMaxHours = math.MaxInt64 / int64(time.Hour) // how many hours fit into a time.Duration? - grpcMaxTimeoutChars = 8 // from gRPC protocol - grpcContentTypeDefault = "application/grpc" grpcWebContentTypeDefault = "application/grpc-web" grpcContentTypePrefix = grpcContentTypeDefault + "+" grpcWebContentTypePrefix = grpcWebContentTypeDefault + "+" + + headerXUserAgent = "X-User-Agent" + + upperhex = "0123456789ABCDEF" ) var ( - grpcTimeoutUnits = []struct { - size time.Duration - char byte - }{ - {time.Nanosecond, 'n'}, - {time.Microsecond, 'u'}, - {time.Millisecond, 'm'}, - {time.Second, 'S'}, - {time.Minute, 'M'}, - {time.Hour, 'H'}, - } - grpcTimeoutUnitLookup = make(map[byte]time.Duration) - errTrailersWithoutGRPCStatus = fmt.Errorf("gRPC protocol error: no %s trailer", grpcHeaderStatus) -) + errTrailersWithoutGRPCStatus = fmt.Errorf("protocol error: no %s trailer: %w", grpcHeaderStatus, io.ErrUnexpectedEOF) -func init() { - for _, pair := range grpcTimeoutUnits { - grpcTimeoutUnitLookup[pair.char] = pair.size + // defaultGrpcUserAgent follows + // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#user-agents: + // + // While the protocol does not require a user-agent to function it is recommended + // that clients provide a structured user-agent string that provides a basic + // description of the calling library, version & platform to facilitate issue diagnosis + // in heterogeneous environments. The following structure is recommended to library developers: + // + // User-Agent → "grpc-" Language ?("-" Variant) "/" Version ?( " (" *(AdditionalProperty ";") ")" ) + defaultGrpcUserAgent = fmt.Sprintf("grpc-go-connect/%s (%s)", Version, runtime.Version()) + grpcAllowedMethods = map[string]struct{}{ + http.MethodPost: {}, } -} +) type protocolGRPC struct { web bool @@ -85,7 +81,7 @@ func (g *protocolGRPC) NewHandler(params *protocolHandlerParams) protocolHandler } contentTypes := make(map[string]struct{}) for _, name := range params.Codecs.Names() { - contentTypes[prefix+name] = struct{}{} + contentTypes[canonicalizeContentType(prefix+name)] = struct{}{} } if params.Codecs.Get(codecNameProto) != nil { contentTypes[bare] = struct{}{} @@ -99,12 +95,14 @@ func (g *protocolGRPC) NewHandler(params *protocolHandlerParams) protocolHandler // NewClient implements protocol, so it must return an interface. func (g *protocolGRPC) NewClient(params *protocolClientParams) (protocolClient, error) { - if err := validateRequestURL(params.URL); err != nil { - return nil, err + peer := newPeerFromURL(params.URL, ProtocolGRPC) + if g.web { + peer = newPeerFromURL(params.URL, ProtocolGRPCWeb) } return &grpcClient{ protocolClientParams: *params, web: g.web, + peer: peer, }, nil } @@ -115,12 +113,16 @@ type grpcHandler struct { accept map[string]struct{} } +func (g *grpcHandler) Methods() map[string]struct{} { + return grpcAllowedMethods +} + func (g *grpcHandler) ContentTypes() map[string]struct{} { return g.accept } func (*grpcHandler) SetTimeout(request *http.Request) (context.Context, context.CancelFunc, error) { - timeout, err := grpcParseTimeout(request.Header.Get(grpcHeaderTimeout)) + timeout, err := grpcParseTimeout(getHeaderCanonical(request.Header, grpcHeaderTimeout)) if err != nil && !errors.Is(err, errNoTimeout) { // Errors here indicate that the client sent an invalid timeout header, so // the error text is safe to send back. @@ -133,16 +135,22 @@ func (*grpcHandler) SetTimeout(request *http.Request) (context.Context, context. return ctx, cancel, nil } +func (g *grpcHandler) CanHandlePayload(_ *http.Request, contentType string) bool { + _, ok := g.accept[contentType] + return ok +} + func (g *grpcHandler) NewConn( responseWriter http.ResponseWriter, request *http.Request, ) (handlerConnCloser, bool) { + ctx := request.Context() // We need to parse metadata before entering the interceptor stack; we'll // send the error to the client later on. requestCompression, responseCompression, failed := negotiateCompression( g.CompressionPools, - request.Header.Get(grpcHeaderCompression), - request.Header.Get(grpcHeaderAcceptCompression), + getHeaderCanonical(request.Header, grpcHeaderCompression), + getHeaderCanonical(request.Header, grpcHeaderAcceptCompression), ) if failed == nil { failed = checkServerStreamsCanFlush(g.Spec, responseWriter) @@ -156,13 +164,13 @@ func (g *grpcHandler) NewConn( // Since we know that these header keys are already in canonical form, we can // skip the normalization in Header.Set. header := responseWriter.Header() - header[headerContentType] = []string{request.Header.Get(headerContentType)} + header[headerContentType] = []string{getHeaderCanonical(request.Header, headerContentType)} header[grpcHeaderAcceptCompression] = []string{g.CompressionPools.CommaSeparatedNames()} if responseCompression != compressionIdentity { header[grpcHeaderCompression] = []string{responseCompression} } - codecName := grpcCodecFromContentType(g.web, request.Header.Get(headerContentType)) + codecName := grpcCodecFromContentType(g.web, getHeaderCanonical(request.Header, headerContentType)) codec := g.Codecs.Get(codecName) // handler.go guarantees this is not nil protocolName := ProtocolGRPC if g.web { @@ -179,7 +187,8 @@ func (g *grpcHandler) NewConn( protobuf: g.Codecs.Protobuf(), // for errors marshaler: grpcMarshaler{ envelopeWriter: envelopeWriter{ - writer: responseWriter, + ctx: ctx, + sender: writeSender{writer: responseWriter}, compressionPool: g.CompressionPools.Get(responseCompression), codec: codec, compressMinBytes: g.CompressMinBytes, @@ -193,6 +202,7 @@ func (g *grpcHandler) NewConn( request: request, unmarshaler: grpcUnmarshaler{ envelopeReader: envelopeReader{ + ctx: ctx, reader: request.Body, codec: codec, compressionPool: g.CompressionPools.Get(requestCompression), @@ -213,21 +223,26 @@ func (g *grpcHandler) NewConn( type grpcClient struct { protocolClientParams - web bool + web bool + peer Peer } func (g *grpcClient) Peer() Peer { - if g.web { - return newPeerFromURL(g.URL, ProtocolGRPCWeb) - } - return newPeerFromURL(g.URL, ProtocolGRPC) + return g.peer } func (g *grpcClient) WriteRequestHeader(_ StreamType, header http.Header) { // We know these header keys are in canonical form, so we can bypass all the // checks in Header.Set. - if header.Get(headerUserAgent) == "" { - header[headerUserAgent] = []string{grpcUserAgent()} + if getHeaderCanonical(header, headerUserAgent) == "" { + header[headerUserAgent] = []string{defaultGrpcUserAgent} + } + if g.web && getHeaderCanonical(header, headerXUserAgent) == "" { + // The gRPC-Web pseudo-specification seems to require X-User-Agent rather + // than User-Agent for all clients, even if they're not browser-based. This + // is very odd for a backend client, so we'll split the difference and set + // both. + header[headerXUserAgent] = []string{defaultGrpcUserAgent} } header[headerContentType] = []string{grpcContentTypeFromCodecName(g.web, g.Codec.Name())} // gRPC handles compression on a per-message basis, so we don't want to @@ -251,13 +266,10 @@ func (g *grpcClient) NewConn( ctx context.Context, spec Spec, header http.Header, -) StreamingClientConn { +) streamingClientConn { if deadline, ok := ctx.Deadline(); ok { - if encodedDeadline, err := grpcEncodeTimeout(time.Until(deadline)); err == nil { - // Tests verify that the error in encodeTimeout is unreachable, so we - // don't need to handle the error case. - header[grpcHeaderTimeout] = []string{encodedDeadline} - } + encodedDeadline := grpcEncodeTimeout(time.Until(deadline)) + header[grpcHeaderTimeout] = []string{encodedDeadline} } duplexCall := newDuplexHTTPCall( ctx, @@ -275,7 +287,8 @@ func (g *grpcClient) NewConn( protobuf: g.Protobuf, marshaler: grpcMarshaler{ envelopeWriter: envelopeWriter{ - writer: duplexCall, + ctx: ctx, + sender: duplexCall, compressionPool: g.CompressionPools.Get(g.CompressionName), codec: g.Codec, compressMinBytes: g.CompressMinBytes, @@ -285,6 +298,7 @@ func (g *grpcClient) NewConn( }, unmarshaler: grpcUnmarshaler{ envelopeReader: envelopeReader{ + ctx: ctx, reader: duplexCall, codec: g.Codec, bufferPool: g.BufferPool, @@ -303,7 +317,7 @@ func (g *grpcClient) NewConn( } else { conn.readTrailers = func(_ *grpcUnmarshaler, call *duplexHTTPCall) http.Header { // To access HTTP trailers, we need to read the body to EOF. - _ = discard(call) + _, _ = discard(call) return call.ResponseTrailer() } } @@ -349,23 +363,39 @@ func (cc *grpcClientConn) CloseRequest() error { } func (cc *grpcClientConn) Receive(msg any) error { - cc.duplexCall.BlockUntilResponseReady() + if err := cc.duplexCall.BlockUntilResponseReady(); err != nil { + return err + } err := cc.unmarshaler.Unmarshal(msg) if err == nil { return nil } - if cc.responseHeader.Get(grpcHeaderStatus) != "" { - // We got what gRPC calls a trailers-only response, which puts the trailing - // metadata (including errors) into HTTP headers. validateResponse has - // already extracted the error. - return err - } - // See if the server sent an explicit error in the HTTP or gRPC-Web trailers. mergeHeaders( cc.responseTrailer, cc.readTrailers(&cc.unmarshaler, cc.duplexCall), ) - serverErr := grpcErrorFromTrailer(cc.bufferPool, cc.protobuf, cc.responseTrailer) + if errors.Is(err, io.EOF) && cc.unmarshaler.bytesRead == 0 && len(cc.responseTrailer) == 0 { + // No body and no trailers means a trailers-only response. + // Note: per the specification, only the HTTP status code and Content-Type + // should be treated as headers. The rest should be treated as trailing + // metadata. But it would be unsafe to mutate cc.responseHeader at this + // point. So we'll leave cc.responseHeader alone but copy the relevant + // metadata into cc.responseTrailer. + mergeHeaders(cc.responseTrailer, cc.responseHeader) + delHeaderCanonical(cc.responseTrailer, headerContentType) + + // Try to read the status out of the headers. + serverErr := grpcErrorFromTrailer(cc.protobuf, cc.responseHeader) + if serverErr == nil { + // Status says "OK". So return original error (io.EOF). + return err + } + serverErr.meta = cc.responseHeader.Clone() + return serverErr + } + + // See if the server sent an explicit error in the HTTP or gRPC-Web trailers. + serverErr := grpcErrorFromTrailer(cc.protobuf, cc.responseTrailer) if serverErr != nil && (errors.Is(err, io.EOF) || !errors.Is(serverErr, errTrailersWithoutGRPCStatus)) { // We've either: // - Cleanly read until the end of the response body and *not* received @@ -377,23 +407,23 @@ func (cc *grpcClientConn) Receive(msg any) error { // the stream has ended, Receive must return an error. serverErr.meta = cc.responseHeader.Clone() mergeHeaders(serverErr.meta, cc.responseTrailer) - cc.duplexCall.SetError(serverErr) + _ = cc.duplexCall.CloseWrite() return serverErr } // This was probably an error converting the bytes to a message or an error // reading from the network. We're going to return it to the - // user, but we also want to setResponseError so Send errors out. - cc.duplexCall.SetError(err) + // user, but we also want to close writes so Send errors out. + _ = cc.duplexCall.CloseWrite() return err } func (cc *grpcClientConn) ResponseHeader() http.Header { - cc.duplexCall.BlockUntilResponseReady() + _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseHeader } func (cc *grpcClientConn) ResponseTrailer() http.Header { - cc.duplexCall.BlockUntilResponseReady() + _ = cc.duplexCall.BlockUntilResponseReady() return cc.responseTrailer } @@ -401,19 +431,22 @@ func (cc *grpcClientConn) CloseResponse() error { return cc.duplexCall.CloseRead() } +func (cc *grpcClientConn) onRequestSend(fn func(*http.Request)) { + cc.duplexCall.onRequestSend = fn +} + func (cc *grpcClientConn) validateResponse(response *http.Response) *Error { if err := grpcValidateResponse( response, cc.responseHeader, - cc.responseTrailer, cc.compressionPools, - cc.bufferPool, - cc.protobuf, + cc.unmarshaler.web, + cc.marshaler.codec.Name(), ); err != nil { return err } - compression := response.Header.Get(grpcHeaderCompression) - cc.unmarshaler.envelopeReader.compressionPool = cc.compressionPools.Get(compression) + compression := getHeaderCanonical(response.Header, grpcHeaderCompression) + cc.unmarshaler.compressionPool = cc.compressionPools.Get(compression) return nil } @@ -497,15 +530,11 @@ func (hc *grpcHandlerConn) Close(err error) (retErr error) { len(hc.responseTrailer)+2, // always make space for status & message ) mergeHeaders(mergedTrailers, hc.responseTrailer) - grpcErrorToTrailer(hc.bufferPool, mergedTrailers, hc.protobuf, err) - if hc.web && !hc.wroteToBody { - // We're using gRPC-Web and we haven't yet written to the body. Since we're - // not sending any response messages, the gRPC specification calls this a - // "trailers-only" response. Under those circumstances, the gRPC-Web spec - // says that implementations _may_ send trailing metadata as HTTP headers - // instead. Envoy is the canonical implementation of the gRPC-Web protocol, - // so we emulate Envoy's behavior and put the trailing metadata in the HTTP - // headers. + grpcErrorToTrailer(mergedTrailers, hc.protobuf, err) + if hc.web && !hc.wroteToBody && len(hc.responseHeader) == 0 { + // We're using gRPC-Web, we haven't yet written to the body, and there are no + // custom headers. That means we can send a "trailers-only" response and send + // trailing metadata as HTTP headers (instead of as trailers). mergeHeaders(hc.responseWriter.Header(), mergedTrailers) return nil } @@ -522,25 +551,7 @@ func (hc *grpcHandlerConn) Close(err error) (retErr error) { // as HTTP trailers. (If we had frame-level control of the HTTP/2 layer, we // could send trailers-only responses as a single HEADER frame and no DATA // frames, but net/http doesn't expose APIs that low-level.) - if !hc.wroteToBody { - // This block works around a bug in x/net/http2. Until Go 1.20, trailers - // written using http.TrailerPrefix were only sent if either (1) there's - // data in the body, or (2) the innermost http.ResponseWriter is flushed. - // To ensure that we always send a valid gRPC response, even if the user - // has wrapped the response writer in net/http middleware that doesn't - // implement http.Flusher, we must pre-declare our HTTP trailers. We can - // remove this when Go 1.21 ships and we drop support for Go 1.19. - for key := range mergedTrailers { - hc.responseWriter.Header().Add("Trailer", key) - } - hc.responseWriter.WriteHeader(http.StatusOK) - for key, values := range mergedTrailers { - for _, value := range values { - hc.responseWriter.Header().Add(key, value) - } - } - return nil - } + // // In net/http's ResponseWriter API, we send HTTP trailers by writing to the // headers map with a special prefix. This prefixing is an implementation // detail, so we should hide it and _not_ mutate the user-visible headers. @@ -550,6 +561,8 @@ func (hc *grpcHandlerConn) Close(err error) (retErr error) { // logic breaks Envoy's gRPC-Web translation. for key, values := range mergedTrailers { for _, value := range values { + // These are potentially user-supplied, so we can't assume they're in + // canonical form. hc.responseWriter.Header().Add(http.TrailerPrefix+key, value) } } @@ -563,6 +576,17 @@ type grpcMarshaler struct { func (m *grpcMarshaler) MarshalWebTrailers(trailer http.Header) *Error { raw := m.envelopeWriter.bufferPool.Get() defer m.envelopeWriter.bufferPool.Put(raw) + for key, values := range trailer { + // Per the Go specification, keys inserted during iteration may be produced + // later in the iteration or may be skipped. For safety, avoid mutating the + // map if the key is already lower-cased. + lower := strings.ToLower(key) + if key == lower { + continue + } + delete(trailer, key) + trailer[lower] = values + } if err := trailer.Write(raw); err != nil { return errorf(CodeInternal, "format trailers: %w", err) } @@ -573,9 +597,10 @@ func (m *grpcMarshaler) MarshalWebTrailers(trailer http.Header) *Error { } type grpcUnmarshaler struct { - envelopeReader envelopeReader - web bool - webTrailer http.Header + envelopeReader + + web bool + webTrailer http.Header } func (u *grpcUnmarshaler) Unmarshal(message any) *Error { @@ -586,7 +611,10 @@ func (u *grpcUnmarshaler) Unmarshal(message any) *Error { if !errors.Is(err, errSpecialEnvelope) { return err } - env := u.envelopeReader.last + env := u.last + data := env.Data + u.last.Data = nil // don't keep a reference to it + defer u.bufferPool.Put(data) if !u.web || !env.IsSet(grpcFlagEnvelopeTrailer) { return errorf(CodeInternal, "protocol error: invalid envelope flags %d", env.Flags) } @@ -594,10 +622,10 @@ func (u *grpcUnmarshaler) Unmarshal(message any) *Error { // Per the gRPC-Web specification, trailers should be encoded as an HTTP/1 // headers block _without_ the terminating newline. To make the headers // parseable by net/textproto, we need to add the newline. - if err := env.Data.WriteByte('\n'); err != nil { + if err := data.WriteByte('\n'); err != nil { return errorf(CodeInternal, "unmarshal web trailers: %w", err) } - bufferedReader := bufio.NewReader(env.Data) + bufferedReader := bufio.NewReader(data) mimeReader := textproto.NewReader(bufferedReader) mimeHeader, mimeErr := mimeReader.ReadMIMEHeader() if mimeErr != nil { @@ -617,15 +645,22 @@ func (u *grpcUnmarshaler) WebTrailer() http.Header { func grpcValidateResponse( response *http.Response, - header, trailer http.Header, + header http.Header, availableCompressors readOnlyCompressionPools, - bufferPool *bufferPool, - protobuf Codec, + web bool, + codecName string, ) *Error { if response.StatusCode != http.StatusOK { - return errorf(grpcHTTPToCode(response.StatusCode), "HTTP status %v", response.Status) + return errorf(httpToCode(response.StatusCode), "HTTP status %v", response.Status) + } + if err := grpcValidateResponseContentType( + web, + codecName, + getHeaderCanonical(response.Header, headerContentType), + ); err != nil { + return err } - if compression := response.Header.Get(grpcHeaderCompression); compression != "" && + if compression := getHeaderCanonical(response.Header, grpcHeaderCompression); compression != "" && compression != compressionIdentity && !availableCompressors.Contains(compression) { // Per https://github.com/grpc/grpc/blob/master/doc/compression.md, we @@ -638,59 +673,30 @@ func grpcValidateResponse( availableCompressors.CommaSeparatedNames(), ) } - // When there's no body, gRPC and gRPC-Web servers may send error information - // in the HTTP headers. - if err := grpcErrorFromTrailer( - bufferPool, - protobuf, - response.Header, - ); err != nil && !errors.Is(err, errTrailersWithoutGRPCStatus) { - // Per the specification, only the HTTP status code and Content-Type should - // be treated as headers. The rest should be treated as trailing metadata. - if contentType := response.Header.Get(headerContentType); contentType != "" { - header.Set(headerContentType, contentType) - } - mergeHeaders(trailer, response.Header) - trailer.Del(headerContentType) - // Also set the error metadata - err.meta = header.Clone() - mergeHeaders(err.meta, trailer) - return err - } // The response is valid, so we should expose the headers. mergeHeaders(header, response.Header) return nil } -func grpcHTTPToCode(httpCode int) Code { - // https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md - // Note that this is not just the inverse of the gRPC-to-HTTP mapping. - switch httpCode { - case 400: - return CodeInternal - case 401: - return CodeUnauthenticated - case 403: - return CodePermissionDenied - case 404: - return CodeUnimplemented - case 429: - return CodeUnavailable - case 502, 503, 504: - return CodeUnavailable - default: - return CodeUnknown - } -} - // The gRPC wire protocol specifies that errors should be serialized using the // binary Protobuf format, even if the messages in the request/response stream // use a different codec. Consequently, this function needs a Protobuf codec to // unmarshal error information in the headers. -func grpcErrorFromTrailer(bufferPool *bufferPool, protobuf Codec, trailer http.Header) *Error { - codeHeader := trailer.Get(grpcHeaderStatus) +// +// A nil error is only returned when a grpc-status key IS present, but it +// indicates a code of zero (no error). If no grpc-status key is present, this +// returns a non-nil *Error that wraps errTrailersWithoutGRPCStatus. +func grpcErrorFromTrailer(protobuf Codec, trailer http.Header) *Error { + codeHeader := getHeaderCanonical(trailer, grpcHeaderStatus) if codeHeader == "" { - return NewError(CodeInternal, errTrailersWithoutGRPCStatus) + // If there are no trailers at all, that's an internal error. + // But if it's an error determining the status code from the + // trailers, it's unknown. + code := CodeUnknown + if len(trailer) == 0 { + code = CodeInternal + } + return NewError(code, errTrailersWithoutGRPCStatus) } if codeHeader == "0" { return nil @@ -698,13 +704,15 @@ func grpcErrorFromTrailer(bufferPool *bufferPool, protobuf Codec, trailer http.H code, err := strconv.ParseUint(codeHeader, 10 /* base */, 32 /* bitsize */) if err != nil { - return errorf(CodeInternal, "gRPC protocol error: invalid error code %q", codeHeader) + return errorf(CodeUnknown, "protocol error: invalid error code %q", codeHeader) } - message := grpcPercentDecode(bufferPool, trailer.Get(grpcHeaderMessage)) - retErr := NewError(Code(code), errors.New(message)) - retErr.wireErr = true + message, err := grpcPercentDecode(getHeaderCanonical(trailer, grpcHeaderMessage)) + if err != nil { + return errorf(CodeInternal, "protocol error: invalid error message %q", message) + } + retErr := NewWireError(Code(code), errors.New(message)) - detailsBinaryEncoded := trailer.Get(grpcHeaderDetails) + detailsBinaryEncoded := getHeaderCanonical(trailer, grpcHeaderDetails) if len(detailsBinaryEncoded) > 0 { detailsBinary, err := DecodeBinaryHeader(detailsBinaryEncoded) if err != nil { @@ -714,12 +722,12 @@ func grpcErrorFromTrailer(bufferPool *bufferPool, protobuf Codec, trailer http.H if err := protobuf.Unmarshal(detailsBinary, &status); err != nil { return errorf(CodeInternal, "server returned invalid protobuf for error details: %w", err) } - for _, d := range status.Details { - retErr.details = append(retErr.details, &ErrorDetail{pb: d}) + for _, d := range status.GetDetails() { + retErr.details = append(retErr.details, &ErrorDetail{pbAny: d}) } // Prefer the Protobuf-encoded data to the headers (grpc-go does this too). - retErr.code = Code(status.Code) - retErr.err = errors.New(status.Message) + retErr.code = Code(status.GetCode()) + retErr.err = errors.New(status.GetMessage()) } return retErr @@ -729,17 +737,18 @@ func grpcParseTimeout(timeout string) (time.Duration, error) { if timeout == "" { return 0, errNoTimeout } - unit, ok := grpcTimeoutUnitLookup[timeout[len(timeout)-1]] - if !ok { - return 0, fmt.Errorf("gRPC protocol error: timeout %q has invalid unit", timeout) + unit, err := grpcTimeoutUnitLookup(timeout[len(timeout)-1]) + if err != nil { + return 0, err } num, err := strconv.ParseInt(timeout[:len(timeout)-1], 10 /* base */, 64 /* bitsize */) if err != nil || num < 0 { - return 0, fmt.Errorf("gRPC protocol error: invalid timeout %q", timeout) + return 0, fmt.Errorf("protocol error: invalid timeout %q", timeout) } if num > 99999999 { // timeout must be ASCII string of at most 8 digits - return 0, fmt.Errorf("gRPC protocol error: timeout %q is too long", timeout) + return 0, fmt.Errorf("protocol error: timeout %q is too long", timeout) } + const grpcTimeoutMaxHours = math.MaxInt64 / int64(time.Hour) // how many hours fit into a time.Duration? if unit == time.Hour && num > grpcTimeoutMaxHours { // Timeout is effectively unbounded, so ignore it. The grpc-go // implementation does the same thing. @@ -748,32 +757,56 @@ func grpcParseTimeout(timeout string) (time.Duration, error) { return time.Duration(num) * unit, nil } -func grpcEncodeTimeout(timeout time.Duration) (string, error) { +func grpcEncodeTimeout(timeout time.Duration) string { if timeout <= 0 { - return "0n", nil + return "0n" } - for _, pair := range grpcTimeoutUnits { - digits := strconv.FormatInt(int64(timeout/pair.size), 10 /* base */) - if len(digits) < grpcMaxTimeoutChars { - return digits + string(pair.char), nil - } + // The gRPC protocol limits timeouts to 8 characters (not counting the unit), + // so timeouts must be strictly less than 1e8 of the appropriate unit. + const grpcTimeoutMaxValue = 1e8 + var ( + size time.Duration + unit byte + ) + switch { + case timeout < time.Nanosecond*grpcTimeoutMaxValue: + size, unit = time.Nanosecond, 'n' + case timeout < time.Microsecond*grpcTimeoutMaxValue: + size, unit = time.Microsecond, 'u' + case timeout < time.Millisecond*grpcTimeoutMaxValue: + size, unit = time.Millisecond, 'm' + case timeout < time.Second*grpcTimeoutMaxValue: + size, unit = time.Second, 'S' + case timeout < time.Minute*grpcTimeoutMaxValue: + size, unit = time.Minute, 'M' + default: + // time.Duration is an int64 number of nanoseconds, so the largest + // expressible duration is less than 1e8 hours. + size, unit = time.Hour, 'H' + } + buf := make([]byte, 0, 9) + buf = strconv.AppendInt(buf, int64(timeout/size), 10 /* base */) + buf = append(buf, unit) + return string(buf) +} + +func grpcTimeoutUnitLookup(unit byte) (time.Duration, error) { + switch unit { + case 'n': + return time.Nanosecond, nil + case 'u': + return time.Microsecond, nil + case 'm': + return time.Millisecond, nil + case 'S': + return time.Second, nil + case 'M': + return time.Minute, nil + case 'H': + return time.Hour, nil + default: + return 0, fmt.Errorf("protocol error: timeout has invalid unit %q", unit) } - // The max time.Duration is smaller than the maximum expressible gRPC - // timeout, so we can't reach this case. - return "", errNoTimeout -} - -// grpcUserAgent follows -// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#user-agents: -// -// While the protocol does not require a user-agent to function it is recommended -// that clients provide a structured user-agent string that provides a basic -// description of the calling library, version & platform to facilitate issue diagnosis -// in heterogeneous environments. The following structure is recommended to library developers: -// -// User-Agent → "grpc-" Language ?("-" Variant) "/" Version ?( " (" *(AdditionalProperty ";") ")" ) -func grpcUserAgent() string { - return fmt.Sprintf("grpc-go-connect/%s (%s)", Version, runtime.Version()) } func grpcCodecFromContentType(web bool, contentType string) string { @@ -792,38 +825,43 @@ func grpcContentTypeFromCodecName(web bool, name string) string { if web { return grpcWebContentTypePrefix + name } + if name == codecNameProto { + // For compatibility with Google Cloud Platform's frontends, prefer an + // implicit default codec. See + // https://github.com/connectrpc/connect-go/pull/655#issuecomment-1915754523 + // for details. + return grpcContentTypeDefault + } return grpcContentTypePrefix + name } -func grpcErrorToTrailer(bufferPool *bufferPool, trailer http.Header, protobuf Codec, err error) { +func grpcErrorToTrailer(trailer http.Header, protobuf Codec, err error) { if err == nil { - trailer.Set(grpcHeaderStatus, "0") // zero is the gRPC OK status - trailer.Set(grpcHeaderMessage, "") + setHeaderCanonical(trailer, grpcHeaderStatus, "0") // zero is the gRPC OK status return } - status := grpcStatusFromError(err) - code := strconv.Itoa(int(status.Code)) - bin, binErr := protobuf.Marshal(status) - if binErr != nil { - trailer.Set( - grpcHeaderStatus, - strconv.FormatInt(int64(CodeInternal), 10 /* base */), - ) - trailer.Set( - grpcHeaderMessage, - grpcPercentEncode( - bufferPool, - fmt.Sprintf("marshal protobuf status: %v", binErr), - ), - ) - return + if connectErr, ok := asError(err); ok && !connectErr.wireErr { + mergeMetadataHeaders(trailer, connectErr.meta) } - if connectErr, ok := asError(err); ok { - mergeHeaders(trailer, connectErr.meta) + var ( + status = grpcStatusFromError(err) + code = status.GetCode() + message = status.GetMessage() + bin []byte + ) + if len(status.Details) > 0 { + var binErr error + bin, binErr = protobuf.Marshal(status) + if binErr != nil { + code = int32(CodeInternal) + message = fmt.Sprintf("marshal protobuf status: %v", binErr) + } + } + setHeaderCanonical(trailer, grpcHeaderStatus, strconv.Itoa(int(code))) + setHeaderCanonical(trailer, grpcHeaderMessage, grpcPercentEncode(message)) + if len(bin) > 0 { + setHeaderCanonical(trailer, grpcHeaderDetails, EncodeBinaryHeader(bin)) } - trailer.Set(grpcHeaderStatus, code) - trailer.Set(grpcHeaderMessage, grpcPercentEncode(bufferPool, status.Message)) - trailer.Set(grpcHeaderDetails, EncodeBinaryHeader(bin)) } func grpcStatusFromError(err error) *statusv1.Status { @@ -851,62 +889,119 @@ func grpcStatusFromError(err error) *statusv1.Status { // // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses // https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 -func grpcPercentEncode(bufferPool *bufferPool, msg string) string { +func grpcPercentEncode(msg string) string { + var hexCount int for i := 0; i < len(msg); i++ { - // Characters that need to be escaped are defined in gRPC's HTTP/2 spec. - // They're different from the generic set defined in RFC 3986. - if c := msg[i]; c < ' ' || c > '~' || c == '%' { - return grpcPercentEncodeSlow(bufferPool, msg, i) + if grpcShouldEscape(msg[i]) { + hexCount++ } } - return msg -} - -// msg needs some percent-escaping. Bytes before offset don't require -// percent-encoding, so they can be copied to the output as-is. -func grpcPercentEncodeSlow(bufferPool *bufferPool, msg string, offset int) string { - out := bufferPool.Get() - defer bufferPool.Put(out) - out.WriteString(msg[:offset]) - for i := offset; i < len(msg); i++ { - c := msg[i] - if c < ' ' || c > '~' || c == '%' { - out.WriteString(fmt.Sprintf("%%%02X", c)) - continue + if hexCount == 0 { + return msg + } + // We need to escape some characters, so we'll need to allocate a new string. + var out strings.Builder + out.Grow(len(msg) + 2*hexCount) + for i := 0; i < len(msg); i++ { + switch char := msg[i]; { + case grpcShouldEscape(char): + out.WriteByte('%') + out.WriteByte(upperhex[char>>4]) + out.WriteByte(upperhex[char&15]) + default: + out.WriteByte(char) } - out.WriteByte(c) } return out.String() } -func grpcPercentDecode(bufferPool *bufferPool, encoded string) string { - for i := 0; i < len(encoded); i++ { - if c := encoded[i]; c == '%' && i+2 < len(encoded) { - return grpcPercentDecodeSlow(bufferPool, encoded, i) +func grpcPercentDecode(input string) (string, error) { + percentCount := 0 + for i := 0; i < len(input); { + switch input[i] { + case '%': + percentCount++ + if err := validateHex(input[i:]); err != nil { + return "", err + } + i += 3 + default: + i++ + } + } + if percentCount == 0 { + return input, nil + } + // We need to unescape some characters, so we'll need to allocate a new string. + var out strings.Builder + out.Grow(len(input) - 2*percentCount) + for i := 0; i < len(input); i++ { + switch input[i] { + case '%': + out.WriteByte(unhex(input[i+1])<<4 | unhex(input[i+2])) + i += 2 + default: + out.WriteByte(input[i]) } } - return encoded + return out.String(), nil } -// Similar to percentEncodeSlow: encoded is percent-encoded, and needs to be -// decoded byte-by-byte starting at offset. -func grpcPercentDecodeSlow(bufferPool *bufferPool, encoded string, offset int) string { - out := bufferPool.Get() - defer bufferPool.Put(out) - out.WriteString(encoded[:offset]) - for i := offset; i < len(encoded); i++ { - c := encoded[i] - if c != '%' || i+2 >= len(encoded) { - out.WriteByte(c) - continue - } - parsed, err := strconv.ParseUint(encoded[i+1:i+3], 16 /* hex */, 8 /* bitsize */) - if err != nil { - out.WriteRune(utf8.RuneError) - } else { - out.WriteByte(byte(parsed)) +// Characters that need to be escaped are defined in gRPC's HTTP/2 spec. +// They're different from the generic set defined in RFC 3986. +func grpcShouldEscape(char byte) bool { + return char < ' ' || char > '~' || char == '%' +} + +func unhex(char byte) byte { + switch { + case '0' <= char && char <= '9': + return char - '0' + case 'a' <= char && char <= 'f': + return char - 'a' + 10 + case 'A' <= char && char <= 'F': + return char - 'A' + 10 + } + return 0 +} + +func isHex(char byte) bool { + return ('0' <= char && char <= '9') || ('a' <= char && char <= 'f') || ('A' <= char && char <= 'F') +} + +func validateHex(input string) error { + if len(input) < 3 || input[0] != '%' || !isHex(input[1]) || !isHex(input[2]) { + if len(input) > 3 { + input = input[:3] } - i += 2 + return fmt.Errorf("invalid percent-encoded string %q", input) } - return out.String() + return nil +} + +func grpcValidateResponseContentType(web bool, requestCodecName string, responseContentType string) *Error { + // Responses must have valid content-type that indicates same codec as the request. + bare, prefix := grpcContentTypeDefault, grpcContentTypePrefix + if web { + bare, prefix = grpcWebContentTypeDefault, grpcWebContentTypePrefix + } + if responseContentType == prefix+requestCodecName || + (requestCodecName == codecNameProto && responseContentType == bare) { + return nil + } + expectedContentType := bare + if requestCodecName != codecNameProto { + expectedContentType = prefix + requestCodecName + } + code := CodeInternal + if responseContentType != bare && !strings.HasPrefix(responseContentType, prefix) { + // Doesn't even look like a gRPC response? Use code "unknown". + code = CodeUnknown + } + return errorf( + code, + "invalid content-type: %q; expecting %q", + responseContentType, + expectedContentType, + ) } diff --git a/vendor/github.com/bufbuild/connect-go/recover.go b/vendor/connectrpc.com/connect/recover.go similarity index 98% rename from vendor/github.com/bufbuild/connect-go/recover.go rename to vendor/connectrpc.com/connect/recover.go index c126d36a..72f535ea 100644 --- a/vendor/github.com/bufbuild/connect-go/recover.go +++ b/vendor/connectrpc.com/connect/recover.go @@ -1,4 +1,4 @@ -// Copyright 2021-2022 Buf Technologies, Inc. +// Copyright 2021-2024 The Connect Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/vendor/connectrpc.com/cors/.gitignore b/vendor/connectrpc.com/cors/.gitignore new file mode 100644 index 00000000..987d6a1c --- /dev/null +++ b/vendor/connectrpc.com/cors/.gitignore @@ -0,0 +1,4 @@ +/.tmp/ +*.pprof +*.svg +cover.out diff --git a/vendor/connectrpc.com/cors/.golangci.yml b/vendor/connectrpc.com/cors/.golangci.yml new file mode 100644 index 00000000..3f573aab --- /dev/null +++ b/vendor/connectrpc.com/cors/.golangci.yml @@ -0,0 +1,54 @@ +run: + skip-dirs-use-default: false +linters-settings: + errcheck: + check-type-assertions: true + forbidigo: + forbid: + - '^fmt\.Print' + - '^log\.' + - '^print$' + - '^println$' + - '^panic$' + godox: + # TODO, OPT, etc. comments are fine to commit. Use FIXME comments for + # temporary hacks, and use godox to prevent committing them. + keywords: [FIXME] + varnamelen: + ignore-decls: + - T any + - i int + - wg sync.WaitGroup +linters: + enable-all: true + disable: + - cyclop # covered by gocyclo + - depguard # unnecessary for small libraries + - deadcode # abandoned + - exhaustivestruct # replaced by exhaustruct + - funlen # rely on code review to limit function length + - gocognit # dubious "cognitive overhead" quantification + - gofumpt # prefer standard gofmt + - goimports # rely on gci instead + - golint # deprecated by Go team + - gomnd # some unnamed constants are okay + - ifshort # deprecated by author + - interfacer # deprecated by author + - ireturn # "accept interfaces, return structs" isn't ironclad + - lll # don't want hard limits for line length + - maintidx # covered by gocyclo + - maligned # readability trumps efficient struct packing + - nlreturn # generous whitespace violates house style + - nonamedreturns # named returns are fine; it's *bare* returns that are bad + - nosnakecase # deprecated in https://github.com/golangci/golangci-lint/pull/3065 + - scopelint # deprecated by author + - structcheck # abandoned + - testpackage # internal tests are fine + - varcheck # abandoned + - wrapcheck # don't _always_ need to wrap errors + - wsl # generous whitespace violates house style +issues: + exclude: + # Don't ban use of fmt.Errorf to create new errors, but the remaining + # checks from err113 are useful. + - "err113: do not define dynamic errors.*" diff --git a/vendor/connectrpc.com/cors/LICENSE b/vendor/connectrpc.com/cors/LICENSE new file mode 100644 index 00000000..9348264f --- /dev/null +++ b/vendor/connectrpc.com/cors/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 The Connect Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/connectrpc.com/cors/MAINTAINERS.md b/vendor/connectrpc.com/cors/MAINTAINERS.md new file mode 100644 index 00000000..d95df072 --- /dev/null +++ b/vendor/connectrpc.com/cors/MAINTAINERS.md @@ -0,0 +1,7 @@ +Maintainers +=========== + +## Current +* [Akshay Shah](https://github.com/akshayjshah), [Buf](https://buf.build) +* [Edward McFarlane](https://github.com/emcfarlane), [Buf](https://buf.build) +* [Timo Stamm](https://github.com/timostamm), [Buf](https://buf.build) diff --git a/vendor/connectrpc.com/cors/Makefile b/vendor/connectrpc.com/cors/Makefile new file mode 100644 index 00000000..41471a63 --- /dev/null +++ b/vendor/connectrpc.com/cors/Makefile @@ -0,0 +1,73 @@ +# See https://tech.davis-hansson.com/p/make/ +SHELL := bash +.DELETE_ON_ERROR: +.SHELLFLAGS := -eu -o pipefail -c +.DEFAULT_GOAL := all +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules +MAKEFLAGS += --no-print-directory +BIN := .tmp/bin +export PATH := $(BIN):$(PATH) +export GOBIN := $(abspath $(BIN)) +COPYRIGHT_YEARS := 2023 +LICENSE_IGNORE := --ignore testdata/ + +.PHONY: help +help: ## Describe useful make targets + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' + +.PHONY: all +all: ## Build, test, and lint (default) + $(MAKE) test + $(MAKE) lint + +.PHONY: clean +clean: ## Delete intermediate build artifacts + @# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs + git clean -Xdf + +.PHONY: test +test: build ## Run unit tests + go test -vet=off -race -cover ./... + +.PHONY: build +build: generate ## Build all packages + go build ./... + +.PHONY: generate +generate: $(BIN)/license-header ## Regenerate code and licenses + license-header \ + --license-type apache \ + --copyright-holder "The Connect Authors" \ + --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) + +.PHONY: lint +lint: $(BIN)/golangci-lint ## Lint + go vet ./... + golangci-lint run --modules-download-mode=readonly --timeout=3m0s + +.PHONY: lintfix +lintfix: $(BIN)/golangci-lint ## Automatically fix some lint errors + golangci-lint run --fix --modules-download-mode=readonly --timeout=3m0s + +.PHONY: install +install: ## Install all binaries + go install ./... + +.PHONY: upgrade +upgrade: ## Upgrade dependencies + go get -u -t ./... + go mod tidy -v + +.PHONY: checkgenerate +checkgenerate: + @# Used in CI to verify that `make generate` doesn't produce a diff. + test -z "$$(git status --porcelain | tee /dev/stderr)" + +$(BIN)/license-header: Makefile + @mkdir -p $(@D) + go install github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v1.26.1 + +$(BIN)/golangci-lint: Makefile + @mkdir -p $(@D) + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.1 diff --git a/vendor/connectrpc.com/cors/README.md b/vendor/connectrpc.com/cors/README.md new file mode 100644 index 00000000..997a3ab8 --- /dev/null +++ b/vendor/connectrpc.com/cors/README.md @@ -0,0 +1,85 @@ +cors +==== +[![Build](https://github.com/connectrpc/cors-go/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/connectrpc/cors-go/actions/workflows/ci.yaml) +[![Report Card](https://goreportcard.com/badge/connectrpc.com/cors)](https://goreportcard.com/report/connectrpc.com/cors) +[![GoDoc](https://pkg.go.dev/badge/connectrpc.com/cors.svg)](https://pkg.go.dev/connectrpc.com/cors) +[![Slack](https://img.shields.io/badge/slack-buf-%23e01563)][slack] + +`connectrpc.com/cors` simplifies Cross-Origin Resource Sharing (CORS) for +[Connect](https://github.com/connectrpc/connect-go) servers. CORS is usually +required for the Connect and gRPC-Web protocols to work correctly in +web browsers. + +For background, more details, and best practices, see [Connect's CORS +documentation](https://connectrpc.com/docs/cors). For more on Connect, see the +[announcement blog post][blog], the documentation on [connectrpc.com][docs] +(especially the [Getting Started] guide for Go), the [demo +service][examples-go], or the [protocol specification][protocol]. + +## Example + +This package should work with any CORS implementation. As an example, we'll use +it with [github.com/rs/cors](https://github.com/rs/cors). + +```go +import ( + connectcors "connectrpc.com/cors" + "github.com/rs/cors" +) + +// withCORS adds CORS support to a Connect HTTP handler. +func withCORS(connectHandler http.Handler) http.Handler { + c := cors.New(cors.Options{ + AllowedOrigins: []string{"https://acme.com"}, // replace with your domain + AllowedMethods: connectcors.AllowedMethods(), + AllowedHeaders: connectcors.AllowedHeaders(), + ExposedHeaders: connectcors.ExposedHeaders(), + MaxAge: 7200, // 2 hours in seconds + }) + return c.Handler(connectHandler) +} +``` + +## Ecosystem + +* [connect-go]: the Go implementation of Connect's RPC runtime +* [examples-go]: service powering [demo.connectrpc.com][demo], including bidi streaming +* [grpchealth]: gRPC-compatible health checks +* [grpcreflect]: gRPC-compatible server reflection +* [authn]: pluggable authentication for Connect servers +* [connect-es]: Type-safe APIs with Protobuf and TypeScript +* [conformance]: Connect, gRPC, and gRPC-Web interoperability tests + +## Status: Unstable + +This module isn't stable yet, but it's fairly small — we expect to reach +a stable release quickly. + +It supports the three most recent major releases of Go. +Keep in mind that [only the last two releases receive security +patches][go-support-policy]. + +Within those parameters, `cors` follows semantic versioning. We will _not_ +make breaking changes in the 1.x series of releases. + +## Legal + +Offered under the [Apache 2 license][license]. + +[Getting Started]: https://connectrpc.com/docs/go/getting-started +[authn]: https://github.com/connectrpc/authn-go +[blog]: https://buf.build/blog/connect-a-better-grpc +[conformance]: https://github.com/connectrpc/conformance +[connect-es]: https://github.com/connectrpc/connect-es +[connect-go]: https://github.com/connectrpc/connect-go +[cors]: https://github.com/connectrpc/cors-go +[demo]: https://demo.connectrpc.com +[docs]: https://connectrpc.com +[examples-go]: https://github.com/connectrpc/examples-go +[go-support-policy]: https://golang.org/doc/devel/release#policy +[godoc]: https://pkg.go.dev/connectrpc.com/authn +[grpchealth]: https://github.com/connectrpc/grpchealth-go +[grpcreflect]: https://github.com/connectrpc/grpcreflect-go +[license]: https://github.com/connectrpc/cors-go/blob/main/LICENSE +[protocol]: https://connectrpc.com/docs/protocol +[slack]: https://buf.build/links/slack diff --git a/vendor/connectrpc.com/cors/SECURITY.md b/vendor/connectrpc.com/cors/SECURITY.md new file mode 100644 index 00000000..04dcde52 --- /dev/null +++ b/vendor/connectrpc.com/cors/SECURITY.md @@ -0,0 +1,5 @@ +Security Policy +=============== + +This project follows the [Connect security policy and reporting +process](https://connectrpc.com/docs/governance/security). diff --git a/vendor/connectrpc.com/cors/cors.go b/vendor/connectrpc.com/cors/cors.go new file mode 100644 index 00000000..52b52088 --- /dev/null +++ b/vendor/connectrpc.com/cors/cors.go @@ -0,0 +1,66 @@ +// Copyright 2023 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package cors provides helpers to configure cross-origin resource sharing +// (CORS) for Connect servers. +package cors + +// AllowedMethods returns the allowed HTTP methods that scripts running in the +// browser are permitted to use. +// +// To support cross-domain requests with the protocols supported by Connect, +// these headers fields must be included in the preflight response header +// Access-Control-Allow-Methods. +func AllowedMethods() []string { + return []string{ + "GET", // for Connect + "POST", // for all protocols + } +} + +// AllowedHeaders returns the headers that scripts running in the browser send +// when making RPC requests. To support cross-domain requests with the +// protocols supported by Connect, these field names must be included in the +// Access-Control-Allow-Headers header of the preflight response. +// +// When configuring CORS, make sure to also include any application-specific +// headers your server expects to receive from the browser. +func AllowedHeaders() []string { + return []string{ + "Content-Type", // for all protocols + "Connect-Protocol-Version", // for Connect + "Connect-Timeout-Ms", // for Connect + "Grpc-Timeout", // for gRPC-web + "X-Grpc-Web", // for gRPC-web + "X-User-Agent", // for all protocols + } +} + +// ExposedHeaders returns the headers that scripts running in the +// browser expect to access when receiving RPC responses. To support +// cross-domain requests with the protocols supported by Connect, these field +// names must be included in the Access-Control-Expose-Headers header of the +// actual response. +// +// When configuring CORS, make sure to also include any application-specific +// headers your server expects to send to the browser. If your application uses +// trailers, they will be sent as headers with a `Trailer-` prefix for +// unary Connect RPCs - make sure to expose them! +func ExposedHeaders() []string { + return []string{ + "Grpc-Status", // for gRPC-web + "Grpc-Message", // for gRPC-web + "Grpc-Status-Details-Bin", // for gRPC-web + } +} diff --git a/vendor/github.com/99designs/go-keychain/.gitignore b/vendor/github.com/99designs/go-keychain/.gitignore new file mode 100644 index 00000000..daf913b1 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/99designs/go-keychain/.golangci.yml b/vendor/github.com/99designs/go-keychain/.golangci.yml new file mode 100644 index 00000000..23aaf432 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/.golangci.yml @@ -0,0 +1,11 @@ +linters-settings: + gocritic: + disabled-checks: + - ifElseChain + - elseif + +linters: + enable: + - gofmt + - gocritic + - unconvert diff --git a/vendor/github.com/99designs/go-keychain/.travis.yml b/vendor/github.com/99designs/go-keychain/.travis.yml new file mode 100644 index 00000000..2fba2395 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/.travis.yml @@ -0,0 +1,20 @@ +language: go + +os: + - osx + - linux + +before_install: + - go get golang.org/x/lint/golint + +script: + - go vet ./... + - golint ./... + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.16.0 + - golangci-lint run + - go test -tags skipsecretserviceintegrationtests ./... + +go: + - 1.10.x + - 1.11.x + - 1.12.x diff --git a/vendor/github.com/99designs/go-keychain/LICENSE b/vendor/github.com/99designs/go-keychain/LICENSE new file mode 100644 index 00000000..2d54c656 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Keybase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/99designs/go-keychain/README.md b/vendor/github.com/99designs/go-keychain/README.md new file mode 100644 index 00000000..4a9eeb24 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/README.md @@ -0,0 +1,159 @@ +# Go Keychain + +[![Travis CI](https://travis-ci.org/keybase/go-keychain.svg?branch=master)](https://travis-ci.org/keybase/go-keychain) + +A library for accessing the Keychain for macOS, iOS, and Linux in Go (golang). + +Requires macOS 10.9 or greater and iOS 8 or greater. On Linux, communicates to +a provider of the DBUS SecretService spec like gnome-keyring or ksecretservice. + +```go +import "github.com/keybase/go-keychain" +``` + + +## Mac/iOS Usage + +The API is meant to mirror the macOS/iOS Keychain API and is not necessarily idiomatic go. + +#### Add Item + +```go +item := keychain.NewItem() +item.SetSecClass(keychain.SecClassGenericPassword) +item.SetService("MyService") +item.SetAccount("gabriel") +item.SetLabel("A label") +item.SetAccessGroup("A123456789.group.com.mycorp") +item.SetData([]byte("toomanysecrets")) +item.SetSynchronizable(keychain.SynchronizableNo) +item.SetAccessible(keychain.AccessibleWhenUnlocked) +err := keychain.AddItem(item) + +if err == keychain.ErrorDuplicateItem { + // Duplicate +} +``` + +#### Query Item + +Query for multiple results, returning attributes: + +```go +query := keychain.NewItem() +query.SetSecClass(keychain.SecClassGenericPassword) +query.SetService(service) +query.SetAccount(account) +query.SetAccessGroup(accessGroup) +query.SetMatchLimit(keychain.MatchLimitAll) +query.SetReturnAttributes(true) +results, err := keychain.QueryItem(query) +if err != nil { + // Error +} else { + for _, r := range results { + fmt.Printf("%#v\n", r) + } +} +``` + +Query for a single result, returning data: + +```go +query := keychain.NewItem() +query.SetSecClass(keychain.SecClassGenericPassword) +query.SetService(service) +query.SetAccount(account) +query.SetAccessGroup(accessGroup) +query.SetMatchLimit(keychain.MatchLimitOne) +query.SetReturnData(true) +results, err := keychain.QueryItem(query) +if err != nil { + // Error +} else if len(results) != 1 { + // Not found +} else { + password := string(results[0].Data) +} +``` + +#### Delete Item + +Delete a generic password item with service and account: + +```go +item := keychain.NewItem() +item.SetSecClass(keychain.SecClassGenericPassword) +item.SetService(service) +item.SetAccount(account) +err := keychain.DeleteItem(item) +``` + +### Other + +There are some convenience methods for generic password: + +```go +// Create generic password item with service, account, label, password, access group +item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp") +item.SetSynchronizable(keychain.SynchronizableNo) +item.SetAccessible(keychain.AccessibleWhenUnlocked) +err := keychain.AddItem(item) +if err == keychain.ErrorDuplicateItem { + // Duplicate +} + +accounts, err := keychain.GetGenericPasswordAccounts("MyService") +// Should have 1 account == "gabriel" + +err := keychain.DeleteGenericPasswordItem("MyService", "gabriel") +if err == keychain.ErrorNotFound { + // Not found +} +``` + +### OS X + +Creating a new keychain and add an item to it: + +```go + +// Add a new key chain into ~/Application Support/Keychains, with the provided password +k, err := keychain.NewKeychain("mykeychain.keychain", "my keychain password") +if err != nil { + // Error creating +} + +// Create generic password item with service, account, label, password, access group +item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp") +item.UseKeychain(k) +err := keychain.AddItem(item) +if err != nil { + // Error creating +} +``` + +Using a Keychain at path: + +```go +k, err := keychain.NewWithPath("mykeychain.keychain") +``` + +Set a trusted applications for item (OS X only): + +```go +item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp") +trustedApplications := []string{"/Applications/Mail.app"} +item.SetAccess(&keychain.Access{Label: "Mail", TrustedApplications: trustedApplications}) +err := keychain.AddItem(item) +``` + +## iOS + +Bindable package in `bind`. iOS project in `ios`. Run that project to test iOS. + +To re-generate framework: + +``` +(cd bind && gomobile bind -target=ios -tags=ios -o ../ios/bind.framework) +``` diff --git a/vendor/github.com/99designs/go-keychain/corefoundation.go b/vendor/github.com/99designs/go-keychain/corefoundation.go new file mode 100644 index 00000000..c45ee7a8 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/corefoundation.go @@ -0,0 +1,359 @@ +// +build darwin ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation + +#include + +// Can't cast a *uintptr to *unsafe.Pointer in Go, and casting +// C.CFTypeRef to unsafe.Pointer is unsafe in Go, so have shim functions to +// do the casting in C (where it's safe). + +// We add a suffix to the C functions below, because we copied this +// file from go-kext, which means that any project that depends on this +// package and go-kext would run into duplicate symbol errors otherwise. +// +// TODO: Move this file into its own package depended on by go-kext +// and this package. + +CFDictionaryRef CFDictionaryCreateSafe2(CFAllocatorRef allocator, const uintptr_t *keys, const uintptr_t *values, CFIndex numValues, const CFDictionaryKeyCallBacks *keyCallBacks, const CFDictionaryValueCallBacks *valueCallBacks) { + return CFDictionaryCreate(allocator, (const void **)keys, (const void **)values, numValues, keyCallBacks, valueCallBacks); +} + +CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values, CFIndex numValues, const CFArrayCallBacks *callBacks) { + return CFArrayCreate(allocator, (const void **)values, numValues, callBacks); +} +*/ +import "C" +import ( + "errors" + "fmt" + "math" + "reflect" + "unicode/utf8" + "unsafe" +) + +// Release releases memory pointed to by a CFTypeRef. +func Release(ref C.CFTypeRef) { + C.CFRelease(ref) +} + +// BytesToCFData will return a CFDataRef and if non-nil, must be released with +// Release(ref). +func BytesToCFData(b []byte) (C.CFDataRef, error) { + if uint64(len(b)) > math.MaxUint32 { + return 0, errors.New("Data is too large") + } + var p *C.UInt8 + if len(b) > 0 { + p = (*C.UInt8)(&b[0]) + } + cfData := C.CFDataCreate(C.kCFAllocatorDefault, p, C.CFIndex(len(b))) + if cfData == 0 { + return 0, fmt.Errorf("CFDataCreate failed") + } + return cfData, nil +} + +// CFDataToBytes converts CFData to bytes. +func CFDataToBytes(cfData C.CFDataRef) ([]byte, error) { + return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))), nil +} + +// MapToCFDictionary will return a CFDictionaryRef and if non-nil, must be +// released with Release(ref). +func MapToCFDictionary(m map[C.CFTypeRef]C.CFTypeRef) (C.CFDictionaryRef, error) { + var keys, values []C.uintptr_t + for key, value := range m { + keys = append(keys, C.uintptr_t(key)) + values = append(values, C.uintptr_t(value)) + } + numValues := len(values) + var keysPointer, valuesPointer *C.uintptr_t + if numValues > 0 { + keysPointer = &keys[0] + valuesPointer = &values[0] + } + cfDict := C.CFDictionaryCreateSafe2(C.kCFAllocatorDefault, keysPointer, valuesPointer, C.CFIndex(numValues), + &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) //nolint + if cfDict == 0 { + return 0, fmt.Errorf("CFDictionaryCreate failed") + } + return cfDict, nil +} + +// CFDictionaryToMap converts CFDictionaryRef to a map. +func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) { + count := C.CFDictionaryGetCount(cfDict) + if count > 0 { + keys := make([]C.CFTypeRef, count) + values := make([]C.CFTypeRef, count) + C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(unsafe.Pointer(&keys[0])), (*unsafe.Pointer)(unsafe.Pointer(&values[0]))) + m = make(map[C.CFTypeRef]C.CFTypeRef, count) + for i := C.CFIndex(0); i < count; i++ { + m[keys[i]] = values[i] + } + } + return +} + +// StringToCFString will return a CFStringRef and if non-nil, must be released with +// Release(ref). +func StringToCFString(s string) (C.CFStringRef, error) { + if !utf8.ValidString(s) { + return 0, errors.New("Invalid UTF-8 string") + } + if uint64(len(s)) > math.MaxUint32 { + return 0, errors.New("String is too large") + } + + bytes := []byte(s) + var p *C.UInt8 + if len(bytes) > 0 { + p = (*C.UInt8)(&bytes[0]) + } + return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, p, C.CFIndex(len(s)), C.kCFStringEncodingUTF8, C.false), nil +} + +// CFStringToString converts a CFStringRef to a string. +func CFStringToString(s C.CFStringRef) string { + p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8) + if p != nil { + return C.GoString(p) + } + length := C.CFStringGetLength(s) + if length == 0 { + return "" + } + maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8) + if maxBufLen == 0 { + return "" + } + buf := make([]byte, maxBufLen) + var usedBufLen C.CFIndex + _ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen) + return string(buf[:usedBufLen]) +} + +// ArrayToCFArray will return a CFArrayRef and if non-nil, must be released with +// Release(ref). +func ArrayToCFArray(a []C.CFTypeRef) C.CFArrayRef { + var values []C.uintptr_t + for _, value := range a { + values = append(values, C.uintptr_t(value)) + } + numValues := len(values) + var valuesPointer *C.uintptr_t + if numValues > 0 { + valuesPointer = &values[0] + } + return C.CFArrayCreateSafe2(C.kCFAllocatorDefault, valuesPointer, C.CFIndex(numValues), &C.kCFTypeArrayCallBacks) //nolint +} + +// CFArrayToArray converts a CFArrayRef to an array of CFTypes. +func CFArrayToArray(cfArray C.CFArrayRef) (a []C.CFTypeRef) { + count := C.CFArrayGetCount(cfArray) + if count > 0 { + a = make([]C.CFTypeRef, count) + C.CFArrayGetValues(cfArray, C.CFRange{0, count}, (*unsafe.Pointer)(unsafe.Pointer(&a[0]))) + } + return +} + +// Convertable knows how to convert an instance to a CFTypeRef. +type Convertable interface { + Convert() (C.CFTypeRef, error) +} + +// ConvertMapToCFDictionary converts a map to a CFDictionary and if non-nil, +// must be released with Release(ref). +func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, error) { + m := make(map[C.CFTypeRef]C.CFTypeRef) + for key, i := range attr { + var valueRef C.CFTypeRef + switch val := i.(type) { + default: + return 0, fmt.Errorf("Unsupported value type: %v", reflect.TypeOf(i)) + case C.CFTypeRef: + valueRef = val + case bool: + if val { + valueRef = C.CFTypeRef(C.kCFBooleanTrue) + } else { + valueRef = C.CFTypeRef(C.kCFBooleanFalse) + } + case []byte: + bytesRef, err := BytesToCFData(val) + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(bytesRef) + defer Release(valueRef) + case string: + stringRef, err := StringToCFString(val) + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(stringRef) + defer Release(valueRef) + case Convertable: + convertedRef, err := val.Convert() + if err != nil { + return 0, err + } + valueRef = convertedRef + defer Release(valueRef) + } + keyRef, err := StringToCFString(key) + if err != nil { + return 0, err + } + m[C.CFTypeRef(keyRef)] = valueRef + defer Release(C.CFTypeRef(keyRef)) + } + + cfDict, err := MapToCFDictionary(m) + if err != nil { + return 0, err + } + return cfDict, nil +} + +// CFTypeDescription returns type string for CFTypeRef. +func CFTypeDescription(ref C.CFTypeRef) string { + typeID := C.CFGetTypeID(ref) + typeDesc := C.CFCopyTypeIDDescription(typeID) + defer Release(C.CFTypeRef(typeDesc)) + return CFStringToString(typeDesc) +} + +// Convert converts a CFTypeRef to a go instance. +func Convert(ref C.CFTypeRef) (interface{}, error) { + typeID := C.CFGetTypeID(ref) + if typeID == C.CFStringGetTypeID() { + return CFStringToString(C.CFStringRef(ref)), nil + } else if typeID == C.CFDictionaryGetTypeID() { + return ConvertCFDictionary(C.CFDictionaryRef(ref)) + } else if typeID == C.CFArrayGetTypeID() { + arr := CFArrayToArray(C.CFArrayRef(ref)) + results := make([]interface{}, 0, len(arr)) + for _, ref := range arr { + v, err := Convert(ref) + if err != nil { + return nil, err + } + results = append(results, v) + } + return results, nil + } else if typeID == C.CFDataGetTypeID() { + b, err := CFDataToBytes(C.CFDataRef(ref)) + if err != nil { + return nil, err + } + return b, nil + } else if typeID == C.CFNumberGetTypeID() { + return CFNumberToInterface(C.CFNumberRef(ref)), nil + } else if typeID == C.CFBooleanGetTypeID() { + if C.CFBooleanGetValue(C.CFBooleanRef(ref)) != 0 { + return true, nil + } + return false, nil + } + + return nil, fmt.Errorf("Invalid type: %s", CFTypeDescription(ref)) +} + +// ConvertCFDictionary converts a CFDictionary to map (deep). +func ConvertCFDictionary(d C.CFDictionaryRef) (map[interface{}]interface{}, error) { + m := CFDictionaryToMap(d) + result := make(map[interface{}]interface{}) + + for k, v := range m { + gk, err := Convert(k) + if err != nil { + return nil, err + } + gv, err := Convert(v) + if err != nil { + return nil, err + } + result[gk] = gv + } + return result, nil +} + +// CFNumberToInterface converts the CFNumberRef to the most appropriate numeric +// type. +// This code is from github.com/kballard/go-osx-plist. +func CFNumberToInterface(cfNumber C.CFNumberRef) interface{} { + typ := C.CFNumberGetType(cfNumber) + switch typ { + case C.kCFNumberSInt8Type: + var sint C.SInt8 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int8(sint) + case C.kCFNumberSInt16Type: + var sint C.SInt16 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int16(sint) + case C.kCFNumberSInt32Type: + var sint C.SInt32 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int32(sint) + case C.kCFNumberSInt64Type: + var sint C.SInt64 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int64(sint) + case C.kCFNumberFloat32Type: + var float C.Float32 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint + return float32(float) + case C.kCFNumberFloat64Type: + var float C.Float64 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint + return float64(float) + case C.kCFNumberCharType: + var char C.char + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&char)) //nolint + return byte(char) + case C.kCFNumberShortType: + var short C.short + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&short)) //nolint + return int16(short) + case C.kCFNumberIntType: + var i C.int + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&i)) //nolint + return int32(i) + case C.kCFNumberLongType: + var long C.long + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&long)) //nolint + return int(long) + case C.kCFNumberLongLongType: + // This is the only type that may actually overflow us + var longlong C.longlong + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&longlong)) //nolint + return int64(longlong) + case C.kCFNumberFloatType: + var float C.float + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint + return float32(float) + case C.kCFNumberDoubleType: + var double C.double + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&double)) //nolint + return float64(double) + case C.kCFNumberCFIndexType: + // CFIndex is a long + var index C.CFIndex + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&index)) //nolint + return int(index) + case C.kCFNumberNSIntegerType: + // We don't have a definition of NSInteger, but we know it's either an int or a long + var nsInt C.long + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&nsInt)) //nolint + return int(nsInt) + } + panic("Unknown CFNumber type") +} diff --git a/vendor/github.com/99designs/go-keychain/datetime.go b/vendor/github.com/99designs/go-keychain/datetime.go new file mode 100644 index 00000000..8124a6ee --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/datetime.go @@ -0,0 +1,68 @@ +// +build darwin ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation + +#include +*/ +import "C" +import ( + "math" + "time" +) + +const nsPerSec = 1000 * 1000 * 1000 + +// absoluteTimeIntervalSince1970() returns the number of seconds from +// the Unix epoch (1970-01-01T00:00:00+00:00) to the Core Foundation +// absolute reference date (2001-01-01T00:00:00+00:00). It should be +// exactly 978307200. +func absoluteTimeIntervalSince1970() int64 { + return int64(C.kCFAbsoluteTimeIntervalSince1970) +} + +func unixToAbsoluteTime(s int64, ns int64) C.CFAbsoluteTime { + // Subtract as int64s first before converting to floating + // point to minimize precision loss (assuming the given time + // isn't much earlier than the Core Foundation absolute + // reference date). + abs := s - absoluteTimeIntervalSince1970() + return C.CFAbsoluteTime(abs) + C.CFTimeInterval(ns)/nsPerSec +} + +func absoluteTimeToUnix(abs C.CFAbsoluteTime) (int64, int64) { + int, frac := math.Modf(float64(abs)) + return int64(int) + absoluteTimeIntervalSince1970(), int64(frac * nsPerSec) +} + +// TimeToCFDate will convert the given time.Time to a CFDateRef, which +// must be released with Release(ref). +func TimeToCFDate(t time.Time) C.CFDateRef { + s := t.Unix() + ns := int64(t.Nanosecond()) + abs := unixToAbsoluteTime(s, ns) + return C.CFDateCreate(C.kCFAllocatorDefault, abs) +} + +// CFDateToTime will convert the given CFDateRef to a time.Time. +func CFDateToTime(d C.CFDateRef) time.Time { + abs := C.CFDateGetAbsoluteTime(d) + s, ns := absoluteTimeToUnix(abs) + return time.Unix(s, ns) +} + +// Wrappers around C functions for testing. + +func cfDateToAbsoluteTime(d C.CFDateRef) C.CFAbsoluteTime { + return C.CFDateGetAbsoluteTime(d) +} + +func absoluteTimeToCFDate(abs C.CFAbsoluteTime) C.CFDateRef { + return C.CFDateCreate(C.kCFAllocatorDefault, abs) +} + +func releaseCFDate(d C.CFDateRef) { + Release(C.CFTypeRef(d)) +} diff --git a/vendor/github.com/99designs/go-keychain/ios.go b/vendor/github.com/99designs/go-keychain/ios.go new file mode 100644 index 00000000..abbaf285 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/ios.go @@ -0,0 +1,22 @@ +// +build darwin,ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include +#include +*/ +import "C" + +var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) +var accessibleTypeRef = map[Accessible]C.CFTypeRef{ + AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), + AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), + AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), + AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), + AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), + AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), +} diff --git a/vendor/github.com/99designs/go-keychain/keychain.go b/vendor/github.com/99designs/go-keychain/keychain.go new file mode 100644 index 00000000..f5d02adf --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/keychain.go @@ -0,0 +1,531 @@ +// +build darwin + +package keychain + +// See https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/index.html for the APIs used below. + +// Also see https://developer.apple.com/library/ios/documentation/Security/Conceptual/keychainServConcepts/01introduction/introduction.html . + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include +#include +*/ +import "C" +import ( + "fmt" + "time" +) + +// Error defines keychain errors +type Error int + +var ( + // ErrorUnimplemented corresponds to errSecUnimplemented result code + ErrorUnimplemented = Error(C.errSecUnimplemented) + // ErrorParam corresponds to errSecParam result code + ErrorParam = Error(C.errSecParam) + // ErrorAllocate corresponds to errSecAllocate result code + ErrorAllocate = Error(C.errSecAllocate) + // ErrorNotAvailable corresponds to errSecNotAvailable result code + ErrorNotAvailable = Error(C.errSecNotAvailable) + // ErrorAuthFailed corresponds to errSecAuthFailed result code + ErrorAuthFailed = Error(C.errSecAuthFailed) + // ErrorDuplicateItem corresponds to errSecDuplicateItem result code + ErrorDuplicateItem = Error(C.errSecDuplicateItem) + // ErrorItemNotFound corresponds to errSecItemNotFound result code + ErrorItemNotFound = Error(C.errSecItemNotFound) + // ErrorInteractionNotAllowed corresponds to errSecInteractionNotAllowed result code + ErrorInteractionNotAllowed = Error(C.errSecInteractionNotAllowed) + // ErrorDecode corresponds to errSecDecode result code + ErrorDecode = Error(C.errSecDecode) + // ErrorNoSuchKeychain corresponds to errSecNoSuchKeychain result code + ErrorNoSuchKeychain = Error(C.errSecNoSuchKeychain) + // ErrorNoAcccessForItem corresponds to errSecNoAccessForItem result code + ErrorNoAccessForItem = Error(C.errSecNoAccessForItem) +) + +func checkError(errCode C.OSStatus) error { + if errCode == C.errSecSuccess { + return nil + } + return Error(errCode) +} + +func (k Error) Error() (msg string) { + // SecCopyErrorMessageString is only available on OSX, so derive manually. + // Messages derived from `$ security error $errcode`. + switch k { + case ErrorUnimplemented: + msg = "Function or operation not implemented." + case ErrorParam: + msg = "One or more parameters passed to the function were not valid." + case ErrorAllocate: + msg = "Failed to allocate memory." + case ErrorNotAvailable: + msg = "No keychain is available. You may need to restart your computer." + case ErrorAuthFailed: + msg = "The user name or passphrase you entered is not correct." + case ErrorDuplicateItem: + msg = "The specified item already exists in the keychain." + case ErrorItemNotFound: + msg = "The specified item could not be found in the keychain." + case ErrorInteractionNotAllowed: + msg = "User interaction is not allowed." + case ErrorDecode: + msg = "Unable to decode the provided data." + case ErrorNoSuchKeychain: + msg = "The specified keychain could not be found." + case ErrorNoAccessForItem: + msg = "The specified item has no access control." + default: + msg = "Keychain Error." + } + return fmt.Sprintf("%s (%d)", msg, k) +} + +// SecClass is the items class code +type SecClass int + +// Keychain Item Classes +var ( + /* + kSecClassGenericPassword item attributes: + kSecAttrAccess (OS X only) + kSecAttrAccessGroup (iOS; also OS X if kSecAttrSynchronizable specified) + kSecAttrAccessible (iOS; also OS X if kSecAttrSynchronizable specified) + kSecAttrAccount + kSecAttrService + */ + SecClassGenericPassword SecClass = 1 + SecClassInternetPassword SecClass = 2 +) + +// SecClassKey is the key type for SecClass +var SecClassKey = attrKey(C.CFTypeRef(C.kSecClass)) +var secClassTypeRef = map[SecClass]C.CFTypeRef{ + SecClassGenericPassword: C.CFTypeRef(C.kSecClassGenericPassword), + SecClassInternetPassword: C.CFTypeRef(C.kSecClassInternetPassword), +} + +var ( + // ServiceKey is for kSecAttrService + ServiceKey = attrKey(C.CFTypeRef(C.kSecAttrService)) + // LabelKey is for kSecAttrLabel + LabelKey = attrKey(C.CFTypeRef(C.kSecAttrLabel)) + // AccountKey is for kSecAttrAccount + AccountKey = attrKey(C.CFTypeRef(C.kSecAttrAccount)) + // AccessGroupKey is for kSecAttrAccessGroup + AccessGroupKey = attrKey(C.CFTypeRef(C.kSecAttrAccessGroup)) + // DataKey is for kSecValueData + DataKey = attrKey(C.CFTypeRef(C.kSecValueData)) + // DescriptionKey is for kSecAttrDescription + DescriptionKey = attrKey(C.CFTypeRef(C.kSecAttrDescription)) + // CreationDateKey is for kSecAttrCreationDate + CreationDateKey = attrKey(C.CFTypeRef(C.kSecAttrCreationDate)) + // ModificationDateKey is for kSecAttrModificationDate + ModificationDateKey = attrKey(C.CFTypeRef(C.kSecAttrModificationDate)) +) + +// Synchronizable is the items synchronizable status +type Synchronizable int + +const ( + // SynchronizableDefault is the default setting + SynchronizableDefault Synchronizable = 0 + // SynchronizableAny is for kSecAttrSynchronizableAny + SynchronizableAny = 1 + // SynchronizableYes enables synchronization + SynchronizableYes = 2 + // SynchronizableNo disables synchronization + SynchronizableNo = 3 +) + +// SynchronizableKey is the key type for Synchronizable +var SynchronizableKey = attrKey(C.CFTypeRef(C.kSecAttrSynchronizable)) +var syncTypeRef = map[Synchronizable]C.CFTypeRef{ + SynchronizableAny: C.CFTypeRef(C.kSecAttrSynchronizableAny), + SynchronizableYes: C.CFTypeRef(C.kCFBooleanTrue), + SynchronizableNo: C.CFTypeRef(C.kCFBooleanFalse), +} + +// Accessible is the items accessibility +type Accessible int + +const ( + // AccessibleDefault is the default + AccessibleDefault Accessible = 0 + // AccessibleWhenUnlocked is when unlocked + AccessibleWhenUnlocked = 1 + // AccessibleAfterFirstUnlock is after first unlock + AccessibleAfterFirstUnlock = 2 + // AccessibleAlways is always + AccessibleAlways = 3 + // AccessibleWhenPasscodeSetThisDeviceOnly is when passcode is set + AccessibleWhenPasscodeSetThisDeviceOnly = 4 + // AccessibleWhenUnlockedThisDeviceOnly is when unlocked for this device only + AccessibleWhenUnlockedThisDeviceOnly = 5 + // AccessibleAfterFirstUnlockThisDeviceOnly is after first unlock for this device only + AccessibleAfterFirstUnlockThisDeviceOnly = 6 + // AccessibleAccessibleAlwaysThisDeviceOnly is always for this device only + AccessibleAccessibleAlwaysThisDeviceOnly = 7 +) + +// MatchLimit is whether to limit results on query +type MatchLimit int + +const ( + // MatchLimitDefault is the default + MatchLimitDefault MatchLimit = 0 + // MatchLimitOne limits to one result + MatchLimitOne = 1 + // MatchLimitAll is no limit + MatchLimitAll = 2 +) + +// MatchLimitKey is key type for MatchLimit +var MatchLimitKey = attrKey(C.CFTypeRef(C.kSecMatchLimit)) +var matchTypeRef = map[MatchLimit]C.CFTypeRef{ + MatchLimitOne: C.CFTypeRef(C.kSecMatchLimitOne), + MatchLimitAll: C.CFTypeRef(C.kSecMatchLimitAll), +} + +// ReturnAttributesKey is key type for kSecReturnAttributes +var ReturnAttributesKey = attrKey(C.CFTypeRef(C.kSecReturnAttributes)) + +// ReturnDataKey is key type for kSecReturnData +var ReturnDataKey = attrKey(C.CFTypeRef(C.kSecReturnData)) + +// ReturnRefKey is key type for kSecReturnRef +var ReturnRefKey = attrKey(C.CFTypeRef(C.kSecReturnRef)) + +// Item for adding, querying or deleting. +type Item struct { + // Values can be string, []byte, Convertable or CFTypeRef (constant). + attr map[string]interface{} +} + +// SetSecClass sets the security class +func (k *Item) SetSecClass(sc SecClass) { + k.attr[SecClassKey] = secClassTypeRef[sc] +} + +// SetString sets a string attibute for a string key +func (k *Item) SetString(key string, s string) { + if s != "" { + k.attr[key] = s + } else { + delete(k.attr, key) + } +} + +// SetService sets the service attribute +func (k *Item) SetService(s string) { + k.SetString(ServiceKey, s) +} + +// SetAccount sets the account attribute +func (k *Item) SetAccount(a string) { + k.SetString(AccountKey, a) +} + +// SetLabel sets the label attribute +func (k *Item) SetLabel(l string) { + k.SetString(LabelKey, l) +} + +// SetDescription sets the description attribute +func (k *Item) SetDescription(s string) { + k.SetString(DescriptionKey, s) +} + +// SetData sets the data attribute +func (k *Item) SetData(b []byte) { + if b != nil { + k.attr[DataKey] = b + } else { + delete(k.attr, DataKey) + } +} + +// SetAccessGroup sets the access group attribute +func (k *Item) SetAccessGroup(ag string) { + k.SetString(AccessGroupKey, ag) +} + +// SetSynchronizable sets the synchronizable attribute +func (k *Item) SetSynchronizable(sync Synchronizable) { + if sync != SynchronizableDefault { + k.attr[SynchronizableKey] = syncTypeRef[sync] + } else { + delete(k.attr, SynchronizableKey) + } +} + +// SetAccessible sets the accessible attribute +func (k *Item) SetAccessible(accessible Accessible) { + if accessible != AccessibleDefault { + k.attr[AccessibleKey] = accessibleTypeRef[accessible] + } else { + delete(k.attr, AccessibleKey) + } +} + +// SetMatchLimit sets the match limit +func (k *Item) SetMatchLimit(matchLimit MatchLimit) { + if matchLimit != MatchLimitDefault { + k.attr[MatchLimitKey] = matchTypeRef[matchLimit] + } else { + delete(k.attr, MatchLimitKey) + } +} + +// SetReturnAttributes sets the return value type on query +func (k *Item) SetReturnAttributes(b bool) { + k.attr[ReturnAttributesKey] = b +} + +// SetReturnData enables returning data on query +func (k *Item) SetReturnData(b bool) { + k.attr[ReturnDataKey] = b +} + +// SetReturnRef enables returning references on query +func (k *Item) SetReturnRef(b bool) { + k.attr[ReturnRefKey] = b +} + +// NewItem is a new empty keychain item +func NewItem() Item { + return Item{make(map[string]interface{})} +} + +// NewGenericPassword creates a generic password item with the default keychain. This is a convenience method. +func NewGenericPassword(service string, account string, label string, data []byte, accessGroup string) Item { + item := NewItem() + item.SetSecClass(SecClassGenericPassword) + item.SetService(service) + item.SetAccount(account) + item.SetLabel(label) + item.SetData(data) + item.SetAccessGroup(accessGroup) + return item +} + +// AddItem adds a Item to a Keychain +func AddItem(item Item) error { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + + errCode := C.SecItemAdd(cfDict, nil) + err = checkError(errCode) + return err +} + +// UpdateItem updates the queryItem with the parameters from updateItem +func UpdateItem(queryItem Item, updateItem Item) error { + cfDict, err := ConvertMapToCFDictionary(queryItem.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + cfDictUpdate, err := ConvertMapToCFDictionary(updateItem.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDictUpdate)) + errCode := C.SecItemUpdate(cfDict, cfDictUpdate) + err = checkError(errCode) + return err +} + +// QueryResult stores all possible results from queries. +// Not all fields are applicable all the time. Results depend on query. +type QueryResult struct { + Service string + Account string + AccessGroup string + Label string + Description string + Data []byte + CreationDate time.Time + ModificationDate time.Time +} + +// QueryItemRef returns query result as CFTypeRef. You must release it when you are done. +func QueryItemRef(item Item) (C.CFTypeRef, error) { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return 0, err + } + defer Release(C.CFTypeRef(cfDict)) + + var resultsRef C.CFTypeRef + errCode := C.SecItemCopyMatching(cfDict, &resultsRef) //nolint + if Error(errCode) == ErrorItemNotFound { + return 0, nil + } + err = checkError(errCode) + if err != nil { + return 0, err + } + return resultsRef, nil +} + +// QueryItem returns a list of query results. +func QueryItem(item Item) ([]QueryResult, error) { + resultsRef, err := QueryItemRef(item) + if err != nil { + return nil, err + } + if resultsRef == 0 { + return nil, nil + } + defer Release(resultsRef) + + results := make([]QueryResult, 0, 1) + + typeID := C.CFGetTypeID(resultsRef) + if typeID == C.CFArrayGetTypeID() { + arr := CFArrayToArray(C.CFArrayRef(resultsRef)) + for _, ref := range arr { + elementTypeID := C.CFGetTypeID(ref) + if elementTypeID == C.CFDictionaryGetTypeID() { + item, err := convertResult(C.CFDictionaryRef(ref)) + if err != nil { + return nil, err + } + results = append(results, *item) + } else { + return nil, fmt.Errorf("invalid result type (If you SetReturnRef(true) you should use QueryItemRef directly)") + } + } + } else if typeID == C.CFDictionaryGetTypeID() { + item, err := convertResult(C.CFDictionaryRef(resultsRef)) + if err != nil { + return nil, err + } + results = append(results, *item) + } else if typeID == C.CFDataGetTypeID() { + b, err := CFDataToBytes(C.CFDataRef(resultsRef)) + if err != nil { + return nil, err + } + item := QueryResult{Data: b} + results = append(results, item) + } else { + return nil, fmt.Errorf("Invalid result type: %s", CFTypeDescription(resultsRef)) + } + + return results, nil +} + +func attrKey(ref C.CFTypeRef) string { + return CFStringToString(C.CFStringRef(ref)) +} + +func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { + m := CFDictionaryToMap(d) + result := QueryResult{} + for k, v := range m { + switch attrKey(k) { + case ServiceKey: + result.Service = CFStringToString(C.CFStringRef(v)) + case AccountKey: + result.Account = CFStringToString(C.CFStringRef(v)) + case AccessGroupKey: + result.AccessGroup = CFStringToString(C.CFStringRef(v)) + case LabelKey: + result.Label = CFStringToString(C.CFStringRef(v)) + case DescriptionKey: + result.Description = CFStringToString(C.CFStringRef(v)) + case DataKey: + b, err := CFDataToBytes(C.CFDataRef(v)) + if err != nil { + return nil, err + } + result.Data = b + case CreationDateKey: + result.CreationDate = CFDateToTime(C.CFDateRef(v)) + case ModificationDateKey: + result.ModificationDate = CFDateToTime(C.CFDateRef(v)) + // default: + // fmt.Printf("Unhandled key in conversion: %v = %v\n", cfTypeValue(k), cfTypeValue(v)) + } + } + return &result, nil +} + +// DeleteGenericPasswordItem removes a generic password item. +func DeleteGenericPasswordItem(service string, account string) error { + item := NewItem() + item.SetSecClass(SecClassGenericPassword) + item.SetService(service) + item.SetAccount(account) + return DeleteItem(item) +} + +// DeleteItem removes a Item +func DeleteItem(item Item) error { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + + errCode := C.SecItemDelete(cfDict) + return checkError(errCode) +} + +// GetAccountsForService is deprecated +func GetAccountsForService(service string) ([]string, error) { + return GetGenericPasswordAccounts(service) +} + +// GetGenericPasswordAccounts returns generic password accounts for service. This is a convenience method. +func GetGenericPasswordAccounts(service string) ([]string, error) { + query := NewItem() + query.SetSecClass(SecClassGenericPassword) + query.SetService(service) + query.SetMatchLimit(MatchLimitAll) + query.SetReturnAttributes(true) + results, err := QueryItem(query) + if err != nil { + return nil, err + } + + accounts := make([]string, 0, len(results)) + for _, r := range results { + accounts = append(accounts, r.Account) + } + + return accounts, nil +} + +// GetGenericPassword returns password data for service and account. This is a convenience method. +// If item is not found returns nil, nil. +func GetGenericPassword(service string, account string, label string, accessGroup string) ([]byte, error) { + query := NewItem() + query.SetSecClass(SecClassGenericPassword) + query.SetService(service) + query.SetAccount(account) + query.SetLabel(label) + query.SetAccessGroup(accessGroup) + query.SetMatchLimit(MatchLimitOne) + query.SetReturnData(true) + results, err := QueryItem(query) + if err != nil { + return nil, err + } + if len(results) > 1 { + return nil, fmt.Errorf("Too many results") + } + if len(results) == 1 { + return results[0].Data, nil + } + return nil, nil +} diff --git a/vendor/github.com/99designs/go-keychain/macos.go b/vendor/github.com/99designs/go-keychain/macos.go new file mode 100644 index 00000000..4004349a --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/macos.go @@ -0,0 +1,272 @@ +// +build darwin,!ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security +#cgo CFLAGS: -w + +#include +#include +*/ +import "C" +import ( + "os" + "unsafe" +) + +// AccessibleKey is key for kSecAttrAccessible +var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) +var accessibleTypeRef = map[Accessible]C.CFTypeRef{ + AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), + AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), + AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), + AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), + AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), + + // Only available in 10.10 + //AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), +} + +var ( + // AccessKey is key for kSecAttrAccess + AccessKey = attrKey(C.CFTypeRef(C.kSecAttrAccess)) +) + +// createAccess creates a SecAccessRef as CFTypeRef. +// The returned SecAccessRef, if non-nil, must be released via CFRelease. +func createAccess(label string, trustedApplications []string) (C.CFTypeRef, error) { + var err error + var labelRef C.CFStringRef + if labelRef, err = StringToCFString(label); err != nil { + return 0, err + } + defer C.CFRelease(C.CFTypeRef(labelRef)) + + var trustedApplicationsArray C.CFArrayRef + if trustedApplications != nil { + if len(trustedApplications) > 0 { + // Always prepend with empty string which signifies that we + // include a NULL application, which means ourselves. + trustedApplications = append([]string{""}, trustedApplications...) + } + + var trustedApplicationsRefs []C.CFTypeRef + for _, trustedApplication := range trustedApplications { + trustedApplicationRef, createErr := createTrustedApplication(trustedApplication) + if createErr != nil { + return 0, createErr + } + defer C.CFRelease(trustedApplicationRef) + trustedApplicationsRefs = append(trustedApplicationsRefs, trustedApplicationRef) + } + + trustedApplicationsArray = ArrayToCFArray(trustedApplicationsRefs) + defer C.CFRelease(C.CFTypeRef(trustedApplicationsArray)) + } + + var access C.SecAccessRef + errCode := C.SecAccessCreate(labelRef, trustedApplicationsArray, &access) //nolint + err = checkError(errCode) + if err != nil { + return 0, err + } + + return C.CFTypeRef(access), nil +} + +// createTrustedApplication creates a SecTrustedApplicationRef as a CFTypeRef. +// The returned SecTrustedApplicationRef, if non-nil, must be released via CFRelease. +func createTrustedApplication(trustedApplication string) (C.CFTypeRef, error) { + var trustedApplicationCStr *C.char + if trustedApplication != "" { + trustedApplicationCStr = C.CString(trustedApplication) + defer C.free(unsafe.Pointer(trustedApplicationCStr)) + } + + var trustedApplicationRef C.SecTrustedApplicationRef + errCode := C.SecTrustedApplicationCreateFromPath(trustedApplicationCStr, &trustedApplicationRef) //nolint + err := checkError(errCode) + if err != nil { + return 0, err + } + + return C.CFTypeRef(trustedApplicationRef), nil +} + +// Access defines whats applications can use the keychain item +type Access struct { + Label string + TrustedApplications []string +} + +// Convert converts Access to CFTypeRef. +// The returned CFTypeRef, if non-nil, must be released via CFRelease. +func (a Access) Convert() (C.CFTypeRef, error) { + return createAccess(a.Label, a.TrustedApplications) +} + +// SetAccess sets Access on Item +func (k *Item) SetAccess(a *Access) { + if a != nil { + k.attr[AccessKey] = a + } else { + delete(k.attr, AccessKey) + } +} + +// DeleteItemRef deletes a keychain item reference. +func DeleteItemRef(ref C.CFTypeRef) error { + errCode := C.SecKeychainItemDelete(C.SecKeychainItemRef(ref)) + return checkError(errCode) +} + +var ( + // KeychainKey is key for kSecUseKeychain + KeychainKey = attrKey(C.CFTypeRef(C.kSecUseKeychain)) + // MatchSearchListKey is key for kSecMatchSearchList + MatchSearchListKey = attrKey(C.CFTypeRef(C.kSecMatchSearchList)) +) + +// Keychain represents the path to a specific OSX keychain +type Keychain struct { + path string +} + +// NewKeychain creates a new keychain file with a password +func NewKeychain(path string, password string) (Keychain, error) { + return newKeychain(path, password, false) +} + +// NewKeychainWithPrompt creates a new Keychain and prompts user for password +func NewKeychainWithPrompt(path string) (Keychain, error) { + return newKeychain(path, "", true) +} + +func newKeychain(path, password string, promptUser bool) (Keychain, error) { + pathRef := C.CString(path) + defer C.free(unsafe.Pointer(pathRef)) + + var errCode C.OSStatus + var kref C.SecKeychainRef + + if promptUser { + errCode = C.SecKeychainCreate(pathRef, C.UInt32(0), nil, C.Boolean(1), 0, &kref) //nolint + } else { + passwordRef := C.CString(password) + defer C.free(unsafe.Pointer(passwordRef)) + errCode = C.SecKeychainCreate(pathRef, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(0), 0, &kref) //nolint + } + + if err := checkError(errCode); err != nil { + return Keychain{}, err + } + + // TODO: Without passing in kref I get 'One or more parameters passed to the function were not valid (-50)' + defer Release(C.CFTypeRef(kref)) + + return Keychain{ + path: path, + }, nil +} + +// NewWithPath to use an existing keychain +func NewWithPath(path string) Keychain { + return Keychain{ + path: path, + } +} + +// Status returns the status of the keychain +func (kc Keychain) Status() error { + // returns no error even if it doesn't exist + kref, err := openKeychainRef(kc.path) + if err != nil { + return err + } + defer C.CFRelease(C.CFTypeRef(kref)) + + var status C.SecKeychainStatus + return checkError(C.SecKeychainGetStatus(kref, &status)) +} + +// The returned SecKeychainRef, if non-nil, must be released via CFRelease. +func openKeychainRef(path string) (C.SecKeychainRef, error) { + pathName := C.CString(path) + defer C.free(unsafe.Pointer(pathName)) + + var kref C.SecKeychainRef + if err := checkError(C.SecKeychainOpen(pathName, &kref)); err != nil { //nolint + return 0, err + } + + return kref, nil +} + +// UnlockAtPath unlocks keychain at path +func UnlockAtPath(path string, password string) error { + kref, err := openKeychainRef(path) + defer Release(C.CFTypeRef(kref)) + if err != nil { + return err + } + passwordRef := C.CString(password) + defer C.free(unsafe.Pointer(passwordRef)) + return checkError(C.SecKeychainUnlock(kref, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(1))) +} + +// LockAtPath locks keychain at path +func LockAtPath(path string) error { + kref, err := openKeychainRef(path) + defer Release(C.CFTypeRef(kref)) + if err != nil { + return err + } + return checkError(C.SecKeychainLock(kref)) +} + +// Delete the Keychain +func (kc *Keychain) Delete() error { + return os.Remove(kc.path) +} + +// Convert Keychain to CFTypeRef. +// The returned CFTypeRef, if non-nil, must be released via CFRelease. +func (kc Keychain) Convert() (C.CFTypeRef, error) { + keyRef, err := openKeychainRef(kc.path) + return C.CFTypeRef(keyRef), err +} + +type keychainArray []Keychain + +// Convert the keychainArray to a CFTypeRef. +// The returned CFTypeRef, if non-nil, must be released via CFRelease. +func (ka keychainArray) Convert() (C.CFTypeRef, error) { + var refs = make([]C.CFTypeRef, len(ka)) + var err error + + for idx, kc := range ka { + if refs[idx], err = kc.Convert(); err != nil { + // If we error trying to convert lets release any we converted before + for _, ref := range refs { + if ref != 0 { + Release(ref) + } + } + return 0, err + } + } + + return C.CFTypeRef(ArrayToCFArray(refs)), nil +} + +// SetMatchSearchList sets match type on keychains +func (k *Item) SetMatchSearchList(karr ...Keychain) { + k.attr[MatchSearchListKey] = keychainArray(karr) +} + +// UseKeychain tells item to use the specified Keychain +func (k *Item) UseKeychain(kc Keychain) { + k.attr[KeychainKey] = kc +} diff --git a/vendor/github.com/99designs/go-keychain/util.go b/vendor/github.com/99designs/go-keychain/util.go new file mode 100644 index 00000000..29cbfc66 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/util.go @@ -0,0 +1,31 @@ +package keychain + +import ( + "crypto/rand" + "encoding/base32" + "strings" +) + +var randRead = rand.Read + +// RandomID returns random ID (base32) string with prefix, using 256 bits as +// recommended by tptacek: https://gist.github.com/tqbf/be58d2d39690c3b366ad +func RandomID(prefix string) (string, error) { + buf, err := RandBytes(32) + if err != nil { + return "", err + } + str := base32.StdEncoding.EncodeToString(buf) + str = strings.Replace(str, "=", "", -1) + str = prefix + str + return str, nil +} + +// RandBytes returns random bytes of length +func RandBytes(length int) ([]byte, error) { + buf := make([]byte, length) + if _, err := randRead(buf); err != nil { + return nil, err + } + return buf, nil +} diff --git a/vendor/github.com/99designs/keyring/.gitattributes b/vendor/github.com/99designs/keyring/.gitattributes new file mode 100644 index 00000000..d207b180 --- /dev/null +++ b/vendor/github.com/99designs/keyring/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf diff --git a/vendor/github.com/99designs/keyring/.gitignore b/vendor/github.com/99designs/keyring/.gitignore new file mode 100644 index 00000000..8000dd9d --- /dev/null +++ b/vendor/github.com/99designs/keyring/.gitignore @@ -0,0 +1 @@ +.vagrant diff --git a/vendor/github.com/99designs/keyring/.golangci.yml b/vendor/github.com/99designs/keyring/.golangci.yml new file mode 100644 index 00000000..f83428ef --- /dev/null +++ b/vendor/github.com/99designs/keyring/.golangci.yml @@ -0,0 +1,29 @@ +linters: + enable: + - bodyclose + - contextcheck + - depguard + - durationcheck + - dupl + - errchkjson + - errname + - exhaustive + - exportloopref + - gocritic + - gofmt + - goimports + - makezero + - misspell + - nakedret + - nilerr + - nilnil + - noctx + - prealloc + - revive + # - rowserrcheck + - thelper + - tparallel + - unconvert + - unparam + # - wastedassign + - whitespace diff --git a/vendor/github.com/99designs/keyring/LICENSE b/vendor/github.com/99designs/keyring/LICENSE new file mode 100644 index 00000000..0fe9e469 --- /dev/null +++ b/vendor/github.com/99designs/keyring/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 99designs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/99designs/keyring/README.md b/vendor/github.com/99designs/keyring/README.md new file mode 100644 index 00000000..629a6aaf --- /dev/null +++ b/vendor/github.com/99designs/keyring/README.md @@ -0,0 +1,67 @@ +Keyring +======= +[![Build Status](https://github.com/99designs/keyring/workflows/Continuous%20Integration/badge.svg)](https://github.com/99designs/keyring/actions) +[![Documentation](https://godoc.org/github.com/99designs/keyring?status.svg)](https://godoc.org/github.com/99designs/keyring) + +Keyring provides a common interface to a range of secure credential storage services. Originally developed as part of [AWS Vault](https://github.com/99designs/aws-vault), a command line tool for securely managing AWS access from developer workstations. + +Currently Keyring supports the following backends + * [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac) + * [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager) + * Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)) + * [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5) + * [Pass](https://www.passwordstore.org/) + * [Encrypted file (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) + * [KeyCtl](https://linux.die.net/man/1/keyctl) + + +## Usage + +The short version of how to use keyring is shown below. + +```go +ring, _ := keyring.Open(keyring.Config{ + ServiceName: "example", +}) + +_ = ring.Set(keyring.Item{ + Key: "foo", + Data: []byte("secret-bar"), +}) + +i, _ := ring.Get("foo") + +fmt.Printf("%s", i.Data) +``` + +For more detail on the API please check [the keyring godocs](https://godoc.org/github.com/99designs/keyring) + + +## Testing + +[Vagrant](https://www.vagrantup.com/) is used to create linux and windows test environments. + +```bash +# Start vagrant +vagrant up + +# Run go tests on all platforms +./bin/go-test +``` + + +## Contributing + +Contributions to the keyring package are most welcome from engineers of all backgrounds and skill levels. In particular the addition of extra backends across popular operating systems would be appreciated. + +This project will adhere to the [Go Community Code of Conduct](https://golang.org/conduct) in the github provided discussion spaces, with the moderators being the 99designs engineering team. + +To make a contribution: + + * Fork the repository + * Make your changes on the fork + * Submit a pull request back to this repo with a clear description of the problem you're solving + * Ensure your PR passes all current (and new) tests + * Ideally verify that [aws-vault](https://github.com/99designs/aws-vault) works with your changes (optional) + +...and we'll do our best to get your work merged in diff --git a/vendor/github.com/99designs/keyring/Vagrantfile b/vendor/github.com/99designs/keyring/Vagrantfile new file mode 100644 index 00000000..7d30d2ac --- /dev/null +++ b/vendor/github.com/99designs/keyring/Vagrantfile @@ -0,0 +1,85 @@ +Vagrant.configure("2") do |config| + + config.vm.define "linux" do |linux| + linux.vm.box = "generic/fedora32" + + linux.vm.provider "virtualbox" do |vb| + vb.gui = true + vb.memory = 2048 + vb.cpus = 2 + + # VBoxVGA flickers constantly, use vmsvga instead which doesn't have that problem + vb.customize ["modifyvm", :id, "--graphicscontroller", "vmsvga"] + end + + # mount the project into /keyring + linux.vm.synced_folder ".", "/keyring" + + # install gnome desktop and auto login + linux.vm.provision "shell", inline: "sudo dnf install -y --exclude='gnome-initial-setup' @gnome-desktop langpacks-en" + linux.vm.provision "shell", inline: <<-SHELL + sudo sed -i -e 's/\\[daemon\\]/\\[daemon\\]\\nAutomaticLoginEnable=True\\nAutomaticLogin=vagrant\\n/' \ + /etc/gdm/custom.conf + SHELL + linux.vm.provision "shell", inline: "sudo systemctl set-default graphical.target" + linux.vm.provision "shell", inline: "sudo systemctl isolate graphical.target" + + # set the root password - sometimes prompts show up in gnome needing to install software + linux.vm.provision "shell", inline: "echo 'vagrant' | sudo passwd root --stdin" + + # install gnome keyring + linux.vm.provision "shell", inline: "sudo dnf install -y gnome-keyring seahorse" + + # install kwallet + linux.vm.provision "shell", inline: "sudo dnf install -y kwalletmanager5" + + # install pass + linux.vm.provision "shell", inline: "sudo dnf install -y pass" + + # install golang + linux.vm.provision "shell", inline: "sudo dnf install -y go" + end + + + config.vm.define "windows" do |windows| + windows.vm.box = "StefanScherer/windows_10" + + windows.vm.provider "virtualbox" do |vb| + vb.gui = true + vb.memory = 2048 + vb.cpus = 2 + end + + # mount the project into c:\keyring + windows.vm.synced_folder ".", "/keyring" + + # install chocolately + windows.vm.provision "shell", privileged: true, inline: <<-SHELL + Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + choco feature disable -n=showDownloadProgress + SHELL + + # install golang + windows.vm.provision "shell", privileged: true, inline: "choco install -y git golang" + end + + config.vm.post_up_message = <<-MESSAGE + There are 2 vagrant boxes: + - linux + - OS: Fedora 32 with Gnome Desktop + - The keyring directory is mounted at /keyring + - Get a shell with 'vagrant ssh linux' + - When running go test, you'll need to use the GUI to click "Continue" on the prompts + - After provisioning, adjusting the virtualbox GUI window size doesn't cause the resolution to update. A 'vagrant reload linux' solves the problem + - windows + - OS: Windows 10 + - The keyring directory is mounted at C:\keyring + - Get a shell by starting PowerShell in the GUI + - You can run commands remotely using 'vagrant winrm -e windows CMD'. You'll need the -e (elevated privileges) if you want to interact with wincred + + Automated scripts for running go test on vagrant boxes (run these locally): + - ./bin/go-test-linux - Run tests on Linux + - ./bin/go-test-windows - Run tests on Windows + - ./bin/go-test - Run all tests - locally, linux and windows + MESSAGE +end diff --git a/vendor/github.com/99designs/keyring/array.go b/vendor/github.com/99designs/keyring/array.go new file mode 100644 index 00000000..3179cb59 --- /dev/null +++ b/vendor/github.com/99designs/keyring/array.go @@ -0,0 +1,54 @@ +package keyring + +// ArrayKeyring is a mock/non-secure backend that meets the Keyring interface. +// It is intended to be used to aid unit testing of code that relies on the package. +// NOTE: Do not use in production code. +type ArrayKeyring struct { + items map[string]Item +} + +// NewArrayKeyring returns an ArrayKeyring, optionally constructed with an initial slice +// of items. +func NewArrayKeyring(initial []Item) *ArrayKeyring { + kr := &ArrayKeyring{} + for _, i := range initial { + _ = kr.Set(i) + } + return kr +} + +// Get returns an Item matching Key. +func (k *ArrayKeyring) Get(key string) (Item, error) { + if i, ok := k.items[key]; ok { + return i, nil + } + return Item{}, ErrKeyNotFound +} + +// Set will store an item on the mock Keyring. +func (k *ArrayKeyring) Set(i Item) error { + if k.items == nil { + k.items = map[string]Item{} + } + k.items[i.Key] = i + return nil +} + +// Remove will delete an Item from the Keyring. +func (k *ArrayKeyring) Remove(key string) error { + delete(k.items, key) + return nil +} + +// Keys provides a slice of all Item keys on the Keyring. +func (k *ArrayKeyring) Keys() ([]string, error) { + var keys = []string{} + for key := range k.items { + keys = append(keys, key) + } + return keys, nil +} + +func (k *ArrayKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNeedsCredentials +} diff --git a/vendor/github.com/99designs/keyring/config.go b/vendor/github.com/99designs/keyring/config.go new file mode 100644 index 00000000..590af7cf --- /dev/null +++ b/vendor/github.com/99designs/keyring/config.go @@ -0,0 +1,58 @@ +package keyring + +// Config contains configuration for keyring. +type Config struct { + // AllowedBackends is a whitelist of backend providers that can be used. Nil means all available. + AllowedBackends []BackendType + + // ServiceName is a generic service name that is used by backends that support the concept + ServiceName string + + // MacOSKeychainNameKeychainName is the name of the macOS keychain that is used + KeychainName string + + // KeychainTrustApplication is whether the calling application should be trusted by default by items + KeychainTrustApplication bool + + // KeychainSynchronizable is whether the item can be synchronized to iCloud + KeychainSynchronizable bool + + // KeychainAccessibleWhenUnlocked is whether the item is accessible when the device is locked + KeychainAccessibleWhenUnlocked bool + + // KeychainPasswordFunc is an optional function used to prompt the user for a password + KeychainPasswordFunc PromptFunc + + // FilePasswordFunc is a required function used to prompt the user for a password + FilePasswordFunc PromptFunc + + // FileDir is the directory that keyring files are stored in, ~/ is resolved to the users' home dir + FileDir string + + // KeyCtlScope is the scope of the kernel keyring (either "user", "session", "process" or "thread") + KeyCtlScope string + + // KeyCtlPerm is the permission mask to use for new keys + KeyCtlPerm uint32 + + // KWalletAppID is the application id for KWallet + KWalletAppID string + + // KWalletFolder is the folder for KWallet + KWalletFolder string + + // LibSecretCollectionName is the name collection in secret-service + LibSecretCollectionName string + + // PassDir is the pass password-store directory, ~/ is resolved to the users' home dir + PassDir string + + // PassCmd is the name of the pass executable + PassCmd string + + // PassPrefix is a string prefix to prepend to the item path stored in pass + PassPrefix string + + // WinCredPrefix is a string prefix to prepend to the key name + WinCredPrefix string +} diff --git a/vendor/github.com/99designs/keyring/docker-compose.yml b/vendor/github.com/99designs/keyring/docker-compose.yml new file mode 100644 index 00000000..90202209 --- /dev/null +++ b/vendor/github.com/99designs/keyring/docker-compose.yml @@ -0,0 +1,7 @@ +version: "3.9" +services: + keyring: + image: golang:1.19 + volumes: + - .:/usr/local/src/keyring + working_dir: /usr/local/src/keyring diff --git a/vendor/github.com/99designs/keyring/file.go b/vendor/github.com/99designs/keyring/file.go new file mode 100644 index 00000000..0d25b572 --- /dev/null +++ b/vendor/github.com/99designs/keyring/file.go @@ -0,0 +1,180 @@ +package keyring + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + jose "github.com/dvsekhvalnov/jose2go" + "github.com/mtibben/percent" +) + +func init() { + supportedBackends[FileBackend] = opener(func(cfg Config) (Keyring, error) { + return &fileKeyring{ + dir: cfg.FileDir, + passwordFunc: cfg.FilePasswordFunc, + }, nil + }) +} + +var filenameEscape = func(s string) string { + return percent.Encode(s, "/") +} +var filenameUnescape = percent.Decode + +type fileKeyring struct { + dir string + passwordFunc PromptFunc + password string +} + +func (k *fileKeyring) resolveDir() (string, error) { + if k.dir == "" { + return "", fmt.Errorf("No directory provided for file keyring") + } + + dir, err := ExpandTilde(k.dir) + if err != nil { + return "", err + } + + stat, err := os.Stat(dir) + if os.IsNotExist(err) { + err = os.MkdirAll(dir, 0700) + } else if err != nil && stat != nil && !stat.IsDir() { + err = fmt.Errorf("%s is a file, not a directory", dir) + } + + return dir, err +} + +func (k *fileKeyring) unlock() error { + dir, err := k.resolveDir() + if err != nil { + return err + } + + if k.password == "" { + pwd, err := k.passwordFunc(fmt.Sprintf("Enter passphrase to unlock %q", dir)) + if err != nil { + return err + } + k.password = pwd + } + + return nil +} + +func (k *fileKeyring) Get(key string) (Item, error) { + filename, err := k.filename(key) + if err != nil { + return Item{}, err + } + + bytes, err := os.ReadFile(filename) + if os.IsNotExist(err) { + return Item{}, ErrKeyNotFound + } else if err != nil { + return Item{}, err + } + + if err = k.unlock(); err != nil { + return Item{}, err + } + + payload, _, err := jose.Decode(string(bytes), k.password) + if err != nil { + return Item{}, err + } + + var decoded Item + err = json.Unmarshal([]byte(payload), &decoded) + + return decoded, err +} + +func (k *fileKeyring) GetMetadata(key string) (Metadata, error) { + filename, err := k.filename(key) + if err != nil { + return Metadata{}, err + } + + stat, err := os.Stat(filename) + if os.IsNotExist(err) { + return Metadata{}, ErrKeyNotFound + } else if err != nil { + return Metadata{}, err + } + + // For the File provider, all internal data is encrypted, not just the + // credentials. Thus we only have the timestamps. Return a nil *Item. + // + // If we want to change this ... how portable are extended file attributes + // these days? Would it break user expectations of the security model to + // leak data into those? I'm hesitant to do so. + + return Metadata{ + ModificationTime: stat.ModTime(), + }, nil +} + +func (k *fileKeyring) Set(i Item) error { + bytes, err := json.Marshal(i) + if err != nil { + return err + } + + if err = k.unlock(); err != nil { + return err + } + + token, err := jose.Encrypt(string(bytes), jose.PBES2_HS256_A128KW, jose.A256GCM, k.password, + jose.Headers(map[string]interface{}{ + "created": time.Now().String(), + })) + if err != nil { + return err + } + + filename, err := k.filename(i.Key) + if err != nil { + return err + } + return os.WriteFile(filename, []byte(token), 0600) +} + +func (k *fileKeyring) filename(key string) (string, error) { + dir, err := k.resolveDir() + if err != nil { + return "", err + } + + return filepath.Join(dir, filenameEscape(key)), nil +} + +func (k *fileKeyring) Remove(key string) error { + filename, err := k.filename(key) + if err != nil { + return err + } + + return os.Remove(filename) +} + +func (k *fileKeyring) Keys() ([]string, error) { + dir, err := k.resolveDir() + if err != nil { + return nil, err + } + + var keys = []string{} + files, _ := os.ReadDir(dir) + for _, f := range files { + keys = append(keys, filenameUnescape(f.Name())) + } + + return keys, nil +} diff --git a/vendor/github.com/99designs/keyring/keychain.go b/vendor/github.com/99designs/keyring/keychain.go new file mode 100644 index 00000000..d4e634e1 --- /dev/null +++ b/vendor/github.com/99designs/keyring/keychain.go @@ -0,0 +1,301 @@ +//go:build darwin && cgo +// +build darwin,cgo + +package keyring + +import ( + "errors" + "fmt" + + gokeychain "github.com/99designs/go-keychain" +) + +type keychain struct { + path string + service string + + passwordFunc PromptFunc + + isSynchronizable bool + isAccessibleWhenUnlocked bool + isTrusted bool +} + +func init() { + supportedBackends[KeychainBackend] = opener(func(cfg Config) (Keyring, error) { + kc := &keychain{ + service: cfg.ServiceName, + passwordFunc: cfg.KeychainPasswordFunc, + + // Set the isAccessibleWhenUnlocked to the boolean value of + // KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value. + // See: https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked + isAccessibleWhenUnlocked: cfg.KeychainAccessibleWhenUnlocked, + } + if cfg.KeychainName != "" { + kc.path = cfg.KeychainName + ".keychain" + } + if cfg.KeychainTrustApplication { + kc.isTrusted = true + } + return kc, nil + }) +} + +func (k *keychain) Get(key string) (Item, error) { + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(k.service) + query.SetAccount(key) + query.SetMatchLimit(gokeychain.MatchLimitOne) + query.SetReturnAttributes(true) + query.SetReturnData(true) + + if k.path != "" { + // When we are querying, we don't create by default + query.SetMatchSearchList(gokeychain.NewWithPath(k.path)) + } + + debugf("Querying keychain for service=%q, account=%q, keychain=%q", k.service, key, k.path) + results, err := gokeychain.QueryItem(query) + if err == gokeychain.ErrorItemNotFound || len(results) == 0 { + debugf("No results found") + return Item{}, ErrKeyNotFound + } + + if err != nil { + debugf("Error: %#v", err) + return Item{}, err + } + + item := Item{ + Key: key, + Data: results[0].Data, + Label: results[0].Label, + Description: results[0].Description, + } + + debugf("Found item %q", results[0].Label) + return item, nil +} + +func (k *keychain) GetMetadata(key string) (Metadata, error) { + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(k.service) + query.SetAccount(key) + query.SetMatchLimit(gokeychain.MatchLimitOne) + query.SetReturnAttributes(true) + query.SetReturnData(false) + query.SetReturnRef(true) + + debugf("Querying keychain for metadata of service=%q, account=%q, keychain=%q", k.service, key, k.path) + results, err := gokeychain.QueryItem(query) + if err == gokeychain.ErrorItemNotFound || len(results) == 0 { + debugf("No results found") + return Metadata{}, ErrKeyNotFound + } else if err != nil { + debugf("Error: %#v", err) + return Metadata{}, err + } + + md := Metadata{ + Item: &Item{ + Key: key, + Label: results[0].Label, + Description: results[0].Description, + }, + ModificationTime: results[0].ModificationDate, + } + + debugf("Found metadata for %q", md.Item.Label) + + return md, nil +} + +func (k *keychain) updateItem(kc gokeychain.Keychain, kcItem gokeychain.Item, account string) error { + queryItem := gokeychain.NewItem() + queryItem.SetSecClass(gokeychain.SecClassGenericPassword) + queryItem.SetService(k.service) + queryItem.SetAccount(account) + queryItem.SetMatchLimit(gokeychain.MatchLimitOne) + queryItem.SetReturnAttributes(true) + + if k.path != "" { + queryItem.SetMatchSearchList(kc) + } + + results, err := gokeychain.QueryItem(queryItem) + if err != nil { + return fmt.Errorf("Failed to query keychain: %v", err) + } + if len(results) == 0 { + return errors.New("no results") + } + + // Don't call SetAccess() as this will cause multiple prompts on update, even when we are not updating the AccessList + kcItem.SetAccess(nil) + + if err := gokeychain.UpdateItem(queryItem, kcItem); err != nil { + return fmt.Errorf("Failed to update item in keychain: %v", err) + } + + return nil +} + +func (k *keychain) Set(item Item) error { + var kc gokeychain.Keychain + + // when we are setting a value, we create or open + if k.path != "" { + var err error + kc, err = k.createOrOpen() + if err != nil { + return err + } + } + + kcItem := gokeychain.NewItem() + kcItem.SetSecClass(gokeychain.SecClassGenericPassword) + kcItem.SetService(k.service) + kcItem.SetAccount(item.Key) + kcItem.SetLabel(item.Label) + kcItem.SetDescription(item.Description) + kcItem.SetData(item.Data) + + if k.path != "" { + kcItem.UseKeychain(kc) + } + + if k.isSynchronizable && !item.KeychainNotSynchronizable { + kcItem.SetSynchronizable(gokeychain.SynchronizableYes) + } + + if k.isAccessibleWhenUnlocked { + kcItem.SetAccessible(gokeychain.AccessibleWhenUnlocked) + } + + isTrusted := k.isTrusted && !item.KeychainNotTrustApplication + + if isTrusted { + debugf("Keychain item trusts keyring") + kcItem.SetAccess(&gokeychain.Access{ + Label: item.Label, + TrustedApplications: nil, + }) + } else { + debugf("Keychain item doesn't trust keyring") + kcItem.SetAccess(&gokeychain.Access{ + Label: item.Label, + TrustedApplications: []string{}, + }) + } + + debugf("Adding service=%q, label=%q, account=%q, trusted=%v to osx keychain %q", k.service, item.Label, item.Key, isTrusted, k.path) + + err := gokeychain.AddItem(kcItem) + + if err == gokeychain.ErrorDuplicateItem { + debugf("Item already exists, updating") + err = k.updateItem(kc, kcItem, item.Key) + } + + if err != nil { + return err + } + + return nil +} + +func (k *keychain) Remove(key string) error { + item := gokeychain.NewItem() + item.SetSecClass(gokeychain.SecClassGenericPassword) + item.SetService(k.service) + item.SetAccount(key) + + if k.path != "" { + kc := gokeychain.NewWithPath(k.path) + + if err := kc.Status(); err != nil { + if err == gokeychain.ErrorNoSuchKeychain { + return ErrKeyNotFound + } + return err + } + + item.SetMatchSearchList(kc) + } + + debugf("Removing keychain item service=%q, account=%q, keychain %q", k.service, key, k.path) + err := gokeychain.DeleteItem(item) + if err == gokeychain.ErrorItemNotFound { + return ErrKeyNotFound + } + + return err +} + +func (k *keychain) Keys() ([]string, error) { + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(k.service) + query.SetMatchLimit(gokeychain.MatchLimitAll) + query.SetReturnAttributes(true) + + if k.path != "" { + kc := gokeychain.NewWithPath(k.path) + + if err := kc.Status(); err != nil { + if err == gokeychain.ErrorNoSuchKeychain { + return []string{}, nil + } + return nil, err + } + + query.SetMatchSearchList(kc) + } + + debugf("Querying keychain for service=%q, keychain=%q", k.service, k.path) + results, err := gokeychain.QueryItem(query) + if err != nil { + return nil, err + } + + debugf("Found %d results", len(results)) + accountNames := make([]string, len(results)) + for idx, r := range results { + accountNames[idx] = r.Account + } + + return accountNames, nil +} + +func (k *keychain) createOrOpen() (gokeychain.Keychain, error) { + kc := gokeychain.NewWithPath(k.path) + + debugf("Checking keychain status") + err := kc.Status() + if err == nil { + debugf("Keychain status returned nil, keychain exists") + return kc, nil + } + + debugf("Keychain status returned error: %v", err) + + if err != gokeychain.ErrorNoSuchKeychain { + return gokeychain.Keychain{}, err + } + + if k.passwordFunc == nil { + debugf("Creating keychain %s with prompt", k.path) + return gokeychain.NewKeychainWithPrompt(k.path) + } + + passphrase, err := k.passwordFunc("Enter passphrase for keychain") + if err != nil { + return gokeychain.Keychain{}, err + } + + debugf("Creating keychain %s with provided password", k.path) + return gokeychain.NewKeychain(k.path, passphrase) +} diff --git a/vendor/github.com/99designs/keyring/keyctl.go b/vendor/github.com/99designs/keyring/keyctl.go new file mode 100644 index 00000000..bd8018ab --- /dev/null +++ b/vendor/github.com/99designs/keyring/keyctl.go @@ -0,0 +1,327 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "errors" + "fmt" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +//nolint:revive +const ( + KEYCTL_PERM_VIEW = uint32(1 << 0) + KEYCTL_PERM_READ = uint32(1 << 1) + KEYCTL_PERM_WRITE = uint32(1 << 2) + KEYCTL_PERM_SEARCH = uint32(1 << 3) + KEYCTL_PERM_LINK = uint32(1 << 4) + KEYCTL_PERM_SETATTR = uint32(1 << 5) + KEYCTL_PERM_ALL = uint32((1 << 6) - 1) + + KEYCTL_PERM_OTHERS = 0 + KEYCTL_PERM_GROUP = 8 + KEYCTL_PERM_USER = 16 + KEYCTL_PERM_PROCESS = 24 +) + +// GetPermissions constructs the permission mask from the elements. +func GetPermissions(process, user, group, others uint32) uint32 { + perm := others << KEYCTL_PERM_OTHERS + perm |= group << KEYCTL_PERM_GROUP + perm |= user << KEYCTL_PERM_USER + perm |= process << KEYCTL_PERM_PROCESS + + return perm +} + +// GetKeyringIDForScope get the keyring ID for a given scope. +func GetKeyringIDForScope(scope string) (int32, error) { + ringRef, err := getKeyringForScope(scope) + if err != nil { + return 0, err + } + id, err := unix.KeyctlGetKeyringID(int(ringRef), false) + return int32(id), err +} + +type keyctlKeyring struct { + keyring int32 + perm uint32 +} + +func init() { + supportedBackends[KeyCtlBackend] = opener(func(cfg Config) (Keyring, error) { + keyring := keyctlKeyring{} + if cfg.KeyCtlPerm > 0 { + keyring.perm = cfg.KeyCtlPerm + } + + parent, err := getKeyringForScope(cfg.KeyCtlScope) + if err != nil { + return nil, fmt.Errorf("accessing %q keyring failed: %v", cfg.KeyCtlScope, err) + } + + // Check for named keyrings + keyring.keyring = parent + if cfg.ServiceName != "" { + namedKeyring, err := keyctlSearch(parent, "keyring", cfg.ServiceName) + if err != nil { + if !errors.Is(err, syscall.ENOKEY) { + return nil, fmt.Errorf("opening named %q keyring failed: %v", cfg.KeyCtlScope, err) + } + + // Keyring does not yet exist, create it + namedKeyring, err = keyring.createNamedKeyring(parent, cfg.ServiceName) + if err != nil { + return nil, fmt.Errorf("creating named %q keyring failed: %v", cfg.KeyCtlScope, err) + } + } + keyring.keyring = namedKeyring + } + + return &keyring, nil + }) +} + +func (k *keyctlKeyring) Get(name string) (Item, error) { + key, err := keyctlSearch(k.keyring, "user", name) + if err != nil { + if errors.Is(err, syscall.ENOKEY) { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + // data, err := key.Get() + data, err := keyctlRead(key) + if err != nil { + return Item{}, err + } + + item := Item{ + Key: name, + Data: data, + } + + return item, nil +} + +// GetMetadata for pass returns an error indicating that it's unsupported for this backend. +// TODO: We can deliver metadata different from the defined ones (e.g. permissions, expire-time, etc). +func (k *keyctlKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNotSupported +} + +func (k *keyctlKeyring) Set(item Item) error { + if k.perm == 0 { + // Keep the default permissions (alswrv-----v------------) + _, err := keyctlAdd(k.keyring, "user", item.Key, item.Data) + return err + } + + // By default we loose possession of the key in anything above the session keyring. + // Together with the default permissions (which cannot be changed during creation) we + // cannot change the permissions without possessing the key. Therefore, create the + // key in the session keyring, change permissions and then link to the target + // keyring and unlink from the intermediate keyring again. + key, err := keyctlAdd(unix.KEY_SPEC_SESSION_KEYRING, "user", item.Key, item.Data) + if err != nil { + return fmt.Errorf("adding key to session failed: %v", err) + } + + if err := keyctlSetperm(key, k.perm); err != nil { + return fmt.Errorf("setting permission 0x%x failed: %v", k.perm, err) + } + + if err := keyctlLink(k.keyring, key); err != nil { + return fmt.Errorf("linking key to keyring failed: %v", err) + } + + if err := keyctlUnlink(unix.KEY_SPEC_SESSION_KEYRING, key); err != nil { + return fmt.Errorf("unlinking key from session failed: %v", err) + } + + return nil +} + +func (k *keyctlKeyring) Remove(name string) error { + key, err := keyctlSearch(k.keyring, "user", name) + if err != nil { + return ErrKeyNotFound + } + + return keyctlUnlink(k.keyring, key) +} + +func (k *keyctlKeyring) Keys() ([]string, error) { + results := []string{} + + data, err := keyctlRead(k.keyring) + if err != nil { + return nil, fmt.Errorf("reading keyring failed: %v", err) + } + ids, err := keyctlConvertKeyBuffer(data) + if err != nil { + return nil, fmt.Errorf("converting raw keylist failed: %v", err) + } + + for _, id := range ids { + info, err := keyctlDescribe(id) + if err != nil { + return nil, err + } + if info["type"] == "user" { + results = append(results, info["description"]) + } + } + + return results, nil +} + +func (k *keyctlKeyring) createNamedKeyring(parent int32, name string) (int32, error) { + if k.perm == 0 { + // Keep the default permissions (alswrv-----v------------) + return keyctlAdd(parent, "keyring", name, nil) + } + + // By default we loose possession of the keyring in anything above the session keyring. + // Together with the default permissions (which cannot be changed during creation) we + // cannot change the permissions without possessing the keyring. Therefore, create the + // keyring linked to the session keyring, change permissions and then link to the target + // keyring and unlink from the intermediate keyring again. + keyring, err := keyctlAdd(unix.KEY_SPEC_SESSION_KEYRING, "keyring", name, nil) + if err != nil { + return 0, fmt.Errorf("creating keyring failed: %v", err) + } + + if err := keyctlSetperm(keyring, k.perm); err != nil { + return 0, fmt.Errorf("setting permission 0x%x failed: %v", k.perm, err) + } + + if err := keyctlLink(k.keyring, keyring); err != nil { + return 0, fmt.Errorf("linking keyring failed: %v", err) + } + + if err := keyctlUnlink(unix.KEY_SPEC_SESSION_KEYRING, keyring); err != nil { + return 0, fmt.Errorf("unlinking keyring from session failed: %v", err) + } + + return keyring, nil +} + +func getKeyringForScope(scope string) (int32, error) { + switch scope { + case "user": + return int32(unix.KEY_SPEC_USER_KEYRING), nil + case "usersession": + return int32(unix.KEY_SPEC_USER_SESSION_KEYRING), nil + case "group": + // Not yet implemented in the kernel + // return int32(unix.KEY_SPEC_GROUP_KEYRING) + return 0, fmt.Errorf("scope %q not yet implemented", scope) + case "session": + return int32(unix.KEY_SPEC_SESSION_KEYRING), nil + case "process": + return int32(unix.KEY_SPEC_PROCESS_KEYRING), nil + case "thread": + return int32(unix.KEY_SPEC_THREAD_KEYRING), nil + } + return 0, fmt.Errorf("unknown scope %q", scope) +} + +func keyctlAdd(parent int32, keytype, key string, data []byte) (int32, error) { + id, err := unix.AddKey(keytype, key, data, int(parent)) + if err != nil { + return 0, err + } + return int32(id), nil +} + +func keyctlSearch(id int32, idtype, name string) (int32, error) { + key, err := unix.KeyctlSearch(int(id), idtype, name, 0) + if err != nil { + return 0, err + } + return int32(key), nil +} + +func keyctlRead(id int32) ([]byte, error) { + var buffer []byte + + for { + length, err := unix.KeyctlBuffer(unix.KEYCTL_READ, int(id), buffer, 0) + if err != nil { + return nil, err + } + + // Return the buffer if it was large enough + if length <= len(buffer) { + return buffer[:length], nil + } + + // Next try with a larger buffer + buffer = make([]byte, length) + } +} + +func keyctlDescribe(id int32) (map[string]string, error) { + description, err := unix.KeyctlString(unix.KEYCTL_DESCRIBE, int(id)) + if err != nil { + return nil, err + } + fields := strings.Split(description, ";") + if len(fields) < 1 { + return nil, fmt.Errorf("no data") + } + + data := make(map[string]string) + names := []string{"type", "uid", "gid", "perm"} // according to keyctlDescribe(3) new fields are added at the end + data["description"] = fields[len(fields)-1] // according to keyctlDescribe(3) description is always last + for i, f := range fields[:len(fields)-1] { + if i >= len(names) { + // Do not stumble upon unknown fields + break + } + data[names[i]] = f + } + + return data, nil +} + +func keyctlLink(parent, child int32) error { + _, _, errno := syscall.Syscall(syscall.SYS_KEYCTL, uintptr(unix.KEYCTL_LINK), uintptr(child), uintptr(parent)) + if errno != 0 { + return errno + } + return nil +} + +func keyctlUnlink(parent, child int32) error { + _, _, errno := syscall.Syscall(syscall.SYS_KEYCTL, uintptr(unix.KEYCTL_UNLINK), uintptr(child), uintptr(parent)) + if errno != 0 { + return errno + } + return nil +} + +func keyctlSetperm(id int32, perm uint32) error { + return unix.KeyctlSetperm(int(id), perm) +} + +func keyctlConvertKeyBuffer(buffer []byte) ([]int32, error) { + if len(buffer)%4 != 0 { + return nil, fmt.Errorf("buffer size %d not a multiple of 4", len(buffer)) + } + + results := make([]int32, 0, len(buffer)/4) + for i := 0; i < len(buffer); i += 4 { + // We need to case in host-native endianess here as this is what we get from the kernel. + r := *((*int32)(unsafe.Pointer(&buffer[i]))) + results = append(results, r) + } + return results, nil +} diff --git a/vendor/github.com/99designs/keyring/keyring.go b/vendor/github.com/99designs/keyring/keyring.go new file mode 100644 index 00000000..12161b79 --- /dev/null +++ b/vendor/github.com/99designs/keyring/keyring.go @@ -0,0 +1,134 @@ +// Package keyring provides a uniform API over a range of desktop credential storage engines. +package keyring + +import ( + "errors" + "log" + "time" +) + +// BackendType is an identifier for a credential storage service. +type BackendType string + +// All currently supported secure storage backends. +const ( + InvalidBackend BackendType = "" + SecretServiceBackend BackendType = "secret-service" + KeychainBackend BackendType = "keychain" + KeyCtlBackend BackendType = "keyctl" + KWalletBackend BackendType = "kwallet" + WinCredBackend BackendType = "wincred" + FileBackend BackendType = "file" + PassBackend BackendType = "pass" +) + +// This order makes sure the OS-specific backends +// are picked over the more generic backends. +var backendOrder = []BackendType{ + // Windows + WinCredBackend, + // MacOS + KeychainBackend, + // Linux + SecretServiceBackend, + KWalletBackend, + KeyCtlBackend, + // General + PassBackend, + FileBackend, +} + +var supportedBackends = map[BackendType]opener{} + +// AvailableBackends provides a slice of all available backend keys on the current OS. +func AvailableBackends() []BackendType { + b := []BackendType{} + for _, k := range backendOrder { + _, ok := supportedBackends[k] + if ok { + b = append(b, k) + } + } + return b +} + +type opener func(cfg Config) (Keyring, error) + +// Open will open a specific keyring backend. +func Open(cfg Config) (Keyring, error) { + if cfg.AllowedBackends == nil { + cfg.AllowedBackends = AvailableBackends() + } + debugf("Considering backends: %v", cfg.AllowedBackends) + for _, backend := range cfg.AllowedBackends { + if opener, ok := supportedBackends[backend]; ok { + openBackend, err := opener(cfg) + if err != nil { + debugf("Failed backend %s: %s", backend, err) + continue + } + return openBackend, nil + } + } + return nil, ErrNoAvailImpl +} + +// Item is a thing stored on the keyring. +type Item struct { + Key string + Data []byte + Label string + Description string + + // Backend specific config + KeychainNotTrustApplication bool + KeychainNotSynchronizable bool +} + +// Metadata is information about a thing stored on the keyring; retrieving +// metadata must not require authentication. The embedded Item should be +// filled in with an empty Data field. +// It's allowed for Item to be a nil pointer, indicating that all we +// have is the timestamps. +type Metadata struct { + *Item + ModificationTime time.Time +} + +// Keyring provides the uniform interface over the underlying backends. +type Keyring interface { + // Returns an Item matching the key or ErrKeyNotFound + Get(key string) (Item, error) + // Returns the non-secret parts of an Item + GetMetadata(key string) (Metadata, error) + // Stores an Item on the keyring + Set(item Item) error + // Removes the item with matching key + Remove(key string) error + // Provides a slice of all keys stored on the keyring + Keys() ([]string, error) +} + +// ErrNoAvailImpl is returned by Open when a backend cannot be found. +var ErrNoAvailImpl = errors.New("Specified keyring backend not available") + +// ErrKeyNotFound is returned by Keyring Get when the item is not on the keyring. +var ErrKeyNotFound = errors.New("The specified item could not be found in the keyring") + +// ErrMetadataNeedsCredentials is returned when Metadata is called against a +// backend which requires credentials even to see metadata. +var ErrMetadataNeedsCredentials = errors.New("The keyring backend requires credentials for metadata access") + +// ErrMetadataNotSupported is returned when Metadata is not available for the backend. +var ErrMetadataNotSupported = errors.New("The keyring backend does not support metadata access") + +var ( + // Debug specifies whether to print debugging output. + Debug bool +) + +func debugf(pattern string, args ...interface{}) { + if Debug { + log.Printf("[keyring] "+pattern, args...) + } +} diff --git a/vendor/github.com/99designs/keyring/kwallet.go b/vendor/github.com/99designs/keyring/kwallet.go new file mode 100644 index 00000000..771baba1 --- /dev/null +++ b/vendor/github.com/99designs/keyring/kwallet.go @@ -0,0 +1,237 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "encoding/json" + "os" + + "github.com/godbus/dbus" +) + +const ( + dbusServiceName = "org.kde.kwalletd5" + dbusPath = "/modules/kwalletd5" +) + +func init() { + if os.Getenv("DISABLE_KWALLET") == "1" { + return + } + + // silently fail if dbus isn't available + _, err := dbus.SessionBus() + if err != nil { + return + } + + supportedBackends[KWalletBackend] = opener(func(cfg Config) (Keyring, error) { + if cfg.ServiceName == "" { + cfg.ServiceName = "kdewallet" + } + + if cfg.KWalletAppID == "" { + cfg.KWalletAppID = "keyring" + } + + if cfg.KWalletFolder == "" { + cfg.KWalletFolder = "keyring" + } + + wallet, err := newKwallet() + if err != nil { + return nil, err + } + + ring := &kwalletKeyring{ + wallet: *wallet, + name: cfg.ServiceName, + appID: cfg.KWalletAppID, + folder: cfg.KWalletFolder, + } + + return ring, ring.openWallet() + }) +} + +type kwalletKeyring struct { + wallet kwalletBinding + name string + handle int32 + appID string + folder string +} + +func (k *kwalletKeyring) openWallet() error { + isOpen, err := k.wallet.IsOpen(k.handle) + if err != nil { + return err + } + + if !isOpen { + handle, err := k.wallet.Open(k.name, 0, k.appID) + if err != nil { + return err + } + k.handle = handle + } + + return nil +} + +func (k *kwalletKeyring) Get(key string) (Item, error) { + err := k.openWallet() + if err != nil { + return Item{}, err + } + + data, err := k.wallet.ReadEntry(k.handle, k.folder, key, k.appID) + if err != nil { + return Item{}, err + } + if len(data) == 0 { + return Item{}, ErrKeyNotFound + } + + item := Item{} + err = json.Unmarshal(data, &item) + if err != nil { + return Item{}, err + } + + return item, nil +} + +// GetMetadata for kwallet returns an error indicating that it's unsupported +// for this backend. +// +// The only APIs found around KWallet are for retrieving content, no indication +// found in docs for methods to use to retrieve metadata without needing unlock +// credentials. +func (k *kwalletKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNeedsCredentials +} + +func (k *kwalletKeyring) Set(item Item) error { + err := k.openWallet() + if err != nil { + return err + } + + data, err := json.Marshal(item) + if err != nil { + return err + } + + err = k.wallet.WriteEntry(k.handle, k.folder, item.Key, data, k.appID) + if err != nil { + return err + } + + return nil +} + +func (k *kwalletKeyring) Remove(key string) error { + err := k.openWallet() + if err != nil { + return err + } + + err = k.wallet.RemoveEntry(k.handle, k.folder, key, k.appID) + if err != nil { + return err + } + + return nil +} + +func (k *kwalletKeyring) Keys() ([]string, error) { + err := k.openWallet() + if err != nil { + return []string{}, err + } + + entries, err := k.wallet.EntryList(k.handle, k.folder, k.appID) + if err != nil { + return []string{}, err + } + + return entries, nil +} + +func newKwallet() (*kwalletBinding, error) { + conn, err := dbus.SessionBus() + if err != nil { + return nil, err + } + + return &kwalletBinding{ + conn.Object(dbusServiceName, dbusPath), + }, nil +} + +// Dumb Dbus bindings for kwallet bindings with types. +type kwalletBinding struct { + dbus dbus.BusObject +} + +// method bool org.kde.KWallet.isOpen(int handle) +func (k *kwalletBinding) IsOpen(handle int32) (bool, error) { + call := k.dbus.Call("org.kde.KWallet.isOpen", 0, handle) + if call.Err != nil { + return false, call.Err + } + + return call.Body[0].(bool), call.Err +} + +// method int org.kde.KWallet.open(QString wallet, qlonglong wId, QString appid) +func (k *kwalletBinding) Open(name string, wID int64, appid string) (int32, error) { + call := k.dbus.Call("org.kde.KWallet.open", 0, name, wID, appid) + if call.Err != nil { + return 0, call.Err + } + + return call.Body[0].(int32), call.Err +} + +// method QStringList org.kde.KWallet.entryList(int handle, QString folder, QString appid) +func (k *kwalletBinding) EntryList(handle int32, folder string, appid string) ([]string, error) { + call := k.dbus.Call("org.kde.KWallet.entryList", 0, handle, folder, appid) + if call.Err != nil { + return []string{}, call.Err + } + + return call.Body[0].([]string), call.Err +} + +// method int org.kde.KWallet.writeEntry(int handle, QString folder, QString key, QByteArray value, QString appid) +func (k *kwalletBinding) WriteEntry(handle int32, folder string, key string, value []byte, appid string) error { + call := k.dbus.Call("org.kde.KWallet.writeEntry", 0, handle, folder, key, value, appid) + if call.Err != nil { + return call.Err + } + + return call.Err +} + +// method int org.kde.KWallet.removeEntry(int handle, QString folder, QString key, QString appid) +func (k *kwalletBinding) RemoveEntry(handle int32, folder string, key string, appid string) error { + call := k.dbus.Call("org.kde.KWallet.removeEntry", 0, handle, folder, key, appid) + if call.Err != nil { + return call.Err + } + + return call.Err +} + +// method QByteArray org.kde.KWallet.readEntry(int handle, QString folder, QString key, QString appid) +func (k *kwalletBinding) ReadEntry(handle int32, folder string, key string, appid string) ([]byte, error) { + call := k.dbus.Call("org.kde.KWallet.readEntry", 0, handle, folder, key, appid) + if call.Err != nil { + return []byte{}, call.Err + } + + return call.Body[0].([]byte), call.Err +} diff --git a/vendor/github.com/99designs/keyring/pass.go b/vendor/github.com/99designs/keyring/pass.go new file mode 100644 index 00000000..5ca0bf0f --- /dev/null +++ b/vendor/github.com/99designs/keyring/pass.go @@ -0,0 +1,166 @@ +//go:build !windows +// +build !windows + +package keyring + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func init() { + supportedBackends[PassBackend] = opener(func(cfg Config) (Keyring, error) { + var err error + + pass := &passKeyring{ + passcmd: cfg.PassCmd, + dir: cfg.PassDir, + prefix: cfg.PassPrefix, + } + + if pass.passcmd == "" { + pass.passcmd = "pass" + } + + if pass.dir == "" { + if passDir, found := os.LookupEnv("PASSWORD_STORE_DIR"); found { + pass.dir = passDir + } else { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + pass.dir = filepath.Join(homeDir, ".password-store") + } + } + + pass.dir, err = ExpandTilde(pass.dir) + if err != nil { + return nil, err + } + + // fail if the pass program is not available + _, err = exec.LookPath(pass.passcmd) + if err != nil { + return nil, errors.New("The pass program is not available") + } + + return pass, nil + }) +} + +type passKeyring struct { + dir string + passcmd string + prefix string +} + +func (k *passKeyring) pass(args ...string) *exec.Cmd { + cmd := exec.Command(k.passcmd, args...) + if k.dir != "" { + cmd.Env = append(os.Environ(), fmt.Sprintf("PASSWORD_STORE_DIR=%s", k.dir)) + } + cmd.Stderr = os.Stderr + + return cmd +} + +func (k *passKeyring) Get(key string) (Item, error) { + if !k.itemExists(key) { + return Item{}, ErrKeyNotFound + } + + name := filepath.Join(k.prefix, key) + cmd := k.pass("show", name) + output, err := cmd.Output() + if err != nil { + return Item{}, err + } + + var decoded Item + err = json.Unmarshal(output, &decoded) + + return decoded, err +} + +func (k *passKeyring) GetMetadata(key string) (Metadata, error) { + return Metadata{}, nil +} + +func (k *passKeyring) Set(i Item) error { + bytes, err := json.Marshal(i) + if err != nil { + return err + } + + name := filepath.Join(k.prefix, i.Key) + cmd := k.pass("insert", "-m", "-f", name) + cmd.Stdin = strings.NewReader(string(bytes)) + + err = cmd.Run() + if err != nil { + return err + } + + return nil +} + +func (k *passKeyring) Remove(key string) error { + if !k.itemExists(key) { + return ErrKeyNotFound + } + + name := filepath.Join(k.prefix, key) + cmd := k.pass("rm", "-f", name) + err := cmd.Run() + if err != nil { + return err + } + + return nil +} + +func (k *passKeyring) itemExists(key string) bool { + var path = filepath.Join(k.dir, k.prefix, key+".gpg") + _, err := os.Stat(path) + + return err == nil +} + +func (k *passKeyring) Keys() ([]string, error) { + var keys = []string{} + var path = filepath.Join(k.dir, k.prefix) + + info, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return keys, nil + } + return keys, err + } + if !info.IsDir() { + return keys, fmt.Errorf("%s is not a directory", path) + } + + err = filepath.Walk(path, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() && filepath.Ext(p) == ".gpg" { + name := strings.TrimPrefix(p, path) + if name[0] == os.PathSeparator { + name = name[1:] + } + keys = append(keys, name[:len(name)-4]) + } + return nil + }) + + return keys, err +} diff --git a/vendor/github.com/99designs/keyring/prompt.go b/vendor/github.com/99designs/keyring/prompt.go new file mode 100644 index 00000000..59ad81c3 --- /dev/null +++ b/vendor/github.com/99designs/keyring/prompt.go @@ -0,0 +1,27 @@ +package keyring + +import ( + "fmt" + "os" + + "golang.org/x/term" +) + +// PromptFunc is a function used to prompt the user for a password. +type PromptFunc func(string) (string, error) + +func TerminalPrompt(prompt string) (string, error) { + fmt.Printf("%s: ", prompt) + b, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", err + } + fmt.Println() + return string(b), nil +} + +func FixedStringPrompt(value string) PromptFunc { + return func(_ string) (string, error) { + return value, nil + } +} diff --git a/vendor/github.com/99designs/keyring/secretservice.go b/vendor/github.com/99designs/keyring/secretservice.go new file mode 100644 index 00000000..2a1f9d99 --- /dev/null +++ b/vendor/github.com/99designs/keyring/secretservice.go @@ -0,0 +1,293 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "encoding/hex" + "encoding/json" + "errors" + + "strings" + + "github.com/godbus/dbus" + "github.com/gsterjov/go-libsecret" +) + +func init() { + // silently fail if dbus isn't available + _, err := dbus.SessionBus() + if err != nil { + return + } + + supportedBackends[SecretServiceBackend] = opener(func(cfg Config) (Keyring, error) { + if cfg.ServiceName == "" { + cfg.ServiceName = "secret-service" + } + if cfg.LibSecretCollectionName == "" { + cfg.LibSecretCollectionName = cfg.ServiceName + } + + service, err := libsecret.NewService() + if err != nil { + return &secretsKeyring{}, err + } + + ring := &secretsKeyring{ + name: cfg.LibSecretCollectionName, + service: service, + } + + return ring, ring.openSecrets() + }) +} + +type secretsKeyring struct { + name string + service *libsecret.Service + collection *libsecret.Collection + session *libsecret.Session +} + +var errCollectionNotFound = errors.New("The collection does not exist. Please add a key first") + +func decodeKeyringString(src string) string { + var dst strings.Builder + for i := 0; i < len(src); i++ { + if src[i] != '_' { + dst.WriteString(string(src[i])) + } else { + if i+3 > len(src) { + return src + } + hexstring := src[i+1 : i+3] + decoded, err := hex.DecodeString(hexstring) + if err != nil { + return src + } + dst.Write(decoded) + i += 2 + } + } + return dst.String() +} + +func (k *secretsKeyring) openSecrets() error { + session, err := k.service.Open() + if err != nil { + return err + } + k.session = session + + // get the collection if it already exists + collections, err := k.service.Collections() + if err != nil { + return err + } + + path := libsecret.DBusPath + "/collection/" + k.name + + for _, collection := range collections { + if decodeKeyringString(string(collection.Path())) == path { + c := collection // fix variable into the local variable to ensure it's referenced correctly, see https://github.com/kyoh86/exportloopref + k.collection = &c + return nil + } + } + + return nil +} + +func (k *secretsKeyring) openCollection() error { + if err := k.openSecrets(); err != nil { + return err + } + + if k.collection == nil { + return errCollectionNotFound + // return &secretsError{fmt.Sprintf( + // "The collection %q does not exist. Please add a key first", + // k.name, + // )} + } + + return nil +} + +func (k *secretsKeyring) Get(key string) (Item, error) { + if err := k.openCollection(); err != nil { + if err == errCollectionNotFound { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + + items, err := k.collection.SearchItems(key) + if err != nil { + return Item{}, err + } + + if len(items) == 0 { + return Item{}, ErrKeyNotFound + } + + // use the first item whenever there are multiples + // with the same profile name + item := items[0] + + locked, err := item.Locked() + if err != nil { + return Item{}, err + } + + if locked { + if err := k.service.Unlock(item); err != nil { + return Item{}, err + } + } + + secret, err := item.GetSecret(k.session) + if err != nil { + return Item{}, err + } + + // pack the secret into the item + var ret Item + if err = json.Unmarshal(secret.Value, &ret); err != nil { + return Item{}, err + } + + return ret, err +} + +// GetMetadata for libsecret returns an error indicating that it's unsupported +// for this backend. +// +// libsecret actually implements a metadata system which we could use, "Secret +// Attributes"; I found no indication in documentation of anything like an +// automatically maintained last-modification timestamp, so to use this we'd +// need to have a SetMetadata API too. Which we're not yet doing, but feel +// free to contribute patches. +func (k *secretsKeyring) GetMetadata(key string) (Metadata, error) { + return Metadata{}, ErrMetadataNeedsCredentials +} + +func (k *secretsKeyring) Set(item Item) error { + err := k.openSecrets() + if err != nil { + return err + } + + // create the collection if it doesn't already exist + if k.collection == nil { + collection, err := k.service.CreateCollection(k.name) + if err != nil { + return err + } + + k.collection = collection + } + + if err := k.ensureCollectionUnlocked(); err != nil { + return err + } + + // create the new item + data, err := json.Marshal(item) + if err != nil { + return err + } + + secret := libsecret.NewSecret(k.session, []byte{}, data, "application/json") + + if _, err := k.collection.CreateItem(item.Key, secret, true); err != nil { + return err + } + + return nil +} + +func (k *secretsKeyring) Remove(key string) error { + if err := k.openCollection(); err != nil { + if err == errCollectionNotFound { + return ErrKeyNotFound + } + return err + } + + items, err := k.collection.SearchItems(key) + if err != nil { + return err + } + + // nothing to delete + if len(items) == 0 { + return nil + } + + // we dont want to delete more than one anyway + // so just get the first item found + item := items[0] + + locked, err := item.Locked() + if err != nil { + return err + } + + if locked { + if err := k.service.Unlock(item); err != nil { + return err + } + } + + if err := item.Delete(); err != nil { + return err + } + + return nil +} + +func (k *secretsKeyring) Keys() ([]string, error) { + if err := k.openCollection(); err != nil { + if err == errCollectionNotFound { + return []string{}, nil + } + return nil, err + } + if err := k.ensureCollectionUnlocked(); err != nil { + return nil, err + } + items, err := k.collection.Items() + if err != nil { + return nil, err + } + keys := []string{} + for _, item := range items { + label, err := item.Label() // FIXME: err is being silently ignored + if err == nil { + keys = append(keys, label) + } + } + return keys, nil +} + +// deleteCollection deletes the keyring's collection if it exists. This is mainly to support testing. +func (k *secretsKeyring) deleteCollection() error { + if err := k.openCollection(); err != nil { + return err + } + return k.collection.Delete() +} + +// unlock the collection if it's locked +func (k *secretsKeyring) ensureCollectionUnlocked() error { + locked, err := k.collection.Locked() + if err != nil { + return err + } + if !locked { + return nil + } + return k.service.Unlock(k.collection) +} diff --git a/vendor/github.com/99designs/keyring/tilde.go b/vendor/github.com/99designs/keyring/tilde.go new file mode 100644 index 00000000..c1847a61 --- /dev/null +++ b/vendor/github.com/99designs/keyring/tilde.go @@ -0,0 +1,22 @@ +package keyring + +import ( + "os" + "path/filepath" + "strings" +) + +var tildePrefix = string([]rune{'~', filepath.Separator}) + +// ExpandTilde will expand tilde (~/ or ~\ depending on OS) for the user home directory. +func ExpandTilde(dir string) (string, error) { + if strings.HasPrefix(dir, tildePrefix) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + dir = strings.Replace(dir, "~", homeDir, 1) + debugf("Expanded file dir to %s", dir) + } + return dir, nil +} diff --git a/vendor/github.com/99designs/keyring/wincred.go b/vendor/github.com/99designs/keyring/wincred.go new file mode 100644 index 00000000..2ed43f2f --- /dev/null +++ b/vendor/github.com/99designs/keyring/wincred.go @@ -0,0 +1,98 @@ +//go:build windows +// +build windows + +package keyring + +import ( + "strings" + "syscall" + + "github.com/danieljoos/wincred" +) + +// ERROR_NOT_FOUND from https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--1000-1299- +const elementNotFoundError = syscall.Errno(1168) + +type windowsKeyring struct { + name string + prefix string +} + +func init() { + supportedBackends[WinCredBackend] = opener(func(cfg Config) (Keyring, error) { + name := cfg.ServiceName + if name == "" { + name = "default" + } + + prefix := cfg.WinCredPrefix + if prefix == "" { + prefix = "keyring" + } + + return &windowsKeyring{ + name: name, + prefix: prefix, + }, nil + }) +} + +func (k *windowsKeyring) Get(key string) (Item, error) { + cred, err := wincred.GetGenericCredential(k.credentialName(key)) + if err != nil { + if err == elementNotFoundError { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + + item := Item{ + Key: key, + Data: cred.CredentialBlob, + } + + return item, nil +} + +// GetMetadata for pass returns an error indicating that it's unsupported +// for this backend. +// TODO: This is a stub. Look into whether pass would support metadata in a usable way for keyring. +func (k *windowsKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNotSupported +} + +func (k *windowsKeyring) Set(item Item) error { + cred := wincred.NewGenericCredential(k.credentialName(item.Key)) + cred.CredentialBlob = item.Data + return cred.Write() +} + +func (k *windowsKeyring) Remove(key string) error { + cred, err := wincred.GetGenericCredential(k.credentialName(key)) + if err != nil { + if err == elementNotFoundError { + return ErrKeyNotFound + } + return err + } + return cred.Delete() +} + +func (k *windowsKeyring) Keys() ([]string, error) { + results := []string{} + + if creds, err := wincred.List(); err == nil { + for _, cred := range creds { + prefix := k.credentialName("") + if strings.HasPrefix(cred.TargetName, prefix) { + results = append(results, strings.TrimPrefix(cred.TargetName, prefix)) + } + } + } + + return results, nil +} + +func (k *windowsKeyring) credentialName(key string) string { + return k.prefix + ":" + k.name + ":" + key +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/LICENSE.txt b/vendor/github.com/NimbleMarkets/ntcharts/LICENSE.txt new file mode 100644 index 00000000..189104ae --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Neomantra Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/NimbleMarkets/ntcharts/canvas/buffer/buffer.go b/vendor/github.com/NimbleMarkets/ntcharts/canvas/buffer/buffer.go new file mode 100644 index 00000000..58322efb --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/canvas/buffer/buffer.go @@ -0,0 +1,342 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +// Package buffer contain buffers used with charts. +package buffer + +import ( + "github.com/NimbleMarkets/ntcharts/canvas" +) + +// Float64ScaleBuffer is a variable size buffer +// that stores float64 data values and a scaled version of the data. +// Scaling is done by multiplying incoming values +// by a constant scale factor. +type Float64ScaleBuffer struct { + buf []float64 // original data + sbuf []float64 // scaled data + offset float64 // offset to subtract + scale float64 // scaling factor +} + +// NewFloat64ScaleBuffer returns *Float64ScaleBuffer initialized to default settings. +func NewFloat64ScaleBuffer(o, sc float64) *Float64ScaleBuffer { + return &Float64ScaleBuffer{ + buf: []float64{}, + sbuf: []float64{}, + offset: o, + scale: sc, + } +} + +// Clear resets buffer contents. +func (b *Float64ScaleBuffer) Clear() { + b.buf = []float64{} + b.sbuf = []float64{} +} + +// Length returns number of data in buffer. +func (b *Float64ScaleBuffer) Length() int { + return len(b.buf) +} + +// Scale returns scaling factor. +func (b *Float64ScaleBuffer) Scale() float64 { + return b.scale +} + +// SetScale updates scaling factor and recomputes all scaled data. +func (b *Float64ScaleBuffer) SetScale(sc float64) { + b.scale = sc + b.sbuf = make([]float64, 0, len(b.buf)) + for _, v := range b.buf { + b.sbuf = append(b.sbuf, (v-b.offset)*b.scale) + } +} + +// Offset returns data value offset. +func (b *Float64ScaleBuffer) Offset() float64 { + return b.offset +} + +// SetOffset updates offset and recomputes all scaled data. +func (b *Float64ScaleBuffer) SetOffset(o float64) { + b.offset = o + b.sbuf = make([]float64, 0, len(b.buf)) + for _, v := range b.buf { + b.sbuf = append(b.sbuf, (v-b.offset)*b.scale) + } +} + +// Push adds Float64Point data to the back of the buffer. +func (b *Float64ScaleBuffer) Push(p float64) { + b.buf = append(b.buf, p) + b.sbuf = append(b.sbuf, (p-b.offset)*b.scale) +} + +// Pop erases the oldest Float64Point from the buffer. +func (b *Float64ScaleBuffer) Pop() { + b.buf = b.buf[1:] + b.sbuf = b.sbuf[1:] +} + +// SetData sets contents of internal buffer +// to given []float64 and scales the data. +func (b *Float64ScaleBuffer) SetData(d []float64) { + b.buf = make([]float64, 0, len(d)) + b.sbuf = make([]float64, 0, len(d)) + for _, p := range d { + b.buf = append(b.buf, p) + b.sbuf = append(b.sbuf, (p-b.offset)*b.scale) + } +} + +// ReadAll returns entire scaled data buffer. +func (b *Float64ScaleBuffer) ReadAll() []float64 { + return b.sbuf +} + +// ReadAllRaw returns entire original data buffer. +func (b *Float64ScaleBuffer) ReadAllRaw() []float64 { + return b.buf +} + +// At returns Float64Point of scaled data at index i of buffer. +func (b *Float64ScaleBuffer) At(i int) float64 { + return b.sbuf[i] +} + +// AtRaw returns Float64Point of original data at index i of buffer. +func (b *Float64ScaleBuffer) AtRaw(i int) float64 { + return b.buf[i] +} + +// Float64ScaleRingBuffer is a fix-sized ring buffer +// that stores float64 data values and a scaled version of the data. +// Scaling is done by first subtracting by the offset and then multiplying +// incoming values by a constant scale factor. +// Unlike traditional ring buffers, pushing data to the buffer +// while at full capacity will erase the oldest datum +// from the buffer to create room for writing. +type Float64ScaleRingBuffer struct { + buf []float64 // original data + sbuf []float64 // scaled data + offset float64 // offset to subtract + scale float64 // scaling factor + + length int // number of elements + sz int // capacitiy + + wIdx int // write index + rIdx int // read index +} + +// NewFloat64ScaleRingBuffer returns *Float64ScaleRingBuffer initialized to default settings. +func NewFloat64ScaleRingBuffer(s int, o, sc float64) *Float64ScaleRingBuffer { + return &Float64ScaleRingBuffer{ + buf: make([]float64, s, s), + sbuf: make([]float64, s, s), + offset: o, + scale: sc, + sz: s, + wIdx: 0, + rIdx: 0} +} + +// Clear resets buffer contents. +func (b *Float64ScaleRingBuffer) Clear() { + b.length = 0 + b.wIdx = 0 + b.rIdx = 0 +} + +// Length returns number of data in buffer. +func (b *Float64ScaleRingBuffer) Length() int { + return b.length +} + +// Size returns buffer capacity. +func (b *Float64ScaleRingBuffer) Size() int { + return b.sz +} + +// Scale returns scaling factor. +func (b *Float64ScaleRingBuffer) Scale() float64 { + return b.scale +} + +// SetScale updates scaling factor and recomputes all scaled data. +func (b *Float64ScaleRingBuffer) SetScale(sc float64) { + b.scale = sc + for i, v := range b.buf { + b.sbuf[i] = (v - b.offset) * b.scale + } +} + +// Offset returns data value offset. +func (b *Float64ScaleRingBuffer) Offset() float64 { + return b.offset +} + +// SetOffset updates offset and recomputes all scaled data. +func (b *Float64ScaleRingBuffer) SetOffset(o float64) { + b.offset = o + for i, v := range b.buf { + b.sbuf[i] = (v - b.offset) * b.scale + } +} + +// Push adds float64 data to the back of the buffer. +func (b *Float64ScaleRingBuffer) Push(f float64) { + b.buf[b.wIdx] = f + b.sbuf[b.wIdx] = (f - b.offset) * b.scale + b.wIdx++ + if b.wIdx >= b.sz { + b.wIdx = 0 + } + if b.length == b.sz { // on full buffer, just increment read index + b.rIdx++ + if b.rIdx >= b.sz { + b.rIdx = 0 + } + } else { + b.length++ + } +} + +// Pop erases the oldest float64 from the buffer. +func (b *Float64ScaleRingBuffer) Pop() { + b.rIdx++ + if b.rIdx >= b.sz { + b.rIdx = 0 + } + b.length-- +} + +// ReadAll returns entire scaled data buffer. +func (b *Float64ScaleRingBuffer) ReadAll() []float64 { + return b.getBuffer(b.sbuf) +} + +// ReadAllRaw returns entire original data buffer. +func (b *Float64ScaleRingBuffer) ReadAllRaw() []float64 { + return b.getBuffer(b.buf) +} + +func (b *Float64ScaleRingBuffer) getBuffer(buf []float64) (f []float64) { + sz := b.sz + ln := b.length + idx := b.rIdx + + f = make([]float64, 0, sz) + for i := 0; i < ln; i++ { + f = append(f, buf[idx]) + idx++ + if idx >= sz { + idx = 0 + } + } + return +} + +// Float64PointScaleBuffer is a variable size buffer +// that stores Float64Points and a scaled version of the Float64Points. +// Scaling is done by multiplying incoming values (X,Y) coordinates +// by a constant scale factor. +type Float64PointScaleBuffer struct { + buf []canvas.Float64Point // original data + sbuf []canvas.Float64Point // scaled data + offsetP canvas.Float64Point // offset to subtract X,Y values from + scale canvas.Float64Point // scaling factor for X,Y values +} + +// NewFloat64PointScaleBuffer returns *Float64PointScaleBuffer initialized to default settings. +func NewFloat64PointScaleBuffer(o, sc canvas.Float64Point) *Float64PointScaleBuffer { + return &Float64PointScaleBuffer{ + buf: []canvas.Float64Point{}, + sbuf: []canvas.Float64Point{}, + offsetP: o, + scale: sc, + } +} + +// Clear resets buffer contents. +func (b *Float64PointScaleBuffer) Clear() { + b.buf = []canvas.Float64Point{} + b.sbuf = []canvas.Float64Point{} +} + +// Length returns number of data in buffer. +func (b *Float64PointScaleBuffer) Length() int { + return len(b.buf) +} + +// Scale returns Float64Point used to multiple data points by. +func (b *Float64PointScaleBuffer) Scale() canvas.Float64Point { + return b.scale +} + +// SetScale updates scaling factor and recomputes all scaled data. +func (b *Float64PointScaleBuffer) SetScale(sc canvas.Float64Point) { + b.scale = sc + b.sbuf = make([]canvas.Float64Point, 0, len(b.buf)) + for _, v := range b.buf { + b.sbuf = append(b.sbuf, v.Sub(b.offsetP).Mul(b.scale)) + } +} + +// Offset returns Float64Point used to subtract data points from. +func (b *Float64PointScaleBuffer) Offset() canvas.Float64Point { + return b.scale +} + +// SetOffset updates offsets and recomputes all scaled data. +func (b *Float64PointScaleBuffer) SetOffset(o canvas.Float64Point) { + b.offsetP = o + b.sbuf = make([]canvas.Float64Point, 0, len(b.buf)) + for _, v := range b.buf { + b.sbuf = append(b.sbuf, v.Sub(b.offsetP).Mul(b.scale)) + } +} + +// Push adds Float64Point data to the back of the buffer. +func (b *Float64PointScaleBuffer) Push(p canvas.Float64Point) { + b.buf = append(b.buf, p) + b.sbuf = append(b.sbuf, p.Sub(b.offsetP).Mul(b.scale)) +} + +// Pop erases the oldest Float64Point from the buffer. +func (b *Float64PointScaleBuffer) Pop() { + b.buf = b.buf[1:] + b.sbuf = b.sbuf[1:] +} + +// SetData sets contents of internal buffer +// to given []float64 and scales the data. +func (b *Float64PointScaleBuffer) SetData(d []canvas.Float64Point) { + b.buf = make([]canvas.Float64Point, 0, len(d)) + b.sbuf = make([]canvas.Float64Point, 0, len(d)) + for _, p := range d { + b.buf = append(b.buf, p) + b.sbuf = append(b.sbuf, p.Sub(b.offsetP).Mul(b.scale)) + } +} + +// ReadAll returns entire scaled data buffer. +func (b *Float64PointScaleBuffer) ReadAll() []canvas.Float64Point { + return b.sbuf +} + +// ReadAllRaw returns entire original data buffer. +func (b *Float64PointScaleBuffer) ReadAllRaw() []canvas.Float64Point { + return b.buf +} + +// At returns Float64Point of scaled data at index i of buffer. +func (b *Float64PointScaleBuffer) At(i int) canvas.Float64Point { + return b.sbuf[i] +} + +// AtRaw returns Float64Point of original data at index i of buffer. +func (b *Float64PointScaleBuffer) AtRaw(i int) canvas.Float64Point { + return b.buf[i] +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/canvas/canvas.go b/vendor/github.com/NimbleMarkets/ntcharts/canvas/canvas.go new file mode 100644 index 00000000..e096b40e --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/canvas/canvas.go @@ -0,0 +1,514 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +// Package canvas implements an abstract 2D area used to plot +// arbitary runes that can be displayed using the bubbletea framework. +package canvas + +// File contains a Model using the bubbletea framework +// representing the state of the canvas, +// types and functions of the coordinates system used by the canvas +// and types and functions of the contents of the canvas. + +import ( + "image" + "math" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + zone "github.com/lrstanley/bubblezone" +) + +// Point is alias for image.Point +type Point = image.Point + +// Float64Point represents a point in a coordinate system +// with floating point precision. +type Float64Point struct { + X float64 + Y float64 +} + +// NewFloat64PointFromPoint returns a new Float64Point from a given Point. +func NewFloat64PointFromPoint(p Point) Float64Point { + return Float64Point{X: float64(p.X), Y: float64(p.Y)} +} + +// Mul returns a Float64Point with both X and Y values +// multiplied by the X and Y values of the given Float64Point. +func (p Float64Point) Mul(f Float64Point) Float64Point { + return Float64Point{p.X * f.X, p.Y * f.Y} +} + +// Add returns a Float64Point with both X and Y values +// added to the X and Y values of the given Float64Point. +func (p Float64Point) Add(f Float64Point) Float64Point { + return Float64Point{p.X + f.X, p.Y + f.Y} +} + +// Sub returns a Float64Point with both X and Y values +// subtracted by the X and Y values of the given Float64Point. +func (p Float64Point) Sub(f Float64Point) Float64Point { + return Float64Point{p.X - f.X, p.Y - f.Y} +} + +// NewPointFromFloat64Point returns a new Point from a given Float64Point. +func NewPointFromFloat64Point(f Float64Point) Point { + return Point{int(math.Round(f.X)), int(math.Round(f.Y))} +} + +// CanvasYCoordinates returns a sequence of Y coordinates in the +// canvas coordinates system (X,Y is top left) from a given sequence of Y coordinates +// in the Cartesian coordinates system (X,Y is bottom left) +// by passing the graph X axis in the canvas coordinates system. +func CanvasYCoordinates(xAxis int, seq []int) (r []int) { + r = make([]int, 0, len(seq)) + for _, v := range seq { + r = append(r, CanvasYCoordinate(xAxis, v)) + } + return +} + +// CanvasYCoordinate returns a Y coordinates in the +// canvas coordinates system (X,Y is top left) from a Y coordinate +// in the Cartesian coordinates system (X,Y is bottom left) +// by passing the graph X axis in the canvas coordinates system. +func CanvasYCoordinate(xAxis int, y int) (r int) { + return xAxis - y +} + +// CanvasPoints returns a sequence of Points in the +// canvas coordinates system (X,Y is top left) from a given sequence of Points +// in the Cartesian coordinates system (X,Y is bottom left) +// by passing the graph origin in the canvas coordinates system. +func CanvasPoints(origin Point, seq []Point) (r []Point) { + r = make([]Point, 0, len(seq)) + for _, v := range seq { + r = append(r, CanvasPoint(origin, v)) + } + return +} + +// CanvasPoint returns a Point in the +// canvas coordinates system (X,Y is top left) from a given Point +// in the Cartesian coordinates system (X,Y is bottom left) +// by passing the graph origin in the canvas coordinates system. +func CanvasPoint(origin Point, p Point) (r Point) { + return Point{origin.X + p.X, origin.Y - p.Y} +} + +// CanvasPointFromFloat64Point returns a Point +// in the canvas coordinates systems (X,Y is top left) +// from a canvas Float64Point in the +// Cartesian coordinates system (X,Y is bottom left) +// by passing the graph origin in the canvas coordinates system. +func CanvasPointFromFloat64Point(origin Point, f Float64Point) Point { + // round coordinates to nearest integer and + // convert Cartesian coordinates to canvas coordinates + return CanvasPoint(origin, NewPointFromFloat64Point(f)) +} + +// CanvasFloat64Point returns a Float64Point in the +// canvas coordinates system (X,Y is top left) from a given Float64Point +// in the Cartesian coordinates system (X,Y is bottom left) +// by passing the graph origin in the canvas coordinates system. +func CanvasFloat64Point(origin Point, p Float64Point) (r Float64Point) { + return Float64Point{float64(origin.X) + p.X, float64(origin.Y) - p.Y} +} + +var defaultStyle = lipgloss.NewStyle() + +// Cell contains a rune and lipgloss Style for rendering +type Cell struct { + Rune rune + Style lipgloss.Style +} + +// NewCell returns Cell with given rune and default lipgloss Style. +func NewCell(r rune) Cell { + return Cell{Rune: r, Style: defaultStyle} +} + +// NewCellWithStyle returns Cell with given rune and lipgloss Style. +func NewCellWithStyle(r rune, s lipgloss.Style) Cell { + return Cell{Rune: r, Style: s} +} + +// CellLine is a slice of Cells +type CellLine []Cell + +// Model contains state of a canvas +type Model struct { + Style lipgloss.Style // default style applied to all cells + KeyMap KeyMap // KeyMap used for keyboard msgs during Update() + UpdateHandler UpdateHandler // callback invoked during Update() + + // overall canvas size + area image.Rectangle // 0,0 is top left of canvas + content []CellLine + focus bool + + // simulates a viewport width and height + // to display contents of the canvas + ViewWidth int + ViewHeight int + + // internal coordinates tracking viewport cursor + // the contents will be displayed from top to bottom + // and left to right of the cursor + // 0,0 is top left of canvas + cursor Point + + // bubblezone Manager used to handler mouse events during Update() + zoneManager *zone.Manager + zoneID string +} + +// New returns a canvas Model initialized with given width, height +// and various options. +func New(w, h int, opts ...Option) Model { + m := Model{ + Style: defaultStyle, + KeyMap: DefaultKeyMap(), + UpdateHandler: DefaultUpdateHandler(), + area: image.Rect(0, 0, w, h), + content: make([]CellLine, h), + ViewWidth: w, + ViewHeight: h, + } + for i := range m.content { + m.content[i] = make(CellLine, w) + } + for _, opt := range opts { + opt(&m) + } + return m +} + +// Width returns canvas width. +func (m *Model) Width() int { + return m.area.Dx() +} + +// Height returns canvas height. +func (m *Model) Height() int { + return m.area.Dy() +} + +// Cursor returns Point containg (X,Y) coordinates pointing to top left of viewport. +func (m *Model) Cursor() Point { + return m.cursor +} + +// SetCursor sets (X,Y) coordinates of cursor pointing to top left of viewport. +// Coordinates will be bounded by canvas if x, y coordinates are out of bound. +func (m *Model) SetCursor(p Point) { + m.cursor.X = p.X + m.cursor.Y = p.Y + if m.cursor.X < 0 { + m.cursor.X = 0 + } else if m.cursor.X >= m.area.Dx() { + m.cursor.X = m.area.Dx() - 1 + } + if m.cursor.Y < 0 { + m.cursor.Y = 0 + } else if m.cursor.Y >= m.area.Dy() { + m.cursor.Y = m.area.Dy() - 1 + } +} + +// Resize will resize canvas to new height and width, and resets cursor. +// Will truncate existing content if canvas size shrinks. +// Does not change viewport for displaying contents. +func (m *Model) Resize(w, h int) { + // create new lines and copy over previous contents + newLines := make([]CellLine, h) + for i := range newLines { + newLines[i] = make(CellLine, w) + // copy over previous line + if i < m.area.Dy() { + for j, r := range m.content[i] { + if j >= w { + break + } + newLines[i][j] = r + } + } + } + m.area = image.Rect(0, 0, w, h) + m.cursor.X = 0 + m.cursor.Y = 0 + m.content = newLines +} + +// Clear will reset canvas contents. +func (m *Model) Clear() { + for i := range m.content { + m.content[i] = make(CellLine, m.area.Dx()) + } +} + +// SetLines copies []string into canvas as contents. +// Each string element represents a line in the canvas starting from top to bottom. +// Truncates contents if contents are greater than canvas height and width. +func (m *Model) SetLines(lines []string) bool { + return m.SetLinesWithStyle(lines, m.Style) +} + +// SetLinesWithStyle copies []string into canvas as contents with style applied to all Cells. +// Each string element represents a line in the canvas starting from top to bottom. +// Truncates contents if contents are greater than canvas height and width. +func (m *Model) SetLinesWithStyle(lines []string, s lipgloss.Style) bool { + for y, l := range lines { + if y >= m.area.Dy() { + break + } + if !m.SetStringWithStyle(Point{0, y}, l, s) { + return false // should not happen + } + } + return true +} + +// SetString copies string as rune values into canvas CellLine starting at coordinates (X, Y). +// Truncates values execeeding the canvas width. +func (m *Model) SetString(p Point, l string) bool { + return m.SetStringWithStyle(p, l, m.Style) +} + +// SetStringWithStyle copies string as rune values into canvas CellLine starting at coordinates (X, Y). +// Style will be applied to all Cells. +// Truncates values execeeding the canvas width. +func (m *Model) SetStringWithStyle(p Point, l string, s lipgloss.Style) bool { + return m.SetRunesWithStyle(p, []rune(l), s) +} + +// SetRunes copies rune values into canvas CellLine starting at coordinates (X, Y). +// Style will be applied to all Cells. +// Truncates values execeeding the canvas width. +func (m *Model) SetRunes(p Point, l []rune) bool { + return m.SetRunesWithStyle(p, l, m.Style) +} + +// SetRunesWithStyle copies rune values into canvas CellLine starting at coordinates (X, Y). +// Style will be applied to all Cells. +// Truncates values execeeding the canvas width. +func (m *Model) SetRunesWithStyle(p Point, l []rune, s lipgloss.Style) bool { + if !m.insideYBounds(p.Y) { + return false + } + xIdx := p.X + for _, r := range l { + if m.insideXBounds(xIdx) { + m.content[p.Y][xIdx] = NewCellWithStyle(r, s) + } + xIdx += 1 + } + return true +} + +// SetCell sets a Cell using (X,Y) coordinates of canvas. +func (m *Model) SetCell(p Point, c Cell) bool { + if !p.In(m.area) { + return false + } + m.content[p.Y][p.X] = c + return true +} + +// SetCellStyle sets Cell.Style using (X,Y) coordinates of canvas. +func (m *Model) SetCellStyle(p Point, s lipgloss.Style) bool { + if !p.In(m.area) { + return false + } + m.content[p.Y][p.X].Style = s + return true +} + +// SetRune sets Cell.Rune using (X,Y) coordinates of canvas. +func (m *Model) SetRune(p Point, r rune) bool { + if !p.In(m.area) { + return false + } + m.content[p.Y][p.X] = NewCell(r) + return true +} + +// Cell returns Cell located at (X,Y) coordinates of canvas. +// Returns default Cell if coorindates are out of bounds. +func (m *Model) Cell(p Point) (c Cell) { + if !p.In(m.area) { + return + } + c = m.content[p.Y][p.X] + return +} + +// Fill sets all content in canvas to Cell. +func (m *Model) Fill(c Cell) { + for i := range m.content { + for j := range m.content[i] { + m.content[i][j] = c + } + } +} + +// FillLine sets all Cells in a CellLine y away +// from origin to given Cell. +func (m *Model) FillLine(y int, c Cell) { + if !m.insideYBounds(y) { + return + } + for j := range m.content[y] { + m.content[y][j] = c + } +} + +// SetStyle applies a lipgloss.Style to all Cells to change +// visual elements of each rune in the canvas. +func (m *Model) SetStyle(s lipgloss.Style) { + for i := range m.content { + for j := range m.content[i] { + m.content[i][j].Style = s + } + } +} + +// SetZoneManager enables mouse functionality +// by setting a bubblezone.Manager to the canvas. +// The bubblezone.Manager can check bubbletea mouse event Msgs +// passed to the UpdateHandler handler during an Update(). +// The root bubbletea model must wrap the View() string with +// bubblezone.Manager.Scan() to enable mouse functionality. +// To disable mouse functionality after enabling, call SetZoneManager on nil. +func (m *Model) SetZoneManager(zm *zone.Manager) { + m.zoneManager = zm + if (zm != nil) && (m.zoneID == "") { + m.zoneID = zm.NewPrefix() + } +} + +// ZoneManager will return canvas zone Manager. +func (m *Model) ZoneManager() *zone.Manager { + return m.zoneManager +} + +// ZoneID will return canvas zone ID used by zone Manager. +func (m *Model) ZoneID() string { + return m.zoneID +} + +// ShiftUp moves all Cells up once. +// Last CellLine will be set to a new CellLine. +func (m *Model) ShiftUp() { + c := m.content + copy(c, c[1:]) + c[len(c)-1] = make(CellLine, m.area.Dx()) +} + +// ShiftDown moves all Cells down once. +// First CellLine will be set to a new CellLine. +func (m *Model) ShiftDown() { + c := m.content + copy(c[1:], c) + c[0] = make(CellLine, m.area.Dx()) +} + +// ShiftLeft moves all Cells left once. +// Last cell in each CellLine will be a new default Cell. +func (m *Model) ShiftLeft() { + for i := range m.content { + cl := m.content[i] + copy(cl, cl[1:]) + cl[len(cl)-1] = Cell{} + } +} + +// ShiftRight moves all Cells right once. +// First cell in each CellLine will be a new default Cell. +func (m *Model) ShiftRight() { + for i := range m.content { + cl := m.content[i] + copy(cl[1:], cl) + cl[0] = Cell{} + } +} + +// Focused returns whether canvas is being focused. +func (m *Model) Focused() bool { + return m.focus +} + +// Focus enables Update events processing. +func (m *Model) Focus() { + m.focus = true +} + +// Blur disables Update events processing. +func (m *Model) Blur() { + m.focus = false +} + +// Init initializes the canvas. +func (m Model) Init() tea.Cmd { + return nil +} + +// Update processes bubbletea Msg to by invoking +// UpdateHandler callback if canvas is focused. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + if !m.focus { + return m, nil + } + m.UpdateHandler(&m, msg) + return m, nil +} + +// View returns a string used by the bubbletea framework to display the canvas. +func (m Model) View() (r string) { + var sb strings.Builder + sb.Grow(m.area.Dx() * m.area.Dy()) + + endY := m.cursor.Y + m.ViewHeight - 1 + endX := m.cursor.X + m.ViewWidth - 1 + for i := m.cursor.Y; i <= endY; i++ { + if i >= m.area.Dy() { + break + } + for j := m.cursor.X; j <= endX; j++ { + if j >= m.area.Dx() { + break + } + cell := m.content[i][j] + if cell.Rune == 0 { + sb.WriteString(cell.Style.Render(" ")) + } else { + sb.WriteString(cell.Style.Render(string(cell.Rune))) + } + } + if i != endY { + sb.WriteRune('\n') + } + } + r = sb.String() + if m.zoneManager != nil { + r = m.zoneManager.Mark(m.zoneID, r) + } + return +} + +// insideXBounds returns whether X coordinate is within canvas bounds. +func (m *Model) insideXBounds(x int) bool { + if (x < 0) || (x >= m.area.Dx()) { + return false + } + return true +} + +// insideYBounds returns whether Y coordinate is within canvas bounds. +func (m *Model) insideYBounds(y int) bool { + if (y < 0) || (y >= m.area.Dy()) { + return false + } + return true +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/canvas/graph/graph.go b/vendor/github.com/NimbleMarkets/ntcharts/canvas/graph/graph.go new file mode 100644 index 00000000..857efdd8 --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/canvas/graph/graph.go @@ -0,0 +1,737 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +// Package graph contains data structures and functions to help draw runes on to a canvas. +package graph + +// https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm +// https://en.wikipedia.org/wiki/Midpoint_circle_algorithm + +import ( + "math" + "sort" + + "github.com/NimbleMarkets/ntcharts/canvas" + "github.com/NimbleMarkets/ntcharts/canvas/runes" + + "github.com/charmbracelet/lipgloss" +) + +// BrailleGrid wraps a runes.PatternDotsGrid +// to implements a 2D grid with (X, Y) floating point coordinates +// used to display Braille Pattern runes. +// Since Braille Pattern runes are 4 high and 2 wide, +// the BrailleGrid will internally scale the width and height +// sizes to match those patterns. +// BrailleGrid uses canvas coordinates system with (0,0) being top left. +type BrailleGrid struct { + cWidth int // canvas width + cHeight int // canvas height + + minX float64 + maxX float64 + minY float64 + maxY float64 + + gWidth int // grid width + gHeight int // grid height + grid *runes.PatternDotsGrid +} + +// NewBrailleGrid returns new initialized *BrailleGrid +// with given canvas width, canvas height and +// minimums and maximums X and Y values of the data points. +func NewBrailleGrid(w, h int, minX, maxX, minY, maxY float64) *BrailleGrid { + gridW := w * 2 + gridH := h * 4 + g := BrailleGrid{ + cWidth: w, + cHeight: h, + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY, + gWidth: gridW, + gHeight: gridH, + grid: runes.NewPatternDotsGrid(gridW, gridH), + } + g.Clear() + return &g +} + +// Clear will reset the internal grid +func (g *BrailleGrid) Clear() { + g.grid.Reset() +} + +// GridPoint returns a canvas Point representing a point in the braille grid +// in the canvas coordinates system from a Float64Point data point +// in the Cartesian coordinates system. +func (g *BrailleGrid) GridPoint(f canvas.Float64Point) canvas.Point { + var sf canvas.Float64Point + dx := g.maxX - g.minX + dy := g.maxY - g.minY + if dx > 0 { + xs := float64(g.gWidth-1) / dx + sf.X = (f.X - g.minX) * xs + } + if dy > 0 { + ys := float64(g.gHeight-1) / dy + sf.Y = (f.Y - g.minY) * ys + } + return canvas.CanvasPointFromFloat64Point(canvas.Point{X: 0, Y: g.gHeight - 1}, sf) +} + +// Set will set point on grid from given canvas Point. +func (g *BrailleGrid) Set(p canvas.Point) { + g.grid.Set(p.X, p.Y) +} + +// BraillePatterns returns [][]rune containing +// braille pattern runes to draw on to the canvas. +func (g *BrailleGrid) BraillePatterns() [][]rune { + return g.grid.BraillePatterns() +} + +// DrawVerticalLineUp draws a vertical line going up starting from (X,Y) coordinates. +// Applies given style to all runes. +// Coordinates (0,0) is top left of canvas. +func DrawVerticalLineUp(m *canvas.Model, p canvas.Point, s lipgloss.Style) { + x := p.X + r := canvas.NewCellWithStyle(runes.LineVertical, s) + for i := p.Y; i >= 0; i-- { + m.SetCell(canvas.Point{x, i}, r) + } +} + +// DrawVerticalLineDown draws a vertical line going down starting from (X,Y) coordinates. +// Applies given style to all runes. +// Coordinates (0,0) is top left of canvas. +func DrawVerticalLineDown(m *canvas.Model, p canvas.Point, s lipgloss.Style) { + x := p.X + r := canvas.NewCellWithStyle(runes.LineVertical, s) + for i := p.Y; i < m.Height(); i++ { + m.SetCell(canvas.Point{x, i}, r) + } +} + +// DrawHorizonalLineLeft draws a horizontal line going to the left starting from (X,Y) coordinates. +// Applies given style to all runes. +// Coordinates (0,0) is top left of canvas. +func DrawHorizonalLineLeft(m *canvas.Model, p canvas.Point, s lipgloss.Style) { + y := p.Y + r := canvas.NewCellWithStyle(runes.LineHorizontal, s) + for i := p.X; i >= 0; i-- { + m.SetCell(canvas.Point{i, y}, r) + } +} + +// DrawHorizonalLineRight draws a horizontal line going to the right starting from (X,Y) coordinates. +// Applies given style to all runes. +// Coordinates (0,0) is top left of canvas. +func DrawHorizonalLineRight(m *canvas.Model, p canvas.Point, s lipgloss.Style) { + y := p.Y + r := canvas.NewCellWithStyle(runes.LineHorizontal, s) + for i := p.X; i < m.Width(); i++ { + m.SetCell(canvas.Point{i, y}, r) + } +} + +// DrawXYAxis draws X and Y axes with origin at (X,Y cordinates) with given style. +// Y axis extends up, and X axis extends right. +// Coordinates (0,0) is top left of canvas. +func DrawXYAxis(m *canvas.Model, p canvas.Point, s lipgloss.Style) { + m.SetCell(p, canvas.NewCellWithStyle(runes.LineUpRight, s)) + DrawVerticalLineUp(m, canvas.Point{p.X, p.Y - 1}, s) + DrawHorizonalLineRight(m, canvas.Point{p.X + 1, p.Y}, s) +} + +// DrawXYAxisDown draws X and Y axes with origin at (X,Y cordinates) with given style. +// Y axis extends up and down, and X axis extends right. +// Coordinates (0,0) is top left of canvas. +func DrawXYAxisDown(m *canvas.Model, p canvas.Point, s lipgloss.Style) { + m.SetCell(p, canvas.NewCellWithStyle(runes.LineVerticalRight, s)) + DrawVerticalLineUp(m, canvas.Point{p.X, p.Y - 1}, s) + DrawVerticalLineDown(m, canvas.Point{p.X, p.Y + 1}, s) + DrawHorizonalLineRight(m, canvas.Point{p.X + 1, p.Y}, s) +} + +// DrawXYAxisLeft draws X and Y axes with origin at (X,Y cordinates) with given style. +// Y axis extends up, and X axis extends left and right. +// Coordinates (0,0) is top left of canvas. +func DrawXYAxisLeft(m *canvas.Model, p canvas.Point, s lipgloss.Style) { + m.SetCell(p, canvas.NewCellWithStyle(runes.LineHorizontalUp, s)) + DrawVerticalLineUp(m, canvas.Point{p.X, p.Y - 1}, s) + DrawHorizonalLineRight(m, canvas.Point{p.X + 1, p.Y}, s) + DrawHorizonalLineLeft(m, canvas.Point{p.X - 1, p.Y}, s) +} + +// DrawXYAxisAll draws X and Y axes with origin at (X,Y cordinates) with given style. +// Y axis extends up and down, and X axis extends left and right. +// Coordinates (0,0) is top left of canvas. +func DrawXYAxisAll(m *canvas.Model, p canvas.Point, s lipgloss.Style) { + m.SetCell(p, canvas.NewCellWithStyle(runes.LineHorizontalVertical, s)) + DrawVerticalLineUp(m, canvas.Point{p.X, p.Y - 1}, s) + DrawVerticalLineDown(m, canvas.Point{p.X, p.Y + 1}, s) + DrawHorizonalLineRight(m, canvas.Point{p.X + 1, p.Y}, s) + DrawHorizonalLineLeft(m, canvas.Point{p.X - 1, p.Y}, s) +} + +// DrawColumnRune draws a braille rune on to the canvas at given (X,Y) coordinates with given style. +// The function checks for existing braille runes already on the canvas and +// will draw a new braille pattern with the dot patterns of both the existing and given runes. +// Does nothing if given rune is Null or is not a braille rune. +func DrawBrailleRune(m *canvas.Model, p canvas.Point, r rune, s lipgloss.Style) { + if (r == runes.Null) || !runes.IsBraillePattern(r) { + return + } + cr := m.Cell(p).Rune + if cr == 0 { // set rune if nothing exists on canvas + m.SetCell(p, canvas.NewCellWithStyle(r, s)) + return + } + m.SetCell(p, canvas.NewCellWithStyle(runes.CombineBraillePatterns(m.Cell(p).Rune, r), s)) +} + +// DrawBraillePatterns draws braille runes from a [][]rune representing a 2D grid of +// Braille Pattern runes. The runes will be drawn onto the canvas from starting from top +// left of the grid to the bottom right of the grid starting at the given canvas Point. +// Given style will be applied to all runes drawn. +// This function can be used with the output [][]rune from PatternDotsGrid.BraillePatterns(). +func DrawBraillePatterns(m *canvas.Model, p canvas.Point, b [][]rune, s lipgloss.Style) { + for y, row := range b { + for x, r := range row { + if r != runes.BrailleBlockOffset { + DrawBrailleRune(m, p.Add(canvas.Point{X: x, Y: y}), r, s) + } + } + } +} + +// DrawLineSequence draws line runes on to the canvas starting +// from a given X coordinate and a sequence of Y coordinates. +// `startYAxis` should be true if `startX` is the Y axis. +// Sequential Y coordinates will increment X coordinates. +// Applies style to all line runes. +// Handles overlapping lines. +// Handles X and Y axes drawn using DrawXYAxis functions. +// Coordinates (0,0) is top left of canvas. +func DrawLineSequence(m *canvas.Model, startYAxis bool, startX int, seqY []int, ls runes.LineStyle, s lipgloss.Style) { + var prevY int + for i, y := range seqY { + if i == 0 { // draw first point + p := canvas.Point{startX, y} + r := runes.LineHorizontal + if startYAxis { + switch m.Cell(p).Rune { + case runes.LineUpRight: // first point is origin + m.SetCell(p, canvas.NewCellWithStyle(runes.LineUpRight, s)) + case runes.LineVertical: // first point on Y axis + m.SetCell(p, canvas.NewCellWithStyle(runes.LineVerticalRight, s)) + case runes.LineVerticalRight: // first point on Y axis overlapping another line + m.SetCell(p, canvas.NewCellWithStyle(runes.LineVerticalRight, s)) + default: + DrawLineRune(m, p, r, ls, s) + } + } else { + DrawLineRune(m, p, r, ls, s) + } + } else { + DrawLineSequenceLeftToRight(m, canvas.Point{i + startX - 1, prevY}, canvas.Point{i + startX, y}, ls, s) + } + prevY = y + } +} + +// DrawLineSequenceLeftToRight draws line runes from point A to point B where B.X = A.X+1. +// Assumes point A has already been drawn and does not draw point A. +// Applies style to all line runes. +// Handles overlapping lines. +// Handles X and Y axes drawn using DrawXYAxis functions. +// Coordinates (0,0) is top left of canvas. +func DrawLineSequenceLeftToRight(m *canvas.Model, a canvas.Point, b canvas.Point, ls runes.LineStyle, s lipgloss.Style) { + if a.X >= b.X { + return + } + + prevY := a.Y + y := b.Y + x := b.X + r := runes.LineHorizontal // default: point A has same Y coordinate as point B + + // if not the same Y coordinates, + // draw vertical lines from point A to point B + if prevY > y { // drawing line up + r = runes.ArcDownRight + DrawLineRune(m, canvas.Point{x, prevY}, runes.ArcUpLeft, ls, s) + for j := prevY - 1; j > y; j-- { // draw vertical lines + DrawLineRune(m, canvas.Point{x, j}, runes.LineVertical, ls, s) + } + } else if prevY < y { // drawing line down + r = runes.ArcUpRight + DrawLineRune(m, canvas.Point{x, prevY}, runes.ArcDownLeft, ls, s) + for j := prevY + 1; j < y; j++ { // draw vertical lines + DrawLineRune(m, canvas.Point{x, j}, runes.LineVertical, ls, s) + } + } + + DrawLineRune(m, b, r, ls, s) +} + +// DrawLinePoints draws line runes on to the canvas from a []canvas.Point. +// Each canvas Point is expected to be either adjacent or diagonal from each other. +// At least two Points are required to draw any runes on to the canvas. +// This function can be used with the []canvas.Point output from GetLinePoints(). +func DrawLinePoints(m *canvas.Model, points []canvas.Point, ls runes.LineStyle, s lipgloss.Style) { + if len(points) < 2 { + return + } + extraPoints := []canvas.Point{} + extraRunes := []rune{} // additional corner runes to draw + dir := make([]runes.LineSegments, len(points), len(points)) + for i := 1; i < len(points); i++ { + p := points[i] + prev := points[i-1] + + if p.X > prev.X { + if p.Y > prev.Y { // p down right of prev + dir[i-1].Right = true + dir[i].Up = true + extraPoints = append(extraPoints, canvas.Point{X: p.X, Y: p.Y - 1}) + extraRunes = append(extraRunes, runes.ArcDownLeft) + } else if p.Y < prev.Y { // p up right of prev + dir[i-1].Right = true + dir[i].Down = true + extraPoints = append(extraPoints, canvas.Point{X: p.X, Y: p.Y + 1}) + extraRunes = append(extraRunes, runes.ArcUpLeft) + } else { // p right of prev + dir[i-1].Right = true + dir[i].Left = true + } + } else if p.X < prev.X { + if p.Y > prev.Y { // p down left of prev + dir[i-1].Left = true + dir[i].Up = true + extraPoints = append(extraPoints, canvas.Point{X: p.X, Y: p.Y - 1}) + extraRunes = append(extraRunes, runes.ArcDownRight) + } else if p.Y < prev.Y { // p up left of prev + dir[i-1].Left = true + dir[i].Down = true + extraPoints = append(extraPoints, canvas.Point{X: p.X, Y: p.Y + 1}) + extraRunes = append(extraRunes, runes.ArcUpRight) + } else { // p left of prev + dir[i-1].Left = true + dir[i].Right = true + } + } else { + if p.Y > prev.Y { // p below prev + dir[i-1].Down = true + dir[i].Up = true + } else if p.Y < prev.Y { // p above prev + dir[i-1].Up = true + dir[i].Down = true + } else { + // same point - do nothing + } + } + } + for i, l := range dir { + DrawLineRune(m, points[i], runes.ArcLineFromLineSegments(l), ls, s) + } + for i, r := range extraRunes { + DrawLineRune(m, extraPoints[i], r, ls, s) + } +} + +// DrawLineRune draws a line rune on to the canvas at given (X,Y) coordinates with given style. +// The given rune is used to check line directions, and the final output line rune +// depends on the given runes.LineStyle. +// The function checks for existing X,Y axis or line runes already on the canvas and draws runes +// such that the lines appear overlapping. +// Does nothing if given rune is empty or is not a line rune. +func DrawLineRune(m *canvas.Model, p canvas.Point, r rune, ls runes.LineStyle, s lipgloss.Style) { + if (r == runes.Null) || !runes.IsLine(r) { + return + } + m.SetCell(p, canvas.NewCellWithStyle(runes.CombineLines(m.Cell(p).Rune, r, ls), s)) +} + +// DrawColumns draws columns going upwards on to canvas +// starting from a given (X,Y) coordinate and a sequence of column lengths. +// Columns will be drawn from left to right and +// sequential column lengths will increment X coordinates for drawing. +// Handles overlapping columns of diferent rune heights. +// If there exists an existing column at given Point with same height as new column, +// then the existing column will be replaced. +// Applies style to all block runes. +// Coordinates (0,0) is top left of canvas. +func DrawColumns(m *canvas.Model, p canvas.Point, seqLen []float64, s lipgloss.Style) { + y := p.Y + x := p.X + for i, f := range seqLen { + DrawColumnBottomToTop(m, canvas.Point{x + i, y}, f, s) + } +} + +// DrawColumnBottomToTop draws block element runes going up from given point. +// The value of float64 is the number of characters to draw going up. +// A fractional value is used since there are 1/8th lower block elements and +// fractional values will map to the nearest 1/8th block for the last rune drawn. +// Handles overlapping columns of diferent rune heights. +// If there exists an existing column at given Point with same height as new column, +// then the existing column will be replaced. +// Applies style to all block runes. +// Coordinates (0,0) is top left of canvas. +func DrawColumnBottomToTop(m *canvas.Model, p canvas.Point, v float64, s lipgloss.Style) { + if v <= 0 { + return + } + x := p.X + y := p.Y + + h := getColumnHeight(m, p) // height of existing column on canvas + n := math.Floor(v) // number of full blocks to show + nh := int(n) // height of new column to draw on canvas + + r := runes.LowerBlockElementFromFloat64(v - n) + if r != runes.Null { + nh++ + } + + fb := canvas.NewCellWithStyle(runes.FullBlock, s) + if (h == 0) || (nh == h) { // replace entire column if same height or no existing column + // set full block columns + end := int(n) + for i := 0; i < end; i++ { + m.SetCell(canvas.Point{x, y - i}, fb) + } + // set column top rune + DrawColumnRune(m, canvas.Point{x, y - end}, r, s) + } else if nh < h { // new column shorter than old column + // replace existing full blocks with new full blocks + end := int(n) + for i := 0; i < end; i++ { + m.SetCell(canvas.Point{x, y - i}, fb) + } + // overlap new column top rune on top of old full block + DrawColumnRune(m, canvas.Point{x, y - end}, r, s) + } else if nh > h { // new column taller than old column + oc := (h - 1) // index of existing column top + if oc <= 0 { + oc = 0 + } + // overlap existing column top rune on top of new full block + DrawColumnRune(m, canvas.Point{x, y - oc}, runes.FullBlock, s) + // draw new full blocks above existing columns + end := int(n) + for i := h; i < end; i++ { + m.SetCell(canvas.Point{x, y - i}, fb) + } + // set new column top rune + m.SetCell(canvas.Point{x, y - end}, canvas.NewCellWithStyle(r, s)) + } +} + +// DrawColumnRune draws a column rune on to the canvas at given (X,Y) coordinates with given style. +// The function checks for existing column runes already on the canvas and attempts to +// draws runes such that the runes appear overlapping. +// Overlapping runes can only occur if either one of the runes is a full block element rune, +// and the other rune is not a full block element rune. +// If the runes cannot overlap, then it will the existing rune will be replaced. +// Does nothing if given rune is Null or is not a column rune. +func DrawColumnRune(m *canvas.Model, p canvas.Point, r rune, s lipgloss.Style) { + if (r == runes.Null) || !runes.IsLowerBlockElement(r) { + return + } + rs := s.Copy() + c := m.Cell(p) + if runes.IsLowerBlockElement(c.Rune) { + if (r == runes.FullBlock) && (c.Rune != runes.FullBlock) { // existing rune on top of new full block + r = c.Rune + rs.Background(s.GetForeground()).Foreground(c.Style.GetForeground()) + } else if (c.Rune == runes.FullBlock) && r != runes.FullBlock { // new rune on top of existing full block + rs.Background(c.Style.GetForeground()).Foreground(s.GetForeground()) + } + } + m.SetCell(p, canvas.NewCellWithStyle(r, rs)) +} + +// getColumnHeight obtains number of runes drawn +// by the DrawColumnBottomToTop function at given Point. +func getColumnHeight(m *canvas.Model, p canvas.Point) int { + x := p.X + y := p.Y + i := 0 + c := m.Cell(canvas.Point{x, y}) + for runes.IsLowerBlockElement(c.Rune) { + i++ + c = m.Cell(canvas.Point{x, y - i}) + } + return i +} + +// DrawRows draws rows going right on to canvas +// starting from a given (X,Y) coordinate and a sequence of row widths. +// Rows will be drawn from top to bottom and +// sequential row widths will increment Y coordinates for drawing. +// Handles overlapping rows of diferent rune widths. +// If there exists an existing row at given Point with same width as new row, +// then the existing row will be replaced. +// Applies style to all block runes. +// Coordinates (0,0) is top left of canvas. +func DrawRows(m *canvas.Model, p canvas.Point, seqLen []float64, s lipgloss.Style) { + y := p.Y + x := p.X + for i, f := range seqLen { + DrawRowLeftToRight(m, canvas.Point{x, y + i}, f, s) + } +} + +// DrawRowLeftToRight draws block element runes going right from given point. +// The value of float64 is the number of characters to draw going right. +// A fractional value is used since there are 1/8th left block elements and +// fractional values will map to the nearest 1/8th block for the last rune drawn. +// Handles overlapping rows of diferent rune widths. +// If there exists an existing row at given Point with same width as new row, +// then the existing row will be replaced. +// Applies style to all block runes. +// Coordinates (0,0) is top left of canvas. +func DrawRowLeftToRight(m *canvas.Model, p canvas.Point, v float64, s lipgloss.Style) { + if v <= 0 { + return + } + x := p.X + y := p.Y + + w := getRowWidth(m, p) // width of existing row on canvas + n := math.Floor(v) // number of full blocks to show + nw := int(n) // width of new row to draw on canvas + + r := runes.LeftBlockElementFromFloat64(v - n) + if r != runes.Null { + nw++ + } + + fb := canvas.NewCellWithStyle(runes.FullBlock, s) + if (w == 0) || (nw == w) { // replace entire row if same width or no existing row + // set full block rows + end := int(n) + for i := 0; i < end; i++ { + m.SetCell(canvas.Point{x + i, y}, fb) + } + // set row rightmost rune + DrawRowRune(m, canvas.Point{x + end, y}, r, s) + } else if nw < w { // new row thinner than old row + // replace existing full blocks with new full blocks + end := int(n) + for i := 0; i < end; i++ { + m.SetCell(canvas.Point{x + i, y}, fb) + } + // overlap new row rightmost rune on top of old full block + DrawRowRune(m, canvas.Point{x + end, y}, r, s) + } else if nw > w { // new row wider than old row + oc := (w - 1) // index of existing row rightmost rune + if oc <= 0 { + oc = 0 + } + // overlap existing row rightmost rune on top of new full block + DrawRowRune(m, canvas.Point{x + oc, y}, runes.FullBlock, s) + // draw new full blocks above existing rows + end := int(n) + for i := w; i < end; i++ { + m.SetCell(canvas.Point{x + i, y}, fb) + } + // set new row rightmost rune + m.SetCell(canvas.Point{x + end, y}, canvas.NewCellWithStyle(r, s)) + } +} + +// DrawRowRune draws a row rune on to the canvas at given (X,Y) coordinates with given style. +// The function checks for existing row runes already on the canvas and attempts to +// draws runes such that the runes appear overlapping. +// Overlapping runes can only occur if either one of the runes is a full block element rune, +// and the other rune is not a full block element rune. +// If the runes cannot overlap, then it will the existing rune will be replaced. +// Does nothing if given rune is Null or is not a row rune. +func DrawRowRune(m *canvas.Model, p canvas.Point, r rune, s lipgloss.Style) { + if (r == runes.Null) || !runes.IsLeftBlockElement(r) { + return + } + rs := s.Copy() + c := m.Cell(p) + if runes.IsLeftBlockElement(c.Rune) { + if (r == runes.FullBlock) && (c.Rune != runes.FullBlock) { // existing rune on top of new full block + r = c.Rune + rs.Background(s.GetForeground()).Foreground(c.Style.GetForeground()) + } else if (c.Rune == runes.FullBlock) && r != runes.FullBlock { // new rune on top of existing full block + rs.Background(c.Style.GetForeground()).Foreground(s.GetForeground()) + } + } + m.SetCell(p, canvas.NewCellWithStyle(r, rs)) +} + +// getRowWidth obtains number of runes drawn +// by the DrawRowRightToLeft function at given Point. +func getRowWidth(m *canvas.Model, p canvas.Point) int { + x := p.X + y := p.Y + i := 0 + c := m.Cell(canvas.Point{x, y}) + for runes.IsLeftBlockElement(c.Rune) { + i++ + c = m.Cell(canvas.Point{x + i, y}) + } + return i +} + +// abs returns absolute value of given integer. +func abs(i int) int { + if i < 0 { + return i * -1 + } + return i +} + +// GetFullCirclePoints returns a []canvas.Point containing points +// that approximates a filled circle of radius r for center Point c. +func GetFullCirclePoints(c canvas.Point, r int) (p []canvas.Point) { + if r <= 0 { + return + } + // sort points + cPoints := GetCirclePoints(c, r) + sort.Slice(cPoints, func(i, j int) bool { + a := cPoints[i] + b := cPoints[j] + if a.Y == b.Y { + return a.X < b.X + } + return a.Y < b.Y + }) + // set all cells between first and last point of a row + f := cPoints[0] + l := cPoints[0] + for _, v := range cPoints { + // if new row, draw line between previous row first and last points + if v.Y != l.Y { + for i := f.X; i < l.X; i++ { + p = append(p, canvas.Point{X: i, Y: l.Y}) + } + f = v + } + l = v + p = append(p, v) + } + return +} + +// GetCirclePoints returns a []canvas.Point containing points +// that approximates a circle of radius r for center Point c. +func GetCirclePoints(c canvas.Point, r int) (p []canvas.Point) { + if r <= 0 { + return + } + t1 := r / 16 + t2 := 0 + x := r + y := 0 + for x >= y { + p = append(p, c.Add(canvas.Point{X: x, Y: y})) + p = append(p, c.Add(canvas.Point{X: x, Y: -y})) + p = append(p, c.Add(canvas.Point{X: -x, Y: y})) + p = append(p, c.Add(canvas.Point{X: -x, Y: -y})) + p = append(p, c.Add(canvas.Point{X: y, Y: x})) + p = append(p, c.Add(canvas.Point{X: y, Y: -x})) + p = append(p, c.Add(canvas.Point{X: -y, Y: x})) + p = append(p, c.Add(canvas.Point{X: -y, Y: -x})) + y++ + t1 += y + t2 = t1 - x + if t2 >= 0 { + t1 = t2 + x-- + } + } + return +} + +// GetLinePoints returns a []canvas.Point containing points +// that approximates a line between points p1 and p2. +func GetLinePoints(p1 canvas.Point, p2 canvas.Point) []canvas.Point { + if abs(p2.Y-p1.Y) < abs(p2.X-p1.X) { + if p1.X > p2.X { + return getLinePointsLow(p2, p1) + } else { + return getLinePointsLow(p1, p2) + } + } else { + if p1.Y > p2.Y { + return getLinePointsHigh(p2, p1) + } else { + return getLinePointsHigh(p1, p2) + } + } +} + +// getLinePointsLow returns a []canvas.Point containing points +// that approximates a line between points p1 and p2 for +// slight line slopes between -1 and 1. +func getLinePointsLow(p1 canvas.Point, p2 canvas.Point) (r []canvas.Point) { + dx := (p2.X - p1.X) + dy := (p2.Y - p1.Y) + yi := 1 + if dy < 0 { + yi = -1 + dy = -dy + } + D := (2 * dy) - dx + y := p1.Y + + start := p1.X + end := p2.X + if start > end { + start = p2.X + end = p1.X + } + for x := start; x <= end; x++ { + r = append(r, canvas.Point{X: x, Y: y}) + if D > 0 { + y += yi + D += (2 * (dy - dx)) + } else { + D += 2 * dy + } + } + return +} + +// getLinePointsHigh returns a []canvas.Point containing points +// that approximates a line between points p1 and p2 for +// steep line slopes <= -1 or >= 1. +func getLinePointsHigh(p1 canvas.Point, p2 canvas.Point) (r []canvas.Point) { + dx := (p2.X - p1.X) + dy := (p2.Y - p1.Y) + xi := 1 + if dx < 0 { + xi = -1 + dx = -dx + } + D := (2 * dx) - dy + x := p1.X + + start := p1.Y + end := p2.Y + if start > end { + start = p2.Y + end = p1.Y + } + for y := start; y <= end; y++ { + r = append(r, canvas.Point{X: x, Y: y}) + if D > 0 { + x += xi + D += (2 * (dx - dy)) + } else { + D += 2 * dx + } + } + return +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/canvas/options.go b/vendor/github.com/NimbleMarkets/ntcharts/canvas/options.go new file mode 100644 index 00000000..e4f8e297 --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/canvas/options.go @@ -0,0 +1,86 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +package canvas + +// File contains options used by the canvas during initialization with New(). + +import ( + "github.com/charmbracelet/lipgloss" + zone "github.com/lrstanley/bubblezone" +) + +// Option is used to set options when initializing a sparkline. Example: +// +// canvas := New(width, height, WithStyle(someStyle), WithKeyMap(someKeyMap)) +type Option func(*Model) + +// WithStyle sets the default Cell style. +func WithStyle(s lipgloss.Style) Option { + return func(m *Model) { + m.Style = s + } +} + +// WithKeyMap sets the KeyMap used +// when processing keyboard event messages in Update(). +func WithKeyMap(k KeyMap) Option { + return func(m *Model) { + m.KeyMap = k + } +} + +// WithUpdateHandler sets the UpdateHandler used +// when processing bubbletea Msg events in Update(). +func WithUpdateHandler(h UpdateHandler) Option { + return func(m *Model) { + m.UpdateHandler = h + } +} + +// WithZoneManager sets the bubblezone Manager used +// when processing bubbletea Msg mouse events in Update(). +func WithZoneManager(zm *zone.Manager) Option { + return func(m *Model) { + m.SetZoneManager(zm) + } +} + +// WithCursor sets the cursor starting position for the viewport. +func WithCursor(p Point) Option { + return func(m *Model) { + m.SetCursor(p) + } +} + +// WithFocus sets the canvas to be focused. +func WithFocus() Option { + return func(m *Model) { + m.Focus() + } +} + +// WithContent copies the given []string into +// the contents of the canvas with default style. +// Each string will be copied into a row starting +// from the top of the canvas to bottom. +// Use option WithStyle() to set canvas style +// before using WithContent() for styling. +func WithContent(l []string) Option { + return func(m *Model) { + m.SetLines(l) + } +} + +// WithViewWidth sets the viewport width of the canvas. +func WithViewWidth(w int) Option { + return func(m *Model) { + m.ViewWidth = w + } +} + +// WithViewHeight sets the viewport height of the canvas. +func WithViewHeight(h int) Option { + return func(m *Model) { + m.ViewHeight = h + } +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/canvas/runes/runes.go b/vendor/github.com/NimbleMarkets/ntcharts/canvas/runes/runes.go new file mode 100644 index 00000000..45b2a843 --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/canvas/runes/runes.go @@ -0,0 +1,457 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +// Package runes contains commonly used runes and functions to obtain runes. +package runes + +// https://en.wikipedia.org/wiki/Box-drawing_character +// https://en.wikipedia.org/wiki/Braille_Patterns + +const ( + Null = '\u0000' + + LineHorizontal = '\u2500' // ─ + LineVertical = '\u2502' // │ + LineDownRight = '\u250C' // ┌ + LineDownLeft = '\u2510' // â” + LineUpRight = '\u2514' // â”” + LineUpLeft = '\u2518' // ┘ + LineVerticalRight = '\u251C' // ├ + LineVerticalLeft = '\u2524' // ┤ + LineHorizontalUp = '\u2534' // â”´ + LineHorizontalDown = '\u252C' // ┬ + LineHorizontalVertical = '\u253C' // ┼ + LineLeft = '\u2574' // â•´ + LineUp = '\u2575' // ╵ + LineRight = '\u2576' // ╶ + LineDown = '\u2577' // â•· + + ArcDownRight = '\u256D' // â•­ + ArcDownLeft = '\u256E' // â•® + ArcUpLeft = '\u256F' // ╯ + ArcUpRight = '\u2570' // â•° + + LowerBlockOne = '\u2581' // â– + LowerBlockTwo = '\u2582' // â–‚ + LowerBlockThree = '\u2583' // â–ƒ + LowerBlockFour = '\u2584' // â–„ + LowerBlockFive = '\u2585' // â–… + LowerBlockSix = '\u2586' // â–† + LowerBlockSeven = '\u2587' // â–‡ + FullBlock = '\u2588' // â–ˆ + LeftBlockSeven = '\u2589' // â–‰ + LeftBlockSix = '\u258A' // â–Š + LeftBlockFive = '\u258B' // â–‹ + LeftBlockFour = '\u258C' // â–Œ + LeftBlockThree = '\u258D' // â– + LeftBlockTwo = '\u258E' // â–Ž + LeftBlockOne = '\u258F' // â– +) + +/* +Braille dot number offsets + +Unicode Braille Patterns can be computed by +adding hex values to the beginning block offset + +[0][3] = [0x0001][0x0008] +[1][4] [0x0002][0x0010] +[2][5] [0x0004][0x0020] +[6][7] [0x0040][0x0080] +*/ + +const BrailleBlockOffset = 0x2800 // beginning of Unicode Braille Patterns (empty Braille Pattern) + +var brailleDotNumberOffsets = [8]int32{0x0001, 0x0002, 0x0004, 0x0008, 0x00010, 0x0020, 0x0040, 0x0080} + +// PatternDots indicates whether a dot in a Braille Pattern is displayed. +type PatternDots [8]bool + +// PatternDotsGrid is a 2D array where each row and column indicates whether +// a dot in a sequence of Braille Patterns runes should be displayed. +// Example: +// +// width = 4, height = 4 will give 2 Braille Pattern runes +// [0][3][0][3] +// [1][4][1][4] +// [2][5][2][5] +// [6][7][6][7] +// +// setting (0,0) will set Dot 0 of first braille rune +// setting (0,3) will set Dot 6 of first braille rune +// setting (3,0) will set Dot 3 of second braille rune +// setting (3,3) will set Dot 7 of second braille rune +type PatternDotsGrid struct { + w int // grid width + h int // grid height + g [][]bool // each index indicates whether to display Braille Pattern dot +} + +// NewPatternDotsGrid returns new initialized *PatternDotsGrid +func NewPatternDotsGrid(w, h int) *PatternDotsGrid { + g := PatternDotsGrid{ + w: w, + h: h, + } + g.Reset() + return &g +} + +// Reset will reset the internal grid +func (g *PatternDotsGrid) Reset() { + g.g = make([][]bool, g.h, g.h) + for i := range g.g { + g.g[i] = make([]bool, g.w) + } +} + +// Set will set value in grid at given column and row +func (g *PatternDotsGrid) Set(x int, y int) { + if (x < 0) || (x >= g.w) || (y < 0) || (y >= g.h) { + return + } + g.g[y][x] = true +} + +// Unset will unset value in grid at given column and row +func (g *PatternDotsGrid) Unset(x int, y int) { + if (x < 0) || (x >= g.w) || (y < 0) || (y >= g.h) { + return + } + g.g[y][x] = false +} + +// BraillePatterns returns a [][]rune containing Braille Pattern +// runes based on internal grid values. +func (g *PatternDotsGrid) BraillePatterns() (p [][]rune) { + for y := 0; y < g.h; { + xb := []rune{} + for x := 0; x < g.w; { + xb = append(xb, g.getBraillePattern(x, y)) + x += 2 // each braille pattern rune has a width of 2 + } + p = append(p, xb) + y += 4 // each braille pattern rune has a height of 4 + } + return +} + +// getBraillePattern returns Braille Pattern rune +// starting at internal grid column and row. +func (g *PatternDotsGrid) getBraillePattern(x int, y int) (b rune) { + if (x < 0) || (x >= g.w) || (y < 0) || (y >= g.h) { + return + } + b = BrailleBlockOffset + // set left side of braille pattern + if g.g[y][x] { + b |= brailleDotNumberOffsets[0] + } + if (y+1 < g.h) && (g.g[y+1][x]) { + b |= brailleDotNumberOffsets[1] + } + if (y+2 < g.h) && (g.g[y+2][x]) { + b |= brailleDotNumberOffsets[2] + } + if (y+3 < g.h) && (g.g[y+3][x]) { + b |= brailleDotNumberOffsets[6] + } + // set right side of braille pattern + if (x+1 < g.w) && (g.g[y][x+1]) { + b |= brailleDotNumberOffsets[3] + } + if (y+1 < g.h) && (x+1 < g.w) && (g.g[y+1][x+1]) { + b |= brailleDotNumberOffsets[4] + } + if (y+2 < g.h) && (x+1 < g.w) && (g.g[y+2][x+1]) { + b |= brailleDotNumberOffsets[5] + } + if (y+3 < g.h) && (x+1 < g.w) && (g.g[y+3][x+1]) { + b |= brailleDotNumberOffsets[7] + } + return +} + +// IsBraillePattern returns whether a given rune is +// considered a Braile Pattern rune. +func IsBraillePattern(r rune) bool { + if r >= 0x2800 && r <= 0x28FF { + return true + } + return false +} + +// BraillePatternFromPatternDots returns a Braille Pattern rune using given PatternDots. +// Each index in PatternDots corresponds to Braille dot number +// and whether the dot should be displayed. +func BraillePatternFromPatternDots(p PatternDots) (r rune) { + r = BrailleBlockOffset + for i, b := range p { + if b { + r |= brailleDotNumberOffsets[i] + } + } + return +} + +// SetPatternDots sets given PatternDots dots based on given rune. +func SetPatternDots(r rune, p *PatternDots) { + if !IsBraillePattern(r) { + return + } + for i, b := range brailleDotNumberOffsets { + if (b & r) != Null { + p[i] = true + } + } + return +} + +// CombineBraillePatterns returns a rune +// that is a combination of two braille pattern runes. +// Any invalid braille pattern rune combinations will return r2. +func CombineBraillePatterns(r1 rune, r2 rune) rune { + if !IsBraillePattern(r1) || !IsBraillePattern(r2) { + return r2 + } + return (r1 | r2) +} + +var lowerBlockElements = [9]rune{ + Null, + LowerBlockOne, + LowerBlockTwo, + LowerBlockThree, + LowerBlockFour, + LowerBlockFive, + LowerBlockSix, + LowerBlockSeven, + FullBlock, +} + +// IsLowerBlockElement returns whether a given rune is +// considered a lower block or full block element. +func IsLowerBlockElement(r rune) bool { + if r >= 0x2581 && r <= 0x2588 { + return true + } + return false +} + +// LowerBlockElementFromFloat64 returns either an empty rune +// or a lower Block Element rune using given float64. +// A float64 < 1.0 will return the nearest one eights lower block element +// corresponding to the float value. An empty rune will be returned if +// float64 does not round to lowest 1/8 lower block. +func LowerBlockElementFromFloat64(f float64) rune { + if f >= 1 { + return lowerBlockElements[8] + } else if f <= 0 { + return lowerBlockElements[0] + } + e := int(f / .125) // number of 1/8s blocks to show + // round remaining fraction smaller than 1/8 to nearest 1/16 + if n := f - (float64(e) * .125); n >= 0.0625 { + e++ + } + return lowerBlockElements[e] +} + +var leftBlockElements = [9]rune{ + Null, + LeftBlockOne, + LeftBlockTwo, + LeftBlockThree, + LeftBlockFour, + LeftBlockFive, + LeftBlockSix, + LeftBlockSeven, + FullBlock, +} + +// IsLeftBlockElement returns whether a given rune is +// considered a left block or full block element. +func IsLeftBlockElement(r rune) bool { + if r >= 0x2588 && r <= 0x258F { + return true + } + return false +} + +// LeftBlockElementFromFloat64 returns either an empty rune +// or a left Block Element rune using given float64. +// A float64 < 1.0 will return the nearest one eights left block element +// corresponding to the float value. An empty rune will be returned if +// float64 does not round to lowest 1/8 left block. +func LeftBlockElementFromFloat64(f float64) rune { + if f >= 1 { + return leftBlockElements[8] + } else if f <= 0 { + return leftBlockElements[0] + } + e := int(f / .125) // number of 1/8s blocks to show + // round remaining fraction smaller than 1/8 to nearest 1/16 + if n := f - (float64(e) * .125); n >= 0.0625 { + e++ + } + return leftBlockElements[e] +} + +// LineStyle enumerates the different style of line runes to display. +type LineStyle int + +const ( + ThinLineStyle LineStyle = iota + ArcLineStyle +) + +// LineSegments indicates whether a line segment +// going up, down, left, or right is displayed. +type LineSegments struct { + Up bool + Down bool + Left bool + Right bool +} + +var arcLineSegmentsMap = map[LineSegments]rune{ + {false, false, false, false}: Null, + {false, false, false, true}: LineRight, + {false, false, true, false}: LineLeft, + {false, false, true, true}: LineHorizontal, + {false, true, false, false}: LineDown, + {false, true, false, true}: ArcDownRight, + {false, true, true, false}: ArcDownLeft, + {false, true, true, true}: LineHorizontalDown, + {true, false, false, false}: LineUp, + {true, false, false, true}: ArcUpRight, + {true, false, true, false}: ArcUpLeft, + {true, false, true, true}: LineHorizontalUp, + {true, true, false, false}: LineVertical, + {true, true, false, true}: LineVerticalRight, + {true, true, true, false}: LineVerticalLeft, + {true, true, true, true}: LineHorizontalVertical, +} + +var thinLineSegmentsMap = map[LineSegments]rune{ + {false, false, false, false}: Null, + {false, false, false, true}: LineRight, + {false, false, true, false}: LineLeft, + {false, false, true, true}: LineHorizontal, + {false, true, false, false}: LineDown, + {false, true, false, true}: LineDownRight, + {false, true, true, false}: LineDownLeft, + {false, true, true, true}: LineHorizontalDown, + {true, false, false, false}: LineUp, + {true, false, false, true}: LineUpRight, + {true, false, true, false}: LineUpLeft, + {true, false, true, true}: LineHorizontalUp, + {true, true, false, false}: LineVertical, + {true, true, false, true}: LineVerticalRight, + {true, true, true, false}: LineVerticalLeft, + {true, true, true, true}: LineHorizontalVertical, +} + +// SetLineSegments sets given LineSegments directions based on given rune. +func SetLineSegments(r rune, l *LineSegments) { + switch r { + case ArcDownRight, LineDownRight: + l.Down = true + l.Right = true + case ArcDownLeft, LineDownLeft: + l.Down = true + l.Left = true + case ArcUpLeft, LineUpLeft: + l.Up = true + l.Left = true + case ArcUpRight, LineUpRight: + l.Up = true + l.Right = true + case LineHorizontal: + l.Left = true + l.Right = true + case LineVertical: + l.Up = true + l.Down = true + case LineHorizontalUp: + l.Up = true + l.Left = true + l.Right = true + case LineHorizontalDown: + l.Down = true + l.Left = true + l.Right = true + case LineVerticalRight: + l.Up = true + l.Down = true + l.Right = true + case LineVerticalLeft: + l.Up = true + l.Down = true + l.Left = true + case LineHorizontalVertical: + l.Up = true + l.Down = true + l.Left = true + l.Right = true + case LineUp: + l.Up = true + case LineDown: + l.Down = true + case LineLeft: + l.Left = true + case LineRight: + l.Right = true + } + return +} + +// IsLine returns whether a given rune is considered a line rune. +func IsLine(r rune) bool { + if (r >= 0x2500 && r <= 0x253C) || (r >= 0x256D && r <= 0x2570) || (r >= 0x2574 && r <= 0x2577) { + return true + } + return false +} + +// ArcLineFromLineSegments returns either an empty rune +// or a line rune using given LineSegments. +// LineSegments contain whether or not the returned rune +// should display arc lines going up, down, left or right. +func ArcLineFromLineSegments(l LineSegments) rune { + return arcLineSegmentsMap[l] +} + +// ThinLineFromLineSegments returns either an empty rune +// or a line rune using given LineSegments. +// LineSegments contain whether or not the returned rune +// should display thin lines going up, down, left or right. +func ThinLineFromLineSegments(l LineSegments) rune { + return thinLineSegmentsMap[l] +} + +// CombineLines returns a rune that is a combination of two line runes. +// Any invalid line rune combinations or invalid LineStyle will return r2. +// The Linestyle determines the output line rune, even if +// the two input line runes are not of that style. +func CombineLines(r1 rune, r2 rune, ls LineStyle) (r rune) { + r = r2 + r1ok := IsLine(r1) + r2ok := IsLine(r2) + if !r1ok && !r2ok { + return + } + var l LineSegments + if r1ok { + SetLineSegments(r1, &l) + } + if r2ok { + SetLineSegments(r2, &l) + } + switch ls { + case ThinLineStyle: + r = ThinLineFromLineSegments(l) + case ArcLineStyle: + r = ArcLineFromLineSegments(l) + } + return +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/canvas/updatehandler.go b/vendor/github.com/NimbleMarkets/ntcharts/canvas/updatehandler.go new file mode 100644 index 00000000..df57891f --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/canvas/updatehandler.go @@ -0,0 +1,155 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +package canvas + +// File contains methods and objects used during canvas Model Update() +// to modify internal state. +// canvas is able to move the viewport displaying the contents +// either up, down, left and right + +import ( + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +type KeyMap struct { + Up key.Binding + Down key.Binding + Left key.Binding + Right key.Binding + PgUp key.Binding + PgDown key.Binding +} + +// DefaultKeyMap returns a default KeyMap for canvas. +func DefaultKeyMap() KeyMap { + return KeyMap{ + Up: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "move up"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "move down"), + ), + Left: key.NewBinding( + key.WithKeys("left", "h"), + key.WithHelp("â†/h", "move left"), + ), + Right: key.NewBinding( + key.WithKeys("right", "l"), + key.WithHelp("→/l", "move right"), + ), + PgUp: key.NewBinding( + key.WithKeys("pgup"), + key.WithHelp("PgUp", "zoom in"), + ), + PgDown: key.NewBinding( + key.WithKeys("pgdown"), + key.WithHelp("PgDown", "zoom out"), + ), + } +} + +// UpdateHandler callback invoked during an Update() +// and passes in the canvas *Model and bubbletea Msg. +type UpdateHandler func(*Model, tea.Msg) + +// DefaultUpdateHandler is used by canvas chart to enable +// moving viewing window using the mouse wheel, +// holding down mouse left button and moving, +// and with the arrow keys. +// Uses canvas Keymap for keyboard messages. +func DefaultUpdateHandler() UpdateHandler { + var lastPos Point // tracks zone position of last zone mouse position + return func(m *Model, tm tea.Msg) { + switch msg := tm.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.KeyMap.Up): + m.MoveUp(1) + case key.Matches(msg, m.KeyMap.Down): + m.MoveDown(1) + case key.Matches(msg, m.KeyMap.Left): + m.MoveLeft(1) + case key.Matches(msg, m.KeyMap.Right): + m.MoveRight(1) + case key.Matches(msg, m.KeyMap.PgUp): + m.MoveUp(1) + case key.Matches(msg, m.KeyMap.PgDown): + m.MoveDown(1) + } + case tea.MouseMsg: + switch msg.Button { + case tea.MouseButtonWheelUp: + m.MoveUp(1) + case tea.MouseButtonWheelDown: + m.MoveDown(1) + case tea.MouseButtonWheelRight: + m.MoveRight(1) + case tea.MouseButtonWheelLeft: + m.MoveLeft(1) + } + + if m.zoneManager == nil { + return + } + switch msg.Action { + case tea.MouseActionPress: + zInfo := m.zoneManager.Get(m.zoneID) + if zInfo.InBounds(msg) { + x, y := zInfo.Pos(msg) + lastPos = Point{X: x, Y: y} // set position of last click + } + case tea.MouseActionMotion: // event occurs when mouse is pressed + zInfo := m.zoneManager.Get(m.zoneID) + if zInfo.InBounds(msg) { + x, y := zInfo.Pos(msg) + if x > lastPos.X { + m.MoveRight(1) + } else if x < lastPos.X { + m.MoveLeft(1) + } + if y > lastPos.Y { + m.MoveDown(1) + } else if y < lastPos.Y { + m.MoveUp(1) + } + lastPos = Point{X: x, Y: y} // update last mouse position + } + } + } + } +} + +// MoveUp moves cursor up if possible. +func (m *Model) MoveUp(i int) { + m.cursor.Y -= i + if m.cursor.Y < 0 { + m.cursor.Y = 0 + } +} + +// MoveDown moves cursor down if possible. +func (m *Model) MoveDown(i int) { + endY := m.cursor.Y + m.ViewHeight + i + if endY <= m.area.Dy() { + m.cursor.Y += i + } +} + +// MoveLeft moves cursor left if possible. +func (m *Model) MoveLeft(i int) { + m.cursor.X -= i + if m.cursor.X < 0 { + m.cursor.X = 0 + } +} + +// MoveRight moves cursor right if possible. +func (m *Model) MoveRight(i int) { + endX := m.cursor.X + m.ViewWidth + 1 + if endX <= m.area.Dx() { + m.cursor.X += i + } +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/linechart/linechart.go b/vendor/github.com/NimbleMarkets/ntcharts/linechart/linechart.go new file mode 100644 index 00000000..12ccba49 --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/linechart/linechart.go @@ -0,0 +1,733 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +// Package linechart implements a canvas that displays +// (X,Y) Cartesian coordinates as a line chart. +package linechart + +import ( + "fmt" + "math" + + "github.com/NimbleMarkets/ntcharts/canvas" + "github.com/NimbleMarkets/ntcharts/canvas/graph" + "github.com/NimbleMarkets/ntcharts/canvas/runes" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + zone "github.com/lrstanley/bubblezone" +) + +var defaultStyle = lipgloss.NewStyle() + +// LabelFormatter converts a float64 into text +// for displaying the X and Y axis labels +// given an index of label and numeric value +// Index increments from minimum value to maximum values. +type LabelFormatter func(int, float64) string + +// DefaultLabelFormatter returns a LabelFormatter +// that convert float64 to integers +func DefaultLabelFormatter() LabelFormatter { + return func(i int, v float64) string { + return fmt.Sprintf("%.0f", v) + } +} + +// Model contains state of a linechart with an embedded canvas.Model +type Model struct { + UpdateHandler UpdateHandler + Canvas canvas.Model + Style lipgloss.Style // style applied when drawing runes + AxisStyle lipgloss.Style // style applied when drawing X and Y axes + LabelStyle lipgloss.Style // style applied when drawing X and Y number value + XLabelFormatter LabelFormatter // convert to X number values display string + YLabelFormatter LabelFormatter // convert to Y number values display string + xStep int // number of steps when displaying X axis values + yStep int // number of steps when displaying Y axis values + focus bool + + // the expected min and max values + minX float64 + maxX float64 + minY float64 + maxY float64 + + // current min and max axes values to display + viewMinX float64 + viewMaxX float64 + viewMinY float64 + viewMaxY float64 + + // whether to automatically set expected values + // when a value appears beyond the existing bounds + AutoMinX bool + AutoMaxX bool + AutoMinY bool + AutoMaxY bool + + origin canvas.Point // start of X and Y axes lines on canvas for graphing area + graphWidth int // width of graphing area - excludes X axis and labels + graphHeight int // height of graphing area - excludes Y axis and labels + + zoneManager *zone.Manager // provides mouse functionality + zoneID string +} + +// New returns a linechart Model initialized with given width, height, +// expected data value ranges and various options. +// Width and height includes area used for chart labeling. +// If xStep is 0, then will not draw X axis or values below X axis. +// If yStep is 0, then will not draw Y axis or values left of Y axis. +func New(w, h int, minX, maxX, minY, maxY float64, opts ...Option) Model { + m := Model{ + UpdateHandler: XYAxesUpdateHandler(1, 1), + Canvas: canvas.New(w, h), + Style: defaultStyle, + AxisStyle: defaultStyle, + LabelStyle: defaultStyle, + XLabelFormatter: DefaultLabelFormatter(), + YLabelFormatter: DefaultLabelFormatter(), + yStep: 2, + xStep: 2, + minX: minX, + maxX: maxX, + minY: minY, + maxY: maxY, + viewMinX: minX, + viewMaxX: maxX, + viewMinY: minY, + viewMaxY: maxY, + } + for _, opt := range opts { + opt(&m) + } + m.UpdateGraphSizes() + return m +} + +// getGraphSizeAndOrigin calculates and returns the linechart origin and graph width and height +func getGraphSizeAndOrigin(w, h int, minY, maxY float64, xStep, yStep int, yFmter LabelFormatter) (canvas.Point, int, int) { + // graph width and height exclude area used by axes + // origin point is canvas coordinates of where axes are drawn + origin := canvas.Point{X: 0, Y: h - 1} + gWidth := w + gHeight := h + if xStep > 0 { + // use last 2 rows of canvas to plot X axis and tick values + origin.Y -= 1 + gHeight -= 2 + } + if yStep > 0 { + // find out how many spaces left of the Y axis + // to reserve for axis tick value by checking the string length + // of all values to be displayed + var lastVal string + valueLen := 0 + rangeSz := maxY - minY // range of possible expected values + increment := rangeSz / float64(gHeight) + for i := 0; i <= gHeight; { + v := minY + (increment * float64(i)) // value to set left of Y axis + s := yFmter(i, v) + if lastVal != s { + if len(s) > valueLen { + valueLen = len(s) + } + lastVal = s + } + i += yStep + } + origin.X += valueLen + gWidth -= (valueLen + 1) // ignore Y axis and tick values + } + return origin, gWidth, gHeight +} + +// UpdateGraphSizes updates the Model origin, graph width and graph height. +// This method is should be called whenever the X and Y axes values have changed, +// for example when X and Y ranges have been adjusted by AutoAdjustRange(). +func (m *Model) UpdateGraphSizes() { + origin, gWidth, gHeight := getGraphSizeAndOrigin( + m.Canvas.Width(), + m.Canvas.Height(), + m.viewMinY, + m.viewMaxY, + m.xStep, + m.yStep, + m.YLabelFormatter, + ) + m.origin = origin + m.graphWidth = gWidth + m.graphHeight = gHeight +} + +// Width returns linechart width. +func (m *Model) Width() int { + return m.Canvas.Width() +} + +// Height returns linechart height. +func (m *Model) Height() int { + return m.Canvas.Height() +} + +// GraphWidth returns linechart graphing area width. +func (m *Model) GraphWidth() int { + return m.graphWidth +} + +// GraphHeight returns linechart graphing area height. +func (m *Model) GraphHeight() int { + return m.graphHeight +} + +// MinX returns linechart expected minimum X value. +func (m *Model) MinX() float64 { + return m.minX +} + +// MaxX returns linechart expected maximum X value. +func (m *Model) MaxX() float64 { + return m.maxX +} + +// MinY returns linechart expected minimum Y value. +func (m *Model) MinY() float64 { + return m.minY +} + +// MaxY returns linechart expected maximum Y value. +func (m *Model) MaxY() float64 { + return m.maxY +} + +// ViewMinX returns linechart displayed minimum X value. +func (m *Model) ViewMinX() float64 { + return m.viewMinX +} + +// ViewMaxX returns linechart displayed maximum X value. +func (m *Model) ViewMaxX() float64 { + return m.viewMaxX +} + +// ViewMinY returns linechart displayed minimum Y value. +func (m *Model) ViewMinY() float64 { + return m.viewMinY +} + +// ViewMaxY returns linechart displayed maximum Y value. +func (m *Model) ViewMaxY() float64 { + return m.viewMaxY +} + +// XStep returns number of steps when displaying Y axis values. +func (m *Model) XStep() int { + return m.xStep +} + +// XStep returns number of steps when displaying Y axis values. +func (m *Model) YStep() int { + return m.yStep +} + +// Origin returns a canvas Point with the coordinates +// of the linechart graph (X,Y) origin. +func (m *Model) Origin() canvas.Point { + return m.origin +} + +// Clear will reset linechart canvas including axes and labels. +func (m *Model) Clear() { + m.Canvas.Clear() +} + +// SetXStep updates the number of steps when displaying X axis values. +func (m *Model) SetXStep(xStep int) { + m.xStep = xStep + m.UpdateGraphSizes() +} + +// SetYStep updates the number of steps when displaying Y axis values. +func (m *Model) SetYStep(yStep int) { + m.yStep = yStep + m.UpdateGraphSizes() +} + +// SetXRange updates the minimum and maximum expected X values. +func (m *Model) SetXRange(min, max float64) { + m.minX = min + m.maxX = max +} + +// SetYRange updates the minimum and maximum expected Y values. +func (m *Model) SetYRange(min, max float64) { + m.minY = min + m.maxY = max +} + +// SetXYRange updates the minimum and maximum expected X and Y values. +func (m *Model) SetXYRange(minX, maxX, minY, maxY float64) { + m.SetXRange(minX, maxX) + m.SetYRange(minY, maxY) +} + +// SetXRange updates the displayed minimum and maximum X values. +// Minimum and maximum values will be bounded by the expected X values. +// Returns whether not displayed X values have updated. +func (m *Model) SetViewXRange(min, max float64) bool { + vMin := math.Max(m.minX, min) + vMax := math.Min(m.maxX, max) + if vMin < vMax { + m.viewMinX = vMin + m.viewMaxX = vMax + m.UpdateGraphSizes() + return true + } + return false +} + +// SetYRange updates the displayed minimum and maximum Y values. +// Minimum and maximum values will be bounded by the expected Y values. +// Returns whether not displayed Y values have updated. +func (m *Model) SetViewYRange(min, max float64) bool { + vMin := math.Max(m.minY, min) + vMax := math.Min(m.maxY, max) + if vMin < vMax { + m.viewMinY = vMin + m.viewMaxY = vMax + m.UpdateGraphSizes() + return true + } + return false +} + +// SetViewXYRange updates the displayed minimum and maximum X and Y values. +// Minimum and maximum values will be bounded by the expected values. +func (m *Model) SetViewXYRange(minX, maxX, minY, maxY float64) { + m.SetViewXRange(minX, maxX) + m.SetViewYRange(minY, maxY) +} + +// Resize will change linechart display width and height. +// Existing runes on the linechart will not be redrawn. +func (m *Model) Resize(w, h int) { + m.Canvas.Resize(w, h) + m.Canvas.ViewWidth = w + m.Canvas.ViewHeight = h + m.UpdateGraphSizes() +} + +// AutoAdjustRange automatically adjusts both the expected X and Y values +// and the displayed X and Y values if enabled and the given Float64Point +// is outside of expected ranges. +// It returns whether or not the display X and Y ranges have been adjusted. +func (m *Model) AutoAdjustRange(f canvas.Float64Point) (b bool) { + // adjusts both expected range and + // the display range (if not zoomed in) + if m.AutoMinX && (f.X < m.minX) { + if m.minX == m.viewMinX { + m.viewMinX = f.X + b = true + } + m.minX = f.X + } + if m.AutoMaxX && (f.X > m.maxX) { + if m.maxX == m.viewMaxX { + m.viewMaxX = f.X + b = true + } + m.maxX = f.X + } + if m.AutoMinY && (f.Y < m.minY) { + if m.minY == m.viewMinY { + m.viewMinY = f.Y + b = true + } + m.minY = f.Y + } + if m.AutoMaxY && (f.Y > m.maxY) { + if m.maxY == m.viewMaxY { + m.viewMaxY = f.Y + b = true + } + m.maxY = f.Y + } + return +} + +// SetZoneManager enables mouse functionality +// by setting a bubblezone.Manager to the linechart. +// The bubblezone.Manager can check bubbletea mouse event Msgs +// passed to the UpdateHandler handler during an Update(). +// The root bubbletea model must wrap the View() string with +// bubblezone.Manager.Scan() to enable mouse functionality. +// To disable mouse functionality after enabling, call SetZoneManager on nil. +func (m *Model) SetZoneManager(zm *zone.Manager) { + m.zoneManager = zm + if (zm != nil) && (m.zoneID == "") { + m.zoneID = zm.NewPrefix() + } +} + +// ZoneManager will return linechart zone Manager. +func (m *Model) ZoneManager() *zone.Manager { + return m.zoneManager +} + +// ZoneID will return linechart zone ID used by zone Manager. +func (m *Model) ZoneID() string { + return m.zoneID +} + +// drawYLabel draws Y axis values left of the Y axis every n step. +// Repeating values will be hidden. +// Does nothing if n <= 0. +func (m *Model) drawYLabel(n int) { + // from origin going up, draw data value left of the Y axis every n steps + // origin X coordinates already set such that there is space available + if n <= 0 { + return + } + var lastVal string + rangeSz := m.viewMaxY - m.viewMinY // range of possible expected values + increment := rangeSz / float64(m.graphHeight) + for i := 0; i <= m.graphHeight; { + v := m.viewMinY + (increment * float64(i)) // value to set left of Y axis + s := m.YLabelFormatter(i, v) + if lastVal != s { + m.Canvas.SetStringWithStyle(canvas.Point{m.origin.X - len(s), m.origin.Y - i}, s, m.LabelStyle) + lastVal = s + } + i += n + } +} + +// drawXLabel draws X axis values below the X axis every n step. +// Repeating values will be hidden. +// Does nothing if n <= 0. +func (m *Model) drawXLabel(n int) { + // from origin going right, draw data value left of the Y axis every n steps + if n <= 0 { + return + } + var lastVal string + rangeSz := m.viewMaxX - m.viewMinX // range of possible expected values + increment := rangeSz / float64(m.graphWidth) + for i := 0; i < m.graphWidth; { + // can only set if rune to the left of target coordinates is empty + if c := m.Canvas.Cell(canvas.Point{m.origin.X + i - 1, m.origin.Y + 1}); c.Rune == runes.Null { + v := m.viewMinX + (increment * float64(i)) // value to set under X axis + s := m.XLabelFormatter(i, v) + // dont display if number will be cut off or value repeats + sLen := len(s) + m.origin.X + i + if (s != lastVal) && (sLen <= m.Canvas.Width()) { + m.Canvas.SetStringWithStyle(canvas.Point{m.origin.X + i, m.origin.Y + 1}, s, m.LabelStyle) + lastVal = s + } + } + i += n + } +} + +// DrawXYAxisAndLabel draws the X, Y axes. +func (m *Model) DrawXYAxisAndLabel() { + drawY := m.yStep > 0 + drawX := m.xStep > 0 + if drawY && drawX { + graph.DrawXYAxis(&m.Canvas, m.origin, m.AxisStyle) + } else { + if drawY { // draw Y axis + graph.DrawVerticalLineUp(&m.Canvas, m.origin, m.AxisStyle) + } + if drawX { // draw X axis + graph.DrawHorizonalLineRight(&m.Canvas, m.origin, m.AxisStyle) + } + } + m.drawYLabel(m.yStep) + m.drawXLabel(m.xStep) +} + +// scalePoint returns a Float64Point scaled to the graph size +// of the linechart from a Float64Point data point, width and height. +func (m *Model) scalePoint(f canvas.Float64Point, w, h int) (r canvas.Float64Point) { + dx := m.viewMaxX - m.viewMinX + dy := m.viewMaxY - m.viewMinY + if dx > 0 { + xs := float64(w) / dx + r.X = (f.X - m.viewMinX) * xs + } + if dy > 0 { + ys := float64(h) / dy + r.Y = (f.Y - m.viewMinY) * ys + } + return +} + +// ScaleFloat64Point returns a Float64Point scaled to the graph size +// of the linechart from a Float64Point data point. +func (m *Model) ScaleFloat64Point(f canvas.Float64Point) (r canvas.Float64Point) { + // Need to use one less width and height, otherwise values rounded to the nearest + // integer would be would be between 0 to graph width/height, + // and indexing the full graph width/height would be outside of the canvas + return m.scalePoint(f, m.graphWidth-1, m.graphHeight-1) +} + +// ScaleFloat64PointForLine returns a Float64Point scaled to the graph size +// of the linechart from a Float64Point data point. Used when drawing line runes +// with line styles that can combine with the axes. +func (m *Model) ScaleFloat64PointForLine(f canvas.Float64Point) (r canvas.Float64Point) { + // Full graph height and can be used since LineStyle runes + // can be combined with axes instead of overriding them + return m.scalePoint(f, m.graphWidth, m.graphHeight) +} + +// DrawRune draws the rune on to the linechart +// from a given Float64Point data point. +func (m *Model) DrawRune(f canvas.Float64Point, r rune) { + m.DrawRuneWithStyle(f, r, m.Style) +} + +// DrawRuneWithStyle draws the rune with style on to the linechart +// from a given Float64Point data point. +func (m *Model) DrawRuneWithStyle(f canvas.Float64Point, r rune, s lipgloss.Style) { + if m.AutoAdjustRange(f) { // auto adjust x and y ranges if enabled + m.UpdateGraphSizes() + } + sf := m.ScaleFloat64Point(f) // scale Cartesian coordinates data point to graphing area + p := canvas.CanvasPointFromFloat64Point(m.origin, sf) + // draw rune avoiding the axes + if m.yStep > 0 { + p.X++ + } + if m.xStep > 0 { + p.Y-- + } + m.Canvas.SetCell(p, canvas.NewCellWithStyle(r, s)) +} + +// DrawRuneLine draws the rune on to the linechart +// such that there is an approximate straight line between the two given +// Float64Point data points. +func (m *Model) DrawRuneLine(f1 canvas.Float64Point, f2 canvas.Float64Point, r rune) { + m.DrawRuneLineWithStyle(f1, f2, r, m.Style) +} + +// DrawRuneLineWithStyle draws the rune with style on to the linechart +// such that there is an approximate straight line between the two given +// Float64Point data points. +func (m *Model) DrawRuneLineWithStyle(f1 canvas.Float64Point, f2 canvas.Float64Point, r rune, s lipgloss.Style) { + // auto adjust x and y ranges if enabled + r1 := m.AutoAdjustRange(f1) + r2 := m.AutoAdjustRange(f2) + if r1 || r2 { + m.UpdateGraphSizes() + } + + // scale Cartesian coordinates data point to graphing area + sf1 := m.ScaleFloat64Point(f1) + sf2 := m.ScaleFloat64Point(f2) + + // convert scaled points to canvas points + p1 := canvas.CanvasPointFromFloat64Point(m.origin, sf1) + p2 := canvas.CanvasPointFromFloat64Point(m.origin, sf2) + + // draw rune on all canvas coordinates between + // the two canvas points that approximates a line + points := graph.GetLinePoints(p1, p2) + for _, p := range points { + if m.yStep > 0 { + p.X++ + } + if m.xStep > 0 { + p.Y-- + } + m.Canvas.SetCell(p, canvas.NewCellWithStyle(r, s)) + } +} + +// DrawRuneCircle draws the rune on to the linechart +// such that there is an approximate circle of float64 radious around +// the center of a circle at Float64Point data point. +func (m *Model) DrawRuneCircle(c canvas.Float64Point, f float64, r rune) { + m.DrawRuneCircleWithStyle(c, f, r, m.Style) +} + +// DrawRuneCircleWithStyle draws the rune with style on to the linechart +// such that there is an approximate circle of float64 radious around +// the center of a circle at Float64Point data point. +func (m *Model) DrawRuneCircleWithStyle(c canvas.Float64Point, f float64, r rune, s lipgloss.Style) { + center := canvas.NewPointFromFloat64Point(c) // round center to nearest integers + radius := int(math.Round(f)) // round radius to nearest integer + + points := graph.GetCirclePoints(center, radius) + for _, v := range points { + np := canvas.NewFloat64PointFromPoint(v) + // auto adjust x and y ranges if enabled + if m.AutoAdjustRange(np) { + m.UpdateGraphSizes() + } + // scale Cartesian coordinates data point to graphing area + sf := m.ScaleFloat64Point(np) + // convert scaled points to canvas points + p := canvas.CanvasPointFromFloat64Point(m.origin, sf) + // draw rune while avoiding drawing outside of graphing area + // or on the X and Y axes + ok := (p.X >= m.origin.X) && (p.Y <= m.origin.Y) + if (m.yStep > 0) && (p.X == m.origin.X) { + ok = false + } + if (m.xStep > 0) && (p.Y == m.origin.Y) { + ok = false + } + if ok { + m.Canvas.SetCell(p, canvas.NewCellWithStyle(r, s)) + } + } +} + +// DrawLine draws line runes of a given LineStyle on to the linechart +// such that there is an approximate straight line between the two given Float64Point data points. +func (m *Model) DrawLine(f1 canvas.Float64Point, f2 canvas.Float64Point, ls runes.LineStyle) { + m.DrawLineWithStyle(f1, f2, ls, m.Style) +} + +// DrawLineWithStyle draws line runes of a given LineStyle and style on to the linechart +// such that there is an approximate straight line between the two given Float64Point data points. +func (m *Model) DrawLineWithStyle(f1 canvas.Float64Point, f2 canvas.Float64Point, ls runes.LineStyle, s lipgloss.Style) { + // auto adjust x and y ranges if enabled + r1 := m.AutoAdjustRange(f1) + r2 := m.AutoAdjustRange(f2) + if r1 || r2 { + m.UpdateGraphSizes() + } + + // scale Cartesian coordinates data points to graphing area + sf1 := m.ScaleFloat64PointForLine(f1) + sf2 := m.ScaleFloat64PointForLine(f2) + + // convert scaled points to canvas points + p1 := canvas.CanvasPointFromFloat64Point(m.origin, sf1) + p2 := canvas.CanvasPointFromFloat64Point(m.origin, sf2) + + // draw line runes on all canvas coordinates between + // the two canvas points that approximates a line + points := graph.GetLinePoints(p1, p2) + if len(points) <= 0 { + return + } + graph.DrawLinePoints(&m.Canvas, points, ls, s) +} + +// DrawBrailleLine draws braille line runes of a given LineStyle on to the linechart +// such that there is an approximate straight line between the two given Float64Point data points. +// Braille runes will not overlap the axes. +func (m *Model) DrawBrailleLine(f1 canvas.Float64Point, f2 canvas.Float64Point) { + m.DrawBrailleLineWithStyle(f1, f2, m.Style) +} + +// DrawBrailleLineWithStyle draws braille line runes of a given LineStyle and style on to the linechart +// such that there is an approximate straight line between the two given Float64Point data points. +// Braille runes will not overlap the axes. +func (m *Model) DrawBrailleLineWithStyle(f1 canvas.Float64Point, f2 canvas.Float64Point, s lipgloss.Style) { + // auto adjust x and y ranges if enabled + r1 := m.AutoAdjustRange(f1) + r2 := m.AutoAdjustRange(f2) + if r1 || r2 { + m.UpdateGraphSizes() + } + + bGrid := graph.NewBrailleGrid(m.graphWidth, m.graphHeight, m.minX, m.maxX, m.minY, m.maxY) + + // get braille grid points from two Float64Point data points + p1 := bGrid.GridPoint(f1) + p2 := bGrid.GridPoint(f2) + + // set all points in the braille grid between two points that approximates a line + points := graph.GetLinePoints(p1, p2) + for _, p := range points { + bGrid.Set(p) + } + + // get all rune patterns for braille grid and draw them on to the canvas + startX := 0 + if m.yStep > 0 { + startX = m.origin.X + 1 + } + patterns := bGrid.BraillePatterns() + graph.DrawBraillePatterns(&m.Canvas, canvas.Point{X: startX, Y: 0}, patterns, s) +} + +// DrawBrailleCircle draws braille line runes of a given LineStyle on to the linechart +// such that there is an approximate circle of given float64 radius +// around the center of a circle at Float64Point data point. +// Braille runes will not overlap the axes. +func (m *Model) DrawBrailleCircle(p canvas.Float64Point, f float64) { + m.DrawBrailleCircleWithStyle(p, f, m.Style) +} + +// DrawBrailleCircleWithStyle draws braille line runes of a given LineStyle and style on to the linechart +// such that there is an approximate circle of given float64 radius +// around the center of a circle at Float64Point data point. +// Braille runes will not overlap the axes. +func (m *Model) DrawBrailleCircleWithStyle(c canvas.Float64Point, f float64, s lipgloss.Style) { + center := canvas.NewPointFromFloat64Point(c) // round center to nearest integer + radius := int(math.Round(f)) // round radius to nearest integer + + // set braille grid points from computed circle points around center + bGrid := graph.NewBrailleGrid(m.graphWidth, m.graphHeight, m.minX, m.maxX, m.minY, m.maxY) + points := graph.GetCirclePoints(center, radius) + for _, p := range points { + np := canvas.NewFloat64PointFromPoint(p) + if m.AutoAdjustRange(np) { + m.UpdateGraphSizes() + } + bGrid.Set(bGrid.GridPoint(np)) + } + + // get all rune patterns for braille grid and draw them on to the canvas + startX := 0 + if m.yStep > 0 { + startX = m.origin.X + 1 + } + patterns := bGrid.BraillePatterns() + graph.DrawBraillePatterns(&m.Canvas, canvas.Point{X: startX, Y: 0}, patterns, s) +} + +// Focused returns whether canvas is being focused. +func (m *Model) Focused() bool { + return m.focus +} + +// Focus enables Update events processing. +func (m *Model) Focus() { + m.focus = true +} + +// Blur disables Update events processing. +func (m *Model) Blur() { + m.focus = false +} + +// Init initializes the linechart. +func (m Model) Init() tea.Cmd { + return m.Canvas.Init() +} + +// Update processes bubbletea Msg to by invoking +// UpdateHandlerFunc callback if linechart is focused. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + if !m.focus { + return m, nil + } + m.UpdateHandler(&m, msg) + return m, nil +} + +// View returns a string used by the bubbletea framework to display the linechart. +func (m Model) View() (r string) { + r = m.Canvas.View() + if m.zoneManager != nil { + r = m.zoneManager.Mark(m.zoneID, r) + } + return +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/linechart/options.go b/vendor/github.com/NimbleMarkets/ntcharts/linechart/options.go new file mode 100644 index 00000000..648481d2 --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/linechart/options.go @@ -0,0 +1,103 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +package linechart + +// File contains options used by the linechart during initialization with New(). + +import ( + "github.com/NimbleMarkets/ntcharts/canvas" + + "github.com/charmbracelet/lipgloss" + zone "github.com/lrstanley/bubblezone" +) + +// Option is used to set options when initializing a linechart. Example: +// +// lc := New(width, height, minX, maxX, minY, maxY, WithZoneManager(someZoneManager)) +type Option func(*Model) + +// WithStyles sets the default style of the axes, the value shown for the axes +// and runes drawn on linechart. +func WithStyles(as lipgloss.Style, ls lipgloss.Style, s lipgloss.Style) Option { + return func(m *Model) { + m.AxisStyle = as + m.LabelStyle = ls + m.Style = s + } +} + +// WithXLabelFormatter sets the default X label formatter for displaying X values as strings. +func WithXLabelFormatter(fmter LabelFormatter) Option { + return func(m *Model) { + m.XLabelFormatter = fmter + } +} + +// WithYLabelFormatter sets the default Y label formatter for displaying Y values as strings. +func WithYLabelFormatter(fmter LabelFormatter) Option { + return func(m *Model) { + m.YLabelFormatter = fmter + } +} + +// / WithKeyMap sets the KeyMap used +// when processing keyboard event messages in Update(). +func WithKeyMap(k canvas.KeyMap) Option { + return func(m *Model) { + m.Canvas.KeyMap = k + } +} + +// WithUpdateHandler sets the UpdateHandler used +// when processing bubbletea Msg events in Update(). +func WithUpdateHandler(h UpdateHandler) Option { + return func(m *Model) { + m.UpdateHandler = h + } +} + +// WithZoneManager sets the bubblezone Manager used +// when processing bubbletea Msg mouse events in Update(). +func WithZoneManager(zm *zone.Manager) Option { + return func(m *Model) { + m.SetZoneManager(zm) + } +} + +// WithXYSteps sets the number of steps when drawing +// X and Y axes values. +func WithXYSteps(x, y int) Option { + return func(m *Model) { + m.xStep = x + m.yStep = y + } +} + +// WithAutoXYRange enables automatically setting the minimum and maximum +// expected X and Y values if new data values are beyond the current ranges. +func WithAutoXYRange() Option { + return func(m *Model) { + m.AutoMinX = true + m.AutoMaxX = true + m.AutoMinY = true + m.AutoMaxY = true + } +} + +// WithAutoXRange enables automatically setting the minimum and maximum +// expected X values if new data values are beyond the current range. +func WithAutoXRange() Option { + return func(m *Model) { + m.AutoMinX = true + m.AutoMaxX = true + } +} + +// WithAutoYRange enables automatically setting the minimum and maximum +// expected Y values if new data values are beyond the current range. +func WithAutoYRange() Option { + return func(m *Model) { + m.AutoMinY = true + m.AutoMaxY = true + } +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/options.go b/vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/options.go new file mode 100644 index 00000000..c68b1cc6 --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/options.go @@ -0,0 +1,139 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +package timeserieslinechart + +// File contains options used by the timeserieslinechart during initialization with New(). + +import ( + "time" + + "github.com/NimbleMarkets/ntcharts/canvas/runes" + "github.com/NimbleMarkets/ntcharts/linechart" + + "github.com/charmbracelet/lipgloss" + zone "github.com/lrstanley/bubblezone" +) + +// Option is used to set options when initializing a timeserieslinechart. Example: +// +// tslc := New(width, height, minX, maxX, minY, maxY, WithStyles(someLineStyle, someLipglossStyle)) +type Option func(*Model) + +// WithLineChart sets internal linechart to given linechart. +func WithLineChart(lc *linechart.Model) Option { + return func(m *Model) { + m.Model = *lc + } +} + +// WithUpdateHandler sets the UpdateHandler used +// when processing bubbletea Msg events in Update(). +func WithUpdateHandler(h linechart.UpdateHandler) Option { + return func(m *Model) { + m.UpdateHandler = h + } +} + +// WithZoneManager sets the bubblezone Manager used +// when processing bubbletea Msg mouse events in Update(). +func WithZoneManager(zm *zone.Manager) Option { + return func(m *Model) { + m.SetZoneManager(zm) + } +} + +// WithXLabelFormatter sets the default X label formatter for displaying X values as strings. +func WithXLabelFormatter(fmter linechart.LabelFormatter) Option { + return func(m *Model) { + m.XLabelFormatter = fmter + } +} + +// WithYLabelFormatter sets the default Y label formatter for displaying Y values as strings. +func WithYLabelFormatter(fmter linechart.LabelFormatter) Option { + return func(m *Model) { + m.YLabelFormatter = fmter + } +} + +// WithAxesStyles sets the axes line and line label styles. +func WithAxesStyles(as lipgloss.Style, ls lipgloss.Style) Option { + return func(m *Model) { + m.AxisStyle = as + m.LabelStyle = ls + } +} + +// WithXYSteps sets the number of steps when drawing X and Y axes values. +// If X steps 0, then X axis will be hidden. +// If Y steps 0, then Y axis will be hidden. +func WithXYSteps(x, y int) Option { + return func(m *Model) { + m.SetXStep(x) + m.SetYStep(y) + } +} + +// WithYRange sets expected and displayed +// minimum and maximum Y value range. +func WithYRange(min, max float64) Option { + return func(m *Model) { + m.SetYRange(min, max) + m.SetViewYRange(min, max) + } +} + +// WithTimeRange sets expected and displayed minimun and maximum +// time values range on the X axis. +func WithTimeRange(min, max time.Time) Option { + return func(m *Model) { + m.SetTimeRange(min, max) + m.SetViewTimeRange(min, max) + } +} + +// WithLineStyle sets the default line style of data sets. +func WithLineStyle(ls runes.LineStyle) Option { + return func(m *Model) { + m.SetLineStyle(ls) + } +} + +// WithDataSetLineStyle sets the line style of the data set given by name. +func WithDataSetLineStyle(n string, ls runes.LineStyle) Option { + return func(m *Model) { + m.SetDataSetLineStyle(n, ls) + } +} + +// WithStyle sets the default lipgloss style of data sets. +func WithStyle(s lipgloss.Style) Option { + return func(m *Model) { + m.SetStyle(s) + } +} + +// WithDataSetStyle sets the lipgloss style of the data set given by name. +func WithDataSetStyle(n string, s lipgloss.Style) Option { + return func(m *Model) { + m.SetDataSetStyle(n, s) + } +} + +// WithTimeSeries adds []TimePoint values to the default data set. +func WithTimeSeries(p []TimePoint) Option { + return func(m *Model) { + for _, v := range p { + m.Push(v) + } + } +} + +// WithDataSetTimeSeries adds []TimePoint data points to the data set given by name. +func WithDataSetTimeSeries(n string, p []TimePoint) Option { + return func(m *Model) { + for _, v := range p { + m.PushDataSet(n, v) + } + } +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/timeserieslinechart.go b/vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/timeserieslinechart.go new file mode 100644 index 00000000..f81b4911 --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/timeserieslinechart.go @@ -0,0 +1,450 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +// Package timeserieslinechart implements a linechart that draws lines +// for time series data points +package timeserieslinechart + +// https://en.wikipedia.org/wiki/Moving_average + +import ( + "fmt" + "math" + "sort" + "time" + + "github.com/NimbleMarkets/ntcharts/canvas" + "github.com/NimbleMarkets/ntcharts/canvas/buffer" + "github.com/NimbleMarkets/ntcharts/canvas/graph" + "github.com/NimbleMarkets/ntcharts/canvas/runes" + "github.com/NimbleMarkets/ntcharts/linechart" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const DefaultDataSetName = "default" + +func DateTimeLabelFormatter() linechart.LabelFormatter { + var yearLabel string + return func(i int, v float64) string { + if i == 0 { // reset year labeling if redisplaying values + yearLabel = "" + } + t := time.Unix(int64(v), 0).UTC() + monthDay := t.Format("01/02") + year := t.Format("'06") + if yearLabel != year { // apply year label if first time seeing year + yearLabel = year + return fmt.Sprintf("%s %s", yearLabel, monthDay) + } else { + return monthDay + } + } +} + +func HourTimeLabelFormatter() linechart.LabelFormatter { + return func(i int, v float64) string { + t := time.Unix(int64(v), 0).UTC() + return t.Format("15:04:05") + } +} + +type TimePoint struct { + Time time.Time + Value float64 +} + +// cAverage tracks cumulative average +type cAverage struct { + Avg float64 + Count float64 +} + +// Add adds a float64 to current cumulative average +func (a *cAverage) Add(f float64) float64 { + a.Count += 1 + a.Avg += (f - a.Avg) / a.Count + return a.Avg +} + +type dataSet struct { + LineStyle runes.LineStyle // type of line runes to draw + Style lipgloss.Style + + lastTime time.Time // last seen time value + + // stores TimePoints as FloatPoint64{X:time.Time, Y: value} + // time.Time will be converted to seconds since epoch. + // both time and value will be scaled to fit the graphing area + tBuf *buffer.Float64PointScaleBuffer +} + +// Model contains state of a timeserieslinechart with an embedded linechart.Model +// The X axis contains time.Time values and the Y axis contains float64 values. +// A data set consists of a sequence TimePoints in chronological order. +// If multiple TimePoints map to the same column, then average value the time points +// will be used as the Y value of the column. +// The X axis contains a time range and the Y axis contains a numeric value range. +// Uses linechart Model UpdateHandler() for processing keyboard and mouse messages. +type Model struct { + linechart.Model + dLineStyle runes.LineStyle // default data set LineStyletype + dStyle lipgloss.Style // default data set Style + dSets map[string]*dataSet // maps names to data sets +} + +// New returns a timeserieslinechart Model initialized from +// width, height, Y value range and various options. +// By default, the chart will set time.Now() as the minimum time, +// enable auto set X and Y value ranges, +// and only allow moving viewport on X axis. +func New(w, h int, opts ...Option) Model { + min := time.Now() + max := min.Add(time.Second) + m := Model{ + Model: linechart.New(w, h, float64(min.Unix()), float64(max.Unix()), 0, 1, + linechart.WithXYSteps(4, 2), + linechart.WithXLabelFormatter(DateTimeLabelFormatter()), + linechart.WithAutoXYRange(), // automatically adjust value ranges + linechart.WithUpdateHandler(DateUpdateHandler(1))), // only scroll on X axis, increments by 1 day + dLineStyle: runes.ArcLineStyle, + dStyle: lipgloss.NewStyle(), + dSets: make(map[string]*dataSet), + } + for _, opt := range opts { + opt(&m) + } + m.UpdateGraphSizes() + m.rescaleData() + if _, ok := m.dSets[DefaultDataSetName]; !ok { + m.dSets[DefaultDataSetName] = m.newDataSet() + } + return m +} + +// newDataSet returns a new initialize *dataSet. +func (m *Model) newDataSet() *dataSet { + xs := float64(m.GraphWidth()) / (m.ViewMaxX() - m.ViewMinX()) // x scale factor + ys := float64(m.Origin().Y) / (m.ViewMaxY() - m.ViewMinY()) // y scale factor + offset := canvas.Float64Point{X: m.ViewMinX(), Y: m.ViewMinY()} + scale := canvas.Float64Point{X: xs, Y: ys} + return &dataSet{ + LineStyle: m.dLineStyle, + Style: m.dStyle, + tBuf: buffer.NewFloat64PointScaleBuffer(offset, scale), + } +} + +// rescaleData will reinitialize time chunks and +// map time points into graph columns for display +func (m *Model) rescaleData() { + // rescale time points buffer + xs := float64(m.GraphWidth()) / (m.ViewMaxX() - m.ViewMinX()) // x scale factor + ys := float64(m.Origin().Y) / (m.ViewMaxY() - m.ViewMinY()) // y scale factor + offset := canvas.Float64Point{X: m.ViewMinX(), Y: m.ViewMinY()} + scale := canvas.Float64Point{X: xs, Y: ys} + for _, ds := range m.dSets { + if ds.tBuf.Offset() != offset { + ds.tBuf.SetOffset(offset) + } + if ds.tBuf.Scale() != scale { + ds.tBuf.SetScale(scale) + } + } +} + +// ClearAllData will reset stored data values in all data sets. +func (m *Model) ClearAllData() { + for _, ds := range m.dSets { + ds.tBuf.Clear() + } + m.dSets[DefaultDataSetName] = m.newDataSet() +} + +// ClearDataSet will erase stored data set given by name string. +func (m *Model) ClearDataSet(n string) { + if ds, ok := m.dSets[n]; ok { + ds.tBuf.Clear() + } +} + +// SetTimeRange updates the minimum and maximum expected time values. +// Existing data will be rescaled. +func (m *Model) SetTimeRange(min, max time.Time) { + m.Model.SetXRange(float64(min.Unix()), float64(max.Unix())) + m.rescaleData() +} + +// SetYRange updates the minimum and maximum expected Y values. +// Existing data will be rescaled. +func (m *Model) SetYRange(min, max float64) { + m.Model.SetYRange(min, max) + m.rescaleData() +} + +// SetViewTimeRange updates the displayed minimum and maximum time values. +// Existing data will be rescaled. +func (m *Model) SetViewTimeRange(min, max time.Time) { + m.Model.SetViewXRange(float64(min.Unix()), float64(max.Unix())) + m.rescaleData() +} + +// SetViewYRange updates the displayed minimum and maximum Y values. +// Existing data will be rescaled. +func (m *Model) SetViewYRange(min, max float64) { + m.Model.SetViewYRange(min, max) + m.rescaleData() +} + +// SetViewTimeAndYRange updates the displayed minimum and maximum time and Y values. +// Existing data will be rescaled. +func (m *Model) SetViewTimeAndYRange(minX, maxX time.Time, minY, maxY float64) { + m.Model.SetViewXRange(float64(minX.Unix()), float64(maxX.Unix())) + m.Model.SetViewYRange(minY, maxY) + m.rescaleData() +} + +// Resize will change timeserieslinechart display width and height. +// Existing data will be rescaled. +func (m *Model) Resize(w, h int) { + m.Model.Resize(w, h) + m.rescaleData() +} + +// SetLineStyle will set the default line styles of data sets. +func (m *Model) SetLineStyle(ls runes.LineStyle) { + m.dLineStyle = ls + m.SetDataSetLineStyle(DefaultDataSetName, ls) +} + +// SetStyle will set the default lipgloss styles of data sets. +func (m *Model) SetStyle(s lipgloss.Style) { + m.dStyle = s + m.SetDataSetStyle(DefaultDataSetName, s) +} + +// SetDataSetLineStyle will set the line style of the given data set by name string. +func (m *Model) SetDataSetLineStyle(n string, ls runes.LineStyle) { + if _, ok := m.dSets[n]; !ok { + m.dSets[n] = m.newDataSet() + } + ds := m.dSets[n] + ds.LineStyle = ls +} + +// SetDataSetStyle will set the lipgloss style of the given data set by name string. +func (m *Model) SetDataSetStyle(n string, s lipgloss.Style) { + if _, ok := m.dSets[n]; !ok { + m.dSets[n] = m.newDataSet() + } + ds := m.dSets[n] + ds.Style = s +} + +// Push will push a TimePoint data value to the default data set +// to be displayed with Draw. +func (m *Model) Push(t TimePoint) { + m.PushDataSet(DefaultDataSetName, t) +} + +// Push will push a TimePoint data value to a data set +// to be displayed with Draw. Using given data set by name string. +func (m *Model) PushDataSet(n string, t TimePoint) { + f := canvas.Float64Point{X: float64(t.Time.Unix()), Y: t.Value} + // auto adjust x and y ranges if enabled + if m.AutoAdjustRange(f) { + m.UpdateGraphSizes() + m.rescaleData() + } + if _, ok := m.dSets[n]; !ok { + m.dSets[n] = m.newDataSet() + } + ds := m.dSets[n] + ds.tBuf.Push(f) +} + +// Draw will draw lines runes displayed from right to left +// of the graphing area of the canvas. Uses default data set. +func (m *Model) Draw() { + m.DrawDataSets([]string{DefaultDataSetName}) +} + +// DrawAll will draw lines runes for all data sets +// from right to left of the graphing area of the canvas. +func (m *Model) DrawAll() { + names := make([]string, 0, len(m.dSets)) + for n, ds := range m.dSets { + if ds.tBuf.Length() > 0 { + names = append(names, n) + } + } + sort.Strings(names) + m.DrawDataSets(names) +} + +// DrawDataSets will draw lines runes from right to left +// of the graphing area of the canvas for each data set given +// by name strings. +func (m *Model) DrawDataSets(names []string) { + if len(names) == 0 { + return + } + m.Clear() + m.DrawXYAxisAndLabel() + for _, n := range names { + if ds, ok := m.dSets[n]; ok { + dataPoints := ds.tBuf.ReadAll() + dataLen := len(dataPoints) + if dataLen == 0 { + return + } + // get sequence of line values for graphing + seqY := m.getLineSequence(dataPoints) + // convert to canvas coordinates and avoid drawing below X axis + yCoords := canvas.CanvasYCoordinates(m.Origin().Y, seqY) + if m.XStep() > 0 { + for i, v := range yCoords { + if v > m.Origin().Y { + yCoords[i] = m.Origin().Y + } + } + } + startX := m.Canvas.Width() - len(yCoords) + graph.DrawLineSequence(&m.Canvas, + (startX == m.Origin().X), + startX, + yCoords, + ds.LineStyle, + ds.Style) + } + } +} + +// DrawBraille will draw braille runes displayed from right to left +// of the graphing area of the canvas. Uses default data set. +func (m *Model) DrawBraille() { + m.DrawBrailleDataSets([]string{DefaultDataSetName}) +} + +// DrawBrailleAll will draw braille runes for all data sets +// from right to left of the graphing area of the canvas. +func (m *Model) DrawBrailleAll() { + names := make([]string, 0, len(m.dSets)) + for n, ds := range m.dSets { + if ds.tBuf.Length() > 0 { + names = append(names, n) + } + } + sort.Strings(names) + m.DrawBrailleDataSets(names) +} + +// DrawBrailleDataSets will draw braille runes from right to left +// of the graphing area of the canvas for each data set given +// by name strings. +func (m *Model) DrawBrailleDataSets(names []string) { + if len(names) == 0 { + return + } + m.Clear() + m.DrawXYAxisAndLabel() + for _, n := range names { + if ds, ok := m.dSets[n]; ok { + dataPoints := ds.tBuf.ReadAll() + dataLen := len(dataPoints) + if dataLen == 0 { + return + } + // draw lines from each point to the next point + bGrid := graph.NewBrailleGrid(m.GraphWidth(), m.GraphHeight(), + 0, float64(m.GraphWidth()), // X values already scaled to graph + 0, float64(m.GraphHeight())) // Y values already scaled to graph + for i := 0; i < dataLen; i++ { + j := i + 1 + if j >= dataLen { + j = i + } + p1 := dataPoints[i] + p2 := dataPoints[j] + // ignore points that will not be displayed + bothBeforeMin := (p1.X < 0 && p2.X < 0) + bothAfterMax := (p1.X > float64(m.GraphWidth()) && p2.X > float64(m.GraphWidth())) + if bothBeforeMin || bothAfterMax { + continue + } + // get braille grid points from two Float64Point data points + gp1 := bGrid.GridPoint(p1) + gp2 := bGrid.GridPoint(p2) + // set all points in the braille grid + // between two points that approximates a line + points := graph.GetLinePoints(gp1, gp2) + for _, p := range points { + bGrid.Set(p) + } + } + + // get all rune patterns for braille grid + // and draw them on to the canvas + startX := 0 + if m.YStep() > 0 { + startX = m.Origin().X + 1 + } + patterns := bGrid.BraillePatterns() + graph.DrawBraillePatterns(&m.Canvas, + canvas.Point{X: startX, Y: 0}, patterns, ds.Style) + } + } +} + +// getLineSequence returns a sequence of Y values +// to draw line runes from a given set of scaled []FloatPoint64. +func (m *Model) getLineSequence(points []canvas.Float64Point) []int { + width := m.Width() - m.Origin().X // line runes can draw on axes + if width <= 0 { + return []int{} + } + dataLen := len(points) + // each index of the bucket corresponds to a graph column. + // each index value is the average of data point values + // that is mapped to that graph column. + buckets := make([]cAverage, width, width) + for i := 0; i < dataLen; i++ { + j := i + 1 + if j >= dataLen { + j = i + } + p1 := canvas.NewPointFromFloat64Point(points[i]) + p2 := canvas.NewPointFromFloat64Point(points[j]) + // ignore points that will not be displayed on the graph + bothBeforeMin := (p1.X < 0 && p2.X < 0) + bothAfterMax := (p1.X > m.GraphWidth() && p2.X > m.GraphWidth()) + if bothBeforeMin || bothAfterMax { + continue + } + // place all points between two points + // that approximates a line into buckets + points := graph.GetLinePoints(p1, p2) + for _, p := range points { + if (p.X >= 0) && (p.X) < width { + buckets[p.X].Add(float64(p.Y)) + } + } + } + // populate sequence of Y values to for drawing + r := make([]int, width, width) + for i, v := range buckets { + r[i] = int(math.Round(v.Avg)) + } + return r +} + +// Update processes bubbletea Msg to by invoking +// UpdateHandlerFunc callback if linechart is focused. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + if !m.Focused() { + return m, nil + } + m.UpdateHandler(&m.Model, msg) + m.rescaleData() + return m, nil +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/updatehandler.go b/vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/updatehandler.go new file mode 100644 index 00000000..0192293f --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/linechart/timeserieslinechart/updatehandler.go @@ -0,0 +1,77 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +package timeserieslinechart + +// File contains methods and objects used during +// timeserieslinechart Model Update() to modify internal state. +// timeserieslinechart is able to zoom in and out of the graph, +// and increase and decrease the X values to simulating moving +// the viewport of the linechart + +import ( + "github.com/NimbleMarkets/ntcharts/linechart" +) + +// DateUpdateHandler is used by timeserieslinechart to enable +// zooming in and out with the mouse wheel or page up and page down, +// moving the viewing window by holding down mouse button and moving, +// and moving the viewing window with the arrow keys. +// There is only movement along the X axis by day increments. +// Uses linechart Canvas Keymap for keyboard messages. +func DateUpdateHandler(i int) linechart.UpdateHandler { + daySeconds := 86400 * i // number of seconds in a day + return linechart.XAxisUpdateHandler(float64(daySeconds)) +} + +// DateNoZoomUpdateHandler is used by timeserieslinechart to enable +// moving the viewing window by using the mouse scroll wheel, +// holding down mouse button and moving, +// and moving the viewing window with the arrow keys. +// There is only movement along the X axis by day increments. +// Uses linechart Canvas Keymap for keyboard messages. +func DateNoZoomUpdateHandler(i int) linechart.UpdateHandler { + daySeconds := 86400 * i // number of seconds in a day + return linechart.XAxisNoZoomUpdateHandler(float64(daySeconds)) +} + +// HourUpdateHandler is used by timeserieslinechart to enable +// zooming in and out with the mouse wheel or page up and page down, +// moving the viewing window by holding down mouse button and moving, +// and moving the viewing window with the arrow keys. +// There is only movement along the X axis by hour increments. +// Uses linechart Canvas Keymap for keyboard messages. +func HourUpdateHandler(i int) linechart.UpdateHandler { + hourSeconds := 3600 * i // number of seconds in a hour + return linechart.XAxisUpdateHandler(float64(hourSeconds)) +} + +// HourNoZoomUpdateHandler is used by timeserieslinechart to enable +// moving the viewing window by using the mouse scroll wheel, +// holding down mouse button and moving, +// and moving the viewing window with the arrow keys. +// There is only movement along the X axis by hour increments. +// Uses linechart Canvas Keymap for keyboard messages. +func HourNoZoomUpdateHandler(i int) linechart.UpdateHandler { + hourSeconds := 3600 * i // number of seconds in a hour + return linechart.XAxisNoZoomUpdateHandler(float64(hourSeconds)) +} + +// SecondUpdateHandler is used by timeserieslinechart to enable +// zooming in and out with the mouse wheel or page up and page down, +// moving the viewing window by holding down mouse button and moving, +// and moving the viewing window with the arrow keys. +// There is only movement along the X axis by second increments. +// Uses linechart Canvas Keymap for keyboard messages. +func SecondUpdateHandler(i int) linechart.UpdateHandler { + return linechart.XAxisUpdateHandler(float64(i)) +} + +// SecondNoZoomUpdateHandler is used by timeserieslinechart to enable +// moving the viewing window by using the mouse scroll wheel, +// holding down mouse button and moving, +// and moving the viewing window with the arrow keys. +// There is only movement along the X axis by second increments. +// Uses linechart Canvas Keymap for keyboard messages. +func SecondNoZoomUpdateHandler(i int) linechart.UpdateHandler { + return linechart.XAxisNoZoomUpdateHandler(float64(i)) +} diff --git a/vendor/github.com/NimbleMarkets/ntcharts/linechart/updatehandler.go b/vendor/github.com/NimbleMarkets/ntcharts/linechart/updatehandler.go new file mode 100644 index 00000000..974af8e4 --- /dev/null +++ b/vendor/github.com/NimbleMarkets/ntcharts/linechart/updatehandler.go @@ -0,0 +1,361 @@ +// ntcharts - Copyright (c) 2024 Neomantra Corp. + +package linechart + +// File contains methods and objects used during linechart Model Update() +// to modify internal state. +// linechart is able to zoom in and out of the graph, +// and increase and decrease the X and Y values to simulating moving +// the viewport of the linechart + +import ( + "github.com/NimbleMarkets/ntcharts/canvas" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +// UpdateHandler callback invoked during an Update() +// and passes in the linechart Model and bubbletea Msg. +type UpdateHandler func(*Model, tea.Msg) + +// XYAxesUpdateHandler is used by linechart to enable +// zooming in and out with the mouse wheel or page up and page down, +// moving the viewing window by holding down mouse button and moving, +// and moving the viewing window with the arrow keys. +// Uses linechart Canvas Keymap for keyboard messages. +func XYAxesUpdateHandler(xIncrement, yIncrement float64) UpdateHandler { + var lastPos canvas.Point + return func(m *Model, tm tea.Msg) { + switch msg := tm.(type) { + case tea.KeyMsg: + keyXYHandler(m, msg, xIncrement, yIncrement) + keyXYZoomHandler(m, msg, xIncrement, yIncrement) + case tea.MouseMsg: + switch msg.Button { + case tea.MouseButtonWheelUp: + // zoom in limited values cannot cross + m.ZoomIn(xIncrement, yIncrement) + case tea.MouseButtonWheelDown: + // zoom out limited by max values + m.ZoomOut(xIncrement, yIncrement) + } + mouseActionXYHandler(m, msg, &lastPos, xIncrement, yIncrement) + } + } +} + +// XAxisUpdateHandler is used by linechart to enable +// zooming in and out with the mouse wheel or page up and page down, +// moving the viewing window by holding down mouse button and moving, +// and moving the viewing window with the arrow keys. +// There is only movement along the X axis with the given increment. +// Uses linechart Canvas Keymap for keyboard messages. +func XAxisUpdateHandler(increment float64) UpdateHandler { + var lastPos canvas.Point + return func(m *Model, tm tea.Msg) { + switch msg := tm.(type) { + case tea.KeyMsg: + keyXHandler(m, msg, increment) + keyXZoomHandler(m, msg, increment) + case tea.MouseMsg: + switch msg.Button { + case tea.MouseButtonWheelUp: + // zoom in limited values cannot cross + m.ZoomIn(increment, 0) + case tea.MouseButtonWheelDown: + // zoom out limited by max values + m.ZoomOut(increment, 0) + } + mouseActionXHandler(m, msg, &lastPos, increment) + } + } +} + +// XAxisNoZoomUpdateHandler is used by linechart to enable +// moving the viewing window along the X axis with mouse wheel, +// holding down the mouse button and moving, and with arrow keys. +// There is only movement along the X axis with the given increment. +// Uses linechart Canvas Keymap for keyboard messages. +func XAxisNoZoomUpdateHandler(increment float64) UpdateHandler { + var lastPos canvas.Point + return func(m *Model, tm tea.Msg) { + switch msg := tm.(type) { + case tea.KeyMsg: + keyXHandler(m, msg, increment) + case tea.MouseMsg: + switch msg.Button { + case tea.MouseButtonWheelUp, tea.MouseButtonWheelLeft: + m.MoveLeft(increment) + case tea.MouseButtonWheelDown, tea.MouseButtonWheelRight: + m.MoveRight(increment) + } + mouseActionXHandler(m, msg, &lastPos, increment) + } + } +} + +// YAxisUpdateHandler is used by steamlinechart to enable +// zooming in and out with the mouse wheel or page up and page down, +// moving the viewing window by holding down mouse button and moving, +// and moving the viewing window with the arrow keys. +// There is only movement along the Y axis with the given increment. +// Uses linechart Canvas Keymap for keyboard messages. +func YAxisUpdateHandler(increment float64) UpdateHandler { + var lastPos canvas.Point + return func(m *Model, tm tea.Msg) { + switch msg := tm.(type) { + case tea.KeyMsg: + keyYHandler(m, msg, increment) + keyYZoomHandler(m, msg, increment) + case tea.MouseMsg: + switch msg.Button { + case tea.MouseButtonWheelUp: + // zoom in limited values cannot cross + m.ZoomIn(0, increment) + case tea.MouseButtonWheelDown: + // zoom out limited by max values + m.ZoomOut(0, increment) + } + mouseActionYHandler(m, msg, &lastPos, increment) + } + } +} + +// YAxisNoZoomUpdateHandler is used by steamlinechart to enable +// moving the viewing window along the Y axis with mouse wheel, +// holding down the mouse button and moving, and with arrow keys. +// There is only movement along the Y axis with the given increment. +// Uses linechart Canvas Keymap for keyboard messages. +func YAxisNoZoomUpdateHandler(increment float64) UpdateHandler { + var lastPos canvas.Point + return func(m *Model, tm tea.Msg) { + switch msg := tm.(type) { + case tea.KeyMsg: + keyYHandler(m, msg, increment) + case tea.MouseMsg: + switch msg.Button { + case tea.MouseButtonWheelUp: + m.MoveUp(increment) + case tea.MouseButtonWheelDown: + m.MoveDown(increment) + } + mouseActionYHandler(m, msg, &lastPos, increment) + } + } +} + +// ZoomIn will update display X and Y values to simulate +// zooming into the linechart by given increments. +func (m *Model) ZoomIn(x, y float64) { + m.SetViewXYRange( + m.viewMinX+x, + m.viewMaxX-x, + m.viewMinY+y, + m.viewMaxY-y, + ) +} + +// ZoomOut will update display X and Y values to simulate +// zooming into the linechart by given increments. +func (m *Model) ZoomOut(x, y float64) { + m.SetViewXYRange( + m.viewMinX-x, + m.viewMaxX+x, + m.viewMinY-y, + m.viewMaxY+y, + ) +} + +// MoveLeft will update display Y values to simulate +// moving left on the linechart by given increment +func (m *Model) MoveLeft(i float64) { + if (m.viewMinX - i) >= m.MinX() { + m.SetViewXRange(m.viewMinX-i, m.viewMaxX-i) + } else { + i = m.viewMinX - m.MinX() + m.SetViewXRange(m.viewMinX-i, m.viewMaxX-i) + } +} + +// MoveRight will update display Y values to simulate +// moving right on the linechart by given increment. +func (m *Model) MoveRight(i float64) { + if (m.viewMaxX + i) <= m.MaxX() { + m.SetViewXRange(m.viewMinX+i, m.viewMaxX+i) + } else { + i = m.MaxX() - m.viewMaxX + m.SetViewXRange(m.viewMinX+i, m.viewMaxX+i) + } +} + +// MoveUp will update display X values to simulate +// moving up on the linechart chart by given increment. +func (m *Model) MoveUp(i float64) { + if (m.viewMaxY + i) <= m.MaxY() { + m.SetViewYRange(m.viewMinY+i, m.viewMaxY+i) + } else { + i = m.MaxY() - m.viewMaxY + m.SetViewYRange(m.viewMinY+i, m.viewMaxY+i) + } +} + +// MoveDown will update display Y values to simulate +// moving down on the linechart chart by given increment. +func (m *Model) MoveDown(i float64) { + if (m.viewMinY - i) >= m.MinY() { + m.SetViewYRange(m.viewMinY-i, m.viewMaxY-i) + } else { + i = m.viewMinY - m.MinY() + m.SetViewYRange(m.viewMinY-i, m.viewMaxY-i) + } + +} + +// keyXYHandler handles keyboard messages for X and Y axis moving +func keyXYHandler(m *Model, msg tea.KeyMsg, xIncrement, yIncrement float64) { + switch { + case key.Matches(msg, m.Canvas.KeyMap.Up): + m.MoveUp(yIncrement) + case key.Matches(msg, m.Canvas.KeyMap.Down): + m.MoveDown(yIncrement) + case key.Matches(msg, m.Canvas.KeyMap.Left): + m.MoveLeft(xIncrement) + case key.Matches(msg, m.Canvas.KeyMap.Right): + m.MoveRight(xIncrement) + } +} + +// keyXHandler handles keyboard messages for X axis moving +func keyXHandler(m *Model, msg tea.KeyMsg, xIncrement float64) { + switch { + case key.Matches(msg, m.Canvas.KeyMap.Left): + m.MoveLeft(xIncrement) + case key.Matches(msg, m.Canvas.KeyMap.Right): + m.MoveRight(xIncrement) + } +} + +// keyYHandler handles keyboard messages for Y axis moving +func keyYHandler(m *Model, msg tea.KeyMsg, yIncrement float64) { + switch { + case key.Matches(msg, m.Canvas.KeyMap.Up): + m.MoveUp(yIncrement) + case key.Matches(msg, m.Canvas.KeyMap.Down): + m.MoveDown(yIncrement) + } +} + +// keyXYZoomHandler handles keyboard messages for X and Y axis zooming +func keyXYZoomHandler(m *Model, msg tea.KeyMsg, xIncrement, yIncrement float64) { + switch { + case key.Matches(msg, m.Canvas.KeyMap.PgUp): + m.ZoomIn(xIncrement, yIncrement) + case key.Matches(msg, m.Canvas.KeyMap.PgDown): + m.ZoomOut(xIncrement, yIncrement) + } +} + +// keyXZoomHandler handles keyboard messages for X axis zooming +func keyXZoomHandler(m *Model, msg tea.KeyMsg, xIncrement float64) { + switch { + case key.Matches(msg, m.Canvas.KeyMap.PgUp): + m.ZoomIn(xIncrement, 0) + case key.Matches(msg, m.Canvas.KeyMap.PgDown): + m.ZoomOut(xIncrement, 0) + } +} + +// keyYZoomHandler handles keyboard messages for Y axis zooming +func keyYZoomHandler(m *Model, msg tea.KeyMsg, yIncrement float64) { + switch { + case key.Matches(msg, m.Canvas.KeyMap.PgUp): + m.ZoomIn(0, yIncrement) + case key.Matches(msg, m.Canvas.KeyMap.PgDown): + m.ZoomOut(0, yIncrement) + } +} + +// mouseActionXYHandler handles mouse click messages for X and Y axes +func mouseActionXYHandler(m *Model, msg tea.MouseMsg, lastPos *canvas.Point, xIncrement, yIncrement float64) { + if m.ZoneManager() == nil { + return + } + switch msg.Action { + case tea.MouseActionPress: + zInfo := m.ZoneManager().Get(m.ZoneID()) + if zInfo.InBounds(msg) { + x, y := zInfo.Pos(msg) + *lastPos = canvas.Point{X: x, Y: y} + } + case tea.MouseActionMotion: + zInfo := m.ZoneManager().Get(m.ZoneID()) + if zInfo.InBounds(msg) { + x, y := zInfo.Pos(msg) + if x > lastPos.X { + m.MoveRight(xIncrement) + } else if x < lastPos.X { + m.MoveLeft(xIncrement) + } + if y > lastPos.Y { + m.MoveDown(yIncrement) + } else if y < lastPos.Y { + m.MoveUp(yIncrement) + } + *lastPos = canvas.Point{X: x, Y: y} + } + } +} + +// mouseActionXHandler handles mouse click messages for X axis +func mouseActionXHandler(m *Model, msg tea.MouseMsg, lastPos *canvas.Point, increment float64) { + if m.ZoneManager() == nil { + return + } + switch msg.Action { + case tea.MouseActionPress: + zInfo := m.ZoneManager().Get(m.ZoneID()) + if zInfo.InBounds(msg) { + x, y := zInfo.Pos(msg) + *lastPos = canvas.Point{X: x, Y: y} + } + case tea.MouseActionMotion: + zInfo := m.ZoneManager().Get(m.ZoneID()) + if zInfo.InBounds(msg) { + x, y := zInfo.Pos(msg) + if x > lastPos.X { + m.MoveRight(increment) + } else if x < lastPos.X { + m.MoveLeft(increment) + } + *lastPos = canvas.Point{X: x, Y: y} + } + } + +} + +// mouseActionYHandler handles mouse click messages for Y axis +func mouseActionYHandler(m *Model, msg tea.MouseMsg, lastPos *canvas.Point, increment float64) { + if m.ZoneManager() == nil { + return + } + switch msg.Action { + case tea.MouseActionPress: + zInfo := m.ZoneManager().Get(m.ZoneID()) + if zInfo.InBounds(msg) { + x, y := zInfo.Pos(msg) + *lastPos = canvas.Point{X: x, Y: y} // set position of last click + } + case tea.MouseActionMotion: // event occurs when mouse is pressed + zInfo := m.ZoneManager().Get(m.ZoneID()) + if zInfo.InBounds(msg) { + x, y := zInfo.Pos(msg) + if y > lastPos.Y { + m.MoveDown(increment) + } else if y < lastPos.Y { + m.MoveUp(increment) + } + *lastPos = canvas.Point{X: x, Y: y} // update last mouse position + } + } +} diff --git a/vendor/github.com/atotto/clipboard/.travis.yml b/vendor/github.com/atotto/clipboard/.travis.yml new file mode 100644 index 00000000..23f21d83 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/.travis.yml @@ -0,0 +1,22 @@ +language: go + +os: + - linux + - osx + - windows + +go: + - go1.13.x + - go1.x + +services: + - xvfb + +before_install: + - export DISPLAY=:99.0 + +script: + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xsel; fi + - go test -v . + - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get install xclip; fi + - go test -v . diff --git a/vendor/github.com/atotto/clipboard/LICENSE b/vendor/github.com/atotto/clipboard/LICENSE new file mode 100644 index 00000000..dee3257b --- /dev/null +++ b/vendor/github.com/atotto/clipboard/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013 Ato Araki. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of @atotto. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/atotto/clipboard/README.md b/vendor/github.com/atotto/clipboard/README.md new file mode 100644 index 00000000..41fdd57b --- /dev/null +++ b/vendor/github.com/atotto/clipboard/README.md @@ -0,0 +1,48 @@ +[![Build Status](https://travis-ci.org/atotto/clipboard.svg?branch=master)](https://travis-ci.org/atotto/clipboard) + +[![GoDoc](https://godoc.org/github.com/atotto/clipboard?status.svg)](http://godoc.org/github.com/atotto/clipboard) + +# Clipboard for Go + +Provide copying and pasting to the Clipboard for Go. + +Build: + + $ go get github.com/atotto/clipboard + +Platforms: + +* OSX +* Windows 7 (probably work on other Windows) +* Linux, Unix (requires 'xclip' or 'xsel' command to be installed) + + +Document: + +* http://godoc.org/github.com/atotto/clipboard + +Notes: + +* Text string only +* UTF-8 text encoding only (no conversion) + +TODO: + +* Clipboard watcher(?) + +## Commands: + +paste shell command: + + $ go get github.com/atotto/clipboard/cmd/gopaste + $ # example: + $ gopaste > document.txt + +copy shell command: + + $ go get github.com/atotto/clipboard/cmd/gocopy + $ # example: + $ cat document.txt | gocopy + + + diff --git a/vendor/github.com/atotto/clipboard/clipboard.go b/vendor/github.com/atotto/clipboard/clipboard.go new file mode 100644 index 00000000..d7907d3a --- /dev/null +++ b/vendor/github.com/atotto/clipboard/clipboard.go @@ -0,0 +1,20 @@ +// Copyright 2013 @atotto. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package clipboard read/write on clipboard +package clipboard + +// ReadAll read string from clipboard +func ReadAll() (string, error) { + return readAll() +} + +// WriteAll write string to clipboard +func WriteAll(text string) error { + return writeAll(text) +} + +// Unsupported might be set true during clipboard init, to help callers decide +// whether or not to offer clipboard options. +var Unsupported bool diff --git a/vendor/github.com/atotto/clipboard/clipboard_darwin.go b/vendor/github.com/atotto/clipboard/clipboard_darwin.go new file mode 100644 index 00000000..6f33078d --- /dev/null +++ b/vendor/github.com/atotto/clipboard/clipboard_darwin.go @@ -0,0 +1,52 @@ +// Copyright 2013 @atotto. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin + +package clipboard + +import ( + "os/exec" +) + +var ( + pasteCmdArgs = "pbpaste" + copyCmdArgs = "pbcopy" +) + +func getPasteCommand() *exec.Cmd { + return exec.Command(pasteCmdArgs) +} + +func getCopyCommand() *exec.Cmd { + return exec.Command(copyCmdArgs) +} + +func readAll() (string, error) { + pasteCmd := getPasteCommand() + out, err := pasteCmd.Output() + if err != nil { + return "", err + } + return string(out), nil +} + +func writeAll(text string) error { + copyCmd := getCopyCommand() + in, err := copyCmd.StdinPipe() + if err != nil { + return err + } + + if err := copyCmd.Start(); err != nil { + return err + } + if _, err := in.Write([]byte(text)); err != nil { + return err + } + if err := in.Close(); err != nil { + return err + } + return copyCmd.Wait() +} diff --git a/vendor/github.com/atotto/clipboard/clipboard_plan9.go b/vendor/github.com/atotto/clipboard/clipboard_plan9.go new file mode 100644 index 00000000..9d2fef4e --- /dev/null +++ b/vendor/github.com/atotto/clipboard/clipboard_plan9.go @@ -0,0 +1,42 @@ +// Copyright 2013 @atotto. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build plan9 + +package clipboard + +import ( + "os" + "io/ioutil" +) + +func readAll() (string, error) { + f, err := os.Open("/dev/snarf") + if err != nil { + return "", err + } + defer f.Close() + + str, err := ioutil.ReadAll(f) + if err != nil { + return "", err + } + + return string(str), nil +} + +func writeAll(text string) error { + f, err := os.OpenFile("/dev/snarf", os.O_WRONLY, 0666) + if err != nil { + return err + } + defer f.Close() + + _, err = f.Write([]byte(text)) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/atotto/clipboard/clipboard_unix.go b/vendor/github.com/atotto/clipboard/clipboard_unix.go new file mode 100644 index 00000000..d9f6a561 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/clipboard_unix.go @@ -0,0 +1,149 @@ +// Copyright 2013 @atotto. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build freebsd linux netbsd openbsd solaris dragonfly + +package clipboard + +import ( + "errors" + "os" + "os/exec" +) + +const ( + xsel = "xsel" + xclip = "xclip" + powershellExe = "powershell.exe" + clipExe = "clip.exe" + wlcopy = "wl-copy" + wlpaste = "wl-paste" + termuxClipboardGet = "termux-clipboard-get" + termuxClipboardSet = "termux-clipboard-set" +) + +var ( + Primary bool + trimDos bool + + pasteCmdArgs []string + copyCmdArgs []string + + xselPasteArgs = []string{xsel, "--output", "--clipboard"} + xselCopyArgs = []string{xsel, "--input", "--clipboard"} + + xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"} + xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"} + + powershellExePasteArgs = []string{powershellExe, "Get-Clipboard"} + clipExeCopyArgs = []string{clipExe} + + wlpasteArgs = []string{wlpaste, "--no-newline"} + wlcopyArgs = []string{wlcopy} + + termuxPasteArgs = []string{termuxClipboardGet} + termuxCopyArgs = []string{termuxClipboardSet} + + missingCommands = errors.New("No clipboard utilities available. Please install xsel, xclip, wl-clipboard or Termux:API add-on for termux-clipboard-get/set.") +) + +func init() { + if os.Getenv("WAYLAND_DISPLAY") != "" { + pasteCmdArgs = wlpasteArgs + copyCmdArgs = wlcopyArgs + + if _, err := exec.LookPath(wlcopy); err == nil { + if _, err := exec.LookPath(wlpaste); err == nil { + return + } + } + } + + pasteCmdArgs = xclipPasteArgs + copyCmdArgs = xclipCopyArgs + + if _, err := exec.LookPath(xclip); err == nil { + return + } + + pasteCmdArgs = xselPasteArgs + copyCmdArgs = xselCopyArgs + + if _, err := exec.LookPath(xsel); err == nil { + return + } + + pasteCmdArgs = termuxPasteArgs + copyCmdArgs = termuxCopyArgs + + if _, err := exec.LookPath(termuxClipboardSet); err == nil { + if _, err := exec.LookPath(termuxClipboardGet); err == nil { + return + } + } + + pasteCmdArgs = powershellExePasteArgs + copyCmdArgs = clipExeCopyArgs + trimDos = true + + if _, err := exec.LookPath(clipExe); err == nil { + if _, err := exec.LookPath(powershellExe); err == nil { + return + } + } + + Unsupported = true +} + +func getPasteCommand() *exec.Cmd { + if Primary { + pasteCmdArgs = pasteCmdArgs[:1] + } + return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...) +} + +func getCopyCommand() *exec.Cmd { + if Primary { + copyCmdArgs = copyCmdArgs[:1] + } + return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...) +} + +func readAll() (string, error) { + if Unsupported { + return "", missingCommands + } + pasteCmd := getPasteCommand() + out, err := pasteCmd.Output() + if err != nil { + return "", err + } + result := string(out) + if trimDos && len(result) > 1 { + result = result[:len(result)-2] + } + return result, nil +} + +func writeAll(text string) error { + if Unsupported { + return missingCommands + } + copyCmd := getCopyCommand() + in, err := copyCmd.StdinPipe() + if err != nil { + return err + } + + if err := copyCmd.Start(); err != nil { + return err + } + if _, err := in.Write([]byte(text)); err != nil { + return err + } + if err := in.Close(); err != nil { + return err + } + return copyCmd.Wait() +} diff --git a/vendor/github.com/atotto/clipboard/clipboard_windows.go b/vendor/github.com/atotto/clipboard/clipboard_windows.go new file mode 100644 index 00000000..253bb932 --- /dev/null +++ b/vendor/github.com/atotto/clipboard/clipboard_windows.go @@ -0,0 +1,157 @@ +// Copyright 2013 @atotto. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build windows + +package clipboard + +import ( + "runtime" + "syscall" + "time" + "unsafe" +) + +const ( + cfUnicodetext = 13 + gmemMoveable = 0x0002 +) + +var ( + user32 = syscall.MustLoadDLL("user32") + isClipboardFormatAvailable = user32.MustFindProc("IsClipboardFormatAvailable") + openClipboard = user32.MustFindProc("OpenClipboard") + closeClipboard = user32.MustFindProc("CloseClipboard") + emptyClipboard = user32.MustFindProc("EmptyClipboard") + getClipboardData = user32.MustFindProc("GetClipboardData") + setClipboardData = user32.MustFindProc("SetClipboardData") + + kernel32 = syscall.NewLazyDLL("kernel32") + globalAlloc = kernel32.NewProc("GlobalAlloc") + globalFree = kernel32.NewProc("GlobalFree") + globalLock = kernel32.NewProc("GlobalLock") + globalUnlock = kernel32.NewProc("GlobalUnlock") + lstrcpy = kernel32.NewProc("lstrcpyW") +) + +// waitOpenClipboard opens the clipboard, waiting for up to a second to do so. +func waitOpenClipboard() error { + started := time.Now() + limit := started.Add(time.Second) + var r uintptr + var err error + for time.Now().Before(limit) { + r, _, err = openClipboard.Call(0) + if r != 0 { + return nil + } + time.Sleep(time.Millisecond) + } + return err +} + +func readAll() (string, error) { + // LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution). + // Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + if formatAvailable, _, err := isClipboardFormatAvailable.Call(cfUnicodetext); formatAvailable == 0 { + return "", err + } + err := waitOpenClipboard() + if err != nil { + return "", err + } + + h, _, err := getClipboardData.Call(cfUnicodetext) + if h == 0 { + _, _, _ = closeClipboard.Call() + return "", err + } + + l, _, err := globalLock.Call(h) + if l == 0 { + _, _, _ = closeClipboard.Call() + return "", err + } + + text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:]) + + r, _, err := globalUnlock.Call(h) + if r == 0 { + _, _, _ = closeClipboard.Call() + return "", err + } + + closed, _, err := closeClipboard.Call() + if closed == 0 { + return "", err + } + return text, nil +} + +func writeAll(text string) error { + // LockOSThread ensure that the whole method will keep executing on the same thread from begin to end (it actually locks the goroutine thread attribution). + // Otherwise if the goroutine switch thread during execution (which is a common practice), the OpenClipboard and CloseClipboard will happen on two different threads, and it will result in a clipboard deadlock. + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := waitOpenClipboard() + if err != nil { + return err + } + + r, _, err := emptyClipboard.Call(0) + if r == 0 { + _, _, _ = closeClipboard.Call() + return err + } + + data := syscall.StringToUTF16(text) + + // "If the hMem parameter identifies a memory object, the object must have + // been allocated using the function with the GMEM_MOVEABLE flag." + h, _, err := globalAlloc.Call(gmemMoveable, uintptr(len(data)*int(unsafe.Sizeof(data[0])))) + if h == 0 { + _, _, _ = closeClipboard.Call() + return err + } + defer func() { + if h != 0 { + globalFree.Call(h) + } + }() + + l, _, err := globalLock.Call(h) + if l == 0 { + _, _, _ = closeClipboard.Call() + return err + } + + r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0]))) + if r == 0 { + _, _, _ = closeClipboard.Call() + return err + } + + r, _, err = globalUnlock.Call(h) + if r == 0 { + if err.(syscall.Errno) != 0 { + _, _, _ = closeClipboard.Call() + return err + } + } + + r, _, err = setClipboardData.Call(cfUnicodetext, h) + if r == 0 { + _, _, _ = closeClipboard.Call() + return err + } + h = 0 // suppress deferred cleanup + closed, _, err := closeClipboard.Call() + if closed == 0 { + return err + } + return nil +} diff --git a/vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE b/vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE new file mode 100644 index 00000000..25cec1ed --- /dev/null +++ b/vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Ayman Bagabas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/aymanbagabas/go-osc52/v2/README.md b/vendor/github.com/aymanbagabas/go-osc52/v2/README.md new file mode 100644 index 00000000..4de3a22d --- /dev/null +++ b/vendor/github.com/aymanbagabas/go-osc52/v2/README.md @@ -0,0 +1,83 @@ + +# go-osc52 + +

+ Latest Release + GoDoc +

+ +A Go library to work with the [ANSI OSC52](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) terminal sequence. + +## Usage + +You can use this small library to construct an ANSI OSC52 sequence suitable for +your terminal. + + +### Example + +```go +import ( + "os" + "fmt" + + "github.com/aymanbagabas/go-osc52/v2" +) + +func main() { + s := "Hello World!" + + // Copy `s` to system clipboard + osc52.New(s).WriteTo(os.Stderr) + + // Copy `s` to primary clipboard (X11) + osc52.New(s).Primary().WriteTo(os.Stderr) + + // Query the clipboard + osc52.Query().WriteTo(os.Stderr) + + // Clear system clipboard + osc52.Clear().WriteTo(os.Stderr) + + // Use the fmt.Stringer interface to copy `s` to system clipboard + fmt.Fprint(os.Stderr, osc52.New(s)) + + // Or to primary clipboard + fmt.Fprint(os.Stderr, osc52.New(s).Primary()) +} +``` + +## SSH Example + +You can use this over SSH using [gliderlabs/ssh](https://github.com/gliderlabs/ssh) for instance: + +```go +var sshSession ssh.Session +seq := osc52.New("Hello awesome!") +// Check if term is screen or tmux +pty, _, _ := s.Pty() +if pty.Term == "screen" { + seq = seq.Screen() +} else if isTmux { + seq = seq.Tmux() +} +seq.WriteTo(sshSession.Stderr()) +``` + +## Tmux + +Make sure you have `set-clipboard on` in your config, otherwise, tmux won't +allow your application to access the clipboard [^1]. + +Using the tmux option, `osc52.TmuxMode` or `osc52.New(...).Tmux()`, wraps the +OSC52 sequence in a special tmux DCS sequence and pass it to the outer +terminal. This requires `allow-passthrough on` in your config. +`allow-passthrough` is no longer enabled by default +[since tmux 3.3a](https://github.com/tmux/tmux/issues/3218#issuecomment-1153089282) [^2]. + +[^1]: See [tmux clipboard](https://github.com/tmux/tmux/wiki/Clipboard) +[^2]: [What is allow-passthrough](https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it) + +## Credits + +* [vim-oscyank](https://github.com/ojroques/vim-oscyank) this is heavily inspired by vim-oscyank. diff --git a/vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go b/vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go new file mode 100644 index 00000000..dc758d28 --- /dev/null +++ b/vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go @@ -0,0 +1,305 @@ +// OSC52 is a terminal escape sequence that allows copying text to the clipboard. +// +// The sequence consists of the following: +// +// OSC 52 ; Pc ; Pd BEL +// +// Pc is the clipboard choice: +// +// c: clipboard +// p: primary +// q: secondary (not supported) +// s: select (not supported) +// 0-7: cut-buffers (not supported) +// +// Pd is the data to copy to the clipboard. This string should be encoded in +// base64 (RFC-4648). +// +// If Pd is "?", the terminal replies to the host with the current contents of +// the clipboard. +// +// If Pd is neither a base64 string nor "?", the terminal clears the clipboard. +// +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +// where Ps = 52 => Manipulate Selection Data. +// +// Examples: +// +// // copy "hello world" to the system clipboard +// fmt.Fprint(os.Stderr, osc52.New("hello world")) +// +// // copy "hello world" to the primary Clipboard +// fmt.Fprint(os.Stderr, osc52.New("hello world").Primary()) +// +// // limit the size of the string to copy 10 bytes +// fmt.Fprint(os.Stderr, osc52.New("0123456789").Limit(10)) +// +// // escape the OSC52 sequence for screen using DCS sequences +// fmt.Fprint(os.Stderr, osc52.New("hello world").Screen()) +// +// // escape the OSC52 sequence for Tmux +// fmt.Fprint(os.Stderr, osc52.New("hello world").Tmux()) +// +// // query the system Clipboard +// fmt.Fprint(os.Stderr, osc52.Query()) +// +// // query the primary clipboard +// fmt.Fprint(os.Stderr, osc52.Query().Primary()) +// +// // clear the system Clipboard +// fmt.Fprint(os.Stderr, osc52.Clear()) +// +// // clear the primary Clipboard +// fmt.Fprint(os.Stderr, osc52.Clear().Primary()) +package osc52 + +import ( + "encoding/base64" + "fmt" + "io" + "strings" +) + +// Clipboard is the clipboard buffer to use. +type Clipboard rune + +const ( + // SystemClipboard is the system clipboard buffer. + SystemClipboard Clipboard = 'c' + // PrimaryClipboard is the primary clipboard buffer (X11). + PrimaryClipboard = 'p' +) + +// Mode is the mode to use for the OSC52 sequence. +type Mode uint + +const ( + // DefaultMode is the default OSC52 sequence mode. + DefaultMode Mode = iota + // ScreenMode escapes the OSC52 sequence for screen using DCS sequences. + ScreenMode + // TmuxMode escapes the OSC52 sequence for tmux. Not needed if tmux + // clipboard is set to `set-clipboard on` + TmuxMode +) + +// Operation is the OSC52 operation. +type Operation uint + +const ( + // SetOperation is the copy operation. + SetOperation Operation = iota + // QueryOperation is the query operation. + QueryOperation + // ClearOperation is the clear operation. + ClearOperation +) + +// Sequence is the OSC52 sequence. +type Sequence struct { + str string + limit int + op Operation + mode Mode + clipboard Clipboard +} + +var _ fmt.Stringer = Sequence{} + +var _ io.WriterTo = Sequence{} + +// String returns the OSC52 sequence. +func (s Sequence) String() string { + var seq strings.Builder + // mode escape sequences start + seq.WriteString(s.seqStart()) + // actual OSC52 sequence start + seq.WriteString(fmt.Sprintf("\x1b]52;%c;", s.clipboard)) + switch s.op { + case SetOperation: + str := s.str + if s.limit > 0 && len(str) > s.limit { + return "" + } + b64 := base64.StdEncoding.EncodeToString([]byte(str)) + switch s.mode { + case ScreenMode: + // Screen doesn't support OSC52 but will pass the contents of a DCS + // sequence to the outer terminal unchanged. + // + // Here, we split the encoded string into 76 bytes chunks and then + // join the chunks with sequences. Finally, + // wrap the whole thing in + // . + // s := strings.SplitN(b64, "", 76) + s := make([]string, 0, len(b64)/76+1) + for i := 0; i < len(b64); i += 76 { + end := i + 76 + if end > len(b64) { + end = len(b64) + } + s = append(s, b64[i:end]) + } + seq.WriteString(strings.Join(s, "\x1b\\\x1bP")) + default: + seq.WriteString(b64) + } + case QueryOperation: + // OSC52 queries the clipboard using "?" + seq.WriteString("?") + case ClearOperation: + // OSC52 clears the clipboard if the data is neither a base64 string nor "?" + // we're using "!" as a default + seq.WriteString("!") + } + // actual OSC52 sequence end + seq.WriteString("\x07") + // mode escape end + seq.WriteString(s.seqEnd()) + return seq.String() +} + +// WriteTo writes the OSC52 sequence to the writer. +func (s Sequence) WriteTo(out io.Writer) (int64, error) { + n, err := out.Write([]byte(s.String())) + return int64(n), err +} + +// Mode sets the mode for the OSC52 sequence. +func (s Sequence) Mode(m Mode) Sequence { + s.mode = m + return s +} + +// Tmux sets the mode to TmuxMode. +// Used to escape the OSC52 sequence for `tmux`. +// +// Note: this is not needed if tmux clipboard is set to `set-clipboard on`. If +// TmuxMode is used, tmux must have `allow-passthrough on` set. +// +// This is a syntactic sugar for s.Mode(TmuxMode). +func (s Sequence) Tmux() Sequence { + return s.Mode(TmuxMode) +} + +// Screen sets the mode to ScreenMode. +// Used to escape the OSC52 sequence for `screen`. +// +// This is a syntactic sugar for s.Mode(ScreenMode). +func (s Sequence) Screen() Sequence { + return s.Mode(ScreenMode) +} + +// Clipboard sets the clipboard buffer for the OSC52 sequence. +func (s Sequence) Clipboard(c Clipboard) Sequence { + s.clipboard = c + return s +} + +// Primary sets the clipboard buffer to PrimaryClipboard. +// This is the X11 primary clipboard. +// +// This is a syntactic sugar for s.Clipboard(PrimaryClipboard). +func (s Sequence) Primary() Sequence { + return s.Clipboard(PrimaryClipboard) +} + +// Limit sets the limit for the OSC52 sequence. +// The default limit is 0 (no limit). +// +// Strings longer than the limit get ignored. Settting the limit to 0 or a +// negative value disables the limit. Each terminal defines its own escapse +// sequence limit. +func (s Sequence) Limit(l int) Sequence { + if l < 0 { + s.limit = 0 + } else { + s.limit = l + } + return s +} + +// Operation sets the operation for the OSC52 sequence. +// The default operation is SetOperation. +func (s Sequence) Operation(o Operation) Sequence { + s.op = o + return s +} + +// Clear sets the operation to ClearOperation. +// This clears the clipboard. +// +// This is a syntactic sugar for s.Operation(ClearOperation). +func (s Sequence) Clear() Sequence { + return s.Operation(ClearOperation) +} + +// Query sets the operation to QueryOperation. +// This queries the clipboard contents. +// +// This is a syntactic sugar for s.Operation(QueryOperation). +func (s Sequence) Query() Sequence { + return s.Operation(QueryOperation) +} + +// SetString sets the string for the OSC52 sequence. Strings are joined with a +// space character. +func (s Sequence) SetString(strs ...string) Sequence { + s.str = strings.Join(strs, " ") + return s +} + +// New creates a new OSC52 sequence with the given string(s). Strings are +// joined with a space character. +func New(strs ...string) Sequence { + s := Sequence{ + str: strings.Join(strs, " "), + limit: 0, + mode: DefaultMode, + clipboard: SystemClipboard, + op: SetOperation, + } + return s +} + +// Query creates a new OSC52 sequence with the QueryOperation. +// This returns a new OSC52 sequence to query the clipboard contents. +// +// This is a syntactic sugar for New().Query(). +func Query() Sequence { + return New().Query() +} + +// Clear creates a new OSC52 sequence with the ClearOperation. +// This returns a new OSC52 sequence to clear the clipboard. +// +// This is a syntactic sugar for New().Clear(). +func Clear() Sequence { + return New().Clear() +} + +func (s Sequence) seqStart() string { + switch s.mode { + case TmuxMode: + // Write the start of a tmux escape sequence. + return "\x1bPtmux;\x1b" + case ScreenMode: + // Write the start of a DCS sequence. + return "\x1bP" + default: + return "" + } +} + +func (s Sequence) seqEnd() string { + switch s.mode { + case TmuxMode: + // Terminate the tmux escape sequence. + return "\x1b\\" + case ScreenMode: + // Write the end of a DCS sequence. + return "\x1b\x5c" + default: + return "" + } +} diff --git a/vendor/github.com/bufbuild/connect-go/.gitignore b/vendor/github.com/bufbuild/connect-go/.gitignore deleted file mode 100644 index b300197a..00000000 --- a/vendor/github.com/bufbuild/connect-go/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# Autogenerated by makego. DO NOT EDIT. -/.tmp/ -*.pprof -*.svg -cover.out diff --git a/vendor/github.com/bufbuild/connect-go/Makefile b/vendor/github.com/bufbuild/connect-go/Makefile deleted file mode 100644 index ea1d4c6a..00000000 --- a/vendor/github.com/bufbuild/connect-go/Makefile +++ /dev/null @@ -1,102 +0,0 @@ -# See https://tech.davis-hansson.com/p/make/ -SHELL := bash -.DELETE_ON_ERROR: -.SHELLFLAGS := -eu -o pipefail -c -.DEFAULT_GOAL := all -MAKEFLAGS += --warn-undefined-variables -MAKEFLAGS += --no-builtin-rules -MAKEFLAGS += --no-print-directory -BIN := .tmp/bin -COPYRIGHT_YEARS := 2021-2022 -LICENSE_IGNORE := -e /testdata/ -# Set to use a different compiler. For example, `GO=go1.18rc1 make test`. -GO ?= go - -.PHONY: help -help: ## Describe useful make targets - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-30s %s\n", $$1, $$2}' - -.PHONY: all -all: ## Build, test, and lint (default) - $(MAKE) test - $(MAKE) lint - -.PHONY: clean -clean: ## Delete intermediate build artifacts - @# -X only removes untracked files, -d recurses into directories, -f actually removes files/dirs - git clean -Xdf - -.PHONY: test -test: build ## Run unit tests - $(GO) test -vet=off -race -cover ./... - -.PHONY: build -build: generate ## Build all packages - $(GO) build ./... - -.PHONY: install -install: ## Install all binaries - $(GO) install ./... - -.PHONY: lint -lint: $(BIN)/golangci-lint $(BIN)/buf ## Lint Go and protobuf - test -z "$$($(BIN)/buf format -d . | tee /dev/stderr)" - $(GO) vet ./... - $(BIN)/golangci-lint run - $(BIN)/buf lint - -.PHONY: lintfix -lintfix: $(BIN)/golangci-lint $(BIN)/buf ## Automatically fix some lint errors - $(BIN)/golangci-lint run --fix - $(BIN)/buf format -w . - -.PHONY: generate -generate: $(BIN)/buf $(BIN)/protoc-gen-go $(BIN)/protoc-gen-connect-go $(BIN)/license-header ## Regenerate code and licenses - rm -rf internal/gen - PATH=$(abspath $(BIN)) $(BIN)/buf generate - @# We want to operate on a list of modified and new files, excluding - @# deleted and ignored files. git-ls-files can't do this alone. comm -23 takes - @# two files and prints the union, dropping lines common to both (-3) and - @# those only in the second file (-2). We make one git-ls-files call for - @# the modified, cached, and new (--others) files, and a second for the - @# deleted files. - comm -23 \ - <(git ls-files --cached --modified --others --no-empty-directory --exclude-standard | sort -u | grep -v $(LICENSE_IGNORE) ) \ - <(git ls-files --deleted | sort -u) | \ - xargs $(BIN)/license-header \ - --license-type apache \ - --copyright-holder "Buf Technologies, Inc." \ - --year-range "$(COPYRIGHT_YEARS)" - -.PHONY: upgrade -upgrade: ## Upgrade dependencies - go get -u -t ./... && go mod tidy -v - -.PHONY: checkgenerate -checkgenerate: - @# Used in CI to verify that `make generate` doesn't produce a diff. - test -z "$$(git status --porcelain | tee /dev/stderr)" - -.PHONY: $(BIN)/protoc-gen-connect-go -$(BIN)/protoc-gen-connect-go: - @mkdir -p $(@D) - $(GO) build -o $(@) ./cmd/protoc-gen-connect-go - -$(BIN)/buf: Makefile - @mkdir -p $(@D) - GOBIN=$(abspath $(@D)) $(GO) install github.com/bufbuild/buf/cmd/buf@v1.9.0 - -$(BIN)/license-header: Makefile - @mkdir -p $(@D) - GOBIN=$(abspath $(@D)) $(GO) install \ - github.com/bufbuild/buf/private/pkg/licenseheader/cmd/license-header@v1.9.0 - -$(BIN)/golangci-lint: Makefile - @mkdir -p $(@D) - GOBIN=$(abspath $(@D)) $(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.0 - -$(BIN)/protoc-gen-go: Makefile go.mod - @mkdir -p $(@D) - @# The version of protoc-gen-go is determined by the version in go.mod - GOBIN=$(abspath $(@D)) $(GO) install google.golang.org/protobuf/cmd/protoc-gen-go - diff --git a/vendor/github.com/bufbuild/connect-go/codec.go b/vendor/github.com/bufbuild/connect-go/codec.go deleted file mode 100644 index 5d3f19b3..00000000 --- a/vendor/github.com/bufbuild/connect-go/codec.go +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2021-2022 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package connect - -import ( - "fmt" - - "google.golang.org/protobuf/encoding/protojson" - "google.golang.org/protobuf/proto" -) - -const ( - codecNameProto = "proto" - codecNameJSON = "json" - codecNameJSONCharsetUTF8 = codecNameJSON + "; charset=utf-8" -) - -// Codec marshals structs (typically generated from a schema) to and from bytes. -type Codec interface { - // Name returns the name of the Codec. - // - // This may be used as part of the Content-Type within HTTP. For example, - // with gRPC this is the content subtype, so "application/grpc+proto" will - // map to the Codec with name "proto". - // - // Names must not be empty. - Name() string - // Marshal marshals the given message. - // - // Marshal may expect a specific type of message, and will error if this type - // is not given. - Marshal(any) ([]byte, error) - // Unmarshal unmarshals the given message. - // - // Unmarshal may expect a specific type of message, and will error if this - // type is not given. - Unmarshal([]byte, any) error -} - -type protoBinaryCodec struct{} - -var _ Codec = (*protoBinaryCodec)(nil) - -func (c *protoBinaryCodec) Name() string { return codecNameProto } - -func (c *protoBinaryCodec) Marshal(message any) ([]byte, error) { - protoMessage, ok := message.(proto.Message) - if !ok { - return nil, errNotProto(message) - } - return proto.Marshal(protoMessage) -} - -func (c *protoBinaryCodec) Unmarshal(data []byte, message any) error { - protoMessage, ok := message.(proto.Message) - if !ok { - return errNotProto(message) - } - return proto.Unmarshal(data, protoMessage) -} - -type protoJSONCodec struct { - name string -} - -var _ Codec = (*protoJSONCodec)(nil) - -func (c *protoJSONCodec) Name() string { return c.name } - -func (c *protoJSONCodec) Marshal(message any) ([]byte, error) { - protoMessage, ok := message.(proto.Message) - if !ok { - return nil, errNotProto(message) - } - var options protojson.MarshalOptions - return options.Marshal(protoMessage) -} - -func (c *protoJSONCodec) Unmarshal(binary []byte, message any) error { - protoMessage, ok := message.(proto.Message) - if !ok { - return errNotProto(message) - } - var options protojson.UnmarshalOptions - return options.Unmarshal(binary, protoMessage) -} - -// readOnlyCodecs is a read-only interface to a map of named codecs. -type readOnlyCodecs interface { - // Get gets the Codec with the given name. - Get(string) Codec - // Protobuf gets the user-supplied protobuf codec, falling back to the default - // implementation if necessary. - // - // This is helpful in the gRPC protocol, where the wire protocol requires - // marshaling protobuf structs to binary even if the RPC procedures were - // generated from a different IDL. - Protobuf() Codec - // Names returns a copy of the registered codec names. The returned slice is - // safe for the caller to mutate. - Names() []string -} - -func newReadOnlyCodecs(nameToCodec map[string]Codec) readOnlyCodecs { - return &codecMap{ - nameToCodec: nameToCodec, - } -} - -type codecMap struct { - nameToCodec map[string]Codec -} - -func (m *codecMap) Get(name string) Codec { - return m.nameToCodec[name] -} - -func (m *codecMap) Protobuf() Codec { - if pb, ok := m.nameToCodec[codecNameProto]; ok { - return pb - } - return &protoBinaryCodec{} -} - -func (m *codecMap) Names() []string { - names := make([]string, 0, len(m.nameToCodec)) - for name := range m.nameToCodec { - names = append(names, name) - } - return names -} - -func errNotProto(message any) error { - return fmt.Errorf("%T doesn't implement proto.Message", message) -} diff --git a/vendor/github.com/bufbuild/connect-go/duplex_http_call.go b/vendor/github.com/bufbuild/connect-go/duplex_http_call.go deleted file mode 100644 index c365f34d..00000000 --- a/vendor/github.com/bufbuild/connect-go/duplex_http_call.go +++ /dev/null @@ -1,278 +0,0 @@ -// Copyright 2021-2022 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package connect - -import ( - "context" - "errors" - "fmt" - "io" - "net/http" - "sync" -) - -// duplexHTTPCall is a full-duplex stream between the client and server. The -// request body is the stream from client to server, and the response body is -// the reverse. -// -// Be warned: we need to use some lesser-known APIs to do this with net/http. -type duplexHTTPCall struct { - ctx context.Context - httpClient HTTPClient - streamType StreamType - validateResponse func(*http.Response) *Error - - // We'll use a pipe as the request body. We hand the read side of the pipe to - // net/http, and we write to the write side (naturally). The two ends are - // safe to use concurrently. - requestBodyReader *io.PipeReader - requestBodyWriter *io.PipeWriter - - sendRequestOnce sync.Once - responseReady chan struct{} - request *http.Request - response *http.Response - - errMu sync.Mutex - err error -} - -func newDuplexHTTPCall( - ctx context.Context, - httpClient HTTPClient, - url string, - spec Spec, - header http.Header, -) *duplexHTTPCall { - pipeReader, pipeWriter := io.Pipe() - request, err := http.NewRequestWithContext( - ctx, - http.MethodPost, - url, - pipeReader, - ) - request.Header = header - client := &duplexHTTPCall{ - ctx: ctx, - httpClient: httpClient, - streamType: spec.StreamType, - requestBodyReader: pipeReader, - requestBodyWriter: pipeWriter, - request: request, - responseReady: make(chan struct{}), - } - if err != nil { - // We can't construct a request, so we definitely can't send it over the - // network. Exhaust the sync.Once immediately and short-circuit Read and - // Write by setting an error. - client.sendRequestOnce.Do(func() {}) - connectErr := errorf(CodeUnavailable, "construct *http.Request: %w", err) - client.SetError(connectErr) - } - return client -} - -// Write to the request body. Returns an error wrapping io.EOF after SetError -// is called. -func (d *duplexHTTPCall) Write(data []byte) (int, error) { - d.ensureRequestMade() - // Before we send any data, check if the context has been canceled. - if err := d.ctx.Err(); err != nil { - d.SetError(err) - return 0, wrapIfContextError(err) - } - // It's safe to write to this side of the pipe while net/http concurrently - // reads from the other side. - bytesWritten, err := d.requestBodyWriter.Write(data) - if err != nil && errors.Is(err, io.ErrClosedPipe) { - // Signal that the stream is closed with the more-typical io.EOF instead of - // io.ErrClosedPipe. This makes it easier for protocol-specific wrappers to - // match grpc-go's behavior. - return bytesWritten, io.EOF - } - return bytesWritten, err -} - -// Close the request body. Callers *must* call CloseWrite before Read when -// using HTTP/1.x. -func (d *duplexHTTPCall) CloseWrite() error { - // Even if Write was never called, we need to make an HTTP request. This - // ensures that we've sent any headers to the server and that we have an HTTP - // response to read from. - d.ensureRequestMade() - // The user calls CloseWrite to indicate that they're done sending data. It's - // safe to close the write side of the pipe while net/http is reading from - // it. - // - // Because connect also supports some RPC types over HTTP/1.1, we need to be - // careful how we expose this method to users. HTTP/1.1 doesn't support - // bidirectional streaming - the write side of the stream (aka request body) - // must be closed before we start reading the response or we'll just block - // forever. To make sure users don't have to worry about this, the generated - // code for unary, client streaming, and server streaming RPCs must call - // CloseWrite automatically rather than requiring the user to do it. - return d.requestBodyWriter.Close() -} - -// Header returns the HTTP request headers. -func (d *duplexHTTPCall) Header() http.Header { - return d.request.Header -} - -// Trailer returns the HTTP request trailers. -func (d *duplexHTTPCall) Trailer() http.Header { - return d.request.Trailer -} - -// Read from the response body. Returns the first error passed to SetError. -func (d *duplexHTTPCall) Read(data []byte) (int, error) { - // First, we wait until we've gotten the response headers and established the - // server-to-client side of the stream. - d.BlockUntilResponseReady() - if err := d.getError(); err != nil { - // The stream is already closed or corrupted. - return 0, err - } - // Before we read, check if the context has been canceled. - if err := d.ctx.Err(); err != nil { - d.SetError(err) - return 0, wrapIfContextError(err) - } - if d.response == nil { - return 0, fmt.Errorf("nil response from %v", d.request.URL) - } - n, err := d.response.Body.Read(data) - return n, wrapIfRSTError(err) -} - -func (d *duplexHTTPCall) CloseRead() error { - d.BlockUntilResponseReady() - if d.response == nil { - return nil - } - if err := discard(d.response.Body); err != nil { - return wrapIfRSTError(err) - } - return wrapIfRSTError(d.response.Body.Close()) -} - -// ResponseStatusCode is the response's HTTP status code. -func (d *duplexHTTPCall) ResponseStatusCode() (int, error) { - d.BlockUntilResponseReady() - if d.response == nil { - return 0, fmt.Errorf("nil response from %v", d.request.URL) - } - return d.response.StatusCode, nil -} - -// ResponseHeader returns the response HTTP headers. -func (d *duplexHTTPCall) ResponseHeader() http.Header { - d.BlockUntilResponseReady() - if d.response != nil { - return d.response.Header - } - return make(http.Header) -} - -// ResponseTrailer returns the response HTTP trailers. -func (d *duplexHTTPCall) ResponseTrailer() http.Header { - d.BlockUntilResponseReady() - if d.response != nil { - return d.response.Trailer - } - return make(http.Header) -} - -// SetError stores any error encountered processing the response. All -// subsequent calls to Read return this error, and all subsequent calls to -// Write return an error wrapping io.EOF. It's safe to call concurrently with -// any other method. -func (d *duplexHTTPCall) SetError(err error) { - d.errMu.Lock() - if d.err == nil { - d.err = wrapIfContextError(err) - } - // Closing the read side of the request body pipe acquires an internal lock, - // so we want to scope errMu's usage narrowly and avoid defer. - d.errMu.Unlock() - - // We've already hit an error, so we should stop writing to the request body. - // It's safe to call Close more than once and/or concurrently (calls after - // the first are no-ops), so it's okay for us to call this even though - // net/http sometimes closes the reader too. - // - // It's safe to ignore the returned error here. Under the hood, Close calls - // CloseWithError, which is documented to always return nil. - _ = d.requestBodyReader.Close() -} - -// SetValidateResponse sets the response validation function. The function runs -// in a background goroutine. -func (d *duplexHTTPCall) SetValidateResponse(validate func(*http.Response) *Error) { - d.validateResponse = validate -} - -func (d *duplexHTTPCall) BlockUntilResponseReady() { - <-d.responseReady -} - -func (d *duplexHTTPCall) ensureRequestMade() { - d.sendRequestOnce.Do(func() { - go d.makeRequest() - }) -} - -func (d *duplexHTTPCall) makeRequest() { - // This runs concurrently with Write and CloseWrite. Read and CloseRead wait - // on d.responseReady, so we can't race with them. - defer close(d.responseReady) - - // Once we send a message to the server, they send a message back and - // establish the receive side of the stream. - response, err := d.httpClient.Do(d.request) //nolint:bodyclose - if err != nil { - err = wrapIfContextError(err) - err = wrapIfLikelyH2CNotConfiguredError(d.request, err) - err = wrapIfLikelyWithGRPCNotUsedError(err) - err = wrapIfRSTError(err) - if _, ok := asError(err); !ok { - err = NewError(CodeUnavailable, err) - } - d.SetError(err) - return - } - d.response = response - if err := d.validateResponse(response); err != nil { - d.SetError(err) - return - } - if (d.streamType&StreamTypeBidi) == StreamTypeBidi && response.ProtoMajor < 2 { - // If we somehow dialed an HTTP/1.x server, fail with an explicit message - // rather than returning a more cryptic error later on. - d.SetError(errorf( - CodeUnimplemented, - "response from %v is HTTP/%d.%d: bidi streams require at least HTTP/2", - d.request.URL, - response.ProtoMajor, - response.ProtoMinor, - )) - } -} - -func (d *duplexHTTPCall) getError() error { - d.errMu.Lock() - defer d.errMu.Unlock() - return d.err -} diff --git a/vendor/github.com/bufbuild/connect-go/envelope.go b/vendor/github.com/bufbuild/connect-go/envelope.go deleted file mode 100644 index ecde9570..00000000 --- a/vendor/github.com/bufbuild/connect-go/envelope.go +++ /dev/null @@ -1,273 +0,0 @@ -// Copyright 2021-2022 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package connect - -import ( - "bytes" - "encoding/binary" - "errors" - "io" -) - -// flagEnvelopeCompressed indicates that the data is compressed. It has the -// same meaning in the gRPC-Web, gRPC-HTTP2, and Connect protocols. -const flagEnvelopeCompressed = 0b00000001 - -var errSpecialEnvelope = errorf( - CodeUnknown, - "final message has protocol-specific flags: %w", - // User code checks for end of stream with errors.Is(err, io.EOF). - io.EOF, -) - -// envelope is a block of arbitrary bytes wrapped in gRPC and Connect's framing -// protocol. -// -// Each message is preceded by a 5-byte prefix. The first byte is a uint8 used -// as a set of bitwise flags, and the remainder is a uint32 indicating the -// message length. gRPC and Connect interpret the bitwise flags differently, so -// envelope leaves their interpretation up to the caller. -type envelope struct { - Data *bytes.Buffer - Flags uint8 -} - -func (e *envelope) IsSet(flag uint8) bool { - return e.Flags&flag == flag -} - -type envelopeWriter struct { - writer io.Writer - codec Codec - compressMinBytes int - compressionPool *compressionPool - bufferPool *bufferPool - sendMaxBytes int -} - -func (w *envelopeWriter) Marshal(message any) *Error { - if message == nil { - if _, err := w.writer.Write(nil); err != nil { - if connectErr, ok := asError(err); ok { - return connectErr - } - return NewError(CodeUnknown, err) - } - return nil - } - raw, err := w.codec.Marshal(message) - if err != nil { - return errorf(CodeInternal, "marshal message: %w", err) - } - // We can't avoid allocating the byte slice, so we may as well reuse it once - // we're done with it. - buffer := bytes.NewBuffer(raw) - defer w.bufferPool.Put(buffer) - envelope := &envelope{Data: buffer} - return w.Write(envelope) -} - -// Write writes the enveloped message, compressing as necessary. It doesn't -// retain any references to the supplied envelope or its underlying data. -func (w *envelopeWriter) Write(env *envelope) *Error { - if env.IsSet(flagEnvelopeCompressed) || - w.compressionPool == nil || - env.Data.Len() < w.compressMinBytes { - if w.sendMaxBytes > 0 && env.Data.Len() > w.sendMaxBytes { - return errorf(CodeResourceExhausted, "message size %d exceeds sendMaxBytes %d", env.Data.Len(), w.sendMaxBytes) - } - return w.write(env) - } - data := w.bufferPool.Get() - defer w.bufferPool.Put(data) - if err := w.compressionPool.Compress(data, env.Data); err != nil { - return err - } - if w.sendMaxBytes > 0 && data.Len() > w.sendMaxBytes { - return errorf(CodeResourceExhausted, "compressed message size %d exceeds sendMaxBytes %d", data.Len(), w.sendMaxBytes) - } - return w.write(&envelope{ - Data: data, - Flags: env.Flags | flagEnvelopeCompressed, - }) -} - -func (w *envelopeWriter) write(env *envelope) *Error { - prefix := [5]byte{} - prefix[0] = env.Flags - binary.BigEndian.PutUint32(prefix[1:5], uint32(env.Data.Len())) - if _, err := w.writer.Write(prefix[:]); err != nil { - if connectErr, ok := asError(err); ok { - return connectErr - } - return errorf(CodeUnknown, "write envelope: %w", err) - } - if _, err := io.Copy(w.writer, env.Data); err != nil { - return errorf(CodeUnknown, "write message: %w", err) - } - return nil -} - -type envelopeReader struct { - reader io.Reader - codec Codec - last envelope - compressionPool *compressionPool - bufferPool *bufferPool - readMaxBytes int -} - -func (r *envelopeReader) Unmarshal(message any) *Error { - buffer := r.bufferPool.Get() - defer r.bufferPool.Put(buffer) - - env := &envelope{Data: buffer} - err := r.Read(env) - switch { - case err == nil && - (env.Flags == 0 || env.Flags == flagEnvelopeCompressed) && - env.Data.Len() == 0: - // This is a standard message (because none of the top 7 bits are set) and - // there's no data, so the zero value of the message is correct. - return nil - case err != nil && errors.Is(err, io.EOF): - // The stream has ended. Propagate the EOF to the caller. - return err - case err != nil: - // Something's wrong. - return err - } - - data := env.Data - if data.Len() > 0 && env.IsSet(flagEnvelopeCompressed) { - if r.compressionPool == nil { - return errorf( - CodeInvalidArgument, - "gRPC protocol error: sent compressed message without Grpc-Encoding header", - ) - } - decompressed := r.bufferPool.Get() - defer r.bufferPool.Put(decompressed) - if err := r.compressionPool.Decompress(decompressed, data, int64(r.readMaxBytes)); err != nil { - return err - } - data = decompressed - } - - if env.Flags != 0 && env.Flags != flagEnvelopeCompressed { - // One of the protocol-specific flags are set, so this is the end of the - // stream. Save the message for protocol-specific code to process and - // return a sentinel error. Since we've deferred functions to return env's - // underlying buffer to a pool, we need to keep a copy. - r.last = envelope{ - Data: r.bufferPool.Get(), - Flags: env.Flags, - } - // Don't return last to the pool! We're going to reference the data - // elsewhere. - if _, err := r.last.Data.ReadFrom(data); err != nil { - return errorf(CodeUnknown, "copy final envelope: %w", err) - } - return errSpecialEnvelope - } - - if err := r.codec.Unmarshal(data.Bytes(), message); err != nil { - return errorf(CodeInvalidArgument, "unmarshal into %T: %w", message, err) - } - return nil -} - -func (r *envelopeReader) Read(env *envelope) *Error { - prefixes := [5]byte{} - prefixBytesRead, err := r.reader.Read(prefixes[:]) - - switch { - case (err == nil || errors.Is(err, io.EOF)) && - prefixBytesRead == 5 && - isSizeZeroPrefix(prefixes): - // Successfully read prefix and expect no additional data. - env.Flags = prefixes[0] - return nil - case err != nil && errors.Is(err, io.EOF) && prefixBytesRead == 0: - // The stream ended cleanly. That's expected, but we need to propagate them - // to the user so that they know that the stream has ended. We shouldn't - // add any alarming text about protocol errors, though. - return NewError(CodeUnknown, err) - case err != nil || prefixBytesRead < 5: - // Something else has gone wrong - the stream didn't end cleanly. - if connectErr, ok := asError(err); ok { - return connectErr - } - if maxBytesErr := asMaxBytesError(err, "read 5 byte message prefix"); maxBytesErr != nil { - // We're reading from an http.MaxBytesHandler, and we've exceeded the read limit. - return maxBytesErr - } - return errorf( - CodeInvalidArgument, - "protocol error: incomplete envelope: %w", err, - ) - } - size := int(binary.BigEndian.Uint32(prefixes[1:5])) - if size < 0 { - return errorf(CodeInvalidArgument, "message size %d overflowed uint32", size) - } - if r.readMaxBytes > 0 && size > r.readMaxBytes { - _, err := io.CopyN(io.Discard, r.reader, int64(size)) - if err != nil && !errors.Is(err, io.EOF) { - return errorf(CodeUnknown, "read enveloped message: %w", err) - } - return errorf(CodeResourceExhausted, "message size %d is larger than configured max %d", size, r.readMaxBytes) - } - if size > 0 { - env.Data.Grow(size) - // At layer 7, we don't know exactly what's happening down in L4. Large - // length-prefixed messages may arrive in chunks, so we may need to read - // the request body past EOF. We also need to take care that we don't retry - // forever if the message is malformed. - remaining := int64(size) - for remaining > 0 { - bytesRead, err := io.CopyN(env.Data, r.reader, remaining) - if err != nil && !errors.Is(err, io.EOF) { - if maxBytesErr := asMaxBytesError(err, "read %d byte message", size); maxBytesErr != nil { - // We're reading from an http.MaxBytesHandler, and we've exceeded the read limit. - return maxBytesErr - } - return errorf(CodeUnknown, "read enveloped message: %w", err) - } - if errors.Is(err, io.EOF) && bytesRead == 0 { - // We've gotten zero-length chunk of data. Message is likely malformed, - // don't wait for additional chunks. - return errorf( - CodeInvalidArgument, - "protocol error: promised %d bytes in enveloped message, got %d bytes", - size, - int64(size)-remaining, - ) - } - remaining -= bytesRead - } - } - env.Flags = prefixes[0] - return nil -} - -func isSizeZeroPrefix(prefix [5]byte) bool { - for i := 1; i < 5; i++ { - if prefix[i] != 0 { - return false - } - } - return true -} diff --git a/vendor/github.com/bufbuild/connect-go/error_writer.go b/vendor/github.com/bufbuild/connect-go/error_writer.go deleted file mode 100644 index 1c62760f..00000000 --- a/vendor/github.com/bufbuild/connect-go/error_writer.go +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2021-2022 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package connect - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" -) - -// An ErrorWriter writes errors to an [http.ResponseWriter] in the format -// expected by an RPC client. This is especially useful in server-side net/http -// middleware, where you may wish to handle requests from RPC and non-RPC -// clients with the same code. -// -// ErrorWriters are safe to use concurrently. -type ErrorWriter struct { - bufferPool *bufferPool - protobuf Codec - allContentTypes map[string]struct{} - grpcContentTypes map[string]struct{} - grpcWebContentTypes map[string]struct{} - unaryConnectContentTypes map[string]struct{} - streamingConnectContentTypes map[string]struct{} -} - -// NewErrorWriter constructs an ErrorWriter. To properly recognize supported -// RPC Content-Types in net/http middleware, you must pass the same -// HandlerOptions to NewErrorWriter and any wrapped Connect handlers. -func NewErrorWriter(opts ...HandlerOption) *ErrorWriter { - config := newHandlerConfig("", opts) - writer := &ErrorWriter{ - bufferPool: config.BufferPool, - protobuf: newReadOnlyCodecs(config.Codecs).Protobuf(), - allContentTypes: make(map[string]struct{}), - grpcContentTypes: make(map[string]struct{}), - grpcWebContentTypes: make(map[string]struct{}), - unaryConnectContentTypes: make(map[string]struct{}), - streamingConnectContentTypes: make(map[string]struct{}), - } - for name := range config.Codecs { - unary := connectContentTypeFromCodecName(StreamTypeUnary, name) - writer.allContentTypes[unary] = struct{}{} - writer.unaryConnectContentTypes[unary] = struct{}{} - streaming := connectContentTypeFromCodecName(StreamTypeBidi, name) - writer.streamingConnectContentTypes[streaming] = struct{}{} - writer.allContentTypes[streaming] = struct{}{} - } - if config.HandleGRPC { - writer.grpcContentTypes[grpcContentTypeDefault] = struct{}{} - writer.allContentTypes[grpcContentTypeDefault] = struct{}{} - for name := range config.Codecs { - ct := grpcContentTypeFromCodecName(false /* web */, name) - writer.grpcContentTypes[ct] = struct{}{} - writer.allContentTypes[ct] = struct{}{} - } - } - if config.HandleGRPCWeb { - writer.grpcWebContentTypes[grpcWebContentTypeDefault] = struct{}{} - writer.allContentTypes[grpcWebContentTypeDefault] = struct{}{} - for name := range config.Codecs { - ct := grpcContentTypeFromCodecName(true /* web */, name) - writer.grpcWebContentTypes[ct] = struct{}{} - writer.allContentTypes[ct] = struct{}{} - } - } - return writer -} - -// IsSupported checks whether a request is using one of the ErrorWriter's -// supported RPC protocols. -func (w *ErrorWriter) IsSupported(request *http.Request) bool { - ctype := canonicalizeContentType(request.Header.Get(headerContentType)) - _, ok := w.allContentTypes[ctype] - return ok -} - -// Write an error, using the format appropriate for the RPC protocol in use. -// Callers should first use IsSupported to verify that the request is using one -// of the ErrorWriter's supported RPC protocols. -// -// Write does not read or close the request body. -func (w *ErrorWriter) Write(response http.ResponseWriter, request *http.Request, err error) error { - ctype := canonicalizeContentType(request.Header.Get(headerContentType)) - if _, ok := w.unaryConnectContentTypes[ctype]; ok { - // Unary errors are always JSON. - response.Header().Set(headerContentType, connectUnaryContentTypeJSON) - return w.writeConnectUnary(response, err) - } - if _, ok := w.streamingConnectContentTypes[ctype]; ok { - response.Header().Set(headerContentType, ctype) - return w.writeConnectStreaming(response, err) - } - if _, ok := w.grpcContentTypes[ctype]; ok { - response.Header().Set(headerContentType, ctype) - return w.writeGRPC(response, err) - } - if _, ok := w.grpcWebContentTypes[ctype]; ok { - response.Header().Set(headerContentType, ctype) - return w.writeGRPCWeb(response, err) - } - return fmt.Errorf("unsupported Content-Type %q", ctype) -} - -func (w *ErrorWriter) writeConnectUnary(response http.ResponseWriter, err error) error { - if connectErr, ok := asError(err); ok { - mergeHeaders(response.Header(), connectErr.meta) - } - response.WriteHeader(connectCodeToHTTP(CodeOf(err))) - data, marshalErr := json.Marshal(newConnectWireError(err)) - if marshalErr != nil { - return fmt.Errorf("marshal error: %w", marshalErr) - } - _, writeErr := response.Write(data) - return writeErr -} - -func (w *ErrorWriter) writeConnectStreaming(response http.ResponseWriter, err error) error { - response.WriteHeader(http.StatusOK) - marshaler := &connectStreamingMarshaler{ - envelopeWriter: envelopeWriter{ - writer: response, - bufferPool: w.bufferPool, - }, - } - // MarshalEndStream returns *Error: check return value to avoid typed nils. - if marshalErr := marshaler.MarshalEndStream(err, make(http.Header)); marshalErr != nil { - return marshalErr - } - return nil -} - -func (w *ErrorWriter) writeGRPC(response http.ResponseWriter, err error) error { - trailers := make(http.Header, 2) // need space for at least code & message - grpcErrorToTrailer(w.bufferPool, trailers, w.protobuf, err) - // To make net/http reliably send trailers without a body, we must set the - // Trailers header rather than using http.TrailerPrefix. See - // https://github.com/golang/go/issues/54723. - keys := make([]string, 0, len(trailers)) - for k := range trailers { - keys = append(keys, k) - } - response.Header().Set("Trailer", strings.Join(keys, ",")) - response.WriteHeader(http.StatusOK) - mergeHeaders(response.Header(), trailers) - return nil -} - -func (w *ErrorWriter) writeGRPCWeb(response http.ResponseWriter, err error) error { - // This is a trailers-only response. To match the behavior of Envoy and - // protocol_grpc.go, put the trailers in the HTTP headers. - grpcErrorToTrailer(w.bufferPool, response.Header(), w.protobuf, err) - response.WriteHeader(http.StatusOK) - return nil -} diff --git a/vendor/github.com/bufbuild/connect-go/header.go b/vendor/github.com/bufbuild/connect-go/header.go deleted file mode 100644 index d0ca7f51..00000000 --- a/vendor/github.com/bufbuild/connect-go/header.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2021-2022 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package connect - -import ( - "encoding/base64" - "net/http" -) - -// EncodeBinaryHeader base64-encodes the data. It always emits unpadded values. -// -// In the Connect, gRPC, and gRPC-Web protocols, binary headers must have keys -// ending in "-Bin". -func EncodeBinaryHeader(data []byte) string { - // gRPC specification says that implementations should emit unpadded values. - return base64.RawStdEncoding.EncodeToString(data) -} - -// DecodeBinaryHeader base64-decodes the data. It can decode padded or unpadded -// values. Following usual HTTP semantics, multiple base64-encoded values may -// be joined with a comma. When receiving such comma-separated values, split -// them with [strings.Split] before calling DecodeBinaryHeader. -// -// Binary headers sent using the Connect, gRPC, and gRPC-Web protocols have -// keys ending in "-Bin". -func DecodeBinaryHeader(data string) ([]byte, error) { - if len(data)%4 != 0 { - // Data definitely isn't padded. - return base64.RawStdEncoding.DecodeString(data) - } - // Either the data was padded, or padding wasn't necessary. In both cases, - // the padding-aware decoder works. - return base64.StdEncoding.DecodeString(data) -} - -func mergeHeaders(into, from http.Header) { - for k, vals := range from { - into[k] = append(into[k], vals...) - } -} diff --git a/vendor/github.com/bufbuild/connect-go/maxbytes.go b/vendor/github.com/bufbuild/connect-go/maxbytes.go deleted file mode 100644 index 455dba2b..00000000 --- a/vendor/github.com/bufbuild/connect-go/maxbytes.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021-2022 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build go1.19 - -package connect - -import ( - "errors" - "fmt" - "net/http" -) - -func asMaxBytesError(err error, tmpl string, args ...any) *Error { - var maxBytesErr *http.MaxBytesError - if ok := errors.As(err, &maxBytesErr); !ok { - return nil - } - prefix := fmt.Sprintf(tmpl, args...) - return errorf(CodeResourceExhausted, "%s: exceeded %d byte http.MaxBytesReader limit", prefix, maxBytesErr.Limit) -} diff --git a/vendor/github.com/bufbuild/connect-go/maxbytes_go118.go b/vendor/github.com/bufbuild/connect-go/maxbytes_go118.go deleted file mode 100644 index 32a3e022..00000000 --- a/vendor/github.com/bufbuild/connect-go/maxbytes_go118.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021-2022 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//go:build !go1.19 - -package connect - -import ( - "fmt" - "strings" -) - -func asMaxBytesError(err error, tmpl string, args ...any) *Error { - const expect = "http: request body too large" - text := err.Error() - if text != expect && !strings.HasSuffix(text, ": "+expect) { - return nil - } - prefix := fmt.Sprintf(tmpl, args...) - return errorf(CodeResourceExhausted, "%s: exceeded http.MaxBytesReader limit", prefix) -} diff --git a/vendor/github.com/catppuccin/go/.editorconfig b/vendor/github.com/catppuccin/go/.editorconfig new file mode 100644 index 00000000..d86ac027 --- /dev/null +++ b/vendor/github.com/catppuccin/go/.editorconfig @@ -0,0 +1,34 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 2 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# go +[*.go] +indent_style = tab +indent_size = 4 + +# python +[*.{ini,py,py.tpl,rst}] +indent_size = 4 + +# rust +[*.rs] +indent_size = 4 + +# documentation, utils +[*.{md,mdx,diff}] +trim_trailing_whitespace = false + +# windows shell scripts +[*.{cmd,bat,ps1}] +end_of_line = crlf diff --git a/vendor/github.com/catppuccin/go/.gitignore b/vendor/github.com/catppuccin/go/.gitignore new file mode 100644 index 00000000..68f24f11 --- /dev/null +++ b/vendor/github.com/catppuccin/go/.gitignore @@ -0,0 +1,8 @@ +coverage.txt +<<<<<<< HEAD + +dist/ +======= +_examples/catppuccin +_examples/frames/ +>>>>>>> 448501b (feat: documentation, better examples) diff --git a/vendor/github.com/catppuccin/go/.goreleaser.yaml b/vendor/github.com/catppuccin/go/.goreleaser.yaml new file mode 100644 index 00000000..20a664a8 --- /dev/null +++ b/vendor/github.com/catppuccin/go/.goreleaser.yaml @@ -0,0 +1,9 @@ +before: + hooks: + - go mod tidy +builds: +- skip: true + +# modelines, feel free to remove those if you don't want/use them: +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj diff --git a/vendor/github.com/catppuccin/go/LICENSE b/vendor/github.com/catppuccin/go/LICENSE new file mode 100644 index 00000000..006383b8 --- /dev/null +++ b/vendor/github.com/catppuccin/go/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Catppuccin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/catppuccin/go/README.md b/vendor/github.com/catppuccin/go/README.md new file mode 100644 index 00000000..984068e5 --- /dev/null +++ b/vendor/github.com/catppuccin/go/README.md @@ -0,0 +1,43 @@ +

+ Logo
+ + Catppuccin for Go + +

+ +

+ + + +

+ +![Catppuccin Variants](https://raw.githubusercontent.com/catppuccin/go/main/_examples/catppuccin.gif) + +## Usage + +1. `go get github.com/catppuccin/go` +2. Add an`import catppuccin "github.com/catppuccin/go"` to your file +3. Use the flavours as you wish. + +## 🙋 FAQ (optional) + +- Q: **_"Where can I find the doc?"_**\ + A: https://pkg.go.dev/github.com/catppuccin/go + +## 💠Thanks to + +- [Carlos Becker](https://github.com/caarlos0) + +  + +

+ +

+ +

+ Copyright © 2021-present Catppuccin Org +

+ +

+ +

diff --git a/vendor/github.com/catppuccin/go/frappe.go b/vendor/github.com/catppuccin/go/frappe.go new file mode 100644 index 00000000..14d1308c --- /dev/null +++ b/vendor/github.com/catppuccin/go/frappe.go @@ -0,0 +1,217 @@ +package catppuccingo + +// frappe variant. +type frappe struct{} + +// Frappe flavour variant. +var Frappe Flavour = frappe{} + +func (frappe) Name() string { return "frappe" } + +func (frappe) Rosewater() Color { + return Color{ + Hex: "#f2d5cf", + RGB: [3]uint32{242, 213, 207}, + HSL: [3]float32{10, 0.57, 0.88}, + } +} + +func (frappe) Flamingo() Color { + return Color{ + Hex: "#eebebe", + RGB: [3]uint32{238, 190, 190}, + HSL: [3]float32{0, 0.59, 0.84}, + } +} + +func (frappe) Pink() Color { + return Color{ + Hex: "#f4b8e4", + RGB: [3]uint32{244, 184, 228}, + HSL: [3]float32{316, 0.73, 0.84}, + } +} + +func (frappe) Mauve() Color { + return Color{ + Hex: "#ca9ee6", + RGB: [3]uint32{202, 158, 230}, + HSL: [3]float32{277, 0.59, 0.76}, + } +} + +func (frappe) Red() Color { + return Color{ + Hex: "#e78284", + RGB: [3]uint32{231, 130, 132}, + HSL: [3]float32{359, 0.68, 0.71}, + } +} + +func (frappe) Maroon() Color { + return Color{ + Hex: "#ea999c", + RGB: [3]uint32{234, 153, 156}, + HSL: [3]float32{358, 0.66, 0.76}, + } +} + +func (frappe) Peach() Color { + return Color{ + Hex: "#ef9f76", + RGB: [3]uint32{239, 159, 118}, + HSL: [3]float32{20, 0.79, 0.70}, + } +} + +func (frappe) Yellow() Color { + return Color{ + Hex: "#e5c890", + RGB: [3]uint32{229, 200, 144}, + HSL: [3]float32{40, 0.62, 0.73}, + } +} + +func (frappe) Green() Color { + return Color{ + Hex: "#a6d189", + RGB: [3]uint32{166, 209, 137}, + HSL: [3]float32{96, 0.44, 0.68}, + } +} + +func (frappe) Teal() Color { + return Color{ + Hex: "#81c8be", + RGB: [3]uint32{129, 200, 190}, + HSL: [3]float32{172, 0.39, 0.65}, + } +} + +func (frappe) Sky() Color { + return Color{ + Hex: "#99d1db", + RGB: [3]uint32{153, 209, 219}, + HSL: [3]float32{189, 0.48, 0.73}, + } +} + +func (frappe) Sapphire() Color { + return Color{ + Hex: "#85c1dc", + RGB: [3]uint32{133, 193, 220}, + HSL: [3]float32{199, 0.55, 0.69}, + } +} + +func (frappe) Blue() Color { + return Color{ + Hex: "#8caaee", + RGB: [3]uint32{140, 170, 238}, + HSL: [3]float32{222, 0.74, 0.74}, + } +} + +func (frappe) Lavender() Color { + return Color{ + Hex: "#babbf1", + RGB: [3]uint32{186, 187, 241}, + HSL: [3]float32{239, 0.66, 0.84}, + } +} + +func (frappe) Text() Color { + return Color{ + Hex: "#c6d0f5", + RGB: [3]uint32{198, 208, 245}, + HSL: [3]float32{227, 0.70, 0.87}, + } +} + +func (frappe) Subtext1() Color { + return Color{ + Hex: "#b5bfe2", + RGB: [3]uint32{181, 191, 226}, + HSL: [3]float32{227, 0.44, 0.80}, + } +} + +func (frappe) Subtext0() Color { + return Color{ + Hex: "#a5adce", + RGB: [3]uint32{165, 173, 206}, + HSL: [3]float32{228, 0.29, 0.73}, + } +} + +func (frappe) Overlay2() Color { + return Color{ + Hex: "#949cbb", + RGB: [3]uint32{148, 156, 187}, + HSL: [3]float32{228, 0.22, 0.66}, + } +} + +func (frappe) Overlay1() Color { + return Color{ + Hex: "#838ba7", + RGB: [3]uint32{131, 139, 167}, + HSL: [3]float32{227, 0.17, 0.58}, + } +} + +func (frappe) Overlay0() Color { + return Color{ + Hex: "#737994", + RGB: [3]uint32{115, 121, 148}, + HSL: [3]float32{229, 0.13, 0.52}, + } +} + +func (frappe) Surface2() Color { + return Color{ + Hex: "#626880", + RGB: [3]uint32{98, 104, 128}, + HSL: [3]float32{228, 0.13, 0.44}, + } +} + +func (frappe) Surface1() Color { + return Color{ + Hex: "#51576d", + RGB: [3]uint32{81, 87, 109}, + HSL: [3]float32{227, 0.15, 0.37}, + } +} + +func (frappe) Surface0() Color { + return Color{ + Hex: "#414559", + RGB: [3]uint32{65, 69, 89}, + HSL: [3]float32{230, 0.16, 0.30}, + } +} + +func (frappe) Base() Color { + return Color{ + Hex: "#303446", + RGB: [3]uint32{48, 52, 70}, + HSL: [3]float32{229, 0.19, 0.23}, + } +} + +func (frappe) Mantle() Color { + return Color{ + Hex: "#292c3c", + RGB: [3]uint32{41, 44, 60}, + HSL: [3]float32{231, 0.19, 0.20}, + } +} + +func (frappe) Crust() Color { + return Color{ + Hex: "#232634", + RGB: [3]uint32{35, 38, 52}, + HSL: [3]float32{229, 0.20, 0.17}, + } +} diff --git a/vendor/github.com/catppuccin/go/latte.go b/vendor/github.com/catppuccin/go/latte.go new file mode 100644 index 00000000..35cfd338 --- /dev/null +++ b/vendor/github.com/catppuccin/go/latte.go @@ -0,0 +1,217 @@ +package catppuccingo + +// latte variant. +type latte struct{} + +// Latte flavour variant. +var Latte Flavour = latte{} + +func (latte) Name() string { return "latte" } + +func (latte) Rosewater() Color { + return Color{ + Hex: "#dc8a78", + RGB: [3]uint32{220, 138, 120}, + HSL: [3]float32{11, 0.59, 0.67}, + } +} + +func (latte) Flamingo() Color { + return Color{ + Hex: "#dd7878", + RGB: [3]uint32{221, 120, 120}, + HSL: [3]float32{0, 0.60, 0.67}, + } +} + +func (latte) Pink() Color { + return Color{ + Hex: "#ea76cb", + RGB: [3]uint32{234, 118, 203}, + HSL: [3]float32{316, 0.73, 0.69}, + } +} + +func (latte) Mauve() Color { + return Color{ + Hex: "#8839ef", + RGB: [3]uint32{136, 57, 239}, + HSL: [3]float32{266, 0.85, 0.58}, + } +} + +func (latte) Red() Color { + return Color{ + Hex: "#d20f39", + RGB: [3]uint32{210, 15, 57}, + HSL: [3]float32{347, 0.87, 0.44}, + } +} + +func (latte) Maroon() Color { + return Color{ + Hex: "#e64553", + RGB: [3]uint32{230, 69, 83}, + HSL: [3]float32{355, 0.76, 0.59}, + } +} + +func (latte) Peach() Color { + return Color{ + Hex: "#fe640b", + RGB: [3]uint32{254, 100, 11}, + HSL: [3]float32{22, 0.99, 0.52}, + } +} + +func (latte) Yellow() Color { + return Color{ + Hex: "#df8e1d", + RGB: [3]uint32{223, 142, 29}, + HSL: [3]float32{35, 0.77, 0.49}, + } +} + +func (latte) Green() Color { + return Color{ + Hex: "#40a02b", + RGB: [3]uint32{64, 160, 43}, + HSL: [3]float32{109, 0.58, 0.40}, + } +} + +func (latte) Teal() Color { + return Color{ + Hex: "#179299", + RGB: [3]uint32{23, 146, 153}, + HSL: [3]float32{183, 0.74, 0.35}, + } +} + +func (latte) Sky() Color { + return Color{ + Hex: "#04a5e5", + RGB: [3]uint32{4, 165, 229}, + HSL: [3]float32{197, 0.97, 0.46}, + } +} + +func (latte) Sapphire() Color { + return Color{ + Hex: "#209fb5", + RGB: [3]uint32{32, 159, 181}, + HSL: [3]float32{189, 0.70, 0.42}, + } +} + +func (latte) Blue() Color { + return Color{ + Hex: "#1e66f5", + RGB: [3]uint32{30, 102, 245}, + HSL: [3]float32{220, 0.91, 0.54}, + } +} + +func (latte) Lavender() Color { + return Color{ + Hex: "#7287fd", + RGB: [3]uint32{114, 135, 253}, + HSL: [3]float32{231, 0.97, 0.72}, + } +} + +func (latte) Text() Color { + return Color{ + Hex: "#4c4f69", + RGB: [3]uint32{76, 79, 105}, + HSL: [3]float32{234, 0.16, 0.35}, + } +} + +func (latte) Subtext1() Color { + return Color{ + Hex: "#5c5f77", + RGB: [3]uint32{92, 95, 119}, + HSL: [3]float32{233, 0.13, 0.41}, + } +} + +func (latte) Subtext0() Color { + return Color{ + Hex: "#6c6f85", + RGB: [3]uint32{108, 111, 133}, + HSL: [3]float32{233, 0.10, 0.47}, + } +} + +func (latte) Overlay2() Color { + return Color{ + Hex: "#7c7f93", + RGB: [3]uint32{124, 127, 147}, + HSL: [3]float32{232, 0.10, 0.53}, + } +} + +func (latte) Overlay1() Color { + return Color{ + Hex: "#8c8fa1", + RGB: [3]uint32{140, 143, 161}, + HSL: [3]float32{231, 0.10, 0.59}, + } +} + +func (latte) Overlay0() Color { + return Color{ + Hex: "#9ca0b0", + RGB: [3]uint32{156, 160, 176}, + HSL: [3]float32{228, 0.11, 0.65}, + } +} + +func (latte) Surface2() Color { + return Color{ + Hex: "#acb0be", + RGB: [3]uint32{172, 176, 190}, + HSL: [3]float32{227, 0.12, 0.71}, + } +} + +func (latte) Surface1() Color { + return Color{ + Hex: "#bcc0cc", + RGB: [3]uint32{188, 192, 204}, + HSL: [3]float32{225, 0.14, 0.77}, + } +} + +func (latte) Surface0() Color { + return Color{ + Hex: "#ccd0da", + RGB: [3]uint32{204, 208, 218}, + HSL: [3]float32{223, 0.16, 0.83}, + } +} + +func (latte) Crust() Color { + return Color{ + Hex: "#dce0e8", + RGB: [3]uint32{220, 224, 232}, + HSL: [3]float32{220, 0.21, 0.89}, + } +} + +func (latte) Mantle() Color { + return Color{ + Hex: "#e6e9ef", + RGB: [3]uint32{230, 233, 239}, + HSL: [3]float32{220, 0.22, 0.92}, + } +} + +func (latte) Base() Color { + return Color{ + Hex: "#eff1f5", + RGB: [3]uint32{239, 241, 245}, + HSL: [3]float32{220, 0.23, 0.95}, + } +} diff --git a/vendor/github.com/catppuccin/go/macchiato.go b/vendor/github.com/catppuccin/go/macchiato.go new file mode 100644 index 00000000..4d3062f3 --- /dev/null +++ b/vendor/github.com/catppuccin/go/macchiato.go @@ -0,0 +1,217 @@ +package catppuccingo + +// macchiato variant. +type macchiato struct{} + +// Macchiato flavour variant. +var Macchiato Flavour = macchiato{} + +func (macchiato) Name() string { return "macchiato" } + +func (macchiato) Rosewater() Color { + return Color{ + Hex: "#f4dbd6", + RGB: [3]uint32{244, 219, 214}, + HSL: [3]float32{10, 0.58, 0.90}, + } +} + +func (macchiato) Flamingo() Color { + return Color{ + Hex: "#f0c6c6", + RGB: [3]uint32{240, 198, 198}, + HSL: [3]float32{0, 0.58, 0.86}, + } +} + +func (macchiato) Pink() Color { + return Color{ + Hex: "#f5bde6", + RGB: [3]uint32{245, 189, 230}, + HSL: [3]float32{316, 0.74, 0.85}, + } +} + +func (macchiato) Mauve() Color { + return Color{ + Hex: "#c6a0f6", + RGB: [3]uint32{198, 160, 246}, + HSL: [3]float32{267, 0.83, 0.80}, + } +} + +func (macchiato) Red() Color { + return Color{ + Hex: "#ed8796", + RGB: [3]uint32{237, 135, 150}, + HSL: [3]float32{351, 0.74, 0.73}, + } +} + +func (macchiato) Maroon() Color { + return Color{ + Hex: "#ee99a0", + RGB: [3]uint32{238, 153, 160}, + HSL: [3]float32{355, 0.71, 0.77}, + } +} + +func (macchiato) Peach() Color { + return Color{ + Hex: "#f5a97f", + RGB: [3]uint32{245, 169, 127}, + HSL: [3]float32{21, 0.86, 0.73}, + } +} + +func (macchiato) Yellow() Color { + return Color{ + Hex: "#eed49f", + RGB: [3]uint32{238, 212, 159}, + HSL: [3]float32{40, 0.70, 0.78}, + } +} + +func (macchiato) Green() Color { + return Color{ + Hex: "#a6da95", + RGB: [3]uint32{166, 218, 149}, + HSL: [3]float32{105, 0.48, 0.72}, + } +} + +func (macchiato) Teal() Color { + return Color{ + Hex: "#8bd5ca", + RGB: [3]uint32{139, 213, 202}, + HSL: [3]float32{171, 0.47, 0.69}, + } +} + +func (macchiato) Sky() Color { + return Color{ + Hex: "#91d7e3", + RGB: [3]uint32{145, 215, 227}, + HSL: [3]float32{189, 0.59, 0.73}, + } +} + +func (macchiato) Sapphire() Color { + return Color{ + Hex: "#7dc4e4", + RGB: [3]uint32{125, 196, 228}, + HSL: [3]float32{199, 0.66, 0.69}, + } +} + +func (macchiato) Blue() Color { + return Color{ + Hex: "#8aadf4", + RGB: [3]uint32{138, 173, 244}, + HSL: [3]float32{220, 0.83, 0.75}, + } +} + +func (macchiato) Lavender() Color { + return Color{ + Hex: "#b7bdf8", + RGB: [3]uint32{183, 189, 248}, + HSL: [3]float32{234, 0.82, 0.85}, + } +} + +func (macchiato) Text() Color { + return Color{ + Hex: "#cad3f5", + RGB: [3]uint32{202, 211, 245}, + HSL: [3]float32{227, 0.68, 0.88}, + } +} + +func (macchiato) Subtext1() Color { + return Color{ + Hex: "#b8c0e0", + RGB: [3]uint32{184, 192, 224}, + HSL: [3]float32{228, 0.39, 0.80}, + } +} + +func (macchiato) Subtext0() Color { + return Color{ + Hex: "#a5adcb", + RGB: [3]uint32{165, 173, 203}, + HSL: [3]float32{227, 0.27, 0.72}, + } +} + +func (macchiato) Overlay2() Color { + return Color{ + Hex: "#939ab7", + RGB: [3]uint32{147, 154, 183}, + HSL: [3]float32{228, 0.20, 0.65}, + } +} + +func (macchiato) Overlay1() Color { + return Color{ + Hex: "#8087a2", + RGB: [3]uint32{128, 135, 162}, + HSL: [3]float32{228, 0.15, 0.57}, + } +} + +func (macchiato) Overlay0() Color { + return Color{ + Hex: "#6e738d", + RGB: [3]uint32{110, 115, 141}, + HSL: [3]float32{230, 0.12, 0.49}, + } +} + +func (macchiato) Surface2() Color { + return Color{ + Hex: "#5b6078", + RGB: [3]uint32{91, 96, 120}, + HSL: [3]float32{230, 0.14, 0.41}, + } +} + +func (macchiato) Surface1() Color { + return Color{ + Hex: "#494d64", + RGB: [3]uint32{73, 77, 100}, + HSL: [3]float32{231, 0.16, 0.34}, + } +} + +func (macchiato) Surface0() Color { + return Color{ + Hex: "#363a4f", + RGB: [3]uint32{54, 58, 79}, + HSL: [3]float32{230, 0.19, 0.26}, + } +} + +func (macchiato) Base() Color { + return Color{ + Hex: "#24273a", + RGB: [3]uint32{36, 39, 58}, + HSL: [3]float32{232, 0.23, 0.18}, + } +} + +func (macchiato) Mantle() Color { + return Color{ + Hex: "#1e2030", + RGB: [3]uint32{30, 32, 48}, + HSL: [3]float32{233, 0.23, 0.15}, + } +} + +func (macchiato) Crust() Color { + return Color{ + Hex: "#181926", + RGB: [3]uint32{24, 25, 38}, + HSL: [3]float32{236, 0.23, 0.12}, + } +} diff --git a/vendor/github.com/catppuccin/go/main.go b/vendor/github.com/catppuccin/go/main.go new file mode 100644 index 00000000..ad0c2a5b --- /dev/null +++ b/vendor/github.com/catppuccin/go/main.go @@ -0,0 +1,69 @@ +package catppuccingo + +import ( + "image/color" + "strings" +) + +// Flavour is an interface implemented by all Catppuccin variations. +type Flavour interface { + Rosewater() Color + Flamingo() Color + Pink() Color + Mauve() Color + Red() Color + Maroon() Color + Peach() Color + Yellow() Color + Green() Color + Teal() Color + Sky() Color + Sapphire() Color + Blue() Color + Lavender() Color + Text() Color + Subtext1() Color + Subtext0() Color + Overlay2() Color + Overlay1() Color + Overlay0() Color + Surface2() Color + Surface1() Color + Surface0() Color + Crust() Color + Mantle() Color + Base() Color + Name() string +} + +// Theme is a type alias of Flavour to keep compatibility with previous versions. +type Theme = Flavour + +// Color is a color in Hex, RGB, and HSL. +type Color struct { + Hex string + RGB [3]uint32 + HSL [3]float32 +} + +// RGBA implements color.Color +func (c Color) RGBA() (r uint32, g uint32, b uint32, a uint32) { + return c.RGB[0], c.RGB[1], c.RGB[2], 1 +} + +var _ color.Color = Color{} + +// Variant returns the Theme variant by name. +func Variant(flavour string) Theme { + for _, t := range []Theme{ + Mocha, + Frappe, + Macchiato, + Latte, + } { + if strings.EqualFold(t.Name(), flavour) { + return t + } + } + return nil +} diff --git a/vendor/github.com/catppuccin/go/mocha.go b/vendor/github.com/catppuccin/go/mocha.go new file mode 100644 index 00000000..8d9d2e3b --- /dev/null +++ b/vendor/github.com/catppuccin/go/mocha.go @@ -0,0 +1,216 @@ +package catppuccingo + +type mocha struct{} + +// Mocha flavour variant. +var Mocha Flavour = mocha{} + +func (mocha) Name() string { return "mocha" } + +func (mocha) Rosewater() Color { + return Color{ + Hex: "#f5e0dc", + RGB: [3]uint32{245, 224, 220}, + HSL: [3]float32{10, 0.56, 0.91}, + } +} + +func (mocha) Flamingo() Color { + return Color{ + Hex: "#f2cdcd", + RGB: [3]uint32{242, 205, 205}, + HSL: [3]float32{0, 0.59, 0.88}, + } +} + +func (mocha) Pink() Color { + return Color{ + Hex: "#f5c2e7", + RGB: [3]uint32{245, 194, 231}, + HSL: [3]float32{316, 0.72, 0.86}, + } +} + +func (mocha) Mauve() Color { + return Color{ + Hex: "#cba6f7", + RGB: [3]uint32{203, 166, 247}, + HSL: [3]float32{267, 0.84, 0.81}, + } +} + +func (mocha) Red() Color { + return Color{ + Hex: "#f38ba8", + RGB: [3]uint32{243, 139, 168}, + HSL: [3]float32{343, 0.81, 0.75}, + } +} + +func (mocha) Maroon() Color { + return Color{ + Hex: "#eba0ac", + RGB: [3]uint32{235, 160, 172}, + HSL: [3]float32{350, 0.65, 0.77}, + } +} + +func (mocha) Peach() Color { + return Color{ + Hex: "#fab387", + RGB: [3]uint32{250, 179, 135}, + HSL: [3]float32{23, 0.92, 0.75}, + } +} + +func (mocha) Yellow() Color { + return Color{ + Hex: "#f9e2af", + RGB: [3]uint32{249, 226, 175}, + HSL: [3]float32{41, 0.86, 0.83}, + } +} + +func (mocha) Green() Color { + return Color{ + Hex: "#a6e3a1", + RGB: [3]uint32{166, 227, 161}, + HSL: [3]float32{115, 0.54, 0.76}, + } +} + +func (mocha) Teal() Color { + return Color{ + Hex: "#94e2d5", + RGB: [3]uint32{148, 226, 213}, + HSL: [3]float32{170, 0.57, 0.73}, + } +} + +func (mocha) Sky() Color { + return Color{ + Hex: "#89dceb", + RGB: [3]uint32{137, 220, 235}, + HSL: [3]float32{189, 0.71, 0.73}, + } +} + +func (mocha) Sapphire() Color { + return Color{ + Hex: "#74c7ec", + RGB: [3]uint32{116, 199, 236}, + HSL: [3]float32{199, 0.76, 0.69}, + } +} + +func (mocha) Blue() Color { + return Color{ + Hex: "#89b4fa", + RGB: [3]uint32{137, 180, 250}, + HSL: [3]float32{217, 0.92, 0.76}, + } +} + +func (mocha) Lavender() Color { + return Color{ + Hex: "#b4befe", + RGB: [3]uint32{180, 190, 254}, + HSL: [3]float32{232, 0.97, 0.85}, + } +} + +func (mocha) Text() Color { + return Color{ + Hex: "#cdd6f4", + RGB: [3]uint32{205, 214, 244}, + HSL: [3]float32{226, 0.64, 0.88}, + } +} + +func (mocha) Subtext1() Color { + return Color{ + Hex: "#bac2de", + RGB: [3]uint32{186, 194, 222}, + HSL: [3]float32{227, 0.35, 0.80}, + } +} + +func (mocha) Subtext0() Color { + return Color{ + Hex: "#a6adc8", + RGB: [3]uint32{166, 173, 200}, + HSL: [3]float32{228, 0.24, 0.72}, + } +} + +func (mocha) Overlay2() Color { + return Color{ + Hex: "#9399b2", + RGB: [3]uint32{147, 153, 178}, + HSL: [3]float32{228, 0.17, 0.64}, + } +} + +func (mocha) Overlay1() Color { + return Color{ + Hex: "#7f849c", + RGB: [3]uint32{127, 132, 156}, + HSL: [3]float32{230, 0.13, 0.55}, + } +} + +func (mocha) Overlay0() Color { + return Color{ + Hex: "#6c7086", + RGB: [3]uint32{108, 112, 134}, + HSL: [3]float32{231, 0.11, 0.47}, + } +} + +func (mocha) Surface2() Color { + return Color{ + Hex: "#585b70", + RGB: [3]uint32{88, 91, 112}, + HSL: [3]float32{233, 0.12, 0.39}, + } +} + +func (mocha) Surface1() Color { + return Color{ + Hex: "#45475a", + RGB: [3]uint32{69, 71, 90}, + HSL: [3]float32{234, 0.13, 0.31}, + } +} + +func (mocha) Surface0() Color { + return Color{ + Hex: "#313244", + RGB: [3]uint32{49, 50, 68}, + HSL: [3]float32{237, 0.16, 0.23}, + } +} + +func (mocha) Base() Color { + return Color{ + Hex: "#1e1e2e", + RGB: [3]uint32{30, 30, 46}, + HSL: [3]float32{240, 0.21, 0.15}, + } +} + +func (mocha) Mantle() Color { + return Color{ + Hex: "#181825", + RGB: [3]uint32{24, 24, 37}, + HSL: [3]float32{240, 0.21, 0.12}, + } +} + +func (mocha) Crust() Color { + return Color{ + Hex: "#11111b", + RGB: [3]uint32{17, 17, 27}, + HSL: [3]float32{240, 0.23, 0.9}, + } +} diff --git a/vendor/github.com/charmbracelet/bubbles/LICENSE b/vendor/github.com/charmbracelet/bubbles/LICENSE new file mode 100644 index 00000000..31d76c1c --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2023 Charmbracelet, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/charmbracelet/bubbles/cursor/cursor.go b/vendor/github.com/charmbracelet/bubbles/cursor/cursor.go new file mode 100644 index 00000000..5abda654 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/cursor/cursor.go @@ -0,0 +1,207 @@ +package cursor + +import ( + "context" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +const defaultBlinkSpeed = time.Millisecond * 530 + +// initialBlinkMsg initializes cursor blinking. +type initialBlinkMsg struct{} + +// BlinkMsg signals that the cursor should blink. It contains metadata that +// allows us to tell if the blink message is the one we're expecting. +type BlinkMsg struct { + id int + tag int +} + +// blinkCanceled is sent when a blink operation is canceled. +type blinkCanceled struct{} + +// blinkCtx manages cursor blinking. +type blinkCtx struct { + ctx context.Context + cancel context.CancelFunc +} + +// Mode describes the behavior of the cursor. +type Mode int + +// Available cursor modes. +const ( + CursorBlink Mode = iota + CursorStatic + CursorHide +) + +// String returns the cursor mode in a human-readable format. This method is +// provisional and for informational purposes only. +func (c Mode) String() string { + return [...]string{ + "blink", + "static", + "hidden", + }[c] +} + +// Model is the Bubble Tea model for this cursor element. +type Model struct { + BlinkSpeed time.Duration + // Style for styling the cursor block. + Style lipgloss.Style + // TextStyle is the style used for the cursor when it is hidden (when blinking). + // I.e. displaying normal text. + TextStyle lipgloss.Style + + // char is the character under the cursor + char string + // The ID of this Model as it relates to other cursors + id int + // focus indicates whether the containing input is focused + focus bool + // Cursor Blink state. + Blink bool + // Used to manage cursor blink + blinkCtx *blinkCtx + // The ID of the blink message we're expecting to receive. + blinkTag int + // mode determines the behavior of the cursor + mode Mode +} + +// New creates a new model with default settings. +func New() Model { + return Model{ + BlinkSpeed: defaultBlinkSpeed, + + Blink: true, + mode: CursorBlink, + + blinkCtx: &blinkCtx{ + ctx: context.Background(), + }, + } +} + +// Update updates the cursor. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + switch msg := msg.(type) { + case initialBlinkMsg: + // We accept all initialBlinkMsgs generated by the Blink command. + + if m.mode != CursorBlink || !m.focus { + return m, nil + } + + cmd := m.BlinkCmd() + return m, cmd + + case BlinkMsg: + // We're choosy about whether to accept blinkMsgs so that our cursor + // only exactly when it should. + + // Is this model blink-able? + if m.mode != CursorBlink || !m.focus { + return m, nil + } + + // Were we expecting this blink message? + if msg.id != m.id || msg.tag != m.blinkTag { + return m, nil + } + + var cmd tea.Cmd + if m.mode == CursorBlink { + m.Blink = !m.Blink + cmd = m.BlinkCmd() + } + return m, cmd + + case blinkCanceled: // no-op + return m, nil + } + return m, nil +} + +// Mode returns the model's cursor mode. For available cursor modes, see +// type Mode. +func (m Model) Mode() Mode { + return m.mode +} + +// SetMode sets the model's cursor mode. This method returns a command. +// +// For available cursor modes, see type CursorMode. +func (m *Model) SetMode(mode Mode) tea.Cmd { + m.mode = mode + m.Blink = m.mode == CursorHide || !m.focus + if mode == CursorBlink { + return Blink + } + return nil +} + +// BlinkCmd is a command used to manage cursor blinking. +func (m *Model) BlinkCmd() tea.Cmd { + if m.mode != CursorBlink { + return nil + } + + if m.blinkCtx != nil && m.blinkCtx.cancel != nil { + m.blinkCtx.cancel() + } + + ctx, cancel := context.WithTimeout(m.blinkCtx.ctx, m.BlinkSpeed) + m.blinkCtx.cancel = cancel + + m.blinkTag++ + + return func() tea.Msg { + defer cancel() + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + return BlinkMsg{id: m.id, tag: m.blinkTag} + } + return blinkCanceled{} + } +} + +// Blink is a command used to initialize cursor blinking. +func Blink() tea.Msg { + return initialBlinkMsg{} +} + +// Focus focuses the cursor to allow it to blink if desired. +func (m *Model) Focus() tea.Cmd { + m.focus = true + m.Blink = m.mode == CursorHide // show the cursor unless we've explicitly hidden it + + if m.mode == CursorBlink && m.focus { + return m.BlinkCmd() + } + return nil +} + +// Blur blurs the cursor. +func (m *Model) Blur() { + m.focus = false + m.Blink = true +} + +// SetChar sets the character under the cursor. +func (m *Model) SetChar(char string) { + m.char = char +} + +// View displays the cursor. +func (m Model) View() string { + if m.Blink { + return m.TextStyle.Inline(true).Render(m.char) + } + return m.Style.Inline(true).Reverse(true).Render(m.char) +} diff --git a/vendor/github.com/charmbracelet/bubbles/filepicker/filepicker.go b/vendor/github.com/charmbracelet/bubbles/filepicker/filepicker.go new file mode 100644 index 00000000..9a1235a7 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/filepicker/filepicker.go @@ -0,0 +1,523 @@ +package filepicker + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "sync" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/dustin/go-humanize" +) + +var ( + lastID int + idMtx sync.Mutex +) + +// Return the next ID we should use on the Model. +func nextID() int { + idMtx.Lock() + defer idMtx.Unlock() + lastID++ + return lastID +} + +// New returns a new filepicker model with default styling and key bindings. +func New() Model { + return Model{ + id: nextID(), + CurrentDirectory: ".", + Cursor: ">", + AllowedTypes: []string{}, + selected: 0, + ShowPermissions: true, + ShowSize: true, + ShowHidden: false, + DirAllowed: false, + FileAllowed: true, + AutoHeight: true, + Height: 0, + max: 0, + min: 0, + selectedStack: newStack(), + minStack: newStack(), + maxStack: newStack(), + KeyMap: DefaultKeyMap(), + Styles: DefaultStyles(), + } +} + +type errorMsg struct { + err error +} + +type readDirMsg struct { + id int + entries []os.DirEntry +} + +const ( + marginBottom = 5 + fileSizeWidth = 7 + paddingLeft = 2 +) + +// KeyMap defines key bindings for each user action. +type KeyMap struct { + GoToTop key.Binding + GoToLast key.Binding + Down key.Binding + Up key.Binding + PageUp key.Binding + PageDown key.Binding + Back key.Binding + Open key.Binding + Select key.Binding +} + +// DefaultKeyMap defines the default keybindings. +func DefaultKeyMap() KeyMap { + return KeyMap{ + GoToTop: key.NewBinding(key.WithKeys("g"), key.WithHelp("g", "first")), + GoToLast: key.NewBinding(key.WithKeys("G"), key.WithHelp("G", "last")), + Down: key.NewBinding(key.WithKeys("j", "down", "ctrl+n"), key.WithHelp("j", "down")), + Up: key.NewBinding(key.WithKeys("k", "up", "ctrl+p"), key.WithHelp("k", "up")), + PageUp: key.NewBinding(key.WithKeys("K", "pgup"), key.WithHelp("pgup", "page up")), + PageDown: key.NewBinding(key.WithKeys("J", "pgdown"), key.WithHelp("pgdown", "page down")), + Back: key.NewBinding(key.WithKeys("h", "backspace", "left", "esc"), key.WithHelp("h", "back")), + Open: key.NewBinding(key.WithKeys("l", "right", "enter"), key.WithHelp("l", "open")), + Select: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")), + } +} + +// Styles defines the possible customizations for styles in the file picker. +type Styles struct { + DisabledCursor lipgloss.Style + Cursor lipgloss.Style + Symlink lipgloss.Style + Directory lipgloss.Style + File lipgloss.Style + DisabledFile lipgloss.Style + Permission lipgloss.Style + Selected lipgloss.Style + DisabledSelected lipgloss.Style + FileSize lipgloss.Style + EmptyDirectory lipgloss.Style +} + +// DefaultStyles defines the default styling for the file picker. +func DefaultStyles() Styles { + return DefaultStylesWithRenderer(lipgloss.DefaultRenderer()) +} + +// DefaultStylesWithRenderer defines the default styling for the file picker, +// with a given Lip Gloss renderer. +func DefaultStylesWithRenderer(r *lipgloss.Renderer) Styles { + return Styles{ + DisabledCursor: r.NewStyle().Foreground(lipgloss.Color("247")), + Cursor: r.NewStyle().Foreground(lipgloss.Color("212")), + Symlink: r.NewStyle().Foreground(lipgloss.Color("36")), + Directory: r.NewStyle().Foreground(lipgloss.Color("99")), + File: r.NewStyle(), + DisabledFile: r.NewStyle().Foreground(lipgloss.Color("243")), + DisabledSelected: r.NewStyle().Foreground(lipgloss.Color("247")), + Permission: r.NewStyle().Foreground(lipgloss.Color("244")), + Selected: r.NewStyle().Foreground(lipgloss.Color("212")).Bold(true), + FileSize: r.NewStyle().Foreground(lipgloss.Color("240")).Width(fileSizeWidth).Align(lipgloss.Right), + EmptyDirectory: r.NewStyle().Foreground(lipgloss.Color("240")).PaddingLeft(paddingLeft).SetString("Bummer. No Files Found."), + } +} + +// Model represents a file picker. +type Model struct { + id int + + // Path is the path which the user has selected with the file picker. + Path string + + // CurrentDirectory is the directory that the user is currently in. + CurrentDirectory string + + // AllowedTypes specifies which file types the user may select. + // If empty the user may select any file. + AllowedTypes []string + + KeyMap KeyMap + files []os.DirEntry + ShowPermissions bool + ShowSize bool + ShowHidden bool + DirAllowed bool + FileAllowed bool + + FileSelected string + selected int + selectedStack stack + + min int + max int + maxStack stack + minStack stack + + Height int + AutoHeight bool + + Cursor string + Styles Styles +} + +type stack struct { + Push func(int) + Pop func() int + Length func() int +} + +func newStack() stack { + slice := make([]int, 0) + return stack{ + Push: func(i int) { + slice = append(slice, i) + }, + Pop: func() int { + res := slice[len(slice)-1] + slice = slice[:len(slice)-1] + return res + }, + Length: func() int { + return len(slice) + }, + } +} + +func (m *Model) pushView(selected, min, max int) { + m.selectedStack.Push(selected) + m.minStack.Push(min) + m.maxStack.Push(max) +} + +func (m *Model) popView() (int, int, int) { + return m.selectedStack.Pop(), m.minStack.Pop(), m.maxStack.Pop() +} + +func (m Model) readDir(path string, showHidden bool) tea.Cmd { + return func() tea.Msg { + dirEntries, err := os.ReadDir(path) + if err != nil { + return errorMsg{err} + } + + sort.Slice(dirEntries, func(i, j int) bool { + if dirEntries[i].IsDir() == dirEntries[j].IsDir() { + return dirEntries[i].Name() < dirEntries[j].Name() + } + return dirEntries[i].IsDir() + }) + + if showHidden { + return readDirMsg{id: m.id, entries: dirEntries} + } + + var sanitizedDirEntries []os.DirEntry + for _, dirEntry := range dirEntries { + isHidden, _ := IsHidden(dirEntry.Name()) + if isHidden { + continue + } + sanitizedDirEntries = append(sanitizedDirEntries, dirEntry) + } + return readDirMsg{id: m.id, entries: sanitizedDirEntries} + } +} + +// Init initializes the file picker model. +func (m Model) Init() tea.Cmd { + return m.readDir(m.CurrentDirectory, m.ShowHidden) +} + +// Update handles user interactions within the file picker model. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + switch msg := msg.(type) { + case readDirMsg: + if msg.id != m.id { + break + } + m.files = msg.entries + m.max = max(m.max, m.Height-1) + case tea.WindowSizeMsg: + if m.AutoHeight { + m.Height = msg.Height - marginBottom + } + m.max = m.Height - 1 + case tea.KeyMsg: + switch { + case key.Matches(msg, m.KeyMap.GoToTop): + m.selected = 0 + m.min = 0 + m.max = m.Height - 1 + case key.Matches(msg, m.KeyMap.GoToLast): + m.selected = len(m.files) - 1 + m.min = len(m.files) - m.Height + m.max = len(m.files) - 1 + case key.Matches(msg, m.KeyMap.Down): + m.selected++ + if m.selected >= len(m.files) { + m.selected = len(m.files) - 1 + } + if m.selected > m.max { + m.min++ + m.max++ + } + case key.Matches(msg, m.KeyMap.Up): + m.selected-- + if m.selected < 0 { + m.selected = 0 + } + if m.selected < m.min { + m.min-- + m.max-- + } + case key.Matches(msg, m.KeyMap.PageDown): + m.selected += m.Height + if m.selected >= len(m.files) { + m.selected = len(m.files) - 1 + } + m.min += m.Height + m.max += m.Height + + if m.max >= len(m.files) { + m.max = len(m.files) - 1 + m.min = m.max - m.Height + } + case key.Matches(msg, m.KeyMap.PageUp): + m.selected -= m.Height + if m.selected < 0 { + m.selected = 0 + } + m.min -= m.Height + m.max -= m.Height + + if m.min < 0 { + m.min = 0 + m.max = m.min + m.Height + } + case key.Matches(msg, m.KeyMap.Back): + m.CurrentDirectory = filepath.Dir(m.CurrentDirectory) + if m.selectedStack.Length() > 0 { + m.selected, m.min, m.max = m.popView() + } else { + m.selected = 0 + m.min = 0 + m.max = m.Height - 1 + } + return m, m.readDir(m.CurrentDirectory, m.ShowHidden) + case key.Matches(msg, m.KeyMap.Open): + if len(m.files) == 0 { + break + } + + f := m.files[m.selected] + info, err := f.Info() + if err != nil { + break + } + isSymlink := info.Mode()&os.ModeSymlink != 0 + isDir := f.IsDir() + + if isSymlink { + symlinkPath, _ := filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, f.Name())) + info, err := os.Stat(symlinkPath) + if err != nil { + break + } + if info.IsDir() { + isDir = true + } + } + + if (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) { + if key.Matches(msg, m.KeyMap.Select) { + // Select the current path as the selection + m.Path = filepath.Join(m.CurrentDirectory, f.Name()) + } + } + + if !isDir { + break + } + + m.CurrentDirectory = filepath.Join(m.CurrentDirectory, f.Name()) + m.pushView(m.selected, m.min, m.max) + m.selected = 0 + m.min = 0 + m.max = m.Height - 1 + return m, m.readDir(m.CurrentDirectory, m.ShowHidden) + } + } + return m, nil +} + +// View returns the view of the file picker. +func (m Model) View() string { + if len(m.files) == 0 { + return m.Styles.EmptyDirectory.Height(m.Height).MaxHeight(m.Height).String() + } + var s strings.Builder + + for i, f := range m.files { + if i < m.min || i > m.max { + continue + } + + var symlinkPath string + info, _ := f.Info() + isSymlink := info.Mode()&os.ModeSymlink != 0 + size := strings.Replace(humanize.Bytes(uint64(info.Size())), " ", "", 1) + name := f.Name() + + if isSymlink { + symlinkPath, _ = filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, name)) + } + + disabled := !m.canSelect(name) && !f.IsDir() + + if m.selected == i { + selected := "" + if m.ShowPermissions { + selected += " " + info.Mode().String() + } + if m.ShowSize { + selected += fmt.Sprintf("%"+strconv.Itoa(m.Styles.FileSize.GetWidth())+"s", size) + } + selected += " " + name + if isSymlink { + selected += " → " + symlinkPath + } + if disabled { + s.WriteString(m.Styles.DisabledSelected.Render(m.Cursor) + m.Styles.DisabledSelected.Render(selected)) + } else { + s.WriteString(m.Styles.Cursor.Render(m.Cursor) + m.Styles.Selected.Render(selected)) + } + s.WriteRune('\n') + continue + } + + style := m.Styles.File + if f.IsDir() { + style = m.Styles.Directory + } else if isSymlink { + style = m.Styles.Symlink + } else if disabled { + style = m.Styles.DisabledFile + } + + fileName := style.Render(name) + s.WriteString(m.Styles.Cursor.Render(" ")) + if isSymlink { + fileName += " → " + symlinkPath + } + if m.ShowPermissions { + s.WriteString(" " + m.Styles.Permission.Render(info.Mode().String())) + } + if m.ShowSize { + s.WriteString(m.Styles.FileSize.Render(size)) + } + s.WriteString(" " + fileName) + s.WriteRune('\n') + } + + for i := lipgloss.Height(s.String()); i <= m.Height; i++ { + s.WriteRune('\n') + } + + return s.String() +} + +// DidSelectFile returns whether a user has selected a file (on this msg). +func (m Model) DidSelectFile(msg tea.Msg) (bool, string) { + didSelect, path := m.didSelectFile(msg) + if didSelect && m.canSelect(path) { + return true, path + } + return false, "" +} + +// DidSelectDisabledFile returns whether a user tried to select a disabled file +// (on this msg). This is necessary only if you would like to warn the user that +// they tried to select a disabled file. +func (m Model) DidSelectDisabledFile(msg tea.Msg) (bool, string) { + didSelect, path := m.didSelectFile(msg) + if didSelect && !m.canSelect(path) { + return true, path + } + return false, "" +} + +func (m Model) didSelectFile(msg tea.Msg) (bool, string) { + if len(m.files) == 0 { + return false, "" + } + switch msg := msg.(type) { + case tea.KeyMsg: + // If the msg does not match the Select keymap then this could not have been a selection. + if !key.Matches(msg, m.KeyMap.Select) { + return false, "" + } + + // The key press was a selection, let's confirm whether the current file could + // be selected or used for navigating deeper into the stack. + f := m.files[m.selected] + info, err := f.Info() + if err != nil { + return false, "" + } + isSymlink := info.Mode()&os.ModeSymlink != 0 + isDir := f.IsDir() + + if isSymlink { + symlinkPath, _ := filepath.EvalSymlinks(filepath.Join(m.CurrentDirectory, f.Name())) + info, err := os.Stat(symlinkPath) + if err != nil { + break + } + if info.IsDir() { + isDir = true + } + } + + if (!isDir && m.FileAllowed) || (isDir && m.DirAllowed) && m.Path != "" { + return true, m.Path + } + + // If the msg was not a KeyMsg, then the file could not have been selected this iteration. + // Only a KeyMsg can select a file. + default: + return false, "" + } + return false, "" +} + +func (m Model) canSelect(file string) bool { + if len(m.AllowedTypes) <= 0 { + return true + } + + for _, ext := range m.AllowedTypes { + if strings.HasSuffix(file, ext) { + return true + } + } + return false +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/vendor/github.com/charmbracelet/bubbles/filepicker/hidden_unix.go b/vendor/github.com/charmbracelet/bubbles/filepicker/hidden_unix.go new file mode 100644 index 00000000..f611d0e1 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/filepicker/hidden_unix.go @@ -0,0 +1,11 @@ +//go:build !windows +// +build !windows + +package filepicker + +import "strings" + +// IsHidden reports whether a file is hidden or not. +func IsHidden(file string) (bool, error) { + return strings.HasPrefix(file, "."), nil +} diff --git a/vendor/github.com/charmbracelet/bubbles/filepicker/hidden_windows.go b/vendor/github.com/charmbracelet/bubbles/filepicker/hidden_windows.go new file mode 100644 index 00000000..d9ec5add --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/filepicker/hidden_windows.go @@ -0,0 +1,21 @@ +//go:build windows +// +build windows + +package filepicker + +import ( + "syscall" +) + +// IsHidden reports whether a file is hidden or not. +func IsHidden(file string) (bool, error) { + pointer, err := syscall.UTF16PtrFromString(file) + if err != nil { + return false, err + } + attributes, err := syscall.GetFileAttributes(pointer) + if err != nil { + return false, err + } + return attributes&syscall.FILE_ATTRIBUTE_HIDDEN != 0, nil +} diff --git a/vendor/github.com/charmbracelet/bubbles/help/help.go b/vendor/github.com/charmbracelet/bubbles/help/help.go new file mode 100644 index 00000000..8e5f77f1 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/help/help.go @@ -0,0 +1,233 @@ +package help + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// KeyMap is a map of keybindings used to generate help. Since it's an +// interface it can be any type, though struct or a map[string][]key.Binding +// are likely candidates. +// +// Note that if a key is disabled (via key.Binding.SetEnabled) it will not be +// rendered in the help view, so in theory generated help should self-manage. +type KeyMap interface { + + // ShortHelp returns a slice of bindings to be displayed in the short + // version of the help. The help bubble will render help in the order in + // which the help items are returned here. + ShortHelp() []key.Binding + + // FullHelp returns an extended group of help items, grouped by columns. + // The help bubble will render the help in the order in which the help + // items are returned here. + FullHelp() [][]key.Binding +} + +// Styles is a set of available style definitions for the Help bubble. +type Styles struct { + Ellipsis lipgloss.Style + + // Styling for the short help + ShortKey lipgloss.Style + ShortDesc lipgloss.Style + ShortSeparator lipgloss.Style + + // Styling for the full help + FullKey lipgloss.Style + FullDesc lipgloss.Style + FullSeparator lipgloss.Style +} + +// Model contains the state of the help view. +type Model struct { + Width int + ShowAll bool // if true, render the "full" help menu + + ShortSeparator string + FullSeparator string + + // The symbol we use in the short help when help items have been truncated + // due to width. Periods of ellipsis by default. + Ellipsis string + + Styles Styles +} + +// New creates a new help view with some useful defaults. +func New() Model { + keyStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#909090", + Dark: "#626262", + }) + + descStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#B2B2B2", + Dark: "#4A4A4A", + }) + + sepStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{ + Light: "#DDDADA", + Dark: "#3C3C3C", + }) + + return Model{ + ShortSeparator: " • ", + FullSeparator: " ", + Ellipsis: "…", + Styles: Styles{ + ShortKey: keyStyle, + ShortDesc: descStyle, + ShortSeparator: sepStyle, + Ellipsis: sepStyle.Copy(), + FullKey: keyStyle.Copy(), + FullDesc: descStyle.Copy(), + FullSeparator: sepStyle.Copy(), + }, + } +} + +// NewModel creates a new help view with some useful defaults. +// +// Deprecated: use [New] instead. +var NewModel = New + +// Update helps satisfy the Bubble Tea Model interface. It's a no-op. +func (m Model) Update(_ tea.Msg) (Model, tea.Cmd) { + return m, nil +} + +// View renders the help view's current state. +func (m Model) View(k KeyMap) string { + if m.ShowAll { + return m.FullHelpView(k.FullHelp()) + } + return m.ShortHelpView(k.ShortHelp()) +} + +// ShortHelpView renders a single line help view from a slice of keybindings. +// If the line is longer than the maximum width it will be gracefully +// truncated, showing only as many help items as possible. +func (m Model) ShortHelpView(bindings []key.Binding) string { + if len(bindings) == 0 { + return "" + } + + var b strings.Builder + var totalWidth int + var separator = m.Styles.ShortSeparator.Inline(true).Render(m.ShortSeparator) + + for i, kb := range bindings { + if !kb.Enabled() { + continue + } + + var sep string + if totalWidth > 0 && i < len(bindings) { + sep = separator + } + + str := sep + + m.Styles.ShortKey.Inline(true).Render(kb.Help().Key) + " " + + m.Styles.ShortDesc.Inline(true).Render(kb.Help().Desc) + + w := lipgloss.Width(str) + + // If adding this help item would go over the available width, stop + // drawing. + if m.Width > 0 && totalWidth+w > m.Width { + // Although if there's room for an ellipsis, print that. + tail := " " + m.Styles.Ellipsis.Inline(true).Render(m.Ellipsis) + tailWidth := lipgloss.Width(tail) + + if totalWidth+tailWidth < m.Width { + b.WriteString(tail) + } + + break + } + + totalWidth += w + b.WriteString(str) + } + + return b.String() +} + +// FullHelpView renders help columns from a slice of key binding slices. Each +// top level slice entry renders into a column. +func (m Model) FullHelpView(groups [][]key.Binding) string { + if len(groups) == 0 { + return "" + } + + // Linter note: at this time we don't think it's worth the additional + // code complexity involved in preallocating this slice. + //nolint:prealloc + var ( + out []string + + totalWidth int + sep = m.Styles.FullSeparator.Render(m.FullSeparator) + sepWidth = lipgloss.Width(sep) + ) + + // Iterate over groups to build columns + for i, group := range groups { + if group == nil || !shouldRenderColumn(group) { + continue + } + + var ( + keys []string + descriptions []string + ) + + // Separate keys and descriptions into different slices + for _, kb := range group { + if !kb.Enabled() { + continue + } + keys = append(keys, kb.Help().Key) + descriptions = append(descriptions, kb.Help().Desc) + } + + col := lipgloss.JoinHorizontal(lipgloss.Top, + m.Styles.FullKey.Render(strings.Join(keys, "\n")), + m.Styles.FullKey.Render(" "), + m.Styles.FullDesc.Render(strings.Join(descriptions, "\n")), + ) + + // Column + totalWidth += lipgloss.Width(col) + if m.Width > 0 && totalWidth > m.Width { + break + } + + out = append(out, col) + + // Separator + if i < len(group)-1 { + totalWidth += sepWidth + if m.Width > 0 && totalWidth > m.Width { + break + } + } + + out = append(out, sep) + } + + return lipgloss.JoinHorizontal(lipgloss.Top, out...) +} + +func shouldRenderColumn(b []key.Binding) (ok bool) { + for _, v := range b { + if v.Enabled() { + return true + } + } + return false +} diff --git a/vendor/github.com/charmbracelet/bubbles/key/key.go b/vendor/github.com/charmbracelet/bubbles/key/key.go new file mode 100644 index 00000000..c7888fa7 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/key/key.go @@ -0,0 +1,142 @@ +// Package key provides some types and functions for generating user-definable +// keymappings useful in Bubble Tea components. There are a few different ways +// you can define a keymapping with this package. Here's one example: +// +// type KeyMap struct { +// Up key.Binding +// Down key.Binding +// } +// +// var DefaultKeyMap = KeyMap{ +// Up: key.NewBinding( +// key.WithKeys("k", "up"), // actual keybindings +// key.WithHelp("↑/k", "move up"), // corresponding help text +// ), +// Down: key.NewBinding( +// key.WithKeys("j", "down"), +// key.WithHelp("↓/j", "move down"), +// ), +// } +// +// func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +// switch msg := msg.(type) { +// case tea.KeyMsg: +// switch { +// case key.Matches(msg, DefaultKeyMap.Up): +// // The user pressed up +// case key.Matches(msg, DefaultKeyMap.Down): +// // The user pressed down +// } +// } +// +// // ... +// } +// +// The help information, which is not used in the example above, can be used +// to render help text for keystrokes in your views. +package key + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +// Binding describes a set of keybindings and, optionally, their associated +// help text. +type Binding struct { + keys []string + help Help + disabled bool +} + +// BindingOpt is an initialization option for a keybinding. It's used as an +// argument to NewBinding. +type BindingOpt func(*Binding) + +// NewBinding returns a new keybinding from a set of BindingOpt options. +func NewBinding(opts ...BindingOpt) Binding { + b := &Binding{} + for _, opt := range opts { + opt(b) + } + return *b +} + +// WithKeys initializes a keybinding with the given keystrokes. +func WithKeys(keys ...string) BindingOpt { + return func(b *Binding) { + b.keys = keys + } +} + +// WithHelp initializes a keybinding with the given help text. +func WithHelp(key, desc string) BindingOpt { + return func(b *Binding) { + b.help = Help{Key: key, Desc: desc} + } +} + +// WithDisabled initializes a disabled keybinding. +func WithDisabled() BindingOpt { + return func(b *Binding) { + b.disabled = true + } +} + +// SetKeys sets the keys for the keybinding. +func (b *Binding) SetKeys(keys ...string) { + b.keys = keys +} + +// Keys returns the keys for the keybinding. +func (b Binding) Keys() []string { + return b.keys +} + +// SetHelp sets the help text for the keybinding. +func (b *Binding) SetHelp(key, desc string) { + b.help = Help{Key: key, Desc: desc} +} + +// Help returns the Help information for the keybinding. +func (b Binding) Help() Help { + return b.help +} + +// Enabled returns whether or not the keybinding is enabled. Disabled +// keybindings won't be activated and won't show up in help. Keybindings are +// enabled by default. +func (b Binding) Enabled() bool { + return !b.disabled && b.keys != nil +} + +// SetEnabled enables or disables the keybinding. +func (b *Binding) SetEnabled(v bool) { + b.disabled = !v +} + +// Unbind removes the keys and help from this binding, effectively nullifying +// it. This is a step beyond disabling it, since applications can enable +// or disable key bindings based on application state. +func (b *Binding) Unbind() { + b.keys = nil + b.help = Help{} +} + +// Help is help information for a given keybinding. +type Help struct { + Key string + Desc string +} + +// Matches checks if the given KeyMsg matches the given bindings. +func Matches(k tea.KeyMsg, b ...Binding) bool { + keys := k.String() + for _, binding := range b { + for _, v := range binding.keys { + if keys == v && binding.Enabled() { + return true + } + } + } + return false +} diff --git a/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go b/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go new file mode 100644 index 00000000..82dc3ed3 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/paginator/paginator.go @@ -0,0 +1,198 @@ +// Package paginator provides a Bubble Tea package for calculating pagination +// and rendering pagination info. Note that this package does not render actual +// pages: it's purely for handling keystrokes related to pagination, and +// rendering pagination status. +package paginator + +import ( + "fmt" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +// Type specifies the way we render pagination. +type Type int + +// Pagination rendering options. +const ( + Arabic Type = iota + Dots +) + +// KeyMap is the key bindings for different actions within the paginator. +type KeyMap struct { + PrevPage key.Binding + NextPage key.Binding +} + +// DefaultKeyMap is the default set of key bindings for navigating and acting +// upon the paginator. +var DefaultKeyMap = KeyMap{ + PrevPage: key.NewBinding(key.WithKeys("pgup", "left", "h")), + NextPage: key.NewBinding(key.WithKeys("pgdown", "right", "l")), +} + +// Model is the Bubble Tea model for this user interface. +type Model struct { + // Type configures how the pagination is rendered (Arabic, Dots). + Type Type + // Page is the current page number. + Page int + // PerPage is the number of items per page. + PerPage int + // TotalPages is the total number of pages. + TotalPages int + // ActiveDot is used to mark the current page under the Dots display type. + ActiveDot string + // InactiveDot is used to mark inactive pages under the Dots display type. + InactiveDot string + // ArabicFormat is the printf-style format to use for the Arabic display type. + ArabicFormat string + + // KeyMap encodes the keybindings recognized by the widget. + KeyMap KeyMap + + // Deprecated: customize [KeyMap] instead. + UsePgUpPgDownKeys bool + // Deprecated: customize [KeyMap] instead. + UseLeftRightKeys bool + // Deprecated: customize [KeyMap] instead. + UseUpDownKeys bool + // Deprecated: customize [KeyMap] instead. + UseHLKeys bool + // Deprecated: customize [KeyMap] instead. + UseJKKeys bool +} + +// SetTotalPages is a helper function for calculating the total number of pages +// from a given number of items. Its use is optional since this pager can be +// used for other things beyond navigating sets. Note that it both returns the +// number of total pages and alters the model. +func (m *Model) SetTotalPages(items int) int { + if items < 1 { + return m.TotalPages + } + n := items / m.PerPage + if items%m.PerPage > 0 { + n++ + } + m.TotalPages = n + return n +} + +// ItemsOnPage is a helper function for returning the number of items on the +// current page given the total number of items passed as an argument. +func (m Model) ItemsOnPage(totalItems int) int { + if totalItems < 1 { + return 0 + } + start, end := m.GetSliceBounds(totalItems) + return end - start +} + +// GetSliceBounds is a helper function for paginating slices. Pass the length +// of the slice you're rendering and you'll receive the start and end bounds +// corresponding to the pagination. For example: +// +// bunchOfStuff := []stuff{...} +// start, end := model.GetSliceBounds(len(bunchOfStuff)) +// sliceToRender := bunchOfStuff[start:end] +func (m *Model) GetSliceBounds(length int) (start int, end int) { + start = m.Page * m.PerPage + end = min(m.Page*m.PerPage+m.PerPage, length) + return start, end +} + +// PrevPage is a helper function for navigating one page backward. It will not +// page beyond the first page (i.e. page 0). +func (m *Model) PrevPage() { + if m.Page > 0 { + m.Page-- + } +} + +// NextPage is a helper function for navigating one page forward. It will not +// page beyond the last page (i.e. totalPages - 1). +func (m *Model) NextPage() { + if !m.OnLastPage() { + m.Page++ + } +} + +// OnLastPage returns whether or not we're on the last page. +func (m Model) OnLastPage() bool { + return m.Page == m.TotalPages-1 +} + +// OnFirstPage returns whether or not we're on the first page. +func (m Model) OnFirstPage() bool { + return m.Page == 0 +} + +// New creates a new model with defaults. +func New() Model { + return Model{ + Type: Arabic, + Page: 0, + PerPage: 1, + TotalPages: 1, + KeyMap: DefaultKeyMap, + ActiveDot: "•", + InactiveDot: "○", + ArabicFormat: "%d/%d", + } +} + +// NewModel creates a new model with defaults. +// +// Deprecated: use [New] instead. +var NewModel = New + +// Update is the Tea update function which binds keystrokes to pagination. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.KeyMap.NextPage): + m.NextPage() + case key.Matches(msg, m.KeyMap.PrevPage): + m.PrevPage() + } + } + + return m, nil +} + +// View renders the pagination to a string. +func (m Model) View() string { + switch m.Type { + case Dots: + return m.dotsView() + default: + return m.arabicView() + } +} + +func (m Model) dotsView() string { + var s string + for i := 0; i < m.TotalPages; i++ { + if i == m.Page { + s += m.ActiveDot + continue + } + s += m.InactiveDot + } + return s +} + +func (m Model) arabicView() string { + return fmt.Sprintf(m.ArabicFormat, m.Page+1, m.TotalPages) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/vendor/github.com/charmbracelet/bubbles/runeutil/runeutil.go b/vendor/github.com/charmbracelet/bubbles/runeutil/runeutil.go new file mode 100644 index 00000000..82ea90a2 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/runeutil/runeutil.go @@ -0,0 +1,102 @@ +// Package runeutil provides a utility function for use in Bubbles +// that can process Key messages containing runes. +package runeutil + +import ( + "unicode" + "unicode/utf8" +) + +// Sanitizer is a helper for bubble widgets that want to process +// Runes from input key messages. +type Sanitizer interface { + // Sanitize removes control characters from runes in a KeyRunes + // message, and optionally replaces newline/carriage return/tabs by a + // specified character. + // + // The rune array is modified in-place if possible. In that case, the + // returned slice is the original slice shortened after the control + // characters have been removed/translated. + Sanitize(runes []rune) []rune +} + +// NewSanitizer constructs a rune sanitizer. +func NewSanitizer(opts ...Option) Sanitizer { + s := sanitizer{ + replaceNewLine: []rune("\n"), + replaceTab: []rune(" "), + } + for _, o := range opts { + s = o(s) + } + return &s +} + +// Option is the type of option that can be passed to Sanitize(). +type Option func(sanitizer) sanitizer + +// ReplaceTabs replaces tabs by the specified string. +func ReplaceTabs(tabRepl string) Option { + return func(s sanitizer) sanitizer { + s.replaceTab = []rune(tabRepl) + return s + } +} + +// ReplaceNewlines replaces newline characters by the specified string. +func ReplaceNewlines(nlRepl string) Option { + return func(s sanitizer) sanitizer { + s.replaceNewLine = []rune(nlRepl) + return s + } +} + +func (s *sanitizer) Sanitize(runes []rune) []rune { + // dstrunes are where we are storing the result. + dstrunes := runes[:0:len(runes)] + // copied indicates whether dstrunes is an alias of runes + // or a copy. We need a copy when dst moves past src. + // We use this as an optimization to avoid allocating + // a new rune slice in the common case where the output + // is smaller or equal to the input. + copied := false + + for src := 0; src < len(runes); src++ { + r := runes[src] + switch { + case r == utf8.RuneError: + // skip + + case r == '\r' || r == '\n': + if len(dstrunes)+len(s.replaceNewLine) > src && !copied { + dst := len(dstrunes) + dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine)) + copy(dstrunes, runes[:dst]) + copied = true + } + dstrunes = append(dstrunes, s.replaceNewLine...) + + case r == '\t': + if len(dstrunes)+len(s.replaceTab) > src && !copied { + dst := len(dstrunes) + dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab)) + copy(dstrunes, runes[:dst]) + copied = true + } + dstrunes = append(dstrunes, s.replaceTab...) + + case unicode.IsControl(r): + // Other control characters: skip. + + default: + // Keep the character. + dstrunes = append(dstrunes, runes[src]) + } + } + return dstrunes +} + +type sanitizer struct { + replaceNewLine []rune + replaceTab []rune +} diff --git a/vendor/github.com/charmbracelet/bubbles/table/table.go b/vendor/github.com/charmbracelet/bubbles/table/table.go new file mode 100644 index 00000000..36549e3b --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/table/table.go @@ -0,0 +1,427 @@ +package table + +import ( + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/mattn/go-runewidth" +) + +// Model defines a state for the table widget. +type Model struct { + KeyMap KeyMap + + cols []Column + rows []Row + cursor int + focus bool + styles Styles + + viewport viewport.Model + start int + end int +} + +// Row represents one line in the table. +type Row []string + +// Column defines the table structure. +type Column struct { + Title string + Width int +} + +// KeyMap defines keybindings. It satisfies to the help.KeyMap interface, which +// is used to render the menu. +type KeyMap struct { + LineUp key.Binding + LineDown key.Binding + PageUp key.Binding + PageDown key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding + GotoTop key.Binding + GotoBottom key.Binding +} + +// DefaultKeyMap returns a default set of keybindings. +func DefaultKeyMap() KeyMap { + const spacebar = " " + return KeyMap{ + LineUp: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "up"), + ), + LineDown: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "down"), + ), + PageUp: key.NewBinding( + key.WithKeys("b", "pgup"), + key.WithHelp("b/pgup", "page up"), + ), + PageDown: key.NewBinding( + key.WithKeys("f", "pgdown", spacebar), + key.WithHelp("f/pgdn", "page down"), + ), + HalfPageUp: key.NewBinding( + key.WithKeys("u", "ctrl+u"), + key.WithHelp("u", "½ page up"), + ), + HalfPageDown: key.NewBinding( + key.WithKeys("d", "ctrl+d"), + key.WithHelp("d", "½ page down"), + ), + GotoTop: key.NewBinding( + key.WithKeys("home", "g"), + key.WithHelp("g/home", "go to start"), + ), + GotoBottom: key.NewBinding( + key.WithKeys("end", "G"), + key.WithHelp("G/end", "go to end"), + ), + } +} + +// Styles contains style definitions for this list component. By default, these +// values are generated by DefaultStyles. +type Styles struct { + Header lipgloss.Style + Cell lipgloss.Style + Selected lipgloss.Style +} + +// DefaultStyles returns a set of default style definitions for this table. +func DefaultStyles() Styles { + return Styles{ + Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")), + Header: lipgloss.NewStyle().Bold(true).Padding(0, 1), + Cell: lipgloss.NewStyle().Padding(0, 1), + } +} + +// SetStyles sets the table styles. +func (m *Model) SetStyles(s Styles) { + m.styles = s + m.UpdateViewport() +} + +// Option is used to set options in New. For example: +// +// table := New(WithColumns([]Column{{Title: "ID", Width: 10}})) +type Option func(*Model) + +// New creates a new model for the table widget. +func New(opts ...Option) Model { + m := Model{ + cursor: 0, + viewport: viewport.New(0, 20), + + KeyMap: DefaultKeyMap(), + styles: DefaultStyles(), + } + + for _, opt := range opts { + opt(&m) + } + + m.UpdateViewport() + + return m +} + +// WithColumns sets the table columns (headers). +func WithColumns(cols []Column) Option { + return func(m *Model) { + m.cols = cols + } +} + +// WithRows sets the table rows (data). +func WithRows(rows []Row) Option { + return func(m *Model) { + m.rows = rows + } +} + +// WithHeight sets the height of the table. +func WithHeight(h int) Option { + return func(m *Model) { + m.viewport.Height = h + } +} + +// WithWidth sets the width of the table. +func WithWidth(w int) Option { + return func(m *Model) { + m.viewport.Width = w + } +} + +// WithFocused sets the focus state of the table. +func WithFocused(f bool) Option { + return func(m *Model) { + m.focus = f + } +} + +// WithStyles sets the table styles. +func WithStyles(s Styles) Option { + return func(m *Model) { + m.styles = s + } +} + +// WithKeyMap sets the key map. +func WithKeyMap(km KeyMap) Option { + return func(m *Model) { + m.KeyMap = km + } +} + +// Update is the Bubble Tea update loop. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + if !m.focus { + return m, nil + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.KeyMap.LineUp): + m.MoveUp(1) + case key.Matches(msg, m.KeyMap.LineDown): + m.MoveDown(1) + case key.Matches(msg, m.KeyMap.PageUp): + m.MoveUp(m.viewport.Height) + case key.Matches(msg, m.KeyMap.PageDown): + m.MoveDown(m.viewport.Height) + case key.Matches(msg, m.KeyMap.HalfPageUp): + m.MoveUp(m.viewport.Height / 2) + case key.Matches(msg, m.KeyMap.HalfPageDown): + m.MoveDown(m.viewport.Height / 2) + case key.Matches(msg, m.KeyMap.LineDown): + m.MoveDown(1) + case key.Matches(msg, m.KeyMap.GotoTop): + m.GotoTop() + case key.Matches(msg, m.KeyMap.GotoBottom): + m.GotoBottom() + } + } + + return m, nil +} + +// Focused returns the focus state of the table. +func (m Model) Focused() bool { + return m.focus +} + +// Focus focuses the table, allowing the user to move around the rows and +// interact. +func (m *Model) Focus() { + m.focus = true + m.UpdateViewport() +} + +// Blur blurs the table, preventing selection or movement. +func (m *Model) Blur() { + m.focus = false + m.UpdateViewport() +} + +// View renders the component. +func (m Model) View() string { + return m.headersView() + "\n" + m.viewport.View() +} + +// UpdateViewport updates the list content based on the previously defined +// columns and rows. +func (m *Model) UpdateViewport() { + renderedRows := make([]string, 0, len(m.rows)) + + // Render only rows from: m.cursor-m.viewport.Height to: m.cursor+m.viewport.Height + // Constant runtime, independent of number of rows in a table. + // Limits the number of renderedRows to a maximum of 2*m.viewport.Height + if m.cursor >= 0 { + m.start = clamp(m.cursor-m.viewport.Height, 0, m.cursor) + } else { + m.start = 0 + } + m.end = clamp(m.cursor+m.viewport.Height, m.cursor, len(m.rows)) + for i := m.start; i < m.end; i++ { + renderedRows = append(renderedRows, m.renderRow(i)) + } + + m.viewport.SetContent( + lipgloss.JoinVertical(lipgloss.Left, renderedRows...), + ) +} + +// SelectedRow returns the selected row. +// You can cast it to your own implementation. +func (m Model) SelectedRow() Row { + if m.cursor < 0 || m.cursor >= len(m.rows) { + return nil + } + + return m.rows[m.cursor] +} + +// Rows returns the current rows. +func (m Model) Rows() []Row { + return m.rows +} + +// SetRows sets a new rows state. +func (m *Model) SetRows(r []Row) { + m.rows = r + m.UpdateViewport() +} + +// SetColumns sets a new columns state. +func (m *Model) SetColumns(c []Column) { + m.cols = c + m.UpdateViewport() +} + +// SetWidth sets the width of the viewport of the table. +func (m *Model) SetWidth(w int) { + m.viewport.Width = w + m.UpdateViewport() +} + +// SetHeight sets the height of the viewport of the table. +func (m *Model) SetHeight(h int) { + m.viewport.Height = h + m.UpdateViewport() +} + +// Height returns the viewport height of the table. +func (m Model) Height() int { + return m.viewport.Height +} + +// Width returns the viewport width of the table. +func (m Model) Width() int { + return m.viewport.Width +} + +// Cursor returns the index of the selected row. +func (m Model) Cursor() int { + return m.cursor +} + +// SetCursor sets the cursor position in the table. +func (m *Model) SetCursor(n int) { + m.cursor = clamp(n, 0, len(m.rows)-1) + m.UpdateViewport() +} + +// MoveUp moves the selection up by any number of rows. +// It can not go above the first row. +func (m *Model) MoveUp(n int) { + m.cursor = clamp(m.cursor-n, 0, len(m.rows)-1) + switch { + case m.start == 0: + m.viewport.SetYOffset(clamp(m.viewport.YOffset, 0, m.cursor)) + case m.start < m.viewport.Height: + m.viewport.SetYOffset(clamp(m.viewport.YOffset+n, 0, m.cursor)) + case m.viewport.YOffset >= 1: + m.viewport.YOffset = clamp(m.viewport.YOffset+n, 1, m.viewport.Height) + } + m.UpdateViewport() +} + +// MoveDown moves the selection down by any number of rows. +// It can not go below the last row. +func (m *Model) MoveDown(n int) { + m.cursor = clamp(m.cursor+n, 0, len(m.rows)-1) + m.UpdateViewport() + + switch { + case m.end == len(m.rows): + m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.viewport.Height)) + case m.cursor > (m.end-m.start)/2: + m.viewport.SetYOffset(clamp(m.viewport.YOffset-n, 1, m.cursor)) + case m.viewport.YOffset > 1: + case m.cursor > m.viewport.YOffset+m.viewport.Height-1: + m.viewport.SetYOffset(clamp(m.viewport.YOffset+1, 0, 1)) + } +} + +// GotoTop moves the selection to the first row. +func (m *Model) GotoTop() { + m.MoveUp(m.cursor) +} + +// GotoBottom moves the selection to the last row. +func (m *Model) GotoBottom() { + m.MoveDown(len(m.rows)) +} + +// FromValues create the table rows from a simple string. It uses `\n` by +// default for getting all the rows and the given separator for the fields on +// each row. +func (m *Model) FromValues(value, separator string) { + rows := []Row{} + for _, line := range strings.Split(value, "\n") { + r := Row{} + for _, field := range strings.Split(line, separator) { + r = append(r, field) + } + rows = append(rows, r) + } + + m.SetRows(rows) +} + +func (m Model) headersView() string { + var s = make([]string, 0, len(m.cols)) + for _, col := range m.cols { + style := lipgloss.NewStyle().Width(col.Width).MaxWidth(col.Width).Inline(true) + renderedCell := style.Render(runewidth.Truncate(col.Title, col.Width, "…")) + s = append(s, m.styles.Header.Render(renderedCell)) + } + return lipgloss.JoinHorizontal(lipgloss.Left, s...) +} + +func (m *Model) renderRow(rowID int) string { + var s = make([]string, 0, len(m.cols)) + for i, value := range m.rows[rowID] { + style := lipgloss.NewStyle().Width(m.cols[i].Width).MaxWidth(m.cols[i].Width).Inline(true) + renderedCell := m.styles.Cell.Render(style.Render(runewidth.Truncate(value, m.cols[i].Width, "…"))) + s = append(s, renderedCell) + } + + row := lipgloss.JoinHorizontal(lipgloss.Left, s...) + + if rowID == m.cursor { + return m.styles.Selected.Render(row) + } + + return row +} + +func max(a, b int) int { + if a > b { + return a + } + + return b +} + +func min(a, b int) int { + if a < b { + return a + } + + return b +} + +func clamp(v, low, high int) int { + return min(max(v, low), high) +} diff --git a/vendor/github.com/charmbracelet/bubbles/textarea/memoization/memoization.go b/vendor/github.com/charmbracelet/bubbles/textarea/memoization/memoization.go new file mode 100644 index 00000000..ccb80a9f --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/textarea/memoization/memoization.go @@ -0,0 +1,123 @@ +package memoization + +import ( + "container/list" + "crypto/sha256" + "fmt" + "sync" +) + +// Hasher is an interface that requires a Hash method. The Hash method is +// expected to return a string representation of the hash of the object. +type Hasher interface { + Hash() string +} + +// entry is a struct that holds a key-value pair. It is used as an element +// in the evictionList of the MemoCache. +type entry[T any] struct { + key string + value T +} + +// MemoCache is a struct that represents a cache with a set capacity. It +// uses an LRU (Least Recently Used) eviction policy. It is safe for +// concurrent use. +type MemoCache[H Hasher, T any] struct { + capacity int + mutex sync.Mutex + cache map[string]*list.Element // The cache holding the results + evictionList *list.List // A list to keep track of the order for LRU + hashableItems map[string]T // This map keeps track of the original hashable items (optional) +} + +// NewMemoCache is a function that creates a new MemoCache with a given +// capacity. It returns a pointer to the created MemoCache. +func NewMemoCache[H Hasher, T any](capacity int) *MemoCache[H, T] { + return &MemoCache[H, T]{ + capacity: capacity, + cache: make(map[string]*list.Element), + evictionList: list.New(), + hashableItems: make(map[string]T), + } +} + +// Capacity is a method that returns the capacity of the MemoCache. +func (m *MemoCache[H, T]) Capacity() int { + return m.capacity +} + +// Size is a method that returns the current size of the MemoCache. It is +// the number of items currently stored in the cache. +func (m *MemoCache[H, T]) Size() int { + m.mutex.Lock() + defer m.mutex.Unlock() + return m.evictionList.Len() +} + +// Get is a method that returns the value associated with the given +// hashable item in the MemoCache. If there is no corresponding value, the +// method returns nil. +func (m *MemoCache[H, T]) Get(h H) (T, bool) { + m.mutex.Lock() + defer m.mutex.Unlock() + + hashedKey := h.Hash() + if element, found := m.cache[hashedKey]; found { + m.evictionList.MoveToFront(element) + return element.Value.(*entry[T]).value, true + } + var result T + return result, false +} + +// Set is a method that sets the value for the given hashable item in the +// MemoCache. If the cache is at capacity, it evicts the least recently +// used item before adding the new item. +func (m *MemoCache[H, T]) Set(h H, value T) { + m.mutex.Lock() + defer m.mutex.Unlock() + + hashedKey := h.Hash() + if element, found := m.cache[hashedKey]; found { + m.evictionList.MoveToFront(element) + element.Value.(*entry[T]).value = value + return + } + + // Check if the cache is at capacity + if m.evictionList.Len() >= m.capacity { + // Evict the least recently used item from the cache + toEvict := m.evictionList.Back() + if toEvict != nil { + evictedEntry := m.evictionList.Remove(toEvict).(*entry[T]) + delete(m.cache, evictedEntry.key) + delete(m.hashableItems, evictedEntry.key) // if you're keeping track of original items + } + } + + // Add the value to the cache and the evictionList + newEntry := &entry[T]{ + key: hashedKey, + value: value, + } + element := m.evictionList.PushFront(newEntry) + m.cache[hashedKey] = element + m.hashableItems[hashedKey] = value // if you're keeping track of original items +} + +// HString is a type that implements the Hasher interface for strings. +type HString string + +// Hash is a method that returns the hash of the string. +func (h HString) Hash() string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(h))) +} + +// HInt is a type that implements the Hasher interface for integers. +type HInt int + +// Hash is a method that returns the hash of the integer. +func (h HInt) Hash() string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d", h)))) +} diff --git a/vendor/github.com/charmbracelet/bubbles/textarea/textarea.go b/vendor/github.com/charmbracelet/bubbles/textarea/textarea.go new file mode 100644 index 00000000..ce77c4f5 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/textarea/textarea.go @@ -0,0 +1,1390 @@ +package textarea + +import ( + "crypto/sha256" + "fmt" + "strings" + "unicode" + + "github.com/atotto/clipboard" + "github.com/charmbracelet/bubbles/cursor" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/runeutil" + "github.com/charmbracelet/bubbles/textarea/memoization" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + rw "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" +) + +const ( + minHeight = 1 + minWidth = 2 + defaultHeight = 6 + defaultWidth = 40 + defaultCharLimit = 400 + defaultMaxHeight = 99 + defaultMaxWidth = 500 +) + +// Internal messages for clipboard operations. +type pasteMsg string +type pasteErrMsg struct{ error } + +// KeyMap is the key bindings for different actions within the textarea. +type KeyMap struct { + CharacterBackward key.Binding + CharacterForward key.Binding + DeleteAfterCursor key.Binding + DeleteBeforeCursor key.Binding + DeleteCharacterBackward key.Binding + DeleteCharacterForward key.Binding + DeleteWordBackward key.Binding + DeleteWordForward key.Binding + InsertNewline key.Binding + LineEnd key.Binding + LineNext key.Binding + LinePrevious key.Binding + LineStart key.Binding + Paste key.Binding + WordBackward key.Binding + WordForward key.Binding + InputBegin key.Binding + InputEnd key.Binding + + UppercaseWordForward key.Binding + LowercaseWordForward key.Binding + CapitalizeWordForward key.Binding + + TransposeCharacterBackward key.Binding +} + +// DefaultKeyMap is the default set of key bindings for navigating and acting +// upon the textarea. +var DefaultKeyMap = KeyMap{ + CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f")), + CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b")), + WordForward: key.NewBinding(key.WithKeys("alt+right", "alt+f")), + WordBackward: key.NewBinding(key.WithKeys("alt+left", "alt+b")), + LineNext: key.NewBinding(key.WithKeys("down", "ctrl+n")), + LinePrevious: key.NewBinding(key.WithKeys("up", "ctrl+p")), + DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w")), + DeleteWordForward: key.NewBinding(key.WithKeys("alt+delete", "alt+d")), + DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k")), + DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u")), + InsertNewline: key.NewBinding(key.WithKeys("enter", "ctrl+m")), + DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h")), + DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d")), + LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")), + LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")), + Paste: key.NewBinding(key.WithKeys("ctrl+v")), + InputBegin: key.NewBinding(key.WithKeys("alt+<", "ctrl+home")), + InputEnd: key.NewBinding(key.WithKeys("alt+>", "ctrl+end")), + + CapitalizeWordForward: key.NewBinding(key.WithKeys("alt+c")), + LowercaseWordForward: key.NewBinding(key.WithKeys("alt+l")), + UppercaseWordForward: key.NewBinding(key.WithKeys("alt+u")), + + TransposeCharacterBackward: key.NewBinding(key.WithKeys("ctrl+t")), +} + +// LineInfo is a helper for keeping track of line information regarding +// soft-wrapped lines. +type LineInfo struct { + // Width is the number of columns in the line. + Width int + // CharWidth is the number of characters in the line to account for + // double-width runes. + CharWidth int + // Height is the number of rows in the line. + Height int + // StartColumn is the index of the first column of the line. + StartColumn int + // ColumnOffset is the number of columns that the cursor is offset from the + // start of the line. + ColumnOffset int + // RowOffset is the number of rows that the cursor is offset from the start + // of the line. + RowOffset int + // CharOffset is the number of characters that the cursor is offset + // from the start of the line. This will generally be equivalent to + // ColumnOffset, but will be different there are double-width runes before + // the cursor. + CharOffset int +} + +// Style that will be applied to the text area. +// +// Style can be applied to focused and unfocused states to change the styles +// depending on the focus state. +// +// For an introduction to styling with Lip Gloss see: +// https://github.com/charmbracelet/lipgloss +type Style struct { + Base lipgloss.Style + CursorLine lipgloss.Style + CursorLineNumber lipgloss.Style + EndOfBuffer lipgloss.Style + LineNumber lipgloss.Style + Placeholder lipgloss.Style + Prompt lipgloss.Style + Text lipgloss.Style +} + +// line is the input to the text wrapping function. This is stored in a struct +// so that it can be hashed and memoized. +type line struct { + runes []rune + width int +} + +// Hash returns a hash of the line. +func (w line) Hash() string { + v := fmt.Sprintf("%s:%d", string(w.runes), w.width) + return fmt.Sprintf("%x", sha256.Sum256([]byte(v))) +} + +// Model is the Bubble Tea model for this text area element. +type Model struct { + Err error + + // General settings. + cache *memoization.MemoCache[line, [][]rune] + + // Prompt is printed at the beginning of each line. + // + // When changing the value of Prompt after the model has been + // initialized, ensure that SetWidth() gets called afterwards. + // + // See also SetPromptFunc(). + Prompt string + + // Placeholder is the text displayed when the user + // hasn't entered anything yet. + Placeholder string + + // ShowLineNumbers, if enabled, causes line numbers to be printed + // after the prompt. + ShowLineNumbers bool + + // EndOfBufferCharacter is displayed at the end of the input. + EndOfBufferCharacter rune + + // KeyMap encodes the keybindings recognized by the widget. + KeyMap KeyMap + + // Styling. FocusedStyle and BlurredStyle are used to style the textarea in + // focused and blurred states. + FocusedStyle Style + BlurredStyle Style + // style is the current styling to use. + // It is used to abstract the differences in focus state when styling the + // model, since we can simply assign the set of styles to this variable + // when switching focus states. + style *Style + + // Cursor is the text area cursor. + Cursor cursor.Model + + // CharLimit is the maximum number of characters this input element will + // accept. If 0 or less, there's no limit. + CharLimit int + + // MaxHeight is the maximum height of the text area in rows. If 0 or less, + // there's no limit. + MaxHeight int + + // MaxWidth is the maximum width of the text area in columns. If 0 or less, + // there's no limit. + MaxWidth int + + // If promptFunc is set, it replaces Prompt as a generator for + // prompt strings at the beginning of each line. + promptFunc func(line int) string + + // promptWidth is the width of the prompt. + promptWidth int + + // width is the maximum number of characters that can be displayed at once. + // If 0 or less this setting is ignored. + width int + + // height is the maximum number of lines that can be displayed at once. It + // essentially treats the text field like a vertically scrolling viewport + // if there are more lines than the permitted height. + height int + + // Underlying text value. + value [][]rune + + // focus indicates whether user input focus should be on this input + // component. When false, ignore keyboard input and hide the cursor. + focus bool + + // Cursor column. + col int + + // Cursor row. + row int + + // Last character offset, used to maintain state when the cursor is moved + // vertically such that we can maintain the same navigating position. + lastCharOffset int + + // lineNumberFormat is the format string used to display line numbers. + lineNumberFormat string + + // viewport is the vertically-scrollable viewport of the multi-line text + // input. + viewport *viewport.Model + + // rune sanitizer for input. + rsan runeutil.Sanitizer +} + +// New creates a new model with default settings. +func New() Model { + vp := viewport.New(0, 0) + vp.KeyMap = viewport.KeyMap{} + cur := cursor.New() + + focusedStyle, blurredStyle := DefaultStyles() + + m := Model{ + CharLimit: defaultCharLimit, + MaxHeight: defaultMaxHeight, + MaxWidth: defaultMaxWidth, + Prompt: lipgloss.ThickBorder().Left + " ", + style: &blurredStyle, + FocusedStyle: focusedStyle, + BlurredStyle: blurredStyle, + cache: memoization.NewMemoCache[line, [][]rune](defaultMaxHeight), + EndOfBufferCharacter: '~', + ShowLineNumbers: true, + Cursor: cur, + KeyMap: DefaultKeyMap, + + value: make([][]rune, minHeight, defaultMaxHeight), + focus: false, + col: 0, + row: 0, + lineNumberFormat: "%3v ", + + viewport: &vp, + } + + m.SetHeight(defaultHeight) + m.SetWidth(defaultWidth) + + return m +} + +// DefaultStyles returns the default styles for focused and blurred states for +// the textarea. +func DefaultStyles() (Style, Style) { + focused := Style{ + Base: lipgloss.NewStyle(), + CursorLine: lipgloss.NewStyle().Background(lipgloss.AdaptiveColor{Light: "255", Dark: "0"}), + CursorLineNumber: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "240"}), + EndOfBuffer: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "254", Dark: "0"}), + LineNumber: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "249", Dark: "7"}), + Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), + Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), + Text: lipgloss.NewStyle(), + } + blurred := Style{ + Base: lipgloss.NewStyle(), + CursorLine: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "245", Dark: "7"}), + CursorLineNumber: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "249", Dark: "7"}), + EndOfBuffer: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "254", Dark: "0"}), + LineNumber: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "249", Dark: "7"}), + Placeholder: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), + Prompt: lipgloss.NewStyle().Foreground(lipgloss.Color("7")), + Text: lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "245", Dark: "7"}), + } + + return focused, blurred +} + +// SetValue sets the value of the text input. +func (m *Model) SetValue(s string) { + m.Reset() + m.InsertString(s) +} + +// InsertString inserts a string at the cursor position. +func (m *Model) InsertString(s string) { + m.insertRunesFromUserInput([]rune(s)) +} + +// InsertRune inserts a rune at the cursor position. +func (m *Model) InsertRune(r rune) { + m.insertRunesFromUserInput([]rune{r}) +} + +// insertRunesFromUserInput inserts runes at the current cursor position. +func (m *Model) insertRunesFromUserInput(runes []rune) { + // Clean up any special characters in the input provided by the + // clipboard. This avoids bugs due to e.g. tab characters and + // whatnot. + runes = m.san().Sanitize(runes) + + var availSpace int + if m.CharLimit > 0 { + availSpace = m.CharLimit - m.Length() + // If the char limit's been reached, cancel. + if availSpace <= 0 { + return + } + // If there's not enough space to paste the whole thing cut the pasted + // runes down so they'll fit. + if availSpace < len(runes) { + runes = runes[:availSpace] + } + } + + // Split the input into lines. + var lines [][]rune + lstart := 0 + for i := 0; i < len(runes); i++ { + if runes[i] == '\n' { + // Queue a line to become a new row in the text area below. + // Beware to clamp the max capacity of the slice, to ensure no + // data from different rows get overwritten when later edits + // will modify this line. + lines = append(lines, runes[lstart:i:i]) + lstart = i + 1 + } + } + if lstart <= len(runes) { + // The last line did not end with a newline character. + // Take it now. + lines = append(lines, runes[lstart:]) + } + + // Obey the maximum height limit. + if m.MaxHeight > 0 && len(m.value)+len(lines)-1 > m.MaxHeight { + allowedHeight := max(0, m.MaxHeight-len(m.value)+1) + lines = lines[:allowedHeight] + } + + if len(lines) == 0 { + // Nothing left to insert. + return + } + + // Save the remainder of the original line at the current + // cursor position. + tail := make([]rune, len(m.value[m.row][m.col:])) + copy(tail, m.value[m.row][m.col:]) + + // Paste the first line at the current cursor position. + m.value[m.row] = append(m.value[m.row][:m.col], lines[0]...) + m.col += len(lines[0]) + + if numExtraLines := len(lines) - 1; numExtraLines > 0 { + // Add the new lines. + // We try to reuse the slice if there's already space. + var newGrid [][]rune + if cap(m.value) >= len(m.value)+numExtraLines { + // Can reuse the extra space. + newGrid = m.value[:len(m.value)+numExtraLines] + } else { + // No space left; need a new slice. + newGrid = make([][]rune, len(m.value)+numExtraLines) + copy(newGrid, m.value[:m.row+1]) + } + // Add all the rows that were after the cursor in the original + // grid at the end of the new grid. + copy(newGrid[m.row+1+numExtraLines:], m.value[m.row+1:]) + m.value = newGrid + // Insert all the new lines in the middle. + for _, l := range lines[1:] { + m.row++ + m.value[m.row] = l + m.col = len(l) + } + } + + // Finally add the tail at the end of the last line inserted. + m.value[m.row] = append(m.value[m.row], tail...) + + m.SetCursor(m.col) +} + +// Value returns the value of the text input. +func (m Model) Value() string { + if m.value == nil { + return "" + } + + var v strings.Builder + for _, l := range m.value { + v.WriteString(string(l)) + v.WriteByte('\n') + } + + return strings.TrimSuffix(v.String(), "\n") +} + +// Length returns the number of characters currently in the text input. +func (m *Model) Length() int { + var l int + for _, row := range m.value { + l += uniseg.StringWidth(string(row)) + } + // We add len(m.value) to include the newline characters. + return l + len(m.value) - 1 +} + +// LineCount returns the number of lines that are currently in the text input. +func (m *Model) LineCount() int { + return len(m.value) +} + +// Line returns the line position. +func (m Model) Line() int { + return m.row +} + +// CursorDown moves the cursor down by one line. +// Returns whether or not the cursor blink should be reset. +func (m *Model) CursorDown() { + li := m.LineInfo() + charOffset := max(m.lastCharOffset, li.CharOffset) + m.lastCharOffset = charOffset + + if li.RowOffset+1 >= li.Height && m.row < len(m.value)-1 { + m.row++ + m.col = 0 + } else { + // Move the cursor to the start of the next line so that we can get + // the line information. We need to add 2 columns to account for the + // trailing space wrapping. + m.col = min(li.StartColumn+li.Width+2, len(m.value[m.row])-1) + } + + nli := m.LineInfo() + m.col = nli.StartColumn + + if nli.Width <= 0 { + return + } + + offset := 0 + for offset < charOffset { + if m.row >= len(m.value) || m.col >= len(m.value[m.row]) || offset >= nli.CharWidth-1 { + break + } + offset += rw.RuneWidth(m.value[m.row][m.col]) + m.col++ + } +} + +// CursorUp moves the cursor up by one line. +func (m *Model) CursorUp() { + li := m.LineInfo() + charOffset := max(m.lastCharOffset, li.CharOffset) + m.lastCharOffset = charOffset + + if li.RowOffset <= 0 && m.row > 0 { + m.row-- + m.col = len(m.value[m.row]) + } else { + // Move the cursor to the end of the previous line. + // This can be done by moving the cursor to the start of the line and + // then subtracting 2 to account for the trailing space we keep on + // soft-wrapped lines. + m.col = li.StartColumn - 2 + } + + nli := m.LineInfo() + m.col = nli.StartColumn + + if nli.Width <= 0 { + return + } + + offset := 0 + for offset < charOffset { + if m.col >= len(m.value[m.row]) || offset >= nli.CharWidth-1 { + break + } + offset += rw.RuneWidth(m.value[m.row][m.col]) + m.col++ + } +} + +// SetCursor moves the cursor to the given position. If the position is +// out of bounds the cursor will be moved to the start or end accordingly. +func (m *Model) SetCursor(col int) { + m.col = clamp(col, 0, len(m.value[m.row])) + // Any time that we move the cursor horizontally we need to reset the last + // offset so that the horizontal position when navigating is adjusted. + m.lastCharOffset = 0 +} + +// CursorStart moves the cursor to the start of the input field. +func (m *Model) CursorStart() { + m.SetCursor(0) +} + +// CursorEnd moves the cursor to the end of the input field. +func (m *Model) CursorEnd() { + m.SetCursor(len(m.value[m.row])) +} + +// Focused returns the focus state on the model. +func (m Model) Focused() bool { + return m.focus +} + +// Focus sets the focus state on the model. When the model is in focus it can +// receive keyboard input and the cursor will be hidden. +func (m *Model) Focus() tea.Cmd { + m.focus = true + m.style = &m.FocusedStyle + return m.Cursor.Focus() +} + +// Blur removes the focus state on the model. When the model is blurred it can +// not receive keyboard input and the cursor will be hidden. +func (m *Model) Blur() { + m.focus = false + m.style = &m.BlurredStyle + m.Cursor.Blur() +} + +// Reset sets the input to its default state with no input. +func (m *Model) Reset() { + startCap := m.MaxHeight + if startCap <= 0 { + startCap = defaultMaxHeight + } + m.value = make([][]rune, minHeight, startCap) + m.col = 0 + m.row = 0 + m.viewport.GotoTop() + m.SetCursor(0) +} + +// san initializes or retrieves the rune sanitizer. +func (m *Model) san() runeutil.Sanitizer { + if m.rsan == nil { + // Textinput has all its input on a single line so collapse + // newlines/tabs to single spaces. + m.rsan = runeutil.NewSanitizer() + } + return m.rsan +} + +// deleteBeforeCursor deletes all text before the cursor. Returns whether or +// not the cursor blink should be reset. +func (m *Model) deleteBeforeCursor() { + m.value[m.row] = m.value[m.row][m.col:] + m.SetCursor(0) +} + +// deleteAfterCursor deletes all text after the cursor. Returns whether or not +// the cursor blink should be reset. If input is masked delete everything after +// the cursor so as not to reveal word breaks in the masked input. +func (m *Model) deleteAfterCursor() { + m.value[m.row] = m.value[m.row][:m.col] + m.SetCursor(len(m.value[m.row])) +} + +// transposeLeft exchanges the runes at the cursor and immediately +// before. No-op if the cursor is at the beginning of the line. If +// the cursor is not at the end of the line yet, moves the cursor to +// the right. +func (m *Model) transposeLeft() { + if m.col == 0 || len(m.value[m.row]) < 2 { + return + } + if m.col >= len(m.value[m.row]) { + m.SetCursor(m.col - 1) + } + m.value[m.row][m.col-1], m.value[m.row][m.col] = + m.value[m.row][m.col], m.value[m.row][m.col-1] + if m.col < len(m.value[m.row]) { + m.SetCursor(m.col + 1) + } +} + +// deleteWordLeft deletes the word left to the cursor. Returns whether or not +// the cursor blink should be reset. +func (m *Model) deleteWordLeft() { + if m.col == 0 || len(m.value[m.row]) == 0 { + return + } + + // Linter note: it's critical that we acquire the initial cursor position + // here prior to altering it via SetCursor() below. As such, moving this + // call into the corresponding if clause does not apply here. + oldCol := m.col //nolint:ifshort + + m.SetCursor(m.col - 1) + for unicode.IsSpace(m.value[m.row][m.col]) { + if m.col <= 0 { + break + } + // ignore series of whitespace before cursor + m.SetCursor(m.col - 1) + } + + for m.col > 0 { + if !unicode.IsSpace(m.value[m.row][m.col]) { + m.SetCursor(m.col - 1) + } else { + if m.col > 0 { + // keep the previous space + m.SetCursor(m.col + 1) + } + break + } + } + + if oldCol > len(m.value[m.row]) { + m.value[m.row] = m.value[m.row][:m.col] + } else { + m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][oldCol:]...) + } +} + +// deleteWordRight deletes the word right to the cursor. +func (m *Model) deleteWordRight() { + if m.col >= len(m.value[m.row]) || len(m.value[m.row]) == 0 { + return + } + + oldCol := m.col + + for m.col < len(m.value[m.row]) && unicode.IsSpace(m.value[m.row][m.col]) { + // ignore series of whitespace after cursor + m.SetCursor(m.col + 1) + } + + for m.col < len(m.value[m.row]) { + if !unicode.IsSpace(m.value[m.row][m.col]) { + m.SetCursor(m.col + 1) + } else { + break + } + } + + if m.col > len(m.value[m.row]) { + m.value[m.row] = m.value[m.row][:oldCol] + } else { + m.value[m.row] = append(m.value[m.row][:oldCol], m.value[m.row][m.col:]...) + } + + m.SetCursor(oldCol) +} + +// characterRight moves the cursor one character to the right. +func (m *Model) characterRight() { + if m.col < len(m.value[m.row]) { + m.SetCursor(m.col + 1) + } else { + if m.row < len(m.value)-1 { + m.row++ + m.CursorStart() + } + } +} + +// characterLeft moves the cursor one character to the left. +// If insideLine is set, the cursor is moved to the last +// character in the previous line, instead of one past that. +func (m *Model) characterLeft(insideLine bool) { + if m.col == 0 && m.row != 0 { + m.row-- + m.CursorEnd() + if !insideLine { + return + } + } + if m.col > 0 { + m.SetCursor(m.col - 1) + } +} + +// wordLeft moves the cursor one word to the left. Returns whether or not the +// cursor blink should be reset. If input is masked, move input to the start +// so as not to reveal word breaks in the masked input. +func (m *Model) wordLeft() { + for { + m.characterLeft(true /* insideLine */) + if m.col < len(m.value[m.row]) && !unicode.IsSpace(m.value[m.row][m.col]) { + break + } + } + + for m.col > 0 { + if unicode.IsSpace(m.value[m.row][m.col-1]) { + break + } + m.SetCursor(m.col - 1) + } +} + +// wordRight moves the cursor one word to the right. Returns whether or not the +// cursor blink should be reset. If the input is masked, move input to the end +// so as not to reveal word breaks in the masked input. +func (m *Model) wordRight() { + m.doWordRight(func(int, int) { /* nothing */ }) +} + +func (m *Model) doWordRight(fn func(charIdx int, pos int)) { + // Skip spaces forward. + for { + if m.col < len(m.value[m.row]) && !unicode.IsSpace(m.value[m.row][m.col]) { + break + } + if m.row == len(m.value)-1 && m.col == len(m.value[m.row]) { + // End of text. + break + } + m.characterRight() + } + + charIdx := 0 + for m.col < len(m.value[m.row]) { + if unicode.IsSpace(m.value[m.row][m.col]) { + break + } + fn(charIdx, m.col) + m.SetCursor(m.col + 1) + charIdx++ + } +} + +// uppercaseRight changes the word to the right to uppercase. +func (m *Model) uppercaseRight() { + m.doWordRight(func(_ int, i int) { + m.value[m.row][i] = unicode.ToUpper(m.value[m.row][i]) + }) +} + +// lowercaseRight changes the word to the right to lowercase. +func (m *Model) lowercaseRight() { + m.doWordRight(func(_ int, i int) { + m.value[m.row][i] = unicode.ToLower(m.value[m.row][i]) + }) +} + +// capitalizeRight changes the word to the right to title case. +func (m *Model) capitalizeRight() { + m.doWordRight(func(charIdx int, i int) { + if charIdx == 0 { + m.value[m.row][i] = unicode.ToTitle(m.value[m.row][i]) + } + }) +} + +// LineInfo returns the number of characters from the start of the +// (soft-wrapped) line and the (soft-wrapped) line width. +func (m Model) LineInfo() LineInfo { + grid := m.memoizedWrap(m.value[m.row], m.width) + + // Find out which line we are currently on. This can be determined by the + // m.col and counting the number of runes that we need to skip. + var counter int + for i, line := range grid { + // We've found the line that we are on + if counter+len(line) == m.col && i+1 < len(grid) { + // We wrap around to the next line if we are at the end of the + // previous line so that we can be at the very beginning of the row + return LineInfo{ + CharOffset: 0, + ColumnOffset: 0, + Height: len(grid), + RowOffset: i + 1, + StartColumn: m.col, + Width: len(grid[i+1]), + CharWidth: uniseg.StringWidth(string(line)), + } + } + + if counter+len(line) >= m.col { + return LineInfo{ + CharOffset: uniseg.StringWidth(string(line[:max(0, m.col-counter)])), + ColumnOffset: m.col - counter, + Height: len(grid), + RowOffset: i, + StartColumn: counter, + Width: len(line), + CharWidth: uniseg.StringWidth(string(line)), + } + } + + counter += len(line) + } + return LineInfo{} +} + +// repositionView repositions the view of the viewport based on the defined +// scrolling behavior. +func (m *Model) repositionView() { + min := m.viewport.YOffset + max := min + m.viewport.Height - 1 + + if row := m.cursorLineNumber(); row < min { + m.viewport.LineUp(min - row) + } else if row > max { + m.viewport.LineDown(row - max) + } +} + +// Width returns the width of the textarea. +func (m Model) Width() int { + return m.width +} + +// moveToBegin moves the cursor to the beginning of the input. +func (m *Model) moveToBegin() { + m.row = 0 + m.SetCursor(0) +} + +// moveToEnd moves the cursor to the end of the input. +func (m *Model) moveToEnd() { + m.row = len(m.value) - 1 + m.SetCursor(len(m.value[m.row])) +} + +// SetWidth sets the width of the textarea to fit exactly within the given width. +// This means that the textarea will account for the width of the prompt and +// whether or not line numbers are being shown. +// +// Ensure that SetWidth is called after setting the Prompt and ShowLineNumbers, +// It is important that the width of the textarea be exactly the given width +// and no more. +func (m *Model) SetWidth(w int) { + if m.MaxWidth > 0 { + m.viewport.Width = clamp(w, minWidth, m.MaxWidth) + } else { + m.viewport.Width = max(w, minWidth) + } + + // Since the width of the textarea input is dependent on the width of the + // prompt and line numbers, we need to calculate it by subtracting. + inputWidth := w + if m.ShowLineNumbers { + inputWidth -= uniseg.StringWidth(fmt.Sprintf(m.lineNumberFormat, 0)) + } + + // Account for base style borders and padding. + inputWidth -= m.style.Base.GetHorizontalFrameSize() + + if m.promptFunc == nil { + m.promptWidth = uniseg.StringWidth(m.Prompt) + } + + inputWidth -= m.promptWidth + if m.MaxWidth > 0 { + m.width = clamp(inputWidth, minWidth, m.MaxWidth) + } else { + m.width = max(inputWidth, minWidth) + } +} + +// SetPromptFunc supersedes the Prompt field and sets a dynamic prompt +// instead. +// If the function returns a prompt that is shorter than the +// specified promptWidth, it will be padded to the left. +// If it returns a prompt that is longer, display artifacts +// may occur; the caller is responsible for computing an adequate +// promptWidth. +func (m *Model) SetPromptFunc(promptWidth int, fn func(lineIdx int) string) { + m.promptFunc = fn + m.promptWidth = promptWidth +} + +// Height returns the current height of the textarea. +func (m Model) Height() int { + return m.height +} + +// SetHeight sets the height of the textarea. +func (m *Model) SetHeight(h int) { + if m.MaxHeight > 0 { + m.height = clamp(h, minHeight, m.MaxHeight) + m.viewport.Height = clamp(h, minHeight, m.MaxHeight) + } else { + m.height = max(h, minHeight) + m.viewport.Height = max(h, minHeight) + } +} + +// Update is the Bubble Tea update loop. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + if !m.focus { + m.Cursor.Blur() + return m, nil + } + + // Used to determine if the cursor should blink. + oldRow, oldCol := m.cursorLineNumber(), m.col + + var cmds []tea.Cmd + + if m.value[m.row] == nil { + m.value[m.row] = make([]rune, 0) + } + + if m.MaxHeight > 0 && m.MaxHeight != m.cache.Capacity() { + m.cache = memoization.NewMemoCache[line, [][]rune](m.MaxHeight) + } + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.KeyMap.DeleteAfterCursor): + m.col = clamp(m.col, 0, len(m.value[m.row])) + if m.col >= len(m.value[m.row]) { + m.mergeLineBelow(m.row) + break + } + m.deleteAfterCursor() + case key.Matches(msg, m.KeyMap.DeleteBeforeCursor): + m.col = clamp(m.col, 0, len(m.value[m.row])) + if m.col <= 0 { + m.mergeLineAbove(m.row) + break + } + m.deleteBeforeCursor() + case key.Matches(msg, m.KeyMap.DeleteCharacterBackward): + m.col = clamp(m.col, 0, len(m.value[m.row])) + if m.col <= 0 { + m.mergeLineAbove(m.row) + break + } + if len(m.value[m.row]) > 0 { + m.value[m.row] = append(m.value[m.row][:max(0, m.col-1)], m.value[m.row][m.col:]...) + if m.col > 0 { + m.SetCursor(m.col - 1) + } + } + case key.Matches(msg, m.KeyMap.DeleteCharacterForward): + if len(m.value[m.row]) > 0 && m.col < len(m.value[m.row]) { + m.value[m.row] = append(m.value[m.row][:m.col], m.value[m.row][m.col+1:]...) + } + if m.col >= len(m.value[m.row]) { + m.mergeLineBelow(m.row) + break + } + case key.Matches(msg, m.KeyMap.DeleteWordBackward): + if m.col <= 0 { + m.mergeLineAbove(m.row) + break + } + m.deleteWordLeft() + case key.Matches(msg, m.KeyMap.DeleteWordForward): + m.col = clamp(m.col, 0, len(m.value[m.row])) + if m.col >= len(m.value[m.row]) { + m.mergeLineBelow(m.row) + break + } + m.deleteWordRight() + case key.Matches(msg, m.KeyMap.InsertNewline): + if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight { + return m, nil + } + m.col = clamp(m.col, 0, len(m.value[m.row])) + m.splitLine(m.row, m.col) + case key.Matches(msg, m.KeyMap.LineEnd): + m.CursorEnd() + case key.Matches(msg, m.KeyMap.LineStart): + m.CursorStart() + case key.Matches(msg, m.KeyMap.CharacterForward): + m.characterRight() + case key.Matches(msg, m.KeyMap.LineNext): + m.CursorDown() + case key.Matches(msg, m.KeyMap.WordForward): + m.wordRight() + case key.Matches(msg, m.KeyMap.Paste): + return m, Paste + case key.Matches(msg, m.KeyMap.CharacterBackward): + m.characterLeft(false /* insideLine */) + case key.Matches(msg, m.KeyMap.LinePrevious): + m.CursorUp() + case key.Matches(msg, m.KeyMap.WordBackward): + m.wordLeft() + case key.Matches(msg, m.KeyMap.InputBegin): + m.moveToBegin() + case key.Matches(msg, m.KeyMap.InputEnd): + m.moveToEnd() + case key.Matches(msg, m.KeyMap.LowercaseWordForward): + m.lowercaseRight() + case key.Matches(msg, m.KeyMap.UppercaseWordForward): + m.uppercaseRight() + case key.Matches(msg, m.KeyMap.CapitalizeWordForward): + m.capitalizeRight() + case key.Matches(msg, m.KeyMap.TransposeCharacterBackward): + m.transposeLeft() + + default: + m.insertRunesFromUserInput(msg.Runes) + } + + case pasteMsg: + m.insertRunesFromUserInput([]rune(msg)) + + case pasteErrMsg: + m.Err = msg + } + + vp, cmd := m.viewport.Update(msg) + m.viewport = &vp + cmds = append(cmds, cmd) + + newRow, newCol := m.cursorLineNumber(), m.col + m.Cursor, cmd = m.Cursor.Update(msg) + if (newRow != oldRow || newCol != oldCol) && m.Cursor.Mode() == cursor.CursorBlink { + m.Cursor.Blink = false + cmd = m.Cursor.BlinkCmd() + } + cmds = append(cmds, cmd) + + m.repositionView() + + return m, tea.Batch(cmds...) +} + +// View renders the text area in its current state. +func (m Model) View() string { + if m.Value() == "" && m.row == 0 && m.col == 0 && m.Placeholder != "" { + return m.placeholderView() + } + m.Cursor.TextStyle = m.style.CursorLine + + var s strings.Builder + var style lipgloss.Style + lineInfo := m.LineInfo() + + var newLines int + + displayLine := 0 + for l, line := range m.value { + wrappedLines := m.memoizedWrap(line, m.width) + + if m.row == l { + style = m.style.CursorLine + } else { + style = m.style.Text + } + + for wl, wrappedLine := range wrappedLines { + prompt := m.getPromptString(displayLine) + prompt = m.style.Prompt.Render(prompt) + s.WriteString(style.Render(prompt)) + displayLine++ + + if m.ShowLineNumbers { + if wl == 0 { + if m.row == l { + s.WriteString(style.Render(m.style.CursorLineNumber.Render(fmt.Sprintf(m.lineNumberFormat, l+1)))) + } else { + s.WriteString(style.Render(m.style.LineNumber.Render(fmt.Sprintf(m.lineNumberFormat, l+1)))) + } + } else { + if m.row == l { + s.WriteString(style.Render(m.style.CursorLineNumber.Render(fmt.Sprintf(m.lineNumberFormat, " ")))) + } else { + s.WriteString(style.Render(m.style.LineNumber.Render(fmt.Sprintf(m.lineNumberFormat, " ")))) + } + } + } + + strwidth := uniseg.StringWidth(string(wrappedLine)) + padding := m.width - strwidth + // If the trailing space causes the line to be wider than the + // width, we should not draw it to the screen since it will result + // in an extra space at the end of the line which can look off when + // the cursor line is showing. + if strwidth > m.width { + // The character causing the line to be wider than the width is + // guaranteed to be a space since any other character would + // have been wrapped. + wrappedLine = []rune(strings.TrimSuffix(string(wrappedLine), " ")) + padding -= m.width - strwidth + } + if m.row == l && lineInfo.RowOffset == wl { + s.WriteString(style.Render(string(wrappedLine[:lineInfo.ColumnOffset]))) + if m.col >= len(line) && lineInfo.CharOffset >= m.width { + m.Cursor.SetChar(" ") + s.WriteString(m.Cursor.View()) + } else { + m.Cursor.SetChar(string(wrappedLine[lineInfo.ColumnOffset])) + s.WriteString(style.Render(m.Cursor.View())) + s.WriteString(style.Render(string(wrappedLine[lineInfo.ColumnOffset+1:]))) + } + } else { + s.WriteString(style.Render(string(wrappedLine))) + } + s.WriteString(style.Render(strings.Repeat(" ", max(0, padding)))) + s.WriteRune('\n') + newLines++ + } + } + + // Always show at least `m.Height` lines at all times. + // To do this we can simply pad out a few extra new lines in the view. + for i := 0; i < m.height; i++ { + prompt := m.getPromptString(displayLine) + prompt = m.style.Prompt.Render(prompt) + s.WriteString(prompt) + displayLine++ + + if m.ShowLineNumbers { + lineNumber := m.style.EndOfBuffer.Render((fmt.Sprintf(m.lineNumberFormat, string(m.EndOfBufferCharacter)))) + s.WriteString(lineNumber) + } + s.WriteRune('\n') + } + + m.viewport.SetContent(s.String()) + return m.style.Base.Render(m.viewport.View()) +} + +func (m Model) getPromptString(displayLine int) (prompt string) { + prompt = m.Prompt + if m.promptFunc == nil { + return prompt + } + prompt = m.promptFunc(displayLine) + pl := uniseg.StringWidth(prompt) + if pl < m.promptWidth { + prompt = fmt.Sprintf("%*s%s", m.promptWidth-pl, "", prompt) + } + return prompt +} + +// placeholderView returns the prompt and placeholder view, if any. +func (m Model) placeholderView() string { + var ( + s strings.Builder + p = rw.Truncate(m.Placeholder, m.width, "...") + style = m.style.Placeholder.Inline(true) + ) + + prompt := m.getPromptString(0) + prompt = m.style.Prompt.Render(prompt) + s.WriteString(m.style.CursorLine.Render(prompt)) + + if m.ShowLineNumbers { + s.WriteString(m.style.CursorLine.Render(m.style.CursorLineNumber.Render((fmt.Sprintf(m.lineNumberFormat, 1))))) + } + + m.Cursor.TextStyle = m.style.Placeholder + m.Cursor.SetChar(string(p[0])) + s.WriteString(m.style.CursorLine.Render(m.Cursor.View())) + + // The rest of the placeholder text + s.WriteString(m.style.CursorLine.Render(style.Render(p[1:] + strings.Repeat(" ", max(0, m.width-uniseg.StringWidth(p)))))) + + // The rest of the new lines + for i := 1; i < m.height; i++ { + s.WriteRune('\n') + prompt := m.getPromptString(i) + prompt = m.style.Prompt.Render(prompt) + s.WriteString(prompt) + + if m.ShowLineNumbers { + eob := m.style.EndOfBuffer.Render((fmt.Sprintf(m.lineNumberFormat, string(m.EndOfBufferCharacter)))) + s.WriteString(eob) + } + } + + m.viewport.SetContent(s.String()) + return m.style.Base.Render(m.viewport.View()) +} + +// Blink returns the blink command for the cursor. +func Blink() tea.Msg { + return cursor.Blink() +} + +func (m Model) memoizedWrap(runes []rune, width int) [][]rune { + input := line{runes: runes, width: width} + if v, ok := m.cache.Get(input); ok { + return v + } + v := wrap(runes, width) + m.cache.Set(input, v) + return v +} + +// cursorLineNumber returns the line number that the cursor is on. +// This accounts for soft wrapped lines. +func (m Model) cursorLineNumber() int { + line := 0 + for i := 0; i < m.row; i++ { + // Calculate the number of lines that the current line will be split + // into. + line += len(m.memoizedWrap(m.value[i], m.width)) + } + line += m.LineInfo().RowOffset + return line +} + +// mergeLineBelow merges the current line the cursor is on with the line below. +func (m *Model) mergeLineBelow(row int) { + if row >= len(m.value)-1 { + return + } + + // To perform a merge, we will need to combine the two lines and then + m.value[row] = append(m.value[row], m.value[row+1]...) + + // Shift all lines up by one + for i := row + 1; i < len(m.value)-1; i++ { + m.value[i] = m.value[i+1] + } + + // And, remove the last line + if len(m.value) > 0 { + m.value = m.value[:len(m.value)-1] + } +} + +// mergeLineAbove merges the current line the cursor is on with the line above. +func (m *Model) mergeLineAbove(row int) { + if row <= 0 { + return + } + + m.col = len(m.value[row-1]) + m.row = m.row - 1 + + // To perform a merge, we will need to combine the two lines and then + m.value[row-1] = append(m.value[row-1], m.value[row]...) + + // Shift all lines up by one + for i := row; i < len(m.value)-1; i++ { + m.value[i] = m.value[i+1] + } + + // And, remove the last line + if len(m.value) > 0 { + m.value = m.value[:len(m.value)-1] + } +} + +func (m *Model) splitLine(row, col int) { + // To perform a split, take the current line and keep the content before + // the cursor, take the content after the cursor and make it the content of + // the line underneath, and shift the remaining lines down by one + head, tailSrc := m.value[row][:col], m.value[row][col:] + tail := make([]rune, len(tailSrc)) + copy(tail, tailSrc) + + m.value = append(m.value[:row+1], m.value[row:]...) + + m.value[row] = head + m.value[row+1] = tail + + m.col = 0 + m.row++ +} + +// Paste is a command for pasting from the clipboard into the text input. +func Paste() tea.Msg { + str, err := clipboard.ReadAll() + if err != nil { + return pasteErrMsg{err} + } + return pasteMsg(str) +} + +func wrap(runes []rune, width int) [][]rune { + var ( + lines = [][]rune{{}} + word = []rune{} + row int + spaces int + ) + + // Word wrap the runes + for _, r := range runes { + if unicode.IsSpace(r) { + spaces++ + } else { + word = append(word, r) + } + + if spaces > 0 { + if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces > width { + row++ + lines = append(lines, []rune{}) + lines[row] = append(lines[row], word...) + lines[row] = append(lines[row], repeatSpaces(spaces)...) + spaces = 0 + word = nil + } else { + lines[row] = append(lines[row], word...) + lines[row] = append(lines[row], repeatSpaces(spaces)...) + spaces = 0 + word = nil + } + } else { + // If the last character is a double-width rune, then we may not be able to add it to this line + // as it might cause us to go past the width. + lastCharLen := rw.RuneWidth(word[len(word)-1]) + if uniseg.StringWidth(string(word))+lastCharLen > width { + // If the current line has any content, let's move to the next + // line because the current word fills up the entire line. + if len(lines[row]) > 0 { + row++ + lines = append(lines, []rune{}) + } + lines[row] = append(lines[row], word...) + word = nil + } + } + } + + if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces >= width { + lines = append(lines, []rune{}) + lines[row+1] = append(lines[row+1], word...) + // We add an extra space at the end of the line to account for the + // trailing space at the end of the previous soft-wrapped lines so that + // behaviour when navigating is consistent and so that we don't need to + // continually add edges to handle the last line of the wrapped input. + spaces++ + lines[row+1] = append(lines[row+1], repeatSpaces(spaces)...) + } else { + lines[row] = append(lines[row], word...) + spaces++ + lines[row] = append(lines[row], repeatSpaces(spaces)...) + } + + return lines +} + +func repeatSpaces(n int) []rune { + return []rune(strings.Repeat(string(' '), n)) +} + +func clamp(v, low, high int) int { + if high < low { + low, high = high, low + } + return min(high, max(low, v)) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go b/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go new file mode 100644 index 00000000..501f9a79 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/textinput/textinput.go @@ -0,0 +1,882 @@ +package textinput + +import ( + "reflect" + "strings" + "time" + "unicode" + + "github.com/atotto/clipboard" + "github.com/charmbracelet/bubbles/cursor" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/runeutil" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + rw "github.com/mattn/go-runewidth" + "github.com/rivo/uniseg" +) + +// Internal messages for clipboard operations. +type pasteMsg string +type pasteErrMsg struct{ error } + +// EchoMode sets the input behavior of the text input field. +type EchoMode int + +const ( + // EchoNormal displays text as is. This is the default behavior. + EchoNormal EchoMode = iota + + // EchoPassword displays the EchoCharacter mask instead of actual + // characters. This is commonly used for password fields. + EchoPassword + + // EchoNone displays nothing as characters are entered. This is commonly + // seen for password fields on the command line. + EchoNone +) + +// ValidateFunc is a function that returns an error if the input is invalid. +type ValidateFunc func(string) error + +// KeyMap is the key bindings for different actions within the textinput. +type KeyMap struct { + CharacterForward key.Binding + CharacterBackward key.Binding + WordForward key.Binding + WordBackward key.Binding + DeleteWordBackward key.Binding + DeleteWordForward key.Binding + DeleteAfterCursor key.Binding + DeleteBeforeCursor key.Binding + DeleteCharacterBackward key.Binding + DeleteCharacterForward key.Binding + LineStart key.Binding + LineEnd key.Binding + Paste key.Binding + AcceptSuggestion key.Binding + NextSuggestion key.Binding + PrevSuggestion key.Binding +} + +// DefaultKeyMap is the default set of key bindings for navigating and acting +// upon the textinput. +var DefaultKeyMap = KeyMap{ + CharacterForward: key.NewBinding(key.WithKeys("right", "ctrl+f")), + CharacterBackward: key.NewBinding(key.WithKeys("left", "ctrl+b")), + WordForward: key.NewBinding(key.WithKeys("alt+right", "alt+f")), + WordBackward: key.NewBinding(key.WithKeys("alt+left", "alt+b")), + DeleteWordBackward: key.NewBinding(key.WithKeys("alt+backspace", "ctrl+w")), + DeleteWordForward: key.NewBinding(key.WithKeys("alt+delete", "alt+d")), + DeleteAfterCursor: key.NewBinding(key.WithKeys("ctrl+k")), + DeleteBeforeCursor: key.NewBinding(key.WithKeys("ctrl+u")), + DeleteCharacterBackward: key.NewBinding(key.WithKeys("backspace", "ctrl+h")), + DeleteCharacterForward: key.NewBinding(key.WithKeys("delete", "ctrl+d")), + LineStart: key.NewBinding(key.WithKeys("home", "ctrl+a")), + LineEnd: key.NewBinding(key.WithKeys("end", "ctrl+e")), + Paste: key.NewBinding(key.WithKeys("ctrl+v")), + AcceptSuggestion: key.NewBinding(key.WithKeys("tab")), + NextSuggestion: key.NewBinding(key.WithKeys("down", "ctrl+n")), + PrevSuggestion: key.NewBinding(key.WithKeys("up", "ctrl+p")), +} + +// Model is the Bubble Tea model for this text input element. +type Model struct { + Err error + + // General settings. + Prompt string + Placeholder string + EchoMode EchoMode + EchoCharacter rune + Cursor cursor.Model + + // Deprecated: use [cursor.BlinkSpeed] instead. + BlinkSpeed time.Duration + + // Styles. These will be applied as inline styles. + // + // For an introduction to styling with Lip Gloss see: + // https://github.com/charmbracelet/lipgloss + PromptStyle lipgloss.Style + TextStyle lipgloss.Style + PlaceholderStyle lipgloss.Style + CompletionStyle lipgloss.Style + + // Deprecated: use Cursor.Style instead. + CursorStyle lipgloss.Style + + // CharLimit is the maximum amount of characters this input element will + // accept. If 0 or less, there's no limit. + CharLimit int + + // Width is the maximum number of characters that can be displayed at once. + // It essentially treats the text field like a horizontally scrolling + // viewport. If 0 or less this setting is ignored. + Width int + + // KeyMap encodes the keybindings recognized by the widget. + KeyMap KeyMap + + // Underlying text value. + value []rune + + // focus indicates whether user input focus should be on this input + // component. When false, ignore keyboard input and hide the cursor. + focus bool + + // Cursor position. + pos int + + // Used to emulate a viewport when width is set and the content is + // overflowing. + offset int + offsetRight int + + // Validate is a function that checks whether or not the text within the + // input is valid. If it is not valid, the `Err` field will be set to the + // error returned by the function. If the function is not defined, all + // input is considered valid. + Validate ValidateFunc + + // rune sanitizer for input. + rsan runeutil.Sanitizer + + // Should the input suggest to complete + ShowSuggestions bool + + // suggestions is a list of suggestions that may be used to complete the + // input. + suggestions [][]rune + matchedSuggestions [][]rune + currentSuggestionIndex int +} + +// New creates a new model with default settings. +func New() Model { + return Model{ + Prompt: "> ", + EchoCharacter: '*', + CharLimit: 0, + PlaceholderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), + ShowSuggestions: false, + CompletionStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("240")), + Cursor: cursor.New(), + KeyMap: DefaultKeyMap, + + suggestions: [][]rune{}, + value: nil, + focus: false, + pos: 0, + } +} + +// NewModel creates a new model with default settings. +// +// Deprecated: Use [New] instead. +var NewModel = New + +// SetValue sets the value of the text input. +func (m *Model) SetValue(s string) { + // Clean up any special characters in the input provided by the + // caller. This avoids bugs due to e.g. tab characters and whatnot. + runes := m.san().Sanitize([]rune(s)) + m.setValueInternal(runes) +} + +func (m *Model) setValueInternal(runes []rune) { + if m.Validate != nil { + if err := m.Validate(string(runes)); err != nil { + m.Err = err + return + } + } + + empty := len(m.value) == 0 + m.Err = nil + + if m.CharLimit > 0 && len(runes) > m.CharLimit { + m.value = runes[:m.CharLimit] + } else { + m.value = runes + } + if (m.pos == 0 && empty) || m.pos > len(m.value) { + m.SetCursor(len(m.value)) + } + m.handleOverflow() +} + +// Value returns the value of the text input. +func (m Model) Value() string { + return string(m.value) +} + +// Position returns the cursor position. +func (m Model) Position() int { + return m.pos +} + +// SetCursor moves the cursor to the given position. If the position is +// out of bounds the cursor will be moved to the start or end accordingly. +func (m *Model) SetCursor(pos int) { + m.pos = clamp(pos, 0, len(m.value)) + m.handleOverflow() +} + +// CursorStart moves the cursor to the start of the input field. +func (m *Model) CursorStart() { + m.SetCursor(0) +} + +// CursorEnd moves the cursor to the end of the input field. +func (m *Model) CursorEnd() { + m.SetCursor(len(m.value)) +} + +// Focused returns the focus state on the model. +func (m Model) Focused() bool { + return m.focus +} + +// Focus sets the focus state on the model. When the model is in focus it can +// receive keyboard input and the cursor will be shown. +func (m *Model) Focus() tea.Cmd { + m.focus = true + return m.Cursor.Focus() +} + +// Blur removes the focus state on the model. When the model is blurred it can +// not receive keyboard input and the cursor will be hidden. +func (m *Model) Blur() { + m.focus = false + m.Cursor.Blur() +} + +// Reset sets the input to its default state with no input. +func (m *Model) Reset() { + m.value = nil + m.SetCursor(0) +} + +// SetSuggestions sets the suggestions for the input. +func (m *Model) SetSuggestions(suggestions []string) { + m.suggestions = make([][]rune, len(suggestions)) + for i, s := range suggestions { + m.suggestions[i] = []rune(s) + } + + m.updateSuggestions() +} + +// rsan initializes or retrieves the rune sanitizer. +func (m *Model) san() runeutil.Sanitizer { + if m.rsan == nil { + // Textinput has all its input on a single line so collapse + // newlines/tabs to single spaces. + m.rsan = runeutil.NewSanitizer( + runeutil.ReplaceTabs(" "), runeutil.ReplaceNewlines(" ")) + } + return m.rsan +} + +func (m *Model) insertRunesFromUserInput(v []rune) { + // Clean up any special characters in the input provided by the + // clipboard. This avoids bugs due to e.g. tab characters and + // whatnot. + paste := m.san().Sanitize(v) + + var availSpace int + if m.CharLimit > 0 { + availSpace = m.CharLimit - len(m.value) + + // If the char limit's been reached, cancel. + if availSpace <= 0 { + return + } + + // If there's not enough space to paste the whole thing cut the pasted + // runes down so they'll fit. + if availSpace < len(paste) { + paste = paste[:availSpace] + } + } + + // Stuff before and after the cursor + head := m.value[:m.pos] + tailSrc := m.value[m.pos:] + tail := make([]rune, len(tailSrc)) + copy(tail, tailSrc) + + oldPos := m.pos + + // Insert pasted runes + for _, r := range paste { + head = append(head, r) + m.pos++ + if m.CharLimit > 0 { + availSpace-- + if availSpace <= 0 { + break + } + } + } + + // Put it all back together + value := append(head, tail...) + m.setValueInternal(value) + + if m.Err != nil { + m.pos = oldPos + } +} + +// If a max width is defined, perform some logic to treat the visible area +// as a horizontally scrolling viewport. +func (m *Model) handleOverflow() { + if m.Width <= 0 || uniseg.StringWidth(string(m.value)) <= m.Width { + m.offset = 0 + m.offsetRight = len(m.value) + return + } + + // Correct right offset if we've deleted characters + m.offsetRight = min(m.offsetRight, len(m.value)) + + if m.pos < m.offset { + m.offset = m.pos + + w := 0 + i := 0 + runes := m.value[m.offset:] + + for i < len(runes) && w <= m.Width { + w += rw.RuneWidth(runes[i]) + if w <= m.Width+1 { + i++ + } + } + + m.offsetRight = m.offset + i + } else if m.pos >= m.offsetRight { + m.offsetRight = m.pos + + w := 0 + runes := m.value[:m.offsetRight] + i := len(runes) - 1 + + for i > 0 && w < m.Width { + w += rw.RuneWidth(runes[i]) + if w <= m.Width { + i-- + } + } + + m.offset = m.offsetRight - (len(runes) - 1 - i) + } +} + +// deleteBeforeCursor deletes all text before the cursor. +func (m *Model) deleteBeforeCursor() { + m.value = m.value[m.pos:] + m.offset = 0 + m.SetCursor(0) +} + +// deleteAfterCursor deletes all text after the cursor. If input is masked +// delete everything after the cursor so as not to reveal word breaks in the +// masked input. +func (m *Model) deleteAfterCursor() { + m.value = m.value[:m.pos] + m.SetCursor(len(m.value)) +} + +// deleteWordBackward deletes the word left to the cursor. +func (m *Model) deleteWordBackward() { + if m.pos == 0 || len(m.value) == 0 { + return + } + + if m.EchoMode != EchoNormal { + m.deleteBeforeCursor() + return + } + + // Linter note: it's critical that we acquire the initial cursor position + // here prior to altering it via SetCursor() below. As such, moving this + // call into the corresponding if clause does not apply here. + oldPos := m.pos //nolint:ifshort + + m.SetCursor(m.pos - 1) + for unicode.IsSpace(m.value[m.pos]) { + if m.pos <= 0 { + break + } + // ignore series of whitespace before cursor + m.SetCursor(m.pos - 1) + } + + for m.pos > 0 { + if !unicode.IsSpace(m.value[m.pos]) { + m.SetCursor(m.pos - 1) + } else { + if m.pos > 0 { + // keep the previous space + m.SetCursor(m.pos + 1) + } + break + } + } + + if oldPos > len(m.value) { + m.value = m.value[:m.pos] + } else { + m.value = append(m.value[:m.pos], m.value[oldPos:]...) + } +} + +// deleteWordForward deletes the word right to the cursor. If input is masked +// delete everything after the cursor so as not to reveal word breaks in the +// masked input. +func (m *Model) deleteWordForward() { + if m.pos >= len(m.value) || len(m.value) == 0 { + return + } + + if m.EchoMode != EchoNormal { + m.deleteAfterCursor() + return + } + + oldPos := m.pos + m.SetCursor(m.pos + 1) + for unicode.IsSpace(m.value[m.pos]) { + // ignore series of whitespace after cursor + m.SetCursor(m.pos + 1) + + if m.pos >= len(m.value) { + break + } + } + + for m.pos < len(m.value) { + if !unicode.IsSpace(m.value[m.pos]) { + m.SetCursor(m.pos + 1) + } else { + break + } + } + + if m.pos > len(m.value) { + m.value = m.value[:oldPos] + } else { + m.value = append(m.value[:oldPos], m.value[m.pos:]...) + } + + m.SetCursor(oldPos) +} + +// wordBackward moves the cursor one word to the left. If input is masked, move +// input to the start so as not to reveal word breaks in the masked input. +func (m *Model) wordBackward() { + if m.pos == 0 || len(m.value) == 0 { + return + } + + if m.EchoMode != EchoNormal { + m.CursorStart() + return + } + + i := m.pos - 1 + for i >= 0 { + if unicode.IsSpace(m.value[i]) { + m.SetCursor(m.pos - 1) + i-- + } else { + break + } + } + + for i >= 0 { + if !unicode.IsSpace(m.value[i]) { + m.SetCursor(m.pos - 1) + i-- + } else { + break + } + } +} + +// wordForward moves the cursor one word to the right. If the input is masked, +// move input to the end so as not to reveal word breaks in the masked input. +func (m *Model) wordForward() { + if m.pos >= len(m.value) || len(m.value) == 0 { + return + } + + if m.EchoMode != EchoNormal { + m.CursorEnd() + return + } + + i := m.pos + for i < len(m.value) { + if unicode.IsSpace(m.value[i]) { + m.SetCursor(m.pos + 1) + i++ + } else { + break + } + } + + for i < len(m.value) { + if !unicode.IsSpace(m.value[i]) { + m.SetCursor(m.pos + 1) + i++ + } else { + break + } + } +} + +func (m Model) echoTransform(v string) string { + switch m.EchoMode { + case EchoPassword: + return strings.Repeat(string(m.EchoCharacter), uniseg.StringWidth(v)) + case EchoNone: + return "" + case EchoNormal: + return v + default: + return v + } +} + +// Update is the Bubble Tea update loop. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + if !m.focus { + return m, nil + } + + // Need to check for completion before, because key is configurable and might be double assigned + keyMsg, ok := msg.(tea.KeyMsg) + if ok && key.Matches(keyMsg, m.KeyMap.AcceptSuggestion) { + if m.canAcceptSuggestion() { + m.value = append(m.value, m.matchedSuggestions[m.currentSuggestionIndex][len(m.value):]...) + m.CursorEnd() + } + } + + // Let's remember where the position of the cursor currently is so that if + // the cursor position changes, we can reset the blink. + oldPos := m.pos //nolint + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.KeyMap.DeleteWordBackward): + m.Err = nil + m.deleteWordBackward() + case key.Matches(msg, m.KeyMap.DeleteCharacterBackward): + m.Err = nil + if len(m.value) > 0 { + m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...) + if m.pos > 0 { + m.SetCursor(m.pos - 1) + } + } + case key.Matches(msg, m.KeyMap.WordBackward): + m.wordBackward() + case key.Matches(msg, m.KeyMap.CharacterBackward): + if m.pos > 0 { + m.SetCursor(m.pos - 1) + } + case key.Matches(msg, m.KeyMap.WordForward): + m.wordForward() + case key.Matches(msg, m.KeyMap.CharacterForward): + if m.pos < len(m.value) { + m.SetCursor(m.pos + 1) + } + case key.Matches(msg, m.KeyMap.DeleteWordBackward): + m.deleteWordBackward() + case key.Matches(msg, m.KeyMap.LineStart): + m.CursorStart() + case key.Matches(msg, m.KeyMap.DeleteCharacterForward): + if len(m.value) > 0 && m.pos < len(m.value) { + m.value = append(m.value[:m.pos], m.value[m.pos+1:]...) + } + case key.Matches(msg, m.KeyMap.LineEnd): + m.CursorEnd() + case key.Matches(msg, m.KeyMap.DeleteAfterCursor): + m.deleteAfterCursor() + case key.Matches(msg, m.KeyMap.DeleteBeforeCursor): + m.deleteBeforeCursor() + case key.Matches(msg, m.KeyMap.Paste): + return m, Paste + case key.Matches(msg, m.KeyMap.DeleteWordForward): + m.deleteWordForward() + case key.Matches(msg, m.KeyMap.NextSuggestion): + m.nextSuggestion() + case key.Matches(msg, m.KeyMap.PrevSuggestion): + m.previousSuggestion() + default: + // Input one or more regular characters. + m.insertRunesFromUserInput(msg.Runes) + } + + // Check again if can be completed + // because value might be something that does not match the completion prefix + m.updateSuggestions() + + case pasteMsg: + m.insertRunesFromUserInput([]rune(msg)) + + case pasteErrMsg: + m.Err = msg + } + + var cmds []tea.Cmd + var cmd tea.Cmd + + m.Cursor, cmd = m.Cursor.Update(msg) + cmds = append(cmds, cmd) + + if oldPos != m.pos && m.Cursor.Mode() == cursor.CursorBlink { + m.Cursor.Blink = false + cmds = append(cmds, m.Cursor.BlinkCmd()) + } + + m.handleOverflow() + return m, tea.Batch(cmds...) +} + +// View renders the textinput in its current state. +func (m Model) View() string { + // Placeholder text + if len(m.value) == 0 && m.Placeholder != "" { + return m.placeholderView() + } + + styleText := m.TextStyle.Inline(true).Render + + value := m.value[m.offset:m.offsetRight] + pos := max(0, m.pos-m.offset) + v := styleText(m.echoTransform(string(value[:pos]))) + + if pos < len(value) { + char := m.echoTransform(string(value[pos])) + m.Cursor.SetChar(char) + v += m.Cursor.View() // cursor and text under it + v += styleText(m.echoTransform(string(value[pos+1:]))) // text after cursor + v += m.completionView(0) // suggested completion + } else { + if m.canAcceptSuggestion() { + suggestion := m.matchedSuggestions[m.currentSuggestionIndex] + if len(value) < len(suggestion) { + m.Cursor.TextStyle = m.CompletionStyle + m.Cursor.SetChar(m.echoTransform(string(suggestion[pos]))) + v += m.Cursor.View() + v += m.completionView(1) + } else { + m.Cursor.SetChar(" ") + v += m.Cursor.View() + } + } else { + m.Cursor.SetChar(" ") + v += m.Cursor.View() + } + } + + // If a max width and background color were set fill the empty spaces with + // the background color. + valWidth := uniseg.StringWidth(string(value)) + if m.Width > 0 && valWidth <= m.Width { + padding := max(0, m.Width-valWidth) + if valWidth+padding <= m.Width && pos < len(value) { + padding++ + } + v += styleText(strings.Repeat(" ", padding)) + } + + return m.PromptStyle.Render(m.Prompt) + v +} + +// placeholderView returns the prompt and placeholder view, if any. +func (m Model) placeholderView() string { + var ( + v string + p = []rune(m.Placeholder) + style = m.PlaceholderStyle.Inline(true).Render + ) + + m.Cursor.TextStyle = m.PlaceholderStyle + m.Cursor.SetChar(string(p[:1])) + v += m.Cursor.View() + + // If the entire placeholder is already set and no padding is needed, finish + if m.Width < 1 && len(p) <= 1 { + return m.PromptStyle.Render(m.Prompt) + v + } + + // If Width is set then size placeholder accordingly + if m.Width > 0 { + // available width is width - len + cursor offset of 1 + minWidth := lipgloss.Width(m.Placeholder) + availWidth := m.Width - minWidth + 1 + + // if width < len, 'subtract'(add) number to len and dont add padding + if availWidth < 0 { + minWidth += availWidth + availWidth = 0 + } + // append placeholder[len] - cursor, append padding + v += style(string(p[1:minWidth])) + v += style(strings.Repeat(" ", availWidth)) + } else { + // if there is no width, the placeholder can be any length + v += style(string(p[1:])) + } + + return m.PromptStyle.Render(m.Prompt) + v +} + +// Blink is a command used to initialize cursor blinking. +func Blink() tea.Msg { + return cursor.Blink() +} + +// Paste is a command for pasting from the clipboard into the text input. +func Paste() tea.Msg { + str, err := clipboard.ReadAll() + if err != nil { + return pasteErrMsg{err} + } + return pasteMsg(str) +} + +func clamp(v, low, high int) int { + if high < low { + low, high = high, low + } + return min(high, max(low, v)) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +// Deprecated. + +// Deprecated: use cursor.Mode. +type CursorMode int + +const ( + // Deprecated: use cursor.CursorBlink. + CursorBlink = CursorMode(cursor.CursorBlink) + // Deprecated: use cursor.CursorStatic. + CursorStatic = CursorMode(cursor.CursorStatic) + // Deprecated: use cursor.CursorHide. + CursorHide = CursorMode(cursor.CursorHide) +) + +func (c CursorMode) String() string { + return cursor.Mode(c).String() +} + +// Deprecated: use cursor.Mode(). +func (m Model) CursorMode() CursorMode { + return CursorMode(m.Cursor.Mode()) +} + +// Deprecated: use cursor.SetMode(). +func (m *Model) SetCursorMode(mode CursorMode) tea.Cmd { + return m.Cursor.SetMode(cursor.Mode(mode)) +} + +func (m Model) completionView(offset int) string { + var ( + value = m.value + style = m.PlaceholderStyle.Inline(true).Render + ) + + if m.canAcceptSuggestion() { + suggestion := m.matchedSuggestions[m.currentSuggestionIndex] + if len(value) < len(suggestion) { + return style(string(suggestion[len(value)+offset:])) + } + } + return "" +} + +// AvailableSuggestions returns the list of available suggestions. +func (m *Model) AvailableSuggestions() []string { + suggestions := make([]string, len(m.suggestions)) + for i, s := range m.suggestions { + suggestions[i] = string(s) + } + + return suggestions +} + +// CurrentSuggestion returns the currently selected suggestion. +func (m *Model) CurrentSuggestion() string { + return string(m.matchedSuggestions[m.currentSuggestionIndex]) +} + +// canAcceptSuggestion returns whether there is an acceptable suggestion to +// autocomplete the current value. +func (m *Model) canAcceptSuggestion() bool { + return len(m.matchedSuggestions) > 0 +} + +// updateSuggestions refreshes the list of matching suggestions. +func (m *Model) updateSuggestions() { + if !m.ShowSuggestions { + return + } + + if len(m.value) <= 0 || len(m.suggestions) <= 0 { + m.matchedSuggestions = [][]rune{} + return + } + + matches := [][]rune{} + for _, s := range m.suggestions { + suggestion := string(s) + + if strings.HasPrefix(strings.ToLower(suggestion), strings.ToLower(string(m.value))) { + matches = append(matches, []rune(suggestion)) + } + } + if !reflect.DeepEqual(matches, m.matchedSuggestions) { + m.currentSuggestionIndex = 0 + } + + m.matchedSuggestions = matches +} + +// nextSuggestion selects the next suggestion. +func (m *Model) nextSuggestion() { + m.currentSuggestionIndex = (m.currentSuggestionIndex + 1) + if m.currentSuggestionIndex >= len(m.matchedSuggestions) { + m.currentSuggestionIndex = 0 + } +} + +// previousSuggestion selects the previous suggestion. +func (m *Model) previousSuggestion() { + m.currentSuggestionIndex = (m.currentSuggestionIndex - 1) + if m.currentSuggestionIndex < 0 { + m.currentSuggestionIndex = len(m.matchedSuggestions) - 1 + } +} diff --git a/vendor/github.com/charmbracelet/bubbles/viewport/keymap.go b/vendor/github.com/charmbracelet/bubbles/viewport/keymap.go new file mode 100644 index 00000000..9289706a --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/viewport/keymap.go @@ -0,0 +1,48 @@ +package viewport + +import "github.com/charmbracelet/bubbles/key" + +const spacebar = " " + +// KeyMap defines the keybindings for the viewport. Note that you don't +// necessary need to use keybindings at all; the viewport can be controlled +// programmatically with methods like Model.LineDown(1). See the GoDocs for +// details. +type KeyMap struct { + PageDown key.Binding + PageUp key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding + Down key.Binding + Up key.Binding +} + +// DefaultKeyMap returns a set of pager-like default keybindings. +func DefaultKeyMap() KeyMap { + return KeyMap{ + PageDown: key.NewBinding( + key.WithKeys("pgdown", spacebar, "f"), + key.WithHelp("f/pgdn", "page down"), + ), + PageUp: key.NewBinding( + key.WithKeys("pgup", "b"), + key.WithHelp("b/pgup", "page up"), + ), + HalfPageUp: key.NewBinding( + key.WithKeys("u", "ctrl+u"), + key.WithHelp("u", "½ page up"), + ), + HalfPageDown: key.NewBinding( + key.WithKeys("d", "ctrl+d"), + key.WithHelp("d", "½ page down"), + ), + Up: key.NewBinding( + key.WithKeys("up", "k"), + key.WithHelp("↑/k", "up"), + ), + Down: key.NewBinding( + key.WithKeys("down", "j"), + key.WithHelp("↓/j", "down"), + ), + } +} diff --git a/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go b/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go new file mode 100644 index 00000000..0a5a73a8 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbles/viewport/viewport.go @@ -0,0 +1,405 @@ +package viewport + +import ( + "math" + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// New returns a new model with the given width and height as well as default +// key mappings. +func New(width, height int) (m Model) { + m.Width = width + m.Height = height + m.setInitialValues() + return m +} + +// Model is the Bubble Tea model for this viewport element. +type Model struct { + Width int + Height int + KeyMap KeyMap + + // Whether or not to respond to the mouse. The mouse must be enabled in + // Bubble Tea for this to work. For details, see the Bubble Tea docs. + MouseWheelEnabled bool + + // The number of lines the mouse wheel will scroll. By default, this is 3. + MouseWheelDelta int + + // YOffset is the vertical scroll position. + YOffset int + + // YPosition is the position of the viewport in relation to the terminal + // window. It's used in high performance rendering only. + YPosition int + + // Style applies a lipgloss style to the viewport. Realistically, it's most + // useful for setting borders, margins and padding. + Style lipgloss.Style + + // HighPerformanceRendering bypasses the normal Bubble Tea renderer to + // provide higher performance rendering. Most of the time the normal Bubble + // Tea rendering methods will suffice, but if you're passing content with + // a lot of ANSI escape codes you may see improved rendering in certain + // terminals with this enabled. + // + // This should only be used in program occupying the entire terminal, + // which is usually via the alternate screen buffer. + HighPerformanceRendering bool + + initialized bool + lines []string +} + +func (m *Model) setInitialValues() { + m.KeyMap = DefaultKeyMap() + m.MouseWheelEnabled = true + m.MouseWheelDelta = 3 + m.initialized = true +} + +// Init exists to satisfy the tea.Model interface for composability purposes. +func (m Model) Init() tea.Cmd { + return nil +} + +// AtTop returns whether or not the viewport is at the very top position. +func (m Model) AtTop() bool { + return m.YOffset <= 0 +} + +// AtBottom returns whether or not the viewport is at or past the very bottom +// position. +func (m Model) AtBottom() bool { + return m.YOffset >= m.maxYOffset() +} + +// PastBottom returns whether or not the viewport is scrolled beyond the last +// line. This can happen when adjusting the viewport height. +func (m Model) PastBottom() bool { + return m.YOffset > m.maxYOffset() +} + +// ScrollPercent returns the amount scrolled as a float between 0 and 1. +func (m Model) ScrollPercent() float64 { + if m.Height >= len(m.lines) { + return 1.0 + } + y := float64(m.YOffset) + h := float64(m.Height) + t := float64(len(m.lines) - 1) + v := y / (t - h) + return math.Max(0.0, math.Min(1.0, v)) +} + +// SetContent set the pager's text content. For high performance rendering the +// Sync command should also be called. +func (m *Model) SetContent(s string) { + s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings + m.lines = strings.Split(s, "\n") + + if m.YOffset > len(m.lines)-1 { + m.GotoBottom() + } +} + +// maxYOffset returns the maximum possible value of the y-offset based on the +// viewport's content and set height. +func (m Model) maxYOffset() int { + return max(0, len(m.lines)-m.Height) +} + +// visibleLines returns the lines that should currently be visible in the +// viewport. +func (m Model) visibleLines() (lines []string) { + if len(m.lines) > 0 { + top := max(0, m.YOffset) + bottom := clamp(m.YOffset+m.Height, top, len(m.lines)) + lines = m.lines[top:bottom] + } + return lines +} + +// scrollArea returns the scrollable boundaries for high performance rendering. +func (m Model) scrollArea() (top, bottom int) { + top = max(0, m.YPosition) + bottom = max(top, top+m.Height) + if top > 0 && bottom > top { + bottom-- + } + return top, bottom +} + +// SetYOffset sets the Y offset. +func (m *Model) SetYOffset(n int) { + m.YOffset = clamp(n, 0, m.maxYOffset()) +} + +// ViewDown moves the view down by the number of lines in the viewport. +// Basically, "page down". +func (m *Model) ViewDown() []string { + if m.AtBottom() { + return nil + } + + return m.LineDown(m.Height) +} + +// ViewUp moves the view up by one height of the viewport. Basically, "page up". +func (m *Model) ViewUp() []string { + if m.AtTop() { + return nil + } + + return m.LineUp(m.Height) +} + +// HalfViewDown moves the view down by half the height of the viewport. +func (m *Model) HalfViewDown() (lines []string) { + if m.AtBottom() { + return nil + } + + return m.LineDown(m.Height / 2) +} + +// HalfViewUp moves the view up by half the height of the viewport. +func (m *Model) HalfViewUp() (lines []string) { + if m.AtTop() { + return nil + } + + return m.LineUp(m.Height / 2) +} + +// LineDown moves the view down by the given number of lines. +func (m *Model) LineDown(n int) (lines []string) { + if m.AtBottom() || n == 0 || len(m.lines) == 0 { + return nil + } + + // Make sure the number of lines by which we're going to scroll isn't + // greater than the number of lines we actually have left before we reach + // the bottom. + m.SetYOffset(m.YOffset + n) + + // Gather lines to send off for performance scrolling. + bottom := clamp(m.YOffset+m.Height, 0, len(m.lines)) + top := clamp(m.YOffset+m.Height-n, 0, bottom) + return m.lines[top:bottom] +} + +// LineUp moves the view down by the given number of lines. Returns the new +// lines to show. +func (m *Model) LineUp(n int) (lines []string) { + if m.AtTop() || n == 0 || len(m.lines) == 0 { + return nil + } + + // Make sure the number of lines by which we're going to scroll isn't + // greater than the number of lines we are from the top. + m.SetYOffset(m.YOffset - n) + + // Gather lines to send off for performance scrolling. + top := max(0, m.YOffset) + bottom := clamp(m.YOffset+n, 0, m.maxYOffset()) + return m.lines[top:bottom] +} + +// TotalLineCount returns the total number of lines (both hidden and visible) within the viewport. +func (m Model) TotalLineCount() int { + return len(m.lines) +} + +// VisibleLineCount returns the number of the visible lines within the viewport. +func (m Model) VisibleLineCount() int { + return len(m.visibleLines()) +} + +// GotoTop sets the viewport to the top position. +func (m *Model) GotoTop() (lines []string) { + if m.AtTop() { + return nil + } + + m.SetYOffset(0) + return m.visibleLines() +} + +// GotoBottom sets the viewport to the bottom position. +func (m *Model) GotoBottom() (lines []string) { + m.SetYOffset(m.maxYOffset()) + return m.visibleLines() +} + +// Sync tells the renderer where the viewport will be located and requests +// a render of the current state of the viewport. It should be called for the +// first render and after a window resize. +// +// For high performance rendering only. +func Sync(m Model) tea.Cmd { + if len(m.lines) == 0 { + return nil + } + top, bottom := m.scrollArea() + return tea.SyncScrollArea(m.visibleLines(), top, bottom) +} + +// ViewDown is a high performance command that moves the viewport up by a given +// number of lines. Use Model.ViewDown to get the lines that should be rendered. +// For example: +// +// lines := model.ViewDown(1) +// cmd := ViewDown(m, lines) +func ViewDown(m Model, lines []string) tea.Cmd { + if len(lines) == 0 { + return nil + } + top, bottom := m.scrollArea() + return tea.ScrollDown(lines, top, bottom) +} + +// ViewUp is a high performance command the moves the viewport down by a given +// number of lines height. Use Model.ViewUp to get the lines that should be +// rendered. +func ViewUp(m Model, lines []string) tea.Cmd { + if len(lines) == 0 { + return nil + } + top, bottom := m.scrollArea() + return tea.ScrollUp(lines, top, bottom) +} + +// Update handles standard message-based viewport updates. +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + var cmd tea.Cmd + m, cmd = m.updateAsModel(msg) + return m, cmd +} + +// Author's note: this method has been broken out to make it easier to +// potentially transition Update to satisfy tea.Model. +func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) { + if !m.initialized { + m.setInitialValues() + } + + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, m.KeyMap.PageDown): + lines := m.ViewDown() + if m.HighPerformanceRendering { + cmd = ViewDown(m, lines) + } + + case key.Matches(msg, m.KeyMap.PageUp): + lines := m.ViewUp() + if m.HighPerformanceRendering { + cmd = ViewUp(m, lines) + } + + case key.Matches(msg, m.KeyMap.HalfPageDown): + lines := m.HalfViewDown() + if m.HighPerformanceRendering { + cmd = ViewDown(m, lines) + } + + case key.Matches(msg, m.KeyMap.HalfPageUp): + lines := m.HalfViewUp() + if m.HighPerformanceRendering { + cmd = ViewUp(m, lines) + } + + case key.Matches(msg, m.KeyMap.Down): + lines := m.LineDown(1) + if m.HighPerformanceRendering { + cmd = ViewDown(m, lines) + } + + case key.Matches(msg, m.KeyMap.Up): + lines := m.LineUp(1) + if m.HighPerformanceRendering { + cmd = ViewUp(m, lines) + } + } + + case tea.MouseMsg: + if !m.MouseWheelEnabled || msg.Action != tea.MouseActionPress { + break + } + switch msg.Button { + case tea.MouseButtonWheelUp: + lines := m.LineUp(m.MouseWheelDelta) + if m.HighPerformanceRendering { + cmd = ViewUp(m, lines) + } + + case tea.MouseButtonWheelDown: + lines := m.LineDown(m.MouseWheelDelta) + if m.HighPerformanceRendering { + cmd = ViewDown(m, lines) + } + } + } + + return m, cmd +} + +// View renders the viewport into a string. +func (m Model) View() string { + if m.HighPerformanceRendering { + // Just send newlines since we're going to be rendering the actual + // content separately. We still need to send something that equals the + // height of this view so that the Bubble Tea standard renderer can + // position anything below this view properly. + return strings.Repeat("\n", max(0, m.Height-1)) + } + + w, h := m.Width, m.Height + if sw := m.Style.GetWidth(); sw != 0 { + w = min(w, sw) + } + if sh := m.Style.GetHeight(); sh != 0 { + h = min(h, sh) + } + contentWidth := w - m.Style.GetHorizontalFrameSize() + contentHeight := h - m.Style.GetVerticalFrameSize() + contents := lipgloss.NewStyle(). + Width(contentWidth). // pad to width. + Height(contentHeight). // pad to height. + MaxHeight(contentHeight). // truncate height if taller. + MaxWidth(contentWidth). // truncate width if wider. + Render(strings.Join(m.visibleLines(), "\n")) + return m.Style.Copy(). + UnsetWidth().UnsetHeight(). // Style size already applied in contents. + Render(contents) +} + +func clamp(v, low, high int) int { + if high < low { + low, high = high, low + } + return min(high, max(low, v)) +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/vendor/github.com/charmbracelet/bubbletea/.gitattributes b/vendor/github.com/charmbracelet/bubbletea/.gitattributes new file mode 100644 index 00000000..6c929d48 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/.gitattributes @@ -0,0 +1 @@ +*.golden -text diff --git a/vendor/github.com/charmbracelet/bubbletea/.gitignore b/vendor/github.com/charmbracelet/bubbletea/.gitignore new file mode 100644 index 00000000..9cc52352 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/.gitignore @@ -0,0 +1,22 @@ +.DS_Store +.envrc + +examples/fullscreen/fullscreen +examples/help/help +examples/http/http +examples/list-default/list-default +examples/list-fancy/list-fancy +examples/list-simple/list-simple +examples/mouse/mouse +examples/pager/pager +examples/progress-download/color_vortex.blend +examples/progress-download/progress-download +examples/simple/simple +examples/spinner/spinner +examples/textinput/textinput +examples/textinputs/textinputs +examples/views/views +tutorials/basics/basics +tutorials/commands/commands +.idea +coverage.txt diff --git a/vendor/github.com/charmbracelet/bubbletea/.golangci-soft.yml b/vendor/github.com/charmbracelet/bubbletea/.golangci-soft.yml new file mode 100644 index 00000000..1b6824bb --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/.golangci-soft.yml @@ -0,0 +1,46 @@ +run: + tests: false + +issues: + include: + - EXC0001 + - EXC0005 + - EXC0011 + - EXC0012 + - EXC0013 + + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + enable: + # - dupl + - exhaustive + # - exhaustivestruct + - goconst + - godot + - godox + - gomnd + - gomoddirectives + - goprintffuncname + # - lll + - misspell + - nakedret + - nestif + - noctx + - nolintlint + - prealloc + - wrapcheck + + # disable default linters, they are already enabled in .golangci.yml + disable: + - deadcode + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - unused + - varcheck diff --git a/vendor/github.com/charmbracelet/bubbletea/.golangci.yml b/vendor/github.com/charmbracelet/bubbletea/.golangci.yml new file mode 100644 index 00000000..3affce91 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/.golangci.yml @@ -0,0 +1,30 @@ +run: + tests: false + +issues: + include: + - EXC0001 + - EXC0005 + - EXC0011 + - EXC0012 + - EXC0013 + + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + enable: + - bodyclose + - exportloopref + - gofumpt + - goimports + - gosec + - nilerr + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - tparallel + - unconvert + - unparam + - whitespace diff --git a/vendor/github.com/charmbracelet/bubbletea/.goreleaser.yml b/vendor/github.com/charmbracelet/bubbletea/.goreleaser.yml new file mode 100644 index 00000000..40d9f298 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/.goreleaser.yml @@ -0,0 +1,6 @@ +includes: + - from_url: + url: charmbracelet/meta/main/goreleaser-lib.yaml + +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json + diff --git a/vendor/github.com/charmbracelet/bubbletea/CONTRIBUTING.md b/vendor/github.com/charmbracelet/bubbletea/CONTRIBUTING.md new file mode 100644 index 00000000..19ee18c7 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing + +Pull requests are welcome for any changes. + +Consider opening an issue for larger changes to get feedback on the idea from the team. + +If your change touches parts of the Bubble Tea renderer or internals, make sure +that all the examples in the `examples/` folder continue to run correctly. + +For commit messages, please use conventional commits[^1] to make it easier to +generate release notes. + +[^1]: https://www.conventionalcommits.org/en/v1.0.0 diff --git a/vendor/github.com/charmbracelet/bubbletea/LICENSE b/vendor/github.com/charmbracelet/bubbletea/LICENSE new file mode 100644 index 00000000..31d76c1c --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2023 Charmbracelet, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/charmbracelet/bubbletea/README.md b/vendor/github.com/charmbracelet/bubbletea/README.md new file mode 100644 index 00000000..f80e04ea --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/README.md @@ -0,0 +1,387 @@ +# Bubble Tea + +

+ Bubble Tea Title Treatment
+ Latest Release + GoDoc + Build Status + phorm.ai +

+ +The fun, functional and stateful way to build terminal apps. A Go framework +based on [The Elm Architecture][elm]. Bubble Tea is well-suited for simple and +complex terminal applications, either inline, full-window, or a mix of both. + +

+ Bubble Tea Example +

+ +Bubble Tea is in use in production and includes a number of features and +performance optimizations we’ve added along the way. Among those is a standard +framerate-based renderer, a renderer for high-performance scrollable +regions which works alongside the main renderer, and mouse support. + +To get started, see the tutorial below, the [examples][examples], the +[docs][docs], the [video tutorials][youtube] and some common [resources](#libraries-we-use-with-bubble-tea). + +[youtube]: https://charm.sh/yt + +## By the way + +Be sure to check out [Bubbles][bubbles], a library of common UI components for Bubble Tea. + +

+ Bubbles Badge   + Text Input Example from Bubbles +

+ +--- + +## Tutorial + +Bubble Tea is based on the functional design paradigms of [The Elm +Architecture][elm], which happens to work nicely with Go. It's a delightful way +to build applications. + +This tutorial assumes you have a working knowledge of Go. + +By the way, the non-annotated source code for this program is available +[on GitHub][tut-source]. + +[elm]: https://guide.elm-lang.org/architecture/ +[tut-source]: https://github.com/charmbracelet/bubbletea/tree/master/tutorials/basics + +### Enough! Let's get to it. + +For this tutorial, we're making a shopping list. + +To start we'll define our package and import some libraries. Our only external +import will be the Bubble Tea library, which we'll call `tea` for short. + +```go +package main + +import ( + "fmt" + "os" + + tea "github.com/charmbracelet/bubbletea" +) +``` + +Bubble Tea programs are comprised of a **model** that describes the application +state and three simple methods on that model: + +- **Init**, a function that returns an initial command for the application to run. +- **Update**, a function that handles incoming events and updates the model accordingly. +- **View**, a function that renders the UI based on the data in the model. + +### The Model + +So let's start by defining our model which will store our application's state. +It can be any type, but a `struct` usually makes the most sense. + +```go +type model struct { + choices []string // items on the to-do list + cursor int // which to-do list item our cursor is pointing at + selected map[int]struct{} // which to-do items are selected +} +``` + +### Initialization + +Next, we’ll define our application’s initial state. In this case, we’re defining +a function to return our initial model, however, we could just as easily define +the initial model as a variable elsewhere, too. + +```go +func initialModel() model { + return model{ + // Our to-do list is a grocery list + choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"}, + + // A map which indicates which choices are selected. We're using + // the map like a mathematical set. The keys refer to the indexes + // of the `choices` slice, above. + selected: make(map[int]struct{}), + } +} +``` + +Next, we define the `Init` method. `Init` can return a `Cmd` that could perform +some initial I/O. For now, we don't need to do any I/O, so for the command, +we'll just return `nil`, which translates to "no command." + +```go +func (m model) Init() tea.Cmd { + // Just return `nil`, which means "no I/O right now, please." + return nil +} +``` + +### The Update Method + +Next up is the update method. The update function is called when â€things +happen.†Its job is to look at what has happened and return an updated model in +response. It can also return a `Cmd` to make more things happen, but for now +don't worry about that part. + +In our case, when a user presses the down arrow, `Update`’s job is to notice +that the down arrow was pressed and move the cursor accordingly (or not). + +The “something happened†comes in the form of a `Msg`, which can be any type. +Messages are the result of some I/O that took place, such as a keypress, timer +tick, or a response from a server. + +We usually figure out which type of `Msg` we received with a type switch, but +you could also use a type assertion. + +For now, we'll just deal with `tea.KeyMsg` messages, which are automatically +sent to the update function when keys are pressed. + +```go +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + + // Is it a key press? + case tea.KeyMsg: + + // Cool, what was the actual key pressed? + switch msg.String() { + + // These keys should exit the program. + case "ctrl+c", "q": + return m, tea.Quit + + // The "up" and "k" keys move the cursor up + case "up", "k": + if m.cursor > 0 { + m.cursor-- + } + + // The "down" and "j" keys move the cursor down + case "down", "j": + if m.cursor < len(m.choices)-1 { + m.cursor++ + } + + // The "enter" key and the spacebar (a literal space) toggle + // the selected state for the item that the cursor is pointing at. + case "enter", " ": + _, ok := m.selected[m.cursor] + if ok { + delete(m.selected, m.cursor) + } else { + m.selected[m.cursor] = struct{}{} + } + } + } + + // Return the updated model to the Bubble Tea runtime for processing. + // Note that we're not returning a command. + return m, nil +} +``` + +You may have noticed that ctrl+c and q above return +a `tea.Quit` command with the model. That’s a special command which instructs +the Bubble Tea runtime to quit, exiting the program. + +### The View Method + +At last, it’s time to render our UI. Of all the methods, the view is the +simplest. We look at the model in its current state and use it to return +a `string`. That string is our UI! + +Because the view describes the entire UI of your application, you don’t have to +worry about redrawing logic and stuff like that. Bubble Tea takes care of it +for you. + +```go +func (m model) View() string { + // The header + s := "What should we buy at the market?\n\n" + + // Iterate over our choices + for i, choice := range m.choices { + + // Is the cursor pointing at this choice? + cursor := " " // no cursor + if m.cursor == i { + cursor = ">" // cursor! + } + + // Is this choice selected? + checked := " " // not selected + if _, ok := m.selected[i]; ok { + checked = "x" // selected! + } + + // Render the row + s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice) + } + + // The footer + s += "\nPress q to quit.\n" + + // Send the UI for rendering + return s +} +``` + +### All Together Now + +The last step is to simply run our program. We pass our initial model to +`tea.NewProgram` and let it rip: + +```go +func main() { + p := tea.NewProgram(initialModel()) + if _, err := p.Run(); err != nil { + fmt.Printf("Alas, there's been an error: %v", err) + os.Exit(1) + } +} +``` + +## What’s Next? + +This tutorial covers the basics of building an interactive terminal UI, but +in the real world you'll also need to perform I/O. To learn about that have a +look at the [Command Tutorial][cmd]. It's pretty simple. + +There are also several [Bubble Tea examples][examples] available and, of course, +there are [Go Docs][docs]. + +[cmd]: http://github.com/charmbracelet/bubbletea/tree/master/tutorials/commands/ +[examples]: http://github.com/charmbracelet/bubbletea/tree/master/examples +[docs]: https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc + +## Debugging + +### Debugging with Delve + +Since Bubble Tea apps assume control of stdin and stdout, you’ll need to run +delve in headless mode and then connect to it: + +```bash +# Start the debugger +$ dlv debug --headless --api-version=2 --listen=127.0.0.1:43000 . +API server listening at: 127.0.0.1:43000 + +# Connect to it from another terminal +$ dlv connect 127.0.0.1:43000 +``` + +If you do not explicitly supply the `--listen` flag, the port used will vary +per run, so passing this in makes the debugger easier to use from a script +or your IDE of choice. + +Additionally, we pass in `--api-version=2` because delve defaults to version 1 +for backwards compatibility reasons. However, delve recommends using version 2 +for all new development and some clients may no longer work with version 1. +For more information, see the [Delve documentation](https://github.com/go-delve/delve/tree/master/Documentation/api). + +### Logging Stuff + +You can’t really log to stdout with Bubble Tea because your TUI is busy +occupying that! You can, however, log to a file by including something like +the following prior to starting your Bubble Tea program: + +```go +if len(os.Getenv("DEBUG")) > 0 { + f, err := tea.LogToFile("debug.log", "debug") + if err != nil { + fmt.Println("fatal:", err) + os.Exit(1) + } + defer f.Close() +} +``` + +To see what’s being logged in real time, run `tail -f debug.log` while you run +your program in another window. + +## Libraries we use with Bubble Tea + +- [Bubbles][bubbles]: Common Bubble Tea components such as text inputs, viewports, spinners and so on +- [Lip Gloss][lipgloss]: Style, format and layout tools for terminal applications +- [Harmonica][harmonica]: A spring animation library for smooth, natural motion +- [BubbleZone][bubblezone]: Easy mouse event tracking for Bubble Tea components +- [ntcharts][ntcharts]: A terminal charting library built for Bubble Tea and [Lip Gloss][lipgloss] +- [Termenv][termenv]: Advanced ANSI styling for terminal applications +- [Reflow][reflow]: Advanced ANSI-aware methods for working with text + +[bubbles]: https://github.com/charmbracelet/bubbles +[lipgloss]: https://github.com/charmbracelet/lipgloss +[harmonica]: https://github.com/charmbracelet/harmonica +[bubblezone]: https://github.com/lrstanley/bubblezone +[ntcharts]: https://github.com/NimbleMarkets/ntcharts +[termenv]: https://github.com/muesli/termenv +[reflow]: https://github.com/muesli/reflow + +## Bubble Tea in the Wild + +There are over 8k applications built with Bubble Tea! Here are a handful of ’em. + +### Staff favourites + +- [chezmoi](https://github.com/twpayne/chezmoi): securely manage your dotfiles across multiple machines +- [circumflex](https://github.com/bensadeh/circumflex): read Hacker News in the terminal +- [gh-dash](https://www.github.com/dlvhdr/gh-dash): a GitHub CLI extension for PRs and issues +- [Tetrigo](https://github.com/Broderick-Westrope/tetrigo): Tetris in the terminal + +### In Industry + +- Microsoft Azure – [Aztify](https://github.com/Azure/aztfy): bring Microsoft Azure resources under Terraform +- Daytona – [Daytona](https://github.com/daytonaio/daytona): open source dev environment manager +- Truffle Security Co. – [Trufflehog](https://github.com/trufflesecurity/trufflehog): find leaked credentials +- NVIDIA – [container-canary](https://github.com/NVIDIA/container-canary) from NVIDIA: a container validator +- AWS – [eks-node-viewer](https://github.com/awslabs/eks-node-viewer) from AWS: a tool for visualizing dynamic node usage within an EKS cluster +- MinIO – [mc](https://github.com/minio/mc) from Min.io: the official [MinIO](https://min.io) client + +### Charm stuff + +- [Glow](https://github.com/charmbracelet/glow): a markdown reader, browser, and online markdown stash +- [Huh?](https://github.com/charmbracelet/huh): an interactive prompt and form toolkit +- [Mods](https://github.com/charmbracelet/mods): AI on the CLI, built for pipelines +- [Wishlist](https://github.com/charmbracelet/wishlist): an SSH directory (and bastion!) + +### There’s so much more where that came from + +For more applications built with Bubble Tea see [Charm & Friends][community]. +Is there something cool you made with Bubble Tea you want to share? [PRs][community] are +welcome! + +## Feedback + +We’d love to hear your thoughts on this project. Feel free to drop us a note! + +- [Twitter](https://twitter.com/charmcli) +- [The Fediverse](https://mastodon.social/@charmcli) +- [Discord](https://charm.sh/chat) + +## Acknowledgments + +Bubble Tea is based on the paradigms of [The Elm Architecture][elm] by Evan +Czaplicki et alia and the excellent [go-tea][gotea] by TJ Holowaychuk. It’s +inspired by the many great [_Zeichenorientierte Benutzerschnittstellen_][zb] +of days past. + +[elm]: https://guide.elm-lang.org/architecture/ +[gotea]: https://github.com/tj/go-tea +[zb]: https://de.wikipedia.org/wiki/Zeichenorientierte_Benutzerschnittstelle +[community]: https://github.com/charm-and-friends/charm-in-the-wild + +## License + +[MIT](https://github.com/charmbracelet/bubbletea/raw/master/LICENSE) + +--- + +Part of [Charm](https://charm.sh). + +The Charm logo + +Charmçƒ­çˆ±å¼€æº â€¢ Charm loves open source • نحن٠نحب المصادر المÙتوحة diff --git a/vendor/github.com/charmbracelet/bubbletea/commands.go b/vendor/github.com/charmbracelet/bubbletea/commands.go new file mode 100644 index 00000000..bfa3b704 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/commands.go @@ -0,0 +1,216 @@ +package tea + +import ( + "time" +) + +// Batch performs a bunch of commands concurrently with no ordering guarantees +// about the results. Use a Batch to return several commands. +// +// Example: +// +// func (m model) Init() Cmd { +// return tea.Batch(someCommand, someOtherCommand) +// } +func Batch(cmds ...Cmd) Cmd { + var validCmds []Cmd //nolint:prealloc + for _, c := range cmds { + if c == nil { + continue + } + validCmds = append(validCmds, c) + } + switch len(validCmds) { + case 0: + return nil + case 1: + return validCmds[0] + default: + return func() Msg { + return BatchMsg(validCmds) + } + } +} + +// BatchMsg is a message used to perform a bunch of commands concurrently with +// no ordering guarantees. You can send a BatchMsg with Batch. +type BatchMsg []Cmd + +// Sequence runs the given commands one at a time, in order. Contrast this with +// Batch, which runs commands concurrently. +func Sequence(cmds ...Cmd) Cmd { + return func() Msg { + return sequenceMsg(cmds) + } +} + +// sequenceMsg is used internally to run the given commands in order. +type sequenceMsg []Cmd + +// Every is a command that ticks in sync with the system clock. So, if you +// wanted to tick with the system clock every second, minute or hour you +// could use this. It's also handy for having different things tick in sync. +// +// Because we're ticking with the system clock the tick will likely not run for +// the entire specified duration. For example, if we're ticking for one minute +// and the clock is at 12:34:20 then the next tick will happen at 12:35:00, 40 +// seconds later. +// +// To produce the command, pass a duration and a function which returns +// a message containing the time at which the tick occurred. +// +// type TickMsg time.Time +// +// cmd := Every(time.Second, func(t time.Time) Msg { +// return TickMsg(t) +// }) +// +// Beginners' note: Every sends a single message and won't automatically +// dispatch messages at an interval. To do that, you'll want to return another +// Every command after receiving your tick message. For example: +// +// type TickMsg time.Time +// +// // Send a message every second. +// func tickEvery() Cmd { +// return Every(time.Second, func(t time.Time) Msg { +// return TickMsg(t) +// }) +// } +// +// func (m model) Init() Cmd { +// // Start ticking. +// return tickEvery() +// } +// +// func (m model) Update(msg Msg) (Model, Cmd) { +// switch msg.(type) { +// case TickMsg: +// // Return your Every command again to loop. +// return m, tickEvery() +// } +// return m, nil +// } +// +// Every is analogous to Tick in the Elm Architecture. +func Every(duration time.Duration, fn func(time.Time) Msg) Cmd { + n := time.Now() + d := n.Truncate(duration).Add(duration).Sub(n) + t := time.NewTimer(d) + return func() Msg { + ts := <-t.C + t.Stop() + for len(t.C) > 0 { + <-t.C + } + return fn(ts) + } +} + +// Tick produces a command at an interval independent of the system clock at +// the given duration. That is, the timer begins precisely when invoked, +// and runs for its entire duration. +// +// To produce the command, pass a duration and a function which returns +// a message containing the time at which the tick occurred. +// +// type TickMsg time.Time +// +// cmd := Tick(time.Second, func(t time.Time) Msg { +// return TickMsg(t) +// }) +// +// Beginners' note: Tick sends a single message and won't automatically +// dispatch messages at an interval. To do that, you'll want to return another +// Tick command after receiving your tick message. For example: +// +// type TickMsg time.Time +// +// func doTick() Cmd { +// return Tick(time.Second, func(t time.Time) Msg { +// return TickMsg(t) +// }) +// } +// +// func (m model) Init() Cmd { +// // Start ticking. +// return doTick() +// } +// +// func (m model) Update(msg Msg) (Model, Cmd) { +// switch msg.(type) { +// case TickMsg: +// // Return your Tick command again to loop. +// return m, doTick() +// } +// return m, nil +// } +func Tick(d time.Duration, fn func(time.Time) Msg) Cmd { + t := time.NewTimer(d) + return func() Msg { + ts := <-t.C + t.Stop() + for len(t.C) > 0 { + <-t.C + } + return fn(ts) + } +} + +// Sequentially produces a command that sequentially executes the given +// commands. +// The Msg returned is the first non-nil message returned by a Cmd. +// +// func saveStateCmd() Msg { +// if err := save(); err != nil { +// return errMsg{err} +// } +// return nil +// } +// +// cmd := Sequentially(saveStateCmd, Quit) +// +// Deprecated: use Sequence instead. +func Sequentially(cmds ...Cmd) Cmd { + return func() Msg { + for _, cmd := range cmds { + if cmd == nil { + continue + } + if msg := cmd(); msg != nil { + return msg + } + } + return nil + } +} + +// setWindowTitleMsg is an internal message used to set the window title. +type setWindowTitleMsg string + +// SetWindowTitle produces a command that sets the terminal title. +// +// For example: +// +// func (m model) Init() Cmd { +// // Set title. +// return tea.SetWindowTitle("My App") +// } +func SetWindowTitle(title string) Cmd { + return func() Msg { + return setWindowTitleMsg(title) + } +} + +type windowSizeMsg struct{} + +// WindowSize is a command that queries the terminal for its current size. It +// delivers the results to Update via a [WindowSizeMsg]. Keep in mind that +// WindowSizeMsgs will automatically be delivered to Update when the [Program] +// starts and when the window dimensions change so in many cases you will not +// need to explicitly invoke this command. +func WindowSize() Cmd { + return func() Msg { + return windowSizeMsg{} + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/exec.go b/vendor/github.com/charmbracelet/bubbletea/exec.go new file mode 100644 index 00000000..7a14d2a7 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/exec.go @@ -0,0 +1,129 @@ +package tea + +import ( + "io" + "os" + "os/exec" +) + +// execMsg is used internally to run an ExecCommand sent with Exec. +type execMsg struct { + cmd ExecCommand + fn ExecCallback +} + +// Exec is used to perform arbitrary I/O in a blocking fashion, effectively +// pausing the Program while execution is running and resuming it when +// execution has completed. +// +// Most of the time you'll want to use ExecProcess, which runs an exec.Cmd. +// +// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd). +func Exec(c ExecCommand, fn ExecCallback) Cmd { + return func() Msg { + return execMsg{cmd: c, fn: fn} + } +} + +// ExecProcess runs the given *exec.Cmd in a blocking fashion, effectively +// pausing the Program while the command is running. After the *exec.Cmd exists +// the Program resumes. It's useful for spawning other interactive applications +// such as editors and shells from within a Program. +// +// To produce the command, pass an *exec.Cmd and a function which returns +// a message containing the error which may have occurred when running the +// ExecCommand. +// +// type VimFinishedMsg struct { err error } +// +// c := exec.Command("vim", "file.txt") +// +// cmd := ExecProcess(c, func(err error) Msg { +// return VimFinishedMsg{err: err} +// }) +// +// Or, if you don't care about errors, you could simply: +// +// cmd := ExecProcess(exec.Command("vim", "file.txt"), nil) +// +// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd). +func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd { + return Exec(wrapExecCommand(c), fn) +} + +// ExecCallback is used when executing an *exec.Command to return a message +// with an error, which may or may not be nil. +type ExecCallback func(error) Msg + +// ExecCommand can be implemented to execute things in a blocking fashion in +// the current terminal. +type ExecCommand interface { + Run() error + SetStdin(io.Reader) + SetStdout(io.Writer) + SetStderr(io.Writer) +} + +// wrapExecCommand wraps an exec.Cmd so that it satisfies the ExecCommand +// interface so it can be used with Exec. +func wrapExecCommand(c *exec.Cmd) ExecCommand { + return &osExecCommand{Cmd: c} +} + +// osExecCommand is a layer over an exec.Cmd that satisfies the ExecCommand +// interface. +type osExecCommand struct{ *exec.Cmd } + +// SetStdin sets stdin on underlying exec.Cmd to the given io.Reader. +func (c *osExecCommand) SetStdin(r io.Reader) { + // If unset, have the command use the same input as the terminal. + if c.Stdin == nil { + c.Stdin = r + } +} + +// SetStdout sets stdout on underlying exec.Cmd to the given io.Writer. +func (c *osExecCommand) SetStdout(w io.Writer) { + // If unset, have the command use the same output as the terminal. + if c.Stdout == nil { + c.Stdout = w + } +} + +// SetStderr sets stderr on the underlying exec.Cmd to the given io.Writer. +func (c *osExecCommand) SetStderr(w io.Writer) { + // If unset, use stderr for the command's stderr + if c.Stderr == nil { + c.Stderr = w + } +} + +// exec runs an ExecCommand and delivers the results to the program as a Msg. +func (p *Program) exec(c ExecCommand, fn ExecCallback) { + if err := p.ReleaseTerminal(); err != nil { + // If we can't release input, abort. + if fn != nil { + go p.Send(fn(err)) + } + return + } + + c.SetStdin(p.input) + c.SetStdout(p.output) + c.SetStderr(os.Stderr) + + // Execute system command. + if err := c.Run(); err != nil { + _ = p.RestoreTerminal() // also try to restore the terminal. + if fn != nil { + go p.Send(fn(err)) + } + return + } + + // Have the program re-capture input. + err := p.RestoreTerminal() + if fn != nil { + go p.Send(fn(err)) + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/focus.go b/vendor/github.com/charmbracelet/bubbletea/focus.go new file mode 100644 index 00000000..4d34bea6 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/focus.go @@ -0,0 +1,9 @@ +package tea + +// FocusMsg represents a terminal focus message. +// This occurs when the terminal gains focus. +type FocusMsg struct{} + +// BlurMsg represents a terminal blur message. +// This occurs when the terminal loses focus. +type BlurMsg struct{} diff --git a/vendor/github.com/charmbracelet/bubbletea/inputreader_other.go b/vendor/github.com/charmbracelet/bubbletea/inputreader_other.go new file mode 100644 index 00000000..8e63a87d --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/inputreader_other.go @@ -0,0 +1,14 @@ +//go:build !windows +// +build !windows + +package tea + +import ( + "io" + + "github.com/muesli/cancelreader" +) + +func newInputReader(r io.Reader) (cancelreader.CancelReader, error) { + return cancelreader.NewReader(r) +} diff --git a/vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go b/vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go new file mode 100644 index 00000000..449df479 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go @@ -0,0 +1,107 @@ +//go:build windows +// +build windows + +package tea + +import ( + "fmt" + "io" + "os" + "sync" + + "github.com/charmbracelet/x/term" + "github.com/erikgeiser/coninput" + "github.com/muesli/cancelreader" + "golang.org/x/sys/windows" +) + +type conInputReader struct { + cancelMixin + + conin windows.Handle + + originalMode uint32 +} + +var _ cancelreader.CancelReader = &conInputReader{} + +func newInputReader(r io.Reader) (cancelreader.CancelReader, error) { + fallback := func(io.Reader) (cancelreader.CancelReader, error) { + return cancelreader.NewReader(r) + } + if f, ok := r.(term.File); !ok || f.Fd() != os.Stdin.Fd() { + return fallback(r) + } + + conin, err := coninput.NewStdinHandle() + if err != nil { + return fallback(r) + } + + originalMode, err := prepareConsole(conin, + windows.ENABLE_MOUSE_INPUT, + windows.ENABLE_WINDOW_INPUT, + windows.ENABLE_EXTENDED_FLAGS, + ) + if err != nil { + return nil, fmt.Errorf("failed to prepare console input: %w", err) + } + + return &conInputReader{ + conin: conin, + originalMode: originalMode, + }, nil +} + +// Cancel implements cancelreader.CancelReader. +func (r *conInputReader) Cancel() bool { + r.setCanceled() + + return windows.CancelIo(r.conin) == nil +} + +// Close implements cancelreader.CancelReader. +func (r *conInputReader) Close() error { + if r.originalMode != 0 { + err := windows.SetConsoleMode(r.conin, r.originalMode) + if err != nil { + return fmt.Errorf("reset console mode: %w", err) + } + } + + return nil +} + +// Read implements cancelreader.CancelReader. +func (*conInputReader) Read(_ []byte) (n int, err error) { + return 0, nil +} + +func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) { + err = windows.GetConsoleMode(input, &originalMode) + if err != nil { + return 0, fmt.Errorf("get console mode: %w", err) + } + + newMode := coninput.AddInputModes(0, modes...) + + err = windows.SetConsoleMode(input, newMode) + if err != nil { + return 0, fmt.Errorf("set console mode: %w", err) + } + + return originalMode, nil +} + +// cancelMixin represents a goroutine-safe cancelation status. +type cancelMixin struct { + unsafeCanceled bool + lock sync.Mutex +} + +func (c *cancelMixin) setCanceled() { + c.lock.Lock() + defer c.lock.Unlock() + + c.unsafeCanceled = true +} diff --git a/vendor/github.com/charmbracelet/bubbletea/key.go b/vendor/github.com/charmbracelet/bubbletea/key.go new file mode 100644 index 00000000..ab4792ac --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/key.go @@ -0,0 +1,715 @@ +package tea + +import ( + "context" + "fmt" + "io" + "regexp" + "strings" + "unicode/utf8" +) + +// KeyMsg contains information about a keypress. KeyMsgs are always sent to +// the program's update function. There are a couple general patterns you could +// use to check for keypresses: +// +// // Switch on the string representation of the key (shorter) +// switch msg := msg.(type) { +// case KeyMsg: +// switch msg.String() { +// case "enter": +// fmt.Println("you pressed enter!") +// case "a": +// fmt.Println("you pressed a!") +// } +// } +// +// // Switch on the key type (more foolproof) +// switch msg := msg.(type) { +// case KeyMsg: +// switch msg.Type { +// case KeyEnter: +// fmt.Println("you pressed enter!") +// case KeyRunes: +// switch string(msg.Runes) { +// case "a": +// fmt.Println("you pressed a!") +// } +// } +// } +// +// Note that Key.Runes will always contain at least one character, so you can +// always safely call Key.Runes[0]. In most cases Key.Runes will only contain +// one character, though certain input method editors (most notably Chinese +// IMEs) can input multiple runes at once. +type KeyMsg Key + +// String returns a string representation for a key message. It's safe (and +// encouraged) for use in key comparison. +func (k KeyMsg) String() (str string) { + return Key(k).String() +} + +// Key contains information about a keypress. +type Key struct { + Type KeyType + Runes []rune + Alt bool + Paste bool +} + +// String returns a friendly string representation for a key. It's safe (and +// encouraged) for use in key comparison. +// +// k := Key{Type: KeyEnter} +// fmt.Println(k) +// // Output: enter +func (k Key) String() (str string) { + var buf strings.Builder + if k.Alt { + buf.WriteString("alt+") + } + if k.Type == KeyRunes { + if k.Paste { + // Note: bubbles/keys bindings currently do string compares to + // recognize shortcuts. Since pasted text should never activate + // shortcuts, we need to ensure that the binding code doesn't + // match Key events that result from pastes. We achieve this + // here by enclosing pastes in '[...]' so that the string + // comparison in Matches() fails in that case. + buf.WriteByte('[') + } + buf.WriteString(string(k.Runes)) + if k.Paste { + buf.WriteByte(']') + } + return buf.String() + } else if s, ok := keyNames[k.Type]; ok { + buf.WriteString(s) + return buf.String() + } + return "" +} + +// KeyType indicates the key pressed, such as KeyEnter or KeyBreak or KeyCtrlC. +// All other keys will be type KeyRunes. To get the rune value, check the Rune +// method on a Key struct, or use the Key.String() method: +// +// k := Key{Type: KeyRunes, Runes: []rune{'a'}, Alt: true} +// if k.Type == KeyRunes { +// +// fmt.Println(k.Runes) +// // Output: a +// +// fmt.Println(k.String()) +// // Output: alt+a +// +// } +type KeyType int + +func (k KeyType) String() (str string) { + if s, ok := keyNames[k]; ok { + return s + } + return "" +} + +// Control keys. We could do this with an iota, but the values are very +// specific, so we set the values explicitly to avoid any confusion. +// +// See also: +// https://en.wikipedia.org/wiki/C0_and_C1_control_codes +const ( + keyNUL KeyType = 0 // null, \0 + keySOH KeyType = 1 // start of heading + keySTX KeyType = 2 // start of text + keyETX KeyType = 3 // break, ctrl+c + keyEOT KeyType = 4 // end of transmission + keyENQ KeyType = 5 // enquiry + keyACK KeyType = 6 // acknowledge + keyBEL KeyType = 7 // bell, \a + keyBS KeyType = 8 // backspace + keyHT KeyType = 9 // horizontal tabulation, \t + keyLF KeyType = 10 // line feed, \n + keyVT KeyType = 11 // vertical tabulation \v + keyFF KeyType = 12 // form feed \f + keyCR KeyType = 13 // carriage return, \r + keySO KeyType = 14 // shift out + keySI KeyType = 15 // shift in + keyDLE KeyType = 16 // data link escape + keyDC1 KeyType = 17 // device control one + keyDC2 KeyType = 18 // device control two + keyDC3 KeyType = 19 // device control three + keyDC4 KeyType = 20 // device control four + keyNAK KeyType = 21 // negative acknowledge + keySYN KeyType = 22 // synchronous idle + keyETB KeyType = 23 // end of transmission block + keyCAN KeyType = 24 // cancel + keyEM KeyType = 25 // end of medium + keySUB KeyType = 26 // substitution + keyESC KeyType = 27 // escape, \e + keyFS KeyType = 28 // file separator + keyGS KeyType = 29 // group separator + keyRS KeyType = 30 // record separator + keyUS KeyType = 31 // unit separator + keyDEL KeyType = 127 // delete. on most systems this is mapped to backspace, I hear +) + +// Control key aliases. +const ( + KeyNull KeyType = keyNUL + KeyBreak KeyType = keyETX + KeyEnter KeyType = keyCR + KeyBackspace KeyType = keyDEL + KeyTab KeyType = keyHT + KeyEsc KeyType = keyESC + KeyEscape KeyType = keyESC + + KeyCtrlAt KeyType = keyNUL // ctrl+@ + KeyCtrlA KeyType = keySOH + KeyCtrlB KeyType = keySTX + KeyCtrlC KeyType = keyETX + KeyCtrlD KeyType = keyEOT + KeyCtrlE KeyType = keyENQ + KeyCtrlF KeyType = keyACK + KeyCtrlG KeyType = keyBEL + KeyCtrlH KeyType = keyBS + KeyCtrlI KeyType = keyHT + KeyCtrlJ KeyType = keyLF + KeyCtrlK KeyType = keyVT + KeyCtrlL KeyType = keyFF + KeyCtrlM KeyType = keyCR + KeyCtrlN KeyType = keySO + KeyCtrlO KeyType = keySI + KeyCtrlP KeyType = keyDLE + KeyCtrlQ KeyType = keyDC1 + KeyCtrlR KeyType = keyDC2 + KeyCtrlS KeyType = keyDC3 + KeyCtrlT KeyType = keyDC4 + KeyCtrlU KeyType = keyNAK + KeyCtrlV KeyType = keySYN + KeyCtrlW KeyType = keyETB + KeyCtrlX KeyType = keyCAN + KeyCtrlY KeyType = keyEM + KeyCtrlZ KeyType = keySUB + KeyCtrlOpenBracket KeyType = keyESC // ctrl+[ + KeyCtrlBackslash KeyType = keyFS // ctrl+\ + KeyCtrlCloseBracket KeyType = keyGS // ctrl+] + KeyCtrlCaret KeyType = keyRS // ctrl+^ + KeyCtrlUnderscore KeyType = keyUS // ctrl+_ + KeyCtrlQuestionMark KeyType = keyDEL // ctrl+? +) + +// Other keys. +const ( + KeyRunes KeyType = -(iota + 1) + KeyUp + KeyDown + KeyRight + KeyLeft + KeyShiftTab + KeyHome + KeyEnd + KeyPgUp + KeyPgDown + KeyCtrlPgUp + KeyCtrlPgDown + KeyDelete + KeyInsert + KeySpace + KeyCtrlUp + KeyCtrlDown + KeyCtrlRight + KeyCtrlLeft + KeyCtrlHome + KeyCtrlEnd + KeyShiftUp + KeyShiftDown + KeyShiftRight + KeyShiftLeft + KeyShiftHome + KeyShiftEnd + KeyCtrlShiftUp + KeyCtrlShiftDown + KeyCtrlShiftLeft + KeyCtrlShiftRight + KeyCtrlShiftHome + KeyCtrlShiftEnd + KeyF1 + KeyF2 + KeyF3 + KeyF4 + KeyF5 + KeyF6 + KeyF7 + KeyF8 + KeyF9 + KeyF10 + KeyF11 + KeyF12 + KeyF13 + KeyF14 + KeyF15 + KeyF16 + KeyF17 + KeyF18 + KeyF19 + KeyF20 +) + +// Mappings for control keys and other special keys to friendly consts. +var keyNames = map[KeyType]string{ + // Control keys. + keyNUL: "ctrl+@", // also ctrl+` (that's ctrl+backtick) + keySOH: "ctrl+a", + keySTX: "ctrl+b", + keyETX: "ctrl+c", + keyEOT: "ctrl+d", + keyENQ: "ctrl+e", + keyACK: "ctrl+f", + keyBEL: "ctrl+g", + keyBS: "ctrl+h", + keyHT: "tab", // also ctrl+i + keyLF: "ctrl+j", + keyVT: "ctrl+k", + keyFF: "ctrl+l", + keyCR: "enter", + keySO: "ctrl+n", + keySI: "ctrl+o", + keyDLE: "ctrl+p", + keyDC1: "ctrl+q", + keyDC2: "ctrl+r", + keyDC3: "ctrl+s", + keyDC4: "ctrl+t", + keyNAK: "ctrl+u", + keySYN: "ctrl+v", + keyETB: "ctrl+w", + keyCAN: "ctrl+x", + keyEM: "ctrl+y", + keySUB: "ctrl+z", + keyESC: "esc", + keyFS: "ctrl+\\", + keyGS: "ctrl+]", + keyRS: "ctrl+^", + keyUS: "ctrl+_", + keyDEL: "backspace", + + // Other keys. + KeyRunes: "runes", + KeyUp: "up", + KeyDown: "down", + KeyRight: "right", + KeySpace: " ", // for backwards compatibility + KeyLeft: "left", + KeyShiftTab: "shift+tab", + KeyHome: "home", + KeyEnd: "end", + KeyCtrlHome: "ctrl+home", + KeyCtrlEnd: "ctrl+end", + KeyShiftHome: "shift+home", + KeyShiftEnd: "shift+end", + KeyCtrlShiftHome: "ctrl+shift+home", + KeyCtrlShiftEnd: "ctrl+shift+end", + KeyPgUp: "pgup", + KeyPgDown: "pgdown", + KeyCtrlPgUp: "ctrl+pgup", + KeyCtrlPgDown: "ctrl+pgdown", + KeyDelete: "delete", + KeyInsert: "insert", + KeyCtrlUp: "ctrl+up", + KeyCtrlDown: "ctrl+down", + KeyCtrlRight: "ctrl+right", + KeyCtrlLeft: "ctrl+left", + KeyShiftUp: "shift+up", + KeyShiftDown: "shift+down", + KeyShiftRight: "shift+right", + KeyShiftLeft: "shift+left", + KeyCtrlShiftUp: "ctrl+shift+up", + KeyCtrlShiftDown: "ctrl+shift+down", + KeyCtrlShiftLeft: "ctrl+shift+left", + KeyCtrlShiftRight: "ctrl+shift+right", + KeyF1: "f1", + KeyF2: "f2", + KeyF3: "f3", + KeyF4: "f4", + KeyF5: "f5", + KeyF6: "f6", + KeyF7: "f7", + KeyF8: "f8", + KeyF9: "f9", + KeyF10: "f10", + KeyF11: "f11", + KeyF12: "f12", + KeyF13: "f13", + KeyF14: "f14", + KeyF15: "f15", + KeyF16: "f16", + KeyF17: "f17", + KeyF18: "f18", + KeyF19: "f19", + KeyF20: "f20", +} + +// Sequence mappings. +var sequences = map[string]Key{ + // Arrow keys + "\x1b[A": {Type: KeyUp}, + "\x1b[B": {Type: KeyDown}, + "\x1b[C": {Type: KeyRight}, + "\x1b[D": {Type: KeyLeft}, + "\x1b[1;2A": {Type: KeyShiftUp}, + "\x1b[1;2B": {Type: KeyShiftDown}, + "\x1b[1;2C": {Type: KeyShiftRight}, + "\x1b[1;2D": {Type: KeyShiftLeft}, + "\x1b[OA": {Type: KeyShiftUp}, // DECCKM + "\x1b[OB": {Type: KeyShiftDown}, // DECCKM + "\x1b[OC": {Type: KeyShiftRight}, // DECCKM + "\x1b[OD": {Type: KeyShiftLeft}, // DECCKM + "\x1b[a": {Type: KeyShiftUp}, // urxvt + "\x1b[b": {Type: KeyShiftDown}, // urxvt + "\x1b[c": {Type: KeyShiftRight}, // urxvt + "\x1b[d": {Type: KeyShiftLeft}, // urxvt + "\x1b[1;3A": {Type: KeyUp, Alt: true}, + "\x1b[1;3B": {Type: KeyDown, Alt: true}, + "\x1b[1;3C": {Type: KeyRight, Alt: true}, + "\x1b[1;3D": {Type: KeyLeft, Alt: true}, + + "\x1b[1;4A": {Type: KeyShiftUp, Alt: true}, + "\x1b[1;4B": {Type: KeyShiftDown, Alt: true}, + "\x1b[1;4C": {Type: KeyShiftRight, Alt: true}, + "\x1b[1;4D": {Type: KeyShiftLeft, Alt: true}, + + "\x1b[1;5A": {Type: KeyCtrlUp}, + "\x1b[1;5B": {Type: KeyCtrlDown}, + "\x1b[1;5C": {Type: KeyCtrlRight}, + "\x1b[1;5D": {Type: KeyCtrlLeft}, + "\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt + "\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt + "\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt + "\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt + "\x1b[1;6A": {Type: KeyCtrlShiftUp}, + "\x1b[1;6B": {Type: KeyCtrlShiftDown}, + "\x1b[1;6C": {Type: KeyCtrlShiftRight}, + "\x1b[1;6D": {Type: KeyCtrlShiftLeft}, + "\x1b[1;7A": {Type: KeyCtrlUp, Alt: true}, + "\x1b[1;7B": {Type: KeyCtrlDown, Alt: true}, + "\x1b[1;7C": {Type: KeyCtrlRight, Alt: true}, + "\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true}, + "\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true}, + "\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true}, + "\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true}, + "\x1b[1;8D": {Type: KeyCtrlShiftLeft, Alt: true}, + + // Miscellaneous keys + "\x1b[Z": {Type: KeyShiftTab}, + + "\x1b[2~": {Type: KeyInsert}, + "\x1b[3;2~": {Type: KeyInsert, Alt: true}, + + "\x1b[3~": {Type: KeyDelete}, + "\x1b[3;3~": {Type: KeyDelete, Alt: true}, + + "\x1b[5~": {Type: KeyPgUp}, + "\x1b[5;3~": {Type: KeyPgUp, Alt: true}, + "\x1b[5;5~": {Type: KeyCtrlPgUp}, + "\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt + "\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true}, + + "\x1b[6~": {Type: KeyPgDown}, + "\x1b[6;3~": {Type: KeyPgDown, Alt: true}, + "\x1b[6;5~": {Type: KeyCtrlPgDown}, + "\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt + "\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true}, + + "\x1b[1~": {Type: KeyHome}, + "\x1b[H": {Type: KeyHome}, // xterm, lxterm + "\x1b[1;3H": {Type: KeyHome, Alt: true}, // xterm, lxterm + "\x1b[1;5H": {Type: KeyCtrlHome}, // xterm, lxterm + "\x1b[1;7H": {Type: KeyCtrlHome, Alt: true}, // xterm, lxterm + "\x1b[1;2H": {Type: KeyShiftHome}, // xterm, lxterm + "\x1b[1;4H": {Type: KeyShiftHome, Alt: true}, // xterm, lxterm + "\x1b[1;6H": {Type: KeyCtrlShiftHome}, // xterm, lxterm + "\x1b[1;8H": {Type: KeyCtrlShiftHome, Alt: true}, // xterm, lxterm + + "\x1b[4~": {Type: KeyEnd}, + "\x1b[F": {Type: KeyEnd}, // xterm, lxterm + "\x1b[1;3F": {Type: KeyEnd, Alt: true}, // xterm, lxterm + "\x1b[1;5F": {Type: KeyCtrlEnd}, // xterm, lxterm + "\x1b[1;7F": {Type: KeyCtrlEnd, Alt: true}, // xterm, lxterm + "\x1b[1;2F": {Type: KeyShiftEnd}, // xterm, lxterm + "\x1b[1;4F": {Type: KeyShiftEnd, Alt: true}, // xterm, lxterm + "\x1b[1;6F": {Type: KeyCtrlShiftEnd}, // xterm, lxterm + "\x1b[1;8F": {Type: KeyCtrlShiftEnd, Alt: true}, // xterm, lxterm + + "\x1b[7~": {Type: KeyHome}, // urxvt + "\x1b[7^": {Type: KeyCtrlHome}, // urxvt + "\x1b[7$": {Type: KeyShiftHome}, // urxvt + "\x1b[7@": {Type: KeyCtrlShiftHome}, // urxvt + + "\x1b[8~": {Type: KeyEnd}, // urxvt + "\x1b[8^": {Type: KeyCtrlEnd}, // urxvt + "\x1b[8$": {Type: KeyShiftEnd}, // urxvt + "\x1b[8@": {Type: KeyCtrlShiftEnd}, // urxvt + + // Function keys, Linux console + "\x1b[[A": {Type: KeyF1}, // linux console + "\x1b[[B": {Type: KeyF2}, // linux console + "\x1b[[C": {Type: KeyF3}, // linux console + "\x1b[[D": {Type: KeyF4}, // linux console + "\x1b[[E": {Type: KeyF5}, // linux console + + // Function keys, X11 + "\x1bOP": {Type: KeyF1}, // vt100, xterm + "\x1bOQ": {Type: KeyF2}, // vt100, xterm + "\x1bOR": {Type: KeyF3}, // vt100, xterm + "\x1bOS": {Type: KeyF4}, // vt100, xterm + + "\x1b[1;3P": {Type: KeyF1, Alt: true}, // vt100, xterm + "\x1b[1;3Q": {Type: KeyF2, Alt: true}, // vt100, xterm + "\x1b[1;3R": {Type: KeyF3, Alt: true}, // vt100, xterm + "\x1b[1;3S": {Type: KeyF4, Alt: true}, // vt100, xterm + + "\x1b[11~": {Type: KeyF1}, // urxvt + "\x1b[12~": {Type: KeyF2}, // urxvt + "\x1b[13~": {Type: KeyF3}, // urxvt + "\x1b[14~": {Type: KeyF4}, // urxvt + + "\x1b[15~": {Type: KeyF5}, // vt100, xterm, also urxvt + + "\x1b[15;3~": {Type: KeyF5, Alt: true}, // vt100, xterm, also urxvt + + "\x1b[17~": {Type: KeyF6}, // vt100, xterm, also urxvt + "\x1b[18~": {Type: KeyF7}, // vt100, xterm, also urxvt + "\x1b[19~": {Type: KeyF8}, // vt100, xterm, also urxvt + "\x1b[20~": {Type: KeyF9}, // vt100, xterm, also urxvt + "\x1b[21~": {Type: KeyF10}, // vt100, xterm, also urxvt + + "\x1b[17;3~": {Type: KeyF6, Alt: true}, // vt100, xterm + "\x1b[18;3~": {Type: KeyF7, Alt: true}, // vt100, xterm + "\x1b[19;3~": {Type: KeyF8, Alt: true}, // vt100, xterm + "\x1b[20;3~": {Type: KeyF9, Alt: true}, // vt100, xterm + "\x1b[21;3~": {Type: KeyF10, Alt: true}, // vt100, xterm + + "\x1b[23~": {Type: KeyF11}, // vt100, xterm, also urxvt + "\x1b[24~": {Type: KeyF12}, // vt100, xterm, also urxvt + + "\x1b[23;3~": {Type: KeyF11, Alt: true}, // vt100, xterm + "\x1b[24;3~": {Type: KeyF12, Alt: true}, // vt100, xterm + + "\x1b[1;2P": {Type: KeyF13}, + "\x1b[1;2Q": {Type: KeyF14}, + + "\x1b[25~": {Type: KeyF13}, // vt100, xterm, also urxvt + "\x1b[26~": {Type: KeyF14}, // vt100, xterm, also urxvt + + "\x1b[25;3~": {Type: KeyF13, Alt: true}, // vt100, xterm + "\x1b[26;3~": {Type: KeyF14, Alt: true}, // vt100, xterm + + "\x1b[1;2R": {Type: KeyF15}, + "\x1b[1;2S": {Type: KeyF16}, + + "\x1b[28~": {Type: KeyF15}, // vt100, xterm, also urxvt + "\x1b[29~": {Type: KeyF16}, // vt100, xterm, also urxvt + + "\x1b[28;3~": {Type: KeyF15, Alt: true}, // vt100, xterm + "\x1b[29;3~": {Type: KeyF16, Alt: true}, // vt100, xterm + + "\x1b[15;2~": {Type: KeyF17}, + "\x1b[17;2~": {Type: KeyF18}, + "\x1b[18;2~": {Type: KeyF19}, + "\x1b[19;2~": {Type: KeyF20}, + + "\x1b[31~": {Type: KeyF17}, + "\x1b[32~": {Type: KeyF18}, + "\x1b[33~": {Type: KeyF19}, + "\x1b[34~": {Type: KeyF20}, + + // Powershell sequences. + "\x1bOA": {Type: KeyUp, Alt: false}, + "\x1bOB": {Type: KeyDown, Alt: false}, + "\x1bOC": {Type: KeyRight, Alt: false}, + "\x1bOD": {Type: KeyLeft, Alt: false}, +} + +// unknownInputByteMsg is reported by the input reader when an invalid +// utf-8 byte is detected on the input. Currently, it is not handled +// further by bubbletea. However, having this event makes it possible +// to troubleshoot invalid inputs. +type unknownInputByteMsg byte + +func (u unknownInputByteMsg) String() string { + return fmt.Sprintf("?%#02x?", int(u)) +} + +// unknownCSISequenceMsg is reported by the input reader when an +// unrecognized CSI sequence is detected on the input. Currently, it +// is not handled further by bubbletea. However, having this event +// makes it possible to troubleshoot invalid inputs. +type unknownCSISequenceMsg []byte + +func (u unknownCSISequenceMsg) String() string { + return fmt.Sprintf("?CSI%+v?", []byte(u)[2:]) +} + +var spaceRunes = []rune{' '} + +// readAnsiInputs reads keypress and mouse inputs from a TTY and produces messages +// containing information about the key or mouse events accordingly. +func readAnsiInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { + var buf [256]byte + + var leftOverFromPrevIteration []byte +loop: + for { + // Read and block. + numBytes, err := input.Read(buf[:]) + if err != nil { + return fmt.Errorf("error reading input: %w", err) + } + b := buf[:numBytes] + if leftOverFromPrevIteration != nil { + b = append(leftOverFromPrevIteration, b...) + } + + // If we had a short read (numBytes < len(buf)), we're sure that + // the end of this read is an event boundary, so there is no doubt + // if we are encountering the end of the buffer while parsing a message. + // However, if we've succeeded in filling up the buffer, there may + // be more data in the OS buffer ready to be read in, to complete + // the last message in the input. In that case, we will retry with + // the left over data in the next iteration. + canHaveMoreData := numBytes == len(buf) + + var i, w int + for i, w = 0, 0; i < len(b); i += w { + var msg Msg + w, msg = detectOneMsg(b[i:], canHaveMoreData) + if w == 0 { + // Expecting more bytes beyond the current buffer. Try waiting + // for more input. + leftOverFromPrevIteration = make([]byte, 0, len(b[i:])+len(buf)) + leftOverFromPrevIteration = append(leftOverFromPrevIteration, b[i:]...) + continue loop + } + + select { + case msgs <- msg: + case <-ctx.Done(): + err := ctx.Err() + if err != nil { + err = fmt.Errorf("found context error while reading input: %w", err) + } + return err + } + } + leftOverFromPrevIteration = nil + } +} + +var ( + unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`) + mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`) +) + +func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) { + // Detect mouse events. + // X10 mouse events have a length of 6 bytes + const mouseEventX10Len = 6 + if len(b) >= mouseEventX10Len && b[0] == '\x1b' && b[1] == '[' { + switch b[2] { + case 'M': + return mouseEventX10Len, MouseMsg(parseX10MouseEvent(b)) + case '<': + if matchIndices := mouseSGRRegex.FindSubmatchIndex(b[3:]); matchIndices != nil { + // SGR mouse events length is the length of the match plus the length of the escape sequence + mouseEventSGRLen := matchIndices[1] + 3 //nolint:gomnd + return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b)) + } + } + } + + // Detect focus events. + var foundRF bool + foundRF, w, msg = detectReportFocus(b) + if foundRF { + return w, msg + } + + // Detect bracketed paste. + var foundbp bool + foundbp, w, msg = detectBracketedPaste(b) + if foundbp { + return w, msg + } + + // Detect escape sequence and control characters other than NUL, + // possibly with an escape character in front to mark the Alt + // modifier. + var foundSeq bool + foundSeq, w, msg = detectSequence(b) + if foundSeq { + return w, msg + } + + // No non-NUL control character or escape sequence. + // If we are seeing at least an escape character, remember it for later below. + alt := false + i := 0 + if b[0] == '\x1b' { + alt = true + i++ + } + + // Are we seeing a standalone NUL? This is not handled by detectSequence(). + if i < len(b) && b[i] == 0 { + return i + 1, KeyMsg{Type: keyNUL, Alt: alt} + } + + // Find the longest sequence of runes that are not control + // characters from this point. + var runes []rune + for rw := 0; i < len(b); i += rw { + var r rune + r, rw = utf8.DecodeRune(b[i:]) + if r == utf8.RuneError || r <= rune(keyUS) || r == rune(keyDEL) || r == ' ' { + // Rune errors are handled below; control characters and spaces will + // be handled by detectSequence in the next call to detectOneMsg. + break + } + runes = append(runes, r) + if alt { + // We only support a single rune after an escape alt modifier. + i += rw + break + } + } + if i >= len(b) && canHaveMoreData { + // We have encountered the end of the input buffer. Alas, we can't + // be sure whether the data in the remainder of the buffer is + // complete (maybe there was a short read). Instead of sending anything + // dumb to the message channel, do a short read. The outer loop will + // handle this case by extending the buffer as necessary. + return 0, nil + } + + // If we found at least one rune, we report the bunch of them as + // a single KeyRunes or KeySpace event. + if len(runes) > 0 { + k := Key{Type: KeyRunes, Runes: runes, Alt: alt} + if len(runes) == 1 && runes[0] == ' ' { + k.Type = KeySpace + } + return i, KeyMsg(k) + } + + // We didn't find an escape sequence, nor a valid rune. Was this a + // lone escape character at the end of the input? + if alt && len(b) == 1 { + return 1, KeyMsg(Key{Type: KeyEscape}) + } + + // The character at the current position is neither an escape + // sequence, a valid rune start or a sole escape character. Report + // it as an invalid byte. + return 1, unknownInputByteMsg(b[0]) +} diff --git a/vendor/github.com/charmbracelet/bubbletea/key_other.go b/vendor/github.com/charmbracelet/bubbletea/key_other.go new file mode 100644 index 00000000..b8c46082 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/key_other.go @@ -0,0 +1,13 @@ +//go:build !windows +// +build !windows + +package tea + +import ( + "context" + "io" +) + +func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { + return readAnsiInputs(ctx, msgs, input) +} diff --git a/vendor/github.com/charmbracelet/bubbletea/key_sequences.go b/vendor/github.com/charmbracelet/bubbletea/key_sequences.go new file mode 100644 index 00000000..15483ef5 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/key_sequences.go @@ -0,0 +1,131 @@ +package tea + +import ( + "bytes" + "sort" + "unicode/utf8" +) + +// extSequences is used by the map-based algorithm below. It contains +// the sequences plus their alternatives with an escape character +// prefixed, plus the control chars, plus the space. +// It does not contain the NUL character, which is handled specially +// by detectOneMsg. +var extSequences = func() map[string]Key { + s := map[string]Key{} + for seq, key := range sequences { + key := key + s[seq] = key + if !key.Alt { + key.Alt = true + s["\x1b"+seq] = key + } + } + for i := keyNUL + 1; i <= keyDEL; i++ { + if i == keyESC { + continue + } + s[string([]byte{byte(i)})] = Key{Type: i} + s[string([]byte{'\x1b', byte(i)})] = Key{Type: i, Alt: true} + if i == keyUS { + i = keyDEL - 1 + } + } + s[" "] = Key{Type: KeySpace, Runes: spaceRunes} + s["\x1b "] = Key{Type: KeySpace, Alt: true, Runes: spaceRunes} + s["\x1b\x1b"] = Key{Type: KeyEscape, Alt: true} + return s +}() + +// seqLengths is the sizes of valid sequences, starting with the +// largest size. +var seqLengths = func() []int { + sizes := map[int]struct{}{} + for seq := range extSequences { + sizes[len(seq)] = struct{}{} + } + lsizes := make([]int, 0, len(sizes)) + for sz := range sizes { + lsizes = append(lsizes, sz) + } + sort.Slice(lsizes, func(i, j int) bool { return lsizes[i] > lsizes[j] }) + return lsizes +}() + +// detectSequence uses a longest prefix match over the input +// sequence and a hash map. +func detectSequence(input []byte) (hasSeq bool, width int, msg Msg) { + seqs := extSequences + for _, sz := range seqLengths { + if sz > len(input) { + continue + } + prefix := input[:sz] + key, ok := seqs[string(prefix)] + if ok { + return true, sz, KeyMsg(key) + } + } + // Is this an unknown CSI sequence? + if loc := unknownCSIRe.FindIndex(input); loc != nil { + return true, loc[1], unknownCSISequenceMsg(input[:loc[1]]) + } + + return false, 0, nil +} + +// detectBracketedPaste detects an input pasted while bracketed +// paste mode was enabled. +// +// Note: this function is a no-op if bracketed paste was not enabled +// on the terminal, since in that case we'd never see this +// particular escape sequence. +func detectBracketedPaste(input []byte) (hasBp bool, width int, msg Msg) { + // Detect the start sequence. + const bpStart = "\x1b[200~" + if len(input) < len(bpStart) || string(input[:len(bpStart)]) != bpStart { + return false, 0, nil + } + + // Skip over the start sequence. + input = input[len(bpStart):] + + // If we saw the start sequence, then we must have an end sequence + // as well. Find it. + const bpEnd = "\x1b[201~" + idx := bytes.Index(input, []byte(bpEnd)) + inputLen := len(bpStart) + idx + len(bpEnd) + if idx == -1 { + // We have encountered the end of the input buffer without seeing + // the marker for the end of the bracketed paste. + // Tell the outer loop we have done a short read and we want more. + return true, 0, nil + } + + // The paste is everything in-between. + paste := input[:idx] + + // All there is in-between is runes, not to be interpreted further. + k := Key{Type: KeyRunes, Paste: true} + for len(paste) > 0 { + r, w := utf8.DecodeRune(paste) + if r != utf8.RuneError { + k.Runes = append(k.Runes, r) + } + paste = paste[w:] + } + + return true, inputLen, KeyMsg(k) +} + +// detectReportFocus detects a focus report sequence. +// nolint: gomnd +func detectReportFocus(input []byte) (hasRF bool, width int, msg Msg) { + switch { + case bytes.Equal(input, []byte("\x1b[I")): + return true, 3, FocusMsg{} + case bytes.Equal(input, []byte("\x1b[O")): + return true, 3, BlurMsg{} + } + return false, 0, nil +} diff --git a/vendor/github.com/charmbracelet/bubbletea/key_windows.go b/vendor/github.com/charmbracelet/bubbletea/key_windows.go new file mode 100644 index 00000000..b693efd6 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/key_windows.go @@ -0,0 +1,351 @@ +//go:build windows +// +build windows + +package tea + +import ( + "context" + "fmt" + "io" + + "github.com/erikgeiser/coninput" + localereader "github.com/mattn/go-localereader" + "golang.org/x/sys/windows" +) + +func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error { + if coninReader, ok := input.(*conInputReader); ok { + return readConInputs(ctx, msgs, coninReader.conin) + } + + return readAnsiInputs(ctx, msgs, localereader.NewReader(input)) +} + +func readConInputs(ctx context.Context, msgsch chan<- Msg, con windows.Handle) error { + var ps coninput.ButtonState // keep track of previous mouse state + var ws coninput.WindowBufferSizeEventRecord // keep track of the last window size event + for { + events, err := coninput.ReadNConsoleInputs(con, 16) + if err != nil { + return fmt.Errorf("read coninput events: %w", err) + } + + for _, event := range events { + var msgs []Msg + switch e := event.Unwrap().(type) { + case coninput.KeyEventRecord: + if !e.KeyDown || e.VirtualKeyCode == coninput.VK_SHIFT { + continue + } + + for i := 0; i < int(e.RepeatCount); i++ { + eventKeyType := keyType(e) + var runes []rune + + // Add the character only if the key type is an actual character and not a control sequence. + // This mimics the behavior in readAnsiInputs where the character is also removed. + // We don't need to handle KeySpace here. See the comment in keyType(). + if eventKeyType == KeyRunes { + runes = []rune{e.Char} + } + + msgs = append(msgs, KeyMsg{ + Type: eventKeyType, + Runes: runes, + Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED), + }) + } + case coninput.WindowBufferSizeEventRecord: + if e != ws { + ws = e + msgs = append(msgs, WindowSizeMsg{ + Width: int(e.Size.X), + Height: int(e.Size.Y), + }) + } + case coninput.MouseEventRecord: + event := mouseEvent(ps, e) + if event.Type != MouseUnknown { + msgs = append(msgs, event) + } + ps = e.ButtonState + case coninput.FocusEventRecord, coninput.MenuEventRecord: + // ignore + default: // unknown event + continue + } + + // Send all messages to the channel + for _, msg := range msgs { + select { + case msgsch <- msg: + case <-ctx.Done(): + err := ctx.Err() + if err != nil { + return fmt.Errorf("coninput context error: %w", err) + } + return err + } + } + } + } +} + +func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action MouseAction) { + btn := p ^ s + action = MouseActionPress + if btn&s == 0 { + action = MouseActionRelease + } + + if btn == 0 { + switch { + case s&coninput.FROM_LEFT_1ST_BUTTON_PRESSED > 0: + button = MouseButtonLeft + case s&coninput.FROM_LEFT_2ND_BUTTON_PRESSED > 0: + button = MouseButtonMiddle + case s&coninput.RIGHTMOST_BUTTON_PRESSED > 0: + button = MouseButtonRight + case s&coninput.FROM_LEFT_3RD_BUTTON_PRESSED > 0: + button = MouseButtonBackward + case s&coninput.FROM_LEFT_4TH_BUTTON_PRESSED > 0: + button = MouseButtonForward + } + return + } + + switch { + case btn == coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button + button = MouseButtonLeft + case btn == coninput.RIGHTMOST_BUTTON_PRESSED: // right button + button = MouseButtonRight + case btn == coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button + button = MouseButtonMiddle + case btn == coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward) + button = MouseButtonBackward + case btn == coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward) + button = MouseButtonForward + } + + return button, action +} + +func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) MouseMsg { + ev := MouseMsg{ + X: int(e.MousePositon.X), + Y: int(e.MousePositon.Y), + Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED), + Ctrl: e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED), + Shift: e.ControlKeyState.Contains(coninput.SHIFT_PRESSED), + } + switch e.EventFlags { + case coninput.CLICK, coninput.DOUBLE_CLICK: + ev.Button, ev.Action = mouseEventButton(p, e.ButtonState) + if ev.Action == MouseActionRelease { + ev.Type = MouseRelease + } + switch ev.Button { + case MouseButtonLeft: + ev.Type = MouseLeft + case MouseButtonMiddle: + ev.Type = MouseMiddle + case MouseButtonRight: + ev.Type = MouseRight + case MouseButtonBackward: + ev.Type = MouseBackward + case MouseButtonForward: + ev.Type = MouseForward + } + case coninput.MOUSE_WHEELED: + if e.WheelDirection > 0 { + ev.Button = MouseButtonWheelUp + ev.Type = MouseWheelUp + } else { + ev.Button = MouseButtonWheelDown + ev.Type = MouseWheelDown + } + case coninput.MOUSE_HWHEELED: + if e.WheelDirection > 0 { + ev.Button = MouseButtonWheelRight + ev.Type = MouseWheelRight + } else { + ev.Button = MouseButtonWheelLeft + ev.Type = MouseWheelLeft + } + case coninput.MOUSE_MOVED: + ev.Button, _ = mouseEventButton(p, e.ButtonState) + ev.Action = MouseActionMotion + ev.Type = MouseMotion + } + + return ev +} + +func keyType(e coninput.KeyEventRecord) KeyType { + code := e.VirtualKeyCode + + shiftPressed := e.ControlKeyState.Contains(coninput.SHIFT_PRESSED) + ctrlPressed := e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED) + + switch code { + case coninput.VK_RETURN: + return KeyEnter + case coninput.VK_BACK: + return KeyBackspace + case coninput.VK_TAB: + if shiftPressed { + return KeyShiftTab + } + return KeyTab + case coninput.VK_SPACE: + return KeyRunes // this could be KeySpace but on unix space also produces KeyRunes + case coninput.VK_ESCAPE: + return KeyEscape + case coninput.VK_UP: + switch { + case shiftPressed && ctrlPressed: + return KeyCtrlShiftUp + case shiftPressed: + return KeyShiftUp + case ctrlPressed: + return KeyCtrlUp + default: + return KeyUp + } + case coninput.VK_DOWN: + switch { + case shiftPressed && ctrlPressed: + return KeyCtrlShiftDown + case shiftPressed: + return KeyShiftDown + case ctrlPressed: + return KeyCtrlDown + default: + return KeyDown + } + case coninput.VK_RIGHT: + switch { + case shiftPressed && ctrlPressed: + return KeyCtrlShiftRight + case shiftPressed: + return KeyShiftRight + case ctrlPressed: + return KeyCtrlRight + default: + return KeyRight + } + case coninput.VK_LEFT: + switch { + case shiftPressed && ctrlPressed: + return KeyCtrlShiftLeft + case shiftPressed: + return KeyShiftLeft + case ctrlPressed: + return KeyCtrlLeft + default: + return KeyLeft + } + case coninput.VK_HOME: + switch { + case shiftPressed && ctrlPressed: + return KeyCtrlShiftHome + case shiftPressed: + return KeyShiftHome + case ctrlPressed: + return KeyCtrlHome + default: + return KeyHome + } + case coninput.VK_END: + switch { + case shiftPressed && ctrlPressed: + return KeyCtrlShiftEnd + case shiftPressed: + return KeyShiftEnd + case ctrlPressed: + return KeyCtrlEnd + default: + return KeyEnd + } + case coninput.VK_PRIOR: + return KeyPgUp + case coninput.VK_NEXT: + return KeyPgDown + case coninput.VK_DELETE: + return KeyDelete + default: + if e.ControlKeyState&(coninput.LEFT_CTRL_PRESSED|coninput.RIGHT_CTRL_PRESSED) == 0 { + return KeyRunes + } + + switch e.Char { + case '@': + return KeyCtrlAt + case '\x01': + return KeyCtrlA + case '\x02': + return KeyCtrlB + case '\x03': + return KeyCtrlC + case '\x04': + return KeyCtrlD + case '\x05': + return KeyCtrlE + case '\x06': + return KeyCtrlF + case '\a': + return KeyCtrlG + case '\b': + return KeyCtrlH + case '\t': + return KeyCtrlI + case '\n': + return KeyCtrlJ + case '\v': + return KeyCtrlK + case '\f': + return KeyCtrlL + case '\r': + return KeyCtrlM + case '\x0e': + return KeyCtrlN + case '\x0f': + return KeyCtrlO + case '\x10': + return KeyCtrlP + case '\x11': + return KeyCtrlQ + case '\x12': + return KeyCtrlR + case '\x13': + return KeyCtrlS + case '\x14': + return KeyCtrlT + case '\x15': + return KeyCtrlU + case '\x16': + return KeyCtrlV + case '\x17': + return KeyCtrlW + case '\x18': + return KeyCtrlX + case '\x19': + return KeyCtrlY + case '\x1a': + return KeyCtrlZ + case '\x1b': + return KeyCtrlCloseBracket + case '\x1c': + return KeyCtrlBackslash + case '\x1f': + return KeyCtrlUnderscore + } + + switch code { + case coninput.VK_OEM_4: + return KeyCtrlOpenBracket + } + + return KeyRunes + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/logging.go b/vendor/github.com/charmbracelet/bubbletea/logging.go new file mode 100644 index 00000000..a5311819 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/logging.go @@ -0,0 +1,53 @@ +package tea + +import ( + "fmt" + "io" + "log" + "os" + "unicode" +) + +// LogToFile sets up default logging to log to a file. This is helpful as we +// can't print to the terminal since our TUI is occupying it. If the file +// doesn't exist it will be created. +// +// Don't forget to close the file when you're done with it. +// +// f, err := LogToFile("debug.log", "debug") +// if err != nil { +// fmt.Println("fatal:", err) +// os.Exit(1) +// } +// defer f.Close() +func LogToFile(path string, prefix string) (*os.File, error) { + return LogToFileWith(path, prefix, log.Default()) +} + +// LogOptionsSetter is an interface implemented by stdlib's log and charm's log +// libraries. +type LogOptionsSetter interface { + SetOutput(io.Writer) + SetPrefix(string) +} + +// LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter. +func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) { + f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:gomnd + if err != nil { + return nil, fmt.Errorf("error opening file for logging: %w", err) + } + log.SetOutput(f) + + // Add a space after the prefix if a prefix is being specified and it + // doesn't already have a trailing space. + if len(prefix) > 0 { + finalChar := prefix[len(prefix)-1] + if !unicode.IsSpace(rune(finalChar)) { + prefix += " " + } + } + log.SetPrefix(prefix) + + return f, nil +} diff --git a/vendor/github.com/charmbracelet/bubbletea/mouse.go b/vendor/github.com/charmbracelet/bubbletea/mouse.go new file mode 100644 index 00000000..6ec51cc0 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/mouse.go @@ -0,0 +1,308 @@ +package tea + +import "strconv" + +// MouseMsg contains information about a mouse event and are sent to a programs +// update function when mouse activity occurs. Note that the mouse must first +// be enabled in order for the mouse events to be received. +type MouseMsg MouseEvent + +// String returns a string representation of a mouse event. +func (m MouseMsg) String() string { + return MouseEvent(m).String() +} + +// MouseEvent represents a mouse event, which could be a click, a scroll wheel +// movement, a cursor movement, or a combination. +type MouseEvent struct { + X int + Y int + Shift bool + Alt bool + Ctrl bool + Action MouseAction + Button MouseButton + + // Deprecated: Use MouseAction & MouseButton instead. + Type MouseEventType +} + +// IsWheel returns true if the mouse event is a wheel event. +func (m MouseEvent) IsWheel() bool { + return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown || + m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight +} + +// String returns a string representation of a mouse event. +func (m MouseEvent) String() (s string) { + if m.Ctrl { + s += "ctrl+" + } + if m.Alt { + s += "alt+" + } + if m.Shift { + s += "shift+" + } + + if m.Button == MouseButtonNone { //nolint:nestif + if m.Action == MouseActionMotion || m.Action == MouseActionRelease { + s += mouseActions[m.Action] + } else { + s += "unknown" + } + } else if m.IsWheel() { + s += mouseButtons[m.Button] + } else { + btn := mouseButtons[m.Button] + if btn != "" { + s += btn + } + act := mouseActions[m.Action] + if act != "" { + s += " " + act + } + } + + return s +} + +// MouseAction represents the action that occurred during a mouse event. +type MouseAction int + +// Mouse event actions. +const ( + MouseActionPress MouseAction = iota + MouseActionRelease + MouseActionMotion +) + +var mouseActions = map[MouseAction]string{ + MouseActionPress: "press", + MouseActionRelease: "release", + MouseActionMotion: "motion", +} + +// MouseButton represents the button that was pressed during a mouse event. +type MouseButton int + +// Mouse event buttons +// +// This is based on X11 mouse button codes. +// +// 1 = left button +// 2 = middle button (pressing the scroll wheel) +// 3 = right button +// 4 = turn scroll wheel up +// 5 = turn scroll wheel down +// 6 = push scroll wheel left +// 7 = push scroll wheel right +// 8 = 4th button (aka browser backward button) +// 9 = 5th button (aka browser forward button) +// 10 +// 11 +// +// Other buttons are not supported. +const ( + MouseButtonNone MouseButton = iota + MouseButtonLeft + MouseButtonMiddle + MouseButtonRight + MouseButtonWheelUp + MouseButtonWheelDown + MouseButtonWheelLeft + MouseButtonWheelRight + MouseButtonBackward + MouseButtonForward + MouseButton10 + MouseButton11 +) + +var mouseButtons = map[MouseButton]string{ + MouseButtonNone: "none", + MouseButtonLeft: "left", + MouseButtonMiddle: "middle", + MouseButtonRight: "right", + MouseButtonWheelUp: "wheel up", + MouseButtonWheelDown: "wheel down", + MouseButtonWheelLeft: "wheel left", + MouseButtonWheelRight: "wheel right", + MouseButtonBackward: "backward", + MouseButtonForward: "forward", + MouseButton10: "button 10", + MouseButton11: "button 11", +} + +// MouseEventType indicates the type of mouse event occurring. +// +// Deprecated: Use MouseAction & MouseButton instead. +type MouseEventType int + +// Mouse event types. +// +// Deprecated: Use MouseAction & MouseButton instead. +const ( + MouseUnknown MouseEventType = iota + MouseLeft + MouseRight + MouseMiddle + MouseRelease // mouse button release (X10 only) + MouseWheelUp + MouseWheelDown + MouseWheelLeft + MouseWheelRight + MouseBackward + MouseForward + MouseMotion +) + +// Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events +// look like: +// +// ESC [ < Cb ; Cx ; Cy (M or m) +// +// where: +// +// Cb is the encoded button code +// Cx is the x-coordinate of the mouse +// Cy is the y-coordinate of the mouse +// M is for button press, m is for button release +// +// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates +func parseSGRMouseEvent(buf []byte) MouseEvent { + str := string(buf[3:]) + matches := mouseSGRRegex.FindStringSubmatch(str) + if len(matches) != 5 { //nolint:gomnd + // Unreachable, we already checked the regex in `detectOneMsg`. + panic("invalid mouse event") + } + + b, _ := strconv.Atoi(matches[1]) + px := matches[2] + py := matches[3] + release := matches[4] == "m" + m := parseMouseButton(b, true) + + // Wheel buttons don't have release events + // Motion can be reported as a release event in some terminals (Windows Terminal) + if m.Action != MouseActionMotion && !m.IsWheel() && release { + m.Action = MouseActionRelease + m.Type = MouseRelease + } + + x, _ := strconv.Atoi(px) + y, _ := strconv.Atoi(py) + + // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). + m.X = x - 1 + m.Y = y - 1 + + return m +} + +const x10MouseByteOffset = 32 + +// Parse X10-encoded mouse events; the simplest kind. The last release of X10 +// was December 1986, by the way. The original X10 mouse protocol limits the Cx +// and Cy coordinates to 223 (=255-032). +// +// X10 mouse events look like: +// +// ESC [M Cb Cx Cy +// +// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking +func parseX10MouseEvent(buf []byte) MouseEvent { + v := buf[3:6] + m := parseMouseButton(int(v[0]), false) + + // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). + m.X = int(v[1]) - x10MouseByteOffset - 1 + m.Y = int(v[2]) - x10MouseByteOffset - 1 + + return m +} + +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates +func parseMouseButton(b int, isSGR bool) MouseEvent { + var m MouseEvent + e := b + if !isSGR { + e -= x10MouseByteOffset + } + + const ( + bitShift = 0b0000_0100 + bitAlt = 0b0000_1000 + bitCtrl = 0b0001_0000 + bitMotion = 0b0010_0000 + bitWheel = 0b0100_0000 + bitAdd = 0b1000_0000 // additional buttons 8-11 + + bitsMask = 0b0000_0011 + ) + + if e&bitAdd != 0 { + m.Button = MouseButtonBackward + MouseButton(e&bitsMask) + } else if e&bitWheel != 0 { + m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask) + } else { + m.Button = MouseButtonLeft + MouseButton(e&bitsMask) + // X10 reports a button release as 0b0000_0011 (3) + if e&bitsMask == bitsMask { + m.Action = MouseActionRelease + m.Button = MouseButtonNone + } + } + + // Motion bit doesn't get reported for wheel events. + if e&bitMotion != 0 && !m.IsWheel() { + m.Action = MouseActionMotion + } + + // Modifiers + m.Alt = e&bitAlt != 0 + m.Ctrl = e&bitCtrl != 0 + m.Shift = e&bitShift != 0 + + // backward compatibility + switch { + case m.Button == MouseButtonLeft && m.Action == MouseActionPress: + m.Type = MouseLeft + case m.Button == MouseButtonMiddle && m.Action == MouseActionPress: + m.Type = MouseMiddle + case m.Button == MouseButtonRight && m.Action == MouseActionPress: + m.Type = MouseRight + case m.Button == MouseButtonNone && m.Action == MouseActionRelease: + m.Type = MouseRelease + case m.Button == MouseButtonWheelUp && m.Action == MouseActionPress: + m.Type = MouseWheelUp + case m.Button == MouseButtonWheelDown && m.Action == MouseActionPress: + m.Type = MouseWheelDown + case m.Button == MouseButtonWheelLeft && m.Action == MouseActionPress: + m.Type = MouseWheelLeft + case m.Button == MouseButtonWheelRight && m.Action == MouseActionPress: + m.Type = MouseWheelRight + case m.Button == MouseButtonBackward && m.Action == MouseActionPress: + m.Type = MouseBackward + case m.Button == MouseButtonForward && m.Action == MouseActionPress: + m.Type = MouseForward + case m.Action == MouseActionMotion: + m.Type = MouseMotion + switch m.Button { //nolint:exhaustive + case MouseButtonLeft: + m.Type = MouseLeft + case MouseButtonMiddle: + m.Type = MouseMiddle + case MouseButtonRight: + m.Type = MouseRight + case MouseButtonBackward: + m.Type = MouseBackward + case MouseButtonForward: + m.Type = MouseForward + } + default: + m.Type = MouseUnknown + } + + return m +} diff --git a/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go b/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go new file mode 100644 index 00000000..0bc4a172 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/nil_renderer.go @@ -0,0 +1,28 @@ +package tea + +type nilRenderer struct{} + +func (n nilRenderer) start() {} +func (n nilRenderer) stop() {} +func (n nilRenderer) kill() {} +func (n nilRenderer) write(_ string) {} +func (n nilRenderer) repaint() {} +func (n nilRenderer) clearScreen() {} +func (n nilRenderer) altScreen() bool { return false } +func (n nilRenderer) enterAltScreen() {} +func (n nilRenderer) exitAltScreen() {} +func (n nilRenderer) showCursor() {} +func (n nilRenderer) hideCursor() {} +func (n nilRenderer) enableMouseCellMotion() {} +func (n nilRenderer) disableMouseCellMotion() {} +func (n nilRenderer) enableMouseAllMotion() {} +func (n nilRenderer) disableMouseAllMotion() {} +func (n nilRenderer) enableBracketedPaste() {} +func (n nilRenderer) disableBracketedPaste() {} +func (n nilRenderer) enableMouseSGRMode() {} +func (n nilRenderer) disableMouseSGRMode() {} +func (n nilRenderer) bracketedPasteActive() bool { return false } +func (n nilRenderer) setWindowTitle(_ string) {} +func (n nilRenderer) reportFocus() bool { return false } +func (n nilRenderer) enableReportFocus() {} +func (n nilRenderer) disableReportFocus() {} diff --git a/vendor/github.com/charmbracelet/bubbletea/options.go b/vendor/github.com/charmbracelet/bubbletea/options.go new file mode 100644 index 00000000..06abf4a9 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/options.go @@ -0,0 +1,249 @@ +package tea + +import ( + "context" + "io" + "sync/atomic" +) + +// ProgramOption is used to set options when initializing a Program. Program can +// accept a variable number of options. +// +// Example usage: +// +// p := NewProgram(model, WithInput(someInput), WithOutput(someOutput)) +type ProgramOption func(*Program) + +// WithContext lets you specify a context in which to run the Program. This is +// useful if you want to cancel the execution from outside. When a Program gets +// cancelled it will exit with an error ErrProgramKilled. +func WithContext(ctx context.Context) ProgramOption { + return func(p *Program) { + p.ctx = ctx + } +} + +// WithOutput sets the output which, by default, is stdout. In most cases you +// won't need to use this. +func WithOutput(output io.Writer) ProgramOption { + return func(p *Program) { + p.output = output + } +} + +// WithInput sets the input which, by default, is stdin. In most cases you +// won't need to use this. To disable input entirely pass nil. +// +// p := NewProgram(model, WithInput(nil)) +func WithInput(input io.Reader) ProgramOption { + return func(p *Program) { + p.input = input + p.inputType = customInput + } +} + +// WithInputTTY opens a new TTY for input (or console input device on Windows). +func WithInputTTY() ProgramOption { + return func(p *Program) { + p.inputType = ttyInput + } +} + +// WithEnvironment sets the environment variables that the program will use. +// This useful when the program is running in a remote session (e.g. SSH) and +// you want to pass the environment variables from the remote session to the +// program. +// +// Example: +// +// var sess ssh.Session // ssh.Session is a type from the github.com/charmbracelet/ssh package +// pty, _, _ := sess.Pty() +// environ := append(sess.Environ(), "TERM="+pty.Term) +// p := tea.NewProgram(model, tea.WithEnvironment(environ) +func WithEnvironment(env []string) ProgramOption { + return func(p *Program) { + p.environ = env + } +} + +// WithoutSignalHandler disables the signal handler that Bubble Tea sets up for +// Programs. This is useful if you want to handle signals yourself. +func WithoutSignalHandler() ProgramOption { + return func(p *Program) { + p.startupOptions |= withoutSignalHandler + } +} + +// WithoutCatchPanics disables the panic catching that Bubble Tea does by +// default. If panic catching is disabled the terminal will be in a fairly +// unusable state after a panic because Bubble Tea will not perform its usual +// cleanup on exit. +func WithoutCatchPanics() ProgramOption { + return func(p *Program) { + p.startupOptions |= withoutCatchPanics + } +} + +// WithoutSignals will ignore OS signals. +// This is mainly useful for testing. +func WithoutSignals() ProgramOption { + return func(p *Program) { + atomic.StoreUint32(&p.ignoreSignals, 1) + } +} + +// WithAltScreen starts the program with the alternate screen buffer enabled +// (i.e. the program starts in full window mode). Note that the altscreen will +// be automatically exited when the program quits. +// +// Example: +// +// p := tea.NewProgram(Model{}, tea.WithAltScreen()) +// if _, err := p.Run(); err != nil { +// fmt.Println("Error running program:", err) +// os.Exit(1) +// } +// +// To enter the altscreen once the program has already started running use the +// EnterAltScreen command. +func WithAltScreen() ProgramOption { + return func(p *Program) { + p.startupOptions |= withAltScreen + } +} + +// WithoutBracketedPaste starts the program with bracketed paste disabled. +func WithoutBracketedPaste() ProgramOption { + return func(p *Program) { + p.startupOptions |= withoutBracketedPaste + } +} + +// WithMouseCellMotion starts the program with the mouse enabled in "cell +// motion" mode. +// +// Cell motion mode enables mouse click, release, and wheel events. Mouse +// movement events are also captured if a mouse button is pressed (i.e., drag +// events). Cell motion mode is better supported than all motion mode. +// +// This will try to enable the mouse in extended mode (SGR), if that is not +// supported by the terminal it will fall back to normal mode (X10). +// +// To enable mouse cell motion once the program has already started running use +// the EnableMouseCellMotion command. To disable the mouse when the program is +// running use the DisableMouse command. +// +// The mouse will be automatically disabled when the program exits. +func WithMouseCellMotion() ProgramOption { + return func(p *Program) { + p.startupOptions |= withMouseCellMotion // set + p.startupOptions &^= withMouseAllMotion // clear + } +} + +// WithMouseAllMotion starts the program with the mouse enabled in "all motion" +// mode. +// +// EnableMouseAllMotion is a special command that enables mouse click, release, +// wheel, and motion events, which are delivered regardless of whether a mouse +// button is pressed, effectively enabling support for hover interactions. +// +// This will try to enable the mouse in extended mode (SGR), if that is not +// supported by the terminal it will fall back to normal mode (X10). +// +// Many modern terminals support this, but not all. If in doubt, use +// EnableMouseCellMotion instead. +// +// To enable the mouse once the program has already started running use the +// EnableMouseAllMotion command. To disable the mouse when the program is +// running use the DisableMouse command. +// +// The mouse will be automatically disabled when the program exits. +func WithMouseAllMotion() ProgramOption { + return func(p *Program) { + p.startupOptions |= withMouseAllMotion // set + p.startupOptions &^= withMouseCellMotion // clear + } +} + +// WithoutRenderer disables the renderer. When this is set output and log +// statements will be plainly sent to stdout (or another output if one is set) +// without any rendering and redrawing logic. In other words, printing and +// logging will behave the same way it would in a non-TUI commandline tool. +// This can be useful if you want to use the Bubble Tea framework for a non-TUI +// application, or to provide an additional non-TUI mode to your Bubble Tea +// programs. For example, your program could behave like a daemon if output is +// not a TTY. +func WithoutRenderer() ProgramOption { + return func(p *Program) { + p.renderer = &nilRenderer{} + } +} + +// WithANSICompressor removes redundant ANSI sequences to produce potentially +// smaller output, at the cost of some processing overhead. +// +// This feature is provisional, and may be changed or removed in a future version +// of this package. +func WithANSICompressor() ProgramOption { + return func(p *Program) { + p.startupOptions |= withANSICompressor + } +} + +// WithFilter supplies an event filter that will be invoked before Bubble Tea +// processes a tea.Msg. The event filter can return any tea.Msg which will then +// get handled by Bubble Tea instead of the original event. If the event filter +// returns nil, the event will be ignored and Bubble Tea will not process it. +// +// As an example, this could be used to prevent a program from shutting down if +// there are unsaved changes. +// +// Example: +// +// func filter(m tea.Model, msg tea.Msg) tea.Msg { +// if _, ok := msg.(tea.QuitMsg); !ok { +// return msg +// } +// +// model := m.(myModel) +// if model.hasChanges { +// return nil +// } +// +// return msg +// } +// +// p := tea.NewProgram(Model{}, tea.WithFilter(filter)); +// +// if _,err := p.Run(); err != nil { +// fmt.Println("Error running program:", err) +// os.Exit(1) +// } +func WithFilter(filter func(Model, Msg) Msg) ProgramOption { + return func(p *Program) { + p.filter = filter + } +} + +// WithFPS sets a custom maximum FPS at which the renderer should run. If +// less than 1, the default value of 60 will be used. If over 120, the FPS +// will be capped at 120. +func WithFPS(fps int) ProgramOption { + return func(p *Program) { + p.fps = fps + } +} + +// WithReportFocus enables reporting when the terminal gains and loses +// focus. When this is enabled [FocusMsg] and [BlurMsg] messages will be sent +// to your Update method. +// +// Note that while most terminals and multiplexers support focus reporting, +// some do not. Also note that tmux needs to be configured to report focus +// events. +func WithReportFocus() ProgramOption { + return func(p *Program) { + p.startupOptions |= withReportFocus + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/renderer.go b/vendor/github.com/charmbracelet/bubbletea/renderer.go new file mode 100644 index 00000000..9eb7943b --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/renderer.go @@ -0,0 +1,85 @@ +package tea + +// renderer is the interface for Bubble Tea renderers. +type renderer interface { + // Start the renderer. + start() + + // Stop the renderer, but render the final frame in the buffer, if any. + stop() + + // Stop the renderer without doing any final rendering. + kill() + + // Write a frame to the renderer. The renderer can write this data to + // output at its discretion. + write(string) + + // Request a full re-render. Note that this will not trigger a render + // immediately. Rather, this method causes the next render to be a full + // repaint. Because of this, it's safe to call this method multiple times + // in succession. + repaint() + + // Clears the terminal. + clearScreen() + + // Whether or not the alternate screen buffer is enabled. + altScreen() bool + // Enable the alternate screen buffer. + enterAltScreen() + // Disable the alternate screen buffer. + exitAltScreen() + + // Show the cursor. + showCursor() + // Hide the cursor. + hideCursor() + + // enableMouseCellMotion enables mouse click, release, wheel and motion + // events if a mouse button is pressed (i.e., drag events). + enableMouseCellMotion() + + // disableMouseCellMotion disables Mouse Cell Motion tracking. + disableMouseCellMotion() + + // enableMouseAllMotion enables mouse click, release, wheel and motion + // events, regardless of whether a mouse button is pressed. Many modern + // terminals support this, but not all. + enableMouseAllMotion() + + // disableMouseAllMotion disables All Motion mouse tracking. + disableMouseAllMotion() + + // enableMouseSGRMode enables mouse extended mode (SGR). + enableMouseSGRMode() + + // disableMouseSGRMode disables mouse extended mode (SGR). + disableMouseSGRMode() + + // enableBracketedPaste enables bracketed paste, where characters + // inside the input are not interpreted when pasted as a whole. + enableBracketedPaste() + + // disableBracketedPaste disables bracketed paste. + disableBracketedPaste() + + // bracketedPasteActive reports whether bracketed paste mode is + // currently enabled. + bracketedPasteActive() bool + + // setWindowTitle sets the terminal window title. + setWindowTitle(string) + + // reportFocus returns whether reporting focus events is enabled. + reportFocus() bool + + // enableReportFocus reports focus events to the program. + enableReportFocus() + + // disableReportFocus stops reporting focus events to the program. + disableReportFocus() +} + +// repaintMsg forces a full repaint. +type repaintMsg struct{} diff --git a/vendor/github.com/charmbracelet/bubbletea/screen.go b/vendor/github.com/charmbracelet/bubbletea/screen.go new file mode 100644 index 00000000..dfec48f0 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/screen.go @@ -0,0 +1,248 @@ +package tea + +// WindowSizeMsg is used to report the terminal size. It's sent to Update once +// initially and then on every terminal resize. Note that Windows does not +// have support for reporting when resizes occur as it does not support the +// SIGWINCH signal. +type WindowSizeMsg struct { + Width int + Height int +} + +// ClearScreen is a special command that tells the program to clear the screen +// before the next update. This can be used to move the cursor to the top left +// of the screen and clear visual clutter when the alt screen is not in use. +// +// Note that it should never be necessary to call ClearScreen() for regular +// redraws. +func ClearScreen() Msg { + return clearScreenMsg{} +} + +// clearScreenMsg is an internal message that signals to clear the screen. +// You can send a clearScreenMsg with ClearScreen. +type clearScreenMsg struct{} + +// EnterAltScreen is a special command that tells the Bubble Tea program to +// enter the alternate screen buffer. +// +// Because commands run asynchronously, this command should not be used in your +// model's Init function. To initialize your program with the altscreen enabled +// use the WithAltScreen ProgramOption instead. +func EnterAltScreen() Msg { + return enterAltScreenMsg{} +} + +// enterAltScreenMsg in an internal message signals that the program should +// enter alternate screen buffer. You can send a enterAltScreenMsg with +// EnterAltScreen. +type enterAltScreenMsg struct{} + +// ExitAltScreen is a special command that tells the Bubble Tea program to exit +// the alternate screen buffer. This command should be used to exit the +// alternate screen buffer while the program is running. +// +// Note that the alternate screen buffer will be automatically exited when the +// program quits. +func ExitAltScreen() Msg { + return exitAltScreenMsg{} +} + +// exitAltScreenMsg in an internal message signals that the program should exit +// alternate screen buffer. You can send a exitAltScreenMsg with ExitAltScreen. +type exitAltScreenMsg struct{} + +// EnableMouseCellMotion is a special command that enables mouse click, +// release, and wheel events. Mouse movement events are also captured if +// a mouse button is pressed (i.e., drag events). +// +// Because commands run asynchronously, this command should not be used in your +// model's Init function. Use the WithMouseCellMotion ProgramOption instead. +func EnableMouseCellMotion() Msg { + return enableMouseCellMotionMsg{} +} + +// enableMouseCellMotionMsg is a special command that signals to start +// listening for "cell motion" type mouse events (ESC[?1002l). To send an +// enableMouseCellMotionMsg, use the EnableMouseCellMotion command. +type enableMouseCellMotionMsg struct{} + +// EnableMouseAllMotion is a special command that enables mouse click, release, +// wheel, and motion events, which are delivered regardless of whether a mouse +// button is pressed, effectively enabling support for hover interactions. +// +// Many modern terminals support this, but not all. If in doubt, use +// EnableMouseCellMotion instead. +// +// Because commands run asynchronously, this command should not be used in your +// model's Init function. Use the WithMouseAllMotion ProgramOption instead. +func EnableMouseAllMotion() Msg { + return enableMouseAllMotionMsg{} +} + +// enableMouseAllMotionMsg is a special command that signals to start listening +// for "all motion" type mouse events (ESC[?1003l). To send an +// enableMouseAllMotionMsg, use the EnableMouseAllMotion command. +type enableMouseAllMotionMsg struct{} + +// DisableMouse is a special command that stops listening for mouse events. +func DisableMouse() Msg { + return disableMouseMsg{} +} + +// disableMouseMsg is an internal message that signals to stop listening +// for mouse events. To send a disableMouseMsg, use the DisableMouse command. +type disableMouseMsg struct{} + +// HideCursor is a special command for manually instructing Bubble Tea to hide +// the cursor. In some rare cases, certain operations will cause the terminal +// to show the cursor, which is normally hidden for the duration of a Bubble +// Tea program's lifetime. You will most likely not need to use this command. +func HideCursor() Msg { + return hideCursorMsg{} +} + +// hideCursorMsg is an internal command used to hide the cursor. You can send +// this message with HideCursor. +type hideCursorMsg struct{} + +// ShowCursor is a special command for manually instructing Bubble Tea to show +// the cursor. +func ShowCursor() Msg { + return showCursorMsg{} +} + +// showCursorMsg is an internal command used to show the cursor. You can send +// this message with ShowCursor. +type showCursorMsg struct{} + +// EnableBracketedPaste is a special command that tells the Bubble Tea program +// to accept bracketed paste input. +// +// Note that bracketed paste will be automatically disabled when the +// program quits. +func EnableBracketedPaste() Msg { + return enableBracketedPasteMsg{} +} + +// enableBracketedPasteMsg in an internal message signals that +// bracketed paste should be enabled. You can send an +// enableBracketedPasteMsg with EnableBracketedPaste. +type enableBracketedPasteMsg struct{} + +// DisableBracketedPaste is a special command that tells the Bubble Tea program +// to accept bracketed paste input. +// +// Note that bracketed paste will be automatically disabled when the +// program quits. +func DisableBracketedPaste() Msg { + return disableBracketedPasteMsg{} +} + +// disableBracketedPasteMsg in an internal message signals that +// bracketed paste should be disabled. You can send an +// disableBracketedPasteMsg with DisableBracketedPaste. +type disableBracketedPasteMsg struct{} + +// enableReportFocusMsg is an internal message that signals to enable focus +// reporting. You can send an enableReportFocusMsg with EnableReportFocus. +type enableReportFocusMsg struct{} + +// EnableReportFocus is a special command that tells the Bubble Tea program to +// report focus events to the program. +func EnableReportFocus() Msg { + return enableReportFocusMsg{} +} + +// disableReportFocusMsg is an internal message that signals to disable focus +// reporting. You can send an disableReportFocusMsg with DisableReportFocus. +type disableReportFocusMsg struct{} + +// DisableReportFocus is a special command that tells the Bubble Tea program to +// stop reporting focus events to the program. +func DisableReportFocus() Msg { + return disableReportFocusMsg{} +} + +// EnterAltScreen enters the alternate screen buffer, which consumes the entire +// terminal window. ExitAltScreen will return the terminal to its former state. +// +// Deprecated: Use the WithAltScreen ProgramOption instead. +func (p *Program) EnterAltScreen() { + if p.renderer != nil { + p.renderer.enterAltScreen() + } else { + p.startupOptions |= withAltScreen + } +} + +// ExitAltScreen exits the alternate screen buffer. +// +// Deprecated: The altscreen will exited automatically when the program exits. +func (p *Program) ExitAltScreen() { + if p.renderer != nil { + p.renderer.exitAltScreen() + } else { + p.startupOptions &^= withAltScreen + } +} + +// EnableMouseCellMotion enables mouse click, release, wheel and motion events +// if a mouse button is pressed (i.e., drag events). +// +// Deprecated: Use the WithMouseCellMotion ProgramOption instead. +func (p *Program) EnableMouseCellMotion() { + if p.renderer != nil { + p.renderer.enableMouseCellMotion() + } else { + p.startupOptions |= withMouseCellMotion + } +} + +// DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be +// called automatically when exiting a Bubble Tea program. +// +// Deprecated: The mouse will automatically be disabled when the program exits. +func (p *Program) DisableMouseCellMotion() { + if p.renderer != nil { + p.renderer.disableMouseCellMotion() + } else { + p.startupOptions &^= withMouseCellMotion + } +} + +// EnableMouseAllMotion enables mouse click, release, wheel and motion events, +// regardless of whether a mouse button is pressed. Many modern terminals +// support this, but not all. +// +// Deprecated: Use the WithMouseAllMotion ProgramOption instead. +func (p *Program) EnableMouseAllMotion() { + if p.renderer != nil { + p.renderer.enableMouseAllMotion() + } else { + p.startupOptions |= withMouseAllMotion + } +} + +// DisableMouseAllMotion disables All Motion mouse tracking. This will be +// called automatically when exiting a Bubble Tea program. +// +// Deprecated: The mouse will automatically be disabled when the program exits. +func (p *Program) DisableMouseAllMotion() { + if p.renderer != nil { + p.renderer.disableMouseAllMotion() + } else { + p.startupOptions &^= withMouseAllMotion + } +} + +// SetWindowTitle sets the terminal window title. +// +// Deprecated: Use the SetWindowTitle command instead. +func (p *Program) SetWindowTitle(title string) { + if p.renderer != nil { + p.renderer.setWindowTitle(title) + } else { + p.startupTitle = title + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/signals_unix.go b/vendor/github.com/charmbracelet/bubbletea/signals_unix.go new file mode 100644 index 00000000..40954038 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/signals_unix.go @@ -0,0 +1,33 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos +// +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos + +package tea + +import ( + "os" + "os/signal" + "syscall" +) + +// listenForResize sends messages (or errors) when the terminal resizes. +// Argument output should be the file descriptor for the terminal; usually +// os.Stdout. +func (p *Program) listenForResize(done chan struct{}) { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGWINCH) + + defer func() { + signal.Stop(sig) + close(done) + }() + + for { + select { + case <-p.ctx.Done(): + return + case <-sig: + } + + p.checkResize() + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/signals_windows.go b/vendor/github.com/charmbracelet/bubbletea/signals_windows.go new file mode 100644 index 00000000..2fc6f8ae --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/signals_windows.go @@ -0,0 +1,10 @@ +//go:build windows +// +build windows + +package tea + +// listenForResize is not available on windows because windows does not +// implement syscall.SIGWINCH. +func (p *Program) listenForResize(done chan struct{}) { + close(done) +} diff --git a/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go b/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go new file mode 100644 index 00000000..59448e1d --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/standard_renderer.go @@ -0,0 +1,755 @@ +package tea + +import ( + "bytes" + "fmt" + "io" + "strings" + "sync" + "time" + + "github.com/charmbracelet/x/ansi" + "github.com/muesli/ansi/compressor" +) + +const ( + // defaultFramerate specifies the maximum interval at which we should + // update the view. + defaultFPS = 60 + maxFPS = 120 +) + +// standardRenderer is a framerate-based terminal renderer, updating the view +// at a given framerate to avoid overloading the terminal emulator. +// +// In cases where very high performance is needed the renderer can be told +// to exclude ranges of lines, allowing them to be written to directly. +type standardRenderer struct { + mtx *sync.Mutex + out io.Writer + + buf bytes.Buffer + queuedMessageLines []string + framerate time.Duration + ticker *time.Ticker + done chan struct{} + lastRender string + linesRendered int + useANSICompressor bool + once sync.Once + + // cursor visibility state + cursorHidden bool + + // essentially whether or not we're using the full size of the terminal + altScreenActive bool + + // whether or not we're currently using bracketed paste + bpActive bool + + // reportingFocus whether reporting focus events is enabled + reportingFocus bool + + // renderer dimensions; usually the size of the window + width int + height int + + // lines explicitly set not to render + ignoreLines map[int]struct{} +} + +// newRenderer creates a new renderer. Normally you'll want to initialize it +// with os.Stdout as the first argument. +func newRenderer(out io.Writer, useANSICompressor bool, fps int) renderer { + if fps < 1 { + fps = defaultFPS + } else if fps > maxFPS { + fps = maxFPS + } + r := &standardRenderer{ + out: out, + mtx: &sync.Mutex{}, + done: make(chan struct{}), + framerate: time.Second / time.Duration(fps), + useANSICompressor: useANSICompressor, + queuedMessageLines: []string{}, + } + if r.useANSICompressor { + r.out = &compressor.Writer{Forward: out} + } + return r +} + +// start starts the renderer. +func (r *standardRenderer) start() { + if r.ticker == nil { + r.ticker = time.NewTicker(r.framerate) + } else { + // If the ticker already exists, it has been stopped and we need to + // reset it. + r.ticker.Reset(r.framerate) + } + + // Since the renderer can be restarted after a stop, we need to reset + // the done channel and its corresponding sync.Once. + r.once = sync.Once{} + + go r.listen() +} + +// stop permanently halts the renderer, rendering the final frame. +func (r *standardRenderer) stop() { + // Stop the renderer before acquiring the mutex to avoid a deadlock. + r.once.Do(func() { + r.done <- struct{}{} + }) + + // flush locks the mutex + r.flush() + + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.EraseEntireLine) + // Move the cursor back to the beginning of the line + r.execute("\r") + + if r.useANSICompressor { + if w, ok := r.out.(io.WriteCloser); ok { + _ = w.Close() + } + } +} + +// execute writes a sequence to the terminal. +func (r *standardRenderer) execute(seq string) { + _, _ = io.WriteString(r.out, seq) +} + +// kill halts the renderer. The final frame will not be rendered. +func (r *standardRenderer) kill() { + // Stop the renderer before acquiring the mutex to avoid a deadlock. + r.once.Do(func() { + r.done <- struct{}{} + }) + + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.EraseEntireLine) + // Move the cursor back to the beginning of the line + r.execute("\r") +} + +// listen waits for ticks on the ticker, or a signal to stop the renderer. +func (r *standardRenderer) listen() { + for { + select { + case <-r.done: + r.ticker.Stop() + return + + case <-r.ticker.C: + r.flush() + } + } +} + +// flush renders the buffer. +func (r *standardRenderer) flush() { + r.mtx.Lock() + defer r.mtx.Unlock() + + if r.buf.Len() == 0 || r.buf.String() == r.lastRender { + // Nothing to do + return + } + + // Output buffer + buf := &bytes.Buffer{} + + newLines := strings.Split(r.buf.String(), "\n") + + // If we know the output's height, we can use it to determine how many + // lines we can render. We drop lines from the top of the render buffer if + // necessary, as we can't navigate the cursor into the terminal's scrollback + // buffer. + if r.height > 0 && len(newLines) > r.height { + newLines = newLines[len(newLines)-r.height:] + } + + numLinesThisFlush := len(newLines) + oldLines := strings.Split(r.lastRender, "\n") + skipLines := make(map[int]struct{}) + flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive + + // Clear any lines we painted in the last render. + if r.linesRendered > 0 { + for i := r.linesRendered - 1; i > 0; i-- { + // if we are clearing queued messages, we want to clear all lines, since + // printing messages allows for native terminal word-wrap, we + // don't have control over the queued lines + if flushQueuedMessages { + buf.WriteString(ansi.EraseEntireLine) + } else if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) { + // If the number of lines we want to render hasn't increased and + // new line is the same as the old line we can skip rendering for + // this line as a performance optimization. + skipLines[i] = struct{}{} + } else if _, exists := r.ignoreLines[i]; !exists { + buf.WriteString(ansi.EraseEntireLine) + } + + buf.WriteString(ansi.CursorUp1) + } + + if _, exists := r.ignoreLines[0]; !exists { + // We need to return to the start of the line here to properly + // erase it. Going back the entire width of the terminal will + // usually be farther than we need to go, but terminal emulators + // will stop the cursor at the start of the line as a rule. + // + // We use this sequence in particular because it's part of the ANSI + // standard (whereas others are proprietary to, say, VT100/VT52). + // If cursor previous line (ESC[ + + F) were better supported + // we could use that above to eliminate this step. + buf.WriteString(ansi.CursorLeft(r.width)) + buf.WriteString(ansi.EraseEntireLine) + } + } + + // Merge the set of lines we're skipping as a rendering optimization with + // the set of lines we've explicitly asked the renderer to ignore. + for k, v := range r.ignoreLines { + skipLines[k] = v + } + + if flushQueuedMessages { + // Dump the lines we've queued up for printing + for _, line := range r.queuedMessageLines { + _, _ = buf.WriteString(line) + _, _ = buf.WriteString("\r\n") + } + // clear the queued message lines + r.queuedMessageLines = []string{} + } + + // Paint new lines + for i := 0; i < len(newLines); i++ { + if _, skip := skipLines[i]; skip { + // Unless this is the last line, move the cursor down. + if i < len(newLines)-1 { + buf.WriteString(ansi.CursorDown1) + } + } else { + if i == 0 && r.lastRender == "" { + // On first render, reset the cursor to the start of the line + // before writing anything. + buf.WriteByte('\r') + } + + line := newLines[i] + + // Truncate lines wider than the width of the window to avoid + // wrapping, which will mess up rendering. If we don't have the + // width of the window this will be ignored. + // + // Note that on Windows we only get the width of the window on + // program initialization, so after a resize this won't perform + // correctly (signal SIGWINCH is not supported on Windows). + if r.width > 0 { + line = ansi.Truncate(line, r.width, "") + } + + _, _ = buf.WriteString(line) + + if i < len(newLines)-1 { + _, _ = buf.WriteString("\r\n") + } + } + } + r.linesRendered = numLinesThisFlush + + // Make sure the cursor is at the start of the last line to keep rendering + // behavior consistent. + if r.altScreenActive { + // This case fixes a bug in macOS terminal. In other terminals the + // other case seems to do the job regardless of whether or not we're + // using the full terminal window. + buf.WriteString(ansi.MoveCursor(r.linesRendered, 0)) + } else { + buf.WriteString(ansi.CursorLeft(r.width)) + } + + _, _ = r.out.Write(buf.Bytes()) + r.lastRender = r.buf.String() + r.buf.Reset() +} + +// write writes to the internal buffer. The buffer will be outputted via the +// ticker which calls flush(). +func (r *standardRenderer) write(s string) { + r.mtx.Lock() + defer r.mtx.Unlock() + r.buf.Reset() + + // If an empty string was passed we should clear existing output and + // rendering nothing. Rather than introduce additional state to manage + // this, we render a single space as a simple (albeit less correct) + // solution. + if s == "" { + s = " " + } + + _, _ = r.buf.WriteString(s) +} + +func (r *standardRenderer) repaint() { + r.lastRender = "" +} + +func (r *standardRenderer) clearScreen() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.EraseEntireDisplay) + r.execute(ansi.MoveCursorOrigin) + + r.repaint() +} + +func (r *standardRenderer) altScreen() bool { + r.mtx.Lock() + defer r.mtx.Unlock() + + return r.altScreenActive +} + +func (r *standardRenderer) enterAltScreen() { + r.mtx.Lock() + defer r.mtx.Unlock() + + if r.altScreenActive { + return + } + + r.altScreenActive = true + r.execute(ansi.EnableAltScreenBuffer) + + // Ensure that the terminal is cleared, even when it doesn't support + // alt screen (or alt screen support is disabled, like GNU screen by + // default). + // + // Note: we can't use r.clearScreen() here because the mutex is already + // locked. + r.execute(ansi.EraseEntireDisplay) + r.execute(ansi.MoveCursorOrigin) + + // cmd.exe and other terminals keep separate cursor states for the AltScreen + // and the main buffer. We have to explicitly reset the cursor visibility + // whenever we enter AltScreen. + if r.cursorHidden { + r.execute(ansi.HideCursor) + } else { + r.execute(ansi.ShowCursor) + } + + r.repaint() +} + +func (r *standardRenderer) exitAltScreen() { + r.mtx.Lock() + defer r.mtx.Unlock() + + if !r.altScreenActive { + return + } + + r.altScreenActive = false + r.execute(ansi.DisableAltScreenBuffer) + + // cmd.exe and other terminals keep separate cursor states for the AltScreen + // and the main buffer. We have to explicitly reset the cursor visibility + // whenever we exit AltScreen. + if r.cursorHidden { + r.execute(ansi.HideCursor) + } else { + r.execute(ansi.ShowCursor) + } + + r.repaint() +} + +func (r *standardRenderer) showCursor() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.cursorHidden = false + r.execute(ansi.ShowCursor) +} + +func (r *standardRenderer) hideCursor() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.cursorHidden = true + r.execute(ansi.HideCursor) +} + +func (r *standardRenderer) enableMouseCellMotion() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.EnableMouseCellMotion) +} + +func (r *standardRenderer) disableMouseCellMotion() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.DisableMouseCellMotion) +} + +func (r *standardRenderer) enableMouseAllMotion() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.EnableMouseAllMotion) +} + +func (r *standardRenderer) disableMouseAllMotion() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.DisableMouseAllMotion) +} + +func (r *standardRenderer) enableMouseSGRMode() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.EnableMouseSgrExt) +} + +func (r *standardRenderer) disableMouseSGRMode() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.DisableMouseSgrExt) +} + +func (r *standardRenderer) enableBracketedPaste() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.EnableBracketedPaste) + r.bpActive = true +} + +func (r *standardRenderer) disableBracketedPaste() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.DisableBracketedPaste) + r.bpActive = false +} + +func (r *standardRenderer) bracketedPasteActive() bool { + r.mtx.Lock() + defer r.mtx.Unlock() + + return r.bpActive +} + +func (r *standardRenderer) enableReportFocus() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.EnableReportFocus) + r.reportingFocus = true +} + +func (r *standardRenderer) disableReportFocus() { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.execute(ansi.DisableReportFocus) + r.reportingFocus = false +} + +func (r *standardRenderer) reportFocus() bool { + r.mtx.Lock() + defer r.mtx.Unlock() + + return r.reportingFocus +} + +// setWindowTitle sets the terminal window title. +func (r *standardRenderer) setWindowTitle(title string) { + r.execute(ansi.SetWindowTitle(title)) +} + +// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea +// renderer. +func (r *standardRenderer) setIgnoredLines(from int, to int) { + // Lock if we're going to be clearing some lines since we don't want + // anything jacking our cursor. + if r.linesRendered > 0 { + r.mtx.Lock() + defer r.mtx.Unlock() + } + + if r.ignoreLines == nil { + r.ignoreLines = make(map[int]struct{}) + } + for i := from; i < to; i++ { + r.ignoreLines[i] = struct{}{} + } + + // Erase ignored lines + if r.linesRendered > 0 { + buf := &bytes.Buffer{} + + for i := r.linesRendered - 1; i >= 0; i-- { + if _, exists := r.ignoreLines[i]; exists { + buf.WriteString(ansi.EraseEntireLine) + } + buf.WriteString(ansi.CursorUp1) + } + buf.WriteString(ansi.MoveCursor(r.linesRendered, 0)) // put cursor back + _, _ = r.out.Write(buf.Bytes()) + } +} + +// clearIgnoredLines returns control of any ignored lines to the standard +// Bubble Tea renderer. That is, any lines previously set to be ignored can be +// rendered to again. +func (r *standardRenderer) clearIgnoredLines() { + r.ignoreLines = nil +} + +// insertTop effectively scrolls up. It inserts lines at the top of a given +// area designated to be a scrollable region, pushing everything else down. +// This is roughly how ncurses does it. +// +// To call this function use command ScrollUp(). +// +// For this to work renderer.ignoreLines must be set to ignore the scrollable +// region since we are bypassing the normal Bubble Tea renderer here. +// +// Because this method relies on the terminal dimensions, it's only valid for +// full-window applications (generally those that use the alternate screen +// buffer). +// +// This method bypasses the normal rendering buffer and is philosophically +// different than the normal way we approach rendering in Bubble Tea. It's for +// use in high-performance rendering, such as a pager that could potentially +// be rendering very complicated ansi. In cases where the content is simpler +// standard Bubble Tea rendering should suffice. +func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary int) { + r.mtx.Lock() + defer r.mtx.Unlock() + + buf := &bytes.Buffer{} + + buf.WriteString(ansi.SetScrollingRegion(topBoundary, bottomBoundary)) + buf.WriteString(ansi.MoveCursor(topBoundary, 0)) + buf.WriteString(ansi.InsertLine(len(lines))) + _, _ = buf.WriteString(strings.Join(lines, "\r\n")) + buf.WriteString(ansi.SetScrollingRegion(0, r.height)) + + // Move cursor back to where the main rendering routine expects it to be + buf.WriteString(ansi.MoveCursor(r.linesRendered, 0)) + + _, _ = r.out.Write(buf.Bytes()) +} + +// insertBottom effectively scrolls down. It inserts lines at the bottom of +// a given area designated to be a scrollable region, pushing everything else +// up. This is roughly how ncurses does it. +// +// To call this function use the command ScrollDown(). +// +// See note in insertTop() for caveats, how this function only makes sense for +// full-window applications, and how it differs from the normal way we do +// rendering in Bubble Tea. +func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBoundary int) { + r.mtx.Lock() + defer r.mtx.Unlock() + + buf := &bytes.Buffer{} + + buf.WriteString(ansi.SetScrollingRegion(topBoundary, bottomBoundary)) + buf.WriteString(ansi.MoveCursor(bottomBoundary, 0)) + _, _ = buf.WriteString("\r\n" + strings.Join(lines, "\r\n")) + buf.WriteString(ansi.SetScrollingRegion(0, r.height)) + + // Move cursor back to where the main rendering routine expects it to be + buf.WriteString(ansi.MoveCursor(r.linesRendered, 0)) + + _, _ = r.out.Write(buf.Bytes()) +} + +// handleMessages handles internal messages for the renderer. +func (r *standardRenderer) handleMessages(msg Msg) { + switch msg := msg.(type) { + case repaintMsg: + // Force a repaint by clearing the render cache as we slide into a + // render. + r.mtx.Lock() + r.repaint() + r.mtx.Unlock() + + case WindowSizeMsg: + r.mtx.Lock() + r.width = msg.Width + r.height = msg.Height + r.repaint() + r.mtx.Unlock() + + case clearScrollAreaMsg: + r.clearIgnoredLines() + + // Force a repaint on the area where the scrollable stuff was in this + // update cycle + r.mtx.Lock() + r.repaint() + r.mtx.Unlock() + + case syncScrollAreaMsg: + // Re-render scrolling area + r.clearIgnoredLines() + r.setIgnoredLines(msg.topBoundary, msg.bottomBoundary) + r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary) + + // Force non-scrolling stuff to repaint in this update cycle + r.mtx.Lock() + r.repaint() + r.mtx.Unlock() + + case scrollUpMsg: + r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary) + + case scrollDownMsg: + r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary) + + case printLineMessage: + if !r.altScreenActive { + lines := strings.Split(msg.messageBody, "\n") + r.mtx.Lock() + r.queuedMessageLines = append(r.queuedMessageLines, lines...) + r.repaint() + r.mtx.Unlock() + } + } +} + +// HIGH-PERFORMANCE RENDERING STUFF + +type syncScrollAreaMsg struct { + lines []string + topBoundary int + bottomBoundary int +} + +// SyncScrollArea performs a paint of the entire region designated to be the +// scrollable area. This is required to initialize the scrollable region and +// should also be called on resize (WindowSizeMsg). +// +// For high-performance, scroll-based rendering only. +func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd { + return func() Msg { + return syncScrollAreaMsg{ + lines: lines, + topBoundary: topBoundary, + bottomBoundary: bottomBoundary, + } + } +} + +type clearScrollAreaMsg struct{} + +// ClearScrollArea deallocates the scrollable region and returns the control of +// those lines to the main rendering routine. +// +// For high-performance, scroll-based rendering only. +func ClearScrollArea() Msg { + return clearScrollAreaMsg{} +} + +type scrollUpMsg struct { + lines []string + topBoundary int + bottomBoundary int +} + +// ScrollUp adds lines to the top of the scrollable region, pushing existing +// lines below down. Lines that are pushed out the scrollable region disappear +// from view. +// +// For high-performance, scroll-based rendering only. +func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd { + return func() Msg { + return scrollUpMsg{ + lines: newLines, + topBoundary: topBoundary, + bottomBoundary: bottomBoundary, + } + } +} + +type scrollDownMsg struct { + lines []string + topBoundary int + bottomBoundary int +} + +// ScrollDown adds lines to the bottom of the scrollable region, pushing +// existing lines above up. Lines that are pushed out of the scrollable region +// disappear from view. +// +// For high-performance, scroll-based rendering only. +func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd { + return func() Msg { + return scrollDownMsg{ + lines: newLines, + topBoundary: topBoundary, + bottomBoundary: bottomBoundary, + } + } +} + +type printLineMessage struct { + messageBody string +} + +// Println prints above the Program. This output is unmanaged by the program and +// will persist across renders by the Program. +// +// Unlike fmt.Println (but similar to log.Println) the message will be print on +// its own line. +// +// If the altscreen is active no output will be printed. +func Println(args ...interface{}) Cmd { + return func() Msg { + return printLineMessage{ + messageBody: fmt.Sprint(args...), + } + } +} + +// Printf prints above the Program. It takes a format template followed by +// values similar to fmt.Printf. This output is unmanaged by the program and +// will persist across renders by the Program. +// +// Unlike fmt.Printf (but similar to log.Printf) the message will be print on +// its own line. +// +// If the altscreen is active no output will be printed. +func Printf(template string, args ...interface{}) Cmd { + return func() Msg { + return printLineMessage{ + messageBody: fmt.Sprintf(template, args...), + } + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/tea.go b/vendor/github.com/charmbracelet/bubbletea/tea.go new file mode 100644 index 00000000..87211ba2 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/tea.go @@ -0,0 +1,788 @@ +// Package tea provides a framework for building rich terminal user interfaces +// based on the paradigms of The Elm Architecture. It's well-suited for simple +// and complex terminal applications, either inline, full-window, or a mix of +// both. It's been battle-tested in several large projects and is +// production-ready. +// +// A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/master/tutorials +// +// Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/master/examples +package tea + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "os/signal" + "runtime/debug" + "sync" + "sync/atomic" + "syscall" + + "github.com/charmbracelet/x/term" + "github.com/muesli/cancelreader" + "golang.org/x/sync/errgroup" +) + +// ErrProgramKilled is returned by [Program.Run] when the program got killed. +var ErrProgramKilled = errors.New("program was killed") + +// Msg contain data from the result of a IO operation. Msgs trigger the update +// function and, henceforth, the UI. +type Msg interface{} + +// Model contains the program's state as well as its core functions. +type Model interface { + // Init is the first function that will be called. It returns an optional + // initial command. To not perform an initial command return nil. + Init() Cmd + + // Update is called when a message is received. Use it to inspect messages + // and, in response, update the model and/or send a command. + Update(Msg) (Model, Cmd) + + // View renders the program's UI, which is just a string. The view is + // rendered after every Update. + View() string +} + +// Cmd is an IO operation that returns a message when it's complete. If it's +// nil it's considered a no-op. Use it for things like HTTP requests, timers, +// saving and loading from disk, and so on. +// +// Note that there's almost never a reason to use a command to send a message +// to another part of your program. That can almost always be done in the +// update function. +type Cmd func() Msg + +type inputType int + +const ( + defaultInput inputType = iota + ttyInput + customInput +) + +// String implements the stringer interface for [inputType]. It is inteded to +// be used in testing. +func (i inputType) String() string { + return [...]string{ + "default input", + "tty input", + "custom input", + }[i] +} + +// Options to customize the program during its initialization. These are +// generally set with ProgramOptions. +// +// The options here are treated as bits. +type startupOptions int16 + +func (s startupOptions) has(option startupOptions) bool { + return s&option != 0 +} + +const ( + withAltScreen startupOptions = 1 << iota + withMouseCellMotion + withMouseAllMotion + withANSICompressor + withoutSignalHandler + // Catching panics is incredibly useful for restoring the terminal to a + // usable state after a panic occurs. When this is set, Bubble Tea will + // recover from panics, print the stack trace, and disable raw mode. This + // feature is on by default. + withoutCatchPanics + withoutBracketedPaste + withReportFocus +) + +// channelHandlers manages the series of channels returned by various processes. +// It allows us to wait for those processes to terminate before exiting the +// program. +type channelHandlers []chan struct{} + +// Adds a channel to the list of handlers. We wait for all handlers to terminate +// gracefully on shutdown. +func (h *channelHandlers) add(ch chan struct{}) { + *h = append(*h, ch) +} + +// shutdown waits for all handlers to terminate. +func (h channelHandlers) shutdown() { + var wg sync.WaitGroup + for _, ch := range h { + wg.Add(1) + go func(ch chan struct{}) { + <-ch + wg.Done() + }(ch) + } + wg.Wait() +} + +// Program is a terminal user interface. +type Program struct { + initialModel Model + + // handlers is a list of channels that need to be waited on before the + // program can exit. + handlers channelHandlers + + // Configuration options that will set as the program is initializing, + // treated as bits. These options can be set via various ProgramOptions. + startupOptions startupOptions + + // startupTitle is the title that will be set on the terminal when the + // program starts. + startupTitle string + + inputType inputType + + ctx context.Context + cancel context.CancelFunc + + msgs chan Msg + errs chan error + finished chan struct{} + + // where to send output, this will usually be os.Stdout. + output io.Writer + // ttyOutput is null if output is not a TTY. + ttyOutput term.File + previousOutputState *term.State + renderer renderer + + // the environment variables for the program, defaults to os.Environ(). + environ []string + + // where to read inputs from, this will usually be os.Stdin. + input io.Reader + // ttyInput is null if input is not a TTY. + ttyInput term.File + previousTtyInputState *term.State + cancelReader cancelreader.CancelReader + readLoopDone chan struct{} + + // was the altscreen active before releasing the terminal? + altScreenWasActive bool + ignoreSignals uint32 + + bpWasActive bool // was the bracketed paste mode active before releasing the terminal? + reportFocus bool // was focus reporting active before releasing the terminal? + + filter func(Model, Msg) Msg + + // fps is the frames per second we should set on the renderer, if + // applicable, + fps int +} + +// Quit is a special command that tells the Bubble Tea program to exit. +func Quit() Msg { + return QuitMsg{} +} + +// QuitMsg signals that the program should quit. You can send a QuitMsg with +// Quit. +type QuitMsg struct{} + +// Suspend is a special command that tells the Bubble Tea program to suspend. +func Suspend() Msg { + return SuspendMsg{} +} + +// SuspendMsg signals the program should suspend. +// This usually happens when ctrl+z is pressed on common programs, but since +// bubbletea puts the terminal in raw mode, we need to handle it in a +// per-program basis. +// You can send this message with Suspend. +type SuspendMsg struct{} + +// ResumeMsg can be listen to to do something once a program is resumed back +// from a suspend state. +type ResumeMsg struct{} + +// NewProgram creates a new Program. +func NewProgram(model Model, opts ...ProgramOption) *Program { + p := &Program{ + initialModel: model, + msgs: make(chan Msg), + } + + // Apply all options to the program. + for _, opt := range opts { + opt(p) + } + + // A context can be provided with a ProgramOption, but if none was provided + // we'll use the default background context. + if p.ctx == nil { + p.ctx = context.Background() + } + // Initialize context and teardown channel. + p.ctx, p.cancel = context.WithCancel(p.ctx) + + // if no output was set, set it to stdout + if p.output == nil { + p.output = os.Stdout + } + + // if no environment was set, set it to os.Environ() + if p.environ == nil { + p.environ = os.Environ() + } + + return p +} + +func (p *Program) handleSignals() chan struct{} { + ch := make(chan struct{}) + + // Listen for SIGINT and SIGTERM. + // + // In most cases ^C will not send an interrupt because the terminal will be + // in raw mode and ^C will be captured as a keystroke and sent along to + // Program.Update as a KeyMsg. When input is not a TTY, however, ^C will be + // caught here. + // + // SIGTERM is sent by unix utilities (like kill) to terminate a process. + go func() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + defer func() { + signal.Stop(sig) + close(ch) + }() + + for { + select { + case <-p.ctx.Done(): + return + + case <-sig: + if atomic.LoadUint32(&p.ignoreSignals) == 0 { + p.msgs <- QuitMsg{} + return + } + } + } + }() + + return ch +} + +// handleResize handles terminal resize events. +func (p *Program) handleResize() chan struct{} { + ch := make(chan struct{}) + + if p.ttyOutput != nil { + // Get the initial terminal size and send it to the program. + go p.checkResize() + + // Listen for window resizes. + go p.listenForResize(ch) + } else { + close(ch) + } + + return ch +} + +// handleCommands runs commands in a goroutine and sends the result to the +// program's message channel. +func (p *Program) handleCommands(cmds chan Cmd) chan struct{} { + ch := make(chan struct{}) + + go func() { + defer close(ch) + + for { + select { + case <-p.ctx.Done(): + return + + case cmd := <-cmds: + if cmd == nil { + continue + } + + // Don't wait on these goroutines, otherwise the shutdown + // latency would get too large as a Cmd can run for some time + // (e.g. tick commands that sleep for half a second). It's not + // possible to cancel them so we'll have to leak the goroutine + // until Cmd returns. + go func() { + // Recover from panics. + if !p.startupOptions.has(withoutCatchPanics) { + defer p.recoverFromPanic() + } + + msg := cmd() // this can be long. + p.Send(msg) + }() + } + } + }() + + return ch +} + +func (p *Program) disableMouse() { + p.renderer.disableMouseCellMotion() + p.renderer.disableMouseAllMotion() + p.renderer.disableMouseSGRMode() +} + +// eventLoop is the central message loop. It receives and handles the default +// Bubble Tea messages, update the model and triggers redraws. +func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { + for { + select { + case <-p.ctx.Done(): + return model, nil + + case err := <-p.errs: + return model, err + + case msg := <-p.msgs: + // Filter messages. + if p.filter != nil { + msg = p.filter(model, msg) + } + if msg == nil { + continue + } + + // Handle special internal messages. + switch msg := msg.(type) { + case QuitMsg: + return model, nil + + case SuspendMsg: + if suspendSupported { + p.suspend() + } + + case clearScreenMsg: + p.renderer.clearScreen() + + case enterAltScreenMsg: + p.renderer.enterAltScreen() + + case exitAltScreenMsg: + p.renderer.exitAltScreen() + + case enableMouseCellMotionMsg, enableMouseAllMotionMsg: + switch msg.(type) { + case enableMouseCellMotionMsg: + p.renderer.enableMouseCellMotion() + case enableMouseAllMotionMsg: + p.renderer.enableMouseAllMotion() + } + // mouse mode (1006) is a no-op if the terminal doesn't support it. + p.renderer.enableMouseSGRMode() + + case disableMouseMsg: + p.disableMouse() + + case showCursorMsg: + p.renderer.showCursor() + + case hideCursorMsg: + p.renderer.hideCursor() + + case enableBracketedPasteMsg: + p.renderer.enableBracketedPaste() + + case disableBracketedPasteMsg: + p.renderer.disableBracketedPaste() + + case enableReportFocusMsg: + p.renderer.enableReportFocus() + + case disableReportFocusMsg: + p.renderer.disableReportFocus() + + case execMsg: + // NB: this blocks. + p.exec(msg.cmd, msg.fn) + + case BatchMsg: + for _, cmd := range msg { + cmds <- cmd + } + continue + + case sequenceMsg: + go func() { + // Execute commands one at a time, in order. + for _, cmd := range msg { + if cmd == nil { + continue + } + + msg := cmd() + if batchMsg, ok := msg.(BatchMsg); ok { + g, _ := errgroup.WithContext(p.ctx) + for _, cmd := range batchMsg { + cmd := cmd + g.Go(func() error { + p.Send(cmd()) + return nil + }) + } + + //nolint:errcheck + g.Wait() // wait for all commands from batch msg to finish + continue + } + + p.Send(msg) + } + }() + + case setWindowTitleMsg: + p.SetWindowTitle(string(msg)) + + case windowSizeMsg: + go p.checkResize() + } + + // Process internal messages for the renderer. + if r, ok := p.renderer.(*standardRenderer); ok { + r.handleMessages(msg) + } + + var cmd Cmd + model, cmd = model.Update(msg) // run update + cmds <- cmd // process command (if any) + p.renderer.write(model.View()) // send view to renderer + } + } +} + +// Run initializes the program and runs its event loops, blocking until it gets +// terminated by either [Program.Quit], [Program.Kill], or its signal handler. +// Returns the final model. +func (p *Program) Run() (Model, error) { + p.handlers = channelHandlers{} + cmds := make(chan Cmd) + p.errs = make(chan error) + p.finished = make(chan struct{}, 1) + + defer p.cancel() + + switch p.inputType { + case defaultInput: + p.input = os.Stdin + + // The user has not set a custom input, so we need to check whether or + // not standard input is a terminal. If it's not, we open a new TTY for + // input. This will allow things to "just work" in cases where data was + // piped in or redirected to the application. + // + // To disable input entirely pass nil to the [WithInput] program option. + f, isFile := p.input.(term.File) + if !isFile { + break + } + if term.IsTerminal(f.Fd()) { + break + } + + f, err := openInputTTY() + if err != nil { + return p.initialModel, err + } + defer f.Close() //nolint:errcheck + p.input = f + + case ttyInput: + // Open a new TTY, by request + f, err := openInputTTY() + if err != nil { + return p.initialModel, err + } + defer f.Close() //nolint:errcheck + p.input = f + + case customInput: + // (There is nothing extra to do.) + } + + // Handle signals. + if !p.startupOptions.has(withoutSignalHandler) { + p.handlers.add(p.handleSignals()) + } + + // Recover from panics. + if !p.startupOptions.has(withoutCatchPanics) { + defer p.recoverFromPanic() + } + + // If no renderer is set use the standard one. + if p.renderer == nil { + p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor), p.fps) + } + + // Check if output is a TTY before entering raw mode, hiding the cursor and + // so on. + if err := p.initTerminal(); err != nil { + return p.initialModel, err + } + + // Honor program startup options. + if p.startupTitle != "" { + p.renderer.setWindowTitle(p.startupTitle) + } + if p.startupOptions&withAltScreen != 0 { + p.renderer.enterAltScreen() + } + if p.startupOptions&withoutBracketedPaste == 0 { + p.renderer.enableBracketedPaste() + } + if p.startupOptions&withMouseCellMotion != 0 { + p.renderer.enableMouseCellMotion() + p.renderer.enableMouseSGRMode() + } else if p.startupOptions&withMouseAllMotion != 0 { + p.renderer.enableMouseAllMotion() + p.renderer.enableMouseSGRMode() + } + if p.startupOptions&withReportFocus != 0 { + p.renderer.enableReportFocus() + } + + // Start the renderer. + p.renderer.start() + + // Initialize the program. + model := p.initialModel + if initCmd := model.Init(); initCmd != nil { + ch := make(chan struct{}) + p.handlers.add(ch) + + go func() { + defer close(ch) + + select { + case cmds <- initCmd: + case <-p.ctx.Done(): + } + }() + } + + // Render the initial view. + p.renderer.write(model.View()) + + // Subscribe to user input. + if p.input != nil { + if err := p.initCancelReader(); err != nil { + return model, err + } + } + + // Handle resize events. + p.handlers.add(p.handleResize()) + + // Process commands. + p.handlers.add(p.handleCommands(cmds)) + + // Run event loop, handle updates and draw. + model, err := p.eventLoop(model, cmds) + killed := p.ctx.Err() != nil + if killed { + err = fmt.Errorf("%w: %s", ErrProgramKilled, p.ctx.Err()) + } else { + // Ensure we rendered the final state of the model. + p.renderer.write(model.View()) + } + + // Restore terminal state. + p.shutdown(killed) + + return model, err +} + +// StartReturningModel initializes the program and runs its event loops, +// blocking until it gets terminated by either [Program.Quit], [Program.Kill], +// or its signal handler. Returns the final model. +// +// Deprecated: please use [Program.Run] instead. +func (p *Program) StartReturningModel() (Model, error) { + return p.Run() +} + +// Start initializes the program and runs its event loops, blocking until it +// gets terminated by either [Program.Quit], [Program.Kill], or its signal +// handler. +// +// Deprecated: please use [Program.Run] instead. +func (p *Program) Start() error { + _, err := p.Run() + return err +} + +// Send sends a message to the main update function, effectively allowing +// messages to be injected from outside the program for interoperability +// purposes. +// +// If the program hasn't started yet this will be a blocking operation. +// If the program has already been terminated this will be a no-op, so it's safe +// to send messages after the program has exited. +func (p *Program) Send(msg Msg) { + select { + case <-p.ctx.Done(): + case p.msgs <- msg: + } +} + +// Quit is a convenience function for quitting Bubble Tea programs. Use it +// when you need to shut down a Bubble Tea program from the outside. +// +// If you wish to quit from within a Bubble Tea program use the Quit command. +// +// If the program is not running this will be a no-op, so it's safe to call +// if the program is unstarted or has already exited. +func (p *Program) Quit() { + p.Send(Quit()) +} + +// Kill stops the program immediately and restores the former terminal state. +// The final render that you would normally see when quitting will be skipped. +// [program.Run] returns a [ErrProgramKilled] error. +func (p *Program) Kill() { + p.shutdown(true) +} + +// Wait waits/blocks until the underlying Program finished shutting down. +func (p *Program) Wait() { + <-p.finished +} + +// shutdown performs operations to free up resources and restore the terminal +// to its original state. +func (p *Program) shutdown(kill bool) { + p.cancel() + + // Wait for all handlers to finish. + p.handlers.shutdown() + + // Check if the cancel reader has been setup before waiting and closing. + if p.cancelReader != nil { + // Wait for input loop to finish. + if p.cancelReader.Cancel() { + if !kill { + p.waitForReadLoop() + } + } + _ = p.cancelReader.Close() + } + + if p.renderer != nil { + if kill { + p.renderer.kill() + } else { + p.renderer.stop() + } + } + + _ = p.restoreTerminalState() + if !kill { + p.finished <- struct{}{} + } +} + +// recoverFromPanic recovers from a panic, prints the stack trace, and restores +// the terminal to a usable state. +func (p *Program) recoverFromPanic() { + if r := recover(); r != nil { + p.shutdown(true) + fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r) + debug.PrintStack() + } +} + +// ReleaseTerminal restores the original terminal state and cancels the input +// reader. You can return control to the Program with RestoreTerminal. +func (p *Program) ReleaseTerminal() error { + atomic.StoreUint32(&p.ignoreSignals, 1) + if p.cancelReader != nil { + p.cancelReader.Cancel() + } + + p.waitForReadLoop() + + if p.renderer != nil { + p.renderer.stop() + p.altScreenWasActive = p.renderer.altScreen() + p.bpWasActive = p.renderer.bracketedPasteActive() + p.reportFocus = p.renderer.reportFocus() + } + + return p.restoreTerminalState() +} + +// RestoreTerminal reinitializes the Program's input reader, restores the +// terminal to the former state when the program was running, and repaints. +// Use it to reinitialize a Program after running ReleaseTerminal. +func (p *Program) RestoreTerminal() error { + atomic.StoreUint32(&p.ignoreSignals, 0) + + if err := p.initTerminal(); err != nil { + return err + } + if err := p.initCancelReader(); err != nil { + return err + } + if p.altScreenWasActive { + p.renderer.enterAltScreen() + } else { + // entering alt screen already causes a repaint. + go p.Send(repaintMsg{}) + } + if p.renderer != nil { + p.renderer.start() + } + if p.bpWasActive { + p.renderer.enableBracketedPaste() + } + if p.reportFocus { + p.renderer.enableReportFocus() + } + + // If the output is a terminal, it may have been resized while another + // process was at the foreground, in which case we may not have received + // SIGWINCH. Detect any size change now and propagate the new size as + // needed. + go p.checkResize() + + return nil +} + +// Println prints above the Program. This output is unmanaged by the program +// and will persist across renders by the Program. +// +// If the altscreen is active no output will be printed. +func (p *Program) Println(args ...interface{}) { + p.msgs <- printLineMessage{ + messageBody: fmt.Sprint(args...), + } +} + +// Printf prints above the Program. It takes a format template followed by +// values similar to fmt.Printf. This output is unmanaged by the program and +// will persist across renders by the Program. +// +// Unlike fmt.Printf (but similar to log.Printf) the message will be print on +// its own line. +// +// If the altscreen is active no output will be printed. +func (p *Program) Printf(template string, args ...interface{}) { + p.msgs <- printLineMessage{ + messageBody: fmt.Sprintf(template, args...), + } +} diff --git a/vendor/github.com/charmbracelet/bubbletea/tea_init.go b/vendor/github.com/charmbracelet/bubbletea/tea_init.go new file mode 100644 index 00000000..19b6cc39 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/tea_init.go @@ -0,0 +1,22 @@ +package tea + +import ( + "github.com/charmbracelet/lipgloss" +) + +func init() { + // XXX: This is a workaround to make assure that Lip Gloss and Termenv + // query the terminal before any Bubble Tea Program runs and acquires the + // terminal. Without this, Programs that use Lip Gloss/Termenv might hang + // while waiting for a a [termenv.OSCTimeout] while querying the terminal + // for its background/foreground colors. + // + // This happens because Bubble Tea acquires the terminal before termenv + // reads any responses. + // + // Note that this will only affect programs running on the default IO i.e. + // [os.Stdout] and [os.Stdin]. + // + // This workaround will be removed in v2. + _ = lipgloss.HasDarkBackground() +} diff --git a/vendor/github.com/charmbracelet/bubbletea/tty.go b/vendor/github.com/charmbracelet/bubbletea/tty.go new file mode 100644 index 00000000..02507782 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/tty.go @@ -0,0 +1,136 @@ +package tea + +import ( + "errors" + "fmt" + "io" + "time" + + "github.com/charmbracelet/x/term" + "github.com/muesli/cancelreader" +) + +func (p *Program) suspend() { + if err := p.ReleaseTerminal(); err != nil { + // If we can't release input, abort. + return + } + + suspendProcess() + + _ = p.RestoreTerminal() + go p.Send(ResumeMsg{}) +} + +func (p *Program) initTerminal() error { + if _, ok := p.renderer.(*nilRenderer); ok { + // No need to initialize the terminal if we're not rendering + return nil + } + + if err := p.initInput(); err != nil { + return err + } + + p.renderer.hideCursor() + return nil +} + +// restoreTerminalState restores the terminal to the state prior to running the +// Bubble Tea program. +func (p *Program) restoreTerminalState() error { + if p.renderer != nil { + p.renderer.disableBracketedPaste() + p.renderer.showCursor() + p.disableMouse() + + if p.renderer.reportFocus() { + p.renderer.disableReportFocus() + } + + if p.renderer.altScreen() { + p.renderer.exitAltScreen() + + // give the terminal a moment to catch up + time.Sleep(time.Millisecond * 10) //nolint:gomnd + } + } + + return p.restoreInput() +} + +// restoreInput restores the tty input to its original state. +func (p *Program) restoreInput() error { + if p.ttyInput != nil && p.previousTtyInputState != nil { + if err := term.Restore(p.ttyInput.Fd(), p.previousTtyInputState); err != nil { + return fmt.Errorf("error restoring console: %w", err) + } + } + if p.ttyOutput != nil && p.previousOutputState != nil { + if err := term.Restore(p.ttyOutput.Fd(), p.previousOutputState); err != nil { + return fmt.Errorf("error restoring console: %w", err) + } + } + return nil +} + +// initCancelReader (re)commences reading inputs. +func (p *Program) initCancelReader() error { + var err error + p.cancelReader, err = newInputReader(p.input) + if err != nil { + return fmt.Errorf("error creating cancelreader: %w", err) + } + + p.readLoopDone = make(chan struct{}) + go p.readLoop() + + return nil +} + +func (p *Program) readLoop() { + defer close(p.readLoopDone) + + err := readInputs(p.ctx, p.msgs, p.cancelReader) + if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) { + select { + case <-p.ctx.Done(): + case p.errs <- err: + } + } +} + +// waitForReadLoop waits for the cancelReader to finish its read loop. +func (p *Program) waitForReadLoop() { + select { + case <-p.readLoopDone: + case <-time.After(500 * time.Millisecond): //nolint:gomnd + // The read loop hangs, which means the input + // cancelReader's cancel function has returned true even + // though it was not able to cancel the read. + } +} + +// checkResize detects the current size of the output and informs the program +// via a WindowSizeMsg. +func (p *Program) checkResize() { + if p.ttyOutput == nil { + // can't query window size + return + } + + w, h, err := term.GetSize(p.ttyOutput.Fd()) + if err != nil { + select { + case <-p.ctx.Done(): + case p.errs <- err: + } + + return + } + + p.Send(WindowSizeMsg{ + Width: w, + Height: h, + }) +} diff --git a/vendor/github.com/charmbracelet/bubbletea/tty_unix.go b/vendor/github.com/charmbracelet/bubbletea/tty_unix.go new file mode 100644 index 00000000..5cbb4fe1 --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/tty_unix.go @@ -0,0 +1,49 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos +// +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos + +package tea + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/charmbracelet/x/term" +) + +func (p *Program) initInput() (err error) { + // Check if input is a terminal + if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) { + p.ttyInput = f + p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd()) + if err != nil { + return fmt.Errorf("error entering raw mode: %w", err) + } + } + + if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) { + p.ttyOutput = f + } + + return nil +} + +func openInputTTY() (*os.File, error) { + f, err := os.Open("/dev/tty") + if err != nil { + return nil, fmt.Errorf("could not open a new TTY: %w", err) + } + return f, nil +} + +const suspendSupported = true + +// Send SIGTSTP to the entire process group. +func suspendProcess() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGCONT) + _ = syscall.Kill(0, syscall.SIGTSTP) + // blocks until a CONT happens... + <-c +} diff --git a/vendor/github.com/charmbracelet/bubbletea/tty_windows.go b/vendor/github.com/charmbracelet/bubbletea/tty_windows.go new file mode 100644 index 00000000..a3a2525b --- /dev/null +++ b/vendor/github.com/charmbracelet/bubbletea/tty_windows.go @@ -0,0 +1,68 @@ +//go:build windows +// +build windows + +package tea + +import ( + "fmt" + "os" + + "github.com/charmbracelet/x/term" + "golang.org/x/sys/windows" +) + +func (p *Program) initInput() (err error) { + // Save stdin state and enable VT input + // We also need to enable VT + // input here. + if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) { + p.ttyInput = f + p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd()) + if err != nil { + return err + } + + // Enable VT input + var mode uint32 + if err := windows.GetConsoleMode(windows.Handle(p.ttyInput.Fd()), &mode); err != nil { + return fmt.Errorf("error getting console mode: %w", err) + } + + if err := windows.SetConsoleMode(windows.Handle(p.ttyInput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil { + return fmt.Errorf("error setting console mode: %w", err) + } + } + + // Save output screen buffer state and enable VT processing. + if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) { + p.ttyOutput = f + p.previousOutputState, err = term.GetState(f.Fd()) + if err != nil { + return err + } + + var mode uint32 + if err := windows.GetConsoleMode(windows.Handle(p.ttyOutput.Fd()), &mode); err != nil { + return fmt.Errorf("error getting console mode: %w", err) + } + + if err := windows.SetConsoleMode(windows.Handle(p.ttyOutput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil { + return fmt.Errorf("error setting console mode: %w", err) + } + } + + return +} + +// Open the Windows equivalent of a TTY. +func openInputTTY() (*os.File, error) { + f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644) + if err != nil { + return nil, err + } + return f, nil +} + +const suspendSupported = false + +func suspendProcess() {} diff --git a/vendor/github.com/charmbracelet/huh/.gitattributes b/vendor/github.com/charmbracelet/huh/.gitattributes new file mode 100644 index 00000000..5f9b1ec3 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/.gitattributes @@ -0,0 +1,2 @@ +*.gif filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/vendor/github.com/charmbracelet/huh/.gitignore b/vendor/github.com/charmbracelet/huh/.gitignore new file mode 100644 index 00000000..3b735ec4 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/.gitignore @@ -0,0 +1,21 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work diff --git a/vendor/github.com/charmbracelet/huh/.golangci-soft.yml b/vendor/github.com/charmbracelet/huh/.golangci-soft.yml new file mode 100644 index 00000000..55255e59 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/.golangci-soft.yml @@ -0,0 +1,47 @@ +run: + tests: false + +issues: + include: + - EXC0001 + - EXC0005 + - EXC0011 + - EXC0012 + - EXC0013 + + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + enable: + # - dupl + - exhaustive + # - exhaustivestruct + - goconst + - godot + - godox + - gomnd + - gomoddirectives + - goprintffuncname + # - ifshort + # - lll + - misspell + - nakedret + - nestif + - noctx + - nolintlint + - prealloc + - wrapcheck + + # disable default linters, they are already enabled in .golangci.yml + disable: + - deadcode + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - unused + - varcheck diff --git a/vendor/github.com/charmbracelet/huh/.golangci.yml b/vendor/github.com/charmbracelet/huh/.golangci.yml new file mode 100644 index 00000000..a5a91d0d --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/.golangci.yml @@ -0,0 +1,29 @@ +run: + tests: false + +issues: + include: + - EXC0001 + - EXC0005 + - EXC0011 + - EXC0012 + - EXC0013 + + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + enable: + - bodyclose + - exportloopref + - goimports + - gosec + - nilerr + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - tparallel + - unconvert + - unparam + - whitespace diff --git a/vendor/github.com/charmbracelet/huh/CONTRIBUTING.md b/vendor/github.com/charmbracelet/huh/CONTRIBUTING.md new file mode 100644 index 00000000..ea9a9632 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/CONTRIBUTING.md @@ -0,0 +1,3 @@ +Contributions are welcome! + +Please submit a PR or open an issue to discuss features. diff --git a/vendor/github.com/charmbracelet/huh/LICENSE b/vendor/github.com/charmbracelet/huh/LICENSE new file mode 100644 index 00000000..2a08f15d --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Charm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/charmbracelet/huh/Makefile b/vendor/github.com/charmbracelet/huh/Makefile new file mode 100644 index 00000000..d3f18ae9 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/Makefile @@ -0,0 +1,17 @@ +.PHONY: spinner + +$(V).SILENT: +test: + go test ./... + +spinner: + cd spinner/examples/loading && go run . + +burger: + cd examples/burger && go run . + +theme: + cd examples/theme && go run . + +gh: + cd examples/gh && go run . diff --git a/vendor/github.com/charmbracelet/huh/README.md b/vendor/github.com/charmbracelet/huh/README.md new file mode 100644 index 00000000..cff542f4 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/README.md @@ -0,0 +1,431 @@ +# Huh? + +

+ Hey there! I'm Glenn! +

+ Latest Release + Go Docs + Build Status +

+ +A simple, powerful library for building interactive forms and prompts in the terminal. + +Running a burger form + +`huh?` is easy to use in a standalone fashion, can be +[integrated into a Bubble Tea application](#what-about-bubble-tea), and contains +a first-class [accessible mode](#accessibility) for screen readers. + +The above example is running from a single Go program ([source](./examples/burger/main.go)). + +## Tutorial + +Let’s build a form for ordering burgers. To start, we’ll import the library and +define a few variables where’ll we store answers. + +```go +package main + +import "github.com/charmbracelet/huh" + +var ( + burger string + toppings []string + sauceLevel int + name string + instructions string + discount bool +) +``` + +`huh?` separates forms into groups (you can think of groups as pages). Groups +are made of fields (e.g. `Select`, `Input`, `Text`). We will set up three +groups for the customer to fill out. + +```go +form := huh.NewForm( + huh.NewGroup( + // Ask the user for a base burger and toppings. + huh.NewSelect[string](). + Title("Choose your burger"). + Options( + huh.NewOption("Charmburger Classic", "classic"), + huh.NewOption("Chickwich", "chickwich"), + huh.NewOption("Fishburger", "fishburger"), + huh.NewOption("Charmpossible™ Burger", "charmpossible"), + ). + Value(&burger), // store the chosen option in the "burger" variable + + // Let the user select multiple toppings. + huh.NewMultiSelect[string](). + Title("Toppings"). + Options( + huh.NewOption("Lettuce", "lettuce").Selected(true), + huh.NewOption("Tomatoes", "tomatoes").Selected(true), + huh.NewOption("Jalapeños", "jalapeños"), + huh.NewOption("Cheese", "cheese"), + huh.NewOption("Vegan Cheese", "vegan cheese"), + huh.NewOption("Nutella", "nutella"), + ). + Limit(4). // there’s a 4 topping limit! + Value(&toppings), + + // Option values in selects and multi selects can be any type you + // want. We’ve been recording strings above, but here we’ll store + // answers as integers. Note the generic "[int]" directive below. + huh.NewSelect[int](). + Title("How much Charm Sauce do you want?"). + Options( + huh.NewOption("None", 0), + huh.NewOption("A little", 1), + huh.NewOption("A lot", 2), + ). + Value(&sauceLevel), + ), + + // Gather some final details about the order. + huh.NewGroup( + huh.NewInput(). + Title("What's your name?"). + Value(&name). + // Validating fields is easy. The form will mark erroneous fields + // and display error messages accordingly. + Validate(func(str string) error { + if str == "Frank" { + return errors.New("Sorry, we don’t serve customers named Frank.") + } + return nil + }), + + huh.NewText(). + Title("Special Instructions"). + CharLimit(400). + Value(&instructions), + + huh.NewConfirm(). + Title("Would you like 15% off?"). + Value(&discount), + ), +) +``` + +Finally, run the form: + +```go +err := form.Run() +if err != nil { + log.Fatal(err) +} + +if !discount { + fmt.Println("What? You didn’t take the discount?!") +} +``` + +And that’s it! For more info see [the full source][burgersource] for this +example as well as [the docs][docs]. + +[burgersource]: ./examples/burger/main.go +[docs]: https://pkg.go.dev/github.com/charmbracelet/huh?tab=doc + +## Field Reference + +- [`Input`](#input): single line text input +- [`Text`](#text): multi-line text input +- [`Select`](#select): select an option from a list +- [`MultiSelect`](#multiple-select): select multiple options from a list +- [`Confirm`](#confirm): confirm an action (yes or no) + +> [!TIP] +> Just want to prompt the user with a single field? Each field has a `Run` +> method that can be used as a shorthand for gathering quick and easy input. + +```go +var name string + +huh.NewInput(). + Title("What's your name?"). + Value(&name). + Run() // this is blocking... + +fmt.Printf("Hey, %s!\n", name) +``` + +### Input + +Prompt the user for a single line of text. + +Input field + +```go +huh.NewInput(). + Title("What's for lunch?"). + Prompt("?"). + Validate(isFood). + Value(&lunch) +``` + +### Text + +Prompt the user for multiple lines of text. + +Text field + +```go +huh.NewText(). + Title("Tell me a story."). + Validate(checkForPlagiarism). + Value(&story) +``` + +### Select + +Prompt the user to select a single option from a list. + +Select field + +```go +huh.NewSelect[string](). + Title("Pick a country."). + Options( + huh.NewOption("United States", "US"), + huh.NewOption("Germany", "DE"), + huh.NewOption("Brazil", "BR"), + huh.NewOption("Canada", "CA"), + ). + Value(&country) +``` + +### Multiple Select + +Prompt the user to select multiple (zero or more) options from a list. + +Multiselect field + +```go +huh.NewMultiSelect[string](). + Options( + huh.NewOption("Lettuce", "Lettuce").Selected(true), + huh.NewOption("Tomatoes", "Tomatoes").Selected(true), + huh.NewOption("Charm Sauce", "Charm Sauce"), + huh.NewOption("Jalapeños", "Jalapeños"), + huh.NewOption("Cheese", "Cheese"), + huh.NewOption("Vegan Cheese", "Vegan Cheese"), + huh.NewOption("Nutella", "Nutella"), + ). + Title("Toppings"). + Limit(4). + Value(&toppings) +``` + +### Confirm + +Prompt the user to confirm (Yes or No). + +Confirm field + +```go +huh.NewConfirm(). + Title("Are you sure?"). + Affirmative("Yes!"). + Negative("No."). + Value(&confirm) +``` + +## Accessibility + +`huh?` has a special rendering option designed specifically for screen readers. +You can enable it with `form.WithAccessible(true)`. + +> [!TIP] +> We recommend setting this through an environment variable or configuration +> option to allow the user to control accessibility. + +```go +accessibleMode := os.Getenv("ACCESSIBLE") != "" +form.WithAccessible(accessibleMode) +``` + +Accessible forms will drop TUIs in favor of standard prompts, providing better +dictation and feedback of the information on screen for the visually impaired. + +Accessible cuisine form + +## Themes + +`huh?` contains a powerful theme abstraction. Supply your own custom theme or +choose from one of the five predefined themes: + +- `Charm` +- `Dracula` +- `Catppuccin` +- `Base 16` +- `Default` + +
+

+ Charm-themed form + Dracula-themed form + Catppuccin-themed form + Base 16-themed form + Default-themed form +

+ +Themes can take advantage of the full range of +[Lip Gloss][lipgloss] style options. For a high level theme reference see +[the docs](https://pkg.go.dev/github.com/charmbracelet/huh#Theme). + +[lipgloss]: https://github.com/charmbracelet/lipgloss + +## Bonus: Spinner + +`huh?` ships with a standalone spinner package. It’s useful for indicating +background activity after a form is submitted. + +Spinner while making a burger + +Create a new spinner, set a title, set the action (or provide a `Context`), and run the spinner: + + + + + + + + + +
Action Style Context Style
+ +```go +err := spinner.New(). + Title("Making your burger..."). + Action(makeBurger). + Run() + +fmt.Println("Order up!") +``` + + + +```go +go makeBurger() + +err := spinner.New(). + Type(spinner.Line). + Title("Making your burger..."). + Context(ctx). + Run() + +fmt.Println("Order up!") +``` + +
+ +For more on Spinners see the [spinner examples](./spinner/examples) and +[the spinner docs](https://pkg.go.dev/github.com/charmbracelet/huh/spinner). + +## What about Bubble Tea? + +Bubbletea + Huh? + +In addition to its standalone mode, `huh?` has first-class support for +[Bubble Tea][tea] and can be easily integrated into Bubble Tea applications. +It’s incredibly useful in portions of your Bubble Tea application that need +form-like input. + +Bubble Tea embedded form example + +A `huh.Form` is merely a `tea.Model`, so you can use it just as +you would any other [Bubble](https://github.com/charmbracelet/bubbles). + +```go +type Model struct { + form *huh.Form // huh.Form is just a tea.Model +} + +func NewModel() Model { + return Model{ + form: huh.NewForm( + huh.NewGroup( + huh.NewSelect[string](). + Key("class"). + Options(huh.NewOptions("Warrior", "Mage", "Rogue")...). + Title("Choose your class"), + + huh.NewSelect[int](). + Key("level"). + Options(huh.NewOptions(1, 20, 9999)...). + Title("Choose your level"), + ), + ) + } +} + +func (m Model) Init() tea.Cmd { + return m.form.Init() +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // ... + + form, cmd := m.form.Update(msg) + if f, ok := form.(*huh.Form); ok { + m.form = f + } + + return m, cmd +} + +func (m Model) View() string { + if m.form.State == huh.StateCompleted { + class := m.form.GetString("class") + level := m.form.GetString("level") + return fmt.Sprintf("You selected: %s, Lvl. %d", class, level) + } + return m.form.View() +} + +``` + +For more info in using `huh?` in Bubble Tea applications see [the full Bubble +Tea example][example]. + +[tea]: https://github.com/charmbracelet/bubbletea +[bubbles]: https://github.com/charmbracelet/bubbles +[example]: https://github.com/charmbracelet/huh/blob/main/examples/bubbletea/main.go + +## `Huh?` in the Wild +For some `Huh?` programs in production, see: + +* [glyphs](https://github.com/maaslalani/glyphs): a unicode symbol picker +* [meteor](https://github.com/stefanlogue/meteor): a highly customisable conventional commit message tool +* [freeze](https://github.com/charmbracelet/freeze): a tool for generating images of code and terminal output +* [gum](https://github.com/charmbracelet/gum): a tool for glamorous shell scripts +* [savvy](https://github.com/getsavvyinc/savvy-cli): the easiest way to create, share, and run runbooks in the terminal + + +## Feedback + +We'd love to hear your thoughts on this project. Feel free to drop us a note! + +- [Twitter](https://twitter.com/charmcli) +- [The Fediverse](https://mastodon.social/@charmcli) +- [Discord](https://charm.sh/chat) + +## Acknowledgments + +`huh?` is inspired by the wonderful [Survey][survey] library by Alec Aivazis. + +[survey]: https://github.com/AlecAivazis/survey + +## License + +[MIT](https://github.com/charmbracelet/bubbletea/raw/master/LICENSE) + +--- + +Part of [Charm](https://charm.sh). + +The Charm logo + +Charmçƒ­çˆ±å¼€æº â€¢ Charm loves open source • نحن٠نحب المصادر المÙتوحة diff --git a/vendor/github.com/charmbracelet/huh/SECURITY.md b/vendor/github.com/charmbracelet/huh/SECURITY.md new file mode 100644 index 00000000..8fa013b3 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please email [vt100@charm.sh](mailto:vt100@charm.sh) for any possible security vulnerabilities. diff --git a/vendor/github.com/charmbracelet/huh/accessibility/accessibility.go b/vendor/github.com/charmbracelet/huh/accessibility/accessibility.go new file mode 100644 index 00000000..8f957e2a --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/accessibility/accessibility.go @@ -0,0 +1,94 @@ +package accessibility + +import ( + "bufio" + "errors" + "fmt" + "os" + "strconv" + "strings" +) + +// PromptInt prompts a user for an integer between a certain range. +// +// Given invalid input (non-integers, integers outside of the range), the user +// will continue to be reprompted until a valid input is given, ensuring that +// the return value is always valid. +func PromptInt(prompt string, min, max int) int { + var ( + input string + choice int + ) + + validInt := func(s string) error { + i, err := strconv.Atoi(s) + if err != nil || i < min || i > max { + return errors.New("invalid input. please try again") + } + return nil + } + + input = PromptString(prompt, validInt) + choice, _ = strconv.Atoi(input) + return choice +} + +func parseBool(s string) (bool, error) { + s = strings.ToLower(s) + + for _, y := range []string{"y", "yes"} { + if y == s { + return true, nil + } + } + + for _, n := range []string{"n", "no"} { + if n == s { + return false, nil + } + } + + return false, errors.New("invalid input. please try again") +} + +// PromptBool prompts a user for a boolean value. +// +// Given invalid input (non-boolean), the user will continue to be reprompted +// until a valid input is given, ensuring that the return value is always valid. +func PromptBool() bool { + validBool := func(s string) error { + _, err := parseBool(s) + return err + } + + input := PromptString("Choose [y/N]: ", validBool) + b, _ := parseBool(input) + return b +} + +// PromptString prompts a user for a string value and validates it against a +// validator function. It re-prompts the user until a valid input is given. +func PromptString(prompt string, validator func(input string) error) string { + scanner := bufio.NewScanner(os.Stdin) + + var ( + valid bool + input string + ) + + for !valid { + fmt.Print(prompt) + _ = scanner.Scan() + input = scanner.Text() + + err := validator(input) + if err != nil { + fmt.Println(err) + continue + } + + break + } + + return input +} diff --git a/vendor/github.com/charmbracelet/huh/clamp.go b/vendor/github.com/charmbracelet/huh/clamp.go new file mode 100644 index 00000000..e210621b --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/clamp.go @@ -0,0 +1,22 @@ +package huh + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func clamp(n, low, high int) int { + if low > high { + low, high = high, low + } + return min(high, max(low, n)) +} diff --git a/vendor/github.com/charmbracelet/huh/field_confirm.go b/vendor/github.com/charmbracelet/huh/field_confirm.go new file mode 100644 index 00000000..debed0f4 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/field_confirm.go @@ -0,0 +1,285 @@ +package huh + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh/accessibility" + "github.com/charmbracelet/lipgloss" +) + +// Confirm is a form confirm field. +type Confirm struct { + value *bool + key string + + // customization + title string + description string + affirmative string + negative string + + // error handling + validate func(bool) error + err error + + // state + focused bool + + // options + width int + height int + inline bool + accessible bool + theme *Theme + keymap ConfirmKeyMap +} + +// NewConfirm returns a new confirm field. +func NewConfirm() *Confirm { + return &Confirm{ + value: new(bool), + affirmative: "Yes", + negative: "No", + validate: func(bool) error { return nil }, + } +} + +// Validate sets the validation function of the confirm field. +func (c *Confirm) Validate(validate func(bool) error) *Confirm { + c.validate = validate + return c +} + +// Error returns the error of the confirm field. +func (c *Confirm) Error() error { + return c.err +} + +// Skip returns whether the confirm should be skipped or should be blocking. +func (*Confirm) Skip() bool { + return false +} + +// Zoom returns whether the input should be zoomed. +func (*Confirm) Zoom() bool { + return false +} + +// Affirmative sets the affirmative value of the confirm field. +func (c *Confirm) Affirmative(affirmative string) *Confirm { + c.affirmative = affirmative + return c +} + +// Negative sets the negative value of the confirm field. +func (c *Confirm) Negative(negative string) *Confirm { + c.negative = negative + return c +} + +// Value sets the value of the confirm field. +func (c *Confirm) Value(value *bool) *Confirm { + c.value = value + return c +} + +// Key sets the key of the confirm field. +func (c *Confirm) Key(key string) *Confirm { + c.key = key + return c +} + +// Title sets the title of the confirm field. +func (c *Confirm) Title(title string) *Confirm { + c.title = title + return c +} + +// Description sets the description of the confirm field. +func (c *Confirm) Description(description string) *Confirm { + c.description = description + return c +} + +// Inline sets whether the field should be inline. +func (c *Confirm) Inline(inline bool) *Confirm { + c.inline = inline + return c +} + +// Focus focuses the confirm field. +func (c *Confirm) Focus() tea.Cmd { + c.focused = true + return nil +} + +// Blur blurs the confirm field. +func (c *Confirm) Blur() tea.Cmd { + c.focused = false + c.err = c.validate(*c.value) + return nil +} + +// KeyBinds returns the help message for the confirm field. +func (c *Confirm) KeyBinds() []key.Binding { + return []key.Binding{c.keymap.Toggle, c.keymap.Prev, c.keymap.Submit, c.keymap.Next} +} + +// Init initializes the confirm field. +func (c *Confirm) Init() tea.Cmd { + return nil +} + +// Update updates the confirm field. +func (c *Confirm) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + c.err = nil + switch { + case key.Matches(msg, c.keymap.Toggle): + if c.negative == "" { + break + } + v := !*c.value + *c.value = v + case key.Matches(msg, c.keymap.Prev): + cmds = append(cmds, PrevField) + case key.Matches(msg, c.keymap.Next, c.keymap.Submit): + cmds = append(cmds, NextField) + } + } + + return c, tea.Batch(cmds...) +} + +func (c *Confirm) activeStyles() *FieldStyles { + theme := c.theme + if theme == nil { + theme = ThemeCharm() + } + if c.focused { + return &theme.Focused + } + return &theme.Blurred +} + +// View renders the confirm field. +func (c *Confirm) View() string { + styles := c.activeStyles() + + var sb strings.Builder + sb.WriteString(styles.Title.Render(c.title)) + if c.err != nil { + sb.WriteString(styles.ErrorIndicator.String()) + } + + description := styles.Description.Render(c.description) + + if !c.inline && c.description != "" { + sb.WriteString("\n") + } + sb.WriteString(description) + + if !c.inline { + sb.WriteString("\n") + sb.WriteString("\n") + } + + var negative string + var affirmative string + if c.negative != "" { + if *c.value { + affirmative = styles.FocusedButton.Render(c.affirmative) + negative = styles.BlurredButton.Render(c.negative) + } else { + affirmative = styles.BlurredButton.Render(c.affirmative) + negative = styles.FocusedButton.Render(c.negative) + } + } else { + affirmative = styles.FocusedButton.Render(c.affirmative) + } + + sb.WriteString(lipgloss.JoinHorizontal(lipgloss.Center, affirmative, negative)) + return styles.Base.Render(sb.String()) +} + +// Run runs the confirm field in accessible mode. +func (c *Confirm) Run() error { + if c.accessible { + return c.runAccessible() + } + return Run(c) +} + +// runAccessible runs the confirm field in accessible mode. +func (c *Confirm) runAccessible() error { + styles := c.activeStyles() + fmt.Println(styles.Title.Render(c.title)) + fmt.Println() + *c.value = accessibility.PromptBool() + fmt.Println(styles.SelectedOption.Render("Chose: "+c.String()) + "\n") + return nil +} + +func (c *Confirm) String() string { + if *c.value { + return c.affirmative + } + return c.negative +} + +// WithTheme sets the theme of the confirm field. +func (c *Confirm) WithTheme(theme *Theme) Field { + if c.theme != nil { + return c + } + c.theme = theme + return c +} + +// WithKeyMap sets the keymap of the confirm field. +func (c *Confirm) WithKeyMap(k *KeyMap) Field { + c.keymap = k.Confirm + return c +} + +// WithAccessible sets the accessible mode of the confirm field. +func (c *Confirm) WithAccessible(accessible bool) Field { + c.accessible = accessible + return c +} + +// WithWidth sets the width of the confirm field. +func (c *Confirm) WithWidth(width int) Field { + c.width = width + return c +} + +// WithHeight sets the height of the confirm field. +func (c *Confirm) WithHeight(height int) Field { + c.height = height + return c +} + +// WithPosition sets the position of the confirm field. +func (c *Confirm) WithPosition(p FieldPosition) Field { + c.keymap.Prev.SetEnabled(!p.IsFirst()) + c.keymap.Next.SetEnabled(!p.IsLast()) + c.keymap.Submit.SetEnabled(p.IsLast()) + return c +} + +// GetKey returns the key of the field. +func (c *Confirm) GetKey() string { + return c.key +} + +// GetValue returns the value of the field. +func (c *Confirm) GetValue() any { + return *c.value +} diff --git a/vendor/github.com/charmbracelet/huh/field_filepicker.go b/vendor/github.com/charmbracelet/huh/field_filepicker.go new file mode 100644 index 00000000..340407b4 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/field_filepicker.go @@ -0,0 +1,407 @@ +package huh + +import ( + "errors" + "fmt" + "os" + "strings" + + xstrings "github.com/charmbracelet/x/exp/strings" + + "github.com/charmbracelet/bubbles/filepicker" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh/accessibility" + "github.com/charmbracelet/lipgloss" +) + +// FilePicker is a form file file field. +type FilePicker struct { + value *string + key string + picker filepicker.Model + + // state + focused bool + picking bool + + // customization + title string + description string + + // error handling + validate func(string) error + err error + + // options + width int + height int + accessible bool + theme *Theme + keymap FilePickerKeyMap +} + +// NewFilePicker returns a new file field. +func NewFilePicker() *FilePicker { + fp := filepicker.New() + fp.ShowSize = false + fp.AutoHeight = false + + if cmd := fp.Init(); cmd != nil { + fp, _ = fp.Update(cmd()) + } + + return &FilePicker{ + value: new(string), + validate: func(string) error { return nil }, + picker: fp, + } +} + +// CurrentDirectory sets the directory of the file field. +func (f *FilePicker) CurrentDirectory(directory string) *FilePicker { + f.picker.CurrentDirectory = directory + return f +} + +// Picking sets whether the file picker should be in the picking files state. +func (f *FilePicker) Picking(v bool) *FilePicker { + f.setPicking(v) + return f +} + +// ShowHidden sets whether to show hidden files. +func (f *FilePicker) ShowHidden(v bool) *FilePicker { + f.picker.ShowHidden = v + return f +} + +// ShowSize sets whether to show file sizes. +func (f *FilePicker) ShowSize(v bool) *FilePicker { + f.picker.ShowSize = v + return f +} + +// ShowPermissions sets whether to show file permissions. +func (f *FilePicker) ShowPermissions(v bool) *FilePicker { + f.picker.ShowPermissions = v + return f +} + +// FileAllowed sets whether to allow files to be selected. +func (f *FilePicker) FileAllowed(v bool) *FilePicker { + f.picker.FileAllowed = v + return f +} + +// DirAllowed sets whether to allow files to be selected. +func (f *FilePicker) DirAllowed(v bool) *FilePicker { + f.picker.DirAllowed = v + return f +} + +// Value sets the value of the file field. +func (f *FilePicker) Value(value *string) *FilePicker { + f.value = value + return f +} + +// Key sets the key of the file field which can be used to retrieve the value +// after submission. +func (f *FilePicker) Key(key string) *FilePicker { + f.key = key + return f +} + +// Title sets the title of the file field. +func (f *FilePicker) Title(title string) *FilePicker { + f.title = title + return f +} + +// Description sets the description of the file field. +func (f *FilePicker) Description(description string) *FilePicker { + f.description = description + return f +} + +// AllowedTypes sets the allowed types of the file field. These will be the only +// valid file types accepted, other files will show as disabled. +func (f *FilePicker) AllowedTypes(types []string) *FilePicker { + f.picker.AllowedTypes = types + return f +} + +// Height sets the height of the file field. If the number of options +// exceeds the height, the file field will become scrollable. +func (f *FilePicker) Height(height int) *FilePicker { + adjust := 0 + if f.title != "" { + adjust++ + } + if f.description != "" { + adjust++ + } + f.picker.Height = height - adjust + f.picker.AutoHeight = false + return f +} + +// Validate sets the validation function of the file field. +func (f *FilePicker) Validate(validate func(string) error) *FilePicker { + f.validate = validate + return f +} + +// Error returns the error of the file field. +func (f *FilePicker) Error() error { + return f.err +} + +// Skip returns whether the file should be skipped or should be blocking. +func (*FilePicker) Skip() bool { + return false +} + +// Zoom returns whether the input should be zoomed. +func (f *FilePicker) Zoom() bool { + return f.picking +} + +// Focus focuses the file field. +func (f *FilePicker) Focus() tea.Cmd { + f.focused = true + return f.picker.Init() +} + +// Blur blurs the file field. +func (f *FilePicker) Blur() tea.Cmd { + f.focused = false + f.setPicking(false) + f.err = f.validate(*f.value) + return nil +} + +// KeyBinds returns the help keybindings for the file field. +func (f *FilePicker) KeyBinds() []key.Binding { + return []key.Binding{f.keymap.Up, f.keymap.Down, f.keymap.Close, f.keymap.Open, f.keymap.Prev, f.keymap.Next, f.keymap.Submit} +} + +// Init initializes the file field. +func (f *FilePicker) Init() tea.Cmd { + return f.picker.Init() +} + +// Update updates the file field. +func (f *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + f.err = nil + + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, f.keymap.Open): + if f.picking { + break + } + f.setPicking(true) + return f, f.picker.Init() + case key.Matches(msg, f.keymap.Close): + f.setPicking(false) + return f, nil + case key.Matches(msg, f.keymap.Next): + f.setPicking(false) + return f, NextField + case key.Matches(msg, f.keymap.Prev): + f.setPicking(false) + return f, PrevField + } + } + + var cmd tea.Cmd + f.picker, cmd = f.picker.Update(msg) + didSelect, file := f.picker.DidSelectFile(msg) + if didSelect { + *f.value = file + f.setPicking(false) + return f, NextField + } + didSelect, _ = f.picker.DidSelectDisabledFile(msg) + if didSelect { + f.err = errors.New(xstrings.EnglishJoin(f.picker.AllowedTypes, true) + " files only") + return f, nil + } + + return f, cmd +} + +func (f *FilePicker) activeStyles() *FieldStyles { + theme := f.theme + if theme == nil { + theme = ThemeCharm() + } + if f.focused { + return &theme.Focused + } + return &theme.Blurred +} + +// View renders the file field. +func (f *FilePicker) View() string { + styles := f.activeStyles() + + var sb strings.Builder + if f.title != "" { + sb.WriteString(styles.Title.Render(f.title) + "\n") + } + if f.description != "" { + sb.WriteString(styles.Description.Render(f.description) + "\n") + } + if f.picking { + sb.WriteString(strings.TrimSuffix(f.picker.View(), "\n")) + } else { + if *f.value != "" { + sb.WriteString(styles.SelectedOption.Render(*f.value)) + } else { + sb.WriteString(styles.TextInput.Placeholder.Render("No file selected.")) + } + } + return styles.Base.Render(sb.String()) +} + +func (f *FilePicker) setPicking(v bool) { + f.picking = v + + f.keymap.Close.SetEnabled(v) + f.keymap.Up.SetEnabled(v) + f.keymap.Down.SetEnabled(v) + f.keymap.Select.SetEnabled(v) + f.keymap.Back.SetEnabled(v) + + f.picker.KeyMap.Up.SetEnabled(v) + f.picker.KeyMap.Down.SetEnabled(v) + f.picker.KeyMap.Select.SetEnabled(v) + f.picker.KeyMap.Open.SetEnabled(v) + f.picker.KeyMap.Back.SetEnabled(v) +} + +// Run runs the file field. +func (f *FilePicker) Run() error { + if f.accessible { + return f.runAccessible() + } + return Run(f) +} + +// runAccessible runs an accessible file field. +func (f *FilePicker) runAccessible() error { + styles := f.activeStyles() + fmt.Println(styles.Title.Render(f.title)) + fmt.Println() + + validateFile := func(s string) error { + // is the string a file? + if _, err := os.Open(s); err != nil { + return errors.New("not a file") + } + + // is it one of the allowed types? + valid := false + for _, ext := range f.picker.AllowedTypes { + if strings.HasSuffix(s, ext) { + valid = true + break + } + } + if !valid { + return errors.New("cannot select: " + s) + } + + // does it pass user validation? + return f.validate(s) + } + + *f.value = accessibility.PromptString("File: ", validateFile) + fmt.Println(styles.SelectedOption.Render(*f.value + "\n")) + return nil +} + +// WithTheme sets the theme of the file field. +func (f *FilePicker) WithTheme(theme *Theme) Field { + if f.theme != nil || theme == nil { + return f + } + f.theme = theme + + // XXX: add specific themes + f.picker.Styles = filepicker.Styles{ + DisabledCursor: lipgloss.Style{}, + Cursor: theme.Focused.TextInput.Prompt, + Symlink: lipgloss.NewStyle(), + Directory: theme.Focused.Directory, + File: theme.Focused.File, + DisabledFile: theme.Focused.TextInput.Placeholder, + Permission: theme.Focused.TextInput.Placeholder, + Selected: theme.Focused.SelectedOption, + DisabledSelected: theme.Focused.TextInput.Placeholder, + FileSize: theme.Focused.TextInput.Placeholder, + EmptyDirectory: theme.Focused.TextInput.Placeholder.SetString("No files found."), + } + + return f +} + +// WithKeyMap sets the keymap on a file field. +func (f *FilePicker) WithKeyMap(k *KeyMap) Field { + f.keymap = k.FilePicker + f.picker.KeyMap = filepicker.KeyMap{ + GoToTop: k.FilePicker.GoToTop, + GoToLast: k.FilePicker.GoToLast, + Down: k.FilePicker.Down, + Up: k.FilePicker.Up, + PageUp: k.FilePicker.PageUp, + PageDown: k.FilePicker.PageDown, + Back: k.FilePicker.Back, + Open: k.FilePicker.Open, + Select: k.FilePicker.Select, + } + f.setPicking(f.picking) + return f +} + +// WithAccessible sets the accessible mode of the file field. +func (f *FilePicker) WithAccessible(accessible bool) Field { + f.accessible = accessible + return f +} + +// WithWidth sets the width of the file field. +func (f *FilePicker) WithWidth(width int) Field { + f.width = width + return f +} + +// WithHeight sets the height of the file field. +func (f *FilePicker) WithHeight(height int) Field { + f.height = height + f.Height(height) + f.picker, _ = f.picker.Update(nil) + return f +} + +// WithPosition sets the position of the file field. +func (f *FilePicker) WithPosition(p FieldPosition) Field { + f.keymap.Prev.SetEnabled(!p.IsFirst()) + f.keymap.Next.SetEnabled(!p.IsLast()) + f.keymap.Submit.SetEnabled(p.IsLast()) + return f +} + +// GetKey returns the key of the field. +func (f *FilePicker) GetKey() string { + return f.key +} + +// GetValue returns the value of the field. +func (f *FilePicker) GetValue() any { + return *f.value +} diff --git a/vendor/github.com/charmbracelet/huh/field_input.go b/vendor/github.com/charmbracelet/huh/field_input.go new file mode 100644 index 00000000..ad0b06b1 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/field_input.go @@ -0,0 +1,356 @@ +package huh + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh/accessibility" + "github.com/charmbracelet/lipgloss" +) + +// Input is a form input field. +type Input struct { + value *string + key string + + // customization + title string + description string + inline bool + + // error handling + validate func(string) error + err error + + // model + textinput textinput.Model + + // state + focused bool + + // options + width int + height int + accessible bool + theme *Theme + keymap InputKeyMap +} + +// NewInput returns a new input field. +func NewInput() *Input { + input := textinput.New() + + i := &Input{ + value: new(string), + textinput: input, + validate: func(string) error { return nil }, + } + + return i +} + +// Value sets the value of the input field. +func (i *Input) Value(value *string) *Input { + i.value = value + i.textinput.SetValue(*value) + return i +} + +// Key sets the key of the input field. +func (i *Input) Key(key string) *Input { + i.key = key + return i +} + +// Title sets the title of the input field. +func (i *Input) Title(title string) *Input { + i.title = title + return i +} + +// Description sets the description of the input field. +func (i *Input) Description(description string) *Input { + i.description = description + return i +} + +// Prompt sets the prompt of the input field. +func (i *Input) Prompt(prompt string) *Input { + i.textinput.Prompt = prompt + return i +} + +// CharLimit sets the character limit of the input field. +func (i *Input) CharLimit(charlimit int) *Input { + i.textinput.CharLimit = charlimit + return i +} + +// Suggestions sets the suggestions to display for autocomplete in the input +// field. +func (i *Input) Suggestions(suggestions []string) *Input { + i.textinput.ShowSuggestions = len(suggestions) > 0 + i.textinput.KeyMap.AcceptSuggestion.SetEnabled(len(suggestions) > 0) + i.textinput.SetSuggestions(suggestions) + return i +} + +// EchoMode sets the input behavior of the text Input field. +type EchoMode textinput.EchoMode + +const ( + // EchoNormal displays text as is. + // This is the default behavior. + EchoModeNormal EchoMode = EchoMode(textinput.EchoNormal) + + // EchoPassword displays the EchoCharacter mask instead of actual characters. + // This is commonly used for password fields. + EchoModePassword EchoMode = EchoMode(textinput.EchoPassword) + + // EchoNone displays nothing as characters are entered. + // This is commonly seen for password fields on the command line. + EchoModeNone EchoMode = EchoMode(textinput.EchoNone) +) + +// EchoMode sets the echo mode of the input. +func (i *Input) EchoMode(mode EchoMode) *Input { + i.textinput.EchoMode = textinput.EchoMode(mode) + return i +} + +// Password sets whether or not to hide the input while the user is typing. +// +// Deprecated: use EchoMode(EchoPassword) instead. +func (i *Input) Password(password bool) *Input { + if password { + i.textinput.EchoMode = textinput.EchoPassword + } else { + i.textinput.EchoMode = textinput.EchoNormal + } + return i +} + +// Placeholder sets the placeholder of the text input. +func (i *Input) Placeholder(str string) *Input { + i.textinput.Placeholder = str + return i +} + +// Inline sets whether the title and input should be on the same line. +func (i *Input) Inline(inline bool) *Input { + i.inline = inline + return i +} + +// Validate sets the validation function of the input field. +func (i *Input) Validate(validate func(string) error) *Input { + i.validate = validate + return i +} + +// Error returns the error of the input field. +func (i *Input) Error() error { + return i.err +} + +// Skip returns whether the input should be skipped or should be blocking. +func (*Input) Skip() bool { + return false +} + +// Zoom returns whether the input should be zoomed. +func (*Input) Zoom() bool { + return false +} + +// Focus focuses the input field. +func (i *Input) Focus() tea.Cmd { + i.focused = true + return i.textinput.Focus() +} + +// Blur blurs the input field. +func (i *Input) Blur() tea.Cmd { + i.focused = false + *i.value = i.textinput.Value() + i.textinput.Blur() + i.err = i.validate(*i.value) + return nil +} + +// KeyBinds returns the help message for the input field. +func (i *Input) KeyBinds() []key.Binding { + if i.textinput.ShowSuggestions { + return []key.Binding{i.keymap.AcceptSuggestion, i.keymap.Prev, i.keymap.Submit, i.keymap.Next} + } + return []key.Binding{i.keymap.Prev, i.keymap.Submit, i.keymap.Next} +} + +// Init initializes the input field. +func (i *Input) Init() tea.Cmd { + i.textinput.Blur() + return nil +} + +// Update updates the input field. +func (i *Input) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + i.textinput, cmd = i.textinput.Update(msg) + cmds = append(cmds, cmd) + *i.value = i.textinput.Value() + + switch msg := msg.(type) { + case tea.KeyMsg: + i.err = nil + + switch { + case key.Matches(msg, i.keymap.Prev): + value := i.textinput.Value() + i.err = i.validate(value) + if i.err != nil { + return i, nil + } + cmds = append(cmds, PrevField) + case key.Matches(msg, i.keymap.Next, i.keymap.Submit): + value := i.textinput.Value() + i.err = i.validate(value) + if i.err != nil { + return i, nil + } + cmds = append(cmds, NextField) + } + } + + return i, tea.Batch(cmds...) +} + +func (i *Input) activeStyles() *FieldStyles { + theme := i.theme + if theme == nil { + theme = ThemeCharm() + } + if i.focused { + return &theme.Focused + } + return &theme.Blurred +} + +// View renders the input field. +func (i *Input) View() string { + styles := i.activeStyles() + + // NB: since the method is on a pointer receiver these are being mutated. + // Because this runs on every render this shouldn't matter in practice, + // however. + i.textinput.PlaceholderStyle = styles.TextInput.Placeholder + i.textinput.PromptStyle = styles.TextInput.Prompt + i.textinput.Cursor.Style = styles.TextInput.Cursor + i.textinput.TextStyle = styles.TextInput.Text + + var sb strings.Builder + if i.title != "" { + sb.WriteString(styles.Title.Render(i.title)) + if !i.inline { + sb.WriteString("\n") + } + } + if i.description != "" { + sb.WriteString(styles.Description.Render(i.description)) + if !i.inline { + sb.WriteString("\n") + } + } + sb.WriteString(i.textinput.View()) + + return styles.Base.Render(sb.String()) +} + +// Run runs the input field in accessible mode. +func (i *Input) Run() error { + if i.accessible { + return i.runAccessible() + } + return i.run() +} + +// run runs the input field. +func (i *Input) run() error { + return Run(i) +} + +// runAccessible runs the input field in accessible mode. +func (i *Input) runAccessible() error { + styles := i.activeStyles() + fmt.Println(styles.Title.Render(i.title)) + fmt.Println() + *i.value = accessibility.PromptString("Input: ", i.validate) + fmt.Println(styles.SelectedOption.Render("Input: " + *i.value + "\n")) + return nil +} + +// WithKeyMap sets the keymap on an input field. +func (i *Input) WithKeyMap(k *KeyMap) Field { + i.keymap = k.Input + i.textinput.KeyMap.AcceptSuggestion = i.keymap.AcceptSuggestion + return i +} + +// WithAccessible sets the accessible mode of the input field. +func (i *Input) WithAccessible(accessible bool) Field { + i.accessible = accessible + return i +} + +// WithTheme sets the theme of the input field. +func (i *Input) WithTheme(theme *Theme) Field { + if i.theme != nil { + return i + } + i.theme = theme + return i +} + +// WithWidth sets the width of the input field. +func (i *Input) WithWidth(width int) Field { + styles := i.activeStyles() + i.width = width + frameSize := styles.Base.GetHorizontalFrameSize() + promptWidth := lipgloss.Width(i.textinput.PromptStyle.Render(i.textinput.Prompt)) + titleWidth := lipgloss.Width(styles.Title.Render(i.title)) + descriptionWidth := lipgloss.Width(styles.Description.Render(i.description)) + i.textinput.Width = width - frameSize - promptWidth - 1 + if i.inline { + i.textinput.Width -= titleWidth + i.textinput.Width -= descriptionWidth + } + return i +} + +// WithHeight sets the height of the input field. +func (i *Input) WithHeight(height int) Field { + i.height = height + return i +} + +// WithPosition sets the position of the input field. +func (i *Input) WithPosition(p FieldPosition) Field { + i.keymap.Prev.SetEnabled(!p.IsFirst()) + i.keymap.Next.SetEnabled(!p.IsLast()) + i.keymap.Submit.SetEnabled(p.IsLast()) + return i +} + +// GetKey returns the key of the field. +func (i *Input) GetKey() string { + return i.key +} + +// GetValue returns the value of the field. +func (i *Input) GetValue() any { + return *i.value +} diff --git a/vendor/github.com/charmbracelet/huh/field_multiselect.go b/vendor/github.com/charmbracelet/huh/field_multiselect.go new file mode 100644 index 00000000..a9fb70a5 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/field_multiselect.go @@ -0,0 +1,565 @@ +package huh + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh/accessibility" + "github.com/charmbracelet/lipgloss" +) + +// MultiSelect is a form multi-select field. +type MultiSelect[T comparable] struct { + value *[]T + key string + + // customization + title string + description string + options []Option[T] + filterable bool + filteredOptions []Option[T] + limit int + height int + + // error handling + validate func([]T) error + err error + + // state + cursor int + focused bool + filtering bool + filter textinput.Model + viewport viewport.Model + + // options + width int + accessible bool + theme *Theme + keymap MultiSelectKeyMap +} + +// NewMultiSelect returns a new multi-select field. +func NewMultiSelect[T comparable]() *MultiSelect[T] { + filter := textinput.New() + filter.Prompt = "/" + + return &MultiSelect[T]{ + options: []Option[T]{}, + value: new([]T), + validate: func([]T) error { return nil }, + filtering: false, + filter: filter, + } +} + +// Value sets the value of the multi-select field. +func (m *MultiSelect[T]) Value(value *[]T) *MultiSelect[T] { + m.value = value + for i, o := range m.options { + for _, v := range *value { + if o.Value == v { + m.options[i].selected = true + break + } + } + } + return m +} + +// Key sets the key of the select field which can be used to retrieve the value +// after submission. +func (m *MultiSelect[T]) Key(key string) *MultiSelect[T] { + m.key = key + return m +} + +// Title sets the title of the multi-select field. +func (m *MultiSelect[T]) Title(title string) *MultiSelect[T] { + m.title = title + return m +} + +// Description sets the description of the multi-select field. +func (m *MultiSelect[T]) Description(description string) *MultiSelect[T] { + m.description = description + return m +} + +// Options sets the options of the multi-select field. +func (m *MultiSelect[T]) Options(options ...Option[T]) *MultiSelect[T] { + if len(options) <= 0 { + return m + } + + for i, o := range options { + for _, v := range *m.value { + if o.Value == v { + options[i].selected = true + break + } + } + } + m.options = options + m.filteredOptions = options + m.updateViewportHeight() + return m +} + +// Filterable sets the multi-select field as filterable. +func (m *MultiSelect[T]) Filterable(filterable bool) *MultiSelect[T] { + m.filterable = filterable + return m +} + +// Limit sets the limit of the multi-select field. +func (m *MultiSelect[T]) Limit(limit int) *MultiSelect[T] { + m.limit = limit + return m +} + +// Height sets the height of the multi-select field. +func (m *MultiSelect[T]) Height(height int) *MultiSelect[T] { + // What we really want to do is set the height of the viewport, but we + // need a theme applied before we can calcualate its height. + m.height = height + m.updateViewportHeight() + return m +} + +// Validate sets the validation function of the multi-select field. +func (m *MultiSelect[T]) Validate(validate func([]T) error) *MultiSelect[T] { + m.validate = validate + return m +} + +// Error returns the error of the multi-select field. +func (m *MultiSelect[T]) Error() error { + return m.err +} + +// Skip returns whether the multiselect should be skipped or should be blocking. +func (*MultiSelect[T]) Skip() bool { + return false +} + +// Zoom returns whether the multiselect should be zoomed. +func (*MultiSelect[T]) Zoom() bool { + return false +} + +// Focus focuses the multi-select field. +func (m *MultiSelect[T]) Focus() tea.Cmd { + m.focused = true + return nil +} + +// Blur blurs the multi-select field. +func (m *MultiSelect[T]) Blur() tea.Cmd { + m.focused = false + return nil +} + +// KeyBinds returns the help message for the multi-select field. +func (m *MultiSelect[T]) KeyBinds() []key.Binding { + return []key.Binding{ + m.keymap.Toggle, + m.keymap.Up, + m.keymap.Down, + m.keymap.Filter, + m.keymap.SetFilter, + m.keymap.ClearFilter, + m.keymap.Prev, + m.keymap.Submit, + m.keymap.Next, + } +} + +// Init initializes the multi-select field. +func (m *MultiSelect[T]) Init() tea.Cmd { + return nil +} + +// Update updates the multi-select field. +func (m *MultiSelect[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // Enforce height on the viewport during update as we need themes to + // be applied before we can calculate the height. + m.updateViewportHeight() + + var cmd tea.Cmd + if m.filtering { + m.filter, cmd = m.filter.Update(msg) + } + + switch msg := msg.(type) { + case tea.KeyMsg: + + m.err = nil + + switch { + case key.Matches(msg, m.keymap.Filter): + m.setFilter(true) + return m, m.filter.Focus() + case key.Matches(msg, m.keymap.SetFilter): + if len(m.filteredOptions) <= 0 { + m.filter.SetValue("") + m.filteredOptions = m.options + } + m.setFilter(false) + case key.Matches(msg, m.keymap.ClearFilter): + m.filter.SetValue("") + m.filteredOptions = m.options + m.setFilter(false) + case key.Matches(msg, m.keymap.Up): + if m.filtering && msg.String() == "k" { + break + } + + m.cursor = max(m.cursor-1, 0) + if m.cursor < m.viewport.YOffset { + m.viewport.SetYOffset(m.cursor) + } + case key.Matches(msg, m.keymap.Down): + if m.filtering && msg.String() == "j" { + break + } + + m.cursor = min(m.cursor+1, len(m.filteredOptions)-1) + if m.cursor >= m.viewport.YOffset+m.viewport.Height { + m.viewport.LineDown(1) + } + case key.Matches(msg, m.keymap.GotoTop): + if m.filtering { + break + } + m.cursor = 0 + m.viewport.GotoTop() + case key.Matches(msg, m.keymap.GotoBottom): + if m.filtering { + break + } + m.cursor = len(m.filteredOptions) - 1 + m.viewport.GotoBottom() + case key.Matches(msg, m.keymap.HalfPageUp): + m.cursor = max(m.cursor-m.viewport.Height/2, 0) + m.viewport.HalfViewUp() + case key.Matches(msg, m.keymap.HalfPageDown): + m.cursor = min(m.cursor+m.viewport.Height/2, len(m.filteredOptions)-1) + m.viewport.HalfViewDown() + case key.Matches(msg, m.keymap.Toggle): + for i, option := range m.options { + if option.Key == m.filteredOptions[m.cursor].Key { + if !m.options[m.cursor].selected && m.limit > 0 && m.numSelected() >= m.limit { + break + } + selected := m.options[i].selected + m.options[i].selected = !selected + m.filteredOptions[m.cursor].selected = !selected + } + } + case key.Matches(msg, m.keymap.Prev): + m.finalize() + if m.err != nil { + return m, nil + } + return m, PrevField + case key.Matches(msg, m.keymap.Next, m.keymap.Submit): + m.finalize() + if m.err != nil { + return m, nil + } + return m, NextField + } + + if m.filtering { + m.filteredOptions = m.options + if m.filter.Value() != "" { + m.filteredOptions = nil + for _, option := range m.options { + if m.filterFunc(option.Key) { + m.filteredOptions = append(m.filteredOptions, option) + } + } + } + if len(m.filteredOptions) > 0 { + m.cursor = min(m.cursor, len(m.filteredOptions)-1) + m.viewport.SetYOffset(clamp(m.cursor, 0, len(m.filteredOptions)-m.viewport.Height)) + } + } + } + + return m, cmd +} + +// updateViewportHeight updates the viewport size according to the Height setting +// on this multi-select field. +func (m *MultiSelect[T]) updateViewportHeight() { + // If no height is set size the viewport to the number of options. + if m.height <= 0 { + m.viewport.Height = len(m.options) + return + } + + const minHeight = 1 + m.viewport.Height = max(minHeight, m.height- + lipgloss.Height(m.titleView())- + lipgloss.Height(m.descriptionView())) +} + +func (m *MultiSelect[T]) numSelected() int { + var count int + for _, o := range m.options { + if o.selected { + count++ + } + } + return count +} + +func (m *MultiSelect[T]) finalize() { + *m.value = make([]T, 0) + for _, option := range m.options { + if option.selected { + *m.value = append(*m.value, option.Value) + } + } + m.err = m.validate(*m.value) +} + +func (m *MultiSelect[T]) activeStyles() *FieldStyles { + theme := m.theme + if theme == nil { + theme = ThemeCharm() + } + if m.focused { + return &theme.Focused + } + return &theme.Blurred +} + +func (m *MultiSelect[T]) titleView() string { + if m.title == "" { + return "" + } + var ( + styles = m.activeStyles() + sb = strings.Builder{} + ) + if m.filtering { + sb.WriteString(m.filter.View()) + } else if m.filter.Value() != "" { + sb.WriteString(styles.Title.Render(m.title) + styles.Description.Render("/"+m.filter.Value())) + } else { + sb.WriteString(styles.Title.Render(m.title)) + } + if m.err != nil { + sb.WriteString(styles.ErrorIndicator.String()) + } + return sb.String() +} + +func (m *MultiSelect[T]) descriptionView() string { + return m.activeStyles().Description.Render(m.description) +} + +func (m *MultiSelect[T]) choicesView() string { + var ( + styles = m.activeStyles() + c = styles.MultiSelectSelector.String() + sb strings.Builder + ) + for i, option := range m.filteredOptions { + if m.cursor == i { + sb.WriteString(c) + } else { + sb.WriteString(strings.Repeat(" ", lipgloss.Width(c))) + } + + if m.filteredOptions[i].selected { + sb.WriteString(styles.SelectedPrefix.String()) + sb.WriteString(styles.SelectedOption.Render(option.Key)) + } else { + sb.WriteString(styles.UnselectedPrefix.String()) + sb.WriteString(styles.UnselectedOption.Render(option.Key)) + } + if i < len(m.options)-1 { + sb.WriteString("\n") + } + } + + for i := len(m.filteredOptions); i < len(m.options)-1; i++ { + sb.WriteString("\n") + } + + return sb.String() +} + +// View renders the multi-select field. +func (m *MultiSelect[T]) View() string { + styles := m.activeStyles() + m.viewport.SetContent(m.choicesView()) + + var sb strings.Builder + if m.title != "" { + sb.WriteString(m.titleView()) + sb.WriteString("\n") + } + if m.description != "" { + sb.WriteString(m.descriptionView() + "\n") + } + sb.WriteString(m.viewport.View()) + return styles.Base.Render(sb.String()) +} + +func (m *MultiSelect[T]) printOptions() { + styles := m.activeStyles() + var sb strings.Builder + + sb.WriteString(styles.Title.Render(m.title)) + sb.WriteString("\n") + + for i, option := range m.options { + if option.selected { + sb.WriteString(styles.SelectedOption.Render(fmt.Sprintf("%d. %s %s", i+1, "✓", option.Key))) + } else { + sb.WriteString(fmt.Sprintf("%d. %s %s", i+1, " ", option.Key)) + } + sb.WriteString("\n") + } + + fmt.Println(sb.String()) +} + +// setFilter sets the filter of the select field. +func (m *MultiSelect[T]) setFilter(filter bool) { + m.filtering = filter + m.keymap.SetFilter.SetEnabled(filter) + m.keymap.Filter.SetEnabled(!filter) + m.keymap.Next.SetEnabled(!filter) + m.keymap.Submit.SetEnabled(!filter) + m.keymap.Prev.SetEnabled(!filter) + m.keymap.ClearFilter.SetEnabled(!filter && m.filter.Value() != "") +} + +// filterFunc returns true if the option matches the filter. +func (m *MultiSelect[T]) filterFunc(option string) bool { + // XXX: remove diacritics or allow customization of filter function. + return strings.Contains(strings.ToLower(option), strings.ToLower(m.filter.Value())) +} + +// Run runs the multi-select field. +func (m *MultiSelect[T]) Run() error { + if m.accessible { + return m.runAccessible() + } + return Run(m) +} + +// runAccessible() runs the multi-select field in accessible mode. +func (m *MultiSelect[T]) runAccessible() error { + m.printOptions() + styles := m.activeStyles() + + var choice int + for { + fmt.Printf("Select up to %d options. 0 to continue.\n", m.limit) + + choice = accessibility.PromptInt("Select: ", 0, len(m.options)) + if choice == 0 { + m.finalize() + err := m.validate(*m.value) + if err != nil { + fmt.Println(err) + continue + } + break + } + + if !m.options[choice-1].selected && m.limit > 0 && m.numSelected() >= m.limit { + fmt.Printf("You can't select more than %d options.\n", m.limit) + continue + } + m.options[choice-1].selected = !m.options[choice-1].selected + if m.options[choice-1].selected { + fmt.Printf("Selected: %s\n\n", m.options[choice-1].Key) + } else { + fmt.Printf("Deselected: %s\n\n", m.options[choice-1].Key) + } + + m.printOptions() + } + + var values []string + + for _, option := range m.options { + if option.selected { + *m.value = append(*m.value, option.Value) + values = append(values, option.Key) + } + } + + fmt.Println(styles.SelectedOption.Render("Selected:", strings.Join(values, ", ")+"\n")) + return nil +} + +// WithTheme sets the theme of the multi-select field. +func (m *MultiSelect[T]) WithTheme(theme *Theme) Field { + if m.theme != nil { + return m + } + m.theme = theme + m.filter.Cursor.Style = m.theme.Focused.TextInput.Cursor + m.filter.PromptStyle = m.theme.Focused.TextInput.Prompt + m.updateViewportHeight() + return m +} + +// WithKeyMap sets the keymap of the multi-select field. +func (m *MultiSelect[T]) WithKeyMap(k *KeyMap) Field { + m.keymap = k.MultiSelect + return m +} + +// WithAccessible sets the accessible mode of the multi-select field. +func (m *MultiSelect[T]) WithAccessible(accessible bool) Field { + m.accessible = accessible + return m +} + +// WithWidth sets the width of the multi-select field. +func (m *MultiSelect[T]) WithWidth(width int) Field { + m.width = width + return m +} + +// WithHeight sets the height of the multi-select field. +func (m *MultiSelect[T]) WithHeight(height int) Field { + m.height = height + return m +} + +// WithPosition sets the position of the multi-select field. +func (m *MultiSelect[T]) WithPosition(p FieldPosition) Field { + if m.filtering { + return m + } + m.keymap.Prev.SetEnabled(!p.IsFirst()) + m.keymap.Next.SetEnabled(!p.IsLast()) + m.keymap.Submit.SetEnabled(p.IsLast()) + return m +} + +// GetKey returns the multi-select's key. +func (m *MultiSelect[T]) GetKey() string { + return m.key +} + +// GetValue returns the multi-select's value. +func (m *MultiSelect[T]) GetValue() any { + return *m.value +} diff --git a/vendor/github.com/charmbracelet/huh/field_note.go b/vendor/github.com/charmbracelet/huh/field_note.go new file mode 100644 index 00000000..d8ad5885 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/field_note.go @@ -0,0 +1,274 @@ +package huh + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" +) + +// Note is a form note field. +type Note struct { + // customization + title string + description string + nextLabel string + + // state + showNextButton bool + focused bool + + // options + skip bool + width int + height int + accessible bool + theme *Theme + keymap NoteKeyMap +} + +// NewNote creates a new note field. +func NewNote() *Note { + return &Note{ + showNextButton: false, + skip: true, + nextLabel: "Next", + } +} + +// Title sets the title of the note field. +func (n *Note) Title(title string) *Note { + n.title = title + return n +} + +// Description sets the description of the note field. +func (n *Note) Description(description string) *Note { + n.description = description + return n +} + +// Next sets whether to show the next button. +func (n *Note) Next(show bool) *Note { + n.showNextButton = show + return n +} + +// NextLabel sets the next button label. +func (n *Note) NextLabel(label string) *Note { + n.nextLabel = label + return n +} + +// Focus focuses the note field. +func (n *Note) Focus() tea.Cmd { + n.focused = true + return nil +} + +// Blur blurs the note field. +func (n *Note) Blur() tea.Cmd { + n.focused = false + return nil +} + +// Error returns the error of the note field. +func (n *Note) Error() error { + return nil +} + +// Skip returns whether the note should be skipped or should be blocking. +func (n *Note) Skip() bool { + return n.skip +} + +// Zoom returns whether the note should be zoomed. +func (n *Note) Zoom() bool { + return false +} + +// KeyBinds returns the help message for the note field. +func (n *Note) KeyBinds() []key.Binding { + return []key.Binding{n.keymap.Prev, n.keymap.Submit, n.keymap.Next} +} + +// Init initializes the note field. +func (n *Note) Init() tea.Cmd { + return nil +} + +// Update updates the note field. +func (n *Note) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + switch { + case key.Matches(msg, n.keymap.Prev): + return n, PrevField + case key.Matches(msg, n.keymap.Next, n.keymap.Submit): + return n, NextField + } + return n, NextField + } + return n, nil +} + +func (n *Note) activeStyles() *FieldStyles { + theme := n.theme + if theme == nil { + theme = ThemeCharm() + } + if n.focused { + return &theme.Focused + } + return &theme.Focused +} + +// View renders the note field. +func (n *Note) View() string { + var ( + styles = n.activeStyles() + sb strings.Builder + ) + + if n.title != "" { + sb.WriteString(styles.NoteTitle.Render(n.title)) + } + if n.description != "" { + sb.WriteString("\n") + sb.WriteString(render(n.description)) + } + if n.showNextButton { + sb.WriteString(styles.Next.Render(n.nextLabel)) + } + return styles.Card.Render(sb.String()) +} + +// Run runs the note field. +func (n *Note) Run() error { + if n.accessible { + return n.runAccessible() + } + return Run(n) +} + +// runAccessible runs an accessible note field. +func (n *Note) runAccessible() error { + var body string + + if n.title != "" { + body = n.title + "\n\n" + } + + body += n.description + + fmt.Println(body) + fmt.Println() + return nil +} + +// WithTheme sets the theme on a note field. +func (n *Note) WithTheme(theme *Theme) Field { + if n.theme != nil { + return n + } + n.theme = theme + return n +} + +// WithKeyMap sets the keymap on a note field. +func (n *Note) WithKeyMap(k *KeyMap) Field { + n.keymap = k.Note + return n +} + +// WithAccessible sets the accessible mode of the note field. +func (n *Note) WithAccessible(accessible bool) Field { + n.accessible = accessible + return n +} + +// WithWidth sets the width of the note field. +func (n *Note) WithWidth(width int) Field { + n.width = width + return n +} + +// WithHeight sets the height of the note field. +func (n *Note) WithHeight(height int) Field { + n.height = height + return n +} + +// WithPosition sets the position information of the note field. +func (n *Note) WithPosition(p FieldPosition) Field { + // if the note is the only field on the screen, + // we shouldn't skip the entire group. + if p.Field == p.FirstField && p.Field == p.LastField { + n.skip = false + } + n.keymap.Prev.SetEnabled(!p.IsFirst()) + n.keymap.Next.SetEnabled(!p.IsLast()) + n.keymap.Submit.SetEnabled(p.IsLast()) + return n +} + +// GetValue satisfies the Field interface, notes do not have values. +func (n *Note) GetValue() any { + return nil +} + +// GetKey satisfies the Field interface, notes do not have keys. +func (n *Note) GetKey() string { + return "" +} + +func render(input string) string { + var result strings.Builder + var italic, bold, codeblock bool + + for _, char := range input { + switch char { + case '_': + if !italic { + result.WriteString("\033[3m") + italic = true + } else { + result.WriteString("\033[23m") + italic = false + } + case '*': + if !bold { + result.WriteString("\033[1m") + bold = true + } else { + result.WriteString("\033[22m") + bold = false + } + case '`': + if !codeblock { + result.WriteString("\033[0;37;40m") + result.WriteString(" ") + codeblock = true + } else { + result.WriteString(" ") + result.WriteString("\033[0m") + codeblock = false + + if bold { + result.WriteString("\033[1m") + } + if italic { + result.WriteString("\033[3m") + } + } + default: + result.WriteRune(char) + } + } + + // Reset any open formatting + result.WriteString("\033[0m") + + return result.String() +} diff --git a/vendor/github.com/charmbracelet/huh/field_select.go b/vendor/github.com/charmbracelet/huh/field_select.go new file mode 100644 index 00000000..8e503b05 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/field_select.go @@ -0,0 +1,544 @@ +package huh + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh/accessibility" + "github.com/charmbracelet/lipgloss" +) + +// Select is a form select field. +type Select[T comparable] struct { + value *T + key string + viewport viewport.Model + + // customization + title string + description string + options []Option[T] + filteredOptions []Option[T] + height int + + // error handling + validate func(T) error + err error + + // state + selected int + focused bool + filtering bool + filter textinput.Model + + // options + inline bool + width int + accessible bool + theme *Theme + keymap SelectKeyMap +} + +// NewSelect returns a new select field. +func NewSelect[T comparable]() *Select[T] { + filter := textinput.New() + filter.Prompt = "/" + + return &Select[T]{ + options: []Option[T]{}, + value: new(T), + validate: func(T) error { return nil }, + filtering: false, + filter: filter, + } +} + +// Value sets the value of the select field. +func (s *Select[T]) Value(value *T) *Select[T] { + s.value = value + s.selectValue(*value) + return s +} + +func (s *Select[T]) selectValue(value T) { + for i, o := range s.options { + if o.Value == value { + s.selected = i + break + } + } +} + +// Key sets the key of the select field which can be used to retrieve the value +// after submission. +func (s *Select[T]) Key(key string) *Select[T] { + s.key = key + return s +} + +// Title sets the title of the select field. +func (s *Select[T]) Title(title string) *Select[T] { + s.title = title + return s +} + +// Description sets the description of the select field. +func (s *Select[T]) Description(description string) *Select[T] { + s.description = description + return s +} + +// Options sets the options of the select field. +func (s *Select[T]) Options(options ...Option[T]) *Select[T] { + if len(options) <= 0 { + return s + } + s.options = options + s.filteredOptions = options + + // Set the cursor to the existing value or the last selected option. + for i, option := range options { + if option.Value == *s.value { + s.selected = i + break + } else if option.selected { + s.selected = i + } + } + + s.updateViewportHeight() + + return s +} + +// Inline sets whether the select input should be inline. +func (s *Select[T]) Inline(v bool) *Select[T] { + s.inline = v + if v { + s.Height(1) + } + s.keymap.Left.SetEnabled(v) + s.keymap.Right.SetEnabled(v) + s.keymap.Up.SetEnabled(!v) + s.keymap.Down.SetEnabled(!v) + return s +} + +// Height sets the height of the select field. If the number of options +// exceeds the height, the select field will become scrollable. +func (s *Select[T]) Height(height int) *Select[T] { + s.height = height + s.updateViewportHeight() + return s +} + +// Validate sets the validation function of the select field. +func (s *Select[T]) Validate(validate func(T) error) *Select[T] { + s.validate = validate + return s +} + +// Error returns the error of the select field. +func (s *Select[T]) Error() error { + return s.err +} + +// Skip returns whether the select should be skipped or should be blocking. +func (*Select[T]) Skip() bool { + return false +} + +// Zoom returns whether the input should be zoomed. +func (*Select[T]) Zoom() bool { + return false +} + +// Focus focuses the select field. +func (s *Select[T]) Focus() tea.Cmd { + s.focused = true + return nil +} + +// Blur blurs the select field. +func (s *Select[T]) Blur() tea.Cmd { + value := *s.value + if s.inline { + s.clearFilter() + s.selectValue(value) + } + s.focused = false + s.err = s.validate(value) + return nil +} + +// KeyBinds returns the help keybindings for the select field. +func (s *Select[T]) KeyBinds() []key.Binding { + return []key.Binding{ + s.keymap.Up, + s.keymap.Down, + s.keymap.Left, + s.keymap.Right, + s.keymap.Filter, + s.keymap.SetFilter, + s.keymap.ClearFilter, + s.keymap.Prev, + s.keymap.Next, + s.keymap.Submit, + } +} + +// Init initializes the select field. +func (s *Select[T]) Init() tea.Cmd { + return nil +} + +// Update updates the select field. +func (s *Select[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + s.updateViewportHeight() + + var cmd tea.Cmd + if s.filtering { + s.filter, cmd = s.filter.Update(msg) + + // Keep the selected item in view. + if s.selected < s.viewport.YOffset || s.selected >= s.viewport.YOffset+s.viewport.Height { + s.viewport.SetYOffset(s.selected) + } + } + + switch msg := msg.(type) { + case tea.KeyMsg: + s.err = nil + switch { + case key.Matches(msg, s.keymap.Filter): + s.setFiltering(true) + return s, s.filter.Focus() + case key.Matches(msg, s.keymap.SetFilter): + if len(s.filteredOptions) <= 0 { + s.filter.SetValue("") + s.filteredOptions = s.options + } + s.setFiltering(false) + case key.Matches(msg, s.keymap.ClearFilter): + s.clearFilter() + case key.Matches(msg, s.keymap.Up, s.keymap.Left): + // When filtering we should ignore j/k keybindings + // + // XXX: Currently, the below check doesn't account for keymap + // changes. When making this fix it's worth considering ignoring + // whether to ignore all up/down keybindings as ignoring a-zA-Z0-9 + // may not be enough when international keyboards are considered. + if s.filtering && (msg.String() == "k" || msg.String() == "h") { + break + } + s.selected = max(s.selected-1, 0) + if s.selected < s.viewport.YOffset { + s.viewport.SetYOffset(s.selected) + } + case key.Matches(msg, s.keymap.GotoTop): + if s.filtering { + break + } + s.selected = 0 + s.viewport.GotoTop() + case key.Matches(msg, s.keymap.GotoBottom): + if s.filtering { + break + } + s.selected = len(s.filteredOptions) - 1 + s.viewport.GotoBottom() + case key.Matches(msg, s.keymap.HalfPageUp): + s.selected = max(s.selected-s.viewport.Height/2, 0) + s.viewport.HalfViewUp() + case key.Matches(msg, s.keymap.HalfPageDown): + s.selected = min(s.selected+s.viewport.Height/2, len(s.filteredOptions)-1) + s.viewport.HalfViewDown() + case key.Matches(msg, s.keymap.Down, s.keymap.Right): + // When filtering we should ignore j/k keybindings + // + // XXX: See note in the previous case match. + if s.filtering && (msg.String() == "j" || msg.String() == "l") { + break + } + s.selected = min(s.selected+1, len(s.filteredOptions)-1) + if s.selected >= s.viewport.YOffset+s.viewport.Height { + s.viewport.LineDown(1) + } + case key.Matches(msg, s.keymap.Prev): + if s.selected >= len(s.filteredOptions) { + break + } + value := s.filteredOptions[s.selected].Value + s.err = s.validate(value) + if s.err != nil { + return s, nil + } + *s.value = value + return s, PrevField + case key.Matches(msg, s.keymap.Next, s.keymap.Submit): + if s.selected >= len(s.filteredOptions) { + break + } + value := s.filteredOptions[s.selected].Value + s.setFiltering(false) + s.err = s.validate(value) + if s.err != nil { + return s, nil + } + *s.value = value + return s, NextField + } + + if s.filtering { + s.filteredOptions = s.options + if s.filter.Value() != "" { + s.filteredOptions = nil + for _, option := range s.options { + if s.filterFunc(option.Key) { + s.filteredOptions = append(s.filteredOptions, option) + } + } + } + if len(s.filteredOptions) > 0 { + s.selected = min(s.selected, len(s.filteredOptions)-1) + s.viewport.SetYOffset(clamp(s.selected, 0, len(s.filteredOptions)-s.viewport.Height)) + } + } + } + + return s, cmd +} + +// updateViewportHeight updates the viewport size according to the Height setting +// on this select field. +func (s *Select[T]) updateViewportHeight() { + // If no height is set size the viewport to the number of options. + if s.height <= 0 { + s.viewport.Height = len(s.options) + return + } + + const minHeight = 1 + s.viewport.Height = max(minHeight, s.height- + lipgloss.Height(s.titleView())- + lipgloss.Height(s.descriptionView())) +} + +func (s *Select[T]) activeStyles() *FieldStyles { + theme := s.theme + if theme == nil { + theme = ThemeCharm() + } + if s.focused { + return &theme.Focused + } + return &theme.Blurred +} + +func (s *Select[T]) titleView() string { + if s.title == "" { + return "" + } + var ( + styles = s.activeStyles() + sb = strings.Builder{} + ) + if s.filtering { + sb.WriteString(styles.Title.Render(s.filter.View())) + } else if s.filter.Value() != "" && !s.inline { + sb.WriteString(styles.Title.Render(s.title) + styles.Description.Render("/"+s.filter.Value())) + } else { + sb.WriteString(styles.Title.Render(s.title)) + } + if s.err != nil { + sb.WriteString(styles.ErrorIndicator.String()) + } + return sb.String() +} + +func (s *Select[T]) descriptionView() string { + return s.activeStyles().Description.Render(s.description) +} + +func (s *Select[T]) choicesView() string { + var ( + styles = s.activeStyles() + c = styles.SelectSelector.String() + sb strings.Builder + ) + + if s.inline { + sb.WriteString(styles.PrevIndicator.Faint(s.selected <= 0).String()) + if len(s.filteredOptions) > 0 { + sb.WriteString(styles.SelectedOption.Render(s.filteredOptions[s.selected].Key)) + } else { + sb.WriteString(styles.TextInput.Placeholder.Render("No matches")) + } + sb.WriteString(styles.NextIndicator.Faint(s.selected == len(s.filteredOptions)-1).String()) + return sb.String() + } + + for i, option := range s.filteredOptions { + if s.selected == i { + sb.WriteString(c + styles.SelectedOption.Render(option.Key)) + } else { + sb.WriteString(strings.Repeat(" ", lipgloss.Width(c)) + styles.Option.Render(option.Key)) + } + if i < len(s.options)-1 { + sb.WriteString("\n") + } + } + + for i := len(s.filteredOptions); i < len(s.options)-1; i++ { + sb.WriteString("\n") + } + + return sb.String() +} + +// View renders the select field. +func (s *Select[T]) View() string { + styles := s.activeStyles() + s.viewport.SetContent(s.choicesView()) + + var sb strings.Builder + if s.title != "" { + sb.WriteString(s.titleView()) + if !s.inline { + sb.WriteString("\n") + } + } + if s.description != "" { + sb.WriteString(s.descriptionView()) + if !s.inline { + sb.WriteString("\n") + } + } + sb.WriteString(s.viewport.View()) + return styles.Base.Render(sb.String()) +} + +// clearFilter clears the value of the filter. +func (s *Select[T]) clearFilter() { + s.filter.SetValue("") + s.filteredOptions = s.options + s.setFiltering(false) +} + +// setFiltering sets the filter of the select field. +func (s *Select[T]) setFiltering(filtering bool) { + if s.inline && filtering { + s.filter.Width = lipgloss.Width(s.titleView()) - 1 - 1 + } + s.filtering = filtering + s.keymap.SetFilter.SetEnabled(filtering) + s.keymap.Filter.SetEnabled(!filtering) + s.keymap.ClearFilter.SetEnabled(!filtering && s.filter.Value() != "") +} + +// filterFunc returns true if the option matches the filter. +func (s *Select[T]) filterFunc(option string) bool { + // XXX: remove diacritics or allow customization of filter function. + return strings.Contains(strings.ToLower(option), strings.ToLower(s.filter.Value())) +} + +// Run runs the select field. +func (s *Select[T]) Run() error { + if s.accessible { + return s.runAccessible() + } + return Run(s) +} + +// runAccessible runs an accessible select field. +func (s *Select[T]) runAccessible() error { + var sb strings.Builder + styles := s.activeStyles() + + sb.WriteString(styles.Title.Render(s.title) + "\n") + + for i, option := range s.options { + sb.WriteString(fmt.Sprintf("%d. %s", i+1, option.Key)) + sb.WriteString("\n") + } + + fmt.Println(sb.String()) + + for { + choice := accessibility.PromptInt("Choose: ", 1, len(s.options)) + option := s.options[choice-1] + if err := s.validate(option.Value); err != nil { + fmt.Println(err.Error()) + continue + } + fmt.Println(styles.SelectedOption.Render("Chose: " + option.Key + "\n")) + *s.value = option.Value + break + } + + return nil +} + +// WithTheme sets the theme of the select field. +func (s *Select[T]) WithTheme(theme *Theme) Field { + if s.theme != nil { + return s + } + s.theme = theme + s.filter.Cursor.Style = s.theme.Focused.TextInput.Cursor + s.filter.PromptStyle = s.theme.Focused.TextInput.Prompt + s.updateViewportHeight() + return s +} + +// WithKeyMap sets the keymap on a select field. +func (s *Select[T]) WithKeyMap(k *KeyMap) Field { + s.keymap = k.Select + s.keymap.Left.SetEnabled(s.inline) + s.keymap.Right.SetEnabled(s.inline) + s.keymap.Up.SetEnabled(!s.inline) + s.keymap.Down.SetEnabled(!s.inline) + return s +} + +// WithAccessible sets the accessible mode of the select field. +func (s *Select[T]) WithAccessible(accessible bool) Field { + s.accessible = accessible + return s +} + +// WithWidth sets the width of the select field. +func (s *Select[T]) WithWidth(width int) Field { + s.width = width + return s +} + +// WithHeight sets the height of the select field. +func (s *Select[T]) WithHeight(height int) Field { + return s.Height(height) +} + +// WithPosition sets the position of the select field. +func (s *Select[T]) WithPosition(p FieldPosition) Field { + if s.filtering { + return s + } + s.keymap.Prev.SetEnabled(!p.IsFirst()) + s.keymap.Next.SetEnabled(!p.IsLast()) + s.keymap.Submit.SetEnabled(p.IsLast()) + return s +} + +// GetKey returns the key of the field. +func (s *Select[T]) GetKey() string { + return s.key +} + +// GetValue returns the value of the field. +func (s *Select[T]) GetValue() any { + return *s.value +} diff --git a/vendor/github.com/charmbracelet/huh/field_text.go b/vendor/github.com/charmbracelet/huh/field_text.go new file mode 100644 index 00000000..51e22b2b --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/field_text.go @@ -0,0 +1,383 @@ +package huh + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/textarea" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/huh/accessibility" + "github.com/charmbracelet/lipgloss" +) + +// Text is a form text field. It allows for a multi-line string input. +type Text struct { + value *string + key string + + // error handling + validate func(string) error + err error + + // model + textarea textarea.Model + + // customization + title string + description string + editorCmd string + editorArgs []string + editorExtension string + + // state + focused bool + + // form options + width int + accessible bool + theme *Theme + keymap TextKeyMap +} + +// NewText returns a new text field. +func NewText() *Text { + text := textarea.New() + text.ShowLineNumbers = false + text.Prompt = "" + text.FocusedStyle.CursorLine = lipgloss.NewStyle() + + editorCmd, editorArgs := getEditor() + + t := &Text{ + value: new(string), + textarea: text, + validate: func(string) error { return nil }, + editorCmd: editorCmd, + editorArgs: editorArgs, + editorExtension: "md", + } + + return t +} + +// Value sets the value of the text field. +func (t *Text) Value(value *string) *Text { + t.value = value + t.textarea.SetValue(*value) + return t +} + +// Key sets the key of the text field. +func (t *Text) Key(key string) *Text { + t.key = key + return t +} + +// Title sets the title of the text field. +func (t *Text) Title(title string) *Text { + t.title = title + return t +} + +// Lines sets the number of lines to show of the text field. +func (t *Text) Lines(lines int) *Text { + t.textarea.SetHeight(lines) + return t +} + +// Description sets the description of the text field. +func (t *Text) Description(description string) *Text { + t.description = description + return t +} + +// CharLimit sets the character limit of the text field. +func (t *Text) CharLimit(charlimit int) *Text { + t.textarea.CharLimit = charlimit + return t +} + +// ShowLineNumbers sets whether or not to show line numbers. +func (t *Text) ShowLineNumbers(show bool) *Text { + t.textarea.ShowLineNumbers = show + return t +} + +// Placeholder sets the placeholder of the text field. +func (t *Text) Placeholder(str string) *Text { + t.textarea.Placeholder = str + return t +} + +// Validate sets the validation function of the text field. +func (t *Text) Validate(validate func(string) error) *Text { + t.validate = validate + return t +} + +const defaultEditor = "nano" + +// getEditor returns the editor command and arguments. +func getEditor() (string, []string) { + editor := strings.Fields(os.Getenv("EDITOR")) + if len(editor) > 0 { + return editor[0], editor[1:] + } + return defaultEditor, nil +} + +// Editor specifies which editor to use. +// +// The first argument provided is used as the editor command (vim, nvim, nano, etc...) +// The following (optional) arguments provided are passed as arguments to the editor command. +func (t *Text) Editor(editor ...string) *Text { + if len(editor) > 0 { + t.editorCmd = editor[0] + } + if len(editor) > 1 { + t.editorArgs = editor[1:] + } + return t +} + +// EditorExtension specifies arguments to pass into the editor. +func (t *Text) EditorExtension(extension string) *Text { + t.editorExtension = extension + return t +} + +// Error returns the error of the text field. +func (t *Text) Error() error { + return t.err +} + +// Skip returns whether the textarea should be skipped or should be blocking. +func (*Text) Skip() bool { + return false +} + +// Zoom returns whether the note should be zoomed. +func (*Text) Zoom() bool { + return false +} + +// Focus focuses the text field. +func (t *Text) Focus() tea.Cmd { + t.focused = true + return t.textarea.Focus() +} + +// Blur blurs the text field. +func (t *Text) Blur() tea.Cmd { + t.focused = false + *t.value = t.textarea.Value() + t.textarea.Blur() + t.err = t.validate(*t.value) + return nil +} + +// KeyBinds returns the help message for the text field. +func (t *Text) KeyBinds() []key.Binding { + return []key.Binding{t.keymap.NewLine, t.keymap.Editor, t.keymap.Prev, t.keymap.Submit, t.keymap.Next} +} + +type updateValueMsg []byte + +// Init initializes the text field. +func (t *Text) Init() tea.Cmd { + t.textarea.Blur() + return nil +} + +// Update updates the text field. +func (t *Text) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + var cmd tea.Cmd + + t.textarea, cmd = t.textarea.Update(msg) + cmds = append(cmds, cmd) + *t.value = t.textarea.Value() + + switch msg := msg.(type) { + case updateValueMsg: + t.textarea.SetValue(string(msg)) + t.textarea, cmd = t.textarea.Update(msg) + cmds = append(cmds, cmd) + *t.value = t.textarea.Value() + + case tea.KeyMsg: + t.err = nil + + switch { + case key.Matches(msg, t.keymap.Editor): + ext := strings.TrimPrefix(t.editorExtension, ".") + tmpFile, _ := os.CreateTemp(os.TempDir(), "*."+ext) + cmd := exec.Command(t.editorCmd, append(t.editorArgs, tmpFile.Name())...) + _ = os.WriteFile(tmpFile.Name(), []byte(t.textarea.Value()), 0600) + cmds = append(cmds, tea.ExecProcess(cmd, func(error) tea.Msg { + content, _ := os.ReadFile(tmpFile.Name()) + _ = os.Remove(tmpFile.Name()) + return updateValueMsg(content) + })) + case key.Matches(msg, t.keymap.Next, t.keymap.Submit): + value := t.textarea.Value() + t.err = t.validate(value) + if t.err != nil { + return t, nil + } + cmds = append(cmds, NextField) + case key.Matches(msg, t.keymap.Prev): + value := t.textarea.Value() + t.err = t.validate(value) + if t.err != nil { + return t, nil + } + cmds = append(cmds, PrevField) + } + } + + return t, tea.Batch(cmds...) +} + +func (t *Text) activeStyles() *FieldStyles { + theme := t.theme + if theme == nil { + theme = ThemeCharm() + } + if t.focused { + return &theme.Focused + } + return &theme.Blurred +} + +func (t *Text) activeTextAreaStyles() *textarea.Style { + if t.theme == nil { + return &t.textarea.BlurredStyle + } + if t.focused { + return &t.textarea.FocusedStyle + } + return &t.textarea.BlurredStyle +} + +// View renders the text field. +func (t *Text) View() string { + var styles = t.activeStyles() + var textareaStyles = t.activeTextAreaStyles() + + // NB: since the method is on a pointer receiver these are being mutated. + // Because this runs on every render this shouldn't matter in practice, + // however. + textareaStyles.Placeholder = styles.TextInput.Placeholder + textareaStyles.Text = styles.TextInput.Text + textareaStyles.Prompt = styles.TextInput.Prompt + textareaStyles.CursorLine = styles.TextInput.Text + t.textarea.Cursor.Style = styles.TextInput.Cursor + + var sb strings.Builder + if t.title != "" { + sb.WriteString(styles.Title.Render(t.title)) + if t.err != nil { + sb.WriteString(styles.ErrorIndicator.String()) + } + sb.WriteString("\n") + } + if t.description != "" { + sb.WriteString(styles.Description.Render(t.description)) + sb.WriteString("\n") + } + sb.WriteString(t.textarea.View()) + + return styles.Base.Render(sb.String()) +} + +// Run runs the text field. +func (t *Text) Run() error { + if t.accessible { + return t.runAccessible() + } + return Run(t) +} + +// runAccessible runs an accessible text field. +func (t *Text) runAccessible() error { + styles := t.activeStyles() + fmt.Println(styles.Title.Render(t.title)) + fmt.Println() + *t.value = accessibility.PromptString("Input: ", func(input string) error { + if err := t.validate(input); err != nil { + // Handle the error from t.validate, return it + return err + } + + if len(input) > t.textarea.CharLimit { + return fmt.Errorf("Input cannot exceed %d characters", t.textarea.CharLimit) + } + return nil + }) + fmt.Println() + return nil +} + +// WithTheme sets the theme on a text field. +func (t *Text) WithTheme(theme *Theme) Field { + if t.theme != nil { + return t + } + t.theme = theme + return t +} + +// WithKeyMap sets the keymap on a text field. +func (t *Text) WithKeyMap(k *KeyMap) Field { + t.keymap = k.Text + t.textarea.KeyMap.InsertNewline.SetKeys(t.keymap.NewLine.Keys()...) + return t +} + +// WithAccessible sets the accessible mode of the text field. +func (t *Text) WithAccessible(accessible bool) Field { + t.accessible = accessible + return t +} + +// WithWidth sets the width of the text field. +func (t *Text) WithWidth(width int) Field { + t.width = width + t.textarea.SetWidth(width - t.activeStyles().Base.GetHorizontalFrameSize()) + return t +} + +// WithHeight sets the height of the text field. +func (t *Text) WithHeight(height int) Field { + adjust := 0 + if t.title != "" { + adjust++ + } + if t.description != "" { + adjust++ + } + t.textarea.SetHeight(height - t.activeStyles().Base.GetVerticalFrameSize() - adjust) + return t +} + +// WithPosition sets the position information of the text field. +func (t *Text) WithPosition(p FieldPosition) Field { + t.keymap.Prev.SetEnabled(!p.IsFirst()) + t.keymap.Next.SetEnabled(!p.IsLast()) + t.keymap.Submit.SetEnabled(p.IsLast()) + return t +} + +// GetKey returns the key of the field. +func (t *Text) GetKey() string { + return t.key +} + +// GetValue returns the value of the field. +func (t *Text) GetValue() any { + return *t.value +} diff --git a/vendor/github.com/charmbracelet/huh/form.go b/vendor/github.com/charmbracelet/huh/form.go new file mode 100644 index 00000000..322afb35 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/form.go @@ -0,0 +1,589 @@ +package huh + +import ( + "errors" + "io" + "os" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/paginator" + tea "github.com/charmbracelet/bubbletea" +) + +const defaultWidth = 80 + +// FormState represents the current state of the form. +type FormState int + +const ( + // StateNormal is when the user is completing the form. + StateNormal FormState = iota + + // StateCompleted is when the user has completed the form. + StateCompleted + + // StateAborted is when the user has aborted the form. + StateAborted +) + +// ErrUserAborted is the error returned when a user exits the form before submitting. +var ErrUserAborted = errors.New("user aborted") + +// Form is a collection of groups that are displayed one at a time on a "page". +// +// The form can navigate between groups and is complete once all the groups are +// complete. +type Form struct { + // collection of groups + groups []*Group + + results map[string]any + + // navigation + paginator paginator.Model + + // callbacks + submitCmd tea.Cmd + cancelCmd tea.Cmd + + State FormState + + // whether or not to use bubble tea rendering for accessibility + // purposes, if true, the form will render with basic prompting primitives + // to be more accessible to screen readers. + accessible bool + + quitting bool + aborted bool + + // options + width int + height int + keymap *KeyMap + teaOptions []tea.ProgramOption + output io.Writer +} + +// NewForm returns a form with the given groups and default themes and +// keybindings. +// +// Use With* methods to customize the form with options, such as setting +// different themes and keybindings. +func NewForm(groups ...*Group) *Form { + p := paginator.New() + p.SetTotalPages(len(groups)) + + f := &Form{ + groups: groups, + paginator: p, + keymap: NewDefaultKeyMap(), + results: make(map[string]any), + teaOptions: []tea.ProgramOption{ + tea.WithOutput(os.Stderr), + }, + } + + // NB: If dynamic forms come into play this will need to be applied when + // groups and fields are added. + f.WithKeyMap(f.keymap) + f.WithWidth(f.width) + f.WithHeight(f.height) + f.UpdateFieldPositions() + + if os.Getenv("TERM") == "dumb" { + f.WithWidth(defaultWidth) + f.WithAccessible(true) + } + + return f +} + +// Field is a primitive of a form. +// +// A field represents a single input control on a form such as a text input, +// confirm button, select option, etc... +// +// Each field implements the Bubble Tea Model interface. +type Field interface { + // Bubble Tea Model + Init() tea.Cmd + Update(tea.Msg) (tea.Model, tea.Cmd) + View() string + + // Bubble Tea Events + Blur() tea.Cmd + Focus() tea.Cmd + + // Errors and Validation + Error() error + + // Run runs the field individually. + Run() error + + // Skip returns whether this input should be skipped or not. + Skip() bool + + // Zoom returns whether this input should be zoomed or not. + // Zoom allows the field to take focus of the group / form height. + Zoom() bool + + // KeyBinds returns help keybindings. + KeyBinds() []key.Binding + + // WithTheme sets the theme on a field. + WithTheme(*Theme) Field + + // WithAccessible sets whether the field should run in accessible mode. + WithAccessible(bool) Field + + // WithKeyMap sets the keymap on a field. + WithKeyMap(*KeyMap) Field + + // WithWidth sets the width of a field. + WithWidth(int) Field + + // WithHeight sets the height of a field. + WithHeight(int) Field + + // WithPosition tells the field the index of the group and position it is in. + WithPosition(FieldPosition) Field + + // GetKey returns the field's key. + GetKey() string + + // GetValue returns the field's value. + GetValue() any +} + +// FieldPosition is positional information about the given field and form. +type FieldPosition struct { + Group int + Field int + FirstField int + LastField int + GroupCount int + FirstGroup int + LastGroup int +} + +// IsFirst returns whether a field is the form's first field. +func (p FieldPosition) IsFirst() bool { + return p.Field == p.FirstField && p.Group == p.FirstGroup +} + +// IsLast returns whether a field is the form's last field. +func (p FieldPosition) IsLast() bool { + return p.Field == p.LastField && p.Group == p.LastGroup +} + +// nextGroupMsg is a message to move to the next group. +type nextGroupMsg struct{} + +// prevGroupMsg is a message to move to the previous group. +type prevGroupMsg struct{} + +// nextGroup is the command to move to the next group. +func nextGroup() tea.Msg { + return nextGroupMsg{} +} + +// prevGroup is the command to move to the previous group. +func prevGroup() tea.Msg { + return prevGroupMsg{} +} + +// WithAccessible sets the form to run in accessible mode to avoid redrawing the +// views which makes it easier for screen readers to read and describe the form. +// +// This avoids using the Bubble Tea renderer and instead simply uses basic +// terminal prompting to gather input which degrades the user experience but +// provides accessibility. +func (f *Form) WithAccessible(accessible bool) *Form { + f.accessible = accessible + return f +} + +// WithShowHelp sets whether or not the form should show help. +// +// This allows the form groups and field to show what keybindings are available +// to the user. +func (f *Form) WithShowHelp(v bool) *Form { + for _, group := range f.groups { + group.WithShowHelp(v) + } + return f +} + +// WithShowErrors sets whether or not the form should show errors. +// +// This allows the form groups and fields to show errors when the Validate +// function returns an error. +func (f *Form) WithShowErrors(v bool) *Form { + for _, group := range f.groups { + group.WithShowErrors(v) + } + return f +} + +// WithTheme sets the theme on a form. +// +// This allows all groups and fields to be themed consistently, however themes +// can be applied to each group and field individually for more granular +// control. +func (f *Form) WithTheme(theme *Theme) *Form { + if theme == nil { + return f + } + for _, group := range f.groups { + group.WithTheme(theme) + } + return f +} + +// WithKeyMap sets the keymap on a form. +// +// This allows customization of the form key bindings. +func (f *Form) WithKeyMap(keymap *KeyMap) *Form { + if keymap == nil { + return f + } + f.keymap = keymap + for _, group := range f.groups { + group.WithKeyMap(keymap) + } + f.UpdateFieldPositions() + return f +} + +// WithWidth sets the width of a form. +// +// This allows all groups and fields to be sized consistently, however width +// can be applied to each group and field individually for more granular +// control. +func (f *Form) WithWidth(width int) *Form { + if width <= 0 { + return f + } + f.width = width + for _, group := range f.groups { + group.WithWidth(width) + } + return f +} + +// WithHeight sets the height of a form. +func (f *Form) WithHeight(height int) *Form { + if height <= 0 { + return f + } + f.height = height + for _, group := range f.groups { + group.WithHeight(height) + } + return f +} + +// WithOutput sets the io.Writer to output the form. +func (f *Form) WithOutput(w io.Writer) *Form { + f.output = w + f.teaOptions = append(f.teaOptions, tea.WithOutput(w)) + return f +} + +// WithProgramOptions sets the tea options of the form. +func (f *Form) WithProgramOptions(opts ...tea.ProgramOption) *Form { + f.teaOptions = opts + return f +} + +// UpdateFieldPositions sets the position on all the fields. +func (f *Form) UpdateFieldPositions() *Form { + firstGroup := 0 + lastGroup := len(f.groups) - 1 + + // determine the first non-hidden group. + for g := range f.groups { + if !f.isGroupHidden(g) { + break + } + firstGroup++ + } + + // determine the last non-hidden group. + for g := len(f.groups) - 1; g > 0; g-- { + if !f.isGroupHidden(g) { + break + } + lastGroup-- + } + + for g, group := range f.groups { + // determine the first non-skippable field. + var firstField int + for _, field := range group.fields { + if !field.Skip() || len(group.fields) == 1 { + break + } + firstField++ + } + + // determine the last non-skippable field. + var lastField int + for i := len(group.fields) - 1; i > 0; i-- { + lastField = i + if !group.fields[i].Skip() || len(group.fields) == 1 { + break + } + } + + for i, field := range group.fields { + field.WithPosition(FieldPosition{ + Group: g, + Field: i, + FirstField: firstField, + LastField: lastField, + FirstGroup: firstGroup, + LastGroup: lastGroup, + }) + } + } + return f +} + +// Errors returns the current groups' errors. +func (f *Form) Errors() []error { + return f.groups[f.paginator.Page].Errors() +} + +// Help returns the current groups' help. +func (f *Form) Help() help.Model { + return f.groups[f.paginator.Page].help +} + +// KeyBinds returns the current fields' keybinds. +func (f *Form) KeyBinds() []key.Binding { + group := f.groups[f.paginator.Page] + return group.fields[group.paginator.Page].KeyBinds() +} + +// Get returns a result from the form. +func (f *Form) Get(key string) any { + return f.results[key] +} + +// GetString returns a result as a string from the form. +func (f *Form) GetString(key string) string { + v, ok := f.results[key].(string) + if !ok { + return "" + } + return v +} + +// GetInt returns a result as a string from the form. +func (f *Form) GetInt(key string) int { + v, ok := f.results[key].(int) + if !ok { + return 0 + } + return v +} + +// GetBool returns a result as a string from the form. +func (f *Form) GetBool(key string) bool { + v, ok := f.results[key].(bool) + if !ok { + return false + } + return v +} + +// NextGroup moves the form to the next group. +func (f *Form) NextGroup() tea.Cmd { + _, cmd := f.Update(nextGroup()) + return cmd +} + +// PrevGroup moves the form to the next group. +func (f *Form) PrevGroup() tea.Cmd { + _, cmd := f.Update(prevGroup()) + return cmd +} + +// NextField moves the form to the next field. +func (f *Form) NextField() tea.Cmd { + _, cmd := f.Update(NextField()) + return cmd +} + +// NextField moves the form to the next field. +func (f *Form) PrevField() tea.Cmd { + _, cmd := f.Update(PrevField()) + return cmd +} + +// Init initializes the form. +func (f *Form) Init() tea.Cmd { + cmds := make([]tea.Cmd, len(f.groups)) + for i, group := range f.groups { + cmds[i] = group.Init() + } + + if f.isGroupHidden(f.paginator.Page) { + cmds = append(cmds, nextGroup) + } + + return tea.Batch(cmds...) +} + +// Update updates the form. +func (f *Form) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // If the form is aborted or completed there's no need to update it. + if f.State != StateNormal { + return f, nil + } + + page := f.paginator.Page + group := f.groups[page] + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + if f.width > 0 { + break + } + for _, group := range f.groups { + group.WithWidth(msg.Width) + } + if f.height > 0 { + break + } + for _, group := range f.groups { + if group.fullHeight() > msg.Height { + group.WithHeight(msg.Height) + } + } + case tea.KeyMsg: + switch { + case key.Matches(msg, f.keymap.Quit): + f.aborted = true + f.quitting = true + f.State = StateAborted + return f, f.cancelCmd + } + + case nextFieldMsg: + // Form is progressing to the next field, let's save the value of the current field. + field := group.fields[group.paginator.Page] + f.results[field.GetKey()] = field.GetValue() + + case nextGroupMsg: + if len(group.Errors()) > 0 { + return f, nil + } + + submit := func() (tea.Model, tea.Cmd) { + f.quitting = true + f.State = StateCompleted + return f, f.submitCmd + } + + if f.paginator.OnLastPage() { + return submit() + } + + for i := f.paginator.Page + 1; i < f.paginator.TotalPages; i++ { + if !f.isGroupHidden(i) { + f.paginator.Page = i + break + } + // all subsequent groups are hidden, so we must act as + // if we were in the last one. + if i == f.paginator.TotalPages-1 { + return submit() + } + } + return f, f.groups[f.paginator.Page].Init() + + case prevGroupMsg: + if len(group.Errors()) > 0 { + return f, nil + } + + for i := f.paginator.Page - 1; i >= 0; i-- { + if !f.isGroupHidden(i) { + f.paginator.Page = i + break + } + } + + return f, f.groups[f.paginator.Page].Init() + } + + m, cmd := group.Update(msg) + f.groups[page] = m.(*Group) + + // A user input a key, this could hide or show other groups, + // let's update all of their positions. + switch msg.(type) { + case tea.KeyMsg: + f.UpdateFieldPositions() + } + + return f, cmd +} + +func (f *Form) isGroupHidden(page int) bool { + hide := f.groups[page].hide + if hide == nil { + return false + } + return hide() +} + +// View renders the form. +func (f *Form) View() string { + if f.quitting { + return "" + } + + return f.groups[f.paginator.Page].View() +} + +// Run runs the form. +func (f *Form) Run() error { + f.submitCmd = tea.Quit + f.cancelCmd = tea.Quit + + if len(f.groups) == 0 { + return nil + } + + if f.accessible { + return f.runAccessible() + } + + return f.run() +} + +// run runs the form in normal mode. +func (f *Form) run() error { + m, err := tea.NewProgram(f, f.teaOptions...).Run() + if m.(*Form).aborted { + err = ErrUserAborted + } + return err +} + +// runAccessible runs the form in accessible mode. +func (f *Form) runAccessible() error { + for _, group := range f.groups { + for _, field := range group.fields { + field.Init() + field.Focus() + _ = field.WithAccessible(true).Run() + } + } + + return nil +} diff --git a/vendor/github.com/charmbracelet/huh/group.go b/vendor/github.com/charmbracelet/huh/group.go new file mode 100644 index 00000000..ee97fa36 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/group.go @@ -0,0 +1,304 @@ +package huh + +import ( + "strings" + + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/bubbles/paginator" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// Group is a collection of fields that are displayed together with a page of +// the form. While a group is displayed the form completer can switch between +// fields in the group. +// +// If any of the fields in a group have errors, the form will not be able to +// progress to the next group. +type Group struct { + // collection of fields + fields []Field + + // information + title string + description string + + // navigation + paginator paginator.Model + viewport viewport.Model + + // help + showHelp bool + help help.Model + + // errors + showErrors bool + + // group options + width int + height int + keymap *KeyMap + hide func() bool +} + +// NewGroup returns a new group with the given fields. +func NewGroup(fields ...Field) *Group { + p := paginator.New() + p.SetTotalPages(len(fields)) + + group := &Group{ + fields: fields, + paginator: p, + help: help.New(), + showHelp: true, + showErrors: true, + } + + height := group.fullHeight() + //nolint:gomnd + v := viewport.New(80, height) + group.viewport = v + group.height = height + + return group +} + +// Title sets the group's title. +func (g *Group) Title(title string) *Group { + g.title = title + return g +} + +// Description sets the group's description. +func (g *Group) Description(description string) *Group { + g.description = description + return g +} + +// WithShowHelp sets whether or not the group's help should be shown. +func (g *Group) WithShowHelp(show bool) *Group { + g.showHelp = show + return g +} + +// WithShowErrors sets whether or not the group's errors should be shown. +func (g *Group) WithShowErrors(show bool) *Group { + g.showErrors = show + return g +} + +// WithTheme sets the theme on a group. +func (g *Group) WithTheme(t *Theme) *Group { + g.help.Styles = t.Help + for _, field := range g.fields { + field.WithTheme(t) + } + if g.height <= 0 { + g.WithHeight(g.fullHeight()) + } + return g +} + +// WithKeyMap sets the keymap on a group. +func (g *Group) WithKeyMap(k *KeyMap) *Group { + g.keymap = k + for _, field := range g.fields { + field.WithKeyMap(k) + } + return g +} + +// WithWidth sets the width on a group. +func (g *Group) WithWidth(width int) *Group { + g.width = width + g.viewport.Width = width + for _, field := range g.fields { + field.WithWidth(width) + } + return g +} + +// WithHeight sets the height on a group. +func (g *Group) WithHeight(height int) *Group { + g.height = height + g.viewport.Height = height + for _, field := range g.fields { + // A field height must not exceed the form height. + if height-1 <= lipgloss.Height(field.View()) { + field.WithHeight(height) + } + } + return g +} + +// WithHide sets whether this group should be skipped. +func (g *Group) WithHide(hide bool) *Group { + g.WithHideFunc(func() bool { return hide }) + return g +} + +// WithHideFunc sets the function that checks if this group should be skipped. +func (g *Group) WithHideFunc(hideFunc func() bool) *Group { + g.hide = hideFunc + return g +} + +// Errors returns the groups' fields' errors. +func (g *Group) Errors() []error { + var errs []error + for _, field := range g.fields { + if err := field.Error(); err != nil { + errs = append(errs, err) + } + } + return errs +} + +// nextFieldMsg is a message to move to the next field, +// +// each field controls when to send this message such that it is able to use +// different key bindings or events to trigger group progression. +type nextFieldMsg struct{} + +// prevFieldMsg is a message to move to the previous field. +// +// each field controls when to send this message such that it is able to use +// different key bindings or events to trigger group progression. +type prevFieldMsg struct{} + +// NextField is the command to move to the next field. +func NextField() tea.Msg { + return nextFieldMsg{} +} + +// PrevField is the command to move to the previous field. +func PrevField() tea.Msg { + return prevFieldMsg{} +} + +// Init initializes the group. +func (g *Group) Init() tea.Cmd { + var cmds []tea.Cmd + + if g.fields[g.paginator.Page].Skip() { + if g.paginator.OnLastPage() { + cmds = append(cmds, g.prevField()...) + } else if g.paginator.Page == 0 { + cmds = append(cmds, g.nextField()...) + } + return tea.Batch(cmds...) + } + + cmd := g.fields[g.paginator.Page].Focus() + cmds = append(cmds, cmd) + g.buildView() + return tea.Batch(cmds...) +} + +// nextField moves to the next field. +func (g *Group) nextField() []tea.Cmd { + blurCmd := g.fields[g.paginator.Page].Blur() + if g.paginator.OnLastPage() { + return []tea.Cmd{blurCmd, nextGroup} + } + g.paginator.NextPage() + for g.fields[g.paginator.Page].Skip() { + if g.paginator.OnLastPage() { + return []tea.Cmd{blurCmd, nextGroup} + } + g.paginator.NextPage() + } + focusCmd := g.fields[g.paginator.Page].Focus() + return []tea.Cmd{blurCmd, focusCmd} +} + +// prevField moves to the previous field. +func (g *Group) prevField() []tea.Cmd { + blurCmd := g.fields[g.paginator.Page].Blur() + if g.paginator.Page <= 0 { + return []tea.Cmd{blurCmd, prevGroup} + } + g.paginator.PrevPage() + for g.fields[g.paginator.Page].Skip() { + if g.paginator.Page <= 0 { + return []tea.Cmd{blurCmd, prevGroup} + } + g.paginator.PrevPage() + } + focusCmd := g.fields[g.paginator.Page].Focus() + return []tea.Cmd{blurCmd, focusCmd} +} + +// Update updates the group. +func (g *Group) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmds []tea.Cmd + + m, cmd := g.fields[g.paginator.Page].Update(msg) + g.fields[g.paginator.Page] = m.(Field) + cmds = append(cmds, cmd) + + switch msg := msg.(type) { + case tea.WindowSizeMsg: + g.WithHeight(min(g.height, min(g.fullHeight(), msg.Height-1))) + case nextFieldMsg: + cmds = append(cmds, g.nextField()...) + case prevFieldMsg: + cmds = append(cmds, g.prevField()...) + } + + g.buildView() + + return g, tea.Batch(cmds...) +} + +// height returns the full height of the group. +func (g *Group) fullHeight() int { + height := len(g.fields) + for _, f := range g.fields { + height += lipgloss.Height(f.View()) + } + return height +} + +func (g *Group) buildView() { + var fields strings.Builder + offset := 0 + gap := "\n\n" + + // if the focused field is requesting it be zoomed, only show that field. + if g.fields[g.paginator.Page].Zoom() { + g.fields[g.paginator.Page].WithHeight(g.height - 1) + fields.WriteString(g.fields[g.paginator.Page].View()) + } else { + for i, field := range g.fields { + fields.WriteString(field.View()) + if i == g.paginator.Page { + offset = lipgloss.Height(fields.String()) - lipgloss.Height(field.View()) + } + if i < len(g.fields)-1 { + fields.WriteString(gap) + } + } + } + + g.viewport.SetContent(fields.String() + "\n") + g.viewport.SetYOffset(offset) +} + +// View renders the group. +func (g *Group) View() string { + var view strings.Builder + view.WriteString(g.viewport.View()) + view.WriteRune('\n') + errors := g.Errors() + if g.showHelp && len(errors) <= 0 { + view.WriteString(g.help.ShortHelpView(g.fields[g.paginator.Page].KeyBinds())) + } + if g.showErrors { + for _, err := range errors { + view.WriteString(ThemeCharm().Focused.ErrorMessage.Render(err.Error())) + } + } + return view.String() +} diff --git a/vendor/github.com/charmbracelet/huh/keymap.go b/vendor/github.com/charmbracelet/huh/keymap.go new file mode 100644 index 00000000..31f74099 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/keymap.go @@ -0,0 +1,178 @@ +package huh + +import "github.com/charmbracelet/bubbles/key" + +// KeyMap is the keybindings to navigate the form. +type KeyMap struct { + Quit key.Binding + + Confirm ConfirmKeyMap + FilePicker FilePickerKeyMap + Input InputKeyMap + MultiSelect MultiSelectKeyMap + Note NoteKeyMap + Select SelectKeyMap + Text TextKeyMap +} + +// InputKeyMap is the keybindings for input fields. +type InputKeyMap struct { + AcceptSuggestion key.Binding + Next key.Binding + Prev key.Binding + Submit key.Binding +} + +// TextKeyMap is the keybindings for text fields. +type TextKeyMap struct { + Next key.Binding + Prev key.Binding + NewLine key.Binding + Editor key.Binding + Submit key.Binding +} + +// SelectKeyMap is the keybindings for select fields. +type SelectKeyMap struct { + Next key.Binding + Prev key.Binding + Up key.Binding + Down key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding + GotoTop key.Binding + GotoBottom key.Binding + Left key.Binding + Right key.Binding + Filter key.Binding + SetFilter key.Binding + ClearFilter key.Binding + Submit key.Binding +} + +// MultiSelectKeyMap is the keybindings for multi-select fields. +type MultiSelectKeyMap struct { + Next key.Binding + Prev key.Binding + Up key.Binding + Down key.Binding + HalfPageUp key.Binding + HalfPageDown key.Binding + GotoTop key.Binding + GotoBottom key.Binding + Toggle key.Binding + Filter key.Binding + SetFilter key.Binding + ClearFilter key.Binding + Submit key.Binding +} + +// FilePickerKey is the keybindings for filepicker fields. +type FilePickerKeyMap struct { + Open key.Binding + Close key.Binding + GoToTop key.Binding + GoToLast key.Binding + PageUp key.Binding + PageDown key.Binding + Back key.Binding + Select key.Binding + Up key.Binding + Down key.Binding + Prev key.Binding + Next key.Binding + Submit key.Binding +} + +// NoteKeyMap is the keybindings for note fields. +type NoteKeyMap struct { + Next key.Binding + Prev key.Binding + Submit key.Binding +} + +// ConfirmKeyMap is the keybindings for confirm fields. +type ConfirmKeyMap struct { + Next key.Binding + Prev key.Binding + Toggle key.Binding + Submit key.Binding +} + +// NewDefaultKeyMap returns a new default keymap. +func NewDefaultKeyMap() *KeyMap { + return &KeyMap{ + Quit: key.NewBinding(key.WithKeys("ctrl+c")), + Input: InputKeyMap{ + AcceptSuggestion: key.NewBinding(key.WithKeys("ctrl+e"), key.WithHelp("ctrl+e", "complete")), + Prev: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "back")), + Next: key.NewBinding(key.WithKeys("enter", "tab"), key.WithHelp("enter", "next")), + Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), + }, + FilePicker: FilePickerKeyMap{ + GoToTop: key.NewBinding(key.WithKeys("g"), key.WithHelp("g", "first"), key.WithDisabled()), + GoToLast: key.NewBinding(key.WithKeys("G"), key.WithHelp("G", "last"), key.WithDisabled()), + PageUp: key.NewBinding(key.WithKeys("K", "pgup"), key.WithHelp("pgup", "page up"), key.WithDisabled()), + PageDown: key.NewBinding(key.WithKeys("J", "pgdown"), key.WithHelp("pgdown", "page down"), key.WithDisabled()), + Back: key.NewBinding(key.WithKeys("h", "backspace", "left", "esc"), key.WithHelp("h", "back"), key.WithDisabled()), + Select: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select"), key.WithDisabled()), + Up: key.NewBinding(key.WithKeys("up", "k", "ctrl+k", "ctrl+p"), key.WithHelp("↑", "up"), key.WithDisabled()), + Down: key.NewBinding(key.WithKeys("down", "j", "ctrl+j", "ctrl+n"), key.WithHelp("↓", "down"), key.WithDisabled()), + + Open: key.NewBinding(key.WithKeys("l", "right", "enter"), key.WithHelp("enter", "open")), + Close: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "close"), key.WithDisabled()), + Prev: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "back")), + Next: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next")), + Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), + }, + Text: TextKeyMap{ + Prev: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "back")), + Next: key.NewBinding(key.WithKeys("tab", "enter"), key.WithHelp("enter", "next")), + Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), + NewLine: key.NewBinding(key.WithKeys("alt+enter", "ctrl+j"), key.WithHelp("alt+enter / ctrl+j", "new line")), + Editor: key.NewBinding(key.WithKeys("ctrl+e"), key.WithHelp("ctrl+e", "open editor")), + }, + Select: SelectKeyMap{ + Prev: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "back")), + Next: key.NewBinding(key.WithKeys("enter", "tab"), key.WithHelp("enter", "select")), + Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), + Up: key.NewBinding(key.WithKeys("up", "k", "ctrl+k", "ctrl+p"), key.WithHelp("↑", "up")), + Down: key.NewBinding(key.WithKeys("down", "j", "ctrl+j", "ctrl+n"), key.WithHelp("↓", "down")), + Left: key.NewBinding(key.WithKeys("h", "left"), key.WithHelp("â†", "left"), key.WithDisabled()), + Right: key.NewBinding(key.WithKeys("l", "right"), key.WithHelp("→", "right"), key.WithDisabled()), + Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")), + SetFilter: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "set filter"), key.WithDisabled()), + ClearFilter: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "clear filter"), key.WithDisabled()), + HalfPageUp: key.NewBinding(key.WithKeys("ctrl+u"), key.WithHelp("ctrl+u", "½ page up")), + HalfPageDown: key.NewBinding(key.WithKeys("ctrl+d"), key.WithHelp("ctrl+d", "½ page down")), + GotoTop: key.NewBinding(key.WithKeys("home", "g"), key.WithHelp("g/home", "go to start")), + GotoBottom: key.NewBinding(key.WithKeys("end", "G"), key.WithHelp("G/end", "go to end")), + }, + MultiSelect: MultiSelectKeyMap{ + Prev: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "back")), + Next: key.NewBinding(key.WithKeys("enter", "tab"), key.WithHelp("enter", "confirm")), + Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), + Toggle: key.NewBinding(key.WithKeys(" ", "x"), key.WithHelp("x", "toggle")), + Up: key.NewBinding(key.WithKeys("up", "k", "ctrl+p"), key.WithHelp("↑", "up")), + Down: key.NewBinding(key.WithKeys("down", "j", "ctrl+n"), key.WithHelp("↓", "down")), + Filter: key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter")), + SetFilter: key.NewBinding(key.WithKeys("enter", "esc"), key.WithHelp("esc", "set filter"), key.WithDisabled()), + ClearFilter: key.NewBinding(key.WithKeys("esc"), key.WithHelp("esc", "clear filter"), key.WithDisabled()), + HalfPageUp: key.NewBinding(key.WithKeys("ctrl+u"), key.WithHelp("ctrl+u", "½ page up")), + HalfPageDown: key.NewBinding(key.WithKeys("ctrl+d"), key.WithHelp("ctrl+d", "½ page down")), + GotoTop: key.NewBinding(key.WithKeys("home", "g"), key.WithHelp("g/home", "go to start")), + GotoBottom: key.NewBinding(key.WithKeys("end", "G"), key.WithHelp("G/end", "go to end")), + }, + Note: NoteKeyMap{ + Prev: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "back")), + Next: key.NewBinding(key.WithKeys("enter", "tab"), key.WithHelp("enter", "next")), + Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), + }, + Confirm: ConfirmKeyMap{ + Prev: key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "back")), + Next: key.NewBinding(key.WithKeys("enter", "tab"), key.WithHelp("enter", "next")), + Submit: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "submit")), + Toggle: key.NewBinding(key.WithKeys("h", "l", "right", "left"), key.WithHelp("â†/→", "toggle")), + }, + } +} diff --git a/vendor/github.com/charmbracelet/huh/option.go b/vendor/github.com/charmbracelet/huh/option.go new file mode 100644 index 00000000..2dcea0be --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/option.go @@ -0,0 +1,38 @@ +package huh + +import "fmt" + +// Option is an option for select fields. +type Option[T comparable] struct { + Key string + Value T + selected bool +} + +// NewOptions returns new options from a list of values. +func NewOptions[T comparable](values ...T) []Option[T] { + options := make([]Option[T], len(values)) + for i, o := range values { + options[i] = Option[T]{ + Key: fmt.Sprint(o), + Value: o, + } + } + return options +} + +// NewOption returns a new select option. +func NewOption[T comparable](key string, value T) Option[T] { + return Option[T]{Key: key, Value: value} +} + +// Selected sets whether the option is currently selected. +func (o Option[T]) Selected(selected bool) Option[T] { + o.selected = selected + return o +} + +// String returns the key of the option. +func (o Option[T]) String() string { + return o.Key +} diff --git a/vendor/github.com/charmbracelet/huh/run.go b/vendor/github.com/charmbracelet/huh/run.go new file mode 100644 index 00000000..81e164d0 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/run.go @@ -0,0 +1,8 @@ +package huh + +// Run runs a single field by wrapping it within a group and a form. +func Run(field Field) error { + group := NewGroup(field) + form := NewForm(group).WithShowHelp(false) + return form.Run() +} diff --git a/vendor/github.com/charmbracelet/huh/theme.go b/vendor/github.com/charmbracelet/huh/theme.go new file mode 100644 index 00000000..7541a070 --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/theme.go @@ -0,0 +1,296 @@ +package huh + +import ( + catppuccin "github.com/catppuccin/go" + "github.com/charmbracelet/bubbles/help" + "github.com/charmbracelet/lipgloss" +) + +// Theme is a collection of styles for components of the form. +// Themes can be applied to a form using the WithTheme option. +type Theme struct { + Form lipgloss.Style + Group lipgloss.Style + FieldSeparator lipgloss.Style + Blurred FieldStyles + Focused FieldStyles + Help help.Styles +} + +// FieldStyles are the styles for input fields. +type FieldStyles struct { + Base lipgloss.Style + Title lipgloss.Style + Description lipgloss.Style + ErrorIndicator lipgloss.Style + ErrorMessage lipgloss.Style + + // Select styles. + SelectSelector lipgloss.Style // Selection indicator + Option lipgloss.Style // Select options + NextIndicator lipgloss.Style + PrevIndicator lipgloss.Style + + // FilePicker styles. + Directory lipgloss.Style + File lipgloss.Style + + // Multi-select styles. + MultiSelectSelector lipgloss.Style + SelectedOption lipgloss.Style + SelectedPrefix lipgloss.Style + UnselectedOption lipgloss.Style + UnselectedPrefix lipgloss.Style + + // Textinput and teatarea styles. + TextInput TextInputStyles + + // Confirm styles. + FocusedButton lipgloss.Style + BlurredButton lipgloss.Style + + // Card styles. + Card lipgloss.Style + NoteTitle lipgloss.Style + Next lipgloss.Style +} + +// TextInputStyles are the styles for text inputs. +type TextInputStyles struct { + Cursor lipgloss.Style + Placeholder lipgloss.Style + Prompt lipgloss.Style + Text lipgloss.Style +} + +const ( + buttonPaddingHorizontal = 2 + buttonPaddingVertical = 0 +) + +// ThemeBase returns a new base theme with general styles to be inherited by +// other themes. +func ThemeBase() *Theme { + var t Theme + + t.FieldSeparator = lipgloss.NewStyle().SetString("\n\n") + + button := lipgloss.NewStyle(). + Padding(buttonPaddingVertical, buttonPaddingHorizontal). + MarginRight(1) + + // Focused styles. + t.Focused.Base = lipgloss.NewStyle().PaddingLeft(1).BorderStyle(lipgloss.ThickBorder()).BorderLeft(true) + t.Focused.Card = lipgloss.NewStyle().PaddingLeft(1) + t.Focused.ErrorIndicator = lipgloss.NewStyle().SetString(" *") + t.Focused.ErrorMessage = lipgloss.NewStyle().SetString(" *") + t.Focused.SelectSelector = lipgloss.NewStyle().SetString("> ") + t.Focused.NextIndicator = lipgloss.NewStyle().MarginLeft(1).SetString("→") + t.Focused.PrevIndicator = lipgloss.NewStyle().MarginRight(1).SetString("â†") + t.Focused.MultiSelectSelector = lipgloss.NewStyle().SetString("> ") + t.Focused.SelectedPrefix = lipgloss.NewStyle().SetString("[•] ") + t.Focused.UnselectedPrefix = lipgloss.NewStyle().SetString("[ ] ") + t.Focused.FocusedButton = button.Foreground(lipgloss.Color("0")).Background(lipgloss.Color("7")) + t.Focused.BlurredButton = button.Foreground(lipgloss.Color("7")).Background(lipgloss.Color("0")) + t.Focused.TextInput.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) + + t.Help = help.New().Styles + + // Blurred styles. + t.Blurred = t.Focused + t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.MultiSelectSelector = lipgloss.NewStyle().SetString(" ") + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + return &t +} + +// ThemeCharm returns a new theme based on the Charm color scheme. +func ThemeCharm() *Theme { + t := ThemeBase() + + var ( + normalFg = lipgloss.AdaptiveColor{Light: "235", Dark: "252"} + indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} + cream = lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"} + fuchsia = lipgloss.Color("#F780E2") + green = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} + red = lipgloss.AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"} + ) + + t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("238")) + t.Focused.Title = t.Focused.Title.Foreground(indigo).Bold(true) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(indigo).Bold(true).MarginBottom(1) + t.Focused.Directory = t.Focused.Directory.Foreground(indigo) + t.Focused.Description = t.Focused.Description.Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(fuchsia) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(fuchsia) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(fuchsia) + t.Focused.Option = t.Focused.Option.Foreground(normalFg) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(fuchsia) + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green) + t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#02CF92", Dark: "#02A877"}).SetString("✓ ") + t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "", Dark: "243"}).SetString("• ") + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(normalFg) + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(cream).Background(fuchsia) + t.Focused.Next = t.Focused.FocusedButton + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "252", Dark: "237"}) + + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(green) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(lipgloss.AdaptiveColor{Light: "248", Dark: "238"}) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(fuchsia) + + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + return t +} + +// ThemeDracula returns a new theme based on the Dracula color scheme. +func ThemeDracula() *Theme { + t := ThemeBase() + + var ( + background = lipgloss.AdaptiveColor{Dark: "#282a36"} + selection = lipgloss.AdaptiveColor{Dark: "#44475a"} + foreground = lipgloss.AdaptiveColor{Dark: "#f8f8f2"} + comment = lipgloss.AdaptiveColor{Dark: "#6272a4"} + green = lipgloss.AdaptiveColor{Dark: "#50fa7b"} + purple = lipgloss.AdaptiveColor{Dark: "#bd93f9"} + red = lipgloss.AdaptiveColor{Dark: "#ff5555"} + yellow = lipgloss.AdaptiveColor{Dark: "#f1fa8c"} + ) + + t.Focused.Base = t.Focused.Base.BorderForeground(selection) + t.Focused.Title = t.Focused.Title.Foreground(purple) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(purple) + t.Focused.Description = t.Focused.Description.Foreground(comment) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red) + t.Focused.Directory = t.Focused.Directory.Foreground(purple) + t.Focused.File = t.Focused.File.Foreground(foreground) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(yellow) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(yellow) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(yellow) + t.Focused.Option = t.Focused.Option.Foreground(foreground) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(yellow) + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green) + t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(green) + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(foreground) + t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(comment) + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(yellow).Background(purple).Bold(true) + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(foreground).Background(background) + + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(yellow) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(comment) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(yellow) + + t.Blurred = t.Focused + t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + return t +} + +// ThemeBase16 returns a new theme based on the base16 color scheme. +func ThemeBase16() *Theme { + t := ThemeBase() + + t.Focused.Base = t.Focused.Base.BorderForeground(lipgloss.Color("8")) + t.Focused.Title = t.Focused.Title.Foreground(lipgloss.Color("6")) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(lipgloss.Color("6")) + t.Focused.Directory = t.Focused.Directory.Foreground(lipgloss.Color("6")) + t.Focused.Description = t.Focused.Description.Foreground(lipgloss.Color("8")) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(lipgloss.Color("9")) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(lipgloss.Color("9")) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(lipgloss.Color("3")) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(lipgloss.Color("3")) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(lipgloss.Color("3")) + t.Focused.Option = t.Focused.Option.Foreground(lipgloss.Color("7")) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(lipgloss.Color("3")) + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(lipgloss.Color("2")) + t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(lipgloss.Color("2")) + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(lipgloss.Color("7")) + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(lipgloss.Color("7")).Background(lipgloss.Color("5")) + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(lipgloss.Color("7")).Background(lipgloss.Color("0")) + + t.Focused.TextInput.Cursor.Foreground(lipgloss.Color("5")) + t.Focused.TextInput.Placeholder.Foreground(lipgloss.Color("8")) + t.Focused.TextInput.Prompt.Foreground(lipgloss.Color("3")) + + t.Blurred = t.Focused + t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.NoteTitle = t.Blurred.NoteTitle.Foreground(lipgloss.Color("8")) + t.Blurred.Title = t.Blurred.NoteTitle.Foreground(lipgloss.Color("8")) + + t.Blurred.TextInput.Prompt = t.Blurred.TextInput.Prompt.Foreground(lipgloss.Color("8")) + t.Blurred.TextInput.Text = t.Blurred.TextInput.Text.Foreground(lipgloss.Color("7")) + + t.Blurred.NextIndicator = lipgloss.NewStyle() + t.Blurred.PrevIndicator = lipgloss.NewStyle() + + return t +} + +// ThemeCatppuccin returns a new theme based on the Catppuccin color scheme. +func ThemeCatppuccin() *Theme { + t := ThemeBase() + + light := catppuccin.Latte + dark := catppuccin.Mocha + var ( + base = lipgloss.AdaptiveColor{Light: light.Base().Hex, Dark: dark.Base().Hex} + text = lipgloss.AdaptiveColor{Light: light.Text().Hex, Dark: dark.Text().Hex} + subtext1 = lipgloss.AdaptiveColor{Light: light.Subtext1().Hex, Dark: dark.Subtext1().Hex} + subtext0 = lipgloss.AdaptiveColor{Light: light.Subtext0().Hex, Dark: dark.Subtext0().Hex} + overlay1 = lipgloss.AdaptiveColor{Light: light.Overlay1().Hex, Dark: dark.Overlay1().Hex} + overlay0 = lipgloss.AdaptiveColor{Light: light.Overlay0().Hex, Dark: dark.Overlay0().Hex} + green = lipgloss.AdaptiveColor{Light: light.Green().Hex, Dark: dark.Green().Hex} + red = lipgloss.AdaptiveColor{Light: light.Red().Hex, Dark: dark.Red().Hex} + pink = lipgloss.AdaptiveColor{Light: light.Pink().Hex, Dark: dark.Pink().Hex} + mauve = lipgloss.AdaptiveColor{Light: light.Mauve().Hex, Dark: dark.Mauve().Hex} + cursor = lipgloss.AdaptiveColor{Light: light.Rosewater().Hex, Dark: dark.Rosewater().Hex} + ) + + t.Focused.Base = t.Focused.Base.BorderForeground(subtext1) + t.Focused.Title = t.Focused.Title.Foreground(mauve) + t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(mauve) + t.Focused.Directory = t.Focused.Directory.Foreground(mauve) + t.Focused.Description = t.Focused.Description.Foreground(subtext0) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(red) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(red) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(pink) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(pink) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(pink) + t.Focused.Option = t.Focused.Option.Foreground(text) + t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(pink) + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(green) + t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(green) + t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(text) + t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(text) + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(base).Background(pink) + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(text).Background(base) + + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(cursor) + t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(overlay0) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(pink) + + t.Blurred = t.Focused + t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder()) + + t.Help.Ellipsis = t.Help.Ellipsis.Foreground(subtext0) + t.Help.ShortKey = t.Help.ShortKey.Foreground(subtext0) + t.Help.ShortDesc = t.Help.ShortDesc.Foreground(overlay1) + t.Help.ShortSeparator = t.Help.ShortSeparator.Foreground(subtext0) + t.Help.FullKey = t.Help.FullKey.Foreground(subtext0) + t.Help.FullDesc = t.Help.FullDesc.Foreground(overlay1) + t.Help.FullSeparator = t.Help.FullSeparator.Foreground(subtext0) + + return t +} diff --git a/vendor/github.com/charmbracelet/huh/validate.go b/vendor/github.com/charmbracelet/huh/validate.go new file mode 100644 index 00000000..829d947f --- /dev/null +++ b/vendor/github.com/charmbracelet/huh/validate.go @@ -0,0 +1,61 @@ +package huh + +import ( + "fmt" + "unicode/utf8" +) + +// ValidateNotEmpty checks if the input is not empty. +func ValidateNotEmpty() func(s string) error { + return func(s string) error { + if err := ValidateMinLength(1)(s); err != nil { + return fmt.Errorf("input cannot be empty") + } + return nil + } +} + +// ValidateMinLength checks if the length of the input is at least min. +func ValidateMinLength(min int) func(s string) error { + return func(s string) error { + if utf8.RuneCountInString(s) < min { + return fmt.Errorf("input must be at least %d characters long", min) + } + return nil + } +} + +// ValidateMaxLength checks if the length of the input is at most max. +func ValidateMaxLength(max int) func(s string) error { + return func(s string) error { + if utf8.RuneCountInString(s) > max { + return fmt.Errorf("input must be at most %d characters long", max) + } + return nil + } +} + +// ValidateLength checks if the length of the input is within the specified range. +func ValidateLength(min, max int) func(s string) error { + return func(s string) error { + if err := ValidateMinLength(min)(s); err != nil { + return err + } + return ValidateMaxLength(max)(s) + } +} + +// ValidateOneOf checks if a string is one of the specified options. +func ValidateOneOf(options ...string) func(string) error { + validOptions := make(map[string]struct{}) + for _, option := range options { + validOptions[option] = struct{}{} + } + + return func(value string) error { + if _, ok := validOptions[value]; !ok { + return fmt.Errorf("invalid option: %s", value) + } + return nil + } +} diff --git a/vendor/github.com/charmbracelet/lipgloss/.gitignore b/vendor/github.com/charmbracelet/lipgloss/.gitignore new file mode 100644 index 00000000..53e1c2b7 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/.gitignore @@ -0,0 +1 @@ +ssh_example_ed25519* diff --git a/vendor/github.com/charmbracelet/lipgloss/.golangci-soft.yml b/vendor/github.com/charmbracelet/lipgloss/.golangci-soft.yml new file mode 100644 index 00000000..1b6824bb --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/.golangci-soft.yml @@ -0,0 +1,46 @@ +run: + tests: false + +issues: + include: + - EXC0001 + - EXC0005 + - EXC0011 + - EXC0012 + - EXC0013 + + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + enable: + # - dupl + - exhaustive + # - exhaustivestruct + - goconst + - godot + - godox + - gomnd + - gomoddirectives + - goprintffuncname + # - lll + - misspell + - nakedret + - nestif + - noctx + - nolintlint + - prealloc + - wrapcheck + + # disable default linters, they are already enabled in .golangci.yml + disable: + - deadcode + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - structcheck + - typecheck + - unused + - varcheck diff --git a/vendor/github.com/charmbracelet/lipgloss/.golangci.yml b/vendor/github.com/charmbracelet/lipgloss/.golangci.yml new file mode 100644 index 00000000..3affce91 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/.golangci.yml @@ -0,0 +1,30 @@ +run: + tests: false + +issues: + include: + - EXC0001 + - EXC0005 + - EXC0011 + - EXC0012 + - EXC0013 + + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + enable: + - bodyclose + - exportloopref + - gofumpt + - goimports + - gosec + - nilerr + - predeclared + - revive + - rowserrcheck + - sqlclosecheck + - tparallel + - unconvert + - unparam + - whitespace diff --git a/vendor/github.com/charmbracelet/lipgloss/.goreleaser.yml b/vendor/github.com/charmbracelet/lipgloss/.goreleaser.yml new file mode 100644 index 00000000..c61970e0 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/.goreleaser.yml @@ -0,0 +1,5 @@ +includes: + - from_url: + url: charmbracelet/meta/main/goreleaser-lib.yaml +# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json + diff --git a/vendor/github.com/charmbracelet/lipgloss/LICENSE b/vendor/github.com/charmbracelet/lipgloss/LICENSE new file mode 100644 index 00000000..6f5b1fa6 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021-2023 Charmbracelet, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/charmbracelet/lipgloss/README.md b/vendor/github.com/charmbracelet/lipgloss/README.md new file mode 100644 index 00000000..42661d6c --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/README.md @@ -0,0 +1,751 @@ +# Lip Gloss + +

+ Lip Gloss title treatment
+ Latest Release + GoDoc + Build Status + phorm.ai +

+ +Style definitions for nice terminal layouts. Built with TUIs in mind. + +![Lip Gloss example](https://stuff.charm.sh/lipgloss/lipgloss-example.png) + +Lip Gloss takes an expressive, declarative approach to terminal rendering. +Users familiar with CSS will feel at home with Lip Gloss. + +```go + +import "github.com/charmbracelet/lipgloss" + +var style = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FAFAFA")). + Background(lipgloss.Color("#7D56F4")). + PaddingTop(2). + PaddingLeft(4). + Width(22) + +fmt.Println(style.Render("Hello, kitty")) +``` + +## Colors + +Lip Gloss supports the following color profiles: + +### ANSI 16 colors (4-bit) + +```go +lipgloss.Color("5") // magenta +lipgloss.Color("9") // red +lipgloss.Color("12") // light blue +``` + +### ANSI 256 Colors (8-bit) + +```go +lipgloss.Color("86") // aqua +lipgloss.Color("201") // hot pink +lipgloss.Color("202") // orange +``` + +### True Color (16,777,216 colors; 24-bit) + +```go +lipgloss.Color("#0000FF") // good ol' 100% blue +lipgloss.Color("#04B575") // a green +lipgloss.Color("#3C3C3C") // a dark gray +``` + +...as well as a 1-bit ASCII profile, which is black and white only. + +The terminal's color profile will be automatically detected, and colors outside +the gamut of the current palette will be automatically coerced to their closest +available value. + +### Adaptive Colors + +You can also specify color options for light and dark backgrounds: + +```go +lipgloss.AdaptiveColor{Light: "236", Dark: "248"} +``` + +The terminal's background color will automatically be detected and the +appropriate color will be chosen at runtime. + +### Complete Colors + +CompleteColor specifies exact values for True Color, ANSI256, and ANSI color +profiles. + +```go +lipgloss.CompleteColor{TrueColor: "#0000FF", ANSI256: "86", ANSI: "5"} +``` + +Automatic color degradation will not be performed in this case and it will be +based on the color specified. + +### Complete Adaptive Colors + +You can use `CompleteColor` with `AdaptiveColor` to specify the exact values for +light and dark backgrounds without automatic color degradation. + +```go +lipgloss.CompleteAdaptiveColor{ + Light: CompleteColor{TrueColor: "#d7ffae", ANSI256: "193", ANSI: "11"}, + Dark: CompleteColor{TrueColor: "#d75fee", ANSI256: "163", ANSI: "5"}, +} +``` + +## Inline Formatting + +Lip Gloss supports the usual ANSI text formatting options: + +```go +var style = lipgloss.NewStyle(). + Bold(true). + Italic(true). + Faint(true). + Blink(true). + Strikethrough(true). + Underline(true). + Reverse(true) +``` + +## Block-Level Formatting + +Lip Gloss also supports rules for block-level formatting: + +```go +// Padding +var style = lipgloss.NewStyle(). + PaddingTop(2). + PaddingRight(4). + PaddingBottom(2). + PaddingLeft(4) + +// Margins +var style = lipgloss.NewStyle(). + MarginTop(2). + MarginRight(4). + MarginBottom(2). + MarginLeft(4) +``` + +There is also shorthand syntax for margins and padding, which follows the same +format as CSS: + +```go +// 2 cells on all sides +lipgloss.NewStyle().Padding(2) + +// 2 cells on the top and bottom, 4 cells on the left and right +lipgloss.NewStyle().Margin(2, 4) + +// 1 cell on the top, 4 cells on the sides, 2 cells on the bottom +lipgloss.NewStyle().Padding(1, 4, 2) + +// Clockwise, starting from the top: 2 cells on the top, 4 on the right, 3 on +// the bottom, and 1 on the left +lipgloss.NewStyle().Margin(2, 4, 3, 1) +``` + +## Aligning Text + +You can align paragraphs of text to the left, right, or center. + +```go +var style = lipgloss.NewStyle(). + Width(24). + Align(lipgloss.Left). // align it left + Align(lipgloss.Right). // no wait, align it right + Align(lipgloss.Center) // just kidding, align it in the center +``` + +## Width and Height + +Setting a minimum width and height is simple and straightforward. + +```go +var style = lipgloss.NewStyle(). + SetString("What’s for lunch?"). + Width(24). + Height(32). + Foreground(lipgloss.Color("63")) +``` + +## Borders + +Adding borders is easy: + +```go +// Add a purple, rectangular border +var style = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("63")) + +// Set a rounded, yellow-on-purple border to the top and left +var anotherStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("228")). + BorderBackground(lipgloss.Color("63")). + BorderTop(true). + BorderLeft(true) + +// Make your own border +var myCuteBorder = lipgloss.Border{ + Top: "._.:*:", + Bottom: "._.:*:", + Left: "|*", + Right: "|*", + TopLeft: "*", + TopRight: "*", + BottomLeft: "*", + BottomRight: "*", +} +``` + +There are also shorthand functions for defining borders, which follow a similar +pattern to the margin and padding shorthand functions. + +```go +// Add a thick border to the top and bottom +lipgloss.NewStyle(). + Border(lipgloss.ThickBorder(), true, false) + +// Add a double border to the top and left sides. Rules are set clockwise +// from top. +lipgloss.NewStyle(). + Border(lipgloss.DoubleBorder(), true, false, false, true) +``` + +For more on borders see [the docs][docs]. + +## Copying Styles + +Just use assignment: + +```go +style := lipgloss.NewStyle().Foreground(lipgloss.Color("219")) + +copiedStyle := style // this is a true copy + +wildStyle := style.Blink(true) // this is also true copy, with blink added + +``` + +Since `Style` data structures contains only primitive types, assigning a style +to another effectively creates a new copy of the style without mutating the +original. + +## Inheritance + +Styles can inherit rules from other styles. When inheriting, only unset rules +on the receiver are inherited. + +```go +var styleA = lipgloss.NewStyle(). + Foreground(lipgloss.Color("229")). + Background(lipgloss.Color("63")) + +// Only the background color will be inherited here, because the foreground +// color will have been already set: +var styleB = lipgloss.NewStyle(). + Foreground(lipgloss.Color("201")). + Inherit(styleA) +``` + +## Unsetting Rules + +All rules can be unset: + +```go +var style = lipgloss.NewStyle(). + Bold(true). // make it bold + UnsetBold(). // jk don't make it bold + Background(lipgloss.Color("227")). // yellow background + UnsetBackground() // never mind +``` + +When a rule is unset, it won't be inherited or copied. + +## Enforcing Rules + +Sometimes, such as when developing a component, you want to make sure style +definitions respect their intended purpose in the UI. This is where `Inline` +and `MaxWidth`, and `MaxHeight` come in: + +```go +// Force rendering onto a single line, ignoring margins, padding, and borders. +someStyle.Inline(true).Render("yadda yadda") + +// Also limit rendering to five cells +someStyle.Inline(true).MaxWidth(5).Render("yadda yadda") + +// Limit rendering to a 5x5 cell block +someStyle.MaxWidth(5).MaxHeight(5).Render("yadda yadda") +``` + +## Tabs + +The tab character (`\t`) is rendered differently in different terminals (often +as 8 spaces, sometimes 4). Because of this inconsistency, Lip Gloss converts +tabs to 4 spaces at render time. This behavior can be changed on a per-style +basis, however: + +```go +style := lipgloss.NewStyle() // tabs will render as 4 spaces, the default +style = style.TabWidth(2) // render tabs as 2 spaces +style = style.TabWidth(0) // remove tabs entirely +style = style.TabWidth(lipgloss.NoTabConversion) // leave tabs intact +``` + +## Rendering + +Generally, you just call the `Render(string...)` method on a `lipgloss.Style`: + +```go +style := lipgloss.NewStyle().Bold(true).SetString("Hello,") +fmt.Println(style.Render("kitty.")) // Hello, kitty. +fmt.Println(style.Render("puppy.")) // Hello, puppy. +``` + +But you could also use the Stringer interface: + +```go +var style = lipgloss.NewStyle().SetString("你好,猫咪。").Bold(true) +fmt.Println(style) // 你好,猫咪。 +``` + +### Custom Renderers + +Custom renderers allow you to render to a specific outputs. This is +particularly important when you want to render to different outputs and +correctly detect the color profile and dark background status for each, such as +in a server-client situation. + +```go +func myLittleHandler(sess ssh.Session) { + // Create a renderer for the client. + renderer := lipgloss.NewRenderer(sess) + + // Create a new style on the renderer. + style := renderer.NewStyle().Background(lipgloss.AdaptiveColor{Light: "63", Dark: "228"}) + + // Render. The color profile and dark background state will be correctly detected. + io.WriteString(sess, style.Render("Heyyyyyyy")) +} +``` + +For an example on using a custom renderer over SSH with [Wish][wish] see the +[SSH example][ssh-example]. + +## Utilities + +In addition to pure styling, Lip Gloss also ships with some utilities to help +assemble your layouts. + +### Joining Paragraphs + +Horizontally and vertically joining paragraphs is a cinch. + +```go +// Horizontally join three paragraphs along their bottom edges +lipgloss.JoinHorizontal(lipgloss.Bottom, paragraphA, paragraphB, paragraphC) + +// Vertically join two paragraphs along their center axes +lipgloss.JoinVertical(lipgloss.Center, paragraphA, paragraphB) + +// Horizontally join three paragraphs, with the shorter ones aligning 20% +// from the top of the tallest +lipgloss.JoinHorizontal(0.2, paragraphA, paragraphB, paragraphC) +``` + +### Measuring Width and Height + +Sometimes you’ll want to know the width and height of text blocks when building +your layouts. + +```go +// Render a block of text. +var style = lipgloss.NewStyle(). + Width(40). + Padding(2) +var block string = style.Render(someLongString) + +// Get the actual, physical dimensions of the text block. +width := lipgloss.Width(block) +height := lipgloss.Height(block) + +// Here's a shorthand function. +w, h := lipgloss.Size(block) +``` + +### Placing Text in Whitespace + +Sometimes you’ll simply want to place a block of text in whitespace. + +```go +// Center a paragraph horizontally in a space 80 cells wide. The height of +// the block returned will be as tall as the input paragraph. +block := lipgloss.PlaceHorizontal(80, lipgloss.Center, fancyStyledParagraph) + +// Place a paragraph at the bottom of a space 30 cells tall. The width of +// the text block returned will be as wide as the input paragraph. +block := lipgloss.PlaceVertical(30, lipgloss.Bottom, fancyStyledParagraph) + +// Place a paragraph in the bottom right corner of a 30x80 cell space. +block := lipgloss.Place(30, 80, lipgloss.Right, lipgloss.Bottom, fancyStyledParagraph) +``` + +You can also style the whitespace. For details, see [the docs][docs]. + +### Rendering Tables + +Lip Gloss ships with a table rendering sub-package. + +```go +import "github.com/charmbracelet/lipgloss/table" +``` + +Define some rows of data. + +```go +rows := [][]string{ + {"Chinese", "您好", "你好"}, + {"Japanese", "ã“ã‚“ã«ã¡ã¯", "ã‚„ã‚"}, + {"Arabic", "أهلين", "أهلا"}, + {"Russian", "ЗдравÑтвуйте", "Привет"}, + {"Spanish", "Hola", "¿Qué tal?"}, +} +``` + +Use the table package to style and render the table. + +```go +t := table.New(). + Border(lipgloss.NormalBorder()). + BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). + StyleFunc(func(row, col int) lipgloss.Style { + switch { + case row == 0: + return HeaderStyle + case row%2 == 0: + return EvenRowStyle + default: + return OddRowStyle + } + }). + Headers("LANGUAGE", "FORMAL", "INFORMAL"). + Rows(rows...) + +// You can also add tables row-by-row +t.Row("English", "You look absolutely fabulous.", "How's it going?") +``` + +Print the table. + +```go +fmt.Println(t) +``` + +![Table Example](https://github.com/charmbracelet/lipgloss/assets/42545625/6e4b70c4-f494-45da-a467-bdd27df30d5d) + +For more on tables see [the docs](https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc) and [examples](https://github.com/charmbracelet/lipgloss/tree/master/examples/table). + +## Rendering Lists + +Lip Gloss ships with a list rendering sub-package. + +```go +import "github.com/charmbracelet/lipgloss/list" +``` + +Define a new list. + +```go +l := list.New("A", "B", "C") +``` + +Print the list. + +```go +fmt.Println(l) + +// • A +// • B +// • C +``` + +Lists have the ability to nest. + +```go +l := list.New( + "A", list.New("Artichoke"), + "B", list.New("Baking Flour", "Bananas", "Barley", "Bean Sprouts"), + "C", list.New("Cashew Apple", "Cashews", "Coconut Milk", "Curry Paste", "Currywurst"), + "D", list.New("Dill", "Dragonfruit", "Dried Shrimp"), + "E", list.New("Eggs"), + "F", list.New("Fish Cake", "Furikake"), + "J", list.New("Jicama"), + "K", list.New("Kohlrabi"), + "L", list.New("Leeks", "Lentils", "Licorice Root"), +) +``` + +Print the list. + +```go +fmt.Println(l) +``` + +

+image +

+ +Lists can be customized via their enumeration function as well as using +`lipgloss.Style`s. + +```go +enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1) +itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")).MarginRight(1) + +l := list.New( + "Glossier", + "Claire’s Boutique", + "Nyx", + "Mac", + "Milk", +). + Enumerator(list.Roman). + EnumeratorStyle(enumeratorStyle). + ItemStyle(itemStyle) +``` + +Print the list. + +

+List example +

+ +In addition to the predefined enumerators (`Arabic`, `Alphabet`, `Roman`, `Bullet`, `Tree`), +you may also define your own custom enumerator: + +```go +l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck") + +func DuckDuckGooseEnumerator(l list.Items, i int) string { + if l.At(i).Value() == "Goose" { + return "Honk →" + } + return "" +} + +l = l.Enumerator(DuckDuckGooseEnumerator) +``` + +Print the list: + +

+image +

+ +If you need, you can also build lists incrementally: + +```go +l := list.New() + +for i := 0; i < repeat; i++ { + l.Item("Lip Gloss") +} +``` + +## Rendering Trees + +Lip Gloss ships with a tree rendering sub-package. + +```go +import "github.com/charmbracelet/lipgloss/tree" +``` + +Define a new tree. + +```go +t := tree.Root("."). + Child("A", "B", "C") +``` + +Print the tree. + +```go +fmt.Println(t) + +// . +// ├── A +// ├── B +// └── C +``` + +Trees have the ability to nest. + +```go +t := tree.Root("."). + Child("Item 1"). + Child( + tree.Root("Item 2"). + Child("Item 2.1"). + Child("Item 2.2"). + Child("Item 2.3"), + ). + Child( + tree.Root("Item 3"). + Child("Item 3.1"). + Child("Item 3.2"), + ) +``` + +Print the tree. + +```go +fmt.Println(t) +``` + +

+Tree Example (simple) +

+ +Trees can be customized via their enumeration function as well as using +`lipgloss.Style`s. + +```go +enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1) +itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")).MarginRight(1) + +t := tree.Root("Makeup"). + Child( + "Glossier", + "Claire’s Boutique", + "Nyx", + "Mac", + "Milk", + ). + Enumerator(tree.RoundedEnumerator). + EnumeratorStyle(enumeratorStyle). + ItemStyle(itemStyle). + RootStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575"))) +``` + +Print the tree. + +

+Tree Example (makeup) +

+ +The predefined enumerators for trees are `DefaultEnumerator` and `RoundedEnumerator`. + +If you need, you can also build trees incrementally: + +```go +t := tree.New() + +for i := 0; i < repeat; i++ { + t.Child("Lip Gloss") +} +``` + +--- + +## FAQ + +
+ +Why are things misaligning? Why are borders at the wrong widths? + +

This is most likely due to your locale and encoding, particularly with +regard to Chinese, Japanese, and Korean (for example, zh_CN.UTF-8 +or ja_JP.UTF-8). The most direct way to fix this is to set +RUNEWIDTH_EASTASIAN=0 in your environment.

+ +

For details see https://github.com/charmbracelet/lipgloss/issues/40.

+
+ +
+ +Why isn't Lip Gloss displaying colors? + +

Lip Gloss automatically degrades colors to the best available option in the +given terminal, and if output's not a TTY it will remove color output entirely. +This is common when running tests, CI, or when piping output elsewhere.

+ +

If necessary, you can force a color profile in your tests with +SetColorProfile.

+ +```go +import ( + "github.com/charmbracelet/lipgloss" + "github.com/muesli/termenv" +) + +lipgloss.SetColorProfile(termenv.TrueColor) +``` + +_Note:_ this option limits the flexibility of your application and can cause +ANSI escape codes to be output in cases where that might not be desired. Take +careful note of your use case and environment before choosing to force a color +profile. + +
+ +## What about [Bubble Tea][tea]? + +Lip Gloss doesn’t replace Bubble Tea. Rather, it is an excellent Bubble Tea +companion. It was designed to make assembling terminal user interface views as +simple and fun as possible so that you can focus on building your application +instead of concerning yourself with low-level layout details. + +In simple terms, you can use Lip Gloss to help build your Bubble Tea views. + +[tea]: https://github.com/charmbracelet/tea + +## Under the Hood + +Lip Gloss is built on the excellent [Termenv][termenv] and [Reflow][reflow] +libraries which deal with color and ANSI-aware text operations, respectively. +For many use cases Termenv and Reflow will be sufficient for your needs. + +[termenv]: https://github.com/muesli/termenv +[reflow]: https://github.com/muesli/reflow + +## Rendering Markdown + +For a more document-centric rendering solution with support for things like +lists, tables, and syntax-highlighted code have a look at [Glamour][glamour], +the stylesheet-based Markdown renderer. + +[glamour]: https://github.com/charmbracelet/glamour + +## Feedback + +We’d love to hear your thoughts on this project. Feel free to drop us a note! + +- [Twitter](https://twitter.com/charmcli) +- [The Fediverse](https://mastodon.social/@charmcli) +- [Discord](https://charm.sh/chat) + +## License + +[MIT](https://github.com/charmbracelet/lipgloss/raw/master/LICENSE) + +--- + +Part of [Charm](https://charm.sh). + +The Charm logo + +Charmçƒ­çˆ±å¼€æº â€¢ Charm loves open source + +[docs]: https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc +[wish]: https://github.com/charmbracelet/wish +[ssh-example]: examples/ssh diff --git a/vendor/github.com/charmbracelet/lipgloss/align.go b/vendor/github.com/charmbracelet/lipgloss/align.go new file mode 100644 index 00000000..805d1e0d --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/align.go @@ -0,0 +1,83 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/ansi" + "github.com/muesli/termenv" +) + +// Perform text alignment. If the string is multi-lined, we also make all lines +// the same width by padding them with spaces. If a termenv style is passed, +// use that to style the spaces added. +func alignTextHorizontal(str string, pos Position, width int, style *termenv.Style) string { + lines, widestLine := getLines(str) + var b strings.Builder + + for i, l := range lines { + lineWidth := ansi.StringWidth(l) + + shortAmount := widestLine - lineWidth // difference from the widest line + shortAmount += max(0, width-(shortAmount+lineWidth)) // difference from the total width, if set + + if shortAmount > 0 { + switch pos { //nolint:exhaustive + case Right: + s := strings.Repeat(" ", shortAmount) + if style != nil { + s = style.Styled(s) + } + l = s + l + case Center: + // Note: remainder goes on the right. + left := shortAmount / 2 //nolint:gomnd + right := left + shortAmount%2 //nolint:gomnd + + leftSpaces := strings.Repeat(" ", left) + rightSpaces := strings.Repeat(" ", right) + + if style != nil { + leftSpaces = style.Styled(leftSpaces) + rightSpaces = style.Styled(rightSpaces) + } + l = leftSpaces + l + rightSpaces + default: // Left + s := strings.Repeat(" ", shortAmount) + if style != nil { + s = style.Styled(s) + } + l += s + } + } + + b.WriteString(l) + if i < len(lines)-1 { + b.WriteRune('\n') + } + } + + return b.String() +} + +func alignTextVertical(str string, pos Position, height int, _ *termenv.Style) string { + strHeight := strings.Count(str, "\n") + 1 + if height < strHeight { + return str + } + + switch pos { + case Top: + return str + strings.Repeat("\n", height-strHeight) + case Center: + topPadding, bottomPadding := (height-strHeight)/2, (height-strHeight)/2 //nolint:gomnd + if strHeight+topPadding+bottomPadding > height { + topPadding-- + } else if strHeight+topPadding+bottomPadding < height { + bottomPadding++ + } + return strings.Repeat("\n", topPadding) + str + strings.Repeat("\n", bottomPadding) + case Bottom: + return strings.Repeat("\n", height-strHeight) + str + } + return str +} diff --git a/vendor/github.com/charmbracelet/lipgloss/ansi_unix.go b/vendor/github.com/charmbracelet/lipgloss/ansi_unix.go new file mode 100644 index 00000000..d416b8c9 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/ansi_unix.go @@ -0,0 +1,7 @@ +//go:build !windows +// +build !windows + +package lipgloss + +// enableLegacyWindowsANSI is only needed on Windows. +func enableLegacyWindowsANSI() {} diff --git a/vendor/github.com/charmbracelet/lipgloss/ansi_windows.go b/vendor/github.com/charmbracelet/lipgloss/ansi_windows.go new file mode 100644 index 00000000..0cf56e4c --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/ansi_windows.go @@ -0,0 +1,22 @@ +//go:build windows +// +build windows + +package lipgloss + +import ( + "sync" + + "github.com/muesli/termenv" +) + +var enableANSI sync.Once + +// enableANSIColors enables support for ANSI color sequences in the Windows +// default console (cmd.exe and the PowerShell application). Note that this +// only works with Windows 10. Also note that Windows Terminal supports colors +// by default. +func enableLegacyWindowsANSI() { + enableANSI.Do(func() { + _, _ = termenv.EnableWindowsANSIConsole() + }) +} diff --git a/vendor/github.com/charmbracelet/lipgloss/borders.go b/vendor/github.com/charmbracelet/lipgloss/borders.go new file mode 100644 index 00000000..deb6b35a --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/borders.go @@ -0,0 +1,443 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/ansi" + "github.com/muesli/termenv" + "github.com/rivo/uniseg" +) + +// Border contains a series of values which comprise the various parts of a +// border. +type Border struct { + Top string + Bottom string + Left string + Right string + TopLeft string + TopRight string + BottomLeft string + BottomRight string + MiddleLeft string + MiddleRight string + Middle string + MiddleTop string + MiddleBottom string +} + +// GetTopSize returns the width of the top border. If borders contain runes of +// varying widths, the widest rune is returned. If no border exists on the top +// edge, 0 is returned. +func (b Border) GetTopSize() int { + return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight) +} + +// GetRightSize returns the width of the right border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the right edge, 0 is returned. +func (b Border) GetRightSize() int { + return getBorderEdgeWidth(b.TopRight, b.Right, b.BottomRight) +} + +// GetBottomSize returns the width of the bottom border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the bottom edge, 0 is returned. +func (b Border) GetBottomSize() int { + return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight) +} + +// GetLeftSize returns the width of the left border. If borders contain runes +// of varying widths, the widest rune is returned. If no border exists on the +// left edge, 0 is returned. +func (b Border) GetLeftSize() int { + return getBorderEdgeWidth(b.TopLeft, b.Left, b.BottomLeft) +} + +func getBorderEdgeWidth(borderParts ...string) (maxWidth int) { + for _, piece := range borderParts { + w := maxRuneWidth(piece) + if w > maxWidth { + maxWidth = w + } + } + return maxWidth +} + +var ( + noBorder = Border{} + + normalBorder = Border{ + Top: "─", + Bottom: "─", + Left: "│", + Right: "│", + TopLeft: "┌", + TopRight: "â”", + BottomLeft: "â””", + BottomRight: "┘", + MiddleLeft: "├", + MiddleRight: "┤", + Middle: "┼", + MiddleTop: "┬", + MiddleBottom: "â”´", + } + + roundedBorder = Border{ + Top: "─", + Bottom: "─", + Left: "│", + Right: "│", + TopLeft: "â•­", + TopRight: "â•®", + BottomLeft: "â•°", + BottomRight: "╯", + MiddleLeft: "├", + MiddleRight: "┤", + Middle: "┼", + MiddleTop: "┬", + MiddleBottom: "â”´", + } + + blockBorder = Border{ + Top: "â–ˆ", + Bottom: "â–ˆ", + Left: "â–ˆ", + Right: "â–ˆ", + TopLeft: "â–ˆ", + TopRight: "â–ˆ", + BottomLeft: "â–ˆ", + BottomRight: "â–ˆ", + } + + outerHalfBlockBorder = Border{ + Top: "â–€", + Bottom: "â–„", + Left: "â–Œ", + Right: "â–", + TopLeft: "â–›", + TopRight: "â–œ", + BottomLeft: "â–™", + BottomRight: "â–Ÿ", + } + + innerHalfBlockBorder = Border{ + Top: "â–„", + Bottom: "â–€", + Left: "â–", + Right: "â–Œ", + TopLeft: "â–—", + TopRight: "â––", + BottomLeft: "â–", + BottomRight: "â–˜", + } + + thickBorder = Border{ + Top: "â”", + Bottom: "â”", + Left: "┃", + Right: "┃", + TopLeft: "â”", + TopRight: "┓", + BottomLeft: "â”—", + BottomRight: "â”›", + MiddleLeft: "┣", + MiddleRight: "┫", + Middle: "â•‹", + MiddleTop: "┳", + MiddleBottom: "â”»", + } + + doubleBorder = Border{ + Top: "â•", + Bottom: "â•", + Left: "â•‘", + Right: "â•‘", + TopLeft: "â•”", + TopRight: "â•—", + BottomLeft: "â•š", + BottomRight: "â•", + MiddleLeft: "â• ", + MiddleRight: "â•£", + Middle: "╬", + MiddleTop: "╦", + MiddleBottom: "â•©", + } + + hiddenBorder = Border{ + Top: " ", + Bottom: " ", + Left: " ", + Right: " ", + TopLeft: " ", + TopRight: " ", + BottomLeft: " ", + BottomRight: " ", + MiddleLeft: " ", + MiddleRight: " ", + Middle: " ", + MiddleTop: " ", + MiddleBottom: " ", + } +) + +// NormalBorder returns a standard-type border with a normal weight and 90 +// degree corners. +func NormalBorder() Border { + return normalBorder +} + +// RoundedBorder returns a border with rounded corners. +func RoundedBorder() Border { + return roundedBorder +} + +// BlockBorder returns a border that takes the whole block. +func BlockBorder() Border { + return blockBorder +} + +// OuterHalfBlockBorder returns a half-block border that sits outside the frame. +func OuterHalfBlockBorder() Border { + return outerHalfBlockBorder +} + +// InnerHalfBlockBorder returns a half-block border that sits inside the frame. +func InnerHalfBlockBorder() Border { + return innerHalfBlockBorder +} + +// ThickBorder returns a border that's thicker than the one returned by +// NormalBorder. +func ThickBorder() Border { + return thickBorder +} + +// DoubleBorder returns a border comprised of two thin strokes. +func DoubleBorder() Border { + return doubleBorder +} + +// HiddenBorder returns a border that renders as a series of single-cell +// spaces. It's useful for cases when you want to remove a standard border but +// maintain layout positioning. This said, you can still apply a background +// color to a hidden border. +func HiddenBorder() Border { + return hiddenBorder +} + +func (s Style) applyBorder(str string) string { + var ( + topSet = s.isSet(borderTopKey) + rightSet = s.isSet(borderRightKey) + bottomSet = s.isSet(borderBottomKey) + leftSet = s.isSet(borderLeftKey) + + border = s.getBorderStyle() + hasTop = s.getAsBool(borderTopKey, false) + hasRight = s.getAsBool(borderRightKey, false) + hasBottom = s.getAsBool(borderBottomKey, false) + hasLeft = s.getAsBool(borderLeftKey, false) + + topFG = s.getAsColor(borderTopForegroundKey) + rightFG = s.getAsColor(borderRightForegroundKey) + bottomFG = s.getAsColor(borderBottomForegroundKey) + leftFG = s.getAsColor(borderLeftForegroundKey) + + topBG = s.getAsColor(borderTopBackgroundKey) + rightBG = s.getAsColor(borderRightBackgroundKey) + bottomBG = s.getAsColor(borderBottomBackgroundKey) + leftBG = s.getAsColor(borderLeftBackgroundKey) + ) + + // If a border is set and no sides have been specifically turned on or off + // render borders on all sides. + if border != noBorder && !(topSet || rightSet || bottomSet || leftSet) { + hasTop = true + hasRight = true + hasBottom = true + hasLeft = true + } + + // If no border is set or all borders are been disabled, abort. + if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) { + return str + } + + lines, width := getLines(str) + + if hasLeft { + if border.Left == "" { + border.Left = " " + } + width += maxRuneWidth(border.Left) + } + + if hasRight && border.Right == "" { + border.Right = " " + } + + // If corners should be rendered but are set with the empty string, fill them + // with a single space. + if hasTop && hasLeft && border.TopLeft == "" { + border.TopLeft = " " + } + if hasTop && hasRight && border.TopRight == "" { + border.TopRight = " " + } + if hasBottom && hasLeft && border.BottomLeft == "" { + border.BottomLeft = " " + } + if hasBottom && hasRight && border.BottomRight == "" { + border.BottomRight = " " + } + + // Figure out which corners we should actually be using based on which + // sides are set to show. + if hasTop { + switch { + case !hasLeft && !hasRight: + border.TopLeft = "" + border.TopRight = "" + case !hasLeft: + border.TopLeft = "" + case !hasRight: + border.TopRight = "" + } + } + if hasBottom { + switch { + case !hasLeft && !hasRight: + border.BottomLeft = "" + border.BottomRight = "" + case !hasLeft: + border.BottomLeft = "" + case !hasRight: + border.BottomRight = "" + } + } + + // For now, limit corners to one rune. + border.TopLeft = getFirstRuneAsString(border.TopLeft) + border.TopRight = getFirstRuneAsString(border.TopRight) + border.BottomRight = getFirstRuneAsString(border.BottomRight) + border.BottomLeft = getFirstRuneAsString(border.BottomLeft) + + var out strings.Builder + + // Render top + if hasTop { + top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width) + top = s.styleBorder(top, topFG, topBG) + out.WriteString(top) + out.WriteRune('\n') + } + + leftRunes := []rune(border.Left) + leftIndex := 0 + + rightRunes := []rune(border.Right) + rightIndex := 0 + + // Render sides + for i, l := range lines { + if hasLeft { + r := string(leftRunes[leftIndex]) + leftIndex++ + if leftIndex >= len(leftRunes) { + leftIndex = 0 + } + out.WriteString(s.styleBorder(r, leftFG, leftBG)) + } + out.WriteString(l) + if hasRight { + r := string(rightRunes[rightIndex]) + rightIndex++ + if rightIndex >= len(rightRunes) { + rightIndex = 0 + } + out.WriteString(s.styleBorder(r, rightFG, rightBG)) + } + if i < len(lines)-1 { + out.WriteRune('\n') + } + } + + // Render bottom + if hasBottom { + bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width) + bottom = s.styleBorder(bottom, bottomFG, bottomBG) + out.WriteRune('\n') + out.WriteString(bottom) + } + + return out.String() +} + +// Render the horizontal (top or bottom) portion of a border. +func renderHorizontalEdge(left, middle, right string, width int) string { + if middle == "" { + middle = " " + } + + leftWidth := ansi.StringWidth(left) + rightWidth := ansi.StringWidth(right) + + runes := []rune(middle) + j := 0 + + out := strings.Builder{} + out.WriteString(left) + for i := leftWidth + rightWidth; i < width+rightWidth; { + out.WriteRune(runes[j]) + j++ + if j >= len(runes) { + j = 0 + } + i += ansi.StringWidth(string(runes[j])) + } + out.WriteString(right) + + return out.String() +} + +// Apply foreground and background styling to a border. +func (s Style) styleBorder(border string, fg, bg TerminalColor) string { + if fg == noColor && bg == noColor { + return border + } + + style := termenv.Style{} + + if fg != noColor { + style = style.Foreground(fg.color(s.r)) + } + if bg != noColor { + style = style.Background(bg.color(s.r)) + } + + return style.Styled(border) +} + +func maxRuneWidth(str string) int { + var width int + + state := -1 + for len(str) > 0 { + var w int + _, str, w, state = uniseg.FirstGraphemeClusterInString(str, state) + if w > width { + width = w + } + } + + return width +} + +func getFirstRuneAsString(str string) string { + if str == "" { + return str + } + r := []rune(str) + return string(r[0]) +} diff --git a/vendor/github.com/charmbracelet/lipgloss/color.go b/vendor/github.com/charmbracelet/lipgloss/color.go new file mode 100644 index 00000000..5dfb3cfe --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/color.go @@ -0,0 +1,172 @@ +package lipgloss + +import ( + "strconv" + + "github.com/muesli/termenv" +) + +// TerminalColor is a color intended to be rendered in the terminal. +type TerminalColor interface { + color(*Renderer) termenv.Color + RGBA() (r, g, b, a uint32) +} + +var noColor = NoColor{} + +// NoColor is used to specify the absence of color styling. When this is active +// foreground colors will be rendered with the terminal's default text color, +// and background colors will not be drawn at all. +// +// Example usage: +// +// var style = someStyle.Background(lipgloss.NoColor{}) +type NoColor struct{} + +func (NoColor) color(*Renderer) termenv.Color { + return termenv.NoColor{} +} + +// RGBA returns the RGBA value of this color. Because we have to return +// something, despite this color being the absence of color, we're returning +// black with 100% opacity. +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (n NoColor) RGBA() (r, g, b, a uint32) { + return 0x0, 0x0, 0x0, 0xFFFF //nolint:gomnd +} + +// Color specifies a color by hex or ANSI value. For example: +// +// ansiColor := lipgloss.Color("21") +// hexColor := lipgloss.Color("#0000ff") +type Color string + +func (c Color) color(r *Renderer) termenv.Color { + return r.ColorProfile().Color(string(c)) +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (c Color) RGBA() (r, g, b, a uint32) { + return termenv.ConvertToRGB(c.color(renderer)).RGBA() +} + +// ANSIColor is a color specified by an ANSI color value. It's merely syntactic +// sugar for the more general Color function. Invalid colors will render as +// black. +// +// Example usage: +// +// // These two statements are equivalent. +// colorA := lipgloss.ANSIColor(21) +// colorB := lipgloss.Color("21") +type ANSIColor uint + +func (ac ANSIColor) color(r *Renderer) termenv.Color { + return Color(strconv.FormatUint(uint64(ac), 10)).color(r) +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (ac ANSIColor) RGBA() (r, g, b, a uint32) { + cf := Color(strconv.FormatUint(uint64(ac), 10)) + return cf.RGBA() +} + +// AdaptiveColor provides color options for light and dark backgrounds. The +// appropriate color will be returned at runtime based on the darkness of the +// terminal background color. +// +// Example usage: +// +// color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"} +type AdaptiveColor struct { + Light string + Dark string +} + +func (ac AdaptiveColor) color(r *Renderer) termenv.Color { + if r.HasDarkBackground() { + return Color(ac.Dark).color(r) + } + return Color(ac.Light).color(r) +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) { + return termenv.ConvertToRGB(ac.color(renderer)).RGBA() +} + +// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color +// profiles. Automatic color degradation will not be performed. +type CompleteColor struct { + TrueColor string + ANSI256 string + ANSI string +} + +func (c CompleteColor) color(r *Renderer) termenv.Color { + p := r.ColorProfile() + switch p { //nolint:exhaustive + case termenv.TrueColor: + return p.Color(c.TrueColor) + case termenv.ANSI256: + return p.Color(c.ANSI256) + case termenv.ANSI: + return p.Color(c.ANSI) + default: + return termenv.NoColor{} + } +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color +// +// Deprecated. +func (c CompleteColor) RGBA() (r, g, b, a uint32) { + return termenv.ConvertToRGB(c.color(renderer)).RGBA() +} + +// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color +// profiles, with separate options for light and dark backgrounds. Automatic +// color degradation will not be performed. +type CompleteAdaptiveColor struct { + Light CompleteColor + Dark CompleteColor +} + +func (cac CompleteAdaptiveColor) color(r *Renderer) termenv.Color { + if r.HasDarkBackground() { + return cac.Dark.color(r) + } + return cac.Light.color(r) +} + +// RGBA returns the RGBA value of this color. This satisfies the Go Color +// interface. Note that on error we return black with 100% opacity, or: +// +// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF. +// +// Deprecated. +func (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) { + return termenv.ConvertToRGB(cac.color(renderer)).RGBA() +} diff --git a/vendor/github.com/charmbracelet/lipgloss/get.go b/vendor/github.com/charmbracelet/lipgloss/get.go new file mode 100644 index 00000000..9c2f06fe --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/get.go @@ -0,0 +1,542 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/ansi" +) + +// GetBold returns the style's bold value. If no value is set false is returned. +func (s Style) GetBold() bool { + return s.getAsBool(boldKey, false) +} + +// GetItalic returns the style's italic value. If no value is set false is +// returned. +func (s Style) GetItalic() bool { + return s.getAsBool(italicKey, false) +} + +// GetUnderline returns the style's underline value. If no value is set false is +// returned. +func (s Style) GetUnderline() bool { + return s.getAsBool(underlineKey, false) +} + +// GetStrikethrough returns the style's strikethrough value. If no value is set false +// is returned. +func (s Style) GetStrikethrough() bool { + return s.getAsBool(strikethroughKey, false) +} + +// GetReverse returns the style's reverse value. If no value is set false is +// returned. +func (s Style) GetReverse() bool { + return s.getAsBool(reverseKey, false) +} + +// GetBlink returns the style's blink value. If no value is set false is +// returned. +func (s Style) GetBlink() bool { + return s.getAsBool(blinkKey, false) +} + +// GetFaint returns the style's faint value. If no value is set false is +// returned. +func (s Style) GetFaint() bool { + return s.getAsBool(faintKey, false) +} + +// GetForeground returns the style's foreground color. If no value is set +// NoColor{} is returned. +func (s Style) GetForeground() TerminalColor { + return s.getAsColor(foregroundKey) +} + +// GetBackground returns the style's background color. If no value is set +// NoColor{} is returned. +func (s Style) GetBackground() TerminalColor { + return s.getAsColor(backgroundKey) +} + +// GetWidth returns the style's width setting. If no width is set 0 is +// returned. +func (s Style) GetWidth() int { + return s.getAsInt(widthKey) +} + +// GetHeight returns the style's height setting. If no height is set 0 is +// returned. +func (s Style) GetHeight() int { + return s.getAsInt(heightKey) +} + +// GetAlign returns the style's implicit horizontal alignment setting. +// If no alignment is set Position.Left is returned. +func (s Style) GetAlign() Position { + v := s.getAsPosition(alignHorizontalKey) + if v == Position(0) { + return Left + } + return v +} + +// GetAlignHorizontal returns the style's implicit horizontal alignment setting. +// If no alignment is set Position.Left is returned. +func (s Style) GetAlignHorizontal() Position { + v := s.getAsPosition(alignHorizontalKey) + if v == Position(0) { + return Left + } + return v +} + +// GetAlignVertical returns the style's implicit vertical alignment setting. +// If no alignment is set Position.Top is returned. +func (s Style) GetAlignVertical() Position { + v := s.getAsPosition(alignVerticalKey) + if v == Position(0) { + return Top + } + return v +} + +// GetPadding returns the style's top, right, bottom, and left padding values, +// in that order. 0 is returned for unset values. +func (s Style) GetPadding() (top, right, bottom, left int) { + return s.getAsInt(paddingTopKey), + s.getAsInt(paddingRightKey), + s.getAsInt(paddingBottomKey), + s.getAsInt(paddingLeftKey) +} + +// GetPaddingTop returns the style's top padding. If no value is set 0 is +// returned. +func (s Style) GetPaddingTop() int { + return s.getAsInt(paddingTopKey) +} + +// GetPaddingRight returns the style's right padding. If no value is set 0 is +// returned. +func (s Style) GetPaddingRight() int { + return s.getAsInt(paddingRightKey) +} + +// GetPaddingBottom returns the style's bottom padding. If no value is set 0 is +// returned. +func (s Style) GetPaddingBottom() int { + return s.getAsInt(paddingBottomKey) +} + +// GetPaddingLeft returns the style's left padding. If no value is set 0 is +// returned. +func (s Style) GetPaddingLeft() int { + return s.getAsInt(paddingLeftKey) +} + +// GetHorizontalPadding returns the style's left and right padding. Unset +// values are measured as 0. +func (s Style) GetHorizontalPadding() int { + return s.getAsInt(paddingLeftKey) + s.getAsInt(paddingRightKey) +} + +// GetVerticalPadding returns the style's top and bottom padding. Unset values +// are measured as 0. +func (s Style) GetVerticalPadding() int { + return s.getAsInt(paddingTopKey) + s.getAsInt(paddingBottomKey) +} + +// GetColorWhitespace returns the style's whitespace coloring setting. If no +// value is set false is returned. +func (s Style) GetColorWhitespace() bool { + return s.getAsBool(colorWhitespaceKey, false) +} + +// GetMargin returns the style's top, right, bottom, and left margins, in that +// order. 0 is returned for unset values. +func (s Style) GetMargin() (top, right, bottom, left int) { + return s.getAsInt(marginTopKey), + s.getAsInt(marginRightKey), + s.getAsInt(marginBottomKey), + s.getAsInt(marginLeftKey) +} + +// GetMarginTop returns the style's top margin. If no value is set 0 is +// returned. +func (s Style) GetMarginTop() int { + return s.getAsInt(marginTopKey) +} + +// GetMarginRight returns the style's right margin. If no value is set 0 is +// returned. +func (s Style) GetMarginRight() int { + return s.getAsInt(marginRightKey) +} + +// GetMarginBottom returns the style's bottom margin. If no value is set 0 is +// returned. +func (s Style) GetMarginBottom() int { + return s.getAsInt(marginBottomKey) +} + +// GetMarginLeft returns the style's left margin. If no value is set 0 is +// returned. +func (s Style) GetMarginLeft() int { + return s.getAsInt(marginLeftKey) +} + +// GetHorizontalMargins returns the style's left and right margins. Unset +// values are measured as 0. +func (s Style) GetHorizontalMargins() int { + return s.getAsInt(marginLeftKey) + s.getAsInt(marginRightKey) +} + +// GetVerticalMargins returns the style's top and bottom margins. Unset values +// are measured as 0. +func (s Style) GetVerticalMargins() int { + return s.getAsInt(marginTopKey) + s.getAsInt(marginBottomKey) +} + +// GetBorder returns the style's border style (type Border) and value for the +// top, right, bottom, and left in that order. If no value is set for the +// border style, Border{} is returned. For all other unset values false is +// returned. +func (s Style) GetBorder() (b Border, top, right, bottom, left bool) { + return s.getBorderStyle(), + s.getAsBool(borderTopKey, false), + s.getAsBool(borderRightKey, false), + s.getAsBool(borderBottomKey, false), + s.getAsBool(borderLeftKey, false) +} + +// GetBorderStyle returns the style's border style (type Border). If no value +// is set Border{} is returned. +func (s Style) GetBorderStyle() Border { + return s.getBorderStyle() +} + +// GetBorderTop returns the style's top border setting. If no value is set +// false is returned. +func (s Style) GetBorderTop() bool { + return s.getAsBool(borderTopKey, false) +} + +// GetBorderRight returns the style's right border setting. If no value is set +// false is returned. +func (s Style) GetBorderRight() bool { + return s.getAsBool(borderRightKey, false) +} + +// GetBorderBottom returns the style's bottom border setting. If no value is +// set false is returned. +func (s Style) GetBorderBottom() bool { + return s.getAsBool(borderBottomKey, false) +} + +// GetBorderLeft returns the style's left border setting. If no value is +// set false is returned. +func (s Style) GetBorderLeft() bool { + return s.getAsBool(borderLeftKey, false) +} + +// GetBorderTopForeground returns the style's border top foreground color. If +// no value is set NoColor{} is returned. +func (s Style) GetBorderTopForeground() TerminalColor { + return s.getAsColor(borderTopForegroundKey) +} + +// GetBorderRightForeground returns the style's border right foreground color. +// If no value is set NoColor{} is returned. +func (s Style) GetBorderRightForeground() TerminalColor { + return s.getAsColor(borderRightForegroundKey) +} + +// GetBorderBottomForeground returns the style's border bottom foreground +// color. If no value is set NoColor{} is returned. +func (s Style) GetBorderBottomForeground() TerminalColor { + return s.getAsColor(borderBottomForegroundKey) +} + +// GetBorderLeftForeground returns the style's border left foreground +// color. If no value is set NoColor{} is returned. +func (s Style) GetBorderLeftForeground() TerminalColor { + return s.getAsColor(borderLeftForegroundKey) +} + +// GetBorderTopBackground returns the style's border top background color. If +// no value is set NoColor{} is returned. +func (s Style) GetBorderTopBackground() TerminalColor { + return s.getAsColor(borderTopBackgroundKey) +} + +// GetBorderRightBackground returns the style's border right background color. +// If no value is set NoColor{} is returned. +func (s Style) GetBorderRightBackground() TerminalColor { + return s.getAsColor(borderRightBackgroundKey) +} + +// GetBorderBottomBackground returns the style's border bottom background +// color. If no value is set NoColor{} is returned. +func (s Style) GetBorderBottomBackground() TerminalColor { + return s.getAsColor(borderBottomBackgroundKey) +} + +// GetBorderLeftBackground returns the style's border left background +// color. If no value is set NoColor{} is returned. +func (s Style) GetBorderLeftBackground() TerminalColor { + return s.getAsColor(borderLeftBackgroundKey) +} + +// GetBorderTopWidth returns the width of the top border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the top edge, 0 is returned. +// +// Deprecated: This function simply calls Style.GetBorderTopSize. +func (s Style) GetBorderTopWidth() int { + return s.GetBorderTopSize() +} + +// GetBorderTopSize returns the width of the top border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the top edge, 0 is returned. +func (s Style) GetBorderTopSize() int { + if !s.getAsBool(borderTopKey, false) { + return 0 + } + return s.getBorderStyle().GetTopSize() +} + +// GetBorderLeftSize returns the width of the left border. If borders contain +// runes of varying widths, the widest rune is returned. If no border exists on +// the left edge, 0 is returned. +func (s Style) GetBorderLeftSize() int { + if !s.getAsBool(borderLeftKey, false) { + return 0 + } + return s.getBorderStyle().GetLeftSize() +} + +// GetBorderBottomSize returns the width of the bottom border. If borders +// contain runes of varying widths, the widest rune is returned. If no border +// exists on the left edge, 0 is returned. +func (s Style) GetBorderBottomSize() int { + if !s.getAsBool(borderBottomKey, false) { + return 0 + } + return s.getBorderStyle().GetBottomSize() +} + +// GetBorderRightSize returns the width of the right border. If borders +// contain runes of varying widths, the widest rune is returned. If no border +// exists on the right edge, 0 is returned. +func (s Style) GetBorderRightSize() int { + if !s.getAsBool(borderRightKey, false) { + return 0 + } + return s.getBorderStyle().GetRightSize() +} + +// GetHorizontalBorderSize returns the width of the horizontal borders. If +// borders contain runes of varying widths, the widest rune is returned. If no +// border exists on the horizontal edges, 0 is returned. +func (s Style) GetHorizontalBorderSize() int { + return s.GetBorderLeftSize() + s.GetBorderRightSize() +} + +// GetVerticalBorderSize returns the width of the vertical borders. If +// borders contain runes of varying widths, the widest rune is returned. If no +// border exists on the vertical edges, 0 is returned. +func (s Style) GetVerticalBorderSize() int { + return s.GetBorderTopSize() + s.GetBorderBottomSize() +} + +// GetInline returns the style's inline setting. If no value is set false is +// returned. +func (s Style) GetInline() bool { + return s.getAsBool(inlineKey, false) +} + +// GetMaxWidth returns the style's max width setting. If no value is set 0 is +// returned. +func (s Style) GetMaxWidth() int { + return s.getAsInt(maxWidthKey) +} + +// GetMaxHeight returns the style's max height setting. If no value is set 0 is +// returned. +func (s Style) GetMaxHeight() int { + return s.getAsInt(maxHeightKey) +} + +// GetTabWidth returns the style's tab width setting. If no value is set 4 is +// returned which is the implicit default. +func (s Style) GetTabWidth() int { + return s.getAsInt(tabWidthKey) +} + +// GetUnderlineSpaces returns whether or not the style is set to underline +// spaces. If not value is set false is returned. +func (s Style) GetUnderlineSpaces() bool { + return s.getAsBool(underlineSpacesKey, false) +} + +// GetStrikethroughSpaces returns whether or not the style is set to strikethrough +// spaces. If not value is set false is returned. +func (s Style) GetStrikethroughSpaces() bool { + return s.getAsBool(strikethroughSpacesKey, false) +} + +// GetHorizontalFrameSize returns the sum of the style's horizontal margins, padding +// and border widths. +// +// Provisional: this method may be renamed. +func (s Style) GetHorizontalFrameSize() int { + return s.GetHorizontalMargins() + s.GetHorizontalPadding() + s.GetHorizontalBorderSize() +} + +// GetVerticalFrameSize returns the sum of the style's vertical margins, padding +// and border widths. +// +// Provisional: this method may be renamed. +func (s Style) GetVerticalFrameSize() int { + return s.GetVerticalMargins() + s.GetVerticalPadding() + s.GetVerticalBorderSize() +} + +// GetFrameSize returns the sum of the margins, padding and border width for +// both the horizontal and vertical margins. +func (s Style) GetFrameSize() (x, y int) { + return s.GetHorizontalFrameSize(), s.GetVerticalFrameSize() +} + +// GetTransform returns the transform set on the style. If no transform is set +// nil is returned. +func (s Style) GetTransform() func(string) string { + return s.getAsTransform(transformKey) +} + +// Returns whether or not the given property is set. +func (s Style) isSet(k propKey) bool { + return s.props.has(k) +} + +func (s Style) getAsBool(k propKey, defaultVal bool) bool { + if !s.isSet(k) { + return defaultVal + } + return s.attrs&int(k) != 0 +} + +func (s Style) getAsColor(k propKey) TerminalColor { + if !s.isSet(k) { + return noColor + } + + var c TerminalColor + switch k { //nolint:exhaustive + case foregroundKey: + c = s.fgColor + case backgroundKey: + c = s.bgColor + case marginBackgroundKey: + c = s.marginBgColor + case borderTopForegroundKey: + c = s.borderTopFgColor + case borderRightForegroundKey: + c = s.borderRightFgColor + case borderBottomForegroundKey: + c = s.borderBottomFgColor + case borderLeftForegroundKey: + c = s.borderLeftFgColor + case borderTopBackgroundKey: + c = s.borderTopBgColor + case borderRightBackgroundKey: + c = s.borderRightBgColor + case borderBottomBackgroundKey: + c = s.borderBottomBgColor + case borderLeftBackgroundKey: + c = s.borderLeftBgColor + } + + if c != nil { + return c + } + + return noColor +} + +func (s Style) getAsInt(k propKey) int { + if !s.isSet(k) { + return 0 + } + switch k { //nolint:exhaustive + case widthKey: + return s.width + case heightKey: + return s.height + case paddingTopKey: + return s.paddingTop + case paddingRightKey: + return s.paddingRight + case paddingBottomKey: + return s.paddingBottom + case paddingLeftKey: + return s.paddingLeft + case marginTopKey: + return s.marginTop + case marginRightKey: + return s.marginRight + case marginBottomKey: + return s.marginBottom + case marginLeftKey: + return s.marginLeft + case maxWidthKey: + return s.maxWidth + case maxHeightKey: + return s.maxHeight + case tabWidthKey: + return s.tabWidth + } + return 0 +} + +func (s Style) getAsPosition(k propKey) Position { + if !s.isSet(k) { + return Position(0) + } + switch k { //nolint:exhaustive + case alignHorizontalKey: + return s.alignHorizontal + case alignVerticalKey: + return s.alignVertical + } + return Position(0) +} + +func (s Style) getBorderStyle() Border { + if !s.isSet(borderStyleKey) { + return noBorder + } + return s.borderStyle +} + +func (s Style) getAsTransform(propKey) func(string) string { + if !s.isSet(transformKey) { + return nil + } + return s.transform +} + +// Split a string into lines, additionally returning the size of the widest +// line. +func getLines(s string) (lines []string, widest int) { + lines = strings.Split(s, "\n") + + for _, l := range lines { + w := ansi.StringWidth(l) + if widest < w { + widest = w + } + } + + return lines, widest +} diff --git a/vendor/github.com/charmbracelet/lipgloss/join.go b/vendor/github.com/charmbracelet/lipgloss/join.go new file mode 100644 index 00000000..b0a23a54 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/join.go @@ -0,0 +1,175 @@ +package lipgloss + +import ( + "math" + "strings" + + "github.com/charmbracelet/x/ansi" +) + +// JoinHorizontal is a utility function for horizontally joining two +// potentially multi-lined strings along a vertical axis. The first argument is +// the position, with 0 being all the way at the top and 1 being all the way +// at the bottom. +// +// If you just want to align to the top, center or bottom you may as well just +// use the helper constants Top, Center, and Bottom. +// +// Example: +// +// blockB := "...\n...\n..." +// blockA := "...\n...\n...\n...\n..." +// +// // Join 20% from the top +// str := lipgloss.JoinHorizontal(0.2, blockA, blockB) +// +// // Join on the top edge +// str := lipgloss.JoinHorizontal(lipgloss.Top, blockA, blockB) +func JoinHorizontal(pos Position, strs ...string) string { + if len(strs) == 0 { + return "" + } + if len(strs) == 1 { + return strs[0] + } + + var ( + // Groups of strings broken into multiple lines + blocks = make([][]string, len(strs)) + + // Max line widths for the above text blocks + maxWidths = make([]int, len(strs)) + + // Height of the tallest block + maxHeight int + ) + + // Break text blocks into lines and get max widths for each text block + for i, str := range strs { + blocks[i], maxWidths[i] = getLines(str) + if len(blocks[i]) > maxHeight { + maxHeight = len(blocks[i]) + } + } + + // Add extra lines to make each side the same height + for i := range blocks { + if len(blocks[i]) >= maxHeight { + continue + } + + extraLines := make([]string, maxHeight-len(blocks[i])) + + switch pos { //nolint:exhaustive + case Top: + blocks[i] = append(blocks[i], extraLines...) + + case Bottom: + blocks[i] = append(extraLines, blocks[i]...) + + default: // Somewhere in the middle + n := len(extraLines) + split := int(math.Round(float64(n) * pos.value())) + top := n - split + bottom := n - top + + blocks[i] = append(extraLines[top:], blocks[i]...) + blocks[i] = append(blocks[i], extraLines[bottom:]...) + } + } + + // Merge lines + var b strings.Builder + for i := range blocks[0] { // remember, all blocks have the same number of members now + for j, block := range blocks { + b.WriteString(block[i]) + + // Also make lines the same length + b.WriteString(strings.Repeat(" ", maxWidths[j]-ansi.StringWidth(block[i]))) + } + if i < len(blocks[0])-1 { + b.WriteRune('\n') + } + } + + return b.String() +} + +// JoinVertical is a utility function for vertically joining two potentially +// multi-lined strings along a horizontal axis. The first argument is the +// position, with 0 being all the way to the left and 1 being all the way to +// the right. +// +// If you just want to align to the left, right or center you may as well just +// use the helper constants Left, Center, and Right. +// +// Example: +// +// blockB := "...\n...\n..." +// blockA := "...\n...\n...\n...\n..." +// +// // Join 20% from the top +// str := lipgloss.JoinVertical(0.2, blockA, blockB) +// +// // Join on the right edge +// str := lipgloss.JoinVertical(lipgloss.Right, blockA, blockB) +func JoinVertical(pos Position, strs ...string) string { + if len(strs) == 0 { + return "" + } + if len(strs) == 1 { + return strs[0] + } + + var ( + blocks = make([][]string, len(strs)) + maxWidth int + ) + + for i := range strs { + var w int + blocks[i], w = getLines(strs[i]) + if w > maxWidth { + maxWidth = w + } + } + + var b strings.Builder + for i, block := range blocks { + for j, line := range block { + w := maxWidth - ansi.StringWidth(line) + + switch pos { //nolint:exhaustive + case Left: + b.WriteString(line) + b.WriteString(strings.Repeat(" ", w)) + + case Right: + b.WriteString(strings.Repeat(" ", w)) + b.WriteString(line) + + default: // Somewhere in the middle + if w < 1 { + b.WriteString(line) + break + } + + split := int(math.Round(float64(w) * pos.value())) + right := w - split + left := w - right + + b.WriteString(strings.Repeat(" ", left)) + b.WriteString(line) + b.WriteString(strings.Repeat(" ", right)) + } + + // Write a newline as long as we're not on the last line of the + // last block. + if !(i == len(blocks)-1 && j == len(block)-1) { + b.WriteRune('\n') + } + } + } + + return b.String() +} diff --git a/vendor/github.com/charmbracelet/lipgloss/position.go b/vendor/github.com/charmbracelet/lipgloss/position.go new file mode 100644 index 00000000..185f5af3 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/position.go @@ -0,0 +1,154 @@ +package lipgloss + +import ( + "math" + "strings" + + "github.com/charmbracelet/x/ansi" +) + +// Position represents a position along a horizontal or vertical axis. It's in +// situations where an axis is involved, like alignment, joining, placement and +// so on. +// +// A value of 0 represents the start (the left or top) and 1 represents the end +// (the right or bottom). 0.5 represents the center. +// +// There are constants Top, Bottom, Center, Left and Right in this package that +// can be used to aid readability. +type Position float64 + +func (p Position) value() float64 { + return math.Min(1, math.Max(0, float64(p))) +} + +// Position aliases. +const ( + Top Position = 0.0 + Bottom Position = 1.0 + Center Position = 0.5 + Left Position = 0.0 + Right Position = 1.0 +) + +// Place places a string or text block vertically in an unstyled box of a given +// width or height. +func Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string { + return renderer.Place(width, height, hPos, vPos, str, opts...) +} + +// Place places a string or text block vertically in an unstyled box of a given +// width or height. +func (r *Renderer) Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string { + return r.PlaceVertical(height, vPos, r.PlaceHorizontal(width, hPos, str, opts...), opts...) +} + +// PlaceHorizontal places a string or text block horizontally in an unstyled +// block of a given width. If the given width is shorter than the max width of +// the string (measured by its longest line) this will be a noop. +func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string { + return renderer.PlaceHorizontal(width, pos, str, opts...) +} + +// PlaceHorizontal places a string or text block horizontally in an unstyled +// block of a given width. If the given width is shorter than the max width of +// the string (measured by its longest line) this will be a noöp. +func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string { + lines, contentWidth := getLines(str) + gap := width - contentWidth + + if gap <= 0 { + return str + } + + ws := newWhitespace(r, opts...) + + var b strings.Builder + for i, l := range lines { + // Is this line shorter than the longest line? + short := max(0, contentWidth-ansi.StringWidth(l)) + + switch pos { //nolint:exhaustive + case Left: + b.WriteString(l) + b.WriteString(ws.render(gap + short)) + + case Right: + b.WriteString(ws.render(gap + short)) + b.WriteString(l) + + default: // somewhere in the middle + totalGap := gap + short + + split := int(math.Round(float64(totalGap) * pos.value())) + left := totalGap - split + right := totalGap - left + + b.WriteString(ws.render(left)) + b.WriteString(l) + b.WriteString(ws.render(right)) + } + + if i < len(lines)-1 { + b.WriteRune('\n') + } + } + + return b.String() +} + +// PlaceVertical places a string or text block vertically in an unstyled block +// of a given height. If the given height is shorter than the height of the +// string (measured by its newlines) then this will be a noop. +func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string { + return renderer.PlaceVertical(height, pos, str, opts...) +} + +// PlaceVertical places a string or text block vertically in an unstyled block +// of a given height. If the given height is shorter than the height of the +// string (measured by its newlines) then this will be a noöp. +func (r *Renderer) PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string { + contentHeight := strings.Count(str, "\n") + 1 + gap := height - contentHeight + + if gap <= 0 { + return str + } + + ws := newWhitespace(r, opts...) + + _, width := getLines(str) + emptyLine := ws.render(width) + b := strings.Builder{} + + switch pos { //nolint:exhaustive + case Top: + b.WriteString(str) + b.WriteRune('\n') + for i := 0; i < gap; i++ { + b.WriteString(emptyLine) + if i < gap-1 { + b.WriteRune('\n') + } + } + + case Bottom: + b.WriteString(strings.Repeat(emptyLine+"\n", gap)) + b.WriteString(str) + + default: // Somewhere in the middle + split := int(math.Round(float64(gap) * pos.value())) + top := gap - split + bottom := gap - top + + b.WriteString(strings.Repeat(emptyLine+"\n", top)) + b.WriteString(str) + + for i := 0; i < bottom; i++ { + b.WriteRune('\n') + b.WriteString(emptyLine) + } + } + + return b.String() +} diff --git a/vendor/github.com/charmbracelet/lipgloss/renderer.go b/vendor/github.com/charmbracelet/lipgloss/renderer.go new file mode 100644 index 00000000..233aa7c0 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/renderer.go @@ -0,0 +1,181 @@ +package lipgloss + +import ( + "io" + "sync" + + "github.com/muesli/termenv" +) + +// We're manually creating the struct here to avoid initializing the output and +// query the terminal multiple times. +var renderer = &Renderer{ + output: termenv.DefaultOutput(), +} + +// Renderer is a lipgloss terminal renderer. +type Renderer struct { + output *termenv.Output + colorProfile termenv.Profile + hasDarkBackground bool + + getColorProfile sync.Once + explicitColorProfile bool + + getBackgroundColor sync.Once + explicitBackgroundColor bool + + mtx sync.RWMutex +} + +// DefaultRenderer returns the default renderer. +func DefaultRenderer() *Renderer { + return renderer +} + +// SetDefaultRenderer sets the default global renderer. +func SetDefaultRenderer(r *Renderer) { + renderer = r +} + +// NewRenderer creates a new Renderer. +// +// w will be used to determine the terminal's color capabilities. +func NewRenderer(w io.Writer, opts ...termenv.OutputOption) *Renderer { + r := &Renderer{ + output: termenv.NewOutput(w, opts...), + } + return r +} + +// Output returns the termenv output. +func (r *Renderer) Output() *termenv.Output { + r.mtx.RLock() + defer r.mtx.RUnlock() + return r.output +} + +// SetOutput sets the termenv output. +func (r *Renderer) SetOutput(o *termenv.Output) { + r.mtx.Lock() + defer r.mtx.Unlock() + r.output = o +} + +// ColorProfile returns the detected termenv color profile. +func (r *Renderer) ColorProfile() termenv.Profile { + r.mtx.RLock() + defer r.mtx.RUnlock() + + if !r.explicitColorProfile { + r.getColorProfile.Do(func() { + // NOTE: we don't need to lock here because sync.Once provides its + // own locking mechanism. + r.colorProfile = r.output.EnvColorProfile() + }) + } + + return r.colorProfile +} + +// ColorProfile returns the detected termenv color profile. +func ColorProfile() termenv.Profile { + return renderer.ColorProfile() +} + +// SetColorProfile sets the color profile on the renderer. This function exists +// mostly for testing purposes so that you can assure you're testing against +// a specific profile. +// +// Outside of testing you likely won't want to use this function as the color +// profile will detect and cache the terminal's color capabilities and choose +// the best available profile. +// +// Available color profiles are: +// +// termenv.Ascii // no color, 1-bit +// termenv.ANSI //16 colors, 4-bit +// termenv.ANSI256 // 256 colors, 8-bit +// termenv.TrueColor // 16,777,216 colors, 24-bit +// +// This function is thread-safe. +func (r *Renderer) SetColorProfile(p termenv.Profile) { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.colorProfile = p + r.explicitColorProfile = true +} + +// SetColorProfile sets the color profile on the default renderer. This +// function exists mostly for testing purposes so that you can assure you're +// testing against a specific profile. +// +// Outside of testing you likely won't want to use this function as the color +// profile will detect and cache the terminal's color capabilities and choose +// the best available profile. +// +// Available color profiles are: +// +// termenv.Ascii // no color, 1-bit +// termenv.ANSI //16 colors, 4-bit +// termenv.ANSI256 // 256 colors, 8-bit +// termenv.TrueColor // 16,777,216 colors, 24-bit +// +// This function is thread-safe. +func SetColorProfile(p termenv.Profile) { + renderer.SetColorProfile(p) +} + +// HasDarkBackground returns whether or not the terminal has a dark background. +func HasDarkBackground() bool { + return renderer.HasDarkBackground() +} + +// HasDarkBackground returns whether or not the renderer will render to a dark +// background. A dark background can either be auto-detected, or set explicitly +// on the renderer. +func (r *Renderer) HasDarkBackground() bool { + r.mtx.RLock() + defer r.mtx.RUnlock() + + if !r.explicitBackgroundColor { + r.getBackgroundColor.Do(func() { + // NOTE: we don't need to lock here because sync.Once provides its + // own locking mechanism. + r.hasDarkBackground = r.output.HasDarkBackground() + }) + } + + return r.hasDarkBackground +} + +// SetHasDarkBackground sets the background color detection value for the +// default renderer. This function exists mostly for testing purposes so that +// you can assure you're testing against a specific background color setting. +// +// Outside of testing you likely won't want to use this function as the +// backgrounds value will be automatically detected and cached against the +// terminal's current background color setting. +// +// This function is thread-safe. +func SetHasDarkBackground(b bool) { + renderer.SetHasDarkBackground(b) +} + +// SetHasDarkBackground sets the background color detection value on the +// renderer. This function exists mostly for testing purposes so that you can +// assure you're testing against a specific background color setting. +// +// Outside of testing you likely won't want to use this function as the +// backgrounds value will be automatically detected and cached against the +// terminal's current background color setting. +// +// This function is thread-safe. +func (r *Renderer) SetHasDarkBackground(b bool) { + r.mtx.Lock() + defer r.mtx.Unlock() + + r.hasDarkBackground = b + r.explicitBackgroundColor = true +} diff --git a/vendor/github.com/charmbracelet/lipgloss/runes.go b/vendor/github.com/charmbracelet/lipgloss/runes.go new file mode 100644 index 00000000..7a49e326 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/runes.go @@ -0,0 +1,43 @@ +package lipgloss + +import ( + "strings" +) + +// StyleRunes apply a given style to runes at the given indices in the string. +// Note that you must provide styling options for both matched and unmatched +// runes. Indices out of bounds will be ignored. +func StyleRunes(str string, indices []int, matched, unmatched Style) string { + // Convert slice of indices to a map for easier lookups + m := make(map[int]struct{}) + for _, i := range indices { + m[i] = struct{}{} + } + + var ( + out strings.Builder + group strings.Builder + style Style + runes = []rune(str) + ) + + for i, r := range runes { + group.WriteRune(r) + + _, matches := m[i] + _, nextMatches := m[i+1] + + if matches != nextMatches || i == len(runes)-1 { + // Flush + if matches { + style = matched + } else { + style = unmatched + } + out.WriteString(style.Render(group.String())) + group.Reset() + } + } + + return out.String() +} diff --git a/vendor/github.com/charmbracelet/lipgloss/set.go b/vendor/github.com/charmbracelet/lipgloss/set.go new file mode 100644 index 00000000..ed6e272c --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/set.go @@ -0,0 +1,799 @@ +package lipgloss + +// Set a value on the underlying rules map. +func (s *Style) set(key propKey, value interface{}) { + // We don't allow negative integers on any of our other values, so just keep + // them at zero or above. We could use uints instead, but the + // conversions are a little tedious, so we're sticking with ints for + // sake of usability. + switch key { //nolint:exhaustive + case foregroundKey: + s.fgColor = colorOrNil(value) + case backgroundKey: + s.bgColor = colorOrNil(value) + case widthKey: + s.width = max(0, value.(int)) + case heightKey: + s.height = max(0, value.(int)) + case alignHorizontalKey: + s.alignHorizontal = value.(Position) + case alignVerticalKey: + s.alignVertical = value.(Position) + case paddingTopKey: + s.paddingTop = max(0, value.(int)) + case paddingRightKey: + s.paddingRight = max(0, value.(int)) + case paddingBottomKey: + s.paddingBottom = max(0, value.(int)) + case paddingLeftKey: + s.paddingLeft = max(0, value.(int)) + case marginTopKey: + s.marginTop = max(0, value.(int)) + case marginRightKey: + s.marginRight = max(0, value.(int)) + case marginBottomKey: + s.marginBottom = max(0, value.(int)) + case marginLeftKey: + s.marginLeft = max(0, value.(int)) + case marginBackgroundKey: + s.marginBgColor = colorOrNil(value) + case borderStyleKey: + s.borderStyle = value.(Border) + case borderTopForegroundKey: + s.borderTopFgColor = colorOrNil(value) + case borderRightForegroundKey: + s.borderRightFgColor = colorOrNil(value) + case borderBottomForegroundKey: + s.borderBottomFgColor = colorOrNil(value) + case borderLeftForegroundKey: + s.borderLeftFgColor = colorOrNil(value) + case borderTopBackgroundKey: + s.borderTopBgColor = colorOrNil(value) + case borderRightBackgroundKey: + s.borderRightBgColor = colorOrNil(value) + case borderBottomBackgroundKey: + s.borderBottomBgColor = colorOrNil(value) + case borderLeftBackgroundKey: + s.borderLeftBgColor = colorOrNil(value) + case maxWidthKey: + s.maxWidth = max(0, value.(int)) + case maxHeightKey: + s.maxHeight = max(0, value.(int)) + case tabWidthKey: + // TabWidth is the only property that may have a negative value (and + // that negative value can be no less than -1). + s.tabWidth = value.(int) + case transformKey: + s.transform = value.(func(string) string) + default: + if v, ok := value.(bool); ok { //nolint:nestif + if v { + s.attrs |= int(key) + } else { + s.attrs &^= int(key) + } + } else if attrs, ok := value.(int); ok { + // bool attrs + if attrs&int(key) != 0 { + s.attrs |= int(key) + } else { + s.attrs &^= int(key) + } + } + } + + // Set the prop on + s.props = s.props.set(key) +} + +// setFrom sets the property from another style. +func (s *Style) setFrom(key propKey, i Style) { + switch key { //nolint:exhaustive + case foregroundKey: + s.set(foregroundKey, i.fgColor) + case backgroundKey: + s.set(backgroundKey, i.bgColor) + case widthKey: + s.set(widthKey, i.width) + case heightKey: + s.set(heightKey, i.height) + case alignHorizontalKey: + s.set(alignHorizontalKey, i.alignHorizontal) + case alignVerticalKey: + s.set(alignVerticalKey, i.alignVertical) + case paddingTopKey: + s.set(paddingTopKey, i.paddingTop) + case paddingRightKey: + s.set(paddingRightKey, i.paddingRight) + case paddingBottomKey: + s.set(paddingBottomKey, i.paddingBottom) + case paddingLeftKey: + s.set(paddingLeftKey, i.paddingLeft) + case marginTopKey: + s.set(marginTopKey, i.marginTop) + case marginRightKey: + s.set(marginRightKey, i.marginRight) + case marginBottomKey: + s.set(marginBottomKey, i.marginBottom) + case marginLeftKey: + s.set(marginLeftKey, i.marginLeft) + case marginBackgroundKey: + s.set(marginBackgroundKey, i.marginBgColor) + case borderStyleKey: + s.set(borderStyleKey, i.borderStyle) + case borderTopForegroundKey: + s.set(borderTopForegroundKey, i.borderTopFgColor) + case borderRightForegroundKey: + s.set(borderRightForegroundKey, i.borderRightFgColor) + case borderBottomForegroundKey: + s.set(borderBottomForegroundKey, i.borderBottomFgColor) + case borderLeftForegroundKey: + s.set(borderLeftForegroundKey, i.borderLeftFgColor) + case borderTopBackgroundKey: + s.set(borderTopBackgroundKey, i.borderTopBgColor) + case borderRightBackgroundKey: + s.set(borderRightBackgroundKey, i.borderRightBgColor) + case borderBottomBackgroundKey: + s.set(borderBottomBackgroundKey, i.borderBottomBgColor) + case borderLeftBackgroundKey: + s.set(borderLeftBackgroundKey, i.borderLeftBgColor) + case maxWidthKey: + s.set(maxWidthKey, i.maxWidth) + case maxHeightKey: + s.set(maxHeightKey, i.maxHeight) + case tabWidthKey: + s.set(tabWidthKey, i.tabWidth) + case transformKey: + s.set(transformKey, i.transform) + default: + // Set attributes for set bool properties + s.set(key, i.attrs) + } +} + +func colorOrNil(c interface{}) TerminalColor { + if c, ok := c.(TerminalColor); ok { + return c + } + return nil +} + +// Bold sets a bold formatting rule. +func (s Style) Bold(v bool) Style { + s.set(boldKey, v) + return s +} + +// Italic sets an italic formatting rule. In some terminal emulators this will +// render with "reverse" coloring if not italic font variant is available. +func (s Style) Italic(v bool) Style { + s.set(italicKey, v) + return s +} + +// Underline sets an underline rule. By default, underlines will not be drawn on +// whitespace like margins and padding. To change this behavior set +// UnderlineSpaces. +func (s Style) Underline(v bool) Style { + s.set(underlineKey, v) + return s +} + +// Strikethrough sets a strikethrough rule. By default, strikes will not be +// drawn on whitespace like margins and padding. To change this behavior set +// StrikethroughSpaces. +func (s Style) Strikethrough(v bool) Style { + s.set(strikethroughKey, v) + return s +} + +// Reverse sets a rule for inverting foreground and background colors. +func (s Style) Reverse(v bool) Style { + s.set(reverseKey, v) + return s +} + +// Blink sets a rule for blinking foreground text. +func (s Style) Blink(v bool) Style { + s.set(blinkKey, v) + return s +} + +// Faint sets a rule for rendering the foreground color in a dimmer shade. +func (s Style) Faint(v bool) Style { + s.set(faintKey, v) + return s +} + +// Foreground sets a foreground color. +// +// // Sets the foreground to blue +// s := lipgloss.NewStyle().Foreground(lipgloss.Color("#0000ff")) +// +// // Removes the foreground color +// s.Foreground(lipgloss.NoColor) +func (s Style) Foreground(c TerminalColor) Style { + s.set(foregroundKey, c) + return s +} + +// Background sets a background color. +func (s Style) Background(c TerminalColor) Style { + s.set(backgroundKey, c) + return s +} + +// Width sets the width of the block before applying margins. The width, if +// set, also determines where text will wrap. +func (s Style) Width(i int) Style { + s.set(widthKey, i) + return s +} + +// Height sets the height of the block before applying margins. If the height of +// the text block is less than this value after applying padding (or not), the +// block will be set to this height. +func (s Style) Height(i int) Style { + s.set(heightKey, i) + return s +} + +// Align is a shorthand method for setting horizontal and vertical alignment. +// +// With one argument, the position value is applied to the horizontal alignment. +// +// With two arguments, the value is applied to the horizontal and vertical +// alignments, in that order. +func (s Style) Align(p ...Position) Style { + if len(p) > 0 { + s.set(alignHorizontalKey, p[0]) + } + if len(p) > 1 { + s.set(alignVerticalKey, p[1]) + } + return s +} + +// AlignHorizontal sets a horizontal text alignment rule. +func (s Style) AlignHorizontal(p Position) Style { + s.set(alignHorizontalKey, p) + return s +} + +// AlignVertical sets a vertical text alignment rule. +func (s Style) AlignVertical(p Position) Style { + s.set(alignVerticalKey, p) + return s +} + +// Padding is a shorthand method for setting padding on all sides at once. +// +// With one argument, the value is applied to all sides. +// +// With two arguments, the value is applied to the vertical and horizontal +// sides, in that order. +// +// With three arguments, the value is applied to the top side, the horizontal +// sides, and the bottom side, in that order. +// +// With four arguments, the value is applied clockwise starting from the top +// side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments no padding will be added. +func (s Style) Padding(i ...int) Style { + top, right, bottom, left, ok := whichSidesInt(i...) + if !ok { + return s + } + + s.set(paddingTopKey, top) + s.set(paddingRightKey, right) + s.set(paddingBottomKey, bottom) + s.set(paddingLeftKey, left) + return s +} + +// PaddingLeft adds padding on the left. +func (s Style) PaddingLeft(i int) Style { + s.set(paddingLeftKey, i) + return s +} + +// PaddingRight adds padding on the right. +func (s Style) PaddingRight(i int) Style { + s.set(paddingRightKey, i) + return s +} + +// PaddingTop adds padding to the top of the block. +func (s Style) PaddingTop(i int) Style { + s.set(paddingTopKey, i) + return s +} + +// PaddingBottom adds padding to the bottom of the block. +func (s Style) PaddingBottom(i int) Style { + s.set(paddingBottomKey, i) + return s +} + +// ColorWhitespace determines whether or not the background color should be +// applied to the padding. This is true by default as it's more than likely the +// desired and expected behavior, but it can be disabled for certain graphic +// effects. +// +// Deprecated: Just use margins and padding. +func (s Style) ColorWhitespace(v bool) Style { + s.set(colorWhitespaceKey, v) + return s +} + +// Margin is a shorthand method for setting margins on all sides at once. +// +// With one argument, the value is applied to all sides. +// +// With two arguments, the value is applied to the vertical and horizontal +// sides, in that order. +// +// With three arguments, the value is applied to the top side, the horizontal +// sides, and the bottom side, in that order. +// +// With four arguments, the value is applied clockwise starting from the top +// side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments no margin will be added. +func (s Style) Margin(i ...int) Style { + top, right, bottom, left, ok := whichSidesInt(i...) + if !ok { + return s + } + + s.set(marginTopKey, top) + s.set(marginRightKey, right) + s.set(marginBottomKey, bottom) + s.set(marginLeftKey, left) + return s +} + +// MarginLeft sets the value of the left margin. +func (s Style) MarginLeft(i int) Style { + s.set(marginLeftKey, i) + return s +} + +// MarginRight sets the value of the right margin. +func (s Style) MarginRight(i int) Style { + s.set(marginRightKey, i) + return s +} + +// MarginTop sets the value of the top margin. +func (s Style) MarginTop(i int) Style { + s.set(marginTopKey, i) + return s +} + +// MarginBottom sets the value of the bottom margin. +func (s Style) MarginBottom(i int) Style { + s.set(marginBottomKey, i) + return s +} + +// MarginBackground sets the background color of the margin. Note that this is +// also set when inheriting from a style with a background color. In that case +// the background color on that style will set the margin color on this style. +func (s Style) MarginBackground(c TerminalColor) Style { + s.set(marginBackgroundKey, c) + return s +} + +// Border is shorthand for setting the border style and which sides should +// have a border at once. The variadic argument sides works as follows: +// +// With one value, the value is applied to all sides. +// +// With two values, the values are applied to the vertical and horizontal +// sides, in that order. +// +// With three values, the values are applied to the top side, the horizontal +// sides, and the bottom side, in that order. +// +// With four values, the values are applied clockwise starting from the top +// side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments the border will be applied to all sides. +// +// Examples: +// +// // Applies borders to the top and bottom only +// lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false) +// +// // Applies rounded borders to the right and bottom only +// lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), false, true, true, false) +func (s Style) Border(b Border, sides ...bool) Style { + s.set(borderStyleKey, b) + + top, right, bottom, left, ok := whichSidesBool(sides...) + if !ok { + top = true + right = true + bottom = true + left = true + } + + s.set(borderTopKey, top) + s.set(borderRightKey, right) + s.set(borderBottomKey, bottom) + s.set(borderLeftKey, left) + + return s +} + +// BorderStyle defines the Border on a style. A Border contains a series of +// definitions for the sides and corners of a border. +// +// Note that if border visibility has not been set for any sides when setting +// the border style, the border will be enabled for all sides during rendering. +// +// You can define border characters as you'd like, though several default +// styles are included: NormalBorder(), RoundedBorder(), BlockBorder(), +// OuterHalfBlockBorder(), InnerHalfBlockBorder(), ThickBorder(), +// and DoubleBorder(). +// +// Example: +// +// lipgloss.NewStyle().BorderStyle(lipgloss.ThickBorder()) +func (s Style) BorderStyle(b Border) Style { + s.set(borderStyleKey, b) + return s +} + +// BorderTop determines whether or not to draw a top border. +func (s Style) BorderTop(v bool) Style { + s.set(borderTopKey, v) + return s +} + +// BorderRight determines whether or not to draw a right border. +func (s Style) BorderRight(v bool) Style { + s.set(borderRightKey, v) + return s +} + +// BorderBottom determines whether or not to draw a bottom border. +func (s Style) BorderBottom(v bool) Style { + s.set(borderBottomKey, v) + return s +} + +// BorderLeft determines whether or not to draw a left border. +func (s Style) BorderLeft(v bool) Style { + s.set(borderLeftKey, v) + return s +} + +// BorderForeground is a shorthand function for setting all of the +// foreground colors of the borders at once. The arguments work as follows: +// +// With one argument, the argument is applied to all sides. +// +// With two arguments, the arguments are applied to the vertical and horizontal +// sides, in that order. +// +// With three arguments, the arguments are applied to the top side, the +// horizontal sides, and the bottom side, in that order. +// +// With four arguments, the arguments are applied clockwise starting from the +// top side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments nothing will be set. +func (s Style) BorderForeground(c ...TerminalColor) Style { + if len(c) == 0 { + return s + } + + top, right, bottom, left, ok := whichSidesColor(c...) + if !ok { + return s + } + + s.set(borderTopForegroundKey, top) + s.set(borderRightForegroundKey, right) + s.set(borderBottomForegroundKey, bottom) + s.set(borderLeftForegroundKey, left) + + return s +} + +// BorderTopForeground set the foreground color for the top of the border. +func (s Style) BorderTopForeground(c TerminalColor) Style { + s.set(borderTopForegroundKey, c) + return s +} + +// BorderRightForeground sets the foreground color for the right side of the +// border. +func (s Style) BorderRightForeground(c TerminalColor) Style { + s.set(borderRightForegroundKey, c) + return s +} + +// BorderBottomForeground sets the foreground color for the bottom of the +// border. +func (s Style) BorderBottomForeground(c TerminalColor) Style { + s.set(borderBottomForegroundKey, c) + return s +} + +// BorderLeftForeground sets the foreground color for the left side of the +// border. +func (s Style) BorderLeftForeground(c TerminalColor) Style { + s.set(borderLeftForegroundKey, c) + return s +} + +// BorderBackground is a shorthand function for setting all of the +// background colors of the borders at once. The arguments work as follows: +// +// With one argument, the argument is applied to all sides. +// +// With two arguments, the arguments are applied to the vertical and horizontal +// sides, in that order. +// +// With three arguments, the arguments are applied to the top side, the +// horizontal sides, and the bottom side, in that order. +// +// With four arguments, the arguments are applied clockwise starting from the +// top side, followed by the right side, then the bottom, and finally the left. +// +// With more than four arguments nothing will be set. +func (s Style) BorderBackground(c ...TerminalColor) Style { + if len(c) == 0 { + return s + } + + top, right, bottom, left, ok := whichSidesColor(c...) + if !ok { + return s + } + + s.set(borderTopBackgroundKey, top) + s.set(borderRightBackgroundKey, right) + s.set(borderBottomBackgroundKey, bottom) + s.set(borderLeftBackgroundKey, left) + + return s +} + +// BorderTopBackground sets the background color of the top of the border. +func (s Style) BorderTopBackground(c TerminalColor) Style { + s.set(borderTopBackgroundKey, c) + return s +} + +// BorderRightBackground sets the background color of right side the border. +func (s Style) BorderRightBackground(c TerminalColor) Style { + s.set(borderRightBackgroundKey, c) + return s +} + +// BorderBottomBackground sets the background color of the bottom of the +// border. +func (s Style) BorderBottomBackground(c TerminalColor) Style { + s.set(borderBottomBackgroundKey, c) + return s +} + +// BorderLeftBackground set the background color of the left side of the +// border. +func (s Style) BorderLeftBackground(c TerminalColor) Style { + s.set(borderLeftBackgroundKey, c) + return s +} + +// Inline makes rendering output one line and disables the rendering of +// margins, padding and borders. This is useful when you need a style to apply +// only to font rendering and don't want it to change any physical dimensions. +// It works well with Style.MaxWidth. +// +// Because this in intended to be used at the time of render, this method will +// not mutate the style and instead return a copy. +// +// Example: +// +// var userInput string = "..." +// var userStyle = text.Style{ /* ... */ } +// fmt.Println(userStyle.Inline(true).Render(userInput)) +func (s Style) Inline(v bool) Style { + o := s // copy + o.set(inlineKey, v) + return o +} + +// MaxWidth applies a max width to a given style. This is useful in enforcing +// a certain width at render time, particularly with arbitrary strings and +// styles. +// +// Because this in intended to be used at the time of render, this method will +// not mutate the style and instead return a copy. +// +// Example: +// +// var userInput string = "..." +// var userStyle = text.Style{ /* ... */ } +// fmt.Println(userStyle.MaxWidth(16).Render(userInput)) +func (s Style) MaxWidth(n int) Style { + o := s // copy + o.set(maxWidthKey, n) + return o +} + +// MaxHeight applies a max height to a given style. This is useful in enforcing +// a certain height at render time, particularly with arbitrary strings and +// styles. +// +// Because this in intended to be used at the time of render, this method will +// not mutate the style and instead returns a copy. +func (s Style) MaxHeight(n int) Style { + o := s // copy + o.set(maxHeightKey, n) + return o +} + +// NoTabConversion can be passed to [Style.TabWidth] to disable the replacement +// of tabs with spaces at render time. +const NoTabConversion = -1 + +// TabWidth sets the number of spaces that a tab (/t) should be rendered as. +// When set to 0, tabs will be removed. To disable the replacement of tabs with +// spaces entirely, set this to [NoTabConversion]. +// +// By default, tabs will be replaced with 4 spaces. +func (s Style) TabWidth(n int) Style { + if n <= -1 { + n = -1 + } + s.set(tabWidthKey, n) + return s +} + +// UnderlineSpaces determines whether to underline spaces between words. By +// default, this is true. Spaces can also be underlined without underlining the +// text itself. +func (s Style) UnderlineSpaces(v bool) Style { + s.set(underlineSpacesKey, v) + return s +} + +// StrikethroughSpaces determines whether to apply strikethroughs to spaces +// between words. By default, this is true. Spaces can also be struck without +// underlining the text itself. +func (s Style) StrikethroughSpaces(v bool) Style { + s.set(strikethroughSpacesKey, v) + return s +} + +// Transform applies a given function to a string at render time, allowing for +// the string being rendered to be manipuated. +// +// Example: +// +// s := NewStyle().Transform(strings.ToUpper) +// fmt.Println(s.Render("raow!") // "RAOW!" +func (s Style) Transform(fn func(string) string) Style { + s.set(transformKey, fn) + return s +} + +// Renderer sets the renderer for the style. This is useful for changing the +// renderer for a style that is being used in a different context. +func (s Style) Renderer(r *Renderer) Style { + s.r = r + return s +} + +// whichSidesInt is a helper method for setting values on sides of a block based +// on the number of arguments. It follows the CSS shorthand rules for blocks +// like margin, padding. and borders. Here are how the rules work: +// +// 0 args: do nothing +// 1 arg: all sides +// 2 args: top -> bottom +// 3 args: top -> horizontal -> bottom +// 4 args: top -> right -> bottom -> left +// 5+ args: do nothing. +func whichSidesInt(i ...int) (top, right, bottom, left int, ok bool) { + switch len(i) { + case 1: + top = i[0] + bottom = i[0] + left = i[0] + right = i[0] + ok = true + case 2: //nolint:gomnd + top = i[0] + bottom = i[0] + left = i[1] + right = i[1] + ok = true + case 3: //nolint:gomnd + top = i[0] + left = i[1] + right = i[1] + bottom = i[2] + ok = true + case 4: //nolint:gomnd + top = i[0] + right = i[1] + bottom = i[2] + left = i[3] + ok = true + } + return top, right, bottom, left, ok +} + +// whichSidesBool is like whichSidesInt, except it operates on a series of +// boolean values. See the comment on whichSidesInt for details on how this +// works. +func whichSidesBool(i ...bool) (top, right, bottom, left bool, ok bool) { + switch len(i) { + case 1: + top = i[0] + bottom = i[0] + left = i[0] + right = i[0] + ok = true + case 2: //nolint:gomnd + top = i[0] + bottom = i[0] + left = i[1] + right = i[1] + ok = true + case 3: //nolint:gomnd + top = i[0] + left = i[1] + right = i[1] + bottom = i[2] + ok = true + case 4: //nolint:gomnd + top = i[0] + right = i[1] + bottom = i[2] + left = i[3] + ok = true + } + return top, right, bottom, left, ok +} + +// whichSidesColor is like whichSides, except it operates on a series of +// boolean values. See the comment on whichSidesInt for details on how this +// works. +func whichSidesColor(i ...TerminalColor) (top, right, bottom, left TerminalColor, ok bool) { + switch len(i) { + case 1: + top = i[0] + bottom = i[0] + left = i[0] + right = i[0] + ok = true + case 2: //nolint:gomnd + top = i[0] + bottom = i[0] + left = i[1] + right = i[1] + ok = true + case 3: //nolint:gomnd + top = i[0] + left = i[1] + right = i[1] + bottom = i[2] + ok = true + case 4: //nolint:gomnd + top = i[0] + right = i[1] + bottom = i[2] + left = i[3] + ok = true + } + return top, right, bottom, left, ok +} diff --git a/vendor/github.com/charmbracelet/lipgloss/size.go b/vendor/github.com/charmbracelet/lipgloss/size.go new file mode 100644 index 00000000..e169ff5e --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/size.go @@ -0,0 +1,41 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/ansi" +) + +// Width returns the cell width of characters in the string. ANSI sequences are +// ignored and characters wider than one cell (such as Chinese characters and +// emojis) are appropriately measured. +// +// You should use this instead of len(string) len([]rune(string) as neither +// will give you accurate results. +func Width(str string) (width int) { + for _, l := range strings.Split(str, "\n") { + w := ansi.StringWidth(l) + if w > width { + width = w + } + } + + return width +} + +// Height returns height of a string in cells. This is done simply by +// counting \n characters. If your strings use \r\n for newlines you should +// convert them to \n first, or simply write a separate function for measuring +// height. +func Height(str string) int { + return strings.Count(str, "\n") + 1 +} + +// Size returns the width and height of the string in cells. ANSI sequences are +// ignored and characters wider than one cell (such as Chinese characters and +// emojis) are appropriately measured. +func Size(str string) (width, height int) { + width = Width(str) + height = Height(str) + return width, height +} diff --git a/vendor/github.com/charmbracelet/lipgloss/style.go b/vendor/github.com/charmbracelet/lipgloss/style.go new file mode 100644 index 00000000..4343d3cd --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/style.go @@ -0,0 +1,585 @@ +package lipgloss + +import ( + "strings" + "unicode" + + "github.com/charmbracelet/x/ansi" + "github.com/muesli/termenv" +) + +const tabWidthDefault = 4 + +// Property for a key. +type propKey int64 + +// Available properties. +const ( + // Boolean props come first. + boldKey propKey = 1 << iota + italicKey + underlineKey + strikethroughKey + reverseKey + blinkKey + faintKey + underlineSpacesKey + strikethroughSpacesKey + colorWhitespaceKey + + // Non-boolean props. + foregroundKey + backgroundKey + widthKey + heightKey + alignHorizontalKey + alignVerticalKey + + // Padding. + paddingTopKey + paddingRightKey + paddingBottomKey + paddingLeftKey + + // Margins. + marginTopKey + marginRightKey + marginBottomKey + marginLeftKey + marginBackgroundKey + + // Border runes. + borderStyleKey + + // Border edges. + borderTopKey + borderRightKey + borderBottomKey + borderLeftKey + + // Border foreground colors. + borderTopForegroundKey + borderRightForegroundKey + borderBottomForegroundKey + borderLeftForegroundKey + + // Border background colors. + borderTopBackgroundKey + borderRightBackgroundKey + borderBottomBackgroundKey + borderLeftBackgroundKey + + inlineKey + maxWidthKey + maxHeightKey + tabWidthKey + + transformKey +) + +// props is a set of properties. +type props int64 + +// set sets a property. +func (p props) set(k propKey) props { + return p | props(k) +} + +// unset unsets a property. +func (p props) unset(k propKey) props { + return p &^ props(k) +} + +// has checks if a property is set. +func (p props) has(k propKey) bool { + return p&props(k) != 0 +} + +// NewStyle returns a new, empty Style. While it's syntactic sugar for the +// Style{} primitive, it's recommended to use this function for creating styles +// in case the underlying implementation changes. It takes an optional string +// value to be set as the underlying string value for this style. +func NewStyle() Style { + return renderer.NewStyle() +} + +// NewStyle returns a new, empty Style. While it's syntactic sugar for the +// Style{} primitive, it's recommended to use this function for creating styles +// in case the underlying implementation changes. It takes an optional string +// value to be set as the underlying string value for this style. +func (r *Renderer) NewStyle() Style { + s := Style{r: r} + return s +} + +// Style contains a set of rules that comprise a style as a whole. +type Style struct { + r *Renderer + props props + value string + + // we store bool props values here + attrs int + + // props that have values + fgColor TerminalColor + bgColor TerminalColor + + width int + height int + + alignHorizontal Position + alignVertical Position + + paddingTop int + paddingRight int + paddingBottom int + paddingLeft int + + marginTop int + marginRight int + marginBottom int + marginLeft int + marginBgColor TerminalColor + + borderStyle Border + borderTopFgColor TerminalColor + borderRightFgColor TerminalColor + borderBottomFgColor TerminalColor + borderLeftFgColor TerminalColor + borderTopBgColor TerminalColor + borderRightBgColor TerminalColor + borderBottomBgColor TerminalColor + borderLeftBgColor TerminalColor + + maxWidth int + maxHeight int + tabWidth int + + transform func(string) string +} + +// joinString joins a list of strings into a single string separated with a +// space. +func joinString(strs ...string) string { + return strings.Join(strs, " ") +} + +// SetString sets the underlying string value for this style. To render once +// the underlying string is set, use the Style.String. This method is +// a convenience for cases when having a stringer implementation is handy, such +// as when using fmt.Sprintf. You can also simply define a style and render out +// strings directly with Style.Render. +func (s Style) SetString(strs ...string) Style { + s.value = joinString(strs...) + return s +} + +// Value returns the raw, unformatted, underlying string value for this style. +func (s Style) Value() string { + return s.value +} + +// String implements stringer for a Style, returning the rendered result based +// on the rules in this style. An underlying string value must be set with +// Style.SetString prior to using this method. +func (s Style) String() string { + return s.Render() +} + +// Copy returns a copy of this style, including any underlying string values. +// +// Deprecated: to copy just use assignment (i.e. a := b). All methods also +// return a new style. +func (s Style) Copy() Style { + return s +} + +// Inherit overlays the style in the argument onto this style by copying each explicitly +// set value from the argument style onto this style if it is not already explicitly set. +// Existing set values are kept intact and not overwritten. +// +// Margins, padding, and underlying string values are not inherited. +func (s Style) Inherit(i Style) Style { + for k := boldKey; k <= transformKey; k <<= 1 { + if !i.isSet(k) { + continue + } + + switch k { //nolint:exhaustive + case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey: + // Margins are not inherited + continue + case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey: + // Padding is not inherited + continue + case backgroundKey: + // The margins also inherit the background color + if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) { + s.set(marginBackgroundKey, i.bgColor) + } + } + + if s.isSet(k) { + continue + } + + s.setFrom(k, i) + } + return s +} + +// Render applies the defined style formatting to a given string. +func (s Style) Render(strs ...string) string { + if s.r == nil { + s.r = renderer + } + if s.value != "" { + strs = append([]string{s.value}, strs...) + } + + var ( + str = joinString(strs...) + + p = s.r.ColorProfile() + te = p.String() + teSpace = p.String() + teWhitespace = p.String() + + bold = s.getAsBool(boldKey, false) + italic = s.getAsBool(italicKey, false) + underline = s.getAsBool(underlineKey, false) + strikethrough = s.getAsBool(strikethroughKey, false) + reverse = s.getAsBool(reverseKey, false) + blink = s.getAsBool(blinkKey, false) + faint = s.getAsBool(faintKey, false) + + fg = s.getAsColor(foregroundKey) + bg = s.getAsColor(backgroundKey) + + width = s.getAsInt(widthKey) + height = s.getAsInt(heightKey) + horizontalAlign = s.getAsPosition(alignHorizontalKey) + verticalAlign = s.getAsPosition(alignVerticalKey) + + topPadding = s.getAsInt(paddingTopKey) + rightPadding = s.getAsInt(paddingRightKey) + bottomPadding = s.getAsInt(paddingBottomKey) + leftPadding = s.getAsInt(paddingLeftKey) + + colorWhitespace = s.getAsBool(colorWhitespaceKey, true) + inline = s.getAsBool(inlineKey, false) + maxWidth = s.getAsInt(maxWidthKey) + maxHeight = s.getAsInt(maxHeightKey) + + underlineSpaces = s.getAsBool(underlineSpacesKey, false) || (underline && s.getAsBool(underlineSpacesKey, true)) + strikethroughSpaces = s.getAsBool(strikethroughSpacesKey, false) || (strikethrough && s.getAsBool(strikethroughSpacesKey, true)) + + // Do we need to style whitespace (padding and space outside + // paragraphs) separately? + styleWhitespace = reverse + + // Do we need to style spaces separately? + useSpaceStyler = (underline && !underlineSpaces) || (strikethrough && !strikethroughSpaces) || underlineSpaces || strikethroughSpaces + + transform = s.getAsTransform(transformKey) + ) + + if transform != nil { + str = transform(str) + } + + if s.props == 0 { + return s.maybeConvertTabs(str) + } + + // Enable support for ANSI on the legacy Windows cmd.exe console. This is a + // no-op on non-Windows systems and on Windows runs only once. + enableLegacyWindowsANSI() + + if bold { + te = te.Bold() + } + if italic { + te = te.Italic() + } + if underline { + te = te.Underline() + } + if reverse { + teWhitespace = teWhitespace.Reverse() + te = te.Reverse() + } + if blink { + te = te.Blink() + } + if faint { + te = te.Faint() + } + + if fg != noColor { + te = te.Foreground(fg.color(s.r)) + if styleWhitespace { + teWhitespace = teWhitespace.Foreground(fg.color(s.r)) + } + if useSpaceStyler { + teSpace = teSpace.Foreground(fg.color(s.r)) + } + } + + if bg != noColor { + te = te.Background(bg.color(s.r)) + if colorWhitespace { + teWhitespace = teWhitespace.Background(bg.color(s.r)) + } + if useSpaceStyler { + teSpace = teSpace.Background(bg.color(s.r)) + } + } + + if underline { + te = te.Underline() + } + if strikethrough { + te = te.CrossOut() + } + + if underlineSpaces { + teSpace = teSpace.Underline() + } + if strikethroughSpaces { + teSpace = teSpace.CrossOut() + } + + // Potentially convert tabs to spaces + str = s.maybeConvertTabs(str) + + // Strip newlines in single line mode + if inline { + str = strings.ReplaceAll(str, "\n", "") + } + + // Word wrap + if !inline && width > 0 { + wrapAt := width - leftPadding - rightPadding + str = ansi.Wrap(str, wrapAt, "") + } + + // Render core text + { + var b strings.Builder + + l := strings.Split(str, "\n") + for i := range l { + if useSpaceStyler { + // Look for spaces and apply a different styler + for _, r := range l[i] { + if unicode.IsSpace(r) { + b.WriteString(teSpace.Styled(string(r))) + continue + } + b.WriteString(te.Styled(string(r))) + } + } else { + b.WriteString(te.Styled(l[i])) + } + if i != len(l)-1 { + b.WriteRune('\n') + } + } + + str = b.String() + } + + // Padding + if !inline { //nolint:nestif + if leftPadding > 0 { + var st *termenv.Style + if colorWhitespace || styleWhitespace { + st = &teWhitespace + } + str = padLeft(str, leftPadding, st) + } + + if rightPadding > 0 { + var st *termenv.Style + if colorWhitespace || styleWhitespace { + st = &teWhitespace + } + str = padRight(str, rightPadding, st) + } + + if topPadding > 0 { + str = strings.Repeat("\n", topPadding) + str + } + + if bottomPadding > 0 { + str += strings.Repeat("\n", bottomPadding) + } + } + + // Height + if height > 0 { + str = alignTextVertical(str, verticalAlign, height, nil) + } + + // Set alignment. This will also pad short lines with spaces so that all + // lines are the same length, so we run it under a few different conditions + // beyond alignment. + { + numLines := strings.Count(str, "\n") + + if !(numLines == 0 && width == 0) { + var st *termenv.Style + if colorWhitespace || styleWhitespace { + st = &teWhitespace + } + str = alignTextHorizontal(str, horizontalAlign, width, st) + } + } + + if !inline { + str = s.applyBorder(str) + str = s.applyMargins(str, inline) + } + + // Truncate according to MaxWidth + if maxWidth > 0 { + lines := strings.Split(str, "\n") + + for i := range lines { + lines[i] = ansi.Truncate(lines[i], maxWidth, "") + } + + str = strings.Join(lines, "\n") + } + + // Truncate according to MaxHeight + if maxHeight > 0 { + lines := strings.Split(str, "\n") + height := min(maxHeight, len(lines)) + if len(lines) > 0 { + str = strings.Join(lines[:height], "\n") + } + } + + return str +} + +func (s Style) maybeConvertTabs(str string) string { + tw := tabWidthDefault + if s.isSet(tabWidthKey) { + tw = s.getAsInt(tabWidthKey) + } + switch tw { + case -1: + return str + case 0: + return strings.ReplaceAll(str, "\t", "") + default: + return strings.ReplaceAll(str, "\t", strings.Repeat(" ", tw)) + } +} + +func (s Style) applyMargins(str string, inline bool) string { + var ( + topMargin = s.getAsInt(marginTopKey) + rightMargin = s.getAsInt(marginRightKey) + bottomMargin = s.getAsInt(marginBottomKey) + leftMargin = s.getAsInt(marginLeftKey) + + styler termenv.Style + ) + + bgc := s.getAsColor(marginBackgroundKey) + if bgc != noColor { + styler = styler.Background(bgc.color(s.r)) + } + + // Add left and right margin + str = padLeft(str, leftMargin, &styler) + str = padRight(str, rightMargin, &styler) + + // Top/bottom margin + if !inline { + _, width := getLines(str) + spaces := strings.Repeat(" ", width) + + if topMargin > 0 { + str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str + } + if bottomMargin > 0 { + str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin)) + } + } + + return str +} + +// Apply left padding. +func padLeft(str string, n int, style *termenv.Style) string { + return pad(str, -n, style) +} + +// Apply right padding. +func padRight(str string, n int, style *termenv.Style) string { + return pad(str, n, style) +} + +// pad adds padding to either the left or right side of a string. +// Positive values add to the right side while negative values +// add to the left side. +func pad(str string, n int, style *termenv.Style) string { + if n == 0 { + return str + } + + sp := strings.Repeat(" ", abs(n)) + if style != nil { + sp = style.Styled(sp) + } + + b := strings.Builder{} + l := strings.Split(str, "\n") + + for i := range l { + switch { + // pad right + case n > 0: + b.WriteString(l[i]) + b.WriteString(sp) + // pad left + default: + b.WriteString(sp) + b.WriteString(l[i]) + } + + if i != len(l)-1 { + b.WriteRune('\n') + } + } + + return b.String() +} + +func max(a, b int) int { //nolint:unparam + if a > b { + return a + } + return b +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func abs(a int) int { + if a < 0 { + return -a + } + + return a +} diff --git a/vendor/github.com/charmbracelet/lipgloss/tree/children.go b/vendor/github.com/charmbracelet/lipgloss/tree/children.go new file mode 100644 index 00000000..6727092a --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/tree/children.go @@ -0,0 +1,98 @@ +package tree + +// Children is the interface that wraps the basic methods of a tree model. +type Children interface { + // At returns the content item of the given index. + At(index int) Node + + // Length returns the number of children in the tree. + Length() int +} + +// NodeChildren is the implementation of the Children interface with tree Nodes. +type NodeChildren []Node + +// Append appends a child to the list of children. +func (n NodeChildren) Append(child Node) NodeChildren { + n = append(n, child) + return n +} + +// Remove removes a child from the list at the given index. +func (n NodeChildren) Remove(index int) NodeChildren { + if index < 0 || len(n) < index+1 { + return n + } + n = append(n[:index], n[index+1:]...) + return n +} + +// Length returns the number of children in the list. +func (n NodeChildren) Length() int { + return len(n) +} + +// At returns the child at the given index. +func (n NodeChildren) At(i int) Node { + if i >= 0 && i < len(n) { + return n[i] + } + return nil +} + +// NewStringData returns a Data of strings. +func NewStringData(data ...string) Children { + result := make([]Node, 0, len(data)) + for _, d := range data { + s := Leaf{value: d} + result = append(result, &s) + } + return NodeChildren(result) +} + +var _ Children = NewFilter(nil) + +// Filter applies a filter on some data. You could use this to create a new +// tree whose values all satisfy the condition provided in the Filter() function. +type Filter struct { + data Children + filter func(index int) bool +} + +// NewFilter initializes a new Filter. +func NewFilter(data Children) *Filter { + return &Filter{data: data} +} + +// At returns the item at the given index. +// The index is relative to the filtered results. +func (m *Filter) At(index int) Node { + j := 0 + for i := 0; i < m.data.Length(); i++ { + if m.filter(i) { + if j == index { + return m.data.At(i) + } + j++ + } + } + + return nil +} + +// Filter uses a filter function to set a condition that all the data must satisfy to be in the Tree. +func (m *Filter) Filter(f func(index int) bool) *Filter { + m.filter = f + return m +} + +// Length returns the number of children in the tree. +func (m *Filter) Length() int { + j := 0 + for i := 0; i < m.data.Length(); i++ { + if m.filter(i) { + j++ + } + } + return j +} diff --git a/vendor/github.com/charmbracelet/lipgloss/tree/enumerator.go b/vendor/github.com/charmbracelet/lipgloss/tree/enumerator.go new file mode 100644 index 00000000..5e5bc59d --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/tree/enumerator.go @@ -0,0 +1,74 @@ +package tree + +// Enumerator enumerates a tree. Typically, this is used to draw the branches +// for the tree nodes and is different for the last child. +// +// For example, the default enumerator would be: +// +// func TreeEnumerator(children Children, index int) string { +// if children.Length()-1 == index { +// return "└──" +// } +// +// return "├──" +// } +type Enumerator func(children Children, index int) string + +// DefaultEnumerator enumerates a tree. +// +// ├── Foo +// ├── Bar +// ├── Baz +// └── Qux. +func DefaultEnumerator(children Children, index int) string { + if children.Length()-1 == index { + return "└──" + } + return "├──" +} + +// RoundedEnumerator enumerates a tree with rounded edges. +// +// ├── Foo +// ├── Bar +// ├── Baz +// ╰── Qux. +func RoundedEnumerator(children Children, index int) string { + if children.Length()-1 == index { + return "╰──" + } + return "├──" +} + +// Indenter indents the children of a tree. +// +// Indenters allow for displaying nested tree items with connecting borders +// to sibling nodes. +// +// For example, the default indenter would be: +// +// func TreeIndenter(children Children, index int) string { +// if children.Length()-1 == index { +// return "│ " +// } +// +// return " " +// } +type Indenter func(children Children, index int) string + +// DefaultIndenter indents a tree for nested trees and multiline content. +// +// ├── Foo +// ├── Bar +// │ ├── Qux +// │ ├── Quux +// │ │ ├── Foo +// │ │ └── Bar +// │ └── Quuux +// └── Baz. +func DefaultIndenter(children Children, index int) string { + if children.Length()-1 == index { + return " " + } + return "│ " +} diff --git a/vendor/github.com/charmbracelet/lipgloss/tree/renderer.go b/vendor/github.com/charmbracelet/lipgloss/tree/renderer.go new file mode 100644 index 00000000..8fd86930 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/tree/renderer.go @@ -0,0 +1,138 @@ +package tree + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// StyleFunc allows the tree to be styled per item. +type StyleFunc func(children Children, i int) lipgloss.Style + +// Style is the styling applied to the tree. +type Style struct { + enumeratorFunc StyleFunc + itemFunc StyleFunc + root lipgloss.Style +} + +// newRenderer returns the renderer used to render a tree. +func newRenderer() *renderer { + return &renderer{ + style: Style{ + enumeratorFunc: func(Children, int) lipgloss.Style { + return lipgloss.NewStyle().PaddingRight(1) + }, + itemFunc: func(Children, int) lipgloss.Style { + return lipgloss.NewStyle() + }, + }, + enumerator: DefaultEnumerator, + indenter: DefaultIndenter, + } +} + +type renderer struct { + style Style + enumerator Enumerator + indenter Indenter +} + +// render is responsible for actually rendering the tree. +func (r *renderer) render(node Node, root bool, prefix string) string { + if node.Hidden() { + return "" + } + var strs []string + var maxLen int + children := node.Children() + enumerator := r.enumerator + indenter := r.indenter + + // print the root node name if its not empty. + if name := node.Value(); name != "" && root { + strs = append(strs, r.style.root.Render(name)) + } + + for i := 0; i < children.Length(); i++ { + prefix := enumerator(children, i) + prefix = r.style.enumeratorFunc(children, i).Render(prefix) + maxLen = max(lipgloss.Width(prefix), maxLen) + } + + for i := 0; i < children.Length(); i++ { + child := children.At(i) + if child.Hidden() { + continue + } + indent := indenter(children, i) + nodePrefix := enumerator(children, i) + enumStyle := r.style.enumeratorFunc(children, i) + itemStyle := r.style.itemFunc(children, i) + + nodePrefix = enumStyle.Render(nodePrefix) + if l := maxLen - lipgloss.Width(nodePrefix); l > 0 { + nodePrefix = strings.Repeat(" ", l) + nodePrefix + } + + item := itemStyle.Render(child.Value()) + multineLinePrefix := prefix + + // This dance below is to account for multiline prefixes, e.g. "|\n|". + // In that case, we need to make sure that both the parent prefix and + // the current node's prefix have the same height. + for lipgloss.Height(item) > lipgloss.Height(nodePrefix) { + nodePrefix = lipgloss.JoinVertical( + lipgloss.Left, + nodePrefix, + enumStyle.Render(indent), + ) + } + for lipgloss.Height(nodePrefix) > lipgloss.Height(multineLinePrefix) { + multineLinePrefix = lipgloss.JoinVertical( + lipgloss.Left, + multineLinePrefix, + prefix, + ) + } + + strs = append( + strs, + lipgloss.JoinHorizontal( + lipgloss.Top, + multineLinePrefix, + nodePrefix, + item, + ), + ) + + if children.Length() > 0 { + // here we see if the child has a custom renderer, which means the + // user set a custom enumerator, style, etc. + // if it has one, we'll use it to render itself. + // otherwise, we keep using the current renderer. + renderer := r + switch child := child.(type) { + case *Tree: + if child.r != nil { + renderer = child.r + } + } + if s := renderer.render( + child, + false, + prefix+enumStyle.Render(indent), + ); s != "" { + strs = append(strs, s) + } + } + } + return strings.Join(strs, "\n") +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/vendor/github.com/charmbracelet/lipgloss/tree/tree.go b/vendor/github.com/charmbracelet/lipgloss/tree/tree.go new file mode 100644 index 00000000..94f5dd08 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/tree/tree.go @@ -0,0 +1,338 @@ +// Package tree allows you to build trees, as simple or complicated as you need. +// +// Define a tree with a root node, and children, set rendering properties (such +// as style, enumerators, etc...), and print it. +// +// t := tree.New(). +// Child( +// ".git", +// tree.Root("examples/"). +// Child( +// tree.Root("list/"). +// Child("main.go"). +// tree.Root("table/"). +// Child("main.go"). +// ). +// tree.Root("list/"). +// Child("list.go", "list_test.go"). +// tree.New(). +// Root("table/"). +// Child("table.go", "table_test.go"). +// "align.go", +// "align_test.go", +// "join.go", +// "join_test.go", +// ) +package tree + +import ( + "fmt" + "sync" + + "github.com/charmbracelet/lipgloss" +) + +// Node defines a node in a tree. +type Node interface { + fmt.Stringer + Value() string + Children() Children + Hidden() bool +} + +// Leaf is a node without children. +type Leaf struct { + value string + hidden bool +} + +// Children of a Leaf node are always empty. +func (Leaf) Children() Children { + return NodeChildren(nil) +} + +// Value of a leaf node returns its value. +func (s Leaf) Value() string { + return s.value +} + +// Hidden returns whether a Leaf node is hidden. +func (s Leaf) Hidden() bool { + return s.hidden +} + +// String returns the string representation of a Leaf node. +func (s Leaf) String() string { + return s.Value() +} + +// Tree implements a Node. +type Tree struct { //nolint:revive + value string + hidden bool + offset [2]int + children Children + + r *renderer + ronce sync.Once +} + +// Hidden returns whether this node is hidden. +func (t *Tree) Hidden() bool { + return t.hidden +} + +// Hide sets whether to hide the tree node. +func (t *Tree) Hide(hide bool) *Tree { + t.hidden = hide + return t +} + +// Offset sets the tree children offsets. +func (t *Tree) Offset(start, end int) *Tree { + if start > end { + _start := start + start = end + end = _start + } + + if start < 0 { + start = 0 + } + if end < 0 || end > t.children.Length() { + end = t.children.Length() + } + + t.offset[0] = start + t.offset[1] = end + return t +} + +// Value returns the root name of this node. +func (t *Tree) Value() string { + return t.value +} + +// String returns the string representation of the tree node. +func (t *Tree) String() string { + return t.ensureRenderer().render(t, true, "") +} + +// Child adds a child to this tree. +// +// If a Child Tree is passed without a root, it will be parented to it's sibling +// child (auto-nesting). +// +// tree.Root("Foo").Child("Bar", tree.New().Child("Baz"), "Qux") +// tree.Root("Foo").Child(tree.Root("Bar").Child("Baz"), "Qux") +// +// ├── Foo +// ├── Bar +// │ └── Baz +// └── Qux +func (t *Tree) Child(children ...any) *Tree { + for _, child := range children { + switch item := child.(type) { + case *Tree: + newItem, rm := ensureParent(t.children, item) + if rm >= 0 { + t.children = t.children.(NodeChildren).Remove(rm) + } + t.children = t.children.(NodeChildren).Append(newItem) + case Children: + for i := 0; i < item.Length(); i++ { + t.children = t.children.(NodeChildren).Append(item.At(i)) + } + case Node: + t.children = t.children.(NodeChildren).Append(item) + case fmt.Stringer: + s := Leaf{value: item.String()} + t.children = t.children.(NodeChildren).Append(s) + case string: + s := Leaf{value: item} + t.children = t.children.(NodeChildren).Append(&s) + case []any: + return t.Child(item...) + case []string: + ss := make([]any, 0, len(item)) + for _, s := range item { + ss = append(ss, s) + } + return t.Child(ss...) + case nil: + continue + default: + return t.Child(fmt.Sprintf("%v", item)) + } + } + return t +} + +func ensureParent(nodes Children, item *Tree) (*Tree, int) { + if item.Value() != "" || nodes.Length() == 0 { + return item, -1 + } + j := nodes.Length() - 1 + parent := nodes.At(j) + switch parent := parent.(type) { + case *Tree: + for i := 0; i < item.Children().Length(); i++ { + parent.Child(item.children.At(i)) + } + return parent, j + case Leaf: + item.value = parent.Value() + return item, j + case *Leaf: + item.value = parent.Value() + return item, j + } + return item, -1 +} + +func (t *Tree) ensureRenderer() *renderer { + t.ronce.Do(func() { t.r = newRenderer() }) + return t.r +} + +// EnumeratorStyle sets a static style for all enumerators. +// +// Use EnumeratorStyleFunc to conditionally set styles based on the tree node. +func (t *Tree) EnumeratorStyle(style lipgloss.Style) *Tree { + t.ensureRenderer().style.enumeratorFunc = func(Children, int) lipgloss.Style { + return style + } + return t +} + +// EnumeratorStyleFunc sets the enumeration style function. Use this function +// for conditional styling. +// +// t := tree.New(). +// EnumeratorStyleFunc(func(_ tree.Children, i int) lipgloss.Style { +// if selected == i { +// return lipgloss.NewStyle().Foreground(hightlightColor) +// } +// return lipgloss.NewStyle().Foreground(dimColor) +// }) +func (t *Tree) EnumeratorStyleFunc(fn StyleFunc) *Tree { + if fn == nil { + fn = func(Children, int) lipgloss.Style { return lipgloss.NewStyle() } + } + t.ensureRenderer().style.enumeratorFunc = fn + return t +} + +// RootStyle sets a style for the root element. +func (t *Tree) RootStyle(style lipgloss.Style) *Tree { + t.ensureRenderer().style.root = style + return t +} + +// ItemStyle sets a static style for all items. +// +// Use ItemStyleFunc to conditionally set styles based on the tree node. +func (t *Tree) ItemStyle(style lipgloss.Style) *Tree { + t.ensureRenderer().style.itemFunc = func(Children, int) lipgloss.Style { return style } + return t +} + +// ItemStyleFunc sets the item style function. Use this for conditional styling. +// For example: +// +// t := tree.New(). +// ItemStyleFunc(func(_ tree.Data, i int) lipgloss.Style { +// if selected == i { +// return lipgloss.NewStyle().Foreground(hightlightColor) +// } +// return lipgloss.NewStyle().Foreground(dimColor) +// }) +func (t *Tree) ItemStyleFunc(fn StyleFunc) *Tree { + if fn == nil { + fn = func(Children, int) lipgloss.Style { return lipgloss.NewStyle() } + } + t.ensureRenderer().style.itemFunc = fn + return t +} + +// Enumerator sets the enumerator implementation. This can be used to change the +// way the branches indicators look. Lipgloss includes predefined enumerators +// for a classic or rounded tree. For example, you can have a rounded tree: +// +// tree.New(). +// Enumerator(RoundedEnumerator) +func (t *Tree) Enumerator(enum Enumerator) *Tree { + t.ensureRenderer().enumerator = enum + return t +} + +// Indenter sets the indenter implementation. This is used to change the way +// the tree is indented. The default indentor places a border connecting sibling +// elements and no border for the last child. +// +// └── Foo +// └── Bar +// └── Baz +// └── Qux +// └── Quux +// +// You can define your own indenter. +// +// func ArrowIndenter(children tree.Children, index int) string { +// return "→ " +// } +// +// → Foo +// → → Bar +// → → → Baz +// → → → → Qux +// → → → → → Quux +func (t *Tree) Indenter(indenter Indenter) *Tree { + t.ensureRenderer().indenter = indenter + return t +} + +// Children returns the children of a node. +func (t *Tree) Children() Children { + var data []Node + for i := t.offset[0]; i < t.children.Length()-t.offset[1]; i++ { + data = append(data, t.children.At(i)) + } + return NodeChildren(data) +} + +// Root returns a new tree with the root set. +// +// tree.Root(root) +// +// It is a shorthand for: +// +// tree.New().Root(root) +func Root(root any) *Tree { + t := New() + return t.Root(root) +} + +// Root sets the root value of this tree. +func (t *Tree) Root(root any) *Tree { + // root is a tree or string + switch item := root.(type) { + case *Tree: + t.value = item.value + t = t.Child(item.children) + case Node, fmt.Stringer: + t.value = item.(fmt.Stringer).String() + case string, nil: + t.value = item.(string) + default: + t.value = fmt.Sprintf("%v", item) + } + return t +} + +// New returns a new tree. +func New() *Tree { + return &Tree{ + children: NodeChildren(nil), + } +} diff --git a/vendor/github.com/charmbracelet/lipgloss/unset.go b/vendor/github.com/charmbracelet/lipgloss/unset.go new file mode 100644 index 00000000..1086e722 --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/unset.go @@ -0,0 +1,331 @@ +package lipgloss + +// unset unsets a property from a style. +func (s *Style) unset(key propKey) { + s.props = s.props.unset(key) +} + +// UnsetBold removes the bold style rule, if set. +func (s Style) UnsetBold() Style { + s.unset(boldKey) + return s +} + +// UnsetItalic removes the italic style rule, if set. +func (s Style) UnsetItalic() Style { + s.unset(italicKey) + return s +} + +// UnsetUnderline removes the underline style rule, if set. +func (s Style) UnsetUnderline() Style { + s.unset(underlineKey) + return s +} + +// UnsetStrikethrough removes the strikethrough style rule, if set. +func (s Style) UnsetStrikethrough() Style { + s.unset(strikethroughKey) + return s +} + +// UnsetReverse removes the reverse style rule, if set. +func (s Style) UnsetReverse() Style { + s.unset(reverseKey) + return s +} + +// UnsetBlink removes the blink style rule, if set. +func (s Style) UnsetBlink() Style { + s.unset(blinkKey) + return s +} + +// UnsetFaint removes the faint style rule, if set. +func (s Style) UnsetFaint() Style { + s.unset(faintKey) + return s +} + +// UnsetForeground removes the foreground style rule, if set. +func (s Style) UnsetForeground() Style { + s.unset(foregroundKey) + return s +} + +// UnsetBackground removes the background style rule, if set. +func (s Style) UnsetBackground() Style { + s.unset(backgroundKey) + return s +} + +// UnsetWidth removes the width style rule, if set. +func (s Style) UnsetWidth() Style { + s.unset(widthKey) + return s +} + +// UnsetHeight removes the height style rule, if set. +func (s Style) UnsetHeight() Style { + s.unset(heightKey) + return s +} + +// UnsetAlign removes the horizontal and vertical text alignment style rule, if set. +func (s Style) UnsetAlign() Style { + s.unset(alignHorizontalKey) + s.unset(alignVerticalKey) + return s +} + +// UnsetAlignHorizontal removes the horizontal text alignment style rule, if set. +func (s Style) UnsetAlignHorizontal() Style { + s.unset(alignHorizontalKey) + return s +} + +// UnsetAlignVertical removes the vertical text alignment style rule, if set. +func (s Style) UnsetAlignVertical() Style { + s.unset(alignVerticalKey) + return s +} + +// UnsetPadding removes all padding style rules. +func (s Style) UnsetPadding() Style { + s.unset(paddingLeftKey) + s.unset(paddingRightKey) + s.unset(paddingTopKey) + s.unset(paddingBottomKey) + return s +} + +// UnsetPaddingLeft removes the left padding style rule, if set. +func (s Style) UnsetPaddingLeft() Style { + s.unset(paddingLeftKey) + return s +} + +// UnsetPaddingRight removes the right padding style rule, if set. +func (s Style) UnsetPaddingRight() Style { + s.unset(paddingRightKey) + return s +} + +// UnsetPaddingTop removes the top padding style rule, if set. +func (s Style) UnsetPaddingTop() Style { + s.unset(paddingTopKey) + return s +} + +// UnsetPaddingBottom removes the bottom padding style rule, if set. +func (s Style) UnsetPaddingBottom() Style { + s.unset(paddingBottomKey) + return s +} + +// UnsetColorWhitespace removes the rule for coloring padding, if set. +func (s Style) UnsetColorWhitespace() Style { + s.unset(colorWhitespaceKey) + return s +} + +// UnsetMargins removes all margin style rules. +func (s Style) UnsetMargins() Style { + s.unset(marginLeftKey) + s.unset(marginRightKey) + s.unset(marginTopKey) + s.unset(marginBottomKey) + return s +} + +// UnsetMarginLeft removes the left margin style rule, if set. +func (s Style) UnsetMarginLeft() Style { + s.unset(marginLeftKey) + return s +} + +// UnsetMarginRight removes the right margin style rule, if set. +func (s Style) UnsetMarginRight() Style { + s.unset(marginRightKey) + return s +} + +// UnsetMarginTop removes the top margin style rule, if set. +func (s Style) UnsetMarginTop() Style { + s.unset(marginTopKey) + return s +} + +// UnsetMarginBottom removes the bottom margin style rule, if set. +func (s Style) UnsetMarginBottom() Style { + s.unset(marginBottomKey) + return s +} + +// UnsetMarginBackground removes the margin's background color. Note that the +// margin's background color can be set from the background color of another +// style during inheritance. +func (s Style) UnsetMarginBackground() Style { + s.unset(marginBackgroundKey) + return s +} + +// UnsetBorderStyle removes the border style rule, if set. +func (s Style) UnsetBorderStyle() Style { + s.unset(borderStyleKey) + return s +} + +// UnsetBorderTop removes the border top style rule, if set. +func (s Style) UnsetBorderTop() Style { + s.unset(borderTopKey) + return s +} + +// UnsetBorderRight removes the border right style rule, if set. +func (s Style) UnsetBorderRight() Style { + s.unset(borderRightKey) + return s +} + +// UnsetBorderBottom removes the border bottom style rule, if set. +func (s Style) UnsetBorderBottom() Style { + s.unset(borderBottomKey) + return s +} + +// UnsetBorderLeft removes the border left style rule, if set. +func (s Style) UnsetBorderLeft() Style { + s.unset(borderLeftKey) + return s +} + +// UnsetBorderForeground removes all border foreground color styles, if set. +func (s Style) UnsetBorderForeground() Style { + s.unset(borderTopForegroundKey) + s.unset(borderRightForegroundKey) + s.unset(borderBottomForegroundKey) + s.unset(borderLeftForegroundKey) + return s +} + +// UnsetBorderTopForeground removes the top border foreground color rule, +// if set. +func (s Style) UnsetBorderTopForeground() Style { + s.unset(borderTopForegroundKey) + return s +} + +// UnsetBorderRightForeground removes the right border foreground color rule, +// if set. +func (s Style) UnsetBorderRightForeground() Style { + s.unset(borderRightForegroundKey) + return s +} + +// UnsetBorderBottomForeground removes the bottom border foreground color +// rule, if set. +func (s Style) UnsetBorderBottomForeground() Style { + s.unset(borderBottomForegroundKey) + return s +} + +// UnsetBorderLeftForeground removes the left border foreground color rule, +// if set. +func (s Style) UnsetBorderLeftForeground() Style { + s.unset(borderLeftForegroundKey) + return s +} + +// UnsetBorderBackground removes all border background color styles, if +// set. +func (s Style) UnsetBorderBackground() Style { + s.unset(borderTopBackgroundKey) + s.unset(borderRightBackgroundKey) + s.unset(borderBottomBackgroundKey) + s.unset(borderLeftBackgroundKey) + return s +} + +// UnsetBorderTopBackgroundColor removes the top border background color rule, +// if set. +// +// Deprecated: This function simply calls Style.UnsetBorderTopBackground. +func (s Style) UnsetBorderTopBackgroundColor() Style { + return s.UnsetBorderTopBackground() +} + +// UnsetBorderTopBackground removes the top border background color rule, +// if set. +func (s Style) UnsetBorderTopBackground() Style { + s.unset(borderTopBackgroundKey) + return s +} + +// UnsetBorderRightBackground removes the right border background color +// rule, if set. +func (s Style) UnsetBorderRightBackground() Style { + s.unset(borderRightBackgroundKey) + return s +} + +// UnsetBorderBottomBackground removes the bottom border background color +// rule, if set. +func (s Style) UnsetBorderBottomBackground() Style { + s.unset(borderBottomBackgroundKey) + return s +} + +// UnsetBorderLeftBackground removes the left border color rule, if set. +func (s Style) UnsetBorderLeftBackground() Style { + s.unset(borderLeftBackgroundKey) + return s +} + +// UnsetInline removes the inline style rule, if set. +func (s Style) UnsetInline() Style { + s.unset(inlineKey) + return s +} + +// UnsetMaxWidth removes the max width style rule, if set. +func (s Style) UnsetMaxWidth() Style { + s.unset(maxWidthKey) + return s +} + +// UnsetMaxHeight removes the max height style rule, if set. +func (s Style) UnsetMaxHeight() Style { + s.unset(maxHeightKey) + return s +} + +// UnsetTabWidth removes the tab width style rule, if set. +func (s Style) UnsetTabWidth() Style { + s.unset(tabWidthKey) + return s +} + +// UnsetUnderlineSpaces removes the value set by UnderlineSpaces. +func (s Style) UnsetUnderlineSpaces() Style { + s.unset(underlineSpacesKey) + return s +} + +// UnsetStrikethroughSpaces removes the value set by StrikethroughSpaces. +func (s Style) UnsetStrikethroughSpaces() Style { + s.unset(strikethroughSpacesKey) + return s +} + +// UnsetTransform removes the value set by Transform. +func (s Style) UnsetTransform() Style { + s.unset(transformKey) + return s +} + +// UnsetString sets the underlying string value to the empty string. +func (s Style) UnsetString() Style { + s.value = "" + return s +} diff --git a/vendor/github.com/charmbracelet/lipgloss/whitespace.go b/vendor/github.com/charmbracelet/lipgloss/whitespace.go new file mode 100644 index 00000000..040dc98e --- /dev/null +++ b/vendor/github.com/charmbracelet/lipgloss/whitespace.go @@ -0,0 +1,83 @@ +package lipgloss + +import ( + "strings" + + "github.com/charmbracelet/x/ansi" + "github.com/muesli/termenv" +) + +// whitespace is a whitespace renderer. +type whitespace struct { + re *Renderer + style termenv.Style + chars string +} + +// newWhitespace creates a new whitespace renderer. The order of the options +// matters, if you're using WithWhitespaceRenderer, make sure it comes first as +// other options might depend on it. +func newWhitespace(r *Renderer, opts ...WhitespaceOption) *whitespace { + w := &whitespace{ + re: r, + style: r.ColorProfile().String(), + } + for _, opt := range opts { + opt(w) + } + return w +} + +// Render whitespaces. +func (w whitespace) render(width int) string { + if w.chars == "" { + w.chars = " " + } + + r := []rune(w.chars) + j := 0 + b := strings.Builder{} + + // Cycle through runes and print them into the whitespace. + for i := 0; i < width; { + b.WriteRune(r[j]) + j++ + if j >= len(r) { + j = 0 + } + i += ansi.StringWidth(string(r[j])) + } + + // Fill any extra gaps white spaces. This might be necessary if any runes + // are more than one cell wide, which could leave a one-rune gap. + short := width - ansi.StringWidth(b.String()) + if short > 0 { + b.WriteString(strings.Repeat(" ", short)) + } + + return w.style.Styled(b.String()) +} + +// WhitespaceOption sets a styling rule for rendering whitespace. +type WhitespaceOption func(*whitespace) + +// WithWhitespaceForeground sets the color of the characters in the whitespace. +func WithWhitespaceForeground(c TerminalColor) WhitespaceOption { + return func(w *whitespace) { + w.style = w.style.Foreground(c.color(w.re)) + } +} + +// WithWhitespaceBackground sets the background color of the whitespace. +func WithWhitespaceBackground(c TerminalColor) WhitespaceOption { + return func(w *whitespace) { + w.style = w.style.Background(c.color(w.re)) + } +} + +// WithWhitespaceChars sets the characters to be rendered in the whitespace. +func WithWhitespaceChars(s string) WhitespaceOption { + return func(w *whitespace) { + w.chars = s + } +} diff --git a/vendor/github.com/charmbracelet/x/ansi/LICENSE b/vendor/github.com/charmbracelet/x/ansi/LICENSE new file mode 100644 index 00000000..65a5654e --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Charmbracelet, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/charmbracelet/x/ansi/ansi.go b/vendor/github.com/charmbracelet/x/ansi/ansi.go new file mode 100644 index 00000000..48d873c3 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/ansi.go @@ -0,0 +1,11 @@ +package ansi + +import "io" + +// Execute is a function that "execute" the given escape sequence by writing it +// to the provided output writter. +// +// This is a syntactic sugar over [io.WriteString]. +func Execute(w io.Writer, s string) (int, error) { + return io.WriteString(w, s) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/ascii.go b/vendor/github.com/charmbracelet/x/ansi/ascii.go new file mode 100644 index 00000000..188582f7 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/ascii.go @@ -0,0 +1,8 @@ +package ansi + +const ( + // SP is the space character (Char: \x20). + SP = 0x20 + // DEL is the delete character (Caret: ^?, Char: \x7f). + DEL = 0x7F +) diff --git a/vendor/github.com/charmbracelet/x/ansi/background.go b/vendor/github.com/charmbracelet/x/ansi/background.go new file mode 100644 index 00000000..f519af08 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/background.go @@ -0,0 +1,61 @@ +package ansi + +import ( + "image/color" +) + +// SetForegroundColor returns a sequence that sets the default terminal +// foreground color. +// +// OSC 10 ; color ST +// OSC 10 ; color BEL +// +// Where color is the encoded color number. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetForegroundColor(c color.Color) string { + return "\x1b]10;" + colorToHexString(c) + "\x07" +} + +// RequestForegroundColor is a sequence that requests the current default +// terminal foreground color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const RequestForegroundColor = "\x1b]10;?\x07" + +// SetBackgroundColor returns a sequence that sets the default terminal +// background color. +// +// OSC 11 ; color ST +// OSC 11 ; color BEL +// +// Where color is the encoded color number. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetBackgroundColor(c color.Color) string { + return "\x1b]11;" + colorToHexString(c) + "\x07" +} + +// RequestBackgroundColor is a sequence that requests the current default +// terminal background color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const RequestBackgroundColor = "\x1b]11;?\x07" + +// SetCursorColor returns a sequence that sets the terminal cursor color. +// +// OSC 12 ; color ST +// OSC 12 ; color BEL +// +// Where color is the encoded color number. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetCursorColor(c color.Color) string { + return "\x1b]12;" + colorToHexString(c) + "\x07" +} + +// RequestCursorColor is a sequence that requests the current terminal cursor +// color. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +const RequestCursorColor = "\x1b]12;?\x07" diff --git a/vendor/github.com/charmbracelet/x/ansi/c0.go b/vendor/github.com/charmbracelet/x/ansi/c0.go new file mode 100644 index 00000000..13e3c6c3 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/c0.go @@ -0,0 +1,72 @@ +package ansi + +// C0 control characters. +// +// These range from (0x00-0x1F) as defined in ISO 646 (ASCII). +// See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes +const ( + // NUL is the null character (Caret: ^@, Char: \0). + NUL = 0x00 + // SOH is the start of heading character (Caret: ^A). + SOH = 0x01 + // STX is the start of text character (Caret: ^B). + STX = 0x02 + // ETX is the end of text character (Caret: ^C). + ETX = 0x03 + // EOT is the end of transmission character (Caret: ^D). + EOT = 0x04 + // ENQ is the enquiry character (Caret: ^E). + ENQ = 0x05 + // ACK is the acknowledge character (Caret: ^F). + ACK = 0x06 + // BEL is the bell character (Caret: ^G, Char: \a). + BEL = 0x07 + // BS is the backspace character (Caret: ^H, Char: \b). + BS = 0x08 + // HT is the horizontal tab character (Caret: ^I, Char: \t). + HT = 0x09 + // LF is the line feed character (Caret: ^J, Char: \n). + LF = 0x0A + // VT is the vertical tab character (Caret: ^K, Char: \v). + VT = 0x0B + // FF is the form feed character (Caret: ^L, Char: \f). + FF = 0x0C + // CR is the carriage return character (Caret: ^M, Char: \r). + CR = 0x0D + // SO is the shift out character (Caret: ^N). + SO = 0x0E + // SI is the shift in character (Caret: ^O). + SI = 0x0F + // DLE is the data link escape character (Caret: ^P). + DLE = 0x10 + // DC1 is the device control 1 character (Caret: ^Q). + DC1 = 0x11 + // DC2 is the device control 2 character (Caret: ^R). + DC2 = 0x12 + // DC3 is the device control 3 character (Caret: ^S). + DC3 = 0x13 + // DC4 is the device control 4 character (Caret: ^T). + DC4 = 0x14 + // NAK is the negative acknowledge character (Caret: ^U). + NAK = 0x15 + // SYN is the synchronous idle character (Caret: ^V). + SYN = 0x16 + // ETB is the end of transmission block character (Caret: ^W). + ETB = 0x17 + // CAN is the cancel character (Caret: ^X). + CAN = 0x18 + // EM is the end of medium character (Caret: ^Y). + EM = 0x19 + // SUB is the substitute character (Caret: ^Z). + SUB = 0x1A + // ESC is the escape character (Caret: ^[, Char: \e). + ESC = 0x1B + // FS is the file separator character (Caret: ^\). + FS = 0x1C + // GS is the group separator character (Caret: ^]). + GS = 0x1D + // RS is the record separator character (Caret: ^^). + RS = 0x1E + // US is the unit separator character (Caret: ^_). + US = 0x1F +) diff --git a/vendor/github.com/charmbracelet/x/ansi/c1.go b/vendor/github.com/charmbracelet/x/ansi/c1.go new file mode 100644 index 00000000..71058f53 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/c1.go @@ -0,0 +1,72 @@ +package ansi + +// C1 control characters. +// +// These range from (0x80-0x9F) as defined in ISO 6429 (ECMA-48). +// See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes +const ( + // PAD is the padding character. + PAD = 0x80 + // HOP is the high octet preset character. + HOP = 0x81 + // BPH is the break permitted here character. + BPH = 0x82 + // NBH is the no break here character. + NBH = 0x83 + // IND is the index character. + IND = 0x84 + // NEL is the next line character. + NEL = 0x85 + // SSA is the start of selected area character. + SSA = 0x86 + // ESA is the end of selected area character. + ESA = 0x87 + // HTS is the horizontal tab set character. + HTS = 0x88 + // HTJ is the horizontal tab with justification character. + HTJ = 0x89 + // VTS is the vertical tab set character. + VTS = 0x8A + // PLD is the partial line forward character. + PLD = 0x8B + // PLU is the partial line backward character. + PLU = 0x8C + // RI is the reverse index character. + RI = 0x8D + // SS2 is the single shift 2 character. + SS2 = 0x8E + // SS3 is the single shift 3 character. + SS3 = 0x8F + // DCS is the device control string character. + DCS = 0x90 + // PU1 is the private use 1 character. + PU1 = 0x91 + // PU2 is the private use 2 character. + PU2 = 0x92 + // STS is the set transmit state character. + STS = 0x93 + // CCH is the cancel character. + CCH = 0x94 + // MW is the message waiting character. + MW = 0x95 + // SPA is the start of guarded area character. + SPA = 0x96 + // EPA is the end of guarded area character. + EPA = 0x97 + // SOS is the start of string character. + SOS = 0x98 + // SGCI is the single graphic character introducer character. + SGCI = 0x99 + // SCI is the single character introducer character. + SCI = 0x9A + // CSI is the control sequence introducer character. + CSI = 0x9B + // ST is the string terminator character. + ST = 0x9C + // OSC is the operating system command character. + OSC = 0x9D + // PM is the privacy message character. + PM = 0x9E + // APC is the application program command character. + APC = 0x9F +) diff --git a/vendor/github.com/charmbracelet/x/ansi/clipboard.go b/vendor/github.com/charmbracelet/x/ansi/clipboard.go new file mode 100644 index 00000000..94d26c36 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/clipboard.go @@ -0,0 +1,75 @@ +package ansi + +import "encoding/base64" + +// Clipboard names. +const ( + SystemClipboard = 'c' + PrimaryClipboard = 'p' +) + +// SetClipboard returns a sequence for manipulating the clipboard. +// +// OSC 52 ; Pc ; Pd ST +// OSC 52 ; Pc ; Pd BEL +// +// Where Pc is the clipboard name and Pd is the base64 encoded data. +// Empty data or invalid base64 data will reset the clipboard. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func SetClipboard(c byte, d string) string { + if d != "" { + d = base64.StdEncoding.EncodeToString([]byte(d)) + } + return "\x1b]52;" + string(c) + ";" + d + "\x07" +} + +// SetSystemClipboard returns a sequence for setting the system clipboard. +// +// This is equivalent to SetClipboard(SystemClipboard, d). +func SetSystemClipboard(d string) string { + return SetClipboard(SystemClipboard, d) +} + +// SetPrimaryClipboard returns a sequence for setting the primary clipboard. +// +// This is equivalent to SetClipboard(PrimaryClipboard, d). +func SetPrimaryClipboard(d string) string { + return SetClipboard(PrimaryClipboard, d) +} + +// ResetClipboard returns a sequence for resetting the clipboard. +// +// This is equivalent to SetClipboard(c, ""). +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func ResetClipboard(c byte) string { + return SetClipboard(c, "") +} + +// ResetSystemClipboard is a sequence for resetting the system clipboard. +// +// This is equivalent to ResetClipboard(SystemClipboard). +const ResetSystemClipboard = "\x1b]52;c;\x07" + +// ResetPrimaryClipboard is a sequence for resetting the primary clipboard. +// +// This is equivalent to ResetClipboard(PrimaryClipboard). +const ResetPrimaryClipboard = "\x1b]52;p;\x07" + +// RequestClipboard returns a sequence for requesting the clipboard. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func RequestClipboard(c byte) string { + return "\x1b]52;" + string(c) + ";?\x07" +} + +// RequestSystemClipboard is a sequence for requesting the system clipboard. +// +// This is equivalent to RequestClipboard(SystemClipboard). +const RequestSystemClipboard = "\x1b]52;c;?\x07" + +// RequestPrimaryClipboard is a sequence for requesting the primary clipboard. +// +// This is equivalent to RequestClipboard(PrimaryClipboard). +const RequestPrimaryClipboard = "\x1b]52;p;?\x07" diff --git a/vendor/github.com/charmbracelet/x/ansi/color.go b/vendor/github.com/charmbracelet/x/ansi/color.go new file mode 100644 index 00000000..2ff78bd7 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/color.go @@ -0,0 +1,196 @@ +package ansi + +import ( + "image/color" +) + +// Technically speaking, the 16 basic ANSI colors are arbitrary and can be +// customized at the terminal level. Given that, we're returning what we feel +// are good defaults. +// +// This could also be a slice, but we use a map to make the mappings very +// explicit. +// +// See: https://www.ditig.com/publications/256-colors-cheat-sheet +var lowANSI = map[uint32]uint32{ + 0: 0x000000, // black + 1: 0x800000, // red + 2: 0x008000, // green + 3: 0x808000, // yellow + 4: 0x000080, // blue + 5: 0x800080, // magenta + 6: 0x008080, // cyan + 7: 0xc0c0c0, // white + 8: 0x808080, // bright black + 9: 0xff0000, // bright red + 10: 0x00ff00, // bright green + 11: 0xffff00, // bright yellow + 12: 0x0000ff, // bright blue + 13: 0xff00ff, // bright magenta + 14: 0x00ffff, // bright cyan + 15: 0xffffff, // bright white +} + +// Color is a color that can be used in a terminal. ANSI (including +// ANSI256) and 24-bit "true colors" fall under this category. +type Color interface { + color.Color +} + +// BasicColor is an ANSI 3-bit or 4-bit color with a value from 0 to 15. +type BasicColor uint8 + +var _ Color = BasicColor(0) + +const ( + // Black is the ANSI black color. + Black BasicColor = iota + + // Red is the ANSI red color. + Red + + // Green is the ANSI green color. + Green + + // Yellow is the ANSI yellow color. + Yellow + + // Blue is the ANSI blue color. + Blue + + // Magenta is the ANSI magenta color. + Magenta + + // Cyan is the ANSI cyan color. + Cyan + + // White is the ANSI white color. + White + + // BrightBlack is the ANSI bright black color. + BrightBlack + + // BrightRed is the ANSI bright red color. + BrightRed + + // BrightGreen is the ANSI bright green color. + BrightGreen + + // BrightYellow is the ANSI bright yellow color. + BrightYellow + + // BrightBlue is the ANSI bright blue color. + BrightBlue + + // BrightMagenta is the ANSI bright magenta color. + BrightMagenta + + // BrightCyan is the ANSI bright cyan color. + BrightCyan + + // BrightWhite is the ANSI bright white color. + BrightWhite +) + +// RGBA returns the red, green, blue and alpha components of the color. It +// satisfies the color.Color interface. +func (c BasicColor) RGBA() (uint32, uint32, uint32, uint32) { + ansi := uint32(c) + if ansi > 15 { + return 0, 0, 0, 0xffff + } + + r, g, b := ansiToRGB(ansi) + return toRGBA(r, g, b) +} + +// ExtendedColor is an ANSI 256 (8-bit) color with a value from 0 to 255. +type ExtendedColor uint8 + +var _ Color = ExtendedColor(0) + +// RGBA returns the red, green, blue and alpha components of the color. It +// satisfies the color.Color interface. +func (c ExtendedColor) RGBA() (uint32, uint32, uint32, uint32) { + r, g, b := ansiToRGB(uint32(c)) + return toRGBA(r, g, b) +} + +// TrueColor is a 24-bit color that can be used in the terminal. +// This can be used to represent RGB colors. +// +// For example, the color red can be represented as: +// +// TrueColor(0xff0000) +type TrueColor uint32 + +var _ Color = TrueColor(0) + +// RGBA returns the red, green, blue and alpha components of the color. It +// satisfies the color.Color interface. +func (c TrueColor) RGBA() (uint32, uint32, uint32, uint32) { + r, g, b := hexToRGB(uint32(c)) + return toRGBA(r, g, b) +} + +// ansiToRGB converts an ANSI color to a 24-bit RGB color. +// +// r, g, b := ansiToRGB(57) +func ansiToRGB(ansi uint32) (uint32, uint32, uint32) { + // For out-of-range values return black. + if ansi > 255 { + return 0, 0, 0 + } + + // Low ANSI. + if ansi < 16 { + h, ok := lowANSI[ansi] + if !ok { + return 0, 0, 0 + } + r, g, b := hexToRGB(h) + return r, g, b + } + + // Grays. + if ansi > 231 { + s := (ansi-232)*10 + 8 + return s, s, s + } + + // ANSI256. + n := ansi - 16 + b := n % 6 + g := (n - b) / 6 % 6 + r := (n - b - g*6) / 36 % 6 + for _, v := range []*uint32{&r, &g, &b} { + if *v > 0 { + c := *v*40 + 55 + *v = c + } + } + + return r, g, b +} + +// hexToRGB converts a number in hexadecimal format to red, green, and blue +// values. +// +// r, g, b := hexToRGB(0x0000FF) +func hexToRGB(hex uint32) (uint32, uint32, uint32) { + return hex >> 16, hex >> 8 & 0xff, hex & 0xff +} + +// toRGBA converts an RGB 8-bit color values to 32-bit color values suitable +// for color.Color. +// +// color.Color requires 16-bit color values, so we duplicate the 8-bit values +// to fill the 16-bit values. +// +// This always returns 0xffff (opaque) for the alpha channel. +func toRGBA(r, g, b uint32) (uint32, uint32, uint32, uint32) { + r |= r << 8 + g |= g << 8 + b |= b << 8 + return r, g, b, 0xffff +} diff --git a/vendor/github.com/charmbracelet/x/ansi/csi.go b/vendor/github.com/charmbracelet/x/ansi/csi.go new file mode 100644 index 00000000..b7e5bd2d --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/csi.go @@ -0,0 +1,141 @@ +package ansi + +import ( + "bytes" + "strconv" + + "github.com/charmbracelet/x/ansi/parser" +) + +// CsiSequence represents a control sequence introducer (CSI) sequence. +// +// The sequence starts with a CSI sequence, CSI (0x9B) in a 8-bit environment +// or ESC [ (0x1B 0x5B) in a 7-bit environment, followed by any number of +// parameters in the range of 0x30-0x3F, then by any number of intermediate +// byte in the range of 0x20-0x2F, then finally with a single final byte in the +// range of 0x20-0x7E. +// +// CSI P..P I..I F +// +// See ECMA-48 § 5.4. +type CsiSequence struct { + // Params contains the raw parameters of the sequence. + // This is a slice of integers, where each integer is a 32-bit integer + // containing the parameter value in the lower 31 bits and a flag in the + // most significant bit indicating whether there are more sub-parameters. + Params []int + + // Cmd contains the raw command of the sequence. + // The command is a 32-bit integer containing the CSI command byte in the + // lower 8 bits, the private marker in the next 8 bits, and the intermediate + // byte in the next 8 bits. + // + // CSI ? u + // + // Is represented as: + // + // 'u' | '?' << 8 + Cmd int +} + +var _ Sequence = CsiSequence{} + +// Marker returns the marker byte of the CSI sequence. +// This is always gonna be one of the following '<' '=' '>' '?' and in the +// range of 0x3C-0x3F. +// Zero is returned if the sequence does not have a marker. +func (s CsiSequence) Marker() int { + return parser.Marker(s.Cmd) +} + +// Intermediate returns the intermediate byte of the CSI sequence. +// An intermediate byte is in the range of 0x20-0x2F. This includes these +// characters from ' ', '!', '"', '#', '$', '%', '&', â€', '(', ')', '*', '+', +// ',', '-', '.', '/'. +// Zero is returned if the sequence does not have an intermediate byte. +func (s CsiSequence) Intermediate() int { + return parser.Intermediate(s.Cmd) +} + +// Command returns the command byte of the CSI sequence. +func (s CsiSequence) Command() int { + return parser.Command(s.Cmd) +} + +// Param returns the parameter at the given index. +// It returns -1 if the parameter does not exist. +func (s CsiSequence) Param(i int) int { + return parser.Param(s.Params, i) +} + +// HasMore returns true if the parameter has more sub-parameters. +func (s CsiSequence) HasMore(i int) bool { + return parser.HasMore(s.Params, i) +} + +// Subparams returns the sub-parameters of the given parameter. +// It returns nil if the parameter does not exist. +func (s CsiSequence) Subparams(i int) []int { + return parser.Subparams(s.Params, i) +} + +// Len returns the number of parameters in the sequence. +// This will return the number of parameters in the sequence, excluding any +// sub-parameters. +func (s CsiSequence) Len() int { + return parser.Len(s.Params) +} + +// Range iterates over the parameters of the sequence and calls the given +// function for each parameter. +// The function should return false to stop the iteration. +func (s CsiSequence) Range(fn func(i int, param int, hasMore bool) bool) { + parser.Range(s.Params, fn) +} + +// Clone returns a copy of the CSI sequence. +func (s CsiSequence) Clone() Sequence { + return CsiSequence{ + Params: append([]int(nil), s.Params...), + Cmd: s.Cmd, + } +} + +// String returns a string representation of the sequence. +// The string will always be in the 7-bit format i.e (ESC [ P..P I..I F). +func (s CsiSequence) String() string { + return s.buffer().String() +} + +// buffer returns a buffer containing the sequence. +func (s CsiSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteString("\x1b[") + if m := s.Marker(); m != 0 { + b.WriteByte(byte(m)) + } + s.Range(func(i, param int, hasMore bool) bool { + if param >= 0 { + b.WriteString(strconv.Itoa(param)) + } + if i < len(s.Params)-1 { + if hasMore { + b.WriteByte(':') + } else { + b.WriteByte(';') + } + } + return true + }) + if i := s.Intermediate(); i != 0 { + b.WriteByte(byte(i)) + } + b.WriteByte(byte(s.Command())) + return &b +} + +// Bytes returns the byte representation of the sequence. +// The bytes will always be in the 7-bit format i.e (ESC [ P..P I..I F). +func (s CsiSequence) Bytes() []byte { + return s.buffer().Bytes() +} diff --git a/vendor/github.com/charmbracelet/x/ansi/ctrl.go b/vendor/github.com/charmbracelet/x/ansi/ctrl.go new file mode 100644 index 00000000..21beb9cd --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/ctrl.go @@ -0,0 +1,17 @@ +package ansi + +// RequestXTVersion is a control sequence that requests the terminal's XTVERSION. It responds with a DSR sequence identifying the version. +// +// CSI > Ps q +// DCS > | text ST +// +// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys +const RequestXTVersion = "\x1b[>0q" + +// RequestPrimaryDeviceAttributes is a control sequence that requests the +// terminal's primary device attributes (DA1). +// +// CSI c +// +// See https://vt100.net/docs/vt510-rm/DA1.html +const RequestPrimaryDeviceAttributes = "\x1b[c" diff --git a/vendor/github.com/charmbracelet/x/ansi/cursor.go b/vendor/github.com/charmbracelet/x/ansi/cursor.go new file mode 100644 index 00000000..ec1fc4d2 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/cursor.go @@ -0,0 +1,235 @@ +package ansi + +import "strconv" + +// SaveCursor (DECSC) is an escape sequence that saves the current cursor +// position. +// +// ESC 7 +// +// See: https://vt100.net/docs/vt510-rm/DECSC.html +const SaveCursor = "\x1b7" + +// RestoreCursor (DECRC) is an escape sequence that restores the cursor +// position. +// +// ESC 8 +// +// See: https://vt100.net/docs/vt510-rm/DECRC.html +const RestoreCursor = "\x1b8" + +// RequestCursorPosition (CPR) is an escape sequence that requests the current +// cursor position. +// +// CSI 6 n +// +// The terminal will report the cursor position as a CSI sequence in the +// following format: +// +// CSI Pl ; Pc R +// +// Where Pl is the line number and Pc is the column number. +// See: https://vt100.net/docs/vt510-rm/CPR.html +const RequestCursorPosition = "\x1b[6n" + +// RequestExtendedCursorPosition (DECXCPR) is a sequence for requesting the +// cursor position report including the current page number. +// +// CSI ? 6 n +// +// The terminal will report the cursor position as a CSI sequence in the +// following format: +// +// CSI ? Pl ; Pc ; Pp R +// +// Where Pl is the line number, Pc is the column number, and Pp is the page +// number. +// See: https://vt100.net/docs/vt510-rm/DECXCPR.html +const RequestExtendedCursorPosition = "\x1b[?6n" + +// CursorUp (CUU) returns a sequence for moving the cursor up n cells. +// +// CSI n A +// +// See: https://vt100.net/docs/vt510-rm/CUU.html +func CursorUp(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "A" +} + +// CursorUp1 is a sequence for moving the cursor up one cell. +// +// This is equivalent to CursorUp(1). +const CursorUp1 = "\x1b[A" + +// CursorDown (CUD) returns a sequence for moving the cursor down n cells. +// +// CSI n B +// +// See: https://vt100.net/docs/vt510-rm/CUD.html +func CursorDown(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "B" +} + +// CursorDown1 is a sequence for moving the cursor down one cell. +// +// This is equivalent to CursorDown(1). +const CursorDown1 = "\x1b[B" + +// CursorRight (CUF) returns a sequence for moving the cursor right n cells. +// +// CSI n C +// +// See: https://vt100.net/docs/vt510-rm/CUF.html +func CursorRight(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "C" +} + +// CursorRight1 is a sequence for moving the cursor right one cell. +// +// This is equivalent to CursorRight(1). +const CursorRight1 = "\x1b[C" + +// CursorLeft (CUB) returns a sequence for moving the cursor left n cells. +// +// CSI n D +// +// See: https://vt100.net/docs/vt510-rm/CUB.html +func CursorLeft(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "D" +} + +// CursorLeft1 is a sequence for moving the cursor left one cell. +// +// This is equivalent to CursorLeft(1). +const CursorLeft1 = "\x1b[D" + +// CursorNextLine (CNL) returns a sequence for moving the cursor to the +// beginning of the next line n times. +// +// CSI n E +// +// See: https://vt100.net/docs/vt510-rm/CNL.html +func CursorNextLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "E" +} + +// CursorPreviousLine (CPL) returns a sequence for moving the cursor to the +// beginning of the previous line n times. +// +// CSI n F +// +// See: https://vt100.net/docs/vt510-rm/CPL.html +func CursorPreviousLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "F" +} + +// MoveCursor (CUP) returns a sequence for moving the cursor to the given row +// and column. +// +// CSI n ; m H +// +// See: https://vt100.net/docs/vt510-rm/CUP.html +func MoveCursor(row, col int) string { + if row < 0 { + row = 0 + } + if col < 0 { + col = 0 + } + return "\x1b[" + strconv.Itoa(row) + ";" + strconv.Itoa(col) + "H" +} + +// MoveCursorOrigin is a sequence for moving the cursor to the upper left +// corner of the screen. This is equivalent to MoveCursor(1, 1). +const MoveCursorOrigin = "\x1b[1;1H" + +// SaveCursorPosition (SCP or SCOSC) is a sequence for saving the cursor +// position. +// +// CSI s +// +// This acts like Save, except the page number where the cursor is located is +// not saved. +// +// See: https://vt100.net/docs/vt510-rm/SCOSC.html +const SaveCursorPosition = "\x1b[s" + +// RestoreCursorPosition (RCP or SCORC) is a sequence for restoring the cursor +// position. +// +// CSI u +// +// This acts like Restore, except the cursor stays on the same page where the +// cursor was saved. +// +// See: https://vt100.net/docs/vt510-rm/SCORC.html +const RestoreCursorPosition = "\x1b[u" + +// SetCursorStyle (DECSCUSR) returns a sequence for changing the cursor style. +// +// CSI Ps SP q +// +// Where Ps is the cursor style: +// +// 0: Blinking block +// 1: Blinking block (default) +// 2: Steady block +// 3: Blinking underline +// 4: Steady underline +// 5: Blinking bar (xterm) +// 6: Steady bar (xterm) +// +// See: https://vt100.net/docs/vt510-rm/DECSCUSR.html +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps-SP-q.1D81 +func SetCursorStyle(style int) string { + if style < 0 { + style = 0 + } + return "\x1b[" + strconv.Itoa(style) + " q" +} + +// SetPointerShape returns a sequence for changing the mouse pointer cursor +// shape. Use "default" for the default pointer shape. +// +// OSC 22 ; Pt ST +// OSC 22 ; Pt BEL +// +// Where Pt is the pointer shape name. The name can be anything that the +// operating system can understand. Some common names are: +// +// - copy +// - crosshair +// - default +// - ew-resize +// - n-resize +// - text +// - wait +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetPointerShape(shape string) string { + return "\x1b]22;" + shape + "\x07" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/dcs.go b/vendor/github.com/charmbracelet/x/ansi/dcs.go new file mode 100644 index 00000000..185f0b52 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/dcs.go @@ -0,0 +1,148 @@ +package ansi + +import ( + "bytes" + "strconv" + + "github.com/charmbracelet/x/ansi/parser" +) + +// DcsSequence represents a Device Control String (DCS) escape sequence. +// +// The DCS sequence is used to send device control strings to the terminal. The +// sequence starts with the C1 control code character DCS (0x9B) or ESC P in +// 7-bit environments, followed by parameter bytes, intermediate bytes, a +// command byte, followed by data bytes, and ends with the C1 control code +// character ST (0x9C) or ESC \ in 7-bit environments. +// +// This follows the parameter string format. +// See ECMA-48 § 5.4.1 +type DcsSequence struct { + // Params contains the raw parameters of the sequence. + // This is a slice of integers, where each integer is a 32-bit integer + // containing the parameter value in the lower 31 bits and a flag in the + // most significant bit indicating whether there are more sub-parameters. + Params []int + + // Data contains the string raw data of the sequence. + // This is the data between the final byte and the escape sequence terminator. + Data []byte + + // Cmd contains the raw command of the sequence. + // The command is a 32-bit integer containing the DCS command byte in the + // lower 8 bits, the private marker in the next 8 bits, and the intermediate + // byte in the next 8 bits. + // + // DCS > 0 ; 1 $ r ST + // + // Is represented as: + // + // 'r' | '>' << 8 | '$' << 16 + Cmd int +} + +var _ Sequence = DcsSequence{} + +// Marker returns the marker byte of the DCS sequence. +// This is always gonna be one of the following '<' '=' '>' '?' and in the +// range of 0x3C-0x3F. +// Zero is returned if the sequence does not have a marker. +func (s DcsSequence) Marker() int { + return parser.Marker(s.Cmd) +} + +// Intermediate returns the intermediate byte of the DCS sequence. +// An intermediate byte is in the range of 0x20-0x2F. This includes these +// characters from ' ', '!', '"', '#', '$', '%', '&', â€', '(', ')', '*', '+', +// ',', '-', '.', '/'. +// Zero is returned if the sequence does not have an intermediate byte. +func (s DcsSequence) Intermediate() int { + return parser.Intermediate(s.Cmd) +} + +// Command returns the command byte of the CSI sequence. +func (s DcsSequence) Command() int { + return parser.Command(s.Cmd) +} + +// Param returns the parameter at the given index. +// It returns -1 if the parameter does not exist. +func (s DcsSequence) Param(i int) int { + return parser.Param(s.Params, i) +} + +// HasMore returns true if the parameter has more sub-parameters. +func (s DcsSequence) HasMore(i int) bool { + return parser.HasMore(s.Params, i) +} + +// Subparams returns the sub-parameters of the given parameter. +// It returns nil if the parameter does not exist. +func (s DcsSequence) Subparams(i int) []int { + return parser.Subparams(s.Params, i) +} + +// Len returns the number of parameters in the sequence. +// This will return the number of parameters in the sequence, excluding any +// sub-parameters. +func (s DcsSequence) Len() int { + return parser.Len(s.Params) +} + +// Range iterates over the parameters of the sequence and calls the given +// function for each parameter. +// The function should return false to stop the iteration. +func (s DcsSequence) Range(fn func(i int, param int, hasMore bool) bool) { + parser.Range(s.Params, fn) +} + +// Clone returns a copy of the DCS sequence. +func (s DcsSequence) Clone() Sequence { + return DcsSequence{ + Params: append([]int(nil), s.Params...), + Data: append([]byte(nil), s.Data...), + Cmd: s.Cmd, + } +} + +// String returns a string representation of the sequence. +// The string will always be in the 7-bit format i.e (ESC P p..p i..i f ESC \). +func (s DcsSequence) String() string { + return s.buffer().String() +} + +// buffer returns a buffer containing the sequence. +func (s DcsSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteString("\x1bP") + if m := s.Marker(); m != 0 { + b.WriteByte(byte(m)) + } + s.Range(func(i, param int, hasMore bool) bool { + if param >= -1 { + b.WriteString(strconv.Itoa(param)) + } + if i < len(s.Params)-1 { + if hasMore { + b.WriteByte(':') + } else { + b.WriteByte(';') + } + } + return true + }) + if i := s.Intermediate(); i != 0 { + b.WriteByte(byte(i)) + } + b.WriteByte(byte(s.Command())) + b.Write(s.Data) + b.WriteByte(ESC) + b.WriteByte('\\') + return &b +} + +// Bytes returns the byte representation of the sequence. +// The bytes will always be in the 7-bit format i.e (ESC P p..p i..i F ESC \). +func (s DcsSequence) Bytes() []byte { + return s.buffer().Bytes() +} diff --git a/vendor/github.com/charmbracelet/x/ansi/doc.go b/vendor/github.com/charmbracelet/x/ansi/doc.go new file mode 100644 index 00000000..e955e9f1 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/doc.go @@ -0,0 +1,7 @@ +// Package ansi defines common ANSI escape sequences based on the ECMA-48 +// specs. +// +// All sequences use 7-bit C1 control codes, which are supported by most +// terminal emulators. OSC sequences are terminated by a BEL for wider +// compatibility with terminals. +package ansi diff --git a/vendor/github.com/charmbracelet/x/ansi/hyperlink.go b/vendor/github.com/charmbracelet/x/ansi/hyperlink.go new file mode 100644 index 00000000..323bfe93 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/hyperlink.go @@ -0,0 +1,28 @@ +package ansi + +import "strings" + +// SetHyperlink returns a sequence for starting a hyperlink. +// +// OSC 8 ; Params ; Uri ST +// OSC 8 ; Params ; Uri BEL +// +// To reset the hyperlink, omit the URI. +// +// See: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda +func SetHyperlink(uri string, params ...string) string { + var p string + if len(params) > 0 { + p = strings.Join(params, ":") + } + return "\x1b]8;" + p + ";" + uri + "\x07" +} + +// ResetHyperlink returns a sequence for resetting the hyperlink. +// +// This is equivalent to SetHyperlink("", params...). +// +// See: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda +func ResetHyperlink(params ...string) string { + return SetHyperlink("", params...) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/kitty.go b/vendor/github.com/charmbracelet/x/ansi/kitty.go new file mode 100644 index 00000000..c56d8d1c --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/kitty.go @@ -0,0 +1,90 @@ +package ansi + +import "strconv" + +// Kitty keyboard protocol progressive enhancement flags. +// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +const ( + KittyDisambiguateEscapeCodes = 1 << iota + KittyReportEventTypes + KittyReportAlternateKeys + KittyReportAllKeysAsEscapeCodes + KittyReportAssociatedKeys + + KittyAllFlags = KittyDisambiguateEscapeCodes | KittyReportEventTypes | + KittyReportAlternateKeys | KittyReportAllKeysAsEscapeCodes | KittyReportAssociatedKeys +) + +// RequestKittyKeyboard is a sequence to request the terminal Kitty keyboard +// protocol enabled flags. +// +// See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/ +const RequestKittyKeyboard = "\x1b[?u" + +// KittyKeyboard returns a sequence to request keyboard enhancements from the terminal. +// The flags argument is a bitmask of the Kitty keyboard protocol flags. While +// mode specifies how the flags should be interpreted. +// +// Possible values for flags mask: +// +// 1: Disambiguate escape codes +// 2: Report event types +// 4: Report alternate keys +// 8: Report all keys as escape codes +// 16: Report associated text +// +// Possible values for mode: +// +// 1: Set given flags and unset all others +// 2: Set given flags and keep existing flags unchanged +// 3: Unset given flags and keep existing flags unchanged +// +// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +func KittyKeyboard(flags, mode int) string { + return "\x1b[=" + strconv.Itoa(flags) + ";" + strconv.Itoa(mode) + "u" +} + +// PushKittyKeyboard returns a sequence to push the given flags to the terminal +// Kitty Keyboard stack. +// +// Possible values for flags mask: +// +// 0: Disable all features +// 1: Disambiguate escape codes +// 2: Report event types +// 4: Report alternate keys +// 8: Report all keys as escape codes +// 16: Report associated text +// +// CSI > flags u +// +// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +func PushKittyKeyboard(flags int) string { + var f string + if flags > 0 { + f = strconv.Itoa(flags) + } + + return "\x1b[>" + f + "u" +} + +// DisableKittyKeyboard is a sequence to push zero into the terminal Kitty +// Keyboard stack to disable the protocol. +// +// This is equivalent to PushKittyKeyboard(0). +const DisableKittyKeyboard = "\x1b[>0u" + +// PopKittyKeyboard returns a sequence to pop n number of flags from the +// terminal Kitty Keyboard stack. +// +// CSI < flags u +// +// See https://sw.kovidgoyal.net/kitty/keyboard-protocol/#progressive-enhancement +func PopKittyKeyboard(n int) string { + var num string + if n > 0 { + num = strconv.Itoa(n) + } + + return "\x1b[<" + num + "u" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/mode.go b/vendor/github.com/charmbracelet/x/ansi/mode.go new file mode 100644 index 00000000..5d705434 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/mode.go @@ -0,0 +1,179 @@ +package ansi + +// This file define uses multiple sequences to set (SM), reset (RM), and request +// (DECRQM) different ANSI and DEC modes. +// +// See: https://vt100.net/docs/vt510-rm/SM.html +// See: https://vt100.net/docs/vt510-rm/RM.html +// See: https://vt100.net/docs/vt510-rm/DECRQM.html +// +// The terminal then responds to the request with a Report Mode function +// (DECRPM) in the format: +// +// ANSI format: +// +// CSI Pa ; Ps ; $ y +// +// DEC format: +// +// CSI ? Pa ; Ps $ y +// +// Where Pa is the mode number, and Ps is the mode value. +// See: https://vt100.net/docs/vt510-rm/DECRPM.html + +// Application Cursor Keys (DECCKM) is a mode that determines whether the +// cursor keys send ANSI cursor sequences or application sequences. +// +// See: https://vt100.net/docs/vt510-rm/DECCKM.html +const ( + CursorKeysMode = "?1" + + EnableCursorKeys = "\x1b[" + CursorKeysMode + "h" + DisableCursorKeys = "\x1b[" + CursorKeysMode + "l" + RequestCursorKeys = "\x1b[" + CursorKeysMode + "$p" +) + +// Text Cursor Enable Mode (DECTCEM) is a mode that shows/hides the cursor. +// +// See: https://vt100.net/docs/vt510-rm/DECTCEM.html +const ( + CursorVisibilityMode = "?25" + + ShowCursor = "\x1b[" + CursorVisibilityMode + "h" + HideCursor = "\x1b[" + CursorVisibilityMode + "l" + RequestCursorVisibility = "\x1b[" + CursorVisibilityMode + "$p" +) + +// VT Mouse Tracking is a mode that determines whether the mouse reports on +// button press and release. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + MouseMode = "?1000" + + EnableMouse = "\x1b[" + MouseMode + "h" + DisableMouse = "\x1b[" + MouseMode + "l" + RequestMouse = "\x1b[" + MouseMode + "$p" +) + +// VT Hilite Mouse Tracking is a mode that determines whether the mouse reports on +// button presses, releases, and highlighted cells. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + MouseHiliteMode = "?1001" + + EnableMouseHilite = "\x1b[" + MouseHiliteMode + "h" + DisableMouseHilite = "\x1b[" + MouseHiliteMode + "l" + RequestMouseHilite = "\x1b[" + MouseHiliteMode + "$p" +) + +// Cell Motion Mouse Tracking is a mode that determines whether the mouse +// reports on button press, release, and motion events. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + MouseCellMotionMode = "?1002" + + EnableMouseCellMotion = "\x1b[" + MouseCellMotionMode + "h" + DisableMouseCellMotion = "\x1b[" + MouseCellMotionMode + "l" + RequestMouseCellMotion = "\x1b[" + MouseCellMotionMode + "$p" +) + +// All Mouse Tracking is a mode that determines whether the mouse reports on +// button press, release, motion, and highlight events. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + MouseAllMotionMode = "?1003" + + EnableMouseAllMotion = "\x1b[" + MouseAllMotionMode + "h" + DisableMouseAllMotion = "\x1b[" + MouseAllMotionMode + "l" + RequestMouseAllMotion = "\x1b[" + MouseAllMotionMode + "$p" +) + +// Report Focus is a mode that makes the terminal report focus-in and focus-out events. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-FocusIn_FocusOut +const ( + ReportFocusMode = "?1004" + + EnableReportFocus = "\x1b[" + ReportFocusMode + "h" + DisableReportFocus = "\x1b[" + ReportFocusMode + "l" + RequestReportFocus = "\x1b[" + ReportFocusMode + "$p" +) + +// SGR Mouse Extension is a mode that determines whether the mouse reports events +// formatted with SGR parameters. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Mouse-Tracking +const ( + MouseSgrExtMode = "?1006" + + EnableMouseSgrExt = "\x1b[" + MouseSgrExtMode + "h" + DisableMouseSgrExt = "\x1b[" + MouseSgrExtMode + "l" + RequestMouseSgrExt = "\x1b[" + MouseSgrExtMode + "$p" +) + +// Alternate Screen Buffer is a mode that determines whether the alternate screen +// buffer is active. +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer +const ( + AltScreenBufferMode = "?1049" + + EnableAltScreenBuffer = "\x1b[" + AltScreenBufferMode + "h" + DisableAltScreenBuffer = "\x1b[" + AltScreenBufferMode + "l" + RequestAltScreenBuffer = "\x1b[" + AltScreenBufferMode + "$p" +) + +// Bracketed Paste Mode is a mode that determines whether pasted text is +// bracketed with escape sequences. +// +// See: https://cirw.in/blog/bracketed-paste +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode +const ( + BracketedPasteMode = "?2004" + + EnableBracketedPaste = "\x1b[" + BracketedPasteMode + "h" + DisableBracketedPaste = "\x1b[" + BracketedPasteMode + "l" + RequestBracketedPaste = "\x1b[" + BracketedPasteMode + "$p" +) + +// Synchronized Output Mode is a mode that determines whether output is +// synchronized with the terminal. +// +// See: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036 +const ( + SyncdOutputMode = "?2026" + + EnableSyncdOutput = "\x1b[" + SyncdOutputMode + "h" + DisableSyncdOutput = "\x1b[" + SyncdOutputMode + "l" + RequestSyncdOutput = "\x1b[" + SyncdOutputMode + "$p" +) + +// Grapheme Clustering Mode is a mode that determines whether the terminal +// should look for grapheme clusters instead of single runes in the rendered +// text. This makes the terminal properly render combining characters such as +// emojis. +// +// See: https://github.com/contour-terminal/terminal-unicode-core +const ( + GraphemeClusteringMode = "?2027" + + EnableGraphemeClustering = "\x1b[" + GraphemeClusteringMode + "h" + DisableGraphemeClustering = "\x1b[" + GraphemeClusteringMode + "l" + RequestGraphemeClustering = "\x1b[" + GraphemeClusteringMode + "$p" +) + +// Win32Input is a mode that determines whether input is processed by the +// Win32 console and Conpty. +// +// See: https://github.com/microsoft/terminal/blob/main/doc/specs/%234999%20-%20Improved%20keyboard%20handling%20in%20Conpty.md +const ( + Win32InputMode = "?9001" + + EnableWin32Input = "\x1b[" + Win32InputMode + "h" + DisableWin32Input = "\x1b[" + Win32InputMode + "l" + RequestWin32Input = "\x1b[" + Win32InputMode + "$p" +) diff --git a/vendor/github.com/charmbracelet/x/ansi/osc.go b/vendor/github.com/charmbracelet/x/ansi/osc.go new file mode 100644 index 00000000..40b543c2 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/osc.go @@ -0,0 +1,69 @@ +package ansi + +import ( + "bytes" + "strings" +) + +// OscSequence represents an OSC sequence. +// +// The sequence starts with a OSC sequence, OSC (0x9D) in a 8-bit environment +// or ESC ] (0x1B 0x5D) in a 7-bit environment, followed by positive integer identifier, +// then by arbitrary data terminated by a ST (0x9C) in a 8-bit environment, +// ESC \ (0x1B 0x5C) in a 7-bit environment, or BEL (0x07) for backwards compatibility. +// +// OSC Ps ; Pt ST +// OSC Ps ; Pt BEL +// +// See ECMA-48 § 5.7. +type OscSequence struct { + // Data contains the raw data of the sequence including the identifier + // command. + Data []byte + + // Cmd contains the raw command of the sequence. + Cmd int +} + +var _ Sequence = OscSequence{} + +// Command returns the command of the OSC sequence. +func (s OscSequence) Command() int { + return s.Cmd +} + +// Params returns the parameters of the OSC sequence split by ';'. +// The first element is the identifier command. +func (s OscSequence) Params() []string { + return strings.Split(string(s.Data), ";") +} + +// Clone returns a copy of the OSC sequence. +func (s OscSequence) Clone() Sequence { + return OscSequence{ + Data: append([]byte(nil), s.Data...), + Cmd: s.Cmd, + } +} + +// String returns the string representation of the OSC sequence. +// To be more compatible with different terminal, this will always return a +// 7-bit formatted sequence, terminated by BEL. +func (s OscSequence) String() string { + return s.buffer().String() +} + +// Bytes returns the byte representation of the OSC sequence. +// To be more compatible with different terminal, this will always return a +// 7-bit formatted sequence, terminated by BEL. +func (s OscSequence) Bytes() []byte { + return s.buffer().Bytes() +} + +func (s OscSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteString("\x1b]") + b.Write(s.Data) + b.WriteByte(BEL) + return &b +} diff --git a/vendor/github.com/charmbracelet/x/ansi/params.go b/vendor/github.com/charmbracelet/x/ansi/params.go new file mode 100644 index 00000000..a1bb4249 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/params.go @@ -0,0 +1,45 @@ +package ansi + +import ( + "bytes" +) + +// Params parses and returns a list of control sequence parameters. +// +// Parameters are positive integers separated by semicolons. Empty parameters +// default to zero. Parameters can have sub-parameters separated by colons. +// +// Any non-parameter bytes are ignored. This includes bytes that are not in the +// range of 0x30-0x3B. +// +// See ECMA-48 § 5.4.1. +func Params(p []byte) [][]uint { + if len(p) == 0 { + return [][]uint{} + } + + // Filter out non-parameter bytes i.e. non 0x30-0x3B. + p = bytes.TrimFunc(p, func(r rune) bool { + return r < 0x30 || r > 0x3B + }) + + parts := bytes.Split(p, []byte{';'}) + params := make([][]uint, len(parts)) + for i, part := range parts { + sparts := bytes.Split(part, []byte{':'}) + params[i] = make([]uint, len(sparts)) + for j, spart := range sparts { + params[i][j] = bytesToUint16(spart) + } + } + + return params +} + +func bytesToUint16(b []byte) uint { + var n uint + for _, c := range b { + n = n*10 + uint(c-'0') + } + return n +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser.go b/vendor/github.com/charmbracelet/x/ansi/parser.go new file mode 100644 index 00000000..e1a09df7 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser.go @@ -0,0 +1,363 @@ +package ansi + +import ( + "unicode/utf8" + "unsafe" + + "github.com/charmbracelet/x/ansi/parser" +) + +// ParserDispatcher is a function that dispatches a sequence. +type ParserDispatcher func(Sequence) + +// Parser represents a DEC ANSI compatible sequence parser. +// +// It uses a state machine to parse ANSI escape sequences and control +// characters. The parser is designed to be used with a terminal emulator or +// similar application that needs to parse ANSI escape sequences and control +// characters. +// See package [parser] for more information. +// +//go:generate go run ./gen.go +type Parser struct { + // Params contains the raw parameters of the sequence. + // These parameters used when constructing CSI and DCS sequences. + Params []int + + // Data contains the raw data of the sequence. + // These data used when constructing OSC, DCS, SOS, PM, and APC sequences. + Data []byte + + // DataLen keeps track of the length of the data buffer. + // If DataLen is -1, the data buffer is unlimited and will grow as needed. + // Otherwise, DataLen is limited by the size of the Data buffer. + DataLen int + + // ParamsLen keeps track of the number of parameters. + // This is limited by the size of the Params buffer. + // + // This is also used when collecting UTF-8 runes to keep track of the + // number of rune bytes collected. + ParamsLen int + + // Cmd contains the raw command along with the private marker and + // intermediate bytes of the sequence. + // The first lower byte contains the command byte, the next byte contains + // the private marker, and the next byte contains the intermediate byte. + // + // This is also used when collecting UTF-8 runes treating it as a slice of + // 4 bytes. + Cmd int + + // State is the current state of the parser. + State byte +} + +// NewParser returns a new parser with the given sizes allocated. +// If dataSize is zero, the underlying data buffer will be unlimited and will +// grow as needed. +func NewParser(paramsSize, dataSize int) *Parser { + s := new(Parser) + if dataSize <= 0 { + dataSize = 0 + s.DataLen = -1 + } + s.Params = make([]int, paramsSize) + s.Data = make([]byte, dataSize) + return s +} + +// Reset resets the parser to its initial state. +func (p *Parser) Reset() { + p.clear() + p.State = parser.GroundState +} + +// clear clears the parser parameters and command. +func (p *Parser) clear() { + if len(p.Params) > 0 { + p.Params[0] = parser.MissingParam + } + p.ParamsLen = 0 + p.Cmd = 0 +} + +// StateName returns the name of the current state. +func (p *Parser) StateName() string { + return parser.StateNames[p.State] +} + +// Parse parses the given dispatcher and byte buffer. +func (p *Parser) Parse(dispatcher ParserDispatcher, b []byte) { + for i := 0; i < len(b); i++ { + p.Advance(dispatcher, b[i], i < len(b)-1) + } +} + +// Advance advances the parser with the given dispatcher and byte. +func (p *Parser) Advance(dispatcher ParserDispatcher, b byte, more bool) parser.Action { + switch p.State { + case parser.Utf8State: + // We handle UTF-8 here. + return p.advanceUtf8(dispatcher, b) + default: + return p.advance(dispatcher, b, more) + } +} + +func (p *Parser) collectRune(b byte) { + if p.ParamsLen >= utf8.UTFMax { + return + } + + shift := p.ParamsLen * 8 + p.Cmd &^= 0xff << shift + p.Cmd |= int(b) << shift + p.ParamsLen++ +} + +func (p *Parser) advanceUtf8(dispatcher ParserDispatcher, b byte) parser.Action { + // Collect UTF-8 rune bytes. + p.collectRune(b) + rw := utf8ByteLen(byte(p.Cmd & 0xff)) + if rw == -1 { + // We panic here because the first byte comes from the state machine, + // if this panics, it means there is a bug in the state machine! + panic("invalid rune") // unreachable + } + + if p.ParamsLen < rw { + return parser.CollectAction + } + + // We have enough bytes to decode the rune using unsafe + r, _ := utf8.DecodeRune((*[utf8.UTFMax]byte)(unsafe.Pointer(&p.Cmd))[:rw]) + if dispatcher != nil { + dispatcher(Rune(r)) + } + + p.State = parser.GroundState + p.ParamsLen = 0 + + return parser.PrintAction +} + +func (p *Parser) advance(d ParserDispatcher, b byte, more bool) parser.Action { + state, action := parser.Table.Transition(p.State, b) + + // We need to clear the parser state if the state changes from EscapeState. + // This is because when we enter the EscapeState, we don't get a chance to + // clear the parser state. For example, when a sequence terminates with a + // ST (\x1b\\ or \x9c), we dispatch the current sequence and transition to + // EscapeState. However, the parser state is not cleared in this case and + // we need to clear it here before dispatching the esc sequence. + if p.State != state { + if p.State == parser.EscapeState { + p.performAction(d, parser.ClearAction, state, b) + } + if action == parser.PutAction && + p.State == parser.DcsEntryState && state == parser.DcsStringState { + // XXX: This is a special case where we need to start collecting + // non-string parameterized data i.e. doesn't follow the ECMA-48 § + // 5.4.1 string parameters format. + p.performAction(d, parser.StartAction, state, 0) + } + } + + // Handle special cases + switch { + case b == ESC && p.State == parser.EscapeState: + // Two ESCs in a row + p.performAction(d, parser.ExecuteAction, state, b) + if !more { + // Two ESCs at the end of the buffer + p.performAction(d, parser.ExecuteAction, state, b) + } + case b == ESC && !more: + // Last byte is an ESC + p.performAction(d, parser.ExecuteAction, state, b) + case p.State == parser.EscapeState && b == 'P' && !more: + // ESC P (DCS) at the end of the buffer + p.performAction(d, parser.DispatchAction, state, b) + case p.State == parser.EscapeState && b == 'X' && !more: + // ESC X (SOS) at the end of the buffer + p.performAction(d, parser.DispatchAction, state, b) + case p.State == parser.EscapeState && b == '[' && !more: + // ESC [ (CSI) at the end of the buffer + p.performAction(d, parser.DispatchAction, state, b) + case p.State == parser.EscapeState && b == ']' && !more: + // ESC ] (OSC) at the end of the buffer + p.performAction(d, parser.DispatchAction, state, b) + case p.State == parser.EscapeState && b == '^' && !more: + // ESC ^ (PM) at the end of the buffer + p.performAction(d, parser.DispatchAction, state, b) + case p.State == parser.EscapeState && b == '_' && !more: + // ESC _ (APC) at the end of the buffer + p.performAction(d, parser.DispatchAction, state, b) + default: + p.performAction(d, action, state, b) + } + + p.State = state + + return action +} + +func (p *Parser) performAction(dispatcher ParserDispatcher, action parser.Action, state parser.State, b byte) { + switch action { + case parser.IgnoreAction: + break + + case parser.ClearAction: + p.clear() + + case parser.PrintAction: + if dispatcher != nil { + dispatcher(Rune(b)) + } + + case parser.ExecuteAction: + if dispatcher != nil { + dispatcher(ControlCode(b)) + } + + case parser.MarkerAction: + // Collect private marker + // we only store the last marker + p.Cmd &^= 0xff << parser.MarkerShift + p.Cmd |= int(b) << parser.MarkerShift + + case parser.CollectAction: + if state == parser.Utf8State { + // Reset the UTF-8 counter + p.ParamsLen = 0 + p.collectRune(b) + } else { + // Collect intermediate bytes + // we only store the last intermediate byte + p.Cmd &^= 0xff << parser.IntermedShift + p.Cmd |= int(b) << parser.IntermedShift + } + + case parser.ParamAction: + // Collect parameters + if p.ParamsLen >= len(p.Params) { + break + } + + if b >= '0' && b <= '9' { + if p.Params[p.ParamsLen] == parser.MissingParam { + p.Params[p.ParamsLen] = 0 + } + + p.Params[p.ParamsLen] *= 10 + p.Params[p.ParamsLen] += int(b - '0') + } + + if b == ':' { + p.Params[p.ParamsLen] |= parser.HasMoreFlag + } + + if b == ';' || b == ':' { + p.ParamsLen++ + if p.ParamsLen < len(p.Params) { + p.Params[p.ParamsLen] = parser.MissingParam + } + } + + case parser.StartAction: + if p.DataLen < 0 && p.Data != nil { + p.Data = p.Data[:0] + } else { + p.DataLen = 0 + } + if p.State >= parser.DcsEntryState && p.State <= parser.DcsStringState { + // Collect the command byte for DCS + p.Cmd |= int(b) + } else { + p.Cmd = parser.MissingCommand + } + + case parser.PutAction: + switch p.State { + case parser.OscStringState: + if b == ';' && p.Cmd == parser.MissingCommand { + // Try to parse the command + datalen := len(p.Data) + if p.DataLen >= 0 { + datalen = p.DataLen + } + for i := 0; i < datalen; i++ { + d := p.Data[i] + if d < '0' || d > '9' { + break + } + if p.Cmd == parser.MissingCommand { + p.Cmd = 0 + } + p.Cmd *= 10 + p.Cmd += int(d - '0') + } + } + } + + if p.DataLen < 0 { + p.Data = append(p.Data, b) + } else { + if p.DataLen < len(p.Data) { + p.Data[p.DataLen] = b + p.DataLen++ + } + } + + case parser.DispatchAction: + // Increment the last parameter + if p.ParamsLen > 0 && p.ParamsLen < len(p.Params)-1 || + p.ParamsLen == 0 && len(p.Params) > 0 && p.Params[0] != parser.MissingParam { + p.ParamsLen++ + } + + if dispatcher == nil { + break + } + + var seq Sequence + data := p.Data + if p.DataLen >= 0 { + data = data[:p.DataLen] + } + switch p.State { + case parser.CsiEntryState, parser.CsiParamState, parser.CsiIntermediateState: + p.Cmd |= int(b) + seq = CsiSequence{Cmd: p.Cmd, Params: p.Params[:p.ParamsLen]} + case parser.EscapeState, parser.EscapeIntermediateState: + p.Cmd |= int(b) + seq = EscSequence(p.Cmd) + case parser.DcsEntryState, parser.DcsParamState, parser.DcsIntermediateState, parser.DcsStringState: + seq = DcsSequence{Cmd: p.Cmd, Params: p.Params[:p.ParamsLen], Data: data} + case parser.OscStringState: + seq = OscSequence{Cmd: p.Cmd, Data: data} + case parser.SosStringState: + seq = SosSequence{Data: data} + case parser.PmStringState: + seq = PmSequence{Data: data} + case parser.ApcStringState: + seq = ApcSequence{Data: data} + } + + dispatcher(seq) + } +} + +func utf8ByteLen(b byte) int { + if b <= 0b0111_1111 { // 0x00-0x7F + return 1 + } else if b >= 0b1100_0000 && b <= 0b1101_1111 { // 0xC0-0xDF + return 2 + } else if b >= 0b1110_0000 && b <= 0b1110_1111 { // 0xE0-0xEF + return 3 + } else if b >= 0b1111_0000 && b <= 0b1111_0111 { // 0xF0-0xF7 + return 4 + } + return -1 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/const.go b/vendor/github.com/charmbracelet/x/ansi/parser/const.go new file mode 100644 index 00000000..54b7383b --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser/const.go @@ -0,0 +1,78 @@ +package parser + +// Action is a DEC ANSI parser action. +type Action = byte + +// These are the actions that the parser can take. +const ( + NoneAction Action = iota + ClearAction + CollectAction + MarkerAction + DispatchAction + ExecuteAction + StartAction // Start of a data string + PutAction // Put into the data string + ParamAction + PrintAction + + IgnoreAction = NoneAction +) + +// nolint: unused +var ActionNames = []string{ + "NoneAction", + "ClearAction", + "CollectAction", + "MarkerAction", + "DispatchAction", + "ExecuteAction", + "StartAction", + "PutAction", + "ParamAction", + "PrintAction", +} + +// State is a DEC ANSI parser state. +type State = byte + +// These are the states that the parser can be in. +const ( + GroundState State = iota + CsiEntryState + CsiIntermediateState + CsiParamState + DcsEntryState + DcsIntermediateState + DcsParamState + DcsStringState + EscapeState + EscapeIntermediateState + OscStringState + SosStringState + PmStringState + ApcStringState + + // Utf8State is not part of the DEC ANSI standard. It is used to handle + // UTF-8 sequences. + Utf8State +) + +// nolint: unused +var StateNames = []string{ + "GroundState", + "CsiEntryState", + "CsiIntermediateState", + "CsiParamState", + "DcsEntryState", + "DcsIntermediateState", + "DcsParamState", + "DcsStringState", + "EscapeState", + "EscapeIntermediateState", + "OscStringState", + "SosStringState", + "PmStringState", + "ApcStringState", + "Utf8State", +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/seq.go b/vendor/github.com/charmbracelet/x/ansi/parser/seq.go new file mode 100644 index 00000000..c99f1632 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser/seq.go @@ -0,0 +1,136 @@ +package parser + +import "math" + +// Shift and masks for sequence parameters and intermediates. +const ( + MarkerShift = 8 + IntermedShift = 16 + CommandMask = 0xff + HasMoreFlag = math.MinInt32 + ParamMask = ^HasMoreFlag + MissingParam = ParamMask + MissingCommand = MissingParam + MaxParam = math.MaxUint16 // the maximum value a parameter can have +) + +const ( + // MaxParamsSize is the maximum number of parameters a sequence can have. + MaxParamsSize = 32 + + // DefaultParamValue is the default value used for missing parameters. + DefaultParamValue = 0 +) + +// Marker returns the marker byte of the sequence. +// This is always gonna be one of the following '<' '=' '>' '?' and in the +// range of 0x3C-0x3F. +// Zero is returned if the sequence does not have a marker. +func Marker(cmd int) int { + return (cmd >> MarkerShift) & CommandMask +} + +// Intermediate returns the intermediate byte of the sequence. +// An intermediate byte is in the range of 0x20-0x2F. This includes these +// characters from ' ', '!', '"', '#', '$', '%', '&', â€', '(', ')', '*', '+', +// ',', '-', '.', '/'. +// Zero is returned if the sequence does not have an intermediate byte. +func Intermediate(cmd int) int { + return (cmd >> IntermedShift) & CommandMask +} + +// Command returns the command byte of the CSI sequence. +func Command(cmd int) int { + return cmd & CommandMask +} + +// Param returns the parameter at the given index. +// It returns -1 if the parameter does not exist. +func Param(params []int, i int) int { + if len(params) == 0 || i < 0 || i >= len(params) { + return -1 + } + + p := params[i] & ParamMask + if p == MissingParam { + return -1 + } + + return p +} + +// HasMore returns true if the parameter has more sub-parameters. +func HasMore(params []int, i int) bool { + if len(params) == 0 || i >= len(params) { + return false + } + + return params[i]&HasMoreFlag != 0 +} + +// Subparams returns the sub-parameters of the given parameter. +// It returns nil if the parameter does not exist. +func Subparams(params []int, i int) []int { + if len(params) == 0 || i < 0 || i >= len(params) { + return nil + } + + // Count the number of parameters before the given parameter index. + var count int + var j int + for j = 0; j < len(params); j++ { + if count == i { + break + } + if !HasMore(params, j) { + count++ + } + } + + if count > i || j >= len(params) { + return nil + } + + var subs []int + for ; j < len(params); j++ { + if !HasMore(params, j) { + break + } + p := Param(params, j) + if p == -1 { + p = DefaultParamValue + } + subs = append(subs, p) + } + + p := Param(params, j) + if p == -1 { + p = DefaultParamValue + } + + return append(subs, p) +} + +// Len returns the number of parameters in the sequence. +// This will return the number of parameters in the sequence, excluding any +// sub-parameters. +func Len(params []int) int { + var n int + for i := 0; i < len(params); i++ { + if !HasMore(params, i) { + n++ + } + } + return n +} + +// Range iterates over the parameters of the sequence and calls the given +// function for each parameter. +// The function should return false to stop the iteration. +func Range(params []int, fn func(i int, param int, hasMore bool) bool) { + for i := 0; i < len(params); i++ { + if !fn(i, Param(params, i), HasMore(params, i)) { + break + } + } +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go b/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go new file mode 100644 index 00000000..5d368ebf --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser/transition_table.go @@ -0,0 +1,273 @@ +package parser + +// Table values are generated like this: +// +// index: currentState << IndexStateShift | charCode +// value: action << TransitionActionShift | nextState +const ( + TransitionActionShift = 4 + TransitionStateMask = 15 + IndexStateShift = 8 + + // DefaultTableSize is the default size of the transition table. + DefaultTableSize = 4096 +) + +// Table is a DEC ANSI transition table. +var Table = GenerateTransitionTable() + +// TransitionTable is a DEC ANSI transition table. +// https://vt100.net/emu/dec_ansi_parser +type TransitionTable []byte + +// NewTransitionTable returns a new DEC ANSI transition table. +func NewTransitionTable(size int) TransitionTable { + if size <= 0 { + size = DefaultTableSize + } + return TransitionTable(make([]byte, size)) +} + +// SetDefault sets default transition. +func (t TransitionTable) SetDefault(action Action, state State) { + for i := 0; i < len(t); i++ { + t[i] = action<> TransitionActionShift +} + +// byte range macro +func r(start, end byte) []byte { + var a []byte + for i := int(start); i <= int(end); i++ { + a = append(a, byte(i)) + } + return a +} + +// GenerateTransitionTable generates a DEC ANSI transition table compatible +// with the VT500-series of terminals. This implementation includes a few +// modifications that include: +// - A new Utf8State is introduced to handle UTF8 sequences. +// - Osc and Dcs data accept UTF8 sequences by extending the printable range +// to 0xFF and 0xFE respectively. +// - We don't ignore 0x3A (':') when building Csi and Dcs parameters and +// instead use it to denote sub-parameters. +// - Support dispatching SosPmApc sequences. +// - The DEL (0x7F) character is executed in the Ground state. +// - The DEL (0x7F) character is collected in the DcsPassthrough string state. +// - The ST C1 control character (0x9C) is executed and not ignored. +func GenerateTransitionTable() TransitionTable { + table := NewTransitionTable(DefaultTableSize) + table.SetDefault(NoneAction, GroundState) + + // Anywhere + for _, state := range r(GroundState, Utf8State) { + // Anywhere -> Ground + table.AddMany([]byte{0x18, 0x1a, 0x99, 0x9a}, state, ExecuteAction, GroundState) + table.AddRange(0x80, 0x8F, state, ExecuteAction, GroundState) + table.AddRange(0x90, 0x97, state, ExecuteAction, GroundState) + table.AddOne(0x9C, state, ExecuteAction, GroundState) + // Anywhere -> Escape + table.AddOne(0x1B, state, ClearAction, EscapeState) + // Anywhere -> SosStringState + table.AddOne(0x98, state, StartAction, SosStringState) + // Anywhere -> PmStringState + table.AddOne(0x9E, state, StartAction, PmStringState) + // Anywhere -> ApcStringState + table.AddOne(0x9F, state, StartAction, ApcStringState) + // Anywhere -> CsiEntry + table.AddOne(0x9B, state, ClearAction, CsiEntryState) + // Anywhere -> DcsEntry + table.AddOne(0x90, state, ClearAction, DcsEntryState) + // Anywhere -> OscString + table.AddOne(0x9D, state, StartAction, OscStringState) + // Anywhere -> Utf8 + table.AddRange(0xC2, 0xDF, state, CollectAction, Utf8State) // UTF8 2 byte sequence + table.AddRange(0xE0, 0xEF, state, CollectAction, Utf8State) // UTF8 3 byte sequence + table.AddRange(0xF0, 0xF4, state, CollectAction, Utf8State) // UTF8 4 byte sequence + } + + // Ground + table.AddRange(0x00, 0x17, GroundState, ExecuteAction, GroundState) + table.AddOne(0x19, GroundState, ExecuteAction, GroundState) + table.AddRange(0x1C, 0x1F, GroundState, ExecuteAction, GroundState) + table.AddRange(0x20, 0x7E, GroundState, PrintAction, GroundState) + table.AddOne(0x7F, GroundState, ExecuteAction, GroundState) + + // EscapeIntermediate + table.AddRange(0x00, 0x17, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState) + table.AddOne(0x19, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState) + table.AddRange(0x1C, 0x1F, EscapeIntermediateState, ExecuteAction, EscapeIntermediateState) + table.AddRange(0x20, 0x2F, EscapeIntermediateState, CollectAction, EscapeIntermediateState) + table.AddOne(0x7F, EscapeIntermediateState, IgnoreAction, EscapeIntermediateState) + // EscapeIntermediate -> Ground + table.AddRange(0x30, 0x7E, EscapeIntermediateState, DispatchAction, GroundState) + + // Escape + table.AddRange(0x00, 0x17, EscapeState, ExecuteAction, EscapeState) + table.AddOne(0x19, EscapeState, ExecuteAction, EscapeState) + table.AddRange(0x1C, 0x1F, EscapeState, ExecuteAction, EscapeState) + table.AddOne(0x7F, EscapeState, IgnoreAction, EscapeState) + // Escape -> Ground + table.AddRange(0x30, 0x4F, EscapeState, DispatchAction, GroundState) + table.AddRange(0x51, 0x57, EscapeState, DispatchAction, GroundState) + table.AddOne(0x59, EscapeState, DispatchAction, GroundState) + table.AddOne(0x5A, EscapeState, DispatchAction, GroundState) + table.AddOne(0x5C, EscapeState, DispatchAction, GroundState) + table.AddRange(0x60, 0x7E, EscapeState, DispatchAction, GroundState) + // Escape -> Escape_intermediate + table.AddRange(0x20, 0x2F, EscapeState, CollectAction, EscapeIntermediateState) + // Escape -> Sos_pm_apc_string + table.AddOne('X', EscapeState, StartAction, SosStringState) // SOS + table.AddOne('^', EscapeState, StartAction, PmStringState) // PM + table.AddOne('_', EscapeState, StartAction, ApcStringState) // APC + // Escape -> Dcs_entry + table.AddOne('P', EscapeState, ClearAction, DcsEntryState) + // Escape -> Csi_entry + table.AddOne('[', EscapeState, ClearAction, CsiEntryState) + // Escape -> Osc_string + table.AddOne(']', EscapeState, StartAction, OscStringState) + + // Sos_pm_apc_string + for _, state := range r(SosStringState, ApcStringState) { + table.AddRange(0x00, 0x17, state, PutAction, state) + table.AddOne(0x19, state, PutAction, state) + table.AddRange(0x1C, 0x1F, state, PutAction, state) + table.AddRange(0x20, 0x7F, state, PutAction, state) + // ESC, ST, CAN, and SUB terminate the sequence + table.AddOne(0x1B, state, DispatchAction, EscapeState) + table.AddOne(0x9C, state, DispatchAction, GroundState) + table.AddMany([]byte{0x18, 0x1A}, state, IgnoreAction, GroundState) + } + + // Dcs_entry + table.AddRange(0x00, 0x07, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddRange(0x0E, 0x17, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddOne(0x19, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddRange(0x1C, 0x1F, DcsEntryState, IgnoreAction, DcsEntryState) + table.AddOne(0x7F, DcsEntryState, IgnoreAction, DcsEntryState) + // Dcs_entry -> Dcs_intermediate + table.AddRange(0x20, 0x2F, DcsEntryState, CollectAction, DcsIntermediateState) + // Dcs_entry -> Dcs_param + table.AddRange(0x30, 0x3B, DcsEntryState, ParamAction, DcsParamState) + table.AddRange(0x3C, 0x3F, DcsEntryState, MarkerAction, DcsParamState) + // Dcs_entry -> Dcs_passthrough + table.AddRange(0x08, 0x0D, DcsEntryState, PutAction, DcsStringState) // Follows ECMA-48 § 8.3.27 + // XXX: allows passing ESC (not a ECMA-48 standard) this to allow for + // passthrough of ANSI sequences like in Screen or Tmux passthrough mode. + table.AddOne(0x1B, DcsEntryState, PutAction, DcsStringState) + table.AddRange(0x40, 0x7E, DcsEntryState, StartAction, DcsStringState) + + // Dcs_intermediate + table.AddRange(0x00, 0x17, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + table.AddOne(0x19, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + table.AddRange(0x1C, 0x1F, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + table.AddRange(0x20, 0x2F, DcsIntermediateState, CollectAction, DcsIntermediateState) + table.AddOne(0x7F, DcsIntermediateState, IgnoreAction, DcsIntermediateState) + // Dcs_intermediate -> Dcs_passthrough + table.AddRange(0x30, 0x3F, DcsIntermediateState, StartAction, DcsStringState) + table.AddRange(0x40, 0x7E, DcsIntermediateState, StartAction, DcsStringState) + + // Dcs_param + table.AddRange(0x00, 0x17, DcsParamState, IgnoreAction, DcsParamState) + table.AddOne(0x19, DcsParamState, IgnoreAction, DcsParamState) + table.AddRange(0x1C, 0x1F, DcsParamState, IgnoreAction, DcsParamState) + table.AddRange(0x30, 0x3B, DcsParamState, ParamAction, DcsParamState) + table.AddOne(0x7F, DcsParamState, IgnoreAction, DcsParamState) + table.AddRange(0x3C, 0x3F, DcsParamState, IgnoreAction, DcsParamState) + // Dcs_param -> Dcs_intermediate + table.AddRange(0x20, 0x2F, DcsParamState, CollectAction, DcsIntermediateState) + // Dcs_param -> Dcs_passthrough + table.AddRange(0x40, 0x7E, DcsParamState, StartAction, DcsStringState) + + // Dcs_passthrough + table.AddRange(0x00, 0x17, DcsStringState, PutAction, DcsStringState) + table.AddOne(0x19, DcsStringState, PutAction, DcsStringState) + table.AddRange(0x1C, 0x1F, DcsStringState, PutAction, DcsStringState) + table.AddRange(0x20, 0x7E, DcsStringState, PutAction, DcsStringState) + table.AddOne(0x7F, DcsStringState, PutAction, DcsStringState) + table.AddRange(0x80, 0xFF, DcsStringState, PutAction, DcsStringState) // Allow Utf8 characters by extending the printable range from 0x7F to 0xFF + // ST, CAN, SUB, and ESC terminate the sequence + table.AddOne(0x1B, DcsStringState, DispatchAction, EscapeState) + table.AddOne(0x9C, DcsStringState, DispatchAction, GroundState) + table.AddMany([]byte{0x18, 0x1A}, DcsStringState, IgnoreAction, GroundState) + + // Csi_param + table.AddRange(0x00, 0x17, CsiParamState, ExecuteAction, CsiParamState) + table.AddOne(0x19, CsiParamState, ExecuteAction, CsiParamState) + table.AddRange(0x1C, 0x1F, CsiParamState, ExecuteAction, CsiParamState) + table.AddRange(0x30, 0x3B, CsiParamState, ParamAction, CsiParamState) + table.AddOne(0x7F, CsiParamState, IgnoreAction, CsiParamState) + table.AddRange(0x3C, 0x3F, CsiParamState, IgnoreAction, CsiParamState) + // Csi_param -> Ground + table.AddRange(0x40, 0x7E, CsiParamState, DispatchAction, GroundState) + // Csi_param -> Csi_intermediate + table.AddRange(0x20, 0x2F, CsiParamState, CollectAction, CsiIntermediateState) + + // Csi_intermediate + table.AddRange(0x00, 0x17, CsiIntermediateState, ExecuteAction, CsiIntermediateState) + table.AddOne(0x19, CsiIntermediateState, ExecuteAction, CsiIntermediateState) + table.AddRange(0x1C, 0x1F, CsiIntermediateState, ExecuteAction, CsiIntermediateState) + table.AddRange(0x20, 0x2F, CsiIntermediateState, CollectAction, CsiIntermediateState) + table.AddOne(0x7F, CsiIntermediateState, IgnoreAction, CsiIntermediateState) + // Csi_intermediate -> Ground + table.AddRange(0x40, 0x7E, CsiIntermediateState, DispatchAction, GroundState) + // Csi_intermediate -> Csi_ignore + table.AddRange(0x30, 0x3F, CsiIntermediateState, IgnoreAction, GroundState) + + // Csi_entry + table.AddRange(0x00, 0x17, CsiEntryState, ExecuteAction, CsiEntryState) + table.AddOne(0x19, CsiEntryState, ExecuteAction, CsiEntryState) + table.AddRange(0x1C, 0x1F, CsiEntryState, ExecuteAction, CsiEntryState) + table.AddOne(0x7F, CsiEntryState, IgnoreAction, CsiEntryState) + // Csi_entry -> Ground + table.AddRange(0x40, 0x7E, CsiEntryState, DispatchAction, GroundState) + // Csi_entry -> Csi_intermediate + table.AddRange(0x20, 0x2F, CsiEntryState, CollectAction, CsiIntermediateState) + // Csi_entry -> Csi_param + table.AddRange(0x30, 0x3B, CsiEntryState, ParamAction, CsiParamState) + table.AddRange(0x3C, 0x3F, CsiEntryState, MarkerAction, CsiParamState) + + // Osc_string + table.AddRange(0x00, 0x06, OscStringState, IgnoreAction, OscStringState) + table.AddRange(0x08, 0x17, OscStringState, IgnoreAction, OscStringState) + table.AddOne(0x19, OscStringState, IgnoreAction, OscStringState) + table.AddRange(0x1C, 0x1F, OscStringState, IgnoreAction, OscStringState) + table.AddRange(0x20, 0xFF, OscStringState, PutAction, OscStringState) // Allow Utf8 characters by extending the printable range from 0x7F to 0xFF + + // ST, CAN, SUB, ESC, and BEL terminate the sequence + table.AddOne(0x1B, OscStringState, DispatchAction, EscapeState) + table.AddOne(0x07, OscStringState, DispatchAction, GroundState) + table.AddOne(0x9C, OscStringState, DispatchAction, GroundState) + table.AddMany([]byte{0x18, 0x1A}, OscStringState, IgnoreAction, GroundState) + + return table +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser_decode.go b/vendor/github.com/charmbracelet/x/ansi/parser_decode.go new file mode 100644 index 00000000..76688d0b --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser_decode.go @@ -0,0 +1,423 @@ +package ansi + +import ( + "bytes" + "strings" + "unicode/utf8" + + "github.com/charmbracelet/x/ansi/parser" + "github.com/rivo/uniseg" +) + +// State represents the state of the ANSI escape sequence parser used by +// [DecodeSequence]. +type State = byte + +// ANSI escape sequence states used by [DecodeSequence]. +const ( + NormalState State = iota + MarkerState + ParamsState + IntermedState + EscapeState + StringState +) + +// DecodeSequence decodes the first ANSI escape sequence or a printable +// grapheme from the given data. It returns the sequence slice, the number of +// bytes read, the cell width for each sequence, and the new state. +// +// The cell width will always be 0 for control and escape sequences, 1 for +// ASCII printable characters, and the number of cells other Unicode characters +// occupy. It uses the uniseg package to calculate the width of Unicode +// graphemes and characters. This means it will always do grapheme clustering +// (mode 2027). +// +// Passing a non-nil [*Parser] as the last argument will allow the decoder to +// collect sequence parameters, data, and commands. The parser cmd will have +// the packed command value that contains intermediate and marker characters. +// In the case of a OSC sequence, the cmd will be the OSC command number. Use +// [Cmd] and [Param] types to unpack command intermediates and markers as well +// as parameters. +// +// Zero [p.Cmd] means the CSI, DCS, or ESC sequence is invalid. Moreover, checking the +// validity of other data sequences, OSC, DCS, etc, will require checking for +// the returned sequence terminator bytes such as ST (ESC \\) and BEL). +// +// We store the command byte in [p.Cmd] in the most significant byte, the +// marker byte in the next byte, and the intermediate byte in the least +// significant byte. This is done to avoid using a struct to store the command +// and its intermediates and markers. The command byte is always the least +// significant byte i.e. [p.Cmd & 0xff]. Use the [Cmd] type to unpack the +// command, intermediate, and marker bytes. Note that we only collect the last +// marker character and intermediate byte. +// +// The [p.Params] slice will contain the parameters of the sequence. Any +// sub-parameter will have the [parser.HasMoreFlag] set. Use the [Param] type +// to unpack the parameters. +// +// Example: +// +// var state byte // the initial state is always zero [NormalState] +// p := NewParser(32, 1024) // create a new parser with a 32 params buffer and 1024 data buffer (optional) +// input := []byte("\x1b[31mHello, World!\x1b[0m") +// for len(input) > 0 { +// seq, width, n, newState := DecodeSequence(input, state, p) +// log.Printf("seq: %q, width: %d", seq, width) +// state = newState +// input = input[n:] +// } +func DecodeSequence[T string | []byte](b T, state byte, p *Parser) (seq T, width int, n int, newState byte) { + for i := 0; i < len(b); i++ { + c := b[i] + + switch state { + case NormalState: + switch c { + case ESC: + if p != nil { + if len(p.Params) > 0 { + p.Params[0] = parser.MissingParam + } + p.Cmd = 0 + p.ParamsLen = 0 + p.DataLen = 0 + } + state = EscapeState + continue + case CSI, DCS: + if p != nil { + if len(p.Params) > 0 { + p.Params[0] = parser.MissingParam + } + p.Cmd = 0 + p.ParamsLen = 0 + p.DataLen = 0 + } + state = MarkerState + continue + case OSC, APC, SOS, PM: + if p != nil { + p.Cmd = parser.MissingCommand + p.DataLen = 0 + } + state = StringState + continue + } + + if p != nil { + p.DataLen = 0 + p.ParamsLen = 0 + p.Cmd = 0 + } + if c > US && c < DEL { + // ASCII printable characters + return b[i : i+1], 1, 1, NormalState + } + + if c <= US || c == DEL || c < 0xC0 { + // C0 & C1 control characters & DEL + return b[i : i+1], 0, 1, NormalState + } + + if utf8.RuneStart(c) { + seq, _, width, _ = FirstGraphemeCluster(b, -1) + i += len(seq) + return b[:i], width, i, NormalState + } + + // Invalid UTF-8 sequence + return b[:i], 0, i, NormalState + case MarkerState: + if c >= '<' && c <= '?' { + if p != nil { + // We only collect the last marker character. + p.Cmd &^= 0xff << parser.MarkerShift + p.Cmd |= int(c) << parser.MarkerShift + } + break + } + + state = ParamsState + fallthrough + case ParamsState: + if c >= '0' && c <= '9' { + if p != nil { + if p.Params[p.ParamsLen] == parser.MissingParam { + p.Params[p.ParamsLen] = 0 + } + + p.Params[p.ParamsLen] *= 10 + p.Params[p.ParamsLen] += int(c - '0') + } + break + } + + if c == ':' { + if p != nil { + p.Params[p.ParamsLen] |= parser.HasMoreFlag + } + } + + if c == ';' || c == ':' { + if p != nil { + p.ParamsLen++ + if p.ParamsLen < len(p.Params) { + p.Params[p.ParamsLen] = parser.MissingParam + } + } + break + } + + state = IntermedState + fallthrough + case IntermedState: + if c >= ' ' && c <= '/' { + if p != nil { + p.Cmd &^= 0xff << parser.IntermedShift + p.Cmd |= int(c) << parser.IntermedShift + } + break + } + + state = NormalState + if c >= '@' && c <= '~' { + if p != nil { + // Increment the last parameter + if p.ParamsLen > 0 && p.ParamsLen < len(p.Params)-1 || + p.ParamsLen == 0 && len(p.Params) > 0 && p.Params[0] != parser.MissingParam { + p.ParamsLen++ + } + + p.Cmd &^= 0xff + p.Cmd |= int(c) + } + + if HasDcsPrefix(b) { + // Continue to collect DCS data + if p != nil { + p.DataLen = 0 + } + state = StringState + continue + } + + return b[:i+1], 0, i + 1, state + } + + // Invalid CSI/DCS sequence + return b[:i], 0, i, NormalState + case EscapeState: + switch c { + case '[', 'P': + if p != nil { + if len(p.Params) > 0 { + p.Params[0] = parser.MissingParam + } + p.ParamsLen = 0 + p.Cmd = 0 + } + state = MarkerState + continue + case ']', 'X', '^', '_': + if p != nil { + p.Cmd = parser.MissingCommand + p.DataLen = 0 + } + state = StringState + continue + } + + if c >= ' ' && c <= '/' { + if p != nil { + p.Cmd &^= 0xff << parser.IntermedShift + p.Cmd |= int(c) << parser.IntermedShift + } + continue + } else if c >= '0' && c <= '~' { + if p != nil { + p.Cmd &^= 0xff + p.Cmd |= int(c) + } + return b[:i+1], 0, i + 1, NormalState + } + + // Invalid escape sequence + return b[:i], 0, i, NormalState + case StringState: + switch c { + case BEL: + if HasOscPrefix(b) { + return b[:i+1], 0, i + 1, NormalState + } + case CAN, SUB: + // Cancel the sequence + return b[:i], 0, i, NormalState + case ST: + return b[:i+1], 0, i + 1, NormalState + case ESC: + if HasStPrefix(b[i:]) { + // End of string 7-bit (ST) + return b[:i+2], 0, i + 2, NormalState + } + + // Otherwise, cancel the sequence + return b[:i], 0, i, NormalState + } + + if p != nil && p.DataLen < len(p.Data) { + p.Data[p.DataLen] = c + p.DataLen++ + + // Parse the OSC command number + if c == ';' && p.Cmd == parser.MissingCommand && HasOscPrefix(b) { + for j := 0; j < p.DataLen; j++ { + d := p.Data[j] + if d < '0' || d > '9' { + break + } + if p.Cmd == parser.MissingCommand { + p.Cmd = 0 + } + p.Cmd *= 10 + p.Cmd += int(d - '0') + } + } + } + } + } + + return b, 0, len(b), state +} + +// Index returns the index of the first occurrence of the given byte slice in +// the data. It returns -1 if the byte slice is not found. +func Index[T string | []byte](data, b T) int { + switch data := any(data).(type) { + case string: + return strings.Index(data, string(b)) + case []byte: + return bytes.Index(data, []byte(b)) + } + panic("unreachable") +} + +// Equal returns true if the given byte slices are equal. +func Equal[T string | []byte](a, b T) bool { + return string(a) == string(b) +} + +// HasPrefix returns true if the given byte slice has prefix. +func HasPrefix[T string | []byte](b, prefix T) bool { + return len(b) >= len(prefix) && Equal(b[0:len(prefix)], prefix) +} + +// HasSuffix returns true if the given byte slice has suffix. +func HasSuffix[T string | []byte](b, suffix T) bool { + return len(b) >= len(suffix) && Equal(b[len(b)-len(suffix):], suffix) +} + +// HasCsiPrefix returns true if the given byte slice has a CSI prefix. +func HasCsiPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == CSI) || + (len(b) > 1 && b[0] == ESC && b[1] == '[') +} + +// HasOscPrefix returns true if the given byte slice has an OSC prefix. +func HasOscPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == OSC) || + (len(b) > 1 && b[0] == ESC && b[1] == ']') +} + +// HasApcPrefix returns true if the given byte slice has an APC prefix. +func HasApcPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == APC) || + (len(b) > 1 && b[0] == ESC && b[1] == '_') +} + +// HasDcsPrefix returns true if the given byte slice has a DCS prefix. +func HasDcsPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == DCS) || + (len(b) > 1 && b[0] == ESC && b[1] == 'P') +} + +// HasSosPrefix returns true if the given byte slice has a SOS prefix. +func HasSosPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == SOS) || + (len(b) > 1 && b[0] == ESC && b[1] == 'X') +} + +// HasPmPrefix returns true if the given byte slice has a PM prefix. +func HasPmPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == PM) || + (len(b) > 1 && b[0] == ESC && b[1] == '^') +} + +// HasStPrefix returns true if the given byte slice has a ST prefix. +func HasStPrefix[T string | []byte](b T) bool { + return (len(b) > 0 && b[0] == ST) || + (len(b) > 1 && b[0] == ESC && b[1] == '\\') +} + +// HasEscPrefix returns true if the given byte slice has an ESC prefix. +func HasEscPrefix[T string | []byte](b T) bool { + return len(b) > 0 && b[0] == ESC +} + +// FirstGraphemeCluster returns the first grapheme cluster in the given string or byte slice. +// This is a syntactic sugar function that wraps +// uniseg.FirstGraphemeClusterInString and uniseg.FirstGraphemeCluster. +func FirstGraphemeCluster[T string | []byte](b T, state int) (T, T, int, int) { + switch b := any(b).(type) { + case string: + cluster, rest, width, newState := uniseg.FirstGraphemeClusterInString(b, state) + return T(cluster), T(rest), width, newState + case []byte: + cluster, rest, width, newState := uniseg.FirstGraphemeCluster(b, state) + return T(cluster), T(rest), width, newState + } + panic("unreachable") +} + +// Cmd represents a sequence command. This is used to pack/unpack a sequence +// command with its intermediate and marker characters. Those are commonly +// found in CSI and DCS sequences. +type Cmd int + +// Marker returns the marker byte of the CSI sequence. +// This is always gonna be one of the following '<' '=' '>' '?' and in the +// range of 0x3C-0x3F. +// Zero is returned if the sequence does not have a marker. +func (c Cmd) Marker() int { + return parser.Marker(int(c)) +} + +// Intermediate returns the intermediate byte of the CSI sequence. +// An intermediate byte is in the range of 0x20-0x2F. This includes these +// characters from ' ', '!', '"', '#', '$', '%', '&', â€', '(', ')', '*', '+', +// ',', '-', '.', '/'. +// Zero is returned if the sequence does not have an intermediate byte. +func (c Cmd) Intermediate() int { + return parser.Intermediate(int(c)) +} + +// Command returns the command byte of the CSI sequence. +func (c Cmd) Command() int { + return parser.Command(int(c)) +} + +// Param represents a sequence parameter. Sequence parameters with +// sub-parameters are packed with the HasMoreFlag set. This is used to unpack +// the parameters from a CSI and DCS sequences. +type Param int + +// Param returns the parameter at the given index. +// It returns -1 if the parameter does not exist. +func (s Param) Param() int { + return int(s) & parser.ParamMask +} + +// HasMore returns true if the parameter has more sub-parameters. +func (s Param) HasMore() bool { + return int(s)&parser.HasMoreFlag != 0 +} diff --git a/vendor/github.com/charmbracelet/x/ansi/parser_sync.go b/vendor/github.com/charmbracelet/x/ansi/parser_sync.go new file mode 100644 index 00000000..562e806a --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/parser_sync.go @@ -0,0 +1,26 @@ +package ansi + +import ( + "sync" + + "github.com/charmbracelet/x/ansi/parser" +) + +var parserPool = sync.Pool{ + New: func() any { + return NewParser(parser.MaxParamsSize, 1024*4) // 4MB data buffer + }, +} + +// GetParser returns a parser from a sync pool. +func GetParser() *Parser { + return parserPool.Get().(*Parser) +} + +// PutParser returns a parser to a sync pool. The parser is reset +// automatically. +func PutParser(p *Parser) { + p.Reset() + p.DataLen = 0 + parserPool.Put(p) +} diff --git a/vendor/github.com/charmbracelet/x/ansi/passthrough.go b/vendor/github.com/charmbracelet/x/ansi/passthrough.go new file mode 100644 index 00000000..14a74522 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/passthrough.go @@ -0,0 +1,63 @@ +package ansi + +import ( + "bytes" +) + +// ScreenPassthrough wraps the given ANSI sequence in a DCS passthrough +// sequence to be sent to the outer terminal. This is used to send raw escape +// sequences to the outer terminal when running inside GNU Screen. +// +// DCS ST +// +// Note: Screen limits the length of string sequences to 768 bytes (since 2014). +// Use zero to indicate no limit, otherwise, this will chunk the returned +// string into limit sized chunks. +// +// See: https://www.gnu.org/software/screen/manual/screen.html#String-Escapes +// See: https://git.savannah.gnu.org/cgit/screen.git/tree/src/screen.h?id=c184c6ec27683ff1a860c45be5cf520d896fd2ef#n44 +func ScreenPassthrough(seq string, limit int) string { + var b bytes.Buffer + b.WriteString("\x1bP") + if limit > 0 { + for i := 0; i < len(seq); i += limit { + end := i + limit + if end > len(seq) { + end = len(seq) + } + b.WriteString(seq[i:end]) + if end < len(seq) { + b.WriteString("\x1b\\\x1bP") + } + } + } else { + b.WriteString(seq) + } + b.WriteString("\x1b\\") + return b.String() +} + +// TmuxPassthrough wraps the given ANSI sequence in a special DCS passthrough +// sequence to be sent to the outer terminal. This is used to send raw escape +// sequences to the outer terminal when running inside Tmux. +// +// DCS tmux ; ST +// +// Where is the given sequence in which all occurrences of ESC +// (0x1b) are doubled i.e. replaced with ESC ESC (0x1b 0x1b). +// +// Note: this needs the `allow-passthrough` option to be set to `on`. +// +// See: https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it +func TmuxPassthrough(seq string) string { + var b bytes.Buffer + b.WriteString("\x1bPtmux;") + for i := 0; i < len(seq); i++ { + if seq[i] == ESC { + b.WriteByte(ESC) + } + b.WriteByte(seq[i]) + } + b.WriteString("\x1b\\") + return b.String() +} diff --git a/vendor/github.com/charmbracelet/x/ansi/screen.go b/vendor/github.com/charmbracelet/x/ansi/screen.go new file mode 100644 index 00000000..f1fd30c4 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/screen.go @@ -0,0 +1,126 @@ +package ansi + +import "strconv" + +// EraseDisplay (ED) clears the screen or parts of the screen. Possible values: +// +// 0: Clear from cursor to end of screen. +// 1: Clear from cursor to beginning of the screen. +// 2: Clear entire screen (and moves cursor to upper left on DOS). +// 3: Clear entire screen and delete all lines saved in the scrollback buffer. +// +// CSI J +// +// See: https://vt100.net/docs/vt510-rm/ED.html +func EraseDisplay(n int) string { + if n < 0 { + n = 0 + } + return "\x1b[" + strconv.Itoa(n) + "J" +} + +// EraseDisplay constants. +// These are the possible values for the EraseDisplay function. +const ( + EraseDisplayBelow = "\x1b[0J" + EraseDisplayAbove = "\x1b[1J" + EraseEntireDisplay = "\x1b[2J" +) + +// EraseLine (EL) clears the current line or parts of the line. Possible values: +// +// 0: Clear from cursor to end of line. +// 1: Clear from cursor to beginning of the line. +// 2: Clear entire line. +// +// The cursor position is not affected. +// +// CSI K +// +// See: https://vt100.net/docs/vt510-rm/EL.html +func EraseLine(n int) string { + if n < 0 { + n = 0 + } + return "\x1b[" + strconv.Itoa(n) + "K" +} + +// EraseLine constants. +// These are the possible values for the EraseLine function. +const ( + EraseLineRight = "\x1b[0K" + EraseLineLeft = "\x1b[1K" + EraseEntireLine = "\x1b[2K" +) + +// ScrollUp (SU) scrolls the screen up n lines. New lines are added at the +// bottom of the screen. +// +// CSI S +// +// See: https://vt100.net/docs/vt510-rm/SU.html +func ScrollUp(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "S" +} + +// ScrollDown (SD) scrolls the screen down n lines. New lines are added at the +// top of the screen. +// +// CSI T +// +// See: https://vt100.net/docs/vt510-rm/SD.html +func ScrollDown(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "T" +} + +// InsertLine (IL) inserts n blank lines at the current cursor position. +// Existing lines are moved down. +// +// CSI L +// +// See: https://vt100.net/docs/vt510-rm/IL.html +func InsertLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "L" +} + +// DeleteLine (DL) deletes n lines at the current cursor position. Existing +// lines are moved up. +// +// CSI M +// +// See: https://vt100.net/docs/vt510-rm/DL.html +func DeleteLine(n int) string { + var s string + if n > 1 { + s = strconv.Itoa(n) + } + return "\x1b[" + s + "M" +} + +// SetScrollingRegion (DECSTBM) sets the top and bottom margins for the scrolling +// region. The default is the entire screen. +// +// CSI ; r +// +// See: https://vt100.net/docs/vt510-rm/DECSTBM.html +func SetScrollingRegion(t, b int) string { + if t < 0 { + t = 0 + } + if b < 0 { + b = 0 + } + return "\x1b[" + strconv.Itoa(t) + ";" + strconv.Itoa(b) + "r" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/sequence.go b/vendor/github.com/charmbracelet/x/ansi/sequence.go new file mode 100644 index 00000000..f294a229 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/sequence.go @@ -0,0 +1,199 @@ +package ansi + +import ( + "bytes" + + "github.com/charmbracelet/x/ansi/parser" +) + +// Sequence represents an ANSI sequence. This can be a control sequence, escape +// sequence, a printable character, etc. +type Sequence interface { + // String returns the string representation of the sequence. + String() string + // Bytes returns the byte representation of the sequence. + Bytes() []byte + // Clone returns a copy of the sequence. + Clone() Sequence +} + +// Rune represents a printable character. +type Rune rune + +var _ Sequence = Rune(0) + +// Bytes implements Sequence. +func (r Rune) Bytes() []byte { + return []byte(string(r)) +} + +// String implements Sequence. +func (r Rune) String() string { + return string(r) +} + +// Clone implements Sequence. +func (r Rune) Clone() Sequence { + return r +} + +// ControlCode represents a control code character. This is a character that +// is not printable and is used to control the terminal. This would be a +// character in the C0 or C1 set in the range of 0x00-0x1F and 0x80-0x9F. +type ControlCode byte + +var _ Sequence = ControlCode(0) + +// Bytes implements Sequence. +func (c ControlCode) Bytes() []byte { + return []byte{byte(c)} +} + +// String implements Sequence. +func (c ControlCode) String() string { + return string(c) +} + +// Clone implements Sequence. +func (c ControlCode) Clone() Sequence { + return c +} + +// EscSequence represents an escape sequence. +type EscSequence int + +var _ Sequence = EscSequence(0) + +// buffer returns the buffer of the escape sequence. +func (e EscSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteByte('\x1b') + if i := parser.Intermediate(int(e)); i != 0 { + b.WriteByte(byte(i)) + } + b.WriteByte(byte(e.Command())) + return &b +} + +// Bytes implements Sequence. +func (e EscSequence) Bytes() []byte { + return e.buffer().Bytes() +} + +// String implements Sequence. +func (e EscSequence) String() string { + return e.buffer().String() +} + +// Clone implements Sequence. +func (e EscSequence) Clone() Sequence { + return e +} + +// Command returns the command byte of the escape sequence. +func (e EscSequence) Command() int { + return parser.Command(int(e)) +} + +// Intermediate returns the intermediate byte of the escape sequence. +func (e EscSequence) Intermediate() int { + return parser.Intermediate(int(e)) +} + +// SosSequence represents a SOS sequence. +type SosSequence struct { + // Data contains the raw data of the sequence. + Data []byte +} + +var _ Sequence = &SosSequence{} + +// Clone implements Sequence. +func (s SosSequence) Clone() Sequence { + return SosSequence{Data: append([]byte(nil), s.Data...)} +} + +// Bytes implements Sequence. +func (s SosSequence) Bytes() []byte { + return s.buffer().Bytes() +} + +// String implements Sequence. +func (s SosSequence) String() string { + return s.buffer().String() +} + +func (s SosSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteByte('\x1b') + b.WriteByte('X') + b.Write(s.Data) + b.WriteString("\x1b\\") + return &b +} + +// PmSequence represents a PM sequence. +type PmSequence struct { + // Data contains the raw data of the sequence. + Data []byte +} + +var _ Sequence = &PmSequence{} + +// Clone implements Sequence. +func (s PmSequence) Clone() Sequence { + return PmSequence{Data: append([]byte(nil), s.Data...)} +} + +// Bytes implements Sequence. +func (s PmSequence) Bytes() []byte { + return s.buffer().Bytes() +} + +// String implements Sequence. +func (s PmSequence) String() string { + return s.buffer().String() +} + +// buffer returns the buffer of the PM sequence. +func (s PmSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteByte('\x1b') + b.WriteByte('^') + b.Write(s.Data) + b.WriteString("\x1b\\") + return &b +} + +// ApcSequence represents an APC sequence. +type ApcSequence struct { + // Data contains the raw data of the sequence. + Data []byte +} + +var _ Sequence = &ApcSequence{} + +// Clone implements Sequence. +func (s ApcSequence) Clone() Sequence { + return ApcSequence{Data: append([]byte(nil), s.Data...)} +} + +// Bytes implements Sequence. +func (s ApcSequence) Bytes() []byte { + return s.buffer().Bytes() +} + +// String implements Sequence. +func (s ApcSequence) String() string { + return s.buffer().String() +} + +// buffer returns the buffer of the APC sequence. +func (s ApcSequence) buffer() *bytes.Buffer { + var b bytes.Buffer + b.WriteByte('\x1b') + b.WriteByte('_') + b.Write(s.Data) + b.WriteString("\x1b\\") + return &b +} diff --git a/vendor/github.com/charmbracelet/x/ansi/style.go b/vendor/github.com/charmbracelet/x/ansi/style.go new file mode 100644 index 00000000..5ab3dd47 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/style.go @@ -0,0 +1,496 @@ +package ansi + +import ( + "image/color" + "strconv" + "strings" +) + +// ResetStyle is a SGR (Select Graphic Rendition) style sequence that resets +// all attributes. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +const ResetStyle = "\x1b[m" + +// Attr is a SGR (Select Graphic Rendition) style attribute. +type Attr = int + +// Style represents an ANSI SGR (Select Graphic Rendition) style. +type Style []string + +// String returns the ANSI SGR (Select Graphic Rendition) style sequence for +// the given style. +func (s Style) String() string { + if len(s) == 0 { + return ResetStyle + } + return "\x1b[" + strings.Join(s, ";") + "m" +} + +// Styled returns a styled string with the given style applied. +func (s Style) Styled(str string) string { + if len(s) == 0 { + return str + } + return s.String() + str + ResetStyle +} + +// Reset appends the reset style attribute to the style. +func (s Style) Reset() Style { + return append(s, resetAttr) +} + +// Bold appends the bold style attribute to the style. +func (s Style) Bold() Style { + return append(s, boldAttr) +} + +// Faint appends the faint style attribute to the style. +func (s Style) Faint() Style { + return append(s, faintAttr) +} + +// Italic appends the italic style attribute to the style. +func (s Style) Italic() Style { + return append(s, italicAttr) +} + +// Underline appends the underline style attribute to the style. +func (s Style) Underline() Style { + return append(s, underlineAttr) +} + +// UnderlineStyle appends the underline style attribute to the style. +func (s Style) UnderlineStyle(u UnderlineStyle) Style { + switch u { + case NoUnderlineStyle: + return s.NoUnderline() + case SingleUnderlineStyle: + return s.Underline() + case DoubleUnderlineStyle: + return append(s, doubleUnderlineStyle) + case CurlyUnderlineStyle: + return append(s, curlyUnderlineStyle) + case DottedUnderlineStyle: + return append(s, dottedUnderlineStyle) + case DashedUnderlineStyle: + return append(s, dashedUnderlineStyle) + } + return s +} + +// DoubleUnderline appends the double underline style attribute to the style. +// This is a convenience method for UnderlineStyle(DoubleUnderlineStyle). +func (s Style) DoubleUnderline() Style { + return s.UnderlineStyle(DoubleUnderlineStyle) +} + +// CurlyUnderline appends the curly underline style attribute to the style. +// This is a convenience method for UnderlineStyle(CurlyUnderlineStyle). +func (s Style) CurlyUnderline() Style { + return s.UnderlineStyle(CurlyUnderlineStyle) +} + +// DottedUnderline appends the dotted underline style attribute to the style. +// This is a convenience method for UnderlineStyle(DottedUnderlineStyle). +func (s Style) DottedUnderline() Style { + return s.UnderlineStyle(DottedUnderlineStyle) +} + +// DashedUnderline appends the dashed underline style attribute to the style. +// This is a convenience method for UnderlineStyle(DashedUnderlineStyle). +func (s Style) DashedUnderline() Style { + return s.UnderlineStyle(DashedUnderlineStyle) +} + +// SlowBlink appends the slow blink style attribute to the style. +func (s Style) SlowBlink() Style { + return append(s, slowBlinkAttr) +} + +// RapidBlink appends the rapid blink style attribute to the style. +func (s Style) RapidBlink() Style { + return append(s, rapidBlinkAttr) +} + +// Reverse appends the reverse style attribute to the style. +func (s Style) Reverse() Style { + return append(s, reverseAttr) +} + +// Conceal appends the conceal style attribute to the style. +func (s Style) Conceal() Style { + return append(s, concealAttr) +} + +// Strikethrough appends the strikethrough style attribute to the style. +func (s Style) Strikethrough() Style { + return append(s, strikethroughAttr) +} + +// NoBold appends the no bold style attribute to the style. +func (s Style) NoBold() Style { + return append(s, noBoldAttr) +} + +// NormalIntensity appends the normal intensity style attribute to the style. +func (s Style) NormalIntensity() Style { + return append(s, normalIntensityAttr) +} + +// NoItalic appends the no italic style attribute to the style. +func (s Style) NoItalic() Style { + return append(s, noItalicAttr) +} + +// NoUnderline appends the no underline style attribute to the style. +func (s Style) NoUnderline() Style { + return append(s, noUnderlineAttr) +} + +// NoBlink appends the no blink style attribute to the style. +func (s Style) NoBlink() Style { + return append(s, noBlinkAttr) +} + +// NoReverse appends the no reverse style attribute to the style. +func (s Style) NoReverse() Style { + return append(s, noReverseAttr) +} + +// NoConceal appends the no conceal style attribute to the style. +func (s Style) NoConceal() Style { + return append(s, noConcealAttr) +} + +// NoStrikethrough appends the no strikethrough style attribute to the style. +func (s Style) NoStrikethrough() Style { + return append(s, noStrikethroughAttr) +} + +// DefaultForegroundColor appends the default foreground color style attribute to the style. +func (s Style) DefaultForegroundColor() Style { + return append(s, defaultForegroundColorAttr) +} + +// DefaultBackgroundColor appends the default background color style attribute to the style. +func (s Style) DefaultBackgroundColor() Style { + return append(s, defaultBackgroundColorAttr) +} + +// DefaultUnderlineColor appends the default underline color style attribute to the style. +func (s Style) DefaultUnderlineColor() Style { + return append(s, defaultUnderlineColorAttr) +} + +// ForegroundColor appends the foreground color style attribute to the style. +func (s Style) ForegroundColor(c Color) Style { + return append(s, foregroundColorString(c)) +} + +// BackgroundColor appends the background color style attribute to the style. +func (s Style) BackgroundColor(c Color) Style { + return append(s, backgroundColorString(c)) +} + +// UnderlineColor appends the underline color style attribute to the style. +func (s Style) UnderlineColor(c Color) Style { + return append(s, underlineColorString(c)) +} + +// UnderlineStyle represents an ANSI SGR (Select Graphic Rendition) underline +// style. +type UnderlineStyle = int + +const ( + doubleUnderlineStyle = "4:2" + curlyUnderlineStyle = "4:3" + dottedUnderlineStyle = "4:4" + dashedUnderlineStyle = "4:5" +) + +const ( + // NoUnderlineStyle is the default underline style. + NoUnderlineStyle UnderlineStyle = iota + // SingleUnderlineStyle is a single underline style. + SingleUnderlineStyle + // DoubleUnderlineStyle is a double underline style. + DoubleUnderlineStyle + // CurlyUnderlineStyle is a curly underline style. + CurlyUnderlineStyle + // DottedUnderlineStyle is a dotted underline style. + DottedUnderlineStyle + // DashedUnderlineStyle is a dashed underline style. + DashedUnderlineStyle +) + +// SGR (Select Graphic Rendition) style attributes. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +const ( + ResetAttr Attr = 0 + BoldAttr Attr = 1 + FaintAttr Attr = 2 + ItalicAttr Attr = 3 + UnderlineAttr Attr = 4 + SlowBlinkAttr Attr = 5 + RapidBlinkAttr Attr = 6 + ReverseAttr Attr = 7 + ConcealAttr Attr = 8 + StrikethroughAttr Attr = 9 + NoBoldAttr Attr = 21 // Some terminals treat this as double underline. + NormalIntensityAttr Attr = 22 + NoItalicAttr Attr = 23 + NoUnderlineAttr Attr = 24 + NoBlinkAttr Attr = 25 + NoReverseAttr Attr = 27 + NoConcealAttr Attr = 28 + NoStrikethroughAttr Attr = 29 + BlackForegroundColorAttr Attr = 30 + RedForegroundColorAttr Attr = 31 + GreenForegroundColorAttr Attr = 32 + YellowForegroundColorAttr Attr = 33 + BlueForegroundColorAttr Attr = 34 + MagentaForegroundColorAttr Attr = 35 + CyanForegroundColorAttr Attr = 36 + WhiteForegroundColorAttr Attr = 37 + ExtendedForegroundColorAttr Attr = 38 + DefaultForegroundColorAttr Attr = 39 + BlackBackgroundColorAttr Attr = 40 + RedBackgroundColorAttr Attr = 41 + GreenBackgroundColorAttr Attr = 42 + YellowBackgroundColorAttr Attr = 43 + BlueBackgroundColorAttr Attr = 44 + MagentaBackgroundColorAttr Attr = 45 + CyanBackgroundColorAttr Attr = 46 + WhiteBackgroundColorAttr Attr = 47 + ExtendedBackgroundColorAttr Attr = 48 + DefaultBackgroundColorAttr Attr = 49 + ExtendedUnderlineColorAttr Attr = 58 + DefaultUnderlineColorAttr Attr = 59 + BrightBlackForegroundColorAttr Attr = 90 + BrightRedForegroundColorAttr Attr = 91 + BrightGreenForegroundColorAttr Attr = 92 + BrightYellowForegroundColorAttr Attr = 93 + BrightBlueForegroundColorAttr Attr = 94 + BrightMagentaForegroundColorAttr Attr = 95 + BrightCyanForegroundColorAttr Attr = 96 + BrightWhiteForegroundColorAttr Attr = 97 + BrightBlackBackgroundColorAttr Attr = 100 + BrightRedBackgroundColorAttr Attr = 101 + BrightGreenBackgroundColorAttr Attr = 102 + BrightYellowBackgroundColorAttr Attr = 103 + BrightBlueBackgroundColorAttr Attr = 104 + BrightMagentaBackgroundColorAttr Attr = 105 + BrightCyanBackgroundColorAttr Attr = 106 + BrightWhiteBackgroundColorAttr Attr = 107 + + RGBColorIntroducerAttr Attr = 2 + ExtendedColorIntroducerAttr Attr = 5 +) + +const ( + resetAttr = "0" + boldAttr = "1" + faintAttr = "2" + italicAttr = "3" + underlineAttr = "4" + slowBlinkAttr = "5" + rapidBlinkAttr = "6" + reverseAttr = "7" + concealAttr = "8" + strikethroughAttr = "9" + noBoldAttr = "21" + normalIntensityAttr = "22" + noItalicAttr = "23" + noUnderlineAttr = "24" + noBlinkAttr = "25" + noReverseAttr = "27" + noConcealAttr = "28" + noStrikethroughAttr = "29" + blackForegroundColorAttr = "30" + redForegroundColorAttr = "31" + greenForegroundColorAttr = "32" + yellowForegroundColorAttr = "33" + blueForegroundColorAttr = "34" + magentaForegroundColorAttr = "35" + cyanForegroundColorAttr = "36" + whiteForegroundColorAttr = "37" + extendedForegroundColorAttr = "38" + defaultForegroundColorAttr = "39" + blackBackgroundColorAttr = "40" + redBackgroundColorAttr = "41" + greenBackgroundColorAttr = "42" + yellowBackgroundColorAttr = "43" + blueBackgroundColorAttr = "44" + magentaBackgroundColorAttr = "45" + cyanBackgroundColorAttr = "46" + whiteBackgroundColorAttr = "47" + extendedBackgroundColorAttr = "48" + defaultBackgroundColorAttr = "49" + extendedUnderlineColorAttr = "58" + defaultUnderlineColorAttr = "59" + brightBlackForegroundColorAttr = "90" + brightRedForegroundColorAttr = "91" + brightGreenForegroundColorAttr = "92" + brightYellowForegroundColorAttr = "93" + brightBlueForegroundColorAttr = "94" + brightMagentaForegroundColorAttr = "95" + brightCyanForegroundColorAttr = "96" + brightWhiteForegroundColorAttr = "97" + brightBlackBackgroundColorAttr = "100" + brightRedBackgroundColorAttr = "101" + brightGreenBackgroundColorAttr = "102" + brightYellowBackgroundColorAttr = "103" + brightBlueBackgroundColorAttr = "104" + brightMagentaBackgroundColorAttr = "105" + brightCyanBackgroundColorAttr = "106" + brightWhiteBackgroundColorAttr = "107" +) + +// foregroundColorString returns the style SGR attribute for the given +// foreground color. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +func foregroundColorString(c Color) string { + switch c := c.(type) { + case BasicColor: + // 3-bit or 4-bit ANSI foreground + // "3" or "9" where n is the color number from 0 to 7 + switch c { + case Black: + return blackForegroundColorAttr + case Red: + return redForegroundColorAttr + case Green: + return greenForegroundColorAttr + case Yellow: + return yellowForegroundColorAttr + case Blue: + return blueForegroundColorAttr + case Magenta: + return magentaForegroundColorAttr + case Cyan: + return cyanForegroundColorAttr + case White: + return whiteForegroundColorAttr + case BrightBlack: + return brightBlackForegroundColorAttr + case BrightRed: + return brightRedForegroundColorAttr + case BrightGreen: + return brightGreenForegroundColorAttr + case BrightYellow: + return brightYellowForegroundColorAttr + case BrightBlue: + return brightBlueForegroundColorAttr + case BrightMagenta: + return brightMagentaForegroundColorAttr + case BrightCyan: + return brightCyanForegroundColorAttr + case BrightWhite: + return brightWhiteForegroundColorAttr + } + case ExtendedColor: + // 256-color ANSI foreground + // "38;5;" + return "38;5;" + strconv.FormatUint(uint64(c), 10) + case TrueColor, color.Color: + // 24-bit "true color" foreground + // "38;2;;;" + r, g, b, _ := c.RGBA() + return "38;2;" + + strconv.FormatUint(uint64(shift(r)), 10) + ";" + + strconv.FormatUint(uint64(shift(g)), 10) + ";" + + strconv.FormatUint(uint64(shift(b)), 10) + } + return defaultForegroundColorAttr +} + +// backgroundColorString returns the style SGR attribute for the given +// background color. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +func backgroundColorString(c Color) string { + switch c := c.(type) { + case BasicColor: + // 3-bit or 4-bit ANSI foreground + // "4" or "10" where n is the color number from 0 to 7 + switch c { + case Black: + return blackBackgroundColorAttr + case Red: + return redBackgroundColorAttr + case Green: + return greenBackgroundColorAttr + case Yellow: + return yellowBackgroundColorAttr + case Blue: + return blueBackgroundColorAttr + case Magenta: + return magentaBackgroundColorAttr + case Cyan: + return cyanBackgroundColorAttr + case White: + return whiteBackgroundColorAttr + case BrightBlack: + return brightBlackBackgroundColorAttr + case BrightRed: + return brightRedBackgroundColorAttr + case BrightGreen: + return brightGreenBackgroundColorAttr + case BrightYellow: + return brightYellowBackgroundColorAttr + case BrightBlue: + return brightBlueBackgroundColorAttr + case BrightMagenta: + return brightMagentaBackgroundColorAttr + case BrightCyan: + return brightCyanBackgroundColorAttr + case BrightWhite: + return brightWhiteBackgroundColorAttr + } + case ExtendedColor: + // 256-color ANSI foreground + // "48;5;" + return "48;5;" + strconv.FormatUint(uint64(c), 10) + case TrueColor, color.Color: + // 24-bit "true color" foreground + // "38;2;;;" + r, g, b, _ := c.RGBA() + return "48;2;" + + strconv.FormatUint(uint64(shift(r)), 10) + ";" + + strconv.FormatUint(uint64(shift(g)), 10) + ";" + + strconv.FormatUint(uint64(shift(b)), 10) + } + return defaultBackgroundColorAttr +} + +// underlineColorString returns the style SGR attribute for the given underline +// color. +// See: https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters +func underlineColorString(c Color) string { + switch c := c.(type) { + // NOTE: we can't use 3-bit and 4-bit ANSI color codes with underline + // color, use 256-color instead. + // + // 256-color ANSI underline color + // "58;5;" + case BasicColor: + return "58;5;" + strconv.FormatUint(uint64(c), 10) + case ExtendedColor: + return "58;5;" + strconv.FormatUint(uint64(c), 10) + case TrueColor, color.Color: + // 24-bit "true color" foreground + // "38;2;;;" + r, g, b, _ := c.RGBA() + return "58;2;" + + strconv.FormatUint(uint64(shift(r)), 10) + ";" + + strconv.FormatUint(uint64(shift(g)), 10) + ";" + + strconv.FormatUint(uint64(shift(b)), 10) + } + return defaultUnderlineColorAttr +} + +func shift(v uint32) uint32 { + if v > 0xff { + return v >> 8 + } + return v +} diff --git a/vendor/github.com/charmbracelet/x/ansi/termcap.go b/vendor/github.com/charmbracelet/x/ansi/termcap.go new file mode 100644 index 00000000..1dfc52a6 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/termcap.go @@ -0,0 +1,31 @@ +package ansi + +import ( + "encoding/hex" + "strings" +) + +// RequestTermcap (XTGETTCAP) requests Termcap/Terminfo strings. +// +// DCS + q ST +// +// Where is a list of Termcap/Terminfo capabilities, encoded in 2-digit +// hexadecimals, separated by semicolons. +// +// See: https://man7.org/linux/man-pages/man5/terminfo.5.html +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands +func RequestTermcap(caps ...string) string { + if len(caps) == 0 { + return "" + } + + s := "\x1bP+q" + for i, c := range caps { + if i > 0 { + s += ";" + } + s += strings.ToUpper(hex.EncodeToString([]byte(c))) + } + + return s + "\x1b\\" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/title.go b/vendor/github.com/charmbracelet/x/ansi/title.go new file mode 100644 index 00000000..8fd8bf98 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/title.go @@ -0,0 +1,32 @@ +package ansi + +// SetIconNameWindowTitle returns a sequence for setting the icon name and +// window title. +// +// OSC 0 ; title ST +// OSC 0 ; title BEL +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetIconNameWindowTitle(s string) string { + return "\x1b]0;" + s + "\x07" +} + +// SetIconName returns a sequence for setting the icon name. +// +// OSC 1 ; title ST +// OSC 1 ; title BEL +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetIconName(s string) string { + return "\x1b]1;" + s + "\x07" +} + +// SetWindowTitle returns a sequence for setting the window title. +// +// OSC 2 ; title ST +// OSC 2 ; title BEL +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands +func SetWindowTitle(s string) string { + return "\x1b]2;" + s + "\x07" +} diff --git a/vendor/github.com/charmbracelet/x/ansi/truncate.go b/vendor/github.com/charmbracelet/x/ansi/truncate.go new file mode 100644 index 00000000..db0782c8 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/truncate.go @@ -0,0 +1,107 @@ +package ansi + +import ( + "bytes" + + "github.com/charmbracelet/x/ansi/parser" + "github.com/rivo/uniseg" +) + +// Truncate truncates a string to a given length, adding a tail to the +// end if the string is longer than the given length. +// This function is aware of ANSI escape codes and will not break them, and +// accounts for wide-characters (such as East Asians and emojis). +func Truncate(s string, length int, tail string) string { + if sw := StringWidth(s); sw <= length { + return s + } + + tw := StringWidth(tail) + length -= tw + if length < 0 { + return "" + } + + var cluster []byte + var buf bytes.Buffer + curWidth := 0 + ignoring := false + pstate := parser.GroundState // initial state + b := []byte(s) + i := 0 + + // Here we iterate over the bytes of the string and collect printable + // characters and runes. We also keep track of the width of the string + // in cells. + // Once we reach the given length, we start ignoring characters and only + // collect ANSI escape codes until we reach the end of string. + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + if state == parser.Utf8State { + // This action happens when we transition to the Utf8State. + var width int + cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1) + + // increment the index by the length of the cluster + i += len(cluster) + + // Are we ignoring? Skip to the next byte + if ignoring { + continue + } + + // Is this gonna be too wide? + // If so write the tail and stop collecting. + if curWidth+width > length && !ignoring { + ignoring = true + buf.WriteString(tail) + } + + if curWidth+width > length { + continue + } + + curWidth += width + buf.Write(cluster) + + // Done collecting, now we're back in the ground state. + pstate = parser.GroundState + continue + } + + switch action { + case parser.PrintAction: + // Is this gonna be too wide? + // If so write the tail and stop collecting. + if curWidth >= length && !ignoring { + ignoring = true + buf.WriteString(tail) + } + + // Skip to the next byte if we're ignoring + if ignoring { + i++ + continue + } + + // collects printable ASCII + curWidth++ + fallthrough + default: + buf.WriteByte(b[i]) + i++ + } + + // Transition to the next state. + pstate = state + + // Once we reach the given length, we start ignoring runes and write + // the tail to the buffer. + if curWidth > length && !ignoring { + ignoring = true + buf.WriteString(tail) + } + } + + return buf.String() +} diff --git a/vendor/github.com/charmbracelet/x/ansi/util.go b/vendor/github.com/charmbracelet/x/ansi/util.go new file mode 100644 index 00000000..767093f9 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/util.go @@ -0,0 +1,29 @@ +package ansi + +import ( + "fmt" + "image/color" +) + +// colorToHexString returns a hex string representation of a color. +func colorToHexString(c color.Color) string { + if c == nil { + return "" + } + shift := func(v uint32) uint32 { + if v > 0xff { + return v >> 8 + } + return v + } + r, g, b, _ := c.RGBA() + r, g, b = shift(r), shift(g), shift(b) + return fmt.Sprintf("#%02x%02x%02x", r, g, b) +} + +// rgbToHex converts red, green, and blue values to a hexadecimal value. +// +// hex := rgbToHex(0, 0, 255) // 0x0000FF +func rgbToHex(r, g, b uint32) uint32 { + return r<<16 + g<<8 + b +} diff --git a/vendor/github.com/charmbracelet/x/ansi/width.go b/vendor/github.com/charmbracelet/x/ansi/width.go new file mode 100644 index 00000000..80890e42 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/width.go @@ -0,0 +1,95 @@ +package ansi + +import ( + "bytes" + + "github.com/charmbracelet/x/ansi/parser" + "github.com/rivo/uniseg" +) + +// Strip removes ANSI escape codes from a string. +func Strip(s string) string { + var ( + buf bytes.Buffer // buffer for collecting printable characters + ri int // rune index + rw int // rune width + pstate = parser.GroundState // initial state + ) + + // This implements a subset of the Parser to only collect runes and + // printable characters. + for i := 0; i < len(s); i++ { + if pstate == parser.Utf8State { + // During this state, collect rw bytes to form a valid rune in the + // buffer. After getting all the rune bytes into the buffer, + // transition to GroundState and reset the counters. + buf.WriteByte(s[i]) + ri++ + if ri < rw { + continue + } + pstate = parser.GroundState + ri = 0 + rw = 0 + continue + } + + state, action := parser.Table.Transition(pstate, s[i]) + switch action { + case parser.CollectAction: + if state == parser.Utf8State { + // This action happens when we transition to the Utf8State. + rw = utf8ByteLen(s[i]) + buf.WriteByte(s[i]) + ri++ + } + case parser.PrintAction, parser.ExecuteAction: + // collects printable ASCII and non-printable characters + buf.WriteByte(s[i]) + } + + // Transition to the next state. + // The Utf8State is managed separately above. + if pstate != parser.Utf8State { + pstate = state + } + } + + return buf.String() +} + +// StringWidth returns the width of a string in cells. This is the number of +// cells that the string will occupy when printed in a terminal. ANSI escape +// codes are ignored and wide characters (such as East Asians and emojis) are +// accounted for. +func StringWidth(s string) int { + if s == "" { + return 0 + } + + var ( + pstate = parser.GroundState // initial state + cluster string + width int + ) + + for i := 0; i < len(s); i++ { + state, action := parser.Table.Transition(pstate, s[i]) + if state == parser.Utf8State { + var w int + cluster, _, w, _ = uniseg.FirstGraphemeClusterInString(s[i:], -1) + width += w + i += len(cluster) - 1 + pstate = parser.GroundState + continue + } + + if action == parser.PrintAction { + width++ + } + + pstate = state + } + + return width +} diff --git a/vendor/github.com/charmbracelet/x/ansi/wrap.go b/vendor/github.com/charmbracelet/x/ansi/wrap.go new file mode 100644 index 00000000..2cab5cec --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/wrap.go @@ -0,0 +1,395 @@ +package ansi + +import ( + "bytes" + "unicode" + "unicode/utf8" + + "github.com/charmbracelet/x/ansi/parser" + "github.com/rivo/uniseg" +) + +// nbsp is a non-breaking space +const nbsp = 0xA0 + +// Hardwrap wraps a string or a block of text to a given line length, breaking +// word boundaries. This will preserve ANSI escape codes and will account for +// wide-characters in the string. +// When preserveSpace is true, spaces at the beginning of a line will be +// preserved. +func Hardwrap(s string, limit int, preserveSpace bool) string { + if limit < 1 { + return s + } + + var ( + cluster []byte + buf bytes.Buffer + curWidth int + forceNewline bool + pstate = parser.GroundState // initial state + b = []byte(s) + ) + + addNewline := func() { + buf.WriteByte('\n') + curWidth = 0 + } + + i := 0 + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + if state == parser.Utf8State { + var width int + cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1) + i += len(cluster) + + if curWidth+width > limit { + addNewline() + } + if !preserveSpace && curWidth == 0 && len(cluster) <= 4 { + // Skip spaces at the beginning of a line + if r, _ := utf8.DecodeRune(cluster); r != utf8.RuneError && unicode.IsSpace(r) { + pstate = parser.GroundState + continue + } + } + + buf.Write(cluster) + curWidth += width + pstate = parser.GroundState + continue + } + + switch action { + case parser.PrintAction, parser.ExecuteAction: + if b[i] == '\n' { + addNewline() + forceNewline = false + break + } + + if curWidth+1 > limit { + addNewline() + forceNewline = true + } + + // Skip spaces at the beginning of a line + if curWidth == 0 { + if !preserveSpace && forceNewline && unicode.IsSpace(rune(b[i])) { + break + } + forceNewline = false + } + + buf.WriteByte(b[i]) + curWidth++ + default: + buf.WriteByte(b[i]) + } + + // We manage the UTF8 state separately manually above. + if pstate != parser.Utf8State { + pstate = state + } + i++ + } + + return buf.String() +} + +// Wordwrap wraps a string or a block of text to a given line length, not +// breaking word boundaries. This will preserve ANSI escape codes and will +// account for wide-characters in the string. +// The breakpoints string is a list of characters that are considered +// breakpoints for word wrapping. A hyphen (-) is always considered a +// breakpoint. +// +// Note: breakpoints must be a string of 1-cell wide rune characters. +func Wordwrap(s string, limit int, breakpoints string) string { + if limit < 1 { + return s + } + + var ( + cluster []byte + buf bytes.Buffer + word bytes.Buffer + space bytes.Buffer + curWidth int + wordLen int + pstate = parser.GroundState // initial state + b = []byte(s) + ) + + addSpace := func() { + curWidth += space.Len() + buf.Write(space.Bytes()) + space.Reset() + } + + addWord := func() { + if word.Len() == 0 { + return + } + + addSpace() + curWidth += wordLen + buf.Write(word.Bytes()) + word.Reset() + wordLen = 0 + } + + addNewline := func() { + buf.WriteByte('\n') + curWidth = 0 + space.Reset() + } + + i := 0 + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + if state == parser.Utf8State { + var width int + cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1) + i += len(cluster) + + r, _ := utf8.DecodeRune(cluster) + if r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp { + addWord() + space.WriteRune(r) + } else if bytes.ContainsAny(cluster, breakpoints) { + addSpace() + addWord() + buf.Write(cluster) + curWidth++ + } else { + word.Write(cluster) + wordLen += width + if curWidth+space.Len()+wordLen > limit && + wordLen < limit { + addNewline() + } + } + + pstate = parser.GroundState + continue + } + + switch action { + case parser.PrintAction, parser.ExecuteAction: + r := rune(b[i]) + switch { + case r == '\n': + if wordLen == 0 { + if curWidth+space.Len() > limit { + curWidth = 0 + } else { + buf.Write(space.Bytes()) + } + space.Reset() + } + + addWord() + addNewline() + case unicode.IsSpace(r): + addWord() + space.WriteByte(b[i]) + case r == '-': + fallthrough + case runeContainsAny(r, breakpoints): + addSpace() + addWord() + buf.WriteByte(b[i]) + curWidth++ + default: + word.WriteByte(b[i]) + wordLen++ + if curWidth+space.Len()+wordLen > limit && + wordLen < limit { + addNewline() + } + } + + default: + word.WriteByte(b[i]) + } + + // We manage the UTF8 state separately manually above. + if pstate != parser.Utf8State { + pstate = state + } + i++ + } + + addWord() + + return buf.String() +} + +// Wrap wraps a string or a block of text to a given line length, breaking word +// boundaries if necessary. This will preserve ANSI escape codes and will +// account for wide-characters in the string. The breakpoints string is a list +// of characters that are considered breakpoints for word wrapping. A hyphen +// (-) is always considered a breakpoint. +// +// Note: breakpoints must be a string of 1-cell wide rune characters. +func Wrap(s string, limit int, breakpoints string) string { + if limit < 1 { + return s + } + + var ( + cluster []byte + buf bytes.Buffer + word bytes.Buffer + space bytes.Buffer + curWidth int // written width of the line + wordLen int // word buffer len without ANSI escape codes + pstate = parser.GroundState // initial state + b = []byte(s) + ) + + addSpace := func() { + curWidth += space.Len() + buf.Write(space.Bytes()) + space.Reset() + } + + addWord := func() { + if word.Len() == 0 { + return + } + + addSpace() + curWidth += wordLen + buf.Write(word.Bytes()) + word.Reset() + wordLen = 0 + } + + addNewline := func() { + buf.WriteByte('\n') + curWidth = 0 + space.Reset() + } + + i := 0 + for i < len(b) { + state, action := parser.Table.Transition(pstate, b[i]) + if state == parser.Utf8State { + var width int + cluster, _, width, _ = uniseg.FirstGraphemeCluster(b[i:], -1) + i += len(cluster) + + r, _ := utf8.DecodeRune(cluster) + switch { + case r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp: // nbsp is a non-breaking space + addWord() + space.WriteRune(r) + case bytes.ContainsAny(cluster, breakpoints): + addSpace() + if curWidth+wordLen+width > limit { + word.Write(cluster) + wordLen += width + } else { + addWord() + buf.Write(cluster) + curWidth += width + } + default: + if wordLen+width > limit { + // Hardwrap the word if it's too long + addWord() + } + + word.Write(cluster) + wordLen += width + + if curWidth+wordLen+space.Len() > limit { + addNewline() + } + } + + pstate = parser.GroundState + continue + } + + switch action { + case parser.PrintAction, parser.ExecuteAction: + switch r := rune(b[i]); { + case r == '\n': + if wordLen == 0 { + if curWidth+space.Len() > limit { + curWidth = 0 + } else { + // preserve whitespaces + buf.Write(space.Bytes()) + } + space.Reset() + } + + addWord() + addNewline() + case unicode.IsSpace(r): + addWord() + space.WriteRune(r) + case r == '-': + fallthrough + case runeContainsAny(r, breakpoints): + addSpace() + if curWidth+wordLen >= limit { + // We can't fit the breakpoint in the current line, treat + // it as part of the word. + word.WriteRune(r) + wordLen++ + } else { + addWord() + buf.WriteRune(r) + curWidth++ + } + default: + word.WriteRune(r) + wordLen++ + + if wordLen == limit { + // Hardwrap the word if it's too long + addWord() + } + + if curWidth+wordLen+space.Len() > limit { + addNewline() + } + } + + default: + word.WriteByte(b[i]) + } + + // We manage the UTF8 state separately manually above. + if pstate != parser.Utf8State { + pstate = state + } + i++ + } + + if word.Len() != 0 { + // Preserve ANSI wrapped spaces at the end of string + if curWidth+space.Len() > limit { + buf.WriteByte('\n') + } + addSpace() + } + buf.Write(word.Bytes()) + + return buf.String() +} + +func runeContainsAny(r rune, s string) bool { + for _, c := range s { + if c == r { + return true + } + } + return false +} diff --git a/vendor/github.com/charmbracelet/x/ansi/xterm.go b/vendor/github.com/charmbracelet/x/ansi/xterm.go new file mode 100644 index 00000000..f87712a6 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/ansi/xterm.go @@ -0,0 +1,50 @@ +package ansi + +import "strconv" + +// ModifyOtherKeys returns a sequence that sets XTerm modifyOtherKeys mode. +// The mode argument specifies the mode to set. +// +// 0: Disable modifyOtherKeys mode. +// 1: Enable modifyOtherKeys mode 1. +// 2: Enable modifyOtherKeys mode 2. +// +// CSI > 4 ; mode m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +func ModifyOtherKeys(mode int) string { + return "\x1b[>4;" + strconv.Itoa(mode) + "m" +} + +// DisableModifyOtherKeys disables the modifyOtherKeys mode. +// +// CSI > 4 ; 0 m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +const DisableModifyOtherKeys = "\x1b[>4;0m" + +// EnableModifyOtherKeys1 enables the modifyOtherKeys mode 1. +// +// CSI > 4 ; 1 m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +const EnableModifyOtherKeys1 = "\x1b[>4;1m" + +// EnableModifyOtherKeys2 enables the modifyOtherKeys mode 2. +// +// CSI > 4 ; 2 m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +const EnableModifyOtherKeys2 = "\x1b[>4;2m" + +// RequestModifyOtherKeys requests the modifyOtherKeys mode. +// +// CSI ? 4 m +// +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Functions-using-CSI-_-ordered-by-the-final-character_s_ +// See: https://invisible-island.net/xterm/manpage/xterm.html#VT100-Widget-Resources:modifyOtherKeys +const RequestModifyOtherKeys = "\x1b[?4m" diff --git a/vendor/github.com/charmbracelet/x/exp/strings/LICENSE b/vendor/github.com/charmbracelet/x/exp/strings/LICENSE new file mode 100644 index 00000000..65a5654e --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/strings/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Charmbracelet, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/charmbracelet/x/exp/strings/join.go b/vendor/github.com/charmbracelet/x/exp/strings/join.go new file mode 100644 index 00000000..d9d51b52 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/exp/strings/join.go @@ -0,0 +1,133 @@ +package strings + +// The so-called spoken language join here works well for some Western +// languages. PRs for other languages are welcome, but do note that +// implementation for some languages will be less straightforward than the ones +// in use here. + +import ( + "strings" +) + +// Language is a spoken Language. +type Language int + +// Available spoken lanaguges. +const ( + DE Language = iota + DK + EN + ES + FR + IT + NO + PT + SE +) + +// String returns the English name of the [Language] code. +func (l Language) String() string { + return map[Language]string{ + DE: "German", + DK: "Danish", + EN: "English", + ES: "Spanish", + FR: "French", + IT: "Italian", + NO: "Norwegian", + PT: "Portuguese", + SE: "Swedish", + }[l] +} + +func (l Language) conjuction() string { + switch l { + case DE: + return "und" + case DK: + return "og" + case EN: + return "and" + case ES: + return "y" + case FR: + return "et" + case NO: + return "og" + case IT: + return "e" + case PT: + return "e" + case SE: + return "och" + default: + return "" + } +} + +func (l Language) separator() string { + switch l { + case DE, DK, EN, ES, FR, NO, IT, PT, SE: + return ", " + default: + return " " + } +} + +// EnglishJoin joins a slice of strings with commas and the "and" conjugation +// before the final item. The Oxford comma can optionally be applied. +// +// Example: +// +// str := EnglishJoin([]string{"meow", "purr", "raow"}, true) +// fmt.Println(str) // meow, purr, and raow +func EnglishJoin(words []string, oxfordComma bool) string { + return spokenLangJoin(words, EN, oxfordComma) +} + +// SpokenLangaugeJoin joins a slice of strings with commas and a conjuction +// before the final item. You may specify the language with [Language]. +// +// If you are using English and need the Oxford Comma, use [EnglishJoin]. +// +// Example: +// +// str := SpokenLanguageJoin([]string{"eins", "zwei", "drei"}, DE) +// fmt.Println(str) // eins, zwei und drei +func SpokenLanguageJoin(words []string, language Language) string { + return spokenLangJoin(words, language, false) +} + +func spokenLangJoin(words []string, language Language, oxfordComma bool) string { + conjuction := language.conjuction() + " " + separator := language.separator() + + b := strings.Builder{} + for i, word := range words { + if word == "" { + continue + } + + if i == 0 { + b.WriteString(word) + continue + } + + // Is this the final word? + if len(words) > 1 && i == len(words)-1 { + // Apply the Oxford comma if requested as long as the language is + // English. + if language == EN && oxfordComma && i > 1 { + b.WriteString(separator) + } else { + b.WriteRune(' ') + } + + b.WriteString(conjuction + word) + continue + } + + b.WriteString(separator + word) + } + return b.String() +} diff --git a/vendor/github.com/charmbracelet/x/term/LICENSE b/vendor/github.com/charmbracelet/x/term/LICENSE new file mode 100644 index 00000000..65a5654e --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Charmbracelet, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/charmbracelet/x/term/term.go b/vendor/github.com/charmbracelet/x/term/term.go new file mode 100644 index 00000000..58d6522c --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term.go @@ -0,0 +1,49 @@ +package term + +// State contains platform-specific state of a terminal. +type State struct { + state +} + +// IsTerminal returns whether the given file descriptor is a terminal. +func IsTerminal(fd uintptr) bool { + return isTerminal(fd) +} + +// MakeRaw puts the terminal connected to the given file descriptor into raw +// mode and returns the previous state of the terminal so that it can be +// restored. +func MakeRaw(fd uintptr) (*State, error) { + return makeRaw(fd) +} + +// GetState returns the current state of a terminal which may be useful to +// restore the terminal after a signal. +func GetState(fd uintptr) (*State, error) { + return getState(fd) +} + +// SetState sets the given state of the terminal. +func SetState(fd uintptr, state *State) error { + return setState(fd, state) +} + +// Restore restores the terminal connected to the given file descriptor to a +// previous state. +func Restore(fd uintptr, oldState *State) error { + return restore(fd, oldState) +} + +// GetSize returns the visible dimensions of the given terminal. +// +// These dimensions don't include any scrollback buffer height. +func GetSize(fd uintptr) (width, height int, err error) { + return getSize(fd) +} + +// ReadPassword reads a line of input from a terminal without local echo. This +// is commonly used for inputting passwords and other sensitive data. The slice +// returned does not include the \n. +func ReadPassword(fd uintptr) ([]byte, error) { + return readPassword(fd) +} diff --git a/vendor/github.com/charmbracelet/x/term/term_other.go b/vendor/github.com/charmbracelet/x/term/term_other.go new file mode 100644 index 00000000..092c7e9d --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_other.go @@ -0,0 +1,39 @@ +//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !zos && !windows && !solaris && !plan9 +// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!zos,!windows,!solaris,!plan9 + +package term + +import ( + "fmt" + "runtime" +) + +type state struct{} + +func isTerminal(fd uintptr) bool { + return false +} + +func makeRaw(fd uintptr) (*State, error) { + return nil, fmt.Errorf("terminal: MakeRaw not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +func getState(fd uintptr) (*State, error) { + return nil, fmt.Errorf("terminal: GetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +func restore(fd uintptr, state *State) error { + return fmt.Errorf("terminal: Restore not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +func getSize(fd uintptr) (width, height int, err error) { + return 0, 0, fmt.Errorf("terminal: GetSize not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +func setState(fd uintptr, state *State) error { + return fmt.Errorf("terminal: SetState not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} + +func readPassword(fd uintptr) ([]byte, error) { + return nil, fmt.Errorf("terminal: ReadPassword not implemented on %s/%s", runtime.GOOS, runtime.GOARCH) +} diff --git a/vendor/github.com/charmbracelet/x/term/term_unix.go b/vendor/github.com/charmbracelet/x/term/term_unix.go new file mode 100644 index 00000000..1459cb1b --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_unix.go @@ -0,0 +1,96 @@ +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos + +package term + +import ( + "golang.org/x/sys/unix" +) + +type state struct { + unix.Termios +} + +func isTerminal(fd uintptr) bool { + _, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) + return err == nil +} + +func makeRaw(fd uintptr) (*State, error) { + termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) + if err != nil { + return nil, err + } + + oldState := State{state{Termios: *termios}} + + // This attempts to replicate the behaviour documented for cfmakeraw in + // the termios(3) manpage. + termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON + termios.Oflag &^= unix.OPOST + termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN + termios.Cflag &^= unix.CSIZE | unix.PARENB + termios.Cflag |= unix.CS8 + termios.Cc[unix.VMIN] = 1 + termios.Cc[unix.VTIME] = 0 + if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios); err != nil { + return nil, err + } + + return &oldState, nil +} + +func setState(fd uintptr, state *State) error { + var termios *unix.Termios + if state != nil { + termios = &state.Termios + } + return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios) +} + +func getState(fd uintptr) (*State, error) { + termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) + if err != nil { + return nil, err + } + + return &State{state{Termios: *termios}}, nil +} + +func restore(fd uintptr, state *State) error { + return unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &state.Termios) +} + +func getSize(fd uintptr) (width, height int, err error) { + ws, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ) + if err != nil { + return 0, 0, err + } + return int(ws.Col), int(ws.Row), nil +} + +// passwordReader is an io.Reader that reads from a specific file descriptor. +type passwordReader int + +func (r passwordReader) Read(buf []byte) (int, error) { + return unix.Read(int(r), buf) +} + +func readPassword(fd uintptr) ([]byte, error) { + termios, err := unix.IoctlGetTermios(int(fd), ioctlReadTermios) + if err != nil { + return nil, err + } + + newState := *termios + newState.Lflag &^= unix.ECHO + newState.Lflag |= unix.ICANON | unix.ISIG + newState.Iflag |= unix.ICRNL + if err := unix.IoctlSetTermios(int(fd), ioctlWriteTermios, &newState); err != nil { + return nil, err + } + + defer unix.IoctlSetTermios(int(fd), ioctlWriteTermios, termios) + + return readPasswordLine(passwordReader(fd)) +} diff --git a/vendor/github.com/charmbracelet/x/term/term_unix_bsd.go b/vendor/github.com/charmbracelet/x/term/term_unix_bsd.go new file mode 100644 index 00000000..b435031a --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_unix_bsd.go @@ -0,0 +1,11 @@ +//go:build darwin || dragonfly || freebsd || netbsd || openbsd +// +build darwin dragonfly freebsd netbsd openbsd + +package term + +import "golang.org/x/sys/unix" + +const ( + ioctlReadTermios = unix.TIOCGETA + ioctlWriteTermios = unix.TIOCSETA +) diff --git a/vendor/github.com/charmbracelet/x/term/term_unix_other.go b/vendor/github.com/charmbracelet/x/term/term_unix_other.go new file mode 100644 index 00000000..ee2a29eb --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_unix_other.go @@ -0,0 +1,11 @@ +//go:build aix || linux || solaris || zos +// +build aix linux solaris zos + +package term + +import "golang.org/x/sys/unix" + +const ( + ioctlReadTermios = unix.TCGETS + ioctlWriteTermios = unix.TCSETS +) diff --git a/vendor/github.com/charmbracelet/x/term/term_windows.go b/vendor/github.com/charmbracelet/x/term/term_windows.go new file mode 100644 index 00000000..9e1c4740 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/term_windows.go @@ -0,0 +1,86 @@ +//go:build windows +// +build windows + +package term + +import ( + "os" + + "golang.org/x/sys/windows" +) + +type state struct { + Mode uint32 +} + +func isTerminal(fd uintptr) bool { + var st uint32 + err := windows.GetConsoleMode(windows.Handle(fd), &st) + return err == nil +} + +func makeRaw(fd uintptr) (*State, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + raw := st &^ (windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_LINE_INPUT | windows.ENABLE_PROCESSED_OUTPUT) + if err := windows.SetConsoleMode(windows.Handle(fd), raw); err != nil { + return nil, err + } + return &State{state{st}}, nil +} + +func setState(fd uintptr, state *State) error { + var mode uint32 + if state != nil { + mode = state.Mode + } + return windows.SetConsoleMode(windows.Handle(fd), mode) +} + +func getState(fd uintptr) (*State, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + return &State{state{st}}, nil +} + +func restore(fd uintptr, state *State) error { + return windows.SetConsoleMode(windows.Handle(fd), state.Mode) +} + +func getSize(fd uintptr) (width, height int, err error) { + var info windows.ConsoleScreenBufferInfo + if err := windows.GetConsoleScreenBufferInfo(windows.Handle(fd), &info); err != nil { + return 0, 0, err + } + return int(info.Window.Right - info.Window.Left + 1), int(info.Window.Bottom - info.Window.Top + 1), nil +} + +func readPassword(fd uintptr) ([]byte, error) { + var st uint32 + if err := windows.GetConsoleMode(windows.Handle(fd), &st); err != nil { + return nil, err + } + old := st + + st &^= (windows.ENABLE_ECHO_INPUT | windows.ENABLE_LINE_INPUT) + st |= (windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_PROCESSED_INPUT) + if err := windows.SetConsoleMode(windows.Handle(fd), st); err != nil { + return nil, err + } + + defer windows.SetConsoleMode(windows.Handle(fd), old) + + var h windows.Handle + p, _ := windows.GetCurrentProcess() + if err := windows.DuplicateHandle(p, windows.Handle(fd), p, &h, 0, false, windows.DUPLICATE_SAME_ACCESS); err != nil { + return nil, err + } + + f := os.NewFile(uintptr(h), "stdin") + defer f.Close() + return readPasswordLine(f) +} diff --git a/vendor/github.com/charmbracelet/x/term/terminal.go b/vendor/github.com/charmbracelet/x/term/terminal.go new file mode 100644 index 00000000..8963163f --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/terminal.go @@ -0,0 +1,12 @@ +package term + +import ( + "io" +) + +// File represents a file that has a file descriptor and can be read from, +// written to, and closed. +type File interface { + io.ReadWriteCloser + Fd() uintptr +} diff --git a/vendor/github.com/charmbracelet/x/term/util.go b/vendor/github.com/charmbracelet/x/term/util.go new file mode 100644 index 00000000..b7313418 --- /dev/null +++ b/vendor/github.com/charmbracelet/x/term/util.go @@ -0,0 +1,47 @@ +package term + +import ( + "io" + "runtime" +) + +// readPasswordLine reads from reader until it finds \n or io.EOF. +// The slice returned does not include the \n. +// readPasswordLine also ignores any \r it finds. +// Windows uses \r as end of line. So, on Windows, readPasswordLine +// reads until it finds \r and ignores any \n it finds during processing. +func readPasswordLine(reader io.Reader) ([]byte, error) { + var buf [1]byte + var ret []byte + + for { + n, err := reader.Read(buf[:]) + if n > 0 { + switch buf[0] { + case '\b': + if len(ret) > 0 { + ret = ret[:len(ret)-1] + } + case '\n': + if runtime.GOOS != "windows" { + return ret, nil + } + // otherwise ignore \n + case '\r': + if runtime.GOOS == "windows" { + return ret, nil + } + // otherwise ignore \r + default: + ret = append(ret, buf[0]) + } + continue + } + if err != nil { + if err == io.EOF && len(ret) > 0 { + return ret, nil + } + return ret, err + } + } +} diff --git a/vendor/github.com/danieljoos/wincred/.gitattributes b/vendor/github.com/danieljoos/wincred/.gitattributes new file mode 100644 index 00000000..d207b180 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf diff --git a/vendor/github.com/danieljoos/wincred/.gitignore b/vendor/github.com/danieljoos/wincred/.gitignore new file mode 100644 index 00000000..6142c069 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/.gitignore @@ -0,0 +1,25 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test + +coverage.txt diff --git a/vendor/github.com/danieljoos/wincred/LICENSE b/vendor/github.com/danieljoos/wincred/LICENSE new file mode 100644 index 00000000..2f436f1b --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Daniel Joos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/danieljoos/wincred/README.md b/vendor/github.com/danieljoos/wincred/README.md new file mode 100644 index 00000000..8a879b0c --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/README.md @@ -0,0 +1,145 @@ +wincred +======= + +Go wrapper around the Windows Credential Manager API functions. + +[![GitHub release](https://img.shields.io/github/release/danieljoos/wincred.svg?style=flat-square)](https://github.com/danieljoos/wincred/releases/latest) +[![Test Status](https://img.shields.io/github/actions/workflow/status/danieljoos/wincred/test.yml?label=test&logo=github&style=flat-square)](https://github.com/danieljoos/wincred/actions?query=workflow%3Atest) +[![Go Report Card](https://goreportcard.com/badge/github.com/danieljoos/wincred)](https://goreportcard.com/report/github.com/danieljoos/wincred) +[![Codecov](https://img.shields.io/codecov/c/github/danieljoos/wincred?logo=codecov&style=flat-square)](https://codecov.io/gh/danieljoos/wincred) +[![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/danieljoos/wincred) + +Installation +------------ + +```Go +go get github.com/danieljoos/wincred +``` + + +Usage +----- + +See the following examples: + +### Create and store a new generic credential object +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + cred := wincred.NewGenericCredential("myGoApplication") + cred.CredentialBlob = []byte("my secret") + err := cred.Write() + + if err != nil { + fmt.Println(err) + } +} +``` + +### Retrieve a credential object +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + cred, err := wincred.GetGenericCredential("myGoApplication") + if err == nil { + fmt.Println(string(cred.CredentialBlob)) + } +} +``` + +### Remove a credential object +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + cred, err := wincred.GetGenericCredential("myGoApplication") + if err != nil { + fmt.Println(err) + return + } + cred.Delete() +} +``` + +### List all available credentials +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + creds, err := wincred.List() + if err != nil { + fmt.Println(err) + return + } + for i := range(creds) { + fmt.Println(creds[i].TargetName) + } +} +``` + +Hints +----- + +### Encoding + +The credential objects simply store byte arrays without specific meaning or encoding. +For sharing between different applications, it might make sense to apply an explicit string encoding - for example **UTF-16 LE** (used nearly everywhere in the Win32 API). + +```Go +package main + +import ( + "fmt" + "os" + + "github.com/danieljoos/wincred" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +func main() { + cred := wincred.NewGenericCredential("myGoApplication") + + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + blob, _, err := transform.Bytes(encoder, []byte("mysecret")) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + cred.CredentialBlob = blob + err = cred.Write() + + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +``` + +### Limitations + +The size of a credential blob is limited to **2560 Bytes** by the Windows API. diff --git a/vendor/github.com/danieljoos/wincred/conversion.go b/vendor/github.com/danieljoos/wincred/conversion.go new file mode 100644 index 00000000..bc04f50f --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/conversion.go @@ -0,0 +1,116 @@ +// +build windows + +package wincred + +import ( + "encoding/binary" + "reflect" + "time" + "unsafe" + + syscall "golang.org/x/sys/windows" +) + +// utf16ToByte creates a byte array from a given UTF 16 char array. +func utf16ToByte(wstr []uint16) (result []byte) { + result = make([]byte, len(wstr)*2) + for i := range wstr { + binary.LittleEndian.PutUint16(result[(i*2):(i*2)+2], wstr[i]) + } + return +} + +// utf16FromString creates a UTF16 char array from a string. +func utf16FromString(str string) []uint16 { + res, err := syscall.UTF16FromString(str) + if err != nil { + return []uint16{} + } + return res +} + +// goBytes copies the given C byte array to a Go byte array (see `C.GoBytes`). +// This function avoids having cgo as dependency. +func goBytes(src uintptr, len uint32) []byte { + if src == uintptr(0) { + return []byte{} + } + rv := make([]byte, len) + copy(rv, *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: src, + Len: int(len), + Cap: int(len), + }))) + return rv +} + +// Convert the given CREDENTIAL struct to a more usable structure +func sysToCredential(cred *sysCREDENTIAL) (result *Credential) { + if cred == nil { + return nil + } + result = new(Credential) + result.Comment = syscall.UTF16PtrToString(cred.Comment) + result.TargetName = syscall.UTF16PtrToString(cred.TargetName) + result.TargetAlias = syscall.UTF16PtrToString(cred.TargetAlias) + result.UserName = syscall.UTF16PtrToString(cred.UserName) + result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds()) + result.Persist = CredentialPersistence(cred.Persist) + result.CredentialBlob = goBytes(cred.CredentialBlob, cred.CredentialBlobSize) + result.Attributes = make([]CredentialAttribute, cred.AttributeCount) + attrSlice := *(*[]sysCREDENTIAL_ATTRIBUTE)(unsafe.Pointer(&reflect.SliceHeader{ + Data: cred.Attributes, + Len: int(cred.AttributeCount), + Cap: int(cred.AttributeCount), + })) + for i, attr := range attrSlice { + resultAttr := &result.Attributes[i] + resultAttr.Keyword = syscall.UTF16PtrToString(attr.Keyword) + resultAttr.Value = goBytes(attr.Value, attr.ValueSize) + } + return result +} + +// Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the +// Windows APIs +func sysFromCredential(cred *Credential) (result *sysCREDENTIAL) { + if cred == nil { + return nil + } + result = new(sysCREDENTIAL) + result.Flags = 0 + result.Type = 0 + result.TargetName, _ = syscall.UTF16PtrFromString(cred.TargetName) + result.Comment, _ = syscall.UTF16PtrFromString(cred.Comment) + result.LastWritten = syscall.NsecToFiletime(cred.LastWritten.UnixNano()) + result.CredentialBlobSize = uint32(len(cred.CredentialBlob)) + if len(cred.CredentialBlob) > 0 { + result.CredentialBlob = uintptr(unsafe.Pointer(&cred.CredentialBlob[0])) + } else { + result.CredentialBlob = 0 + } + result.Persist = uint32(cred.Persist) + result.AttributeCount = uint32(len(cred.Attributes)) + attributes := make([]sysCREDENTIAL_ATTRIBUTE, len(cred.Attributes)) + if len(attributes) > 0 { + result.Attributes = uintptr(unsafe.Pointer(&attributes[0])) + } else { + result.Attributes = 0 + } + for i := range cred.Attributes { + inAttr := &cred.Attributes[i] + outAttr := &attributes[i] + outAttr.Keyword, _ = syscall.UTF16PtrFromString(inAttr.Keyword) + outAttr.Flags = 0 + outAttr.ValueSize = uint32(len(inAttr.Value)) + if len(inAttr.Value) > 0 { + outAttr.Value = uintptr(unsafe.Pointer(&inAttr.Value[0])) + } else { + outAttr.Value = 0 + } + } + result.TargetAlias, _ = syscall.UTF16PtrFromString(cred.TargetAlias) + result.UserName, _ = syscall.UTF16PtrFromString(cred.UserName) + + return +} diff --git a/vendor/github.com/danieljoos/wincred/conversion_unsupported.go b/vendor/github.com/danieljoos/wincred/conversion_unsupported.go new file mode 100644 index 00000000..a1ea7207 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/conversion_unsupported.go @@ -0,0 +1,11 @@ +// +build !windows + +package wincred + +func utf16ToByte(...interface{}) []byte { + return nil +} + +func utf16FromString(...interface{}) []uint16 { + return nil +} diff --git a/vendor/github.com/danieljoos/wincred/sys.go b/vendor/github.com/danieljoos/wincred/sys.go new file mode 100644 index 00000000..dfff5962 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/sys.go @@ -0,0 +1,147 @@ +//go:build windows +// +build windows + +package wincred + +import ( + "reflect" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + procCredRead = modadvapi32.NewProc("CredReadW") + procCredWrite proc = modadvapi32.NewProc("CredWriteW") + procCredDelete proc = modadvapi32.NewProc("CredDeleteW") + procCredFree proc = modadvapi32.NewProc("CredFree") + procCredEnumerate = modadvapi32.NewProc("CredEnumerateW") +) + +// Interface for syscall.Proc: helps testing +type proc interface { + Call(a ...uintptr) (r1, r2 uintptr, lastErr error) +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw +type sysCREDENTIAL struct { + Flags uint32 + Type uint32 + TargetName *uint16 + Comment *uint16 + LastWritten windows.Filetime + CredentialBlobSize uint32 + CredentialBlob uintptr + Persist uint32 + AttributeCount uint32 + Attributes uintptr + TargetAlias *uint16 + UserName *uint16 +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credential_attributew +type sysCREDENTIAL_ATTRIBUTE struct { + Keyword *uint16 + Flags uint32 + ValueSize uint32 + Value uintptr +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw +type sysCRED_TYPE uint32 + +const ( + sysCRED_TYPE_GENERIC sysCRED_TYPE = 0x1 + sysCRED_TYPE_DOMAIN_PASSWORD sysCRED_TYPE = 0x2 + sysCRED_TYPE_DOMAIN_CERTIFICATE sysCRED_TYPE = 0x3 + sysCRED_TYPE_DOMAIN_VISIBLE_PASSWORD sysCRED_TYPE = 0x4 + sysCRED_TYPE_GENERIC_CERTIFICATE sysCRED_TYPE = 0x5 + sysCRED_TYPE_DOMAIN_EXTENDED sysCRED_TYPE = 0x6 + + // https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes + sysERROR_NOT_FOUND = windows.Errno(1168) + sysERROR_INVALID_PARAMETER = windows.Errno(87) +) + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credreadw +func sysCredRead(targetName string, typ sysCRED_TYPE) (*Credential, error) { + var pcred *sysCREDENTIAL + targetNamePtr, _ := windows.UTF16PtrFromString(targetName) + ret, _, err := syscall.SyscallN( + procCredRead.Addr(), + uintptr(unsafe.Pointer(targetNamePtr)), + uintptr(typ), + 0, + uintptr(unsafe.Pointer(&pcred)), + ) + if ret == 0 { + return nil, err + } + defer procCredFree.Call(uintptr(unsafe.Pointer(pcred))) + + return sysToCredential(pcred), nil +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credwritew +func sysCredWrite(cred *Credential, typ sysCRED_TYPE) error { + ncred := sysFromCredential(cred) + ncred.Type = uint32(typ) + ret, _, err := procCredWrite.Call( + uintptr(unsafe.Pointer(ncred)), + 0, + ) + if ret == 0 { + return err + } + + return nil +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-creddeletew +func sysCredDelete(cred *Credential, typ sysCRED_TYPE) error { + targetNamePtr, _ := windows.UTF16PtrFromString(cred.TargetName) + ret, _, err := procCredDelete.Call( + uintptr(unsafe.Pointer(targetNamePtr)), + uintptr(typ), + 0, + ) + if ret == 0 { + return err + } + + return nil +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credenumeratew +func sysCredEnumerate(filter string, all bool) ([]*Credential, error) { + var count int + var pcreds uintptr + var filterPtr *uint16 + if !all { + filterPtr, _ = windows.UTF16PtrFromString(filter) + } + ret, _, err := syscall.SyscallN( + procCredEnumerate.Addr(), + uintptr(unsafe.Pointer(filterPtr)), + 0, + uintptr(unsafe.Pointer(&count)), + uintptr(unsafe.Pointer(&pcreds)), + ) + if ret == 0 { + return nil, err + } + defer procCredFree.Call(pcreds) + credsSlice := *(*[]*sysCREDENTIAL)(unsafe.Pointer(&reflect.SliceHeader{ + Data: pcreds, + Len: count, + Cap: count, + })) + creds := make([]*Credential, count, count) + for i, cred := range credsSlice { + creds[i] = sysToCredential(cred) + } + + return creds, nil +} diff --git a/vendor/github.com/danieljoos/wincred/sys_unsupported.go b/vendor/github.com/danieljoos/wincred/sys_unsupported.go new file mode 100644 index 00000000..b47bccf8 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/sys_unsupported.go @@ -0,0 +1,36 @@ +// +build !windows + +package wincred + +import ( + "errors" + "syscall" +) + +const ( + sysCRED_TYPE_GENERIC = 0 + sysCRED_TYPE_DOMAIN_PASSWORD = 0 + sysCRED_TYPE_DOMAIN_CERTIFICATE = 0 + sysCRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 0 + sysCRED_TYPE_GENERIC_CERTIFICATE = 0 + sysCRED_TYPE_DOMAIN_EXTENDED = 0 + + sysERROR_NOT_FOUND = syscall.Errno(1) + sysERROR_INVALID_PARAMETER = syscall.Errno(1) +) + +func sysCredRead(...interface{}) (*Credential, error) { + return nil, errors.New("Operation not supported") +} + +func sysCredWrite(...interface{}) error { + return errors.New("Operation not supported") +} + +func sysCredDelete(...interface{}) error { + return errors.New("Operation not supported") +} + +func sysCredEnumerate(...interface{}) ([]*Credential, error) { + return nil, errors.New("Operation not supported") +} diff --git a/vendor/github.com/danieljoos/wincred/types.go b/vendor/github.com/danieljoos/wincred/types.go new file mode 100644 index 00000000..28debc93 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/types.go @@ -0,0 +1,69 @@ +package wincred + +import ( + "time" +) + +// CredentialPersistence describes one of three persistence modes of a credential. +// A detailed description of the available modes can be found on +// Docs: https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw +type CredentialPersistence uint32 + +const ( + // PersistSession indicates that the credential only persists for the life + // of the current Windows login session. Such a credential is not visible in + // any other logon session, even from the same user. + PersistSession CredentialPersistence = 0x1 + + // PersistLocalMachine indicates that the credential persists for this and + // all subsequent logon sessions on this local machine/computer. It is + // however not visible for logon sessions of this user on a different + // machine. + PersistLocalMachine CredentialPersistence = 0x2 + + // PersistEnterprise indicates that the credential persists for this and all + // subsequent logon sessions for this user. It is also visible for logon + // sessions on different computers. + PersistEnterprise CredentialPersistence = 0x3 +) + +// CredentialAttribute represents an application-specific attribute of a credential. +type CredentialAttribute struct { + Keyword string + Value []byte +} + +// Credential is the basic credential structure. +// A credential is identified by its target name. +// The actual credential secret is available in the CredentialBlob field. +type Credential struct { + TargetName string + Comment string + LastWritten time.Time + CredentialBlob []byte + Attributes []CredentialAttribute + TargetAlias string + UserName string + Persist CredentialPersistence +} + +// GenericCredential holds a credential for generic usage. +// It is typically defined and used by applications that need to manage user +// secrets. +// +// More information about the available kinds of credentials of the Windows +// Credential Management API can be found on Docs: +// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials +type GenericCredential struct { + Credential +} + +// DomainPassword holds a domain credential that is typically used by the +// operating system for user logon. +// +// More information about the available kinds of credentials of the Windows +// Credential Management API can be found on Docs: +// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials +type DomainPassword struct { + Credential +} diff --git a/vendor/github.com/danieljoos/wincred/wincred.go b/vendor/github.com/danieljoos/wincred/wincred.go new file mode 100644 index 00000000..998de7b5 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/wincred.go @@ -0,0 +1,111 @@ +// Package wincred provides primitives for accessing the Windows Credentials Management API. +// This includes functions for retrieval, listing and storage of credentials as well as Go structures for convenient access to the credential data. +// +// A more detailed description of Windows Credentials Management can be found on +// Docs: https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/credentials-management +package wincred + +import "errors" + +const ( + // ErrElementNotFound is the error that is returned if a requested element cannot be found. + // This error constant can be used to check if a credential could not be found. + ErrElementNotFound = sysERROR_NOT_FOUND + + // ErrInvalidParameter is the error that is returned for invalid parameters. + // This error constant can be used to check if the given function parameters were invalid. + // For example when trying to create a new generic credential with an empty target name. + ErrInvalidParameter = sysERROR_INVALID_PARAMETER +) + +// GetGenericCredential fetches the generic credential with the given name from Windows credential manager. +// It returns nil and an error if the credential could not be found or an error occurred. +func GetGenericCredential(targetName string) (*GenericCredential, error) { + cred, err := sysCredRead(targetName, sysCRED_TYPE_GENERIC) + if cred != nil { + return &GenericCredential{Credential: *cred}, err + } + return nil, err +} + +// NewGenericCredential creates a new generic credential object with the given name. +// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage. +// The credential object is NOT yet persisted to the Windows credential vault. +func NewGenericCredential(targetName string) (result *GenericCredential) { + result = new(GenericCredential) + result.TargetName = targetName + result.Persist = PersistLocalMachine + return +} + +// Write persists the generic credential object to Windows credential manager. +func (t *GenericCredential) Write() (err error) { + err = sysCredWrite(&t.Credential, sysCRED_TYPE_GENERIC) + return +} + +// Delete removes the credential object from Windows credential manager. +func (t *GenericCredential) Delete() (err error) { + err = sysCredDelete(&t.Credential, sysCRED_TYPE_GENERIC) + return +} + +// GetDomainPassword fetches the domain-password credential with the given target host name from Windows credential manager. +// It returns nil and an error if the credential could not be found or an error occurred. +func GetDomainPassword(targetName string) (*DomainPassword, error) { + cred, err := sysCredRead(targetName, sysCRED_TYPE_DOMAIN_PASSWORD) + if cred != nil { + return &DomainPassword{Credential: *cred}, err + } + return nil, err +} + +// NewDomainPassword creates a new domain-password credential used for login to the given target host name. +// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage. +// The credential object is NOT yet persisted to the Windows credential vault. +func NewDomainPassword(targetName string) (result *DomainPassword) { + result = new(DomainPassword) + result.TargetName = targetName + result.Persist = PersistLocalMachine + return +} + +// Write persists the domain-password credential to Windows credential manager. +func (t *DomainPassword) Write() (err error) { + err = sysCredWrite(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD) + return +} + +// Delete removes the domain-password credential from Windows credential manager. +func (t *DomainPassword) Delete() (err error) { + err = sysCredDelete(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD) + return +} + +// SetPassword sets the CredentialBlob field of a domain password credential to the given string. +func (t *DomainPassword) SetPassword(pw string) { + t.CredentialBlob = utf16ToByte(utf16FromString(pw)) +} + +// List retrieves all credentials of the Credentials store. +func List() ([]*Credential, error) { + creds, err := sysCredEnumerate("", true) + if err != nil && errors.Is(err, ErrElementNotFound) { + // Ignore ERROR_NOT_FOUND and return an empty list instead + creds = []*Credential{} + err = nil + } + return creds, err +} + +// FilteredList retrieves the list of credentials from the Credentials store that match the given filter. +// The filter string defines the prefix followed by an asterisk for the `TargetName` attribute of the credentials. +func FilteredList(filter string) ([]*Credential, error) { + creds, err := sysCredEnumerate(filter, false) + if err != nil && errors.Is(err, ErrElementNotFound) { + // Ignore ERROR_NOT_FOUND and return an empty list instead + creds = []*Credential{} + err = nil + } + return creds, err +} diff --git a/vendor/github.com/dustin/go-humanize/.travis.yml b/vendor/github.com/dustin/go-humanize/.travis.yml new file mode 100644 index 00000000..ac12e485 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/.travis.yml @@ -0,0 +1,21 @@ +sudo: false +language: go +go_import_path: github.com/dustin/go-humanize +go: + - 1.13.x + - 1.14.x + - 1.15.x + - 1.16.x + - stable + - master +matrix: + allow_failures: + - go: master + fast_finish: true +install: + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). +script: + - diff -u <(echo -n) <(gofmt -d -s .) + - go vet . + - go install -v -race ./... + - go test -v -race ./... diff --git a/vendor/github.com/dustin/go-humanize/LICENSE b/vendor/github.com/dustin/go-humanize/LICENSE new file mode 100644 index 00000000..8d9a94a9 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2005-2008 Dustin Sallings + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + diff --git a/vendor/github.com/dustin/go-humanize/README.markdown b/vendor/github.com/dustin/go-humanize/README.markdown new file mode 100644 index 00000000..7d0b16b3 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/README.markdown @@ -0,0 +1,124 @@ +# Humane Units [![Build Status](https://travis-ci.org/dustin/go-humanize.svg?branch=master)](https://travis-ci.org/dustin/go-humanize) [![GoDoc](https://godoc.org/github.com/dustin/go-humanize?status.svg)](https://godoc.org/github.com/dustin/go-humanize) + +Just a few functions for helping humanize times and sizes. + +`go get` it as `github.com/dustin/go-humanize`, import it as +`"github.com/dustin/go-humanize"`, use it as `humanize`. + +See [godoc](https://pkg.go.dev/github.com/dustin/go-humanize) for +complete documentation. + +## Sizes + +This lets you take numbers like `82854982` and convert them to useful +strings like, `83 MB` or `79 MiB` (whichever you prefer). + +Example: + +```go +fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB. +``` + +## Times + +This lets you take a `time.Time` and spit it out in relative terms. +For example, `12 seconds ago` or `3 days from now`. + +Example: + +```go +fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago. +``` + +Thanks to Kyle Lemons for the time implementation from an IRC +conversation one day. It's pretty neat. + +## Ordinals + +From a [mailing list discussion][odisc] where a user wanted to be able +to label ordinals. + + 0 -> 0th + 1 -> 1st + 2 -> 2nd + 3 -> 3rd + 4 -> 4th + [...] + +Example: + +```go +fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend. +``` + +## Commas + +Want to shove commas into numbers? Be my guest. + + 0 -> 0 + 100 -> 100 + 1000 -> 1,000 + 1000000000 -> 1,000,000,000 + -100000 -> -100,000 + +Example: + +```go +fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491. +``` + +## Ftoa + +Nicer float64 formatter that removes trailing zeros. + +```go +fmt.Printf("%f", 2.24) // 2.240000 +fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24 +fmt.Printf("%f", 2.0) // 2.000000 +fmt.Printf("%s", humanize.Ftoa(2.0)) // 2 +``` + +## SI notation + +Format numbers with [SI notation][sinotation]. + +Example: + +```go +humanize.SI(0.00000000223, "M") // 2.23 nM +``` + +## English-specific functions + +The following functions are in the `humanize/english` subpackage. + +### Plurals + +Simple English pluralization + +```go +english.PluralWord(1, "object", "") // object +english.PluralWord(42, "object", "") // objects +english.PluralWord(2, "bus", "") // buses +english.PluralWord(99, "locus", "loci") // loci + +english.Plural(1, "object", "") // 1 object +english.Plural(42, "object", "") // 42 objects +english.Plural(2, "bus", "") // 2 buses +english.Plural(99, "locus", "loci") // 99 loci +``` + +### Word series + +Format comma-separated words lists with conjuctions: + +```go +english.WordSeries([]string{"foo"}, "and") // foo +english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar +english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz + +english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz +``` + +[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion +[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix diff --git a/vendor/github.com/dustin/go-humanize/big.go b/vendor/github.com/dustin/go-humanize/big.go new file mode 100644 index 00000000..f49dc337 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/big.go @@ -0,0 +1,31 @@ +package humanize + +import ( + "math/big" +) + +// order of magnitude (to a max order) +func oomm(n, b *big.Int, maxmag int) (float64, int) { + mag := 0 + m := &big.Int{} + for n.Cmp(b) >= 0 { + n.DivMod(n, b, m) + mag++ + if mag == maxmag && maxmag >= 0 { + break + } + } + return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag +} + +// total order of magnitude +// (same as above, but with no upper limit) +func oom(n, b *big.Int) (float64, int) { + mag := 0 + m := &big.Int{} + for n.Cmp(b) >= 0 { + n.DivMod(n, b, m) + mag++ + } + return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag +} diff --git a/vendor/github.com/dustin/go-humanize/bigbytes.go b/vendor/github.com/dustin/go-humanize/bigbytes.go new file mode 100644 index 00000000..3b015fd5 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/bigbytes.go @@ -0,0 +1,189 @@ +package humanize + +import ( + "fmt" + "math/big" + "strings" + "unicode" +) + +var ( + bigIECExp = big.NewInt(1024) + + // BigByte is one byte in bit.Ints + BigByte = big.NewInt(1) + // BigKiByte is 1,024 bytes in bit.Ints + BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp) + // BigMiByte is 1,024 k bytes in bit.Ints + BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp) + // BigGiByte is 1,024 m bytes in bit.Ints + BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp) + // BigTiByte is 1,024 g bytes in bit.Ints + BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp) + // BigPiByte is 1,024 t bytes in bit.Ints + BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp) + // BigEiByte is 1,024 p bytes in bit.Ints + BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp) + // BigZiByte is 1,024 e bytes in bit.Ints + BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp) + // BigYiByte is 1,024 z bytes in bit.Ints + BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp) + // BigRiByte is 1,024 y bytes in bit.Ints + BigRiByte = (&big.Int{}).Mul(BigYiByte, bigIECExp) + // BigQiByte is 1,024 r bytes in bit.Ints + BigQiByte = (&big.Int{}).Mul(BigRiByte, bigIECExp) +) + +var ( + bigSIExp = big.NewInt(1000) + + // BigSIByte is one SI byte in big.Ints + BigSIByte = big.NewInt(1) + // BigKByte is 1,000 SI bytes in big.Ints + BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp) + // BigMByte is 1,000 SI k bytes in big.Ints + BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp) + // BigGByte is 1,000 SI m bytes in big.Ints + BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp) + // BigTByte is 1,000 SI g bytes in big.Ints + BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp) + // BigPByte is 1,000 SI t bytes in big.Ints + BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp) + // BigEByte is 1,000 SI p bytes in big.Ints + BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp) + // BigZByte is 1,000 SI e bytes in big.Ints + BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp) + // BigYByte is 1,000 SI z bytes in big.Ints + BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp) + // BigRByte is 1,000 SI y bytes in big.Ints + BigRByte = (&big.Int{}).Mul(BigYByte, bigSIExp) + // BigQByte is 1,000 SI r bytes in big.Ints + BigQByte = (&big.Int{}).Mul(BigRByte, bigSIExp) +) + +var bigBytesSizeTable = map[string]*big.Int{ + "b": BigByte, + "kib": BigKiByte, + "kb": BigKByte, + "mib": BigMiByte, + "mb": BigMByte, + "gib": BigGiByte, + "gb": BigGByte, + "tib": BigTiByte, + "tb": BigTByte, + "pib": BigPiByte, + "pb": BigPByte, + "eib": BigEiByte, + "eb": BigEByte, + "zib": BigZiByte, + "zb": BigZByte, + "yib": BigYiByte, + "yb": BigYByte, + "rib": BigRiByte, + "rb": BigRByte, + "qib": BigQiByte, + "qb": BigQByte, + // Without suffix + "": BigByte, + "ki": BigKiByte, + "k": BigKByte, + "mi": BigMiByte, + "m": BigMByte, + "gi": BigGiByte, + "g": BigGByte, + "ti": BigTiByte, + "t": BigTByte, + "pi": BigPiByte, + "p": BigPByte, + "ei": BigEiByte, + "e": BigEByte, + "z": BigZByte, + "zi": BigZiByte, + "y": BigYByte, + "yi": BigYiByte, + "r": BigRByte, + "ri": BigRiByte, + "q": BigQByte, + "qi": BigQiByte, +} + +var ten = big.NewInt(10) + +func humanateBigBytes(s, base *big.Int, sizes []string) string { + if s.Cmp(ten) < 0 { + return fmt.Sprintf("%d B", s) + } + c := (&big.Int{}).Set(s) + val, mag := oomm(c, base, len(sizes)-1) + suffix := sizes[mag] + f := "%.0f %s" + if val < 10 { + f = "%.1f %s" + } + + return fmt.Sprintf(f, val, suffix) + +} + +// BigBytes produces a human readable representation of an SI size. +// +// See also: ParseBigBytes. +// +// BigBytes(82854982) -> 83 MB +func BigBytes(s *big.Int) string { + sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "RB", "QB"} + return humanateBigBytes(s, bigSIExp, sizes) +} + +// BigIBytes produces a human readable representation of an IEC size. +// +// See also: ParseBigBytes. +// +// BigIBytes(82854982) -> 79 MiB +func BigIBytes(s *big.Int) string { + sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB"} + return humanateBigBytes(s, bigIECExp, sizes) +} + +// ParseBigBytes parses a string representation of bytes into the number +// of bytes it represents. +// +// See also: BigBytes, BigIBytes. +// +// ParseBigBytes("42 MB") -> 42000000, nil +// ParseBigBytes("42 mib") -> 44040192, nil +func ParseBigBytes(s string) (*big.Int, error) { + lastDigit := 0 + hasComma := false + for _, r := range s { + if !(unicode.IsDigit(r) || r == '.' || r == ',') { + break + } + if r == ',' { + hasComma = true + } + lastDigit++ + } + + num := s[:lastDigit] + if hasComma { + num = strings.Replace(num, ",", "", -1) + } + + val := &big.Rat{} + _, err := fmt.Sscanf(num, "%f", val) + if err != nil { + return nil, err + } + + extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) + if m, ok := bigBytesSizeTable[extra]; ok { + mv := (&big.Rat{}).SetInt(m) + val.Mul(val, mv) + rv := &big.Int{} + rv.Div(val.Num(), val.Denom()) + return rv, nil + } + + return nil, fmt.Errorf("unhandled size name: %v", extra) +} diff --git a/vendor/github.com/dustin/go-humanize/bytes.go b/vendor/github.com/dustin/go-humanize/bytes.go new file mode 100644 index 00000000..0b498f48 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/bytes.go @@ -0,0 +1,143 @@ +package humanize + +import ( + "fmt" + "math" + "strconv" + "strings" + "unicode" +) + +// IEC Sizes. +// kibis of bits +const ( + Byte = 1 << (iota * 10) + KiByte + MiByte + GiByte + TiByte + PiByte + EiByte +) + +// SI Sizes. +const ( + IByte = 1 + KByte = IByte * 1000 + MByte = KByte * 1000 + GByte = MByte * 1000 + TByte = GByte * 1000 + PByte = TByte * 1000 + EByte = PByte * 1000 +) + +var bytesSizeTable = map[string]uint64{ + "b": Byte, + "kib": KiByte, + "kb": KByte, + "mib": MiByte, + "mb": MByte, + "gib": GiByte, + "gb": GByte, + "tib": TiByte, + "tb": TByte, + "pib": PiByte, + "pb": PByte, + "eib": EiByte, + "eb": EByte, + // Without suffix + "": Byte, + "ki": KiByte, + "k": KByte, + "mi": MiByte, + "m": MByte, + "gi": GiByte, + "g": GByte, + "ti": TiByte, + "t": TByte, + "pi": PiByte, + "p": PByte, + "ei": EiByte, + "e": EByte, +} + +func logn(n, b float64) float64 { + return math.Log(n) / math.Log(b) +} + +func humanateBytes(s uint64, base float64, sizes []string) string { + if s < 10 { + return fmt.Sprintf("%d B", s) + } + e := math.Floor(logn(float64(s), base)) + suffix := sizes[int(e)] + val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10 + f := "%.0f %s" + if val < 10 { + f = "%.1f %s" + } + + return fmt.Sprintf(f, val, suffix) +} + +// Bytes produces a human readable representation of an SI size. +// +// See also: ParseBytes. +// +// Bytes(82854982) -> 83 MB +func Bytes(s uint64) string { + sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} + return humanateBytes(s, 1000, sizes) +} + +// IBytes produces a human readable representation of an IEC size. +// +// See also: ParseBytes. +// +// IBytes(82854982) -> 79 MiB +func IBytes(s uint64) string { + sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"} + return humanateBytes(s, 1024, sizes) +} + +// ParseBytes parses a string representation of bytes into the number +// of bytes it represents. +// +// See Also: Bytes, IBytes. +// +// ParseBytes("42 MB") -> 42000000, nil +// ParseBytes("42 mib") -> 44040192, nil +func ParseBytes(s string) (uint64, error) { + lastDigit := 0 + hasComma := false + for _, r := range s { + if !(unicode.IsDigit(r) || r == '.' || r == ',') { + break + } + if r == ',' { + hasComma = true + } + lastDigit++ + } + + num := s[:lastDigit] + if hasComma { + num = strings.Replace(num, ",", "", -1) + } + + f, err := strconv.ParseFloat(num, 64) + if err != nil { + return 0, err + } + + extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) + if m, ok := bytesSizeTable[extra]; ok { + f *= float64(m) + if f >= math.MaxUint64 { + return 0, fmt.Errorf("too large: %v", s) + } + return uint64(f), nil + } + + return 0, fmt.Errorf("unhandled size name: %v", extra) +} diff --git a/vendor/github.com/dustin/go-humanize/comma.go b/vendor/github.com/dustin/go-humanize/comma.go new file mode 100644 index 00000000..520ae3e5 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/comma.go @@ -0,0 +1,116 @@ +package humanize + +import ( + "bytes" + "math" + "math/big" + "strconv" + "strings" +) + +// Comma produces a string form of the given number in base 10 with +// commas after every three orders of magnitude. +// +// e.g. Comma(834142) -> 834,142 +func Comma(v int64) string { + sign := "" + + // Min int64 can't be negated to a usable value, so it has to be special cased. + if v == math.MinInt64 { + return "-9,223,372,036,854,775,808" + } + + if v < 0 { + sign = "-" + v = 0 - v + } + + parts := []string{"", "", "", "", "", "", ""} + j := len(parts) - 1 + + for v > 999 { + parts[j] = strconv.FormatInt(v%1000, 10) + switch len(parts[j]) { + case 2: + parts[j] = "0" + parts[j] + case 1: + parts[j] = "00" + parts[j] + } + v = v / 1000 + j-- + } + parts[j] = strconv.Itoa(int(v)) + return sign + strings.Join(parts[j:], ",") +} + +// Commaf produces a string form of the given number in base 10 with +// commas after every three orders of magnitude. +// +// e.g. Commaf(834142.32) -> 834,142.32 +func Commaf(v float64) string { + buf := &bytes.Buffer{} + if v < 0 { + buf.Write([]byte{'-'}) + v = 0 - v + } + + comma := []byte{','} + + parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".") + pos := 0 + if len(parts[0])%3 != 0 { + pos += len(parts[0]) % 3 + buf.WriteString(parts[0][:pos]) + buf.Write(comma) + } + for ; pos < len(parts[0]); pos += 3 { + buf.WriteString(parts[0][pos : pos+3]) + buf.Write(comma) + } + buf.Truncate(buf.Len() - 1) + + if len(parts) > 1 { + buf.Write([]byte{'.'}) + buf.WriteString(parts[1]) + } + return buf.String() +} + +// CommafWithDigits works like the Commaf but limits the resulting +// string to the given number of decimal places. +// +// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3 +func CommafWithDigits(f float64, decimals int) string { + return stripTrailingDigits(Commaf(f), decimals) +} + +// BigComma produces a string form of the given big.Int in base 10 +// with commas after every three orders of magnitude. +func BigComma(b *big.Int) string { + sign := "" + if b.Sign() < 0 { + sign = "-" + b.Abs(b) + } + + athousand := big.NewInt(1000) + c := (&big.Int{}).Set(b) + _, m := oom(c, athousand) + parts := make([]string, m+1) + j := len(parts) - 1 + + mod := &big.Int{} + for b.Cmp(athousand) >= 0 { + b.DivMod(b, athousand, mod) + parts[j] = strconv.FormatInt(mod.Int64(), 10) + switch len(parts[j]) { + case 2: + parts[j] = "0" + parts[j] + case 1: + parts[j] = "00" + parts[j] + } + j-- + } + parts[j] = strconv.Itoa(int(b.Int64())) + return sign + strings.Join(parts[j:], ",") +} diff --git a/vendor/github.com/dustin/go-humanize/commaf.go b/vendor/github.com/dustin/go-humanize/commaf.go new file mode 100644 index 00000000..2bc83a03 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/commaf.go @@ -0,0 +1,41 @@ +//go:build go1.6 +// +build go1.6 + +package humanize + +import ( + "bytes" + "math/big" + "strings" +) + +// BigCommaf produces a string form of the given big.Float in base 10 +// with commas after every three orders of magnitude. +func BigCommaf(v *big.Float) string { + buf := &bytes.Buffer{} + if v.Sign() < 0 { + buf.Write([]byte{'-'}) + v.Abs(v) + } + + comma := []byte{','} + + parts := strings.Split(v.Text('f', -1), ".") + pos := 0 + if len(parts[0])%3 != 0 { + pos += len(parts[0]) % 3 + buf.WriteString(parts[0][:pos]) + buf.Write(comma) + } + for ; pos < len(parts[0]); pos += 3 { + buf.WriteString(parts[0][pos : pos+3]) + buf.Write(comma) + } + buf.Truncate(buf.Len() - 1) + + if len(parts) > 1 { + buf.Write([]byte{'.'}) + buf.WriteString(parts[1]) + } + return buf.String() +} diff --git a/vendor/github.com/dustin/go-humanize/ftoa.go b/vendor/github.com/dustin/go-humanize/ftoa.go new file mode 100644 index 00000000..bce923f3 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/ftoa.go @@ -0,0 +1,49 @@ +package humanize + +import ( + "strconv" + "strings" +) + +func stripTrailingZeros(s string) string { + if !strings.ContainsRune(s, '.') { + return s + } + offset := len(s) - 1 + for offset > 0 { + if s[offset] == '.' { + offset-- + break + } + if s[offset] != '0' { + break + } + offset-- + } + return s[:offset+1] +} + +func stripTrailingDigits(s string, digits int) string { + if i := strings.Index(s, "."); i >= 0 { + if digits <= 0 { + return s[:i] + } + i++ + if i+digits >= len(s) { + return s + } + return s[:i+digits] + } + return s +} + +// Ftoa converts a float to a string with no trailing zeros. +func Ftoa(num float64) string { + return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64)) +} + +// FtoaWithDigits converts a float to a string but limits the resulting string +// to the given number of decimal places, and no trailing zeros. +func FtoaWithDigits(num float64, digits int) string { + return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits)) +} diff --git a/vendor/github.com/dustin/go-humanize/humanize.go b/vendor/github.com/dustin/go-humanize/humanize.go new file mode 100644 index 00000000..a2c2da31 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/humanize.go @@ -0,0 +1,8 @@ +/* +Package humanize converts boring ugly numbers to human-friendly strings and back. + +Durations can be turned into strings such as "3 days ago", numbers +representing sizes like 82854982 into useful strings like, "83 MB" or +"79 MiB" (whichever you prefer). +*/ +package humanize diff --git a/vendor/github.com/dustin/go-humanize/number.go b/vendor/github.com/dustin/go-humanize/number.go new file mode 100644 index 00000000..6470d0d4 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/number.go @@ -0,0 +1,192 @@ +package humanize + +/* +Slightly adapted from the source to fit go-humanize. + +Author: https://github.com/gorhill +Source: https://gist.github.com/gorhill/5285193 + +*/ + +import ( + "math" + "strconv" +) + +var ( + renderFloatPrecisionMultipliers = [...]float64{ + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, + } + + renderFloatPrecisionRounders = [...]float64{ + 0.5, + 0.05, + 0.005, + 0.0005, + 0.00005, + 0.000005, + 0.0000005, + 0.00000005, + 0.000000005, + 0.0000000005, + } +) + +// FormatFloat produces a formatted number as string based on the following user-specified criteria: +// * thousands separator +// * decimal separator +// * decimal precision +// +// Usage: s := RenderFloat(format, n) +// The format parameter tells how to render the number n. +// +// See examples: http://play.golang.org/p/LXc1Ddm1lJ +// +// Examples of format strings, given n = 12345.6789: +// "#,###.##" => "12,345.67" +// "#,###." => "12,345" +// "#,###" => "12345,678" +// "#\u202F###,##" => "12 345,68" +// "#.###,###### => 12.345,678900 +// "" (aka default format) => 12,345.67 +// +// The highest precision allowed is 9 digits after the decimal symbol. +// There is also a version for integer number, FormatInteger(), +// which is convenient for calls within template. +func FormatFloat(format string, n float64) string { + // Special cases: + // NaN = "NaN" + // +Inf = "+Infinity" + // -Inf = "-Infinity" + if math.IsNaN(n) { + return "NaN" + } + if n > math.MaxFloat64 { + return "Infinity" + } + if n < (0.0 - math.MaxFloat64) { + return "-Infinity" + } + + // default format + precision := 2 + decimalStr := "." + thousandStr := "," + positiveStr := "" + negativeStr := "-" + + if len(format) > 0 { + format := []rune(format) + + // If there is an explicit format directive, + // then default values are these: + precision = 9 + thousandStr = "" + + // collect indices of meaningful formatting directives + formatIndx := []int{} + for i, char := range format { + if char != '#' && char != '0' { + formatIndx = append(formatIndx, i) + } + } + + if len(formatIndx) > 0 { + // Directive at index 0: + // Must be a '+' + // Raise an error if not the case + // index: 0123456789 + // +0.000,000 + // +000,000.0 + // +0000.00 + // +0000 + if formatIndx[0] == 0 { + if format[formatIndx[0]] != '+' { + panic("RenderFloat(): invalid positive sign directive") + } + positiveStr = "+" + formatIndx = formatIndx[1:] + } + + // Two directives: + // First is thousands separator + // Raise an error if not followed by 3-digit + // 0123456789 + // 0.000,000 + // 000,000.00 + if len(formatIndx) == 2 { + if (formatIndx[1] - formatIndx[0]) != 4 { + panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers") + } + thousandStr = string(format[formatIndx[0]]) + formatIndx = formatIndx[1:] + } + + // One directive: + // Directive is decimal separator + // The number of digit-specifier following the separator indicates wanted precision + // 0123456789 + // 0.00 + // 000,0000 + if len(formatIndx) == 1 { + decimalStr = string(format[formatIndx[0]]) + precision = len(format) - formatIndx[0] - 1 + } + } + } + + // generate sign part + var signStr string + if n >= 0.000000001 { + signStr = positiveStr + } else if n <= -0.000000001 { + signStr = negativeStr + n = -n + } else { + signStr = "" + n = 0.0 + } + + // split number into integer and fractional parts + intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision]) + + // generate integer part string + intStr := strconv.FormatInt(int64(intf), 10) + + // add thousand separator if required + if len(thousandStr) > 0 { + for i := len(intStr); i > 3; { + i -= 3 + intStr = intStr[:i] + thousandStr + intStr[i:] + } + } + + // no fractional part, we can leave now + if precision == 0 { + return signStr + intStr + } + + // generate fractional part + fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision])) + // may need padding + if len(fracStr) < precision { + fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr + } + + return signStr + intStr + decimalStr + fracStr +} + +// FormatInteger produces a formatted number as string. +// See FormatFloat. +func FormatInteger(format string, n int) string { + return FormatFloat(format, float64(n)) +} diff --git a/vendor/github.com/dustin/go-humanize/ordinals.go b/vendor/github.com/dustin/go-humanize/ordinals.go new file mode 100644 index 00000000..43d88a86 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/ordinals.go @@ -0,0 +1,25 @@ +package humanize + +import "strconv" + +// Ordinal gives you the input number in a rank/ordinal format. +// +// Ordinal(3) -> 3rd +func Ordinal(x int) string { + suffix := "th" + switch x % 10 { + case 1: + if x%100 != 11 { + suffix = "st" + } + case 2: + if x%100 != 12 { + suffix = "nd" + } + case 3: + if x%100 != 13 { + suffix = "rd" + } + } + return strconv.Itoa(x) + suffix +} diff --git a/vendor/github.com/dustin/go-humanize/si.go b/vendor/github.com/dustin/go-humanize/si.go new file mode 100644 index 00000000..8b850198 --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/si.go @@ -0,0 +1,127 @@ +package humanize + +import ( + "errors" + "math" + "regexp" + "strconv" +) + +var siPrefixTable = map[float64]string{ + -30: "q", // quecto + -27: "r", // ronto + -24: "y", // yocto + -21: "z", // zepto + -18: "a", // atto + -15: "f", // femto + -12: "p", // pico + -9: "n", // nano + -6: "µ", // micro + -3: "m", // milli + 0: "", + 3: "k", // kilo + 6: "M", // mega + 9: "G", // giga + 12: "T", // tera + 15: "P", // peta + 18: "E", // exa + 21: "Z", // zetta + 24: "Y", // yotta + 27: "R", // ronna + 30: "Q", // quetta +} + +var revSIPrefixTable = revfmap(siPrefixTable) + +// revfmap reverses the map and precomputes the power multiplier +func revfmap(in map[float64]string) map[string]float64 { + rv := map[string]float64{} + for k, v := range in { + rv[v] = math.Pow(10, k) + } + return rv +} + +var riParseRegex *regexp.Regexp + +func init() { + ri := `^([\-0-9.]+)\s?([` + for _, v := range siPrefixTable { + ri += v + } + ri += `]?)(.*)` + + riParseRegex = regexp.MustCompile(ri) +} + +// ComputeSI finds the most appropriate SI prefix for the given number +// and returns the prefix along with the value adjusted to be within +// that prefix. +// +// See also: SI, ParseSI. +// +// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p") +func ComputeSI(input float64) (float64, string) { + if input == 0 { + return 0, "" + } + mag := math.Abs(input) + exponent := math.Floor(logn(mag, 10)) + exponent = math.Floor(exponent/3) * 3 + + value := mag / math.Pow(10, exponent) + + // Handle special case where value is exactly 1000.0 + // Should return 1 M instead of 1000 k + if value == 1000.0 { + exponent += 3 + value = mag / math.Pow(10, exponent) + } + + value = math.Copysign(value, input) + + prefix := siPrefixTable[exponent] + return value, prefix +} + +// SI returns a string with default formatting. +// +// SI uses Ftoa to format float value, removing trailing zeros. +// +// See also: ComputeSI, ParseSI. +// +// e.g. SI(1000000, "B") -> 1 MB +// e.g. SI(2.2345e-12, "F") -> 2.2345 pF +func SI(input float64, unit string) string { + value, prefix := ComputeSI(input) + return Ftoa(value) + " " + prefix + unit +} + +// SIWithDigits works like SI but limits the resulting string to the +// given number of decimal places. +// +// e.g. SIWithDigits(1000000, 0, "B") -> 1 MB +// e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF +func SIWithDigits(input float64, decimals int, unit string) string { + value, prefix := ComputeSI(input) + return FtoaWithDigits(value, decimals) + " " + prefix + unit +} + +var errInvalid = errors.New("invalid input") + +// ParseSI parses an SI string back into the number and unit. +// +// See also: SI, ComputeSI. +// +// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil) +func ParseSI(input string) (float64, string, error) { + found := riParseRegex.FindStringSubmatch(input) + if len(found) != 4 { + return 0, "", errInvalid + } + mag := revSIPrefixTable[found[2]] + unit := found[3] + + base, err := strconv.ParseFloat(found[1], 64) + return base * mag, unit, err +} diff --git a/vendor/github.com/dustin/go-humanize/times.go b/vendor/github.com/dustin/go-humanize/times.go new file mode 100644 index 00000000..dd3fbf5e --- /dev/null +++ b/vendor/github.com/dustin/go-humanize/times.go @@ -0,0 +1,117 @@ +package humanize + +import ( + "fmt" + "math" + "sort" + "time" +) + +// Seconds-based time units +const ( + Day = 24 * time.Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month + LongTime = 37 * Year +) + +// Time formats a time into a relative string. +// +// Time(someT) -> "3 weeks ago" +func Time(then time.Time) string { + return RelTime(then, time.Now(), "ago", "from now") +} + +// A RelTimeMagnitude struct contains a relative time point at which +// the relative format of time will switch to a new format string. A +// slice of these in ascending order by their "D" field is passed to +// CustomRelTime to format durations. +// +// The Format field is a string that may contain a "%s" which will be +// replaced with the appropriate signed label (e.g. "ago" or "from +// now") and a "%d" that will be replaced by the quantity. +// +// The DivBy field is the amount of time the time difference must be +// divided by in order to display correctly. +// +// e.g. if D is 2*time.Minute and you want to display "%d minutes %s" +// DivBy should be time.Minute so whatever the duration is will be +// expressed in minutes. +type RelTimeMagnitude struct { + D time.Duration + Format string + DivBy time.Duration +} + +var defaultMagnitudes = []RelTimeMagnitude{ + {time.Second, "now", time.Second}, + {2 * time.Second, "1 second %s", 1}, + {time.Minute, "%d seconds %s", time.Second}, + {2 * time.Minute, "1 minute %s", 1}, + {time.Hour, "%d minutes %s", time.Minute}, + {2 * time.Hour, "1 hour %s", 1}, + {Day, "%d hours %s", time.Hour}, + {2 * Day, "1 day %s", 1}, + {Week, "%d days %s", Day}, + {2 * Week, "1 week %s", 1}, + {Month, "%d weeks %s", Week}, + {2 * Month, "1 month %s", 1}, + {Year, "%d months %s", Month}, + {18 * Month, "1 year %s", 1}, + {2 * Year, "2 years %s", 1}, + {LongTime, "%d years %s", Year}, + {math.MaxInt64, "a long while %s", 1}, +} + +// RelTime formats a time into a relative string. +// +// It takes two times and two labels. In addition to the generic time +// delta string (e.g. 5 minutes), the labels are used applied so that +// the label corresponding to the smaller time is applied. +// +// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier" +func RelTime(a, b time.Time, albl, blbl string) string { + return CustomRelTime(a, b, albl, blbl, defaultMagnitudes) +} + +// CustomRelTime formats a time into a relative string. +// +// It takes two times two labels and a table of relative time formats. +// In addition to the generic time delta string (e.g. 5 minutes), the +// labels are used applied so that the label corresponding to the +// smaller time is applied. +func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string { + lbl := albl + diff := b.Sub(a) + + if a.After(b) { + lbl = blbl + diff = a.Sub(b) + } + + n := sort.Search(len(magnitudes), func(i int) bool { + return magnitudes[i].D > diff + }) + + if n >= len(magnitudes) { + n = len(magnitudes) - 1 + } + mag := magnitudes[n] + args := []interface{}{} + escaped := false + for _, ch := range mag.Format { + if escaped { + switch ch { + case 's': + args = append(args, lbl) + case 'd': + args = append(args, diff/mag.DivBy) + } + escaped = false + } else { + escaped = ch == '%' + } + } + return fmt.Sprintf(mag.Format, args...) +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/.gitignore b/vendor/github.com/dvsekhvalnov/jose2go/.gitignore new file mode 100644 index 00000000..83656241 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test diff --git a/vendor/github.com/dvsekhvalnov/jose2go/LICENSE b/vendor/github.com/dvsekhvalnov/jose2go/LICENSE new file mode 100644 index 00000000..326fe253 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/dvsekhvalnov/jose2go/README.md b/vendor/github.com/dvsekhvalnov/jose2go/README.md new file mode 100644 index 00000000..9df801c5 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/README.md @@ -0,0 +1,938 @@ +# Golang (GO) Javascript Object Signing and Encryption (JOSE) and JSON Web Token (JWT) implementation + +[![GoDoc](https://godoc.org/github.com/dvsekhvalnov/jose2go?status.svg)](http://godoc.org/github.com/dvsekhvalnov/jose2go) + +Pure Golang (GO) library for generating, decoding and encrypting [JSON Web Tokens](https://tools.ietf.org/html/rfc7519). Zero dependency, relies only +on standard library. + +Supports full suite of signing, encryption and compression algorithms defined by [JSON Web Algorithms](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-31) as of July 4, 2014 version. + +Extensively unit tested and cross tested (100+ tests) for compatibility with [jose.4.j](https://bitbucket.org/b_c/jose4j/wiki/Home), [Nimbus-JOSE-JWT](https://bitbucket.org/nimbusds/nimbus-jose-jwt/wiki/Home), [json-jwt](https://github.com/nov/json-jwt) and +[jose-jwt](https://github.com/dvsekhvalnov/jose-jwt) libraries. + + +## Status +Used in production. GA ready. Current version is 1.5. + +## Important +v1.5 bug fix release + +v1.4 changes default behavior of inserting `typ=JWT` header if not overriden. As of 1.4 no +extra headers added by library automatically. To mimic pre 1.4 behaviour use: +```Go +token, err := jose.Sign(..., jose.Header("typ", "JWT")) + +//or + +token, err := jose.Encrypt(..., jose.Header("typ", "JWT")) +``` + +v1.3 fixed potential Invalid Curve Attack on NIST curves within ECDH key management. +Upgrade strongly recommended. + +v1.2 breaks `jose.Decode` interface by returning 3 values instead of 2. + +v1.2 deprecates `jose.Compress` method in favor of using configuration options to `jose.Encrypt`, +the method will be removed in next release. + +### Migration to v1.2 +Pre v1.2 decoding: + +```Go +payload,err := jose.Decode(token,sharedKey) +``` + +Should be updated to v1.2: + +```Go +payload, headers, err := jose.Decode(token,sharedKey) +``` + +Pre v1.2 compression: + +```Go +token,err := jose.Compress(payload,jose.DIR,jose.A128GCM,jose.DEF, key) +``` + +Should be update to v1.2: + +```Go +token, err := jose.Encrypt(payload, jose.DIR, jose.A128GCM, key, jose.Zip(jose.DEF)) +``` + +## Supported JWA algorithms + +**Signing** +- HMAC signatures with HS256, HS384 and HS512. +- RSASSA-PKCS1-V1_5 signatures with RS256, RS384 and RS512. +- RSASSA-PSS signatures (probabilistic signature scheme with appendix) with PS256, PS384 and PS512. +- ECDSA signatures with ES256, ES384 and ES512. +- NONE (unprotected) plain text algorithm without integrity protection + +**Encryption** +- RSAES OAEP (using SHA-1 and MGF1 with SHA-1) encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- RSAES OAEP 256 (using SHA-256 and MGF1 with SHA-256) encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- RSAES-PKCS1-V1_5 encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- A128KW, A192KW, A256KW encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- A128GCMKW, A192GCMKW, A256GCMKW encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- ECDH-ES with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- Direct symmetric key encryption with pre-shared key A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM and A256GCM + +**Compression** +- DEFLATE compression + +## Installation +### Grab package from github +`go get github.com/dvsekhvalnov/jose2go` or `go get -u github.com/dvsekhvalnov/jose2go` to update to latest version + +### Import package +```Go +import ( + "github.com/dvsekhvalnov/jose2go" +) +``` + +## Usage +#### Creating Plaintext (unprotected) Tokens + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + token,err := jose.Sign(payload,jose.NONE, nil) + + if(err==nil) { + //go use token + fmt.Printf("\nPlaintext = %v\n",token) + } +} +``` + +### Creating signed tokens +#### HS-256, HS-384 and HS-512 +Signing with HS256, HS384, HS512 expecting `[]byte` array key of corresponding length: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + key := []byte{97,48,97,50,97,98,100,56,45,54,49,54,50,45,52,49,99,51,45,56,51,100,54,45,49,99,102,53,53,57,98,52,54,97,102,99} + + token,err := jose.Sign(payload,jose.HS256,key) + + if(err==nil) { + //go use token + fmt.Printf("\nHS256 = %v\n",token) + } +} +``` + +#### RS-256, RS-384 and RS-512, PS-256, PS-384 and PS-512 +Signing with RS256, RS384, RS512, PS256, PS384, PS512 expecting `*rsa.PrivateKey` private key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*rsa.PrivateKey` instance from PEM encoded PKCS1 or PKCS8 data: `Rsa.ReadPrivate([]byte)` under `jose2go/keys/rsa` package. + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + keyBytes,err := ioutil.ReadFile("private.key") + + if(err!=nil) { + panic("invalid key file") + } + + privateKey,e:=Rsa.ReadPrivate(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + token,err := jose.Sign(payload,jose.RS256, privateKey) + + if(err==nil) { + //go use token + fmt.Printf("\nRS256 = %v\n",token) + } +} +``` + +#### ES-256, ES-384 and ES-512 +ES256, ES384, ES512 ECDSA signatures expecting `*ecdsa.PrivateKey` private elliptic curve key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*ecdsa.PrivateKey` instance from PEM encoded PKCS1 or PKCS8 data: `ecc.ReadPrivate([]byte)` or directly from `X,Y,D` parameters: `ecc.NewPrivate(x,y,d []byte)` under `jose2go/keys/ecc` package. + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello":"world"}` + + privateKey:=ecc.NewPrivate([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}, + []byte{ 42, 148, 231, 48, 225, 196, 166, 201, 23, 190, 229, 199, 20, 39, 226, 70, 209, 148, 29, 70, 125, 14, 174, 66, 9, 198, 80, 251, 95, 107, 98, 206 }) + + token,err := jose.Sign(payload, jose.ES256, privateKey) + + if(err==nil) { + //go use token + fmt.Printf("\ntoken = %v\n",token) + } +} +``` + +### Creating encrypted tokens +#### RSA-OAEP-256, RSA-OAEP and RSA1\_5 key management algorithm +RSA-OAEP-256, RSA-OAEP and RSA1_5 key management expecting `*rsa.PublicKey` public key of corresponding length. + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + keyBytes,err := ioutil.ReadFile("public.key") + + if(err!=nil) { + panic("invalid key file") + } + + publicKey,e:=Rsa.ReadPublic(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + //OR: + //token,err := jose.Encrypt(payload, jose.RSA1_5, jose.A256GCM, publicKey) + token,err := jose.Encrypt(payload, jose.RSA_OAEP, jose.A256GCM, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\ntoken = %v\n",token) + } +} +``` + +#### AES Key Wrap key management family of algorithms +AES128KW, AES192KW and AES256KW key management requires `[]byte` array key of corresponding length + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + sharedKey :=[]byte{194,164,235,6,138,248,171,239,24,216,11,22,137,199,215,133} + + token,err := jose.Encrypt(payload,jose.A128KW,jose.A128GCM,sharedKey) + + if(err==nil) { + //go use token + fmt.Printf("\nA128KW A128GCM = %v\n",token) + } +} +``` + +#### AES GCM Key Wrap key management family of algorithms +AES128GCMKW, AES192GCMKW and AES256GCMKW key management requires `[]byte` array key of corresponding length + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + sharedKey :=[]byte{194,164,235,6,138,248,171,239,24,216,11,22,137,199,215,133} + + token,err := jose.Encrypt(payload,jose.A128GCMKW,jose.A128GCM,sharedKey) + + if(err==nil) { + //go use token + fmt.Printf("\nA128GCMKW A128GCM = %v\n",token) + } +} +``` + +#### ECDH-ES and ECDH-ES with AES Key Wrap key management family of algorithms +ECDH-ES and ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW key management requires `*ecdsa.PublicKey` elliptic curve key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*ecdsa.PublicKey` instance from PEM encoded PKCS1 X509 certificate or PKIX data: `ecc.ReadPublic([]byte)` or directly from `X,Y` parameters: `ecc.NewPublic(x,y []byte)`under `jose2go/keys/ecc` package: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello":"world"}` + + publicKey:=ecc.NewPublic([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}) + + token,err := jose.Encrypt(payload, jose.ECDH_ES, jose.A128CBC_HS256, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\ntoken = %v\n",token) + } +} +``` + +#### PBES2 using HMAC SHA with AES Key Wrap key management family of algorithms +PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW key management requires `string` passphrase from which actual key will be derived + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + passphrase := `top secret` + + token,err := jose.Encrypt(payload,jose.PBES2_HS256_A128KW,jose.A256GCM,passphrase) + + if(err==nil) { + //go use token + fmt.Printf("\nPBES2_HS256_A128KW A256GCM = %v\n",token) + } +} +``` + +#### DIR direct pre-shared symmetric key management +Direct key management with pre-shared symmetric keys expecting `[]byte` array key of corresponding length: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + sharedKey :=[]byte{194,164,235,6,138,248,171,239,24,216,11,22,137,199,215,133} + + token,err := jose.Encrypt(payload,jose.DIR,jose.A128GCM,sharedKey) + + if(err==nil) { + //go use token + fmt.Printf("\nDIR A128GCM = %v\n",token) + } +} +``` + +### Creating compressed & encrypted tokens +#### DEFLATE compression +**jose2go** supports optional DEFLATE compression of payload before encrypting, can be used with all supported encryption and key management algorithms: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + sharedKey := []byte{194, 164, 235, 6, 138, 248, 171, 239, 24, 216, 11, 22, 137, 199, 215, 133} + + token, err := jose.Encrypt(payload, jose.DIR, jose.A128GCM, sharedKey, jose.Zip(jose.DEF)) + + if err == nil { + //go use token + fmt.Printf("\nDIR A128GCM DEFLATED= %v\n", token) + } +} +``` + +### Verifying, Decoding and Decompressing tokens +Decoding json web tokens is fully symmetric to creating signed or encrypted tokens (with respect to public/private cryptography), decompressing deflated payloads is handled automatically: + +As of v1.2 decode method defined as `jose.Decode() payload string, headers map[string]interface{}, err error` and returns both payload as unprocessed string and headers as map. + +**HS256, HS384, HS512** signatures, **A128KW, A192KW, A256KW**,**A128GCMKW, A192GCMKW, A256GCMKW** and **DIR** key management algorithm expecting `[]byte` array key: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJIUzI1NiIsImN0eSI6InRleHRcL3BsYWluIn0.eyJoZWxsbyI6ICJ3b3JsZCJ9.chIoYWrQMA8XL5nFz6oLDJyvgHk2KA4BrFGrKymjC8E" + + sharedKey :=[]byte{97,48,97,50,97,98,100,56,45,54,49,54,50,45,52,49,99,51,45,56,51,100,54,45,49,99,102,53,53,57,98,52,54,97,102,99} + + payload, headers, err := jose.Decode(token,sharedKey) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**RS256, RS384, RS512**,**PS256, PS384, PS512** signatures expecting `*rsa.PublicKey` public key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*rsa.PublicKey` instance from PEM encoded PKCS1 X509 certificate or PKIX data: `Rsa.ReadPublic([]byte)` under `jose2go/keys/rsa` package: + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJSUzI1NiIsImN0eSI6InRleHRcL3BsYWluIn0.eyJoZWxsbyI6ICJ3b3JsZCJ9.NL_dfVpZkhNn4bZpCyMq5TmnXbT4yiyecuB6Kax_lV8Yq2dG8wLfea-T4UKnrjLOwxlbwLwuKzffWcnWv3LVAWfeBxhGTa0c4_0TX_wzLnsgLuU6s9M2GBkAIuSMHY6UTFumJlEeRBeiqZNrlqvmAzQ9ppJHfWWkW4stcgLCLMAZbTqvRSppC1SMxnvPXnZSWn_Fk_q3oGKWw6Nf0-j-aOhK0S0Lcr0PV69ZE4xBYM9PUS1MpMe2zF5J3Tqlc1VBcJ94fjDj1F7y8twmMT3H1PI9RozO-21R0SiXZ_a93fxhE_l_dj5drgOek7jUN9uBDjkXUwJPAyp9YPehrjyLdw" + + keyBytes, err := ioutil.ReadFile("public.key") + + if(err!=nil) { + panic("invalid key file") + } + + publicKey, e:=Rsa.ReadPublic(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + payload, headers, err := jose.Decode(token, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**RSA-OAEP-256**, **RSA-OAEP** and **RSA1_5** key management algorithms expecting `*rsa.PrivateKey` private key of corresponding length: + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMjU2R0NNIn0.ixD3WVOkvaxeLKi0kyVqTzM6W2EW25SHHYCAr9473Xq528xSK0AVux6kUtv7QMkQKgkMvO8X4VdvonyGkDZTK2jgYUiI06dz7I1sjWJIbyNVrANbBsmBiwikwB-9DLEaKuM85Lwu6gnzbOF6B9R0428ckxmITCPDrzMaXwYZHh46FiSg9djChUTex0pHGhNDiEIgaINpsmqsOFX1L2Y7KM2ZR7wtpR3kidMV3JlxHdKheiPKnDx_eNcdoE-eogPbRGFdkhEE8Dyass1ZSxt4fP27NwsIer5pc0b922_3XWdi1r1TL_fLvGktHLvt6HK6IruXFHpU4x5Z2gTXWxEIog.zzTNmovBowdX2_hi.QSPSgXn0w25ugvzmu2TnhePn.0I3B9BE064HFNP2E0I7M9g" + + keyBytes, err := ioutil.ReadFile("private.key") + + if(err!=nil) { + panic("invalid key file") + } + + privateKey, e:=Rsa.ReadPrivate(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + payload, headers, err := jose.Decode(token, privateKey) + + if(err==nil) { + //go use payload + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW** key management algorithms expects `string` passpharase as a key + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := `eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJjIjo4MTkyLCJwMnMiOiJlZWpFZTF0YmJVbU5XV2s2In0.J2HTgltxH3p7A2zDgQWpZPgA2CHTSnDmMhlZWeSOMoZ0YvhphCeg-w.FzYG5AOptknu7jsG.L8jAxfxZhDNIqb0T96YWoznQ.yNeOfQWUbm8KuDGZ_5lL_g` + + passphrase := `top secret` + + payload, headers, err := jose.Decode(token,passphrase) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**ES256, ES284, ES512** signatures expecting `*ecdsa.PublicKey` public elliptic curve key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*ecdsa.PublicKey` instance from PEM encoded PKCS1 X509 certificate or PKIX data: `ecc.ReadPublic([]byte)` or directly from `X,Y` parameters: `ecc.NewPublic(x,y []byte)`under `jose2go/keys/ecc` package: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJFUzI1NiIsImN0eSI6InRleHRcL3BsYWluIn0.eyJoZWxsbyI6ICJ3b3JsZCJ9.EVnmDMlz-oi05AQzts-R3aqWvaBlwVZddWkmaaHyMx5Phb2NSLgyI0kccpgjjAyo1S5KCB3LIMPfmxCX_obMKA" + + publicKey:=ecc.NewPublic([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}) + + payload, headers, err := jose.Decode(token, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**ECDH-ES** and **ECDH-ES+A128KW**, **ECDH-ES+A192KW**, **ECDH-ES+A256KW** key management expecting `*ecdsa.PrivateKey` private elliptic curve key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*ecdsa.PrivateKey` instance from PEM encoded PKCS1 or PKCS8 data: `ecc.ReadPrivate([]byte)` or directly from `X,Y,D` parameters: `ecc.NewPrivate(x,y,d []byte)` under `jose2go/keys/ecc` package: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTEyOENCQy1IUzI1NiIsImVwayI6eyJrdHkiOiJFQyIsIngiOiItVk1LTG5NeW9IVHRGUlpGNnFXNndkRm5BN21KQkdiNzk4V3FVMFV3QVhZIiwieSI6ImhQQWNReTgzVS01Qjl1U21xbnNXcFZzbHVoZGJSZE1nbnZ0cGdmNVhXTjgiLCJjcnYiOiJQLTI1NiJ9fQ..UA3N2j-TbYKKD361AxlXUA.XxFur_nY1GauVp5W_KO2DEHfof5s7kUwvOgghiNNNmnB4Vxj5j8VRS8vMOb51nYy2wqmBb2gBf1IHDcKZdACkCOMqMIcpBvhyqbuKiZPLHiilwSgVV6ubIV88X0vK0C8ZPe5lEyRudbgFjdlTnf8TmsvuAsdtPn9dXwDjUR23bD2ocp8UGAV0lKqKzpAw528vTfD0gwMG8gt_op8yZAxqqLLljMuZdTnjofAfsW2Rq3Z6GyLUlxR51DAUlQKi6UpsKMJoXTrm1Jw8sXBHpsRqA.UHCYOtnqk4SfhAknCnymaQ" + + privateKey:=ecc.NewPrivate([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}, + []byte{ 42, 148, 231, 48, 225, 196, 166, 201, 23, 190, 229, 199, 20, 39, 226, 70, 209, 148, 29, 70, 125, 14, 174, 66, 9, 198, 80, 251, 95, 107, 98, 206 }) + + payload, headers, err := jose.Decode(token, privateKey) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +### Adding extra headers +It's possible to pass additional headers while encoding token. **jose2go** provides convenience configuration helpers: `Header(name string, value interface{})` and `Headers(headers map[string]interface{})` that can be passed to `Sign(..)` and `Encrypt(..)` calls. + +Note: **jose2go** do not allow to override `alg`, `enc` and `zip` headers. + +Example of signing with extra headers: +```Go + token, err := jose.Sign(payload, jose.ES256, key, + jose.Header("keyid", "111-222-333"), + jose.Header("trans-id", "aaa-bbb")) +``` + +Encryption with extra headers: +```Go +token, err := jose.Encrypt(payload, jose.DIR, jose.A128GCM, sharedKey, + jose.Headers(map[string]interface{}{"keyid": "111-22-33", "cty": "text/plain"})) +``` + +### Two phase validation +In some cases validation (decoding) key can be unknown prior to examining token content. For instance one can use different keys per token issuer or rely on headers information to determine which key to use, do logging or other things. + +**jose2go** allows to pass `func(headers map[string]interface{}, payload string) key interface{}` callback instead of key to `jose.Decode(..)`. Callback will be executed prior to decoding and integrity validation and will recieve parsed headers and payload as is (for encrypted tokens it will be cipher text). Callback should return key to be used for actual decoding process or `error` if decoding should be stopped, given error object will be returned from `jose.Decode(..)` call. + +Example of decoding token with callback: + +```Go +package main + +import ( + "crypto/rsa" + "fmt" + "github.com/dvsekhvalnov/jose2go" + "github.com/dvsekhvalnov/jose2go/keys/rsa" + "io/ioutil" + "errors" +) + +func main() { + + token := "eyJhbGciOiJSUzI1NiIsImN0eSI6InRleHRcL3BsYWluIn0.eyJoZWxsbyI6ICJ3b3JsZCJ9.NL_dfVpZkhNn4bZpCyMq5TmnXbT4yiyecuB6Kax_lV8Yq2dG8wLfea-T4UKnrjLOwxlbwLwuKzffWcnWv3LVAWfeBxhGTa0c4_0TX_wzLnsgLuU6s9M2GBkAIuSMHY6UTFumJlEeRBeiqZNrlqvmAzQ9ppJHfWWkW4stcgLCLMAZbTqvRSppC1SMxnvPXnZSWn_Fk_q3oGKWw6Nf0-j-aOhK0S0Lcr0PV69ZE4xBYM9PUS1MpMe2zF5J3Tqlc1VBcJ94fjDj1F7y8twmMT3H1PI9RozO-21R0SiXZ_a93fxhE_l_dj5drgOek7jUN9uBDjkXUwJPAyp9YPehrjyLdw" + + payload, _, err := jose.Decode(token, + func(headers map[string]interface{}, payload string) interface{} { + //log something + fmt.Printf("\nHeaders before decoding: %v\n", headers) + fmt.Printf("\nPayload before decoding: %v\n", payload) + + //lookup key based on keyid header as en example + //or lookup based on something from payload, e.g. 'iss' claim for instance + key := FindKey(headers['keyid']) + + if(key==nil) { + return errors.New("Key not found") + } + + return key; + }) + + if err == nil { + //go use token + fmt.Printf("\ndecoded payload = %v\n", payload) + } +} +``` + +### Working with binary payload +In addition to work with string payloads (typical use-case) `jose2go` supports +encoding and decoding of raw binary data. `jose.DecodeBytes`, `jose.SignBytes` +and `jose.EncryptBytes` functions provides similar interface but accepting +`[]byte` payloads. + +Examples: + +```Go +package main + +import ( + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := `eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJjIjo4MTkyLCJwMnMiOiJlZWpFZTF0YmJVbU5XV2s2In0.J2HTgltxH3p7A2zDgQWpZPgA2CHTSnDmMhlZWeSOMoZ0YvhphCeg-w.FzYG5AOptknu7jsG.L8jAxfxZhDNIqb0T96YWoznQ.yNeOfQWUbm8KuDGZ_5lL_g` + + passphrase := `top secret` + + payload, headers, err := jose.DecodeBytes(token,passphrase) + + if(err==nil) { + //go use token + //payload = []byte{....} + } +} +``` + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := []byte {0x01, 0x02, 0x03, 0x04} + + keyBytes,err := ioutil.ReadFile("private.key") + + if(err!=nil) { + panic("invalid key file") + } + + privateKey,e:=Rsa.ReadPrivate(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + token,err := jose.SignBytes(payload,jose.RS256, privateKey) + + if(err==nil) { + //go use token + fmt.Printf("\nRS256 = %v\n",token) + } +} +``` + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := []byte {0x01, 0x02, 0x03, 0x04} + + keyBytes,err := ioutil.ReadFile("public.key") + + if(err!=nil) { + panic("invalid key file") + } + + publicKey,e:=Rsa.ReadPublic(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + token,err := jose.EncryptBytes(payload, jose.RSA_OAEP, jose.A256GCM, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\ntoken = %v\n",token) + } +} +``` +### Dealing with keys +**jose2go** provides several helper methods to simplify loading & importing of elliptic and rsa keys. Import `jose2go/keys/rsa` or `jose2go/keys/ecc` respectively: + +#### RSA keys +1. `Rsa.ReadPrivate(raw []byte) (key *rsa.PrivateKey,err error)` attempts to parse RSA private key from PKCS1 or PKCS8 format (`BEGIN RSA PRIVATE KEY` and `BEGIN PRIVATE KEY` headers) + +```Go +package main + +import ( + "fmt" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "io/ioutil" +) + +func main() { + + keyBytes, _ := ioutil.ReadFile("private.key") + + privateKey, err:=Rsa.ReadPrivate(keyBytes) + + if(err!=nil) { + panic("invalid key format") + } + + fmt.Printf("privateKey = %v\n",privateKey) +} +``` + +2. `Rsa.ReadPublic(raw []byte) (key *rsa.PublicKey,err error)` attempts to parse RSA public key from PKIX key format or PKCS1 X509 certificate (`BEGIN PUBLIC KEY` and `BEGIN CERTIFICATE` headers) + +```Go +package main + +import ( + "fmt" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "io/ioutil" +) + +func main() { + + keyBytes, _ := ioutil.ReadFile("public.cer") + + publicKey, err:=Rsa.ReadPublic(keyBytes) + + if(err!=nil) { + panic("invalid key format") + } + + fmt.Printf("publicKey = %v\n",publicKey) +} +``` + +#### ECC keys +1. `ecc.ReadPrivate(raw []byte) (key *ecdsa.PrivateKey,err error)` attemps to parse elliptic curve private key from PKCS1 or PKCS8 format (`BEGIN EC PRIVATE KEY` and `BEGIN PRIVATE KEY` headers) + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "io/ioutil" +) + +func main() { + + keyBytes, _ := ioutil.ReadFile("ec-private.pem") + + ecPrivKey, err:=ecc.ReadPrivate(keyBytes) + + if(err!=nil) { + panic("invalid key format") + } + + fmt.Printf("ecPrivKey = %v\n",ecPrivKey) +} +``` + +2. `ecc.ReadPublic(raw []byte) (key *ecdsa.PublicKey,err error)` attemps to parse elliptic curve public key from PKCS1 X509 or PKIX format (`BEGIN PUBLIC KEY` and `BEGIN CERTIFICATE` headers) + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "io/ioutil" +) + +func main() { + + keyBytes, _ := ioutil.ReadFile("ec-public.key") + + ecPubKey, err:=ecc.ReadPublic(keyBytes) + + if(err!=nil) { + panic("invalid key format") + } + + fmt.Printf("ecPubKey = %v\n",ecPubKey) +} +``` + +3. `ecc.NewPublic(x,y []byte) (*ecdsa.PublicKey)` constructs elliptic public key from (X,Y) represented as bytes. Supported are NIST curves P-256,P-384 and P-521. Curve detected automatically by input length. + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" +) + +func main() { + + ecPubKey:=ecc.NewPublic([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}) + + fmt.Printf("ecPubKey = %v\n",ecPubKey) +} +``` + +4. `ecc.NewPrivate(x,y,d []byte) (*ecdsa.PrivateKey)` constructs elliptic private key from (X,Y) and D represented as bytes. Supported are NIST curves P-256,P-384 and P-521. Curve detected automatically by input length. + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" +) + +func main() { + + ecPrivKey:=ecc.NewPrivate([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}, + []byte{ 42, 148, 231, 48, 225, 196, 166, 201, 23, 190, 229, 199, 20, 39, 226, 70, 209, 148, 29, 70, 125, 14, 174, 66, 9, 198, 80, 251, 95, 107, 98, 206 }) + + fmt.Printf("ecPrivKey = %v\n",ecPrivKey) +} +``` + +### More examples +Checkout `jose_test.go` for more examples. + +## Changelog +### 1.2 +- interface to access token headers after decoding +- interface to provide extra headers for token encoding +- two-phase validation support + +### 1.1 +- security and bug fixes + +### 1.0 +- initial stable version with full suite JOSE spec support diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes/ecb.go b/vendor/github.com/dvsekhvalnov/jose2go/aes/ecb.go new file mode 100644 index 00000000..ec9af99d --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes/ecb.go @@ -0,0 +1,68 @@ +// Package aes contains provides AES Key Wrap and ECB mode implementations +package aes + +import ( + "crypto/cipher" +) + +type ecb struct { + b cipher.Block +} + +type ecbEncrypter ecb +type ecbDecrypter ecb + +// NewECBEncrypter creates BlockMode for AES encryption in ECB mode +func NewECBEncrypter(b cipher.Block) cipher.BlockMode { + return &ecbEncrypter{b: b} +} + +// NewECBDecrypter creates BlockMode for AES decryption in ECB mode +func NewECBDecrypter(b cipher.Block) cipher.BlockMode { + return &ecbDecrypter{b: b} +} + +func (x *ecbEncrypter) BlockSize() int { return x.b.BlockSize() } +func (x *ecbDecrypter) BlockSize() int { return x.b.BlockSize() } + +func (x *ecbDecrypter) CryptBlocks(dst, src []byte) { + bs := x.BlockSize() + + if len(src)%bs != 0 { + panic("ecbDecrypter.CryptBlocks(): input not full blocks") + } + + if len(dst) < len(src) { + panic("ecbDecrypter.CryptBlocks(): output smaller than input") + } + + if len(src) == 0 { + return + } + + for len(src) > 0 { + x.b.Decrypt(dst, src) + src = src[bs:] + } +} + +func (x *ecbEncrypter) CryptBlocks(dst, src []byte) { + bs := x.BlockSize() + + if len(src)%bs != 0 { + panic("ecbEncrypter.CryptBlocks(): input not full blocks") + } + + if len(dst) < len(src) { + panic("ecbEncrypter.CryptBlocks(): output smaller than input") + } + + if len(src) == 0 { + return + } + + for len(src) > 0 { + x.b.Encrypt(dst, src) + src = src[bs:] + } +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes/key_wrap.go b/vendor/github.com/dvsekhvalnov/jose2go/aes/key_wrap.go new file mode 100644 index 00000000..8e5963ea --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes/key_wrap.go @@ -0,0 +1,113 @@ +package aes + +import ( + "github.com/dvsekhvalnov/jose2go/arrays" + "crypto/cipher" + "crypto/aes" + "crypto/hmac" + "errors" +) + +var defaultIV=[]byte { 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6 } + +// KeyWrap encrypts provided key (CEK) with KEK key using AES Key Wrap (rfc 3394) algorithm +func KeyWrap(cek,kek []byte) ([]byte,error) { + // 1) Initialize variables + a := defaultIV // Set A = IV, an initial value + r := arrays.Slice(cek, 8) // For i = 1 to n + // R[0][i] = P[i] + n := uint64(len(r)) + + // 2) Calculate intermediate values. + var j,i,t uint64 + + for j = 0; j < 6; j++ { // For j = 0 to 5 + for i = 0; i < n; i++ { // For i=1 to n + t = n * j + i + 1; + b,e := aesEnc(kek, arrays.Concat(a, r[i])) // B=AES(K, A | R[i]) + + if e!=nil { return nil, e } + + a = b[:len(b)/2] // A=MSB(64,B) ^ t where t = (n*j)+i + r[i] = b[len(b)/2:] // R[i] = LSB(64, B) + a = arrays.Xor(a, arrays.UInt64ToBytes(t)) + } + } + + // 3) Output the results + c := make([][]byte, n+1, n+1) + c[0] = a; // Set C[0] = A + for i = 1; i <= n; i++ { // For i = 1 to n + c[i] = r[i - 1] // C[i] = R[i] + } + + return arrays.Unwrap(c),nil +} + +// KeyUnwrap decrypts previously encrypted key (CEK) with KEK key using AES Key Wrap (rfc 3394) algorithm +func KeyUnwrap(encryptedCek, kek []byte) ([]byte,error) { + // 1) Initialize variables + c := arrays.Slice(encryptedCek, 8); + a := c[0]; // Set A = C[0] + r := make([][]byte,len(c) - 1); + + for i := 1; i < len(c); i++ { // For i = 1 to n + r[i - 1] = c[i]; // R[i] = C[i] + } + + n := uint64(len(r)) + + // 2) Calculate intermediate values + var t,j uint64 + + for j = 6; j > 0; j-- { // For j = 5 to 0 + for i := n; i > 0; i-- { // For i = n to 1 + t = n * (j-1) + i; + a = arrays.Xor(a, arrays.UInt64ToBytes(t)) + b,e := aesDec(kek, arrays.Concat(a, r[i-1])) // B = AES-1(K, (A ^ t) | R[i]) where t = n*j+i + + if e!=nil { return nil,e } + + a = b[:len(b)/2] // A = MSB(64, B) + r[i-1] = b[len(b)/2:] // R[i] = LSB(64, B) + } + } + + // 3) Output the results + if (!hmac.Equal(defaultIV, a)) { // If A is an appropriate initial value + return nil, errors.New("aes.KeyUnwrap(): integrity check failed.") + } + + // For i = 1 to n + return arrays.Unwrap(r),nil // P[i] = R[i] + +} + +func aesEnc(kek, plainText []byte) (cipherText []byte, err error) { + var block cipher.Block + + if block, err = aes.NewCipher(kek);err!=nil { + return nil,err + } + + cipherText = make([]byte, len(plainText)) + + NewECBEncrypter(block).CryptBlocks(cipherText,plainText) + + return cipherText,nil +} + +func aesDec(kek, cipherText []byte) (plainText []byte,err error) { + + var block cipher.Block + + if block, err = aes.NewCipher(kek);err!=nil { + return nil,err + } + + plainText = make([]byte, len(cipherText)) + + NewECBDecrypter(block).CryptBlocks(plainText,cipherText) + + return plainText,nil +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes_cbc_hmac.go b/vendor/github.com/dvsekhvalnov/jose2go/aes_cbc_hmac.go new file mode 100644 index 00000000..a2217e17 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes_cbc_hmac.go @@ -0,0 +1,112 @@ +package jose + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "errors" + "fmt" + "github.com/dvsekhvalnov/jose2go/arrays" + "github.com/dvsekhvalnov/jose2go/padding" +) + +// AES CBC with HMAC authenticated encryption algorithm implementation +type AesCbcHmac struct { + keySizeBits int +} + +func init() { + RegisterJwe(&AesCbcHmac{keySizeBits: 256}) + RegisterJwe(&AesCbcHmac{keySizeBits: 384}) + RegisterJwe(&AesCbcHmac{keySizeBits: 512}) +} + +func (alg *AesCbcHmac) Name() string { + switch alg.keySizeBits { + case 256: + return A128CBC_HS256 + case 384: + return A192CBC_HS384 + default: + return A256CBC_HS512 + } +} + +func (alg *AesCbcHmac) KeySizeBits() int { + return alg.keySizeBits +} + +func (alg *AesCbcHmac) SetKeySizeBits(bits int) { + alg.keySizeBits = bits +} + +func (alg *AesCbcHmac) Encrypt(aad, plainText, cek []byte) (iv, cipherText, authTag []byte, err error) { + + cekSizeBits := len(cek) << 3 + if cekSizeBits != alg.keySizeBits { + return nil, nil, nil, errors.New(fmt.Sprintf("AesCbcHmac.Encrypt(): expected key of size %v bits, but was given %v bits.", alg.keySizeBits, cekSizeBits)) + } + + hmacKey := cek[0 : len(cek)/2] + aesKey := cek[len(cek)/2:] + + if iv, err = arrays.Random(16); err != nil { + return nil, nil, nil, err + } + + var block cipher.Block + + if block, err = aes.NewCipher(aesKey); err != nil { + return nil, nil, nil, err + } + + padded := padding.AddPkcs7(plainText, 16) + + cipherText = make([]byte, len(padded), cap(padded)) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(cipherText, padded) + + authTag = alg.computeAuthTag(aad, iv, cipherText, hmacKey) + + return iv, cipherText, authTag, nil +} + +func (alg *AesCbcHmac) Decrypt(aad, cek, iv, cipherText, authTag []byte) (plainText []byte, err error) { + + cekSizeBits := len(cek) << 3 + + if cekSizeBits != alg.keySizeBits { + return nil, errors.New(fmt.Sprintf("AesCbcHmac.Decrypt(): expected key of size %v bits, but was given %v bits.", alg.keySizeBits, cekSizeBits)) + } + + hmacKey := cek[0 : len(cek)/2] + aesKey := cek[len(cek)/2:] + + // Check MAC + expectedAuthTag := alg.computeAuthTag(aad, iv, cipherText, hmacKey) + + if !hmac.Equal(expectedAuthTag, authTag) { + return nil, errors.New("AesCbcHmac.Decrypt(): Authentication tag do not match.") + } + + var block cipher.Block + + if block, err = aes.NewCipher(aesKey); err == nil { + mode := cipher.NewCBCDecrypter(block, iv) + + var padded []byte = make([]byte, len(cipherText), cap(cipherText)) + mode.CryptBlocks(padded, cipherText) + + return padding.RemovePkcs7(padded, 16), nil + } + + return nil, err +} + +func (alg *AesCbcHmac) computeAuthTag(aad []byte, iv []byte, cipherText []byte, hmacKey []byte) (signature []byte) { + al := arrays.UInt64ToBytes(uint64(len(aad) << 3)) + hmacInput := arrays.Concat(aad, iv, cipherText, al) + hmac := calculateHmac(alg.keySizeBits, hmacInput, hmacKey) + + return hmac[0 : len(hmac)/2] +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm.go b/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm.go new file mode 100644 index 00000000..57fc8615 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm.go @@ -0,0 +1,98 @@ +package jose + +import ( + "fmt" + "errors" + "crypto/aes" + "crypto/cipher" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +// AES GCM authenticated encryption algorithm implementation +type AesGcm struct{ + keySizeBits int +} + +func init() { + RegisterJwe(&AesGcm{keySizeBits:128}) + RegisterJwe(&AesGcm{keySizeBits:192}) + RegisterJwe(&AesGcm{keySizeBits:256}) +} + +func (alg *AesGcm) Name() string { + switch alg.keySizeBits { + case 128: return A128GCM + case 192: return A192GCM + default: return A256GCM + } +} + +func (alg *AesGcm) KeySizeBits() int { + return alg.keySizeBits +} + +func (alg *AesGcm) Encrypt(aad, plainText, cek []byte) (iv, cipherText, authTag []byte, err error) { + + cekSizeBits := len(cek)<<3 + + if cekSizeBits != alg.keySizeBits { + return nil,nil,nil, errors.New(fmt.Sprintf("AesGcm.Encrypt(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits, cekSizeBits)) + } + + if iv,err = arrays.Random(12);err!=nil { + return nil,nil,nil,err + } + + var block cipher.Block + + if block, err = aes.NewCipher(cek);err!=nil { + return nil,nil,nil,err + } + + var aesgcm cipher.AEAD + + if aesgcm,err = cipher.NewGCM(block);err!=nil { + return nil,nil,nil,err + } + + cipherWithTag := aesgcm.Seal(nil, iv, plainText, aad) + + cipherText=cipherWithTag[:len(cipherWithTag)-aesgcm.Overhead()] + authTag=cipherWithTag[len(cipherWithTag)-aesgcm.Overhead():] + + return iv, cipherText, authTag, nil +} + +func (alg *AesGcm) Decrypt(aad, cek, iv, cipherText, authTag []byte) (plainText []byte, err error) { + + cekSizeBits := len(cek)<<3 + + if cekSizeBits != alg.keySizeBits { + return nil, errors.New(fmt.Sprintf("AesGcm.Decrypt(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits, cekSizeBits)) + } + + var block cipher.Block + + if block, err = aes.NewCipher(cek);err!=nil { + return nil,err + } + + var aesgcm cipher.AEAD + + if aesgcm,err = cipher.NewGCM(block);err!=nil { + return nil,err + } + + cipherWithTag:=append(cipherText,authTag...) + + if nonceSize := len(iv); nonceSize != aesgcm.NonceSize() { + return nil, errors.New(fmt.Sprintf("AesGcm.Decrypt(): expected nonce of size %v bits, but was given %v bits.", aesgcm.NonceSize()<<3, nonceSize<<3)) + } + + if plainText,err = aesgcm.Open(nil, iv, cipherWithTag, aad);err!=nil { + return nil,err + } + + return plainText,nil +} + diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm_kw.go b/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm_kw.go new file mode 100644 index 00000000..e6f56818 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm_kw.go @@ -0,0 +1,128 @@ +package jose + +import ( + "errors" + "fmt" + "github.com/dvsekhvalnov/jose2go/base64url" + "github.com/dvsekhvalnov/jose2go/arrays" + "crypto/aes" + "crypto/cipher" +) + +func init() { + RegisterJwa(&AesGcmKW{ keySizeBits: 128}) + RegisterJwa(&AesGcmKW{ keySizeBits: 192}) + RegisterJwa(&AesGcmKW{ keySizeBits: 256}) +} + +// AES GCM Key Wrap key management algorithm implementation +type AesGcmKW struct { + keySizeBits int +} + +func (alg *AesGcmKW) Name() string { + switch alg.keySizeBits { + case 128: return A128GCMKW + case 192: return A192GCMKW + default: return A256GCMKW + } +} + +func (alg *AesGcmKW) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if kek,ok:=key.([]byte); ok { + + kekSizeBits := len(kek) << 3 + + if kekSizeBits != alg.keySizeBits { + return nil,nil, errors.New(fmt.Sprintf("AesGcmKW.WrapNewKey(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits, kekSizeBits)) + } + + if cek,err = arrays.Random(cekSizeBits>>3);err!=nil { + return nil,nil,err + } + + var iv []byte + + if iv,err = arrays.Random(12);err!=nil { + return nil,nil,err + } + + var block cipher.Block + + if block, err = aes.NewCipher(kek);err!=nil { + return nil,nil,err + } + + var aesgcm cipher.AEAD + + if aesgcm,err = cipher.NewGCM(block);err!=nil { + return nil,nil,err + } + + cipherWithTag := aesgcm.Seal(nil, iv, cek, nil) + + cipherText := cipherWithTag[:len(cipherWithTag)-aesgcm.Overhead()] + authTag := cipherWithTag[len(cipherWithTag)-aesgcm.Overhead():] + + header["iv"]=base64url.Encode(iv) + header["tag"]=base64url.Encode(authTag) + + return cek,cipherText,nil + } + + return nil,nil,errors.New("AesGcmKW.WrapNewKey(): expected key to be '[]byte' array") +} + +func (alg *AesGcmKW) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + if kek,ok:=key.([]byte); ok { + + kekSizeBits := len(kek) << 3 + + if kekSizeBits != alg.keySizeBits { + return nil,errors.New(fmt.Sprintf("AesGcmKW.Unwrap(): expected key of size %v bits, but was given %v bits.", alg.keySizeBits, kekSizeBits)) + } + + var iv,tag string + + if iv,ok = header["iv"].(string);!ok { + return nil,errors.New("AesGcmKW.Unwrap(): expected 'iv' param in JWT header, but was not found.") + } + + if tag,ok = header["tag"].(string);!ok { + return nil,errors.New("AesGcmKW.Unwrap(): expected 'tag' param in JWT header, but was not found.") + } + + var ivBytes,tagBytes []byte + + if ivBytes,err = base64url.Decode(iv);err!=nil { + return nil,err + } + + if tagBytes,err = base64url.Decode(tag);err!=nil { + return nil,err + } + + var block cipher.Block + + if block, err = aes.NewCipher(kek);err!=nil { + return nil,err + } + + var aesgcm cipher.AEAD + + if aesgcm,err = cipher.NewGCM(block);err!=nil { + return nil,err + } + + cipherAndTag:=append(encryptedCek,tagBytes...) + + if cek,err = aesgcm.Open(nil, ivBytes,cipherAndTag , nil);err!=nil { + fmt.Printf("err = %v\n",err) + return nil,err + } + + return cek,nil + } + + return nil,errors.New("AesGcmKW.Unwrap(): expected key to be '[]byte' array") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aeskw.go b/vendor/github.com/dvsekhvalnov/jose2go/aeskw.go new file mode 100644 index 00000000..c5b3e4f6 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aeskw.go @@ -0,0 +1,64 @@ +package jose + +import ( + "errors" + "fmt" + "github.com/dvsekhvalnov/jose2go/aes" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +func init() { + RegisterJwa(&AesKW{ keySizeBits: 128}) + RegisterJwa(&AesKW{ keySizeBits: 192}) + RegisterJwa(&AesKW{ keySizeBits: 256}) +} + +// AES Key Wrap key management algorithm implementation +type AesKW struct { + keySizeBits int +} + +func (alg *AesKW) Name() string { + switch alg.keySizeBits { + case 128: return A128KW + case 192: return A192KW + default: return A256KW + } +} + +func (alg *AesKW) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if kek,ok:=key.([]byte); ok { + + kekSizeBits := len(kek) << 3 + + if kekSizeBits != alg.keySizeBits { + return nil,nil, errors.New(fmt.Sprintf("AesKW.WrapNewKey(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits, kekSizeBits)) + } + + if cek,err = arrays.Random(cekSizeBits>>3);err==nil { + encryptedCek,err=aes.KeyWrap(cek,kek) + return + } + + return nil,nil,err + + } + + return nil,nil,errors.New("AesKW.WrapNewKey(): expected key to be '[]byte' array") +} + +func (alg *AesKW) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + + if kek,ok:=key.([]byte); ok { + + kekSizeBits := len(kek) << 3 + + if kekSizeBits != alg.keySizeBits { + return nil,errors.New(fmt.Sprintf("AesKW.Unwrap(): expected key of size %v bits, but was given %v bits.", alg.keySizeBits, kekSizeBits)) + } + + return aes.KeyUnwrap(encryptedCek, kek) + } + + return nil,errors.New("AesKW.Unwrap(): expected key to be '[]byte' array") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/arrays/arrays.go b/vendor/github.com/dvsekhvalnov/jose2go/arrays/arrays.go new file mode 100644 index 00000000..17ff0bd8 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/arrays/arrays.go @@ -0,0 +1,116 @@ +// Package arrays provides various byte array utilities +package arrays + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "fmt" + "github.com/dvsekhvalnov/jose2go/base64url" +) + +// Xor is doing byte by byte exclusive or of 2 byte arrays +func Xor(left, right []byte) []byte { + result := make([]byte, len(left)) + + for i := 0; i < len(left); i++ { + result[i] = left[i] ^ right[i] + } + + return result +} + +// Slice is splitting input byte array into slice of subarrays. Each of count length. +func Slice(arr []byte, count int) [][]byte { + + sliceCount := len(arr) / count + result := make([][]byte, sliceCount) + + for i := 0; i < sliceCount; i++ { + start := i * count + end := i*count + count + + result[i] = arr[start:end] + } + + return result +} + +// Random generates byte array with random data of byteCount length +func Random(byteCount int) ([]byte, error) { + data := make([]byte, byteCount) + + if _, err := rand.Read(data); err != nil { + return nil, err + } + + return data, nil +} + +// Concat combine several arrays into single one, resulting slice = A1 | A2 | A3 | ... | An +func Concat(arrays ...[]byte) []byte { + var result []byte = arrays[0] + + for _, arr := range arrays[1:] { + result = append(result, arr...) + } + + return result +} + +// Unwrap same thing as Contact, just different interface, combines several array into single one +func Unwrap(arrays [][]byte) []byte { + var result []byte = arrays[0] + + for _, arr := range arrays[1:] { + result = append(result, arr...) + } + + return result +} + +// UInt64ToBytes unwrap uint64 value to byte array of length 8 using big endian +func UInt64ToBytes(value uint64) []byte { + result := make([]byte, 8) + binary.BigEndian.PutUint64(result, value) + + return result +} + +// UInt32ToBytes unwrap uint32 value to byte array of length 4 using big endian +func UInt32ToBytes(value uint32) []byte { + result := make([]byte, 4) + binary.BigEndian.PutUint32(result, value) + + return result +} + +// Dump produces printable debug representation of byte array as string +func Dump(arr []byte) string { + var buf bytes.Buffer + + buf.WriteString("(") + buf.WriteString(fmt.Sprintf("%v", len(arr))) + buf.WriteString(" bytes)[") + + for idx, b := range arr { + buf.WriteString(fmt.Sprintf("%v", b)) + if idx != len(arr)-1 { + buf.WriteString(", ") + } + } + + buf.WriteString("], Hex: [") + + for idx, b := range arr { + buf.WriteString(fmt.Sprintf("%X", b)) + if idx != len(arr)-1 { + buf.WriteString(" ") + } + } + + buf.WriteString("], Base64Url:") + buf.WriteString(base64url.Encode(arr)) + + return buf.String() +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/base64url/base64url.go b/vendor/github.com/dvsekhvalnov/jose2go/base64url/base64url.go new file mode 100644 index 00000000..7229a85e --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/base64url/base64url.go @@ -0,0 +1,31 @@ +// package base64url provides base64url encoding/decoding support +package base64url + +import ( + "strings" + "encoding/base64" +) + +// Decode decodes base64url string to byte array +func Decode(data string) ([]byte,error) { + data = strings.Replace(data, "-", "+", -1) // 62nd char of encoding + data = strings.Replace(data, "_", "/", -1) // 63rd char of encoding + + switch(len(data) % 4) { // Pad with trailing '='s + case 0: // no padding + case 2: data+="==" // 2 pad chars + case 3: data+="=" // 1 pad char + } + + return base64.StdEncoding.DecodeString(data) +} + +// Encode encodes given byte array to base64url string +func Encode(data []byte) string { + result := base64.StdEncoding.EncodeToString(data) + result = strings.Replace(result, "+", "-", -1) // 62nd char of encoding + result = strings.Replace(result, "/", "_", -1) // 63rd char of encoding + result = strings.Replace(result, "=", "", -1) // Remove any trailing '='s + + return result +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/compact/compact.go b/vendor/github.com/dvsekhvalnov/jose2go/compact/compact.go new file mode 100644 index 00000000..1e9dd2f5 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/compact/compact.go @@ -0,0 +1,33 @@ +// package compact provides function to work with json compact serialization format +package compact + +import ( + "github.com/dvsekhvalnov/jose2go/base64url" + "strings" +) + +// Parse splitting & decoding compact serialized json web token, returns slice of byte arrays, each representing part of token +func Parse(token string) (result [][]byte, e error) { + parts := strings.Split(token, ".") + + result = make([][]byte, len(parts)) + + for i, part := range parts { + if result[i], e = base64url.Decode(part); e != nil { + return nil, e + } + } + + return result, nil +} + +// Serialize converts given parts into compact serialization format +func Serialize(parts ...[]byte) string { + result := make([]string, len(parts)) + + for i, part := range parts { + result[i] = base64url.Encode(part) + } + + return strings.Join(result, ".") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/deflate.go b/vendor/github.com/dvsekhvalnov/jose2go/deflate.go new file mode 100644 index 00000000..c788f5bd --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/deflate.go @@ -0,0 +1,39 @@ +package jose + +import ( + "bytes" + "compress/flate" + "io/ioutil" +) + +func init() { + RegisterJwc(new(Deflate)) +} + +// Deflate compression algorithm implementation +type Deflate struct {} + +func (alg *Deflate) Name() string { + return DEF +} + +func (alg *Deflate) Compress(plainText []byte) []byte { + var buf bytes.Buffer + deflate,_ := flate.NewWriter(&buf, 8) //level=DEFLATED + + deflate.Write(plainText) + deflate.Close() + + return buf.Bytes() +} + +func (alg *Deflate) Decompress(compressedText []byte) []byte { + + enflated,_ := ioutil.ReadAll( + flate.NewReader( + bytes.NewReader(compressedText))) + + return enflated +} + + diff --git a/vendor/github.com/dvsekhvalnov/jose2go/direct.go b/vendor/github.com/dvsekhvalnov/jose2go/direct.go new file mode 100644 index 00000000..51f0a2f0 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/direct.go @@ -0,0 +1,39 @@ +package jose + +import ( + "errors" +) + +func init() { + RegisterJwa(new(Direct)) +} + +// Direct (pre-shared) key management algorithm implementation +type Direct struct{ +} + +func (alg *Direct) Name() string { + return DIR +} + +func (alg *Direct) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + + if cek,ok:=key.([]byte); ok { + return cek,[]byte{},nil + } + + return nil,nil,errors.New("Direct.WrapNewKey(): expected key to be '[]byte' array") +} + +func (alg *Direct) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + + if(len(encryptedCek)!=0) { + return nil, errors.New("Direct.Unwrap(): expected empty encrypted CEK") + } + + if cek,ok:=key.([]byte); ok { + return cek,nil + } + + return nil,errors.New("Direct.Unwrap(): expected key to be '[]byte' array") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/ecdh.go b/vendor/github.com/dvsekhvalnov/jose2go/ecdh.go new file mode 100644 index 00000000..c3512fb3 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/ecdh.go @@ -0,0 +1,157 @@ +package jose + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "errors" + "fmt" + "github.com/dvsekhvalnov/jose2go/arrays" + "github.com/dvsekhvalnov/jose2go/base64url" + "github.com/dvsekhvalnov/jose2go/kdf" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go/padding" + "math/big" +) + +func init() { + RegisterJwa(&Ecdh{directAgreement: true}) +} + +// Elliptic curve Diffie–Hellman key management (key agreement) algorithm implementation +type Ecdh struct { + directAgreement bool +} + +func (alg *Ecdh) Name() string { + return ECDH_ES +} + +func (alg *Ecdh) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + + if pubKey, ok := key.(*ecdsa.PublicKey); ok { + + if _, ok := header[alg.idHeader()].(string); !ok { + return nil, nil, errors.New(fmt.Sprintf("Ecdh.WrapNewKey(): expected '%v' param in JWT header, but was not found.", alg.idHeader())) + } + + var d []byte + var x, y *big.Int + + if d, x, y, err = elliptic.GenerateKey(pubKey.Curve, rand.Reader); err != nil { + return nil, nil, err + } + + ephemeral := ecc.NewPrivate(x.Bytes(), y.Bytes(), d) + + xBytes := padding.Align(x.Bytes(), pubKey.Curve.Params().BitSize) + yBytes := padding.Align(y.Bytes(), pubKey.Curve.Params().BitSize) + + epk := map[string]string{ + "kty": "EC", + "x": base64url.Encode(xBytes), + "y": base64url.Encode(yBytes), + "crv": name(pubKey.Curve), + } + + header["epk"] = epk + + return alg.deriveKey(pubKey, ephemeral, cekSizeBits, header), nil, nil + } + + return nil, nil, errors.New("Ecdh.WrapNewKey(): expected key to be '*ecdsa.PublicKey'") +} + +func (alg *Ecdh) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + + if privKey, ok := key.(*ecdsa.PrivateKey); ok { + + var epk map[string]interface{} + + if epk, ok = header["epk"].(map[string]interface{}); !ok { + return nil, errors.New("Ecdh.Unwrap(): expected 'epk' param in JWT header, but was not found.") + } + + if _, ok := header[alg.idHeader()].(string); !ok { + return nil, errors.New(fmt.Sprintf("Ecdh.Unwrap(): expected '%v' param in JWT header, but was not found.", alg.idHeader())) + } + + var x, y, crv string + var xBytes, yBytes []byte + + if x, ok = epk["x"].(string); !ok { + return nil, errors.New("Ecdh.Unwrap(): expects 'epk' key to contain 'x','y' and 'crv' fields, but 'x' was not found.") + } + + if y, ok = epk["y"].(string); !ok { + return nil, errors.New("Ecdh.Unwrap(): expects 'epk' key to contain 'x','y' and 'crv' fields, but 'y' was not found.") + } + + if crv, ok = epk["crv"].(string); !ok { + return nil, errors.New("Ecdh.Unwrap(): expects 'epk' key to contain 'x','y' and 'crv' fields, but 'crv' was not found.") + } + + if crv != "P-256" && crv != "P-384" && crv != "P-521" { + return nil, errors.New(fmt.Sprintf("Ecdh.Unwrap(): unknown or unsupported curve %v", crv)) + } + + if xBytes, err = base64url.Decode(x); err != nil { + return nil, err + } + if yBytes, err = base64url.Decode(y); err != nil { + return nil, err + } + + pubKey := ecc.NewPublic(xBytes, yBytes) + + if !privKey.Curve.IsOnCurve(pubKey.X, pubKey.Y) { + return nil, errors.New(fmt.Sprintf("Ephemeral public key received in header is invalid for reciever's private key.")) + } + + return alg.deriveKey(pubKey, privKey, cekSizeBits, header), nil + } + + return nil, errors.New("Ecdh.Unwrap(): expected key to be '*ecdsa.PrivateKey'") +} + +func (alg *Ecdh) deriveKey(pubKey *ecdsa.PublicKey, privKey *ecdsa.PrivateKey, keySizeBits int, header map[string]interface{}) []byte { + + var enc, apv, apu []byte + var err error + + enc = []byte(header[alg.idHeader()].(string)) + + if a, ok := header["apv"].(string); !ok { + if apv, err = base64url.Decode(a); err != nil { + apv = nil + } + } + + if a, ok := header["apu"].(string); !ok { + if apu, err = base64url.Decode(a); err != nil { + apu = nil + } + } + + z, _ := pubKey.Curve.ScalarMult(pubKey.X, pubKey.Y, privKey.D.Bytes()) + zBytes := padding.Align(z.Bytes(), privKey.Curve.Params().BitSize) + + return kdf.DeriveConcatKDF(keySizeBits, zBytes, prependDatalen(enc), prependDatalen(apu), prependDatalen(apv), arrays.UInt32ToBytes(uint32(keySizeBits)), nil, sha256.New()) +} + +func (alg *Ecdh) idHeader() string { + if alg.directAgreement { + return "enc" + } + + return "alg" +} + +func name(curve elliptic.Curve) string { + return fmt.Sprintf("P-%v", curve.Params().BitSize) +} + +func prependDatalen(bytes []byte) []byte { + return arrays.Concat(arrays.UInt32ToBytes(uint32(len(bytes))), bytes) +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/ecdh_aeskw.go b/vendor/github.com/dvsekhvalnov/jose2go/ecdh_aeskw.go new file mode 100644 index 00000000..bff34809 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/ecdh_aeskw.go @@ -0,0 +1,42 @@ +package jose + +func init() { + RegisterJwa(&EcdhAesKW{ keySizeBits: 128, aesKW: &AesKW{ keySizeBits: 128}, ecdh: &Ecdh{directAgreement:false}}) + RegisterJwa(&EcdhAesKW{ keySizeBits: 192, aesKW: &AesKW{ keySizeBits: 192}, ecdh: &Ecdh{directAgreement:false}}) + RegisterJwa(&EcdhAesKW{ keySizeBits: 256, aesKW: &AesKW{ keySizeBits: 256}, ecdh: &Ecdh{directAgreement:false}}) +} + +// Elliptic curve Diffie–Hellman with AES Key Wrap key management algorithm implementation +type EcdhAesKW struct{ + keySizeBits int + aesKW JwaAlgorithm + ecdh JwaAlgorithm +} + +func (alg *EcdhAesKW) Name() string { + switch alg.keySizeBits { + case 128: return ECDH_ES_A128KW + case 192: return ECDH_ES_A192KW + default: return ECDH_ES_A256KW + } +} + +func (alg *EcdhAesKW) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + var kek []byte + + if kek,_,err=alg.ecdh.WrapNewKey(alg.keySizeBits, key, header);err!=nil { + return nil,nil,err + } + + return alg.aesKW.WrapNewKey(cekSizeBits,kek,header) +} + +func (alg *EcdhAesKW) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + var kek []byte + + if kek,err=alg.ecdh.Unwrap(nil, key, alg.keySizeBits, header);err!=nil { + return nil,err + } + + return alg.aesKW.Unwrap(encryptedCek,kek,cekSizeBits,header) +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/ecdsa_using_sha.go b/vendor/github.com/dvsekhvalnov/jose2go/ecdsa_using_sha.go new file mode 100644 index 00000000..23ac6ee3 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/ecdsa_using_sha.go @@ -0,0 +1,76 @@ +package jose + +import ( + "crypto/rand" + "math/big" + "crypto/ecdsa" + "errors" + "github.com/dvsekhvalnov/jose2go/arrays" + "github.com/dvsekhvalnov/jose2go/padding" + "fmt" +) + +func init() { + RegisterJws(&EcdsaUsingSha{keySizeBits: 256, hashSizeBits: 256}) + RegisterJws(&EcdsaUsingSha{keySizeBits: 384, hashSizeBits: 384}) + RegisterJws(&EcdsaUsingSha{keySizeBits: 521, hashSizeBits: 512}) +} + +// ECDSA signing algorithm implementation +type EcdsaUsingSha struct{ + keySizeBits int + hashSizeBits int +} + +func (alg *EcdsaUsingSha) Name() string { + switch alg.keySizeBits { + case 256: return ES256 + case 384: return ES384 + default: return ES512 + } +} + +func (alg *EcdsaUsingSha) Verify(securedInput, signature []byte, key interface{}) error { + + if pubKey,ok:=key.(*ecdsa.PublicKey);ok { + + if sizeBits:=pubKey.Curve.Params().BitSize;sizeBits!=alg.keySizeBits { + return errors.New(fmt.Sprintf("EcdsaUsingSha.Verify(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits,sizeBits)) + } + + r:=new(big.Int).SetBytes(signature[:len(signature)/2]) + s:=new(big.Int).SetBytes(signature[len(signature)/2:]) + + if ok:=ecdsa.Verify(pubKey, sha(alg.hashSizeBits, securedInput), r,s); ok { + return nil + } + + return errors.New("EcdsaUsingSha.Verify(): Signature is not valid.") + } + + return errors.New("EcdsaUsingSha.Verify(): expects key to be '*ecdsa.PublicKey'") +} + +func (alg *EcdsaUsingSha) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + + if privKey,ok := key.(*ecdsa.PrivateKey);ok { + + if sizeBits:=privKey.Curve.Params().BitSize;sizeBits!=alg.keySizeBits { + return nil,errors.New(fmt.Sprintf("EcdsaUsingSha.Sign(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits,sizeBits)) + } + + var r,s *big.Int + + if r,s,err = ecdsa.Sign(rand.Reader, privKey, sha(alg.hashSizeBits, securedInput));err==nil { + + rBytes:=padding.Align(r.Bytes(), alg.keySizeBits) + sBytes:=padding.Align(s.Bytes(), alg.keySizeBits) + + return arrays.Concat(rBytes,sBytes),nil + } + + return nil, err + } + + return nil,errors.New("EcdsaUsingSha.Sign(): expects key to be '*ecdsa.PrivateKey'") +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/hmac.go b/vendor/github.com/dvsekhvalnov/jose2go/hmac.go new file mode 100644 index 00000000..d3726b77 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/hmac.go @@ -0,0 +1,13 @@ +package jose + +import ( + "crypto/hmac" + "hash" +) + +func calculateHmac(keySizeBits int, securedInput []byte, key []byte) []byte { + hasher := hmac.New(func() hash.Hash { return hashAlg(keySizeBits)}, key) + hasher.Write(securedInput) + + return hasher.Sum(nil) +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/hmac_using_sha.go b/vendor/github.com/dvsekhvalnov/jose2go/hmac_using_sha.go new file mode 100644 index 00000000..e0b664f6 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/hmac_using_sha.go @@ -0,0 +1,46 @@ +package jose + +import ( + "crypto/hmac" + "errors" +) + +func init() { + RegisterJws(&HmacUsingSha{keySizeBits: 256}) + RegisterJws(&HmacUsingSha{keySizeBits: 384}) + RegisterJws(&HmacUsingSha{keySizeBits: 512}) +} + +// HMAC with SHA signing algorithm implementation +type HmacUsingSha struct{ + keySizeBits int +} + +func (alg *HmacUsingSha) Name() string { + switch alg.keySizeBits { + case 256: return HS256 + case 384: return HS384 + default: return HS512 + } +} + +func (alg *HmacUsingSha) Verify(securedInput, signature []byte, key interface{}) error { + + actualSig,_ := alg.Sign(securedInput, key) + + if !hmac.Equal(signature, actualSig) { + return errors.New("HmacUsingSha.Verify(): Signature is invalid") + } + + return nil +} + +func (alg *HmacUsingSha) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + //TODO: assert min key size + + if pubKey,ok:=key.([]byte); ok { + return calculateHmac(alg.keySizeBits, securedInput, pubKey),nil + } + + return nil,errors.New("HmacUsingSha.Sign(): expects key to be '[]byte' array") +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/jose.go b/vendor/github.com/dvsekhvalnov/jose2go/jose.go new file mode 100644 index 00000000..1f1c19ed --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/jose.go @@ -0,0 +1,424 @@ +//Package jose provides high level functions for producing (signing, encrypting and +// compressing) or consuming (decoding) Json Web Tokens using Java Object Signing and Encryption spec +package jose + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/dvsekhvalnov/jose2go/compact" +) + +const ( + NONE = "none" //plaintext (unprotected) without signature / encryption + + HS256 = "HS256" //HMAC using SHA-256 hash + HS384 = "HS384" //HMAC using SHA-384 hash + HS512 = "HS512" //HMAC using SHA-512 hash + RS256 = "RS256" //RSASSA-PKCS-v1_5 using SHA-256 hash + RS384 = "RS384" //RSASSA-PKCS-v1_5 using SHA-384 hash + RS512 = "RS512" //RSASSA-PKCS-v1_5 using SHA-512 hash + PS256 = "PS256" //RSASSA-PSS using SHA-256 hash + PS384 = "PS384" //RSASSA-PSS using SHA-384 hash + PS512 = "PS512" //RSASSA-PSS using SHA-512 hash + ES256 = "ES256" //ECDSA using P-256 curve and SHA-256 hash + ES384 = "ES384" //ECDSA using P-384 curve and SHA-384 hash + ES512 = "ES512" //ECDSA using P-521 curve and SHA-512 hash + + A128CBC_HS256 = "A128CBC-HS256" //AES in CBC mode with PKCS #5 (NIST.800-38A) padding with HMAC using 256 bit key + A192CBC_HS384 = "A192CBC-HS384" //AES in CBC mode with PKCS #5 (NIST.800-38A) padding with HMAC using 384 bit key + A256CBC_HS512 = "A256CBC-HS512" //AES in CBC mode with PKCS #5 (NIST.800-38A) padding with HMAC using 512 bit key + A128GCM = "A128GCM" //AES in GCM mode with 128 bit key + A192GCM = "A192GCM" //AES in GCM mode with 192 bit key + A256GCM = "A256GCM" //AES in GCM mode with 256 bit key + + DIR = "dir" //Direct use of pre-shared symmetric key + RSA1_5 = "RSA1_5" //RSAES with PKCS #1 v1.5 padding, RFC 3447 + RSA_OAEP = "RSA-OAEP" //RSAES using Optimal Assymetric Encryption Padding, RFC 3447 + RSA_OAEP_256 = "RSA-OAEP-256" //RSAES using Optimal Assymetric Encryption Padding with SHA-256, RFC 3447 + A128KW = "A128KW" //AES Key Wrap Algorithm using 128 bit keys, RFC 3394 + A192KW = "A192KW" //AES Key Wrap Algorithm using 192 bit keys, RFC 3394 + A256KW = "A256KW" //AES Key Wrap Algorithm using 256 bit keys, RFC 3394 + A128GCMKW = "A128GCMKW" //AES GCM Key Wrap Algorithm using 128 bit keys + A192GCMKW = "A192GCMKW" //AES GCM Key Wrap Algorithm using 192 bit keys + A256GCMKW = "A256GCMKW" //AES GCM Key Wrap Algorithm using 256 bit keys + PBES2_HS256_A128KW = "PBES2-HS256+A128KW" //Password Based Encryption using PBES2 schemes with HMAC-SHA and AES Key Wrap using 128 bit key + PBES2_HS384_A192KW = "PBES2-HS384+A192KW" //Password Based Encryption using PBES2 schemes with HMAC-SHA and AES Key Wrap using 192 bit key + PBES2_HS512_A256KW = "PBES2-HS512+A256KW" //Password Based Encryption using PBES2 schemes with HMAC-SHA and AES Key Wrap using 256 bit key + ECDH_ES = "ECDH-ES" //Elliptic Curve Diffie Hellman key agreement + ECDH_ES_A128KW = "ECDH-ES+A128KW" //Elliptic Curve Diffie Hellman key agreement with AES Key Wrap using 128 bit key + ECDH_ES_A192KW = "ECDH-ES+A192KW" //Elliptic Curve Diffie Hellman key agreement with AES Key Wrap using 192 bit key + ECDH_ES_A256KW = "ECDH-ES+A256KW" //Elliptic Curve Diffie Hellman key agreement with AES Key Wrap using 256 bit key + + DEF = "DEF" //DEFLATE compression, RFC 1951 +) + +var jwsHashers = map[string]JwsAlgorithm{} +var jweEncryptors = map[string]JweEncryption{} +var jwaAlgorithms = map[string]JwaAlgorithm{} +var jwcCompressors = map[string]JwcAlgorithm{} + +// RegisterJwe register new encryption algorithm +func RegisterJwe(alg JweEncryption) { + jweEncryptors[alg.Name()] = alg +} + +// RegisterJwa register new key management algorithm +func RegisterJwa(alg JwaAlgorithm) { + jwaAlgorithms[alg.Name()] = alg +} + +// RegisterJws register new signing algorithm +func RegisterJws(alg JwsAlgorithm) { + jwsHashers[alg.Name()] = alg +} + +// RegisterJwc register new compression algorithm +func RegisterJwc(alg JwcAlgorithm) { + jwcCompressors[alg.Name()] = alg +} + +// JweEncryption is a contract for implementing encryption algorithm +type JweEncryption interface { + Encrypt(aad, plainText, cek []byte) (iv, cipherText, authTag []byte, err error) + Decrypt(aad, cek, iv, cipherText, authTag []byte) (plainText []byte, err error) + KeySizeBits() int + Name() string +} + +// JwaAlgorithm is a contract for implementing key management algorithm +type JwaAlgorithm interface { + WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) + Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) + Name() string +} + +// JwsAlgorithm is a contract for implementing signing algorithm +type JwsAlgorithm interface { + Verify(securedInput, signature []byte, key interface{}) error + Sign(securedInput []byte, key interface{}) (signature []byte, err error) + Name() string +} + +// JwcAlgorithm is a contract for implementing compression algorithm +type JwcAlgorithm interface { + Compress(plainText []byte) []byte + Decompress(compressedText []byte) []byte + Name() string +} + +func Zip(alg string) func(cfg *JoseConfig) { + return func(cfg *JoseConfig) { + cfg.CompressionAlg = alg + } +} + +func Header(name string, value interface{}) func(cfg *JoseConfig) { + return func(cfg *JoseConfig) { + cfg.Headers[name] = value + } +} + +func Headers(headers map[string]interface{}) func(cfg *JoseConfig) { + return func(cfg *JoseConfig) { + for k, v := range headers { + cfg.Headers[k] = v + } + } +} + +type JoseConfig struct { + CompressionAlg string + Headers map[string]interface{} +} + +// Sign produces signed JWT token given arbitrary string payload, signature algorithm to use (see constants for list of supported algs), signing key and extra options (see option functions) +// Signing key is of different type for different signing alg, see specific +// signing alg implementation documentation. +// +// It returns 3 parts signed JWT token as string and not nil error if something went wrong. +func Sign(payload string, signingAlg string, key interface{}, options ...func(*JoseConfig)) (token string, err error) { + return SignBytes([]byte(payload), signingAlg, key, options...) +} + +// Sign produces signed JWT token given arbitrary binary payload, signature algorithm to use (see constants for list of supported algs), signing key and extra options (see option functions) +// Signing key is of different type for different signing alg, see specific +// signing alg implementation documentation. +// +// It returns 3 parts signed JWT token as string and not nil error if something went wrong. +func SignBytes(payload []byte, signingAlg string, key interface{}, options ...func(*JoseConfig)) (token string, err error) { + if signer, ok := jwsHashers[signingAlg]; ok { + + cfg := &JoseConfig{CompressionAlg: "", Headers: make(map[string]interface{})} + + //apply extra options + for _, option := range options { + option(cfg) + } + + //make sure defaults and requires are managed by us + cfg.Headers["alg"] = signingAlg + + paloadBytes := payload + var header []byte + var signature []byte + + if header, err = json.Marshal(cfg.Headers); err == nil { + securedInput := []byte(compact.Serialize(header, paloadBytes)) + + if signature, err = signer.Sign(securedInput, key); err == nil { + return compact.Serialize(header, paloadBytes, signature), nil + } + } + + return "", err + } + + return "", errors.New(fmt.Sprintf("jwt.Sign(): unknown algorithm: '%v'", signingAlg)) +} + +// Encrypt produces encrypted JWT token given arbitrary string payload, key management and encryption algorithms to use (see constants for list of supported algs) and management key. +// Management key is of different type for different key management alg, see specific +// key management alg implementation documentation. +// +// It returns 5 parts encrypted JWT token as string and not nil error if something went wrong. +func Encrypt(payload string, alg string, enc string, key interface{}, options ...func(*JoseConfig)) (token string, err error) { + return EncryptBytes([]byte(payload), alg, enc, key, options...) +} + +// Encrypt produces encrypted JWT token given arbitrary binary payload, key management and encryption algorithms to use (see constants for list of supported algs) and management key. +// Management key is of different type for different key management alg, see specific +// key management alg implementation documentation. +// +// It returns 5 parts encrypted JWT token as string and not nil error if something went wrong. +func EncryptBytes(payload []byte, alg string, enc string, key interface{}, options ...func(*JoseConfig)) (token string, err error) { + + cfg := &JoseConfig{CompressionAlg: "", Headers: make(map[string]interface{})} + + //apply extra options + for _, option := range options { + option(cfg) + } + + //make sure required headers are managed by us + cfg.Headers["alg"] = alg + cfg.Headers["enc"] = enc + + byteContent := payload + + if cfg.CompressionAlg != "" { + if zipAlg, ok := jwcCompressors[cfg.CompressionAlg]; ok { + byteContent = zipAlg.Compress([]byte(payload)) + cfg.Headers["zip"] = cfg.CompressionAlg + } else { + return "", errors.New(fmt.Sprintf("jwt.Compress(): Unknown compression method '%v'", cfg.CompressionAlg)) + } + + } else { + delete(cfg.Headers, "zip") //we not allow to manage 'zip' header manually for encryption + } + + return encrypt(byteContent, cfg.Headers, key) +} + +// This method is DEPRICATED and subject to be removed in next version. +// Use Encrypt(..) with Zip option instead. +// +// Compress produces encrypted & comressed JWT token given arbitrary payload, key management , encryption and compression algorithms to use (see constants for list of supported algs) and management key. +// Management key is of different type for different key management alg, see specific +// key management alg implementation documentation. +// +// It returns 5 parts encrypted & compressed JWT token as string and not nil error if something went wrong. +func Compress(payload string, alg string, enc string, zip string, key interface{}) (token string, err error) { + + if zipAlg, ok := jwcCompressors[zip]; ok { + compressed := zipAlg.Compress([]byte(payload)) + + jwtHeader := map[string]interface{}{ + "enc": enc, + "alg": alg, + "zip": zip, + } + + return encrypt(compressed, jwtHeader, key) + } + + return "", errors.New(fmt.Sprintf("jwt.Compress(): Unknown compression method '%v'", zip)) +} + +// Decode verifies, decrypts and decompresses given JWT token using management key. +// Management key is of different type for different key management or signing algorithms, see specific alg implementation documentation. +// +// Returns decoded payload as a string, headers and not nil error if something went wrong. +func Decode(token string, key interface{}) (string, map[string]interface{}, error) { + + payload, headers, err := DecodeBytes(token, key) + + if err != nil { + return "", nil, err + } + + return string(payload), headers, nil +} + +// Decode verifies, decrypts and decompresses given JWT token using management key. +// Management key is of different type for different key management or signing algorithms, see specific alg implementation documentation. +// +// Returns decoded payload as a raw bytes, headers and not nil error if something went wrong. +func DecodeBytes(token string, key interface{}) ([]byte, map[string]interface{}, error) { + parts, err := compact.Parse(token) + + if err != nil { + return nil, nil, err + } + + if len(parts) == 3 { + return verify(parts, key) + } + + if len(parts) == 5 { + return decrypt(parts, key) + } + + return nil, nil, errors.New(fmt.Sprintf("jwt.DecodeBytes() expects token of 3 or 5 parts, but was given: %v parts", len(parts))) +} + +func encrypt(payload []byte, jwtHeader map[string]interface{}, key interface{}) (token string, err error) { + var ok bool + var keyMgmtAlg JwaAlgorithm + var encAlg JweEncryption + + alg := jwtHeader["alg"].(string) + enc := jwtHeader["enc"].(string) + + if keyMgmtAlg, ok = jwaAlgorithms[alg]; !ok { + return "", errors.New(fmt.Sprintf("jwt.encrypt(): Unknown key management algorithm '%v'", alg)) + } + + if encAlg, ok = jweEncryptors[enc]; !ok { + return "", errors.New(fmt.Sprintf("jwt.encrypt(): Unknown encryption algorithm '%v'", enc)) + } + + var cek, encryptedCek, header, iv, cipherText, authTag []byte + + if cek, encryptedCek, err = keyMgmtAlg.WrapNewKey(encAlg.KeySizeBits(), key, jwtHeader); err != nil { + return "", err + } + + if header, err = json.Marshal(jwtHeader); err != nil { + return "", err + } + + if iv, cipherText, authTag, err = encAlg.Encrypt([]byte(compact.Serialize(header)), payload, cek); err != nil { + return "", err + } + + return compact.Serialize(header, encryptedCek, iv, cipherText, authTag), nil +} + +func verify(parts [][]byte, key interface{}) (plainText []byte, headers map[string]interface{}, err error) { + + header, payload, signature := parts[0], parts[1], parts[2] + + secured := []byte(compact.Serialize(header, payload)) + + var jwtHeader map[string]interface{} + + if err = json.Unmarshal(header, &jwtHeader); err != nil { + return nil, nil, err + } + + if alg, ok := jwtHeader["alg"].(string); ok { + if verifier, ok := jwsHashers[alg]; ok { + if key, err = retrieveActualKey(jwtHeader, string(payload), key); err != nil { + return nil, nil, err + } + + if err = verifier.Verify(secured, signature, key); err == nil { + return payload, jwtHeader, nil + } + + return nil, nil, err + } + + return nil, nil, errors.New(fmt.Sprintf("jwt.Decode(): Unknown algorithm: '%v'", alg)) + } + + return nil, nil, errors.New(fmt.Sprint("jwt.Decode(): required 'alg' header is missing or of invalid type")) +} + +func decrypt(parts [][]byte, key interface{}) (plainText []byte, headers map[string]interface{}, err error) { + + header, encryptedCek, iv, cipherText, authTag := parts[0], parts[1], parts[2], parts[3], parts[4] + + var jwtHeader map[string]interface{} + + if e := json.Unmarshal(header, &jwtHeader); e != nil { + return nil, nil, e + } + + var keyMgmtAlg JwaAlgorithm + var encAlg JweEncryption + var zipAlg JwcAlgorithm + var cek, plainBytes []byte + var ok bool + var alg, enc string + + if alg, ok = jwtHeader["alg"].(string); !ok { + return nil, nil, errors.New(fmt.Sprint("jwt.Decode(): required 'alg' header is missing or of invalid type")) + } + + if enc, ok = jwtHeader["enc"].(string); !ok { + return nil, nil, errors.New(fmt.Sprint("jwt.Decode(): required 'enc' header is missing or of invalid type")) + } + + aad := []byte(compact.Serialize(header)) + + if keyMgmtAlg, ok = jwaAlgorithms[alg]; ok { + if encAlg, ok = jweEncryptors[enc]; ok { + + if key, err = retrieveActualKey(jwtHeader, string(cipherText), key); err != nil { + return nil, nil, err + } + + if cek, err = keyMgmtAlg.Unwrap(encryptedCek, key, encAlg.KeySizeBits(), jwtHeader); err == nil { + if plainBytes, err = encAlg.Decrypt(aad, cek, iv, cipherText, authTag); err == nil { + + if zip, compressed := jwtHeader["zip"].(string); compressed { + + if zipAlg, ok = jwcCompressors[zip]; !ok { + return nil, nil, errors.New(fmt.Sprintf("jwt.decrypt(): Unknown compression algorithm '%v'", zip)) + } + + plainBytes = zipAlg.Decompress(plainBytes) + } + + return plainBytes, jwtHeader, nil + } + + return nil, nil, err + } + + return nil, nil, err + } + + return nil, nil, errors.New(fmt.Sprintf("jwt.decrypt(): Unknown encryption algorithm '%v'", enc)) + } + + return nil, nil, errors.New(fmt.Sprintf("jwt.decrypt(): Unknown key management algorithm '%v'", alg)) +} + +func retrieveActualKey(headers map[string]interface{}, payload string, key interface{}) (interface{}, error) { + if keyCallback, ok := key.(func(headers map[string]interface{}, payload string) interface{}); ok { + result := keyCallback(headers, payload) + + if err, ok := result.(error); ok { + return nil, err + } + + return result, nil + } + + return key, nil +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/kdf/nist_sp800_56a.go b/vendor/github.com/dvsekhvalnov/jose2go/kdf/nist_sp800_56a.go new file mode 100644 index 00000000..dd76790a --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/kdf/nist_sp800_56a.go @@ -0,0 +1,43 @@ +package kdf + +import ( + "hash" + "math" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +const ( + MaxInt = int(^uint(0)>>1); +) + +// DeriveConcatKDF implements NIST SP 800-56A Concatenation Key Derivation Function. Derives +// key material of keydatalen bits size given Z (sharedSecret), OtherInfo (AlgorithmID | +// PartyUInfo | PartyVInfo | SuppPubInfo | SuppPrivInfo) and hash function +func DeriveConcatKDF(keydatalen int, sharedSecret, algId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo []byte, h hash.Hash) []byte { + + otherInfo := arrays.Concat(algId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo) + + keyLenBytes := keydatalen >> 3 + + reps := int(math.Ceil(float64(keyLenBytes) / float64(h.Size()))) + + if reps > MaxInt { + panic("kdf.DeriveConcatKDF: too much iterations (more than 2^32-1).") + } + + dk:=make([]byte, 0, keyLenBytes) + + for counter := 1;counter <= reps;counter++ { + h.Reset() + + counterBytes:=arrays.UInt32ToBytes(uint32(counter)) + + h.Write(counterBytes) + h.Write(sharedSecret) + h.Write(otherInfo) + + dk = h.Sum(dk) + } + + return dk[:keyLenBytes] +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/kdf/pbkdf2.go b/vendor/github.com/dvsekhvalnov/jose2go/kdf/pbkdf2.go new file mode 100644 index 00000000..aec58e76 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/kdf/pbkdf2.go @@ -0,0 +1,63 @@ +// package kdf contains implementations of various key derivation functions +package kdf + +import ( + "crypto/hmac" + "fmt" + "hash" + "math" + + "github.com/dvsekhvalnov/jose2go/arrays" +) + +// DerivePBKDF2 implements Password Based Key Derivation Function 2, RFC 2898. Derives key of keyBitLength size, given password, salt, iteration count and hash function +func DerivePBKDF2(password, salt []byte, iterationCount, keyBitLength int, h func() hash.Hash) []byte { + + prf := hmac.New(h, password) + hLen := prf.Size() + dkLen := keyBitLength >> 3 //size of derived key in bytes + + l := int(math.Ceil(float64(dkLen) / float64(hLen))) // l = CEIL (dkLen / hLen) + r := dkLen - (l-1)*hLen + + // 1. If dkLen > (2^32 - 1) * hLen, output "derived key too long" and stop. + if dkLen > MaxInt { + panic(fmt.Sprintf("kdf.DerivePBKDF2: expects derived key size to be not more that (2^32-1) bits, but was requested %v bits.", keyBitLength)) + } + + dk := make([]byte, 0, dkLen) + + for i := 0; i < l; i++ { + + t := f(salt, iterationCount, i+1, prf) // T_l = F (P, S, c, l) + + if i == (l - 1) { + t = t[:r] + } // truncate last block to r bits + + dk = append(dk, t...) // DK = T_1 || T_2 || ... || T_l<0..r-1> + } + + return dk +} + +func f(salt []byte, iterationCount, blockIndex int, prf hash.Hash) []byte { + + prf.Reset() + prf.Write(salt) + prf.Write(arrays.UInt32ToBytes(uint32(blockIndex))) + + u := prf.Sum(nil) // U_1 = PRF (P, S || INT (i)) + + result := u + + for i := 2; i <= iterationCount; i++ { + prf.Reset() + prf.Write(u) + + u = prf.Sum(nil) // U_c = PRF (P, U_{c-1}) . + result = arrays.Xor(result, u) // U_1 \xor U_2 \xor ... \xor U_c + } + + return result +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_cert.pem b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_cert.pem new file mode 100644 index 00000000..384cb469 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_cert.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICJjCCAc6gAwIBAgIJAOCtH/xv+cfpMAkGByqGSM49BAEwRTELMAkGA1UEBhMC +QVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdp +dHMgUHR5IEx0ZDAeFw0xNDA4MTIxMTU5MTVaFw0xODA1MDgxMTU5MTVaMEUxCzAJ +BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASqGi1p +Eudyh+Nw//zeAGIYSQG/H/q/x6Xz2TIg3GN4hbD5BS1f4/vEitpui+TpmtJyggzo +x5D55N9kjq0X/hyyo4GnMIGkMB0GA1UdDgQWBBQvGSEecx9JdDECRIorVpeWy7oA +ujB1BgNVHSMEbjBsgBQvGSEecx9JdDECRIorVpeWy7oAuqFJpEcwRTELMAkGA1UE +BhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdp +ZGdpdHMgUHR5IEx0ZIIJAOCtH/xv+cfpMAwGA1UdEwQFMAMBAf8wCQYHKoZIzj0E +AQNHADBEAiAUQheZrGjbsy6PfpWGZEhTFzqvBVXtbqtz+6aTkOCrCQIgLfvw9C+0 +SDn/abV4NtgYOM0OLkoNRTCIzzguHxhhaJ4= +-----END CERTIFICATE----- diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.key b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.key new file mode 100644 index 00000000..399b84b2 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIA/32XgQpS/tKRhw8jSdr8ivBmMyD/vbG5KT3s3XPArcoAoGCCqGSM49 +AwEHoUQDQgAEqhotaRLncofjcP/83gBiGEkBvx/6v8el89kyINxjeIWw+QUtX+P7 +xIrabovk6ZrScoIM6MeQ+eTfZI6tF/4csg== +-----END EC PRIVATE KEY----- diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.pem b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.pem new file mode 100644 index 00000000..5247be2b --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD/fZeBClL+0pGHDy +NJ2vyK8GYzIP+9sbkpPezdc8CtyhRANCAASqGi1pEudyh+Nw//zeAGIYSQG/H/q/ +x6Xz2TIg3GN4hbD5BS1f4/vEitpui+TpmtJyggzox5D55N9kjq0X/hyy +-----END PRIVATE KEY----- diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_public.key b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_public.key new file mode 100644 index 00000000..bc2345d6 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_public.key @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqhotaRLncofjcP/83gBiGEkBvx/6 +v8el89kyINxjeIWw+QUtX+P7xIrabovk6ZrScoIM6MeQ+eTfZI6tF/4csg== +-----END PUBLIC KEY----- diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ecc.go b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ecc.go new file mode 100644 index 00000000..b1debe48 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ecc.go @@ -0,0 +1,88 @@ +//package ecc provides helpers for creating elliptic curve leys +package ecc + +import ( + "math/big" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/x509" + "encoding/pem" + "errors" +) + +// ReadPublic loads ecdsa.PublicKey from given PKCS1 X509 or PKIX blobs +func ReadPublic(raw []byte) (key *ecdsa.PublicKey,err error) { + var encoded *pem.Block + + if encoded, _ = pem.Decode(raw); encoded == nil { + return nil, errors.New("Ecc.ReadPublic(): Key must be PEM encoded PKCS1 X509 certificate or PKIX EC public key") + } + + var parsedKey interface{} + var cert *x509.Certificate + + if parsedKey, err = x509.ParsePKIXPublicKey(encoded.Bytes); err != nil { + if cert,err = x509.ParseCertificate(encoded.Bytes);err!=nil { + return nil, err + } + + parsedKey=cert.PublicKey + } + + var ok bool + + if key, ok = parsedKey.(*ecdsa.PublicKey); !ok { + return nil, errors.New("Ecc.ReadPublic(): Key is not a valid *ecdsa.PublicKey") + } + + return key, nil +} + +// ReadPrivate loads ecdsa.PrivateKey from given PKCS1 or PKCS8 blobs +func ReadPrivate(raw []byte) (key *ecdsa.PrivateKey,err error) { + var encoded *pem.Block + + if encoded, _ = pem.Decode(raw); encoded == nil { + return nil, errors.New("Ecc.ReadPrivate(): Key must be PEM encoded PKCS1 or PKCS8 EC private key") + } + + var parsedKey interface{} + + if parsedKey,err=x509.ParseECPrivateKey(encoded.Bytes);err!=nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(encoded.Bytes);err!=nil { + return nil,err + } + } + + var ok bool + + if key,ok=parsedKey.(*ecdsa.PrivateKey);!ok { + return nil, errors.New("Ecc.ReadPrivate(): Key is not valid *ecdsa.PrivateKey") + } + + return key,nil +} + +// NewPublic constructs ecdsa.PublicKey from given (X,Y) +func NewPublic(x,y []byte) (*ecdsa.PublicKey) { + return &ecdsa.PublicKey{ Curve: curve(len(x)), + X:new(big.Int).SetBytes(x), + Y:new(big.Int).SetBytes(y) } +} + +// NewPrivate constructs ecdsa.PrivateKey from given (X,Y) and D +func NewPrivate(x,y,d []byte) (*ecdsa.PrivateKey) { + return &ecdsa.PrivateKey {D:new(big.Int).SetBytes(d), + PublicKey: ecdsa.PublicKey{ Curve:curve(len(x)), + X:new(big.Int).SetBytes(x), + Y:new(big.Int).SetBytes(y)}} +} + +func curve(size int) (elliptic.Curve) { + switch size { + case 32: return elliptic.P256() + case 48: return elliptic.P384() + case 65,66: return elliptic.P521() //adjust for P-521 curve, which can be 65 or 66 bytes + default: return nil //unsupported curve + } +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/padding/align.go b/vendor/github.com/dvsekhvalnov/jose2go/padding/align.go new file mode 100644 index 00000000..0ef601f9 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/padding/align.go @@ -0,0 +1,23 @@ +// package padding provides various padding algorithms +package padding + +import ( + "bytes" +) + +// Align left pads given byte array with zeros till it have at least bitSize length. +func Align(data []byte, bitSize int) []byte { + + actual:=len(data) + required:=bitSize >> 3 + + if (bitSize % 8) > 0 { + required++ //extra byte if needed + } + + if (actual >= required) { + return data + } + + return append(bytes.Repeat([]byte{0}, required-actual), data...) +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/padding/pkcs7.go b/vendor/github.com/dvsekhvalnov/jose2go/padding/pkcs7.go new file mode 100644 index 00000000..2f64e7e1 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/padding/pkcs7.go @@ -0,0 +1,38 @@ +package padding + +import ( + "bytes" +) + +// AddPkcs7 pads given byte array using pkcs7 padding schema till it has blockSize length in bytes +func AddPkcs7(data []byte, blockSize int) []byte { + + var paddingCount int + + if paddingCount = blockSize - (len(data) % blockSize);paddingCount == 0 { + paddingCount=blockSize + } + + return append(data, bytes.Repeat([]byte{byte(paddingCount)}, paddingCount)...) +} + +// RemovePkcs7 removes pkcs7 padding from previously padded byte array +func RemovePkcs7(padded []byte, blockSize int) []byte { + + dataLen:=len(padded) + paddingCount:=int(padded[dataLen-1]) + + if(paddingCount > blockSize || paddingCount <= 0) { + return padded //data is not padded (or not padded correctly), return as is + } + + padding := padded[dataLen-paddingCount : dataLen-1] + + for _, b := range padding { + if int(b) != paddingCount { + return padded //data is not padded (or not padded correcly), return as is + } + } + + return padded[:len(padded)-paddingCount] //return data - padding +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/pbse2_hmac_aeskw.go b/vendor/github.com/dvsekhvalnov/jose2go/pbse2_hmac_aeskw.go new file mode 100644 index 00000000..baeaf9c7 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/pbse2_hmac_aeskw.go @@ -0,0 +1,103 @@ +package jose + +import ( + "crypto/sha256" + "crypto/sha512" + "errors" + "hash" + + "github.com/dvsekhvalnov/jose2go/arrays" + "github.com/dvsekhvalnov/jose2go/base64url" + "github.com/dvsekhvalnov/jose2go/kdf" +) + +func init() { + RegisterJwa(&Pbse2HmacAesKW{keySizeBits: 128, aesKW: &AesKW{keySizeBits: 128}}) + RegisterJwa(&Pbse2HmacAesKW{keySizeBits: 192, aesKW: &AesKW{keySizeBits: 192}}) + RegisterJwa(&Pbse2HmacAesKW{keySizeBits: 256, aesKW: &AesKW{keySizeBits: 256}}) +} + +// PBSE2 with HMAC key management algorithm implementation +type Pbse2HmacAesKW struct { + keySizeBits int + aesKW JwaAlgorithm +} + +func (alg *Pbse2HmacAesKW) Name() string { + switch alg.keySizeBits { + case 128: + return PBES2_HS256_A128KW + case 192: + return PBES2_HS384_A192KW + default: + return PBES2_HS512_A256KW + } +} + +func (alg *Pbse2HmacAesKW) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if passphrase, ok := key.(string); ok { + + algId := []byte(header["alg"].(string)) + + iterationCount := 8192 + var saltInput []byte + + if saltInput, err = arrays.Random(12); err != nil { + return nil, nil, err + } + + header["p2c"] = iterationCount + header["p2s"] = base64url.Encode(saltInput) + + salt := arrays.Concat(algId, []byte{0}, saltInput) + + kek := kdf.DerivePBKDF2([]byte(passphrase), salt, iterationCount, alg.keySizeBits, alg.prf) + return alg.aesKW.WrapNewKey(cekSizeBits, kek, header) + } + + return nil, nil, errors.New("Pbse2HmacAesKW.WrapNewKey(): expected key to be 'string' array") +} + +func (alg *Pbse2HmacAesKW) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + + if passphrase, ok := key.(string); ok { + + var p2s string + var p2c float64 + + if p2c, ok = header["p2c"].(float64); !ok { + return nil, errors.New("Pbse2HmacAesKW.Unwrap(): expected 'p2c' param in JWT header, but was not found.") + } + + if p2s, ok = header["p2s"].(string); !ok { + return nil, errors.New("Pbse2HmacAesKW.Unwrap(): expected 'p2s' param in JWT header, but was not found.") + } + + var saltInput []byte + + algId := []byte(header["alg"].(string)) + + if saltInput, err = base64url.Decode(p2s); err != nil { + return nil, err + } + + salt := arrays.Concat(algId, []byte{0}, saltInput) + + kek := kdf.DerivePBKDF2([]byte(passphrase), salt, int(p2c), alg.keySizeBits, alg.prf) + + return alg.aesKW.Unwrap(encryptedCek, kek, cekSizeBits, header) + } + + return nil, errors.New("Pbse2HmacAesKW.Unwrap(): expected key to be 'string' array") +} + +func (alg *Pbse2HmacAesKW) prf() hash.Hash { + switch alg.keySizeBits { + case 128: + return sha256.New() + case 192: + return sha512.New384() + default: + return sha512.New() + } +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/plaintext.go b/vendor/github.com/dvsekhvalnov/jose2go/plaintext.go new file mode 100644 index 00000000..761ce5ad --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/plaintext.go @@ -0,0 +1,38 @@ +package jose + +import ( + "errors" +) + +// Plaintext (no signing) signing algorithm implementation +type Plaintext struct{} + +func init() { + RegisterJws(new(Plaintext)) +} + +func (alg *Plaintext) Name() string { + return NONE +} + +func (alg *Plaintext) Verify(securedInput []byte, signature []byte, key interface{}) error { + + if key != nil { + return errors.New("Plaintext.Verify() expects key to be nil") + } + + if len(signature) != 0 { + return errors.New("Plaintext.Verify() expects signature to be empty.") + } + + return nil +} + +func (alg *Plaintext) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + + if key != nil { + return nil, errors.New("Plaintext.Verify() expects key to be nil") + } + + return []byte{}, nil +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/rsa_oaep.go b/vendor/github.com/dvsekhvalnov/jose2go/rsa_oaep.go new file mode 100644 index 00000000..b0d1b52a --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/rsa_oaep.go @@ -0,0 +1,57 @@ +package jose + +import ( + "errors" + "crypto/rsa" + "crypto/rand" + "hash" + "crypto/sha1" + "crypto/sha256" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +// RS-AES using OAEP key management algorithm implementation +func init() { + RegisterJwa(&RsaOaep {shaSizeBits:1}) + RegisterJwa(&RsaOaep {shaSizeBits:256}) +} + +type RsaOaep struct{ + shaSizeBits int + // func shaF() hash.Hash +} + +func (alg *RsaOaep) Name() string { + switch alg.shaSizeBits { + case 1: return RSA_OAEP + default: return RSA_OAEP_256 + } +} + +func (alg *RsaOaep) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if pubKey,ok:=key.(*rsa.PublicKey);ok { + if cek,err = arrays.Random(cekSizeBits>>3);err==nil { + encryptedCek,err=rsa.EncryptOAEP(alg.sha(),rand.Reader,pubKey,cek,nil) + return + } + + return nil,nil,err + } + + return nil,nil,errors.New("RsaOaep.WrapNewKey(): expected key to be '*rsa.PublicKey'") +} + +func (alg *RsaOaep) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + if privKey,ok:=key.(*rsa.PrivateKey);ok { + return rsa.DecryptOAEP(alg.sha(), rand.Reader, privKey, encryptedCek, nil) + } + + return nil,errors.New("RsaOaep.Unwrap(): expected key to be '*rsa.PrivateKey'") +} + +func (alg *RsaOaep) sha() hash.Hash { + switch alg.shaSizeBits { + case 1: return sha1.New() + default: return sha256.New() + } +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/rsa_pkcs1v15.go b/vendor/github.com/dvsekhvalnov/jose2go/rsa_pkcs1v15.go new file mode 100644 index 00000000..10dac066 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/rsa_pkcs1v15.go @@ -0,0 +1,41 @@ +package jose + +import ( + "errors" + "crypto/rsa" + "crypto/rand" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +func init() { + RegisterJwa(new(RsaPkcs1v15)) +} + +// RS-AES using PKCS #1 v1.5 padding key management algorithm implementation +type RsaPkcs1v15 struct{ +} + +func (alg *RsaPkcs1v15) Name() string { + return RSA1_5 +} + +func (alg *RsaPkcs1v15) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if pubKey,ok:=key.(*rsa.PublicKey);ok { + if cek,err = arrays.Random(cekSizeBits>>3);err==nil { + encryptedCek,err=rsa.EncryptPKCS1v15(rand.Reader,pubKey,cek) + return + } + + return nil,nil,err + } + + return nil,nil,errors.New("RsaPkcs1v15.WrapNewKey(): expected key to be '*rsa.PublicKey'") +} + +func (alg *RsaPkcs1v15) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + if privKey,ok:=key.(*rsa.PrivateKey);ok { + return rsa.DecryptPKCS1v15(rand.Reader,privKey,encryptedCek) + } + + return nil,errors.New("RsaPkcs1v15.Unwrap(): expected key to be '*rsa.PrivateKey'") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/rsa_using_sha.go b/vendor/github.com/dvsekhvalnov/jose2go/rsa_using_sha.go new file mode 100644 index 00000000..647a45a4 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/rsa_using_sha.go @@ -0,0 +1,50 @@ +package jose + +import ( + "crypto/rand" + "crypto/rsa" + "errors" +) + +func init() { + RegisterJws(&RsaUsingSha{keySizeBits: 256}) + RegisterJws(&RsaUsingSha{keySizeBits: 384}) + RegisterJws(&RsaUsingSha{keySizeBits: 512}) +} + +// RSA using SHA signature algorithm implementation +type RsaUsingSha struct{ + keySizeBits int +} + +func (alg *RsaUsingSha) Name() string { + switch alg.keySizeBits { + case 256: return RS256 + case 384: return RS384 + default: return RS512 + } +} + +func (alg *RsaUsingSha) Verify(securedInput, signature []byte, key interface{}) error { + + if pubKey,ok:=key.(*rsa.PublicKey);ok { + return rsa.VerifyPKCS1v15(pubKey, hashFunc(alg.keySizeBits), sha(alg.keySizeBits, securedInput), signature) + } + + return errors.New("RsaUsingSha.Verify(): expects key to be '*rsa.PublicKey'") +} + +func (alg *RsaUsingSha) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + + if privKey,ok:=key.(*rsa.PrivateKey);ok { + return rsa.SignPKCS1v15(rand.Reader, privKey, hashFunc(alg.keySizeBits), sha(alg.keySizeBits, securedInput)) + } + + return nil,errors.New("RsaUsingSha.Sign(): expects key to be '*rsa.PrivateKey'") +} + +func sha(keySizeBits int, input []byte) (hash []byte) { + hasher := hashAlg(keySizeBits) + hasher.Write(input) + return hasher.Sum(nil) +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/rsapss_using_sha.go b/vendor/github.com/dvsekhvalnov/jose2go/rsapss_using_sha.go new file mode 100644 index 00000000..fc111db2 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/rsapss_using_sha.go @@ -0,0 +1,43 @@ +package jose + +import ( + "crypto/rand" + "crypto/rsa" + "errors" +) + +func init() { + RegisterJws(&RsaPssUsingSha{keySizeBits: 256, saltSizeBytes: 32}) + RegisterJws(&RsaPssUsingSha{keySizeBits: 384, saltSizeBytes: 48}) + RegisterJws(&RsaPssUsingSha{keySizeBits: 512, saltSizeBytes: 64}) +} + +// RSA with PSS using SHA signing algorithm implementation +type RsaPssUsingSha struct{ + keySizeBits int + saltSizeBytes int +} + +func (alg *RsaPssUsingSha) Name() string { + switch alg.keySizeBits { + case 256: return PS256 + case 384: return PS384 + default: return PS512 + } +} + +func (alg *RsaPssUsingSha) Verify(securedInput, signature []byte, key interface{}) error { + if pubKey,ok:=key.(*rsa.PublicKey);ok { + return rsa.VerifyPSS(pubKey, hashFunc(alg.keySizeBits), sha(alg.keySizeBits, securedInput), signature, &rsa.PSSOptions{SaltLength:alg.saltSizeBytes}) + } + + return errors.New("RsaPssUsingSha.Verify(): expects key to be '*rsa.PublicKey'") +} + +func (alg *RsaPssUsingSha) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + if privKey,ok:=key.(*rsa.PrivateKey);ok { + return rsa.SignPSS(rand.Reader, privKey, hashFunc(alg.keySizeBits), sha(alg.keySizeBits, securedInput), &rsa.PSSOptions{SaltLength:alg.saltSizeBytes}) + } + + return nil,errors.New("RsaPssUsingSha.Sign(): expects key to be '*rsa.PrivateKey'") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/sha.go b/vendor/github.com/dvsekhvalnov/jose2go/sha.go new file mode 100644 index 00000000..13b7b17e --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/sha.go @@ -0,0 +1,24 @@ +package jose + +import ( + "hash" + "crypto" + "crypto/sha256" + "crypto/sha512" +) + +func hashFunc(keySizeBits int) crypto.Hash { + switch keySizeBits { + case 256: return crypto.SHA256 + case 384: return crypto.SHA384 + default: return crypto.SHA512 + } +} + +func hashAlg(keySizeBits int) hash.Hash { + switch keySizeBits { + case 256: return sha256.New() + case 384: return sha512.New384() + default: return sha512.New() + } +} \ No newline at end of file diff --git a/vendor/github.com/erikgeiser/coninput/.gitignore b/vendor/github.com/erikgeiser/coninput/.gitignore new file mode 100644 index 00000000..66fd13c9 --- /dev/null +++ b/vendor/github.com/erikgeiser/coninput/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/vendor/github.com/erikgeiser/coninput/.golangci.yml b/vendor/github.com/erikgeiser/coninput/.golangci.yml new file mode 100644 index 00000000..d3ed7103 --- /dev/null +++ b/vendor/github.com/erikgeiser/coninput/.golangci.yml @@ -0,0 +1,24 @@ +linters: + enable-all: true + disable: + - golint + - interfacer + - scopelint + - maligned + - rowserrcheck + - funlen + - depguard + - goerr113 + - exhaustivestruct + - testpackage + - gochecknoglobals + - wrapcheck + - forbidigo + - ifshort + - cyclop + - gomoddirectives +linters-settings: + exhaustive: + default-signifies-exhaustive: true +issues: + exclude-use-default: false diff --git a/vendor/github.com/erikgeiser/coninput/LICENSE b/vendor/github.com/erikgeiser/coninput/LICENSE new file mode 100644 index 00000000..83c24408 --- /dev/null +++ b/vendor/github.com/erikgeiser/coninput/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Erik G. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/erikgeiser/coninput/README.md b/vendor/github.com/erikgeiser/coninput/README.md new file mode 100644 index 00000000..22619d13 --- /dev/null +++ b/vendor/github.com/erikgeiser/coninput/README.md @@ -0,0 +1,2 @@ +# coninput +Go library for input handling using Windows Console API diff --git a/vendor/github.com/erikgeiser/coninput/keycodes.go b/vendor/github.com/erikgeiser/coninput/keycodes.go new file mode 100644 index 00000000..902ee1b7 --- /dev/null +++ b/vendor/github.com/erikgeiser/coninput/keycodes.go @@ -0,0 +1,205 @@ +package coninput + +// VirtualKeyCode holds a virtual key code (see +// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes). +type VirtualKeyCode uint16 + +const ( + VK_LBUTTON VirtualKeyCode = 0x01 + VK_RBUTTON VirtualKeyCode = 0x02 + VK_CANCEL VirtualKeyCode = 0x03 + VK_MBUTTON VirtualKeyCode = 0x04 + VK_XBUTTON1 VirtualKeyCode = 0x05 + VK_XBUTTON2 VirtualKeyCode = 0x06 + VK_BACK VirtualKeyCode = 0x08 + VK_TAB VirtualKeyCode = 0x09 + VK_CLEAR VirtualKeyCode = 0x0C + VK_RETURN VirtualKeyCode = 0x0D + VK_SHIFT VirtualKeyCode = 0x10 + VK_CONTROL VirtualKeyCode = 0x11 + VK_MENU VirtualKeyCode = 0x12 + VK_PAUSE VirtualKeyCode = 0x13 + VK_CAPITAL VirtualKeyCode = 0x14 + VK_KANA VirtualKeyCode = 0x15 + VK_HANGEUL VirtualKeyCode = 0x15 + VK_HANGUL VirtualKeyCode = 0x15 + VK_IME_ON VirtualKeyCode = 0x16 + VK_JUNJA VirtualKeyCode = 0x17 + VK_FINAL VirtualKeyCode = 0x18 + VK_HANJA VirtualKeyCode = 0x19 + VK_KANJI VirtualKeyCode = 0x19 + VK_IME_OFF VirtualKeyCode = 0x1A + VK_ESCAPE VirtualKeyCode = 0x1B + VK_CONVERT VirtualKeyCode = 0x1C + VK_NONCONVERT VirtualKeyCode = 0x1D + VK_ACCEPT VirtualKeyCode = 0x1E + VK_MODECHANGE VirtualKeyCode = 0x1F + VK_SPACE VirtualKeyCode = 0x20 + VK_PRIOR VirtualKeyCode = 0x21 + VK_NEXT VirtualKeyCode = 0x22 + VK_END VirtualKeyCode = 0x23 + VK_HOME VirtualKeyCode = 0x24 + VK_LEFT VirtualKeyCode = 0x25 + VK_UP VirtualKeyCode = 0x26 + VK_RIGHT VirtualKeyCode = 0x27 + VK_DOWN VirtualKeyCode = 0x28 + VK_SELECT VirtualKeyCode = 0x29 + VK_PRINT VirtualKeyCode = 0x2A + VK_EXECUTE VirtualKeyCode = 0x2B + VK_SNAPSHOT VirtualKeyCode = 0x2C + VK_INSERT VirtualKeyCode = 0x2D + VK_DELETE VirtualKeyCode = 0x2E + VK_HELP VirtualKeyCode = 0x2F + VK_0 VirtualKeyCode = 0x30 + VK_1 VirtualKeyCode = 0x31 + VK_2 VirtualKeyCode = 0x32 + VK_3 VirtualKeyCode = 0x33 + VK_4 VirtualKeyCode = 0x34 + VK_5 VirtualKeyCode = 0x35 + VK_6 VirtualKeyCode = 0x36 + VK_7 VirtualKeyCode = 0x37 + VK_8 VirtualKeyCode = 0x38 + VK_9 VirtualKeyCode = 0x39 + VK_A VirtualKeyCode = 0x41 + VK_B VirtualKeyCode = 0x42 + VK_C VirtualKeyCode = 0x43 + VK_D VirtualKeyCode = 0x44 + VK_E VirtualKeyCode = 0x45 + VK_F VirtualKeyCode = 0x46 + VK_G VirtualKeyCode = 0x47 + VK_H VirtualKeyCode = 0x48 + VK_I VirtualKeyCode = 0x49 + VK_J VirtualKeyCode = 0x4A + VK_K VirtualKeyCode = 0x4B + VK_L VirtualKeyCode = 0x4C + VK_M VirtualKeyCode = 0x4D + VK_N VirtualKeyCode = 0x4E + VK_O VirtualKeyCode = 0x4F + VK_P VirtualKeyCode = 0x50 + VK_Q VirtualKeyCode = 0x51 + VK_R VirtualKeyCode = 0x52 + VK_S VirtualKeyCode = 0x53 + VK_T VirtualKeyCode = 0x54 + VK_U VirtualKeyCode = 0x55 + VK_V VirtualKeyCode = 0x56 + VK_W VirtualKeyCode = 0x57 + VK_X VirtualKeyCode = 0x58 + VK_Y VirtualKeyCode = 0x59 + VK_Z VirtualKeyCode = 0x5A + VK_LWIN VirtualKeyCode = 0x5B + VK_RWIN VirtualKeyCode = 0x5C + VK_APPS VirtualKeyCode = 0x5D + VK_SLEEP VirtualKeyCode = 0x5F + VK_NUMPAD0 VirtualKeyCode = 0x60 + VK_NUMPAD1 VirtualKeyCode = 0x61 + VK_NUMPAD2 VirtualKeyCode = 0x62 + VK_NUMPAD3 VirtualKeyCode = 0x63 + VK_NUMPAD4 VirtualKeyCode = 0x64 + VK_NUMPAD5 VirtualKeyCode = 0x65 + VK_NUMPAD6 VirtualKeyCode = 0x66 + VK_NUMPAD7 VirtualKeyCode = 0x67 + VK_NUMPAD8 VirtualKeyCode = 0x68 + VK_NUMPAD9 VirtualKeyCode = 0x69 + VK_MULTIPLY VirtualKeyCode = 0x6A + VK_ADD VirtualKeyCode = 0x6B + VK_SEPARATOR VirtualKeyCode = 0x6C + VK_SUBTRACT VirtualKeyCode = 0x6D + VK_DECIMAL VirtualKeyCode = 0x6E + VK_DIVIDE VirtualKeyCode = 0x6F + VK_F1 VirtualKeyCode = 0x70 + VK_F2 VirtualKeyCode = 0x71 + VK_F3 VirtualKeyCode = 0x72 + VK_F4 VirtualKeyCode = 0x73 + VK_F5 VirtualKeyCode = 0x74 + VK_F6 VirtualKeyCode = 0x75 + VK_F7 VirtualKeyCode = 0x76 + VK_F8 VirtualKeyCode = 0x77 + VK_F9 VirtualKeyCode = 0x78 + VK_F10 VirtualKeyCode = 0x79 + VK_F11 VirtualKeyCode = 0x7A + VK_F12 VirtualKeyCode = 0x7B + VK_F13 VirtualKeyCode = 0x7C + VK_F14 VirtualKeyCode = 0x7D + VK_F15 VirtualKeyCode = 0x7E + VK_F16 VirtualKeyCode = 0x7F + VK_F17 VirtualKeyCode = 0x80 + VK_F18 VirtualKeyCode = 0x81 + VK_F19 VirtualKeyCode = 0x82 + VK_F20 VirtualKeyCode = 0x83 + VK_F21 VirtualKeyCode = 0x84 + VK_F22 VirtualKeyCode = 0x85 + VK_F23 VirtualKeyCode = 0x86 + VK_F24 VirtualKeyCode = 0x87 + VK_NUMLOCK VirtualKeyCode = 0x90 + VK_SCROLL VirtualKeyCode = 0x91 + VK_OEM_NEC_EQUAL VirtualKeyCode = 0x92 + VK_OEM_FJ_JISHO VirtualKeyCode = 0x92 + VK_OEM_FJ_MASSHOU VirtualKeyCode = 0x93 + VK_OEM_FJ_TOUROKU VirtualKeyCode = 0x94 + VK_OEM_FJ_LOYA VirtualKeyCode = 0x95 + VK_OEM_FJ_ROYA VirtualKeyCode = 0x96 + VK_LSHIFT VirtualKeyCode = 0xA0 + VK_RSHIFT VirtualKeyCode = 0xA1 + VK_LCONTROL VirtualKeyCode = 0xA2 + VK_RCONTROL VirtualKeyCode = 0xA3 + VK_LMENU VirtualKeyCode = 0xA4 + VK_RMENU VirtualKeyCode = 0xA5 + VK_BROWSER_BACK VirtualKeyCode = 0xA6 + VK_BROWSER_FORWARD VirtualKeyCode = 0xA7 + VK_BROWSER_REFRESH VirtualKeyCode = 0xA8 + VK_BROWSER_STOP VirtualKeyCode = 0xA9 + VK_BROWSER_SEARCH VirtualKeyCode = 0xAA + VK_BROWSER_FAVORITES VirtualKeyCode = 0xAB + VK_BROWSER_HOME VirtualKeyCode = 0xAC + VK_VOLUME_MUTE VirtualKeyCode = 0xAD + VK_VOLUME_DOWN VirtualKeyCode = 0xAE + VK_VOLUME_UP VirtualKeyCode = 0xAF + VK_MEDIA_NEXT_TRACK VirtualKeyCode = 0xB0 + VK_MEDIA_PREV_TRACK VirtualKeyCode = 0xB1 + VK_MEDIA_STOP VirtualKeyCode = 0xB2 + VK_MEDIA_PLAY_PAUSE VirtualKeyCode = 0xB3 + VK_LAUNCH_MAIL VirtualKeyCode = 0xB4 + VK_LAUNCH_MEDIA_SELECT VirtualKeyCode = 0xB5 + VK_LAUNCH_APP1 VirtualKeyCode = 0xB6 + VK_LAUNCH_APP2 VirtualKeyCode = 0xB7 + VK_OEM_1 VirtualKeyCode = 0xBA + VK_OEM_PLUS VirtualKeyCode = 0xBB + VK_OEM_COMMA VirtualKeyCode = 0xBC + VK_OEM_MINUS VirtualKeyCode = 0xBD + VK_OEM_PERIOD VirtualKeyCode = 0xBE + VK_OEM_2 VirtualKeyCode = 0xBF + VK_OEM_3 VirtualKeyCode = 0xC0 + VK_OEM_4 VirtualKeyCode = 0xDB + VK_OEM_5 VirtualKeyCode = 0xDC + VK_OEM_6 VirtualKeyCode = 0xDD + VK_OEM_7 VirtualKeyCode = 0xDE + VK_OEM_8 VirtualKeyCode = 0xDF + VK_OEM_AX VirtualKeyCode = 0xE1 + VK_OEM_102 VirtualKeyCode = 0xE2 + VK_ICO_HELP VirtualKeyCode = 0xE3 + VK_ICO_00 VirtualKeyCode = 0xE4 + VK_PROCESSKEY VirtualKeyCode = 0xE5 + VK_ICO_CLEAR VirtualKeyCode = 0xE6 + VK_OEM_RESET VirtualKeyCode = 0xE9 + VK_OEM_JUMP VirtualKeyCode = 0xEA + VK_OEM_PA1 VirtualKeyCode = 0xEB + VK_OEM_PA2 VirtualKeyCode = 0xEC + VK_OEM_PA3 VirtualKeyCode = 0xED + VK_OEM_WSCTRL VirtualKeyCode = 0xEE + VK_OEM_CUSEL VirtualKeyCode = 0xEF + VK_OEM_ATTN VirtualKeyCode = 0xF0 + VK_OEM_FINISH VirtualKeyCode = 0xF1 + VK_OEM_COPY VirtualKeyCode = 0xF2 + VK_OEM_AUTO VirtualKeyCode = 0xF3 + VK_OEM_ENLW VirtualKeyCode = 0xF4 + VK_OEM_BACKTAB VirtualKeyCode = 0xF5 + VK_ATTN VirtualKeyCode = 0xF6 + VK_CRSEL VirtualKeyCode = 0xF7 + VK_EXSEL VirtualKeyCode = 0xF8 + VK_EREOF VirtualKeyCode = 0xF9 + VK_PLAY VirtualKeyCode = 0xFA + VK_ZOOM VirtualKeyCode = 0xFB + VK_NONAME VirtualKeyCode = 0xFC + VK_PA1 VirtualKeyCode = 0xFD + VK_OEM_CLEAR VirtualKeyCode = 0xFE +) diff --git a/vendor/github.com/erikgeiser/coninput/mode.go b/vendor/github.com/erikgeiser/coninput/mode.go new file mode 100644 index 00000000..e6d8bf44 --- /dev/null +++ b/vendor/github.com/erikgeiser/coninput/mode.go @@ -0,0 +1,82 @@ +//go:build windows +// +build windows + +package coninput + +import ( + "strings" + + "golang.org/x/sys/windows" +) + +// AddInputModes returns the given mode with one or more additional modes enabled. +func AddInputModes(mode uint32, enableModes ...uint32) uint32 { + for _, enableMode := range enableModes { + mode |= enableMode + } + + return mode +} + +// RemoveInputModes returns the given mode with one or more additional modes disabled. +func RemoveInputModes(mode uint32, disableModes ...uint32) uint32 { + for _, disableMode := range disableModes { + mode &^= disableMode + } + + return mode +} + +// ToggleInputModes returns the given mode with one or more additional modes toggeled. +func ToggleInputModes(mode uint32, toggleModes ...uint32) uint32 { + for _, toggeMode := range toggleModes { + mode ^= toggeMode + } + + return mode +} + +var inputModes = []struct { + mode uint32 + name string +}{ + {mode: windows.ENABLE_ECHO_INPUT, name: "ENABLE_ECHO_INPUT"}, + {mode: windows.ENABLE_INSERT_MODE, name: "ENABLE_INSERT_MODE"}, + {mode: windows.ENABLE_LINE_INPUT, name: "ENABLE_LINE_INPUT"}, + {mode: windows.ENABLE_MOUSE_INPUT, name: "ENABLE_MOUSE_INPUT"}, + {mode: windows.ENABLE_PROCESSED_INPUT, name: "ENABLE_PROCESSED_INPUT"}, + {mode: windows.ENABLE_QUICK_EDIT_MODE, name: "ENABLE_QUICK_EDIT_MODE"}, + {mode: windows.ENABLE_WINDOW_INPUT, name: "ENABLE_WINDOW_INPUT"}, + {mode: windows.ENABLE_VIRTUAL_TERMINAL_INPUT, name: "ENABLE_VIRTUAL_TERMINAL_INPUT"}, +} + +// ListInputMode returnes the isolated enabled input modes as a list. +func ListInputModes(mode uint32) []uint32 { + modes := []uint32{} + + for _, inputMode := range inputModes { + if mode&inputMode.mode > 0 { + modes = append(modes, inputMode.mode) + } + } + + return modes +} + +// ListInputMode returnes the isolated enabled input mode names as a list. +func ListInputModeNames(mode uint32) []string { + modes := []string{} + + for _, inputMode := range inputModes { + if mode&inputMode.mode > 0 { + modes = append(modes, inputMode.name) + } + } + + return modes +} + +// DescribeInputMode returns a string containing the names of each enabled input mode. +func DescribeInputMode(mode uint32) string { + return strings.Join(ListInputModeNames(mode), "|") +} diff --git a/vendor/github.com/erikgeiser/coninput/read.go b/vendor/github.com/erikgeiser/coninput/read.go new file mode 100644 index 00000000..b2dd82f1 --- /dev/null +++ b/vendor/github.com/erikgeiser/coninput/read.go @@ -0,0 +1,154 @@ +//go:build windows +// +build windows + +package coninput + +import ( + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + procReadConsoleInputW = modkernel32.NewProc("ReadConsoleInputW") + procPeekConsoleInputW = modkernel32.NewProc("PeekConsoleInputW") + procGetNumberOfConsoleInputEvents = modkernel32.NewProc("GetNumberOfConsoleInputEvents") + procFlushConsoleInputBuffer = modkernel32.NewProc("FlushConsoleInputBuffer") +) + +// NewStdinHandle is a shortcut for windows.GetStdHandle(windows.STD_INPUT_HANDLE). +func NewStdinHandle() (windows.Handle, error) { + return windows.GetStdHandle(windows.STD_INPUT_HANDLE) +} + +// WinReadConsoleInput is a thin wrapper around the Windows console API function +// ReadConsoleInput (see +// https://docs.microsoft.com/en-us/windows/console/readconsoleinput). In most +// cases it is more practical to either use ReadConsoleInput or +// ReadNConsoleInputs. +func WinReadConsoleInput(consoleInput windows.Handle, buffer *InputRecord, + length uint32, numberOfEventsRead *uint32) error { + r, _, e := syscall.Syscall6(procReadConsoleInputW.Addr(), 4, + uintptr(consoleInput), uintptr(unsafe.Pointer(buffer)), uintptr(length), + uintptr(unsafe.Pointer(numberOfEventsRead)), 0, 0) + if r == 0 { + return error(e) + } + + return nil +} + +// ReadNConsoleInputs is a wrapper around ReadConsoleInput (see +// https://docs.microsoft.com/en-us/windows/console/readconsoleinput) that +// automates the event buffer allocation in oder to provide io.Reader-like +// sematics. maxEvents must be greater than zero. +func ReadNConsoleInputs(console windows.Handle, maxEvents uint32) ([]InputRecord, error) { + if maxEvents == 0 { + return nil, fmt.Errorf("maxEvents cannot be zero") + } + + var inputRecords = make([]InputRecord, maxEvents) + n, err := ReadConsoleInput(console, inputRecords) + + return inputRecords[:n], err +} + +// ReadConsoleInput provides an ideomatic interface to the Windows console API +// function ReadConsoleInput (see +// https://docs.microsoft.com/en-us/windows/console/readconsoleinput). The size +// of inputRecords must be greater than zero. +func ReadConsoleInput(console windows.Handle, inputRecords []InputRecord) (uint32, error) { + if len(inputRecords) == 0 { + return 0, fmt.Errorf("size of input record buffer cannot be zero") + } + + var read uint32 + err := WinReadConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) + + return read, err +} + +// WinPeekConsoleInput is a thin wrapper around the Windows console API function +// PeekConsoleInput (see +// https://docs.microsoft.com/en-us/windows/console/peekconsoleinput). In most +// cases it is more practical to either use PeekConsoleInput or +// PeekNConsoleInputs. +func WinPeekConsoleInput(consoleInput windows.Handle, buffer *InputRecord, + length uint32, numberOfEventsRead *uint32) error { + r, _, e := syscall.Syscall6(procPeekConsoleInputW.Addr(), 4, + uintptr(consoleInput), uintptr(unsafe.Pointer(buffer)), uintptr(length), + uintptr(unsafe.Pointer(numberOfEventsRead)), 0, 0) + if r == 0 { + return error(e) + } + + return nil + +} + +// PeekNConsoleInputs is a wrapper around PeekConsoleInput (see +// https://docs.microsoft.com/en-us/windows/console/peekconsoleinput) that +// automates the event buffer allocation in oder to provide io.Reader-like +// sematics. maxEvents must be greater than zero. +func PeekNConsoleInputs(console windows.Handle, maxEvents uint32) ([]InputRecord, error) { + if maxEvents == 0 { + return nil, fmt.Errorf("maxEvents cannot be zero") + } + + var inputRecords = make([]InputRecord, maxEvents) + n, err := PeekConsoleInput(console, inputRecords) + + return inputRecords[:n], err +} + +// PeekConsoleInput provides an ideomatic interface to the Windows console API +// function PeekConsoleInput (see +// https://docs.microsoft.com/en-us/windows/console/peekconsoleinput). The size +// of inputRecords must be greater than zero. +func PeekConsoleInput(console windows.Handle, inputRecords []InputRecord) (uint32, error) { + if len(inputRecords) == 0 { + return 0, fmt.Errorf("size of input record buffer cannot be zero") + } + + var read uint32 + + err := WinPeekConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read) + + return read, err +} + +// WinGetNumberOfConsoleInputEvents provides an ideomatic interface to the +// Windows console API function GetNumberOfConsoleInputEvents (see +// https://docs.microsoft.com/en-us/windows/console/getnumberofconsoleinputevents). +func WinGetNumberOfConsoleInputEvents(consoleInput windows.Handle, numberOfEvents *uint32) error { + r, _, e := syscall.Syscall6(procGetNumberOfConsoleInputEvents.Addr(), 2, + uintptr(consoleInput), uintptr(unsafe.Pointer(numberOfEvents)), 0, + 0, 0, 0) + if r == 0 { + return error(e) + } + + return nil +} + +// GetNumberOfConsoleInputEvents provides an ideomatic interface to the Windows +// console API function GetNumberOfConsoleInputEvents (see +// https://docs.microsoft.com/en-us/windows/console/getnumberofconsoleinputevents). +func GetNumberOfConsoleInputEvents(console windows.Handle) (uint32, error) { + var nEvents uint32 + err := WinGetNumberOfConsoleInputEvents(console, &nEvents) + + return nEvents, err +} + +func FlushConsoleInputBuffer(consoleInput windows.Handle) error { + r, _, e := syscall.Syscall(procFlushConsoleInputBuffer.Addr(), 1, uintptr(consoleInput), 0, 0) + if r == 0 { + return error(e) + } + + return nil +} diff --git a/vendor/github.com/erikgeiser/coninput/records.go b/vendor/github.com/erikgeiser/coninput/records.go new file mode 100644 index 00000000..cccf7fbf --- /dev/null +++ b/vendor/github.com/erikgeiser/coninput/records.go @@ -0,0 +1,486 @@ +package coninput + +import ( + "encoding/binary" + "fmt" + "strconv" + "strings" +) + +const ( + maxEventSize = 16 + wordPaddingBytes = 2 +) + +// EventType denots the type of an event +type EventType uint16 + +// EventUnion is the union data type that contains the data for any event. +type EventUnion [maxEventSize]byte + +// InputRecord corresponds to the INPUT_RECORD structure from the Windows +// console API (see +// https://docs.microsoft.com/en-us/windows/console/input-record-str). +type InputRecord struct { + // EventType specifies the type of event that helt in Event. + EventType EventType + + // Padding of the 16-bit EventType to a whole 32-bit dword. + _ [wordPaddingBytes]byte + + // Event holds the actual event data. Use Unrap to access it as its + // respective event type. + Event EventUnion +} + +// String implements fmt.Stringer for InputRecord. +func (ir InputRecord) String() string { + return ir.Unwrap().String() +} + +// Unwrap parses the event data into an EventRecord of the respective event +// type. The data in the returned EventRecord does not contain any references to +// the passed InputRecord. +func (ir InputRecord) Unwrap() EventRecord { + switch ir.EventType { + case FocusEventType: + return FocusEventRecord{SetFocus: ir.Event[0] > 0} + case KeyEventType: + return KeyEventRecord{ + KeyDown: binary.LittleEndian.Uint32(ir.Event[0:4]) > 0, + RepeatCount: binary.LittleEndian.Uint16(ir.Event[4:6]), + VirtualKeyCode: VirtualKeyCode(binary.LittleEndian.Uint16(ir.Event[6:8])), + VirtualScanCode: VirtualKeyCode(binary.LittleEndian.Uint16(ir.Event[8:10])), + Char: rune(binary.LittleEndian.Uint16(ir.Event[10:12])), + ControlKeyState: ControlKeyState(binary.LittleEndian.Uint32(ir.Event[12:16])), + } + case MouseEventType: + m := MouseEventRecord{ + MousePositon: Coord{ + X: binary.LittleEndian.Uint16(ir.Event[0:2]), + Y: binary.LittleEndian.Uint16(ir.Event[2:4]), + }, + ButtonState: ButtonState(binary.LittleEndian.Uint32(ir.Event[4:8])), + ControlKeyState: ControlKeyState(binary.LittleEndian.Uint32(ir.Event[8:12])), + EventFlags: EventFlags(binary.LittleEndian.Uint32(ir.Event[12:16])), + } + + if (m.EventFlags&MOUSE_WHEELED > 0) || (m.EventFlags&MOUSE_HWHEELED > 0) { + if int16(highWord(uint32(m.ButtonState))) > 0 { + m.WheelDirection = 1 + } else { + m.WheelDirection = -1 + } + } + + return m + case WindowBufferSizeEventType: + return WindowBufferSizeEventRecord{ + Size: Coord{ + X: binary.LittleEndian.Uint16(ir.Event[0:2]), + Y: binary.LittleEndian.Uint16(ir.Event[2:4]), + }, + } + case MenuEventType: + return MenuEventRecord{ + CommandID: binary.LittleEndian.Uint32(ir.Event[0:4]), + } + default: + return &UnknownEvent{InputRecord: ir} + } +} + +// EventRecord represents one of the following event types: +// TypeFocusEventRecord, TypeKeyEventRecord, TypeMouseEventRecord, +// TypeWindowBufferSizeEvent, TypeMenuEventRecord and UnknownEvent. +type EventRecord interface { + Type() string + fmt.Stringer +} + +// FocusEventType is the event type for a FocusEventRecord (see +// https://docs.microsoft.com/en-us/windows/console/input-record-str). +const FocusEventType EventType = 0x0010 + +// FocusEventRecord represent the FOCUS_EVENT_RECORD structure from the Windows +// console API (see +// https://docs.microsoft.com/en-us/windows/console/focus-event-record-str). +// These events are used internally by the Windows console API and should be +// ignored. +type FocusEventRecord struct { + // SetFocus is reserved and should not be used. + SetFocus bool +} + +// Ensure that FocusEventRecord satisfies EventRecord interface. +var _ EventRecord = FocusEventRecord{} + +// Type ensures that FocusEventRecord satisfies EventRecord interface. +func (e FocusEventRecord) Type() string { return "FocusEvent" } + +// String ensures that FocusEventRecord satisfies EventRecord and fmt.Stringer +// interfaces. +func (e FocusEventRecord) String() string { return fmt.Sprintf("%s[%v]", e.Type(), e.SetFocus) } + +// KeyEventType is the event type for a KeyEventRecord (see +// https://docs.microsoft.com/en-us/windows/console/input-record-str). +const KeyEventType EventType = 0x0001 + +// KeyEventRecord represent the KEY_EVENT_RECORD structure from the Windows +// console API (see +// https://docs.microsoft.com/en-us/windows/console/key-event-record-str). +type KeyEventRecord struct { + // KeyDown specified whether the key is pressed or released. + KeyDown bool + + // RepeatCount indicates that a key is being held down. For example, when a + // key is held down, five events with RepeatCount equal to 1 may be + // generated, one event with RepeatCount equal to 5, or multiple events + // with RepeatCount greater than or equal to 1. + RepeatCount uint16 + + // VirtualKeyCode identifies the given key in a device-independent manner + // (see + // https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes). + VirtualKeyCode VirtualKeyCode + + // VirtualScanCode represents the device-dependent value generated by the + // keyboard hardware. + VirtualScanCode VirtualKeyCode + + // Char is the character that corresponds to the pressed key. Char can be + // zero for some keys. + Char rune + + //ControlKeyState holds the state of the control keys. + ControlKeyState ControlKeyState +} + +// Ensure that KeyEventRecord satisfies EventRecord interface. +var _ EventRecord = KeyEventRecord{} + +// Type ensures that KeyEventRecord satisfies EventRecord interface. +func (e KeyEventRecord) Type() string { return "KeyEvent" } + +// String ensures that KeyEventRecord satisfies EventRecord and fmt.Stringer +// interfaces. +func (e KeyEventRecord) String() string { + infos := []string{} + + repeat := "" + if e.RepeatCount > 1 { + repeat = "x" + strconv.Itoa(int(e.RepeatCount)) + } + + infos = append(infos, fmt.Sprintf("%q%s", e.Char, repeat)) + + direction := "up" + if e.KeyDown { + direction = "down" + } + + infos = append(infos, direction) + + if e.ControlKeyState != NO_CONTROL_KEY { + infos = append(infos, e.ControlKeyState.String()) + } + + infos = append(infos, fmt.Sprintf("KeyCode: %d", e.VirtualKeyCode)) + infos = append(infos, fmt.Sprintf("ScanCode: %d", e.VirtualScanCode)) + + return fmt.Sprintf("%s[%s]", e.Type(), strings.Join(infos, ", ")) +} + +// MenuEventType is the event type for a MenuEventRecord (see +// https://docs.microsoft.com/en-us/windows/console/input-record-str). +const MenuEventType EventType = 0x0008 + +// MenuEventRecord represent the MENU_EVENT_RECORD structure from the Windows +// console API (see +// https://docs.microsoft.com/en-us/windows/console/menu-event-record-str). +// These events are deprecated by the Windows console API and should be ignored. +type MenuEventRecord struct { + CommandID uint32 +} + +// Ensure that MenuEventRecord satisfies EventRecord interface. +var _ EventRecord = MenuEventRecord{} + +// Type ensures that MenuEventRecord satisfies EventRecord interface. +func (e MenuEventRecord) Type() string { return "MenuEvent" } + +// String ensures that MenuEventRecord satisfies EventRecord and fmt.Stringer +// interfaces. +func (e MenuEventRecord) String() string { return fmt.Sprintf("MenuEvent[%d]", e.CommandID) } + +// MouseEventType is the event type for a MouseEventRecord (see +// https://docs.microsoft.com/en-us/windows/console/input-record-str). +const MouseEventType EventType = 0x0002 + +// MouseEventRecord represent the MOUSE_EVENT_RECORD structure from the Windows +// console API (see +// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str). +type MouseEventRecord struct { + // MousePosition contains the location of the cursor, in terms of the + // console screen buffer's character-cell coordinates. + MousePositon Coord + + // ButtonState holds the status of the mouse buttons. + ButtonState ButtonState + + // ControlKeyState holds the state of the control keys. + ControlKeyState ControlKeyState + + // EventFlags specify tge type of mouse event. + EventFlags EventFlags + + // WheelDirection specified the direction in which the mouse wheel is + // spinning when EventFlags contains MOUSE_HWHEELED or MOUSE_WHEELED. When + // the event flags specify MOUSE_WHEELED it is 1 if the wheel rotated + // forward (away from the user) or -1 when it rotates backwards. When + // MOUSE_HWHEELED is specified it is 1 when the wheel rotates right and -1 + // when it rotates left. When the EventFlags do not indicate a mouse wheel + // event it is 0. + WheelDirection int +} + +// Ensure that MouseEventRecord satisfies EventRecord interface. +var _ EventRecord = MouseEventRecord{} + +func (e MouseEventRecord) WheelDirectionName() string { + if e.EventFlags&MOUSE_WHEELED > 0 { + if e.WheelDirection > 0 { + return "Forward" + } + + return "Backward" + } else if e.EventFlags&MOUSE_HWHEELED > 0 { + if e.WheelDirection > 0 { + return "Right" + } + + return "Left" + } + + return "" +} + +// Type ensures that MouseEventRecord satisfies EventRecord interface. +func (e MouseEventRecord) Type() string { return "MouseEvent" } + +// String ensures that MouseEventRecord satisfies EventRecord and fmt.Stringer +// interfaces. +func (e MouseEventRecord) String() string { + infos := []string{e.MousePositon.String()} + + if e.ButtonState&0xFF != 0 { + infos = append(infos, e.ButtonState.String()) + } + + eventDescription := e.EventFlags.String() + + wheelDirection := e.WheelDirectionName() + if wheelDirection != "" { + eventDescription += "(" + wheelDirection + ")" + } + + infos = append(infos, eventDescription) + + if e.ControlKeyState != NO_CONTROL_KEY { + infos = append(infos, e.ControlKeyState.String()) + } + + return fmt.Sprintf("%s[%s]", e.Type(), strings.Join(infos, ", ")) +} + +// WindowBufferSizeEventType is the event type for a WindowBufferSizeEventRecord +// (see https://docs.microsoft.com/en-us/windows/console/input-record-str). +const WindowBufferSizeEventType EventType = 0x0004 + +// WindowBufferSizeEventRecord represent the WINDOW_BUFFER_SIZE_RECORD structure +// from the Windows console API (see +// https://docs.microsoft.com/en-us/windows/console/window-buffer-size-record-str). +type WindowBufferSizeEventRecord struct { + // Size contains the size of the console screen buffer, in character cell columns and rows. + Size Coord +} + +// Ensure that WindowBufferSizeEventRecord satisfies EventRecord interface. +var _ EventRecord = WindowBufferSizeEventRecord{} + +// Type ensures that WindowBufferSizeEventRecord satisfies EventRecord interface. +func (e WindowBufferSizeEventRecord) Type() string { return "WindowBufferSizeEvent" } + +// String ensures that WindowBufferSizeEventRecord satisfies EventRecord and fmt.Stringer +// interfaces. +func (e WindowBufferSizeEventRecord) String() string { + return fmt.Sprintf("WindowBufferSizeEvent[%s]", e.Size) +} + +// UnknownEvent is generated when the event type does not match one of the +// following types: TypeFocusEventRecord, TypeKeyEventRecord, +// TypeMouseEventRecord, TypeWindowBufferSizeEvent, TypeMenuEventRecord and +// UnknownEvent. +type UnknownEvent struct { + InputRecord +} + +// Ensure that UnknownEvent satisfies EventRecord interface. +var _ EventRecord = UnknownEvent{} + +// Type ensures that UnknownEvent satisfies EventRecord interface. +func (e UnknownEvent) Type() string { return "UnknownEvent" } + +// String ensures that UnknownEvent satisfies EventRecord and fmt.Stringer +// interfaces. +func (e UnknownEvent) String() string { + return fmt.Sprintf("%s[Type: %d, Data: %v]", e.Type(), e.InputRecord.EventType, e.InputRecord.Event[:]) +} + +// Coord represent the COORD structure from the Windows +// console API (see https://docs.microsoft.com/en-us/windows/console/coord-str). +type Coord struct { + // X is the horizontal coordinate or column value. The units depend on the function call. + X uint16 + // Y is the vertical coordinate or row value. The units depend on the function call. + Y uint16 +} + +// String ensures that Coord satisfies the fmt.Stringer interface. +func (c Coord) String() string { + return fmt.Sprintf("(%d, %d)", c.X, c.Y) +} + +// ButtonState holds the state of the mouse buttons (see +// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str). +type ButtonState uint32 + +func (bs ButtonState) Contains(state ButtonState) bool { + return bs&state > 0 +} + +// String ensures that ButtonState satisfies the fmt.Stringer interface. +func (bs ButtonState) String() string { + switch { + case bs&FROM_LEFT_1ST_BUTTON_PRESSED > 0: + return "Left" + case bs&FROM_LEFT_2ND_BUTTON_PRESSED > 0: + return "2" + case bs&FROM_LEFT_3RD_BUTTON_PRESSED > 0: + return "3" + case bs&FROM_LEFT_4TH_BUTTON_PRESSED > 0: + return "4" + case bs&RIGHTMOST_BUTTON_PRESSED > 0: + return "Right" + case bs&0xFF == 0: + return "No Button" + default: + return fmt.Sprintf("Unknown(%d)", bs) + } +} + +func (bs ButtonState) IsReleased() bool { + return bs&0xff > 0 +} + +// Valid values for ButtonState. +const ( + FROM_LEFT_1ST_BUTTON_PRESSED ButtonState = 0x0001 + RIGHTMOST_BUTTON_PRESSED ButtonState = 0x0002 + FROM_LEFT_2ND_BUTTON_PRESSED ButtonState = 0x0004 + FROM_LEFT_3RD_BUTTON_PRESSED ButtonState = 0x0008 + FROM_LEFT_4TH_BUTTON_PRESSED ButtonState = 0x0010 +) + +// ControlKeyState holds the state of the control keys for key and mouse events +// (see https://docs.microsoft.com/en-us/windows/console/key-event-record-str +// and https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str). +type ControlKeyState uint32 + +func (cks ControlKeyState) Contains(state ControlKeyState) bool { + return cks&state > 0 +} + +// Valid values for ControlKeyState. +const ( + CAPSLOCK_ON ControlKeyState = 0x0080 + ENHANCED_KEY ControlKeyState = 0x0100 + LEFT_ALT_PRESSED ControlKeyState = 0x0002 + LEFT_CTRL_PRESSED ControlKeyState = 0x0008 + NUMLOCK_ON ControlKeyState = 0x0020 + RIGHT_ALT_PRESSED ControlKeyState = 0x0001 + RIGHT_CTRL_PRESSED ControlKeyState = 0x0004 + SCROLLLOCK_ON ControlKeyState = 0x0040 + SHIFT_PRESSED ControlKeyState = 0x0010 + NO_CONTROL_KEY ControlKeyState = 0x0000 +) + +// String ensures that ControlKeyState satisfies the fmt.Stringer interface. +func (cks ControlKeyState) String() string { + controlKeys := []string{} + + switch { + case cks&CAPSLOCK_ON > 0: + controlKeys = append(controlKeys, "CapsLock") + case cks&ENHANCED_KEY > 0: + controlKeys = append(controlKeys, "Enhanced") + case cks&LEFT_ALT_PRESSED > 0: + controlKeys = append(controlKeys, "Alt") + case cks&LEFT_CTRL_PRESSED > 0: + controlKeys = append(controlKeys, "CTRL") + case cks&NUMLOCK_ON > 0: + controlKeys = append(controlKeys, "NumLock") + case cks&RIGHT_ALT_PRESSED > 0: + controlKeys = append(controlKeys, "RightAlt") + case cks&RIGHT_CTRL_PRESSED > 0: + controlKeys = append(controlKeys, "RightCTRL") + case cks&SCROLLLOCK_ON > 0: + controlKeys = append(controlKeys, "ScrollLock") + case cks&SHIFT_PRESSED > 0: + controlKeys = append(controlKeys, "Shift") + case cks == NO_CONTROL_KEY: + default: + return fmt.Sprintf("Unknown(%d)", cks) + } + + return strings.Join(controlKeys, ",") +} + +// EventFlags specifies the type of a mouse event (see +// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str). +type EventFlags uint32 + +// String ensures that EventFlags satisfies the fmt.Stringer interface. +func (ef EventFlags) String() string { + switch { + case ef&DOUBLE_CLICK > 0: + return "DoubleClick" + case ef&MOUSE_WHEELED > 0: + return "Wheeled" + case ef&MOUSE_MOVED > 0: + return "Moved" + case ef&MOUSE_HWHEELED > 0: + return "HWheeld" + case ef == CLICK: + return "Click" + default: + return fmt.Sprintf("Unknown(%d)", ef) + } +} + +func (ef EventFlags) Contains(flag EventFlags) bool { + return ef&flag > 0 +} + +// Valid values for EventFlags. +const ( + CLICK EventFlags = 0x0000 + MOUSE_MOVED EventFlags = 0x0001 + DOUBLE_CLICK EventFlags = 0x0002 + MOUSE_WHEELED EventFlags = 0x0004 + MOUSE_HWHEELED EventFlags = 0x0008 +) + +func highWord(data uint32) uint16 { + return uint16((data & 0xFFFF0000) >> 16) +} diff --git a/vendor/github.com/fatih/color/README.md b/vendor/github.com/fatih/color/README.md index 5152bf59..be82827c 100644 --- a/vendor/github.com/fatih/color/README.md +++ b/vendor/github.com/fatih/color/README.md @@ -7,7 +7,6 @@ suits you. ![Color](https://user-images.githubusercontent.com/438920/96832689-03b3e000-13f4-11eb-9803-46f4c4de3406.jpg) - ## Install ```bash @@ -124,17 +123,17 @@ fmt.Println("All text will now be bold magenta.") ``` ### Disable/Enable color - + There might be a case where you want to explicitly disable/enable color output. the `go-isatty` package will automatically disable color output for non-tty output streams (for example if the output were piped directly to `less`). The `color` package also disables color output if the [`NO_COLOR`](https://no-color.org) environment -variable is set (regardless of its value). +variable is set to a non-empty string. -`Color` has support to disable/enable colors programatically both globally and +`Color` has support to disable/enable colors programmatically both globally and for single color definitions. For example suppose you have a CLI app and a -`--no-color` bool flag. You can easily disable the color output with: +`-no-color` bool flag. You can easily disable the color output with: ```go var flagNoColor = flag.Bool("no-color", false, "Disable color output") @@ -167,11 +166,10 @@ To output color in GitHub Actions (or other CI systems that support ANSI colors) * Save/Return previous values * Evaluate fmt.Formatter interface - ## Credits - * [Fatih Arslan](https://github.com/fatih) - * Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable) +* [Fatih Arslan](https://github.com/fatih) +* Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable) ## License diff --git a/vendor/github.com/fatih/color/color.go b/vendor/github.com/fatih/color/color.go index 98a60f3c..c4234287 100644 --- a/vendor/github.com/fatih/color/color.go +++ b/vendor/github.com/fatih/color/color.go @@ -19,10 +19,10 @@ var ( // set (regardless of its value). This is a global option and affects all // colors. For more control over each color block use the methods // DisableColor() individually. - NoColor = noColorExists() || os.Getenv("TERM") == "dumb" || + NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) - // Output defines the standard output of the print functions. By default + // Output defines the standard output of the print functions. By default, // os.Stdout is used. Output = colorable.NewColorableStdout() @@ -35,10 +35,9 @@ var ( colorsCacheMu sync.Mutex // protects colorsCache ) -// noColorExists returns true if the environment variable NO_COLOR exists. -func noColorExists() bool { - _, exists := os.LookupEnv("NO_COLOR") - return exists +// noColorIsSet returns true if the environment variable NO_COLOR is set to a non-empty string. +func noColorIsSet() bool { + return os.Getenv("NO_COLOR") != "" } // Color defines a custom color object which is defined by SGR parameters. @@ -66,6 +65,29 @@ const ( CrossedOut ) +const ( + ResetBold Attribute = iota + 22 + ResetItalic + ResetUnderline + ResetBlinking + _ + ResetReversed + ResetConcealed + ResetCrossedOut +) + +var mapResetAttributes map[Attribute]Attribute = map[Attribute]Attribute{ + Bold: ResetBold, + Faint: ResetBold, + Italic: ResetItalic, + Underline: ResetUnderline, + BlinkSlow: ResetBlinking, + BlinkRapid: ResetBlinking, + ReverseVideo: ResetReversed, + Concealed: ResetConcealed, + CrossedOut: ResetCrossedOut, +} + // Foreground text colors const ( FgBlack Attribute = iota + 30 @@ -120,7 +142,7 @@ func New(value ...Attribute) *Color { params: make([]Attribute, 0), } - if noColorExists() { + if noColorIsSet() { c.noColor = boolPtr(true) } @@ -152,7 +174,7 @@ func (c *Color) Set() *Color { return c } - fmt.Fprintf(Output, c.format()) + fmt.Fprint(Output, c.format()) return c } @@ -164,16 +186,21 @@ func (c *Color) unset() { Unset() } -func (c *Color) setWriter(w io.Writer) *Color { +// SetWriter is used to set the SGR sequence with the given io.Writer. This is +// a low-level function, and users should use the higher-level functions, such +// as color.Fprint, color.Print, etc. +func (c *Color) SetWriter(w io.Writer) *Color { if c.isNoColorSet() { return c } - fmt.Fprintf(w, c.format()) + fmt.Fprint(w, c.format()) return c } -func (c *Color) unsetWriter(w io.Writer) { +// UnsetWriter resets all escape attributes and clears the output with the give +// io.Writer. Usually should be called after SetWriter(). +func (c *Color) UnsetWriter(w io.Writer) { if c.isNoColorSet() { return } @@ -192,20 +219,14 @@ func (c *Color) Add(value ...Attribute) *Color { return c } -func (c *Color) prepend(value Attribute) { - c.params = append(c.params, 0) - copy(c.params[1:], c.params[0:]) - c.params[0] = value -} - // Fprint formats using the default formats for its operands and writes to w. // Spaces are added between operands when neither is a string. // It returns the number of bytes written and any write error encountered. // On Windows, users should wrap w with colorable.NewColorable() if w is of // type *os.File. func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) { - c.setWriter(w) - defer c.unsetWriter(w) + c.SetWriter(w) + defer c.UnsetWriter(w) return fmt.Fprint(w, a...) } @@ -227,8 +248,8 @@ func (c *Color) Print(a ...interface{}) (n int, err error) { // On Windows, users should wrap w with colorable.NewColorable() if w is of // type *os.File. func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { - c.setWriter(w) - defer c.unsetWriter(w) + c.SetWriter(w) + defer c.UnsetWriter(w) return fmt.Fprintf(w, format, a...) } @@ -248,10 +269,7 @@ func (c *Color) Printf(format string, a ...interface{}) (n int, err error) { // On Windows, users should wrap w with colorable.NewColorable() if w is of // type *os.File. func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { - c.setWriter(w) - defer c.unsetWriter(w) - - return fmt.Fprintln(w, a...) + return fmt.Fprintln(w, c.wrap(fmt.Sprint(a...))) } // Println formats using the default formats for its operands and writes to @@ -260,10 +278,7 @@ func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { // encountered. This is the standard fmt.Print() method wrapped with the given // color. func (c *Color) Println(a ...interface{}) (n int, err error) { - c.Set() - defer c.unset() - - return fmt.Fprintln(Output, a...) + return fmt.Fprintln(Output, c.wrap(fmt.Sprint(a...))) } // Sprint is just like Print, but returns a string instead of printing it. @@ -273,7 +288,7 @@ func (c *Color) Sprint(a ...interface{}) string { // Sprintln is just like Println, but returns a string instead of printing it. func (c *Color) Sprintln(a ...interface{}) string { - return c.wrap(fmt.Sprintln(a...)) + return fmt.Sprintln(c.Sprint(a...)) } // Sprintf is just like Printf, but returns a string instead of printing it. @@ -355,7 +370,7 @@ func (c *Color) SprintfFunc() func(format string, a ...interface{}) string { // string. Windows users should use this in conjunction with color.Output. func (c *Color) SprintlnFunc() func(a ...interface{}) string { return func(a ...interface{}) string { - return c.wrap(fmt.Sprintln(a...)) + return fmt.Sprintln(c.Sprint(a...)) } } @@ -385,7 +400,18 @@ func (c *Color) format() string { } func (c *Color) unformat() string { - return fmt.Sprintf("%s[%dm", escape, Reset) + //return fmt.Sprintf("%s[%dm", escape, Reset) + //for each element in sequence let's use the speficic reset escape, ou the generic one if not found + format := make([]string, len(c.params)) + for i, v := range c.params { + format[i] = strconv.Itoa(int(Reset)) + ra, ok := mapResetAttributes[v] + if ok { + format[i] = strconv.Itoa(int(ra)) + } + } + + return fmt.Sprintf("%s[%sm", escape, strings.Join(format, ";")) } // DisableColor disables the color output. Useful to not change any existing @@ -396,7 +422,7 @@ func (c *Color) DisableColor() { } // EnableColor enables the color output. Use it in conjunction with -// DisableColor(). Otherwise this method has no side effects. +// DisableColor(). Otherwise, this method has no side effects. func (c *Color) EnableColor() { c.noColor = boolPtr(false) } @@ -413,6 +439,12 @@ func (c *Color) isNoColorSet() bool { // Equals returns a boolean value indicating whether two colors are equal. func (c *Color) Equals(c2 *Color) bool { + if c == nil && c2 == nil { + return true + } + if c == nil || c2 == nil { + return false + } if len(c.params) != len(c2.params) { return false } diff --git a/vendor/github.com/fatih/color/color_windows.go b/vendor/github.com/fatih/color/color_windows.go new file mode 100644 index 00000000..be01c558 --- /dev/null +++ b/vendor/github.com/fatih/color/color_windows.go @@ -0,0 +1,19 @@ +package color + +import ( + "os" + + "golang.org/x/sys/windows" +) + +func init() { + // Opt-in for ansi color support for current process. + // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#output-sequences + var outMode uint32 + out := windows.Handle(os.Stdout.Fd()) + if err := windows.GetConsoleMode(out, &outMode); err != nil { + return + } + outMode |= windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING + _ = windows.SetConsoleMode(out, outMode) +} diff --git a/vendor/github.com/fatih/color/doc.go b/vendor/github.com/fatih/color/doc.go index 04541de7..9491ad54 100644 --- a/vendor/github.com/fatih/color/doc.go +++ b/vendor/github.com/fatih/color/doc.go @@ -5,106 +5,105 @@ that suits you. Use simple and default helper functions with predefined foreground colors: - color.Cyan("Prints text in cyan.") + color.Cyan("Prints text in cyan.") - // a newline will be appended automatically - color.Blue("Prints %s in blue.", "text") + // a newline will be appended automatically + color.Blue("Prints %s in blue.", "text") - // More default foreground colors.. - color.Red("We have red") - color.Yellow("Yellow color too!") - color.Magenta("And many others ..") + // More default foreground colors.. + color.Red("We have red") + color.Yellow("Yellow color too!") + color.Magenta("And many others ..") - // Hi-intensity colors - color.HiGreen("Bright green color.") - color.HiBlack("Bright black means gray..") - color.HiWhite("Shiny white color!") + // Hi-intensity colors + color.HiGreen("Bright green color.") + color.HiBlack("Bright black means gray..") + color.HiWhite("Shiny white color!") -However there are times where custom color mixes are required. Below are some +However, there are times when custom color mixes are required. Below are some examples to create custom color objects and use the print functions of each separate color object. - // Create a new color object - c := color.New(color.FgCyan).Add(color.Underline) - c.Println("Prints cyan text with an underline.") + // Create a new color object + c := color.New(color.FgCyan).Add(color.Underline) + c.Println("Prints cyan text with an underline.") - // Or just add them to New() - d := color.New(color.FgCyan, color.Bold) - d.Printf("This prints bold cyan %s\n", "too!.") + // Or just add them to New() + d := color.New(color.FgCyan, color.Bold) + d.Printf("This prints bold cyan %s\n", "too!.") - // Mix up foreground and background colors, create new mixes! - red := color.New(color.FgRed) + // Mix up foreground and background colors, create new mixes! + red := color.New(color.FgRed) - boldRed := red.Add(color.Bold) - boldRed.Println("This will print text in bold red.") + boldRed := red.Add(color.Bold) + boldRed.Println("This will print text in bold red.") - whiteBackground := red.Add(color.BgWhite) - whiteBackground.Println("Red text with White background.") + whiteBackground := red.Add(color.BgWhite) + whiteBackground.Println("Red text with White background.") - // Use your own io.Writer output - color.New(color.FgBlue).Fprintln(myWriter, "blue color!") + // Use your own io.Writer output + color.New(color.FgBlue).Fprintln(myWriter, "blue color!") - blue := color.New(color.FgBlue) - blue.Fprint(myWriter, "This will print text in blue.") + blue := color.New(color.FgBlue) + blue.Fprint(myWriter, "This will print text in blue.") You can create PrintXxx functions to simplify even more: - // Create a custom print function for convenient - red := color.New(color.FgRed).PrintfFunc() - red("warning") - red("error: %s", err) + // Create a custom print function for convenient + red := color.New(color.FgRed).PrintfFunc() + red("warning") + red("error: %s", err) - // Mix up multiple attributes - notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() - notice("don't forget this...") + // Mix up multiple attributes + notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() + notice("don't forget this...") You can also FprintXxx functions to pass your own io.Writer: - blue := color.New(FgBlue).FprintfFunc() - blue(myWriter, "important notice: %s", stars) - - // Mix up with multiple attributes - success := color.New(color.Bold, color.FgGreen).FprintlnFunc() - success(myWriter, don't forget this...") + blue := color.New(FgBlue).FprintfFunc() + blue(myWriter, "important notice: %s", stars) + // Mix up with multiple attributes + success := color.New(color.Bold, color.FgGreen).FprintlnFunc() + success(myWriter, don't forget this...") Or create SprintXxx functions to mix strings with other non-colorized strings: - yellow := New(FgYellow).SprintFunc() - red := New(FgRed).SprintFunc() + yellow := New(FgYellow).SprintFunc() + red := New(FgRed).SprintFunc() - fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error")) + fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error")) - info := New(FgWhite, BgGreen).SprintFunc() - fmt.Printf("this %s rocks!\n", info("package")) + info := New(FgWhite, BgGreen).SprintFunc() + fmt.Printf("this %s rocks!\n", info("package")) Windows support is enabled by default. All Print functions work as intended. -However only for color.SprintXXX functions, user should use fmt.FprintXXX and +However, only for color.SprintXXX functions, user should use fmt.FprintXXX and set the output to color.Output: - fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) + fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) - info := New(FgWhite, BgGreen).SprintFunc() - fmt.Fprintf(color.Output, "this %s rocks!\n", info("package")) + info := New(FgWhite, BgGreen).SprintFunc() + fmt.Fprintf(color.Output, "this %s rocks!\n", info("package")) Using with existing code is possible. Just use the Set() method to set the standard output to the given parameters. That way a rewrite of an existing code is not required. - // Use handy standard colors. - color.Set(color.FgYellow) + // Use handy standard colors. + color.Set(color.FgYellow) - fmt.Println("Existing text will be now in Yellow") - fmt.Printf("This one %s\n", "too") + fmt.Println("Existing text will be now in Yellow") + fmt.Printf("This one %s\n", "too") - color.Unset() // don't forget to unset + color.Unset() // don't forget to unset - // You can mix up parameters - color.Set(color.FgMagenta, color.Bold) - defer color.Unset() // use it in your function + // You can mix up parameters + color.Set(color.FgMagenta, color.Bold) + defer color.Unset() // use it in your function - fmt.Println("All text will be now bold magenta.") + fmt.Println("All text will be now bold magenta.") There might be a case where you want to disable color output (for example to pipe the standard output of your app to somewhere else). `Color` has support to @@ -112,24 +111,24 @@ disable colors both globally and for single color definition. For example suppose you have a CLI app and a `--no-color` bool flag. You can easily disable the color output with: - var flagNoColor = flag.Bool("no-color", false, "Disable color output") + var flagNoColor = flag.Bool("no-color", false, "Disable color output") - if *flagNoColor { - color.NoColor = true // disables colorized output - } + if *flagNoColor { + color.NoColor = true // disables colorized output + } You can also disable the color by setting the NO_COLOR environment variable to any value. It also has support for single color definitions (local). You can disable/enable color output on the fly: - c := color.New(color.FgCyan) - c.Println("Prints cyan text") + c := color.New(color.FgCyan) + c.Println("Prints cyan text") - c.DisableColor() - c.Println("This is printed without any color") + c.DisableColor() + c.Println("This is printed without any color") - c.EnableColor() - c.Println("This prints again cyan...") + c.EnableColor() + c.Println("This prints again cyan...") */ package color diff --git a/vendor/github.com/godbus/dbus/.travis.yml b/vendor/github.com/godbus/dbus/.travis.yml new file mode 100644 index 00000000..752dc0a0 --- /dev/null +++ b/vendor/github.com/godbus/dbus/.travis.yml @@ -0,0 +1,46 @@ +dist: xenial +language: go +go_import_path: github.com/godbus/dbus +sudo: true + +go: + - 1.7.3 + - 1.8.7 + - 1.9.5 + - 1.10.1 + - tip + +env: + global: + matrix: + - TARGET=amd64 + - TARGET=arm64 + - TARGET=arm + - TARGET=386 + - TARGET=ppc64le + +matrix: + fast_finish: true + allow_failures: + - go: tip + exclude: + - go: tip + env: TARGET=arm + - go: tip + env: TARGET=arm64 + - go: tip + env: TARGET=386 + - go: tip + env: TARGET=ppc64le + +addons: + apt: + packages: + - dbus + - dbus-x11 + +before_install: + +script: + - go test -v -race ./... # Run all the tests with the race detector enabled + - go vet ./... # go vet is the official Go static analyzer diff --git a/vendor/github.com/godbus/dbus/CONTRIBUTING.md b/vendor/github.com/godbus/dbus/CONTRIBUTING.md new file mode 100644 index 00000000..c88f9b2b --- /dev/null +++ b/vendor/github.com/godbus/dbus/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# How to Contribute + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.markdown) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +