Application Launcher in Python with GTK3
For the longest time, I was using a wrapper for dmenu to launch my applications. Specifically, the application launcher would be triggered by pressing ALT+SPACE on the keyboard, and then I would see a list of applications on my screen.
Then the process was:
- Typing a string would shorten the list
- arrow keys would move a highlight through the list
- pressing ENTER would launch the highlighted application
One fateful day however, the launcher stopped working and I was sad. Bleak were my days, toiling to fix my beloved launcher. Alas, it was not meant to be, and a foul mood nested upon my countenance. It was during those dark times, while pining for dmenu
, that I realized it wasn't the application that I missed, it was the process of launching applications that I missed. Fog lifted, a realization illuminated my path and set my mission: regain the process!
Armed with trusty keyboard, I quested to Python GTK3 documentation, intent on collecting the various knick-knacks and doodads that would be assembled into a doppelganger of the process. A bit of the good ol "code, tinker, break, fix" and swearing later, and this is the result :)
Enter the Python
#!/usr/bin/env python
# -- this code is licensed GPLv3
# Copyright 2018 Jezra
import signal
import os.path
import subprocess
import sys
#Gtk
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GObject, Gtk, Gdk
class myWindow(Gtk.Window):
__gsignals__ = {
'quit' : (GObject.SignalFlags.RUN_LAST, GObject.TYPE_NONE, ())
}
#create array to hold string name of executables
executables = []
#listStore holds data for the treeView widget
store = Gtk.ListStore(str)
def __init__(self):
Gtk.Window.__init__(self, title="launcher dohickey")
#load the executables
self.load_executables()
#determine the max size of the screen
sizes = Gdk.Screen.get_default()
max_height= sizes.get_height()
self.set_default_size(200, max_height)
#create an accellerator group for this window
accel = Gtk.AccelGroup()
#add the ctrl+q to quit
accel.connect(Gdk.KEY_Q, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE, self._quit )
#lock the group
accel.lock()
#add the group to the window
self.add_accel_group(accel)
#create an accellerator group for this window
accel = Gtk.AccelGroup()
#add the ctrl+q to quit
accel.connect(Gdk.KEY_Q, Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE, self._quit )
#lock the group
accel.lock()
#add the group to the window
self.add_accel_group(accel)
#create an entry for accepting user text
entry = Gtk.Entry()
entry.connect('changed', self.entry_changed)
entry.connect("key-press-event",self.entry_keypress)
#layout with a vbox
vbox = Gtk.VBox()
#create listStore to hold data
self.store = Gtk.ListStore(str)
#create tree view for the store
self.tree = Gtk.TreeView.new_with_model(self.store)
#self.tree.set_text_column(0)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Commands", renderer, text=0)
self.tree.append_column(column)
self.tree.set_search_equal_func(self.search_equal)
#set the entry as the search entry
self.tree.set_search_entry(entry)
#create a scrolled box
scroller = Gtk.ScrolledWindow()
#add tree to scrolled box
scroller.add(self.tree)
vbox.pack_start(entry, False, False, 0)
vbox.pack_start(scroller, True, True, 0)
self.add(vbox)
# load the store
self.filter_store()
#do not decorate
self.set_decorated(False)
def search_equal(self, tree, column, key, iter, search_data=None):
#get the value from the store
store_val = self.store[iter][0]
#return False if a row matches! crazy.
#https://lazka.github.io/pgi-docs/Gtk-3.0/callbacks.html#Gtk.TreeViewSearchEqualFunc
return key not in store_val
def entry_keypress(self, entry, event, *args):
if event.keyval == Gdk.KEY_Tab:
#get the currently highlighted value from the tree
selection = self.tree.get_selection()
model, treeiter = selection.get_selected()
if selection:
command = (model[treeiter][0] )
print (command)
#set the entries value to command
entry.set_text(command)
#move cursor to the end of the text
entry.set_position( len(command) )
return True
elif event.keyval == Gdk.KEY_Escape:
self._quit()
elif event.keyval == Gdk.KEY_Return:
#get the text of the current selected item
selection = self.tree.get_selection()
item_text = ''
model, treeiter = selection.get_selected()
if model and treeiter:
item_text = model[treeiter][0]
#get the entry text
entry_text = entry.get_text()
#the command is the longer string
cmd = item_text if (len(item_text) > len(entry_text) ) else entry_text
#hide the UI
self.hide()
#run the command
print(cmd)
subprocess.run("sleep 0.2 && "+cmd+" &", shell=True)
#quit
self._quit()
def entry_changed(self, entry):
entry_text = entry.get_text()
#filter based on the text
self.filter_store(entry_text)
def load_executables(self):
#get the paths to search
paths = os.environ["PATH"].split(os.path.pathsep)
for path in paths:
#is this path a dir?
if os.path.isdir(path):
#loop the files in the path
for f in os.listdir(path):
#is the file executable by the user?
if os.access(os.path.join(path, f), os.X_OK):
self.executables.append(f)
#sort the executables
self.executables.sort()
def filter_store(self, string=None):
#clear the store
self.store.clear()
self.ex_count = 0
#is a string provided?
if string:
#loop the executables
for x in self.executables:
# is the string a substring of executable name?
if string in x:
# add the executable name to the liststore
storeiter = self.store.append([x])
# no string to filter add all
else:
for x in self.executables:
# add the executable name to the liststore
storeiter = self.store.append([x])
def _quit(self, *args):
#see ya later buddy!
self.emit('quit')
if __name__ == "__main__":
window = myWindow()
window.connect("destroy", Gtk.main_quit)
window.connect("quit", Gtk.main_quit)
window.show_all()
#start the main loop
Gtk.main()
Wow, trying to launch vlc
is being problematic. Alphabetically, cvlc
shows up on the list before vlc
, and when I try for a quick launch, I type "v-l-c ENTER", which launched cvlc
.
Well, removing cvlc
fixed that feature :)
jezra