Keypirinha Search CSV package

Keypirinha CSV Package

Keypirinha is an open-source fast launcher which boosts your efficiency with hotkeys, keywords, text expansion and more. It allows users to quickly find and launch applications, files, bookmarks, URLs, and even perform tasks like evaluating mathematical expressions, hashing strings, and generating random UUIDs.

It features a plugin-oriented architecture, meaning its functionality is largely driven by plugins, which help populate its internal database (the Catalog) with searchable items. Users can configure Keypirinha manually via INI files, as it does not have a graphical interface for configuration.

Unfortunately, Keypirinha appears to have been discontinued, with no new updates since November 8, 2020 (v2.26) although it remains fully functional.

Last week I was wondering how I can easily search and copy values from different csv files. After some hours of searching I wasn’t able to detect any existing solution. So I set to deploy my own Search CSV package for Keypirinha.

Installing Keypirnha

1) Download latest release (v2.26) from the official page (I prefer the full version (portable))
2) Extract it to your desired location
3) Run keypirinha.exe from the extracted folder Keypirinha
4) The application’s icon will be visible on the taskbar—if it’s not immediately seen, check the hidden icons section.
5) Clicking the app’s icon will bring up the launcher.

6) Start typing configure, then choose the Keypirinha: Configure option from the suggestions list.

7) The first time you select this action, two text editors will open. The left editor provides a read-only documentation reference, while the right displays your current configuration. The Official Documentation is comprehensive and well-structured, offering various customization options such as launch shortcut combinations, package management, styling and more.

Keypirinha Configuration Editor

Configuring Keypirnha

My preferred configuration scheme, running on Windows 11, is the following:

[app]
hotkey_run = Ctrl + Shift + S
hotkey_paste = Ctrl + Shift + A
hotkey_history = Ctrl + Shift + E
write_log_file = yes
ignored_packages = <all>,-PhoneBook

[gui]
theme = DarkSpotlight
always_on_top = yes
hide_on_focus_lost = yes

[theme/DarkSpotlight]
font_face = SF Pro Text, Helvetica, Tahoma, Arial, Segoe UI

font_small_size = 12
font_small_style = cleartype
font_snormal_size = 14
font_snormal_style = cleartype
font_normal_size = 16
font_normal_style = cleartype
font_large_size = 22
font_large_style = cleartype

mono_font_face = Fira Code, Consolas

color_background = #111
color_foreground = #fff
color_faded = #888

color_accent = #fdf16f

color_textbox_back = ${color_background}

color_listitem_back = ${color_background}
color_listitem_title = ${color_foreground}
color_listitem_desc = ${color_accent}

color_listitem_selected_back = #116dd6
color_listitem_selected_title = ${color_foreground}
color_listitem_selected_desc = ${color_accent}

listitem_padding = 10

opacity_back = 92

layout = list_icon

satellite_show = never

Specifically my configuration:
1) Further enhances the appearance by slightly increasing font size and spacing.
2) Keeps the launcher always on top of other applications and hides it when it loses focus.
3) Sets launch shortcut combination to {Ctrl}+{Shift}+{S}, launch and automatically paste current clipboard to {Ctrl}+{Shift}+{A} and launch showing the history of previous selected items to {Ctrl}+{Shift}+{E}
4) It disables all preinstalled packages, except for the one that will be deployed in the next steps to search within a specific CSV file.

Packages

Keypirinha is shipped with some packages, also called Official Packages that are enabled by default. You may find some of them really interesting and helpful for your daily tasks. You can find more for the shipped packages in the Official Documentation and you can enable any package at will, by appending it’s Name prefixed with a hypen(-) to the ignored_packages = similar to the PhoneBook package in the configuration mentioned earlier.

Search CSV Package

Official Documentation also describes how to create a New Package. This implementation searches a CSV file for the text entered in the launcher and displays a list of possible matches. When a match is selected, the corresponding value is copied to the clipboard. Everything is fully configurable through properties, requiring no code modifications, and it’s completely portable—no installation needed.

For this demo, I will create a package named PhoneBook (case-sensitive). You can create as many packages as needed, especially if working with multiple CSV files. Just make sure to name your classes exactly the same as your package to prevent conflicts.

Instructions

1) First navigate to the directory where you extracted Keypirinha. And create a new folder under the portable/Profile/packages/ named PhoneBook.
2) In this folder move your CSV file which will be used for searching. (I generated one from Mockaroo.com with 1000 records)
3) In this folder create a new Python filed named main.py and append the following code:

import keypirinha as kp
import keypirinha_util as kpu
import csv, json
import os

# EDIT PROPERTIES BASED ON YOUR OWN FILE AND REQUIREMENTS
# specify columns to search, leaving it empty will search every column
# less columns will result in faster resolve times
SEARCH_COLUMNS = ['first_name', 'last_name']
CASE_SENSITIVE = False
# suggestions will appear after typing 3 characters
MINIMUM_CHARACTERS_INPUT = 3
# maximum number of suggestions to display
MAX_RESULTS = 5

# Each result has two rows, the first one is the label and the second the description. If you define more than one columns it will join them using space
LABEL_COLUMNS = ['first_name','last_name']
DESCRIPTION_COLUMNS = ['phone_number','email', 'company']
# when you hit enter or double click this column will be copied to clipboard
TARGET_COLUMNS = ['phone_number']

