mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 09:26:06 -10:00
build(deps): combine compatible Dependabot updates (#2424)
This commit is contained in:
372 files changed
+25522
-2897
No files matched your search
Generated
+55
-55
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
+5
-19
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,5 +1,4 @@
|
||||
//go:build !go1.21
|
||||
// +build !go1.21
|
||||
|
||||
/*
|
||||
Copyright 2019 The logr Authors.
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
/*
|
||||
Copyright 2019 The logr Authors.
|
||||
|
||||
+36
-11
@@ -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
@@ -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
@@ -1,5 +1,4 @@
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
/*
|
||||
Copyright 2023 The logr Authors.
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
/*
|
||||
Copyright 2023 The logr Authors.
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build go1.21
|
||||
// +build go1.21
|
||||
|
||||
/*
|
||||
Copyright 2023 The logr Authors.
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
* -text
|
||||
*.bin -text -diff
|
||||
*.md text eol=lf
|
||||
+10
-1
@@ -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
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -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
@@ -1,5 +1,4 @@
|
||||
//go:build !amd64
|
||||
// +build !amd64
|
||||
|
||||
package flate
|
||||
|
||||
|
||||
+78
-78
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,5 +1,4 @@
|
||||
//go:build amd64 && !appengine && !noasm && gc
|
||||
// +build amd64,!appengine,!noasm,gc
|
||||
|
||||
package cpuinfo
|
||||
|
||||
|
||||
+1
@@ -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
@@ -21,7 +21,7 @@ type fastBase struct {
|
||||
crc *xxhash.Digest
|
||||
tmp [8]byte
|
||||
blk *blockEnc
|
||||
lastDictID uint32
|
||||
lastDict *dict
|
||||
lowMem bool
|
||||
}
|
||||
|
||||
|
||||
+9
-5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -42,6 +42,7 @@ func (o *encoderOptions) setDefault() {
|
||||
level: SpeedDefault,
|
||||
allLitEntropy: false,
|
||||
lowMem: false,
|
||||
fullZero: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build amd64 && !appengine && !noasm && gc
|
||||
// +build amd64,!appengine,!noasm,gc
|
||||
|
||||
package zstd
|
||||
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build !amd64 || appengine || !gc || noasm
|
||||
// +build !amd64 appengine !gc noasm
|
||||
|
||||
package zstd
|
||||
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build (!amd64 && !arm64) || appengine || !gc || purego || noasm
|
||||
// +build !amd64,!arm64 appengine !gc purego noasm
|
||||
|
||||
package xxhash
|
||||
|
||||
|
||||
-1
@@ -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
@@ -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
@@ -1,5 +1,4 @@
|
||||
//go:build amd64 && !appengine && !noasm && gc
|
||||
// +build amd64,!appengine,!noasm,gc
|
||||
|
||||
package zstd
|
||||
|
||||
|
||||
-1
@@ -1,5 +1,4 @@
|
||||
//go:build !amd64 || appengine || !gc || noasm
|
||||
// +build !amd64 appengine !gc noasm
|
||||
|
||||
package zstd
|
||||
|
||||
|
||||
+514
-97
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -4,7 +4,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build libsqlite3
|
||||
// +build libsqlite3
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
-1
@@ -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
|
||||
|
||||
|
||||
-1
@@ -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
@@ -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
@@ -1,5 +1,4 @@
|
||||
//go:build sqlite_column_metadata
|
||||
// +build sqlite_column_metadata
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
-1
@@ -5,7 +5,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build sqlite_dbstat
|
||||
// +build sqlite_dbstat
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
-1
@@ -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
@@ -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
@@ -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
@@ -5,7 +5,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build sqlite_introspect
|
||||
// +build sqlite_introspect
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
-1
@@ -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
@@ -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
@@ -5,7 +5,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build sqlite_percentile
|
||||
// +build sqlite_percentile
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
-1
@@ -5,7 +5,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,5 +1,4 @@
|
||||
//go:build libsqlite3 && !sqlite_serialize
|
||||
// +build libsqlite3,!sqlite_serialize
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
-1
@@ -5,7 +5,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build sqlite_stat4
|
||||
// +build sqlite_stat4
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
-1
@@ -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
@@ -4,7 +4,6 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build sqlite_userauth
|
||||
// +build sqlite_userauth
|
||||
|
||||
package sqlite3
|
||||
|
||||
|
||||
-1
@@ -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
@@ -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
Reference in new issue
Block a user