Compare commits
1540 Commits
v0.2.0
...
docs/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6d580bc96 | ||
|
|
392b28c194 | ||
|
|
5814c7c395 | ||
|
|
9fb054453d | ||
|
|
4393fae54c | ||
|
|
9ea70e28c6 | ||
|
|
774ab788d4 | ||
|
|
f05feac337 | ||
|
|
7eab88fe9a | ||
|
|
910d16e63a | ||
|
|
dbfb7da9e3 | ||
|
|
cb2a58aaa0 | ||
|
|
7e00b646fc | ||
|
|
8127590812 | ||
|
|
7c40c0bbdd | ||
|
|
d87354f400 | ||
|
|
e48bcf5f21 | ||
|
|
d4415204e1 | ||
|
|
92c4078413 | ||
|
|
38a1474ca1 | ||
|
|
621226f2e2 | ||
|
|
9ba7d25b71 | ||
|
|
bb94d1c0fa | ||
|
|
f5fc8197ff | ||
|
|
9f399ac7a6 | ||
|
|
37a1c6f89b | ||
|
|
104d6a6c2b | ||
|
|
fa8ca0121a | ||
|
|
f5fde0ef53 | ||
|
|
fc70288954 | ||
|
|
325f96102a | ||
|
|
867c4bc4e9 | ||
|
|
0b24e6c7bf | ||
|
|
f907c74bb3 | ||
|
|
d045305c67 | ||
|
|
7a308c2090 | ||
|
|
24e71ab6ce | ||
|
|
1f41a61008 | ||
|
|
7ad9c29eac | ||
|
|
a0a37008da | ||
|
|
fd08d5b541 | ||
|
|
9e319b92b2 | ||
|
|
985cd05573 | ||
|
|
57c2326574 | ||
|
|
a195d59a47 | ||
|
|
e7831aedd4 | ||
|
|
4a871f993e | ||
|
|
da05957fa0 | ||
|
|
41d883da7a | ||
|
|
0552223511 | ||
|
|
21a561b1b3 | ||
|
|
079d74ce14 | ||
|
|
22ec4f7414 | ||
|
|
6f213191ef | ||
|
|
088aac136d | ||
|
|
a692a6e371 | ||
|
|
1798512e94 | ||
|
|
32f3833a6d | ||
|
|
2ccc40e116 | ||
|
|
019e34e006 | ||
|
|
11cbb2ba87 | ||
|
|
50ba96518f | ||
|
|
904b0aa723 | ||
|
|
b544e394c9 | ||
|
|
1d28c89fe5 | ||
|
|
1d2882636e | ||
|
|
157cb3401b | ||
|
|
3d0c96a838 | ||
|
|
03066d81bf | ||
|
|
5f57d35234 | ||
|
|
5c021bf344 | ||
|
|
9721300a47 | ||
|
|
9a541981b8 | ||
|
|
a6a1368250 | ||
|
|
1fcca6369e | ||
|
|
694c788c24 | ||
|
|
6e436725e4 | ||
|
|
dafb716f9d | ||
|
|
819e92cd44 | ||
|
|
a38066d2d1 | ||
|
|
3646c97840 | ||
|
|
1c3b698b82 | ||
|
|
c767f6bc3c | ||
|
|
08ea837753 | ||
|
|
a8aca0ec12 | ||
|
|
ed5dd73084 | ||
|
|
b5f7e44183 | ||
|
|
fab532171d | ||
|
|
898aef6e2f | ||
|
|
f57b696fdc | ||
|
|
452366aa9e | ||
|
|
ff729b7607 | ||
|
|
6ddde0e8a8 | ||
|
|
93ad6b828c | ||
|
|
e411d9ec3e | ||
|
|
a0979d6871 | ||
|
|
ed071f3723 | ||
|
|
9275d3421c | ||
|
|
15f442a71e | ||
|
|
18e70d3d51 | ||
|
|
17bba14540 | ||
|
|
ce4856a65f | ||
|
|
f2451e7f1e | ||
|
|
7c8573f575 | ||
|
|
4f0a8b21af | ||
|
|
91147c4d75 | ||
|
|
6dd25a3111 | ||
|
|
35eba76b4d | ||
|
|
357ae7e251 | ||
|
|
3b13240728 | ||
|
|
d201b8e5dd | ||
|
|
21e62d84c2 | ||
|
|
d3f01ebf6e | ||
|
|
2892bddce6 | ||
|
|
fbf6050c86 | ||
|
|
16ba867c58 | ||
|
|
2b7ec5cb7f | ||
|
|
d291042e69 | ||
|
|
5c1c97d5a2 | ||
|
|
f51d1ccc07 | ||
|
|
d137456ca1 | ||
|
|
881fe3eff1 | ||
|
|
99ac005b06 | ||
|
|
f132fa1715 | ||
|
|
369b18eef2 | ||
|
|
2ce958e38c | ||
|
|
217c57cd60 | ||
|
|
3ae6bf1d6f | ||
|
|
ec30390446 | ||
|
|
56d5e05762 | ||
|
|
b76ad3b02e | ||
|
|
afd1ce179b | ||
|
|
8f282473b2 | ||
|
|
36e2d1bda1 | ||
|
|
9d5aba69e9 | ||
|
|
1b0d6b473b | ||
|
|
c8339494a8 | ||
|
|
3ef1face9a | ||
|
|
f4cbab4101 | ||
|
|
ae6a8501ee | ||
|
|
1bb41e7165 | ||
|
|
4d7704fba5 | ||
|
|
e4e95bcecf | ||
|
|
a41c97b413 | ||
|
|
46902f5587 | ||
|
|
e7085e3a3e | ||
|
|
9f90f7495f | ||
|
|
260af68a34 | ||
|
|
e461b724a6 | ||
|
|
02c8c9373e | ||
|
|
f40fa787d1 | ||
|
|
7b875091e1 | ||
|
|
17316ec5d0 | ||
|
|
eaa403856e | ||
|
|
e5e2316451 | ||
|
|
98df774d7f | ||
|
|
0a47ebd94b | ||
|
|
abe2f27328 | ||
|
|
fcde9cb9c3 | ||
|
|
2ef3583eff | ||
|
|
a6b579223f | ||
|
|
f1d0a18375 | ||
|
|
55fb2d2e56 | ||
|
|
836634734f | ||
|
|
f804c90f96 | ||
|
|
860e48b0f0 | ||
|
|
04e1b32cd2 | ||
|
|
bbaa9a5432 | ||
|
|
28732176e1 | ||
|
|
6515097434 | ||
|
|
4c4851ca3d | ||
|
|
4f5503dbf6 | ||
|
|
611086eba4 | ||
|
|
514d273875 | ||
|
|
60cc15bbb0 | ||
|
|
a52ee82fc7 | ||
|
|
381ec75329 | ||
|
|
f6f7794dd7 | ||
|
|
453a308b46 | ||
|
|
9fd1beedb2 | ||
|
|
8db7a9a44a | ||
|
|
b7e488507d | ||
|
|
4728f0e68b | ||
|
|
6db16d67fc | ||
|
|
cc7497532a | ||
|
|
d72968d86b | ||
|
|
7bdccce3d5 | ||
|
|
3df685e114 | ||
|
|
4069aa8274 | ||
|
|
e5a7609588 | ||
|
|
69e0cd2fc4 | ||
|
|
ab6b1feaec | ||
|
|
3a43274881 | ||
|
|
dc8d0587ec | ||
|
|
23c0d52c29 | ||
|
|
c32baa7cd8 | ||
|
|
1153a9ebaf | ||
|
|
2805dddf05 | ||
|
|
baf047f556 | ||
|
|
6745a10508 | ||
|
|
7799f4ff5b | ||
|
|
edcdc8a814 | ||
|
|
5ad623c29b | ||
|
|
bc10af5931 | ||
|
|
784f67a912 | ||
|
|
f4880b40cc | ||
|
|
67c0ea243b | ||
|
|
b9653ba05a | ||
|
|
9875d9facc | ||
|
|
b88717b65f | ||
|
|
5635b930c7 | ||
|
|
870bc6a64a | ||
|
|
da821b431e | ||
|
|
68886d1787 | ||
|
|
0f48239778 | ||
|
|
b13e2f9473 | ||
|
|
c777beb658 | ||
|
|
c45a4de47c | ||
|
|
20c88aaa5b | ||
|
|
e02947be61 | ||
|
|
3a90e2a761 | ||
|
|
65da535745 | ||
|
|
9ed85fd1dd | ||
|
|
aed60b9839 | ||
|
|
3631b34f53 | ||
|
|
0d5f3c091f | ||
|
|
ed51c4b342 | ||
|
|
23516bce76 | ||
|
|
6d1bd99544 | ||
|
|
2fb0b8a741 | ||
|
|
0256269a7f | ||
|
|
345e6a1ebd | ||
|
|
fdd5d8c092 | ||
|
|
8b624f5952 | ||
|
|
57d8b742e5 | ||
|
|
d5477b50d5 | ||
|
|
28f5a6dbd4 | ||
|
|
b75df78cdc | ||
|
|
730dfd4940 | ||
|
|
097ee86e39 | ||
|
|
3fdb5e8987 | ||
|
|
ec88bb81e5 | ||
|
|
f04bf855cb | ||
|
|
4753b7241b | ||
|
|
36fa3c11c1 | ||
|
|
69e8ed7db8 | ||
|
|
5f7a7fbe19 | ||
|
|
e6d2e04bcf | ||
|
|
45fcab7497 | ||
|
|
1b9bdd425c | ||
|
|
c68ee6c64a | ||
|
|
afe15349c8 | ||
|
|
fe4eeab676 | ||
|
|
a23ecd9b45 | ||
|
|
bb71e5ffd4 | ||
|
|
f2fa1ae9aa | ||
|
|
f687af7c0d | ||
|
|
f97e07c08a | ||
|
|
afc5cf2140 | ||
|
|
bb68bc6968 | ||
|
|
ffeb8e46b9 | ||
|
|
2fd5ae64bf | ||
|
|
82b70fd329 | ||
|
|
94328a2977 | ||
|
|
d468463fc6 | ||
|
|
6e7b4e4d55 | ||
|
|
864cd9ffef | ||
|
|
c08b522d34 | ||
|
|
716c93136e | ||
|
|
5eeb1ccbc4 | ||
|
|
f77503050f | ||
|
|
cd0d31c2dc | ||
|
|
41a910004d | ||
|
|
8433d0958b | ||
|
|
f28b973006 | ||
|
|
8d4a1026ab | ||
|
|
edc2af9822 | ||
|
|
a80a8a6a47 | ||
|
|
29c8c84fd0 | ||
|
|
c2d38509b4 | ||
|
|
b70cd03c02 | ||
|
|
476ac87c99 | ||
|
|
8857037bff | ||
|
|
e707ff11d1 | ||
|
|
a9fe4284ac | ||
|
|
ffc4300558 | ||
|
|
84cb16483a | ||
|
|
5b89bd04a8 | ||
|
|
32d0695cc2 | ||
|
|
cd93547db8 | ||
|
|
c245c13cc1 | ||
|
|
b2aa843b31 | ||
|
|
3ca920e881 | ||
|
|
b344f95b7c | ||
|
|
b304bb99bd | ||
|
|
be3eb75ea5 | ||
|
|
efef0d0dc0 | ||
|
|
663486f1e8 | ||
|
|
7ddfbc0010 | ||
|
|
3725262ca3 | ||
|
|
84f334163b | ||
|
|
03f3124c1d | ||
|
|
c34fb77818 | ||
|
|
f8a70ea9da | ||
|
|
d0f75eb371 | ||
|
|
6ce447c4f3 | ||
|
|
272d0591a7 | ||
|
|
e81663bec0 | ||
|
|
7e1bab049b | ||
|
|
379dab9cdb | ||
|
|
5b51018501 | ||
|
|
7bab9f0d80 | ||
|
|
6d210b3b6b | ||
|
|
935a7187c2 | ||
|
|
3bb374df88 | ||
|
|
50e5674a20 | ||
|
|
7eeb6afb3d | ||
|
|
810da72f26 | ||
|
|
7c0665cb0e | ||
|
|
ccf83e6d76 | ||
|
|
8cecfdf2f6 | ||
|
|
ca4fa0b9bf | ||
|
|
89d7dd4603 | ||
|
|
1b9e310300 | ||
|
|
8913e2ce1f | ||
|
|
c7649575e7 | ||
|
|
60bd7f4814 | ||
|
|
55e0880d2f | ||
|
|
3e7458fdb8 | ||
|
|
32a0b26525 | ||
|
|
36d49e549b | ||
|
|
ad54cf29ad | ||
|
|
a1acdcdc4c | ||
|
|
0a18dcb329 | ||
|
|
7ef2daee06 | ||
|
|
46977d8851 | ||
|
|
38bb196404 | ||
|
|
1908b06b4a | ||
|
|
3f2f2cd6ab | ||
|
|
efa965e1e8 | ||
|
|
127d706ee4 | ||
|
|
1365620606 | ||
|
|
cd64367e24 | ||
|
|
07efde5233 | ||
|
|
308c1df649 | ||
|
|
4bfdc15b80 | ||
|
|
7d175f85c1 | ||
|
|
d370aa75af | ||
|
|
7fdccafd52 | ||
|
|
10d778866e | ||
|
|
e6871b9e21 | ||
|
|
4f307e69db | ||
|
|
7f3efb02e6 | ||
|
|
7b45f74b71 | ||
|
|
1520ed9d10 | ||
|
|
2a74f9d8c1 | ||
|
|
d7ed6c8bad | ||
|
|
313135c68e | ||
|
|
8061813f32 | ||
|
|
74a32afbae | ||
|
|
4ce67fc84e | ||
|
|
df4b706674 | ||
|
|
8b447ec4d6 | ||
|
|
7a48c5b11b | ||
|
|
8cfc316bcc | ||
|
|
2f8a9363fc | ||
|
|
d92997105b | ||
|
|
42cda6d287 | ||
|
|
4f7791079e | ||
|
|
cf67ed9b88 | ||
|
|
8a60a561c9 | ||
|
|
35941809e1 | ||
|
|
73fd367a74 | ||
|
|
1de9a82b7a | ||
|
|
d6587bc6b0 | ||
|
|
f429f688da | ||
|
|
4770e71581 | ||
|
|
eef1afe915 | ||
|
|
257db6257f | ||
|
|
70df102de0 | ||
|
|
bf2036987f | ||
|
|
0b5fd6bf8e | ||
|
|
39d5a745ac | ||
|
|
2f97d35bd8 | ||
|
|
fafabb8dab | ||
|
|
fadc73d62e | ||
|
|
fcb5d589bb | ||
|
|
4955380932 | ||
|
|
828d17a3f5 | ||
|
|
4ac4d9d3ab | ||
|
|
be8def9639 | ||
|
|
9bd89c218a | ||
|
|
2cfe82a47e | ||
|
|
1a4bb1cbb8 | ||
|
|
839cca20bf | ||
|
|
f945a0bcff | ||
|
|
eb281df974 | ||
|
|
28e81c0714 | ||
|
|
ae2868c0e0 | ||
|
|
f71d1ac73e | ||
|
|
c50b01d098 | ||
|
|
204307fa50 | ||
|
|
76e5fe5a9a | ||
|
|
f78d3bfec3 | ||
|
|
366cbae09f | ||
|
|
ec763af851 | ||
|
|
699c2d7c8d | ||
|
|
3cc29bdada | ||
|
|
8de3d52469 | ||
|
|
b30411d1c7 | ||
|
|
6cf08d4a2f | ||
|
|
93372f35c1 | ||
|
|
3855c3a84a | ||
|
|
570c358681 | ||
|
|
8ecdd892f5 | ||
|
|
0dc5b2d2e0 | ||
|
|
8060f7bc57 | ||
|
|
d24747d469 | ||
|
|
a0c35f1d7b | ||
|
|
9a7467b305 | ||
|
|
f6d49dde14 | ||
|
|
07aff91b01 | ||
|
|
aa4260f92c | ||
|
|
5f1e119563 | ||
|
|
4d1784f2de | ||
|
|
baedc39494 | ||
|
|
366c2a0e6d | ||
|
|
bef2bc1e7c | ||
|
|
64eb3913a4 | ||
|
|
e95230beda | ||
|
|
f4637d40c3 | ||
|
|
da1ade7b2e | ||
|
|
1706b0a3e4 | ||
|
|
4392759501 | ||
|
|
20fc0ddfca | ||
|
|
3687f78f6a | ||
|
|
5fbb77ad20 | ||
|
|
97ee102f17 | ||
|
|
c442dfd1ad | ||
|
|
0a164965ea | ||
|
|
bf0923473c | ||
|
|
81b96338ea | ||
|
|
2e71c1874e | ||
|
|
c75aa1990f | ||
|
|
326a461f9a | ||
|
|
f3172c59d4 | ||
|
|
bef5bcf750 | ||
|
|
11264787d0 | ||
|
|
9b3c260b76 | ||
|
|
363c4c54e8 | ||
|
|
b7778e5cd1 | ||
|
|
b5061c5250 | ||
|
|
359204c929 | ||
|
|
14461c3a35 | ||
|
|
bf02106029 | ||
|
|
2c6f324b9a | ||
|
|
3b002fdcab | ||
|
|
6f52350ecf | ||
|
|
dcba0bcd5d | ||
|
|
b4aacb045e | ||
|
|
0207160784 | ||
|
|
26af65043e | ||
|
|
07da90a718 | ||
|
|
125ee929ee | ||
|
|
742a5ead06 | ||
|
|
8719608bda | ||
|
|
f6c4e447e6 | ||
|
|
c56f49b9fb | ||
|
|
078e97e4ff | ||
|
|
dce1e4b138 | ||
|
|
8e68db9e2f | ||
|
|
541f0f9953 | ||
|
|
88bfb5a430 | ||
|
|
c4ce7e8ff6 | ||
|
|
3be189e3c6 | ||
|
|
5c4efacd1d | ||
|
|
bbb6d65e06 | ||
|
|
fdb14dc7cd | ||
|
|
9b3b23ac14 | ||
|
|
58b6e0be0f | ||
|
|
6e6ba27a12 | ||
|
|
c870a41057 | ||
|
|
a6036ad789 | ||
|
|
060d26b6dc | ||
|
|
a4e84a6a7f | ||
|
|
fcbea9ee68 | ||
|
|
14b24e7585 | ||
|
|
5ed1f43c62 | ||
|
|
c8c7924e0c | ||
|
|
e3afe7c8a1 | ||
|
|
a1f54de7d6 | ||
|
|
b8ea190bf2 | ||
|
|
0de5238ed3 | ||
|
|
df5dddfbc9 | ||
|
|
f1398ae6cb | ||
|
|
525848ff4e | ||
|
|
660c7183c7 | ||
|
|
ab951fae81 | ||
|
|
3cd4369176 | ||
|
|
9bc014d7f1 | ||
|
|
36a0cd56e5 | ||
|
|
b831c5688c | ||
|
|
8195f526cb | ||
|
|
f7f66928a8 | ||
|
|
01418eb7c2 | ||
|
|
8536760e78 | ||
|
|
a558b19c9a | ||
|
|
183c07ef43 | ||
|
|
a13867ffce | ||
|
|
5b00e3aae9 | ||
|
|
38c17e091c | ||
|
|
3834374652 | ||
|
|
27680c05ce | ||
|
|
6fd5f631bb | ||
|
|
37b957c7e1 | ||
|
|
e02f4768ce | ||
|
|
c12bcfefa2 | ||
|
|
94f4547dcf | ||
|
|
3a6b8808ed | ||
|
|
1cff511934 | ||
|
|
b5bdde079e | ||
|
|
654949bb00 | ||
|
|
943c0431d9 | ||
|
|
65e7923753 | ||
|
|
d0067c8815 | ||
|
|
35e971f7eb | ||
|
|
b0314c5731 | ||
|
|
12f67e810f | ||
|
|
11b452d56f | ||
|
|
efd1e47642 | ||
|
|
410d08b2b5 | ||
|
|
a4892ad444 | ||
|
|
18870ce990 | ||
|
|
1f208ffd03 | ||
|
|
e51ca6e0d2 | ||
|
|
9182f47026 | ||
|
|
91040c0865 | ||
|
|
2202059259 | ||
|
|
8fb46301a0 | ||
|
|
0dcdbea083 | ||
|
|
74a051147a | ||
|
|
c3fb25898f | ||
|
|
fae5862c6e | ||
|
|
788e6d9fb8 | ||
|
|
14c67fbb52 | ||
|
|
096346350e | ||
|
|
61a827821d | ||
|
|
fbb5dfaaa9 | ||
|
|
d2d91f754c | ||
|
|
bcf43688ec | ||
|
|
b7942ee252 | ||
|
|
c8dd87918d | ||
|
|
652dc469ea | ||
|
|
87bf1dd9df | ||
|
|
f8367fdfdd | ||
|
|
9ba7354335 | ||
|
|
dab08b99b6 | ||
|
|
2a12f7bddf | ||
|
|
4278b4088d | ||
|
|
86168aa711 | ||
|
|
78f1c1446b | ||
|
|
9ec43eff1c | ||
|
|
4ee4e6d78a | ||
|
|
525479546a | ||
|
|
cf861232c7 | ||
|
|
dd5ca3a0c8 | ||
|
|
aeec16369b | ||
|
|
eb31617539 | ||
|
|
1ad629acd7 | ||
|
|
04e6ccd448 | ||
|
|
540fd2df03 | ||
|
|
984afd580b | ||
|
|
bbcfa55a88 | ||
|
|
1cbe1f52ab | ||
|
|
663bbde9c3 | ||
|
|
27e9216cea | ||
|
|
be4fdaa0c7 | ||
|
|
c1ed5c3637 | ||
|
|
4b8e54e811 | ||
|
|
5b7ad2ad82 | ||
|
|
ba20372c23 | ||
|
|
f383625f0e | ||
|
|
847bacf32e | ||
|
|
815757fcbb | ||
|
|
736605ec88 | ||
|
|
6e76729ce8 | ||
|
|
7f42ec9713 | ||
|
|
d7132011f9 | ||
|
|
d726e928d2 | ||
|
|
b80264de87 | ||
|
|
804c841fdc | ||
|
|
79ceb9f7b6 | ||
|
|
f780be31f3 | ||
|
|
eb1484b6db | ||
|
|
6ecaeed549 | ||
|
|
a489d85f2d | ||
|
|
065b6b05b7 | ||
|
|
1e755967c5 | ||
|
|
1d3fbc1b15 | ||
|
|
330a899eac | ||
|
|
405a125c82 | ||
|
|
b3a57f3dff | ||
|
|
2819eea82b | ||
|
|
41de8846fd | ||
|
|
68d5783a69 | ||
|
|
d49bbb2590 | ||
|
|
fd4703c086 | ||
|
|
0df935473f | ||
|
|
9df6cebb58 | ||
|
|
f299463847 | ||
|
|
4d262d21cb | ||
|
|
ae6a2b0007 | ||
|
|
f71bf18297 | ||
|
|
cddf4b2930 | ||
|
|
813f707892 | ||
|
|
6b9417db5f | ||
|
|
48b0380cb3 | ||
|
|
c959bd2881 | ||
|
|
5131c813ce | ||
|
|
11e4f6a0ba | ||
|
|
cc6737b8bc | ||
|
|
3e7810a2ab | ||
|
|
fc0879f98d | ||
|
|
bb5444f618 | ||
|
|
e0aa6c5e1f | ||
|
|
1746a61659 | ||
|
|
7a8af8da6b | ||
|
|
dfd6db988f | ||
|
|
151db6ac7d | ||
|
|
de97a1f1da | ||
|
|
5d5a1ccb0b | ||
|
|
91cd81aaa0 | ||
|
|
f33d51e7d9 | ||
|
|
2c02a56bce | ||
|
|
7d23bd2cea | ||
|
|
778f2f5ec5 | ||
|
|
9a3815b66d | ||
|
|
425a65140b | ||
|
|
fe06f0c7b0 | ||
|
|
43b2b57191 | ||
|
|
50b81c9d4e | ||
|
|
2b4aa46a6a | ||
|
|
e1e85aa7af | ||
|
|
bf67850739 | ||
|
|
1561d64c80 | ||
|
|
ffd5fc79fc | ||
|
|
34648941d4 | ||
|
|
c69ca47922 | ||
|
|
f29c73fb1c | ||
|
|
f2eab71ccf | ||
|
|
6645d2e058 | ||
|
|
2faa879658 | ||
|
|
0a0997702d | ||
|
|
5cee13ab6d | ||
|
|
eb79256cee | ||
|
|
903bb0ae32 | ||
|
|
21aa3232d7 | ||
|
|
38b2f27efe | ||
|
|
7419475975 | ||
|
|
b820c0c7e4 | ||
|
|
bd90e3d928 | ||
|
|
c6d2cee3e9 | ||
|
|
39bd72b1f7 | ||
|
|
8f35437d5a | ||
|
|
4756526829 | ||
|
|
819499d6ff | ||
|
|
29edc3a7a3 | ||
|
|
388aa467f1 | ||
|
|
92540b2f6a | ||
|
|
30d9daa59b | ||
|
|
fa88152c80 | ||
|
|
63441e259b | ||
|
|
693003314a | ||
|
|
68b55a12a2 | ||
|
|
20e41f1d1d | ||
|
|
2dc81833c6 | ||
|
|
12aa58601d | ||
|
|
57a0a34f92 | ||
|
|
76d1e5b173 | ||
|
|
8b32f82b4d | ||
|
|
131b9ec417 | ||
|
|
57b681b053 | ||
|
|
31a2c4548c | ||
|
|
b7f8ec0ff9 | ||
|
|
2e684c6500 | ||
|
|
9acdab32df | ||
|
|
06af14107e | ||
|
|
77dea441e5 | ||
|
|
bc73f2dcbe | ||
|
|
a4bb143e47 | ||
|
|
38490ff8da | ||
|
|
e1a31db559 | ||
|
|
71480242a9 | ||
|
|
4335a90a00 | ||
|
|
0fb1036800 | ||
|
|
ac342231c3 | ||
|
|
8dd177a051 | ||
|
|
bbf2f906fb | ||
|
|
c24216cf30 | ||
|
|
5db895dcbf | ||
|
|
c50ff08a63 | ||
|
|
06141900b4 | ||
|
|
fab943b61a | ||
|
|
a67815e138 | ||
|
|
bd6b91c958 | ||
|
|
5aba988fac | ||
|
|
e64e194b6b | ||
|
|
bc274e2bd9 | ||
|
|
8339cce10a | ||
|
|
6443f7408a | ||
|
|
590a392ab1 | ||
|
|
250c222cc4 | ||
|
|
487edc8399 | ||
|
|
3870583ea8 | ||
|
|
6440eb9d76 | ||
|
|
885558b6f8 | ||
|
|
b6356aa7a5 | ||
|
|
31711dbf82 | ||
|
|
adc8fdc35a | ||
|
|
272e9709c6 | ||
|
|
f13fd73d9e | ||
|
|
50af9a5d80 | ||
|
|
fe84141119 | ||
|
|
fb93db0730 | ||
|
|
23f6938498 | ||
|
|
98bcf1c0a5 | ||
|
|
803a72df27 | ||
|
|
0494ee52f1 | ||
|
|
6d15b2570f | ||
|
|
659460e19c | ||
|
|
ba036cd579 | ||
|
|
8724aeb9e7 | ||
|
|
da6c299804 | ||
|
|
50374b2456 | ||
|
|
49df5d4626 | ||
|
|
7ab12ed8ce | ||
|
|
b459228e26 | ||
|
|
8f56fabcdd | ||
|
|
a62632a947 | ||
|
|
f025d2bfa2 | ||
|
|
63645333d6 | ||
|
|
5d410c6895 | ||
|
|
8d77b734bb | ||
|
|
9574198958 | ||
|
|
ee54493163 | ||
|
|
c977293f14 | ||
|
|
b0ed658970 | ||
|
|
37c183636b | ||
|
|
e67d3c64e0 | ||
|
|
4f2db82a77 | ||
|
|
d6b851301e | ||
|
|
b7a479392e | ||
|
|
e1cc849554 | ||
|
|
7f5884829c | ||
|
|
a15c3b2660 | ||
|
|
41c44a4af6 | ||
|
|
1b8b6261e2 | ||
|
|
5bf4f52119 | ||
|
|
f4c8de041d | ||
|
|
910ad00059 | ||
|
|
b282a06932 | ||
|
|
b8f71c0d6e | ||
|
|
113b4b7a4e | ||
|
|
b82451fb33 | ||
|
|
4be18aba8b | ||
|
|
ebf1f42942 | ||
|
|
2169a0da01 | ||
|
|
d118565ef6 | ||
|
|
aaeba2709c | ||
|
|
d19b266e0e | ||
|
|
f767ea7d37 | ||
|
|
0576a8aa32 | ||
|
|
03401cd46e | ||
|
|
f69d57c3b5 | ||
|
|
2a87251152 | ||
|
|
aef495604c | ||
|
|
8bfd6661e2 | ||
|
|
3ec4e24d00 | ||
|
|
7ced7c0aa3 | ||
|
|
dd22e721e3 | ||
|
|
4424637af2 | ||
|
|
37c70dbb8e | ||
|
|
91c67eb100 | ||
|
|
e49385b78c | ||
|
|
6b2efd0f6c | ||
|
|
34d099c99a | ||
|
|
987f7eed4c | ||
|
|
e4579f0db2 | ||
|
|
6a6e9dde9d | ||
|
|
28ac55bc62 | ||
|
|
458fa90362 | ||
|
|
56fc410105 | ||
|
|
753e246531 | ||
|
|
211160ca16 | ||
|
|
1229b96e42 | ||
|
|
fe632d70cb | ||
|
|
c862aa5e9e | ||
|
|
18e19f6ce6 | ||
|
|
7ef0afcb62 | ||
|
|
1e2f0be75a | ||
|
|
a58cce2dba | ||
|
|
ffa78aa67c | ||
|
|
7cbb1060ac | ||
|
|
a05541358e | ||
|
|
1f88da7538 | ||
|
|
36d8c53645 | ||
|
|
ec7b3872b4 | ||
|
|
edacaf7ff4 | ||
|
|
df0eb1f8e9 | ||
|
|
59b9c32fbc | ||
|
|
9f37100096 | ||
|
|
a2f2bd5df5 | ||
|
|
c597b87f72 | ||
|
|
82a0d01a42 | ||
|
|
0e573cd6c7 | ||
|
|
b07000835f | ||
|
|
c6c3f88a79 | ||
|
|
a20bd6adb5 | ||
|
|
5213f78d25 | ||
|
|
12f92911c7 | ||
|
|
ad2dc5646d | ||
|
|
6cbdb06fd8 | ||
|
|
27c5637675 | ||
|
|
0c52ff431a | ||
|
|
8d507c43fa | ||
|
|
b61f65bc20 | ||
|
|
3a57e76ed1 | ||
|
|
6c7bef8d11 | ||
|
|
88ae3485c2 | ||
|
|
e5caf170c8 | ||
|
|
089f8ba66a | ||
|
|
fbf1a451c8 | ||
|
|
4541336514 | ||
|
|
346e7b4f4d | ||
|
|
15641c8475 | ||
|
|
2fd85af33c | ||
|
|
401a7a7f71 | ||
|
|
e35e4135c9 | ||
|
|
8ae4403b63 | ||
|
|
11076d0af3 | ||
|
|
9cfb133a98 | ||
|
|
4548a9b7e2 | ||
|
|
61af0d9906 | ||
|
|
c0991cc576 | ||
|
|
301366c4fa | ||
|
|
3bda372847 | ||
|
|
082cbcbc50 | ||
|
|
cbf86da0e7 | ||
|
|
32e461953c | ||
|
|
d67fa2c00d | ||
|
|
c3155a2489 | ||
|
|
c5ea656385 | ||
|
|
be55a5fbcd | ||
|
|
21303f2167 | ||
|
|
c9b8e7cf41 | ||
|
|
5498a889ae | ||
|
|
0fe738500c | ||
|
|
dd9a8df03a | ||
|
|
0c7d547db1 | ||
|
|
638d596a3b | ||
|
|
d4976d4b63 | ||
|
|
a7bf4b3f36 | ||
|
|
94af2a29e1 | ||
|
|
42f816999e | ||
|
|
1414fbcc05 | ||
|
|
1947c58c60 | ||
|
|
1e20475061 | ||
|
|
af36282df5 | ||
|
|
322e46f15d | ||
|
|
983ea7f7a5 | ||
|
|
384e616231 | ||
|
|
6b8725f091 | ||
|
|
17797d83da | ||
|
|
ebd3680a47 | ||
|
|
74c5244be1 | ||
|
|
3cf0b83bda | ||
|
|
127813120e | ||
|
|
0c68ebed4f | ||
|
|
232be80325 | ||
|
|
c95a75c5d5 | ||
|
|
c8ab2d5908 | ||
|
|
572df758ba | ||
|
|
b996102837 | ||
|
|
080a05bbd3 | ||
|
|
343c6cdc47 | ||
|
|
5c785b2270 | ||
|
|
ca9bcd3156 | ||
|
|
82b40be4ab | ||
|
|
e098731d6c | ||
|
|
5f6aa30be5 | ||
|
|
ea70bffe5d | ||
|
|
47ae602df4 | ||
|
|
878b6fc258 | ||
|
|
0696f484e8 | ||
|
|
927a5d8251 | ||
|
|
28e7fd4bc5 | ||
|
|
28c61571e8 | ||
|
|
d0779034e7 | ||
|
|
51fdcbe7e9 | ||
|
|
eda2fb7077 | ||
|
|
3f781cad0a | ||
|
|
fc727df7d2 | ||
|
|
47fe4ad69f | ||
|
|
7a70602ec6 | ||
|
|
14eb6b6979 | ||
|
|
6009844e25 | ||
|
|
8b36683571 | ||
|
|
e9bd736b1a | ||
|
|
a890f2ac00 | ||
|
|
b35f19ec44 | ||
|
|
ad3413eeec | ||
|
|
f0716edbcf | ||
|
|
fc9f637fb0 | ||
|
|
292a11d81e | ||
|
|
ad4d6e7dec | ||
|
|
e4bcf78afa | ||
|
|
d0ee04a69f | ||
|
|
6d6eceeb88 | ||
|
|
0dca6a689a | ||
|
|
a937500ae4 | ||
|
|
80fd77e476 | ||
|
|
98155dce25 | ||
|
|
1ba2246d95 | ||
|
|
57ea871753 | ||
|
|
61533712be | ||
|
|
dc552116cf | ||
|
|
ab5e616635 | ||
|
|
b6b2da5eb7 | ||
|
|
89ef0e29f5 | ||
|
|
4cd843eda9 | ||
|
|
d2429bc3e4 | ||
|
|
b090101b23 | ||
|
|
56455e0fee | ||
|
|
f4ed3b7584 | ||
|
|
c86924b925 | ||
|
|
de25de0a95 | ||
|
|
ea48af1c9a | ||
|
|
418ed20479 | ||
|
|
519509945b | ||
|
|
8c55158822 | ||
|
|
7748720963 | ||
|
|
4d70169bef | ||
|
|
10dbd6f207 | ||
|
|
778c320008 | ||
|
|
268bbed17e | ||
|
|
f63ac72305 | ||
|
|
3293c6b80b | ||
|
|
149d48919d | ||
|
|
8c4a2e0fbf | ||
|
|
664fb4cffd | ||
|
|
6ad4bd4cf2 | ||
|
|
37fa6abe9d | ||
|
|
8b28672131 | ||
|
|
de9f52ff2c | ||
|
|
c8ddc164c7 | ||
|
|
e18393dbc6 | ||
|
|
aad164a531 | ||
|
|
3a37d2f6ed | ||
|
|
8cd3205d70 | ||
|
|
e82521ea79 | ||
|
|
9191ad60fd | ||
|
|
49a82e062f | ||
|
|
181706c564 | ||
|
|
554805d6cb | ||
|
|
1727fa5120 | ||
|
|
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 | ||
|
|
dd71d6471c | ||
|
|
f795173886 | ||
|
|
e42ab1fed8 | ||
|
|
0544c023f5 | ||
|
|
ff47f9480b | ||
|
|
70561b7c54 | ||
|
|
559c9c75f3 | ||
|
|
6c69160d6b | ||
|
|
d0cee47e22 | ||
|
|
ccebb56a83 | ||
|
|
cf169d1582 | ||
|
|
bcd1e30376 | ||
|
|
40bad7a718 | ||
|
|
3d63f9607f | ||
|
|
13e194cd26 | ||
|
|
d6016788ef | ||
|
|
ad602a54bf | ||
|
|
7181970a32 | ||
|
|
cfc90ab7f6 | ||
|
|
05c96eaa28 | ||
|
|
9a9f49f467 | ||
|
|
c552ae98b4 | ||
|
|
df7493fd33 | ||
|
|
5de571fb03 | ||
|
|
62df7badf3 | ||
|
|
597e219257 | ||
|
|
3f8a9079ee | ||
|
|
5981767543 | ||
|
|
36146d970a | ||
|
|
464ba4f334 | ||
|
|
36a5eb2110 | ||
|
|
55840210c7 | ||
|
|
3c38abb203 | ||
|
|
4816563452 | ||
|
|
24dc73912b | ||
|
|
f96db9c74f | ||
|
|
ef2054a45b | ||
|
|
f4052e0e71 | ||
|
|
524845cc74 | ||
|
|
4c356c5077 | ||
|
|
36dea8373f | ||
|
|
2cb823a15b | ||
|
|
169dc43565 | ||
|
|
4b53acab14 | ||
|
|
c3acac797a | ||
|
|
dd2bf0ad13 | ||
|
|
f620af1455 | ||
|
|
fcd1b7b187 | ||
|
|
d0d2f88346 | ||
|
|
c56173462a | ||
|
|
58074f23c5 | ||
|
|
f816e6bbc4 | ||
|
|
d53ecaeade | ||
|
|
299279dc2d |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[alias]
|
||||||
|
xtask = "run --package xtask --"
|
||||||
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)"
|
||||||
17
.editorconfig
Normal file
17
.editorconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# configuration for https://editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.rs]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
11
.github/CODEOWNERS
vendored
Normal file
11
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
* @ratatui/maintainers
|
||||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github: ratatui
|
||||||
|
open_collective: ratatui
|
||||||
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
56
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create an issue about a bug you encountered
|
||||||
|
title: ''
|
||||||
|
labels: 'Type: 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.
|
||||||
|
-->
|
||||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Frequently Asked Questions
|
||||||
|
url: https://ratatui.rs/faq/
|
||||||
|
about: Check the website FAQ section to see if your question has already been answered
|
||||||
|
- name: Ratatui Forum
|
||||||
|
url: https://forum.ratatui.rs
|
||||||
|
about: Ask questions about ratatui on our Forum
|
||||||
|
- name: Discord Chat
|
||||||
|
url: https://discord.gg/pMCEU9hNEj
|
||||||
|
about: Ask questions about ratatui on Discord
|
||||||
|
- name: Matrix Chat
|
||||||
|
url: https://matrix.to/#/#ratatui:matrix.org
|
||||||
|
about: Ask questions about ratatui on Matrix
|
||||||
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: 'Type: 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.
|
||||||
|
-->
|
||||||
18
.github/dependabot.yml
vendored
Normal file
18
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Maintain dependencies for Cargo
|
||||||
|
- package-ecosystem: "cargo"
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
# Maintain dependencies for GitHub Actions
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
open-pull-requests-limit: 10
|
||||||
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. -->
|
||||||
25
.github/workflows/bench_base.yml
vendored
Normal file
25
.github/workflows/bench_base.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Run Benchmarks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
benchmark_base_branch:
|
||||||
|
name: Continuous Benchmarking with Bencher
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- uses: bencherdev/bencher@main
|
||||||
|
- name: Track base branch benchmarks with Bencher
|
||||||
|
run: |
|
||||||
|
bencher run \
|
||||||
|
--project ratatui-org \
|
||||||
|
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||||
|
--branch main \
|
||||||
|
--testbed ubuntu-latest \
|
||||||
|
--adapter rust_criterion \
|
||||||
|
--err \
|
||||||
|
cargo bench
|
||||||
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
25
.github/workflows/bench_run_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
name: Run and Cache Benchmarks
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, reopened, edited, synchronize]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
benchmark_fork_pr_branch:
|
||||||
|
name: Run Fork PR Benchmarks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Run Benchmarks
|
||||||
|
run: cargo bench > benchmark_results.txt
|
||||||
|
- name: Upload Benchmark Results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: benchmark_results.txt
|
||||||
|
path: ./benchmark_results.txt
|
||||||
|
- name: Upload GitHub Pull Request Event
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: event.json
|
||||||
|
path: ${{ github.event_path }}
|
||||||
56
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
56
.github/workflows/bench_track_fork_pr.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
name: Track Benchmarks with Bencher
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [Run and Cache Benchmarks]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
track_fork_pr_branch:
|
||||||
|
if: github.event.workflow_run.conclusion == 'success'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
BENCHMARK_RESULTS: benchmark_results.txt
|
||||||
|
PR_EVENT: event.json
|
||||||
|
steps:
|
||||||
|
- name: Download Benchmark Results
|
||||||
|
uses: dawidd6/action-download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: ${{ env.BENCHMARK_RESULTS }}
|
||||||
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
|
- name: Download PR Event
|
||||||
|
uses: dawidd6/action-download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: ${{ env.PR_EVENT }}
|
||||||
|
run_id: ${{ github.event.workflow_run.id }}
|
||||||
|
- name: Export PR Event Data
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let fs = require('fs');
|
||||||
|
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
|
||||||
|
core.exportVariable("PR_HEAD", prEvent.pull_request.head.ref);
|
||||||
|
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
|
||||||
|
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
|
||||||
|
core.exportVariable("PR_NUMBER", prEvent.number);
|
||||||
|
- uses: bencherdev/bencher@main
|
||||||
|
- name: Track Benchmarks with Bencher
|
||||||
|
run: |
|
||||||
|
bencher run \
|
||||||
|
--project ratatui-org \
|
||||||
|
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||||
|
--branch "$PR_HEAD" \
|
||||||
|
--start-point "$PR_BASE" \
|
||||||
|
--start-point-hash "$PR_BASE_SHA" \
|
||||||
|
--start-point-clone-thresholds \
|
||||||
|
--start-point-reset \
|
||||||
|
--testbed ubuntu-latest \
|
||||||
|
--adapter rust_criterion \
|
||||||
|
--err \
|
||||||
|
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
|
||||||
|
--ci-number "$PR_NUMBER" \
|
||||||
|
--file "$BENCHMARK_RESULTS"
|
||||||
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
52
.github/workflows/calculate-alpha-release.bash
vendored
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Exit on error. Append "|| true" if you expect an error.
|
||||||
|
set -o errexit
|
||||||
|
# Exit on error inside any functions or subshells.
|
||||||
|
set -o errtrace
|
||||||
|
# Do not allow use of undefined vars. Use ${VAR:-} to use an undefined VAR
|
||||||
|
set -o nounset
|
||||||
|
# Catch the error in case mysqldump fails (but gzip succeeds) in `mysqldump |gzip`
|
||||||
|
set -o pipefail
|
||||||
|
# Turn on traces, useful while debugging but commented out by default
|
||||||
|
# set -o xtrace
|
||||||
|
|
||||||
|
last_release="$(git tag --sort=committerdate | grep -P "v0+\.\d+\.\d+$" | tail -1)"
|
||||||
|
echo "🐭 Last release: ${last_release}"
|
||||||
|
|
||||||
|
# detect breaking changes
|
||||||
|
if [ -n "$(git log --oneline ${last_release}..HEAD | grep '!:')" ]; then
|
||||||
|
echo "🐭 Breaking changes detected since ${last_release}"
|
||||||
|
git log --oneline ${last_release}..HEAD | grep '!:'
|
||||||
|
# increment the minor version
|
||||||
|
minor="${last_release##v0.}"
|
||||||
|
minor="${minor%.*}"
|
||||||
|
next_minor="$((minor + 1))"
|
||||||
|
next_release="v0.${next_minor}.0"
|
||||||
|
else
|
||||||
|
# increment the patch version
|
||||||
|
patch="${last_release##*.}"
|
||||||
|
next_patch="$((patch + 1))"
|
||||||
|
next_release="${last_release/%${patch}/${next_patch}}"
|
||||||
|
fi
|
||||||
|
echo "🐭 Next release: ${next_release}"
|
||||||
|
|
||||||
|
suffix="alpha"
|
||||||
|
last_tag="$(git tag --sort=committerdate | tail -1)"
|
||||||
|
if [[ "${last_tag}" = "${next_release}-${suffix}"* ]]; then
|
||||||
|
echo "🐭 Last alpha release: ${last_tag}"
|
||||||
|
# increment the alpha version
|
||||||
|
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
|
||||||
|
alpha="${last_tag##*-${suffix}.}"
|
||||||
|
next_alpha="$((alpha + 1))"
|
||||||
|
next_tag="${last_tag/%${alpha}/${next_alpha}}"
|
||||||
|
else
|
||||||
|
# increment the patch and start the alpha version from 0
|
||||||
|
# e.g. v0.22.0 -> v0.22.1-alpha.0
|
||||||
|
next_tag="${next_release}-${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 alpha release: ${next_tag}"
|
||||||
86
.github/workflows/check-pr.yml
vendored
Normal file
86
.github/workflows/check-pr.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
name: Check Pull Requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
- labeled
|
||||||
|
- unlabeled
|
||||||
|
merge_group:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-title:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check PR title
|
||||||
|
if: github.event_name == 'pull_request_target'
|
||||||
|
uses: amannn/action-semantic-pull-request@v5
|
||||||
|
id: check_pr_title
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Add comment indicating we require pull request titles to follow conventional commits specification
|
||||||
|
- uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
if: always() && (steps.check_pr_title.outputs.error_message != null)
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
message: |
|
||||||
|
Thank you for opening this pull request!
|
||||||
|
|
||||||
|
We require pull request titles to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||||
|
|
||||||
|
Details:
|
||||||
|
|
||||||
|
> ${{ steps.check_pr_title.outputs.error_message }}
|
||||||
|
|
||||||
|
# Delete a previous comment when the issue has been resolved
|
||||||
|
- if: ${{ steps.check_pr_title.outputs.error_message == null }}
|
||||||
|
uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
with:
|
||||||
|
header: pr-title-lint-error
|
||||||
|
delete: true
|
||||||
|
|
||||||
|
check-breaking-change-label:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
# use an environment variable to pass untrusted input to the script
|
||||||
|
# see https://securitylab.github.com/research/github-actions-untrusted-input/
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
steps:
|
||||||
|
- name: Check breaking change label
|
||||||
|
id: check_breaking_change
|
||||||
|
run: |
|
||||||
|
pattern='^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\(\w+\))?!:'
|
||||||
|
# Check if pattern matches
|
||||||
|
if echo "${PR_TITLE}" | grep -qE "$pattern"; then
|
||||||
|
echo "breaking_change=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "breaking_change=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
- name: Add label
|
||||||
|
if: steps.check_breaking_change.outputs.breaking_change == 'true'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
script: |
|
||||||
|
github.rest.issues.addLabels({
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
labels: ['Type: Breaking Change']
|
||||||
|
})
|
||||||
|
|
||||||
|
do-not-merge:
|
||||||
|
if: ${{ contains(github.event.*.labels.*.name, 'do not merge') }}
|
||||||
|
name: Prevent Merging
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check for label
|
||||||
|
run: |
|
||||||
|
echo "Pull request is labeled as 'do not merge'"
|
||||||
|
echo "This workflow fails so that the pull request cannot be merged"
|
||||||
|
exit 1
|
||||||
16
.github/workflows/check-semver.yml
vendored
Normal file
16
.github/workflows/check-semver.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: Check Semver
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-semver:
|
||||||
|
name: Check semver
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Check semver
|
||||||
|
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||||
190
.github/workflows/ci.yml
vendored
Normal file
190
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
name: Continuous Integration
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# lint, clippy and coverage 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 the formatting of the codebase.
|
||||||
|
lint-formatting:
|
||||||
|
name: Check Formatting
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
|
with: { components: rustfmt }
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- uses: taiki-e/install-action@v2
|
||||||
|
with:
|
||||||
|
tool: taplo-cli
|
||||||
|
- run: cargo xtask format --check
|
||||||
|
|
||||||
|
# Check for typos in the codebase.
|
||||||
|
# See <https://github.com/crate-ci/typos/>
|
||||||
|
lint-typos:
|
||||||
|
name: Check Typos
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: crate-ci/typos@master
|
||||||
|
|
||||||
|
# Check for any disallowed dependencies in the codebase due to license / security issues.
|
||||||
|
# See <https://github.com/EmbarkStudios/cargo-deny>
|
||||||
|
dependencies:
|
||||||
|
name: Check Dependencies
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: taiki-e/install-action@cargo-deny
|
||||||
|
- run: cargo deny --log-level info --all-features check
|
||||||
|
|
||||||
|
# Check for any unused dependencies in the codebase.
|
||||||
|
# See <https://github.com/bnjbvr/cargo-machete/>
|
||||||
|
cargo-machete:
|
||||||
|
name: Check Unused Dependencies
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: bnjbvr/cargo-machete@v0.7.0
|
||||||
|
|
||||||
|
# Run cargo clippy.
|
||||||
|
lint-clippy:
|
||||||
|
name: Check Clippy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with: { components: clippy }
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo xtask clippy
|
||||||
|
|
||||||
|
# Run markdownlint on all markdown files in the repository.
|
||||||
|
lint-markdown:
|
||||||
|
name: Check Markdown
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: DavidAnson/markdownlint-cli2-action@v19
|
||||||
|
with:
|
||||||
|
globs: |
|
||||||
|
'**/*.md'
|
||||||
|
'!target'
|
||||||
|
|
||||||
|
# Run cargo coverage. This will generate a coverage report and upload it to codecov.
|
||||||
|
# <https://app.codecov.io/gh/ratatui/ratatui>
|
||||||
|
coverage:
|
||||||
|
name: Coverage Report
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: llvm-tools
|
||||||
|
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo xtask coverage
|
||||||
|
- uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
fail_ci_if_error: true
|
||||||
|
|
||||||
|
# Run cargo check. This is a fast way to catch any obvious errors in the code.
|
||||||
|
check:
|
||||||
|
name: Check ${{ matrix.os }} ${{ matrix.toolchain }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
toolchain: ["1.74.0", "stable"]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.toolchain }}
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo xtask check --all-features
|
||||||
|
|
||||||
|
# Check if README.md is up-to-date with the crate's documentation.
|
||||||
|
check-readme:
|
||||||
|
name: Check README
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- uses: taiki-e/install-action@cargo-rdme
|
||||||
|
- run: cargo xtask readme --check
|
||||||
|
|
||||||
|
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account the
|
||||||
|
# package.metadata.docs.rs configured in Cargo.toml. https://github.com/dtolnay/cargo-docs-rs
|
||||||
|
lint-docs:
|
||||||
|
name: Check Docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RUSTDOCFLAGS: -Dwarnings
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@nightly
|
||||||
|
- uses: dtolnay/install@cargo-docs-rs
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo xtask docs
|
||||||
|
|
||||||
|
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
|
||||||
|
# compile, or any other issues in the documentation.
|
||||||
|
test-docs:
|
||||||
|
name: Test Docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo xtask test-docs
|
||||||
|
|
||||||
|
# Run cargo test on the libraries of the crate.
|
||||||
|
test-libs:
|
||||||
|
name: Test Libs ${{ matrix.toolchain }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
toolchain: ["1.74.0", "stable"]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo xtask test-libs
|
||||||
|
|
||||||
|
# Run cargo test on all the backends.
|
||||||
|
test-backends:
|
||||||
|
name: Test ${{matrix.backend}} on ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
backend: [crossterm, termion, termwiz]
|
||||||
|
exclude:
|
||||||
|
# termion is not supported on windows
|
||||||
|
- os: windows-latest
|
||||||
|
backend: termion
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- run: cargo xtask test-backend ${{ matrix.backend }}
|
||||||
48
.github/workflows/release-alpha.yml
vendored
Normal file
48
.github/workflows/release-alpha.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: Release alpha version
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
# At 00:00 on Saturday
|
||||||
|
# https://crontab.guru/#0_0_*_*_6
|
||||||
|
- cron: "0 0 * * 6"
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-alpha:
|
||||||
|
name: Create an alpha release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Calculate the next release
|
||||||
|
run: .github/workflows/calculate-alpha-release.bash
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: cargo publish --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
|
||||||
|
|
||||||
|
- name: Generate a changelog
|
||||||
|
uses: orhun/git-cliff-action@v4
|
||||||
|
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
|
||||||
52
.github/workflows/release-plz.yml
vendored
Normal file
52
.github/workflows/release-plz.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
name: Release-plz
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Release unpublished packages.
|
||||||
|
release-plz-release:
|
||||||
|
name: Release-plz release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Run release-plz
|
||||||
|
uses: release-plz/action@v0.5
|
||||||
|
with:
|
||||||
|
command: release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
||||||
|
|
||||||
|
# Create a PR with the new versions and changelog, preparing the next release.
|
||||||
|
release-plz-pr:
|
||||||
|
name: Release-plz PR
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
concurrency:
|
||||||
|
group: release-plz-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Run release-plz
|
||||||
|
uses: release-plz/action@v0.5
|
||||||
|
with:
|
||||||
|
command: release-pr
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_TOKEN }}
|
||||||
45
.github/workflows/release-stable.yml
vendored
Normal file
45
.github/workflows/release-stable.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Release stable version
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-stable:
|
||||||
|
name: Create an stable release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Generate a changelog
|
||||||
|
uses: orhun/git-cliff-action@v4
|
||||||
|
with:
|
||||||
|
config: cliff.toml
|
||||||
|
args: --latest --strip header
|
||||||
|
env:
|
||||||
|
OUTPUT: BODY.md
|
||||||
|
|
||||||
|
- name: Publish on GitHub
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
prerelease: false
|
||||||
|
bodyFile: BODY.md
|
||||||
|
|
||||||
|
publish-crate:
|
||||||
|
name: Publish crate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: cargo publish --token ${{ secrets.CARGO_TOKEN }}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
target
|
target
|
||||||
Cargo.lock
|
|
||||||
*.log
|
*.log
|
||||||
*.rs.rustfmt
|
*.rs.rustfmt
|
||||||
.gdb_history
|
.gdb_history
|
||||||
|
.idea/
|
||||||
|
|||||||
15
.markdownlint.yaml
Normal file
15
.markdownlint.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# configuration for https://github.com/DavidAnson/markdownlint
|
||||||
|
|
||||||
|
first-line-heading: false
|
||||||
|
no-inline-html:
|
||||||
|
allowed_elements:
|
||||||
|
- img
|
||||||
|
- details
|
||||||
|
- summary
|
||||||
|
- div
|
||||||
|
- br
|
||||||
|
line-length:
|
||||||
|
line_length: 100
|
||||||
|
|
||||||
|
# to support repeated headers in the changelog
|
||||||
|
no-duplicate-heading: false
|
||||||
12
.taplo.toml
Normal file
12
.taplo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
include = ["**/*.toml"]
|
||||||
|
|
||||||
|
[formatting]
|
||||||
|
column_width = 100
|
||||||
|
reorder_keys = false
|
||||||
|
|
||||||
|
[[rule]]
|
||||||
|
include = ["**/Cargo.toml"]
|
||||||
|
keys = ["dependencies", "dev-dependencies", "build-dependencies", "workspace.dependencies"]
|
||||||
|
|
||||||
|
[rule.formatting]
|
||||||
|
reorder_keys = true
|
||||||
22
.travis.yml
22
.travis.yml
@@ -1,22 +0,0 @@
|
|||||||
language: rust
|
|
||||||
|
|
||||||
rust:
|
|
||||||
- stable
|
|
||||||
- beta
|
|
||||||
- nightly
|
|
||||||
|
|
||||||
env:
|
|
||||||
- NO_RUSTUP=1
|
|
||||||
|
|
||||||
cache: cargo
|
|
||||||
|
|
||||||
matrix:
|
|
||||||
allow_failures:
|
|
||||||
- rust: nightly
|
|
||||||
- rust: beta
|
|
||||||
|
|
||||||
before_script:
|
|
||||||
- ./scripts/travis/before_script.sh
|
|
||||||
|
|
||||||
script:
|
|
||||||
- ./scripts/travis/script.sh
|
|
||||||
913
BREAKING-CHANGES.md
Normal file
913
BREAKING-CHANGES.md
Normal file
@@ -0,0 +1,913 @@
|
|||||||
|
# Breaking Changes
|
||||||
|
|
||||||
|
This document contains a list of breaking changes in each version and some notes to help migrate
|
||||||
|
between versions. It is compiled manually from the commit history and changelog. We also tag PRs on
|
||||||
|
GitHub with a [breaking change] label.
|
||||||
|
|
||||||
|
[breaking change]: (https://github.com/ratatui/ratatui/issues?q=label%3A%22breaking+change%22)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
This is a quick summary of the sections below:
|
||||||
|
|
||||||
|
- [Unreleased](#unreleased)
|
||||||
|
- The `From` impls for backend types are now replaced with more specific traits
|
||||||
|
- `FrameExt` trait for `unstable-widget-ref` feature
|
||||||
|
- [v0.29.0](#v0290)
|
||||||
|
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
|
||||||
|
- Removed public fields from `Rect` iterators
|
||||||
|
- `Line` now implements `From<Cow<str>`
|
||||||
|
- `Table::highlight_style` is now `Table::row_highlight_style`
|
||||||
|
- `Tabs::select` now accepts `Into<Option<usize>>`
|
||||||
|
- `Color::from_hsl` is now behind the `palette` feature
|
||||||
|
- [v0.28.0](#v0280)
|
||||||
|
- `Backend::size` returns `Size` instead of `Rect`
|
||||||
|
- `Backend` trait migrates to `get/set_cursor_position`
|
||||||
|
- Ratatui now requires Crossterm 0.28.0
|
||||||
|
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
|
||||||
|
- `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize`
|
||||||
|
- `ratatui::terminal` module is now private
|
||||||
|
- `ToText` no longer has a lifetime
|
||||||
|
- `Frame::size` is deprecated and renamed to `Frame::area`
|
||||||
|
- [v0.27.0](#v0270)
|
||||||
|
- List no clamps the selected index to list
|
||||||
|
- Prelude items added / removed
|
||||||
|
- 'termion' updated to 4.0
|
||||||
|
- `Rect::inner` takes `Margin` directly instead of reference
|
||||||
|
- `Buffer::filled` takes `Cell` directly instead of reference
|
||||||
|
- `Stylize::bg()` now accepts `Into<Color>`
|
||||||
|
- Removed deprecated `List::start_corner`
|
||||||
|
- `LineGauge::gauge_style` is deprecated
|
||||||
|
- [v0.26.0](#v0260)
|
||||||
|
- `Flex::Start` is the new default flex mode for `Layout`
|
||||||
|
- `patch_style` & `reset_style` now consume and return `Self`
|
||||||
|
- Removed deprecated `Block::title_on_bottom`
|
||||||
|
- `Line` now has an extra `style` field which applies the style to the entire line
|
||||||
|
- `Block` style methods cannot be created in a const context
|
||||||
|
- `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>`
|
||||||
|
- `Table::new` now accepts `IntoIterator<Item: Into<Row<'a>>>`.
|
||||||
|
- [v0.25.0](#v0250)
|
||||||
|
- Removed `Axis::title_style` and `Buffer::set_background`
|
||||||
|
- `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>`
|
||||||
|
- `Table::new()` now requires specifying the widths
|
||||||
|
- `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>`
|
||||||
|
- Layout::new() now accepts direction and constraint parameters
|
||||||
|
- The default `Tabs::highlight_style` is now `Style::new().reversed()`
|
||||||
|
- [v0.24.0](#v0240)
|
||||||
|
- MSRV is now 1.70.0
|
||||||
|
- `ScrollbarState`: `position`, `content_length`, and `viewport_content_length` are now `usize`
|
||||||
|
- `BorderType`: `line_symbols` is now `border_symbols` and returns `symbols::border::set`
|
||||||
|
- `Frame<'a, B: Backend>` is now `Frame<'a>`
|
||||||
|
- `Stylize` shorthands for `String` now consume the value and return `Span<'static>`
|
||||||
|
- `Spans` is removed
|
||||||
|
- [v0.23.0](#v0230)
|
||||||
|
- `Scrollbar`: `track_symbol` now takes `Option<&str>`
|
||||||
|
- `Scrollbar`: symbols moved to `symbols` module
|
||||||
|
- MSRV is now 1.67.0
|
||||||
|
- [v0.22.0](#v0220)
|
||||||
|
- `serde` representation of `Borders` and `Modifiers` has changed
|
||||||
|
- [v0.21.0](#v0210)
|
||||||
|
- MSRV is now 1.65.0
|
||||||
|
- `terminal::ViewPort` is now an enum
|
||||||
|
- `"".as_ref()` must be annotated to implement `Into<Text<'a>>`
|
||||||
|
- `Marker::Block` renders as a block char instead of a bar char
|
||||||
|
- [v0.20.0](#v0200)
|
||||||
|
- MSRV is now 1.63.0
|
||||||
|
- `List` no longer ignores empty strings
|
||||||
|
|
||||||
|
## Unreleased (0.30.0)
|
||||||
|
|
||||||
|
### `FrameExt` trait for `unstable-widget-ref` feature ([#1530])
|
||||||
|
|
||||||
|
[#1530]: https://github.com/ratatui/ratatui/pull/1530
|
||||||
|
|
||||||
|
To call `Frame::render_widget_ref()` or `Frame::render_stateful_widget_ref()` you now need to:
|
||||||
|
|
||||||
|
1. Import the `FrameExt` trait from `ratatui::widgets`.
|
||||||
|
2. Enable the `unstable-widget-ref` feature.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ratatui::{
|
||||||
|
layout::Rect,
|
||||||
|
widgets::{Block, FrameExt},
|
||||||
|
};
|
||||||
|
|
||||||
|
let block = Block::new();
|
||||||
|
let area = Rect::new(0, 0, 5, 5);
|
||||||
|
frame.render_widget_ref(&block, area);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `WidgetRef` no longer has a blanket implementation of Widget
|
||||||
|
|
||||||
|
Previously there was a blanket implementation of Widget for WidgetRef. This has been reversed to
|
||||||
|
instead be a blanket implementation of WidgetRef for all &W where W: Widget. Any widgets that
|
||||||
|
previously implemented WidgetRef directly should now instead implement Widget for a reference to the
|
||||||
|
type.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
-impl WidgetRef for Foo {
|
||||||
|
- fn render_ref(&self, area: Rect, buf: &mut Buffer)
|
||||||
|
+impl Widget for &Foo {
|
||||||
|
+ fn render(self, area: Rect, buf: &mut Buffer)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The `From` impls for backend types are now replaced with more specific traits [#1464]
|
||||||
|
|
||||||
|
[#1464]: https://github.com/ratatui/ratatui/pull/1464
|
||||||
|
|
||||||
|
Crossterm gains `ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm}`
|
||||||
|
Termwiz gains `ratatui::backend::termwiz::{FromTermwiz, IntoTermwiz}`
|
||||||
|
|
||||||
|
This is necessary in order to avoid the orphan rule when implementing `From` for crossterm types
|
||||||
|
once the crossterm types are moved to a separate crate.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
+ use ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm};
|
||||||
|
|
||||||
|
let crossterm_color = crossterm::style::Color::Black;
|
||||||
|
- let ratatui_color = crossterm_color.into();
|
||||||
|
- let ratatui_color = ratatui::style::Color::from(crossterm_color);
|
||||||
|
+ let ratatui_color = ratatui::style::Color::from_crossterm(crossterm_color);
|
||||||
|
- let crossterm_color = ratatui_color.into();
|
||||||
|
- let crossterm_color = crossterm::style::Color::from(ratatui_color);
|
||||||
|
+ let crossterm_color = ratatui_color.into_crossterm();
|
||||||
|
|
||||||
|
let crossterm_attribute = crossterm::style::types::Attribute::Bold;
|
||||||
|
- let ratatui_modifier = crossterm_attribute.into();
|
||||||
|
- let ratatui_modifier = ratatui::style::Modifier::from(crossterm_attribute);
|
||||||
|
+ let ratatui_modifier = ratatui::style::Modifier::from_crossterm(crossterm_attribute);
|
||||||
|
- let crossterm_attribute = ratatui_modifier.into();
|
||||||
|
- let crossterm_attribute = crossterm::style::types::Attribute::from(ratatui_modifier);
|
||||||
|
+ let crossterm_attribute = ratatui_modifier.into_crossterm();
|
||||||
|
```
|
||||||
|
|
||||||
|
Similar conversions for `ContentStyle` -> `Style` and `Attributes` -> `Modifier` exist for
|
||||||
|
Crossterm and the various Termion and Termwiz types as well.
|
||||||
|
|
||||||
|
### `Bar::label()` and `BarGroup::label()` now accepts `Into<Line<'a>>`. ([#1471])
|
||||||
|
|
||||||
|
[#1471]: https://github.com/ratatui/ratatui/pull/1471
|
||||||
|
|
||||||
|
Previously `Bar::label()` and `BarGroup::label()` accepted `Line<'a>`, but they now accepts `Into<Line<'a>>`.
|
||||||
|
|
||||||
|
for `Bar::label()`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- Bar::default().label("foo".into());
|
||||||
|
+ Bar::default().label("foo");
|
||||||
|
```
|
||||||
|
|
||||||
|
for `BarGroup::label()`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- BarGroup::default().label("bar".into());
|
||||||
|
+ BarGroup::default().label("bar");
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Bar::text_value` now accepts `Into<String>` ([#1471])
|
||||||
|
|
||||||
|
Previously `Bar::text_value` accepted `String`, but now it accepts `Into<String>`.
|
||||||
|
|
||||||
|
for `Bar::text_value()`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- Bar::default().text_value("foobar".into());
|
||||||
|
+ Bar::default().text_value("foobar");
|
||||||
|
```
|
||||||
|
|
||||||
|
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0)
|
||||||
|
|
||||||
|
### `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const ([#1326])
|
||||||
|
|
||||||
|
[#1326]: https://github.com/ratatui/ratatui/pull/1326
|
||||||
|
|
||||||
|
The `Sparkline::data` method has been modified to accept `IntoIterator<Item = SparklineBar>`
|
||||||
|
instead of `&[u64]`.
|
||||||
|
|
||||||
|
`SparklineBar` is a struct that contains an `Option<u64>` value, which represents an possible
|
||||||
|
_absent_ value, as distinct from a `0` value. This change allows the `Sparkline` to style
|
||||||
|
data points differently, depending on whether they are present or absent.
|
||||||
|
|
||||||
|
`SparklineBar` also contains an `Option<Style>` that will be used to apply a style the bar in
|
||||||
|
addition to any other styling applied to the `Sparkline`.
|
||||||
|
|
||||||
|
Several `From` implementations have been added to `SparklineBar` to support existing callers who
|
||||||
|
provide `&[u64]` and other types that can be converted to `SparklineBar`, such as `Option<u64>`.
|
||||||
|
|
||||||
|
If you encounter any type inference issues, you may need to provide an explicit type for the data
|
||||||
|
passed to `Sparkline::data`. For example, if you are passing a single value, you may need to use
|
||||||
|
`into()` to convert it to form that can be used as a `SparklineBar`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
let value = 1u8;
|
||||||
|
- Sparkline::default().data(&[value.into()]);
|
||||||
|
+ Sparkline::default().data(&[u64::from(value)]);
|
||||||
|
```
|
||||||
|
|
||||||
|
As a consequence of this change, the `data` method is no longer a `const fn`.
|
||||||
|
|
||||||
|
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
|
||||||
|
|
||||||
|
[#1418]: https://github.com/ratatui/ratatui/pull/1418
|
||||||
|
|
||||||
|
Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a
|
||||||
|
single `palette::Hsl` value and is gated behind a `palette` feature flag.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- Color::from_hsl(360.0, 100.0, 100.0)
|
||||||
|
+ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Removed public fields from `Rect` iterators ([#1358], [#1424])
|
||||||
|
|
||||||
|
[#1358]: https://github.com/ratatui/ratatui/pull/1358
|
||||||
|
[#1424]: https://github.com/ratatui/ratatui/pull/1424
|
||||||
|
|
||||||
|
The `pub` modifier has been removed from fields on the `Columns`,`Rows`, and `Positions` iterators.
|
||||||
|
These fields were not intended to be public and should not have been accessed directly.
|
||||||
|
|
||||||
|
### `Rect::area()` now returns u32 instead of u16 ([#1378])
|
||||||
|
|
||||||
|
[#1378]: https://github.com/ratatui/ratatui/pull/1378
|
||||||
|
|
||||||
|
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
|
||||||
|
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
|
||||||
|
|
||||||
|
### `Line` now implements `From<Cow<str>` ([#1373])
|
||||||
|
|
||||||
|
[#1373]: https://github.com/ratatui/ratatui/pull/1373
|
||||||
|
|
||||||
|
As this adds an extra conversion, ambiguous inferred expressions may no longer compile.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// given:
|
||||||
|
struct Foo { ... }
|
||||||
|
impl From<Foo> for String { ... }
|
||||||
|
impl From<Foo> for Cow<str> { ... }
|
||||||
|
|
||||||
|
let foo = Foo { ... };
|
||||||
|
let line = Line::from(foo); // now fails due to now ambiguous inferred type
|
||||||
|
// replace with e.g.
|
||||||
|
let line = Line::from(String::from(foo));
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
|
||||||
|
|
||||||
|
[#1413]: https://github.com/ratatui/ratatui/pull/1413
|
||||||
|
|
||||||
|
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
|
||||||
|
any code already using parameter type inference:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
let selected = 1u8;
|
||||||
|
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
|
||||||
|
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
|
||||||
|
|
||||||
|
[#1331]: https://github.com/ratatui/ratatui/pull/1331
|
||||||
|
|
||||||
|
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
|
||||||
|
|
||||||
|
Also, the serialized output of the `TableState` will now include the "selected_column" field.
|
||||||
|
Software that manually parse the serialized the output (with anything other than the `Serialize`
|
||||||
|
implementation on `TableState`) may have to be refactored if the "selected_column" field is not
|
||||||
|
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
|
||||||
|
implementation on the state.
|
||||||
|
|
||||||
|
## [v0.28.0](https://github.com/ratatui/ratatui/releases/tag/v0.28.0)
|
||||||
|
|
||||||
|
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
|
||||||
|
|
||||||
|
[#1254]: https://github.com/ratatui/ratatui/pull/1254
|
||||||
|
|
||||||
|
The `Backend::size` method returns a `Size` instead of a `Rect`.
|
||||||
|
There is no need for the position here as it was always 0,0.
|
||||||
|
|
||||||
|
### `Backend` trait migrates to `get/set_cursor_position` ([#1284])
|
||||||
|
|
||||||
|
[#1284]: https://github.com/ratatui/ratatui/pull/1284
|
||||||
|
|
||||||
|
If you just use the types implementing the `Backend` trait, you will see deprecation hints but
|
||||||
|
nothing is a breaking change for you.
|
||||||
|
|
||||||
|
If you implement the Backend trait yourself, you need to update the implementation and add the
|
||||||
|
`get/set_cursor_position` method. You can remove the `get/set_cursor` methods as they are deprecated
|
||||||
|
and a default implementation for them exists.
|
||||||
|
|
||||||
|
### Ratatui now requires Crossterm 0.28.0 ([#1278])
|
||||||
|
|
||||||
|
[#1278]: https://github.com/ratatui/ratatui/pull/1278
|
||||||
|
|
||||||
|
Crossterm is updated to version 0.28.0, which is a semver incompatible version with the previous
|
||||||
|
version (0.27.0). Ratatui re-exports the version of crossterm that it is compatible with under
|
||||||
|
`ratatui::crossterm`, which can be used to avoid incompatible versions in your dependency list.
|
||||||
|
|
||||||
|
### `Axis::labels()` now accepts `IntoIterator<Into<Line>>` ([#1273] and [#1283])
|
||||||
|
|
||||||
|
[#1273]: https://github.com/ratatui/ratatui/pull/1173
|
||||||
|
[#1283]: https://github.com/ratatui/ratatui/pull/1283
|
||||||
|
|
||||||
|
Previously Axis::labels accepted `Vec<Span>`. Any code that uses conversion methods that infer the
|
||||||
|
type will need to be rewritten as the compiler cannot infer the correct type.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- Axis::default().labels(vec!["a".into(), "b".into()])
|
||||||
|
+ Axis::default().labels(["a", "b"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Layout::init_cache` no longer returns bool and takes a `NonZeroUsize` instead of `usize` ([#1245])
|
||||||
|
|
||||||
|
[#1245]: https://github.com/ratatui/ratatui/pull/1245
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let is_initialized = Layout::init_cache(100);
|
||||||
|
+ Layout::init_cache(NonZeroUsize::new(100).unwrap());
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ratatui::terminal` module is now private ([#1160])
|
||||||
|
|
||||||
|
[#1160]: https://github.com/ratatui/ratatui/pull/1160
|
||||||
|
|
||||||
|
The `terminal` module is now private and can not be used directly. The types under this module are
|
||||||
|
exported from the root of the crate. This reduces clashes with other modules in the backends that
|
||||||
|
are also named terminal, and confusion about module exports for newer Rust users.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- use ratatui::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
|
||||||
|
+ use ratatui::{CompletedFrame, Frame, Terminal, TerminalOptions, ViewPort};
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ToText` no longer has a lifetime ([#1234])
|
||||||
|
|
||||||
|
[#1234]: https://github.com/ratatui/ratatui/pull/1234
|
||||||
|
|
||||||
|
This change simplifies the trait and makes it easier to implement.
|
||||||
|
|
||||||
|
### `Frame::size` is deprecated and renamed to `Frame::area` ([#1293])
|
||||||
|
|
||||||
|
[#1293]: https://github.com/ratatui/ratatui/pull/1293
|
||||||
|
|
||||||
|
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
|
||||||
|
|
||||||
|
## [v0.27.0](https://github.com/ratatui/ratatui/releases/tag/v0.27.0)
|
||||||
|
|
||||||
|
### List no clamps the selected index to list ([#1159])
|
||||||
|
|
||||||
|
[#1149]: https://github.com/ratatui/ratatui/pull/1149
|
||||||
|
|
||||||
|
The `List` widget now clamps the selected index to the bounds of the list when navigating with
|
||||||
|
`first`, `last`, `previous`, and `next`, as well as when setting the index directly with `select`.
|
||||||
|
|
||||||
|
Previously selecting an index past the end of the list would show treat the list as having a
|
||||||
|
selection which was not visible. Now the last item in the list will be selected instead.
|
||||||
|
|
||||||
|
### Prelude items added / removed ([#1149])
|
||||||
|
|
||||||
|
The following items have been removed from the prelude:
|
||||||
|
|
||||||
|
- `style::Styled` - this trait is useful for widgets that want to
|
||||||
|
support the Stylize trait, but it adds complexity as widgets have two
|
||||||
|
`style` methods and a `set_style` method.
|
||||||
|
- `symbols::Marker` - this item is used by code that needs to draw to
|
||||||
|
the `Canvas` widget, but it's not a common item that would be used by
|
||||||
|
most users of the library.
|
||||||
|
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
|
||||||
|
are rarely used by code that needs to interact with the terminal, and
|
||||||
|
they're generally only ever used once in any app.
|
||||||
|
|
||||||
|
The following items have been added to the prelude:
|
||||||
|
|
||||||
|
- `layout::{Position, Size}` - these items are used by code that needs
|
||||||
|
to interact with the layout system. These are newer items that were
|
||||||
|
added in the last few releases, which should be used more liberally.
|
||||||
|
This may cause conflicts for types defined elsewhere with a similar
|
||||||
|
name.
|
||||||
|
|
||||||
|
To update your app:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
// if your app uses Styled::style() or Styled::set_style():
|
||||||
|
-use ratatui::prelude::*;
|
||||||
|
+use ratatui::{prelude::*, style::Styled};
|
||||||
|
|
||||||
|
// if your app uses symbols::Marker:
|
||||||
|
-use ratatui::prelude::*;
|
||||||
|
+use ratatui::{prelude::*, symbols::Marker}
|
||||||
|
|
||||||
|
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
|
||||||
|
-use ratatui::prelude::*;
|
||||||
|
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
|
||||||
|
|
||||||
|
// to disambiguate existing types named Position or Size:
|
||||||
|
- use some_crate::{Position, Size};
|
||||||
|
- let size: Size = ...;
|
||||||
|
- let position: Position = ...;
|
||||||
|
+ let size: some_crate::Size = ...;
|
||||||
|
+ let position: some_crate::Position = ...;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Termion is updated to 4.0 [#1106]
|
||||||
|
|
||||||
|
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
|
||||||
|
|
||||||
|
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
|
||||||
|
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
|
||||||
|
`MouseRight`, or add a wildcard.
|
||||||
|
|
||||||
|
[#1106]: https://github.com/ratatui/ratatui/pull/1106
|
||||||
|
|
||||||
|
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
|
||||||
|
|
||||||
|
[#1008]: https://github.com/ratatui/ratatui/pull/1008
|
||||||
|
|
||||||
|
`Margin` needs to be passed without reference now.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
-let area = area.inner(&Margin {
|
||||||
|
+let area = area.inner(Margin {
|
||||||
|
vertical: 0,
|
||||||
|
horizontal: 2,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Buffer::filled` takes `Cell` directly instead of reference ([#1148])
|
||||||
|
|
||||||
|
[#1148]: https://github.com/ratatui/ratatui/pull/1148
|
||||||
|
|
||||||
|
`Buffer::filled` moves the `Cell` instead of taking a reference.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
-Buffer::filled(area, &Cell::new("X"));
|
||||||
|
+Buffer::filled(area, Cell::new("X"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Stylize::bg()` now accepts `Into<Color>` ([#1103])
|
||||||
|
|
||||||
|
[#1103]: https://github.com/ratatui/ratatui/pull/1103
|
||||||
|
|
||||||
|
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
|
||||||
|
flexible types from calling scopes, though it can break some type inference in the calling scope.
|
||||||
|
|
||||||
|
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
|
||||||
|
|
||||||
|
[#759]: https://github.com/ratatui/ratatui/pull/759
|
||||||
|
|
||||||
|
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- list.start_corner(Corner::TopLeft);
|
||||||
|
- list.start_corner(Corner::TopRight);
|
||||||
|
// This is not an error, BottomRight rendered top to bottom previously
|
||||||
|
- list.start_corner(Corner::BottomRight);
|
||||||
|
// all becomes
|
||||||
|
+ list.direction(ListDirection::TopToBottom);
|
||||||
|
```
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- list.start_corner(Corner::BottomLeft);
|
||||||
|
// becomes
|
||||||
|
+ list.direction(ListDirection::BottomToTop);
|
||||||
|
```
|
||||||
|
|
||||||
|
`layout::Corner` was removed entirely.
|
||||||
|
|
||||||
|
### `LineGauge::gauge_style` is deprecated ([#565])
|
||||||
|
|
||||||
|
[#565]: https://github.com/ratatui/ratatui/pull/1148
|
||||||
|
|
||||||
|
`LineGauge::gauge_style` is deprecated and replaced with `LineGauge::filled_style` and `LineGauge::unfilled_style`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
let gauge = LineGauge::default()
|
||||||
|
- .gauge_style(Style::default().fg(Color::Red).bg(Color::Blue)
|
||||||
|
+ .filled_style(Style::default().fg(Color::Green))
|
||||||
|
+ .unfilled_style(Style::default().fg(Color::White));
|
||||||
|
```
|
||||||
|
|
||||||
|
## [v0.26.0](https://github.com/ratatui/ratatui/releases/tag/v0.26.0)
|
||||||
|
|
||||||
|
### `Flex::Start` is the new default flex mode for `Layout` ([#881])
|
||||||
|
|
||||||
|
[#881]: https://github.com/ratatui/ratatui/pull/881
|
||||||
|
|
||||||
|
Previously, constraints would stretch to fill all available space, violating constraints if
|
||||||
|
necessary.
|
||||||
|
|
||||||
|
With v0.26.0, `Flex` modes are introduced, and the default is `Flex::Start`, which will align
|
||||||
|
areas associated with constraints to be beginning of the area. With v0.26.0, additionally,
|
||||||
|
`Min` constraints grow to fill excess space. These changes will allow users to build layouts
|
||||||
|
more easily.
|
||||||
|
|
||||||
|
With v0.26.0, users will most likely not need to change what constraints they use to create
|
||||||
|
existing layouts with `Flex::Start`. However, to get old behavior, use `Flex::Legacy`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let rects = Layout::horizontal([Length(1), Length(2)]).split(area);
|
||||||
|
// becomes
|
||||||
|
+ let rects = Layout::horizontal([Length(1), Length(2)]).flex(Flex::Legacy).split(area);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Table::new()` now accepts `IntoIterator<Item: Into<Row<'a>>>` ([#774])
|
||||||
|
|
||||||
|
[#774]: https://github.com/ratatui/ratatui/pull/774
|
||||||
|
|
||||||
|
Previously, `Table::new()` accepted `IntoIterator<Item=Row<'a>>`. The argument change to
|
||||||
|
`IntoIterator<Item: Into<Row<'a>>>`, This allows more flexible types from calling scopes, though it
|
||||||
|
can some break type inference in the calling scope for empty containers.
|
||||||
|
|
||||||
|
This can be resolved either by providing an explicit type (e.g. `Vec::<Row>::new()`), or by using
|
||||||
|
`Table::default()`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let table = Table::new(vec![], widths);
|
||||||
|
// becomes
|
||||||
|
+ let table = Table::default().widths(widths);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Tabs::new()` now accepts `IntoIterator<Item: Into<Line<'a>>>` ([#776])
|
||||||
|
|
||||||
|
[#776]: https://github.com/ratatui/ratatui/pull/776
|
||||||
|
|
||||||
|
Previously, `Tabs::new()` accepted `Vec<T>` where `T: Into<Line<'a>>`. This allows more flexible
|
||||||
|
types from calling scopes, though it can break some type inference in the calling scope.
|
||||||
|
|
||||||
|
This typically occurs when collecting an iterator prior to calling `Tabs::new`, and can be resolved
|
||||||
|
by removing the call to `.collect()`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let tabs = Tabs::new((0.3).map(|i| format!("{i}")).collect());
|
||||||
|
// becomes
|
||||||
|
+ let tabs = Tabs::new((0.3).map(|i| format!("{i}")));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Table::default() now sets segment_size to None and column_spacing to ([#751])
|
||||||
|
|
||||||
|
[#751]: https://github.com/ratatui/ratatui/pull/751
|
||||||
|
|
||||||
|
The default() implementation of Table now sets the column_spacing field to 1 and the segment_size
|
||||||
|
field to `SegmentSize::None`. This will affect the rendering of a small amount of apps.
|
||||||
|
|
||||||
|
To use the previous default values, call `table.segment_size(Default::default())` and
|
||||||
|
`table.column_spacing(0)`.
|
||||||
|
|
||||||
|
### `patch_style` & `reset_style` now consumes and returns `Self` ([#754])
|
||||||
|
|
||||||
|
[#754]: https://github.com/ratatui/ratatui/pull/754
|
||||||
|
|
||||||
|
Previously, `patch_style` and `reset_style` in `Text`, `Line` and `Span` were using a mutable
|
||||||
|
reference to `Self`. To be more consistent with the rest of `ratatui`, which is using fluent
|
||||||
|
setters, these now take ownership of `Self` and return it.
|
||||||
|
|
||||||
|
The following example shows how to migrate for `Line`, but the same applies for `Text` and `Span`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let mut line = Line::from("foobar");
|
||||||
|
- line.patch_style(style);
|
||||||
|
// becomes
|
||||||
|
+ let line = Line::new("foobar").patch_style(style);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove deprecated `Block::title_on_bottom` ([#757])
|
||||||
|
|
||||||
|
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- block.title("foobar").title_on_bottom();
|
||||||
|
+ block.title(Title::from("foobar").position(Position::Bottom));
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Block` style methods cannot be used in a const context ([#720])
|
||||||
|
|
||||||
|
[#720]: https://github.com/ratatui/ratatui/pull/720
|
||||||
|
|
||||||
|
Previously the `style()`, `border_style()` and `title_style()` methods could be used to create a
|
||||||
|
`Block` in a constant context. These now accept `Into<Style>` instead of `Style`. These methods no
|
||||||
|
longer can be called from a constant context.
|
||||||
|
|
||||||
|
### `Line` now has a `style` field that applies to the entire line ([#708])
|
||||||
|
|
||||||
|
[#708]: https://github.com/ratatui/ratatui/pull/708
|
||||||
|
|
||||||
|
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
|
||||||
|
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
|
||||||
|
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
|
||||||
|
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
|
||||||
|
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).
|
||||||
|
|
||||||
|
Each `Span` contained within the line will no longer have the style that is applied to the line in
|
||||||
|
the `Span::style` field.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
let line = Line {
|
||||||
|
spans: vec!["".into()],
|
||||||
|
alignment: Alignment::Left,
|
||||||
|
+ ..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
// or
|
||||||
|
|
||||||
|
let line = Line::raw(vec!["".into()])
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
```
|
||||||
|
|
||||||
|
## [v0.25.0](https://github.com/ratatui/ratatui/releases/tag/v0.25.0)
|
||||||
|
|
||||||
|
### Removed `Axis::title_style` and `Buffer::set_background` ([#691])
|
||||||
|
|
||||||
|
[#691]: https://github.com/ratatui/ratatui/pull/691
|
||||||
|
|
||||||
|
These items were deprecated since 0.10.
|
||||||
|
|
||||||
|
- You should use styling capabilities of [`text::Line`] given as argument of [`Axis::title`]
|
||||||
|
instead of `Axis::title_style`
|
||||||
|
- You should use styling capabilities of [`Buffer::set_style`] instead of `Buffer::set_background`
|
||||||
|
|
||||||
|
[`text::Line`]: https://docs.rs/ratatui/latest/ratatui/text/struct.Line.html
|
||||||
|
[`Axis::title`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.Axis.html#method.title
|
||||||
|
[`Buffer::set_style`]: https://docs.rs/ratatui/latest/ratatui/buffer/struct.Buffer.html#method.set_style
|
||||||
|
|
||||||
|
### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])
|
||||||
|
|
||||||
|
[#672]: https://github.com/ratatui/ratatui/pull/672
|
||||||
|
|
||||||
|
Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. This change will throw a compilation
|
||||||
|
error for `IntoIterator`s with an indeterminate item (e.g. empty vecs).
|
||||||
|
|
||||||
|
E.g.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let list = List::new(vec![]);
|
||||||
|
// becomes
|
||||||
|
+ let list = List::default();
|
||||||
|
```
|
||||||
|
|
||||||
|
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||||
|
|
||||||
|
[#635]: https://github.com/ratatui/ratatui/pull/635
|
||||||
|
|
||||||
|
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||||
|
widget in the default configuration would not show any indication of the selected tab.
|
||||||
|
|
||||||
|
### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635])
|
||||||
|
|
||||||
|
Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs`
|
||||||
|
widget in the default configuration would not show any indication of the selected tab.
|
||||||
|
|
||||||
|
### `Table::new()` now requires specifying the widths of the columns ([#664])
|
||||||
|
|
||||||
|
[#664]: https://github.com/ratatui/ratatui/pull/664
|
||||||
|
|
||||||
|
Previously `Table`s could be constructed without `widths`. In almost all cases this is an error.
|
||||||
|
A new `widths` parameter is now mandatory on `Table::new()`. Existing code of the form:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- Table::new(rows).widths(widths)
|
||||||
|
```
|
||||||
|
|
||||||
|
Should be updated to:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
+ Table::new(rows, widths)
|
||||||
|
```
|
||||||
|
|
||||||
|
For ease of automated replacement in cases where the amount of code broken by this change is large
|
||||||
|
or complex, it may be convenient to replace `Table::new` with `Table::default().rows`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- Table::new(rows).block(block).widths(widths);
|
||||||
|
// becomes
|
||||||
|
+ Table::default().rows(rows).widths(widths)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>` ([#663])
|
||||||
|
|
||||||
|
[#663]: https://github.com/ratatui/ratatui/pull/663
|
||||||
|
|
||||||
|
Previously `Table::widths()` took a slice (`&'a [Constraint]`). This change will introduce clippy
|
||||||
|
`needless_borrow` warnings for places where slices are passed to this method. To fix these, remove
|
||||||
|
the `&`.
|
||||||
|
|
||||||
|
E.g.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let table = Table::new(rows).widths(&[Constraint::Length(1)]);
|
||||||
|
// becomes
|
||||||
|
+ let table = Table::new(rows, [Constraint::Length(1)]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layout::new() now accepts direction and constraint parameters ([#557])
|
||||||
|
|
||||||
|
[#557]: https://github.com/ratatui/ratatui/pull/557
|
||||||
|
|
||||||
|
Previously layout new took no parameters. Existing code should either use `Layout::default()` or
|
||||||
|
the new constructor.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let layout = layout::new()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(1), Constraint::Max(2)]);
|
||||||
|
// becomes either
|
||||||
|
let layout = layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Min(1), Constraint::Max(2)]);
|
||||||
|
// or
|
||||||
|
let layout = layout::new(Direction::Vertical, [Constraint::Min(1), Constraint::Max(2)]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## [v0.24.0](https://github.com/ratatui/ratatui/releases/tag/v0.24.0)
|
||||||
|
|
||||||
|
### `ScrollbarState` field type changed from `u16` to `usize` ([#456])
|
||||||
|
|
||||||
|
[#456]: https://github.com/ratatui/ratatui/pull/456
|
||||||
|
|
||||||
|
In order to support larger content lengths, the `position`, `content_length` and
|
||||||
|
`viewport_content_length` methods on `ScrollbarState` now take `usize` instead of `u16`
|
||||||
|
|
||||||
|
### `BorderType::line_symbols` renamed to `border_symbols` ([#529])
|
||||||
|
|
||||||
|
[#529]: https://github.com/ratatui/ratatui/issues/529
|
||||||
|
|
||||||
|
Applications can now set custom borders on a `Block` by calling `border_set()`. The
|
||||||
|
`BorderType::line_symbols()` is renamed to `border_symbols()` and now returns a new struct
|
||||||
|
`symbols::border::Set`. E.g.:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let line_set: symbols::line::Set = BorderType::line_symbols(BorderType::Plain);
|
||||||
|
// becomes
|
||||||
|
+ let border_set: symbols::border::Set = BorderType::border_symbols(BorderType::Plain);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic `Backend` parameter removed from `Frame` ([#530])
|
||||||
|
|
||||||
|
[#530]: https://github.com/ratatui/ratatui/issues/530
|
||||||
|
|
||||||
|
`Frame` is no longer generic over Backend. Code that accepted `Frame<Backend>` will now need to
|
||||||
|
accept `Frame`. To migrate existing code, remove any generic parameters from code that uses an
|
||||||
|
instance of a Frame. E.g.:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- fn ui<B: Backend>(frame: &mut Frame<B>) { ... }
|
||||||
|
// becomes
|
||||||
|
+ fn ui(frame: Frame) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Stylize` shorthands now consume rather than borrow `String` ([#466])
|
||||||
|
|
||||||
|
[#466]: https://github.com/ratatui/ratatui/issues/466
|
||||||
|
|
||||||
|
In order to support using `Stylize` shorthands (e.g. `"foo".red()`) on temporary `String` values, a
|
||||||
|
new implementation of `Stylize` was added that returns a `Span<'static>`. This causes the value to
|
||||||
|
be consumed rather than borrowed. Existing code that expects to use the string after a call will no
|
||||||
|
longer compile. E.g.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let s = String::new("foo");
|
||||||
|
- let span1 = s.red();
|
||||||
|
- let span2 = s.blue(); // will no longer compile as s is consumed by the previous line
|
||||||
|
// becomes
|
||||||
|
+ let span1 = s.clone().red();
|
||||||
|
+ let span2 = s.blue();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deprecated `Spans` type removed (replaced with `Line`) ([#426])
|
||||||
|
|
||||||
|
[#426]: https://github.com/ratatui/ratatui/issues/426
|
||||||
|
|
||||||
|
`Spans` was replaced with `Line` in 0.21.0. `Buffer::set_spans` was replaced with
|
||||||
|
`Buffer::set_line`.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let spans = Spans::from(some_string_str_span_or_vec_span);
|
||||||
|
- buffer.set_spans(0, 0, spans, 10);
|
||||||
|
// becomes
|
||||||
|
+ let line - Line::from(some_string_str_span_or_vec_span);
|
||||||
|
+ buffer.set_line(0, 0, line, 10);
|
||||||
|
```
|
||||||
|
|
||||||
|
## [v0.23.0](https://github.com/ratatui/ratatui/releases/tag/v0.23.0)
|
||||||
|
|
||||||
|
### `Scrollbar::track_symbol()` now takes an `Option<&str>` instead of `&str` ([#360])
|
||||||
|
|
||||||
|
[#360]: https://github.com/ratatui/ratatui/issues/360
|
||||||
|
|
||||||
|
The track symbol of `Scrollbar` is now optional, this method now takes an optional value.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let scrollbar = Scrollbar::default().track_symbol("|");
|
||||||
|
// becomes
|
||||||
|
+ let scrollbar = Scrollbar::default().track_symbol(Some("|"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Scrollbar` symbols moved to `symbols::scrollbar` and `widgets::scrollbar` module is private ([#330])
|
||||||
|
|
||||||
|
[#330]: https://github.com/ratatui/ratatui/issues/330
|
||||||
|
|
||||||
|
The symbols for defining scrollbars have been moved to the `symbols` module from the
|
||||||
|
`widgets::scrollbar` module which is no longer public. To update your code update any imports to the
|
||||||
|
new module locations. E.g.:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- use ratatui::{widgets::scrollbar::{Scrollbar, Set}};
|
||||||
|
// becomes
|
||||||
|
+ use ratatui::{widgets::Scrollbar, symbols::scrollbar::Set}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MSRV updated to 1.67 ([#361])
|
||||||
|
|
||||||
|
[#361]: https://github.com/ratatui/ratatui/issues/361
|
||||||
|
|
||||||
|
The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
|
||||||
|
|
||||||
|
## [v0.22.0](https://github.com/ratatui/ratatui/releases/tag/v0.22.0)
|
||||||
|
|
||||||
|
### `bitflags` updated to 2.3 ([#205])
|
||||||
|
|
||||||
|
[#205]: https://github.com/ratatui/ratatui/issues/205
|
||||||
|
|
||||||
|
The `serde` representation of `bitflags` has changed. Any existing serialized types that have
|
||||||
|
Borders or Modifiers will need to be re-serialized. This is documented in the [`bitflags`
|
||||||
|
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
|
||||||
|
|
||||||
|
## [v0.21.0](https://github.com/ratatui/ratatui/releases/tag/v0.21.0)
|
||||||
|
|
||||||
|
### MSRV is 1.65.0 ([#171])
|
||||||
|
|
||||||
|
[#171]: https://github.com/ratatui/ratatui/issues/171
|
||||||
|
|
||||||
|
The minimum supported rust version is now 1.65.0.
|
||||||
|
|
||||||
|
### `Terminal::with_options()` stabilized to allow configuring the viewport ([#114])
|
||||||
|
|
||||||
|
[#114]: https://github.com/ratatui/ratatui/issues/114
|
||||||
|
|
||||||
|
In order to support inline viewports, the unstable method `Terminal::with_options()` was stabilized
|
||||||
|
and `ViewPort` was changed from a struct to an enum.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||||
|
- viewport: Viewport::fixed(area),
|
||||||
|
});
|
||||||
|
// becomes
|
||||||
|
let terminal = Terminal::with_options(backend, TerminalOptions {
|
||||||
|
+ viewport: Viewport::Fixed(area),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code that binds `Into<Text<'a>>` now requires type annotations ([#168])
|
||||||
|
|
||||||
|
[#168]: https://github.com/ratatui/ratatui/issues/168
|
||||||
|
|
||||||
|
A new type `Masked` was introduced that implements `From<Text<'a>>`. This causes any code that
|
||||||
|
previously did not need to use type annotations to fail to compile. To fix this, annotate or call
|
||||||
|
`to_string()` / `to_owned()` / `as_str()` on the value. E.g.:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let paragraph = Paragraph::new("".as_ref());
|
||||||
|
// becomes
|
||||||
|
+ let paragraph = Paragraph::new("".as_str());
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Marker::Block` now renders as a block rather than a bar character ([#133])
|
||||||
|
|
||||||
|
[#133]: https://github.com/ratatui/ratatui/issues/133
|
||||||
|
|
||||||
|
Code using the `Block` marker that previously rendered using a half block character (`'▀'``) now
|
||||||
|
renders using the full block character (`'█'`). A new marker variant`Bar` is introduced to replace
|
||||||
|
the existing code.
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- let canvas = Canvas::default().marker(Marker::Block);
|
||||||
|
// becomes
|
||||||
|
+ let canvas = Canvas::default().marker(Marker::Bar);
|
||||||
|
```
|
||||||
|
|
||||||
|
## [v0.20.0](https://github.com/ratatui/ratatui/releases/tag/v0.20.0)
|
||||||
|
|
||||||
|
v0.20.0 was the first release of Ratatui - versions prior to this were release as tui-rs. See the
|
||||||
|
[Changelog](./CHANGELOG.md) for more details.
|
||||||
|
|
||||||
|
### MSRV is update to 1.63.0 ([#80])
|
||||||
|
|
||||||
|
[#80]: https://github.com/ratatui/ratatui/issues/80
|
||||||
|
|
||||||
|
The minimum supported rust version is 1.63.0
|
||||||
|
|
||||||
|
### List no longer ignores empty string in items ([#42])
|
||||||
|
|
||||||
|
[#42]: https://github.com/ratatui/ratatui/issues/42
|
||||||
|
|
||||||
|
The following code now renders 3 items instead of 2. Code which relies on the previous behavior will
|
||||||
|
need to manually filter empty items prior to display.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let items = vec![
|
||||||
|
ListItem::new("line one"),
|
||||||
|
ListItem::new(""),
|
||||||
|
ListItem::new("line four"),
|
||||||
|
];
|
||||||
|
```
|
||||||
7189
CHANGELOG.md
7189
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
https://forum.ratatui.rs/ or https://discord.gg/pMCEU9hNEj.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||||
|
enforcement ladder](https://github.com/mozilla/diversity).
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
https://www.contributor-covenant.org/faq. Translations are available at
|
||||||
|
https://www.contributor-covenant.org/translations.
|
||||||
211
CONTRIBUTING.md
Normal file
211
CONTRIBUTING.md
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
# 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/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.
|
||||||
|
|
||||||
|
### Code formatting
|
||||||
|
|
||||||
|
Run `cargo xtask format` before committing to ensure that code is consistently formatted with
|
||||||
|
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
### Run CI tests before pushing a PR
|
||||||
|
|
||||||
|
Running `cargo xtask ci` before pushing will perform the same checks that we do in the CI process.
|
||||||
|
It's not mandatory to do this before pushing, however it may save you time to do so instead of
|
||||||
|
waiting for GitHub to run the checks.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
|
TL;DR: Clone the repo and build it using `cargo xtask`.
|
||||||
|
|
||||||
|
Ratatui is an ordinary Rust project where common tasks are managed with
|
||||||
|
[cargo-xtask](https://github.com/matklad/cargo-xtask). It wraps common `cargo` commands with sane
|
||||||
|
defaults depending on your platform of choice. Building the project should be as easy as running
|
||||||
|
`cargo xtask build`.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/ratatui/ratatui.git
|
||||||
|
cd ratatui
|
||||||
|
cargo xtask build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
The [test coverage](https://app.codecov.io/gh/ratatui/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>
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
Here are some guidelines for writing documentation in Ratatui.
|
||||||
|
|
||||||
|
Every public API **must** be documented.
|
||||||
|
|
||||||
|
Keep in mind that Ratatui tends to attract beginner Rust users that may not be familiar with Rust
|
||||||
|
concepts.
|
||||||
|
|
||||||
|
#### Content
|
||||||
|
|
||||||
|
The main doc comment should talk about the general features that the widget supports and introduce
|
||||||
|
the concepts pointing to the various methods. Focus on interaction with various features and giving
|
||||||
|
enough information that helps understand why you might want something.
|
||||||
|
|
||||||
|
Examples should help users understand a particular usage, not test a feature. They should be as
|
||||||
|
simple as possible. Prefer hiding imports and using wildcards to keep things concise. Some imports
|
||||||
|
may still be shown to demonstrate a particular non-obvious import (e.g. `Stylize` trait to use style
|
||||||
|
methods). Speaking of `Stylize`, you should use it over the more verbose style setters:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let style = Style::new().red().bold();
|
||||||
|
// not
|
||||||
|
let style = Style::default().fg(Color::Red).add_modifier(Modifiers::BOLD);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Format
|
||||||
|
|
||||||
|
- First line is summary, second is blank, third onward is more detail
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Summary
|
||||||
|
///
|
||||||
|
/// A detailed description
|
||||||
|
/// with examples.
|
||||||
|
fn foo() {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Max line length is 100 characters
|
||||||
|
See [VS Code rewrap extension](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap)
|
||||||
|
|
||||||
|
- Doc comments are above macros
|
||||||
|
i.e.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// doc comment
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Foo {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Code items should be between backticks
|
||||||
|
i.e. ``[`Block`]``, **NOT** ``[Block]``
|
||||||
|
|
||||||
|
### Deprecation notice
|
||||||
|
|
||||||
|
We generally want to wait at least two versions before removing deprecated items, so users have
|
||||||
|
time to update. However, if a deprecation is blocking for us to implement a new feature we may
|
||||||
|
*consider* removing it in a one version notice.
|
||||||
|
|
||||||
|
### 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/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 xtask 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, 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/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.
|
||||||
4428
Cargo.lock
generated
Normal file
4428
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
166
Cargo.toml
166
Cargo.toml
@@ -1,83 +1,99 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "tui"
|
resolver = "2"
|
||||||
version = "0.2.0"
|
members = ["ratatui", "ratatui-*", "xtask", "examples/apps/*"]
|
||||||
authors = ["Florian Dehau <work@fdehau.com>"]
|
default-members = [
|
||||||
description = """
|
"ratatui",
|
||||||
A library to build rich terminal user interfaces or dashboards
|
"ratatui-core",
|
||||||
"""
|
"ratatui-crossterm",
|
||||||
|
# this is not included as it doesn't compile on windows
|
||||||
|
# "ratatui-termion",
|
||||||
|
"ratatui-macros",
|
||||||
|
"ratatui-termwiz",
|
||||||
|
"ratatui-widgets",
|
||||||
|
"examples/apps/*",
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||||
|
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||||
|
repository = "https://github.com/ratatui/ratatui"
|
||||||
|
homepage = "https://ratatui.rs"
|
||||||
keywords = ["tui", "terminal", "dashboard"]
|
keywords = ["tui", "terminal", "dashboard"]
|
||||||
repository = "https://github.com/fdehau/tui-rs"
|
categories = ["command-line-interface"]
|
||||||
|
readme = "README.md"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
exclude = ["docs/*", ".travis.yml"]
|
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.74.0"
|
||||||
|
|
||||||
[badges]
|
[workspace.dependencies]
|
||||||
travis-ci = { repository = "fdehau/tui-rs" }
|
bitflags = "2.7.0"
|
||||||
|
color-eyre = "0.6.3"
|
||||||
|
crossterm = "0.28.1"
|
||||||
|
document-features = "0.2.7"
|
||||||
|
indoc = "2.0.5"
|
||||||
|
instability = "0.3.7"
|
||||||
|
itertools = "0.13.0"
|
||||||
|
pretty_assertions = "1.4.1"
|
||||||
|
ratatui = { path = "ratatui", version = "0.30.0-alpha.1" }
|
||||||
|
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.2" }
|
||||||
|
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.1" }
|
||||||
|
ratatui-macros = { path = "ratatui-macros", version = "0.7.0-alpha.0" }
|
||||||
|
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.1" }
|
||||||
|
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.1" }
|
||||||
|
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.1" }
|
||||||
|
rstest = "0.24.0"
|
||||||
|
serde = { version = "1.0.217", features = ["derive"] }
|
||||||
|
serde_json = "1.0.138"
|
||||||
|
strum = { version = "0.26.3", features = ["derive"] }
|
||||||
|
termion = "4.0.0"
|
||||||
|
termwiz = { version = "0.22.0" }
|
||||||
|
unicode-segmentation = "1.12.0"
|
||||||
|
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
|
||||||
|
unicode-width = "=0.2.0"
|
||||||
|
|
||||||
[features]
|
# Improve benchmark consistency
|
||||||
default = ["termion"]
|
[profile.bench]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
|
||||||
[dependencies]
|
[workspace.lints.rust]
|
||||||
bitflags = "1.0.1"
|
unsafe_code = "forbid"
|
||||||
cassowary = "0.3.0"
|
|
||||||
log = "0.4.0"
|
|
||||||
unicode-segmentation = "1.2.0"
|
|
||||||
unicode-width = "0.1.4"
|
|
||||||
termion = { version = "1.5.1", optional = true }
|
|
||||||
rustbox = { version = "0.9.0", optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[workspace.lints.clippy]
|
||||||
stderrlog = "0.2.3"
|
pedantic = { level = "warn", priority = -1 }
|
||||||
rand = "0.4.1"
|
cast_possible_truncation = "allow"
|
||||||
|
cast_possible_wrap = "allow"
|
||||||
|
cast_precision_loss = "allow"
|
||||||
|
cast_sign_loss = "allow"
|
||||||
|
missing_errors_doc = "allow"
|
||||||
|
missing_panics_doc = "allow"
|
||||||
|
module_name_repetitions = "allow"
|
||||||
|
must_use_candidate = "allow"
|
||||||
|
|
||||||
[[example]]
|
# we often split up a module into multiple files with the main type in a file named after the
|
||||||
name = "barchart"
|
# module, so we want to allow this pattern
|
||||||
path = "examples/barchart.rs"
|
module_inception = "allow"
|
||||||
|
|
||||||
[[example]]
|
# nursery or restricted
|
||||||
name = "block"
|
as_underscore = "warn"
|
||||||
path = "examples/block.rs"
|
deref_by_slicing = "warn"
|
||||||
|
else_if_without_else = "warn"
|
||||||
[[example]]
|
empty_line_after_doc_comments = "warn"
|
||||||
name = "canvas"
|
equatable_if_let = "warn"
|
||||||
path = "examples/canvas.rs"
|
fn_to_numeric_cast_any = "warn"
|
||||||
|
format_push_string = "warn"
|
||||||
[[example]]
|
map_err_ignore = "warn"
|
||||||
name = "chart"
|
missing_const_for_fn = "warn"
|
||||||
path = "examples/chart.rs"
|
mixed_read_write_in_expression = "warn"
|
||||||
|
mod_module_files = "warn"
|
||||||
[[example]]
|
needless_pass_by_ref_mut = "warn"
|
||||||
name = "custom_widget"
|
needless_raw_strings = "warn"
|
||||||
path = "examples/custom_widget.rs"
|
or_fun_call = "warn"
|
||||||
|
redundant_type_annotations = "warn"
|
||||||
[[example]]
|
rest_pat_in_fully_bound_structs = "warn"
|
||||||
name = "demo"
|
string_lit_chars_any = "warn"
|
||||||
path = "examples/demo.rs"
|
string_slice = "warn"
|
||||||
|
string_to_string = "warn"
|
||||||
[[example]]
|
unnecessary_self_imports = "warn"
|
||||||
name = "gauge"
|
use_self = "warn"
|
||||||
path = "examples/gauge.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "list"
|
|
||||||
path = "examples/list.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "paragraph"
|
|
||||||
path = "examples/paragraph.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "rustbox"
|
|
||||||
path = "examples/rustbox.rs"
|
|
||||||
required-features = ["rustbox"]
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "sparkline"
|
|
||||||
path = "examples/sparkline.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "table"
|
|
||||||
path = "examples/table.rs"
|
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "tabs"
|
|
||||||
path = "examples/tabs.rs"
|
|
||||||
|
|||||||
7
FUNDING.json
Normal file
7
FUNDING.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"drips": {
|
||||||
|
"ethereum": {
|
||||||
|
"ownedBy": "0x6053C8984f4F214Ad12c4653F28514E1E09213B5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
LICENSE
3
LICENSE
@@ -1,6 +1,7 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016 Florian Dehau
|
Copyright (c) 2016-2022 Florian Dehau
|
||||||
|
Copyright (c) 2023-2025 The Ratatui Developers
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
15
MAINTAINERS.md
Normal file
15
MAINTAINERS.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Maintainers
|
||||||
|
|
||||||
|
This file documents current and past maintainers.
|
||||||
|
|
||||||
|
- [orhun](https://github.com/orhun)
|
||||||
|
- [joshka](https://github.com/joshka)
|
||||||
|
- [kdheepak](https://github.com/kdheepak)
|
||||||
|
- [Valentin271](https://github.com/Valentin271)
|
||||||
|
|
||||||
|
## Past Maintainers
|
||||||
|
|
||||||
|
- [fdehau](https://github.com/fdehau)
|
||||||
|
- [mindoodoo](https://github.com/mindoodoo)
|
||||||
|
- [sayanarijit](https://github.com/sayanarijit)
|
||||||
|
- [EdJoPaTo](https://github.com/EdJoPaTo)
|
||||||
111
Makefile
111
Makefile
@@ -1,111 +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}'
|
|
||||||
|
|
||||||
|
|
||||||
# ================================ Tools ======================================
|
|
||||||
|
|
||||||
|
|
||||||
install-tools: install-rustfmt install-clippy ## Install tools dependencies
|
|
||||||
|
|
||||||
INSTALL_RUSTFMT = ./scripts/tools/install.sh --name=rustfmt-nightly
|
|
||||||
ifndef CI
|
|
||||||
INSTALL_RUSTFMT += --channel=nightly
|
|
||||||
endif
|
|
||||||
install-rustfmt: ## Intall rustfmt
|
|
||||||
$(INSTALL_RUSTFMT)
|
|
||||||
|
|
||||||
INSTALL_CLIPPY = ./scripts/tools/install.sh --name=clippy
|
|
||||||
ifndef CI
|
|
||||||
INSTALL_CLIPPY += --channel=nightly
|
|
||||||
endif
|
|
||||||
install-clippy: ## Intall rustfmt
|
|
||||||
$(INSTALL_CLIPPY)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================== 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 -- --write-mode=$(RUSTFMT_WRITEMODE)
|
|
||||||
|
|
||||||
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: install-tools build lint test ## Run build, lint and tests for nightly
|
|
||||||
195
README.md
195
README.md
@@ -1,63 +1,170 @@
|
|||||||
# tui-rs
|
<details>
|
||||||
|
<summary>Table of Contents</summary>
|
||||||
|
|
||||||
[](https://travis-ci.org/fdehau/tui-rs)
|
- [Quickstart](#quickstart)
|
||||||
[](https://crates.io/crates/tui)
|
- [Documentation](#documentation)
|
||||||
[](https://docs.rs/crate/tui/)
|
- [Templates](#templates)
|
||||||
|
- [Built with Ratatui](#built-with-ratatui)
|
||||||
|
- [Alternatives](#alternatives)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Acknowledgements](#acknowledgements)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
<img src="./docs/demo.gif" alt="Demo cast under Linux Termite with Inconsolata font 12pt">
|
</details>
|
||||||
|
|
||||||
`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).
|
|
||||||
|
|
||||||
The library itself supports two different backends to draw to the terminal. You
|
<div align="center">
|
||||||
can either choose from:
|
|
||||||
|
|
||||||
- [termion](https://github.com/ticki/termion)
|
[![Crate Badge]][Crate] [![Repo Badge]][Repo] [![Docs Badge]][Docs] [![License Badge]][License] \
|
||||||
- [rustbox](https://github.com/gchp/rustbox)
|
[![CI Badge]][CI] [![Deps Badge]][Deps] [![Codecov Badge]][Codecov] [![Sponsors Badge]][Sponsors] \
|
||||||
|
[Ratatui Website] · [Docs] · [Widget Examples] · [App Examples] · [Changelog] \
|
||||||
|
[Breaking Changes] · [Contributing] · [Report a bug] · [Request a Feature]
|
||||||
|
|
||||||
However, some features may only be available in one of the two.
|
</div>
|
||||||
|
|
||||||
The library is based on the principle of immediate rendering with intermediate
|
[Ratatui][Ratatui Website] (_ˌræ.təˈtu.i_) is a Rust crate for cooking up terminal user interfaces
|
||||||
buffers. This means that at each new frame you should build all widgets that are
|
(TUIs). It provides a simple and flexible way to create text-based user interfaces in the terminal,
|
||||||
supposed to be part of the UI. While providing a great flexibility for rich and
|
which can be used for command-line applications, dashboards, and other interactive console programs.
|
||||||
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.
|
|
||||||
|
|
||||||
Moreover, the library does not provide any input handling nor any event system and
|
## Quickstart
|
||||||
you may rely on the previously cited libraries to achieve such features.
|
|
||||||
|
|
||||||
### [Documentation](https://docs.rs/tui)
|
Ratatui has [templates] available to help you get started quickly. You can use the
|
||||||
|
[`cargo-generate`] command to create a new project with Ratatui:
|
||||||
|
|
||||||
### Widgets
|
```shell
|
||||||
|
cargo install --locked cargo-generate
|
||||||
|
cargo generate ratatui/templates
|
||||||
|
```
|
||||||
|
|
||||||
The library comes with the following list of widgets:
|
Selecting the Hello World template produces the following application:
|
||||||
|
|
||||||
* [Block](examples/block.rs)
|
```rust
|
||||||
* [Gauge](examples/gauge.rs)
|
use color_eyre::Result;
|
||||||
* [Sparkline](examples/sparkline.rs)
|
use crossterm::event::{self, Event};
|
||||||
* [Chart](examples/chart.rs)
|
use ratatui::{DefaultTerminal, Frame};
|
||||||
* [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)
|
|
||||||
|
|
||||||
Click on each item to see the source of the example. Run the examples with with
|
fn main() -> Result<()> {
|
||||||
cargo (e.g. to run the demo `cargo run --example demo`), and quit by pressing `q`.
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let result = run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
### Demo
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
terminal.draw(render)?;
|
||||||
|
if matches!(event::read()?, Event::Key(_)) {
|
||||||
|
break Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
The [source code](examples/demo.rs) of the demo gif.
|
fn render(frame: &mut Frame) {
|
||||||
|
frame.render_widget("hello world", frame.area());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Docs] - the full API documentation for the library on docs.rs.
|
||||||
|
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials.
|
||||||
|
- [Ratatui Forum] - a place to ask questions and discuss the library.
|
||||||
|
- [Widget Examples] - a collection of examples that demonstrate how to use the library.
|
||||||
|
- [App Examples] - a collection of more complex examples that demonstrate how to build apps.
|
||||||
|
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||||
|
- [Breaking Changes] - a list of breaking changes in the library.
|
||||||
|
|
||||||
|
You can also watch the [EuroRust 2024 talk] to learn about common concepts in Ratatui and what's
|
||||||
|
possible to build with it.
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
If you're looking to get started quickly, you can use one of the available templates from the
|
||||||
|
[templates] repository using [`cargo-generate`]:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo generate ratatui/templates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built with Ratatui
|
||||||
|
|
||||||
|
[][awesome-ratatui]
|
||||||
|
|
||||||
|
Check out the [showcase] section of the website, or the [awesome-ratatui] repository for a curated
|
||||||
|
list of awesome apps and libraries built with Ratatui!
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
- [Cursive](https://crates.io/crates/cursive) - a ncurses-based TUI library.
|
||||||
|
- [iocraft](https://crates.io/crates/iocraft) - a declarative TUI library.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
[![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix] [![Forum Badge]][Ratatui Forum]
|
||||||
|
|
||||||
|
Feel free to join our [Discord server](https://discord.gg/pMCEU9hNEj) for discussions and questions!
|
||||||
|
There is also a [Matrix](https://matrix.org/) bridge available at
|
||||||
|
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org). We have also recently launched the
|
||||||
|
[Ratatui Forum].
|
||||||
|
|
||||||
|
We rely on GitHub for [bugs][Report a bug] and [feature requests][Request a Feature].
|
||||||
|
|
||||||
|
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines before [creating a pull
|
||||||
|
request][Create a Pull Request].
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development. None of
|
||||||
|
this could be possible without [Florian Dehau] who originally created [tui-rs] which inspired many
|
||||||
|
Rust TUIs.
|
||||||
|
|
||||||
|
Special thanks to [Pavel Fomchenkov] for his work in designing an awesome logo for the Ratatui
|
||||||
|
project and organization.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](LICENSE)
|
This project is licensed under the [MIT License][License].
|
||||||
|
|
||||||
## Author
|
[Repo]: https://github.com/ratatui/ratatui
|
||||||
|
[Ratatui Website]: https://ratatui.rs/
|
||||||
Florian Dehau
|
[Ratatui Forum]: https://forum.ratatui.rs
|
||||||
|
[Docs]: https://docs.rs/ratatui
|
||||||
|
[Widget Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui-widgets/examples
|
||||||
|
[App Examples]: https://github.com/ratatui/ratatui/tree/main/examples
|
||||||
|
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
|
||||||
|
[git-cliff]: https://git-cliff.org
|
||||||
|
[Conventional Commits]: https://www.conventionalcommits.org
|
||||||
|
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
|
||||||
|
[EuroRust 2024 talk]: https://www.youtube.com/watch?v=hWG51Mc1DlM
|
||||||
|
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||||
|
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||||
|
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
|
||||||
|
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
|
||||||
|
[Crate]: https://crates.io/crates/ratatui
|
||||||
|
[tui-rs]: https://crates.io/crates/tui
|
||||||
|
[Sponsors]: https://github.com/sponsors/ratatui
|
||||||
|
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&color=E05D44
|
||||||
|
[Repo Badge]: https://img.shields.io/badge/repo-ratatui/ratatui-1370D3?style=flat-square&logo=github
|
||||||
|
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||||
|
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
|
||||||
|
[CI]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||||
|
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3
|
||||||
|
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
|
||||||
|
[Deps Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?path=ratatui&style=flat-square
|
||||||
|
[Deps]: https://deps.rs/repo/github/ratatui/ratatui?path=ratatui
|
||||||
|
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||||
|
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||||
|
[Docs Badge]: https://img.shields.io/badge/docs-ratatui-1370D3?style=flat-square&logo=rust
|
||||||
|
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||||
|
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||||
|
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
|
||||||
|
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
|
||||||
|
[templates]: https://github.com/ratatui/templates/
|
||||||
|
[showcase]: https://ratatui.rs/showcase/
|
||||||
|
[awesome-ratatui]: https://github.com/ratatui/awesome-ratatui
|
||||||
|
[Pavel Fomchenkov]: https://github.com/nawok
|
||||||
|
[Florian Dehau]: https://github.com/fdehau
|
||||||
|
[`cargo-generate`]: https://crates.io/crates/cargo-generate
|
||||||
|
[License]: ./LICENSE
|
||||||
|
|||||||
51
RELEASE.md
Normal file
51
RELEASE.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Creating a Release
|
||||||
|
|
||||||
|
Our release strategy is:
|
||||||
|
|
||||||
|
> Release major versions with detailed summaries when necessary, while releasing minor versions
|
||||||
|
> weekly or as needed without extensive announcements.
|
||||||
|
>
|
||||||
|
> Versioning scheme being `0.x.y`, where `x` is the major version and `y` is the minor version.
|
||||||
|
|
||||||
|
[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 if necessary. The preferred tool for this is
|
||||||
|
[vhs](https://github.com/charmbracelet/vhs) (installation instructions in README).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo build --example demo2
|
||||||
|
vhs examples/demo2.tape
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Switch branches to the images branch and copy demo2.gif to examples/, commit, and push.
|
||||||
|
1. Grab the permalink from <https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif> and
|
||||||
|
append `?raw=true` to redirect to the actual image url. Then update the link in the main README.
|
||||||
|
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. Ensure that any breaking changes are documented in [BREAKING-CHANGES.md](./BREAKING-CHANGES.md)
|
||||||
|
1. Commit and push the changes.
|
||||||
|
1. Create a new tag: `git tag -a v[0.x.y]`
|
||||||
|
1. Push the tag: `git push --tags`
|
||||||
|
1. Wait for [Continuous Deployment](https://github.com/ratatui/ratatui/actions) workflow to
|
||||||
|
finish.
|
||||||
|
|
||||||
|
## Alpha Releases
|
||||||
|
|
||||||
|
Alpha releases are automatically released every Saturday via [cd.yml](./.github/workflows/cd.yml)
|
||||||
|
and can be manually be created when necessary by triggering the [Continuous
|
||||||
|
Deployment](https://github.com/ratatui/ratatui/actions/workflows/cd.yml) workflow.
|
||||||
|
|
||||||
|
We automatically release an alpha release with a patch level bump + alpha.num weekly (and when we
|
||||||
|
need to manually). E.g. the last release was 0.22.0, and the most recent alpha release is
|
||||||
|
0.22.1-alpha.1.
|
||||||
|
|
||||||
|
These releases will have whatever happened to be in main at the time of release, so they're useful
|
||||||
|
for apps that need to get releases from crates.io, but may contain more bugs and be generally less
|
||||||
|
tested than normal releases.
|
||||||
|
|
||||||
|
See [#147](https://github.com/ratatui/ratatui/issues/147) and
|
||||||
|
[#359](https://github.com/ratatui/ratatui/pull/359) for more info on the alpha release process.
|
||||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We only support the latest version of this crate.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
To report secuirity vulnerability, please use the form at <https://github.com/ratatui/ratatui/security/advisories/new>
|
||||||
BIN
assets/favicon.ico
Normal file
BIN
assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/logo.png
Normal file
BIN
assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
76
bacon.toml
Normal file
76
bacon.toml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# 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", "xtask", "check"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.check-all]
|
||||||
|
command = ["cargo", "xtask", "check", "--all-features"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.check-crossterm]
|
||||||
|
command = ["cargo", "xtask", "check-backend", "crossterm"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.check-termion]
|
||||||
|
command = ["cargo", "xtask", "check-backend", "termion"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.check-termwiz]
|
||||||
|
command = ["cargo", "xtask", "check-backend", "termwiz"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.clippy-all]
|
||||||
|
command = ["cargo", "xtask", "clippy"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.test]
|
||||||
|
command = ["cargo", "xtask", "test"]
|
||||||
|
need_stdout = true
|
||||||
|
|
||||||
|
[jobs.test-unit]
|
||||||
|
command = ["cargo", "xtask", "test-libs"]
|
||||||
|
need_stdout = true
|
||||||
|
|
||||||
|
[jobs.doc]
|
||||||
|
command = ["cargo", "xtask", "docs"]
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.doc-open]
|
||||||
|
command = ["cargo", "xtask", "docs", "--open"]
|
||||||
|
on_success = "job:doc"
|
||||||
|
need_stdout = false
|
||||||
|
|
||||||
|
[jobs.coverage]
|
||||||
|
command = ["cargo", "xtask", "coverage"]
|
||||||
|
|
||||||
|
[jobs.coverage-unit-tests-only]
|
||||||
|
command = ["cargo", "xtask", "coverage", "--lib"]
|
||||||
|
|
||||||
|
[jobs.hack]
|
||||||
|
command = ["cargo", "xtask", "hack"]
|
||||||
|
|
||||||
|
[jobs.format]
|
||||||
|
command = ["cargo", "xtask", "format"]
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
ctrl-h = "job:hack"
|
||||||
|
ctrl-c = "job:check-crossterm"
|
||||||
|
ctrl-t = "job:check-termion"
|
||||||
|
ctrl-w = "job:check-termwiz"
|
||||||
|
v = "job:coverage"
|
||||||
|
ctrl-v = "job:coverage-unit-tests-only"
|
||||||
|
u = "job:test-unit"
|
||||||
|
n = "job:nextest"
|
||||||
|
f = "job:format"
|
||||||
128
cliff.toml
Normal file
128
cliff.toml
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# git-cliff ~ configuration file
|
||||||
|
# https://git-cliff.org/docs/configuration
|
||||||
|
|
||||||
|
[remote.github]
|
||||||
|
owner = "ratatui"
|
||||||
|
repo = "ratatui"
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
# changelog header
|
||||||
|
header = """
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
|
||||||
|
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
|
||||||
|
"""
|
||||||
|
# template for the changelog body
|
||||||
|
# https://keats.github.io/tera/docs/#introduction
|
||||||
|
# note that the - before / after the % controls whether whitespace is rendered between each line.
|
||||||
|
# Getting this right so that the markdown renders with the correct number of lines between headings
|
||||||
|
# code fences and list items is pretty finicky. Note also that the 4 backticks in the commit macro
|
||||||
|
# is intentional as this escapes any backticks in the commit body.
|
||||||
|
body = """
|
||||||
|
{%- if not version %}
|
||||||
|
## [unreleased]
|
||||||
|
{% else -%}
|
||||||
|
## {{ package }} - [{{ version }}]({{ release_link }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% macro commit(commit) -%}
|
||||||
|
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
|
||||||
|
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
|
||||||
|
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
|
||||||
|
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
|
||||||
|
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||||
|
{% endmacro -%}
|
||||||
|
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | striptags | trim | upper_first }}
|
||||||
|
{% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
|
||||||
|
{{ self::commit(commit=commit) }}
|
||||||
|
{%- endfor -%}
|
||||||
|
{% for commit in commits %}
|
||||||
|
{%- if not commit.scope %}
|
||||||
|
{{ self::commit(commit=commit) }}
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- endfor %}
|
||||||
|
|
||||||
|
{% if version %}
|
||||||
|
{% if previous.version %}
|
||||||
|
**Full Changelog**: {{ release_link }}
|
||||||
|
{% endif %}
|
||||||
|
{% else -%}
|
||||||
|
{% raw %}\n{% endraw %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{%- macro remote_url() -%}
|
||||||
|
https://github.com/{{ remote.owner }}/{{ remote.repo }}\
|
||||||
|
{% endmacro %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# remove the leading and trailing whitespace from the template
|
||||||
|
trim = false
|
||||||
|
# changelog footer
|
||||||
|
footer = """
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
|
"""
|
||||||
|
postprocessors = [
|
||||||
|
{ pattern = '<!-- Please read CONTRIBUTING.md before submitting any pull request. -->', replace = "" },
|
||||||
|
{ pattern = '>---+\n', replace = '' },
|
||||||
|
{ pattern = ' +\n', replace = "\n" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[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 = "" },
|
||||||
|
{ 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\\(deps\\)", skip = true },
|
||||||
|
{ message = "^chore\\(changelog\\)", skip = true },
|
||||||
|
{ message = "^[cC]hore", 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" },
|
||||||
|
# handle some old commits styles from pre 0.4
|
||||||
|
{ message = "^(Buffer|buffer|Frame|frame|Gauge|gauge|Paragraph|paragraph):", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||||
|
{ message = "^\\[", group = "<!-- 07 -->Miscellaneous Tasks" },
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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 = "alpha"
|
||||||
|
# sort the tags topologically
|
||||||
|
topo_order = false
|
||||||
|
# sort the commits inside sections by oldest/newest order
|
||||||
|
sort_commits = "newest"
|
||||||
1
clippy.toml
Normal file
1
clippy.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
avoid-breaking-exported-api = false
|
||||||
14
codecov.yml
Normal file
14
codecov.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
coverage: # https://docs.codecov.com/docs/codecovyml-reference#coverage
|
||||||
|
precision: 1 # e.g. 89.1%
|
||||||
|
round: down
|
||||||
|
range: 85..100 # https://docs.codecov.com/docs/coverage-configuration#section-range
|
||||||
|
status: # https://docs.codecov.com/docs/commit-status
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
threshold: 1% # Avoid false negatives
|
||||||
|
ignore:
|
||||||
|
- "examples"
|
||||||
|
- "benches"
|
||||||
|
comment: # https://docs.codecov.com/docs/pull-request-comments
|
||||||
|
# make the comments less noisy
|
||||||
|
require_changes: true
|
||||||
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",
|
||||||
|
]
|
||||||
40
deny.toml
Normal file
40
deny.toml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# configuration for https://github.com/EmbarkStudios/cargo-deny
|
||||||
|
|
||||||
|
[licenses]
|
||||||
|
version = 2
|
||||||
|
confidence-threshold = 0.8
|
||||||
|
allow = [
|
||||||
|
"Apache-2.0",
|
||||||
|
"BSD-2-Clause",
|
||||||
|
"BSD-3-Clause",
|
||||||
|
"ISC",
|
||||||
|
"MIT",
|
||||||
|
"OpenSSL",
|
||||||
|
"Unicode-3.0",
|
||||||
|
"Unicode-DFS-2016",
|
||||||
|
"WTFPL",
|
||||||
|
"Zlib",
|
||||||
|
]
|
||||||
|
|
||||||
|
[advisories]
|
||||||
|
version = 2
|
||||||
|
|
||||||
|
[bans]
|
||||||
|
multiple-versions = "allow"
|
||||||
|
|
||||||
|
[sources]
|
||||||
|
unknown-registry = "deny"
|
||||||
|
unknown-git = "warn"
|
||||||
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||||
|
|
||||||
|
[[licenses.clarify]]
|
||||||
|
crate = "ring"
|
||||||
|
# SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses
|
||||||
|
# https://spdx.org/licenses/OpenSSL.html
|
||||||
|
# ISC - Both BoringSSL and ring use this for their new files
|
||||||
|
# MIT - "Files in third_party/ have their own licenses, as described therein. The MIT
|
||||||
|
# license, for third_party/fiat, which, unlike other third_party directories, is
|
||||||
|
# compiled into non-test libraries, is included below."
|
||||||
|
# OpenSSL - Obviously
|
||||||
|
expression = "ISC AND MIT AND OpenSSL"
|
||||||
|
license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
|
||||||
BIN
docs/demo.gif
BIN
docs/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
272
examples/README.md
Normal file
272
examples/README.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Examples
|
||||||
|
|
||||||
|
This folder contains examples that are more application focused.
|
||||||
|
There are also [widget examples] in `ratatui-widgets`.
|
||||||
|
|
||||||
|
[widget examples]: ../ratatui-widgets/examples
|
||||||
|
|
||||||
|
You can run these examples using:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p example-name
|
||||||
|
```
|
||||||
|
|
||||||
|
This folder might use unreleased code. Consider viewing the examples in the `latest` branch instead
|
||||||
|
of the `main` branch for code which is guaranteed to work with the released ratatui version.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
>
|
||||||
|
> There may be backwards incompatible changes in these examples, as they are designed to compile
|
||||||
|
> against the `main` branch.
|
||||||
|
>
|
||||||
|
> There are a few workaround for this problem:
|
||||||
|
>
|
||||||
|
> - View the examples as they were when the latest version was release by selecting the tag that
|
||||||
|
> matches that version. E.g. <https://github.com/ratatui/ratatui/tree/v0.26.1/examples>.
|
||||||
|
> - If you're viewing this file on GitHub, there is a combo box at the top of this page which
|
||||||
|
> allows you to select any previous tagged version.
|
||||||
|
> - To view the code locally, checkout the tag. E.g. `git switch --detach v0.26.1`.
|
||||||
|
> - Use the latest [alpha version of Ratatui] in your app. These are released weekly on Saturdays.
|
||||||
|
> - Compile your code against the main branch either locally by adding e.g. `path = "../ratatui"` to
|
||||||
|
> the dependency, or remotely by adding `git = "https://github.com/ratatui/ratatui"`
|
||||||
|
>
|
||||||
|
> For a list of unreleased breaking changes, see [BREAKING-CHANGES.md].
|
||||||
|
>
|
||||||
|
> We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run
|
||||||
|
> `git-cliff -u` against a cloned version of this repository.
|
||||||
|
|
||||||
|
## Design choices
|
||||||
|
|
||||||
|
The examples contain some opinionated choices in order to make it easier for newer rustaceans to
|
||||||
|
easily be productive in creating applications:
|
||||||
|
|
||||||
|
- Each example has an `App` struct, with methods that implement a main loop, handle events and drawing
|
||||||
|
the UI.
|
||||||
|
- We use `color_eyre` for handling errors and panics. See [How to use color-eyre with Ratatui] on the
|
||||||
|
website for more information about this.
|
||||||
|
- Common code is not extracted into a separate file. This makes each example self-contained and easy
|
||||||
|
to read as a whole.
|
||||||
|
|
||||||
|
[How to use color-eyre with Ratatui]: https://ratatui.rs/recipes/apps/color-eyre/
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
This is the original demo example from the main README. It is available for each of the backends.
|
||||||
|
[Source](./apps/demo/).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Demo2
|
||||||
|
|
||||||
|
This is the demo example from the main README and crate page. [Source](./apps/demo2/).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Async GitHub
|
||||||
|
|
||||||
|
Shows how to fetch data from GitHub API asynchronously. [Source](./apps/async-github/).
|
||||||
|
|
||||||
|
![Async GitHub demo][async-github.gif]
|
||||||
|
|
||||||
|
## Calendar Explorer
|
||||||
|
|
||||||
|
Shows how to render a calendar with different styles. [Source](./apps/calendar-explorer/).
|
||||||
|
|
||||||
|
![Calendar explorer demo][calendar-explorer.gif]
|
||||||
|
|
||||||
|
## Canvas
|
||||||
|
|
||||||
|
Shows how to render a canvas with different shapes. [Source](./apps/canvas/).
|
||||||
|
|
||||||
|
![Canvas demo][canvas.gif]
|
||||||
|
|
||||||
|
## Chart
|
||||||
|
|
||||||
|
Shows how to render line, bar, and scatter charts. [Source](./apps/chart/).
|
||||||
|
|
||||||
|
![Chart demo][chart.gif]
|
||||||
|
|
||||||
|
## Color Explorer
|
||||||
|
|
||||||
|
Shows how to handle the supported colors. [Source](./apps/color-explorer/).
|
||||||
|
|
||||||
|
![Color explorer demo][color-explorer.gif]
|
||||||
|
|
||||||
|
## Colors-RGB demo
|
||||||
|
|
||||||
|
Shows the full range of RGB colors in an animation. [Source](./apps/colors-rgb/).
|
||||||
|
|
||||||
|
![Colors-RGB demo][colors-rgb.gif]
|
||||||
|
|
||||||
|
## Constraint Explorer
|
||||||
|
|
||||||
|
Shows how different constraints can be used to layout widgets. [Source](./apps/constraint-explorer/).
|
||||||
|
|
||||||
|
![Constraint Explorer demo][constraint-explorer.gif]
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
Shows different types of constraints. [Source](./apps/constraints/).
|
||||||
|
|
||||||
|
![Constraints demo][constraints.gif]
|
||||||
|
|
||||||
|
## Custom Widget
|
||||||
|
|
||||||
|
Shows how to create a custom widget that can be interacted with the mouse. [Source](./apps/custom-widget/).
|
||||||
|
|
||||||
|
![Custom widget demo][custom-widget.gif]
|
||||||
|
|
||||||
|
## Hyperlink
|
||||||
|
|
||||||
|
Shows how to render hyperlinks in a terminal using [OSC
|
||||||
|
8](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda). [Source](./apps/hyperlink/).
|
||||||
|
|
||||||
|
![Hyperlink demo][hyperlink.gif]
|
||||||
|
|
||||||
|
## Flex
|
||||||
|
|
||||||
|
Shows how to use the flex layouts. [Source](./apps/flex/).
|
||||||
|
|
||||||
|
![Flex demo][flex.gif]
|
||||||
|
|
||||||
|
## Hello World
|
||||||
|
|
||||||
|
Shows how to create a simple TUI with a text. [Source](./apps/hello-world/).
|
||||||
|
|
||||||
|
![Hello World demo][hello-world.gif]
|
||||||
|
|
||||||
|
## Gauge
|
||||||
|
|
||||||
|
Shows different types of gauges. [Source](./apps/gauge/).
|
||||||
|
|
||||||
|
## Inline
|
||||||
|
|
||||||
|
Shows how to use the inlined viewport to render in a specific area of the screen. [Source](./apps/inline/).
|
||||||
|
|
||||||
|
![Inline demo][inline.gif]
|
||||||
|
|
||||||
|
## Input Form
|
||||||
|
|
||||||
|
Shows how to render a form with input fields. [Source](./apps/input-form/).
|
||||||
|
|
||||||
|
## Modifiers
|
||||||
|
|
||||||
|
Shows different types of modifiers. [Source](./apps/modifiers/).
|
||||||
|
|
||||||
|
![Modifiers demo][modifiers.gif]
|
||||||
|
|
||||||
|
## Mouse Drawing
|
||||||
|
|
||||||
|
Shows how to handle mouse events. [Source](./apps/mouse-drawing/).
|
||||||
|
|
||||||
|
## Minimal
|
||||||
|
|
||||||
|
Shows how to create a minimal application. [Source](./apps/minimal/).
|
||||||
|
|
||||||
|
![Minimal demo][minimal.gif]
|
||||||
|
|
||||||
|
## Panic
|
||||||
|
|
||||||
|
Shows how to handle panics. [Source](./apps/panic/).
|
||||||
|
|
||||||
|
![Panic demo][panic.gif]
|
||||||
|
|
||||||
|
## Popup
|
||||||
|
|
||||||
|
Shows how to handle popups. [Source](./apps/popup/).
|
||||||
|
|
||||||
|
![Popup demo][popup.gif]
|
||||||
|
|
||||||
|
## Scrollbar
|
||||||
|
|
||||||
|
Shows how to render different types of scrollbars. [Source](./apps/scrollbar/).
|
||||||
|
|
||||||
|
![Scrollbar demo][scrollbar.gif]
|
||||||
|
|
||||||
|
## Table
|
||||||
|
|
||||||
|
Shows how to create an interactive table. [Source](./apps/table/).
|
||||||
|
|
||||||
|
![Table demo][table.gif]
|
||||||
|
|
||||||
|
## Todo List
|
||||||
|
|
||||||
|
Shows how to create a simple todo list application. [Source](./apps/todo-list/).
|
||||||
|
|
||||||
|
![Todo List demo][todo-list.gif]
|
||||||
|
|
||||||
|
## Tracing
|
||||||
|
|
||||||
|
Shows how to use the [tracing](https://crates.io/crates/tracing) crate to log to a file. [Source](./apps/tracing/).
|
||||||
|
|
||||||
|
![Tracing demo][tracing.gif]
|
||||||
|
|
||||||
|
## User Input
|
||||||
|
|
||||||
|
Shows how to handle user input. [Source](./apps/user-input/). [Source](./apps/user-input/).
|
||||||
|
|
||||||
|
![User input demo][user-input.gif]
|
||||||
|
|
||||||
|
## Weather
|
||||||
|
|
||||||
|
Shows how to render weather data using barchart widget. [Source](./apps/weather/).
|
||||||
|
|
||||||
|
## WidgetRef Container
|
||||||
|
|
||||||
|
Shows how to use [`WidgetRef`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.WidgetRef.html) to store widgets in a container. [Source](./apps/widget-ref-container/).
|
||||||
|
|
||||||
|
## Advanced Widget Implementation
|
||||||
|
|
||||||
|
Shows how to render the `Widget` trait in different ways.
|
||||||
|
|
||||||
|
![Advanced widget impl demo][advanced-widget-impl.gif]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>How to update these examples?</summary>
|
||||||
|
|
||||||
|
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
|
||||||
|
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
|
||||||
|
images themselves are stored in a separate `images` git branch to avoid bloating the `main`
|
||||||
|
branch.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Links to images to make them easier to update in bulk. Use the following script to update and upload
|
||||||
|
the examples to the images branch. (Requires push access to the branch).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
vhs/generate.bash
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
[advanced-widget-impl.gif]: https://github.com/ratatui/ratatui/blob/images/examples/advanced-widget-impl.gif?raw=true
|
||||||
|
[async-github.gif]: https://github.com/ratatui/ratatui/blob/images/examples/async-github.gif?raw=true
|
||||||
|
[calendar-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/calendar-explorer.gif?raw=true
|
||||||
|
[canvas.gif]: https://github.com/ratatui/ratatui/blob/images/examples/canvas.gif?raw=true
|
||||||
|
[chart.gif]: https://github.com/ratatui/ratatui/blob/images/examples/chart.gif?raw=true
|
||||||
|
[color-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/color-explorer.gif?raw=true
|
||||||
|
[colors-rgb.gif]: https://github.com/ratatui/ratatui/blob/images/examples/colors-rgb.gif?raw=true
|
||||||
|
[constraint-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraint-explorer.gif?raw=true
|
||||||
|
[constraints.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraints.gif?raw=true
|
||||||
|
[custom-widget.gif]: https://github.com/ratatui/ratatui/blob/images/examples/custom-widget.gif?raw=true
|
||||||
|
[demo2-destroy.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2-destroy.gif?raw=true
|
||||||
|
[demo2-social.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2-social.gif?raw=true
|
||||||
|
[demo2.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif?raw=true
|
||||||
|
[demo.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo.gif?raw=true
|
||||||
|
[flex.gif]: https://github.com/ratatui/ratatui/blob/images/examples/flex.gif?raw=true
|
||||||
|
[hello-world.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hello-world.gif?raw=true
|
||||||
|
[hyperlink.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hyperlink.gif?raw=true
|
||||||
|
[inline.gif]: https://github.com/ratatui/ratatui/blob/images/examples/inline.gif?raw=true
|
||||||
|
[minimal.gif]: https://github.com/ratatui/ratatui/blob/images/examples/minimal.gif?raw=true
|
||||||
|
[modifiers.gif]: https://github.com/ratatui/ratatui/blob/images/examples/modifiers.gif?raw=true
|
||||||
|
[panic.gif]: https://github.com/ratatui/ratatui/blob/images/examples/panic.gif?raw=true
|
||||||
|
[popup.gif]: https://github.com/ratatui/ratatui/blob/images/examples/popup.gif?raw=true
|
||||||
|
[scrollbar.gif]: https://github.com/ratatui/ratatui/blob/images/examples/scrollbar.gif?raw=true
|
||||||
|
[table.gif]: https://github.com/ratatui/ratatui/blob/images/examples/table.gif?raw=true
|
||||||
|
[todo-list.gif]: https://github.com/ratatui/ratatui/blob/images/examples/todo-list.gif?raw=true
|
||||||
|
[tracing.gif]: https://github.com/ratatui/ratatui/blob/images/examples/tracing.gif?raw=true
|
||||||
|
[user-input.gif]: https://github.com/ratatui/ratatui/blob/images/examples/user-input.gif?raw=true
|
||||||
14
examples/apps/advanced-widget-impl/Cargo.toml
Normal file
14
examples/apps/advanced-widget-impl/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "advanced-widget-impl"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
ratatui = { workspace = true, features = ["unstable-widget-ref"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/advanced-widget-impl/README.md
Normal file
9
examples/apps/advanced-widget-impl/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Advanced Widget Implementation demo
|
||||||
|
|
||||||
|
This example shows how to render the `Widget` trait in different ways.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p advanced-widget-impl
|
||||||
|
```
|
||||||
249
examples/apps/advanced-widget-impl/src/main.rs
Normal file
249
examples/apps/advanced-widget-impl/src/main.rs
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
/// A Ratatui example that demonstrates how to implement the `Widget` trait.
|
||||||
|
///
|
||||||
|
/// This example demonstrates various ways to implement `Widget` traits in Ratatui on a type, a
|
||||||
|
/// reference, and a mutable reference. It also shows how to use the `WidgetRef` trait to
|
||||||
|
/// render boxed widgets.
|
||||||
|
///
|
||||||
|
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||||
|
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||||
|
/// release.
|
||||||
|
///
|
||||||
|
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use crossterm::event::{self, Event, KeyCode};
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Layout, Position, Rect, Size},
|
||||||
|
style::{Color, Style},
|
||||||
|
widgets::{Widget, WidgetRef},
|
||||||
|
DefaultTerminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct App {
|
||||||
|
should_quit: bool,
|
||||||
|
timer: Timer,
|
||||||
|
boxed_squares: BoxedSquares,
|
||||||
|
green_square: RightAlignedSquare,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
while !self.should_quit {
|
||||||
|
self.draw(&mut terminal)?;
|
||||||
|
self.handle_events()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&mut self, tui: &mut DefaultTerminal) -> Result<()> {
|
||||||
|
tui.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
|
// Handle events at least 50 frames per second (gifs are usually 50fps)
|
||||||
|
let timeout = Duration::from_secs_f64(1.0 / 50.0);
|
||||||
|
if !event::poll(timeout)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement the `Widget` trait on a mutable reference to the `App` type.
|
||||||
|
///
|
||||||
|
/// This allows the `App` type to be rendered as a widget. The `App` type owns several other widgets
|
||||||
|
/// that are rendered as part of the app. The `Widget` trait is implemented on a mutable reference
|
||||||
|
/// to the `App` type, which allows this to be rendered without consuming the `App` type, and allows
|
||||||
|
/// the sub-widgets to be mutable.
|
||||||
|
impl Widget for &mut App {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let constraints = Constraint::from_lengths([1, 1, 2, 1]);
|
||||||
|
let [greeting, timer, squares, position] = Layout::vertical(constraints).areas(area);
|
||||||
|
|
||||||
|
// render an ephemeral greeting widget
|
||||||
|
Greeting::new("Ratatui!").render(greeting, buf);
|
||||||
|
|
||||||
|
// render a reference to the timer widget
|
||||||
|
self.timer.render(timer, buf);
|
||||||
|
|
||||||
|
// render a boxed widget containing red and blue squares
|
||||||
|
self.boxed_squares.render(squares, buf);
|
||||||
|
|
||||||
|
// render a mutable reference to the green square widget
|
||||||
|
self.green_square.render(squares, buf);
|
||||||
|
// Display the dynamically updated position of the green square
|
||||||
|
let square_position = format!("Green square is at {}", self.green_square.last_position);
|
||||||
|
square_position.render(position, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ephemeral greeting widget.
|
||||||
|
///
|
||||||
|
/// This widget is implemented on the type itself, which means that it is consumed when it is
|
||||||
|
/// rendered. This is useful for widgets that are cheap to create, don't need to be reused, and
|
||||||
|
/// don't need to store any state between renders. This is the simplest way to implement a widget in
|
||||||
|
/// Ratatui, but in most cases, it is better to implement the `Widget` trait on a reference to the
|
||||||
|
/// type, as shown in the other examples below.
|
||||||
|
///
|
||||||
|
/// This was the way most widgets were implemented in Ratatui before `Widget` was implemented on
|
||||||
|
/// references in [PR #903] (merged in Ratatui 0.26.0).
|
||||||
|
///
|
||||||
|
/// [PR #903]: https://github.com/ratatui/ratatui/pull/903
|
||||||
|
struct Greeting {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Greeting {
|
||||||
|
fn new(name: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Greeting {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let greeting = format!("Hello, {}!", self.name);
|
||||||
|
greeting.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A timer widget that displays the elapsed time since the timer was started.
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Timer {
|
||||||
|
start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Timer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
start: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This implements `Widget` on a reference to the type, which means that it can be reused and
|
||||||
|
/// doesn't need to be consumed when it is rendered. This is useful for widgets that need to store
|
||||||
|
/// state and be updated over time.
|
||||||
|
///
|
||||||
|
/// This approach was probably always available in Ratatui, but it wasn't widely used until `Widget`
|
||||||
|
/// was implemented on references in [PR #903] (merged in Ratatui 0.26.0). This is because all the
|
||||||
|
/// built-in widgets previously would consume themselves when rendered.
|
||||||
|
impl Widget for &Timer {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let elapsed = self.start.elapsed().as_secs_f32();
|
||||||
|
let message = format!("Elapsed: {elapsed:.1?}s");
|
||||||
|
message.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that contains a list of several different widgets.
|
||||||
|
struct BoxedSquares {
|
||||||
|
squares: Vec<Box<dyn WidgetRef>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BoxedSquares {
|
||||||
|
fn default() -> Self {
|
||||||
|
let red_square: Box<dyn WidgetRef> = Box::new(RedSquare);
|
||||||
|
let blue_square: Box<dyn WidgetRef> = Box::new(BlueSquare);
|
||||||
|
Self {
|
||||||
|
squares: vec![red_square, blue_square],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that renders a red square.
|
||||||
|
struct RedSquare;
|
||||||
|
|
||||||
|
/// A widget that renders a blue square.
|
||||||
|
struct BlueSquare;
|
||||||
|
|
||||||
|
/// This implements the `Widget` trait on a reference to the type. It contains a list of boxed
|
||||||
|
/// widgets that implement the `WidgetRef` trait. This is useful for widgets that contain a list of
|
||||||
|
/// other widgets that can be different types.
|
||||||
|
impl Widget for &BoxedSquares {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let constraints = vec![Constraint::Length(4); self.squares.len()];
|
||||||
|
let areas = Layout::horizontal(constraints).split(area);
|
||||||
|
for (widget, area) in self.squares.iter().zip(areas.iter()) {
|
||||||
|
widget.render_ref(*area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `RedSquare` and `BlueSquare` are widgets that render a red and blue square, respectively. They
|
||||||
|
/// implement the `WidgetRef` trait instead of the `Widget` trait, which which allows them to be
|
||||||
|
/// rendered as boxed widgets. It's not possible to use Widget for this as a dynamic reference to a
|
||||||
|
/// widget cannot generally be moved out of the box.
|
||||||
|
impl WidgetRef for RedSquare {
|
||||||
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
fill(area, buf, "█", Color::Red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidgetRef for BlueSquare {
|
||||||
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
fill(area, buf, "█", Color::Blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that renders a green square aligned to the right of the area.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct RightAlignedSquare {
|
||||||
|
last_position: Position,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This widget is implemented on a mutable reference to the type, which means that it can store
|
||||||
|
/// state and update it when it is rendered. This is useful for widgets that need to store the
|
||||||
|
/// result of some calculation that can only be done when the widget is rendered.
|
||||||
|
///
|
||||||
|
/// The x and y coordinates of the square are stored in the widget and updated when the widget is
|
||||||
|
/// rendered. This allows the square to be aligned to the right of the area. These coordinates could
|
||||||
|
/// be used to perform hit testing (e.g. checking if a mouse click is inside the square). This app
|
||||||
|
/// just displays the coordinates as a string.
|
||||||
|
///
|
||||||
|
/// This approach was probably always available in Ratatui, but it wasn't widely used either. This
|
||||||
|
/// is an alternative to implementing the `StatefulWidget` trait, for situations where you want to
|
||||||
|
/// store the state in the widget itself instead of a separate struct.
|
||||||
|
impl Widget for &mut RightAlignedSquare {
|
||||||
|
/// Render a green square aligned to the right of the area and store the position.
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
const WIDTH: u16 = 4;
|
||||||
|
let x = area.right() - WIDTH; // Align to the right
|
||||||
|
self.last_position = Position { x, y: area.y };
|
||||||
|
let size = Size::new(WIDTH, area.height);
|
||||||
|
let area = Rect::from((self.last_position, size));
|
||||||
|
fill(area, buf, "█", Color::Green);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill the area with the specified symbol and style.
|
||||||
|
///
|
||||||
|
/// This probably should be a method on the `Buffer` type, but it is defined here for simplicity.
|
||||||
|
/// <https://github.com/ratatui/ratatui/issues/1146>
|
||||||
|
fn fill<S: Into<Style>>(area: Rect, buf: &mut Buffer, symbol: &str, style: S) {
|
||||||
|
let style = style.into();
|
||||||
|
for y in area.top()..area.bottom() {
|
||||||
|
for x in area.left()..area.right() {
|
||||||
|
buf[(x, y)].set_symbol(symbol).set_style(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
examples/apps/async-github/Cargo.toml
Normal file
22
examples/apps/async-github/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "async-github"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
keywords.workspace = true
|
||||||
|
categories.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
exclude.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre = "0.6.3"
|
||||||
|
crossterm = { workspace = true, features = ["event-stream"] }
|
||||||
|
octocrab = "0.43.0"
|
||||||
|
ratatui.workspace = true
|
||||||
|
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros"] }
|
||||||
|
tokio-stream = "0.1.17"
|
||||||
9
examples/apps/async-github/README.md
Normal file
9
examples/apps/async-github/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Async GitHub demo
|
||||||
|
|
||||||
|
This example demonstrates how to use Ratatui with widgets that fetch data from GitHub API asynchronously.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p async-github
|
||||||
|
```
|
||||||
243
examples/apps/async-github/src/main.rs
Normal file
243
examples/apps/async-github/src/main.rs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
//! # [Ratatui] Async example
|
||||||
|
//!
|
||||||
|
//! This example demonstrates how to use Ratatui with widgets that fetch data asynchronously. It
|
||||||
|
//! uses the `octocrab` crate to fetch a list of pull requests from the GitHub API.
|
||||||
|
//!
|
||||||
|
//! <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token>
|
||||||
|
//! <https://github.com/settings/tokens/new> to create a new token (select classic, and no scopes)
|
||||||
|
//!
|
||||||
|
//! This example does not cover message passing between threads, it only demonstrates how to manage
|
||||||
|
//! shared state between the main thread and a background task, which acts mostly as a one-shot
|
||||||
|
//! fetcher. For more complex scenarios, you may need to use channels or other synchronization
|
||||||
|
//! primitives.
|
||||||
|
//!
|
||||||
|
//! A simple app might have multiple widgets that fetch data from different sources, and each widget
|
||||||
|
//! would have its own background task to fetch the data. The main thread would then render the
|
||||||
|
//! widgets with the latest data.
|
||||||
|
//!
|
||||||
|
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||||
|
//!
|
||||||
|
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||||
|
//! repository. This means that you may not be able to compile with the latest release version on
|
||||||
|
//! crates.io, or the one that you have installed locally.
|
||||||
|
//!
|
||||||
|
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||||
|
//! library you are using.
|
||||||
|
//!
|
||||||
|
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||||
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
use std::{
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use octocrab::{
|
||||||
|
params::{pulls::Sort, Direction},
|
||||||
|
Page,
|
||||||
|
};
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
crossterm::event::{Event, EventStream, KeyCode, KeyEventKind},
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
style::{Style, Stylize},
|
||||||
|
text::Line,
|
||||||
|
widgets::{Block, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal).await;
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct App {
|
||||||
|
should_quit: bool,
|
||||||
|
pull_requests: PullRequestListWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
const FRAMES_PER_SECOND: f32 = 60.0;
|
||||||
|
|
||||||
|
pub async fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
self.pull_requests.run();
|
||||||
|
|
||||||
|
let period = Duration::from_secs_f32(1.0 / Self::FRAMES_PER_SECOND);
|
||||||
|
let mut interval = tokio::time::interval(period);
|
||||||
|
let mut events = EventStream::new();
|
||||||
|
|
||||||
|
while !self.should_quit {
|
||||||
|
tokio::select! {
|
||||||
|
_ = interval.tick() => { terminal.draw(|frame| self.draw(frame))?; },
|
||||||
|
Some(Ok(event)) = events.next() => self.handle_event(&event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, frame: &mut Frame) {
|
||||||
|
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]);
|
||||||
|
let [title_area, body_area] = vertical.areas(frame.area());
|
||||||
|
let title = Line::from("Ratatui async example").centered().bold();
|
||||||
|
frame.render_widget(title, title_area);
|
||||||
|
frame.render_widget(&self.pull_requests, body_area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(&mut self, event: &Event) {
|
||||||
|
if let Event::Key(key) = event {
|
||||||
|
if key.kind == KeyEventKind::Press {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
|
||||||
|
KeyCode::Char('j') | KeyCode::Down => self.pull_requests.scroll_down(),
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => self.pull_requests.scroll_up(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that displays a list of pull requests.
|
||||||
|
///
|
||||||
|
/// This is an async widget that fetches the list of pull requests from the GitHub API. It contains
|
||||||
|
/// an inner `Arc<RwLock<PullRequestListState>>` that holds the state of the widget. Cloning the
|
||||||
|
/// widget will clone the Arc, so you can pass it around to other threads, and this is used to spawn
|
||||||
|
/// a background task to fetch the pull requests.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct PullRequestListWidget {
|
||||||
|
state: Arc<RwLock<PullRequestListState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct PullRequestListState {
|
||||||
|
pull_requests: Vec<PullRequest>,
|
||||||
|
loading_state: LoadingState,
|
||||||
|
table_state: TableState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct PullRequest {
|
||||||
|
id: String,
|
||||||
|
title: String,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
enum LoadingState {
|
||||||
|
#[default]
|
||||||
|
Idle,
|
||||||
|
Loading,
|
||||||
|
Loaded,
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PullRequestListWidget {
|
||||||
|
/// Start fetching the pull requests in the background.
|
||||||
|
///
|
||||||
|
/// This method spawns a background task that fetches the pull requests from the GitHub API.
|
||||||
|
/// The result of the fetch is then passed to the `on_load` or `on_err` methods.
|
||||||
|
fn run(&self) {
|
||||||
|
let this = self.clone(); // clone the widget to pass to the background task
|
||||||
|
tokio::spawn(this.fetch_pulls());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn fetch_pulls(self) {
|
||||||
|
// this runs once, but you could also run this in a loop, using a channel that accepts
|
||||||
|
// messages to refresh on demand, or with an interval timer to refresh every N seconds
|
||||||
|
self.set_loading_state(LoadingState::Loading);
|
||||||
|
match octocrab::instance()
|
||||||
|
.pulls("ratatui", "ratatui")
|
||||||
|
.list()
|
||||||
|
.sort(Sort::Updated)
|
||||||
|
.direction(Direction::Descending)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(page) => self.on_load(&page),
|
||||||
|
Err(err) => self.on_err(&err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn on_load(&self, page: &Page<OctoPullRequest>) {
|
||||||
|
let prs = page.items.iter().map(Into::into);
|
||||||
|
let mut state = self.state.write().unwrap();
|
||||||
|
state.loading_state = LoadingState::Loaded;
|
||||||
|
state.pull_requests.extend(prs);
|
||||||
|
if !state.pull_requests.is_empty() {
|
||||||
|
state.table_state.select(Some(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_err(&self, err: &octocrab::Error) {
|
||||||
|
self.set_loading_state(LoadingState::Error(err.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_loading_state(&self, state: LoadingState) {
|
||||||
|
self.state.write().unwrap().loading_state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_down(&self) {
|
||||||
|
self.state.write().unwrap().table_state.scroll_down_by(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scroll_up(&self) {
|
||||||
|
self.state.write().unwrap().table_state.scroll_up_by(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type OctoPullRequest = octocrab::models::pulls::PullRequest;
|
||||||
|
|
||||||
|
impl From<&OctoPullRequest> for PullRequest {
|
||||||
|
fn from(pr: &OctoPullRequest) -> Self {
|
||||||
|
Self {
|
||||||
|
id: pr.number.to_string(),
|
||||||
|
title: pr.title.as_ref().unwrap().to_string(),
|
||||||
|
url: pr
|
||||||
|
.html_url
|
||||||
|
.as_ref()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &PullRequestListWidget {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let mut state = self.state.write().unwrap();
|
||||||
|
|
||||||
|
// a block with a right aligned title with the loading state on the right
|
||||||
|
let loading_state = Line::from(format!("{:?}", state.loading_state)).right_aligned();
|
||||||
|
let block = Block::bordered()
|
||||||
|
.title("Pull Requests")
|
||||||
|
.title(loading_state)
|
||||||
|
.title_bottom("j/k to scroll, q to quit");
|
||||||
|
|
||||||
|
// a table with the list of pull requests
|
||||||
|
let rows = state.pull_requests.iter();
|
||||||
|
let widths = [
|
||||||
|
Constraint::Length(5),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Max(49),
|
||||||
|
];
|
||||||
|
let table = Table::new(rows, widths)
|
||||||
|
.block(block)
|
||||||
|
.highlight_spacing(HighlightSpacing::Always)
|
||||||
|
.highlight_symbol(">>")
|
||||||
|
.row_highlight_style(Style::new().on_blue());
|
||||||
|
|
||||||
|
StatefulWidget::render(table, area, buf, &mut state.table_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PullRequest> for Row<'_> {
|
||||||
|
fn from(pr: &PullRequest) -> Self {
|
||||||
|
let pr = pr.clone();
|
||||||
|
Row::new(vec![pr.id, pr.title, pr.url])
|
||||||
|
}
|
||||||
|
}
|
||||||
15
examples/apps/calendar-explorer/Cargo.toml
Normal file
15
examples/apps/calendar-explorer/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "calendar-explorer"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
time = { version = "0.3.37", features = ["formatting", "parsing"] }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/calendar-explorer/README.md
Normal file
9
examples/apps/calendar-explorer/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Calendar explorer demo
|
||||||
|
|
||||||
|
This example shows how to render a calendar with different styles.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p calendar-explorer
|
||||||
|
```
|
||||||
248
examples/apps/calendar-explorer/src/main.rs
Normal file
248
examples/apps/calendar-explorer/src/main.rs
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
//! A Ratatui example that demonstrates how to render calendar with different styles.
|
||||||
|
//!
|
||||||
|
//! Marks the holidays and seasons on the calendar.
|
||||||
|
//!
|
||||||
|
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||||
|
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||||
|
//!
|
||||||
|
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||||
|
//! [`BarChart`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||||
|
use ratatui::{
|
||||||
|
layout::{Constraint, Layout, Margin, Rect},
|
||||||
|
style::{Color, Modifier, Style, Stylize},
|
||||||
|
text::{Line, Text},
|
||||||
|
widgets::calendar::{CalendarEventStore, Monthly},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
|
use time::{ext::NumericalDuration, Date, Month, OffsetDateTime};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let result = run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the application.
|
||||||
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
let mut selected_date = OffsetDateTime::now_local()?.date();
|
||||||
|
let mut calendar_style = StyledCalendar::Default;
|
||||||
|
loop {
|
||||||
|
terminal.draw(|frame| render(frame, calendar_style, selected_date))?;
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind == KeyEventKind::Press {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') => break Ok(()),
|
||||||
|
KeyCode::Char('s') => calendar_style = calendar_style.next(),
|
||||||
|
KeyCode::Char('n') | KeyCode::Tab => selected_date = next_month(selected_date),
|
||||||
|
KeyCode::Char('p') | KeyCode::BackTab => {
|
||||||
|
selected_date = previous_month(selected_date);
|
||||||
|
}
|
||||||
|
KeyCode::Char('h') | KeyCode::Left => selected_date -= 1.days(),
|
||||||
|
KeyCode::Char('j') | KeyCode::Down => selected_date += 1.weeks(),
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => selected_date -= 1.weeks(),
|
||||||
|
KeyCode::Char('l') | KeyCode::Right => selected_date += 1.days(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_month(date: Date) -> Date {
|
||||||
|
if date.month() == Month::December {
|
||||||
|
date.replace_month(Month::January)
|
||||||
|
.unwrap()
|
||||||
|
.replace_year(date.year() + 1)
|
||||||
|
.unwrap()
|
||||||
|
} else {
|
||||||
|
date.replace_month(date.month().next()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous_month(date: Date) -> Date {
|
||||||
|
if date.month() == Month::January {
|
||||||
|
date.replace_month(Month::December)
|
||||||
|
.unwrap()
|
||||||
|
.replace_year(date.year() - 1)
|
||||||
|
.unwrap()
|
||||||
|
} else {
|
||||||
|
date.replace_month(date.month().previous()).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw the UI with a calendar.
|
||||||
|
fn render(frame: &mut Frame, calendar_style: StyledCalendar, selected_date: Date) {
|
||||||
|
let header = Text::from_iter([
|
||||||
|
Line::from("Calendar Example".bold()),
|
||||||
|
Line::from(
|
||||||
|
"<q> Quit | <s> Change Style | <n> Next Month | <p> Previous Month, <hjkl> Move",
|
||||||
|
),
|
||||||
|
Line::from(format!(
|
||||||
|
"Current date: {selected_date} | Current style: {calendar_style}"
|
||||||
|
)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let vertical = Layout::vertical([
|
||||||
|
Constraint::Length(header.height() as u16),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
]);
|
||||||
|
let [text_area, area] = vertical.areas(frame.area());
|
||||||
|
frame.render_widget(header.centered(), text_area);
|
||||||
|
calendar_style
|
||||||
|
.render_year(frame, area, selected_date)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum StyledCalendar {
|
||||||
|
Default,
|
||||||
|
Surrounding,
|
||||||
|
WeekdaysHeader,
|
||||||
|
SurroundingAndWeekdaysHeader,
|
||||||
|
MonthHeader,
|
||||||
|
MonthAndWeekdaysHeader,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyledCalendar {
|
||||||
|
// Cycle through the different styles.
|
||||||
|
const fn next(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Default => Self::Surrounding,
|
||||||
|
Self::Surrounding => Self::WeekdaysHeader,
|
||||||
|
Self::WeekdaysHeader => Self::SurroundingAndWeekdaysHeader,
|
||||||
|
Self::SurroundingAndWeekdaysHeader => Self::MonthHeader,
|
||||||
|
Self::MonthHeader => Self::MonthAndWeekdaysHeader,
|
||||||
|
Self::MonthAndWeekdaysHeader => Self::Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for StyledCalendar {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Default => write!(f, "Default"),
|
||||||
|
Self::Surrounding => write!(f, "Show Surrounding"),
|
||||||
|
Self::WeekdaysHeader => write!(f, "Show Weekdays Header"),
|
||||||
|
Self::SurroundingAndWeekdaysHeader => write!(f, "Show Surrounding and Weekdays Header"),
|
||||||
|
Self::MonthHeader => write!(f, "Show Month Header"),
|
||||||
|
Self::MonthAndWeekdaysHeader => write!(f, "Show Month Header and Weekdays Header"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StyledCalendar {
|
||||||
|
fn render_year(self, frame: &mut Frame, area: Rect, date: Date) -> Result<()> {
|
||||||
|
let events = events(date)?;
|
||||||
|
|
||||||
|
let area = area.inner(Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 1,
|
||||||
|
});
|
||||||
|
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
|
||||||
|
let areas = rows.iter().flat_map(|row| {
|
||||||
|
Layout::horizontal([Constraint::Ratio(1, 4); 4])
|
||||||
|
.split(*row)
|
||||||
|
.to_vec()
|
||||||
|
});
|
||||||
|
for (i, area) in areas.enumerate() {
|
||||||
|
let month = date
|
||||||
|
.replace_day(1)
|
||||||
|
.unwrap()
|
||||||
|
.replace_month(Month::try_from(i as u8 + 1).unwrap())
|
||||||
|
.unwrap();
|
||||||
|
self.render_month(frame, area, month, &events);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_month(self, frame: &mut Frame, area: Rect, date: Date, events: &CalendarEventStore) {
|
||||||
|
let calendar = match self {
|
||||||
|
Self::Default => Monthly::new(date, events)
|
||||||
|
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||||
|
.show_month_header(Style::default()),
|
||||||
|
Self::Surrounding => Monthly::new(date, events)
|
||||||
|
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||||
|
.show_month_header(Style::default())
|
||||||
|
.show_surrounding(Style::new().dim()),
|
||||||
|
Self::WeekdaysHeader => Monthly::new(date, events)
|
||||||
|
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||||
|
.show_month_header(Style::default())
|
||||||
|
.show_weekdays_header(Style::new().bold().green()),
|
||||||
|
Self::SurroundingAndWeekdaysHeader => Monthly::new(date, events)
|
||||||
|
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||||
|
.show_month_header(Style::default())
|
||||||
|
.show_surrounding(Style::new().dim())
|
||||||
|
.show_weekdays_header(Style::new().bold().green()),
|
||||||
|
Self::MonthHeader => Monthly::new(date, events)
|
||||||
|
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||||
|
.show_month_header(Style::default())
|
||||||
|
.show_month_header(Style::new().bold().green()),
|
||||||
|
Self::MonthAndWeekdaysHeader => Monthly::new(date, events)
|
||||||
|
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||||
|
.show_month_header(Style::default())
|
||||||
|
.show_weekdays_header(Style::new().bold().dim().light_yellow()),
|
||||||
|
};
|
||||||
|
frame.render_widget(calendar, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a list of dates for the current year.
|
||||||
|
fn events(selected_date: Date) -> Result<CalendarEventStore> {
|
||||||
|
const SELECTED: Style = Style::new()
|
||||||
|
.fg(Color::White)
|
||||||
|
.bg(Color::Red)
|
||||||
|
.add_modifier(Modifier::BOLD);
|
||||||
|
const HOLIDAY: Style = Style::new()
|
||||||
|
.fg(Color::Red)
|
||||||
|
.add_modifier(Modifier::UNDERLINED);
|
||||||
|
const SEASON: Style = Style::new()
|
||||||
|
.fg(Color::Green)
|
||||||
|
.bg(Color::Black)
|
||||||
|
.add_modifier(Modifier::UNDERLINED);
|
||||||
|
|
||||||
|
let mut list = CalendarEventStore::today(
|
||||||
|
Style::default()
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
.bg(Color::Blue),
|
||||||
|
);
|
||||||
|
let y = selected_date.year();
|
||||||
|
|
||||||
|
// new year's
|
||||||
|
list.add(Date::from_calendar_date(y, Month::January, 1)?, HOLIDAY);
|
||||||
|
// next new_year's for December "show surrounding"
|
||||||
|
list.add(Date::from_calendar_date(y + 1, Month::January, 1)?, HOLIDAY);
|
||||||
|
// groundhog day
|
||||||
|
list.add(Date::from_calendar_date(y, Month::February, 2)?, HOLIDAY);
|
||||||
|
// april fool's
|
||||||
|
list.add(Date::from_calendar_date(y, Month::April, 1)?, HOLIDAY);
|
||||||
|
// earth day
|
||||||
|
list.add(Date::from_calendar_date(y, Month::April, 22)?, HOLIDAY);
|
||||||
|
// star wars day
|
||||||
|
list.add(Date::from_calendar_date(y, Month::May, 4)?, HOLIDAY);
|
||||||
|
// festivus
|
||||||
|
list.add(Date::from_calendar_date(y, Month::December, 23)?, HOLIDAY);
|
||||||
|
// new year's eve
|
||||||
|
list.add(Date::from_calendar_date(y, Month::December, 31)?, HOLIDAY);
|
||||||
|
|
||||||
|
// seasons
|
||||||
|
// spring equinox
|
||||||
|
list.add(Date::from_calendar_date(y, Month::March, 22)?, SEASON);
|
||||||
|
// summer solstice
|
||||||
|
list.add(Date::from_calendar_date(y, Month::June, 21)?, SEASON);
|
||||||
|
// fall equinox
|
||||||
|
list.add(Date::from_calendar_date(y, Month::September, 22)?, SEASON);
|
||||||
|
// winter solstice
|
||||||
|
list.add(Date::from_calendar_date(y, Month::December, 21)?, SEASON);
|
||||||
|
|
||||||
|
// selected date
|
||||||
|
list.add(selected_date, SELECTED);
|
||||||
|
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
15
examples/apps/canvas/Cargo.toml
Normal file
15
examples/apps/canvas/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "canvas"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/canvas/README.md
Normal file
9
examples/apps/canvas/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Canvas demo
|
||||||
|
|
||||||
|
This example shows how to render various shapes and a map on a canvas.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p canvas
|
||||||
|
```
|
||||||
265
examples/apps/canvas/src/main.rs
Normal file
265
examples/apps/canvas/src/main.rs
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/// A Ratatui example that demonstrates how to draw on a canvas.
|
||||||
|
///
|
||||||
|
/// This example demonstrates how to draw various shapes such as rectangles, circles, and lines
|
||||||
|
/// on a canvas. It also demonstrates how to draw a map.
|
||||||
|
///
|
||||||
|
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||||
|
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||||
|
/// release.
|
||||||
|
///
|
||||||
|
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||||
|
use std::{
|
||||||
|
io::stdout,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use crossterm::{
|
||||||
|
event::{DisableMouseCapture, EnableMouseCapture, KeyEventKind},
|
||||||
|
ExecutableCommand,
|
||||||
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ratatui::{
|
||||||
|
crossterm::event::{self, Event, KeyCode, MouseEventKind},
|
||||||
|
layout::{Constraint, Layout, Position, Rect},
|
||||||
|
style::{Color, Stylize},
|
||||||
|
symbols::Marker,
|
||||||
|
text::Text,
|
||||||
|
widgets::{
|
||||||
|
canvas::{Canvas, Circle, Map, MapResolution, Points, Rectangle},
|
||||||
|
Block, Widget,
|
||||||
|
},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
stdout().execute(EnableMouseCapture)?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
stdout().execute(DisableMouseCapture)?;
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
exit: bool,
|
||||||
|
x: f64,
|
||||||
|
y: f64,
|
||||||
|
ball: Circle,
|
||||||
|
playground: Rect,
|
||||||
|
vx: f64,
|
||||||
|
vy: f64,
|
||||||
|
marker: Marker,
|
||||||
|
points: Vec<Position>,
|
||||||
|
is_drawing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
exit: false,
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
ball: Circle {
|
||||||
|
x: 20.0,
|
||||||
|
y: 40.0,
|
||||||
|
radius: 10.0,
|
||||||
|
color: Color::Yellow,
|
||||||
|
},
|
||||||
|
playground: Rect::new(10, 10, 200, 100),
|
||||||
|
vx: 1.0,
|
||||||
|
vy: 1.0,
|
||||||
|
marker: Marker::Dot,
|
||||||
|
points: vec![],
|
||||||
|
is_drawing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
let tick_rate = Duration::from_millis(16);
|
||||||
|
let mut last_tick = Instant::now();
|
||||||
|
while !self.exit {
|
||||||
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
|
if event::poll(timeout)? {
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key) => self.handle_key_press(key),
|
||||||
|
Event::Mouse(event) => self.handle_mouse_event(event),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if last_tick.elapsed() >= tick_rate {
|
||||||
|
self.on_tick();
|
||||||
|
last_tick = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_press(&mut self, key: event::KeyEvent) {
|
||||||
|
if key.kind != KeyEventKind::Press {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') => self.exit = true,
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
|
||||||
|
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
|
||||||
|
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
|
||||||
|
KeyCode::Enter => {
|
||||||
|
self.marker = match self.marker {
|
||||||
|
Marker::Dot => Marker::Braille,
|
||||||
|
Marker::Braille => Marker::Block,
|
||||||
|
Marker::Block => Marker::HalfBlock,
|
||||||
|
Marker::HalfBlock => Marker::Bar,
|
||||||
|
Marker::Bar => Marker::Dot,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mouse_event(&mut self, event: event::MouseEvent) {
|
||||||
|
match event.kind {
|
||||||
|
MouseEventKind::Down(_) => self.is_drawing = true,
|
||||||
|
MouseEventKind::Up(_) => self.is_drawing = false,
|
||||||
|
MouseEventKind::Drag(_) => {
|
||||||
|
self.points.push(Position::new(event.column, event.row));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_tick(&mut self) {
|
||||||
|
// bounce the ball by flipping the velocity vector
|
||||||
|
let ball = &self.ball;
|
||||||
|
let playground = self.playground;
|
||||||
|
if ball.x - ball.radius < f64::from(playground.left())
|
||||||
|
|| ball.x + ball.radius > f64::from(playground.right())
|
||||||
|
{
|
||||||
|
self.vx = -self.vx;
|
||||||
|
}
|
||||||
|
if ball.y - ball.radius < f64::from(playground.top())
|
||||||
|
|| ball.y + ball.radius > f64::from(playground.bottom())
|
||||||
|
{
|
||||||
|
self.vy = -self.vy;
|
||||||
|
}
|
||||||
|
self.ball.x += self.vx;
|
||||||
|
self.ball.y += self.vy;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, frame: &mut Frame) {
|
||||||
|
let header = Text::from_iter([
|
||||||
|
"Canvas Example".bold(),
|
||||||
|
"<q> Quit | <enter> Change Marker | <hjkl> Move".into(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let vertical = Layout::vertical([
|
||||||
|
Constraint::Length(header.height() as u16),
|
||||||
|
Constraint::Percentage(50),
|
||||||
|
Constraint::Percentage(50),
|
||||||
|
]);
|
||||||
|
let [text_area, up, down] = vertical.areas(frame.area());
|
||||||
|
frame.render_widget(header.centered(), text_area);
|
||||||
|
|
||||||
|
let horizontal =
|
||||||
|
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||||
|
let [draw, pong] = horizontal.areas(up);
|
||||||
|
let [map, boxes] = horizontal.areas(down);
|
||||||
|
|
||||||
|
frame.render_widget(self.map_canvas(), map);
|
||||||
|
frame.render_widget(self.draw_canvas(draw), draw);
|
||||||
|
frame.render_widget(self.pong_canvas(), pong);
|
||||||
|
frame.render_widget(self.boxes_canvas(boxes), boxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_canvas(&self) -> impl Widget + '_ {
|
||||||
|
Canvas::default()
|
||||||
|
.block(Block::bordered().title("World"))
|
||||||
|
.marker(self.marker)
|
||||||
|
.paint(|ctx| {
|
||||||
|
ctx.draw(&Map {
|
||||||
|
color: Color::Green,
|
||||||
|
resolution: MapResolution::High,
|
||||||
|
});
|
||||||
|
ctx.print(self.x, -self.y, "You are here".yellow());
|
||||||
|
})
|
||||||
|
.x_bounds([-180.0, 180.0])
|
||||||
|
.y_bounds([-90.0, 90.0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_canvas(&self, area: Rect) -> impl Widget + '_ {
|
||||||
|
Canvas::default()
|
||||||
|
.block(Block::bordered().title("Draw here"))
|
||||||
|
.marker(self.marker)
|
||||||
|
.x_bounds([0.0, f64::from(area.width)])
|
||||||
|
.y_bounds([0.0, f64::from(area.height)])
|
||||||
|
.paint(move |ctx| {
|
||||||
|
let points = self
|
||||||
|
.points
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
(
|
||||||
|
f64::from(p.x) - f64::from(area.left()),
|
||||||
|
f64::from(area.bottom()) - f64::from(p.y),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
ctx.draw(&Points {
|
||||||
|
coords: &points,
|
||||||
|
color: Color::White,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pong_canvas(&self) -> impl Widget + '_ {
|
||||||
|
Canvas::default()
|
||||||
|
.block(Block::bordered().title("Pong"))
|
||||||
|
.marker(self.marker)
|
||||||
|
.paint(|ctx| {
|
||||||
|
ctx.draw(&self.ball);
|
||||||
|
})
|
||||||
|
.x_bounds([10.0, 210.0])
|
||||||
|
.y_bounds([10.0, 110.0])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn boxes_canvas(&self, area: Rect) -> impl Widget {
|
||||||
|
let left = 0.0;
|
||||||
|
let right = f64::from(area.width);
|
||||||
|
let bottom = 0.0;
|
||||||
|
let top = f64::from(area.height).mul_add(2.0, -4.0);
|
||||||
|
Canvas::default()
|
||||||
|
.block(Block::bordered().title("Rects"))
|
||||||
|
.marker(self.marker)
|
||||||
|
.x_bounds([left, right])
|
||||||
|
.y_bounds([bottom, top])
|
||||||
|
.paint(|ctx| {
|
||||||
|
for i in 0..=11 {
|
||||||
|
ctx.draw(&Rectangle {
|
||||||
|
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||||
|
y: 2.0,
|
||||||
|
width: f64::from(i),
|
||||||
|
height: f64::from(i),
|
||||||
|
color: Color::Red,
|
||||||
|
});
|
||||||
|
ctx.draw(&Rectangle {
|
||||||
|
x: f64::from(i * i + 3 * i) / 2.0 + 2.0,
|
||||||
|
y: 21.0,
|
||||||
|
width: f64::from(i),
|
||||||
|
height: f64::from(i),
|
||||||
|
color: Color::Blue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for i in 0..100 {
|
||||||
|
if i % 10 != 0 {
|
||||||
|
ctx.print(f64::from(i) + 1.0, 0.0, format!("{i}", i = i % 10));
|
||||||
|
}
|
||||||
|
if i % 2 == 0 && i % 10 != 0 {
|
||||||
|
ctx.print(0.0, f64::from(i), format!("{i}", i = i % 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
14
examples/apps/chart/Cargo.toml
Normal file
14
examples/apps/chart/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "chart"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/chart/README.md
Normal file
9
examples/apps/chart/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Chart demo
|
||||||
|
|
||||||
|
This example shows how to render line, bar, and scatter charts.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p chart
|
||||||
|
```
|
||||||
354
examples/apps/chart/src/main.rs
Normal file
354
examples/apps/chart/src/main.rs
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
/// A Ratatui example that demonstrates how to handle charts.
|
||||||
|
///
|
||||||
|
/// This example demonstrates how to draw various types of charts such as line, bar, and
|
||||||
|
/// scatter charts.
|
||||||
|
///
|
||||||
|
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||||
|
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||||
|
/// release.
|
||||||
|
///
|
||||||
|
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use ratatui::{
|
||||||
|
crossterm::event::{self, Event, KeyCode},
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
style::{Color, Modifier, Style, Stylize},
|
||||||
|
symbols::{self, Marker},
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Axis, Block, Chart, Dataset, GraphType, LegendPosition},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::new().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
signal1: SinSignal,
|
||||||
|
data1: Vec<(f64, f64)>,
|
||||||
|
signal2: SinSignal,
|
||||||
|
data2: Vec<(f64, f64)>,
|
||||||
|
window: [f64; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SinSignal {
|
||||||
|
x: f64,
|
||||||
|
interval: f64,
|
||||||
|
period: f64,
|
||||||
|
scale: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SinSignal {
|
||||||
|
const fn new(interval: f64, period: f64, scale: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn new() -> Self {
|
||||||
|
let mut signal1 = SinSignal::new(0.2, 3.0, 18.0);
|
||||||
|
let mut signal2 = SinSignal::new(0.1, 2.0, 10.0);
|
||||||
|
let data1 = signal1.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||||
|
let data2 = signal2.by_ref().take(200).collect::<Vec<(f64, f64)>>();
|
||||||
|
Self {
|
||||||
|
signal1,
|
||||||
|
data1,
|
||||||
|
signal2,
|
||||||
|
data2,
|
||||||
|
window: [0.0, 20.0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
let tick_rate = Duration::from_millis(250);
|
||||||
|
let mut last_tick = Instant::now();
|
||||||
|
loop {
|
||||||
|
terminal.draw(|frame| self.draw(frame))?;
|
||||||
|
|
||||||
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
|
if event::poll(timeout)? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.code == KeyCode::Char('q') {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last_tick.elapsed() >= tick_rate {
|
||||||
|
self.on_tick();
|
||||||
|
last_tick = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_tick(&mut self) {
|
||||||
|
self.data1.drain(0..5);
|
||||||
|
self.data1.extend(self.signal1.by_ref().take(5));
|
||||||
|
|
||||||
|
self.data2.drain(0..10);
|
||||||
|
self.data2.extend(self.signal2.by_ref().take(10));
|
||||||
|
|
||||||
|
self.window[0] += 1.0;
|
||||||
|
self.window[1] += 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, frame: &mut Frame) {
|
||||||
|
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(frame.area());
|
||||||
|
let [animated_chart, bar_chart] =
|
||||||
|
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
|
||||||
|
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
|
||||||
|
|
||||||
|
self.render_animated_chart(frame, animated_chart);
|
||||||
|
render_barchart(frame, bar_chart);
|
||||||
|
render_line_chart(frame, line_chart);
|
||||||
|
render_scatter(frame, scatter);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_animated_chart(&self, frame: &mut Frame, area: Rect) {
|
||||||
|
let x_labels = vec![
|
||||||
|
Span::styled(
|
||||||
|
format!("{}", self.window[0]),
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(format!("{}", (self.window[0] + self.window[1]) / 2.0)),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}", self.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(&self.data1),
|
||||||
|
Dataset::default()
|
||||||
|
.name("data3")
|
||||||
|
.marker(symbols::Marker::Braille)
|
||||||
|
.style(Style::default().fg(Color::Yellow))
|
||||||
|
.data(&self.data2),
|
||||||
|
];
|
||||||
|
|
||||||
|
let chart = Chart::new(datasets)
|
||||||
|
.block(Block::bordered())
|
||||||
|
.x_axis(
|
||||||
|
Axis::default()
|
||||||
|
.title("X Axis")
|
||||||
|
.style(Style::default().fg(Color::Gray))
|
||||||
|
.labels(x_labels)
|
||||||
|
.bounds(self.window),
|
||||||
|
)
|
||||||
|
.y_axis(
|
||||||
|
Axis::default()
|
||||||
|
.title("Y Axis")
|
||||||
|
.style(Style::default().fg(Color::Gray))
|
||||||
|
.labels(["-20".bold(), "0".into(), "20".bold()])
|
||||||
|
.bounds([-20.0, 20.0]),
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.render_widget(chart, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
|
||||||
|
let dataset = Dataset::default()
|
||||||
|
.marker(symbols::Marker::HalfBlock)
|
||||||
|
.style(Style::new().fg(Color::Blue))
|
||||||
|
.graph_type(GraphType::Bar)
|
||||||
|
// a bell curve
|
||||||
|
.data(&[
|
||||||
|
(0., 0.4),
|
||||||
|
(10., 2.9),
|
||||||
|
(20., 13.5),
|
||||||
|
(30., 41.1),
|
||||||
|
(40., 80.1),
|
||||||
|
(50., 100.0),
|
||||||
|
(60., 80.1),
|
||||||
|
(70., 41.1),
|
||||||
|
(80., 13.5),
|
||||||
|
(90., 2.9),
|
||||||
|
(100., 0.4),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let chart = Chart::new(vec![dataset])
|
||||||
|
.block(Block::bordered().title_top(Line::from("Bar chart").cyan().bold().centered()))
|
||||||
|
.x_axis(
|
||||||
|
Axis::default()
|
||||||
|
.style(Style::default().gray())
|
||||||
|
.bounds([0.0, 100.0])
|
||||||
|
.labels(["0".bold(), "50".into(), "100.0".bold()]),
|
||||||
|
)
|
||||||
|
.y_axis(
|
||||||
|
Axis::default()
|
||||||
|
.style(Style::default().gray())
|
||||||
|
.bounds([0.0, 100.0])
|
||||||
|
.labels(["0".bold(), "50".into(), "100.0".bold()]),
|
||||||
|
)
|
||||||
|
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||||
|
|
||||||
|
frame.render_widget(chart, bar_chart);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_line_chart(frame: &mut Frame, area: Rect) {
|
||||||
|
let datasets = vec![Dataset::default()
|
||||||
|
.name("Line from only 2 points".italic())
|
||||||
|
.marker(symbols::Marker::Braille)
|
||||||
|
.style(Style::default().fg(Color::Yellow))
|
||||||
|
.graph_type(GraphType::Line)
|
||||||
|
.data(&[(1., 1.), (4., 4.)])];
|
||||||
|
|
||||||
|
let chart = Chart::new(datasets)
|
||||||
|
.block(Block::bordered().title(Line::from("Line chart").cyan().bold().centered()))
|
||||||
|
.x_axis(
|
||||||
|
Axis::default()
|
||||||
|
.title("X Axis")
|
||||||
|
.style(Style::default().gray())
|
||||||
|
.bounds([0.0, 5.0])
|
||||||
|
.labels(["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||||
|
)
|
||||||
|
.y_axis(
|
||||||
|
Axis::default()
|
||||||
|
.title("Y Axis")
|
||||||
|
.style(Style::default().gray())
|
||||||
|
.bounds([0.0, 5.0])
|
||||||
|
.labels(["0".bold(), "2.5".into(), "5.0".bold()]),
|
||||||
|
)
|
||||||
|
.legend_position(Some(LegendPosition::TopLeft))
|
||||||
|
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||||
|
|
||||||
|
frame.render_widget(chart, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_scatter(frame: &mut Frame, area: Rect) {
|
||||||
|
let datasets = vec![
|
||||||
|
Dataset::default()
|
||||||
|
.name("Heavy")
|
||||||
|
.marker(Marker::Dot)
|
||||||
|
.graph_type(GraphType::Scatter)
|
||||||
|
.style(Style::new().yellow())
|
||||||
|
.data(&HEAVY_PAYLOAD_DATA),
|
||||||
|
Dataset::default()
|
||||||
|
.name("Medium".underlined())
|
||||||
|
.marker(Marker::Braille)
|
||||||
|
.graph_type(GraphType::Scatter)
|
||||||
|
.style(Style::new().magenta())
|
||||||
|
.data(&MEDIUM_PAYLOAD_DATA),
|
||||||
|
Dataset::default()
|
||||||
|
.name("Small")
|
||||||
|
.marker(Marker::Dot)
|
||||||
|
.graph_type(GraphType::Scatter)
|
||||||
|
.style(Style::new().cyan())
|
||||||
|
.data(&SMALL_PAYLOAD_DATA),
|
||||||
|
];
|
||||||
|
|
||||||
|
let chart = Chart::new(datasets)
|
||||||
|
.block(Block::bordered().title(Line::from("Scatter chart").cyan().bold().centered()))
|
||||||
|
.x_axis(
|
||||||
|
Axis::default()
|
||||||
|
.title("Year")
|
||||||
|
.bounds([1960., 2020.])
|
||||||
|
.style(Style::default().fg(Color::Gray))
|
||||||
|
.labels(["1960", "1990", "2020"]),
|
||||||
|
)
|
||||||
|
.y_axis(
|
||||||
|
Axis::default()
|
||||||
|
.title("Cost")
|
||||||
|
.bounds([0., 75000.])
|
||||||
|
.style(Style::default().fg(Color::Gray))
|
||||||
|
.labels(["0", "37 500", "75 000"]),
|
||||||
|
)
|
||||||
|
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
|
||||||
|
|
||||||
|
frame.render_widget(chart, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data from https://ourworldindata.org/space-exploration-satellites
|
||||||
|
const HEAVY_PAYLOAD_DATA: [(f64, f64); 9] = [
|
||||||
|
(1965., 8200.),
|
||||||
|
(1967., 5400.),
|
||||||
|
(1981., 65400.),
|
||||||
|
(1989., 30800.),
|
||||||
|
(1997., 10200.),
|
||||||
|
(2004., 11600.),
|
||||||
|
(2014., 4500.),
|
||||||
|
(2016., 7900.),
|
||||||
|
(2018., 1500.),
|
||||||
|
];
|
||||||
|
|
||||||
|
const MEDIUM_PAYLOAD_DATA: [(f64, f64); 29] = [
|
||||||
|
(1963., 29500.),
|
||||||
|
(1964., 30600.),
|
||||||
|
(1965., 177_900.),
|
||||||
|
(1965., 21000.),
|
||||||
|
(1966., 17900.),
|
||||||
|
(1966., 8400.),
|
||||||
|
(1975., 17500.),
|
||||||
|
(1982., 8300.),
|
||||||
|
(1985., 5100.),
|
||||||
|
(1988., 18300.),
|
||||||
|
(1990., 38800.),
|
||||||
|
(1990., 9900.),
|
||||||
|
(1991., 18700.),
|
||||||
|
(1992., 9100.),
|
||||||
|
(1994., 10500.),
|
||||||
|
(1994., 8500.),
|
||||||
|
(1994., 8700.),
|
||||||
|
(1997., 6200.),
|
||||||
|
(1999., 18000.),
|
||||||
|
(1999., 7600.),
|
||||||
|
(1999., 8900.),
|
||||||
|
(1999., 9600.),
|
||||||
|
(2000., 16000.),
|
||||||
|
(2001., 10000.),
|
||||||
|
(2002., 10400.),
|
||||||
|
(2002., 8100.),
|
||||||
|
(2010., 2600.),
|
||||||
|
(2013., 13600.),
|
||||||
|
(2017., 8000.),
|
||||||
|
];
|
||||||
|
|
||||||
|
const SMALL_PAYLOAD_DATA: [(f64, f64); 23] = [
|
||||||
|
(1961., 118_500.),
|
||||||
|
(1962., 14900.),
|
||||||
|
(1975., 21400.),
|
||||||
|
(1980., 32800.),
|
||||||
|
(1988., 31100.),
|
||||||
|
(1990., 41100.),
|
||||||
|
(1993., 23600.),
|
||||||
|
(1994., 20600.),
|
||||||
|
(1994., 34600.),
|
||||||
|
(1996., 50600.),
|
||||||
|
(1997., 19200.),
|
||||||
|
(1997., 45800.),
|
||||||
|
(1998., 19100.),
|
||||||
|
(2000., 73100.),
|
||||||
|
(2003., 11200.),
|
||||||
|
(2008., 12600.),
|
||||||
|
(2010., 30500.),
|
||||||
|
(2012., 20000.),
|
||||||
|
(2013., 10600.),
|
||||||
|
(2013., 34500.),
|
||||||
|
(2015., 10600.),
|
||||||
|
(2018., 23100.),
|
||||||
|
(2019., 17300.),
|
||||||
|
];
|
||||||
15
examples/apps/color-explorer/Cargo.toml
Normal file
15
examples/apps/color-explorer/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "color-explorer"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/color-explorer/README.md
Normal file
9
examples/apps/color-explorer/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Color explorer demo
|
||||||
|
|
||||||
|
This example shows how to handle the supported colors.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p color-explorer
|
||||||
|
```
|
||||||
241
examples/apps/color-explorer/src/main.rs
Normal file
241
examples/apps/color-explorer/src/main.rs
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
//! A Ratatui example that demonstrates how to handle colors.
|
||||||
|
//!
|
||||||
|
//! This example shows all the colors supported by Ratatui. It will render a grid of foreground
|
||||||
|
//! and background colors with their names and indexes.
|
||||||
|
//!
|
||||||
|
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||||
|
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||||
|
//!
|
||||||
|
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ratatui::{
|
||||||
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
|
layout::{Alignment, Constraint, Layout, Rect},
|
||||||
|
style::{Color, Style, Stylize},
|
||||||
|
text::Line,
|
||||||
|
widgets::{Block, Borders, Paragraph},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
terminal.draw(draw)?;
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(frame: &mut Frame) {
|
||||||
|
let layout = Layout::vertical([
|
||||||
|
Constraint::Length(30),
|
||||||
|
Constraint::Length(17),
|
||||||
|
Constraint::Length(2),
|
||||||
|
])
|
||||||
|
.split(frame.area());
|
||||||
|
|
||||||
|
render_named_colors(frame, layout[0]);
|
||||||
|
render_indexed_colors(frame, layout[1]);
|
||||||
|
render_indexed_grayscale(frame, layout[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAMED_COLORS: [Color; 16] = [
|
||||||
|
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,
|
||||||
|
];
|
||||||
|
|
||||||
|
fn render_named_colors(frame: &mut Frame, area: Rect) {
|
||||||
|
let layout = Layout::vertical([Constraint::Length(3); 10]).split(area);
|
||||||
|
|
||||||
|
render_fg_named_colors(frame, Color::Reset, layout[0]);
|
||||||
|
render_fg_named_colors(frame, Color::Black, layout[1]);
|
||||||
|
render_fg_named_colors(frame, Color::DarkGray, layout[2]);
|
||||||
|
render_fg_named_colors(frame, Color::Gray, layout[3]);
|
||||||
|
render_fg_named_colors(frame, Color::White, layout[4]);
|
||||||
|
|
||||||
|
render_bg_named_colors(frame, Color::Reset, layout[5]);
|
||||||
|
render_bg_named_colors(frame, Color::Black, layout[6]);
|
||||||
|
render_bg_named_colors(frame, Color::DarkGray, layout[7]);
|
||||||
|
render_bg_named_colors(frame, Color::Gray, layout[8]);
|
||||||
|
render_bg_named_colors(frame, Color::White, layout[9]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_fg_named_colors(frame: &mut Frame, bg: Color, area: Rect) {
|
||||||
|
let block = title_block(format!("Foreground colors on {bg} background"));
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
|
||||||
|
let areas = vertical.iter().flat_map(|area| {
|
||||||
|
Layout::horizontal([Constraint::Ratio(1, 8); 8])
|
||||||
|
.split(*area)
|
||||||
|
.to_vec()
|
||||||
|
});
|
||||||
|
for (fg, area) in NAMED_COLORS.into_iter().zip(areas) {
|
||||||
|
let color_name = fg.to_string();
|
||||||
|
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
|
||||||
|
frame.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_bg_named_colors(frame: &mut Frame, fg: Color, area: Rect) {
|
||||||
|
let block = title_block(format!("Background colors with {fg} foreground"));
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
let vertical = Layout::vertical([Constraint::Length(1); 2]).split(inner);
|
||||||
|
let areas = vertical.iter().flat_map(|area| {
|
||||||
|
Layout::horizontal([Constraint::Ratio(1, 8); 8])
|
||||||
|
.split(*area)
|
||||||
|
.to_vec()
|
||||||
|
});
|
||||||
|
for (bg, area) in NAMED_COLORS.into_iter().zip(areas) {
|
||||||
|
let color_name = bg.to_string();
|
||||||
|
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
|
||||||
|
frame.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_indexed_colors(frame: &mut Frame, area: Rect) {
|
||||||
|
let block = title_block("Indexed colors".into());
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
let layout = Layout::vertical([
|
||||||
|
Constraint::Length(1), // 0 - 15
|
||||||
|
Constraint::Length(1), // blank
|
||||||
|
Constraint::Min(6), // 16 - 123
|
||||||
|
Constraint::Length(1), // blank
|
||||||
|
Constraint::Min(6), // 124 - 231
|
||||||
|
Constraint::Length(1), // blank
|
||||||
|
])
|
||||||
|
.split(inner);
|
||||||
|
|
||||||
|
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||||
|
let color_layout = Layout::horizontal([Constraint::Length(5); 16]).split(layout[0]);
|
||||||
|
for i in 0..16 {
|
||||||
|
let color = Color::Indexed(i);
|
||||||
|
let color_index = format!("{i:0>2}");
|
||||||
|
let bg = if i < 1 { Color::DarkGray } else { Color::Black };
|
||||||
|
let paragraph = Paragraph::new(Line::from(vec![
|
||||||
|
color_index.fg(color).bg(bg),
|
||||||
|
"██".bg(color).fg(color),
|
||||||
|
]));
|
||||||
|
frame.render_widget(paragraph, color_layout[i as usize]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 16 17 18 19 20 21 52 53 54 55 56 57 88 89 90 91 92 93
|
||||||
|
// 22 23 24 25 26 27 58 59 60 61 62 63 94 95 96 97 98 99
|
||||||
|
// 28 29 30 31 32 33 64 65 66 67 68 69 100 101 102 103 104 105
|
||||||
|
// 34 35 36 37 38 39 70 71 72 73 74 75 106 107 108 109 110 111
|
||||||
|
// 40 41 42 43 44 45 76 77 78 79 80 81 112 113 114 115 116 117
|
||||||
|
// 46 47 48 49 50 51 82 83 84 85 86 87 118 119 120 121 122 123
|
||||||
|
//
|
||||||
|
// 124 125 126 127 128 129 160 161 162 163 164 165 196 197 198 199 200 201
|
||||||
|
// 130 131 132 133 134 135 166 167 168 169 170 171 202 203 204 205 206 207
|
||||||
|
// 136 137 138 139 140 141 172 173 174 175 176 177 208 209 210 211 212 213
|
||||||
|
// 142 143 144 145 146 147 178 179 180 181 182 183 214 215 216 217 218 219
|
||||||
|
// 148 149 150 151 152 153 184 185 186 187 188 189 220 221 222 223 224 225
|
||||||
|
// 154 155 156 157 158 159 190 191 192 193 194 195 226 227 228 229 230 231
|
||||||
|
|
||||||
|
// the above looks complex but it's so the colors are grouped into blocks that display nicely
|
||||||
|
let index_layout = [layout[2], layout[4]]
|
||||||
|
.iter()
|
||||||
|
// two rows of 3 columns
|
||||||
|
.flat_map(|area| {
|
||||||
|
Layout::horizontal([Constraint::Length(27); 3])
|
||||||
|
.split(*area)
|
||||||
|
.to_vec()
|
||||||
|
})
|
||||||
|
// each with 6 rows
|
||||||
|
.flat_map(|area| {
|
||||||
|
Layout::vertical([Constraint::Length(1); 6])
|
||||||
|
.split(area)
|
||||||
|
.to_vec()
|
||||||
|
})
|
||||||
|
// each with 6 columns
|
||||||
|
.flat_map(|area| {
|
||||||
|
Layout::horizontal([Constraint::Min(4); 6])
|
||||||
|
.split(area)
|
||||||
|
.to_vec()
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
for i in 16..=231 {
|
||||||
|
let color = Color::Indexed(i);
|
||||||
|
let color_index = format!("{i:0>3}");
|
||||||
|
let paragraph = Paragraph::new(Line::from(vec![
|
||||||
|
color_index.fg(color).bg(Color::Reset),
|
||||||
|
".".bg(color).fg(color),
|
||||||
|
// There's a bug in VHS that seems to bleed backgrounds into the next
|
||||||
|
// character. This is a workaround to make the bug less obvious.
|
||||||
|
"███".reversed(),
|
||||||
|
]));
|
||||||
|
frame.render_widget(paragraph, index_layout[i as usize - 16]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title_block(title: String) -> Block<'static> {
|
||||||
|
Block::new()
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.border_style(Style::new().dark_gray())
|
||||||
|
.title_style(Style::new().reset())
|
||||||
|
.title(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_indexed_grayscale(frame: &mut Frame, area: Rect) {
|
||||||
|
let layout = Layout::vertical([
|
||||||
|
Constraint::Length(1), // 232 - 243
|
||||||
|
Constraint::Length(1), // 244 - 255
|
||||||
|
])
|
||||||
|
.split(area)
|
||||||
|
.iter()
|
||||||
|
.flat_map(|area| {
|
||||||
|
Layout::horizontal([Constraint::Length(6); 12])
|
||||||
|
.split(*area)
|
||||||
|
.to_vec()
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
for i in 232..=255 {
|
||||||
|
let color = Color::Indexed(i);
|
||||||
|
let color_index = format!("{i:0>3}");
|
||||||
|
// make the dark colors easier to read
|
||||||
|
let bg = if i < 244 { Color::Gray } else { Color::Black };
|
||||||
|
let paragraph = Paragraph::new(Line::from(vec![
|
||||||
|
color_index.fg(color).bg(bg),
|
||||||
|
"██".bg(color).fg(color),
|
||||||
|
// There's a bug in VHS that seems to bleed backgrounds into the next
|
||||||
|
// character. This is a workaround to make the bug less obvious.
|
||||||
|
"███████".reversed(),
|
||||||
|
]));
|
||||||
|
frame.render_widget(paragraph, layout[i as usize - 232]);
|
||||||
|
}
|
||||||
|
}
|
||||||
15
examples/apps/colors-rgb/Cargo.toml
Normal file
15
examples/apps/colors-rgb/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "colors-rgb"
|
||||||
|
version = "0.1.0"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
palette = "0.7.6"
|
||||||
|
ratatui.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/colors-rgb/README.md
Normal file
9
examples/apps/colors-rgb/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Colors-RGB demo
|
||||||
|
|
||||||
|
This example shows the full range of RGB colors in an animation.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p colors-rgb
|
||||||
|
```
|
||||||
246
examples/apps/colors-rgb/src/main.rs
Normal file
246
examples/apps/colors-rgb/src/main.rs
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
//! A Ratatui example that shows the full range of RGB colors that can be displayed in the terminal.
|
||||||
|
//!
|
||||||
|
//! Requires a terminal that supports 24-bit color (true color) and unicode.
|
||||||
|
//!
|
||||||
|
//! This example also demonstrates how implementing the Widget trait on a mutable reference
|
||||||
|
//! allows the widget to update its state while it is being rendered. This allows the fps
|
||||||
|
//! widget to update the fps calculation and the colors widget to update a cached version of
|
||||||
|
//! the colors to render instead of recalculating them every frame.
|
||||||
|
//!
|
||||||
|
//! This is an alternative to using the `StatefulWidget` trait and a separate state struct. It
|
||||||
|
//! is useful when the state is only used by the widget and doesn't need to be shared with
|
||||||
|
//! other widgets.
|
||||||
|
//!
|
||||||
|
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||||
|
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||||
|
//!
|
||||||
|
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||||
|
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
|
layout::{Constraint, Layout, Position, Rect},
|
||||||
|
style::Color,
|
||||||
|
text::Text,
|
||||||
|
widgets::Widget,
|
||||||
|
DefaultTerminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct App {
|
||||||
|
/// The current state of the app (running or quit)
|
||||||
|
state: AppState,
|
||||||
|
|
||||||
|
/// A widget that displays the current frames per second
|
||||||
|
fps_widget: FpsWidget,
|
||||||
|
|
||||||
|
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
|
||||||
|
colors_widget: ColorsWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
enum AppState {
|
||||||
|
/// The app is running
|
||||||
|
#[default]
|
||||||
|
Running,
|
||||||
|
|
||||||
|
/// The user has requested the app to quit
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that displays the current frames per second
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FpsWidget {
|
||||||
|
/// The number of elapsed frames that have passed - used to calculate the fps
|
||||||
|
frame_count: usize,
|
||||||
|
|
||||||
|
/// The last instant that the fps was calculated
|
||||||
|
last_instant: Instant,
|
||||||
|
|
||||||
|
/// The current frames per second
|
||||||
|
fps: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that displays the full range of RGB colors that can be displayed in the terminal.
|
||||||
|
///
|
||||||
|
/// This widget is animated and will change colors over time.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ColorsWidget {
|
||||||
|
/// The colors to render - should be double the height of the area as we render two rows of
|
||||||
|
/// pixels for each row of the widget using the half block character. This is computed any time
|
||||||
|
/// the size of the widget changes.
|
||||||
|
colors: Vec<Vec<Color>>,
|
||||||
|
|
||||||
|
/// the number of elapsed frames that have passed - used to animate the colors by shifting the
|
||||||
|
/// x index by the frame number
|
||||||
|
frame_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// Run the app
|
||||||
|
///
|
||||||
|
/// This is the main event loop for the app.
|
||||||
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
while self.is_running() {
|
||||||
|
terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?;
|
||||||
|
self.handle_events()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn is_running(&self) -> bool {
|
||||||
|
matches!(self.state, AppState::Running)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle any events that have occurred since the last time the app was rendered.
|
||||||
|
///
|
||||||
|
/// Currently, this only handles the q key to quit the app.
|
||||||
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
|
// Ensure that the app only blocks for a period that allows the app to render at
|
||||||
|
// approximately 60 FPS (this doesn't account for the time to render the frame, and will
|
||||||
|
// also update the app immediately any time an event occurs)
|
||||||
|
let timeout = Duration::from_secs_f32(1.0 / 60.0);
|
||||||
|
if event::poll(timeout)? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||||
|
self.state = AppState::Quit;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement the Widget trait for &mut App so that it can be rendered
|
||||||
|
///
|
||||||
|
/// This is implemented on a mutable reference so that the app can update its state while it is
|
||||||
|
/// being rendered. This allows the fps widget to update the fps calculation and the colors widget
|
||||||
|
/// to update the colors to render.
|
||||||
|
impl Widget for &mut App {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
use Constraint::{Length, Min};
|
||||||
|
let [top, colors] = Layout::vertical([Length(1), Min(0)]).areas(area);
|
||||||
|
let [title, fps] = Layout::horizontal([Min(0), Length(8)]).areas(top);
|
||||||
|
Text::from("colors_rgb example. Press q to quit")
|
||||||
|
.centered()
|
||||||
|
.render(title, buf);
|
||||||
|
self.fps_widget.render(fps, buf);
|
||||||
|
self.colors_widget.render(colors, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default impl for `FpsWidget`
|
||||||
|
///
|
||||||
|
/// Manual impl is required because we need to initialize the `last_instant` field to the current
|
||||||
|
/// instant.
|
||||||
|
impl Default for FpsWidget {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
frame_count: 0,
|
||||||
|
last_instant: Instant::now(),
|
||||||
|
fps: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget impl for `FpsWidget`
|
||||||
|
///
|
||||||
|
/// This is implemented on a mutable reference so that we can update the frame count and fps
|
||||||
|
/// calculation while rendering.
|
||||||
|
impl Widget for &mut FpsWidget {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
self.calculate_fps();
|
||||||
|
if let Some(fps) = self.fps {
|
||||||
|
let text = format!("{fps:.1} fps");
|
||||||
|
Text::from(text).render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FpsWidget {
|
||||||
|
/// Update the fps calculation.
|
||||||
|
///
|
||||||
|
/// This updates the fps once a second, but only if the widget has rendered at least 2 frames
|
||||||
|
/// since the last calculation. This avoids noise in the fps calculation when rendering on slow
|
||||||
|
/// machines that can't render at least 2 frames per second.
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
fn calculate_fps(&mut self) {
|
||||||
|
self.frame_count += 1;
|
||||||
|
let elapsed = self.last_instant.elapsed();
|
||||||
|
if elapsed > Duration::from_secs(1) && self.frame_count > 2 {
|
||||||
|
self.fps = Some(self.frame_count as f32 / elapsed.as_secs_f32());
|
||||||
|
self.frame_count = 0;
|
||||||
|
self.last_instant = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Widget impl for `ColorsWidget`
|
||||||
|
///
|
||||||
|
/// This is implemented on a mutable reference so that we can update the frame count and store a
|
||||||
|
/// cached version of the colors to render instead of recalculating them every frame.
|
||||||
|
impl Widget for &mut ColorsWidget {
|
||||||
|
/// Render the widget
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
self.setup_colors(area);
|
||||||
|
let colors = &self.colors;
|
||||||
|
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||||
|
// animate the colors by shifting the x index by the frame number
|
||||||
|
let xi = (xi + self.frame_count) % (area.width as usize);
|
||||||
|
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||||
|
// render a half block character for each row of pixels with the foreground color
|
||||||
|
// set to the color of the pixel and the background color set to the color of the
|
||||||
|
// pixel below it
|
||||||
|
let fg = colors[yi * 2][xi];
|
||||||
|
let bg = colors[yi * 2 + 1][xi];
|
||||||
|
buf[Position::new(x, y)].set_char('▀').set_fg(fg).set_bg(bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.frame_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorsWidget {
|
||||||
|
/// Setup the colors to render.
|
||||||
|
///
|
||||||
|
/// This is called once per frame to setup the colors to render. It caches the colors so that
|
||||||
|
/// they don't need to be recalculated every frame.
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
fn setup_colors(&mut self, size: Rect) {
|
||||||
|
let Rect { width, height, .. } = size;
|
||||||
|
// double the height because each screen row has two rows of half block pixels
|
||||||
|
let height = height as usize * 2;
|
||||||
|
let width = width as usize;
|
||||||
|
// only update the colors if the size has changed since the last time we rendered
|
||||||
|
if self.colors.len() == height && self.colors[0].len() == width {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.colors = Vec::with_capacity(height);
|
||||||
|
for y in 0..height {
|
||||||
|
let mut row = Vec::with_capacity(width);
|
||||||
|
for x in 0..width {
|
||||||
|
let hue = x as f32 * 360.0 / width as f32;
|
||||||
|
let value = (height - y) as f32 / height as f32;
|
||||||
|
let saturation = Okhsv::max_saturation();
|
||||||
|
let color = Okhsv::new(hue, saturation, value);
|
||||||
|
let color = Srgb::<f32>::from_color_unclamped(color);
|
||||||
|
let color: Srgb<u8> = color.into_format();
|
||||||
|
let color = Color::Rgb(color.red, color.green, color.blue);
|
||||||
|
row.push(color);
|
||||||
|
}
|
||||||
|
self.colors.push(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
examples/apps/constraint-explorer/Cargo.toml
Normal file
16
examples/apps/constraint-explorer/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "constraint-explorer"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/constraint-explorer/README.md
Normal file
9
examples/apps/constraint-explorer/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Constraint explorer demo
|
||||||
|
|
||||||
|
This interactive example shows how different constraints can be used to layout widgets.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p constraint-explorer
|
||||||
|
```
|
||||||
601
examples/apps/constraint-explorer/src/main.rs
Normal file
601
examples/apps/constraint-explorer/src/main.rs
Normal file
@@ -0,0 +1,601 @@
|
|||||||
|
/// A Ratatui example that demonstrates how different layout constraints work.
|
||||||
|
///
|
||||||
|
/// It also supports swapping constraints, adding and removing blocks, and changing the spacing
|
||||||
|
/// between blocks.
|
||||||
|
///
|
||||||
|
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||||
|
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||||
|
/// release.
|
||||||
|
///
|
||||||
|
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||||
|
use color_eyre::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
|
layout::{
|
||||||
|
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||||
|
Flex, Layout, Rect,
|
||||||
|
},
|
||||||
|
style::{
|
||||||
|
palette::tailwind::{BLUE, SKY, SLATE, STONE},
|
||||||
|
Color, Style, Stylize,
|
||||||
|
},
|
||||||
|
symbols::{self, line},
|
||||||
|
text::{Line, Span, Text},
|
||||||
|
widgets::{Block, Paragraph, Widget, Wrap},
|
||||||
|
DefaultTerminal,
|
||||||
|
};
|
||||||
|
use strum::{Display, EnumIter, FromRepr};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct App {
|
||||||
|
mode: AppMode,
|
||||||
|
spacing: u16,
|
||||||
|
constraints: Vec<Constraint>,
|
||||||
|
selected_index: usize,
|
||||||
|
value: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq)]
|
||||||
|
enum AppMode {
|
||||||
|
#[default]
|
||||||
|
Running,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A variant of [`Constraint`] that can be rendered as a tab.
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, EnumIter, FromRepr, Display)]
|
||||||
|
enum ConstraintName {
|
||||||
|
#[default]
|
||||||
|
Length,
|
||||||
|
Percentage,
|
||||||
|
Ratio,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
Fill,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that renders a [`Constraint`] as a block. E.g.:
|
||||||
|
/// ```plain
|
||||||
|
/// ┌──────────────┐
|
||||||
|
/// │ Length(16) │
|
||||||
|
/// │ 16px │
|
||||||
|
/// └──────────────┘
|
||||||
|
/// ```
|
||||||
|
struct ConstraintBlock {
|
||||||
|
constraint: Constraint,
|
||||||
|
legend: bool,
|
||||||
|
selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A widget that renders a spacer with a label indicating the width of the spacer. E.g.:
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
/// ┌ ┐
|
||||||
|
/// 8 px
|
||||||
|
/// └ ┘
|
||||||
|
/// ```
|
||||||
|
struct SpacerBlock;
|
||||||
|
|
||||||
|
// App behaviour
|
||||||
|
impl App {
|
||||||
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
self.insert_test_defaults();
|
||||||
|
|
||||||
|
while self.is_running() {
|
||||||
|
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||||
|
self.handle_events()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove these - these are just for testing
|
||||||
|
fn insert_test_defaults(&mut self) {
|
||||||
|
self.constraints = vec![
|
||||||
|
Constraint::Length(20),
|
||||||
|
Constraint::Length(20),
|
||||||
|
Constraint::Length(20),
|
||||||
|
];
|
||||||
|
self.value = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_running(&self) -> bool {
|
||||||
|
self.mode == AppMode::Running
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => self.exit(),
|
||||||
|
KeyCode::Char('1') => self.swap_constraint(ConstraintName::Min),
|
||||||
|
KeyCode::Char('2') => self.swap_constraint(ConstraintName::Max),
|
||||||
|
KeyCode::Char('3') => self.swap_constraint(ConstraintName::Length),
|
||||||
|
KeyCode::Char('4') => self.swap_constraint(ConstraintName::Percentage),
|
||||||
|
KeyCode::Char('5') => self.swap_constraint(ConstraintName::Ratio),
|
||||||
|
KeyCode::Char('6') => self.swap_constraint(ConstraintName::Fill),
|
||||||
|
KeyCode::Char('+') => self.increment_spacing(),
|
||||||
|
KeyCode::Char('-') => self.decrement_spacing(),
|
||||||
|
KeyCode::Char('x') => self.delete_block(),
|
||||||
|
KeyCode::Char('a') => self.insert_block(),
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => self.increment_value(),
|
||||||
|
KeyCode::Char('j') | KeyCode::Down => self.decrement_value(),
|
||||||
|
KeyCode::Char('h') | KeyCode::Left => self.prev_block(),
|
||||||
|
KeyCode::Char('l') | KeyCode::Right => self.next_block(),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_value(&mut self) {
|
||||||
|
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match constraint {
|
||||||
|
Constraint::Length(v)
|
||||||
|
| Constraint::Min(v)
|
||||||
|
| Constraint::Max(v)
|
||||||
|
| Constraint::Fill(v)
|
||||||
|
| Constraint::Percentage(v) => *v = v.saturating_add(1),
|
||||||
|
Constraint::Ratio(_n, d) => *d = d.saturating_add(1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_value(&mut self) {
|
||||||
|
let Some(constraint) = self.constraints.get_mut(self.selected_index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match constraint {
|
||||||
|
Constraint::Length(v)
|
||||||
|
| Constraint::Min(v)
|
||||||
|
| Constraint::Max(v)
|
||||||
|
| Constraint::Fill(v)
|
||||||
|
| Constraint::Percentage(v) => *v = v.saturating_sub(1),
|
||||||
|
Constraint::Ratio(_n, d) => *d = d.saturating_sub(1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// select the next block with wrap around
|
||||||
|
fn next_block(&mut self) {
|
||||||
|
if self.constraints.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let len = self.constraints.len();
|
||||||
|
self.selected_index = (self.selected_index + 1) % len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// select the previous block with wrap around
|
||||||
|
fn prev_block(&mut self) {
|
||||||
|
if self.constraints.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let len = self.constraints.len();
|
||||||
|
self.selected_index = (self.selected_index + self.constraints.len() - 1) % len;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// delete the selected block
|
||||||
|
fn delete_block(&mut self) {
|
||||||
|
if self.constraints.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.constraints.remove(self.selected_index);
|
||||||
|
self.selected_index = self.selected_index.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// insert a block after the selected block
|
||||||
|
fn insert_block(&mut self) {
|
||||||
|
let index = self
|
||||||
|
.selected_index
|
||||||
|
.saturating_add(1)
|
||||||
|
.min(self.constraints.len());
|
||||||
|
let constraint = Constraint::Length(self.value);
|
||||||
|
self.constraints.insert(index, constraint);
|
||||||
|
self.selected_index = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_spacing(&mut self) {
|
||||||
|
self.spacing = self.spacing.saturating_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_spacing(&mut self) {
|
||||||
|
self.spacing = self.spacing.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exit(&mut self) {
|
||||||
|
self.mode = AppMode::Quit;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn swap_constraint(&mut self, name: ConstraintName) {
|
||||||
|
if self.constraints.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let constraint = match name {
|
||||||
|
ConstraintName::Length => Length(self.value),
|
||||||
|
ConstraintName::Percentage => Percentage(self.value),
|
||||||
|
ConstraintName::Min => Min(self.value),
|
||||||
|
ConstraintName::Max => Max(self.value),
|
||||||
|
ConstraintName::Fill => Fill(self.value),
|
||||||
|
ConstraintName::Ratio => Ratio(1, u32::from(self.value) / 4), // for balance
|
||||||
|
};
|
||||||
|
self.constraints[self.selected_index] = constraint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Constraint> for ConstraintName {
|
||||||
|
fn from(constraint: Constraint) -> Self {
|
||||||
|
match constraint {
|
||||||
|
Length(_) => Self::Length,
|
||||||
|
Percentage(_) => Self::Percentage,
|
||||||
|
Ratio(_, _) => Self::Ratio,
|
||||||
|
Min(_) => Self::Min,
|
||||||
|
Max(_) => Self::Max,
|
||||||
|
Fill(_) => Self::Fill,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for &App {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let [header_area, instructions_area, swap_legend_area, _, blocks_area] =
|
||||||
|
Layout::vertical([
|
||||||
|
Length(2), // header
|
||||||
|
Length(2), // instructions
|
||||||
|
Length(1), // swap key legend
|
||||||
|
Length(1), // gap
|
||||||
|
Fill(1), // blocks
|
||||||
|
])
|
||||||
|
.areas(area);
|
||||||
|
|
||||||
|
App::header().render(header_area, buf);
|
||||||
|
App::instructions().render(instructions_area, buf);
|
||||||
|
App::swap_legend().render(swap_legend_area, buf);
|
||||||
|
self.render_layout_blocks(blocks_area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// App rendering
|
||||||
|
impl App {
|
||||||
|
const HEADER_COLOR: Color = SLATE.c200;
|
||||||
|
const TEXT_COLOR: Color = SLATE.c400;
|
||||||
|
const AXIS_COLOR: Color = SLATE.c500;
|
||||||
|
|
||||||
|
fn header() -> impl Widget {
|
||||||
|
let text = "Constraint Explorer";
|
||||||
|
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn instructions() -> impl Widget {
|
||||||
|
let text = "◄ ►: select, ▲ ▼: edit, 1-6: swap, a: add, x: delete, q: quit, + -: spacing";
|
||||||
|
Paragraph::new(text)
|
||||||
|
.fg(Self::TEXT_COLOR)
|
||||||
|
.centered()
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn swap_legend() -> impl Widget {
|
||||||
|
#[allow(unstable_name_collisions)]
|
||||||
|
Paragraph::new(
|
||||||
|
Line::from(
|
||||||
|
[
|
||||||
|
ConstraintName::Min,
|
||||||
|
ConstraintName::Max,
|
||||||
|
ConstraintName::Length,
|
||||||
|
ConstraintName::Percentage,
|
||||||
|
ConstraintName::Ratio,
|
||||||
|
ConstraintName::Fill,
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, name)| {
|
||||||
|
format!(" {i}: {name} ", i = i + 1)
|
||||||
|
.fg(SLATE.c200)
|
||||||
|
.bg(name.color())
|
||||||
|
})
|
||||||
|
.intersperse(Span::from(" "))
|
||||||
|
.collect_vec(),
|
||||||
|
)
|
||||||
|
.centered(),
|
||||||
|
)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A bar like `<----- 80 px (gap: 2 px) ----->`
|
||||||
|
///
|
||||||
|
/// Only shows the gap when spacing is not zero
|
||||||
|
fn axis(&self, width: u16) -> impl Widget {
|
||||||
|
let label = if self.spacing != 0 {
|
||||||
|
format!("{} px (gap: {} px)", width, self.spacing)
|
||||||
|
} else {
|
||||||
|
format!("{width} px")
|
||||||
|
};
|
||||||
|
let bar_width = width.saturating_sub(2) as usize; // we want to `<` and `>` at the ends
|
||||||
|
let width_bar = format!("<{label:-^bar_width$}>");
|
||||||
|
Paragraph::new(width_bar).fg(Self::AXIS_COLOR).centered()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_layout_blocks(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let [user_constraints, area] = Layout::vertical([Length(3), Fill(1)])
|
||||||
|
.spacing(1)
|
||||||
|
.areas(area);
|
||||||
|
|
||||||
|
self.render_user_constraints_legend(user_constraints, buf);
|
||||||
|
|
||||||
|
let [start, center, end, space_around, space_between] =
|
||||||
|
Layout::vertical([Length(7); 5]).areas(area);
|
||||||
|
|
||||||
|
self.render_layout_block(Flex::Start, start, buf);
|
||||||
|
self.render_layout_block(Flex::Center, center, buf);
|
||||||
|
self.render_layout_block(Flex::End, end, buf);
|
||||||
|
self.render_layout_block(Flex::SpaceAround, space_around, buf);
|
||||||
|
self.render_layout_block(Flex::SpaceBetween, space_between, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_user_constraints_legend(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let constraints = self.constraints.iter().map(|_| Constraint::Fill(1));
|
||||||
|
let blocks = Layout::horizontal(constraints).split(area);
|
||||||
|
|
||||||
|
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
|
||||||
|
let selected = self.selected_index == i;
|
||||||
|
ConstraintBlock::new(*constraint, selected, true).render(*area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_layout_block(&self, flex: Flex, area: Rect, buf: &mut Buffer) {
|
||||||
|
let [label_area, axis_area, blocks_area] =
|
||||||
|
Layout::vertical([Length(1), Max(1), Length(4)]).areas(area);
|
||||||
|
|
||||||
|
if label_area.height > 0 {
|
||||||
|
format!("Flex::{flex:?}").bold().render(label_area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.axis(area.width).render(axis_area, buf);
|
||||||
|
|
||||||
|
let (blocks, spacers) = Layout::horizontal(&self.constraints)
|
||||||
|
.flex(flex)
|
||||||
|
.spacing(self.spacing)
|
||||||
|
.split_with_spacers(blocks_area);
|
||||||
|
|
||||||
|
for (i, (area, constraint)) in blocks.iter().zip(self.constraints.iter()).enumerate() {
|
||||||
|
let selected = self.selected_index == i;
|
||||||
|
ConstraintBlock::new(*constraint, selected, false).render(*area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
for area in spacers.iter() {
|
||||||
|
SpacerBlock.render(*area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for ConstraintBlock {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
match area.height {
|
||||||
|
1 => self.render_1px(area, buf),
|
||||||
|
2 => self.render_2px(area, buf),
|
||||||
|
_ => self.render_4px(area, buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConstraintBlock {
|
||||||
|
const TEXT_COLOR: Color = SLATE.c200;
|
||||||
|
|
||||||
|
const fn new(constraint: Constraint, selected: bool, legend: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
constraint,
|
||||||
|
legend,
|
||||||
|
selected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label(&self, width: u16) -> String {
|
||||||
|
let long_width = format!("{width} px");
|
||||||
|
let short_width = format!("{width}");
|
||||||
|
// border takes up 2 columns
|
||||||
|
let available_space = width.saturating_sub(2) as usize;
|
||||||
|
let width_label = if long_width.len() < available_space {
|
||||||
|
long_width
|
||||||
|
} else if short_width.len() < available_space {
|
||||||
|
short_width
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
format!("{}\n{}", self.constraint, width_label)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_1px(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
||||||
|
let main_color = ConstraintName::from(self.constraint).color();
|
||||||
|
let selected_color = if self.selected {
|
||||||
|
lighter_color
|
||||||
|
} else {
|
||||||
|
main_color
|
||||||
|
};
|
||||||
|
Block::new()
|
||||||
|
.fg(Self::TEXT_COLOR)
|
||||||
|
.bg(selected_color)
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_2px(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
||||||
|
let main_color = ConstraintName::from(self.constraint).color();
|
||||||
|
let selected_color = if self.selected {
|
||||||
|
lighter_color
|
||||||
|
} else {
|
||||||
|
main_color
|
||||||
|
};
|
||||||
|
Block::bordered()
|
||||||
|
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||||
|
.border_style(Style::reset().fg(selected_color).reversed())
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_4px(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let lighter_color = ConstraintName::from(self.constraint).lighter_color();
|
||||||
|
let main_color = ConstraintName::from(self.constraint).color();
|
||||||
|
let selected_color = if self.selected {
|
||||||
|
lighter_color
|
||||||
|
} else {
|
||||||
|
main_color
|
||||||
|
};
|
||||||
|
let color = if self.legend {
|
||||||
|
selected_color
|
||||||
|
} else {
|
||||||
|
main_color
|
||||||
|
};
|
||||||
|
let label = self.label(area.width);
|
||||||
|
let block = Block::bordered()
|
||||||
|
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||||
|
.border_style(Style::reset().fg(color).reversed())
|
||||||
|
.fg(Self::TEXT_COLOR)
|
||||||
|
.bg(color);
|
||||||
|
Paragraph::new(label)
|
||||||
|
.centered()
|
||||||
|
.fg(Self::TEXT_COLOR)
|
||||||
|
.bg(color)
|
||||||
|
.block(block)
|
||||||
|
.render(area, buf);
|
||||||
|
|
||||||
|
if !self.legend {
|
||||||
|
let border_color = if self.selected {
|
||||||
|
lighter_color
|
||||||
|
} else {
|
||||||
|
main_color
|
||||||
|
};
|
||||||
|
if let Some(last_row) = area.rows().last() {
|
||||||
|
buf.set_style(last_row, border_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for SpacerBlock {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
match area.height {
|
||||||
|
1 => (),
|
||||||
|
2 => Self::render_2px(area, buf),
|
||||||
|
3 => Self::render_3px(area, buf),
|
||||||
|
_ => Self::render_4px(area, buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpacerBlock {
|
||||||
|
const TEXT_COLOR: Color = SLATE.c500;
|
||||||
|
const BORDER_COLOR: Color = SLATE.c600;
|
||||||
|
|
||||||
|
/// A block with a corner borders
|
||||||
|
fn block() -> impl Widget {
|
||||||
|
let corners_only = symbols::border::Set {
|
||||||
|
top_left: line::NORMAL.top_left,
|
||||||
|
top_right: line::NORMAL.top_right,
|
||||||
|
bottom_left: line::NORMAL.bottom_left,
|
||||||
|
bottom_right: line::NORMAL.bottom_right,
|
||||||
|
vertical_left: " ",
|
||||||
|
vertical_right: " ",
|
||||||
|
horizontal_top: " ",
|
||||||
|
horizontal_bottom: " ",
|
||||||
|
};
|
||||||
|
Block::bordered()
|
||||||
|
.border_set(corners_only)
|
||||||
|
.border_style(Self::BORDER_COLOR)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A vertical line used if there is not enough space to render the block
|
||||||
|
fn line() -> impl Widget {
|
||||||
|
Paragraph::new(Text::from(vec![
|
||||||
|
Line::from(""),
|
||||||
|
Line::from("│"),
|
||||||
|
Line::from("│"),
|
||||||
|
Line::from(""),
|
||||||
|
]))
|
||||||
|
.style(Self::BORDER_COLOR)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A label that says "Spacer" if there is enough space
|
||||||
|
fn spacer_label(width: u16) -> impl Widget {
|
||||||
|
let label = if width >= 6 { "Spacer" } else { "" };
|
||||||
|
label.fg(Self::TEXT_COLOR).into_centered_line()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A label that says "8 px" if there is enough space
|
||||||
|
fn label(width: u16) -> impl Widget {
|
||||||
|
let long_label = format!("{width} px");
|
||||||
|
let short_label = format!("{width}");
|
||||||
|
let label = if long_label.len() < width as usize {
|
||||||
|
long_label
|
||||||
|
} else if short_label.len() < width as usize {
|
||||||
|
short_label
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
Line::styled(label, Self::TEXT_COLOR).centered()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_2px(area: Rect, buf: &mut Buffer) {
|
||||||
|
if area.width > 1 {
|
||||||
|
Self::block().render(area, buf);
|
||||||
|
} else {
|
||||||
|
Self::line().render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_3px(area: Rect, buf: &mut Buffer) {
|
||||||
|
if area.width > 1 {
|
||||||
|
Self::block().render(area, buf);
|
||||||
|
} else {
|
||||||
|
Self::line().render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = area.rows().nth(1).unwrap_or_default();
|
||||||
|
Self::spacer_label(area.width).render(row, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_4px(area: Rect, buf: &mut Buffer) {
|
||||||
|
if area.width > 1 {
|
||||||
|
Self::block().render(area, buf);
|
||||||
|
} else {
|
||||||
|
Self::line().render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
let row = area.rows().nth(1).unwrap_or_default();
|
||||||
|
Self::spacer_label(area.width).render(row, buf);
|
||||||
|
|
||||||
|
let row = area.rows().nth(2).unwrap_or_default();
|
||||||
|
Self::label(area.width).render(row, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConstraintName {
|
||||||
|
const fn color(self) -> Color {
|
||||||
|
match self {
|
||||||
|
Self::Length => SLATE.c700,
|
||||||
|
Self::Percentage => SLATE.c800,
|
||||||
|
Self::Ratio => SLATE.c900,
|
||||||
|
Self::Fill => SLATE.c950,
|
||||||
|
Self::Min => BLUE.c800,
|
||||||
|
Self::Max => BLUE.c900,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn lighter_color(self) -> Color {
|
||||||
|
match self {
|
||||||
|
Self::Length => STONE.c500,
|
||||||
|
Self::Percentage => STONE.c600,
|
||||||
|
Self::Ratio => STONE.c700,
|
||||||
|
Self::Fill => STONE.c800,
|
||||||
|
Self::Min => SKY.c600,
|
||||||
|
Self::Max => SKY.c700,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
examples/apps/constraints/Cargo.toml
Normal file
15
examples/apps/constraints/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "constraints"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/constraints/README.md
Normal file
9
examples/apps/constraints/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Constraints demo
|
||||||
|
|
||||||
|
This example shows different types of constraints.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p constraints
|
||||||
|
```
|
||||||
397
examples/apps/constraints/src/main.rs
Normal file
397
examples/apps/constraints/src/main.rs
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
/// A Ratatui example that demonstrates different types of constraints.
|
||||||
|
///
|
||||||
|
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||||
|
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||||
|
/// release.
|
||||||
|
///
|
||||||
|
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||||
|
use color_eyre::Result;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||||
|
layout::{
|
||||||
|
Constraint::{self, Fill, Length, Max, Min, Percentage, Ratio},
|
||||||
|
Layout, Rect,
|
||||||
|
},
|
||||||
|
style::{palette::tailwind, Color, Modifier, Style, Stylize},
|
||||||
|
symbols,
|
||||||
|
text::Line,
|
||||||
|
widgets::{
|
||||||
|
Block, Padding, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
|
||||||
|
Tabs, Widget,
|
||||||
|
},
|
||||||
|
DefaultTerminal,
|
||||||
|
};
|
||||||
|
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||||
|
|
||||||
|
const SPACER_HEIGHT: u16 = 0;
|
||||||
|
const ILLUSTRATION_HEIGHT: u16 = 4;
|
||||||
|
const EXAMPLE_HEIGHT: u16 = ILLUSTRATION_HEIGHT + SPACER_HEIGHT;
|
||||||
|
|
||||||
|
// priority 2
|
||||||
|
const MIN_COLOR: Color = tailwind::BLUE.c900;
|
||||||
|
const MAX_COLOR: Color = tailwind::BLUE.c800;
|
||||||
|
// priority 3
|
||||||
|
const LENGTH_COLOR: Color = tailwind::SLATE.c700;
|
||||||
|
const PERCENTAGE_COLOR: Color = tailwind::SLATE.c800;
|
||||||
|
const RATIO_COLOR: Color = tailwind::SLATE.c900;
|
||||||
|
// priority 4
|
||||||
|
const FILL_COLOR: Color = tailwind::SLATE.c950;
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Copy)]
|
||||||
|
struct App {
|
||||||
|
selected_tab: SelectedTab,
|
||||||
|
scroll_offset: u16,
|
||||||
|
max_scroll_offset: u16,
|
||||||
|
state: AppState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tabs for the different examples
|
||||||
|
///
|
||||||
|
/// The order of the variants is the order in which they are displayed.
|
||||||
|
#[derive(Default, Debug, Copy, Clone, Display, FromRepr, EnumIter, PartialEq, Eq)]
|
||||||
|
enum SelectedTab {
|
||||||
|
#[default]
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
Length,
|
||||||
|
Percentage,
|
||||||
|
Ratio,
|
||||||
|
Fill,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum AppState {
|
||||||
|
#[default]
|
||||||
|
Running,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
self.update_max_scroll_offset();
|
||||||
|
while self.is_running() {
|
||||||
|
terminal.draw(|frame| frame.render_widget(self, frame.area()))?;
|
||||||
|
self.handle_events()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_max_scroll_offset(&mut self) {
|
||||||
|
self.max_scroll_offset = (self.selected_tab.get_example_count() - 1) * EXAMPLE_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_running(self) -> bool {
|
||||||
|
self.state == AppState::Running
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind != KeyEventKind::Press {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||||
|
KeyCode::Char('l') | KeyCode::Right => self.next(),
|
||||||
|
KeyCode::Char('h') | KeyCode::Left => self.previous(),
|
||||||
|
KeyCode::Char('j') | KeyCode::Down => self.down(),
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => self.up(),
|
||||||
|
KeyCode::Char('g') | KeyCode::Home => self.top(),
|
||||||
|
KeyCode::Char('G') | KeyCode::End => self.bottom(),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quit(&mut self) {
|
||||||
|
self.state = AppState::Quit;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&mut self) {
|
||||||
|
self.selected_tab = self.selected_tab.next();
|
||||||
|
self.update_max_scroll_offset();
|
||||||
|
self.scroll_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn previous(&mut self) {
|
||||||
|
self.selected_tab = self.selected_tab.previous();
|
||||||
|
self.update_max_scroll_offset();
|
||||||
|
self.scroll_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn up(&mut self) {
|
||||||
|
self.scroll_offset = self.scroll_offset.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down(&mut self) {
|
||||||
|
self.scroll_offset = self
|
||||||
|
.scroll_offset
|
||||||
|
.saturating_add(1)
|
||||||
|
.min(self.max_scroll_offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn top(&mut self) {
|
||||||
|
self.scroll_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bottom(&mut self) {
|
||||||
|
self.scroll_offset = self.max_scroll_offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for App {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let [tabs, axis, demo] = Layout::vertical([Length(3), Length(3), Fill(0)]).areas(area);
|
||||||
|
|
||||||
|
self.render_tabs(tabs, buf);
|
||||||
|
Self::render_axis(axis, buf);
|
||||||
|
self.render_demo(demo, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn render_tabs(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let titles = SelectedTab::iter().map(SelectedTab::to_tab_title);
|
||||||
|
let block = Block::new()
|
||||||
|
.title("Constraints ".bold())
|
||||||
|
.title(" Use h l or ◄ ► to change tab and j k or ▲ ▼ to scroll");
|
||||||
|
Tabs::new(titles)
|
||||||
|
.block(block)
|
||||||
|
.highlight_style(Modifier::REVERSED)
|
||||||
|
.select(self.selected_tab as usize)
|
||||||
|
.padding("", "")
|
||||||
|
.divider(" ")
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_axis(area: Rect, buf: &mut Buffer) {
|
||||||
|
let width = area.width as usize;
|
||||||
|
// a bar like `<----- 80 px ----->`
|
||||||
|
let width_label = format!("{width} px");
|
||||||
|
let width_bar = format!(
|
||||||
|
"<{width_label:-^width$}>",
|
||||||
|
width = width - width_label.len() / 2
|
||||||
|
);
|
||||||
|
Paragraph::new(width_bar.dark_gray())
|
||||||
|
.centered()
|
||||||
|
.block(Block::new().padding(Padding {
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 1,
|
||||||
|
bottom: 0,
|
||||||
|
}))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the demo content
|
||||||
|
///
|
||||||
|
/// This function renders the demo content into a separate buffer and then splices the buffer
|
||||||
|
/// into the main buffer. This is done to make it possible to handle scrolling easily.
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
fn render_demo(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
// render demo content into a separate buffer so all examples fit we add an extra
|
||||||
|
// area.height to make sure the last example is fully visible even when the scroll offset is
|
||||||
|
// at the max
|
||||||
|
let height = self.selected_tab.get_example_count() * EXAMPLE_HEIGHT;
|
||||||
|
let demo_area = Rect::new(0, 0, area.width, height + area.height);
|
||||||
|
let mut demo_buf = Buffer::empty(demo_area);
|
||||||
|
|
||||||
|
let scrollbar_needed = self.scroll_offset != 0 || height > area.height;
|
||||||
|
let content_area = if scrollbar_needed {
|
||||||
|
Rect {
|
||||||
|
width: demo_area.width - 1,
|
||||||
|
..demo_area
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
demo_area
|
||||||
|
};
|
||||||
|
self.selected_tab.render(content_area, &mut demo_buf);
|
||||||
|
|
||||||
|
let visible_content = demo_buf
|
||||||
|
.content
|
||||||
|
.into_iter()
|
||||||
|
.skip((demo_area.width * self.scroll_offset) as usize)
|
||||||
|
.take(area.area() as usize);
|
||||||
|
for (i, cell) in visible_content.enumerate() {
|
||||||
|
let x = i as u16 % area.width;
|
||||||
|
let y = i as u16 / area.width;
|
||||||
|
buf[(area.x + x, area.y + y)] = cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
if scrollbar_needed {
|
||||||
|
let mut state = ScrollbarState::new(self.max_scroll_offset as usize)
|
||||||
|
.position(self.scroll_offset as usize);
|
||||||
|
Scrollbar::new(ScrollbarOrientation::VerticalRight).render(area, buf, &mut state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectedTab {
|
||||||
|
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||||
|
fn previous(self) -> Self {
|
||||||
|
let current_index: usize = self as usize;
|
||||||
|
let previous_index = current_index.saturating_sub(1);
|
||||||
|
Self::from_repr(previous_index).unwrap_or(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the next tab, if there is no next tab return the current tab.
|
||||||
|
fn next(self) -> Self {
|
||||||
|
let current_index = self as usize;
|
||||||
|
let next_index = current_index.saturating_add(1);
|
||||||
|
Self::from_repr(next_index).unwrap_or(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn get_example_count(self) -> u16 {
|
||||||
|
#[allow(clippy::match_same_arms)]
|
||||||
|
match self {
|
||||||
|
Self::Length => 4,
|
||||||
|
Self::Percentage => 5,
|
||||||
|
Self::Ratio => 4,
|
||||||
|
Self::Fill => 2,
|
||||||
|
Self::Min => 5,
|
||||||
|
Self::Max => 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_tab_title(value: Self) -> Line<'static> {
|
||||||
|
let text = format!(" {value} ");
|
||||||
|
let color = match value {
|
||||||
|
Self::Length => LENGTH_COLOR,
|
||||||
|
Self::Percentage => PERCENTAGE_COLOR,
|
||||||
|
Self::Ratio => RATIO_COLOR,
|
||||||
|
Self::Fill => FILL_COLOR,
|
||||||
|
Self::Min => MIN_COLOR,
|
||||||
|
Self::Max => MAX_COLOR,
|
||||||
|
};
|
||||||
|
text.fg(tailwind::SLATE.c200).bg(color).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for SelectedTab {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
match self {
|
||||||
|
Self::Length => Self::render_length_example(area, buf),
|
||||||
|
Self::Percentage => Self::render_percentage_example(area, buf),
|
||||||
|
Self::Ratio => Self::render_ratio_example(area, buf),
|
||||||
|
Self::Fill => Self::render_fill_example(area, buf),
|
||||||
|
Self::Min => Self::render_min_example(area, buf),
|
||||||
|
Self::Max => Self::render_max_example(area, buf),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectedTab {
|
||||||
|
fn render_length_example(area: Rect, buf: &mut Buffer) {
|
||||||
|
let [example1, example2, example3, _] =
|
||||||
|
Layout::vertical([Length(EXAMPLE_HEIGHT); 4]).areas(area);
|
||||||
|
|
||||||
|
Example::new(&[Length(20), Length(20)]).render(example1, buf);
|
||||||
|
Example::new(&[Length(20), Min(20)]).render(example2, buf);
|
||||||
|
Example::new(&[Length(20), Max(20)]).render(example3, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_percentage_example(area: Rect, buf: &mut Buffer) {
|
||||||
|
let [example1, example2, example3, example4, example5, _] =
|
||||||
|
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||||
|
|
||||||
|
Example::new(&[Percentage(75), Fill(0)]).render(example1, buf);
|
||||||
|
Example::new(&[Percentage(25), Fill(0)]).render(example2, buf);
|
||||||
|
Example::new(&[Percentage(50), Min(20)]).render(example3, buf);
|
||||||
|
Example::new(&[Percentage(0), Max(0)]).render(example4, buf);
|
||||||
|
Example::new(&[Percentage(0), Fill(0)]).render(example5, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_ratio_example(area: Rect, buf: &mut Buffer) {
|
||||||
|
let [example1, example2, example3, example4, _] =
|
||||||
|
Layout::vertical([Length(EXAMPLE_HEIGHT); 5]).areas(area);
|
||||||
|
|
||||||
|
Example::new(&[Ratio(1, 2); 2]).render(example1, buf);
|
||||||
|
Example::new(&[Ratio(1, 4); 4]).render(example2, buf);
|
||||||
|
Example::new(&[Ratio(1, 2), Ratio(1, 3), Ratio(1, 4)]).render(example3, buf);
|
||||||
|
Example::new(&[Ratio(1, 2), Percentage(25), Length(10)]).render(example4, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_fill_example(area: Rect, buf: &mut Buffer) {
|
||||||
|
let [example1, example2, _] = Layout::vertical([Length(EXAMPLE_HEIGHT); 3]).areas(area);
|
||||||
|
|
||||||
|
Example::new(&[Fill(1), Fill(2), Fill(3)]).render(example1, buf);
|
||||||
|
Example::new(&[Fill(1), Percentage(50), Fill(1)]).render(example2, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_min_example(area: Rect, buf: &mut Buffer) {
|
||||||
|
let [example1, example2, example3, example4, example5, _] =
|
||||||
|
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||||
|
|
||||||
|
Example::new(&[Percentage(100), Min(0)]).render(example1, buf);
|
||||||
|
Example::new(&[Percentage(100), Min(20)]).render(example2, buf);
|
||||||
|
Example::new(&[Percentage(100), Min(40)]).render(example3, buf);
|
||||||
|
Example::new(&[Percentage(100), Min(60)]).render(example4, buf);
|
||||||
|
Example::new(&[Percentage(100), Min(80)]).render(example5, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_max_example(area: Rect, buf: &mut Buffer) {
|
||||||
|
let [example1, example2, example3, example4, example5, _] =
|
||||||
|
Layout::vertical([Length(EXAMPLE_HEIGHT); 6]).areas(area);
|
||||||
|
|
||||||
|
Example::new(&[Percentage(0), Max(0)]).render(example1, buf);
|
||||||
|
Example::new(&[Percentage(0), Max(20)]).render(example2, buf);
|
||||||
|
Example::new(&[Percentage(0), Max(40)]).render(example3, buf);
|
||||||
|
Example::new(&[Percentage(0), Max(60)]).render(example4, buf);
|
||||||
|
Example::new(&[Percentage(0), Max(80)]).render(example5, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Example {
|
||||||
|
constraints: Vec<Constraint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Example {
|
||||||
|
fn new(constraints: &[Constraint]) -> Self {
|
||||||
|
Self {
|
||||||
|
constraints: constraints.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Example {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let [area, _] =
|
||||||
|
Layout::vertical([Length(ILLUSTRATION_HEIGHT), Length(SPACER_HEIGHT)]).areas(area);
|
||||||
|
let blocks = Layout::horizontal(&self.constraints).split(area);
|
||||||
|
|
||||||
|
for (block, constraint) in blocks.iter().zip(&self.constraints) {
|
||||||
|
Self::illustration(*constraint, block.width).render(*block, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Example {
|
||||||
|
fn illustration(constraint: Constraint, width: u16) -> impl Widget {
|
||||||
|
let color = match constraint {
|
||||||
|
Constraint::Length(_) => LENGTH_COLOR,
|
||||||
|
Constraint::Percentage(_) => PERCENTAGE_COLOR,
|
||||||
|
Constraint::Ratio(_, _) => RATIO_COLOR,
|
||||||
|
Constraint::Fill(_) => FILL_COLOR,
|
||||||
|
Constraint::Min(_) => MIN_COLOR,
|
||||||
|
Constraint::Max(_) => MAX_COLOR,
|
||||||
|
};
|
||||||
|
let fg = Color::White;
|
||||||
|
let title = format!("{constraint}");
|
||||||
|
let content = format!("{width} px");
|
||||||
|
let text = format!("{title}\n{content}");
|
||||||
|
let block = Block::bordered()
|
||||||
|
.border_set(symbols::border::QUADRANT_OUTSIDE)
|
||||||
|
.border_style(Style::reset().fg(color).reversed())
|
||||||
|
.style(Style::default().fg(fg).bg(color));
|
||||||
|
Paragraph::new(text).centered().block(block)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
examples/apps/custom-widget/Cargo.toml
Normal file
14
examples/apps/custom-widget/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "custom-widget"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/custom-widget/README.md
Normal file
9
examples/apps/custom-widget/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Custom widget demo
|
||||||
|
|
||||||
|
This example shows how to create a custom widget that can be interacted with the mouse.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p custom-widget
|
||||||
|
```
|
||||||
267
examples/apps/custom-widget/src/main.rs
Normal file
267
examples/apps/custom-widget/src/main.rs
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/// A Ratatui example that demonstrates how to create a custom widget that can be interacted
|
||||||
|
/// with using the mouse.
|
||||||
|
///
|
||||||
|
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||||
|
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||||
|
/// release.
|
||||||
|
///
|
||||||
|
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||||
|
use std::{io::stdout, ops::ControlFlow, time::Duration};
|
||||||
|
|
||||||
|
use color_eyre::Result;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
crossterm::{
|
||||||
|
event::{
|
||||||
|
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
|
||||||
|
MouseEventKind,
|
||||||
|
},
|
||||||
|
execute,
|
||||||
|
},
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::Line,
|
||||||
|
widgets::{Paragraph, Widget},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
let terminal = ratatui::init();
|
||||||
|
execute!(stdout(), EnableMouseCapture)?;
|
||||||
|
let app_result = run(terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
if let Err(err) = execute!(stdout(), DisableMouseCapture) {
|
||||||
|
eprintln!("Error disabling mouse capture: {err}");
|
||||||
|
}
|
||||||
|
app_result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A custom widget that renders a button with a label, theme and state.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Button<'a> {
|
||||||
|
label: Line<'a>,
|
||||||
|
theme: Theme,
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum State {
|
||||||
|
Normal,
|
||||||
|
Selected,
|
||||||
|
Active,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Theme {
|
||||||
|
text: Color,
|
||||||
|
background: Color,
|
||||||
|
highlight: Color,
|
||||||
|
shadow: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLUE: Theme = Theme {
|
||||||
|
text: Color::Rgb(16, 24, 48),
|
||||||
|
background: Color::Rgb(48, 72, 144),
|
||||||
|
highlight: Color::Rgb(64, 96, 192),
|
||||||
|
shadow: Color::Rgb(32, 48, 96),
|
||||||
|
};
|
||||||
|
|
||||||
|
const RED: Theme = Theme {
|
||||||
|
text: Color::Rgb(48, 16, 16),
|
||||||
|
background: Color::Rgb(144, 48, 48),
|
||||||
|
highlight: Color::Rgb(192, 64, 64),
|
||||||
|
shadow: Color::Rgb(96, 32, 32),
|
||||||
|
};
|
||||||
|
|
||||||
|
const GREEN: Theme = Theme {
|
||||||
|
text: Color::Rgb(16, 48, 16),
|
||||||
|
background: Color::Rgb(48, 144, 48),
|
||||||
|
highlight: Color::Rgb(64, 192, 64),
|
||||||
|
shadow: Color::Rgb(32, 96, 32),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A button with a label that can be themed.
|
||||||
|
impl<'a> Button<'a> {
|
||||||
|
pub fn new<T: Into<Line<'a>>>(label: T) -> Self {
|
||||||
|
Button {
|
||||||
|
label: label.into(),
|
||||||
|
theme: BLUE,
|
||||||
|
state: State::Normal,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn theme(mut self, theme: Theme) -> Self {
|
||||||
|
self.theme = theme;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn state(mut self, state: State) -> Self {
|
||||||
|
self.state = state;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for Button<'_> {
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let (background, text, shadow, highlight) = self.colors();
|
||||||
|
buf.set_style(area, Style::new().bg(background).fg(text));
|
||||||
|
|
||||||
|
// render top line if there's enough space
|
||||||
|
if area.height > 2 {
|
||||||
|
buf.set_string(
|
||||||
|
area.x,
|
||||||
|
area.y,
|
||||||
|
"▔".repeat(area.width as usize),
|
||||||
|
Style::new().fg(highlight).bg(background),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// render bottom line if there's enough space
|
||||||
|
if area.height > 1 {
|
||||||
|
buf.set_string(
|
||||||
|
area.x,
|
||||||
|
area.y + area.height - 1,
|
||||||
|
"▁".repeat(area.width as usize),
|
||||||
|
Style::new().fg(shadow).bg(background),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// render label centered
|
||||||
|
buf.set_line(
|
||||||
|
area.x + (area.width.saturating_sub(self.label.width() as u16)) / 2,
|
||||||
|
area.y + (area.height.saturating_sub(1)) / 2,
|
||||||
|
&self.label,
|
||||||
|
area.width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Button<'_> {
|
||||||
|
const fn colors(&self) -> (Color, Color, Color, Color) {
|
||||||
|
let theme = self.theme;
|
||||||
|
match self.state {
|
||||||
|
State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
|
||||||
|
State::Selected => (theme.highlight, theme.text, theme.shadow, theme.highlight),
|
||||||
|
State::Active => (theme.background, theme.text, theme.highlight, theme.shadow),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
let mut selected_button: usize = 0;
|
||||||
|
let mut button_states = [State::Selected, State::Normal, State::Normal];
|
||||||
|
loop {
|
||||||
|
terminal.draw(|frame| draw(frame, button_states))?;
|
||||||
|
if !event::poll(Duration::from_millis(100))? {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key) => {
|
||||||
|
if key.kind != event::KeyEventKind::Press {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if handle_key_event(key, &mut button_states, &mut selected_button).is_break() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse) => {
|
||||||
|
handle_mouse_event(mouse, &mut button_states, &mut selected_button);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(frame: &mut Frame, states: [State; 3]) {
|
||||||
|
let vertical = Layout::vertical([
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Max(3),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Min(0), // ignore remaining space
|
||||||
|
]);
|
||||||
|
let [title, buttons, help, _] = vertical.areas(frame.area());
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new("Custom Widget Example (mouse enabled)"),
|
||||||
|
title,
|
||||||
|
);
|
||||||
|
render_buttons(frame, buttons, states);
|
||||||
|
frame.render_widget(Paragraph::new("←/→: select, Space: toggle, q: quit"), help);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: [State; 3]) {
|
||||||
|
let horizontal = Layout::horizontal([
|
||||||
|
Constraint::Length(15),
|
||||||
|
Constraint::Length(15),
|
||||||
|
Constraint::Length(15),
|
||||||
|
Constraint::Min(0), // ignore remaining space
|
||||||
|
]);
|
||||||
|
let [red, green, blue, _] = horizontal.areas(area);
|
||||||
|
|
||||||
|
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), red);
|
||||||
|
frame.render_widget(Button::new("Green").theme(GREEN).state(states[1]), green);
|
||||||
|
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_event(
|
||||||
|
key: event::KeyEvent,
|
||||||
|
button_states: &mut [State; 3],
|
||||||
|
selected_button: &mut usize,
|
||||||
|
) -> ControlFlow<()> {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') => return ControlFlow::Break(()),
|
||||||
|
KeyCode::Left | KeyCode::Char('h') => {
|
||||||
|
button_states[*selected_button] = State::Normal;
|
||||||
|
*selected_button = selected_button.saturating_sub(1);
|
||||||
|
button_states[*selected_button] = State::Selected;
|
||||||
|
}
|
||||||
|
KeyCode::Right | KeyCode::Char('l') => {
|
||||||
|
button_states[*selected_button] = State::Normal;
|
||||||
|
*selected_button = selected_button.saturating_add(1).min(2);
|
||||||
|
button_states[*selected_button] = State::Selected;
|
||||||
|
}
|
||||||
|
KeyCode::Char(' ') => {
|
||||||
|
if button_states[*selected_button] == State::Active {
|
||||||
|
button_states[*selected_button] = State::Normal;
|
||||||
|
} else {
|
||||||
|
button_states[*selected_button] = State::Active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_mouse_event(
|
||||||
|
mouse: MouseEvent,
|
||||||
|
button_states: &mut [State; 3],
|
||||||
|
selected_button: &mut usize,
|
||||||
|
) {
|
||||||
|
match mouse.kind {
|
||||||
|
MouseEventKind::Moved => {
|
||||||
|
let old_selected_button = *selected_button;
|
||||||
|
*selected_button = match mouse.column {
|
||||||
|
x if x < 15 => 0,
|
||||||
|
x if x < 30 => 1,
|
||||||
|
_ => 2,
|
||||||
|
};
|
||||||
|
if old_selected_button != *selected_button {
|
||||||
|
if button_states[old_selected_button] != State::Active {
|
||||||
|
button_states[old_selected_button] = State::Normal;
|
||||||
|
}
|
||||||
|
if button_states[*selected_button] != State::Active {
|
||||||
|
button_states[*selected_button] = State::Selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseEventKind::Down(MouseButton::Left) => {
|
||||||
|
if button_states[*selected_button] == State::Active {
|
||||||
|
button_states[*selected_button] = State::Normal;
|
||||||
|
} else {
|
||||||
|
button_states[*selected_button] = State::Active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
17
examples/apps/demo/Cargo.toml
Normal file
17
examples/apps/demo/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "demo"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["crossterm"]
|
||||||
|
crossterm = ["ratatui/crossterm"]
|
||||||
|
termion = ["ratatui/termion"]
|
||||||
|
termwiz = ["ratatui/termwiz"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.5.27", features = ["derive"] }
|
||||||
|
rand = "0.9.0"
|
||||||
|
ratatui.workspace = true
|
||||||
25
examples/apps/demo/README.md
Normal file
25
examples/apps/demo/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Demo example
|
||||||
|
|
||||||
|
This is the original demo that was developed for Tui-rs (the library that Ratatui was forked from).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This example is available for each backend. To run it:
|
||||||
|
|
||||||
|
## crossterm
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p demo
|
||||||
|
```
|
||||||
|
|
||||||
|
## termion
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p demo --no-default-features --features termion
|
||||||
|
```
|
||||||
|
|
||||||
|
## termwiz
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p demo --no-default-features --features termwiz
|
||||||
|
```
|
||||||
346
examples/apps/demo/src/app.rs
Normal file
346
examples/apps/demo/src/app.rs
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
use rand::{
|
||||||
|
distr::{Distribution, Uniform},
|
||||||
|
rngs::ThreadRng,
|
||||||
|
};
|
||||||
|
use ratatui::widgets::ListState;
|
||||||
|
|
||||||
|
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) -> Self {
|
||||||
|
Self {
|
||||||
|
distribution: Uniform::new(lower, upper).expect("invalid range"),
|
||||||
|
rng: rand::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 const fn new(interval: f64, period: f64, scale: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
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 const fn new(titles: Vec<&'a str>) -> Self {
|
||||||
|
Self { 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>) -> Self {
|
||||||
|
Self {
|
||||||
|
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) {
|
||||||
|
self.points.drain(0..self.tick_rate);
|
||||||
|
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) -> Self {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
79
examples/apps/demo/src/crossterm.rs
Normal file
79
examples/apps/demo/src/crossterm.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
io,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ratatui::{
|
||||||
|
backend::{Backend, CrosstermBackend},
|
||||||
|
crossterm::{
|
||||||
|
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
},
|
||||||
|
Terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 app_result = 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) = app_result {
|
||||||
|
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(|frame| ui::draw(frame, &mut app))?;
|
||||||
|
|
||||||
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
|
if event::poll(timeout)? {
|
||||||
|
if let Event::Key(key) = event::read()? {
|
||||||
|
if key.kind == KeyEventKind::Press {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Left | KeyCode::Char('h') => app.on_left(),
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => app.on_up(),
|
||||||
|
KeyCode::Right | KeyCode::Char('l') => app.on_right(),
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => app.on_down(),
|
||||||
|
KeyCode::Char(c) => app.on_key(c),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last_tick.elapsed() >= tick_rate {
|
||||||
|
app.on_tick();
|
||||||
|
last_tick = Instant::now();
|
||||||
|
}
|
||||||
|
if app.should_quit {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
examples/apps/demo/src/main.rs
Normal file
56
examples/apps/demo/src/main.rs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
//! # [Ratatui] Original Demo example
|
||||||
|
//!
|
||||||
|
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||||
|
//!
|
||||||
|
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||||
|
//! repository. This means that you may not be able to compile with the latest release version on
|
||||||
|
//! crates.io, or the one that you have installed locally.
|
||||||
|
//!
|
||||||
|
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||||
|
//! library you are using.
|
||||||
|
//!
|
||||||
|
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||||
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
|
use std::{error::Error, time::Duration};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
#[cfg(feature = "crossterm")]
|
||||||
|
mod crossterm;
|
||||||
|
#[cfg(all(not(windows), feature = "termion"))]
|
||||||
|
mod termion;
|
||||||
|
#[cfg(feature = "termwiz")]
|
||||||
|
mod termwiz;
|
||||||
|
|
||||||
|
mod ui;
|
||||||
|
|
||||||
|
/// Demo
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
struct Cli {
|
||||||
|
/// time in ms between two ticks.
|
||||||
|
#[arg(short, long, default_value_t = 250)]
|
||||||
|
tick_rate: u64,
|
||||||
|
|
||||||
|
/// whether unicode symbols are used to improve the overall look of the app
|
||||||
|
#[arg(short, long, default_value_t = true)]
|
||||||
|
unicode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||||
|
#[cfg(feature = "crossterm")]
|
||||||
|
crate::crossterm::run(tick_rate, cli.unicode)?;
|
||||||
|
#[cfg(all(not(windows), feature = "termion", not(feature = "crossterm")))]
|
||||||
|
crate::termion::run(tick_rate, cli.unicode)?;
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "termwiz",
|
||||||
|
not(feature = "crossterm"),
|
||||||
|
not(feature = "termion")
|
||||||
|
))]
|
||||||
|
crate::termwiz::run(tick_rate, cli.unicode)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
86
examples/apps/demo/src/termion.rs
Normal file
86
examples/apps/demo/src/termion.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
use std::{error::Error, io, sync::mpsc, thread, time::Duration};
|
||||||
|
|
||||||
|
use ratatui::{
|
||||||
|
backend::{Backend, TermionBackend},
|
||||||
|
termion::{
|
||||||
|
event::Key,
|
||||||
|
input::{MouseTerminal, TermRead},
|
||||||
|
raw::IntoRawMode,
|
||||||
|
screen::IntoAlternateScreen,
|
||||||
|
},
|
||||||
|
Terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
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(|frame| ui::draw(frame, &mut app))?;
|
||||||
|
|
||||||
|
match events.recv()? {
|
||||||
|
Event::Input(key) => match key {
|
||||||
|
Key::Up | Key::Char('k') => app.on_up(),
|
||||||
|
Key::Down | Key::Char('j') => app.on_down(),
|
||||||
|
Key::Left | Key::Char('h') => app.on_left(),
|
||||||
|
Key::Right | Key::Char('l') => app.on_right(),
|
||||||
|
Key::Char(c) => app.on_key(c),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
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
|
||||||
|
}
|
||||||
80
examples/apps/demo/src/termwiz.rs
Normal file
80
examples/apps/demo/src/termwiz.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ratatui::{
|
||||||
|
backend::TermwizBackend,
|
||||||
|
termwiz::{
|
||||||
|
input::{InputEvent, KeyCode},
|
||||||
|
terminal::Terminal as TermwizTerminal,
|
||||||
|
},
|
||||||
|
Terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 app_result = run_app(&mut terminal, app, tick_rate);
|
||||||
|
|
||||||
|
terminal.show_cursor()?;
|
||||||
|
terminal.flush()?;
|
||||||
|
|
||||||
|
if let Err(err) = app_result {
|
||||||
|
println!("{err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_app(
|
||||||
|
terminal: &mut Terminal<TermwizBackend>,
|
||||||
|
mut app: App,
|
||||||
|
tick_rate: Duration,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut last_tick = Instant::now();
|
||||||
|
loop {
|
||||||
|
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
||||||
|
|
||||||
|
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||||
|
if let 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 | KeyCode::Char('k') => app.on_up(),
|
||||||
|
KeyCode::DownArrow | KeyCode::Char('j') => app.on_down(),
|
||||||
|
KeyCode::LeftArrow | KeyCode::Char('h') => app.on_left(),
|
||||||
|
KeyCode::RightArrow | KeyCode::Char('l') => 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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
405
examples/apps/demo/src/ui.rs
Normal file
405
examples/apps/demo/src/ui.rs
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
use ratatui::{
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
symbols,
|
||||||
|
text::{self, Span},
|
||||||
|
widgets::{
|
||||||
|
canvas::{self, Canvas, Circle, Map, MapResolution, Rectangle},
|
||||||
|
Axis, BarChart, Block, Cell, Chart, Dataset, Gauge, LineGauge, List, ListItem, Paragraph,
|
||||||
|
Row, Sparkline, Table, Tabs, Wrap,
|
||||||
|
},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::App;
|
||||||
|
|
||||||
|
pub fn draw(frame: &mut Frame, app: &mut App) {
|
||||||
|
let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(frame.area());
|
||||||
|
let tabs = app
|
||||||
|
.tabs
|
||||||
|
.titles
|
||||||
|
.iter()
|
||||||
|
.map(|t| text::Line::from(Span::styled(*t, Style::default().fg(Color::Green))))
|
||||||
|
.collect::<Tabs>()
|
||||||
|
.block(Block::bordered().title(app.title))
|
||||||
|
.highlight_style(Style::default().fg(Color::Yellow))
|
||||||
|
.select(app.tabs.index);
|
||||||
|
frame.render_widget(tabs, chunks[0]);
|
||||||
|
match app.tabs.index {
|
||||||
|
0 => draw_first_tab(frame, app, chunks[1]),
|
||||||
|
1 => draw_second_tab(frame, app, chunks[1]),
|
||||||
|
2 => draw_third_tab(frame, app, chunks[1]),
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_first_tab(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
let chunks = Layout::vertical([
|
||||||
|
Constraint::Length(9),
|
||||||
|
Constraint::Min(8),
|
||||||
|
Constraint::Length(7),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
draw_gauges(frame, app, chunks[0]);
|
||||||
|
draw_charts(frame, app, chunks[1]);
|
||||||
|
draw_text(frame, chunks[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_gauges(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
let chunks = Layout::vertical([
|
||||||
|
Constraint::Length(2),
|
||||||
|
Constraint::Length(3),
|
||||||
|
Constraint::Length(2),
|
||||||
|
])
|
||||||
|
.margin(1)
|
||||||
|
.split(area);
|
||||||
|
let block = Block::bordered().title("Graphs");
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
let label = format!("{:.2}%", app.progress * 100.0);
|
||||||
|
let gauge = Gauge::default()
|
||||||
|
.block(Block::new().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);
|
||||||
|
frame.render_widget(gauge, chunks[0]);
|
||||||
|
|
||||||
|
let sparkline = Sparkline::default()
|
||||||
|
.block(Block::new().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
|
||||||
|
});
|
||||||
|
frame.render_widget(sparkline, chunks[1]);
|
||||||
|
|
||||||
|
let line_gauge = LineGauge::default()
|
||||||
|
.block(Block::new().title("LineGauge:"))
|
||||||
|
.filled_style(Style::default().fg(Color::Magenta))
|
||||||
|
.filled_symbol(if app.enhanced_graphics {
|
||||||
|
symbols::line::THICK_HORIZONTAL
|
||||||
|
} else {
|
||||||
|
symbols::line::HORIZONTAL
|
||||||
|
})
|
||||||
|
.unfilled_symbol(if app.enhanced_graphics {
|
||||||
|
symbols::line::THICK_HORIZONTAL
|
||||||
|
} else {
|
||||||
|
symbols::line::HORIZONTAL
|
||||||
|
})
|
||||||
|
.ratio(app.progress);
|
||||||
|
frame.render_widget(line_gauge, chunks[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn draw_charts(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
let constraints = if app.show_chart {
|
||||||
|
vec![Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||||
|
} else {
|
||||||
|
vec![Constraint::Percentage(100)]
|
||||||
|
};
|
||||||
|
let chunks = Layout::horizontal(constraints).split(area);
|
||||||
|
{
|
||||||
|
let chunks = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
|
.split(chunks[0]);
|
||||||
|
{
|
||||||
|
let chunks =
|
||||||
|
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
|
.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::bordered().title("List"))
|
||||||
|
.highlight_style(Style::default().add_modifier(Modifier::BOLD))
|
||||||
|
.highlight_symbol("> ");
|
||||||
|
frame.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::bordered().title("List"));
|
||||||
|
frame.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
let barchart = BarChart::default()
|
||||||
|
.block(Block::bordered().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));
|
||||||
|
frame.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::bordered().title(Span::styled(
|
||||||
|
"Chart",
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Cyan)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.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([
|
||||||
|
Span::styled("-20", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
Span::raw("0"),
|
||||||
|
Span::styled("20", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
frame.render_widget(chart, chunks[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_text(frame: &mut Frame, area: Rect) {
|
||||||
|
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::bordered().title(Span::styled(
|
||||||
|
"Footer",
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::Magenta)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
));
|
||||||
|
let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
|
||||||
|
frame.render_widget(paragraph, area);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_second_tab(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
|
let chunks =
|
||||||
|
Layout::horizontal([Constraint::Percentage(30), Constraint::Percentage(70)]).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,
|
||||||
|
[
|
||||||
|
Constraint::Length(15),
|
||||||
|
Constraint::Length(15),
|
||||||
|
Constraint::Length(10),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.header(
|
||||||
|
Row::new(vec!["Server", "Location", "Status"])
|
||||||
|
.style(Style::default().fg(Color::Yellow))
|
||||||
|
.bottom_margin(1),
|
||||||
|
)
|
||||||
|
.block(Block::bordered().title("Servers"));
|
||||||
|
frame.render_widget(table, chunks[0]);
|
||||||
|
|
||||||
|
let map = Canvas::default()
|
||||||
|
.block(Block::bordered().title("World"))
|
||||||
|
.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]);
|
||||||
|
frame.render_widget(map, chunks[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_third_tab(frame: &mut Frame, _app: &mut App, area: Rect) {
|
||||||
|
let chunks = Layout::horizontal([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,
|
||||||
|
[
|
||||||
|
Constraint::Ratio(1, 3),
|
||||||
|
Constraint::Ratio(1, 3),
|
||||||
|
Constraint::Ratio(1, 3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.block(Block::bordered().title("Colors"));
|
||||||
|
frame.render_widget(table, chunks[0]);
|
||||||
|
}
|
||||||
19
examples/apps/demo2/Cargo.toml
Normal file
19
examples/apps/demo2/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "demo2"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre = "0.6.3"
|
||||||
|
crossterm.workspace = true
|
||||||
|
indoc.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
palette = "0.7.6"
|
||||||
|
rand = "0.9.0"
|
||||||
|
rand_chacha = "0.9.0"
|
||||||
|
ratatui = { workspace = true, features = ["all-widgets"] }
|
||||||
|
strum.workspace = true
|
||||||
|
time = "0.3.37"
|
||||||
|
unicode-width = "0.2.0"
|
||||||
9
examples/apps/demo2/README.md
Normal file
9
examples/apps/demo2/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## Demo2
|
||||||
|
|
||||||
|
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p demo2
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
225
examples/apps/demo2/src/app.rs
Normal file
225
examples/apps/demo2/src/app.rs
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use color_eyre::{eyre::Context, Result};
|
||||||
|
use crossterm::event;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind},
|
||||||
|
layout::{Constraint, Layout, Rect},
|
||||||
|
style::Color,
|
||||||
|
text::{Line, Span},
|
||||||
|
widgets::{Block, Tabs, Widget},
|
||||||
|
DefaultTerminal, Frame,
|
||||||
|
};
|
||||||
|
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
destroy,
|
||||||
|
tabs::{AboutTab, EmailTab, RecipeTab, TracerouteTab, WeatherTab},
|
||||||
|
THEME,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct App {
|
||||||
|
mode: Mode,
|
||||||
|
tab: Tab,
|
||||||
|
about_tab: AboutTab,
|
||||||
|
recipe_tab: RecipeTab,
|
||||||
|
email_tab: EmailTab,
|
||||||
|
traceroute_tab: TracerouteTab,
|
||||||
|
weather_tab: WeatherTab,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Mode {
|
||||||
|
#[default]
|
||||||
|
Running,
|
||||||
|
Destroy,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, Display, EnumIter, FromRepr, PartialEq, Eq)]
|
||||||
|
enum Tab {
|
||||||
|
#[default]
|
||||||
|
About,
|
||||||
|
Recipe,
|
||||||
|
Email,
|
||||||
|
Traceroute,
|
||||||
|
Weather,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// Run the app until the user quits.
|
||||||
|
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||||
|
while self.is_running() {
|
||||||
|
terminal
|
||||||
|
.draw(|frame| self.draw(frame))
|
||||||
|
.wrap_err("terminal.draw")?;
|
||||||
|
self.handle_events()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_running(&self) -> bool {
|
||||||
|
self.mode != Mode::Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draw a single frame of the app.
|
||||||
|
fn draw(&self, frame: &mut Frame) {
|
||||||
|
frame.render_widget(self, frame.area());
|
||||||
|
if self.mode == Mode::Destroy {
|
||||||
|
destroy::destroy(frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle events from the terminal.
|
||||||
|
///
|
||||||
|
/// This function is called once per frame, The events are polled from the stdin with timeout of
|
||||||
|
/// 1/50th of a second. This was chosen to try to match the default frame rate of a GIF in VHS.
|
||||||
|
fn handle_events(&mut self) -> Result<()> {
|
||||||
|
let timeout = Duration::from_secs_f64(1.0 / 50.0);
|
||||||
|
if !event::poll(timeout)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
match event::read()? {
|
||||||
|
Event::Key(key) if key.kind == KeyEventKind::Press => self.handle_key_press(key),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key_press(&mut self, key: KeyEvent) {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('q') | KeyCode::Esc => self.mode = Mode::Quit,
|
||||||
|
KeyCode::Char('h') | KeyCode::Left => self.prev_tab(),
|
||||||
|
KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
|
||||||
|
KeyCode::Char('k') | KeyCode::Up => self.prev(),
|
||||||
|
KeyCode::Char('j') | KeyCode::Down => self.next(),
|
||||||
|
KeyCode::Char('d') | KeyCode::Delete => self.destroy(),
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev(&mut self) {
|
||||||
|
match self.tab {
|
||||||
|
Tab::About => self.about_tab.prev_row(),
|
||||||
|
Tab::Recipe => self.recipe_tab.prev(),
|
||||||
|
Tab::Email => self.email_tab.prev(),
|
||||||
|
Tab::Traceroute => self.traceroute_tab.prev_row(),
|
||||||
|
Tab::Weather => self.weather_tab.prev(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&mut self) {
|
||||||
|
match self.tab {
|
||||||
|
Tab::About => self.about_tab.next_row(),
|
||||||
|
Tab::Recipe => self.recipe_tab.next(),
|
||||||
|
Tab::Email => self.email_tab.next(),
|
||||||
|
Tab::Traceroute => self.traceroute_tab.next_row(),
|
||||||
|
Tab::Weather => self.weather_tab.next(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev_tab(&mut self) {
|
||||||
|
self.tab = self.tab.prev();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_tab(&mut self) {
|
||||||
|
self.tab = self.tab.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(&mut self) {
|
||||||
|
self.mode = Mode::Destroy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implement Widget for &App rather than for App as we would otherwise have to clone or copy the
|
||||||
|
/// entire app state on every frame. For this example, the app state is small enough that it doesn't
|
||||||
|
/// matter, but for larger apps this can be a significant performance improvement.
|
||||||
|
impl Widget for &App {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let vertical = Layout::vertical([
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(1),
|
||||||
|
]);
|
||||||
|
let [title_bar, tab, bottom_bar] = vertical.areas(area);
|
||||||
|
|
||||||
|
Block::new().style(THEME.root).render(area, buf);
|
||||||
|
self.render_title_bar(title_bar, buf);
|
||||||
|
self.render_selected_tab(tab, buf);
|
||||||
|
App::render_bottom_bar(bottom_bar, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn render_title_bar(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
let layout = Layout::horizontal([Constraint::Min(0), Constraint::Length(43)]);
|
||||||
|
let [title, tabs] = layout.areas(area);
|
||||||
|
|
||||||
|
Span::styled("Ratatui", THEME.app_title).render(title, buf);
|
||||||
|
let titles = Tab::iter().map(Tab::title);
|
||||||
|
Tabs::new(titles)
|
||||||
|
.style(THEME.tabs)
|
||||||
|
.highlight_style(THEME.tabs_selected)
|
||||||
|
.select(self.tab as usize)
|
||||||
|
.divider("")
|
||||||
|
.padding("", "")
|
||||||
|
.render(tabs, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_selected_tab(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
match self.tab {
|
||||||
|
Tab::About => self.about_tab.render(area, buf),
|
||||||
|
Tab::Recipe => self.recipe_tab.render(area, buf),
|
||||||
|
Tab::Email => self.email_tab.render(area, buf),
|
||||||
|
Tab::Traceroute => self.traceroute_tab.render(area, buf),
|
||||||
|
Tab::Weather => self.weather_tab.render(area, buf),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_bottom_bar(area: Rect, buf: &mut Buffer) {
|
||||||
|
let keys = [
|
||||||
|
("H/←", "Left"),
|
||||||
|
("L/→", "Right"),
|
||||||
|
("K/↑", "Up"),
|
||||||
|
("J/↓", "Down"),
|
||||||
|
("D/Del", "Destroy"),
|
||||||
|
("Q/Esc", "Quit"),
|
||||||
|
];
|
||||||
|
let spans = keys
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(key, desc)| {
|
||||||
|
let key = Span::styled(format!(" {key} "), THEME.key_binding.key);
|
||||||
|
let desc = Span::styled(format!(" {desc} "), THEME.key_binding.description);
|
||||||
|
[key, desc]
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
Line::from(spans)
|
||||||
|
.centered()
|
||||||
|
.style((Color::Indexed(236), Color::Indexed(232)))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tab {
|
||||||
|
fn next(self) -> Self {
|
||||||
|
let current_index = self as usize;
|
||||||
|
let next_index = current_index.saturating_add(1);
|
||||||
|
Self::from_repr(next_index).unwrap_or(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev(self) -> Self {
|
||||||
|
let current_index = self as usize;
|
||||||
|
let prev_index = current_index.saturating_sub(1);
|
||||||
|
Self::from_repr(prev_index).unwrap_or(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(self) -> String {
|
||||||
|
match self {
|
||||||
|
Self::About => String::new(),
|
||||||
|
tab => format!(" {tab} "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
examples/apps/demo2/src/colors.rs
Normal file
35
examples/apps/demo2/src/colors.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
use palette::{IntoColor, Okhsv, Srgb};
|
||||||
|
use ratatui::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
|
||||||
|
|
||||||
|
/// A widget that renders a color swatch of RGB colors.
|
||||||
|
///
|
||||||
|
/// The widget is rendered as a rectangle with the hue changing along the x-axis from 0.0 to 360.0
|
||||||
|
/// and the value changing along the y-axis (from 1.0 to 0.0). Each pixel is rendered as a block
|
||||||
|
/// character with the top half slightly lighter than the bottom half.
|
||||||
|
pub struct RgbSwatch;
|
||||||
|
|
||||||
|
impl Widget for RgbSwatch {
|
||||||
|
#[allow(clippy::cast_precision_loss, clippy::similar_names)]
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
for (yi, y) in (area.top()..area.bottom()).enumerate() {
|
||||||
|
let value = f32::from(area.height) - yi as f32;
|
||||||
|
let value_fg = value / f32::from(area.height);
|
||||||
|
let value_bg = (value - 0.5) / f32::from(area.height);
|
||||||
|
for (xi, x) in (area.left()..area.right()).enumerate() {
|
||||||
|
let hue = xi as f32 * 360.0 / f32::from(area.width);
|
||||||
|
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value_fg);
|
||||||
|
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value_bg);
|
||||||
|
buf[(x, y)].set_char('▀').set_fg(fg).set_bg(bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a hue and value into an RGB color via the Oklab color space.
|
||||||
|
///
|
||||||
|
/// See <https://bottosson.github.io/posts/oklab/> for more details.
|
||||||
|
pub fn color_from_oklab(hue: f32, saturation: f32, value: f32) -> Color {
|
||||||
|
let color: Srgb = Okhsv::new(hue, saturation, value).into_color();
|
||||||
|
let color = color.into_format();
|
||||||
|
Color::Rgb(color.red, color.green, color.blue)
|
||||||
|
}
|
||||||
142
examples/apps/demo2/src/destroy.rs
Normal file
142
examples/apps/demo2/src/destroy.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
use rand_chacha::rand_core::SeedableRng;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Flex, Layout, Rect},
|
||||||
|
style::{Color, Style},
|
||||||
|
text::Text,
|
||||||
|
widgets::Widget,
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// delay the start of the animation so it doesn't start immediately
|
||||||
|
const DELAY: usize = 120;
|
||||||
|
/// higher means more pixels per frame are modified in the animation
|
||||||
|
const DRIP_SPEED: usize = 500;
|
||||||
|
/// delay the start of the text animation so it doesn't start immediately after the initial delay
|
||||||
|
const TEXT_DELAY: usize = 180;
|
||||||
|
|
||||||
|
/// Destroy mode activated by pressing `d`
|
||||||
|
pub fn destroy(frame: &mut Frame<'_>) {
|
||||||
|
let frame_count = frame.count().saturating_sub(DELAY);
|
||||||
|
if frame_count == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let area = frame.area();
|
||||||
|
let buf = frame.buffer_mut();
|
||||||
|
|
||||||
|
drip(frame_count, area, buf);
|
||||||
|
text(frame_count, area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move a bunch of random pixels down one row.
|
||||||
|
///
|
||||||
|
/// Each pick some random pixels and move them each down one row. This is a very inefficient way to
|
||||||
|
/// do this, but it works well enough for this demo.
|
||||||
|
#[allow(
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_precision_loss,
|
||||||
|
clippy::cast_sign_loss
|
||||||
|
)]
|
||||||
|
fn drip(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
// a seeded rng as we have to move the same random pixels each frame
|
||||||
|
let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(10);
|
||||||
|
let ramp_frames = 450;
|
||||||
|
let fractional_speed = frame_count as f64 / f64::from(ramp_frames);
|
||||||
|
let variable_speed = DRIP_SPEED as f64 * fractional_speed * fractional_speed * fractional_speed;
|
||||||
|
let pixel_count = (frame_count as f64 * variable_speed).floor() as usize;
|
||||||
|
for _ in 0..pixel_count {
|
||||||
|
let src_x = rng.random_range(0..area.width);
|
||||||
|
let src_y = rng.random_range(1..area.height - 2);
|
||||||
|
let src = buf[(src_x, src_y)].clone();
|
||||||
|
// 1% of the time, move a blank or pixel (10:1) to the top line of the screen
|
||||||
|
if rng.random_ratio(1, 100) {
|
||||||
|
let dest_x = rng
|
||||||
|
.random_range(src_x.saturating_sub(5)..src_x.saturating_add(5))
|
||||||
|
.clamp(area.left(), area.right() - 1);
|
||||||
|
let dest_y = area.top() + 1;
|
||||||
|
|
||||||
|
let dest = &mut buf[(dest_x, dest_y)];
|
||||||
|
// copy the cell to the new location about 1/10 of the time blank out the cell the rest
|
||||||
|
// of the time. This has the effect of gradually removing the pixels from the screen.
|
||||||
|
if rng.random_ratio(1, 10) {
|
||||||
|
*dest = src;
|
||||||
|
} else {
|
||||||
|
dest.reset();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// move the pixel down one row
|
||||||
|
let dest_x = src_x;
|
||||||
|
let dest_y = src_y.saturating_add(1).min(area.bottom() - 2);
|
||||||
|
// copy the cell to the new location
|
||||||
|
buf[(dest_x, dest_y)] = src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// draw some text fading in and out from black to red and back
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
|
||||||
|
fn text(frame_count: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
let sub_frame = frame_count.saturating_sub(TEXT_DELAY);
|
||||||
|
if sub_frame == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let logo = indoc::indoc! {"
|
||||||
|
██████ ████ ██████ ████ ██████ ██ ██ ██
|
||||||
|
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||||
|
██████ ████████ ██ ████████ ██ ██ ██ ██
|
||||||
|
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
|
||||||
|
██ ██ ██ ██ ██ ██ ██ ██ ████ ██
|
||||||
|
"};
|
||||||
|
let logo_text = Text::styled(logo, Color::Rgb(255, 255, 255));
|
||||||
|
let area = centered_rect(area, logo_text.width() as u16, logo_text.height() as u16);
|
||||||
|
|
||||||
|
let mask_buf = &mut Buffer::empty(area);
|
||||||
|
logo_text.render(area, mask_buf);
|
||||||
|
|
||||||
|
let percentage = (sub_frame as f64 / 480.0).clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
for row in area.rows() {
|
||||||
|
for col in row.columns() {
|
||||||
|
let cell = &mut buf[(col.x, col.y)];
|
||||||
|
let mask_cell = &mut mask_buf[(col.x, col.y)];
|
||||||
|
cell.set_symbol(mask_cell.symbol());
|
||||||
|
|
||||||
|
// blend the mask cell color with the cell color
|
||||||
|
let cell_color = cell.style().bg.unwrap_or(Color::Rgb(0, 0, 0));
|
||||||
|
let mask_color = mask_cell.style().fg.unwrap_or(Color::Rgb(255, 0, 0));
|
||||||
|
|
||||||
|
let color = blend(mask_color, cell_color, percentage);
|
||||||
|
cell.set_style(Style::new().fg(color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blend(mask_color: Color, cell_color: Color, percentage: f64) -> Color {
|
||||||
|
let Color::Rgb(mask_red, mask_green, mask_blue) = mask_color else {
|
||||||
|
return mask_color;
|
||||||
|
};
|
||||||
|
let Color::Rgb(cell_red, cell_green, cell_blue) = cell_color else {
|
||||||
|
return mask_color;
|
||||||
|
};
|
||||||
|
|
||||||
|
let remain = 1.0 - percentage;
|
||||||
|
|
||||||
|
let red = f64::from(mask_red).mul_add(percentage, f64::from(cell_red) * remain);
|
||||||
|
let green = f64::from(mask_green).mul_add(percentage, f64::from(cell_green) * remain);
|
||||||
|
let blue = f64::from(mask_blue).mul_add(percentage, f64::from(cell_blue) * remain);
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||||
|
Color::Rgb(red as u8, green as u8, blue as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// a centered rect of the given size
|
||||||
|
fn centered_rect(area: Rect, width: u16, height: u16) -> Rect {
|
||||||
|
let horizontal = Layout::horizontal([width]).flex(Flex::Center);
|
||||||
|
let vertical = Layout::vertical([height]).flex(Flex::Center);
|
||||||
|
let [area] = vertical.areas(area);
|
||||||
|
let [area] = horizontal.areas(area);
|
||||||
|
area
|
||||||
|
}
|
||||||
54
examples/apps/demo2/src/main.rs
Normal file
54
examples/apps/demo2/src/main.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
//! # [Ratatui] Demo2 example
|
||||||
|
//!
|
||||||
|
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||||
|
//!
|
||||||
|
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||||
|
//! repository. This means that you may not be able to compile with the latest release version on
|
||||||
|
//! crates.io, or the one that you have installed locally.
|
||||||
|
//!
|
||||||
|
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||||
|
//! library you are using.
|
||||||
|
//!
|
||||||
|
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||||
|
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||||
|
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||||
|
|
||||||
|
#![allow(
|
||||||
|
clippy::missing_errors_doc,
|
||||||
|
clippy::module_name_repetitions,
|
||||||
|
clippy::must_use_candidate
|
||||||
|
)]
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
mod colors;
|
||||||
|
mod destroy;
|
||||||
|
mod tabs;
|
||||||
|
mod theme;
|
||||||
|
|
||||||
|
use std::io::stdout;
|
||||||
|
|
||||||
|
use app::App;
|
||||||
|
use color_eyre::Result;
|
||||||
|
use crossterm::{
|
||||||
|
execute,
|
||||||
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use ratatui::{layout::Rect, TerminalOptions, Viewport};
|
||||||
|
|
||||||
|
pub use self::{
|
||||||
|
colors::{color_from_oklab, RgbSwatch},
|
||||||
|
theme::THEME,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install()?;
|
||||||
|
// this size is to match the size of the terminal when running the demo
|
||||||
|
// using vhs in a 1280x640 sized window (github social preview size)
|
||||||
|
let viewport = Viewport::Fixed(Rect::new(0, 0, 81, 18));
|
||||||
|
let terminal = ratatui::init_with_options(TerminalOptions { viewport });
|
||||||
|
execute!(stdout(), EnterAlternateScreen).expect("failed to enter alternate screen");
|
||||||
|
let app_result = App::default().run(terminal);
|
||||||
|
execute!(stdout(), LeaveAlternateScreen).expect("failed to leave alternate screen");
|
||||||
|
ratatui::restore();
|
||||||
|
app_result
|
||||||
|
}
|
||||||
11
examples/apps/demo2/src/tabs.rs
Normal file
11
examples/apps/demo2/src/tabs.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
mod about;
|
||||||
|
mod email;
|
||||||
|
mod recipe;
|
||||||
|
mod traceroute;
|
||||||
|
mod weather;
|
||||||
|
|
||||||
|
pub use about::AboutTab;
|
||||||
|
pub use email::EmailTab;
|
||||||
|
pub use recipe::RecipeTab;
|
||||||
|
pub use traceroute::TracerouteTab;
|
||||||
|
pub use weather::WeatherTab;
|
||||||
75
examples/apps/demo2/src/tabs/about.rs
Normal file
75
examples/apps/demo2/src/tabs/about.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||||
|
widgets::{
|
||||||
|
Block, Borders, Clear, MascotEyeColor, Padding, Paragraph, RatatuiMascot, Widget, Wrap,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{RgbSwatch, THEME};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct AboutTab {
|
||||||
|
row_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AboutTab {
|
||||||
|
pub fn prev_row(&mut self) {
|
||||||
|
self.row_index = self.row_index.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_row(&mut self) {
|
||||||
|
self.row_index = self.row_index.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for AboutTab {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
RgbSwatch.render(area, buf);
|
||||||
|
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
|
||||||
|
let [logo_area, description] = horizontal.areas(area);
|
||||||
|
render_crate_description(description, buf);
|
||||||
|
let eye_state = if self.row_index % 2 == 0 {
|
||||||
|
MascotEyeColor::Default
|
||||||
|
} else {
|
||||||
|
MascotEyeColor::Red
|
||||||
|
};
|
||||||
|
RatatuiMascot::default().set_eye(eye_state).render(
|
||||||
|
logo_area.inner(Margin {
|
||||||
|
vertical: 0,
|
||||||
|
horizontal: 2,
|
||||||
|
}),
|
||||||
|
buf,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_crate_description(area: Rect, buf: &mut Buffer) {
|
||||||
|
let area = area.inner(Margin {
|
||||||
|
vertical: 4,
|
||||||
|
horizontal: 2,
|
||||||
|
});
|
||||||
|
Clear.render(area, buf); // clear out the color swatches
|
||||||
|
Block::new().style(THEME.content).render(area, buf);
|
||||||
|
let area = area.inner(Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 2,
|
||||||
|
});
|
||||||
|
let text = "- cooking up terminal user interfaces -
|
||||||
|
|
||||||
|
Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \
|
||||||
|
screen efficiently every frame.";
|
||||||
|
Paragraph::new(text)
|
||||||
|
.style(THEME.description)
|
||||||
|
.block(
|
||||||
|
Block::new()
|
||||||
|
.title(" Ratatui ")
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.border_style(THEME.description_title)
|
||||||
|
.padding(Padding::new(0, 0, 0, 0)),
|
||||||
|
)
|
||||||
|
.wrap(Wrap { trim: true })
|
||||||
|
.scroll((0, 0))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
158
examples/apps/demo2/src/tabs/email.rs
Normal file
158
examples/apps/demo2/src/tabs/email.rs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Layout, Margin, Rect},
|
||||||
|
style::{Styled, Stylize},
|
||||||
|
text::Line,
|
||||||
|
widgets::{
|
||||||
|
Block, BorderType, Borders, Clear, List, ListItem, ListState, Padding, Paragraph,
|
||||||
|
Scrollbar, ScrollbarState, StatefulWidget, Tabs, Widget,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use crate::{RgbSwatch, THEME};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Email {
|
||||||
|
from: &'static str,
|
||||||
|
subject: &'static str,
|
||||||
|
body: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMAILS: &[Email] = &[
|
||||||
|
Email {
|
||||||
|
from: "Alice <alice@example.com>",
|
||||||
|
subject: "Hello",
|
||||||
|
body: "Hi Bob,\nHow are you?\n\nAlice",
|
||||||
|
},
|
||||||
|
Email {
|
||||||
|
from: "Bob <bob@example.com>",
|
||||||
|
subject: "Re: Hello",
|
||||||
|
body: "Hi Alice,\nI'm fine, thanks!\n\nBob",
|
||||||
|
},
|
||||||
|
Email {
|
||||||
|
from: "Charlie <charlie@example.com>",
|
||||||
|
subject: "Re: Hello",
|
||||||
|
body: "Hi Alice,\nI'm fine, thanks!\n\nCharlie",
|
||||||
|
},
|
||||||
|
Email {
|
||||||
|
from: "Dave <dave@example.com>",
|
||||||
|
subject: "Re: Hello (STOP REPLYING TO ALL)",
|
||||||
|
body: "Hi Everyone,\nPlease stop replying to all.\n\nDave",
|
||||||
|
},
|
||||||
|
Email {
|
||||||
|
from: "Eve <eve@example.com>",
|
||||||
|
subject: "Re: Hello (STOP REPLYING TO ALL)",
|
||||||
|
body: "Hi Everyone,\nI'm reading all your emails.\n\nEve",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct EmailTab {
|
||||||
|
row_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EmailTab {
|
||||||
|
/// Select the previous email (with wrap around).
|
||||||
|
pub fn prev(&mut self) {
|
||||||
|
self.row_index = self.row_index.saturating_add(EMAILS.len() - 1) % EMAILS.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the next email (with wrap around).
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
self.row_index = self.row_index.saturating_add(1) % EMAILS.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for EmailTab {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
RgbSwatch.render(area, buf);
|
||||||
|
let area = area.inner(Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 2,
|
||||||
|
});
|
||||||
|
Clear.render(area, buf);
|
||||||
|
let vertical = Layout::vertical([Constraint::Length(5), Constraint::Min(0)]);
|
||||||
|
let [inbox, email] = vertical.areas(area);
|
||||||
|
render_inbox(self.row_index, inbox, buf);
|
||||||
|
render_email(self.row_index, email, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn render_inbox(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||||
|
let [tabs, inbox] = vertical.areas(area);
|
||||||
|
let theme = THEME.email;
|
||||||
|
Tabs::new(vec![" Inbox ", " Sent ", " Drafts "])
|
||||||
|
.style(theme.tabs)
|
||||||
|
.highlight_style(theme.tabs_selected)
|
||||||
|
.select(0)
|
||||||
|
.divider("")
|
||||||
|
.render(tabs, buf);
|
||||||
|
|
||||||
|
let highlight_symbol = ">>";
|
||||||
|
let from_width = EMAILS
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.from.width())
|
||||||
|
.max()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let items = EMAILS.iter().map(|e| {
|
||||||
|
let from = format!("{:width$}", e.from, width = from_width).into();
|
||||||
|
ListItem::new(Line::from(vec![from, " ".into(), e.subject.into()]))
|
||||||
|
});
|
||||||
|
let mut state = ListState::default().with_selected(Some(selected_index));
|
||||||
|
StatefulWidget::render(
|
||||||
|
List::new(items)
|
||||||
|
.style(theme.inbox)
|
||||||
|
.highlight_style(theme.selected_item)
|
||||||
|
.highlight_symbol(highlight_symbol),
|
||||||
|
inbox,
|
||||||
|
buf,
|
||||||
|
&mut state,
|
||||||
|
);
|
||||||
|
let mut scrollbar_state = ScrollbarState::default()
|
||||||
|
.content_length(EMAILS.len())
|
||||||
|
.position(selected_index);
|
||||||
|
Scrollbar::default()
|
||||||
|
.begin_symbol(None)
|
||||||
|
.end_symbol(None)
|
||||||
|
.track_symbol(None)
|
||||||
|
.thumb_symbol("▐")
|
||||||
|
.render(inbox, buf, &mut scrollbar_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_email(selected_index: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
let theme = THEME.email;
|
||||||
|
let email = EMAILS.get(selected_index);
|
||||||
|
let block = Block::new()
|
||||||
|
.style(theme.body)
|
||||||
|
.padding(Padding::new(2, 2, 0, 0))
|
||||||
|
.borders(Borders::TOP)
|
||||||
|
.border_type(BorderType::Thick);
|
||||||
|
let inner = block.inner(area);
|
||||||
|
block.render(area, buf);
|
||||||
|
if let Some(email) = email {
|
||||||
|
let vertical = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]);
|
||||||
|
let [headers_area, body_area] = vertical.areas(inner);
|
||||||
|
let headers = vec![
|
||||||
|
Line::from(vec![
|
||||||
|
"From: ".set_style(theme.header),
|
||||||
|
email.from.set_style(theme.header_value),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Subject: ".set_style(theme.header),
|
||||||
|
email.subject.set_style(theme.header_value),
|
||||||
|
]),
|
||||||
|
"-".repeat(inner.width as usize).dim().into(),
|
||||||
|
];
|
||||||
|
Paragraph::new(headers)
|
||||||
|
.style(theme.body)
|
||||||
|
.render(headers_area, buf);
|
||||||
|
let body = email.body.lines().map(Line::from).collect_vec();
|
||||||
|
Paragraph::new(body)
|
||||||
|
.style(theme.body)
|
||||||
|
.render(body_area, buf);
|
||||||
|
} else {
|
||||||
|
Paragraph::new("No email selected").render(inner, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
185
examples/apps/demo2/src/tabs/recipe.rs
Normal file
185
examples/apps/demo2/src/tabs/recipe.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||||
|
style::{Style, Stylize},
|
||||||
|
text::Line,
|
||||||
|
widgets::{
|
||||||
|
Block, Clear, Padding, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||||
|
StatefulWidget, Table, TableState, Widget, Wrap,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{RgbSwatch, THEME};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
|
struct Ingredient {
|
||||||
|
quantity: &'static str,
|
||||||
|
name: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ingredient {
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
fn height(&self) -> u16 {
|
||||||
|
self.name.lines().count() as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Ingredient> for Row<'_> {
|
||||||
|
fn from(i: Ingredient) -> Self {
|
||||||
|
Row::new(vec![i.quantity, i.name]).height(i.height())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.realsimple.com/food-recipes/browse-all-recipes/ratatouille
|
||||||
|
const RECIPE: &[(&str, &str)] = &[
|
||||||
|
(
|
||||||
|
"Step 1: ",
|
||||||
|
"Over medium-low heat, add the oil to a large skillet with the onion, garlic, and bay \
|
||||||
|
leaf, stirring occasionally, until the onion has softened.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Step 2: ",
|
||||||
|
"Add the eggplant and cook, stirring occasionally, for 8 minutes or until the eggplant \
|
||||||
|
has softened. Stir in the zucchini, red bell pepper, tomatoes, and salt, and cook over \
|
||||||
|
medium heat, stirring occasionally, for 5 to 7 minutes or until the vegetables are \
|
||||||
|
tender. Stir in the basil and few grinds of pepper to taste.",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const INGREDIENTS: &[Ingredient] = &[
|
||||||
|
Ingredient {
|
||||||
|
quantity: "4 tbsp",
|
||||||
|
name: "olive oil",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "1",
|
||||||
|
name: "onion thinly sliced",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "4",
|
||||||
|
name: "cloves garlic\npeeled and sliced",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "1",
|
||||||
|
name: "small bay leaf",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "1",
|
||||||
|
name: "small eggplant cut\ninto 1/2 inch cubes",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "1",
|
||||||
|
name: "small zucchini halved\nlengthwise and cut\ninto thin slices",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "1",
|
||||||
|
name: "red bell pepper cut\ninto slivers",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "4",
|
||||||
|
name: "plum tomatoes\ncoarsely chopped",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "1 tsp",
|
||||||
|
name: "kosher salt",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "1/4 cup",
|
||||||
|
name: "shredded fresh basil\nleaves",
|
||||||
|
},
|
||||||
|
Ingredient {
|
||||||
|
quantity: "",
|
||||||
|
name: "freshly ground black\npepper",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct RecipeTab {
|
||||||
|
row_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecipeTab {
|
||||||
|
/// Select the previous item in the ingredients list (with wrap around)
|
||||||
|
pub fn prev(&mut self) {
|
||||||
|
self.row_index = self.row_index.saturating_add(INGREDIENTS.len() - 1) % INGREDIENTS.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the next item in the ingredients list (with wrap around)
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
self.row_index = self.row_index.saturating_add(1) % INGREDIENTS.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for RecipeTab {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
RgbSwatch.render(area, buf);
|
||||||
|
let area = area.inner(Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 2,
|
||||||
|
});
|
||||||
|
Clear.render(area, buf);
|
||||||
|
Block::new()
|
||||||
|
.title("Ratatouille Recipe".bold().white())
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.style(THEME.content)
|
||||||
|
.padding(Padding::new(1, 1, 2, 1))
|
||||||
|
.render(area, buf);
|
||||||
|
|
||||||
|
let scrollbar_area = Rect {
|
||||||
|
y: area.y + 2,
|
||||||
|
height: area.height - 3,
|
||||||
|
..area
|
||||||
|
};
|
||||||
|
render_scrollbar(self.row_index, scrollbar_area, buf);
|
||||||
|
|
||||||
|
let area = area.inner(Margin {
|
||||||
|
horizontal: 2,
|
||||||
|
vertical: 1,
|
||||||
|
});
|
||||||
|
let [recipe, ingredients] =
|
||||||
|
Layout::horizontal([Constraint::Length(44), Constraint::Min(0)]).areas(area);
|
||||||
|
|
||||||
|
render_recipe(recipe, buf);
|
||||||
|
render_ingredients(self.row_index, ingredients, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_recipe(area: Rect, buf: &mut Buffer) {
|
||||||
|
let lines = RECIPE
|
||||||
|
.iter()
|
||||||
|
.map(|(step, text)| Line::from(vec![step.white().bold(), text.gray()]))
|
||||||
|
.collect_vec();
|
||||||
|
Paragraph::new(lines)
|
||||||
|
.wrap(Wrap { trim: true })
|
||||||
|
.block(Block::new().padding(Padding::new(0, 1, 0, 0)))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
let mut state = TableState::default().with_selected(Some(selected_row));
|
||||||
|
let rows = INGREDIENTS.iter().copied();
|
||||||
|
let theme = THEME.recipe;
|
||||||
|
StatefulWidget::render(
|
||||||
|
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
|
||||||
|
.block(Block::new().style(theme.ingredients))
|
||||||
|
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
|
||||||
|
.row_highlight_style(Style::new().light_yellow()),
|
||||||
|
area,
|
||||||
|
buf,
|
||||||
|
&mut state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_scrollbar(position: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
let mut state = ScrollbarState::default()
|
||||||
|
.content_length(INGREDIENTS.len())
|
||||||
|
.viewport_content_length(6)
|
||||||
|
.position(position);
|
||||||
|
Scrollbar::new(ScrollbarOrientation::VerticalRight)
|
||||||
|
.begin_symbol(None)
|
||||||
|
.end_symbol(None)
|
||||||
|
.track_symbol(None)
|
||||||
|
.thumb_symbol("▐")
|
||||||
|
.render(area, buf, &mut state);
|
||||||
|
}
|
||||||
206
examples/apps/demo2/src/tabs/traceroute.rs
Normal file
206
examples/apps/demo2/src/tabs/traceroute.rs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||||
|
style::{Styled, Stylize},
|
||||||
|
symbols::Marker,
|
||||||
|
widgets::{
|
||||||
|
canvas::{self, Canvas, Map, MapResolution, Points},
|
||||||
|
Block, BorderType, Clear, Padding, Row, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||||
|
Sparkline, StatefulWidget, Table, TableState, Widget,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{RgbSwatch, THEME};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct TracerouteTab {
|
||||||
|
row_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TracerouteTab {
|
||||||
|
/// Select the previous row (with wrap around).
|
||||||
|
pub fn prev_row(&mut self) {
|
||||||
|
self.row_index = self.row_index.saturating_add(HOPS.len() - 1) % HOPS.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select the next row (with wrap around).
|
||||||
|
pub fn next_row(&mut self) {
|
||||||
|
self.row_index = self.row_index.saturating_add(1) % HOPS.len();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for TracerouteTab {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
RgbSwatch.render(area, buf);
|
||||||
|
let area = area.inner(Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 2,
|
||||||
|
});
|
||||||
|
Clear.render(area, buf);
|
||||||
|
Block::new().style(THEME.content).render(area, buf);
|
||||||
|
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
|
||||||
|
let vertical = Layout::vertical([Constraint::Min(0), Constraint::Length(3)]);
|
||||||
|
let [left, map] = horizontal.areas(area);
|
||||||
|
let [hops, pings] = vertical.areas(left);
|
||||||
|
|
||||||
|
render_hops(self.row_index, hops, buf);
|
||||||
|
render_ping(self.row_index, pings, buf);
|
||||||
|
render_map(self.row_index, map, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
let mut state = TableState::default().with_selected(Some(selected_row));
|
||||||
|
let rows = HOPS.iter().map(|hop| Row::new(vec![hop.host, hop.address]));
|
||||||
|
let block = Block::new()
|
||||||
|
.padding(Padding::new(1, 1, 1, 1))
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.title("Traceroute bad.horse".bold().white());
|
||||||
|
StatefulWidget::render(
|
||||||
|
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
|
||||||
|
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
|
||||||
|
.row_highlight_style(THEME.traceroute.selected)
|
||||||
|
.block(block),
|
||||||
|
area,
|
||||||
|
buf,
|
||||||
|
&mut state,
|
||||||
|
);
|
||||||
|
let mut scrollbar_state = ScrollbarState::default()
|
||||||
|
.content_length(HOPS.len())
|
||||||
|
.position(selected_row);
|
||||||
|
let area = Rect {
|
||||||
|
width: area.width + 1,
|
||||||
|
y: area.y + 3,
|
||||||
|
height: area.height - 4,
|
||||||
|
..area
|
||||||
|
};
|
||||||
|
Scrollbar::default()
|
||||||
|
.orientation(ScrollbarOrientation::VerticalLeft)
|
||||||
|
.begin_symbol(None)
|
||||||
|
.end_symbol(None)
|
||||||
|
.track_symbol(None)
|
||||||
|
.thumb_symbol("▌")
|
||||||
|
.render(area, buf, &mut scrollbar_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
let mut data = [
|
||||||
|
8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 2, 2, 3, 4, 5, 6, 7, 7, 8, 8, 8, 7,
|
||||||
|
7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 2, 4, 6, 7, 8, 8, 8, 8, 6, 4, 2, 1, 1, 1, 1, 2, 2, 2, 3,
|
||||||
|
3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7,
|
||||||
|
];
|
||||||
|
let mid = progress % data.len();
|
||||||
|
data.rotate_left(mid);
|
||||||
|
Sparkline::default()
|
||||||
|
.block(
|
||||||
|
Block::new()
|
||||||
|
.title("Ping")
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.border_type(BorderType::Thick),
|
||||||
|
)
|
||||||
|
.data(data)
|
||||||
|
.style(THEME.traceroute.ping)
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_map(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
let theme = THEME.traceroute.map;
|
||||||
|
let path: Option<(&Hop, &Hop)> = HOPS.iter().tuple_windows().nth(selected_row);
|
||||||
|
let map = Map {
|
||||||
|
resolution: MapResolution::High,
|
||||||
|
color: theme.color,
|
||||||
|
};
|
||||||
|
Canvas::default()
|
||||||
|
.background_color(theme.background_color)
|
||||||
|
.block(
|
||||||
|
Block::new()
|
||||||
|
.padding(Padding::new(1, 0, 1, 0))
|
||||||
|
.style(theme.style),
|
||||||
|
)
|
||||||
|
.marker(Marker::HalfBlock)
|
||||||
|
// picked to show Australia for the demo as it's the most interesting part of the map
|
||||||
|
// (and the only part with hops ;))
|
||||||
|
.x_bounds([112.0, 155.0])
|
||||||
|
.y_bounds([-46.0, -11.0])
|
||||||
|
.paint(|context| {
|
||||||
|
context.draw(&map);
|
||||||
|
if let Some(path) = path {
|
||||||
|
context.draw(&canvas::Line::new(
|
||||||
|
path.0.location.0,
|
||||||
|
path.0.location.1,
|
||||||
|
path.1.location.0,
|
||||||
|
path.1.location.1,
|
||||||
|
theme.path,
|
||||||
|
));
|
||||||
|
context.draw(&Points {
|
||||||
|
color: theme.source,
|
||||||
|
coords: &[path.0.location], // sydney
|
||||||
|
});
|
||||||
|
context.draw(&Points {
|
||||||
|
color: theme.destination,
|
||||||
|
coords: &[path.1.location], // perth
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Hop {
|
||||||
|
host: &'static str,
|
||||||
|
address: &'static str,
|
||||||
|
location: (f64, f64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Hop {
|
||||||
|
const fn new(name: &'static str, address: &'static str, location: (f64, f64)) -> Self {
|
||||||
|
Self {
|
||||||
|
host: name,
|
||||||
|
address,
|
||||||
|
location,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CANBERRA: (f64, f64) = (149.1, -35.3);
|
||||||
|
const SYDNEY: (f64, f64) = (151.1, -33.9);
|
||||||
|
const MELBOURNE: (f64, f64) = (144.9, -37.8);
|
||||||
|
const PERTH: (f64, f64) = (115.9, -31.9);
|
||||||
|
const DARWIN: (f64, f64) = (130.8, -12.4);
|
||||||
|
const BRISBANE: (f64, f64) = (153.0, -27.5);
|
||||||
|
const ADELAIDE: (f64, f64) = (138.6, -34.9);
|
||||||
|
|
||||||
|
// Go traceroute bad.horse some time, it's fun. these locations are made up and don't correspond
|
||||||
|
// to the actual IP addresses (which are in Toronto, Canada).
|
||||||
|
const HOPS: &[Hop] = &[
|
||||||
|
Hop::new("home", "127.0.0.1", CANBERRA),
|
||||||
|
Hop::new("bad.horse", "162.252.205.130", SYDNEY),
|
||||||
|
Hop::new("bad.horse", "162.252.205.131", MELBOURNE),
|
||||||
|
Hop::new("bad.horse", "162.252.205.132", BRISBANE),
|
||||||
|
Hop::new("bad.horse", "162.252.205.133", SYDNEY),
|
||||||
|
Hop::new("he.rides.across.the.nation", "162.252.205.134", PERTH),
|
||||||
|
Hop::new("the.thoroughbred.of.sin", "162.252.205.135", DARWIN),
|
||||||
|
Hop::new("he.got.the.application", "162.252.205.136", BRISBANE),
|
||||||
|
Hop::new("that.you.just.sent.in", "162.252.205.137", ADELAIDE),
|
||||||
|
Hop::new("it.needs.evaluation", "162.252.205.138", DARWIN),
|
||||||
|
Hop::new("so.let.the.games.begin", "162.252.205.139", PERTH),
|
||||||
|
Hop::new("a.heinous.crime", "162.252.205.140", BRISBANE),
|
||||||
|
Hop::new("a.show.of.force", "162.252.205.141", CANBERRA),
|
||||||
|
Hop::new("a.murder.would.be.nice.of.course", "162.252.205.142", PERTH),
|
||||||
|
Hop::new("bad.horse", "162.252.205.143", MELBOURNE),
|
||||||
|
Hop::new("bad.horse", "162.252.205.144", DARWIN),
|
||||||
|
Hop::new("bad.horse", "162.252.205.145", MELBOURNE),
|
||||||
|
Hop::new("he-s.bad", "162.252.205.146", PERTH),
|
||||||
|
Hop::new("the.evil.league.of.evil", "162.252.205.147", BRISBANE),
|
||||||
|
Hop::new("is.watching.so.beware", "162.252.205.148", DARWIN),
|
||||||
|
Hop::new("the.grade.that.you.receive", "162.252.205.149", PERTH),
|
||||||
|
Hop::new("will.be.your.last.we.swear", "162.252.205.150", ADELAIDE),
|
||||||
|
Hop::new("so.make.the.bad.horse.gleeful", "162.252.205.151", SYDNEY),
|
||||||
|
Hop::new("or.he-ll.make.you.his.mare", "162.252.205.152", MELBOURNE),
|
||||||
|
Hop::new("o_o", "162.252.205.153", BRISBANE),
|
||||||
|
Hop::new("you-re.saddled.up", "162.252.205.154", DARWIN),
|
||||||
|
Hop::new("there-s.no.recourse", "162.252.205.155", PERTH),
|
||||||
|
Hop::new("it-s.hi-ho.silver", "162.252.205.156", SYDNEY),
|
||||||
|
Hop::new("signed.bad.horse", "162.252.205.157", CANBERRA),
|
||||||
|
];
|
||||||
165
examples/apps/demo2/src/tabs/weather.rs
Normal file
165
examples/apps/demo2/src/tabs/weather.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
|
use palette::Okhsv;
|
||||||
|
use ratatui::{
|
||||||
|
buffer::Buffer,
|
||||||
|
layout::{Constraint, Direction, Layout, Margin, Rect},
|
||||||
|
style::{Color, Style, Stylize},
|
||||||
|
symbols,
|
||||||
|
widgets::{
|
||||||
|
calendar::{CalendarEventStore, Monthly},
|
||||||
|
Bar, BarChart, BarGroup, Block, Clear, LineGauge, Padding, Widget,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
use crate::{color_from_oklab, RgbSwatch, THEME};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct WeatherTab {
|
||||||
|
pub download_progress: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WeatherTab {
|
||||||
|
/// Simulate a download indicator by decrementing the row index.
|
||||||
|
pub fn prev(&mut self) {
|
||||||
|
self.download_progress = self.download_progress.saturating_sub(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simulate a download indicator by incrementing the row index.
|
||||||
|
pub fn next(&mut self) {
|
||||||
|
self.download_progress = self.download_progress.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for WeatherTab {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
RgbSwatch.render(area, buf);
|
||||||
|
let area = area.inner(Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 2,
|
||||||
|
});
|
||||||
|
Clear.render(area, buf);
|
||||||
|
Block::new().style(THEME.content).render(area, buf);
|
||||||
|
|
||||||
|
let area = area.inner(Margin {
|
||||||
|
horizontal: 2,
|
||||||
|
vertical: 1,
|
||||||
|
});
|
||||||
|
let [main, _, gauges] = Layout::vertical([
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
|
.areas(area);
|
||||||
|
let [calendar, charts] =
|
||||||
|
Layout::horizontal([Constraint::Length(23), Constraint::Min(0)]).areas(main);
|
||||||
|
let [simple, horizontal] =
|
||||||
|
Layout::vertical([Constraint::Length(29), Constraint::Min(0)]).areas(charts);
|
||||||
|
|
||||||
|
render_calendar(calendar, buf);
|
||||||
|
render_simple_barchart(simple, buf);
|
||||||
|
render_horizontal_barchart(horizontal, buf);
|
||||||
|
render_gauge(self.download_progress, gauges, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_calendar(area: Rect, buf: &mut Buffer) {
|
||||||
|
let date = OffsetDateTime::now_utc().date();
|
||||||
|
Monthly::new(date, CalendarEventStore::today(Style::new().red().bold()))
|
||||||
|
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
|
||||||
|
.show_month_header(Style::new().bold())
|
||||||
|
.show_weekdays_header(Style::new().italic())
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
|
||||||
|
let data = [
|
||||||
|
("Sat", 76),
|
||||||
|
("Sun", 69),
|
||||||
|
("Mon", 65),
|
||||||
|
("Tue", 67),
|
||||||
|
("Wed", 65),
|
||||||
|
("Thu", 69),
|
||||||
|
("Fri", 73),
|
||||||
|
];
|
||||||
|
let data = data
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, value)| {
|
||||||
|
Bar::default()
|
||||||
|
.value(value)
|
||||||
|
// This doesn't actually render correctly as the text is too wide for the bar
|
||||||
|
// See https://github.com/ratatui/ratatui/issues/513 for more info
|
||||||
|
// (the demo GIFs hack around this by hacking the calculation in bars.rs)
|
||||||
|
.text_value(format!("{value}°"))
|
||||||
|
.style(if value > 70 {
|
||||||
|
Style::new().fg(Color::Red)
|
||||||
|
} else {
|
||||||
|
Style::new().fg(Color::Yellow)
|
||||||
|
})
|
||||||
|
.value_style(if value > 70 {
|
||||||
|
Style::new().fg(Color::Gray).bg(Color::Red).bold()
|
||||||
|
} else {
|
||||||
|
Style::new().fg(Color::DarkGray).bg(Color::Yellow).bold()
|
||||||
|
})
|
||||||
|
.label(label)
|
||||||
|
})
|
||||||
|
.collect_vec();
|
||||||
|
let group = BarGroup::default().bars(&data);
|
||||||
|
BarChart::default()
|
||||||
|
.data(group)
|
||||||
|
.bar_width(3)
|
||||||
|
.bar_gap(1)
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
|
||||||
|
let bg = Color::Rgb(32, 48, 96);
|
||||||
|
let data = [
|
||||||
|
Bar::default().text_value("Winter 37-51").value(51),
|
||||||
|
Bar::default().text_value("Spring 40-65").value(65),
|
||||||
|
Bar::default().text_value("Summer 54-77").value(77),
|
||||||
|
Bar::default()
|
||||||
|
.text_value("Fall 41-71")
|
||||||
|
.value(71)
|
||||||
|
.value_style(Style::new().bold()), // current season
|
||||||
|
];
|
||||||
|
let group = BarGroup::default().label("GPU").bars(&data);
|
||||||
|
BarChart::default()
|
||||||
|
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.data(group)
|
||||||
|
.bar_gap(1)
|
||||||
|
.bar_style(Style::new().fg(bg))
|
||||||
|
.value_style(Style::new().bg(bg).fg(Color::Gray))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
pub fn render_gauge(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
let percent = (progress * 3).min(100) as f64;
|
||||||
|
|
||||||
|
render_line_gauge(percent, area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
||||||
|
// cycle color hue based on the percent for a neat effect yellow -> red
|
||||||
|
let hue = 90.0 - (percent as f32 * 0.6);
|
||||||
|
let value = Okhsv::max_value();
|
||||||
|
let filled_color = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||||
|
let unfilled_color = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||||
|
let label = if percent < 100.0 {
|
||||||
|
format!("Downloading: {percent}%")
|
||||||
|
} else {
|
||||||
|
"Download Complete!".into()
|
||||||
|
};
|
||||||
|
LineGauge::default()
|
||||||
|
.ratio(percent / 100.0)
|
||||||
|
.label(label)
|
||||||
|
.style(Style::new().light_blue())
|
||||||
|
.filled_style(Style::new().fg(filled_color))
|
||||||
|
.unfilled_style(Style::new().fg(unfilled_color))
|
||||||
|
.filled_symbol(symbols::line::THICK_HORIZONTAL)
|
||||||
|
.unfilled_symbol(symbols::line::THICK_HORIZONTAL)
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
132
examples/apps/demo2/src/theme.rs
Normal file
132
examples/apps/demo2/src/theme.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
|
|
||||||
|
pub struct Theme {
|
||||||
|
pub root: Style,
|
||||||
|
pub content: Style,
|
||||||
|
pub app_title: Style,
|
||||||
|
pub tabs: Style,
|
||||||
|
pub tabs_selected: Style,
|
||||||
|
pub borders: Style,
|
||||||
|
pub description: Style,
|
||||||
|
pub description_title: Style,
|
||||||
|
pub key_binding: KeyBinding,
|
||||||
|
pub logo: Logo,
|
||||||
|
pub email: Email,
|
||||||
|
pub traceroute: Traceroute,
|
||||||
|
pub recipe: Recipe,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct KeyBinding {
|
||||||
|
pub key: Style,
|
||||||
|
pub description: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Logo {
|
||||||
|
pub rat_eye: Color,
|
||||||
|
pub rat_eye_alt: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Email {
|
||||||
|
pub tabs: Style,
|
||||||
|
pub tabs_selected: Style,
|
||||||
|
pub inbox: Style,
|
||||||
|
pub item: Style,
|
||||||
|
pub selected_item: Style,
|
||||||
|
pub header: Style,
|
||||||
|
pub header_value: Style,
|
||||||
|
pub body: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Traceroute {
|
||||||
|
pub header: Style,
|
||||||
|
pub selected: Style,
|
||||||
|
pub ping: Style,
|
||||||
|
pub map: Map,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Map {
|
||||||
|
pub style: Style,
|
||||||
|
pub color: Color,
|
||||||
|
pub path: Color,
|
||||||
|
pub source: Color,
|
||||||
|
pub destination: Color,
|
||||||
|
pub background_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Recipe {
|
||||||
|
pub ingredients: Style,
|
||||||
|
pub ingredients_header: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const THEME: Theme = Theme {
|
||||||
|
root: Style::new().bg(DARK_BLUE),
|
||||||
|
content: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
|
||||||
|
app_title: Style::new()
|
||||||
|
.fg(WHITE)
|
||||||
|
.bg(DARK_BLUE)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
tabs: Style::new().fg(MID_GRAY).bg(DARK_BLUE),
|
||||||
|
tabs_selected: Style::new()
|
||||||
|
.fg(WHITE)
|
||||||
|
.bg(DARK_BLUE)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
.add_modifier(Modifier::REVERSED),
|
||||||
|
borders: Style::new().fg(LIGHT_GRAY),
|
||||||
|
description: Style::new().fg(LIGHT_GRAY).bg(DARK_BLUE),
|
||||||
|
description_title: Style::new().fg(LIGHT_GRAY).add_modifier(Modifier::BOLD),
|
||||||
|
logo: Logo {
|
||||||
|
rat_eye: BLACK,
|
||||||
|
rat_eye_alt: RED,
|
||||||
|
},
|
||||||
|
key_binding: KeyBinding {
|
||||||
|
key: Style::new().fg(BLACK).bg(DARK_GRAY),
|
||||||
|
description: Style::new().fg(DARK_GRAY).bg(BLACK),
|
||||||
|
},
|
||||||
|
email: Email {
|
||||||
|
tabs: Style::new().fg(MID_GRAY).bg(DARK_BLUE),
|
||||||
|
tabs_selected: Style::new()
|
||||||
|
.fg(WHITE)
|
||||||
|
.bg(DARK_BLUE)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
inbox: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
|
||||||
|
item: Style::new().fg(LIGHT_GRAY),
|
||||||
|
selected_item: Style::new().fg(LIGHT_YELLOW),
|
||||||
|
header: Style::new().add_modifier(Modifier::BOLD),
|
||||||
|
header_value: Style::new().fg(LIGHT_GRAY),
|
||||||
|
body: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
|
||||||
|
},
|
||||||
|
traceroute: Traceroute {
|
||||||
|
header: Style::new()
|
||||||
|
.bg(DARK_BLUE)
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
.add_modifier(Modifier::UNDERLINED),
|
||||||
|
selected: Style::new().fg(LIGHT_YELLOW),
|
||||||
|
ping: Style::new().fg(WHITE),
|
||||||
|
map: Map {
|
||||||
|
style: Style::new().bg(DARK_BLUE),
|
||||||
|
background_color: DARK_BLUE,
|
||||||
|
color: LIGHT_GRAY,
|
||||||
|
path: LIGHT_BLUE,
|
||||||
|
source: LIGHT_GREEN,
|
||||||
|
destination: LIGHT_RED,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
recipe: Recipe {
|
||||||
|
ingredients: Style::new().bg(DARK_BLUE).fg(LIGHT_GRAY),
|
||||||
|
ingredients_header: Style::new()
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
.add_modifier(Modifier::UNDERLINED),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DARK_BLUE: Color = Color::Rgb(16, 24, 48);
|
||||||
|
const LIGHT_BLUE: Color = Color::Rgb(64, 96, 192);
|
||||||
|
const LIGHT_YELLOW: Color = Color::Rgb(192, 192, 96);
|
||||||
|
const LIGHT_GREEN: Color = Color::Rgb(64, 192, 96);
|
||||||
|
const LIGHT_RED: Color = Color::Rgb(192, 96, 96);
|
||||||
|
const RED: Color = Color::Rgb(215, 0, 0);
|
||||||
|
const BLACK: Color = Color::Rgb(8, 8, 8); // not really black, often #080808
|
||||||
|
const DARK_GRAY: Color = Color::Rgb(68, 68, 68);
|
||||||
|
const MID_GRAY: Color = Color::Rgb(128, 128, 128);
|
||||||
|
const LIGHT_GRAY: Color = Color::Rgb(188, 188, 188);
|
||||||
|
const WHITE: Color = Color::Rgb(238, 238, 238); // not really white, often #eeeeee
|
||||||
15
examples/apps/flex/Cargo.toml
Normal file
15
examples/apps/flex/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "flex"
|
||||||
|
publish = false
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
color-eyre.workspace = true
|
||||||
|
crossterm.workspace = true
|
||||||
|
ratatui.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
9
examples/apps/flex/README.md
Normal file
9
examples/apps/flex/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Flex demo
|
||||||
|
|
||||||
|
This interactive example shows how to use the flex layouts.
|
||||||
|
|
||||||
|
To run this demo:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo run -p flex
|
||||||
|
```
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user