Skip to content

Instantly share code, notes, and snippets.

@jeena
Created September 26, 2025 02:54
Show Gist options
  • Select an option

  • Save jeena/9df0f9b59cec1225bed21223353c9137 to your computer and use it in GitHub Desktop.

Select an option

Save jeena/9df0f9b59cec1225bed21223353c9137 to your computer and use it in GitHub Desktop.
#!/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