import os
import visidata
from visidata import Extensible, VisiData, vd, EscapeException, MissingAttrFormatter, AttrDict
UNLOADED = tuple() # sentinel for a sheet not yet loaded for the first time
vd.beforeExecHooks = [] # func(sheet, cmd, args, keystrokes) called before the exec()
class LazyChainMap:
'provides a lazy mapping to obj attributes. useful when some attributes are expensive properties.'
def __init__(self, *objs, locals=None):
self.locals = {} if locals is None else locals
self.objs = {} # [k] -> obj
for obj in objs:
for k in dir(obj):
if k not in self.objs:
self.objs[k] = obj
def __iter__(self):
return iter(self.objs)
def __contains__(self, k):
return k in self.objs
def keys(self):
return list(self.objs.keys()) # sum(set(dir(obj)) for obj in self.objs))
def get(self, key, default=None):
if key in self.locals:
return self.locals[key]
return self.objs.get(key, default)
def clear(self):
self.locals.clear()
def __getitem__(self, k):
obj = self.objs.get(k, None)
if obj:
return getattr(obj, k)
return self.locals[k]
def __setitem__(self, k, v):
obj = self.objs.get(k, None)
if obj:
return setattr(obj, k, v)
self.locals[k] = v
class DrawablePane(Extensible):
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
'Base class for all interaction owners that can be drawn in a window.'
def draw(self, scr):
'Draw on the terminal window *scr*. Should be overridden.'
vd.error('no draw')
@property
def windowHeight(self):
'Height of the current sheet window, in terminal lines.'
return self._scr.getmaxyx()[0] if self._scr else 25
@property
def windowWidth(self):
'Width of the current sheet window, in single-width characters.'
return self._scr.getmaxyx()[1] if self._scr else 80
@property
def currow(self):
return None
def execCommand2(self, cmd, vdglobals=None):
"Execute `cmd` with `vdglobals` as globals and this sheet's attributes as locals. Return True if user cancelled."
try:
self.sheet = self
code = compile(cmd.execstr, cmd.longname, 'exec')
exec(code, vdglobals, LazyChainMap(vd, self))
return False
except EscapeException as e: # user aborted
vd.warning(str(e))
return True
class _dualproperty:
'Return *obj_method* or *cls_method* depending on whether property is on instance or class.'
def __init__(self, obj_method, cls_method):
self._obj_method = obj_method
self._cls_method = cls_method
def __get__(self, obj, objtype=None):
if obj is None:
return self._cls_method(objtype)
else:
return self._obj_method(obj)
[docs]class BaseSheet(DrawablePane):
'Base class for all sheet types.'
_rowtype = object # callable (no parms) that returns new empty item
_coltype = None # callable (no parms) that returns new settable view into that item
rowtype = 'objects' # one word, plural, describing the items
precious = True # False for a few discardable metasheets
defer = False # False for not deferring changes until save
guide = '' # default to show in sidebar
def _obj_options(self):
return vd.OptionsObject(vd._options, obj=self)
def _class_options(cls):
return vd.OptionsObject(vd._options, obj=cls)
class_options = options = _dualproperty(_obj_options, _class_options)
def __init__(self, *names, rows=UNLOADED, **kwargs):
self._name = None # initial cache value necessary for self.options
self._names = []
self.loading = False
self.names = list(names)
self.source = None
self.rows = rows # list of opaque objects
self._scr = None
self.hasBeenModified = False
super().__init__(**kwargs)
self._sidebar = ''
def setModified(self):
if not self.hasBeenModified:
vd.addUndo(setattr, self, 'hasBeenModified', self.hasBeenModified)
self.hasBeenModified = True
def __lt__(self, other):
if self.name != other.name:
return self.name < other.name
else:
return id(self) < id(other)
def __copy__(self):
'Return shallow copy of sheet.'
cls = self.__class__
ret = cls.__new__(cls)
ret.__dict__.update(self.__dict__)
ret.precious = True # copy can be precious even if original is not
ret.hasBeenModified = False # copy is not modified even if original is
return ret
def __bool__(self):
'an instantiated Sheet always tests true'
return True
def __len__(self):
'Number of elements on this sheet.'
return self.nRows
def __str__(self):
return self.name
@property
def rows(self):
return self._rows
@rows.setter
def rows(self, rows):
self._rows = rows
@property
def nRows(self):
'Number of rows on this sheet. Override in subclass.'
return 0
def __contains__(self, vs):
if self.source is vs:
return True
if isinstance(self.source, BaseSheet):
return vs in self.source
return False
@property
def displaySource(self):
if isinstance(self.source, BaseSheet):
return f'the *{self.source}* sheet'
if isinstance(self.source, (list, tuple)):
if len(self.source) == 1:
return f'the **{self.source[0]}** sheet'
return f'{len(self.source)} sheets'
return f'**{self.source}**'
def execCommand(self, longname, vdglobals=None, keystrokes=None):
if ' ' in longname:
cmd, arg = longname.split(' ', maxsplit=1)
vd.injectInput(arg)
cmd = self.getCommand(longname or keystrokes)
if not cmd:
vd.fail('no command for %s' % (longname or keystrokes))
return False
escaped = False
err = ''
if vdglobals is None:
vdglobals = vd.getGlobals()
vd.cmdlog # make sure cmdlog has been created for first command
try:
for hookfunc in vd.beforeExecHooks:
hookfunc(self, cmd, '', keystrokes)
escaped = super().execCommand2(cmd, vdglobals=vdglobals)
except Exception as e:
vd.debug(cmd.execstr)
err = vd.exceptionCaught(e)
escaped = True
if vd.cmdlog:
# sheet may have changed
vd.callNoExceptions(vd.cmdlog.afterExecSheet, vd.activeSheet, escaped, err)
vd.callNoExceptions(self.checkCursor)
vd.clearCaches()
for t in self.currentThreads:
if not hasattr(t, 'lastCommand'):
t.lastCommand = True
return escaped
@property
def lastCommandThreads(self):
return [t for t in self.currentThreads if getattr(t, 'lastCommand', None)]
@property
def names(self):
return self._names
@names.setter
def names(self, names):
if self._names:
vd.addUndo(setattr, self, 'names', self._names)
self._names = names
self._name = self.options.name_joiner.join(self.maybeClean(str(x)) for x in self._names)
@property
def name(self):
'Name of this sheet.'
return self._name
@name.setter
def name(self, name):
'Set name without spaces.'
if self._names:
vd.addUndo(setattr, self, 'names', self._names)
self._name = self.maybeClean(str(name))
self._names = [self._name]
def maybeClean(self, s):
'stub'
return s
def recalc(self):
'Clear any calculated value caches.'
pass
def refresh(self):
'Recalculate any internal state needed for `draw()`. Overridable.'
pass
def ensureLoaded(self):
'Call ``reload()`` if not already loaded.'
if self.rows is UNLOADED:
self.rows = [] # prevent auto-reload from running twice
return self.reload() # likely launches new thread
def reload(self):
'Load sheet from *self.source*. Override in subclass.'
vd.error('no reload')
@property
def cursorRow(self):
'The row object at the row cursor. Overridable.'
return None
def checkCursor(self):
'Check cursor and fix if out-of-bounds. Overridable.'
pass
def evalExpr(self, expr, **kwargs):
'Evaluate Python expression *expr* in the context of *kwargs* (may vary by sheet type).'
return eval(expr, vd.getGlobals(), dict(sheet=self))
def formatString(self, fmt, **kwargs):
'Return formatted string with *sheet* and *vd* accessible to expressions. Missing expressions return empty strings instead of error.'
return MissingAttrFormatter().format(fmt, sheet=self, vd=vd, **kwargs)
@VisiData.api
def redraw(vd):
'Clear the terminal screen and let the next draw cycle recreate the windows and redraw everything.'
for vs in vd.sheets:
vs._scr = None
if vd.win1: vd.win1.clear()
if vd.win2: vd.win2.clear()
if vd.scrFull:
vd.scrFull.clear()
vd.setWindows(vd.scrFull)
@VisiData.property
def sheet(self):
return self.activeSheet
@VisiData.api
def isLongname(self, ks:str):
'Return True if *ks* is a longname.'
return ('-' in ks) and (ks[-1] != '-') or (len(ks) > 3 and ks.islower())
@VisiData.api
def getSheet(vd, sheetname):
'Return Sheet from the sheet stack. *sheetname* can be a sheet name or a sheet number indexing directly into ``vd.sheets``.'
if isinstance(sheetname, BaseSheet):
return sheetname
matchingSheets = [x for x in vd.sheets if x.name == sheetname]
if matchingSheets:
if len(matchingSheets) > 1:
vd.warning('more than one sheet named "%s"' % sheetname)
return matchingSheets[0]
try:
sheetidx = int(sheetname)
return vd.sheets[sheetidx]
except ValueError:
pass
if sheetname == 'options':
vs = vd.globalOptionsSheet
vs.reload()
vs.vd = vd
return vs