JustToThePoint English Website Version
JustToThePoint en español
Colaborate with us

Nix for development II. Espanso clone for Wayland

If you find yourself in a hole, stop digging, Will Rogers

I’d far rather be happy than right any day, Douglas Adams, The Hitchhiker’s Guide to the Galaxy

Introduction to NixOS

NixOS is a unique, innovative, and powerful Linux distribution that leverages the Nix package manager. Unlike traditional Linux distributions that update packages and system configurations in-place, NixOS uses a purely functional and declarative approach to define the system state, reducing the risk of system breakage.

NixOS is a Linux distribution that uses the Nix package manager to handle packages and system configuration in a purely functional manner. This approach ensures that builds are reproducible and that the system state can be reliably replicated or rolled back.

  1. Immutable Design: It uses a configuration file (usually /etc/nixos/configuration.nix) to define the entire system state, including installed packages, services, and system settings. This makes the system “immutable” in the sense that you don’t manually modify system files. Instead, you declare what you want, and NixOS builds the system from that declaration.
  2. Atomic Updates: When you run nixos-rebuild switch, the system builds a new generation of the environment. If the build is successful, you can switch to this new environment atomically. This means that either the entire update is applied, or nothing changes at all, preventing partial updates that could leave the system in an inconsistent state: sudo nixos-rebuild switch, build and switch to the new generation. If anything goes wrong, you can easily roll back to a previous generation: sudo nix-env ‐‐rollback This rollback is seamless because each generation is stored separately.

    sudo nix-env ‐‐rollback is a command used to revert your system to the previous configuration. It’s a powerful tool for undoing unintended changes or recovering from failed package installations. Before rolling back, consider if there’s a more targeted solution, like uninstalling specific packages or reverting configuration files.

  3. Purely Functional System Configuration: NixOS uses a functional paradigm for configuration. This means that changes are expressed as pure functions from a configuration to a system state, ensuring reproducible builds and easy rollback.

Non-linear Autonomous Systems

Nix shell II

nix-shell is a command provided by the Nix package manager that allows users to create temporary, isolated development environments. It is particularly useful for running commands or scripts with specific dependencies without permanently installing those packages on your system.

When you enter a nix-shell, it sets up a temporary environment (a ephemeral box) with the specified packages (dependencies) available in your PATH. Once you exit, poof, it’s all gone. No leftover packages cluttering your main system, no messy version conflicts, and no stress whatsoever about messing with your environment.

By default, if you run nix-shell without arguments (if path is not given), nix-shell defaults to a file shell.nix in the current directory.

A Nix shell is a powerful development environment tool that allows you to:

Temporary interactive shell

A temporary interactive shell is created when you run nix-shell without specifying any additional arguments. In this case, nix-shell will use a default environment, which typically includes only the basic tools available in the Nix environment, which can be very handy for ad-hoc tasks or testing.

nmaximo7 on nixos ~ took 10s
❯ nix-shell -p bc
# The nix-shell command create a temporary interactive shell where you can use the tools and packages that are part of the default Nix environment.
# However, if you want to specify certain packages or settings, you can pass a -p (--packages) option.
# This environment lasts only for that session.
# Once you exit (Ctrl+D or exit), you lose access to any packages you added for that session unless you run nix-shell again.

[nix-shell:~]$ bc
bc 1.07.1
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006, 2008, 2012-2017 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
3*4
12

Limit Cycles

Using a shell.nix File for an isolated project environment

A shell.nix file is a configuration file used by nix-shell to define declaratively the environment for a specific project. This file is usually placed in the root directory of the project and specifies all the necessary dependencies and configurations.

When you run nix-shell in the directory containing shell.nix, Nix will enter a shell with all dependencies specified in that file.

# To create an isolated environment on NixOS with the necessary Python packages, we'll use a file named shell.nix.
# pkgs will default to the Nix packages set imported from nixpkgs.
# This is the official Nixpkgs repository and it is a common pattern in Nix expressions.
{ pkgs ? import  {} }:

pkgs.mkShell {
# This function creates a shell derivation with the specified packages (dependencies) and configurations.
  name = "espanso-replacer-env";
  # This assigns a name to the shell environment, which can help you identify it later.
  buildInputs = with pkgs; [
    pkgs.neofetch # A command-line tool that displays system information. It is useful for quick checks on your environment.
    pkgs.xclip # A utility for managing the clipboard. It is required by the pyperclip Python library.
    (python3.withPackages (ps: with ps; [
    # This function allows you to specify Python packages. Here, it includes:
      autokey  # Future integration with autokey
      pyperclip # Pyperclip provides a cross-platform Python module for copying and pasting text to the clipboard.
    ]))
    curl # A command-line tool for transferring data; it can be useful for making API calls or downloading resources.
  ];

  shellHook = ''
    echo "Welcome to the Espanso Replacer environment!"
    python espanso_replacer.py
  '';
}

The shellHook runs commands whenever you enter the shell. It provides a welcome message and runs the espanso_replacer.py script automatically.

Usage:

  1. Navigate to Your Project Directory: cd /path/to/your/project-directory. Make sure that both shell.nix and your Python script (espanso_replacer.py) are in the same directory.
  2. Enter the Nix Shell: nix-shell. This command will build the environment as specified in shell.nix and drop you into a shell where your Python script runs automatically.

The Python “Text Expander” Clone

A text expander is an application that replaces repetitive typing tasks with a few keystrokes. By typing in a custom abbreviation, a text expander quickly inserts snippets of text, such as words, phrases, paragraphs, quotes, verses, blocks of code, or even templates.

Given our specific case, it’s very efficient to save the data in JSON format. We are going to structure the JSON as a dictionary where each trigger is a key mapped to its replacement value. This allows for constant-time lookup when searching for triggers. Each trigger (e.g., “:name”) is a unique key in the JSON object.

{
  ":name": "Doris D. Taftn",
  ":dni": "31661375N",
  ":address": "920 Pratt Way",
  ":telephone": "+33(151)-4602392",
  ":email": "jiwines8@yopmail.com",
  ":Postal Code": "81632",
  [...]
}

We’ll create a Python script that:

  1. Reads the JSON file (myespanso.json).
  2. Continuously listens for user input.
  3. Responds with the corresponding replacement if the trigger is found.
  4. Handles unknown triggers gracefully.
import json
import pyperclip
import datetime
import subprocess

def process_replacement(user_input, replacement):
    """
    Process the replacement string by handling dynamic placeholders (e.g., date, time, weather) and replaces them with the current values.

    Args:
        user_input (str): The trigger entered by the user (e.g., ":date").
        replacement (str): The replacement string from the JSON configuration.

    Returns:
        str: The processed replacement string, with placeholders replaced by actual values.
    """
    if "{{" in replacement and "}}" in replacement:
        # Checks if the replacement text includes placeholders like "{{" and "}}".
        if user_input == ":date":
            # If the user typed :date,...
            # it sets already_process_replacement to today’s date in %d/%m/%y format.
            already_process_replacement = datetime.datetime.now().strftime("%d/%m/%y")
        elif user_input == ":time":
            # If the user typed :time,
            # it sets already_process_replacement to %H:%M.
            already_process_replacement = datetime.datetime.now().strftime("%H:%M")
        elif user_input == ":weather":
            # If the user typed :weather,...
            # it attempts to fetch data via curl http://wttr.in/?format=3.
            try:
                already_process_replacement = subprocess.check_output(
                    ["curl", "http://wttr.in/?format=3"],
                    stderr=subprocess.DEVNULL
                ).decode().strip()
            except Exception as e:
                already_process_replacement = "Weather service unavailable."
        else:
            # Otherwise, it returns the original replacement text.
            already_process_replacement = replacement
    else:
        already_process_replacement = replacement

    return already_process_replacement

def load_triggers(json_file):
    """
    Load triggers and their associated replacements from a JSON file.

    This function attempts to read a JSON file containing trigger-replacement pairs.
    It handles common file-related errors and returns a dictionary mapping triggers to their respective replacements.

    Args:
        json_file (str): Path to the JSON file containing trigger-replacement mappings.

    Returns:
        dict: Dictionary mapping triggers to replacements, an empty dictionary or an error.
    """
    try:
        with open(json_file, 'r', encoding='utf-8') as f:
            triggers = json.load(f)
        return triggers
    # The script gracefully handles scenarios where the JSON file isn't found or contains invalid JSON.
    except FileNotFoundError:
        print(f"Error: The file {json_file} was not found.")
        return {}
    except json.JSONDecodeError as e:
        print(f"Error parsing JSON: {e}")
        return {}

def main():
    """
    Main function to run the Espanso Text Expander.

    This function loads triggers from a specified JSON file and enters a loop where the user can input triggers to get their replacements. The replacements are processed and copied to the clipboard.

    It handles user input, error conditions, and allows for graceful exit.
    """
    json_file = '../Espanso/myespanso.json' # Ensure this path is correct
    triggers = load_triggers(json_file) # Load triggers from the JSON file

    if not triggers:
        print("No triggers loaded. Exiting.")
        return

    print("Espanso Replacer is running. Type your triggers below:")
    print("Press Ctrl+C to exit.\n")

    try:
        while True:
            user_input = input("Enter trigger: ").strip()
            if user_input in triggers:
            # If the user’s input matches a key in the triggers dictionary, it calls process_replacement(), prints and copies the result to the clipboard (via pyperclip.copy).
                replacement = triggers[user_input]
                already_process_replacement = process_replacement(user_input, replacement)
                print(f"Replacement: {already_process_replacement}\n")
                pyperclip.copy(f"{already_process_replacement}")
                # Copy the processed replacement to the clipboard

            else:
                print("Trigger not found.\n")
    except KeyboardInterrupt:
        print("\nExiting Espanso Replacer. Goodbye!")

if __name__ == "__main__":
    main()

For better convenience, you may want to define shell aliases in your NixOS or Home Manager config for Zsh (~/dotfiles/zsh.nix):

{ config, pkgs, ... }:

let
  username = config.home.username;
  dotDir = "/home/${username}/dotfiles";
in

{
  programs.zsh = {
    enable = true;
    enableCompletion = true;
    [...]

    shellAliases = {
      ll = "exa -a --icons";
      tree = "exa --tree --icons";
      myespanso = "cd /home/nmaximo7/dotfiles/scripts && nix-shell";

    };

Beyond exact matching

In the previous version, the instruction if user_input in triggers: only performs an exact match. If we want to find the trigger in your dictionary that is closest to the user input (e.g., the user mistakenly typed “:naem” instead of “name”), rather than an exact match, we can leverage difflib.get_close_matches:

from difflib import get_close_matches
# The difflib module provides functions to compare sequences and find close matches.
# We'll use difflib.get_close_matches to find the trigger that most closely resembles the user input.
# This allows the program to suggest replacements even if the user makes minor typing errors.
def main():
    json_file = '../Espanso/myespanso.json'  # Ensure this path is correct
    triggers = load_triggers(json_file)
    # The path to the JSON file containing the triggers and their corresponding replacements is defined in the json_file variable.
    # Then, The load_triggers function is called to read this data into a dictionary.

    if not triggers:
        # This check ensures that the program exits gracefully if no triggers are loaded, preventing errors during the matching process.
        print("No triggers loaded. Exiting.")
        return

    print("Espanso Replacer is running. Type your triggers below:")
    print("Press Ctrl+C to exit.\n")

    try:
        while True:
            # A loop is initiated to continuously accept input from the user. strip() is used to remove any leading or trailing whitespace from the input.
            user_input = input("Enter trigger: ").strip()
            trigger_keys = triggers.keys()

            # This line uses get_close_matches to find the closest matching trigger based on the user input. The parameters specify that only the closest or best match (n=1) is needed...
            # and it should only return matches with a similarity ratio of at least 0.6.
            closest_matches = get_close_matches(user_input, trigger_keys, n=1, cutoff=0.6)

            if closest_matches:
                # If a match is found, the corresponding replacement text is retrieved from the triggers dictionary.
                closest_trigger = closest_matches[0]
                replacement = triggers[closest_trigger]

                # The replacement text is processed, printed to the console, and copied to the clipboard using pyperclip.copy.
                already_process_replacement =  process_replacement(user_input, replacement)
                print(f"Replacement: {already_process_replacement}\n")
                pyperclip.copy(f"{already_process_replacement}")

            else:
                # If no close matches are found, the program informs the user that the trigger was not recognized.
                print("Trigger not found.\n")
    except KeyboardInterrupt:
    # The program handles the interruption KeyboardInterrupt (e.g., when the user presses Ctrl+C)
    # and exit gracefully, providing a friendly goodbye message.
        print("\nExiting Espanso Replacer. Goodbye!")
Bitcoin donation

JustToThePoint Copyright © 2011 - 2025 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.