renamed tools to editcurses

+ fix current row for completion menu

Change-Id: I46424c7b8d22f4b6646851576848c86c995a080e
This commit is contained in:
2026-02-23 16:05:45 +01:00
parent cb14e7f51d
commit 8946c96e1f
6 changed files with 165 additions and 151 deletions
@@ -6,8 +6,8 @@ from pathlib import Path
from psutil import pid_exists
from frappy.errors import ConfigError
from frappy.lib import generalConfig
from frappy.lib import mkthread, formatExtendedTraceback
from frappy.config import process_file, to_config_path
from frappy.lib import formatExtendedTraceback
from frappy.config import to_config_path
from frappy.tools.configdata import Value, cfgdata_to_py, cfgdata_from_py, get_datatype, site, ModuleClass, stringtype
from frappy.tools.completion import class_completion, recommended_prs
import frappy.tools.terminalgui as tg
@@ -290,8 +290,8 @@ class ModuleWidget(BaseWidget):
MenuItem('add parameter/property', 'p', self.new_widget),
MenuItem('add module', 'm', self.add_module),
MenuItem('add io module', 'i', self.add_iomodule),
MenuItem('purge empty prs', 'e', self.purge_prs),
MenuItem('add recommended prs', '+', self.complete_prs),
MenuItem('purge empty items', 'e', self.purge_prs),
MenuItem('add configurable items', '+', self.complete_prs),
MenuItem('cut module', KEY.CUT, parent.cut_module),
]
@@ -333,7 +333,7 @@ class ModuleWidget(BaseWidget):
def height(self, to_focus=None):
main = self.parent
return super().height(to_focus) if main.detailed else 1
return super().height() if main.detailed else 1
def check_data(self):
"""check clsobj is valid and check all params and props"""
@@ -367,7 +367,7 @@ class ModuleWidget(BaseWidget):
self.update_widget_dict()
def purge_prs(self):
self.widgets = [w for w in self.widgets if w.get_name() not in self.fixed_names and w.is_valid()]
self.widgets = [w for w in self.widgets if w.get_name() in self.fixed_names or w.is_valid()]
self.update_widget_dict()
def draw_summary_right(self, wr):
@@ -512,13 +512,11 @@ class NodeWidget(BaseWidget):
return True
focus = self.focus + step
def height(self, to_focus=None):
def focus_row(self, to_focus):
main = self.parent
if not main.detailed:
return super().height(to_focus)
if main.detailed:
return super().focus_row(to_focus)
height = 0
if to_focus is None:
to_focus = len(self.widgets)
for nr, widget in enumerate(self.widgets[:to_focus]):
name = widget.get_name()
if name in self.summ_edit:
@@ -942,7 +940,5 @@ class EditorMain(Main):
if __name__ == "__main__":
# os.environ['FRAPPY_CONFDIR'] = 'cfg:cfg/main:cfg/stick:cfg/addons'
generalConfig.init()
os.environ['FRAPPY_CONFDIR']
EditorMain(sys.argv[1]).run()
@@ -21,12 +21,14 @@
import re
import frappy
import inspect
from pathlib import Path
from ast import literal_eval
from importlib import import_module
from frappy.config import process_file, Node, fix_io_modules
from frappy.core import Module
from frappy.datatypes import DataType
from frappy.lib.comparestring import compare
from frappy.config import process_file, Node
from frappy.core import Module, Parameter, Property
from frappy.datatypes import DataType, EnumType
HEADER = "# please edit this file with frappy edit"
@@ -134,6 +136,8 @@ class Value:
if value is None:
raise ValueError(datatype)
self.datatype = datatype
if isinstance(datatype, EnumType):
self.completion = NameCompletion([v.name for v in datatype._enum.members])
self.error = error
if callback:
self.callback = callback
@@ -477,6 +481,10 @@ def cfgdata_from_py(name, cfgpath, filecontent, logger):
continue
iomodcls = iomodcfg['cls']
modcls = modcfg['cls']
except Exception as e:
logger.info('error %r when checking io for %r', e, modname)
continue
try:
ioclass = f"{modcls}.ioClass"
if ModuleClass.validate(iomodcls) != ModuleClass.validate(ioclass):
continue
@@ -484,16 +492,122 @@ def cfgdata_from_py(name, cfgpath, filecontent, logger):
iomodcfg.pop('description', None)
iodict[ioname] = iomodcfg
except Exception as e:
if ioclass:
iomod, iocls = iomodcls.rsplit('.', 1)
mod, cls = modcls.rsplit('.', 1)
if mod == iomod:
iomodcls = iocls
errors[ioname] = f'{ioname}: missing ioClass={iomodcls} in source code of {modcls}'
else:
logger.info('error %r when checking io for %r', e, modname)
iomod, iocls = iomodcls.rsplit('.', 1)
mod, cls = modcls.rsplit('.', 1)
if mod == iomod:
iomodcls = iocls
errors[ioname] = f'{ioname}: missing ioClass={iomodcls} in source code of {modcls}'
modules = {k: moddata_from_cfgfile(k, **v) for k, v in config.items()}
for error in errors.values():
logger.info(error)
return nodedata_from_cfgfile(name, **nodecfg), modules
SKIP_PROPS = {'implementation', 'features',
'interface_classes', 'slowinterval', 'omit_unchanged_within', 'original_id'}
def recommended_prs(cls):
"""get properties and parameters which are useful in configuration
returns a dict <pname> of <is mandatory (bool)>
"""
if isinstance(cls, str):
checker = ClassChecker(cls)
if checker.error or not checker.clsobj:
return {}
cls = checker.clsobj
result = {}
for pname, pdict in cls.configurables.items():
pr = getattr(cls, pname)
if isinstance(pr, Property):
if pname not in SKIP_PROPS:
result[pname] = pr.mandatory
elif isinstance(pr, Parameter):
if pr.needscfg:
result[pname] = True
elif not pr.readonly or hasattr(cls, f'write_{pname}'):
result[pname] = False
if result.get('uri') is False and result.get('io') is False:
result['io'] = True
return result
class FrappyModule(str):
"""checker for finding subclasses of Module defined in a python module"""
def check(self, cls):
return isinstance(cls, type) and issubclass(cls, Module) and self.startswith(cls.__module__)
def get_suggested(guess, allowed_keys):
"""select and sort values from allowed_keys based on similarity to a given value
:param guess: given value. when empty, the allowed keys are return in the given order
:param allowed_keys: list of values sorted in a given order
:return: a list of values
"""
if len(guess) == 0:
return allowed_keys
low = guess.lower()
if len(guess) == 1:
# return only items starting with a single letter
return [k for k in allowed_keys if k.lower().startswith(low)]
comp = {}
# if there are items containing guess, return them only
result = [k for k in allowed_keys if low in k.lower()]
if result:
return result
# calculate similarity
for key in allowed_keys:
comp[key] = compare(guess, key)
comp = sorted(comp.items(), key=lambda t: t[1], reverse=True)
scorelimit = 2
result = []
for i, (key, score) in enumerate(comp):
if score < scorelimit:
break
if i > 2:
scorelimit = max(2, score - 0.05)
result.append(key)
return result or allowed_keys
def class_completion(value):
"""analyze class path and return an array of suggestions for
the last incomplete element"""
checker = ClassChecker(value)
if not checker.error:
return checker.position, []
if checker.root is None:
sdict = {p: f'{p}.' for p in site.packages}
else:
sdict = {}
if not checker.clsobj:
file = checker.pyfile
if file.name == '__init__.py':
sdict = {p.stem: f'{p.stem}.'
for p in sorted(file.parent.glob('*.py'))
if p.stem != '__init__'}
sdict.update((k, k) for k, v in sorted(inspect.getmembers(
checker.root, FrappyModule(checker.modname).check)))
found = sdict.get(checker.name, None)
if found:
selection = [found]
# selection = [found] + list(sdict.values())
else:
selection = list(get_suggested(checker.name, sdict.values()))
return checker.position, [checker.name] + selection
class NameCompletion:
def __init__(self, names):
self.names = names
self.nameset = set(names)
def __call__(self, value):
if value in self.nameset:
return len(value), []
return 0, [value] + list(get_suggested(value, self.names))
@@ -104,6 +104,17 @@ def clamp(*args):
return sorted(args)[len(args) // 2]
def mkthread(func, *args, **kwds):
t = threading.Thread(
name=f'{func.__module__}:{func.__name__}',
target=func,
args=args,
kwargs=kwds)
t.daemon = True
t.start()
return t
class Logger:
def __init__(self):
# self.parent = self
@@ -150,7 +161,7 @@ class Widget:
"""returns current row"""
return 0
def height(self, to_focus=None):
def height(self):
"""returns current height"""
return self.default_height
@@ -166,13 +177,16 @@ class HasWidgets:
widgets = None # list of subwidgets
def current_row(self):
return self.height(self.focus) + self.get_focus_widget().current_row()
return self.focus_row(self.focus) + self.get_focus_widget().current_row()
def height(self, to_focus=None):
if to_focus is None:
to_focus = len(self.widgets)
def focus_row(self, to_focus):
"""return relative row position of focus widget"""
return sum(w.height() for w in self.widgets[:to_focus])
def height(self):
"""return the summed up height"""
return self.focus_row(len(self.widgets))
def handle(self, main):
try:
while True:
@@ -427,8 +441,8 @@ class TextEditCompl(TextEdit):
if self.highlighted:
return super().get_key(main)
main.completion_widget = self
# mkthread(self.get_selection_menu, main, self.value)
self.get_selection_menu(main, self.value)
mkthread(self.get_selection_menu, main, self.value)
# self.get_selection_menu(main, self.value)
key = super().get_key(main)
self.menu = menu = main.popupmenu
if not menu or self.highlighted or key != KEY.DOWN or self.pos < self.completion_pos:
@@ -660,9 +674,6 @@ class PopUpMenu:
pos = 0
height = 0
def __repr__(self):
return f'PopUp(pos={self.pos}/{len(self.selection)}, selected={self.selection[self.pos]!r})'
def advance(self, step):
pos = self.pos + step
if 0 <= pos < self.height:
@@ -694,16 +705,16 @@ class CompletionMenu(PopUpMenu):
return self.selection[self.pos]
def draw(self, wr):
row = wr.nextrow
row = wr.nextrow # the edit row
wr.left = max(1, wr.left)
col = wr.left - 1
wr.nextrow -= self.pos - 1
height = self.height + 1
wr.nextrow -= self.pos
if self.pos == 0:
# rectangle is open on top
wr.rectangle(row + 1, col, height - 1, self.width + 1, top=row + 2)
wr.rectangle(row - 1, col, height, self.width + 1, top=row + 1)
else:
wr.rectangle(row - self.pos, col, height, self.width + 1)
wr.rectangle(row - self.pos - 1, col, height, self.width + 1)
for i, value in enumerate(self.selection):
wr.startrow()
if i == self.pos:
@@ -924,7 +935,7 @@ class ContextMenu(PopUpMenu):
class BaseWriter:
"""base for writer. does nothing else than keeping track of position"""
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = None
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = buttonstyle = None
errorflag = '! '
def __init__(self):
@@ -1110,7 +1121,7 @@ class Writer(BaseWriter):
"""draw a vertical line
:param row, col: upper start point
:param length: unclipped length
: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)
"""
@@ -1134,7 +1145,7 @@ class Writer(BaseWriter):
"""draw a horizontal line
:param row, col: left start point
:param length: unclipped length
: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)
"""
@@ -1153,7 +1164,6 @@ class Writer(BaseWriter):
if length > 0:
self.scr.hline(row, col, curses.ACS_HLINE, length, *attr)
else:
raise ValueError(length)
return None, None
return beg, end
@@ -1239,7 +1249,7 @@ class Main(HasWidgets):
curses.noecho()
curses.raw() # disable ctrl-C interrupt and ctrl-Q/S flow control
curses.nonl() # accept ctrl-J
self.scr.keypad(1)
self.scr.keypad(True)
KEY.init()
self.context_menu = [
MenuItem('help', KEY.HELP, self.handle_help, None),
@@ -1255,7 +1265,7 @@ class Main(HasWidgets):
raise
finally:
if self.scr:
self.scr.keypad(0)
self.scr.keypad(False)
curses.echo()
curses.nocbreak()
curses.endwin()
@@ -1346,7 +1356,7 @@ class Main(HasWidgets):
botmargin = sum(v.height() for v in self.footers)
bot = wr.height - botmargin
end_scroll = bot - topmargin - 1
end_scroll = bot - topmargin - 1 - wr.height // 5
current_row = self.current_row()
-106
View File
@@ -1,106 +0,0 @@
# *****************************************************************************
#
# 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>
#
# *****************************************************************************
"""helper functions for configuration editor"""
import re
from frappy.config import Node
from frappy.lib import get_class
SITE_TAIL = 'psi.ch'
def repr_param(value=object, **kwds):
if value is object:
if not kwds:
return 'Param()'
items = []
else:
if not kwds:
return value
items = [value]
items.extend(f'{k}={v}' for k, v in kwds.items())
return f"Param({', '.join(items)})"
def repr_module(modcfg, name, cls):
# items = [f'Mod({name}', f'cls={cls}', f'description={repr_param(**description)}'] + [
# f'{k}={repr_param(**v)}' for k, v in kwds.items()] + [')']
description = modcfg.pop('description', '')
items = [f'Mod({name}', cls, repr_param(**description)] + [
f'{k}={repr_param(**v)}' for k, v in modcfg.items()] + [')']
return ',\n '.join(items)
def repr_node(name, description, cls=None, equipment_id='', **kwds):
equipment_id = fix_equipment_id(name, equipment_id)
items = [f'Node({equipment_id!r}', f'description={description}']
add_node_class(cls, kwds)
items.extend(f'{k}={v}' for k, v in kwds.items())
items.append(')')
return ',\n '.join(items)
def fix_equipment_id(name, equipment_id):
if not re.match(r'[a-zA_Z0-9_]+(\.[a-zA_Z0-9_]+)*$', equipment_id):
equipment_id = f'{name}.{SITE_TAIL}'
return equipment_id
def add_node_class(cls, result):
if cls != Node('', '')['cls']:
result['cls'] = cls
def normalize_node(name, equipment_id='', description='', cls=None, interface=None, **kwds):
result = {}
eq = fix_equipment_id(name, equipment_id)
if equipment_id and eq != equipment_id:
result['equipment_id'] = eq
result['description'] = description
result['interface'] = interface or 'tcp://5555'
add_node_class(cls, result)
result.update(kwds)
return result
def convert_modcfg(cfgdict):
"""convert cfgdict
convert parameter properties to individual items <paramname>.<propname>
"""
result = {}
for key, cfgvalue in cfgdict.items():
if isinstance(cfgvalue, dict):
result.update((key, v) if k == 'value' else (f'{key}.{k}', v)
for k, v in cfgvalue.items())
else:
result[key] = cfgvalue
return result
def needed_properties(cls):
if isinstance(cls, str):
cls = get_class(cls)
result = []
for pname, prop in cls.propertyDict.items():
if prop.mandatory and pname not in {'implementation', 'features'}:
result.append(pname)
return result