beta version of frappy-edit
Change-Id: I82b35505207429cddac44d28222e20627b3a90b3
This commit is contained in:
922
frappy/tools/cfgedit.py
Normal file
922
frappy/tools/cfgedit.py
Normal file
@@ -0,0 +1,922 @@
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
from subprocess import Popen, PIPE
|
||||
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.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
|
||||
|
||||
|
||||
KEY.add(
|
||||
TOGGLE_DETAILED='^t',
|
||||
NEXT_VERSION='^n',
|
||||
PREV_VERSION='^b',
|
||||
)
|
||||
|
||||
|
||||
VERSION_SEPARATOR = "\n''''"
|
||||
TIMESTAMP_FMT = '%Y-%m-%d-%H%M%S'
|
||||
# TODO:
|
||||
# - ctrl-K: on previous versions copy and advance to next module
|
||||
# - use also shift-Tab for level up?
|
||||
# - directory to save to
|
||||
# - remove older versions, for which a newer exist already: a restore removes the older one
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Child(tg.Widget):
|
||||
"""child widget of NodeWidget ot ModuleWidget"""
|
||||
parent = TopWidget
|
||||
|
||||
def get_name(self):
|
||||
return None
|
||||
|
||||
def collect(self, cfgdict):
|
||||
pass
|
||||
|
||||
def check_data(self):
|
||||
pass
|
||||
|
||||
def is_valid(self):
|
||||
return True
|
||||
|
||||
|
||||
class HasValue(Child):
|
||||
clsobj = None
|
||||
|
||||
def init_value_widget(self, parent, valobj):
|
||||
self.init_parent(parent)
|
||||
self.valobj = valobj
|
||||
|
||||
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.error = None
|
||||
except Exception as e:
|
||||
self.error = str(e)
|
||||
if main and (valobj.value, valobj.strvalue, valobj.error) != prev:
|
||||
main.touch()
|
||||
return valobj.strvalue
|
||||
|
||||
def check_data(self):
|
||||
self.validate(self.valobj.strvalue)
|
||||
|
||||
def is_valid(self):
|
||||
return self.get_name() and self.valobj.strvalue
|
||||
|
||||
|
||||
class ValueWidget(HasValue, tg.LineEdit):
|
||||
fixedname = None
|
||||
|
||||
def __init__(self, parent, name, valobj, label=None):
|
||||
"""init a value widget
|
||||
|
||||
:param parent: the parent widget
|
||||
:param name: the initial name
|
||||
:param valobj: the object containing value and datatype
|
||||
:param label: None: the name is changeable, else: a label (which might be different from name)
|
||||
"""
|
||||
self.init_value_widget(parent, valobj)
|
||||
if label is not None:
|
||||
labelwidget = tg.TextWidget(label)
|
||||
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:
|
||||
valueedit = TextEdit(valobj.strvalue, self.validate)
|
||||
super().__init__(labelwidget, valueedit)
|
||||
|
||||
def validate_name(self, name, main):
|
||||
widget_dict = self.parent.widget_dict
|
||||
if name.isidentifier():
|
||||
other = widget_dict.get(name)
|
||||
if other and other != self:
|
||||
self.error = f'duplicate name {name!r}'
|
||||
return self.get_name()
|
||||
self.clsobj = None
|
||||
self.error = None
|
||||
widget = widget_dict.pop(self.get_name(), None)
|
||||
if widget:
|
||||
widget_dict[name] = widget
|
||||
else:
|
||||
self.error = f'illegal name {name!r}'
|
||||
return self.get_name()
|
||||
return name
|
||||
|
||||
def get_name(self):
|
||||
if self.fixedname:
|
||||
return self.fixedname
|
||||
return self.labelwidget.value
|
||||
|
||||
def collect(self, as_dict):
|
||||
"""collect data"""
|
||||
name = self.get_name()
|
||||
if name:
|
||||
as_dict[name] = self.valobj
|
||||
|
||||
|
||||
class DocWidget(HasValue, tg.MultiLineEdit):
|
||||
parent_cls = TopWidget
|
||||
|
||||
def __init__(self, parent, name, valobj):
|
||||
self.init_value_widget(parent, valobj)
|
||||
self.valobj = valobj
|
||||
self.name = name
|
||||
super().__init__(name, valobj.strvalue, 'doc: ')
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def collect(self, config):
|
||||
self.valobj.set_value(self.value)
|
||||
config[self.name] = self.valobj
|
||||
|
||||
|
||||
class BaseWidget(TopWidget, tg.Container):
|
||||
"""base for Module or Node"""
|
||||
clsobj = None
|
||||
header = None
|
||||
special_names = 'name', 'cls', 'description'
|
||||
endline_help = 'RET: add module p: add property'
|
||||
|
||||
def init(self, parent):
|
||||
self.widgets = []
|
||||
self.init_parent(parent, EditorMain)
|
||||
self.focus = 0
|
||||
self.widget_dict = {}
|
||||
self.fixed_names = self.get_fixed_names()
|
||||
|
||||
def get_menu(self):
|
||||
main = self.parent
|
||||
if main.version_view:
|
||||
return main.get_menu()
|
||||
return self.context_menu + main.get_menu()
|
||||
|
||||
def get_fixed_names(self):
|
||||
result = {k: k for k in self.special_names}
|
||||
result['name'] = self.header
|
||||
return result
|
||||
|
||||
def add_widget(self, name, valobj, pos=None):
|
||||
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:
|
||||
if pos < 0:
|
||||
pos += len(self.widgets)
|
||||
self.widgets.insert(pos, widget)
|
||||
return widget
|
||||
|
||||
def new_widget(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def insert_module(self, module, after_current=False):
|
||||
main = self.parent
|
||||
main.insert(main.focus + after_current, module)
|
||||
main.set_focus(main.focus + 1)
|
||||
if not after_current:
|
||||
self.set_focus(1)
|
||||
main.advance(-1)
|
||||
# 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('')}
|
||||
self.insert_module(ModuleWidget(self.parent, '', modcfg), after_current)
|
||||
|
||||
def add_iomodule(self, after_current=False):
|
||||
modcfg = {'name': Value(''), 'uri': Value('')}
|
||||
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 ''
|
||||
|
||||
def get_name(self):
|
||||
return self.get_widget_value('name')
|
||||
|
||||
def draw_summary(self, wr, in_focus):
|
||||
raise NotImplementedError
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
main = self.parent
|
||||
wr.set_leftwidth(main.leftwidth)
|
||||
|
||||
if main.detailed:
|
||||
self.draw_widgets(wr, in_focus)
|
||||
else:
|
||||
self.draw_summary(wr, in_focus)
|
||||
|
||||
def collect(self, result):
|
||||
name = self.get_name()
|
||||
if name:
|
||||
result[name] = modcfg = {}
|
||||
for w in self.widgets:
|
||||
w.collect(modcfg)
|
||||
|
||||
|
||||
class ModuleName(Value):
|
||||
def __init__(self, main, name):
|
||||
self.main = main
|
||||
super().__init__(name)
|
||||
|
||||
def validate_from_string(self, value):
|
||||
if not value:
|
||||
self.strvalue = self.value = ''
|
||||
raise ValueError('empty name')
|
||||
if value != self.value and value in self.main.widget_dict:
|
||||
self.strvalue = self.value = ''
|
||||
raise ValueError(f'duplicate name {value!r}')
|
||||
self.value = self.strvalue = value
|
||||
|
||||
|
||||
class ModuleWidget(BaseWidget):
|
||||
header = 'Module'
|
||||
endline_help = 'RET: add module i: add io module p: add parameter or property'
|
||||
|
||||
def __init__(self, parent, name, modulecfg):
|
||||
assert name == modulecfg['name'].value
|
||||
modulecfg['name'] = ModuleName(parent, name)
|
||||
self.init(parent)
|
||||
self.context_menu = [
|
||||
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('cut module', KEY.CUT, parent.cut_module),
|
||||
]
|
||||
|
||||
self.configure_class(modulecfg.get('cls'))
|
||||
|
||||
for name, valobj in modulecfg.items():
|
||||
self.add_widget(name, valobj)
|
||||
self.widgets.append(EndLine(self))
|
||||
|
||||
def configure_class(self, clsvalue):
|
||||
clsvalue.callback = self.update_cls
|
||||
clsvalue.completion = class_completion
|
||||
clsobj = clsvalue.value
|
||||
if clsobj:
|
||||
self.fixed_names.update({k: k for k, v in recommended_prs(clsobj).items() if v})
|
||||
|
||||
def new_widget(self, name='', pos=None):
|
||||
self.add_widget(name, Value('', *get_datatype('', self.clsobj, ''), from_string=True), self.focus)
|
||||
|
||||
def update_widget_dict(self):
|
||||
self.widget_dict = {w.get_name(): w for w in self.widgets}
|
||||
|
||||
def get_name_info(self):
|
||||
return self.clsobj, self.widget_dict
|
||||
|
||||
def handle(self, main):
|
||||
while True:
|
||||
key = super().handle(main) if main.detailed else main.get_key()
|
||||
if key in (KEY.RIGHT, KEY.TAB):
|
||||
main.detailed = True
|
||||
main.status('')
|
||||
main.offset = None # recalculate offset from screen pos
|
||||
else:
|
||||
return key
|
||||
|
||||
def current_row(self):
|
||||
main = self.parent
|
||||
return super().current_row() if main.detailed else 0
|
||||
|
||||
def height(self, to_focus=None):
|
||||
main = self.parent
|
||||
return super().height(to_focus) if main.detailed else 1
|
||||
|
||||
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()
|
||||
|
||||
def update_cls(self, cls):
|
||||
if cls != self.clsobj:
|
||||
self.complete_prs(True)
|
||||
self.clsobj = cls
|
||||
self.check_data()
|
||||
return cls
|
||||
|
||||
def complete_prs(self, only_mandatory=False):
|
||||
if self.clsobj:
|
||||
fixed_names = self.get_fixed_names()
|
||||
names = set(w.get_name() for w in self.widgets)
|
||||
for name, mandatory in recommended_prs(self.clsobj).items():
|
||||
if mandatory:
|
||||
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'
|
||||
self.fixed_names = fixed_names
|
||||
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.update_widget_dict()
|
||||
|
||||
def draw_summary_right(self, wr):
|
||||
half = (wr.width - wr.col) // 2
|
||||
wr.norm(f"{self.get_widget_value('description').ljust(half)} {self.get_widget_value('cls')} ")
|
||||
|
||||
def draw_summary(self, wr, in_focus):
|
||||
wr.startrow()
|
||||
wr.norm(self.header.ljust(7))
|
||||
name = self.get_widget_value('name')
|
||||
if in_focus:
|
||||
wr.set_cursor_pos()
|
||||
wr.bright(name, round(wr.width * 0.2))
|
||||
else:
|
||||
wr.norm(name.ljust(round(wr.width * 0.2)))
|
||||
self.draw_summary_right(wr)
|
||||
|
||||
def collect(self, result):
|
||||
super().collect(result)
|
||||
name = self.get_name()
|
||||
if name:
|
||||
assert result[name].pop('name').value == name
|
||||
|
||||
|
||||
class IOWidget(ModuleWidget):
|
||||
header = 'IO'
|
||||
endline_help = 'RET: add module p: add property'
|
||||
special_names = 'name', 'uri'
|
||||
|
||||
def __init__(self, parent, name, modulecfg):
|
||||
urivalue = modulecfg.get('uri')
|
||||
if urivalue is None:
|
||||
modulecfg['uri'] = Value('uri', stringtype)
|
||||
super().__init__(parent, name, modulecfg)
|
||||
|
||||
def add_widget(self, name, valobj, pos=None):
|
||||
if name != 'cls' and (
|
||||
name != 'description' or valobj.strvalue):
|
||||
super().add_widget(name, valobj)
|
||||
|
||||
def draw_summary_right(self, wr):
|
||||
half = (wr.width - wr.col) // 2
|
||||
ioname = self.get_name()
|
||||
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 collect(self, result):
|
||||
name = self.get_name()
|
||||
if name:
|
||||
super().collect(result)
|
||||
modcfg = result[name]
|
||||
modcfg['cls'] = Value('<auto>', stringtype)
|
||||
modcfg.setdefault('description', Value('', stringtype))
|
||||
|
||||
def configure_class(self, clsvalue):
|
||||
pass
|
||||
|
||||
|
||||
class EndLine(Child):
|
||||
parent_cls = TopWidget
|
||||
|
||||
def __init__(self, parent):
|
||||
self.init_parent(parent)
|
||||
super().__init__()
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
wr.startrow()
|
||||
if in_focus:
|
||||
wr.set_cursor_pos(wr.leftwidth)
|
||||
wr.col = wr.leftwidth
|
||||
wr.high(self.parent.endline_help)
|
||||
|
||||
def collect(self, result):
|
||||
pass
|
||||
|
||||
def check_data(self):
|
||||
pass
|
||||
|
||||
def get_name(self):
|
||||
return None
|
||||
|
||||
def handle(self, main):
|
||||
self.showhelp = False
|
||||
while True:
|
||||
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):
|
||||
return key
|
||||
elif key == 'i':
|
||||
self.parent.add_iomodule(True)
|
||||
elif key == 'p':
|
||||
self.parent.new_widget()
|
||||
return KEY.GOTO_MAIN
|
||||
return None
|
||||
|
||||
|
||||
class NodeName(Value):
|
||||
def __init__(self, main, name):
|
||||
self.main = main
|
||||
super().__init__(name)
|
||||
|
||||
def validate_from_string(self, value):
|
||||
try:
|
||||
self.main.set_node_name(value)
|
||||
except Exception:
|
||||
self.value = self.strvalue = self.main.cfgname
|
||||
raise
|
||||
|
||||
|
||||
class NodeWidget(BaseWidget):
|
||||
header = 'Node'
|
||||
special_names = 'name', 'equipment_id', 'interface', 'title', 'doc'
|
||||
summ_edit = {'name', 'title', 'doc'} # editable widgets in summary
|
||||
|
||||
def __init__(self, parent, name, nodecfg):
|
||||
nodecfg['name'] = NodeName(parent, name)
|
||||
self.init(parent)
|
||||
self.context_menu = [
|
||||
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)
|
||||
self.widgets.append(docwidget)
|
||||
self.widget_dict['doc'] = docwidget
|
||||
else:
|
||||
self.add_widget(name, valobj)
|
||||
self.widgets.append(EndLine(self))
|
||||
|
||||
def new_widget(self, name=''):
|
||||
"""insert new widget at focus pos"""
|
||||
self.add_widget(name, Value('', None, from_string=True), self.focus)
|
||||
|
||||
def get_name(self):
|
||||
return 'node'
|
||||
|
||||
def set_focus(self, focus, step=1):
|
||||
while super().set_focus(focus, step):
|
||||
if self.parent.detailed or self.get_focus_widget().get_name() in self.summ_edit:
|
||||
return True
|
||||
focus = self.focus + step
|
||||
|
||||
def height(self, to_focus=None):
|
||||
main = self.parent
|
||||
if not main.detailed:
|
||||
return super().height(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:
|
||||
height += widget.height()
|
||||
return height
|
||||
|
||||
def draw_summary(self, wr, in_focus):
|
||||
# wr.startrow()
|
||||
# wr.norm('Node ')
|
||||
wr.set_leftwidth(7)
|
||||
focus = self.focus if in_focus else None
|
||||
for nr, widget in enumerate(self.widgets):
|
||||
name = widget.get_name()
|
||||
if name in self.summ_edit:
|
||||
widget.draw(wr, nr == focus)
|
||||
|
||||
|
||||
class SaveDialog(ModalDialog):
|
||||
def __init__(self, filename):
|
||||
self.fileedit = tg.DialogInput(self, 'file', str(filename))
|
||||
self.result = None
|
||||
super().__init__([self.fileedit,
|
||||
PushButton(self, 'save and quit', self.save),
|
||||
PushButton(self, 'quit without saving', self.nosave),
|
||||
PushButton(self, 'cancel', self.cancel)])
|
||||
|
||||
def execute(self, main):
|
||||
self.set_focus(1) # go to save button
|
||||
super().execute(main)
|
||||
if self.fileedit.result:
|
||||
return self.save()
|
||||
if self.result is None:
|
||||
# no button was pressed
|
||||
return self.cancel()
|
||||
return self.result()
|
||||
|
||||
def save(self):
|
||||
self.filename = self.fileedit.get_value()
|
||||
return KEY.QUIT
|
||||
|
||||
def nosave(self):
|
||||
self.filename = None
|
||||
return KEY.QUIT
|
||||
|
||||
def cancel(self):
|
||||
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
|
||||
cut_modules = ()
|
||||
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]
|
||||
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 = {}
|
||||
|
||||
def get_menu(self):
|
||||
self.detailed_menuitem.name = 'summary view' if self.detailed else 'detailed view'
|
||||
menu = self.version_menu if self.version_view else self.main_menu
|
||||
if self.cut_modules and not self.version_view:
|
||||
self.cut_menuitem.name = f'insert {self.describe_buffer()}'
|
||||
menu.append(self.cut_menuitem)
|
||||
return menu + [self.detailed_menuitem] + self.context_menu
|
||||
|
||||
def init_from_content(self, filecontent):
|
||||
widgets = []
|
||||
nodedata, moddata = cfgdata_from_py(self.cfgname, self.cfgpath, filecontent, self.log)
|
||||
widgets.append(NodeWidget(self, self.cfgname, nodedata))
|
||||
for key, modcfg in moddata.items():
|
||||
clsvalue = modcfg.get('cls')
|
||||
if clsvalue:
|
||||
if clsvalue.strvalue == '<auto>':
|
||||
widgets.append(IOWidget(self, key, modcfg))
|
||||
continue
|
||||
else:
|
||||
modcfg['cls'] = Value('', ModuleClass, from_string=True)
|
||||
widgets.append(ModuleWidget(self, key, modcfg))
|
||||
self.widgets = widgets
|
||||
# self.dirty = False
|
||||
|
||||
def toggle_detailed(self):
|
||||
self.detailed = not self.detailed
|
||||
self.offset = None # recalculate offset from screen pos
|
||||
self.status(None)
|
||||
|
||||
def get_key(self):
|
||||
if self.dirty:
|
||||
if not self.version_view:
|
||||
self.save()
|
||||
self.dirty = False
|
||||
while True:
|
||||
if self.completion_widget:
|
||||
key = super().get_key(0.1)
|
||||
if key is None:
|
||||
continue
|
||||
else:
|
||||
key = super().get_key()
|
||||
if self.version_view:
|
||||
if isinstance(key, str) or key in [KEY.DEL]:
|
||||
self.status('', 'can not edit previous version')
|
||||
else:
|
||||
break
|
||||
else:
|
||||
break
|
||||
return key
|
||||
|
||||
def describe_buffer(self,):
|
||||
cm = self.cut_modules
|
||||
if not cm:
|
||||
return ''
|
||||
if len(cm) > 1:
|
||||
sep = ',' if len(cm) == 2 else '..'
|
||||
return f'modules {cm[0].get_name()}{sep}{cm[-1].get_name()}'
|
||||
return f'module {cm[0].get_name()}'
|
||||
|
||||
def cut_module(self):
|
||||
if not self.cut_modules:
|
||||
self.cut_extend = False
|
||||
module = self.get_focus_widget()
|
||||
if not isinstance(module, ModuleWidget):
|
||||
self.status('', warn='can not cut node')
|
||||
return
|
||||
if not self.version_view:
|
||||
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:
|
||||
text = 'copied'
|
||||
if not self.set_focus(self.focus + 1):
|
||||
if self.cut_modules[-1] == module:
|
||||
self.cut_modules.pop()
|
||||
else:
|
||||
text = 'cut'
|
||||
self.status(f'{self.describe_buffer()} {text}')
|
||||
|
||||
def insert_cut(self):
|
||||
if self.cut_modules:
|
||||
self.widgets[self.focus:self.focus] = self.cut_modules
|
||||
self.cut_modules = []
|
||||
|
||||
def set_node_name(self, name, cfgpath=None):
|
||||
if name == self.cfgname:
|
||||
return
|
||||
if not name:
|
||||
raise ValueError(f'{name!r} is not a valid node name')
|
||||
self.write_pidfile(name)
|
||||
self.cfgname = name
|
||||
self.titlebar.mid = name
|
||||
versions_path = self.version_dir / f'{name}.versions'
|
||||
try:
|
||||
sections = versions_path.read_text().split(VERSION_SEPARATOR)
|
||||
assert sections.pop(0) == ''
|
||||
except FileNotFoundError:
|
||||
sections = []
|
||||
self.versions = dict((v.split('\n', 1) for v in sections))
|
||||
self.tmppath = self.version_dir / f'{name}.current'
|
||||
if cfgpath:
|
||||
cfgpaths = [cfgpath]
|
||||
else:
|
||||
try:
|
||||
cfgpaths = [to_config_path(name, self.log)]
|
||||
except ConfigError:
|
||||
cfgpaths = []
|
||||
cfgpaths.append(self.tmppath)
|
||||
for cfgpath in cfgpaths:
|
||||
try:
|
||||
filecontent = cfgpath.read_text()
|
||||
self.cfgpath = cfgpath
|
||||
if cfgpath != self.tmppath:
|
||||
self.titlebar.mid = str(cfgpath)
|
||||
timestamp = time.strftime(TIMESTAMP_FMT, time.localtime(cfgpath.stat().st_mtime))
|
||||
break
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
filecontent = None
|
||||
timestamp = time.strftime(TIMESTAMP_FMT)
|
||||
self.cfgpath = cfgpaths[0]
|
||||
self.filecontent = filecontent
|
||||
self.add_version(filecontent, timestamp)
|
||||
|
||||
def add_version(self, filecontent, timestamp):
|
||||
if self.versions:
|
||||
to_remove = []
|
||||
# remove matching versions
|
||||
for key, content in self.versions.items():
|
||||
if content == filecontent:
|
||||
to_remove.append(key)
|
||||
for key in to_remove:
|
||||
self.versions.pop(key)
|
||||
if filecontent:
|
||||
self.versions[timestamp] = filecontent
|
||||
sep = VERSION_SEPARATOR
|
||||
versions_path = self.version_dir / f'{self.cfgname}.versions'
|
||||
tmpname = versions_path.with_suffix('.tmp')
|
||||
with open(tmpname, 'w') as f:
|
||||
for ts, section in self.versions.items():
|
||||
f.write(sep)
|
||||
f.write(f'{ts}\n')
|
||||
f.write(section)
|
||||
os.rename(tmpname, versions_path)
|
||||
|
||||
def restore_version(self):
|
||||
if not self.version_view:
|
||||
self.status('this is already the current version')
|
||||
return
|
||||
self.popupmenu = menu = tg.ConfirmDialog('restore this version? [N]')
|
||||
if not menu.handle(self):
|
||||
self.status('cancelled restore')
|
||||
return
|
||||
version = list(self.versions)[-self.version_view]
|
||||
self.status(f'restored from {version}')
|
||||
content = self.versions.pop(version)
|
||||
self.add_version(self.filecontent, time.strftime(TIMESTAMP_FMT))
|
||||
self.filecontent = content
|
||||
self.version_view = 0
|
||||
self.titlebar.right = ''
|
||||
self.init_from_content(self.filecontent)
|
||||
self.save()
|
||||
|
||||
def set_version(self):
|
||||
if self.version_view:
|
||||
version = list(self.versions)[-self.version_view]
|
||||
self.titlebar.right = f'version {version}'
|
||||
try:
|
||||
self.init_from_content(self.versions[version])
|
||||
self.status(None)
|
||||
except Exception as e:
|
||||
self.status('', f'bad version: {e}')
|
||||
else:
|
||||
self.init_from_content(self.filecontent)
|
||||
self.titlebar.right = ''
|
||||
|
||||
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
|
||||
else:
|
||||
self.set_version()
|
||||
|
||||
def next_version(self):
|
||||
if self.version_view:
|
||||
self.version_view -= 1
|
||||
self.set_version()
|
||||
else:
|
||||
self.status('this is the current version')
|
||||
|
||||
def current_row(self):
|
||||
return super().current_row() + self.get_topmargin()
|
||||
|
||||
def touch(self):
|
||||
self.dirty = True
|
||||
|
||||
def save(self):
|
||||
cfgdata = {}
|
||||
for widget in self.widgets:
|
||||
widget.collect(cfgdata)
|
||||
if 'node' not in cfgdata:
|
||||
raise ValueError(list(cfgdata), len(self.widgets))
|
||||
content = cfgdata_to_py(**cfgdata)
|
||||
# if self.cfgpath:
|
||||
# self.cfgpath.write_text(config_code)
|
||||
self.tmppath.write_text(content)
|
||||
self.filecontent = content
|
||||
|
||||
def quit(self):
|
||||
self.save()
|
||||
savedialog = SaveDialog(self.cfgpath)
|
||||
if savedialog.execute(self) == KEY.QUIT:
|
||||
filename = savedialog.filename
|
||||
if filename:
|
||||
self.log.info('saved %r to %r', self.cfgname, filename)
|
||||
Path(filename).write_text(self.filecontent)
|
||||
return True
|
||||
return False
|
||||
|
||||
def advance(self, step):
|
||||
done = super().advance(step)
|
||||
if done:
|
||||
self.get_focus_widget().set_focus(None, step)
|
||||
return done
|
||||
|
||||
def write_pidfile(self, name):
|
||||
pidfile = self.version_dir / f'{name}.pid'
|
||||
mypid = os.getpid()
|
||||
for itry in range(15):
|
||||
try:
|
||||
with open(pidfile, 'x') as f:
|
||||
f.write(str(mypid))
|
||||
if self.pidfile and self.pidfile.exists():
|
||||
self.pidfile.unlink()
|
||||
self.pidfile = pidfile
|
||||
return None
|
||||
except FileExistsError:
|
||||
pass
|
||||
try:
|
||||
pid = int(pidfile.read_text())
|
||||
if pid == mypid:
|
||||
if self.pidfile and self.pidfile != pidfile and self.pidfile.exists():
|
||||
self.pidfile.unlink()
|
||||
return None
|
||||
if pid_exists(pid):
|
||||
raise FileExistsError(f'{name} is already edited by process {pid}')
|
||||
pidfile.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
time.sleep(itry * 0.01)
|
||||
raise RuntimeError('pidfile error: too many tries')
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
super().run()
|
||||
except Exception:
|
||||
print(formatExtendedTraceback())
|
||||
finally:
|
||||
if self.pidfile:
|
||||
self.pidfile.unlink()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ['FRAPPY_CONFDIR'] = 'cfg:cfg/main:cfg/stick:cfg/addons'
|
||||
generalConfig.init()
|
||||
EditorMain(sys.argv[1]).run()
|
||||
Reference in New Issue
Block a user