Banner of screnshot: my dmenu program launcher, Personalized dmenu Launcher: Effortlessly Access Your Favorite Programs on Linux

My Personalized Dmenu Launcher


Category: Linux

Date: February 2023
Views: 889


My personalized dmenu launcher is a convenient tool that allows me to quickly launch frequently used programs. The launcher gives priority to programs that are frequently used, making them appear first in the dmenu list. Each time a program is executed, its usage is incremented in a SQLite database, ensuring that it will appear at the top of the list in the future.

Usage

There are three ways to use the launcher:

  • launcher.sh update: Updates the launcher database with the latest program information. This command should be run whenever new programs are installed or removed.
  • launcher.sh list: Displays a list of all programs in the launcher database.
  • launcher.sh: Launches the dmenu and allows the user to select a program to run.

The launcher is designed to be used with the i3 window manager. To bind the launcher to a keyboard shortcut in i3, add the following line to the i3 config file:

    
bindsym $mod+x exec --no-startup-id /path/to/launcher.sh
    

The Bash Script

Below is the Bash script that powers the launcher. It uses a Python script to interact with the SQLite database. Enjoy

    
#!/bin/sh

WORKINGDIR="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
pythonScript="${WORKINGDIR}/database.py"

clean_up(){
	while read -r file ; do
		if ! [ -f "$file" ] ; then
			echo "removed: $file"
			python "$pythonScript" del "$file"
		fi
	done <<< "$( python "$pythonScript" get | jq -r '.[]|.location' )"
}

update(){
	tmpfile="/tmp/mydmenu_update_file.csv"
	rm "$tmpfile" 2>/dev/null
	while IFS= read -r -u 3 -d ':'  dir ; do
		#echo "===================================== $dir"
		find "$dir" -type f -iname "*.desktop" -exec grep -L 'NoDisplay=true' {} \; 2>/dev/null |
		while read -r file ; do
			location="$file"
			tname="$(sed -n '/^[nN]ame=/p' "$file" | sed 's/^[nN]ame=//' | tr '\n' ' ' | sed 's/[ \t]*$//' )"
			name=$(echo "$tname" | cut -d' ' -f1,2)
			#echo $file === $name
			exec="$(sed -n '/^[eE]xec=/{s/^[eE]xec=//;s/ %.*//;p;q}' "$file" | tr '\n' ' '  | sed 's/[ \t]*$//' )"
			[[ -z "$exec" ]] && continue
			description="$(sed -n '/^[cC]ategories=/p' "$file" | sed 's/^[cC]ategories=//'  | tr '\n' ' '  | sed 's/[ \t]*$//' )"
			echo "$name,$exec,$location,$description $tname" >> "$tmpfile"
		done
	done 3<<< "$HOME/.local/share/applications:/usr/share/applications:/usr/local/share/applications:"
	python "$pythonScript" update
}

if [ "$1" = "update" ] ; then
	update
	clean_up
	exit
fi

if [ "$1" = "list" ] ; then
	python $WORKINGDIR/database.py get | jq -r '.[]|select(.name)' | less
	exit
fi


data=$(python "$pythonScript" get)
selected=$( echo "$data" | jq -r '.[]|.name' | dmenu -i -fn monospace:20 -p run )
[[ -n "$selected" ]] || exit
program=$(echo "$data" | jq -r ".[]|select(.name|match(\"$selected\"))|.exec" )
python "$pythonScript" increment "$selected"
#echo "$selected"
#echo "$program"
echo "$program" | ${SHELL:-"/bin/sh"} &

    

Python Script

    
import sqlite3
import json
import csv
import sys
import os
import inspect

def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)

def connectDB():
    try:
        path = os.path.dirname(os.path.realpath(__file__)) + "/database.db"
        con = sqlite3.connect(path)
        return con
    except sqlite3.Error as error:
        eprint("@%s: %s" % (inspect.stack()[0][3], error))


def createDB():
    con = connectDB()
    cursor = con.cursor()
    cursor.execute( """
        CREATE TABLE IF NOT EXISTS "programs" (
                "name"	TEXT NOT NULL,
                "exec"	TEXT UNIQUE,
                "location"	TEXT,
                "description"	TEXT,
                "uses"	INTEGER DEFAULT 0
        ); """
    )
    con.commit()
    cursor.close()
    con.close()

def update():
    try:
        con = connectDB()
        cursor = con.cursor()
        with open("/tmp/mydmenu_update_file.csv") as ifile:
            reader = csv.reader(ifile,delimiter = ',')
            for f in reader:
                cursor.execute( """ INSERT INTO programs
                 (name,exec,location,description) VALUES (?, ?, ?, ? )
                 ON CONFLICT(location) DO UPDATE SET
                 name= ? , exec= ? , location= ? , description= ?
                 """ , (f[0],f[1],f[2],f[3],f[0],f[1],f[2],f[3]) )
        con.commit()
        cursor.close()
    except sqlite3.Error as error:
        eprint("@%s: %s" % (inspect.stack()[0][3], error))
        print(error)
    finally:
        if con:
            con.close()


def clean_up(p):
    try:
        con = connectDB()
        cursor = con.cursor()
        str_query = """ DELETE FROM programs WHERE location = ? """
        cursor.execute(str_query, (p,) )
        con.commit()
        cursor.close()
    except sqlite3.Error as error:
        eprint("@%s: %s" % (inspect.stack()[0][3], error))
    finally:
        if con:
            con.close()


def increment(p):
    try:
        con = connectDB()
        cursor = con.cursor()
        str_query = """ UPDATE programs SET uses = uses + 1 WHERE name = ? """
        cursor.execute(str_query, (p,) )
        con.commit()
        cursor.close()
    except sqlite3.Error as error:
        eprint("@%s: %s" % (inspect.stack()[0][3], error))
    finally:
        if con:
            con.close()


def get():
    try:
        con = connectDB()
        con.row_factory = sqlite3.Row
        cursor = con.cursor()
        str_query = """ SELECT * from programs ORDER BY uses DESC """
        cursor.execute(str_query)
        rows = cursor.fetchall()
        cursor.close()
        print(json.dumps( [ dict(x) for x in rows] ))
    except sqlite3.Error as error:
        eprint("@%s: %s" % (inspect.stack()[0][3], error))
    finally:
        if con:
            con.close()


def myfuncSwitch(arg):
    cmd = arg[1]
    switcher = {
        "update": update,
        "get": get,
        "del": clean_up,
        "increment": increment,
    }
    func = switcher.get(cmd)
    func(*arg[2:])


if __name__ == "__main__":
    createDB()
    myfuncSwitch(sys.argv)

    

If you would like to contribute to this personalized dmenu launcher, you can find the Github repository at my github repository. The repository contains the source code for this launcher, and you can use it to make any changes or improvements that you see fit. Additionally, you can submit bug reports or feature requests in the Github issues section. Contributions are always welcome and appreciated, and they can help make this launcher even better. Thank you for your interest in this project, and I look forward to seeing your contributions.

I hope you found this article on my personalized dmenu launcher informative and useful. If you have any ideas or insights that you would like to share, or if you would like to share your experience with this launcher, please feel free to leave a comment below. Your feedback is valuable, and it can help improve this launcher and make it more useful for everyone. Additionally, if you have any questions or concerns, please do not hesitate to ask in the comments section.



889 views

Previous Article Next Article

0 Comments, latest

No comments.