File: bj.py
   1 #!/usr/bin/python
   2 
   3 # The MIT License (MIT)
   4 #
   5 # Copyright (c) 2026 pacman64
   6 #
   7 # Permission is hereby granted, free of charge, to any person obtaining a copy
   8 # of this software and associated documentation files (the "Software"), to deal
   9 # in the Software without restriction, including without limitation the rights
  10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 # copies of the Software, and to permit persons to whom the Software is
  12 # furnished to do so, subject to the following conditions:
  13 #
  14 # The above copyright notice and this permission notice shall be included in
  15 # all copies or substantial portions of the Software.
  16 #
  17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23 # SOFTWARE.
  24 
  25 
  26 from curses import (
  27     cbreak, curs_set, endwin, initscr, noecho, resetty, savetty, set_escdelay,
  28     A_NORMAL, A_REVERSE,
  29 )
  30 from json import dumps, load, loads
  31 from os import dup2
  32 from sys import argv, stderr, stdin
  33 
  34 
  35 info = '''
  36 bj [options...] [file...]
  37 
  38 Browse Json is a text user-interface (TUI) which lets interactively pick/zoom
  39 JSON input data. Picking anything is optional, so the final result on stdout
  40 is either nothing or valid JSON, assuming the input is valid JSON.
  41 
  42 All (optional) leading options start with either single or double-dash:
  43 
  44     -c, -compact, -json0    emit compact JSON output
  45     -d, -default [json]     default JSON string, when not picking anything
  46     -f, -fallback [json]    fallback JSON string, same as `-d`, or `-default`
  47     -h, -help               show this help message
  48     -v, -verbose            also show gron-style path on the standard error
  49 
  50 You can also view this help message by pressing the F1 key while browsing
  51 folders: you can exit the help viewer via any of F1, F10, F12, or Escape.
  52 '''
  53 
  54 
  55 class SimpleTUI:
  56     '''
  57     Manager to start/stop a no-color text user-interface (TUI), allowing for
  58     standard input/output to be used normally before method `start` is called
  59     and after method `stop` is called.
  60     '''
  61 
  62     def __init__(self):
  63         self.screen = None
  64 
  65     def start(self, out_fd = -1, esc_delay = -1):
  66         '''
  67         Start interactive-mode: the first optional argument should be more
  68         than 2, if given, since it would mess with stdio, which is precisely
  69         what it's meant to avoid doing.
  70         '''
  71 
  72         if out_fd >= 0:
  73             from os import dup2
  74 
  75             # keep original stdout as /dev/fd/...
  76             dup2(1, out_fd)
  77             # separate live output from final (optional) result on stdout
  78             with open('/dev/tty', 'rb') as inp, open('/dev/tty', 'wb') as out:
  79                 dup2(inp.fileno(), 0)
  80                 dup2(out.fileno(), 1)
  81 
  82         self.screen = initscr()
  83         savetty()
  84         noecho()
  85         cbreak()
  86         self.screen.keypad(True)
  87         curs_set(0)
  88         if esc_delay >= 0:
  89             set_escdelay(esc_delay)
  90 
  91     def stop(self):
  92         'Stop interactive-mode.'
  93         if self.screen:
  94             resetty()
  95             endwin()
  96 
  97 
  98 class ValueBrowserTUI:
  99     '''
 100     This is a scrollable viewer to browse JSON fields. After initializing it
 101     with a TUI screen value, you can call its method `run`.
 102     '''
 103 
 104     def __init__(self, screen, quit_set = ('KEY_F(10)', 'KEY_F(12)', '\x1b')):
 105         'Optional argument controls which ncurses keys quit the viewer.'
 106 
 107         self.data = None
 108         self.max_view_size = -1
 109         self.side_step = 1
 110         self.handlers = {
 111             'KEY_RESIZE': lambda: self._on_resize(),
 112             'KEY_UP': lambda: self._on_up(),
 113             'KEY_DOWN': lambda: self._on_down(),
 114             'KEY_NPAGE': lambda: self._on_page_down(),
 115             'KEY_PPAGE': lambda: self._on_page_up(),
 116             'KEY_HOME': lambda: self._on_home(),
 117             'KEY_END': lambda: self._on_end(),
 118             'KEY_LEFT': lambda: self._on_left(),
 119             'KEY_RIGHT': lambda: self._on_right(),
 120             'KEY_BACKSPACE': lambda: self._on_left(),
 121             '\t': lambda: self._on_right(),
 122         }
 123         if quit_set:
 124             for k in quit_set:
 125                 self.handlers[k] = None
 126 
 127         self._screen = screen
 128         self._inner_width = 0
 129         self._inner_height = 0
 130         self._max_line_width = 0
 131         self._pick = 0
 132         self._max_top = 0
 133         self._max_left = 0
 134         self._path = []
 135         self._stack = []
 136         self._values = []
 137         self.pick = None
 138 
 139     def run(self):
 140         'Interactively view/browse the fields/items in the value given.'
 141 
 142         self._path = []
 143         self._stack = []
 144         self._values = []
 145         res = self._run()
 146         self.data = None
 147         self._path = []
 148         self._stack = []
 149         self._values = []
 150         return res
 151 
 152     def _run(self):
 153         self._on_resize()
 154         self._cache_entries()
 155 
 156         while True:
 157             self._redraw()
 158             k = self._screen.getkey()
 159             if k == '\n':
 160                 if len(self._stack) == 0:
 161                     return ('data', self.data, k)
 162                 _, pick = self._get()
 163 
 164                 from io import StringIO
 165 
 166                 sb = StringIO()
 167                 self._gron(sb)
 168                 return (sb.getvalue(), pick, k)
 169 
 170             if k in self.handlers:
 171                 h = self.handlers[k]
 172                 if h is None or h() is False:
 173                     return (None, None, None)
 174             elif len(k) == 1:
 175                 i = self._seek(k, self._pick + 1)
 176                 if i < 0:
 177                     i = self._seek(k, 0)
 178                 if i >= 0:
 179                     self._pick = i
 180 
 181     def _fit_string(self, s):
 182         maxlen = max(self._inner_width, 0)
 183         return s if len(s) <= maxlen else s[:maxlen]
 184 
 185     def _is_ident(self, s: str):
 186         if len(s) == 0 or '0' <= s[0] <= '9':
 187             return False
 188         return all(e.isalnum() or e == '_' for e in s)
 189 
 190     def _gron(self, sb):
 191         from itertools import islice
 192         from json import dumps
 193 
 194         sb.write('data')
 195         for k in islice(self._path, 1, None):
 196             if k is None:
 197                 continue
 198             if isinstance(k, int):
 199                 sb.write('[')
 200                 sb.write(str(k))
 201                 sb.write(']')
 202             else:
 203                 if self._is_ident(k):
 204                     sb.write('.')
 205                     sb.write(k)
 206                 else:
 207                     sb.write('[')
 208                     sb.write(dumps(k))
 209                     sb.write(']')
 210 
 211     def _redraw(self):
 212         screen = self._screen
 213         iw = self._inner_width
 214         ih = self._inner_height
 215 
 216         if iw < 10 or ih < 10:
 217             return
 218 
 219         from io import StringIO
 220 
 221         sb = StringIO()
 222         if self.title:
 223             sb.write(self.title)
 224             sb.write(': ')
 225         self._gron(sb)
 226 
 227         if len(self._stack) > 1:
 228             title = sb.getvalue()
 229         elif self.title:
 230             title = f'{self.title}: {self._describe_type(self.data)}'
 231         else:
 232             title = self._describe_type(self.data)
 233 
 234         screen.erase()
 235 
 236         if not isinstance(self.data, (dict, list, tuple)):
 237             if title:
 238                 screen.addstr(0, 0, f'{title:<{iw}}')
 239             screen.addstr(1, 2, self._values[0], A_REVERSE)
 240             screen.refresh()
 241             return
 242 
 243         if title:
 244             style = A_NORMAL if len(self._stack) > 0 else A_REVERSE
 245             screen.addstr(0, 0, f'{title:<{iw}}', style)
 246 
 247         if len(self._stack) == 0:
 248             screen.refresh()
 249             return
 250 
 251         end = self._current_value()
 252         n = max(self._count_entries(), 1)
 253         start = self._pick - (self._pick % ih)
 254         stop = min(start + ih, n)
 255 
 256         if isinstance(end, (list, tuple)):
 257             n = len(end)
 258             if n > 0:
 259                 from math import ceil, log10
 260                 w = int(ceil(log10(n - 1))) if n > 1 else 1
 261                 msg = f'({self._pick:>{w},} < {n:,})'
 262                 style = A_NORMAL
 263             else:
 264                 style = A_REVERSE
 265                 msg = '[]'
 266             screen.addstr(0, iw - len(msg), self._fit_string(msg), style)
 267             self._redraw_array_entries(end)
 268         elif isinstance(end, dict):
 269             n = len(end)
 270             msg = f'({n:,} keys)' if n > 0 else '{}'
 271             style = A_NORMAL if n > 0 else A_REVERSE
 272             screen.addstr(0, iw - len(msg), self._fit_string(msg), style)
 273             self._redraw_object_entries(end)
 274         else:
 275             self._redraw_value(end)
 276 
 277         # show up/down arrows
 278         if start > 0 and n > 0:
 279             self._screen.addstr(1, iw - 1, '▲')
 280         if stop < n and n > 0:
 281             self._screen.addstr(ih, iw - 1, '▼')
 282 
 283         screen.refresh()
 284 
 285     def _redraw_array_entries(self, data, indent=2):
 286         screen = self._screen
 287         iw = self._inner_width
 288         ih = self._inner_height
 289         start = self._pick - (self._pick % ih)
 290         stop = min(start + ih, len(data))
 291 
 292         for i in range(start, stop):
 293             j = i - start
 294             try:
 295                 style = A_REVERSE if j == self._pick % ih else A_NORMAL
 296                 s = self._values[j]
 297                 screen.addnstr(j + 1, indent, s, iw - indent, style)
 298             except Exception as _:
 299                 # some utf-8 files have lines which upset func addstr
 300                 screen.addnstr(j + 1, 0, '?' * 20, iw, style)
 301 
 302     def _redraw_object_entries(self, data, indent=2):
 303         from itertools import islice
 304 
 305         screen = self._screen
 306         iw = self._inner_width
 307         ih = self._inner_height
 308         start = self._pick - (self._pick % ih)
 309         stop = min(start + ih, len(data))
 310         maxw = 0
 311         for k in islice(data.keys(), start, stop):
 312             maxw = max(maxw, len(k))
 313 
 314         for i, k in enumerate(islice(data.keys(), start, stop), start):
 315             j = i - start
 316             try:
 317                 style = A_REVERSE if j == self._pick % ih else A_NORMAL
 318                 screen.addnstr(j + 1, indent, k, iw - indent, A_NORMAL)
 319                 v = self._values[j]
 320                 x = indent + maxw + 2
 321                 screen.addnstr(j + 1, x, v, iw - x, style)
 322             except Exception as _:
 323                 # some utf-8 files have lines which upset func addstr
 324                 screen.addnstr(j + 1, 0, '?' * 20, iw, style)
 325 
 326     def _redraw_value(self, data, indent=2):
 327         screen = self._screen
 328         iw = self._inner_width
 329         style = A_REVERSE
 330 
 331         try:
 332             screen.addnstr(1, indent, self._values[0], iw - indent, style)
 333         except Exception as _:
 334             # some utf-8 files have lines which upset func addstr
 335             screen.addnstr(1, 0, '?' * 20, iw)
 336 
 337     def _cache_entries(self):
 338         from itertools import islice
 339 
 340         iw = self._inner_width
 341         ih = self._inner_height
 342         end = self._current_value()
 343 
 344         if len(self._stack) == 0 and len(self._values) == 0:
 345             self._values = tuple([self._json0(self.data)[:self._inner_width]])
 346             return
 347 
 348         if isinstance(end, (list, tuple)):
 349             start = self._pick - (self._pick % ih)
 350             stop = min(start + ih, len(end))
 351             self._values = tuple(
 352                 self._json0(e)[:iw] for e in islice(end, start, stop)
 353             )
 354         elif isinstance(end, dict):
 355             start = self._pick - (self._pick % ih)
 356             stop = min(start + ih, len(end))
 357             self._values = tuple(
 358                 self._json0(v)[:iw] for v in islice(end.values(), start, stop)
 359             )
 360         else:
 361             self._values = tuple([self._json0(end)[:iw]])
 362 
 363     def _change_selection(self, new):
 364         new = max(new, 0)
 365         limit = max(self._count_entries() - 1, 0)
 366         new = min(new, limit)
 367 
 368         if not self._in_page(new):
 369             self._pick = new
 370             self._cache_entries()
 371         else:
 372             self._pick = new
 373 
 374     def _in_page(self, new):
 375         end = self._current_value()
 376 
 377         if isinstance(end, (dict, list, tuple)):
 378             ih = self._inner_height
 379             start = self._pick - (self._pick % ih)
 380             stop = min(start + ih, len(end))
 381             return start <= new < stop
 382         else:
 383             return new == 0
 384 
 385     def _count_entries(self):
 386         end = self._current_value()
 387         n = len(end) if isinstance(end, (dict, list, tuple)) else -1
 388         return n
 389 
 390     def _current_value(self):
 391         return self._stack[-1] if len(self._stack) > 0 else self.data
 392 
 393     def _describe_type(self, data):
 394         kind = {
 395             bool: 'boolean',
 396             int: 'number',
 397             float: 'number',
 398             str: 'string',
 399             list: 'array',
 400             tuple: 'array',
 401             dict: 'object',
 402         }.get(type(data), '?')
 403         return f'{kind} ({len(data)})' if kind in ('array', 'object') else kind
 404 
 405     def _json0(self, data):
 406         from json import dumps
 407         return dumps(data, indent=None, separators=(', ', ': '),
 408                     allow_nan=False, check_circular=False)
 409 
 410     def _seek(self, k, start):
 411         from itertools import islice
 412 
 413         end = self._current_value()
 414         if not isinstance(end, dict):
 415             return -1
 416         if len(k) != 1:
 417             return -1
 418 
 419         k = k.lower()
 420         for i, e in enumerate(islice(end.keys(), start, None)):
 421             name = e[0]
 422             if name.startswith(k) or name.lower().startswith(k):
 423                 return start + i
 424         return -1
 425 
 426     def _on_resize(self):
 427         height, width = self._screen.getmaxyx()
 428         self._inner_width = width - 1
 429         self._inner_height = height - 1
 430         n = max(self._count_entries(), 1)
 431         self._max_top = max(n - self._inner_height, 0)
 432         ss = self.side_step
 433         self._max_left = self._max_line_width - self._inner_width - 1 + ss
 434         self._max_left = max(self._max_left, 0)
 435         if self._max_left >= self._inner_width - 1 + ss:
 436             self._max_left = 0
 437         self._cache_entries()
 438 
 439     def _on_up(self):
 440         if len(self._stack) == 0:
 441             self._on_right()
 442         else:
 443             self._change_selection(self._pick - 1)
 444 
 445     def _on_down(self):
 446         if len(self._stack) == 0:
 447             self._on_right()
 448         else:
 449             self._change_selection(self._pick + 1)
 450 
 451     def _on_page_up(self):
 452         if len(self._stack) == 0:
 453             self._on_right()
 454         else:
 455             self._change_selection(self._pick - self._inner_height)
 456 
 457     def _on_page_down(self):
 458         if len(self._stack) == 0:
 459             self._on_right()
 460         else:
 461             self._change_selection(self._pick + self._inner_height)
 462 
 463     def _on_home(self):
 464         if len(self._stack) == 0:
 465             self._on_right()
 466         else:
 467             self._change_selection(0)
 468 
 469     def _on_end(self):
 470         if len(self._stack) == 0:
 471             self._on_right()
 472         else:
 473             self._change_selection(self._count_entries() - 1)
 474 
 475     def _on_left(self):
 476         if len(self._stack) > 0:
 477             pick = self._path.pop()
 478             self._stack.pop()
 479             self._pick = 0
 480             if isinstance(pick, int):
 481                 self._pick = pick
 482             elif len(self._stack) > 0:
 483                 end = self._stack[-1]
 484                 for i, k in enumerate(end.keys()):
 485                     if k == pick:
 486                         self._pick = i
 487                         break
 488         self._cache_entries()
 489 
 490     def _on_right(self):
 491         if len(self._stack) == 0:
 492             self._path.append(None)
 493             self._stack.append(self.data)
 494             self._pick = 0
 495             self._cache_entries()
 496             return
 497 
 498         end = self._current_value()
 499         if isinstance(end, (dict, list, tuple)):
 500             k, v = self._get()
 501             self._path.append(k)
 502             self._stack.append(v)
 503             self._pick = 0
 504         self._cache_entries()
 505 
 506     def _get(self):
 507         end = self._current_value()
 508         if len(self._stack) == 0:
 509             return (None, end)
 510 
 511         if isinstance(end, (list, tuple)):
 512             i = self._pick
 513             return (i, end[i]) if 0 <= i < len(end) else (None, None)
 514 
 515         if isinstance(end, dict):
 516             for i, k in enumerate(end.keys()):
 517                 if i == self._pick:
 518                     return (k, end[k])
 519             return (None, None)
 520 
 521         return (0, end)
 522 
 523 
 524 class TextViewerTUI:
 525     '''
 526     This is a scrollable viewer for plain-text content. After initializing it
 527     with a TUI screen value, you can configure various fields, before running
 528     it by calling method `run`:
 529         - title, which is shown at the top in reverse-style
 530         - tab_stop, which controls how tabs are turned into spaces
 531         - side_step, which controls the speed of lateral side-scrolling
 532         - handlers, which has all ncurses key-bindings for the viewer
 533     '''
 534 
 535     def __init__(self, screen, quit_set = ('KEY_F(10)', 'KEY_F(12)', '\x1b')):
 536         'Optional argument controls which ncurses keys quit the viewer.'
 537 
 538         self.title = ''
 539         self.tab_stop = 4
 540         self.side_step = 1
 541         self.handlers = {
 542             'KEY_RESIZE': lambda: self._on_resize(),
 543             'KEY_UP': lambda: self._on_up(),
 544             'KEY_DOWN': lambda: self._on_down(),
 545             'KEY_NPAGE': lambda: self._on_page_down(),
 546             'KEY_PPAGE': lambda: self._on_page_up(),
 547             'KEY_HOME': lambda: self._on_home(),
 548             'KEY_END': lambda: self._on_end(),
 549             'KEY_LEFT': lambda: self._on_left(),
 550             'KEY_RIGHT': lambda: self._on_right(),
 551         }
 552         if quit_set:
 553             for k in quit_set:
 554                 self.handlers[k] = None
 555 
 556         self._screen = screen
 557         self._inner_width = 0
 558         self._inner_height = 0
 559         self._max_line_width = 0
 560         self._top = 0
 561         self._left = 0
 562         self._max_top = 0
 563         self._max_left = 0
 564         self._lines = tuple()
 565 
 566     def run(self, content):
 567         'Interactively view/browse the string/strings given.'
 568 
 569         if isinstance(content, BaseException):
 570             self._on_resize()
 571             self._show_error(content)
 572             self._screen.getkey()
 573             return
 574 
 575         ts = self.tab_stop
 576         if isinstance(content, str):
 577             self._lines = tuple(l.expandtabs(ts) for l in content.splitlines())
 578         else:
 579             self._lines = tuple(l.expandtabs(ts) for l in content)
 580         content = '' # try to deallocate a few MBs when viewing big files
 581 
 582         if len(self._lines) == 0:
 583             self._max_line_width = 0
 584         else:
 585             self._max_line_width = max(len(l) for l in self._lines)
 586         self._on_resize()
 587 
 588         iw = self._inner_width
 589         ih = self._inner_height
 590 
 591         if iw < 10 or ih < 10:
 592             return
 593 
 594         while True:
 595             self._redraw()
 596             k = self._screen.getkey()
 597             if self.handlers and (k in self.handlers):
 598                 h = self.handlers[k]
 599                 if (h is None) or (h() is False):
 600                     self._lines = tuple()
 601                     return k
 602 
 603     def _fit_string(self, s):
 604         maxlen = max(self._inner_width, 0)
 605         return s if len(s) <= maxlen else s[:maxlen]
 606 
 607     def _redraw(self):
 608         title = self._fit_string(self.title)
 609         lines = self._lines
 610         screen = self._screen
 611         iw = self._inner_width
 612         ih = self._inner_height
 613 
 614         if iw < 10 or ih < 10:
 615             return
 616 
 617         screen.erase()
 618 
 619         if title:
 620             screen.addstr(0, 0, f'{title:<{iw}}', A_REVERSE)
 621 
 622         at_bottom = len(self._lines) - self._top <= ih
 623         if at_bottom:
 624             msg = f'END ({len(lines):,})' if len(lines) > 0 else '(empty)'
 625         else:
 626             from math import ceil, log10
 627             n = max(len(lines) - 1, 0)
 628             w = int(ceil(log10(n))) if n > 0 else 1
 629             msg = f'({self._top + 1:>{w},} / {n:,})'
 630         screen.addstr(0, iw - len(msg), self._fit_string(msg), A_REVERSE)
 631 
 632         from itertools import islice
 633 
 634         for i, l in enumerate(islice(lines, self._top, self._top + ih)):
 635             if self._left > 0:
 636                 l = l[self._left:]
 637             try:
 638                 screen.addnstr(i + 1, 0, l, iw)
 639             except Exception as _:
 640                 # some utf-8 files have lines which upset func addstr
 641                 screen.addnstr(i + 1, 0, '?' * len(l), iw)
 642 
 643         # show up/down arrows
 644         if self._top > 0:
 645             self._screen.addstr(1, iw - 1, '▲')
 646         if self._top < self._max_top:
 647             self._screen.addstr(ih, iw - 1, '▼')
 648 
 649         screen.refresh()
 650 
 651     def _show_error(self, err):
 652         title = self._fit_string(self.title)
 653         screen = self._screen
 654         iw = self._inner_width
 655         ih = self._inner_height
 656 
 657         if iw < 10 or ih < 10:
 658             return
 659 
 660         screen.erase()
 661         if title:
 662             screen.addstr(0, 0, f'{title:<{iw}}', A_REVERSE)
 663         screen.addstr(2, 0, self._fit_string(str(err)), A_REVERSE)
 664         screen.refresh()
 665 
 666     def _on_resize(self):
 667         height, width = self._screen.getmaxyx()
 668         self._inner_width = width - 1
 669         self._inner_height = height - 1
 670         self._max_top = max(len(self._lines) - self._inner_height, 0)
 671         ss = self.side_step
 672         self._max_left = self._max_line_width - self._inner_width - 1 + ss
 673         self._max_left = max(self._max_left, 0)
 674 
 675     def _on_up(self):
 676         self._top = max(self._top - 1, 0)
 677 
 678     def _on_down(self):
 679         self._top = min(self._top + 1, self._max_top)
 680 
 681     def _on_page_up(self):
 682         self._top = max(self._top - self._inner_height, 0)
 683 
 684     def _on_page_down(self):
 685         self._top = min(self._top + self._inner_height, self._max_top)
 686 
 687     def _on_home(self):
 688         self._top = 0
 689 
 690     def _on_end(self):
 691         self._top = self._max_top
 692 
 693     def _on_left(self):
 694         self._left = max(self._left - self.side_step, 0)
 695 
 696     def _on_right(self):
 697         self._left = min(self._left + self.side_step, self._max_left)
 698 
 699 
 700 def browse_file(name, screen):
 701     tv = TextViewerTUI(screen)
 702     tv.title = name
 703     tv.side_step = 4
 704     tv.handlers['KEY_F(1)'] = lambda: show_help(screen)
 705     tv.handlers['kLFT5'] = None
 706     tv.handlers['KEY_BACKSPACE'] = None
 707     return tv.run(slurp(name))
 708 
 709 
 710 def run(name, fallback, verbose, compact):
 711     try:
 712         tui = None
 713         # can read piped input only before entering the `ui-mode`
 714         if name == '-':
 715             data = loads(stdin.read())
 716         else:
 717             with open(name, 'r') as inp:
 718                 data = load(inp)
 719 
 720         tui = SimpleTUI()
 721         tui.start(3, 10)
 722         vb = ValueBrowserTUI(tui.screen)
 723         vb.title = '<stdin>' if name == '-' else name
 724         vb.side_step = 4
 725         vb.handlers['KEY_F(1)'] = lambda: show_help(tui.screen)
 726         vb.data = data
 727 
 728         path, pick, last = vb.run()
 729         tui.stop()
 730         dup2(3, 1)
 731 
 732         indent = None if compact else 2
 733         seps = (',', ':' if compact else ': ')
 734 
 735         if last is None or last in ('\x1b', 'KEY_F(10)', 'KEY_F(12)'):
 736             if not fallback:
 737                 return 1
 738             pick = loads(fallback)
 739             verbose = False
 740 
 741         if verbose:
 742             print(path, file=stderr)
 743         print(dumps(pick, indent=indent, separators=seps,
 744                     allow_nan=False, check_circular=False))
 745         return 0
 746     except KeyboardInterrupt:
 747         if tui:
 748             tui.stop()
 749             dup2(3, 1)
 750         return 1
 751     except Exception as e:
 752         if tui:
 753             tui.stop()
 754             dup2(3, 1)
 755         # raise e
 756         print(str(e), file=stderr)
 757         return 1
 758 
 759 
 760 def show_help(screen):
 761     # quit_set = ('KEY_F(10)', 'KEY_F(12)', '\x1b', 'KEY_F(1)')
 762     quit_set = ('\x1b', 'KEY_F(1)', 'KEY_F(10)', 'KEY_F(12)', 'KEY_BACKSPACE')
 763     h = TextViewerTUI(screen, quit_set)
 764     h.title = 'Help for Browse JSON (bj)'
 765     return h.run(info) != '\x1b'
 766 
 767 
 768 def slurp(name):
 769     try:
 770         with open(name, 'r') as inp:
 771             return inp.read()
 772     except Exception as e:
 773         return e
 774 
 775 
 776 if len(argv) > 1 and argv[1] in ('-h', '--h', '-help', '--help'):
 777     print(info.strip())
 778     exit(0)
 779 
 780 compact_output_opts = (
 781     '-c', '--c', '-compact', '--compact', '-j0', '--j0', '-json0', '--json0',
 782 )
 783 fallback_opts = (
 784     '-d', '--d', '-f', '--f', '-fallback', '--fallback', '-default',
 785     '--default',
 786 )
 787 verbose_output_opts = (
 788     '-v', '--v', '-verbose', '--verbose', '-gron-path', '--gron-path',
 789 )
 790 
 791 args = argv[1:]
 792 compact = False
 793 verbose = False
 794 fallback = ''
 795 
 796 while len(args) > 0:
 797     if args[0] == '-':
 798         break
 799 
 800     if args[0] == '--':
 801         args = args[1:]
 802         break
 803 
 804     if args[0] in compact_output_opts:
 805         compact = True
 806         args = args[1:]
 807         continue
 808 
 809     if args[0] in fallback_opts:
 810         if len(args) < 2:
 811             print('forgot the JSON fallback value', file=stderr)
 812             exit(1)
 813         try:
 814             fallback = args[1]
 815             loads(fallback)
 816         except Exception as e:
 817             print(str(e), file=stderr)
 818             exit(1)
 819         args = args[2:]
 820         continue
 821 
 822     if args[0] in verbose_output_opts:
 823         verbose = True
 824         args = args[1:]
 825         continue
 826 
 827     if args[0].startswith('-'):
 828         print(f'bj: unsupported option {args[0]}', file=stderr)
 829         exit(1)
 830 
 831     break
 832 
 833 name = args[0] if len(args) > 0 else '-'
 834 
 835 if len(args) > 1:
 836     msg = 'there can only be one (optional) filename argument'
 837     print(msg, file=stderr)
 838     exit(4)
 839 
 840 # avoid func curses.wrapper, since it calls func curses.start_color, which in
 841 # turn forces a black background no matter the terminal configuration
 842 
 843 exit(run(name, fallback, verbose, compact))