Compare commits
524 Commits
v0.3.0-bet
...
v0.20.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b700afdb9 | ||
|
|
c70a84b3fe | ||
|
|
8ed244eeb9 | ||
|
|
440f62ff54 | ||
|
|
6f659cfb07 | ||
|
|
bf4944683d | ||
|
|
7539f775fe | ||
|
|
8db9fb4aeb | ||
|
|
d05ab6fb70 | ||
|
|
2920e045ba | ||
|
|
add578a7d6 | ||
|
|
60a4131384 | ||
|
|
964190a859 | ||
|
|
b9290b35d1 | ||
|
|
daf5890152 | ||
|
|
7e37a96678 | ||
|
|
bcb7417785 | ||
|
|
9c956733f7 | ||
|
|
13fb11a62c | ||
|
|
0fb1ed85c6 | ||
|
|
e2cb11cc30 | ||
|
|
c3f87f245a | ||
|
|
df90982632 | ||
|
|
bb061fdab6 | ||
|
|
1ff85535c8 | ||
|
|
33f3212cbf | ||
|
|
fb6d4b2f51 | ||
|
|
446efae185 | ||
|
|
b347201b9f | ||
|
|
9f1f59a51c | ||
|
|
6f6c355c5c | ||
|
|
60150f6236 | ||
|
|
2889c7d084 | ||
|
|
57678a5fe8 | ||
|
|
ae8ed8867d | ||
|
|
e66d5cdee0 | ||
|
|
804115ac6f | ||
|
|
a1813af297 | ||
|
|
085fde7d4a | ||
|
|
860a40c13a | ||
|
|
0833c9018b | ||
|
|
f7c4b44962 | ||
|
|
56e44a0efa | ||
|
|
ad288f5168 | ||
|
|
c5d387cb53 | ||
|
|
2f4413be6e | ||
|
|
83d3ec73e7 | ||
|
|
cf8eda04a1 | ||
|
|
6bdb97c55c | ||
|
|
7a6c3d9db1 | ||
|
|
bfcc5504bb | ||
|
|
669a4d5652 | ||
|
|
b808305507 | ||
|
|
a04b190251 | ||
|
|
e869869462 | ||
|
|
20c0051026 | ||
|
|
284b0b8de0 | ||
|
|
130bdf8337 | ||
|
|
8b7b7881f5 | ||
|
|
28a8435a52 | ||
|
|
dca9871744 | ||
|
|
6c2fbbf275 | ||
|
|
43bac80e4d | ||
|
|
0bf6af17e7 | ||
|
|
f7af8a3863 | ||
|
|
492af7a92d | ||
|
|
de4f2b9990 | ||
|
|
4a2ff204ec | ||
|
|
26dbb29b3d | ||
|
|
231aae2920 | ||
|
|
4cc7380a88 | ||
|
|
593fd29d00 | ||
|
|
f84d97b17b | ||
|
|
ef4d743af3 | ||
|
|
9ecc4a15df | ||
|
|
e165025c94 | ||
|
|
d711f2aef3 | ||
|
|
e724bec987 | ||
|
|
40b3543c3f | ||
|
|
e95b5127ca | ||
|
|
5243aa0628 | ||
|
|
509d18501c | ||
|
|
77067bdc58 | ||
|
|
358b50ba21 | ||
|
|
b40ca44e1a | ||
|
|
a68d621f2d | ||
|
|
21ca38d9e9 | ||
|
|
769efc20d1 | ||
|
|
32e416ea95 | ||
|
|
49e0f4e983 | ||
|
|
e08b466166 | ||
|
|
3f9935bbcc | ||
|
|
ef8bc7c5a8 | ||
|
|
fa02cf0f2b | ||
|
|
bc66a27baf | ||
|
|
3e54ac3aca | ||
|
|
a425102cf3 | ||
|
|
60cd28005f | ||
|
|
728f82c084 | ||
|
|
4437835057 | ||
|
|
1cc405d2dc | ||
|
|
2f0d549a50 | ||
|
|
c7aca64ba1 | ||
|
|
548961f610 | ||
|
|
b3072ce354 | ||
|
|
98b6b1911c | ||
|
|
ef96109ea5 | ||
|
|
cf1a759fa5 | ||
|
|
86c3fc9fac | ||
|
|
5f12f06297 | ||
|
|
33b3f4e312 | ||
|
|
603ec3f10a | ||
|
|
37bb82e87d | ||
|
|
047e0b7e8d | ||
|
|
782820c34a | ||
|
|
26cf1f7a89 | ||
|
|
f7ab4f04ac | ||
|
|
2751f08bdc | ||
|
|
0904d448e0 | ||
|
|
fab7952af6 | ||
|
|
6af75d6d40 | ||
|
|
5f1a37f0db | ||
|
|
b7bd3051b1 | ||
|
|
7adc3fe19b | ||
|
|
4842885aa6 | ||
|
|
00e8c0d1b0 | ||
|
|
239efa5fbd | ||
|
|
a9ba23bae8 | ||
|
|
62930f2821 | ||
|
|
4334c71bc7 | ||
|
|
21a029f17e | ||
|
|
9d37c3bd45 | ||
|
|
343ec220b4 | ||
|
|
2da4c10384 | ||
|
|
829dfee9e5 | ||
|
|
26c7bcfdd5 | ||
|
|
cf1b05d16e | ||
|
|
cfa8b8042a | ||
|
|
f0c0985708 | ||
|
|
73c937bc30 | ||
|
|
33e67abbe9 | ||
|
|
c20fb723ef | ||
|
|
ccd142df97 | ||
|
|
f6dbd1c0b5 | ||
|
|
d38d185d1c | ||
|
|
ef79b72471 | ||
|
|
ed12ab16e0 | ||
|
|
24820cfcff | ||
|
|
e10f62663e | ||
|
|
02573b0ad2 | ||
|
|
0dc39434c2 | ||
|
|
33acfce083 | ||
|
|
79d0eadbd6 | ||
|
|
73f7f16298 | ||
|
|
66eb0e42fe | ||
|
|
052ae53b6e | ||
|
|
1c0ed3268b | ||
|
|
0456abb327 | ||
|
|
ec50458491 | ||
|
|
b834ccaa2f | ||
|
|
ffb3de6c36 | ||
|
|
454c8459f2 | ||
|
|
94a0d09591 | ||
|
|
9b7a6ed85d | ||
|
|
7e31035114 | ||
|
|
e15a6146f8 | ||
|
|
e49cb1126b | ||
|
|
142bc5720e | ||
|
|
feaeb7870f | ||
|
|
9df0eefe49 | ||
|
|
8e89a9377a | ||
|
|
d3df8fe7ef | ||
|
|
9feda988a5 | ||
|
|
9534d533e3 | ||
|
|
85eefe1d8b | ||
|
|
3343270680 | ||
|
|
bf9d502742 | ||
|
|
85c0779ac0 | ||
|
|
070de44069 | ||
|
|
33087e3a99 | ||
|
|
fe4eb5e771 | ||
|
|
2fead23556 | ||
|
|
bc5a9e4c06 | ||
|
|
fafad6c961 | ||
|
|
a4de409235 | ||
|
|
a05fd45959 | ||
|
|
24de2f8a96 | ||
|
|
eee37011a5 | ||
|
|
a67706bea0 | ||
|
|
faa69b6cfe | ||
|
|
ba5ea2deff | ||
|
|
a6b25a4877 | ||
|
|
90d8cb6526 | ||
|
|
e71faa988e | ||
|
|
ed0ae81aae | ||
|
|
a61b078dea | ||
|
|
85939306e3 | ||
|
|
cf2d9c2c1d | ||
|
|
853d9047b0 | ||
|
|
6069d89dee | ||
|
|
d25e263b8e | ||
|
|
d05e696d45 | ||
|
|
ef583cead9 | ||
|
|
90c4da4e68 | ||
|
|
8032191366 | ||
|
|
c8c03294e1 | ||
|
|
e00df22588 | ||
|
|
9806217a6a | ||
|
|
1be5cf2d90 | ||
|
|
ca68bae4ed | ||
|
|
8c1f58079f | ||
|
|
4845c03eec | ||
|
|
532a595c41 | ||
|
|
25ce5bc90b | ||
|
|
80a929ccc6 | ||
|
|
3797863e14 | ||
|
|
7870793b4b | ||
|
|
a7c21a9729 | ||
|
|
914d54e672 | ||
|
|
a68e38e59e | ||
|
|
e870e5d8a5 | ||
|
|
29387e785c | ||
|
|
8eb6336f5e | ||
|
|
34a2be6458 | ||
|
|
fbd834469f | ||
|
|
8da5f740af | ||
|
|
38dcddb126 | ||
|
|
92948d2394 | ||
|
|
a3a0a80a02 | ||
|
|
a5f7019b2a | ||
|
|
e05b80cec1 | ||
|
|
23d5fbde56 | ||
|
|
a346704cdc | ||
|
|
24396d97ed | ||
|
|
703e41cd49 | ||
|
|
975c4165d0 | ||
|
|
dbf38d847a | ||
|
|
91a2519cc3 | ||
|
|
a1c3ba2088 | ||
|
|
d47565be5c | ||
|
|
1028d39db0 | ||
|
|
b250825c38 | ||
|
|
90a6a8f2d6 | ||
|
|
414386e797 | ||
|
|
3a843d5074 | ||
|
|
4e76bfa2ca | ||
|
|
8832281dcf | ||
|
|
853f3d9200 | ||
|
|
67e996c5f4 | ||
|
|
f09863faa0 | ||
|
|
eb1e3be722 | ||
|
|
4ec902b96f | ||
|
|
74243394d9 | ||
|
|
e7f263efa7 | ||
|
|
0991145c58 | ||
|
|
01d2a8588a | ||
|
|
45431a2649 | ||
|
|
0b78fb9201 | ||
|
|
9cdff275cb | ||
|
|
77c6e106e4 | ||
|
|
efdd6bfb19 | ||
|
|
117098d2d2 | ||
|
|
f933d892aa | ||
|
|
5ea54792c0 | ||
|
|
23a9280db7 | ||
|
|
79e27b1778 | ||
|
|
0a05579a1c | ||
|
|
0030eb4a13 | ||
|
|
5bf40343eb | ||
|
|
1e35f983c4 | ||
|
|
a15ac8870b | ||
|
|
8a27036a54 | ||
|
|
8543523f18 | ||
|
|
5a9b59866b | ||
|
|
dc76956215 | ||
|
|
98fb5e4bbd | ||
|
|
25ff2e5e61 | ||
|
|
5050f1ce1c | ||
|
|
51b691e7ac | ||
|
|
c4cd0a5f31 | ||
|
|
41142732ec | ||
|
|
62495c3bd1 | ||
|
|
d00184a7c3 | ||
|
|
ce32d5537d | ||
|
|
25921fa91a | ||
|
|
932a496c3c | ||
|
|
57862eeda6 | ||
|
|
11df94d601 | ||
|
|
0abaa20de9 | ||
|
|
c35a1dd79f | ||
|
|
e0b2572eba | ||
|
|
aada695b3f | ||
|
|
90f3858eff | ||
|
|
ecb482f297 | ||
|
|
641f391137 | ||
|
|
dc26f7ba9f | ||
|
|
6504930888 | ||
|
|
6b52c91257 | ||
|
|
0ffea495b1 | ||
|
|
72ba4ff2d4 | ||
|
|
88c4b191fb | ||
|
|
112d2a65f6 | ||
|
|
d999c1b434 | ||
|
|
3aa8b9a259 | ||
|
|
fdbea9e2ee | ||
|
|
6204eddade | ||
|
|
e789c671b0 | ||
|
|
8c2ee0ed85 | ||
|
|
2b48409cfd | ||
|
|
7251186762 | ||
|
|
82fda4ac0e | ||
|
|
1d12ddbdfc | ||
|
|
f474c76e19 | ||
|
|
ac99104114 | ||
|
|
0bb9b388f7 | ||
|
|
b59e4bb808 | ||
|
|
4fe647df0a | ||
|
|
a00350ab54 | ||
|
|
96c6b4efcb | ||
|
|
18714caa60 | ||
|
|
7110fe0159 | ||
|
|
5a590bca74 | ||
|
|
963f11a6b1 | ||
|
|
a7761fe55d | ||
|
|
10cf9305f1 | ||
|
|
b72ced4511 | ||
|
|
eb47c778db | ||
|
|
359b7feb8c | ||
|
|
6ffdede95a | ||
|
|
4db0250b95 | ||
|
|
69780bbbec | ||
|
|
fda89d6859 | ||
|
|
5d99b4af00 | ||
|
|
da4d4e1672 | ||
|
|
8f9aa276e8 | ||
|
|
8debb0d338 | ||
|
|
bc2a512101 | ||
|
|
4f728d363f | ||
|
|
e81af75427 | ||
|
|
8387b32bb8 | ||
|
|
2fccee740b | ||
|
|
c98002eb76 | ||
|
|
584e1b0500 | ||
|
|
cee65ed283 | ||
|
|
8104b17ee6 | ||
|
|
7676d3c7df | ||
|
|
3e6211e0a3 | ||
|
|
05c472b741 | ||
|
|
867ba1fd8c | ||
|
|
fd48719040 | ||
|
|
d987225ac8 | ||
|
|
503bdeeadb | ||
|
|
d3f1669234 | ||
|
|
3f62ce9c19 | ||
|
|
278c153d31 | ||
|
|
ae677099d6 | ||
|
|
140db9b2e2 | ||
|
|
a6b35031ae | ||
|
|
004cf2687a | ||
|
|
cf8db5ea23 | ||
|
|
1683e8d609 | ||
|
|
f372e034e8 | ||
|
|
8c3db49fba | ||
|
|
02b1aac0b0 | ||
|
|
6cb57f5d2a | ||
|
|
67dd1ac608 | ||
|
|
808a5c9ffd | ||
|
|
d16db5ed90 | ||
|
|
6e24f9d47b | ||
|
|
92ab09496a | ||
|
|
28017f97ea | ||
|
|
ea43413507 | ||
|
|
829b7b6b70 | ||
|
|
262bf441ce | ||
|
|
7aae9b380e | ||
|
|
d50327548b | ||
|
|
e6ce0ab9a7 | ||
|
|
9085c81e76 | ||
|
|
682349c03e | ||
|
|
a72389b28c | ||
|
|
f1bc00b67f | ||
|
|
06d159fb7b | ||
|
|
578560766d | ||
|
|
9e5c924ef1 | ||
|
|
cf39de882a | ||
|
|
8293cef703 | ||
|
|
60b99cfc66 | ||
|
|
7cc4189eb0 | ||
|
|
86d4a32314 | ||
|
|
67c9c64eab | ||
|
|
b8d0f947e8 | ||
|
|
bbd4363fa9 | ||
|
|
e0083fb8de | ||
|
|
3abafc307c | ||
|
|
055af0f78a | ||
|
|
e4873e4da9 | ||
|
|
2233cdc9cc | ||
|
|
816bc9b5c8 | ||
|
|
a82c82fcd7 | ||
|
|
bb28d02277 | ||
|
|
94877f4e7e | ||
|
|
3747ddbefb | ||
|
|
42731da546 | ||
|
|
e183d63a5e | ||
|
|
e5fdd442c3 | ||
|
|
97357c0e08 | ||
|
|
8649ce4c78 | ||
|
|
a0f6605f59 | ||
|
|
db9b1dd689 | ||
|
|
9c8d62151b | ||
|
|
c44d521279 | ||
|
|
ba9da05cef | ||
|
|
abd552fde6 | ||
|
|
3726761549 | ||
|
|
06c7145ac5 | ||
|
|
85f74dd802 | ||
|
|
86f681a007 | ||
|
|
bd5862437d | ||
|
|
a3827aaeae | ||
|
|
47c68e40a2 | ||
|
|
2a7eec816a | ||
|
|
fe0ddf6c83 | ||
|
|
9a73ead88d | ||
|
|
8fbb764c9e | ||
|
|
4756801fd9 | ||
|
|
f0e0b515ad | ||
|
|
25a0825ae4 | ||
|
|
b1ac297d71 | ||
|
|
2dfe9c1663 | ||
|
|
8a9c76b003 | ||
|
|
41cdd3e261 | ||
|
|
fe17165c39 | ||
|
|
e18671c1e4 | ||
|
|
b5f6219d39 | ||
|
|
5ed82aac5f | ||
|
|
f6a0a91a23 | ||
|
|
5645d0de03 | ||
|
|
ffaaf5e39c | ||
|
|
567cf7b8e5 | ||
|
|
5f8dd38135 | ||
|
|
a74d335cb4 | ||
|
|
6d594143ed | ||
|
|
7a5ad3fbdb | ||
|
|
584f7688f4 | ||
|
|
4436110c44 | ||
|
|
8a7c9d49b2 | ||
|
|
b5d41caace | ||
|
|
206813d560 | ||
|
|
e0ab1e906e | ||
|
|
f8b3526426 | ||
|
|
d83baab433 | ||
|
|
43e38ac483 | ||
|
|
b079d4da4c | ||
|
|
21e79ca078 | ||
|
|
a25bbea555 | ||
|
|
b7664a4108 | ||
|
|
d360cd3434 | ||
|
|
e037db076c | ||
|
|
3ef19f41e6 | ||
|
|
da90ec15fa | ||
|
|
7f5af46300 | ||
|
|
624e6ee047 | ||
|
|
4a1f3cd61f | ||
|
|
7c4a3d2b02 | ||
|
|
8db1bb56f2 | ||
|
|
d75198a8ee | ||
|
|
cadb41c9e3 | ||
|
|
b30cae0473 | ||
|
|
7290086fe9 | ||
|
|
bca920bea0 | ||
|
|
32de7a3fdc | ||
|
|
f20512b599 | ||
|
|
cd41ca571f | ||
|
|
dc654e9f6c | ||
|
|
f5d7f70472 | ||
|
|
0168442c22 | ||
|
|
22579b77cc | ||
|
|
09c09d2fd1 | ||
|
|
b669cf9ce7 | ||
|
|
5bc617a9a6 | ||
|
|
a75b811061 | ||
|
|
ec6b46324e | ||
|
|
97f764b45d | ||
|
|
7f31a55506 | ||
|
|
2286d097dc | ||
|
|
52a40ec99a | ||
|
|
a78fa73b34 | ||
|
|
d7e4a252fb | ||
|
|
1c0b0abf61 | ||
|
|
f7c6620e25 | ||
|
|
16372f7847 | ||
|
|
72c2eb7182 | ||
|
|
144bfb71cf | ||
|
|
3fd9e23851 | ||
|
|
10642d0e04 | ||
|
|
063ab2f87d | ||
|
|
1802cf8dbc | ||
|
|
090975481b | ||
|
|
228816f5f8 | ||
|
|
8522e028f1 | ||
|
|
a2776dfc86 | ||
|
|
cc95c8cfb0 | ||
|
|
89dac9d2a6 | ||
|
|
8cdfc883b9 | ||
|
|
b3689eceb7 | ||
|
|
5cee2afc6d | ||
|
|
50fef0fb26 | ||
|
|
4c46ef69e9 | ||
|
|
22e8fade7e | ||
|
|
37aa06f508 | ||
|
|
f6d2f8f929 | ||
|
|
32947669d5 | ||
|
|
fdf3015ad0 | ||
|
|
03bfcde147 | ||
|
|
56fc43400a | ||
|
|
7b4d35d224 | ||
|
|
a99fc115f8 | ||
|
|
d8e5f57d53 | ||
|
|
aa85e597d9 | ||
|
|
08ab92da80 | ||
|
|
5d52fd2486 | ||
|
|
4ae9850e13 | ||
|
|
e14190ae4b | ||
|
|
ce445a8096 |
16
.cargo-husky/hooks/pre-push
Executable file
16
.cargo-husky/hooks/pre-push
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
if !(command cargo-make >/dev/null 2>&1); then # Check if cargo-make is installed
|
||||
echo Attempting to run cargo-make as part of the pre-push hook but it\'s not installed.
|
||||
echo Please install it by running the following command:
|
||||
echo
|
||||
echo " cargo install --force cargo-make"
|
||||
echo
|
||||
echo If you don\'t want to run cargo-make as part of the pre-push hook, you can run
|
||||
echo the following command instead of git push:
|
||||
echo
|
||||
echo " git push --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cargo make ci
|
||||
78
.cz.toml
Normal file
78
.cz.toml
Normal file
@@ -0,0 +1,78 @@
|
||||
# configuration for https://github.com/commitizen/cz-cli
|
||||
|
||||
[tool.commitizen]
|
||||
name = "cz_customize"
|
||||
tag_format = "$version"
|
||||
version_type = "semver"
|
||||
version_provider = "cargo"
|
||||
update_changelog_on_bump = true
|
||||
major_version_zero = true
|
||||
use_shortcuts = true
|
||||
|
||||
[tool.commitizen.customize]
|
||||
message_template = """{{change_type}}({{scope}}): {{subject}}
|
||||
|
||||
{% if body %}\
|
||||
{{body}}\
|
||||
{% endif %}
|
||||
|
||||
{%if is_breaking_change %}\
|
||||
BREAKING_CHANGE: \
|
||||
{% endif %}\
|
||||
{{footer}}\
|
||||
"""
|
||||
example = "feature: this feature enable customize through config file"
|
||||
schema = "<type>(<scope>): <subject>\n\n<body>\n\n<footer>"
|
||||
schema_pattern = "(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)\\(\\w+\\):\\s(?P<subject>.*)(\\n\\n(?P<body>.*))?(\\n\\n(?P<footer>.*))?"
|
||||
|
||||
# The order needs to be preserved, as it influences the order when executing cz commit/cz c
|
||||
|
||||
# Change types
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "list"
|
||||
name = "change_type"
|
||||
choices = [
|
||||
{ value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)", key = "b" },
|
||||
{ value = "chore", name = "chore: A modification that generally does not fall into any other category", key = "c" },
|
||||
{ value = "ci", name = "ci: Changes to our CI configuration files and scripts (example scopes: GitLabCI)", key = "i" },
|
||||
{ value = "docs", name = "docs: Documentation only changes", key = "d" },
|
||||
{ value = "feat", name = "feat: A new feature.", key = "f" },
|
||||
{ value = "fix", name = "fix: A bug fix.", key = "x" },
|
||||
{ value = "perf", name = "perf: A code change that improves performance", key = "p" },
|
||||
{ value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature", key = "r" },
|
||||
{ value = "revert", name = "revert: Revert previous commits", key = "v" },
|
||||
{ value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", key = "s" },
|
||||
{ value = "test", name = "test: Adding missing or correcting existing tests", key = "t" },
|
||||
]
|
||||
message = "Select the type of change you are committing"
|
||||
|
||||
# The scope of the change, can be a file, class name or other context
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "input"
|
||||
name = "scope"
|
||||
message = "What is the scope of this change? (class or file name): (press [enter] to skip)\n"
|
||||
|
||||
# Summary of the changes
|
||||
[[tool.commitizen.customize.questions]]
|
||||
"type" = "input"
|
||||
"name" = "subject"
|
||||
"message" = "Write a short and imperative summary of the code changes: (lower case and no period)\n"
|
||||
|
||||
# The commit body, elaborate the changes if need be.
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "input"
|
||||
name = "body"
|
||||
message = "Provide additional contextual information about the code changes: (press [enter] to skip)\n"
|
||||
|
||||
# Specify if the changes are breaking
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "confirm"
|
||||
name = "is_breaking_change"
|
||||
message = "Is this a BREAKING CHANGE?"
|
||||
default = false
|
||||
|
||||
# Reference closing issues and share other
|
||||
[[tool.commitizen.customize.questions]]
|
||||
type = "input"
|
||||
name = "footer"
|
||||
message = "Footer. Information about Breaking Changes and reference issues that this commit closes: (press [enter] to skip)"
|
||||
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@@ -0,0 +1,11 @@
|
||||
# configuration for https://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*.rs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
8
.github/CODEOWNERS
vendored
Normal file
8
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# See https://help.github.com/articles/about-codeowners/
|
||||
# for more info about CODEOWNERS file
|
||||
|
||||
# It uses the same pattern rule for gitignore file
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @sophacles @joshka @kdheepak
|
||||
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
60
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create an issue about a bug you encountered
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
<!--
|
||||
Hi there, sorry `ratatui` is not working as expected.
|
||||
Please fill this bug report conscientiously.
|
||||
A detailed and complete issue is more likely to be processed quickly.
|
||||
-->
|
||||
|
||||
## Description
|
||||
<!--
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
|
||||
## To Reproduce
|
||||
<!--
|
||||
Try to reduce the issue to a simple code sample exhibiting the problem.
|
||||
Ideally, fork the project and add a test or an example.
|
||||
-->
|
||||
|
||||
|
||||
## Expected behavior
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
|
||||
## Screenshots
|
||||
<!--
|
||||
If applicable, add screenshots, gifs or videos to help explain your problem.
|
||||
-->
|
||||
|
||||
|
||||
## Environment
|
||||
<!--
|
||||
Add a description of the systems where you are observing the issue. For example:
|
||||
- OS: Linux
|
||||
- Terminal Emulator: xterm
|
||||
- Font: Inconsolata (Patched)
|
||||
- Crate version: 0.7
|
||||
- Backend: termion
|
||||
-->
|
||||
|
||||
- OS:
|
||||
- Terminal Emulator:
|
||||
- Font:
|
||||
- Crate version:
|
||||
- Backend:
|
||||
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
If you already looked into the issue, include all the leads you have explored.
|
||||
-->
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
32
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
<!--
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
## Solution
|
||||
<!--
|
||||
A clear and concise description of what you want to happen.
|
||||
Things to consider:
|
||||
- backward compatibility
|
||||
- ease of use of the API (https://rust-lang.github.io/api-guidelines/)
|
||||
- consistency with the rest of the crate
|
||||
-->
|
||||
|
||||
## Alternatives
|
||||
<!--
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
## Additional context
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
1
.github/pull_request_template.md
vendored
Normal file
1
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<!-- Please read CONTRIBUTING.md before submitting any pull request. -->
|
||||
80
.github/workflows/cd.yml
vendored
Normal file
80
.github/workflows/cd.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: Continuous Deployment
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# At 00:00 on Saturday
|
||||
# https://crontab.guru/#0_0_*_*_6
|
||||
- cron: "0 0 * * 6"
|
||||
push:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
publish-nightly:
|
||||
name: Create a nightly release
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Calculate the next release
|
||||
run: |
|
||||
suffix="-alpha"
|
||||
last_tag="$(git describe --abbrev=0 --tags `git rev-list --tags --max-count=1`)"
|
||||
if [[ "${last_tag}" = *"${suffix}"* ]]; then
|
||||
# increment the alpha version
|
||||
alpha=$(echo "${last_tag}" | grep -oE '([0-9]+)$')
|
||||
next_alpha=$((alpha + 1))
|
||||
next_tag=$(echo "${last_tag}" | sed "s/\.[0-9]\+$/\.${next_alpha}/")
|
||||
else
|
||||
# start the alpha version from 0
|
||||
next_tag="${last_tag}${suffix}.0"
|
||||
fi
|
||||
# update the crate version
|
||||
msg="# crate version"
|
||||
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
|
||||
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
|
||||
echo "Next nightly release: ${next_tag} 🐭"
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --dry-run --allow-dirty
|
||||
|
||||
- name: Generate a changelog
|
||||
uses: orhun/git-cliff-action@v2
|
||||
with:
|
||||
config: cliff.toml
|
||||
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
|
||||
env:
|
||||
OUTPUT: BODY.md
|
||||
|
||||
- name: Publish on GitHub
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: ${{ env.NEXT_TAG }}
|
||||
prerelease: true
|
||||
bodyFile: BODY.md
|
||||
|
||||
publish-stable:
|
||||
name: Create a stable release
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ startsWith(github.event.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Publish on crates.io
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: publish
|
||||
args: --dry-run
|
||||
152
.github/workflows/ci.yml
vendored
Normal file
152
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
|
||||
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# don't install husky hooks during CI as they are only needed for for pre-push
|
||||
CARGO_HUSKY_DONT_INSTALL_HOOKS: true
|
||||
|
||||
# lint, clippy and coveraget jobs are intentionally early in the workflow to catch simple
|
||||
# formatting, typos, and missing tests as early as possible. This allows us to fix these and
|
||||
# resubmit the PR without having to wait for the comprehensive matrix of tests to complete.
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Check conventional commits
|
||||
uses: crate-ci/committed@master
|
||||
with:
|
||||
args: "-vv"
|
||||
commits: HEAD
|
||||
- name: Check typos
|
||||
uses: crate-ci/typos@master
|
||||
- name: Lint dependencies
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- name: Install Rust nightly
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Check formatting
|
||||
run: cargo make fmt
|
||||
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Run cargo make clippy-all
|
||||
run: cargo make clippy
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools
|
||||
- name: Install cargo-llvm-cov and cargo-make
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-make
|
||||
- name: Generate coverage
|
||||
run: cargo make coverage
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
check:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.65.0", "stable" ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust {{ matrix.toolchain }}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Run cargo make check
|
||||
run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
test-doc:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Test docs
|
||||
run: cargo make test-doc
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
toolchain: [ "1.65.0", "stable" ]
|
||||
backend: [ crossterm, termion, termwiz ]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Install Rust ${{ matrix.toolchain }}}
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- name: Install cargo-make
|
||||
uses: taiki-e/install-action@cargo-make
|
||||
- name: Test ${{ matrix.backend }}
|
||||
run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ Cargo.lock
|
||||
*.log
|
||||
*.rs.rustfmt
|
||||
.gdb_history
|
||||
.idea/
|
||||
|
||||
9
.markdownlint.yaml
Normal file
9
.markdownlint.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# configuration for https://github.com/DavidAnson/markdownlint
|
||||
|
||||
no-inline-html:
|
||||
allowed_elements:
|
||||
- img
|
||||
- details
|
||||
- summary
|
||||
line-length:
|
||||
line_length: 100
|
||||
21
.travis.yml
21
.travis.yml
@@ -1,21 +0,0 @@
|
||||
language: rust
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
cache: cargo
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
|
||||
before_script:
|
||||
- rustup component add rustfmt-preview
|
||||
|
||||
script:
|
||||
- if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then make fmt; fi
|
||||
- make build
|
||||
- make test
|
||||
1017
CHANGELOG.md
1017
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
157
CONTRIBUTING.md
Normal file
157
CONTRIBUTING.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Contribution guidelines
|
||||
|
||||
First off, thank you for considering contributing to Ratatui.
|
||||
|
||||
If your contribution is not straightforward, please first discuss the change you wish to make by
|
||||
creating a new issue before making the change, or starting a discussion on
|
||||
[discord](https://discord.gg/pMCEU9hNEj).
|
||||
|
||||
## Reporting issues
|
||||
|
||||
Before reporting an issue on the [issue tracker](https://github.com/ratatui-org/ratatui/issues),
|
||||
please check that it has not already been reported by searching for some related keywords. Please
|
||||
also check [`tui-rs` issues](https://github.com/fdehau/tui-rs/issues/) and link any related issues
|
||||
found.
|
||||
|
||||
## Pull requests
|
||||
|
||||
All contributions are obviously welcome. Please include as many details as possible in your PR
|
||||
description to help the reviewer (follow the provided template). Make sure to highlight changes
|
||||
which may need additional attention or you are uncertain about. Any idea with a large scale impact
|
||||
on the crate or its users should ideally be discussed in a "Feature Request" issue beforehand.
|
||||
|
||||
### Keep PRs small, intentional and focused
|
||||
|
||||
Try to do one pull request per change. The time taken to review a PR grows exponential with the size
|
||||
of the change. Small focused PRs will generally be much more faster to review. PRs that include both
|
||||
refactoring (or reformatting) with actual changes are more difficult to review as every line of the
|
||||
change becomes a place where a bug may have been introduced. Consider splitting refactoring /
|
||||
reformatting changes into a separate PR from those that make a behavioral change, as the tests help
|
||||
guarantee that the behavior is unchanged.
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
|
||||
The original fork of Ratatui, [`tui-rs`](https://github.com/fdehau/tui-rs/), has a large amount of
|
||||
history of the project. Please search, read, link, and summarize any relevant
|
||||
[issues](https://github.com/fdehau/tui-rs/issues/),
|
||||
[discussions](https://github.com/fdehau/tui-rs/discussions/) and [pull
|
||||
requests](https://github.com/fdehau/tui-rs/pulls).
|
||||
|
||||
### Use conventional commits
|
||||
|
||||
We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) and check for them as
|
||||
a lint build step. To help adhere to the format, we recommend to install
|
||||
[Commitizen](https://commitizen-tools.github.io/commitizen/). By using this tool you automatically
|
||||
follow the configuration defined in [.cz.toml](.cz.toml). Your commit messages should have enough
|
||||
information to help someone reading the [CHANGELOG](./CHANGELOG.md) understand what is new just from
|
||||
the title. The summary helps expand on that to provide information that helps provide more context,
|
||||
describes the nature of the problem that the commit is solving and any unintuitive effects of the
|
||||
change. It's rare that code changes can easily communicate intent, so make sure this is clearly
|
||||
documented.
|
||||
|
||||
### Clean up your commits
|
||||
|
||||
The final version of your PR that will be committed to the repository should be rebased and tested
|
||||
against main. Every commit will end up as a line in the changelog, so please squash commits that are
|
||||
only formatting or incremental fixes to things brought up as part of the PR review. Aim for a single
|
||||
commit (unless there is a strong reason to stack the commits). See [Git Best Practices - On Sausage
|
||||
Making](https://sethrobertson.github.io/GitBestPractices/#sausage) for more on this.
|
||||
|
||||
### Run CI tests before pushing a PR
|
||||
|
||||
We're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run git hooks,
|
||||
which will run `cargo make ci` before each push. To initialize the hook run `cargo test`. If
|
||||
`cargo-make` is not installed, it will provide instructions to install it for you. This will ensure
|
||||
that your code is formatted, compiles and passes all tests before you push. If you need to skip this
|
||||
check, you can use `git push --no-verify`.
|
||||
|
||||
### Sign your commits
|
||||
|
||||
We use commit signature verification, which will block commits from being merged via the UI unless
|
||||
they are signed. To set up your machine to sign commits, see [managing commit signature
|
||||
verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification)
|
||||
in GitHub docs.
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Setup
|
||||
|
||||
Clone the repo and build it using [cargo-make](https://sagiegurari.github.io/cargo-make/)
|
||||
|
||||
Ratatui is an ordinary Rust project where common tasks are managed with
|
||||
[cargo-make](https://github.com/sagiegurari/cargo-make/). It wraps common `cargo` commands with sane
|
||||
defaults depending on your platform of choice. Building the project should be as easy as running
|
||||
`cargo make build`.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/ratatui-org/ratatui.git
|
||||
cd ratatui
|
||||
cargo make build
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
The [test coverage](https://app.codecov.io/gh/ratatui-org/ratatui) of the crate is reasonably
|
||||
good, but this can always be improved. Focus on keeping the tests simple and obvious and write unit
|
||||
tests for all new or modified code. Beside the usual doc and unit tests, one of the most valuable
|
||||
test you can write for Ratatui is a test against the `TestBackend`. It allows you to assert the
|
||||
content of the output buffer that would have been flushed to the terminal after a given draw call.
|
||||
See `widgets_block_renders` in [tests/widgets_block.rs](./tests/widget_block.rs) for an example.
|
||||
|
||||
When writing tests, generally prefer to write unit tests and doc tests directly in the code file
|
||||
being tested rather than integration tests in the `tests/` folder.
|
||||
|
||||
If an area that you're making a change in is not tested, write tests to characterize the existing
|
||||
behavior before changing it. This helps ensure that we don't introduce bugs to existing software
|
||||
using Ratatui (and helps make it easy to migrate apps still using `tui-rs`).
|
||||
|
||||
For coverage, we have two [bacon](https://dystroy.org/bacon/) jobs (one for all tests, and one for
|
||||
unit tests, keyboard shortcuts `v` and `u` respectively) that run
|
||||
[cargo-llvm-cov](https://github.com/taiki-e/cargo-llvm-cov) to report the coverage. Several plugins
|
||||
exist to show coverage directly in your editor. E.g.:
|
||||
|
||||
- <https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters>
|
||||
- <https://github.com/alepez/vim-llvmcov>
|
||||
|
||||
### Use of unsafe for optimization purposes
|
||||
|
||||
We don't currently use any unsafe code in Ratatui, and would like to keep it that way. However there
|
||||
may be specific cases that this becomes necessary in order to avoid slowness. Please see [this
|
||||
discussion](https://github.com/ratatui-org/ratatui/discussions/66) for more about the decision.
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
We use Github Actions for the CI where we perform the following checks:
|
||||
|
||||
- The code should compile on `stable` and the Minimum Supported Rust Version (MSRV).
|
||||
- The tests (docs, lib, tests and examples) should pass.
|
||||
- The code should conform to the default format enforced by `rustfmt`.
|
||||
- The code should not contain common style issues `clippy`.
|
||||
|
||||
You can also check most of those things yourself locally using `cargo make ci` which will offer you
|
||||
a shorter feedback loop than pushing to github.
|
||||
|
||||
## Relationship with `tui-rs`
|
||||
|
||||
This project was forked from [`tui-rs`](https://github.com/fdehau/tui-rs/) in February 2023, with the
|
||||
[blessing of the original author](https://github.com/fdehau/tui-rs/issues/654), Florian Dehau
|
||||
([@fdehau](https://github.com/fdehau)).
|
||||
|
||||
The original repository contains all the issues, PRs and discussion that were raised originally, and
|
||||
it is useful to refer to when contributing code, documentation, or issues with Ratatui.
|
||||
|
||||
We imported all the PRs from the original repository and implemented many of the smaller ones and
|
||||
made notes on the leftovers. These are marked as draft PRs and labelled as [imported from
|
||||
tui](https://github.com/ratatui-org/ratatui/pulls?q=is%3Apr+is%3Aopen+label%3A%22imported+from+tui%22).
|
||||
We have documented the current state of those PRs, and anyone is welcome to pick them up and
|
||||
continue the work on them.
|
||||
|
||||
We have not imported all issues opened on the previous repository. For that reason, anyone wanting
|
||||
to **work on or discuss** an issue will have to follow the following workflow:
|
||||
|
||||
- Recreate the issue
|
||||
- Start by referencing the **original issue**: ```Referencing issue #[<issue number>](<original
|
||||
issue link>)```
|
||||
- Then, paste the original issues **opening** text
|
||||
|
||||
You can then resume the conversation by replying to this new issue you have created.
|
||||
172
Cargo.toml
172
Cargo.toml
@@ -1,44 +1,162 @@
|
||||
[package]
|
||||
name = "tui"
|
||||
version = "0.3.0-beta.1"
|
||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
"""
|
||||
name = "ratatui"
|
||||
version = "0.22.0" # crate version
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library to build rich terminal user interfaces or dashboards"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
keywords = ["tui", "terminal", "dashboard"]
|
||||
repository = "https://github.com/fdehau/tui-rs"
|
||||
repository = "https://github.com/ratatui-org/ratatui"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = ["docs/*", ".travis.yml"]
|
||||
exclude = [
|
||||
"assets/*",
|
||||
".github",
|
||||
"Makefile.toml",
|
||||
"CONTRIBUTING.md",
|
||||
"*.log",
|
||||
"tags",
|
||||
]
|
||||
autoexamples = true
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "fdehau/tui-rs" }
|
||||
|
||||
[features]
|
||||
default = ["termion"]
|
||||
default = ["crossterm"]
|
||||
all-widgets = ["widget-calendar"]
|
||||
widget-calendar = ["time"]
|
||||
macros = []
|
||||
serde = ["dep:serde", "bitflags/serde"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.0"
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
itertools = "0.7"
|
||||
log = "0.4"
|
||||
either = "1.5"
|
||||
unicode-segmentation = "1.2"
|
||||
crossterm = { version = "0.26", optional = true }
|
||||
indoc = "2.0"
|
||||
paste = "1.0.2"
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
termion = { version = "2.0", optional = true }
|
||||
termwiz = { version = "0.20.0", optional = true }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-width = "0.1"
|
||||
termion = { version = "1.5", optional = true }
|
||||
rustbox = { version = "0.11", optional = true }
|
||||
crossterm = { version = "0.4", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
stderrlog = "0.4"
|
||||
rand = "0.4"
|
||||
anyhow = "1.0.71"
|
||||
argh = "0.1"
|
||||
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
fakeit = "1.1"
|
||||
itertools = "0.10"
|
||||
rand = "0.8"
|
||||
|
||||
[[bench]]
|
||||
name = "paragraph"
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "rustbox"
|
||||
path = "examples/rustbox.rs"
|
||||
required-features = ["rustbox"]
|
||||
|
||||
[[example]]
|
||||
name = "crossterm"
|
||||
path = "examples/crossterm.rs"
|
||||
name = "barchart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "chart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "demo"
|
||||
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "panic"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "scrollbar"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
3
LICENSE
3
LICENSE
@@ -1,6 +1,7 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Florian Dehau
|
||||
Copyright (c) 2016-2022 Florian Dehau
|
||||
Copyright (c) 2023 The Ratatui Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
91
Makefile
91
Makefile
@@ -1,91 +0,0 @@
|
||||
# Makefile for the tui-rs project (https://github.com/fdehau/tui-rs)
|
||||
|
||||
|
||||
# ================================ Cargo ======================================
|
||||
|
||||
|
||||
RUST_CHANNEL ?= stable
|
||||
CARGO_FLAGS =
|
||||
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
|
||||
|
||||
ifndef RUSTUP_INSTALLED
|
||||
CARGO = cargo
|
||||
else
|
||||
ifdef CI
|
||||
CARGO = cargo
|
||||
else
|
||||
CARGO = rustup run $(RUST_CHANNEL) cargo
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
# ================================ Help =======================================
|
||||
|
||||
|
||||
help: ## Print all the available commands
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
|
||||
# =============================== Build =======================================
|
||||
|
||||
check: ## Validate the project code
|
||||
$(CARGO) check
|
||||
|
||||
build: ## Build the project in debug mode
|
||||
$(CARGO) build $(CARGO_FLAGS)
|
||||
|
||||
release: CARGO_FLAGS += --release
|
||||
release: build ## Build the project in release mode
|
||||
|
||||
|
||||
# ================================ Lint =======================================
|
||||
|
||||
RUSTFMT_WRITEMODE ?= 'diff'
|
||||
|
||||
lint: fmt clippy ## Lint project files
|
||||
|
||||
fmt: ## Check the format of the source code
|
||||
cargo fmt --all -- --check
|
||||
|
||||
clippy: RUST_CHANNEL = nightly
|
||||
clippy: ## Check the style of the source code and catch common errors
|
||||
$(CARGO) clippy --features="termion rustbox"
|
||||
|
||||
|
||||
# ================================ Test =======================================
|
||||
|
||||
|
||||
test: ## Run the tests
|
||||
$(CARGO) test
|
||||
|
||||
# ================================ Doc ========================================
|
||||
|
||||
|
||||
doc: ## Build the documentation (available at ./target/doc)
|
||||
$(CARGO) doc
|
||||
|
||||
|
||||
# ================================= Watch =====================================
|
||||
|
||||
# Requires watchman and watchman-make (https://facebook.github.io/watchman/docs/install.html)
|
||||
|
||||
watch: ## Watch file changes and build the project if any
|
||||
watchman-make -p 'src/**/*.rs' -t check build
|
||||
|
||||
watch-test: ## Watch files changes and run the tests if any
|
||||
watchman-make -p 'src/**/*.rs' 'tests/**/*.rs' 'examples/**/*.rs' -t test
|
||||
|
||||
watch-doc: ## Watch file changes and rebuild the documentation if any
|
||||
watchman-make -p 'src/**/*.rs' -t doc
|
||||
|
||||
# ================================= Pipelines =================================
|
||||
|
||||
stable: RUST_CHANNEL = stable
|
||||
stable: build test ## Run build and tests for stable
|
||||
|
||||
beta: RUST_CHANNEL = beta
|
||||
beta: build test ## Run build and tests for beta
|
||||
|
||||
nightly: RUST_CHANNEL = nightly
|
||||
nightly: build lint test ## Run build, lint and tests for nightly
|
||||
173
Makefile.toml
Normal file
173
Makefile.toml
Normal file
@@ -0,0 +1,173 @@
|
||||
# configuration for https://github.com/sagiegurari/cargo-make
|
||||
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env]
|
||||
# all features except the backend ones
|
||||
ALL_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
[tasks.ci]
|
||||
dependencies = [
|
||||
"style-check",
|
||||
"clippy",
|
||||
"check",
|
||||
"test",
|
||||
]
|
||||
|
||||
[tasks.style-check]
|
||||
dependencies = ["fmt", "typos"]
|
||||
|
||||
[tasks.fmt]
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all", "--check"]
|
||||
|
||||
[tasks.typos]
|
||||
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
|
||||
command = "typos"
|
||||
|
||||
[tasks.check]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--all-features"
|
||||
]
|
||||
|
||||
[tasks.check.windows]
|
||||
args = [
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
|
||||
]
|
||||
|
||||
[tasks.build]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"build",
|
||||
"--all-targets",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.build.windows]
|
||||
args = [
|
||||
"build",
|
||||
"--all-targets",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
|
||||
]
|
||||
|
||||
[tasks.clippy]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--all-features",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.clippy.windows]
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.test]
|
||||
dependencies = [
|
||||
"test-doc",
|
||||
]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--all-targets",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
|
||||
[tasks.test-windows]
|
||||
dependencies = [
|
||||
"test-doc",
|
||||
]
|
||||
args = [
|
||||
"test",
|
||||
"--all-targets",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
|
||||
]
|
||||
|
||||
[tasks.test-doc]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test", "--doc",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.test-doc.windows]
|
||||
args = [
|
||||
"test", "--doc",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},crossterm,termwiz"
|
||||
]
|
||||
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
command = "cargo"
|
||||
args = [
|
||||
"test",
|
||||
"--all-targets",
|
||||
"--no-default-features", "--features", "${ALL_FEATURES},${@}"
|
||||
]
|
||||
|
||||
|
||||
[tasks.coverage]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.coverage.windows]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path", "target/lcov.info",
|
||||
"--no-default-features",
|
||||
"--features", "${ALL_FEATURES},crossterm,termwiz",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
command = "cargo"
|
||||
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}", "--features", "all-widgets"]
|
||||
|
||||
[tasks.build-examples]
|
||||
command = "cargo"
|
||||
args = ["build", "--examples", "--release", "--features", "all-widgets"]
|
||||
|
||||
[tasks.run-examples]
|
||||
dependencies = ["build-examples"]
|
||||
script = '''
|
||||
#!@duckscript
|
||||
files = glob_array ./examples/*.rs
|
||||
for file in ${files}
|
||||
name = basename ${file}
|
||||
name = substring ${name} -3
|
||||
set_env TUI_EXAMPLE_NAME ${name}
|
||||
cm_run_task run-example
|
||||
end
|
||||
'''
|
||||
289
README.md
289
README.md
@@ -1,68 +1,265 @@
|
||||
# tui-rs
|
||||
# Ratatui
|
||||
|
||||
[](https://travis-ci.org/fdehau/tui-rs)
|
||||
[](https://crates.io/crates/tui)
|
||||
[](https://docs.rs/crate/tui/)
|
||||
<img align="left" src="https://avatars.githubusercontent.com/u/125200832?s=128&v=4">
|
||||
|
||||
<img src="./docs/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
|
||||
`ratatui` is a [Rust](https://www.rust-lang.org) library to build rich terminal user interfaces and
|
||||
dashboards. It is a community fork of the original [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
project.
|
||||
|
||||
`tui-rs` is a [Rust](https://www.rust-lang.org) library to build rich terminal
|
||||
user interfaces and dashboards. It is heavily inspired by the `Javascript`
|
||||
library [blessed-contrib](https://github.com/yaronn/blessed-contrib) and the
|
||||
`Go` library [termui](https://github.com/gizak/termui).
|
||||
[](https://crates.io/crates/ratatui)
|
||||
[](./LICENSE) [](https://github.com/ratatui-org/ratatui/actions?query=workflow%3ACI+)
|
||||
[](https://docs.rs/crate/ratatui/)
|
||||
[](https://deps.rs/repo/github/ratatui-org/ratatui)
|
||||
[](https://app.codecov.io/gh/ratatui-org/ratatui)
|
||||
[](https://discord.gg/pMCEU9hNEj)
|
||||
|
||||
The library itself supports two different backends to draw to the terminal. You
|
||||
can either choose from:
|
||||
<!-- See RELEASE.md for instructions on creating the demo gif --->
|
||||

|
||||
|
||||
- [termion](https://github.com/ticki/termion)
|
||||
- [rustbox](https://github.com/gchp/rustbox)
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
However, some features may only be available in one of the two.
|
||||
* [Ratatui](#ratatui)
|
||||
* [Installation](#installation)
|
||||
* [Introduction](#introduction)
|
||||
* [Quickstart](#quickstart)
|
||||
* [Status of this fork](#status-of-this-fork)
|
||||
* [Rust version requirements](#rust-version-requirements)
|
||||
* [Documentation](#documentation)
|
||||
* [Examples](#examples)
|
||||
* [Widgets](#widgets)
|
||||
* [Built in](#built-in)
|
||||
* [Third\-party libraries, bootstrapping templates and
|
||||
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
|
||||
* [Apps](#apps)
|
||||
* [Alternatives](#alternatives)
|
||||
* [Contributors](#contributors)
|
||||
* [Acknowledgments](#acknowledgments)
|
||||
* [License](#license)
|
||||
|
||||
The library is based on the principle of immediate rendering with intermediate
|
||||
buffers. This means that at each new frame you should build all widgets that are
|
||||
supposed to be part of the UI. While providing a great flexibility for rich and
|
||||
interactive UI, this may introduce overhead for highly dynamic content. So, the
|
||||
implementation try to minimize the number of ansi escapes sequences generated to
|
||||
draw the updated UI. In practice, given the speed of `Rust` the overhead rather
|
||||
comes from the terminal emulator than the library itself.
|
||||
</details>
|
||||
|
||||
Moreover, the library does not provide any input handling nor any event system and
|
||||
you may rely on the previously cited libraries to achieve such features.
|
||||
## Installation
|
||||
|
||||
### [Documentation](https://docs.rs/tui)
|
||||
```shell
|
||||
cargo add ratatui --features all-widgets
|
||||
```
|
||||
|
||||
### Demo
|
||||
Or modify your `Cargo.toml`
|
||||
|
||||
The [source code](examples/demo.rs) of the demo gif.
|
||||
```toml
|
||||
[dependencies]
|
||||
ratatui = { version = "0.22.0", features = ["all-widgets"]}
|
||||
```
|
||||
|
||||
### Widgets
|
||||
Ratatui is mostly backwards compatible with `tui-rs`. To migrate an existing project, it may be
|
||||
easier to rename the ratatui dependency to `tui` rather than updating every usage of the crate.
|
||||
E.g.:
|
||||
|
||||
The library comes with the following list of widgets:
|
||||
```toml
|
||||
[dependencies]
|
||||
tui = { package = "ratatui", version = "0.22.0", features = ["all-widgets"]}
|
||||
```
|
||||
|
||||
* [Block](examples/block.rs)
|
||||
* [Gauge](examples/gauge.rs)
|
||||
* [Sparkline](examples/sparkline.rs)
|
||||
* [Chart](examples/chart.rs)
|
||||
* [BarChart](examples/bar_chart.rs)
|
||||
* [List](examples/list.rs)
|
||||
* [Table](examples/table.rs)
|
||||
* [Paragraph](examples/paragraph.rs)
|
||||
* [Canvas (with line, point cloud, map)](examples/canvas.rs)
|
||||
* [Tabs](examples/tabs.rs)
|
||||
## Introduction
|
||||
|
||||
Click on each item to see the source of the example. Run the examples with with
|
||||
cargo (e.g. to run the demo `cargo run --example demo`), and quit by pressing `q`.
|
||||
`ratatui` is a terminal UI library that supports multiple backends:
|
||||
|
||||
### Third-party widgets
|
||||
* [crossterm](https://github.com/crossterm-rs/crossterm) [default]
|
||||
* [termion](https://github.com/ticki/termion)
|
||||
* [termwiz](https://github.com/wez/wezterm/tree/master/termwiz)
|
||||
|
||||
* [tui-logger](https://github.com/gin66/tui-logger)
|
||||
The library is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that at each new frame you should build all widgets that are supposed to be part of the UI. While
|
||||
providing a great flexibility for rich and interactive UI, this may introduce overhead for highly
|
||||
dynamic content. So, the implementation try to minimize the number of ansi escapes sequences
|
||||
generated to draw the updated UI. In practice, given the speed of `Rust` the overhead rather comes
|
||||
from the terminal emulator than the library itself.
|
||||
|
||||
### Alternatives
|
||||
Moreover, the library does not provide any input handling nor any event system and you may rely on
|
||||
the previously cited libraries to achieve such features.
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an
|
||||
alternative solution to build text user interfaces in Rust.
|
||||
We keep a [CHANGELOG](./CHANGELOG.md) generated by [git-cliff](https://github.com/orhun/git-cliff)
|
||||
utilizing [Conventional Commits](https://www.conventionalcommits.org/).
|
||||
|
||||
## Quickstart
|
||||
|
||||
The following example demonstrates the minimal amount of code necessary to setup a terminal and
|
||||
render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
[hello_world.rs](./examples/hello_world.rs). For more guidance on how to create Ratatui apps, see
|
||||
the [Docs](https://docs.rs/ratatui) and [Examples](#examples). There is also a starter template
|
||||
available at [rust-tui-template](https://github.com/ratatui-org/rust-tui-template).
|
||||
|
||||
```rust
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut terminal = setup_terminal()?;
|
||||
run(&mut terminal)?;
|
||||
restore_terminal(&mut terminal)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>, Box<dyn Error>> {
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode()?;
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
Ok(Terminal::new(CrosstermBackend::new(stdout))?)
|
||||
}
|
||||
|
||||
fn restore_terminal(
|
||||
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen,)?;
|
||||
Ok(terminal.show_cursor()?)
|
||||
}
|
||||
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), Box<dyn Error>> {
|
||||
Ok(loop {
|
||||
terminal.draw(|frame| {
|
||||
let greeting = Paragraph::new("Hello World!");
|
||||
frame.render_widget(greeting, frame.size());
|
||||
})?;
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if KeyCode::Char('q') == key.code {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Status of this fork
|
||||
|
||||
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
|
||||
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
|
||||
the community forked the project and created this crate. We look forward to continuing the work
|
||||
started by Florian 🚀
|
||||
|
||||
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
|
||||
feel free to join and come chat! There are also plans to implement a [Matrix](https://matrix.org/)
|
||||
bridge in the near future. **Discord is not a MUST to contribute**. We follow a pretty standard
|
||||
github centered open source workflow keeping the most important conversations on GitHub, open an
|
||||
issue or PR and it will be addressed. 😄
|
||||
|
||||
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
|
||||
you are interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
## Rust version requirements
|
||||
|
||||
Since version 0.21.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.65.0.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation can be found on [docs.rs.](https://docs.rs/ratatui)
|
||||
|
||||
## Examples
|
||||
|
||||
The demo shown in the gif above is available on all available backends.
|
||||
|
||||
```shell
|
||||
# crossterm
|
||||
cargo run --example demo
|
||||
# termion
|
||||
cargo run --example demo --no-default-features --features=termion
|
||||
# termwiz
|
||||
cargo run --example demo --no-default-features --features=termwiz
|
||||
```
|
||||
|
||||
The UI code for this is in [examples/demo/ui.rs](./examples/demo/ui.rs) while the application state
|
||||
is in [examples/demo/app.rs](./examples/demo/app.rs).
|
||||
|
||||
If the user interface contains glyphs that are not displayed correctly by your terminal, you may
|
||||
want to run the demo without those symbols:
|
||||
|
||||
```shell
|
||||
cargo run --example demo --release -- --tick-rate 200 --enhanced-graphics false
|
||||
```
|
||||
|
||||
More examples are available in the [examples](./examples/) folder.
|
||||
|
||||
## Widgets
|
||||
|
||||
### Built in
|
||||
|
||||
The library comes with the following
|
||||
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
|
||||
|
||||
* [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
* [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
* [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
* [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
|
||||
rendering [points, lines, shapes and a world
|
||||
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
|
||||
* [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
|
||||
* [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
|
||||
* [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
|
||||
* [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
|
||||
* [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
* [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
|
||||
* [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
* [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
|
||||
* [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
|
||||
|
||||
Each widget has an associated example which can be found in the [examples](./examples/) folder. Run
|
||||
each examples with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by
|
||||
pressing `q`.
|
||||
|
||||
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
|
||||
be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
* [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
|
||||
`tui::text::Text`
|
||||
* [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`tui::style::Color`
|
||||
* [rust-tui-template](https://github.com/ratatui-org/rust-tui-template) — A template for bootstrapping a
|
||||
Rust TUI application with Tui-rs & crossterm
|
||||
* [simple-tui-rs](https://github.com/pmsanford/simple-tui-rs) — A simple example tui-rs app
|
||||
* [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
|
||||
Tui-rs + Crossterm apps
|
||||
* [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
|
||||
* [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
|
||||
* [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
|
||||
* [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
|
||||
with a React/Elm inspired approach
|
||||
* [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
|
||||
Tui-realm
|
||||
* [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget): Widget for tree data
|
||||
structures.
|
||||
* [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
|
||||
windows and their rendering
|
||||
* [tui-textarea](https://github.com/rhysd/tui-textarea): Simple yet powerful multi-line text editor
|
||||
widget supporting several key shortcuts, undo/redo, text search, etc.
|
||||
* [tui-input](https://github.com/sayanarijit/tui-input): TUI input library supporting multiple
|
||||
backends and tui-rs.
|
||||
* [tui-term](https://github.com/a-kenji/tui-term): A pseudoterminal widget library
|
||||
that enables the rendering of terminal applications as ratatui widgets.
|
||||
|
||||
## Apps
|
||||
|
||||
Check out the list of more than 50 [Apps using
|
||||
`Ratatui`](https://github.com/ratatui-org/ratatui/wiki/Apps-using-Ratatui)!
|
||||
|
||||
## Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
|
||||
to build text user interfaces in Rust.
|
||||
|
||||
## Contributors
|
||||
|
||||
[](https://github.com/ratatui-org/ratatui/graphs/contributors)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
|
||||
awesome logo** for the ratatui project and ratatui-org organization.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
[MIT](./LICENSE)
|
||||
|
||||
30
RELEASE.md
Normal file
30
RELEASE.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Creating a Release
|
||||
|
||||
[crates.io](https://crates.io/crates/ratatui) releases are automated via [GitHub
|
||||
actions](.github/workflows/cd.yml) and triggered by pushing a tag.
|
||||
|
||||
1. Record a new demo gif. The preferred tool for this is [ttyrec](http://0xcc.net/ttyrec/) and
|
||||
[ttygif](https://github.com/icholy/ttygif). [Asciinema](https://asciinema.org/) handles block
|
||||
character height poorly, [termanilizer](https://www.terminalizer.com/) takes forever to render,
|
||||
[vhs](https://github.com/charmbracelet/vhs) handles braille
|
||||
characters poorly (though if <https://github.com/charmbracelet/vhs/issues/322> is fixed, then
|
||||
it's probably the best option).
|
||||
|
||||
```shell
|
||||
cargo build --example demo
|
||||
ttyrec -e 'cargo --quiet run --release --example demo -- --tick-rate 100' demo.rec
|
||||
ttygif demo.rec
|
||||
```
|
||||
|
||||
Then upload it somewhere (e.g. use `vhs publish tty.gif` to publish it or upload it to a GitHub
|
||||
wiki page as an attachment). Avoid adding the gif to the git repo as binary files tend to bloat
|
||||
repositories.
|
||||
|
||||
1. Bump the version in [Cargo.toml](Cargo.toml).
|
||||
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
|
||||
can be used for generating the entries.
|
||||
1. Commit and push the changes.
|
||||
1. Create a new tag: `git tag -a v[X.Y.Z]`
|
||||
1. Push the tag: `git push --tags`
|
||||
1. Wait for [Continuous Deployment](https://github.com/ratatui-org/ratatui/actions) workflow to
|
||||
finish.
|
||||
100
bacon.toml
Normal file
100
bacon.toml
Normal file
@@ -0,0 +1,100 @@
|
||||
# This is a configuration file for the bacon tool
|
||||
#
|
||||
# Bacon repository: https://github.com/Canop/bacon
|
||||
# Complete help on configuration: https://dystroy.org/bacon/config/
|
||||
# You can also check bacon's own bacon.toml file
|
||||
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
|
||||
|
||||
default_job = "check"
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "check", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-crossterm]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
"--all-targets",
|
||||
"--color", "always",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.test]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
need_stdout = false
|
||||
|
||||
# If the doc compiles, then it opens in your browser and bacon switches
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
"--open",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
need_stdout = false
|
||||
on_success = "job:doc" # so that we don't open the browser at each change
|
||||
|
||||
[jobs.coverage]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
[jobs.coverage-unit-tests-only]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
# a project, for example a shortcut to launch a specific job.
|
||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
||||
# should go in your personal global prefs.toml file instead.
|
||||
[keybindings]
|
||||
# alt-m = "job:my-job"
|
||||
ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
v = "job:coverage"
|
||||
u = "job:coverage-unit-tests-only"
|
||||
89
benches/paragraph.rs
Normal file
89
benches/paragraph.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{Paragraph, Widget, Wrap},
|
||||
};
|
||||
|
||||
/// because the scroll offset is a u16, the maximum number of lines that can be scrolled is 65535.
|
||||
/// This is a limitation of the current implementation and may be fixed by changing the type of the
|
||||
/// scroll offset to a u32.
|
||||
const MAX_SCROLL_OFFSET: u16 = u16::MAX;
|
||||
const NO_WRAP_WIDTH: u16 = 200;
|
||||
const WRAP_WIDTH: u16 = 100;
|
||||
|
||||
/// Benchmark for rendering a paragraph with a given number of lines. The design of this benchmark
|
||||
/// allows comparison of the performance of rendering a paragraph with different numbers of lines.
|
||||
/// as well as comparing with the various settings on the scroll and wrap features.
|
||||
pub fn paragraph(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("paragraph");
|
||||
for &line_count in [64, 2048, MAX_SCROLL_OFFSET].iter() {
|
||||
let lines = random_lines(line_count);
|
||||
let lines = lines.as_str();
|
||||
|
||||
// benchmark that measures the overhead of creating a paragraph separately from rendering
|
||||
group.bench_with_input(BenchmarkId::new("new", line_count), lines, |b, lines| {
|
||||
b.iter(|| Paragraph::new(black_box(lines)))
|
||||
});
|
||||
|
||||
// render the paragraph with no scroll
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render", line_count),
|
||||
&Paragraph::new(lines),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by half the number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_half", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count / 2)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by the full number of lines and render
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_scroll_full", line_count),
|
||||
&Paragraph::new(lines).scroll((0u16, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, NO_WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// render the paragraph wrapped to 100 characters
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_wrap", line_count),
|
||||
&Paragraph::new(lines).wrap(Wrap { trim: false }),
|
||||
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
|
||||
);
|
||||
|
||||
// scroll the paragraph by the full number of lines and render wrapped to 100 characters
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("render_wrap_scroll_full", line_count),
|
||||
&Paragraph::new(lines)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((0u16, line_count)),
|
||||
|bencher, paragraph| render(bencher, paragraph, WRAP_WIDTH),
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
/// render the paragraph into a buffer with the given width
|
||||
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
|
||||
bencher.iter(|| {
|
||||
paragraph.clone().render(buffer.area, &mut buffer);
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a string with the given number of lines filled with nonsense words
|
||||
///
|
||||
/// English language has about 5.1 average characters per word so including the space between words
|
||||
/// this should emit around 200 characters per paragraph on average.
|
||||
fn random_lines(count: u16) -> String {
|
||||
let count = count as i64;
|
||||
let sentence_count = 3;
|
||||
let word_count = 11;
|
||||
fakeit::words::paragraph(count, sentence_count, word_count, "\n".into())
|
||||
}
|
||||
|
||||
criterion_group!(benches, paragraph);
|
||||
criterion_main!(benches);
|
||||
86
cliff.toml
Normal file
86
cliff.toml
Normal file
@@ -0,0 +1,86 @@
|
||||
# configuration for https://github.com/orhun/git-cliff
|
||||
|
||||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to this project will be documented in this file.\n
|
||||
"""
|
||||
# template for the changelog body
|
||||
# https://tera.netlify.app/docs/#introduction
|
||||
body = """
|
||||
{% if version %}\
|
||||
## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
- *({{commit.scope}})* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
- *(uncategorized)* {{ commit.message | upper_first }}{% if commit.breaking %} [**breaking**]{% endif %}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
# remove the leading and trailing whitespace from the template
|
||||
trim = true
|
||||
# changelog footer
|
||||
footer = """
|
||||
<!-- generated by git-cliff -->
|
||||
"""
|
||||
|
||||
[git]
|
||||
# parse the commits based on https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# filter out the commits that are not conventional
|
||||
filter_unconventional = true
|
||||
# process each line of a commit as an individual commit
|
||||
split_commits = false
|
||||
# regex for preprocessing the commit messages
|
||||
commit_preprocessors = [
|
||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/ratatui-org/ratatui/issues/${2}))" },
|
||||
{ pattern = '(better safe shared layout cache)', replace = "perf(layout): ${1}" },
|
||||
{ pattern = '(Clarify README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(Update README.md)', replace = "docs(readme): ${1}" },
|
||||
{ pattern = '(fix typos|Fix typos)', replace = "fix: ${1}" },
|
||||
]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 00 -->Features" },
|
||||
{ message = "^[fF]ix", group = "<!-- 01 -->Bug Fixes" },
|
||||
{ message = "^refactor", group = "<!-- 02 -->Refactor" },
|
||||
{ message = "^doc", group = "<!-- 03 -->Documentation" },
|
||||
{ message = "^perf", group = "<!-- 04 -->Performance" },
|
||||
{ message = "^style", group = "<!-- 05 -->Styling" },
|
||||
{ message = "^test", group = "<!-- 06 -->Testing" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 08 -->Security" },
|
||||
{ message = "^build", group = "<!-- 09 -->Build" },
|
||||
{ message = "^ci", group = "<!-- 10 -->Continuous Integration" },
|
||||
{ message = "^revert", group = "<!-- 11 -->Reverted Commits" },
|
||||
]
|
||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||
protect_breaking_commits = false
|
||||
# filter out the commits that are not matched by commit parsers
|
||||
filter_commits = false
|
||||
# glob pattern for matching git tags
|
||||
tag_pattern = "v[0-9]*"
|
||||
# regex for skipping tags
|
||||
skip_tags = "v0.1.0-rc.1"
|
||||
# regex for ignoring tags
|
||||
ignore_tags = ""
|
||||
# sort the tags topologically
|
||||
topo_order = false
|
||||
# sort the commits inside sections by oldest/newest order
|
||||
sort_commits = "newest"
|
||||
30
committed.toml
Normal file
30
committed.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
# configuration for https://github.com/crate-ci/committed
|
||||
|
||||
# https://www.conventionalcommits.org
|
||||
style = "conventional"
|
||||
# disallow merge commits
|
||||
merge_commit = false
|
||||
# subject is not required to be capitalized
|
||||
subject_capitalized = false
|
||||
# subject should start with an imperative verb
|
||||
imperative_subject = true
|
||||
# subject should not end with a punctuation
|
||||
subject_not_punctuated = true
|
||||
# disable line length
|
||||
line_length = 0
|
||||
# disable subject length
|
||||
subject_length = 0
|
||||
# default allowed_types [ "chore", "docs", "feat", "fix", "perf", "refactor", "style", "test" ]
|
||||
allowed_types = [
|
||||
"build",
|
||||
"chore",
|
||||
"ci",
|
||||
"docs",
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"refactor",
|
||||
"revert",
|
||||
"style",
|
||||
"test",
|
||||
]
|
||||
28
deny.toml
Normal file
28
deny.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
# configuration for https://github.com/EmbarkStudios/cargo-deny
|
||||
|
||||
[licenses]
|
||||
default = "deny"
|
||||
unlicensed = "deny"
|
||||
copyleft = "deny"
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
]
|
||||
|
||||
[advisories]
|
||||
unmaintained = "deny"
|
||||
yanked = "deny"
|
||||
|
||||
[bans]
|
||||
multiple-versions = "allow"
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "warn"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
BIN
docs/demo.gif
BIN
docs/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
209
examples/README.md
Normal file
209
examples/README.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# Examples
|
||||
|
||||
These gifs were created using [Charm VHS](https://github.com/charmbracelet/vhs).
|
||||
|
||||
VHS has a problem rendering some background color transitions, which shows up in several examples
|
||||
below. See <https://github.com/charmbracelet/vhs/issues/344> for more info. These problems don't
|
||||
occur in a terminal.
|
||||
|
||||
## Barchart ([barchart.rs](./barchart.rs)
|
||||
|
||||
```shell
|
||||
cargo run --example=barchart --features=crossterm
|
||||
```
|
||||
|
||||
![Barchart][barchart.gif]
|
||||
|
||||
## Block ([block.rs](./block.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=block --features=crossterm
|
||||
```
|
||||
|
||||
![Block][block.gif]
|
||||
|
||||
## Calendar ([calendar.rs](./calendar.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=calendar --features=crossterm widget-calendar
|
||||
```
|
||||
|
||||
![Calendar][calendar.gif]
|
||||
|
||||
## Canvas ([canvas.rs](./canvas.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=canvas --features=crossterm
|
||||
```
|
||||
|
||||
![Canvas][canvas.gif]
|
||||
|
||||
## Chart ([chart.rs](./chart.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=chart --features=crossterm
|
||||
```
|
||||
|
||||
![Chart][chart.gif]
|
||||
|
||||
## Custom Widget ([custom_widget.rs](./custom_widget.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=custom_widget --features=crossterm
|
||||
```
|
||||
|
||||
This is not a particularly exciting example visually, but it demonstrates how to implement your own widget.
|
||||
|
||||
![Custom Widget][custom_widget.gif]
|
||||
|
||||
## Gauge ([gauge.rs](./gauge.rs))
|
||||
|
||||
Please note: the background renders poorly when we generate this example using VHS.
|
||||
This problem doesn't generally happen during normal rendering in a terminal.
|
||||
See <https://github.com/charmbracelet/vhs/issues/344> for more details
|
||||
|
||||
```shell
|
||||
cargo run --example=gauge --features=crossterm
|
||||
```
|
||||
|
||||
![Gauge][gauge.gif]
|
||||
|
||||
## Hello World ([hello_world.rs](./hello_world.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=hello_world --features=crossterm
|
||||
```
|
||||
|
||||
This is a pretty boring example, but it contains some good comments of documentation on some of the
|
||||
standard approaches to writing tui apps.
|
||||
|
||||
![Hello World][hello_world.gif]
|
||||
|
||||
## Inline ([inline.rs](./inline.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=inline --features=crossterm
|
||||
```
|
||||
|
||||
![Inline][inline.gif]
|
||||
|
||||
## Layout ([layout.rs](./layout.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=layout --features=crossterm
|
||||
```
|
||||
|
||||
![Layout][layout.gif]
|
||||
|
||||
## List ([list.rs](./list.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=list --features=crossterm
|
||||
```
|
||||
|
||||
![List][list.gif]
|
||||
|
||||
## Panic ([panic.rs](./panic.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=panic --features=crossterm
|
||||
```
|
||||
|
||||
![Panic][panic.gif]
|
||||
|
||||
## Paragraph ([paragraph.rs](./paragraph.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=paragraph --features=crossterm
|
||||
```
|
||||
|
||||
![Paragraph][paragraph.gif]
|
||||
|
||||
## Popup ([popup.rs](./popup.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=popup --features=crossterm
|
||||
```
|
||||
|
||||
Please note: the background renders poorly when we generate this example using VHS.
|
||||
This problem doesn't generally happen during normal rendering in a terminal.
|
||||
See <https://github.com/charmbracelet/vhs/issues/344> for more details
|
||||
|
||||
![Popup][popup.gif]
|
||||
|
||||
## Scrollbar ([scrollbar.rs](./scrollbar.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=scrollbar --features=crossterm
|
||||
```
|
||||
|
||||
![Scrollbar][scrollbar.gif]
|
||||
|
||||
## Sparkline ([sparkline.rs](./sparkline.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=sparkline --features=crossterm
|
||||
```
|
||||
|
||||
![Sparkline][sparkline.gif]
|
||||
|
||||
## Table ([table.rs](./table.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=table --features=crossterm
|
||||
```
|
||||
|
||||
![Table][table.gif]
|
||||
|
||||
## Tabs ([tabs.rs](./tabs.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=tabs --features=crossterm
|
||||
```
|
||||
|
||||
![Tabs][tabs.gif]
|
||||
|
||||
## User Input ([user_input.rs](./user_input.rs))
|
||||
|
||||
```shell
|
||||
cargo run --example=user_input --features=crossterm
|
||||
```
|
||||
|
||||
![User Input][user_input.gif]
|
||||
|
||||
<!--
|
||||
links to images to make it easier to update in bulk
|
||||
These are generated with `vhs publish examples/xxx.gif`
|
||||
|
||||
To update these examples in bulk:
|
||||
```shell
|
||||
# build to ensure that running the examples doesn't have to wait so long
|
||||
cargo build --examples --features=crossterm,all-widgets
|
||||
for i in examples/*.tape
|
||||
do
|
||||
echo -n "[${i:s:examples/:::s:.tape:.gif:}]: "
|
||||
vhs $i --publish --quiet
|
||||
# may need to adjust this depending on if you see rate limiting from VHS
|
||||
sleep 1
|
||||
done
|
||||
```
|
||||
-->
|
||||
[barchart.gif]: https://vhs.charm.sh/vhs-6ioxdeRBVkVpyXcjIEVaJU.gif
|
||||
[block.gif]: https://vhs.charm.sh/vhs-1sEo9vVkHRwFtu95MOXrTj.gif
|
||||
[calendar.gif]: https://vhs.charm.sh/vhs-1dBcpMSSP80WkBgm4lBhNo.gif
|
||||
[canvas.gif]: https://vhs.charm.sh/vhs-4zeWEPF6bLEFSHuJrvaHlN.gif
|
||||
[chart.gif]: https://vhs.charm.sh/vhs-zRzsE2AwRixQhcWMTAeF1.gif
|
||||
[custom_widget.gif]: https://vhs.charm.sh/vhs-32mW1TpkrovTcm79QXmBSu.gif
|
||||
[gauge.gif]: https://vhs.charm.sh/vhs-2rvSeP5r4lRkGTzNCKpm9a.gif
|
||||
[hello_world.gif]: https://vhs.charm.sh/vhs-3CKUwxFuQi8oKQMS5zkPfQ.gif
|
||||
[inline.gif]: https://vhs.charm.sh/vhs-miRl1mosKFoJV7LjjvF4T.gif
|
||||
[layout.gif]: https://vhs.charm.sh/vhs-5R8O3LQGQ5pQVWwlPVrdbQ.gif
|
||||
[list.gif]: https://vhs.charm.sh/vhs-4goo9reeUM9r0nYb54R7SP.gif
|
||||
[panic.gif]: https://vhs.charm.sh/vhs-HrvKCHV4yeN69fb1EadTH.gif
|
||||
[paragraph.gif]: https://vhs.charm.sh/vhs-2qIPDi79DUmtmeNDEeHVEF.gif
|
||||
[popup.gif]: https://vhs.charm.sh/vhs-2QnC682AUeNYNXcjNlKTyp.gif
|
||||
[scrollbar.gif]: https://vhs.charm.sh/vhs-2p13MMFreW7Gwt1xIonIWu.gif
|
||||
[sparkline.gif]: https://vhs.charm.sh/vhs-4t59Vxw5Za33Rtvt9QrftA.gif
|
||||
[table.gif]: https://vhs.charm.sh/vhs-6IrGHgT385DqA6xnwGF9oD.gif
|
||||
[tabs.gif]: https://vhs.charm.sh/vhs-61WkbfhyDk0kbkjncErdHT.gif
|
||||
[user_input.gif]: https://vhs.charm.sh/vhs-4fxUgkpEWcVyBRXuyYKODY.gif
|
||||
@@ -1,29 +1,33 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{BarChart, Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
struct Company<'a> {
|
||||
revenue: [u64; 4],
|
||||
label: &'a str,
|
||||
bar_style: Style,
|
||||
}
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
data: Vec<(&'a str, u64)>,
|
||||
months: [&'a str; 4],
|
||||
companies: [Company<'a>; 3],
|
||||
}
|
||||
|
||||
const TOTAL_REVENUE: &str = "Total Revenue";
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
data: vec![
|
||||
("B1", 9),
|
||||
("B2", 12),
|
||||
@@ -50,115 +54,197 @@ impl<'a> App<'a> {
|
||||
("B23", 3),
|
||||
("B24", 5),
|
||||
],
|
||||
companies: [
|
||||
Company {
|
||||
label: "Comp.A",
|
||||
revenue: [9500, 12500, 5300, 8500],
|
||||
bar_style: Style::default().fg(Color::Green),
|
||||
},
|
||||
Company {
|
||||
label: "Comp.B",
|
||||
revenue: [1500, 2500, 3000, 4100],
|
||||
bar_style: Style::default().fg(Color::Yellow),
|
||||
},
|
||||
Company {
|
||||
label: "Comp.C",
|
||||
revenue: [10500, 10600, 9000, 4200],
|
||||
bar_style: Style::default().fg(Color::White),
|
||||
},
|
||||
],
|
||||
months: ["Mars", "Apr", "May", "Jun"],
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.data.pop().unwrap();
|
||||
self.data.insert(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(app.size);
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow))
|
||||
.render(&mut f, chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data2").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(5)
|
||||
.bar_gap(3)
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.value_style(Style::default().bg(Color::Green).modifier(Modifier::Bold))
|
||||
.render(&mut f, chunks[0]);
|
||||
BarChart::default()
|
||||
.block(Block::default().title("Data3").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.bar_width(7)
|
||||
.bar_gap(0)
|
||||
.value_style(Style::default().bg(Color::Red))
|
||||
.label_style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic))
|
||||
.render(&mut f, chunks[1]);
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.data(&app.data)
|
||||
.bar_width(9)
|
||||
.bar_style(Style::default().fg(Color::Yellow))
|
||||
.value_style(Style::default().fg(Color::Black).bg(Color::Yellow));
|
||||
f.render_widget(barchart, chunks[0]);
|
||||
|
||||
draw_bar_with_group_labels(f, app, chunks[1], false);
|
||||
draw_bar_with_group_labels(f, app, chunks[2], true);
|
||||
}
|
||||
|
||||
fn draw_bar_with_group_labels<B>(f: &mut Frame<B>, app: &App, area: Rect, bar_labels: bool)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let groups: Vec<BarGroup> = app
|
||||
.months
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &month)| {
|
||||
let bars: Vec<Bar> = app
|
||||
.companies
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let mut bar = Bar::default()
|
||||
.value(c.revenue[i])
|
||||
.style(c.bar_style)
|
||||
.value_style(
|
||||
Style::default()
|
||||
.bg(c.bar_style.fg.unwrap())
|
||||
.fg(Color::Black),
|
||||
)
|
||||
.text_value(format!("{:.1}", (c.revenue[i] as f64) / 1000.));
|
||||
if bar_labels {
|
||||
bar = bar.label(c.label.into());
|
||||
}
|
||||
bar
|
||||
})
|
||||
.collect();
|
||||
BarGroup::default().label(month.into()).bars(&bars)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::default().title("Data1").borders(Borders::ALL))
|
||||
.bar_width(7)
|
||||
.group_gap(3);
|
||||
|
||||
for group in groups {
|
||||
barchart = barchart.data(group)
|
||||
}
|
||||
|
||||
f.render_widget(barchart, area);
|
||||
|
||||
const LEGEND_HEIGHT: u16 = 6;
|
||||
if area.height >= LEGEND_HEIGHT && area.width >= TOTAL_REVENUE.len() as u16 + 2 {
|
||||
let legend_area = Rect {
|
||||
height: LEGEND_HEIGHT,
|
||||
width: TOTAL_REVENUE.len() as u16 + 2,
|
||||
y: area.y,
|
||||
x: area.x,
|
||||
};
|
||||
draw_legend(f, legend_area);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_legend<B>(f: &mut Frame<B>, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let text = vec![
|
||||
Line::from(Span::styled(
|
||||
TOTAL_REVENUE,
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::White),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company A",
|
||||
Style::default().fg(Color::Green),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
"- Company B",
|
||||
Style::default().fg(Color::Yellow),
|
||||
)),
|
||||
Line::from(vec![Span::styled(
|
||||
"- Company C",
|
||||
Style::default().fg(Color::White),
|
||||
)]),
|
||||
];
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::White));
|
||||
let paragraph = Paragraph::new(text).block(block);
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
11
examples/barchart.tape
Normal file
11
examples/barchart.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
|
||||
Output "target/barchart.gif"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=barchart"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,84 +1,123 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{error::Error, io, time::Duration};
|
||||
|
||||
use std::io;
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
|
||||
let stdin = io::stdin();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
let mut term_size = terminal.size().unwrap();
|
||||
draw(&mut terminal, term_size).unwrap();
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if term_size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
term_size = size;
|
||||
}
|
||||
draw(&mut terminal, term_size).unwrap();
|
||||
let evt = c.unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.clear()?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if event::poll(Duration::from_millis(250))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, size: Rect) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
// Wrapping block for a group
|
||||
// Just draw the block and the group on the same area and build the group
|
||||
// with at least a margin of 1
|
||||
Block::default().borders(Borders::ALL).render(&mut f, size);
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(4)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(size);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
Block::default()
|
||||
.title("With background")
|
||||
.title_style(Style::default().fg(Color::Yellow))
|
||||
.style(Style::default().bg(Color::Green))
|
||||
.render(&mut f, chunks[0]);
|
||||
Block::default()
|
||||
.title("Styled title")
|
||||
.title_style(
|
||||
Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.modifier(Modifier::Bold),
|
||||
)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[1]);
|
||||
Block::default()
|
||||
.title("With borders")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[0]);
|
||||
Block::default()
|
||||
.title("With styled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
})
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
// Wrapping block for a group
|
||||
// Just draw the block and the group on the same area and build the group
|
||||
let outer = f.size();
|
||||
let outer_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(block::Title::from("Main block with round corners").alignment(Alignment::Center))
|
||||
.border_type(BorderType::Rounded);
|
||||
let inner = outer_block.inner(outer);
|
||||
let [top, bottom] = *Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(inner)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let [top_left, top_right] = *Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(top)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let [bottom_left, bottom_right] = *Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(bottom)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let top_left_block = Block::default()
|
||||
.title("With Green Background")
|
||||
.borders(Borders::all())
|
||||
.on_green();
|
||||
let top_right_block = Block::default()
|
||||
.title(
|
||||
block::Title::from("With styled title".white().on_red().bold())
|
||||
.alignment(Alignment::Right),
|
||||
)
|
||||
.borders(Borders::ALL);
|
||||
let bottom_left_block = Paragraph::new("Text inside padded block").block(
|
||||
Block::default()
|
||||
.title("With borders")
|
||||
.borders(Borders::ALL)
|
||||
.padding(Padding {
|
||||
left: 4,
|
||||
right: 4,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
}),
|
||||
);
|
||||
let bottom_right_block = Block::default()
|
||||
.title("With styled borders and doubled borders")
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.border_type(BorderType::Double)
|
||||
.padding(Padding::uniform(1));
|
||||
let bottom_inner_block = Block::default()
|
||||
.title("Block inside padded block")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
f.render_widget(outer_block, outer);
|
||||
f.render_widget(Clear, top_left);
|
||||
f.render_widget(top_left_block, top_left);
|
||||
f.render_widget(top_right_block, top_right);
|
||||
f.render_widget(bottom_left_block, bottom_left);
|
||||
let bottom_right_inner = bottom_right_block.inner(bottom_right);
|
||||
f.render_widget(bottom_right_block, bottom_right);
|
||||
f.render_widget(bottom_inner_block, bottom_right_inner);
|
||||
}
|
||||
|
||||
11
examples/block.tape
Normal file
11
examples/block.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/block.tape`
|
||||
Output "target/block.gif"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=block"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
277
examples/calendar.rs
Normal file
277
examples/calendar.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
use std::{error::Error, io, rc::Rc};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::calendar::*};
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
loop {
|
||||
let _ = terminal.draw(|f| draw(f));
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
#[allow(clippy::single_match)]
|
||||
match key.code {
|
||||
KeyCode::Char(_) => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(f: &mut Frame<B>) {
|
||||
let app_area = f.size();
|
||||
|
||||
let calarea = Rect {
|
||||
x: app_area.x + 1,
|
||||
y: app_area.y + 1,
|
||||
height: app_area.height - 1,
|
||||
width: app_area.width - 1,
|
||||
};
|
||||
|
||||
let mut start = OffsetDateTime::now_local()
|
||||
.unwrap()
|
||||
.date()
|
||||
.replace_month(Month::January)
|
||||
.unwrap()
|
||||
.replace_day(1)
|
||||
.unwrap();
|
||||
|
||||
let list = make_dates(start.year());
|
||||
|
||||
for chunk in split_rows(&calarea)
|
||||
.iter()
|
||||
.flat_map(|row| split_cols(row).to_vec())
|
||||
{
|
||||
let cal = cals::get_cal(start.month(), start.year(), &list);
|
||||
f.render_widget(cal, chunk);
|
||||
start = start.replace_month(start.month().next()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn split_rows(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
Constraint::Percentage(33),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn split_cols(area: &Rect) -> Rc<[Rect]> {
|
||||
let list_layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(0)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
list_layout.split(*area)
|
||||
}
|
||||
|
||||
fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
let mut list = CalendarEventStore::today(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
|
||||
// Holidays
|
||||
let holiday_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
|
||||
// new year's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// next new_year's for December "show surrounding"
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year + 1, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// groundhog day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::February, 2).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// april fool's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// earth day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 22).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// star wars day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::May, 4).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// festivus
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 23).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// new year's eve
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 31).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
|
||||
// seasons
|
||||
let season_style = Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
// spring equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::March, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// summer solstice
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::June, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// fall equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::September, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list
|
||||
}
|
||||
|
||||
mod cals {
|
||||
use super::*;
|
||||
|
||||
pub(super) fn get_cal<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
use Month::*;
|
||||
match m {
|
||||
May => example1(m, y, es),
|
||||
June => example2(m, y, es),
|
||||
July => example3(m, y, es),
|
||||
December => example3(m, y, es),
|
||||
February => example4(m, y, es),
|
||||
November => example5(m, y, es),
|
||||
_ => default(m, y, es),
|
||||
}
|
||||
}
|
||||
|
||||
fn default<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(Style::default())
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example1<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(default_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example2<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::DIM)
|
||||
.fg(Color::LightYellow);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example3<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(Style::default().add_modifier(Modifier::DIM))
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example4<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example5<'a, S: DateStyler>(m: Month, y: i32, es: S) -> Monthly<'a, S> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
}
|
||||
11
examples/calendar.tape
Normal file
11
examples/calendar.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/calendar.tape`
|
||||
Output "target/calendar.gif"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=calendar --features=crossterm,widget-calendar"
|
||||
Enter
|
||||
Sleep 3s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,54 +1,72 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::Color;
|
||||
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
x: f64,
|
||||
y: f64,
|
||||
ball: Rect,
|
||||
ball: Rectangle,
|
||||
playground: Rect,
|
||||
vx: u16,
|
||||
vy: u16,
|
||||
vx: f64,
|
||||
vy: f64,
|
||||
dir_x: bool,
|
||||
dir_y: bool,
|
||||
tick_count: u64,
|
||||
marker: Marker,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
size: Default::default(),
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Rect::new(10, 30, 10, 10),
|
||||
ball: Rectangle {
|
||||
x: 10.0,
|
||||
y: 30.0,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
color: Color::Yellow,
|
||||
},
|
||||
playground: Rect::new(10, 10, 100, 100),
|
||||
vx: 1,
|
||||
vy: 1,
|
||||
vx: 1.0,
|
||||
vy: 1.0,
|
||||
dir_x: true,
|
||||
dir_y: true,
|
||||
tick_count: 0,
|
||||
marker: Marker::Dot,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
if self.ball.left() < self.playground.left() || self.ball.right() > self.playground.right()
|
||||
fn on_tick(&mut self) {
|
||||
self.tick_count += 1;
|
||||
// only change marker every 4 ticks (1s) to avoid stroboscopic effect
|
||||
if (self.tick_count % 4) == 0 {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Block,
|
||||
Marker::Block => Marker::Bar,
|
||||
Marker::Bar => Marker::Braille,
|
||||
Marker::Braille => Marker::Dot,
|
||||
};
|
||||
}
|
||||
if self.ball.x < self.playground.left() as f64
|
||||
|| self.ball.x + self.ball.width > self.playground.right() as f64
|
||||
{
|
||||
self.dir_x = !self.dir_x;
|
||||
}
|
||||
if self.ball.top() < self.playground.top() || self.ball.bottom() > self.playground.bottom()
|
||||
if self.ball.y < self.playground.top() as f64
|
||||
|| self.ball.y + self.ball.height > self.playground.bottom() as f64
|
||||
{
|
||||
self.dir_y = !self.dir_y;
|
||||
}
|
||||
@@ -67,138 +85,102 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
event::Key::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
event::Key::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
event::Key::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(app.size);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(app.x, -app.y, "You are here", Color::Yellow);
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.render(&mut f, chunks[0]);
|
||||
Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.left()),
|
||||
y1: f64::from(app.ball.top()),
|
||||
x2: f64::from(app.ball.right()),
|
||||
y2: f64::from(app.ball.top()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.right()),
|
||||
y1: f64::from(app.ball.top()),
|
||||
x2: f64::from(app.ball.right()),
|
||||
y2: f64::from(app.ball.bottom()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.right()),
|
||||
y1: f64::from(app.ball.bottom()),
|
||||
x2: f64::from(app.ball.left()),
|
||||
y2: f64::from(app.ball.bottom()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Line {
|
||||
x1: f64::from(app.ball.left()),
|
||||
y1: f64::from(app.ball.bottom()),
|
||||
x2: f64::from(app.ball.left()),
|
||||
y2: f64::from(app.ball.top()),
|
||||
color: Color::Yellow,
|
||||
});
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0])
|
||||
.render(&mut f, chunks[1]);
|
||||
})
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
KeyCode::Down => {
|
||||
app.y += 1.0;
|
||||
}
|
||||
KeyCode::Up => {
|
||||
app.y -= 1.0;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.x += 1.0;
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.x -= 1.0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("World"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.print(app.x, -app.y, "You are here".yellow());
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(canvas, chunks[0]);
|
||||
let canvas = Canvas::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Pong"))
|
||||
.marker(app.marker)
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&app.ball);
|
||||
})
|
||||
.x_bounds([10.0, 110.0])
|
||||
.y_bounds([10.0, 110.0]);
|
||||
f.render_widget(canvas, chunks[1]);
|
||||
}
|
||||
|
||||
11
examples/canvas.tape
Normal file
11
examples/canvas.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/canvas.tape`
|
||||
Output "target/canvas.gif"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=canvas --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,25 +1,56 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
mod util;
|
||||
use util::*;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
const DATA: [(f64, f64); 5] = [(0.0, 0.0), (1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0)];
|
||||
const DATA2: [(f64, f64); 7] = [
|
||||
(0.0, 0.0),
|
||||
(10.0, 1.0),
|
||||
(20.0, 0.5),
|
||||
(30.0, 1.5),
|
||||
(40.0, 1.0),
|
||||
(50.0, 2.5),
|
||||
(60.0, 3.0),
|
||||
];
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Axis, Block, Borders, Chart, Dataset, Marker, Widget};
|
||||
use tui::Terminal;
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
signal1: SinSignal,
|
||||
data1: Vec<(f64, f64)>,
|
||||
signal2: SinSignal,
|
||||
@@ -34,7 +65,6 @@ impl App {
|
||||
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||
App {
|
||||
size: Rect::default(),
|
||||
signal1,
|
||||
data1,
|
||||
signal2,
|
||||
@@ -43,7 +73,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..5 {
|
||||
self.data1.remove(0);
|
||||
}
|
||||
@@ -57,111 +87,173 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::Bold))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds(app.window)
|
||||
.labels(&[
|
||||
&format!("{}", app.window[0]),
|
||||
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
||||
&format!("{}", app.window[1]),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(&["-20", "0", "20"]),
|
||||
)
|
||||
.datasets(&[
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
])
|
||||
.render(&mut f, app.size);
|
||||
})
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.window[0]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
|
||||
Span::styled(
|
||||
format!("{}", app.window[1]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data1),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data2),
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 1".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(x_labels)
|
||||
.bounds(app.window),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels(vec!["-20".bold(), "0".into(), "20".bold()])
|
||||
.bounds([-20.0, 20.0]),
|
||||
);
|
||||
f.render_widget(chart, chunks[0]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 2".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||
);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
|
||||
let datasets = vec![Dataset::default()
|
||||
.name("data")
|
||||
.marker(symbols::Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.graph_type(GraphType::Line)
|
||||
.data(&DATA2)];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart 3".cyan().bold())
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 50.0])
|
||||
.labels(vec!["0".bold(), "25".into(), "50".bold()]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([0.0, 5.0])
|
||||
.labels(vec!["0".bold(), "2.5".into(), "5".bold()]),
|
||||
);
|
||||
f.render_widget(chart, chunks[2]);
|
||||
}
|
||||
|
||||
11
examples/chart.tape
Normal file
11
examples/chart.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/chart.tape`
|
||||
Output "target/chart.gif"
|
||||
Set Width 1200
|
||||
Set Height 800
|
||||
Hide
|
||||
Type "cargo run --example=chart --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,50 +0,0 @@
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
|
||||
use tui::backend::CrosstermBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Paragraph, Text, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new()).unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
draw(&mut terminal).unwrap();
|
||||
loop {
|
||||
{
|
||||
let input = crossterm::input(terminal.backend().screen());
|
||||
match input.read_char() {
|
||||
Ok(c) => if c == 'q' {
|
||||
break;
|
||||
},
|
||||
Err(e) => panic!("{}", e.description()),
|
||||
};
|
||||
}
|
||||
draw(&mut terminal).unwrap();
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<CrosstermBackend>) -> io::Result<()> {
|
||||
let size = t.size()?;
|
||||
t.draw(|mut f| {
|
||||
let text = [
|
||||
Text::Data("It "),
|
||||
Text::StyledData("works", Style::default().fg(Color::Yellow)),
|
||||
];
|
||||
Paragraph::new(text.iter())
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Crossterm Backend")
|
||||
.title_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Magenta)),
|
||||
)
|
||||
.render(&mut f, size);
|
||||
})
|
||||
}
|
||||
@@ -1,42 +1,71 @@
|
||||
extern crate tui;
|
||||
use std::{error::Error, io};
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::Style;
|
||||
use tui::widgets::Widget;
|
||||
use tui::Terminal;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
text: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Default for Label<'a> {
|
||||
fn default() -> Label<'a> {
|
||||
Label { text: "" }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Label<'a> {
|
||||
fn draw(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.left(), area.top(), self.text, Style::default());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Label<'a> {
|
||||
fn text(&mut self, text: &'a str) -> &mut Label<'a> {
|
||||
fn text(mut self, text: &'a str) -> Label<'a> {
|
||||
self.text = text;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
|
||||
let size = terminal.size().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal
|
||||
.draw(|mut f| {
|
||||
Label::default().text("Test").render(&mut f, size);
|
||||
})
|
||||
.unwrap();
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
let size = f.size();
|
||||
let label = Label::default().text("Test");
|
||||
f.render_widget(label, size);
|
||||
}
|
||||
|
||||
11
examples/custom_widget.tape
Normal file
11
examples/custom_widget.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/custom_widget.tape`
|
||||
Output "target/custom_widget.gif"
|
||||
Set Width 1200
|
||||
Set Height 200
|
||||
Hide
|
||||
Type "cargo run --example=custom_widget --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
522
examples/demo.rs
522
examples/demo.rs
@@ -1,522 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate stderrlog;
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
|
||||
mod util;
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
|
||||
use tui::widgets::{
|
||||
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Marker, Paragraph, Row,
|
||||
SelectableList, Sparkline, Table, Tabs, Text, Widget,
|
||||
};
|
||||
use tui::{Frame, Terminal};
|
||||
|
||||
use util::*;
|
||||
|
||||
struct Server<'a> {
|
||||
name: &'a str,
|
||||
location: &'a str,
|
||||
coords: (f64, f64),
|
||||
status: &'a str,
|
||||
}
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
items: Vec<&'a str>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
selected: usize,
|
||||
tabs: MyTabs<'a>,
|
||||
show_chart: bool,
|
||||
progress: u16,
|
||||
data: Vec<u64>,
|
||||
data2: Vec<(f64, f64)>,
|
||||
data3: Vec<(f64, f64)>,
|
||||
data4: Vec<(&'a str, u64)>,
|
||||
window: [f64; 2],
|
||||
colors: [Color; 2],
|
||||
color_index: usize,
|
||||
servers: Vec<Server<'a>>,
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
stderrlog::new()
|
||||
.module(module_path!())
|
||||
.verbosity(4)
|
||||
.init()
|
||||
.unwrap();
|
||||
info!("Start");
|
||||
|
||||
let mut rand_signal = RandomSignal::new(0, 100);
|
||||
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
|
||||
let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0);
|
||||
|
||||
let mut app = App {
|
||||
size: Rect::default(),
|
||||
items: vec![
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9",
|
||||
"Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17",
|
||||
"Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
|
||||
],
|
||||
events: vec![
|
||||
("Event1", "INFO"),
|
||||
("Event2", "INFO"),
|
||||
("Event3", "CRITICAL"),
|
||||
("Event4", "ERROR"),
|
||||
("Event5", "INFO"),
|
||||
("Event6", "INFO"),
|
||||
("Event7", "WARNING"),
|
||||
("Event8", "INFO"),
|
||||
("Event9", "INFO"),
|
||||
("Event10", "INFO"),
|
||||
("Event11", "CRITICAL"),
|
||||
("Event12", "INFO"),
|
||||
("Event13", "INFO"),
|
||||
("Event14", "INFO"),
|
||||
("Event15", "INFO"),
|
||||
("Event16", "INFO"),
|
||||
("Event17", "ERROR"),
|
||||
("Event18", "ERROR"),
|
||||
("Event19", "INFO"),
|
||||
("Event20", "INFO"),
|
||||
("Event21", "WARNING"),
|
||||
("Event22", "INFO"),
|
||||
("Event23", "INFO"),
|
||||
("Event24", "WARNING"),
|
||||
("Event25", "INFO"),
|
||||
("Event26", "INFO"),
|
||||
],
|
||||
selected: 0,
|
||||
tabs: MyTabs {
|
||||
titles: vec!["Tab0", "Tab1"],
|
||||
selection: 0,
|
||||
},
|
||||
show_chart: true,
|
||||
progress: 0,
|
||||
data: rand_signal.clone().take(300).collect(),
|
||||
data2: sin_signal.clone().take(100).collect(),
|
||||
data3: sin_signal2.clone().take(200).collect(),
|
||||
data4: vec![
|
||||
("B1", 9),
|
||||
("B2", 12),
|
||||
("B3", 5),
|
||||
("B4", 8),
|
||||
("B5", 2),
|
||||
("B6", 4),
|
||||
("B7", 5),
|
||||
("B8", 9),
|
||||
("B9", 14),
|
||||
("B10", 15),
|
||||
("B11", 1),
|
||||
("B12", 0),
|
||||
("B13", 4),
|
||||
("B14", 6),
|
||||
("B15", 4),
|
||||
("B16", 6),
|
||||
("B17", 4),
|
||||
("B18", 7),
|
||||
("B19", 13),
|
||||
("B20", 8),
|
||||
("B21", 11),
|
||||
("B22", 9),
|
||||
("B23", 3),
|
||||
("B24", 5),
|
||||
],
|
||||
window: [0.0, 20.0],
|
||||
colors: [Color::Magenta, Color::Red],
|
||||
color_index: 0,
|
||||
servers: vec![
|
||||
Server {
|
||||
name: "NorthAmerica-1",
|
||||
location: "New York City",
|
||||
coords: (40.71, -74.00),
|
||||
status: "Up",
|
||||
},
|
||||
Server {
|
||||
name: "Europe-1",
|
||||
location: "Paris",
|
||||
coords: (48.85, 2.35),
|
||||
status: "Failure",
|
||||
},
|
||||
Server {
|
||||
name: "SouthAmerica-1",
|
||||
location: "São Paulo",
|
||||
coords: (-23.54, -46.62),
|
||||
status: "Up",
|
||||
},
|
||||
Server {
|
||||
name: "Asia-1",
|
||||
location: "Singapore",
|
||||
coords: (1.35, 103.86),
|
||||
status: "Up",
|
||||
},
|
||||
],
|
||||
};
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
|
||||
for _ in 0..100 {
|
||||
sin_signal.next();
|
||||
}
|
||||
for _ in 0..200 {
|
||||
sin_signal2.next();
|
||||
}
|
||||
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
let tx = tx.clone();
|
||||
loop {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(250));
|
||||
}
|
||||
});
|
||||
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Up => {
|
||||
if app.selected > 0 {
|
||||
app.selected -= 1
|
||||
};
|
||||
}
|
||||
event::Key::Down => if app.selected < app.items.len() - 1 {
|
||||
app.selected += 1;
|
||||
},
|
||||
event::Key::Left => {
|
||||
app.tabs.previous();
|
||||
}
|
||||
event::Key::Right => {
|
||||
app.tabs.next();
|
||||
}
|
||||
event::Key::Char('t') => {
|
||||
app.show_chart = !app.show_chart;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.progress += 5;
|
||||
if app.progress > 100 {
|
||||
app.progress = 0;
|
||||
}
|
||||
app.data.insert(0, rand_signal.next().unwrap());
|
||||
app.data.pop();
|
||||
for _ in 0..5 {
|
||||
app.data2.remove(0);
|
||||
app.data2.push(sin_signal.next().unwrap());
|
||||
}
|
||||
for _ in 0..10 {
|
||||
app.data3.remove(0);
|
||||
app.data3.push(sin_signal2.next().unwrap());
|
||||
}
|
||||
let i = app.data4.pop().unwrap();
|
||||
app.data4.insert(0, i);
|
||||
app.window[0] += 1.0;
|
||||
app.window[1] += 1.0;
|
||||
let i = app.events.pop().unwrap();
|
||||
app.events.insert(0, i);
|
||||
app.color_index += 1;
|
||||
if app.color_index >= app.colors.len() {
|
||||
app.color_index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(app.size);
|
||||
Tabs::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.titles(&app.tabs.titles)
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.select(app.tabs.selection)
|
||||
.render(&mut f, chunks[0]);
|
||||
match app.tabs.selection {
|
||||
0 => {
|
||||
draw_first_tab(&mut f, app, chunks[1]);
|
||||
}
|
||||
1 => {
|
||||
draw_second_tab(&mut f, app, chunks[1]);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
fn draw_first_tab(f: &mut Frame<MouseBackend>, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(7),
|
||||
Constraint::Length(7),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
draw_gauges(f, app, chunks[0]);
|
||||
draw_charts(f, app, chunks[1]);
|
||||
draw_text(f, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_gauges(f: &mut Frame<MouseBackend>, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(2), Constraint::Length(3)].as_ref())
|
||||
.margin(1)
|
||||
.split(area);
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Graphs")
|
||||
.render(f, area);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge:"))
|
||||
.style(
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.bg(Color::Black)
|
||||
.modifier(Modifier::Italic),
|
||||
)
|
||||
.label(&format!("{} / 100", app.progress))
|
||||
.percent(app.progress)
|
||||
.render(f, chunks[0]);
|
||||
Sparkline::default()
|
||||
.block(Block::default().title("Sparkline:"))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&app.data)
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_charts(f: &mut Frame<MouseBackend>, app: &App, area: Rect) {
|
||||
let constraints = if app.show_chart {
|
||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
vec![Constraint::Percentage(100)]
|
||||
};
|
||||
let chunks = Layout::default()
|
||||
.constraints(constraints)
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(chunks[0]);
|
||||
SelectableList::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.items(&app.items)
|
||||
.select(Some(app.selected))
|
||||
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
|
||||
.highlight_symbol(">")
|
||||
.render(f, chunks[0]);
|
||||
let info_style = Style::default().fg(Color::White);
|
||||
let warning_style = Style::default().fg(Color::Yellow);
|
||||
let error_style = Style::default().fg(Color::Magenta);
|
||||
let critical_style = Style::default().fg(Color::Red);
|
||||
let events = app.events.iter().map(|&(evt, level)| {
|
||||
Text::styled(
|
||||
format!("{}: {}", level, evt),
|
||||
match level {
|
||||
"ERROR" => error_style,
|
||||
"CRITICAL" => critical_style,
|
||||
"WARNING" => warning_style,
|
||||
_ => info_style,
|
||||
},
|
||||
)
|
||||
});
|
||||
List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
|
||||
.data(&app.data4)
|
||||
.bar_width(3)
|
||||
.bar_gap(2)
|
||||
.value_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.modifier(Modifier::Italic),
|
||||
)
|
||||
.label_style(Style::default().fg(Color::Yellow))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
if app.show_chart {
|
||||
Chart::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Chart")
|
||||
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::Bold))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds(app.window)
|
||||
.labels(&[
|
||||
&format!("{}", app.window[0]),
|
||||
&format!("{}", (app.window[0] + app.window[1]) / 2.0),
|
||||
&format!("{}", app.window[1]),
|
||||
]),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.labels_style(Style::default().modifier(Modifier::Italic))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(&["-20", "0", "20"]),
|
||||
)
|
||||
.datasets(&[
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.data2),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(Marker::Braille)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.data3),
|
||||
])
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text(f: &mut Frame<MouseBackend>, area: Rect) {
|
||||
let text = [
|
||||
Text::raw("This is a paragraph with several lines. You can change style your text the way you want.\n\nFox example: "),
|
||||
Text::styled("under", Style::default().fg(Color::Red)),
|
||||
Text::raw(" "),
|
||||
Text::styled("the", Style::default().fg(Color::Green)),
|
||||
Text::raw(" "),
|
||||
Text::styled("rainbow", Style::default().fg(Color::Blue)),
|
||||
Text::raw(".\nOh and if you didn't "),
|
||||
Text::styled("notice", Style::default().modifier(Modifier::Italic)),
|
||||
Text::raw(" you can "),
|
||||
Text::styled("automatically", Style::default().modifier(Modifier::Bold)),
|
||||
Text::raw(" "),
|
||||
Text::styled("wrap", Style::default().modifier(Modifier::Invert)),
|
||||
Text::raw(" your "),
|
||||
Text::styled("text", Style::default().modifier(Modifier::Underline)),
|
||||
Text::raw(".\nOne more thing is that it should display unicode characters: 10€")
|
||||
];
|
||||
Paragraph::new(text.iter())
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Footer")
|
||||
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::Bold)),
|
||||
)
|
||||
.wrap(true)
|
||||
.render(f, area);
|
||||
}
|
||||
|
||||
fn draw_second_tab(f: &mut Frame<MouseBackend>, app: &App, area: Rect) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
let up_style = Style::default().fg(Color::Green);
|
||||
let failure_style = Style::default().fg(Color::Red);
|
||||
Table::new(
|
||||
["Server", "Location", "Status"].into_iter(),
|
||||
app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
up_style
|
||||
} else {
|
||||
failure_style
|
||||
};
|
||||
Row::StyledData(vec![s.name, s.location, s.status].into_iter(), style)
|
||||
}),
|
||||
).block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.header_style(Style::default().fg(Color::Yellow))
|
||||
.widths(&[15, 15, 10])
|
||||
.render(f, chunks[0]);
|
||||
|
||||
Canvas::default()
|
||||
.block(Block::default().title("World").borders(Borders::ALL))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.layer();
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
for s2 in &app.servers[i + 1..] {
|
||||
ctx.draw(&Line {
|
||||
x1: s1.coords.1,
|
||||
y1: s1.coords.0,
|
||||
y2: s2.coords.0,
|
||||
x2: s2.coords.1,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
}
|
||||
}
|
||||
for server in &app.servers {
|
||||
let color = if server.status == "Up" {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
ctx.print(server.coords.1, server.coords.0, "X", color);
|
||||
}
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.render(f, chunks[1]);
|
||||
}
|
||||
348
examples/demo/app.rs
Normal file
348
examples/demo/app.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::widgets::*;
|
||||
|
||||
const TASKS: [&str; 24] = [
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", "Item10",
|
||||
"Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17", "Item18", "Item19",
|
||||
"Item20", "Item21", "Item22", "Item23", "Item24",
|
||||
];
|
||||
|
||||
const LOGS: [(&str, &str); 26] = [
|
||||
("Event1", "INFO"),
|
||||
("Event2", "INFO"),
|
||||
("Event3", "CRITICAL"),
|
||||
("Event4", "ERROR"),
|
||||
("Event5", "INFO"),
|
||||
("Event6", "INFO"),
|
||||
("Event7", "WARNING"),
|
||||
("Event8", "INFO"),
|
||||
("Event9", "INFO"),
|
||||
("Event10", "INFO"),
|
||||
("Event11", "CRITICAL"),
|
||||
("Event12", "INFO"),
|
||||
("Event13", "INFO"),
|
||||
("Event14", "INFO"),
|
||||
("Event15", "INFO"),
|
||||
("Event16", "INFO"),
|
||||
("Event17", "ERROR"),
|
||||
("Event18", "ERROR"),
|
||||
("Event19", "INFO"),
|
||||
("Event20", "INFO"),
|
||||
("Event21", "WARNING"),
|
||||
("Event22", "INFO"),
|
||||
("Event23", "INFO"),
|
||||
("Event24", "WARNING"),
|
||||
("Event25", "INFO"),
|
||||
("Event26", "INFO"),
|
||||
];
|
||||
|
||||
const EVENTS: [(&str, u64); 24] = [
|
||||
("B1", 9),
|
||||
("B2", 12),
|
||||
("B3", 5),
|
||||
("B4", 8),
|
||||
("B5", 2),
|
||||
("B6", 4),
|
||||
("B7", 5),
|
||||
("B8", 9),
|
||||
("B9", 14),
|
||||
("B10", 15),
|
||||
("B11", 1),
|
||||
("B12", 0),
|
||||
("B13", 4),
|
||||
("B14", 6),
|
||||
("B15", 4),
|
||||
("B16", 6),
|
||||
("B17", 4),
|
||||
("B18", 7),
|
||||
("B19", 13),
|
||||
("B20", 8),
|
||||
("B21", 11),
|
||||
("B22", 9),
|
||||
("B23", 3),
|
||||
("B24", 5),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TabsState<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl<'a> TabsState<'a> {
|
||||
pub fn new(titles: Vec<&'a str>) -> TabsState {
|
||||
TabsState { titles, index: 0 }
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StatefulList<T> {
|
||||
pub state: ListState,
|
||||
pub items: Vec<T>,
|
||||
}
|
||||
|
||||
impl<T> StatefulList<T> {
|
||||
pub fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Signal<S: Iterator> {
|
||||
source: S,
|
||||
pub points: Vec<S::Item>,
|
||||
tick_rate: usize,
|
||||
}
|
||||
|
||||
impl<S> Signal<S>
|
||||
where
|
||||
S: Iterator,
|
||||
{
|
||||
fn on_tick(&mut self) {
|
||||
for _ in 0..self.tick_rate {
|
||||
self.points.remove(0);
|
||||
}
|
||||
self.points
|
||||
.extend(self.source.by_ref().take(self.tick_rate));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Signals {
|
||||
pub sin1: Signal<SinSignal>,
|
||||
pub sin2: Signal<SinSignal>,
|
||||
pub window: [f64; 2],
|
||||
}
|
||||
|
||||
impl Signals {
|
||||
fn on_tick(&mut self) {
|
||||
self.sin1.on_tick();
|
||||
self.sin2.on_tick();
|
||||
self.window[0] += 1.0;
|
||||
self.window[1] += 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Server<'a> {
|
||||
pub name: &'a str,
|
||||
pub location: &'a str,
|
||||
pub coords: (f64, f64),
|
||||
pub status: &'a str,
|
||||
}
|
||||
|
||||
pub struct App<'a> {
|
||||
pub title: &'a str,
|
||||
pub should_quit: bool,
|
||||
pub tabs: TabsState<'a>,
|
||||
pub show_chart: bool,
|
||||
pub progress: f64,
|
||||
pub sparkline: Signal<RandomSignal>,
|
||||
pub tasks: StatefulList<&'a str>,
|
||||
pub logs: StatefulList<(&'a str, &'a str)>,
|
||||
pub signals: Signals,
|
||||
pub barchart: Vec<(&'a str, u64)>,
|
||||
pub servers: Vec<Server<'a>>,
|
||||
pub enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
pub fn new(title: &'a str, enhanced_graphics: bool) -> App<'a> {
|
||||
let mut rand_signal = RandomSignal::new(0, 100);
|
||||
let sparkline_points = rand_signal.by_ref().take(300).collect();
|
||||
let mut sin_signal = SinSignal::new(0.2, 3.0, 18.0);
|
||||
let sin1_points = sin_signal.by_ref().take(100).collect();
|
||||
let mut sin_signal2 = SinSignal::new(0.1, 2.0, 10.0);
|
||||
let sin2_points = sin_signal2.by_ref().take(200).collect();
|
||||
App {
|
||||
title,
|
||||
should_quit: false,
|
||||
tabs: TabsState::new(vec!["Tab0", "Tab1", "Tab2"]),
|
||||
show_chart: true,
|
||||
progress: 0.0,
|
||||
sparkline: Signal {
|
||||
source: rand_signal,
|
||||
points: sparkline_points,
|
||||
tick_rate: 1,
|
||||
},
|
||||
tasks: StatefulList::with_items(TASKS.to_vec()),
|
||||
logs: StatefulList::with_items(LOGS.to_vec()),
|
||||
signals: Signals {
|
||||
sin1: Signal {
|
||||
source: sin_signal,
|
||||
points: sin1_points,
|
||||
tick_rate: 5,
|
||||
},
|
||||
sin2: Signal {
|
||||
source: sin_signal2,
|
||||
points: sin2_points,
|
||||
tick_rate: 10,
|
||||
},
|
||||
window: [0.0, 20.0],
|
||||
},
|
||||
barchart: EVENTS.to_vec(),
|
||||
servers: vec![
|
||||
Server {
|
||||
name: "NorthAmerica-1",
|
||||
location: "New York City",
|
||||
coords: (40.71, -74.00),
|
||||
status: "Up",
|
||||
},
|
||||
Server {
|
||||
name: "Europe-1",
|
||||
location: "Paris",
|
||||
coords: (48.85, 2.35),
|
||||
status: "Failure",
|
||||
},
|
||||
Server {
|
||||
name: "SouthAmerica-1",
|
||||
location: "São Paulo",
|
||||
coords: (-23.54, -46.62),
|
||||
status: "Up",
|
||||
},
|
||||
Server {
|
||||
name: "Asia-1",
|
||||
location: "Singapore",
|
||||
coords: (1.35, 103.86),
|
||||
status: "Up",
|
||||
},
|
||||
],
|
||||
enhanced_graphics,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_up(&mut self) {
|
||||
self.tasks.previous();
|
||||
}
|
||||
|
||||
pub fn on_down(&mut self) {
|
||||
self.tasks.next();
|
||||
}
|
||||
|
||||
pub fn on_right(&mut self) {
|
||||
self.tabs.next();
|
||||
}
|
||||
|
||||
pub fn on_left(&mut self) {
|
||||
self.tabs.previous();
|
||||
}
|
||||
|
||||
pub fn on_key(&mut self, c: char) {
|
||||
match c {
|
||||
'q' => {
|
||||
self.should_quit = true;
|
||||
}
|
||||
't' => {
|
||||
self.show_chart = !self.show_chart;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_tick(&mut self) {
|
||||
// Update progress
|
||||
self.progress += 0.001;
|
||||
if self.progress > 1.0 {
|
||||
self.progress = 0.0;
|
||||
}
|
||||
|
||||
self.sparkline.on_tick();
|
||||
self.signals.on_tick();
|
||||
|
||||
let log = self.logs.items.pop().unwrap();
|
||||
self.logs.items.insert(0, log);
|
||||
|
||||
let event = self.barchart.pop().unwrap();
|
||||
self.barchart.insert(0, event);
|
||||
}
|
||||
}
|
||||
78
examples/demo/crossterm.rs
Normal file
78
examples/demo/crossterm.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::prelude::*;
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Crossterm Demo", enhanced_graphics);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
KeyCode::Left => app.on_left(),
|
||||
KeyCode::Up => app.on_up(),
|
||||
KeyCode::Right => app.on_right(),
|
||||
KeyCode::Down => app.on_down(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
36
examples/demo/main.rs
Normal file
36
examples/demo/main.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
use argh::FromArgs;
|
||||
|
||||
mod app;
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
|
||||
mod ui;
|
||||
|
||||
/// Demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
tick_rate: u64,
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
#[cfg(feature = "crossterm")]
|
||||
crate::crossterm::run(tick_rate, cli.enhanced_graphics)?;
|
||||
#[cfg(feature = "termion")]
|
||||
crate::termion::run(tick_rate, cli.enhanced_graphics)?;
|
||||
#[cfg(feature = "termwiz")]
|
||||
crate::termwiz::run(tick_rate, cli.enhanced_graphics)?;
|
||||
Ok(())
|
||||
}
|
||||
82
examples/demo/termion.rs
Normal file
82
examples/demo/termion.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||
|
||||
use ratatui::prelude::*;
|
||||
use termion::{
|
||||
event::Key,
|
||||
input::{MouseTerminal, TermRead},
|
||||
raw::IntoRawMode,
|
||||
screen::IntoAlternateScreen,
|
||||
};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
let stdout = io::stdout()
|
||||
.into_raw_mode()
|
||||
.unwrap()
|
||||
.into_alternate_screen()
|
||||
.unwrap();
|
||||
let stdout = MouseTerminal::from(stdout);
|
||||
let backend = TermionBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Termion demo", enhanced_graphics);
|
||||
run_app(&mut terminal, app, tick_rate)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let events = events(tick_rate);
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
match events.recv()? {
|
||||
Event::Input(key) => match key {
|
||||
Key::Char(c) => app.on_key(c),
|
||||
Key::Up => app.on_up(),
|
||||
Key::Down => app.on_down(),
|
||||
Key::Left => app.on_left(),
|
||||
Key::Right => app.on_right(),
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => app.on_tick(),
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(Key),
|
||||
Tick,
|
||||
}
|
||||
|
||||
fn events(tick_rate: Duration) -> mpsc::Receiver<Event> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let keys_tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for key in stdin.keys().flatten() {
|
||||
if let Err(err) = keys_tx.send(Event::Input(key)) {
|
||||
eprintln!("{err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
thread::spawn(move || loop {
|
||||
if let Err(err) = tx.send(Event::Tick) {
|
||||
eprintln!("{err}");
|
||||
break;
|
||||
}
|
||||
thread::sleep(tick_rate);
|
||||
});
|
||||
rx
|
||||
}
|
||||
76
examples/demo/termwiz.rs
Normal file
76
examples/demo/termwiz.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ratatui::prelude::*;
|
||||
use termwiz::{input::*, terminal::Terminal as TermwizTerminal};
|
||||
|
||||
use crate::{app::App, ui};
|
||||
|
||||
pub fn run(tick_rate: Duration, enhanced_graphics: bool) -> Result<(), Box<dyn Error>> {
|
||||
let backend = TermwizBackend::new()?;
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new("Termwiz Demo", enhanced_graphics);
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
terminal.show_cursor()?;
|
||||
terminal.flush()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app(
|
||||
terminal: &mut Terminal<TermwizBackend>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if let Ok(Some(input)) = terminal
|
||||
.backend_mut()
|
||||
.buffered_terminal_mut()
|
||||
.terminal()
|
||||
.poll_input(Some(timeout))
|
||||
{
|
||||
match input {
|
||||
InputEvent::Key(key_code) => match key_code.key {
|
||||
KeyCode::UpArrow => app.on_up(),
|
||||
KeyCode::DownArrow => app.on_down(),
|
||||
KeyCode::LeftArrow => app.on_left(),
|
||||
KeyCode::RightArrow => app.on_right(),
|
||||
KeyCode::Char(c) => app.on_key(c),
|
||||
_ => {}
|
||||
},
|
||||
InputEvent::Resized { cols, rows } => {
|
||||
terminal
|
||||
.backend_mut()
|
||||
.buffered_terminal_mut()
|
||||
.resize(cols, rows);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
if app.should_quit {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
428
examples/demo/ui.rs
Normal file
428
examples/demo/ui.rs
Normal file
@@ -0,0 +1,428 @@
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{canvas::*, *},
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
|
||||
pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(f.size());
|
||||
let titles = app
|
||||
.tabs
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title(app.title))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.select(app.tabs.index);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
match app.tabs.index {
|
||||
0 => draw_first_tab(f, app, chunks[1]),
|
||||
1 => draw_second_tab(f, app, chunks[1]),
|
||||
2 => draw_third_tab(f, app, chunks[1]),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn draw_first_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(9),
|
||||
Constraint::Min(8),
|
||||
Constraint::Length(7),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
draw_gauges(f, app, chunks[0]);
|
||||
draw_charts(f, app, chunks[1]);
|
||||
draw_text(f, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_gauges<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.margin(1)
|
||||
.split(area);
|
||||
let block = Block::default().borders(Borders::ALL).title("Graphs");
|
||||
f.render_widget(block, area);
|
||||
|
||||
let label = format!("{:.2}%", app.progress * 100.0);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge:"))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
)
|
||||
.use_unicode(app.enhanced_graphics)
|
||||
.label(label)
|
||||
.ratio(app.progress);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
|
||||
let sparkline = Sparkline::default()
|
||||
.block(Block::default().title("Sparkline:"))
|
||||
.style(Style::default().fg(Color::Green))
|
||||
.data(&app.sparkline.points)
|
||||
.bar_set(if app.enhanced_graphics {
|
||||
symbols::bar::NINE_LEVELS
|
||||
} else {
|
||||
symbols::bar::THREE_LEVELS
|
||||
});
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
|
||||
let line_gauge = LineGauge::default()
|
||||
.block(Block::default().title("LineGauge:"))
|
||||
.gauge_style(Style::default().fg(Color::Magenta))
|
||||
.line_set(if app.enhanced_graphics {
|
||||
symbols::line::THICK
|
||||
} else {
|
||||
symbols::line::NORMAL
|
||||
})
|
||||
.ratio(app.progress);
|
||||
f.render_widget(line_gauge, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_charts<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let constraints = if app.show_chart {
|
||||
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
vec![Constraint::Percentage(100)]
|
||||
};
|
||||
let chunks = Layout::default()
|
||||
.constraints(constraints)
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(chunks[0]);
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(chunks[0]);
|
||||
|
||||
// Draw tasks
|
||||
let tasks: Vec<ListItem> = app
|
||||
.tasks
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| ListItem::new(vec![text::Line::from(Span::raw(*i))]))
|
||||
.collect();
|
||||
let tasks = List::new(tasks)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||
.highlight_symbol("> ");
|
||||
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
|
||||
|
||||
// Draw logs
|
||||
let info_style = Style::default().fg(Color::Blue);
|
||||
let warning_style = Style::default().fg(Color::Yellow);
|
||||
let error_style = Style::default().fg(Color::Magenta);
|
||||
let critical_style = Style::default().fg(Color::Red);
|
||||
let logs: Vec<ListItem> = app
|
||||
.logs
|
||||
.items
|
||||
.iter()
|
||||
.map(|&(evt, level)| {
|
||||
let s = match level {
|
||||
"ERROR" => error_style,
|
||||
"CRITICAL" => critical_style,
|
||||
"WARNING" => warning_style,
|
||||
_ => info_style,
|
||||
};
|
||||
let content = vec![text::Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
Span::raw(evt),
|
||||
])];
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
|
||||
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
|
||||
}
|
||||
|
||||
let barchart = BarChart::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Bar chart"))
|
||||
.data(&app.barchart)
|
||||
.bar_width(3)
|
||||
.bar_gap(2)
|
||||
.bar_set(if app.enhanced_graphics {
|
||||
symbols::bar::NINE_LEVELS
|
||||
} else {
|
||||
symbols::bar::THREE_LEVELS
|
||||
})
|
||||
.value_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Green)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.label_style(Style::default().fg(Color::Yellow))
|
||||
.bar_style(Style::default().fg(Color::Green));
|
||||
f.render_widget(barchart, chunks[1]);
|
||||
}
|
||||
if app.show_chart {
|
||||
let x_labels = vec![
|
||||
Span::styled(
|
||||
format!("{}", app.signals.window[0]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!(
|
||||
"{}",
|
||||
(app.signals.window[0] + app.signals.window[1]) / 2.0
|
||||
)),
|
||||
Span::styled(
|
||||
format!("{}", app.signals.window[1]),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
];
|
||||
let datasets = vec![
|
||||
Dataset::default()
|
||||
.name("data2")
|
||||
.marker(symbols::Marker::Dot)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.data(&app.signals.sin1.points),
|
||||
Dataset::default()
|
||||
.name("data3")
|
||||
.marker(if app.enhanced_graphics {
|
||||
symbols::Marker::Braille
|
||||
} else {
|
||||
symbols::Marker::Dot
|
||||
})
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.data(&app.signals.sin2.points),
|
||||
];
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::default()
|
||||
.title(Span::styled(
|
||||
"Chart",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds(app.signals.window)
|
||||
.labels(x_labels),
|
||||
)
|
||||
.y_axis(
|
||||
Axis::default()
|
||||
.title("Y Axis")
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.bounds([-20.0, 20.0])
|
||||
.labels(vec![
|
||||
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw("0"),
|
||||
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]),
|
||||
);
|
||||
f.render_widget(chart, chunks[1]);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_text<B>(f: &mut Frame<B>, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let text = vec![
|
||||
text::Line::from("This is a paragraph with several lines. You can change style your text the way you want"),
|
||||
text::Line::from(""),
|
||||
text::Line::from(vec![
|
||||
Span::from("For example: "),
|
||||
Span::styled("under", Style::default().fg(Color::Red)),
|
||||
Span::raw(" "),
|
||||
Span::styled("the", Style::default().fg(Color::Green)),
|
||||
Span::raw(" "),
|
||||
Span::styled("rainbow", Style::default().fg(Color::Blue)),
|
||||
Span::raw("."),
|
||||
]),
|
||||
text::Line::from(vec![
|
||||
Span::raw("Oh and if you didn't "),
|
||||
Span::styled("notice", Style::default().add_modifier(Modifier::ITALIC)),
|
||||
Span::raw(" you can "),
|
||||
Span::styled("automatically", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" "),
|
||||
Span::styled("wrap", Style::default().add_modifier(Modifier::REVERSED)),
|
||||
Span::raw(" your "),
|
||||
Span::styled("text", Style::default().add_modifier(Modifier::UNDERLINED)),
|
||||
Span::raw(".")
|
||||
]),
|
||||
text::Line::from(
|
||||
"One more thing is that it should display unicode characters: 10€"
|
||||
),
|
||||
];
|
||||
let block = Block::default().borders(Borders::ALL).title(Span::styled(
|
||||
"Footer",
|
||||
Style::default()
|
||||
.fg(Color::Magenta)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn draw_second_tab<B>(f: &mut Frame<B>, app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(area);
|
||||
let up_style = Style::default().fg(Color::Green);
|
||||
let failure_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::RAPID_BLINK | Modifier::CROSSED_OUT);
|
||||
let rows = app.servers.iter().map(|s| {
|
||||
let style = if s.status == "Up" {
|
||||
up_style
|
||||
} else {
|
||||
failure_style
|
||||
};
|
||||
Row::new(vec![s.name, s.location, s.status]).style(style)
|
||||
});
|
||||
let table = Table::new(rows)
|
||||
.header(
|
||||
Row::new(vec!["Server", "Location", "Status"])
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.bottom_margin(1),
|
||||
)
|
||||
.block(Block::default().title("Servers").borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(10),
|
||||
]);
|
||||
f.render_widget(table, chunks[0]);
|
||||
|
||||
let map = Canvas::default()
|
||||
.block(Block::default().title("World").borders(Borders::ALL))
|
||||
.paint(|ctx| {
|
||||
ctx.draw(&Map {
|
||||
color: Color::White,
|
||||
resolution: MapResolution::High,
|
||||
});
|
||||
ctx.layer();
|
||||
ctx.draw(&Rectangle {
|
||||
x: 0.0,
|
||||
y: 30.0,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
ctx.draw(&Circle {
|
||||
x: app.servers[2].coords.1,
|
||||
y: app.servers[2].coords.0,
|
||||
radius: 10.0,
|
||||
color: Color::Green,
|
||||
});
|
||||
for (i, s1) in app.servers.iter().enumerate() {
|
||||
for s2 in &app.servers[i + 1..] {
|
||||
ctx.draw(&canvas::Line {
|
||||
x1: s1.coords.1,
|
||||
y1: s1.coords.0,
|
||||
y2: s2.coords.0,
|
||||
x2: s2.coords.1,
|
||||
color: Color::Yellow,
|
||||
});
|
||||
}
|
||||
}
|
||||
for server in &app.servers {
|
||||
let color = if server.status == "Up" {
|
||||
Color::Green
|
||||
} else {
|
||||
Color::Red
|
||||
};
|
||||
ctx.print(
|
||||
server.coords.1,
|
||||
server.coords.0,
|
||||
Span::styled("X", Style::default().fg(color)),
|
||||
);
|
||||
}
|
||||
})
|
||||
.marker(if app.enhanced_graphics {
|
||||
symbols::Marker::Braille
|
||||
} else {
|
||||
symbols::Marker::Dot
|
||||
})
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0]);
|
||||
f.render_widget(map, chunks[1]);
|
||||
}
|
||||
|
||||
fn draw_third_tab<B>(f: &mut Frame<B>, _app: &mut App, area: Rect)
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)])
|
||||
.split(area);
|
||||
let colors = [
|
||||
Color::Reset,
|
||||
Color::Black,
|
||||
Color::Red,
|
||||
Color::Green,
|
||||
Color::Yellow,
|
||||
Color::Blue,
|
||||
Color::Magenta,
|
||||
Color::Cyan,
|
||||
Color::Gray,
|
||||
Color::DarkGray,
|
||||
Color::LightRed,
|
||||
Color::LightGreen,
|
||||
Color::LightYellow,
|
||||
Color::LightBlue,
|
||||
Color::LightMagenta,
|
||||
Color::LightCyan,
|
||||
Color::White,
|
||||
];
|
||||
let items: Vec<Row> = colors
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let cells = vec![
|
||||
Cell::from(Span::raw(format!("{c:?}: "))),
|
||||
Cell::from(Span::styled("Foreground", Style::default().fg(*c))),
|
||||
Cell::from(Span::styled("Background", Style::default().bg(*c))),
|
||||
];
|
||||
Row::new(cells)
|
||||
})
|
||||
.collect();
|
||||
let table = Table::new(items)
|
||||
.block(Block::default().title("Colors").borders(Borders::ALL))
|
||||
.widths(&[
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
Constraint::Ratio(1, 3),
|
||||
]);
|
||||
f.render_widget(table, chunks[0]);
|
||||
}
|
||||
@@ -1,158 +1,159 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Gauge, Widget};
|
||||
use tui::Terminal;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
progress1: u16,
|
||||
progress2: u16,
|
||||
progress3: u16,
|
||||
progress3: f64,
|
||||
progress4: u16,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
progress1: 0,
|
||||
progress2: 0,
|
||||
progress3: 0,
|
||||
progress3: 0.45,
|
||||
progress4: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
self.progress1 += 5;
|
||||
fn on_tick(&mut self) {
|
||||
self.progress1 += 1;
|
||||
if self.progress1 > 100 {
|
||||
self.progress1 = 0;
|
||||
}
|
||||
self.progress2 += 10;
|
||||
self.progress2 += 2;
|
||||
if self.progress2 > 100 {
|
||||
self.progress2 = 0;
|
||||
}
|
||||
self.progress3 += 1;
|
||||
if self.progress3 > 100 {
|
||||
self.progress3 = 0;
|
||||
self.progress3 += 0.001;
|
||||
if self.progress3 > 1.0 {
|
||||
self.progress3 = 0.0;
|
||||
}
|
||||
self.progress4 += 3;
|
||||
self.progress4 += 1;
|
||||
if self.progress4 > 100 {
|
||||
self.progress4 = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(app.size);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress1)
|
||||
.render(&mut f, chunks[0]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(&format!("{}/100", app.progress2))
|
||||
.render(&mut f, chunks[1]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress3)
|
||||
.render(&mut f, chunks[2]);
|
||||
Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.style(Style::default().fg(Color::Cyan).modifier(Modifier::Italic))
|
||||
.percent(app.progress4)
|
||||
.label(&format!("{}/100", app.progress2))
|
||||
.render(&mut f, chunks[3]);
|
||||
})
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge1").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.percent(app.progress1);
|
||||
f.render_widget(gauge, chunks[0]);
|
||||
|
||||
let label = format!("{}/100", app.progress2);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge2").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green))
|
||||
.percent(app.progress2)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[1]);
|
||||
|
||||
let label = Span::styled(
|
||||
format!("{:.2}%", app.progress3 * 100.0),
|
||||
Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge3").borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(app.progress3)
|
||||
.label(label)
|
||||
.use_unicode(true);
|
||||
f.render_widget(gauge, chunks[2]);
|
||||
|
||||
let label = format!("{}/100", app.progress4);
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().title("Gauge4").borders(Borders::ALL))
|
||||
.gauge_style(
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
)
|
||||
.percent(app.progress4)
|
||||
.label(label);
|
||||
f.render_widget(gauge, chunks[3]);
|
||||
}
|
||||
|
||||
11
examples/gauge.tape
Normal file
11
examples/gauge.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/gauge.tape`
|
||||
Output "target/gauge.gif"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=gauge --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 20s
|
||||
80
examples/hello_world.rs
Normal file
80
examples/hello_world.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::{
|
||||
io::{self, Stdout},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
/// This is a bare minimum example. There are many approaches to running an application loop, so
|
||||
/// this is not meant to be prescriptive. It is only meant to demonstrate the basic setup and
|
||||
/// teardown of a terminal application.
|
||||
///
|
||||
/// A more robust application would probably want to handle errors and ensure that the terminal is
|
||||
/// restored to a sane state before exiting. This example does not do that. It also does not handle
|
||||
/// events or update the application state. It just draws a greeting and exits when the user
|
||||
/// presses 'q'.
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = setup_terminal().context("setup failed")?;
|
||||
run(&mut terminal).context("app loop failed")?;
|
||||
restore_terminal(&mut terminal).context("restore terminal failed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Setup the terminal. This is where you would enable raw mode, enter the alternate screen, and
|
||||
/// hide the cursor. This example does not handle errors. A more robust application would probably
|
||||
/// want to handle errors and ensure that the terminal is restored to a sane state before exiting.
|
||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode().context("failed to enable raw mode")?;
|
||||
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
|
||||
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
|
||||
}
|
||||
|
||||
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
|
||||
/// the cursor.
|
||||
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
disable_raw_mode().context("failed to disable raw mode")?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)
|
||||
.context("unable to switch to main screen")?;
|
||||
terminal.show_cursor().context("unable to show cursor")
|
||||
}
|
||||
|
||||
/// Run the application loop. This is where you would handle events and update the application
|
||||
/// state. This example exits when the user presses 'q'. Other styles of application loops are
|
||||
/// possible, for example, you could have multiple application states and switch between them based
|
||||
/// on events, or you could have a single application state and update it based on events.
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(crate::render_app)?;
|
||||
if should_quit()? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render the application. This is where you would draw the application UI. This example just
|
||||
/// draws a greeting.
|
||||
fn render_app(frame: &mut ratatui::Frame<CrosstermBackend<Stdout>>) {
|
||||
let greeting = Paragraph::new("Hello World! (press 'q' to quit)");
|
||||
frame.render_widget(greeting, frame.size());
|
||||
}
|
||||
|
||||
/// Check if the user has pressed 'q'. This is where you would handle events. This example just
|
||||
/// checks if the user has pressed 'q' and returns true if they have. It does not handle any other
|
||||
/// events. There is a 250ms timeout on the event poll so that the application can exit in a timely
|
||||
/// manner, and to ensure that the terminal is rendered at least once every 250ms.
|
||||
fn should_quit() -> Result<bool> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
return Ok(KeyCode::Char('q') == key.code);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
11
examples/hello_world.tape
Normal file
11
examples/hello_world.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
|
||||
Output "target/hello_world.gif"
|
||||
Set Width 1200
|
||||
Set Height 200
|
||||
Hide
|
||||
Type "cargo run --example=hello_world --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
283
examples/inline.rs
Normal file
283
examples/inline.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, VecDeque},
|
||||
error::Error,
|
||||
io,
|
||||
sync::mpsc,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
const NUM_DOWNLOADS: usize = 10;
|
||||
|
||||
type DownloadId = usize;
|
||||
type WorkerId = usize;
|
||||
|
||||
enum Event {
|
||||
Input(crossterm::event::KeyEvent),
|
||||
Tick,
|
||||
Resize,
|
||||
DownloadUpdate(WorkerId, DownloadId, f64),
|
||||
DownloadDone(WorkerId, DownloadId),
|
||||
}
|
||||
|
||||
struct Downloads {
|
||||
pending: VecDeque<Download>,
|
||||
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
|
||||
}
|
||||
|
||||
impl Downloads {
|
||||
fn next(&mut self, worker_id: WorkerId) -> Option<Download> {
|
||||
match self.pending.pop_front() {
|
||||
Some(d) => {
|
||||
self.in_progress.insert(
|
||||
worker_id,
|
||||
DownloadInProgress {
|
||||
id: d.id,
|
||||
started_at: Instant::now(),
|
||||
progress: 0.0,
|
||||
},
|
||||
);
|
||||
Some(d)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DownloadInProgress {
|
||||
id: DownloadId,
|
||||
started_at: Instant,
|
||||
progress: f64,
|
||||
}
|
||||
|
||||
struct Download {
|
||||
id: DownloadId,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
id: WorkerId,
|
||||
tx: mpsc::Sender<Download>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
let stdout = io::stdout();
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Inline(8),
|
||||
},
|
||||
)?;
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
input_handling(tx.clone());
|
||||
let workers = workers(tx);
|
||||
let mut downloads = downloads();
|
||||
|
||||
for w in &workers {
|
||||
let d = downloads.next(w.id).unwrap();
|
||||
w.tx.send(d).unwrap();
|
||||
}
|
||||
|
||||
run_app(&mut terminal, workers, downloads, rx)?;
|
||||
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
terminal.clear()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_handling(tx: mpsc::Sender<Event>) {
|
||||
let tick_rate = Duration::from_millis(200);
|
||||
thread::spawn(move || {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
// poll for tick rate duration, if no events, sent tick event.
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout).unwrap() {
|
||||
match crossterm::event::read().unwrap() {
|
||||
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
|
||||
crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
tx.send(Event::Tick).unwrap();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
|
||||
(0..4)
|
||||
.map(|id| {
|
||||
let (worker_tx, worker_rx) = mpsc::channel::<Download>();
|
||||
let tx = tx.clone();
|
||||
thread::spawn(move || {
|
||||
while let Ok(download) = worker_rx.recv() {
|
||||
let mut remaining = download.size;
|
||||
while remaining > 0 {
|
||||
let wait = (remaining as u64).min(10);
|
||||
thread::sleep(Duration::from_millis(wait * 10));
|
||||
remaining = remaining.saturating_sub(10);
|
||||
let progress = (download.size - remaining) * 100 / download.size;
|
||||
tx.send(Event::DownloadUpdate(id, download.id, progress as f64))
|
||||
.unwrap();
|
||||
}
|
||||
tx.send(Event::DownloadDone(id, download.id)).unwrap();
|
||||
}
|
||||
});
|
||||
Worker { id, tx: worker_tx }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn downloads() -> Downloads {
|
||||
let distribution = Uniform::new(0, 1000);
|
||||
let mut rng = rand::thread_rng();
|
||||
let pending = (0..NUM_DOWNLOADS)
|
||||
.map(|id| {
|
||||
let size = distribution.sample(&mut rng);
|
||||
Download { id, size }
|
||||
})
|
||||
.collect();
|
||||
Downloads {
|
||||
pending,
|
||||
in_progress: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
workers: Vec<Worker>,
|
||||
mut downloads: Downloads,
|
||||
rx: mpsc::Receiver<Event>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let mut redraw = true;
|
||||
loop {
|
||||
if redraw {
|
||||
terminal.draw(|f| ui(f, &downloads))?;
|
||||
}
|
||||
redraw = true;
|
||||
|
||||
match rx.recv()? {
|
||||
Event::Input(event) => {
|
||||
if event.code == crossterm::event::KeyCode::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Resize => {
|
||||
terminal.autoresize()?;
|
||||
}
|
||||
Event::Tick => {}
|
||||
Event::DownloadUpdate(worker_id, _download_id, progress) => {
|
||||
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
|
||||
download.progress = progress;
|
||||
redraw = false
|
||||
}
|
||||
Event::DownloadDone(worker_id, download_id) => {
|
||||
let download = downloads.in_progress.remove(&worker_id).unwrap();
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::from("Finished "),
|
||||
Span::styled(
|
||||
format!("download {download_id}"),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::from(format!(
|
||||
" in {}ms",
|
||||
download.started_at.elapsed().as_millis()
|
||||
)),
|
||||
]))
|
||||
.render(buf.area, buf);
|
||||
})?;
|
||||
match downloads.next(worker_id) {
|
||||
Some(d) => workers[worker_id].tx.send(d).unwrap(),
|
||||
None => {
|
||||
if downloads.in_progress.is_empty() {
|
||||
terminal.insert_before(1, |buf| {
|
||||
Paragraph::new("Done !").render(buf.area, buf);
|
||||
})?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
|
||||
let size = f.size();
|
||||
|
||||
let block = Block::default().title(block::Title::from("Progress").alignment(Alignment::Center));
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
|
||||
.margin(1)
|
||||
.split(size);
|
||||
|
||||
// total progress
|
||||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
let progress = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||
f.render_widget(progress, chunks[0]);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||
.split(chunks[1]);
|
||||
|
||||
// in progress downloads
|
||||
let items: Vec<ListItem> = downloads
|
||||
.in_progress
|
||||
.values()
|
||||
.map(|download| {
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::raw(symbols::DOT),
|
||||
Span::styled(
|
||||
format!(" download {:>2}", download.id),
|
||||
Style::default()
|
||||
.fg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(format!(
|
||||
" ({}ms)",
|
||||
download.started_at.elapsed().as_millis()
|
||||
)),
|
||||
]))
|
||||
})
|
||||
.collect();
|
||||
let list = List::new(items);
|
||||
f.render_widget(list, chunks[0]);
|
||||
|
||||
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
||||
let gauge = Gauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Yellow))
|
||||
.ratio(download.progress / 100.0);
|
||||
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
|
||||
continue;
|
||||
}
|
||||
f.render_widget(
|
||||
gauge,
|
||||
Rect {
|
||||
x: chunks[1].left(),
|
||||
y: chunks[1].top().saturating_add(i as u16),
|
||||
width: chunks[1].width,
|
||||
height: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
8
examples/inline.tape
Normal file
8
examples/inline.tape
Normal file
@@ -0,0 +1,8 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/inline.tape`
|
||||
Output "target/inline.gif"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Type "cargo run --example=inline --features=crossterm"
|
||||
Enter
|
||||
Sleep 20s
|
||||
@@ -1,107 +1,96 @@
|
||||
extern crate log;
|
||||
extern crate stderrlog;
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{error::Error, io};
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
use tui::Terminal;
|
||||
// create app and run it
|
||||
let res = run_app(&mut terminal);
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
}
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
}
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
}
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f))?;
|
||||
|
||||
fn main() {
|
||||
stderrlog::new().verbosity(4).init().unwrap();
|
||||
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if let event::Key::Char('q') = input {
|
||||
break;
|
||||
},
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Percentage(80),
|
||||
Constraint::Percentage(10),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(app.size);
|
||||
fn ui<B: Backend>(frame: &mut Frame<B>) {
|
||||
let [top, mid, bottom] = *Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(4),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Min(4),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(frame.size())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let [left, right] = *Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.horizontal_margin(5)
|
||||
.vertical_margin(2)
|
||||
.constraints([Constraint::Ratio(2, 5), Constraint::Ratio(3, 5)].as_ref())
|
||||
.split(mid)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new("Constraint::Length(4)").block(Block::default().borders(Borders::ALL)),
|
||||
top,
|
||||
);
|
||||
|
||||
Block::default()
|
||||
.title("Block")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[0]);
|
||||
Block::default()
|
||||
.title("Block 2")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[2]);
|
||||
})
|
||||
frame.render_widget(
|
||||
Paragraph::new("Constraint::Percentage(50)").block(Block::default().borders(Borders::ALL)),
|
||||
mid,
|
||||
);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new("Constraint::Ratio(2, 5)\nhorizontal_margin(5)\nvertical_margin(2)")
|
||||
.block(Block::default().borders(Borders::ALL)),
|
||||
left,
|
||||
);
|
||||
frame.render_widget(
|
||||
Paragraph::new("Constraint::Ratio(3, 5)").block(Block::default().borders(Borders::ALL)),
|
||||
right,
|
||||
);
|
||||
frame.render_widget(
|
||||
Paragraph::new("Constraint::Min(4)").block(Block::default().borders(Borders::ALL)),
|
||||
bottom,
|
||||
);
|
||||
}
|
||||
|
||||
11
examples/layout.tape
Normal file
11
examples/layout.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/layout.tape`
|
||||
Output "target/layout.gif"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=layout --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
377
examples/list.rs
377
examples/list.rs
@@ -1,41 +1,103 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
struct StatefulList<T> {
|
||||
state: ListState,
|
||||
items: Vec<T>,
|
||||
}
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Corner, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, List, SelectableList, Text, Widget};
|
||||
use tui::Terminal;
|
||||
impl<T> StatefulList<T> {
|
||||
fn with_items(items: Vec<T>) -> StatefulList<T> {
|
||||
StatefulList {
|
||||
state: ListState::default(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
fn unselect(&mut self) {
|
||||
self.state.select(None);
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct holds the current state of the app. In particular, it has the `items` field which is
|
||||
/// a wrapper around `ListState`. Keeping track of the items state let us render the associated
|
||||
/// widget with its state and have access to features such as natural scrolling.
|
||||
///
|
||||
/// Check the event handling at the bottom to see how to change the state on incoming events.
|
||||
/// Check the drawing logic for items on how to specify the highlighting style for selected items.
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
items: Vec<&'a str>,
|
||||
selected: Option<usize>,
|
||||
items: StatefulList<(&'a str, usize)>,
|
||||
events: Vec<(&'a str, &'a str)>,
|
||||
info_style: Style,
|
||||
warning_style: Style,
|
||||
error_style: Style,
|
||||
critical_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
items: vec![
|
||||
"Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9",
|
||||
"Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17",
|
||||
"Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
|
||||
],
|
||||
selected: None,
|
||||
items: StatefulList::with_items(vec![
|
||||
("Item0", 1),
|
||||
("Item1", 2),
|
||||
("Item2", 1),
|
||||
("Item3", 3),
|
||||
("Item4", 1),
|
||||
("Item5", 4),
|
||||
("Item6", 1),
|
||||
("Item7", 3),
|
||||
("Item8", 1),
|
||||
("Item9", 6),
|
||||
("Item10", 1),
|
||||
("Item11", 3),
|
||||
("Item12", 1),
|
||||
("Item13", 2),
|
||||
("Item14", 1),
|
||||
("Item15", 1),
|
||||
("Item16", 4),
|
||||
("Item17", 1),
|
||||
("Item18", 5),
|
||||
("Item19", 4),
|
||||
("Item20", 1),
|
||||
("Item21", 2),
|
||||
("Item22", 1),
|
||||
("Item23", 3),
|
||||
("Item24", 1),
|
||||
]),
|
||||
events: vec![
|
||||
("Event1", "INFO"),
|
||||
("Event2", "INFO"),
|
||||
@@ -64,144 +126,155 @@ impl<'a> App<'a> {
|
||||
("Event25", "INFO"),
|
||||
("Event26", "INFO"),
|
||||
],
|
||||
info_style: Style::default().fg(Color::White),
|
||||
warning_style: Style::default().fg(Color::Yellow),
|
||||
error_style: Style::default().fg(Color::Magenta),
|
||||
critical_style: Style::default().fg(Color::Red),
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
let event = self.events.pop().unwrap();
|
||||
self.events.insert(0, event);
|
||||
/// Rotate through the event list.
|
||||
/// This only exists to simulate some kind of "progress"
|
||||
fn on_tick(&mut self) {
|
||||
let event = self.events.remove(0);
|
||||
self.events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Left => {
|
||||
app.selected = None;
|
||||
}
|
||||
event::Key::Down => {
|
||||
app.selected = if let Some(selected) = app.selected {
|
||||
if selected >= app.items.len() - 1 {
|
||||
Some(0)
|
||||
} else {
|
||||
Some(selected + 1)
|
||||
}
|
||||
} else {
|
||||
Some(0)
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Left => app.items.unselect(),
|
||||
KeyCode::Down => app.items.next(),
|
||||
KeyCode::Up => app.items.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
event::Key::Up => {
|
||||
app.selected = if let Some(selected) = app.selected {
|
||||
if selected > 0 {
|
||||
Some(selected - 1)
|
||||
} else {
|
||||
Some(app.items.len() - 1)
|
||||
}
|
||||
} else {
|
||||
Some(0)
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(app.size);
|
||||
|
||||
let style = Style::default().fg(Color::Black).bg(Color::White);
|
||||
SelectableList::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.items(&app.items)
|
||||
.select(app.selected)
|
||||
.style(style)
|
||||
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::Bold))
|
||||
.highlight_symbol(">")
|
||||
.render(&mut f, chunks[0]);
|
||||
{
|
||||
let events = app.events.iter().map(|&(evt, level)| {
|
||||
Text::styled(
|
||||
format!("{}: {}", level, evt),
|
||||
match level {
|
||||
"ERROR" => app.error_style,
|
||||
"CRITICAL" => app.critical_style,
|
||||
"WARNING" => app.warning_style,
|
||||
_ => app.info_style,
|
||||
},
|
||||
)
|
||||
});
|
||||
List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft)
|
||||
.render(&mut f, chunks[1]);
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
// Create two chunks with equal horizontal screen space
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
// Iterate through all elements in the `items` app and append some debug text to it.
|
||||
let items: Vec<ListItem> = app
|
||||
.items
|
||||
.items
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let mut lines = vec![Line::from(i.0)];
|
||||
for _ in 0..i.1 {
|
||||
lines.push(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||
.italic()
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
ListItem::new(lines).style(Style::default().fg(Color::Black).bg(Color::White))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let items = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_symbol(">> ");
|
||||
|
||||
// We can now render the item list
|
||||
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
|
||||
|
||||
// Let's do the same for the events.
|
||||
// The event list doesn't have any state and only displays the current state of the list.
|
||||
let events: Vec<ListItem> = app
|
||||
.events
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|&(event, level)| {
|
||||
// Colorcode the level depending on its type
|
||||
let s = match level {
|
||||
"CRITICAL" => Style::default().fg(Color::Red),
|
||||
"ERROR" => Style::default().fg(Color::Magenta),
|
||||
"WARNING" => Style::default().fg(Color::Yellow),
|
||||
"INFO" => Style::default().fg(Color::Blue),
|
||||
_ => Style::default(),
|
||||
};
|
||||
// Add a example datetime and apply proper spacing between them
|
||||
let header = Line::from(vec![
|
||||
Span::styled(format!("{level:<9}"), s),
|
||||
" ".into(),
|
||||
"2020-01-01 10:00:00".italic(),
|
||||
]);
|
||||
// The event gets its own line
|
||||
let log = Line::from(vec![event.into()]);
|
||||
|
||||
// Here several things happen:
|
||||
// 1. Add a `---` spacing line above the final list entry
|
||||
// 2. Add the Level + datetime
|
||||
// 3. Add a spacer line
|
||||
// 4. Add the actual event
|
||||
ListItem::new(vec![
|
||||
Line::from("-".repeat(chunks[1].width as usize)),
|
||||
header,
|
||||
Line::from(""),
|
||||
log,
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
let events_list = List::new(events)
|
||||
.block(Block::default().borders(Borders::ALL).title("List"))
|
||||
.start_corner(Corner::BottomLeft);
|
||||
f.render_widget(events_list, chunks[1]);
|
||||
}
|
||||
|
||||
14
examples/list.tape
Normal file
14
examples/list.tape
Normal file
@@ -0,0 +1,14 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/list.tape`
|
||||
Output "target/list.gif"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=list --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Down@1s 4
|
||||
Up@1s 2
|
||||
Left@1s 1
|
||||
Sleep 5s
|
||||
134
examples/panic.rs
Normal file
134
examples/panic.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//! How to use a panic hook to reset the terminal before printing the panic to
|
||||
//! the terminal.
|
||||
//!
|
||||
//! When exiting normally or when handling `Result::Err`, we can reset the
|
||||
//! terminal manually at the end of `main` just before we print the error.
|
||||
//!
|
||||
//! Because a panic interrupts the normal control flow, manually resetting the
|
||||
//! terminal at the end of `main` won't do us any good. Instead, we need to
|
||||
//! make sure to set up a panic hook that first resets the terminal before
|
||||
//! handling the panic. This both reuses the standard panic hook to ensure a
|
||||
//! consistent panic handling UX and properly resets the terminal to not
|
||||
//! distort the output.
|
||||
//!
|
||||
//! That's why this example is set up to show both situations, with and without
|
||||
//! the chained panic hook, to see the difference.
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn Error>>;
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
hook_enabled: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn chain_hook(&mut self) {
|
||||
let original_hook = std::panic::take_hook();
|
||||
|
||||
std::panic::set_hook(Box::new(move |panic| {
|
||||
reset_terminal().unwrap();
|
||||
original_hook(panic);
|
||||
}));
|
||||
|
||||
self.hook_enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut terminal = init_terminal()?;
|
||||
|
||||
let mut app = App::default();
|
||||
let res = run_tui(&mut terminal, &mut app);
|
||||
|
||||
reset_terminal()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initializes the terminal.
|
||||
fn init_terminal() -> Result<Terminal<CrosstermBackend<io::Stdout>>> {
|
||||
crossterm::execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
enable_raw_mode()?;
|
||||
|
||||
let backend = CrosstermBackend::new(io::stdout());
|
||||
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
terminal.hide_cursor()?;
|
||||
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
/// Resets the terminal.
|
||||
fn reset_terminal() -> Result<()> {
|
||||
disable_raw_mode()?;
|
||||
crossterm::execute!(io::stdout(), LeaveAlternateScreen)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs the TUI loop.
|
||||
fn run_tui<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('p') => {
|
||||
panic!("intentional demo panic");
|
||||
}
|
||||
|
||||
KeyCode::Char('e') => {
|
||||
app.chain_hook();
|
||||
}
|
||||
|
||||
_ => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the TUI.
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let text = vec![
|
||||
if app.hook_enabled {
|
||||
Line::from("HOOK IS CURRENTLY **ENABLED**")
|
||||
} else {
|
||||
Line::from("HOOK IS CURRENTLY **DISABLED**")
|
||||
},
|
||||
Line::from(""),
|
||||
Line::from("press `p` to panic"),
|
||||
Line::from("press `e` to enable the terminal-resetting panic hook"),
|
||||
Line::from("press any other key to quit without panic"),
|
||||
Line::from(""),
|
||||
Line::from("when you panic without the chained hook,"),
|
||||
Line::from("you will likely have to reset your terminal afterwards"),
|
||||
Line::from("with the `reset` command"),
|
||||
Line::from(""),
|
||||
Line::from("with the chained panic hook enabled,"),
|
||||
Line::from("you should see the panic report as you would without ratatui"),
|
||||
Line::from(""),
|
||||
Line::from("try first without the panic handler to see the difference"),
|
||||
];
|
||||
|
||||
let b = Block::default()
|
||||
.title("Panic Handler Demo")
|
||||
.borders(Borders::ALL);
|
||||
|
||||
let p = Paragraph::new(text).block(b).alignment(Alignment::Center);
|
||||
|
||||
f.render_widget(p, f.size());
|
||||
}
|
||||
19
examples/panic.tape
Normal file
19
examples/panic.tape
Normal file
@@ -0,0 +1,19 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/panic.tape`
|
||||
Output "target/panic.gif"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Type "cargo run --example=panic --features=crossterm"
|
||||
Enter
|
||||
Sleep 5s
|
||||
Type p
|
||||
Sleep 2s
|
||||
Type reset
|
||||
Enter
|
||||
Type "cargo run --example=panic --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Type e
|
||||
Sleep 2s
|
||||
Type p
|
||||
Sleep 5s
|
||||
@@ -1,83 +1,159 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Paragraph, Text, Widget};
|
||||
use tui::Terminal;
|
||||
struct App {
|
||||
scroll: u16,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(MouseBackend::new().unwrap()).unwrap();
|
||||
let stdin = io::stdin();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { scroll: 0 }
|
||||
}
|
||||
|
||||
let mut term_size = terminal.size().unwrap();
|
||||
draw(&mut terminal, term_size).unwrap();
|
||||
fn on_tick(&mut self) {
|
||||
self.scroll += 1;
|
||||
self.scroll %= 10;
|
||||
}
|
||||
}
|
||||
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != term_size {
|
||||
terminal.resize(size).unwrap();
|
||||
term_size = size;
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw(&mut terminal, term_size).unwrap();
|
||||
let evt = c.unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, size: Rect) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_blue()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.on_green()),
|
||||
Line::from("This is a line".green().italic()),
|
||||
Line::from(vec![
|
||||
"Masked text: ".into(),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
];
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(&mut f, size);
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
Constraint::Percentage(30),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), no wrap"));
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let text = [
|
||||
Text::raw("This a line\n"),
|
||||
Text::styled("This a line\n", Style::default().fg(Color::Red)),
|
||||
Text::styled("This a line\n", Style::default().bg(Color::Blue)),
|
||||
Text::styled(
|
||||
"This a longer line\n",
|
||||
Style::default().modifier(Modifier::CrossedOut),
|
||||
),
|
||||
Text::styled(
|
||||
"This a line\n",
|
||||
Style::default().fg(Color::Green).modifier(Modifier::Italic),
|
||||
),
|
||||
];
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Default alignment (Left), with wrap"))
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
|
||||
Paragraph::new(text.iter())
|
||||
.alignment(Alignment::Left)
|
||||
.render(&mut f, chunks[0]);
|
||||
Paragraph::new(text.iter())
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(true)
|
||||
.render(&mut f, chunks[1]);
|
||||
Paragraph::new(text.iter())
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(true)
|
||||
.render(&mut f, chunks[2]);
|
||||
})
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Right alignment, with wrap"))
|
||||
.alignment(Alignment::Right)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.style(Style::default().fg(Color::Gray))
|
||||
.block(create_block("Center alignment, with wrap, with scroll"))
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((app.scroll, 0));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
}
|
||||
|
||||
11
examples/paragraph.tape
Normal file
11
examples/paragraph.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/paragraph.tape`
|
||||
Output "target/paragraph.gif"
|
||||
Set Width 1200
|
||||
Set Height 1800
|
||||
Hide
|
||||
Type "cargo run --example=paragraph --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
120
examples/popup.rs
Normal file
120
examples/popup.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App {
|
||||
show_popup: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
App { show_popup: false }
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('p') => app.show_popup = !app.show_popup,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
|
||||
let chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(20), Constraint::Percentage(80)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let text = if app.show_popup {
|
||||
"Press p to close the popup"
|
||||
} else {
|
||||
"Press p to show the popup"
|
||||
};
|
||||
let paragraph = Paragraph::new(text.slow_blink())
|
||||
.alignment(Alignment::Center)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(paragraph, chunks[0]);
|
||||
|
||||
let block = Block::default()
|
||||
.title("Content")
|
||||
.borders(Borders::ALL)
|
||||
.on_blue();
|
||||
f.render_widget(block, chunks[1]);
|
||||
|
||||
if app.show_popup {
|
||||
let block = Block::default().title("Popup").borders(Borders::ALL);
|
||||
let area = centered_rect(60, 20, size);
|
||||
f.render_widget(Clear, area); //this clears out the background
|
||||
f.render_widget(block, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// helper function to create a centered rect using up certain percentage of the available rect `r`
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
Constraint::Percentage(percent_y),
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(r);
|
||||
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
Constraint::Percentage(percent_x),
|
||||
Constraint::Percentage((100 - percent_x) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
15
examples/popup.tape
Normal file
15
examples/popup.tape
Normal file
@@ -0,0 +1,15 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/popup.tape`
|
||||
Output "target/popup.gif"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=popup --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 2s
|
||||
Type p
|
||||
Sleep 2s
|
||||
Type p
|
||||
Sleep 5s
|
||||
@@ -1,48 +0,0 @@
|
||||
extern crate rustbox;
|
||||
extern crate tui;
|
||||
|
||||
use rustbox::Key;
|
||||
use std::error::Error;
|
||||
|
||||
use tui::backend::RustboxBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Paragraph, Widget};
|
||||
use tui::Terminal;
|
||||
|
||||
fn main() {
|
||||
let mut terminal = Terminal::new(RustboxBackend::new().unwrap()).unwrap();
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
draw(&mut terminal);
|
||||
loop {
|
||||
match terminal.backend().rustbox().poll_event(false) {
|
||||
Ok(rustbox::Event::KeyEvent(key)) => if key == Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Err(e) => panic!("{}", e.description()),
|
||||
_ => {}
|
||||
};
|
||||
draw(&mut terminal);
|
||||
}
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<RustboxBackend>) {
|
||||
let size = t.size().unwrap();
|
||||
{
|
||||
let mut f = t.get_frame();
|
||||
Paragraph::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Rustbox backend")
|
||||
.title_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold))
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Magenta)),
|
||||
)
|
||||
.text("It {yellow works}!")
|
||||
.render(&mut f, &size);
|
||||
}
|
||||
|
||||
t.draw().unwrap();
|
||||
}
|
||||
245
examples/scrollbar.rs
Normal file
245
examples/scrollbar.rs
Normal file
@@ -0,0 +1,245 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, symbols::scrollbar, widgets::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
pub vertical_scroll_state: ScrollbarState,
|
||||
pub horizontal_scroll_state: ScrollbarState,
|
||||
pub vertical_scroll: usize,
|
||||
pub horizontal_scroll: usize,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::default();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Char('j') => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_add(1);
|
||||
app.vertical_scroll_state = app
|
||||
.vertical_scroll_state
|
||||
.position(app.vertical_scroll as u16);
|
||||
}
|
||||
KeyCode::Char('k') => {
|
||||
app.vertical_scroll = app.vertical_scroll.saturating_sub(1);
|
||||
app.vertical_scroll_state = app
|
||||
.vertical_scroll_state
|
||||
.position(app.vertical_scroll as u16);
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_sub(1);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.position(app.horizontal_scroll as u16);
|
||||
}
|
||||
KeyCode::Char('l') => {
|
||||
app.horizontal_scroll = app.horizontal_scroll.saturating_add(1);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.position(app.horizontal_scroll as u16);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let size = f.size();
|
||||
|
||||
// Words made "loooong" to demonstrate line breaking.
|
||||
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
|
||||
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
|
||||
long_line.push('\n');
|
||||
|
||||
let block = Block::default().black();
|
||||
f.render_widget(block, size);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Min(1),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
Constraint::Percentage(25),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(size);
|
||||
|
||||
let text = vec![
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_dark_gray()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.reset()),
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
Line::from("This is a line "),
|
||||
Line::from("This is a line ".red()),
|
||||
Line::from("This is a line".on_dark_gray()),
|
||||
Line::from("This is a longer line".crossed_out()),
|
||||
Line::from(long_line.reset()),
|
||||
Line::from("This is a line".reset()),
|
||||
Line::from(vec![
|
||||
Span::raw("Masked text: "),
|
||||
Span::styled(
|
||||
Masked::new("password", '*'),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
]),
|
||||
];
|
||||
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len() as u16);
|
||||
app.horizontal_scroll_state = app
|
||||
.horizontal_scroll_state
|
||||
.content_length(long_line.len() as u16);
|
||||
|
||||
let create_block = |title| {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.gray()
|
||||
.title(Span::styled(
|
||||
title,
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
};
|
||||
|
||||
let title = Block::default()
|
||||
.title("Use h j k l to scroll ◄ ▲ ▼ ►")
|
||||
.title_alignment(Alignment::Center);
|
||||
f.render_widget(title, chunks[0]);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block("Vertical scrollbar with arrows"))
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[1]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalRight)
|
||||
.begin_symbol(Some("↑"))
|
||||
.end_symbol(Some("↓")),
|
||||
chunks[1],
|
||||
&mut app.vertical_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Vertical scrollbar without arrows and mirrored",
|
||||
))
|
||||
.scroll((app.vertical_scroll as u16, 0));
|
||||
f.render_widget(paragraph, chunks[2]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::VerticalLeft)
|
||||
.symbols(scrollbar::VERTICAL)
|
||||
.begin_symbol(None)
|
||||
.end_symbol(None),
|
||||
chunks[2].inner(&Margin {
|
||||
vertical: 1,
|
||||
horizontal: 0,
|
||||
}),
|
||||
&mut app.vertical_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Horizontal scrollbar with only begin arrow & custom thumb symbol",
|
||||
))
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[3]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("🬋")
|
||||
.end_symbol(None),
|
||||
chunks[3].inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
&mut app.horizontal_scroll_state,
|
||||
);
|
||||
|
||||
let paragraph = Paragraph::new(text.clone())
|
||||
.gray()
|
||||
.block(create_block(
|
||||
"Horizontal scrollbar without arrows & custom thumb and track symbol",
|
||||
))
|
||||
.scroll((0, app.horizontal_scroll as u16));
|
||||
f.render_widget(paragraph, chunks[4]);
|
||||
f.render_stateful_widget(
|
||||
Scrollbar::default()
|
||||
.orientation(ScrollbarOrientation::HorizontalBottom)
|
||||
.thumb_symbol("░")
|
||||
.track_symbol("─"),
|
||||
chunks[4].inner(&Margin {
|
||||
vertical: 0,
|
||||
horizontal: 1,
|
||||
}),
|
||||
&mut app.horizontal_scroll_state,
|
||||
);
|
||||
}
|
||||
11
examples/scrollbar.tape
Normal file
11
examples/scrollbar.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/scrollbar.tape`
|
||||
Output "target/scrollbar.gif"
|
||||
Set Width 1200
|
||||
Set Height 1200
|
||||
Hide
|
||||
Type "cargo run --example=scrollbar --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,25 +1,43 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
mod util;
|
||||
use util::*;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, Sparkline, Widget};
|
||||
use tui::Terminal;
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
struct App {
|
||||
size: Rect,
|
||||
signal: RandomSignal,
|
||||
data1: Vec<u64>,
|
||||
data2: Vec<u64>,
|
||||
@@ -33,7 +51,6 @@ impl App {
|
||||
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
App {
|
||||
size: Rect::default(),
|
||||
signal,
|
||||
data1,
|
||||
data2,
|
||||
@@ -41,7 +58,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance(&mut self) {
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data1.pop();
|
||||
self.data1.insert(0, value);
|
||||
@@ -54,111 +71,100 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
Tick,
|
||||
}
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
// create app and run it
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app, tick_rate);
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
let clock_tx = tx.clone();
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Tick
|
||||
thread::spawn(move || loop {
|
||||
clock_tx.send(Event::Tick).unwrap();
|
||||
thread::sleep(time::Duration::from_millis(500));
|
||||
});
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => if input == event::Key::Char('q') {
|
||||
break;
|
||||
},
|
||||
Event::Tick => {
|
||||
app.advance();
|
||||
}
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(0),
|
||||
].as_ref(),
|
||||
)
|
||||
.split(app.size);
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.render(&mut f, chunks[0]);
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green))
|
||||
.render(&mut f, chunks[1]);
|
||||
// Multiline
|
||||
Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.render(&mut f, chunks[2]);
|
||||
})
|
||||
fn run_app<B: Backend>(
|
||||
terminal: &mut Terminal<B>,
|
||||
mut app: App,
|
||||
tick_rate: Duration,
|
||||
) -> io::Result<()> {
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
let timeout = tick_rate
|
||||
.checked_sub(last_tick.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_secs(0));
|
||||
if crossterm::event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
app.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(7),
|
||||
Constraint::Min(0),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data1")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
f.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data2")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
f.render_widget(sparkline, chunks[1]);
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::default()
|
||||
.title("Data3")
|
||||
.borders(Borders::LEFT | Borders::RIGHT),
|
||||
)
|
||||
.data(&app.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
f.render_widget(sparkline, chunks[2]);
|
||||
}
|
||||
|
||||
11
examples/sparkline.tape
Normal file
11
examples/sparkline.tape
Normal file
@@ -0,0 +1,11 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/sparkline.tape`
|
||||
Output "target/sparkline.gif"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=sparkline --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 5s
|
||||
@@ -1,108 +1,151 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{error::Error, io};
|
||||
|
||||
use std::io;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Layout, Rect};
|
||||
use tui::style::{Color, Modifier, Style};
|
||||
use tui::widgets::{Block, Borders, Row, Table, Widget};
|
||||
use tui::Terminal;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
state: TableState,
|
||||
items: Vec<Vec<&'a str>>,
|
||||
selected: usize,
|
||||
}
|
||||
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
state: TableState::default(),
|
||||
items: vec![
|
||||
vec!["Row12", "Row12", "Row13"],
|
||||
vec!["Row11", "Row12", "Row13"],
|
||||
vec!["Row21", "Row22", "Row23"],
|
||||
vec!["Row31", "Row32", "Row33"],
|
||||
vec!["Row41", "Row42", "Row43"],
|
||||
vec!["Row51", "Row52", "Row53"],
|
||||
vec!["Row61", "Row62", "Row63"],
|
||||
vec!["Row61", "Row62\nTest", "Row63"],
|
||||
vec!["Row71", "Row72", "Row73"],
|
||||
vec!["Row81", "Row82", "Row83"],
|
||||
vec!["Row91", "Row92", "Row93"],
|
||||
vec!["Row101", "Row102", "Row103"],
|
||||
vec!["Row111", "Row112", "Row113"],
|
||||
vec!["Row121", "Row122", "Row123"],
|
||||
vec!["Row131", "Row132", "Row133"],
|
||||
vec!["Row141", "Row142", "Row143"],
|
||||
vec!["Row151", "Row152", "Row153"],
|
||||
vec!["Row161", "Row162", "Row163"],
|
||||
vec!["Row171", "Row172", "Row173"],
|
||||
vec!["Row181", "Row182", "Row183"],
|
||||
vec!["Row191", "Row192", "Row193"],
|
||||
],
|
||||
selected: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
|
||||
// Input
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
}
|
||||
|
||||
let evt = c.unwrap();
|
||||
match evt {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Down => {
|
||||
app.selected += 1;
|
||||
if app.selected > app.items.len() - 1 {
|
||||
app.selected = 0;
|
||||
pub fn next(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i >= self.items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
i + 1
|
||||
}
|
||||
}
|
||||
event::Key::Up => if app.selected > 0 {
|
||||
app.selected -= 1;
|
||||
} else {
|
||||
app.selected = app.items.len() - 1;
|
||||
},
|
||||
_ => {}
|
||||
None => 0,
|
||||
};
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let selected_style = Style::default().fg(Color::Yellow).modifier(Modifier::Bold);
|
||||
let normal_style = Style::default().fg(Color::White);
|
||||
let header = ["Header1", "Header2", "Header3"];
|
||||
let rows = app.items.iter().enumerate().map(|(i, item)| {
|
||||
if i == app.selected {
|
||||
Row::StyledData(item.into_iter(), selected_style)
|
||||
} else {
|
||||
Row::StyledData(item.into_iter(), normal_style)
|
||||
pub fn previous(&mut self) {
|
||||
let i = match self.state.selected() {
|
||||
Some(i) => {
|
||||
if i == 0 {
|
||||
self.items.len() - 1
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.margin(5)
|
||||
.split(app.size);
|
||||
Table::new(header.into_iter(), rows)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.widths(&[10, 10, 10])
|
||||
.render(&mut f, rects[0]);
|
||||
})
|
||||
None => 0,
|
||||
};
|
||||
self.state.select(Some(i));
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &mut app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Down => app.next(),
|
||||
KeyCode::Up => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
|
||||
let rects = Layout::default()
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.split(f.size());
|
||||
|
||||
let selected_style = Style::default().add_modifier(Modifier::REVERSED);
|
||||
let normal_style = Style::default().bg(Color::Blue);
|
||||
let header_cells = ["Header1", "Header2", "Header3"]
|
||||
.iter()
|
||||
.map(|h| Cell::from(*h).style(Style::default().fg(Color::Red)));
|
||||
let header = Row::new(header_cells)
|
||||
.style(normal_style)
|
||||
.height(1)
|
||||
.bottom_margin(1);
|
||||
let rows = app.items.iter().map(|item| {
|
||||
let height = item
|
||||
.iter()
|
||||
.map(|content| content.chars().filter(|c| *c == '\n').count())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
+ 1;
|
||||
let cells = item.iter().map(|c| Cell::from(*c));
|
||||
Row::new(cells).height(height as u16).bottom_margin(1)
|
||||
});
|
||||
let t = Table::new(rows)
|
||||
.header(header)
|
||||
.block(Block::default().borders(Borders::ALL).title("Table"))
|
||||
.highlight_style(selected_style)
|
||||
.highlight_symbol(">> ")
|
||||
.widths(&[
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Max(30),
|
||||
Constraint::Min(10),
|
||||
]);
|
||||
f.render_stateful_widget(t, rects[0], &mut app.state);
|
||||
}
|
||||
|
||||
15
examples/table.tape
Normal file
15
examples/table.tape
Normal file
@@ -0,0 +1,15 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/table.tape`
|
||||
Output "target/table.gif"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=table --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Down@1s 4
|
||||
Up@1s 2
|
||||
Down@1s 8
|
||||
Up@1s 12
|
||||
Sleep 5s
|
||||
204
examples/tabs.rs
204
examples/tabs.rs
@@ -1,112 +1,116 @@
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
use std::{error::Error, io};
|
||||
|
||||
mod util;
|
||||
use util::*;
|
||||
|
||||
use std::io;
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::MouseBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, Tabs, Widget};
|
||||
use tui::Terminal;
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
struct App<'a> {
|
||||
size: Rect,
|
||||
tabs: MyTabs<'a>,
|
||||
pub titles: Vec<&'a str>,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = MouseBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
|
||||
// App
|
||||
let mut app = App {
|
||||
size: Rect::default(),
|
||||
tabs: MyTabs {
|
||||
impl<'a> App<'a> {
|
||||
fn new() -> App<'a> {
|
||||
App {
|
||||
titles: vec!["Tab0", "Tab1", "Tab2", "Tab3"],
|
||||
selection: 0,
|
||||
},
|
||||
};
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &mut app).unwrap();
|
||||
|
||||
// Main loop
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let size = terminal.size().unwrap();
|
||||
if size != app.size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
index: 0,
|
||||
}
|
||||
|
||||
let evt = c.unwrap();
|
||||
match evt {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Right => app.tabs.next(),
|
||||
event::Key::Left => app.tabs.previous(),
|
||||
_ => {}
|
||||
}
|
||||
draw(&mut terminal, &mut app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.index = (self.index + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<MouseBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(5)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(app.size);
|
||||
|
||||
Block::default()
|
||||
.style(Style::default().bg(Color::White))
|
||||
.render(&mut f, app.size);
|
||||
Tabs::default()
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.titles(&app.tabs.titles)
|
||||
.select(app.tabs.selection)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(Style::default().fg(Color::Yellow))
|
||||
.render(&mut f, chunks[0]);
|
||||
match app.tabs.selection {
|
||||
0 => {
|
||||
Block::default()
|
||||
.title("Inner 0")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
1 => {
|
||||
Block::default()
|
||||
.title("Inner 1")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
2 => {
|
||||
Block::default()
|
||||
.title("Inner 2")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
3 => {
|
||||
Block::default()
|
||||
.title("Inner 3")
|
||||
.borders(Borders::ALL)
|
||||
.render(&mut f, chunks[1]);
|
||||
}
|
||||
_ => {}
|
||||
pub fn previous(&mut self) {
|
||||
if self.index > 0 {
|
||||
self.index -= 1;
|
||||
} else {
|
||||
self.index = self.titles.len() - 1;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// create app and run it
|
||||
let app = App::new();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return Ok(()),
|
||||
KeyCode::Right => app.next(),
|
||||
KeyCode::Left => app.previous(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let size = f.size();
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
|
||||
.split(size);
|
||||
|
||||
let block = Block::default().on_white().black();
|
||||
f.render_widget(block, size);
|
||||
let titles = app
|
||||
.titles
|
||||
.iter()
|
||||
.map(|t| {
|
||||
let (first, rest) = t.split_at(1);
|
||||
Line::from(vec![first.yellow(), rest.green()])
|
||||
})
|
||||
.collect();
|
||||
let tabs = Tabs::new(titles)
|
||||
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||
.select(app.index)
|
||||
.style(Style::default().fg(Color::Cyan))
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Black),
|
||||
);
|
||||
f.render_widget(tabs, chunks[0]);
|
||||
let inner = match app.index {
|
||||
0 => Block::default().title("Inner 0").borders(Borders::ALL),
|
||||
1 => Block::default().title("Inner 1").borders(Borders::ALL),
|
||||
2 => Block::default().title("Inner 2").borders(Borders::ALL),
|
||||
3 => Block::default().title("Inner 3").borders(Borders::ALL),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
f.render_widget(inner, chunks[1]);
|
||||
}
|
||||
|
||||
13
examples/tabs.tape
Normal file
13
examples/tabs.tape
Normal file
@@ -0,0 +1,13 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/tabs.tape`
|
||||
Output "target/tabs.gif"
|
||||
Set Width 1200
|
||||
Set Height 300
|
||||
Hide
|
||||
Type "cargo run --example=tabs --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Right@1s 4
|
||||
Left@1s 2
|
||||
Sleep 5s
|
||||
@@ -1,129 +1,251 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
/// A simple example demonstrating how to handle user input. This is
|
||||
/// a bit out of the scope of the library as it does not provide any
|
||||
/// input handling out of the box. However, it may helps some to get
|
||||
/// started.
|
||||
///
|
||||
/// This is a very simple example:
|
||||
/// * A input box always focused. Every character you type is registered
|
||||
/// here
|
||||
/// * Pressing Backspace erases a character
|
||||
/// * An input box always focused. Every character you type is registered
|
||||
/// here.
|
||||
/// * An entered character is inserted at the cursor position.
|
||||
/// * Pressing Backspace erases the left character before the cursor position
|
||||
/// * Pressing Enter pushes the current input in the history of previous
|
||||
/// messages
|
||||
extern crate termion;
|
||||
extern crate tui;
|
||||
/// messages.
|
||||
/// **Note: ** as this is a relatively simple example unicode characters are unsupported and
|
||||
/// their use will result in undefined behaviour.
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
use std::io;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
use termion::event;
|
||||
use termion::input::TermRead;
|
||||
|
||||
use tui::backend::AlternateScreenBackend;
|
||||
use tui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget};
|
||||
use tui::Terminal;
|
||||
enum InputMode {
|
||||
Normal,
|
||||
Editing,
|
||||
}
|
||||
|
||||
/// App holds the state of the application
|
||||
struct App {
|
||||
size: Rect,
|
||||
/// Current value of the input box
|
||||
input: String,
|
||||
/// Position of cursor in the editor area.
|
||||
cursor_position: usize,
|
||||
/// Current input mode
|
||||
input_mode: InputMode,
|
||||
/// History of recorded messages
|
||||
messages: Vec<String>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> App {
|
||||
impl Default for App {
|
||||
fn default() -> App {
|
||||
App {
|
||||
size: Rect::default(),
|
||||
input: String::new(),
|
||||
input_mode: InputMode::Normal,
|
||||
messages: Vec::new(),
|
||||
cursor_position: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Event {
|
||||
Input(event::Key),
|
||||
impl App {
|
||||
fn move_cursor_left(&mut self) {
|
||||
let cursor_moved_left = self.cursor_position.saturating_sub(1);
|
||||
self.cursor_position = self.clamp_cursor(cursor_moved_left);
|
||||
}
|
||||
|
||||
fn move_cursor_right(&mut self) {
|
||||
let cursor_moved_right = self.cursor_position.saturating_add(1);
|
||||
self.cursor_position = self.clamp_cursor(cursor_moved_right);
|
||||
}
|
||||
|
||||
fn enter_char(&mut self, new_char: char) {
|
||||
self.input.insert(self.cursor_position, new_char);
|
||||
|
||||
self.move_cursor_right();
|
||||
}
|
||||
|
||||
fn delete_char(&mut self) {
|
||||
let is_not_cursor_leftmost = self.cursor_position != 0;
|
||||
if is_not_cursor_leftmost {
|
||||
// Method "remove" is not used on the saved text for deleting the selected char.
|
||||
// Reason: Using remove on String works on bytes instead of the chars.
|
||||
// Using remove would require special care because of char boundaries.
|
||||
|
||||
let current_index = self.cursor_position;
|
||||
let from_left_to_current_index = current_index - 1;
|
||||
|
||||
// Getting all characters before the selected character.
|
||||
let before_char_to_delete = self.input.chars().take(from_left_to_current_index);
|
||||
// Getting all characters after selected character.
|
||||
let after_char_to_delete = self.input.chars().skip(current_index);
|
||||
|
||||
// Put all characters together except the selected one.
|
||||
// By leaving the selected one out, it is forgotten and therefore deleted.
|
||||
self.input = before_char_to_delete.chain(after_char_to_delete).collect();
|
||||
self.move_cursor_left();
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
|
||||
new_cursor_pos.clamp(0, self.input.len())
|
||||
}
|
||||
|
||||
fn reset_cursor(&mut self) {
|
||||
self.cursor_position = 0;
|
||||
}
|
||||
|
||||
fn submit_message(&mut self) {
|
||||
self.messages.push(self.input.clone());
|
||||
self.input.clear();
|
||||
self.reset_cursor();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Terminal initialization
|
||||
let backend = AlternateScreenBackend::new().unwrap();
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// setup terminal
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
// Channels
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let input_tx = tx.clone();
|
||||
// create app and run it
|
||||
let app = App::default();
|
||||
let res = run_app(&mut terminal, app);
|
||||
|
||||
// Input
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for c in stdin.keys() {
|
||||
let evt = c.unwrap();
|
||||
input_tx.send(Event::Input(evt)).unwrap();
|
||||
if evt == event::Key::Char('q') {
|
||||
break;
|
||||
// restore terminal
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
if let Err(err) = res {
|
||||
println!("{err:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| ui(f, &app))?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match app.input_mode {
|
||||
InputMode::Normal => match key.code {
|
||||
KeyCode::Char('e') => {
|
||||
app.input_mode = InputMode::Editing;
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
return Ok(());
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Editing if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Enter => app.submit_message(),
|
||||
KeyCode::Char(to_insert) => {
|
||||
app.enter_char(to_insert);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
app.delete_char();
|
||||
}
|
||||
KeyCode::Left => {
|
||||
app.move_cursor_left();
|
||||
}
|
||||
KeyCode::Right => {
|
||||
app.move_cursor_right();
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.input_mode = InputMode::Normal;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// App
|
||||
let mut app = App::new();
|
||||
fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
|
||||
// First draw call
|
||||
terminal.clear().unwrap();
|
||||
terminal.hide_cursor().unwrap();
|
||||
app.size = terminal.size().unwrap();
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
let (msg, style) = match app.input_mode {
|
||||
InputMode::Normal => (
|
||||
vec![
|
||||
"Press ".into(),
|
||||
"q".bold(),
|
||||
" to exit, ".into(),
|
||||
"e".bold(),
|
||||
" to start editing.".bold(),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::RAPID_BLINK),
|
||||
),
|
||||
InputMode::Editing => (
|
||||
vec![
|
||||
"Press ".into(),
|
||||
"Esc".bold(),
|
||||
" to stop editing, ".into(),
|
||||
"Enter".bold(),
|
||||
" to record the message".into(),
|
||||
],
|
||||
Style::default(),
|
||||
),
|
||||
};
|
||||
let mut text = Text::from(Line::from(msg));
|
||||
text.patch_style(style);
|
||||
let help_message = Paragraph::new(text);
|
||||
f.render_widget(help_message, chunks[0]);
|
||||
|
||||
loop {
|
||||
let size = terminal.size().unwrap();
|
||||
if app.size != size {
|
||||
terminal.resize(size).unwrap();
|
||||
app.size = size;
|
||||
let input = Paragraph::new(app.input.as_str())
|
||||
.style(match app.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(Color::Yellow),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"));
|
||||
f.render_widget(input, chunks[1]);
|
||||
match app.input_mode {
|
||||
InputMode::Normal =>
|
||||
// Hide the cursor. `Frame` does this by default, so we don't need to do anything here
|
||||
{}
|
||||
|
||||
InputMode::Editing => {
|
||||
// Make the cursor visible and ask ratatui to put it at the specified coordinates after
|
||||
// rendering
|
||||
f.set_cursor(
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position is can be controlled via the left and right arrow key
|
||||
chunks[1].x + app.cursor_position as u16 + 1,
|
||||
// Move one line down, from the border to the input line
|
||||
chunks[1].y + 1,
|
||||
)
|
||||
}
|
||||
|
||||
let evt = rx.recv().unwrap();
|
||||
match evt {
|
||||
Event::Input(input) => match input {
|
||||
event::Key::Char('q') => {
|
||||
break;
|
||||
}
|
||||
event::Key::Char('\n') => {
|
||||
app.messages.push(app.input.drain(..).collect());
|
||||
}
|
||||
event::Key::Char(c) => {
|
||||
app.input.push(c);
|
||||
}
|
||||
event::Key::Backspace => {
|
||||
app.input.pop();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
draw(&mut terminal, &app).unwrap();
|
||||
}
|
||||
|
||||
terminal.show_cursor().unwrap();
|
||||
terminal.clear().unwrap();
|
||||
}
|
||||
|
||||
fn draw(t: &mut Terminal<AlternateScreenBackend>, app: &App) -> Result<(), io::Error> {
|
||||
t.draw(|mut f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
|
||||
.split(app.size);
|
||||
Paragraph::new([Text::raw(&app.input)].iter())
|
||||
.style(Style::default().fg(Color::Yellow))
|
||||
.block(Block::default().borders(Borders::ALL).title("Input"))
|
||||
.render(&mut f, chunks[0]);
|
||||
List::new(
|
||||
app.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| Text::raw(format!("{}: {}", i, m))),
|
||||
).block(Block::default().borders(Borders::ALL).title("Messages"))
|
||||
.render(&mut f, chunks[1]);
|
||||
})
|
||||
let messages: Vec<ListItem> = app
|
||||
.messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let content = Line::from(Span::raw(format!("{i}: {m}")));
|
||||
ListItem::new(content)
|
||||
})
|
||||
.collect();
|
||||
let messages =
|
||||
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
|
||||
f.render_widget(messages, chunks[2]);
|
||||
}
|
||||
|
||||
21
examples/user_input.tape
Normal file
21
examples/user_input.tape
Normal file
@@ -0,0 +1,21 @@
|
||||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/user_input.tape`
|
||||
Output "target/user_input.gif"
|
||||
Set Width 1200
|
||||
Set Height 600
|
||||
Hide
|
||||
Type "cargo run --example=user_input --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Show
|
||||
Sleep 2s
|
||||
Type e
|
||||
Sleep 1s
|
||||
Type "Hello, world!"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Backspace 13
|
||||
Sleep 1s
|
||||
Type "Goodbye, world!"
|
||||
Enter
|
||||
Sleep 5s
|
||||
@@ -1,74 +0,0 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
extern crate rand;
|
||||
|
||||
use self::rand::distributions::{IndependentSample, Range};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RandomSignal {
|
||||
range: Range<u64>,
|
||||
rng: rand::ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
pub fn new(lower: u64, upper: u64) -> RandomSignal {
|
||||
RandomSignal {
|
||||
range: Range::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.range.ind_sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SinSignal {
|
||||
x: f64,
|
||||
interval: f64,
|
||||
period: f64,
|
||||
scale: f64,
|
||||
}
|
||||
|
||||
impl SinSignal {
|
||||
pub fn new(interval: f64, period: f64, scale: f64) -> SinSignal {
|
||||
SinSignal {
|
||||
x: 0.0,
|
||||
interval,
|
||||
period,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SinSignal {
|
||||
type Item = (f64, f64);
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let point = (self.x, (self.x * 1.0 / self.period).sin() * self.scale);
|
||||
self.x += self.interval;
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MyTabs<'a> {
|
||||
pub titles: Vec<&'a str>,
|
||||
pub selection: usize,
|
||||
}
|
||||
|
||||
impl<'a> MyTabs<'a> {
|
||||
pub fn next(&mut self) {
|
||||
self.selection = (self.selection + 1) % self.titles.len();
|
||||
}
|
||||
|
||||
pub fn previous(&mut self) {
|
||||
if self.selection > 0 {
|
||||
self.selection -= 1;
|
||||
} else {
|
||||
self.selection = self.titles.len() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
rustfmt.toml
Normal file
5
rustfmt.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
# configuration for https://rust-lang.github.io/rustfmt/
|
||||
group_imports = "StdExternalCrate"
|
||||
imports_granularity = "Crate"
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build all examples in examples directory
|
||||
|
||||
set -u -o pipefail
|
||||
|
||||
for file in examples/*.rs; do
|
||||
name=$(basename ${file//.rs/})
|
||||
echo "[EXAMPLE] $name"
|
||||
if [[ "$name" == "rustbox" ]]; then
|
||||
cargo build --features rustbox --example "$name"
|
||||
else
|
||||
cargo build --example "$name"
|
||||
fi
|
||||
done
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run all examples in examples directory
|
||||
|
||||
set -u -o pipefail
|
||||
|
||||
for file in examples/*.rs; do
|
||||
name=$(basename ${file//.rs/})
|
||||
if [[ "$name" == "rustbox" ]]; then
|
||||
cargo run --features rustbox --example "$name"
|
||||
else
|
||||
cargo run --example "$name"
|
||||
fi
|
||||
done
|
||||
@@ -1,128 +1,282 @@
|
||||
extern crate crossterm;
|
||||
//! This module provides the `CrosstermBackend` implementation for the `Backend` trait.
|
||||
//! It uses the `crossterm` crate to interact with the terminal.
|
||||
//!
|
||||
//!
|
||||
//! [`Backend`]: trait.Backend.html
|
||||
//! [`CrosstermBackend`]: struct.CrosstermBackend.html
|
||||
|
||||
use std::io;
|
||||
use std::io::{self, Write};
|
||||
|
||||
use backend::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier};
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||
SetForegroundColor, SetUnderlineColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
|
||||
pub struct CrosstermBackend {
|
||||
screen: crossterm::Screen,
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// A backend implementation using the `crossterm` crate.
|
||||
///
|
||||
/// The `CrosstermBackend` struct is a wrapper around a type implementing `Write`, which
|
||||
/// is used to send commands to the terminal. It provides methods for drawing content,
|
||||
/// manipulating the cursor, and clearing the terminal screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::backend::{Backend, CrosstermBackend};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let buffer = std::io::stdout();
|
||||
/// let mut backend = CrosstermBackend::new(buffer);
|
||||
/// backend.clear()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
buffer: W,
|
||||
}
|
||||
|
||||
impl CrosstermBackend {
|
||||
pub fn new() -> CrosstermBackend {
|
||||
CrosstermBackend {
|
||||
screen: crossterm::Screen::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn screen(&self) -> &crossterm::Screen {
|
||||
&self.screen
|
||||
impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Creates a new `CrosstermBackend` with the given buffer.
|
||||
pub fn new(buffer: W) -> CrosstermBackend<W> {
|
||||
CrosstermBackend { buffer }
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for CrosstermBackend {
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
let terminal = crossterm::terminal::terminal(&self.screen);
|
||||
terminal.clear(crossterm::terminal::ClearType::All);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
let cursor = crossterm::cursor(&self.screen);
|
||||
cursor.hide();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
let cursor = crossterm::cursor(&self.screen);
|
||||
cursor.show();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = crossterm::terminal::terminal(&self.screen);
|
||||
let (width, height) = terminal.terminal_size();
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
impl<W> Write for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Writes a buffer of bytes to the underlying buffer.
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.buffer.write(buf)
|
||||
}
|
||||
|
||||
/// Flushes the underlying buffer.
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Backend for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let cursor = crossterm::cursor(&self.screen);
|
||||
let mut last_y = 0;
|
||||
let mut last_x = 0;
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
if y != last_y || x != last_x + 1 {
|
||||
cursor.goto(x + 1, y + 1);
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
map_error(queue!(self.buffer, MoveTo(x, y)))?;
|
||||
}
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.buffer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg {
|
||||
let color = CColor::from(cell.fg);
|
||||
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.bg != bg {
|
||||
let color = CColor::from(cell.bg);
|
||||
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
|
||||
bg = cell.bg;
|
||||
}
|
||||
if cell.underline_color != underline_color {
|
||||
let color = CColor::from(cell.underline_color);
|
||||
map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
last_x = x;
|
||||
last_y = y;
|
||||
|
||||
let mut s = crossterm::style(&cell.symbol);
|
||||
if let Some(color) = cell.style.fg.into() {
|
||||
s = s.with(color)
|
||||
}
|
||||
if let Some(color) = cell.style.bg.into() {
|
||||
s = s.on(color)
|
||||
}
|
||||
if let Some(attr) = cell.style.modifier.into() {
|
||||
s = s.attr(attr)
|
||||
}
|
||||
s.paint(&self.screen);
|
||||
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
|
||||
}
|
||||
|
||||
map_error(queue!(
|
||||
self.buffer,
|
||||
SetForegroundColor(CColor::Reset),
|
||||
SetBackgroundColor(CColor::Reset),
|
||||
SetUnderlineColor(CColor::Reset),
|
||||
SetAttribute(CAttribute::Reset)
|
||||
))
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Hide))
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, Show))
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
crossterm::cursor::position()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
map_error(execute!(self.buffer, MoveTo(x, y)))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
map_error(execute!(
|
||||
self.buffer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
map_error(queue!(self.buffer, Print("\n")))?;
|
||||
}
|
||||
self.buffer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let (width, height) =
|
||||
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
Ok(Rect::new(0, 0, width, height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
|
||||
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
impl From<Color> for CColor {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
Color::Reset => CColor::Reset,
|
||||
Color::Black => CColor::Black,
|
||||
Color::Red => CColor::DarkRed,
|
||||
Color::Green => CColor::DarkGreen,
|
||||
Color::Yellow => CColor::DarkYellow,
|
||||
Color::Blue => CColor::DarkBlue,
|
||||
Color::Magenta => CColor::DarkMagenta,
|
||||
Color::Cyan => CColor::DarkCyan,
|
||||
Color::Gray => CColor::Grey,
|
||||
Color::DarkGray => CColor::DarkGrey,
|
||||
Color::LightRed => CColor::Red,
|
||||
Color::LightGreen => CColor::Green,
|
||||
Color::LightBlue => CColor::Blue,
|
||||
Color::LightYellow => CColor::Yellow,
|
||||
Color::LightMagenta => CColor::Magenta,
|
||||
Color::LightCyan => CColor::Cyan,
|
||||
Color::White => CColor::White,
|
||||
Color::Indexed(i) => CColor::AnsiValue(i),
|
||||
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Option<crossterm::Color> {
|
||||
fn from(color: Color) -> Option<crossterm::Color> {
|
||||
match color {
|
||||
Color::Reset => None,
|
||||
Color::Black => Some(crossterm::Color::Black),
|
||||
Color::Red => Some(crossterm::Color::DarkRed),
|
||||
Color::Green => Some(crossterm::Color::DarkGreen),
|
||||
Color::Yellow => Some(crossterm::Color::DarkYellow),
|
||||
Color::Blue => Some(crossterm::Color::DarkBlue),
|
||||
Color::Magenta => Some(crossterm::Color::DarkMagenta),
|
||||
Color::Cyan => Some(crossterm::Color::DarkCyan),
|
||||
Color::Gray => Some(crossterm::Color::Grey),
|
||||
Color::DarkGray => Some(crossterm::Color::Grey),
|
||||
Color::LightRed => Some(crossterm::Color::Red),
|
||||
Color::LightGreen => Some(crossterm::Color::Green),
|
||||
Color::LightBlue => Some(crossterm::Color::Blue),
|
||||
Color::LightYellow => Some(crossterm::Color::Yellow),
|
||||
Color::LightMagenta => Some(crossterm::Color::Magenta),
|
||||
Color::LightCyan => Some(crossterm::Color::Cyan),
|
||||
Color::White => Some(crossterm::Color::White),
|
||||
Color::Rgb(r, g, b) => Some(crossterm::Color::Rgb { r, g, b }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Modifier> for Option<crossterm::Attribute> {
|
||||
fn from(modifier: Modifier) -> Option<crossterm::Attribute> {
|
||||
match modifier {
|
||||
Modifier::Blink => Some(crossterm::Attribute::SlowBlink),
|
||||
Modifier::Bold => Some(crossterm::Attribute::Bold),
|
||||
Modifier::CrossedOut => Some(crossterm::Attribute::CrossedOut),
|
||||
Modifier::Faint => Some(crossterm::Attribute::Dim),
|
||||
Modifier::Invert => Some(crossterm::Attribute::Reverse),
|
||||
Modifier::Italic => Some(crossterm::Attribute::Italic),
|
||||
Modifier::Underline => Some(crossterm::Attribute::Underlined),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,117 @@
|
||||
//! This module provides the backend implementations for different terminal libraries.
|
||||
//! It defines the [`Backend`] trait which is used to abstract over the specific
|
||||
//! terminal library being used.
|
||||
//!
|
||||
//! The following terminal libraries are supported:
|
||||
//! - Crossterm (with the `crossterm` feature)
|
||||
//! - Termion (with the `termion` feature)
|
||||
//! - Termwiz (with the `termwiz` feature)
|
||||
//!
|
||||
//! Additionally, a [`TestBackend`] is provided for testing purposes.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui::backend::{Backend, CrosstermBackend};
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let buffer = std::io::stdout();
|
||||
//! let mut backend = CrosstermBackend::new(buffer);
|
||||
//! backend.clear()?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! [`Backend`]: trait.Backend.html
|
||||
//! [`TestBackend`]: struct.TestBackend.html
|
||||
|
||||
use std::io;
|
||||
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
|
||||
#[cfg(feature = "rustbox")]
|
||||
mod rustbox;
|
||||
#[cfg(feature = "rustbox")]
|
||||
pub use self::rustbox::RustboxBackend;
|
||||
use crate::{buffer::Cell, layout::Rect};
|
||||
|
||||
#[cfg(feature = "termion")]
|
||||
mod termion;
|
||||
#[cfg(feature = "termion")]
|
||||
pub use self::termion::{AlternateScreenBackend, MouseBackend, RawBackend, TermionBackend};
|
||||
pub use self::termion::TermionBackend;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use self::termwiz::TermwizBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
/// Enum representing the different types of clearing operations that can be performed
|
||||
/// on the terminal screen.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ClearType {
|
||||
All,
|
||||
AfterCursor,
|
||||
BeforeCursor,
|
||||
CurrentLine,
|
||||
UntilNewLine,
|
||||
}
|
||||
|
||||
/// The `Backend` trait provides an abstraction over different terminal libraries.
|
||||
/// It defines the methods required to draw content, manipulate the cursor, and
|
||||
/// clear the terminal screen.
|
||||
pub trait Backend {
|
||||
/// Draw the given content to the terminal screen.
|
||||
///
|
||||
/// The content is provided as an iterator over `(u16, u16, &Cell)` tuples,
|
||||
/// where the first two elements represent the x and y coordinates, and the
|
||||
/// third element is a reference to the [`Cell`] to be drawn.
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||
|
||||
/// Insert `n` line breaks to the terminal screen.
|
||||
///
|
||||
/// This method is optional and may not be implemented by all backends.
|
||||
fn append_lines(&mut self, _n: u16) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hide the cursor on the terminal screen.
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||
|
||||
/// Show the cursor on the terminal screen.
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error>;
|
||||
|
||||
/// Get the current cursor position on the terminal screen.
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||
|
||||
/// Set the cursor position on the terminal screen to the given x and y coordinates.
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||
|
||||
/// Clears the whole terminal screen
|
||||
fn clear(&mut self) -> Result<(), io::Error>;
|
||||
|
||||
/// Clears a specific region of the terminal specified by the [`ClearType`] parameter
|
||||
///
|
||||
/// This method is optional and may not be implemented by all backends.
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), io::Error> {
|
||||
match clear_type {
|
||||
ClearType::All => self.clear(),
|
||||
ClearType::AfterCursor
|
||||
| ClearType::BeforeCursor
|
||||
| ClearType::CurrentLine
|
||||
| ClearType::UntilNewLine => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("clear_type [{clear_type:?}] not supported with this backend"),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the size of the terminal screen as a [`Rect`].
|
||||
fn size(&self) -> Result<Rect, io::Error>;
|
||||
|
||||
/// Flush any buffered content to the terminal screen.
|
||||
fn flush(&mut self) -> Result<(), io::Error>;
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
extern crate rustbox;
|
||||
|
||||
use std::io;
|
||||
|
||||
use super::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier};
|
||||
|
||||
pub struct RustboxBackend {
|
||||
rustbox: rustbox::RustBox,
|
||||
}
|
||||
|
||||
impl RustboxBackend {
|
||||
pub fn new() -> Result<RustboxBackend, rustbox::InitError> {
|
||||
let rustbox = try!(rustbox::RustBox::init(Default::default()));
|
||||
Ok(RustboxBackend { rustbox: rustbox })
|
||||
}
|
||||
|
||||
pub fn with_rustbox(instance: rustbox::RustBox) -> RustboxBackend {
|
||||
RustboxBackend { rustbox: instance }
|
||||
}
|
||||
|
||||
pub fn rustbox(&self) -> &rustbox::RustBox {
|
||||
&self.rustbox
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for RustboxBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut inst = 0;
|
||||
for (x, y, cell) in content {
|
||||
inst += 1;
|
||||
self.rustbox.print(
|
||||
x as usize,
|
||||
y as usize,
|
||||
cell.style.modifier.into(),
|
||||
cell.style.fg.into(),
|
||||
cell.style.bg.into(),
|
||||
&cell.symbol,
|
||||
);
|
||||
}
|
||||
debug!("{} instructions outputed", inst);
|
||||
Ok(())
|
||||
}
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.rustbox.clear();
|
||||
Ok(())
|
||||
}
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: self.rustbox.width() as u16,
|
||||
height: self.rustbox.height() as u16,
|
||||
})
|
||||
}
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
self.rustbox.present();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn rgb_to_byte(r: u8, g: u8, b: u8) -> u16 {
|
||||
u16::from((r & 0xC0) + ((g & 0xE0) >> 2) + ((b & 0xE0) >> 5))
|
||||
}
|
||||
|
||||
impl Into<rustbox::Color> for Color {
|
||||
fn into(self) -> rustbox::Color {
|
||||
match self {
|
||||
Color::Reset => rustbox::Color::Default,
|
||||
Color::Black | Color::Gray | Color::DarkGray => rustbox::Color::Black,
|
||||
Color::Red | Color::LightRed => rustbox::Color::Red,
|
||||
Color::Green | Color::LightGreen => rustbox::Color::Green,
|
||||
Color::Yellow | Color::LightYellow => rustbox::Color::Yellow,
|
||||
Color::Magenta | Color::LightMagenta => rustbox::Color::Magenta,
|
||||
Color::Cyan | Color::LightCyan => rustbox::Color::Cyan,
|
||||
Color::White => rustbox::Color::White,
|
||||
Color::Blue | Color::LightBlue => rustbox::Color::Blue,
|
||||
Color::Rgb(r, g, b) => rustbox::Color::Byte(rgb_to_byte(r, g, b)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<rustbox::Style> for Modifier {
|
||||
fn into(self) -> rustbox::Style {
|
||||
match self {
|
||||
Modifier::Bold => rustbox::RB_BOLD,
|
||||
Modifier::Underline => rustbox::RB_UNDERLINE,
|
||||
Modifier::Invert => rustbox::RB_REVERSE,
|
||||
_ => rustbox::RB_NORMAL,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,37 @@
|
||||
extern crate termion;
|
||||
//! This module provides the `TermionBackend` implementation for the [`Backend`] trait.
|
||||
//! It uses the Termion crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: crate::backend::Backend
|
||||
//! [`TermionBackend`]: crate::backend::TermionBackend
|
||||
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use self::termion::raw::IntoRawMode;
|
||||
|
||||
use super::Backend;
|
||||
use buffer::Cell;
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier, Style};
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// A backend that uses the Termion library to draw content, manipulate the cursor,
|
||||
/// and clear the terminal screen.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::backend::{Backend, TermionBackend};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let stdout = std::io::stdout();
|
||||
/// let mut backend = TermionBackend::new(stdout);
|
||||
/// backend.clear()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
@@ -17,40 +39,12 @@ where
|
||||
stdout: W,
|
||||
}
|
||||
|
||||
pub type RawBackend = TermionBackend<termion::raw::RawTerminal<io::Stdout>>;
|
||||
pub type MouseBackend =
|
||||
TermionBackend<termion::input::MouseTerminal<termion::raw::RawTerminal<io::Stdout>>>;
|
||||
pub type AlternateScreenBackend =
|
||||
TermionBackend<termion::screen::AlternateScreen<termion::raw::RawTerminal<io::Stdout>>>;
|
||||
|
||||
impl RawBackend {
|
||||
pub fn new() -> Result<RawBackend, io::Error> {
|
||||
let raw = io::stdout().into_raw_mode()?;
|
||||
Ok(TermionBackend::with_stdout(raw))
|
||||
}
|
||||
}
|
||||
|
||||
impl MouseBackend {
|
||||
pub fn new() -> Result<MouseBackend, io::Error> {
|
||||
let raw = io::stdout().into_raw_mode()?;
|
||||
let mouse = termion::input::MouseTerminal::from(raw);
|
||||
Ok(TermionBackend::with_stdout(mouse))
|
||||
}
|
||||
}
|
||||
|
||||
impl AlternateScreenBackend {
|
||||
pub fn new() -> Result<AlternateScreenBackend, io::Error> {
|
||||
let raw = io::stdout().into_raw_mode()?;
|
||||
let screen = termion::screen::AlternateScreen::from(raw);
|
||||
Ok(TermionBackend::with_stdout(screen))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
pub fn with_stdout(stdout: W) -> TermionBackend<W> {
|
||||
/// Creates a new Termion backend with the given output.
|
||||
pub fn new(stdout: W) -> TermionBackend<W> {
|
||||
TermionBackend { stdout }
|
||||
}
|
||||
}
|
||||
@@ -72,186 +66,233 @@ impl<W> Backend for TermionBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Clears the entire screen and move the cursor to the top left of the screen
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::clear::All)?;
|
||||
write!(self.stdout, "{}", termion::cursor::Goto(1, 1))?;
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
match clear_type {
|
||||
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
|
||||
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
|
||||
ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?,
|
||||
ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?,
|
||||
ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?,
|
||||
};
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||
for _ in 0..n {
|
||||
writeln!(self.stdout)?;
|
||||
}
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
/// Hides cursor
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Hide)?;
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
/// Shows cursor
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Show)?;
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
termion::cursor::DetectCursorPos::cursor_pos(&mut self.stdout).map(|(x, y)| (x - 1, y - 1))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
write!(self.stdout, "{}", termion::cursor::Goto(x + 1, y + 1))?;
|
||||
self.stdout.flush()
|
||||
}
|
||||
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut string = String::with_capacity(content.size_hint().0 * 3);
|
||||
let mut style = Style::default();
|
||||
let mut last_y = 0;
|
||||
let mut last_x = 0;
|
||||
let mut inst = 0;
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<(u16, u16)> = None;
|
||||
for (x, y, cell) in content {
|
||||
if y != last_y || x != last_x + 1 {
|
||||
string.push_str(&format!("{}", termion::cursor::Goto(x + 1, y + 1)));
|
||||
inst += 1;
|
||||
// Move the cursor if the previous location was not (x - 1, y)
|
||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||
write!(string, "{}", termion::cursor::Goto(x + 1, y + 1)).unwrap();
|
||||
}
|
||||
last_x = x;
|
||||
last_y = y;
|
||||
if cell.style.modifier != style.modifier {
|
||||
string.push_str(&cell.style.modifier.termion_modifier());
|
||||
style.modifier = cell.style.modifier;
|
||||
if style.modifier == Modifier::Reset {
|
||||
style.bg = Color::Reset;
|
||||
style.fg = Color::Reset;
|
||||
}
|
||||
inst += 1;
|
||||
last_pos = Some((x, y));
|
||||
if cell.modifier != modifier {
|
||||
write!(
|
||||
string,
|
||||
"{}",
|
||||
ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier
|
||||
}
|
||||
)
|
||||
.unwrap();
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.style.fg != style.fg {
|
||||
string.push_str(&cell.style.fg.termion_fg());
|
||||
style.fg = cell.style.fg;
|
||||
inst += 1;
|
||||
if cell.fg != fg {
|
||||
write!(string, "{}", Fg(cell.fg)).unwrap();
|
||||
fg = cell.fg;
|
||||
}
|
||||
if cell.style.bg != style.bg {
|
||||
string.push_str(&cell.style.bg.termion_bg());
|
||||
style.bg = cell.style.bg;
|
||||
inst += 1;
|
||||
if cell.bg != bg {
|
||||
write!(string, "{}", Bg(cell.bg)).unwrap();
|
||||
bg = cell.bg;
|
||||
}
|
||||
string.push_str(&cell.symbol);
|
||||
inst += 1;
|
||||
}
|
||||
debug!("{} instructions outputed.", inst);
|
||||
write!(
|
||||
self.stdout,
|
||||
"{}{}{}{}",
|
||||
string,
|
||||
Color::Reset.termion_fg(),
|
||||
Color::Reset.termion_bg(),
|
||||
Modifier::Reset.termion_modifier()
|
||||
"{string}{}{}{}",
|
||||
Fg(Color::Reset),
|
||||
Bg(Color::Reset),
|
||||
termion::style::Reset,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the size of the terminal
|
||||
fn size(&self) -> io::Result<Rect> {
|
||||
let terminal = try!(termion::terminal_size());
|
||||
Ok(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: terminal.0,
|
||||
height: terminal.1,
|
||||
})
|
||||
let terminal = termion::terminal_size()?;
|
||||
Ok(Rect::new(0, 0, terminal.0, terminal.1))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.stdout.flush()
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct Fg(Color);
|
||||
|
||||
macro_rules! termion_fg {
|
||||
($color:ident) => {
|
||||
format!("{}", termion::color::Fg(termion::color::$color))
|
||||
};
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct Bg(Color);
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct ModifierDiff {
|
||||
from: Modifier,
|
||||
to: Modifier,
|
||||
}
|
||||
|
||||
macro_rules! termion_fg_rgb {
|
||||
($r:expr, $g:expr, $b:expr) => {
|
||||
format!("{}", termion::color::Fg(termion::color::Rgb($r, $g, $b)))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! termion_bg {
|
||||
($color:ident) => {
|
||||
format!("{}", termion::color::Bg(termion::color::$color))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! termion_bg_rgb {
|
||||
($r:expr, $g:expr, $b:expr) => {
|
||||
format!("{}", termion::color::Bg(termion::color::Rgb($r, $g, $b)))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! termion_modifier {
|
||||
($style:ident) => {
|
||||
format!("{}", termion::style::$style)
|
||||
};
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn termion_fg(self) -> String {
|
||||
match self {
|
||||
Color::Reset => termion_fg!(Reset),
|
||||
Color::Black => termion_fg!(Black),
|
||||
Color::Red => termion_fg!(Red),
|
||||
Color::Green => termion_fg!(Green),
|
||||
Color::Yellow => termion_fg!(Yellow),
|
||||
Color::Blue => termion_fg!(Blue),
|
||||
Color::Magenta => termion_fg!(Magenta),
|
||||
Color::Cyan => termion_fg!(Cyan),
|
||||
Color::Gray => termion_fg!(White),
|
||||
Color::DarkGray => termion_fg!(LightBlack),
|
||||
Color::LightRed => termion_fg!(LightRed),
|
||||
Color::LightGreen => termion_fg!(LightGreen),
|
||||
Color::LightBlue => termion_fg!(LightBlue),
|
||||
Color::LightYellow => termion_fg!(LightYellow),
|
||||
Color::LightMagenta => termion_fg!(LightMagenta),
|
||||
Color::LightCyan => termion_fg!(LightCyan),
|
||||
Color::White => termion_fg!(LightWhite),
|
||||
Color::Rgb(r, g, b) => termion_fg_rgb!(r, g, b),
|
||||
impl fmt::Display for Fg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_fg(f),
|
||||
Color::Black => termion::color::Black.write_fg(f),
|
||||
Color::Red => termion::color::Red.write_fg(f),
|
||||
Color::Green => termion::color::Green.write_fg(f),
|
||||
Color::Yellow => termion::color::Yellow.write_fg(f),
|
||||
Color::Blue => termion::color::Blue.write_fg(f),
|
||||
Color::Magenta => termion::color::Magenta.write_fg(f),
|
||||
Color::Cyan => termion::color::Cyan.write_fg(f),
|
||||
Color::Gray => termion::color::White.write_fg(f),
|
||||
Color::DarkGray => termion::color::LightBlack.write_fg(f),
|
||||
Color::LightRed => termion::color::LightRed.write_fg(f),
|
||||
Color::LightGreen => termion::color::LightGreen.write_fg(f),
|
||||
Color::LightBlue => termion::color::LightBlue.write_fg(f),
|
||||
Color::LightYellow => termion::color::LightYellow.write_fg(f),
|
||||
Color::LightMagenta => termion::color::LightMagenta.write_fg(f),
|
||||
Color::LightCyan => termion::color::LightCyan.write_fg(f),
|
||||
Color::White => termion::color::LightWhite.write_fg(f),
|
||||
Color::Indexed(i) => termion::color::AnsiValue(i).write_fg(f),
|
||||
Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_fg(f),
|
||||
}
|
||||
}
|
||||
pub fn termion_bg(self) -> String {
|
||||
match self {
|
||||
Color::Reset => termion_bg!(Reset),
|
||||
Color::Black => termion_bg!(Black),
|
||||
Color::Red => termion_bg!(Red),
|
||||
Color::Green => termion_bg!(Green),
|
||||
Color::Yellow => termion_bg!(Yellow),
|
||||
Color::Blue => termion_bg!(Blue),
|
||||
Color::Magenta => termion_bg!(Magenta),
|
||||
Color::Cyan => termion_bg!(Cyan),
|
||||
Color::Gray => termion_bg!(White),
|
||||
Color::DarkGray => termion_bg!(LightBlack),
|
||||
Color::LightRed => termion_bg!(LightRed),
|
||||
Color::LightGreen => termion_bg!(LightGreen),
|
||||
Color::LightBlue => termion_bg!(LightBlue),
|
||||
Color::LightYellow => termion_bg!(LightYellow),
|
||||
Color::LightMagenta => termion_bg!(LightMagenta),
|
||||
Color::LightCyan => termion_bg!(LightCyan),
|
||||
Color::White => termion_bg!(LightWhite),
|
||||
Color::Rgb(r, g, b) => termion_bg_rgb!(r, g, b),
|
||||
}
|
||||
impl fmt::Display for Bg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use termion::color::Color as TermionColor;
|
||||
match self.0 {
|
||||
Color::Reset => termion::color::Reset.write_bg(f),
|
||||
Color::Black => termion::color::Black.write_bg(f),
|
||||
Color::Red => termion::color::Red.write_bg(f),
|
||||
Color::Green => termion::color::Green.write_bg(f),
|
||||
Color::Yellow => termion::color::Yellow.write_bg(f),
|
||||
Color::Blue => termion::color::Blue.write_bg(f),
|
||||
Color::Magenta => termion::color::Magenta.write_bg(f),
|
||||
Color::Cyan => termion::color::Cyan.write_bg(f),
|
||||
Color::Gray => termion::color::White.write_bg(f),
|
||||
Color::DarkGray => termion::color::LightBlack.write_bg(f),
|
||||
Color::LightRed => termion::color::LightRed.write_bg(f),
|
||||
Color::LightGreen => termion::color::LightGreen.write_bg(f),
|
||||
Color::LightBlue => termion::color::LightBlue.write_bg(f),
|
||||
Color::LightYellow => termion::color::LightYellow.write_bg(f),
|
||||
Color::LightMagenta => termion::color::LightMagenta.write_bg(f),
|
||||
Color::LightCyan => termion::color::LightCyan.write_bg(f),
|
||||
Color::White => termion::color::LightWhite.write_bg(f),
|
||||
Color::Indexed(i) => termion::color::AnsiValue(i).write_bg(f),
|
||||
Color::Rgb(r, g, b) => termion::color::Rgb(r, g, b).write_bg(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Modifier {
|
||||
pub fn termion_modifier(self) -> String {
|
||||
match self {
|
||||
Modifier::Blink => termion_modifier!(Blink),
|
||||
Modifier::Bold => termion_modifier!(Bold),
|
||||
Modifier::CrossedOut => termion_modifier!(CrossedOut),
|
||||
Modifier::Faint => termion_modifier!(Faint),
|
||||
Modifier::Framed => termion_modifier!(Framed),
|
||||
Modifier::Invert => termion_modifier!(Invert),
|
||||
Modifier::Italic => termion_modifier!(Italic),
|
||||
Modifier::NoBlink => termion_modifier!(NoBlink),
|
||||
Modifier::NoBold => termion_modifier!(NoBold),
|
||||
Modifier::NoCrossedOut => termion_modifier!(NoCrossedOut),
|
||||
Modifier::NoFaint => termion_modifier!(NoFaint),
|
||||
Modifier::NoInvert => termion_modifier!(NoInvert),
|
||||
Modifier::NoItalic => termion_modifier!(NoItalic),
|
||||
Modifier::NoUnderline => termion_modifier!(NoUnderline),
|
||||
Modifier::Reset => termion_modifier!(Reset),
|
||||
Modifier::Underline => termion_modifier!(Underline),
|
||||
impl fmt::Display for ModifierDiff {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let remove = self.from - self.to;
|
||||
if remove.contains(Modifier::REVERSED) {
|
||||
write!(f, "{}", termion::style::NoInvert)?;
|
||||
}
|
||||
if remove.contains(Modifier::BOLD) {
|
||||
// XXX: the termion NoBold flag actually enables double-underline on ECMA-48 compliant
|
||||
// terminals, and NoFaint additionally disables bold... so we use this trick to get
|
||||
// the right semantics.
|
||||
write!(f, "{}", termion::style::NoFaint)?;
|
||||
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::Faint)?;
|
||||
}
|
||||
}
|
||||
if remove.contains(Modifier::ITALIC) {
|
||||
write!(f, "{}", termion::style::NoItalic)?;
|
||||
}
|
||||
if remove.contains(Modifier::UNDERLINED) {
|
||||
write!(f, "{}", termion::style::NoUnderline)?;
|
||||
}
|
||||
if remove.contains(Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::NoFaint)?;
|
||||
|
||||
// XXX: the NoFaint flag additionally disables bold as well, so we need to re-enable it
|
||||
// here if we want it.
|
||||
if self.to.contains(Modifier::BOLD) {
|
||||
write!(f, "{}", termion::style::Bold)?;
|
||||
}
|
||||
}
|
||||
if remove.contains(Modifier::CROSSED_OUT) {
|
||||
write!(f, "{}", termion::style::NoCrossedOut)?;
|
||||
}
|
||||
if remove.contains(Modifier::SLOW_BLINK) || remove.contains(Modifier::RAPID_BLINK) {
|
||||
write!(f, "{}", termion::style::NoBlink)?;
|
||||
}
|
||||
|
||||
let add = self.to - self.from;
|
||||
if add.contains(Modifier::REVERSED) {
|
||||
write!(f, "{}", termion::style::Invert)?;
|
||||
}
|
||||
if add.contains(Modifier::BOLD) {
|
||||
write!(f, "{}", termion::style::Bold)?;
|
||||
}
|
||||
if add.contains(Modifier::ITALIC) {
|
||||
write!(f, "{}", termion::style::Italic)?;
|
||||
}
|
||||
if add.contains(Modifier::UNDERLINED) {
|
||||
write!(f, "{}", termion::style::Underline)?;
|
||||
}
|
||||
if add.contains(Modifier::DIM) {
|
||||
write!(f, "{}", termion::style::Faint)?;
|
||||
}
|
||||
if add.contains(Modifier::CROSSED_OUT) {
|
||||
write!(f, "{}", termion::style::CrossedOut)?;
|
||||
}
|
||||
if add.contains(Modifier::SLOW_BLINK) || add.contains(Modifier::RAPID_BLINK) {
|
||||
write!(f, "{}", termion::style::Blink)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
223
src/backend/termwiz.rs
Normal file
223
src/backend/termwiz.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
//! This module provides the `TermwizBackend` implementation for the [`Backend`] trait.
|
||||
//! It uses the `termwiz` crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: trait.Backend.html
|
||||
//! [`TermwizBackend`]: crate::backend::TermionBackend
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position},
|
||||
terminal::{buffered::BufferedTerminal, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::Cell,
|
||||
layout::Rect,
|
||||
style::{Color, Modifier},
|
||||
};
|
||||
|
||||
/// Termwiz backend implementation for the [`Backend`] trait.
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use ratatui::backend::{Backend, TermwizBackend};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut backend = TermwizBackend::new()?;
|
||||
/// backend.clear()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub struct TermwizBackend {
|
||||
buffered_terminal: BufferedTerminal<SystemTerminal>,
|
||||
}
|
||||
|
||||
impl TermwizBackend {
|
||||
/// Creates a new Termwiz backend instance.
|
||||
pub fn new() -> Result<TermwizBackend, Box<dyn Error>> {
|
||||
let mut buffered_terminal =
|
||||
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
|
||||
buffered_terminal.terminal().set_raw_mode()?;
|
||||
buffered_terminal.terminal().enter_alternate_screen()?;
|
||||
Ok(TermwizBackend { buffered_terminal })
|
||||
}
|
||||
|
||||
/// Creates a new Termwiz backend instance with the given buffered terminal.
|
||||
pub fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> TermwizBackend {
|
||||
TermwizBackend {
|
||||
buffered_terminal: instance,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the buffered terminal used by the backend.
|
||||
pub fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
|
||||
&self.buffered_terminal
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the buffered terminal used by the backend.
|
||||
pub fn buffered_terminal_mut(&mut self) -> &mut BufferedTerminal<SystemTerminal> {
|
||||
&mut self.buffered_terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TermwizBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, cell) in content {
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
},
|
||||
Change::Attribute(AttributeChange::Foreground(cell.fg.into())),
|
||||
Change::Attribute(AttributeChange::Background(cell.bg.into())),
|
||||
]);
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Intensity(
|
||||
if cell.modifier.contains(Modifier::BOLD) {
|
||||
Intensity::Bold
|
||||
} else if cell.modifier.contains(Modifier::DIM) {
|
||||
Intensity::Half
|
||||
} else {
|
||||
Intensity::Normal
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Italic(
|
||||
cell.modifier.contains(Modifier::ITALIC),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Underline(
|
||||
if cell.modifier.contains(Modifier::UNDERLINED) {
|
||||
Underline::Single
|
||||
} else {
|
||||
Underline::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Reverse(
|
||||
cell.modifier.contains(Modifier::REVERSED),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Invisible(
|
||||
cell.modifier.contains(Modifier::HIDDEN),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::StrikeThrough(
|
||||
cell.modifier.contains(Modifier::CROSSED_OUT),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Blink(
|
||||
if cell.modifier.contains(Modifier::SLOW_BLINK) {
|
||||
Blink::Slow
|
||||
} else if cell.modifier.contains(Modifier::RAPID_BLINK) {
|
||||
Blink::Rapid
|
||||
} else {
|
||||
Blink::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal.add_change(&cell.symbol);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
let (x, y) = self.buffered_terminal.cursor_position();
|
||||
Ok((x as u16, y as u16))
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.buffered_terminal.add_change(Change::CursorPosition {
|
||||
x: Position::Absolute(x as usize),
|
||||
y: Position::Absolute(y as usize),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
let (term_width, term_height) = self.buffered_terminal.dimensions();
|
||||
let max = u16::max_value();
|
||||
Ok(Rect::new(
|
||||
0,
|
||||
0,
|
||||
if term_width > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_width as u16
|
||||
},
|
||||
if term_height > usize::from(max) {
|
||||
max
|
||||
} else {
|
||||
term_height as u16
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
self.buffered_terminal
|
||||
.flush()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for ColorAttribute {
|
||||
fn from(color: Color) -> ColorAttribute {
|
||||
match color {
|
||||
Color::Reset => ColorAttribute::Default,
|
||||
Color::Black => AnsiColor::Black.into(),
|
||||
Color::Gray | Color::DarkGray => AnsiColor::Grey.into(),
|
||||
Color::Red => AnsiColor::Maroon.into(),
|
||||
Color::LightRed => AnsiColor::Red.into(),
|
||||
Color::Green => AnsiColor::Green.into(),
|
||||
Color::LightGreen => AnsiColor::Lime.into(),
|
||||
Color::Yellow => AnsiColor::Olive.into(),
|
||||
Color::LightYellow => AnsiColor::Yellow.into(),
|
||||
Color::Magenta => AnsiColor::Purple.into(),
|
||||
Color::LightMagenta => AnsiColor::Fuchsia.into(),
|
||||
Color::Cyan => AnsiColor::Teal.into(),
|
||||
Color::LightCyan => AnsiColor::Aqua.into(),
|
||||
Color::White => AnsiColor::White.into(),
|
||||
Color::Blue => AnsiColor::Navy.into(),
|
||||
Color::LightBlue => AnsiColor::Blue.into(),
|
||||
Color::Indexed(i) => ColorAttribute::PaletteIndex(i),
|
||||
Color::Rgb(r, g, b) => {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
183
src/backend/test.rs
Normal file
183
src/backend/test.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
//! This module provides the `TestBackend` implementation for the [`Backend`] trait.
|
||||
//! It is used in the integration tests to verify the correctness of the library.
|
||||
|
||||
use std::{
|
||||
fmt::{Display, Write},
|
||||
io,
|
||||
};
|
||||
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
backend::Backend,
|
||||
buffer::{Buffer, Cell},
|
||||
layout::Rect,
|
||||
};
|
||||
|
||||
/// A backend used for the integration tests.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{backend::{Backend, TestBackend}, buffer::Buffer};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let mut backend = TestBackend::new(10, 2);
|
||||
/// backend.clear()?;
|
||||
/// backend.assert_buffer(&Buffer::with_lines(vec![" "; 2]));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestBackend {
|
||||
width: u16,
|
||||
buffer: Buffer,
|
||||
height: u16,
|
||||
cursor: bool,
|
||||
pos: (u16, u16),
|
||||
}
|
||||
|
||||
/// Returns a string representation of the given buffer for debugging purpose.
|
||||
///
|
||||
/// This function is used to visualize the buffer content in a human-readable format.
|
||||
/// It iterates through the buffer content and appends each cell's symbol to the view string.
|
||||
/// If a cell is hidden by a multi-width symbol, it is added to the overwritten vector and
|
||||
/// displayed at the end of the line.
|
||||
fn buffer_view(buffer: &Buffer) -> String {
|
||||
let mut view = String::with_capacity(buffer.content.len() + buffer.area.height as usize * 3);
|
||||
for cells in buffer.content.chunks(buffer.area.width as usize) {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
view.push('"');
|
||||
for (x, c) in cells.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
view.push_str(&c.symbol);
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
}
|
||||
view.push('"');
|
||||
if !overwritten.is_empty() {
|
||||
write!(&mut view, " Hidden by multi-width symbols: {overwritten:?}").unwrap();
|
||||
}
|
||||
view.push('\n');
|
||||
}
|
||||
view
|
||||
}
|
||||
|
||||
impl TestBackend {
|
||||
/// Creates a new TestBackend with the specified width and height.
|
||||
pub fn new(width: u16, height: u16) -> TestBackend {
|
||||
TestBackend {
|
||||
width,
|
||||
height,
|
||||
buffer: Buffer::empty(Rect::new(0, 0, width, height)),
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal buffer of the TestBackend.
|
||||
pub fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Resizes the TestBackend to the specified width and height.
|
||||
pub fn resize(&mut self, width: u16, height: u16) {
|
||||
self.buffer.resize(Rect::new(0, 0, width, height));
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
/// Asserts that the TestBackend's buffer is equal to the expected buffer.
|
||||
/// If the buffers are not equal, a panic occurs with a detailed error message
|
||||
/// showing the differences between the expected and actual buffers.
|
||||
pub fn assert_buffer(&self, expected: &Buffer) {
|
||||
assert_eq!(expected.area, self.buffer.area);
|
||||
let diff = expected.diff(&self.buffer);
|
||||
if diff.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut debug_info = String::from("Buffers are not equal");
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Expected:");
|
||||
debug_info.push('\n');
|
||||
let expected_view = buffer_view(expected);
|
||||
debug_info.push_str(&expected_view);
|
||||
debug_info.push('\n');
|
||||
debug_info.push_str("Got:");
|
||||
debug_info.push('\n');
|
||||
let view = buffer_view(&self.buffer);
|
||||
debug_info.push_str(&view);
|
||||
debug_info.push('\n');
|
||||
|
||||
debug_info.push_str("Diff:");
|
||||
debug_info.push('\n');
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
format!("{i}: at ({x}, {y}) expected {expected_cell:?} got {cell:?}")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
debug_info.push_str(&nice_diff);
|
||||
panic!("{debug_info}");
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TestBackend {
|
||||
/// Formats the TestBackend for display by calling the buffer_view function
|
||||
/// on its internal buffer.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", buffer_view(&self.buffer))
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TestBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, c) in content {
|
||||
let cell = self.buffer.get_mut(x, y);
|
||||
*cell = c.clone();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error> {
|
||||
Ok(self.pos)
|
||||
}
|
||||
|
||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error> {
|
||||
self.pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.buffer.reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> Result<Rect, io::Error> {
|
||||
Ok(Rect::new(0, 0, self.width, self.height))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
787
src/buffer.rs
787
src/buffer.rs
@@ -1,16 +1,27 @@
|
||||
use std::cmp::min;
|
||||
use std::usize;
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use layout::Rect;
|
||||
use style::{Color, Modifier, Style};
|
||||
#[allow(deprecated)]
|
||||
use crate::{
|
||||
layout::Rect,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Spans},
|
||||
};
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Cell {
|
||||
pub symbol: String,
|
||||
pub style: Style,
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub underline_color: Color,
|
||||
pub modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
@@ -27,29 +38,58 @@ impl Cell {
|
||||
}
|
||||
|
||||
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
|
||||
self.style.fg = color;
|
||||
self.fg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
|
||||
self.style.bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_modifier(&mut self, modifier: Modifier) -> &mut Cell {
|
||||
self.style.modifier = modifier;
|
||||
self.bg = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, style: Style) -> &mut Cell {
|
||||
self.style = style;
|
||||
if let Some(c) = style.fg {
|
||||
self.fg = c;
|
||||
}
|
||||
if let Some(c) = style.bg {
|
||||
self.bg = c;
|
||||
}
|
||||
#[cfg(feature = "crossterm")]
|
||||
if let Some(c) = style.underline_color {
|
||||
self.underline_color = c;
|
||||
}
|
||||
self.modifier.insert(style.add_modifier);
|
||||
self.modifier.remove(style.sub_modifier);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub fn style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.underline_color(self.underline_color)
|
||||
.add_modifier(self.modifier)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
pub fn style(&self) -> Style {
|
||||
Style::default()
|
||||
.fg(self.fg)
|
||||
.bg(self.bg)
|
||||
.add_modifier(self.modifier)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.symbol.clear();
|
||||
self.symbol.push(' ');
|
||||
self.style.reset();
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
#[cfg(feature = "crossterm")]
|
||||
{
|
||||
self.underline_color = Color::Reset;
|
||||
}
|
||||
self.modifier = Modifier::empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +97,11 @@ impl Default for Cell {
|
||||
fn default() -> Cell {
|
||||
Cell {
|
||||
symbol: " ".into(),
|
||||
style: Default::default(),
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
#[cfg(feature = "crossterm")]
|
||||
underline_color: Color::Reset,
|
||||
modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,28 +116,26 @@ impl Default for Cell {
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// use tui::buffer::{Buffer, Cell};
|
||||
/// use tui::layout::Rect;
|
||||
/// use tui::style::{Color, Style, Modifier};
|
||||
/// use ratatui::buffer::{Buffer, Cell};
|
||||
/// use ratatui::layout::Rect;
|
||||
/// use ratatui::style::{Color, Style, Modifier};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
|
||||
/// buf.get_mut(0, 2).set_symbol("x");
|
||||
/// assert_eq!(buf.get(0, 2).symbol, "x");
|
||||
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
|
||||
/// assert_eq!(buf.get(5, 0), &Cell{
|
||||
/// symbol: String::from("r"),
|
||||
/// style: Style {
|
||||
/// fg: Color::Red,
|
||||
/// bg: Color::White,
|
||||
/// modifier: Modifier::Reset
|
||||
/// }});
|
||||
/// fg: Color::Red,
|
||||
/// bg: Color::White,
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// underline_color: Color::Reset,
|
||||
/// modifier: Modifier::empty()
|
||||
/// });
|
||||
/// buf.get_mut(5, 0).set_char('x');
|
||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Default, Clone, Eq, PartialEq)]
|
||||
pub struct Buffer {
|
||||
/// The area represented by this buffer
|
||||
pub area: Rect,
|
||||
@@ -102,19 +144,10 @@ pub struct Buffer {
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl Default for Buffer {
|
||||
fn default() -> Buffer {
|
||||
Buffer {
|
||||
area: Default::default(),
|
||||
content: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
pub fn empty(area: Rect) -> Buffer {
|
||||
let cell: Cell = Default::default();
|
||||
let cell = Cell::default();
|
||||
Buffer::filled(area, &cell)
|
||||
}
|
||||
|
||||
@@ -128,6 +161,29 @@ impl Buffer {
|
||||
Buffer { area, content }
|
||||
}
|
||||
|
||||
/// Returns a Buffer containing the given lines
|
||||
pub fn with_lines<S>(lines: Vec<S>) -> Buffer
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let height = lines.len() as u16;
|
||||
let width = lines
|
||||
.iter()
|
||||
.map(|i| i.as_ref().width() as u16)
|
||||
.max()
|
||||
.unwrap_or_default();
|
||||
let mut buffer = Buffer::empty(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
for (y, line) in lines.iter().enumerate() {
|
||||
buffer.set_string(0, y as u16, line, Style::default());
|
||||
}
|
||||
buffer
|
||||
}
|
||||
|
||||
/// Returns the content of the buffer as a slice
|
||||
pub fn content(&self) -> &[Cell] {
|
||||
&self.content
|
||||
@@ -150,15 +206,15 @@ impl Buffer {
|
||||
&mut self.content[i]
|
||||
}
|
||||
|
||||
/// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
|
||||
/// Returns the index in the `Vec<Cell>` for the given global (x, y) coordinates.
|
||||
///
|
||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
@@ -170,8 +226,8 @@ impl Buffer {
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
@@ -184,9 +240,7 @@ impl Buffer {
|
||||
&& x < self.area.right()
|
||||
&& y >= self.area.top()
|
||||
&& y < self.area.bottom(),
|
||||
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
|
||||
x,
|
||||
y,
|
||||
"Trying to access position outside the buffer: x={x}, y={y}, area={:?}",
|
||||
self.area
|
||||
);
|
||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
||||
@@ -199,8 +253,8 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
@@ -212,8 +266,8 @@ impl Buffer {
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use tui::buffer::Buffer;
|
||||
/// # use tui::layout::Rect;
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
@@ -222,13 +276,12 @@ impl Buffer {
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={} len={}",
|
||||
i,
|
||||
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
|
||||
self.content.len()
|
||||
);
|
||||
(
|
||||
self.area.x + i as u16 % self.area.width,
|
||||
self.area.y + i as u16 / self.area.width,
|
||||
self.area.x + (i as u16) % self.area.width,
|
||||
self.area.y + (i as u16) / self.area.width,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -242,18 +295,109 @@ impl Buffer {
|
||||
|
||||
/// Print at most the first n characters of a string if enough space is available
|
||||
/// until the end of the line
|
||||
pub fn set_stringn<S>(&mut self, x: u16, y: u16, string: S, limit: usize, style: Style)
|
||||
pub fn set_stringn<S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
y: u16,
|
||||
string: S,
|
||||
width: usize,
|
||||
style: Style,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let mut index = self.index_of(x, y);
|
||||
let mut x_offset = x as usize;
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_index = min((self.area.right() - x) as usize, limit);
|
||||
for s in graphemes.take(max_index) {
|
||||
self.content[index].symbol.clear();
|
||||
self.content[index].symbol.push_str(s);
|
||||
self.content[index].style = style;
|
||||
index += 1;
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
for s in graphemes {
|
||||
let width = s.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
||||
// change dimensions to usize or u32 and someone resizes the terminal to 1x2^32.
|
||||
if width > max_offset.saturating_sub(x_offset) {
|
||||
break;
|
||||
}
|
||||
|
||||
self.content[index].set_symbol(s);
|
||||
self.content[index].set_style(style);
|
||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
||||
for i in index + 1..index + width {
|
||||
self.content[i].reset();
|
||||
}
|
||||
index += width;
|
||||
x_offset += width;
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[deprecated(note = "Use `Buffer::set_line` instead")]
|
||||
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &spans.0 {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
pub fn set_line(&mut self, x: u16, y: u16, line: &Line<'_>, width: u16) -> (u16, u16) {
|
||||
let mut remaining_width = width;
|
||||
let mut x = x;
|
||||
for span in &line.spans {
|
||||
if remaining_width == 0 {
|
||||
break;
|
||||
}
|
||||
let pos = self.set_stringn(
|
||||
x,
|
||||
y,
|
||||
span.content.as_ref(),
|
||||
remaining_width as usize,
|
||||
span.style,
|
||||
);
|
||||
let w = pos.0.saturating_sub(x);
|
||||
x = pos.0;
|
||||
remaining_width = remaining_width.saturating_sub(w);
|
||||
}
|
||||
(x, y)
|
||||
}
|
||||
|
||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
||||
}
|
||||
|
||||
#[deprecated(
|
||||
since = "0.10.0",
|
||||
note = "You should use styling capabilities of `Buffer::set_style`"
|
||||
)]
|
||||
pub fn set_background(&mut self, area: Rect, color: Color) {
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_bg(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, area: Rect, style: Style) {
|
||||
for y in area.top()..area.bottom() {
|
||||
for x in area.left()..area.right() {
|
||||
self.get_mut(x, y).set_style(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +408,7 @@ impl Buffer {
|
||||
if self.content.len() > length {
|
||||
self.content.truncate(length);
|
||||
} else {
|
||||
self.content.resize(length, Default::default());
|
||||
self.content.resize(length, Cell::default());
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
@@ -279,42 +423,293 @@ impl Buffer {
|
||||
/// Merge an other buffer into this one
|
||||
pub fn merge(&mut self, other: &Buffer) {
|
||||
let area = self.area.union(other.area);
|
||||
let cell: Cell = Default::default();
|
||||
let cell = Cell::default();
|
||||
self.content.resize(area.area() as usize, cell.clone());
|
||||
|
||||
// Move original content to the appropriate space
|
||||
let offset_x = self.area.x - area.x;
|
||||
let offset_y = self.area.y - area.y;
|
||||
let size = self.area.area() as usize;
|
||||
for i in (0..size).rev() {
|
||||
let (x, y) = self.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
self.content[k] = self.content[i].clone();
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
if i != k {
|
||||
self.content[k] = self.content[i].clone();
|
||||
self.content[i] = cell.clone();
|
||||
}
|
||||
}
|
||||
|
||||
// Push content of the other buffer into this one (may erase previous
|
||||
// data)
|
||||
let offset_x = other.area.x - area.x;
|
||||
let offset_y = other.area.y - area.y;
|
||||
let size = other.area.area() as usize;
|
||||
for i in 0..size {
|
||||
let (x, y) = other.pos_of(i);
|
||||
// New index in content
|
||||
let k = ((y + offset_y) * area.width + (x + offset_x)) as usize;
|
||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
||||
self.content[k] = other.content[i].clone();
|
||||
}
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
|
||||
/// self to other.
|
||||
///
|
||||
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
|
||||
/// a non-blank cell.
|
||||
///
|
||||
/// # Multi-width characters handling:
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `コ`
|
||||
/// Next: `aa`
|
||||
/// Updates: `0: a, 1: a'
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `01`
|
||||
/// Prev: `a `
|
||||
/// Next: `コ`
|
||||
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
|
||||
/// ```
|
||||
///
|
||||
/// ```text
|
||||
/// (Index:) `012`
|
||||
/// Prev: `aaa`
|
||||
/// Next: `aコ`
|
||||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
||||
/// ```
|
||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
|
||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||
// Cells invalidated by drawing/replacing preceding multi-width characters:
|
||||
let mut invalidated: usize = 0;
|
||||
// Cells from the current buffer to skip due to preceding multi-width characters taking
|
||||
// their place (the skipped cells should be blank anyway):
|
||||
let mut to_skip: usize = 0;
|
||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||
if (current != previous || invalidated > 0) && to_skip == 0 {
|
||||
let (x, y) = self.pos_of(i);
|
||||
updates.push((x, y, &next_buffer[i]));
|
||||
}
|
||||
|
||||
to_skip = current.symbol.width().saturating_sub(1);
|
||||
|
||||
let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
|
||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
||||
}
|
||||
updates
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that two buffers are equal by comparing their areas and content.
|
||||
///
|
||||
/// On panic, displays the areas or the content and a diff of the contents.
|
||||
#[macro_export]
|
||||
macro_rules! assert_buffer_eq {
|
||||
($actual_expr:expr, $expected_expr:expr) => {
|
||||
match (&$actual_expr, &$expected_expr) {
|
||||
(actual, expected) => {
|
||||
if actual.area != expected.area {
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer areas not equal
|
||||
expected: {:?}
|
||||
actual: {:?}"
|
||||
),
|
||||
expected, actual
|
||||
);
|
||||
}
|
||||
let diff = expected.diff(&actual);
|
||||
if !diff.is_empty() {
|
||||
let nice_diff = diff
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (x, y, cell))| {
|
||||
let expected_cell = expected.get(*x, *y);
|
||||
indoc::formatdoc! {"
|
||||
{i}: at ({x}, {y})
|
||||
expected: {expected_cell:?}
|
||||
actual: {cell:?}
|
||||
"}
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
panic!(
|
||||
indoc::indoc!(
|
||||
"
|
||||
buffer contents not equal
|
||||
expected: {:?}
|
||||
actual: {:?}
|
||||
diff:
|
||||
{}"
|
||||
),
|
||||
expected, actual, nice_diff
|
||||
);
|
||||
}
|
||||
// shouldn't get here, but this guards against future behavior
|
||||
// that changes equality but not area or content
|
||||
assert_eq!(actual, expected, "buffers not equal");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Debug for Buffer {
|
||||
/// Writes a debug representation of the buffer to the given formatter.
|
||||
///
|
||||
/// The format is like a pretty printed struct, with the following fields:
|
||||
/// * `area`: displayed as `Rect { x: 1, y: 2, width: 3, height: 4 }`
|
||||
/// * `content`: displayed as a list of strings representing the content of the buffer
|
||||
/// * `styles`: displayed as a list of: `{ x: 1, y: 2, fg: Color::Red, bg: Color::Blue,
|
||||
/// modifier: Modifier::BOLD }` only showing a value when there is a change in style.
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.write_fmt(format_args!(
|
||||
"Buffer {{\n area: {:?},\n content: [\n",
|
||||
&self.area
|
||||
))?;
|
||||
let mut last_style = None;
|
||||
let mut styles = vec![];
|
||||
for (y, line) in self.content.chunks(self.area.width as usize).enumerate() {
|
||||
let mut overwritten = vec![];
|
||||
let mut skip: usize = 0;
|
||||
f.write_str(" \"")?;
|
||||
for (x, c) in line.iter().enumerate() {
|
||||
if skip == 0 {
|
||||
f.write_str(&c.symbol)?;
|
||||
} else {
|
||||
overwritten.push((x, &c.symbol));
|
||||
}
|
||||
skip = std::cmp::max(skip, c.symbol.width()).saturating_sub(1);
|
||||
#[cfg(feature = "crossterm")]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.underline_color, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.underline_color, c.modifier));
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
{
|
||||
let style = (c.fg, c.bg, c.modifier);
|
||||
if last_style != Some(style) {
|
||||
last_style = Some(style);
|
||||
styles.push((x, y, c.fg, c.bg, c.modifier));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !overwritten.is_empty() {
|
||||
f.write_fmt(format_args!(
|
||||
"// hidden by multi-width symbols: {overwritten:?}"
|
||||
))?;
|
||||
}
|
||||
f.write_str("\",\n")?;
|
||||
}
|
||||
f.write_str(" ],\n styles: [\n")?;
|
||||
for s in styles {
|
||||
#[cfg(feature = "crossterm")]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, underline: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4, s.5
|
||||
))?;
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
f.write_fmt(format_args!(
|
||||
" x: {}, y: {}, fg: {:?}, bg: {:?}, modifier: {:?},\n",
|
||||
s.0, s.1, s.2, s.3, s.4
|
||||
))?;
|
||||
}
|
||||
f.write_str(" ]\n}")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cell(s: &str) -> Cell {
|
||||
let mut cell = Cell::default();
|
||||
cell.set_symbol(s);
|
||||
cell
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_implements_debug() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 2));
|
||||
buf.set_string(0, 0, "Hello World!", Style::default());
|
||||
buf.set_string(
|
||||
0,
|
||||
1,
|
||||
"G'day World!",
|
||||
Style::default()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
#[cfg(feature = "crossterm")]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, underline: Reset, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
#[cfg(not(feature = "crossterm"))]
|
||||
assert_eq!(
|
||||
format!("{buf:?}"),
|
||||
indoc::indoc!(
|
||||
"
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 12, height: 2 },
|
||||
content: [
|
||||
\"Hello World!\",
|
||||
\"G'day World!\",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Green, bg: Yellow, modifier: BOLD,
|
||||
]
|
||||
}"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_area() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let other_buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[should_panic]
|
||||
#[test]
|
||||
fn assert_buffer_eq_panics_on_unequal_style() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
let mut other_buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
other_buffer.set_string(0, 0, " ", Style::default().fg(Color::Red));
|
||||
assert_buffer_eq!(buffer, other_buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_translates_to_and_from_coordinates() {
|
||||
let rect = Rect::new(200, 100, 50, 80);
|
||||
@@ -348,4 +743,256 @@ mod tests {
|
||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
||||
buf.index_of(10, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Zero-width
|
||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" "]));
|
||||
|
||||
buffer.set_string(0, 0, "aaa", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["aaa "]));
|
||||
|
||||
// Width limit:
|
||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["bbbb "]));
|
||||
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// Width truncation:
|
||||
buffer.set_string(0, 0, "123456", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345"]));
|
||||
|
||||
// multi-line
|
||||
buffer = Buffer::empty(Rect::new(0, 0, 5, 2));
|
||||
buffer.set_string(0, 0, "12345", Style::default());
|
||||
buffer.set_string(0, 1, "67890", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["12345", "67890"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_multi_width_overwrite() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// multi-width overwrite
|
||||
buffer.set_string(0, 0, "aaaaa", Style::default());
|
||||
buffer.set_string(0, 0, "称号", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["称号a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_zero_width() {
|
||||
let area = Rect::new(0, 0, 1, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
// Leading grapheme with zero width
|
||||
let s = "\u{1}a";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
|
||||
// Trailing grapheme with zero with
|
||||
let s = "a\u{1}";
|
||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["a"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_set_string_double_width() {
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
buffer.set_string(0, 0, "コン", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
|
||||
// Only 1 space left.
|
||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
||||
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["コン "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_with_lines() {
|
||||
let buffer =
|
||||
Buffer::with_lines(vec!["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
|
||||
assert_eq!(buffer.area.x, 0);
|
||||
assert_eq!(buffer.area.y, 0);
|
||||
assert_eq!(buffer.area.width, 10);
|
||||
assert_eq!(buffer.area.height, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_empty_empty() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::empty(area);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_empty_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::empty(area);
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff.len(), 40 * 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_filled_filled() {
|
||||
let area = Rect::new(0, 0, 40, 40);
|
||||
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(diff, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_single_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌Title─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
" ",
|
||||
"┌TITLE─┐ ",
|
||||
"│ │ ",
|
||||
"│ │ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(2, 1, &cell("I")),
|
||||
(3, 1, &cell("T")),
|
||||
(4, 1, &cell("L")),
|
||||
(5, 1, &cell("E")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn buffer_diffing_multi_width() {
|
||||
let prev = Buffer::with_lines(vec![
|
||||
"┌Title─┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let next = Buffer::with_lines(vec![
|
||||
"┌称号──┐ ",
|
||||
"└──────┘ ",
|
||||
]);
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![
|
||||
(1, 0, &cell("称")),
|
||||
// Skipped "i"
|
||||
(3, 0, &cell("号")),
|
||||
// Skipped "l"
|
||||
(5, 0, &cell("─")),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_diffing_multi_width_offset() {
|
||||
let prev = Buffer::with_lines(vec!["┌称号──┐"]);
|
||||
let next = Buffer::with_lines(vec!["┌─称号─┐"]);
|
||||
|
||||
let diff = prev.diff(&next);
|
||||
assert_eq!(
|
||||
diff,
|
||||
vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(one, Buffer::with_lines(vec!["11", "11", "22", "22"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge2() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
assert_buffer_eq!(
|
||||
one,
|
||||
Buffer::with_lines(vec!["22 ", "22 ", " 11", " 11"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_merge3() {
|
||||
let mut one = Buffer::filled(
|
||||
Rect {
|
||||
x: 3,
|
||||
y: 3,
|
||||
width: 2,
|
||||
height: 2,
|
||||
},
|
||||
Cell::default().set_symbol("1"),
|
||||
);
|
||||
let two = Buffer::filled(
|
||||
Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 3,
|
||||
height: 4,
|
||||
},
|
||||
Cell::default().set_symbol("2"),
|
||||
);
|
||||
one.merge(&two);
|
||||
let mut merged = Buffer::with_lines(vec!["222 ", "222 ", "2221", "2221"]);
|
||||
merged.area = Rect {
|
||||
x: 1,
|
||||
y: 1,
|
||||
width: 4,
|
||||
height: 4,
|
||||
};
|
||||
assert_buffer_eq!(one, merged);
|
||||
}
|
||||
}
|
||||
|
||||
466
src/layout.rs
466
src/layout.rs
@@ -1,64 +1,117 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::HashMap;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
cmp::{max, min},
|
||||
collections::HashMap,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use cassowary::strength::{REQUIRED, WEAK};
|
||||
use cassowary::WeightedRelation::*;
|
||||
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
|
||||
use cassowary::{
|
||||
strength::{MEDIUM, REQUIRED, WEAK},
|
||||
Constraint as CassowaryConstraint, Expression, Solver, Variable,
|
||||
WeightedRelation::{EQ, GE, LE},
|
||||
};
|
||||
|
||||
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Hash, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
#[default]
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomRight,
|
||||
BottomLeft,
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Direction {
|
||||
Horizontal,
|
||||
#[default]
|
||||
Vertical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum Constraint {
|
||||
// TODO: enforce range 0 - 100
|
||||
Percentage(u16),
|
||||
Ratio(u32, u32),
|
||||
Length(u16),
|
||||
Max(u16),
|
||||
Min(u16),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
impl Default for Constraint {
|
||||
fn default() -> Self {
|
||||
Constraint::Percentage(100)
|
||||
}
|
||||
}
|
||||
|
||||
impl Constraint {
|
||||
pub fn apply(&self, length: u16) -> u16 {
|
||||
match *self {
|
||||
Constraint::Percentage(p) => {
|
||||
let p = p as f32 / 100.0;
|
||||
let length = length as f32;
|
||||
(p * length).min(length) as u16
|
||||
}
|
||||
Constraint::Ratio(numerator, denominator) => {
|
||||
// avoid division by zero by using 1 when denominator is 0
|
||||
// this results in 0/0 -> 0 and x/0 -> x for x != 0
|
||||
let percentage = numerator as f32 / denominator.max(1) as f32;
|
||||
let length = length as f32;
|
||||
(percentage * length).min(length) as u16
|
||||
}
|
||||
Constraint::Length(l) => length.min(l),
|
||||
Constraint::Max(m) => length.min(m),
|
||||
Constraint::Min(m) => length.max(m),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Margin {
|
||||
pub vertical: u16,
|
||||
pub horizontal: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Alignment {
|
||||
#[default]
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
// TODO: enforce constraints size once const generics has landed
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct Layout {
|
||||
direction: Direction,
|
||||
margin: u16,
|
||||
margin: Margin,
|
||||
constraints: Vec<Constraint>,
|
||||
/// Whether the last chunk of the computed layout should be expanded to fill the available
|
||||
/// space.
|
||||
expand_to_fill: bool,
|
||||
}
|
||||
|
||||
type Cache = HashMap<(Rect, Layout), Rc<[Rect]>>;
|
||||
thread_local! {
|
||||
static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
|
||||
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
impl Default for Layout {
|
||||
fn default() -> Layout {
|
||||
Layout {
|
||||
direction: Direction::Vertical,
|
||||
margin: 0,
|
||||
constraints: Vec::new(),
|
||||
}
|
||||
Layout::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
pub const fn new() -> Layout {
|
||||
Layout {
|
||||
direction: Direction::Vertical,
|
||||
margin: Margin {
|
||||
horizontal: 0,
|
||||
vertical: 0,
|
||||
},
|
||||
constraints: Vec::new(),
|
||||
expand_to_fill: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn constraints<C>(mut self, constraints: C) -> Layout
|
||||
where
|
||||
C: Into<Vec<Constraint>>,
|
||||
@@ -67,46 +120,106 @@ impl Layout {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn margin(mut self, margin: u16) -> Layout {
|
||||
self.margin = margin;
|
||||
pub const fn margin(mut self, margin: u16) -> Layout {
|
||||
self.margin = Margin {
|
||||
horizontal: margin,
|
||||
vertical: margin,
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn direction(mut self, direction: Direction) -> Layout {
|
||||
pub const fn horizontal_margin(mut self, horizontal: u16) -> Layout {
|
||||
self.margin.horizontal = horizontal;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn vertical_margin(mut self, vertical: u16) -> Layout {
|
||||
self.margin.vertical = vertical;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn direction(mut self, direction: Direction) -> Layout {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) const fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
|
||||
self.expand_to_fill = expand_to_fill;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wrapper function around the cassowary-rs solver to be able to split a given
|
||||
/// area into smaller ones based on the preferred widths or heights and the direction.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # extern crate tui;
|
||||
/// # use tui::layout::{Rect, Constraint, Direction, Layout};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
||||
/// .split(Rect{x: 2, y: 2, width: 10, height: 10});
|
||||
/// assert_eq!(chunks, vec![Rect{x:2, y: 2, width: 10, height: 5},
|
||||
/// Rect{x: 2, y: 7, width: 10, height: 5}])
|
||||
/// # }
|
||||
/// # use ratatui::layout::{Rect, Constraint, Direction, Layout};
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
||||
/// .split(Rect {
|
||||
/// x: 2,
|
||||
/// y: 2,
|
||||
/// width: 10,
|
||||
/// height: 10,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks[..],
|
||||
/// [
|
||||
/// Rect {
|
||||
/// x: 2,
|
||||
/// y: 2,
|
||||
/// width: 10,
|
||||
/// height: 5
|
||||
/// },
|
||||
/// Rect {
|
||||
/// x: 2,
|
||||
/// y: 7,
|
||||
/// width: 10,
|
||||
/// height: 5
|
||||
/// }
|
||||
/// ]
|
||||
/// );
|
||||
///
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Horizontal)
|
||||
/// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
|
||||
/// .split(Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 9,
|
||||
/// height: 2,
|
||||
/// });
|
||||
/// assert_eq!(
|
||||
/// chunks[..],
|
||||
/// [
|
||||
/// Rect {
|
||||
/// x: 0,
|
||||
/// y: 0,
|
||||
/// width: 3,
|
||||
/// height: 2
|
||||
/// },
|
||||
/// Rect {
|
||||
/// x: 3,
|
||||
/// y: 0,
|
||||
/// width: 6,
|
||||
/// height: 2
|
||||
/// }
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn split(self, area: Rect) -> Vec<Rect> {
|
||||
pub fn split(&self, area: Rect) -> Rc<[Rect]> {
|
||||
// TODO: Maybe use a fixed size cache ?
|
||||
LAYOUT_CACHE.with(|c| {
|
||||
c.borrow_mut()
|
||||
.entry((area, self.clone()))
|
||||
.or_insert_with(|| split(area, &self))
|
||||
.or_insert_with(|| split(area, self))
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
|
||||
let mut solver = Solver::new();
|
||||
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
|
||||
let elements = layout
|
||||
@@ -114,13 +227,15 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
.iter()
|
||||
.map(|_| Element::new())
|
||||
.collect::<Vec<Element>>();
|
||||
let mut results = layout
|
||||
let mut res = layout
|
||||
.constraints
|
||||
.iter()
|
||||
.map(|_| Rect::default())
|
||||
.collect::<Vec<Rect>>();
|
||||
.collect::<Rc<[Rect]>>();
|
||||
|
||||
let dest_area = area.inner(layout.margin);
|
||||
let results = Rc::get_mut(&mut res).expect("newly created Rc should have no shared refs");
|
||||
|
||||
let dest_area = area.inner(&layout.margin);
|
||||
for (i, e) in elements.iter().enumerate() {
|
||||
vars.insert(e.x, (i, 0));
|
||||
vars.insert(e.y, (i, 1));
|
||||
@@ -130,6 +245,8 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
let mut ccs: Vec<CassowaryConstraint> =
|
||||
Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
|
||||
for elt in &elements {
|
||||
ccs.push(elt.width | GE(REQUIRED) | 0f64);
|
||||
ccs.push(elt.height | GE(REQUIRED) | 0f64);
|
||||
ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
|
||||
ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
|
||||
ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
|
||||
@@ -141,11 +258,13 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
|
||||
});
|
||||
}
|
||||
if let Some(last) = elements.last() {
|
||||
ccs.push(match layout.direction {
|
||||
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
|
||||
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
|
||||
});
|
||||
if layout.expand_to_fill {
|
||||
if let Some(last) = elements.last() {
|
||||
ccs.push(match layout.direction {
|
||||
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
|
||||
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
|
||||
});
|
||||
}
|
||||
}
|
||||
match layout.direction {
|
||||
Direction::Horizontal => {
|
||||
@@ -156,13 +275,25 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
|
||||
ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
|
||||
ccs.push(match *size {
|
||||
Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
|
||||
Constraint::Length(v) => elements[i].width | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
|
||||
elements[i].width | EQ(MEDIUM) | (f64::from(v * dest_area.width) / 100.0)
|
||||
}
|
||||
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].width
|
||||
| EQ(MEDIUM)
|
||||
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].width | GE(MEDIUM) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].width | LE(MEDIUM) | f64::from(v),
|
||||
});
|
||||
|
||||
match *size {
|
||||
Constraint::Min(v) | Constraint::Max(v) => {
|
||||
ccs.push(elements[i].width | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Direction::Vertical => {
|
||||
@@ -173,13 +304,25 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
|
||||
ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
|
||||
ccs.push(match *size {
|
||||
Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
|
||||
Constraint::Length(v) => elements[i].height | EQ(MEDIUM) | f64::from(v),
|
||||
Constraint::Percentage(v) => {
|
||||
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
|
||||
elements[i].height | EQ(MEDIUM) | (f64::from(v * dest_area.height) / 100.0)
|
||||
}
|
||||
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
|
||||
Constraint::Ratio(n, d) => {
|
||||
elements[i].height
|
||||
| EQ(MEDIUM)
|
||||
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
|
||||
}
|
||||
Constraint::Min(v) => elements[i].height | GE(MEDIUM) | f64::from(v),
|
||||
Constraint::Max(v) => elements[i].height | LE(MEDIUM) | f64::from(v),
|
||||
});
|
||||
|
||||
match *size {
|
||||
Constraint::Min(v) | Constraint::Max(v) => {
|
||||
ccs.push(elements[i].height | EQ(WEAK) | f64::from(v));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,21 +351,24 @@ fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
||||
}
|
||||
}
|
||||
|
||||
// Fix imprecision by extending the last item a bit if necessary
|
||||
if let Some(last) = results.last_mut() {
|
||||
match layout.direction {
|
||||
Direction::Vertical => {
|
||||
last.height = dest_area.bottom() - last.y;
|
||||
}
|
||||
Direction::Horizontal => {
|
||||
last.width = dest_area.right() - last.x;
|
||||
if layout.expand_to_fill {
|
||||
// Fix imprecision by extending the last item a bit if necessary
|
||||
if let Some(last) = results.last_mut() {
|
||||
match layout.direction {
|
||||
Direction::Vertical => {
|
||||
last.height = dest_area.bottom() - last.y;
|
||||
}
|
||||
Direction::Horizontal => {
|
||||
last.width = dest_area.right() - last.x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
res
|
||||
}
|
||||
|
||||
/// A container used by the solver inside split
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Element {
|
||||
x: Variable,
|
||||
y: Variable,
|
||||
@@ -257,9 +403,9 @@ impl Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
|
||||
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
|
||||
/// area they are supposed to render to.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Rect {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
@@ -267,56 +413,58 @@ pub struct Rect {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Default for Rect {
|
||||
fn default() -> Rect {
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Creates a new rect, with width and height limited to keep the area under max u16.
|
||||
/// If clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
|
||||
let max_area = u16::max_value();
|
||||
let (clipped_width, clipped_height) =
|
||||
if u32::from(width) * u32::from(height) > u32::from(max_area) {
|
||||
let aspect_ratio = f64::from(width) / f64::from(height);
|
||||
let max_area_f = f64::from(max_area);
|
||||
let height_f = (max_area_f / aspect_ratio).sqrt();
|
||||
let width_f = height_f * aspect_ratio;
|
||||
(width_f as u16, height_f as u16)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
Rect {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
width: clipped_width,
|
||||
height: clipped_height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn area(self) -> u16 {
|
||||
pub const fn area(self) -> u16 {
|
||||
self.width * self.height
|
||||
}
|
||||
|
||||
pub fn left(self) -> u16 {
|
||||
pub const fn left(self) -> u16 {
|
||||
self.x
|
||||
}
|
||||
|
||||
pub fn right(self) -> u16 {
|
||||
self.x + self.width
|
||||
pub const fn right(self) -> u16 {
|
||||
self.x.saturating_add(self.width)
|
||||
}
|
||||
|
||||
pub fn top(self) -> u16 {
|
||||
pub const fn top(self) -> u16 {
|
||||
self.y
|
||||
}
|
||||
|
||||
pub fn bottom(self) -> u16 {
|
||||
self.y + self.height
|
||||
pub const fn bottom(self) -> u16 {
|
||||
self.y.saturating_add(self.height)
|
||||
}
|
||||
|
||||
pub fn inner(self, margin: u16) -> Rect {
|
||||
if self.width < 2 * margin || self.height < 2 * margin {
|
||||
pub fn inner(self, margin: &Margin) -> Rect {
|
||||
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
|
||||
Rect::default()
|
||||
} else {
|
||||
Rect {
|
||||
x: self.x + margin,
|
||||
y: self.y + margin,
|
||||
width: self.width - 2 * margin,
|
||||
height: self.height - 2 * margin,
|
||||
x: self.x + margin.horizontal,
|
||||
y: self.y + margin.vertical,
|
||||
width: self.width - 2 * margin.horizontal,
|
||||
height: self.height - 2 * margin.vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,10 +495,148 @@ impl Rect {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersects(self, other: Rect) -> bool {
|
||||
pub const fn intersects(self, other: Rect) -> bool {
|
||||
self.x < other.x + other.width
|
||||
&& self.x + self.width > other.x
|
||||
&& self.y < other.y + other.height
|
||||
&& self.y + self.height > other.y
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_vertical_split_by_height() {
|
||||
let target = Rect {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
};
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Max(5),
|
||||
Constraint::Min(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(target);
|
||||
|
||||
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
|
||||
chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_truncation() {
|
||||
for width in 256u16..300u16 {
|
||||
for height in 256u16..300u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
// The target dimensions are rounded down so the math will not be too precise
|
||||
// but let's make sure the ratios don't diverge crazily.
|
||||
assert!(
|
||||
(f64::from(rect.width) / f64::from(rect.height)
|
||||
- f64::from(width) / f64::from(height))
|
||||
.abs()
|
||||
< 1.0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area above max u16.
|
||||
let width = 900;
|
||||
let height = 100;
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
assert_ne!(rect.width, 900);
|
||||
assert_ne!(rect.height, 100);
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rect_size_preservation() {
|
||||
for width in 0..256u16 {
|
||||
for height in 0..256u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert_eq!(rect.width, width);
|
||||
assert_eq!(rect.height, height);
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area below max u16.
|
||||
let rect = Rect::new(0, 0, 300, 100);
|
||||
assert_eq!(rect.width, 300);
|
||||
assert_eq!(rect.height, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_constraint_apply() {
|
||||
assert_eq!(Constraint::Percentage(0).apply(100), 0);
|
||||
assert_eq!(Constraint::Percentage(50).apply(100), 50);
|
||||
assert_eq!(Constraint::Percentage(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Percentage(200).apply(100), 100);
|
||||
assert_eq!(Constraint::Percentage(u16::MAX).apply(100), 100);
|
||||
|
||||
// 0/0 intentionally avoids a panic by returning 0.
|
||||
assert_eq!(Constraint::Ratio(0, 0).apply(100), 0);
|
||||
// 1/0 intentionally avoids a panic by returning 100% of the length.
|
||||
assert_eq!(Constraint::Ratio(1, 0).apply(100), 100);
|
||||
assert_eq!(Constraint::Ratio(0, 1).apply(100), 0);
|
||||
assert_eq!(Constraint::Ratio(1, 2).apply(100), 50);
|
||||
assert_eq!(Constraint::Ratio(2, 2).apply(100), 100);
|
||||
assert_eq!(Constraint::Ratio(3, 2).apply(100), 100);
|
||||
assert_eq!(Constraint::Ratio(u32::MAX, 2).apply(100), 100);
|
||||
|
||||
assert_eq!(Constraint::Length(0).apply(100), 0);
|
||||
assert_eq!(Constraint::Length(50).apply(100), 50);
|
||||
assert_eq!(Constraint::Length(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Length(200).apply(100), 100);
|
||||
assert_eq!(Constraint::Length(u16::MAX).apply(100), 100);
|
||||
|
||||
assert_eq!(Constraint::Max(0).apply(100), 0);
|
||||
assert_eq!(Constraint::Max(50).apply(100), 50);
|
||||
assert_eq!(Constraint::Max(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Max(200).apply(100), 100);
|
||||
assert_eq!(Constraint::Max(u16::MAX).apply(100), 100);
|
||||
|
||||
assert_eq!(Constraint::Min(0).apply(100), 100);
|
||||
assert_eq!(Constraint::Min(50).apply(100), 100);
|
||||
assert_eq!(Constraint::Min(100).apply(100), 100);
|
||||
assert_eq!(Constraint::Min(200).apply(100), 200);
|
||||
assert_eq!(Constraint::Min(u16::MAX).apply(100), u16::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rect_can_be_const() {
|
||||
const RECT: Rect = Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 10,
|
||||
height: 10,
|
||||
};
|
||||
const _AREA: u16 = RECT.area();
|
||||
const _LEFT: u16 = RECT.left();
|
||||
const _RIGHT: u16 = RECT.right();
|
||||
const _TOP: u16 = RECT.top();
|
||||
const _BOTTOM: u16 = RECT.bottom();
|
||||
assert!(RECT.intersects(RECT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_can_be_const() {
|
||||
const _LAYOUT: Layout = Layout::new();
|
||||
const _DEFAULT_LAYOUT: Layout = Layout::new()
|
||||
.direction(Direction::Horizontal)
|
||||
.margin(1)
|
||||
.expand_to_fill(false);
|
||||
const _HORIZONTAL_LAYOUT: Layout = Layout::new().horizontal_margin(1);
|
||||
const _VERTICAL_LAYOUT: Layout = Layout::new().vertical_margin(1);
|
||||
}
|
||||
}
|
||||
|
||||
263
src/lib.rs
263
src/lib.rs
@@ -1,95 +1,137 @@
|
||||
//! [tui](https://github.com/fdehau/tui-rs) is a library used to build rich
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! [ratatui](https://github.com/ratatui-org/ratatui) is a library used to build rich
|
||||
//! terminal users interfaces and dashboards.
|
||||
//!
|
||||
//! 
|
||||
//! 
|
||||
//!
|
||||
//! # Get started
|
||||
//!
|
||||
//! ## Creating a `Terminal`
|
||||
//! ## Adding `ratatui` as a dependency
|
||||
//!
|
||||
//! Every application using `tui` should start by instantiating a `Terminal`. It is
|
||||
//! a light abstraction over available backends that provides basic functionalities
|
||||
//! such as clearing the screen, hiding the cursor, etc. By default only the `termion`
|
||||
//! backend is available.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//!
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RawBackend;
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let backend = RawBackend::new().unwrap();
|
||||
//! let mut terminal = Terminal::new(backend).unwrap();
|
||||
//! }
|
||||
//! Add the following to your `Cargo.toml`:
|
||||
//! ```toml
|
||||
//! [dependencies]
|
||||
//! crossterm = "0.26"
|
||||
//! ratatui = "0.20"
|
||||
//! ```
|
||||
//!
|
||||
//! If for some reason, you might want to use the `rustbox` backend instead, you
|
||||
//! need the to replace your `tui` dependency specification by:
|
||||
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
|
||||
//! example you want to use the `termion` backend instead. This can be done by changing your
|
||||
//! dependencies specification to the following:
|
||||
//!
|
||||
//! ```toml
|
||||
//! [dependencies.tui]
|
||||
//! version = "0.2.0"
|
||||
//! default-features = false
|
||||
//! features = ['rustbox']
|
||||
//! [dependencies]
|
||||
//! termion = "1.5"
|
||||
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
|
||||
//! ```
|
||||
//!
|
||||
//! and then create the terminal in a similar way:
|
||||
//! The same logic applies for all other available backends.
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! extern crate tui;
|
||||
//! ### Features
|
||||
//!
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RustboxBackend;
|
||||
//! Widgets which add dependencies are gated behind feature flags to prevent unused transitive
|
||||
//! dependencies. The available features are:
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let backend = RustboxBackend::new().unwrap();
|
||||
//! let mut terminal = Terminal::new(backend).unwrap();
|
||||
//! * `widget-calendar` - enables [`widgets::calendar`] and adds a dependency on the [time
|
||||
//! crate](https://crates.io/crates/time).
|
||||
//!
|
||||
//! ## Creating a `Terminal`
|
||||
//!
|
||||
//! Every application using `ratatui` should start by instantiating a `Terminal`. It is a light
|
||||
//! abstraction over available backends that provides basic functionalities such as clearing the
|
||||
//! screen, hiding the cursor, etc.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use std::io;
|
||||
//! use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout();
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! If you had previously chosen `termion` as a backend, the terminal can be created in a similar
|
||||
//! way:
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use std::io;
|
||||
//! use ratatui::{backend::TermionBackend, Terminal};
|
||||
//! use termion::raw::IntoRawMode;
|
||||
//!
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! let stdout = io::stdout().into_raw_mode()?;
|
||||
//! let backend = TermionBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! You may also refer to the examples to find out how to create a `Terminal` for each available
|
||||
//! backend.
|
||||
//!
|
||||
//! ## Building a User Interface (UI)
|
||||
//!
|
||||
//! Every component of your interface will be implementing the `Widget` trait.
|
||||
//! The library comes with a predefined set of widgets that should met most of
|
||||
//! your use cases. You are also free to implement your owns.
|
||||
//! Every component of your interface will be implementing the `Widget` trait. The library comes
|
||||
//! with a predefined set of widgets that should meet most of your use cases. You are also free to
|
||||
//! implement your own.
|
||||
//!
|
||||
//! Each widget follows a builder pattern API providing a default configuration
|
||||
//! along with methods to customize them. The widget is then registered using
|
||||
//! its `render` method that take a `Frame` instance and an area to draw
|
||||
//! to.
|
||||
//! Each widget follows a builder pattern API providing a default configuration along with methods
|
||||
//! to customize them. The widget is then rendered using [`Frame::render_widget`] which takes
|
||||
//! your widget instance and an area to draw to.
|
||||
//!
|
||||
//! The following example renders a block of the size of the terminal:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//! use std::{io, thread, time::Duration};
|
||||
//! use ratatui::{
|
||||
//! backend::CrosstermBackend,
|
||||
//! widgets::{Block, Borders},
|
||||
//! Terminal
|
||||
//! };
|
||||
//! use crossterm::{
|
||||
//! event::{self, DisableMouseCapture, EnableMouseCapture},
|
||||
//! execute,
|
||||
//! terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
//! };
|
||||
//!
|
||||
//! use std::io;
|
||||
//! fn main() -> Result<(), io::Error> {
|
||||
//! // setup terminal
|
||||
//! enable_raw_mode()?;
|
||||
//! let mut stdout = io::stdout();
|
||||
//! execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
|
||||
//! let backend = CrosstermBackend::new(stdout);
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
//!
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RawBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Layout, Constraint, Direction};
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let mut terminal = init().expect("Failed initialization");
|
||||
//! draw(&mut terminal).expect("Failed to draw");
|
||||
//! }
|
||||
//!
|
||||
//! fn init() -> Result<Terminal<RawBackend>, io::Error> {
|
||||
//! let backend = RawBackend::new()?;
|
||||
//! Terminal::new(backend)
|
||||
//! }
|
||||
//!
|
||||
//! fn draw(t: &mut Terminal<RawBackend>) -> Result<(), io::Error> {
|
||||
//!
|
||||
//! let size = t.size()?;
|
||||
//! t.draw(|mut f| {
|
||||
//! Block::default()
|
||||
//! terminal.draw(|f| {
|
||||
//! let size = f.size();
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL)
|
||||
//! .render(&mut f, size);
|
||||
//! })
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, size);
|
||||
//! })?;
|
||||
//!
|
||||
//! // Start a thread to discard any input events. Without handling events, the
|
||||
//! // stdin buffer will fill up, and be read into the shell when the program exits.
|
||||
//! thread::spawn(|| loop {
|
||||
//! event::read();
|
||||
//! });
|
||||
//!
|
||||
//! thread::sleep(Duration::from_millis(5000));
|
||||
//!
|
||||
//! // restore terminal
|
||||
//! disable_raw_mode()?;
|
||||
//! execute!(
|
||||
//! terminal.backend_mut(),
|
||||
//! LeaveAlternateScreen,
|
||||
//! DisableMouseCapture
|
||||
//! )?;
|
||||
//! terminal.show_cursor()?;
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -100,66 +142,42 @@
|
||||
//! full customization. And `Layout` is no exception:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! extern crate tui;
|
||||
//!
|
||||
//! use std::io;
|
||||
//!
|
||||
//! use tui::Terminal;
|
||||
//! use tui::backend::RawBackend;
|
||||
//! use tui::widgets::{Widget, Block, Borders};
|
||||
//! use tui::layout::{Layout, Constraint, Direction};
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let mut terminal = init().expect("Failed initialization");
|
||||
//! draw(&mut terminal).expect("Failed to draw");
|
||||
//! }
|
||||
//!
|
||||
//! fn init() -> Result<Terminal<RawBackend>, io::Error> {
|
||||
//! let backend = RawBackend::new()?;
|
||||
//! Terminal::new(backend)
|
||||
//! }
|
||||
//!
|
||||
//! fn draw(t: &mut Terminal<RawBackend>) -> Result<(), io::Error> {
|
||||
//!
|
||||
//! let size = t.size()?;
|
||||
//! t.draw(|mut f| {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .margin(1)
|
||||
//! .constraints(
|
||||
//! [
|
||||
//! Constraint::Percentage(10),
|
||||
//! Constraint::Percentage(80),
|
||||
//! Constraint::Percentage(10)
|
||||
//! ].as_ref()
|
||||
//! )
|
||||
//! .split(size);
|
||||
//! Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL)
|
||||
//! .render(&mut f, chunks[0]);
|
||||
//! Block::default()
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL)
|
||||
//! .render(&mut f, chunks[2]);
|
||||
//! })
|
||||
//! use ratatui::{
|
||||
//! backend::Backend,
|
||||
//! layout::{Constraint, Direction, Layout},
|
||||
//! widgets::{Block, Borders},
|
||||
//! Frame,
|
||||
//! };
|
||||
//! fn ui<B: Backend>(f: &mut Frame<B>) {
|
||||
//! let chunks = Layout::default()
|
||||
//! .direction(Direction::Vertical)
|
||||
//! .margin(1)
|
||||
//! .constraints(
|
||||
//! [
|
||||
//! Constraint::Percentage(10),
|
||||
//! Constraint::Percentage(80),
|
||||
//! Constraint::Percentage(10)
|
||||
//! ].as_ref()
|
||||
//! )
|
||||
//! .split(f.size());
|
||||
//! let block = Block::default()
|
||||
//! .title("Block")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[0]);
|
||||
//! let block = Block::default()
|
||||
//! .title("Block 2")
|
||||
//! .borders(Borders::ALL);
|
||||
//! f.render_widget(block, chunks[1]);
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! This let you describe responsive terminal UI by nesting layouts. You should note
|
||||
//! that by default the computed layout tries to fill the available space
|
||||
//! completely. So if for any reason you might need a blank space somewhere, try to
|
||||
//! pass an additional constraint and don't use the corresponding area.
|
||||
//! This let you describe responsive terminal UI by nesting layouts. You should note that by
|
||||
//! default the computed layout tries to fill the available space completely. So if for any reason
|
||||
//! you might need a blank space somewhere, try to pass an additional constraint and don't use the
|
||||
//! corresponding area.
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
extern crate cassowary;
|
||||
extern crate either;
|
||||
extern crate itertools;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate unicode_segmentation;
|
||||
extern crate unicode_width;
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
@@ -167,6 +185,9 @@ pub mod layout;
|
||||
pub mod style;
|
||||
pub mod symbols;
|
||||
pub mod terminal;
|
||||
pub mod text;
|
||||
pub mod widgets;
|
||||
|
||||
pub use self::terminal::{Frame, Terminal};
|
||||
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
35
src/prelude.rs
Normal file
35
src/prelude.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
//! A prelude for conveniently writing applications using this library.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use ratatui::prelude::*;
|
||||
//! ```
|
||||
//!
|
||||
//! Aside from the main types that are used in the library, this prelude also re-exports several
|
||||
//! modules to make it easy to qualify types that would otherwise collide. E.g.:
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//! use ratatui::widgets::{Block, Borders};
|
||||
//!
|
||||
//! #[derive(Debug, Default, PartialEq, Eq)]
|
||||
//! struct Line;
|
||||
//!
|
||||
//! assert_eq!(Line::default(), Line);
|
||||
//! assert_eq!(text::Line::default(), ratatui::text::Line::from(vec![]));
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use crate::backend::CrosstermBackend;
|
||||
#[cfg(feature = "termion")]
|
||||
pub use crate::backend::TermionBackend;
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use crate::backend::TermwizBackend;
|
||||
pub use crate::{
|
||||
backend::{self, Backend},
|
||||
buffer::{self, Buffer},
|
||||
layout::{self, Alignment, Constraint, Corner, Direction, Layout, Margin, Rect},
|
||||
style::{self, Color, Modifier, Style, Styled, Stylize},
|
||||
symbols::{self, Marker},
|
||||
terminal::{self, Frame, Terminal, TerminalOptions, Viewport},
|
||||
text::{self, Line, Masked, Span, Text},
|
||||
};
|
||||
866
src/style.rs
866
src/style.rs
@@ -1,79 +1,869 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
//! `style` contains the primitives used to control how your user interface will look.
|
||||
//!
|
||||
//! # Using the `Style` struct
|
||||
//!
|
||||
//! This is useful when creating style variables.
|
||||
//! ## Example
|
||||
//! ```
|
||||
//! use ratatui::style::{Color, Modifier, Style};
|
||||
//!
|
||||
//! Style::default()
|
||||
//! .fg(Color::Black)
|
||||
//! .bg(Color::Green)
|
||||
//! .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
//! ```
|
||||
//!
|
||||
//! # Using style shorthands
|
||||
//!
|
||||
//! This is best for concise styling.
|
||||
//! ## Example
|
||||
//! ```
|
||||
//! use ratatui::prelude::*;
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! "hello".red().on_blue().bold(),
|
||||
//! Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
//! )
|
||||
//! ```
|
||||
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
mod stylize;
|
||||
pub use stylize::{Styled, Stylize};
|
||||
|
||||
/// ANSI Color
|
||||
///
|
||||
/// All colors from the [ANSI color table](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors)
|
||||
/// are supported (though some names are not exactly the same).
|
||||
///
|
||||
/// | Color Name | Color | Foreground | Background |
|
||||
/// |----------------|-------------------------|------------|------------|
|
||||
/// | `black` | [`Color::Black`] | 30 | 40 |
|
||||
/// | `red` | [`Color::Red`] | 31 | 41 |
|
||||
/// | `green` | [`Color::Green`] | 32 | 42 |
|
||||
/// | `yellow` | [`Color::Yellow`] | 33 | 43 |
|
||||
/// | `blue` | [`Color::Blue`] | 34 | 44 |
|
||||
/// | `magenta` | [`Color::Magenta`] | 35 | 45 |
|
||||
/// | `cyan` | [`Color::Cyan`] | 36 | 46 |
|
||||
/// | `gray`* | [`Color::Gray`] | 37 | 47 |
|
||||
/// | `darkgray`* | [`Color::DarkGray`] | 90 | 100 |
|
||||
/// | `lightred` | [`Color::LightRed`] | 91 | 101 |
|
||||
/// | `lightgreen` | [`Color::LightGreen`] | 92 | 102 |
|
||||
/// | `lightyellow` | [`Color::LightYellow`] | 93 | 103 |
|
||||
/// | `lightblue` | [`Color::LightBlue`] | 94 | 104 |
|
||||
/// | `lightmagenta` | [`Color::LightMagenta`] | 95 | 105 |
|
||||
/// | `lightcyan` | [`Color::LightCyan`] | 96 | 106 |
|
||||
/// | `white`* | [`Color::White`] | 97 | 107 |
|
||||
///
|
||||
/// - `gray` is sometimes called `white` - this is not supported as we use `white` for bright white
|
||||
/// - `gray` is sometimes called `silver` - this is supported
|
||||
/// - `darkgray` is sometimes called `light black` or `bright black` (both are supported)
|
||||
/// - `white` is sometimes called `light white` or `bright white` (both are supported)
|
||||
/// - we support `bright` and `light` prefixes for all colors
|
||||
/// - we support `-` and `_` and ` ` as separators for all colors
|
||||
/// - we support both `gray` and `grey` spellings
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::style::Color;
|
||||
/// use std::str::FromStr;
|
||||
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
|
||||
/// assert_eq!("red".parse(), Ok(Color::Red));
|
||||
/// assert_eq!("lightred".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("light red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("light-red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("light_red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("lightRed".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("bright red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("bright-red".parse(), Ok(Color::LightRed));
|
||||
/// assert_eq!("silver".parse(), Ok(Color::Gray));
|
||||
/// assert_eq!("dark-grey".parse(), Ok(Color::DarkGray));
|
||||
/// assert_eq!("dark gray".parse(), Ok(Color::DarkGray));
|
||||
/// assert_eq!("light-black".parse(), Ok(Color::DarkGray));
|
||||
/// assert_eq!("white".parse(), Ok(Color::White));
|
||||
/// assert_eq!("bright white".parse(), Ok(Color::White));
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Color {
|
||||
/// Resets the foreground or background color
|
||||
#[default]
|
||||
Reset,
|
||||
/// ANSI Color: Black. Foreground: 30, Background: 40
|
||||
Black,
|
||||
/// ANSI Color: Red. Foreground: 31, Background: 41
|
||||
Red,
|
||||
/// ANSI Color: Green. Foreground: 32, Background: 42
|
||||
Green,
|
||||
/// ANSI Color: Yellow. Foreground: 33, Background: 43
|
||||
Yellow,
|
||||
/// ANSI Color: Blue. Foreground: 34, Background: 44
|
||||
Blue,
|
||||
/// ANSI Color: Magenta. Foreground: 35, Background: 45
|
||||
Magenta,
|
||||
/// ANSI Color: Cyan. Foreground: 36, Background: 46
|
||||
Cyan,
|
||||
/// ANSI Color: White. Foreground: 37, Background: 47
|
||||
///
|
||||
/// Note that this is sometimes called `silver` or `white` but we use `white` for bright white
|
||||
Gray,
|
||||
/// ANSI Color: Bright Black. Foreground: 90, Background: 100
|
||||
///
|
||||
/// Note that this is sometimes called `light black` or `bright black` but we use `dark gray`
|
||||
DarkGray,
|
||||
/// ANSI Color: Bright Red. Foreground: 91, Background: 101
|
||||
LightRed,
|
||||
/// ANSI Color: Bright Green. Foreground: 92, Background: 102
|
||||
LightGreen,
|
||||
/// ANSI Color: Bright Yellow. Foreground: 93, Background: 103
|
||||
LightYellow,
|
||||
/// ANSI Color: Bright Blue. Foreground: 94, Background: 104
|
||||
LightBlue,
|
||||
/// ANSI Color: Bright Magenta. Foreground: 95, Background: 105
|
||||
LightMagenta,
|
||||
/// ANSI Color: Bright Cyan. Foreground: 96, Background: 106
|
||||
LightCyan,
|
||||
/// ANSI Color: Bright White. Foreground: 97, Background: 107
|
||||
/// Sometimes called `bright white` or `light white` in some terminals
|
||||
White,
|
||||
/// An RGB color
|
||||
Rgb(u8, u8, u8),
|
||||
/// An 8-bit 256 color. See <https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit>
|
||||
Indexed(u8),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Modifier {
|
||||
Blink,
|
||||
Bold,
|
||||
CrossedOut,
|
||||
Faint,
|
||||
Framed,
|
||||
Invert,
|
||||
Italic,
|
||||
NoBlink,
|
||||
NoBold,
|
||||
NoCrossedOut,
|
||||
NoFaint,
|
||||
NoInvert,
|
||||
NoItalic,
|
||||
NoUnderline,
|
||||
Reset,
|
||||
Underline,
|
||||
bitflags! {
|
||||
/// Modifier changes the way a piece of text is displayed.
|
||||
///
|
||||
/// They are bitflags so they can easily be composed.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::Modifier;
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Modifier: u16 {
|
||||
const BOLD = 0b0000_0000_0001;
|
||||
const DIM = 0b0000_0000_0010;
|
||||
const ITALIC = 0b0000_0000_0100;
|
||||
const UNDERLINED = 0b0000_0000_1000;
|
||||
const SLOW_BLINK = 0b0000_0001_0000;
|
||||
const RAPID_BLINK = 0b0000_0010_0000;
|
||||
const REVERSED = 0b0000_0100_0000;
|
||||
const HIDDEN = 0b0000_1000_0000;
|
||||
const CROSSED_OUT = 0b0001_0000_0000;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// Implement the `Debug` trait for `Modifier` manually.
|
||||
///
|
||||
/// This will avoid printing the empty modifier as 'Borders(0x0)' and instead print it as 'NONE'.
|
||||
impl fmt::Debug for Modifier {
|
||||
/// Format the modifier as `NONE` if the modifier is empty or as a list of flags separated by
|
||||
/// `|` otherwise.
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if self.is_empty() {
|
||||
return write!(f, "NONE");
|
||||
}
|
||||
fmt::Debug::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Style let you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
/// .bg(Color::Green)
|
||||
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
||||
/// ```
|
||||
///
|
||||
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::default().bg(Color::Red).add_modifier(Modifier::UNDERLINED),
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// Style::default().underline_color(Color::Green),
|
||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Red),
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// underline_color: Some(Color::Green),
|
||||
/// add_modifier: Modifier::BOLD | Modifier::UNDERLINED,
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// The default implementation returns a `Style` that does not modify anything. If you wish to
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use ratatui::buffer::Buffer;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// let styles = [
|
||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
||||
/// Style::reset().fg(Color::Yellow),
|
||||
/// ];
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
/// for style in &styles {
|
||||
/// buffer.get_mut(0, 0).set_style(*style);
|
||||
/// }
|
||||
/// assert_eq!(
|
||||
/// Style {
|
||||
/// fg: Some(Color::Yellow),
|
||||
/// bg: Some(Color::Reset),
|
||||
/// #[cfg(feature = "crossterm")]
|
||||
/// underline_color: Some(Color::Reset),
|
||||
/// add_modifier: Modifier::empty(),
|
||||
/// sub_modifier: Modifier::empty(),
|
||||
/// },
|
||||
/// buffer.get(0, 0).style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
pub fg: Color,
|
||||
pub bg: Color,
|
||||
pub modifier: Modifier,
|
||||
pub fg: Option<Color>,
|
||||
pub bg: Option<Color>,
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub underline_color: Option<Color>,
|
||||
pub add_modifier: Modifier,
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
impl Default for Style {
|
||||
fn default() -> Style {
|
||||
Style::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Style {
|
||||
type Item = Style;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
*self
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
self.patch(style)
|
||||
}
|
||||
}
|
||||
impl Style {
|
||||
pub const fn new() -> Style {
|
||||
Style {
|
||||
fg: Color::Reset,
|
||||
bg: Color::Reset,
|
||||
modifier: Modifier::Reset,
|
||||
fg: None,
|
||||
bg: None,
|
||||
#[cfg(feature = "crossterm")]
|
||||
underline_color: None,
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub fn reset(&mut self) {
|
||||
self.fg = Color::Reset;
|
||||
self.bg = Color::Reset;
|
||||
self.modifier = Modifier::Reset;
|
||||
/// Returns a `Style` resetting all properties.
|
||||
pub const fn reset() -> Style {
|
||||
Style {
|
||||
fg: Some(Color::Reset),
|
||||
bg: Some(Color::Reset),
|
||||
#[cfg(feature = "crossterm")]
|
||||
underline_color: Some(Color::Reset),
|
||||
add_modifier: Modifier::empty(),
|
||||
sub_modifier: Modifier::all(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fg(mut self, color: Color) -> Style {
|
||||
self.fg = color;
|
||||
/// Changes the foreground color.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let style = Style::default().fg(Color::Blue);
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
/// ```
|
||||
pub const fn fg(mut self, color: Color) -> Style {
|
||||
self.fg = Some(color);
|
||||
self
|
||||
}
|
||||
pub fn bg(mut self, color: Color) -> Style {
|
||||
self.bg = color;
|
||||
|
||||
/// Changes the background color.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let style = Style::default().bg(Color::Blue);
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
/// ```
|
||||
pub const fn bg(mut self, color: Color) -> Style {
|
||||
self.bg = Some(color);
|
||||
self
|
||||
}
|
||||
pub fn modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.modifier = modifier;
|
||||
|
||||
/// Changes the underline color. The text must be underlined with a modifier for this to work.
|
||||
///
|
||||
/// This uses a non-standard ANSI escape sequence. It is supported by most terminal emulators,
|
||||
/// but is only implemented in the crossterm backend.
|
||||
///
|
||||
/// See [Wikipedia](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters) code `58` and `59` for more information.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().underline_color(Color::Blue).add_modifier(Modifier::UNDERLINED);
|
||||
/// let diff = Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED);
|
||||
/// assert_eq!(style.patch(diff), Style::default().underline_color(Color::Red).add_modifier(Modifier::UNDERLINED));
|
||||
/// ```
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub const fn underline_color(mut self, color: Color) -> Style {
|
||||
self.underline_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the text emphasis.
|
||||
///
|
||||
/// When applied, it adds the given modifier to the `Style` modifiers.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD);
|
||||
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::empty());
|
||||
/// ```
|
||||
pub const fn add_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.sub_modifier = self.sub_modifier.difference(modifier);
|
||||
self.add_modifier = self.add_modifier.union(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Changes the text emphasis.
|
||||
///
|
||||
/// When applied, it removes the given modifier from the `Style` modifiers.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
|
||||
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
|
||||
/// ```
|
||||
pub const fn remove_modifier(mut self, modifier: Modifier) -> Style {
|
||||
self.add_modifier = self.add_modifier.difference(modifier);
|
||||
self.sub_modifier = self.sub_modifier.union(modifier);
|
||||
self
|
||||
}
|
||||
|
||||
/// Results in a combined style that is equivalent to applying the two individual styles to
|
||||
/// a style one after the other.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style_1 = Style::default().fg(Color::Yellow);
|
||||
/// let style_2 = Style::default().bg(Color::Red);
|
||||
/// let combined = style_1.patch(style_2);
|
||||
/// assert_eq!(
|
||||
/// Style::default().patch(style_1).patch(style_2),
|
||||
/// Style::default().patch(combined));
|
||||
/// ```
|
||||
pub fn patch(mut self, other: Style) -> Style {
|
||||
self.fg = other.fg.or(self.fg);
|
||||
self.bg = other.bg.or(self.bg);
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
{
|
||||
self.underline_color = other.underline_color.or(self.underline_color);
|
||||
}
|
||||
|
||||
self.add_modifier.remove(other.sub_modifier);
|
||||
self.add_modifier.insert(other.add_modifier);
|
||||
self.sub_modifier.remove(other.add_modifier);
|
||||
self.sub_modifier.insert(other.sub_modifier);
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type indicating a failure to parse a color string.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ParseColorError;
|
||||
|
||||
impl std::fmt::Display for ParseColorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Failed to parse Colors")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ParseColorError {}
|
||||
|
||||
/// Converts a string representation to a `Color` instance.
|
||||
///
|
||||
/// The `from_str` function attempts to parse the given string and convert it to the corresponding
|
||||
/// `Color` variant. It supports named colors, RGB values, and indexed colors. If the string cannot
|
||||
/// be parsed, a `ParseColorError` is returned.
|
||||
///
|
||||
/// See the [`Color`](Color) documentation for more information on the supported color names.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use ratatui::style::Color;
|
||||
/// let color: Color = Color::from_str("blue").unwrap();
|
||||
/// assert_eq!(color, Color::Blue);
|
||||
///
|
||||
/// let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
///
|
||||
/// let color: Color = Color::from_str("10").unwrap();
|
||||
/// assert_eq!(color, Color::Indexed(10));
|
||||
///
|
||||
/// let color: Result<Color, _> = Color::from_str("invalid_color");
|
||||
/// assert!(color.is_err());
|
||||
/// ```
|
||||
impl FromStr for Color {
|
||||
type Err = ParseColorError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(
|
||||
// There is a mix of different color names and formats in the wild.
|
||||
// This is an attempt to support as many as possible.
|
||||
match s
|
||||
.to_lowercase()
|
||||
.replace([' ', '-', '_'], "")
|
||||
.replace("bright", "light")
|
||||
.replace("grey", "gray")
|
||||
.replace("silver", "gray")
|
||||
.replace("lightblack", "darkgray")
|
||||
.replace("lightwhite", "white")
|
||||
.replace("lightgray", "white")
|
||||
.as_ref()
|
||||
{
|
||||
"reset" => Self::Reset,
|
||||
"black" => Self::Black,
|
||||
"red" => Self::Red,
|
||||
"green" => Self::Green,
|
||||
"yellow" => Self::Yellow,
|
||||
"blue" => Self::Blue,
|
||||
"magenta" => Self::Magenta,
|
||||
"cyan" => Self::Cyan,
|
||||
"gray" => Self::Gray,
|
||||
"darkgray" => Self::DarkGray,
|
||||
"lightred" => Self::LightRed,
|
||||
"lightgreen" => Self::LightGreen,
|
||||
"lightyellow" => Self::LightYellow,
|
||||
"lightblue" => Self::LightBlue,
|
||||
"lightmagenta" => Self::LightMagenta,
|
||||
"lightcyan" => Self::LightCyan,
|
||||
"white" => Self::White,
|
||||
_ => {
|
||||
if let Ok(index) = s.parse::<u8>() {
|
||||
Self::Indexed(index)
|
||||
} else if let (Ok(r), Ok(g), Ok(b)) = {
|
||||
if !s.starts_with('#') || s.len() != 7 {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
(
|
||||
u8::from_str_radix(&s[1..3], 16),
|
||||
u8::from_str_radix(&s[3..5], 16),
|
||||
u8::from_str_radix(&s[5..7], 16),
|
||||
)
|
||||
} {
|
||||
Self::Rgb(r, g, b)
|
||||
} else {
|
||||
return Err(ParseColorError);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::error::Error;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn styles() -> Vec<Style> {
|
||||
vec![
|
||||
Style::default(),
|
||||
Style::default().fg(Color::Yellow),
|
||||
Style::default().bg(Color::Yellow),
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::BOLD),
|
||||
Style::default().add_modifier(Modifier::ITALIC),
|
||||
Style::default().remove_modifier(Modifier::ITALIC),
|
||||
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_patch_gives_same_result_as_individual_patch() {
|
||||
let styles = styles();
|
||||
for &a in &styles {
|
||||
for &b in &styles {
|
||||
for &c in &styles {
|
||||
for &d in &styles {
|
||||
let combined = a.patch(b.patch(c.patch(d)));
|
||||
|
||||
assert_eq!(
|
||||
Style::default().patch(a).patch(b).patch(c).patch(d),
|
||||
Style::default().patch(combined)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_individual_modifiers() {
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
let mods = vec![
|
||||
Modifier::BOLD,
|
||||
Modifier::DIM,
|
||||
Modifier::ITALIC,
|
||||
Modifier::UNDERLINED,
|
||||
Modifier::SLOW_BLINK,
|
||||
Modifier::RAPID_BLINK,
|
||||
Modifier::REVERSED,
|
||||
Modifier::HIDDEN,
|
||||
Modifier::CROSSED_OUT,
|
||||
];
|
||||
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
||||
|
||||
for m in &mods {
|
||||
buffer.get_mut(0, 0).set_style(Style::reset());
|
||||
buffer
|
||||
.get_mut(0, 0)
|
||||
.set_style(Style::default().add_modifier(*m));
|
||||
let style = buffer.get(0, 0).style();
|
||||
assert!(style.add_modifier.contains(*m));
|
||||
assert!(!style.sub_modifier.contains(*m));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_debug() {
|
||||
assert_eq!(format!("{:?}", Modifier::empty()), "NONE");
|
||||
assert_eq!(format!("{:?}", Modifier::BOLD), "BOLD");
|
||||
assert_eq!(format!("{:?}", Modifier::DIM), "DIM");
|
||||
assert_eq!(format!("{:?}", Modifier::ITALIC), "ITALIC");
|
||||
assert_eq!(format!("{:?}", Modifier::UNDERLINED), "UNDERLINED");
|
||||
assert_eq!(format!("{:?}", Modifier::SLOW_BLINK), "SLOW_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::RAPID_BLINK), "RAPID_BLINK");
|
||||
assert_eq!(format!("{:?}", Modifier::REVERSED), "REVERSED");
|
||||
assert_eq!(format!("{:?}", Modifier::HIDDEN), "HIDDEN");
|
||||
assert_eq!(format!("{:?}", Modifier::CROSSED_OUT), "CROSSED_OUT");
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::BOLD | Modifier::DIM),
|
||||
"BOLD | DIM"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", Modifier::all()),
|
||||
"BOLD | DIM | ITALIC | UNDERLINED | SLOW_BLINK | RAPID_BLINK | REVERSED | HIDDEN | CROSSED_OUT"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rgb_color() {
|
||||
let color: Color = Color::from_str("#FF0000").unwrap();
|
||||
assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_indexed_color() {
|
||||
let color: Color = Color::from_str("10").unwrap();
|
||||
assert_eq!(color, Color::Indexed(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ansi_color() -> Result<(), Box<dyn Error>> {
|
||||
assert_eq!(Color::from_str("reset")?, Color::Reset);
|
||||
assert_eq!(Color::from_str("black")?, Color::Black);
|
||||
assert_eq!(Color::from_str("red")?, Color::Red);
|
||||
assert_eq!(Color::from_str("green")?, Color::Green);
|
||||
assert_eq!(Color::from_str("yellow")?, Color::Yellow);
|
||||
assert_eq!(Color::from_str("blue")?, Color::Blue);
|
||||
assert_eq!(Color::from_str("magenta")?, Color::Magenta);
|
||||
assert_eq!(Color::from_str("cyan")?, Color::Cyan);
|
||||
assert_eq!(Color::from_str("gray")?, Color::Gray);
|
||||
assert_eq!(Color::from_str("darkgray")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("lightred")?, Color::LightRed);
|
||||
assert_eq!(Color::from_str("lightgreen")?, Color::LightGreen);
|
||||
assert_eq!(Color::from_str("lightyellow")?, Color::LightYellow);
|
||||
assert_eq!(Color::from_str("lightblue")?, Color::LightBlue);
|
||||
assert_eq!(Color::from_str("lightmagenta")?, Color::LightMagenta);
|
||||
assert_eq!(Color::from_str("lightcyan")?, Color::LightCyan);
|
||||
assert_eq!(Color::from_str("white")?, Color::White);
|
||||
|
||||
// aliases
|
||||
assert_eq!(Color::from_str("lightblack")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("lightwhite")?, Color::White);
|
||||
assert_eq!(Color::from_str("lightgray")?, Color::White);
|
||||
|
||||
// silver = grey = gray
|
||||
assert_eq!(Color::from_str("grey")?, Color::Gray);
|
||||
assert_eq!(Color::from_str("silver")?, Color::Gray);
|
||||
|
||||
// spaces are ignored
|
||||
assert_eq!(Color::from_str("light black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("light white")?, Color::White);
|
||||
assert_eq!(Color::from_str("light gray")?, Color::White);
|
||||
|
||||
// dashes are ignored
|
||||
assert_eq!(Color::from_str("light-black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("light-white")?, Color::White);
|
||||
assert_eq!(Color::from_str("light-gray")?, Color::White);
|
||||
|
||||
// underscores are ignored
|
||||
assert_eq!(Color::from_str("light_black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("light_white")?, Color::White);
|
||||
assert_eq!(Color::from_str("light_gray")?, Color::White);
|
||||
|
||||
// bright = light
|
||||
assert_eq!(Color::from_str("bright-black")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("bright-white")?, Color::White);
|
||||
|
||||
// bright = light
|
||||
assert_eq!(Color::from_str("brightblack")?, Color::DarkGray);
|
||||
assert_eq!(Color::from_str("brightwhite")?, Color::White);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_invalid_colors() {
|
||||
let bad_colors = [
|
||||
"invalid_color", // not a color string
|
||||
"abcdef0", // 7 chars is not a color
|
||||
" bcdefa", // doesn't start with a '#'
|
||||
"#abcdef00", // too many chars
|
||||
"resett", // typo
|
||||
"lightblackk", // typo
|
||||
];
|
||||
|
||||
for bad_color in bad_colors {
|
||||
assert!(
|
||||
Color::from_str(bad_color).is_err(),
|
||||
"bad color: '{bad_color}'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_can_be_const() {
|
||||
const RED: Color = Color::Red;
|
||||
const BLACK: Color = Color::Black;
|
||||
const BOLD: Modifier = Modifier::BOLD;
|
||||
const ITALIC: Modifier = Modifier::ITALIC;
|
||||
|
||||
const _RESET: Style = Style::reset();
|
||||
const _RED_FG: Style = Style::new().fg(RED);
|
||||
const _BLACK_BG: Style = Style::new().bg(BLACK);
|
||||
const _ADD_BOLD: Style = Style::new().add_modifier(BOLD);
|
||||
const _REMOVE_ITALIC: Style = Style::new().remove_modifier(ITALIC);
|
||||
const ALL: Style = Style::new()
|
||||
.fg(RED)
|
||||
.bg(BLACK)
|
||||
.add_modifier(BOLD)
|
||||
.remove_modifier(ITALIC);
|
||||
assert_eq!(
|
||||
ALL,
|
||||
Style::new()
|
||||
.fg(Color::Red)
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_can_be_stylized() {
|
||||
// foreground colors
|
||||
assert_eq!(Style::new().black(), Style::new().fg(Color::Black));
|
||||
assert_eq!(Style::new().red(), Style::new().fg(Color::Red));
|
||||
assert_eq!(Style::new().green(), Style::new().fg(Color::Green));
|
||||
assert_eq!(Style::new().yellow(), Style::new().fg(Color::Yellow));
|
||||
assert_eq!(Style::new().blue(), Style::new().fg(Color::Blue));
|
||||
assert_eq!(Style::new().magenta(), Style::new().fg(Color::Magenta));
|
||||
assert_eq!(Style::new().cyan(), Style::new().fg(Color::Cyan));
|
||||
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
|
||||
assert_eq!(Style::new().gray(), Style::new().fg(Color::Gray));
|
||||
assert_eq!(Style::new().dark_gray(), Style::new().fg(Color::DarkGray));
|
||||
assert_eq!(Style::new().light_red(), Style::new().fg(Color::LightRed));
|
||||
assert_eq!(
|
||||
Style::new().light_green(),
|
||||
Style::new().fg(Color::LightGreen)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().light_yellow(),
|
||||
Style::new().fg(Color::LightYellow)
|
||||
);
|
||||
assert_eq!(Style::new().light_blue(), Style::new().fg(Color::LightBlue));
|
||||
assert_eq!(
|
||||
Style::new().light_magenta(),
|
||||
Style::new().fg(Color::LightMagenta)
|
||||
);
|
||||
assert_eq!(Style::new().light_cyan(), Style::new().fg(Color::LightCyan));
|
||||
assert_eq!(Style::new().white(), Style::new().fg(Color::White));
|
||||
|
||||
// Background colors
|
||||
assert_eq!(Style::new().on_black(), Style::new().bg(Color::Black));
|
||||
assert_eq!(Style::new().on_red(), Style::new().bg(Color::Red));
|
||||
assert_eq!(Style::new().on_green(), Style::new().bg(Color::Green));
|
||||
assert_eq!(Style::new().on_yellow(), Style::new().bg(Color::Yellow));
|
||||
assert_eq!(Style::new().on_blue(), Style::new().bg(Color::Blue));
|
||||
assert_eq!(Style::new().on_magenta(), Style::new().bg(Color::Magenta));
|
||||
assert_eq!(Style::new().on_cyan(), Style::new().bg(Color::Cyan));
|
||||
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
|
||||
assert_eq!(Style::new().on_gray(), Style::new().bg(Color::Gray));
|
||||
assert_eq!(
|
||||
Style::new().on_dark_gray(),
|
||||
Style::new().bg(Color::DarkGray)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_red(),
|
||||
Style::new().bg(Color::LightRed)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_green(),
|
||||
Style::new().bg(Color::LightGreen)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_yellow(),
|
||||
Style::new().bg(Color::LightYellow)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_blue(),
|
||||
Style::new().bg(Color::LightBlue)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_magenta(),
|
||||
Style::new().bg(Color::LightMagenta)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().on_light_cyan(),
|
||||
Style::new().bg(Color::LightCyan)
|
||||
);
|
||||
assert_eq!(Style::new().on_white(), Style::new().bg(Color::White));
|
||||
|
||||
// Add Modifiers
|
||||
assert_eq!(
|
||||
Style::new().bold(),
|
||||
Style::new().add_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(Style::new().dim(), Style::new().add_modifier(Modifier::DIM));
|
||||
assert_eq!(
|
||||
Style::new().italic(),
|
||||
Style::new().add_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().underlined(),
|
||||
Style::new().add_modifier(Modifier::UNDERLINED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().slow_blink(),
|
||||
Style::new().add_modifier(Modifier::SLOW_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().rapid_blink(),
|
||||
Style::new().add_modifier(Modifier::RAPID_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().reversed(),
|
||||
Style::new().add_modifier(Modifier::REVERSED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().hidden(),
|
||||
Style::new().add_modifier(Modifier::HIDDEN)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().crossed_out(),
|
||||
Style::new().add_modifier(Modifier::CROSSED_OUT)
|
||||
);
|
||||
|
||||
// Remove Modifiers
|
||||
assert_eq!(
|
||||
Style::new().not_bold(),
|
||||
Style::new().remove_modifier(Modifier::BOLD)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_dim(),
|
||||
Style::new().remove_modifier(Modifier::DIM)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_italic(),
|
||||
Style::new().remove_modifier(Modifier::ITALIC)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_underlined(),
|
||||
Style::new().remove_modifier(Modifier::UNDERLINED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_slow_blink(),
|
||||
Style::new().remove_modifier(Modifier::SLOW_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_rapid_blink(),
|
||||
Style::new().remove_modifier(Modifier::RAPID_BLINK)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_reversed(),
|
||||
Style::new().remove_modifier(Modifier::REVERSED)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_hidden(),
|
||||
Style::new().remove_modifier(Modifier::HIDDEN)
|
||||
);
|
||||
assert_eq!(
|
||||
Style::new().not_crossed_out(),
|
||||
Style::new().remove_modifier(Modifier::CROSSED_OUT)
|
||||
);
|
||||
|
||||
// reset
|
||||
assert_eq!(Style::new().reset(), Style::reset());
|
||||
}
|
||||
}
|
||||
|
||||
260
src/style/stylize.rs
Normal file
260
src/style/stylize.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
use paste::paste;
|
||||
|
||||
use crate::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
/// A trait for objects that have a `Style`.
|
||||
///
|
||||
/// This trait enables generic code to be written that can interact with any object that has a
|
||||
/// `Style`. This is used by the `Stylize` trait to allow generic code to be written that can
|
||||
/// interact with any object that can be styled.
|
||||
pub trait Styled {
|
||||
type Item;
|
||||
|
||||
fn style(&self) -> Style;
|
||||
fn set_style(self, style: Style) -> Self::Item;
|
||||
}
|
||||
|
||||
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
|
||||
/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets
|
||||
/// the color of the style to the corresponding color.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// color!(black);
|
||||
///
|
||||
/// // generates
|
||||
///
|
||||
/// #[doc = "Sets the foreground color to [`black`](Color::Black)."]
|
||||
/// fn black(self) -> T {
|
||||
/// self.fg(Color::Black)
|
||||
/// }
|
||||
///
|
||||
/// #[doc = "Sets the background color to [`black`](Color::Black)."]
|
||||
/// fn on_black(self) -> T {
|
||||
/// self.bg(Color::Black)
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! color {
|
||||
( $color:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Sets the foreground color to [`" $color "`](Color::" $color:camel ")."]
|
||||
fn $color(self) -> T {
|
||||
self.fg(Color::[<$color:camel>])
|
||||
}
|
||||
|
||||
#[doc = "Sets the background color to [`" $color "`](Color::" $color:camel ")."]
|
||||
fn [<on_ $color>](self) -> T {
|
||||
self.bg(Color::[<$color:camel>])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Generates a method for a modifier (`bold()`, `italic()`, etc.). Each method sets the modifier
|
||||
/// of the style to the corresponding modifier.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// modifier!(bold);
|
||||
///
|
||||
/// // generates
|
||||
///
|
||||
/// #[doc = "Adds the [`BOLD`](Modifier::BOLD) modifier."]
|
||||
/// fn bold(self) -> T {
|
||||
/// self.add_modifier(Modifier::BOLD)
|
||||
/// }
|
||||
///
|
||||
/// #[doc = "Removes the [`BOLD`](Modifier::BOLD) modifier."]
|
||||
/// fn not_bold(self) -> T {
|
||||
/// self.remove_modifier(Modifier::BOLD)
|
||||
/// }
|
||||
/// ```
|
||||
macro_rules! modifier {
|
||||
( $modifier:ident ) => {
|
||||
paste! {
|
||||
#[doc = "Adds the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
fn [<$modifier>](self) -> T {
|
||||
self.add_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
}
|
||||
|
||||
paste! {
|
||||
#[doc = "Removes the [`" $modifier:upper "`](Modifier::" $modifier:upper ") modifier."]
|
||||
fn [<not_ $modifier>](self) -> T {
|
||||
self.remove_modifier(Modifier::[<$modifier:upper>])
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// The trait that enables something to be have a style.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::{Color, Modifier, Style, Styled, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// "hello".red().on_blue().bold(),
|
||||
/// Span::styled("hello", Style::default().fg(Color::Red).bg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||
/// )
|
||||
pub trait Stylize<'a, T>: Sized {
|
||||
fn bg(self, color: Color) -> T;
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T;
|
||||
fn reset(self) -> T;
|
||||
fn add_modifier(self, modifier: Modifier) -> T;
|
||||
fn remove_modifier(self, modifier: Modifier) -> T;
|
||||
|
||||
color!(black);
|
||||
color!(red);
|
||||
color!(green);
|
||||
color!(yellow);
|
||||
color!(blue);
|
||||
color!(magenta);
|
||||
color!(cyan);
|
||||
color!(gray);
|
||||
color!(dark_gray);
|
||||
color!(light_red);
|
||||
color!(light_green);
|
||||
color!(light_yellow);
|
||||
color!(light_blue);
|
||||
color!(light_magenta);
|
||||
color!(light_cyan);
|
||||
color!(white);
|
||||
|
||||
modifier!(bold);
|
||||
modifier!(dim);
|
||||
modifier!(italic);
|
||||
modifier!(underlined);
|
||||
modifier!(slow_blink);
|
||||
modifier!(rapid_blink);
|
||||
modifier!(reversed);
|
||||
modifier!(hidden);
|
||||
modifier!(crossed_out);
|
||||
}
|
||||
|
||||
impl<'a, T, U> Stylize<'a, T> for U
|
||||
where
|
||||
U: Styled<Item = T>,
|
||||
{
|
||||
fn bg(self, color: Color) -> T {
|
||||
let style = self.style().bg(color);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn fg<S: Into<Color>>(self, color: S) -> T {
|
||||
let style = self.style().fg(color.into());
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn add_modifier(self, modifier: Modifier) -> T {
|
||||
let style = self.style().add_modifier(modifier);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn remove_modifier(self, modifier: Modifier) -> T {
|
||||
let style = self.style().remove_modifier(modifier);
|
||||
self.set_style(style)
|
||||
}
|
||||
|
||||
fn reset(self) -> T {
|
||||
self.set_style(Style::reset())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for &'a str {
|
||||
type Item = Span<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
Style::default()
|
||||
}
|
||||
|
||||
fn set_style(self, style: Style) -> Self::Item {
|
||||
Span::styled(self, style)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reset() {
|
||||
assert_eq!(
|
||||
"hello".on_cyan().light_red().bold().underlined().reset(),
|
||||
Span::styled("hello", Style::reset())
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fg() {
|
||||
let cyan_fg = Style::default().fg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".cyan(), Span::styled("hello", cyan_fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bg() {
|
||||
let cyan_bg = Style::default().bg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".on_cyan(), Span::styled("hello", cyan_bg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_modifier() {
|
||||
let cyan_bold = Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
|
||||
assert_eq!("hello".cyan().bold(), Span::styled("hello", cyan_bold))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fg_bg() {
|
||||
let cyan_fg_bg = Style::default().bg(Color::Cyan).fg(Color::Cyan);
|
||||
|
||||
assert_eq!("hello".cyan().on_cyan(), Span::styled("hello", cyan_fg_bg))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_attributes() {
|
||||
let cyan_bg = Style::default().bg(Color::Cyan);
|
||||
let cyan_fg = Style::default().fg(Color::Cyan);
|
||||
|
||||
// Behavior: the last one set is the definitive one
|
||||
assert_eq!("hello".on_red().on_cyan(), Span::styled("hello", cyan_bg));
|
||||
assert_eq!("hello".red().cyan(), Span::styled("hello", cyan_fg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_chained() {
|
||||
let all_modifier_black = Style::default()
|
||||
.bg(Color::Black)
|
||||
.fg(Color::Black)
|
||||
.add_modifier(
|
||||
Modifier::UNDERLINED
|
||||
| Modifier::BOLD
|
||||
| Modifier::DIM
|
||||
| Modifier::SLOW_BLINK
|
||||
| Modifier::REVERSED
|
||||
| Modifier::CROSSED_OUT,
|
||||
);
|
||||
assert_eq!(
|
||||
"hello"
|
||||
.on_black()
|
||||
.black()
|
||||
.bold()
|
||||
.underlined()
|
||||
.dim()
|
||||
.slow_blink()
|
||||
.crossed_out()
|
||||
.reversed(),
|
||||
Span::styled("hello", all_modifier_black)
|
||||
);
|
||||
}
|
||||
}
|
||||
277
src/symbols.rs
277
src/symbols.rs
@@ -1,36 +1,303 @@
|
||||
pub mod block {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▉";
|
||||
pub const THREE_QUATERS: &str = "▊";
|
||||
pub const THREE_QUARTERS: &str = "▊";
|
||||
pub const FIVE_EIGHTHS: &str = "▋";
|
||||
pub const HALF: &str = "▌";
|
||||
pub const THREE_EIGHTHS: &str = "▍";
|
||||
pub const ONE_QUATER: &str = "▎";
|
||||
pub const ONE_QUARTER: &str = "▎";
|
||||
pub const ONE_EIGHTH: &str = "▏";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Set {
|
||||
pub full: &'static str,
|
||||
pub seven_eighths: &'static str,
|
||||
pub three_quarters: &'static str,
|
||||
pub five_eighths: &'static str,
|
||||
pub half: &'static str,
|
||||
pub three_eighths: &'static str,
|
||||
pub one_quarter: &'static str,
|
||||
pub one_eighth: &'static str,
|
||||
pub empty: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NINE_LEVELS
|
||||
}
|
||||
}
|
||||
|
||||
pub const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
three_quarters: HALF,
|
||||
five_eighths: HALF,
|
||||
half: HALF,
|
||||
three_eighths: HALF,
|
||||
one_quarter: HALF,
|
||||
one_eighth: " ",
|
||||
empty: " ",
|
||||
};
|
||||
|
||||
pub const NINE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: SEVEN_EIGHTHS,
|
||||
three_quarters: THREE_QUARTERS,
|
||||
five_eighths: FIVE_EIGHTHS,
|
||||
half: HALF,
|
||||
three_eighths: THREE_EIGHTHS,
|
||||
one_quarter: ONE_QUARTER,
|
||||
one_eighth: ONE_EIGHTH,
|
||||
empty: " ",
|
||||
};
|
||||
}
|
||||
|
||||
pub mod bar {
|
||||
pub const FULL: &str = "█";
|
||||
pub const SEVEN_EIGHTHS: &str = "▇";
|
||||
pub const THREE_QUATERS: &str = "▆";
|
||||
pub const THREE_QUARTERS: &str = "▆";
|
||||
pub const FIVE_EIGHTHS: &str = "▅";
|
||||
pub const HALF: &str = "▄";
|
||||
pub const THREE_EIGHTHS: &str = "▃";
|
||||
pub const ONE_QUATER: &str = "▂";
|
||||
pub const ONE_QUARTER: &str = "▂";
|
||||
pub const ONE_EIGHTH: &str = "▁";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Set {
|
||||
pub full: &'static str,
|
||||
pub seven_eighths: &'static str,
|
||||
pub three_quarters: &'static str,
|
||||
pub five_eighths: &'static str,
|
||||
pub half: &'static str,
|
||||
pub three_eighths: &'static str,
|
||||
pub one_quarter: &'static str,
|
||||
pub one_eighth: &'static str,
|
||||
pub empty: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NINE_LEVELS
|
||||
}
|
||||
}
|
||||
|
||||
pub const THREE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: FULL,
|
||||
three_quarters: HALF,
|
||||
five_eighths: HALF,
|
||||
half: HALF,
|
||||
three_eighths: HALF,
|
||||
one_quarter: HALF,
|
||||
one_eighth: " ",
|
||||
empty: " ",
|
||||
};
|
||||
|
||||
pub const NINE_LEVELS: Set = Set {
|
||||
full: FULL,
|
||||
seven_eighths: SEVEN_EIGHTHS,
|
||||
three_quarters: THREE_QUARTERS,
|
||||
five_eighths: FIVE_EIGHTHS,
|
||||
half: HALF,
|
||||
three_eighths: THREE_EIGHTHS,
|
||||
one_quarter: ONE_QUARTER,
|
||||
one_eighth: ONE_EIGHTH,
|
||||
empty: " ",
|
||||
};
|
||||
}
|
||||
|
||||
pub mod line {
|
||||
pub const TOP_RIGHT: &str = "┐";
|
||||
pub const VERTICAL: &str = "│";
|
||||
pub const DOUBLE_VERTICAL: &str = "║";
|
||||
pub const THICK_VERTICAL: &str = "┃";
|
||||
|
||||
pub const HORIZONTAL: &str = "─";
|
||||
pub const DOUBLE_HORIZONTAL: &str = "═";
|
||||
pub const THICK_HORIZONTAL: &str = "━";
|
||||
|
||||
pub const TOP_RIGHT: &str = "┐";
|
||||
pub const ROUNDED_TOP_RIGHT: &str = "╮";
|
||||
pub const DOUBLE_TOP_RIGHT: &str = "╗";
|
||||
pub const THICK_TOP_RIGHT: &str = "┓";
|
||||
|
||||
pub const TOP_LEFT: &str = "┌";
|
||||
pub const ROUNDED_TOP_LEFT: &str = "╭";
|
||||
pub const DOUBLE_TOP_LEFT: &str = "╔";
|
||||
pub const THICK_TOP_LEFT: &str = "┏";
|
||||
|
||||
pub const BOTTOM_RIGHT: &str = "┘";
|
||||
pub const ROUNDED_BOTTOM_RIGHT: &str = "╯";
|
||||
pub const DOUBLE_BOTTOM_RIGHT: &str = "╝";
|
||||
pub const THICK_BOTTOM_RIGHT: &str = "┛";
|
||||
|
||||
pub const BOTTOM_LEFT: &str = "└";
|
||||
pub const ROUNDED_BOTTOM_LEFT: &str = "╰";
|
||||
pub const DOUBLE_BOTTOM_LEFT: &str = "╚";
|
||||
pub const THICK_BOTTOM_LEFT: &str = "┗";
|
||||
|
||||
pub const VERTICAL_LEFT: &str = "┤";
|
||||
pub const DOUBLE_VERTICAL_LEFT: &str = "╣";
|
||||
pub const THICK_VERTICAL_LEFT: &str = "┫";
|
||||
|
||||
pub const VERTICAL_RIGHT: &str = "├";
|
||||
pub const DOUBLE_VERTICAL_RIGHT: &str = "╠";
|
||||
pub const THICK_VERTICAL_RIGHT: &str = "┣";
|
||||
|
||||
pub const HORIZONTAL_DOWN: &str = "┬";
|
||||
pub const DOUBLE_HORIZONTAL_DOWN: &str = "╦";
|
||||
pub const THICK_HORIZONTAL_DOWN: &str = "┳";
|
||||
|
||||
pub const HORIZONTAL_UP: &str = "┴";
|
||||
pub const DOUBLE_HORIZONTAL_UP: &str = "╩";
|
||||
pub const THICK_HORIZONTAL_UP: &str = "┻";
|
||||
|
||||
pub const CROSS: &str = "┼";
|
||||
pub const DOUBLE_CROSS: &str = "╬";
|
||||
pub const THICK_CROSS: &str = "╋";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Set {
|
||||
pub vertical: &'static str,
|
||||
pub horizontal: &'static str,
|
||||
pub top_right: &'static str,
|
||||
pub top_left: &'static str,
|
||||
pub bottom_right: &'static str,
|
||||
pub bottom_left: &'static str,
|
||||
pub vertical_left: &'static str,
|
||||
pub vertical_right: &'static str,
|
||||
pub horizontal_down: &'static str,
|
||||
pub horizontal_up: &'static str,
|
||||
pub cross: &'static str,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
pub const NORMAL: Set = Set {
|
||||
vertical: VERTICAL,
|
||||
horizontal: HORIZONTAL,
|
||||
top_right: TOP_RIGHT,
|
||||
top_left: TOP_LEFT,
|
||||
bottom_right: BOTTOM_RIGHT,
|
||||
bottom_left: BOTTOM_LEFT,
|
||||
vertical_left: VERTICAL_LEFT,
|
||||
vertical_right: VERTICAL_RIGHT,
|
||||
horizontal_down: HORIZONTAL_DOWN,
|
||||
horizontal_up: HORIZONTAL_UP,
|
||||
cross: CROSS,
|
||||
};
|
||||
|
||||
pub const ROUNDED: Set = Set {
|
||||
top_right: ROUNDED_TOP_RIGHT,
|
||||
top_left: ROUNDED_TOP_LEFT,
|
||||
bottom_right: ROUNDED_BOTTOM_RIGHT,
|
||||
bottom_left: ROUNDED_BOTTOM_LEFT,
|
||||
..NORMAL
|
||||
};
|
||||
|
||||
pub const DOUBLE: Set = Set {
|
||||
vertical: DOUBLE_VERTICAL,
|
||||
horizontal: DOUBLE_HORIZONTAL,
|
||||
top_right: DOUBLE_TOP_RIGHT,
|
||||
top_left: DOUBLE_TOP_LEFT,
|
||||
bottom_right: DOUBLE_BOTTOM_RIGHT,
|
||||
bottom_left: DOUBLE_BOTTOM_LEFT,
|
||||
vertical_left: DOUBLE_VERTICAL_LEFT,
|
||||
vertical_right: DOUBLE_VERTICAL_RIGHT,
|
||||
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
|
||||
horizontal_up: DOUBLE_HORIZONTAL_UP,
|
||||
cross: DOUBLE_CROSS,
|
||||
};
|
||||
|
||||
pub const THICK: Set = Set {
|
||||
vertical: THICK_VERTICAL,
|
||||
horizontal: THICK_HORIZONTAL,
|
||||
top_right: THICK_TOP_RIGHT,
|
||||
top_left: THICK_TOP_LEFT,
|
||||
bottom_right: THICK_BOTTOM_RIGHT,
|
||||
bottom_left: THICK_BOTTOM_LEFT,
|
||||
vertical_left: THICK_VERTICAL_LEFT,
|
||||
vertical_right: THICK_VERTICAL_RIGHT,
|
||||
horizontal_down: THICK_HORIZONTAL_DOWN,
|
||||
horizontal_up: THICK_HORIZONTAL_UP,
|
||||
cross: THICK_CROSS,
|
||||
};
|
||||
}
|
||||
|
||||
pub const DOT: &str = "•";
|
||||
|
||||
pub mod braille {
|
||||
pub const BLANK: u16 = 0x2800;
|
||||
pub const DOTS: [[u16; 2]; 4] = [
|
||||
[0x0001, 0x0008],
|
||||
[0x0002, 0x0010],
|
||||
[0x0004, 0x0020],
|
||||
[0x0040, 0x0080],
|
||||
];
|
||||
}
|
||||
|
||||
/// Marker to use when plotting data points
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub enum Marker {
|
||||
/// One point per cell in shape of dot
|
||||
#[default]
|
||||
Dot,
|
||||
/// One point per cell in shape of a block
|
||||
Block,
|
||||
/// One point per cell in the shape of a bar
|
||||
Bar,
|
||||
/// Up to 8 points per cell
|
||||
Braille,
|
||||
}
|
||||
|
||||
pub mod scrollbar {
|
||||
use super::{block, line};
|
||||
|
||||
/// Scrollbar Set
|
||||
/// ```text
|
||||
/// <--▮------->
|
||||
/// ^ ^ ^ ^
|
||||
/// │ │ │ └ end
|
||||
/// │ │ └──── track
|
||||
/// │ └──────── thumb
|
||||
/// └─────────── begin
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Set {
|
||||
pub track: &'static str,
|
||||
pub thumb: &'static str,
|
||||
pub begin: &'static str,
|
||||
pub end: &'static str,
|
||||
}
|
||||
|
||||
pub const DOUBLE_VERTICAL: Set = Set {
|
||||
track: line::DOUBLE_VERTICAL,
|
||||
thumb: block::FULL,
|
||||
begin: "▲",
|
||||
end: "▼",
|
||||
};
|
||||
|
||||
pub const DOUBLE_HORIZONTAL: Set = Set {
|
||||
track: line::DOUBLE_HORIZONTAL,
|
||||
thumb: block::FULL,
|
||||
begin: "◄",
|
||||
end: "►",
|
||||
};
|
||||
|
||||
pub const VERTICAL: Set = Set {
|
||||
track: line::VERTICAL,
|
||||
thumb: block::FULL,
|
||||
begin: "↑",
|
||||
end: "↓",
|
||||
};
|
||||
|
||||
pub const HORIZONTAL: Set = Set {
|
||||
track: line::HORIZONTAL,
|
||||
thumb: block::FULL,
|
||||
begin: "←",
|
||||
end: "→",
|
||||
};
|
||||
}
|
||||
|
||||
458
src/terminal.rs
458
src/terminal.rs
@@ -1,12 +1,29 @@
|
||||
use std::io;
|
||||
|
||||
use backend::Backend;
|
||||
use buffer::Buffer;
|
||||
use layout::Rect;
|
||||
use widgets::Widget;
|
||||
use crate::{
|
||||
backend::{Backend, ClearType},
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub enum Viewport {
|
||||
#[default]
|
||||
Fullscreen,
|
||||
Inline(u16),
|
||||
Fixed(Rect),
|
||||
}
|
||||
|
||||
/// Options to pass to [`Terminal::with_options`]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub struct TerminalOptions {
|
||||
/// Viewport used to draw to the terminal
|
||||
pub viewport: Viewport,
|
||||
}
|
||||
|
||||
/// Interface to the terminal backed by Termion
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
@@ -17,25 +34,128 @@ where
|
||||
buffers: [Buffer; 2],
|
||||
/// Index of the current buffer in the previous array
|
||||
current: usize,
|
||||
/// Whether the cursor is currently hidden
|
||||
hidden_cursor: bool,
|
||||
/// Viewport
|
||||
viewport: Viewport,
|
||||
viewport_area: Rect,
|
||||
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
|
||||
last_known_size: Rect,
|
||||
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||
/// and the terminal resized.
|
||||
last_known_cursor_pos: (u16, u16),
|
||||
}
|
||||
|
||||
/// Represents a consistent terminal interface for rendering.
|
||||
#[derive(Debug)]
|
||||
pub struct Frame<'a, B: 'a>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
terminal: &'a mut Terminal<B>,
|
||||
|
||||
/// Where should the cursor be after drawing this frame?
|
||||
///
|
||||
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
|
||||
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
|
||||
cursor_position: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
impl<'a, B> Frame<'a, B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Calls the draw method of a given widget on the current buffer
|
||||
pub fn render<W>(&mut self, widget: &mut W, area: Rect)
|
||||
/// Frame size, guaranteed not to change when rendering.
|
||||
pub fn size(&self) -> Rect {
|
||||
self.terminal.viewport_area
|
||||
}
|
||||
|
||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::Block;
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let block = Block::default();
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_widget(block, area);
|
||||
/// ```
|
||||
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
|
||||
where
|
||||
W: Widget,
|
||||
{
|
||||
widget.draw(area, self.terminal.current_buffer_mut());
|
||||
widget.render(area, self.terminal.current_buffer_mut());
|
||||
}
|
||||
|
||||
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
|
||||
///
|
||||
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
|
||||
/// given [`StatefulWidget`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::Terminal;
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # use ratatui::layout::Rect;
|
||||
/// # use ratatui::widgets::{List, ListItem, ListState};
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select(Some(1));
|
||||
/// let items = vec![
|
||||
/// ListItem::new("Item 1"),
|
||||
/// ListItem::new("Item 2"),
|
||||
/// ];
|
||||
/// let list = List::new(items);
|
||||
/// let area = Rect::new(0, 0, 5, 5);
|
||||
/// let mut frame = terminal.get_frame();
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// ```
|
||||
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
||||
where
|
||||
W: StatefulWidget,
|
||||
{
|
||||
widget.render(area, self.terminal.current_buffer_mut(), state);
|
||||
}
|
||||
|
||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
||||
///
|
||||
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
|
||||
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
|
||||
/// with it.
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
||||
self.cursor_position = Some((x, y));
|
||||
}
|
||||
}
|
||||
|
||||
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
|
||||
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
|
||||
/// [`Terminal::draw`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CompletedFrame<'a> {
|
||||
pub buffer: &'a Buffer,
|
||||
pub area: Rect,
|
||||
}
|
||||
|
||||
impl<B> Drop for Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
// Attempt to restore the cursor state
|
||||
if self.hidden_cursor {
|
||||
if let Err(err) = self.show_cursor() {
|
||||
eprintln!("Failed to show the cursor: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,19 +163,45 @@ impl<B> Terminal<B>
|
||||
where
|
||||
B: Backend,
|
||||
{
|
||||
/// Wrapper around Termion initialization. Each buffer is initialized with a blank string and
|
||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||
/// default colors for the foreground and the background
|
||||
pub fn new(backend: B) -> Result<Terminal<B>, io::Error> {
|
||||
let size = backend.size()?;
|
||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||
Terminal::with_options(
|
||||
backend,
|
||||
TerminalOptions {
|
||||
viewport: Viewport::Fullscreen,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||
let size = match options.viewport {
|
||||
Viewport::Fullscreen | Viewport::Inline(_) => backend.size()?,
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
let (viewport_area, cursor_pos) = match options.viewport {
|
||||
Viewport::Fullscreen => (size, (0, 0)),
|
||||
Viewport::Inline(height) => compute_inline_size(&mut backend, height, size, 0)?,
|
||||
Viewport::Fixed(area) => (area, (area.left(), area.top())),
|
||||
};
|
||||
Ok(Terminal {
|
||||
backend,
|
||||
buffers: [Buffer::empty(size), Buffer::empty(size)],
|
||||
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||
current: 0,
|
||||
hidden_cursor: false,
|
||||
viewport: options.viewport,
|
||||
viewport_area,
|
||||
last_known_size: size,
|
||||
last_known_cursor_pos: cursor_pos,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
||||
pub fn get_frame(&mut self) -> Frame<B> {
|
||||
Frame { terminal: self }
|
||||
Frame {
|
||||
terminal: self,
|
||||
cursor_position: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
||||
@@ -70,66 +216,274 @@ where
|
||||
&mut self.backend
|
||||
}
|
||||
|
||||
/// Builds a string representing the minimal escape sequences and characters set necessary to
|
||||
/// update the UI and writes it to stdout.
|
||||
pub fn flush(&mut self) -> Result<(), io::Error> {
|
||||
let width = self.buffers[self.current].area.width;
|
||||
let content = self.buffers[self.current]
|
||||
.content
|
||||
.iter()
|
||||
.zip(self.buffers[1 - self.current].content.iter())
|
||||
.enumerate()
|
||||
.filter_map(|(i, (c, p))| {
|
||||
if c != p {
|
||||
let i = i as u16;
|
||||
let x = i % width;
|
||||
let y = i / width;
|
||||
Some((x, y, c))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
self.backend.draw(content)
|
||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
||||
/// current backend for drawing.
|
||||
pub fn flush(&mut self) -> io::Result<()> {
|
||||
let previous_buffer = &self.buffers[1 - self.current];
|
||||
let current_buffer = &self.buffers[self.current];
|
||||
let updates = previous_buffer.diff(current_buffer);
|
||||
if let Some((col, row, _)) = updates.last() {
|
||||
self.last_known_cursor_pos = (*col, *row);
|
||||
}
|
||||
self.backend.draw(updates.into_iter())
|
||||
}
|
||||
|
||||
/// Updates the interface so that internal buffers matches the current size of the terminal.
|
||||
/// This leads to a full redraw of the screen.
|
||||
pub fn resize(&mut self, area: Rect) -> Result<(), io::Error> {
|
||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
||||
/// be saved so the size can remain consistent when rendering.
|
||||
/// This leads to a full clear of the screen.
|
||||
pub fn resize(&mut self, size: Rect) -> io::Result<()> {
|
||||
let next_area = match self.viewport {
|
||||
Viewport::Fullscreen => size,
|
||||
Viewport::Inline(height) => {
|
||||
let offset_in_previous_viewport = self
|
||||
.last_known_cursor_pos
|
||||
.1
|
||||
.saturating_sub(self.viewport_area.top());
|
||||
compute_inline_size(&mut self.backend, height, size, offset_in_previous_viewport)?.0
|
||||
}
|
||||
Viewport::Fixed(area) => area,
|
||||
};
|
||||
self.set_viewport_area(next_area);
|
||||
self.clear()?;
|
||||
|
||||
self.last_known_size = size;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_viewport_area(&mut self, area: Rect) {
|
||||
self.buffers[self.current].resize(area);
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.buffers[1 - self.current].resize(area);
|
||||
self.backend.clear()
|
||||
self.viewport_area = area;
|
||||
}
|
||||
|
||||
/// Flushes the current internal state and prepares the interface for the next draw call
|
||||
pub fn draw<F>(&mut self, f: F) -> Result<(), io::Error>
|
||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
// fixed viewports do not get autoresized
|
||||
if matches!(self.viewport, Viewport::Fullscreen | Viewport::Inline(_)) {
|
||||
let size = self.size()?;
|
||||
if size != self.last_known_size {
|
||||
self.resize(size)?;
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||
/// and prepares for the next draw call.
|
||||
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
|
||||
where
|
||||
F: FnOnce(Frame<B>),
|
||||
F: FnOnce(&mut Frame<B>),
|
||||
{
|
||||
f(self.get_frame());
|
||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||
// and the terminal (if growing), which may OOB.
|
||||
self.autoresize()?;
|
||||
|
||||
let mut frame = self.get_frame();
|
||||
f(&mut frame);
|
||||
// We can't change the cursor position right away because we have to flush the frame to
|
||||
// stdout first. But we also can't keep the frame around, since it holds a &mut to
|
||||
// Terminal. Thus, we're taking the important data out of the Frame and dropping it.
|
||||
let cursor_position = frame.cursor_position;
|
||||
|
||||
// Draw to stdout
|
||||
self.flush()?;
|
||||
|
||||
// Swap buffers
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
match cursor_position {
|
||||
None => self.hide_cursor()?,
|
||||
Some((x, y)) => {
|
||||
self.show_cursor()?;
|
||||
self.set_cursor(x, y)?;
|
||||
}
|
||||
}
|
||||
|
||||
self.swap_buffers();
|
||||
|
||||
// Flush
|
||||
self.backend.flush()?;
|
||||
|
||||
Ok(CompletedFrame {
|
||||
buffer: &self.buffers[1 - self.current],
|
||||
area: self.last_known_size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.hide_cursor()?;
|
||||
self.hidden_cursor = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.backend.hide_cursor()
|
||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.backend.show_cursor()?;
|
||||
self.hidden_cursor = false;
|
||||
Ok(())
|
||||
}
|
||||
pub fn show_cursor(&mut self) -> Result<(), io::Error> {
|
||||
self.backend.show_cursor()
|
||||
|
||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||
self.backend.get_cursor()
|
||||
}
|
||||
pub fn clear(&mut self) -> Result<(), io::Error> {
|
||||
self.backend.clear()
|
||||
|
||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||
self.backend.set_cursor(x, y)?;
|
||||
self.last_known_cursor_pos = (x, y);
|
||||
Ok(())
|
||||
}
|
||||
pub fn size(&self) -> Result<Rect, io::Error> {
|
||||
|
||||
/// Clear the terminal and force a full redraw on the next draw call.
|
||||
pub fn clear(&mut self) -> io::Result<()> {
|
||||
match self.viewport {
|
||||
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
|
||||
Viewport::Inline(_) => {
|
||||
self.backend
|
||||
.set_cursor(self.viewport_area.left(), self.viewport_area.top())?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
Viewport::Fixed(area) => {
|
||||
for row in area.top()..area.bottom() {
|
||||
self.backend.set_cursor(0, row)?;
|
||||
self.backend.clear_region(ClearType::AfterCursor)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset the back buffer to make sure the next update will redraw everything.
|
||||
self.buffers[1 - self.current].reset();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clears the inactive buffer and swaps it with the current buffer
|
||||
pub fn swap_buffers(&mut self) {
|
||||
self.buffers[1 - self.current].reset();
|
||||
self.current = 1 - self.current;
|
||||
}
|
||||
|
||||
/// Queries the real size of the backend.
|
||||
pub fn size(&self) -> io::Result<Rect> {
|
||||
self.backend.size()
|
||||
}
|
||||
|
||||
/// Insert some content before the current inline viewport. This has no effect when the
|
||||
/// viewport is fullscreen.
|
||||
///
|
||||
/// This function scrolls down the current viewport by the given height. The newly freed space
|
||||
/// is then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||
///
|
||||
/// Before:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
/// ```ignore
|
||||
/// +-------------------+
|
||||
/// | buffer |
|
||||
/// +-------------------+
|
||||
/// +-------------------+
|
||||
/// | |
|
||||
/// | viewport |
|
||||
/// | |
|
||||
/// +-------------------+
|
||||
/// ```
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Insert a single line before the current viewport
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::widgets::{Paragraph, Widget};
|
||||
/// # use ratatui::text::{Line, Span};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// # use ratatui::{Terminal};
|
||||
/// # use ratatui::backend::TestBackend;
|
||||
/// # let backend = TestBackend::new(10, 10);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
/// terminal.insert_before(1, |buf| {
|
||||
/// Paragraph::new(Line::from(vec![
|
||||
/// Span::raw("This line will be added "),
|
||||
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||
/// Span::raw(" the current viewport")
|
||||
/// ])).render(buf.area, buf);
|
||||
/// });
|
||||
/// ```
|
||||
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||
where
|
||||
F: FnOnce(&mut Buffer),
|
||||
{
|
||||
if !matches!(self.viewport, Viewport::Inline(_)) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.clear()?;
|
||||
let height = height.min(self.last_known_size.height);
|
||||
self.backend.append_lines(height)?;
|
||||
let missing_lines =
|
||||
height.saturating_sub(self.last_known_size.bottom() - self.viewport_area.top());
|
||||
let area = Rect {
|
||||
x: self.viewport_area.left(),
|
||||
y: self.viewport_area.top().saturating_sub(missing_lines),
|
||||
width: self.viewport_area.width,
|
||||
height,
|
||||
};
|
||||
let mut buffer = Buffer::empty(area);
|
||||
|
||||
draw_fn(&mut buffer);
|
||||
|
||||
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
|
||||
let (x, y) = buffer.pos_of(i);
|
||||
(x, y, c)
|
||||
});
|
||||
self.backend.draw(iter)?;
|
||||
self.backend.flush()?;
|
||||
|
||||
let remaining_lines = self.last_known_size.height - area.bottom();
|
||||
let missing_lines = self.viewport_area.height.saturating_sub(remaining_lines);
|
||||
self.backend.append_lines(self.viewport_area.height)?;
|
||||
|
||||
self.set_viewport_area(Rect {
|
||||
x: area.left(),
|
||||
y: area.bottom().saturating_sub(missing_lines),
|
||||
width: area.width,
|
||||
height: self.viewport_area.height,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inline_size<B: Backend>(
|
||||
backend: &mut B,
|
||||
height: u16,
|
||||
size: Rect,
|
||||
offset_in_previous_viewport: u16,
|
||||
) -> io::Result<(Rect, (u16, u16))> {
|
||||
let pos = backend.get_cursor()?;
|
||||
let mut row = pos.1;
|
||||
|
||||
let max_height = size.height.min(height);
|
||||
|
||||
let lines_after_cursor = height
|
||||
.saturating_sub(offset_in_previous_viewport)
|
||||
.saturating_sub(1);
|
||||
|
||||
backend.append_lines(lines_after_cursor)?;
|
||||
|
||||
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||
if missing_lines > 0 {
|
||||
row = row.saturating_sub(missing_lines);
|
||||
}
|
||||
row = row.saturating_sub(offset_in_previous_viewport);
|
||||
|
||||
Ok((
|
||||
Rect {
|
||||
x: 0,
|
||||
y: row,
|
||||
width: size.width,
|
||||
height: max_height,
|
||||
},
|
||||
pos,
|
||||
))
|
||||
}
|
||||
|
||||
31
src/text/grapheme.rs
Normal file
31
src/text/grapheme.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use crate::style::{Style, Styled};
|
||||
|
||||
/// A grapheme associated to a style.
|
||||
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,
|
||||
/// it actually is not a member of the text type hierarchy (`Text` -> `Line` -> `Span`).
|
||||
/// It is a separate type used mostly for rendering purposes. A `Span` consists of components that
|
||||
/// can be split into `StyledGrapheme`s, but it does not contain a collection of `StyledGrapheme`s.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct StyledGrapheme<'a> {
|
||||
pub symbol: &'a str,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<'a> StyledGrapheme<'a> {
|
||||
pub fn new(symbol: &'a str, style: Style) -> StyledGrapheme<'a> {
|
||||
StyledGrapheme { symbol, style }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for StyledGrapheme<'a> {
|
||||
type Item = StyledGrapheme<'a>;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self::Item {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
333
src/text/line.rs
Normal file
333
src/text/line.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
#![allow(deprecated)]
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{Span, Spans, Style, StyledGrapheme};
|
||||
use crate::layout::Alignment;
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub struct Line<'a> {
|
||||
pub spans: Vec<Span<'a>>,
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
impl<'a> Line<'a> {
|
||||
/// Create a line with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Line;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Line<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Line::from(Span::styled(content, style))
|
||||
}
|
||||
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, line.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.spans.iter().map(Span::width).sum()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this line.
|
||||
///
|
||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Line, StyledGrapheme};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
/// assert_eq!(
|
||||
/// line.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("e", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("x", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("t", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
self.spans
|
||||
.iter()
|
||||
.flat_map(move |span| span.styled_graphemes(base_style))
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Line, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_line = Line::from(vec![
|
||||
/// Span::raw("My"),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// let mut styled_line = Line::from(vec![
|
||||
/// Span::styled("My", style),
|
||||
/// Span::styled(" text", style),
|
||||
/// ]);
|
||||
///
|
||||
/// assert_ne!(raw_line, styled_line);
|
||||
///
|
||||
/// raw_line.patch_style(style);
|
||||
/// assert_eq!(raw_line, styled_line);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for span in &mut self.spans {
|
||||
span.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of each Span in the Line.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut line = Line::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]);
|
||||
///
|
||||
/// line.reset_style();
|
||||
/// assert_eq!(Style::reset(), line.spans[0].style);
|
||||
/// assert_eq!(Style::reset(), line.spans[1].style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for span in &mut self.spans {
|
||||
span.reset_style();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// # use ratatui::text::{Span, Line};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(Some(Alignment::Right), line.alignment(Alignment::Right).alignment)
|
||||
/// ```
|
||||
pub fn alignment(self, alignment: Alignment) -> Self {
|
||||
Self {
|
||||
alignment: Some(alignment),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Line<'a> {
|
||||
fn from(s: String) -> Self {
|
||||
Self::from(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Line<'a> {
|
||||
fn from(s: &'a str) -> Self {
|
||||
Self::from(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Self {
|
||||
Self {
|
||||
spans,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Line<'a> {
|
||||
fn from(span: Span<'a>) -> Self {
|
||||
Self::from(vec![span])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for String {
|
||||
fn from(line: Line<'a>) -> String {
|
||||
line.spans.iter().fold(String::new(), |mut acc, s| {
|
||||
acc.push_str(s.content.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Spans<'a>> for Line<'a> {
|
||||
fn from(value: Spans<'a>) -> Self {
|
||||
Self::from(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Line, Span, Spans, StyledGrapheme},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_width() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
]);
|
||||
assert_eq!(7, line.width());
|
||||
|
||||
let empty_line = Line::default();
|
||||
assert_eq!(0, empty_line.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
let mut raw_line = Line::from(vec![Span::raw("My"), Span::raw(" text")]);
|
||||
let styled_line = Line::from(vec![
|
||||
Span::styled("My", style),
|
||||
Span::styled(" text", style),
|
||||
]);
|
||||
|
||||
assert_ne!(raw_line, styled_line);
|
||||
|
||||
raw_line.patch_style(style);
|
||||
assert_eq!(raw_line, styled_line);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_style() {
|
||||
let mut line = Line::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]);
|
||||
|
||||
line.reset_style();
|
||||
assert_eq!(Style::reset(), line.spans[0].style);
|
||||
assert_eq!(Style::reset(), line.spans[1].style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let s = "Hello, world!";
|
||||
let line = Line::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
let spans = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
let line = Line::from(spans.clone());
|
||||
assert_eq!(spans, line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let line = Line::from(span.clone());
|
||||
assert_eq!(vec![span], line.spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_spans() {
|
||||
let spans = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
assert_eq!(Line::from(Spans::from(spans.clone())), Line::from(spans));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
]);
|
||||
let s: String = line.into();
|
||||
assert_eq!("Hello, world!", s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alignment() {
|
||||
let line = Line::from("This is left").alignment(Alignment::Left);
|
||||
assert_eq!(Some(Alignment::Left), line.alignment);
|
||||
|
||||
let line = Line::from("This is default");
|
||||
assert_eq!(None, line.alignment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn styled_graphemes() {
|
||||
const RED: Style = Style::new().fg(Color::Red);
|
||||
const GREEN: Style = Style::new().fg(Color::Green);
|
||||
const BLUE: Style = Style::new().fg(Color::Blue);
|
||||
const RED_ON_WHITE: Style = Style::new().fg(Color::Red).bg(Color::White);
|
||||
const GREEN_ON_WHITE: Style = Style::new().fg(Color::Green).bg(Color::White);
|
||||
const BLUE_ON_WHITE: Style = Style::new().fg(Color::Blue).bg(Color::White);
|
||||
|
||||
let line = Line::from(vec![
|
||||
Span::styled("He", RED),
|
||||
Span::styled("ll", GREEN),
|
||||
Span::styled("o!", BLUE),
|
||||
]);
|
||||
let styled_graphemes = line
|
||||
.styled_graphemes(Style::new().bg(Color::White))
|
||||
.collect::<Vec<StyledGrapheme>>();
|
||||
assert_eq!(
|
||||
styled_graphemes,
|
||||
vec![
|
||||
StyledGrapheme::new("H", RED_ON_WHITE),
|
||||
StyledGrapheme::new("e", RED_ON_WHITE),
|
||||
StyledGrapheme::new("l", GREEN_ON_WHITE),
|
||||
StyledGrapheme::new("l", GREEN_ON_WHITE),
|
||||
StyledGrapheme::new("o", BLUE_ON_WHITE),
|
||||
StyledGrapheme::new("!", BLUE_ON_WHITE),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
143
src/text/masked.rs
Normal file
143
src/text/masked.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Debug, Display},
|
||||
};
|
||||
|
||||
use super::Text;
|
||||
|
||||
/// A wrapper around a string that is masked when displayed.
|
||||
///
|
||||
/// The masked string is displayed as a series of the same character.
|
||||
/// This might be used to display a password field or similar secure data.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{buffer::Buffer, layout::Rect, text::Masked, widgets::{Paragraph, Widget}};
|
||||
///
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
/// let password = Masked::new("12345", 'x');
|
||||
///
|
||||
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||
/// assert_eq!(buffer, Buffer::with_lines(vec!["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Masked<'a> {
|
||||
inner: Cow<'a, str>,
|
||||
mask_char: char,
|
||||
}
|
||||
|
||||
impl<'a> Masked<'a> {
|
||||
pub fn new(s: impl Into<Cow<'a, str>>, mask_char: char) -> Self {
|
||||
Self {
|
||||
inner: s.into(),
|
||||
mask_char,
|
||||
}
|
||||
}
|
||||
|
||||
/// The character to use for masking.
|
||||
pub fn mask_char(&self) -> char {
|
||||
self.mask_char
|
||||
}
|
||||
|
||||
/// The underlying string, with all characters masked.
|
||||
pub fn value(&self) -> Cow<'a, str> {
|
||||
self.inner.chars().map(|_| self.mask_char).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Masked<'_> {
|
||||
/// Debug representation of a masked string is the underlying string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.inner).map_err(|_| fmt::Error)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Masked<'_> {
|
||||
/// Display representation of a masked string is the masked string
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.value()).map_err(|_| fmt::Error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: &'a Masked) -> Cow<'a, str> {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Cow<'a, str> {
|
||||
fn from(masked: Masked<'a>) -> Cow<'a, str> {
|
||||
masked.value()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Masked<'_>> for Text<'a> {
|
||||
fn from(masked: &'a Masked) -> Text<'a> {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Masked<'a>> for Text<'a> {
|
||||
fn from(masked: Masked<'a>) -> Text<'a> {
|
||||
Text::raw(masked.value())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::text::Line;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.inner, "12345");
|
||||
assert_eq!(masked.mask_char, 'x');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.value(), "xxxxx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mask_char() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(masked.mask_char(), 'x');
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked:?}"), "12345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
assert_eq!(format!("{masked}"), "xxxxx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_text() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
|
||||
let text: Text = (&masked).into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
|
||||
let text: Text = masked.into();
|
||||
assert_eq!(text.lines, vec![Line::from("xxxxx")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_cow() {
|
||||
let masked = Masked::new("12345", 'x');
|
||||
let cow: Cow<str> = (&masked).into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
|
||||
let cow: Cow<str> = masked.into();
|
||||
assert_eq!(cow, "xxxxx");
|
||||
}
|
||||
}
|
||||
71
src/text/mod.rs
Normal file
71
src/text/mod.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
//! Primitives for styled text.
|
||||
//!
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
|
||||
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
|
||||
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
|
||||
//! - A multiple line string where each grapheme may have its own style is represented by a
|
||||
//! [`Text`].
|
||||
//!
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Line`].
|
||||
//!
|
||||
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
|
||||
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
|
||||
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
|
||||
//! primitives when you need additional styling capabilities.
|
||||
//!
|
||||
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
|
||||
//! its `title` property (which is a [`Line`] under the hood):
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::widgets::Block;
|
||||
//! # use ratatui::text::{Span, Line};
|
||||
//! # use ratatui::style::{Color, Style};
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
//! // ])
|
||||
//! let block = Block::default().title("My title");
|
||||
//!
|
||||
//! // A simple string with a unique style.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(
|
||||
//! Span::styled("My title", Style::default().fg(Color::Yellow))
|
||||
//! );
|
||||
//!
|
||||
//! // A string with multiple styles.
|
||||
//! // Converted to Line(vec![
|
||||
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
|
||||
//! // Span { content: Cow::Borrowed(" title"), .. }
|
||||
//! // ])
|
||||
//! let block = Block::default().title(vec![
|
||||
//! Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
//! Span::raw(" title"),
|
||||
//! ]);
|
||||
//! ```
|
||||
|
||||
use crate::style::Style;
|
||||
|
||||
mod grapheme;
|
||||
pub use grapheme::StyledGrapheme;
|
||||
|
||||
mod line;
|
||||
pub use line::Line;
|
||||
|
||||
mod masked;
|
||||
pub use masked::Masked;
|
||||
|
||||
mod span;
|
||||
pub use span::Span;
|
||||
|
||||
/// We keep this for backward compatibility.
|
||||
mod spans;
|
||||
#[allow(deprecated)]
|
||||
pub use spans::Spans;
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod text;
|
||||
pub use text::Text;
|
||||
157
src/text/span.rs
Normal file
157
src/text/span.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use std::{borrow::Cow, fmt::Debug};
|
||||
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use super::StyledGrapheme;
|
||||
use crate::style::{Style, Styled};
|
||||
|
||||
/// A string where all graphemes have the same style.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub struct Span<'a> {
|
||||
pub content: Cow<'a, str>,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<'a> Span<'a> {
|
||||
/// Create a span with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// Span::raw("My text");
|
||||
/// Span::raw(String::from("My text"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a span with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Span::styled("My text", style);
|
||||
/// Span::styled(String::from("My text"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
Span {
|
||||
content: content.into(),
|
||||
style,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the width of the content held by this span.
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
|
||||
/// Returns an iterator over the graphemes held by this span.
|
||||
///
|
||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
||||
/// the resulting [`Style`].
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, StyledGrapheme};
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let span = Span::styled("Text", Style::default().fg(Color::Yellow));
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
/// assert_eq!(
|
||||
/// span.styled_graphemes(style).collect::<Vec<StyledGrapheme>>(),
|
||||
/// vec![
|
||||
/// StyledGrapheme::new("T", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("e", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("x", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// StyledGrapheme::new("t", Style::default().fg(Color::Yellow).bg(Color::Black)),
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
pub fn styled_graphemes(
|
||||
&'a self,
|
||||
base_style: Style,
|
||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
||||
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
|
||||
.map(move |g| StyledGrapheme {
|
||||
symbol: g,
|
||||
style: base_style.patch(self.style),
|
||||
})
|
||||
.filter(|s| s.symbol != "\n")
|
||||
}
|
||||
|
||||
/// Patches the style an existing Span, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_span = Span::raw("My text");
|
||||
/// let mut styled_span = Span::styled("My text", style);
|
||||
///
|
||||
/// assert_ne!(raw_span, styled_span);
|
||||
///
|
||||
/// raw_span.patch_style(style);
|
||||
/// assert_eq!(raw_span, styled_span);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
self.style = self.style.patch(style);
|
||||
}
|
||||
|
||||
/// Resets the style of the Span.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Span;
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut span = Span::styled("My text", Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC));
|
||||
///
|
||||
/// span.reset_style();
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
self.patch_style(Style::reset());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Span<'a> {
|
||||
fn from(s: String) -> Span<'a> {
|
||||
Span::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Span<'a> {
|
||||
fn from(s: &'a str) -> Span<'a> {
|
||||
Span::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Span<'a> {
|
||||
type Item = Span<'a>;
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
225
src/text/spans.rs
Normal file
225
src/text/spans.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
#![allow(deprecated)]
|
||||
|
||||
use super::{Span, Style};
|
||||
use crate::{layout::Alignment, text::Line};
|
||||
|
||||
/// A string composed of clusters of graphemes, each with their own style.
|
||||
///
|
||||
/// `Spans` has been deprecated in favor of `Line`, and will be removed in the
|
||||
/// future. All methods that accept Spans have been replaced with methods that
|
||||
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
|
||||
/// this crate to gradually transition to Line.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
#[deprecated(note = "Use `ratatui::text::Line` instead")]
|
||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
||||
|
||||
impl<'a> Spans<'a> {
|
||||
/// Returns the width of the underlying string.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style};
|
||||
/// let spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// assert_eq!(7, spans.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.0.iter().map(Span::width).sum()
|
||||
}
|
||||
|
||||
/// Patches the style of each Span in an existing Spans, adding modifiers from the given style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_spans = Spans::from(vec![
|
||||
/// Span::raw("My"),
|
||||
/// Span::raw(" text"),
|
||||
/// ]);
|
||||
/// let mut styled_spans = Spans::from(vec![
|
||||
/// Span::styled("My", style),
|
||||
/// Span::styled(" text", style),
|
||||
/// ]);
|
||||
///
|
||||
/// assert_ne!(raw_spans, styled_spans);
|
||||
///
|
||||
/// raw_spans.patch_style(style);
|
||||
/// assert_eq!(raw_spans, styled_spans);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for span in &mut self.0 {
|
||||
span.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of each Span in the Spans.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
/// ]);
|
||||
///
|
||||
/// spans.reset_style();
|
||||
/// assert_eq!(Style::reset(), spans.0[0].style);
|
||||
/// assert_eq!(Style::reset(), spans.0[1].style);
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for span in &mut self.0 {
|
||||
span.reset_style();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the target alignment for this line of text.
|
||||
/// Defaults to: [`None`], meaning the alignment is determined by the rendering widget.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::borrow::Cow;
|
||||
/// # use ratatui::layout::Alignment;
|
||||
/// # use ratatui::text::{Span, Spans};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let mut line = Spans::from("Hi, what's up?").alignment(Alignment::Right);
|
||||
/// assert_eq!(Some(Alignment::Right), line.alignment)
|
||||
/// ```
|
||||
pub fn alignment(self, alignment: Alignment) -> Line<'a> {
|
||||
let line = Line::from(self);
|
||||
line.alignment(alignment)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Spans<'a> {
|
||||
fn from(s: String) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Spans<'a> {
|
||||
fn from(s: &'a str) -> Spans<'a> {
|
||||
Spans(vec![Span::from(s)])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
|
||||
Spans(spans)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Spans<'a> {
|
||||
fn from(span: Span<'a>) -> Spans<'a> {
|
||||
Spans(vec![span])
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Spans<'a>> for String {
|
||||
fn from(line: Spans<'a>) -> String {
|
||||
line.0.iter().fold(String::new(), |mut acc, s| {
|
||||
acc.push_str(s.content.as_ref());
|
||||
acc
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_width() {
|
||||
let spans = Spans::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::raw(" text"),
|
||||
]);
|
||||
assert_eq!(7, spans.width());
|
||||
|
||||
let empty_spans = Spans::default();
|
||||
assert_eq!(0, empty_spans.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_patch_style() {
|
||||
let style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::ITALIC);
|
||||
let mut raw_spans = Spans::from(vec![Span::raw("My"), Span::raw(" text")]);
|
||||
let styled_spans = Spans::from(vec![
|
||||
Span::styled("My", style),
|
||||
Span::styled(" text", style),
|
||||
]);
|
||||
|
||||
assert_ne!(raw_spans, styled_spans);
|
||||
|
||||
raw_spans.patch_style(style);
|
||||
assert_eq!(raw_spans, styled_spans);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset_style() {
|
||||
let mut spans = Spans::from(vec![
|
||||
Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
Span::styled(" text", Style::default().add_modifier(Modifier::BOLD)),
|
||||
]);
|
||||
|
||||
spans.reset_style();
|
||||
assert_eq!(Style::reset(), spans.0[0].style);
|
||||
assert_eq!(Style::reset(), spans.0[1].style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
let s = String::from("Hello, world!");
|
||||
let spans = Spans::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
let s = "Hello, world!";
|
||||
let spans = Spans::from(s);
|
||||
assert_eq!(vec![Span::from("Hello, world!")], spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
let spans_vec = vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
];
|
||||
let spans = Spans::from(spans_vec.clone());
|
||||
assert_eq!(spans_vec, spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_span() {
|
||||
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
|
||||
let spans = Spans::from(span.clone());
|
||||
assert_eq!(vec![span], spans.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_string() {
|
||||
let spans = Spans::from(vec![
|
||||
Span::styled("Hello,", Style::default().fg(Color::Red)),
|
||||
Span::styled(" world!", Style::default().fg(Color::Green)),
|
||||
]);
|
||||
let s: String = spans.into();
|
||||
assert_eq!("Hello, world!", s);
|
||||
}
|
||||
}
|
||||
225
src/text/text.rs
Normal file
225
src/text/text.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[allow(deprecated)]
|
||||
use super::{Line, Span, Spans};
|
||||
use crate::style::Style;
|
||||
|
||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
||||
/// their own style.
|
||||
///
|
||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
/// let mut text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
///
|
||||
/// // Adding two more unstyled lines
|
||||
/// text.extend(Text::raw("These are two\nmore lines!"));
|
||||
/// assert_eq!(4, text.height());
|
||||
///
|
||||
/// // Adding a final two styled lines
|
||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
||||
/// assert_eq!(6, text.height());
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub struct Text<'a> {
|
||||
pub lines: Vec<Line<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
/// Create some text (potentially multiple lines) with no style.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
pub fn raw<T>(content: T) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let lines: Vec<_> = match content.into() {
|
||||
Cow::Borrowed("") => vec![Line::from("")],
|
||||
Cow::Borrowed(s) => s.lines().map(Line::from).collect(),
|
||||
Cow::Owned(s) if s.is_empty() => vec![Line::from("")],
|
||||
Cow::Owned(s) => s.lines().map(|l| Line::from(l.to_owned())).collect(),
|
||||
};
|
||||
|
||||
Text::from(lines)
|
||||
}
|
||||
|
||||
/// Create some text (potentially multiple lines) with a style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
{
|
||||
let mut text = Text::raw(content);
|
||||
text.patch_style(style);
|
||||
text
|
||||
}
|
||||
|
||||
/// Returns the max width of all the lines.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(15, text.width());
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.lines.iter().map(Line::width).max().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the height.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::text::Text;
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
/// ```
|
||||
pub fn height(&self) -> usize {
|
||||
self.lines.len()
|
||||
}
|
||||
|
||||
/// Patches the style of each line in an existing Text, adding modifiers from the given style.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::Text;
|
||||
/// # use ratatui::style::{Color, Modifier, Style};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// assert_ne!(raw_text, styled_text);
|
||||
///
|
||||
/// raw_text.patch_style(style);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
pub fn patch_style(&mut self, style: Style) {
|
||||
for line in &mut self.lines {
|
||||
line.patch_style(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the style of the Text.
|
||||
/// Equivalent to calling `patch_style(Style::reset())`.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::text::{Span, Line, Text};
|
||||
/// # use ratatui::style::{Color, Style, Modifier};
|
||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
||||
/// let mut text = Text::styled("The first line\nThe second line", style);
|
||||
///
|
||||
/// text.reset_style();
|
||||
/// for line in &text.lines {
|
||||
/// for span in &line.spans {
|
||||
/// assert_eq!(Style::reset(), span.style);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn reset_style(&mut self) {
|
||||
for line in &mut self.lines {
|
||||
line.reset_style();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Text<'a> {
|
||||
fn from(s: String) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Text<'a> {
|
||||
fn from(s: &'a str) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Text<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Text<'a> {
|
||||
Text::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Span<'a>> for Text<'a> {
|
||||
fn from(span: Span<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![Line::from(span)],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Spans<'a>> for Text<'a> {
|
||||
fn from(spans: Spans<'a>) -> Text<'a> {
|
||||
Text {
|
||||
lines: vec![spans.into()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Line<'a>> for Text<'a> {
|
||||
fn from(line: Line<'a>) -> Text<'a> {
|
||||
Text { lines: vec![line] }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
|
||||
Text {
|
||||
lines: lines.into_iter().map(|l| l.0.into()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Line<'a>>> for Text<'a> {
|
||||
fn from(lines: Vec<Line<'a>>) -> Text<'a> {
|
||||
Text { lines }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for Text<'a> {
|
||||
type Item = Line<'a>;
|
||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.lines.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Extend<T> for Text<'a>
|
||||
where
|
||||
T: Into<Line<'a>>,
|
||||
{
|
||||
fn extend<I: IntoIterator<Item = T>>(&mut self, iter: I) {
|
||||
let lines = iter.into_iter().map(Into::into);
|
||||
self.lines.extend(lines);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user