From 28550a2a014693ee3b7cc6fd91ca5e5bcbfec348 Mon Sep 17 00:00:00 2001 From: Lars Maier Date: Sun, 16 Dec 2018 13:16:27 +0100 Subject: [PATCH 1/3] Added advanced json view with highlighting and collapsing. --- aaa.py | 46 ++----- controls.py | 36 ++++- jsonctl.py | 372 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 415 insertions(+), 39 deletions(-) create mode 100644 jsonctl.py diff --git a/aaa.py b/aaa.py index f89dedb..7fb1f13 100755 --- a/aaa.py +++ b/aaa.py @@ -2,10 +2,10 @@ import sys import json -import textwrap import datetime import re from time import sleep +from jsonctl import JsonView, JsonColors import agency from controls import * @@ -216,44 +216,22 @@ def selectClosest(self, idx): self.highlight = idx self.top = idx -class AgencyLogView(LineView): +class AgencyLogView(JsonView): def __init__(self, app, rect): super().__init__(app, rect) self.idx = None self.head = None def update(self): - self.idx = self.app.list.getSelectedIndex() - - if not self.idx == None and self.idx < len(self.app.log): - entry = self.app.log[self.idx] - self.head = entry['_key'] - self.jsonLines(entry) - self.highlightLines() - + idx = self.app.list.getSelectedIndex() + if not idx == self.idx: + self.set(idx) super().update() - def highlightLines(self): - def intersperse(lst, item): - result = [item] * (len(lst) * 2 - 1) - result[0::2] = lst - return result - - logList = self.app.list - if not logList.filterType == AgencyLogList.FILTER_GREP: - return - - filt = logList.filterStr - if filt: - for i, line in enumerate(self.lines): - part = intersperse(line.split(filt), (curses.A_BOLD, filt)) - self.lines[i] = part - - def jsonLines(self, value): - self.lines = json.dumps(value, indent=4, separators=(',', ': ')).splitlines() - def set(self, idx): self.idx = idx + entry = self.app.log[self.idx] + super().set(entry) class AgencyStoreView(LineView): @@ -460,11 +438,6 @@ def layout(self): super().layout() self.split.layout(self.rect) -class ColorPairs: - CP_RED_WHITE = 1 - -class ColorFormat: - CF_ERROR = None def main(stdscr, argv): @@ -472,10 +445,11 @@ def main(stdscr, argv): curses.curs_set(0) # initialise some colors - curses.init_pair(ColorPairs.CP_RED_WHITE, curses.COLOR_RED, curses.COLOR_BLACK) + ColorPairs.init() + ColorFormat.init() + JsonColors.init() # Init color formats - ColorFormat.CF_ERROR = curses.A_BOLD | curses.color_pair(ColorPairs.CP_RED_WHITE); app = ArangoAgencyAnalyserApp(stdscr, argv) app.run() diff --git a/controls.py b/controls.py index 8c63746..3df287d 100644 --- a/controls.py +++ b/controls.py @@ -1,4 +1,6 @@ import curses, curses.ascii +import textwrap +import datetime class Rect: def __init__(self, x, y, width, height): @@ -227,7 +229,7 @@ def run(self): self.update() self.userInput() except Exception as err: - self.displayMsg("Error: {}".format(err), ColorFormat.CF_ERROR) + self.displayMsg("Error: {}".format(err), ColorFormat.ERROR) if self.debug: raise err @@ -384,13 +386,22 @@ def userStringLine(self, label = None, complete = None, default = None, prompt = finally: curses.curs_set(0) - def printStyleLine(self, y, x, line, maxlen, defaultAttr = 0): + def printStyleLine(self, y, x, line, maxlen, defaultAttr = 0, modifier = None, indent = 0): if isinstance(line, str): line = [line] - for p in line: + for i in range(0, indent): + if maxlen <= 0: + return + self.stdscr.delch(y, x) + x += 1 + maxlen -= 1 + + for i, p in enumerate(line): if isinstance(p, str): p = (defaultAttr, p) + if not modifier == None: + p = modifier(i, p) strlen = len(p[1]) self.stdscr.addnstr(y, x, p[1], maxlen, p[0]) maxlen -= strlen @@ -421,3 +432,22 @@ def showProgress(self, progress, msg, label = None): self.stdscr.addnstr(self.rect.height - 1, donelen, string[donelen:], maxlen - donelen, 0) self.stdscr.refresh() + +class ColorPairs: + RED_BLACK = 1 + BLUE_BLACK = 2 + GREEN_BLACK = 3 + CYAN_BLACK = 4 + + def init(): + curses.init_pair(ColorPairs.RED_BLACK, curses.COLOR_RED, curses.COLOR_BLACK) + curses.init_pair(ColorPairs.BLUE_BLACK, curses.COLOR_BLUE, curses.COLOR_BLACK) + curses.init_pair(ColorPairs.GREEN_BLACK, curses.COLOR_GREEN, curses.COLOR_BLACK) + curses.init_pair(ColorPairs.CYAN_BLACK, curses.COLOR_CYAN, curses.COLOR_BLACK) + + +class ColorFormat: + ERROR = None + + def init(): + ColorFormat.ERROR = curses.A_BOLD | curses.color_pair(ColorPairs.RED_BLACK) diff --git a/jsonctl.py b/jsonctl.py new file mode 100644 index 0000000..1e7400b --- /dev/null +++ b/jsonctl.py @@ -0,0 +1,372 @@ +from controls import Control, ColorPairs +import copy +import curses + +# JsonView Control allows to open and close sub objects and arrays. (+/-) +# Additional the syntax of json is highlighted. +# Controls: +# Up/Down next/prev property +# Left/Right switch name and value +# :(h)ere set path to the current selected path + + + +class JsonView(Control): + + # Given the python internal representation of the json data, i.e. via + # dicts and lists, a list of lines is generated usable for printStyleLine. + # The line data consists of: + # indent: how many cells to clear before printing + # keyRange: range of the line pieces representing the key value (None if none) + # valueRange: same as above but for values (None if not selectable, i.e. array) + # line: the actual string + # path: list of strings representing the path to this element, may be none + + class LineData(): + + TYPE_NONE = 0 + TYPE_PRIMITIVE = 1 + TYPE_OBJECT = 2 + TYPE_ARRAY = 3 + + def __init__(self, indent, path = None): + self.indent = indent + self.keyRange = None + self.valueRange = None + self.valueLineRange = None + self.valueType = self.TYPE_NONE + self.line = [] + self.path = path + self.collapsed = False + self.parentLine = None + + def __repr__(self): + return "{{indent = {}, key = {}, value = {}, line = {}, path = {}}}".format( + self.indent, + self.keyRange, + self.valueRange, + self.line, + "/".join(self.path) if not self.path == None else "None") + + def add(self, string): + self.line += string + + def key(self, string): + assert self.keyRange == None + self.keyRange = (len(self.line), len(string)) + self.line += string + + def value(self, string): + assert self.valueRange == None + self.valueRange = (len(self.line), len(string)) + self.line += string + + def lineRange(self, lineRange, valueType): + self.valueLineRange = lineRange + self.valueType = valueType + + def selectable(self): + return not self.valueRange == None or not self.keyRange == None + + def setCollapse(self, yesNo): + if not self.valueLineRange == None: + self.collapsed = yesNo + elif not self.parentLine == None: + self.parentLine.setCollapse(yesNo) + + def __init__(self, app, rect): + super().__init__(app, rect) + self.highlight = 3 + self.top = 0 + self.lines = [] + + def parseDict(value, lineData, indent, path): + assert isinstance(value, dict) + + if len(value) == 0: + lineData[-1].add(["{}"]) + return + + firstIdx = len(lineData) - 1 + first = lineData[-1] + # append { to the last line + first.value(["{"]) + + for key in value: + subindent = indent + 2 + subpath = path + [key] + data = JsonView.LineData(subindent, subpath) + # add the key + data.key([(JsonColors.KEY, '"{}"'.format(key))]) + data.add(": ") + lineData.append(data) + + JsonView.parseValue(value[key], lineData, subindent, subpath) + + # place the comma + lineData[-1].add([","]) + + # add a final line with closing } + lastIdx = len(lineData) + last = JsonView.LineData(indent, path) + last.value(["}"]) + last.parentLine = first + lineData.append(last) + + first.lineRange(lastIdx - firstIdx, JsonView.LineData.TYPE_OBJECT) + + def parseList(value, lineData, indent, path): + assert isinstance(value, list) + + if len(value) == 0: + lineData[-1].add(["[]"]) + return + + firstIdx = len(lineData) - 1 + first = lineData[-1] + + # append [ to the last line + first.value(["["]) + + for idx, subvalue in enumerate(value): + subindent = indent + 2 + subpath = path + ["[{}]".format(idx)] + data = JsonView.LineData(subindent, subpath) + lineData.append(data) + JsonView.parseValue(subvalue, lineData, subindent, subpath) + + # place the comma + lineData[-1].add([","]) + + # add a final line with closing ] + lastIdx = len(lineData) + last = JsonView.LineData(indent, path) + last.value(["]"]) + last.parentLine = first + lineData.append(last) + + first.lineRange(lastIdx - firstIdx, JsonView.LineData.TYPE_ARRAY) + + def parsePrimitiveValue(value, lineData, indent, path): + if isinstance(value, str): + lineData[-1].value([ + (JsonColors.STRING, '"{}"'.format(value)) + ]) + elif value == False: + lineData[-1].value([ + (JsonColors.FALSE, "false") + ]) + elif value == True: + lineData[-1].value([ + (JsonColors.TRUE, "true") + ]) + elif value == None: + lineData[-1].value([ + (JsonColors.NULL, "null") + ]) + elif isinstance(value, int) or isinstance(value, float): + lineData[-1].value([ + (JsonColors.NUMBER, str(value)) + ]) + else: + assert False + + def parseValue(value, lineData, indent, path): + if isinstance(value, dict): + JsonView.parseDict(value, lineData, indent, path) + elif isinstance(value, list): + JsonView.parseList(value, lineData, indent, path) + else: + JsonView.parsePrimitiveValue(value, lineData, indent, path) + + + def set(self, value): + self.highlight = 0 + self.lines = [JsonView.LineData(0)] + JsonView.parseValue(value, self.lines, 0, []) + + def previousVisibleIndex(self, idx): + while True: + + idx -= 1 + if idx < 0: + return None + + + + line = self.lines[idx] + + parentLine = line.parentLine + if not parentLine == None and parentLine.collapsed: + idx -= parentLine.valueLineRange - 1 + continue + + if not line.selectable(): + continue + + return idx + + def nextVisibleIndex(self, idx): + while True: + if idx >= len(self.lines): + return None + current = self.lines[idx] + if current.collapsed: + idx += current.valueLineRange + continue + + idx += 1 + if idx >= len(self.lines): + return None + line = self.lines[idx] + if not line.selectable(): + continue + + return idx + + def nextHighlight(self): + idx = self.nextVisibleIndex(self.highlight) + if not idx == None: + self.highlight = idx + + def prevHighlight(self): + idx = self.previousVisibleIndex(self.highlight) + if not idx == None: + self.highlight = idx + + def pageDownHighlight(self): + idx = self.nextVisibleIndex(self.highlight + self.rect.height) + if not idx == None: + self.highlight = idx + else: + idx = self.previousVisibleIndex(self.highlight + self.rect.height) + if not idx == None: + self.highlight = idx + + def pageUpHighlight(self): + idx = self.previousVisibleIndex(self.highlight - self.rect.height) + if not idx == None: + self.highlight = idx + else: + idx = self.nextVisibleIndex(self.highlight - self.rect.height) + if not idx == None: + self.highlight = idx + + def input(self, c): + if c == curses.KEY_DOWN: + self.nextHighlight() + elif c == curses.KEY_UP: + self.prevHighlight() + elif c == curses.KEY_NPAGE: + self.pageDownHighlight() + elif c == curses.KEY_PPAGE: + self.pageUpHighlight() + elif c == curses.KEY_HOME: + self.highlight = 0 + elif c == curses.KEY_END: + self.highlight = len(self.lines) - 1 + elif c == ord('-'): + self.lines[self.highlight].setCollapse(True) + elif c == ord('+'): + self.lines[self.highlight].setCollapse(False) + elif c == ord('.'): + line = self.lines[self.highlight] + line.setCollapse(not line.collapsed) + + def update(self): + # Update indexes + self.__updateIndexes() + # Prepare lines + self.__updatePaint() + + def __updateIndexes(self): + if self.top > self.highlight: + self.top = self.highlight + + elif self.top < self.highlight: + # This code tries to find the screen distance between top + # and highlight to adjust top of required + screenY = 0 + idx = self.top + while True: + if idx >= len(self.lines): + self.highlight = 0 + self.top = 0 + return # break try to recover + elif idx > self.highlight: + self.highlight = idx + break + elif idx == self.highlight: + break + line = self.lines[idx] + + if line.collapsed: + idx += line.valueLineRange + + idx += 1 + screenY += 1 + + if screenY >= self.rect.height: + self.top += screenY - self.rect.height + 1 + if self.top > len(self.lines): + self.top = len(self.lines) - 1 + + def __updatePaint(self): + # Paint lines, make sure top and highlight are printed lines + idx = self.top + x = self.rect.x + + for y in range(0, self.rect.height): + + if idx < len(self.lines): + lineData = self.lines[idx] + line = lineData.line + + modifier = None + + # check if this is highlighted + if idx == self.highlight: + # get the required indexes + updateRange = None + if not lineData.keyRange == None: + updateRange = lineData.keyRange + elif not lineData.valueRange == None: + updateRange = lineData.valueRange + # if needed set a line modifier to highlight key parts + if not updateRange == None: + modifier = lambda i, p: p if not i in range(updateRange[0], updateRange[1]) else (p[0] | curses.A_STANDOUT, p[1]) + + suffix = [] + + if lineData.collapsed: + if lineData.valueType == JsonView.LineData.TYPE_OBJECT: + suffix = [" ... },"] + elif lineData.valueType == JsonView.LineData.TYPE_ARRAY: + suffix += [" ... ],"] + idx += lineData.valueLineRange + + self.app.printStyleLine(self.rect.y + y, x, line + suffix, self.rect.width, modifier = modifier, indent = lineData.indent) + + else: + self.app.stdscr.move(y, x) + self.app.stdscr.clrtoeol() + idx += 1 + + def layout(self, rect): + super().layout(rect) + + +class JsonColors: + STRING = 0 + KEY = 0 + NUMBER = 0 + TRUE = 0 + FALSE = 0 + NULL = 0 + + def init(): + JsonColors.STRING = curses.color_pair(ColorPairs.GREEN_BLACK) + JsonColors.KEY = curses.color_pair(ColorPairs.BLUE_BLACK) + JsonColors.NUMBER = curses.color_pair(ColorPairs.CYAN_BLACK) + + From b36d1e828fe159ad8fe9bcc9991e79dae86861e1 Mon Sep 17 00:00:00 2001 From: Lars Maier Date: Sun, 23 Dec 2018 10:53:28 +0100 Subject: [PATCH 2/3] Fixed backspace. --- controls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controls.py b/controls.py index e936a15..d59665f 100644 --- a/controls.py +++ b/controls.py @@ -501,10 +501,10 @@ def userStringLine(self, label = None, complete = None, default = None, prompt = if c == curses.KEY_RESIZE: self.resize() self.update() - elif c == curses.KEY_DC or c == curses.ascii.DEL: + elif c == curses.KEY_DC: if not cursorIndex == len(user): user = user[:cursorIndex] + user[cursorIndex+1:] - elif c == curses.KEY_BACKSPACE: + elif c == curses.KEY_BACKSPACE or c == curses.ascii.DEL: if not cursorIndex == 0: user = user[:cursorIndex-1] + user[cursorIndex:] cursorIndex -= 1 From a9f12768130e77686399a61ebc579f9cc38f2c16 Mon Sep 17 00:00:00 2001 From: Lars Maier Date: Thu, 3 Jan 2019 15:19:22 +0100 Subject: [PATCH 3/3] Use JsonView for StoreView. --- aaa.py | 10 ++-------- jsonctl.py | 11 +++++++---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/aaa.py b/aaa.py index 25f0a4b..31dfab3 100755 --- a/aaa.py +++ b/aaa.py @@ -281,7 +281,7 @@ def set(self, idx): super().set(entry) -class AgencyStoreView(LineView): +class AgencyStoreView(JsonView): def __init__(self, app, rect): super().__init__(app, rect) self.store = None @@ -344,7 +344,7 @@ def updateStore(self): if log[idx]["_key"] >= snapshot["_key"]: self.store.apply(self.app.log[i]["request"]) - self.jsonLines(self.store._ref(self.path)) + self.set(self.store._ref(self.path)) def update(self): @@ -361,12 +361,6 @@ def input(self, c): else: super().input(c) - def set(self, store): - self.store = store - - def jsonLines(self, value): - self.lines = json.dumps(value, indent=4, separators=(',', ': ')).splitlines() - def __common_prefix_idx(self, strings): if len(strings) == 0: return None diff --git a/jsonctl.py b/jsonctl.py index c9e6c24..1741c3a 100644 --- a/jsonctl.py +++ b/jsonctl.py @@ -53,12 +53,12 @@ def add(self, string): def key(self, string): assert self.keyRange == None - self.keyRange = (len(self.line), len(string)) + self.keyRange = (len(self.line), len(self.line) + len(string)) self.line += string def value(self, string): assert self.valueRange == None - self.valueRange = (len(self.line), len(string)) + self.valueRange = (len(self.line), len(self.line) + len(string)) self.line += string def lineRange(self, lineRange, valueType): @@ -149,9 +149,12 @@ def parseList(value, lineData, indent, path): def parsePrimitiveValue(value, lineData, indent, path): if isinstance(value, str): - lineData[-1].value([ - (JsonColors.STRING, '"{}"'.format(value)) + line = lineData[-1] + line.add([(JsonColors.STRING, '"')]) + line.value([ + (JsonColors.STRING, value) ]) + line.add([(JsonColors.STRING, '"')]) elif value is False: lineData[-1].value([ (JsonColors.FALSE, "false")