├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── demo.gif ├── pyproject.lock ├── pyproject.toml ├── scrmbl ├── __init__.py ├── cli.py └── main.py └── tests ├── __init__.py ├── chars.txt ├── lipsum.txt ├── test_cli.py └── test_scrmbl.py /.gitignore: -------------------------------------------------------------------------------- 1 | # macos 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | .vscode/ 117 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.6" 5 | - "3.7-dev" 6 | before_install: 7 | - pip install poetry 8 | install: 9 | - poetry develop 10 | script: 11 | - flake8 . 12 | - coverage run --source scrmbl -m pytest -v 13 | - coverage report 14 | - coveralls 15 | before_deploy: 16 | - poetry build 17 | deploy: 18 | - provider: releases 19 | api_key: $GITHUB_TOKEN 20 | file_glob: true 21 | file: dist/* 22 | skip_cleanup: true 23 | on: 24 | tags: true 25 | python: 3.6 26 | - provider: pypi 27 | user: $PYPI_USERNAME 28 | password: $PYPI_PASSWORD 29 | skip_cleanup: true 30 | on: 31 | tags: true 32 | python: 3.6 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Etienne Napoleone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scrmbl 2 | 3 | Library and CLI for "scrambled" printing in terminal. 4 | 5 | Have you ever wanted to print your text like some corny action movies? 6 | 7 |

8 | 9 |

10 | 11 | note: the `%` are coming from my tty recorder 12 | 13 | ## Requirements 14 | 15 | - Tested on Python >= 3.5 16 | 17 | ## Install 18 | 19 | ### CLI 20 | 21 | ``` 22 | pip3 install --user scrmbl 23 | ``` 24 | 25 | ### Library 26 | 27 | Using pip in a virtualenv. 28 | 29 | ```bash 30 | pip install scrmbl 31 | ``` 32 | 33 | Using Poetry: 34 | 35 | ```bash 36 | poetry add scrmbl 37 | ``` 38 | 39 | Using Pipenv: 40 | 41 | ```bash 42 | pipenv install scrmbl 43 | ``` 44 | 45 | ## Usage 46 | 47 | Refer to the gif to see the effect 48 | 49 | ### CLI 50 | 51 | ``` 52 | Usage: scrmbl [OPTIONS] [MESSAGE] 53 | 54 | Scrmbl print the given message. 55 | 56 | Options: 57 | -s, --speed FLOAT Time in seconds between prints. Default: 0.05 58 | -i, --iterations INTEGER Number of iterations per character. Default: 2 59 | -c, --charset FILE Set of chars to scramble. 60 | --version Show the version and exit. 61 | --help Show this message and exit. 62 | ``` 63 | 64 | Can also read from stdin. 65 | 66 | ```bash 67 | ls -lrtha | scrmbl 68 | ``` 69 | 70 | ## Library 71 | 72 | ```python 73 | >>> import scrmbl 74 | 75 | >>> scrmbl.echo('09:30pm, Washington, NSA HEADQUARTERS') 76 | '09:30pm, Washington, NSA HEADQUARTERS' 77 | 78 | # handle multiline 79 | >>> scrmbl.echo('09:30pm, Washington\nNSA HEADQUARTERS') 80 | '02:56am, New-York' 81 | 'FBI HEADQUARTERS' 82 | 83 | # custom settings: 84 | # charset = String of characters to scramble with 85 | # speed = Time in seconds between prints 86 | # iterations = number of iterations before printing the final character 87 | >>> scrmbl.echo('NSA OFFICE', charset='ABCDefg/-', speed=0.2, iterations=6) 88 | 'CIA OFFICE' 89 | ``` 90 | 91 | ## Thanks 92 | 93 | Special thanks for contributing: 94 | - [@podstava](https://github.com/podstava) 95 | - [@0jdxt](https://github.com/0jdxt) 96 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etienne-napoleone/scrmbl/e98fe2bfd378e703cfaaa7f1b9763c85d0b8177d/demo.gif -------------------------------------------------------------------------------- /pyproject.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | category = "dev" 3 | description = "Atomic file writes." 4 | name = "atomicwrites" 5 | optional = false 6 | platform = "*" 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | version = "1.2.1" 9 | 10 | [[package]] 11 | category = "dev" 12 | description = "Classes Without Boilerplate" 13 | name = "attrs" 14 | optional = false 15 | platform = "*" 16 | python-versions = "*" 17 | version = "18.2.0" 18 | 19 | [[package]] 20 | category = "dev" 21 | description = "Python package for providing Mozilla's CA Bundle." 22 | name = "certifi" 23 | optional = false 24 | platform = "*" 25 | python-versions = "*" 26 | version = "2018.8.24" 27 | 28 | [[package]] 29 | category = "dev" 30 | description = "Universal encoding detector for Python 2 and 3" 31 | name = "chardet" 32 | optional = false 33 | platform = "*" 34 | python-versions = "*" 35 | version = "3.0.4" 36 | 37 | [[package]] 38 | category = "main" 39 | description = "Composable command line interface toolkit" 40 | name = "click" 41 | optional = false 42 | platform = "*" 43 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 44 | version = "7.0" 45 | 46 | [[package]] 47 | category = "dev" 48 | description = "Cross-platform colored terminal text." 49 | name = "colorama" 50 | optional = false 51 | platform = "UNKNOWN" 52 | python-versions = "*" 53 | version = "0.3.9" 54 | 55 | [package.requirements] 56 | platform = "win32" 57 | 58 | [[package]] 59 | category = "dev" 60 | description = "Code coverage measurement for Python" 61 | name = "coverage" 62 | optional = false 63 | platform = "*" 64 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" 65 | version = "4.5.1" 66 | 67 | [[package]] 68 | category = "dev" 69 | description = "Show coverage stats online via coveralls.io" 70 | name = "coveralls" 71 | optional = false 72 | platform = "*" 73 | python-versions = "*" 74 | version = "1.5.1" 75 | 76 | [package.dependencies] 77 | coverage = ">=3.6" 78 | docopt = ">=0.6.1" 79 | requests = ">=1.0.0" 80 | 81 | [[package]] 82 | category = "dev" 83 | description = "Pythonic argument parser, that will make you smile" 84 | name = "docopt" 85 | optional = false 86 | platform = "UNKNOWN" 87 | python-versions = "*" 88 | version = "0.6.2" 89 | 90 | [[package]] 91 | category = "dev" 92 | description = "the modular source code checker: pep8, pyflakes and co" 93 | name = "flake8" 94 | optional = false 95 | platform = "*" 96 | python-versions = "*" 97 | version = "3.5.0" 98 | 99 | [package.dependencies] 100 | mccabe = ">=0.6.0,<0.7.0" 101 | pycodestyle = ">=2.0.0,<2.4.0" 102 | pyflakes = ">=1.5.0,<1.7.0" 103 | 104 | [[package]] 105 | category = "dev" 106 | description = "Internationalized Domain Names in Applications (IDNA)" 107 | name = "idna" 108 | optional = false 109 | platform = "*" 110 | python-versions = "*" 111 | version = "2.7" 112 | 113 | [[package]] 114 | category = "dev" 115 | description = "McCabe checker, plugin for flake8" 116 | name = "mccabe" 117 | optional = false 118 | platform = "*" 119 | python-versions = "*" 120 | version = "0.6.1" 121 | 122 | [[package]] 123 | category = "dev" 124 | description = "More routines for operating on iterables, beyond itertools" 125 | name = "more-itertools" 126 | optional = false 127 | platform = "*" 128 | python-versions = "*" 129 | version = "4.3.0" 130 | 131 | [package.dependencies] 132 | six = ">=1.0.0,<2.0.0" 133 | 134 | [[package]] 135 | category = "dev" 136 | description = "Object-oriented filesystem paths" 137 | name = "pathlib2" 138 | optional = false 139 | platform = "*" 140 | python-versions = "*" 141 | version = "2.3.2" 142 | 143 | [package.dependencies] 144 | six = "*" 145 | 146 | [package.requirements] 147 | python = "<3.6" 148 | 149 | [[package]] 150 | category = "dev" 151 | description = "plugin and hook calling mechanisms for python" 152 | name = "pluggy" 153 | optional = false 154 | platform = "unix" 155 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 156 | version = "0.7.1" 157 | 158 | [[package]] 159 | category = "dev" 160 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 161 | name = "py" 162 | optional = false 163 | platform = "unix" 164 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 165 | version = "1.6.0" 166 | 167 | [[package]] 168 | category = "dev" 169 | description = "Python style guide checker" 170 | name = "pycodestyle" 171 | optional = false 172 | platform = "*" 173 | python-versions = "*" 174 | version = "2.3.1" 175 | 176 | [[package]] 177 | category = "dev" 178 | description = "passive checker of Python programs" 179 | name = "pyflakes" 180 | optional = false 181 | platform = "*" 182 | python-versions = "*" 183 | version = "1.6.0" 184 | 185 | [[package]] 186 | category = "dev" 187 | description = "pytest: simple powerful testing with Python" 188 | name = "pytest" 189 | optional = false 190 | platform = "unix" 191 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 192 | version = "3.8.2" 193 | 194 | [package.dependencies] 195 | atomicwrites = ">=1.0" 196 | attrs = ">=17.4.0" 197 | more-itertools = ">=4.0.0" 198 | pluggy = ">=0.7" 199 | py = ">=1.5.0" 200 | setuptools = "*" 201 | six = ">=1.10.0" 202 | 203 | [package.dependencies.colorama] 204 | platform = "win32" 205 | version = "*" 206 | 207 | [package.dependencies.pathlib2] 208 | python = "<3.6" 209 | version = ">=2.2.0" 210 | 211 | [[package]] 212 | category = "dev" 213 | description = "Pytest plugin for measuring coverage." 214 | name = "pytest-cov" 215 | optional = false 216 | platform = "*" 217 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 218 | version = "2.6.0" 219 | 220 | [package.dependencies] 221 | coverage = ">=4.4" 222 | pytest = ">=2.9" 223 | 224 | [[package]] 225 | category = "dev" 226 | description = "Python HTTP for Humans." 227 | name = "requests" 228 | optional = false 229 | platform = "*" 230 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 231 | version = "2.19.1" 232 | 233 | [package.dependencies] 234 | certifi = ">=2017.4.17" 235 | chardet = ">=3.0.2,<3.1.0" 236 | idna = ">=2.5,<2.8" 237 | urllib3 = ">=1.21.1,<1.24" 238 | 239 | [[package]] 240 | category = "dev" 241 | description = "Python 2 and 3 compatibility utilities" 242 | name = "six" 243 | optional = false 244 | platform = "*" 245 | python-versions = "*" 246 | version = "1.11.0" 247 | 248 | [[package]] 249 | category = "dev" 250 | description = "HTTP library with thread-safe connection pooling, file post, and more." 251 | name = "urllib3" 252 | optional = false 253 | platform = "*" 254 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" 255 | version = "1.23" 256 | 257 | [metadata] 258 | content-hash = "b8e8bd0c8b465ec69f789637c436c083c1ecd8934a7ac5a7fa24e7a2ecd9e09b" 259 | platform = "*" 260 | python-versions = "^3.5" 261 | 262 | [metadata.hashes] 263 | atomicwrites = ["0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", "ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"] 264 | attrs = ["10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", "ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"] 265 | certifi = ["376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", "456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"] 266 | chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] 267 | click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] 268 | colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"] 269 | coverage = ["03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", "0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", "104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a", "10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", "15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd", "198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", "1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2", "23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", "28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", "2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", "2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", "337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", "3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", "3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", "3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", "3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", "4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", "56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", "5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", "69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", "6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", "701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", "7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", "76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", "7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", "7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", "7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", "8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", "9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", "9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4", "ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91", "b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d", "be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", "c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", "de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", "e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", "e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77", "f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80", "f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"] 270 | coveralls = ["ab638e88d38916a6cedbf80a9cd8992d5fa55c77ab755e262e00b36792b7cd6d", "b2388747e2529fa4c669fb1e3e2756e4e07b6ee56c7d9fce05f35ccccc913aa0"] 271 | docopt = ["49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"] 272 | flake8 = ["7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", "c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"] 273 | idna = ["156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"] 274 | mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] 275 | more-itertools = ["c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", "c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", "fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"] 276 | pathlib2 = ["8eb170f8d0d61825e09a95b38be068299ddeda82f35e96c3301a8a5e7604cb83", "d1aa2a11ba7b8f7b21ab852b1fb5afb277e1bb99d5dfc663380b5015c0d80c5a"] 277 | pluggy = ["6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"] 278 | py = ["06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1", "50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"] 279 | pycodestyle = ["682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", "6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"] 280 | pyflakes = ["08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", "8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"] 281 | pytest = ["7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e", "9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"] 282 | pytest-cov = ["513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", "e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"] 283 | requests = ["63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", "ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"] 284 | six = ["70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"] 285 | urllib3 = ["a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"] 286 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "scrmbl" 3 | version = "1.0.0" 4 | description = "Library for scrambled printing in terminal" 5 | authors = ["Etienne Napoleone "] 6 | readme = "README.md" 7 | license = "MIT" 8 | homepage = "https://github.com/etienne-napoleone/scrmbl" 9 | repository = "https://github.com/etienne-napoleone/scrmbl" 10 | documentation = "https://github.com/etienne-napoleone/scrmbl/blob/master/README.md" 11 | keywords = ["print", "library", "movie", "action", "scramble"] 12 | 13 | [tool.poetry.dependencies] 14 | python = "^3.5" 15 | click = "^7.0" 16 | 17 | [tool.poetry.dev-dependencies] 18 | pytest = "*" 19 | flake8 = "^3.5" 20 | coveralls = "^1.5" 21 | pytest-cov = "^2.6" 22 | 23 | [tool.poetry.scripts] 24 | scrmbl = 'scrmbl.cli:cli' 25 | -------------------------------------------------------------------------------- /scrmbl/__init__.py: -------------------------------------------------------------------------------- 1 | from scrmbl.main import echo 2 | 3 | __all__ = ['echo'] 4 | 5 | __version__ = '1.0.0' 6 | -------------------------------------------------------------------------------- /scrmbl/cli.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | import click 5 | 6 | import scrmbl 7 | 8 | 9 | @click.command() 10 | @click.argument('message', default='') 11 | @click.option('-s', '--speed', type=click.FLOAT, default=0.05, 12 | help='Time in seconds between prints. Default: 0.05') 13 | @click.option('-i', '--iterations', type=click.INT, default=2, 14 | help='Number of iterations per character. Default: 2') 15 | @click.option('-c', '--charset', 16 | type=click.Path(exists=True, allow_dash=True, dir_okay=False), 17 | help='Set of chars to scramble.') 18 | @click.version_option(version=scrmbl.__version__) 19 | def cli(message: str, speed: float, iterations: int, charset: str) -> None: 20 | """Scrmbl print the given message.""" 21 | # no text input 22 | if not message: 23 | # if no stdin or just '-c -' 24 | if sys.stdin.isatty() or charset == '-': 25 | raise click.UsageError('"MESSAGE" is empty. No argument or stdin.') 26 | for line in sys.stdin: 27 | message += line 28 | message = _strip_ansi_colors(message) 29 | if charset: 30 | with click.open_file(charset) as f: 31 | charset_content = f.read() 32 | charset_content = charset_content.replace('\n', '') 33 | else: 34 | charset_content = None 35 | scrmbl.echo(message, charset=charset_content, speed=speed, 36 | iterations=iterations) 37 | 38 | 39 | def _strip_ansi_colors(message: str) -> str: 40 | """Strip ANSI color codes""" 41 | escape_codes = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') 42 | return escape_codes.sub('', message) 43 | -------------------------------------------------------------------------------- /scrmbl/main.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import time 4 | 5 | import click 6 | 7 | random.seed() 8 | 9 | ALL_CHARS = string.digits + string.ascii_letters + string.punctuation 10 | COLS, _ = click.get_terminal_size() 11 | 12 | 13 | def echo(message: str, charset: str = ALL_CHARS, speed: float = 0.05, 14 | iterations: int = 2) -> None: 15 | """Scrmbl print the given message.""" 16 | if not charset: 17 | charset = ALL_CHARS 18 | for line in message.split('\n'): 19 | echoed = '' 20 | for char in line: 21 | for _ in range(iterations): 22 | if char != ' ': 23 | ran_char = random.choice(charset) 24 | click.echo('\r{}{}'.format(echoed, ran_char), nl=False) 25 | else: 26 | click.echo('\r{}'.format(echoed), nl=False) 27 | time.sleep(speed) 28 | echoed += char 29 | # wrap if line longer than console cols 30 | if len(echoed) >= COLS - 1: 31 | click.echo('\r' + echoed) 32 | echoed = '' 33 | if echoed: 34 | click.echo('\r' + echoed) 35 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etienne-napoleone/scrmbl/e98fe2bfd378e703cfaaa7f1b9763c85d0b8177d/tests/__init__.py -------------------------------------------------------------------------------- /tests/chars.txt: -------------------------------------------------------------------------------- 1 | abcdefg 2 | -------------------------------------------------------------------------------- /tests/lipsum.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras accumsan arcu quis ex mollis suscipit. Sed non orci porttitor, tincidunt mauris id, volutpat diam. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aliquam eget porta massa. Aliquam facilisis sit amet quam id sodales. Donec egestas pharetra pellentesque. Nam vestibulum dictum pharetra. Ut eget est at nibh condimentum sagittis ac a dui. Duis iaculis convallis rhoncus. Mauris maximus erat scelerisque, semper leo eget, rhoncus enim. Nulla ullamcorper in purus eget varius. 2 | 3 | Praesent imperdiet odio in massa egestas, a laoreet quam viverra. In hac habitasse platea dictumst. Suspendisse eu lobortis felis. Maecenas auctor purus risus, eget tempor turpis tincidunt at. Aliquam elementum feugiat ligula ac dictum. Aenean sit amet enim non lectus semper dignissim. Aenean posuere pretium faucibus. Suspendisse blandit est vel ultricies mollis. Sed pellentesque sem vel ullamcorper tempus. Fusce non nisi lorem. Phasellus at urna ut est viverra auctor. Vestibulum faucibus dapibus nisi nec mollis. Aenean commodo libero ipsum, ac mattis lectus dapibus vitae. Praesent urna massa, condimentum a sapien dapibus, fringilla gravida urna. 4 | 5 | Vestibulum gravida feugiat urna vitae commodo. Sed consequat mollis sem, ultrices ornare sapien dignissim ac. Mauris vestibulum ut nunc eget ultricies. Fusce vel fringilla arcu. Aenean felis ipsum, accumsan scelerisque ullamcorper ut, dignissim ac purus. Integer ornare, ligula sit amet condimentum hendrerit, turpis neque mattis erat, vitae fermentum lectus eros at urna. Sed nec dignissim felis, ac condimentum velit. Nam et tortor auctor, semper eros eget, lacinia dui. Maecenas id placerat mauris. Proin eget volutpat risus, sed ultrices tortor. Vivamus hendrerit congue dui, ut volutpat purus rhoncus sed. Maecenas consequat diam non metus dignissim faucibus. Donec sit amet posuere ex. In convallis placerat mi ac porttitor. Nullam blandit posuere mauris, et venenatis dui lobortis eget. Sed id est nec urna placerat pellentesque ac ut massa. 6 | 7 | Vivamus quis tristique metus. Fusce lobortis laoreet porta. Nam a luctus magna. Etiam eu tempor turpis, et scelerisque eros. Curabitur placerat mattis nibh sit amet lobortis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec dictum erat in justo sollicitudin suscipit. Nulla at odio quis magna posuere congue mollis non mi. 8 | 9 | Nam bibendum, lacus at molestie porttitor, risus ipsum egestas mauris, non vulputate ex erat nec diam. Nunc nec sollicitudin lectus. Aenean elementum sollicitudin augue, quis luctus dui mollis ut. Sed sed risus id enim gravida lobortis et in metus. Suspendisse eget sollicitudin risus. Praesent eget lacinia nunc. Maecenas laoreet nibh sit amet arcu hendrerit auctor. Proin in nulla in ex iaculis rutrum sed eu lectus. Vestibulum sollicitudin turpis ut odio aliquet, in gravida tellus sodales. Vestibulum vel tortor nec ante elementum venenatis in eu erat. Proin ornare semper ex a tempus. Etiam placerat orci ut tellus aliquet, a cursus nisl volutpat. 10 | 11 | Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris non ligula sapien. Donec eu felis sed diam suscipit accumsan. Etiam eu aliquet ligula, non tristique massa. Proin quis tempus mi, in hendrerit ex. Fusce vestibulum sodales mauris feugiat eleifend. Suspendisse at est accumsan, laoreet tortor quis, volutpat quam. Curabitur pellentesque nunc eu tempus cursus. Donec eget mollis enim, in fringilla eros. Curabitur malesuada eget orci eget faucibus. Sed id auctor velit. Vivamus ac diam quis eros pellentesque sollicitudin. Ut eu nisi ultricies justo finibus finibus. 12 | 13 | Morbi vel leo ut mi congue elementum. Donec scelerisque est sapien, ut malesuada arcu malesuada nec. Cras euismod ligula id leo posuere pellentesque. Ut et quam ut massa condimentum bibendum. Mauris venenatis fermentum nisl, quis convallis sem aliquam ac. Phasellus lobortis ac neque at interdum. Pellentesque non convallis magna. Vivamus ornare velit a leo interdum, a auctor nunc commodo. Donec lobortis lacus et nisl volutpat, at pharetra ex posuere. 14 | 15 | Nunc malesuada arcu non faucibus sodales. Morbi placerat quam ipsum, nec aliquam nulla vulputate at. Integer dignissim molestie faucibus. Mauris quis tortor porttitor sem pharetra ullamcorper. Vestibulum sodales varius efficitur. Quisque fringilla luctus massa vitae porttitor. Phasellus fermentum nibh eros, vitae condimentum felis finibus quis. In faucibus eget metus malesuada lobortis. Donec ut neque eget tellus imperdiet convallis nec eu tortor. Quisque felis purus, blandit eu fringilla eu, scelerisque a enim. 16 | 17 | Donec rhoncus lectus ex, vitae rhoncus sem aliquam in. Cras vitae facilisis justo. Nullam non fringilla arcu. In viverra lobortis lacus, a convallis nunc bibendum a. Nunc porta risus eu nisl varius euismod. Vestibulum sodales rutrum diam, nec elementum sapien suscipit non. Duis id turpis id justo aliquet lacinia at metus. 18 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from click.testing import CliRunner 4 | 5 | from scrmbl.cli import cli 6 | 7 | 8 | def test_hello_world(): 9 | runner = CliRunner() 10 | result = runner.invoke(cli, ['Hello, world!']) 11 | assert result.exit_code == 0 12 | assert result.output.split('\r')[-1] == 'Hello, world!\n' 13 | 14 | 15 | def test_input_stream(): 16 | with open('tests/lipsum.txt', 'r') as fin: 17 | data = fin.read() 18 | runner = CliRunner() 19 | result = runner.invoke(cli, ['-s', '0'], input=data) 20 | 21 | assert result.exit_code == 0 22 | 23 | lines = iter(result.output.split('\r')) 24 | # only retrieve longest lines i.e. full lines 25 | filt = [] 26 | prev, curr = None, next(lines) 27 | while curr: 28 | if prev and len(curr) < len(prev): 29 | filt.append(prev) 30 | 31 | try: 32 | prev, curr = curr, next(lines) 33 | except StopIteration: 34 | curr = None 35 | 36 | for line in filt: 37 | assert line.split('\n')[0] in data 38 | 39 | 40 | def test_invalid_cmd(): 41 | runner = CliRunner() 42 | result = runner.invoke(cli, ['-c', '-'], input='abcdefg') 43 | assert result.exit_code == 2 44 | assert 'Usage:' in result.output 45 | assert 'Error' in result.output 46 | 47 | 48 | def test_charset(): 49 | runner = CliRunner() 50 | result = runner.invoke(cli, ['-c', 'tests/chars.txt', 'test']) 51 | assert result.exit_code == 0 52 | for line in result.output.split('\r'): 53 | if line: 54 | assert re.match(r'^[tes]{0,4}[abcdefg]?$', line) 55 | -------------------------------------------------------------------------------- /tests/test_scrmbl.py: -------------------------------------------------------------------------------- 1 | import scrmbl 2 | 3 | 4 | def test_version(): 5 | assert scrmbl.__version__ == '1.0.0' 6 | 7 | 8 | def test_echo_single(capsys): 9 | scrmbl.echo('this is a test') 10 | captured = capsys.readouterr().out 11 | assert captured.split('\r')[-1] == 'this is a test\n' 12 | 13 | 14 | def test_echo_multi(capsys): 15 | scrmbl.echo('this is a test\nthis is also a test') 16 | captured = capsys.readouterr().out 17 | assert captured.split('\n')[0].split('\r')[-1] == 'this is a test' 18 | assert captured.split('\r')[-1] == 'this is also a test\n' 19 | --------------------------------------------------------------------------------