1#!/usr/bin/env python3
2#
3# Display operations on block devices based on trace output
4#
5# Example:
6# ./scripts/tracebd.py trace
7#
8# Copyright (c) 2022, The littlefs authors.
9# SPDX-License-Identifier: BSD-3-Clause
10#
11
12import collections as co
13import functools as ft
14import io
15import itertools as it
16import math as m
17import os
18import re
19import shutil
20import threading as th
21import time
22
23
24CHARS = 'rpe.'
25COLORS = ['42', '45', '44', '']
26
27WEAR_CHARS = '0123456789'
28WEAR_CHARS_SUBSCRIPTS = '.₁₂₃₄₅₆789'
29WEAR_COLORS = ['', '', '', '', '', '', '', '35', '35', '1;31']
30
31CHARS_DOTS = " .':"
32COLORS_DOTS = ['32', '35', '34', '']
33CHARS_BRAILLE = (
34    '⠀⢀⡀⣀⠠⢠⡠⣠⠄⢄⡄⣄⠤⢤⡤⣤' '⠐⢐⡐⣐⠰⢰⡰⣰⠔⢔⡔⣔⠴⢴⡴⣴'
35    '⠂⢂⡂⣂⠢⢢⡢⣢⠆⢆⡆⣆⠦⢦⡦⣦' '⠒⢒⡒⣒⠲⢲⡲⣲⠖⢖⡖⣖⠶⢶⡶⣶'
36    '⠈⢈⡈⣈⠨⢨⡨⣨⠌⢌⡌⣌⠬⢬⡬⣬' '⠘⢘⡘⣘⠸⢸⡸⣸⠜⢜⡜⣜⠼⢼⡼⣼'
37    '⠊⢊⡊⣊⠪⢪⡪⣪⠎⢎⡎⣎⠮⢮⡮⣮' '⠚⢚⡚⣚⠺⢺⡺⣺⠞⢞⡞⣞⠾⢾⡾⣾'
38    '⠁⢁⡁⣁⠡⢡⡡⣡⠅⢅⡅⣅⠥⢥⡥⣥' '⠑⢑⡑⣑⠱⢱⡱⣱⠕⢕⡕⣕⠵⢵⡵⣵'
39    '⠃⢃⡃⣃⠣⢣⡣⣣⠇⢇⡇⣇⠧⢧⡧⣧' '⠓⢓⡓⣓⠳⢳⡳⣳⠗⢗⡗⣗⠷⢷⡷⣷'
40    '⠉⢉⡉⣉⠩⢩⡩⣩⠍⢍⡍⣍⠭⢭⡭⣭' '⠙⢙⡙⣙⠹⢹⡹⣹⠝⢝⡝⣝⠽⢽⡽⣽'
41    '⠋⢋⡋⣋⠫⢫⡫⣫⠏⢏⡏⣏⠯⢯⡯⣯' '⠛⢛⡛⣛⠻⢻⡻⣻⠟⢟⡟⣟⠿⢿⡿⣿')
42
43
44def openio(path, mode='r', buffering=-1):
45    # allow '-' for stdin/stdout
46    if path == '-':
47        if mode == 'r':
48            return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
49        else:
50            return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
51    else:
52        return open(path, mode, buffering)
53
54class LinesIO:
55    def __init__(self, maxlen=None):
56        self.maxlen = maxlen
57        self.lines = co.deque(maxlen=maxlen)
58        self.tail = io.StringIO()
59
60        # trigger automatic sizing
61        if maxlen == 0:
62            self.resize(0)
63
64    def write(self, s):
65        # note using split here ensures the trailing string has no newline
66        lines = s.split('\n')
67
68        if len(lines) > 1 and self.tail.getvalue():
69            self.tail.write(lines[0])
70            lines[0] = self.tail.getvalue()
71            self.tail = io.StringIO()
72
73        self.lines.extend(lines[:-1])
74
75        if lines[-1]:
76            self.tail.write(lines[-1])
77
78    def resize(self, maxlen):
79        self.maxlen = maxlen
80        if maxlen == 0:
81            maxlen = shutil.get_terminal_size((80, 5))[1]
82        if maxlen != self.lines.maxlen:
83            self.lines = co.deque(self.lines, maxlen=maxlen)
84
85    canvas_lines = 1
86    def draw(self):
87        # did terminal size change?
88        if self.maxlen == 0:
89            self.resize(0)
90
91        # first thing first, give ourself a canvas
92        while LinesIO.canvas_lines < len(self.lines):
93            sys.stdout.write('\n')
94            LinesIO.canvas_lines += 1
95
96        # clear the bottom of the canvas if we shrink
97        shrink = LinesIO.canvas_lines - len(self.lines)
98        if shrink > 0:
99            for i in range(shrink):
100                sys.stdout.write('\r')
101                if shrink-1-i > 0:
102                    sys.stdout.write('\x1b[%dA' % (shrink-1-i))
103                sys.stdout.write('\x1b[K')
104                if shrink-1-i > 0:
105                    sys.stdout.write('\x1b[%dB' % (shrink-1-i))
106            sys.stdout.write('\x1b[%dA' % shrink)
107            LinesIO.canvas_lines = len(self.lines)
108
109        for i, line in enumerate(self.lines):
110            # move cursor, clear line, disable/reenable line wrapping
111            sys.stdout.write('\r')
112            if len(self.lines)-1-i > 0:
113                sys.stdout.write('\x1b[%dA' % (len(self.lines)-1-i))
114            sys.stdout.write('\x1b[K')
115            sys.stdout.write('\x1b[?7l')
116            sys.stdout.write(line)
117            sys.stdout.write('\x1b[?7h')
118            if len(self.lines)-1-i > 0:
119                sys.stdout.write('\x1b[%dB' % (len(self.lines)-1-i))
120        sys.stdout.flush()
121
122
123# space filling Hilbert-curve
124#
125# note we memoize the last curve since this is a bit expensive
126#
127@ft.lru_cache(1)
128def hilbert_curve(width, height):
129    # based on generalized Hilbert curves:
130    # https://github.com/jakubcerveny/gilbert
131    #
132    def hilbert_(x, y, a_x, a_y, b_x, b_y):
133        w = abs(a_x+a_y)
134        h = abs(b_x+b_y)
135        a_dx = -1 if a_x < 0 else +1 if a_x > 0 else 0
136        a_dy = -1 if a_y < 0 else +1 if a_y > 0 else 0
137        b_dx = -1 if b_x < 0 else +1 if b_x > 0 else 0
138        b_dy = -1 if b_y < 0 else +1 if b_y > 0 else 0
139
140        # trivial row
141        if h == 1:
142            for _ in range(w):
143                yield (x,y)
144                x, y = x+a_dx, y+a_dy
145            return
146
147        # trivial column
148        if w == 1:
149            for _ in range(h):
150                yield (x,y)
151                x, y = x+b_dx, y+b_dy
152            return
153
154        a_x_, a_y_ = a_x//2, a_y//2
155        b_x_, b_y_ = b_x//2, b_y//2
156        w_ = abs(a_x_+a_y_)
157        h_ = abs(b_x_+b_y_)
158
159        if 2*w > 3*h:
160            # prefer even steps
161            if w_ % 2 != 0 and w > 2:
162                a_x_, a_y_ = a_x_+a_dx, a_y_+a_dy
163
164            # split in two
165            yield from hilbert_(x, y, a_x_, a_y_, b_x, b_y)
166            yield from hilbert_(x+a_x_, y+a_y_, a_x-a_x_, a_y-a_y_, b_x, b_y)
167        else:
168            # prefer even steps
169            if h_ % 2 != 0 and h > 2:
170                b_x_, b_y_ = b_x_+b_dx, b_y_+b_dy
171
172            # split in three
173            yield from hilbert_(x, y, b_x_, b_y_, a_x_, a_y_)
174            yield from hilbert_(x+b_x_, y+b_y_, a_x, a_y, b_x-b_x_, b_y-b_y_)
175            yield from hilbert_(
176                x+(a_x-a_dx)+(b_x_-b_dx), y+(a_y-a_dy)+(b_y_-b_dy),
177                -b_x_, -b_y_, -(a_x-a_x_), -(a_y-a_y_))
178
179    if width >= height:
180        curve = hilbert_(0, 0, +width, 0, 0, +height)
181    else:
182        curve = hilbert_(0, 0, 0, +height, +width, 0)
183
184    return list(curve)
185
186# space filling Z-curve/Lebesgue-curve
187#
188# note we memoize the last curve since this is a bit expensive
189#
190@ft.lru_cache(1)
191def lebesgue_curve(width, height):
192    # we create a truncated Z-curve by simply filtering out the points
193    # that are outside our region
194    curve = []
195    for i in range(2**(2*m.ceil(m.log2(max(width, height))))):
196        # we just operate on binary strings here because it's easier
197        b = '{:0{}b}'.format(i, 2*m.ceil(m.log2(i+1)/2))
198        x = int(b[1::2], 2) if b[1::2] else 0
199        y = int(b[0::2], 2) if b[0::2] else 0
200        if x < width and y < height:
201            curve.append((x, y))
202
203    return curve
204
205
206class Block(int):
207    __slots__ = ()
208    def __new__(cls, state=0, *,
209            wear=0,
210            readed=False,
211            proged=False,
212            erased=False):
213        return super().__new__(cls,
214            state
215            | (wear << 3)
216            | (1 if readed else 0)
217            | (2 if proged else 0)
218            | (4 if erased else 0))
219
220    @property
221    def wear(self):
222        return self >> 3
223
224    @property
225    def readed(self):
226        return (self & 1) != 0
227
228    @property
229    def proged(self):
230        return (self & 2) != 0
231
232    @property
233    def erased(self):
234        return (self & 4) != 0
235
236    def read(self):
237        return Block(int(self) | 1)
238
239    def prog(self):
240        return Block(int(self) | 2)
241
242    def erase(self):
243        return Block((int(self) | 4) + 8)
244
245    def clear(self):
246        return Block(int(self) & ~7)
247
248    def __or__(self, other):
249        return Block(
250            (int(self) | int(other)) & 7,
251            wear=max(self.wear, other.wear))
252
253    def worn(self, max_wear, *,
254            block_cycles=None,
255            wear_chars=None,
256            **_):
257        if wear_chars is None:
258            wear_chars = WEAR_CHARS
259
260        if block_cycles:
261            return self.wear / block_cycles
262        else:
263            return self.wear / max(max_wear, len(wear_chars))
264
265    def draw(self, max_wear, char=None, *,
266            read=True,
267            prog=True,
268            erase=True,
269            wear=False,
270            block_cycles=None,
271            color=True,
272            subscripts=False,
273            dots=False,
274            braille=False,
275            chars=None,
276            wear_chars=None,
277            colors=None,
278            wear_colors=None,
279            **_):
280        # fallback to default chars/colors
281        if chars is None:
282            chars = CHARS
283        if len(chars) < len(CHARS):
284            chars = chars + CHARS[len(chars):]
285
286        if colors is None:
287            if braille or dots:
288                colors = COLORS_DOTS
289            else:
290                colors = COLORS
291        if len(colors) < len(COLORS):
292            colors = colors + COLORS[len(colors):]
293
294        if wear_chars is None:
295            if subscripts:
296                wear_chars = WEAR_CHARS_SUBSCRIPTS
297            else:
298                wear_chars = WEAR_CHARS
299
300        if wear_colors is None:
301            wear_colors = WEAR_COLORS
302
303        # compute char/color
304        c = chars[3]
305        f = [colors[3]]
306
307        if wear:
308            w = min(
309                self.worn(
310                    max_wear,
311                    block_cycles=block_cycles,
312                    wear_chars=wear_chars),
313                1)
314
315            c = wear_chars[int(w * (len(wear_chars)-1))]
316            f.append(wear_colors[int(w * (len(wear_colors)-1))])
317
318        if erase and self.erased:
319            c = chars[2]
320            f.append(colors[2])
321        elif prog and self.proged:
322            c = chars[1]
323            f.append(colors[1])
324        elif read and self.readed:
325            c = chars[0]
326            f.append(colors[0])
327
328        # override char?
329        if char:
330            c = char
331
332        # apply colors
333        if f and color:
334            c = '%s%s\x1b[m' % (
335                ''.join('\x1b[%sm' % f_ for f_ in f),
336                c)
337
338        return c
339
340
341class Bd:
342    def __init__(self, *,
343            size=1,
344            count=1,
345            width=None,
346            height=1,
347            blocks=None):
348        if width is None:
349            width = count
350
351        if blocks is None:
352            self.blocks = [Block() for _ in range(width*height)]
353        else:
354            self.blocks = blocks
355        self.size = size
356        self.count = count
357        self.width = width
358        self.height = height
359
360    def _op(self, f, block=None, off=None, size=None):
361        if block is None:
362            range_ = range(len(self.blocks))
363        else:
364            if off is None:
365                off, size = 0, self.size
366            elif size is None:
367                off, size = 0, off
368
369            # update our geometry? this will do nothing if we haven't changed
370            self.resize(
371                size=max(self.size, off+size),
372                count=max(self.count, block+1))
373
374            # map to our block space
375            start = (block*self.size + off) / (self.size*self.count)
376            stop = (block*self.size + off+size) / (self.size*self.count)
377
378            range_ = range(
379                m.floor(start*len(self.blocks)),
380                m.ceil(stop*len(self.blocks)))
381
382        # apply the op
383        for i in range_:
384            self.blocks[i] = f(self.blocks[i])
385
386    def read(self, block=None, off=None, size=None):
387        self._op(Block.read, block, off, size)
388
389    def prog(self, block=None, off=None, size=None):
390        self._op(Block.prog, block, off, size)
391
392    def erase(self, block=None, off=None, size=None):
393        self._op(Block.erase, block, off, size)
394
395    def clear(self, block=None, off=None, size=None):
396        self._op(Block.clear, block, off, size)
397
398    def copy(self):
399        return Bd(
400            blocks=self.blocks.copy(),
401            size=self.size,
402            count=self.count,
403            width=self.width,
404            height=self.height)
405
406    def resize(self, *,
407            size=None,
408            count=None,
409            width=None,
410            height=None):
411        size = size if size is not None else self.size
412        count = count if count is not None else self.count
413        width = width if width is not None else self.width
414        height = height if height is not None else self.height
415
416        if (size == self.size
417                and count == self.count
418                and width == self.width
419                and height == self.height):
420            return
421
422        # transform our blocks
423        blocks = []
424        for x in range(width*height):
425            # map from new bd space
426            start = m.floor(x * (size*count)/(width*height))
427            stop = m.ceil((x+1) * (size*count)/(width*height))
428            start_block = start // size
429            start_off = start % size
430            stop_block = stop // size
431            stop_off = stop % size
432            # map to old bd space
433            start = start_block*self.size + start_off
434            stop = stop_block*self.size + stop_off
435            start = m.floor(start * len(self.blocks)/(self.size*self.count))
436            stop = m.ceil(stop * len(self.blocks)/(self.size*self.count))
437
438            # aggregate state
439            blocks.append(ft.reduce(
440                Block.__or__,
441                self.blocks[start:stop],
442                Block()))
443
444        self.size = size
445        self.count = count
446        self.width = width
447        self.height = height
448        self.blocks = blocks
449
450    def draw(self, row, *,
451            read=False,
452            prog=False,
453            erase=False,
454            wear=False,
455            hilbert=False,
456            lebesgue=False,
457            dots=False,
458            braille=False,
459            **args):
460        # find max wear?
461        max_wear = None
462        if wear:
463            max_wear = max(b.wear for b in self.blocks)
464
465        # fold via a curve?
466        if hilbert:
467            grid = [None]*(self.width*self.height)
468            for (x,y), b in zip(
469                    hilbert_curve(self.width, self.height),
470                    self.blocks):
471                grid[x + y*self.width] = b
472        elif lebesgue:
473            grid = [None]*(self.width*self.height)
474            for (x,y), b in zip(
475                    lebesgue_curve(self.width, self.height),
476                    self.blocks):
477                grid[x + y*self.width] = b
478        else:
479            grid = self.blocks
480
481        # need to wait for more trace output before rendering
482        #
483        # this is sort of a hack that knows the output is going to a terminal
484        if (braille and self.height < 4) or (dots and self.height < 2):
485            needed_height = 4 if braille else 2
486
487            self.history = getattr(self, 'history', [])
488            self.history.append(grid)
489
490            if len(self.history)*self.height < needed_height:
491                # skip for now
492                return None
493
494            grid = list(it.chain.from_iterable(
495                # did we resize?
496                it.islice(it.chain(h, it.repeat(Block())),
497                    self.width*self.height)
498                for h in self.history))
499            self.history = []
500
501        line = []
502        if braille:
503            # encode into a byte
504            for x in range(0, self.width, 2):
505                byte_b = 0
506                best_b = Block()
507                for i in range(2*4):
508                    b = grid[x+(2-1-(i%2)) + ((row*4)+(4-1-(i//2)))*self.width]
509                    best_b |= b
510                    if ((read and b.readed)
511                            or (prog and b.proged)
512                            or (erase and b.erased)
513                            or (not read and not prog and not erase
514                                and wear and b.worn(max_wear, **args) >= 0.7)):
515                        byte_b |= 1 << i
516
517                line.append(best_b.draw(
518                    max_wear,
519                    CHARS_BRAILLE[byte_b],
520                    braille=True,
521                    read=read,
522                    prog=prog,
523                    erase=erase,
524                    wear=wear,
525                    **args))
526        elif dots:
527            # encode into a byte
528            for x in range(self.width):
529                byte_b = 0
530                best_b = Block()
531                for i in range(2):
532                    b = grid[x + ((row*2)+(2-1-i))*self.width]
533                    best_b |= b
534                    if ((read and b.readed)
535                            or (prog and b.proged)
536                            or (erase and b.erased)
537                            or (not read and not prog and not erase
538                                and wear and b.worn(max_wear, **args) >= 0.7)):
539                        byte_b |= 1 << i
540
541                line.append(best_b.draw(
542                    max_wear,
543                    CHARS_DOTS[byte_b],
544                    dots=True,
545                    read=read,
546                    prog=prog,
547                    erase=erase,
548                    wear=wear,
549                    **args))
550        else:
551            for x in range(self.width):
552                line.append(grid[x + row*self.width].draw(
553                    max_wear,
554                    read=read,
555                    prog=prog,
556                    erase=erase,
557                    wear=wear,
558                    **args))
559
560        return ''.join(line)
561
562
563
564def main(path='-', *,
565        read=False,
566        prog=False,
567        erase=False,
568        wear=False,
569        block=(None,None),
570        off=(None,None),
571        block_size=None,
572        block_count=None,
573        block_cycles=None,
574        reset=False,
575        color='auto',
576        dots=False,
577        braille=False,
578        width=None,
579        height=None,
580        lines=None,
581        cat=False,
582        hilbert=False,
583        lebesgue=False,
584        coalesce=None,
585        sleep=None,
586        keep_open=False,
587        **args):
588    # figure out what color should be
589    if color == 'auto':
590        color = sys.stdout.isatty()
591    elif color == 'always':
592        color = True
593    else:
594        color = False
595
596    # exclusive wear or read/prog/erase by default
597    if not read and not prog and not erase and not wear:
598        read = True
599        prog = True
600        erase = True
601
602    # assume a reasonable lines/height if not specified
603    #
604    # note that we let height = None if neither hilbert or lebesgue
605    # are specified, this is a bit special as the default may be less
606    # than one character in height.
607    if height is None and (hilbert or lebesgue):
608        if lines is not None:
609            height = lines
610        else:
611            height = 5
612
613    if lines is None:
614        if height is not None:
615            lines = height
616        else:
617            lines = 5
618
619    # allow ranges for blocks/offs
620    block_start = block[0]
621    block_stop = block[1] if len(block) > 1 else block[0]+1
622    off_start = off[0]
623    off_stop = off[1] if len(off) > 1 else off[0]+1
624
625    if block_start is None:
626        block_start = 0
627    if block_stop is None and block_count is not None:
628        block_stop = block_count
629    if off_start is None:
630        off_start = 0
631    if off_stop is None and block_size is not None:
632        off_stop = block_size
633
634    # create a block device representation
635    bd = Bd()
636
637    def resize(*, size=None, count=None):
638        nonlocal bd
639
640        # size may be overriden by cli args
641        if block_size is not None:
642            size = block_size
643        elif off_stop is not None:
644            size = off_stop-off_start
645
646        if block_count is not None:
647            count = block_count
648        elif block_stop is not None:
649            count = block_stop-block_start
650
651        # figure out best width/height
652        if width is None:
653            width_ = min(80, shutil.get_terminal_size((80, 5))[0])
654        elif width:
655            width_ = width
656        else:
657            width_ = shutil.get_terminal_size((80, 5))[0]
658
659        if height is None:
660            height_ = 0
661        elif height:
662            height_ = height
663        else:
664            height_ = shutil.get_terminal_size((80, 5))[1]
665
666        bd.resize(
667            size=size,
668            count=count,
669            # scale if we're printing with dots or braille
670            width=2*width_ if braille else width_,
671            height=max(1,
672                4*height_ if braille
673                else 2*height_ if dots
674                else height_))
675    resize()
676
677    # parse a line of trace output
678    pattern = re.compile(
679        '^(?P<file>[^:]*):(?P<line>[0-9]+):trace:.*?bd_(?:'
680            '(?P<create>create\w*)\('
681                '(?:'
682                    'block_size=(?P<block_size>\w+)'
683                    '|' 'block_count=(?P<block_count>\w+)'
684                    '|' '.*?' ')*' '\)'
685            '|' '(?P<read>read)\('
686                '\s*(?P<read_ctx>\w+)' '\s*,'
687                '\s*(?P<read_block>\w+)' '\s*,'
688                '\s*(?P<read_off>\w+)' '\s*,'
689                '\s*(?P<read_buffer>\w+)' '\s*,'
690                '\s*(?P<read_size>\w+)' '\s*\)'
691            '|' '(?P<prog>prog)\('
692                '\s*(?P<prog_ctx>\w+)' '\s*,'
693                '\s*(?P<prog_block>\w+)' '\s*,'
694                '\s*(?P<prog_off>\w+)' '\s*,'
695                '\s*(?P<prog_buffer>\w+)' '\s*,'
696                '\s*(?P<prog_size>\w+)' '\s*\)'
697            '|' '(?P<erase>erase)\('
698                '\s*(?P<erase_ctx>\w+)' '\s*,'
699                '\s*(?P<erase_block>\w+)'
700                '\s*\(\s*(?P<erase_size>\w+)\s*\)' '\s*\)'
701            '|' '(?P<sync>sync)\('
702                '\s*(?P<sync_ctx>\w+)' '\s*\)' ')\s*$')
703    def parse(line):
704        nonlocal bd
705
706        # string searching is much faster than the regex here, and this
707        # actually has a big impact given how much trace output comes
708        # through here
709        if 'trace' not in line or 'bd' not in line:
710            return False
711        m = pattern.match(line)
712        if not m:
713            return False
714
715        if m.group('create'):
716            # update our block size/count
717            size = int(m.group('block_size'), 0)
718            count = int(m.group('block_count'), 0)
719
720            resize(size=size, count=count)
721            if reset:
722                bd = Bd(
723                    size=bd.size,
724                    count=bd.count,
725                    width=bd.width,
726                    height=bd.height)
727            return True
728
729        elif m.group('read') and read:
730            block = int(m.group('read_block'), 0)
731            off = int(m.group('read_off'), 0)
732            size = int(m.group('read_size'), 0)
733
734            if block_stop is not None and block >= block_stop:
735                return False
736            block -= block_start
737            if off_stop is not None:
738                if off >= off_stop:
739                    return False
740                size = min(size, off_stop-off)
741            off -= off_start
742
743            bd.read(block, off, size)
744            return True
745
746        elif m.group('prog') and prog:
747            block = int(m.group('prog_block'), 0)
748            off = int(m.group('prog_off'), 0)
749            size = int(m.group('prog_size'), 0)
750
751            if block_stop is not None and block >= block_stop:
752                return False
753            block -= block_start
754            if off_stop is not None:
755                if off >= off_stop:
756                    return False
757                size = min(size, off_stop-off)
758            off -= off_start
759
760            bd.prog(block, off, size)
761            return True
762
763        elif m.group('erase') and (erase or wear):
764            block = int(m.group('erase_block'), 0)
765            size = int(m.group('erase_size'), 0)
766
767            if block_stop is not None and block >= block_stop:
768                return False
769            block -= block_start
770            if off_stop is not None:
771                size = min(size, off_stop)
772            off = -off_start
773
774            bd.erase(block, off, size)
775            return True
776
777        else:
778            return False
779
780    # print trace output
781    def draw(f):
782        def writeln(s=''):
783            f.write(s)
784            f.write('\n')
785        f.writeln = writeln
786
787        # don't forget we've scaled this for braille/dots!
788        for row in range(
789                m.ceil(bd.height/4) if braille
790                else m.ceil(bd.height/2) if dots
791                else bd.height):
792            line = bd.draw(row,
793                read=read,
794                prog=prog,
795                erase=erase,
796                wear=wear,
797                block_cycles=block_cycles,
798                color=color,
799                dots=dots,
800                braille=braille,
801                hilbert=hilbert,
802                lebesgue=lebesgue,
803                **args)
804            if line:
805                f.writeln(line)
806
807        bd.clear()
808        resize()
809
810
811    # read/parse/coalesce operations
812    if cat:
813        ring = sys.stdout
814    else:
815        ring = LinesIO(lines)
816
817    # if sleep print in background thread to avoid getting stuck in a read call
818    event = th.Event()
819    lock = th.Lock()
820    if sleep:
821        done = False
822        def background():
823            while not done:
824                event.wait()
825                event.clear()
826                with lock:
827                    draw(ring)
828                    if not cat:
829                        ring.draw()
830                time.sleep(sleep or 0.01)
831        th.Thread(target=background, daemon=True).start()
832
833    try:
834        while True:
835            with openio(path) as f:
836                changed = 0
837                for line in f:
838                    with lock:
839                        changed += parse(line)
840
841                        # need to redraw?
842                        if changed and (not coalesce or changed >= coalesce):
843                            if sleep:
844                                event.set()
845                            else:
846                                draw(ring)
847                                if not cat:
848                                    ring.draw()
849                            changed = 0
850
851            if not keep_open:
852                break
853            # don't just flood open calls
854            time.sleep(sleep or 0.1)
855    except FileNotFoundError as e:
856        print("error: file not found %r" % path)
857        sys.exit(-1)
858    except KeyboardInterrupt:
859        pass
860
861    if sleep:
862        done = True
863        lock.acquire() # avoids https://bugs.python.org/issue42717
864    if not cat:
865        sys.stdout.write('\n')
866
867
868if __name__ == "__main__":
869    import sys
870    import argparse
871    parser = argparse.ArgumentParser(
872        description="Display operations on block devices based on "
873            "trace output.",
874        allow_abbrev=False)
875    parser.add_argument(
876        'path',
877        nargs='?',
878        help="Path to read from.")
879    parser.add_argument(
880        '-r', '--read',
881        action='store_true',
882        help="Render reads.")
883    parser.add_argument(
884        '-p', '--prog',
885        action='store_true',
886        help="Render progs.")
887    parser.add_argument(
888        '-e', '--erase',
889        action='store_true',
890        help="Render erases.")
891    parser.add_argument(
892        '-w', '--wear',
893        action='store_true',
894        help="Render wear.")
895    parser.add_argument(
896        '-b', '--block',
897        type=lambda x: tuple(
898            int(x, 0) if x.strip() else None
899            for x in x.split(',')),
900        help="Show a specific block or range of blocks.")
901    parser.add_argument(
902        '-i', '--off',
903        type=lambda x: tuple(
904            int(x, 0) if x.strip() else None
905            for x in x.split(',')),
906        help="Show a specific offset or range of offsets.")
907    parser.add_argument(
908        '-B', '--block-size',
909        type=lambda x: int(x, 0),
910        help="Assume a specific block size.")
911    parser.add_argument(
912        '--block-count',
913        type=lambda x: int(x, 0),
914        help="Assume a specific block count.")
915    parser.add_argument(
916        '-C', '--block-cycles',
917        type=lambda x: int(x, 0),
918        help="Assumed maximum number of erase cycles when measuring wear.")
919    parser.add_argument(
920        '-R', '--reset',
921        action='store_true',
922        help="Reset wear on block device initialization.")
923    parser.add_argument(
924        '--color',
925        choices=['never', 'always', 'auto'],
926        default='auto',
927        help="When to use terminal colors. Defaults to 'auto'.")
928    parser.add_argument(
929        '--subscripts',
930        action='store_true',
931        help="Use unicode subscripts for showing wear.")
932    parser.add_argument(
933        '-:', '--dots',
934        action='store_true',
935        help="Use 1x2 ascii dot characters.")
936    parser.add_argument(
937        '-⣿', '--braille',
938        action='store_true',
939        help="Use 2x4 unicode braille characters. Note that braille characters "
940            "sometimes suffer from inconsistent widths.")
941    parser.add_argument(
942        '--chars',
943        help="Characters to use for read, prog, erase, noop operations.")
944    parser.add_argument(
945        '--wear-chars',
946        help="Characters to use for showing wear.")
947    parser.add_argument(
948        '--colors',
949        type=lambda x: [x.strip() for x in x.split(',')],
950        help="Colors to use for read, prog, erase, noop operations.")
951    parser.add_argument(
952        '--wear-colors',
953        type=lambda x: [x.strip() for x in x.split(',')],
954        help="Colors to use for showing wear.")
955    parser.add_argument(
956        '-W', '--width',
957        nargs='?',
958        type=lambda x: int(x, 0),
959        const=0,
960        help="Width in columns. 0 uses the terminal width. Defaults to "
961            "min(terminal, 80).")
962    parser.add_argument(
963        '-H', '--height',
964        nargs='?',
965        type=lambda x: int(x, 0),
966        const=0,
967        help="Height in rows. 0 uses the terminal height. Defaults to 1.")
968    parser.add_argument(
969        '-n', '--lines',
970        nargs='?',
971        type=lambda x: int(x, 0),
972        const=0,
973        help="Show this many lines of history. 0 uses the terminal height. "
974            "Defaults to 5.")
975    parser.add_argument(
976        '-z', '--cat',
977        action='store_true',
978        help="Pipe directly to stdout.")
979    parser.add_argument(
980        '-U', '--hilbert',
981        action='store_true',
982        help="Render as a space-filling Hilbert curve.")
983    parser.add_argument(
984        '-Z', '--lebesgue',
985        action='store_true',
986        help="Render as a space-filling Z-curve.")
987    parser.add_argument(
988        '-c', '--coalesce',
989        type=lambda x: int(x, 0),
990        help="Number of operations to coalesce together.")
991    parser.add_argument(
992        '-s', '--sleep',
993        type=float,
994        help="Time in seconds to sleep between reads, coalescing operations.")
995    parser.add_argument(
996        '-k', '--keep-open',
997        action='store_true',
998        help="Reopen the pipe on EOF, useful when multiple "
999            "processes are writing.")
1000    sys.exit(main(**{k: v
1001        for k, v in vars(parser.parse_intermixed_args()).items()
1002        if v is not None}))
1003