build(deps): combine compatible Dependabot updates (#2424)

This commit is contained in:
Joe authored and GitHub committed 2026-10-24 20:29:44 -07:00
1 parent ad6ba0a61b
commit 56e140826f
372 files changed
+25522 -2897

No files matched your search

+55 -55
View File
@@ -23,7 +23,7 @@
"fuse.js": "^7.5.0",
"lunr": "^2.3.9",
"mermaid": "^11.17.2",
"next": "^16.3.8",
"next": "^16.4.1",
"next-themes": "^0.4.6",
"react": "^19.3.0",
"react-dom": "^19.3.0",
@@ -1418,9 +1418,9 @@
}
},
"node_modules/@next/env": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.3.8.tgz",
"integrity": "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.4.1.tgz",
"integrity": "sha512-5sUUrrSvs/2/2LnLoesybMGx12EtPB46fdZ6RhAw0XDGSPxU2LFyQBB7yuEiOFFJPKmBY6+q2jTr1/AMbnp0/g==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -1467,9 +1467,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.3.8.tgz",
"integrity": "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.4.1.tgz",
"integrity": "sha512-U+hQ5Gj3fpzf4fveTIF3dpUFYhaSW8vHhdAnbiIomzXzg7boreYFgBqprepXFtW4sw+aTttiNmg0gew/iFNhbg==",
"cpu": [
"arm64"
],
@@ -1483,9 +1483,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.3.8.tgz",
"integrity": "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.4.1.tgz",
"integrity": "sha512-/hqC3V1XLUjhMASf/WridKdO9FXoEgN3HBWmtKbj00MPgnltutfraWJ3bYTpCRIgEIpbjJyOVLpYgAhh/BkdcQ==",
"cpu": [
"x64"
],
@@ -1499,9 +1499,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.3.8.tgz",
"integrity": "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.4.1.tgz",
"integrity": "sha512-GHzncHbcaS9g95kcvTNtKQW3aCLoR+xc3Jm7qrkpk1R+D2BjMHIPyvQGRPKCdMX95BsH/421jBUK06q9tEwzjw==",
"cpu": [
"arm64"
],
@@ -1518,9 +1518,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.3.8.tgz",
"integrity": "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.4.1.tgz",
"integrity": "sha512-1SuJyDxW/PR9dHJat/ZRgcYLhuSZ+1psP4DlKb2tKIuXbjygqpLrJWRSq+onZQBv8LcmU67EIrmb6c3XTaJLjw==",
"cpu": [
"arm64"
],
@@ -1537,9 +1537,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.3.8.tgz",
"integrity": "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.4.1.tgz",
"integrity": "sha512-Istng1hMGp9TGO4lUqeKLd70MPN/OOkucVTP2Nmj+gLkwBgmPqUw8CJYBCC7nbl/B/23bBCcjCI5Hsh6hazC1A==",
"cpu": [
"x64"
],
@@ -1556,9 +1556,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.3.8.tgz",
"integrity": "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.4.1.tgz",
"integrity": "sha512-aSC9wMNiZRRFVpPkPWBzRwLQxIGm/CiQcXyFF1XNcgodb+uMxdk7aSSGGpDXCbnvZdf4itjHxpOd5h7i7zaiYw==",
"cpu": [
"x64"
],
@@ -1575,9 +1575,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.3.8.tgz",
"integrity": "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.4.1.tgz",
"integrity": "sha512-t/myvwFDlypMUjRvbBf8qLPyuaB0iXYYdBtwMt6DM6f+60wtGr4bSYholdPqNeV5VUXZhrpMwfza829kmhS3iA==",
"cpu": [
"arm64"
],
@@ -1591,9 +1591,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.3.8.tgz",
"integrity": "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.4.1.tgz",
"integrity": "sha512-6ICqPo9awCkrtYNPhwkx1uU7rlFWrZRrIboDHS2LU+jv088UBYQtmJ25h6pCp27/+FZjF6BQ4Te4ErugyMeuBg==",
"cpu": [
"x64"
],
@@ -4898,9 +4898,9 @@
}
},
"node_modules/dompurify": {
"version": "3.4.16",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz",
"integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==",
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz",
"integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -8430,12 +8430,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "16.3.8",
"resolved": "https://registry.npmjs.org/next/-/next-16.3.8.tgz",
"integrity": "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw==",
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/next/-/next-16.4.1.tgz",
"integrity": "sha512-uc657V62E34fOI6W5NtG9IzwsPcms0Z60grz2MJGtglgD+pTFnNt4Yw2PzaU622tfX4vhrDgLZzicE2YlhI20Q==",
"license": "MIT",
"dependencies": {
"@next/env": "16.3.8",
"@next/env": "16.4.1",
"@swc/helpers": "0.5.23",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -8449,15 +8449,15 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.3.8",
"@next/swc-darwin-x64": "16.3.8",
"@next/swc-linux-arm64-gnu": "16.3.8",
"@next/swc-linux-arm64-musl": "16.3.8",
"@next/swc-linux-x64-gnu": "16.3.8",
"@next/swc-linux-x64-musl": "16.3.8",
"@next/swc-win32-arm64-msvc": "16.3.8",
"@next/swc-win32-x64-msvc": "16.3.8",
"sharp": "^0.35.4"
"@next/swc-darwin-arm64": "16.4.1",
"@next/swc-darwin-x64": "16.4.1",
"@next/swc-linux-arm64-gnu": "16.4.1",
"@next/swc-linux-arm64-musl": "16.4.1",
"@next/swc-linux-x64-gnu": "16.4.1",
"@next/swc-linux-x64-musl": "16.4.1",
"@next/swc-win32-arm64-msvc": "16.4.1",
"@next/swc-win32-x64-msvc": "16.4.1",
"sharp": "^0.35.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
@@ -9428,18 +9428,18 @@
}
},
"node_modules/seroval": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.6.tgz",
"integrity": "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==",
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.6.9.tgz",
"integrity": "sha512-UeEzHwAK+yl0YLbsvROV1t5yMYEwZZ0CfKYxDvUgS2fO9Vxdf8djqVIB+OuVpuawRNPqw68LE1Du/GMsW2ZPqA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/seroval-plugins": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.6.tgz",
"integrity": "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==",
"version": "1.6.9",
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.6.9.tgz",
"integrity": "sha512-kiDK/Tb8IWZ61WEU8pA2PDmAuw37gKph9Y77OZlD56mg0Y2g0Bg7gZ1HeHhs5d40w+nRoUq/wj08V26ilbez9g==",
"license": "MIT",
"engines": {
"node": ">=10"
@@ -9673,14 +9673,14 @@
}
},
"node_modules/solid-js": {
"version": "1.9.15",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.15.tgz",
"integrity": "sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg==",
"version": "1.9.17",
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.17.tgz",
"integrity": "sha512-4GAmLOWPfNV8H9Jfn2LSFTpNEG6jiOKPiwd5lnLGQtehHaX6l3TOLzJqQ7mqwB/3k1Yy3zyumFVopmc/tm34ow==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.0",
"seroval": "~1.5.4",
"seroval-plugins": "~1.5.4"
"seroval": "~1.6.8",
"seroval-plugins": "~1.6.8"
}
},
"node_modules/solid-transition-group": {
+2 -2
View File
@@ -27,7 +27,7 @@
"fuse.js": "^7.5.0",
"lunr": "^2.3.9",
"mermaid": "^11.17.2",
"next": "^16.3.8",
"next": "^16.4.1",
"next-themes": "^0.4.6",
"react": "^19.3.0",
"react-dom": "^19.3.0",
@@ -51,7 +51,7 @@
"typescript": "^6.0.3"
},
"overrides": {
"dompurify": "^3.4.16",
"dompurify": "^3.4.17",
"postcss": "^8.5.28",
"sharp": "^0.35.5",
"shell-quote": "^1.12.0",
+16 -16
View File
@@ -10,7 +10,7 @@ replace github.com/reeflective/console => github.com/moloch--/console v0.0.0-202
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.2
charm.land/bubbletea/v2 v2.1.0
charm.land/glamour/v2 v2.0.1
charm.land/huh/v2 v2.0.3
charm.land/lipgloss/v2 v2.0.6
@@ -24,27 +24,29 @@ require (
github.com/atotto/clipboard v0.1.4
github.com/carapace-sh/carapace v1.15.3
github.com/charmbracelet/colorprofile v0.4.3
github.com/charmbracelet/x/ansi v0.11.8
github.com/charmbracelet/x/ansi v0.11.9
github.com/charmbracelet/x/exp/charmtone v0.0.0-20260109001716-2fbdffcb221f
github.com/cheggaaa/pb/v3 v3.1.7
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.1
github.com/ebitengine/purego v0.11.0
github.com/glebarez/sqlite v1.11.0
github.com/go-sql-driver/mysql v1.9.3
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang/protobuf v1.5.4
github.com/gorilla/mux v1.8.1
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/hashicorp/yamux v0.1.2
github.com/jackc/pgx/v5 v5.10.0
github.com/jedib0t/go-pretty/v6 v6.8.3
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
github.com/klauspost/compress v1.18.4
github.com/klauspost/compress v1.18.7
github.com/lesnuages/go-winio v0.4.19
github.com/lesnuages/snitch v0.6.0
github.com/lucasb-eyer/go-colorful v1.4.1
github.com/mark3labs/mcp-go v0.45.0
github.com/mattn/go-sqlite3 v1.14.50
github.com/mattn/go-sqlite3 v1.14.52
github.com/miekg/dns v1.1.70
github.com/moloch--/asciicast v0.1.1
github.com/moloch--/go-keystone v0.0.2
@@ -73,7 +75,7 @@ require (
golang.org/x/crypto v0.55.0
golang.org/x/mod v0.40.0
golang.org/x/net v0.58.0
golang.org/x/sys v0.47.0
golang.org/x/sys v0.49.0
golang.org/x/term v0.45.0
golang.org/x/text v0.41.0
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
@@ -150,7 +152,7 @@ require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 // indirect
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
@@ -187,12 +189,11 @@ require (
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
github.com/go-lark/lark v1.16.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-redis/redis/v8 v8.11.6-0.20220405070650-99c79f7041fc // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
@@ -218,7 +219,6 @@ require (
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jezek/xgb v1.1.1 // indirect
@@ -238,7 +238,7 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.30 // indirect
github.com/mdlayher/netlink v1.8.0 // indirect
github.com/mdlayher/socket v0.5.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
@@ -286,7 +286,7 @@ require (
github.com/utahta/go-linenotify v0.5.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xo/terminfo v1.0.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.8.5 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
@@ -295,11 +295,11 @@ require (
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
+30 -32
View File
@@ -6,8 +6,8 @@ cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/bubbletea/v2 v2.1.0 h1:A9fj9h5LFrvQhQHFNggp8K8rvY6CUfgn9dMbb6p2HIw=
charm.land/bubbletea/v2 v2.1.0/go.mod h1:xWHeyxIX9/wiFU2kgPW04ojOZyTzrWf+8lGjUCaftiU=
charm.land/glamour/v2 v2.0.1 h1:xl+r00A4aJWU0z8fgwKd9fQQ4rsphqGUzuEiXZP5n+c=
charm.land/glamour/v2 v2.0.1/go.mod h1:jo9z8XqVKPeEFMVdvCRLGk++RyJ3CdUwgNr7EvXLw3k=
charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
@@ -179,10 +179,10 @@ github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 h1:hISLzUbwcvJbGoNR6A3L/TpguRRnACBmZOee8LcHLJw=
github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4/go.mod h1:aJDVk+WOVM1K04rdc/71PMPoUGfjxjhvXfP3Pjz1Zio=
github.com/charmbracelet/x/ansi v0.11.9 h1:kIrqLBxtTVbLyyHO2Evoi93EjQo1mylWmpNEsfPxXAM=
github.com/charmbracelet/x/ansi v0.11.9/go.mod h1:DG/hpOnaI2QJ/KMMqLL2h0KVhluuJLazf8973P+Vy5w=
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
@@ -316,8 +316,8 @@ github.com/go-lark/lark v1.16.0 h1:U6BwkLM9wrZedSM7cIiMofganr8PCvJN+M75w2lf2Gg=
github.com/go-lark/lark v1.16.0/go.mod h1:6ltbSztPZRT6IaO9ZIQyVaY5pVp/KeMizDYtfZkU+vM=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
@@ -484,8 +484,8 @@ github.com/kevinburke/twilio-go v0.0.0-20260522200016-74f727d55757 h1:dQyNAHiOY8
github.com/kevinburke/twilio-go v0.0.0-20260522200016-74f727d55757/go.mod h1:Z7bqFOTtIqCCIO+uPtod7Fp3gPMR8aTCcp5Pe8D+3fE=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -528,10 +528,10 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-sqlite3 v1.14.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY=
github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
github.com/mdlayher/netlink v1.8.0 h1:e7XNIYJKD7hUct3Px04RuIGJbBxy1/c4nX7D5YyvvlM=
@@ -552,8 +552,6 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/moloch--/asciicast v0.1.1 h1:btwwniCK9br3ys33XZ71IG/M3ZujSSCcBzsH/sieiXg=
github.com/moloch--/asciicast v0.1.1/go.mod h1:OckO16UDLgxVLclrCnbocL1ix15Br/8Xv/caBoYq98o=
github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701 h1:6caCgDZpovjvv+eclRytFDf0GpVEnjDmoJRHpfkb42E=
github.com/moloch--/console v0.0.0-20260129035459-883dcb25c701/go.mod h1:UdqyqqCSnPu3Zc8Q3kpDszrq1hcyBHsG17hlWlRJmWI=
github.com/moloch--/console v0.0.0-20261002031128-9c7d5fba75f5 h1:BwamAdS8GG5tsxKisLphOOC5RVmTcy8Ixf+oGyhh/Jc=
github.com/moloch--/console v0.0.0-20261002031128-9c7d5fba75f5/go.mod h1:UdqyqqCSnPu3Zc8Q3kpDszrq1hcyBHsG17hlWlRJmWI=
github.com/moloch--/go-keystone v0.0.2 h1:xThbYs45CSzoDUz5kBZxAyTPIpK3t5M7NB1x+NTTRL8=
@@ -763,8 +761,8 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yiya1989/sshkrb5 v0.0.1 h1:oAKpXLBeZExjYDV9eTL2KypmXU+5oPrJtqDuR5JbWic=
github.com/yiya1989/sshkrb5 v0.0.1/go.mod h1:r1bHcNa22x8rrg/Kj/cDF5bX6UtZrHiQgRFvj3Mnpl8=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
@@ -793,20 +791,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
@@ -928,8 +926,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+3
View File
@@ -33,6 +33,9 @@ linters:
generated: lax
presets:
- common-false-positives
settings:
exhaustive:
default-signifies-exhaustive: true
issues:
max-issues-per-linter: 0
max-same-issues: 0
+2 -2
View File
@@ -64,7 +64,7 @@ import will be the Bubble Tea library, which we'll call `tea` for short.
```go
package main
// These imports will be used later on the tutorial. If you save the file
// These imports will be used later in the tutorial. If you save the file
// now, Go might complain they are unused, but that's fine.
// You may also need to run `go mod tidy` to download bubbletea and its
// dependencies.
@@ -397,6 +397,6 @@ of days past.
Part of [Charm](https://charm.sh).
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-softy.jpg" width="400"></a>
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
+26 -2
View File
@@ -1,6 +1,6 @@
# https://taskfile.dev
version: '3'
version: "3"
tasks:
lint:
@@ -11,4 +11,28 @@ tasks:
test:
desc: Run tests
cmds:
- go test ./... {{.CLI_ARGS}}
- go test -race -count 4 -cpu 1,4 ./... {{.CLI_ARGS}}
release:
desc: Create and push a new tag following semver
vars:
NEXT:
sh: svu next --always || go run github.com/caarlos0/svu/v3@latest next --always
prompt: "This will release {{.NEXT}}. Continue?"
preconditions:
- sh: '[ $(git symbolic-ref --short HEAD) = "main" ]'
msg: Not on main branch
- sh: "[ $(git status --porcelain=2 | wc -l) = 0 ]"
msg: "Git is dirty"
- sh: 'gh run list --workflow build.yml --commit $(git rev-parse HEAD) --status success --json conclusion -q ".[0].conclusion" | grep -q success'
msg: "Test build for this commit failed or not present"
cmds:
- task: fetch-tags
- git commit --allow-empty -m "{{.NEXT}}"
- git tag --annotate --sign -m "{{.NEXT}}" {{.NEXT}} {{.CLI_ARGS}}
- echo "Pushing {{.NEXT}}..."
- git push origin main --follow-tags
fetch-tags:
cmds:
- git fetch --tags
+1
View File
@@ -98,6 +98,7 @@ The `tea.View` struct has fields for everything that used to be controlled by op
| `ForegroundColor` | Set the terminal foreground color |
| `BackgroundColor` | Set the terminal background color |
| `ProgressBar` | Show a native terminal progress bar |
| `ProgramStatus` | Report whether the program is working, waiting on the user, done, or failed (OSC 7501) |
| `KeyboardEnhancements` | Request keyboard enhancement features |
| `OnMouse` | Intercept mouse messages based on view content |
+4 -2
View File
@@ -38,7 +38,8 @@ func SetClipboard(s string) Cmd {
type readClipboardMsg struct{}
// ReadClipboard produces a command that reads the system clipboard using OSC52.
// Note that OSC52 is not supported in all terminals.
// Note that OSC52 is not supported in all terminals. Use [WithInput] to enable
// input, since the reply cannot be read otherwise.
func ReadClipboard() Msg {
return readClipboardMsg{}
}
@@ -64,7 +65,8 @@ type readPrimaryClipboardMsg struct{}
// ReadPrimaryClipboard produces a command that reads the primary clipboard
// using OSC52. Primary clipboard selection is a feature present in X11 and
// Wayland only.
// Note that OSC52 is not supported in all terminals.
// Note that OSC52 is not supported in all terminals. Use [WithInput] to enable
// input, since the reply cannot be read otherwise.
func ReadPrimaryClipboard() Msg {
return readPrimaryClipboardMsg{}
}
+8 -4
View File
@@ -10,6 +10,7 @@ import (
type backgroundColorMsg struct{}
// RequestBackgroundColor is a command that requests the terminal background color.
// Use [WithInput] to enable input, since the reply cannot be read otherwise.
func RequestBackgroundColor() Msg {
return backgroundColorMsg{}
}
@@ -18,6 +19,7 @@ func RequestBackgroundColor() Msg {
type foregroundColorMsg struct{}
// RequestForegroundColor is a command that requests the terminal foreground color.
// Use [WithInput] to enable input, since the reply cannot be read otherwise.
func RequestForegroundColor() Msg {
return foregroundColorMsg{}
}
@@ -26,6 +28,7 @@ func RequestForegroundColor() Msg {
type cursorColorMsg struct{}
// RequestCursorColor is a command that requests the terminal cursor color.
// Use [WithInput] to enable input, since the reply cannot be read otherwise.
func RequestCursorColor() Msg {
return cursorColorMsg{}
}
@@ -49,20 +52,21 @@ func (e ForegroundColorMsg) IsDark() bool {
// emitted when the program requests the terminal background color with the
// [RequestBackgroundColor] Cmd.
//
// This is commonly used in [Update.Init] to get the terminal background color
// This is commonly used in [Model.Init] to get the terminal background color
// for style definitions. For that you'll want to call
// [BackgroundColorMsg.IsDark] to determine if the color is dark or light. For
// example:
//
// func (m Model) Init() (Model, Cmd) {
// return m, RequestBackgroundColor()
// func (m model) Init() Cmd {
// return RequestBackgroundColor
// }
//
// func (m Model) Update(msg Msg) (Model, Cmd) {
// func (m model) Update(msg Msg) (Model, Cmd) {
// switch msg := msg.(type) {
// case BackgroundColorMsg:
// m.styles = newStyles(msg.IsDark())
// }
// return m, nil
// }
type BackgroundColorMsg struct{ color.Color }
+3 -3
View File
@@ -9,9 +9,9 @@ import (
//
// Example:
//
// func (m model) Init() (Model, Cmd) {
// return m, tea.Batch(someCommand, someOtherCommand)
// }
// func (m model) Init() tea.Cmd {
// return tea.Batch(someCommand, someOtherCommand)
// }
func Batch(cmds ...Cmd) Cmd {
return compactCmds[BatchMsg](cmds)
}
+143 -26
View File
@@ -33,6 +33,8 @@ type cursedRenderer struct {
mapnl bool
syncdUpdates bool // whether to use synchronized output mode for updates
starting bool // indicates whether the renderer is starting after being stopped
pendingErase bool // an scr.Erase() is pending and hasn't been drained by flush yet
noInput bool // whether input is disabled, in which case keyboard enhancement queries are pointless
}
var _ renderer = &cursedRenderer{}
@@ -55,13 +57,40 @@ func (s *cursedRenderer) setLogger(logger uv.Logger) {
s.mu.Unlock()
}
// setNoInput disables keyboard enhancement requests. When the program runs
// without input, the terminal's response to a keyboard enhancement query
// would arrive after the program has exited and leak into the shell.
func (s *cursedRenderer) setNoInput(noInput bool) {
s.noInput = noInput
}
// resetKeyboardEnhancements writes the sequences that reset keyboard
// enhancement protocols when switching between the main and alt screens.
// modifyOtherKeys has no stack, so it is reset in place; the Kitty keyboard
// stack is popped, but only if we previously pushed an entry (i.e. this is
// not the first render). With input disabled the keyboard protocol is never
// touched.
func (s *cursedRenderer) resetKeyboardEnhancements(buf *bytes.Buffer) {
if s.noInput {
return
}
_, _ = buf.WriteString(ansi.ResetModifyOtherKeys)
if s.lastView != nil {
_, _ = buf.WriteString(ansi.PopKittyKeyboard(1))
}
}
// setOptimizations sets the cursor movement optimizations.
func (s *cursedRenderer) setOptimizations(hardTabs, backspace, mapnl bool) {
s.mu.Lock()
s.hardTabs = hardTabs
s.backspace = backspace
s.mapnl = mapnl
s.scr.SetTabStops(s.width)
if s.hardTabs {
s.scr.SetTabStops(s.width)
} else {
s.scr.SetTabStops(-1)
}
s.scr.SetBackspace(s.backspace)
s.scr.SetMapNewline(s.mapnl)
s.mu.Unlock()
@@ -127,15 +156,19 @@ func (s *cursedRenderer) start() {
if s.lastView.ProgressBar != nil {
setProgressBar(s, s.lastView.ProgressBar)
}
// Enable modifyOtherKeys and Kitty keyboard protocol.
// Both can coexist; terminals ignore what they don't support.
_, _ = s.scr.WriteString(ansi.SetModifyOtherKeys2)
if s.lastView.ProgramStatus != nil {
setProgramStatus(s, nil, s.lastView.ProgramStatus)
}
if !s.noInput {
// Enable modifyOtherKeys and Kitty keyboard protocol.
// Both can coexist; terminals ignore what they don't support.
_, _ = s.scr.WriteString(ansi.SetModifyOtherKeys2)
kittyFlags := ansi.KittyDisambiguateEscapeCodes
if s.lastView.KeyboardEnhancements.ReportEventTypes {
kittyFlags |= ansi.KittyReportEventTypes
kittyFlags := keyboardEnhancementsFlags(s.lastView.KeyboardEnhancements)
// The entry was popped when the renderer was stopped, so push a fresh
// one for the screen we're about to restore.
_, _ = s.scr.WriteString(ansi.PushKittyKeyboard(kittyFlags))
}
_, _ = s.scr.WriteString(ansi.KittyKeyboard(kittyFlags, 1))
}
// close implements renderer.
@@ -152,10 +185,14 @@ func (s *cursedRenderer) close() (err error) {
// two registries for the main and alt screens. We disable keyboard
// enhancements whenever we enter/exit alt screen mode in
// [cursedRenderer.flush].
// Here, we reset the keyboard protocol of the last screen used
// assuming the other screen is already reset when we switched screens.
_, _ = s.buf.WriteString(ansi.ResetModifyOtherKeys)
_, _ = s.buf.WriteString(ansi.KittyKeyboard(0, 1))
// Here, we pop the keyboard protocol of the last screen used
// assuming the other screen is already popped when we switched
// screens. With input disabled we never pushed an entry, so there is
// nothing to pop.
if !s.noInput {
_, _ = s.buf.WriteString(ansi.ResetModifyOtherKeys)
_, _ = s.buf.WriteString(ansi.PopKittyKeyboard(1))
}
// Go to the bottom of the screen.
// We need to go to the bottom of the screen regardless of whether
@@ -211,6 +248,9 @@ func (s *cursedRenderer) close() (err error) {
if lv.ProgressBar != nil && lv.ProgressBar.State != ProgressBarNone {
_, _ = s.scr.WriteString(ansi.ResetProgressBar)
}
// Unlike the progress bar, the program status is left in place: done
// and error reports should outlive the program, and terminals drop
// working and blocked records when the process exits.
}
if s.cellbuf.Method == ansi.GraphemeWidth {
@@ -278,13 +318,19 @@ func (s *cursedRenderer) flush(closing bool) error {
}
}
if !s.starting && !closing && s.lastView != nil && viewEquals(s.lastView, &view) && frameArea == s.cellbuf.Bounds() {
// Restore tab stops if we have tab optimizations enabled.
if s.starting && s.hardTabs {
_, _ = s.scr.WriteString(ansi.SetTabEvery8Columns)
}
if !s.starting && !closing && !s.pendingErase && s.lastView != nil && viewEquals(s.lastView, &view) && frameArea == s.cellbuf.Bounds() {
// No changes, nothing to do.
return nil
}
// We're no longer starting.
s.starting = false
s.pendingErase = false
if frameArea != s.cellbuf.Bounds() {
s.scr.Erase() // Force a full redraw to avoid artifacts.
@@ -369,9 +415,12 @@ func (s *cursedRenderer) flush(closing bool) error {
}
}
// kitty keyboard protocol
if s.lastView == nil || view.KeyboardEnhancements != s.lastView.KeyboardEnhancements ||
view.AltScreen != s.lastView.AltScreen {
// kitty keyboard protocol. Skipped entirely when input is disabled: the
// enhancements only affect keyboard input, and querying the terminal
// would leave its response unconsumed, leaking into the shell after
// the program exits.
if !s.noInput && (s.lastView == nil || view.KeyboardEnhancements != s.lastView.KeyboardEnhancements ||
view.AltScreen != s.lastView.AltScreen) {
// NOTE: We need to reset the keyboard protocol when switching
// between main and alt screen. This is because the specs specify
// two different states for the main and alt screen.
@@ -379,11 +428,20 @@ func (s *cursedRenderer) flush(closing bool) error {
// Enable modifyOtherKeys and Kitty keyboard protocol.
_, _ = s.scr.WriteString(ansi.SetModifyOtherKeys2)
kittyFlags := ansi.KittyDisambiguateEscapeCodes // always enable basic key disambiguation
if view.KeyboardEnhancements.ReportEventTypes {
kittyFlags |= ansi.KittyReportEventTypes
kittyFlags := keyboardEnhancementsFlags(view.KeyboardEnhancements)
if s.lastView == nil || view.AltScreen != s.lastView.AltScreen {
// First render or screen switch: the previous screen's entry
// (if any) is popped below, so push a fresh one for this
// screen.
_, _ = s.scr.WriteString(ansi.PushKittyKeyboard(kittyFlags))
} else {
// Only the flags changed while the same screen stays active.
// Update the topmost stack entry in place instead of popping
// and re-pushing, so a keyboard change doesn't churn the
// stack. Note that this overwrites whatever entry is currently
// on top, which is normally ours.
_, _ = s.scr.WriteString(ansi.KittyKeyboard(kittyFlags, 1))
}
_, _ = s.scr.WriteString(ansi.KittyKeyboard(kittyFlags, 1))
if !closing {
// Request keyboard enhancements when they change
_, _ = s.scr.WriteString(ansi.RequestKittyKeyboard)
@@ -454,6 +512,15 @@ func (s *cursedRenderer) flush(closing bool) error {
setProgressBar(s, view.ProgressBar)
}
// Report the program status if it's changed.
var lps *ProgramStatus
if s.lastView != nil {
lps = s.lastView.ProgramStatus
}
if !programStatusEquals(lps, view.ProgramStatus) {
setProgramStatus(s, lps, view.ProgramStatus)
}
// Render and queue changes to the screen buffer.
s.scr.Render(s.cellbuf.RenderBuffer)
@@ -508,11 +575,10 @@ func (s *cursedRenderer) flush(closing bool) error {
var buf bytes.Buffer
if shouldUpdateAltScreen {
// We always disable keyboard enhancements when switching screens
// We always reset keyboard enhancements when switching screens
// because the terminal is expected to have two different keyboard
// registries for main and alt screens.
_, _ = buf.WriteString(ansi.ResetModifyOtherKeys)
_, _ = buf.WriteString(ansi.KittyKeyboard(0, 1))
s.resetKeyboardEnhancements(&buf)
if view.AltScreen {
// Entering alt screen mode.
buf.WriteString(ansi.SetModeAltScreenSaveCursor)
@@ -593,7 +659,11 @@ func reset(s *cursedRenderer) {
scr.SetColorProfile(s.profile)
scr.SetRelativeCursor(true) // Always start in inline mode
scr.SetFullscreen(false) // Always start in inline mode
scr.SetTabStops(s.width)
if s.hardTabs {
scr.SetTabStops(s.width)
} else {
scr.SetTabStops(-1)
}
scr.SetBackspace(s.backspace)
scr.SetMapNewline(s.mapnl)
scr.SetScrollOptim(runtime.GOOS != "windows") // disable scroll optimization on Windows due to bugs in some terminals
@@ -620,6 +690,7 @@ func (s *cursedRenderer) resize(w, h int) {
s.scr.Erase()
s.width, s.height = w, h
s.scr.Resize(s.width, s.height)
s.pendingErase = true
s.mu.Unlock()
}
@@ -630,6 +701,7 @@ func (s *cursedRenderer) clearScreen() {
// screen redraw.
s.scr.MoveTo(0, 0)
s.scr.Erase()
s.pendingErase = true
s.mu.Unlock()
}
@@ -757,8 +829,14 @@ func (s *cursedRenderer) insertAbove(str string) error {
// onMouse implements renderer.
func (s *cursedRenderer) onMouse(m MouseMsg) Cmd {
if s.lastView != nil && s.lastView.OnMouse != nil {
return s.lastView.OnMouse(m)
var onMouse func(MouseMsg) Cmd
s.mu.Lock()
if s.lastView != nil {
onMouse = s.lastView.OnMouse
}
s.mu.Unlock()
if onMouse != nil {
return onMouse(m)
}
return nil
}
@@ -787,11 +865,33 @@ func setProgressBar(s *cursedRenderer, pb *ProgressBar) {
}
}
func programStatusEquals(a, b *ProgramStatus) bool {
if a == nil || b == nil {
return a == b
}
return *a == *b
}
// setProgramStatus reports cur to the terminal. When cur is nil, or addresses
// a different record than prev, the record of prev is cleared first.
func setProgramStatus(s *cursedRenderer, prev, cur *ProgramStatus) {
if prev != nil && (cur == nil || cur.ID != prev.ID) {
_, _ = s.scr.WriteString(ansi.ClearProgramStatusID(prev.ID))
}
if cur != nil {
_, _ = s.scr.WriteString(ansi.SetProgramStatus(*cur))
}
}
func viewEquals(a, b *View) bool {
if a == nil || b == nil {
return false
}
if !programStatusEquals(a.ProgramStatus, b.ProgramStatus) {
return false
}
if a.Content != b.Content ||
a.AltScreen != b.AltScreen ||
a.DisableBracketedPasteMode != b.DisableBracketedPasteMode ||
@@ -828,3 +928,20 @@ func viewEquals(a, b *View) bool {
return true
}
func keyboardEnhancementsFlags(ke KeyboardEnhancements) int {
flags := 1 // always enable basic key disambiguation
if ke.ReportEventTypes {
flags |= ansi.KittyReportEventTypes
}
if ke.ReportAlternateKeys {
flags |= ansi.KittyReportAlternateKeys
}
if ke.ReportAllKeysAsEscapeCodes {
flags |= ansi.KittyReportAllKeysAsEscapeCodes
}
if ke.ReportAssociatedText {
flags |= ansi.KittyReportAssociatedKeys
}
return flags
}
+1
View File
@@ -23,6 +23,7 @@ type requestCursorPosMsg struct{}
// RequestCursorPosition is a command that requests the cursor position.
// The cursor position will be sent as a [CursorPositionMsg] message.
// Use [WithInput] to enable input, since the reply cannot be read otherwise.
func RequestCursorPosition() Msg {
return requestCursorPosMsg{}
}
+2
View File
@@ -49,6 +49,8 @@ func (p *Program) translateInputEvent(e uv.Event) Msg {
return KeyboardEnhancementsMsg(e)
case uv.ModeReportEvent:
return ModeReportMsg(e)
case uv.ProgramStatusSupportEvent:
return ProgramStatusSupportMsg{}
}
return e
}
+1 -1
View File
@@ -152,7 +152,7 @@ const (
KeyMediaRewind = uv.KeyMediaRewind
KeyMediaNext = uv.KeyMediaNext
KeyMediaPrev = uv.KeyMediaPrev
KeyMediaRecord
KeyMediaRecord = uv.KeyMediaRecord
KeyLowerVol = uv.KeyLowerVol
KeyRaiseVol = uv.KeyRaiseVol
+18
View File
@@ -39,3 +39,21 @@ func (k KeyboardEnhancementsMsg) SupportsKeyDisambiguation() bool {
func (k KeyboardEnhancementsMsg) SupportsEventTypes() bool {
return k.Flags&ansi.KittyReportEventTypes != 0
}
// SupportsAlternateKeys returns whether the terminal supports reporting
// alternate key codes.
func (k KeyboardEnhancementsMsg) SupportsAlternateKeys() bool {
return k.Flags&ansi.KittyReportAlternateKeys != 0
}
// SupportsAllKeysAsEscapeCodes returns whether the terminal supports reporting
// all keys as escape codes.
func (k KeyboardEnhancementsMsg) SupportsAllKeysAsEscapeCodes() bool {
return k.Flags&ansi.KittyReportAllKeysAsEscapeCodes != 0
}
// SupportsAssociatedText returns whether the terminal supports reporting
// associated text with key events.
func (k KeyboardEnhancementsMsg) SupportsAssociatedText() bool {
return k.Flags&ansi.KittyReportAssociatedKeys != 0
}
+1 -1
View File
@@ -38,7 +38,7 @@ const (
MouseBackward = uv.MouseBackward
MouseForward = uv.MouseForward
MouseButton10 = uv.MouseButton10
MouseButton11
MouseButton11 = uv.MouseButton11
)
// MouseMsg represents a mouse message. This is a generic mouse message that
+8 -1
View File
@@ -37,6 +37,13 @@ func WithOutput(output io.Writer) ProgramOption {
// won't need to use this. To disable input entirely pass nil.
//
// p := NewProgram(model, WithInput(nil))
//
// Note that with input disabled, no terminal query is ever sent, since the
// reply could not be read: commands that expect a response, such as
// [RequestBackgroundColor], [RequestCursorPosition], [ReadClipboard], or
// [RequestTerminalVersion], are skipped, as is the startup capability probe.
// A model waiting on the reply message of any of those commands will never
// receive it.
func WithInput(input io.Reader) ProgramOption {
return func(p *Program) {
p.input = input
@@ -54,7 +61,7 @@ func WithInput(input io.Reader) ProgramOption {
// 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)
// p := tea.NewProgram(model, tea.WithEnvironment(environ))
func WithEnvironment(env []string) ProgramOption {
return func(p *Program) {
p.environ = env
+55
View File
@@ -0,0 +1,55 @@
package tea
import uv "github.com/charmbracelet/ultraviolet"
// ProgramStatus is a Program Status Protocol (OSC 7501) report. It tells the
// terminal whether the program is idle, working, waiting on the user, done,
// or failed, and why. Set it on [View.ProgramStatus].
//
// Support depends on the terminal. Terminals that don't support it ignore it.
//
// See: https://www.superlogical.com/rex/docs/build/program-status
type ProgramStatus = uv.ProgramStatus
// ProgramState is the state of a [ProgramStatus].
type ProgramState = uv.ProgramState
// ProgramStatusKind says what a blocked program waits for.
type ProgramStatusKind = uv.ProgramStatusKind
// Program states.
const (
ProgramStateIdle = uv.ProgramStateIdle
ProgramStateWorking = uv.ProgramStateWorking
ProgramStateDone = uv.ProgramStateDone
ProgramStateBlocked = uv.ProgramStateBlocked
ProgramStateError = uv.ProgramStateError
ProgramStateClear = uv.ProgramStateClear
)
// Program status kinds.
const (
ProgramStatusKindPermission = uv.ProgramStatusKindPermission
ProgramStatusKindQuestion = uv.ProgramStatusKindQuestion
ProgramStatusKindAuth = uv.ProgramStatusKindAuth
)
// ProgramStatusSupportMsg is sent when the terminal replies to
// [RequestProgramStatusSupport], meaning it supports the Program Status
// Protocol.
type ProgramStatusSupportMsg struct{}
// programStatusSupportMsg is an internal message that queries the terminal
// for Program Status Protocol support.
type programStatusSupportMsg struct{}
// RequestProgramStatusSupport is a command that asks the terminal whether it
// supports the Program Status Protocol (OSC 7501). A supporting terminal
// replies with a [ProgramStatusSupportMsg]. Terminals that don't support it
// never reply, so pick your own timeout. Use [WithInput] to enable input,
// since the reply cannot be read otherwise.
//
// Detection is optional: reports can be sent without it.
func RequestProgramStatusSupport() Msg {
return programStatusSupportMsg{}
}
+1 -3
View File
@@ -3,9 +3,7 @@ package tea
import "github.com/charmbracelet/x/ansi"
// 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.
// initially and then on every terminal resize.
type WindowSizeMsg struct {
Width int
Height int
+93 -28
View File
@@ -4,9 +4,9 @@
// 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
// A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/main/tutorials
//
// Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/master/examples
// Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/main/examples
package tea
import (
@@ -146,6 +146,20 @@ type View struct {
// progress bar section. Support depends on the terminal.
ProgressBar *ProgressBar
// ProgramStatus when not nil, reports the program status to the terminal
// using the Program Status Protocol (OSC 7501): whether the program is
// idle, working, waiting on the user, done, or failed. Support depends on
// the terminal.
//
// The status usually addresses the root record (empty ID). Changing it
// sends a new report, and setting it back to nil clears the record.
//
// The status is not cleared when the program exits, so done and error
// reports stay visible to the user. Report [ProgramStateDone],
// [ProgramStateError], or [ProgramStateIdle] before quitting rather than
// leaving a working or blocked status behind.
ProgramStatus *ProgramStatus
// AltScreen puts the program in the alternate screen buffer
// (i.e. the program goes into full window mode). Note that the altscreen will
// be automatically exited when the program quits.
@@ -245,6 +259,25 @@ type KeyboardEnhancements struct {
// [KeyPressMsg] with the [Key.IsRepeat] field set indicating that this is
// a it's part of a key repeat sequence.
ReportEventTypes bool
// ReportAlternateKeys requests the terminal to report alternate key values
// in addition to the main ones.
// Note that only key events represented as escape codes will affected by
// this enhancement.
ReportAlternateKeys bool
// ReportAllKeysAsEscapeCodes requests the terminal to report all key
// events, including plain text keys, as escape codes.
// When this is enabled, text won't be sent as plain text but instead as
// escape codes that encode the key value and modifiers.
ReportAllKeysAsEscapeCodes bool
// ReportAssociatedText requests the terminal to report the text associated
// with key events.
// Note that this is an enhancement to
// [KeyboardEnhancements.ReportAllKeysAsEscapeCodes] and only has an effect
// if that is enabled.
ReportAssociatedText bool
}
// SetContent is a helper method to set the content of a [View] with a styled
@@ -298,15 +331,23 @@ const (
ProgressBarWarning
)
// String return a human-readable value for the given [ProgressBarState].
// String returns a human-readable name for the given [ProgressBarState].
// Values outside the known range return "Unknown".
func (s ProgressBarState) String() string {
return [...]string{
"None",
"Default",
"Error",
"Indeterminate",
"Warning",
}[s]
switch s {
case ProgressBarNone:
return "None"
case ProgressBarDefault:
return "Default"
case ProgressBarError:
return "Error"
case ProgressBarIndeterminate:
return "Indeterminate"
case ProgressBarWarning:
return "Warning"
default:
return "Unknown"
}
}
// ProgressBar represents the terminal progress bar.
@@ -317,7 +358,7 @@ func (s ProgressBarState) String() string {
type ProgressBar struct {
// State is the current state of the progress bar. It can be one of
// [ProgressBarNone], [ProgressBarDefault], [ProgressBarError],
// [ProgressBarIndeterminate], and [ProgressBarWarn].
// [ProgressBarIndeterminate], and [ProgressBarWarning].
State ProgressBarState
// Value is the current value of the progress bar. It should be between
// 0 and 100.
@@ -647,11 +688,13 @@ func (p *Program) handleSignals() chan struct{} {
case s := <-sig:
if atomic.LoadUint32(&p.ignoreSignals) == 0 {
switch s {
case syscall.SIGINT:
p.msgs <- InterruptMsg{}
default:
p.msgs <- QuitMsg{}
var msg Msg = QuitMsg{}
if s == syscall.SIGINT {
msg = InterruptMsg{}
}
select {
case p.msgs <- msg:
case <-p.ctx.Done():
}
return
}
@@ -789,35 +832,38 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
}
case readClipboardMsg:
p.execute(ansi.RequestSystemClipboard)
p.executeQuery(ansi.RequestSystemClipboard)
case setClipboardMsg:
p.execute(ansi.SetSystemClipboard(string(msg)))
case readPrimaryClipboardMsg:
p.execute(ansi.RequestPrimaryClipboard)
p.executeQuery(ansi.RequestPrimaryClipboard)
case setPrimaryClipboardMsg:
p.execute(ansi.SetPrimaryClipboard(string(msg)))
case backgroundColorMsg:
p.execute(ansi.RequestBackgroundColor)
p.executeQuery(ansi.RequestBackgroundColor)
case foregroundColorMsg:
p.execute(ansi.RequestForegroundColor)
p.executeQuery(ansi.RequestForegroundColor)
case cursorColorMsg:
p.execute(ansi.RequestCursorColor)
p.executeQuery(ansi.RequestCursorColor)
case programStatusSupportMsg:
p.executeQuery(ansi.RequestProgramStatusSupport)
case execMsg:
// NB: this blocks.
p.exec(msg.cmd, msg.fn)
case terminalVersion:
p.execute(ansi.RequestNameVersion)
p.executeQuery(ansi.RequestNameVersion)
case requestCapabilityMsg:
p.execute(ansi.RequestTermcap(string(msg)))
p.executeQuery(ansi.RequestTermcap(string(msg)))
case BatchMsg:
go p.execBatchMsg(msg)
@@ -834,7 +880,7 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
go p.checkResize()
case requestCursorPosMsg:
p.execute(ansi.RequestCursorPositionReport)
p.executeQuery(ansi.RequestCursorPositionReport)
case RawMsg:
p.execute(fmt.Sprint(msg.Msg))
@@ -1028,7 +1074,7 @@ func (p *Program) Run() (returnModel Model, returnErr error) {
return p.initialModel, fmt.Errorf("bubbletea: error getting terminal size: %w", err)
}
width, height = w, h
width, height = p.fallbackDimensions(w, h)
}
p.width, p.height = width, height
@@ -1046,6 +1092,7 @@ func (p *Program) Run() (returnModel Model, returnErr error) {
p.height,
)
r.setLogger(p.logger)
r.setNoInput(p.disableInput)
// XXX: This breaks many things especially when we want the output
// to be compatible with terminals that are not necessary a TTY.
// This was originally done to work around a Wish emulated-pty
@@ -1091,7 +1138,7 @@ func (p *Program) Run() (returnModel Model, returnErr error) {
// Query for synchronized updates support (mode 2026) and unicode core
// (mode 2027). If the terminal supports it, the renderer will enable
// it once we get the response.
p.execute(ansi.RequestModeSynchronizedOutput +
p.executeQuery(ansi.RequestModeSynchronizedOutput +
ansi.RequestModeUnicodeCore)
}
@@ -1198,6 +1245,22 @@ func (p *Program) execute(seq string) {
p.mu.Unlock()
}
// executeQuery writes a terminal query to the program output. Queries expect a
// reply on p.input, so when input is disabled the query is skipped: the
// program cannot read the response, and the terminal's reply would leak into
// the shell once the program exits (#1590).
//
// This covers every query that expects a reply, not just the startup
// capability probe: background, foreground, and cursor color requests, cursor
// position reports, clipboard reads, and terminal version and termcap
// queries.
func (p *Program) executeQuery(seq string) {
if p.disableInput {
return
}
p.execute(seq)
}
// flush flushes the output buffer to the program output.
func (p *Program) flush() error {
p.mu.Lock()
@@ -1328,8 +1391,10 @@ func (p *Program) RestoreTerminal() error {
if err := p.initTerminal(); err != nil {
return err
}
if err := p.initInputReader(false); err != nil {
return err
if p.input != nil {
if err := p.initInputReader(false); err != nil {
return err
}
}
p.startRenderer()
+2 -1
View File
@@ -5,7 +5,8 @@ package tea
type requestCapabilityMsg string
// RequestCapability is a command that requests the terminal to send its
// Termcap/Terminfo response for the given capability.
// Termcap/Terminfo response for the given capability. Use [WithInput] to
// enable input, since the reply cannot be read otherwise.
//
// Bubble Tea recognizes the following capabilities and will use them to
// upgrade the program's color profile:
+40
View File
@@ -3,6 +3,7 @@ package tea
import (
"fmt"
"os"
"strconv"
"time"
uv "github.com/charmbracelet/ultraviolet"
@@ -104,6 +105,43 @@ func (p *Program) waitForReadLoop() {
}
}
const (
defaultWidth = 80
defaultHeight = 24
)
// fallbackDimensions returns width and height, falling back to COLUMNS/LINES
// environment variables or 80x24 defaults when dimensions are non-positive.
func (p *Program) fallbackDimensions(width, height int) (int, int) {
if width <= 0 {
var cols string
if p != nil && p.environ != nil {
cols = p.environ.Getenv("COLUMNS")
} else {
cols = os.Getenv("COLUMNS")
}
if c, err := strconv.Atoi(cols); err == nil && c > 0 {
width = c
} else {
width = defaultWidth
}
}
if height <= 0 {
var lines string
if p != nil && p.environ != nil {
lines = p.environ.Getenv("LINES")
} else {
lines = os.Getenv("LINES")
}
if l, err := strconv.Atoi(lines); err == nil && l > 0 {
height = l
} else {
height = defaultHeight
}
}
return width, height
}
// checkResize detects the current size of the output and informs the program
// via a WindowSizeMsg.
func (p *Program) checkResize() {
@@ -122,6 +160,8 @@ func (p *Program) checkResize() {
return
}
w, h = p.fallbackDimensions(w, h)
p.width, p.height = w, h
p.Send(WindowSizeMsg{Width: w, Height: h})
}
+1
View File
@@ -40,6 +40,7 @@ const suspendSupported = true
func suspendProcess() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGCONT)
defer signal.Stop(c)
_ = syscall.Kill(0, syscall.SIGTSTP)
// blocks until a CONT happens...
<-c
+2 -1
View File
@@ -16,7 +16,8 @@ type terminalVersion struct{}
// RequestTerminalVersion is a command that queries the terminal for its
// version using XTVERSION. Note that some terminals may not support this
// command.
// command. Use [WithInput] to enable input, since the reply cannot be read
// otherwise.
func RequestTerminalVersion() Msg {
return terminalVersion{}
}
+39 -11
View File
@@ -648,6 +648,11 @@ func TrimSpace(s string) string {
// parts of the screen that have changed.
type RenderBuffer struct {
*Buffer
// Touched records which lines changed since the last render. It holds at
// least one entry per screen row, and a nil entry means that row is
// unchanged. Shortening it is not meaningful, since the rows still exist, so
// the renderer restores the length rather than checking it at every read.
Touched []*LineData
}
@@ -665,14 +670,7 @@ func (b *RenderBuffer) TouchLine(x, y, n int) {
return
}
if y >= len(b.Touched) {
b.Touched = append(b.Touched, make([]*LineData, y-len(b.Touched)+1)...)
}
// Re-check bounds: a concurrent resize may have cleared Touched
if y >= len(b.Touched) {
return
}
b.growTouched()
ch := b.Touched[y]
if ch == nil {
@@ -691,9 +689,6 @@ func (b *RenderBuffer) Touch(x, y int) {
// TouchedLines returns the number of touched lines in the buffer.
func (b *RenderBuffer) TouchedLines() int {
if b.Touched == nil {
return 0
}
count := 0
for _, t := range b.Touched {
if t != nil {
@@ -799,6 +794,22 @@ func (b *RenderBuffer) DeleteCellArea(x, y, n int, c *Cell, area Rectangle) {
b.TouchLine(x, y, n)
}
// Resize resizes the buffer and touches every cell it creates, since the
// screen there still shows whatever the last render left.
func (b *RenderBuffer) Resize(width, height int) {
curWidth, curHeight := b.Width(), b.Height()
b.Buffer.Resize(width, height)
if width > curWidth {
for y := range min(curHeight, height) {
b.TouchLine(curWidth, y, width-curWidth)
}
}
for y := curHeight; y < height; y++ {
b.TouchLine(0, y, width)
}
}
// Clear clears the buffer with space cells and marks all lines as touched.
func (b *RenderBuffer) Clear() {
b.Buffer.Clear()
@@ -832,3 +843,20 @@ func (b *RenderBuffer) FillArea(c *Cell, area Rectangle) {
b.TouchLine(area.Min.X, y, w)
}
}
// growTouched restores the one-entry-per-row invariant, which an application is
// free to break by shortening or dropping the list.
//
// Only ever grows. Truncating to the screen would be the tidier rule, but
// [RenderBuffer.TouchedLines] counts non-nil records rather than touched ones,
// so discarding the entries of a frame that collapsed makes it look untouched
// and skips the erase that clears the rows it gave up. Growing is enough: every
// reader indexes by screen row, and a longer list has an entry for each.
func (b *RenderBuffer) growTouched() {
if len(b.Touched) >= len(b.Lines) {
return
}
touched := make([]*LineData, len(b.Lines))
copy(touched, b.Touched)
b.Touched = touched
}
+5 -1
View File
@@ -854,6 +854,10 @@ func (p *EventDecoder) parseOsc(b []byte) (int, Event) {
sel := ClipboardSelection(parts[0][0]) //nolint:unconvert
return i, ClipboardEvent{Selection: sel, Content: string(bts)}
case 7501:
if data[0] == '?' {
return i, ProgramStatusSupportEvent{}
}
}
return i, UnknownOscEvent(b[:i])
@@ -1761,7 +1765,7 @@ func rgbToHSL(r, g, b uint8) (h, s, l float64) {
} else {
switch Cmax {
case Rnot:
h = 60 * (math.Mod((Gnot-Bnot)/Δ, 6))
h = 60 * math.Mod((Gnot-Bnot)/Δ, 6)
case Gnot:
h = 60 * (((Bnot - Rnot) / Δ) + 2)
case Bnot:
+7
View File
@@ -562,6 +562,13 @@ func (e ClipboardEvent) Clipboard() ClipboardSelection {
return e.Selection
}
// ProgramStatusSupportEvent is emitted when the terminal replies to a Program
// Status Protocol (OSC 7501) feature detection query, meaning the terminal
// supports the protocol.
//
// See: https://www.superlogical.com/rex/docs/build/program-status
type ProgramStatusSupportEvent struct{}
// ignoredEvent represents a sequence event that is ignored by the terminal
// reader. This is used to ignore certain sequences that can be canceled.
type ignoredEvent string
+81 -4
View File
@@ -98,6 +98,53 @@ func (s *StyledString) Bounds() Rectangle {
// printString draws a string starting at the given position. If s is nil, it
// will build and return a slice of [Line]s instead (unwrapped, ignoring bounds).
// passThrough reports whether a zero-width sequence should be carried in a
// cell's content and replayed to the terminal, rather than dropped.
//
// The string-type sequences carry private data rather than driving the cursor:
// APC for image protocols, DCS for device control, SOS and PM for whatever an
// application agrees with its terminal. One means it to reach the terminal, and
// a cell carrying one still advances by its own width, so the renderer's column
// model still holds.
//
// Everything else is dropped, as it was before:
//
// - A CSI can move the cursor or erase part of the screen, and an ESC Fs
// sequence can reset the terminal outright. Replaying one from inside a
// cell would move the real cursor somewhere the model cannot see, which is
// the drift a cell buffer exists to keep out.
// - An OSC is dropped despite carrying data, because a cell is painted again
// every time it changes and on every full repaint. A window title survives
// that, but a clipboard write (OSC 52) or a notification (OSC 9) is not
// something to fire again on each resize. Hyperlinks, the one OSC a cell
// has a place for, are read into [Link] above.
func passThrough[T []byte | string](seq T) bool {
if !ansi.HasApcPrefix(seq) && !ansi.HasDcsPrefix(seq) &&
!ansi.HasSosPrefix(seq) && !ansi.HasPmPrefix(seq) {
return false
}
return terminated(seq)
}
// terminated reports whether a string-type sequence ended with ST.
//
// The parser hands back whatever it has when the input runs out, so a string
// that stops mid-sequence still arrives here. Carrying an unterminated
// introducer into a cell would be worse than dropping it: the terminal would
// swallow everything painted after that cell, looking for an end that never
// comes.
//
// Only the two-byte ST is accepted. The one-byte C1 form, 0x9c, is also a
// UTF-8 continuation byte, so a sequence that ran out partway through a
// character such as U+071C ("\u071c", encoded dc 9c) ends in a byte
// indistinguishable from a terminator. The parser and the terminal need not
// resolve that ambiguity the same way, and guessing wrong reintroduces exactly
// the swallowing this guards against.
func terminated[T []byte | string](seq T) bool {
n := len(seq)
return n >= 2 && seq[n-1] == '\\' && seq[n-2] == ansi.ESC
}
func printString[T []byte | string](
s Screen,
m WidthMethod,
@@ -129,6 +176,7 @@ func printString[T []byte | string](
var link Link
var state byte
lastX, lastY := -1, -1 // last cell written, for folding in combining marks
var pending []byte // pass-through sequences awaiting a cell to ride on
for len(str) > 0 {
seq, width, n, newState := decoder(str, state, p)
// The decoder's ASCII fast path doesn't check for trailing combining
@@ -148,6 +196,15 @@ func printString[T []byte | string](
case width > 0:
cell.Width = width
cell.Content = string(seq)
// Any pass-through sequences seen since the last cell belong in
// front of this glyph, the order they arrived in. Checked rather
// than joined unconditionally, because there are none at all on the
// overwhelmingly common path and the join is not free.
if len(pending) > 0 {
pending = append(pending, cell.Content...)
cell.Content = string(pending)
pending = pending[:0]
}
cell.Style = style
cell.Link = link
@@ -232,8 +289,8 @@ func printString[T []byte | string](
s.SetCell(lastX, lastY, &folded)
}
}
default:
cell.Content += string(seq)
case passThrough(seq):
pending = append(pending, string(seq)...)
}
}
@@ -241,13 +298,31 @@ func printString[T []byte | string](
state = newState
str = str[n:]
if y >= bounds.Max.Y {
if s != nil && y >= bounds.Max.Y {
// We've reached the bottom of the bounds, stop processing further
// lines.
break
}
}
// Pass-through sequences left at the end of the string have no glyph to
// ride in front of, so fold them into the last cell written instead. The
// alternative is a width-0 cell one past the content, which lands outside
// the bounds whenever the string filled them.
if len(pending) > 0 {
if s == nil {
if lastY >= 0 && lastY < len(lines) && lastX >= 0 && lastX < len(lines[lastY]) {
lines[lastY][lastX].Content += string(pending)
}
} else if lastX >= 0 {
if prev := s.CellAt(lastX, lastY); prev != nil {
folded := *prev
folded.Content += string(pending)
s.SetCell(lastX, lastY, &folded)
}
}
}
// Make sure to set the last cell if it's not empty.
if !cell.IsZero() && s != nil {
s.SetCell(x, y, &cell)
@@ -365,7 +440,9 @@ func ReadStyle(params ansi.Params, pen *Style) {
// ReadLink reads a hyperlink escape sequence from a data buffer into link.
func ReadLink(p []byte, link *Link) {
params := bytes.Split(p, []byte{';'})
// OSC 8 sequences have this structure `OSC 8 ; params ; URI ST`.
// Only the first two semicolons are delimiters, semicolons that follow after are part of the URI.
params := bytes.SplitN(p, []byte{';'}, 3)
if len(params) != 3 {
return
}
+16 -2
View File
@@ -191,14 +191,28 @@ func (t *Terminal) Start() error {
}
// input loop
//
// The loop holds its own reference to the reader for its lifetime. Stop
// sets t.pr to nil, and a loop that reads the field on every iteration
// can evaluate it after that has happened and dereference nil. Cancel
// and Close on the reader are what stop this loop; nilling the field is
// only bookkeeping for a later Start.
pr := t.pr
t.errg.Go(func() error {
for {
n, err := t.pr.Read(t.buf)
n, err := pr.Read(t.buf)
if err != nil {
return fmt.Errorf("reading terminal input: %w", err)
}
// Hand the event loop its own copy. t.buf is reused by the
// next Read, and the receiver only copies out of the slice
// after the send has completed, so sending t.buf[:n] lets
// one read's bytes be overwritten by the next before they
// have been consumed.
data := make([]byte, n)
copy(data, t.buf[:n])
select {
case bufc <- t.buf[:n]:
case bufc <- data:
case <-t.donec:
return nil
}
+1 -1
View File
@@ -134,7 +134,7 @@ func (d *TerminalReader) serializeWin32InputRecords(records []xwindows.InputReco
var button MouseButton
alt := mevent.ControlKeyState&(xwindows.LEFT_ALT_PRESSED|xwindows.RIGHT_ALT_PRESSED) != 0
ctrl := mevent.ControlKeyState&(xwindows.LEFT_CTRL_PRESSED|xwindows.RIGHT_CTRL_PRESSED) != 0
shift := mevent.ControlKeyState&(xwindows.SHIFT_PRESSED) != 0
shift := mevent.ControlKeyState&xwindows.SHIFT_PRESSED != 0
wheelDirection := int16(highWord(mevent.ButtonState)) //nolint:gosec
switch mevent.EventFlags {
case 0, xwindows.DOUBLE_CLICK:
+301 -111
View File
@@ -6,6 +6,7 @@ import (
"hash/maphash"
"io"
"strings"
"unicode/utf8"
"github.com/charmbracelet/colorprofile"
"github.com/charmbracelet/x/ansi"
@@ -144,7 +145,11 @@ type TerminalRenderer struct {
clear bool // whether to force clear the screen
caps capabilities // terminal control sequence capabilities
atPhantom bool // whether the cursor is out of bounds and at a phantom cell
lineHadWide bool // whether the line currently being transformed contained a wide cell
noWrapLine bool // whether autowrap is off for the line currently being repainted
driftRows []bool // rows holding a cell the terminal may measure differently
termW, termH int // the size [TerminalRenderer.Resize] was last told
termResized bool // whether the terminal changed size since the last render
damaged []bool // rows this render disturbed itself, see [TerminalRenderer.damage]
logger Logger // The logger used for debugging.
// profile is the color profile to use when downsampling colors. This is
@@ -455,7 +460,13 @@ func (s *TerminalRenderer) move(newbuf *RenderBuffer, x, y int) {
// }
if height > 0 {
if s.cur.Y > height-1 {
// Only clamp the remembered cursor row in fullscreen mode, where the
// buffer covers the whole screen. In relative mode the terminal
// screen extends beyond the frame buffer: after a shrink the
// real cursor can legitimately sit below the new frame's last row,
// and clamping the model here would skip the cursor-up move and leave
// the old frame's top lines on screen.
if !s.flags.Contains(tRelativeCursor) && s.cur.Y > height-1 {
s.cur.Y = height - 1
}
if y > height-1 {
@@ -492,9 +503,33 @@ func cellEqual(a, b *Cell) bool {
}
// putCell draws a cell at the current cursor position.
//
// The lower right corner is written with autowrap off, since a pending wrap
// there scrolls the screen as soon as anything else prints. A multi-codepoint
// cluster is the exception: with autowrap off the terminal never advances past
// the margin and reads the combining codepoints as belonging to the cell to the
// left, so the cluster keeps autowrap on and the wrap is cancelled after.
func (s *TerminalRenderer) putCell(newbuf *RenderBuffer, cell *Cell) {
width, height := newbuf.Width(), newbuf.Height()
if s.flags.Contains(tFullscreen) && s.cur.X == width-1 && s.cur.Y == height-1 {
atMargin := s.cur.X == width-1 && !s.noWrapLine
lowerRight := atMargin && s.flags.Contains(tFullscreen) && s.cur.Y == height-1
// Only the corner cell can care, so the cluster is measured only there.
if lowerRight && cell != nil && utf8.RuneCountInString(cell.Content) > 1 {
s.putAttrCell(newbuf, cell)
// The cluster kept autowrap on so it landed whole, which leaves the
// cursor pending wrap. On the last row there is no next row to wrap
// onto, so the next print scrolls the screen instead. Cancel it here
// rather than leave the frame ending in a state the model denies.
if s.atPhantom {
_ = s.buf.WriteByte('\r')
s.cur.X = 0
s.atPhantom = false
}
return
}
if lowerRight {
s.putCellLR(newbuf, cell)
} else {
s.putAttrCell(newbuf, cell)
@@ -542,10 +577,6 @@ func (s *TerminalRenderer) putAttrCell(newbuf *RenderBuffer, cell *Cell) {
if s.cur.X >= newbuf.Width() {
s.atPhantom = true
}
if cellWidth > 1 {
s.lineHadWide = true
}
}
// putCellLR draws a cell at the lower right corner of the screen.
@@ -565,14 +596,7 @@ func (s *TerminalRenderer) putCellLR(newbuf *RenderBuffer, cell *Cell) {
// updatePen updates the cursor pen styles.
func (s *TerminalRenderer) updatePen(cell *Cell) {
if cell == nil {
if !s.cur.Style.IsZero() {
_, _ = s.buf.WriteString(ansi.ResetStyle)
s.cur.Style = Style{} // Reset style
}
if !s.cur.Link.IsZero() {
_, _ = s.buf.WriteString(ansi.ResetHyperlink())
}
return
cell = &EmptyCell
}
// Downsample pen when we don't have a [colorprofile.TrueColor],
@@ -820,22 +844,47 @@ func lineHasDrift(m ansi.Method, line Line) bool {
if c == nil || c.Width == 0 || len(c.Content) == 0 {
continue
}
if c.Width > 1 || m.StringWidth(c.Content) != ansi.StringWidth(c.Content) {
if c.Width > 1 {
return true
}
// One printable ASCII byte is one column under every width model, so
// it can never be a cell the models disagree about. Worth saying out
// loud because working the width out means segmenting the content,
// and this runs for every cell of every line the renderer diffs.
if len(c.Content) == 1 && c.Content[0] >= 0x20 && c.Content[0] < 0x7f {
continue
}
if m.StringWidth(c.Content) != ansi.StringWidth(c.Content) {
return true
}
}
return false
}
// repaintLine repaints a line from scratch. Unlike
// [TerminalRenderer.transformLine] it does not diff against the previous
// frame: it erases the whole line from column 0 with [ansi.EraseLineRight]
// and then writes the new frame's cells. Erasing from column 0 removes the
// entire line including any wide cells the terminal painted at a different
// width than the model measured, whose real extent the model cannot know.
// The repaint then starts from a known-empty line and an absolute cursor
// position, so the result does not depend on where the previous frame left
// the cursor.
// paintLine brings row y of the screen in line with newbuf. A drift-prone row,
// or any row when force is set, is repainted; every other row is diffed.
func (s *TerminalRenderer) paintLine(newbuf *RenderBuffer, y int, force bool) {
drift := lineHasDrift(s.method, s.curbuf.Line(y)) || lineHasDrift(s.method, newbuf.Line(y))
s.markDrift(y, newbuf.Height(), drift)
switch {
case drift && !s.flags.Contains(tGraphemeWidth):
// Clip a cluster the terminal measures wider than the model instead of
// letting it spill onto the next row, then put the cursor back.
s.noWrapLine = true
_, _ = s.buf.WriteString(ansi.ResetModeAutoWrap)
s.repaintLine(newbuf, y)
s.noWrapLine = false
_, _ = s.buf.WriteString(ansi.SetModeAutoWrap)
s.reanchorWideLine(newbuf)
case drift || force:
s.repaintLine(newbuf, y)
default:
s.transformLine(newbuf, y)
}
}
// repaintLine erases row y from column 0 and writes newbuf's cells, so the
// result does not depend on what the model believes is on screen.
func (s *TerminalRenderer) repaintLine(newbuf *RenderBuffer, y int) {
oldLine := s.curbuf.Line(y)
newLine := newbuf.Line(y)
@@ -846,12 +895,24 @@ func (s *TerminalRenderer) repaintLine(newbuf *RenderBuffer, y int) {
_, _ = s.buf.WriteString(ansi.EraseLineRight)
s.cur.X = 0
// A cell whose width runs past the right margin cannot be shown, so stop
// there. The buffer can hold one after a resize narrowed the line around
// it, and painting it would put a clipped half-glyph on the screen.
width := newbuf.Width()
clipped := width
for x := 0; x < width; x++ {
if c := newLine.At(x); c != nil && !c.isWidePlaceholder() && x+c.Width > width {
clipped = x
break
}
}
// Write the content cells. The line was just erased, so trailing blanks
// need no write, and writing them would risk wrapping past the right
// margin if the terminal painted a wide cell wider than the model
// measured.
last := -1
for x := 0; x < newbuf.Width(); x++ {
for x := 0; x < clipped; x++ {
if c := newLine.At(x); c != nil && !c.isWidePlaceholder() && !cellEqual(c, blank) {
last = x
}
@@ -860,39 +921,40 @@ func (s *TerminalRenderer) repaintLine(newbuf *RenderBuffer, y int) {
s.putCell(newbuf, newLine.At(x))
}
// Adopt the new line as the model of what is on screen.
// Adopt the new line as the model of what is on screen. The clipped tail
// was never painted, so the model records it as blank: claiming otherwise
// would let a later frame skip the cell, and a resize that widens the line
// would leave the half-glyph on screen forever.
if len(oldLine) == len(newLine) {
copy(oldLine, newLine)
for x := clipped; x < width && x < len(oldLine); x++ {
oldLine[x] = Cell{}
}
}
}
// transformLine transforms the given line in the current window to the
// corresponding line in the new window. It uses [ansi.ICH] and [ansi.DCH] to
// insert or delete characters.
// markDrift records whether a row holds a cell the terminal may measure
// differently than the model, so a later width change knows which rows to
// repaint. Recorded here rather than rescanned later, because the diff already
// had to work it out.
func (s *TerminalRenderer) markDrift(y, height int, drift bool) {
if y < 0 {
return
}
if n := max(height, y+1); len(s.driftRows) < n {
s.driftRows = append(s.driftRows, make([]bool, n-len(s.driftRows))...)
}
s.driftRows[y] = drift
}
// transformLine diffs row y against the model and writes only what changed,
// using [ansi.ICH] and [ansi.DCH] to shift cells. It must not see a drift-prone
// row; [TerminalRenderer.paintLine] routes those to a repaint.
func (s *TerminalRenderer) transformLine(newbuf *RenderBuffer, y int) {
var firstCell, oLastCell, nLastCell int // first, old last, new last index
oldLine := s.curbuf.Line(y)
newLine := newbuf.Line(y)
s.lineHadWide = false
defer s.reanchorWideLine(newbuf)
// If either frame's line holds a cell that a cell-level diff cannot
// safely reposition across, repaint the whole line instead. A wide cell
// (width > 1) occupies several columns, so a diff that lands on a
// continuation column splits the character, and per DEC semantics (also
// implemented by ghostty and x/vt) an erase that splits a multi-cell
// character erases the whole character, including a head cell that
// belongs to the new frame. A cell whose width the terminal measures
// differently than the model (an emoji cluster, a keycap) leaves the
// real cursor at a column the model cannot predict, so a later erase in
// the same transform fires at the wrong column. Repainting depends only
// on an absolute cursor position and a known-empty line.
if lineHasDrift(s.method, oldLine) || lineHasDrift(s.method, newLine) {
s.repaintLine(newbuf, y)
return
}
// Find the first changed cell in the line
blank := newLine.At(0)
@@ -1083,20 +1145,39 @@ func (s *TerminalRenderer) transformLine(newbuf *RenderBuffer, y int) {
}
}
// reanchorWideLine re-anchors the cursor with a single absolute horizontal
// move after a line that contained a wide cell. This is a best-effort fallback
// that bounds cursor desync to one line on terminals whose width model
// disagrees with ours. When the terminal negotiated Unicode grapheme width
// (mode 2027) the models agree, so no re-anchor is needed.
// reanchorWideLine re-anchors the cursor after a line that may have left it
// adrift. This is a best-effort fallback that bounds cursor desync to one line
// on terminals whose width model disagrees with ours. When the terminal
// negotiated Unicode grapheme width (mode 2027) the models agree, so no
// re-anchor is needed.
//
// The row is re-anchored along with the column. A terminal that measures a
// cluster wider than the model does runs past the right margin and wraps, so a
// line the model believes fits can leave the real cursor a row further down.
// Recovering the column alone would then leave every following line painted one
// row off, which is the drift this re-anchor exists to contain.
func (s *TerminalRenderer) reanchorWideLine(newbuf *RenderBuffer) {
if !s.lineHadWide || s.flags.Contains(tGraphemeWidth) {
// In relative cursor mode there is no absolute row to move to, so the
// column is all that can be recovered.
if s.flags.Contains(tRelativeCursor) {
if s.atPhantom || s.cur.X < 0 || s.cur.X >= newbuf.Width() {
return
}
_, _ = s.buf.WriteString(ansi.CursorHorizontalAbsolute(s.cur.X + 1))
return
}
s.lineHadWide = false
if s.atPhantom || s.cur.X < 0 || s.cur.X >= newbuf.Width() {
if s.cur.X < 0 || s.cur.Y < 0 {
return
}
_, _ = s.buf.WriteString(ansi.CursorHorizontalAbsolute(s.cur.X + 1))
// A pending wrap is as untrustworthy as the column: the terminal may have
// already taken it. An absolute move cancels it and lands on a column the
// model can name, so clamp into the line and record that.
x := min(s.cur.X, newbuf.Width()-1)
_, _ = s.buf.WriteString(ansi.CursorPosition(x+1, s.cur.Y+1))
s.cur.X = x
s.atPhantom = false
}
// deleteCells deletes the count cells at the current cursor position and moves
@@ -1161,7 +1242,11 @@ func (s *TerminalRenderer) clearBottom(newbuf *RenderBuffer, total int) (top int
}
if top < total {
s.move(newbuf, 0, max(0, top-1)) // top is 1-based
// top is the first row of the trailing blank region, so the
// erase starts there. Starting a row earlier would wipe a row
// this frame did not touch, and the loop that repaints only
// touched rows would never put it back.
s.move(newbuf, 0, top)
s.clearToBottom(blank)
if s.oldhash != nil && s.newhash != nil &&
row < len(s.oldhash) && row < len(s.newhash) {
@@ -1209,7 +1294,7 @@ func (s *TerminalRenderer) clearUpdate(newbuf *RenderBuffer) {
}
nonEmpty = s.clearBottom(newbuf, nonEmpty)
for i := 0; i < nonEmpty && i < newbuf.Height(); i++ {
s.transformLine(newbuf, i)
s.paintLine(newbuf, i, false)
}
}
@@ -1239,6 +1324,97 @@ func (s *TerminalRenderer) Flush() (err error) {
return
}
// sizeChange is what a render owes to the screen or the frame having changed
// shape since the last one.
type sizeChange struct {
// frameResized reports that the frame is a different shape than the model,
// so the model has to be resized before anything is diffed against it.
frameResized bool
// repaintAll requires every row to be painted whatever the model says.
repaintAll bool
}
// reconcileSize decides what a change of size costs, and is the only place that
// decides it.
//
// A terminal that changed size rewrapped what was on it, and a frame that
// changed shape leaves no row holding its old meaning. Either way the model
// cannot be trusted. What differs is how much of the screen is the renderer's to
// put right: fullscreen owns every cell and clears it, while an inline frame
// shares the screen with whatever came before and so repaints its own rows and
// leaves the rest alone.
func (s *TerminalRenderer) reconcileSize(newbuf *RenderBuffer) sizeChange {
newWidth, newHeight := newbuf.Width(), newbuf.Height()
curWidth, curHeight := s.curbuf.Width(), s.curbuf.Height()
fullscreen := s.flags.Contains(tFullscreen)
change := sizeChange{
frameResized: curWidth != newWidth || curHeight != newHeight,
}
if s.termResized {
if fullscreen {
s.clear = true
} else {
change.repaintAll = true
}
s.termResized = false
}
if change.frameResized {
// No row keeps its meaning, so the hashes that describe them are worth
// nothing. Inline uses the narrower partial clear in Render instead.
s.oldhash, s.newhash = nil, nil
if fullscreen {
s.clear = true
}
// Nor does the application's touch list, which never saw rows that
// vanished and came back between two frames.
s.damage(0, newHeight)
}
// The terminal clips a row it measures wider than the model does, and the
// model believes it painted the whole row. Changing the width changes what
// fits, so a row holding a cell the two might measure differently has to be
// painted again. Rows of plain cells are measured the same by everyone, so an
// ASCII screen pays nothing.
if !fullscreen && curWidth != newWidth {
for y, drifts := range s.driftRows {
if drifts {
s.damage(y, 1)
}
}
}
return change
}
// damage marks n rows from y as rows this render disturbed on its own: rows it
// scrolled, rows it erased, rows the terminal is about to measure differently
// than the model does. The diff loop paints a damaged row whether or not the
// application drew into it, because the application has no reason to redraw a
// row it did not change and no way to know the renderer moved it.
func (s *TerminalRenderer) damage(y, n int) {
for i := max(y, 0); i < min(y+n, len(s.damaged)); i++ {
s.damaged[i] = true
}
}
// beginDamage starts a fresh account, sized to the screen this render is about
// to paint. A row outside it cannot be painted, so it cannot be damaged, and
// clamping here is what lets every reader index the record without checking.
// Reset on the way in rather than the way out, so no future exit path has to
// remember to do it. The slice is kept when the size holds, so a steady stream
// of frames allocates nothing.
func (s *TerminalRenderer) beginDamage(height int) {
if len(s.damaged) != height {
s.damaged = make([]bool, height)
return
}
clear(s.damaged)
}
// Redraw forces a full redraw of the screen. It's equivalent to calling
// [TerminalRenderer.Erase] and [TerminalRenderer.Render].
func (s *TerminalRenderer) Redraw(newbuf *RenderBuffer) {
@@ -1249,31 +1425,30 @@ func (s *TerminalRenderer) Redraw(newbuf *RenderBuffer) {
// Render renders changes of the screen to the internal buffer. Call
// [terminalWriter.Flush] to flush pending changes to the screen.
func (s *TerminalRenderer) Render(newbuf *RenderBuffer) {
// Do we need to render anything?
// Do we need to render anything? A latched resize counts: the terminal moved
// content the application cannot know about, so a frame it considers
// unchanged still has to be put back on screen.
touchedLines := newbuf.TouchedLines()
if !s.clear && touchedLines == 0 {
if !s.clear && !s.termResized && touchedLines == 0 {
return
}
// Every reader below indexes the touch list by screen row, so the list has to
// cover the screen before any of them run. Growing here is what lets them do
// it without a length check each.
newbuf.growTouched()
if s.curbuf == nil || s.curbuf.Bounds().Empty() {
// Initialize the current buffer
s.curbuf = NewRenderBuffer(newbuf.Width(), newbuf.Height())
}
newWidth, newHeight := newbuf.Width(), newbuf.Height()
curWidth, curHeight := s.curbuf.Width(), s.curbuf.Height()
curHeight := s.curbuf.Height()
if curWidth != newWidth || curHeight != newHeight {
s.oldhash, s.newhash = nil, nil
// A shrink makes the terminal reflow in emulator-defined ways the
// incremental model cannot predict; force a full repaint. Only in
// fullscreen, where the renderer owns every cell it is about to
// clear. Inline mode shares the screen with whatever came before,
// so it uses the narrower partial clear below instead.
if s.flags.Contains(tFullscreen) && (newWidth < curWidth || newHeight < curHeight) {
s.clear = true
}
}
s.beginDamage(newHeight)
size := s.reconcileSize(newbuf)
// TODO: Investigate whether this is necessary. Theoretically, terminals
// can add/remove tab stops and we should be able to handle that. We could
@@ -1288,29 +1463,37 @@ func (s *TerminalRenderer) Render(newbuf *RenderBuffer) {
var nonEmpty int
// XXX: In inline mode, after a screen resize, we need to clear the extra
// lines at the bottom of the screen. This is because in inline mode, we
// don't use the full screen height and the current buffer size might be
// larger than the new buffer size.
// An inline frame that gives up rows has to erase what it no longer covers,
// or the tail of the taller frame stays on screen below the shorter one.
// Fullscreen has no such rows: it repaints the whole screen instead.
//
// The erase runs from the new last row to the bottom of the screen, so it
// reaches that residue wherever the terminal moved it. A width change at
// the same time is therefore a reason to erase rather than a reason to skip
// it, since a terminal that rewrapped those rows has spread them further
// than the model can account for.
partialClear := !s.flags.Contains(tFullscreen) && s.cur.X != -1 && s.cur.Y != -1 &&
curWidth == newWidth &&
curHeight > 0 &&
curHeight > newHeight
if !s.clear && partialClear {
s.clearBelow(newbuf, nil, newHeight-1)
// From the first row the frame gave up, so the frame's own rows are left
// alone: nothing it still owns needs putting back, and a frame that
// collapsed to nothing starts at its own first row rather than the one
// above, which belongs to whatever shared the screen first.
s.clearBelow(newbuf, nil, newHeight)
}
// Resize the model before diffing so the loop below walks every row
// of the new screen, including rows added by a grow.
if curWidth != newWidth || curHeight != newHeight {
if size.frameResized {
s.curbuf.Resize(newWidth, newHeight)
}
if s.clear { //nolint:nestif
s.clearUpdate(newbuf)
s.clear = false
} else if touchedLines > 0 {
} else if touchedLines > 0 || size.repaintAll {
// On Windows, there's a bug with Windows Terminal where [ansi.DECSTBM]
// misbehaves and moves the cursor outside of the scrolling region. For
// now, we disable the optimizations completely on Windows.
@@ -1329,44 +1512,24 @@ func (s *TerminalRenderer) Render(newbuf *RenderBuffer) {
nonEmpty = s.clearBottom(newbuf, nonEmpty)
for i = 0; i < nonEmpty; i++ {
if newbuf.Touched == nil || i >= len(newbuf.Touched) || (newbuf.Touched[i] != nil &&
// A repaintAll row has to be put back whatever the model says.
if size.repaintAll || s.damaged[i] || (newbuf.Touched[i] != nil &&
(newbuf.Touched[i].FirstCell != -1 || newbuf.Touched[i].LastCell != -1)) {
s.transformLine(newbuf, i)
s.paintLine(newbuf, i, size.repaintAll)
changedLines++
}
// Mark line changed successfully.
if i < len(newbuf.Touched) && i <= newbuf.Height()-1 {
newbuf.Touched[i] = &LineData{
FirstCell: -1, LastCell: -1,
}
}
if i < len(s.curbuf.Touched) && i < s.curbuf.Height()-1 {
s.curbuf.Touched[i] = &LineData{
FirstCell: -1, LastCell: -1,
}
}
}
}
if !s.flags.Contains(tFullscreen) && (curWidth != newWidth || curHeight != newHeight) {
s.move(newbuf, 0, newHeight-1)
if !s.flags.Contains(tFullscreen) && size.frameResized {
s.move(newbuf, 0, max(newHeight-1, 0))
}
// Sync windows and screen
newbuf.Touched = make([]*LineData, newHeight)
for i := range newbuf.Touched {
newbuf.Touched[i] = &LineData{
FirstCell: -1, LastCell: -1,
}
}
for i := range s.curbuf.Touched {
s.curbuf.Touched[i] = &LineData{
FirstCell: -1, LastCell: -1,
}
}
newbuf.growTouched()
resetTouched(newbuf.Touched)
s.updatePen(nil) // nil indicates a blank cell with no styles
s.updatePen(nil)
}
// Erase marks the screen to be fully erased on the next render.
@@ -1385,10 +1548,25 @@ func (s *TerminalRenderer) Erase() {
// -1 there means "first move, assume the origin" rather than "unknown", so
// invalidating would assert a position instead of forgetting one. Keep the
// old model in that mode and let the next render diff against it.
func (s *TerminalRenderer) Resize(width, _ int) {
//
// A resize also forces the next render to repaint, since the screen moves
// content around to fit and the model cannot see where it went. Whether that
// means the whole screen or only the frame's own rows is decided in
// [TerminalRenderer.Render].
func (s *TerminalRenderer) Resize(width, height int) {
if s.tabs != nil {
s.tabs.Resize(width)
}
// Latched against the size last reported, not against the model, because
// Render sees only the size the application draws at: a screen that shrinks
// and grows back between two frames looks unchanged by the time it gets
// there. What the resize costs is decided in Render, which knows the mode.
if width > 0 && height > 0 {
s.termResized = s.termResized || (s.termW > 0 && (width != s.termW || height != s.termH))
s.termW, s.termH = width, height
}
if !s.flags.Contains(tRelativeCursor) {
s.cur.X, s.cur.Y = -1, -1
}
@@ -1724,3 +1902,15 @@ func xtermCaps(termtype string) (v capabilities) {
return v
}
// resetTouched marks every line as untouched, reusing the records already there
// rather than allocating a new one per line on every render.
func resetTouched(touched []*LineData) {
for i, ld := range touched {
if ld == nil {
touched[i] = &LineData{FirstCell: -1, LastCell: -1}
continue
}
ld.FirstCell, ld.LastCell = -1, -1
}
}
@@ -110,6 +110,11 @@ func (s *TerminalRenderer) scrolln(newbuf *RenderBuffer, n, top, bot, maxY int)
s.scrollBuffer(s.curbuf, n, top, bot, blank)
// Every row in the range moved, whether or not the application drew into
// it. A row the scroll already put in the right place costs a comparison
// and no output, which is the whole point of scrolling in hardware.
s.damage(top, bot-top+1)
// shift hash values too, they can be reused
s.scrollOldhash(n, top, bot)
@@ -144,25 +149,6 @@ func (s *TerminalRenderer) scrollBuffer(b *RenderBuffer, n, top, bot int, blank
b.FillArea(blank, Rect(0, line, b.Width(), 1))
}
}
s.touchLine(b, top, bot-top+1, true)
}
// touchLine marks the line as touched.
func (s *TerminalRenderer) touchLine(newbuf *RenderBuffer, y, n int, changed bool) {
height := newbuf.Height()
if n < 0 || y < 0 || y >= height || newbuf.Touched == nil || len(newbuf.Touched) < height {
return // Nothing to touch
}
width := newbuf.Width()
for i := y; i < y+n && i < height && i < len(newbuf.Touched); i++ {
if changed {
newbuf.TouchLine(0, i, width)
} else {
newbuf.Touched[i] = nil
}
}
}
// scrollUp scrolls the screen up by n lines.
+1 -1
View File
@@ -30,7 +30,7 @@ func (s *TerminalRenderer) updateHashmap(newbuf *RenderBuffer) {
if len(s.oldhash) >= height && len(s.newhash) >= height {
// rehash changed lines
for i := range height {
if newbuf.Touched == nil || newbuf.Touched[i] != nil {
if newbuf.Touched[i] != nil {
// TODO: Investigate why this is needed. If we remove this
// line, scroll optimization does not work correctly. This
// should happen else where.
+26 -1
View File
@@ -35,6 +35,7 @@ type TerminalScreen struct {
backgroundColor color.Color
foregroundColor color.Color
progressBar *ProgressBar
programStatus *ProgramStatus
windowTitle string
syncUpdates bool // mode 2026
resetTabs bool // DECST8C - reset terminal tabs on start
@@ -205,7 +206,6 @@ func (s *TerminalScreen) Resize(width, height int) {
s.rbuf.Resize(width, height)
s.rend.Resize(width, height)
s.rend.Erase()
s.rbuf.Touched = nil
}
// Display clears the screen and draws the given [Drawable] onto the terminal
@@ -621,6 +621,24 @@ func (s *TerminalScreen) ProgressBar() *ProgressBar {
return s.progressBar
}
// SetProgramStatus reports the program status to the terminal using the
// Program Status Protocol (OSC 7501). A nil status removes every program
// status record on the terminal.
//
// The changes can be committed to the underlying writer by calling the
// [TerminalScreen.Flush] method.
func (s *TerminalScreen) SetProgramStatus(ps *ProgramStatus) {
EncodeProgramStatus(s.buf, ps) //nolint:errcheck
s.programStatus = ps
}
// ProgramStatus returns the last program status reported to the terminal.
//
// A nil value indicates that no program status is currently set.
func (s *TerminalScreen) ProgramStatus() *ProgramStatus {
return s.programStatus
}
// Reset resets the terminal screen to its default state, clearing the screen,
// switching back to the main screen buffer if necessary, and resetting all
// terminal settings to their defaults.
@@ -674,6 +692,10 @@ func (s *TerminalScreen) Reset() {
if s.progressBar != nil && s.progressBar.State != ProgressBarNone {
sb.WriteString(ansi.ResetProgressBar)
}
// The program status is deliberately left in place. The Program Status
// Protocol expects done and error records to outlive the program, and
// terminals drop working and blocked records on their own when the
// process exits or the next shell prompt starts.
s.buf.WriteString(sb.String())
@@ -742,6 +764,9 @@ func (s *TerminalScreen) Restore() {
if s.progressBar != nil && s.progressBar.State != ProgressBarNone {
EncodeProgressBar(&sb, s.progressBar)
}
if s.programStatus != nil {
EncodeProgramStatus(&sb, s.programStatus) //nolint:errcheck
}
s.buf.WriteString(sb.String())
+47 -1
View File
@@ -25,7 +25,7 @@ type Drawable interface {
// DrawableFunc is a function that implements the [Drawable] interface.
type DrawableFunc func(scr Screen, rect Rectangle)
var _ Drawable = (DrawableFunc)(nil)
var _ Drawable = DrawableFunc(nil)
// Draw implements the [Drawable] interface.
func (f DrawableFunc) Draw(scr Screen, rect Rectangle) {
@@ -382,6 +382,52 @@ func EncodeProgressBar(w io.Writer, pb *ProgressBar) error {
return nil
}
// ProgramStatus is a Program Status Protocol (OSC 7501) report.
//
// See: https://www.superlogical.com/rex/docs/build/program-status
type ProgramStatus = ansi.ProgramStatus
// ProgramState is the state of a [ProgramStatus].
type ProgramState = ansi.ProgramState
// ProgramStatusKind says what a blocked program waits for.
type ProgramStatusKind = ansi.ProgramStatusKind
// Program states.
const (
ProgramStateIdle = ansi.ProgramStateIdle
ProgramStateWorking = ansi.ProgramStateWorking
ProgramStateDone = ansi.ProgramStateDone
ProgramStateBlocked = ansi.ProgramStateBlocked
ProgramStateError = ansi.ProgramStateError
ProgramStateClear = ansi.ProgramStateClear
)
// Program status kinds.
const (
ProgramStatusKindPermission = ansi.ProgramStatusKindPermission
ProgramStatusKindQuestion = ansi.ProgramStatusKindQuestion
ProgramStatusKindAuth = ansi.ProgramStatusKindAuth
)
// EncodeProgramStatus encodes the program status to the given writer. A nil
// status removes every program status record on the terminal.
func EncodeProgramStatus(w io.Writer, ps *ProgramStatus) error {
seq := ansi.ClearProgramStatus
if ps != nil {
seq = ansi.SetProgramStatus(*ps)
if seq == "" {
return fmt.Errorf("invalid program status: state=%q id=%q", ps.State, ps.ID)
}
}
if _, err := io.WriteString(w, seq); err != nil {
return fmt.Errorf("failed to set program status: %w", err)
}
return nil
}
// EncodeKeyboardEnhancements encodes the keyboard enhancements to the given
// writer.
func EncodeKeyboardEnhancements(w io.Writer, ke *KeyboardEnhancements) error {
+234
View File
@@ -0,0 +1,234 @@
package ansi
import (
"encoding/base64"
"strconv"
"strings"
"unicode/utf8"
)
// ProgramState is the state of a program reported with the Program Status
// Protocol (OSC 7501).
//
// See: https://www.superlogical.com/rex/docs/build/program-status
type ProgramState string
// Program states.
const (
// ProgramStateIdle means the program is at rest, waiting for the user's
// next instruction.
ProgramStateIdle ProgramState = "idle"
// ProgramStateWorking means the program is running.
ProgramStateWorking ProgramState = "working"
// ProgramStateDone means the program finished a piece of work and the
// result is ready to look at.
ProgramStateDone ProgramState = "done"
// ProgramStateBlocked means the program cannot continue until the user
// does something.
ProgramStateBlocked ProgramState = "blocked"
// ProgramStateError means the program failed and stopped.
ProgramStateError ProgramState = "error"
// ProgramStateClear removes the addressed record and every record beneath
// it. With no id, it removes every record on the terminal.
ProgramStateClear ProgramState = "clear"
)
// ProgramStatusKind says what a blocked program is waiting for.
type ProgramStatusKind string
// Program status kinds.
const (
// ProgramStatusKindPermission means the program waits for approval to do
// something.
ProgramStatusKindPermission ProgramStatusKind = "permission"
// ProgramStatusKindQuestion means the user must type an answer.
ProgramStatusKindQuestion ProgramStatusKind = "question"
// ProgramStatusKindAuth means the program waits for a login, token, or
// credential.
ProgramStatusKindAuth ProgramStatusKind = "auth"
)
// ProgramStatus is a report of the Program Status Protocol (OSC 7501).
//
// Each report replaces its record completely, so fields such as App and Title
// should be included in every report.
//
// See: https://www.superlogical.com/rex/docs/build/program-status
type ProgramStatus struct {
// State is the program state. Required.
State ProgramState
// ID addresses a record. Empty means the root record. It is a "/"
// separated path of segments matching [A-Za-z0-9_.+-]{1,32}.
ID string
// App is a stable machine-readable program name matching
// [A-Za-z0-9_.+-]{1,32}. Invalid values are omitted.
App string
// Kind says what a blocked program waits for. Only used with
// [ProgramStateBlocked].
Kind ProgramStatusKind
// Progress is a percentage between 0 and 100. Only used with
// [ProgramStateWorking] and [ProgramStateBlocked] when HasProgress is
// true. Otherwise progress is indeterminate.
Progress int
// HasProgress reports whether Progress is set.
HasProgress bool
// Title is a short human-readable label for the record.
Title string
// Message is one human-readable line describing the record.
Message string
}
// Program Status Protocol limits on decoded text.
const (
programStatusMaxTitle = 192
programStatusMaxMessage = 2048
)
// ClearProgramStatus is a sequence that removes every program status record
// on the terminal.
//
// OSC 7501 ; state=clear BEL
//
// See: https://www.superlogical.com/rex/docs/build/program-status
const ClearProgramStatus = "\x1b]7501;state=clear\x07"
// RequestProgramStatusSupport is a sequence that asks the terminal whether it
// supports the Program Status Protocol. A supporting terminal replies with
// OSC 7501 ; ? ST.
//
// OSC 7501 ; ? BEL
//
// See: https://www.superlogical.com/rex/docs/build/program-status
const RequestProgramStatusSupport = "\x1b]7501;?\x07"
// ClearProgramStatusID returns a sequence that removes the record with the
// given id and every record beneath it. An empty id removes every record. It
// returns an empty string if the id is invalid.
//
// OSC 7501 ; state=clear:id=Id BEL
//
// See: https://www.superlogical.com/rex/docs/build/program-status
func ClearProgramStatusID(id string) string {
return SetProgramStatus(ProgramStatus{State: ProgramStateClear, ID: id})
}
// SetProgramStatus returns a sequence that reports a program status using the
// Program Status Protocol (OSC 7501).
//
// OSC 7501 ; key=value:key=value BEL
//
// Terminals discard a whole report when any part of it is invalid, so the
// report is sanitized: control characters in Title and Message are replaced
// with spaces and the text is truncated to the protocol limits, and an invalid
// App, or a Kind or Progress used with the wrong state, is omitted. It returns
// an empty string if the state is unknown or the id is invalid.
//
// See: https://www.superlogical.com/rex/docs/build/program-status
func SetProgramStatus(s ProgramStatus) string {
switch s.State {
case ProgramStateIdle, ProgramStateWorking, ProgramStateDone,
ProgramStateBlocked, ProgramStateError, ProgramStateClear:
default:
return ""
}
if s.ID != "" && !validProgramStatusID(s.ID) {
return ""
}
pairs := []string{"state=" + string(s.State)}
if s.ID != "" {
pairs = append(pairs, "id="+s.ID)
}
if s.State == ProgramStateClear {
return programStatusSequence(pairs)
}
if s.State == ProgramStateBlocked && validProgramStatusKind(s.Kind) {
pairs = append(pairs, "kind="+string(s.Kind))
}
if s.HasProgress && (s.State == ProgramStateWorking || s.State == ProgramStateBlocked) {
pairs = append(pairs, "progress="+strconv.Itoa(min(max(s.Progress, 0), 100)))
}
if validProgramStatusSegment(s.App) {
pairs = append(pairs, "app="+s.App)
}
if t := programStatusText(s.Title, programStatusMaxTitle); t != "" {
pairs = append(pairs, "title="+t)
}
if m := programStatusText(s.Message, programStatusMaxMessage); m != "" {
pairs = append(pairs, "msg="+m)
}
return programStatusSequence(pairs)
}
func programStatusSequence(pairs []string) string {
return "\x1b]7501;" + strings.Join(pairs, ":") + "\x07"
}
func validProgramStatusKind(k ProgramStatusKind) bool {
switch k {
case ProgramStatusKindPermission, ProgramStatusKindQuestion, ProgramStatusKindAuth:
return true
}
return false
}
// programStatusText sanitizes, truncates, and base64 encodes free text.
func programStatusText(s string, limit int) string {
s = strings.ToValidUTF8(s, "\uFFFD")
var sb strings.Builder
space := false
for _, r := range s {
if r < 0x20 || (r >= 0x7f && r <= 0x9f) {
if !space {
sb.WriteByte(' ')
space = true
}
continue
}
space = false
sb.WriteRune(r)
}
s = strings.TrimSpace(sb.String())
if len(s) > limit {
s = s[:limit]
for len(s) > 0 && !utf8.ValidString(s) {
s = s[:len(s)-1]
}
s = strings.TrimSpace(s)
}
if s == "" {
return ""
}
return base64.StdEncoding.EncodeToString([]byte(s))
}
func validProgramStatusID(id string) bool {
if len(id) > 128 {
return false
}
segs := strings.Split(id, "/")
if len(segs) > 8 {
return false
}
for _, seg := range segs {
if !validProgramStatusSegment(seg) {
return false
}
}
return true
}
func validProgramStatusSegment(s string) bool {
if len(s) < 1 || len(s) > 32 {
return false
}
for i := range len(s) {
c := s[i]
if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') &&
c != '_' && c != '.' && c != '+' && c != '-' {
return false
}
}
return true
}
-1
View File
@@ -1,5 +1,4 @@
//go:build !go1.21
// +build !go1.21
/*
Copyright 2019 The logr Authors.
-1
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2019 The logr Authors.
+36 -11
View File
@@ -426,7 +426,7 @@ func (f Formatter) colon() byte {
}
func (f Formatter) pretty(value any) string {
return f.prettyWithFlags(value, 0, 0)
return f.prettyWithFlags(value, 0, 0, 0, nil)
}
const (
@@ -434,7 +434,13 @@ const (
)
// TODO: This is not fast. Most of the overhead goes here.
func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
// value: The value to render
// flags: Bitmask of flags (see above)
// depth: The current depth of nested structs, slices, arrays, and maps
// ptrDepth: The current depth of including pointer dereferences
// ptrMap: A map of pointers already seen, to avoid infinite recursion (usually
// nil unless ptrDepth is large)
func (f Formatter) prettyWithFlags(value any, flags uint32, depth int, ptrDepth int, ptrMap map[uintptr]bool) string {
if depth > f.opts.MaxLogDepth {
return `"<max-log-depth-exceeded>"`
}
@@ -504,7 +510,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
// arbitrary keys might need escaping
buf.WriteString(prettyString(k))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1))
buf.WriteString(f.prettyWithFlags(v[i+1], 0, depth+1, ptrDepth+1, ptrMap))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
@@ -576,7 +582,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
}
printComma = true // if we got here, we are rendering a field
if fld.Anonymous && fld.Type.Kind() == reflect.Struct && name == "" {
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), flags|flagRawStruct, depth+1, ptrDepth+1, ptrMap))
continue
}
if name == "" {
@@ -585,7 +591,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
// field names can't contain characters which need escaping
buf.WriteString(f.quoted(name, false))
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1))
buf.WriteString(f.prettyWithFlags(v.Field(i).Interface(), 0, depth+1, ptrDepth+1, ptrMap))
}
if flags&flagRawStruct == 0 {
buf.WriteByte('}')
@@ -612,7 +618,7 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
buf.WriteByte(f.comma())
}
e := v.Index(i)
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1))
buf.WriteString(f.prettyWithFlags(e.Interface(), 0, depth+1, ptrDepth+1, ptrMap))
}
buf.WriteByte(']')
return buf.String()
@@ -637,7 +643,8 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
keystr = prettyString(keystr)
} else {
// prettyWithFlags will produce already-escaped values
keystr = f.prettyWithFlags(it.Key().Interface(), 0, depth+1)
// key depth is unrelated to overall depth
keystr = f.prettyWithFlags(it.Key().Interface(), 0, 0, ptrDepth, ptrMap)
if t.Key().Kind() != reflect.String {
// JSON only does string keys. Unlike Go's standard JSON, we'll
// convert just about anything to a string.
@@ -646,16 +653,34 @@ func (f Formatter) prettyWithFlags(value any, flags uint32, depth int) string {
}
buf.WriteString(keystr)
buf.WriteByte(f.colon())
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1))
buf.WriteString(f.prettyWithFlags(it.Value().Interface(), 0, depth+1, ptrDepth+1, ptrMap))
i++
}
buf.WriteByte('}')
return buf.String()
case reflect.Ptr, reflect.Interface:
case reflect.Pointer, reflect.Interface:
if v.IsNil() {
return "null"
}
return f.prettyWithFlags(v.Elem().Interface(), 0, depth)
// Special case: recursive pointers. For normal use we do not want to
// count pointer dereferences as depth, but if we see the same pointer
// again we have a recursion and need to stop. After a large number of
// pointer dereferences we will start tracking pointers to avoid the
// perf hit of doing it in the normal path.
//
// This should not happen accidentally (e.g. json decoding should never
// do this) but we can handle it gracefully.
if ptrMap != nil && ptrMap[uintptr(v.Pointer())] {
depth = f.opts.MaxLogDepth + 1 // force a depth error
}
const maxDepthFactor = 4 // arbitrary, but we want it large enough to not false-alert
if ptrDepth > f.opts.MaxLogDepth*maxDepthFactor && ptrMap == nil {
ptrMap = map[uintptr]bool{}
}
if ptrMap != nil {
ptrMap[(uintptr)(v.Pointer())] = true
}
return f.prettyWithFlags(v.Elem().Interface(), 0, depth, ptrDepth+1, ptrMap)
}
return fmt.Sprintf(`"<unhandled-%s>"`, t.Kind().String())
}
@@ -697,7 +722,7 @@ func isEmpty(v reflect.Value) bool {
return v.Float() == 0
case reflect.Complex64, reflect.Complex128:
return v.Complex() == 0
case reflect.Interface, reflect.Ptr:
case reflect.Interface, reflect.Pointer:
return v.IsNil()
}
return false
+16 -6
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
@@ -33,7 +32,7 @@ const extraSlogSinkDepth = 3 // 2 for slog, 1 for SlogSink
func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
kvList := make([]any, 0, 2*record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
kvList = attrToKVs(attr, kvList)
kvList = attrToKVs(attr, kvList, l.opts.MaxLogDepth)
return true
})
@@ -49,7 +48,7 @@ func (l fnlogger) Handle(_ context.Context, record slog.Record) error {
func (l fnlogger) WithAttrs(attrs []slog.Attr) logr.SlogSink {
kvList := make([]any, 0, 2*len(attrs))
for _, attr := range attrs {
kvList = attrToKVs(attr, kvList)
kvList = attrToKVs(attr, kvList, l.opts.MaxLogDepth)
}
l.AddValues(kvList)
return &l
@@ -61,14 +60,25 @@ func (l fnlogger) WithGroup(name string) logr.SlogSink {
}
// attrToKVs appends a slog.Attr to a logr-style kvList. It handle slog Groups
// and other details of slog.
func attrToKVs(attr slog.Attr, kvList []any) []any {
// and other details of slog. maxDepth bounds recursion into nested groups so a
// deeply-nested slog.Group cannot exhaust the stack; it is decremented per group
// level and starts at the Formatter's MaxLogDepth (past which the formatter would
// truncate the rendering anyway).
func attrToKVs(attr slog.Attr, kvList []any, maxDepth int) []any {
attrVal := attr.Value.Resolve()
if attrVal.Kind() == slog.KindGroup {
if maxDepth <= 0 {
// Nesting is too deep to build without risking a stack overflow.
// Stop here; the formatter truncates below MaxLogDepth regardless.
if attr.Key != "" {
kvList = append(kvList, attr.Key, "<max-log-depth-exceeded>")
}
return kvList
}
groupVal := attrVal.Group()
grpKVs := make([]any, 0, 2*len(groupVal))
for _, attr := range groupVal {
grpKVs = attrToKVs(attr, grpKVs)
grpKVs = attrToKVs(attr, grpKVs, maxDepth-1)
}
if attr.Key == "" {
// slog says we have to inline these
-1
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
-1
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
-1
View File
@@ -1,5 +1,4 @@
//go:build go1.21
// +build go1.21
/*
Copyright 2023 The logr Authors.
+1
View File
@@ -1,2 +1,3 @@
* -text
*.bin -text -diff
*.md text eol=lf
+10 -1
View File
@@ -31,6 +31,9 @@ builds:
- mips64le
goarm:
- 7
ignore:
- goos: windows
goarch: arm
-
id: "s2d"
binary: s2d
@@ -57,6 +60,9 @@ builds:
- mips64le
goarm:
- 7
ignore:
- goos: windows
goarch: arm
-
id: "s2sx"
binary: s2sx
@@ -84,6 +90,9 @@ builds:
- mips64le
goarm:
- 7
ignore:
- goos: windows
goarch: arm
archives:
-
@@ -91,7 +100,7 @@ archives:
name_template: "s2-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format_overrides:
- goos: windows
format: zip
formats: ['zip']
files:
- unpack/*
- s2/LICENSE
+700 -693
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -407,8 +407,8 @@ func histogramSplit(b []byte, h []uint16) {
for i, t := range x {
v0 := &h[t]
v1 := &h[y[i]]
v3 := &h[w[i]]
v2 := &h[z[i]]
v3 := &h[w[i]]
*v0++
*v1++
*v2++
-1
View File
@@ -1,5 +1,4 @@
//go:build !amd64
// +build !amd64
package flate
+78 -78
View File
@@ -1,79 +1,79 @@
# Finite State Entropy
This package provides Finite State Entropy encoding and decoding.
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
encoding provides a fast near-optimal symbol encoding/decoding
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
## News
* Feb 2018: First implementation released. Consider this beta software for now.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `(error)` | An internal error occurred. |
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
# Performance
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
beneficial to transpose all your input values down by 64.
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
around 300MB/s decompression speed.
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
# Plans
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
# Finite State Entropy
This package provides Finite State Entropy encoding and decoding.
Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
encoding provides a fast near-optimal symbol encoding/decoding
for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
## News
* Feb 2018: First implementation released. Consider this beta software for now.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `(error)` | An internal error occurred. |
As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
# Performance
A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
All compression functions are currently only running on the calling goroutine so only one core will be used per block.
The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
beneficial to transpose all your input values down by 64.
With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
around 300MB/s decompression speed.
The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
# Plans
At one point, more internals will be exposed to facilitate more "expert" usage of the components.
A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
+89 -89
View File
@@ -1,89 +1,89 @@
# Huff0 entropy compression
This package provides Huff0 encoding and decoding as used in zstd.
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
## News
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
This ensures that most functionality is well tested.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
| `(error)` | An internal error occurred. |
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
## Tables and re-use
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
that controls this behaviour. See the documentation for details. This can be altered between each block.
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
based on the boolean reported back from the CompressXX call.
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
## Decompressing
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
This will initialize the decoding tables.
You can supply the complete block to `ReadTable` and it will return the data part of the block
which can be given to the decompressor.
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
# Huff0 entropy compression
This package provides Huff0 encoding and decoding as used in zstd.
[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
## News
This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
This ensures that most functionality is well tested.
# Usage
This package provides a low level interface that allows to compress single independent blocks.
Each block is separate, and there is no built in integrity checks.
This means that the caller should keep track of block sizes and also do checksums if needed.
Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
You must provide input and will receive the output and maybe an error.
These error values can be returned:
| Error | Description |
|---------------------|-----------------------------------------------------------------------------|
| `<nil>` | Everything ok, output is returned |
| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
| `(error)` | An internal error occurred. |
As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
object can be used for both.
Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
## Tables and re-use
Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
that controls this behaviour. See the documentation for details. This can be altered between each block.
Do however note that this information is *not* stored in the output block and it is up to the users of the package to
record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
based on the boolean reported back from the CompressXX call.
If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
## Decompressing
The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
This will initialize the decoding tables.
You can supply the complete block to `ReadTable` and it will return the data part of the block
which can be given to the decompressor.
Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
your input was likely corrupted.
It is important to note that a successful decoding does *not* mean your output matches your original input.
There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
# Contributing
Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
-1
View File
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
// +build amd64,!appengine,!noasm,gc
// This file contains the specialisation of Decoder.Decompress4X
// and Decoder.Decompress1X that use an asm implementation of thir main loops.
-1
View File
@@ -1,5 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
// +build !amd64 appengine !gc noasm
// This file contains a generic implementation of Decoder.Decompress4X.
package huff0
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
// +build amd64,!appengine,!noasm,gc
package cpuinfo
+1
View File
@@ -78,6 +78,7 @@ func (b *blockEnc) initNewEncode() {
b.recentOffsets = [3]uint32{1, 4, 8}
b.litEnc.Reuse = huff0.ReusePolicyNone
b.coders.setPrev(nil, nil, nil)
b.dictLitEnc = nil
}
// reset will reset the block for a new encode, but in the same stream,
+1 -1
View File
@@ -21,7 +21,7 @@ type fastBase struct {
crc *xxhash.Digest
tmp [8]byte
blk *blockEnc
lastDictID uint32
lastDict *dict
lowMem bool
}
+9 -5
View File
@@ -479,10 +479,13 @@ func (e *bestFastEncoder) Reset(d *dict, singleBlock bool) {
if d == nil {
return
}
dictChanged := d != e.lastDict
// Init or copy dict table
if len(e.dictTable) != len(e.table) || d.id != e.lastDictID {
if len(e.dictTable) != len(e.table) || dictChanged {
if len(e.dictTable) != len(e.table) {
e.dictTable = make([]prevEntry, len(e.table))
} else {
clear(e.dictTable)
}
end := int32(len(d.content)) - 8 + e.maxMatchOff
for i := e.maxMatchOff; i < end; i += 4 {
@@ -510,13 +513,14 @@ func (e *bestFastEncoder) Reset(d *dict, singleBlock bool) {
offset: i + 3,
}
}
e.lastDictID = d.id
}
// Init or copy dict table
if len(e.dictLongTable) != len(e.longTable) || d.id != e.lastDictID {
// Init or copy dict long table
if len(e.dictLongTable) != len(e.longTable) || dictChanged {
if len(e.dictLongTable) != len(e.longTable) {
e.dictLongTable = make([]prevEntry, len(e.longTable))
} else {
clear(e.dictLongTable)
}
if len(d.content) >= 8 {
cv := load6432(d.content, 0)
@@ -538,8 +542,8 @@ func (e *bestFastEncoder) Reset(d *dict, singleBlock bool) {
off++
}
}
e.lastDictID = d.id
}
e.lastDict = d
// Reset table to initial state
copy(e.longTable[:], e.dictLongTable)
+9 -5
View File
@@ -1102,10 +1102,13 @@ func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) {
if d == nil {
return
}
dictChanged := d != e.lastDict
// Init or copy dict table
if len(e.dictTable) != len(e.table) || d.id != e.lastDictID {
if len(e.dictTable) != len(e.table) || dictChanged {
if len(e.dictTable) != len(e.table) {
e.dictTable = make([]tableEntry, len(e.table))
} else {
clear(e.dictTable)
}
end := int32(len(d.content)) - 8 + e.maxMatchOff
for i := e.maxMatchOff; i < end; i += 4 {
@@ -1133,14 +1136,15 @@ func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) {
offset: i + 3,
}
}
e.lastDictID = d.id
e.allDirty = true
}
// Init or copy dict table
if len(e.dictLongTable) != len(e.longTable) || d.id != e.lastDictID {
// Init or copy dict long table
if len(e.dictLongTable) != len(e.longTable) || dictChanged {
if len(e.dictLongTable) != len(e.longTable) {
e.dictLongTable = make([]prevEntry, len(e.longTable))
} else {
clear(e.dictLongTable)
}
if len(d.content) >= 8 {
cv := load6432(d.content, 0)
@@ -1162,9 +1166,9 @@ func (e *betterFastEncoderDict) Reset(d *dict, singleBlock bool) {
off++
}
}
e.lastDictID = d.id
e.allDirty = true
}
e.lastDict = d
// Reset table to initial state
{
+4 -2
View File
@@ -1040,15 +1040,18 @@ func (e *doubleFastEncoder) Reset(d *dict, singleBlock bool) {
// ResetDict will reset and set a dictionary if not nil
func (e *doubleFastEncoderDict) Reset(d *dict, singleBlock bool) {
allDirty := e.allDirty
dictChanged := d != e.lastDict
e.fastEncoderDict.Reset(d, singleBlock)
if d == nil {
return
}
// Init or copy dict table
if len(e.dictLongTable) != len(e.longTable) || d.id != e.lastDictID {
if len(e.dictLongTable) != len(e.longTable) || dictChanged {
if len(e.dictLongTable) != len(e.longTable) {
e.dictLongTable = make([]tableEntry, len(e.longTable))
} else {
clear(e.dictLongTable)
}
if len(d.content) >= 8 {
cv := load6432(d.content, 0)
@@ -1065,7 +1068,6 @@ func (e *doubleFastEncoderDict) Reset(d *dict, singleBlock bool) {
}
}
}
e.lastDictID = d.id
allDirty = true
}
// Reset table to initial state
+4 -2
View File
@@ -805,9 +805,11 @@ func (e *fastEncoderDict) Reset(d *dict, singleBlock bool) {
}
// Init or copy dict table
if len(e.dictTable) != len(e.table) || d.id != e.lastDictID {
if len(e.dictTable) != len(e.table) || d != e.lastDict {
if len(e.dictTable) != len(e.table) {
e.dictTable = make([]tableEntry, len(e.table))
} else {
clear(e.dictTable)
}
if true {
end := e.maxMatchOff + int32(len(d.content)) - 8
@@ -827,7 +829,7 @@ func (e *fastEncoderDict) Reset(d *dict, singleBlock bool) {
}
}
}
e.lastDictID = d.id
e.lastDict = d
e.allDirty = true
}
+13
View File
@@ -138,11 +138,18 @@ func (e *Encoder) Reset(w io.Writer) {
func (e *Encoder) ResetWithOptions(w io.Writer, opts ...EOption) error {
e.o.resetOpt = true
defer func() { e.o.resetOpt = false }()
hadDict := e.o.dict != nil
for _, o := range opts {
if err := o(&e.o); err != nil {
return err
}
}
hasDict := e.o.dict != nil
if hadDict != hasDict {
// Dict presence changed — encoder type must be recreated.
e.state.encoder = nil
e.init = sync.Once{}
}
e.Reset(w)
return nil
}
@@ -448,6 +455,12 @@ func (e *Encoder) Close() error {
if s.encoder == nil {
return nil
}
if s.w == nil {
if len(s.filling) == 0 && !s.headerWritten && !s.eofWritten && s.nInput == 0 {
return nil
}
return errors.New("zstd: encoder has no writer")
}
err := e.nextBlock(true)
if err != nil {
if errors.Is(s.err, ErrEncoderClosed) {
+1
View File
@@ -42,6 +42,7 @@ func (o *encoderOptions) setDefault() {
level: SpeedDefault,
allLitEntropy: false,
lowMem: false,
fullZero: true,
}
}
-1
View File
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
// +build amd64,!appengine,!noasm,gc
package zstd
-1
View File
@@ -1,5 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
// +build !amd64 appengine !gc noasm
package zstd
@@ -1,5 +1,4 @@
//go:build (!amd64 && !arm64) || appengine || !gc || purego || noasm
// +build !amd64,!arm64 appengine !gc purego noasm
package xxhash
-1
View File
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
// +build amd64,!appengine,!noasm,gc
// Copyright 2019+ Klaus Post. All rights reserved.
// License information can be found in the LICENSE file.
-1
View File
@@ -1,5 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
// +build !amd64 appengine !gc noasm
// Copyright 2019+ Klaus Post. All rights reserved.
// License information can be found in the LICENSE file.
-1
View File
@@ -1,5 +1,4 @@
//go:build amd64 && !appengine && !noasm && gc
// +build amd64,!appengine,!noasm,gc
package zstd
-1
View File
@@ -1,5 +1,4 @@
//go:build !amd64 || appengine || !gc || noasm
// +build !amd64 appengine !gc noasm
package zstd
+514 -97
View File
@@ -4,6 +4,8 @@ import (
"os"
"sort"
"strings"
"sync"
"sync/atomic"
"unicode/utf8"
"github.com/clipperhouse/uax29/v2/graphemes"
@@ -18,6 +20,14 @@ var (
// StrictEmojiNeutral should be set false if handle broken fonts
StrictEmojiNeutral bool = true
// ZeroWidthJoiner is flag to set to use UTR#51 ZWJ.
//
// Deprecated: ZWJ sequences are always handled through Unicode
// grapheme cluster segmentation now, so this flag has no effect.
// It is kept only for compatibility with code written against
// v0.0.9 and earlier.
ZeroWidthJoiner bool
// DefaultCondition is a condition in current locale
DefaultCondition = &Condition{
EastAsianWidth: false,
@@ -26,20 +36,87 @@ var (
)
var (
zerowidth table // combining + nonprint merged for faster zero-width lookup
widewidth table // ambiguous + doublewidth merged for EA path
eastAsianWidth widthTable
eastAsianWidth0 [0x300]byte
zerowidth table // combining + nonprint merged for faster zero-width lookup
widewidth table // ambiguous + doublewidth merged for EA path
eastAsianWidth widthTable
tablesOnce sync.Once
// strictWidthLUT is mostly built lazily on the first width lookup so
// that importing the package costs neither the build time nor the 2 MB
// of resident memory; see issue #104. Only the entries below
// strictWidthLUTLimit are valid: init fills the first 0x300 entries of
// both planes, and the lazy build fills the rest — never rewriting the
// low region, so readers of it cannot race with the build. The limit
// is loaded with acquire semantics, which makes the non-atomic reads
// of the high region safe once it reports 0x110000. Keeping the whole
// check down to one compare-and-branch matters: RuneWidth is only a
// dozen instructions long.
strictWidthLUT [2][0x110000]byte
strictWidthLUTLimit atomic.Int32
strictWidthLUTOnce sync.Once
// joinerBits is the joiner table as a bitmap over
// [joinerBase, 0x10000), the range where the per-rune test has to be
// cheap for the fast paths in StringWidth and Wrap to pay off. It is
// 7.6 KB and init fills it in a few microseconds. Runes above it stay
// on the interval search: they are rare in the text these fast paths
// are for, and the emoji that are common there leave the fast path at
// their first joiner anyway.
joinerBits [(0x10000 - joinerBase) / 8]byte
)
// joinerBase is the lowest rune in the joiner table. CR is the only rune
// below it that can form a multi-rune cluster, by pairing with LF.
const joinerBase = 0x300
func init() {
initStrictWidthLUTLow()
fillJoinerBits()
strictWidthLUTLimit.Store(0x300)
handleEnv()
}
// initStrictWidthLUTLow paints the first 0x300 entries of strictWidthLUT
// from the static interval tables. The result must stay identical to
// runeWidthNoLUT for runes below 0x300, which TestStrictWidthLUT verifies.
func initStrictWidthLUTLow() {
for i := 0; i < 0x300; i++ {
r := rune(i)
w := byte(1)
if r < 0x20 || (r >= 0x7F && r <= 0x9F) || r == 0xAD { // nonprint
w = 0
}
strictWidthLUT[0][i] = w
}
ea := strictWidthLUT[1][:0x300]
fillBytes(ea, 1)
paint := func(t table, w byte) {
for _, iv := range t {
if iv.first >= 0x300 {
break
}
last := iv.last
if last > 0x2FF {
last = 0x2FF
}
fillBytes(ea[iv.first:last+1], w)
}
}
paint(ambiguous, 2)
paint(doublewidth, 2)
// zero-width wins over wide on overlap, so paint it last.
paint(combining, 0)
paint(nonprint, 0)
}
// initTables builds the merged lookup tables. It runs lazily through
// tablesOnce so that merely importing the package stays cheap; see issue
// #104.
func initTables() {
zerowidth = mergeIntervals(combining, nonprint)
widewidth = mergeIntervals(ambiguous, doublewidth)
eastAsianWidth = makeWidthTable(zerowidth, widewidth)
for r := range eastAsianWidth0 {
eastAsianWidth0[r] = byte(runeWidthEastAsian(rune(r)))
}
handleEnv()
}
func mergeIntervals(t1, t2 table) table {
@@ -84,7 +161,6 @@ func handleEnv() {
if DefaultCondition.EastAsianWidth != EastAsianWidth {
DefaultCondition.EastAsianWidth = EastAsianWidth
if len(DefaultCondition.combinedLut) > 0 {
DefaultCondition.combinedLut = DefaultCondition.combinedLut[:0]
CreateLUT()
}
}
@@ -189,50 +265,9 @@ func inWidthTable(r rune, t widthTable) (int, bool) {
return 0, false
}
func runeWidthEastAsian(r rune) int {
if w, ok := inWidthTable(r, eastAsianWidth); ok {
return w
}
return 1
}
var private = table{
{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
}
var nonprint = table{
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
}
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
combinedLut []byte
EastAsianWidth bool
StrictEmojiNeutral bool
}
// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
return &Condition{
EastAsianWidth: EastAsianWidth,
StrictEmojiNeutral: StrictEmojiNeutral,
}
}
// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
if r < 0 || r > 0x10FFFF {
return 0
}
if len(c.combinedLut) > 0 {
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
}
// optimized version, verified by TestRuneWidthChecksums()
if !c.EastAsianWidth {
func runeWidthNoLUT(r rune, eastAsian, strictEmojiNeutral bool) int {
tablesOnce.Do(initTables)
if !eastAsian {
if r < 0x20 {
return 0
}
@@ -253,17 +288,150 @@ func (c *Condition) RuneWidth(r rune) int {
}
if r < 0x300 {
return int(eastAsianWidth0[r])
return int(strictWidthLUT[1][r])
}
if w, ok := inWidthTable(r, eastAsianWidth); ok {
return w
}
if !c.StrictEmojiNeutral && inTable(r, emoji) {
if !strictEmojiNeutral && inTable(r, emoji) {
return 2
}
return 1
}
// fillBytes sets every byte of b to v. It doubles the copied region on each
// iteration so large slices are filled at memcpy speed instead of one byte
// per loop iteration.
func fillBytes(b []byte, v byte) {
if len(b) == 0 {
return
}
b[0] = v
for i := 1; i < len(b); i *= 2 {
copy(b[i:], b[:i])
}
}
// buildStrictWidthLUT builds the strict-width lookup table above 0x300
// exactly once. It paints whole intervals instead of computing every rune
// through the binary searches in runeWidthNoLUT. It must not write below
// 0x300: that region was filled by init and may be read concurrently. The
// result must stay identical to runeWidthNoLUT(r, eastAsian, true), which
// TestStrictWidthLUT verifies.
func buildStrictWidthLUT() {
strictWidthLUTOnce.Do(func() {
tablesOnce.Do(initTables)
// paintHigh fills lut with w over each interval, clipped to 0x300+.
paintHigh := func(lut []byte, first, last rune, w byte) {
if first < 0x300 {
if last < 0x300 {
return
}
first = 0x300
}
fillBytes(lut[first:last+1], w)
}
// EastAsianWidth=false, StrictEmojiNeutral=true
lut := strictWidthLUT[0][:]
fillBytes(lut[0x300:], 1)
for _, iv := range doublewidth {
paintHigh(lut, iv.first, iv.last, 2)
}
// zerowidth is checked before doublewidth, so it wins on overlap.
for _, iv := range zerowidth {
paintHigh(lut, iv.first, iv.last, 0)
}
// EastAsianWidth=true, StrictEmojiNeutral=true
lut = strictWidthLUT[1][:]
fillBytes(lut[0x300:], 1)
for _, iv := range eastAsianWidth {
paintHigh(lut, iv.first, iv.last, iv.width)
}
strictWidthLUTLimit.Store(0x110000)
})
}
var private = table{
{0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD},
}
var nonprint = table{
{0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD},
{0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F},
{0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF},
{0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF},
}
// Condition have flag EastAsianWidth whether the current locale is CJK or not.
type Condition struct {
combinedLut []byte
// The flags combinedLut was built from, so that CreateLUT can tell a
// table that is still current from one that has to be rebuilt.
lutEastAsianWidth bool
lutStrictEmojiNeutral bool
EastAsianWidth bool
StrictEmojiNeutral bool
// Deprecated: ZWJ sequences are always handled through Unicode
// grapheme cluster segmentation now, so this flag has no effect.
// It is kept only for compatibility with code written against
// v0.0.9 and earlier.
ZeroWidthJoiner bool
}
// NewCondition return new instance of Condition which is current locale.
func NewCondition() *Condition {
return &Condition{
EastAsianWidth: EastAsianWidth,
StrictEmojiNeutral: StrictEmojiNeutral,
ZeroWidthJoiner: ZeroWidthJoiner,
}
}
// RuneWidth returns the number of cells in r.
// See http://www.unicode.org/reports/tr11/
func (c *Condition) RuneWidth(r rune) int {
// This one compare doubles as the range check and the lazy-LUT check:
// out-of-range runes and runes above the built portion of
// strictWidthLUT both take the slow path. Once the LUT is fully built
// the limit is 0x110000 and only invalid runes go slow.
if uint32(r) >= uint32(strictWidthLUTLimit.Load()) {
return c.runeWidthSlow(r)
}
if len(c.combinedLut) > 0 {
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
}
if c.StrictEmojiNeutral {
if c.EastAsianWidth {
return int(strictWidthLUT[1][r])
}
return int(strictWidthLUT[0][r])
}
return runeWidthNoLUT(r, c.EastAsianWidth, c.StrictEmojiNeutral)
}
func (c *Condition) runeWidthSlow(r rune) int {
if r < 0 || r > 0x10FFFF {
return 0
}
buildStrictWidthLUT()
if len(c.combinedLut) > 0 {
return int(c.combinedLut[r>>1]>>(uint(r&1)*4)) & 3
}
if c.StrictEmojiNeutral {
if c.EastAsianWidth {
return int(strictWidthLUT[1][r])
}
return int(strictWidthLUT[0][r])
}
return runeWidthNoLUT(r, c.EastAsianWidth, c.StrictEmojiNeutral)
}
// CreateLUT will create an in-memory lookup table of 557056 bytes for faster operation.
// This should not be called concurrently with other operations on c.
// If options in c is changed, CreateLUT should be called again.
@@ -271,6 +439,11 @@ func (c *Condition) CreateLUT() {
const max = 0x110000
lut := c.combinedLut
if len(c.combinedLut) != 0 {
if c.lutEastAsianWidth == c.EastAsianWidth && c.lutStrictEmojiNeutral == c.StrictEmojiNeutral {
// The table still matches the flags, so rebuilding it
// would produce the same bytes.
return
}
// Remove so we don't use it.
c.combinedLut = nil
} else {
@@ -283,6 +456,77 @@ func (c *Condition) CreateLUT() {
lut[i] = uint8(x0) | uint8(x1)<<4
}
c.combinedLut = lut
c.lutEastAsianWidth = c.EastAsianWidth
c.lutStrictEmojiNeutral = c.StrictEmojiNeutral
}
// isASCII reports whether s has no byte above 0x7F, in which case every
// grapheme cluster in s is a single byte apart from CRLF.
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] >= 0x80 {
return false
}
}
return true
}
// fillJoinerBits paints joinerBits from the joiner table.
func fillJoinerBits() {
for _, iv := range joiner {
if iv.first >= 0x10000 {
break // the table is sorted, the rest is astral
}
lo, hi := int(iv.first)-joinerBase, int(iv.last)-joinerBase
if hi >= len(joinerBits)*8 {
hi = len(joinerBits)*8 - 1
}
for lo <= hi {
if lo&7 == 0 && lo+7 <= hi {
joinerBits[lo>>3] = 0xFF
lo += 8
continue
}
joinerBits[lo>>3] |= 1 << uint(lo&7)
lo++
}
}
// A byte that is not valid UTF-8 decodes to U+FFFD, and the segmenter
// can gather a run of such bytes into one cluster, which a rune loop
// has no way to see. Marking U+FFFD keeps those strings on the
// segmenter, and the only string it holds back needlessly is one that
// really contains U+FFFD.
i := int(utf8.RuneError) - joinerBase
joinerBits[i>>3] |= 1 << uint(i&7)
}
// isJoiner reports whether r can join with a neighbour into a multi-rune
// grapheme cluster. A string of runes for which it reports false has one
// rune per cluster, so measuring it needs no grapheme segmentation.
func isJoiner(r rune) bool {
if r < joinerBase {
return r == '\r'
}
if r < 0x10000 {
i := r - joinerBase
return joinerBits[i>>3]&(1<<(uint(i)&7)) != 0
}
return inTable(r, joiner)
}
// graphemeWidth returns the width of a single grapheme cluster: the sum of
// the widths of its runes, capped at 2 cells. The cap keeps multi-rune
// sequences that render as a single glyph (ZWJ emoji, flags, Hangul jamo)
// from being counted wider than the two cells terminals give them.
func (c *Condition) graphemeWidth(cluster string) int {
width := 0
for _, r := range cluster {
width += c.RuneWidth(r)
}
if width > 2 {
width = 2
}
return width
}
// StringWidth return width as you can see
@@ -313,38 +557,49 @@ func (c *Condition) StringWidth(s string) (width int) {
return
graphemes:
// Runes first: until one of them can join a cluster, each cluster is a
// single rune and segmenting the string would only find that out the
// expensive way.
if w, ok := c.runeWidthSum(s); ok {
width = w
return
}
width = 0
g := graphemes.FromString(s)
for g.Next() {
var chWidth int
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // Our best guess at this point is to use the width of the first non-zero-width rune.
}
}
width += chWidth
width += c.graphemeWidth(g.Value())
}
return
}
// runeWidthSum adds up the widths of the runes in s, and reports false
// without a total once it meets a rune that can join a cluster, which is
// where per-rune widths stop being the whole story.
func (c *Condition) runeWidthSum(s string) (int, bool) {
width := 0
for _, r := range s {
if isJoiner(r) {
return 0, false
}
width += c.RuneWidth(r)
}
return width, true
}
// Truncate return string truncated with w cells
func (c *Condition) Truncate(s string, w int, tail string) string {
if c.StringWidth(s) <= w {
return s
}
w -= c.StringWidth(tail)
if pos, ok := c.truncateRunes(s, w); ok {
return s[:pos] + tail
}
var width int
pos := len(s)
g := graphemes.FromString(s)
for g.Next() {
var chWidth int
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
}
}
chWidth := c.graphemeWidth(g.Value())
if width+chWidth > w {
pos = g.Start()
break
@@ -354,24 +609,51 @@ func (c *Condition) Truncate(s string, w int, tail string) string {
return s[:pos] + tail
}
// truncateRunes is the loop in Truncate with every rune taken for a whole
// cluster, which holds until a rune can join one. It reports false there
// and leaves the string to the segmenter.
func (c *Condition) truncateRunes(s string, w int) (int, bool) {
width := 0
for i, r := range s {
if isJoiner(r) {
return 0, false
}
cw := c.RuneWidth(r)
if width+cw > w {
return i, true
}
width += cw
}
return len(s), true
}
// endsCluster reports whether the byte at i, which follows a rune that
// cannot join a cluster, also starts one. Cutting there is only safe when
// the rune that follows does not reach back.
func endsCluster(s string, i int) bool {
if i >= len(s) {
return true
}
r, _ := utf8.DecodeRuneInString(s[i:])
return !isJoiner(r)
}
// TruncateLeft cuts w cells from the beginning of the `s`.
func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
if c.StringWidth(s) <= w {
return prefix
}
if pos, pad, ok := c.truncateLeftRunes(s, w); ok {
return prefix + strings.Repeat(" ", pad) + s[pos:]
}
var width int
pos := len(s)
g := graphemes.FromString(s)
for g.Next() {
var chWidth int
for _, r := range g.Value() {
chWidth = c.RuneWidth(r)
if chWidth > 0 {
break // See StringWidth() for details.
}
}
chWidth := c.graphemeWidth(g.Value())
if width+chWidth > w {
if width < w {
@@ -390,27 +672,166 @@ func (c *Condition) TruncateLeft(s string, w int, prefix string) string {
return prefix + s[pos:]
}
// Wrap return string wrapped with w cells
func (c *Condition) Wrap(s string, w int) string {
// truncateLeftRunes is the loop in TruncateLeft with every rune taken for a
// whole cluster, returning the cut and the padding that replaces the cell
// the cut lands inside. It reports false at the first rune that can join a
// cluster.
func (c *Condition) truncateLeftRunes(s string, w int) (pos, pad int, ok bool) {
width := 0
for i, r := range s {
if isJoiner(r) {
return 0, 0, false
}
cw := c.RuneWidth(r)
if width+cw > w {
if width >= w {
return i, 0, true
}
end := i + utf8.RuneLen(r)
if !endsCluster(s, end) {
return 0, 0, false
}
return end, width + cw - w, true
}
width += cw
}
return len(s), 0, true
}
// TruncatePrefix cuts the beginning of `s` so the result fits in w cells, with prefix prepended
func (c *Condition) TruncatePrefix(s string, w int, prefix string) string {
if c.StringWidth(prefix) >= w {
return prefix
}
sw := c.StringWidth(s)
if sw <= w {
return s
}
w -= c.StringWidth(prefix)
if pos, ok := c.truncatePrefixRunes(s, sw, w); ok {
return prefix + s[pos:]
}
var width int
var pos int
g := graphemes.FromString(s)
for g.Next() {
chWidth := c.graphemeWidth(g.Value())
if sw-(width+chWidth) <= w {
pos = g.End()
break
}
width += chWidth
}
return prefix + s[pos:]
}
// truncatePrefixRunes is the loop in TruncatePrefix with every rune taken
// for a whole cluster, reporting false at the first rune that can join one.
func (c *Condition) truncatePrefixRunes(s string, sw, w int) (int, bool) {
width := 0
for i, r := range s {
if isJoiner(r) {
return 0, false
}
cw := c.RuneWidth(r)
if sw-(width+cw) <= w {
end := i + utf8.RuneLen(r)
if !endsCluster(s, end) {
return 0, false
}
return end, true
}
width += cw
}
return 0, true
}
// wrapRunes wraps s treating every rune as its own grapheme cluster, and
// reports false without a result once it meets a rune that can join one.
func (c *Condition) wrapRunes(s string, w int) (string, bool) {
width := 0
var out strings.Builder
out.Grow(len(s) + len(s)/w + 1)
out.Grow(len(s) + len(s)/max(w, 1) + 1)
for _, r := range s {
if isJoiner(r) {
return "", false
}
cw := c.RuneWidth(r)
if r == '\n' {
out.WriteRune(r)
width = 0
continue
} else if width+cw > w {
}
if width+cw > w {
out.WriteByte('\n')
width = 0
out.WriteRune(r)
width += cw
continue
}
out.WriteRune(r)
width += cw
}
return out.String(), true
}
// Wrap return string wrapped with w cells
func (c *Condition) Wrap(s string, w int) string {
// ASCII fast path: no grapheme clustering needed for pure ASCII
if isASCII(s) {
width := 0
var out strings.Builder
// max keeps the capacity hint from dividing by zero when w is 0;
// a non-positive width breaks before every cluster, as it always
// has.
out.Grow(len(s) + len(s)/max(w, 1) + 1)
for i := 0; i < len(s); i++ {
b := s[i]
if b == '\n' {
out.WriteByte(b)
width = 0
continue
}
// Same rule as the StringWidth fast path: no ASCII byte is
// wide or ambiguous, so the flags in c cannot change this.
cw := 0
if b >= 0x20 && b != 0x7F {
cw = 1
}
if width+cw > w {
out.WriteByte('\n')
width = 0
}
out.WriteByte(b)
width += cw
}
return out.String()
}
// Runes first, as in StringWidth. Reaching a rune that can join a
// cluster throws the wrapped text away and starts over on the
// segmenter, which is the uncommon case.
if out, ok := c.wrapRunes(s, w); ok {
return out
}
width := 0
var out strings.Builder
out.Grow(len(s) + len(s)/max(w, 1) + 1)
g := graphemes.FromString(s)
for g.Next() {
cluster := g.Value()
// LF and CRLF are each a single cluster
if strings.HasSuffix(cluster, "\n") {
out.WriteString(cluster)
width = 0
continue
}
cw := c.graphemeWidth(cluster)
if width+cw > w {
out.WriteByte('\n')
width = 0
}
out.WriteString(cluster)
width += cw
}
return out.String()
}
@@ -419,11 +840,7 @@ func (c *Condition) FillLeft(s string, w int) string {
width := c.StringWidth(s)
count := w - width
if count > 0 {
b := make([]byte, count)
for i := range b {
b[i] = ' '
}
return string(b) + s
return strings.Repeat(" ", count) + s
}
return s
}
@@ -433,11 +850,7 @@ func (c *Condition) FillRight(s string, w int) string {
width := c.StringWidth(s)
count := w - width
if count > 0 {
b := make([]byte, count)
for i := range b {
b[i] = ' '
}
return s + string(b)
return s + strings.Repeat(" ", count)
}
return s
}
@@ -478,6 +891,11 @@ func TruncateLeft(s string, w int, prefix string) string {
return DefaultCondition.TruncateLeft(s, w, prefix)
}
// TruncatePrefix cuts the beginning of `s` so the result fits in w cells, with prefix prepended
func TruncatePrefix(s string, w int, prefix string) string {
return DefaultCondition.TruncatePrefix(s, w, prefix)
}
// Wrap return string wrapped with w cells
func Wrap(s string, w int) string {
return DefaultCondition.Wrap(s, w)
@@ -495,9 +913,8 @@ func FillRight(s string, w int) string {
// CreateLUT will create an in-memory lookup table of 557055 bytes for faster operation.
// This should not be called concurrently with other operations.
// If flags in DefaultCondition are changed, CreateLUT should be called again;
// a call that finds the table already current is a no-op.
func CreateLUT() {
if len(DefaultCondition.combinedLut) > 0 {
return
}
DefaultCondition.CreateLUT()
}
+37 -25
View File
@@ -5,35 +5,50 @@ package runewidth
import (
"os"
"regexp"
"strings"
)
var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`)
func mblen(charset string) int {
switch charset {
case "utf-8", "utf8":
return 6
case "jis":
return 8
case "eucjp":
return 3
case "euckr", "euccn", "sjis", "cp932", "cp51932", "cp936", "cp949", "cp950", "big5", "gbk", "gb2312":
return 2
}
return 1
}
var mblenTable = map[string]int{
"utf-8": 6,
"utf8": 6,
"jis": 8,
"eucjp": 3,
"euckr": 2,
"euccn": 2,
"sjis": 2,
"cp932": 2,
"cp51932": 2,
"cp936": 2,
"cp949": 2,
"cp950": 2,
"big5": 2,
"gbk": 2,
"gb2312": 2,
// localeCharset extracts the charset part of a locale name of the form
// "ll.CHARSET" or "ll_CC.CHARSET" (two- or three-letter language code,
// optional uppercase country code). It returns "" if locale does not have
// that shape.
func localeCharset(locale string) string {
n := 0
for n < len(locale) && locale[n] >= 'a' && locale[n] <= 'z' {
n++
}
if n < 2 || n > 3 {
return ""
}
rest := locale[n:]
if len(rest) >= 3 && rest[0] == '_' &&
rest[1] >= 'A' && rest[1] <= 'Z' && rest[2] >= 'A' && rest[2] <= 'Z' {
rest = rest[3:]
}
if len(rest) >= 2 && rest[0] == '.' {
return rest[1:]
}
return ""
}
func isEastAsian(locale string) bool {
charset := strings.ToLower(locale)
r := reLoc.FindStringSubmatch(locale)
if len(r) == 2 {
charset = strings.ToLower(r[1])
if cs := localeCharset(locale); cs != "" {
charset = strings.ToLower(cs)
}
if strings.HasSuffix(charset, "@cjk_narrow") {
@@ -46,10 +61,7 @@ func isEastAsian(locale string) bool {
break
}
}
max := 1
if m, ok := mblenTable[charset]; ok {
max = m
}
max := mblen(charset)
if max > 1 && (charset[0] != 'u' ||
strings.HasPrefix(locale, "ja") ||
strings.HasPrefix(locale, "ko") ||
+238 -17
View File
@@ -3,23 +3,127 @@
package runewidth
var combining = table{
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x07EB, 0x07F3},
{0x0C00, 0x0C00}, {0x0C04, 0x0C04}, {0x0CF3, 0x0CF3},
{0x0D00, 0x0D01}, {0x135D, 0x135F}, {0x180B, 0x180D},
{0x180F, 0x180F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1ADD},
{0x1AE0, 0x1AEB}, {0x1B6B, 0x1B73}, {0x1DC0, 0x1DFF},
{0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2DE0, 0x2DFF},
{0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D},
{0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA8E0, 0xA8F1},
{0xFE00, 0xFE0F}, {0xFE20, 0xFE2F}, {0x101FD, 0x101FD},
{0x10376, 0x1037A}, {0x10EAB, 0x10EAC}, {0x10F46, 0x10F50},
{0x10F82, 0x10F85}, {0x11300, 0x11301}, {0x1133B, 0x1133C},
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x16AF0, 0x16AF4},
{0x1CF00, 0x1CF2D}, {0x1CF30, 0x1CF46}, {0x1D165, 0x1D169},
{0x1D16D, 0x1D172}, {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B},
{0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0x1E000, 0x1E006},
{0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024},
{0x1E026, 0x1E02A}, {0x1E08F, 0x1E08F}, {0x1E8D0, 0x1E8D6},
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x0591, 0x05BD},
{0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5},
{0x05C7, 0x05C7}, {0x0610, 0x061A}, {0x064B, 0x065F},
{0x0670, 0x0670}, {0x06D6, 0x06DC}, {0x06DF, 0x06E4},
{0x06E7, 0x06E8}, {0x06EA, 0x06ED}, {0x0711, 0x0711},
{0x0730, 0x074A}, {0x07A6, 0x07B0}, {0x07EB, 0x07F3},
{0x07FD, 0x07FD}, {0x0816, 0x0819}, {0x081B, 0x0823},
{0x0825, 0x0827}, {0x0829, 0x082D}, {0x0859, 0x085B},
{0x0897, 0x089F}, {0x08CA, 0x08E1}, {0x08E3, 0x0902},
{0x093A, 0x093A}, {0x093C, 0x093C}, {0x0941, 0x0948},
{0x094D, 0x094D}, {0x0951, 0x0957}, {0x0962, 0x0963},
{0x0981, 0x0981}, {0x09BC, 0x09BC}, {0x09C1, 0x09C4},
{0x09CD, 0x09CD}, {0x09E2, 0x09E3}, {0x09FE, 0x09FE},
{0x0A01, 0x0A02}, {0x0A3C, 0x0A3C}, {0x0A41, 0x0A42},
{0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51},
{0x0A70, 0x0A71}, {0x0A75, 0x0A75}, {0x0A81, 0x0A82},
{0x0ABC, 0x0ABC}, {0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8},
{0x0ACD, 0x0ACD}, {0x0AE2, 0x0AE3}, {0x0AFA, 0x0AFF},
{0x0B01, 0x0B01}, {0x0B3C, 0x0B3C}, {0x0B3F, 0x0B3F},
{0x0B41, 0x0B44}, {0x0B4D, 0x0B4D}, {0x0B55, 0x0B56},
{0x0B62, 0x0B63}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0},
{0x0BCD, 0x0BCD}, {0x0C00, 0x0C00}, {0x0C04, 0x0C04},
{0x0C3C, 0x0C3C}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48},
{0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0C62, 0x0C63},
{0x0C81, 0x0C81}, {0x0CBC, 0x0CBC}, {0x0CBF, 0x0CBF},
{0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD}, {0x0CE2, 0x0CE3},
{0x0CF3, 0x0CF3}, {0x0D00, 0x0D01}, {0x0D3B, 0x0D3C},
{0x0D41, 0x0D44}, {0x0D4D, 0x0D4D}, {0x0D62, 0x0D63},
{0x0D81, 0x0D81}, {0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4},
{0x0DD6, 0x0DD6}, {0x0E31, 0x0E31}, {0x0E34, 0x0E3A},
{0x0E47, 0x0E4E}, {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EBC},
{0x0EC8, 0x0ECE}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35},
{0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F71, 0x0F7E},
{0x0F80, 0x0F84}, {0x0F86, 0x0F87}, {0x0F8D, 0x0F97},
{0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030},
{0x1032, 0x1037}, {0x1039, 0x103A}, {0x103D, 0x103E},
{0x1058, 0x1059}, {0x105E, 0x1060}, {0x1071, 0x1074},
{0x1082, 0x1082}, {0x1085, 0x1086}, {0x108D, 0x108D},
{0x109D, 0x109D}, {0x135D, 0x135F}, {0x1712, 0x1714},
{0x1732, 0x1733}, {0x1752, 0x1753}, {0x1772, 0x1773},
{0x17B4, 0x17B5}, {0x17B7, 0x17BD}, {0x17C6, 0x17C6},
{0x17C9, 0x17D3}, {0x17DD, 0x17DD}, {0x180B, 0x180D},
{0x180F, 0x180F}, {0x1885, 0x1886}, {0x18A9, 0x18A9},
{0x1920, 0x1922}, {0x1927, 0x1928}, {0x1932, 0x1932},
{0x1939, 0x193B}, {0x1A17, 0x1A18}, {0x1A1B, 0x1A1B},
{0x1A56, 0x1A56}, {0x1A58, 0x1A5E}, {0x1A60, 0x1A60},
{0x1A62, 0x1A62}, {0x1A65, 0x1A6C}, {0x1A73, 0x1A7C},
{0x1A7F, 0x1A7F}, {0x1AB0, 0x1ADD}, {0x1AE0, 0x1AEB},
{0x1B00, 0x1B03}, {0x1B34, 0x1B34}, {0x1B36, 0x1B3A},
{0x1B3C, 0x1B3C}, {0x1B42, 0x1B42}, {0x1B6B, 0x1B73},
{0x1B80, 0x1B81}, {0x1BA2, 0x1BA5}, {0x1BA8, 0x1BA9},
{0x1BAB, 0x1BAD}, {0x1BE6, 0x1BE6}, {0x1BE8, 0x1BE9},
{0x1BED, 0x1BED}, {0x1BEF, 0x1BF1}, {0x1C2C, 0x1C33},
{0x1C36, 0x1C37}, {0x1CD0, 0x1CD2}, {0x1CD4, 0x1CE0},
{0x1CE2, 0x1CE8}, {0x1CED, 0x1CED}, {0x1CF4, 0x1CF4},
{0x1CF8, 0x1CF9}, {0x1DC0, 0x1DFF}, {0x20D0, 0x20F0},
{0x2CEF, 0x2CF1}, {0x2D7F, 0x2D7F}, {0x2DE0, 0x2DFF},
{0x302A, 0x302D}, {0x3099, 0x309A}, {0xA66F, 0xA672},
{0xA674, 0xA67D}, {0xA69E, 0xA69F}, {0xA6F0, 0xA6F1},
{0xA802, 0xA802}, {0xA806, 0xA806}, {0xA80B, 0xA80B},
{0xA825, 0xA826}, {0xA82C, 0xA82C}, {0xA8C4, 0xA8C5},
{0xA8E0, 0xA8F1}, {0xA8FF, 0xA8FF}, {0xA926, 0xA92D},
{0xA947, 0xA951}, {0xA980, 0xA982}, {0xA9B3, 0xA9B3},
{0xA9B6, 0xA9B9}, {0xA9BC, 0xA9BD}, {0xA9E5, 0xA9E5},
{0xAA29, 0xAA2E}, {0xAA31, 0xAA32}, {0xAA35, 0xAA36},
{0xAA43, 0xAA43}, {0xAA4C, 0xAA4C}, {0xAA7C, 0xAA7C},
{0xAAB0, 0xAAB0}, {0xAAB2, 0xAAB4}, {0xAAB7, 0xAAB8},
{0xAABE, 0xAABF}, {0xAAC1, 0xAAC1}, {0xAAEC, 0xAAED},
{0xAAF6, 0xAAF6}, {0xABE5, 0xABE5}, {0xABE8, 0xABE8},
{0xABED, 0xABED}, {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F},
{0xFE20, 0xFE2F}, {0x101FD, 0x101FD}, {0x102E0, 0x102E0},
{0x10376, 0x1037A}, {0x10A01, 0x10A03}, {0x10A05, 0x10A06},
{0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F},
{0x10AE5, 0x10AE6}, {0x10D24, 0x10D27}, {0x10D69, 0x10D6D},
{0x10EAB, 0x10EAC}, {0x10EFA, 0x10EFF}, {0x10F46, 0x10F50},
{0x10F82, 0x10F85}, {0x11001, 0x11001}, {0x11038, 0x11046},
{0x11070, 0x11070}, {0x11073, 0x11074}, {0x1107F, 0x11081},
{0x110B3, 0x110B6}, {0x110B9, 0x110BA}, {0x110C2, 0x110C2},
{0x11100, 0x11102}, {0x11127, 0x1112B}, {0x1112D, 0x11134},
{0x11173, 0x11173}, {0x11180, 0x11181}, {0x111B6, 0x111BE},
{0x111C9, 0x111CC}, {0x111CF, 0x111CF}, {0x1122F, 0x11231},
{0x11234, 0x11234}, {0x11236, 0x11237}, {0x1123E, 0x1123E},
{0x11241, 0x11241}, {0x112DF, 0x112DF}, {0x112E3, 0x112EA},
{0x11300, 0x11301}, {0x1133B, 0x1133C}, {0x11340, 0x11340},
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x113BB, 0x113C0},
{0x113CE, 0x113CE}, {0x113D0, 0x113D0}, {0x113D2, 0x113D2},
{0x113E1, 0x113E2}, {0x11438, 0x1143F}, {0x11442, 0x11444},
{0x11446, 0x11446}, {0x1145E, 0x1145E}, {0x114B3, 0x114B8},
{0x114BA, 0x114BA}, {0x114BF, 0x114C0}, {0x114C2, 0x114C3},
{0x115B2, 0x115B5}, {0x115BC, 0x115BD}, {0x115BF, 0x115C0},
{0x115DC, 0x115DD}, {0x11633, 0x1163A}, {0x1163D, 0x1163D},
{0x1163F, 0x11640}, {0x116AB, 0x116AB}, {0x116AD, 0x116AD},
{0x116B0, 0x116B5}, {0x116B7, 0x116B7}, {0x1171D, 0x1171D},
{0x1171F, 0x1171F}, {0x11722, 0x11725}, {0x11727, 0x1172B},
{0x1182F, 0x11837}, {0x11839, 0x1183A}, {0x1193B, 0x1193C},
{0x1193E, 0x1193E}, {0x11943, 0x11943}, {0x119D4, 0x119D7},
{0x119DA, 0x119DB}, {0x119E0, 0x119E0}, {0x11A01, 0x11A0A},
{0x11A33, 0x11A38}, {0x11A3B, 0x11A3E}, {0x11A47, 0x11A47},
{0x11A51, 0x11A56}, {0x11A59, 0x11A5B}, {0x11A8A, 0x11A96},
{0x11A98, 0x11A99}, {0x11B60, 0x11B60}, {0x11B62, 0x11B64},
{0x11B66, 0x11B66}, {0x11C30, 0x11C36}, {0x11C38, 0x11C3D},
{0x11C3F, 0x11C3F}, {0x11C92, 0x11CA7}, {0x11CAA, 0x11CB0},
{0x11CB2, 0x11CB3}, {0x11CB5, 0x11CB6}, {0x11D31, 0x11D36},
{0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D}, {0x11D3F, 0x11D45},
{0x11D47, 0x11D47}, {0x11D90, 0x11D91}, {0x11D95, 0x11D95},
{0x11D97, 0x11D97}, {0x11EF3, 0x11EF4}, {0x11F00, 0x11F01},
{0x11F36, 0x11F3A}, {0x11F40, 0x11F40}, {0x11F42, 0x11F42},
{0x11F5A, 0x11F5A}, {0x13440, 0x13440}, {0x13447, 0x13455},
{0x1611E, 0x16129}, {0x1612D, 0x1612F}, {0x16AF0, 0x16AF4},
{0x16B30, 0x16B36}, {0x16F4F, 0x16F4F}, {0x16F8F, 0x16F92},
{0x16FE4, 0x16FE4}, {0x1BC9D, 0x1BC9E}, {0x1CF00, 0x1CF2D},
{0x1CF30, 0x1CF46}, {0x1D165, 0x1D169}, {0x1D16D, 0x1D172},
{0x1D17B, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD},
{0x1D242, 0x1D244}, {0x1DA00, 0x1DA36}, {0x1DA3B, 0x1DA6C},
{0x1DA75, 0x1DA75}, {0x1DA84, 0x1DA84}, {0x1DA9B, 0x1DA9F},
{0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006}, {0x1E008, 0x1E018},
{0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, {0x1E026, 0x1E02A},
{0x1E08F, 0x1E08F}, {0x1E130, 0x1E136}, {0x1E2AE, 0x1E2AE},
{0x1E2EC, 0x1E2EF}, {0x1E4EC, 0x1E4EF}, {0x1E5EE, 0x1E5EF},
{0x1E6E3, 0x1E6E3}, {0x1E6E6, 0x1E6E6}, {0x1E6EE, 0x1E6EF},
{0x1E6F5, 0x1E6F5}, {0x1E8D0, 0x1E8D6}, {0x1E944, 0x1E94A},
{0xE0100, 0xE01EF},
}
@@ -486,3 +590,120 @@ var emoji = table{
{0x1F947, 0x1F9FF}, {0x1FA58, 0x1FA5F}, {0x1FA6E, 0x1FAFF},
{0x1FC00, 0x1FFFD},
}
var joiner = table{
{0x0300, 0x036F}, {0x0483, 0x0489}, {0x0591, 0x05BD},
{0x05BF, 0x05BF}, {0x05C1, 0x05C2}, {0x05C4, 0x05C5},
{0x05C7, 0x05C7}, {0x0600, 0x0605}, {0x0610, 0x061A},
{0x064B, 0x065F}, {0x0670, 0x0670}, {0x06D6, 0x06DD},
{0x06DF, 0x06E4}, {0x06E7, 0x06E8}, {0x06EA, 0x06ED},
{0x070F, 0x070F}, {0x0711, 0x0711}, {0x0730, 0x074A},
{0x07A6, 0x07B0}, {0x07EB, 0x07F3}, {0x07FD, 0x07FD},
{0x0816, 0x0819}, {0x081B, 0x0823}, {0x0825, 0x0827},
{0x0829, 0x082D}, {0x0859, 0x085B}, {0x0890, 0x0891},
{0x0897, 0x089F}, {0x08CA, 0x0903}, {0x093A, 0x093C},
{0x093E, 0x094F}, {0x0951, 0x0957}, {0x0962, 0x0963},
{0x0981, 0x0983}, {0x09BC, 0x09BC}, {0x09BE, 0x09C4},
{0x09C7, 0x09C8}, {0x09CB, 0x09CD}, {0x09D7, 0x09D7},
{0x09E2, 0x09E3}, {0x09FE, 0x09FE}, {0x0A01, 0x0A03},
{0x0A3C, 0x0A3C}, {0x0A3E, 0x0A42}, {0x0A47, 0x0A48},
{0x0A4B, 0x0A4D}, {0x0A51, 0x0A51}, {0x0A70, 0x0A71},
{0x0A75, 0x0A75}, {0x0A81, 0x0A83}, {0x0ABC, 0x0ABC},
{0x0ABE, 0x0AC5}, {0x0AC7, 0x0AC9}, {0x0ACB, 0x0ACD},
{0x0AE2, 0x0AE3}, {0x0AFA, 0x0AFF}, {0x0B01, 0x0B03},
{0x0B3C, 0x0B3C}, {0x0B3E, 0x0B44}, {0x0B47, 0x0B48},
{0x0B4B, 0x0B4D}, {0x0B55, 0x0B57}, {0x0B62, 0x0B63},
{0x0B82, 0x0B82}, {0x0BBE, 0x0BC2}, {0x0BC6, 0x0BC8},
{0x0BCA, 0x0BCD}, {0x0BD7, 0x0BD7}, {0x0C00, 0x0C04},
{0x0C3C, 0x0C3C}, {0x0C3E, 0x0C44}, {0x0C46, 0x0C48},
{0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0C62, 0x0C63},
{0x0C81, 0x0C83}, {0x0CBC, 0x0CBC}, {0x0CBE, 0x0CC4},
{0x0CC6, 0x0CC8}, {0x0CCA, 0x0CCD}, {0x0CD5, 0x0CD6},
{0x0CE2, 0x0CE3}, {0x0CF3, 0x0CF3}, {0x0D00, 0x0D03},
{0x0D3B, 0x0D3C}, {0x0D3E, 0x0D44}, {0x0D46, 0x0D48},
{0x0D4A, 0x0D4E}, {0x0D57, 0x0D57}, {0x0D62, 0x0D63},
{0x0D81, 0x0D83}, {0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4},
{0x0DD6, 0x0DD6}, {0x0DD8, 0x0DDF}, {0x0DF2, 0x0DF3},
{0x0E31, 0x0E31}, {0x0E33, 0x0E3A}, {0x0E47, 0x0E4E},
{0x0EB1, 0x0EB1}, {0x0EB3, 0x0EBC}, {0x0EC8, 0x0ECE},
{0x0F18, 0x0F19}, {0x0F35, 0x0F35}, {0x0F37, 0x0F37},
{0x0F39, 0x0F39}, {0x0F3E, 0x0F3F}, {0x0F71, 0x0F84},
{0x0F86, 0x0F87}, {0x0F8D, 0x0F97}, {0x0F99, 0x0FBC},
{0x0FC6, 0x0FC6}, {0x102D, 0x1037}, {0x1039, 0x103E},
{0x1056, 0x1059}, {0x105E, 0x1060}, {0x1071, 0x1074},
{0x1082, 0x1082}, {0x1084, 0x1086}, {0x108D, 0x108D},
{0x109D, 0x109D}, {0x1100, 0x11FF}, {0x135D, 0x135F},
{0x1712, 0x1715}, {0x1732, 0x1734}, {0x1752, 0x1753},
{0x1772, 0x1773}, {0x17B4, 0x17D3}, {0x17DD, 0x17DD},
{0x180B, 0x180D}, {0x180F, 0x180F}, {0x1885, 0x1886},
{0x18A9, 0x18A9}, {0x1920, 0x192B}, {0x1930, 0x193B},
{0x1A17, 0x1A1B}, {0x1A55, 0x1A5E}, {0x1A60, 0x1A60},
{0x1A62, 0x1A62}, {0x1A65, 0x1A7C}, {0x1A7F, 0x1A7F},
{0x1AB0, 0x1ADD}, {0x1AE0, 0x1AEB}, {0x1B00, 0x1B04},
{0x1B34, 0x1B44}, {0x1B6B, 0x1B73}, {0x1B80, 0x1B82},
{0x1BA1, 0x1BAD}, {0x1BE6, 0x1BF3}, {0x1C24, 0x1C37},
{0x1CD0, 0x1CD2}, {0x1CD4, 0x1CE8}, {0x1CED, 0x1CED},
{0x1CF4, 0x1CF4}, {0x1CF7, 0x1CF9}, {0x1DC0, 0x1DFF},
{0x200C, 0x200D}, {0x20D0, 0x20F0}, {0x2CEF, 0x2CF1},
{0x2D7F, 0x2D7F}, {0x2DE0, 0x2DFF}, {0x302A, 0x302F},
{0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D},
{0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA802, 0xA802},
{0xA806, 0xA806}, {0xA80B, 0xA80B}, {0xA823, 0xA827},
{0xA82C, 0xA82C}, {0xA880, 0xA881}, {0xA8B4, 0xA8C5},
{0xA8E0, 0xA8F1}, {0xA8FF, 0xA8FF}, {0xA926, 0xA92D},
{0xA947, 0xA953}, {0xA960, 0xA97C}, {0xA980, 0xA983},
{0xA9B3, 0xA9C0}, {0xA9E5, 0xA9E5}, {0xAA29, 0xAA36},
{0xAA43, 0xAA43}, {0xAA4C, 0xAA4D}, {0xAA7C, 0xAA7C},
{0xAAB0, 0xAAB0}, {0xAAB2, 0xAAB4}, {0xAAB7, 0xAAB8},
{0xAABE, 0xAABF}, {0xAAC1, 0xAAC1}, {0xAAEB, 0xAAEF},
{0xAAF5, 0xAAF6}, {0xABE3, 0xABEA}, {0xABEC, 0xABED},
{0xAC00, 0xD7A3}, {0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB},
{0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F}, {0xFE20, 0xFE2F},
{0xFF9E, 0xFF9F}, {0x101FD, 0x101FD}, {0x102E0, 0x102E0},
{0x10376, 0x1037A}, {0x10A01, 0x10A03}, {0x10A05, 0x10A06},
{0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F},
{0x10AE5, 0x10AE6}, {0x10D24, 0x10D27}, {0x10D69, 0x10D6D},
{0x10EAB, 0x10EAC}, {0x10EFA, 0x10EFF}, {0x10F46, 0x10F50},
{0x10F82, 0x10F85}, {0x11000, 0x11002}, {0x11038, 0x11046},
{0x11070, 0x11070}, {0x11073, 0x11074}, {0x1107F, 0x11082},
{0x110B0, 0x110BA}, {0x110BD, 0x110BD}, {0x110C2, 0x110C2},
{0x110CD, 0x110CD}, {0x11100, 0x11102}, {0x11127, 0x11134},
{0x11145, 0x11146}, {0x11173, 0x11173}, {0x11180, 0x11182},
{0x111B3, 0x111C0}, {0x111C2, 0x111C3}, {0x111C9, 0x111CC},
{0x111CE, 0x111CF}, {0x1122C, 0x11237}, {0x1123E, 0x1123E},
{0x11241, 0x11241}, {0x112DF, 0x112EA}, {0x11300, 0x11303},
{0x1133B, 0x1133C}, {0x1133E, 0x11344}, {0x11347, 0x11348},
{0x1134B, 0x1134D}, {0x11357, 0x11357}, {0x11362, 0x11363},
{0x11366, 0x1136C}, {0x11370, 0x11374}, {0x113B8, 0x113C0},
{0x113C2, 0x113C2}, {0x113C5, 0x113C5}, {0x113C7, 0x113CA},
{0x113CC, 0x113D2}, {0x113E1, 0x113E2}, {0x11435, 0x11446},
{0x1145E, 0x1145E}, {0x114B0, 0x114C3}, {0x115AF, 0x115B5},
{0x115B8, 0x115C0}, {0x115DC, 0x115DD}, {0x11630, 0x11640},
{0x116AB, 0x116B7}, {0x1171D, 0x1171F}, {0x11722, 0x1172B},
{0x1182C, 0x1183A}, {0x11930, 0x11935}, {0x11937, 0x11938},
{0x1193B, 0x11943}, {0x119D1, 0x119D7}, {0x119DA, 0x119E0},
{0x119E4, 0x119E4}, {0x11A01, 0x11A0A}, {0x11A33, 0x11A3E},
{0x11A47, 0x11A47}, {0x11A51, 0x11A5B}, {0x11A84, 0x11A99},
{0x11B60, 0x11B67}, {0x11C2F, 0x11C36}, {0x11C38, 0x11C3F},
{0x11C92, 0x11CA7}, {0x11CA9, 0x11CB6}, {0x11D31, 0x11D36},
{0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D}, {0x11D3F, 0x11D47},
{0x11D8A, 0x11D8E}, {0x11D90, 0x11D91}, {0x11D93, 0x11D97},
{0x11EF3, 0x11EF6}, {0x11F00, 0x11F03}, {0x11F34, 0x11F3A},
{0x11F3E, 0x11F42}, {0x11F5A, 0x11F5A}, {0x13440, 0x13440},
{0x13447, 0x13455}, {0x1611E, 0x1612F}, {0x16AF0, 0x16AF4},
{0x16B30, 0x16B36}, {0x16D63, 0x16D63}, {0x16D67, 0x16D6A},
{0x16F4F, 0x16F4F}, {0x16F51, 0x16F87}, {0x16F8F, 0x16F92},
{0x16FE4, 0x16FE4}, {0x16FF0, 0x16FF1}, {0x1BC9D, 0x1BC9E},
{0x1CF00, 0x1CF2D}, {0x1CF30, 0x1CF46}, {0x1D165, 0x1D169},
{0x1D16D, 0x1D172}, {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B},
{0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0x1DA00, 0x1DA36},
{0x1DA3B, 0x1DA6C}, {0x1DA75, 0x1DA75}, {0x1DA84, 0x1DA84},
{0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006},
{0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024},
{0x1E026, 0x1E02A}, {0x1E08F, 0x1E08F}, {0x1E130, 0x1E136},
{0x1E2AE, 0x1E2AE}, {0x1E2EC, 0x1E2EF}, {0x1E4EC, 0x1E4EF},
{0x1E5EE, 0x1E5EF}, {0x1E6E3, 0x1E6E3}, {0x1E6E6, 0x1E6E6},
{0x1E6EE, 0x1E6EF}, {0x1E6F5, 0x1E6F5}, {0x1E8D0, 0x1E8D6},
{0x1E944, 0x1E94A}, {0x1F1E6, 0x1F1FF}, {0x1F3FB, 0x1F3FF},
{0xE0020, 0xE007F}, {0xE0100, 0xE01EF},
}
+21 -56
View File
@@ -29,7 +29,6 @@ import (
"math"
"reflect"
"sync"
"sync/atomic"
"unsafe"
)
@@ -104,27 +103,28 @@ type handleVal struct {
val any
}
var handleLock sync.Mutex
var handleVals atomic.Value // stores map[unsafe.Pointer]handleVal
// handleVals maps unsafe.Pointer handles to handleVal. A sync.Map keeps
// lookups lock-free on the hot callback path while insertion and removal
// stay O(1); the previous copy-on-write map made every registration copy
// the whole table, so opening N connections (each registering several
// functions) was quadratic in time and allocation.
var handleVals sync.Map
func newHandle(db *SQLiteConn, v any) unsafe.Pointer {
val := handleVal{db: db, val: v}
var p unsafe.Pointer = C.malloc(C.size_t(1))
if p == nil {
panic("can't allocate 'cgo-pointer hack index pointer': ptr == nil")
}
handleLock.Lock()
defer handleLock.Unlock()
next := cloneHandleVals(len(loadHandleVals()) + 1)
next[p] = val
handleVals.Store(next)
handleVals.Store(p, handleVal{db: db, val: v})
return p
}
func lookupHandleVal(handle unsafe.Pointer) handleVal {
return loadHandleVals()[handle]
v, ok := handleVals.Load(handle)
if !ok {
return handleVal{}
}
return v.(handleVal)
}
func lookupHandle(handle unsafe.Pointer) any {
@@ -134,55 +134,20 @@ func lookupHandle(handle unsafe.Pointer) any {
// deleteHandle releases a single handle created by newHandle. It is a no-op
// if the handle is unknown (e.g. already released).
func deleteHandle(handle unsafe.Pointer) {
handleLock.Lock()
defer handleLock.Unlock()
current := loadHandleVals()
if _, ok := current[handle]; !ok {
return
if _, ok := handleVals.LoadAndDelete(handle); ok {
C.free(handle)
}
next := make(map[unsafe.Pointer]handleVal, len(current)-1)
for h, v := range current {
if h == handle {
continue
}
next[h] = v
}
handleVals.Store(next)
C.free(handle)
}
func deleteHandles(db *SQLiteConn) {
handleLock.Lock()
defer handleLock.Unlock()
current := loadHandleVals()
if len(current) == 0 {
return
}
next := make(map[unsafe.Pointer]handleVal, len(current))
for handle, val := range current {
if val.db == db {
C.free(handle)
continue
handleVals.Range(func(handle, val any) bool {
if val.(handleVal).db == db {
if _, ok := handleVals.LoadAndDelete(handle); ok {
C.free(handle.(unsafe.Pointer))
}
}
next[handle] = val
}
handleVals.Store(next)
}
func loadHandleVals() map[unsafe.Pointer]handleVal {
m, _ := handleVals.Load().(map[unsafe.Pointer]handleVal)
return m
}
func cloneHandleVals(size int) map[unsafe.Pointer]handleVal {
next := make(map[unsafe.Pointer]handleVal, size)
for handle, val := range loadHandleVals() {
next[handle] = val
}
return next
return true
})
}
// This is only here so that tests can refer to it.
+2 -2
View File
@@ -159,7 +159,7 @@ func convertAssign(dest, src any) error {
}
dpv := reflect.ValueOf(dest)
if dpv.Kind() != reflect.Ptr {
if dpv.Kind() != reflect.Pointer {
return errors.New("destination not a pointer")
}
if dpv.IsNil() {
@@ -192,7 +192,7 @@ func convertAssign(dest, src any) error {
// This also allows scanning into user defined types such as "type Int int64".
// For symmetry, also check for string destination types.
switch dv.Kind() {
case reflect.Ptr:
case reflect.Pointer:
if src == nil {
dv.Set(reflect.Zero(dv.Type()))
return nil
+183 -40
View File
@@ -220,6 +220,25 @@ _sqlite3_prepare_v2_internal(sqlite3 *db, const char *zSql, int nBytes, sqlite3_
}
#endif
// Steps a statement once and reports the post-step column count and the
// cumulative re-prepare count, in a single CGO crossing. Used for the
// eager first step of cached statements: only after the first step is an
// expired statement guaranteed to have been re-prepared following a
// schema change, so only then do the column count and metadata describe
// the current schema.
static int
_sqlite3_step_columns(sqlite3_stmt* stmt, int* ncol, int* repreps)
{
int rv = _sqlite3_step_internal(stmt);
*ncol = sqlite3_column_count(stmt);
#ifdef SQLITE_STMTSTATUS_REPREPARE
*repreps = sqlite3_stmt_status(stmt, SQLITE_STMTSTATUS_REPREPARE, 0);
#else
*repreps = -1;
#endif
return rv;
}
void _sqlite3_result_text(sqlite3_context* ctx, const char* s, int n) {
sqlite3_result_text(ctx, s, n, &free);
}
@@ -445,8 +464,11 @@ type SQLiteDriver struct {
// SQLiteConn implements driver.Conn.
type SQLiteConn struct {
mu sync.Mutex
db *C.sqlite3
mu sync.Mutex
db *C.sqlite3
// activeRows identifies the cancellable Rows currently calling sqlite3_step.
// It is guarded by mu so a stale cancellation cannot interrupt later work.
activeRows *SQLiteRows
loc *time.Location
txlock string
funcs []*functionInfo
@@ -477,6 +499,10 @@ type SQLiteStmt struct {
namedParams map[string][3]int
cacheKey string
metadata *sqliteStmtMetadata
// repreps is the statement's cumulative re-prepare count observed
// at the last eager first step; a change means SQLite re-prepared
// the statement after a schema change and metadata must be rebuilt.
repreps C.int
}
type sqliteStmtMetadata struct {
@@ -492,14 +518,18 @@ type SQLiteResult struct {
// SQLiteRows implements driver.Rows.
type SQLiteRows struct {
s *SQLiteStmt
nc int32 // Number of columns
cls bool // True if we need to close the parent statement in Close
cols []string
decltype []string
colvals *C.sqlite3_go_col
ctx context.Context // no better alternative to pass context into Next() method
closemu sync.Mutex
s *SQLiteStmt
nc int32 // Number of columns
cls bool // True if we need to close the parent statement in Close
cols []string
decltype []string
colvals *C.sqlite3_go_col
ctx context.Context // no better alternative to pass context into Next() method
stopCancellation func() bool
// pendingStep buffers the result of the eager first step taken for
// cached statements in query(); -1 when no step is buffered.
pendingStep C.int
closemu sync.Mutex
}
type functionInfo struct {
@@ -968,7 +998,7 @@ func (c *SQLiteConn) exec(ctx context.Context, query string, args []driver.Named
na := s.NumInput()
if len(args)-start < na {
s.Close()
return nil, fmt.Errorf("not enough args to execute query: want %d got %d", na, len(args))
return nil, fmt.Errorf("not enough args to execute query: want %d got %d", na, len(args)-start)
}
stmtArgs := stmtArgs(args, start, na)
res, err = s.(*SQLiteStmt).exec(ctx, stmtArgs)
@@ -1609,6 +1639,18 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
return nil, err
}
if conn.stmtCacheEnabled && int(C.sqlite3_libversion_number()) < 3020000 {
// Schema-change detection for cached statements relies on
// SQLITE_STMTSTATUS_REPREPARE (3.20.0); with an older runtime
// library run without the cache rather than risk serving
// statements whose metadata a schema change has expired. The
// compile-time check in _sqlite3_step_columns is not enough:
// with USE_LIBSQLITE3 the header and the runtime library can
// differ.
conn.stmtCache = nil
conn.stmtCacheEnabled = false
}
exec := func(s string) error {
cs := C.CString(s)
rv := C.sqlite3_exec(db, cs, nil, nil, nil)
@@ -2354,13 +2396,24 @@ func (s *SQLiteStmt) query(ctx context.Context, args []driver.NamedValue) (drive
}
rows := &SQLiteRows{
s: s,
nc: int32(C.sqlite3_column_count(s.s)),
cls: s.cls,
cols: nil,
decltype: nil,
colvals: nil,
ctx: ctx,
s: s,
cls: s.cls,
ctx: ctx,
pendingStep: -1,
}
if s.cacheKey != "" {
// A schema change expires cached statements and SQLite only
// re-prepares them on their next step, so the column count and
// metadata read before stepping could describe the old schema.
// Take the query's first step eagerly (it was going to run at
// the first Next anyway) and read them afterwards.
rv, err := rows.eagerFirstStepLocked()
if err != nil {
return nil, err
}
rows.pendingStep = rv
} else {
rows.nc = int32(C.sqlite3_column_count(s.s))
}
if rows.nc > 0 {
rows.colvals = (*C.sqlite3_go_col)(C.malloc(C.size_t(rows.nc) * C.size_t(unsafe.Sizeof(C.sqlite3_go_col{}))))
@@ -2466,6 +2519,7 @@ func (s *SQLiteStmt) Readonly() bool {
func (rc *SQLiteRows) Close() error {
rc.closemu.Lock()
defer rc.closemu.Unlock()
rc.stopWatchingCancellation()
s := rc.s
if s == nil {
if rc.colvals != nil {
@@ -2497,6 +2551,40 @@ func (rc *SQLiteRows) Close() error {
return nil
}
func (c *SQLiteConn) interruptActiveRows(rows *SQLiteRows) {
c.mu.Lock()
defer c.mu.Unlock()
if c.activeRows == rows && c.db != nil {
C.sqlite3_interrupt(c.db)
}
}
func (rc *SQLiteRows) stopWatchingCancellation() {
if rc.stopCancellation == nil {
return
}
// A false return is harmless: a callback already in progress can interrupt
// only while rc owns conn.activeRows, which is guarded by conn.mu.
rc.stopCancellation()
rc.stopCancellation = nil
}
func (rc *SQLiteRows) startStepping() {
conn := rc.s.c
conn.mu.Lock()
conn.activeRows = rc
conn.mu.Unlock()
}
func (rc *SQLiteRows) finishStepping() {
conn := rc.s.c
conn.mu.Lock()
if conn.activeRows == rc {
conn.activeRows = nil
}
conn.mu.Unlock()
}
func (s *SQLiteStmt) cacheMetadata() bool {
return !s.cls || s.cacheKey != ""
}
@@ -2583,33 +2671,88 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
return io.EOF
}
if rc.ctx.Done() == nil {
return rc.nextSyncLocked(dest)
if rv := rc.pendingStep; rv >= 0 {
rc.pendingStep = -1
return rc.readStepResultLocked(dest, rv)
}
sema := make(chan struct{})
var err error
go func() {
err = rc.nextSyncLocked(dest)
close(sema)
}()
select {
case <-sema:
return err
case <-rc.ctx.Done():
select {
case <-sema: // no need to interrupt
default:
// this is still racy and can be no-op if executed between sqlite3_* calls in nextSyncLocked.
C.sqlite3_interrupt(rc.s.c.db)
<-sema // ensure goroutine completed
if rc.stopCancellation == nil {
if rc.ctx.Done() == nil {
rv := C._sqlite3_step_internal(rc.s.s)
return rc.readStepResultLocked(dest, rv)
}
return rc.ctx.Err()
conn := rc.s.c
rc.stopCancellation = context.AfterFunc(rc.ctx, func() {
conn.interruptActiveRows(rc)
})
}
if err := rc.ctx.Err(); err != nil {
return err
}
rv := rc.stepCancellableLocked()
err := rc.readStepResultLocked(dest, rv)
if ctxErr := rc.ctx.Err(); ctxErr != nil {
return ctxErr
}
return err
}
// nextSyncLocked moves cursor to next; must be called with locked mutex.
func (rc *SQLiteRows) nextSyncLocked(dest []driver.Value) error {
rv := C._sqlite3_step_internal(rc.s.s)
// eagerFirstStepLocked performs the first step of a cached statement
// under the same cancellation rules as Next, records the post-step
// column count, and drops the statement's cached metadata when SQLite
// re-prepared it after a schema change. Note that this runs the first
// step at query time, so a data-modifying statement issued through
// Query executes even if Next is never called.
func (rc *SQLiteRows) eagerFirstStepLocked() (C.int, error) {
s := rc.s
s.mu.Lock()
defer s.mu.Unlock()
if rc.ctx.Done() != nil && rc.stopCancellation == nil {
conn := s.c
rc.stopCancellation = context.AfterFunc(rc.ctx, func() {
conn.interruptActiveRows(rc)
})
}
if err := rc.ctx.Err(); err != nil {
rc.stopWatchingCancellation()
return 0, err
}
var ncol, repreps C.int
var rv C.int
if rc.ctx.Done() == nil {
rv = C._sqlite3_step_columns(s.s, &ncol, &repreps)
} else {
rc.startStepping()
rv = C._sqlite3_step_columns(s.s, &ncol, &repreps)
rc.finishStepping()
}
if err := rc.ctx.Err(); err != nil {
rc.stopWatchingCancellation()
C._sqlite3_reset_clear(s.s)
return 0, err
}
if rv != C.SQLITE_ROW && rv != C.SQLITE_DONE {
rc.stopWatchingCancellation()
err := s.c.lastError()
C._sqlite3_reset_clear(s.s)
return 0, err
}
rc.nc = int32(ncol)
if repreps != s.repreps {
s.repreps = repreps
s.metadata = nil
}
return rv, nil
}
func (rc *SQLiteRows) stepCancellableLocked() C.int {
rc.startStepping()
defer rc.finishStepping()
return C._sqlite3_step_internal(rc.s.s)
}
func (rc *SQLiteRows) readStepResultLocked(dest []driver.Value, rv C.int) error {
if rv == C.SQLITE_DONE {
return io.EOF
}
+22 -24
View File
@@ -11,43 +11,41 @@ import (
"crypto/sha512"
)
// This file provides several different implementations for the
// default embedded sqlite_crypt function.
// This function is uses a caesar-cypher by default
// and is used within the UserAuthentication module to encode
// the password.
// This file provides several different implementations for the default
// embedded sqlite_crypt function. This function uses a caesar-cypher by
// default and is used within the UserAuthentication module to encode the
// password.
//
// The provided functions can be used as an overload to the sqlite_crypt
// function through the use of the RegisterFunc on the connection.
//
// Because the functions can serv a purpose to an end-user
// without using the UserAuthentication module
// the functions are default compiled in.
// Because the functions can serve a purpose to an end-user without using the
// UserAuthentication module the functions are default compiled in.
//
// From SQLITE3 - user-auth.txt
// The sqlite_user.pw field is encoded by a built-in SQL function
// "sqlite_crypt(X,Y)". The two arguments are both BLOBs. The first argument
// is the plaintext password supplied to the sqlite3_user_authenticate()
// interface. The second argument is the sqlite_user.pw value and is supplied
// "sqlite_crypt(X,Y)". The two arguments are both BLOBs. The first argument is
// the plain-text password supplied to the sqlite3_user_authenticate()
// interface. The second argument is the sqlite_user.pw value and is supplied
// so that the function can extract the "salt" used by the password encoder.
// The result of sqlite_crypt(X,Y) is another blob which is the value that
// ends up being stored in sqlite_user.pw. To verify credentials X supplied
// by the sqlite3_user_authenticate() routine, SQLite runs:
// The result of sqlite_crypt(X,Y) is another blob which is the value that ends
// up being stored in sqlite_user.pw. To verify credentials X supplied by the
// sqlite3_user_authenticate() routine, SQLite runs:
//
// sqlite_user.pw == sqlite_crypt(X, sqlite_user.pw)
//
// To compute an appropriate sqlite_user.pw value from a new or modified
// password X, sqlite_crypt(X,NULL) is run. A new random salt is selected
// when the second argument is NULL.
// password X, sqlite_crypt(X,NULL) is run. A new random salt is selected when
// the second argument is NULL.
//
// The built-in version of of sqlite_crypt() uses a simple Caesar-cypher
// which prevents passwords from being revealed by searching the raw database
// for ASCII text, but is otherwise trivally broken. For better password
// security, the database should be encrypted using the SQLite Encryption
// Extension or similar technology. Or, the application can use the
// sqlite3_create_function() interface to provide an alternative
// implementation of sqlite_crypt() that computes a stronger password hash,
// perhaps using a cryptographic hash function like SHA1.
// The built-in version of of sqlite_crypt() uses a simple Caesar-cypher which
// prevents passwords from being revealed by searching the raw database for
// ASCII text, but is otherwise trivally broken. For better password security,
// the database should be encrypted using the SQLite Encryption Extension or
// similar technology. Or, the application can use the
// sqlite3_create_function() interface to provide an alternative implementation
// of sqlite_crypt() that computes a stronger password hash, perhaps using a
// cryptographic hash function like SHA1.
// CryptEncoderSHA1 encodes a password with SHA1
func CryptEncoderSHA1(pass []byte, hash any) []byte {
-1
View File
@@ -4,7 +4,6 @@
// license that can be found in the LICENSE file.
//go:build libsqlite3
// +build libsqlite3
package sqlite3
-1
View File
@@ -4,7 +4,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_omit_load_extension
// +build sqlite_omit_load_extension
package sqlite3
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_allow_uri_authority
// +build sqlite_allow_uri_authority
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build !windows && sqlite_app_armor
// +build !windows,sqlite_app_armor
package sqlite3
-1
View File
@@ -1,5 +1,4 @@
//go:build sqlite_column_metadata
// +build sqlite_column_metadata
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_dbstat
// +build sqlite_dbstat
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_foreign_keys
// +build sqlite_foreign_keys
package sqlite3
-1
View File
@@ -4,7 +4,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_fts5 || fts5
// +build sqlite_fts5 fts5
package sqlite3
-1
View File
@@ -4,7 +4,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_icu || icu
// +build sqlite_icu icu
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_introspect
// +build sqlite_introspect
package sqlite3
-1
View File
@@ -4,7 +4,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_math_functions
// +build sqlite_math_functions
package sqlite3
-1
View File
@@ -4,7 +4,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_os_trace
// +build sqlite_os_trace
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_percentile
// +build sqlite_percentile
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build cgo
// +build cgo
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_preupdate_hook
// +build sqlite_preupdate_hook
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build !sqlite_preupdate_hook && cgo
// +build !sqlite_preupdate_hook,cgo
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_secure_delete
// +build sqlite_secure_delete
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_secure_delete_fast
// +build sqlite_secure_delete_fast
package sqlite3
-1
View File
@@ -1,5 +1,4 @@
//go:build libsqlite3 && !sqlite_serialize
// +build libsqlite3,!sqlite_serialize
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_stat4
// +build sqlite_stat4
package sqlite3
-1
View File
@@ -4,7 +4,6 @@
// license that can be found in the LICENSE file.
//go:build cgo && sqlite_unlock_notify
// +build cgo,sqlite_unlock_notify
package sqlite3
-1
View File
@@ -4,7 +4,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_userauth
// +build sqlite_userauth
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_vacuum_full
// +build sqlite_vacuum_full
package sqlite3
-1
View File
@@ -5,7 +5,6 @@
// license that can be found in the LICENSE file.
//go:build sqlite_vacuum_incr
// +build sqlite_vacuum_incr
package sqlite3
Loaded 100 of 372 files, more files were not shown because too many files have changed in this diff. Show more