Banner of d9d1273af48e772beefa.png

My Personalized Dmenu Launcher


Category: Linux

📅 February 16, 2023   |   👁️ Views: 46

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.


← ffmpeg command: basic and advanced usage and script examples Best Animes with Overpowered MCs →