9 Commits
Author SHA1 Message Date
Carlos MartinoandGitHub 169c8c13fd Update README.md 2026-07-21 11:12:45 -04:00
Carlos MartinoandGitHub 42ce0fa6d9 Update README.md 2026-07-21 11:12:18 -04:00
Carlos MartinoandGitHub 00ea296f56 Update README.md 2026-07-21 11:12:03 -04:00
Carlos MartinoandGitHub a8840261dc Add files via upload 2026-07-21 11:11:49 -04:00
imcarlost da1245bbef Update readme 2026-07-20 11:47:36 -04:00
imcarlost 80fd6ba760 Bump version to 2.4.0
Fix click-to-line behavior in the interactive UI (no-op without a filter,
correct row math when the log hasn't filled the screen) and add a hover
highlight, a persistent pin on the clicked line, and always-visible process
create/death separators so filtered runs stay distinguishable.
2026-07-20 11:44:13 -04:00
imcarlost 0cb6331afc Bump version to 2.3.0
Adds Esc-to-quit, working scroll (keys + mouse wheel), and click-to-jump
in the interactive filter UI.
2026-07-20 11:07:20 -04:00
imcarlost 112291d61b add: claude md 2026-07-20 10:03:09 -04:00
imcarlost 8cdee851fa Update readme 2026-07-20 09:48:08 -04:00
6 changed files with 355 additions and 79 deletions
+26
View File
@@ -1,6 +1,32 @@
Change Log Change Log
========== ==========
Version 2.4.0 *(2026-07-20)*
----------------------------
* Fix: Clicking a line with no filter active was recentering the view instead of doing
nothing; it's now a no-op until you actually have something to unfilter.
* Fix: Click-to-line row math was off whenever the log hadn't filled the screen yet
(e.g. right after startup), so clicks could land on the wrong entry or miss entirely.
* New: Hovering over a line highlights it while a filter is active, hinting that it's
clickable.
* New: The line you click stays highlighted after the filter clears, so you can find
it in the full scrollback; the highlight clears once you start another search.
* New: Process create/death separators now stay visible even while a filter is active,
so you can tell separate app runs apart in filtered results.
Version 2.3.0 *(2026-07-20)*
----------------------------
* New: Esc quits the interactive UI. A bare Escape keypress exits; Escape sequences
(arrow keys, mouse reports) are still parsed normally.
* Fix: Scrolling the interactive UI now works. Up/Down, Page Up/Down, Home/End, and
the mouse wheel scroll the retained scrollback; new lines no longer yank you back
to the tail while you're scrolled up.
* New: Clicking a line in the interactive UI clears the filter, unfilters the log,
and scrolls to that line in the full history.
Version 2.2.0 *(2026-07-20)* Version 2.2.0 *(2026-07-20)*
---------------------------- ----------------------------
+82
View File
@@ -0,0 +1,82 @@
# pidcat-repl
A fork of [JakeWharton/pidcat](https://github.com/JakeWharton/pidcat) that adds an
interactive, full-screen filter UI on top of the original package-filtered `adb logcat`
viewer.
## Layout
Everything lives in a single script, `pidcat.py`. There is no build step, no test suite,
and no dependencies beyond Python 3 and `adb` on the PATH.
- `stream(emit)` parses `adb logcat` output and hands each formatted block to a callback.
Both output modes feed off it.
- `InteractiveUI` is the fork's addition: alternate-screen rendering, a bottom prompt line,
and live filtering of the retained scrollback.
- `--plain` bypasses `InteractiveUI` and prints blocks as they arrive, which is the
upstream behavior. Non-TTY stdin or stdout forces plain mode too.
Run it directly during development: `./pidcat.py com.example.app`.
## Homebrew distribution
This fork is distributed through a personal tap, **not** homebrew-core. Two repos are
involved:
- `imcarlost/pidcat-repl` (this repo) holds the source and the version tags.
- `imcarlost/homebrew-pidcat-repl` holds `Formula/pidcat-repl.rb`. Cloned locally at
`../homebrew-pidcat-repl`.
The core formula named `pidcat` is upstream's, unrelated to this one. Homebrew derives the
formula class name from the filename, so `pidcat-repl.rb` must keep the class
`PidcatRepl`.
### Publishing a new version
The formula pins a tarball URL plus its sha256, so the version bump, the tag, and the
formula must all move together. Order matters: the sha256 can only be computed after the
tag is pushed, because it hashes GitHub's generated tarball.
1. Bump `__version__` in `pidcat.py` and promote the `Unreleased` heading in
`CHANGELOG.md` to the new version with today's date.
2. Commit and push to `main`.
3. Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z`.
4. Compute the tarball hash:
```sh
curl -sL https://github.com/imcarlost/pidcat-repl/archive/refs/tags/vX.Y.Z.tar.gz \
| shasum -a 256
```
5. In `../homebrew-pidcat-repl/Formula/pidcat-repl.rb`, update both the `url` version and
the `sha256`. Commit and push.
Keep `__version__` in sync with the tag. A mismatch is invisible until someone runs
`pidcat --version` and sees the old number.
If a tag has to be moved after it was pushed, the tarball hash changes with it, so the
formula's `sha256` must be recomputed and pushed again or installs fail checksum
verification.
### Installing and testing the tap
```sh
brew tap imcarlost/pidcat-repl
brew install pidcat-repl
```
Recent Homebrew versions refuse formulae from untrusted third-party taps. The install
fails with a `Refusing to load formula ... from untrusted tap` error until the tap is
trusted once per machine:
```sh
brew trust imcarlost/pidcat-repl
```
That is a local trust decision each user makes for themselves; there is no way to
pre-authorize the tap from the publishing side. Getting into homebrew-core instead is not
realistic for this fork, since core already carries upstream `pidcat` and forks rarely
meet its notability bar.
After pushing a formula change, `brew update` before reinstalling, or the tap clone under
`$(brew --repository)/Library/Taps/` will still be on the old commit.
+19 -27
View File
@@ -7,6 +7,22 @@ entries for a specific application package; this fork adds an interactive,
full-screen filter UI on top, turning `pidcat` into more of a REPL than a full-screen filter UI on top, turning `pidcat` into more of a REPL than a
one-shot stream. one-shot stream.
![Example screen](screen.jpeg)
Install
-------
Use [Homebrew][2]:
```shell
brew tap imcarlost/pidcat-repl
brew trust imcarlost/pidcat-repl
brew install pidcat-repl
```
Usage
----------
During application development you often want to only display log messages During application development you often want to only display log messages
coming from your app. Unfortunately, because the process ID changes every time coming from your app. Unfortunately, because the process ID changes every time
you deploy to the phone it becomes a challenge to grep for the right thing. you deploy to the phone it becomes a challenge to grep for the right thing.
@@ -22,11 +38,6 @@ If you just want the original, non-interactive `pidcat`, use the upstream
mode](#interactive-mode) below. mode](#interactive-mode) below.
Here is an example of the output when running for the Google Plus app:
![Example screen](screen.png)
Interactive mode Interactive mode
----------------- -----------------
@@ -37,7 +48,7 @@ case-insensitively.
* `Backspace` edits the query, `Ctrl-U` clears it. * `Backspace` edits the query, `Ctrl-U` clears it.
* `Ctrl-L` forces a redraw; the view also tracks terminal resizes. * `Ctrl-L` forces a redraw; the view also tracks terminal resizes.
* `Ctrl-C` or `Ctrl-D` quits and restores your scrollback. * `Esc` or `Ctrl-D` quits and restores your scrollback.
Pass `--plain` to get the original streaming output instead, e.g. for Pass `--plain` to get the original streaming output instead, e.g. for
`pidcat --plain com.oprah.bees.android | grep Foo`. Piped input or output `pidcat --plain com.oprah.bees.android | grep Foo`. Piped input or output
@@ -45,26 +56,8 @@ Pass `--plain` to get the original streaming output instead, e.g. for
uses plain streaming, since there is no terminal to draw the UI on. uses plain streaming, since there is no terminal to draw the UI on.
Install Requirements
------- ------------
This fork isn't packaged anywhere; download `pidcat.py` from this repo and
place it on your PATH.
The Homebrew and AUR packages below install the upstream, non-interactive
`pidcat` instead:
* OS X: Use [Homebrew][2].
brew install pidcat
If you need to install the latest development version
brew unlink pidcat
brew install --HEAD pidcat
* Arch Linux : Install the package called `pidcat-git` from the [AUR][4].
Make sure that `adb` from the [Android SDK][3] is on your PATH. This script will Make sure that `adb` from the [Android SDK][3] is on your PATH. This script will
not work unless this is that case. That means, when you type `adb` and press not work unless this is that case. That means, when you type `adb` and press
@@ -92,5 +85,4 @@ sudo apt-get -t focal install coreutils
[1]: http://jsharkey.org/blog/2009/04/22/modifying-the-android-logcat-stream-for-full-color-debugging/ [1]: http://jsharkey.org/blog/2009/04/22/modifying-the-android-logcat-stream-for-full-color-debugging/
[2]: http://brew.sh [2]: http://brew.sh
[3]: http://developer.android.com/sdk/ [3]: http://developer.android.com/sdk/
[4]: https://aur.archlinux.org/packages/pidcat-git/
[5]: https://github.com/JakeWharton/pidcat [5]: https://github.com/JakeWharton/pidcat
Binary file not shown.
+208 -32
View File
@@ -33,7 +33,7 @@ import subprocess
import threading import threading
from subprocess import PIPE from subprocess import PIPE
__version__ = '2.2.0' __version__ = '2.4.0'
LOG_LEVELS = 'VDIWEF' LOG_LEVELS = 'VDIWEF'
LOG_LEVELS_MAP = dict([(LOG_LEVELS[i], i) for i in range(len(LOG_LEVELS))]) LOG_LEVELS_MAP = dict([(LOG_LEVELS[i], i) for i in range(len(LOG_LEVELS))])
@@ -321,7 +321,7 @@ def stream(emit):
linebuf += colorize(' ' * (header_size - 1), bg=WHITE) linebuf += colorize(' ' * (header_size - 1), bg=WHITE)
linebuf += ' PID: %s UID: %s GIDs: %s' % (line_pid, line_uid, line_gids) linebuf += ' PID: %s UID: %s GIDs: %s' % (line_pid, line_uid, line_gids)
linebuf += '\n' linebuf += '\n'
emit('Process %s created for %s PID: %s' % (line_package, target, line_pid), linebuf) emit('Process %s created for %s PID: %s' % (line_package, target, line_pid), linebuf, is_separator=True)
last_tag = None # Ensure next log gets a tag printed last_tag = None # Ensure next log gets a tag printed
dead_pid, dead_pname = parse_death(tag, message) dead_pid, dead_pname = parse_death(tag, message)
@@ -331,7 +331,7 @@ def stream(emit):
linebuf += colorize(' ' * (header_size - 1), bg=RED) linebuf += colorize(' ' * (header_size - 1), bg=RED)
linebuf += ' Process %s (PID: %s) ended' % (dead_pname, dead_pid) linebuf += ' Process %s (PID: %s) ended' % (dead_pname, dead_pid)
linebuf += '\n' linebuf += '\n'
emit('Process %s (PID: %s) ended' % (dead_pname, dead_pid), linebuf) emit('Process %s (PID: %s) ended' % (dead_pname, dead_pid), linebuf, is_separator=True)
last_tag = None # Ensure next log gets a tag printed last_tag = None # Ensure next log gets a tag printed
# Make sure the backtrace is printed after a native crash # Make sure the backtrace is printed after a native crash
@@ -385,18 +385,24 @@ def stream(emit):
class InteractiveUI: class InteractiveUI:
'''Full-screen filter UI: log lines render above a bottom prompt line, and the '''Full-screen filter UI: log lines render above a bottom prompt line, and the
typed query live-filters the scrollback. Every whitespace-separated word must typed query live-filters the scrollback. Every whitespace-separated word must
appear in a block's plain text (case-insensitive) for it to be shown.''' appear in a block's plain text (case-insensitive) for it to be shown, except
process create/death separators, which always stay visible so filtered runs
can still be told apart.'''
MAX_ENTRIES = 10000 # (search_text, block) pairs kept for re-filtering MAX_ENTRIES = 10000 # (search_text, block) pairs kept for re-filtering
MAX_VISIBLE = 5000 # rendered lines kept for the current query MAX_VISIBLE = 5000 # rendered lines kept for the current query
def __init__(self): def __init__(self):
self.lock = threading.Lock() self.lock = threading.Lock()
self.entries = collections.deque(maxlen=self.MAX_ENTRIES) self.entries = collections.deque(maxlen=self.MAX_ENTRIES) # (entry_id, search_lower, block, is_separator)
self.visible = [] self.visible = [] # (entry_id, line_text)
self.next_entry_id = 0
self.query = '' self.query = ''
self.status = '' self.status = ''
self.resized = False self.resized = False
self.scroll_offset = 0 # lines scrolled up from the tail; 0 == following the live tail
self.hover_row = None # 0-based screen row the pointer is over, or None
self.selected_index = None # index into self.visible pinned by the last click, or None
self.rows, self.cols = self._term_size() self.rows, self.cols = self._term_size()
def _term_size(self): def _term_size(self):
@@ -412,26 +418,46 @@ class InteractiveUI:
def _matches(self, search_lower): def _matches(self, search_lower):
return all(token in search_lower for token in self.query.lower().split()) return all(token in search_lower for token in self.query.lower().split())
def _append_visible(self, block): def _included(self, search_lower, is_separator):
self.visible.extend(block.split('\n')) # Process create/death separators ignore the filter so a run boundary is
if len(self.visible) > self.MAX_VISIBLE: # never hidden by an unrelated search.
del self.visible[:len(self.visible) - self.MAX_VISIBLE] return is_separator or self._matches(search_lower)
def emit(self, search_text, block): def _append_visible(self, entry_id, block):
new_lines = [(entry_id, line) for line in block.split('\n')]
self.visible.extend(new_lines)
if self.scroll_offset > 0:
# Keep whatever the user is currently looking at in place instead of
# letting newly-arrived lines push it down and out of view.
self.scroll_offset += len(new_lines)
overflow = len(self.visible) - self.MAX_VISIBLE
if overflow > 0:
del self.visible[:overflow]
self.scroll_offset = max(0, self.scroll_offset - overflow)
if self.selected_index is not None:
self.selected_index -= overflow
if self.selected_index < 0:
self.selected_index = None
def emit(self, search_text, block, is_separator=False):
with self.lock: with self.lock:
entry_id = self.next_entry_id
self.next_entry_id += 1
search_lower = search_text.lower() search_lower = search_text.lower()
self.entries.append((search_lower, block)) self.entries.append((entry_id, search_lower, block, is_separator))
if self._matches(search_lower): if self._included(search_lower, is_separator):
self._append_visible(block) self._append_visible(entry_id, block)
self._render() self._render()
def set_query(self, query): def set_query(self, query):
with self.lock: with self.lock:
self.query = query self.query = query
self.visible = [] self.visible = []
for search_lower, block in self.entries: self.scroll_offset = 0
if self._matches(search_lower): self.selected_index = None # a new search replaces whatever was pinned
self._append_visible(block) for entry_id, search_lower, block, is_separator in self.entries:
if self._included(search_lower, is_separator):
self._append_visible(entry_id, block)
self._render() self._render()
def refresh(self): def refresh(self):
@@ -441,42 +467,186 @@ class InteractiveUI:
width = self.cols # future indent_wrap calls track the new size width = self.cols # future indent_wrap calls track the new size
self._render() self._render()
def _window_bounds(self):
'''Returns (start, end, pad): the self.visible slice currently on screen and
how many blank padding rows precede it (when there isn't enough content yet
to fill the log area).'''
log_rows = max(1, self.rows - 2)
max_offset = max(0, len(self.visible) - log_rows)
if self.scroll_offset > max_offset:
self.scroll_offset = max_offset
end = len(self.visible) - self.scroll_offset
start = max(0, end - log_rows)
pad = log_rows - (end - start)
return start, end, pad
def _render(self): def _render(self):
log_rows = max(1, self.rows - 2) log_rows = max(1, self.rows - 2)
lines = self.visible[-log_rows:] start, end, pad = self._window_bounds()
window = self.visible[start:end]
out = ['\x1b[H'] out = ['\x1b[H']
# Pad above so the log content hugs the prompt, like a terminal. # Pad above so the log content hugs the prompt, like a terminal.
for _ in range(log_rows - len(lines)): for _ in range(pad):
out.append('\x1b[K\n') out.append('\x1b[K\n')
for line in lines: for i, (_, line) in enumerate(window):
# Only hint clickability when a filter is active; with no filter a
# click is a no-op, so there's nothing to invite the user to click.
hovered = self.query and self.hover_row == pad + i
selected = self.selected_index == start + i
if hovered or selected:
out.append('\x1b[7m' + line.replace(RESET, RESET + '\x1b[7m') + RESET + '\x1b[K\n')
else:
out.append(line + '\x1b[K\n') out.append(line + '\x1b[K\n')
if self.query: if self.query:
state = '%d matching lines of %d blocks' % (len(self.visible), len(self.entries)) state = '%d matching lines of %d blocks' % (len(self.visible), len(self.entries))
else: else:
state = '%d blocks' % len(self.entries) state = '%d blocks' % len(self.entries)
if self.scroll_offset > 0:
state += ' \xb7 scrolled (End to jump to latest)'
if self.status: if self.status:
state += ' \xb7 ' + self.status state += ' \xb7 ' + self.status
separator = ' %s \xb7 type to filter \xb7 ctrl-u clear \xb7 ctrl-c quit' % state hint = ' \xb7 click a line to unfilter' if self.query else ''
separator = ' %s \xb7 type to filter \xb7 ctrl-u clear%s \xb7 esc/ctrl-c quit' % (state, hint)
out.append('\x1b[2m' + separator[:max(0, self.cols - 1)] + '\x1b[0m\x1b[K\n') out.append('\x1b[2m' + separator[:max(0, self.cols - 1)] + '\x1b[0m\x1b[K\n')
out.append('\x1b[36m\x1b[0m ' + self.query + '\x1b[K') out.append('\x1b[36m\x1b[0m ' + self.query + '\x1b[K')
sys.stdout.write(''.join(out)) sys.stdout.write(''.join(out))
sys.stdout.flush() sys.stdout.flush()
def _drain_pending(self, fd): MOUSE_RE = re.compile(r'^\[<(\d+);(\d+);(\d+)([Mm])$')
# Swallow the rest of an escape sequence (arrow keys etc.) we do not handle.
while select.select([fd], [], [], 0)[0]: def _scroll(self, delta):
if not os.read(fd, 64): with self.lock:
log_rows = max(1, self.rows - 2)
max_offset = max(0, len(self.visible) - log_rows)
self.scroll_offset = max(0, min(max_offset, self.scroll_offset + delta))
self._render()
def _handle_click(self, row):
with self.lock:
if not self.query: # nothing to unfilter, so clicking is a no-op
return
log_rows = max(1, self.rows - 2)
start, _end, pad = self._window_bounds()
if row < 1 or row > log_rows or row <= pad:
return
idx = start + (row - 1 - pad)
if idx < 0 or idx >= len(self.visible):
return
entry_id = self.visible[idx][0]
self._jump_to_entry(entry_id)
def _handle_hover(self, row):
with self.lock:
log_rows = max(1, self.rows - 2)
_start, _end, pad = self._window_bounds()
new_hover = (row - 1) if (1 <= row <= log_rows and row > pad) else None
if new_hover == self.hover_row:
return
self.hover_row = new_hover
self._render()
def _jump_to_entry(self, entry_id):
'''Clears the filter, rebuilds the full unfiltered scrollback, and scrolls
so the clicked entry is in view with a bit of context above it. The entry's
line stays pinned/highlighted until another search starts.'''
with self.lock:
self.query = ''
self.visible = []
self.scroll_offset = 0
target_index = None
for eid, _search_lower, block, _is_separator in self.entries:
for line in block.split('\n'):
if target_index is None and eid == entry_id:
target_index = len(self.visible)
self.visible.append((eid, line))
overflow = len(self.visible) - self.MAX_VISIBLE
if overflow > 0:
del self.visible[:overflow]
if target_index is not None:
target_index -= overflow
if target_index is not None and target_index >= 0:
self.selected_index = target_index
log_rows = max(1, self.rows - 2)
max_offset = max(0, len(self.visible) - log_rows)
desired_end = target_index + max(1, log_rows // 3)
self.scroll_offset = max(0, min(max_offset, len(self.visible) - desired_end))
else:
self.selected_index = None
self._render()
def _handle_escape(self, seq):
m = self.MOUSE_RE.match(seq)
if m:
button, _col, row, kind = m.groups()
if kind != 'M': # ignore button-release reports
return
button = int(button)
if button == 0: # left click
self._handle_click(int(row))
elif button == 35: # pointer moved, no button held
self._handle_hover(int(row))
elif button == 64: # wheel up
self._scroll(3)
elif button == 65: # wheel down
self._scroll(-3)
return
log_rows = max(1, self.rows - 2)
if seq in ('[A', 'OA'): # up
self._scroll(1)
elif seq in ('[B', 'OB'): # down
self._scroll(-1)
elif seq == '[5~': # page up
self._scroll(log_rows)
elif seq == '[6~': # page down
self._scroll(-log_rows)
elif seq in ('[H', '[1~'): # home
self._scroll(10 ** 9)
elif seq in ('[F', '[4~'): # end
self._scroll(-(10 ** 9))
def _read_escape(self, pending, fd, decoder):
'''Consumes the sequence following an ESC already pulled from `pending`.
Returns (seq, rest_of_pending). seq is None for a standalone Escape
keypress (nothing followed it within the grace window), '' when ESC was
followed by something unrelated, or the sequence body (e.g. '[A')
otherwise.'''
if not pending:
# Give a fast terminal-generated sequence (arrow keys, mouse reports)
# a brief window to arrive before treating this as a lone Escape.
if select.select([fd], [], [], 0.01)[0]:
data = os.read(fd, 64)
if data:
pending = decoder.decode(data)
if not pending:
return None, pending
introducer = pending[0]
if introducer not in ('[', 'O'):
return '', pending
seq = introducer
pending = pending[1:]
while not (seq[-1].isalpha() or seq[-1] == '~'):
if not pending:
if select.select([fd], [], [], 0.01)[0]:
data = os.read(fd, 64)
if data:
pending += decoder.decode(data)
continue
break break
seq += pending[0]
pending = pending[1:]
return seq, pending
def run(self, reader_thread): def run(self, reader_thread):
fd = sys.stdin.fileno() fd = sys.stdin.fileno()
old_attrs = termios.tcgetattr(fd) old_attrs = termios.tcgetattr(fd)
decoder = codecs.getincrementaldecoder('utf-8')('replace') decoder = codecs.getincrementaldecoder('utf-8')('replace')
signal.signal(signal.SIGWINCH, lambda *_: setattr(self, 'resized', True)) signal.signal(signal.SIGWINCH, lambda *_: setattr(self, 'resized', True))
sys.stdout.write('\x1b[?1049h\x1b[?7l\x1b[2J\x1b[H') # alt screen, no autowrap # Alt screen, no autowrap, mouse click/wheel/motion reporting (SGR encoding).
sys.stdout.write('\x1b[?1049h\x1b[?7l\x1b[2J\x1b[H\x1b[?1000h\x1b[?1003h\x1b[?1006h')
sys.stdout.flush() sys.stdout.flush()
tty.setcbreak(fd) tty.setcbreak(fd)
reader_thread.start() reader_thread.start()
pending = '' # decoded characters carried over between reads
try: try:
with self.lock: with self.lock:
self._render() self._render()
@@ -488,12 +658,16 @@ class InteractiveUI:
self.status = 'adb ended, scrollback still filterable' self.status = 'adb ended, scrollback still filterable'
with self.lock: with self.lock:
self._render() self._render()
if not pending:
if not select.select([fd], [], [], 0.2)[0]: if not select.select([fd], [], [], 0.2)[0]:
continue continue
data = os.read(fd, 64) data = os.read(fd, 64)
if not data: if not data:
break break
for ch in decoder.decode(data): pending = decoder.decode(data)
if not pending:
continue
ch, pending = pending[0], pending[1:]
if ch in ('\x7f', '\x08'): # backspace if ch in ('\x7f', '\x08'): # backspace
if self.query: if self.query:
self.set_query(self.query[:-1]) self.set_query(self.query[:-1])
@@ -505,9 +679,11 @@ class InteractiveUI:
elif ch == '\x0c': # ctrl-l elif ch == '\x0c': # ctrl-l
self.refresh() self.refresh()
elif ch == '\x1b': elif ch == '\x1b':
decoder.reset() seq, pending = self._read_escape(pending, fd, decoder)
self._drain_pending(fd) if seq is None: # standalone Escape keypress
break return
if seq:
self._handle_escape(seq)
elif ch in ('\r', '\n', '\t'): elif ch in ('\r', '\n', '\t'):
pass pass
elif ch >= ' ': elif ch >= ' ':
@@ -516,7 +692,7 @@ class InteractiveUI:
pass pass
finally: finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs) termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
sys.stdout.write('\x1b[?7h\x1b[?1049l') sys.stdout.write('\x1b[?1006l\x1b[?1003l\x1b[?1000l\x1b[?7h\x1b[?1049l')
sys.stdout.flush() sys.stdout.flush()
@@ -537,4 +713,4 @@ else:
# Die quietly like other unix filters when the downstream reader closes, # Die quietly like other unix filters when the downstream reader closes,
# e.g. `pidcat --plain <pkg> | head`. # e.g. `pidcat --plain <pkg> | head`.
signal.signal(signal.SIGPIPE, signal.SIG_DFL) signal.signal(signal.SIGPIPE, signal.SIG_DFL)
stream(lambda search_text, block: print(block)) stream(lambda search_text, block, is_separator=False: print(block))
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB