Skip to content

Instantly share code, notes, and snippets.

@impiaaa
Last active April 6, 2024 00:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save impiaaa/b24cc7ecc5096728c4fb27fce6d94ead to your computer and use it in GitHub Desktop.
Save impiaaa/b24cc7ecc5096728c4fb27fce6d94ead to your computer and use it in GitHub Desktop.
Highlight file changes as they happen
import curses, sys, difflib
import time, os.path
import watchdog.events, watchdog.observers
class Drawer:
def __init__(self, win):
self.xoffset = self.yoffset = 0
self.win = win
def draw(self, event_handler):
self.win.erase()
height, width = self.win.getmaxyx()
first_column = 0 if curses.has_colors() else 1
if not event_handler.first:
for tag, i1, i2, j1, j2 in event_handler.matcher.get_opcodes():
if curses.has_colors():
self.win.attrset(curses.color_pair({'replace': 1,
'insert': 2,
'delete': 3,
'equal': 0
}[tag]))
for i in range(j1, j2):
if i-self.yoffset < 0: continue
if i-self.yoffset >= height: break
if not curses.has_colors():
self.win.addnstr(i-self.yoffset, 0, {'replace': '*',
'insert': '+',
'delete': '-',
'equal': ' '
}[tag], 1)
self.win.addnstr(i-self.yoffset, first_column, event_handler.contents[i][self.xoffset:], width-first_column)
else:
for i, line in enumerate(event_handler.contents):
if i-self.yoffset < 0: continue
if i-self.yoffset >= height: break
self.win.addnstr(i-self.yoffset, first_column, line[self.xoffset:], width-first_column)
self.win.refresh()
def input(self, linecount):
c = self.win.getch()
height, width = self.win.getmaxyx()
if 0 < c < 256 and chr(c) in 'Qq':
return True
elif c == curses.KEY_UP:
if self.yoffset > 0:
self.yoffset -= 1
else:
curses.beep()
elif c == curses.KEY_DOWN:
if self.yoffset+height < linecount:
self.yoffset += 1
else:
curses.beep()
elif c == curses.KEY_LEFT:
if self.xoffset > 0:
self.xoffset -= 1
else:
curses.beep()
elif c == curses.KEY_RIGHT:
self.xoffset += 1
class Handler(watchdog.events.PatternMatchingEventHandler):
def __init__(self, path):
super().__init__(patterns=[path])
self.path = os.path.abspath(path)
self.matcher = difflib.SequenceMatcher(isjunk=lambda s: s.isspace())
self.contents = None
self.first = True
self.read()
def read(self):
old = self.contents
with open(self.path) as f:
self.contents = f.read().splitlines()
if old is not None:
self.matcher.set_seqs(old, self.contents)
self.first = False
def on_deleted(self, event):
self.path = None
def on_modified(self, event):
if self.path is None: return
self.read()
self.drawer.draw(self)
def on_moved(self, event):
if self.path is None: return
self.path = event.dest_path
self.patterns[:] = [event.dest_path]
# TODO: restart the observer if it moved to a different directory
event_handler = Handler(sys.argv[1])
observer = watchdog.observers.Observer()
observer.schedule(event_handler, os.path.dirname(event_handler.path))
def mainloop(win):
curses.curs_set(0)
if curses.has_colors():
curses.init_pair(1, 0, curses.COLOR_BLUE)
curses.init_pair(2, 0, curses.COLOR_GREEN)
curses.init_pair(3, 0, curses.COLOR_RED)
win.nodelay(False)
drawer = Drawer(win)
event_handler.drawer = drawer
observer.start()
while event_handler.path is not None:
drawer.draw(event_handler)
if drawer.input(len(event_handler.contents)):
break
observer.stop()
curses.curs_set(1)
curses.wrapper(mainloop)
observer.join()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment