├── .gitignore ├── MANIFEST ├── setup.py ├── LICENSE ├── README.md └── fish.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.pyc 4 | *.pyo 5 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | fish.py 2 | setup.py 3 | README.rst 4 | MANIFEST 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | from os import path 7 | 8 | long_desc = open(path.join(path.dirname(__file__), "README.md"), "r").read() 9 | 10 | setup(name="fish", version="1.1", 11 | url="http://sendapatch.se/", 12 | author="Ludvig Ericson", 13 | author_email="ludvig@sendapatch.se", 14 | description="Animating fish (and birds) for progress bars", 15 | long_description=long_desc, 16 | use_2to3=True, 17 | py_modules=["fish"]) 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, Ludvig Ericson 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | - Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | - Neither the name of the author nor the names of the contributors may 15 | be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ever wanted to have animating fishes for progress bars in your command-line 2 | script? 3 | 4 | Ever thought about doing it but then realizing you have better things to do 5 | with your time than to write meaningless ASCII animation programs? 6 | 7 | Now you can have the best of both worlds: introducing ``fish``, the module that 8 | makes any program look awesome and display useful data while churning away on 9 | some good 'ole data. 10 | 11 | Usage? Simple enough: 12 | 13 | ```python 14 | >>> import fish 15 | >>> while churning: 16 | ... churn_churn() 17 | ... fish.animate() 18 | ``` 19 | As a boy, I often dreamed of birds going back and forth as progress bars, so I 20 | decided to implement just that: 21 | 22 | ```python 23 | >>> import fish 24 | >>> bird = fish.Bird() 25 | >>> while churning: 26 | ... churn_churn() 27 | ... bird.animate() 28 | ``` 29 | 30 | Want to show the current record number? 31 | 32 | ```python 33 | >>> from fish import ProgressFish 34 | >>> fish = ProgressFish() 35 | >>> for i, x in enumerate(churning): 36 | ... churn_churn() 37 | ... fish.animate(amount=i) 38 | ``` 39 | 40 | Want to show numeric progress when you know the total number?: 41 | 42 | ```python 43 | >>> from fish import ProgressFish 44 | >>> fish = ProgressFish(total=len(data)) 45 | >>> for i, datum in enumerate(data): 46 | ... churn_churn() 47 | ... fish.animate(amount=i) 48 | ``` 49 | 50 | [See a demo on YouTube](http://www.youtube.com/watch?v=xYeG5CVTCmk). 51 | 52 | The default fish is a simple bass at a pretty good velocity for an ASCII fish. 53 | 54 | Possibilities are endless here, gentlemen: 55 | 56 | >The only limit is yourself. 57 | 58 | >-- zombo.com 59 | 60 | [Fork on GitHub](http://github.com/lericson/fish) 61 | -------------------------------------------------------------------------------- /fish.py: -------------------------------------------------------------------------------- 1 | """ 2 | Swimming fishes progress indicator 3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | Well, what can you say really? It's a swimming fish that animates when you 6 | call a certain function. Simple as that. 7 | 8 | How to use: 9 | 10 | .. code-block:: python 11 | 12 | import fish 13 | 14 | for datum_to_churn in data: 15 | fish.animate() 16 | churn_churn(datum_to_churn) 17 | 18 | With a progress indicator: 19 | 20 | .. code-block:: python 21 | 22 | import fish 23 | 24 | progress = fish.ProgressFish(total=1000) 25 | for i in range(1000): 26 | progress.animate() 27 | churn() 28 | 29 | Using other fish or birds: 30 | 31 | .. code-block:: python 32 | 33 | from fish import Bird 34 | 35 | bird = Bird() 36 | while True: 37 | bird.animate() 38 | churn() 39 | """ 40 | 41 | import sys 42 | import time 43 | import string 44 | 45 | from struct import unpack 46 | from fcntl import ioctl 47 | from termios import TIOCGWINSZ 48 | 49 | from itertools import count 50 | 51 | 52 | def get_term_width(): 53 | """Get terminal width or None.""" 54 | for fp in sys.stdin, sys.stdout, sys.stderr: 55 | try: 56 | return unpack("hh", ioctl(fp.fileno(), TIOCGWINSZ, " "))[1] 57 | except IOError: 58 | continue 59 | 60 | 61 | class ANSIControl(object): 62 | def __init__(self, outfile=sys.stderr, flush=True): 63 | self.outfile = outfile 64 | self.flush = flush 65 | 66 | def ansi(self, command): 67 | self.outfile.write("\x1b[%s" % command) 68 | if self.flush: 69 | self.outfile.flush() 70 | 71 | def clear_line_right(self): self.ansi("0K\r") 72 | def clear_line_left(self): self.ansi("1K\r") 73 | def clear_line_whole(self): self.ansi("2K\r") 74 | def clear_forward(self): self.ansi("0J") 75 | def clear_backward(self): self.ansi("1J") 76 | def clear_whole(self): self.ansi("2J") 77 | def save_cursor(self): self.ansi("s") 78 | def restore_cursor(self): self.ansi("u") 79 | def move_up(self, n): self.ansi("%dA" % n) 80 | def move_down(self, n): self.ansi("%dB" % n) 81 | 82 | 83 | class SwimFishBase(object): 84 | def __init__(self, speed=1.0, world_length=None, outfile=sys.stderr): 85 | if not world_length: 86 | world_length = get_term_width() or 79 87 | self.worldstep = self.make_worldstepper() 88 | self.speed = speed 89 | self.world_length = world_length 90 | self.outfile = outfile 91 | self.ansi = ANSIControl(outfile=outfile) 92 | self.last_hash = 0 93 | 94 | def test(self): 95 | while True: 96 | self.animate() 97 | time.sleep(0.0125) 98 | 99 | @property 100 | def actual_length(self): 101 | # Refit the world so that we can move along an axis and not worry about 102 | # overflowing 103 | return self.world_length - self.own_length 104 | 105 | def animate(self, outfile=None, force=False): 106 | step = next(self.worldstep) 107 | # As there are two directions we pretend the world is twice as large as 108 | # it really is, then handle the overflow 109 | pos = (self.speed * step) % (self.actual_length * 2) 110 | reverse = pos < self.actual_length 111 | pos = int(round(abs(pos - self.actual_length), 0)) 112 | fish = self.render(step=step, reverse=reverse) 113 | of = outfile or self.outfile 114 | curr_hash = force or hash((of, pos, "".join(fish))) 115 | if force or curr_hash != self.last_hash: 116 | self.print_fish(of, pos, fish) 117 | of.flush() 118 | self.last_hash = curr_hash 119 | 120 | def print_fish(self, of, pos, fish): 121 | raise NotImplementedError("you must choose a printer type") 122 | 123 | 124 | class SingleLineFishPrinter(SwimFishBase): 125 | def print_fish(self, of, pos, fish): 126 | lead = " " * pos 127 | trail = " " * (self.world_length - self.own_length - pos) 128 | self.ansi.clear_line_whole() 129 | assert len(fish) == 1 130 | of.write(lead + fish[0] + trail + "\r") 131 | 132 | 133 | class MultiLineFishPrinter(SwimFishBase): 134 | _printed = False 135 | 136 | def __init__(self, *args, **kwds): 137 | super(MultiLineFishPrinter, self).__init__(*args, **kwds) 138 | self.reset() 139 | 140 | def reset(self): 141 | """Call this when reusing the animation in a new place""" 142 | self._printed = False 143 | 144 | def _restore_cursor(self, lines): 145 | if self._printed: 146 | self.ansi.move_up(lines) 147 | self._printed = True 148 | 149 | def print_fish(self, of, pos, fish): 150 | lead = " " * pos 151 | trail = " " * (self.world_length - self.own_length - pos) 152 | self._restore_cursor(len(fish)) 153 | self.ansi.clear_forward() 154 | for line in fish: 155 | of.write(lead + line + trail + "\n") 156 | 157 | 158 | class ProgressableFishBase(SwimFishBase): 159 | """Progressing fish, only compatible with single-line fish""" 160 | 161 | def __init__(self, *args, **kwds): 162 | total = kwds.pop("total", None) 163 | self.total = total 164 | super(ProgressableFishBase, self).__init__(*args, **kwds) 165 | if total: 166 | # `pad` is the length required for the progress indicator, 167 | # It, at its longest, is `100% 123/123` 168 | pad = len(str(total)) * 2 169 | pad += 6 170 | self.world_length -= pad 171 | 172 | def test(self): 173 | if not self.total: 174 | return super(ProgressableFishBase, self).test() 175 | for i in range(1, self.total * 2 + 1): 176 | self.animate(amount=i) 177 | time.sleep(0.0125) 178 | 179 | def animate(self, *args, **kwds): 180 | prev_amount = getattr(self, "amount", None) 181 | self.amount = kwds.pop("amount", None) 182 | if self.amount != prev_amount: 183 | kwds["force"] = True 184 | return super(ProgressableFishBase, self).animate(*args, **kwds) 185 | 186 | def print_fish(self, of, pos, fish): 187 | if not self.amount: 188 | return super(ProgressableFishBase, self).print_fish(of, pos, fish) 189 | # Get the progress text 190 | if self.total: 191 | part = self.amount / float(self.total) 192 | done_text = str(self.amount).rjust(len(str(self.total))) 193 | progress = "%3.d%% %s/%d" % (part * 100, done_text, self.total) 194 | lead = " " * pos 195 | trail = " " * (self.world_length - self.own_length - pos) 196 | else: 197 | progress = str(self.amount) 198 | lead = " " * (pos - len(progress)) 199 | trail = " " * (self.world_length - self.own_length - pos - len(progress)) 200 | self.ansi.clear_line_whole() 201 | assert len(fish) == 1 202 | of.write(lead + fish[0] + trail + progress + "\r") 203 | 204 | 205 | class BassLook(SingleLineFishPrinter): 206 | def render(self, step, reverse=False): 207 | return ["<'((<" if reverse else ">))'>"] 208 | 209 | own_length = len(">))'>") 210 | 211 | 212 | class SalmonLook(SingleLineFishPrinter): 213 | def render(self, step, reverse=False): 214 | return ["<*}}}><" if reverse else "><{{{*>"] 215 | 216 | 217 | def docstring2lines(ds): 218 | return list(filter(None, ds.split("\n"))) 219 | 220 | 221 | try: 222 | maketrans = string.maketrans 223 | except AttributeError: 224 | maketrans = str.maketrans 225 | rev_trans = maketrans(r"/\<>76", r"\/>(@)\ | ___7 241 | \\/ \_____,.'__ 242 | ' _/''&;>* 243 | `'----\\` 244 | """ 245 | bird = docstring2lines(bird) 246 | bird_rev = ascii_rev(bird) 247 | 248 | def render(self, step, reverse=False): 249 | return self.bird if reverse else self.bird_rev 250 | 251 | own_length = max(map(len, bird)) 252 | assert sum(map(len, bird)) / float(len(bird)) == own_length 253 | 254 | 255 | class DuckLook(MultiLineFishPrinter): 256 | # ASCII art crediT: jgs 257 | duck = docstring2lines(""" 258 | _ 259 | \. _(9> 260 | \==_) 261 | -'= 262 | """) 263 | 264 | duck_rev = docstring2lines(""" 265 | _ 266 | <6)_ ,/ 267 | (_==/ 268 | ='- 269 | """) 270 | 271 | def render(self, step, reverse=False): 272 | return self.duck_rev if reverse else self.duck 273 | 274 | own_length = max(map(len, duck)) 275 | 276 | 277 | class SwimFishNoSync(SwimFishBase): 278 | @classmethod 279 | def make_worldstepper(cls): 280 | return count() 281 | 282 | 283 | class SwimFishTimeSync(SwimFishBase): 284 | @classmethod 285 | def make_worldstepper(cls): 286 | return iter(time.time, None) 287 | 288 | 289 | class SwimFishProgressSync(ProgressableFishBase): 290 | def make_worldstepper(self): 291 | if self.total: 292 | return iter(self.worldstep_progressive, None) 293 | else: 294 | return count() 295 | 296 | def worldstep_progressive(self): 297 | part = self.amount / float(self.total) 298 | step = (self.actual_length + part * self.actual_length) / self.speed 299 | return step 300 | 301 | 302 | class Fish(SwimFishTimeSync, BassLook): 303 | """The default swimming fish, the one you very likely want to use. 304 | See module-level documentation. 305 | """ 306 | 307 | 308 | class ProgressFish(SwimFishProgressSync, BassLook): 309 | """A progress-based swimming fish.""" 310 | 311 | 312 | class Bird(SwimFishTimeSync, BirdLook): 313 | """What? A bird?""" 314 | 315 | 316 | default_fish = Fish() 317 | animate = default_fish.animate 318 | 319 | 320 | fish_types = {"bass": BassLook, 321 | "salmon": SalmonLook, 322 | "bird": BirdLook, 323 | "duck": DuckLook} 324 | 325 | 326 | if __name__ == "__main__": 327 | import signal 328 | import argparse 329 | signal.signal(signal.SIGINT, lambda *a: sys.exit(0)) 330 | 331 | parser = argparse.ArgumentParser() 332 | parser.add_argument("-f", "--fish", choices=set(fish_types.keys()) | {"?"}, 333 | default="bass", help="fish type (specify ? to list)") 334 | parser.add_argument("-s", "--speed", type=int, default=1, metavar="V", 335 | help="fish speed") 336 | parser.add_argument("--sync", choices=("none", "time"), default="time", 337 | help="synchronization mechanism") 338 | 339 | args = parser.parse_args() 340 | 341 | if args.fish == "?": 342 | for fish_name, fish_type in fish_types.items(): 343 | print(fish_name) 344 | print("=" * len(fish_name)) 345 | print() 346 | class TempFish(SwimFishTimeSync, fish_type): 347 | pass 348 | normal = TempFish().render(0, reverse=False) 349 | reverse = TempFish().render(0, reverse=True) 350 | for normline, revline in zip(normal, reverse): 351 | print(normline, " ", revline) 352 | print() 353 | sys.exit(0) 354 | else: 355 | fish_look = fish_types[args.fish] 356 | 357 | if args.sync == "time": 358 | fish_sync = SwimFishTimeSync 359 | elif args.sync == "none": 360 | fish_sync = SwimFishNoSync 361 | 362 | class FishType(fish_sync, fish_look): 363 | pass 364 | 365 | fish = FishType(speed=args.speed) 366 | fish.test() 367 | --------------------------------------------------------------------------------