beta version of frappy-edit
Change-Id: I82b35505207429cddac44d28222e20627b3a90b3
This commit is contained in:
@@ -4,31 +4,31 @@ import time
|
||||
from subprocess import Popen, PIPE
|
||||
from pathlib import Path
|
||||
from psutil import pid_exists
|
||||
from frappy.core import Module
|
||||
from frappy.errors import ConfigError
|
||||
from frappy.lib import generalConfig
|
||||
from frappy.lib.comparestring import compare
|
||||
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
|
||||
from frappy.tools.terminalgui import Main, Container, LineEdit, MultiLineEdit, MenuItem, Writer, ContextMenu, \
|
||||
TextWidget, TextEdit, NameEdit, TextEditCompl, Completion, TitleBar, StatusBar, Widget, ConfirmDialog, \
|
||||
K
|
||||
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',
|
||||
)
|
||||
|
||||
K.add(TOGGLE_DETAILED='^t', NEXT_VERSION='^n', PREV_VERSION='^b')
|
||||
|
||||
VERSION_SEPARATOR = "\n''''"
|
||||
TIMESTAMP_FMT = '%Y-%m-%d-%H%M%S'
|
||||
# TODO:
|
||||
# - ctrl-K / ctrlV/U for cutting/pasting module(s) or parameter(2)
|
||||
# * separate buffer for modules and parameters?
|
||||
# * when inserting a parameter which already exists, overwrite the value only
|
||||
# - implement IO modules
|
||||
# - version control: create one file, with a separator (save disk space, no numbering housekeeping needed)
|
||||
# - use Exceptions for ctrl-X and ctrl-C
|
||||
# - 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]
|
||||
@@ -55,7 +55,7 @@ class TopWidget:
|
||||
parent_cls = Main
|
||||
|
||||
|
||||
class Child(Widget):
|
||||
class Child(tg.Widget):
|
||||
"""child widget of NodeWidget ot ModuleWidget"""
|
||||
parent = TopWidget
|
||||
|
||||
@@ -81,23 +81,21 @@ class HasValue(Child):
|
||||
|
||||
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
|
||||
self.log.info('validate %r %r %r %r', self.parent.get_name(), self.get_name(), self.clsobj, self.valobj)
|
||||
self.valobj.datatype, self.valobj.error = get_datatype(
|
||||
self.get_name(), self.clsobj, self.valobj.value)
|
||||
self.log.info('validate %r %r %r %r', self.parent.get_name(), self.get_name(), self.clsobj, self.valobj)
|
||||
self.valobj.validate_from_string(strvalue)
|
||||
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 self.get_name() == 'tolerance':
|
||||
self.log.info('checked %r %r', self.valobj, self.valobj.error)
|
||||
if main:
|
||||
if main and (valobj.value, valobj.strvalue, valobj.error) != prev:
|
||||
main.touch()
|
||||
return strvalue
|
||||
return valobj.strvalue
|
||||
|
||||
def check_data(self):
|
||||
self.validate(self.valobj.strvalue)
|
||||
@@ -106,7 +104,7 @@ class HasValue(Child):
|
||||
return self.get_name() and self.valobj.strvalue
|
||||
|
||||
|
||||
class ValueWidget(HasValue, LineEdit):
|
||||
class ValueWidget(HasValue, tg.LineEdit):
|
||||
fixedname = None
|
||||
|
||||
def __init__(self, parent, name, valobj, label=None):
|
||||
@@ -119,13 +117,13 @@ class ValueWidget(HasValue, LineEdit):
|
||||
"""
|
||||
self.init_value_widget(parent, valobj)
|
||||
if label is not None:
|
||||
labelwidget = TextWidget(label)
|
||||
labelwidget = tg.TextWidget(label)
|
||||
self.fixedname = name
|
||||
else:
|
||||
labelwidget = NameEdit(name, self.validate_name)
|
||||
labelwidget = tg.NameEdit(name, self.validate_name)
|
||||
# self.log.info('value widget %r %r', name, self.fixedname)
|
||||
if valobj.completion:
|
||||
valueedit = TextEditCompl(valobj.strvalue, self.validate, valobj.completion)
|
||||
valueedit = tg.TextEditCompl(valobj.strvalue, self.validate, valobj.completion)
|
||||
else:
|
||||
valueedit = TextEdit(valobj.strvalue, self.validate)
|
||||
super().__init__(labelwidget, valueedit)
|
||||
@@ -159,7 +157,7 @@ class ValueWidget(HasValue, LineEdit):
|
||||
as_dict[name] = self.valobj
|
||||
|
||||
|
||||
class DocWidget(HasValue, MultiLineEdit):
|
||||
class DocWidget(HasValue, tg.MultiLineEdit):
|
||||
parent_cls = TopWidget
|
||||
|
||||
def __init__(self, parent, name, valobj):
|
||||
@@ -176,11 +174,12 @@ class DocWidget(HasValue, MultiLineEdit):
|
||||
config[self.name] = self.valobj
|
||||
|
||||
|
||||
class BaseWidget(TopWidget, Container):
|
||||
class BaseWidget(TopWidget, tg.Container):
|
||||
"""base for Module or Node"""
|
||||
clsobj = None
|
||||
header = 'Module'
|
||||
header = None
|
||||
special_names = 'name', 'cls', 'description'
|
||||
endline_help = 'RET: add module p: add property'
|
||||
|
||||
def init(self, parent):
|
||||
self.widgets = []
|
||||
@@ -216,16 +215,22 @@ class BaseWidget(TopWidget, Container):
|
||||
def new_widget(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def add_module(self, after_current=False):
|
||||
modcfg = {'name': Value(''), 'cls': Value(f'{site.frappy_subdir}.'), 'description': Value('')}
|
||||
def insert_module(self, module, after_current=False):
|
||||
main = self.parent
|
||||
module = ModuleWidget(main, '', modcfg)
|
||||
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(1) # go to cls widget
|
||||
# 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:
|
||||
@@ -248,14 +253,6 @@ class BaseWidget(TopWidget, Container):
|
||||
else:
|
||||
self.draw_summary(wr, in_focus)
|
||||
|
||||
def current_row(self):
|
||||
main = self.parent
|
||||
return super().current_row() if main.detailed else 0
|
||||
|
||||
def height(self):
|
||||
main = self.parent
|
||||
return super().height() if main.detailed else 1
|
||||
|
||||
def collect(self, result):
|
||||
name = self.get_name()
|
||||
if name:
|
||||
@@ -280,6 +277,9 @@ class ModuleName(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)
|
||||
@@ -287,20 +287,24 @@ class ModuleWidget(BaseWidget):
|
||||
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', K.CUT, parent.cut_module),
|
||||
MenuItem('cut module', KEY.CUT, parent.cut_module),
|
||||
]
|
||||
|
||||
clsvalue = modulecfg['cls']
|
||||
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})
|
||||
for name, valobj in modulecfg.items():
|
||||
self.add_widget(name, valobj)
|
||||
self.widgets.append(EndModule(self))
|
||||
|
||||
def new_widget(self, name='', pos=None):
|
||||
self.add_widget(name, Value('', *get_datatype('', self.clsobj, ''), from_string=True), self.focus)
|
||||
@@ -314,13 +318,21 @@ class ModuleWidget(BaseWidget):
|
||||
def handle(self, main):
|
||||
while True:
|
||||
key = super().handle(main) if main.detailed else main.get_key()
|
||||
if key in (K.RIGHT, K.TAB):
|
||||
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')
|
||||
@@ -356,17 +368,20 @@ class ModuleWidget(BaseWidget):
|
||||
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('Module ')
|
||||
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)))
|
||||
half = (wr.width - wr.col) // 2
|
||||
wr.norm(f"{self.get_widget_value('description').ljust(half)} {self.get_widget_value('cls')} ")
|
||||
self.draw_summary_right(wr)
|
||||
|
||||
def collect(self, result):
|
||||
super().collect(result)
|
||||
@@ -375,9 +390,42 @@ class ModuleWidget(BaseWidget):
|
||||
assert result[name].pop('name').value == name
|
||||
|
||||
|
||||
class EndNode(Child):
|
||||
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
|
||||
helptext = 'RET: add module p: add parameter or property'
|
||||
|
||||
def __init__(self, parent):
|
||||
self.init_parent(parent)
|
||||
@@ -388,8 +436,7 @@ class EndNode(Child):
|
||||
if in_focus:
|
||||
wr.set_cursor_pos(wr.leftwidth)
|
||||
wr.col = wr.leftwidth
|
||||
wr.bright(' ')
|
||||
wr.norm(' ' + self.helptext)
|
||||
wr.high(self.parent.endline_help)
|
||||
|
||||
def collect(self, result):
|
||||
pass
|
||||
@@ -404,52 +451,29 @@ class EndNode(Child):
|
||||
self.showhelp = False
|
||||
while True:
|
||||
key = main.get_key()
|
||||
if key in (K.RETURN, K.ENTER):
|
||||
if key in (KEY.RETURN, KEY.ENTER):
|
||||
self.parent.add_module(True)
|
||||
elif key in (K.UP, K.DOWN, K.QUIT):
|
||||
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 K.GOTO_MAIN
|
||||
return KEY.GOTO_MAIN
|
||||
return None
|
||||
|
||||
|
||||
class EndModule(EndNode):
|
||||
parent_cls = ModuleWidget
|
||||
|
||||
|
||||
class IOWidget(ModuleWidget):
|
||||
def __init__(self, parent, name, modulecfg):
|
||||
assert name == modulecfg['name'].value
|
||||
modulecfg['name'] = ModuleName(parent, name)
|
||||
self.init(parent)
|
||||
self.context_menu = [
|
||||
MenuItem('add module', 'm', self.add_module),
|
||||
MenuItem('cut module', K.CUT, parent.cut_module),
|
||||
]
|
||||
urivalue = modulecfg.get('uri')
|
||||
if urivalue is None:
|
||||
urivalue = Value('uri', stringtype)
|
||||
self.add_widget('uri', urivalue)
|
||||
self.widgets.append(EndModule(self))
|
||||
|
||||
|
||||
class EndIO(EndNode):
|
||||
parent_cls = IOWidget
|
||||
helptext = 'RET: add module'
|
||||
|
||||
|
||||
class NodeName(Value):
|
||||
def __init__(self, main, name):
|
||||
self.main = main
|
||||
super().__init__(name)
|
||||
|
||||
def set_from_string(self, value):
|
||||
self.error = self.main.set_node_name(value)
|
||||
if self.error:
|
||||
def validate_from_string(self, value):
|
||||
try:
|
||||
self.main.set_node_name(value)
|
||||
except Exception:
|
||||
self.value = self.strvalue = self.main.cfgname
|
||||
else:
|
||||
self.value = self.strvalue = value
|
||||
raise
|
||||
|
||||
|
||||
class NodeWidget(BaseWidget):
|
||||
@@ -471,7 +495,7 @@ class NodeWidget(BaseWidget):
|
||||
self.widget_dict['doc'] = docwidget
|
||||
else:
|
||||
self.add_widget(name, valobj)
|
||||
self.widgets.append(EndNode(self))
|
||||
self.widgets.append(EndLine(self))
|
||||
|
||||
def new_widget(self, name=''):
|
||||
"""insert new widget at focus pos"""
|
||||
@@ -481,43 +505,23 @@ class NodeWidget(BaseWidget):
|
||||
return 'node'
|
||||
|
||||
def set_focus(self, focus, step=1):
|
||||
self.log.info('node focus %r %r %r', self.focus, focus, step)
|
||||
while super().set_focus(focus, step):
|
||||
self.log.info('node find %r name %r', self.focus, self.get_focus_widget().get_name())
|
||||
if self.parent.detailed or self.get_focus_widget().get_name() in self.summ_edit:
|
||||
self.log.info('found')
|
||||
return True
|
||||
focus = self.focus + step
|
||||
self.log.info('node focus end %r', self.focus)
|
||||
|
||||
def height(self):
|
||||
def height(self, to_focus=None):
|
||||
main = self.parent
|
||||
return sum(w.height() for w in self.widgets if main.detailed or w.get_name() in self.summ_edit)
|
||||
|
||||
|
||||
# def handle(self, main):
|
||||
# if main.detailed:
|
||||
# return super().handle(main)
|
||||
# advance = self.next
|
||||
# key = None
|
||||
# itry = 10
|
||||
# while itry > 0:
|
||||
# widget = self.get_focus_widget()
|
||||
# if widget.get_name() in ('title', 'doc'):
|
||||
# key = widget.handle(main)
|
||||
# itry = 10
|
||||
# # if key in (CTRL_X, K.LEFT):
|
||||
# # return key
|
||||
# if key == K.UP:
|
||||
# advance = self.prev
|
||||
# elif key in (K.DOWN, K.RETURN, K.ENTER):
|
||||
# advance = self.next
|
||||
# else:
|
||||
# return key
|
||||
# if not advance():
|
||||
# return key
|
||||
# itry -= 1
|
||||
# raise ValueError('x')
|
||||
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()
|
||||
@@ -530,9 +534,66 @@ class NodeWidget(BaseWidget):
|
||||
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 = """
|
||||
ctrl-X Exit
|
||||
ctrl-T Toggle view mode (summary <-> detailed)
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
@@ -548,19 +609,20 @@ class EditorMain(Main):
|
||||
cut_extend = False
|
||||
|
||||
def __init__(self, cfg):
|
||||
self.titlebar = TitleBar('Frappy Cfg Editor')
|
||||
super().__init__([], [self.titlebar], [StatusBar(self)])
|
||||
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', K.PREV_VERSION, self.prev_version),
|
||||
MenuItem('next version', K.NEXT_VERSION, self.next_version),
|
||||
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', K.PREV_VERSION, self.prev_version),
|
||||
MenuItem('show previous version', KEY.PREV_VERSION, self.prev_version),
|
||||
]
|
||||
self.detailed_menuitem = MenuItem('toggle detailed', K.TOGGLE_DETAILED, self.toggle_detailed)
|
||||
self.cut_menuitem = MenuItem('insert cut modules', K.PASTE, self.insert_cut)
|
||||
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
|
||||
@@ -580,35 +642,34 @@ class EditorMain(Main):
|
||||
else:
|
||||
cfgpath = None
|
||||
self.filecontent = None
|
||||
error = self.set_node_name(cfg, cfgpath)
|
||||
if error:
|
||||
raise RuntimeError(error)
|
||||
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'
|
||||
cmenu = [self.cut_menuitem] if self.cut_modules else []
|
||||
vmenu = self.version_menu if self.version_view else self.main_menu
|
||||
return cmenu + vmenu + [self.detailed_menuitem] + self.context_menu
|
||||
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):
|
||||
self.log.info('init from content %r', len(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.value == '<auto>':
|
||||
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.log.info('widgets %r', len(self.widgets))
|
||||
self.widgets = widgets
|
||||
# self.dirty = False
|
||||
|
||||
def toggle_detailed(self):
|
||||
self.detailed = not self.detailed
|
||||
@@ -628,7 +689,7 @@ class EditorMain(Main):
|
||||
else:
|
||||
key = super().get_key()
|
||||
if self.version_view:
|
||||
if isinstance(key, str) or key in [K.DEL]:
|
||||
if isinstance(key, str) or key in [KEY.DEL]:
|
||||
self.status('', 'can not edit previous version')
|
||||
else:
|
||||
break
|
||||
@@ -636,6 +697,15 @@ class EditorMain(Main):
|
||||
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
|
||||
@@ -643,13 +713,21 @@ class EditorMain(Main):
|
||||
if not isinstance(module, ModuleWidget):
|
||||
self.status('', warn='can not cut node')
|
||||
return
|
||||
self.widgets[self.focus:self.focus+1] = []
|
||||
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
|
||||
self.status(f'{len(self.cut_modules)} modules buffered')
|
||||
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:
|
||||
@@ -658,20 +736,12 @@ class EditorMain(Main):
|
||||
|
||||
def set_node_name(self, name, cfgpath=None):
|
||||
if name == self.cfgname:
|
||||
return None
|
||||
return
|
||||
if not name:
|
||||
return f'{name!r} is not a valid node name'
|
||||
if self.pidfile:
|
||||
self.pidfile.unlink()
|
||||
self.pidfile = self.version_dir / f'{name}.pid'
|
||||
try:
|
||||
pidstr = self.pidfile.read_text()
|
||||
if pid_exists(int(pidstr)):
|
||||
return f'{name} is already edited by process {pidstr}'
|
||||
# overwrite pidfile from dead process
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
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)
|
||||
@@ -692,6 +762,8 @@ class EditorMain(Main):
|
||||
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:
|
||||
@@ -702,13 +774,15 @@ class EditorMain(Main):
|
||||
self.cfgpath = cfgpaths[0]
|
||||
self.filecontent = filecontent
|
||||
self.add_version(filecontent, timestamp)
|
||||
return None
|
||||
|
||||
def add_version(self, filecontent, timestamp):
|
||||
if self.versions:
|
||||
# remove last version if unchanged
|
||||
key, content = next(reversed(self.versions.items()))
|
||||
if content == filecontent:
|
||||
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
|
||||
@@ -726,8 +800,8 @@ class EditorMain(Main):
|
||||
if not self.version_view:
|
||||
self.status('this is already the current version')
|
||||
return
|
||||
self.popupmenu = menu = ConfirmDialog('restore this version? [N]')
|
||||
if not self.popupmenu.handle(self):
|
||||
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]
|
||||
@@ -782,15 +856,22 @@ class EditorMain(Main):
|
||||
widget.collect(cfgdata)
|
||||
if 'node' not in cfgdata:
|
||||
raise ValueError(list(cfgdata), len(self.widgets))
|
||||
config_code = cfgdata_to_py(**cfgdata)
|
||||
content = cfgdata_to_py(**cfgdata)
|
||||
# if self.cfgpath:
|
||||
# self.cfgpath.write_text(config_code)
|
||||
self.tmppath.write_text(config_code)
|
||||
return config_code
|
||||
self.tmppath.write_text(content)
|
||||
self.filecontent = content
|
||||
|
||||
def finish(self, exc):
|
||||
# TODO: ask where to save tmp file
|
||||
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)
|
||||
@@ -798,14 +879,41 @@ class EditorMain(Main):
|
||||
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:
|
||||
self.pidfile.write_text(str(os.getpid()))
|
||||
super().run(Writer)
|
||||
super().run()
|
||||
except Exception:
|
||||
print(formatExtendedTraceback())
|
||||
finally:
|
||||
self.pidfile.unlink()
|
||||
if self.pidfile:
|
||||
self.pidfile.unlink()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -24,11 +24,14 @@ import frappy
|
||||
from pathlib import Path
|
||||
from ast import literal_eval
|
||||
from importlib import import_module
|
||||
from frappy.config import process_file, Node
|
||||
from frappy.config import process_file, Node, fix_io_modules
|
||||
from frappy.core import Module
|
||||
from frappy.datatypes import DataType
|
||||
|
||||
|
||||
HEADER = "# please edit this file with frappy edit"
|
||||
|
||||
|
||||
class Site:
|
||||
domain = 'psi.ch'
|
||||
frappy_subdir = 'frappy_psi'
|
||||
@@ -167,11 +170,8 @@ class Value:
|
||||
def validate_from_string(self, strvalue):
|
||||
self.strvalue = strvalue
|
||||
self.value = self.callback(self.datatype.from_string(strvalue))
|
||||
if type(self.datatype).__name__ == 'ClassCompletion':
|
||||
raise ValueError(self)
|
||||
|
||||
def set_from_string(self, strvalue):
|
||||
self.strvalue = strvalue
|
||||
try:
|
||||
self.validate_from_string(strvalue)
|
||||
except Exception as e:
|
||||
@@ -190,7 +190,7 @@ class Value:
|
||||
return repr(self.strvalue)
|
||||
|
||||
def __repr__(self):
|
||||
return f'Value({self.value!r}, {self.datatype!r})'
|
||||
return f'{type(self).__name__}({self.value!r}, {self.datatype!r})'
|
||||
|
||||
|
||||
def get_datatype(pname, cls, value):
|
||||
@@ -259,7 +259,6 @@ class ClassChecker:
|
||||
error = None
|
||||
else:
|
||||
error = 'empty element'
|
||||
|
||||
if error:
|
||||
self.name = name
|
||||
self.error = error
|
||||
@@ -300,6 +299,8 @@ class ModuleClass(DataType):
|
||||
checker = ClassChecker(value)
|
||||
if checker.error:
|
||||
raise ValueError(checker.error)
|
||||
if checker.clsobj is None:
|
||||
raise ValueError(value)
|
||||
return checker.clsobj
|
||||
|
||||
@classmethod
|
||||
@@ -343,9 +344,15 @@ def moddata_from_cfgfile(name, cls, **kwds):
|
||||
|
||||
|
||||
def moddata_to_py(name, cls, description, **kwds):
|
||||
if '<' in cls.get_repr():
|
||||
raise ValueError(cls)
|
||||
items = [f'Mod({name!r}', cls.get_repr(), description.get_repr()]
|
||||
if cls.strvalue == '<auto>':
|
||||
uri = kwds.pop('uri')
|
||||
items = [f'IO({name!r}, {uri.strvalue!r}']
|
||||
if description.strvalue:
|
||||
items.append(f'description={description.strvalue!r}')
|
||||
else:
|
||||
if '<' in cls.get_repr():
|
||||
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('.')
|
||||
@@ -364,6 +371,8 @@ def moddata_to_py(name, cls, description, **kwds):
|
||||
# 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)})")
|
||||
if len(items) == 1:
|
||||
return f"{items[0]})"
|
||||
items.append(')')
|
||||
return ',\n '.join(items)
|
||||
|
||||
@@ -403,8 +412,8 @@ def nodedata_from_cfgfile(name, equipment_id='', description='', interface='', c
|
||||
return {k: Value(v) for k, v in props.items()}
|
||||
|
||||
|
||||
def nodedata_to_py(name, equipment_id, title, doc, interface=None, cls=None, **kwds):
|
||||
eq_id = fix_equipment_id(name.value, equipment_id.value)
|
||||
def nodedata_to_py(name, title, doc, equipment_id=None, interface=None, cls=None, **kwds):
|
||||
eq_id = fix_equipment_id(name.value, equipment_id.value if equipment_id else '')
|
||||
intfc = site.default_interface if interface is None else interface.value
|
||||
desc = title.value.strip()
|
||||
doc = doc.value.strip()
|
||||
@@ -425,18 +434,57 @@ def cfgdata_to_py(node, **moddata):
|
||||
"""convert cfgdata to python code
|
||||
|
||||
:param node: dict <key> of <value object>
|
||||
:param cfgdata: dict <module name> of dict <key> of <value object>
|
||||
:param moddata: dict <module name> of dict <key> of <value object>
|
||||
:return: python code
|
||||
"""
|
||||
items = [nodedata_to_py(**node)] + [moddata_to_py(k, **v) for k, v in moddata.items()]
|
||||
items = [HEADER, nodedata_to_py(**node)] + [moddata_to_py(k, **v) for k, v in moddata.items()]
|
||||
return '\n\n'.join(items)
|
||||
|
||||
|
||||
def cfgdata_from_py(name, cfgpath, filecontent, logger):
|
||||
if filecontent:
|
||||
config = process_file(cfgpath, logger, filecontent)
|
||||
iodict = {k: v for k, v in config.items() if v.get('cls') == '<auto>'}
|
||||
nodecfg = config.pop('node', {})
|
||||
nodedesc = nodecfg.get('description', '')
|
||||
if not filecontent.startswith(HEADER) and '\n' not in nodedesc:
|
||||
nodecfg['description'] = f'{nodedesc}\n\nafter conversion with frappy edit'
|
||||
else:
|
||||
config = {}
|
||||
nodecfg = config.pop('node', {})
|
||||
modcfg = {k: moddata_from_cfgfile(k, **v) for k, v in config.items()}
|
||||
return nodedata_from_cfgfile(name, **nodecfg), modcfg
|
||||
iodict = {}
|
||||
nodecfg = {}
|
||||
|
||||
errors = {}
|
||||
for modname, modcfg in config.items():
|
||||
modio = modcfg.get('io')
|
||||
if modio: # convert legacy io Mod cfg to IO
|
||||
ioclass = None
|
||||
try:
|
||||
ioname = modio['value']
|
||||
if ioname in iodict:
|
||||
continue
|
||||
iomodcfg = config[ioname]
|
||||
if set(iomodcfg) - {'uri', 'cls', 'description'}:
|
||||
continue
|
||||
iomodcls = iomodcfg['cls']
|
||||
modcls = modcfg['cls']
|
||||
ioclass = f"{modcls}.ioClass"
|
||||
if ModuleClass.validate(iomodcls) != ModuleClass.validate(ioclass):
|
||||
continue
|
||||
iomodcfg['cls'] = '<auto>'
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
@@ -24,6 +24,60 @@ 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):
|
||||
@@ -33,41 +87,18 @@ class Key(int):
|
||||
key.name = name
|
||||
return key
|
||||
|
||||
def __repr__(self):
|
||||
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
|
||||
|
||||
class Keys:
|
||||
def __init__(self, **kwds):
|
||||
# mapping int -> letter
|
||||
self.bynumber = {k: chr(k) for k in range(32, 127)}
|
||||
self.keys = {}
|
||||
self.add(**kwds)
|
||||
def __repr__(self):
|
||||
"""name by function"""
|
||||
return self.name
|
||||
|
||||
def add(self, **kwds):
|
||||
toadd = [(k, Key(k, v)) for k, v in kwds.items()]
|
||||
self.keys.update(toadd)
|
||||
self.bynumber.update((int(v), v) for _, v in toadd)
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self.keys[name]
|
||||
except KeyError:
|
||||
nr = getattr(curses, f'KEY_{name}', None)
|
||||
if nr is None:
|
||||
raise AttributeError(f'no key K.{name} {self.keys}')
|
||||
self.keys[name] = key = Key(name, nr)
|
||||
return key
|
||||
|
||||
|
||||
|
||||
# unfortunately, we can not use some ctrl keys, as they already have a meaning:
|
||||
# H: backspace, I: tab, M: ret
|
||||
|
||||
K = Keys(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 clamp(*args):
|
||||
return sorted(args)[len(args) // 2]
|
||||
@@ -100,6 +131,7 @@ class Widget:
|
||||
default_height = 1
|
||||
log = Logger()
|
||||
context_menu = None
|
||||
default_width = 21
|
||||
|
||||
def get_menu(self):
|
||||
if self.context_menu:
|
||||
@@ -118,10 +150,13 @@ class Widget:
|
||||
"""returns current row"""
|
||||
return 0
|
||||
|
||||
def height(self):
|
||||
def height(self, to_focus=None):
|
||||
"""returns current height"""
|
||||
return self.default_height
|
||||
|
||||
def width(self):
|
||||
return self.default_width
|
||||
|
||||
# def draw(self, wr, in_focus=False):
|
||||
# raise NotImplementedError
|
||||
|
||||
@@ -131,23 +166,22 @@ class HasWidgets:
|
||||
widgets = None # list of subwidgets
|
||||
|
||||
def current_row(self):
|
||||
height = sum(w.height() for w in self.widgets[:self.focus])
|
||||
return height + self.get_focus_widget().current_row()
|
||||
return self.height(self.focus) + self.get_focus_widget().current_row()
|
||||
|
||||
def height(self):
|
||||
return sum(w.height() for w in self.widgets)
|
||||
def height(self, to_focus=None):
|
||||
if to_focus is None:
|
||||
to_focus = len(self.widgets)
|
||||
return sum(w.height() for w in self.widgets[:to_focus])
|
||||
|
||||
def handle(self, main):
|
||||
try:
|
||||
while True:
|
||||
main.current_widget = self.get_focus_widget()
|
||||
key = main.current_widget.handle(main)
|
||||
# if key in (CTRL_X, K.LEFT):
|
||||
# return key
|
||||
if key == K.UP:
|
||||
if key == KEY.UP:
|
||||
if self.advance(-1):
|
||||
continue
|
||||
elif key in (K.DOWN, K.RETURN, K.ENTER):
|
||||
elif key in (KEY.DOWN, KEY.RETURN, KEY.ENTER):
|
||||
if self.advance(1):
|
||||
continue
|
||||
return key
|
||||
@@ -199,17 +233,20 @@ class Container(HasWidgets, Widget):
|
||||
class TitleBar(Widget):
|
||||
default_height = 1
|
||||
|
||||
def __init__(self, left, right=''):
|
||||
def __init__(self, left, mid='', right=''):
|
||||
self.left = left
|
||||
self.mid = mid
|
||||
self.right = right
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
left, mid, right, wid = self.left, self.mid, self.right, wr.width - 1
|
||||
midleft = (wid - len(mid) + 1) // 2
|
||||
if len(left) < midleft and midleft + len(mid) + len(right) < wid:
|
||||
text = (left.ljust(midleft) + mid).ljust(wid - len(right)) + right
|
||||
else:
|
||||
text = left.ljust(wid - len(right)) + right
|
||||
wr.startrow()
|
||||
text = self.left.ljust(wr.width)
|
||||
wr.bar(text)
|
||||
if self.right:
|
||||
wr.col = wr.width - len(self.right) - 3
|
||||
wr.bar(f' {self.right} ')
|
||||
wr.bar(text + ' ')
|
||||
|
||||
|
||||
class StatusBar(Widget):
|
||||
@@ -233,7 +270,6 @@ class StatusBar(Widget):
|
||||
else:
|
||||
wr.wr(self.text[:wid].rjust(wid) + ' ', wr.barstyle)
|
||||
|
||||
|
||||
def set(self, text, warn=None, query=False):
|
||||
self.text = text
|
||||
self.warn = warn
|
||||
@@ -257,6 +293,9 @@ class TextEdit(Widget):
|
||||
self.col_offset = 0
|
||||
self.finish_callback = finish_callback
|
||||
|
||||
def width(self):
|
||||
return max(self.minwidth, len(self.value))
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
text = self.value[self.col_offset:]
|
||||
if in_focus:
|
||||
@@ -291,22 +330,22 @@ class TextEdit(Widget):
|
||||
try:
|
||||
while True:
|
||||
key = self.get_key(main)
|
||||
if key == K.LEFT:
|
||||
if key == KEY.LEFT:
|
||||
if self.highlighted:
|
||||
return key
|
||||
self.pos = max(0, self.pos - 1)
|
||||
continue
|
||||
if key == K.TAB:
|
||||
if key == KEY.TAB:
|
||||
self.highlighted = not self.highlighted
|
||||
self.pos = 0
|
||||
continue
|
||||
if key == K.BEG_LINE:
|
||||
if key == KEY.BEG_LINE:
|
||||
self.pos = 0
|
||||
elif key == K.END_LINE:
|
||||
elif key == KEY.END_LINE:
|
||||
self.pos = len(self.value)
|
||||
elif key == K.RIGHT:
|
||||
elif key == KEY.RIGHT:
|
||||
self.pos = len(self.value) if self.highlighted else min(len(self.value), self.pos + 1)
|
||||
elif key == K.DEL:
|
||||
elif key == KEY.DEL:
|
||||
if self.highlighted:
|
||||
self.value = ''
|
||||
self.pos = 0
|
||||
@@ -319,9 +358,9 @@ class TextEdit(Widget):
|
||||
self.value = self.value[:self.pos] + key + self.value[self.pos:]
|
||||
self.pos += 1
|
||||
elif key is not None:
|
||||
if key == K.ESC:
|
||||
if key == KEY.ESC:
|
||||
save = False
|
||||
return K.DOWN
|
||||
return KEY.DOWN
|
||||
return key
|
||||
self.highlighted = False
|
||||
finally:
|
||||
@@ -344,8 +383,8 @@ class TextEdit(Widget):
|
||||
class NameEdit(TextEdit):
|
||||
def get_key(self, main):
|
||||
key = super().get_key(main)
|
||||
if key == K.TAB:
|
||||
key = K.ENTER
|
||||
if key == KEY.TAB:
|
||||
key = KEY.ENTER
|
||||
return key
|
||||
|
||||
|
||||
@@ -368,11 +407,6 @@ class TextEditCompl(TextEdit):
|
||||
self.completion_pos = 0
|
||||
self.completion = completion
|
||||
|
||||
def finish(self, value, main):
|
||||
result = super().finish(value, main)
|
||||
self.log.info('finish %r %r', value, result)
|
||||
return result
|
||||
|
||||
def enter(self, main):
|
||||
main.popupmenu = None
|
||||
|
||||
@@ -397,35 +431,39 @@ class TextEditCompl(TextEdit):
|
||||
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 != K.DOWN or self.pos < self.completion_pos:
|
||||
if not menu or self.highlighted or key != KEY.DOWN or self.pos < self.completion_pos:
|
||||
self.menu = main.popupmenu = None
|
||||
return key
|
||||
menu.pos = 1
|
||||
key = menu.get_key(main)
|
||||
# we have left the popup menu with returned key
|
||||
if key == K.UP:
|
||||
if key == KEY.UP:
|
||||
return None
|
||||
if key == K.LEFT:
|
||||
if key == KEY.LEFT:
|
||||
self.pos = self.completion_pos
|
||||
return key
|
||||
value = self.value[:self.completion_pos]
|
||||
if key in (K.TAB, K.RETURN, K.ENTER, K.RIGHT):
|
||||
selected = menu.get_value()
|
||||
if key in (KEY.TAB, KEY.RETURN, KEY.ENTER, KEY.RIGHT):
|
||||
selected = menu.get_value()
|
||||
value += selected
|
||||
if key == K.TAB or (key in (K.ENTER, K.RETURN)
|
||||
if key == KEY.TAB or (key in (KEY.ENTER, KEY.RETURN)
|
||||
and self.completion(value)[0] < len(value)):
|
||||
key = K.RIGHT
|
||||
key = KEY.RIGHT
|
||||
self.value = value
|
||||
self.pos = len(value)
|
||||
self.menu = main.popupmenu = None
|
||||
return key
|
||||
|
||||
|
||||
class TextWidget(Widget):
|
||||
# minwidth = 16
|
||||
|
||||
def __init__(self, text):
|
||||
self.value = text
|
||||
|
||||
def width(self):
|
||||
return len(self.value)
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
wr.norm(self.value)
|
||||
|
||||
@@ -438,6 +476,9 @@ class LineEdit(Widget):
|
||||
self.valuewidget = valuewidget
|
||||
self.focus = 1
|
||||
|
||||
def width(self):
|
||||
return self.labelwidget.width() + 2 + self.valuewidget.width()
|
||||
|
||||
def handle(self, main):
|
||||
name = self.labelwidget.value if isinstance(self.labelwidget, TextEdit) else None
|
||||
if name == '':
|
||||
@@ -445,14 +486,14 @@ class LineEdit(Widget):
|
||||
while True:
|
||||
if self.focus:
|
||||
key = self.valuewidget.handle(main)
|
||||
if key == K.LEFT:
|
||||
if key == KEY.LEFT:
|
||||
if name is not None:
|
||||
self.focus = 0
|
||||
continue
|
||||
return key
|
||||
key = self.labelwidget.handle(main)
|
||||
self.focus = 1
|
||||
if key in (K.ENTER, K.RETURN, K.TAB):
|
||||
if key in (KEY.ENTER, KEY.RETURN, KEY.TAB):
|
||||
key = None
|
||||
return key
|
||||
|
||||
@@ -478,8 +519,10 @@ class MultiLineEdit(Container):
|
||||
self.validator = validator
|
||||
self.highlighted = False
|
||||
self.cut_menuitems = [
|
||||
MenuItem('cut line(s)', K.CUT),
|
||||
MenuItem('paste line(s)', K.PASTE)]
|
||||
MenuItem('cut line(s)', KEY.CUT),
|
||||
MenuItem('paste line(s)', KEY.PASTE),
|
||||
MenuItem('go to end of line', KEY.END_LINE),
|
||||
MenuItem('back to start of line', KEY.BEG_LINE)]
|
||||
|
||||
def get_menu(self):
|
||||
return self.cut_menuitems + self.parent.get_menu()
|
||||
@@ -507,10 +550,10 @@ class MultiLineEdit(Container):
|
||||
self.highlighted = True
|
||||
key = main.get_key()
|
||||
self.highlighted = False
|
||||
if key in (K.RIGHT, K.TAB):
|
||||
if key in (KEY.RIGHT, KEY.TAB):
|
||||
self.set_focus(None, -1)
|
||||
self.next_key = K.RIGHT
|
||||
elif isinstance(key, str) or key == K.DEL:
|
||||
self.next_key = KEY.RIGHT
|
||||
elif isinstance(key, str) or key == KEY.DEL:
|
||||
self.set_focus(None, -1)
|
||||
last = self.get_focus_widget()
|
||||
if self.focus == 0:
|
||||
@@ -520,21 +563,21 @@ class MultiLineEdit(Container):
|
||||
self.widgets.append(LineOfMultiline(self, ''))
|
||||
self.advance(1)
|
||||
self.next_key = key
|
||||
elif key == K.LEFT:
|
||||
elif key == KEY.LEFT:
|
||||
self.set_focus(None, 0)
|
||||
self.next_key = K.LEFT
|
||||
self.next_key = KEY.LEFT
|
||||
else:
|
||||
return key
|
||||
while True:
|
||||
widget = self.get_focus_widget()
|
||||
key = widget.handle(main)
|
||||
if key == K.UP:
|
||||
if key == KEY.UP:
|
||||
if self.advance(-1):
|
||||
continue
|
||||
elif key in (K.DOWN, K.RETURN, K.ENTER):
|
||||
elif key in (KEY.DOWN, KEY.RETURN, KEY.ENTER):
|
||||
if self.advance(1):
|
||||
continue
|
||||
elif key == K.GO_UP:
|
||||
elif key == KEY.GO_UP:
|
||||
continue
|
||||
return key
|
||||
|
||||
@@ -564,12 +607,12 @@ class LineOfMultiline(TextEdit):
|
||||
else:
|
||||
self.parent.next_key = None
|
||||
multiline = self.parent
|
||||
if key == K.RETURN:
|
||||
if key == KEY.RETURN:
|
||||
self.value, nextline = self.value[:self.pos], self.value[self.pos:]
|
||||
multiline.widgets.insert(multiline.focus + 1, LineOfMultiline(multiline, nextline))
|
||||
main.cursor_col = 0
|
||||
return K.DOWN
|
||||
if key == K.DEL:
|
||||
return KEY.DOWN
|
||||
if key == KEY.DEL:
|
||||
if self.pos == 0:
|
||||
if multiline.focus > 0:
|
||||
thisline = self.value
|
||||
@@ -578,21 +621,19 @@ class LineOfMultiline(TextEdit):
|
||||
pos = len(prev.value)
|
||||
prev.value += thisline
|
||||
main.cursor_col = pos
|
||||
return K.UP
|
||||
elif key == K.LEFT:
|
||||
return KEY.UP
|
||||
elif key == KEY.LEFT:
|
||||
if self.pos == 0 and multiline.focus > 0:
|
||||
prev = multiline.widgets[multiline.focus - 1]
|
||||
main.cursor_col = len(prev.value)
|
||||
# self.log.info('LEFT %r prev=%r', main.cursor_col, prev.value)
|
||||
return K.UP
|
||||
elif key == K.RIGHT:
|
||||
return KEY.UP
|
||||
elif key == KEY.RIGHT:
|
||||
if self.pos == len(self.value) and multiline.focus < len(multiline.widgets)-1:
|
||||
# self.log.info('RIGHT %r focus=%r', main.cursor_col, multiline.focus)
|
||||
main.cursor_col = 0
|
||||
return K.DOWN
|
||||
elif key in (K.UP, K.DOWN):
|
||||
return KEY.DOWN
|
||||
elif key in (KEY.UP, KEY.DOWN):
|
||||
return key
|
||||
elif key == K.CUT:
|
||||
elif key == KEY.CUT:
|
||||
if not main.cut_lines:
|
||||
main.cut_extend = False
|
||||
if not main.cut_extend:
|
||||
@@ -602,12 +643,11 @@ class LineOfMultiline(TextEdit):
|
||||
multiline.widgets[i:i+1] = []
|
||||
main.cut_extend = True
|
||||
main.status(f'{len(main.cut_lines)} lines buffered')
|
||||
return K.GO_UP
|
||||
elif key == K.PASTE:
|
||||
return KEY.GO_UP
|
||||
elif key == KEY.PASTE:
|
||||
multiline.widgets[multiline.focus: multiline.focus] = [
|
||||
LineOfMultiline(multiline, v) for v in main.cut_lines]
|
||||
return K.GO_UP
|
||||
# self.log.info('SET COL %r', self.pos)
|
||||
return KEY.GO_UP
|
||||
self.cursor_col = self.pos
|
||||
return key
|
||||
|
||||
@@ -636,9 +676,9 @@ class PopUpMenu:
|
||||
def get_key(self, main):
|
||||
while True:
|
||||
key = main.get_key()
|
||||
if key == K.DOWN:
|
||||
if key == KEY.DOWN:
|
||||
self.advance(1)
|
||||
elif key == K.UP:
|
||||
elif key == KEY.UP:
|
||||
self.advance(-1)
|
||||
else:
|
||||
return key
|
||||
@@ -661,7 +701,7 @@ class CompletionMenu(PopUpMenu):
|
||||
height = self.height + 1
|
||||
if self.pos == 0:
|
||||
# rectangle is open on top
|
||||
wr.rectangle(row - self.pos - 1, col, height, self.width + 1, row + 1)
|
||||
wr.rectangle(row - self.pos - 1, col, height, self.width + 1, top=row + 1)
|
||||
else:
|
||||
wr.rectangle(row - self.pos - 1, col, height, self.width + 1)
|
||||
for i, value in enumerate(self.selection):
|
||||
@@ -673,6 +713,113 @@ class CompletionMenu(PopUpMenu):
|
||||
wr.menu(value.ljust(self.width))
|
||||
|
||||
|
||||
class DialogInput(Widget):
|
||||
"""this is used as part of a modal dialog
|
||||
|
||||
pressing return will leave the modal dialog
|
||||
"""
|
||||
def __init__(self, parent, label, value):
|
||||
self.init_parent(parent, ModalDialog)
|
||||
self.label = label
|
||||
self.valuewidget = TextEdit(value)
|
||||
self.result = None
|
||||
|
||||
def get_value(self):
|
||||
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)
|
||||
|
||||
def width(self):
|
||||
return len(self.label) + 2 + self.valuewidget.width()
|
||||
|
||||
def handle(self, main):
|
||||
prev_cursor = main.cursor_pos
|
||||
try:
|
||||
key = self.valuewidget.handle(main)
|
||||
if key in (KEY.RETURN, KEY.ENTER):
|
||||
self.result = self.valuewidget.value
|
||||
return KEY.GOTO_MAIN
|
||||
return key
|
||||
finally:
|
||||
main.cursor_pos = prev_cursor
|
||||
|
||||
|
||||
class PushButton(Widget):
|
||||
def __init__(self, parent, label, arg, startline=True):
|
||||
super().init_parent(parent, ModalDialog)
|
||||
self.label = label
|
||||
self.arg = arg
|
||||
self.startline = startline
|
||||
|
||||
def width(self):
|
||||
return len(self.label) + (0 if self.startline else 1)
|
||||
|
||||
def height(self):
|
||||
return 1 if self.startline else 0
|
||||
|
||||
def draw(self, wr, in_focus=False):
|
||||
if self.startline:
|
||||
wr.startrow()
|
||||
else:
|
||||
wr.menu(' ')
|
||||
if in_focus:
|
||||
wr.high(self.label)
|
||||
else:
|
||||
wr.button(self.label)
|
||||
|
||||
def handle(self, main):
|
||||
key = main.get_key()
|
||||
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
|
||||
|
||||
|
||||
class ModalDialog(Container):
|
||||
result = None
|
||||
|
||||
def __init__(self, widgets):
|
||||
self.widgets = widgets
|
||||
self.calc_size()
|
||||
|
||||
def calc_size(self):
|
||||
self.height = sum(w.height() for w in self.widgets)
|
||||
|
||||
def execute(self, main):
|
||||
try:
|
||||
main.popupmenu = self
|
||||
while True:
|
||||
key = self.handle(main)
|
||||
if key in (KEY.GOTO_MAIN, KEY.QUIT):
|
||||
return key
|
||||
finally:
|
||||
main.popupmenu = None
|
||||
|
||||
def get_menu(self):
|
||||
return None
|
||||
|
||||
def draw(self, wr, in_focus=True):
|
||||
wr.move(wr.row, 1)
|
||||
self.calc_size()
|
||||
wr.rectangle(wr.row - 1, wr.col - 1, self.height + 1, wr.width - 1, wr.menustyle)
|
||||
prevrow = wr.nextrow
|
||||
for _ in range(self.height):
|
||||
wr.startrow()
|
||||
wr.menu(' ' * (wr.width - 2))
|
||||
wr.nextrow = prevrow
|
||||
self.draw_widgets(wr, in_focus)
|
||||
|
||||
|
||||
class ConfirmDialog(Widget):
|
||||
def __init__(self, query, positive_answers=('Y', 'y')):
|
||||
self.query = query
|
||||
@@ -697,7 +844,7 @@ class ConfirmDialog(Widget):
|
||||
|
||||
|
||||
class MenuItem:
|
||||
def __init__(self, name, shortcut=None, action=None, returnkey=K.GOTO_MAIN):
|
||||
def __init__(self, name, shortcut=None, action=None, returnkey=KEY.GOTO_MAIN):
|
||||
"""a menu item for a context menu
|
||||
|
||||
:param name: the displayed command name
|
||||
@@ -710,7 +857,7 @@ class MenuItem:
|
||||
self.shortcut = shortcut or ''
|
||||
if shortcut:
|
||||
if isinstance(shortcut, Key):
|
||||
self.shortcut = f'^{chr(shortcut+96)}'
|
||||
self.shortcut = shortcut.short()
|
||||
key = shortcut
|
||||
letter = chr(96 + shortcut)
|
||||
self.keys = (shortcut, letter)
|
||||
@@ -724,7 +871,6 @@ class MenuItem:
|
||||
self.returnkey = returnkey
|
||||
else:
|
||||
self.returnkey = key
|
||||
self.width = len(name)
|
||||
|
||||
def do_action(self):
|
||||
if self.action:
|
||||
@@ -743,14 +889,13 @@ class ContextMenu(PopUpMenu):
|
||||
"""
|
||||
self.menuitems = [MenuItem('')] + menuitems
|
||||
self.height = len(self.menuitems)
|
||||
self.width = max(v.width for v in 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):
|
||||
menuitem = self.hotkeys.get(key)
|
||||
if not menuitem:
|
||||
return K.UNHANDLED
|
||||
return KEY.UNHANDLED
|
||||
return menuitem.do_action()
|
||||
|
||||
def do_action(self):
|
||||
@@ -765,20 +910,21 @@ class ContextMenu(PopUpMenu):
|
||||
wr.left = max(1, wr.left)
|
||||
wr.nextrow = row = row + fixrow - toprow
|
||||
col = wr.left - 1
|
||||
width = max(len(v.name) for v in self.menuitems)
|
||||
for i, menuitem in enumerate(self.menuitems):
|
||||
wr.startrow()
|
||||
wr.menu(menuitem.shortcut.ljust(2))
|
||||
wr.norm(' ')
|
||||
if i == self.pos:
|
||||
wr.high(menuitem.name, self.width)
|
||||
wr.high(menuitem.name, width)
|
||||
else:
|
||||
wr.menu(menuitem.name, self.width)
|
||||
wr.rectangle(row, col, height - 1, self.width + 4)
|
||||
wr.menu(menuitem.name, width)
|
||||
wr.rectangle(row, col, height - 1, width + 4)
|
||||
|
||||
|
||||
class BaseWriter:
|
||||
"""base for writer. does nothing else than keeping track of position"""
|
||||
highstyle = editstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = None
|
||||
highstyle = brightstyle = errorstyle = barstyle = menustyle = querystyle = warnstyle = None
|
||||
errorflag = '! '
|
||||
|
||||
def __init__(self):
|
||||
@@ -805,7 +951,7 @@ class BaseWriter:
|
||||
self.bright(text, width)
|
||||
|
||||
def bright(self, text, width=0):
|
||||
self.wr(text.ljust(width), self.editstyle, extend_to=width)
|
||||
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)
|
||||
@@ -813,6 +959,9 @@ class BaseWriter:
|
||||
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)
|
||||
|
||||
@@ -843,9 +992,9 @@ class BaseWriter:
|
||||
|
||||
class Writer(BaseWriter):
|
||||
highstyle = curses.A_REVERSE
|
||||
menustyle = curses.A_BOLD
|
||||
buttonstyle = curses.A_BOLD
|
||||
barstyle = curses.A_REVERSE
|
||||
editstyle = 0
|
||||
brightstyle = menustyle = 0
|
||||
errorstyle = 0
|
||||
errorflag = '! '
|
||||
newoffset = None
|
||||
@@ -922,14 +1071,14 @@ class Writer(BaseWriter):
|
||||
dim_white = (680, 680, 680)
|
||||
stdscr.bkgd(' ', cls.make_pair(black, dim_white))
|
||||
bright_white = 1000, 1000, 1000
|
||||
cls.editstyle = cls.make_pair(black, bright_white)
|
||||
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 = (900, 900, 900)
|
||||
cls.menustyle = cls.make_pair(black, light_white)
|
||||
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
|
||||
@@ -957,7 +1106,7 @@ class Writer(BaseWriter):
|
||||
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):
|
||||
def vline(self, row, col, length, top, bottom, left, right, *attr):
|
||||
"""draw a vertical line
|
||||
|
||||
:param row, col: upper start point
|
||||
@@ -978,10 +1127,10 @@ class Writer(BaseWriter):
|
||||
length -= top - beg
|
||||
beg = None
|
||||
if length > 0:
|
||||
self.scr.vline(row, col, curses.ACS_VLINE, length)
|
||||
self.scr.vline(row, col, curses.ACS_VLINE, length, *attr)
|
||||
return beg, end
|
||||
|
||||
def hline(self, row, col, length, top, bottom, left, right):
|
||||
def hline(self, row, col, length, top, bottom, left, right, *attr):
|
||||
"""draw a horizontal line
|
||||
|
||||
:param row, col: left start point
|
||||
@@ -1002,36 +1151,34 @@ class Writer(BaseWriter):
|
||||
length -= left - beg
|
||||
beg = None
|
||||
if length > 0:
|
||||
self.scr.hline(row, col, curses.ACS_HLINE, length)
|
||||
self.scr.hline(row, col, curses.ACS_HLINE, length, *attr)
|
||||
else:
|
||||
raise ValueError(length)
|
||||
return None, None
|
||||
if end is None and beg is not None:
|
||||
raise ValueError('end None')
|
||||
return beg, end
|
||||
|
||||
def rectangle(self, row, col, height, width, top=None, bottom=None, left=None, right=None):
|
||||
def rectangle(self, row, col, height, width, *attr, **clip):
|
||||
"""clipped rectangle"""
|
||||
row = row - self.offset
|
||||
clip = (
|
||||
max(0, (top or 0) - self.offset),
|
||||
self.height if bottom is None else min(self.height, bottom - self.offset),
|
||||
max(0, (left or 0)),
|
||||
self.width if right is None else min(self.width, right)
|
||||
)
|
||||
self.vline(row, col, height, *clip)
|
||||
self.vline(row, col + width, height, *clip)
|
||||
left, right = self.hline(row, col, width, *clip)
|
||||
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)
|
||||
self.scr.addch(row, left, curses.ACS_ULCORNER, *attr)
|
||||
if right is not None:
|
||||
self.scr.addch(row, right, curses.ACS_URCORNER)
|
||||
self.scr.addch(row, right, curses.ACS_URCORNER, *attr)
|
||||
row += height
|
||||
left, right = self.hline(row, col, width, *clip)
|
||||
left, right = self.hline(row, col, width, *args)
|
||||
if left is not None:
|
||||
self.scr.addch(row, left, curses.ACS_LLCORNER)
|
||||
self.scr.addch(row, left, curses.ACS_LLCORNER, *attr)
|
||||
if right is not None:
|
||||
self.scr.addch(row, right, curses.ACS_LRCORNER)
|
||||
self.scr.addch(row, right, curses.ACS_LRCORNER, *attr)
|
||||
|
||||
|
||||
class Empty(Widget):
|
||||
@@ -1042,20 +1189,27 @@ class Empty(Widget):
|
||||
|
||||
|
||||
HELP_TEXT = """
|
||||
ctrl-X Exit
|
||||
ctrl-C context menu
|
||||
ctrl-X Context Menu
|
||||
ctrl-Q Quit
|
||||
"""
|
||||
|
||||
|
||||
class Main(HasWidgets):
|
||||
parent = None
|
||||
popupmenu = None
|
||||
statusbar = None
|
||||
menubar = None
|
||||
context_menu = None
|
||||
help_text = HELP_TEXT
|
||||
log = Widget.log
|
||||
leftwidth = 0.2 # if < 1: a fraction
|
||||
|
||||
def __init__(self, widgets, /, headers=(), footers=()):
|
||||
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)
|
||||
self.writercls = writercls
|
||||
self.focus = 0
|
||||
self.headers = headers
|
||||
self.footers = footers
|
||||
@@ -1070,16 +1224,50 @@ class Main(HasWidgets):
|
||||
self.cursor_col = 0 # position of cursor in MultiLineEdit
|
||||
self.cursor_pos = (0, 0)
|
||||
self.popup_offset = 0
|
||||
self.quit = False
|
||||
self.quit_action = None
|
||||
self.cut_lines = []
|
||||
self.cut_extend = False
|
||||
self.context_menu = [
|
||||
MenuItem('help', K.HELP, self.handle_help, None),
|
||||
MenuItem('exit', K.QUIT, self.do_quit),
|
||||
]
|
||||
|
||||
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(1)
|
||||
KEY.init()
|
||||
self.context_menu = [
|
||||
MenuItem('help', KEY.HELP, self.handle_help, None),
|
||||
MenuItem('exit', KEY.QUIT, self.do_quit),
|
||||
]
|
||||
while self.quit_action is None or not self.quit_action():
|
||||
key = self.handle(self)
|
||||
if key not in (KEY.GOTO_MAIN, None, KEY.UP, KEY.DOWN, KEY.UNHANDLED):
|
||||
self.status(f'unknown key {key}')
|
||||
self.finish(None)
|
||||
except Exception as e:
|
||||
self.finish(e)
|
||||
raise
|
||||
finally:
|
||||
if self.scr:
|
||||
self.scr.keypad(0)
|
||||
curses.echo()
|
||||
curses.nocbreak()
|
||||
curses.endwin()
|
||||
for logline in self.log.loglines:
|
||||
print(logline)
|
||||
|
||||
def do_quit(self):
|
||||
self.quit = True
|
||||
self.quit_action = self.quit
|
||||
return KEY.GOTO_MAIN
|
||||
|
||||
def quit(self):
|
||||
pass
|
||||
|
||||
def get_topmargin(self):
|
||||
return sum(v.height() for v in self.headers)
|
||||
@@ -1098,46 +1286,40 @@ class Main(HasWidgets):
|
||||
key = self.scr.getch()
|
||||
else:
|
||||
continue
|
||||
key = K.bynumber.get(key, key)
|
||||
key = KEY.bynumber.get(key, key)
|
||||
self.status(None)
|
||||
if isinstance(key, str):
|
||||
self.log.info('key letter %r', key)
|
||||
self.cut_extend = False
|
||||
return key
|
||||
if 0 <= key < 32: # control keys
|
||||
self.log.info('key ctrl %r', key)
|
||||
if key != K.CUT:
|
||||
if key != KEY.CUT:
|
||||
self.cut_extend = False
|
||||
self.log.info('extend False')
|
||||
menu = ContextMenu(self.current_widget.get_menu())
|
||||
if key == K.MENU:
|
||||
try:
|
||||
if self.popupmenu:
|
||||
return key
|
||||
self.popupmenu = menu
|
||||
key = menu.get_key(self)
|
||||
if key == K.MENU:
|
||||
menuitems = self.current_widget.get_menu()
|
||||
if menuitems and not self.popupmenu:
|
||||
menu = ContextMenu(menuitems)
|
||||
if key == KEY.MENU:
|
||||
try:
|
||||
if self.popupmenu:
|
||||
return key
|
||||
self.popupmenu = menu
|
||||
key = menu.get_key(self)
|
||||
if key == KEY.MENU:
|
||||
continue
|
||||
if key in (KEY.TAB, KEY.RETURN, KEY.ENTER):
|
||||
return menu.do_action()
|
||||
key = menu.do_hotkey(key)
|
||||
if key != KEY.UNHANDLED:
|
||||
return key
|
||||
finally:
|
||||
self.popupmenu = None
|
||||
result = menu.do_hotkey(key)
|
||||
if result != KEY.UNHANDLED:
|
||||
if result is None:
|
||||
continue
|
||||
if key in (K.TAB, K.RETURN, K.ENTER):
|
||||
return menu.do_action()
|
||||
key = menu.do_hotkey(key)
|
||||
if key != K.UNHANDLED:
|
||||
self.log.info('got key %r', key)
|
||||
return key
|
||||
self.status(f'can not handle key {key}')
|
||||
raise ValueError(menu.menuitems)
|
||||
finally:
|
||||
self.popupmenu = None
|
||||
result = menu.do_hotkey(key)
|
||||
if result != K.UNHANDLED:
|
||||
if result is None:
|
||||
continue
|
||||
return result
|
||||
else:
|
||||
if key != K.GOTO_MAIN:
|
||||
if self.cut_extend:
|
||||
self.log.info('extend False %r', key)
|
||||
if key != KEY.GOTO_MAIN:
|
||||
self.cut_extend = False
|
||||
self.log.info('key special %r', key)
|
||||
return key
|
||||
|
||||
def handle_help(self):
|
||||
@@ -1145,10 +1327,10 @@ class Main(HasWidgets):
|
||||
self.popupmenu = None
|
||||
key = self.get_key()
|
||||
self.help_mode = False
|
||||
return key if key == K.QUIT else None
|
||||
return key if key == KEY.QUIT else None
|
||||
|
||||
def persistent_status(self):
|
||||
return 'ctrl-C: context menu/get help ctrl-X: exit'
|
||||
return 'ctrl-X: context menu/help ctrl-Q: exit'
|
||||
|
||||
def current_row(self):
|
||||
if self.help_mode:
|
||||
@@ -1216,34 +1398,4 @@ class Main(HasWidgets):
|
||||
self.statusbar.set(f'{text} {st}', warn)
|
||||
|
||||
def finish(self, exc):
|
||||
pass
|
||||
|
||||
def run(self, writercls):
|
||||
try:
|
||||
self.scr = curses.initscr()
|
||||
self.writercls = writercls
|
||||
try:
|
||||
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(1)
|
||||
while not self.quit:
|
||||
key = self.handle(self)
|
||||
if key not in (K.GOTO_MAIN, None, K.UP, K.DOWN):
|
||||
self.status(f'unknown key {key}')
|
||||
self.finish(None)
|
||||
except Exception as e:
|
||||
self.finish(e)
|
||||
raise
|
||||
finally:
|
||||
if self.scr:
|
||||
self.scr.keypad(0)
|
||||
curses.echo()
|
||||
curses.nocbreak()
|
||||
curses.endwin()
|
||||
for logline in self.log.loglines:
|
||||
print(logline)
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user