editcurses: improvements while pushing to gerrit
not yet merged Change-Id: Ia8ba4feedb78f008052910f06e1946ade03b853a
This commit is contained in:
+74
-112
@@ -1,7 +1,26 @@
|
||||
# *****************************************************************************
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Module authors:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
from subprocess import Popen, PIPE
|
||||
from pathlib import Path
|
||||
from psutil import pid_exists
|
||||
import frappy
|
||||
@@ -15,7 +34,8 @@ from frappy.editcurses.configdata import Value, cfgdata_to_py, cfgdata_from_py,
|
||||
from frappy.io import IOBase
|
||||
import frappy.editcurses.terminalgui as tg
|
||||
from frappy.editcurses.terminalgui import Main, MenuItem, TextEdit, PushButton, \
|
||||
ModalDialog, KEY
|
||||
ModalDialog
|
||||
from frappy.editcurses.screenwriter import KEY
|
||||
|
||||
|
||||
KEY.add(
|
||||
@@ -32,30 +52,6 @@ TIMESTAMP_FMT = '%Y-%m-%d-%H%M%S'
|
||||
def get_timestamp(file):
|
||||
return time.strftime(TIMESTAMP_FMT, time.localtime(file.stat().st_mtime))
|
||||
|
||||
# TODO:
|
||||
# - use also shift-Tab for level up?
|
||||
|
||||
|
||||
def unix_cmd(cmd, *args):
|
||||
out = Popen(cmd.split() + list(args), stdout=PIPE).communicate()[0]
|
||||
return list(out.decode().split('\n'))
|
||||
|
||||
|
||||
class StringValue: # TODO: unused?
|
||||
error = None
|
||||
|
||||
def __init__(self, value, from_string=False, datatype=None):
|
||||
self.strvalue = value
|
||||
|
||||
def set_value(self, value):
|
||||
self.strvalue = value
|
||||
|
||||
def set_from_string(self, strvalue):
|
||||
self.strvalue = strvalue
|
||||
|
||||
def get_repr(self):
|
||||
return repr(self.strvalue)
|
||||
|
||||
|
||||
class TopWidget:
|
||||
parent_cls = Main
|
||||
@@ -73,9 +69,9 @@ class Child(tg.Widget):
|
||||
parent = TopWidget
|
||||
|
||||
def get_name(self):
|
||||
return None
|
||||
raise NotImplementedError
|
||||
|
||||
def collect(self, cfgdict):
|
||||
def collect(self, result):
|
||||
pass
|
||||
|
||||
def check_data(self):
|
||||
@@ -88,6 +84,9 @@ class Child(tg.Widget):
|
||||
class HasValue(Child):
|
||||
clsobj = None
|
||||
|
||||
def get_name(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def init_value_widget(self, parent, valobj):
|
||||
self.init_parent(parent)
|
||||
self.valobj = valobj
|
||||
@@ -143,7 +142,6 @@ class ValueWidget(HasValue, tg.LineEdit):
|
||||
self.fixedname = name
|
||||
else:
|
||||
labelwidget = tg.NameEdit(name, self.validate_name)
|
||||
# self.log.info('value widget %r %r', name, self.fixedname)
|
||||
if valobj.completion:
|
||||
valueedit = tg.TextEditCompl(valobj.strvalue, self.validate, valobj.completion)
|
||||
else:
|
||||
@@ -173,18 +171,21 @@ class ValueWidget(HasValue, tg.LineEdit):
|
||||
return self.fixedname
|
||||
return self.labelwidget.value
|
||||
|
||||
def collect(self, as_dict):
|
||||
def collect(self, result):
|
||||
"""collect data"""
|
||||
name = self.get_name()
|
||||
if name:
|
||||
as_dict[name] = self.valobj
|
||||
result[name] = self.valobj
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
super().draw(wr, in_focus)
|
||||
valobj = self.valobj
|
||||
if valobj.strvalue == '':
|
||||
wr.dim(valobj.datatype.to_string(valobj.default))
|
||||
elif self.error:
|
||||
if valobj.strvalue == '' and valobj.default is not None:
|
||||
default = valobj.datatype.to_string(valobj.default)
|
||||
if not in_focus:
|
||||
wr.dim(default)
|
||||
return
|
||||
if self.error:
|
||||
wr.norm(' ')
|
||||
wr.error(self.error)
|
||||
|
||||
@@ -201,9 +202,9 @@ class DocWidget(HasValue, tg.MultiLineEdit):
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def collect(self, config):
|
||||
def collect(self, result):
|
||||
self.valobj.set_value(self.value)
|
||||
config[self.name] = self.valobj
|
||||
result[self.name] = self.valobj
|
||||
|
||||
|
||||
class BaseWidget(TopWidget, tg.Container):
|
||||
@@ -235,7 +236,6 @@ class BaseWidget(TopWidget, tg.Container):
|
||||
label = self.fixed_names.get(name)
|
||||
widget = ValueWidget(self, name, valobj, label)
|
||||
self.widget_dict[name] = widget
|
||||
# self.log.info('add widget %r: label=%r name=%r', name, label, widget.get_name())
|
||||
if pos is None:
|
||||
self.widgets.append(widget)
|
||||
else:
|
||||
@@ -257,7 +257,9 @@ class BaseWidget(TopWidget, tg.Container):
|
||||
# module.set_focus(0) # go to name widget
|
||||
|
||||
def add_module(self, after_current=False):
|
||||
modcfg = {'name': Value(''), 'cls': Value(f'{site.frappy_subdir}.'), 'description': Value('')}
|
||||
modcfg = {'name': Value(''),
|
||||
'cls': Value(f'{site.frappy_subdir}.', ModuleClass),
|
||||
'description': Value('')}
|
||||
self.insert_module(ModuleWidget(self.parent, '', modcfg), after_current)
|
||||
|
||||
def add_iomodule(self, after_current=False):
|
||||
@@ -265,10 +267,7 @@ class BaseWidget(TopWidget, tg.Container):
|
||||
self.insert_module(IOWidget(self.parent, '', modcfg), after_current)
|
||||
|
||||
def get_widget_value(self, key):
|
||||
try:
|
||||
return self.widget_dict[key].valobj.strvalue
|
||||
except KeyError:
|
||||
return ''
|
||||
return self.widget_dict[key].valobj.strvalue
|
||||
|
||||
def get_name(self):
|
||||
return self.get_widget_value('name')
|
||||
@@ -298,14 +297,14 @@ class ModuleName(Value):
|
||||
self.main = main
|
||||
super().__init__(name)
|
||||
|
||||
def validate_from_string(self, value):
|
||||
if not value:
|
||||
def validate_from_string(self, strvalue):
|
||||
if not strvalue:
|
||||
self.strvalue = self.value = ''
|
||||
raise ValueError('empty name')
|
||||
if value != self.value and value in self.main.widget_dict:
|
||||
if strvalue != self.value and strvalue in self.main.modules:
|
||||
self.strvalue = self.value = ''
|
||||
raise ValueError(f'duplicate name {value!r}')
|
||||
self.value = self.strvalue = value
|
||||
raise ValueError(f'duplicate name {strvalue!r}')
|
||||
self.value = self.strvalue = strvalue
|
||||
|
||||
|
||||
class ModuleWidget(BaseWidget):
|
||||
@@ -321,14 +320,14 @@ class ModuleWidget(BaseWidget):
|
||||
MenuItem('add module', 'm', self.add_module),
|
||||
MenuItem('add io module', 'i', self.add_iomodule),
|
||||
MenuItem('purge empty items', 'e', self.purge_prs),
|
||||
MenuItem('add configurable items', '+', self.complete_prs),
|
||||
MenuItem('add configurable items', 'a', self.complete_prs),
|
||||
MenuItem('cut module', KEY.CUT, parent.cut_module),
|
||||
]
|
||||
|
||||
self.configure_class(modulecfg.get('cls'))
|
||||
|
||||
for name, valobj in modulecfg.items():
|
||||
self.add_widget(name, valobj)
|
||||
for pname, valobj in modulecfg.items():
|
||||
self.add_widget(pname, valobj)
|
||||
self.widgets.append(EndLine(self))
|
||||
|
||||
def configure_class(self, clsvalue):
|
||||
@@ -372,8 +371,6 @@ class ModuleWidget(BaseWidget):
|
||||
|
||||
def check_data(self):
|
||||
"""check clsobj is valid and check all params and props"""
|
||||
# clswidget, = self.find_widgets('cls')
|
||||
# clsobj = clswidget.valobj.value
|
||||
for widget in self.widgets:
|
||||
widget.check_data()
|
||||
|
||||
@@ -393,8 +390,6 @@ class ModuleWidget(BaseWidget):
|
||||
fixed_names[name] = name
|
||||
if name not in names and mandatory >= only_mandatory:
|
||||
valobj = Value('', *get_datatype(name, self.clsobj, ''))
|
||||
if name == 'cls':
|
||||
self.log.info('add needed %r', valobj)
|
||||
widget = self.add_widget(name, valobj, -1)
|
||||
if mandatory:
|
||||
widget.error = 'please set this mandatory property'
|
||||
@@ -508,9 +503,9 @@ class NodeName(Value):
|
||||
self.main = main
|
||||
super().__init__(name)
|
||||
|
||||
def validate_from_string(self, value):
|
||||
def validate_from_string(self, strvalue):
|
||||
try:
|
||||
self.main.set_node_name(value)
|
||||
self.main.set_node_name(strvalue)
|
||||
except Exception:
|
||||
self.value = self.strvalue = self.main.cfgname
|
||||
raise
|
||||
@@ -528,13 +523,13 @@ class NodeWidget(BaseWidget):
|
||||
MenuItem('add parameter/property', 'p', self.new_widget),
|
||||
# MenuItem('select line', '^K', self.select, None),
|
||||
]
|
||||
for name, valobj in nodecfg.items():
|
||||
if name == 'doc':
|
||||
docwidget = DocWidget(self, name, valobj)
|
||||
for pname, valobj in nodecfg.items():
|
||||
if pname == 'doc':
|
||||
docwidget = DocWidget(self, pname, valobj)
|
||||
self.widgets.append(docwidget)
|
||||
self.widget_dict['doc'] = docwidget
|
||||
else:
|
||||
self.add_widget(name, valobj)
|
||||
self.add_widget(pname, valobj)
|
||||
self.widgets.append(EndLine(self))
|
||||
|
||||
def new_widget(self, name=''):
|
||||
@@ -549,13 +544,14 @@ class NodeWidget(BaseWidget):
|
||||
if self.parent.detailed or self.get_focus_widget().get_name() in self.summ_edit:
|
||||
return True
|
||||
focus = self.focus + step
|
||||
return False
|
||||
|
||||
def focus_row(self, to_focus):
|
||||
main = self.parent
|
||||
if main.detailed:
|
||||
return super().focus_row(to_focus)
|
||||
height = 0
|
||||
for nr, widget in enumerate(self.widgets[:to_focus]):
|
||||
for widget in self.widgets[:to_focus]:
|
||||
name = widget.get_name()
|
||||
if name in self.summ_edit:
|
||||
height += widget.height()
|
||||
@@ -603,43 +599,10 @@ class SaveDialog(ModalDialog):
|
||||
return None
|
||||
|
||||
|
||||
HELP_TEXT = """
|
||||
Frappy Configuration Editor
|
||||
---------------------------
|
||||
|
||||
A configuration files has a Node section, followed by any number of IO and
|
||||
Module sections. IO section typically just contain the name and an uri.
|
||||
A Module sections key item is the 'cls', denoting the python class for
|
||||
the implementation. Entering the class is supported by a completion popup
|
||||
menu, which opens as soon as you start typing.
|
||||
When opening a file, the editor is in summary mode, showing a compact
|
||||
overview over all modules. Use ctrl-T to toggle to detailed view to
|
||||
be able to edit individual items.
|
||||
|
||||
|
||||
Modify entries
|
||||
--------------
|
||||
|
||||
To enter a new value a field, start typing. To modify a value press ctrl-A
|
||||
of ctrl-E to go the the start or end of the string.
|
||||
|
||||
|
||||
Context Menu
|
||||
-------------
|
||||
|
||||
Press ctrl-X to open a context menu. Navigate to an entry an press RETURN
|
||||
or press the key indicated to the left to execute an action. A key starting
|
||||
with ^ indicates to the given action may be performed with a ctrl-<key>
|
||||
directly without preceding ctrl-X. However, within a context menu,
|
||||
pressing the letter without ctrl works also.
|
||||
"""
|
||||
|
||||
|
||||
class EditorMain(Main):
|
||||
name = 'Main'
|
||||
detailed = False
|
||||
tmppath = None
|
||||
help_text = HELP_TEXT
|
||||
version_view = 0 # current version or when > 0 previous versions (not editable)
|
||||
completion_widget = None # widget currently running a thread for guesses
|
||||
leftwidth = 0.15
|
||||
@@ -649,8 +612,8 @@ class EditorMain(Main):
|
||||
def __init__(self, cfg):
|
||||
try:
|
||||
self.titlebar = tg.TitleBar('Frappy Cfg Editor')
|
||||
super().__init__([], tg.Writer, [self.titlebar], [tg.StatusBar(self)],
|
||||
help_file=Path(frappy.__file__).parents[1] / 'resources/editcurses/help.txt')
|
||||
super().__init__([], [self.titlebar], [tg.StatusBar(self)],
|
||||
help_file=Path(frappy.__file__).parents[1] / 'resources/editcurses_help.txt')
|
||||
# self.select_menu = MenuItem('select module', CUT_KEY)
|
||||
self.version_menu = [
|
||||
MenuItem('previous version', KEY.PREV_VERSION, self.prev_version),
|
||||
@@ -670,7 +633,7 @@ class EditorMain(Main):
|
||||
self.dirty = False
|
||||
# cleanup pidfiles
|
||||
for file in self.version_dir.glob('*.pid'):
|
||||
pidstr = file.read_text()
|
||||
pidstr = file.read_text(encoding='ascii')
|
||||
if not pid_exists(int(pidstr)):
|
||||
file.unlink()
|
||||
cfgpath = Path(cfg)
|
||||
@@ -728,7 +691,7 @@ class EditorMain(Main):
|
||||
self.offset = None # recalculate offset from screen pos
|
||||
self.status(None)
|
||||
|
||||
def get_key(self):
|
||||
def get_key(self, timeout=None):
|
||||
if self.dirty:
|
||||
if not self.version_view:
|
||||
self.save()
|
||||
@@ -737,9 +700,10 @@ class EditorMain(Main):
|
||||
if self.completion_widget:
|
||||
key = super().get_key(0.1)
|
||||
if key is None:
|
||||
# may need refresh as contents may be built in background
|
||||
continue
|
||||
else:
|
||||
key = super().get_key()
|
||||
key = super().get_key(timeout)
|
||||
if self.version_view:
|
||||
if isinstance(key, str) or key in [KEY.DEL]:
|
||||
self.status('', 'can not edit previous version')
|
||||
@@ -769,7 +733,6 @@ class EditorMain(Main):
|
||||
self.widgets[self.focus:self.focus+1] = []
|
||||
if not self.cut_extend:
|
||||
self.cut_modules = []
|
||||
self.log.info('start cut modules')
|
||||
self.cut_modules.append(module)
|
||||
self.cut_extend = True
|
||||
if self.version_view:
|
||||
@@ -786,7 +749,7 @@ class EditorMain(Main):
|
||||
self.widgets[self.focus:self.focus] = self.cut_modules
|
||||
self.cut_modules = []
|
||||
|
||||
def set_node_name(self, name, cfgpath=None):
|
||||
def set_node_name(self, name, init_cfgpath=None):
|
||||
if name == self.cfgname:
|
||||
return
|
||||
if not name:
|
||||
@@ -796,7 +759,7 @@ class EditorMain(Main):
|
||||
self.titlebar.mid = name
|
||||
versions_path = self.version_dir / f'{name}.versions'
|
||||
try:
|
||||
sections = versions_path.read_text().split(VERSION_SEPARATOR)
|
||||
sections = versions_path.read_text(encoding='utf-8').split(VERSION_SEPARATOR)
|
||||
assert sections.pop(0) == ''
|
||||
except FileNotFoundError:
|
||||
sections = []
|
||||
@@ -805,12 +768,12 @@ class EditorMain(Main):
|
||||
self.tmppath = self.version_dir / f'{name}.current'
|
||||
try:
|
||||
# if a file exists already, add it to the version history
|
||||
filecontent = self.tmppath.read_text()
|
||||
filecontent = self.tmppath.read_text(encoding='utf-8')
|
||||
self.add_version(filecontent, get_timestamp(self.tmppath))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
if cfgpath:
|
||||
cfgpaths = [cfgpath]
|
||||
if init_cfgpath:
|
||||
cfgpaths = [init_cfgpath]
|
||||
else:
|
||||
try:
|
||||
cfgpaths = [to_config_path(name, self.log)]
|
||||
@@ -819,7 +782,7 @@ class EditorMain(Main):
|
||||
cfgpaths.append(self.tmppath)
|
||||
for cfgpath in cfgpaths:
|
||||
try:
|
||||
filecontent = cfgpath.read_text()
|
||||
filecontent = cfgpath.read_text(encoding='utf-8')
|
||||
self.cfgpath = cfgpath
|
||||
if cfgpath != self.tmppath:
|
||||
self.titlebar.mid = str(cfgpath)
|
||||
@@ -848,7 +811,7 @@ class EditorMain(Main):
|
||||
sep = VERSION_SEPARATOR
|
||||
versions_path = self.version_dir / f'{self.cfgname}.versions'
|
||||
tmpname = versions_path.with_suffix('.tmp')
|
||||
with open(tmpname, 'w') as f:
|
||||
with open(tmpname, 'w', encoding='utf-8') as f:
|
||||
for ts, section in self.versions.items():
|
||||
f.write(sep)
|
||||
f.write(f'{ts}\n')
|
||||
@@ -889,7 +852,6 @@ class EditorMain(Main):
|
||||
def prev_version(self):
|
||||
maxv = len(self.versions)
|
||||
self.version_view += 1
|
||||
self.log.info('back to version %r', self.version_view)
|
||||
if self.version_view > maxv:
|
||||
self.status('this is the oldest version')
|
||||
self.version_view = maxv
|
||||
@@ -935,7 +897,7 @@ class EditorMain(Main):
|
||||
filename = savedialog.filename
|
||||
if filename:
|
||||
self.log.info('saved %r to %r', self.cfgname, filename)
|
||||
Path(filename).write_text(self.filecontent)
|
||||
Path(filename).write_text(self.filecontent, encoding='utf-8')
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -950,7 +912,7 @@ class EditorMain(Main):
|
||||
mypid = os.getpid()
|
||||
for itry in range(15):
|
||||
try:
|
||||
with open(pidfile, 'x') as f:
|
||||
with open(pidfile, 'x', encoding='ascii') as f:
|
||||
f.write(str(mypid))
|
||||
if self.pidfile and self.pidfile.exists():
|
||||
self.pidfile.unlink()
|
||||
@@ -959,7 +921,7 @@ class EditorMain(Main):
|
||||
except FileExistsError:
|
||||
pass
|
||||
try:
|
||||
pid = int(pidfile.read_text())
|
||||
pid = int(pidfile.read_text(encoding='ascii'))
|
||||
if pid == mypid:
|
||||
if self.pidfile and self.pidfile != pidfile and self.pidfile.exists():
|
||||
self.pidfile.unlink()
|
||||
@@ -975,7 +937,7 @@ class EditorMain(Main):
|
||||
def run(self):
|
||||
try:
|
||||
super().run()
|
||||
except Exception:
|
||||
except BaseException:
|
||||
print(formatExtendedTraceback())
|
||||
finally:
|
||||
if self.filecontent:
|
||||
|
||||
@@ -20,15 +20,15 @@
|
||||
# *****************************************************************************
|
||||
|
||||
import re
|
||||
import frappy
|
||||
import inspect
|
||||
import socket
|
||||
from pathlib import Path
|
||||
from ast import literal_eval
|
||||
from importlib import import_module
|
||||
import frappy
|
||||
from frappy.lib.comparestring import compare
|
||||
from frappy.config import process_file, Node
|
||||
from frappy.core import Module, Parameter, Property, Attached
|
||||
from frappy.core import Module, Parameter, Attached
|
||||
from frappy.datatypes import DataType, EnumType
|
||||
from frappy.properties import Property, UNSET
|
||||
|
||||
@@ -67,8 +67,8 @@ class NonStringType:
|
||||
"""convert from string """
|
||||
try:
|
||||
return literal_eval(strvalue)
|
||||
except Exception:
|
||||
raise ValueError('this is no python value')
|
||||
except Exception as e:
|
||||
raise ValueError('this is no python value') from e
|
||||
|
||||
def to_string(self, value):
|
||||
return repr(value)
|
||||
@@ -92,7 +92,7 @@ class SimpleStringType(NonStringType):
|
||||
return strvalue
|
||||
|
||||
def to_string(self, value):
|
||||
return value
|
||||
return value or '' # convert None to empty string
|
||||
|
||||
def format_value(self, value, unit=None):
|
||||
"""convert to string
|
||||
@@ -160,7 +160,8 @@ class Value:
|
||||
self.datatype = stringtype if isinstance(value, str) else nonstringtype
|
||||
self.set_value(value)
|
||||
|
||||
def callback(self, value):
|
||||
def callback(self, value): # pylint: disable=method-hidden
|
||||
"""to be overridden"""
|
||||
return value
|
||||
|
||||
def set_value(self, value):
|
||||
@@ -195,7 +196,7 @@ class Value:
|
||||
if self.datatype:
|
||||
try:
|
||||
return self.datatype.format_value(self.value, False)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
pass
|
||||
return repr(self.strvalue)
|
||||
|
||||
@@ -314,8 +315,7 @@ class ClassChecker:
|
||||
|
||||
|
||||
class ModuleClass(DataType):
|
||||
@classmethod
|
||||
def validate(cls, value, previous=None):
|
||||
def __call__(self, value):
|
||||
if isinstance(value, type):
|
||||
if issubclass(value, Module):
|
||||
return value
|
||||
@@ -327,22 +327,25 @@ class ModuleClass(DataType):
|
||||
raise ValueError(value)
|
||||
return checker.clsobj
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, strvalue):
|
||||
return cls.validate(strvalue)
|
||||
def from_string(self, text):
|
||||
return self.validate(text)
|
||||
|
||||
@classmethod
|
||||
def to_string(cls, value):
|
||||
value = cls.validate(value)
|
||||
def to_string(self, value):
|
||||
value = self.validate(value)
|
||||
return f'{value.__module__}.{value.__qualname__}'
|
||||
|
||||
@classmethod
|
||||
def format_value(cls, value, unit=None):
|
||||
result = repr(cls.to_string(value))
|
||||
def format_value(self, value, unit=None):
|
||||
result = repr(self.to_string(value))
|
||||
if '<' in result:
|
||||
raise ValueError(result, value)
|
||||
return result
|
||||
|
||||
def compatible(self, other):
|
||||
raise NotImplementedError
|
||||
|
||||
def export_datatype(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
module_class = ModuleClass()
|
||||
|
||||
@@ -378,10 +381,10 @@ def moddata_to_py(name, cls, description, **kwds):
|
||||
raise ValueError(cls)
|
||||
items = [f'Mod({name!r}', cls.get_repr(), description.get_repr()]
|
||||
paramdict = {}
|
||||
for name, valobj in kwds.items():
|
||||
param, _, prop = name.partition('.')
|
||||
for pname, valobj in kwds.items():
|
||||
param, _, prop = pname.partition('.')
|
||||
paramdict.setdefault(param, {})[prop or 'value'] = valobj
|
||||
for name, props in paramdict.items():
|
||||
for pname, props in paramdict.items():
|
||||
valueitem = props.pop('value', None)
|
||||
if valueitem is None:
|
||||
args = []
|
||||
@@ -389,12 +392,12 @@ def moddata_to_py(name, cls, description, **kwds):
|
||||
args = [valueitem.get_repr()]
|
||||
if not props:
|
||||
# single value
|
||||
items.append(f'{name} = {args[0]}')
|
||||
items.append(f'{pname} = {args[0]}')
|
||||
continue
|
||||
# args contains value
|
||||
# extend with keyworded values for parameter properties
|
||||
args.extend(f'{k}={v.get_repr()}' for k, v in props.items())
|
||||
items.append(f"{name} = Param({', '.join(args)})")
|
||||
items.append(f"{pname} = Param({', '.join(args)})")
|
||||
if len(items) == 1:
|
||||
return f"{items[0]})"
|
||||
items.append(')')
|
||||
@@ -497,14 +500,14 @@ def cfgdata_from_py(name, cfgpath, filecontent, logger):
|
||||
continue
|
||||
try:
|
||||
ioclass = f"{modcls}.ioClass"
|
||||
if ModuleClass.validate(iomodcls) != ModuleClass.validate(ioclass):
|
||||
if module_class.validate(iomodcls) != module_class.validate(ioclass):
|
||||
continue
|
||||
iomodcfg['cls'] = '<auto>'
|
||||
iomodcfg.pop('description', None)
|
||||
iodict[ioname] = iomodcfg
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
iomod, iocls = iomodcls.rsplit('.', 1)
|
||||
mod, cls = modcls.rsplit('.', 1)
|
||||
mod = modcls.rsplit('.', 1)[0]
|
||||
if mod == iomod:
|
||||
iomodcls = iocls
|
||||
errors[ioname] = f'{ioname}: missing ioClass={iomodcls} in source code of {modcls}'
|
||||
@@ -530,7 +533,7 @@ def recommended_prs(cls):
|
||||
return {}
|
||||
cls = checker.clsobj
|
||||
result = {}
|
||||
for pname, pdict in cls.configurables.items():
|
||||
for pname in cls.configurables:
|
||||
pr = getattr(cls, pname)
|
||||
if isinstance(pr, Property):
|
||||
if pname not in SKIP_PROPS:
|
||||
|
||||
@@ -18,86 +18,10 @@
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
#
|
||||
# *****************************************************************************
|
||||
# pylint: disable=too-many-lines
|
||||
import sys
|
||||
import curses
|
||||
import threading
|
||||
from select import select
|
||||
|
||||
|
||||
class KEY:
|
||||
"""helper class for keys
|
||||
|
||||
converts all keys used into an instance of Key
|
||||
"""
|
||||
# unfortunately, we can not use some ctrl keys, as they already have a meaning:
|
||||
# ^H: backspace, ^I: tab, ^M: ret, ^S/^Q: flow control
|
||||
ESC = 27
|
||||
TAB = 9
|
||||
DEL = 127
|
||||
RETURN = 13
|
||||
QUIT = '^q'
|
||||
BEG_LINE = '^a'
|
||||
END_LINE = '^e'
|
||||
MENU = '^x'
|
||||
CUT = '^k'
|
||||
PASTE = '^v'
|
||||
HELP = '^g'
|
||||
UNHANDLED = -1
|
||||
GOTO_MAIN = -2
|
||||
GO_UP = -3
|
||||
UP = None # get curses.KEY_UP
|
||||
DOWN = None
|
||||
LEFT = None
|
||||
RIGHT = None
|
||||
ENTER = None
|
||||
bynumber = {k: chr(k) for k in range(32, 127)}
|
||||
byname = {}
|
||||
|
||||
@classmethod
|
||||
def init(cls):
|
||||
for name in dir(cls):
|
||||
if name.isupper():
|
||||
cls.add_key(name, getattr(cls, name))
|
||||
for base in cls.__mro__:
|
||||
for name in getattr(base, 'from_curses', ()):
|
||||
cls.add_key(name, getattr(curses, f'KEY_{name}'))
|
||||
|
||||
@classmethod
|
||||
def add_key(cls, name, nr):
|
||||
if isinstance(nr, str):
|
||||
assert nr[0] == '^'
|
||||
nr = ord(nr[1]) & 0x1f
|
||||
elif nr is None:
|
||||
nr = getattr(curses, f'KEY_{name}')
|
||||
cls.byname[name] = cls.bynumber[nr] = key = Key(name, nr)
|
||||
setattr(cls, name, key)
|
||||
|
||||
@classmethod
|
||||
def add(cls, **kwds):
|
||||
for name, nr in kwds.items():
|
||||
cls.add_key(name, nr)
|
||||
|
||||
|
||||
class Key(int):
|
||||
def __new__(cls, name, nr):
|
||||
if isinstance(nr, str):
|
||||
if nr.startswith('^'):
|
||||
nr = ord(nr[1]) & 0x1f
|
||||
key = super().__new__(cls, nr)
|
||||
key.name = name
|
||||
return key
|
||||
|
||||
def short(self):
|
||||
"""as __repr__, but ctrl keys are translated to ^<letter>"""
|
||||
if 0 <= self < 32:
|
||||
if 0 < self <= 26:
|
||||
return f'^{chr(96 + self)}'
|
||||
return f'^{chr(64 + self)}'
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
"""name by function"""
|
||||
return self.name
|
||||
from .screenwriter import Screen, Writer, KEY, Key
|
||||
|
||||
|
||||
# keep this file independent of frappy
|
||||
@@ -169,13 +93,10 @@ class Widget:
|
||||
def width(self):
|
||||
return self.default_width
|
||||
|
||||
# def draw(self, wr, in_focus=False):
|
||||
# raise NotImplementedError
|
||||
|
||||
|
||||
class HasWidgets:
|
||||
focus = 0
|
||||
widgets = None # list of subwidgets
|
||||
widgets = () # list of subwidgets
|
||||
|
||||
def current_row(self):
|
||||
return self.focus_row(self.focus) + self.get_focus_widget().current_row()
|
||||
@@ -261,7 +182,7 @@ class TitleBar(Widget):
|
||||
else:
|
||||
text = left.ljust(wid - len(right)) + right
|
||||
wr.startrow()
|
||||
wr.bar(text + ' ')
|
||||
wr.wr(text + ' ', wr.barstyle)
|
||||
|
||||
|
||||
class StatusBar(Widget):
|
||||
@@ -640,15 +561,13 @@ class HelpWidget(MultiLineEdit):
|
||||
self.readonly = False
|
||||
break
|
||||
self.focus = clamp(0, main.offset + step, height)
|
||||
try:
|
||||
super().handle_inner(main, None, self.focus)
|
||||
return
|
||||
finally:
|
||||
if self.original != self.value:
|
||||
main.popupmenu = menu = ConfirmDialog('save changes to help text?')
|
||||
if menu.handle(main):
|
||||
main.help_file.write_text(self.value)
|
||||
main.status(f'{main.help_file} changed')
|
||||
while super().handle_inner(main, None, self.focus) != KEY.QUIT:
|
||||
pass
|
||||
if self.original != self.value:
|
||||
main.popupmenu = menu = ConfirmDialog('save changes to help text [N]?')
|
||||
if menu.handle(main):
|
||||
main.help_file.write_text(self.value)
|
||||
main.status(f'{main.help_file} changed')
|
||||
|
||||
|
||||
class LineOfMultiline(TextEdit):
|
||||
@@ -683,7 +602,7 @@ class LineOfMultiline(TextEdit):
|
||||
return KEY.DOWN
|
||||
if key == KEY.DEL:
|
||||
if self.pos == 0:
|
||||
if multiline.focus > 0:
|
||||
if 0 < multiline.focus <= len(multiline.widgets):
|
||||
thisline = self.value
|
||||
del multiline.widgets[multiline.focus]
|
||||
prev = multiline.widgets[multiline.focus - 1]
|
||||
@@ -980,277 +899,6 @@ class ContextMenu(PopUpMenu):
|
||||
wr.rectangle(row, col, height - 1, width + 4)
|
||||
|
||||
|
||||
class BaseWriter:
|
||||
"""base for writer. does nothing else than keeping track of position"""
|
||||
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = buttonstyle = dimstyle = None
|
||||
errorflag = '! '
|
||||
|
||||
def __init__(self):
|
||||
self.width = None
|
||||
self.nextrow = 0
|
||||
self.left = 0
|
||||
|
||||
def move(self, row, col):
|
||||
self.row = self.nextrow = row
|
||||
self.col = self.left = col
|
||||
|
||||
def startrow(self):
|
||||
self.row = self.nextrow
|
||||
self.col = self.left
|
||||
self.nextrow = self.row + 1
|
||||
|
||||
def norm(self, text, width=0):
|
||||
self.wr(text, extend_to=width)
|
||||
|
||||
def dim(self, text, width=0):
|
||||
self.wr(text, self.dimstyle, extend_to=width)
|
||||
|
||||
def bar(self, text, width=0):
|
||||
self.wr(text, self.barstyle, extend_to=width)
|
||||
|
||||
def edit(self, text, width, pos):
|
||||
self.bright(text, width)
|
||||
|
||||
def bright(self, text, width=0):
|
||||
self.wr(text.ljust(width), self.brightstyle, extend_to=width)
|
||||
|
||||
def high(self, text, width=0):
|
||||
self.wr(text.ljust(width), self.highstyle, extend_to=width)
|
||||
|
||||
def menu(self, text, width=0):
|
||||
self.wr(text, self.menustyle, extend_to=width)
|
||||
|
||||
def button(self, text, width=0):
|
||||
self.wr(text, self.buttonstyle, extend_to=width)
|
||||
|
||||
def error(self, text, width=0):
|
||||
self.wr(f'{self.errorflag}{text}', self.errorstyle, extend_to=width)
|
||||
|
||||
def write_raw(self, row, text, *attr):
|
||||
self.col += len(text)
|
||||
|
||||
def wr(self, text, *attr, extend_to=0):
|
||||
"""write text on screen
|
||||
|
||||
:param text: the text
|
||||
:param attr: attributes
|
||||
:param extend_to: extend_to >= 0: fill up to given value
|
||||
extend_to < 0: extend to right margin + extend_to
|
||||
"""
|
||||
if self.width:
|
||||
limit = self.width - self.col
|
||||
if limit <= 0:
|
||||
return
|
||||
if extend_to < 0:
|
||||
limit += extend_to
|
||||
text = text.ljust(limit)
|
||||
else: # elif extend_to >= limit
|
||||
text = text.ljust(extend_to)[:limit]
|
||||
else:
|
||||
text = text.ljust(extend_to)
|
||||
self.write_raw(self.row, text, *attr)
|
||||
|
||||
|
||||
class Writer(BaseWriter):
|
||||
highstyle = curses.A_REVERSE
|
||||
buttonstyle = curses.A_BOLD
|
||||
barstyle = curses.A_REVERSE
|
||||
brightstyle = menustyle = 0
|
||||
errorstyle = 0
|
||||
errorflag = '! '
|
||||
newoffset = None
|
||||
querystyle = curses.A_BOLD
|
||||
warnstyle = curses.A_REVERSE
|
||||
dimstyle = 0
|
||||
pairs = {}
|
||||
colors = {}
|
||||
nextcolor = 16
|
||||
lock = threading.RLock()
|
||||
leftwidth = 8 # minimum left column width
|
||||
|
||||
def __init__(self, stdscr, main):
|
||||
super().__init__()
|
||||
self.main = main
|
||||
self.scr = stdscr
|
||||
self.scr.clear()
|
||||
self.height, self.width = stdscr.getmaxyx()
|
||||
self.popup = None
|
||||
self.top = 0
|
||||
self.bot = self.height
|
||||
self.adjust_offset_to = None
|
||||
self.preferred_window_row = None
|
||||
self.offset = 0
|
||||
|
||||
def set_leftwidth(self, leftwidth):
|
||||
if leftwidth < 1:
|
||||
self.leftwidth = int(self.width * leftwidth + 1)
|
||||
else:
|
||||
self.leftwidth = leftwidth
|
||||
|
||||
def write_raw(self, row, text, *attr):
|
||||
row = self.row - self.offset # screen row
|
||||
if self.top <= row < self.bot and self.col < self.width:
|
||||
col = self.col
|
||||
try:
|
||||
self.scr.addstr(row, self.col, text, *attr)
|
||||
self.col += len(text)
|
||||
except curses.error:
|
||||
self.col += len(text)
|
||||
if self.col < self.width or row < self.height - 1:
|
||||
raise
|
||||
@classmethod
|
||||
def make_color(cls, rgb):
|
||||
if isinstance(rgb, int):
|
||||
# a color number was given
|
||||
return rgb
|
||||
idx = cls.colors.get(rgb)
|
||||
if idx is None:
|
||||
idx = cls.nextcolor
|
||||
cls.nextcolor += 1
|
||||
cls.colors[idx] = rgb
|
||||
curses.init_color(idx, *rgb)
|
||||
return idx
|
||||
|
||||
@classmethod
|
||||
def make_pair(cls, fg, bg):
|
||||
fg = cls.make_color(fg)
|
||||
bg = cls.make_color(bg)
|
||||
pair = cls.pairs.get((fg, bg))
|
||||
if pair is not None:
|
||||
return pair
|
||||
idx = len(cls.pairs) + 1
|
||||
curses.init_pair(idx, fg, bg)
|
||||
cls.pairs[(fg, bg)] = pair = curses.color_pair(idx)
|
||||
return pair
|
||||
|
||||
@classmethod
|
||||
def init_colors(cls, stdscr):
|
||||
curses.start_color()
|
||||
for nr in range(cls.nextcolor):
|
||||
rgb = curses.color_content(nr)
|
||||
cls.colors.setdefault(rgb, nr)
|
||||
black = cls.make_color((0, 0, 0))
|
||||
dim_white = (680, 680, 680)
|
||||
stdscr.bkgd(' ', cls.make_pair(black, dim_white))
|
||||
bright_white = 1000, 1000, 1000
|
||||
cls.menustyle = cls.brightstyle = cls.make_pair(black, bright_white)
|
||||
red = cls.make_color((680, 0, 0))
|
||||
cls.errorstyle = cls.make_pair(red, dim_white)
|
||||
cls.errorflag = ''
|
||||
very_light_blue = (800, 900, 1000)
|
||||
cls.highstyle = cls.make_pair(black, very_light_blue)
|
||||
light_white = (800, 800, 800)
|
||||
cls.buttonstyle = cls.make_pair(black, light_white)
|
||||
light_green = 0, 1000, 0
|
||||
cls.querystyle = cls.make_pair(black, light_green)
|
||||
yellow = 1000, 1000, 0
|
||||
cls.warnstyle = cls.make_pair(black, yellow)
|
||||
grey = 400, 400, 400
|
||||
cls.dimstyle = cls.make_pair(grey, dim_white)
|
||||
|
||||
def edit(self, text, width, pos):
|
||||
"""write text and set cursor at given pos
|
||||
|
||||
also scroll horizontally if cursor would be outside screen
|
||||
"""
|
||||
if pos is not None:
|
||||
maxwidth = self.width - self.col
|
||||
scrollrange = len(text) - maxwidth + 1
|
||||
if scrollrange <= 0 or pos < scrollrange:
|
||||
offset = 0
|
||||
else:
|
||||
offset = min(pos - scrollrange, scrollrange)
|
||||
text = text[offset:]
|
||||
if len(text) - offset < maxwidth:
|
||||
text += ' '
|
||||
self.set_cursor_pos(pos - offset, True)
|
||||
self.bright(text, width)
|
||||
|
||||
def set_cursor_pos(self, pos=0, visible=False):
|
||||
self.cursor_visible = visible
|
||||
self.main.cursor_pos = self.row - self.offset, self.col + pos
|
||||
|
||||
def vline(self, row, col, length, top, bottom, left, right, *attr):
|
||||
"""draw a vertical line
|
||||
|
||||
:param row, col: upper start point
|
||||
:param length: length without clipping
|
||||
:param top, bottom, left, right: clipping
|
||||
:return: <start row or None>, <end row or None> (None is returned, when clipped on this corner)
|
||||
"""
|
||||
beg = None
|
||||
end = None
|
||||
if left <= col < right and row < bottom:
|
||||
end = row + length
|
||||
if end >= bottom:
|
||||
length -= end - bottom
|
||||
end = None
|
||||
beg = row
|
||||
if beg < top:
|
||||
row = top
|
||||
length -= top - beg
|
||||
beg = None
|
||||
if length > 0:
|
||||
self.scr.vline(row, col, curses.ACS_VLINE, length, *attr)
|
||||
return beg, end
|
||||
|
||||
def hline(self, row, col, length, top, bottom, left, right, *attr):
|
||||
"""draw a horizontal line
|
||||
|
||||
:param row, col: left start point
|
||||
:param length: length without clipping
|
||||
:param top, bottom, left, right: clipping
|
||||
:return: <start row or None>, <end row or None> (None is returned, when clipped on this corner)
|
||||
"""
|
||||
beg = None
|
||||
end = None
|
||||
if top <= row < bottom and col < right:
|
||||
end = col + length
|
||||
if end >= right:
|
||||
length -= end - right
|
||||
end = None
|
||||
beg = col
|
||||
if beg < left:
|
||||
col = left
|
||||
length -= left - beg
|
||||
beg = None
|
||||
if length > 0:
|
||||
self.scr.hline(row, col, curses.ACS_HLINE, length, *attr)
|
||||
else:
|
||||
return None, None
|
||||
return beg, end
|
||||
|
||||
def rectangle(self, row, col, height, width, *attr, **clip):
|
||||
"""clipped rectangle"""
|
||||
row = row - self.offset
|
||||
args = (
|
||||
max(0, clip.get('top', 0) - self.offset),
|
||||
min(self.height, clip.get('bottom', self.height + self.offset) - self.offset),
|
||||
max(0, clip.get('left', 0)),
|
||||
min(self.width, clip.get('right', self.width))
|
||||
) + attr
|
||||
self.vline(row, col, height, *args)
|
||||
self.vline(row, col + width, height, *args)
|
||||
left, right = self.hline(row, col, width, *args)
|
||||
if left is not None:
|
||||
self.scr.addch(row, left, curses.ACS_ULCORNER, *attr)
|
||||
if right is not None:
|
||||
self.scr.addch(row, right, curses.ACS_URCORNER, *attr)
|
||||
row += height
|
||||
left, right = self.hline(row, col, width, *args)
|
||||
if left is not None:
|
||||
self.scr.addch(row, left, curses.ACS_LLCORNER, *attr)
|
||||
if right is not None:
|
||||
self.scr.addch(row, right, curses.ACS_LRCORNER, *attr)
|
||||
|
||||
|
||||
class Empty(Widget):
|
||||
default_height = 0
|
||||
|
||||
def handle(self, main):
|
||||
return None
|
||||
|
||||
|
||||
HELP_TEXT = """
|
||||
ctrl-X Context Menu
|
||||
ctrl-Q Quit
|
||||
@@ -1267,8 +915,8 @@ class Main(HasWidgets):
|
||||
log = Widget.log
|
||||
leftwidth = 0.2 # if < 1: a fraction
|
||||
|
||||
def __init__(self, widgets, writercls, headers=(), footers=(), help_file=None):
|
||||
self.writercls = writercls
|
||||
def __init__(self, widgets, headers=(), footers=(), help_file=None):
|
||||
self.screen = Screen()
|
||||
self.focus = 0
|
||||
self.headers = headers
|
||||
self.footers = footers
|
||||
@@ -1290,17 +938,11 @@ class Main(HasWidgets):
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.scr = curses.initscr()
|
||||
try:
|
||||
self.writercls.init_colors(self.scr)
|
||||
except Exception:
|
||||
raise RuntimeError('it seems you have a terminal without colors. for a test, remove this line')
|
||||
pass
|
||||
curses.noecho()
|
||||
curses.raw() # disable ctrl-C interrupt and ctrl-Q/S flow control
|
||||
curses.nonl() # accept ctrl-J
|
||||
self.scr.keypad(True)
|
||||
KEY.init()
|
||||
self.screen.init()
|
||||
if not self.screen.hascolors:
|
||||
self.log.info('it seems you have a terminal without colors.')
|
||||
# TODO: remove this test and try out on a terminal without colors
|
||||
return
|
||||
self.context_menu = [
|
||||
MenuItem('help', KEY.HELP, self.handle_help, KEY.GOTO_MAIN),
|
||||
MenuItem('exit', KEY.QUIT, self.do_quit),
|
||||
@@ -1314,11 +956,7 @@ class Main(HasWidgets):
|
||||
self.finish(e)
|
||||
raise
|
||||
finally:
|
||||
if self.scr:
|
||||
self.scr.keypad(False)
|
||||
curses.echo()
|
||||
curses.nocbreak()
|
||||
curses.endwin()
|
||||
self.screen.finish()
|
||||
for logline in self.log.loglines:
|
||||
print(logline)
|
||||
|
||||
@@ -1338,15 +976,9 @@ class Main(HasWidgets):
|
||||
def get_key(self, timeout=None):
|
||||
while True:
|
||||
self.refresh()
|
||||
if timeout is None:
|
||||
key = self.scr.getch()
|
||||
else:
|
||||
self.scr.refresh()
|
||||
if select([sys.stdin], [], [], timeout)[0]:
|
||||
key = self.scr.getch()
|
||||
else:
|
||||
continue
|
||||
key = KEY.bynumber.get(key, key)
|
||||
key = self.screen.get_key(timeout)
|
||||
if key is None:
|
||||
continue
|
||||
self.status(None)
|
||||
if isinstance(key, str):
|
||||
self.cut_extend = False
|
||||
@@ -1409,8 +1041,8 @@ class Main(HasWidgets):
|
||||
return super().current_row()
|
||||
|
||||
def refresh(self):
|
||||
with self.writercls.lock:
|
||||
wr = self.writercls(self.scr, self)
|
||||
with self.screen.lock:
|
||||
wr = Writer(self.screen, self)
|
||||
wr.set_leftwidth(self.leftwidth)
|
||||
topmargin = self.get_topmargin()
|
||||
botmargin = sum(v.height() for v in self.footers)
|
||||
@@ -1447,11 +1079,7 @@ class Main(HasWidgets):
|
||||
col += self.cursor_pos[1]
|
||||
wr.move(current_row, col)
|
||||
self.popupmenu.draw(wr)
|
||||
if wr.cursor_visible:
|
||||
self.scr.move(*self.cursor_pos)
|
||||
curses.curs_set(1)
|
||||
else:
|
||||
curses.curs_set(0)
|
||||
self.screen.set_cursor(wr.cursor_visible, *self.cursor_pos)
|
||||
|
||||
def status(self, text, warn=None):
|
||||
if self.statusbar:
|
||||
|
||||
Reference in New Issue
Block a user