diff --git a/.gitignore b/.gitignore index bc0dfdb71..a133d7b14 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .pytype .DS_Store .vscode +.idea/ mypy_report docs/build docs/source/_build diff --git a/CHANGELOG.md b/CHANGELOG.md index b91cb3a4e..e3c093364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added ProgressColumn `MofNCompleteColumn` to display raw `completed/total` column (similar to DownloadColumn, but displays values as ints, does not convert to floats or add bit/bytes units). https://github.com/Textualize/rich/pull/1941 +- Remove Colorama dependency, call Windows Console API from Rich https://github.com/Textualize/rich/pull/1993 - Add support for namedtuples to `Pretty` https://github.com/Textualize/rich/pull/2031 ### Fixed diff --git a/mypy.ini b/mypy.ini index c61bcf0ac..acccf0825 100644 --- a/mypy.ini +++ b/mypy.ini @@ -9,8 +9,5 @@ ignore_missing_imports = True [mypy-commonmark.*] ignore_missing_imports = True -[mypy-colorama.*] -ignore_missing_imports = True - [mypy-ipywidgets.*] ignore_missing_imports = True diff --git a/poetry.lock b/poetry.lock index e7275a13e..0489808c6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -846,7 +846,7 @@ python-versions = "*" [[package]] name = "pywinpty" -version = "2.0.2" +version = "2.0.3" description = "Pseudo terminal support for Windows from Python." category = "main" optional = true @@ -986,7 +986,7 @@ python-versions = ">=3.6" [[package]] name = "virtualenv" -version = "20.13.2" +version = "20.13.3" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1049,7 +1049,7 @@ jupyter = ["ipywidgets"] [metadata] lock-version = "1.1" python-versions = "^3.6.2" -content-hash = "037fa9d7b0d26a13fb1fe08220d8b1afd33508e5d30111335bcfcc2958a04477" +content-hash = "05eba8d679f7fd74e1fb50881b99c09705bea9330a70beeead174c5b8fe6338c" [metadata.files] appnope = [ @@ -1330,12 +1330,28 @@ jupyterlab-widgets = [ {file = "jupyterlab_widgets-1.0.2.tar.gz", hash = "sha256:7885092b2b96bf189c3a705cc3c412a4472ec5e8382d0b47219a66cccae73cfa"}, ] markupsafe = [ + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, + {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, + {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, @@ -1344,14 +1360,27 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, + {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, + {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, @@ -1361,6 +1390,12 @@ markupsafe = [ {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, + {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, @@ -1533,11 +1568,11 @@ pywin32 = [ {file = "pywin32-303-cp39-cp39-win_amd64.whl", hash = "sha256:79cbb862c11b9af19bcb682891c1b91942ec2ff7de8151e2aea2e175899cda34"}, ] pywinpty = [ - {file = "pywinpty-2.0.2-cp310-none-win_amd64.whl", hash = "sha256:4b421379b407bf2f52a64a4c58f61deffe623b5add02d871acb290b771bb6227"}, - {file = "pywinpty-2.0.2-cp37-none-win_amd64.whl", hash = "sha256:238b75fc456a6bc558761a89c9e6b3c8f2f54d79db03ae28997a68313c24b2ca"}, - {file = "pywinpty-2.0.2-cp38-none-win_amd64.whl", hash = "sha256:344858a0b956fdc64a547d5e1980b0257b47f5433ed7cb89bf7b6268cb280c6c"}, - {file = "pywinpty-2.0.2-cp39-none-win_amd64.whl", hash = "sha256:a4a066eaf2e30944d3028d946883ceb7883a499b53c4b89ca2d54bd7a4210550"}, - {file = "pywinpty-2.0.2.tar.gz", hash = "sha256:20ec117183f79642eff555ce0dd1823f942618d65813fb6122d14b6e34b5d05a"}, + {file = "pywinpty-2.0.3-cp310-none-win_amd64.whl", hash = "sha256:7a330ef7a2ce284370b1a1fdd2a80c523585464fa5e5ab934c9f27220fa7feab"}, + {file = "pywinpty-2.0.3-cp37-none-win_amd64.whl", hash = "sha256:6455f1075f978942d318f95616661c605d5e0f991c5b176c0c852d237aafefc0"}, + {file = "pywinpty-2.0.3-cp38-none-win_amd64.whl", hash = "sha256:2e7a288a8121393c526d4e6ec7d65edef75d68c7787ab9560e438df867b75a5d"}, + {file = "pywinpty-2.0.3-cp39-none-win_amd64.whl", hash = "sha256:def51627e6aa659f33ea7a0ea4c6b68365c83af4aad7940600f844746817a0ed"}, + {file = "pywinpty-2.0.3.tar.gz", hash = "sha256:6b29a826e896105370c38d53904c3aaac6c36146a50448fc0ed5082cf9d092bc"}, ] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -1580,24 +1615,32 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea"}, {file = "pyzmq-22.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f907c7359ce8bf7f7e63c82f75ad0223384105f5126f313400b7e8004d9b33c3"}, + {file = "pyzmq-22.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:902319cfe23366595d3fa769b5b751e6ee6750a0a64c5d9f757d624b2ac3519e"}, {file = "pyzmq-22.3.0-cp310-cp310-win32.whl", hash = "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d"}, {file = "pyzmq-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd"}, {file = "pyzmq-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0"}, {file = "pyzmq-22.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:62bcade20813796c426409a3e7423862d50ff0639f5a2a95be4b85b09a618666"}, + {file = "pyzmq-22.3.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ea5a79e808baef98c48c884effce05c31a0698c1057de8fc1c688891043c1ce1"}, {file = "pyzmq-22.3.0-cp36-cp36m-win32.whl", hash = "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9"}, {file = "pyzmq-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b"}, {file = "pyzmq-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7"}, {file = "pyzmq-22.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08c4e315a76ef26eb833511ebf3fa87d182152adf43dedee8d79f998a2162a0b"}, + {file = "pyzmq-22.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:badb868fff14cfd0e200eaa845887b1011146a7d26d579aaa7f966c203736b92"}, {file = "pyzmq-22.3.0-cp37-cp37m-win32.whl", hash = "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d"}, {file = "pyzmq-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d"}, {file = "pyzmq-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8"}, {file = "pyzmq-22.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:468bd59a588e276961a918a3060948ae68f6ff5a7fa10bb2f9160c18fe341067"}, + {file = "pyzmq-22.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c88fa7410e9fc471e0858638f403739ee869924dd8e4ae26748496466e27ac59"}, {file = "pyzmq-22.3.0-cp38-cp38-win32.whl", hash = "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7"}, {file = "pyzmq-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45"}, {file = "pyzmq-22.3.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356"}, @@ -1605,6 +1648,8 @@ pyzmq = [ {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57"}, {file = "pyzmq-22.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:53f4fd13976789ffafedd4d46f954c7bb01146121812b72b4ddca286034df966"}, + {file = "pyzmq-22.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1b5d457acbadcf8b27561deeaa386b0217f47626b29672fa7bd31deb6e91e1b"}, {file = "pyzmq-22.3.0-cp39-cp39-win32.whl", hash = "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b"}, {file = "pyzmq-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2"}, {file = "pyzmq-22.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f"}, @@ -1719,8 +1764,8 @@ typing-extensions = [ {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] virtualenv = [ - {file = "virtualenv-20.13.2-py2.py3-none-any.whl", hash = "sha256:e7b34c9474e6476ee208c43a4d9ac1510b041c68347eabfe9a9ea0c86aa0a46b"}, - {file = "virtualenv-20.13.2.tar.gz", hash = "sha256:01f5f80744d24a3743ce61858123488e91cb2dd1d3bdf92adaf1bba39ffdedf0"}, + {file = "virtualenv-20.13.3-py2.py3-none-any.whl", hash = "sha256:dd448d1ded9f14d1a4bfa6bfc0c5b96ae3be3f2d6c6c159b23ddcfd701baa021"}, + {file = "virtualenv-20.13.3.tar.gz", hash = "sha256:e9dd1a1359d70137559034c0f5433b34caf504af2dc756367be86a5a32967134"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, diff --git a/pyproject.toml b/pyproject.toml index feee94c8c..f5e1a96e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ typing-extensions = { version = ">=3.7.4, <5.0", python = "<3.8" } dataclasses = { version = ">=0.7,<0.9", python = "<3.7" } pygments = "^2.6.0" commonmark = "^0.9.0" -colorama = "^0.4.0" ipywidgets = { version = "^7.5.1", optional = true } diff --git a/rich/__main__.py b/rich/__main__.py index ea6ccfd9b..7b3ffb4f2 100644 --- a/rich/__main__.py +++ b/rich/__main__.py @@ -226,10 +226,7 @@ def iter_last(values: Iterable[T]) -> Iterable[Tuple[bool, T]]: console.print(test_card) taken = round((process_time() - start) * 1000.0, 1) - text = console.file.getvalue() - # https://bugs.python.org/issue37871 - for line in text.splitlines(True): - print(line, end="") + Console().print(test_card) print(f"rendered in {pre_cache_taken}ms (cold cache)") print(f"rendered in {taken}ms (warm cache)") diff --git a/rich/_win32_console.py b/rich/_win32_console.py new file mode 100644 index 000000000..c88287564 --- /dev/null +++ b/rich/_win32_console.py @@ -0,0 +1,658 @@ +"""Light wrapper around the Win32 Console API - this module should only be imported on Windows + +The API that this module wraps is documented at https://docs.microsoft.com/en-us/windows/console/console-functions +""" +import ctypes +import sys +from typing import IO, Any, NamedTuple, Type, cast + +windll: Any = None +if sys.platform == "win32": + windll = ctypes.LibraryLoader(ctypes.WinDLL) +else: + raise ImportError(f"{__name__} can only be imported on Windows") + +import time +from ctypes import Structure, byref, wintypes + +from rich.color import ColorSystem +from rich.style import Style + +STDOUT = -11 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 + +COORD = wintypes._COORD + + +class LegacyWindowsError(Exception): + pass + + +class WindowsCoordinates(NamedTuple): + """Coordinates in the Windows Console API are (y, x), not (x, y). + This class is intended to prevent that confusion. + Rows and columns are indexed from 0. + This class can be used in place of wintypes._COORD in arguments and argtypes. + """ + + row: int + col: int + + @classmethod + def from_param(cls, value: "WindowsCoordinates") -> COORD: + """Converts a WindowsCoordinates into a wintypes _COORD structure. + This classmethod is internally called by ctypes to perform the conversion. + + Args: + value (WindowsCoordinates): The input coordinates to convert. + + Returns: + wintypes._COORD: The converted coordinates struct. + """ + return COORD(value.col, value.row) + + +class CONSOLE_SCREEN_BUFFER_INFO(Structure): + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", wintypes.SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] + + +class CONSOLE_CURSOR_INFO(ctypes.Structure): + _fields_ = [("dwSize", wintypes.DWORD), ("bVisible", wintypes.BOOL)] + + +_GetStdHandle = windll.kernel32.GetStdHandle +_GetStdHandle.argtypes = [ + wintypes.DWORD, +] +_GetStdHandle.restype = wintypes.HANDLE + + +def GetStdHandle(handle: int = STDOUT) -> wintypes.HANDLE: + """Retrieves a handle to the specified standard device (standard input, standard output, or standard error). + + Args: + handle (int): Integer identifier for the handle. Defaults to -11 (stdout). + + Returns: + wintypes.HANDLE: The handle + """ + return cast(wintypes.HANDLE, _GetStdHandle(handle)) + + +_GetConsoleMode = windll.kernel32.GetConsoleMode +_GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] +_GetConsoleMode.restype = wintypes.BOOL + + +def GetConsoleMode(std_handle: wintypes.HANDLE) -> int: + """Retrieves the current input mode of a console's input buffer + or the current output mode of a console screen buffer. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + + Raises: + LegacyWindowsError: If any error occurs while calling the Windows console API. + + Returns: + int: Value representing the current console mode as documented at + https://docs.microsoft.com/en-us/windows/console/getconsolemode#parameters + """ + + console_mode = wintypes.DWORD() + success = bool(_GetConsoleMode(std_handle, console_mode)) + if not success: + raise LegacyWindowsError("Unable to get legacy Windows Console Mode") + return console_mode.value + + +_FillConsoleOutputCharacterW = windll.kernel32.FillConsoleOutputCharacterW +_FillConsoleOutputCharacterW.argtypes = [ + wintypes.HANDLE, + ctypes.c_char, + wintypes.DWORD, + cast(Type[COORD], WindowsCoordinates), + ctypes.POINTER(wintypes.DWORD), +] +_FillConsoleOutputCharacterW.restype = wintypes.BOOL + + +def FillConsoleOutputCharacter( + std_handle: wintypes.HANDLE, + char: str, + length: int, + start: WindowsCoordinates, +) -> int: + """Writes a character to the console screen buffer a specified number of times, beginning at the specified coordinates. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + char (str): The character to write. Must be a string of length 1. + length (int): The number of times to write the character. + start (WindowsCoordinates): The coordinates to start writing at. + + Returns: + int: The number of characters written. + """ + character = ctypes.c_char(char.encode()) + num_characters = wintypes.DWORD(length) + num_written = wintypes.DWORD(0) + _FillConsoleOutputCharacterW( + std_handle, + character, + num_characters, + start, + byref(num_written), + ) + return num_written.value + + +_FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute +_FillConsoleOutputAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + wintypes.DWORD, + cast(Type[COORD], WindowsCoordinates), + ctypes.POINTER(wintypes.DWORD), +] +_FillConsoleOutputAttribute.restype = wintypes.BOOL + + +def FillConsoleOutputAttribute( + std_handle: wintypes.HANDLE, + attributes: int, + length: int, + start: WindowsCoordinates, +) -> int: + """Sets the character attributes for a specified number of character cells, + beginning at the specified coordinates in a screen buffer. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + attributes (int): Integer value representing the foreground and background colours of the cells. + length (int): The number of cells to set the output attribute of. + start (WindowsCoordinates): The coordinates of the first cell whose attributes are to be set. + + Returns: + int: The number of cells whose attributes were actually set. + """ + num_cells = wintypes.DWORD(length) + style_attrs = wintypes.WORD(attributes) + num_written = wintypes.DWORD(0) + _FillConsoleOutputAttribute( + std_handle, style_attrs, num_cells, start, byref(num_written) + ) + return num_written.value + + +_SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute +_SetConsoleTextAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, +] +_SetConsoleTextAttribute.restype = wintypes.BOOL + + +def SetConsoleTextAttribute( + std_handle: wintypes.HANDLE, attributes: wintypes.WORD +) -> bool: + """Set the colour attributes for all text written after this function is called. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + attributes (int): Integer value representing the foreground and background colours. + + + Returns: + bool: True if the attribute was set successfully, otherwise False. + """ + return bool(_SetConsoleTextAttribute(std_handle, attributes)) + + +_GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo +_GetConsoleScreenBufferInfo.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO), +] +_GetConsoleScreenBufferInfo.restype = wintypes.BOOL + + +def GetConsoleScreenBufferInfo( + std_handle: wintypes.HANDLE, +) -> CONSOLE_SCREEN_BUFFER_INFO: + """Retrieves information about the specified console screen buffer. + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + + Returns: + CONSOLE_SCREEN_BUFFER_INFO: A CONSOLE_SCREEN_BUFFER_INFO ctype struct contain information about + screen size, cursor position, colour attributes, and more.""" + console_screen_buffer_info = CONSOLE_SCREEN_BUFFER_INFO() + _GetConsoleScreenBufferInfo(std_handle, byref(console_screen_buffer_info)) + return console_screen_buffer_info + + +_SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition +_SetConsoleCursorPosition.argtypes = [ + wintypes.HANDLE, + cast(Type[COORD], WindowsCoordinates), +] +_SetConsoleCursorPosition.restype = wintypes.BOOL + + +def SetConsoleCursorPosition( + std_handle: wintypes.HANDLE, coords: WindowsCoordinates +) -> bool: + """Set the position of the cursor in the console screen + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + coords (WindowsCoordinates): The coordinates to move the cursor to. + + Returns: + bool: True if the function succeeds, otherwise False. + """ + return bool(_SetConsoleCursorPosition(std_handle, coords)) + + +_SetConsoleCursorInfo = windll.kernel32.SetConsoleCursorInfo +_SetConsoleCursorInfo.argtypes = [ + wintypes.HANDLE, + ctypes.POINTER(CONSOLE_CURSOR_INFO), +] +_SetConsoleCursorInfo.restype = wintypes.BOOL + + +def SetConsoleCursorInfo( + std_handle: wintypes.HANDLE, cursor_info: CONSOLE_CURSOR_INFO +) -> bool: + """Set the cursor info - used for adjusting cursor visibility and width + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + cursor_info (CONSOLE_CURSOR_INFO): CONSOLE_CURSOR_INFO ctype struct containing the new cursor info. + + Returns: + bool: True if the function succeeds, otherwise False. + """ + return bool(_SetConsoleCursorInfo(std_handle, byref(cursor_info))) + + +_SetConsoleTitle = windll.kernel32.SetConsoleTitleW +_SetConsoleTitle.argtypes = [wintypes.LPCWSTR] +_SetConsoleTitle.restype = wintypes.BOOL + + +def SetConsoleTitle(title: str) -> bool: + """Sets the title of the current console window + + Args: + title (str): The new title of the console window. + + Returns: + bool: True if the function succeeds, otherwise False. + """ + return bool(_SetConsoleTitle(title)) + + +_WriteConsole = windll.kernel32.WriteConsoleW +_WriteConsole.argtypes = [ + wintypes.HANDLE, + wintypes.LPWSTR, + wintypes.DWORD, + wintypes.LPDWORD, + wintypes.LPVOID, +] +_WriteConsole.restype = wintypes.BOOL + + +def WriteConsole(std_handle: wintypes.HANDLE, text: str) -> bool: + """Write a string of text to the console, starting at the current cursor position + + Args: + std_handle (wintypes.HANDLE): A handle to the console input buffer or the console screen buffer. + text (str): The text to write. + + Returns: + bool: True if the function succeeds, otherwise False. + """ + buffer = wintypes.LPWSTR(text) + num_chars_written = wintypes.LPDWORD() + return bool( + _WriteConsole( + std_handle, + buffer, + wintypes.DWORD(len(text)), + num_chars_written, + wintypes.LPVOID(None), + ) + ) + + +class LegacyWindowsTerm: + """This class allows interaction with the legacy Windows Console API. It should only be used in the context + of environments where virtual terminal processing is not available. However, if it is used in a Windows environment, + the entire API should work. + + Args: + file (IO[str]): The file which the Windows Console API HANDLE is retrieved from, defaults to sys.stdout. + """ + + BRIGHT_BIT = 8 + + # Indices are ANSI color numbers, values are the corresponding Windows Console API color numbers + ANSI_TO_WINDOWS = [ + 0, # black The Windows colours are defined in wincon.h as follows: + 4, # red define FOREGROUND_BLUE 0x0001 -- 0000 0001 + 2, # green define FOREGROUND_GREEN 0x0002 -- 0000 0010 + 6, # yellow define FOREGROUND_RED 0x0004 -- 0000 0100 + 1, # blue define FOREGROUND_INTENSITY 0x0008 -- 0000 1000 + 5, # magenta define BACKGROUND_BLUE 0x0010 -- 0001 0000 + 3, # cyan define BACKGROUND_GREEN 0x0020 -- 0010 0000 + 7, # white define BACKGROUND_RED 0x0040 -- 0100 0000 + 8, # bright black (grey) define BACKGROUND_INTENSITY 0x0080 -- 1000 0000 + 12, # bright red + 10, # bright green + 14, # bright yellow + 9, # bright blue + 13, # bright magenta + 11, # bright cyan + 15, # bright white + ] + + def __init__(self) -> None: + handle = GetStdHandle(STDOUT) + self._handle = handle + default_text = GetConsoleScreenBufferInfo(handle).wAttributes + self._default_text = default_text + + self._default_fore = default_text & 7 + self._default_back = (default_text >> 4) & 7 + self._default_attrs = self._default_fore | (self._default_back << 4) + + @property + def cursor_position(self) -> WindowsCoordinates: + """Returns the current position of the cursor (0-based) + + Returns: + WindowsCoordinates: The current cursor position. + """ + coord: COORD = GetConsoleScreenBufferInfo(self._handle).dwCursorPosition + return WindowsCoordinates(row=cast(int, coord.Y), col=cast(int, coord.X)) + + @property + def screen_size(self) -> WindowsCoordinates: + """Returns the current size of the console screen buffer, in character columns and rows + + Returns: + WindowsCoordinates: The width and height of the screen as WindowsCoordinates. + """ + screen_size: COORD = GetConsoleScreenBufferInfo(self._handle).dwSize + return WindowsCoordinates( + row=cast(int, screen_size.Y), col=cast(int, screen_size.X) + ) + + def write_text(self, text: str) -> None: + """Write text directly to the terminal without any modification of styles + + Args: + text (str): The text to write to the console + """ + WriteConsole(self._handle, text) + + def write_styled(self, text: str, style: Style) -> None: + """Write styled text to the terminal. + + Args: + text (str): The text to write + style (Style): The style of the text + """ + color = style.color + bgcolor = style.bgcolor + if style.reverse: + color, bgcolor = bgcolor, color + + if color: + fore = color.downgrade(ColorSystem.WINDOWS).number + fore = fore if fore is not None else 7 # Default to ANSI 7: White + if style.bold: + fore = fore | self.BRIGHT_BIT + if style.dim: + fore = fore & ~self.BRIGHT_BIT + fore = self.ANSI_TO_WINDOWS[fore] + else: + fore = self._default_fore + + if bgcolor: + back = bgcolor.downgrade(ColorSystem.WINDOWS).number + back = back if back is not None else 0 # Default to ANSI 0: Black + back = self.ANSI_TO_WINDOWS[back] + else: + back = self._default_back + + assert fore is not None + assert back is not None + + SetConsoleTextAttribute( + self._handle, attributes=ctypes.c_ushort(fore | (back << 4)) + ) + self.write_text(text) + SetConsoleTextAttribute(self._handle, attributes=self._default_text) + + def move_cursor_to(self, new_position: WindowsCoordinates) -> None: + """Set the position of the cursor + + Args: + new_position (WindowsCoordinates): The WindowsCoordinates representing the new position of the cursor. + """ + if new_position.col < 0 or new_position.row < 0: + return + SetConsoleCursorPosition(self._handle, coords=new_position) + + def erase_line(self) -> None: + """Erase all content on the line the cursor is currently located at""" + screen_size = self.screen_size + cursor_position = self.cursor_position + cells_to_erase = screen_size.col + start_coordinates = WindowsCoordinates(row=cursor_position.row, col=0) + FillConsoleOutputCharacter( + self._handle, " ", length=cells_to_erase, start=start_coordinates + ) + FillConsoleOutputAttribute( + self._handle, + self._default_attrs, + length=cells_to_erase, + start=start_coordinates, + ) + + def erase_end_of_line(self) -> None: + """Erase all content from the cursor position to the end of that line""" + cursor_position = self.cursor_position + cells_to_erase = self.screen_size.col - cursor_position.col + FillConsoleOutputCharacter( + self._handle, " ", length=cells_to_erase, start=cursor_position + ) + FillConsoleOutputAttribute( + self._handle, + self._default_attrs, + length=cells_to_erase, + start=cursor_position, + ) + + def erase_start_of_line(self) -> None: + """Erase all content from the cursor position to the start of that line""" + row, col = self.cursor_position + start = WindowsCoordinates(row, 0) + FillConsoleOutputCharacter(self._handle, " ", length=col, start=start) + FillConsoleOutputAttribute( + self._handle, self._default_attrs, length=col, start=start + ) + + def move_cursor_up(self) -> None: + """Move the cursor up a single cell""" + cursor_position = self.cursor_position + SetConsoleCursorPosition( + self._handle, + coords=WindowsCoordinates( + row=cursor_position.row - 1, col=cursor_position.col + ), + ) + + def move_cursor_down(self) -> None: + """Move the cursor down a single cell""" + cursor_position = self.cursor_position + SetConsoleCursorPosition( + self._handle, + coords=WindowsCoordinates( + row=cursor_position.row + 1, + col=cursor_position.col, + ), + ) + + def move_cursor_forward(self) -> None: + """Move the cursor forward a single cell. Wrap to the next line if required.""" + row, col = self.cursor_position + if col == self.screen_size.col - 1: + row += 1 + col = 0 + else: + col += 1 + SetConsoleCursorPosition( + self._handle, coords=WindowsCoordinates(row=row, col=col) + ) + + def move_cursor_to_column(self, column: int) -> None: + """Move cursor to the column specified by the zero-based column index, staying on the same row + + Args: + column (int): The zero-based column index to move the cursor to. + """ + row, _ = self.cursor_position + SetConsoleCursorPosition(self._handle, coords=WindowsCoordinates(row, column)) + + def move_cursor_backward(self) -> None: + """Move the cursor backward a single cell. Wrap to the previous line if required.""" + row, col = self.cursor_position + if col == 0: + row -= 1 + col = self.screen_size.col - 1 + else: + col -= 1 + SetConsoleCursorPosition( + self._handle, coords=WindowsCoordinates(row=row, col=col) + ) + + def hide_cursor(self) -> None: + """Hide the cursor""" + invisible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=0) + SetConsoleCursorInfo(self._handle, cursor_info=invisible_cursor) + + def show_cursor(self) -> None: + """Show the cursor""" + visible_cursor = CONSOLE_CURSOR_INFO(dwSize=100, bVisible=1) + SetConsoleCursorInfo(self._handle, cursor_info=visible_cursor) + + def set_title(self, title: str) -> None: + """Set the title of the terminal window + + Args: + title (str): The new title of the console window + """ + assert len(title) < 255, "Console title must be less than 255 characters" + SetConsoleTitle(title) + + +if __name__ == "__main__": + handle = GetStdHandle() + + from rich.console import Console + + console = Console() + + term = LegacyWindowsTerm() + term.set_title("Win32 Console Examples") + + style = Style(color="black", bgcolor="red") + + heading = Style.parse("black on green") + + # Check colour output + console.rule("Checking colour output") + console.print("[on red]on red!") + console.print("[blue]blue!") + console.print("[yellow]yellow!") + console.print("[bold yellow]bold yellow!") + console.print("[bright_yellow]bright_yellow!") + console.print("[dim bright_yellow]dim bright_yellow!") + console.print("[italic cyan]italic cyan!") + console.print("[bold white on blue]bold white on blue!") + console.print("[reverse bold white on blue]reverse bold white on blue!") + console.print("[bold black on cyan]bold black on cyan!") + console.print("[black on green]black on green!") + console.print("[blue on green]blue on green!") + console.print("[white on black]white on black!") + console.print("[black on white]black on white!") + console.print("[#1BB152 on #DA812D]#1BB152 on #DA812D!") + + # Check cursor movement + console.rule("Checking cursor movement") + console.print() + term.move_cursor_backward() + term.move_cursor_backward() + term.write_text("went back and wrapped to prev line") + time.sleep(1) + term.move_cursor_up() + term.write_text("we go up") + time.sleep(1) + term.move_cursor_down() + term.write_text("and down") + time.sleep(1) + term.move_cursor_up() + term.move_cursor_backward() + term.move_cursor_backward() + term.write_text("we went up and back 2") + time.sleep(1) + term.move_cursor_down() + term.move_cursor_backward() + term.move_cursor_backward() + term.write_text("we went down and back 2") + time.sleep(1) + + # Check erasing of lines + term.hide_cursor() + console.print() + console.rule("Checking line erasing") + console.print("\n...Deleting to the start of the line...") + term.write_text("The red arrow shows the cursor location, and direction of erase") + time.sleep(1) + term.move_cursor_to_column(16) + term.write_styled("<", Style.parse("black on red")) + term.move_cursor_backward() + time.sleep(1) + term.erase_start_of_line() + time.sleep(1) + + console.print("\n\n...And to the end of the line...") + term.write_text("The red arrow shows the cursor location, and direction of erase") + time.sleep(1) + + term.move_cursor_to_column(16) + term.write_styled(">", Style.parse("black on red")) + time.sleep(1) + term.erase_end_of_line() + time.sleep(1) + + console.print("\n\n...Now the whole line will be erased...") + term.write_styled("I'm going to disappear!", style=Style.parse("black on cyan")) + time.sleep(1) + term.erase_line() + + term.show_cursor() + print("\n") diff --git a/rich/_windows.py b/rich/_windows.py index b1b30b65e..54d834e62 100644 --- a/rich/_windows.py +++ b/rich/_windows.py @@ -21,6 +21,14 @@ class WindowsConsoleFeatures: else: windll = None raise ImportError("Not windows") + + from rich._win32_console import ( + ENABLE_VIRTUAL_TERMINAL_PROCESSING, + GetConsoleMode, + GetStdHandle, + LegacyWindowsError, + ) + except (AttributeError, ImportError, ValueError): # Fallback if we can't load the Windows DLL @@ -30,28 +38,20 @@ def get_windows_console_features() -> WindowsConsoleFeatures: else: - STDOUT = -11 - ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4 - _GetConsoleMode = windll.kernel32.GetConsoleMode - _GetConsoleMode.argtypes = [wintypes.HANDLE, wintypes.LPDWORD] - _GetConsoleMode.restype = wintypes.BOOL - - _GetStdHandle = windll.kernel32.GetStdHandle - _GetStdHandle.argtypes = [ - wintypes.DWORD, - ] - _GetStdHandle.restype = wintypes.HANDLE - def get_windows_console_features() -> WindowsConsoleFeatures: """Get windows console features. Returns: WindowsConsoleFeatures: An instance of WindowsConsoleFeatures. """ - handle = _GetStdHandle(STDOUT) - console_mode = wintypes.DWORD() - result = _GetConsoleMode(handle, console_mode) - vt = bool(result and console_mode.value & ENABLE_VIRTUAL_TERMINAL_PROCESSING) + handle = GetStdHandle() + try: + console_mode = GetConsoleMode(handle) + success = True + except LegacyWindowsError: + console_mode = 0 + success = False + vt = bool(success and console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) truecolor = False if vt: win_version = sys.getwindowsversion() diff --git a/rich/_windows_renderer.py b/rich/_windows_renderer.py new file mode 100644 index 000000000..e9debe30f --- /dev/null +++ b/rich/_windows_renderer.py @@ -0,0 +1,55 @@ +from typing import Iterable, Sequence, Tuple, cast + +from rich._win32_console import LegacyWindowsTerm, WindowsCoordinates +from rich.segment import ControlCode, ControlType, Segment + + +def legacy_windows_render(buffer: Iterable[Segment], term: LegacyWindowsTerm) -> None: + """Makes appropriate Windows Console API calls based on the segments in the buffer. + + Args: + buffer (Iterable[Segment]): Iterable of Segments to convert to Win32 API calls. + term (LegacyWindowsTerm): Used to call the Windows Console API. + """ + for segment in buffer: + text, style, control = segment + + if not control: + if style: + term.write_styled(text, style) + else: + term.write_text(text) + else: + control_codes: Sequence[ControlCode] = control + for control_code in control_codes: + control_type = control_code[0] + if control_type == ControlType.CURSOR_MOVE_TO: + _, x, y = cast(Tuple[ControlType, int, int], control_code) + term.move_cursor_to(WindowsCoordinates(row=y - 1, col=x - 1)) + elif control_type == ControlType.CARRIAGE_RETURN: + term.write_text("\r") + elif control_type == ControlType.HOME: + term.move_cursor_to(WindowsCoordinates(0, 0)) + elif control_type == ControlType.CURSOR_UP: + term.move_cursor_up() + elif control_type == ControlType.CURSOR_DOWN: + term.move_cursor_down() + elif control_type == ControlType.CURSOR_FORWARD: + term.move_cursor_forward() + elif control_type == ControlType.CURSOR_BACKWARD: + term.move_cursor_backward() + elif control_type == ControlType.CURSOR_MOVE_TO_COLUMN: + _, column = cast(Tuple[ControlType, int], control_code) + term.move_cursor_to_column(column - 1) + elif control_type == ControlType.HIDE_CURSOR: + term.hide_cursor() + elif control_type == ControlType.SHOW_CURSOR: + term.show_cursor() + elif control_type == ControlType.ERASE_IN_LINE: + _, mode = cast(Tuple[ControlType, int], control_code) + if mode == 0: + term.erase_end_of_line() + elif mode == 1: + term.erase_start_of_line() + elif mode == 2: + term.erase_line() diff --git a/rich/console.py b/rich/console.py index 1eb2e3eeb..d9564abed 100644 --- a/rich/console.py +++ b/rich/console.py @@ -1,4 +1,5 @@ import inspect +import io import os import platform import sys @@ -11,7 +12,6 @@ from html import escape from inspect import isclass from itertools import islice -from threading import RLock from time import monotonic from types import FrameType, ModuleType, TracebackType from typing import ( @@ -571,12 +571,6 @@ def detect_legacy_windows() -> bool: return WINDOWS and not get_windows_console_features().vt -if detect_legacy_windows(): # pragma: no cover - from colorama import init - - init(strip=False) - - class Console: """A high level console interface. @@ -1141,7 +1135,7 @@ def show_cursor(self, show: bool = True) -> bool: Args: show (bool, optional): Set visibility of the cursor. """ - if self.is_terminal and not self.legacy_windows: + if self.is_terminal: self.control(Control.show_cursor(show)) return True return False @@ -1916,22 +1910,41 @@ def _check_buffer(self) -> None: display(self._buffer, self._render_buffer(self._buffer[:])) del self._buffer[:] else: - text = self._render_buffer(self._buffer[:]) - del self._buffer[:] - if text: + if WINDOWS: try: - if WINDOWS: # pragma: no cover - # https://bugs.python.org/issue37871 - write = self.file.write - for line in text.splitlines(True): + file_no = self.file.fileno() + except (ValueError, io.UnsupportedOperation): + file_no = -1 + + legacy_windows_stdout = self.legacy_windows and file_no == 1 + if legacy_windows_stdout: + from rich._win32_console import LegacyWindowsTerm + from rich._windows_renderer import legacy_windows_render + + legacy_windows_render(self._buffer[:], LegacyWindowsTerm()) + + output_capture_enabled = bool(self._buffer_index) + if not legacy_windows_stdout or output_capture_enabled: + text = self._render_buffer(self._buffer[:]) + # https://bugs.python.org/issue37871 + write = self.file.write + for line in text.splitlines(True): + try: write(line) - else: - self.file.write(text) - self.file.flush() + except UnicodeEncodeError as error: + error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" + raise + else: + text = self._render_buffer(self._buffer[:]) + try: + self.file.write(text) except UnicodeEncodeError as error: error.reason = f"{error.reason}\n*** You may need to add PYTHONIOENCODING=utf-8 to your environment ***" raise + self.file.flush() + del self._buffer[:] + def _render_buffer(self, buffer: Iterable[Segment]) -> str: """Render buffered output, and clear buffer.""" output: List[str] = [] diff --git a/rich/segment.py b/rich/segment.py index 97679cefc..eb44acf11 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -64,7 +64,7 @@ class Segment(NamedTuple): Args: text (str): A piece of text. style (:class:`~rich.style.Style`, optional): An optional style to apply to the text. - control (Tuple[ControlCode..], optional): Optional sequence of control codes. + control (Sequence[ControlCode], optional): Optional sequence of control codes. """ text: str = "" diff --git a/tests/test_win32_console.py b/tests/test_win32_console.py new file mode 100644 index 000000000..4523600d6 --- /dev/null +++ b/tests/test_win32_console.py @@ -0,0 +1,438 @@ +import dataclasses +import sys +from unittest import mock +from unittest.mock import patch + +import pytest + +from rich.style import Style + +if sys.platform == "win32": + from rich import _win32_console + from rich._win32_console import COORD, LegacyWindowsTerm, WindowsCoordinates + + CURSOR_X = 1 + CURSOR_Y = 2 + CURSOR_POSITION = WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) + SCREEN_WIDTH = 20 + SCREEN_HEIGHT = 30 + DEFAULT_STYLE_ATTRIBUTE = 16 + + @dataclasses.dataclass + class StubScreenBufferInfo: + dwCursorPosition: COORD = COORD(CURSOR_X, CURSOR_Y) + dwSize: COORD = COORD(SCREEN_WIDTH, SCREEN_HEIGHT) + wAttributes: int = DEFAULT_STYLE_ATTRIBUTE + + pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only") + + def test_windows_coordinates_to_ctype(): + coord = WindowsCoordinates.from_param(WindowsCoordinates(row=1, col=2)) + assert coord.X == 2 + assert coord.Y == 1 + + @pytest.fixture + def win32_handle(): + handle = mock.sentinel + with mock.patch.object(_win32_console, "GetStdHandle", return_value=handle): + yield handle + + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_cursor_position(_): + term = LegacyWindowsTerm() + assert term.cursor_position == WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X) + + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_screen_size(_): + term = LegacyWindowsTerm() + assert term.screen_size == WindowsCoordinates( + row=SCREEN_HEIGHT, col=SCREEN_WIDTH + ) + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_text(_, WriteConsole, win32_handle): + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_text(text) + + WriteConsole.assert_called_once_with(win32_handle, text) + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled(_, SetConsoleTextAttribute, WriteConsole, win32_handle): + style = Style.parse("black on red") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + # Check that we've called the Console API to write the text + call_args = WriteConsole.call_args_list + assert len(call_args) == 1 + args, _ = call_args[0] + assert args == (win32_handle, text) + + # Ensure we set the text attributes and then reset them after writing styled text + call_args = SetConsoleTextAttribute.call_args_list + assert len(call_args) == 2 + first_args, first_kwargs = call_args[0] + second_args, second_kwargs = call_args[1] + + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == 64 + assert second_args == (win32_handle,) + assert second_kwargs["attributes"] == DEFAULT_STYLE_ATTRIBUTE + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_bold(_, SetConsoleTextAttribute, __, win32_handle): + style = Style.parse("bold black on red") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = 64 + 8 # 64 for red bg, +8 for bright black + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_reverse(_, SetConsoleTextAttribute, __, win32_handle): + style = Style.parse("reverse red on blue") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = 64 + 1 # 64 for red bg (after reverse), +1 for blue fg + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_reverse(_, SetConsoleTextAttribute, __, win32_handle): + style = Style.parse("dim bright_red on blue") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = 4 + 16 # 4 for red text (after dim), +16 for blue bg + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_no_foreground_color( + _, SetConsoleTextAttribute, __, win32_handle + ): + style = Style.parse("on blue") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = 16 | term._default_fore # 16 for blue bg, plus default fg color + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + + @patch.object(_win32_console, "WriteConsole", return_value=True) + @patch.object(_win32_console, "SetConsoleTextAttribute") + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_write_styled_no_background_color( + _, SetConsoleTextAttribute, __, win32_handle + ): + style = Style.parse("blue") + text = "Hello, world!" + term = LegacyWindowsTerm() + + term.write_styled(text, style) + + call_args = SetConsoleTextAttribute.call_args_list + first_args, first_kwargs = call_args[0] + + expected_attr = ( + 16 | term._default_back + ) # 16 for blue foreground, plus default bg color + assert first_args == (win32_handle,) + assert first_kwargs["attributes"].value == expected_attr + + @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) + @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_erase_line( + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + ): + term = LegacyWindowsTerm() + term.erase_line() + start = WindowsCoordinates(row=CURSOR_Y, col=0) + FillConsoleOutputCharacter.assert_called_once_with( + win32_handle, " ", length=SCREEN_WIDTH, start=start + ) + FillConsoleOutputAttribute.assert_called_once_with( + win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=SCREEN_WIDTH, start=start + ) + + @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) + @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_erase_end_of_line( + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + ): + term = LegacyWindowsTerm() + term.erase_end_of_line() + + FillConsoleOutputCharacter.assert_called_once_with( + win32_handle, " ", length=SCREEN_WIDTH - CURSOR_X, start=CURSOR_POSITION + ) + FillConsoleOutputAttribute.assert_called_once_with( + win32_handle, + DEFAULT_STYLE_ATTRIBUTE, + length=SCREEN_WIDTH - CURSOR_X, + start=CURSOR_POSITION, + ) + + @patch.object(_win32_console, "FillConsoleOutputCharacter", return_value=None) + @patch.object(_win32_console, "FillConsoleOutputAttribute", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_erase_start_of_line( + _, FillConsoleOutputAttribute, FillConsoleOutputCharacter, win32_handle + ): + term = LegacyWindowsTerm() + term.erase_start_of_line() + + start = WindowsCoordinates(CURSOR_Y, 0) + + FillConsoleOutputCharacter.assert_called_once_with( + win32_handle, " ", length=CURSOR_X, start=start + ) + FillConsoleOutputAttribute.assert_called_once_with( + win32_handle, DEFAULT_STYLE_ATTRIBUTE, length=CURSOR_X, start=start + ) + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_to(_, SetConsoleCursorPosition, win32_handle): + coords = WindowsCoordinates(row=4, col=5) + term = LegacyWindowsTerm() + + term.move_cursor_to(coords) + + SetConsoleCursorPosition.assert_called_once_with(win32_handle, coords=coords) + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_to_out_of_bounds_row( + _, SetConsoleCursorPosition, win32_handle + ): + coords = WindowsCoordinates(row=-1, col=4) + term = LegacyWindowsTerm() + + term.move_cursor_to(coords) + + assert not SetConsoleCursorPosition.called + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_to_out_of_bounds_col( + _, SetConsoleCursorPosition, win32_handle + ): + coords = WindowsCoordinates(row=10, col=-4) + term = LegacyWindowsTerm() + + term.move_cursor_to(coords) + + assert not SetConsoleCursorPosition.called + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_up(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + + term.move_cursor_up() + + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y - 1, col=CURSOR_X) + ) + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_down(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + + term.move_cursor_down() + + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=CURSOR_X) + ) + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_forward(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + + term.move_cursor_forward() + + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X + 1) + ) + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + def test_move_cursor_forward_newline_wrap(SetConsoleCursorPosition, win32_handle): + cursor_at_end_of_line = StubScreenBufferInfo( + dwCursorPosition=COORD(SCREEN_WIDTH - 1, CURSOR_Y) + ) + with patch.object( + _win32_console, + "GetConsoleScreenBufferInfo", + return_value=cursor_at_end_of_line, + ): + term = LegacyWindowsTerm() + term.move_cursor_forward() + + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y + 1, col=0) + ) + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_to_column(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + term.move_cursor_to_column(5) + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(CURSOR_Y, 5) + ) + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_move_cursor_backward(_, SetConsoleCursorPosition, win32_handle): + term = LegacyWindowsTerm() + term.move_cursor_backward() + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, coords=WindowsCoordinates(row=CURSOR_Y, col=CURSOR_X - 1) + ) + + @patch.object(_win32_console, "SetConsoleCursorPosition", return_value=None) + def test_move_cursor_backward_prev_line_wrap( + SetConsoleCursorPosition, win32_handle + ): + cursor_at_start_of_line = StubScreenBufferInfo( + dwCursorPosition=COORD(0, CURSOR_Y) + ) + with patch.object( + _win32_console, + "GetConsoleScreenBufferInfo", + return_value=cursor_at_start_of_line, + ): + term = LegacyWindowsTerm() + term.move_cursor_backward() + SetConsoleCursorPosition.assert_called_once_with( + win32_handle, + coords=WindowsCoordinates(row=CURSOR_Y - 1, col=SCREEN_WIDTH - 1), + ) + + @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_hide_cursor(_, SetConsoleCursorInfo, win32_handle): + term = LegacyWindowsTerm() + term.hide_cursor() + + call_args = SetConsoleCursorInfo.call_args_list + + assert len(call_args) == 1 + + args, kwargs = call_args[0] + assert kwargs["cursor_info"].bVisible == 0 + assert kwargs["cursor_info"].dwSize == 100 + + @patch.object(_win32_console, "SetConsoleCursorInfo", return_value=None) + @patch.object( + _win32_console, "GetConsoleScreenBufferInfo", return_value=StubScreenBufferInfo + ) + def test_show_cursor(_, SetConsoleCursorInfo, win32_handle): + term = LegacyWindowsTerm() + term.show_cursor() + + call_args = SetConsoleCursorInfo.call_args_list + + assert len(call_args) == 1 + + args, kwargs = call_args[0] + assert kwargs["cursor_info"].bVisible == 1 + assert kwargs["cursor_info"].dwSize == 100 + + @patch.object(_win32_console, "SetConsoleTitle", return_value=None) + def test_set_title(SetConsoleTitle): + term = LegacyWindowsTerm() + term.set_title("title") + + SetConsoleTitle.assert_called_once_with("title") + + @patch.object(_win32_console, "SetConsoleTitle", return_value=None) + def test_set_title_too_long(_): + term = LegacyWindowsTerm() + + with pytest.raises(AssertionError): + term.set_title("a" * 255) diff --git a/tests/test_windows_renderer.py b/tests/test_windows_renderer.py new file mode 100644 index 000000000..ff2b273a7 --- /dev/null +++ b/tests/test_windows_renderer.py @@ -0,0 +1,133 @@ +import sys +from unittest.mock import call, create_autospec + +import pytest + +try: + from rich._win32_console import LegacyWindowsTerm, WindowsCoordinates + from rich._windows_renderer import legacy_windows_render +except: + # These modules can only be imported on Windows + pass +from rich.segment import ControlType, Segment +from rich.style import Style + +pytestmark = pytest.mark.skipif(sys.platform != "win32", reason="windows only") + + +@pytest.fixture +def legacy_term_mock(): + return create_autospec(LegacyWindowsTerm) + + +def test_text_only(legacy_term_mock): + text = "Hello, world!" + buffer = [Segment(text)] + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.write_text.assert_called_once_with(text) + + +def test_text_multiple_segments(legacy_term_mock): + buffer = [Segment("Hello, "), Segment("world!")] + legacy_windows_render(buffer, legacy_term_mock) + + assert legacy_term_mock.write_text.call_args_list == [ + call("Hello, "), + call("world!"), + ] + + +def test_text_with_style(legacy_term_mock): + text = "Hello, world!" + style = Style.parse("black on red") + buffer = [Segment(text, style)] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.write_styled.assert_called_once_with(text, style) + + +def test_control_cursor_move_to(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.CURSOR_MOVE_TO, 20, 30)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.move_cursor_to.assert_called_once_with( + WindowsCoordinates(row=29, col=19) + ) + + +def test_control_carriage_return(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.CARRIAGE_RETURN,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.write_text.assert_called_once_with("\r") + + +def test_control_home(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.HOME,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.move_cursor_to.assert_called_once_with(WindowsCoordinates(0, 0)) + + +@pytest.mark.parametrize( + "control_type, method_name", + [ + (ControlType.CURSOR_UP, "move_cursor_up"), + (ControlType.CURSOR_DOWN, "move_cursor_down"), + (ControlType.CURSOR_FORWARD, "move_cursor_forward"), + (ControlType.CURSOR_BACKWARD, "move_cursor_backward"), + ], +) +def test_control_cursor_single_cell_movement( + legacy_term_mock, control_type, method_name +): + buffer = [Segment("", None, [(control_type,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + getattr(legacy_term_mock, method_name).assert_called_once_with() + + +@pytest.mark.parametrize( + "erase_mode, method_name", + [ + (0, "erase_end_of_line"), + (1, "erase_start_of_line"), + (2, "erase_line"), + ], +) +def test_control_erase_line(legacy_term_mock, erase_mode, method_name): + buffer = [Segment("", None, [(ControlType.ERASE_IN_LINE, erase_mode)])] + + legacy_windows_render(buffer, legacy_term_mock) + + getattr(legacy_term_mock, method_name).assert_called_once_with() + + +def test_control_show_cursor(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.SHOW_CURSOR,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.show_cursor.assert_called_once_with() + + +def test_control_hide_cursor(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.HIDE_CURSOR,)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.hide_cursor.assert_called_once_with() + + +def test_control_cursor_move_to_column(legacy_term_mock): + buffer = [Segment("", None, [(ControlType.CURSOR_MOVE_TO_COLUMN, 3)])] + + legacy_windows_render(buffer, legacy_term_mock) + + legacy_term_mock.move_cursor_to_column.assert_called_once_with(2)