feat: initial commit
This commit is contained in:
commit
a161b86c9a
705 changed files with 288162 additions and 0 deletions
11
.envrc
Normal file
11
.envrc
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Used by https://direnv.net
|
||||||
|
|
||||||
|
# Automatically reload when this file changes
|
||||||
|
watch_file devshell.nix
|
||||||
|
|
||||||
|
# Load `nix develop`
|
||||||
|
use flake
|
||||||
|
|
||||||
|
# Extend the environment with per-user overrides
|
||||||
|
source_env_if_exists .envrc.local
|
||||||
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# direnv
|
||||||
|
.direnv/
|
||||||
|
|
||||||
|
# nix
|
||||||
|
result
|
||||||
|
result-*
|
||||||
1
.ignore
Normal file
1
.ignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
vendor/
|
||||||
8
devshell.nix
Normal file
8
devshell.nix
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{ pkgs }:
|
||||||
|
|
||||||
|
pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
pkgs.go
|
||||||
|
pkgs.gomod2nix
|
||||||
|
];
|
||||||
|
}
|
||||||
119
flake.lock
generated
Normal file
119
flake.lock
generated
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"blueprint": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1763308703,
|
||||||
|
"narHash": "sha256-O9Y+Wer8wOh+N+4kcCK5p/VLrXyX+ktk0/s3HdZvJzk=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "blueprint",
|
||||||
|
"rev": "5a9bba070f801d63e2af3c9ef00b86b212429f4f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "blueprint",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gomod2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1763982521,
|
||||||
|
"narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"rev": "02e63a239d6eabd595db56852535992c898eba72",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "gomod2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1764667669,
|
||||||
|
"narHash": "sha256-7WUCZfmqLAssbDqwg9cUDAXrSoXN79eEEq17qhTNM/Y=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "418468ac9527e799809c900eda37cbff999199b6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"blueprint": "blueprint",
|
||||||
|
"gomod2nix": "gomod2nix",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
22
flake.nix
Normal file
22
flake.nix
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
description = "A fediverse tui client";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
|
||||||
|
|
||||||
|
blueprint = {
|
||||||
|
url = "github:numtide/blueprint";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
gomod2nix = {
|
||||||
|
url = "github:nix-community/gomod2nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = inputs: inputs.blueprint {
|
||||||
|
inherit inputs;
|
||||||
|
nixpkgs.overlays = [ inputs.gomod2nix.overlays.default ];
|
||||||
|
};
|
||||||
|
}
|
||||||
32
go.mod
Normal file
32
go.mod
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
module github.com/cato-001/twink
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexflint/go-arg v1.6.0
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
sourcery.dny.nu/pana v0.1.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
sourcery.dny.nu/longdistance v0.1.1 // indirect
|
||||||
|
)
|
||||||
62
go.sum
Normal file
62
go.sum
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo=
|
||||||
|
github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||||
|
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||||
|
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||||
|
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
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=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||||
|
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
sourcery.dny.nu/longdistance v0.1.1 h1:2YTwDDLqqMh2qT5+XYGwqYvgWCi5tPkc5uzUnE7ZD7Y=
|
||||||
|
sourcery.dny.nu/longdistance v0.1.1/go.mod h1:zBvZfmLWRbVfw7AxFn2uxA4HmNkgb63rKWedfqIASNc=
|
||||||
|
sourcery.dny.nu/pana v0.1.1 h1:a0HQit5k3HU543qbJlX7A0h8kUz0Ai8E0gmev9SDYFM=
|
||||||
|
sourcery.dny.nu/pana v0.1.1/go.mod h1:XOPI7d/VUSjL8K/LJohXrPLaal0/rsdBBfpEdFdYWhM=
|
||||||
72
gomod2nix.toml
Normal file
72
gomod2nix.toml
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
schema = 3
|
||||||
|
|
||||||
|
[mod]
|
||||||
|
[mod."github.com/alexflint/go-arg"]
|
||||||
|
version = "v1.6.0"
|
||||||
|
hash = "sha256-xLuVx2tlvfpSU5ivjderC8/Kxu7OTanJfAsenMnYYIg="
|
||||||
|
[mod."github.com/alexflint/go-scalar"]
|
||||||
|
version = "v1.2.0"
|
||||||
|
hash = "sha256-PivG79qqrPJ1CksNiNNlwC+xPCMMTQxtda3rwk/z6LA="
|
||||||
|
[mod."github.com/aymanbagabas/go-osc52/v2"]
|
||||||
|
version = "v2.0.1"
|
||||||
|
hash = "sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg="
|
||||||
|
[mod."github.com/charmbracelet/bubbletea"]
|
||||||
|
version = "v1.3.10"
|
||||||
|
hash = "sha256-7wr85TLszu1CHNEMv+o4w+r24Z0xdzCgecPv+ZtRX/A="
|
||||||
|
[mod."github.com/charmbracelet/colorprofile"]
|
||||||
|
version = "v0.2.3-0.20250311203215-f60798e515dc"
|
||||||
|
hash = "sha256-D9E/bMOyLXAUVOHA1/6o3i+vVmLfwIMOWib6sU7A6+Q="
|
||||||
|
[mod."github.com/charmbracelet/lipgloss"]
|
||||||
|
version = "v1.1.0"
|
||||||
|
hash = "sha256-RHsRT2EZ1nDOElxAK+6/DC9XAaGVjDTgPvRh3pyCfY4="
|
||||||
|
[mod."github.com/charmbracelet/x/ansi"]
|
||||||
|
version = "v0.10.1"
|
||||||
|
hash = "sha256-nY4zkUGnuD+Lczwt+NMXdQ38cAsy5mtxzXrFSJmR0E4="
|
||||||
|
[mod."github.com/charmbracelet/x/cellbuf"]
|
||||||
|
version = "v0.0.13-0.20250311204145-2c3ea96c31dd"
|
||||||
|
hash = "sha256-XAhCOt8qJ2vR77lH1ez0IVU1/2CaLTq9jSmrHVg5HHU="
|
||||||
|
[mod."github.com/charmbracelet/x/term"]
|
||||||
|
version = "v0.2.1"
|
||||||
|
hash = "sha256-VBkCZLI90PhMasftGw3403IqoV7d3E5WEGAIVrN5xQM="
|
||||||
|
[mod."github.com/erikgeiser/coninput"]
|
||||||
|
version = "v0.0.0-20211004153227-1c3628e74d0f"
|
||||||
|
hash = "sha256-OWSqN1+IoL73rWXWdbbcahZu8n2al90Y3eT5Z0vgHvU="
|
||||||
|
[mod."github.com/lucasb-eyer/go-colorful"]
|
||||||
|
version = "v1.2.0"
|
||||||
|
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
|
||||||
|
[mod."github.com/mattn/go-isatty"]
|
||||||
|
version = "v0.0.20"
|
||||||
|
hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ="
|
||||||
|
[mod."github.com/mattn/go-localereader"]
|
||||||
|
version = "v0.0.1"
|
||||||
|
hash = "sha256-JlWckeGaWG+bXK8l8WEdZqmSiTwCA8b1qbmBKa/Fj3E="
|
||||||
|
[mod."github.com/mattn/go-runewidth"]
|
||||||
|
version = "v0.0.16"
|
||||||
|
hash = "sha256-NC+ntvwIpqDNmXb7aixcg09il80ygq6JAnW0Gb5b/DQ="
|
||||||
|
[mod."github.com/muesli/ansi"]
|
||||||
|
version = "v0.0.0-20230316100256-276c6243b2f6"
|
||||||
|
hash = "sha256-qRKn0Bh2yvP0QxeEMeZe11Vz0BPFIkVcleKsPeybKMs="
|
||||||
|
[mod."github.com/muesli/cancelreader"]
|
||||||
|
version = "v0.2.2"
|
||||||
|
hash = "sha256-uEPpzwRJBJsQWBw6M71FDfgJuR7n55d/7IV8MO+rpwQ="
|
||||||
|
[mod."github.com/muesli/termenv"]
|
||||||
|
version = "v0.16.0"
|
||||||
|
hash = "sha256-hGo275DJlyLtcifSLpWnk8jardOksdeX9lH4lBeE3gI="
|
||||||
|
[mod."github.com/rivo/uniseg"]
|
||||||
|
version = "v0.4.7"
|
||||||
|
hash = "sha256-rDcdNYH6ZD8KouyyiZCUEy8JrjOQoAkxHBhugrfHjFo="
|
||||||
|
[mod."github.com/xo/terminfo"]
|
||||||
|
version = "v0.0.0-20220910002029-abceb7e1c41e"
|
||||||
|
hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
|
||||||
|
[mod."golang.org/x/sys"]
|
||||||
|
version = "v0.36.0"
|
||||||
|
hash = "sha256-9h4SHGnlJzmTENUp6226hC8fQ73QrQC3D85NNMxLuXg="
|
||||||
|
[mod."golang.org/x/text"]
|
||||||
|
version = "v0.3.8"
|
||||||
|
hash = "sha256-KKHU0OMfT4oxTIzIVqan8MY8wNvgBW00s24hF+kWbMA="
|
||||||
|
[mod."sourcery.dny.nu/longdistance"]
|
||||||
|
version = "v0.1.1"
|
||||||
|
hash = "sha256-fnLhJop/iQRIDn/P8DgYyC6JUqLWN83TH8jfj44Wjrg="
|
||||||
|
[mod."sourcery.dny.nu/pana"]
|
||||||
|
version = "v0.1.1"
|
||||||
|
hash = "sha256-+ueVinGEZuFCiW97/GZxUoFN83GL1asvfZbPm/HC4RQ="
|
||||||
8
package.nix
Normal file
8
package.nix
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{ pkgs }:
|
||||||
|
|
||||||
|
pkgs.buildGoApplication {
|
||||||
|
pname = "twink";
|
||||||
|
version = "0.0.1";
|
||||||
|
src = ./.;
|
||||||
|
modules = ./gomod2nix.toml;
|
||||||
|
}
|
||||||
24
vendor/github.com/alexflint/go-arg/.gitignore
generated
vendored
Normal file
24
vendor/github.com/alexflint/go-arg/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
24
vendor/github.com/alexflint/go-arg/LICENSE
generated
vendored
Normal file
24
vendor/github.com/alexflint/go-arg/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
Copyright (c) 2015, Alex Flint
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
800
vendor/github.com/alexflint/go-arg/README.md
generated
vendored
Normal file
800
vendor/github.com/alexflint/go-arg/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,800 @@
|
||||||
|
<h1 align="center">
|
||||||
|
<img src="./.github/banner.jpg" alt="go-arg" height="250px">
|
||||||
|
<br>
|
||||||
|
go-arg
|
||||||
|
</br>
|
||||||
|
</h1>
|
||||||
|
<h4 align="center">Struct-based argument parsing for Go</h4>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://sourcegraph.com/github.com/alexflint/go-arg?badge"><img src="https://sourcegraph.com/github.com/alexflint/go-arg/-/badge.svg" alt="Sourcegraph"></a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/alexflint/go-arg"><img src="https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square" alt="Documentation"></a>
|
||||||
|
<a href="https://github.com/alexflint/go-arg/actions"><img src="https://github.com/alexflint/go-arg/workflows/Go/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://codecov.io/gh/alexflint/go-arg"><img src="https://codecov.io/gh/alexflint/go-arg/branch/master/graph/badge.svg" alt="Coverage Status"></a>
|
||||||
|
<a href="https://goreportcard.com/report/github.com/alexflint/go-arg"><img src="https://goreportcard.com/badge/github.com/alexflint/go-arg" alt="Go Report Card"></a>
|
||||||
|
</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
Declare command line arguments for your program by defining a struct.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Foo string
|
||||||
|
Bar bool
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Println(args.Foo, args.Bar)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example --foo=hello --bar
|
||||||
|
hello true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get github.com/alexflint/go-arg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required arguments
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
ID int `arg:"required"`
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example
|
||||||
|
Usage: example --id ID [--timeout TIMEOUT]
|
||||||
|
error: --id is required
|
||||||
|
```
|
||||||
|
|
||||||
|
### Positional arguments
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Input string `arg:"positional"`
|
||||||
|
Output []string `arg:"positional"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Println("Input:", args.Input)
|
||||||
|
fmt.Println("Output:", args.Output)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example src.txt x.out y.out z.out
|
||||||
|
Input: src.txt
|
||||||
|
Output: [x.out y.out z.out]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Workers int `arg:"env"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Println("Workers:", args.Workers)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ WORKERS=4 ./example
|
||||||
|
Workers: 4
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ WORKERS=4 ./example --workers=6
|
||||||
|
Workers: 6
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also override the name of the environment variable:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Workers int `arg:"env:NUM_WORKERS"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Println("Workers:", args.Workers)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ NUM_WORKERS=4 ./example
|
||||||
|
Workers: 4
|
||||||
|
```
|
||||||
|
|
||||||
|
You can provide multiple values in environment variables using commas:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Workers []int `arg:"env"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Println("Workers:", args.Workers)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ WORKERS='1,99' ./example
|
||||||
|
Workers: [1 99]
|
||||||
|
```
|
||||||
|
|
||||||
|
Command line arguments take precedence over environment variables:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Workers int `arg:"--count,env:NUM_WORKERS"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Println("Workers:", args.Workers)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ NUM_WORKERS=6 ./example
|
||||||
|
Workers: 6
|
||||||
|
$ NUM_WORKERS=6 ./example --count 4
|
||||||
|
Workers: 4
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuring a global environment variable name prefix is also possible:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Workers int `arg:"--count,env:NUM_WORKERS"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := arg.NewParser(arg.Config{
|
||||||
|
EnvPrefix: "MYAPP_",
|
||||||
|
}, &args)
|
||||||
|
|
||||||
|
p.MustParse(os.Args[1:])
|
||||||
|
fmt.Println("Workers:", args.Workers)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ MYAPP_NUM_WORKERS=6 ./example
|
||||||
|
Workers: 6
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage strings
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Input string `arg:"positional"`
|
||||||
|
Output []string `arg:"positional"`
|
||||||
|
Verbose bool `arg:"-v,--verbose" help:"verbosity level"`
|
||||||
|
Dataset string `help:"dataset to use"`
|
||||||
|
Optimize int `arg:"-O" help:"optimization level"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example -h
|
||||||
|
Usage: [--verbose] [--dataset DATASET] [--optimize OPTIMIZE] [--help] INPUT [OUTPUT [OUTPUT ...]]
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
INPUT
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--verbose, -v verbosity level
|
||||||
|
--dataset DATASET dataset to use
|
||||||
|
--optimize OPTIMIZE, -O OPTIMIZE
|
||||||
|
optimization level
|
||||||
|
--help, -h print this help message
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default values
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Foo string `default:"abc"`
|
||||||
|
Bar bool
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
```
|
||||||
|
|
||||||
|
Command line arguments take precedence over environment variables, which take precedence over default values. This means that we check whether a certain option was provided on the command line, then if not, we check for an environment variable (only if an `env` tag was provided), then if none is found, we check for a `default` tag containing a default value.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Test string `arg:"-t,env:TEST" default:"something"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Ignoring environment variables and/or default values
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Test string `arg:"-t,env:TEST" default:"something"`
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := arg.NewParser(arg.Config{
|
||||||
|
IgnoreEnv: true,
|
||||||
|
IgnoreDefault: true,
|
||||||
|
}, &args)
|
||||||
|
|
||||||
|
err = p.Parse(os.Args[1:])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments with multiple values
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Database string
|
||||||
|
IDs []int64
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Printf("Fetching the following IDs from %s: %q", args.Database, args.IDs)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./example -database foo -ids 1 2 3
|
||||||
|
Fetching the following IDs from foo: [1 2 3]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments that can be specified multiple times, mixed with positionals
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Commands []string `arg:"-c,separate"`
|
||||||
|
Files []string `arg:"-f,separate"`
|
||||||
|
Databases []string `arg:"positional"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./example -c cmd1 db1 -f file1 db2 -c cmd2 -f file2 -f file3 db3 -c cmd3
|
||||||
|
Commands: [cmd1 cmd2 cmd3]
|
||||||
|
Files [file1 file2 file3]
|
||||||
|
Databases [db1 db2 db3]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arguments with keys and values
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
UserIDs map[string]int
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Println(args.UserIDs)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./example --userids john=123 mary=456
|
||||||
|
map[john:123 mary:456]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Version strings
|
||||||
|
|
||||||
|
```go
|
||||||
|
type args struct {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args) Version() string {
|
||||||
|
return "someprogram 4.3.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args args
|
||||||
|
arg.MustParse(&args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example --version
|
||||||
|
someprogram 4.3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> If a `--version` flag is defined in `args` or any subcommand, it overrides the built-in versioning.
|
||||||
|
|
||||||
|
### Custom validation
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Foo string
|
||||||
|
Bar string
|
||||||
|
}
|
||||||
|
p := arg.MustParse(&args)
|
||||||
|
if args.Foo == "" && args.Bar == "" {
|
||||||
|
p.Fail("you must provide either --foo or --bar")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
./example
|
||||||
|
Usage: samples [--foo FOO] [--bar BAR]
|
||||||
|
error: you must provide either --foo or --bar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Overriding option names
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Short string `arg:"-s"`
|
||||||
|
Long string `arg:"--custom-long-option"`
|
||||||
|
ShortAndLong string `arg:"-x,--my-option"`
|
||||||
|
OnlyShort string `arg:"-o,--"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example --help
|
||||||
|
Usage: example [-o ONLYSHORT] [--short SHORT] [--custom-long-option CUSTOM-LONG-OPTION] [--my-option MY-OPTION]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--short SHORT, -s SHORT
|
||||||
|
--custom-long-option CUSTOM-LONG-OPTION
|
||||||
|
--my-option MY-OPTION, -x MY-OPTION
|
||||||
|
-o ONLYSHORT
|
||||||
|
--help, -h display this help and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Embedded structs
|
||||||
|
|
||||||
|
The fields of embedded structs are treated just like regular fields:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type DatabaseOptions struct {
|
||||||
|
Host string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogOptions struct {
|
||||||
|
LogFile string
|
||||||
|
Verbose bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args struct {
|
||||||
|
DatabaseOptions
|
||||||
|
LogOptions
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
As usual, any field tagged with `arg:"-"` is ignored.
|
||||||
|
|
||||||
|
### Supported types
|
||||||
|
|
||||||
|
The following types may be used as arguments:
|
||||||
|
- built-in integer types: `int, int8, int16, int32, int64, byte, rune`
|
||||||
|
- built-in floating point types: `float32, float64`
|
||||||
|
- strings
|
||||||
|
- booleans
|
||||||
|
- URLs represented as `url.URL`
|
||||||
|
- time durations represented as `time.Duration`
|
||||||
|
- email addresses represented as `mail.Address`
|
||||||
|
- MAC addresses represented as `net.HardwareAddr`
|
||||||
|
- pointers to any of the above
|
||||||
|
- slices of any of the above
|
||||||
|
- maps using any of the above as keys and values
|
||||||
|
- any type that implements `encoding.TextUnmarshaler`
|
||||||
|
|
||||||
|
### Custom parsing
|
||||||
|
|
||||||
|
Implement `encoding.TextUnmarshaler` to define your own parsing logic.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Accepts command line arguments of the form "head.tail"
|
||||||
|
type NameDotName struct {
|
||||||
|
Head, Tail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NameDotName) UnmarshalText(b []byte) error {
|
||||||
|
s := string(b)
|
||||||
|
pos := strings.Index(s, ".")
|
||||||
|
if pos == -1 {
|
||||||
|
return fmt.Errorf("missing period in %s", s)
|
||||||
|
}
|
||||||
|
n.Head = s[:pos]
|
||||||
|
n.Tail = s[pos+1:]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args struct {
|
||||||
|
Name NameDotName
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Printf("%#v\n", args.Name)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example --name=foo.bar
|
||||||
|
main.NameDotName{Head:"foo", Tail:"bar"}
|
||||||
|
|
||||||
|
$ ./example --name=oops
|
||||||
|
Usage: example [--name NAME]
|
||||||
|
error: error processing --name: missing period in "oops"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom parsing with default values
|
||||||
|
|
||||||
|
Implement `encoding.TextMarshaler` to define your own default value strings:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Accepts command line arguments of the form "head.tail"
|
||||||
|
type NameDotName struct {
|
||||||
|
Head, Tail string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *NameDotName) UnmarshalText(b []byte) error {
|
||||||
|
// same as previous example
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is only needed if you want to display a default value in the usage string
|
||||||
|
func (n *NameDotName) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(fmt.Sprintf("%s.%s", n.Head, n.Tail)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args struct {
|
||||||
|
Name NameDotName `default:"file.txt"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
fmt.Printf("%#v\n", args.Name)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example --help
|
||||||
|
Usage: test [--name NAME]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--name NAME [default: file.txt]
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
$ ./example
|
||||||
|
main.NameDotName{Head:"file", Tail:"txt"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom placeholders
|
||||||
|
|
||||||
|
Use the `placeholder` tag to control which placeholder text is used in the usage text.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Input string `arg:"positional" placeholder:"SRC"`
|
||||||
|
Output []string `arg:"positional" placeholder:"DST"`
|
||||||
|
Optimize int `arg:"-O" help:"optimization level" placeholder:"LEVEL"`
|
||||||
|
MaxJobs int `arg:"-j" help:"maximum number of simultaneous jobs" placeholder:"N"`
|
||||||
|
}
|
||||||
|
arg.MustParse(&args)
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example -h
|
||||||
|
Usage: example [--optimize LEVEL] [--maxjobs N] SRC [DST [DST ...]]
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
SRC
|
||||||
|
DST
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--optimize LEVEL, -O LEVEL
|
||||||
|
optimization level
|
||||||
|
--maxjobs N, -j N maximum number of simultaneous jobs
|
||||||
|
--help, -h display this help and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Description strings
|
||||||
|
|
||||||
|
A descriptive message can be added at the top of the help text by implementing
|
||||||
|
a `Description` function that returns a string.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type args struct {
|
||||||
|
Foo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args) Description() string {
|
||||||
|
return "this program does this and that"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args args
|
||||||
|
arg.MustParse(&args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example -h
|
||||||
|
this program does this and that
|
||||||
|
Usage: example [--foo FOO]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--foo FOO
|
||||||
|
--help, -h display this help and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly an epilogue can be added at the end of the help text by implementing
|
||||||
|
the `Epilogue` function.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type args struct {
|
||||||
|
Foo string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args) Epilogue() string {
|
||||||
|
return "For more information visit github.com/alexflint/go-arg"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args args
|
||||||
|
arg.MustParse(&args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example -h
|
||||||
|
Usage: example [--foo FOO]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--foo FOO
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
For more information visit github.com/alexflint/go-arg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Subcommands
|
||||||
|
|
||||||
|
Subcommands are commonly used in tools that wish to group multiple functions into a single program. An example is the `git` tool:
|
||||||
|
```shell
|
||||||
|
$ git checkout [arguments specific to checking out code]
|
||||||
|
$ git commit [arguments specific to committing]
|
||||||
|
$ git push [arguments specific to pushing]
|
||||||
|
```
|
||||||
|
|
||||||
|
The strings "checkout", "commit", and "push" are different from simple positional arguments because the options available to the user change depending on which subcommand they choose.
|
||||||
|
|
||||||
|
This can be implemented with `go-arg` as follows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CheckoutCmd struct {
|
||||||
|
Branch string `arg:"positional"`
|
||||||
|
Track bool `arg:"-t"`
|
||||||
|
}
|
||||||
|
type CommitCmd struct {
|
||||||
|
All bool `arg:"-a"`
|
||||||
|
Message string `arg:"-m"`
|
||||||
|
}
|
||||||
|
type PushCmd struct {
|
||||||
|
Remote string `arg:"positional"`
|
||||||
|
Branch string `arg:"positional"`
|
||||||
|
SetUpstream bool `arg:"-u"`
|
||||||
|
}
|
||||||
|
var args struct {
|
||||||
|
Checkout *CheckoutCmd `arg:"subcommand:checkout"`
|
||||||
|
Commit *CommitCmd `arg:"subcommand:commit"`
|
||||||
|
Push *PushCmd `arg:"subcommand:push"`
|
||||||
|
Quiet bool `arg:"-q"` // this flag is global to all subcommands
|
||||||
|
}
|
||||||
|
|
||||||
|
arg.MustParse(&args)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case args.Checkout != nil:
|
||||||
|
fmt.Printf("checkout requested for branch %s\n", args.Checkout.Branch)
|
||||||
|
case args.Commit != nil:
|
||||||
|
fmt.Printf("commit requested with message \"%s\"\n", args.Commit.Message)
|
||||||
|
case args.Push != nil:
|
||||||
|
fmt.Printf("push requested from %s to %s\n", args.Push.Branch, args.Push.Remote)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Some additional rules apply when working with subcommands:
|
||||||
|
* The `subcommand` tag can only be used with fields that are pointers to structs
|
||||||
|
* Any struct that contains a subcommand must not contain any positionals
|
||||||
|
|
||||||
|
This package allows to have a program that accepts subcommands, but also does something else
|
||||||
|
when no subcommands are specified.
|
||||||
|
If on the other hand you want the program to terminate when no subcommands are specified,
|
||||||
|
the recommended way is:
|
||||||
|
|
||||||
|
```go
|
||||||
|
p := arg.MustParse(&args)
|
||||||
|
if p.Subcommand() == nil {
|
||||||
|
p.Fail("missing subcommand")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom handling of --help and --version
|
||||||
|
|
||||||
|
The following reproduces the internal logic of `MustParse` for the simple case where
|
||||||
|
you are not using subcommands or --version. This allows you to respond
|
||||||
|
programatically to --help, and to any errors that come up.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var args struct {
|
||||||
|
Something string
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := arg.NewParser(arg.Config{}, &args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.Parse(os.Args[1:])
|
||||||
|
switch {
|
||||||
|
case err == arg.ErrHelp: // indicates that user wrote "--help" on command line
|
||||||
|
p.WriteHelp(os.Stdout)
|
||||||
|
os.Exit(0)
|
||||||
|
case err != nil:
|
||||||
|
fmt.Printf("error: %v\n", err)
|
||||||
|
p.WriteUsage(os.Stdout)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ go run ./example --help
|
||||||
|
Usage: ./example --something SOMETHING
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--something SOMETHING
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
$ ./example --wrong
|
||||||
|
error: unknown argument --wrong
|
||||||
|
Usage: ./example --something SOMETHING
|
||||||
|
|
||||||
|
$ ./example
|
||||||
|
error: --something is required
|
||||||
|
Usage: ./example --something SOMETHING
|
||||||
|
```
|
||||||
|
|
||||||
|
To also handle --version programatically, use the following:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type args struct {
|
||||||
|
Something string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args) Version() string {
|
||||||
|
return "1.2.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args args
|
||||||
|
p, err := arg.NewParser(arg.Config{}, &args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.Parse(os.Args[1:])
|
||||||
|
switch {
|
||||||
|
case err == arg.ErrHelp: // found "--help" on command line
|
||||||
|
p.WriteHelp(os.Stdout)
|
||||||
|
os.Exit(0)
|
||||||
|
case err == arg.ErrVersion: // found "--version" on command line
|
||||||
|
fmt.Println(args.Version())
|
||||||
|
os.Exit(0)
|
||||||
|
case err != nil:
|
||||||
|
fmt.Printf("error: %v\n", err)
|
||||||
|
p.WriteUsage(os.Stdout)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("got %q\n", args.Something)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example --version
|
||||||
|
1.2.3
|
||||||
|
|
||||||
|
$ go run ./example --help
|
||||||
|
1.2.3
|
||||||
|
Usage: example --something SOMETHING
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--something SOMETHING
|
||||||
|
--help, -h display this help and exit
|
||||||
|
|
||||||
|
$ ./example --wrong
|
||||||
|
1.2.3
|
||||||
|
error: unknown argument --wrong
|
||||||
|
Usage: example --something SOMETHING
|
||||||
|
|
||||||
|
$ ./example
|
||||||
|
error: --something is required
|
||||||
|
Usage: example --something SOMETHING
|
||||||
|
```
|
||||||
|
|
||||||
|
To generate subcommand-specific help messages, use the following most general version
|
||||||
|
(this also works in absence of subcommands but is a bit more complex):
|
||||||
|
|
||||||
|
```go
|
||||||
|
type fetchCmd struct {
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
Something string
|
||||||
|
Fetch *fetchCmd `arg:"subcommand"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (args) Version() string {
|
||||||
|
return "1.2.3"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args args
|
||||||
|
p, err := arg.NewParser(arg.Config{}, &args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("there was an error in the definition of the Go struct: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.Parse(os.Args[1:])
|
||||||
|
switch {
|
||||||
|
case err == arg.ErrHelp: // found "--help" on command line
|
||||||
|
p.WriteHelpForSubcommand(os.Stdout, p.SubcommandNames()...)
|
||||||
|
os.Exit(0)
|
||||||
|
case err == arg.ErrVersion: // found "--version" on command line
|
||||||
|
fmt.Println(args.Version())
|
||||||
|
os.Exit(0)
|
||||||
|
case err != nil:
|
||||||
|
fmt.Printf("error: %v\n", err)
|
||||||
|
p.WriteUsageForSubcommand(os.Stdout, p.SubcommandNames()...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ ./example --version
|
||||||
|
1.2.3
|
||||||
|
|
||||||
|
$ ./example --help
|
||||||
|
1.2.3
|
||||||
|
Usage: example [--something SOMETHING] <command> [<args>]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--something SOMETHING
|
||||||
|
--help, -h display this help and exit
|
||||||
|
--version display version and exit
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
fetch
|
||||||
|
|
||||||
|
$ ./example fetch --help
|
||||||
|
1.2.3
|
||||||
|
Usage: example fetch [--count COUNT]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--count COUNT
|
||||||
|
|
||||||
|
Global options:
|
||||||
|
--something SOMETHING
|
||||||
|
--help, -h display this help and exit
|
||||||
|
--version display version and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
|
||||||
|
https://pkg.go.dev/github.com/alexflint/go-arg
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
There are many command line argument parsing libraries for Go, including one in the standard library, so why build another?
|
||||||
|
|
||||||
|
The `flag` library that ships in the standard library seems awkward to me. Positional arguments must precede options, so `./prog x --foo=1` does what you expect but `./prog --foo=1 x` does not. It also does not allow arguments to have both long (`--foo`) and short (`-f`) forms.
|
||||||
|
|
||||||
|
Many third-party argument parsing libraries are great for writing sophisticated command line interfaces, but feel to me like overkill for a simple script with a few flags.
|
||||||
|
|
||||||
|
The idea behind `go-arg` is that Go already has an excellent way to describe data structures using structs, so there is no need to develop additional levels of abstraction. Instead of one API to specify which arguments your program accepts, and then another API to get the values of those arguments, `go-arg` replaces both with a single struct.
|
||||||
|
|
||||||
|
### Backward compatibility notes
|
||||||
|
|
||||||
|
Earlier versions of this library required the help text to be part of the `arg` tag. This is still supported but is now deprecated. Instead, you should use a separate `help` tag, described above, which makes it possible to include commas inside help text.
|
||||||
39
vendor/github.com/alexflint/go-arg/doc.go
generated
vendored
Normal file
39
vendor/github.com/alexflint/go-arg/doc.go
generated
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Package arg parses command line arguments using the fields from a struct.
|
||||||
|
//
|
||||||
|
// For example,
|
||||||
|
//
|
||||||
|
// var args struct {
|
||||||
|
// Iter int
|
||||||
|
// Debug bool
|
||||||
|
// }
|
||||||
|
// arg.MustParse(&args)
|
||||||
|
//
|
||||||
|
// defines two command line arguments, which can be set using any of
|
||||||
|
//
|
||||||
|
// ./example --iter=1 --debug // debug is a boolean flag so its value is set to true
|
||||||
|
// ./example -iter 1 // debug defaults to its zero value (false)
|
||||||
|
// ./example --debug=true // iter defaults to its zero value (zero)
|
||||||
|
//
|
||||||
|
// The fastest way to see how to use go-arg is to read the examples below.
|
||||||
|
//
|
||||||
|
// Fields can be bool, string, any float type, or any signed or unsigned integer type.
|
||||||
|
// They can also be slices of any of the above, or slices of pointers to any of the above.
|
||||||
|
//
|
||||||
|
// Tags can be specified using the `arg` and `help` tag names:
|
||||||
|
//
|
||||||
|
// var args struct {
|
||||||
|
// Input string `arg:"positional"`
|
||||||
|
// Log string `arg:"positional,required"`
|
||||||
|
// Debug bool `arg:"-d" help:"turn on debug mode"`
|
||||||
|
// RealMode bool `arg:"--real"
|
||||||
|
// Wr io.Writer `arg:"-"`
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Any tag string that starts with a single hyphen is the short form for an argument
|
||||||
|
// (e.g. `./example -d`), and any tag string that starts with two hyphens is the long
|
||||||
|
// form for the argument (instead of the field name).
|
||||||
|
//
|
||||||
|
// Other valid tag strings are `positional` and `required`.
|
||||||
|
//
|
||||||
|
// Fields can be excluded from processing with `arg:"-"`.
|
||||||
|
package arg
|
||||||
872
vendor/github.com/alexflint/go-arg/parse.go
generated
vendored
Normal file
872
vendor/github.com/alexflint/go-arg/parse.go
generated
vendored
Normal file
|
|
@ -0,0 +1,872 @@
|
||||||
|
package arg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"encoding/csv"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
scalar "github.com/alexflint/go-scalar"
|
||||||
|
)
|
||||||
|
|
||||||
|
// path represents a sequence of steps to find the output location for an
|
||||||
|
// argument or subcommand in the final destination struct
|
||||||
|
type path struct {
|
||||||
|
root int // index of the destination struct
|
||||||
|
fields []reflect.StructField // sequence of struct fields to traverse
|
||||||
|
}
|
||||||
|
|
||||||
|
// String gets a string representation of the given path
|
||||||
|
func (p path) String() string {
|
||||||
|
s := "args"
|
||||||
|
for _, f := range p.fields {
|
||||||
|
s += "." + f.Name
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child gets a new path representing a child of this path.
|
||||||
|
func (p path) Child(f reflect.StructField) path {
|
||||||
|
// copy the entire slice of fields to avoid possible slice overwrite
|
||||||
|
subfields := make([]reflect.StructField, len(p.fields)+1)
|
||||||
|
copy(subfields, p.fields)
|
||||||
|
subfields[len(subfields)-1] = f
|
||||||
|
return path{
|
||||||
|
root: p.root,
|
||||||
|
fields: subfields,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spec represents a command line option
|
||||||
|
type spec struct {
|
||||||
|
dest path
|
||||||
|
field reflect.StructField // the struct field from which this option was created
|
||||||
|
long string // the --long form for this option, or empty if none
|
||||||
|
short string // the -s short form for this option, or empty if none
|
||||||
|
cardinality cardinality // determines how many tokens will be present (possible values: zero, one, multiple)
|
||||||
|
required bool // if true, this option must be present on the command line
|
||||||
|
positional bool // if true, this option will be looked for in the positional flags
|
||||||
|
separate bool // if true, each slice and map entry will have its own --flag
|
||||||
|
help string // the help text for this option
|
||||||
|
env string // the name of the environment variable for this option, or empty for none
|
||||||
|
defaultValue reflect.Value // default value for this option
|
||||||
|
defaultString string // default value for this option, in string form to be displayed in help text
|
||||||
|
placeholder string // placeholder string in help
|
||||||
|
}
|
||||||
|
|
||||||
|
// command represents a named subcommand, or the top-level command
|
||||||
|
type command struct {
|
||||||
|
name string
|
||||||
|
aliases []string
|
||||||
|
help string
|
||||||
|
dest path
|
||||||
|
specs []*spec
|
||||||
|
subcommands []*command
|
||||||
|
parent *command
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrHelp indicates that the builtin -h or --help were provided
|
||||||
|
var ErrHelp = errors.New("help requested by user")
|
||||||
|
|
||||||
|
// ErrVersion indicates that the builtin --version was provided
|
||||||
|
var ErrVersion = errors.New("version requested by user")
|
||||||
|
|
||||||
|
// for monkey patching in example and test code
|
||||||
|
var mustParseExit = os.Exit
|
||||||
|
var mustParseOut io.Writer = os.Stdout
|
||||||
|
|
||||||
|
// MustParse processes command line arguments and exits upon failure
|
||||||
|
func MustParse(dest ...interface{}) *Parser {
|
||||||
|
return mustParse(Config{Exit: mustParseExit, Out: mustParseOut}, dest...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustParse is a helper that facilitates testing
|
||||||
|
func mustParse(config Config, dest ...interface{}) *Parser {
|
||||||
|
p, err := NewParser(config, dest...)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(config.Out, err)
|
||||||
|
config.Exit(2)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.MustParse(flags())
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse processes command line arguments and stores them in dest
|
||||||
|
func Parse(dest ...interface{}) error {
|
||||||
|
p, err := NewParser(Config{}, dest...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return p.Parse(flags())
|
||||||
|
}
|
||||||
|
|
||||||
|
// flags gets all command line arguments other than the first (program name)
|
||||||
|
func flags() []string {
|
||||||
|
if len(os.Args) == 0 { // os.Args could be empty
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return os.Args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represents configuration options for an argument parser
|
||||||
|
type Config struct {
|
||||||
|
// Program is the name of the program used in the help text
|
||||||
|
Program string
|
||||||
|
|
||||||
|
// IgnoreEnv instructs the library not to read environment variables
|
||||||
|
IgnoreEnv bool
|
||||||
|
|
||||||
|
// IgnoreDefault instructs the library not to reset the variables to the
|
||||||
|
// default values, including pointers to sub commands
|
||||||
|
IgnoreDefault bool
|
||||||
|
|
||||||
|
// StrictSubcommands intructs the library not to allow global commands after
|
||||||
|
// subcommand
|
||||||
|
StrictSubcommands bool
|
||||||
|
|
||||||
|
// EnvPrefix instructs the library to use a name prefix when reading environment variables.
|
||||||
|
EnvPrefix string
|
||||||
|
|
||||||
|
// Exit is called to terminate the process with an error code (defaults to os.Exit)
|
||||||
|
Exit func(int)
|
||||||
|
|
||||||
|
// Out is where help text, usage text, and failure messages are printed (defaults to os.Stdout)
|
||||||
|
Out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parser represents a set of command line options with destination values
|
||||||
|
type Parser struct {
|
||||||
|
cmd *command
|
||||||
|
roots []reflect.Value
|
||||||
|
config Config
|
||||||
|
version string
|
||||||
|
description string
|
||||||
|
epilogue string
|
||||||
|
|
||||||
|
// the following field changes during processing of command line arguments
|
||||||
|
subcommand []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Versioned is the interface that the destination struct should implement to
|
||||||
|
// make a version string appear at the top of the help message.
|
||||||
|
type Versioned interface {
|
||||||
|
// Version returns the version string that will be printed on a line by itself
|
||||||
|
// at the top of the help message.
|
||||||
|
Version() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Described is the interface that the destination struct should implement to
|
||||||
|
// make a description string appear at the top of the help message.
|
||||||
|
type Described interface {
|
||||||
|
// Description returns the string that will be printed on a line by itself
|
||||||
|
// at the top of the help message.
|
||||||
|
Description() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Epilogued is the interface that the destination struct should implement to
|
||||||
|
// add an epilogue string at the bottom of the help message.
|
||||||
|
type Epilogued interface {
|
||||||
|
// Epilogue returns the string that will be printed on a line by itself
|
||||||
|
// at the end of the help message.
|
||||||
|
Epilogue() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// walkFields calls a function for each field of a struct, recursively expanding struct fields.
|
||||||
|
func walkFields(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool) {
|
||||||
|
walkFieldsImpl(t, visit, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func walkFieldsImpl(t reflect.Type, visit func(field reflect.StructField, owner reflect.Type) bool, path []int) {
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
field.Index = make([]int, len(path)+1)
|
||||||
|
copy(field.Index, append(path, i))
|
||||||
|
expand := visit(field, t)
|
||||||
|
if expand && field.Type.Kind() == reflect.Struct {
|
||||||
|
var subpath []int
|
||||||
|
if field.Anonymous {
|
||||||
|
subpath = append(path, i)
|
||||||
|
}
|
||||||
|
walkFieldsImpl(field.Type, visit, subpath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewParser constructs a parser from a list of destination structs
|
||||||
|
func NewParser(config Config, dests ...interface{}) (*Parser, error) {
|
||||||
|
// fill in defaults
|
||||||
|
if config.Exit == nil {
|
||||||
|
config.Exit = os.Exit
|
||||||
|
}
|
||||||
|
if config.Out == nil {
|
||||||
|
config.Out = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
// first pick a name for the command for use in the usage text
|
||||||
|
var name string
|
||||||
|
switch {
|
||||||
|
case config.Program != "":
|
||||||
|
name = config.Program
|
||||||
|
case len(os.Args) > 0:
|
||||||
|
name = filepath.Base(os.Args[0])
|
||||||
|
default:
|
||||||
|
name = "program"
|
||||||
|
}
|
||||||
|
|
||||||
|
// construct a parser
|
||||||
|
p := Parser{
|
||||||
|
cmd: &command{name: name},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a list of roots
|
||||||
|
for _, dest := range dests {
|
||||||
|
p.roots = append(p.roots, reflect.ValueOf(dest))
|
||||||
|
}
|
||||||
|
|
||||||
|
// process each of the destination values
|
||||||
|
for i, dest := range dests {
|
||||||
|
t := reflect.TypeOf(dest)
|
||||||
|
if t.Kind() != reflect.Ptr {
|
||||||
|
panic(fmt.Sprintf("%s is not a pointer (did you forget an ampersand?)", t))
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd, err := cmdFromStruct(name, path{root: i}, t, config.EnvPrefix)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// for backwards compatibility, add nonzero field values as defaults
|
||||||
|
// this applies only to the top-level command, not to subcommands (this inconsistency
|
||||||
|
// is the reason that this method for setting default values was deprecated)
|
||||||
|
for _, spec := range cmd.specs {
|
||||||
|
// get the value
|
||||||
|
v := p.val(spec.dest)
|
||||||
|
|
||||||
|
// if the value is the "zero value" (e.g. nil pointer, empty struct) then ignore
|
||||||
|
if isZero(v) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// store as a default
|
||||||
|
spec.defaultValue = v
|
||||||
|
|
||||||
|
// we need a string to display in help text
|
||||||
|
// if MarshalText is implemented then use that
|
||||||
|
if m, ok := v.Interface().(encoding.TextMarshaler); ok {
|
||||||
|
s, err := m.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%v: error marshaling default value to string: %v", spec.dest, err)
|
||||||
|
}
|
||||||
|
spec.defaultString = string(s)
|
||||||
|
} else {
|
||||||
|
spec.defaultString = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.cmd.specs = append(p.cmd.specs, cmd.specs...)
|
||||||
|
p.cmd.subcommands = append(p.cmd.subcommands, cmd.subcommands...)
|
||||||
|
|
||||||
|
if dest, ok := dest.(Versioned); ok {
|
||||||
|
p.version = dest.Version()
|
||||||
|
}
|
||||||
|
if dest, ok := dest.(Described); ok {
|
||||||
|
p.description = dest.Description()
|
||||||
|
}
|
||||||
|
if dest, ok := dest.(Epilogued); ok {
|
||||||
|
p.epilogue = dest.Epilogue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the parent of the subcommands to be the top-level command
|
||||||
|
// to make sure that global options work when there is more than one
|
||||||
|
// dest supplied.
|
||||||
|
for _, subcommand := range p.cmd.subcommands {
|
||||||
|
subcommand.parent = p.cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
return &p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cmdFromStruct(name string, dest path, t reflect.Type, envPrefix string) (*command, error) {
|
||||||
|
// commands can only be created from pointers to structs
|
||||||
|
if t.Kind() != reflect.Ptr {
|
||||||
|
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a %s",
|
||||||
|
dest, t.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
t = t.Elem()
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("subcommands must be pointers to structs but %s is a pointer to %s",
|
||||||
|
dest, t.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := command{
|
||||||
|
name: name,
|
||||||
|
dest: dest,
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs []string
|
||||||
|
walkFields(t, func(field reflect.StructField, t reflect.Type) bool {
|
||||||
|
// check for the ignore switch in the tag
|
||||||
|
tag := field.Tag.Get("arg")
|
||||||
|
if tag == "-" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is an embedded struct then recurse into its fields, even if
|
||||||
|
// it is unexported, because exported fields on unexported embedded
|
||||||
|
// structs are still writable
|
||||||
|
if field.Anonymous && field.Type.Kind() == reflect.Struct {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore any other unexported field
|
||||||
|
if !isExported(field.Name) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// duplicate the entire path to avoid slice overwrites
|
||||||
|
subdest := dest.Child(field)
|
||||||
|
spec := spec{
|
||||||
|
dest: subdest,
|
||||||
|
field: field,
|
||||||
|
long: strings.ToLower(field.Name),
|
||||||
|
}
|
||||||
|
|
||||||
|
help, exists := field.Tag.Lookup("help")
|
||||||
|
if exists {
|
||||||
|
spec.help = help
|
||||||
|
}
|
||||||
|
|
||||||
|
// process each comma-separated part of the tag
|
||||||
|
var isSubcommand bool
|
||||||
|
for _, key := range strings.Split(tag, ",") {
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key = strings.TrimLeft(key, " ")
|
||||||
|
var value string
|
||||||
|
if pos := strings.Index(key, ":"); pos != -1 {
|
||||||
|
value = key[pos+1:]
|
||||||
|
key = key[:pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(key, "---"):
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: too many hyphens", t.Name(), field.Name))
|
||||||
|
case strings.HasPrefix(key, "--"):
|
||||||
|
spec.long = key[2:]
|
||||||
|
case strings.HasPrefix(key, "-"):
|
||||||
|
if len(key) > 2 {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: short arguments must be one character only",
|
||||||
|
t.Name(), field.Name))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
spec.short = key[1:]
|
||||||
|
case key == "required":
|
||||||
|
spec.required = true
|
||||||
|
case key == "positional":
|
||||||
|
spec.positional = true
|
||||||
|
case key == "separate":
|
||||||
|
spec.separate = true
|
||||||
|
case key == "help": // deprecated
|
||||||
|
spec.help = value
|
||||||
|
case key == "env":
|
||||||
|
// Use override name if provided
|
||||||
|
if value != "" {
|
||||||
|
spec.env = envPrefix + value
|
||||||
|
} else {
|
||||||
|
spec.env = envPrefix + strings.ToUpper(field.Name)
|
||||||
|
}
|
||||||
|
case key == "subcommand":
|
||||||
|
// decide on a name for the subcommand
|
||||||
|
var cmdnames []string
|
||||||
|
if value == "" {
|
||||||
|
cmdnames = []string{strings.ToLower(field.Name)}
|
||||||
|
} else {
|
||||||
|
cmdnames = strings.Split(value, "|")
|
||||||
|
}
|
||||||
|
for i := range cmdnames {
|
||||||
|
cmdnames[i] = strings.TrimSpace(cmdnames[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the subcommand recursively
|
||||||
|
subcmd, err := cmdFromStruct(cmdnames[0], subdest, field.Type, envPrefix)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
subcmd.aliases = cmdnames[1:]
|
||||||
|
subcmd.parent = &cmd
|
||||||
|
subcmd.help = field.Tag.Get("help")
|
||||||
|
|
||||||
|
cmd.subcommands = append(cmd.subcommands, subcmd)
|
||||||
|
isSubcommand = true
|
||||||
|
default:
|
||||||
|
errs = append(errs, fmt.Sprintf("unrecognized tag '%s' on field %s", key, tag))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// placeholder is the string used in the help text like this: "--somearg PLACEHOLDER"
|
||||||
|
placeholder, hasPlaceholder := field.Tag.Lookup("placeholder")
|
||||||
|
if hasPlaceholder {
|
||||||
|
spec.placeholder = placeholder
|
||||||
|
} else if spec.long != "" {
|
||||||
|
spec.placeholder = strings.ToUpper(spec.long)
|
||||||
|
} else {
|
||||||
|
spec.placeholder = strings.ToUpper(spec.field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if this is a subcommand then we've done everything we need to do
|
||||||
|
if isSubcommand {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether this field is supported. It's good to do this here rather than
|
||||||
|
// wait until ParseValue because it means that a program with invalid argument
|
||||||
|
// fields will always fail regardless of whether the arguments it received
|
||||||
|
// exercised those fields.
|
||||||
|
var err error
|
||||||
|
spec.cardinality, err = cardinalityOf(field.Type)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: %s fields are not supported",
|
||||||
|
t.Name(), field.Name, field.Type.String()))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultString, hasDefault := field.Tag.Lookup("default")
|
||||||
|
if hasDefault {
|
||||||
|
// we do not support default values for maps and slices
|
||||||
|
if spec.cardinality == multiple {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: default values are not supported for slice or map fields",
|
||||||
|
t.Name(), field.Name))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// a required field cannot also have a default value
|
||||||
|
if spec.required {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: 'required' cannot be used when a default value is specified",
|
||||||
|
t.Name(), field.Name))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the default value
|
||||||
|
spec.defaultString = defaultString
|
||||||
|
if field.Type.Kind() == reflect.Ptr {
|
||||||
|
// here we have a field of type *T and we create a new T, no need to dereference
|
||||||
|
// in order for the value to be settable
|
||||||
|
spec.defaultValue = reflect.New(field.Type.Elem())
|
||||||
|
} else {
|
||||||
|
// here we have a field of type T and we create a new T and then dereference it
|
||||||
|
// so that the resulting value is settable
|
||||||
|
spec.defaultValue = reflect.New(field.Type).Elem()
|
||||||
|
}
|
||||||
|
err := scalar.ParseValue(spec.defaultValue, defaultString)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("%s.%s: error processing default value: %v", t.Name(), field.Name, err))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the spec to the list of specs
|
||||||
|
cmd.specs = append(cmd.specs, &spec)
|
||||||
|
|
||||||
|
// if this was an embedded field then we already returned true up above
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return nil, errors.New(strings.Join(errs, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that we don't have both positionals and subcommands
|
||||||
|
var hasPositional bool
|
||||||
|
for _, spec := range cmd.specs {
|
||||||
|
if spec.positional {
|
||||||
|
hasPositional = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasPositional && len(cmd.subcommands) > 0 {
|
||||||
|
return nil, fmt.Errorf("%s cannot have both subcommands and positional arguments", dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse processes the given command line option, storing the results in the fields
|
||||||
|
// of the structs from which NewParser was constructed.
|
||||||
|
//
|
||||||
|
// It returns ErrHelp if "--help" is one of the command line args and ErrVersion if
|
||||||
|
// "--version" is one of the command line args (the latter only applies if the
|
||||||
|
// destination struct passed to NewParser implements Versioned.)
|
||||||
|
//
|
||||||
|
// To respond to --help and --version in the way that MustParse does, see examples
|
||||||
|
// in the README under "Custom handling of --help and --version".
|
||||||
|
func (p *Parser) Parse(args []string) error {
|
||||||
|
err := p.process(args)
|
||||||
|
if err != nil {
|
||||||
|
// If -h or --help were specified then make sure help text supercedes other errors
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "-h" || arg == "--help" {
|
||||||
|
return ErrHelp
|
||||||
|
}
|
||||||
|
if arg == "--" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) MustParse(args []string) {
|
||||||
|
err := p.Parse(args)
|
||||||
|
switch {
|
||||||
|
case err == ErrHelp:
|
||||||
|
p.WriteHelpForSubcommand(p.config.Out, p.subcommand...)
|
||||||
|
p.config.Exit(0)
|
||||||
|
case err == ErrVersion:
|
||||||
|
fmt.Fprintln(p.config.Out, p.version)
|
||||||
|
p.config.Exit(0)
|
||||||
|
case err != nil:
|
||||||
|
p.FailSubcommand(err.Error(), p.subcommand...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// process environment vars for the given arguments
|
||||||
|
func (p *Parser) captureEnvVars(specs []*spec, wasPresent map[*spec]bool) error {
|
||||||
|
for _, spec := range specs {
|
||||||
|
if spec.env == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
value, found := os.LookupEnv(spec.env)
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.cardinality == multiple {
|
||||||
|
// expect a CSV string in an environment
|
||||||
|
// variable in the case of multiple values
|
||||||
|
var values []string
|
||||||
|
var err error
|
||||||
|
if len(strings.TrimSpace(value)) > 0 {
|
||||||
|
values, err = csv.NewReader(strings.NewReader(value)).Read()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"error reading a CSV string from environment variable %s with multiple values: %v",
|
||||||
|
spec.env,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = setSliceOrMap(p.val(spec.dest), values, !spec.separate); err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"error processing environment variable %s with multiple values: %v",
|
||||||
|
spec.env,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := scalar.ParseValue(p.val(spec.dest), value); err != nil {
|
||||||
|
return fmt.Errorf("error processing environment variable %s: %v", spec.env, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wasPresent[spec] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// process goes through arguments one-by-one, parses them, and assigns the result to
|
||||||
|
// the underlying struct field
|
||||||
|
func (p *Parser) process(args []string) error {
|
||||||
|
// track the options we have seen
|
||||||
|
wasPresent := make(map[*spec]bool)
|
||||||
|
|
||||||
|
// union of specs for the chain of subcommands encountered so far
|
||||||
|
curCmd := p.cmd
|
||||||
|
p.subcommand = nil
|
||||||
|
|
||||||
|
// make a copy of the specs because we will add to this list each time we expand a subcommand
|
||||||
|
specs := make([]*spec, len(curCmd.specs))
|
||||||
|
copy(specs, curCmd.specs)
|
||||||
|
|
||||||
|
// deal with environment vars
|
||||||
|
if !p.config.IgnoreEnv {
|
||||||
|
err := p.captureEnvVars(specs, wasPresent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine if the current command has a version option spec
|
||||||
|
var hasVersionOption bool
|
||||||
|
for _, spec := range curCmd.specs {
|
||||||
|
if spec.long == "version" {
|
||||||
|
hasVersionOption = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// process each string from the command line
|
||||||
|
var allpositional bool
|
||||||
|
var positionals []string
|
||||||
|
|
||||||
|
// must use explicit for loop, not range, because we manipulate i inside the loop
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
if arg == "--" && !allpositional {
|
||||||
|
allpositional = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isFlag(arg) || allpositional {
|
||||||
|
// each subcommand can have either subcommands or positionals, but not both
|
||||||
|
if len(curCmd.subcommands) == 0 {
|
||||||
|
positionals = append(positionals, arg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have a subcommand then make sure it is valid for the current context
|
||||||
|
subcmd := findSubcommand(curCmd.subcommands, arg)
|
||||||
|
if subcmd == nil {
|
||||||
|
return fmt.Errorf("invalid subcommand: %s", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// instantiate the field to point to a new struct
|
||||||
|
v := p.val(subcmd.dest)
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem())) // we already checked that all subcommands are struct pointers
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the new options to the set of allowed options
|
||||||
|
if p.config.StrictSubcommands {
|
||||||
|
specs = make([]*spec, len(subcmd.specs))
|
||||||
|
copy(specs, subcmd.specs)
|
||||||
|
} else {
|
||||||
|
specs = append(specs, subcmd.specs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// capture environment vars for these new options
|
||||||
|
if !p.config.IgnoreEnv {
|
||||||
|
err := p.captureEnvVars(subcmd.specs, wasPresent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curCmd = subcmd
|
||||||
|
p.subcommand = append(p.subcommand, arg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for special --help and --version flags
|
||||||
|
switch arg {
|
||||||
|
case "-h", "--help":
|
||||||
|
return ErrHelp
|
||||||
|
case "--version":
|
||||||
|
if !hasVersionOption && p.version != "" {
|
||||||
|
return ErrVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for an equals sign, as in "--foo=bar"
|
||||||
|
var value string
|
||||||
|
opt := strings.TrimLeft(arg, "-")
|
||||||
|
if pos := strings.Index(opt, "="); pos != -1 {
|
||||||
|
value = opt[pos+1:]
|
||||||
|
opt = opt[:pos]
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup the spec for this option (note that the "specs" slice changes as
|
||||||
|
// we expand subcommands so it is better not to use a map)
|
||||||
|
spec := findOption(specs, opt)
|
||||||
|
if spec == nil || opt == "" {
|
||||||
|
return fmt.Errorf("unknown argument %s", arg)
|
||||||
|
}
|
||||||
|
wasPresent[spec] = true
|
||||||
|
|
||||||
|
// deal with the case of multiple values
|
||||||
|
if spec.cardinality == multiple {
|
||||||
|
var values []string
|
||||||
|
if value == "" {
|
||||||
|
for i+1 < len(args) && isValue(args[i+1], spec.field.Type, specs) && args[i+1] != "--" {
|
||||||
|
values = append(values, args[i+1])
|
||||||
|
i++
|
||||||
|
if spec.separate {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
values = append(values, value)
|
||||||
|
}
|
||||||
|
err := setSliceOrMap(p.val(spec.dest), values, !spec.separate)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error processing %s: %v", arg, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it's a flag and it has no value then set the value to true
|
||||||
|
// use boolean because this takes account of TextUnmarshaler
|
||||||
|
if spec.cardinality == zero && value == "" {
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have something like "--foo" then the value is the next argument
|
||||||
|
if value == "" {
|
||||||
|
if i+1 == len(args) {
|
||||||
|
return fmt.Errorf("missing value for %s", arg)
|
||||||
|
}
|
||||||
|
if !isValue(args[i+1], spec.field.Type, specs) {
|
||||||
|
return fmt.Errorf("missing value for %s", arg)
|
||||||
|
}
|
||||||
|
value = args[i+1]
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
err := scalar.ParseValue(p.val(spec.dest), value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error processing %s: %v", arg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// process positionals
|
||||||
|
for _, spec := range specs {
|
||||||
|
if !spec.positional {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(positionals) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
wasPresent[spec] = true
|
||||||
|
if spec.cardinality == multiple {
|
||||||
|
err := setSliceOrMap(p.val(spec.dest), positionals, true)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
|
||||||
|
}
|
||||||
|
positionals = nil
|
||||||
|
} else {
|
||||||
|
err := scalar.ParseValue(p.val(spec.dest), positionals[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error processing %s: %v", spec.placeholder, err)
|
||||||
|
}
|
||||||
|
positionals = positionals[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(positionals) > 0 {
|
||||||
|
return fmt.Errorf("too many positional arguments at '%s'", positionals[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill in defaults and check that all the required args were provided
|
||||||
|
for _, spec := range specs {
|
||||||
|
if wasPresent[spec] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.required {
|
||||||
|
if spec.short == "" && spec.long == "" {
|
||||||
|
msg := fmt.Sprintf("environment variable %s is required", spec.env)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("%s is required", spec.placeholder)
|
||||||
|
if spec.env != "" {
|
||||||
|
msg += " (or environment variable " + spec.env + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.defaultValue.IsValid() && !p.config.IgnoreDefault {
|
||||||
|
// One issue here is that if the user now modifies the value then
|
||||||
|
// the default value stored in the spec will be corrupted. There
|
||||||
|
// is no general way to "deep-copy" values in Go, and we still
|
||||||
|
// support the old-style method for specifying defaults as
|
||||||
|
// Go values assigned directly to the struct field, so we are stuck.
|
||||||
|
p.val(spec.dest).Set(spec.defaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFlag returns true if a token is a flag such as "-v" or "--user" but not "-" or "--"
|
||||||
|
func isFlag(s string) bool {
|
||||||
|
return strings.HasPrefix(s, "-") && strings.TrimLeft(s, "-") != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValue returns true if a token should be consumed as a value for a flag of type t. This
|
||||||
|
// is almost always the inverse of isFlag. The one exception is for negative numbers, in which
|
||||||
|
// case we check the list of active options and return true if its not present there.
|
||||||
|
func isValue(s string, t reflect.Type, specs []*spec) bool {
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Ptr, reflect.Slice:
|
||||||
|
return isValue(s, t.Elem(), specs)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
v := reflect.New(t)
|
||||||
|
err := scalar.ParseValue(v, s)
|
||||||
|
// if value can be parsed and is not an explicit option declared elsewhere, then use it as a value
|
||||||
|
if err == nil && (!strings.HasPrefix(s, "-") || findOption(specs, strings.TrimPrefix(s, "-")) == nil) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// default case that is used in all cases other than negative numbers: inverse of isFlag
|
||||||
|
return !isFlag(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// val returns a reflect.Value corresponding to the current value for the
|
||||||
|
// given path
|
||||||
|
func (p *Parser) val(dest path) reflect.Value {
|
||||||
|
v := p.roots[dest.root]
|
||||||
|
for _, field := range dest.fields {
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
return reflect.Value{}
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
v = v.FieldByIndex(field.Index)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// findOption finds an option from its name, or returns null if no spec is found
|
||||||
|
func findOption(specs []*spec, name string) *spec {
|
||||||
|
for _, spec := range specs {
|
||||||
|
if spec.positional {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if spec.long == name || spec.short == name {
|
||||||
|
return spec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSubcommand finds a subcommand using its name, or returns null if no subcommand is found
|
||||||
|
func findSubcommand(cmds []*command, name string) *command {
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if cmd.name == name {
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
for _, alias := range cmd.aliases {
|
||||||
|
if alias == name {
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
112
vendor/github.com/alexflint/go-arg/reflect.go
generated
vendored
Normal file
112
vendor/github.com/alexflint/go-arg/reflect.go
generated
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
package arg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
scalar "github.com/alexflint/go-scalar"
|
||||||
|
)
|
||||||
|
|
||||||
|
var textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
||||||
|
|
||||||
|
// cardinality tracks how many tokens are expected for a given spec
|
||||||
|
// - zero is a boolean, which does to expect any value
|
||||||
|
// - one is an ordinary option that will be parsed from a single token
|
||||||
|
// - multiple is a slice or map that can accept zero or more tokens
|
||||||
|
type cardinality int
|
||||||
|
|
||||||
|
const (
|
||||||
|
zero cardinality = iota
|
||||||
|
one
|
||||||
|
multiple
|
||||||
|
unsupported
|
||||||
|
)
|
||||||
|
|
||||||
|
func (k cardinality) String() string {
|
||||||
|
switch k {
|
||||||
|
case zero:
|
||||||
|
return "zero"
|
||||||
|
case one:
|
||||||
|
return "one"
|
||||||
|
case multiple:
|
||||||
|
return "multiple"
|
||||||
|
case unsupported:
|
||||||
|
return "unsupported"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("unknown(%d)", int(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cardinalityOf returns true if the type can be parsed from a string
|
||||||
|
func cardinalityOf(t reflect.Type) (cardinality, error) {
|
||||||
|
if scalar.CanParse(t) {
|
||||||
|
if isBoolean(t) {
|
||||||
|
return zero, nil
|
||||||
|
}
|
||||||
|
return one, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// look inside pointer types
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// look inside slice and map types
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
if !scalar.CanParse(t.Elem()) {
|
||||||
|
return unsupported, fmt.Errorf("cannot parse into %v because %v not supported", t, t.Elem())
|
||||||
|
}
|
||||||
|
return multiple, nil
|
||||||
|
case reflect.Map:
|
||||||
|
if !scalar.CanParse(t.Key()) {
|
||||||
|
return unsupported, fmt.Errorf("cannot parse into %v because key type %v not supported", t, t.Elem())
|
||||||
|
}
|
||||||
|
if !scalar.CanParse(t.Elem()) {
|
||||||
|
return unsupported, fmt.Errorf("cannot parse into %v because value type %v not supported", t, t.Elem())
|
||||||
|
}
|
||||||
|
return multiple, nil
|
||||||
|
default:
|
||||||
|
return unsupported, fmt.Errorf("cannot parse into %v", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBoolean returns true if the type is a boolean or a pointer to a boolean
|
||||||
|
func isBoolean(t reflect.Type) bool {
|
||||||
|
switch {
|
||||||
|
case isTextUnmarshaler(t):
|
||||||
|
return false
|
||||||
|
case t.Kind() == reflect.Bool:
|
||||||
|
return true
|
||||||
|
case t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Bool:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTextUnmarshaler returns true if the type or its pointer implements encoding.TextUnmarshaler
|
||||||
|
func isTextUnmarshaler(t reflect.Type) bool {
|
||||||
|
return t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isExported returns true if the struct field name is exported
|
||||||
|
func isExported(field string) bool {
|
||||||
|
r, _ := utf8.DecodeRuneInString(field) // returns RuneError for empty string or invalid UTF8
|
||||||
|
return unicode.IsLetter(r) && unicode.IsUpper(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isZero returns true if v contains the zero value for its type
|
||||||
|
func isZero(v reflect.Value) bool {
|
||||||
|
t := v.Type()
|
||||||
|
if t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice || t.Kind() == reflect.Map || t.Kind() == reflect.Chan || t.Kind() == reflect.Interface {
|
||||||
|
return v.IsNil()
|
||||||
|
}
|
||||||
|
if !t.Comparable() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return v.Interface() == reflect.Zero(t).Interface()
|
||||||
|
}
|
||||||
123
vendor/github.com/alexflint/go-arg/sequence.go
generated
vendored
Normal file
123
vendor/github.com/alexflint/go-arg/sequence.go
generated
vendored
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
package arg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
scalar "github.com/alexflint/go-scalar"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setSliceOrMap parses a sequence of strings into a slice or map. If clear is
|
||||||
|
// true then any values already in the slice or map are first removed.
|
||||||
|
func setSliceOrMap(dest reflect.Value, values []string, clear bool) error {
|
||||||
|
if !dest.CanSet() {
|
||||||
|
return fmt.Errorf("field is not writable")
|
||||||
|
}
|
||||||
|
|
||||||
|
t := dest.Type()
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
dest = dest.Elem()
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
return setSlice(dest, values, clear)
|
||||||
|
case reflect.Map:
|
||||||
|
return setMap(dest, values, clear)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("setSliceOrMap cannot insert values into a %v", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setSlice parses a sequence of strings and inserts them into a slice. If clear
|
||||||
|
// is true then any values already in the slice are removed.
|
||||||
|
func setSlice(dest reflect.Value, values []string, clear bool) error {
|
||||||
|
var ptr bool
|
||||||
|
elem := dest.Type().Elem()
|
||||||
|
if elem.Kind() == reflect.Ptr && !elem.Implements(textUnmarshalerType) {
|
||||||
|
ptr = true
|
||||||
|
elem = elem.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear the slice in case default values exist
|
||||||
|
if clear && !dest.IsNil() {
|
||||||
|
dest.SetLen(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the values one-by-one
|
||||||
|
for _, s := range values {
|
||||||
|
v := reflect.New(elem)
|
||||||
|
if err := scalar.ParseValue(v.Elem(), s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
dest.Set(reflect.Append(dest, v))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setMap parses a sequence of name=value strings and inserts them into a map.
|
||||||
|
// If clear is true then any values already in the map are removed.
|
||||||
|
func setMap(dest reflect.Value, values []string, clear bool) error {
|
||||||
|
// determine the key and value type
|
||||||
|
var keyIsPtr bool
|
||||||
|
keyType := dest.Type().Key()
|
||||||
|
if keyType.Kind() == reflect.Ptr && !keyType.Implements(textUnmarshalerType) {
|
||||||
|
keyIsPtr = true
|
||||||
|
keyType = keyType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
var valIsPtr bool
|
||||||
|
valType := dest.Type().Elem()
|
||||||
|
if valType.Kind() == reflect.Ptr && !valType.Implements(textUnmarshalerType) {
|
||||||
|
valIsPtr = true
|
||||||
|
valType = valType.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear the slice in case default values exist
|
||||||
|
if clear && !dest.IsNil() {
|
||||||
|
for _, k := range dest.MapKeys() {
|
||||||
|
dest.SetMapIndex(k, reflect.Value{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// allocate the map if it is not allocated
|
||||||
|
if dest.IsNil() {
|
||||||
|
dest.Set(reflect.MakeMap(dest.Type()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the values one-by-one
|
||||||
|
for _, s := range values {
|
||||||
|
// split at the first equals sign
|
||||||
|
pos := strings.Index(s, "=")
|
||||||
|
if pos == -1 {
|
||||||
|
return fmt.Errorf("cannot parse %q into a map, expected format key=value", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the key
|
||||||
|
k := reflect.New(keyType)
|
||||||
|
if err := scalar.ParseValue(k.Elem(), s[:pos]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !keyIsPtr {
|
||||||
|
k = k.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the value
|
||||||
|
v := reflect.New(valType)
|
||||||
|
if err := scalar.ParseValue(v.Elem(), s[pos+1:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !valIsPtr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// add it to the map
|
||||||
|
dest.SetMapIndex(k, v)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
43
vendor/github.com/alexflint/go-arg/subcommand.go
generated
vendored
Normal file
43
vendor/github.com/alexflint/go-arg/subcommand.go
generated
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package arg
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Subcommand returns the user struct for the subcommand selected by
|
||||||
|
// the command line arguments most recently processed by the parser.
|
||||||
|
// The return value is always a pointer to a struct. If no subcommand
|
||||||
|
// was specified then it returns the top-level arguments struct. If
|
||||||
|
// no command line arguments have been processed by this parser then it
|
||||||
|
// returns nil.
|
||||||
|
func (p *Parser) Subcommand() interface{} {
|
||||||
|
if len(p.subcommand) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cmd, err := p.lookupCommand(p.subcommand...)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.val(cmd.dest).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubcommandNames returns the sequence of subcommands specified by the
|
||||||
|
// user. If no subcommands were given then it returns an empty slice.
|
||||||
|
func (p *Parser) SubcommandNames() []string {
|
||||||
|
return p.subcommand
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupCommand finds a subcommand based on a sequence of subcommand names. The
|
||||||
|
// first string should be a top-level subcommand, the next should be a child
|
||||||
|
// subcommand of that subcommand, and so on. If no strings are given then the
|
||||||
|
// root command is returned. If no such subcommand exists then an error is
|
||||||
|
// returned.
|
||||||
|
func (p *Parser) lookupCommand(path ...string) (*command, error) {
|
||||||
|
cmd := p.cmd
|
||||||
|
for _, name := range path {
|
||||||
|
found := findSubcommand(cmd.subcommands, name)
|
||||||
|
if found == nil {
|
||||||
|
return nil, fmt.Errorf("%q is not a subcommand of %s", name, cmd.name)
|
||||||
|
}
|
||||||
|
cmd = found
|
||||||
|
}
|
||||||
|
return cmd, nil
|
||||||
|
}
|
||||||
343
vendor/github.com/alexflint/go-arg/usage.go
generated
vendored
Normal file
343
vendor/github.com/alexflint/go-arg/usage.go
generated
vendored
Normal file
|
|
@ -0,0 +1,343 @@
|
||||||
|
package arg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// the width of the left column
|
||||||
|
const colWidth = 25
|
||||||
|
|
||||||
|
// Fail prints usage information to p.Config.Out and exits with status code 2.
|
||||||
|
func (p *Parser) Fail(msg string) {
|
||||||
|
p.FailSubcommand(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FailSubcommand prints usage information for a specified subcommand to p.Config.Out,
|
||||||
|
// then exits with status code 2. To write usage information for a top-level
|
||||||
|
// subcommand, provide just the name of that subcommand. To write usage
|
||||||
|
// information for a subcommand that is nested under another subcommand, provide
|
||||||
|
// a sequence of subcommand names starting with the top-level subcommand and so
|
||||||
|
// on down the tree.
|
||||||
|
func (p *Parser) FailSubcommand(msg string, subcommand ...string) error {
|
||||||
|
err := p.WriteUsageForSubcommand(p.config.Out, subcommand...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(p.config.Out, "error:", msg)
|
||||||
|
p.config.Exit(2)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteUsage writes usage information to the given writer
|
||||||
|
func (p *Parser) WriteUsage(w io.Writer) {
|
||||||
|
p.WriteUsageForSubcommand(w, p.subcommand...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteUsageForSubcommand writes the usage information for a specified
|
||||||
|
// subcommand. To write usage information for a top-level subcommand, provide
|
||||||
|
// just the name of that subcommand. To write usage information for a subcommand
|
||||||
|
// that is nested under another subcommand, provide a sequence of subcommand
|
||||||
|
// names starting with the top-level subcommand and so on down the tree.
|
||||||
|
func (p *Parser) WriteUsageForSubcommand(w io.Writer, subcommand ...string) error {
|
||||||
|
cmd, err := p.lookupCommand(subcommand...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var positionals, longOptions, shortOptions []*spec
|
||||||
|
for _, spec := range cmd.specs {
|
||||||
|
switch {
|
||||||
|
case spec.positional:
|
||||||
|
positionals = append(positionals, spec)
|
||||||
|
case spec.long != "":
|
||||||
|
longOptions = append(longOptions, spec)
|
||||||
|
case spec.short != "":
|
||||||
|
shortOptions = append(shortOptions, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// print the beginning of the usage string
|
||||||
|
fmt.Fprintf(w, "Usage: %s", p.cmd.name)
|
||||||
|
for _, s := range subcommand {
|
||||||
|
fmt.Fprint(w, " "+s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the option component of the usage message
|
||||||
|
for _, spec := range shortOptions {
|
||||||
|
// prefix with a space
|
||||||
|
fmt.Fprint(w, " ")
|
||||||
|
if !spec.required {
|
||||||
|
fmt.Fprint(w, "[")
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, synopsis(spec, "-"+spec.short))
|
||||||
|
if !spec.required {
|
||||||
|
fmt.Fprint(w, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, spec := range longOptions {
|
||||||
|
// prefix with a space
|
||||||
|
fmt.Fprint(w, " ")
|
||||||
|
if !spec.required {
|
||||||
|
fmt.Fprint(w, "[")
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, synopsis(spec, "--"+spec.long))
|
||||||
|
if !spec.required {
|
||||||
|
fmt.Fprint(w, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we parse positionals, we check that:
|
||||||
|
// 1. required positionals come before non-required positionals
|
||||||
|
// 2. there is at most one multiple-value positional
|
||||||
|
// 3. if there is a multiple-value positional then it comes after all other positionals
|
||||||
|
// Here we merely print the usage string, so we do not explicitly re-enforce those rules
|
||||||
|
|
||||||
|
// write the positionals in following form:
|
||||||
|
// REQUIRED1 REQUIRED2
|
||||||
|
// REQUIRED1 REQUIRED2 [OPTIONAL1 [OPTIONAL2]]
|
||||||
|
// REQUIRED1 REQUIRED2 REPEATED [REPEATED ...]
|
||||||
|
// REQUIRED1 REQUIRED2 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]
|
||||||
|
// REQUIRED1 REQUIRED2 [OPTIONAL1 [REPEATEDOPTIONAL [REPEATEDOPTIONAL ...]]]
|
||||||
|
var closeBrackets int
|
||||||
|
for _, spec := range positionals {
|
||||||
|
fmt.Fprint(w, " ")
|
||||||
|
if !spec.required {
|
||||||
|
fmt.Fprint(w, "[")
|
||||||
|
closeBrackets += 1
|
||||||
|
}
|
||||||
|
if spec.cardinality == multiple {
|
||||||
|
fmt.Fprintf(w, "%s [%s ...]", spec.placeholder, spec.placeholder)
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(w, spec.placeholder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, strings.Repeat("]", closeBrackets))
|
||||||
|
|
||||||
|
// if the program supports subcommands, give a hint to the user about their existence
|
||||||
|
if len(cmd.subcommands) > 0 {
|
||||||
|
fmt.Fprint(w, " <command> [<args>]")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, "\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// print prints a line like this:
|
||||||
|
//
|
||||||
|
// --option FOO A description of the option [default: 123]
|
||||||
|
//
|
||||||
|
// If the text on the left is longer than a certain threshold, the description is moved to the next line:
|
||||||
|
//
|
||||||
|
// --verylongoptionoption VERY_LONG_VARIABLE
|
||||||
|
// A description of the option [default: 123]
|
||||||
|
//
|
||||||
|
// If multiple "extras" are provided then they are put inside a single set of square brackets:
|
||||||
|
//
|
||||||
|
// --option FOO A description of the option [default: 123, env: FOO]
|
||||||
|
func print(w io.Writer, item, description string, bracketed ...string) {
|
||||||
|
lhs := " " + item
|
||||||
|
fmt.Fprint(w, lhs)
|
||||||
|
if description != "" {
|
||||||
|
if len(lhs)+2 < colWidth {
|
||||||
|
fmt.Fprint(w, strings.Repeat(" ", colWidth-len(lhs)))
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(w, "\n"+strings.Repeat(" ", colWidth))
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
var brack string
|
||||||
|
for _, s := range bracketed {
|
||||||
|
if s != "" {
|
||||||
|
if brack != "" {
|
||||||
|
brack += ", "
|
||||||
|
}
|
||||||
|
brack += s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if brack != "" {
|
||||||
|
fmt.Fprintf(w, " [%s]", brack)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func withDefault(s string) string {
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "default: " + s
|
||||||
|
}
|
||||||
|
|
||||||
|
func withEnv(env string) string {
|
||||||
|
if env == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "env: " + env
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHelp writes the usage string followed by the full help string for each option
|
||||||
|
func (p *Parser) WriteHelp(w io.Writer) {
|
||||||
|
p.WriteHelpForSubcommand(w, p.subcommand...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteHelpForSubcommand writes the usage string followed by the full help
|
||||||
|
// string for a specified subcommand. To write help for a top-level subcommand,
|
||||||
|
// provide just the name of that subcommand. To write help for a subcommand that
|
||||||
|
// is nested under another subcommand, provide a sequence of subcommand names
|
||||||
|
// starting with the top-level subcommand and so on down the tree.
|
||||||
|
func (p *Parser) WriteHelpForSubcommand(w io.Writer, subcommand ...string) error {
|
||||||
|
cmd, err := p.lookupCommand(subcommand...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var positionals, longOptions, shortOptions, envOnlyOptions []*spec
|
||||||
|
var hasVersionOption bool
|
||||||
|
for _, spec := range cmd.specs {
|
||||||
|
switch {
|
||||||
|
case spec.positional:
|
||||||
|
positionals = append(positionals, spec)
|
||||||
|
case spec.long != "":
|
||||||
|
longOptions = append(longOptions, spec)
|
||||||
|
if spec.long == "version" {
|
||||||
|
hasVersionOption = true
|
||||||
|
}
|
||||||
|
case spec.short != "":
|
||||||
|
shortOptions = append(shortOptions, spec)
|
||||||
|
case spec.short == "" && spec.long == "":
|
||||||
|
envOnlyOptions = append(envOnlyOptions, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// obtain a flattened list of options from all ancestors
|
||||||
|
// also determine if any ancestor has a version option spec
|
||||||
|
var globals []*spec
|
||||||
|
ancestor := cmd.parent
|
||||||
|
for ancestor != nil {
|
||||||
|
for _, spec := range ancestor.specs {
|
||||||
|
if spec.long == "version" {
|
||||||
|
hasVersionOption = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globals = append(globals, ancestor.specs...)
|
||||||
|
ancestor = ancestor.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.description != "" {
|
||||||
|
fmt.Fprintln(w, p.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasVersionOption && p.version != "" {
|
||||||
|
fmt.Fprintln(w, p.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.WriteUsageForSubcommand(w, subcommand...)
|
||||||
|
|
||||||
|
// write the list of positionals
|
||||||
|
if len(positionals) > 0 {
|
||||||
|
fmt.Fprint(w, "\nPositional arguments:\n")
|
||||||
|
for _, spec := range positionals {
|
||||||
|
print(w, spec.placeholder, spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the list of options with the short-only ones first to match the usage string
|
||||||
|
if len(shortOptions)+len(longOptions) > 0 || cmd.parent == nil {
|
||||||
|
fmt.Fprint(w, "\nOptions:\n")
|
||||||
|
for _, spec := range shortOptions {
|
||||||
|
p.printOption(w, spec)
|
||||||
|
}
|
||||||
|
for _, spec := range longOptions {
|
||||||
|
p.printOption(w, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the list of global options
|
||||||
|
if len(globals) > 0 {
|
||||||
|
fmt.Fprint(w, "\nGlobal options:\n")
|
||||||
|
for _, spec := range globals {
|
||||||
|
p.printOption(w, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the list of built in options
|
||||||
|
p.printOption(w, &spec{
|
||||||
|
cardinality: zero,
|
||||||
|
long: "help",
|
||||||
|
short: "h",
|
||||||
|
help: "display this help and exit",
|
||||||
|
})
|
||||||
|
if !hasVersionOption && p.version != "" {
|
||||||
|
p.printOption(w, &spec{
|
||||||
|
cardinality: zero,
|
||||||
|
long: "version",
|
||||||
|
help: "display version and exit",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the list of environment only variables
|
||||||
|
if len(envOnlyOptions) > 0 {
|
||||||
|
fmt.Fprint(w, "\nEnvironment variables:\n")
|
||||||
|
for _, spec := range envOnlyOptions {
|
||||||
|
p.printEnvOnlyVar(w, spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the list of subcommands
|
||||||
|
if len(cmd.subcommands) > 0 {
|
||||||
|
fmt.Fprint(w, "\nCommands:\n")
|
||||||
|
for _, subcmd := range cmd.subcommands {
|
||||||
|
names := append([]string{subcmd.name}, subcmd.aliases...)
|
||||||
|
print(w, strings.Join(names, ", "), subcmd.help)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.epilogue != "" {
|
||||||
|
fmt.Fprintln(w, "\n"+p.epilogue)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) printOption(w io.Writer, spec *spec) {
|
||||||
|
ways := make([]string, 0, 2)
|
||||||
|
if spec.long != "" {
|
||||||
|
ways = append(ways, synopsis(spec, "--"+spec.long))
|
||||||
|
}
|
||||||
|
if spec.short != "" {
|
||||||
|
ways = append(ways, synopsis(spec, "-"+spec.short))
|
||||||
|
}
|
||||||
|
if len(ways) > 0 {
|
||||||
|
print(w, strings.Join(ways, ", "), spec.help, withDefault(spec.defaultString), withEnv(spec.env))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) printEnvOnlyVar(w io.Writer, spec *spec) {
|
||||||
|
ways := make([]string, 0, 2)
|
||||||
|
if spec.required {
|
||||||
|
ways = append(ways, "Required.")
|
||||||
|
} else {
|
||||||
|
ways = append(ways, "Optional.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if spec.help != "" {
|
||||||
|
ways = append(ways, spec.help)
|
||||||
|
}
|
||||||
|
|
||||||
|
print(w, spec.env, strings.Join(ways, " "), withDefault(spec.defaultString))
|
||||||
|
}
|
||||||
|
|
||||||
|
func synopsis(spec *spec, form string) string {
|
||||||
|
// if the user omits the placeholder tag then we pick one automatically,
|
||||||
|
// but if the user explicitly specifies an empty placeholder then we
|
||||||
|
// leave out the placeholder in the help message
|
||||||
|
if spec.cardinality == zero || spec.placeholder == "" {
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
return form + " " + spec.placeholder
|
||||||
|
}
|
||||||
24
vendor/github.com/alexflint/go-scalar/.gitignore
generated
vendored
Normal file
24
vendor/github.com/alexflint/go-scalar/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
24
vendor/github.com/alexflint/go-scalar/LICENSE
generated
vendored
Normal file
24
vendor/github.com/alexflint/go-scalar/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
Copyright (c) 2015, Alex Flint
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
28
vendor/github.com/alexflint/go-scalar/README.md
generated
vendored
Normal file
28
vendor/github.com/alexflint/go-scalar/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
[](https://godoc.org/github.com/alexflint/go-scalar)
|
||||||
|
[](https://travis-ci.org/alexflint/go-scalar)
|
||||||
|
[](https://coveralls.io/github/alexflint/go-scalar?branch=master)
|
||||||
|
[](https://goreportcard.com/badge/github.com/alexflint/go-scalar)
|
||||||
|
|
||||||
|
## Scalar parsing library
|
||||||
|
|
||||||
|
Scalar is a library for parsing strings into arbitrary scalars (integers,
|
||||||
|
floats, strings, booleans, etc). It is helpful for tasks such as parsing
|
||||||
|
strings passed as environment variables or command line arguments.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go get github.com/alexflint/go-scalar
|
||||||
|
```
|
||||||
|
|
||||||
|
The main API works as follows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var value int
|
||||||
|
err := scalar.Parse(&value, "123")
|
||||||
|
```
|
||||||
|
|
||||||
|
There is also a variant that takes a `reflect.Value`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var value int
|
||||||
|
err := scalar.ParseValue(reflect.ValueOf(&value), "123")
|
||||||
|
```
|
||||||
162
vendor/github.com/alexflint/go-scalar/scalar.go
generated
vendored
Normal file
162
vendor/github.com/alexflint/go-scalar/scalar.go
generated
vendored
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
// Package scalar parses strings into values of scalar type.
|
||||||
|
|
||||||
|
package scalar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/mail"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The reflected form of some special types
|
||||||
|
var (
|
||||||
|
textUnmarshalerType = reflect.TypeOf([]encoding.TextUnmarshaler{}).Elem()
|
||||||
|
durationType = reflect.TypeOf(time.Duration(0))
|
||||||
|
mailAddressType = reflect.TypeOf(mail.Address{})
|
||||||
|
macType = reflect.TypeOf(net.HardwareAddr{})
|
||||||
|
urlType = reflect.TypeOf(url.URL{})
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNotSettable = errors.New("value is not settable")
|
||||||
|
errPtrNotSettable = errors.New("value is a nil pointer and is not settable")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse assigns a value to v by parsing s.
|
||||||
|
func Parse(dest interface{}, s string) error {
|
||||||
|
return ParseValue(reflect.ValueOf(dest), s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseValue assigns a value to v by parsing s.
|
||||||
|
func ParseValue(v reflect.Value, s string) error {
|
||||||
|
// If we have a nil pointer then allocate a new object
|
||||||
|
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||||
|
if !v.CanSet() {
|
||||||
|
return errPtrNotSettable
|
||||||
|
}
|
||||||
|
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it implements encoding.TextUnmarshaler then use that
|
||||||
|
if scalar, ok := v.Interface().(encoding.TextUnmarshaler); ok {
|
||||||
|
return scalar.UnmarshalText([]byte(s))
|
||||||
|
}
|
||||||
|
// If it's a value instead of a pointer, check that we can unmarshal it
|
||||||
|
// via TextUnmarshaler as well
|
||||||
|
if v.CanAddr() {
|
||||||
|
if scalar, ok := v.Addr().Interface().(encoding.TextUnmarshaler); ok {
|
||||||
|
return scalar.UnmarshalText([]byte(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a pointer then dereference it
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !v.CanSet() {
|
||||||
|
return errNotSettable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch on concrete type
|
||||||
|
switch scalar := v.Interface(); scalar.(type) {
|
||||||
|
case time.Duration:
|
||||||
|
duration, err := time.ParseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Set(reflect.ValueOf(duration))
|
||||||
|
return nil
|
||||||
|
case mail.Address:
|
||||||
|
addr, err := mail.ParseAddress(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Set(reflect.ValueOf(*addr))
|
||||||
|
return nil
|
||||||
|
case net.HardwareAddr:
|
||||||
|
ip, err := net.ParseMAC(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Set(reflect.ValueOf(ip))
|
||||||
|
return nil
|
||||||
|
case url.URL:
|
||||||
|
url, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.Set(reflect.ValueOf(*url))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch on kind so that we can handle derived types
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
v.SetString(s)
|
||||||
|
case reflect.Bool:
|
||||||
|
x, err := strconv.ParseBool(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.SetBool(x)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
x, err := strconv.ParseInt(s, 0, v.Type().Bits())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.SetInt(x)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
x, err := strconv.ParseUint(s, 0, v.Type().Bits())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.SetUint(x)
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
x, err := strconv.ParseFloat(s, v.Type().Bits())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.SetFloat(x)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot parse into %v", v.Type())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanParse returns true if the type can be parsed from a string.
|
||||||
|
func CanParse(t reflect.Type) bool {
|
||||||
|
// If it implements encoding.TextUnmarshaler then use that
|
||||||
|
if t.Implements(textUnmarshalerType) || reflect.PtrTo(t).Implements(textUnmarshalerType) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a pointer then dereference it
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for other special types
|
||||||
|
switch t {
|
||||||
|
case durationType, mailAddressType, macType, urlType:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to checking the kind
|
||||||
|
switch t.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
return true
|
||||||
|
case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||||
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
|
||||||
|
reflect.Float32, reflect.Float64:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
21
vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE
generated
vendored
Normal file
21
vendor/github.com/aymanbagabas/go-osc52/v2/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2022 Ayman Bagabas
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
83
vendor/github.com/aymanbagabas/go-osc52/v2/README.md
generated
vendored
Normal file
83
vendor/github.com/aymanbagabas/go-osc52/v2/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
|
||||||
|
# go-osc52
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/aymanbagabas/go-osc52/releases"><img src="https://img.shields.io/github/release/aymanbagabas/go-osc52.svg" alt="Latest Release"></a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/aymanbagabas/go-osc52/v2?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
A Go library to work with the [ANSI OSC52](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) terminal sequence.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
You can use this small library to construct an ANSI OSC52 sequence suitable for
|
||||||
|
your terminal.
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/aymanbagabas/go-osc52/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
s := "Hello World!"
|
||||||
|
|
||||||
|
// Copy `s` to system clipboard
|
||||||
|
osc52.New(s).WriteTo(os.Stderr)
|
||||||
|
|
||||||
|
// Copy `s` to primary clipboard (X11)
|
||||||
|
osc52.New(s).Primary().WriteTo(os.Stderr)
|
||||||
|
|
||||||
|
// Query the clipboard
|
||||||
|
osc52.Query().WriteTo(os.Stderr)
|
||||||
|
|
||||||
|
// Clear system clipboard
|
||||||
|
osc52.Clear().WriteTo(os.Stderr)
|
||||||
|
|
||||||
|
// Use the fmt.Stringer interface to copy `s` to system clipboard
|
||||||
|
fmt.Fprint(os.Stderr, osc52.New(s))
|
||||||
|
|
||||||
|
// Or to primary clipboard
|
||||||
|
fmt.Fprint(os.Stderr, osc52.New(s).Primary())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSH Example
|
||||||
|
|
||||||
|
You can use this over SSH using [gliderlabs/ssh](https://github.com/gliderlabs/ssh) for instance:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var sshSession ssh.Session
|
||||||
|
seq := osc52.New("Hello awesome!")
|
||||||
|
// Check if term is screen or tmux
|
||||||
|
pty, _, _ := s.Pty()
|
||||||
|
if pty.Term == "screen" {
|
||||||
|
seq = seq.Screen()
|
||||||
|
} else if isTmux {
|
||||||
|
seq = seq.Tmux()
|
||||||
|
}
|
||||||
|
seq.WriteTo(sshSession.Stderr())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tmux
|
||||||
|
|
||||||
|
Make sure you have `set-clipboard on` in your config, otherwise, tmux won't
|
||||||
|
allow your application to access the clipboard [^1].
|
||||||
|
|
||||||
|
Using the tmux option, `osc52.TmuxMode` or `osc52.New(...).Tmux()`, wraps the
|
||||||
|
OSC52 sequence in a special tmux DCS sequence and pass it to the outer
|
||||||
|
terminal. This requires `allow-passthrough on` in your config.
|
||||||
|
`allow-passthrough` is no longer enabled by default
|
||||||
|
[since tmux 3.3a](https://github.com/tmux/tmux/issues/3218#issuecomment-1153089282) [^2].
|
||||||
|
|
||||||
|
[^1]: See [tmux clipboard](https://github.com/tmux/tmux/wiki/Clipboard)
|
||||||
|
[^2]: [What is allow-passthrough](https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it)
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
* [vim-oscyank](https://github.com/ojroques/vim-oscyank) this is heavily inspired by vim-oscyank.
|
||||||
305
vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go
generated
vendored
Normal file
305
vendor/github.com/aymanbagabas/go-osc52/v2/osc52.go
generated
vendored
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
// OSC52 is a terminal escape sequence that allows copying text to the clipboard.
|
||||||
|
//
|
||||||
|
// The sequence consists of the following:
|
||||||
|
//
|
||||||
|
// OSC 52 ; Pc ; Pd BEL
|
||||||
|
//
|
||||||
|
// Pc is the clipboard choice:
|
||||||
|
//
|
||||||
|
// c: clipboard
|
||||||
|
// p: primary
|
||||||
|
// q: secondary (not supported)
|
||||||
|
// s: select (not supported)
|
||||||
|
// 0-7: cut-buffers (not supported)
|
||||||
|
//
|
||||||
|
// Pd is the data to copy to the clipboard. This string should be encoded in
|
||||||
|
// base64 (RFC-4648).
|
||||||
|
//
|
||||||
|
// If Pd is "?", the terminal replies to the host with the current contents of
|
||||||
|
// the clipboard.
|
||||||
|
//
|
||||||
|
// If Pd is neither a base64 string nor "?", the terminal clears the clipboard.
|
||||||
|
//
|
||||||
|
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
// where Ps = 52 => Manipulate Selection Data.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// // copy "hello world" to the system clipboard
|
||||||
|
// fmt.Fprint(os.Stderr, osc52.New("hello world"))
|
||||||
|
//
|
||||||
|
// // copy "hello world" to the primary Clipboard
|
||||||
|
// fmt.Fprint(os.Stderr, osc52.New("hello world").Primary())
|
||||||
|
//
|
||||||
|
// // limit the size of the string to copy 10 bytes
|
||||||
|
// fmt.Fprint(os.Stderr, osc52.New("0123456789").Limit(10))
|
||||||
|
//
|
||||||
|
// // escape the OSC52 sequence for screen using DCS sequences
|
||||||
|
// fmt.Fprint(os.Stderr, osc52.New("hello world").Screen())
|
||||||
|
//
|
||||||
|
// // escape the OSC52 sequence for Tmux
|
||||||
|
// fmt.Fprint(os.Stderr, osc52.New("hello world").Tmux())
|
||||||
|
//
|
||||||
|
// // query the system Clipboard
|
||||||
|
// fmt.Fprint(os.Stderr, osc52.Query())
|
||||||
|
//
|
||||||
|
// // query the primary clipboard
|
||||||
|
// fmt.Fprint(os.Stderr, osc52.Query().Primary())
|
||||||
|
//
|
||||||
|
// // clear the system Clipboard
|
||||||
|
// fmt.Fprint(os.Stderr, osc52.Clear())
|
||||||
|
//
|
||||||
|
// // clear the primary Clipboard
|
||||||
|
// fmt.Fprint(os.Stderr, osc52.Clear().Primary())
|
||||||
|
package osc52
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clipboard is the clipboard buffer to use.
|
||||||
|
type Clipboard rune
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SystemClipboard is the system clipboard buffer.
|
||||||
|
SystemClipboard Clipboard = 'c'
|
||||||
|
// PrimaryClipboard is the primary clipboard buffer (X11).
|
||||||
|
PrimaryClipboard = 'p'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mode is the mode to use for the OSC52 sequence.
|
||||||
|
type Mode uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultMode is the default OSC52 sequence mode.
|
||||||
|
DefaultMode Mode = iota
|
||||||
|
// ScreenMode escapes the OSC52 sequence for screen using DCS sequences.
|
||||||
|
ScreenMode
|
||||||
|
// TmuxMode escapes the OSC52 sequence for tmux. Not needed if tmux
|
||||||
|
// clipboard is set to `set-clipboard on`
|
||||||
|
TmuxMode
|
||||||
|
)
|
||||||
|
|
||||||
|
// Operation is the OSC52 operation.
|
||||||
|
type Operation uint
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SetOperation is the copy operation.
|
||||||
|
SetOperation Operation = iota
|
||||||
|
// QueryOperation is the query operation.
|
||||||
|
QueryOperation
|
||||||
|
// ClearOperation is the clear operation.
|
||||||
|
ClearOperation
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sequence is the OSC52 sequence.
|
||||||
|
type Sequence struct {
|
||||||
|
str string
|
||||||
|
limit int
|
||||||
|
op Operation
|
||||||
|
mode Mode
|
||||||
|
clipboard Clipboard
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fmt.Stringer = Sequence{}
|
||||||
|
|
||||||
|
var _ io.WriterTo = Sequence{}
|
||||||
|
|
||||||
|
// String returns the OSC52 sequence.
|
||||||
|
func (s Sequence) String() string {
|
||||||
|
var seq strings.Builder
|
||||||
|
// mode escape sequences start
|
||||||
|
seq.WriteString(s.seqStart())
|
||||||
|
// actual OSC52 sequence start
|
||||||
|
seq.WriteString(fmt.Sprintf("\x1b]52;%c;", s.clipboard))
|
||||||
|
switch s.op {
|
||||||
|
case SetOperation:
|
||||||
|
str := s.str
|
||||||
|
if s.limit > 0 && len(str) > s.limit {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
b64 := base64.StdEncoding.EncodeToString([]byte(str))
|
||||||
|
switch s.mode {
|
||||||
|
case ScreenMode:
|
||||||
|
// Screen doesn't support OSC52 but will pass the contents of a DCS
|
||||||
|
// sequence to the outer terminal unchanged.
|
||||||
|
//
|
||||||
|
// Here, we split the encoded string into 76 bytes chunks and then
|
||||||
|
// join the chunks with <end-dsc><start-dsc> sequences. Finally,
|
||||||
|
// wrap the whole thing in
|
||||||
|
// <start-dsc><start-osc52><joined-chunks><end-osc52><end-dsc>.
|
||||||
|
// s := strings.SplitN(b64, "", 76)
|
||||||
|
s := make([]string, 0, len(b64)/76+1)
|
||||||
|
for i := 0; i < len(b64); i += 76 {
|
||||||
|
end := i + 76
|
||||||
|
if end > len(b64) {
|
||||||
|
end = len(b64)
|
||||||
|
}
|
||||||
|
s = append(s, b64[i:end])
|
||||||
|
}
|
||||||
|
seq.WriteString(strings.Join(s, "\x1b\\\x1bP"))
|
||||||
|
default:
|
||||||
|
seq.WriteString(b64)
|
||||||
|
}
|
||||||
|
case QueryOperation:
|
||||||
|
// OSC52 queries the clipboard using "?"
|
||||||
|
seq.WriteString("?")
|
||||||
|
case ClearOperation:
|
||||||
|
// OSC52 clears the clipboard if the data is neither a base64 string nor "?"
|
||||||
|
// we're using "!" as a default
|
||||||
|
seq.WriteString("!")
|
||||||
|
}
|
||||||
|
// actual OSC52 sequence end
|
||||||
|
seq.WriteString("\x07")
|
||||||
|
// mode escape end
|
||||||
|
seq.WriteString(s.seqEnd())
|
||||||
|
return seq.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo writes the OSC52 sequence to the writer.
|
||||||
|
func (s Sequence) WriteTo(out io.Writer) (int64, error) {
|
||||||
|
n, err := out.Write([]byte(s.String()))
|
||||||
|
return int64(n), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode sets the mode for the OSC52 sequence.
|
||||||
|
func (s Sequence) Mode(m Mode) Sequence {
|
||||||
|
s.mode = m
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tmux sets the mode to TmuxMode.
|
||||||
|
// Used to escape the OSC52 sequence for `tmux`.
|
||||||
|
//
|
||||||
|
// Note: this is not needed if tmux clipboard is set to `set-clipboard on`. If
|
||||||
|
// TmuxMode is used, tmux must have `allow-passthrough on` set.
|
||||||
|
//
|
||||||
|
// This is a syntactic sugar for s.Mode(TmuxMode).
|
||||||
|
func (s Sequence) Tmux() Sequence {
|
||||||
|
return s.Mode(TmuxMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen sets the mode to ScreenMode.
|
||||||
|
// Used to escape the OSC52 sequence for `screen`.
|
||||||
|
//
|
||||||
|
// This is a syntactic sugar for s.Mode(ScreenMode).
|
||||||
|
func (s Sequence) Screen() Sequence {
|
||||||
|
return s.Mode(ScreenMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clipboard sets the clipboard buffer for the OSC52 sequence.
|
||||||
|
func (s Sequence) Clipboard(c Clipboard) Sequence {
|
||||||
|
s.clipboard = c
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary sets the clipboard buffer to PrimaryClipboard.
|
||||||
|
// This is the X11 primary clipboard.
|
||||||
|
//
|
||||||
|
// This is a syntactic sugar for s.Clipboard(PrimaryClipboard).
|
||||||
|
func (s Sequence) Primary() Sequence {
|
||||||
|
return s.Clipboard(PrimaryClipboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit sets the limit for the OSC52 sequence.
|
||||||
|
// The default limit is 0 (no limit).
|
||||||
|
//
|
||||||
|
// Strings longer than the limit get ignored. Settting the limit to 0 or a
|
||||||
|
// negative value disables the limit. Each terminal defines its own escapse
|
||||||
|
// sequence limit.
|
||||||
|
func (s Sequence) Limit(l int) Sequence {
|
||||||
|
if l < 0 {
|
||||||
|
s.limit = 0
|
||||||
|
} else {
|
||||||
|
s.limit = l
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operation sets the operation for the OSC52 sequence.
|
||||||
|
// The default operation is SetOperation.
|
||||||
|
func (s Sequence) Operation(o Operation) Sequence {
|
||||||
|
s.op = o
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear sets the operation to ClearOperation.
|
||||||
|
// This clears the clipboard.
|
||||||
|
//
|
||||||
|
// This is a syntactic sugar for s.Operation(ClearOperation).
|
||||||
|
func (s Sequence) Clear() Sequence {
|
||||||
|
return s.Operation(ClearOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query sets the operation to QueryOperation.
|
||||||
|
// This queries the clipboard contents.
|
||||||
|
//
|
||||||
|
// This is a syntactic sugar for s.Operation(QueryOperation).
|
||||||
|
func (s Sequence) Query() Sequence {
|
||||||
|
return s.Operation(QueryOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetString sets the string for the OSC52 sequence. Strings are joined with a
|
||||||
|
// space character.
|
||||||
|
func (s Sequence) SetString(strs ...string) Sequence {
|
||||||
|
s.str = strings.Join(strs, " ")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new OSC52 sequence with the given string(s). Strings are
|
||||||
|
// joined with a space character.
|
||||||
|
func New(strs ...string) Sequence {
|
||||||
|
s := Sequence{
|
||||||
|
str: strings.Join(strs, " "),
|
||||||
|
limit: 0,
|
||||||
|
mode: DefaultMode,
|
||||||
|
clipboard: SystemClipboard,
|
||||||
|
op: SetOperation,
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query creates a new OSC52 sequence with the QueryOperation.
|
||||||
|
// This returns a new OSC52 sequence to query the clipboard contents.
|
||||||
|
//
|
||||||
|
// This is a syntactic sugar for New().Query().
|
||||||
|
func Query() Sequence {
|
||||||
|
return New().Query()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear creates a new OSC52 sequence with the ClearOperation.
|
||||||
|
// This returns a new OSC52 sequence to clear the clipboard.
|
||||||
|
//
|
||||||
|
// This is a syntactic sugar for New().Clear().
|
||||||
|
func Clear() Sequence {
|
||||||
|
return New().Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Sequence) seqStart() string {
|
||||||
|
switch s.mode {
|
||||||
|
case TmuxMode:
|
||||||
|
// Write the start of a tmux escape sequence.
|
||||||
|
return "\x1bPtmux;\x1b"
|
||||||
|
case ScreenMode:
|
||||||
|
// Write the start of a DCS sequence.
|
||||||
|
return "\x1bP"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Sequence) seqEnd() string {
|
||||||
|
switch s.mode {
|
||||||
|
case TmuxMode:
|
||||||
|
// Terminate the tmux escape sequence.
|
||||||
|
return "\x1b\\"
|
||||||
|
case ScreenMode:
|
||||||
|
// Write the end of a DCS sequence.
|
||||||
|
return "\x1b\x5c"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
1
vendor/github.com/charmbracelet/bubbletea/.gitattributes
generated
vendored
Normal file
1
vendor/github.com/charmbracelet/bubbletea/.gitattributes
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
*.golden -text
|
||||||
23
vendor/github.com/charmbracelet/bubbletea/.gitignore
generated
vendored
Normal file
23
vendor/github.com/charmbracelet/bubbletea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
.DS_Store
|
||||||
|
.envrc
|
||||||
|
|
||||||
|
examples/fullscreen/fullscreen
|
||||||
|
examples/help/help
|
||||||
|
examples/http/http
|
||||||
|
examples/list-default/list-default
|
||||||
|
examples/list-fancy/list-fancy
|
||||||
|
examples/list-simple/list-simple
|
||||||
|
examples/mouse/mouse
|
||||||
|
examples/pager/pager
|
||||||
|
examples/progress-download/color_vortex.blend
|
||||||
|
examples/progress-download/progress-download
|
||||||
|
examples/simple/simple
|
||||||
|
examples/spinner/spinner
|
||||||
|
examples/textinput/textinput
|
||||||
|
examples/textinputs/textinputs
|
||||||
|
examples/views/views
|
||||||
|
tutorials/basics/basics
|
||||||
|
tutorials/commands/commands
|
||||||
|
.idea
|
||||||
|
coverage.txt
|
||||||
|
dist/
|
||||||
44
vendor/github.com/charmbracelet/bubbletea/.golangci.yml
generated
vendored
Normal file
44
vendor/github.com/charmbracelet/bubbletea/.golangci.yml
generated
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
tests: false
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- exhaustive
|
||||||
|
- goconst
|
||||||
|
- godot
|
||||||
|
- gomoddirectives
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilerr
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- prealloc
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- tparallel
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- whitespace
|
||||||
|
- wrapcheck
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
- text: '(slog|log)\.\w+'
|
||||||
|
linters:
|
||||||
|
- noctx
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- common-false-positives
|
||||||
|
issues:
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
5
vendor/github.com/charmbracelet/bubbletea/.goreleaser.yml
generated
vendored
Normal file
5
vendor/github.com/charmbracelet/bubbletea/.goreleaser.yml
generated
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||||
|
version: 2
|
||||||
|
includes:
|
||||||
|
- from_url:
|
||||||
|
url: charmbracelet/meta/main/goreleaser-lib.yaml
|
||||||
21
vendor/github.com/charmbracelet/bubbletea/LICENSE
generated
vendored
Normal file
21
vendor/github.com/charmbracelet/bubbletea/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-2025 Charmbracelet, Inc
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
400
vendor/github.com/charmbracelet/bubbletea/README.md
generated
vendored
Normal file
400
vendor/github.com/charmbracelet/bubbletea/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
# Bubble Tea
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://stuff.charm.sh/bubbletea/bubble-tea-v2-light.png" width="308">
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://stuff.charm.sh/bubbletea/bubble-tea-v2-dark.png" width="312">
|
||||||
|
<img src="https://stuff.charm.sh/bubbletea/bubble-tea-v2-light.png" width="308" />
|
||||||
|
</picture>
|
||||||
|
<br>
|
||||||
|
<a href="https://github.com/charmbracelet/bubbletea/releases"><img src="https://img.shields.io/github/release/charmbracelet/bubbletea.svg" alt="Latest Release"></a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc"><img src="https://godoc.org/github.com/charmbracelet/bubbletea?status.svg" alt="GoDoc"></a>
|
||||||
|
<a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
The fun, functional and stateful way to build terminal apps. A Go framework
|
||||||
|
based on [The Elm Architecture][elm]. Bubble Tea is well-suited for simple and
|
||||||
|
complex terminal applications, either inline, full-window, or a mix of both.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<img src="https://stuff.charm.sh/bubbletea/bubbletea-example.gif" width="100%" alt="Bubble Tea Example">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Bubble Tea is in use in production and includes a number of features and
|
||||||
|
performance optimizations we’ve added along the way. Among those is
|
||||||
|
a framerate-based renderer, mouse support, focus reporting and more.
|
||||||
|
|
||||||
|
To get started, see the tutorial below, the [examples][examples], the
|
||||||
|
[docs][docs], the [video tutorials][youtube] and some common [resources](#libraries-we-use-with-bubble-tea).
|
||||||
|
|
||||||
|
[youtube]: https://charm.sh/yt
|
||||||
|
|
||||||
|
## By the way
|
||||||
|
|
||||||
|
Be sure to check out [Bubbles][bubbles], a library of common UI components for Bubble Tea.
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/charmbracelet/bubbles"><img src="https://stuff.charm.sh/bubbles/bubbles-badge.png" width="174" alt="Bubbles Badge"></a>
|
||||||
|
<a href="https://github.com/charmbracelet/bubbles"><img src="https://stuff.charm.sh/bubbles-examples/textinput.gif" width="400" alt="Text Input Example from Bubbles"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tutorial
|
||||||
|
|
||||||
|
Bubble Tea is based on the functional design paradigms of [The Elm
|
||||||
|
Architecture][elm], which happens to work nicely with Go. It's a delightful way
|
||||||
|
to build applications.
|
||||||
|
|
||||||
|
This tutorial assumes you have a working knowledge of Go.
|
||||||
|
|
||||||
|
By the way, the non-annotated source code for this program is available
|
||||||
|
[on GitHub][tut-source].
|
||||||
|
|
||||||
|
[elm]: https://guide.elm-lang.org/architecture/
|
||||||
|
[tut-source]: https://github.com/charmbracelet/bubbletea/tree/main/tutorials/basics
|
||||||
|
|
||||||
|
### Enough! Let's get to it.
|
||||||
|
|
||||||
|
For this tutorial, we're making a shopping list.
|
||||||
|
|
||||||
|
To start we'll define our package and import some libraries. Our only external
|
||||||
|
import will be the Bubble Tea library, which we'll call `tea` for short.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
// These imports will be used later on 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.
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Bubble Tea programs are comprised of a **model** that describes the application
|
||||||
|
state and three simple methods on that model:
|
||||||
|
|
||||||
|
- **Init**, a function that returns an initial command for the application to run.
|
||||||
|
- **Update**, a function that handles incoming events and updates the model accordingly.
|
||||||
|
- **View**, a function that renders the UI based on the data in the model.
|
||||||
|
|
||||||
|
### The Model
|
||||||
|
|
||||||
|
So let's start by defining our model which will store our application's state.
|
||||||
|
It can be any type, but a `struct` usually makes the most sense.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type model struct {
|
||||||
|
choices []string // items on the to-do list
|
||||||
|
cursor int // which to-do list item our cursor is pointing at
|
||||||
|
selected map[int]struct{} // which to-do items are selected
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Initialization
|
||||||
|
|
||||||
|
Next, we’ll define our application’s initial state. In this case, we’re defining
|
||||||
|
a function to return our initial model, however, we could just as easily define
|
||||||
|
the initial model as a variable elsewhere, too.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func initialModel() model {
|
||||||
|
return model{
|
||||||
|
// Our to-do list is a grocery list
|
||||||
|
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
|
||||||
|
|
||||||
|
// A map which indicates which choices are selected. We're using
|
||||||
|
// the map like a mathematical set. The keys refer to the indexes
|
||||||
|
// of the `choices` slice, above.
|
||||||
|
selected: make(map[int]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, we define the `Init` method. `Init` can return a `Cmd` that could perform
|
||||||
|
some initial I/O. For now, we don't need to do any I/O, so for the command,
|
||||||
|
we'll just return `nil`, which translates to "no command."
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
// Just return `nil`, which means "no I/O right now, please."
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Update Method
|
||||||
|
|
||||||
|
Next up is the update method. The update function is called when ”things
|
||||||
|
happen.” Its job is to look at what has happened and return an updated model in
|
||||||
|
response. It can also return a `Cmd` to make more things happen, but for now
|
||||||
|
don't worry about that part.
|
||||||
|
|
||||||
|
In our case, when a user presses the down arrow, `Update`’s job is to notice
|
||||||
|
that the down arrow was pressed and move the cursor accordingly (or not).
|
||||||
|
|
||||||
|
The “something happened” comes in the form of a `Msg`, which can be any type.
|
||||||
|
Messages are the result of some I/O that took place, such as a keypress, timer
|
||||||
|
tick, or a response from a server.
|
||||||
|
|
||||||
|
We usually figure out which type of `Msg` we received with a type switch, but
|
||||||
|
you could also use a type assertion.
|
||||||
|
|
||||||
|
For now, we'll just deal with `tea.KeyMsg` messages, which are automatically
|
||||||
|
sent to the update function when keys are pressed.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
|
||||||
|
// Is it a key press?
|
||||||
|
case tea.KeyMsg:
|
||||||
|
|
||||||
|
// Cool, what was the actual key pressed?
|
||||||
|
switch msg.String() {
|
||||||
|
|
||||||
|
// These keys should exit the program.
|
||||||
|
case "ctrl+c", "q":
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
// The "up" and "k" keys move the cursor up
|
||||||
|
case "up", "k":
|
||||||
|
if m.cursor > 0 {
|
||||||
|
m.cursor--
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "down" and "j" keys move the cursor down
|
||||||
|
case "down", "j":
|
||||||
|
if m.cursor < len(m.choices)-1 {
|
||||||
|
m.cursor++
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "enter" key and the spacebar (a literal space) toggle
|
||||||
|
// the selected state for the item that the cursor is pointing at.
|
||||||
|
case "enter", " ":
|
||||||
|
_, ok := m.selected[m.cursor]
|
||||||
|
if ok {
|
||||||
|
delete(m.selected, m.cursor)
|
||||||
|
} else {
|
||||||
|
m.selected[m.cursor] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the updated model to the Bubble Tea runtime for processing.
|
||||||
|
// Note that we're not returning a command.
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You may have noticed that <kbd>ctrl+c</kbd> and <kbd>q</kbd> above return
|
||||||
|
a `tea.Quit` command with the model. That’s a special command which instructs
|
||||||
|
the Bubble Tea runtime to quit, exiting the program.
|
||||||
|
|
||||||
|
### The View Method
|
||||||
|
|
||||||
|
At last, it’s time to render our UI. Of all the methods, the view is the
|
||||||
|
simplest. We look at the model in its current state and use it to return
|
||||||
|
a `string`. That string is our UI!
|
||||||
|
|
||||||
|
Because the view describes the entire UI of your application, you don’t have to
|
||||||
|
worry about redrawing logic and stuff like that. Bubble Tea takes care of it
|
||||||
|
for you.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (m model) View() string {
|
||||||
|
// The header
|
||||||
|
s := "What should we buy at the market?\n\n"
|
||||||
|
|
||||||
|
// Iterate over our choices
|
||||||
|
for i, choice := range m.choices {
|
||||||
|
|
||||||
|
// Is the cursor pointing at this choice?
|
||||||
|
cursor := " " // no cursor
|
||||||
|
if m.cursor == i {
|
||||||
|
cursor = ">" // cursor!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this choice selected?
|
||||||
|
checked := " " // not selected
|
||||||
|
if _, ok := m.selected[i]; ok {
|
||||||
|
checked = "x" // selected!
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the row
|
||||||
|
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The footer
|
||||||
|
s += "\nPress q to quit.\n"
|
||||||
|
|
||||||
|
// Send the UI for rendering
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### All Together Now
|
||||||
|
|
||||||
|
The last step is to simply run our program. We pass our initial model to
|
||||||
|
`tea.NewProgram` and let it rip:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
p := tea.NewProgram(initialModel())
|
||||||
|
if _, err := p.Run(); err != nil {
|
||||||
|
fmt.Printf("Alas, there's been an error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## What’s Next?
|
||||||
|
|
||||||
|
This tutorial covers the basics of building an interactive terminal UI, but
|
||||||
|
in the real world you'll also need to perform I/O. To learn about that have a
|
||||||
|
look at the [Command Tutorial][cmd]. It's pretty simple.
|
||||||
|
|
||||||
|
There are also several [Bubble Tea examples][examples] available and, of course,
|
||||||
|
there are [Go Docs][docs].
|
||||||
|
|
||||||
|
[cmd]: https://github.com/charmbracelet/bubbletea/tree/main/tutorials/commands/
|
||||||
|
[examples]: https://github.com/charmbracelet/bubbletea/tree/main/examples
|
||||||
|
[docs]: https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
### Debugging with Delve
|
||||||
|
|
||||||
|
Since Bubble Tea apps assume control of stdin and stdout, you’ll need to run
|
||||||
|
delve in headless mode and then connect to it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the debugger
|
||||||
|
$ dlv debug --headless --api-version=2 --listen=127.0.0.1:43000 .
|
||||||
|
API server listening at: 127.0.0.1:43000
|
||||||
|
|
||||||
|
# Connect to it from another terminal
|
||||||
|
$ dlv connect 127.0.0.1:43000
|
||||||
|
```
|
||||||
|
|
||||||
|
If you do not explicitly supply the `--listen` flag, the port used will vary
|
||||||
|
per run, so passing this in makes the debugger easier to use from a script
|
||||||
|
or your IDE of choice.
|
||||||
|
|
||||||
|
Additionally, we pass in `--api-version=2` because delve defaults to version 1
|
||||||
|
for backwards compatibility reasons. However, delve recommends using version 2
|
||||||
|
for all new development and some clients may no longer work with version 1.
|
||||||
|
For more information, see the [Delve documentation](https://github.com/go-delve/delve/tree/master/Documentation/api).
|
||||||
|
|
||||||
|
### Logging Stuff
|
||||||
|
|
||||||
|
You can’t really log to stdout with Bubble Tea because your TUI is busy
|
||||||
|
occupying that! You can, however, log to a file by including something like
|
||||||
|
the following prior to starting your Bubble Tea program:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if len(os.Getenv("DEBUG")) > 0 {
|
||||||
|
f, err := tea.LogToFile("debug.log", "debug")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("fatal:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To see what’s being logged in real time, run `tail -f debug.log` while you run
|
||||||
|
your program in another window.
|
||||||
|
|
||||||
|
## Libraries we use with Bubble Tea
|
||||||
|
|
||||||
|
- [Bubbles][bubbles]: Common Bubble Tea components such as text inputs, viewports, spinners and so on
|
||||||
|
- [Lip Gloss][lipgloss]: Style, format and layout tools for terminal applications
|
||||||
|
- [Harmonica][harmonica]: A spring animation library for smooth, natural motion
|
||||||
|
- [BubbleZone][bubblezone]: Easy mouse event tracking for Bubble Tea components
|
||||||
|
- [ntcharts][ntcharts]: A terminal charting library built for Bubble Tea and [Lip Gloss][lipgloss]
|
||||||
|
|
||||||
|
[bubbles]: https://github.com/charmbracelet/bubbles
|
||||||
|
[lipgloss]: https://github.com/charmbracelet/lipgloss
|
||||||
|
[harmonica]: https://github.com/charmbracelet/harmonica
|
||||||
|
[bubblezone]: https://github.com/lrstanley/bubblezone
|
||||||
|
[ntcharts]: https://github.com/NimbleMarkets/ntcharts
|
||||||
|
|
||||||
|
## Bubble Tea in the Wild
|
||||||
|
|
||||||
|
There are over [10,000 applications](https://github.com/charmbracelet/bubbletea/network/dependents) built with Bubble Tea! Here are a handful of ’em.
|
||||||
|
|
||||||
|
### Staff favourites
|
||||||
|
|
||||||
|
- [chezmoi](https://github.com/twpayne/chezmoi): securely manage your dotfiles across multiple machines
|
||||||
|
- [circumflex](https://github.com/bensadeh/circumflex): read Hacker News in the terminal
|
||||||
|
- [gh-dash](https://www.github.com/dlvhdr/gh-dash): a GitHub CLI extension for PRs and issues
|
||||||
|
- [Tetrigo](https://github.com/Broderick-Westrope/tetrigo): Tetris in the terminal
|
||||||
|
- [Signls](https://github.com/emprcl/signls): a generative midi sequencer designed for composition and live performance
|
||||||
|
- [Superfile](https://github.com/yorukot/superfile): a super file manager
|
||||||
|
|
||||||
|
### In Industry
|
||||||
|
|
||||||
|
- Microsoft Azure – [Aztify](https://github.com/Azure/aztfy): bring Microsoft Azure resources under Terraform
|
||||||
|
- Daytona – [Daytona](https://github.com/daytonaio/daytona): open source dev environment manager
|
||||||
|
- Cockroach Labs – [CockroachDB](https://github.com/cockroachdb/cockroach): a cloud-native, high-availability distributed SQL database
|
||||||
|
- Truffle Security Co. – [Trufflehog](https://github.com/trufflesecurity/trufflehog): find leaked credentials
|
||||||
|
- NVIDIA – [container-canary](https://github.com/NVIDIA/container-canary): a container validator
|
||||||
|
- AWS – [eks-node-viewer](https://github.com/awslabs/eks-node-viewer): a tool for visualizing dynamic node usage within an EKS cluster
|
||||||
|
- MinIO – [mc](https://github.com/minio/mc): the official [MinIO](https://min.io) client
|
||||||
|
- Ubuntu – [Authd](https://github.com/ubuntu/authd): an authentication daemon for cloud-based identity providers
|
||||||
|
|
||||||
|
### Charm stuff
|
||||||
|
|
||||||
|
- [Glow](https://github.com/charmbracelet/glow): a markdown reader, browser, and online markdown stash
|
||||||
|
- [Huh?](https://github.com/charmbracelet/huh): an interactive prompt and form toolkit
|
||||||
|
- [Mods](https://github.com/charmbracelet/mods): AI on the CLI, built for pipelines
|
||||||
|
- [Wishlist](https://github.com/charmbracelet/wishlist): an SSH directory (and bastion!)
|
||||||
|
|
||||||
|
### There’s so much more where that came from
|
||||||
|
|
||||||
|
For more applications built with Bubble Tea see [Charm & Friends][community].
|
||||||
|
Is there something cool you made with Bubble Tea you want to share? [PRs][community] are
|
||||||
|
welcome!
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [contributing][contribute].
|
||||||
|
|
||||||
|
[contribute]: https://github.com/charmbracelet/bubbletea/contribute
|
||||||
|
|
||||||
|
## Feedback
|
||||||
|
|
||||||
|
We’d love to hear your thoughts on this project. Feel free to drop us a note!
|
||||||
|
|
||||||
|
- [Twitter](https://twitter.com/charmcli)
|
||||||
|
- [The Fediverse](https://mastodon.social/@charmcli)
|
||||||
|
- [Discord](https://charm.sh/chat)
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
Bubble Tea is based on the paradigms of [The Elm Architecture][elm] by Evan
|
||||||
|
Czaplicki et alia and the excellent [go-tea][gotea] by TJ Holowaychuk. It’s
|
||||||
|
inspired by the many great [_Zeichenorientierte Benutzerschnittstellen_][zb]
|
||||||
|
of days past.
|
||||||
|
|
||||||
|
[elm]: https://guide.elm-lang.org/architecture/
|
||||||
|
[gotea]: https://github.com/tj/go-tea
|
||||||
|
[zb]: https://de.wikipedia.org/wiki/Zeichenorientierte_Benutzerschnittstelle
|
||||||
|
[community]: https://github.com/charm-and-friends/charm-in-the-wild
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](https://github.com/charmbracelet/bubbletea/raw/main/LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
|
||||||
14
vendor/github.com/charmbracelet/bubbletea/Taskfile.yaml
generated
vendored
Normal file
14
vendor/github.com/charmbracelet/bubbletea/Taskfile.yaml
generated
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# https://taskfile.dev
|
||||||
|
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
lint:
|
||||||
|
desc: Run lint
|
||||||
|
cmds:
|
||||||
|
- golangci-lint run
|
||||||
|
|
||||||
|
test:
|
||||||
|
desc: Run tests
|
||||||
|
cmds:
|
||||||
|
- go test ./... {{.CLI_ARGS}}
|
||||||
222
vendor/github.com/charmbracelet/bubbletea/commands.go
generated
vendored
Normal file
222
vendor/github.com/charmbracelet/bubbletea/commands.go
generated
vendored
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Batch performs a bunch of commands concurrently with no ordering guarantees
|
||||||
|
// about the results. Use a Batch to return several commands.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// func (m model) Init() Cmd {
|
||||||
|
// return tea.Batch(someCommand, someOtherCommand)
|
||||||
|
// }
|
||||||
|
func Batch(cmds ...Cmd) Cmd {
|
||||||
|
return compactCmds[BatchMsg](cmds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchMsg is a message used to perform a bunch of commands concurrently with
|
||||||
|
// no ordering guarantees. You can send a BatchMsg with Batch.
|
||||||
|
type BatchMsg []Cmd
|
||||||
|
|
||||||
|
// Sequence runs the given commands one at a time, in order. Contrast this with
|
||||||
|
// Batch, which runs commands concurrently.
|
||||||
|
func Sequence(cmds ...Cmd) Cmd {
|
||||||
|
return compactCmds[sequenceMsg](cmds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sequenceMsg is used internally to run the given commands in order.
|
||||||
|
type sequenceMsg []Cmd
|
||||||
|
|
||||||
|
// compactCmds ignores any nil commands in cmds, and returns the most direct
|
||||||
|
// command possible. That is, considering the non-nil commands, if there are
|
||||||
|
// none it returns nil, if there is exactly one it returns that command
|
||||||
|
// directly, else it returns the non-nil commands as type T.
|
||||||
|
func compactCmds[T ~[]Cmd](cmds []Cmd) Cmd {
|
||||||
|
var validCmds []Cmd //nolint:prealloc
|
||||||
|
for _, c := range cmds {
|
||||||
|
if c == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
validCmds = append(validCmds, c)
|
||||||
|
}
|
||||||
|
switch len(validCmds) {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
return validCmds[0]
|
||||||
|
default:
|
||||||
|
return func() Msg {
|
||||||
|
return T(validCmds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every is a command that ticks in sync with the system clock. So, if you
|
||||||
|
// wanted to tick with the system clock every second, minute or hour you
|
||||||
|
// could use this. It's also handy for having different things tick in sync.
|
||||||
|
//
|
||||||
|
// Because we're ticking with the system clock the tick will likely not run for
|
||||||
|
// the entire specified duration. For example, if we're ticking for one minute
|
||||||
|
// and the clock is at 12:34:20 then the next tick will happen at 12:35:00, 40
|
||||||
|
// seconds later.
|
||||||
|
//
|
||||||
|
// To produce the command, pass a duration and a function which returns
|
||||||
|
// a message containing the time at which the tick occurred.
|
||||||
|
//
|
||||||
|
// type TickMsg time.Time
|
||||||
|
//
|
||||||
|
// cmd := Every(time.Second, func(t time.Time) Msg {
|
||||||
|
// return TickMsg(t)
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// Beginners' note: Every sends a single message and won't automatically
|
||||||
|
// dispatch messages at an interval. To do that, you'll want to return another
|
||||||
|
// Every command after receiving your tick message. For example:
|
||||||
|
//
|
||||||
|
// type TickMsg time.Time
|
||||||
|
//
|
||||||
|
// // Send a message every second.
|
||||||
|
// func tickEvery() Cmd {
|
||||||
|
// return Every(time.Second, func(t time.Time) Msg {
|
||||||
|
// return TickMsg(t)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m model) Init() Cmd {
|
||||||
|
// // Start ticking.
|
||||||
|
// return tickEvery()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m model) Update(msg Msg) (Model, Cmd) {
|
||||||
|
// switch msg.(type) {
|
||||||
|
// case TickMsg:
|
||||||
|
// // Return your Every command again to loop.
|
||||||
|
// return m, tickEvery()
|
||||||
|
// }
|
||||||
|
// return m, nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Every is analogous to Tick in the Elm Architecture.
|
||||||
|
func Every(duration time.Duration, fn func(time.Time) Msg) Cmd {
|
||||||
|
n := time.Now()
|
||||||
|
d := n.Truncate(duration).Add(duration).Sub(n)
|
||||||
|
t := time.NewTimer(d)
|
||||||
|
return func() Msg {
|
||||||
|
ts := <-t.C
|
||||||
|
t.Stop()
|
||||||
|
for len(t.C) > 0 {
|
||||||
|
<-t.C
|
||||||
|
}
|
||||||
|
return fn(ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tick produces a command at an interval independent of the system clock at
|
||||||
|
// the given duration. That is, the timer begins precisely when invoked,
|
||||||
|
// and runs for its entire duration.
|
||||||
|
//
|
||||||
|
// To produce the command, pass a duration and a function which returns
|
||||||
|
// a message containing the time at which the tick occurred.
|
||||||
|
//
|
||||||
|
// type TickMsg time.Time
|
||||||
|
//
|
||||||
|
// cmd := Tick(time.Second, func(t time.Time) Msg {
|
||||||
|
// return TickMsg(t)
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// Beginners' note: Tick sends a single message and won't automatically
|
||||||
|
// dispatch messages at an interval. To do that, you'll want to return another
|
||||||
|
// Tick command after receiving your tick message. For example:
|
||||||
|
//
|
||||||
|
// type TickMsg time.Time
|
||||||
|
//
|
||||||
|
// func doTick() Cmd {
|
||||||
|
// return Tick(time.Second, func(t time.Time) Msg {
|
||||||
|
// return TickMsg(t)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m model) Init() Cmd {
|
||||||
|
// // Start ticking.
|
||||||
|
// return doTick()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (m model) Update(msg Msg) (Model, Cmd) {
|
||||||
|
// switch msg.(type) {
|
||||||
|
// case TickMsg:
|
||||||
|
// // Return your Tick command again to loop.
|
||||||
|
// return m, doTick()
|
||||||
|
// }
|
||||||
|
// return m, nil
|
||||||
|
// }
|
||||||
|
func Tick(d time.Duration, fn func(time.Time) Msg) Cmd {
|
||||||
|
t := time.NewTimer(d)
|
||||||
|
return func() Msg {
|
||||||
|
ts := <-t.C
|
||||||
|
t.Stop()
|
||||||
|
for len(t.C) > 0 {
|
||||||
|
<-t.C
|
||||||
|
}
|
||||||
|
return fn(ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sequentially produces a command that sequentially executes the given
|
||||||
|
// commands.
|
||||||
|
// The Msg returned is the first non-nil message returned by a Cmd.
|
||||||
|
//
|
||||||
|
// func saveStateCmd() Msg {
|
||||||
|
// if err := save(); err != nil {
|
||||||
|
// return errMsg{err}
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// cmd := Sequentially(saveStateCmd, Quit)
|
||||||
|
//
|
||||||
|
// Deprecated: use Sequence instead.
|
||||||
|
func Sequentially(cmds ...Cmd) Cmd {
|
||||||
|
return func() Msg {
|
||||||
|
for _, cmd := range cmds {
|
||||||
|
if cmd == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if msg := cmd(); msg != nil {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setWindowTitleMsg is an internal message used to set the window title.
|
||||||
|
type setWindowTitleMsg string
|
||||||
|
|
||||||
|
// SetWindowTitle produces a command that sets the terminal title.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
//
|
||||||
|
// func (m model) Init() Cmd {
|
||||||
|
// // Set title.
|
||||||
|
// return tea.SetWindowTitle("My App")
|
||||||
|
// }
|
||||||
|
func SetWindowTitle(title string) Cmd {
|
||||||
|
return func() Msg {
|
||||||
|
return setWindowTitleMsg(title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type windowSizeMsg struct{}
|
||||||
|
|
||||||
|
// WindowSize is a command that queries the terminal for its current size. It
|
||||||
|
// delivers the results to Update via a [WindowSizeMsg]. Keep in mind that
|
||||||
|
// WindowSizeMsgs will automatically be delivered to Update when the [Program]
|
||||||
|
// starts and when the window dimensions change so in many cases you will not
|
||||||
|
// need to explicitly invoke this command.
|
||||||
|
func WindowSize() Cmd {
|
||||||
|
return func() Msg {
|
||||||
|
return windowSizeMsg{}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
vendor/github.com/charmbracelet/bubbletea/exec.go
generated
vendored
Normal file
133
vendor/github.com/charmbracelet/bubbletea/exec.go
generated
vendored
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// execMsg is used internally to run an ExecCommand sent with Exec.
|
||||||
|
type execMsg struct {
|
||||||
|
cmd ExecCommand
|
||||||
|
fn ExecCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec is used to perform arbitrary I/O in a blocking fashion, effectively
|
||||||
|
// pausing the Program while execution is running and resuming it when
|
||||||
|
// execution has completed.
|
||||||
|
//
|
||||||
|
// Most of the time you'll want to use ExecProcess, which runs an exec.Cmd.
|
||||||
|
//
|
||||||
|
// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
|
||||||
|
func Exec(c ExecCommand, fn ExecCallback) Cmd {
|
||||||
|
return func() Msg {
|
||||||
|
return execMsg{cmd: c, fn: fn}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecProcess runs the given *exec.Cmd in a blocking fashion, effectively
|
||||||
|
// pausing the Program while the command is running. After the *exec.Cmd exists
|
||||||
|
// the Program resumes. It's useful for spawning other interactive applications
|
||||||
|
// such as editors and shells from within a Program.
|
||||||
|
//
|
||||||
|
// To produce the command, pass an *exec.Cmd and a function which returns
|
||||||
|
// a message containing the error which may have occurred when running the
|
||||||
|
// ExecCommand.
|
||||||
|
//
|
||||||
|
// type VimFinishedMsg struct { err error }
|
||||||
|
//
|
||||||
|
// c := exec.Command("vim", "file.txt")
|
||||||
|
//
|
||||||
|
// cmd := ExecProcess(c, func(err error) Msg {
|
||||||
|
// return VimFinishedMsg{err: err}
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// Or, if you don't care about errors, you could simply:
|
||||||
|
//
|
||||||
|
// cmd := ExecProcess(exec.Command("vim", "file.txt"), nil)
|
||||||
|
//
|
||||||
|
// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
|
||||||
|
func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd {
|
||||||
|
return Exec(wrapExecCommand(c), fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecCallback is used when executing an *exec.Command to return a message
|
||||||
|
// with an error, which may or may not be nil.
|
||||||
|
type ExecCallback func(error) Msg
|
||||||
|
|
||||||
|
// ExecCommand can be implemented to execute things in a blocking fashion in
|
||||||
|
// the current terminal.
|
||||||
|
type ExecCommand interface {
|
||||||
|
Run() error
|
||||||
|
SetStdin(io.Reader)
|
||||||
|
SetStdout(io.Writer)
|
||||||
|
SetStderr(io.Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapExecCommand wraps an exec.Cmd so that it satisfies the ExecCommand
|
||||||
|
// interface so it can be used with Exec.
|
||||||
|
func wrapExecCommand(c *exec.Cmd) ExecCommand {
|
||||||
|
return &osExecCommand{Cmd: c}
|
||||||
|
}
|
||||||
|
|
||||||
|
// osExecCommand is a layer over an exec.Cmd that satisfies the ExecCommand
|
||||||
|
// interface.
|
||||||
|
type osExecCommand struct{ *exec.Cmd }
|
||||||
|
|
||||||
|
// SetStdin sets stdin on underlying exec.Cmd to the given io.Reader.
|
||||||
|
func (c *osExecCommand) SetStdin(r io.Reader) {
|
||||||
|
// If unset, have the command use the same input as the terminal.
|
||||||
|
if c.Stdin == nil {
|
||||||
|
c.Stdin = r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStdout sets stdout on underlying exec.Cmd to the given io.Writer.
|
||||||
|
func (c *osExecCommand) SetStdout(w io.Writer) {
|
||||||
|
// If unset, have the command use the same output as the terminal.
|
||||||
|
if c.Stdout == nil {
|
||||||
|
c.Stdout = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStderr sets stderr on the underlying exec.Cmd to the given io.Writer.
|
||||||
|
func (c *osExecCommand) SetStderr(w io.Writer) {
|
||||||
|
// If unset, use stderr for the command's stderr
|
||||||
|
if c.Stderr == nil {
|
||||||
|
c.Stderr = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// exec runs an ExecCommand and delivers the results to the program as a Msg.
|
||||||
|
func (p *Program) exec(c ExecCommand, fn ExecCallback) {
|
||||||
|
if err := p.ReleaseTerminal(); err != nil {
|
||||||
|
// If we can't release input, abort.
|
||||||
|
if fn != nil {
|
||||||
|
go p.Send(fn(err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SetStdin(p.input)
|
||||||
|
c.SetStdout(p.output)
|
||||||
|
c.SetStderr(os.Stderr)
|
||||||
|
|
||||||
|
// Execute system command.
|
||||||
|
if err := c.Run(); err != nil {
|
||||||
|
p.renderer.resetLinesRendered()
|
||||||
|
_ = p.RestoreTerminal() // also try to restore the terminal.
|
||||||
|
if fn != nil {
|
||||||
|
go p.Send(fn(err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintain the existing output from the command
|
||||||
|
p.renderer.resetLinesRendered()
|
||||||
|
|
||||||
|
// Have the program re-capture input.
|
||||||
|
err := p.RestoreTerminal()
|
||||||
|
if fn != nil {
|
||||||
|
go p.Send(fn(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
9
vendor/github.com/charmbracelet/bubbletea/focus.go
generated
vendored
Normal file
9
vendor/github.com/charmbracelet/bubbletea/focus.go
generated
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
// FocusMsg represents a terminal focus message.
|
||||||
|
// This occurs when the terminal gains focus.
|
||||||
|
type FocusMsg struct{}
|
||||||
|
|
||||||
|
// BlurMsg represents a terminal blur message.
|
||||||
|
// This occurs when the terminal loses focus.
|
||||||
|
type BlurMsg struct{}
|
||||||
19
vendor/github.com/charmbracelet/bubbletea/inputreader_other.go
generated
vendored
Normal file
19
vendor/github.com/charmbracelet/bubbletea/inputreader_other.go
generated
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/muesli/cancelreader"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newInputReader(r io.Reader, _ bool) (cancelreader.CancelReader, error) {
|
||||||
|
cr, err := cancelreader.NewReader(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("bubbletea: error creating cancel reader: %w", err)
|
||||||
|
}
|
||||||
|
return cr, nil
|
||||||
|
}
|
||||||
129
vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
generated
vendored
Normal file
129
vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
generated
vendored
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/term"
|
||||||
|
"github.com/erikgeiser/coninput"
|
||||||
|
"github.com/muesli/cancelreader"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
type conInputReader struct {
|
||||||
|
cancelMixin
|
||||||
|
|
||||||
|
conin windows.Handle
|
||||||
|
|
||||||
|
originalMode uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ cancelreader.CancelReader = &conInputReader{}
|
||||||
|
|
||||||
|
func newInputReader(r io.Reader, enableMouse bool) (cancelreader.CancelReader, error) {
|
||||||
|
fallback := func(io.Reader) (cancelreader.CancelReader, error) {
|
||||||
|
return cancelreader.NewReader(r)
|
||||||
|
}
|
||||||
|
if f, ok := r.(term.File); !ok || f.Fd() != os.Stdin.Fd() {
|
||||||
|
return fallback(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
conin, err := coninput.NewStdinHandle()
|
||||||
|
if err != nil {
|
||||||
|
return fallback(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
modes := []uint32{
|
||||||
|
windows.ENABLE_WINDOW_INPUT,
|
||||||
|
windows.ENABLE_EXTENDED_FLAGS,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since we have options to enable mouse events, [WithMouseCellMotion],
|
||||||
|
// [WithMouseAllMotion], and [EnableMouseCellMotion],
|
||||||
|
// [EnableMouseAllMotion], and [DisableMouse], we need to check if the user
|
||||||
|
// has enabled mouse events and add the appropriate mode accordingly.
|
||||||
|
// Otherwise, mouse events will be enabled all the time.
|
||||||
|
if enableMouse {
|
||||||
|
modes = append(modes, windows.ENABLE_MOUSE_INPUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
originalMode, err := prepareConsole(conin, modes...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to prepare console input: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &conInputReader{
|
||||||
|
conin: conin,
|
||||||
|
originalMode: originalMode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel implements cancelreader.CancelReader.
|
||||||
|
func (r *conInputReader) Cancel() bool {
|
||||||
|
r.setCanceled()
|
||||||
|
|
||||||
|
// Warning: These cancel methods do not reliably work on console input
|
||||||
|
// and should not be counted on.
|
||||||
|
return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements cancelreader.CancelReader.
|
||||||
|
func (r *conInputReader) Close() error {
|
||||||
|
if r.originalMode != 0 {
|
||||||
|
err := windows.SetConsoleMode(r.conin, r.originalMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reset console mode: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements cancelreader.CancelReader.
|
||||||
|
func (r *conInputReader) Read(_ []byte) (n int, err error) {
|
||||||
|
if r.isCanceled() {
|
||||||
|
err = cancelreader.ErrCanceled
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) {
|
||||||
|
err = windows.GetConsoleMode(input, &originalMode)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("get console mode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newMode := coninput.AddInputModes(0, modes...)
|
||||||
|
|
||||||
|
err = windows.SetConsoleMode(input, newMode)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("set console mode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalMode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancelMixin represents a goroutine-safe cancellation status.
|
||||||
|
type cancelMixin struct {
|
||||||
|
unsafeCanceled bool
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cancelMixin) setCanceled() {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
c.unsafeCanceled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *cancelMixin) isCanceled() bool {
|
||||||
|
c.lock.Lock()
|
||||||
|
defer c.lock.Unlock()
|
||||||
|
|
||||||
|
return c.unsafeCanceled
|
||||||
|
}
|
||||||
715
vendor/github.com/charmbracelet/bubbletea/key.go
generated
vendored
Normal file
715
vendor/github.com/charmbracelet/bubbletea/key.go
generated
vendored
Normal file
|
|
@ -0,0 +1,715 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyMsg contains information about a keypress. KeyMsgs are always sent to
|
||||||
|
// the program's update function. There are a couple general patterns you could
|
||||||
|
// use to check for keypresses:
|
||||||
|
//
|
||||||
|
// // Switch on the string representation of the key (shorter)
|
||||||
|
// switch msg := msg.(type) {
|
||||||
|
// case KeyMsg:
|
||||||
|
// switch msg.String() {
|
||||||
|
// case "enter":
|
||||||
|
// fmt.Println("you pressed enter!")
|
||||||
|
// case "a":
|
||||||
|
// fmt.Println("you pressed a!")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Switch on the key type (more foolproof)
|
||||||
|
// switch msg := msg.(type) {
|
||||||
|
// case KeyMsg:
|
||||||
|
// switch msg.Type {
|
||||||
|
// case KeyEnter:
|
||||||
|
// fmt.Println("you pressed enter!")
|
||||||
|
// case KeyRunes:
|
||||||
|
// switch string(msg.Runes) {
|
||||||
|
// case "a":
|
||||||
|
// fmt.Println("you pressed a!")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Note that Key.Runes will always contain at least one character, so you can
|
||||||
|
// always safely call Key.Runes[0]. In most cases Key.Runes will only contain
|
||||||
|
// one character, though certain input method editors (most notably Chinese
|
||||||
|
// IMEs) can input multiple runes at once.
|
||||||
|
type KeyMsg Key
|
||||||
|
|
||||||
|
// String returns a string representation for a key message. It's safe (and
|
||||||
|
// encouraged) for use in key comparison.
|
||||||
|
func (k KeyMsg) String() (str string) {
|
||||||
|
return Key(k).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key contains information about a keypress.
|
||||||
|
type Key struct {
|
||||||
|
Type KeyType
|
||||||
|
Runes []rune
|
||||||
|
Alt bool
|
||||||
|
Paste bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a friendly string representation for a key. It's safe (and
|
||||||
|
// encouraged) for use in key comparison.
|
||||||
|
//
|
||||||
|
// k := Key{Type: KeyEnter}
|
||||||
|
// fmt.Println(k)
|
||||||
|
// // Output: enter
|
||||||
|
func (k Key) String() (str string) {
|
||||||
|
var buf strings.Builder
|
||||||
|
if k.Alt {
|
||||||
|
buf.WriteString("alt+")
|
||||||
|
}
|
||||||
|
if k.Type == KeyRunes {
|
||||||
|
if k.Paste {
|
||||||
|
// Note: bubbles/keys bindings currently do string compares to
|
||||||
|
// recognize shortcuts. Since pasted text should never activate
|
||||||
|
// shortcuts, we need to ensure that the binding code doesn't
|
||||||
|
// match Key events that result from pastes. We achieve this
|
||||||
|
// here by enclosing pastes in '[...]' so that the string
|
||||||
|
// comparison in Matches() fails in that case.
|
||||||
|
buf.WriteByte('[')
|
||||||
|
}
|
||||||
|
buf.WriteString(string(k.Runes))
|
||||||
|
if k.Paste {
|
||||||
|
buf.WriteByte(']')
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
} else if s, ok := keyNames[k.Type]; ok {
|
||||||
|
buf.WriteString(s)
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyType indicates the key pressed, such as KeyEnter or KeyBreak or KeyCtrlC.
|
||||||
|
// All other keys will be type KeyRunes. To get the rune value, check the Rune
|
||||||
|
// method on a Key struct, or use the Key.String() method:
|
||||||
|
//
|
||||||
|
// k := Key{Type: KeyRunes, Runes: []rune{'a'}, Alt: true}
|
||||||
|
// if k.Type == KeyRunes {
|
||||||
|
//
|
||||||
|
// fmt.Println(k.Runes)
|
||||||
|
// // Output: a
|
||||||
|
//
|
||||||
|
// fmt.Println(k.String())
|
||||||
|
// // Output: alt+a
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
type KeyType int
|
||||||
|
|
||||||
|
func (k KeyType) String() (str string) {
|
||||||
|
if s, ok := keyNames[k]; ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control keys. We could do this with an iota, but the values are very
|
||||||
|
// specific, so we set the values explicitly to avoid any confusion.
|
||||||
|
//
|
||||||
|
// See also:
|
||||||
|
// https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
||||||
|
const (
|
||||||
|
keyNUL KeyType = 0 // null, \0
|
||||||
|
keySOH KeyType = 1 // start of heading
|
||||||
|
keySTX KeyType = 2 // start of text
|
||||||
|
keyETX KeyType = 3 // break, ctrl+c
|
||||||
|
keyEOT KeyType = 4 // end of transmission
|
||||||
|
keyENQ KeyType = 5 // enquiry
|
||||||
|
keyACK KeyType = 6 // acknowledge
|
||||||
|
keyBEL KeyType = 7 // bell, \a
|
||||||
|
keyBS KeyType = 8 // backspace
|
||||||
|
keyHT KeyType = 9 // horizontal tabulation, \t
|
||||||
|
keyLF KeyType = 10 // line feed, \n
|
||||||
|
keyVT KeyType = 11 // vertical tabulation \v
|
||||||
|
keyFF KeyType = 12 // form feed \f
|
||||||
|
keyCR KeyType = 13 // carriage return, \r
|
||||||
|
keySO KeyType = 14 // shift out
|
||||||
|
keySI KeyType = 15 // shift in
|
||||||
|
keyDLE KeyType = 16 // data link escape
|
||||||
|
keyDC1 KeyType = 17 // device control one
|
||||||
|
keyDC2 KeyType = 18 // device control two
|
||||||
|
keyDC3 KeyType = 19 // device control three
|
||||||
|
keyDC4 KeyType = 20 // device control four
|
||||||
|
keyNAK KeyType = 21 // negative acknowledge
|
||||||
|
keySYN KeyType = 22 // synchronous idle
|
||||||
|
keyETB KeyType = 23 // end of transmission block
|
||||||
|
keyCAN KeyType = 24 // cancel
|
||||||
|
keyEM KeyType = 25 // end of medium
|
||||||
|
keySUB KeyType = 26 // substitution
|
||||||
|
keyESC KeyType = 27 // escape, \e
|
||||||
|
keyFS KeyType = 28 // file separator
|
||||||
|
keyGS KeyType = 29 // group separator
|
||||||
|
keyRS KeyType = 30 // record separator
|
||||||
|
keyUS KeyType = 31 // unit separator
|
||||||
|
keyDEL KeyType = 127 // delete. on most systems this is mapped to backspace, I hear
|
||||||
|
)
|
||||||
|
|
||||||
|
// Control key aliases.
|
||||||
|
const (
|
||||||
|
KeyNull KeyType = keyNUL
|
||||||
|
KeyBreak KeyType = keyETX
|
||||||
|
KeyEnter KeyType = keyCR
|
||||||
|
KeyBackspace KeyType = keyDEL
|
||||||
|
KeyTab KeyType = keyHT
|
||||||
|
KeyEsc KeyType = keyESC
|
||||||
|
KeyEscape KeyType = keyESC
|
||||||
|
|
||||||
|
KeyCtrlAt KeyType = keyNUL // ctrl+@
|
||||||
|
KeyCtrlA KeyType = keySOH
|
||||||
|
KeyCtrlB KeyType = keySTX
|
||||||
|
KeyCtrlC KeyType = keyETX
|
||||||
|
KeyCtrlD KeyType = keyEOT
|
||||||
|
KeyCtrlE KeyType = keyENQ
|
||||||
|
KeyCtrlF KeyType = keyACK
|
||||||
|
KeyCtrlG KeyType = keyBEL
|
||||||
|
KeyCtrlH KeyType = keyBS
|
||||||
|
KeyCtrlI KeyType = keyHT
|
||||||
|
KeyCtrlJ KeyType = keyLF
|
||||||
|
KeyCtrlK KeyType = keyVT
|
||||||
|
KeyCtrlL KeyType = keyFF
|
||||||
|
KeyCtrlM KeyType = keyCR
|
||||||
|
KeyCtrlN KeyType = keySO
|
||||||
|
KeyCtrlO KeyType = keySI
|
||||||
|
KeyCtrlP KeyType = keyDLE
|
||||||
|
KeyCtrlQ KeyType = keyDC1
|
||||||
|
KeyCtrlR KeyType = keyDC2
|
||||||
|
KeyCtrlS KeyType = keyDC3
|
||||||
|
KeyCtrlT KeyType = keyDC4
|
||||||
|
KeyCtrlU KeyType = keyNAK
|
||||||
|
KeyCtrlV KeyType = keySYN
|
||||||
|
KeyCtrlW KeyType = keyETB
|
||||||
|
KeyCtrlX KeyType = keyCAN
|
||||||
|
KeyCtrlY KeyType = keyEM
|
||||||
|
KeyCtrlZ KeyType = keySUB
|
||||||
|
KeyCtrlOpenBracket KeyType = keyESC // ctrl+[
|
||||||
|
KeyCtrlBackslash KeyType = keyFS // ctrl+\
|
||||||
|
KeyCtrlCloseBracket KeyType = keyGS // ctrl+]
|
||||||
|
KeyCtrlCaret KeyType = keyRS // ctrl+^
|
||||||
|
KeyCtrlUnderscore KeyType = keyUS // ctrl+_
|
||||||
|
KeyCtrlQuestionMark KeyType = keyDEL // ctrl+?
|
||||||
|
)
|
||||||
|
|
||||||
|
// Other keys.
|
||||||
|
const (
|
||||||
|
KeyRunes KeyType = -(iota + 1)
|
||||||
|
KeyUp
|
||||||
|
KeyDown
|
||||||
|
KeyRight
|
||||||
|
KeyLeft
|
||||||
|
KeyShiftTab
|
||||||
|
KeyHome
|
||||||
|
KeyEnd
|
||||||
|
KeyPgUp
|
||||||
|
KeyPgDown
|
||||||
|
KeyCtrlPgUp
|
||||||
|
KeyCtrlPgDown
|
||||||
|
KeyDelete
|
||||||
|
KeyInsert
|
||||||
|
KeySpace
|
||||||
|
KeyCtrlUp
|
||||||
|
KeyCtrlDown
|
||||||
|
KeyCtrlRight
|
||||||
|
KeyCtrlLeft
|
||||||
|
KeyCtrlHome
|
||||||
|
KeyCtrlEnd
|
||||||
|
KeyShiftUp
|
||||||
|
KeyShiftDown
|
||||||
|
KeyShiftRight
|
||||||
|
KeyShiftLeft
|
||||||
|
KeyShiftHome
|
||||||
|
KeyShiftEnd
|
||||||
|
KeyCtrlShiftUp
|
||||||
|
KeyCtrlShiftDown
|
||||||
|
KeyCtrlShiftLeft
|
||||||
|
KeyCtrlShiftRight
|
||||||
|
KeyCtrlShiftHome
|
||||||
|
KeyCtrlShiftEnd
|
||||||
|
KeyF1
|
||||||
|
KeyF2
|
||||||
|
KeyF3
|
||||||
|
KeyF4
|
||||||
|
KeyF5
|
||||||
|
KeyF6
|
||||||
|
KeyF7
|
||||||
|
KeyF8
|
||||||
|
KeyF9
|
||||||
|
KeyF10
|
||||||
|
KeyF11
|
||||||
|
KeyF12
|
||||||
|
KeyF13
|
||||||
|
KeyF14
|
||||||
|
KeyF15
|
||||||
|
KeyF16
|
||||||
|
KeyF17
|
||||||
|
KeyF18
|
||||||
|
KeyF19
|
||||||
|
KeyF20
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mappings for control keys and other special keys to friendly consts.
|
||||||
|
var keyNames = map[KeyType]string{
|
||||||
|
// Control keys.
|
||||||
|
keyNUL: "ctrl+@", // also ctrl+` (that's ctrl+backtick)
|
||||||
|
keySOH: "ctrl+a",
|
||||||
|
keySTX: "ctrl+b",
|
||||||
|
keyETX: "ctrl+c",
|
||||||
|
keyEOT: "ctrl+d",
|
||||||
|
keyENQ: "ctrl+e",
|
||||||
|
keyACK: "ctrl+f",
|
||||||
|
keyBEL: "ctrl+g",
|
||||||
|
keyBS: "ctrl+h",
|
||||||
|
keyHT: "tab", // also ctrl+i
|
||||||
|
keyLF: "ctrl+j",
|
||||||
|
keyVT: "ctrl+k",
|
||||||
|
keyFF: "ctrl+l",
|
||||||
|
keyCR: "enter",
|
||||||
|
keySO: "ctrl+n",
|
||||||
|
keySI: "ctrl+o",
|
||||||
|
keyDLE: "ctrl+p",
|
||||||
|
keyDC1: "ctrl+q",
|
||||||
|
keyDC2: "ctrl+r",
|
||||||
|
keyDC3: "ctrl+s",
|
||||||
|
keyDC4: "ctrl+t",
|
||||||
|
keyNAK: "ctrl+u",
|
||||||
|
keySYN: "ctrl+v",
|
||||||
|
keyETB: "ctrl+w",
|
||||||
|
keyCAN: "ctrl+x",
|
||||||
|
keyEM: "ctrl+y",
|
||||||
|
keySUB: "ctrl+z",
|
||||||
|
keyESC: "esc",
|
||||||
|
keyFS: "ctrl+\\",
|
||||||
|
keyGS: "ctrl+]",
|
||||||
|
keyRS: "ctrl+^",
|
||||||
|
keyUS: "ctrl+_",
|
||||||
|
keyDEL: "backspace",
|
||||||
|
|
||||||
|
// Other keys.
|
||||||
|
KeyRunes: "runes",
|
||||||
|
KeyUp: "up",
|
||||||
|
KeyDown: "down",
|
||||||
|
KeyRight: "right",
|
||||||
|
KeySpace: " ", // for backwards compatibility
|
||||||
|
KeyLeft: "left",
|
||||||
|
KeyShiftTab: "shift+tab",
|
||||||
|
KeyHome: "home",
|
||||||
|
KeyEnd: "end",
|
||||||
|
KeyCtrlHome: "ctrl+home",
|
||||||
|
KeyCtrlEnd: "ctrl+end",
|
||||||
|
KeyShiftHome: "shift+home",
|
||||||
|
KeyShiftEnd: "shift+end",
|
||||||
|
KeyCtrlShiftHome: "ctrl+shift+home",
|
||||||
|
KeyCtrlShiftEnd: "ctrl+shift+end",
|
||||||
|
KeyPgUp: "pgup",
|
||||||
|
KeyPgDown: "pgdown",
|
||||||
|
KeyCtrlPgUp: "ctrl+pgup",
|
||||||
|
KeyCtrlPgDown: "ctrl+pgdown",
|
||||||
|
KeyDelete: "delete",
|
||||||
|
KeyInsert: "insert",
|
||||||
|
KeyCtrlUp: "ctrl+up",
|
||||||
|
KeyCtrlDown: "ctrl+down",
|
||||||
|
KeyCtrlRight: "ctrl+right",
|
||||||
|
KeyCtrlLeft: "ctrl+left",
|
||||||
|
KeyShiftUp: "shift+up",
|
||||||
|
KeyShiftDown: "shift+down",
|
||||||
|
KeyShiftRight: "shift+right",
|
||||||
|
KeyShiftLeft: "shift+left",
|
||||||
|
KeyCtrlShiftUp: "ctrl+shift+up",
|
||||||
|
KeyCtrlShiftDown: "ctrl+shift+down",
|
||||||
|
KeyCtrlShiftLeft: "ctrl+shift+left",
|
||||||
|
KeyCtrlShiftRight: "ctrl+shift+right",
|
||||||
|
KeyF1: "f1",
|
||||||
|
KeyF2: "f2",
|
||||||
|
KeyF3: "f3",
|
||||||
|
KeyF4: "f4",
|
||||||
|
KeyF5: "f5",
|
||||||
|
KeyF6: "f6",
|
||||||
|
KeyF7: "f7",
|
||||||
|
KeyF8: "f8",
|
||||||
|
KeyF9: "f9",
|
||||||
|
KeyF10: "f10",
|
||||||
|
KeyF11: "f11",
|
||||||
|
KeyF12: "f12",
|
||||||
|
KeyF13: "f13",
|
||||||
|
KeyF14: "f14",
|
||||||
|
KeyF15: "f15",
|
||||||
|
KeyF16: "f16",
|
||||||
|
KeyF17: "f17",
|
||||||
|
KeyF18: "f18",
|
||||||
|
KeyF19: "f19",
|
||||||
|
KeyF20: "f20",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sequence mappings.
|
||||||
|
var sequences = map[string]Key{
|
||||||
|
// Arrow keys
|
||||||
|
"\x1b[A": {Type: KeyUp},
|
||||||
|
"\x1b[B": {Type: KeyDown},
|
||||||
|
"\x1b[C": {Type: KeyRight},
|
||||||
|
"\x1b[D": {Type: KeyLeft},
|
||||||
|
"\x1b[1;2A": {Type: KeyShiftUp},
|
||||||
|
"\x1b[1;2B": {Type: KeyShiftDown},
|
||||||
|
"\x1b[1;2C": {Type: KeyShiftRight},
|
||||||
|
"\x1b[1;2D": {Type: KeyShiftLeft},
|
||||||
|
"\x1b[OA": {Type: KeyShiftUp}, // DECCKM
|
||||||
|
"\x1b[OB": {Type: KeyShiftDown}, // DECCKM
|
||||||
|
"\x1b[OC": {Type: KeyShiftRight}, // DECCKM
|
||||||
|
"\x1b[OD": {Type: KeyShiftLeft}, // DECCKM
|
||||||
|
"\x1b[a": {Type: KeyShiftUp}, // urxvt
|
||||||
|
"\x1b[b": {Type: KeyShiftDown}, // urxvt
|
||||||
|
"\x1b[c": {Type: KeyShiftRight}, // urxvt
|
||||||
|
"\x1b[d": {Type: KeyShiftLeft}, // urxvt
|
||||||
|
"\x1b[1;3A": {Type: KeyUp, Alt: true},
|
||||||
|
"\x1b[1;3B": {Type: KeyDown, Alt: true},
|
||||||
|
"\x1b[1;3C": {Type: KeyRight, Alt: true},
|
||||||
|
"\x1b[1;3D": {Type: KeyLeft, Alt: true},
|
||||||
|
|
||||||
|
"\x1b[1;4A": {Type: KeyShiftUp, Alt: true},
|
||||||
|
"\x1b[1;4B": {Type: KeyShiftDown, Alt: true},
|
||||||
|
"\x1b[1;4C": {Type: KeyShiftRight, Alt: true},
|
||||||
|
"\x1b[1;4D": {Type: KeyShiftLeft, Alt: true},
|
||||||
|
|
||||||
|
"\x1b[1;5A": {Type: KeyCtrlUp},
|
||||||
|
"\x1b[1;5B": {Type: KeyCtrlDown},
|
||||||
|
"\x1b[1;5C": {Type: KeyCtrlRight},
|
||||||
|
"\x1b[1;5D": {Type: KeyCtrlLeft},
|
||||||
|
"\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt
|
||||||
|
"\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt
|
||||||
|
"\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt
|
||||||
|
"\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt
|
||||||
|
"\x1b[1;6A": {Type: KeyCtrlShiftUp},
|
||||||
|
"\x1b[1;6B": {Type: KeyCtrlShiftDown},
|
||||||
|
"\x1b[1;6C": {Type: KeyCtrlShiftRight},
|
||||||
|
"\x1b[1;6D": {Type: KeyCtrlShiftLeft},
|
||||||
|
"\x1b[1;7A": {Type: KeyCtrlUp, Alt: true},
|
||||||
|
"\x1b[1;7B": {Type: KeyCtrlDown, Alt: true},
|
||||||
|
"\x1b[1;7C": {Type: KeyCtrlRight, Alt: true},
|
||||||
|
"\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true},
|
||||||
|
"\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true},
|
||||||
|
"\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true},
|
||||||
|
"\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true},
|
||||||
|
"\x1b[1;8D": {Type: KeyCtrlShiftLeft, Alt: true},
|
||||||
|
|
||||||
|
// Miscellaneous keys
|
||||||
|
"\x1b[Z": {Type: KeyShiftTab},
|
||||||
|
|
||||||
|
"\x1b[2~": {Type: KeyInsert},
|
||||||
|
"\x1b[3;2~": {Type: KeyInsert, Alt: true},
|
||||||
|
|
||||||
|
"\x1b[3~": {Type: KeyDelete},
|
||||||
|
"\x1b[3;3~": {Type: KeyDelete, Alt: true},
|
||||||
|
|
||||||
|
"\x1b[5~": {Type: KeyPgUp},
|
||||||
|
"\x1b[5;3~": {Type: KeyPgUp, Alt: true},
|
||||||
|
"\x1b[5;5~": {Type: KeyCtrlPgUp},
|
||||||
|
"\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt
|
||||||
|
"\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true},
|
||||||
|
|
||||||
|
"\x1b[6~": {Type: KeyPgDown},
|
||||||
|
"\x1b[6;3~": {Type: KeyPgDown, Alt: true},
|
||||||
|
"\x1b[6;5~": {Type: KeyCtrlPgDown},
|
||||||
|
"\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt
|
||||||
|
"\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true},
|
||||||
|
|
||||||
|
"\x1b[1~": {Type: KeyHome},
|
||||||
|
"\x1b[H": {Type: KeyHome}, // xterm, lxterm
|
||||||
|
"\x1b[1;3H": {Type: KeyHome, Alt: true}, // xterm, lxterm
|
||||||
|
"\x1b[1;5H": {Type: KeyCtrlHome}, // xterm, lxterm
|
||||||
|
"\x1b[1;7H": {Type: KeyCtrlHome, Alt: true}, // xterm, lxterm
|
||||||
|
"\x1b[1;2H": {Type: KeyShiftHome}, // xterm, lxterm
|
||||||
|
"\x1b[1;4H": {Type: KeyShiftHome, Alt: true}, // xterm, lxterm
|
||||||
|
"\x1b[1;6H": {Type: KeyCtrlShiftHome}, // xterm, lxterm
|
||||||
|
"\x1b[1;8H": {Type: KeyCtrlShiftHome, Alt: true}, // xterm, lxterm
|
||||||
|
|
||||||
|
"\x1b[4~": {Type: KeyEnd},
|
||||||
|
"\x1b[F": {Type: KeyEnd}, // xterm, lxterm
|
||||||
|
"\x1b[1;3F": {Type: KeyEnd, Alt: true}, // xterm, lxterm
|
||||||
|
"\x1b[1;5F": {Type: KeyCtrlEnd}, // xterm, lxterm
|
||||||
|
"\x1b[1;7F": {Type: KeyCtrlEnd, Alt: true}, // xterm, lxterm
|
||||||
|
"\x1b[1;2F": {Type: KeyShiftEnd}, // xterm, lxterm
|
||||||
|
"\x1b[1;4F": {Type: KeyShiftEnd, Alt: true}, // xterm, lxterm
|
||||||
|
"\x1b[1;6F": {Type: KeyCtrlShiftEnd}, // xterm, lxterm
|
||||||
|
"\x1b[1;8F": {Type: KeyCtrlShiftEnd, Alt: true}, // xterm, lxterm
|
||||||
|
|
||||||
|
"\x1b[7~": {Type: KeyHome}, // urxvt
|
||||||
|
"\x1b[7^": {Type: KeyCtrlHome}, // urxvt
|
||||||
|
"\x1b[7$": {Type: KeyShiftHome}, // urxvt
|
||||||
|
"\x1b[7@": {Type: KeyCtrlShiftHome}, // urxvt
|
||||||
|
|
||||||
|
"\x1b[8~": {Type: KeyEnd}, // urxvt
|
||||||
|
"\x1b[8^": {Type: KeyCtrlEnd}, // urxvt
|
||||||
|
"\x1b[8$": {Type: KeyShiftEnd}, // urxvt
|
||||||
|
"\x1b[8@": {Type: KeyCtrlShiftEnd}, // urxvt
|
||||||
|
|
||||||
|
// Function keys, Linux console
|
||||||
|
"\x1b[[A": {Type: KeyF1}, // linux console
|
||||||
|
"\x1b[[B": {Type: KeyF2}, // linux console
|
||||||
|
"\x1b[[C": {Type: KeyF3}, // linux console
|
||||||
|
"\x1b[[D": {Type: KeyF4}, // linux console
|
||||||
|
"\x1b[[E": {Type: KeyF5}, // linux console
|
||||||
|
|
||||||
|
// Function keys, X11
|
||||||
|
"\x1bOP": {Type: KeyF1}, // vt100, xterm
|
||||||
|
"\x1bOQ": {Type: KeyF2}, // vt100, xterm
|
||||||
|
"\x1bOR": {Type: KeyF3}, // vt100, xterm
|
||||||
|
"\x1bOS": {Type: KeyF4}, // vt100, xterm
|
||||||
|
|
||||||
|
"\x1b[1;3P": {Type: KeyF1, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[1;3Q": {Type: KeyF2, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[1;3R": {Type: KeyF3, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[1;3S": {Type: KeyF4, Alt: true}, // vt100, xterm
|
||||||
|
|
||||||
|
"\x1b[11~": {Type: KeyF1}, // urxvt
|
||||||
|
"\x1b[12~": {Type: KeyF2}, // urxvt
|
||||||
|
"\x1b[13~": {Type: KeyF3}, // urxvt
|
||||||
|
"\x1b[14~": {Type: KeyF4}, // urxvt
|
||||||
|
|
||||||
|
"\x1b[15~": {Type: KeyF5}, // vt100, xterm, also urxvt
|
||||||
|
|
||||||
|
"\x1b[15;3~": {Type: KeyF5, Alt: true}, // vt100, xterm, also urxvt
|
||||||
|
|
||||||
|
"\x1b[17~": {Type: KeyF6}, // vt100, xterm, also urxvt
|
||||||
|
"\x1b[18~": {Type: KeyF7}, // vt100, xterm, also urxvt
|
||||||
|
"\x1b[19~": {Type: KeyF8}, // vt100, xterm, also urxvt
|
||||||
|
"\x1b[20~": {Type: KeyF9}, // vt100, xterm, also urxvt
|
||||||
|
"\x1b[21~": {Type: KeyF10}, // vt100, xterm, also urxvt
|
||||||
|
|
||||||
|
"\x1b[17;3~": {Type: KeyF6, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[18;3~": {Type: KeyF7, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[19;3~": {Type: KeyF8, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[20;3~": {Type: KeyF9, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[21;3~": {Type: KeyF10, Alt: true}, // vt100, xterm
|
||||||
|
|
||||||
|
"\x1b[23~": {Type: KeyF11}, // vt100, xterm, also urxvt
|
||||||
|
"\x1b[24~": {Type: KeyF12}, // vt100, xterm, also urxvt
|
||||||
|
|
||||||
|
"\x1b[23;3~": {Type: KeyF11, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[24;3~": {Type: KeyF12, Alt: true}, // vt100, xterm
|
||||||
|
|
||||||
|
"\x1b[1;2P": {Type: KeyF13},
|
||||||
|
"\x1b[1;2Q": {Type: KeyF14},
|
||||||
|
|
||||||
|
"\x1b[25~": {Type: KeyF13}, // vt100, xterm, also urxvt
|
||||||
|
"\x1b[26~": {Type: KeyF14}, // vt100, xterm, also urxvt
|
||||||
|
|
||||||
|
"\x1b[25;3~": {Type: KeyF13, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[26;3~": {Type: KeyF14, Alt: true}, // vt100, xterm
|
||||||
|
|
||||||
|
"\x1b[1;2R": {Type: KeyF15},
|
||||||
|
"\x1b[1;2S": {Type: KeyF16},
|
||||||
|
|
||||||
|
"\x1b[28~": {Type: KeyF15}, // vt100, xterm, also urxvt
|
||||||
|
"\x1b[29~": {Type: KeyF16}, // vt100, xterm, also urxvt
|
||||||
|
|
||||||
|
"\x1b[28;3~": {Type: KeyF15, Alt: true}, // vt100, xterm
|
||||||
|
"\x1b[29;3~": {Type: KeyF16, Alt: true}, // vt100, xterm
|
||||||
|
|
||||||
|
"\x1b[15;2~": {Type: KeyF17},
|
||||||
|
"\x1b[17;2~": {Type: KeyF18},
|
||||||
|
"\x1b[18;2~": {Type: KeyF19},
|
||||||
|
"\x1b[19;2~": {Type: KeyF20},
|
||||||
|
|
||||||
|
"\x1b[31~": {Type: KeyF17},
|
||||||
|
"\x1b[32~": {Type: KeyF18},
|
||||||
|
"\x1b[33~": {Type: KeyF19},
|
||||||
|
"\x1b[34~": {Type: KeyF20},
|
||||||
|
|
||||||
|
// Powershell sequences.
|
||||||
|
"\x1bOA": {Type: KeyUp, Alt: false},
|
||||||
|
"\x1bOB": {Type: KeyDown, Alt: false},
|
||||||
|
"\x1bOC": {Type: KeyRight, Alt: false},
|
||||||
|
"\x1bOD": {Type: KeyLeft, Alt: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknownInputByteMsg is reported by the input reader when an invalid
|
||||||
|
// utf-8 byte is detected on the input. Currently, it is not handled
|
||||||
|
// further by bubbletea. However, having this event makes it possible
|
||||||
|
// to troubleshoot invalid inputs.
|
||||||
|
type unknownInputByteMsg byte
|
||||||
|
|
||||||
|
func (u unknownInputByteMsg) String() string {
|
||||||
|
return fmt.Sprintf("?%#02x?", int(u))
|
||||||
|
}
|
||||||
|
|
||||||
|
// unknownCSISequenceMsg is reported by the input reader when an
|
||||||
|
// unrecognized CSI sequence is detected on the input. Currently, it
|
||||||
|
// is not handled further by bubbletea. However, having this event
|
||||||
|
// makes it possible to troubleshoot invalid inputs.
|
||||||
|
type unknownCSISequenceMsg []byte
|
||||||
|
|
||||||
|
func (u unknownCSISequenceMsg) String() string {
|
||||||
|
return fmt.Sprintf("?CSI%+v?", []byte(u)[2:])
|
||||||
|
}
|
||||||
|
|
||||||
|
var spaceRunes = []rune{' '}
|
||||||
|
|
||||||
|
// readAnsiInputs reads keypress and mouse inputs from a TTY and produces messages
|
||||||
|
// containing information about the key or mouse events accordingly.
|
||||||
|
func readAnsiInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
|
||||||
|
var buf [256]byte
|
||||||
|
|
||||||
|
var leftOverFromPrevIteration []byte
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
// Read and block.
|
||||||
|
numBytes, err := input.Read(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading input: %w", err)
|
||||||
|
}
|
||||||
|
b := buf[:numBytes]
|
||||||
|
if leftOverFromPrevIteration != nil {
|
||||||
|
b = append(leftOverFromPrevIteration, b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we had a short read (numBytes < len(buf)), we're sure that
|
||||||
|
// the end of this read is an event boundary, so there is no doubt
|
||||||
|
// if we are encountering the end of the buffer while parsing a message.
|
||||||
|
// However, if we've succeeded in filling up the buffer, there may
|
||||||
|
// be more data in the OS buffer ready to be read in, to complete
|
||||||
|
// the last message in the input. In that case, we will retry with
|
||||||
|
// the left over data in the next iteration.
|
||||||
|
canHaveMoreData := numBytes == len(buf)
|
||||||
|
|
||||||
|
var i, w int
|
||||||
|
for i, w = 0, 0; i < len(b); i += w {
|
||||||
|
var msg Msg
|
||||||
|
w, msg = detectOneMsg(b[i:], canHaveMoreData)
|
||||||
|
if w == 0 {
|
||||||
|
// Expecting more bytes beyond the current buffer. Try waiting
|
||||||
|
// for more input.
|
||||||
|
leftOverFromPrevIteration = make([]byte, 0, len(b[i:])+len(buf))
|
||||||
|
leftOverFromPrevIteration = append(leftOverFromPrevIteration, b[i:]...)
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case msgs <- msg:
|
||||||
|
case <-ctx.Done():
|
||||||
|
err := ctx.Err()
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("found context error while reading input: %w", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
leftOverFromPrevIteration = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`)
|
||||||
|
mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) {
|
||||||
|
// Detect mouse events.
|
||||||
|
// X10 mouse events have a length of 6 bytes
|
||||||
|
const mouseEventX10Len = 6
|
||||||
|
if len(b) >= mouseEventX10Len && b[0] == '\x1b' && b[1] == '[' {
|
||||||
|
switch b[2] {
|
||||||
|
case 'M':
|
||||||
|
return mouseEventX10Len, MouseMsg(parseX10MouseEvent(b))
|
||||||
|
case '<':
|
||||||
|
if matchIndices := mouseSGRRegex.FindSubmatchIndex(b[3:]); matchIndices != nil {
|
||||||
|
// SGR mouse events length is the length of the match plus the length of the escape sequence
|
||||||
|
mouseEventSGRLen := matchIndices[1] + 3 //nolint:mnd
|
||||||
|
return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect focus events.
|
||||||
|
var foundRF bool
|
||||||
|
foundRF, w, msg = detectReportFocus(b)
|
||||||
|
if foundRF {
|
||||||
|
return w, msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect bracketed paste.
|
||||||
|
var foundbp bool
|
||||||
|
foundbp, w, msg = detectBracketedPaste(b)
|
||||||
|
if foundbp {
|
||||||
|
return w, msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect escape sequence and control characters other than NUL,
|
||||||
|
// possibly with an escape character in front to mark the Alt
|
||||||
|
// modifier.
|
||||||
|
var foundSeq bool
|
||||||
|
foundSeq, w, msg = detectSequence(b)
|
||||||
|
if foundSeq {
|
||||||
|
return w, msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// No non-NUL control character or escape sequence.
|
||||||
|
// If we are seeing at least an escape character, remember it for later below.
|
||||||
|
alt := false
|
||||||
|
i := 0
|
||||||
|
if b[0] == '\x1b' {
|
||||||
|
alt = true
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we seeing a standalone NUL? This is not handled by detectSequence().
|
||||||
|
if i < len(b) && b[i] == 0 {
|
||||||
|
return i + 1, KeyMsg{Type: keyNUL, Alt: alt}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the longest sequence of runes that are not control
|
||||||
|
// characters from this point.
|
||||||
|
var runes []rune
|
||||||
|
for rw := 0; i < len(b); i += rw {
|
||||||
|
var r rune
|
||||||
|
r, rw = utf8.DecodeRune(b[i:])
|
||||||
|
if r == utf8.RuneError || r <= rune(keyUS) || r == rune(keyDEL) || r == ' ' {
|
||||||
|
// Rune errors are handled below; control characters and spaces will
|
||||||
|
// be handled by detectSequence in the next call to detectOneMsg.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
runes = append(runes, r)
|
||||||
|
if alt {
|
||||||
|
// We only support a single rune after an escape alt modifier.
|
||||||
|
i += rw
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i >= len(b) && canHaveMoreData {
|
||||||
|
// We have encountered the end of the input buffer. Alas, we can't
|
||||||
|
// be sure whether the data in the remainder of the buffer is
|
||||||
|
// complete (maybe there was a short read). Instead of sending anything
|
||||||
|
// dumb to the message channel, do a short read. The outer loop will
|
||||||
|
// handle this case by extending the buffer as necessary.
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found at least one rune, we report the bunch of them as
|
||||||
|
// a single KeyRunes or KeySpace event.
|
||||||
|
if len(runes) > 0 {
|
||||||
|
k := Key{Type: KeyRunes, Runes: runes, Alt: alt}
|
||||||
|
if len(runes) == 1 && runes[0] == ' ' {
|
||||||
|
k.Type = KeySpace
|
||||||
|
}
|
||||||
|
return i, KeyMsg(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We didn't find an escape sequence, nor a valid rune. Was this a
|
||||||
|
// lone escape character at the end of the input?
|
||||||
|
if alt && len(b) == 1 {
|
||||||
|
return 1, KeyMsg(Key{Type: KeyEscape})
|
||||||
|
}
|
||||||
|
|
||||||
|
// The character at the current position is neither an escape
|
||||||
|
// sequence, a valid rune start or a sole escape character. Report
|
||||||
|
// it as an invalid byte.
|
||||||
|
return 1, unknownInputByteMsg(b[0])
|
||||||
|
}
|
||||||
13
vendor/github.com/charmbracelet/bubbletea/key_other.go
generated
vendored
Normal file
13
vendor/github.com/charmbracelet/bubbletea/key_other.go
generated
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
|
||||||
|
return readAnsiInputs(ctx, msgs, input)
|
||||||
|
}
|
||||||
130
vendor/github.com/charmbracelet/bubbletea/key_sequences.go
generated
vendored
Normal file
130
vendor/github.com/charmbracelet/bubbletea/key_sequences.go
generated
vendored
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"sort"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// extSequences is used by the map-based algorithm below. It contains
|
||||||
|
// the sequences plus their alternatives with an escape character
|
||||||
|
// prefixed, plus the control chars, plus the space.
|
||||||
|
// It does not contain the NUL character, which is handled specially
|
||||||
|
// by detectOneMsg.
|
||||||
|
var extSequences = func() map[string]Key {
|
||||||
|
s := map[string]Key{}
|
||||||
|
for seq, key := range sequences {
|
||||||
|
key := key
|
||||||
|
s[seq] = key
|
||||||
|
if !key.Alt {
|
||||||
|
key.Alt = true
|
||||||
|
s["\x1b"+seq] = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := keyNUL + 1; i <= keyDEL; i++ {
|
||||||
|
if i == keyESC {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s[string([]byte{byte(i)})] = Key{Type: i}
|
||||||
|
s[string([]byte{'\x1b', byte(i)})] = Key{Type: i, Alt: true}
|
||||||
|
if i == keyUS {
|
||||||
|
i = keyDEL - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s[" "] = Key{Type: KeySpace, Runes: spaceRunes}
|
||||||
|
s["\x1b "] = Key{Type: KeySpace, Alt: true, Runes: spaceRunes}
|
||||||
|
s["\x1b\x1b"] = Key{Type: KeyEscape, Alt: true}
|
||||||
|
return s
|
||||||
|
}()
|
||||||
|
|
||||||
|
// seqLengths is the sizes of valid sequences, starting with the
|
||||||
|
// largest size.
|
||||||
|
var seqLengths = func() []int {
|
||||||
|
sizes := map[int]struct{}{}
|
||||||
|
for seq := range extSequences {
|
||||||
|
sizes[len(seq)] = struct{}{}
|
||||||
|
}
|
||||||
|
lsizes := make([]int, 0, len(sizes))
|
||||||
|
for sz := range sizes {
|
||||||
|
lsizes = append(lsizes, sz)
|
||||||
|
}
|
||||||
|
sort.Slice(lsizes, func(i, j int) bool { return lsizes[i] > lsizes[j] })
|
||||||
|
return lsizes
|
||||||
|
}()
|
||||||
|
|
||||||
|
// detectSequence uses a longest prefix match over the input
|
||||||
|
// sequence and a hash map.
|
||||||
|
func detectSequence(input []byte) (hasSeq bool, width int, msg Msg) {
|
||||||
|
seqs := extSequences
|
||||||
|
for _, sz := range seqLengths {
|
||||||
|
if sz > len(input) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prefix := input[:sz]
|
||||||
|
key, ok := seqs[string(prefix)]
|
||||||
|
if ok {
|
||||||
|
return true, sz, KeyMsg(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Is this an unknown CSI sequence?
|
||||||
|
if loc := unknownCSIRe.FindIndex(input); loc != nil {
|
||||||
|
return true, loc[1], unknownCSISequenceMsg(input[:loc[1]])
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectBracketedPaste detects an input pasted while bracketed
|
||||||
|
// paste mode was enabled.
|
||||||
|
//
|
||||||
|
// Note: this function is a no-op if bracketed paste was not enabled
|
||||||
|
// on the terminal, since in that case we'd never see this
|
||||||
|
// particular escape sequence.
|
||||||
|
func detectBracketedPaste(input []byte) (hasBp bool, width int, msg Msg) {
|
||||||
|
// Detect the start sequence.
|
||||||
|
const bpStart = "\x1b[200~"
|
||||||
|
if len(input) < len(bpStart) || string(input[:len(bpStart)]) != bpStart {
|
||||||
|
return false, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip over the start sequence.
|
||||||
|
input = input[len(bpStart):]
|
||||||
|
|
||||||
|
// If we saw the start sequence, then we must have an end sequence
|
||||||
|
// as well. Find it.
|
||||||
|
const bpEnd = "\x1b[201~"
|
||||||
|
idx := bytes.Index(input, []byte(bpEnd))
|
||||||
|
inputLen := len(bpStart) + idx + len(bpEnd)
|
||||||
|
if idx == -1 {
|
||||||
|
// We have encountered the end of the input buffer without seeing
|
||||||
|
// the marker for the end of the bracketed paste.
|
||||||
|
// Tell the outer loop we have done a short read and we want more.
|
||||||
|
return true, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The paste is everything in-between.
|
||||||
|
paste := input[:idx]
|
||||||
|
|
||||||
|
// All there is in-between is runes, not to be interpreted further.
|
||||||
|
k := Key{Type: KeyRunes, Paste: true}
|
||||||
|
for len(paste) > 0 {
|
||||||
|
r, w := utf8.DecodeRune(paste)
|
||||||
|
if r != utf8.RuneError {
|
||||||
|
k.Runes = append(k.Runes, r)
|
||||||
|
}
|
||||||
|
paste = paste[w:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, inputLen, KeyMsg(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectReportFocus detects a focus report sequence.
|
||||||
|
func detectReportFocus(input []byte) (hasRF bool, width int, msg Msg) {
|
||||||
|
switch {
|
||||||
|
case bytes.Equal(input, []byte("\x1b[I")):
|
||||||
|
return true, 3, FocusMsg{} //nolint:mnd
|
||||||
|
case bytes.Equal(input, []byte("\x1b[O")):
|
||||||
|
return true, 3, BlurMsg{} //nolint:mnd
|
||||||
|
}
|
||||||
|
return false, 0, nil
|
||||||
|
}
|
||||||
441
vendor/github.com/charmbracelet/bubbletea/key_windows.go
generated
vendored
Normal file
441
vendor/github.com/charmbracelet/bubbletea/key_windows.go
generated
vendored
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/erikgeiser/coninput"
|
||||||
|
localereader "github.com/mattn/go-localereader"
|
||||||
|
"github.com/muesli/cancelreader"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
|
||||||
|
if coninReader, ok := input.(*conInputReader); ok {
|
||||||
|
return readConInputs(ctx, msgs, coninReader)
|
||||||
|
}
|
||||||
|
|
||||||
|
return readAnsiInputs(ctx, msgs, localereader.NewReader(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConInputs(ctx context.Context, msgsch chan<- Msg, con *conInputReader) error {
|
||||||
|
var ps coninput.ButtonState // keep track of previous mouse state
|
||||||
|
var ws coninput.WindowBufferSizeEventRecord // keep track of the last window size event
|
||||||
|
for {
|
||||||
|
events, err := peekAndReadConsInput(con)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, event := range events {
|
||||||
|
var msgs []Msg
|
||||||
|
switch e := event.Unwrap().(type) {
|
||||||
|
case coninput.KeyEventRecord:
|
||||||
|
if !e.KeyDown || e.VirtualKeyCode == coninput.VK_SHIFT {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < int(e.RepeatCount); i++ {
|
||||||
|
eventKeyType := keyType(e)
|
||||||
|
var runes []rune
|
||||||
|
|
||||||
|
// Add the character only if the key type is an actual character and not a control sequence.
|
||||||
|
// This mimics the behavior in readAnsiInputs where the character is also removed.
|
||||||
|
// We don't need to handle KeySpace here. See the comment in keyType().
|
||||||
|
if eventKeyType == KeyRunes {
|
||||||
|
runes = []rune{e.Char}
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs = append(msgs, KeyMsg{
|
||||||
|
Type: eventKeyType,
|
||||||
|
Runes: runes,
|
||||||
|
Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case coninput.WindowBufferSizeEventRecord:
|
||||||
|
if e != ws {
|
||||||
|
ws = e
|
||||||
|
msgs = append(msgs, WindowSizeMsg{
|
||||||
|
Width: int(e.Size.X),
|
||||||
|
Height: int(e.Size.Y),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case coninput.MouseEventRecord:
|
||||||
|
event := mouseEvent(ps, e)
|
||||||
|
if event.Type != MouseUnknown {
|
||||||
|
msgs = append(msgs, event)
|
||||||
|
}
|
||||||
|
ps = e.ButtonState
|
||||||
|
case coninput.FocusEventRecord, coninput.MenuEventRecord:
|
||||||
|
// ignore
|
||||||
|
default: // unknown event
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send all messages to the channel
|
||||||
|
for _, msg := range msgs {
|
||||||
|
select {
|
||||||
|
case msgsch <- msg:
|
||||||
|
case <-ctx.Done():
|
||||||
|
err := ctx.Err()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("coninput context error: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek for new input in a tight loop and then read the input.
|
||||||
|
// windows.CancelIo* does not work reliably so peek first and only use the data if
|
||||||
|
// the console input is not cancelled.
|
||||||
|
func peekAndReadConsInput(con *conInputReader) ([]coninput.InputRecord, error) {
|
||||||
|
events, err := peekConsInput(con)
|
||||||
|
if err != nil {
|
||||||
|
return events, err
|
||||||
|
}
|
||||||
|
events, err = coninput.ReadNConsoleInputs(con.conin, intToUint32OrDie(len(events)))
|
||||||
|
if con.isCanceled() {
|
||||||
|
return events, cancelreader.ErrCanceled
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return events, fmt.Errorf("read coninput events: %w", err)
|
||||||
|
}
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert i to unit32 or panic if it cannot be converted. Check satisfies lint G115.
|
||||||
|
func intToUint32OrDie(i int) uint32 {
|
||||||
|
if i < 0 {
|
||||||
|
panic("cannot convert numEvents " + fmt.Sprint(i) + " to uint32")
|
||||||
|
}
|
||||||
|
return uint32(i) //nolint:gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keeps peeking until there is data or the input is cancelled.
|
||||||
|
func peekConsInput(con *conInputReader) ([]coninput.InputRecord, error) {
|
||||||
|
for {
|
||||||
|
events, err := coninput.PeekNConsoleInputs(con.conin, 16)
|
||||||
|
if con.isCanceled() {
|
||||||
|
return events, cancelreader.ErrCanceled
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return events, fmt.Errorf("peek coninput events: %w", err)
|
||||||
|
}
|
||||||
|
if len(events) > 0 {
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
// Sleep for a bit to avoid busy waiting.
|
||||||
|
time.Sleep(16 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action MouseAction) {
|
||||||
|
btn := p ^ s
|
||||||
|
action = MouseActionPress
|
||||||
|
if btn&s == 0 {
|
||||||
|
action = MouseActionRelease
|
||||||
|
}
|
||||||
|
|
||||||
|
if btn == 0 {
|
||||||
|
switch {
|
||||||
|
case s&coninput.FROM_LEFT_1ST_BUTTON_PRESSED > 0:
|
||||||
|
button = MouseButtonLeft
|
||||||
|
case s&coninput.FROM_LEFT_2ND_BUTTON_PRESSED > 0:
|
||||||
|
button = MouseButtonMiddle
|
||||||
|
case s&coninput.RIGHTMOST_BUTTON_PRESSED > 0:
|
||||||
|
button = MouseButtonRight
|
||||||
|
case s&coninput.FROM_LEFT_3RD_BUTTON_PRESSED > 0:
|
||||||
|
button = MouseButtonBackward
|
||||||
|
case s&coninput.FROM_LEFT_4TH_BUTTON_PRESSED > 0:
|
||||||
|
button = MouseButtonForward
|
||||||
|
}
|
||||||
|
return button, action
|
||||||
|
}
|
||||||
|
|
||||||
|
switch btn {
|
||||||
|
case coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
|
||||||
|
button = MouseButtonLeft
|
||||||
|
case coninput.RIGHTMOST_BUTTON_PRESSED: // right button
|
||||||
|
button = MouseButtonRight
|
||||||
|
case coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
|
||||||
|
button = MouseButtonMiddle
|
||||||
|
case coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
|
||||||
|
button = MouseButtonBackward
|
||||||
|
case coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
|
||||||
|
button = MouseButtonForward
|
||||||
|
}
|
||||||
|
|
||||||
|
return button, action
|
||||||
|
}
|
||||||
|
|
||||||
|
func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) MouseMsg {
|
||||||
|
ev := MouseMsg{
|
||||||
|
X: int(e.MousePositon.X),
|
||||||
|
Y: int(e.MousePositon.Y),
|
||||||
|
Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED),
|
||||||
|
Ctrl: e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED),
|
||||||
|
Shift: e.ControlKeyState.Contains(coninput.SHIFT_PRESSED),
|
||||||
|
}
|
||||||
|
switch e.EventFlags {
|
||||||
|
case coninput.CLICK, coninput.DOUBLE_CLICK:
|
||||||
|
ev.Button, ev.Action = mouseEventButton(p, e.ButtonState)
|
||||||
|
if ev.Action == MouseActionRelease {
|
||||||
|
ev.Type = MouseRelease
|
||||||
|
}
|
||||||
|
switch ev.Button { //nolint:exhaustive
|
||||||
|
case MouseButtonLeft:
|
||||||
|
ev.Type = MouseLeft
|
||||||
|
case MouseButtonMiddle:
|
||||||
|
ev.Type = MouseMiddle
|
||||||
|
case MouseButtonRight:
|
||||||
|
ev.Type = MouseRight
|
||||||
|
case MouseButtonBackward:
|
||||||
|
ev.Type = MouseBackward
|
||||||
|
case MouseButtonForward:
|
||||||
|
ev.Type = MouseForward
|
||||||
|
}
|
||||||
|
case coninput.MOUSE_WHEELED:
|
||||||
|
if e.WheelDirection > 0 {
|
||||||
|
ev.Button = MouseButtonWheelUp
|
||||||
|
ev.Type = MouseWheelUp
|
||||||
|
} else {
|
||||||
|
ev.Button = MouseButtonWheelDown
|
||||||
|
ev.Type = MouseWheelDown
|
||||||
|
}
|
||||||
|
case coninput.MOUSE_HWHEELED:
|
||||||
|
if e.WheelDirection > 0 {
|
||||||
|
ev.Button = MouseButtonWheelRight
|
||||||
|
ev.Type = MouseWheelRight
|
||||||
|
} else {
|
||||||
|
ev.Button = MouseButtonWheelLeft
|
||||||
|
ev.Type = MouseWheelLeft
|
||||||
|
}
|
||||||
|
case coninput.MOUSE_MOVED:
|
||||||
|
ev.Button, _ = mouseEventButton(p, e.ButtonState)
|
||||||
|
ev.Action = MouseActionMotion
|
||||||
|
ev.Type = MouseMotion
|
||||||
|
}
|
||||||
|
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyType(e coninput.KeyEventRecord) KeyType {
|
||||||
|
code := e.VirtualKeyCode
|
||||||
|
|
||||||
|
shiftPressed := e.ControlKeyState.Contains(coninput.SHIFT_PRESSED)
|
||||||
|
ctrlPressed := e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED)
|
||||||
|
|
||||||
|
switch code { //nolint:exhaustive
|
||||||
|
case coninput.VK_RETURN:
|
||||||
|
return KeyEnter
|
||||||
|
case coninput.VK_BACK:
|
||||||
|
return KeyBackspace
|
||||||
|
case coninput.VK_TAB:
|
||||||
|
if shiftPressed {
|
||||||
|
return KeyShiftTab
|
||||||
|
}
|
||||||
|
return KeyTab
|
||||||
|
case coninput.VK_SPACE:
|
||||||
|
return KeyRunes // this could be KeySpace but on unix space also produces KeyRunes
|
||||||
|
case coninput.VK_ESCAPE:
|
||||||
|
return KeyEscape
|
||||||
|
case coninput.VK_UP:
|
||||||
|
switch {
|
||||||
|
case shiftPressed && ctrlPressed:
|
||||||
|
return KeyCtrlShiftUp
|
||||||
|
case shiftPressed:
|
||||||
|
return KeyShiftUp
|
||||||
|
case ctrlPressed:
|
||||||
|
return KeyCtrlUp
|
||||||
|
default:
|
||||||
|
return KeyUp
|
||||||
|
}
|
||||||
|
case coninput.VK_DOWN:
|
||||||
|
switch {
|
||||||
|
case shiftPressed && ctrlPressed:
|
||||||
|
return KeyCtrlShiftDown
|
||||||
|
case shiftPressed:
|
||||||
|
return KeyShiftDown
|
||||||
|
case ctrlPressed:
|
||||||
|
return KeyCtrlDown
|
||||||
|
default:
|
||||||
|
return KeyDown
|
||||||
|
}
|
||||||
|
case coninput.VK_RIGHT:
|
||||||
|
switch {
|
||||||
|
case shiftPressed && ctrlPressed:
|
||||||
|
return KeyCtrlShiftRight
|
||||||
|
case shiftPressed:
|
||||||
|
return KeyShiftRight
|
||||||
|
case ctrlPressed:
|
||||||
|
return KeyCtrlRight
|
||||||
|
default:
|
||||||
|
return KeyRight
|
||||||
|
}
|
||||||
|
case coninput.VK_LEFT:
|
||||||
|
switch {
|
||||||
|
case shiftPressed && ctrlPressed:
|
||||||
|
return KeyCtrlShiftLeft
|
||||||
|
case shiftPressed:
|
||||||
|
return KeyShiftLeft
|
||||||
|
case ctrlPressed:
|
||||||
|
return KeyCtrlLeft
|
||||||
|
default:
|
||||||
|
return KeyLeft
|
||||||
|
}
|
||||||
|
case coninput.VK_HOME:
|
||||||
|
switch {
|
||||||
|
case shiftPressed && ctrlPressed:
|
||||||
|
return KeyCtrlShiftHome
|
||||||
|
case shiftPressed:
|
||||||
|
return KeyShiftHome
|
||||||
|
case ctrlPressed:
|
||||||
|
return KeyCtrlHome
|
||||||
|
default:
|
||||||
|
return KeyHome
|
||||||
|
}
|
||||||
|
case coninput.VK_END:
|
||||||
|
switch {
|
||||||
|
case shiftPressed && ctrlPressed:
|
||||||
|
return KeyCtrlShiftEnd
|
||||||
|
case shiftPressed:
|
||||||
|
return KeyShiftEnd
|
||||||
|
case ctrlPressed:
|
||||||
|
return KeyCtrlEnd
|
||||||
|
default:
|
||||||
|
return KeyEnd
|
||||||
|
}
|
||||||
|
case coninput.VK_PRIOR:
|
||||||
|
return KeyPgUp
|
||||||
|
case coninput.VK_NEXT:
|
||||||
|
return KeyPgDown
|
||||||
|
case coninput.VK_DELETE:
|
||||||
|
return KeyDelete
|
||||||
|
case coninput.VK_F1:
|
||||||
|
return KeyF1
|
||||||
|
case coninput.VK_F2:
|
||||||
|
return KeyF2
|
||||||
|
case coninput.VK_F3:
|
||||||
|
return KeyF3
|
||||||
|
case coninput.VK_F4:
|
||||||
|
return KeyF4
|
||||||
|
case coninput.VK_F5:
|
||||||
|
return KeyF5
|
||||||
|
case coninput.VK_F6:
|
||||||
|
return KeyF6
|
||||||
|
case coninput.VK_F7:
|
||||||
|
return KeyF7
|
||||||
|
case coninput.VK_F8:
|
||||||
|
return KeyF8
|
||||||
|
case coninput.VK_F9:
|
||||||
|
return KeyF9
|
||||||
|
case coninput.VK_F10:
|
||||||
|
return KeyF10
|
||||||
|
case coninput.VK_F11:
|
||||||
|
return KeyF11
|
||||||
|
case coninput.VK_F12:
|
||||||
|
return KeyF12
|
||||||
|
case coninput.VK_F13:
|
||||||
|
return KeyF13
|
||||||
|
case coninput.VK_F14:
|
||||||
|
return KeyF14
|
||||||
|
case coninput.VK_F15:
|
||||||
|
return KeyF15
|
||||||
|
case coninput.VK_F16:
|
||||||
|
return KeyF16
|
||||||
|
case coninput.VK_F17:
|
||||||
|
return KeyF17
|
||||||
|
case coninput.VK_F18:
|
||||||
|
return KeyF18
|
||||||
|
case coninput.VK_F19:
|
||||||
|
return KeyF19
|
||||||
|
case coninput.VK_F20:
|
||||||
|
return KeyF20
|
||||||
|
default:
|
||||||
|
switch {
|
||||||
|
case e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED) && e.ControlKeyState.Contains(coninput.RIGHT_ALT_PRESSED):
|
||||||
|
// AltGr is pressed, then it's a rune.
|
||||||
|
fallthrough
|
||||||
|
case !e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED) && !e.ControlKeyState.Contains(coninput.RIGHT_CTRL_PRESSED):
|
||||||
|
return KeyRunes
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Char {
|
||||||
|
case '@':
|
||||||
|
return KeyCtrlAt
|
||||||
|
case '\x01':
|
||||||
|
return KeyCtrlA
|
||||||
|
case '\x02':
|
||||||
|
return KeyCtrlB
|
||||||
|
case '\x03':
|
||||||
|
return KeyCtrlC
|
||||||
|
case '\x04':
|
||||||
|
return KeyCtrlD
|
||||||
|
case '\x05':
|
||||||
|
return KeyCtrlE
|
||||||
|
case '\x06':
|
||||||
|
return KeyCtrlF
|
||||||
|
case '\a':
|
||||||
|
return KeyCtrlG
|
||||||
|
case '\b':
|
||||||
|
return KeyCtrlH
|
||||||
|
case '\t':
|
||||||
|
return KeyCtrlI
|
||||||
|
case '\n':
|
||||||
|
return KeyCtrlJ
|
||||||
|
case '\v':
|
||||||
|
return KeyCtrlK
|
||||||
|
case '\f':
|
||||||
|
return KeyCtrlL
|
||||||
|
case '\r':
|
||||||
|
return KeyCtrlM
|
||||||
|
case '\x0e':
|
||||||
|
return KeyCtrlN
|
||||||
|
case '\x0f':
|
||||||
|
return KeyCtrlO
|
||||||
|
case '\x10':
|
||||||
|
return KeyCtrlP
|
||||||
|
case '\x11':
|
||||||
|
return KeyCtrlQ
|
||||||
|
case '\x12':
|
||||||
|
return KeyCtrlR
|
||||||
|
case '\x13':
|
||||||
|
return KeyCtrlS
|
||||||
|
case '\x14':
|
||||||
|
return KeyCtrlT
|
||||||
|
case '\x15':
|
||||||
|
return KeyCtrlU
|
||||||
|
case '\x16':
|
||||||
|
return KeyCtrlV
|
||||||
|
case '\x17':
|
||||||
|
return KeyCtrlW
|
||||||
|
case '\x18':
|
||||||
|
return KeyCtrlX
|
||||||
|
case '\x19':
|
||||||
|
return KeyCtrlY
|
||||||
|
case '\x1a':
|
||||||
|
return KeyCtrlZ
|
||||||
|
case '\x1b':
|
||||||
|
return KeyCtrlOpenBracket // KeyEscape
|
||||||
|
case '\x1c':
|
||||||
|
return KeyCtrlBackslash
|
||||||
|
case '\x1f':
|
||||||
|
return KeyCtrlUnderscore
|
||||||
|
}
|
||||||
|
|
||||||
|
switch code { //nolint:exhaustive
|
||||||
|
case coninput.VK_OEM_4:
|
||||||
|
return KeyCtrlOpenBracket
|
||||||
|
case coninput.VK_OEM_6:
|
||||||
|
return KeyCtrlCloseBracket
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyRunes
|
||||||
|
}
|
||||||
|
}
|
||||||
53
vendor/github.com/charmbracelet/bubbletea/logging.go
generated
vendored
Normal file
53
vendor/github.com/charmbracelet/bubbletea/logging.go
generated
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogToFile sets up default logging to log to a file. This is helpful as we
|
||||||
|
// can't print to the terminal since our TUI is occupying it. If the file
|
||||||
|
// doesn't exist it will be created.
|
||||||
|
//
|
||||||
|
// Don't forget to close the file when you're done with it.
|
||||||
|
//
|
||||||
|
// f, err := LogToFile("debug.log", "debug")
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println("fatal:", err)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
// defer f.Close()
|
||||||
|
func LogToFile(path string, prefix string) (*os.File, error) {
|
||||||
|
return LogToFileWith(path, prefix, log.Default())
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogOptionsSetter is an interface implemented by stdlib's log and charm's log
|
||||||
|
// libraries.
|
||||||
|
type LogOptionsSetter interface {
|
||||||
|
SetOutput(io.Writer)
|
||||||
|
SetPrefix(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter.
|
||||||
|
func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) {
|
||||||
|
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:mnd
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error opening file for logging: %w", err)
|
||||||
|
}
|
||||||
|
log.SetOutput(f)
|
||||||
|
|
||||||
|
// Add a space after the prefix if a prefix is being specified and it
|
||||||
|
// doesn't already have a trailing space.
|
||||||
|
if len(prefix) > 0 {
|
||||||
|
finalChar := prefix[len(prefix)-1]
|
||||||
|
if !unicode.IsSpace(rune(finalChar)) {
|
||||||
|
prefix += " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.SetPrefix(prefix)
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
308
vendor/github.com/charmbracelet/bubbletea/mouse.go
generated
vendored
Normal file
308
vendor/github.com/charmbracelet/bubbletea/mouse.go
generated
vendored
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
// MouseMsg contains information about a mouse event and are sent to a programs
|
||||||
|
// update function when mouse activity occurs. Note that the mouse must first
|
||||||
|
// be enabled in order for the mouse events to be received.
|
||||||
|
type MouseMsg MouseEvent
|
||||||
|
|
||||||
|
// String returns a string representation of a mouse event.
|
||||||
|
func (m MouseMsg) String() string {
|
||||||
|
return MouseEvent(m).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseEvent represents a mouse event, which could be a click, a scroll wheel
|
||||||
|
// movement, a cursor movement, or a combination.
|
||||||
|
type MouseEvent struct {
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
Shift bool
|
||||||
|
Alt bool
|
||||||
|
Ctrl bool
|
||||||
|
Action MouseAction
|
||||||
|
Button MouseButton
|
||||||
|
|
||||||
|
// Deprecated: Use MouseAction & MouseButton instead.
|
||||||
|
Type MouseEventType
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWheel returns true if the mouse event is a wheel event.
|
||||||
|
func (m MouseEvent) IsWheel() bool {
|
||||||
|
return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown ||
|
||||||
|
m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of a mouse event.
|
||||||
|
func (m MouseEvent) String() (s string) {
|
||||||
|
if m.Ctrl {
|
||||||
|
s += "ctrl+"
|
||||||
|
}
|
||||||
|
if m.Alt {
|
||||||
|
s += "alt+"
|
||||||
|
}
|
||||||
|
if m.Shift {
|
||||||
|
s += "shift+"
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Button == MouseButtonNone { //nolint:nestif
|
||||||
|
if m.Action == MouseActionMotion || m.Action == MouseActionRelease {
|
||||||
|
s += mouseActions[m.Action]
|
||||||
|
} else {
|
||||||
|
s += "unknown"
|
||||||
|
}
|
||||||
|
} else if m.IsWheel() {
|
||||||
|
s += mouseButtons[m.Button]
|
||||||
|
} else {
|
||||||
|
btn := mouseButtons[m.Button]
|
||||||
|
if btn != "" {
|
||||||
|
s += btn
|
||||||
|
}
|
||||||
|
act := mouseActions[m.Action]
|
||||||
|
if act != "" {
|
||||||
|
s += " " + act
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseAction represents the action that occurred during a mouse event.
|
||||||
|
type MouseAction int
|
||||||
|
|
||||||
|
// Mouse event actions.
|
||||||
|
const (
|
||||||
|
MouseActionPress MouseAction = iota
|
||||||
|
MouseActionRelease
|
||||||
|
MouseActionMotion
|
||||||
|
)
|
||||||
|
|
||||||
|
var mouseActions = map[MouseAction]string{
|
||||||
|
MouseActionPress: "press",
|
||||||
|
MouseActionRelease: "release",
|
||||||
|
MouseActionMotion: "motion",
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseButton represents the button that was pressed during a mouse event.
|
||||||
|
type MouseButton int
|
||||||
|
|
||||||
|
// Mouse event buttons
|
||||||
|
//
|
||||||
|
// This is based on X11 mouse button codes.
|
||||||
|
//
|
||||||
|
// 1 = left button
|
||||||
|
// 2 = middle button (pressing the scroll wheel)
|
||||||
|
// 3 = right button
|
||||||
|
// 4 = turn scroll wheel up
|
||||||
|
// 5 = turn scroll wheel down
|
||||||
|
// 6 = push scroll wheel left
|
||||||
|
// 7 = push scroll wheel right
|
||||||
|
// 8 = 4th button (aka browser backward button)
|
||||||
|
// 9 = 5th button (aka browser forward button)
|
||||||
|
// 10
|
||||||
|
// 11
|
||||||
|
//
|
||||||
|
// Other buttons are not supported.
|
||||||
|
const (
|
||||||
|
MouseButtonNone MouseButton = iota
|
||||||
|
MouseButtonLeft
|
||||||
|
MouseButtonMiddle
|
||||||
|
MouseButtonRight
|
||||||
|
MouseButtonWheelUp
|
||||||
|
MouseButtonWheelDown
|
||||||
|
MouseButtonWheelLeft
|
||||||
|
MouseButtonWheelRight
|
||||||
|
MouseButtonBackward
|
||||||
|
MouseButtonForward
|
||||||
|
MouseButton10
|
||||||
|
MouseButton11
|
||||||
|
)
|
||||||
|
|
||||||
|
var mouseButtons = map[MouseButton]string{
|
||||||
|
MouseButtonNone: "none",
|
||||||
|
MouseButtonLeft: "left",
|
||||||
|
MouseButtonMiddle: "middle",
|
||||||
|
MouseButtonRight: "right",
|
||||||
|
MouseButtonWheelUp: "wheel up",
|
||||||
|
MouseButtonWheelDown: "wheel down",
|
||||||
|
MouseButtonWheelLeft: "wheel left",
|
||||||
|
MouseButtonWheelRight: "wheel right",
|
||||||
|
MouseButtonBackward: "backward",
|
||||||
|
MouseButtonForward: "forward",
|
||||||
|
MouseButton10: "button 10",
|
||||||
|
MouseButton11: "button 11",
|
||||||
|
}
|
||||||
|
|
||||||
|
// MouseEventType indicates the type of mouse event occurring.
|
||||||
|
//
|
||||||
|
// Deprecated: Use MouseAction & MouseButton instead.
|
||||||
|
type MouseEventType int
|
||||||
|
|
||||||
|
// Mouse event types.
|
||||||
|
//
|
||||||
|
// Deprecated: Use MouseAction & MouseButton instead.
|
||||||
|
const (
|
||||||
|
MouseUnknown MouseEventType = iota
|
||||||
|
MouseLeft
|
||||||
|
MouseRight
|
||||||
|
MouseMiddle
|
||||||
|
MouseRelease // mouse button release (X10 only)
|
||||||
|
MouseWheelUp
|
||||||
|
MouseWheelDown
|
||||||
|
MouseWheelLeft
|
||||||
|
MouseWheelRight
|
||||||
|
MouseBackward
|
||||||
|
MouseForward
|
||||||
|
MouseMotion
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events
|
||||||
|
// look like:
|
||||||
|
//
|
||||||
|
// ESC [ < Cb ; Cx ; Cy (M or m)
|
||||||
|
//
|
||||||
|
// where:
|
||||||
|
//
|
||||||
|
// Cb is the encoded button code
|
||||||
|
// Cx is the x-coordinate of the mouse
|
||||||
|
// Cy is the y-coordinate of the mouse
|
||||||
|
// M is for button press, m is for button release
|
||||||
|
//
|
||||||
|
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
|
||||||
|
func parseSGRMouseEvent(buf []byte) MouseEvent {
|
||||||
|
str := string(buf[3:])
|
||||||
|
matches := mouseSGRRegex.FindStringSubmatch(str)
|
||||||
|
if len(matches) != 5 { //nolint:mnd
|
||||||
|
// Unreachable, we already checked the regex in `detectOneMsg`.
|
||||||
|
panic("invalid mouse event")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, _ := strconv.Atoi(matches[1])
|
||||||
|
px := matches[2]
|
||||||
|
py := matches[3]
|
||||||
|
release := matches[4] == "m"
|
||||||
|
m := parseMouseButton(b, true)
|
||||||
|
|
||||||
|
// Wheel buttons don't have release events
|
||||||
|
// Motion can be reported as a release event in some terminals (Windows Terminal)
|
||||||
|
if m.Action != MouseActionMotion && !m.IsWheel() && release {
|
||||||
|
m.Action = MouseActionRelease
|
||||||
|
m.Type = MouseRelease
|
||||||
|
}
|
||||||
|
|
||||||
|
x, _ := strconv.Atoi(px)
|
||||||
|
y, _ := strconv.Atoi(py)
|
||||||
|
|
||||||
|
// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
|
||||||
|
m.X = x - 1
|
||||||
|
m.Y = y - 1
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
const x10MouseByteOffset = 32
|
||||||
|
|
||||||
|
// Parse X10-encoded mouse events; the simplest kind. The last release of X10
|
||||||
|
// was December 1986, by the way. The original X10 mouse protocol limits the Cx
|
||||||
|
// and Cy coordinates to 223 (=255-032).
|
||||||
|
//
|
||||||
|
// X10 mouse events look like:
|
||||||
|
//
|
||||||
|
// ESC [M Cb Cx Cy
|
||||||
|
//
|
||||||
|
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
|
||||||
|
func parseX10MouseEvent(buf []byte) MouseEvent {
|
||||||
|
v := buf[3:6]
|
||||||
|
m := parseMouseButton(int(v[0]), false)
|
||||||
|
|
||||||
|
// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
|
||||||
|
m.X = int(v[1]) - x10MouseByteOffset - 1
|
||||||
|
m.Y = int(v[2]) - x10MouseByteOffset - 1
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
|
||||||
|
func parseMouseButton(b int, isSGR bool) MouseEvent {
|
||||||
|
var m MouseEvent
|
||||||
|
e := b
|
||||||
|
if !isSGR {
|
||||||
|
e -= x10MouseByteOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
bitShift = 0b0000_0100
|
||||||
|
bitAlt = 0b0000_1000
|
||||||
|
bitCtrl = 0b0001_0000
|
||||||
|
bitMotion = 0b0010_0000
|
||||||
|
bitWheel = 0b0100_0000
|
||||||
|
bitAdd = 0b1000_0000 // additional buttons 8-11
|
||||||
|
|
||||||
|
bitsMask = 0b0000_0011
|
||||||
|
)
|
||||||
|
|
||||||
|
if e&bitAdd != 0 {
|
||||||
|
m.Button = MouseButtonBackward + MouseButton(e&bitsMask)
|
||||||
|
} else if e&bitWheel != 0 {
|
||||||
|
m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask)
|
||||||
|
} else {
|
||||||
|
m.Button = MouseButtonLeft + MouseButton(e&bitsMask)
|
||||||
|
// X10 reports a button release as 0b0000_0011 (3)
|
||||||
|
if e&bitsMask == bitsMask {
|
||||||
|
m.Action = MouseActionRelease
|
||||||
|
m.Button = MouseButtonNone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Motion bit doesn't get reported for wheel events.
|
||||||
|
if e&bitMotion != 0 && !m.IsWheel() {
|
||||||
|
m.Action = MouseActionMotion
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifiers
|
||||||
|
m.Alt = e&bitAlt != 0
|
||||||
|
m.Ctrl = e&bitCtrl != 0
|
||||||
|
m.Shift = e&bitShift != 0
|
||||||
|
|
||||||
|
// backward compatibility
|
||||||
|
switch {
|
||||||
|
case m.Button == MouseButtonLeft && m.Action == MouseActionPress:
|
||||||
|
m.Type = MouseLeft
|
||||||
|
case m.Button == MouseButtonMiddle && m.Action == MouseActionPress:
|
||||||
|
m.Type = MouseMiddle
|
||||||
|
case m.Button == MouseButtonRight && m.Action == MouseActionPress:
|
||||||
|
m.Type = MouseRight
|
||||||
|
case m.Button == MouseButtonNone && m.Action == MouseActionRelease:
|
||||||
|
m.Type = MouseRelease
|
||||||
|
case m.Button == MouseButtonWheelUp && m.Action == MouseActionPress:
|
||||||
|
m.Type = MouseWheelUp
|
||||||
|
case m.Button == MouseButtonWheelDown && m.Action == MouseActionPress:
|
||||||
|
m.Type = MouseWheelDown
|
||||||
|
case m.Button == MouseButtonWheelLeft && m.Action == MouseActionPress:
|
||||||
|
m.Type = MouseWheelLeft
|
||||||
|
case m.Button == MouseButtonWheelRight && m.Action == MouseActionPress:
|
||||||
|
m.Type = MouseWheelRight
|
||||||
|
case m.Button == MouseButtonBackward && m.Action == MouseActionPress:
|
||||||
|
m.Type = MouseBackward
|
||||||
|
case m.Button == MouseButtonForward && m.Action == MouseActionPress:
|
||||||
|
m.Type = MouseForward
|
||||||
|
case m.Action == MouseActionMotion:
|
||||||
|
m.Type = MouseMotion
|
||||||
|
switch m.Button { //nolint:exhaustive
|
||||||
|
case MouseButtonLeft:
|
||||||
|
m.Type = MouseLeft
|
||||||
|
case MouseButtonMiddle:
|
||||||
|
m.Type = MouseMiddle
|
||||||
|
case MouseButtonRight:
|
||||||
|
m.Type = MouseRight
|
||||||
|
case MouseButtonBackward:
|
||||||
|
m.Type = MouseBackward
|
||||||
|
case MouseButtonForward:
|
||||||
|
m.Type = MouseForward
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
m.Type = MouseUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
29
vendor/github.com/charmbracelet/bubbletea/nil_renderer.go
generated
vendored
Normal file
29
vendor/github.com/charmbracelet/bubbletea/nil_renderer.go
generated
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
type nilRenderer struct{}
|
||||||
|
|
||||||
|
func (n nilRenderer) start() {}
|
||||||
|
func (n nilRenderer) stop() {}
|
||||||
|
func (n nilRenderer) kill() {}
|
||||||
|
func (n nilRenderer) write(_ string) {}
|
||||||
|
func (n nilRenderer) repaint() {}
|
||||||
|
func (n nilRenderer) clearScreen() {}
|
||||||
|
func (n nilRenderer) altScreen() bool { return false }
|
||||||
|
func (n nilRenderer) enterAltScreen() {}
|
||||||
|
func (n nilRenderer) exitAltScreen() {}
|
||||||
|
func (n nilRenderer) showCursor() {}
|
||||||
|
func (n nilRenderer) hideCursor() {}
|
||||||
|
func (n nilRenderer) enableMouseCellMotion() {}
|
||||||
|
func (n nilRenderer) disableMouseCellMotion() {}
|
||||||
|
func (n nilRenderer) enableMouseAllMotion() {}
|
||||||
|
func (n nilRenderer) disableMouseAllMotion() {}
|
||||||
|
func (n nilRenderer) enableBracketedPaste() {}
|
||||||
|
func (n nilRenderer) disableBracketedPaste() {}
|
||||||
|
func (n nilRenderer) enableMouseSGRMode() {}
|
||||||
|
func (n nilRenderer) disableMouseSGRMode() {}
|
||||||
|
func (n nilRenderer) bracketedPasteActive() bool { return false }
|
||||||
|
func (n nilRenderer) setWindowTitle(_ string) {}
|
||||||
|
func (n nilRenderer) reportFocus() bool { return false }
|
||||||
|
func (n nilRenderer) enableReportFocus() {}
|
||||||
|
func (n nilRenderer) disableReportFocus() {}
|
||||||
|
func (n nilRenderer) resetLinesRendered() {}
|
||||||
252
vendor/github.com/charmbracelet/bubbletea/options.go
generated
vendored
Normal file
252
vendor/github.com/charmbracelet/bubbletea/options.go
generated
vendored
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProgramOption is used to set options when initializing a Program. Program can
|
||||||
|
// accept a variable number of options.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// p := NewProgram(model, WithInput(someInput), WithOutput(someOutput))
|
||||||
|
type ProgramOption func(*Program)
|
||||||
|
|
||||||
|
// WithContext lets you specify a context in which to run the Program. This is
|
||||||
|
// useful if you want to cancel the execution from outside. When a Program gets
|
||||||
|
// cancelled it will exit with an error ErrProgramKilled.
|
||||||
|
func WithContext(ctx context.Context) ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.externalCtx = ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOutput sets the output which, by default, is stdout. In most cases you
|
||||||
|
// won't need to use this.
|
||||||
|
func WithOutput(output io.Writer) ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.output = output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInput sets the input which, by default, is stdin. In most cases you
|
||||||
|
// won't need to use this. To disable input entirely pass nil.
|
||||||
|
//
|
||||||
|
// p := NewProgram(model, WithInput(nil))
|
||||||
|
func WithInput(input io.Reader) ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.input = input
|
||||||
|
p.inputType = customInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithInputTTY opens a new TTY for input (or console input device on Windows).
|
||||||
|
func WithInputTTY() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.inputType = ttyInput
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithEnvironment sets the environment variables that the program will use.
|
||||||
|
// This useful when the program is running in a remote session (e.g. SSH) and
|
||||||
|
// you want to pass the environment variables from the remote session to the
|
||||||
|
// program.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// var sess ssh.Session // ssh.Session is a type from the github.com/charmbracelet/ssh package
|
||||||
|
// pty, _, _ := sess.Pty()
|
||||||
|
// environ := append(sess.Environ(), "TERM="+pty.Term)
|
||||||
|
// p := tea.NewProgram(model, tea.WithEnvironment(environ)
|
||||||
|
func WithEnvironment(env []string) ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.environ = env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithoutSignalHandler disables the signal handler that Bubble Tea sets up for
|
||||||
|
// Programs. This is useful if you want to handle signals yourself.
|
||||||
|
func WithoutSignalHandler() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.startupOptions |= withoutSignalHandler
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithoutCatchPanics disables the panic catching that Bubble Tea does by
|
||||||
|
// default. If panic catching is disabled the terminal will be in a fairly
|
||||||
|
// unusable state after a panic because Bubble Tea will not perform its usual
|
||||||
|
// cleanup on exit.
|
||||||
|
func WithoutCatchPanics() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.startupOptions |= withoutCatchPanics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithoutSignals will ignore OS signals.
|
||||||
|
// This is mainly useful for testing.
|
||||||
|
func WithoutSignals() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
atomic.StoreUint32(&p.ignoreSignals, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAltScreen starts the program with the alternate screen buffer enabled
|
||||||
|
// (i.e. the program starts in full window mode). Note that the altscreen will
|
||||||
|
// be automatically exited when the program quits.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// p := tea.NewProgram(Model{}, tea.WithAltScreen())
|
||||||
|
// if _, err := p.Run(); err != nil {
|
||||||
|
// fmt.Println("Error running program:", err)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// To enter the altscreen once the program has already started running use the
|
||||||
|
// EnterAltScreen command.
|
||||||
|
func WithAltScreen() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.startupOptions |= withAltScreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithoutBracketedPaste starts the program with bracketed paste disabled.
|
||||||
|
func WithoutBracketedPaste() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.startupOptions |= withoutBracketedPaste
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMouseCellMotion starts the program with the mouse enabled in "cell
|
||||||
|
// motion" mode.
|
||||||
|
//
|
||||||
|
// Cell motion mode enables mouse click, release, and wheel events. Mouse
|
||||||
|
// movement events are also captured if a mouse button is pressed (i.e., drag
|
||||||
|
// events). Cell motion mode is better supported than all motion mode.
|
||||||
|
//
|
||||||
|
// This will try to enable the mouse in extended mode (SGR), if that is not
|
||||||
|
// supported by the terminal it will fall back to normal mode (X10).
|
||||||
|
//
|
||||||
|
// To enable mouse cell motion once the program has already started running use
|
||||||
|
// the EnableMouseCellMotion command. To disable the mouse when the program is
|
||||||
|
// running use the DisableMouse command.
|
||||||
|
//
|
||||||
|
// The mouse will be automatically disabled when the program exits.
|
||||||
|
func WithMouseCellMotion() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.startupOptions |= withMouseCellMotion // set
|
||||||
|
p.startupOptions &^= withMouseAllMotion // clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMouseAllMotion starts the program with the mouse enabled in "all motion"
|
||||||
|
// mode.
|
||||||
|
//
|
||||||
|
// EnableMouseAllMotion is a special command that enables mouse click, release,
|
||||||
|
// wheel, and motion events, which are delivered regardless of whether a mouse
|
||||||
|
// button is pressed, effectively enabling support for hover interactions.
|
||||||
|
//
|
||||||
|
// This will try to enable the mouse in extended mode (SGR), if that is not
|
||||||
|
// supported by the terminal it will fall back to normal mode (X10).
|
||||||
|
//
|
||||||
|
// Many modern terminals support this, but not all. If in doubt, use
|
||||||
|
// EnableMouseCellMotion instead.
|
||||||
|
//
|
||||||
|
// To enable the mouse once the program has already started running use the
|
||||||
|
// EnableMouseAllMotion command. To disable the mouse when the program is
|
||||||
|
// running use the DisableMouse command.
|
||||||
|
//
|
||||||
|
// The mouse will be automatically disabled when the program exits.
|
||||||
|
func WithMouseAllMotion() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.startupOptions |= withMouseAllMotion // set
|
||||||
|
p.startupOptions &^= withMouseCellMotion // clear
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithoutRenderer disables the renderer. When this is set output and log
|
||||||
|
// statements will be plainly sent to stdout (or another output if one is set)
|
||||||
|
// without any rendering and redrawing logic. In other words, printing and
|
||||||
|
// logging will behave the same way it would in a non-TUI commandline tool.
|
||||||
|
// This can be useful if you want to use the Bubble Tea framework for a non-TUI
|
||||||
|
// application, or to provide an additional non-TUI mode to your Bubble Tea
|
||||||
|
// programs. For example, your program could behave like a daemon if output is
|
||||||
|
// not a TTY.
|
||||||
|
func WithoutRenderer() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.renderer = &nilRenderer{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithANSICompressor removes redundant ANSI sequences to produce potentially
|
||||||
|
// smaller output, at the cost of some processing overhead.
|
||||||
|
//
|
||||||
|
// This feature is provisional, and may be changed or removed in a future version
|
||||||
|
// of this package.
|
||||||
|
//
|
||||||
|
// Deprecated: this incurs a noticeable performance hit. A future release will
|
||||||
|
// optimize ANSI automatically without the performance penalty.
|
||||||
|
func WithANSICompressor() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.startupOptions |= withANSICompressor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFilter supplies an event filter that will be invoked before Bubble Tea
|
||||||
|
// processes a tea.Msg. The event filter can return any tea.Msg which will then
|
||||||
|
// get handled by Bubble Tea instead of the original event. If the event filter
|
||||||
|
// returns nil, the event will be ignored and Bubble Tea will not process it.
|
||||||
|
//
|
||||||
|
// As an example, this could be used to prevent a program from shutting down if
|
||||||
|
// there are unsaved changes.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// func filter(m tea.Model, msg tea.Msg) tea.Msg {
|
||||||
|
// if _, ok := msg.(tea.QuitMsg); !ok {
|
||||||
|
// return msg
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// model := m.(myModel)
|
||||||
|
// if model.hasChanges {
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return msg
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// p := tea.NewProgram(Model{}, tea.WithFilter(filter));
|
||||||
|
//
|
||||||
|
// if _,err := p.Run(); err != nil {
|
||||||
|
// fmt.Println("Error running program:", err)
|
||||||
|
// os.Exit(1)
|
||||||
|
// }
|
||||||
|
func WithFilter(filter func(Model, Msg) Msg) ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.filter = filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFPS sets a custom maximum FPS at which the renderer should run. If
|
||||||
|
// less than 1, the default value of 60 will be used. If over 120, the FPS
|
||||||
|
// will be capped at 120.
|
||||||
|
func WithFPS(fps int) ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.fps = fps
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithReportFocus enables reporting when the terminal gains and loses
|
||||||
|
// focus. When this is enabled [FocusMsg] and [BlurMsg] messages will be sent
|
||||||
|
// to your Update method.
|
||||||
|
//
|
||||||
|
// Note that while most terminals and multiplexers support focus reporting,
|
||||||
|
// some do not. Also note that tmux needs to be configured to report focus
|
||||||
|
// events.
|
||||||
|
func WithReportFocus() ProgramOption {
|
||||||
|
return func(p *Program) {
|
||||||
|
p.startupOptions |= withReportFocus
|
||||||
|
}
|
||||||
|
}
|
||||||
88
vendor/github.com/charmbracelet/bubbletea/renderer.go
generated
vendored
Normal file
88
vendor/github.com/charmbracelet/bubbletea/renderer.go
generated
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
// renderer is the interface for Bubble Tea renderers.
|
||||||
|
type renderer interface {
|
||||||
|
// Start the renderer.
|
||||||
|
start()
|
||||||
|
|
||||||
|
// Stop the renderer, but render the final frame in the buffer, if any.
|
||||||
|
stop()
|
||||||
|
|
||||||
|
// Stop the renderer without doing any final rendering.
|
||||||
|
kill()
|
||||||
|
|
||||||
|
// Write a frame to the renderer. The renderer can write this data to
|
||||||
|
// output at its discretion.
|
||||||
|
write(string)
|
||||||
|
|
||||||
|
// Request a full re-render. Note that this will not trigger a render
|
||||||
|
// immediately. Rather, this method causes the next render to be a full
|
||||||
|
// repaint. Because of this, it's safe to call this method multiple times
|
||||||
|
// in succession.
|
||||||
|
repaint()
|
||||||
|
|
||||||
|
// Clears the terminal.
|
||||||
|
clearScreen()
|
||||||
|
|
||||||
|
// Whether or not the alternate screen buffer is enabled.
|
||||||
|
altScreen() bool
|
||||||
|
// Enable the alternate screen buffer.
|
||||||
|
enterAltScreen()
|
||||||
|
// Disable the alternate screen buffer.
|
||||||
|
exitAltScreen()
|
||||||
|
|
||||||
|
// Show the cursor.
|
||||||
|
showCursor()
|
||||||
|
// Hide the cursor.
|
||||||
|
hideCursor()
|
||||||
|
|
||||||
|
// enableMouseCellMotion enables mouse click, release, wheel and motion
|
||||||
|
// events if a mouse button is pressed (i.e., drag events).
|
||||||
|
enableMouseCellMotion()
|
||||||
|
|
||||||
|
// disableMouseCellMotion disables Mouse Cell Motion tracking.
|
||||||
|
disableMouseCellMotion()
|
||||||
|
|
||||||
|
// enableMouseAllMotion enables mouse click, release, wheel and motion
|
||||||
|
// events, regardless of whether a mouse button is pressed. Many modern
|
||||||
|
// terminals support this, but not all.
|
||||||
|
enableMouseAllMotion()
|
||||||
|
|
||||||
|
// disableMouseAllMotion disables All Motion mouse tracking.
|
||||||
|
disableMouseAllMotion()
|
||||||
|
|
||||||
|
// enableMouseSGRMode enables mouse extended mode (SGR).
|
||||||
|
enableMouseSGRMode()
|
||||||
|
|
||||||
|
// disableMouseSGRMode disables mouse extended mode (SGR).
|
||||||
|
disableMouseSGRMode()
|
||||||
|
|
||||||
|
// enableBracketedPaste enables bracketed paste, where characters
|
||||||
|
// inside the input are not interpreted when pasted as a whole.
|
||||||
|
enableBracketedPaste()
|
||||||
|
|
||||||
|
// disableBracketedPaste disables bracketed paste.
|
||||||
|
disableBracketedPaste()
|
||||||
|
|
||||||
|
// bracketedPasteActive reports whether bracketed paste mode is
|
||||||
|
// currently enabled.
|
||||||
|
bracketedPasteActive() bool
|
||||||
|
|
||||||
|
// setWindowTitle sets the terminal window title.
|
||||||
|
setWindowTitle(string)
|
||||||
|
|
||||||
|
// reportFocus returns whether reporting focus events is enabled.
|
||||||
|
reportFocus() bool
|
||||||
|
|
||||||
|
// enableReportFocus reports focus events to the program.
|
||||||
|
enableReportFocus()
|
||||||
|
|
||||||
|
// disableReportFocus stops reporting focus events to the program.
|
||||||
|
disableReportFocus()
|
||||||
|
|
||||||
|
// resetLinesRendered ensures exec output remains on screen on exit
|
||||||
|
resetLinesRendered()
|
||||||
|
}
|
||||||
|
|
||||||
|
// repaintMsg forces a full repaint.
|
||||||
|
type repaintMsg struct{}
|
||||||
248
vendor/github.com/charmbracelet/bubbletea/screen.go
generated
vendored
Normal file
248
vendor/github.com/charmbracelet/bubbletea/screen.go
generated
vendored
Normal file
|
|
@ -0,0 +1,248 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
// WindowSizeMsg is used to report the terminal size. It's sent to Update once
|
||||||
|
// initially and then on every terminal resize. Note that Windows does not
|
||||||
|
// have support for reporting when resizes occur as it does not support the
|
||||||
|
// SIGWINCH signal.
|
||||||
|
type WindowSizeMsg struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearScreen is a special command that tells the program to clear the screen
|
||||||
|
// before the next update. This can be used to move the cursor to the top left
|
||||||
|
// of the screen and clear visual clutter when the alt screen is not in use.
|
||||||
|
//
|
||||||
|
// Note that it should never be necessary to call ClearScreen() for regular
|
||||||
|
// redraws.
|
||||||
|
func ClearScreen() Msg {
|
||||||
|
return clearScreenMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearScreenMsg is an internal message that signals to clear the screen.
|
||||||
|
// You can send a clearScreenMsg with ClearScreen.
|
||||||
|
type clearScreenMsg struct{}
|
||||||
|
|
||||||
|
// EnterAltScreen is a special command that tells the Bubble Tea program to
|
||||||
|
// enter the alternate screen buffer.
|
||||||
|
//
|
||||||
|
// Because commands run asynchronously, this command should not be used in your
|
||||||
|
// model's Init function. To initialize your program with the altscreen enabled
|
||||||
|
// use the WithAltScreen ProgramOption instead.
|
||||||
|
func EnterAltScreen() Msg {
|
||||||
|
return enterAltScreenMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enterAltScreenMsg in an internal message signals that the program should
|
||||||
|
// enter alternate screen buffer. You can send a enterAltScreenMsg with
|
||||||
|
// EnterAltScreen.
|
||||||
|
type enterAltScreenMsg struct{}
|
||||||
|
|
||||||
|
// ExitAltScreen is a special command that tells the Bubble Tea program to exit
|
||||||
|
// the alternate screen buffer. This command should be used to exit the
|
||||||
|
// alternate screen buffer while the program is running.
|
||||||
|
//
|
||||||
|
// Note that the alternate screen buffer will be automatically exited when the
|
||||||
|
// program quits.
|
||||||
|
func ExitAltScreen() Msg {
|
||||||
|
return exitAltScreenMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// exitAltScreenMsg in an internal message signals that the program should exit
|
||||||
|
// alternate screen buffer. You can send a exitAltScreenMsg with ExitAltScreen.
|
||||||
|
type exitAltScreenMsg struct{}
|
||||||
|
|
||||||
|
// EnableMouseCellMotion is a special command that enables mouse click,
|
||||||
|
// release, and wheel events. Mouse movement events are also captured if
|
||||||
|
// a mouse button is pressed (i.e., drag events).
|
||||||
|
//
|
||||||
|
// Because commands run asynchronously, this command should not be used in your
|
||||||
|
// model's Init function. Use the WithMouseCellMotion ProgramOption instead.
|
||||||
|
func EnableMouseCellMotion() Msg {
|
||||||
|
return enableMouseCellMotionMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableMouseCellMotionMsg is a special command that signals to start
|
||||||
|
// listening for "cell motion" type mouse events (ESC[?1002l). To send an
|
||||||
|
// enableMouseCellMotionMsg, use the EnableMouseCellMotion command.
|
||||||
|
type enableMouseCellMotionMsg struct{}
|
||||||
|
|
||||||
|
// EnableMouseAllMotion is a special command that enables mouse click, release,
|
||||||
|
// wheel, and motion events, which are delivered regardless of whether a mouse
|
||||||
|
// button is pressed, effectively enabling support for hover interactions.
|
||||||
|
//
|
||||||
|
// Many modern terminals support this, but not all. If in doubt, use
|
||||||
|
// EnableMouseCellMotion instead.
|
||||||
|
//
|
||||||
|
// Because commands run asynchronously, this command should not be used in your
|
||||||
|
// model's Init function. Use the WithMouseAllMotion ProgramOption instead.
|
||||||
|
func EnableMouseAllMotion() Msg {
|
||||||
|
return enableMouseAllMotionMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableMouseAllMotionMsg is a special command that signals to start listening
|
||||||
|
// for "all motion" type mouse events (ESC[?1003l). To send an
|
||||||
|
// enableMouseAllMotionMsg, use the EnableMouseAllMotion command.
|
||||||
|
type enableMouseAllMotionMsg struct{}
|
||||||
|
|
||||||
|
// DisableMouse is a special command that stops listening for mouse events.
|
||||||
|
func DisableMouse() Msg {
|
||||||
|
return disableMouseMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// disableMouseMsg is an internal message that signals to stop listening
|
||||||
|
// for mouse events. To send a disableMouseMsg, use the DisableMouse command.
|
||||||
|
type disableMouseMsg struct{}
|
||||||
|
|
||||||
|
// HideCursor is a special command for manually instructing Bubble Tea to hide
|
||||||
|
// the cursor. In some rare cases, certain operations will cause the terminal
|
||||||
|
// to show the cursor, which is normally hidden for the duration of a Bubble
|
||||||
|
// Tea program's lifetime. You will most likely not need to use this command.
|
||||||
|
func HideCursor() Msg {
|
||||||
|
return hideCursorMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hideCursorMsg is an internal command used to hide the cursor. You can send
|
||||||
|
// this message with HideCursor.
|
||||||
|
type hideCursorMsg struct{}
|
||||||
|
|
||||||
|
// ShowCursor is a special command for manually instructing Bubble Tea to show
|
||||||
|
// the cursor.
|
||||||
|
func ShowCursor() Msg {
|
||||||
|
return showCursorMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// showCursorMsg is an internal command used to show the cursor. You can send
|
||||||
|
// this message with ShowCursor.
|
||||||
|
type showCursorMsg struct{}
|
||||||
|
|
||||||
|
// EnableBracketedPaste is a special command that tells the Bubble Tea program
|
||||||
|
// to accept bracketed paste input.
|
||||||
|
//
|
||||||
|
// Note that bracketed paste will be automatically disabled when the
|
||||||
|
// program quits.
|
||||||
|
func EnableBracketedPaste() Msg {
|
||||||
|
return enableBracketedPasteMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// enableBracketedPasteMsg in an internal message signals that
|
||||||
|
// bracketed paste should be enabled. You can send an
|
||||||
|
// enableBracketedPasteMsg with EnableBracketedPaste.
|
||||||
|
type enableBracketedPasteMsg struct{}
|
||||||
|
|
||||||
|
// DisableBracketedPaste is a special command that tells the Bubble Tea program
|
||||||
|
// to stop processing bracketed paste input.
|
||||||
|
//
|
||||||
|
// Note that bracketed paste will be automatically disabled when the
|
||||||
|
// program quits.
|
||||||
|
func DisableBracketedPaste() Msg {
|
||||||
|
return disableBracketedPasteMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// disableBracketedPasteMsg in an internal message signals that
|
||||||
|
// bracketed paste should be disabled. You can send an
|
||||||
|
// disableBracketedPasteMsg with DisableBracketedPaste.
|
||||||
|
type disableBracketedPasteMsg struct{}
|
||||||
|
|
||||||
|
// enableReportFocusMsg is an internal message that signals to enable focus
|
||||||
|
// reporting. You can send an enableReportFocusMsg with EnableReportFocus.
|
||||||
|
type enableReportFocusMsg struct{}
|
||||||
|
|
||||||
|
// EnableReportFocus is a special command that tells the Bubble Tea program to
|
||||||
|
// report focus events to the program.
|
||||||
|
func EnableReportFocus() Msg {
|
||||||
|
return enableReportFocusMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// disableReportFocusMsg is an internal message that signals to disable focus
|
||||||
|
// reporting. You can send an disableReportFocusMsg with DisableReportFocus.
|
||||||
|
type disableReportFocusMsg struct{}
|
||||||
|
|
||||||
|
// DisableReportFocus is a special command that tells the Bubble Tea program to
|
||||||
|
// stop reporting focus events to the program.
|
||||||
|
func DisableReportFocus() Msg {
|
||||||
|
return disableReportFocusMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnterAltScreen enters the alternate screen buffer, which consumes the entire
|
||||||
|
// terminal window. ExitAltScreen will return the terminal to its former state.
|
||||||
|
//
|
||||||
|
// Deprecated: Use the WithAltScreen ProgramOption instead.
|
||||||
|
func (p *Program) EnterAltScreen() {
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.enterAltScreen()
|
||||||
|
} else {
|
||||||
|
p.startupOptions |= withAltScreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExitAltScreen exits the alternate screen buffer.
|
||||||
|
//
|
||||||
|
// Deprecated: The altscreen will exited automatically when the program exits.
|
||||||
|
func (p *Program) ExitAltScreen() {
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.exitAltScreen()
|
||||||
|
} else {
|
||||||
|
p.startupOptions &^= withAltScreen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableMouseCellMotion enables mouse click, release, wheel and motion events
|
||||||
|
// if a mouse button is pressed (i.e., drag events).
|
||||||
|
//
|
||||||
|
// Deprecated: Use the WithMouseCellMotion ProgramOption instead.
|
||||||
|
func (p *Program) EnableMouseCellMotion() {
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.enableMouseCellMotion()
|
||||||
|
} else {
|
||||||
|
p.startupOptions |= withMouseCellMotion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be
|
||||||
|
// called automatically when exiting a Bubble Tea program.
|
||||||
|
//
|
||||||
|
// Deprecated: The mouse will automatically be disabled when the program exits.
|
||||||
|
func (p *Program) DisableMouseCellMotion() {
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.disableMouseCellMotion()
|
||||||
|
} else {
|
||||||
|
p.startupOptions &^= withMouseCellMotion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableMouseAllMotion enables mouse click, release, wheel and motion events,
|
||||||
|
// regardless of whether a mouse button is pressed. Many modern terminals
|
||||||
|
// support this, but not all.
|
||||||
|
//
|
||||||
|
// Deprecated: Use the WithMouseAllMotion ProgramOption instead.
|
||||||
|
func (p *Program) EnableMouseAllMotion() {
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.enableMouseAllMotion()
|
||||||
|
} else {
|
||||||
|
p.startupOptions |= withMouseAllMotion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableMouseAllMotion disables All Motion mouse tracking. This will be
|
||||||
|
// called automatically when exiting a Bubble Tea program.
|
||||||
|
//
|
||||||
|
// Deprecated: The mouse will automatically be disabled when the program exits.
|
||||||
|
func (p *Program) DisableMouseAllMotion() {
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.disableMouseAllMotion()
|
||||||
|
} else {
|
||||||
|
p.startupOptions &^= withMouseAllMotion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWindowTitle sets the terminal window title.
|
||||||
|
//
|
||||||
|
// Deprecated: Use the SetWindowTitle command instead.
|
||||||
|
func (p *Program) SetWindowTitle(title string) {
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.setWindowTitle(title)
|
||||||
|
} else {
|
||||||
|
p.startupTitle = title
|
||||||
|
}
|
||||||
|
}
|
||||||
33
vendor/github.com/charmbracelet/bubbletea/signals_unix.go
generated
vendored
Normal file
33
vendor/github.com/charmbracelet/bubbletea/signals_unix.go
generated
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos
|
||||||
|
// +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos
|
||||||
|
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// listenForResize sends messages (or errors) when the terminal resizes.
|
||||||
|
// Argument output should be the file descriptor for the terminal; usually
|
||||||
|
// os.Stdout.
|
||||||
|
func (p *Program) listenForResize(done chan struct{}) {
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGWINCH)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
signal.Stop(sig)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-sig:
|
||||||
|
}
|
||||||
|
|
||||||
|
p.checkResize()
|
||||||
|
}
|
||||||
|
}
|
||||||
10
vendor/github.com/charmbracelet/bubbletea/signals_windows.go
generated
vendored
Normal file
10
vendor/github.com/charmbracelet/bubbletea/signals_windows.go
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package tea
|
||||||
|
|
||||||
|
// listenForResize is not available on windows because windows does not
|
||||||
|
// implement syscall.SIGWINCH.
|
||||||
|
func (p *Program) listenForResize(done chan struct{}) {
|
||||||
|
close(done)
|
||||||
|
}
|
||||||
790
vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
generated
vendored
Normal file
790
vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
generated
vendored
Normal file
|
|
@ -0,0 +1,790 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
"github.com/muesli/ansi/compressor"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// defaultFramerate specifies the maximum interval at which we should
|
||||||
|
// update the view.
|
||||||
|
defaultFPS = 60
|
||||||
|
maxFPS = 120
|
||||||
|
)
|
||||||
|
|
||||||
|
// standardRenderer is a framerate-based terminal renderer, updating the view
|
||||||
|
// at a given framerate to avoid overloading the terminal emulator.
|
||||||
|
//
|
||||||
|
// In cases where very high performance is needed the renderer can be told
|
||||||
|
// to exclude ranges of lines, allowing them to be written to directly.
|
||||||
|
type standardRenderer struct {
|
||||||
|
mtx *sync.Mutex
|
||||||
|
out io.Writer
|
||||||
|
|
||||||
|
buf bytes.Buffer
|
||||||
|
queuedMessageLines []string
|
||||||
|
framerate time.Duration
|
||||||
|
ticker *time.Ticker
|
||||||
|
done chan struct{}
|
||||||
|
lastRender string
|
||||||
|
lastRenderedLines []string
|
||||||
|
linesRendered int
|
||||||
|
altLinesRendered int
|
||||||
|
useANSICompressor bool
|
||||||
|
once sync.Once
|
||||||
|
|
||||||
|
// cursor visibility state
|
||||||
|
cursorHidden bool
|
||||||
|
|
||||||
|
// essentially whether or not we're using the full size of the terminal
|
||||||
|
altScreenActive bool
|
||||||
|
|
||||||
|
// whether or not we're currently using bracketed paste
|
||||||
|
bpActive bool
|
||||||
|
|
||||||
|
// reportingFocus whether reporting focus events is enabled
|
||||||
|
reportingFocus bool
|
||||||
|
|
||||||
|
// renderer dimensions; usually the size of the window
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
|
||||||
|
// lines explicitly set not to render
|
||||||
|
ignoreLines map[int]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newRenderer creates a new renderer. Normally you'll want to initialize it
|
||||||
|
// with os.Stdout as the first argument.
|
||||||
|
func newRenderer(out io.Writer, useANSICompressor bool, fps int) renderer {
|
||||||
|
if fps < 1 {
|
||||||
|
fps = defaultFPS
|
||||||
|
} else if fps > maxFPS {
|
||||||
|
fps = maxFPS
|
||||||
|
}
|
||||||
|
r := &standardRenderer{
|
||||||
|
out: out,
|
||||||
|
mtx: &sync.Mutex{},
|
||||||
|
done: make(chan struct{}),
|
||||||
|
framerate: time.Second / time.Duration(fps),
|
||||||
|
useANSICompressor: useANSICompressor,
|
||||||
|
queuedMessageLines: []string{},
|
||||||
|
}
|
||||||
|
if r.useANSICompressor {
|
||||||
|
r.out = &compressor.Writer{Forward: out}
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// start starts the renderer.
|
||||||
|
func (r *standardRenderer) start() {
|
||||||
|
if r.ticker == nil {
|
||||||
|
r.ticker = time.NewTicker(r.framerate)
|
||||||
|
} else {
|
||||||
|
// If the ticker already exists, it has been stopped and we need to
|
||||||
|
// reset it.
|
||||||
|
r.ticker.Reset(r.framerate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the renderer can be restarted after a stop, we need to reset
|
||||||
|
// the done channel and its corresponding sync.Once.
|
||||||
|
r.once = sync.Once{}
|
||||||
|
|
||||||
|
go r.listen()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop permanently halts the renderer, rendering the final frame.
|
||||||
|
func (r *standardRenderer) stop() {
|
||||||
|
// Stop the renderer before acquiring the mutex to avoid a deadlock.
|
||||||
|
r.once.Do(func() {
|
||||||
|
r.done <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// flush locks the mutex
|
||||||
|
r.flush()
|
||||||
|
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.EraseEntireLine)
|
||||||
|
// Move the cursor back to the beginning of the line
|
||||||
|
r.execute("\r")
|
||||||
|
|
||||||
|
if r.useANSICompressor {
|
||||||
|
if w, ok := r.out.(io.WriteCloser); ok {
|
||||||
|
_ = w.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute writes a sequence to the terminal.
|
||||||
|
func (r *standardRenderer) execute(seq string) {
|
||||||
|
_, _ = io.WriteString(r.out, seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// kill halts the renderer. The final frame will not be rendered.
|
||||||
|
func (r *standardRenderer) kill() {
|
||||||
|
// Stop the renderer before acquiring the mutex to avoid a deadlock.
|
||||||
|
r.once.Do(func() {
|
||||||
|
r.done <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.EraseEntireLine)
|
||||||
|
// Move the cursor back to the beginning of the line
|
||||||
|
r.execute("\r")
|
||||||
|
}
|
||||||
|
|
||||||
|
// listen waits for ticks on the ticker, or a signal to stop the renderer.
|
||||||
|
func (r *standardRenderer) listen() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.done:
|
||||||
|
r.ticker.Stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
case <-r.ticker.C:
|
||||||
|
r.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flush renders the buffer.
|
||||||
|
func (r *standardRenderer) flush() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
|
||||||
|
// Nothing to do.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output buffer.
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
// Moving to the beginning of the section, that we rendered.
|
||||||
|
if r.altScreenActive {
|
||||||
|
buf.WriteString(ansi.CursorHomePosition)
|
||||||
|
} else if r.linesRendered > 1 {
|
||||||
|
buf.WriteString(ansi.CursorUp(r.linesRendered - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
newLines := strings.Split(r.buf.String(), "\n")
|
||||||
|
|
||||||
|
// If we know the output's height, we can use it to determine how many
|
||||||
|
// lines we can render. We drop lines from the top of the render buffer if
|
||||||
|
// necessary, as we can't navigate the cursor into the terminal's scrollback
|
||||||
|
// buffer.
|
||||||
|
if r.height > 0 && len(newLines) > r.height {
|
||||||
|
newLines = newLines[len(newLines)-r.height:]
|
||||||
|
}
|
||||||
|
|
||||||
|
flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive
|
||||||
|
|
||||||
|
if flushQueuedMessages {
|
||||||
|
// Dump the lines we've queued up for printing.
|
||||||
|
for _, line := range r.queuedMessageLines {
|
||||||
|
if ansi.StringWidth(line) < r.width {
|
||||||
|
// We only erase the rest of the line when the line is shorter than
|
||||||
|
// the width of the terminal. When the cursor reaches the end of
|
||||||
|
// the line, any escape sequences that follow will only affect the
|
||||||
|
// last cell of the line.
|
||||||
|
|
||||||
|
// Removing previously rendered content at the end of line.
|
||||||
|
line = line + ansi.EraseLineRight
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = buf.WriteString(line)
|
||||||
|
_, _ = buf.WriteString("\r\n")
|
||||||
|
}
|
||||||
|
// Clear the queued message lines.
|
||||||
|
r.queuedMessageLines = []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paint new lines.
|
||||||
|
for i := 0; i < len(newLines); i++ {
|
||||||
|
canSkip := !flushQueuedMessages && // Queuing messages triggers repaint -> we don't have access to previous frame content.
|
||||||
|
len(r.lastRenderedLines) > i && r.lastRenderedLines[i] == newLines[i] // Previously rendered line is the same.
|
||||||
|
|
||||||
|
if _, ignore := r.ignoreLines[i]; ignore || canSkip {
|
||||||
|
// Unless this is the last line, move the cursor down.
|
||||||
|
if i < len(newLines)-1 {
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 && r.lastRender == "" {
|
||||||
|
// On first render, reset the cursor to the start of the line
|
||||||
|
// before writing anything.
|
||||||
|
buf.WriteByte('\r')
|
||||||
|
}
|
||||||
|
|
||||||
|
line := newLines[i]
|
||||||
|
|
||||||
|
// Truncate lines wider than the width of the window to avoid
|
||||||
|
// wrapping, which will mess up rendering. If we don't have the
|
||||||
|
// width of the window this will be ignored.
|
||||||
|
//
|
||||||
|
// Note that on Windows we only get the width of the window on
|
||||||
|
// program initialization, so after a resize this won't perform
|
||||||
|
// correctly (signal SIGWINCH is not supported on Windows).
|
||||||
|
if r.width > 0 {
|
||||||
|
line = ansi.Truncate(line, r.width, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if ansi.StringWidth(line) < r.width {
|
||||||
|
// We only erase the rest of the line when the line is shorter than
|
||||||
|
// the width of the terminal. When the cursor reaches the end of
|
||||||
|
// the line, any escape sequences that follow will only affect the
|
||||||
|
// last cell of the line.
|
||||||
|
|
||||||
|
// Removing previously rendered content at the end of line.
|
||||||
|
line = line + ansi.EraseLineRight
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = buf.WriteString(line)
|
||||||
|
|
||||||
|
if i < len(newLines)-1 {
|
||||||
|
_, _ = buf.WriteString("\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clearing left over content from last render.
|
||||||
|
if r.lastLinesRendered() > len(newLines) {
|
||||||
|
buf.WriteString(ansi.EraseScreenBelow)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.altScreenActive {
|
||||||
|
r.altLinesRendered = len(newLines)
|
||||||
|
} else {
|
||||||
|
r.linesRendered = len(newLines)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the cursor is at the start of the last line to keep rendering
|
||||||
|
// behavior consistent.
|
||||||
|
if r.altScreenActive {
|
||||||
|
// This case fixes a bug in macOS terminal. In other terminals the
|
||||||
|
// other case seems to do the job regardless of whether or not we're
|
||||||
|
// using the full terminal window.
|
||||||
|
buf.WriteString(ansi.CursorPosition(0, len(newLines)))
|
||||||
|
} else {
|
||||||
|
buf.WriteByte('\r')
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = r.out.Write(buf.Bytes())
|
||||||
|
r.lastRender = r.buf.String()
|
||||||
|
|
||||||
|
// Save previously rendered lines for comparison in the next render. If we
|
||||||
|
// don't do this, we can't skip rendering lines that haven't changed.
|
||||||
|
// See https://github.com/charmbracelet/bubbletea/pull/1233
|
||||||
|
r.lastRenderedLines = newLines
|
||||||
|
r.buf.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastLinesRendered returns the number of lines rendered lastly.
|
||||||
|
func (r *standardRenderer) lastLinesRendered() int {
|
||||||
|
if r.altScreenActive {
|
||||||
|
return r.altLinesRendered
|
||||||
|
}
|
||||||
|
return r.linesRendered
|
||||||
|
}
|
||||||
|
|
||||||
|
// write writes to the internal buffer. The buffer will be outputted via the
|
||||||
|
// ticker which calls flush().
|
||||||
|
func (r *standardRenderer) write(s string) {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
r.buf.Reset()
|
||||||
|
|
||||||
|
// If an empty string was passed we should clear existing output and
|
||||||
|
// rendering nothing. Rather than introduce additional state to manage
|
||||||
|
// this, we render a single space as a simple (albeit less correct)
|
||||||
|
// solution.
|
||||||
|
if s == "" {
|
||||||
|
s = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = r.buf.WriteString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) repaint() {
|
||||||
|
r.lastRender = ""
|
||||||
|
r.lastRenderedLines = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) clearScreen() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.EraseEntireScreen)
|
||||||
|
r.execute(ansi.CursorHomePosition)
|
||||||
|
|
||||||
|
r.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) altScreen() bool {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
return r.altScreenActive
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) enterAltScreen() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
if r.altScreenActive {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.altScreenActive = true
|
||||||
|
r.execute(ansi.SetAltScreenSaveCursorMode)
|
||||||
|
|
||||||
|
// Ensure that the terminal is cleared, even when it doesn't support
|
||||||
|
// alt screen (or alt screen support is disabled, like GNU screen by
|
||||||
|
// default).
|
||||||
|
//
|
||||||
|
// Note: we can't use r.clearScreen() here because the mutex is already
|
||||||
|
// locked.
|
||||||
|
r.execute(ansi.EraseEntireScreen)
|
||||||
|
r.execute(ansi.CursorHomePosition)
|
||||||
|
|
||||||
|
// cmd.exe and other terminals keep separate cursor states for the AltScreen
|
||||||
|
// and the main buffer. We have to explicitly reset the cursor visibility
|
||||||
|
// whenever we enter AltScreen.
|
||||||
|
if r.cursorHidden {
|
||||||
|
r.execute(ansi.HideCursor)
|
||||||
|
} else {
|
||||||
|
r.execute(ansi.ShowCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entering the alt screen resets the lines rendered count.
|
||||||
|
r.altLinesRendered = 0
|
||||||
|
|
||||||
|
r.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) exitAltScreen() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
if !r.altScreenActive {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.altScreenActive = false
|
||||||
|
r.execute(ansi.ResetAltScreenSaveCursorMode)
|
||||||
|
|
||||||
|
// cmd.exe and other terminals keep separate cursor states for the AltScreen
|
||||||
|
// and the main buffer. We have to explicitly reset the cursor visibility
|
||||||
|
// whenever we exit AltScreen.
|
||||||
|
if r.cursorHidden {
|
||||||
|
r.execute(ansi.HideCursor)
|
||||||
|
} else {
|
||||||
|
r.execute(ansi.ShowCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.repaint()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) showCursor() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.cursorHidden = false
|
||||||
|
r.execute(ansi.ShowCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) hideCursor() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.cursorHidden = true
|
||||||
|
r.execute(ansi.HideCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) enableMouseCellMotion() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.SetButtonEventMouseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) disableMouseCellMotion() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.ResetButtonEventMouseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) enableMouseAllMotion() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.SetAnyEventMouseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) disableMouseAllMotion() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.ResetAnyEventMouseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) enableMouseSGRMode() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.SetSgrExtMouseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) disableMouseSGRMode() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.ResetSgrExtMouseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) enableBracketedPaste() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.SetBracketedPasteMode)
|
||||||
|
r.bpActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) disableBracketedPaste() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.ResetBracketedPasteMode)
|
||||||
|
r.bpActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) bracketedPasteActive() bool {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
return r.bpActive
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) enableReportFocus() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.SetFocusEventMode)
|
||||||
|
r.reportingFocus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) disableReportFocus() {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.execute(ansi.ResetFocusEventMode)
|
||||||
|
r.reportingFocus = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) reportFocus() bool {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
return r.reportingFocus
|
||||||
|
}
|
||||||
|
|
||||||
|
// setWindowTitle sets the terminal window title.
|
||||||
|
func (r *standardRenderer) setWindowTitle(title string) {
|
||||||
|
r.execute(ansi.SetWindowTitle(title))
|
||||||
|
}
|
||||||
|
|
||||||
|
// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
|
||||||
|
// renderer.
|
||||||
|
func (r *standardRenderer) setIgnoredLines(from int, to int) {
|
||||||
|
// Lock if we're going to be clearing some lines since we don't want
|
||||||
|
// anything jacking our cursor.
|
||||||
|
if r.lastLinesRendered() > 0 {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.ignoreLines == nil {
|
||||||
|
r.ignoreLines = make(map[int]struct{})
|
||||||
|
}
|
||||||
|
for i := from; i < to; i++ {
|
||||||
|
r.ignoreLines[i] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase ignored lines
|
||||||
|
lastLinesRendered := r.lastLinesRendered()
|
||||||
|
if lastLinesRendered > 0 {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
for i := lastLinesRendered - 1; i >= 0; i-- {
|
||||||
|
if _, exists := r.ignoreLines[i]; exists {
|
||||||
|
buf.WriteString(ansi.EraseEntireLine)
|
||||||
|
}
|
||||||
|
buf.WriteString(ansi.CUU1)
|
||||||
|
}
|
||||||
|
buf.WriteString(ansi.CursorPosition(0, lastLinesRendered)) // put cursor back
|
||||||
|
_, _ = r.out.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clearIgnoredLines returns control of any ignored lines to the standard
|
||||||
|
// Bubble Tea renderer. That is, any lines previously set to be ignored can be
|
||||||
|
// rendered to again.
|
||||||
|
func (r *standardRenderer) clearIgnoredLines() {
|
||||||
|
r.ignoreLines = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *standardRenderer) resetLinesRendered() {
|
||||||
|
r.linesRendered = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertTop effectively scrolls up. It inserts lines at the top of a given
|
||||||
|
// area designated to be a scrollable region, pushing everything else down.
|
||||||
|
// This is roughly how ncurses does it.
|
||||||
|
//
|
||||||
|
// To call this function use command ScrollUp().
|
||||||
|
//
|
||||||
|
// For this to work renderer.ignoreLines must be set to ignore the scrollable
|
||||||
|
// region since we are bypassing the normal Bubble Tea renderer here.
|
||||||
|
//
|
||||||
|
// Because this method relies on the terminal dimensions, it's only valid for
|
||||||
|
// full-window applications (generally those that use the alternate screen
|
||||||
|
// buffer).
|
||||||
|
//
|
||||||
|
// This method bypasses the normal rendering buffer and is philosophically
|
||||||
|
// different than the normal way we approach rendering in Bubble Tea. It's for
|
||||||
|
// use in high-performance rendering, such as a pager that could potentially
|
||||||
|
// be rendering very complicated ansi. In cases where the content is simpler
|
||||||
|
// standard Bubble Tea rendering should suffice.
|
||||||
|
//
|
||||||
|
// Deprecated: This option is deprecated and will be removed in a future
|
||||||
|
// version of this package.
|
||||||
|
func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary int) {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
buf.WriteString(ansi.SetTopBottomMargins(topBoundary, bottomBoundary))
|
||||||
|
buf.WriteString(ansi.CursorPosition(0, topBoundary))
|
||||||
|
buf.WriteString(ansi.InsertLine(len(lines)))
|
||||||
|
_, _ = buf.WriteString(strings.Join(lines, "\r\n"))
|
||||||
|
buf.WriteString(ansi.SetTopBottomMargins(0, r.height))
|
||||||
|
|
||||||
|
// Move cursor back to where the main rendering routine expects it to be
|
||||||
|
buf.WriteString(ansi.CursorPosition(0, r.lastLinesRendered()))
|
||||||
|
|
||||||
|
_, _ = r.out.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertBottom effectively scrolls down. It inserts lines at the bottom of
|
||||||
|
// a given area designated to be a scrollable region, pushing everything else
|
||||||
|
// up. This is roughly how ncurses does it.
|
||||||
|
//
|
||||||
|
// To call this function use the command ScrollDown().
|
||||||
|
//
|
||||||
|
// See note in insertTop() for caveats, how this function only makes sense for
|
||||||
|
// full-window applications, and how it differs from the normal way we do
|
||||||
|
// rendering in Bubble Tea.
|
||||||
|
//
|
||||||
|
// Deprecated: This option is deprecated and will be removed in a future
|
||||||
|
// version of this package.
|
||||||
|
func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBoundary int) {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
|
buf.WriteString(ansi.SetTopBottomMargins(topBoundary, bottomBoundary))
|
||||||
|
buf.WriteString(ansi.CursorPosition(0, bottomBoundary))
|
||||||
|
_, _ = buf.WriteString("\r\n" + strings.Join(lines, "\r\n"))
|
||||||
|
buf.WriteString(ansi.SetTopBottomMargins(0, r.height))
|
||||||
|
|
||||||
|
// Move cursor back to where the main rendering routine expects it to be
|
||||||
|
buf.WriteString(ansi.CursorPosition(0, r.lastLinesRendered()))
|
||||||
|
|
||||||
|
_, _ = r.out.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMessages handles internal messages for the renderer.
|
||||||
|
func (r *standardRenderer) handleMessages(msg Msg) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case repaintMsg:
|
||||||
|
// Force a repaint by clearing the render cache as we slide into a
|
||||||
|
// render.
|
||||||
|
r.mtx.Lock()
|
||||||
|
r.repaint()
|
||||||
|
r.mtx.Unlock()
|
||||||
|
|
||||||
|
case WindowSizeMsg:
|
||||||
|
r.mtx.Lock()
|
||||||
|
r.width = msg.Width
|
||||||
|
r.height = msg.Height
|
||||||
|
r.repaint()
|
||||||
|
r.mtx.Unlock()
|
||||||
|
|
||||||
|
case clearScrollAreaMsg:
|
||||||
|
r.clearIgnoredLines()
|
||||||
|
|
||||||
|
// Force a repaint on the area where the scrollable stuff was in this
|
||||||
|
// update cycle
|
||||||
|
r.mtx.Lock()
|
||||||
|
r.repaint()
|
||||||
|
r.mtx.Unlock()
|
||||||
|
|
||||||
|
case syncScrollAreaMsg:
|
||||||
|
// Re-render scrolling area
|
||||||
|
r.clearIgnoredLines()
|
||||||
|
r.setIgnoredLines(msg.topBoundary, msg.bottomBoundary)
|
||||||
|
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
|
||||||
|
|
||||||
|
// Force non-scrolling stuff to repaint in this update cycle
|
||||||
|
r.mtx.Lock()
|
||||||
|
r.repaint()
|
||||||
|
r.mtx.Unlock()
|
||||||
|
|
||||||
|
case scrollUpMsg:
|
||||||
|
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
|
||||||
|
|
||||||
|
case scrollDownMsg:
|
||||||
|
r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary)
|
||||||
|
|
||||||
|
case printLineMessage:
|
||||||
|
if !r.altScreenActive {
|
||||||
|
lines := strings.Split(msg.messageBody, "\n")
|
||||||
|
r.mtx.Lock()
|
||||||
|
r.queuedMessageLines = append(r.queuedMessageLines, lines...)
|
||||||
|
r.repaint()
|
||||||
|
r.mtx.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HIGH-PERFORMANCE RENDERING STUFF
|
||||||
|
|
||||||
|
type syncScrollAreaMsg struct {
|
||||||
|
lines []string
|
||||||
|
topBoundary int
|
||||||
|
bottomBoundary int
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncScrollArea performs a paint of the entire region designated to be the
|
||||||
|
// scrollable area. This is required to initialize the scrollable region and
|
||||||
|
// should also be called on resize (WindowSizeMsg).
|
||||||
|
//
|
||||||
|
// For high-performance, scroll-based rendering only.
|
||||||
|
//
|
||||||
|
// Deprecated: This option will be removed in a future version of this package.
|
||||||
|
func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd {
|
||||||
|
return func() Msg {
|
||||||
|
return syncScrollAreaMsg{
|
||||||
|
lines: lines,
|
||||||
|
topBoundary: topBoundary,
|
||||||
|
bottomBoundary: bottomBoundary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type clearScrollAreaMsg struct{}
|
||||||
|
|
||||||
|
// ClearScrollArea deallocates the scrollable region and returns the control of
|
||||||
|
// those lines to the main rendering routine.
|
||||||
|
//
|
||||||
|
// For high-performance, scroll-based rendering only.
|
||||||
|
//
|
||||||
|
// Deprecated: This option will be removed in a future version of this package.
|
||||||
|
func ClearScrollArea() Msg {
|
||||||
|
return clearScrollAreaMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type scrollUpMsg struct {
|
||||||
|
lines []string
|
||||||
|
topBoundary int
|
||||||
|
bottomBoundary int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollUp adds lines to the top of the scrollable region, pushing existing
|
||||||
|
// lines below down. Lines that are pushed out the scrollable region disappear
|
||||||
|
// from view.
|
||||||
|
//
|
||||||
|
// For high-performance, scroll-based rendering only.
|
||||||
|
//
|
||||||
|
// Deprecated: This option will be removed in a future version of this package.
|
||||||
|
func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd {
|
||||||
|
return func() Msg {
|
||||||
|
return scrollUpMsg{
|
||||||
|
lines: newLines,
|
||||||
|
topBoundary: topBoundary,
|
||||||
|
bottomBoundary: bottomBoundary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type scrollDownMsg struct {
|
||||||
|
lines []string
|
||||||
|
topBoundary int
|
||||||
|
bottomBoundary int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollDown adds lines to the bottom of the scrollable region, pushing
|
||||||
|
// existing lines above up. Lines that are pushed out of the scrollable region
|
||||||
|
// disappear from view.
|
||||||
|
//
|
||||||
|
// For high-performance, scroll-based rendering only.
|
||||||
|
//
|
||||||
|
// Deprecated: This option will be removed in a future version of this package.
|
||||||
|
func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd {
|
||||||
|
return func() Msg {
|
||||||
|
return scrollDownMsg{
|
||||||
|
lines: newLines,
|
||||||
|
topBoundary: topBoundary,
|
||||||
|
bottomBoundary: bottomBoundary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type printLineMessage struct {
|
||||||
|
messageBody string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println prints above the Program. This output is unmanaged by the program and
|
||||||
|
// will persist across renders by the Program.
|
||||||
|
//
|
||||||
|
// Unlike fmt.Println (but similar to log.Println) the message will be print on
|
||||||
|
// its own line.
|
||||||
|
//
|
||||||
|
// If the altscreen is active no output will be printed.
|
||||||
|
func Println(args ...interface{}) Cmd {
|
||||||
|
return func() Msg {
|
||||||
|
return printLineMessage{
|
||||||
|
messageBody: fmt.Sprint(args...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf prints above the Program. It takes a format template followed by
|
||||||
|
// values similar to fmt.Printf. This output is unmanaged by the program and
|
||||||
|
// will persist across renders by the Program.
|
||||||
|
//
|
||||||
|
// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
|
||||||
|
// its own line.
|
||||||
|
//
|
||||||
|
// If the altscreen is active no output will be printed.
|
||||||
|
func Printf(template string, args ...interface{}) Cmd {
|
||||||
|
return func() Msg {
|
||||||
|
return printLineMessage{
|
||||||
|
messageBody: fmt.Sprintf(template, args...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
941
vendor/github.com/charmbracelet/bubbletea/tea.go
generated
vendored
Normal file
941
vendor/github.com/charmbracelet/bubbletea/tea.go
generated
vendored
Normal file
|
|
@ -0,0 +1,941 @@
|
||||||
|
// Package tea provides a framework for building rich terminal user interfaces
|
||||||
|
// based on the paradigms of The Elm Architecture. It's well-suited for simple
|
||||||
|
// and complex terminal applications, either inline, full-window, or a mix of
|
||||||
|
// both. It's been battle-tested in several large projects and is
|
||||||
|
// production-ready.
|
||||||
|
//
|
||||||
|
// A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/master/tutorials
|
||||||
|
//
|
||||||
|
// Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/master/examples
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/term"
|
||||||
|
"github.com/muesli/cancelreader"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.
|
||||||
|
var ErrProgramPanic = errors.New("program experienced a panic")
|
||||||
|
|
||||||
|
// ErrProgramKilled is returned by [Program.Run] when the program gets killed.
|
||||||
|
var ErrProgramKilled = errors.New("program was killed")
|
||||||
|
|
||||||
|
// ErrInterrupted is returned by [Program.Run] when the program get a SIGINT
|
||||||
|
// signal, or when it receives a [InterruptMsg].
|
||||||
|
var ErrInterrupted = errors.New("program was interrupted")
|
||||||
|
|
||||||
|
// Msg contain data from the result of a IO operation. Msgs trigger the update
|
||||||
|
// function and, henceforth, the UI.
|
||||||
|
type Msg interface{}
|
||||||
|
|
||||||
|
// Model contains the program's state as well as its core functions.
|
||||||
|
type Model interface {
|
||||||
|
// Init is the first function that will be called. It returns an optional
|
||||||
|
// initial command. To not perform an initial command return nil.
|
||||||
|
Init() Cmd
|
||||||
|
|
||||||
|
// Update is called when a message is received. Use it to inspect messages
|
||||||
|
// and, in response, update the model and/or send a command.
|
||||||
|
Update(Msg) (Model, Cmd)
|
||||||
|
|
||||||
|
// View renders the program's UI, which is just a string. The view is
|
||||||
|
// rendered after every Update.
|
||||||
|
View() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmd is an IO operation that returns a message when it's complete. If it's
|
||||||
|
// nil it's considered a no-op. Use it for things like HTTP requests, timers,
|
||||||
|
// saving and loading from disk, and so on.
|
||||||
|
//
|
||||||
|
// Note that there's almost never a reason to use a command to send a message
|
||||||
|
// to another part of your program. That can almost always be done in the
|
||||||
|
// update function.
|
||||||
|
type Cmd func() Msg
|
||||||
|
|
||||||
|
type inputType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultInput inputType = iota
|
||||||
|
ttyInput
|
||||||
|
customInput
|
||||||
|
)
|
||||||
|
|
||||||
|
// String implements the stringer interface for [inputType]. It is intended to
|
||||||
|
// be used in testing.
|
||||||
|
func (i inputType) String() string {
|
||||||
|
return [...]string{
|
||||||
|
"default input",
|
||||||
|
"tty input",
|
||||||
|
"custom input",
|
||||||
|
}[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options to customize the program during its initialization. These are
|
||||||
|
// generally set with ProgramOptions.
|
||||||
|
//
|
||||||
|
// The options here are treated as bits.
|
||||||
|
type startupOptions int16
|
||||||
|
|
||||||
|
func (s startupOptions) has(option startupOptions) bool {
|
||||||
|
return s&option != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
withAltScreen startupOptions = 1 << iota
|
||||||
|
withMouseCellMotion
|
||||||
|
withMouseAllMotion
|
||||||
|
withANSICompressor
|
||||||
|
withoutSignalHandler
|
||||||
|
// Catching panics is incredibly useful for restoring the terminal to a
|
||||||
|
// usable state after a panic occurs. When this is set, Bubble Tea will
|
||||||
|
// recover from panics, print the stack trace, and disable raw mode. This
|
||||||
|
// feature is on by default.
|
||||||
|
withoutCatchPanics
|
||||||
|
withoutBracketedPaste
|
||||||
|
withReportFocus
|
||||||
|
)
|
||||||
|
|
||||||
|
// channelHandlers manages the series of channels returned by various processes.
|
||||||
|
// It allows us to wait for those processes to terminate before exiting the
|
||||||
|
// program.
|
||||||
|
type channelHandlers []chan struct{}
|
||||||
|
|
||||||
|
// Adds a channel to the list of handlers. We wait for all handlers to terminate
|
||||||
|
// gracefully on shutdown.
|
||||||
|
func (h *channelHandlers) add(ch chan struct{}) {
|
||||||
|
*h = append(*h, ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown waits for all handlers to terminate.
|
||||||
|
func (h channelHandlers) shutdown() {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, ch := range h {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(ch chan struct{}) {
|
||||||
|
<-ch
|
||||||
|
wg.Done()
|
||||||
|
}(ch)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Program is a terminal user interface.
|
||||||
|
type Program struct {
|
||||||
|
initialModel Model
|
||||||
|
|
||||||
|
// handlers is a list of channels that need to be waited on before the
|
||||||
|
// program can exit.
|
||||||
|
handlers channelHandlers
|
||||||
|
|
||||||
|
// Configuration options that will set as the program is initializing,
|
||||||
|
// treated as bits. These options can be set via various ProgramOptions.
|
||||||
|
startupOptions startupOptions
|
||||||
|
|
||||||
|
// startupTitle is the title that will be set on the terminal when the
|
||||||
|
// program starts.
|
||||||
|
startupTitle string
|
||||||
|
|
||||||
|
inputType inputType
|
||||||
|
|
||||||
|
// externalCtx is a context that was passed in via WithContext, otherwise defaulting
|
||||||
|
// to ctx.Background() (in case it was not), the internal context is derived from it.
|
||||||
|
externalCtx context.Context
|
||||||
|
|
||||||
|
// ctx is the programs's internal context for signalling internal teardown.
|
||||||
|
// It is built and derived from the externalCtx in NewProgram().
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
msgs chan Msg
|
||||||
|
errs chan error
|
||||||
|
finished chan struct{}
|
||||||
|
|
||||||
|
// where to send output, this will usually be os.Stdout.
|
||||||
|
output io.Writer
|
||||||
|
// ttyOutput is null if output is not a TTY.
|
||||||
|
ttyOutput term.File
|
||||||
|
previousOutputState *term.State
|
||||||
|
renderer renderer
|
||||||
|
|
||||||
|
// the environment variables for the program, defaults to os.Environ().
|
||||||
|
environ []string
|
||||||
|
|
||||||
|
// where to read inputs from, this will usually be os.Stdin.
|
||||||
|
input io.Reader
|
||||||
|
// ttyInput is null if input is not a TTY.
|
||||||
|
ttyInput term.File
|
||||||
|
previousTtyInputState *term.State
|
||||||
|
cancelReader cancelreader.CancelReader
|
||||||
|
readLoopDone chan struct{}
|
||||||
|
|
||||||
|
// was the altscreen active before releasing the terminal?
|
||||||
|
altScreenWasActive bool
|
||||||
|
ignoreSignals uint32
|
||||||
|
|
||||||
|
bpWasActive bool // was the bracketed paste mode active before releasing the terminal?
|
||||||
|
reportFocus bool // was focus reporting active before releasing the terminal?
|
||||||
|
|
||||||
|
filter func(Model, Msg) Msg
|
||||||
|
|
||||||
|
// fps is the frames per second we should set on the renderer, if
|
||||||
|
// applicable,
|
||||||
|
fps int
|
||||||
|
|
||||||
|
// mouseMode is true if the program should enable mouse mode on Windows.
|
||||||
|
mouseMode bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit is a special command that tells the Bubble Tea program to exit.
|
||||||
|
func Quit() Msg {
|
||||||
|
return QuitMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuitMsg signals that the program should quit. You can send a [QuitMsg] with
|
||||||
|
// [Quit].
|
||||||
|
type QuitMsg struct{}
|
||||||
|
|
||||||
|
// Suspend is a special command that tells the Bubble Tea program to suspend.
|
||||||
|
func Suspend() Msg {
|
||||||
|
return SuspendMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuspendMsg signals the program should suspend.
|
||||||
|
// This usually happens when ctrl+z is pressed on common programs, but since
|
||||||
|
// bubbletea puts the terminal in raw mode, we need to handle it in a
|
||||||
|
// per-program basis.
|
||||||
|
//
|
||||||
|
// You can send this message with [Suspend()].
|
||||||
|
type SuspendMsg struct{}
|
||||||
|
|
||||||
|
// ResumeMsg can be listen to do something once a program is resumed back
|
||||||
|
// from a suspend state.
|
||||||
|
type ResumeMsg struct{}
|
||||||
|
|
||||||
|
// InterruptMsg signals the program should suspend.
|
||||||
|
// This usually happens when ctrl+c is pressed on common programs, but since
|
||||||
|
// bubbletea puts the terminal in raw mode, we need to handle it in a
|
||||||
|
// per-program basis.
|
||||||
|
//
|
||||||
|
// You can send this message with [Interrupt()].
|
||||||
|
type InterruptMsg struct{}
|
||||||
|
|
||||||
|
// Interrupt is a special command that tells the Bubble Tea program to
|
||||||
|
// interrupt.
|
||||||
|
func Interrupt() Msg {
|
||||||
|
return InterruptMsg{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProgram creates a new Program.
|
||||||
|
func NewProgram(model Model, opts ...ProgramOption) *Program {
|
||||||
|
p := &Program{
|
||||||
|
initialModel: model,
|
||||||
|
msgs: make(chan Msg),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply all options to the program.
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A context can be provided with a ProgramOption, but if none was provided
|
||||||
|
// we'll use the default background context.
|
||||||
|
if p.externalCtx == nil {
|
||||||
|
p.externalCtx = context.Background()
|
||||||
|
}
|
||||||
|
// Initialize context and teardown channel.
|
||||||
|
p.ctx, p.cancel = context.WithCancel(p.externalCtx)
|
||||||
|
|
||||||
|
// if no output was set, set it to stdout
|
||||||
|
if p.output == nil {
|
||||||
|
p.output = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no environment was set, set it to os.Environ()
|
||||||
|
if p.environ == nil {
|
||||||
|
p.environ = os.Environ()
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Program) handleSignals() chan struct{} {
|
||||||
|
ch := make(chan struct{})
|
||||||
|
|
||||||
|
// Listen for SIGINT and SIGTERM.
|
||||||
|
//
|
||||||
|
// In most cases ^C will not send an interrupt because the terminal will be
|
||||||
|
// in raw mode and ^C will be captured as a keystroke and sent along to
|
||||||
|
// Program.Update as a KeyMsg. When input is not a TTY, however, ^C will be
|
||||||
|
// caught here.
|
||||||
|
//
|
||||||
|
// SIGTERM is sent by unix utilities (like kill) to terminate a process.
|
||||||
|
go func() {
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer func() {
|
||||||
|
signal.Stop(sig)
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
case s := <-sig:
|
||||||
|
if atomic.LoadUint32(&p.ignoreSignals) == 0 {
|
||||||
|
switch s {
|
||||||
|
case syscall.SIGINT:
|
||||||
|
p.msgs <- InterruptMsg{}
|
||||||
|
default:
|
||||||
|
p.msgs <- QuitMsg{}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleResize handles terminal resize events.
|
||||||
|
func (p *Program) handleResize() chan struct{} {
|
||||||
|
ch := make(chan struct{})
|
||||||
|
|
||||||
|
if p.ttyOutput != nil {
|
||||||
|
// Get the initial terminal size and send it to the program.
|
||||||
|
go p.checkResize()
|
||||||
|
|
||||||
|
// Listen for window resizes.
|
||||||
|
go p.listenForResize(ch)
|
||||||
|
} else {
|
||||||
|
close(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCommands runs commands in a goroutine and sends the result to the
|
||||||
|
// program's message channel.
|
||||||
|
func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
|
||||||
|
ch := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
return
|
||||||
|
|
||||||
|
case cmd := <-cmds:
|
||||||
|
if cmd == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't wait on these goroutines, otherwise the shutdown
|
||||||
|
// latency would get too large as a Cmd can run for some time
|
||||||
|
// (e.g. tick commands that sleep for half a second). It's not
|
||||||
|
// possible to cancel them so we'll have to leak the goroutine
|
||||||
|
// until Cmd returns.
|
||||||
|
go func() {
|
||||||
|
// Recover from panics.
|
||||||
|
if !p.startupOptions.has(withoutCatchPanics) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
p.recoverFromGoPanic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := cmd() // this can be long.
|
||||||
|
p.Send(msg)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Program) disableMouse() {
|
||||||
|
p.renderer.disableMouseCellMotion()
|
||||||
|
p.renderer.disableMouseAllMotion()
|
||||||
|
p.renderer.disableMouseSGRMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventLoop is the central message loop. It receives and handles the default
|
||||||
|
// Bubble Tea messages, update the model and triggers redraws.
|
||||||
|
func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
return model, nil
|
||||||
|
|
||||||
|
case err := <-p.errs:
|
||||||
|
return model, err
|
||||||
|
|
||||||
|
case msg := <-p.msgs:
|
||||||
|
// Filter messages.
|
||||||
|
if p.filter != nil {
|
||||||
|
msg = p.filter(model, msg)
|
||||||
|
}
|
||||||
|
if msg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle special internal messages.
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case QuitMsg:
|
||||||
|
return model, nil
|
||||||
|
|
||||||
|
case InterruptMsg:
|
||||||
|
return model, ErrInterrupted
|
||||||
|
|
||||||
|
case SuspendMsg:
|
||||||
|
if suspendSupported {
|
||||||
|
p.suspend()
|
||||||
|
}
|
||||||
|
|
||||||
|
case clearScreenMsg:
|
||||||
|
p.renderer.clearScreen()
|
||||||
|
|
||||||
|
case enterAltScreenMsg:
|
||||||
|
p.renderer.enterAltScreen()
|
||||||
|
|
||||||
|
case exitAltScreenMsg:
|
||||||
|
p.renderer.exitAltScreen()
|
||||||
|
|
||||||
|
case enableMouseCellMotionMsg, enableMouseAllMotionMsg:
|
||||||
|
switch msg.(type) {
|
||||||
|
case enableMouseCellMotionMsg:
|
||||||
|
p.renderer.enableMouseCellMotion()
|
||||||
|
case enableMouseAllMotionMsg:
|
||||||
|
p.renderer.enableMouseAllMotion()
|
||||||
|
}
|
||||||
|
// mouse mode (1006) is a no-op if the terminal doesn't support it.
|
||||||
|
p.renderer.enableMouseSGRMode()
|
||||||
|
|
||||||
|
// XXX: This is used to enable mouse mode on Windows. We need
|
||||||
|
// to reinitialize the cancel reader to get the mouse events to
|
||||||
|
// work.
|
||||||
|
if runtime.GOOS == "windows" && !p.mouseMode {
|
||||||
|
p.mouseMode = true
|
||||||
|
p.initCancelReader(true) //nolint:errcheck,gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
case disableMouseMsg:
|
||||||
|
p.disableMouse()
|
||||||
|
|
||||||
|
// XXX: On Windows, mouse mode is enabled on the input reader
|
||||||
|
// level. We need to instruct the input reader to stop reading
|
||||||
|
// mouse events.
|
||||||
|
if runtime.GOOS == "windows" && p.mouseMode {
|
||||||
|
p.mouseMode = false
|
||||||
|
p.initCancelReader(true) //nolint:errcheck,gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
case showCursorMsg:
|
||||||
|
p.renderer.showCursor()
|
||||||
|
|
||||||
|
case hideCursorMsg:
|
||||||
|
p.renderer.hideCursor()
|
||||||
|
|
||||||
|
case enableBracketedPasteMsg:
|
||||||
|
p.renderer.enableBracketedPaste()
|
||||||
|
|
||||||
|
case disableBracketedPasteMsg:
|
||||||
|
p.renderer.disableBracketedPaste()
|
||||||
|
|
||||||
|
case enableReportFocusMsg:
|
||||||
|
p.renderer.enableReportFocus()
|
||||||
|
|
||||||
|
case disableReportFocusMsg:
|
||||||
|
p.renderer.disableReportFocus()
|
||||||
|
|
||||||
|
case execMsg:
|
||||||
|
// NB: this blocks.
|
||||||
|
p.exec(msg.cmd, msg.fn)
|
||||||
|
|
||||||
|
case BatchMsg:
|
||||||
|
go p.execBatchMsg(msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case sequenceMsg:
|
||||||
|
go p.execSequenceMsg(msg)
|
||||||
|
continue
|
||||||
|
|
||||||
|
case setWindowTitleMsg:
|
||||||
|
p.SetWindowTitle(string(msg))
|
||||||
|
|
||||||
|
case windowSizeMsg:
|
||||||
|
go p.checkResize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process internal messages for the renderer.
|
||||||
|
if r, ok := p.renderer.(*standardRenderer); ok {
|
||||||
|
r.handleMessages(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd Cmd
|
||||||
|
model, cmd = model.Update(msg) // run update
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
return model, nil
|
||||||
|
case cmds <- cmd: // process command (if any)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.renderer.write(model.View()) // send view to renderer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Program) execSequenceMsg(msg sequenceMsg) {
|
||||||
|
if !p.startupOptions.has(withoutCatchPanics) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
p.recoverFromGoPanic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute commands one at a time, in order.
|
||||||
|
for _, cmd := range msg {
|
||||||
|
if cmd == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg := cmd()
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case BatchMsg:
|
||||||
|
p.execBatchMsg(msg)
|
||||||
|
case sequenceMsg:
|
||||||
|
p.execSequenceMsg(msg)
|
||||||
|
default:
|
||||||
|
p.Send(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Program) execBatchMsg(msg BatchMsg) {
|
||||||
|
if !p.startupOptions.has(withoutCatchPanics) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
p.recoverFromGoPanic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute commands one at a time.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, cmd := range msg {
|
||||||
|
if cmd == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
if !p.startupOptions.has(withoutCatchPanics) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
p.recoverFromGoPanic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := cmd()
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case BatchMsg:
|
||||||
|
p.execBatchMsg(msg)
|
||||||
|
case sequenceMsg:
|
||||||
|
p.execSequenceMsg(msg)
|
||||||
|
default:
|
||||||
|
p.Send(msg)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait() // wait for all commands from batch msg to finish
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run initializes the program and runs its event loops, blocking until it gets
|
||||||
|
// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
|
||||||
|
// Returns the final model.
|
||||||
|
func (p *Program) Run() (returnModel Model, returnErr error) {
|
||||||
|
p.handlers = channelHandlers{}
|
||||||
|
cmds := make(chan Cmd)
|
||||||
|
p.errs = make(chan error, 1)
|
||||||
|
|
||||||
|
p.finished = make(chan struct{})
|
||||||
|
defer func() {
|
||||||
|
close(p.finished)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer p.cancel()
|
||||||
|
|
||||||
|
switch p.inputType {
|
||||||
|
case defaultInput:
|
||||||
|
p.input = os.Stdin
|
||||||
|
|
||||||
|
// The user has not set a custom input, so we need to check whether or
|
||||||
|
// not standard input is a terminal. If it's not, we open a new TTY for
|
||||||
|
// input. This will allow things to "just work" in cases where data was
|
||||||
|
// piped in or redirected to the application.
|
||||||
|
//
|
||||||
|
// To disable input entirely pass nil to the [WithInput] program option.
|
||||||
|
f, isFile := p.input.(term.File)
|
||||||
|
if !isFile {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if term.IsTerminal(f.Fd()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := openInputTTY()
|
||||||
|
if err != nil {
|
||||||
|
return p.initialModel, err
|
||||||
|
}
|
||||||
|
defer f.Close() //nolint:errcheck
|
||||||
|
p.input = f
|
||||||
|
|
||||||
|
case ttyInput:
|
||||||
|
// Open a new TTY, by request
|
||||||
|
f, err := openInputTTY()
|
||||||
|
if err != nil {
|
||||||
|
return p.initialModel, err
|
||||||
|
}
|
||||||
|
defer f.Close() //nolint:errcheck
|
||||||
|
p.input = f
|
||||||
|
|
||||||
|
case customInput:
|
||||||
|
// (There is nothing extra to do.)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle signals.
|
||||||
|
if !p.startupOptions.has(withoutSignalHandler) {
|
||||||
|
p.handlers.add(p.handleSignals())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recover from panics.
|
||||||
|
if !p.startupOptions.has(withoutCatchPanics) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
returnErr = fmt.Errorf("%w: %w", ErrProgramKilled, ErrProgramPanic)
|
||||||
|
p.recoverFromPanic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no renderer is set use the standard one.
|
||||||
|
if p.renderer == nil {
|
||||||
|
p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor), p.fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if output is a TTY before entering raw mode, hiding the cursor and
|
||||||
|
// so on.
|
||||||
|
if err := p.initTerminal(); err != nil {
|
||||||
|
return p.initialModel, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Honor program startup options.
|
||||||
|
if p.startupTitle != "" {
|
||||||
|
p.renderer.setWindowTitle(p.startupTitle)
|
||||||
|
}
|
||||||
|
if p.startupOptions&withAltScreen != 0 {
|
||||||
|
p.renderer.enterAltScreen()
|
||||||
|
}
|
||||||
|
if p.startupOptions&withoutBracketedPaste == 0 {
|
||||||
|
p.renderer.enableBracketedPaste()
|
||||||
|
}
|
||||||
|
if p.startupOptions&withMouseCellMotion != 0 {
|
||||||
|
p.renderer.enableMouseCellMotion()
|
||||||
|
p.renderer.enableMouseSGRMode()
|
||||||
|
} else if p.startupOptions&withMouseAllMotion != 0 {
|
||||||
|
p.renderer.enableMouseAllMotion()
|
||||||
|
p.renderer.enableMouseSGRMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: Should we enable mouse mode on Windows?
|
||||||
|
// This needs to happen before initializing the cancel and input reader.
|
||||||
|
p.mouseMode = p.startupOptions&withMouseCellMotion != 0 || p.startupOptions&withMouseAllMotion != 0
|
||||||
|
|
||||||
|
if p.startupOptions&withReportFocus != 0 {
|
||||||
|
p.renderer.enableReportFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the renderer.
|
||||||
|
p.renderer.start()
|
||||||
|
|
||||||
|
// Initialize the program.
|
||||||
|
model := p.initialModel
|
||||||
|
if initCmd := model.Init(); initCmd != nil {
|
||||||
|
ch := make(chan struct{})
|
||||||
|
p.handlers.add(ch)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case cmds <- initCmd:
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the initial view.
|
||||||
|
p.renderer.write(model.View())
|
||||||
|
|
||||||
|
// Subscribe to user input.
|
||||||
|
if p.input != nil {
|
||||||
|
if err := p.initCancelReader(false); err != nil {
|
||||||
|
return model, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle resize events.
|
||||||
|
p.handlers.add(p.handleResize())
|
||||||
|
|
||||||
|
// Process commands.
|
||||||
|
p.handlers.add(p.handleCommands(cmds))
|
||||||
|
|
||||||
|
// Run event loop, handle updates and draw.
|
||||||
|
model, err := p.eventLoop(model, cmds)
|
||||||
|
|
||||||
|
if err == nil && len(p.errs) > 0 {
|
||||||
|
err = <-p.errs // Drain a leftover error in case eventLoop crashed
|
||||||
|
}
|
||||||
|
|
||||||
|
killed := p.externalCtx.Err() != nil || p.ctx.Err() != nil || err != nil
|
||||||
|
if killed {
|
||||||
|
if err == nil && p.externalCtx.Err() != nil {
|
||||||
|
// Return also as context error the cancellation of an external context.
|
||||||
|
// This is the context the user knows about and should be able to act on.
|
||||||
|
err = fmt.Errorf("%w: %w", ErrProgramKilled, p.externalCtx.Err())
|
||||||
|
} else if err == nil && p.ctx.Err() != nil {
|
||||||
|
// Return only that the program was killed (not the internal mechanism).
|
||||||
|
// The user does not know or need to care about the internal program context.
|
||||||
|
err = ErrProgramKilled
|
||||||
|
} else {
|
||||||
|
// Return that the program was killed and also the error that caused it.
|
||||||
|
err = fmt.Errorf("%w: %w", ErrProgramKilled, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Graceful shutdown of the program (not killed):
|
||||||
|
// Ensure we rendered the final state of the model.
|
||||||
|
p.renderer.write(model.View())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore terminal state.
|
||||||
|
p.shutdown(killed)
|
||||||
|
|
||||||
|
return model, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartReturningModel initializes the program and runs its event loops,
|
||||||
|
// blocking until it gets terminated by either [Program.Quit], [Program.Kill],
|
||||||
|
// or its signal handler. Returns the final model.
|
||||||
|
//
|
||||||
|
// Deprecated: please use [Program.Run] instead.
|
||||||
|
func (p *Program) StartReturningModel() (Model, error) {
|
||||||
|
return p.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes the program and runs its event loops, blocking until it
|
||||||
|
// gets terminated by either [Program.Quit], [Program.Kill], or its signal
|
||||||
|
// handler.
|
||||||
|
//
|
||||||
|
// Deprecated: please use [Program.Run] instead.
|
||||||
|
func (p *Program) Start() error {
|
||||||
|
_, err := p.Run()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send sends a message to the main update function, effectively allowing
|
||||||
|
// messages to be injected from outside the program for interoperability
|
||||||
|
// purposes.
|
||||||
|
//
|
||||||
|
// If the program hasn't started yet this will be a blocking operation.
|
||||||
|
// If the program has already been terminated this will be a no-op, so it's safe
|
||||||
|
// to send messages after the program has exited.
|
||||||
|
func (p *Program) Send(msg Msg) {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
case p.msgs <- msg:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit is a convenience function for quitting Bubble Tea programs. Use it
|
||||||
|
// when you need to shut down a Bubble Tea program from the outside.
|
||||||
|
//
|
||||||
|
// If you wish to quit from within a Bubble Tea program use the Quit command.
|
||||||
|
//
|
||||||
|
// If the program is not running this will be a no-op, so it's safe to call
|
||||||
|
// if the program is unstarted or has already exited.
|
||||||
|
func (p *Program) Quit() {
|
||||||
|
p.Send(Quit())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill signals the program to stop immediately and restore the former terminal state.
|
||||||
|
// The final render that you would normally see when quitting will be skipped.
|
||||||
|
// [program.Run] returns a [ErrProgramKilled] error.
|
||||||
|
func (p *Program) Kill() {
|
||||||
|
p.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait waits/blocks until the underlying Program finished shutting down.
|
||||||
|
func (p *Program) Wait() {
|
||||||
|
<-p.finished
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown performs operations to free up resources and restore the terminal
|
||||||
|
// to its original state. It is called once at the end of the program's lifetime.
|
||||||
|
//
|
||||||
|
// This method should not be called to signal the program to be killed/shutdown.
|
||||||
|
// Doing so can lead to race conditions with the eventual call at the program's end.
|
||||||
|
// As alternatives, the [Quit] or [Kill] convenience methods should be used instead.
|
||||||
|
func (p *Program) shutdown(kill bool) {
|
||||||
|
p.cancel()
|
||||||
|
|
||||||
|
// Wait for all handlers to finish.
|
||||||
|
p.handlers.shutdown()
|
||||||
|
|
||||||
|
// Check if the cancel reader has been setup before waiting and closing.
|
||||||
|
if p.cancelReader != nil {
|
||||||
|
// Wait for input loop to finish.
|
||||||
|
if p.cancelReader.Cancel() {
|
||||||
|
if !kill {
|
||||||
|
p.waitForReadLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = p.cancelReader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.renderer != nil {
|
||||||
|
if kill {
|
||||||
|
p.renderer.kill()
|
||||||
|
} else {
|
||||||
|
p.renderer.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = p.restoreTerminalState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// recoverFromPanic recovers from a panic, prints the stack trace, and restores
|
||||||
|
// the terminal to a usable state.
|
||||||
|
func (p *Program) recoverFromPanic(r interface{}) {
|
||||||
|
select {
|
||||||
|
case p.errs <- ErrProgramPanic:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
p.shutdown(true) // Ok to call here, p.Run() cannot do it anymore.
|
||||||
|
fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
|
||||||
|
debug.PrintStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// recoverFromGoPanic recovers from a goroutine panic, prints a stack trace and
|
||||||
|
// signals for the program to be killed and terminal restored to a usable state.
|
||||||
|
func (p *Program) recoverFromGoPanic(r interface{}) {
|
||||||
|
select {
|
||||||
|
case p.errs <- ErrProgramPanic:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
p.cancel()
|
||||||
|
fmt.Printf("Caught goroutine panic:\n\n%s\n\nRestoring terminal...\n\n", r)
|
||||||
|
debug.PrintStack()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseTerminal restores the original terminal state and cancels the input
|
||||||
|
// reader. You can return control to the Program with RestoreTerminal.
|
||||||
|
func (p *Program) ReleaseTerminal() error {
|
||||||
|
atomic.StoreUint32(&p.ignoreSignals, 1)
|
||||||
|
if p.cancelReader != nil {
|
||||||
|
p.cancelReader.Cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.waitForReadLoop()
|
||||||
|
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.stop()
|
||||||
|
p.altScreenWasActive = p.renderer.altScreen()
|
||||||
|
p.bpWasActive = p.renderer.bracketedPasteActive()
|
||||||
|
p.reportFocus = p.renderer.reportFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.restoreTerminalState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreTerminal reinitializes the Program's input reader, restores the
|
||||||
|
// terminal to the former state when the program was running, and repaints.
|
||||||
|
// Use it to reinitialize a Program after running ReleaseTerminal.
|
||||||
|
func (p *Program) RestoreTerminal() error {
|
||||||
|
atomic.StoreUint32(&p.ignoreSignals, 0)
|
||||||
|
|
||||||
|
if err := p.initTerminal(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := p.initCancelReader(false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if p.altScreenWasActive {
|
||||||
|
p.renderer.enterAltScreen()
|
||||||
|
} else {
|
||||||
|
// entering alt screen already causes a repaint.
|
||||||
|
go p.Send(repaintMsg{})
|
||||||
|
}
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.start()
|
||||||
|
}
|
||||||
|
if p.bpWasActive {
|
||||||
|
p.renderer.enableBracketedPaste()
|
||||||
|
}
|
||||||
|
if p.reportFocus {
|
||||||
|
p.renderer.enableReportFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the output is a terminal, it may have been resized while another
|
||||||
|
// process was at the foreground, in which case we may not have received
|
||||||
|
// SIGWINCH. Detect any size change now and propagate the new size as
|
||||||
|
// needed.
|
||||||
|
go p.checkResize()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println prints above the Program. This output is unmanaged by the program
|
||||||
|
// and will persist across renders by the Program.
|
||||||
|
//
|
||||||
|
// If the altscreen is active no output will be printed.
|
||||||
|
func (p *Program) Println(args ...interface{}) {
|
||||||
|
p.msgs <- printLineMessage{
|
||||||
|
messageBody: fmt.Sprint(args...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf prints above the Program. It takes a format template followed by
|
||||||
|
// values similar to fmt.Printf. This output is unmanaged by the program and
|
||||||
|
// will persist across renders by the Program.
|
||||||
|
//
|
||||||
|
// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
|
||||||
|
// its own line.
|
||||||
|
//
|
||||||
|
// If the altscreen is active no output will be printed.
|
||||||
|
func (p *Program) Printf(template string, args ...interface{}) {
|
||||||
|
p.msgs <- printLineMessage{
|
||||||
|
messageBody: fmt.Sprintf(template, args...),
|
||||||
|
}
|
||||||
|
}
|
||||||
22
vendor/github.com/charmbracelet/bubbletea/tea_init.go
generated
vendored
Normal file
22
vendor/github.com/charmbracelet/bubbletea/tea_init.go
generated
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// XXX: This is a workaround to make assure that Lip Gloss and Termenv
|
||||||
|
// query the terminal before any Bubble Tea Program runs and acquires the
|
||||||
|
// terminal. Without this, Programs that use Lip Gloss/Termenv might hang
|
||||||
|
// while waiting for a a [termenv.OSCTimeout] while querying the terminal
|
||||||
|
// for its background/foreground colors.
|
||||||
|
//
|
||||||
|
// This happens because Bubble Tea acquires the terminal before termenv
|
||||||
|
// reads any responses.
|
||||||
|
//
|
||||||
|
// Note that this will only affect programs running on the default IO i.e.
|
||||||
|
// [os.Stdout] and [os.Stdin].
|
||||||
|
//
|
||||||
|
// This workaround will be removed in v2.
|
||||||
|
_ = lipgloss.HasDarkBackground()
|
||||||
|
}
|
||||||
141
vendor/github.com/charmbracelet/bubbletea/tty.go
generated
vendored
Normal file
141
vendor/github.com/charmbracelet/bubbletea/tty.go
generated
vendored
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/term"
|
||||||
|
"github.com/muesli/cancelreader"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Program) suspend() {
|
||||||
|
if err := p.ReleaseTerminal(); err != nil {
|
||||||
|
// If we can't release input, abort.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
suspendProcess()
|
||||||
|
|
||||||
|
_ = p.RestoreTerminal()
|
||||||
|
go p.Send(ResumeMsg{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Program) initTerminal() error {
|
||||||
|
if _, ok := p.renderer.(*nilRenderer); ok {
|
||||||
|
// No need to initialize the terminal if we're not rendering
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.initInput(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.renderer.hideCursor()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreTerminalState restores the terminal to the state prior to running the
|
||||||
|
// Bubble Tea program.
|
||||||
|
func (p *Program) restoreTerminalState() error {
|
||||||
|
if p.renderer != nil {
|
||||||
|
p.renderer.disableBracketedPaste()
|
||||||
|
p.renderer.showCursor()
|
||||||
|
p.disableMouse()
|
||||||
|
|
||||||
|
if p.renderer.reportFocus() {
|
||||||
|
p.renderer.disableReportFocus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.renderer.altScreen() {
|
||||||
|
p.renderer.exitAltScreen()
|
||||||
|
|
||||||
|
// give the terminal a moment to catch up
|
||||||
|
time.Sleep(time.Millisecond * 10) //nolint:mnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.restoreInput()
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreInput restores the tty input to its original state.
|
||||||
|
func (p *Program) restoreInput() error {
|
||||||
|
if p.ttyInput != nil && p.previousTtyInputState != nil {
|
||||||
|
if err := term.Restore(p.ttyInput.Fd(), p.previousTtyInputState); err != nil {
|
||||||
|
return fmt.Errorf("error restoring console: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.ttyOutput != nil && p.previousOutputState != nil {
|
||||||
|
if err := term.Restore(p.ttyOutput.Fd(), p.previousOutputState); err != nil {
|
||||||
|
return fmt.Errorf("error restoring console: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initCancelReader (re)commences reading inputs.
|
||||||
|
func (p *Program) initCancelReader(cancel bool) error {
|
||||||
|
if cancel && p.cancelReader != nil {
|
||||||
|
p.cancelReader.Cancel()
|
||||||
|
p.waitForReadLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
p.cancelReader, err = newInputReader(p.input, p.mouseMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error creating cancelreader: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.readLoopDone = make(chan struct{})
|
||||||
|
go p.readLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Program) readLoop() {
|
||||||
|
defer close(p.readLoopDone)
|
||||||
|
|
||||||
|
err := readInputs(p.ctx, p.msgs, p.cancelReader)
|
||||||
|
if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
case p.errs <- err:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForReadLoop waits for the cancelReader to finish its read loop.
|
||||||
|
func (p *Program) waitForReadLoop() {
|
||||||
|
select {
|
||||||
|
case <-p.readLoopDone:
|
||||||
|
case <-time.After(500 * time.Millisecond): //nolint:mnd
|
||||||
|
// The read loop hangs, which means the input
|
||||||
|
// cancelReader's cancel function has returned true even
|
||||||
|
// though it was not able to cancel the read.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkResize detects the current size of the output and informs the program
|
||||||
|
// via a WindowSizeMsg.
|
||||||
|
func (p *Program) checkResize() {
|
||||||
|
if p.ttyOutput == nil {
|
||||||
|
// can't query window size
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w, h, err := term.GetSize(p.ttyOutput.Fd())
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
case p.errs <- err:
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Send(WindowSizeMsg{
|
||||||
|
Width: w,
|
||||||
|
Height: h,
|
||||||
|
})
|
||||||
|
}
|
||||||
49
vendor/github.com/charmbracelet/bubbletea/tty_unix.go
generated
vendored
Normal file
49
vendor/github.com/charmbracelet/bubbletea/tty_unix.go
generated
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos
|
||||||
|
// +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos
|
||||||
|
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Program) initInput() (err error) {
|
||||||
|
// Check if input is a terminal
|
||||||
|
if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) {
|
||||||
|
p.ttyInput = f
|
||||||
|
p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error entering raw mode: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) {
|
||||||
|
p.ttyOutput = f
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openInputTTY() (*os.File, error) {
|
||||||
|
f, err := os.Open("/dev/tty")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not open a new TTY: %w", err)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const suspendSupported = true
|
||||||
|
|
||||||
|
// Send SIGTSTP to the entire process group.
|
||||||
|
func suspendProcess() {
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, syscall.SIGCONT)
|
||||||
|
_ = syscall.Kill(0, syscall.SIGTSTP)
|
||||||
|
// blocks until a CONT happens...
|
||||||
|
<-c
|
||||||
|
}
|
||||||
68
vendor/github.com/charmbracelet/bubbletea/tty_windows.go
generated
vendored
Normal file
68
vendor/github.com/charmbracelet/bubbletea/tty_windows.go
generated
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package tea
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/term"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Program) initInput() (err error) {
|
||||||
|
// Save stdin state and enable VT input
|
||||||
|
// We also need to enable VT
|
||||||
|
// input here.
|
||||||
|
if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) {
|
||||||
|
p.ttyInput = f
|
||||||
|
p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error making raw: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable VT input
|
||||||
|
var mode uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(p.ttyInput.Fd()), &mode); err != nil {
|
||||||
|
return fmt.Errorf("error getting console mode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := windows.SetConsoleMode(windows.Handle(p.ttyInput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil {
|
||||||
|
return fmt.Errorf("error setting console mode: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save output screen buffer state and enable VT processing.
|
||||||
|
if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) {
|
||||||
|
p.ttyOutput = f
|
||||||
|
p.previousOutputState, err = term.GetState(f.Fd())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mode uint32
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(p.ttyOutput.Fd()), &mode); err != nil {
|
||||||
|
return fmt.Errorf("error getting console mode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := windows.SetConsoleMode(windows.Handle(p.ttyOutput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil {
|
||||||
|
return fmt.Errorf("error setting console mode: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the Windows equivalent of a TTY.
|
||||||
|
func openInputTTY() (*os.File, error) {
|
||||||
|
f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error opening file: %w", err)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const suspendSupported = false
|
||||||
|
|
||||||
|
func suspendProcess() {}
|
||||||
40
vendor/github.com/charmbracelet/colorprofile/.golangci-soft.yml
generated
vendored
Normal file
40
vendor/github.com/charmbracelet/colorprofile/.golangci-soft.yml
generated
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
run:
|
||||||
|
tests: false
|
||||||
|
issues-exit-code: 0
|
||||||
|
|
||||||
|
issues:
|
||||||
|
include:
|
||||||
|
- EXC0001
|
||||||
|
- EXC0005
|
||||||
|
- EXC0011
|
||||||
|
- EXC0012
|
||||||
|
- EXC0013
|
||||||
|
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- exhaustive
|
||||||
|
- goconst
|
||||||
|
- godot
|
||||||
|
- godox
|
||||||
|
- mnd
|
||||||
|
- gomoddirectives
|
||||||
|
- goprintffuncname
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- prealloc
|
||||||
|
- wrapcheck
|
||||||
|
|
||||||
|
# disable default linters, they are already enabled in .golangci.yml
|
||||||
|
disable:
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
28
vendor/github.com/charmbracelet/colorprofile/.golangci.yml
generated
vendored
Normal file
28
vendor/github.com/charmbracelet/colorprofile/.golangci.yml
generated
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
run:
|
||||||
|
tests: false
|
||||||
|
|
||||||
|
issues:
|
||||||
|
include:
|
||||||
|
- EXC0001
|
||||||
|
- EXC0005
|
||||||
|
- EXC0011
|
||||||
|
- EXC0012
|
||||||
|
- EXC0013
|
||||||
|
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- gosec
|
||||||
|
- nilerr
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- tparallel
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- whitespace
|
||||||
6
vendor/github.com/charmbracelet/colorprofile/.goreleaser.yml
generated
vendored
Normal file
6
vendor/github.com/charmbracelet/colorprofile/.goreleaser.yml
generated
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
includes:
|
||||||
|
- from_url:
|
||||||
|
url: charmbracelet/meta/main/goreleaser-lib.yaml
|
||||||
|
|
||||||
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||||
|
|
||||||
21
vendor/github.com/charmbracelet/colorprofile/LICENSE
generated
vendored
Normal file
21
vendor/github.com/charmbracelet/colorprofile/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-2024 Charmbracelet, Inc
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
103
vendor/github.com/charmbracelet/colorprofile/README.md
generated
vendored
Normal file
103
vendor/github.com/charmbracelet/colorprofile/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# Colorprofile
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/charmbracelet/colorprofile/releases"><img src="https://img.shields.io/github/release/charmbracelet/colorprofile.svg" alt="Latest Release"></a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/charmbracelet/colorprofile?tab=doc"><img src="https://godoc.org/github.com/charmbracelet/colorprofile?status.svg" alt="GoDoc"></a>
|
||||||
|
<a href="https://github.com/charmbracelet/colorprofile/actions"><img src="https://github.com/charmbracelet/colorprofile/actions/workflows/build.yml/badge.svg" alt="Build Status"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
A simple, powerful—and at times magical—package for detecting terminal color
|
||||||
|
profiles and performing color (and CSI) degradation.
|
||||||
|
|
||||||
|
## Detecting the terminal’s color profile
|
||||||
|
|
||||||
|
Detecting the terminal’s color profile is easy.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/charmbracelet/colorprofile"
|
||||||
|
|
||||||
|
// Detect the color profile. If you’re planning on writing to stderr you'd want
|
||||||
|
// to use os.Stderr instead.
|
||||||
|
p := colorprofile.Detect(os.Stdout, os.Environ())
|
||||||
|
|
||||||
|
// Comment on the profile.
|
||||||
|
fmt.Printf("You know, your colors are quite %s.", func() string {
|
||||||
|
switch p {
|
||||||
|
case colorprofile.TrueColor:
|
||||||
|
return "fancy"
|
||||||
|
case colorprofile.ANSI256:
|
||||||
|
return "1990s fancy"
|
||||||
|
case colorprofile.ANSI:
|
||||||
|
return "normcore"
|
||||||
|
case colorprofile.Ascii:
|
||||||
|
return "ancient"
|
||||||
|
case colorprofile.NoTTY:
|
||||||
|
return "naughty!"
|
||||||
|
}
|
||||||
|
return "...IDK" // this should never happen
|
||||||
|
}())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Downsampling colors
|
||||||
|
|
||||||
|
When necessary, colors can be downsampled to a given profile, or manually
|
||||||
|
downsampled to a specific profile.
|
||||||
|
|
||||||
|
```go
|
||||||
|
p := colorprofile.Detect(os.Stdout, os.Environ())
|
||||||
|
c := color.RGBA{0x6b, 0x50, 0xff, 0xff} // #6b50ff
|
||||||
|
|
||||||
|
// Downsample to the detected profile, when necessary.
|
||||||
|
convertedColor := p.Convert(c)
|
||||||
|
|
||||||
|
// Or manually convert to a given profile.
|
||||||
|
ansi256Color := colorprofile.ANSI256.Convert(c)
|
||||||
|
ansiColor := colorprofile.ANSI.Convert(c)
|
||||||
|
noColor := colorprofile.Ascii.Convert(c)
|
||||||
|
noANSI := colorprofile.NoTTY.Convert(c)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Automatic downsampling with a Writer
|
||||||
|
|
||||||
|
You can also magically downsample colors in ANSI output, when necessary. If
|
||||||
|
output is not a TTY ANSI will be dropped entirely.
|
||||||
|
|
||||||
|
```go
|
||||||
|
myFancyANSI := "\x1b[38;2;107;80;255mCute \x1b[1;3mpuppy!!\x1b[m"
|
||||||
|
|
||||||
|
// Automatically downsample for the terminal at stdout.
|
||||||
|
w := colorprofile.NewWriter(os.Stdout, os.Environ())
|
||||||
|
fmt.Fprintf(w, myFancyANSI)
|
||||||
|
|
||||||
|
// Downsample to 4-bit ANSI.
|
||||||
|
w.Profile = colorprofile.ANSI
|
||||||
|
fmt.Fprintf(w, myFancyANSI)
|
||||||
|
|
||||||
|
// Ascii-fy, no colors.
|
||||||
|
w.Profile = colorprofile.Ascii
|
||||||
|
fmt.Fprintf(w, myFancyANSI)
|
||||||
|
|
||||||
|
// Strip ANSI altogether.
|
||||||
|
w.Profile = colorprofile.NoTTY
|
||||||
|
fmt.Fprintf(w, myFancyANSI) // not as fancy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feedback
|
||||||
|
|
||||||
|
We’d love to hear your thoughts on this project. Feel free to drop us a note!
|
||||||
|
|
||||||
|
- [Twitter](https://twitter.com/charmcli)
|
||||||
|
- [The Fediverse](https://mastodon.social/@charmcli)
|
||||||
|
- [Discord](https://charm.sh/chat)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](https://github.com/charmbracelet/bubbletea/raw/master/LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Part of [Charm](https://charm.sh).
|
||||||
|
|
||||||
|
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
|
||||||
|
|
||||||
|
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
|
||||||
287
vendor/github.com/charmbracelet/colorprofile/env.go
generated
vendored
Normal file
287
vendor/github.com/charmbracelet/colorprofile/env.go
generated
vendored
Normal file
|
|
@ -0,0 +1,287 @@
|
||||||
|
package colorprofile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/term"
|
||||||
|
"github.com/xo/terminfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Detect returns the color profile based on the terminal output, and
|
||||||
|
// environment variables. This respects NO_COLOR, CLICOLOR, and CLICOLOR_FORCE
|
||||||
|
// environment variables.
|
||||||
|
//
|
||||||
|
// The rules as follows:
|
||||||
|
// - TERM=dumb is always treated as NoTTY unless CLICOLOR_FORCE=1 is set.
|
||||||
|
// - If COLORTERM=truecolor, and the profile is not NoTTY, it gest upgraded to TrueColor.
|
||||||
|
// - Using any 256 color terminal (e.g. TERM=xterm-256color) will set the profile to ANSI256.
|
||||||
|
// - Using any color terminal (e.g. TERM=xterm-color) will set the profile to ANSI.
|
||||||
|
// - Using CLICOLOR=1 without TERM defined should be treated as ANSI if the
|
||||||
|
// output is a terminal.
|
||||||
|
// - NO_COLOR takes precedence over CLICOLOR/CLICOLOR_FORCE, and will disable
|
||||||
|
// colors but not text decoration, i.e. bold, italic, faint, etc.
|
||||||
|
//
|
||||||
|
// See https://no-color.org/ and https://bixense.com/clicolors/ for more information.
|
||||||
|
func Detect(output io.Writer, env []string) Profile {
|
||||||
|
out, ok := output.(term.File)
|
||||||
|
isatty := ok && term.IsTerminal(out.Fd())
|
||||||
|
environ := newEnviron(env)
|
||||||
|
term := environ.get("TERM")
|
||||||
|
isDumb := term == "dumb"
|
||||||
|
envp := colorProfile(isatty, environ)
|
||||||
|
if envp == TrueColor || envNoColor(environ) {
|
||||||
|
// We already know we have TrueColor, or NO_COLOR is set.
|
||||||
|
return envp
|
||||||
|
}
|
||||||
|
|
||||||
|
if isatty && !isDumb {
|
||||||
|
tip := Terminfo(term)
|
||||||
|
tmuxp := tmux(environ)
|
||||||
|
|
||||||
|
// Color profile is the maximum of env, terminfo, and tmux.
|
||||||
|
return max(envp, max(tip, tmuxp))
|
||||||
|
}
|
||||||
|
|
||||||
|
return envp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Env returns the color profile based on the terminal environment variables.
|
||||||
|
// This respects NO_COLOR, CLICOLOR, and CLICOLOR_FORCE environment variables.
|
||||||
|
//
|
||||||
|
// The rules as follows:
|
||||||
|
// - TERM=dumb is always treated as NoTTY unless CLICOLOR_FORCE=1 is set.
|
||||||
|
// - If COLORTERM=truecolor, and the profile is not NoTTY, it gest upgraded to TrueColor.
|
||||||
|
// - Using any 256 color terminal (e.g. TERM=xterm-256color) will set the profile to ANSI256.
|
||||||
|
// - Using any color terminal (e.g. TERM=xterm-color) will set the profile to ANSI.
|
||||||
|
// - Using CLICOLOR=1 without TERM defined should be treated as ANSI if the
|
||||||
|
// output is a terminal.
|
||||||
|
// - NO_COLOR takes precedence over CLICOLOR/CLICOLOR_FORCE, and will disable
|
||||||
|
// colors but not text decoration, i.e. bold, italic, faint, etc.
|
||||||
|
//
|
||||||
|
// See https://no-color.org/ and https://bixense.com/clicolors/ for more information.
|
||||||
|
func Env(env []string) (p Profile) {
|
||||||
|
return colorProfile(true, newEnviron(env))
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorProfile(isatty bool, env environ) (p Profile) {
|
||||||
|
isDumb := env.get("TERM") == "dumb"
|
||||||
|
envp := envColorProfile(env)
|
||||||
|
if !isatty || isDumb {
|
||||||
|
// Check if the output is a terminal.
|
||||||
|
// Treat dumb terminals as NoTTY
|
||||||
|
p = NoTTY
|
||||||
|
} else {
|
||||||
|
p = envp
|
||||||
|
}
|
||||||
|
|
||||||
|
if envNoColor(env) && isatty {
|
||||||
|
if p > Ascii {
|
||||||
|
p = Ascii
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliColorForced(env) {
|
||||||
|
if p < ANSI {
|
||||||
|
p = ANSI
|
||||||
|
}
|
||||||
|
if envp > p {
|
||||||
|
p = envp
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliColor(env) {
|
||||||
|
if isatty && !isDumb && p < ANSI {
|
||||||
|
p = ANSI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// envNoColor returns true if the environment variables explicitly disable color output
|
||||||
|
// by setting NO_COLOR (https://no-color.org/).
|
||||||
|
func envNoColor(env environ) bool {
|
||||||
|
noColor, _ := strconv.ParseBool(env.get("NO_COLOR"))
|
||||||
|
return noColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func cliColor(env environ) bool {
|
||||||
|
cliColor, _ := strconv.ParseBool(env.get("CLICOLOR"))
|
||||||
|
return cliColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func cliColorForced(env environ) bool {
|
||||||
|
cliColorForce, _ := strconv.ParseBool(env.get("CLICOLOR_FORCE"))
|
||||||
|
return cliColorForce
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorTerm(env environ) bool {
|
||||||
|
colorTerm := strings.ToLower(env.get("COLORTERM"))
|
||||||
|
return colorTerm == "truecolor" || colorTerm == "24bit" ||
|
||||||
|
colorTerm == "yes" || colorTerm == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
// envColorProfile returns infers the color profile from the environment.
|
||||||
|
func envColorProfile(env environ) (p Profile) {
|
||||||
|
term, ok := env.lookup("TERM")
|
||||||
|
if !ok || len(term) == 0 || term == "dumb" {
|
||||||
|
p = NoTTY
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// Use Windows API to detect color profile. Windows Terminal and
|
||||||
|
// cmd.exe don't define $TERM.
|
||||||
|
if wcp, ok := windowsColorProfile(env); ok {
|
||||||
|
p = wcp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
p = ANSI
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(term, "-")
|
||||||
|
switch parts[0] {
|
||||||
|
case "alacritty",
|
||||||
|
"contour",
|
||||||
|
"foot",
|
||||||
|
"ghostty",
|
||||||
|
"kitty",
|
||||||
|
"rio",
|
||||||
|
"st",
|
||||||
|
"wezterm":
|
||||||
|
return TrueColor
|
||||||
|
case "xterm":
|
||||||
|
if len(parts) > 1 {
|
||||||
|
switch parts[1] {
|
||||||
|
case "ghostty", "kitty":
|
||||||
|
// These terminals can be defined as xterm-TERMNAME
|
||||||
|
return TrueColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "tmux", "screen":
|
||||||
|
if p < ANSI256 {
|
||||||
|
p = ANSI256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCloudShell, _ := strconv.ParseBool(env.get("GOOGLE_CLOUD_SHELL")); isCloudShell {
|
||||||
|
return TrueColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// GNU Screen doesn't support TrueColor
|
||||||
|
// Tmux doesn't support $COLORTERM
|
||||||
|
if colorTerm(env) && !strings.HasPrefix(term, "screen") && !strings.HasPrefix(term, "tmux") {
|
||||||
|
return TrueColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(term, "256color") && p < ANSI256 {
|
||||||
|
p = ANSI256
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminfo returns the color profile based on the terminal's terminfo
|
||||||
|
// database. This relies on the Tc and RGB capabilities to determine if the
|
||||||
|
// terminal supports TrueColor.
|
||||||
|
// If term is empty or "dumb", it returns NoTTY.
|
||||||
|
func Terminfo(term string) (p Profile) {
|
||||||
|
if len(term) == 0 || term == "dumb" {
|
||||||
|
return NoTTY
|
||||||
|
}
|
||||||
|
|
||||||
|
p = ANSI
|
||||||
|
ti, err := terminfo.Load(term)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
extbools := ti.ExtBoolCapsShort()
|
||||||
|
if _, ok := extbools["Tc"]; ok {
|
||||||
|
return TrueColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := extbools["RGB"]; ok {
|
||||||
|
return TrueColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tmux returns the color profile based on `tmux info` output. Tmux supports
|
||||||
|
// overriding the terminal's color capabilities, so this function will return
|
||||||
|
// the color profile based on the tmux configuration.
|
||||||
|
func Tmux(env []string) Profile {
|
||||||
|
return tmux(newEnviron(env))
|
||||||
|
}
|
||||||
|
|
||||||
|
// tmux returns the color profile based on the tmux environment variables.
|
||||||
|
func tmux(env environ) (p Profile) {
|
||||||
|
if tmux, ok := env.lookup("TMUX"); !ok || len(tmux) == 0 {
|
||||||
|
// Not in tmux
|
||||||
|
return NoTTY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tmux has either Tc or RGB capabilities. Otherwise, return
|
||||||
|
// ANSI256.
|
||||||
|
p = ANSI256
|
||||||
|
cmd := exec.Command("tmux", "info")
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range bytes.Split(out, []byte("\n")) {
|
||||||
|
if (bytes.Contains(line, []byte("Tc")) || bytes.Contains(line, []byte("RGB"))) &&
|
||||||
|
bytes.Contains(line, []byte("true")) {
|
||||||
|
return TrueColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// environ is a map of environment variables.
|
||||||
|
type environ map[string]string
|
||||||
|
|
||||||
|
// newEnviron returns a new environment map from a slice of environment
|
||||||
|
// variables.
|
||||||
|
func newEnviron(environ []string) environ {
|
||||||
|
m := make(map[string]string, len(environ))
|
||||||
|
for _, e := range environ {
|
||||||
|
parts := strings.SplitN(e, "=", 2)
|
||||||
|
var value string
|
||||||
|
if len(parts) == 2 {
|
||||||
|
value = parts[1]
|
||||||
|
}
|
||||||
|
m[parts[0]] = value
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookup returns the value of an environment variable and a boolean indicating
|
||||||
|
// if it exists.
|
||||||
|
func (e environ) lookup(key string) (string, bool) {
|
||||||
|
v, ok := e[key]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// get returns the value of an environment variable and empty string if it
|
||||||
|
// doesn't exist.
|
||||||
|
func (e environ) get(key string) string {
|
||||||
|
v, _ := e.lookup(key)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func max[T ~byte | ~int](a, b T) T {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
8
vendor/github.com/charmbracelet/colorprofile/env_other.go
generated
vendored
Normal file
8
vendor/github.com/charmbracelet/colorprofile/env_other.go
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package colorprofile
|
||||||
|
|
||||||
|
func windowsColorProfile(map[string]string) (Profile, bool) {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
45
vendor/github.com/charmbracelet/colorprofile/env_windows.go
generated
vendored
Normal file
45
vendor/github.com/charmbracelet/colorprofile/env_windows.go
generated
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package colorprofile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
func windowsColorProfile(env map[string]string) (Profile, bool) {
|
||||||
|
if env["ConEmuANSI"] == "ON" {
|
||||||
|
return TrueColor, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(env["WT_SESSION"]) > 0 {
|
||||||
|
// Windows Terminal supports TrueColor
|
||||||
|
return TrueColor, true
|
||||||
|
}
|
||||||
|
|
||||||
|
major, _, build := windows.RtlGetNtVersionNumbers()
|
||||||
|
if build < 10586 || major < 10 {
|
||||||
|
// No ANSI support before WindowsNT 10 build 10586
|
||||||
|
if len(env["ANSICON"]) > 0 {
|
||||||
|
ansiconVer := env["ANSICON_VER"]
|
||||||
|
cv, err := strconv.Atoi(ansiconVer)
|
||||||
|
if err != nil || cv < 181 {
|
||||||
|
// No 8 bit color support before ANSICON 1.81
|
||||||
|
return ANSI, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return ANSI256, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return NoTTY, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if build < 14931 {
|
||||||
|
// No true color support before build 14931
|
||||||
|
return ANSI256, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrueColor, true
|
||||||
|
}
|
||||||
399
vendor/github.com/charmbracelet/colorprofile/profile.go
generated
vendored
Normal file
399
vendor/github.com/charmbracelet/colorprofile/profile.go
generated
vendored
Normal file
|
|
@ -0,0 +1,399 @@
|
||||||
|
package colorprofile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Profile is a color profile: NoTTY, Ascii, ANSI, ANSI256, or TrueColor.
|
||||||
|
type Profile byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NoTTY, not a terminal profile.
|
||||||
|
NoTTY Profile = iota
|
||||||
|
// Ascii, uncolored profile.
|
||||||
|
Ascii //nolint:revive
|
||||||
|
// ANSI, 4-bit color profile.
|
||||||
|
ANSI
|
||||||
|
// ANSI256, 8-bit color profile.
|
||||||
|
ANSI256
|
||||||
|
// TrueColor, 24-bit color profile.
|
||||||
|
TrueColor
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the string representation of a Profile.
|
||||||
|
func (p Profile) String() string {
|
||||||
|
switch p {
|
||||||
|
case TrueColor:
|
||||||
|
return "TrueColor"
|
||||||
|
case ANSI256:
|
||||||
|
return "ANSI256"
|
||||||
|
case ANSI:
|
||||||
|
return "ANSI"
|
||||||
|
case Ascii:
|
||||||
|
return "Ascii"
|
||||||
|
case NoTTY:
|
||||||
|
return "NoTTY"
|
||||||
|
}
|
||||||
|
return "Unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert transforms a given Color to a Color supported within the Profile.
|
||||||
|
func (p Profile) Convert(c color.Color) color.Color {
|
||||||
|
if p <= Ascii {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c := c.(type) {
|
||||||
|
case ansi.BasicColor:
|
||||||
|
return c
|
||||||
|
|
||||||
|
case ansi.ExtendedColor:
|
||||||
|
if p == ANSI {
|
||||||
|
return ansi256ToANSIColor(c)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
|
||||||
|
case ansi.TrueColor, color.Color:
|
||||||
|
h, ok := colorful.MakeColor(c)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if p != TrueColor {
|
||||||
|
ac := hexToANSI256Color(h)
|
||||||
|
if p == ANSI {
|
||||||
|
return ansi256ToANSIColor(ac)
|
||||||
|
}
|
||||||
|
return ac
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexToANSI256Color(c colorful.Color) ansi.ExtendedColor {
|
||||||
|
v2ci := func(v float64) int {
|
||||||
|
if v < 48 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v < 115 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return int((v - 35) / 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the nearest 0-based color index at 16..231
|
||||||
|
r := v2ci(c.R * 255.0) // 0..5 each
|
||||||
|
g := v2ci(c.G * 255.0)
|
||||||
|
b := v2ci(c.B * 255.0)
|
||||||
|
ci := 36*r + 6*g + b /* 0..215 */
|
||||||
|
|
||||||
|
// Calculate the represented colors back from the index
|
||||||
|
i2cv := [6]int{0, 0x5f, 0x87, 0xaf, 0xd7, 0xff}
|
||||||
|
cr := i2cv[r] // r/g/b, 0..255 each
|
||||||
|
cg := i2cv[g]
|
||||||
|
cb := i2cv[b]
|
||||||
|
|
||||||
|
// Calculate the nearest 0-based gray index at 232..255
|
||||||
|
var grayIdx int
|
||||||
|
average := (cr + cg + cb) / 3
|
||||||
|
if average > 238 {
|
||||||
|
grayIdx = 23
|
||||||
|
} else {
|
||||||
|
grayIdx = (average - 3) / 10 // 0..23
|
||||||
|
}
|
||||||
|
gv := 8 + 10*grayIdx // same value for r/g/b, 0..255
|
||||||
|
|
||||||
|
// Return the one which is nearer to the original input rgb value
|
||||||
|
c2 := colorful.Color{R: float64(cr) / 255.0, G: float64(cg) / 255.0, B: float64(cb) / 255.0}
|
||||||
|
g2 := colorful.Color{R: float64(gv) / 255.0, G: float64(gv) / 255.0, B: float64(gv) / 255.0}
|
||||||
|
colorDist := c.DistanceHSLuv(c2)
|
||||||
|
grayDist := c.DistanceHSLuv(g2)
|
||||||
|
|
||||||
|
if colorDist <= grayDist {
|
||||||
|
return ansi.ExtendedColor(16 + ci) //nolint:gosec
|
||||||
|
}
|
||||||
|
return ansi.ExtendedColor(232 + grayIdx) //nolint:gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
func ansi256ToANSIColor(c ansi.ExtendedColor) ansi.BasicColor {
|
||||||
|
var r int
|
||||||
|
md := math.MaxFloat64
|
||||||
|
|
||||||
|
h, _ := colorful.Hex(ansiHex[c])
|
||||||
|
for i := 0; i <= 15; i++ {
|
||||||
|
hb, _ := colorful.Hex(ansiHex[i])
|
||||||
|
d := h.DistanceHSLuv(hb)
|
||||||
|
|
||||||
|
if d < md {
|
||||||
|
md = d
|
||||||
|
r = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ansi.BasicColor(r) //nolint:gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGB values of ANSI colors (0-255).
|
||||||
|
var ansiHex = []string{
|
||||||
|
"#000000",
|
||||||
|
"#800000",
|
||||||
|
"#008000",
|
||||||
|
"#808000",
|
||||||
|
"#000080",
|
||||||
|
"#800080",
|
||||||
|
"#008080",
|
||||||
|
"#c0c0c0",
|
||||||
|
"#808080",
|
||||||
|
"#ff0000",
|
||||||
|
"#00ff00",
|
||||||
|
"#ffff00",
|
||||||
|
"#0000ff",
|
||||||
|
"#ff00ff",
|
||||||
|
"#00ffff",
|
||||||
|
"#ffffff",
|
||||||
|
"#000000",
|
||||||
|
"#00005f",
|
||||||
|
"#000087",
|
||||||
|
"#0000af",
|
||||||
|
"#0000d7",
|
||||||
|
"#0000ff",
|
||||||
|
"#005f00",
|
||||||
|
"#005f5f",
|
||||||
|
"#005f87",
|
||||||
|
"#005faf",
|
||||||
|
"#005fd7",
|
||||||
|
"#005fff",
|
||||||
|
"#008700",
|
||||||
|
"#00875f",
|
||||||
|
"#008787",
|
||||||
|
"#0087af",
|
||||||
|
"#0087d7",
|
||||||
|
"#0087ff",
|
||||||
|
"#00af00",
|
||||||
|
"#00af5f",
|
||||||
|
"#00af87",
|
||||||
|
"#00afaf",
|
||||||
|
"#00afd7",
|
||||||
|
"#00afff",
|
||||||
|
"#00d700",
|
||||||
|
"#00d75f",
|
||||||
|
"#00d787",
|
||||||
|
"#00d7af",
|
||||||
|
"#00d7d7",
|
||||||
|
"#00d7ff",
|
||||||
|
"#00ff00",
|
||||||
|
"#00ff5f",
|
||||||
|
"#00ff87",
|
||||||
|
"#00ffaf",
|
||||||
|
"#00ffd7",
|
||||||
|
"#00ffff",
|
||||||
|
"#5f0000",
|
||||||
|
"#5f005f",
|
||||||
|
"#5f0087",
|
||||||
|
"#5f00af",
|
||||||
|
"#5f00d7",
|
||||||
|
"#5f00ff",
|
||||||
|
"#5f5f00",
|
||||||
|
"#5f5f5f",
|
||||||
|
"#5f5f87",
|
||||||
|
"#5f5faf",
|
||||||
|
"#5f5fd7",
|
||||||
|
"#5f5fff",
|
||||||
|
"#5f8700",
|
||||||
|
"#5f875f",
|
||||||
|
"#5f8787",
|
||||||
|
"#5f87af",
|
||||||
|
"#5f87d7",
|
||||||
|
"#5f87ff",
|
||||||
|
"#5faf00",
|
||||||
|
"#5faf5f",
|
||||||
|
"#5faf87",
|
||||||
|
"#5fafaf",
|
||||||
|
"#5fafd7",
|
||||||
|
"#5fafff",
|
||||||
|
"#5fd700",
|
||||||
|
"#5fd75f",
|
||||||
|
"#5fd787",
|
||||||
|
"#5fd7af",
|
||||||
|
"#5fd7d7",
|
||||||
|
"#5fd7ff",
|
||||||
|
"#5fff00",
|
||||||
|
"#5fff5f",
|
||||||
|
"#5fff87",
|
||||||
|
"#5fffaf",
|
||||||
|
"#5fffd7",
|
||||||
|
"#5fffff",
|
||||||
|
"#870000",
|
||||||
|
"#87005f",
|
||||||
|
"#870087",
|
||||||
|
"#8700af",
|
||||||
|
"#8700d7",
|
||||||
|
"#8700ff",
|
||||||
|
"#875f00",
|
||||||
|
"#875f5f",
|
||||||
|
"#875f87",
|
||||||
|
"#875faf",
|
||||||
|
"#875fd7",
|
||||||
|
"#875fff",
|
||||||
|
"#878700",
|
||||||
|
"#87875f",
|
||||||
|
"#878787",
|
||||||
|
"#8787af",
|
||||||
|
"#8787d7",
|
||||||
|
"#8787ff",
|
||||||
|
"#87af00",
|
||||||
|
"#87af5f",
|
||||||
|
"#87af87",
|
||||||
|
"#87afaf",
|
||||||
|
"#87afd7",
|
||||||
|
"#87afff",
|
||||||
|
"#87d700",
|
||||||
|
"#87d75f",
|
||||||
|
"#87d787",
|
||||||
|
"#87d7af",
|
||||||
|
"#87d7d7",
|
||||||
|
"#87d7ff",
|
||||||
|
"#87ff00",
|
||||||
|
"#87ff5f",
|
||||||
|
"#87ff87",
|
||||||
|
"#87ffaf",
|
||||||
|
"#87ffd7",
|
||||||
|
"#87ffff",
|
||||||
|
"#af0000",
|
||||||
|
"#af005f",
|
||||||
|
"#af0087",
|
||||||
|
"#af00af",
|
||||||
|
"#af00d7",
|
||||||
|
"#af00ff",
|
||||||
|
"#af5f00",
|
||||||
|
"#af5f5f",
|
||||||
|
"#af5f87",
|
||||||
|
"#af5faf",
|
||||||
|
"#af5fd7",
|
||||||
|
"#af5fff",
|
||||||
|
"#af8700",
|
||||||
|
"#af875f",
|
||||||
|
"#af8787",
|
||||||
|
"#af87af",
|
||||||
|
"#af87d7",
|
||||||
|
"#af87ff",
|
||||||
|
"#afaf00",
|
||||||
|
"#afaf5f",
|
||||||
|
"#afaf87",
|
||||||
|
"#afafaf",
|
||||||
|
"#afafd7",
|
||||||
|
"#afafff",
|
||||||
|
"#afd700",
|
||||||
|
"#afd75f",
|
||||||
|
"#afd787",
|
||||||
|
"#afd7af",
|
||||||
|
"#afd7d7",
|
||||||
|
"#afd7ff",
|
||||||
|
"#afff00",
|
||||||
|
"#afff5f",
|
||||||
|
"#afff87",
|
||||||
|
"#afffaf",
|
||||||
|
"#afffd7",
|
||||||
|
"#afffff",
|
||||||
|
"#d70000",
|
||||||
|
"#d7005f",
|
||||||
|
"#d70087",
|
||||||
|
"#d700af",
|
||||||
|
"#d700d7",
|
||||||
|
"#d700ff",
|
||||||
|
"#d75f00",
|
||||||
|
"#d75f5f",
|
||||||
|
"#d75f87",
|
||||||
|
"#d75faf",
|
||||||
|
"#d75fd7",
|
||||||
|
"#d75fff",
|
||||||
|
"#d78700",
|
||||||
|
"#d7875f",
|
||||||
|
"#d78787",
|
||||||
|
"#d787af",
|
||||||
|
"#d787d7",
|
||||||
|
"#d787ff",
|
||||||
|
"#d7af00",
|
||||||
|
"#d7af5f",
|
||||||
|
"#d7af87",
|
||||||
|
"#d7afaf",
|
||||||
|
"#d7afd7",
|
||||||
|
"#d7afff",
|
||||||
|
"#d7d700",
|
||||||
|
"#d7d75f",
|
||||||
|
"#d7d787",
|
||||||
|
"#d7d7af",
|
||||||
|
"#d7d7d7",
|
||||||
|
"#d7d7ff",
|
||||||
|
"#d7ff00",
|
||||||
|
"#d7ff5f",
|
||||||
|
"#d7ff87",
|
||||||
|
"#d7ffaf",
|
||||||
|
"#d7ffd7",
|
||||||
|
"#d7ffff",
|
||||||
|
"#ff0000",
|
||||||
|
"#ff005f",
|
||||||
|
"#ff0087",
|
||||||
|
"#ff00af",
|
||||||
|
"#ff00d7",
|
||||||
|
"#ff00ff",
|
||||||
|
"#ff5f00",
|
||||||
|
"#ff5f5f",
|
||||||
|
"#ff5f87",
|
||||||
|
"#ff5faf",
|
||||||
|
"#ff5fd7",
|
||||||
|
"#ff5fff",
|
||||||
|
"#ff8700",
|
||||||
|
"#ff875f",
|
||||||
|
"#ff8787",
|
||||||
|
"#ff87af",
|
||||||
|
"#ff87d7",
|
||||||
|
"#ff87ff",
|
||||||
|
"#ffaf00",
|
||||||
|
"#ffaf5f",
|
||||||
|
"#ffaf87",
|
||||||
|
"#ffafaf",
|
||||||
|
"#ffafd7",
|
||||||
|
"#ffafff",
|
||||||
|
"#ffd700",
|
||||||
|
"#ffd75f",
|
||||||
|
"#ffd787",
|
||||||
|
"#ffd7af",
|
||||||
|
"#ffd7d7",
|
||||||
|
"#ffd7ff",
|
||||||
|
"#ffff00",
|
||||||
|
"#ffff5f",
|
||||||
|
"#ffff87",
|
||||||
|
"#ffffaf",
|
||||||
|
"#ffffd7",
|
||||||
|
"#ffffff",
|
||||||
|
"#080808",
|
||||||
|
"#121212",
|
||||||
|
"#1c1c1c",
|
||||||
|
"#262626",
|
||||||
|
"#303030",
|
||||||
|
"#3a3a3a",
|
||||||
|
"#444444",
|
||||||
|
"#4e4e4e",
|
||||||
|
"#585858",
|
||||||
|
"#626262",
|
||||||
|
"#6c6c6c",
|
||||||
|
"#767676",
|
||||||
|
"#808080",
|
||||||
|
"#8a8a8a",
|
||||||
|
"#949494",
|
||||||
|
"#9e9e9e",
|
||||||
|
"#a8a8a8",
|
||||||
|
"#b2b2b2",
|
||||||
|
"#bcbcbc",
|
||||||
|
"#c6c6c6",
|
||||||
|
"#d0d0d0",
|
||||||
|
"#dadada",
|
||||||
|
"#e4e4e4",
|
||||||
|
"#eeeeee",
|
||||||
|
}
|
||||||
166
vendor/github.com/charmbracelet/colorprofile/writer.go
generated
vendored
Normal file
166
vendor/github.com/charmbracelet/colorprofile/writer.go
generated
vendored
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
package colorprofile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"image/color"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewWriter creates a new color profile writer that downgrades color sequences
|
||||||
|
// based on the detected color profile.
|
||||||
|
//
|
||||||
|
// If environ is nil, it will use os.Environ() to get the environment variables.
|
||||||
|
//
|
||||||
|
// It queries the given writer to determine if it supports ANSI escape codes.
|
||||||
|
// If it does, along with the given environment variables, it will determine
|
||||||
|
// the appropriate color profile to use for color formatting.
|
||||||
|
//
|
||||||
|
// This respects the NO_COLOR, CLICOLOR, and CLICOLOR_FORCE environment variables.
|
||||||
|
func NewWriter(w io.Writer, environ []string) *Writer {
|
||||||
|
return &Writer{
|
||||||
|
Forward: w,
|
||||||
|
Profile: Detect(w, environ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer represents a color profile writer that writes ANSI sequences to the
|
||||||
|
// underlying writer.
|
||||||
|
type Writer struct {
|
||||||
|
Forward io.Writer
|
||||||
|
Profile Profile
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes the given text to the underlying writer.
|
||||||
|
func (w *Writer) Write(p []byte) (int, error) {
|
||||||
|
switch w.Profile {
|
||||||
|
case TrueColor:
|
||||||
|
return w.Forward.Write(p)
|
||||||
|
case NoTTY:
|
||||||
|
return io.WriteString(w.Forward, ansi.Strip(string(p)))
|
||||||
|
default:
|
||||||
|
return w.downsample(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// downsample downgrades the given text to the appropriate color profile.
|
||||||
|
func (w *Writer) downsample(p []byte) (int, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var state byte
|
||||||
|
|
||||||
|
parser := ansi.GetParser()
|
||||||
|
defer ansi.PutParser(parser)
|
||||||
|
|
||||||
|
for len(p) > 0 {
|
||||||
|
parser.Reset()
|
||||||
|
seq, _, read, newState := ansi.DecodeSequence(p, state, parser)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ansi.HasCsiPrefix(seq) && parser.Command() == 'm':
|
||||||
|
handleSgr(w, parser, &buf)
|
||||||
|
default:
|
||||||
|
// If we're not a style SGR sequence, just write the bytes.
|
||||||
|
if n, err := buf.Write(seq); err != nil {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p = p[read:]
|
||||||
|
state = newState
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.Forward.Write(buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteString writes the given text to the underlying writer.
|
||||||
|
func (w *Writer) WriteString(s string) (n int, err error) {
|
||||||
|
return w.Write([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSgr(w *Writer, p *ansi.Parser, buf *bytes.Buffer) {
|
||||||
|
var style ansi.Style
|
||||||
|
params := p.Params()
|
||||||
|
for i := 0; i < len(params); i++ {
|
||||||
|
param := params[i]
|
||||||
|
|
||||||
|
switch param := param.Param(0); param {
|
||||||
|
case 0:
|
||||||
|
// SGR default parameter is 0. We use an empty string to reduce the
|
||||||
|
// number of bytes written to the buffer.
|
||||||
|
style = append(style, "")
|
||||||
|
case 30, 31, 32, 33, 34, 35, 36, 37: // 8-bit foreground color
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.ForegroundColor(
|
||||||
|
w.Profile.Convert(ansi.BasicColor(param - 30))) //nolint:gosec
|
||||||
|
case 38: // 16 or 24-bit foreground color
|
||||||
|
var c color.Color
|
||||||
|
if n := ansi.ReadStyleColor(params[i:], &c); n > 0 {
|
||||||
|
i += n - 1
|
||||||
|
}
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.ForegroundColor(w.Profile.Convert(c))
|
||||||
|
case 39: // default foreground color
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.DefaultForegroundColor()
|
||||||
|
case 40, 41, 42, 43, 44, 45, 46, 47: // 8-bit background color
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.BackgroundColor(
|
||||||
|
w.Profile.Convert(ansi.BasicColor(param - 40))) //nolint:gosec
|
||||||
|
case 48: // 16 or 24-bit background color
|
||||||
|
var c color.Color
|
||||||
|
if n := ansi.ReadStyleColor(params[i:], &c); n > 0 {
|
||||||
|
i += n - 1
|
||||||
|
}
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.BackgroundColor(w.Profile.Convert(c))
|
||||||
|
case 49: // default background color
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.DefaultBackgroundColor()
|
||||||
|
case 58: // 16 or 24-bit underline color
|
||||||
|
var c color.Color
|
||||||
|
if n := ansi.ReadStyleColor(params[i:], &c); n > 0 {
|
||||||
|
i += n - 1
|
||||||
|
}
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.UnderlineColor(w.Profile.Convert(c))
|
||||||
|
case 59: // default underline color
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.DefaultUnderlineColor()
|
||||||
|
case 90, 91, 92, 93, 94, 95, 96, 97: // 8-bit bright foreground color
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.ForegroundColor(
|
||||||
|
w.Profile.Convert(ansi.BasicColor(param - 90 + 8))) //nolint:gosec
|
||||||
|
case 100, 101, 102, 103, 104, 105, 106, 107: // 8-bit bright background color
|
||||||
|
if w.Profile < ANSI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style = style.BackgroundColor(
|
||||||
|
w.Profile.Convert(ansi.BasicColor(param - 100 + 8))) //nolint:gosec
|
||||||
|
default:
|
||||||
|
// If this is not a color attribute, just append it to the style.
|
||||||
|
style = append(style, strconv.Itoa(param))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = buf.WriteString(style.String())
|
||||||
|
}
|
||||||
2
vendor/github.com/charmbracelet/lipgloss/.gitignore
generated
vendored
Normal file
2
vendor/github.com/charmbracelet/lipgloss/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ssh_example_ed25519*
|
||||||
|
dist/
|
||||||
41
vendor/github.com/charmbracelet/lipgloss/.golangci.yml
generated
vendored
Normal file
41
vendor/github.com/charmbracelet/lipgloss/.golangci.yml
generated
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
run:
|
||||||
|
tests: false
|
||||||
|
|
||||||
|
issues:
|
||||||
|
include:
|
||||||
|
- EXC0001
|
||||||
|
- EXC0005
|
||||||
|
- EXC0011
|
||||||
|
- EXC0012
|
||||||
|
- EXC0013
|
||||||
|
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- bodyclose
|
||||||
|
- exhaustive
|
||||||
|
- goconst
|
||||||
|
- godot
|
||||||
|
- godox
|
||||||
|
- gofumpt
|
||||||
|
- goimports
|
||||||
|
- gomoddirectives
|
||||||
|
- goprintffuncname
|
||||||
|
- gosec
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- nestif
|
||||||
|
- nilerr
|
||||||
|
- noctx
|
||||||
|
- nolintlint
|
||||||
|
- prealloc
|
||||||
|
- revive
|
||||||
|
- rowserrcheck
|
||||||
|
- sqlclosecheck
|
||||||
|
- tparallel
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- whitespace
|
||||||
|
- wrapcheck
|
||||||
5
vendor/github.com/charmbracelet/lipgloss/.goreleaser.yml
generated
vendored
Normal file
5
vendor/github.com/charmbracelet/lipgloss/.goreleaser.yml
generated
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||||
|
version: 2
|
||||||
|
includes:
|
||||||
|
- from_url:
|
||||||
|
url: charmbracelet/meta/main/goreleaser-lib.yaml
|
||||||
21
vendor/github.com/charmbracelet/lipgloss/LICENSE
generated
vendored
Normal file
21
vendor/github.com/charmbracelet/lipgloss/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021-2023 Charmbracelet, Inc
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
815
vendor/github.com/charmbracelet/lipgloss/README.md
generated
vendored
Normal file
815
vendor/github.com/charmbracelet/lipgloss/README.md
generated
vendored
Normal file
|
|
@ -0,0 +1,815 @@
|
||||||
|
# Lip Gloss
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="https://stuff.charm.sh/lipgloss/lipgloss-mascot-2k.png"><img width="340" alt="Lip Gloss title treatment" src="https://github.com/charmbracelet/lipgloss/assets/25087/147cadb1-4254-43ec-ae6b-8d6ca7b029a1"></a><br>
|
||||||
|
<a href="https://github.com/charmbracelet/lipgloss/releases"><img src="https://img.shields.io/github/release/charmbracelet/lipgloss.svg" alt="Latest Release"></a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
|
||||||
|
<a href="https://github.com/charmbracelet/lipgloss/actions"><img src="https://github.com/charmbracelet/lipgloss/workflows/build/badge.svg" alt="Build Status"></a>
|
||||||
|
<a href="https://www.phorm.ai/query?projectId=a0e324b6-b706-4546-b951-6671ea60c13f"><img src="https://stuff.charm.sh/misc/phorm-badge.svg" alt="phorm.ai"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Style definitions for nice terminal layouts. Built with TUIs in mind.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Table `Rows` need to be declared before `Offset` otherwise it does nothing.
|
||||||
|
|
||||||
|
### Table Borders
|
||||||
|
|
||||||
|
There are helpers to generate tables in markdown or ASCII style:
|
||||||
|
|
||||||
|
#### Markdown Table
|
||||||
|
|
||||||
|
```go
|
||||||
|
table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
| LANGUAGE | FORMAL | INFORMAL |
|
||||||
|
|----------|--------------|-----------|
|
||||||
|
| Chinese | Nǐn hǎo | Nǐ hǎo |
|
||||||
|
| French | Bonjour | Salut |
|
||||||
|
| Russian | Zdravstvuyte | Privet |
|
||||||
|
| Spanish | Hola | ¿Qué tal? |
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ASCII Table
|
||||||
|
|
||||||
|
```go
|
||||||
|
table.New().Border(lipgloss.ASCIIBorder())
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
+----------+--------------+-----------+
|
||||||
|
| LANGUAGE | FORMAL | INFORMAL |
|
||||||
|
+----------+--------------+-----------+
|
||||||
|
| Chinese | Nǐn hǎo | Nǐ hǎo |
|
||||||
|
| French | Bonjour | Salut |
|
||||||
|
| Russian | Zdravstvuyte | Privet |
|
||||||
|
| Spanish | Hola | ¿Qué tal? |
|
||||||
|
+----------+--------------+-----------+
|
||||||
|
```
|
||||||
|
|
||||||
|
For more on tables see [the docs](https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc) and [examples](https://github.com/charmbracelet/lipgloss/tree/master/examples/table).
|
||||||
|
|
||||||
|
## Rendering Lists
|
||||||
|
|
||||||
|
Lip Gloss ships with a list rendering sub-package.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/charmbracelet/lipgloss/list"
|
||||||
|
```
|
||||||
|
|
||||||
|
Define a new list.
|
||||||
|
|
||||||
|
```go
|
||||||
|
l := list.New("A", "B", "C")
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the list.
|
||||||
|
|
||||||
|
```go
|
||||||
|
fmt.Println(l)
|
||||||
|
|
||||||
|
// • A
|
||||||
|
// • B
|
||||||
|
// • C
|
||||||
|
```
|
||||||
|
|
||||||
|
Lists have the ability to nest.
|
||||||
|
|
||||||
|
```go
|
||||||
|
l := list.New(
|
||||||
|
"A", list.New("Artichoke"),
|
||||||
|
"B", list.New("Baking Flour", "Bananas", "Barley", "Bean Sprouts"),
|
||||||
|
"C", list.New("Cashew Apple", "Cashews", "Coconut Milk", "Curry Paste", "Currywurst"),
|
||||||
|
"D", list.New("Dill", "Dragonfruit", "Dried Shrimp"),
|
||||||
|
"E", list.New("Eggs"),
|
||||||
|
"F", list.New("Fish Cake", "Furikake"),
|
||||||
|
"J", list.New("Jicama"),
|
||||||
|
"K", list.New("Kohlrabi"),
|
||||||
|
"L", list.New("Leeks", "Lentils", "Licorice Root"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the list.
|
||||||
|
|
||||||
|
```go
|
||||||
|
fmt.Println(l)
|
||||||
|
```
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/0dc9f440-0748-4151-a3b0-7dcf29dfcdb0">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Lists can be customized via their enumeration function as well as using
|
||||||
|
`lipgloss.Style`s.
|
||||||
|
|
||||||
|
```go
|
||||||
|
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("99")).MarginRight(1)
|
||||||
|
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212")).MarginRight(1)
|
||||||
|
|
||||||
|
l := list.New(
|
||||||
|
"Glossier",
|
||||||
|
"Claire’s Boutique",
|
||||||
|
"Nyx",
|
||||||
|
"Mac",
|
||||||
|
"Milk",
|
||||||
|
).
|
||||||
|
Enumerator(list.Roman).
|
||||||
|
EnumeratorStyle(enumeratorStyle).
|
||||||
|
ItemStyle(itemStyle)
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the list.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img width="600" alt="List example" src="https://github.com/charmbracelet/lipgloss/assets/42545625/360494f1-57fb-4e13-bc19-0006efe01561">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
In addition to the predefined enumerators (`Arabic`, `Alphabet`, `Roman`, `Bullet`, `Tree`),
|
||||||
|
you may also define your own custom enumerator:
|
||||||
|
|
||||||
|
```go
|
||||||
|
l := list.New("Duck", "Duck", "Duck", "Duck", "Goose", "Duck", "Duck")
|
||||||
|
|
||||||
|
func DuckDuckGooseEnumerator(l list.Items, i int) string {
|
||||||
|
if l.At(i).Value() == "Goose" {
|
||||||
|
return "Honk →"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
l = l.Enumerator(DuckDuckGooseEnumerator)
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the list:
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img width="600" alt="image" src="https://github.com/charmbracelet/lipgloss/assets/42545625/157aaf30-140d-4948-9bb4-dfba46e5b87e">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
If you need, you can also build lists incrementally:
|
||||||
|
|
||||||
|
```go
|
||||||
|
l := list.New()
|
||||||
|
|
||||||
|
for i := 0; i < repeat; i++ {
|
||||||
|
l.Item("Lip Gloss")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rendering Trees
|
||||||
|
|
||||||
|
Lip Gloss ships with a tree rendering sub-package.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/charmbracelet/lipgloss/tree"
|
||||||
|
```
|
||||||
|
|
||||||
|
Define a new tree.
|
||||||
|
|
||||||
|
```go
|
||||||
|
t := tree.Root(".").
|
||||||
|
Child("A", "B", "C")
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the tree.
|
||||||
|
|
||||||
|
```go
|
||||||
|
fmt.Println(t)
|
||||||
|
|
||||||
|
// .
|
||||||
|
// ├── A
|
||||||
|
// ├── B
|
||||||
|
// └── C
|
||||||
|
```
|
||||||
|
|
||||||
|
Trees have the ability to nest.
|
||||||
|
|
||||||
|
```go
|
||||||
|
t := tree.Root(".").
|
||||||
|
Child("macOS").
|
||||||
|
Child(
|
||||||
|
tree.New().
|
||||||
|
Root("Linux").
|
||||||
|
Child("NixOS").
|
||||||
|
Child("Arch Linux (btw)").
|
||||||
|
Child("Void Linux"),
|
||||||
|
).
|
||||||
|
Child(
|
||||||
|
tree.New().
|
||||||
|
Root("BSD").
|
||||||
|
Child("FreeBSD").
|
||||||
|
Child("OpenBSD"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the tree.
|
||||||
|
|
||||||
|
```go
|
||||||
|
fmt.Println(t)
|
||||||
|
```
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img width="663" alt="Tree Example (simple)" src="https://github.com/user-attachments/assets/5ef14eb8-a5d4-4f94-8834-e15d1e714f89">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Trees can be customized via their enumeration function as well as using
|
||||||
|
`lipgloss.Style`s.
|
||||||
|
|
||||||
|
```go
|
||||||
|
enumeratorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("63")).MarginRight(1)
|
||||||
|
rootStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("35"))
|
||||||
|
itemStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("212"))
|
||||||
|
|
||||||
|
t := tree.
|
||||||
|
Root("⁜ Makeup").
|
||||||
|
Child(
|
||||||
|
"Glossier",
|
||||||
|
"Fenty Beauty",
|
||||||
|
tree.New().Child(
|
||||||
|
"Gloss Bomb Universal Lip Luminizer",
|
||||||
|
"Hot Cheeks Velour Blushlighter",
|
||||||
|
),
|
||||||
|
"Nyx",
|
||||||
|
"Mac",
|
||||||
|
"Milk",
|
||||||
|
).
|
||||||
|
Enumerator(tree.RoundedEnumerator).
|
||||||
|
EnumeratorStyle(enumeratorStyle).
|
||||||
|
RootStyle(rootStyle).
|
||||||
|
ItemStyle(itemStyle)
|
||||||
|
```
|
||||||
|
|
||||||
|
Print the tree.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img width="663" alt="Tree Example (makeup)" src="https://github.com/user-attachments/assets/06d12d87-744a-4c89-bd98-45de9094a97e">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
The predefined enumerators for trees are `DefaultEnumerator` and `RoundedEnumerator`.
|
||||||
|
|
||||||
|
If you need, you can also build trees incrementally:
|
||||||
|
|
||||||
|
```go
|
||||||
|
t := tree.New()
|
||||||
|
|
||||||
|
for i := 0; i < repeat; i++ {
|
||||||
|
t.Child("Lip Gloss")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
Why are things misaligning? Why are borders at the wrong widths?
|
||||||
|
</summary>
|
||||||
|
<p>This is most likely due to your locale and encoding, particularly with
|
||||||
|
regard to Chinese, Japanese, and Korean (for example, <code>zh_CN.UTF-8</code>
|
||||||
|
or <code>ja_JP.UTF-8</code>). The most direct way to fix this is to set
|
||||||
|
<code>RUNEWIDTH_EASTASIAN=0</code> in your environment.</p>
|
||||||
|
|
||||||
|
<p>For details see <a href="https://github.com/charmbracelet/lipgloss/issues/40">https://github.com/charmbracelet/lipgloss/issues/40.</a></p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>
|
||||||
|
Why isn't Lip Gloss displaying colors?
|
||||||
|
</summary>
|
||||||
|
<p>Lip Gloss automatically degrades colors to the best available option in the
|
||||||
|
given terminal, and if output's not a TTY it will remove color output entirely.
|
||||||
|
This is common when running tests, CI, or when piping output elsewhere.</p>
|
||||||
|
|
||||||
|
<p>If necessary, you can force a color profile in your tests with
|
||||||
|
<a href="https://pkg.go.dev/github.com/charmbracelet/lipgloss#SetColorProfile"><code>SetColorProfile</code></a>.</p>
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
lipgloss.SetColorProfile(termenv.TrueColor)
|
||||||
|
```
|
||||||
|
|
||||||
|
_Note:_ this option limits the flexibility of your application and can cause
|
||||||
|
ANSI escape codes to be output in cases where that might not be desired. Take
|
||||||
|
careful note of your use case and environment before choosing to force a color
|
||||||
|
profile.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## What about [Bubble Tea][tea]?
|
||||||
|
|
||||||
|
Lip Gloss doesn’t replace Bubble Tea. Rather, it is an excellent Bubble Tea
|
||||||
|
companion. It was designed to make assembling terminal user interface views as
|
||||||
|
simple and fun as possible so that you can focus on building your application
|
||||||
|
instead of concerning yourself with low-level layout details.
|
||||||
|
|
||||||
|
In simple terms, you can use Lip Gloss to help build your Bubble Tea views.
|
||||||
|
|
||||||
|
[tea]: https://github.com/charmbracelet/tea
|
||||||
|
|
||||||
|
## Under the Hood
|
||||||
|
|
||||||
|
Lip Gloss is built on the excellent [Termenv][termenv] and [Reflow][reflow]
|
||||||
|
libraries which deal with color and ANSI-aware text operations, respectively.
|
||||||
|
For many use cases Termenv and Reflow will be sufficient for your needs.
|
||||||
|
|
||||||
|
[termenv]: https://github.com/muesli/termenv
|
||||||
|
[reflow]: https://github.com/muesli/reflow
|
||||||
|
|
||||||
|
## Rendering Markdown
|
||||||
|
|
||||||
|
For a more document-centric rendering solution with support for things like
|
||||||
|
lists, tables, and syntax-highlighted code have a look at [Glamour][glamour],
|
||||||
|
the stylesheet-based Markdown renderer.
|
||||||
|
|
||||||
|
[glamour]: https://github.com/charmbracelet/glamour
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
See [contributing][contribute].
|
||||||
|
|
||||||
|
[contribute]: https://github.com/charmbracelet/lipgloss/contribute
|
||||||
|
|
||||||
|
## Feedback
|
||||||
|
|
||||||
|
We’d love to hear your thoughts on this project. Feel free to drop us a note!
|
||||||
|
|
||||||
|
- [Twitter](https://twitter.com/charmcli)
|
||||||
|
- [The Fediverse](https://mastodon.social/@charmcli)
|
||||||
|
- [Discord](https://charm.sh/chat)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](https://github.com/charmbracelet/lipgloss/raw/master/LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Part of [Charm](https://charm.sh).
|
||||||
|
|
||||||
|
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
|
||||||
|
|
||||||
|
Charm热爱开源 • Charm loves open source
|
||||||
|
|
||||||
|
[docs]: https://pkg.go.dev/github.com/charmbracelet/lipgloss?tab=doc
|
||||||
|
[wish]: https://github.com/charmbracelet/wish
|
||||||
|
[ssh-example]: examples/ssh
|
||||||
19
vendor/github.com/charmbracelet/lipgloss/Taskfile.yaml
generated
vendored
Normal file
19
vendor/github.com/charmbracelet/lipgloss/Taskfile.yaml
generated
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# https://taskfile.dev
|
||||||
|
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
lint:
|
||||||
|
desc: Run base linters
|
||||||
|
cmds:
|
||||||
|
- golangci-lint run
|
||||||
|
|
||||||
|
test:
|
||||||
|
desc: Run tests
|
||||||
|
cmds:
|
||||||
|
- go test ./... {{.CLI_ARGS}}
|
||||||
|
|
||||||
|
test:table:
|
||||||
|
desc: Run table tests
|
||||||
|
cmds:
|
||||||
|
- go test ./table {{.CLI_ARGS}}
|
||||||
83
vendor/github.com/charmbracelet/lipgloss/align.go
generated
vendored
Normal file
83
vendor/github.com/charmbracelet/lipgloss/align.go
generated
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Perform text alignment. If the string is multi-lined, we also make all lines
|
||||||
|
// the same width by padding them with spaces. If a termenv style is passed,
|
||||||
|
// use that to style the spaces added.
|
||||||
|
func alignTextHorizontal(str string, pos Position, width int, style *termenv.Style) string {
|
||||||
|
lines, widestLine := getLines(str)
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
for i, l := range lines {
|
||||||
|
lineWidth := ansi.StringWidth(l)
|
||||||
|
|
||||||
|
shortAmount := widestLine - lineWidth // difference from the widest line
|
||||||
|
shortAmount += max(0, width-(shortAmount+lineWidth)) // difference from the total width, if set
|
||||||
|
|
||||||
|
if shortAmount > 0 {
|
||||||
|
switch pos { //nolint:exhaustive
|
||||||
|
case Right:
|
||||||
|
s := strings.Repeat(" ", shortAmount)
|
||||||
|
if style != nil {
|
||||||
|
s = style.Styled(s)
|
||||||
|
}
|
||||||
|
l = s + l
|
||||||
|
case Center:
|
||||||
|
// Note: remainder goes on the right.
|
||||||
|
left := shortAmount / 2 //nolint:mnd
|
||||||
|
right := left + shortAmount%2 //nolint:mnd
|
||||||
|
|
||||||
|
leftSpaces := strings.Repeat(" ", left)
|
||||||
|
rightSpaces := strings.Repeat(" ", right)
|
||||||
|
|
||||||
|
if style != nil {
|
||||||
|
leftSpaces = style.Styled(leftSpaces)
|
||||||
|
rightSpaces = style.Styled(rightSpaces)
|
||||||
|
}
|
||||||
|
l = leftSpaces + l + rightSpaces
|
||||||
|
default: // Left
|
||||||
|
s := strings.Repeat(" ", shortAmount)
|
||||||
|
if style != nil {
|
||||||
|
s = style.Styled(s)
|
||||||
|
}
|
||||||
|
l += s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(l)
|
||||||
|
if i < len(lines)-1 {
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func alignTextVertical(str string, pos Position, height int, _ *termenv.Style) string {
|
||||||
|
strHeight := strings.Count(str, "\n") + 1
|
||||||
|
if height < strHeight {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pos {
|
||||||
|
case Top:
|
||||||
|
return str + strings.Repeat("\n", height-strHeight)
|
||||||
|
case Center:
|
||||||
|
topPadding, bottomPadding := (height-strHeight)/2, (height-strHeight)/2 //nolint:mnd
|
||||||
|
if strHeight+topPadding+bottomPadding > height {
|
||||||
|
topPadding--
|
||||||
|
} else if strHeight+topPadding+bottomPadding < height {
|
||||||
|
bottomPadding++
|
||||||
|
}
|
||||||
|
return strings.Repeat("\n", topPadding) + str + strings.Repeat("\n", bottomPadding)
|
||||||
|
case Bottom:
|
||||||
|
return strings.Repeat("\n", height-strHeight) + str
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
7
vendor/github.com/charmbracelet/lipgloss/ansi_unix.go
generated
vendored
Normal file
7
vendor/github.com/charmbracelet/lipgloss/ansi_unix.go
generated
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
// enableLegacyWindowsANSI is only needed on Windows.
|
||||||
|
func enableLegacyWindowsANSI() {}
|
||||||
22
vendor/github.com/charmbracelet/lipgloss/ansi_windows.go
generated
vendored
Normal file
22
vendor/github.com/charmbracelet/lipgloss/ansi_windows.go
generated
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var enableANSI sync.Once
|
||||||
|
|
||||||
|
// enableANSIColors enables support for ANSI color sequences in the Windows
|
||||||
|
// default console (cmd.exe and the PowerShell application). Note that this
|
||||||
|
// only works with Windows 10. Also note that Windows Terminal supports colors
|
||||||
|
// by default.
|
||||||
|
func enableLegacyWindowsANSI() {
|
||||||
|
enableANSI.Do(func() {
|
||||||
|
_, _ = termenv.EnableWindowsANSIConsole()
|
||||||
|
})
|
||||||
|
}
|
||||||
490
vendor/github.com/charmbracelet/lipgloss/borders.go
generated
vendored
Normal file
490
vendor/github.com/charmbracelet/lipgloss/borders.go
generated
vendored
Normal file
|
|
@ -0,0 +1,490 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Border contains a series of values which comprise the various parts of a
|
||||||
|
// border.
|
||||||
|
type Border struct {
|
||||||
|
Top string
|
||||||
|
Bottom string
|
||||||
|
Left string
|
||||||
|
Right string
|
||||||
|
TopLeft string
|
||||||
|
TopRight string
|
||||||
|
BottomLeft string
|
||||||
|
BottomRight string
|
||||||
|
MiddleLeft string
|
||||||
|
MiddleRight string
|
||||||
|
Middle string
|
||||||
|
MiddleTop string
|
||||||
|
MiddleBottom string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTopSize returns the width of the top border. If borders contain runes of
|
||||||
|
// varying widths, the widest rune is returned. If no border exists on the top
|
||||||
|
// edge, 0 is returned.
|
||||||
|
func (b Border) GetTopSize() int {
|
||||||
|
return getBorderEdgeWidth(b.TopLeft, b.Top, b.TopRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRightSize returns the width of the right border. If borders contain
|
||||||
|
// runes of varying widths, the widest rune is returned. If no border exists on
|
||||||
|
// the right edge, 0 is returned.
|
||||||
|
func (b Border) GetRightSize() int {
|
||||||
|
return getBorderEdgeWidth(b.TopRight, b.Right, b.BottomRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBottomSize returns the width of the bottom border. If borders contain
|
||||||
|
// runes of varying widths, the widest rune is returned. If no border exists on
|
||||||
|
// the bottom edge, 0 is returned.
|
||||||
|
func (b Border) GetBottomSize() int {
|
||||||
|
return getBorderEdgeWidth(b.BottomLeft, b.Bottom, b.BottomRight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLeftSize returns the width of the left border. If borders contain runes
|
||||||
|
// of varying widths, the widest rune is returned. If no border exists on the
|
||||||
|
// left edge, 0 is returned.
|
||||||
|
func (b Border) GetLeftSize() int {
|
||||||
|
return getBorderEdgeWidth(b.TopLeft, b.Left, b.BottomLeft)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBorderEdgeWidth(borderParts ...string) (maxWidth int) {
|
||||||
|
for _, piece := range borderParts {
|
||||||
|
w := maxRuneWidth(piece)
|
||||||
|
if w > maxWidth {
|
||||||
|
maxWidth = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
noBorder = Border{}
|
||||||
|
|
||||||
|
normalBorder = Border{
|
||||||
|
Top: "─",
|
||||||
|
Bottom: "─",
|
||||||
|
Left: "│",
|
||||||
|
Right: "│",
|
||||||
|
TopLeft: "┌",
|
||||||
|
TopRight: "┐",
|
||||||
|
BottomLeft: "└",
|
||||||
|
BottomRight: "┘",
|
||||||
|
MiddleLeft: "├",
|
||||||
|
MiddleRight: "┤",
|
||||||
|
Middle: "┼",
|
||||||
|
MiddleTop: "┬",
|
||||||
|
MiddleBottom: "┴",
|
||||||
|
}
|
||||||
|
|
||||||
|
roundedBorder = Border{
|
||||||
|
Top: "─",
|
||||||
|
Bottom: "─",
|
||||||
|
Left: "│",
|
||||||
|
Right: "│",
|
||||||
|
TopLeft: "╭",
|
||||||
|
TopRight: "╮",
|
||||||
|
BottomLeft: "╰",
|
||||||
|
BottomRight: "╯",
|
||||||
|
MiddleLeft: "├",
|
||||||
|
MiddleRight: "┤",
|
||||||
|
Middle: "┼",
|
||||||
|
MiddleTop: "┬",
|
||||||
|
MiddleBottom: "┴",
|
||||||
|
}
|
||||||
|
|
||||||
|
blockBorder = Border{
|
||||||
|
Top: "█",
|
||||||
|
Bottom: "█",
|
||||||
|
Left: "█",
|
||||||
|
Right: "█",
|
||||||
|
TopLeft: "█",
|
||||||
|
TopRight: "█",
|
||||||
|
BottomLeft: "█",
|
||||||
|
BottomRight: "█",
|
||||||
|
MiddleLeft: "█",
|
||||||
|
MiddleRight: "█",
|
||||||
|
Middle: "█",
|
||||||
|
MiddleTop: "█",
|
||||||
|
MiddleBottom: "█",
|
||||||
|
}
|
||||||
|
|
||||||
|
outerHalfBlockBorder = Border{
|
||||||
|
Top: "▀",
|
||||||
|
Bottom: "▄",
|
||||||
|
Left: "▌",
|
||||||
|
Right: "▐",
|
||||||
|
TopLeft: "▛",
|
||||||
|
TopRight: "▜",
|
||||||
|
BottomLeft: "▙",
|
||||||
|
BottomRight: "▟",
|
||||||
|
}
|
||||||
|
|
||||||
|
innerHalfBlockBorder = Border{
|
||||||
|
Top: "▄",
|
||||||
|
Bottom: "▀",
|
||||||
|
Left: "▐",
|
||||||
|
Right: "▌",
|
||||||
|
TopLeft: "▗",
|
||||||
|
TopRight: "▖",
|
||||||
|
BottomLeft: "▝",
|
||||||
|
BottomRight: "▘",
|
||||||
|
}
|
||||||
|
|
||||||
|
thickBorder = Border{
|
||||||
|
Top: "━",
|
||||||
|
Bottom: "━",
|
||||||
|
Left: "┃",
|
||||||
|
Right: "┃",
|
||||||
|
TopLeft: "┏",
|
||||||
|
TopRight: "┓",
|
||||||
|
BottomLeft: "┗",
|
||||||
|
BottomRight: "┛",
|
||||||
|
MiddleLeft: "┣",
|
||||||
|
MiddleRight: "┫",
|
||||||
|
Middle: "╋",
|
||||||
|
MiddleTop: "┳",
|
||||||
|
MiddleBottom: "┻",
|
||||||
|
}
|
||||||
|
|
||||||
|
doubleBorder = Border{
|
||||||
|
Top: "═",
|
||||||
|
Bottom: "═",
|
||||||
|
Left: "║",
|
||||||
|
Right: "║",
|
||||||
|
TopLeft: "╔",
|
||||||
|
TopRight: "╗",
|
||||||
|
BottomLeft: "╚",
|
||||||
|
BottomRight: "╝",
|
||||||
|
MiddleLeft: "╠",
|
||||||
|
MiddleRight: "╣",
|
||||||
|
Middle: "╬",
|
||||||
|
MiddleTop: "╦",
|
||||||
|
MiddleBottom: "╩",
|
||||||
|
}
|
||||||
|
|
||||||
|
hiddenBorder = Border{
|
||||||
|
Top: " ",
|
||||||
|
Bottom: " ",
|
||||||
|
Left: " ",
|
||||||
|
Right: " ",
|
||||||
|
TopLeft: " ",
|
||||||
|
TopRight: " ",
|
||||||
|
BottomLeft: " ",
|
||||||
|
BottomRight: " ",
|
||||||
|
MiddleLeft: " ",
|
||||||
|
MiddleRight: " ",
|
||||||
|
Middle: " ",
|
||||||
|
MiddleTop: " ",
|
||||||
|
MiddleBottom: " ",
|
||||||
|
}
|
||||||
|
|
||||||
|
markdownBorder = Border{
|
||||||
|
Top: "-",
|
||||||
|
Bottom: "-",
|
||||||
|
Left: "|",
|
||||||
|
Right: "|",
|
||||||
|
TopLeft: "|",
|
||||||
|
TopRight: "|",
|
||||||
|
BottomLeft: "|",
|
||||||
|
BottomRight: "|",
|
||||||
|
MiddleLeft: "|",
|
||||||
|
MiddleRight: "|",
|
||||||
|
Middle: "|",
|
||||||
|
MiddleTop: "|",
|
||||||
|
MiddleBottom: "|",
|
||||||
|
}
|
||||||
|
|
||||||
|
asciiBorder = Border{
|
||||||
|
Top: "-",
|
||||||
|
Bottom: "-",
|
||||||
|
Left: "|",
|
||||||
|
Right: "|",
|
||||||
|
TopLeft: "+",
|
||||||
|
TopRight: "+",
|
||||||
|
BottomLeft: "+",
|
||||||
|
BottomRight: "+",
|
||||||
|
MiddleLeft: "+",
|
||||||
|
MiddleRight: "+",
|
||||||
|
Middle: "+",
|
||||||
|
MiddleTop: "+",
|
||||||
|
MiddleBottom: "+",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NormalBorder returns a standard-type border with a normal weight and 90
|
||||||
|
// degree corners.
|
||||||
|
func NormalBorder() Border {
|
||||||
|
return normalBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundedBorder returns a border with rounded corners.
|
||||||
|
func RoundedBorder() Border {
|
||||||
|
return roundedBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockBorder returns a border that takes the whole block.
|
||||||
|
func BlockBorder() Border {
|
||||||
|
return blockBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// OuterHalfBlockBorder returns a half-block border that sits outside the frame.
|
||||||
|
func OuterHalfBlockBorder() Border {
|
||||||
|
return outerHalfBlockBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// InnerHalfBlockBorder returns a half-block border that sits inside the frame.
|
||||||
|
func InnerHalfBlockBorder() Border {
|
||||||
|
return innerHalfBlockBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThickBorder returns a border that's thicker than the one returned by
|
||||||
|
// NormalBorder.
|
||||||
|
func ThickBorder() Border {
|
||||||
|
return thickBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoubleBorder returns a border comprised of two thin strokes.
|
||||||
|
func DoubleBorder() Border {
|
||||||
|
return doubleBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// HiddenBorder returns a border that renders as a series of single-cell
|
||||||
|
// spaces. It's useful for cases when you want to remove a standard border but
|
||||||
|
// maintain layout positioning. This said, you can still apply a background
|
||||||
|
// color to a hidden border.
|
||||||
|
func HiddenBorder() Border {
|
||||||
|
return hiddenBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkdownBorder return a table border in markdown style.
|
||||||
|
//
|
||||||
|
// Make sure to disable top and bottom border for the best result. This will
|
||||||
|
// ensure that the output is valid markdown.
|
||||||
|
//
|
||||||
|
// table.New().Border(lipgloss.MarkdownBorder()).BorderTop(false).BorderBottom(false)
|
||||||
|
func MarkdownBorder() Border {
|
||||||
|
return markdownBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASCIIBorder returns a table border with ASCII characters.
|
||||||
|
func ASCIIBorder() Border {
|
||||||
|
return asciiBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Style) applyBorder(str string) string {
|
||||||
|
var (
|
||||||
|
border = s.getBorderStyle()
|
||||||
|
hasTop = s.getAsBool(borderTopKey, false)
|
||||||
|
hasRight = s.getAsBool(borderRightKey, false)
|
||||||
|
hasBottom = s.getAsBool(borderBottomKey, false)
|
||||||
|
hasLeft = s.getAsBool(borderLeftKey, false)
|
||||||
|
|
||||||
|
topFG = s.getAsColor(borderTopForegroundKey)
|
||||||
|
rightFG = s.getAsColor(borderRightForegroundKey)
|
||||||
|
bottomFG = s.getAsColor(borderBottomForegroundKey)
|
||||||
|
leftFG = s.getAsColor(borderLeftForegroundKey)
|
||||||
|
|
||||||
|
topBG = s.getAsColor(borderTopBackgroundKey)
|
||||||
|
rightBG = s.getAsColor(borderRightBackgroundKey)
|
||||||
|
bottomBG = s.getAsColor(borderBottomBackgroundKey)
|
||||||
|
leftBG = s.getAsColor(borderLeftBackgroundKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
// If a border is set and no sides have been specifically turned on or off
|
||||||
|
// render borders on all sides.
|
||||||
|
if s.implicitBorders() {
|
||||||
|
hasTop = true
|
||||||
|
hasRight = true
|
||||||
|
hasBottom = true
|
||||||
|
hasLeft = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no border is set or all borders are been disabled, abort.
|
||||||
|
if border == noBorder || (!hasTop && !hasRight && !hasBottom && !hasLeft) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
lines, width := getLines(str)
|
||||||
|
|
||||||
|
if hasLeft {
|
||||||
|
if border.Left == "" {
|
||||||
|
border.Left = " "
|
||||||
|
}
|
||||||
|
width += maxRuneWidth(border.Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasRight && border.Right == "" {
|
||||||
|
border.Right = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// If corners should be rendered but are set with the empty string, fill them
|
||||||
|
// with a single space.
|
||||||
|
if hasTop && hasLeft && border.TopLeft == "" {
|
||||||
|
border.TopLeft = " "
|
||||||
|
}
|
||||||
|
if hasTop && hasRight && border.TopRight == "" {
|
||||||
|
border.TopRight = " "
|
||||||
|
}
|
||||||
|
if hasBottom && hasLeft && border.BottomLeft == "" {
|
||||||
|
border.BottomLeft = " "
|
||||||
|
}
|
||||||
|
if hasBottom && hasRight && border.BottomRight == "" {
|
||||||
|
border.BottomRight = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out which corners we should actually be using based on which
|
||||||
|
// sides are set to show.
|
||||||
|
if hasTop {
|
||||||
|
switch {
|
||||||
|
case !hasLeft && !hasRight:
|
||||||
|
border.TopLeft = ""
|
||||||
|
border.TopRight = ""
|
||||||
|
case !hasLeft:
|
||||||
|
border.TopLeft = ""
|
||||||
|
case !hasRight:
|
||||||
|
border.TopRight = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasBottom {
|
||||||
|
switch {
|
||||||
|
case !hasLeft && !hasRight:
|
||||||
|
border.BottomLeft = ""
|
||||||
|
border.BottomRight = ""
|
||||||
|
case !hasLeft:
|
||||||
|
border.BottomLeft = ""
|
||||||
|
case !hasRight:
|
||||||
|
border.BottomRight = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, limit corners to one rune.
|
||||||
|
border.TopLeft = getFirstRuneAsString(border.TopLeft)
|
||||||
|
border.TopRight = getFirstRuneAsString(border.TopRight)
|
||||||
|
border.BottomRight = getFirstRuneAsString(border.BottomRight)
|
||||||
|
border.BottomLeft = getFirstRuneAsString(border.BottomLeft)
|
||||||
|
|
||||||
|
var out strings.Builder
|
||||||
|
|
||||||
|
// Render top
|
||||||
|
if hasTop {
|
||||||
|
top := renderHorizontalEdge(border.TopLeft, border.Top, border.TopRight, width)
|
||||||
|
top = s.styleBorder(top, topFG, topBG)
|
||||||
|
out.WriteString(top)
|
||||||
|
out.WriteRune('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
leftRunes := []rune(border.Left)
|
||||||
|
leftIndex := 0
|
||||||
|
|
||||||
|
rightRunes := []rune(border.Right)
|
||||||
|
rightIndex := 0
|
||||||
|
|
||||||
|
// Render sides
|
||||||
|
for i, l := range lines {
|
||||||
|
if hasLeft {
|
||||||
|
r := string(leftRunes[leftIndex])
|
||||||
|
leftIndex++
|
||||||
|
if leftIndex >= len(leftRunes) {
|
||||||
|
leftIndex = 0
|
||||||
|
}
|
||||||
|
out.WriteString(s.styleBorder(r, leftFG, leftBG))
|
||||||
|
}
|
||||||
|
out.WriteString(l)
|
||||||
|
if hasRight {
|
||||||
|
r := string(rightRunes[rightIndex])
|
||||||
|
rightIndex++
|
||||||
|
if rightIndex >= len(rightRunes) {
|
||||||
|
rightIndex = 0
|
||||||
|
}
|
||||||
|
out.WriteString(s.styleBorder(r, rightFG, rightBG))
|
||||||
|
}
|
||||||
|
if i < len(lines)-1 {
|
||||||
|
out.WriteRune('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render bottom
|
||||||
|
if hasBottom {
|
||||||
|
bottom := renderHorizontalEdge(border.BottomLeft, border.Bottom, border.BottomRight, width)
|
||||||
|
bottom = s.styleBorder(bottom, bottomFG, bottomBG)
|
||||||
|
out.WriteRune('\n')
|
||||||
|
out.WriteString(bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the horizontal (top or bottom) portion of a border.
|
||||||
|
func renderHorizontalEdge(left, middle, right string, width int) string {
|
||||||
|
if middle == "" {
|
||||||
|
middle = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
leftWidth := ansi.StringWidth(left)
|
||||||
|
rightWidth := ansi.StringWidth(right)
|
||||||
|
|
||||||
|
runes := []rune(middle)
|
||||||
|
j := 0
|
||||||
|
|
||||||
|
out := strings.Builder{}
|
||||||
|
out.WriteString(left)
|
||||||
|
for i := leftWidth + rightWidth; i < width+rightWidth; {
|
||||||
|
out.WriteRune(runes[j])
|
||||||
|
j++
|
||||||
|
if j >= len(runes) {
|
||||||
|
j = 0
|
||||||
|
}
|
||||||
|
i += ansi.StringWidth(string(runes[j]))
|
||||||
|
}
|
||||||
|
out.WriteString(right)
|
||||||
|
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply foreground and background styling to a border.
|
||||||
|
func (s Style) styleBorder(border string, fg, bg TerminalColor) string {
|
||||||
|
if fg == noColor && bg == noColor {
|
||||||
|
return border
|
||||||
|
}
|
||||||
|
|
||||||
|
style := termenv.Style{}
|
||||||
|
|
||||||
|
if fg != noColor {
|
||||||
|
style = style.Foreground(fg.color(s.r))
|
||||||
|
}
|
||||||
|
if bg != noColor {
|
||||||
|
style = style.Background(bg.color(s.r))
|
||||||
|
}
|
||||||
|
|
||||||
|
return style.Styled(border)
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxRuneWidth(str string) int {
|
||||||
|
var width int
|
||||||
|
|
||||||
|
state := -1
|
||||||
|
for len(str) > 0 {
|
||||||
|
var w int
|
||||||
|
_, str, w, state = uniseg.FirstGraphemeClusterInString(str, state)
|
||||||
|
if w > width {
|
||||||
|
width = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFirstRuneAsString(str string) string {
|
||||||
|
if str == "" {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
r := []rune(str)
|
||||||
|
return string(r[0])
|
||||||
|
}
|
||||||
172
vendor/github.com/charmbracelet/lipgloss/color.go
generated
vendored
Normal file
172
vendor/github.com/charmbracelet/lipgloss/color.go
generated
vendored
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TerminalColor is a color intended to be rendered in the terminal.
|
||||||
|
type TerminalColor interface {
|
||||||
|
color(*Renderer) termenv.Color
|
||||||
|
RGBA() (r, g, b, a uint32)
|
||||||
|
}
|
||||||
|
|
||||||
|
var noColor = NoColor{}
|
||||||
|
|
||||||
|
// NoColor is used to specify the absence of color styling. When this is active
|
||||||
|
// foreground colors will be rendered with the terminal's default text color,
|
||||||
|
// and background colors will not be drawn at all.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// var style = someStyle.Background(lipgloss.NoColor{})
|
||||||
|
type NoColor struct{}
|
||||||
|
|
||||||
|
func (NoColor) color(*Renderer) termenv.Color {
|
||||||
|
return termenv.NoColor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the RGBA value of this color. Because we have to return
|
||||||
|
// something, despite this color being the absence of color, we're returning
|
||||||
|
// black with 100% opacity.
|
||||||
|
//
|
||||||
|
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
|
||||||
|
//
|
||||||
|
// Deprecated.
|
||||||
|
func (n NoColor) RGBA() (r, g, b, a uint32) {
|
||||||
|
return 0x0, 0x0, 0x0, 0xFFFF //nolint:mnd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color specifies a color by hex or ANSI value. For example:
|
||||||
|
//
|
||||||
|
// ansiColor := lipgloss.Color("21")
|
||||||
|
// hexColor := lipgloss.Color("#0000ff")
|
||||||
|
type Color string
|
||||||
|
|
||||||
|
func (c Color) color(r *Renderer) termenv.Color {
|
||||||
|
return r.ColorProfile().Color(string(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the RGBA value of this color. This satisfies the Go Color
|
||||||
|
// interface. Note that on error we return black with 100% opacity, or:
|
||||||
|
//
|
||||||
|
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
|
||||||
|
//
|
||||||
|
// Deprecated.
|
||||||
|
func (c Color) RGBA() (r, g, b, a uint32) {
|
||||||
|
return termenv.ConvertToRGB(c.color(renderer)).RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ANSIColor is a color specified by an ANSI color value. It's merely syntactic
|
||||||
|
// sugar for the more general Color function. Invalid colors will render as
|
||||||
|
// black.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// // These two statements are equivalent.
|
||||||
|
// colorA := lipgloss.ANSIColor(21)
|
||||||
|
// colorB := lipgloss.Color("21")
|
||||||
|
type ANSIColor uint
|
||||||
|
|
||||||
|
func (ac ANSIColor) color(r *Renderer) termenv.Color {
|
||||||
|
return Color(strconv.FormatUint(uint64(ac), 10)).color(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the RGBA value of this color. This satisfies the Go Color
|
||||||
|
// interface. Note that on error we return black with 100% opacity, or:
|
||||||
|
//
|
||||||
|
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
|
||||||
|
//
|
||||||
|
// Deprecated.
|
||||||
|
func (ac ANSIColor) RGBA() (r, g, b, a uint32) {
|
||||||
|
cf := Color(strconv.FormatUint(uint64(ac), 10))
|
||||||
|
return cf.RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdaptiveColor provides color options for light and dark backgrounds. The
|
||||||
|
// appropriate color will be returned at runtime based on the darkness of the
|
||||||
|
// terminal background color.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// color := lipgloss.AdaptiveColor{Light: "#0000ff", Dark: "#000099"}
|
||||||
|
type AdaptiveColor struct {
|
||||||
|
Light string
|
||||||
|
Dark string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ac AdaptiveColor) color(r *Renderer) termenv.Color {
|
||||||
|
if r.HasDarkBackground() {
|
||||||
|
return Color(ac.Dark).color(r)
|
||||||
|
}
|
||||||
|
return Color(ac.Light).color(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the RGBA value of this color. This satisfies the Go Color
|
||||||
|
// interface. Note that on error we return black with 100% opacity, or:
|
||||||
|
//
|
||||||
|
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
|
||||||
|
//
|
||||||
|
// Deprecated.
|
||||||
|
func (ac AdaptiveColor) RGBA() (r, g, b, a uint32) {
|
||||||
|
return termenv.ConvertToRGB(ac.color(renderer)).RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteColor specifies exact values for truecolor, ANSI256, and ANSI color
|
||||||
|
// profiles. Automatic color degradation will not be performed.
|
||||||
|
type CompleteColor struct {
|
||||||
|
TrueColor string
|
||||||
|
ANSI256 string
|
||||||
|
ANSI string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c CompleteColor) color(r *Renderer) termenv.Color {
|
||||||
|
p := r.ColorProfile()
|
||||||
|
switch p { //nolint:exhaustive
|
||||||
|
case termenv.TrueColor:
|
||||||
|
return p.Color(c.TrueColor)
|
||||||
|
case termenv.ANSI256:
|
||||||
|
return p.Color(c.ANSI256)
|
||||||
|
case termenv.ANSI:
|
||||||
|
return p.Color(c.ANSI)
|
||||||
|
default:
|
||||||
|
return termenv.NoColor{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the RGBA value of this color. This satisfies the Go Color
|
||||||
|
// interface. Note that on error we return black with 100% opacity, or:
|
||||||
|
//
|
||||||
|
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
|
||||||
|
// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color
|
||||||
|
//
|
||||||
|
// Deprecated.
|
||||||
|
func (c CompleteColor) RGBA() (r, g, b, a uint32) {
|
||||||
|
return termenv.ConvertToRGB(c.color(renderer)).RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteAdaptiveColor specifies exact values for truecolor, ANSI256, and ANSI color
|
||||||
|
// profiles, with separate options for light and dark backgrounds. Automatic
|
||||||
|
// color degradation will not be performed.
|
||||||
|
type CompleteAdaptiveColor struct {
|
||||||
|
Light CompleteColor
|
||||||
|
Dark CompleteColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cac CompleteAdaptiveColor) color(r *Renderer) termenv.Color {
|
||||||
|
if r.HasDarkBackground() {
|
||||||
|
return cac.Dark.color(r)
|
||||||
|
}
|
||||||
|
return cac.Light.color(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the RGBA value of this color. This satisfies the Go Color
|
||||||
|
// interface. Note that on error we return black with 100% opacity, or:
|
||||||
|
//
|
||||||
|
// Red: 0x0, Green: 0x0, Blue: 0x0, Alpha: 0xFFFF.
|
||||||
|
//
|
||||||
|
// Deprecated.
|
||||||
|
func (cac CompleteAdaptiveColor) RGBA() (r, g, b, a uint32) {
|
||||||
|
return termenv.ConvertToRGB(cac.color(renderer)).RGBA()
|
||||||
|
}
|
||||||
556
vendor/github.com/charmbracelet/lipgloss/get.go
generated
vendored
Normal file
556
vendor/github.com/charmbracelet/lipgloss/get.go
generated
vendored
Normal file
|
|
@ -0,0 +1,556 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetBold returns the style's bold value. If no value is set false is returned.
|
||||||
|
func (s Style) GetBold() bool {
|
||||||
|
return s.getAsBool(boldKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetItalic returns the style's italic value. If no value is set false is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetItalic() bool {
|
||||||
|
return s.getAsBool(italicKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUnderline returns the style's underline value. If no value is set false is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetUnderline() bool {
|
||||||
|
return s.getAsBool(underlineKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStrikethrough returns the style's strikethrough value. If no value is set false
|
||||||
|
// is returned.
|
||||||
|
func (s Style) GetStrikethrough() bool {
|
||||||
|
return s.getAsBool(strikethroughKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReverse returns the style's reverse value. If no value is set false is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetReverse() bool {
|
||||||
|
return s.getAsBool(reverseKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlink returns the style's blink value. If no value is set false is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetBlink() bool {
|
||||||
|
return s.getAsBool(blinkKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFaint returns the style's faint value. If no value is set false is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetFaint() bool {
|
||||||
|
return s.getAsBool(faintKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetForeground returns the style's foreground color. If no value is set
|
||||||
|
// NoColor{} is returned.
|
||||||
|
func (s Style) GetForeground() TerminalColor {
|
||||||
|
return s.getAsColor(foregroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBackground returns the style's background color. If no value is set
|
||||||
|
// NoColor{} is returned.
|
||||||
|
func (s Style) GetBackground() TerminalColor {
|
||||||
|
return s.getAsColor(backgroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWidth returns the style's width setting. If no width is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetWidth() int {
|
||||||
|
return s.getAsInt(widthKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHeight returns the style's height setting. If no height is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetHeight() int {
|
||||||
|
return s.getAsInt(heightKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlign returns the style's implicit horizontal alignment setting.
|
||||||
|
// If no alignment is set Position.Left is returned.
|
||||||
|
func (s Style) GetAlign() Position {
|
||||||
|
v := s.getAsPosition(alignHorizontalKey)
|
||||||
|
if v == Position(0) {
|
||||||
|
return Left
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlignHorizontal returns the style's implicit horizontal alignment setting.
|
||||||
|
// If no alignment is set Position.Left is returned.
|
||||||
|
func (s Style) GetAlignHorizontal() Position {
|
||||||
|
v := s.getAsPosition(alignHorizontalKey)
|
||||||
|
if v == Position(0) {
|
||||||
|
return Left
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlignVertical returns the style's implicit vertical alignment setting.
|
||||||
|
// If no alignment is set Position.Top is returned.
|
||||||
|
func (s Style) GetAlignVertical() Position {
|
||||||
|
v := s.getAsPosition(alignVerticalKey)
|
||||||
|
if v == Position(0) {
|
||||||
|
return Top
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPadding returns the style's top, right, bottom, and left padding values,
|
||||||
|
// in that order. 0 is returned for unset values.
|
||||||
|
func (s Style) GetPadding() (top, right, bottom, left int) {
|
||||||
|
return s.getAsInt(paddingTopKey),
|
||||||
|
s.getAsInt(paddingRightKey),
|
||||||
|
s.getAsInt(paddingBottomKey),
|
||||||
|
s.getAsInt(paddingLeftKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPaddingTop returns the style's top padding. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetPaddingTop() int {
|
||||||
|
return s.getAsInt(paddingTopKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPaddingRight returns the style's right padding. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetPaddingRight() int {
|
||||||
|
return s.getAsInt(paddingRightKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPaddingBottom returns the style's bottom padding. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetPaddingBottom() int {
|
||||||
|
return s.getAsInt(paddingBottomKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPaddingLeft returns the style's left padding. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetPaddingLeft() int {
|
||||||
|
return s.getAsInt(paddingLeftKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHorizontalPadding returns the style's left and right padding. Unset
|
||||||
|
// values are measured as 0.
|
||||||
|
func (s Style) GetHorizontalPadding() int {
|
||||||
|
return s.getAsInt(paddingLeftKey) + s.getAsInt(paddingRightKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerticalPadding returns the style's top and bottom padding. Unset values
|
||||||
|
// are measured as 0.
|
||||||
|
func (s Style) GetVerticalPadding() int {
|
||||||
|
return s.getAsInt(paddingTopKey) + s.getAsInt(paddingBottomKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorWhitespace returns the style's whitespace coloring setting. If no
|
||||||
|
// value is set false is returned.
|
||||||
|
func (s Style) GetColorWhitespace() bool {
|
||||||
|
return s.getAsBool(colorWhitespaceKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMargin returns the style's top, right, bottom, and left margins, in that
|
||||||
|
// order. 0 is returned for unset values.
|
||||||
|
func (s Style) GetMargin() (top, right, bottom, left int) {
|
||||||
|
return s.getAsInt(marginTopKey),
|
||||||
|
s.getAsInt(marginRightKey),
|
||||||
|
s.getAsInt(marginBottomKey),
|
||||||
|
s.getAsInt(marginLeftKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMarginTop returns the style's top margin. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetMarginTop() int {
|
||||||
|
return s.getAsInt(marginTopKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMarginRight returns the style's right margin. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetMarginRight() int {
|
||||||
|
return s.getAsInt(marginRightKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMarginBottom returns the style's bottom margin. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetMarginBottom() int {
|
||||||
|
return s.getAsInt(marginBottomKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMarginLeft returns the style's left margin. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetMarginLeft() int {
|
||||||
|
return s.getAsInt(marginLeftKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHorizontalMargins returns the style's left and right margins. Unset
|
||||||
|
// values are measured as 0.
|
||||||
|
func (s Style) GetHorizontalMargins() int {
|
||||||
|
return s.getAsInt(marginLeftKey) + s.getAsInt(marginRightKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerticalMargins returns the style's top and bottom margins. Unset values
|
||||||
|
// are measured as 0.
|
||||||
|
func (s Style) GetVerticalMargins() int {
|
||||||
|
return s.getAsInt(marginTopKey) + s.getAsInt(marginBottomKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorder returns the style's border style (type Border) and value for the
|
||||||
|
// top, right, bottom, and left in that order. If no value is set for the
|
||||||
|
// border style, Border{} is returned. For all other unset values false is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetBorder() (b Border, top, right, bottom, left bool) {
|
||||||
|
return s.getBorderStyle(),
|
||||||
|
s.getAsBool(borderTopKey, false),
|
||||||
|
s.getAsBool(borderRightKey, false),
|
||||||
|
s.getAsBool(borderBottomKey, false),
|
||||||
|
s.getAsBool(borderLeftKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderStyle returns the style's border style (type Border). If no value
|
||||||
|
// is set Border{} is returned.
|
||||||
|
func (s Style) GetBorderStyle() Border {
|
||||||
|
return s.getBorderStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderTop returns the style's top border setting. If no value is set
|
||||||
|
// false is returned.
|
||||||
|
func (s Style) GetBorderTop() bool {
|
||||||
|
return s.getAsBool(borderTopKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderRight returns the style's right border setting. If no value is set
|
||||||
|
// false is returned.
|
||||||
|
func (s Style) GetBorderRight() bool {
|
||||||
|
return s.getAsBool(borderRightKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderBottom returns the style's bottom border setting. If no value is
|
||||||
|
// set false is returned.
|
||||||
|
func (s Style) GetBorderBottom() bool {
|
||||||
|
return s.getAsBool(borderBottomKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderLeft returns the style's left border setting. If no value is
|
||||||
|
// set false is returned.
|
||||||
|
func (s Style) GetBorderLeft() bool {
|
||||||
|
return s.getAsBool(borderLeftKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderTopForeground returns the style's border top foreground color. If
|
||||||
|
// no value is set NoColor{} is returned.
|
||||||
|
func (s Style) GetBorderTopForeground() TerminalColor {
|
||||||
|
return s.getAsColor(borderTopForegroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderRightForeground returns the style's border right foreground color.
|
||||||
|
// If no value is set NoColor{} is returned.
|
||||||
|
func (s Style) GetBorderRightForeground() TerminalColor {
|
||||||
|
return s.getAsColor(borderRightForegroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderBottomForeground returns the style's border bottom foreground
|
||||||
|
// color. If no value is set NoColor{} is returned.
|
||||||
|
func (s Style) GetBorderBottomForeground() TerminalColor {
|
||||||
|
return s.getAsColor(borderBottomForegroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderLeftForeground returns the style's border left foreground
|
||||||
|
// color. If no value is set NoColor{} is returned.
|
||||||
|
func (s Style) GetBorderLeftForeground() TerminalColor {
|
||||||
|
return s.getAsColor(borderLeftForegroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderTopBackground returns the style's border top background color. If
|
||||||
|
// no value is set NoColor{} is returned.
|
||||||
|
func (s Style) GetBorderTopBackground() TerminalColor {
|
||||||
|
return s.getAsColor(borderTopBackgroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderRightBackground returns the style's border right background color.
|
||||||
|
// If no value is set NoColor{} is returned.
|
||||||
|
func (s Style) GetBorderRightBackground() TerminalColor {
|
||||||
|
return s.getAsColor(borderRightBackgroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderBottomBackground returns the style's border bottom background
|
||||||
|
// color. If no value is set NoColor{} is returned.
|
||||||
|
func (s Style) GetBorderBottomBackground() TerminalColor {
|
||||||
|
return s.getAsColor(borderBottomBackgroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderLeftBackground returns the style's border left background
|
||||||
|
// color. If no value is set NoColor{} is returned.
|
||||||
|
func (s Style) GetBorderLeftBackground() TerminalColor {
|
||||||
|
return s.getAsColor(borderLeftBackgroundKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderTopWidth returns the width of the top border. If borders contain
|
||||||
|
// runes of varying widths, the widest rune is returned. If no border exists on
|
||||||
|
// the top edge, 0 is returned.
|
||||||
|
//
|
||||||
|
// Deprecated: This function simply calls Style.GetBorderTopSize.
|
||||||
|
func (s Style) GetBorderTopWidth() int {
|
||||||
|
return s.GetBorderTopSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderTopSize returns the width of the top border. If borders contain
|
||||||
|
// runes of varying widths, the widest rune is returned. If no border exists on
|
||||||
|
// the top edge, 0 is returned.
|
||||||
|
func (s Style) GetBorderTopSize() int {
|
||||||
|
if !s.getAsBool(borderTopKey, false) && !s.implicitBorders() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return s.getBorderStyle().GetTopSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderLeftSize returns the width of the left border. If borders contain
|
||||||
|
// runes of varying widths, the widest rune is returned. If no border exists on
|
||||||
|
// the left edge, 0 is returned.
|
||||||
|
func (s Style) GetBorderLeftSize() int {
|
||||||
|
if !s.getAsBool(borderLeftKey, false) && !s.implicitBorders() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return s.getBorderStyle().GetLeftSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderBottomSize returns the width of the bottom border. If borders
|
||||||
|
// contain runes of varying widths, the widest rune is returned. If no border
|
||||||
|
// exists on the left edge, 0 is returned.
|
||||||
|
func (s Style) GetBorderBottomSize() int {
|
||||||
|
if !s.getAsBool(borderBottomKey, false) && !s.implicitBorders() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return s.getBorderStyle().GetBottomSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBorderRightSize returns the width of the right border. If borders
|
||||||
|
// contain runes of varying widths, the widest rune is returned. If no border
|
||||||
|
// exists on the right edge, 0 is returned.
|
||||||
|
func (s Style) GetBorderRightSize() int {
|
||||||
|
if !s.getAsBool(borderRightKey, false) && !s.implicitBorders() {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return s.getBorderStyle().GetRightSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHorizontalBorderSize returns the width of the horizontal borders. If
|
||||||
|
// borders contain runes of varying widths, the widest rune is returned. If no
|
||||||
|
// border exists on the horizontal edges, 0 is returned.
|
||||||
|
func (s Style) GetHorizontalBorderSize() int {
|
||||||
|
return s.GetBorderLeftSize() + s.GetBorderRightSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerticalBorderSize returns the width of the vertical borders. If
|
||||||
|
// borders contain runes of varying widths, the widest rune is returned. If no
|
||||||
|
// border exists on the vertical edges, 0 is returned.
|
||||||
|
func (s Style) GetVerticalBorderSize() int {
|
||||||
|
return s.GetBorderTopSize() + s.GetBorderBottomSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInline returns the style's inline setting. If no value is set false is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetInline() bool {
|
||||||
|
return s.getAsBool(inlineKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxWidth returns the style's max width setting. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetMaxWidth() int {
|
||||||
|
return s.getAsInt(maxWidthKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxHeight returns the style's max height setting. If no value is set 0 is
|
||||||
|
// returned.
|
||||||
|
func (s Style) GetMaxHeight() int {
|
||||||
|
return s.getAsInt(maxHeightKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTabWidth returns the style's tab width setting. If no value is set 4 is
|
||||||
|
// returned which is the implicit default.
|
||||||
|
func (s Style) GetTabWidth() int {
|
||||||
|
return s.getAsInt(tabWidthKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUnderlineSpaces returns whether or not the style is set to underline
|
||||||
|
// spaces. If not value is set false is returned.
|
||||||
|
func (s Style) GetUnderlineSpaces() bool {
|
||||||
|
return s.getAsBool(underlineSpacesKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStrikethroughSpaces returns whether or not the style is set to strikethrough
|
||||||
|
// spaces. If not value is set false is returned.
|
||||||
|
func (s Style) GetStrikethroughSpaces() bool {
|
||||||
|
return s.getAsBool(strikethroughSpacesKey, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHorizontalFrameSize returns the sum of the style's horizontal margins, padding
|
||||||
|
// and border widths.
|
||||||
|
//
|
||||||
|
// Provisional: this method may be renamed.
|
||||||
|
func (s Style) GetHorizontalFrameSize() int {
|
||||||
|
return s.GetHorizontalMargins() + s.GetHorizontalPadding() + s.GetHorizontalBorderSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVerticalFrameSize returns the sum of the style's vertical margins, padding
|
||||||
|
// and border widths.
|
||||||
|
//
|
||||||
|
// Provisional: this method may be renamed.
|
||||||
|
func (s Style) GetVerticalFrameSize() int {
|
||||||
|
return s.GetVerticalMargins() + s.GetVerticalPadding() + s.GetVerticalBorderSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFrameSize returns the sum of the margins, padding and border width for
|
||||||
|
// both the horizontal and vertical margins.
|
||||||
|
func (s Style) GetFrameSize() (x, y int) {
|
||||||
|
return s.GetHorizontalFrameSize(), s.GetVerticalFrameSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransform returns the transform set on the style. If no transform is set
|
||||||
|
// nil is returned.
|
||||||
|
func (s Style) GetTransform() func(string) string {
|
||||||
|
return s.getAsTransform(transformKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns whether or not the given property is set.
|
||||||
|
func (s Style) isSet(k propKey) bool {
|
||||||
|
return s.props.has(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Style) getAsBool(k propKey, defaultVal bool) bool {
|
||||||
|
if !s.isSet(k) {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
return s.attrs&int(k) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Style) getAsColor(k propKey) TerminalColor {
|
||||||
|
if !s.isSet(k) {
|
||||||
|
return noColor
|
||||||
|
}
|
||||||
|
|
||||||
|
var c TerminalColor
|
||||||
|
switch k { //nolint:exhaustive
|
||||||
|
case foregroundKey:
|
||||||
|
c = s.fgColor
|
||||||
|
case backgroundKey:
|
||||||
|
c = s.bgColor
|
||||||
|
case marginBackgroundKey:
|
||||||
|
c = s.marginBgColor
|
||||||
|
case borderTopForegroundKey:
|
||||||
|
c = s.borderTopFgColor
|
||||||
|
case borderRightForegroundKey:
|
||||||
|
c = s.borderRightFgColor
|
||||||
|
case borderBottomForegroundKey:
|
||||||
|
c = s.borderBottomFgColor
|
||||||
|
case borderLeftForegroundKey:
|
||||||
|
c = s.borderLeftFgColor
|
||||||
|
case borderTopBackgroundKey:
|
||||||
|
c = s.borderTopBgColor
|
||||||
|
case borderRightBackgroundKey:
|
||||||
|
c = s.borderRightBgColor
|
||||||
|
case borderBottomBackgroundKey:
|
||||||
|
c = s.borderBottomBgColor
|
||||||
|
case borderLeftBackgroundKey:
|
||||||
|
c = s.borderLeftBgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if c != nil {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
return noColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Style) getAsInt(k propKey) int {
|
||||||
|
if !s.isSet(k) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch k { //nolint:exhaustive
|
||||||
|
case widthKey:
|
||||||
|
return s.width
|
||||||
|
case heightKey:
|
||||||
|
return s.height
|
||||||
|
case paddingTopKey:
|
||||||
|
return s.paddingTop
|
||||||
|
case paddingRightKey:
|
||||||
|
return s.paddingRight
|
||||||
|
case paddingBottomKey:
|
||||||
|
return s.paddingBottom
|
||||||
|
case paddingLeftKey:
|
||||||
|
return s.paddingLeft
|
||||||
|
case marginTopKey:
|
||||||
|
return s.marginTop
|
||||||
|
case marginRightKey:
|
||||||
|
return s.marginRight
|
||||||
|
case marginBottomKey:
|
||||||
|
return s.marginBottom
|
||||||
|
case marginLeftKey:
|
||||||
|
return s.marginLeft
|
||||||
|
case maxWidthKey:
|
||||||
|
return s.maxWidth
|
||||||
|
case maxHeightKey:
|
||||||
|
return s.maxHeight
|
||||||
|
case tabWidthKey:
|
||||||
|
return s.tabWidth
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Style) getAsPosition(k propKey) Position {
|
||||||
|
if !s.isSet(k) {
|
||||||
|
return Position(0)
|
||||||
|
}
|
||||||
|
switch k { //nolint:exhaustive
|
||||||
|
case alignHorizontalKey:
|
||||||
|
return s.alignHorizontal
|
||||||
|
case alignVerticalKey:
|
||||||
|
return s.alignVertical
|
||||||
|
}
|
||||||
|
return Position(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Style) getBorderStyle() Border {
|
||||||
|
if !s.isSet(borderStyleKey) {
|
||||||
|
return noBorder
|
||||||
|
}
|
||||||
|
return s.borderStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns whether or not the style has implicit borders. This happens when
|
||||||
|
// a border style has been set but no border sides have been explicitly turned
|
||||||
|
// on or off.
|
||||||
|
func (s Style) implicitBorders() bool {
|
||||||
|
var (
|
||||||
|
borderStyle = s.getBorderStyle()
|
||||||
|
topSet = s.isSet(borderTopKey)
|
||||||
|
rightSet = s.isSet(borderRightKey)
|
||||||
|
bottomSet = s.isSet(borderBottomKey)
|
||||||
|
leftSet = s.isSet(borderLeftKey)
|
||||||
|
)
|
||||||
|
return borderStyle != noBorder && !(topSet || rightSet || bottomSet || leftSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Style) getAsTransform(propKey) func(string) string {
|
||||||
|
if !s.isSet(transformKey) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.transform
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split a string into lines, additionally returning the size of the widest
|
||||||
|
// line.
|
||||||
|
func getLines(s string) (lines []string, widest int) {
|
||||||
|
lines = strings.Split(s, "\n")
|
||||||
|
|
||||||
|
for _, l := range lines {
|
||||||
|
w := ansi.StringWidth(l)
|
||||||
|
if widest < w {
|
||||||
|
widest = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines, widest
|
||||||
|
}
|
||||||
175
vendor/github.com/charmbracelet/lipgloss/join.go
generated
vendored
Normal file
175
vendor/github.com/charmbracelet/lipgloss/join.go
generated
vendored
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JoinHorizontal is a utility function for horizontally joining two
|
||||||
|
// potentially multi-lined strings along a vertical axis. The first argument is
|
||||||
|
// the position, with 0 being all the way at the top and 1 being all the way
|
||||||
|
// at the bottom.
|
||||||
|
//
|
||||||
|
// If you just want to align to the top, center or bottom you may as well just
|
||||||
|
// use the helper constants Top, Center, and Bottom.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// blockB := "...\n...\n..."
|
||||||
|
// blockA := "...\n...\n...\n...\n..."
|
||||||
|
//
|
||||||
|
// // Join 20% from the top
|
||||||
|
// str := lipgloss.JoinHorizontal(0.2, blockA, blockB)
|
||||||
|
//
|
||||||
|
// // Join on the top edge
|
||||||
|
// str := lipgloss.JoinHorizontal(lipgloss.Top, blockA, blockB)
|
||||||
|
func JoinHorizontal(pos Position, strs ...string) string {
|
||||||
|
if len(strs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(strs) == 1 {
|
||||||
|
return strs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Groups of strings broken into multiple lines
|
||||||
|
blocks = make([][]string, len(strs))
|
||||||
|
|
||||||
|
// Max line widths for the above text blocks
|
||||||
|
maxWidths = make([]int, len(strs))
|
||||||
|
|
||||||
|
// Height of the tallest block
|
||||||
|
maxHeight int
|
||||||
|
)
|
||||||
|
|
||||||
|
// Break text blocks into lines and get max widths for each text block
|
||||||
|
for i, str := range strs {
|
||||||
|
blocks[i], maxWidths[i] = getLines(str)
|
||||||
|
if len(blocks[i]) > maxHeight {
|
||||||
|
maxHeight = len(blocks[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add extra lines to make each side the same height
|
||||||
|
for i := range blocks {
|
||||||
|
if len(blocks[i]) >= maxHeight {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
extraLines := make([]string, maxHeight-len(blocks[i]))
|
||||||
|
|
||||||
|
switch pos { //nolint:exhaustive
|
||||||
|
case Top:
|
||||||
|
blocks[i] = append(blocks[i], extraLines...)
|
||||||
|
|
||||||
|
case Bottom:
|
||||||
|
blocks[i] = append(extraLines, blocks[i]...)
|
||||||
|
|
||||||
|
default: // Somewhere in the middle
|
||||||
|
n := len(extraLines)
|
||||||
|
split := int(math.Round(float64(n) * pos.value()))
|
||||||
|
top := n - split
|
||||||
|
bottom := n - top
|
||||||
|
|
||||||
|
blocks[i] = append(extraLines[top:], blocks[i]...)
|
||||||
|
blocks[i] = append(blocks[i], extraLines[bottom:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge lines
|
||||||
|
var b strings.Builder
|
||||||
|
for i := range blocks[0] { // remember, all blocks have the same number of members now
|
||||||
|
for j, block := range blocks {
|
||||||
|
b.WriteString(block[i])
|
||||||
|
|
||||||
|
// Also make lines the same length
|
||||||
|
b.WriteString(strings.Repeat(" ", maxWidths[j]-ansi.StringWidth(block[i])))
|
||||||
|
}
|
||||||
|
if i < len(blocks[0])-1 {
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// JoinVertical is a utility function for vertically joining two potentially
|
||||||
|
// multi-lined strings along a horizontal axis. The first argument is the
|
||||||
|
// position, with 0 being all the way to the left and 1 being all the way to
|
||||||
|
// the right.
|
||||||
|
//
|
||||||
|
// If you just want to align to the left, right or center you may as well just
|
||||||
|
// use the helper constants Left, Center, and Right.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// blockB := "...\n...\n..."
|
||||||
|
// blockA := "...\n...\n...\n...\n..."
|
||||||
|
//
|
||||||
|
// // Join 20% from the top
|
||||||
|
// str := lipgloss.JoinVertical(0.2, blockA, blockB)
|
||||||
|
//
|
||||||
|
// // Join on the right edge
|
||||||
|
// str := lipgloss.JoinVertical(lipgloss.Right, blockA, blockB)
|
||||||
|
func JoinVertical(pos Position, strs ...string) string {
|
||||||
|
if len(strs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(strs) == 1 {
|
||||||
|
return strs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
blocks = make([][]string, len(strs))
|
||||||
|
maxWidth int
|
||||||
|
)
|
||||||
|
|
||||||
|
for i := range strs {
|
||||||
|
var w int
|
||||||
|
blocks[i], w = getLines(strs[i])
|
||||||
|
if w > maxWidth {
|
||||||
|
maxWidth = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for i, block := range blocks {
|
||||||
|
for j, line := range block {
|
||||||
|
w := maxWidth - ansi.StringWidth(line)
|
||||||
|
|
||||||
|
switch pos { //nolint:exhaustive
|
||||||
|
case Left:
|
||||||
|
b.WriteString(line)
|
||||||
|
b.WriteString(strings.Repeat(" ", w))
|
||||||
|
|
||||||
|
case Right:
|
||||||
|
b.WriteString(strings.Repeat(" ", w))
|
||||||
|
b.WriteString(line)
|
||||||
|
|
||||||
|
default: // Somewhere in the middle
|
||||||
|
if w < 1 {
|
||||||
|
b.WriteString(line)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
split := int(math.Round(float64(w) * pos.value()))
|
||||||
|
right := w - split
|
||||||
|
left := w - right
|
||||||
|
|
||||||
|
b.WriteString(strings.Repeat(" ", left))
|
||||||
|
b.WriteString(line)
|
||||||
|
b.WriteString(strings.Repeat(" ", right))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write a newline as long as we're not on the last line of the
|
||||||
|
// last block.
|
||||||
|
if !(i == len(blocks)-1 && j == len(block)-1) {
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
154
vendor/github.com/charmbracelet/lipgloss/position.go
generated
vendored
Normal file
154
vendor/github.com/charmbracelet/lipgloss/position.go
generated
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Position represents a position along a horizontal or vertical axis. It's in
|
||||||
|
// situations where an axis is involved, like alignment, joining, placement and
|
||||||
|
// so on.
|
||||||
|
//
|
||||||
|
// A value of 0 represents the start (the left or top) and 1 represents the end
|
||||||
|
// (the right or bottom). 0.5 represents the center.
|
||||||
|
//
|
||||||
|
// There are constants Top, Bottom, Center, Left and Right in this package that
|
||||||
|
// can be used to aid readability.
|
||||||
|
type Position float64
|
||||||
|
|
||||||
|
func (p Position) value() float64 {
|
||||||
|
return math.Min(1, math.Max(0, float64(p)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position aliases.
|
||||||
|
const (
|
||||||
|
Top Position = 0.0
|
||||||
|
Bottom Position = 1.0
|
||||||
|
Center Position = 0.5
|
||||||
|
Left Position = 0.0
|
||||||
|
Right Position = 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
// Place places a string or text block vertically in an unstyled box of a given
|
||||||
|
// width or height.
|
||||||
|
func Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string {
|
||||||
|
return renderer.Place(width, height, hPos, vPos, str, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place places a string or text block vertically in an unstyled box of a given
|
||||||
|
// width or height.
|
||||||
|
func (r *Renderer) Place(width, height int, hPos, vPos Position, str string, opts ...WhitespaceOption) string {
|
||||||
|
return r.PlaceVertical(height, vPos, r.PlaceHorizontal(width, hPos, str, opts...), opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceHorizontal places a string or text block horizontally in an unstyled
|
||||||
|
// block of a given width. If the given width is shorter than the max width of
|
||||||
|
// the string (measured by its longest line) this will be a noop.
|
||||||
|
func PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string {
|
||||||
|
return renderer.PlaceHorizontal(width, pos, str, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceHorizontal places a string or text block horizontally in an unstyled
|
||||||
|
// block of a given width. If the given width is shorter than the max width of
|
||||||
|
// the string (measured by its longest line) this will be a noöp.
|
||||||
|
func (r *Renderer) PlaceHorizontal(width int, pos Position, str string, opts ...WhitespaceOption) string {
|
||||||
|
lines, contentWidth := getLines(str)
|
||||||
|
gap := width - contentWidth
|
||||||
|
|
||||||
|
if gap <= 0 {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
ws := newWhitespace(r, opts...)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for i, l := range lines {
|
||||||
|
// Is this line shorter than the longest line?
|
||||||
|
short := max(0, contentWidth-ansi.StringWidth(l))
|
||||||
|
|
||||||
|
switch pos { //nolint:exhaustive
|
||||||
|
case Left:
|
||||||
|
b.WriteString(l)
|
||||||
|
b.WriteString(ws.render(gap + short))
|
||||||
|
|
||||||
|
case Right:
|
||||||
|
b.WriteString(ws.render(gap + short))
|
||||||
|
b.WriteString(l)
|
||||||
|
|
||||||
|
default: // somewhere in the middle
|
||||||
|
totalGap := gap + short
|
||||||
|
|
||||||
|
split := int(math.Round(float64(totalGap) * pos.value()))
|
||||||
|
left := totalGap - split
|
||||||
|
right := totalGap - left
|
||||||
|
|
||||||
|
b.WriteString(ws.render(left))
|
||||||
|
b.WriteString(l)
|
||||||
|
b.WriteString(ws.render(right))
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < len(lines)-1 {
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceVertical places a string or text block vertically in an unstyled block
|
||||||
|
// of a given height. If the given height is shorter than the height of the
|
||||||
|
// string (measured by its newlines) then this will be a noop.
|
||||||
|
func PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string {
|
||||||
|
return renderer.PlaceVertical(height, pos, str, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceVertical places a string or text block vertically in an unstyled block
|
||||||
|
// of a given height. If the given height is shorter than the height of the
|
||||||
|
// string (measured by its newlines) then this will be a noöp.
|
||||||
|
func (r *Renderer) PlaceVertical(height int, pos Position, str string, opts ...WhitespaceOption) string {
|
||||||
|
contentHeight := strings.Count(str, "\n") + 1
|
||||||
|
gap := height - contentHeight
|
||||||
|
|
||||||
|
if gap <= 0 {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
ws := newWhitespace(r, opts...)
|
||||||
|
|
||||||
|
_, width := getLines(str)
|
||||||
|
emptyLine := ws.render(width)
|
||||||
|
b := strings.Builder{}
|
||||||
|
|
||||||
|
switch pos { //nolint:exhaustive
|
||||||
|
case Top:
|
||||||
|
b.WriteString(str)
|
||||||
|
b.WriteRune('\n')
|
||||||
|
for i := 0; i < gap; i++ {
|
||||||
|
b.WriteString(emptyLine)
|
||||||
|
if i < gap-1 {
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case Bottom:
|
||||||
|
b.WriteString(strings.Repeat(emptyLine+"\n", gap))
|
||||||
|
b.WriteString(str)
|
||||||
|
|
||||||
|
default: // Somewhere in the middle
|
||||||
|
split := int(math.Round(float64(gap) * pos.value()))
|
||||||
|
top := gap - split
|
||||||
|
bottom := gap - top
|
||||||
|
|
||||||
|
b.WriteString(strings.Repeat(emptyLine+"\n", top))
|
||||||
|
b.WriteString(str)
|
||||||
|
|
||||||
|
for i := 0; i < bottom; i++ {
|
||||||
|
b.WriteRune('\n')
|
||||||
|
b.WriteString(emptyLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
48
vendor/github.com/charmbracelet/lipgloss/ranges.go
generated
vendored
Normal file
48
vendor/github.com/charmbracelet/lipgloss/ranges.go
generated
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StyleRanges allows to, given a string, style ranges of it differently.
|
||||||
|
// The function will take into account existing styles.
|
||||||
|
// Ranges should not overlap.
|
||||||
|
func StyleRanges(s string, ranges ...Range) string {
|
||||||
|
if len(ranges) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf strings.Builder
|
||||||
|
lastIdx := 0
|
||||||
|
stripped := ansi.Strip(s)
|
||||||
|
|
||||||
|
// Use Truncate and TruncateLeft to style match.MatchedIndexes without
|
||||||
|
// losing the original option style:
|
||||||
|
for _, rng := range ranges {
|
||||||
|
// Add the text before this match
|
||||||
|
if rng.Start > lastIdx {
|
||||||
|
buf.WriteString(ansi.Cut(s, lastIdx, rng.Start))
|
||||||
|
}
|
||||||
|
// Add the matched range with its highlight
|
||||||
|
buf.WriteString(rng.Style.Render(ansi.Cut(stripped, rng.Start, rng.End)))
|
||||||
|
lastIdx = rng.End
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining text after the last match
|
||||||
|
buf.WriteString(ansi.TruncateLeft(s, lastIdx, ""))
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRange returns a range that can be used with [StyleRanges].
|
||||||
|
func NewRange(start, end int, style Style) Range {
|
||||||
|
return Range{start, end, style}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range to be used with [StyleRanges].
|
||||||
|
type Range struct {
|
||||||
|
Start, End int
|
||||||
|
Style Style
|
||||||
|
}
|
||||||
181
vendor/github.com/charmbracelet/lipgloss/renderer.go
generated
vendored
Normal file
181
vendor/github.com/charmbracelet/lipgloss/renderer.go
generated
vendored
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// We're manually creating the struct here to avoid initializing the output and
|
||||||
|
// query the terminal multiple times.
|
||||||
|
var renderer = &Renderer{
|
||||||
|
output: termenv.DefaultOutput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderer is a lipgloss terminal renderer.
|
||||||
|
type Renderer struct {
|
||||||
|
output *termenv.Output
|
||||||
|
colorProfile termenv.Profile
|
||||||
|
hasDarkBackground bool
|
||||||
|
|
||||||
|
getColorProfile sync.Once
|
||||||
|
explicitColorProfile bool
|
||||||
|
|
||||||
|
getBackgroundColor sync.Once
|
||||||
|
explicitBackgroundColor bool
|
||||||
|
|
||||||
|
mtx sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRenderer returns the default renderer.
|
||||||
|
func DefaultRenderer() *Renderer {
|
||||||
|
return renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaultRenderer sets the default global renderer.
|
||||||
|
func SetDefaultRenderer(r *Renderer) {
|
||||||
|
renderer = r
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRenderer creates a new Renderer.
|
||||||
|
//
|
||||||
|
// w will be used to determine the terminal's color capabilities.
|
||||||
|
func NewRenderer(w io.Writer, opts ...termenv.OutputOption) *Renderer {
|
||||||
|
r := &Renderer{
|
||||||
|
output: termenv.NewOutput(w, opts...),
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output returns the termenv output.
|
||||||
|
func (r *Renderer) Output() *termenv.Output {
|
||||||
|
r.mtx.RLock()
|
||||||
|
defer r.mtx.RUnlock()
|
||||||
|
return r.output
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetOutput sets the termenv output.
|
||||||
|
func (r *Renderer) SetOutput(o *termenv.Output) {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
r.output = o
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorProfile returns the detected termenv color profile.
|
||||||
|
func (r *Renderer) ColorProfile() termenv.Profile {
|
||||||
|
r.mtx.RLock()
|
||||||
|
defer r.mtx.RUnlock()
|
||||||
|
|
||||||
|
if !r.explicitColorProfile {
|
||||||
|
r.getColorProfile.Do(func() {
|
||||||
|
// NOTE: we don't need to lock here because sync.Once provides its
|
||||||
|
// own locking mechanism.
|
||||||
|
r.colorProfile = r.output.EnvColorProfile()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.colorProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorProfile returns the detected termenv color profile.
|
||||||
|
func ColorProfile() termenv.Profile {
|
||||||
|
return renderer.ColorProfile()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetColorProfile sets the color profile on the renderer. This function exists
|
||||||
|
// mostly for testing purposes so that you can assure you're testing against
|
||||||
|
// a specific profile.
|
||||||
|
//
|
||||||
|
// Outside of testing you likely won't want to use this function as the color
|
||||||
|
// profile will detect and cache the terminal's color capabilities and choose
|
||||||
|
// the best available profile.
|
||||||
|
//
|
||||||
|
// Available color profiles are:
|
||||||
|
//
|
||||||
|
// termenv.Ascii // no color, 1-bit
|
||||||
|
// termenv.ANSI //16 colors, 4-bit
|
||||||
|
// termenv.ANSI256 // 256 colors, 8-bit
|
||||||
|
// termenv.TrueColor // 16,777,216 colors, 24-bit
|
||||||
|
//
|
||||||
|
// This function is thread-safe.
|
||||||
|
func (r *Renderer) SetColorProfile(p termenv.Profile) {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.colorProfile = p
|
||||||
|
r.explicitColorProfile = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetColorProfile sets the color profile on the default renderer. This
|
||||||
|
// function exists mostly for testing purposes so that you can assure you're
|
||||||
|
// testing against a specific profile.
|
||||||
|
//
|
||||||
|
// Outside of testing you likely won't want to use this function as the color
|
||||||
|
// profile will detect and cache the terminal's color capabilities and choose
|
||||||
|
// the best available profile.
|
||||||
|
//
|
||||||
|
// Available color profiles are:
|
||||||
|
//
|
||||||
|
// termenv.Ascii // no color, 1-bit
|
||||||
|
// termenv.ANSI //16 colors, 4-bit
|
||||||
|
// termenv.ANSI256 // 256 colors, 8-bit
|
||||||
|
// termenv.TrueColor // 16,777,216 colors, 24-bit
|
||||||
|
//
|
||||||
|
// This function is thread-safe.
|
||||||
|
func SetColorProfile(p termenv.Profile) {
|
||||||
|
renderer.SetColorProfile(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasDarkBackground returns whether or not the terminal has a dark background.
|
||||||
|
func HasDarkBackground() bool {
|
||||||
|
return renderer.HasDarkBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasDarkBackground returns whether or not the renderer will render to a dark
|
||||||
|
// background. A dark background can either be auto-detected, or set explicitly
|
||||||
|
// on the renderer.
|
||||||
|
func (r *Renderer) HasDarkBackground() bool {
|
||||||
|
r.mtx.RLock()
|
||||||
|
defer r.mtx.RUnlock()
|
||||||
|
|
||||||
|
if !r.explicitBackgroundColor {
|
||||||
|
r.getBackgroundColor.Do(func() {
|
||||||
|
// NOTE: we don't need to lock here because sync.Once provides its
|
||||||
|
// own locking mechanism.
|
||||||
|
r.hasDarkBackground = r.output.HasDarkBackground()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.hasDarkBackground
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHasDarkBackground sets the background color detection value for the
|
||||||
|
// default renderer. This function exists mostly for testing purposes so that
|
||||||
|
// you can assure you're testing against a specific background color setting.
|
||||||
|
//
|
||||||
|
// Outside of testing you likely won't want to use this function as the
|
||||||
|
// backgrounds value will be automatically detected and cached against the
|
||||||
|
// terminal's current background color setting.
|
||||||
|
//
|
||||||
|
// This function is thread-safe.
|
||||||
|
func SetHasDarkBackground(b bool) {
|
||||||
|
renderer.SetHasDarkBackground(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHasDarkBackground sets the background color detection value on the
|
||||||
|
// renderer. This function exists mostly for testing purposes so that you can
|
||||||
|
// assure you're testing against a specific background color setting.
|
||||||
|
//
|
||||||
|
// Outside of testing you likely won't want to use this function as the
|
||||||
|
// backgrounds value will be automatically detected and cached against the
|
||||||
|
// terminal's current background color setting.
|
||||||
|
//
|
||||||
|
// This function is thread-safe.
|
||||||
|
func (r *Renderer) SetHasDarkBackground(b bool) {
|
||||||
|
r.mtx.Lock()
|
||||||
|
defer r.mtx.Unlock()
|
||||||
|
|
||||||
|
r.hasDarkBackground = b
|
||||||
|
r.explicitBackgroundColor = true
|
||||||
|
}
|
||||||
43
vendor/github.com/charmbracelet/lipgloss/runes.go
generated
vendored
Normal file
43
vendor/github.com/charmbracelet/lipgloss/runes.go
generated
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StyleRunes apply a given style to runes at the given indices in the string.
|
||||||
|
// Note that you must provide styling options for both matched and unmatched
|
||||||
|
// runes. Indices out of bounds will be ignored.
|
||||||
|
func StyleRunes(str string, indices []int, matched, unmatched Style) string {
|
||||||
|
// Convert slice of indices to a map for easier lookups
|
||||||
|
m := make(map[int]struct{})
|
||||||
|
for _, i := range indices {
|
||||||
|
m[i] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
out strings.Builder
|
||||||
|
group strings.Builder
|
||||||
|
style Style
|
||||||
|
runes = []rune(str)
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, r := range runes {
|
||||||
|
group.WriteRune(r)
|
||||||
|
|
||||||
|
_, matches := m[i]
|
||||||
|
_, nextMatches := m[i+1]
|
||||||
|
|
||||||
|
if matches != nextMatches || i == len(runes)-1 {
|
||||||
|
// Flush
|
||||||
|
if matches {
|
||||||
|
style = matched
|
||||||
|
} else {
|
||||||
|
style = unmatched
|
||||||
|
}
|
||||||
|
out.WriteString(style.Render(group.String()))
|
||||||
|
group.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
799
vendor/github.com/charmbracelet/lipgloss/set.go
generated
vendored
Normal file
799
vendor/github.com/charmbracelet/lipgloss/set.go
generated
vendored
Normal file
|
|
@ -0,0 +1,799 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
// Set a value on the underlying rules map.
|
||||||
|
func (s *Style) set(key propKey, value interface{}) {
|
||||||
|
// We don't allow negative integers on any of our other values, so just keep
|
||||||
|
// them at zero or above. We could use uints instead, but the
|
||||||
|
// conversions are a little tedious, so we're sticking with ints for
|
||||||
|
// sake of usability.
|
||||||
|
switch key { //nolint:exhaustive
|
||||||
|
case foregroundKey:
|
||||||
|
s.fgColor = colorOrNil(value)
|
||||||
|
case backgroundKey:
|
||||||
|
s.bgColor = colorOrNil(value)
|
||||||
|
case widthKey:
|
||||||
|
s.width = max(0, value.(int))
|
||||||
|
case heightKey:
|
||||||
|
s.height = max(0, value.(int))
|
||||||
|
case alignHorizontalKey:
|
||||||
|
s.alignHorizontal = value.(Position)
|
||||||
|
case alignVerticalKey:
|
||||||
|
s.alignVertical = value.(Position)
|
||||||
|
case paddingTopKey:
|
||||||
|
s.paddingTop = max(0, value.(int))
|
||||||
|
case paddingRightKey:
|
||||||
|
s.paddingRight = max(0, value.(int))
|
||||||
|
case paddingBottomKey:
|
||||||
|
s.paddingBottom = max(0, value.(int))
|
||||||
|
case paddingLeftKey:
|
||||||
|
s.paddingLeft = max(0, value.(int))
|
||||||
|
case marginTopKey:
|
||||||
|
s.marginTop = max(0, value.(int))
|
||||||
|
case marginRightKey:
|
||||||
|
s.marginRight = max(0, value.(int))
|
||||||
|
case marginBottomKey:
|
||||||
|
s.marginBottom = max(0, value.(int))
|
||||||
|
case marginLeftKey:
|
||||||
|
s.marginLeft = max(0, value.(int))
|
||||||
|
case marginBackgroundKey:
|
||||||
|
s.marginBgColor = colorOrNil(value)
|
||||||
|
case borderStyleKey:
|
||||||
|
s.borderStyle = value.(Border)
|
||||||
|
case borderTopForegroundKey:
|
||||||
|
s.borderTopFgColor = colorOrNil(value)
|
||||||
|
case borderRightForegroundKey:
|
||||||
|
s.borderRightFgColor = colorOrNil(value)
|
||||||
|
case borderBottomForegroundKey:
|
||||||
|
s.borderBottomFgColor = colorOrNil(value)
|
||||||
|
case borderLeftForegroundKey:
|
||||||
|
s.borderLeftFgColor = colorOrNil(value)
|
||||||
|
case borderTopBackgroundKey:
|
||||||
|
s.borderTopBgColor = colorOrNil(value)
|
||||||
|
case borderRightBackgroundKey:
|
||||||
|
s.borderRightBgColor = colorOrNil(value)
|
||||||
|
case borderBottomBackgroundKey:
|
||||||
|
s.borderBottomBgColor = colorOrNil(value)
|
||||||
|
case borderLeftBackgroundKey:
|
||||||
|
s.borderLeftBgColor = colorOrNil(value)
|
||||||
|
case maxWidthKey:
|
||||||
|
s.maxWidth = max(0, value.(int))
|
||||||
|
case maxHeightKey:
|
||||||
|
s.maxHeight = max(0, value.(int))
|
||||||
|
case tabWidthKey:
|
||||||
|
// TabWidth is the only property that may have a negative value (and
|
||||||
|
// that negative value can be no less than -1).
|
||||||
|
s.tabWidth = value.(int)
|
||||||
|
case transformKey:
|
||||||
|
s.transform = value.(func(string) string)
|
||||||
|
default:
|
||||||
|
if v, ok := value.(bool); ok { //nolint:nestif
|
||||||
|
if v {
|
||||||
|
s.attrs |= int(key)
|
||||||
|
} else {
|
||||||
|
s.attrs &^= int(key)
|
||||||
|
}
|
||||||
|
} else if attrs, ok := value.(int); ok {
|
||||||
|
// bool attrs
|
||||||
|
if attrs&int(key) != 0 {
|
||||||
|
s.attrs |= int(key)
|
||||||
|
} else {
|
||||||
|
s.attrs &^= int(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the prop on
|
||||||
|
s.props = s.props.set(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setFrom sets the property from another style.
|
||||||
|
func (s *Style) setFrom(key propKey, i Style) {
|
||||||
|
switch key { //nolint:exhaustive
|
||||||
|
case foregroundKey:
|
||||||
|
s.set(foregroundKey, i.fgColor)
|
||||||
|
case backgroundKey:
|
||||||
|
s.set(backgroundKey, i.bgColor)
|
||||||
|
case widthKey:
|
||||||
|
s.set(widthKey, i.width)
|
||||||
|
case heightKey:
|
||||||
|
s.set(heightKey, i.height)
|
||||||
|
case alignHorizontalKey:
|
||||||
|
s.set(alignHorizontalKey, i.alignHorizontal)
|
||||||
|
case alignVerticalKey:
|
||||||
|
s.set(alignVerticalKey, i.alignVertical)
|
||||||
|
case paddingTopKey:
|
||||||
|
s.set(paddingTopKey, i.paddingTop)
|
||||||
|
case paddingRightKey:
|
||||||
|
s.set(paddingRightKey, i.paddingRight)
|
||||||
|
case paddingBottomKey:
|
||||||
|
s.set(paddingBottomKey, i.paddingBottom)
|
||||||
|
case paddingLeftKey:
|
||||||
|
s.set(paddingLeftKey, i.paddingLeft)
|
||||||
|
case marginTopKey:
|
||||||
|
s.set(marginTopKey, i.marginTop)
|
||||||
|
case marginRightKey:
|
||||||
|
s.set(marginRightKey, i.marginRight)
|
||||||
|
case marginBottomKey:
|
||||||
|
s.set(marginBottomKey, i.marginBottom)
|
||||||
|
case marginLeftKey:
|
||||||
|
s.set(marginLeftKey, i.marginLeft)
|
||||||
|
case marginBackgroundKey:
|
||||||
|
s.set(marginBackgroundKey, i.marginBgColor)
|
||||||
|
case borderStyleKey:
|
||||||
|
s.set(borderStyleKey, i.borderStyle)
|
||||||
|
case borderTopForegroundKey:
|
||||||
|
s.set(borderTopForegroundKey, i.borderTopFgColor)
|
||||||
|
case borderRightForegroundKey:
|
||||||
|
s.set(borderRightForegroundKey, i.borderRightFgColor)
|
||||||
|
case borderBottomForegroundKey:
|
||||||
|
s.set(borderBottomForegroundKey, i.borderBottomFgColor)
|
||||||
|
case borderLeftForegroundKey:
|
||||||
|
s.set(borderLeftForegroundKey, i.borderLeftFgColor)
|
||||||
|
case borderTopBackgroundKey:
|
||||||
|
s.set(borderTopBackgroundKey, i.borderTopBgColor)
|
||||||
|
case borderRightBackgroundKey:
|
||||||
|
s.set(borderRightBackgroundKey, i.borderRightBgColor)
|
||||||
|
case borderBottomBackgroundKey:
|
||||||
|
s.set(borderBottomBackgroundKey, i.borderBottomBgColor)
|
||||||
|
case borderLeftBackgroundKey:
|
||||||
|
s.set(borderLeftBackgroundKey, i.borderLeftBgColor)
|
||||||
|
case maxWidthKey:
|
||||||
|
s.set(maxWidthKey, i.maxWidth)
|
||||||
|
case maxHeightKey:
|
||||||
|
s.set(maxHeightKey, i.maxHeight)
|
||||||
|
case tabWidthKey:
|
||||||
|
s.set(tabWidthKey, i.tabWidth)
|
||||||
|
case transformKey:
|
||||||
|
s.set(transformKey, i.transform)
|
||||||
|
default:
|
||||||
|
// Set attributes for set bool properties
|
||||||
|
s.set(key, i.attrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorOrNil(c interface{}) TerminalColor {
|
||||||
|
if c, ok := c.(TerminalColor); ok {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bold sets a bold formatting rule.
|
||||||
|
func (s Style) Bold(v bool) Style {
|
||||||
|
s.set(boldKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Italic sets an italic formatting rule. In some terminal emulators this will
|
||||||
|
// render with "reverse" coloring if not italic font variant is available.
|
||||||
|
func (s Style) Italic(v bool) Style {
|
||||||
|
s.set(italicKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Underline sets an underline rule. By default, underlines will not be drawn on
|
||||||
|
// whitespace like margins and padding. To change this behavior set
|
||||||
|
// UnderlineSpaces.
|
||||||
|
func (s Style) Underline(v bool) Style {
|
||||||
|
s.set(underlineKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strikethrough sets a strikethrough rule. By default, strikes will not be
|
||||||
|
// drawn on whitespace like margins and padding. To change this behavior set
|
||||||
|
// StrikethroughSpaces.
|
||||||
|
func (s Style) Strikethrough(v bool) Style {
|
||||||
|
s.set(strikethroughKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse sets a rule for inverting foreground and background colors.
|
||||||
|
func (s Style) Reverse(v bool) Style {
|
||||||
|
s.set(reverseKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blink sets a rule for blinking foreground text.
|
||||||
|
func (s Style) Blink(v bool) Style {
|
||||||
|
s.set(blinkKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Faint sets a rule for rendering the foreground color in a dimmer shade.
|
||||||
|
func (s Style) Faint(v bool) Style {
|
||||||
|
s.set(faintKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Foreground sets a foreground color.
|
||||||
|
//
|
||||||
|
// // Sets the foreground to blue
|
||||||
|
// s := lipgloss.NewStyle().Foreground(lipgloss.Color("#0000ff"))
|
||||||
|
//
|
||||||
|
// // Removes the foreground color
|
||||||
|
// s.Foreground(lipgloss.NoColor)
|
||||||
|
func (s Style) Foreground(c TerminalColor) Style {
|
||||||
|
s.set(foregroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background sets a background color.
|
||||||
|
func (s Style) Background(c TerminalColor) Style {
|
||||||
|
s.set(backgroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Width sets the width of the block before applying margins. The width, if
|
||||||
|
// set, also determines where text will wrap.
|
||||||
|
func (s Style) Width(i int) Style {
|
||||||
|
s.set(widthKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height sets the height of the block before applying margins. If the height of
|
||||||
|
// the text block is less than this value after applying padding (or not), the
|
||||||
|
// block will be set to this height.
|
||||||
|
func (s Style) Height(i int) Style {
|
||||||
|
s.set(heightKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align is a shorthand method for setting horizontal and vertical alignment.
|
||||||
|
//
|
||||||
|
// With one argument, the position value is applied to the horizontal alignment.
|
||||||
|
//
|
||||||
|
// With two arguments, the value is applied to the horizontal and vertical
|
||||||
|
// alignments, in that order.
|
||||||
|
func (s Style) Align(p ...Position) Style {
|
||||||
|
if len(p) > 0 {
|
||||||
|
s.set(alignHorizontalKey, p[0])
|
||||||
|
}
|
||||||
|
if len(p) > 1 {
|
||||||
|
s.set(alignVerticalKey, p[1])
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlignHorizontal sets a horizontal text alignment rule.
|
||||||
|
func (s Style) AlignHorizontal(p Position) Style {
|
||||||
|
s.set(alignHorizontalKey, p)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlignVertical sets a vertical text alignment rule.
|
||||||
|
func (s Style) AlignVertical(p Position) Style {
|
||||||
|
s.set(alignVerticalKey, p)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding is a shorthand method for setting padding on all sides at once.
|
||||||
|
//
|
||||||
|
// With one argument, the value is applied to all sides.
|
||||||
|
//
|
||||||
|
// With two arguments, the value is applied to the vertical and horizontal
|
||||||
|
// sides, in that order.
|
||||||
|
//
|
||||||
|
// With three arguments, the value is applied to the top side, the horizontal
|
||||||
|
// sides, and the bottom side, in that order.
|
||||||
|
//
|
||||||
|
// With four arguments, the value is applied clockwise starting from the top
|
||||||
|
// side, followed by the right side, then the bottom, and finally the left.
|
||||||
|
//
|
||||||
|
// With more than four arguments no padding will be added.
|
||||||
|
func (s Style) Padding(i ...int) Style {
|
||||||
|
top, right, bottom, left, ok := whichSidesInt(i...)
|
||||||
|
if !ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
s.set(paddingTopKey, top)
|
||||||
|
s.set(paddingRightKey, right)
|
||||||
|
s.set(paddingBottomKey, bottom)
|
||||||
|
s.set(paddingLeftKey, left)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaddingLeft adds padding on the left.
|
||||||
|
func (s Style) PaddingLeft(i int) Style {
|
||||||
|
s.set(paddingLeftKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaddingRight adds padding on the right.
|
||||||
|
func (s Style) PaddingRight(i int) Style {
|
||||||
|
s.set(paddingRightKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaddingTop adds padding to the top of the block.
|
||||||
|
func (s Style) PaddingTop(i int) Style {
|
||||||
|
s.set(paddingTopKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaddingBottom adds padding to the bottom of the block.
|
||||||
|
func (s Style) PaddingBottom(i int) Style {
|
||||||
|
s.set(paddingBottomKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorWhitespace determines whether or not the background color should be
|
||||||
|
// applied to the padding. This is true by default as it's more than likely the
|
||||||
|
// desired and expected behavior, but it can be disabled for certain graphic
|
||||||
|
// effects.
|
||||||
|
//
|
||||||
|
// Deprecated: Just use margins and padding.
|
||||||
|
func (s Style) ColorWhitespace(v bool) Style {
|
||||||
|
s.set(colorWhitespaceKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Margin is a shorthand method for setting margins on all sides at once.
|
||||||
|
//
|
||||||
|
// With one argument, the value is applied to all sides.
|
||||||
|
//
|
||||||
|
// With two arguments, the value is applied to the vertical and horizontal
|
||||||
|
// sides, in that order.
|
||||||
|
//
|
||||||
|
// With three arguments, the value is applied to the top side, the horizontal
|
||||||
|
// sides, and the bottom side, in that order.
|
||||||
|
//
|
||||||
|
// With four arguments, the value is applied clockwise starting from the top
|
||||||
|
// side, followed by the right side, then the bottom, and finally the left.
|
||||||
|
//
|
||||||
|
// With more than four arguments no margin will be added.
|
||||||
|
func (s Style) Margin(i ...int) Style {
|
||||||
|
top, right, bottom, left, ok := whichSidesInt(i...)
|
||||||
|
if !ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
s.set(marginTopKey, top)
|
||||||
|
s.set(marginRightKey, right)
|
||||||
|
s.set(marginBottomKey, bottom)
|
||||||
|
s.set(marginLeftKey, left)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarginLeft sets the value of the left margin.
|
||||||
|
func (s Style) MarginLeft(i int) Style {
|
||||||
|
s.set(marginLeftKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarginRight sets the value of the right margin.
|
||||||
|
func (s Style) MarginRight(i int) Style {
|
||||||
|
s.set(marginRightKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarginTop sets the value of the top margin.
|
||||||
|
func (s Style) MarginTop(i int) Style {
|
||||||
|
s.set(marginTopKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarginBottom sets the value of the bottom margin.
|
||||||
|
func (s Style) MarginBottom(i int) Style {
|
||||||
|
s.set(marginBottomKey, i)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarginBackground sets the background color of the margin. Note that this is
|
||||||
|
// also set when inheriting from a style with a background color. In that case
|
||||||
|
// the background color on that style will set the margin color on this style.
|
||||||
|
func (s Style) MarginBackground(c TerminalColor) Style {
|
||||||
|
s.set(marginBackgroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border is shorthand for setting the border style and which sides should
|
||||||
|
// have a border at once. The variadic argument sides works as follows:
|
||||||
|
//
|
||||||
|
// With one value, the value is applied to all sides.
|
||||||
|
//
|
||||||
|
// With two values, the values are applied to the vertical and horizontal
|
||||||
|
// sides, in that order.
|
||||||
|
//
|
||||||
|
// With three values, the values are applied to the top side, the horizontal
|
||||||
|
// sides, and the bottom side, in that order.
|
||||||
|
//
|
||||||
|
// With four values, the values are applied clockwise starting from the top
|
||||||
|
// side, followed by the right side, then the bottom, and finally the left.
|
||||||
|
//
|
||||||
|
// With more than four arguments the border will be applied to all sides.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// // Applies borders to the top and bottom only
|
||||||
|
// lipgloss.NewStyle().Border(lipgloss.NormalBorder(), true, false)
|
||||||
|
//
|
||||||
|
// // Applies rounded borders to the right and bottom only
|
||||||
|
// lipgloss.NewStyle().Border(lipgloss.RoundedBorder(), false, true, true, false)
|
||||||
|
func (s Style) Border(b Border, sides ...bool) Style {
|
||||||
|
s.set(borderStyleKey, b)
|
||||||
|
|
||||||
|
top, right, bottom, left, ok := whichSidesBool(sides...)
|
||||||
|
if !ok {
|
||||||
|
top = true
|
||||||
|
right = true
|
||||||
|
bottom = true
|
||||||
|
left = true
|
||||||
|
}
|
||||||
|
|
||||||
|
s.set(borderTopKey, top)
|
||||||
|
s.set(borderRightKey, right)
|
||||||
|
s.set(borderBottomKey, bottom)
|
||||||
|
s.set(borderLeftKey, left)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderStyle defines the Border on a style. A Border contains a series of
|
||||||
|
// definitions for the sides and corners of a border.
|
||||||
|
//
|
||||||
|
// Note that if border visibility has not been set for any sides when setting
|
||||||
|
// the border style, the border will be enabled for all sides during rendering.
|
||||||
|
//
|
||||||
|
// You can define border characters as you'd like, though several default
|
||||||
|
// styles are included: NormalBorder(), RoundedBorder(), BlockBorder(),
|
||||||
|
// OuterHalfBlockBorder(), InnerHalfBlockBorder(), ThickBorder(),
|
||||||
|
// and DoubleBorder().
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// lipgloss.NewStyle().BorderStyle(lipgloss.ThickBorder())
|
||||||
|
func (s Style) BorderStyle(b Border) Style {
|
||||||
|
s.set(borderStyleKey, b)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderTop determines whether or not to draw a top border.
|
||||||
|
func (s Style) BorderTop(v bool) Style {
|
||||||
|
s.set(borderTopKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderRight determines whether or not to draw a right border.
|
||||||
|
func (s Style) BorderRight(v bool) Style {
|
||||||
|
s.set(borderRightKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderBottom determines whether or not to draw a bottom border.
|
||||||
|
func (s Style) BorderBottom(v bool) Style {
|
||||||
|
s.set(borderBottomKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderLeft determines whether or not to draw a left border.
|
||||||
|
func (s Style) BorderLeft(v bool) Style {
|
||||||
|
s.set(borderLeftKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderForeground is a shorthand function for setting all of the
|
||||||
|
// foreground colors of the borders at once. The arguments work as follows:
|
||||||
|
//
|
||||||
|
// With one argument, the argument is applied to all sides.
|
||||||
|
//
|
||||||
|
// With two arguments, the arguments are applied to the vertical and horizontal
|
||||||
|
// sides, in that order.
|
||||||
|
//
|
||||||
|
// With three arguments, the arguments are applied to the top side, the
|
||||||
|
// horizontal sides, and the bottom side, in that order.
|
||||||
|
//
|
||||||
|
// With four arguments, the arguments are applied clockwise starting from the
|
||||||
|
// top side, followed by the right side, then the bottom, and finally the left.
|
||||||
|
//
|
||||||
|
// With more than four arguments nothing will be set.
|
||||||
|
func (s Style) BorderForeground(c ...TerminalColor) Style {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
top, right, bottom, left, ok := whichSidesColor(c...)
|
||||||
|
if !ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
s.set(borderTopForegroundKey, top)
|
||||||
|
s.set(borderRightForegroundKey, right)
|
||||||
|
s.set(borderBottomForegroundKey, bottom)
|
||||||
|
s.set(borderLeftForegroundKey, left)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderTopForeground set the foreground color for the top of the border.
|
||||||
|
func (s Style) BorderTopForeground(c TerminalColor) Style {
|
||||||
|
s.set(borderTopForegroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderRightForeground sets the foreground color for the right side of the
|
||||||
|
// border.
|
||||||
|
func (s Style) BorderRightForeground(c TerminalColor) Style {
|
||||||
|
s.set(borderRightForegroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderBottomForeground sets the foreground color for the bottom of the
|
||||||
|
// border.
|
||||||
|
func (s Style) BorderBottomForeground(c TerminalColor) Style {
|
||||||
|
s.set(borderBottomForegroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderLeftForeground sets the foreground color for the left side of the
|
||||||
|
// border.
|
||||||
|
func (s Style) BorderLeftForeground(c TerminalColor) Style {
|
||||||
|
s.set(borderLeftForegroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderBackground is a shorthand function for setting all of the
|
||||||
|
// background colors of the borders at once. The arguments work as follows:
|
||||||
|
//
|
||||||
|
// With one argument, the argument is applied to all sides.
|
||||||
|
//
|
||||||
|
// With two arguments, the arguments are applied to the vertical and horizontal
|
||||||
|
// sides, in that order.
|
||||||
|
//
|
||||||
|
// With three arguments, the arguments are applied to the top side, the
|
||||||
|
// horizontal sides, and the bottom side, in that order.
|
||||||
|
//
|
||||||
|
// With four arguments, the arguments are applied clockwise starting from the
|
||||||
|
// top side, followed by the right side, then the bottom, and finally the left.
|
||||||
|
//
|
||||||
|
// With more than four arguments nothing will be set.
|
||||||
|
func (s Style) BorderBackground(c ...TerminalColor) Style {
|
||||||
|
if len(c) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
top, right, bottom, left, ok := whichSidesColor(c...)
|
||||||
|
if !ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
s.set(borderTopBackgroundKey, top)
|
||||||
|
s.set(borderRightBackgroundKey, right)
|
||||||
|
s.set(borderBottomBackgroundKey, bottom)
|
||||||
|
s.set(borderLeftBackgroundKey, left)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderTopBackground sets the background color of the top of the border.
|
||||||
|
func (s Style) BorderTopBackground(c TerminalColor) Style {
|
||||||
|
s.set(borderTopBackgroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderRightBackground sets the background color of right side the border.
|
||||||
|
func (s Style) BorderRightBackground(c TerminalColor) Style {
|
||||||
|
s.set(borderRightBackgroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderBottomBackground sets the background color of the bottom of the
|
||||||
|
// border.
|
||||||
|
func (s Style) BorderBottomBackground(c TerminalColor) Style {
|
||||||
|
s.set(borderBottomBackgroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// BorderLeftBackground set the background color of the left side of the
|
||||||
|
// border.
|
||||||
|
func (s Style) BorderLeftBackground(c TerminalColor) Style {
|
||||||
|
s.set(borderLeftBackgroundKey, c)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline makes rendering output one line and disables the rendering of
|
||||||
|
// margins, padding and borders. This is useful when you need a style to apply
|
||||||
|
// only to font rendering and don't want it to change any physical dimensions.
|
||||||
|
// It works well with Style.MaxWidth.
|
||||||
|
//
|
||||||
|
// Because this in intended to be used at the time of render, this method will
|
||||||
|
// not mutate the style and instead return a copy.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// var userInput string = "..."
|
||||||
|
// var userStyle = text.Style{ /* ... */ }
|
||||||
|
// fmt.Println(userStyle.Inline(true).Render(userInput))
|
||||||
|
func (s Style) Inline(v bool) Style {
|
||||||
|
o := s // copy
|
||||||
|
o.set(inlineKey, v)
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxWidth applies a max width to a given style. This is useful in enforcing
|
||||||
|
// a certain width at render time, particularly with arbitrary strings and
|
||||||
|
// styles.
|
||||||
|
//
|
||||||
|
// Because this in intended to be used at the time of render, this method will
|
||||||
|
// not mutate the style and instead return a copy.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// var userInput string = "..."
|
||||||
|
// var userStyle = text.Style{ /* ... */ }
|
||||||
|
// fmt.Println(userStyle.MaxWidth(16).Render(userInput))
|
||||||
|
func (s Style) MaxWidth(n int) Style {
|
||||||
|
o := s // copy
|
||||||
|
o.set(maxWidthKey, n)
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaxHeight applies a max height to a given style. This is useful in enforcing
|
||||||
|
// a certain height at render time, particularly with arbitrary strings and
|
||||||
|
// styles.
|
||||||
|
//
|
||||||
|
// Because this in intended to be used at the time of render, this method will
|
||||||
|
// not mutate the style and instead returns a copy.
|
||||||
|
func (s Style) MaxHeight(n int) Style {
|
||||||
|
o := s // copy
|
||||||
|
o.set(maxHeightKey, n)
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoTabConversion can be passed to [Style.TabWidth] to disable the replacement
|
||||||
|
// of tabs with spaces at render time.
|
||||||
|
const NoTabConversion = -1
|
||||||
|
|
||||||
|
// TabWidth sets the number of spaces that a tab (/t) should be rendered as.
|
||||||
|
// When set to 0, tabs will be removed. To disable the replacement of tabs with
|
||||||
|
// spaces entirely, set this to [NoTabConversion].
|
||||||
|
//
|
||||||
|
// By default, tabs will be replaced with 4 spaces.
|
||||||
|
func (s Style) TabWidth(n int) Style {
|
||||||
|
if n <= -1 {
|
||||||
|
n = -1
|
||||||
|
}
|
||||||
|
s.set(tabWidthKey, n)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnderlineSpaces determines whether to underline spaces between words. By
|
||||||
|
// default, this is true. Spaces can also be underlined without underlining the
|
||||||
|
// text itself.
|
||||||
|
func (s Style) UnderlineSpaces(v bool) Style {
|
||||||
|
s.set(underlineSpacesKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// StrikethroughSpaces determines whether to apply strikethroughs to spaces
|
||||||
|
// between words. By default, this is true. Spaces can also be struck without
|
||||||
|
// underlining the text itself.
|
||||||
|
func (s Style) StrikethroughSpaces(v bool) Style {
|
||||||
|
s.set(strikethroughSpacesKey, v)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform applies a given function to a string at render time, allowing for
|
||||||
|
// the string being rendered to be manipuated.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// s := NewStyle().Transform(strings.ToUpper)
|
||||||
|
// fmt.Println(s.Render("raow!") // "RAOW!"
|
||||||
|
func (s Style) Transform(fn func(string) string) Style {
|
||||||
|
s.set(transformKey, fn)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderer sets the renderer for the style. This is useful for changing the
|
||||||
|
// renderer for a style that is being used in a different context.
|
||||||
|
func (s Style) Renderer(r *Renderer) Style {
|
||||||
|
s.r = r
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// whichSidesInt is a helper method for setting values on sides of a block based
|
||||||
|
// on the number of arguments. It follows the CSS shorthand rules for blocks
|
||||||
|
// like margin, padding. and borders. Here are how the rules work:
|
||||||
|
//
|
||||||
|
// 0 args: do nothing
|
||||||
|
// 1 arg: all sides
|
||||||
|
// 2 args: top -> bottom
|
||||||
|
// 3 args: top -> horizontal -> bottom
|
||||||
|
// 4 args: top -> right -> bottom -> left
|
||||||
|
// 5+ args: do nothing.
|
||||||
|
func whichSidesInt(i ...int) (top, right, bottom, left int, ok bool) {
|
||||||
|
switch len(i) {
|
||||||
|
case 1:
|
||||||
|
top = i[0]
|
||||||
|
bottom = i[0]
|
||||||
|
left = i[0]
|
||||||
|
right = i[0]
|
||||||
|
ok = true
|
||||||
|
case 2: //nolint:mnd
|
||||||
|
top = i[0]
|
||||||
|
bottom = i[0]
|
||||||
|
left = i[1]
|
||||||
|
right = i[1]
|
||||||
|
ok = true
|
||||||
|
case 3: //nolint:mnd
|
||||||
|
top = i[0]
|
||||||
|
left = i[1]
|
||||||
|
right = i[1]
|
||||||
|
bottom = i[2]
|
||||||
|
ok = true
|
||||||
|
case 4: //nolint:mnd
|
||||||
|
top = i[0]
|
||||||
|
right = i[1]
|
||||||
|
bottom = i[2]
|
||||||
|
left = i[3]
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
return top, right, bottom, left, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// whichSidesBool is like whichSidesInt, except it operates on a series of
|
||||||
|
// boolean values. See the comment on whichSidesInt for details on how this
|
||||||
|
// works.
|
||||||
|
func whichSidesBool(i ...bool) (top, right, bottom, left bool, ok bool) {
|
||||||
|
switch len(i) {
|
||||||
|
case 1:
|
||||||
|
top = i[0]
|
||||||
|
bottom = i[0]
|
||||||
|
left = i[0]
|
||||||
|
right = i[0]
|
||||||
|
ok = true
|
||||||
|
case 2: //nolint:mnd
|
||||||
|
top = i[0]
|
||||||
|
bottom = i[0]
|
||||||
|
left = i[1]
|
||||||
|
right = i[1]
|
||||||
|
ok = true
|
||||||
|
case 3: //nolint:mnd
|
||||||
|
top = i[0]
|
||||||
|
left = i[1]
|
||||||
|
right = i[1]
|
||||||
|
bottom = i[2]
|
||||||
|
ok = true
|
||||||
|
case 4: //nolint:mnd
|
||||||
|
top = i[0]
|
||||||
|
right = i[1]
|
||||||
|
bottom = i[2]
|
||||||
|
left = i[3]
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
return top, right, bottom, left, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// whichSidesColor is like whichSides, except it operates on a series of
|
||||||
|
// boolean values. See the comment on whichSidesInt for details on how this
|
||||||
|
// works.
|
||||||
|
func whichSidesColor(i ...TerminalColor) (top, right, bottom, left TerminalColor, ok bool) {
|
||||||
|
switch len(i) {
|
||||||
|
case 1:
|
||||||
|
top = i[0]
|
||||||
|
bottom = i[0]
|
||||||
|
left = i[0]
|
||||||
|
right = i[0]
|
||||||
|
ok = true
|
||||||
|
case 2: //nolint:mnd
|
||||||
|
top = i[0]
|
||||||
|
bottom = i[0]
|
||||||
|
left = i[1]
|
||||||
|
right = i[1]
|
||||||
|
ok = true
|
||||||
|
case 3: //nolint:mnd
|
||||||
|
top = i[0]
|
||||||
|
left = i[1]
|
||||||
|
right = i[1]
|
||||||
|
bottom = i[2]
|
||||||
|
ok = true
|
||||||
|
case 4: //nolint:mnd
|
||||||
|
top = i[0]
|
||||||
|
right = i[1]
|
||||||
|
bottom = i[2]
|
||||||
|
left = i[3]
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
return top, right, bottom, left, ok
|
||||||
|
}
|
||||||
41
vendor/github.com/charmbracelet/lipgloss/size.go
generated
vendored
Normal file
41
vendor/github.com/charmbracelet/lipgloss/size.go
generated
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Width returns the cell width of characters in the string. ANSI sequences are
|
||||||
|
// ignored and characters wider than one cell (such as Chinese characters and
|
||||||
|
// emojis) are appropriately measured.
|
||||||
|
//
|
||||||
|
// You should use this instead of len(string) len([]rune(string) as neither
|
||||||
|
// will give you accurate results.
|
||||||
|
func Width(str string) (width int) {
|
||||||
|
for _, l := range strings.Split(str, "\n") {
|
||||||
|
w := ansi.StringWidth(l)
|
||||||
|
if w > width {
|
||||||
|
width = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height returns height of a string in cells. This is done simply by
|
||||||
|
// counting \n characters. If your strings use \r\n for newlines you should
|
||||||
|
// convert them to \n first, or simply write a separate function for measuring
|
||||||
|
// height.
|
||||||
|
func Height(str string) int {
|
||||||
|
return strings.Count(str, "\n") + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the width and height of the string in cells. ANSI sequences are
|
||||||
|
// ignored and characters wider than one cell (such as Chinese characters and
|
||||||
|
// emojis) are appropriately measured.
|
||||||
|
func Size(str string) (width, height int) {
|
||||||
|
width = Width(str)
|
||||||
|
height = Height(str)
|
||||||
|
return width, height
|
||||||
|
}
|
||||||
588
vendor/github.com/charmbracelet/lipgloss/style.go
generated
vendored
Normal file
588
vendor/github.com/charmbracelet/lipgloss/style.go
generated
vendored
Normal file
|
|
@ -0,0 +1,588 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
"github.com/charmbracelet/x/cellbuf"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tabWidthDefault = 4
|
||||||
|
|
||||||
|
// Property for a key.
|
||||||
|
type propKey int64
|
||||||
|
|
||||||
|
// Available properties.
|
||||||
|
const (
|
||||||
|
// Boolean props come first.
|
||||||
|
boldKey propKey = 1 << iota
|
||||||
|
italicKey
|
||||||
|
underlineKey
|
||||||
|
strikethroughKey
|
||||||
|
reverseKey
|
||||||
|
blinkKey
|
||||||
|
faintKey
|
||||||
|
underlineSpacesKey
|
||||||
|
strikethroughSpacesKey
|
||||||
|
colorWhitespaceKey
|
||||||
|
|
||||||
|
// Non-boolean props.
|
||||||
|
foregroundKey
|
||||||
|
backgroundKey
|
||||||
|
widthKey
|
||||||
|
heightKey
|
||||||
|
alignHorizontalKey
|
||||||
|
alignVerticalKey
|
||||||
|
|
||||||
|
// Padding.
|
||||||
|
paddingTopKey
|
||||||
|
paddingRightKey
|
||||||
|
paddingBottomKey
|
||||||
|
paddingLeftKey
|
||||||
|
|
||||||
|
// Margins.
|
||||||
|
marginTopKey
|
||||||
|
marginRightKey
|
||||||
|
marginBottomKey
|
||||||
|
marginLeftKey
|
||||||
|
marginBackgroundKey
|
||||||
|
|
||||||
|
// Border runes.
|
||||||
|
borderStyleKey
|
||||||
|
|
||||||
|
// Border edges.
|
||||||
|
borderTopKey
|
||||||
|
borderRightKey
|
||||||
|
borderBottomKey
|
||||||
|
borderLeftKey
|
||||||
|
|
||||||
|
// Border foreground colors.
|
||||||
|
borderTopForegroundKey
|
||||||
|
borderRightForegroundKey
|
||||||
|
borderBottomForegroundKey
|
||||||
|
borderLeftForegroundKey
|
||||||
|
|
||||||
|
// Border background colors.
|
||||||
|
borderTopBackgroundKey
|
||||||
|
borderRightBackgroundKey
|
||||||
|
borderBottomBackgroundKey
|
||||||
|
borderLeftBackgroundKey
|
||||||
|
|
||||||
|
inlineKey
|
||||||
|
maxWidthKey
|
||||||
|
maxHeightKey
|
||||||
|
tabWidthKey
|
||||||
|
|
||||||
|
transformKey
|
||||||
|
)
|
||||||
|
|
||||||
|
// props is a set of properties.
|
||||||
|
type props int64
|
||||||
|
|
||||||
|
// set sets a property.
|
||||||
|
func (p props) set(k propKey) props {
|
||||||
|
return p | props(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unset unsets a property.
|
||||||
|
func (p props) unset(k propKey) props {
|
||||||
|
return p &^ props(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// has checks if a property is set.
|
||||||
|
func (p props) has(k propKey) bool {
|
||||||
|
return p&props(k) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStyle returns a new, empty Style. While it's syntactic sugar for the
|
||||||
|
// Style{} primitive, it's recommended to use this function for creating styles
|
||||||
|
// in case the underlying implementation changes. It takes an optional string
|
||||||
|
// value to be set as the underlying string value for this style.
|
||||||
|
func NewStyle() Style {
|
||||||
|
return renderer.NewStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStyle returns a new, empty Style. While it's syntactic sugar for the
|
||||||
|
// Style{} primitive, it's recommended to use this function for creating styles
|
||||||
|
// in case the underlying implementation changes. It takes an optional string
|
||||||
|
// value to be set as the underlying string value for this style.
|
||||||
|
func (r *Renderer) NewStyle() Style {
|
||||||
|
s := Style{r: r}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Style contains a set of rules that comprise a style as a whole.
|
||||||
|
type Style struct {
|
||||||
|
r *Renderer
|
||||||
|
props props
|
||||||
|
value string
|
||||||
|
|
||||||
|
// we store bool props values here
|
||||||
|
attrs int
|
||||||
|
|
||||||
|
// props that have values
|
||||||
|
fgColor TerminalColor
|
||||||
|
bgColor TerminalColor
|
||||||
|
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
|
||||||
|
alignHorizontal Position
|
||||||
|
alignVertical Position
|
||||||
|
|
||||||
|
paddingTop int
|
||||||
|
paddingRight int
|
||||||
|
paddingBottom int
|
||||||
|
paddingLeft int
|
||||||
|
|
||||||
|
marginTop int
|
||||||
|
marginRight int
|
||||||
|
marginBottom int
|
||||||
|
marginLeft int
|
||||||
|
marginBgColor TerminalColor
|
||||||
|
|
||||||
|
borderStyle Border
|
||||||
|
borderTopFgColor TerminalColor
|
||||||
|
borderRightFgColor TerminalColor
|
||||||
|
borderBottomFgColor TerminalColor
|
||||||
|
borderLeftFgColor TerminalColor
|
||||||
|
borderTopBgColor TerminalColor
|
||||||
|
borderRightBgColor TerminalColor
|
||||||
|
borderBottomBgColor TerminalColor
|
||||||
|
borderLeftBgColor TerminalColor
|
||||||
|
|
||||||
|
maxWidth int
|
||||||
|
maxHeight int
|
||||||
|
tabWidth int
|
||||||
|
|
||||||
|
transform func(string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinString joins a list of strings into a single string separated with a
|
||||||
|
// space.
|
||||||
|
func joinString(strs ...string) string {
|
||||||
|
return strings.Join(strs, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetString sets the underlying string value for this style. To render once
|
||||||
|
// the underlying string is set, use the Style.String. This method is
|
||||||
|
// a convenience for cases when having a stringer implementation is handy, such
|
||||||
|
// as when using fmt.Sprintf. You can also simply define a style and render out
|
||||||
|
// strings directly with Style.Render.
|
||||||
|
func (s Style) SetString(strs ...string) Style {
|
||||||
|
s.value = joinString(strs...)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the raw, unformatted, underlying string value for this style.
|
||||||
|
func (s Style) Value() string {
|
||||||
|
return s.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements stringer for a Style, returning the rendered result based
|
||||||
|
// on the rules in this style. An underlying string value must be set with
|
||||||
|
// Style.SetString prior to using this method.
|
||||||
|
func (s Style) String() string {
|
||||||
|
return s.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy returns a copy of this style, including any underlying string values.
|
||||||
|
//
|
||||||
|
// Deprecated: to copy just use assignment (i.e. a := b). All methods also
|
||||||
|
// return a new style.
|
||||||
|
func (s Style) Copy() Style {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inherit overlays the style in the argument onto this style by copying each explicitly
|
||||||
|
// set value from the argument style onto this style if it is not already explicitly set.
|
||||||
|
// Existing set values are kept intact and not overwritten.
|
||||||
|
//
|
||||||
|
// Margins, padding, and underlying string values are not inherited.
|
||||||
|
func (s Style) Inherit(i Style) Style {
|
||||||
|
for k := boldKey; k <= transformKey; k <<= 1 {
|
||||||
|
if !i.isSet(k) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch k { //nolint:exhaustive
|
||||||
|
case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey:
|
||||||
|
// Margins are not inherited
|
||||||
|
continue
|
||||||
|
case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey:
|
||||||
|
// Padding is not inherited
|
||||||
|
continue
|
||||||
|
case backgroundKey:
|
||||||
|
// The margins also inherit the background color
|
||||||
|
if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) {
|
||||||
|
s.set(marginBackgroundKey, i.bgColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.isSet(k) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.setFrom(k, i)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render applies the defined style formatting to a given string.
|
||||||
|
func (s Style) Render(strs ...string) string {
|
||||||
|
if s.r == nil {
|
||||||
|
s.r = renderer
|
||||||
|
}
|
||||||
|
if s.value != "" {
|
||||||
|
strs = append([]string{s.value}, strs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
str = joinString(strs...)
|
||||||
|
|
||||||
|
p = s.r.ColorProfile()
|
||||||
|
te = p.String()
|
||||||
|
teSpace = p.String()
|
||||||
|
teWhitespace = p.String()
|
||||||
|
|
||||||
|
bold = s.getAsBool(boldKey, false)
|
||||||
|
italic = s.getAsBool(italicKey, false)
|
||||||
|
underline = s.getAsBool(underlineKey, false)
|
||||||
|
strikethrough = s.getAsBool(strikethroughKey, false)
|
||||||
|
reverse = s.getAsBool(reverseKey, false)
|
||||||
|
blink = s.getAsBool(blinkKey, false)
|
||||||
|
faint = s.getAsBool(faintKey, false)
|
||||||
|
|
||||||
|
fg = s.getAsColor(foregroundKey)
|
||||||
|
bg = s.getAsColor(backgroundKey)
|
||||||
|
|
||||||
|
width = s.getAsInt(widthKey)
|
||||||
|
height = s.getAsInt(heightKey)
|
||||||
|
horizontalAlign = s.getAsPosition(alignHorizontalKey)
|
||||||
|
verticalAlign = s.getAsPosition(alignVerticalKey)
|
||||||
|
|
||||||
|
topPadding = s.getAsInt(paddingTopKey)
|
||||||
|
rightPadding = s.getAsInt(paddingRightKey)
|
||||||
|
bottomPadding = s.getAsInt(paddingBottomKey)
|
||||||
|
leftPadding = s.getAsInt(paddingLeftKey)
|
||||||
|
|
||||||
|
colorWhitespace = s.getAsBool(colorWhitespaceKey, true)
|
||||||
|
inline = s.getAsBool(inlineKey, false)
|
||||||
|
maxWidth = s.getAsInt(maxWidthKey)
|
||||||
|
maxHeight = s.getAsInt(maxHeightKey)
|
||||||
|
|
||||||
|
underlineSpaces = s.getAsBool(underlineSpacesKey, false) || (underline && s.getAsBool(underlineSpacesKey, true))
|
||||||
|
strikethroughSpaces = s.getAsBool(strikethroughSpacesKey, false) || (strikethrough && s.getAsBool(strikethroughSpacesKey, true))
|
||||||
|
|
||||||
|
// Do we need to style whitespace (padding and space outside
|
||||||
|
// paragraphs) separately?
|
||||||
|
styleWhitespace = reverse
|
||||||
|
|
||||||
|
// Do we need to style spaces separately?
|
||||||
|
useSpaceStyler = (underline && !underlineSpaces) || (strikethrough && !strikethroughSpaces) || underlineSpaces || strikethroughSpaces
|
||||||
|
|
||||||
|
transform = s.getAsTransform(transformKey)
|
||||||
|
)
|
||||||
|
|
||||||
|
if transform != nil {
|
||||||
|
str = transform(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.props == 0 {
|
||||||
|
return s.maybeConvertTabs(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable support for ANSI on the legacy Windows cmd.exe console. This is a
|
||||||
|
// no-op on non-Windows systems and on Windows runs only once.
|
||||||
|
enableLegacyWindowsANSI()
|
||||||
|
|
||||||
|
if bold {
|
||||||
|
te = te.Bold()
|
||||||
|
}
|
||||||
|
if italic {
|
||||||
|
te = te.Italic()
|
||||||
|
}
|
||||||
|
if underline {
|
||||||
|
te = te.Underline()
|
||||||
|
}
|
||||||
|
if reverse {
|
||||||
|
teWhitespace = teWhitespace.Reverse()
|
||||||
|
te = te.Reverse()
|
||||||
|
}
|
||||||
|
if blink {
|
||||||
|
te = te.Blink()
|
||||||
|
}
|
||||||
|
if faint {
|
||||||
|
te = te.Faint()
|
||||||
|
}
|
||||||
|
|
||||||
|
if fg != noColor {
|
||||||
|
te = te.Foreground(fg.color(s.r))
|
||||||
|
if styleWhitespace {
|
||||||
|
teWhitespace = teWhitespace.Foreground(fg.color(s.r))
|
||||||
|
}
|
||||||
|
if useSpaceStyler {
|
||||||
|
teSpace = teSpace.Foreground(fg.color(s.r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bg != noColor {
|
||||||
|
te = te.Background(bg.color(s.r))
|
||||||
|
if colorWhitespace {
|
||||||
|
teWhitespace = teWhitespace.Background(bg.color(s.r))
|
||||||
|
}
|
||||||
|
if useSpaceStyler {
|
||||||
|
teSpace = teSpace.Background(bg.color(s.r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if underline {
|
||||||
|
te = te.Underline()
|
||||||
|
}
|
||||||
|
if strikethrough {
|
||||||
|
te = te.CrossOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
if underlineSpaces {
|
||||||
|
teSpace = teSpace.Underline()
|
||||||
|
}
|
||||||
|
if strikethroughSpaces {
|
||||||
|
teSpace = teSpace.CrossOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potentially convert tabs to spaces
|
||||||
|
str = s.maybeConvertTabs(str)
|
||||||
|
// carriage returns can cause strange behaviour when rendering.
|
||||||
|
str = strings.ReplaceAll(str, "\r\n", "\n")
|
||||||
|
|
||||||
|
// Strip newlines in single line mode
|
||||||
|
if inline {
|
||||||
|
str = strings.ReplaceAll(str, "\n", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Word wrap
|
||||||
|
if !inline && width > 0 {
|
||||||
|
wrapAt := width - leftPadding - rightPadding
|
||||||
|
str = cellbuf.Wrap(str, wrapAt, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render core text
|
||||||
|
{
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
l := strings.Split(str, "\n")
|
||||||
|
for i := range l {
|
||||||
|
if useSpaceStyler {
|
||||||
|
// Look for spaces and apply a different styler
|
||||||
|
for _, r := range l[i] {
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
b.WriteString(teSpace.Styled(string(r)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b.WriteString(te.Styled(string(r)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.WriteString(te.Styled(l[i]))
|
||||||
|
}
|
||||||
|
if i != len(l)-1 {
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str = b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Padding
|
||||||
|
if !inline { //nolint:nestif
|
||||||
|
if leftPadding > 0 {
|
||||||
|
var st *termenv.Style
|
||||||
|
if colorWhitespace || styleWhitespace {
|
||||||
|
st = &teWhitespace
|
||||||
|
}
|
||||||
|
str = padLeft(str, leftPadding, st)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rightPadding > 0 {
|
||||||
|
var st *termenv.Style
|
||||||
|
if colorWhitespace || styleWhitespace {
|
||||||
|
st = &teWhitespace
|
||||||
|
}
|
||||||
|
str = padRight(str, rightPadding, st)
|
||||||
|
}
|
||||||
|
|
||||||
|
if topPadding > 0 {
|
||||||
|
str = strings.Repeat("\n", topPadding) + str
|
||||||
|
}
|
||||||
|
|
||||||
|
if bottomPadding > 0 {
|
||||||
|
str += strings.Repeat("\n", bottomPadding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Height
|
||||||
|
if height > 0 {
|
||||||
|
str = alignTextVertical(str, verticalAlign, height, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set alignment. This will also pad short lines with spaces so that all
|
||||||
|
// lines are the same length, so we run it under a few different conditions
|
||||||
|
// beyond alignment.
|
||||||
|
{
|
||||||
|
numLines := strings.Count(str, "\n")
|
||||||
|
|
||||||
|
if numLines != 0 || width != 0 {
|
||||||
|
var st *termenv.Style
|
||||||
|
if colorWhitespace || styleWhitespace {
|
||||||
|
st = &teWhitespace
|
||||||
|
}
|
||||||
|
str = alignTextHorizontal(str, horizontalAlign, width, st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inline {
|
||||||
|
str = s.applyBorder(str)
|
||||||
|
str = s.applyMargins(str, inline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate according to MaxWidth
|
||||||
|
if maxWidth > 0 {
|
||||||
|
lines := strings.Split(str, "\n")
|
||||||
|
|
||||||
|
for i := range lines {
|
||||||
|
lines[i] = ansi.Truncate(lines[i], maxWidth, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
str = strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate according to MaxHeight
|
||||||
|
if maxHeight > 0 {
|
||||||
|
lines := strings.Split(str, "\n")
|
||||||
|
height := min(maxHeight, len(lines))
|
||||||
|
if len(lines) > 0 {
|
||||||
|
str = strings.Join(lines[:height], "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Style) maybeConvertTabs(str string) string {
|
||||||
|
tw := tabWidthDefault
|
||||||
|
if s.isSet(tabWidthKey) {
|
||||||
|
tw = s.getAsInt(tabWidthKey)
|
||||||
|
}
|
||||||
|
switch tw {
|
||||||
|
case -1:
|
||||||
|
return str
|
||||||
|
case 0:
|
||||||
|
return strings.ReplaceAll(str, "\t", "")
|
||||||
|
default:
|
||||||
|
return strings.ReplaceAll(str, "\t", strings.Repeat(" ", tw))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Style) applyMargins(str string, inline bool) string {
|
||||||
|
var (
|
||||||
|
topMargin = s.getAsInt(marginTopKey)
|
||||||
|
rightMargin = s.getAsInt(marginRightKey)
|
||||||
|
bottomMargin = s.getAsInt(marginBottomKey)
|
||||||
|
leftMargin = s.getAsInt(marginLeftKey)
|
||||||
|
|
||||||
|
styler termenv.Style
|
||||||
|
)
|
||||||
|
|
||||||
|
bgc := s.getAsColor(marginBackgroundKey)
|
||||||
|
if bgc != noColor {
|
||||||
|
styler = styler.Background(bgc.color(s.r))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add left and right margin
|
||||||
|
str = padLeft(str, leftMargin, &styler)
|
||||||
|
str = padRight(str, rightMargin, &styler)
|
||||||
|
|
||||||
|
// Top/bottom margin
|
||||||
|
if !inline {
|
||||||
|
_, width := getLines(str)
|
||||||
|
spaces := strings.Repeat(" ", width)
|
||||||
|
|
||||||
|
if topMargin > 0 {
|
||||||
|
str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str
|
||||||
|
}
|
||||||
|
if bottomMargin > 0 {
|
||||||
|
str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply left padding.
|
||||||
|
func padLeft(str string, n int, style *termenv.Style) string {
|
||||||
|
return pad(str, -n, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply right padding.
|
||||||
|
func padRight(str string, n int, style *termenv.Style) string {
|
||||||
|
return pad(str, n, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pad adds padding to either the left or right side of a string.
|
||||||
|
// Positive values add to the right side while negative values
|
||||||
|
// add to the left side.
|
||||||
|
func pad(str string, n int, style *termenv.Style) string {
|
||||||
|
if n == 0 {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
sp := strings.Repeat(" ", abs(n))
|
||||||
|
if style != nil {
|
||||||
|
sp = style.Styled(sp)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := strings.Builder{}
|
||||||
|
l := strings.Split(str, "\n")
|
||||||
|
|
||||||
|
for i := range l {
|
||||||
|
switch {
|
||||||
|
// pad right
|
||||||
|
case n > 0:
|
||||||
|
b.WriteString(l[i])
|
||||||
|
b.WriteString(sp)
|
||||||
|
// pad left
|
||||||
|
default:
|
||||||
|
b.WriteString(sp)
|
||||||
|
b.WriteString(l[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if i != len(l)-1 {
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func max(a, b int) int { //nolint:unparam,predeclared
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int { //nolint:predeclared
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func abs(a int) int {
|
||||||
|
if a < 0 {
|
||||||
|
return -a
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
331
vendor/github.com/charmbracelet/lipgloss/unset.go
generated
vendored
Normal file
331
vendor/github.com/charmbracelet/lipgloss/unset.go
generated
vendored
Normal file
|
|
@ -0,0 +1,331 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
// unset unsets a property from a style.
|
||||||
|
func (s *Style) unset(key propKey) {
|
||||||
|
s.props = s.props.unset(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBold removes the bold style rule, if set.
|
||||||
|
func (s Style) UnsetBold() Style {
|
||||||
|
s.unset(boldKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetItalic removes the italic style rule, if set.
|
||||||
|
func (s Style) UnsetItalic() Style {
|
||||||
|
s.unset(italicKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetUnderline removes the underline style rule, if set.
|
||||||
|
func (s Style) UnsetUnderline() Style {
|
||||||
|
s.unset(underlineKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetStrikethrough removes the strikethrough style rule, if set.
|
||||||
|
func (s Style) UnsetStrikethrough() Style {
|
||||||
|
s.unset(strikethroughKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetReverse removes the reverse style rule, if set.
|
||||||
|
func (s Style) UnsetReverse() Style {
|
||||||
|
s.unset(reverseKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBlink removes the blink style rule, if set.
|
||||||
|
func (s Style) UnsetBlink() Style {
|
||||||
|
s.unset(blinkKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetFaint removes the faint style rule, if set.
|
||||||
|
func (s Style) UnsetFaint() Style {
|
||||||
|
s.unset(faintKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetForeground removes the foreground style rule, if set.
|
||||||
|
func (s Style) UnsetForeground() Style {
|
||||||
|
s.unset(foregroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBackground removes the background style rule, if set.
|
||||||
|
func (s Style) UnsetBackground() Style {
|
||||||
|
s.unset(backgroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetWidth removes the width style rule, if set.
|
||||||
|
func (s Style) UnsetWidth() Style {
|
||||||
|
s.unset(widthKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetHeight removes the height style rule, if set.
|
||||||
|
func (s Style) UnsetHeight() Style {
|
||||||
|
s.unset(heightKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetAlign removes the horizontal and vertical text alignment style rule, if set.
|
||||||
|
func (s Style) UnsetAlign() Style {
|
||||||
|
s.unset(alignHorizontalKey)
|
||||||
|
s.unset(alignVerticalKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetAlignHorizontal removes the horizontal text alignment style rule, if set.
|
||||||
|
func (s Style) UnsetAlignHorizontal() Style {
|
||||||
|
s.unset(alignHorizontalKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetAlignVertical removes the vertical text alignment style rule, if set.
|
||||||
|
func (s Style) UnsetAlignVertical() Style {
|
||||||
|
s.unset(alignVerticalKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetPadding removes all padding style rules.
|
||||||
|
func (s Style) UnsetPadding() Style {
|
||||||
|
s.unset(paddingLeftKey)
|
||||||
|
s.unset(paddingRightKey)
|
||||||
|
s.unset(paddingTopKey)
|
||||||
|
s.unset(paddingBottomKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetPaddingLeft removes the left padding style rule, if set.
|
||||||
|
func (s Style) UnsetPaddingLeft() Style {
|
||||||
|
s.unset(paddingLeftKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetPaddingRight removes the right padding style rule, if set.
|
||||||
|
func (s Style) UnsetPaddingRight() Style {
|
||||||
|
s.unset(paddingRightKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetPaddingTop removes the top padding style rule, if set.
|
||||||
|
func (s Style) UnsetPaddingTop() Style {
|
||||||
|
s.unset(paddingTopKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetPaddingBottom removes the bottom padding style rule, if set.
|
||||||
|
func (s Style) UnsetPaddingBottom() Style {
|
||||||
|
s.unset(paddingBottomKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetColorWhitespace removes the rule for coloring padding, if set.
|
||||||
|
func (s Style) UnsetColorWhitespace() Style {
|
||||||
|
s.unset(colorWhitespaceKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetMargins removes all margin style rules.
|
||||||
|
func (s Style) UnsetMargins() Style {
|
||||||
|
s.unset(marginLeftKey)
|
||||||
|
s.unset(marginRightKey)
|
||||||
|
s.unset(marginTopKey)
|
||||||
|
s.unset(marginBottomKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetMarginLeft removes the left margin style rule, if set.
|
||||||
|
func (s Style) UnsetMarginLeft() Style {
|
||||||
|
s.unset(marginLeftKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetMarginRight removes the right margin style rule, if set.
|
||||||
|
func (s Style) UnsetMarginRight() Style {
|
||||||
|
s.unset(marginRightKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetMarginTop removes the top margin style rule, if set.
|
||||||
|
func (s Style) UnsetMarginTop() Style {
|
||||||
|
s.unset(marginTopKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetMarginBottom removes the bottom margin style rule, if set.
|
||||||
|
func (s Style) UnsetMarginBottom() Style {
|
||||||
|
s.unset(marginBottomKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetMarginBackground removes the margin's background color. Note that the
|
||||||
|
// margin's background color can be set from the background color of another
|
||||||
|
// style during inheritance.
|
||||||
|
func (s Style) UnsetMarginBackground() Style {
|
||||||
|
s.unset(marginBackgroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderStyle removes the border style rule, if set.
|
||||||
|
func (s Style) UnsetBorderStyle() Style {
|
||||||
|
s.unset(borderStyleKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderTop removes the border top style rule, if set.
|
||||||
|
func (s Style) UnsetBorderTop() Style {
|
||||||
|
s.unset(borderTopKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderRight removes the border right style rule, if set.
|
||||||
|
func (s Style) UnsetBorderRight() Style {
|
||||||
|
s.unset(borderRightKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderBottom removes the border bottom style rule, if set.
|
||||||
|
func (s Style) UnsetBorderBottom() Style {
|
||||||
|
s.unset(borderBottomKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderLeft removes the border left style rule, if set.
|
||||||
|
func (s Style) UnsetBorderLeft() Style {
|
||||||
|
s.unset(borderLeftKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderForeground removes all border foreground color styles, if set.
|
||||||
|
func (s Style) UnsetBorderForeground() Style {
|
||||||
|
s.unset(borderTopForegroundKey)
|
||||||
|
s.unset(borderRightForegroundKey)
|
||||||
|
s.unset(borderBottomForegroundKey)
|
||||||
|
s.unset(borderLeftForegroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderTopForeground removes the top border foreground color rule,
|
||||||
|
// if set.
|
||||||
|
func (s Style) UnsetBorderTopForeground() Style {
|
||||||
|
s.unset(borderTopForegroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderRightForeground removes the right border foreground color rule,
|
||||||
|
// if set.
|
||||||
|
func (s Style) UnsetBorderRightForeground() Style {
|
||||||
|
s.unset(borderRightForegroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderBottomForeground removes the bottom border foreground color
|
||||||
|
// rule, if set.
|
||||||
|
func (s Style) UnsetBorderBottomForeground() Style {
|
||||||
|
s.unset(borderBottomForegroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderLeftForeground removes the left border foreground color rule,
|
||||||
|
// if set.
|
||||||
|
func (s Style) UnsetBorderLeftForeground() Style {
|
||||||
|
s.unset(borderLeftForegroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderBackground removes all border background color styles, if
|
||||||
|
// set.
|
||||||
|
func (s Style) UnsetBorderBackground() Style {
|
||||||
|
s.unset(borderTopBackgroundKey)
|
||||||
|
s.unset(borderRightBackgroundKey)
|
||||||
|
s.unset(borderBottomBackgroundKey)
|
||||||
|
s.unset(borderLeftBackgroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderTopBackgroundColor removes the top border background color rule,
|
||||||
|
// if set.
|
||||||
|
//
|
||||||
|
// Deprecated: This function simply calls Style.UnsetBorderTopBackground.
|
||||||
|
func (s Style) UnsetBorderTopBackgroundColor() Style {
|
||||||
|
return s.UnsetBorderTopBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderTopBackground removes the top border background color rule,
|
||||||
|
// if set.
|
||||||
|
func (s Style) UnsetBorderTopBackground() Style {
|
||||||
|
s.unset(borderTopBackgroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderRightBackground removes the right border background color
|
||||||
|
// rule, if set.
|
||||||
|
func (s Style) UnsetBorderRightBackground() Style {
|
||||||
|
s.unset(borderRightBackgroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderBottomBackground removes the bottom border background color
|
||||||
|
// rule, if set.
|
||||||
|
func (s Style) UnsetBorderBottomBackground() Style {
|
||||||
|
s.unset(borderBottomBackgroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetBorderLeftBackground removes the left border color rule, if set.
|
||||||
|
func (s Style) UnsetBorderLeftBackground() Style {
|
||||||
|
s.unset(borderLeftBackgroundKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetInline removes the inline style rule, if set.
|
||||||
|
func (s Style) UnsetInline() Style {
|
||||||
|
s.unset(inlineKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetMaxWidth removes the max width style rule, if set.
|
||||||
|
func (s Style) UnsetMaxWidth() Style {
|
||||||
|
s.unset(maxWidthKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetMaxHeight removes the max height style rule, if set.
|
||||||
|
func (s Style) UnsetMaxHeight() Style {
|
||||||
|
s.unset(maxHeightKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetTabWidth removes the tab width style rule, if set.
|
||||||
|
func (s Style) UnsetTabWidth() Style {
|
||||||
|
s.unset(tabWidthKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetUnderlineSpaces removes the value set by UnderlineSpaces.
|
||||||
|
func (s Style) UnsetUnderlineSpaces() Style {
|
||||||
|
s.unset(underlineSpacesKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetStrikethroughSpaces removes the value set by StrikethroughSpaces.
|
||||||
|
func (s Style) UnsetStrikethroughSpaces() Style {
|
||||||
|
s.unset(strikethroughSpacesKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetTransform removes the value set by Transform.
|
||||||
|
func (s Style) UnsetTransform() Style {
|
||||||
|
s.unset(transformKey)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsetString sets the underlying string value to the empty string.
|
||||||
|
func (s Style) UnsetString() Style {
|
||||||
|
s.value = ""
|
||||||
|
return s
|
||||||
|
}
|
||||||
83
vendor/github.com/charmbracelet/lipgloss/whitespace.go
generated
vendored
Normal file
83
vendor/github.com/charmbracelet/lipgloss/whitespace.go
generated
vendored
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
package lipgloss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
"github.com/muesli/termenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// whitespace is a whitespace renderer.
|
||||||
|
type whitespace struct {
|
||||||
|
re *Renderer
|
||||||
|
style termenv.Style
|
||||||
|
chars string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newWhitespace creates a new whitespace renderer. The order of the options
|
||||||
|
// matters, if you're using WithWhitespaceRenderer, make sure it comes first as
|
||||||
|
// other options might depend on it.
|
||||||
|
func newWhitespace(r *Renderer, opts ...WhitespaceOption) *whitespace {
|
||||||
|
w := &whitespace{
|
||||||
|
re: r,
|
||||||
|
style: r.ColorProfile().String(),
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(w)
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render whitespaces.
|
||||||
|
func (w whitespace) render(width int) string {
|
||||||
|
if w.chars == "" {
|
||||||
|
w.chars = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
r := []rune(w.chars)
|
||||||
|
j := 0
|
||||||
|
b := strings.Builder{}
|
||||||
|
|
||||||
|
// Cycle through runes and print them into the whitespace.
|
||||||
|
for i := 0; i < width; {
|
||||||
|
b.WriteRune(r[j])
|
||||||
|
j++
|
||||||
|
if j >= len(r) {
|
||||||
|
j = 0
|
||||||
|
}
|
||||||
|
i += ansi.StringWidth(string(r[j]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill any extra gaps white spaces. This might be necessary if any runes
|
||||||
|
// are more than one cell wide, which could leave a one-rune gap.
|
||||||
|
short := width - ansi.StringWidth(b.String())
|
||||||
|
if short > 0 {
|
||||||
|
b.WriteString(strings.Repeat(" ", short))
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.style.Styled(b.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhitespaceOption sets a styling rule for rendering whitespace.
|
||||||
|
type WhitespaceOption func(*whitespace)
|
||||||
|
|
||||||
|
// WithWhitespaceForeground sets the color of the characters in the whitespace.
|
||||||
|
func WithWhitespaceForeground(c TerminalColor) WhitespaceOption {
|
||||||
|
return func(w *whitespace) {
|
||||||
|
w.style = w.style.Foreground(c.color(w.re))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithWhitespaceBackground sets the background color of the whitespace.
|
||||||
|
func WithWhitespaceBackground(c TerminalColor) WhitespaceOption {
|
||||||
|
return func(w *whitespace) {
|
||||||
|
w.style = w.style.Background(c.color(w.re))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithWhitespaceChars sets the characters to be rendered in the whitespace.
|
||||||
|
func WithWhitespaceChars(s string) WhitespaceOption {
|
||||||
|
return func(w *whitespace) {
|
||||||
|
w.chars = s
|
||||||
|
}
|
||||||
|
}
|
||||||
21
vendor/github.com/charmbracelet/x/ansi/LICENSE
generated
vendored
Normal file
21
vendor/github.com/charmbracelet/x/ansi/LICENSE
generated
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Charmbracelet, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
11
vendor/github.com/charmbracelet/x/ansi/ansi.go
generated
vendored
Normal file
11
vendor/github.com/charmbracelet/x/ansi/ansi.go
generated
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
// Execute is a function that "execute" the given escape sequence by writing it
|
||||||
|
// to the provided output writter.
|
||||||
|
//
|
||||||
|
// This is a syntactic sugar over [io.WriteString].
|
||||||
|
func Execute(w io.Writer, s string) (int, error) {
|
||||||
|
return io.WriteString(w, s) //nolint:wrapcheck
|
||||||
|
}
|
||||||
8
vendor/github.com/charmbracelet/x/ansi/ascii.go
generated
vendored
Normal file
8
vendor/github.com/charmbracelet/x/ansi/ascii.go
generated
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SP is the space character (Char: \x20).
|
||||||
|
SP = 0x20
|
||||||
|
// DEL is the delete character (Caret: ^?, Char: \x7f).
|
||||||
|
DEL = 0x7F
|
||||||
|
)
|
||||||
178
vendor/github.com/charmbracelet/x/ansi/background.go
generated
vendored
Normal file
178
vendor/github.com/charmbracelet/x/ansi/background.go
generated
vendored
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HexColor is a [color.Color] that can be formatted as a hex string.
|
||||||
|
type HexColor string
|
||||||
|
|
||||||
|
// RGBA returns the RGBA values of the color.
|
||||||
|
func (h HexColor) RGBA() (r, g, b, a uint32) {
|
||||||
|
hex := h.color()
|
||||||
|
if hex == nil {
|
||||||
|
return 0, 0, 0, 0
|
||||||
|
}
|
||||||
|
return hex.RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hex returns the hex representation of the color. If the color is invalid, it
|
||||||
|
// returns an empty string.
|
||||||
|
func (h HexColor) Hex() string {
|
||||||
|
hex := h.color()
|
||||||
|
if hex == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return hex.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the color as a hex string. If the color is nil, an empty
|
||||||
|
// string is returned.
|
||||||
|
func (h HexColor) String() string {
|
||||||
|
return h.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
// color returns the underlying color of the HexColor.
|
||||||
|
func (h HexColor) color() *colorful.Color {
|
||||||
|
hex, err := colorful.Hex(string(h))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &hex
|
||||||
|
}
|
||||||
|
|
||||||
|
// XRGBColor is a [color.Color] that can be formatted as an XParseColor
|
||||||
|
// rgb: string.
|
||||||
|
//
|
||||||
|
// See: https://linux.die.net/man/3/xparsecolor
|
||||||
|
type XRGBColor struct {
|
||||||
|
color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the RGBA values of the color.
|
||||||
|
func (x XRGBColor) RGBA() (r, g, b, a uint32) {
|
||||||
|
if x.Color == nil {
|
||||||
|
return 0, 0, 0, 0
|
||||||
|
}
|
||||||
|
return x.Color.RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the color as an XParseColor rgb: string. If the color is nil,
|
||||||
|
// an empty string is returned.
|
||||||
|
func (x XRGBColor) String() string {
|
||||||
|
if x.Color == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
r, g, b, _ := x.Color.RGBA()
|
||||||
|
// Get the lower 8 bits
|
||||||
|
return fmt.Sprintf("rgb:%04x/%04x/%04x", r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// XRGBAColor is a [color.Color] that can be formatted as an XParseColor
|
||||||
|
// rgba: string.
|
||||||
|
//
|
||||||
|
// See: https://linux.die.net/man/3/xparsecolor
|
||||||
|
type XRGBAColor struct {
|
||||||
|
color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the RGBA values of the color.
|
||||||
|
func (x XRGBAColor) RGBA() (r, g, b, a uint32) {
|
||||||
|
if x.Color == nil {
|
||||||
|
return 0, 0, 0, 0
|
||||||
|
}
|
||||||
|
return x.Color.RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the color as an XParseColor rgba: string. If the color is nil,
|
||||||
|
// an empty string is returned.
|
||||||
|
func (x XRGBAColor) String() string {
|
||||||
|
if x.Color == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
r, g, b, a := x.RGBA()
|
||||||
|
// Get the lower 8 bits
|
||||||
|
return fmt.Sprintf("rgba:%04x/%04x/%04x/%04x", r, g, b, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetForegroundColor returns a sequence that sets the default terminal
|
||||||
|
// foreground color.
|
||||||
|
//
|
||||||
|
// OSC 10 ; color ST
|
||||||
|
// OSC 10 ; color BEL
|
||||||
|
//
|
||||||
|
// Where color is the encoded color number. Most terminals support hex,
|
||||||
|
// XParseColor rgb: and rgba: strings. You could use [HexColor], [XRGBColor],
|
||||||
|
// or [XRGBAColor] to format the color.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
func SetForegroundColor(s string) string {
|
||||||
|
return "\x1b]10;" + s + "\x07"
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestForegroundColor is a sequence that requests the current default
|
||||||
|
// terminal foreground color.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
const RequestForegroundColor = "\x1b]10;?\x07"
|
||||||
|
|
||||||
|
// ResetForegroundColor is a sequence that resets the default terminal
|
||||||
|
// foreground color.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
const ResetForegroundColor = "\x1b]110\x07"
|
||||||
|
|
||||||
|
// SetBackgroundColor returns a sequence that sets the default terminal
|
||||||
|
// background color.
|
||||||
|
//
|
||||||
|
// OSC 11 ; color ST
|
||||||
|
// OSC 11 ; color BEL
|
||||||
|
//
|
||||||
|
// Where color is the encoded color number. Most terminals support hex,
|
||||||
|
// XParseColor rgb: and rgba: strings. You could use [HexColor], [XRGBColor],
|
||||||
|
// or [XRGBAColor] to format the color.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
func SetBackgroundColor(s string) string {
|
||||||
|
return "\x1b]11;" + s + "\x07"
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestBackgroundColor is a sequence that requests the current default
|
||||||
|
// terminal background color.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
const RequestBackgroundColor = "\x1b]11;?\x07"
|
||||||
|
|
||||||
|
// ResetBackgroundColor is a sequence that resets the default terminal
|
||||||
|
// background color.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
const ResetBackgroundColor = "\x1b]111\x07"
|
||||||
|
|
||||||
|
// SetCursorColor returns a sequence that sets the terminal cursor color.
|
||||||
|
//
|
||||||
|
// OSC 12 ; color ST
|
||||||
|
// OSC 12 ; color BEL
|
||||||
|
//
|
||||||
|
// Where color is the encoded color number. Most terminals support hex,
|
||||||
|
// XParseColor rgb: and rgba: strings. You could use [HexColor], [XRGBColor],
|
||||||
|
// or [XRGBAColor] to format the color.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
func SetCursorColor(s string) string {
|
||||||
|
return "\x1b]12;" + s + "\x07"
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestCursorColor is a sequence that requests the current terminal cursor
|
||||||
|
// color.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
const RequestCursorColor = "\x1b]12;?\x07"
|
||||||
|
|
||||||
|
// ResetCursorColor is a sequence that resets the terminal cursor color.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
const ResetCursorColor = "\x1b]112\x07"
|
||||||
79
vendor/github.com/charmbracelet/x/ansi/c0.go
generated
vendored
Normal file
79
vendor/github.com/charmbracelet/x/ansi/c0.go
generated
vendored
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
// C0 control characters.
|
||||||
|
//
|
||||||
|
// These range from (0x00-0x1F) as defined in ISO 646 (ASCII).
|
||||||
|
// See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
||||||
|
const (
|
||||||
|
// NUL is the null character (Caret: ^@, Char: \0).
|
||||||
|
NUL = 0x00
|
||||||
|
// SOH is the start of heading character (Caret: ^A).
|
||||||
|
SOH = 0x01
|
||||||
|
// STX is the start of text character (Caret: ^B).
|
||||||
|
STX = 0x02
|
||||||
|
// ETX is the end of text character (Caret: ^C).
|
||||||
|
ETX = 0x03
|
||||||
|
// EOT is the end of transmission character (Caret: ^D).
|
||||||
|
EOT = 0x04
|
||||||
|
// ENQ is the enquiry character (Caret: ^E).
|
||||||
|
ENQ = 0x05
|
||||||
|
// ACK is the acknowledge character (Caret: ^F).
|
||||||
|
ACK = 0x06
|
||||||
|
// BEL is the bell character (Caret: ^G, Char: \a).
|
||||||
|
BEL = 0x07
|
||||||
|
// BS is the backspace character (Caret: ^H, Char: \b).
|
||||||
|
BS = 0x08
|
||||||
|
// HT is the horizontal tab character (Caret: ^I, Char: \t).
|
||||||
|
HT = 0x09
|
||||||
|
// LF is the line feed character (Caret: ^J, Char: \n).
|
||||||
|
LF = 0x0A
|
||||||
|
// VT is the vertical tab character (Caret: ^K, Char: \v).
|
||||||
|
VT = 0x0B
|
||||||
|
// FF is the form feed character (Caret: ^L, Char: \f).
|
||||||
|
FF = 0x0C
|
||||||
|
// CR is the carriage return character (Caret: ^M, Char: \r).
|
||||||
|
CR = 0x0D
|
||||||
|
// SO is the shift out character (Caret: ^N).
|
||||||
|
SO = 0x0E
|
||||||
|
// SI is the shift in character (Caret: ^O).
|
||||||
|
SI = 0x0F
|
||||||
|
// DLE is the data link escape character (Caret: ^P).
|
||||||
|
DLE = 0x10
|
||||||
|
// DC1 is the device control 1 character (Caret: ^Q).
|
||||||
|
DC1 = 0x11
|
||||||
|
// DC2 is the device control 2 character (Caret: ^R).
|
||||||
|
DC2 = 0x12
|
||||||
|
// DC3 is the device control 3 character (Caret: ^S).
|
||||||
|
DC3 = 0x13
|
||||||
|
// DC4 is the device control 4 character (Caret: ^T).
|
||||||
|
DC4 = 0x14
|
||||||
|
// NAK is the negative acknowledge character (Caret: ^U).
|
||||||
|
NAK = 0x15
|
||||||
|
// SYN is the synchronous idle character (Caret: ^V).
|
||||||
|
SYN = 0x16
|
||||||
|
// ETB is the end of transmission block character (Caret: ^W).
|
||||||
|
ETB = 0x17
|
||||||
|
// CAN is the cancel character (Caret: ^X).
|
||||||
|
CAN = 0x18
|
||||||
|
// EM is the end of medium character (Caret: ^Y).
|
||||||
|
EM = 0x19
|
||||||
|
// SUB is the substitute character (Caret: ^Z).
|
||||||
|
SUB = 0x1A
|
||||||
|
// ESC is the escape character (Caret: ^[, Char: \e).
|
||||||
|
ESC = 0x1B
|
||||||
|
// FS is the file separator character (Caret: ^\).
|
||||||
|
FS = 0x1C
|
||||||
|
// GS is the group separator character (Caret: ^]).
|
||||||
|
GS = 0x1D
|
||||||
|
// RS is the record separator character (Caret: ^^).
|
||||||
|
RS = 0x1E
|
||||||
|
// US is the unit separator character (Caret: ^_).
|
||||||
|
US = 0x1F
|
||||||
|
|
||||||
|
// LS0 is the locking shift 0 character.
|
||||||
|
// This is an alias for [SI].
|
||||||
|
LS0 = SI
|
||||||
|
// LS1 is the locking shift 1 character.
|
||||||
|
// This is an alias for [SO].
|
||||||
|
LS1 = SO
|
||||||
|
)
|
||||||
72
vendor/github.com/charmbracelet/x/ansi/c1.go
generated
vendored
Normal file
72
vendor/github.com/charmbracelet/x/ansi/c1.go
generated
vendored
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
// C1 control characters.
|
||||||
|
//
|
||||||
|
// These range from (0x80-0x9F) as defined in ISO 6429 (ECMA-48).
|
||||||
|
// See: https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
||||||
|
const (
|
||||||
|
// PAD is the padding character.
|
||||||
|
PAD = 0x80
|
||||||
|
// HOP is the high octet preset character.
|
||||||
|
HOP = 0x81
|
||||||
|
// BPH is the break permitted here character.
|
||||||
|
BPH = 0x82
|
||||||
|
// NBH is the no break here character.
|
||||||
|
NBH = 0x83
|
||||||
|
// IND is the index character.
|
||||||
|
IND = 0x84
|
||||||
|
// NEL is the next line character.
|
||||||
|
NEL = 0x85
|
||||||
|
// SSA is the start of selected area character.
|
||||||
|
SSA = 0x86
|
||||||
|
// ESA is the end of selected area character.
|
||||||
|
ESA = 0x87
|
||||||
|
// HTS is the horizontal tab set character.
|
||||||
|
HTS = 0x88
|
||||||
|
// HTJ is the horizontal tab with justification character.
|
||||||
|
HTJ = 0x89
|
||||||
|
// VTS is the vertical tab set character.
|
||||||
|
VTS = 0x8A
|
||||||
|
// PLD is the partial line forward character.
|
||||||
|
PLD = 0x8B
|
||||||
|
// PLU is the partial line backward character.
|
||||||
|
PLU = 0x8C
|
||||||
|
// RI is the reverse index character.
|
||||||
|
RI = 0x8D
|
||||||
|
// SS2 is the single shift 2 character.
|
||||||
|
SS2 = 0x8E
|
||||||
|
// SS3 is the single shift 3 character.
|
||||||
|
SS3 = 0x8F
|
||||||
|
// DCS is the device control string character.
|
||||||
|
DCS = 0x90
|
||||||
|
// PU1 is the private use 1 character.
|
||||||
|
PU1 = 0x91
|
||||||
|
// PU2 is the private use 2 character.
|
||||||
|
PU2 = 0x92
|
||||||
|
// STS is the set transmit state character.
|
||||||
|
STS = 0x93
|
||||||
|
// CCH is the cancel character.
|
||||||
|
CCH = 0x94
|
||||||
|
// MW is the message waiting character.
|
||||||
|
MW = 0x95
|
||||||
|
// SPA is the start of guarded area character.
|
||||||
|
SPA = 0x96
|
||||||
|
// EPA is the end of guarded area character.
|
||||||
|
EPA = 0x97
|
||||||
|
// SOS is the start of string character.
|
||||||
|
SOS = 0x98
|
||||||
|
// SGCI is the single graphic character introducer character.
|
||||||
|
SGCI = 0x99
|
||||||
|
// SCI is the single character introducer character.
|
||||||
|
SCI = 0x9A
|
||||||
|
// CSI is the control sequence introducer character.
|
||||||
|
CSI = 0x9B
|
||||||
|
// ST is the string terminator character.
|
||||||
|
ST = 0x9C
|
||||||
|
// OSC is the operating system command character.
|
||||||
|
OSC = 0x9D
|
||||||
|
// PM is the privacy message character.
|
||||||
|
PM = 0x9E
|
||||||
|
// APC is the application program command character.
|
||||||
|
APC = 0x9F
|
||||||
|
)
|
||||||
55
vendor/github.com/charmbracelet/x/ansi/charset.go
generated
vendored
Normal file
55
vendor/github.com/charmbracelet/x/ansi/charset.go
generated
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
// SelectCharacterSet sets the G-set character designator to the specified
|
||||||
|
// character set.
|
||||||
|
//
|
||||||
|
// ESC Ps Pd
|
||||||
|
//
|
||||||
|
// Where Ps is the G-set character designator, and Pd is the identifier.
|
||||||
|
// For 94-character sets, the designator can be one of:
|
||||||
|
// - ( G0
|
||||||
|
// - ) G1
|
||||||
|
// - * G2
|
||||||
|
// - + G3
|
||||||
|
//
|
||||||
|
// For 96-character sets, the designator can be one of:
|
||||||
|
// - - G1
|
||||||
|
// - . G2
|
||||||
|
// - / G3
|
||||||
|
//
|
||||||
|
// Some common 94-character sets are:
|
||||||
|
// - 0 DEC Special Drawing Set
|
||||||
|
// - A United Kingdom (UK)
|
||||||
|
// - B United States (USASCII)
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// ESC ( B Select character set G0 = United States (USASCII)
|
||||||
|
// ESC ( 0 Select character set G0 = Special Character and Line Drawing Set
|
||||||
|
// ESC ) 0 Select character set G1 = Special Character and Line Drawing Set
|
||||||
|
// ESC * A Select character set G2 = United Kingdom (UK)
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/SCS.html
|
||||||
|
func SelectCharacterSet(gset byte, charset byte) string {
|
||||||
|
return "\x1b" + string(gset) + string(charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SCS is an alias for SelectCharacterSet.
|
||||||
|
func SCS(gset byte, charset byte) string {
|
||||||
|
return SelectCharacterSet(gset, charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LS1R (Locking Shift 1 Right) shifts G1 into GR character set.
|
||||||
|
const LS1R = "\x1b~"
|
||||||
|
|
||||||
|
// LS2 (Locking Shift 2) shifts G2 into GL character set.
|
||||||
|
const LS2 = "\x1bn"
|
||||||
|
|
||||||
|
// LS2R (Locking Shift 2 Right) shifts G2 into GR character set.
|
||||||
|
const LS2R = "\x1b}"
|
||||||
|
|
||||||
|
// LS3 (Locking Shift 3) shifts G3 into GL character set.
|
||||||
|
const LS3 = "\x1bo"
|
||||||
|
|
||||||
|
// LS3R (Locking Shift 3 Right) shifts G3 into GR character set.
|
||||||
|
const LS3R = "\x1b|"
|
||||||
75
vendor/github.com/charmbracelet/x/ansi/clipboard.go
generated
vendored
Normal file
75
vendor/github.com/charmbracelet/x/ansi/clipboard.go
generated
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import "encoding/base64"
|
||||||
|
|
||||||
|
// Clipboard names.
|
||||||
|
const (
|
||||||
|
SystemClipboard = 'c'
|
||||||
|
PrimaryClipboard = 'p'
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetClipboard returns a sequence for manipulating the clipboard.
|
||||||
|
//
|
||||||
|
// OSC 52 ; Pc ; Pd ST
|
||||||
|
// OSC 52 ; Pc ; Pd BEL
|
||||||
|
//
|
||||||
|
// Where Pc is the clipboard name and Pd is the base64 encoded data.
|
||||||
|
// Empty data or invalid base64 data will reset the clipboard.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
func SetClipboard(c byte, d string) string {
|
||||||
|
if d != "" {
|
||||||
|
d = base64.StdEncoding.EncodeToString([]byte(d))
|
||||||
|
}
|
||||||
|
return "\x1b]52;" + string(c) + ";" + d + "\x07"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSystemClipboard returns a sequence for setting the system clipboard.
|
||||||
|
//
|
||||||
|
// This is equivalent to SetClipboard(SystemClipboard, d).
|
||||||
|
func SetSystemClipboard(d string) string {
|
||||||
|
return SetClipboard(SystemClipboard, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPrimaryClipboard returns a sequence for setting the primary clipboard.
|
||||||
|
//
|
||||||
|
// This is equivalent to SetClipboard(PrimaryClipboard, d).
|
||||||
|
func SetPrimaryClipboard(d string) string {
|
||||||
|
return SetClipboard(PrimaryClipboard, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetClipboard returns a sequence for resetting the clipboard.
|
||||||
|
//
|
||||||
|
// This is equivalent to SetClipboard(c, "").
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
func ResetClipboard(c byte) string {
|
||||||
|
return SetClipboard(c, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetSystemClipboard is a sequence for resetting the system clipboard.
|
||||||
|
//
|
||||||
|
// This is equivalent to ResetClipboard(SystemClipboard).
|
||||||
|
const ResetSystemClipboard = "\x1b]52;c;\x07"
|
||||||
|
|
||||||
|
// ResetPrimaryClipboard is a sequence for resetting the primary clipboard.
|
||||||
|
//
|
||||||
|
// This is equivalent to ResetClipboard(PrimaryClipboard).
|
||||||
|
const ResetPrimaryClipboard = "\x1b]52;p;\x07"
|
||||||
|
|
||||||
|
// RequestClipboard returns a sequence for requesting the clipboard.
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
||||||
|
func RequestClipboard(c byte) string {
|
||||||
|
return "\x1b]52;" + string(c) + ";?\x07"
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestSystemClipboard is a sequence for requesting the system clipboard.
|
||||||
|
//
|
||||||
|
// This is equivalent to RequestClipboard(SystemClipboard).
|
||||||
|
const RequestSystemClipboard = "\x1b]52;c;?\x07"
|
||||||
|
|
||||||
|
// RequestPrimaryClipboard is a sequence for requesting the primary clipboard.
|
||||||
|
//
|
||||||
|
// This is equivalent to RequestClipboard(PrimaryClipboard).
|
||||||
|
const RequestPrimaryClipboard = "\x1b]52;p;?\x07"
|
||||||
784
vendor/github.com/charmbracelet/x/ansi/color.go
generated
vendored
Normal file
784
vendor/github.com/charmbracelet/x/ansi/color.go
generated
vendored
Normal file
|
|
@ -0,0 +1,784 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Color is a color that can be used in a terminal. ANSI (including
|
||||||
|
// ANSI256) and 24-bit "true colors" fall under this category.
|
||||||
|
type Color interface {
|
||||||
|
color.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// BasicColor is an ANSI 3-bit or 4-bit color with a value from 0 to 15.
|
||||||
|
type BasicColor uint8
|
||||||
|
|
||||||
|
var _ Color = BasicColor(0)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Black is the ANSI black color.
|
||||||
|
Black BasicColor = iota
|
||||||
|
|
||||||
|
// Red is the ANSI red color.
|
||||||
|
Red
|
||||||
|
|
||||||
|
// Green is the ANSI green color.
|
||||||
|
Green
|
||||||
|
|
||||||
|
// Yellow is the ANSI yellow color.
|
||||||
|
Yellow
|
||||||
|
|
||||||
|
// Blue is the ANSI blue color.
|
||||||
|
Blue
|
||||||
|
|
||||||
|
// Magenta is the ANSI magenta color.
|
||||||
|
Magenta
|
||||||
|
|
||||||
|
// Cyan is the ANSI cyan color.
|
||||||
|
Cyan
|
||||||
|
|
||||||
|
// White is the ANSI white color.
|
||||||
|
White
|
||||||
|
|
||||||
|
// BrightBlack is the ANSI bright black color.
|
||||||
|
BrightBlack
|
||||||
|
|
||||||
|
// BrightRed is the ANSI bright red color.
|
||||||
|
BrightRed
|
||||||
|
|
||||||
|
// BrightGreen is the ANSI bright green color.
|
||||||
|
BrightGreen
|
||||||
|
|
||||||
|
// BrightYellow is the ANSI bright yellow color.
|
||||||
|
BrightYellow
|
||||||
|
|
||||||
|
// BrightBlue is the ANSI bright blue color.
|
||||||
|
BrightBlue
|
||||||
|
|
||||||
|
// BrightMagenta is the ANSI bright magenta color.
|
||||||
|
BrightMagenta
|
||||||
|
|
||||||
|
// BrightCyan is the ANSI bright cyan color.
|
||||||
|
BrightCyan
|
||||||
|
|
||||||
|
// BrightWhite is the ANSI bright white color.
|
||||||
|
BrightWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
// RGBA returns the red, green, blue and alpha components of the color. It
|
||||||
|
// satisfies the color.Color interface.
|
||||||
|
func (c BasicColor) RGBA() (uint32, uint32, uint32, uint32) {
|
||||||
|
ansi := uint32(c)
|
||||||
|
if ansi > 15 {
|
||||||
|
return 0, 0, 0, 0xffff
|
||||||
|
}
|
||||||
|
|
||||||
|
return ansiToRGB(byte(ansi)).RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexedColor is an ANSI 256 (8-bit) color with a value from 0 to 255.
|
||||||
|
type IndexedColor uint8
|
||||||
|
|
||||||
|
var _ Color = IndexedColor(0)
|
||||||
|
|
||||||
|
// RGBA returns the red, green, blue and alpha components of the color. It
|
||||||
|
// satisfies the color.Color interface.
|
||||||
|
func (c IndexedColor) RGBA() (uint32, uint32, uint32, uint32) {
|
||||||
|
return ansiToRGB(byte(c)).RGBA()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendedColor is an ANSI 256 (8-bit) color with a value from 0 to 255.
|
||||||
|
//
|
||||||
|
// Deprecated: use [IndexedColor] instead.
|
||||||
|
type ExtendedColor = IndexedColor
|
||||||
|
|
||||||
|
// TrueColor is a 24-bit color that can be used in the terminal.
|
||||||
|
// This can be used to represent RGB colors.
|
||||||
|
//
|
||||||
|
// For example, the color red can be represented as:
|
||||||
|
//
|
||||||
|
// TrueColor(0xff0000)
|
||||||
|
//
|
||||||
|
// Deprecated: use [RGBColor] instead.
|
||||||
|
type TrueColor uint32
|
||||||
|
|
||||||
|
var _ Color = TrueColor(0)
|
||||||
|
|
||||||
|
// RGBA returns the red, green, blue and alpha components of the color. It
|
||||||
|
// satisfies the color.Color interface.
|
||||||
|
func (c TrueColor) RGBA() (uint32, uint32, uint32, uint32) {
|
||||||
|
r, g, b := hexToRGB(uint32(c))
|
||||||
|
return toRGBA(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBColor is a 24-bit color that can be used in the terminal.
|
||||||
|
// This can be used to represent RGB colors.
|
||||||
|
type RGBColor struct {
|
||||||
|
R uint8
|
||||||
|
G uint8
|
||||||
|
B uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA returns the red, green, blue and alpha components of the color. It
|
||||||
|
// satisfies the color.Color interface.
|
||||||
|
func (c RGBColor) RGBA() (uint32, uint32, uint32, uint32) {
|
||||||
|
return toRGBA(uint32(c.R), uint32(c.G), uint32(c.B))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ansiToRGB converts an ANSI color to a 24-bit RGB color.
|
||||||
|
//
|
||||||
|
// r, g, b := ansiToRGB(57)
|
||||||
|
func ansiToRGB(ansi byte) color.Color {
|
||||||
|
return ansiHex[ansi]
|
||||||
|
}
|
||||||
|
|
||||||
|
// hexToRGB converts a number in hexadecimal format to red, green, and blue
|
||||||
|
// values.
|
||||||
|
//
|
||||||
|
// r, g, b := hexToRGB(0x0000FF)
|
||||||
|
func hexToRGB(hex uint32) (uint32, uint32, uint32) {
|
||||||
|
return hex >> 16 & 0xff, hex >> 8 & 0xff, hex & 0xff
|
||||||
|
}
|
||||||
|
|
||||||
|
// toRGBA converts an RGB 8-bit color values to 32-bit color values suitable
|
||||||
|
// for color.Color.
|
||||||
|
//
|
||||||
|
// color.Color requires 16-bit color values, so we duplicate the 8-bit values
|
||||||
|
// to fill the 16-bit values.
|
||||||
|
//
|
||||||
|
// This always returns 0xffff (opaque) for the alpha channel.
|
||||||
|
func toRGBA(r, g, b uint32) (uint32, uint32, uint32, uint32) {
|
||||||
|
r |= r << 8
|
||||||
|
g |= g << 8
|
||||||
|
b |= b << 8
|
||||||
|
return r, g, b, 0xffff
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:unused
|
||||||
|
func distSq(r1, g1, b1, r2, g2, b2 int) int {
|
||||||
|
return ((r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func to6Cube[T int | float64](v T) int {
|
||||||
|
if v < 48 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if v < 115 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return int((v - 35) / 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert256 converts a [color.Color], usually a 24-bit color, to xterm(1) 256
|
||||||
|
// color palette.
|
||||||
|
//
|
||||||
|
// xterm provides a 6x6x6 color cube (16 - 231) and 24 greys (232 - 255). We
|
||||||
|
// map our RGB color to the closest in the cube, also work out the closest
|
||||||
|
// grey, and use the nearest of the two based on the lightness of the color.
|
||||||
|
//
|
||||||
|
// Note that the xterm has much lower resolution for darker colors (they are
|
||||||
|
// not evenly spread out), so our 6 levels are not evenly spread: 0x0, 0x5f
|
||||||
|
// (95), 0x87 (135), 0xaf (175), 0xd7 (215) and 0xff (255). Greys are more
|
||||||
|
// evenly spread (8, 18, 28 ... 238).
|
||||||
|
func Convert256(c color.Color) IndexedColor {
|
||||||
|
// If the color is already an IndexedColor, return it.
|
||||||
|
if i, ok := c.(IndexedColor); ok {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: this is mostly ported from tmux/colour.c.
|
||||||
|
col, ok := colorful.MakeColor(c)
|
||||||
|
if !ok {
|
||||||
|
return IndexedColor(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := col.R * 255
|
||||||
|
g := col.G * 255
|
||||||
|
b := col.B * 255
|
||||||
|
|
||||||
|
q2c := [6]int{0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff}
|
||||||
|
|
||||||
|
// Map RGB to 6x6x6 cube.
|
||||||
|
qr := to6Cube(r)
|
||||||
|
cr := q2c[qr]
|
||||||
|
qg := to6Cube(g)
|
||||||
|
cg := q2c[qg]
|
||||||
|
qb := to6Cube(b)
|
||||||
|
cb := q2c[qb]
|
||||||
|
|
||||||
|
// If we have hit the color exactly, return early.
|
||||||
|
ci := (36 * qr) + (6 * qg) + qb
|
||||||
|
if cr == int(r) && cg == int(g) && cb == int(b) {
|
||||||
|
return IndexedColor(16 + ci) //nolint:gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Work out the closest grey (average of RGB).
|
||||||
|
greyAvg := int(r+g+b) / 3
|
||||||
|
var greyIdx int
|
||||||
|
if greyAvg > 238 {
|
||||||
|
greyIdx = 23
|
||||||
|
} else {
|
||||||
|
greyIdx = (greyAvg - 3) / 10
|
||||||
|
}
|
||||||
|
grey := 8 + (10 * greyIdx)
|
||||||
|
|
||||||
|
// Return the one which is nearer to the original input rgb value
|
||||||
|
// XXX: This is where it differs from tmux's implementation, we prefer the
|
||||||
|
// closer color to the original in terms of light distances rather than the
|
||||||
|
// cube distance.
|
||||||
|
c2 := colorful.Color{R: float64(cr) / 255.0, G: float64(cg) / 255.0, B: float64(cb) / 255.0}
|
||||||
|
g2 := colorful.Color{R: float64(grey) / 255.0, G: float64(grey) / 255.0, B: float64(grey) / 255.0}
|
||||||
|
colorDist := col.DistanceHSLuv(c2)
|
||||||
|
grayDist := col.DistanceHSLuv(g2)
|
||||||
|
|
||||||
|
if colorDist <= grayDist {
|
||||||
|
return IndexedColor(16 + ci) //nolint:gosec
|
||||||
|
}
|
||||||
|
return IndexedColor(232 + greyIdx) //nolint:gosec
|
||||||
|
|
||||||
|
// // Is grey or 6x6x6 color closest?
|
||||||
|
// d := distSq(cr, cg, cb, int(r), int(g), int(b))
|
||||||
|
// if distSq(grey, grey, grey, int(r), int(g), int(b)) < d {
|
||||||
|
// return IndexedColor(232 + greyIdx) //nolint:gosec
|
||||||
|
// }
|
||||||
|
// return IndexedColor(16 + ci) //nolint:gosec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert16 converts a [color.Color] to a 16-color ANSI color. It will first
|
||||||
|
// try to find a match in the 256 xterm(1) color palette, and then map that to
|
||||||
|
// the 16-color ANSI palette.
|
||||||
|
func Convert16(c color.Color) BasicColor {
|
||||||
|
switch c := c.(type) {
|
||||||
|
case BasicColor:
|
||||||
|
// If the color is already a BasicColor, return it.
|
||||||
|
return c
|
||||||
|
case IndexedColor:
|
||||||
|
// If the color is already an IndexedColor, return the corresponding
|
||||||
|
// BasicColor.
|
||||||
|
return ansi256To16[c]
|
||||||
|
default:
|
||||||
|
c256 := Convert256(c)
|
||||||
|
return ansi256To16[c256]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGB values of ANSI colors (0-255).
|
||||||
|
var ansiHex = [...]color.RGBA{
|
||||||
|
0: {R: 0x00, G: 0x00, B: 0x00, A: 0xff}, // "#000000"
|
||||||
|
1: {R: 0x80, G: 0x00, B: 0x00, A: 0xff}, // "#800000"
|
||||||
|
2: {R: 0x00, G: 0x80, B: 0x00, A: 0xff}, // "#008000"
|
||||||
|
3: {R: 0x80, G: 0x80, B: 0x00, A: 0xff}, // "#808000"
|
||||||
|
4: {R: 0x00, G: 0x00, B: 0x80, A: 0xff}, // "#000080"
|
||||||
|
5: {R: 0x80, G: 0x00, B: 0x80, A: 0xff}, // "#800080"
|
||||||
|
6: {R: 0x00, G: 0x80, B: 0x80, A: 0xff}, // "#008080"
|
||||||
|
7: {R: 0xc0, G: 0xc0, B: 0xc0, A: 0xff}, // "#c0c0c0"
|
||||||
|
8: {R: 0x80, G: 0x80, B: 0x80, A: 0xff}, // "#808080"
|
||||||
|
9: {R: 0xff, G: 0x00, B: 0x00, A: 0xff}, // "#ff0000"
|
||||||
|
10: {R: 0x00, G: 0xff, B: 0x00, A: 0xff}, // "#00ff00"
|
||||||
|
11: {R: 0xff, G: 0xff, B: 0x00, A: 0xff}, // "#ffff00"
|
||||||
|
12: {R: 0x00, G: 0x00, B: 0xff, A: 0xff}, // "#0000ff"
|
||||||
|
13: {R: 0xff, G: 0x00, B: 0xff, A: 0xff}, // "#ff00ff"
|
||||||
|
14: {R: 0x00, G: 0xff, B: 0xff, A: 0xff}, // "#00ffff"
|
||||||
|
15: {R: 0xff, G: 0xff, B: 0xff, A: 0xff}, // "#ffffff"
|
||||||
|
16: {R: 0x00, G: 0x00, B: 0x00, A: 0xff}, // "#000000"
|
||||||
|
17: {R: 0x00, G: 0x00, B: 0x5f, A: 0xff}, // "#00005f"
|
||||||
|
18: {R: 0x00, G: 0x00, B: 0x87, A: 0xff}, // "#000087"
|
||||||
|
19: {R: 0x00, G: 0x00, B: 0xaf, A: 0xff}, // "#0000af"
|
||||||
|
20: {R: 0x00, G: 0x00, B: 0xd7, A: 0xff}, // "#0000d7"
|
||||||
|
21: {R: 0x00, G: 0x00, B: 0xff, A: 0xff}, // "#0000ff"
|
||||||
|
22: {R: 0x00, G: 0x5f, B: 0x00, A: 0xff}, // "#005f00"
|
||||||
|
23: {R: 0x00, G: 0x5f, B: 0x5f, A: 0xff}, // "#005f5f"
|
||||||
|
24: {R: 0x00, G: 0x5f, B: 0x87, A: 0xff}, // "#005f87"
|
||||||
|
25: {R: 0x00, G: 0x5f, B: 0xaf, A: 0xff}, // "#005faf"
|
||||||
|
26: {R: 0x00, G: 0x5f, B: 0xd7, A: 0xff}, // "#005fd7"
|
||||||
|
27: {R: 0x00, G: 0x5f, B: 0xff, A: 0xff}, // "#005fff"
|
||||||
|
28: {R: 0x00, G: 0x87, B: 0x00, A: 0xff}, // "#008700"
|
||||||
|
29: {R: 0x00, G: 0x87, B: 0x5f, A: 0xff}, // "#00875f"
|
||||||
|
30: {R: 0x00, G: 0x87, B: 0x87, A: 0xff}, // "#008787"
|
||||||
|
31: {R: 0x00, G: 0x87, B: 0xaf, A: 0xff}, // "#0087af"
|
||||||
|
32: {R: 0x00, G: 0x87, B: 0xd7, A: 0xff}, // "#0087d7"
|
||||||
|
33: {R: 0x00, G: 0x87, B: 0xff, A: 0xff}, // "#0087ff"
|
||||||
|
34: {R: 0x00, G: 0xaf, B: 0x00, A: 0xff}, // "#00af00"
|
||||||
|
35: {R: 0x00, G: 0xaf, B: 0x5f, A: 0xff}, // "#00af5f"
|
||||||
|
36: {R: 0x00, G: 0xaf, B: 0x87, A: 0xff}, // "#00af87"
|
||||||
|
37: {R: 0x00, G: 0xaf, B: 0xaf, A: 0xff}, // "#00afaf"
|
||||||
|
38: {R: 0x00, G: 0xaf, B: 0xd7, A: 0xff}, // "#00afd7"
|
||||||
|
39: {R: 0x00, G: 0xaf, B: 0xff, A: 0xff}, // "#00afff"
|
||||||
|
40: {R: 0x00, G: 0xd7, B: 0x00, A: 0xff}, // "#00d700"
|
||||||
|
41: {R: 0x00, G: 0xd7, B: 0x5f, A: 0xff}, // "#00d75f"
|
||||||
|
42: {R: 0x00, G: 0xd7, B: 0x87, A: 0xff}, // "#00d787"
|
||||||
|
43: {R: 0x00, G: 0xd7, B: 0xaf, A: 0xff}, // "#00d7af"
|
||||||
|
44: {R: 0x00, G: 0xd7, B: 0xd7, A: 0xff}, // "#00d7d7"
|
||||||
|
45: {R: 0x00, G: 0xd7, B: 0xff, A: 0xff}, // "#00d7ff"
|
||||||
|
46: {R: 0x00, G: 0xff, B: 0x00, A: 0xff}, // "#00ff00"
|
||||||
|
47: {R: 0x00, G: 0xff, B: 0x5f, A: 0xff}, // "#00ff5f"
|
||||||
|
48: {R: 0x00, G: 0xff, B: 0x87, A: 0xff}, // "#00ff87"
|
||||||
|
49: {R: 0x00, G: 0xff, B: 0xaf, A: 0xff}, // "#00ffaf"
|
||||||
|
50: {R: 0x00, G: 0xff, B: 0xd7, A: 0xff}, // "#00ffd7"
|
||||||
|
51: {R: 0x00, G: 0xff, B: 0xff, A: 0xff}, // "#00ffff"
|
||||||
|
52: {R: 0x5f, G: 0x00, B: 0x00, A: 0xff}, // "#5f0000"
|
||||||
|
53: {R: 0x5f, G: 0x00, B: 0x5f, A: 0xff}, // "#5f005f"
|
||||||
|
54: {R: 0x5f, G: 0x00, B: 0x87, A: 0xff}, // "#5f0087"
|
||||||
|
55: {R: 0x5f, G: 0x00, B: 0xaf, A: 0xff}, // "#5f00af"
|
||||||
|
56: {R: 0x5f, G: 0x00, B: 0xd7, A: 0xff}, // "#5f00d7"
|
||||||
|
57: {R: 0x5f, G: 0x00, B: 0xff, A: 0xff}, // "#5f00ff"
|
||||||
|
58: {R: 0x5f, G: 0x5f, B: 0x00, A: 0xff}, // "#5f5f00"
|
||||||
|
59: {R: 0x5f, G: 0x5f, B: 0x5f, A: 0xff}, // "#5f5f5f"
|
||||||
|
60: {R: 0x5f, G: 0x5f, B: 0x87, A: 0xff}, // "#5f5f87"
|
||||||
|
61: {R: 0x5f, G: 0x5f, B: 0xaf, A: 0xff}, // "#5f5faf"
|
||||||
|
62: {R: 0x5f, G: 0x5f, B: 0xd7, A: 0xff}, // "#5f5fd7"
|
||||||
|
63: {R: 0x5f, G: 0x5f, B: 0xff, A: 0xff}, // "#5f5fff"
|
||||||
|
64: {R: 0x5f, G: 0x87, B: 0x00, A: 0xff}, // "#5f8700"
|
||||||
|
65: {R: 0x5f, G: 0x87, B: 0x5f, A: 0xff}, // "#5f875f"
|
||||||
|
66: {R: 0x5f, G: 0x87, B: 0x87, A: 0xff}, // "#5f8787"
|
||||||
|
67: {R: 0x5f, G: 0x87, B: 0xaf, A: 0xff}, // "#5f87af"
|
||||||
|
68: {R: 0x5f, G: 0x87, B: 0xd7, A: 0xff}, // "#5f87d7"
|
||||||
|
69: {R: 0x5f, G: 0x87, B: 0xff, A: 0xff}, // "#5f87ff"
|
||||||
|
70: {R: 0x5f, G: 0xaf, B: 0x00, A: 0xff}, // "#5faf00"
|
||||||
|
71: {R: 0x5f, G: 0xaf, B: 0x5f, A: 0xff}, // "#5faf5f"
|
||||||
|
72: {R: 0x5f, G: 0xaf, B: 0x87, A: 0xff}, // "#5faf87"
|
||||||
|
73: {R: 0x5f, G: 0xaf, B: 0xaf, A: 0xff}, // "#5fafaf"
|
||||||
|
74: {R: 0x5f, G: 0xaf, B: 0xd7, A: 0xff}, // "#5fafd7"
|
||||||
|
75: {R: 0x5f, G: 0xaf, B: 0xff, A: 0xff}, // "#5fafff"
|
||||||
|
76: {R: 0x5f, G: 0xd7, B: 0x00, A: 0xff}, // "#5fd700"
|
||||||
|
77: {R: 0x5f, G: 0xd7, B: 0x5f, A: 0xff}, // "#5fd75f"
|
||||||
|
78: {R: 0x5f, G: 0xd7, B: 0x87, A: 0xff}, // "#5fd787"
|
||||||
|
79: {R: 0x5f, G: 0xd7, B: 0xaf, A: 0xff}, // "#5fd7af"
|
||||||
|
80: {R: 0x5f, G: 0xd7, B: 0xd7, A: 0xff}, // "#5fd7d7"
|
||||||
|
81: {R: 0x5f, G: 0xd7, B: 0xff, A: 0xff}, // "#5fd7ff"
|
||||||
|
82: {R: 0x5f, G: 0xff, B: 0x00, A: 0xff}, // "#5fff00"
|
||||||
|
83: {R: 0x5f, G: 0xff, B: 0x5f, A: 0xff}, // "#5fff5f"
|
||||||
|
84: {R: 0x5f, G: 0xff, B: 0x87, A: 0xff}, // "#5fff87"
|
||||||
|
85: {R: 0x5f, G: 0xff, B: 0xaf, A: 0xff}, // "#5fffaf"
|
||||||
|
86: {R: 0x5f, G: 0xff, B: 0xd7, A: 0xff}, // "#5fffd7"
|
||||||
|
87: {R: 0x5f, G: 0xff, B: 0xff, A: 0xff}, // "#5fffff"
|
||||||
|
88: {R: 0x87, G: 0x00, B: 0x00, A: 0xff}, // "#870000"
|
||||||
|
89: {R: 0x87, G: 0x00, B: 0x5f, A: 0xff}, // "#87005f"
|
||||||
|
90: {R: 0x87, G: 0x00, B: 0x87, A: 0xff}, // "#870087"
|
||||||
|
91: {R: 0x87, G: 0x00, B: 0xaf, A: 0xff}, // "#8700af"
|
||||||
|
92: {R: 0x87, G: 0x00, B: 0xd7, A: 0xff}, // "#8700d7"
|
||||||
|
93: {R: 0x87, G: 0x00, B: 0xff, A: 0xff}, // "#8700ff"
|
||||||
|
94: {R: 0x87, G: 0x5f, B: 0x00, A: 0xff}, // "#875f00"
|
||||||
|
95: {R: 0x87, G: 0x5f, B: 0x5f, A: 0xff}, // "#875f5f"
|
||||||
|
96: {R: 0x87, G: 0x5f, B: 0x87, A: 0xff}, // "#875f87"
|
||||||
|
97: {R: 0x87, G: 0x5f, B: 0xaf, A: 0xff}, // "#875faf"
|
||||||
|
98: {R: 0x87, G: 0x5f, B: 0xd7, A: 0xff}, // "#875fd7"
|
||||||
|
99: {R: 0x87, G: 0x5f, B: 0xff, A: 0xff}, // "#875fff"
|
||||||
|
100: {R: 0x87, G: 0x87, B: 0x00, A: 0xff}, // "#878700"
|
||||||
|
101: {R: 0x87, G: 0x87, B: 0x5f, A: 0xff}, // "#87875f"
|
||||||
|
102: {R: 0x87, G: 0x87, B: 0x87, A: 0xff}, // "#878787"
|
||||||
|
103: {R: 0x87, G: 0x87, B: 0xaf, A: 0xff}, // "#8787af"
|
||||||
|
104: {R: 0x87, G: 0x87, B: 0xd7, A: 0xff}, // "#8787d7"
|
||||||
|
105: {R: 0x87, G: 0x87, B: 0xff, A: 0xff}, // "#8787ff"
|
||||||
|
106: {R: 0x87, G: 0xaf, B: 0x00, A: 0xff}, // "#87af00"
|
||||||
|
107: {R: 0x87, G: 0xaf, B: 0x5f, A: 0xff}, // "#87af5f"
|
||||||
|
108: {R: 0x87, G: 0xaf, B: 0x87, A: 0xff}, // "#87af87"
|
||||||
|
109: {R: 0x87, G: 0xaf, B: 0xaf, A: 0xff}, // "#87afaf"
|
||||||
|
110: {R: 0x87, G: 0xaf, B: 0xd7, A: 0xff}, // "#87afd7"
|
||||||
|
111: {R: 0x87, G: 0xaf, B: 0xff, A: 0xff}, // "#87afff"
|
||||||
|
112: {R: 0x87, G: 0xd7, B: 0x00, A: 0xff}, // "#87d700"
|
||||||
|
113: {R: 0x87, G: 0xd7, B: 0x5f, A: 0xff}, // "#87d75f"
|
||||||
|
114: {R: 0x87, G: 0xd7, B: 0x87, A: 0xff}, // "#87d787"
|
||||||
|
115: {R: 0x87, G: 0xd7, B: 0xaf, A: 0xff}, // "#87d7af"
|
||||||
|
116: {R: 0x87, G: 0xd7, B: 0xd7, A: 0xff}, // "#87d7d7"
|
||||||
|
117: {R: 0x87, G: 0xd7, B: 0xff, A: 0xff}, // "#87d7ff"
|
||||||
|
118: {R: 0x87, G: 0xff, B: 0x00, A: 0xff}, // "#87ff00"
|
||||||
|
119: {R: 0x87, G: 0xff, B: 0x5f, A: 0xff}, // "#87ff5f"
|
||||||
|
120: {R: 0x87, G: 0xff, B: 0x87, A: 0xff}, // "#87ff87"
|
||||||
|
121: {R: 0x87, G: 0xff, B: 0xaf, A: 0xff}, // "#87ffaf"
|
||||||
|
122: {R: 0x87, G: 0xff, B: 0xd7, A: 0xff}, // "#87ffd7"
|
||||||
|
123: {R: 0x87, G: 0xff, B: 0xff, A: 0xff}, // "#87ffff"
|
||||||
|
124: {R: 0xaf, G: 0x00, B: 0x00, A: 0xff}, // "#af0000"
|
||||||
|
125: {R: 0xaf, G: 0x00, B: 0x5f, A: 0xff}, // "#af005f"
|
||||||
|
126: {R: 0xaf, G: 0x00, B: 0x87, A: 0xff}, // "#af0087"
|
||||||
|
127: {R: 0xaf, G: 0x00, B: 0xaf, A: 0xff}, // "#af00af"
|
||||||
|
128: {R: 0xaf, G: 0x00, B: 0xd7, A: 0xff}, // "#af00d7"
|
||||||
|
129: {R: 0xaf, G: 0x00, B: 0xff, A: 0xff}, // "#af00ff"
|
||||||
|
130: {R: 0xaf, G: 0x5f, B: 0x00, A: 0xff}, // "#af5f00"
|
||||||
|
131: {R: 0xaf, G: 0x5f, B: 0x5f, A: 0xff}, // "#af5f5f"
|
||||||
|
132: {R: 0xaf, G: 0x5f, B: 0x87, A: 0xff}, // "#af5f87"
|
||||||
|
133: {R: 0xaf, G: 0x5f, B: 0xaf, A: 0xff}, // "#af5faf"
|
||||||
|
134: {R: 0xaf, G: 0x5f, B: 0xd7, A: 0xff}, // "#af5fd7"
|
||||||
|
135: {R: 0xaf, G: 0x5f, B: 0xff, A: 0xff}, // "#af5fff"
|
||||||
|
136: {R: 0xaf, G: 0x87, B: 0x00, A: 0xff}, // "#af8700"
|
||||||
|
137: {R: 0xaf, G: 0x87, B: 0x5f, A: 0xff}, // "#af875f"
|
||||||
|
138: {R: 0xaf, G: 0x87, B: 0x87, A: 0xff}, // "#af8787"
|
||||||
|
139: {R: 0xaf, G: 0x87, B: 0xaf, A: 0xff}, // "#af87af"
|
||||||
|
140: {R: 0xaf, G: 0x87, B: 0xd7, A: 0xff}, // "#af87d7"
|
||||||
|
141: {R: 0xaf, G: 0x87, B: 0xff, A: 0xff}, // "#af87ff"
|
||||||
|
142: {R: 0xaf, G: 0xaf, B: 0x00, A: 0xff}, // "#afaf00"
|
||||||
|
143: {R: 0xaf, G: 0xaf, B: 0x5f, A: 0xff}, // "#afaf5f"
|
||||||
|
144: {R: 0xaf, G: 0xaf, B: 0x87, A: 0xff}, // "#afaf87"
|
||||||
|
145: {R: 0xaf, G: 0xaf, B: 0xaf, A: 0xff}, // "#afafaf"
|
||||||
|
146: {R: 0xaf, G: 0xaf, B: 0xd7, A: 0xff}, // "#afafd7"
|
||||||
|
147: {R: 0xaf, G: 0xaf, B: 0xff, A: 0xff}, // "#afafff"
|
||||||
|
148: {R: 0xaf, G: 0xd7, B: 0x00, A: 0xff}, // "#afd700"
|
||||||
|
149: {R: 0xaf, G: 0xd7, B: 0x5f, A: 0xff}, // "#afd75f"
|
||||||
|
150: {R: 0xaf, G: 0xd7, B: 0x87, A: 0xff}, // "#afd787"
|
||||||
|
151: {R: 0xaf, G: 0xd7, B: 0xaf, A: 0xff}, // "#afd7af"
|
||||||
|
152: {R: 0xaf, G: 0xd7, B: 0xd7, A: 0xff}, // "#afd7d7"
|
||||||
|
153: {R: 0xaf, G: 0xd7, B: 0xff, A: 0xff}, // "#afd7ff"
|
||||||
|
154: {R: 0xaf, G: 0xff, B: 0x00, A: 0xff}, // "#afff00"
|
||||||
|
155: {R: 0xaf, G: 0xff, B: 0x5f, A: 0xff}, // "#afff5f"
|
||||||
|
156: {R: 0xaf, G: 0xff, B: 0x87, A: 0xff}, // "#afff87"
|
||||||
|
157: {R: 0xaf, G: 0xff, B: 0xaf, A: 0xff}, // "#afffaf"
|
||||||
|
158: {R: 0xaf, G: 0xff, B: 0xd7, A: 0xff}, // "#afffd7"
|
||||||
|
159: {R: 0xaf, G: 0xff, B: 0xff, A: 0xff}, // "#afffff"
|
||||||
|
160: {R: 0xd7, G: 0x00, B: 0x00, A: 0xff}, // "#d70000"
|
||||||
|
161: {R: 0xd7, G: 0x00, B: 0x5f, A: 0xff}, // "#d7005f"
|
||||||
|
162: {R: 0xd7, G: 0x00, B: 0x87, A: 0xff}, // "#d70087"
|
||||||
|
163: {R: 0xd7, G: 0x00, B: 0xaf, A: 0xff}, // "#d700af"
|
||||||
|
164: {R: 0xd7, G: 0x00, B: 0xd7, A: 0xff}, // "#d700d7"
|
||||||
|
165: {R: 0xd7, G: 0x00, B: 0xff, A: 0xff}, // "#d700ff"
|
||||||
|
166: {R: 0xd7, G: 0x5f, B: 0x00, A: 0xff}, // "#d75f00"
|
||||||
|
167: {R: 0xd7, G: 0x5f, B: 0x5f, A: 0xff}, // "#d75f5f"
|
||||||
|
168: {R: 0xd7, G: 0x5f, B: 0x87, A: 0xff}, // "#d75f87"
|
||||||
|
169: {R: 0xd7, G: 0x5f, B: 0xaf, A: 0xff}, // "#d75faf"
|
||||||
|
170: {R: 0xd7, G: 0x5f, B: 0xd7, A: 0xff}, // "#d75fd7"
|
||||||
|
171: {R: 0xd7, G: 0x5f, B: 0xff, A: 0xff}, // "#d75fff"
|
||||||
|
172: {R: 0xd7, G: 0x87, B: 0x00, A: 0xff}, // "#d78700"
|
||||||
|
173: {R: 0xd7, G: 0x87, B: 0x5f, A: 0xff}, // "#d7875f"
|
||||||
|
174: {R: 0xd7, G: 0x87, B: 0x87, A: 0xff}, // "#d78787"
|
||||||
|
175: {R: 0xd7, G: 0x87, B: 0xaf, A: 0xff}, // "#d787af"
|
||||||
|
176: {R: 0xd7, G: 0x87, B: 0xd7, A: 0xff}, // "#d787d7"
|
||||||
|
177: {R: 0xd7, G: 0x87, B: 0xff, A: 0xff}, // "#d787ff"
|
||||||
|
178: {R: 0xd7, G: 0xaf, B: 0x00, A: 0xff}, // "#d7af00"
|
||||||
|
179: {R: 0xd7, G: 0xaf, B: 0x5f, A: 0xff}, // "#d7af5f"
|
||||||
|
180: {R: 0xd7, G: 0xaf, B: 0x87, A: 0xff}, // "#d7af87"
|
||||||
|
181: {R: 0xd7, G: 0xaf, B: 0xaf, A: 0xff}, // "#d7afaf"
|
||||||
|
182: {R: 0xd7, G: 0xaf, B: 0xd7, A: 0xff}, // "#d7afd7"
|
||||||
|
183: {R: 0xd7, G: 0xaf, B: 0xff, A: 0xff}, // "#d7afff"
|
||||||
|
184: {R: 0xd7, G: 0xd7, B: 0x00, A: 0xff}, // "#d7d700"
|
||||||
|
185: {R: 0xd7, G: 0xd7, B: 0x5f, A: 0xff}, // "#d7d75f"
|
||||||
|
186: {R: 0xd7, G: 0xd7, B: 0x87, A: 0xff}, // "#d7d787"
|
||||||
|
187: {R: 0xd7, G: 0xd7, B: 0xaf, A: 0xff}, // "#d7d7af"
|
||||||
|
188: {R: 0xd7, G: 0xd7, B: 0xd7, A: 0xff}, // "#d7d7d7"
|
||||||
|
189: {R: 0xd7, G: 0xd7, B: 0xff, A: 0xff}, // "#d7d7ff"
|
||||||
|
190: {R: 0xd7, G: 0xff, B: 0x00, A: 0xff}, // "#d7ff00"
|
||||||
|
191: {R: 0xd7, G: 0xff, B: 0x5f, A: 0xff}, // "#d7ff5f"
|
||||||
|
192: {R: 0xd7, G: 0xff, B: 0x87, A: 0xff}, // "#d7ff87"
|
||||||
|
193: {R: 0xd7, G: 0xff, B: 0xaf, A: 0xff}, // "#d7ffaf"
|
||||||
|
194: {R: 0xd7, G: 0xff, B: 0xd7, A: 0xff}, // "#d7ffd7"
|
||||||
|
195: {R: 0xd7, G: 0xff, B: 0xff, A: 0xff}, // "#d7ffff"
|
||||||
|
196: {R: 0xff, G: 0x00, B: 0x00, A: 0xff}, // "#ff0000"
|
||||||
|
197: {R: 0xff, G: 0x00, B: 0x5f, A: 0xff}, // "#ff005f"
|
||||||
|
198: {R: 0xff, G: 0x00, B: 0x87, A: 0xff}, // "#ff0087"
|
||||||
|
199: {R: 0xff, G: 0x00, B: 0xaf, A: 0xff}, // "#ff00af"
|
||||||
|
200: {R: 0xff, G: 0x00, B: 0xd7, A: 0xff}, // "#ff00d7"
|
||||||
|
201: {R: 0xff, G: 0x00, B: 0xff, A: 0xff}, // "#ff00ff"
|
||||||
|
202: {R: 0xff, G: 0x5f, B: 0x00, A: 0xff}, // "#ff5f00"
|
||||||
|
203: {R: 0xff, G: 0x5f, B: 0x5f, A: 0xff}, // "#ff5f5f"
|
||||||
|
204: {R: 0xff, G: 0x5f, B: 0x87, A: 0xff}, // "#ff5f87"
|
||||||
|
205: {R: 0xff, G: 0x5f, B: 0xaf, A: 0xff}, // "#ff5faf"
|
||||||
|
206: {R: 0xff, G: 0x5f, B: 0xd7, A: 0xff}, // "#ff5fd7"
|
||||||
|
207: {R: 0xff, G: 0x5f, B: 0xff, A: 0xff}, // "#ff5fff"
|
||||||
|
208: {R: 0xff, G: 0x87, B: 0x00, A: 0xff}, // "#ff8700"
|
||||||
|
209: {R: 0xff, G: 0x87, B: 0x5f, A: 0xff}, // "#ff875f"
|
||||||
|
210: {R: 0xff, G: 0x87, B: 0x87, A: 0xff}, // "#ff8787"
|
||||||
|
211: {R: 0xff, G: 0x87, B: 0xaf, A: 0xff}, // "#ff87af"
|
||||||
|
212: {R: 0xff, G: 0x87, B: 0xd7, A: 0xff}, // "#ff87d7"
|
||||||
|
213: {R: 0xff, G: 0x87, B: 0xff, A: 0xff}, // "#ff87ff"
|
||||||
|
214: {R: 0xff, G: 0xaf, B: 0x00, A: 0xff}, // "#ffaf00"
|
||||||
|
215: {R: 0xff, G: 0xaf, B: 0x5f, A: 0xff}, // "#ffaf5f"
|
||||||
|
216: {R: 0xff, G: 0xaf, B: 0x87, A: 0xff}, // "#ffaf87"
|
||||||
|
217: {R: 0xff, G: 0xaf, B: 0xaf, A: 0xff}, // "#ffafaf"
|
||||||
|
218: {R: 0xff, G: 0xaf, B: 0xd7, A: 0xff}, // "#ffafd7"
|
||||||
|
219: {R: 0xff, G: 0xaf, B: 0xff, A: 0xff}, // "#ffafff"
|
||||||
|
220: {R: 0xff, G: 0xd7, B: 0x00, A: 0xff}, // "#ffd700"
|
||||||
|
221: {R: 0xff, G: 0xd7, B: 0x5f, A: 0xff}, // "#ffd75f"
|
||||||
|
222: {R: 0xff, G: 0xd7, B: 0x87, A: 0xff}, // "#ffd787"
|
||||||
|
223: {R: 0xff, G: 0xd7, B: 0xaf, A: 0xff}, // "#ffd7af"
|
||||||
|
224: {R: 0xff, G: 0xd7, B: 0xd7, A: 0xff}, // "#ffd7d7"
|
||||||
|
225: {R: 0xff, G: 0xd7, B: 0xff, A: 0xff}, // "#ffd7ff"
|
||||||
|
226: {R: 0xff, G: 0xff, B: 0x00, A: 0xff}, // "#ffff00"
|
||||||
|
227: {R: 0xff, G: 0xff, B: 0x5f, A: 0xff}, // "#ffff5f"
|
||||||
|
228: {R: 0xff, G: 0xff, B: 0x87, A: 0xff}, // "#ffff87"
|
||||||
|
229: {R: 0xff, G: 0xff, B: 0xaf, A: 0xff}, // "#ffffaf"
|
||||||
|
230: {R: 0xff, G: 0xff, B: 0xd7, A: 0xff}, // "#ffffd7"
|
||||||
|
231: {R: 0xff, G: 0xff, B: 0xff, A: 0xff}, // "#ffffff"
|
||||||
|
232: {R: 0x08, G: 0x08, B: 0x08, A: 0xff}, // "#080808"
|
||||||
|
233: {R: 0x12, G: 0x12, B: 0x12, A: 0xff}, // "#121212"
|
||||||
|
234: {R: 0x1c, G: 0x1c, B: 0x1c, A: 0xff}, // "#1c1c1c"
|
||||||
|
235: {R: 0x26, G: 0x26, B: 0x26, A: 0xff}, // "#262626"
|
||||||
|
236: {R: 0x30, G: 0x30, B: 0x30, A: 0xff}, // "#303030"
|
||||||
|
237: {R: 0x3a, G: 0x3a, B: 0x3a, A: 0xff}, // "#3a3a3a"
|
||||||
|
238: {R: 0x44, G: 0x44, B: 0x44, A: 0xff}, // "#444444"
|
||||||
|
239: {R: 0x4e, G: 0x4e, B: 0x4e, A: 0xff}, // "#4e4e4e"
|
||||||
|
240: {R: 0x58, G: 0x58, B: 0x58, A: 0xff}, // "#585858"
|
||||||
|
241: {R: 0x62, G: 0x62, B: 0x62, A: 0xff}, // "#626262"
|
||||||
|
242: {R: 0x6c, G: 0x6c, B: 0x6c, A: 0xff}, // "#6c6c6c"
|
||||||
|
243: {R: 0x76, G: 0x76, B: 0x76, A: 0xff}, // "#767676"
|
||||||
|
244: {R: 0x80, G: 0x80, B: 0x80, A: 0xff}, // "#808080"
|
||||||
|
245: {R: 0x8a, G: 0x8a, B: 0x8a, A: 0xff}, // "#8a8a8a"
|
||||||
|
246: {R: 0x94, G: 0x94, B: 0x94, A: 0xff}, // "#949494"
|
||||||
|
247: {R: 0x9e, G: 0x9e, B: 0x9e, A: 0xff}, // "#9e9e9e"
|
||||||
|
248: {R: 0xa8, G: 0xa8, B: 0xa8, A: 0xff}, // "#a8a8a8"
|
||||||
|
249: {R: 0xb2, G: 0xb2, B: 0xb2, A: 0xff}, // "#b2b2b2"
|
||||||
|
250: {R: 0xbc, G: 0xbc, B: 0xbc, A: 0xff}, // "#bcbcbc"
|
||||||
|
251: {R: 0xc6, G: 0xc6, B: 0xc6, A: 0xff}, // "#c6c6c6"
|
||||||
|
252: {R: 0xd0, G: 0xd0, B: 0xd0, A: 0xff}, // "#d0d0d0"
|
||||||
|
253: {R: 0xda, G: 0xda, B: 0xda, A: 0xff}, // "#dadada"
|
||||||
|
254: {R: 0xe4, G: 0xe4, B: 0xe4, A: 0xff}, // "#e4e4e4"
|
||||||
|
255: {R: 0xee, G: 0xee, B: 0xee, A: 0xff}, // "#eeeeee"
|
||||||
|
}
|
||||||
|
|
||||||
|
var ansi256To16 = [...]BasicColor{
|
||||||
|
0: 0,
|
||||||
|
1: 1,
|
||||||
|
2: 2,
|
||||||
|
3: 3,
|
||||||
|
4: 4,
|
||||||
|
5: 5,
|
||||||
|
6: 6,
|
||||||
|
7: 7,
|
||||||
|
8: 8,
|
||||||
|
9: 9,
|
||||||
|
10: 10,
|
||||||
|
11: 11,
|
||||||
|
12: 12,
|
||||||
|
13: 13,
|
||||||
|
14: 14,
|
||||||
|
15: 15,
|
||||||
|
16: 0,
|
||||||
|
17: 4,
|
||||||
|
18: 4,
|
||||||
|
19: 4,
|
||||||
|
20: 12,
|
||||||
|
21: 12,
|
||||||
|
22: 2,
|
||||||
|
23: 6,
|
||||||
|
24: 4,
|
||||||
|
25: 4,
|
||||||
|
26: 12,
|
||||||
|
27: 12,
|
||||||
|
28: 2,
|
||||||
|
29: 2,
|
||||||
|
30: 6,
|
||||||
|
31: 4,
|
||||||
|
32: 12,
|
||||||
|
33: 12,
|
||||||
|
34: 2,
|
||||||
|
35: 2,
|
||||||
|
36: 2,
|
||||||
|
37: 6,
|
||||||
|
38: 12,
|
||||||
|
39: 12,
|
||||||
|
40: 10,
|
||||||
|
41: 10,
|
||||||
|
42: 10,
|
||||||
|
43: 10,
|
||||||
|
44: 14,
|
||||||
|
45: 12,
|
||||||
|
46: 10,
|
||||||
|
47: 10,
|
||||||
|
48: 10,
|
||||||
|
49: 10,
|
||||||
|
50: 10,
|
||||||
|
51: 14,
|
||||||
|
52: 1,
|
||||||
|
53: 5,
|
||||||
|
54: 4,
|
||||||
|
55: 4,
|
||||||
|
56: 12,
|
||||||
|
57: 12,
|
||||||
|
58: 3,
|
||||||
|
59: 8,
|
||||||
|
60: 4,
|
||||||
|
61: 4,
|
||||||
|
62: 12,
|
||||||
|
63: 12,
|
||||||
|
64: 2,
|
||||||
|
65: 2,
|
||||||
|
66: 6,
|
||||||
|
67: 4,
|
||||||
|
68: 12,
|
||||||
|
69: 12,
|
||||||
|
70: 2,
|
||||||
|
71: 2,
|
||||||
|
72: 2,
|
||||||
|
73: 6,
|
||||||
|
74: 12,
|
||||||
|
75: 12,
|
||||||
|
76: 10,
|
||||||
|
77: 10,
|
||||||
|
78: 10,
|
||||||
|
79: 10,
|
||||||
|
80: 14,
|
||||||
|
81: 12,
|
||||||
|
82: 10,
|
||||||
|
83: 10,
|
||||||
|
84: 10,
|
||||||
|
85: 10,
|
||||||
|
86: 10,
|
||||||
|
87: 14,
|
||||||
|
88: 1,
|
||||||
|
89: 1,
|
||||||
|
90: 5,
|
||||||
|
91: 4,
|
||||||
|
92: 12,
|
||||||
|
93: 12,
|
||||||
|
94: 1,
|
||||||
|
95: 1,
|
||||||
|
96: 5,
|
||||||
|
97: 4,
|
||||||
|
98: 12,
|
||||||
|
99: 12,
|
||||||
|
100: 3,
|
||||||
|
101: 3,
|
||||||
|
102: 8,
|
||||||
|
103: 4,
|
||||||
|
104: 12,
|
||||||
|
105: 12,
|
||||||
|
106: 2,
|
||||||
|
107: 2,
|
||||||
|
108: 2,
|
||||||
|
109: 6,
|
||||||
|
110: 12,
|
||||||
|
111: 12,
|
||||||
|
112: 10,
|
||||||
|
113: 10,
|
||||||
|
114: 10,
|
||||||
|
115: 10,
|
||||||
|
116: 14,
|
||||||
|
117: 12,
|
||||||
|
118: 10,
|
||||||
|
119: 10,
|
||||||
|
120: 10,
|
||||||
|
121: 10,
|
||||||
|
122: 10,
|
||||||
|
123: 14,
|
||||||
|
124: 1,
|
||||||
|
125: 1,
|
||||||
|
126: 1,
|
||||||
|
127: 5,
|
||||||
|
128: 12,
|
||||||
|
129: 12,
|
||||||
|
130: 1,
|
||||||
|
131: 1,
|
||||||
|
132: 1,
|
||||||
|
133: 5,
|
||||||
|
134: 12,
|
||||||
|
135: 12,
|
||||||
|
136: 1,
|
||||||
|
137: 1,
|
||||||
|
138: 1,
|
||||||
|
139: 5,
|
||||||
|
140: 12,
|
||||||
|
141: 12,
|
||||||
|
142: 3,
|
||||||
|
143: 3,
|
||||||
|
144: 3,
|
||||||
|
145: 7,
|
||||||
|
146: 12,
|
||||||
|
147: 12,
|
||||||
|
148: 10,
|
||||||
|
149: 10,
|
||||||
|
150: 10,
|
||||||
|
151: 10,
|
||||||
|
152: 14,
|
||||||
|
153: 12,
|
||||||
|
154: 10,
|
||||||
|
155: 10,
|
||||||
|
156: 10,
|
||||||
|
157: 10,
|
||||||
|
158: 10,
|
||||||
|
159: 14,
|
||||||
|
160: 9,
|
||||||
|
161: 9,
|
||||||
|
162: 9,
|
||||||
|
163: 9,
|
||||||
|
164: 13,
|
||||||
|
165: 12,
|
||||||
|
166: 9,
|
||||||
|
167: 9,
|
||||||
|
168: 9,
|
||||||
|
169: 9,
|
||||||
|
170: 13,
|
||||||
|
171: 12,
|
||||||
|
172: 9,
|
||||||
|
173: 9,
|
||||||
|
174: 9,
|
||||||
|
175: 9,
|
||||||
|
176: 13,
|
||||||
|
177: 12,
|
||||||
|
178: 9,
|
||||||
|
179: 9,
|
||||||
|
180: 9,
|
||||||
|
181: 9,
|
||||||
|
182: 13,
|
||||||
|
183: 12,
|
||||||
|
184: 11,
|
||||||
|
185: 11,
|
||||||
|
186: 11,
|
||||||
|
187: 11,
|
||||||
|
188: 7,
|
||||||
|
189: 12,
|
||||||
|
190: 10,
|
||||||
|
191: 10,
|
||||||
|
192: 10,
|
||||||
|
193: 10,
|
||||||
|
194: 10,
|
||||||
|
195: 14,
|
||||||
|
196: 9,
|
||||||
|
197: 9,
|
||||||
|
198: 9,
|
||||||
|
199: 9,
|
||||||
|
200: 9,
|
||||||
|
201: 13,
|
||||||
|
202: 9,
|
||||||
|
203: 9,
|
||||||
|
204: 9,
|
||||||
|
205: 9,
|
||||||
|
206: 9,
|
||||||
|
207: 13,
|
||||||
|
208: 9,
|
||||||
|
209: 9,
|
||||||
|
210: 9,
|
||||||
|
211: 9,
|
||||||
|
212: 9,
|
||||||
|
213: 13,
|
||||||
|
214: 9,
|
||||||
|
215: 9,
|
||||||
|
216: 9,
|
||||||
|
217: 9,
|
||||||
|
218: 9,
|
||||||
|
219: 13,
|
||||||
|
220: 9,
|
||||||
|
221: 9,
|
||||||
|
222: 9,
|
||||||
|
223: 9,
|
||||||
|
224: 9,
|
||||||
|
225: 13,
|
||||||
|
226: 11,
|
||||||
|
227: 11,
|
||||||
|
228: 11,
|
||||||
|
229: 11,
|
||||||
|
230: 11,
|
||||||
|
231: 15,
|
||||||
|
232: 0,
|
||||||
|
233: 0,
|
||||||
|
234: 0,
|
||||||
|
235: 0,
|
||||||
|
236: 0,
|
||||||
|
237: 0,
|
||||||
|
238: 8,
|
||||||
|
239: 8,
|
||||||
|
240: 8,
|
||||||
|
241: 8,
|
||||||
|
242: 8,
|
||||||
|
243: 8,
|
||||||
|
244: 7,
|
||||||
|
245: 7,
|
||||||
|
246: 7,
|
||||||
|
247: 7,
|
||||||
|
248: 7,
|
||||||
|
249: 7,
|
||||||
|
250: 15,
|
||||||
|
251: 15,
|
||||||
|
252: 15,
|
||||||
|
253: 15,
|
||||||
|
254: 15,
|
||||||
|
255: 15,
|
||||||
|
}
|
||||||
156
vendor/github.com/charmbracelet/x/ansi/ctrl.go
generated
vendored
Normal file
156
vendor/github.com/charmbracelet/x/ansi/ctrl.go
generated
vendored
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestNameVersion (XTVERSION) is a control sequence that requests the
|
||||||
|
// terminal's name and version. It responds with a DSR sequence identifying the
|
||||||
|
// terminal.
|
||||||
|
//
|
||||||
|
// CSI > 0 q
|
||||||
|
// DCS > | text ST
|
||||||
|
//
|
||||||
|
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys
|
||||||
|
const (
|
||||||
|
RequestNameVersion = "\x1b[>q"
|
||||||
|
XTVERSION = RequestNameVersion
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestXTVersion is a control sequence that requests the terminal's XTVERSION. It responds with a DSR sequence identifying the version.
|
||||||
|
//
|
||||||
|
// CSI > Ps q
|
||||||
|
// DCS > | text ST
|
||||||
|
//
|
||||||
|
// See https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-PC-Style-Function-Keys
|
||||||
|
//
|
||||||
|
// Deprecated: use [RequestNameVersion] instead.
|
||||||
|
const RequestXTVersion = RequestNameVersion
|
||||||
|
|
||||||
|
// PrimaryDeviceAttributes (DA1) is a control sequence that reports the
|
||||||
|
// terminal's primary device attributes.
|
||||||
|
//
|
||||||
|
// CSI c
|
||||||
|
// CSI 0 c
|
||||||
|
// CSI ? Ps ; ... c
|
||||||
|
//
|
||||||
|
// If no attributes are given, or if the attribute is 0, this function returns
|
||||||
|
// the request sequence. Otherwise, it returns the response sequence.
|
||||||
|
//
|
||||||
|
// Common attributes include:
|
||||||
|
// - 1 132 columns
|
||||||
|
// - 2 Printer port
|
||||||
|
// - 4 Sixel
|
||||||
|
// - 6 Selective erase
|
||||||
|
// - 7 Soft character set (DRCS)
|
||||||
|
// - 8 User-defined keys (UDKs)
|
||||||
|
// - 9 National replacement character sets (NRCS) (International terminal only)
|
||||||
|
// - 12 Yugoslavian (SCS)
|
||||||
|
// - 15 Technical character set
|
||||||
|
// - 18 Windowing capability
|
||||||
|
// - 21 Horizontal scrolling
|
||||||
|
// - 23 Greek
|
||||||
|
// - 24 Turkish
|
||||||
|
// - 42 ISO Latin-2 character set
|
||||||
|
// - 44 PCTerm
|
||||||
|
// - 45 Soft key map
|
||||||
|
// - 46 ASCII emulation
|
||||||
|
//
|
||||||
|
// See https://vt100.net/docs/vt510-rm/DA1.html
|
||||||
|
func PrimaryDeviceAttributes(attrs ...int) string {
|
||||||
|
if len(attrs) == 0 {
|
||||||
|
return RequestPrimaryDeviceAttributes
|
||||||
|
} else if len(attrs) == 1 && attrs[0] == 0 {
|
||||||
|
return "\x1b[0c"
|
||||||
|
}
|
||||||
|
|
||||||
|
as := make([]string, len(attrs))
|
||||||
|
for i, a := range attrs {
|
||||||
|
as[i] = strconv.Itoa(a)
|
||||||
|
}
|
||||||
|
return "\x1b[?" + strings.Join(as, ";") + "c"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DA1 is an alias for [PrimaryDeviceAttributes].
|
||||||
|
func DA1(attrs ...int) string {
|
||||||
|
return PrimaryDeviceAttributes(attrs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestPrimaryDeviceAttributes is a control sequence that requests the
|
||||||
|
// terminal's primary device attributes (DA1).
|
||||||
|
//
|
||||||
|
// CSI c
|
||||||
|
//
|
||||||
|
// See https://vt100.net/docs/vt510-rm/DA1.html
|
||||||
|
const RequestPrimaryDeviceAttributes = "\x1b[c"
|
||||||
|
|
||||||
|
// SecondaryDeviceAttributes (DA2) is a control sequence that reports the
|
||||||
|
// terminal's secondary device attributes.
|
||||||
|
//
|
||||||
|
// CSI > c
|
||||||
|
// CSI > 0 c
|
||||||
|
// CSI > Ps ; ... c
|
||||||
|
//
|
||||||
|
// See https://vt100.net/docs/vt510-rm/DA2.html
|
||||||
|
func SecondaryDeviceAttributes(attrs ...int) string {
|
||||||
|
if len(attrs) == 0 {
|
||||||
|
return RequestSecondaryDeviceAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
as := make([]string, len(attrs))
|
||||||
|
for i, a := range attrs {
|
||||||
|
as[i] = strconv.Itoa(a)
|
||||||
|
}
|
||||||
|
return "\x1b[>" + strings.Join(as, ";") + "c"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DA2 is an alias for [SecondaryDeviceAttributes].
|
||||||
|
func DA2(attrs ...int) string {
|
||||||
|
return SecondaryDeviceAttributes(attrs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestSecondaryDeviceAttributes is a control sequence that requests the
|
||||||
|
// terminal's secondary device attributes (DA2).
|
||||||
|
//
|
||||||
|
// CSI > c
|
||||||
|
//
|
||||||
|
// See https://vt100.net/docs/vt510-rm/DA2.html
|
||||||
|
const RequestSecondaryDeviceAttributes = "\x1b[>c"
|
||||||
|
|
||||||
|
// TertiaryDeviceAttributes (DA3) is a control sequence that reports the
|
||||||
|
// terminal's tertiary device attributes.
|
||||||
|
//
|
||||||
|
// CSI = c
|
||||||
|
// CSI = 0 c
|
||||||
|
// DCS ! | Text ST
|
||||||
|
//
|
||||||
|
// Where Text is the unit ID for the terminal.
|
||||||
|
//
|
||||||
|
// If no unit ID is given, or if the unit ID is 0, this function returns the
|
||||||
|
// request sequence. Otherwise, it returns the response sequence.
|
||||||
|
//
|
||||||
|
// See https://vt100.net/docs/vt510-rm/DA3.html
|
||||||
|
func TertiaryDeviceAttributes(unitID string) string {
|
||||||
|
switch unitID {
|
||||||
|
case "":
|
||||||
|
return RequestTertiaryDeviceAttributes
|
||||||
|
case "0":
|
||||||
|
return "\x1b[=0c"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\x1bP!|" + unitID + "\x1b\\"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DA3 is an alias for [TertiaryDeviceAttributes].
|
||||||
|
func DA3(unitID string) string {
|
||||||
|
return TertiaryDeviceAttributes(unitID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestTertiaryDeviceAttributes is a control sequence that requests the
|
||||||
|
// terminal's tertiary device attributes (DA3).
|
||||||
|
//
|
||||||
|
// CSI = c
|
||||||
|
//
|
||||||
|
// See https://vt100.net/docs/vt510-rm/DA3.html
|
||||||
|
const RequestTertiaryDeviceAttributes = "\x1b[=c"
|
||||||
635
vendor/github.com/charmbracelet/x/ansi/cursor.go
generated
vendored
Normal file
635
vendor/github.com/charmbracelet/x/ansi/cursor.go
generated
vendored
Normal file
|
|
@ -0,0 +1,635 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveCursor (DECSC) is an escape sequence that saves the current cursor
|
||||||
|
// position.
|
||||||
|
//
|
||||||
|
// ESC 7
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/DECSC.html
|
||||||
|
const (
|
||||||
|
SaveCursor = "\x1b7"
|
||||||
|
DECSC = SaveCursor
|
||||||
|
)
|
||||||
|
|
||||||
|
// RestoreCursor (DECRC) is an escape sequence that restores the cursor
|
||||||
|
// position.
|
||||||
|
//
|
||||||
|
// ESC 8
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/DECRC.html
|
||||||
|
const (
|
||||||
|
RestoreCursor = "\x1b8"
|
||||||
|
DECRC = RestoreCursor
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequestCursorPosition is an escape sequence that requests the current cursor
|
||||||
|
// position.
|
||||||
|
//
|
||||||
|
// CSI 6 n
|
||||||
|
//
|
||||||
|
// The terminal will report the cursor position as a CSI sequence in the
|
||||||
|
// following format:
|
||||||
|
//
|
||||||
|
// CSI Pl ; Pc R
|
||||||
|
//
|
||||||
|
// Where Pl is the line number and Pc is the column number.
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CPR.html
|
||||||
|
//
|
||||||
|
// Deprecated: use [RequestCursorPositionReport] instead.
|
||||||
|
const RequestCursorPosition = "\x1b[6n"
|
||||||
|
|
||||||
|
// RequestExtendedCursorPosition (DECXCPR) is a sequence for requesting the
|
||||||
|
// cursor position report including the current page number.
|
||||||
|
//
|
||||||
|
// CSI ? 6 n
|
||||||
|
//
|
||||||
|
// The terminal will report the cursor position as a CSI sequence in the
|
||||||
|
// following format:
|
||||||
|
//
|
||||||
|
// CSI ? Pl ; Pc ; Pp R
|
||||||
|
//
|
||||||
|
// Where Pl is the line number, Pc is the column number, and Pp is the page
|
||||||
|
// number.
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/DECXCPR.html
|
||||||
|
//
|
||||||
|
// Deprecated: use [RequestExtendedCursorPositionReport] instead.
|
||||||
|
const RequestExtendedCursorPosition = "\x1b[?6n"
|
||||||
|
|
||||||
|
// CursorUp (CUU) returns a sequence for moving the cursor up n cells.
|
||||||
|
//
|
||||||
|
// CSI n A
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CUU.html
|
||||||
|
func CursorUp(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "A"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CUU is an alias for [CursorUp].
|
||||||
|
func CUU(n int) string {
|
||||||
|
return CursorUp(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CUU1 is a sequence for moving the cursor up one cell.
|
||||||
|
const CUU1 = "\x1b[A"
|
||||||
|
|
||||||
|
// CursorUp1 is a sequence for moving the cursor up one cell.
|
||||||
|
//
|
||||||
|
// This is equivalent to CursorUp(1).
|
||||||
|
//
|
||||||
|
// Deprecated: use [CUU1] instead.
|
||||||
|
const CursorUp1 = "\x1b[A"
|
||||||
|
|
||||||
|
// CursorDown (CUD) returns a sequence for moving the cursor down n cells.
|
||||||
|
//
|
||||||
|
// CSI n B
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CUD.html
|
||||||
|
func CursorDown(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "B"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CUD is an alias for [CursorDown].
|
||||||
|
func CUD(n int) string {
|
||||||
|
return CursorDown(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CUD1 is a sequence for moving the cursor down one cell.
|
||||||
|
const CUD1 = "\x1b[B"
|
||||||
|
|
||||||
|
// CursorDown1 is a sequence for moving the cursor down one cell.
|
||||||
|
//
|
||||||
|
// This is equivalent to CursorDown(1).
|
||||||
|
//
|
||||||
|
// Deprecated: use [CUD1] instead.
|
||||||
|
const CursorDown1 = "\x1b[B"
|
||||||
|
|
||||||
|
// CursorForward (CUF) returns a sequence for moving the cursor right n cells.
|
||||||
|
//
|
||||||
|
// # CSI n C
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CUF.html
|
||||||
|
func CursorForward(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "C"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CUF is an alias for [CursorForward].
|
||||||
|
func CUF(n int) string {
|
||||||
|
return CursorForward(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CUF1 is a sequence for moving the cursor right one cell.
|
||||||
|
const CUF1 = "\x1b[C"
|
||||||
|
|
||||||
|
// CursorRight (CUF) returns a sequence for moving the cursor right n cells.
|
||||||
|
//
|
||||||
|
// CSI n C
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CUF.html
|
||||||
|
//
|
||||||
|
// Deprecated: use [CursorForward] instead.
|
||||||
|
func CursorRight(n int) string {
|
||||||
|
return CursorForward(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorRight1 is a sequence for moving the cursor right one cell.
|
||||||
|
//
|
||||||
|
// This is equivalent to CursorRight(1).
|
||||||
|
//
|
||||||
|
// Deprecated: use [CUF1] instead.
|
||||||
|
const CursorRight1 = CUF1
|
||||||
|
|
||||||
|
// CursorBackward (CUB) returns a sequence for moving the cursor left n cells.
|
||||||
|
//
|
||||||
|
// # CSI n D
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CUB.html
|
||||||
|
func CursorBackward(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "D"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CUB is an alias for [CursorBackward].
|
||||||
|
func CUB(n int) string {
|
||||||
|
return CursorBackward(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CUB1 is a sequence for moving the cursor left one cell.
|
||||||
|
const CUB1 = "\x1b[D"
|
||||||
|
|
||||||
|
// CursorLeft (CUB) returns a sequence for moving the cursor left n cells.
|
||||||
|
//
|
||||||
|
// CSI n D
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CUB.html
|
||||||
|
//
|
||||||
|
// Deprecated: use [CursorBackward] instead.
|
||||||
|
func CursorLeft(n int) string {
|
||||||
|
return CursorBackward(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorLeft1 is a sequence for moving the cursor left one cell.
|
||||||
|
//
|
||||||
|
// This is equivalent to CursorLeft(1).
|
||||||
|
//
|
||||||
|
// Deprecated: use [CUB1] instead.
|
||||||
|
const CursorLeft1 = CUB1
|
||||||
|
|
||||||
|
// CursorNextLine (CNL) returns a sequence for moving the cursor to the
|
||||||
|
// beginning of the next line n times.
|
||||||
|
//
|
||||||
|
// CSI n E
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CNL.html
|
||||||
|
func CursorNextLine(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "E"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CNL is an alias for [CursorNextLine].
|
||||||
|
func CNL(n int) string {
|
||||||
|
return CursorNextLine(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorPreviousLine (CPL) returns a sequence for moving the cursor to the
|
||||||
|
// beginning of the previous line n times.
|
||||||
|
//
|
||||||
|
// CSI n F
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CPL.html
|
||||||
|
func CursorPreviousLine(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "F"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CPL is an alias for [CursorPreviousLine].
|
||||||
|
func CPL(n int) string {
|
||||||
|
return CursorPreviousLine(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorHorizontalAbsolute (CHA) returns a sequence for moving the cursor to
|
||||||
|
// the given column.
|
||||||
|
//
|
||||||
|
// Default is 1.
|
||||||
|
//
|
||||||
|
// CSI n G
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CHA.html
|
||||||
|
func CursorHorizontalAbsolute(col int) string {
|
||||||
|
var s string
|
||||||
|
if col > 0 {
|
||||||
|
s = strconv.Itoa(col)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "G"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHA is an alias for [CursorHorizontalAbsolute].
|
||||||
|
func CHA(col int) string {
|
||||||
|
return CursorHorizontalAbsolute(col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorPosition (CUP) returns a sequence for setting the cursor to the
|
||||||
|
// given row and column.
|
||||||
|
//
|
||||||
|
// Default is 1,1.
|
||||||
|
//
|
||||||
|
// CSI n ; m H
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CUP.html
|
||||||
|
func CursorPosition(col, row int) string {
|
||||||
|
if row <= 0 && col <= 0 {
|
||||||
|
return CursorHomePosition
|
||||||
|
}
|
||||||
|
|
||||||
|
var r, c string
|
||||||
|
if row > 0 {
|
||||||
|
r = strconv.Itoa(row)
|
||||||
|
}
|
||||||
|
if col > 0 {
|
||||||
|
c = strconv.Itoa(col)
|
||||||
|
}
|
||||||
|
return "\x1b[" + r + ";" + c + "H"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CUP is an alias for [CursorPosition].
|
||||||
|
func CUP(col, row int) string {
|
||||||
|
return CursorPosition(col, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorHomePosition is a sequence for moving the cursor to the upper left
|
||||||
|
// corner of the scrolling region. This is equivalent to `CursorPosition(1, 1)`.
|
||||||
|
const CursorHomePosition = "\x1b[H"
|
||||||
|
|
||||||
|
// SetCursorPosition (CUP) returns a sequence for setting the cursor to the
|
||||||
|
// given row and column.
|
||||||
|
//
|
||||||
|
// CSI n ; m H
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CUP.html
|
||||||
|
//
|
||||||
|
// Deprecated: use [CursorPosition] instead.
|
||||||
|
func SetCursorPosition(col, row int) string {
|
||||||
|
if row <= 0 && col <= 0 {
|
||||||
|
return HomeCursorPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
var r, c string
|
||||||
|
if row > 0 {
|
||||||
|
r = strconv.Itoa(row)
|
||||||
|
}
|
||||||
|
if col > 0 {
|
||||||
|
c = strconv.Itoa(col)
|
||||||
|
}
|
||||||
|
return "\x1b[" + r + ";" + c + "H"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HomeCursorPosition is a sequence for moving the cursor to the upper left
|
||||||
|
// corner of the scrolling region. This is equivalent to `SetCursorPosition(1, 1)`.
|
||||||
|
//
|
||||||
|
// Deprecated: use [CursorHomePosition] instead.
|
||||||
|
const HomeCursorPosition = CursorHomePosition
|
||||||
|
|
||||||
|
// MoveCursor (CUP) returns a sequence for setting the cursor to the
|
||||||
|
// given row and column.
|
||||||
|
//
|
||||||
|
// CSI n ; m H
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CUP.html
|
||||||
|
//
|
||||||
|
// Deprecated: use [CursorPosition] instead.
|
||||||
|
func MoveCursor(col, row int) string {
|
||||||
|
return SetCursorPosition(col, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorOrigin is a sequence for moving the cursor to the upper left corner of
|
||||||
|
// the display. This is equivalent to `SetCursorPosition(1, 1)`.
|
||||||
|
//
|
||||||
|
// Deprecated: use [CursorHomePosition] instead.
|
||||||
|
const CursorOrigin = "\x1b[1;1H"
|
||||||
|
|
||||||
|
// MoveCursorOrigin is a sequence for moving the cursor to the upper left
|
||||||
|
// corner of the display. This is equivalent to `SetCursorPosition(1, 1)`.
|
||||||
|
//
|
||||||
|
// Deprecated: use [CursorHomePosition] instead.
|
||||||
|
const MoveCursorOrigin = CursorOrigin
|
||||||
|
|
||||||
|
// CursorHorizontalForwardTab (CHT) returns a sequence for moving the cursor to
|
||||||
|
// the next tab stop n times.
|
||||||
|
//
|
||||||
|
// Default is 1.
|
||||||
|
//
|
||||||
|
// CSI n I
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CHT.html
|
||||||
|
func CursorHorizontalForwardTab(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "I"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHT is an alias for [CursorHorizontalForwardTab].
|
||||||
|
func CHT(n int) string {
|
||||||
|
return CursorHorizontalForwardTab(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EraseCharacter (ECH) returns a sequence for erasing n characters from the
|
||||||
|
// screen. This doesn't affect other cell attributes.
|
||||||
|
//
|
||||||
|
// Default is 1.
|
||||||
|
//
|
||||||
|
// CSI n X
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/ECH.html
|
||||||
|
func EraseCharacter(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "X"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ECH is an alias for [EraseCharacter].
|
||||||
|
func ECH(n int) string {
|
||||||
|
return EraseCharacter(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CursorBackwardTab (CBT) returns a sequence for moving the cursor to the
|
||||||
|
// previous tab stop n times.
|
||||||
|
//
|
||||||
|
// Default is 1.
|
||||||
|
//
|
||||||
|
// CSI n Z
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/CBT.html
|
||||||
|
func CursorBackwardTab(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "Z"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CBT is an alias for [CursorBackwardTab].
|
||||||
|
func CBT(n int) string {
|
||||||
|
return CursorBackwardTab(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerticalPositionAbsolute (VPA) returns a sequence for moving the cursor to
|
||||||
|
// the given row.
|
||||||
|
//
|
||||||
|
// Default is 1.
|
||||||
|
//
|
||||||
|
// CSI n d
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/VPA.html
|
||||||
|
func VerticalPositionAbsolute(row int) string {
|
||||||
|
var s string
|
||||||
|
if row > 0 {
|
||||||
|
s = strconv.Itoa(row)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "d"
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPA is an alias for [VerticalPositionAbsolute].
|
||||||
|
func VPA(row int) string {
|
||||||
|
return VerticalPositionAbsolute(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerticalPositionRelative (VPR) returns a sequence for moving the cursor down
|
||||||
|
// n rows relative to the current position.
|
||||||
|
//
|
||||||
|
// Default is 1.
|
||||||
|
//
|
||||||
|
// CSI n e
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/VPR.html
|
||||||
|
func VerticalPositionRelative(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 1 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "e"
|
||||||
|
}
|
||||||
|
|
||||||
|
// VPR is an alias for [VerticalPositionRelative].
|
||||||
|
func VPR(n int) string {
|
||||||
|
return VerticalPositionRelative(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HorizontalVerticalPosition (HVP) returns a sequence for moving the cursor to
|
||||||
|
// the given row and column.
|
||||||
|
//
|
||||||
|
// Default is 1,1.
|
||||||
|
//
|
||||||
|
// CSI n ; m f
|
||||||
|
//
|
||||||
|
// This has the same effect as [CursorPosition].
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/HVP.html
|
||||||
|
func HorizontalVerticalPosition(col, row int) string {
|
||||||
|
var r, c string
|
||||||
|
if row > 0 {
|
||||||
|
r = strconv.Itoa(row)
|
||||||
|
}
|
||||||
|
if col > 0 {
|
||||||
|
c = strconv.Itoa(col)
|
||||||
|
}
|
||||||
|
return "\x1b[" + r + ";" + c + "f"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HVP is an alias for [HorizontalVerticalPosition].
|
||||||
|
func HVP(col, row int) string {
|
||||||
|
return HorizontalVerticalPosition(col, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HorizontalVerticalHomePosition is a sequence for moving the cursor to the
|
||||||
|
// upper left corner of the scrolling region. This is equivalent to
|
||||||
|
// `HorizontalVerticalPosition(1, 1)`.
|
||||||
|
const HorizontalVerticalHomePosition = "\x1b[f"
|
||||||
|
|
||||||
|
// SaveCurrentCursorPosition (SCOSC) is a sequence for saving the current cursor
|
||||||
|
// position for SCO console mode.
|
||||||
|
//
|
||||||
|
// CSI s
|
||||||
|
//
|
||||||
|
// This acts like [DECSC], except the page number where the cursor is located
|
||||||
|
// is not saved.
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/SCOSC.html
|
||||||
|
const (
|
||||||
|
SaveCurrentCursorPosition = "\x1b[s"
|
||||||
|
SCOSC = SaveCurrentCursorPosition
|
||||||
|
)
|
||||||
|
|
||||||
|
// SaveCursorPosition (SCP or SCOSC) is a sequence for saving the cursor
|
||||||
|
// position.
|
||||||
|
//
|
||||||
|
// CSI s
|
||||||
|
//
|
||||||
|
// This acts like Save, except the page number where the cursor is located is
|
||||||
|
// not saved.
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/SCOSC.html
|
||||||
|
//
|
||||||
|
// Deprecated: use [SaveCurrentCursorPosition] instead.
|
||||||
|
const SaveCursorPosition = "\x1b[s"
|
||||||
|
|
||||||
|
// RestoreCurrentCursorPosition (SCORC) is a sequence for restoring the current
|
||||||
|
// cursor position for SCO console mode.
|
||||||
|
//
|
||||||
|
// CSI u
|
||||||
|
//
|
||||||
|
// This acts like [DECRC], except the page number where the cursor was saved is
|
||||||
|
// not restored.
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/SCORC.html
|
||||||
|
const (
|
||||||
|
RestoreCurrentCursorPosition = "\x1b[u"
|
||||||
|
SCORC = RestoreCurrentCursorPosition
|
||||||
|
)
|
||||||
|
|
||||||
|
// RestoreCursorPosition (RCP or SCORC) is a sequence for restoring the cursor
|
||||||
|
// position.
|
||||||
|
//
|
||||||
|
// CSI u
|
||||||
|
//
|
||||||
|
// This acts like Restore, except the cursor stays on the same page where the
|
||||||
|
// cursor was saved.
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/SCORC.html
|
||||||
|
//
|
||||||
|
// Deprecated: use [RestoreCurrentCursorPosition] instead.
|
||||||
|
const RestoreCursorPosition = "\x1b[u"
|
||||||
|
|
||||||
|
// SetCursorStyle (DECSCUSR) returns a sequence for changing the cursor style.
|
||||||
|
//
|
||||||
|
// Default is 1.
|
||||||
|
//
|
||||||
|
// CSI Ps SP q
|
||||||
|
//
|
||||||
|
// Where Ps is the cursor style:
|
||||||
|
//
|
||||||
|
// 0: Blinking block
|
||||||
|
// 1: Blinking block (default)
|
||||||
|
// 2: Steady block
|
||||||
|
// 3: Blinking underline
|
||||||
|
// 4: Steady underline
|
||||||
|
// 5: Blinking bar (xterm)
|
||||||
|
// 6: Steady bar (xterm)
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/DECSCUSR.html
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h4-Functions-using-CSI-_-ordered-by-the-final-character-lparen-s-rparen:CSI-Ps-SP-q.1D81
|
||||||
|
func SetCursorStyle(style int) string {
|
||||||
|
if style < 0 {
|
||||||
|
style = 0
|
||||||
|
}
|
||||||
|
return "\x1b[" + strconv.Itoa(style) + " q"
|
||||||
|
}
|
||||||
|
|
||||||
|
// DECSCUSR is an alias for [SetCursorStyle].
|
||||||
|
func DECSCUSR(style int) string {
|
||||||
|
return SetCursorStyle(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPointerShape returns a sequence for changing the mouse pointer cursor
|
||||||
|
// shape. Use "default" for the default pointer shape.
|
||||||
|
//
|
||||||
|
// OSC 22 ; Pt ST
|
||||||
|
// OSC 22 ; Pt BEL
|
||||||
|
//
|
||||||
|
// Where Pt is the pointer shape name. The name can be anything that the
|
||||||
|
// operating system can understand. Some common names are:
|
||||||
|
//
|
||||||
|
// - copy
|
||||||
|
// - crosshair
|
||||||
|
// - default
|
||||||
|
// - ew-resize
|
||||||
|
// - n-resize
|
||||||
|
// - text
|
||||||
|
// - wait
|
||||||
|
//
|
||||||
|
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Operating-System-Commands
|
||||||
|
func SetPointerShape(shape string) string {
|
||||||
|
return "\x1b]22;" + shape + "\x07"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReverseIndex (RI) is an escape sequence for moving the cursor up one line in
|
||||||
|
// the same column. If the cursor is at the top margin, the screen scrolls
|
||||||
|
// down.
|
||||||
|
//
|
||||||
|
// This has the same effect as [RI].
|
||||||
|
const ReverseIndex = "\x1bM"
|
||||||
|
|
||||||
|
// HorizontalPositionAbsolute (HPA) returns a sequence for moving the cursor to
|
||||||
|
// the given column. This has the same effect as [CUP].
|
||||||
|
//
|
||||||
|
// Default is 1.
|
||||||
|
//
|
||||||
|
// CSI n \`
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/HPA.html
|
||||||
|
func HorizontalPositionAbsolute(col int) string {
|
||||||
|
var s string
|
||||||
|
if col > 0 {
|
||||||
|
s = strconv.Itoa(col)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "`"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HPA is an alias for [HorizontalPositionAbsolute].
|
||||||
|
func HPA(col int) string {
|
||||||
|
return HorizontalPositionAbsolute(col)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HorizontalPositionRelative (HPR) returns a sequence for moving the cursor
|
||||||
|
// right n columns relative to the current position. This has the same effect
|
||||||
|
// as [CUP].
|
||||||
|
//
|
||||||
|
// Default is 1.
|
||||||
|
//
|
||||||
|
// CSI n a
|
||||||
|
//
|
||||||
|
// See: https://vt100.net/docs/vt510-rm/HPR.html
|
||||||
|
func HorizontalPositionRelative(n int) string {
|
||||||
|
var s string
|
||||||
|
if n > 0 {
|
||||||
|
s = strconv.Itoa(n)
|
||||||
|
}
|
||||||
|
return "\x1b[" + s + "a"
|
||||||
|
}
|
||||||
|
|
||||||
|
// HPR is an alias for [HorizontalPositionRelative].
|
||||||
|
func HPR(n int) string {
|
||||||
|
return HorizontalPositionRelative(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index (IND) is an escape sequence for moving the cursor down one line in the
|
||||||
|
// same column. If the cursor is at the bottom margin, the screen scrolls up.
|
||||||
|
// This has the same effect as [IND].
|
||||||
|
const Index = "\x1bD"
|
||||||
26
vendor/github.com/charmbracelet/x/ansi/cwd.go
generated
vendored
Normal file
26
vendor/github.com/charmbracelet/x/ansi/cwd.go
generated
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package ansi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotifyWorkingDirectory returns a sequence that notifies the terminal
|
||||||
|
// of the current working directory.
|
||||||
|
//
|
||||||
|
// OSC 7 ; Pt BEL
|
||||||
|
//
|
||||||
|
// Where Pt is a URL in the format "file://[host]/[path]".
|
||||||
|
// Set host to "localhost" if this is a path on the local computer.
|
||||||
|
//
|
||||||
|
// See: https://wezfurlong.org/wezterm/shell-integration.html#osc-7-escape-sequence-to-set-the-working-directory
|
||||||
|
// See: https://iterm2.com/documentation-escape-codes.html#:~:text=RemoteHost%20and%20CurrentDir%3A-,OSC%207,-%3B%20%5BPs%5D%20ST
|
||||||
|
func NotifyWorkingDirectory(host string, paths ...string) string {
|
||||||
|
path := path.Join(paths...)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "file",
|
||||||
|
Host: host,
|
||||||
|
Path: path,
|
||||||
|
}
|
||||||
|
return "\x1b]7;" + u.String() + "\x07"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue