Simple key-value editor
from textual.app import App, ComposeResult
from textutils.key_value_edit import KeyValueEditScreen


class TestApp(App):

    def on_mount(self):
        self.push_screen(KeyValueEditScreen("KEY", "Value Value Value"),
                         self.done)
        self.screen.styles.background = "blue"

    def done(self, result):
        self.exit(message=f"Done!\nresult: {result}")

app = TestApp()

if __name__ == '__main___':
    app.run()
src/experiments/exp01.py
holdenweb/skve
from textual.containers import Horizontal, Vertical, Center
from textual.widgets import Input, Button
from textual.screen import ModalScreen
from textual.app import App, ComposeResult

class KeyValueEditScreen(ModalScreen):

    DEFAULT_CSS = """\
KeyValueEditScreen {
    border: $boost 100%;
}

#input-row {
    height: 1fr;
}

#input-value {
    width: 7fr;
    height: auto;
    min-height: 5;
    border: red 100%;
}

#input-key {
    width: 2fr;
    border: red 100%;
}

#label-and-buttons {
    width: 100%;
    align-horizontal: center;
    height: auto;
    border: black 100%;
    background: green 100%;
}
"""

    def __init__(self, key, value, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.key = key
        self.value = value
        self.key_field = Input(id="input-key")
        self.value_field = Input(id="input-value")

    def compose(self) -> ComposeResult:
        with Vertical(id="dialog"):
            yield Horizontal(
                self.key_field,
                self.value_field,
                id="input-row"
            )
            yield Center(
                    Horizontal(
                        Button("Save", variant="primary", id="save"),
                        Button("Cancel", variant="error", id="cancel"),
                        id="label-and-buttons"
                    )
            )

    def on_mount(self):
        self.key_field.value = self.key
        self.value_field.value = self.value
        self.refresh()
        print("NEWRECORDSCREEN TREE")
        self.log(self.css_tree)

    def on_button_pressed(self: ModalScreen, event: Button.Pressed) -> None:
        id = event.button.id
        if id == "save":
            retval = (self.query_one("#input-key").value,
                      self.query_one("#input-value").value)
        elif id == "cancel":
            retval = None
        else:
            raise ValueError("Press of unknown button {id!r}")
        self.dismiss(result=retval)
        self.log(self.css_tree)


class TestApp(App):

    def on_mount(self):
        self.push_screen(KeyValueEditScreen("KEY", "Value Value Value"),
                         self.done)
        self.screen.styles.background = "blue"

    def done(self, result):
        self.exit(message=f"Done!\nresult: {result}")

app = TestApp()

if __name__ == '__main__':
    app.run()
src/experiments/exp02.py
holdenweb/skve
from textual.app import App, ComposeResult

from lib import SaveCancel

class SkelApp(App):
    """A Textual app in which to drop test functionality."""

    def compose(self) -> ComposeResult:
        """Create child widgets for the app."""
        yield SaveCancel(self.report)

    def action_toggle_dark(self) -> None:
        """An action to toggle dark mode."""
        self.dark = not self.dark

    def report(self, arg):
        print(f"ARG: {True}")
        self.panic(self.tree)

app = SkelApp()

if __name__ == "__main__":
    app.run()

src/tests/app_skel.py
holdenweb/skve
from textual.app import App, ComposeResult
from textual.validation import Function, Number, ValidationResult, Validator
from textual.widgets import Input, Label, Pretty, Static, Footer, Button
from textual.containers import Container, Horizontal, Vertical, VerticalScroll, Center
import mongoengine
import importlib.resources as ir
import os
import sys

print("INFO: About to import Acronym")
from .models import Acronym
print("INFO: IMPORTED")
print("__NAME__:", __name__)

os.environ["TEXTUAL"] = "debug,devtools"

def load_css(name=__name__):
    module = sys.modules[name]
    m_path = ir.files(module)
    print("MODULE PATH:", m_path)
    with ir.as_file(m_path / f"styles.css") as f_path:
        with open(f_path) as in_file:
            return in_file.read()

class InputApp(App):

    CSS_PATH = "acronyms.css"

    def __init__(self):
        super().__init__()


    def compose(self) -> ComposeResult:
        yield Horizontal(
            Input(
                placeholder="Enter (the beginning of) an acronym...",
                id="in_val,"
                ),
            id="question-box"
        )
        yield Horizontal(
            VerticalScroll(
                id="buttons"
                ),
            VerticalScroll(
                Static(""),
                id="content"
                ),
            id="body"
        )

        yield Center(Label(f"Initial message"), id="message")


    def on_input_changed(self, event: Input.Changed) -> None:
        # Updating the UI to show the reasons why validation failed
        for btn in self.query(Button):
            btn.remove()
        in_string = self.query_one(Input)
        value = in_string.value
        self.me_query = Acronym.objects(acronym__istartswith=value)
        count = self.me_query.count()
        if count == 0:
            self.replace_message("No matches")
        else:
            self.replace_message(f"{self.me_query.count()} acronyms")
            self.btns = []
            if value:
                for acr in self.me_query.order_by("acronym"):
                    self.query_one("#buttons").mount(
                        btn := ExplainButton(acr)
                    )
                    self.btns.append(btn)

    def replace_message(self, msg):
        self.query_one("#message").remove()
        self.mount(Center(Label(msg), id="message"))

class ExplainButton(Button):
    def __init__(self, acronym):
        super().__init__(acronym.acronym)
        self.title = acronym.title
        self.explanation = acronym.explanation
        print("Acronym:", acronym.acronym, self.title, self.explanation)

    def on_button_pressed(self):
        app.query_one("#content").remove()
        app.query_one("#body").mount(
            VerticalScroll(
                Static(f"{self.title}\n\n{self.explanation}"),
                id="content"
            )
        )

app = InputApp()
dbcon = mongoengine.connect("acronyms")

def main(args=sys.argv):
    return app.run()

if __name__ == "__main__":
    main()

src/textutils/acronyms.py
holdenweb/skve
from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Input, Static, Label
from textual.screen import ModalScreen

from textutils.lib import SaveCancel

class KeyValueEditScreen(ModalScreen):

    DEFAULT_CSS = """\
KeyValueEditScreen {
    background: $primary 100%;
}

#input-value {
    width: 7fr;
    height: auto;
    min-height: 1;
    border: yellow 100%;
    background: $primary 100%;
}

#input-key {
    width: 7fr;
    border: yellow 100%;
    background: $primary 100%;
}

.title {
    text-style: bold;
    width: 1fr;
}
"""

    def __init__(self, key, value, editable_key=False, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.key = key
        self.value = value
        self.editable_key = editable_key

        self.key_field = Input(id="input-key") if editable_key else Label(key, id="input-key")
        self.value_field = Input(id="input-value")

    def compose(self) -> ComposeResult:
        with Vertical(id="dialog"):
            yield Horizontal(
                Static("Key", classes="title"),
                self.key_field
            )
            yield Horizontal(
                Static("Value", classes="title"),
                self.value_field
            )
            yield SaveCancel(self.callback)

    def callback(self, save):
        retval = (self.key_field.value, self.value_field.value) if save else None
        self.dismiss(retval)  # Assumes parent screen will capture this result

    def on_mount(self):
        self.key_field.value = self.key
        self.value_field.value = self.value
        self.refresh()

src/textutils/key_value_edit.py
holdenweb/skve
from textual.widget import Widget
from textual.widgets import Button
from textual.containers import Center, Horizontal, Vertical

class SaveCancel(Widget):

    def __init__(self, callback):
        super().__init__()
        self.callback = callback

    def compose(self):
        with Center(classes=f"id-{self.id} save-cancel-button-row"):
            with Horizontal(classes=f"id-{self.id} save-cancel-buttons"):
               yield Button("Save", variant="primary", classes=f"save id-{self.id} save-cancel-save-button")
               yield Button("Cancel", variant="error", classes=f"cancel id-{self.id} save-cancel-cancel-button")

    def on_button_pressed(self, event: Button.Pressed) -> None:
        id = event.button.id
        self.callback("save" in event.button.classes)

    DEFAULT_CSS = """\
SaveCancel {
    height: auto;
}
.save-cancel-button-row {
    min-height: 3;
}

.save-cancel-buttons {
    width: auto;
    height: 3;
}
SaveCancel Horizontal {
    height: 3;
}

SaveCancel Horizontal Button {
    height: 3;
    outline: white 75%;
}
"""
src/textutils/lib.py
holdenweb/skve
"""
models.py

Interactions for the acronyms app.
"""

import mongoengine as me

class Acronym(me.Document):
    acronym = me.StringField(required=True)
    title = me.StringField(required=True)
    explanation = me.StringField()
    further_info = me.StringField()
    linked_item = me.StringField()
    text_explanation = me.StringField()


class Person(me.DynamicDocument):
    pass


src/textutils/models.py
holdenweb/skve
from textual.app import App, ComposeResult
from textual.widget import Widget
from textual.widgets import Input, Label, Static, Button
from textual.containers import Horizontal, Vertical, VerticalScroll, Center
from textual.screen import Screen
from rich.text import Text

from textutils.key_value_edit import KeyValueEditScreen
from textutils.lib import SaveCancel
from textutils.test_store import people_matching, update_person

import os
from itertools import cycle

os.environ["TEXTUAL"] = "debug,devtools"

class PeopleApp(App):

    CSS_PATH = "people.css"

    def on_mount(self):
        self.push_screen(MainScreen())

class MainScreen(Screen):

    def compose(self) -> ComposeResult:
        yield InputStripe(id="question-box")
        yield Horizontal(
            VerticalScroll(
                id="buttons"
            ),
            VerticalScroll(
                Static("This will be the results"),
                id="content"
            ),
            id="body"
        )
        yield Center(Label(f"Initial message", id="message"))

    def on_input_changed(self, event: Input.Changed) -> None:
        for btn in self.query(".person-button"):
            btn.remove()  # TODO: Use remove_children on parent
        in_string = self.query_one(Input)
        value = in_string.value
        self.me_query = people_matching(value)
        count = len(self.me_query)
        if count == 0:
            self.replace_message("No matches")
        else:
            self.replace_message(f"{count} people")
            self.btns = []
            if value:
                for person in self.me_query:
                    self.query_one("#buttons").mount(
                        btn := PersonButton(person, classes="person-button")
                    )
                    self.btns.append(btn)

    def replace_message(self, msg):
        self.query_one("#message").update(msg)

class InputStripe(Widget):
    def compose(self):
        with Horizontal():
            yield Label("Search: ")
            yield Input(
                    placeholder="Enter (part of) a person's name...",
                    id="in-val",
            )
            yield Button(label="+", variant="primary", id="add-person")

class PersonButton(Button):

    def __init__(self, person, **kw):
        super().__init__(person['name'], **kw)
        self.person = person

    def on_button_pressed(self):
        app.bg_class = cycle(("bg-one", "bg-two"))
        app.query_one("#content").remove_children()
        self.add_new_row("Key", "Value", clickable=False)
        self.add_new_row("name", self.person['name'], clickable=True)
        for k in self.person.keys():
            if k not in {'name', 'id'}:
                self.add_new_row(k,self.person[k])
        app.query_one("#content").mount(SaveCancel(self.save_back))

    def add_new_row(self, key, value, w_type=Static, clickable=True):
        app.query_one("#content").mount(
            ResultRow(key, str(value), clickable=clickable)
        )

    def save_back(self, save_flag):
        if save_flag:
            key_vals = dict((row.key, row.value) for row in self.app.query(".result-row") if row.clickable)
            # Might get ugly with complex field types?
            update_person(self.person, key_vals)
        self.app.query_one("#content").remove_children()

class ResultRow(Widget):

    def __init__(self, key, value, clickable, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.key = key
        self.value = value
        self.clickable = clickable
        self.bg_class = next(app.bg_class)
        self.classes = f"result-row {self.bg_class}"
        self.key_field = Static(classes=f"key {self.bg_class}")
        self.value_field = Static(classes=f"value {self.bg_class}")

    def on_click(self, e):
        if self.clickable:
            app.push_screen(KeyValueEditScreen(self.key, self.value), self.stash_result)

    def compose(self):
        with Horizontal():
            yield Vertical(self.key_field, classes="key-col")
            yield Vertical(self.value_field, classes="value-col")

    def on_mount(self):
        self.update()

    def stash_result(self, return_value):
        if return_value is not None:
            # Some redundant code
            self.key, self.value = return_value
            self.key = self.key.lower()
            self.update()

    def update(self):
        self.key_field.update(Text(text=self.key.capitalize(), style="bold white"))
        self.value_field.update(Text(text=self.value))

app = PeopleApp()

src/textutils/people.py
holdenweb/skve

"""
Testing database
"""
import json

with open("data.json") as j_data:
    in_data = json.load(j_data)

data = {}
for item in in_data:
    data[item['name']] = item

def people_matching(s):
    return [data[k] for k in data if s.lower() in k.lower()]

def update_person(p: dict, kv: dict):
    p.update(kv)
    data[p['name']] = p

if __name__ == '__main__':
    for result, letters in( (2, 'teoln'), (1, 'bprasv'), (0, 'xyz')):
        for letter in letters:
            assert len(people_matching(letter)) == result
    for letters, person in (('bpr', p2), ('hsd', p1)):
        for letter in letters:
            pm = people_matching(letter)
            assert len(pm) == 1
            assert pm[0] is person

src/textutils/test_store.py
holdenweb/skve