# Where your CSV file is located
RELATIVE_FILEPATH = "data.csv" # set it to None and it will use the FILEPATH instead
FILEPATH = "c:\data.csv" # is not used if RELATIVE_FILEPATH is defined
# ------------------- END OF PROPERTIES -------------------

class PhoneBook(kp.Plugin):

    def __init__(self):
        super().__init__()
        self._debug = True
        self.csv_data = []

    def on_start(self):
        self.dbg("PhoneBook started")
        if(RELATIVE_FILEPATH):
            self.csv_data = self._load_csv(os.path.join(os.path.dirname(os.path.realpath(__file__)), RELATIVE_FILEPATH))
        else:
            self.csv_data = self._load_csv(FILEPATH)

        if self.csv_data:
            self.dbg("CSV file loaded successfully.")
        else:
            self.warn("Failed to load CSV file.")

    def on_catalog(self):
        self.dbg("on_catalog called")
        self.set_catalog([self.create_item(
            category=kp.ItemCategory.USER_BASE,
            label="Search CSV",
            short_desc="Search CSV file",
            target="search_csv",
            args_hint=kp.ItemArgsHint.REQUIRED,
            hit_hint=kp.ItemHitHint.KEEPALL
        )])

    def on_suggest(self, user_input, items_chain):
        count = 0
        if len(user_input) < MINIMUM_CHARACTERS_INPUT:
            return

        if not CASE_SENSITIVE:
            search_term = user_input.lower()
        else:
            search_term = user_input

        if not items_chain and search_term:
            suggestions = []

            for row in self.csv_data:
                # Early termination if we've reached our limit
                if len(suggestions) >= MAX_RESULTS:
                    break

                for col in self.search_columns:
                    cell_value = row[col] if CASE_SENSITIVE else row[col].lower()
                    if search_term in cell_value:
                        label = ' '.join(str(row[col]) for col in LABEL_COLUMNS if col in row)
                        description = ' '.join(str(row[col]) for col in DESCRIPTION_COLUMNS if col in row)
                        suggestions.append(self.create_item(
                            category=kp.ItemCategory.USER_BASE,
                            label=label,
                            short_desc=description,
                            target=json.dumps(row),
                            args_hint=kp.ItemArgsHint.FORBIDDEN,
                            hit_hint=kp.ItemHitHint.KEEPALL 
                        ))
                        break            

            self.set_suggestions(suggestions, kp.Match.ANY, kp.Sort.NONE)

    def on_execute(self, item, action):
        self.dbg(f"Selected row: {item.target()}")
        # Handle cases where modifier is None
        row = json.loads(item.target())

        clipboard = ' '.join(str(row[col]) for col in TARGET_COLUMNS if col in row)

        kpu.set_clipboard(clipboard)

    def _load_csv(self, path):
        self.dbg(f"Loading CSV file from: {path}")
        if(not os.path.exists(path)):
            self.warn(f"CSV file: {path} does not exist")
            raise RuntimeError("Plugin loading aborted because CSV file does not exist")            
        try:
            with open(path, newline='', encoding='utf-8') as csvfile:
                # Peek at the first line to determine if we have headers
                first_line = csvfile.readline()
                csvfile.seek(0)  # Rewind to beginning

                if first_line.strip():  # If file is not empty
                    # Try to determine if first line contains headers
                    sniffer = csv.Sniffer()
                    has_header = sniffer.has_header(first_line)

                    if has_header:
                        # Use DictReader if we have headers
                        csvreader = csv.DictReader(csvfile)
                        self.headers = csvreader.fieldnames
                        data = list(csvreader)
                        all_columns = list(set(SEARCH_COLUMNS + LABEL_COLUMNS + DESCRIPTION_COLUMNS + TARGET_COLUMNS))
                        for col in all_columns:
                            if not col in self.headers:
                               self.warn(f"CSV File does not contain column: {col}")
                               raise RuntimeError("Plugin loading aborted due to missing column")                                
                    else:
                       self.warn("CSV File has no headers! Plugin will not load")
                       raise RuntimeError("Plugin loading aborted due to missing headers")

                    # Set search columns
                    if len(SEARCH_COLUMNS)==0:
                        self.search_columns = self.headers if self.headers else list(range(len(data[0])))
                    else:
                        self.search_columns = SEARCH_COLUMNS

                    self.dbg(f"Will search in the following columns:  {self.search_columns}")

                    return data
                else:
                   self.warn("CSV File os empty")
                   raise RuntimeError("Plugin loading aborted because CSV file is empty")

        except Exception as e:
            self.warn(f"Error loading CSV file: {e}")
            raise RuntimeError("Plugin loading aborted")

The code is self-explanatory and the only things you need to modify are the global constants (UPPERCASE properties).

Results

The final result is a launcher activated by pressing {Ctrl}+{Shift}+{S}, allowing you to start typing a search name or last name. If you’ve already copied the name, pressing {Ctrl}+{Shift}+{E} will launch the app and automatically paste the clipboard contents. Once you locate the result, double-clicking or selecting it with the arrow keys and pressing Enter will copy the telephone number to the clipboard.

If you want you can use Search CSV to deploy separate packages for more than one CSV files. Just remember to name your package folder and your Class (in python code) with the same name to avoid conflicts.

Troubleshooting

The code is using the recommended debugging functionality, so using Keypirinha’s Console (By right clicking taskbar’s icon and selecting Open Console) you will be able to see Debug and Error Messages.

Keypirinha debugging

Code

The full code is available on github

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments