GUI editor to tame mod_security rules

⌈⌋ ⎇ branch:  modseccfg


Artifact [a4e463c942]

Artifact a4e463c942da518e859cd8e5cf081cf80ba8fa879c99a38d64dff8d0ff0bba04:

  • File modseccfg/editor.py — part of check-in [0e3331e93e] at 2020-11-30 06:37:22 on branch trunk — Use SPDX id in license: (user: mario size: 6026)

# api: modseccfg
# type: function
# category: gui
# title: editor
# description: simple text window to edit *.conf file
# version: 0.3
# license: Apache-2.0
# config:
#    { name: editor, type: str, value: "", description: External editor to use }
#    { name: editor_font, type: list, value: "Mono,12", description: "Font,Size" }
# state: beta
#
# Just a textbox really.


import os, re
from modseccfg import utils, writer, icons
from modseccfg.utils import srvroot, conf
import PySimpleGUI as sg
from tkinter import font


class editor:
    """
    Basic editor window.
    
    If you want to use an external tool, then configure
    `editor` in the settings. Notably this should be a
    GUI tool, else will show up in the terminal and block
    the main UI.
    """

    def __init__(self, fn, readonly=False, register=None):
        self.fn = fn
        self.readonly = readonly
        self.search = self.search_pos = ""
        self.fonts = [f"{f} 12" for f in list(set(re.grep("Mono|Cons|Robo", font.families())))]
        
        # external
        if conf.get("editor"):
            return os.system(conf.editor + " " + srvroot.fn(fn) + " &")

        # internal    
        src = srvroot.read(fn)
        layout = [
            [sg.Menu([
                ["File", ["Save", "Save To", "Close"]],
                ["Edit", ["Undo", "Redo", "---", "Cut", "Copy", "Paste", "---", "Search..."]],
                ["View", ["Font", self.fonts, "Color", ["Default", "Terminal"]]],
                ["Help", ["Info"]]
            ])],
            [
                sg.Pane([
                    sg.Column([[
                        sg.Button("Save", key="save", disabled=readonly), sg.Button("Cancel", key="cancel"),
                        sg.Text("                    Search:"), sg.Input("", key="findstr", size=(15,1), enable_events=True), sg.Button("â–¼", key="s")
                    ]], expand_x=1)
                ], size=(500,35))
            ],
            [
                sg.Multiline(
                    default_text=src, key="src", font=conf.get("editor_font", "Consolas 11"),
                    border_width=5, autoscroll=1, focus=True, size=(175,35)
                )
            ],
            [
                sg.StatusBar(f"1:1  |  r/{'wo'[int(readonly)]}  |  {len(src)} bytes", k="status", size=(50,1))
            ]
        ]

        # prepare widgets
        self.w = sg.Window(layout=layout, size=(930,670), title=f"Edit {srvroot.srv} {fn}", resizable=1, icon=icons.icon)
        w = self.w
        w.read(timeout=1)
        w_src = w["src"]
        w_src.set_vscroll_position(0.99)
        tk_src = w_src.Widget
        tk_src.tag_config("highlight", background="orange")

        # run event loop here, or per global dispatcher
        if not register:
            while self.event(*w.read()) != "EXIT":
                pass
        else:
            register(self.w, self.event)

    # window events
    def event(self, event, data):
        w = self.w
        w_src = w["src"]
        tk_src = w_src.Widget
        
        #print(event,data)
        if event in (sg.WIN_CLOSED, "Cancel", "cancel", "Close", "Exit") or data.get("cancel"):
            w.close()
            return "EXIT"
        elif event in ["Info"]:
            sg.popup(editor.__doc__)
        elif event == "Save To":
            fn = sg.popup_get_file(
                 title="New target filename", message="Pick new targe filename. (Won't overwrite right away. You'lll have to use [Save] later.)",
                save_as=1, default_path=self.fn, default_extension=".conf"
            )
            if fn != self.fn:
                self.fn = fn
                self.readonly = 0
                self.w["status"].update(f"1:1  |  r/w  |  {fn}")
        elif not self.readonly and event in ["Save", "save"] or data.get("save"):
            srvroot.write(self.fn, data["src"])
            if event == "save":
                w.close()
                return "EXIT"
        elif event in self.fonts:
            conf.editor_font = re.findall("^([\w\-\s]+)\s(\d+)$", event)[0]
            w_src.update(font=conf.editor_font)
            utils.cfg_write()
        elif event == "Terminal":
            w_src.update(text_color='white', background_color='black')
        elif event == "Default":
            w_src.update(text_color='black', background_color='white')
        elif event == "Undo":
            tk_src.edit_undo()
        elif event == "Redo":
            tk_src.edit_redo()
        elif event == "Copy":
            w.TKroot.clipboard_clear()
            w.TKroot.clipboard_append( tk_src.get('sel.first','sel.last') )
        elif event == "Paste":
            try: tk_src.insert("insert", w.TKroot.clipboard_get())
            except: pass
        elif event == "findstr" and len(data.get("findstr")):
            tk_src.tag_remove("highlight", "1.0", "end")
            for pos in self.find_all(tk_src, data["findstr"]):
                if pos:
                    tk_src.tag_add("highlight", pos, self.incr_index(pos, len(data["findstr"])))
        elif event in ["Search", "Search...", "s"]:
            if self.search != data["findstr"]:
                self.search = data["findstr"]
                self.search_pos = "1.0"
            self.search_pos = tk_src.search(self.search, self.incr_index(self.search_pos), nocase=1)
            if self.search_pos:
                tk_src.see(self.search_pos)
                tk_src.mark_set("insert", self.search_pos)
                w_src.SetFocus()

    # search+highlight
    def find_all(self, tk, findstr, pos="1.0"):
        ls = []
        while True:
             pos = tk.search(findstr, self.incr_index(pos, 1), nocase=1)
             if pos in ls:
                 break
             else:
                 ls.append(pos)
        return ls

    # Tk text widget index
    def incr_index(self, pos, add=1):
        if not pos or not pos.find("."):
            return "end"
        y,x = pos.split(".")
        x = int(x) + add
        return f"{y}.{x}"