From c34ed42cf3480187af3e8ad2ad315004fac0feec Mon Sep 17 00:00:00 2001 From: anjakefala Date: Wed, 22 Nov 2023 23:40:08 -0800 Subject: [PATCH] [guide] add API for getting and adding guides - add vd.getGuide and vd.addGuide - add a Macros guide as proof of concept - add color_heading for markdown formatting - change open-guide to open-guide-index --- visidata/__init__.py | 1 + visidata/cliptext.py | 1 + visidata/{experimental => }/guide.py | 62 ++++++++++++++++++++++++---- visidata/interface.py | 2 + visidata/macros.py | 26 +++++++++++- 5 files changed, 83 insertions(+), 9 deletions(-) rename visidata/{experimental => }/guide.py (59%) diff --git a/visidata/__init__.py b/visidata/__init__.py index f79426f21..f01d9d15f 100644 --- a/visidata/__init__.py +++ b/visidata/__init__.py @@ -68,6 +68,7 @@ def getGlobals(): import visidata.textsheet import visidata.threads import visidata.path +import visidata.guide import visidata._input import visidata.tuiwin diff --git a/visidata/cliptext.py b/visidata/cliptext.py index d34ff1967..4a481a38f 100644 --- a/visidata/cliptext.py +++ b/visidata/cliptext.py @@ -282,6 +282,7 @@ def clipdraw_chunks(scr, y, x, chunks, cattr:ColorAttr=ColorAttr(), w=None, clea def _markdown_to_internal(text): 'Return markdown-formatted `text` converted to internal formatting (like `[:color]text[/]`).' text = re.sub(r'`(.*?)`', r'[:code]\1[/]', text) + text = re.sub(r'(#.*?)$', r'[:heading]\1[/]', text) text = re.sub(r'\*\*(.*?)\*\*', r'[:bold]\1[/]', text) text = re.sub(r'\*(.*?)\*', r'[:italic]\1[/]', text) text = re.sub(r'\b_(.*?)_\b', r'[:underline]\1[/]', text) diff --git a/visidata/experimental/guide.py b/visidata/guide.py similarity index 59% rename from visidata/experimental/guide.py rename to visidata/guide.py index b61ecc0d7..e8e5c4eec 100644 --- a/visidata/experimental/guide.py +++ b/visidata/guide.py @@ -1,27 +1,37 @@ ''' # A Guide to VisiData Guides -Each guide shows you how to use a particular feature in VisiData. +Each guide shows you how to use a particular feature in VisiData. Gray guides have not been written yet. We love contributions: [:onclick https://visidata.org/docs/guides]https://visidata.org/docs/guides[/]. - [:keys]Up/Down[/] to move the row cursor - [:keys]Enter[/] to view a topic - [:keys]Backspace[/] to come back to this list of guides +- [:keystrokes]Up/Down[/] to move the row cursor +- [:keystrokes]Enter[/] to view a topic +- [:keystrokes]Backspace[/] to come back to this list of guides ''' import re -from visidata import vd, BaseSheet, Sheet, ItemColumn, Column, VisiData +from visidata import vd, BaseSheet, Sheet, ItemColumn, Column, VisiData, ENTER, RowColorizer +from visidata import wraptext +vd.guides = {} # name -> guidecls + +@VisiData.api +def addGuide(vd, name, guidecls): + vd.guides[name] = guidecls @VisiData.api class GuideGuide(Sheet): help = __doc__ + rowtype = 'guides' # rowdef: list(guide number, guide name, topic description, points, max_points) columns = [ ItemColumn('n', 0, type=int), - ItemColumn('sheetname', 1, width=0), + ItemColumn('name', 1, width=0), ItemColumn('topic', 2, width=60), Column('points', type=int, getter=lambda c,r: 0), Column('max_points', type=int, getter=lambda c,r: 100), ] + colorizers = [ + RowColorizer(7, 'color_guide_unwritten', lambda s,c,r,v: r and r[1] not in vd.guides) + ] def iterload(self): i = 0 for line in ''' @@ -86,4 +96,42 @@ def iterload(self): yield [i] + list(m.groups()) i += 1 -BaseSheet.addCommand('', 'open-guide', 'vd.push(GuideGuide("VisiData_Guide"))') + def openRow(self, row): + name = row[1] + return vd.getGuide(name) + +class GuideSheet(Sheet): + rowtype = 'lines' + filetype = 'guide' + columns = [ + ItemColumn('linenum', 0, type=int, width=0), + ItemColumn('guide', 1, width=80, displayer='full'), + ] + precious = False + guide = '' + + def iterload(self): + winWidth = 78 + for startingLine, text in enumerate(self.guide.splitlines()): + text = text.strip() + if text: + for i, (L, _) in enumerate(wraptext(str(text), width=winWidth)): + yield [startingLine+i+1, L] + else: + yield [startingLine+1, text] + + + +@VisiData.api +def getGuide(vd, name): # -> GuideSheet() + if name in vd.guides: + return vd.guides[name]() + vd.warning(f'no guide named {name}') + +BaseSheet.addCommand('', 'open-guide-index', 'vd.push(GuideGuide("VisiData_Guide"))', 'open VisiData guides table of contents') + +vd.addMenuItems(''' + Help > VisiData Feature Guides > open-guide-index +''') + +vd.addGlobals({'GuideSheet':GuideSheet}) diff --git a/visidata/interface.py b/visidata/interface.py index 12fe5824b..4ccaf3a06 100644 --- a/visidata/interface.py +++ b/visidata/interface.py @@ -47,6 +47,8 @@ vd.theme_option('color_selected_row', '215 yellow', 'color of selected rows') vd.theme_option('color_clickable', 'underline', 'color of internally clickable item') vd.theme_option('color_code', 'bold white on 237', 'color of code sample') +vd.theme_option('color_heading', 'bold 200', 'color of header') +vd.theme_option('color_guide_unwritten', '243 on black', 'color of unwritten guides in GuideGuide') vd.theme_option('force_256_colors', False, 'use 256 colors even if curses reports fewer') diff --git a/visidata/macros.py b/visidata/macros.py index 19eefcd6e..db07a75c2 100644 --- a/visidata/macros.py +++ b/visidata/macros.py @@ -4,7 +4,7 @@ from visidata.cmdlog import CommandLog, CommandLogJsonl from visidata import vd, UNLOADED, asyncthread -from visidata import IndexSheet, VisiData, Sheet, Path, VisiDataMetaSheet, Column, ItemColumn, BaseSheet +from visidata import IndexSheet, VisiData, Sheet, Path, VisiDataMetaSheet, Column, ItemColumn, BaseSheet, GuideSheet vd.macroMode = None vd.macrobindings = {} @@ -125,11 +125,31 @@ def startMacro(cmdlog): vd.status("recording macro; stop recording with `m`") vd.macroMode = CommandLogJsonl('current_macro', rows=[]) - @VisiData.before def run(vd, *args, **kwargs): vd.macrosheet +class MacrosGuide(GuideSheet): + guide = '''# Macros +Macros allow you to bind a command sequence to a keystroke or longname, to replay when that keystroke is pressed or the command is executed by longname. + +The basic usage is: + 1. Press `m` (macro-record) to begin recording the macro. + 2. Go through the commands you wish to record. + 3. Then type `m` again to complete the recording, and prompt for the keystroke or longname to bind it to. + +The macro will then be executed everytime the provided keystroke or longname are used. Note: the Alt+keys and the function keys are left unbound; overriding other keys may conflict with existing bindings, now or in the future. + +Executing a macro will the series of commands starting on the current row and column on the current sheet. + +# The Macros Sheet + +Use `gm` (`macro-sheet`) to open an index of existing macros. + +Use `d` to mark macros for deletion. Use `z Ctrl+S` to then commit any changes. + +`Enter` to open the macro in the current row, and you can view the series of commands composing it.''' + Sheet.addCommand('m', 'macro-record', 'vd.cmdlog.startMacro()', 'record macro') Sheet.addCommand('gm', 'macro-sheet', 'vd.push(vd.macrosheet)', 'open macros sheet') @@ -137,3 +157,5 @@ def run(vd, *args, **kwargs): vd.addMenuItems(''' System > Macros sheet > macro-sheet ''') + +vd.addGuide('MacrosSheet', MacrosGuide)