Skip to content

Instantly share code, notes, and snippets.

@kirk86
Created July 3, 2020 18:41
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kirk86/71ee5d5300a8eb34e87a482514e36f42 to your computer and use it in GitHub Desktop.
Save kirk86/71ee5d5300a8eb34e87a482514e36f42 to your computer and use it in GitHub Desktop.
emojione-picker
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
#
import os
import subprocess
import time
import socket
import sys
import json
from os.path import expanduser
import signal
from collections import OrderedDict
import re
import importlib
# Fix enconding in older Python versions
importlib.reload(sys)
#sys.setdefaultencoding('utf8')
# Import GTK
from gi import require_version
require_version('AppIndicator3', '0.1')
require_version('Gtk', '3.0')
require_version('Notify', '0.7')
from gi.repository import GLib, Gtk, GObject, Gdk, Notify, GdkPixbuf
from gi.repository import AppIndicator3 as appindicator
# Where is the data?
directories = [expanduser("~") + "/.local/share/emojione-picker", "/usr/local/share/emojione-picker", "/usr/share/emojione-picker", os.path.dirname(os.path.realpath(__file__))]
for d in directories:
if os.path.isdir(d):
directory = d;
break
# Categories definitions
categories = ["recent", "people", "food", "nature", "objects", "activity", "travel", "flags", "symbols"]
# Settings handling
default_settings = settings = {
"toned": -1,
"notifications": True,
"lowend": False,
"recent": 20,
"paste": False
}
configpath = expanduser("~") + "/.config/emojione-picker"
os.path.isdir(configpath) or os.mkdir(configpath)
sockname = '/tmp/emojisocket.' + str(os.getuid()) + os.environ['DISPLAY']
builder = Gtk.Builder()
def handle_sock_signal(sock, *args):
GLib.io_add_watch(sock, GLib.IO_IN, handle_sock_signal)
conn, addr = sock.accept()
conn.close()
open_search_window('','')
def exit(self, w):
sys.exit()
def open_settings_window(self, w):
global settings
builder.add_from_file(directory + "/assets/settings.glade")
builder.connect_signals(SettingsButtonHandler())
settings_window = builder.get_object("settings_window")
settings_window.show_all()
settings_window.present()
settings_window.grab_focus()
# Apply settings
builder.get_object("combo_toned").set_active(settings["toned"]+1)
builder.get_object("check_notifications").set_active(settings["notifications"])
builder.get_object("check_lowend").set_active(settings["lowend"])
builder.get_object("recent").set_value(settings["recent"])
builder.get_object("check_paste").set_active(settings["paste"])
class SettingsButtonHandler:
def onButtonPressed(self, button):
apply_settings()
button.get_parent_window().destroy()
def onToggled(self,button):
if builder.get_object("check_paste").get_active() == True:
builder.get_object("check_notifications").set_sensitive(False)
else:
builder.get_object("check_notifications").set_sensitive(True)
def apply_settings():
global settings
# Get settings from dialog
settings = {
"toned": builder.get_object("combo_toned").get_active()-1,
"notifications": builder.get_object("check_notifications").get_active(),
"lowend": builder.get_object("check_lowend").get_active(),
"recent": builder.get_object("recent").get_value_as_int(),
"paste": builder.get_object("check_paste").get_active()
}
# Save settings to file
save_settings()
# Reload app to apply new settings - FIXME: This is ugly and slow, could be much better
os.unlink(sockname)
os.execv(__file__, sys.argv)
def save_settings():
global settings
with open(configfile, 'w') as outfile:
json.dump(settings, outfile)
# Search feature
searchbuilder = None
search_window = None
def open_search_window(self, w):
global searchbuilder
global search_window
# Build window only once
try:
search_window.show_all()
search_window.present()
search_window.grab_focus()
except:
# Build window
searchbuilder = Gtk.Builder()
searchbuilder.add_from_file(directory + "/assets/chooser.glade")
searchbuilder.connect_signals(SearchHandler())
search_window = searchbuilder.get_object("search_window")
search_window.show_all()
search_window.present()
search_window.grab_focus()
# Put recent icons by default
iconstore = searchbuilder.get_object("iconstore")
global sorted_recent, searchresults
for i in sorted_recent:
emoji_name = sorted_recent[i]["name"]
emoji_image = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_recent[i]["unicode"] + ".svg", 24, 24)
emoji_code = sorted_recent[i]["unicode"]
iconstore.append([emoji_image, emoji_code, emoji_name])
searchresults = []
for i in sorted_recent.keys():
searchresults.append(sorted_recent[i])
return
searchresults = None
selectionChanged = False
class SearchHandler:
def onSearchChanged(self, search):
global searchresults
search = searchbuilder.get_object("search").get_text()
iconstore = searchbuilder.get_object("iconstore")
iconstore.clear()
if search == "":
# No search? Put recent icons
global sorted_recent
for i in sorted_recent:
emoji_name = sorted_recent[i]["name"]
emoji_image = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_recent[i]["unicode"] + ".svg", 24, 24)
emoji_code = sorted_recent[i]["unicode"]
iconstore.append([emoji_image, emoji_code, emoji_name])
searchresults = []
for i in sorted_recent.keys():
searchresults.append(sorted_recent[i])
return
numfound=0
searchresults = []
for i in sorted_data:
match = False
for keyword in sorted_data[i]["keywords"]:
if keyword.find(search)>-1:
match = True
if sorted_data[i]["name"].find(search)>-1:
match = True
if match == True:
numfound += 1
if numfound > 50:
return
searchresults.append(sorted_data[i])
emoji_name = sorted_data[i]["name"]
emoji_image = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[i]["unicode"] + ".svg", 24, 24)
emoji_code = sorted_data[i]["unicode"]
iconstore.append([emoji_image, emoji_code, emoji_name])
def onIconActivated(self, icon, index):
global searchresults
item_response(self, searchresults[int(index.to_string())])
icon.get_parent_window().hide()
def onSelectionChanged(self, data):
global selectionChanged
selectionChanged = True
def onKeyReleased(self, window, event):
global selectionChanged
if event.keyval == Gdk.KEY_Escape:
window.hide()
elif event.keyval == Gdk.KEY_Down:
searchbuilder.get_object("iconview").grab_focus()
elif event.keyval == Gdk.KEY_Up and selectionChanged == False:
searchbuilder.get_object("search").grab_focus()
selectionChanged = False
def onOutFocus(self, window, data):
window.hide()
# Load settings at startup
configfile = configpath + "/settings.json"
if os.path.isfile(configfile):
with open(configfile) as settings_json_file:
try:
settings = json.load(settings_json_file)
except:
save_settings()
else:
save_settings()
for k in default_settings.keys():
if not k in settings:
settings[k] = default_settings[k]
# Load recent emojis at startup
recentfile = configpath + "/recent.json"
if os.path.isfile(recentfile):
with open(recentfile) as recent_json_file:
recent = json.load(recent_json_file)
recentindex = 0
for k in recent.keys():
if(int(recent[k]["recent_order"])>recentindex):
recentindex = int(recent[k]["recent_order"])
recentindex += 1
else:
recent = dict()
recentindex = 0
# If using lowend, load lowend information
if settings["lowend"] == True:
lowendfile = directory + "/assets/lowend.json"
if os.path.isfile(lowendfile):
with open(lowendfile) as lowend_json_file:
lowend = json.load(lowend_json_file)
# Refresh recent icons submenu
sorted_recent = None
def refresh_recent_submenu():
global recent, recent_items, sorted_recent
# Rearrange data
def orderfunc(tup):
key, d = tup
return -int(d["recent_order"])
sorted_recent = sorted(recent.items(), key=orderfunc)
sorted_recent = OrderedDict(sorted_recent)
# Refresh icons
i = 0
for key in sorted_recent:
if i >= settings["recent"]:
break
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_recent[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2])
img = Gtk.Image.new_from_pixbuf(pixbuf)
recent_items[i].set_image(img)
recent_items[i].set_label(sorted_recent[key]["name"].title())
recent_items[i].show()
if ("recent_" + str(i)) in signals.keys():
recent_items[i].disconnect(signals["recent_" + str(i)])
signals["recent_" + str(i)] = recent_items[i].connect("activate", item_response, sorted_recent[key])
i = i + 1
# Type character using xdootool [Experimental]
def type_xdotool(text):
time.sleep(0.1)
subprocess.Popen(["xdotool", "type", text.decode('unicode-escape')])
return False # Glib.idle_add needs this
# Click response
def item_response(self, w):
global recentindex, recent
# If this is a toned item with submenu, do nothing
try:
if self.get_submenu() != None:
return
except AttributeError:
pass
# Copy character to clipboard or write it
chars = w["unicode"].split("-")
output = ""
for char in chars:
output = output + '\\U' + (char.zfill(8))
if settings["paste"]==True:
GLib.idle_add(type_xdotool,output)
else:
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
#GLib.idle_add(clipboard.set_text,output.decode('unicode-escape'), -1)
GLib.idle_add(clipboard.set_text, bytes(output, "utf-8").decode("unicode_escape"), -1)
# Show notification
if settings["notifications"]==True:
if settings["paste"]==False:
n = Notify.Notification.new(w["name"].title(), "Emoji is now in the clipboard. Paste it wherever you want!", directory + "/assets/svg/" + w["unicode"] + ".svg")
n.show()
# Remove item from recent if already present
for k in recent.keys():
if recent[k]["emoji_order"] == w["emoji_order"]:
del recent[k]
# Store item on recent
w["recent_order"] = recentindex
recentindex = recentindex + 1
recent.update({w["emoji_order"]:w})
# Remove older items if recent is too big
if len(recent) > settings["recent"]:
biggestindex = 0
for k in recent.keys():
if recent[k]["recent_order"] > biggestindex:
biggestindex = recent[k]["recent_order"]
nextbiggest = biggestindex
for i in range (1, settings["recent"]):
nextbiggest_candidate = 0
for k in recent.keys():
if recent[k]["recent_order"] < nextbiggest and recent[k]["recent_order"] > nextbiggest_candidate:
nextbiggest_candidate = recent[k]["recent_order"]
nextbiggest = nextbiggest_candidate
minindex = nextbiggest
for k in recent.keys():
if recent[k]["recent_order"] < minindex:
del recent[k]
# Save recentfile
with open(recentfile, 'w') as outfile:
json.dump(recent, outfile)
# Refresh recent icons submenu
GLib.idle_add(refresh_recent_submenu)
def get_emoji_group(code):
for key in groups_data:
if code in groups_data[key]["unicodes"]:
return key
return False
if __name__ == "__main__":
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(sockname)
except:
# Calling GObject.threads_init() is not needed for PyGObject 3.10.2+
GObject.threads_init()
# Initialize notifications
Notify.init("emojione-picker")
# Create the main menu
menu = Gtk.Menu()
# Create indicator
ind = appindicator.Indicator.new("emojione-picker", directory + "/assets/icon-default.svg", appindicator.IndicatorCategory.APPLICATION_STATUS)
ind.set_status(appindicator.IndicatorStatus.ACTIVE)
# If there is a icon present in the theme, use it!
try:
icons = Gtk.IconTheme.get_default()
icon = icons.load_icon("emojione-picker", Gtk.IconSize.MENU, 0)
ind.set_icon("emojione-picker")
except:
pass
# Get proper icon sizes
iconsizes = Gtk.IconSize.lookup(Gtk.IconSize.MENU)
# Create categories items and submenus
category_item = {}
category_menu = {}
for category in categories:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/categories/" + category + ".svg", iconsizes[1], iconsizes[2])
img = Gtk.Image.new_from_pixbuf(pixbuf)
category_item[category] = Gtk.ImageMenuItem(category.title())
item_settings = category_item[category].get_settings()
item_settings.set_property('gtk-menu-images', True)
GLib.idle_add(category_item[category].set_image, img)
category_menu[category] = Gtk.Menu()
category_item[category].set_submenu(category_menu[category]);
# Load groups and icons definition and rearrange them in order
with open(directory + "/assets/groups.json") as groups_file:
groups_data = json.load(groups_file)
with open(directory + "/assets/emoji.json") as json_file:
json_data = json.load(json_file)
def orderfunc(tup):
key, d = tup
return int(d["emoji_order"])
sorted_data = sorted(json_data.items(), key=orderfunc)
sorted_data = OrderedDict(sorted_data)
# Load icons into menu items
tones_re = re.compile('(.*) tone[ ]?\d', re.IGNORECASE)
item_groups = {}
submenu_groups = {}
items = {}
tone_groups = {}
submenu_tones = {}
signals = {}
for category in categories:
for key in sorted_data:
if settings["lowend"] == False or not sorted_data[key]["unicode"] in lowend:
if sorted_data[key]["category"] == category:
emoji_group = get_emoji_group(sorted_data[key]["unicode"])
if emoji_group != False:
# Grouped emoji
if not emoji_group in item_groups:
# Create group item
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + groups_data[emoji_group]["icon"] + ".svg", iconsizes[1], iconsizes[2])
img = Gtk.Image.new_from_pixbuf(pixbuf)
item_groups[emoji_group] = Gtk.ImageMenuItem(groups_data[emoji_group]["name"].title())
item_settings = item_groups[emoji_group].get_settings()
item_settings.set_property('gtk-menu-images', True)
GLib.idle_add(item_groups[emoji_group].set_image, img)
GLib.idle_add(category_menu[category].append, item_groups[emoji_group])
GLib.idle_add(item_groups[emoji_group].show)
# Create group submenu
submenu_groups[emoji_group] = Gtk.Menu()
GLib.idle_add(item_groups[emoji_group].set_submenu, submenu_groups[emoji_group])
if tones_re.match(sorted_data[key]["name"]) == None:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2])
img = Gtk.Image.new_from_pixbuf(pixbuf)
items[sorted_data[key]["unicode"]] = Gtk.ImageMenuItem(sorted_data[key]["name"].title())
item_settings = items[sorted_data[key]["unicode"]].get_settings()
item_settings.set_property('gtk-menu-images', True)
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img)
GLib.idle_add(submenu_groups[emoji_group].append, items[sorted_data[key]["unicode"]])
GLib.idle_add(items[sorted_data[key]["unicode"]].show)
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[key])
else:
# This won't happen, we are not grouping toned icons
pass
else:
if tones_re.match(sorted_data[key]["name"]) == None:
# Not toned emoji (aka simplest case), just add to menu
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2])
img = Gtk.Image.new_from_pixbuf(pixbuf)
items[sorted_data[key]["unicode"]] = Gtk.ImageMenuItem(sorted_data[key]["name"].title())
item_settings = items[sorted_data[key]["unicode"]].get_settings()
item_settings.set_property('gtk-menu-images', True)
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img)
GLib.idle_add(category_menu[category].append, items[sorted_data[key]["unicode"]])
GLib.idle_add(items[sorted_data[key]["unicode"]].show)
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[key])
else:
# Toned emoji
if settings["toned"] == -1 and settings["lowend"] == False:
# Show all toned emojis in a submenu
original_key = key
chars = sorted_data[key]["unicode"].split("-")
tone_group = chars[0] # FIXME this can be multibyte so taking the first byt isn't right
if not tone_group in tone_groups:
# Create tone submenu
submenu_tones[tone_group] = Gtk.Menu()
GLib.idle_add(items[tone_group].set_submenu, submenu_tones[tone_group])
tone_groups[tone_group] = items[tone_group]
# Get key from original icon
for k in sorted_data:
if sorted_data[k]["unicode"] == tone_group:
key = k
# Append untoned icon to submenu
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2])
img = Gtk.Image.new_from_pixbuf(pixbuf)
items[sorted_data[key]["unicode"]] = Gtk.ImageMenuItem(sorted_data[key]["name"].title())
item_settings = items[tone_group].get_settings()
item_settings.set_property('gtk-menu-images', True)
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img)
GLib.idle_add(submenu_tones[tone_group].append, items[sorted_data[key]["unicode"]])
GLib.idle_add(items[sorted_data[key]["unicode"]].show)
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[key])
key = original_key
# Append toned emoji to toned submenu
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[key]["unicode"] + ".svg", iconsizes[1], iconsizes[2])
img = Gtk.Image.new_from_pixbuf(pixbuf)
items[sorted_data[key]["unicode"]] = Gtk.ImageMenuItem(sorted_data[key]["name"].title())
item_settings = items[sorted_data[key]["unicode"]].get_settings()
item_settings.set_property('gtk-menu-images', True)
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img)
GLib.idle_add(submenu_tones[tone_group].append, items[sorted_data[key]["unicode"]])
GLib.idle_add(items[sorted_data[key]["unicode"]].show)
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[key])
else:
# Show only one tone for toned emojis
if settings["toned"] == 0 or settings["lowend"] == True:
# Untoned emojis are already shown by default, so do nothing
# When using lowend mode, untoned emojis are enforced
pass
else:
desired_tone = settings["toned"]
current_tone = re.search("\d",sorted_data[key]["name"]).group(0)
if desired_tone == int(current_tone):
# Replace item with desired tone
toned_key = key
chars = sorted_data[key]["unicode"].split("-") # FIXME this can be multibyte so taking the first byt isn't right
untoned_code = chars[0]
for k in sorted_data:
if sorted_data[k]["unicode"] == untoned_code:
key = k
# Replace image in item
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(directory + "/assets/svg/" + sorted_data[toned_key]["unicode"] + ".svg", iconsizes[1], iconsizes[2])
img = Gtk.Image.new_from_pixbuf(pixbuf)
GLib.idle_add(items[sorted_data[key]["unicode"]].set_image, img)
# Replace action
items[sorted_data[key]["unicode"]].disconnect(signals[key])
signals[key] = items[sorted_data[key]["unicode"]].connect("activate", item_response, sorted_data[toned_key])
# Load icons into recent category
recent_items = []
for i in range (0, settings["recent"]):
recent_items.append( Gtk.ImageMenuItem())
item_settings = recent_items[i].get_settings()
item_settings.set_property('gtk-menu-images', True)
category_menu["recent"].append(recent_items[i])
refresh_recent_submenu()
# Append categories to main menu
for category in categories:
GLib.idle_add(menu.append, category_item[category])
GLib.idle_add(category_item[category].show)
# Create separator
separator = Gtk.SeparatorMenuItem()
GLib.idle_add(menu.append,separator)
GLib.idle_add(separator.show)
# Create search item
search_item = Gtk.MenuItem("Search Emoji")
GLib.idle_add(menu.append,search_item)
GLib.idle_add(search_item.show)
search_item.connect("activate", open_search_window, "Search")
# Create settings item
settings_item = Gtk.MenuItem("Settings...")
GLib.idle_add(menu.append,settings_item)
GLib.idle_add(settings_item.show)
settings_item.connect("activate", open_settings_window, "Settings")
# Create another separator
separator = Gtk.SeparatorMenuItem()
GLib.idle_add(menu.append,separator)
GLib.idle_add(separator.show)
# Create exit item
exit_item = Gtk.MenuItem("Exit")
GLib.idle_add(menu.append,exit_item)
GLib.idle_add(exit_item.show)
exit_item.connect("activate", exit, "Exit")
# Associate menu with indicator
ind.set_menu(menu)
# Listen to socket
try:
os.unlink(sockname)
except OSError:
pass
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(sockname)
sock.listen(1)
GLib.io_add_watch(sock, GLib.IO_IN, handle_sock_signal)
except:
print('Could not listen to socket!')
# Run server
signal.signal(signal.SIGINT, signal.SIG_DFL)
Gtk.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment