editcurses: fixes and cleanup

Change-Id: I55a1a12a18f16beceaa62881a915701c11c14270
This commit is contained in:
2026-02-25 10:17:11 +01:00
parent 8946c96e1f
commit 1100607e1a
5 changed files with 426 additions and 134 deletions
+2 -1
View File
@@ -30,7 +30,7 @@ repo = Path(__file__).absolute().parents[1]
sys.path.insert(0, str(repo))
from frappy.lib import generalConfig
from frappy.tools.cfgedit import EditorMain
from frappy.editcurses.cfgedit import EditorMain
# merge cfg dirs from env variable and the ones typically used at psi
# use dicts instead of sets, as we want to keep order
@@ -43,3 +43,4 @@ for cfgdir in 'cfg', 'cfg/main', 'cfg/stick', 'cfg/addons':
os.environ['FRAPPY_CONFDIR'] = ':'.join(cfgdirs)
generalConfig.init()
EditorMain(sys.argv[1]).run()
+99 -47
View File
@@ -4,14 +4,18 @@ import time
from subprocess import Popen, PIPE
from pathlib import Path
from psutil import pid_exists
import frappy
from frappy.errors import ConfigError
from frappy.lib import generalConfig
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
from frappy.tools.terminalgui import Main, MenuItem, TextEdit, PushButton, ModalDialog, KEY
from frappy.editcurses.configdata import Value, cfgdata_to_py, cfgdata_from_py, \
get_datatype, site, ModuleClass, stringtype, class_completion, recommended_prs, \
make_value, ModuleNameCompletion, Module
from frappy.io import IOBase
import frappy.editcurses.terminalgui as tg
from frappy.editcurses.terminalgui import Main, MenuItem, TextEdit, PushButton, \
ModalDialog, KEY
KEY.add(
@@ -56,6 +60,13 @@ class StringValue: # TODO: unused?
class TopWidget:
parent_cls = Main
def handle(self, main):
key = super().handle(main)
if key == KEY.LEFT:
main.toggle_detailed()
return None
return key
class Child(tg.Widget):
"""child widget of NodeWidget ot ModuleWidget"""
@@ -80,25 +91,33 @@ class HasValue(Child):
def init_value_widget(self, parent, valobj):
self.init_parent(parent)
self.valobj = valobj
if isinstance(valobj.completion, ModuleNameCompletion):
valobj.completion.get_names = self.get_module_list
def validate(self, strvalue, main=None):
pname = self.get_name()
valobj = self.valobj
prev = valobj.value, valobj.strvalue, valobj.error
try:
if pname != 'cls':
if self.clsobj != self.parent.clsobj:
self.clsobj = self.parent.clsobj
valobj.datatype, valobj.error = get_datatype(
self.get_name(), self.clsobj, valobj.value)
valobj.validate_from_string(strvalue)
self.valobj = make_value(self.get_name(), self.clsobj, valobj.value)
if isinstance(self.valobj.completion, ModuleNameCompletion):
valobj.completion.get_names = self.get_module_list
self.valobj.validate_from_string(strvalue)
self.error = None
except Exception as e:
self.error = str(e)
if main and (valobj.value, valobj.strvalue, valobj.error) != prev:
if main and valobj != self.valobj:
main.touch()
return valobj.strvalue
def get_module_list(self, basecls):
assert isinstance(self.valobj.completion, ModuleNameCompletion)
module = self.parent
main = module.parent
return main.get_module_list(basecls, module.get_name())
def check_data(self):
self.validate(self.valobj.strvalue)
@@ -108,6 +127,7 @@ class HasValue(Child):
class ValueWidget(HasValue, tg.LineEdit):
fixedname = None
error = None
def __init__(self, parent, name, valobj, label=None):
"""init a value widget
@@ -132,7 +152,8 @@ class ValueWidget(HasValue, tg.LineEdit):
def validate_name(self, name, main):
widget_dict = self.parent.widget_dict
if name.isidentifier():
pname, dot, prop = name.partition('.')
if pname.isidentifier() and (prop.isidentifier or dot == ''):
other = widget_dict.get(name)
if other and other != self:
self.error = f'duplicate name {name!r}'
@@ -158,6 +179,15 @@ class ValueWidget(HasValue, tg.LineEdit):
if name:
as_dict[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:
wr.norm(' ')
wr.error(self.error)
class DocWidget(HasValue, tg.MultiLineEdit):
parent_cls = TopWidget
@@ -308,6 +338,11 @@ class ModuleWidget(BaseWidget):
if clsobj:
self.fixed_names.update({k: k for k, v in recommended_prs(clsobj).items() if v})
def get_class(self):
clsvalue = self.widget_dict['cls'].valobj
clsvalue.set_from_string(clsvalue.strvalue)
return self.clsobj or Module
def new_widget(self, name='', pos=None):
self.add_widget(name, Value('', *get_datatype('', self.clsobj, ''), from_string=True), self.focus)
@@ -414,6 +449,9 @@ class IOWidget(ModuleWidget):
modules = [w.get_name() for w in self.parent.widgets if w.get_widget_value('io') == ioname]
wr.norm(f"{self.get_widget_value('uri').ljust(half)} for {','.join(modules)} ")
def get_class(self):
return IOBase
def collect(self, result):
name = self.get_name()
if name:
@@ -455,7 +493,7 @@ class EndLine(Child):
key = main.get_key()
if key in (KEY.RETURN, KEY.ENTER):
self.parent.add_module(True)
elif key in (KEY.UP, KEY.DOWN, KEY.QUIT):
elif key in (KEY.LEFT, KEY.UP, KEY.DOWN, KEY.QUIT):
return key
elif key == 'i':
self.parent.add_iomodule(True)
@@ -609,43 +647,48 @@ class EditorMain(Main):
cut_extend = False
def __init__(self, cfg):
self.titlebar = tg.TitleBar('Frappy Cfg Editor')
super().__init__([], tg.Writer, [self.titlebar], [tg.StatusBar(self)])
# self.select_menu = MenuItem('select module', CUT_KEY)
self.version_menu = [
MenuItem('previous version', KEY.PREV_VERSION, self.prev_version),
MenuItem('next version', KEY.NEXT_VERSION, self.next_version),
MenuItem('restore this version', 'r', self.restore_version),
MenuItem('copy module', KEY.CUT, self.cut_module),
]
self.main_menu = [
MenuItem('show previous version', KEY.PREV_VERSION, self.prev_version),
]
self.detailed_menuitem = MenuItem('toggle detailed', KEY.TOGGLE_DETAILED, self.toggle_detailed)
self.cut_menuitem = MenuItem('insert cut modules', KEY.PASTE, self.insert_cut)
self.version_dir = Path('~/.local/share/frappy_config_editor').expanduser()
self.version_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
self.cfgname = None
self.pidfile = None
self.dirty = False
# cleanup pidfiles
for file in self.version_dir.glob('*.pid'):
pidstr = file.read_text()
if not pid_exists(int(pidstr)):
file.unlink()
cfgpath = Path(cfg)
if cfg != cfgpath.stem: # this is a filename
if cfg.endswith('_cfg.py'):
cfg = cfgpath.name[:-7]
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')
# self.select_menu = MenuItem('select module', CUT_KEY)
self.version_menu = [
MenuItem('previous version', KEY.PREV_VERSION, self.prev_version),
MenuItem('next version', KEY.NEXT_VERSION, self.next_version),
MenuItem('restore this version', 'r', self.restore_version),
MenuItem('copy module', KEY.CUT, self.cut_module),
]
self.main_menu = [
MenuItem('show previous version', KEY.PREV_VERSION, self.prev_version),
]
self.detailed_menuitem = MenuItem('toggle detailed', KEY.TOGGLE_DETAILED, self.toggle_detailed)
self.cut_menuitem = MenuItem('insert cut modules', KEY.PASTE, self.insert_cut)
self.version_dir = Path('~/.local/share/frappy_config_editor').expanduser()
self.version_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
self.cfgname = None
self.pidfile = None
self.dirty = False
# cleanup pidfiles
for file in self.version_dir.glob('*.pid'):
pidstr = file.read_text()
if not pid_exists(int(pidstr)):
file.unlink()
cfgpath = Path(cfg)
if cfg != cfgpath.stem: # this is a filename
if cfg.endswith('_cfg.py'):
cfg = cfgpath.name[:-7]
else:
cfg = self.cfgpath.stem
else:
cfg = self.cfgpath.stem
else:
cfgpath = None
self.filecontent = None
self.set_node_name(cfg, cfgpath)
self.init_from_content(self.filecontent)
self.module_clipboard = {}
self.pr_clipboard = {}
cfgpath = None
self.filecontent = None
self.set_node_name(cfg, cfgpath)
self.modules = {} # dict <module name> of <module class>
self.init_from_content(self.filecontent)
self.module_clipboard = {}
self.pr_clipboard = {}
except Exception:
print(formatExtendedTraceback())
def get_menu(self):
self.detailed_menuitem.name = 'summary view' if self.detailed else 'detailed view'
@@ -671,6 +714,15 @@ class EditorMain(Main):
self.widgets = widgets
# self.dirty = False
def get_module_list(self, basecls, ownname):
"""get module list for Attached.
filter by basecls ad exclude own name
"""
return [w.get_name() for w in self.widgets
if w.get_name() != ownname and isinstance(w, ModuleWidget)
and issubclass(w.get_class(), basecls)]
def toggle_detailed(self):
self.detailed = not self.detailed
self.offset = None # recalculate offset from screen pos
+64 -29
View File
@@ -22,43 +22,39 @@
import re
import frappy
import inspect
import socket
from pathlib import Path
from ast import literal_eval
from importlib import import_module
from frappy.lib.comparestring import compare
from frappy.config import process_file, Node
from frappy.core import Module, Parameter, Property
from frappy.core import Module, Parameter, Property, Attached
from frappy.datatypes import DataType, EnumType
from frappy.properties import Property, UNSET
HEADER = "# please edit this file with frappy edit"
HEADER = "# please edit this file with bin/frappy-edit"
class Site:
domain = 'psi.ch'
frappy_subdir = 'frappy_psi'
base = Path(frappy.__file__).parent.parent
default_interface = 'tcp://10767'
domain = None
frappy_subdir = None
equipment_postfix = 'your.postfix'
def __init__(self, domain='psi.ch', frappy_subdir='frappy_psi', default_interface='tcp://10767'):
self.init(domain, frappy_subdir, default_interface)
# for individual sites
def init(self, domain=None, frappy_subdir=None, default_interface=None):
if domain:
self.domain = domain
if default_interface:
self.default_interface = default_interface
if frappy_subdir:
self.packages = [v.name for v in self.base.glob('frappy_*')]
try: # psi should be first
self.packages.remove(frappy_subdir)
self.packages.insert(0, frappy_subdir)
def __init__(self):
self.packages = [v.name for v in self.base.glob('frappy_*')]
if self.frappy_subdir:
try: # own subdir should be first
self.packages.remove(self.frappy_subdir)
self.packages.insert(0, self.frappy_subdir)
except ValueError:
pass
site = Site()
class NonStringType:
"""any type except string"""
def validate(self, value):
@@ -67,7 +63,6 @@ class NonStringType:
def __call__(self, value):
return value
def from_string(self, strvalue):
"""convert from string """
try:
@@ -129,15 +124,26 @@ class Value:
strvalue = None
modulecls = None
datatype = None
default = None
value = None
completion = None
def __init__(self, value, datatype=None, error=None, from_string=False, callback=None):
def __init__(self, value, datatype=None, error=None, pr=None,
from_string=False, callback=None):
if value is None:
raise ValueError(datatype)
self.datatype = datatype
if isinstance(pr, Property):
if pr.value is UNSET:
self.default = None if pr.default is UNSET else pr.default
else:
self.default = pr.value
elif isinstance(pr, Parameter):
self.default = pr.default if pr.value is None else pr.value
if isinstance(datatype, EnumType):
self.completion = NameCompletion([v.name for v in datatype._enum.members])
elif isinstance(pr, Attached):
self.completion = ModuleNameCompletion(pr.basecls)
self.error = error
if callback:
self.callback = callback
@@ -193,6 +199,10 @@ class Value:
pass
return repr(self.strvalue)
def __eq__(self, other):
return (self.value, self.error, self.strvalue
) == (other.value, other.error, other.strvalue)
def __repr__(self):
return f'{type(self).__name__}({self.value!r}, {self.datatype!r})'
@@ -203,7 +213,7 @@ def get_datatype(pname, cls, value):
:param pname: <property name> or <parameter name> or <parametger name>.<property name>
:param cls: a frappy Module class or None
:param value: the given value (needed only in case the datatype can not be determined)
:return:
:return: datatype, error or None, property or parameter or None
"""
param, _, prop = pname.partition('.')
error = None
@@ -215,18 +225,19 @@ def get_datatype(pname, cls, value):
if propobj is None:
error = f'{cls.__module__}.{cls.__qualname__}.{param}.{prop} is not configurable'
else:
return propobj.datatype, None
return propobj.datatype, None, propobj
elif prop:
error = f'{cls.__module__}.{cls.__qualname__}.{param} is not a parameter'
else:
return prop_param.datatype, None
return prop_param.datatype, None, prop_param
# return prop_param.datatype, None, None
except AttributeError:
error = f'{cls.__module__}.{cls.__qualname__} is not a Frappy Module'
except KeyError:
error = f'{cls.__module__}.{cls.__qualname__}.{param} is not configurable'
if isinstance(value, str):
return stringtype, error
return nonstringtype, error
return stringtype, error, None
return nonstringtype, error, None
def make_value(pname, cls, value):
@@ -394,7 +405,7 @@ def fix_equipment_id(name, equipment_id):
"""normalize equipment id"""
if re.match(r'[a-zA-Z0-9_]+(\.[a-zA-Z0-9_]+)*$', equipment_id):
return equipment_id
return f'{name}.{site.domain}'
return f'{name}.{site.equipment_postfix}'
def fix_node_class(cls):
@@ -603,11 +614,35 @@ def class_completion(value):
class NameCompletion:
def __init__(self, names):
self.names = names
self.nameset = set(names)
def __call__(self, value):
if value in self.nameset:
if value in self.names:
return len(value), []
return 0, [value] + list(get_suggested(value, self.names))
class ModuleNameCompletion:
def __init__(self, basecls):
self.basecls = basecls
def get_names(self, basecls):
return []
def __call__(self, value):
names = self.get_names(self.basecls)
if value in names:
return len(value), []
return 0, [value] + list(get_suggested(value, names))
class SitePSI(Site):
domain = '.psi.ch'
frappy_subdir = 'frappy_psi'
equipment_postfix = 'psi.ch'
hostname = socket.getfqdn()
for sitecls in [SitePSI]: # add here other sites
if hostname.endswith(sitecls.domain):
break
site = sitecls()
+111 -57
View File
@@ -100,6 +100,7 @@ class Key(int):
return self.name
# keep this file independent of frappy
def clamp(*args):
return sorted(args)[len(args) // 2]
@@ -303,6 +304,7 @@ class TextEdit(Widget):
self.pos = None # cursor pos or None when not editing
if value is None:
raise ValueError('textedit')
assert isinstance(value, str)
self.value = value
self.col_offset = 0
self.finish_callback = finish_callback
@@ -382,15 +384,6 @@ class TextEdit(Widget):
self.value = self.finish(self.value, main)
else:
self.value = self.prev_value
# if save:
# try:
# self.value = self.validator(self.value)
# self.error = None
# except Exception as e:
# self.error = str(e)
# # main.touch()
# else:
# self.value = self.prev_value
self.pos = None
@@ -415,6 +408,7 @@ class Completion:
class TextEditCompl(TextEdit):
menu = None
exc_info = None
def __init__(self, value, finish_callback, completion):
super().__init__(value, finish_callback)
@@ -425,25 +419,29 @@ class TextEditCompl(TextEdit):
main.popupmenu = None
def get_selection_menu(self, main, value):
self.completion_pos, selection = self.completion(value)
if (self.pos >= self.completion_pos and
self == main.completion_widget): # widget has not changed
if selection:
main.popup_offset = self.completion_pos - self.pos
main.popupmenu = CompletionMenu(selection)
else:
main.popupmenu = None
self.completion_widget = None
self.exc_info = None
try:
self.completion_pos, selection = self.completion(value)
if (self.pos >= self.completion_pos and
self == main.completion_widget): # widget has not changed
if selection:
main.popup_offset = self.completion_pos - self.pos
main.popupmenu = CompletionMenu(selection)
else:
main.popupmenu = None
self.completion_widget = None
except Exception:
self.exc_info = sys.exc_info()
def get_key(self, main):
# if self.completion is None:
# return super().get_key(main)
if self.highlighted:
return super().get_key(main)
main.completion_widget = self
mkthread(self.get_selection_menu, main, self.value)
self.selection_thread = mkthread(self.get_selection_menu, main, self.value)
# self.get_selection_menu(main, self.value)
key = super().get_key(main)
if self.exc_info:
raise self.exc_info[1].with_traceback(self.exc_info[2])
self.menu = menu = main.popupmenu
if not menu or self.highlighted or key != KEY.DOWN or self.pos < self.completion_pos:
self.menu = main.popupmenu = None
@@ -470,8 +468,6 @@ class TextEditCompl(TextEdit):
class TextWidget(Widget):
# minwidth = 16
def __init__(self, text):
self.value = text
@@ -483,8 +479,6 @@ class TextWidget(Widget):
class LineEdit(Widget):
error = None
def __init__(self, labelwidget, valuewidget):
self.labelwidget = labelwidget
self.valuewidget = valuewidget
@@ -503,7 +497,7 @@ class LineEdit(Widget):
if key == KEY.LEFT:
if name is not None:
self.focus = 0
continue
continue
return key
key = self.labelwidget.handle(main)
self.focus = 1
@@ -517,9 +511,6 @@ class LineEdit(Widget):
wr.norm(': ')
wr.col = max(wr.col, wr.left + wr.leftwidth)
self.valuewidget.draw(wr, in_focus and self.focus == 1)
if self.error:
wr.norm(' ')
wr.error(self.error)
class MultiLineEdit(Container):
@@ -564,6 +555,9 @@ class MultiLineEdit(Container):
self.highlighted = True
key = main.get_key()
self.highlighted = False
return self.handle_inner(main, key)
def handle_inner(self, main, key, focus=None):
if key in (KEY.RIGHT, KEY.TAB):
self.set_focus(None, -1)
self.next_key = KEY.RIGHT
@@ -580,6 +574,8 @@ class MultiLineEdit(Container):
elif key == KEY.LEFT:
self.set_focus(None, 0)
self.next_key = KEY.LEFT
elif focus is not None:
self.set_focus(focus, 0)
else:
return key
while True:
@@ -596,6 +592,65 @@ class MultiLineEdit(Container):
return key
class HelpWidget(MultiLineEdit):
winsize = 25
def __init__(self, main):
self.init_parent(main)
if main.help_file:
text = main.help_file.read_text()
else:
text = main.help_text
self.original = text
self.readonly = True
self.readonly_menu = [
MenuItem('edit help text', KEY.END_LINE),
MenuItem('quit help', '^q'),
]
self.edit_menu = [
MenuItem('quit help', '^q'),
]
super().__init__('', text, '')
def draw(self, wr, in_focus=False):
wr.leftwidth = 0
self.winsize = wr.height
super().draw(wr, True)
def get_menu(self):
if self.readonly:
return self.readonly_menu
return self.cut_menuitems + self.edit_menu
def handle(self, main):
self.focus = 0
main.offset = 0
height = len(self.widgets)
step = self.winsize // 2
if self.readonly:
while True:
key = main.get_key()
if key == KEY.UP:
main.offset = max(0, main.offset - step)
elif key == KEY.DOWN:
main.offset = max(0, main.offset + step)
else:
if key != KEY.END_LINE:
return
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')
class LineOfMultiline(TextEdit):
minwidth = -1
change_high = None
@@ -739,9 +794,6 @@ class DialogInput(Widget):
return self.valuewidget.value
def draw(self, wr, in_focus=False):
# gap = wr.width - self.width() - 2 - wr.left
# if gap > 0:
# self.valuewidget.minwidth = self.valuewidget.width() + gap
wr.startrow()
wr.menu(f'{self.label}: ')
self.valuewidget.draw(wr, in_focus)
@@ -789,10 +841,6 @@ class PushButton(Widget):
if key in (KEY.RETURN, KEY.ENTER, 'y', 'Y'):
self.parent.result = self.arg
return KEY.GOTO_MAIN
# if key == KEY.LEFT:
# return KEY.UP
# if key == KEY.RIGHT:
# return KEY.DOWN
return key
@@ -900,7 +948,6 @@ class ContextMenu(PopUpMenu):
"""
self.menuitems = [MenuItem('')] + menuitems
self.height = len(self.menuitems)
# self.selection = [v.name for v in menuitems]
self.hotkeys = {k: m for m in menuitems for k in m.keys}
def do_hotkey(self, key):
@@ -935,7 +982,7 @@ class ContextMenu(PopUpMenu):
class BaseWriter:
"""base for writer. does nothing else than keeping track of position"""
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = buttonstyle = None
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = buttonstyle = dimstyle = None
errorflag = '! '
def __init__(self):
@@ -955,6 +1002,9 @@ class BaseWriter:
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)
@@ -1011,6 +1061,7 @@ class Writer(BaseWriter):
newoffset = None
querystyle = curses.A_BOLD
warnstyle = curses.A_REVERSE
dimstyle = 0
pairs = {}
colors = {}
nextcolor = 16
@@ -1094,6 +1145,8 @@ class Writer(BaseWriter):
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
@@ -1214,11 +1267,7 @@ class Main(HasWidgets):
log = Widget.log
leftwidth = 0.2 # if < 1: a fraction
def __init__(self, widgets, writercls, headers=(), footers=()):
#KEY.add(
# ESC=27, TAB=9, DEL=127, RETURN=13,
# QUIT='^x', BEG_LINE='^a', END_LINE='^e', MENU='^c', CUT='^k', PASTE='^v', HELP='^g',
# UNHANDLED=-1, GOTO_MAIN=-2, GO_UP=-3)
def __init__(self, widgets, writercls, headers=(), footers=(), help_file=None):
self.writercls = writercls
self.focus = 0
self.headers = headers
@@ -1237,6 +1286,7 @@ class Main(HasWidgets):
self.quit_action = None
self.cut_lines = []
self.cut_extend = False
self.help_file = help_file
def run(self):
try:
@@ -1252,7 +1302,7 @@ class Main(HasWidgets):
self.scr.keypad(True)
KEY.init()
self.context_menu = [
MenuItem('help', KEY.HELP, self.handle_help, None),
MenuItem('help', KEY.HELP, self.handle_help, KEY.GOTO_MAIN),
MenuItem('exit', KEY.QUIT, self.do_quit),
]
while self.quit_action is None or not self.quit_action():
@@ -1332,20 +1382,30 @@ class Main(HasWidgets):
self.cut_extend = False
return key
def handle(self, main):
if self.help_mode:
widgets = self.widgets
try:
help_widget = HelpWidget(self,)
self.widgets = [help_widget]
key = super().handle(main)
return key
finally:
self.widgets = widgets
self.help_mode = False
else:
return super().handle(main)
def handle_help(self):
self.help_mode = True
self.popupmenu = None
key = self.get_key()
self.help_mode = False
return key if key == KEY.QUIT else None
def persistent_status(self):
return 'ctrl-X: context menu/help ctrl-Q: exit'
def current_row(self):
if self.help_mode:
# TODO: scroll help
return 0
return self.widgets[0].current_row()
return super().current_row()
def refresh(self):
@@ -1364,7 +1424,7 @@ class Main(HasWidgets):
bot_limit = max(self.screen_row, int(end_scroll * 0.7))
top_limit = min(self.screen_row, int(end_scroll * 0.2) + 1, bot_limit)
if self.offset is None:
self.offset = current_row - self.screen_row
self.offset = current_row - max(topmargin, self.screen_row)
offset = clamp(self.offset, current_row - top_limit, current_row - bot_limit)
self.offset = clamp(offset, 0, offsetlimit)
self.screen_row = current_row - self.offset
@@ -1380,18 +1440,12 @@ class Main(HasWidgets):
widget.draw(wr)
wr.offset = self.offset
wr.top, wr.bot = topmargin, bot
if self.help_mode:
for line in self.help_text.split('\n'):
wr.startrow()
wr.norm(line)
else:
self.draw_widgets(wr, True)
self.draw_widgets(wr, True)
wr.top, wr.bot = 0, wr.height
if self.popupmenu:
col = self.popup_offset
col += self.cursor_pos[1]
wr.move(current_row, col)
# wr.move(*self.popupmenu.row_col)
self.popupmenu.draw(wr)
if wr.cursor_visible:
self.scr.move(*self.cursor_pos)
+150
View File
@@ -0,0 +1,150 @@
#
# (C) 2005, Rob W. W. Hooft (rob@hooft.net)
#
# Comparison algorithm as implemented by Reinhard Schneider and Chris Sander
# for comparison of protein sequences, but implemented to compare two
# ASCII strings. This can be very useful for command interpreters to account
# for mistyped commands (use the routine "compare(s1, s2)" in here to get
# a score for each possible command, and see if one stands out). The comparison
# makes use of a similarity matrix for letters: in the protein case this is
# normally a chemical functionality similarity, for our case this is a matrix
# based on keys next to each other on a US Qwerty keyboard and on "capital
# letters are similar to their lowercase equivalent"
#
# The algorithm does not cut corners: it is sure to find the absolute best
# match between the two strings.
#
# No attempt has been made to make this efficient in time or memory. Time taken
# and memory used is proportional to the product of the length of the input
# strings. Use for strings longer than 25 characters is entirely for your own
# risk.
#
# Use freely, but please attribute when using.
# from http://starship.python.net/crew/hooft/
# How much does it cost to make a hole in one of the strings?
GAPOPENPENALTY = -0.3
# How much does it cost to elongate a hole in one of the strings?
GAPELONGATIONPENALTY = -0.2
# How much alike (0.0-1.0) are small and capital letters?
CAPITALIZESCORE = 0.8
# How much alike (0.0-1.0) are characters next to each other on a (US) keyboard?
NEXTKEYSCORE = 0.6
comparematrix = {}
def _makekeyboardmap():
# different characters score 0.0, equal characters score 1.0
for i in range(33, 126 + 1):
for j in range(33, 126 + 1):
comparematrix[i, j] = 0.0
comparematrix[i, i] = 1.0
# Capital and small letters are CAPITALIZESCORE alike
capdist = ord('A') - ord('a')
for i in range(ord('a'), ord('z') + 1):
comparematrix[i, i + capdist] = CAPITALIZESCORE
comparematrix[i + capdist, i] = CAPITALIZESCORE
# Keyboard layout, add some score for letters that are close together
line1 = '`1234567890-= '
line2 = ' qwertyuiop[] '
line3 = ' asdfghjkl; '
line4 = ' zxcvbnm,./ '
for i in range(len(line1) - 1):
_keyboardneighbour(line1[i], line1[i + 1])
_keyboardneighbour(line2[i], line2[i + 1])
_keyboardneighbour(line3[i], line3[i + 1])
_keyboardneighbour(line4[i], line4[i + 1])
_keyboardneighbour(line1[i], line2[i])
_keyboardneighbour(line2[i], line3[i])
_keyboardneighbour(line3[i], line4[i])
_keyboardneighbour(line1[i], line2[i + 1])
_keyboardneighbour(line2[i], line3[i + 1])
_keyboardneighbour(line3[i], line4[i + 1])
def _keyboardneighbour(c1, c2):
i1 = ord(c1)
i2 = ord(c2)
if 33 <= i1 <= 126 and 33 <= i2 <= 126:
comparematrix[i1, i2] = NEXTKEYSCORE
comparematrix[i2, i1] = NEXTKEYSCORE
_makekeyboardmap()
def compare(s1, s2):
lh = {}
gapped = {}
l1 = len(s1)
l2 = len(s2)
if s1 == s2:
return l1 + 1
# Top left of the matrix is "before the first character" in both directions
lh[1, 1] = 0.0
gapped[1, 1] = False
# Start with a gap in s1
lh[2, 1] = GAPOPENPENALTY
gapped[2, 1] = True
for ii in range(3, l1 + 2):
lh[ii, 1] = lh[ii - 1, 1] + GAPELONGATIONPENALTY
gapped[ii, 1] = True
# Start with a gap in s2
lh[1, 2] = GAPOPENPENALTY
gapped[1, 2] = True
for jj in range(3, l2 + 2):
lh[1, jj] = lh[1, jj - 1] + GAPELONGATIONPENALTY
gapped[1, jj] = True
# The main algorithm: for each point in the matrix decide what the best
# route so far is, by comparing the diagonal route forward with the
# possibility to open or elongate a gap either way.
for jj in range(1, l2 + 1):
for ii in range(1, l1 + 1):
oc1 = ord(s1[ii - 1])
oc2 = ord(s2[jj - 1])
if 33 <= oc1 <= 126 and 33 <= oc2 <= 126:
ld = comparematrix[oc1, oc2]
elif oc1 == oc2:
ld = 1.0
else:
ld = 0.0
if gapped[ii + 1, jj]:
gph = GAPELONGATIONPENALTY
else:
gph = GAPOPENPENALTY
if gapped[ii, jj + 1]:
gpv = GAPELONGATIONPENALTY
else:
gpv = GAPOPENPENALTY
s = lh[ii, jj] + ld
sh = lh[ii + 1, jj] + gph
sv = lh[ii, jj + 1] + gpv
sd = max(sh, sv)
if s >= sd:
lh[ii + 1, jj + 1] = s
gapped[ii + 1, jj + 1] = False
else:
lh[ii + 1, jj + 1] = sd
gapped[ii + 1, jj + 1] = True
# The highest alignment score is in the bottom right corner of the matrix,
# behind the last character in both strings
return lh[l1 + 1, l2 + 1]
def test():
assert compare('test', 'test') == len('test') + 1.0
assert compare('Test', 'test') == len('test') - 1.0 + CAPITALIZESCORE
assert compare('rest', 'test') == len('test') - 1.0 + NEXTKEYSCORE
assert compare('rest', 'crest') == len('rest') + GAPOPENPENALTY