Jezra.net

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:

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