-
-
Save jeena/9df0f9b59cec1225bed21223353c9137 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import gi | |
| gi.require_version("Gtk", "4.0") | |
| gi.require_version("Adw", "1") | |
| from gi.repository import Gtk, Adw, Gio | |
| from gi.repository import Gdk | |
| import subprocess | |
| import os | |
| import difflib | |
| MEME_DIR = os.path.expanduser("~/Pictures/Memes") # change this to your memes folder | |
| class MemeFinderWindow(Adw.ApplicationWindow): | |
| def __init__(self, app): | |
| super().__init__(application=app, title="Meme Finder", default_width=1000, default_height=600) | |
| toolbar = Adw.ToolbarView() | |
| toolbar.set_content() | |
| self.set_content(toolbar) | |
| header = Adw.HeaderBar() | |
| header.set_show_end_title_buttons(True) | |
| toolbar.add_top_bar(header) | |
| # Main container | |
| self.paned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL) | |
| toolbar.set_content(self.paned) | |
| # Left side: search entry + list | |
| left_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, margin_top=6, margin_start=6, margin_end=6) | |
| self.search_entry = Gtk.Entry(placeholder_text="Search memes…") | |
| self.search_entry.connect("changed", self.on_search_changed) | |
| self.search_entry.connect("activate", self.on_activate) | |
| left_box.set_hexpand(True) | |
| left_box.set_vexpand(True) | |
| left_box.append(self.search_entry) | |
| controller = Gtk.EventControllerKey.new() | |
| controller.connect("key-pressed", self.on_search_key) | |
| self.search_entry.add_controller(controller) | |
| # Model and selection wrapper | |
| self.liststore = Gtk.StringList() | |
| self.selection = Gtk.SingleSelection.new(self.liststore) | |
| self.selection.connect("notify::selected", self.on_selection_changed) | |
| # Factory for list rows | |
| self.factory = Gtk.SignalListItemFactory() | |
| self.factory.connect("setup", self.on_list_setup) | |
| self.factory.connect("bind", self.on_list_bind) | |
| self.listview = Gtk.ListView.new(model=self.selection, factory=self.factory) | |
| self.listview.connect("activate", self.on_row_activated) | |
| scrolled = Gtk.ScrolledWindow() | |
| scrolled.set_child(self.listview) | |
| scrolled.set_vexpand(True) | |
| left_box.append(scrolled) | |
| # Right side: preview | |
| # Right side: preview | |
| self.preview = Gtk.Picture() | |
| self.preview.set_valign(Gtk.Align.CENTER) | |
| self.preview.set_halign(Gtk.Align.CENTER) | |
| self.preview.set_hexpand(True) | |
| self.preview.set_vexpand(True) | |
| self.preview.set_size_request(300, 300) # minimum width & height | |
| preview_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) | |
| preview_box.set_hexpand(True) | |
| preview_box.set_vexpand(True) | |
| preview_box.append(self.preview) | |
| # Paned layout | |
| self.paned.set_start_child(left_box) | |
| self.paned.set_end_child(preview_box) | |
| # Load memes | |
| self.all_memes = [f for f in os.listdir(MEME_DIR) | |
| if f.lower().endswith((".png", ".jpg", ".jpeg", ".gif"))] | |
| self.update_list(self.all_memes) | |
| def update_list(self, items): | |
| self.liststore.splice(0, len(self.liststore), items) | |
| # Select first item if list is not empty | |
| if items: | |
| self.selection.set_selected(0) | |
| self.show_preview(items[0]) | |
| else: | |
| self.preview.set_file(None) # clear preview if no matches | |
| def on_search_changed(self, entry): | |
| query = entry.get_text().strip().lower() | |
| if not query: | |
| self.update_list(self.all_memes) | |
| else: | |
| results = [f for f in self.all_memes if query.replace(" ", "") in f.replace("-", "").replace("_", "").lower()] | |
| self.update_list(results) | |
| def on_list_setup(self, factory, item): | |
| label = Gtk.Label(xalign=0) | |
| item.set_child(label) | |
| def on_list_bind(self, factory, item): | |
| label = item.get_child() | |
| name = self.liststore.get_string(item.get_position()) | |
| label.set_text(name) | |
| def on_row_activated(self, listview, position): | |
| filename = self.liststore.get_string(position) | |
| self.show_preview(filename) | |
| def on_selection_changed(self, selection, param): | |
| pos = selection.get_selected() | |
| if pos != Gtk.INVALID_LIST_POSITION: | |
| filename = self.liststore.get_string(pos) | |
| self.show_preview(filename) | |
| def show_preview(self, filename): | |
| path = os.path.join(MEME_DIR, filename) | |
| file = Gio.File.new_for_path(path) | |
| self.preview.set_file(file) | |
| def on_activate(self, entry): | |
| pos = self.selection.get_selected() | |
| if pos != Gtk.INVALID_LIST_POSITION: | |
| filename = self.liststore.get_string(pos) | |
| self.copy_to_clipboard(filename) | |
| def copy_to_clipboard(self, filename): | |
| path = os.path.join(MEME_DIR, filename) | |
| # Copy to Wayland clipboard with wl-copy | |
| with open(path, "rb") as f: | |
| subprocess.run(["wl-copy", "-t", "image/png"], input=f.read()) | |
| self.show_copied_alert(filename) | |
| def on_search_key(self, entry, event): | |
| key = event.keyval | |
| selected = self.selection.get_selected() | |
| # Up arrow | |
| if key == Gdk.KEY_Up: | |
| if selected > 0: | |
| self.selection.set_selected(selected - 1) | |
| return True # stop further handling | |
| # Down arrow | |
| elif key == Gdk.KEY_Down: | |
| if selected < len(self.liststore) - 1: | |
| self.selection.set_selected(selected + 1) | |
| return True | |
| return False # allow normal handling for other keys | |
| def on_search_key(self, controller, keyval, keycode, state): | |
| selected = self.selection.get_selected() | |
| if len(self.liststore) == 0: | |
| return False | |
| if keyval == Gdk.KEY_Up: | |
| new_index = max(selected - 1, 0) | |
| self.selection.set_selected(new_index) | |
| return True # stop further handling | |
| elif keyval == Gdk.KEY_Down: | |
| new_index = min(selected + 1, len(self.liststore) - 1) | |
| self.selection.set_selected(new_index) | |
| return True # stop further handling | |
| elif keyval in (Gdk.KEY_Return, Gdk.KEY_KP_Enter): | |
| filename = self.liststore.get_string(self.selection.get_selected()) | |
| self.copy_to_clipboard(filename) | |
| return True | |
| return False | |
| def show_copied_alert(self, filename): | |
| dialog = Adw.AlertDialog( | |
| heading=f"'{filename}' copied", | |
| body="You can now paste it anywhere." | |
| ) | |
| # Add a Cancel button | |
| dialog.add_response("cancel", "Cancel") | |
| # Add a Close button | |
| dialog.add_response("close", "Close") | |
| dialog.set_response_appearance("close", Adw.ResponseAppearance.SUGGESTED) | |
| # Set the default response to Close | |
| dialog.set_default_response("close") | |
| dialog.choose(self, None, self.on_alert_response, None) | |
| # Show the dialog | |
| dialog.present(self) | |
| def on_alert_response(self, dialog, result, user_data): | |
| response_id = dialog.choose_finish(result) | |
| if response_id == "close": | |
| self.get_application().quit() | |
| class MemeFinderApp(Adw.Application): | |
| def __init__(self): | |
| super().__init__(application_id="net.jeena.MemeFinder") | |
| def do_activate(self): | |
| win = MemeFinderWindow(self) | |
| win.present() | |
| if __name__ == "__main__": | |
| app = MemeFinderApp() | |
| app.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment