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
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.
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.
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:
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
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:
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:
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";
};
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!")