diff --git a/lock.py b/lock.py new file mode 100644 index 0000000..4bb1c26 --- /dev/null +++ b/lock.py @@ -0,0 +1,218 @@ +import gi, pam, fabric + +gi.require_version("GtkSessionLock", "0.1") +from gi.repository import Gdk, GtkSessionLock + +from fabric.widgets.window import Window + +# from widgets.wayland import WaylandWindow as Window +import os +from fabric.widgets.entry import Entry +from fabric.widgets.datetime import DateTime +from modules.panel.components.indicators import ( + BatteryIndicator, + BluetoothIndicator, + NetworkIndicator, +) +from fabric.widgets.box import Box +from fabric.widgets.label import Label +from fabric.widgets.centerbox import CenterBox + +# from fabric.widgets.image import Image +import getpass +from fabric import Application +from gi.repository import GLib +from widgets.circle_image import CircleImage as Image +from fabric.utils import get_relative_path + + +class IndicatorBox(Box): + def __init__(self, *args, **kwargs): + super().__init__( + h_align="end", + name="indicatorbox", + spacing=5, + h_expand=True, + children=[ + BatteryIndicator(), + BluetoothIndicator(), + NetworkIndicator(), + ], + ) + + +class ContentBox(CenterBox): + def __init__(self, on_activate, *args, **kwargs): + self.password_entry = Entry( + placeholder="Enter Password", + name="password-entry", + h_align="center", + v_align="center", + visible=False, + password=True, + # on_activate=on_activate, + ) + + self.password_entry.set_property("xalign", 0.5) + self.username_label = Label( + label=f"{getpass.getuser().title()}", + name="username", + visible=True, + h_align="center", + v_align="center", + ) + self.unlock_text = Label( + label="Touch ID or Enter Password", + name="unlock-text", + ) + super().__init__( + name="content-box", + h_expand=True, + orientation="vertical", + v_expand=True, + start_children=[ + IndicatorBox(), + DateTime( + formatters=["%A,%B %-d"], + interval=10000, + h_expand=False, + v_align="start", + v_expand=False, + name="lock-date", + ), + DateTime(formatters=["%I:%M"], name="lock-clock"), + ], + end_children=[ + Box( + name="profile-box", + h_align="center", + h_expand=True, + v_expand=True, + v_align="end", + children=[ + Image( + name="face-icon", + image_file="/home/saumya/.face.icon", + size=64, + ), + ], + ), + Box( + name="container-box", + orientation="v", + v_align="end", + h_align="center", + h_expand=True, + v_expand=True, + children=[ + self.username_label, + self.password_entry, + ], + ), + Box( + name="unlock-box", + v_align="end", + h_align="center", + h_expand=True, + v_expand=True, + children=[ + self.unlock_text, + ], + ), + ], + **kwargs, + ) + + +class LockScreen(Window): + def __init__(self, lock: GtkSessionLock.Lock): + self._hide_timeout_id = None # prevent AttributeError + self.lock = lock + self.content = ContentBox(self.on_activate) + super().__init__( + title="lock", + visible=True, + all_visible=False, + name="lockscreen-bg", + anchor="center", + child=self.content, + ) + + self.content.password_entry.set_visible(False) + self.connect("key-press-event", self._on_keypress) + + bg = os.path.expanduser("~/.current.wall") + self.set_style(f"background-image: url('{bg}');") + + def _on_keypress(self, widget, event): + keyval = event.keyval + + # ESC pressed → hide entry immediately + if keyval == Gdk.KEY_Escape and self.content.password_entry.get_visible(): + self._hide_entry() + return + + # Show entry if hidden + if not self.content.password_entry.get_visible(): + self.content.username_label.set_visible(False) + self.content.password_entry.set_visible(True) + self.content.password_entry.grab_focus() + self._start_hide_timer() + else: + # Reset timer if already visible + self._restart_hide_timer() + + def _start_hide_timer(self): + self._stop_hide_timer() # just in case + self._hide_timeout_id = GLib.timeout_add_seconds(500, self._hide_entry) + # 10 seconds of inactivity before hiding + + def _restart_hide_timer(self): + self._start_hide_timer() + + def _stop_hide_timer(self): + if self._hide_timeout_id: + GLib.source_remove(self._hide_timeout_id) + self._hide_timeout_id = None + + def _hide_entry(self): + self._stop_hide_timer() + self.content.password_entry.set_visible(False) + self.content.username_label.set_visible(True) + return False # stop timeout + + def on_activate(self, entry: Entry, *args): + if not pam.authenticate(getpass.getuser(), (entry.get_text() or "").strip()): + return + self.lock.unlock_and_destroy() + self.destroy() + + +def initialize(): + lock = GtkSessionLock.prepare_lock() + lock.lock_lock() + lockscreen = LockScreen(lock) + lock.new_surface( + lockscreen, + Gdk.Display.get_default().get_monitor( + 0 + ), # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess] + ) + lockscreen.show_all() + + +if __name__ == "__main__": + # initialize() + lockscreen = LockScreen(GtkSessionLock.Lock()) + + app = Application("lock", lockscreen) + + def set_css(): + app.set_stylesheet_from_file( + get_relative_path("main.css"), + ) + + app.set_css = set_css # pyright: ignore[reportAttributeAccessIssue] + + app.set_css() # pyright: ignore[reportAttributeAccessIssue] + app.run() diff --git a/lock/bash.sh b/lock/bash.sh deleted file mode 100755 index f746983..0000000 --- a/lock/bash.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -while true; do - inotifywait -e close_write main.css - fabric-cli exec lock 'app.set_css()' -done - - diff --git a/lock/lock.py b/lock/lock.py deleted file mode 100644 index 4701754..0000000 --- a/lock/lock.py +++ /dev/null @@ -1,92 +0,0 @@ -import gi, pam, fabric - -gi.require_version("GtkSessionLock", "0.1") -from gi.repository import Gdk, GtkSessionLock - -from fabric.widgets.window import Window - -# from widgets.wayland import WaylandWindow as Window -from fabric.widgets.entry import Entry -from fabric.widgets.datetime import DateTime -from fabric.widgets.box import Box -from fabric.widgets.centerbox import CenterBox -from fabric import Application - -from fabric.utils import get_relative_path - - -class ContentBox(CenterBox): - def __init__(self, *args, **kwargs): - super().__init__( - name="content-box", - h_expand=True, - orientation="vertical", - v_expand=True, - start_children=[ - DateTime( - formatters=["%A. %d %B"], - interval=10000, - h_expand=False, - v_align="start", - v_expand=False, - name="date", - ), - DateTime(formatters=["%I:%M"], name="clock"), - ], - end_children=[ - Entry( - placeholder="gib password", - name="password-entry", - h_align="center", - v_align="end", - password=True, - on_activate=on_activate(self), - ), - ], - **kwargs - ) - - -class LockScreen(Window): - def __init__(self, lock: GtkSessionLock.Lock): - self.lock = lock - super().__init__( - title="lock", - visible=True, - all_visible=False, - name="lockscreen-bg", - anchor="center", - child=ContentBox(), - ) - - def on_activate(self, entry: Entry, *args): - if entry.get_text() == "help": - pass - elif not pam.authenticate("saumya", (entry.get_text() or "").strip()): - return - self.lock.unlock_and_destroy() - self.destroy() - - -def initialize(): - lock = GtkSessionLock.prepare_lock() - lock.lock_lock() - lockscreen = LockScreen(lock) - lock.new_surface(lockscreen, Gdk.Display.get_default().get_monitor(0)) - lockscreen.show_all() - - -if __name__ == "__main__": - initialize() - lock = LockScreen(GtkSessionLock.Lock()) - app = Application("lock", lock) - - def set_css(): - app.set_stylesheet_from_file( - get_relative_path("main.css"), - ) - - app.set_css = set_css - - app.set_css() - app.run() diff --git a/lock/main.css b/lock/main.css deleted file mode 100644 index 4950242..0000000 --- a/lock/main.css +++ /dev/null @@ -1,37 +0,0 @@ -/* album_cover.set_style(f"background-image:url('{fallback_cover_path}')") */ -#lockscreen-bg { - /* background-color: rgba(225, 225, 225, 0.9); */ - background-image: url("/home/saumya/.current.wall"); - background-size: cover; -} -* { - all: unset; - font-family: "SF Pro Rounded"; -} -#password-entry { - background-color: white; - border-radius: 20px; - min-width: 300px; - color: black; - min-height: 50px; - font-size: 20px; - margin-bottom: 80px; - font-weight: bold; -} - -#date { - background-color: transparent; - color: alpha(#fff, 0.5); - border: none; - font-size: 50px; - margin-top: 80px; - font-weight: bold; -} -#clock { - background-color: transparent; - color: alpha(#fff, 0.5); - border: none; - font-size: 150px; - margin-top: -20px; - font-weight: bold; -} diff --git a/main.css b/main.css index d07a2d5..311d7af 100644 --- a/main.css +++ b/main.css @@ -13,12 +13,12 @@ @import url("./styles/widgets.css"); @import url("./styles/tray.css"); @import url("./styles/battery-widget.css"); +@import url("./styles/lock.css"); * { all: unset; color: var(--foreground); - font-size: 13px; - font-family: "SF Pro Rounded"; + font-size: unset; } #corner { diff --git a/styles/lock.css b/styles/lock.css new file mode 100644 index 0000000..345ef96 --- /dev/null +++ b/styles/lock.css @@ -0,0 +1,65 @@ +#lockscreen-bg { + background-size: cover; +} +#container-box { + min-height: 50px; + min-width: 250px; + transition: all 0.7s ease-in-out; +} +#password-entry { + background-color: alpha(#fff, 0.4); + border-radius: 20px; + color: white; + min-width: 220px; + min-height: 40px; + font-family: "SF Pro Rounded"; + font-size: 14px; + font-weight: 500; + transition: all 0.7s ease-in-out; +} +#profile-box { + margin-bottom: 10px; +} +#username { + font-size: 21px; + font-weight: bold; + margin-top: 5px; + transition: all 0.7s ease-in-out; +} + +#lock-date label { + background-color: transparent; + font-family: "SF Pro Rounded"; + color: alpha(#fff, 0.6); + border: none; + font-size: 30px; + margin-top: 80px; + font-weight: bold; +} +#lock-clock label { + background-color: transparent; + color: alpha(#fff, 0.6); + font-family: "SF Pro Rounded "; + border: none; + /* font-family: "SF Pro Rounded"; */ + font-size: 130px; + margin-top: -20px; + font-weight: bold; +} + +#face-icon image { + margin-bottom: 10px; +} +#face-icon { + padding-bottom: 10px; +} + +#unlock-box { + margin-bottom: 40px; +} +#unlock-text { + font-family: "SF Pro Rounded"; + font-size: 15px; + color: alpha(#fff, 0.6); + font-weight: bold; +} diff --git a/widgets/circle_image.py b/widgets/circle_image.py new file mode 100644 index 0000000..aefd7e0 --- /dev/null +++ b/widgets/circle_image.py @@ -0,0 +1,125 @@ +import math +from typing import Literal + +import cairo +import gi +from fabric.core.service import Property +from fabric.widgets.widget import Widget + +gi.require_version("Gtk", "3.0") +from gi.repository import Gdk, GdkPixbuf, Gtk # noqa: E402 + + +class CircleImage(Gtk.DrawingArea, Widget): + """A widget that displays an image in a circular shape with a 1:1 aspect ratio.""" + + @Property(int, "read-write") + def angle(self) -> int: + return self._angle + + @angle.setter + def angle(self, value: int): + self._angle = value % 360 + self.queue_draw() + + def __init__( + self, + image_file: str | None = None, + pixbuf: GdkPixbuf.Pixbuf | None = None, + name: str | None = None, + visible: bool = True, + all_visible: bool = False, + style: str | None = None, + tooltip_text: str | None = None, + tooltip_markup: str | None = None, + h_align: ( + Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None + ) = None, + v_align: ( + Literal["fill", "start", "end", "center", "baseline"] | Gtk.Align | None + ) = None, + h_expand: bool = False, + v_expand: bool = False, + size: int | None = None, + **kwargs, + ): + Gtk.DrawingArea.__init__(self) + Widget.__init__( + self, + name=name, + visible=visible, + all_visible=all_visible, + style=style, + tooltip_text=tooltip_text, + tooltip_markup=tooltip_markup, + h_align=h_align, + v_align=v_align, + h_expand=h_expand, + v_expand=v_expand, + size=size, + **kwargs, + ) + self.size = size if size is not None else 100 # Default size if not provided + self._angle = 0 + # Original image for reprocessing + self._orig_image: GdkPixbuf.Pixbuf | None = None + self._image: GdkPixbuf.Pixbuf | None = None + if image_file: + pix = GdkPixbuf.Pixbuf.new_from_file(image_file) + self._orig_image = pix + self._image = self._process_image(pix) + elif pixbuf: + self._orig_image = pixbuf + self._image = self._process_image(pixbuf) + self.connect("draw", self.on_draw) + + def _process_image(self, pixbuf: GdkPixbuf.Pixbuf) -> GdkPixbuf.Pixbuf: + """Crop the image to a centered square and scale it to the widget’s size.""" + width, height = pixbuf.get_width(), pixbuf.get_height() + if width != height: + square_size = min(width, height) + x_offset = (width - square_size) // 2 + y_offset = (height - square_size) // 2 + pixbuf = pixbuf.new_subpixbuf(x_offset, y_offset, square_size, square_size) + else: + square_size = width + if square_size != self.size: + pixbuf = pixbuf.scale_simple( + self.size, self.size, GdkPixbuf.InterpType.BILINEAR + ) + return pixbuf + + def on_draw(self, widget: "CircleImage", ctx: cairo.Context): + if self._image: + ctx.save() + # Create a circular clipping path + ctx.arc(self.size / 2, self.size / 2, self.size / 2, 0, 2 * math.pi) + ctx.clip() + # Rotate around the center of the square image + ctx.translate(self.size / 2, self.size / 2) + ctx.rotate(self._angle * math.pi / 180.0) + ctx.translate(-self.size / 2, -self.size / 2) + Gdk.cairo_set_source_pixbuf(ctx, self._image, 0, 0) + ctx.paint() + ctx.restore() + + def set_image_from_file(self, new_image_file: str): + if not new_image_file: + return + pixbuf = GdkPixbuf.Pixbuf.new_from_file(new_image_file) + self._orig_image = pixbuf + self._image = self._process_image(pixbuf) + self.queue_draw() + + def set_image_from_pixbuf(self, pixbuf: GdkPixbuf.Pixbuf): + if not pixbuf: + return + self._orig_image = pixbuf + self._image = self._process_image(pixbuf) + self.queue_draw() + + def set_image_size(self, size: int): + self.size = size + if self._orig_image: + self._image = self._process_image(self._orig_image) + self.queue_draw()