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