~sircmpwn/hiprompt-gtk-py

7872a36b6f7ca8b1a9084e2d872d6e079f16bd27 — Krystian Chachuła a month ago f744993 master
Async password feedback

Introduces Gio to tell the user that their password is being verified
instead of just freezing the GUI.
2 files changed, 83 insertions(+), 32 deletions(-)

M hiprompt_gtk/proto.py
M hiprompt_gtk/window.py
M hiprompt_gtk/proto.py => hiprompt_gtk/proto.py +51 -15
@@ 1,8 1,39 @@
import sys
from enum import Enum
from gi.repository import GLib, Gio
from .key import Key


def gio_lines(stream):
    line = bytearray()

    while not stream.is_closed():
        byte = stream.read_bytes(1).get_data()
        if not byte:
            return
        line += byte
        if line[-1] == ord("\n"):
            yield line.decode()
            line.clear()

def gio_lines_async(stream, callback):
    line = bytearray()

    def _callback(source, result):
        nonlocal line
        byte = source.read_bytes_finish(result).get_data() if result else b""
        line += byte
        if line and line[-1] == ord("\n"):
            more = callback(line.decode())
            if not more:
                return
            line.clear()

        stream.read_bytes_async(1, GLib.PRIORITY_DEFAULT, None, _callback)

    _callback(stream, None)


class PromptMode(Enum):
    disclose = "disclose"
    delete = "delete"


@@ 14,10 45,11 @@ class Session:
        self.unlock = False
        self.status = 127
        self.mode = None
        self.stream = Gio.UnixInputStream.new(sys.stdin.fileno(), False)
        self.parse()

    def parse(self):
        for line in sys.stdin:
        for line in gio_lines(self.stream):
            parts = line.strip().partition(" ")
            cmd = parts[0]
            args = parts[2]


@@ 44,22 76,26 @@ class Session:
        self.mode = PromptMode(args)
        return False

    def password(self, pw):
    def password(self, pw, callback):
        """
        Sends a password to the daemon in response to an unlock command. Blocks
        until the daemon answers regarding the password's validity, returning
        True if it was correct and False otherwise.
        Sends a password to the daemon in response to an unlock command and
        returns immediately. When the daemon answers regarding the password's
        validity, calls the callback function with one argument: True if it was
        correct and False otherwise.
        """
        sys.stdout.write(f"password {pw}\n")
        sys.stdout.flush()
        # TODO: Don't block
        line = sys.stdin.readline()
        if line == "password incorrect\n":

        def _callback(line):
            if line == "password incorrect\n":
                callback(False)
            elif line == "password correct\n":
                self.unlock = False
                self.parse()
                callback(True)
            else:
                sys.stderr.write(f"Unexpected command '{line}'\n")
                exit(127)
            return False
        elif line == "password correct\n":
            self.unlock = False
            self.parse()
            return True
        else:
            sys.stderr.write(f"Unexpected command '{line}'\n")
            exit(127)

        gio_lines_async(self.stream, _callback)

M hiprompt_gtk/window.py => hiprompt_gtk/window.py +32 -17
@@ 23,6 23,8 @@ class Window:
        self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        self.window.add(self.box)

        self.deny = None
        self.grant = None
        self.entry = None
        self.buttons = []



@@ 63,17 65,30 @@ class Window:

    def on_unlock_grant(self, *args):
        password = self.entry.get_text()
        self.deny.set_sensitive(False)
        self.grant.set_sensitive(False)
        self.entry.set_sensitive(False)
        self.entry.set_text("Verifying...")
        self.entry.set_visibility(True)
        self.window.remove_accel_group(self.accel)
        confirmed = len(self.session.keys) > 0
        if self.session.password(password):
            if not self.session.mode or confirmed:
                self.session.status = 0
                Gtk.main_quit()

        def _callback(status):
            if status:
                if not self.session.mode or confirmed:
                    self.session.status = 0
                    Gtk.main_quit()
                else:
                    self.make_ui()
            else:
                self.make_ui()
        else:
            self.entry.set_text("")
            self.entry.set_sensitive(True)
                self.entry.set_text("")
                self.entry.set_visibility(False)
                self.entry.set_sensitive(True)
                self.grant.set_sensitive(True)
                self.window.add_accel_group(self.accel)
                self.entry.grab_focus()

        self.session.password(password, _callback)

    def on_disclose_grant(self, *args):
        self.session.status = 0


@@ 88,16 103,16 @@ class Window:
        self.header.set_title("Unlock keyring")

        if len(self.session.keys) == 0:
            deny = Gtk.Button("Cancel")
            self.deny = Gtk.Button("Cancel")
        else:
            deny = Gtk.Button("Deny access")
        deny.connect("clicked", self.on_deny)
        self.header.pack_start(deny)

        grant = Gtk.Button("Grant access")
        grant.get_style_context().add_class("destructive-action")
        grant.connect("clicked", self.on_unlock_grant)
        self.header.pack_end(grant)
            self.deny = Gtk.Button("Deny access")
        self.deny.connect("clicked", self.on_deny)
        self.header.pack_start(self.deny)

        self.grant = Gtk.Button("Grant access")
        self.grant.get_style_context().add_class("destructive-action")
        self.grant.connect("clicked", self.on_unlock_grant)
        self.header.pack_end(self.grant)

        scroller = Gtk.ScrolledWindow()
        scroller.set_propagate_natural_height(True)