Tag: customization

  • A Trio of Pibooth Plugins

    A Trio of Pibooth Plugins

    This past weekend I did a thing:

    One of the things that I had done, as previously blargged about here, is having a Pibooth Photo Booth.

    I ended up writing three custom plugins that I’m going to share here. I had plenty of help from ChatGPT because I hate Python and it’s the worst language invented and it should be destroyed.

    I also ended up hacking a bit of Pibooth core because its hook/plugin system was a bit lacking for my needs, but that was only around moving around some of the touchscreen UI items for a better user experience. But enough about that, here’s my three plugins:

    Fullscreen Toggle

    One of the things I wanted to be able to easily do, running a keyboard/mouseless touchscreen Raspbery Pi was to be able to jump in and out of Fullscreen mode to do things like debug printers, change Wifi, etc. So I pulled this plugin out of the ether. It allows you to touch the top right corner (or click, if that’s your thing) to toggle between fullscreen and windowed mode.

    import pygame
    from pibooth import hookimpl
    from pibooth.utils import LOGGER
    
    __version__ = "0.0.1"
    
    """
    Plugin to toggle fullscreen/windowed mode by tapping top right corner in take-a-picture screen.
    """
    
    # Fractional region size
    REGION_WIDTH_FRAC = 0.10  # 10% width
    REGION_HEIGHT_FRAC = 0.10  # 10% height
    
    @hookimpl
    def state_wait_do(cfg, app, win, events):
        """
        In the wait state (take-a-picture), detect a tap/click in the top-right corner
        and toggle fullscreen mode.
        """
        # Only on the initial "take a picture" screen (wait state)
        # events: list of pygame events
        for ev in events:
            if ev.type in (pygame.MOUSEBUTTONUP, pygame.FINGERUP):
                # Determine click position
                if hasattr(ev, 'pos'):
                    x, y = ev.pos
                else:
                    sw, sh = win.surface.get_size()
                    x, y = ev.x * sw, ev.y * sh
                sw, sh = win.surface.get_size()
                # Define top-right region
                region_x0 = sw * (1 - REGION_WIDTH_FRAC)
                region_y1 = sh * REGION_HEIGHT_FRAC
                if x >= region_x0 and y <= region_y1:
                    # Toggle fullscreen
                    try:
                        win.toggle_fullscreen()
                        LOGGER.info("[fullscreen_toggle] Toggled fullscreen mode")
                    except Exception as e:
                        LOGGER.error(f"[fullscreen_toggle] Failed to toggle fullscreen: {e}")
                    # Consume event
                    return ev
        # Return None to fall back to default behaviorCode language: Python (python)

    Upload to WordPress

    Well, yeah. Obvi! This plugin uploads the photos to your WordPress Media library, with an optional tag and optional post object to attach them to. It will also update the URL so that it works with the “QR Code” plugin (not mine, from Pihole or something, IDK). You just have to add some of the options to your Pihole config file, like the hostname, username, application password, etc.

    import os
    import http.client
    import base64
    import json
    import pibooth
    from pibooth.utils import LOGGER
    
    __version__ = "0.0.4"
    
    SECTION = 'WORDPRESS'
    PICTURE_SECTION = 'PICTURE'
    
    @pibooth.hookimpl
    def pibooth_configure(cfg):
        """Declare the new configuration options"""
        cfg.add_option(SECTION, 'wordpress_host', '',
            "WordPress site host (e.g., 'yourwordpresssite.com')")
        cfg.add_option(SECTION, 'wordpress_username', '',
          "WordPress username")
        cfg.add_option(SECTION, 'wordpress_password', '',
            "WordPress application password")
        cfg.add_option(SECTION, 'wordpress_parent_id', '',
            "(Optional) WordPress post or page ID to attach the uploads",
            "Post or Page ID (optional)", '')
        cfg.add_option(SECTION, 'image_title', '',
            "(Optional) Title for the uploaded image",
            "Image title (optional)", '')
        cfg.add_option(SECTION, 'image_caption', '',
            "(Optional) Caption for the uploaded image",
            "Image caption (optional)", '')
        cfg.add_option(SECTION, 'wordpress_post_tag', '',
            "(Optional) Post tag taxonomy term slug to assign to the uploaded media",
            "Post tag (optional)", '')
    
    @pibooth.hookimpl
    def pibooth_startup(app, cfg):
        """Initialize the WordPress uploader plugin"""
        app.previous_picture_url = None
        wordpress_host = cfg.get(SECTION, 'wordpress_host')
        wordpress_username = cfg.get(SECTION, 'wordpress_username')
        wordpress_password = cfg.get(SECTION, 'wordpress_password')
        wordpress_parent_id = cfg.get(SECTION, 'wordpress_parent_id')
        wordpress_post_tag = cfg.get(SECTION, 'wordpress_post_tag')
        
        image_title = ''
        image_caption = ''
    
        if cfg.has_option(SECTION, 'image_title') and cfg.get(SECTION, 'image_title'):
            image_title = cfg.get(SECTION, 'image_title')
            LOGGER.info(f"Using image title from WORDPRESS config: {image_title}")
        elif cfg.has_option(PICTURE_SECTION, 'footer_text1') and cfg.get(PICTURE_SECTION, 'footer_text1'):
            image_title = cfg.get(PICTURE_SECTION, 'footer_text1')
            LOGGER.info(f"Using image title from GENERAL config: {image_title}")
        else:
            LOGGER.info("Image title missing!")
        
        if cfg.has_option(SECTION, 'image_caption') and cfg.get(SECTION, 'image_caption'):
            image_caption = cfg.get(SECTION, 'image_caption')
            LOGGER.info(f"Using image caption from WORDPRESS config: {image_caption}")
        elif cfg.has_option(PICTURE_SECTION, 'footer_text2') and cfg.get(PICTURE_SECTION, 'footer_text2'):
            image_caption = cfg.get(PICTURE_SECTION, 'footer_text2')
            LOGGER.info(f"Using image caption from GENERAL config: {image_caption}")
        else:
            LOGGER.info("Image caption missing!")
    
        if not wordpress_host:
            LOGGER.error(f"WordPress host not defined in [{SECTION}][wordpress_host], uploading deactivated")
        elif not wordpress_username:
            LOGGER.error(f"WordPress username not defined in [{SECTION}][wordpress_username], uploading deactivated")
        elif not wordpress_password:
            LOGGER.error(f"WordPress password not defined in [{SECTION}][wordpress_password], uploading deactivated")
        else:
            LOGGER.info("WordPress uploader plugin installed")
            app.wordpress_host = wordpress_host
            app.auth_token = base64.b64encode(f"{wordpress_username}:{wordpress_password}".encode()).decode()
            app.wordpress_parent_id = wordpress_parent_id if wordpress_parent_id else None
            app.wordpress_post_tag = wordpress_post_tag if wordpress_post_tag else None
            LOGGER.info(f"Post tag set to: {app.wordpress_post_tag}")
            app.image_title = image_title
            app.image_caption = image_caption
    
            LOGGER.info(f"Image title set to: {app.image_title}")
            LOGGER.info(f"Image caption set to: {app.image_caption}")
    
    @pibooth.hookimpl
    def state_processing_exit(app, cfg):
        """Upload picture to WordPress media library"""
        # Skip plugin if disabled in config
        # if not (cfg.has_section('CUSTOM_PROCESSING') and cfg.getboolean('CUSTOM_PROCESSING', 'enabled')):
        #    return
            
        if hasattr(app, 'wordpress_host') and hasattr(app, 'auth_token'):
            name = os.path.basename(app.previous_picture_file)
    
            with open(app.previous_picture_file, 'rb') as fp:
                image_data = fp.read()
    
            headers = {
                'Content-Disposition': f'attachment; filename={name}',
                'Content-Type': 'image/jpeg',
                'Authorization': f'Basic {app.auth_token}'
            }
    
            # Prepare the request path with an optional parent ID
            path = "/wp-json/wp/v2/media"
            if app.wordpress_parent_id:
                path += f"?parent={app.wordpress_parent_id}"
    
            conn = http.client.HTTPSConnection(app.wordpress_host)
            conn.request("POST", path, body=image_data, headers=headers)
            response = conn.getresponse()
            
            if response.status == 201:
                response_data = json.loads(response.read().decode())
                app.previous_picture_url = response_data['source_url']
                
                LOGGER.info(f"Uploaded image URL: {app.previous_picture_url}")
    
                # Set metadata if provided
                metadata = {}
                if app.image_title:
                    metadata['title'] = app.image_title
                if app.image_caption:
                    metadata['caption'] = app.image_caption
                if app.wordpress_post_tag:
                    term_slug = app.wordpress_post_tag
                    tag_headers = {
                        'Content-Type': 'application/json',
                        'Authorization': f'Basic {app.auth_token}'
                    }
                    # Try to fetch existing tag
                    conn.request("GET", f"/wp-json/wp/v2/tags?slug={term_slug}", headers=tag_headers)
                    tag_resp = conn.getresponse()
                    if tag_resp.status == 200:
                        tags_list = json.loads(tag_resp.read().decode())
                        if tags_list:
                            tag_id = tags_list[0]['id']
                        else:
                            # Create the tag if not found
                            payload = json.dumps({'name': term_slug, 'slug': term_slug})
                            conn.request("POST", "/wp-json/wp/v2/tags", body=payload, headers=tag_headers)
                            create_resp = conn.getresponse()
                            if create_resp.status == 201:
                                tag_id = json.loads(create_resp.read().decode())['id']
                            else:
                                LOGGER.error(f"Failed to create tag {term_slug}. Status code: {create_resp.status}")
                                tag_id = None
                    else:
                        LOGGER.error(f"Failed to fetch tag {term_slug}. Status code: {tag_resp.status}")
                        tag_id = None
                    if tag_id:
                        metadata['tags'] = [tag_id]
                        LOGGER.info(f"Assigning tag ID {tag_id} to attachment")
    
                LOGGER.info(f"Metadata to be set: {metadata}")
    
                if metadata:
                    meta_headers = {
                        'Content-Type': 'application/json',
                        'Authorization': f'Basic {app.auth_token}'
                    }
                    conn.request("POST", f"/wp-json/wp/v2/media/{response_data['id']}", body=json.dumps(metadata), headers=meta_headers)
                    meta_response = conn.getresponse()
                    if meta_response.status != 200:
                        LOGGER.error(f"Failed to set metadata. Status code: {meta_response.status}, Response: {meta_response.read().decode()}")
    
                LOGGER.info(f"Successfully uploaded {name} to WordPress. URL: {app.previous_picture_url}")
            else:
                LOGGER.error(f"Failed to upload {name}. Status code: {response.status}, Response: {response.read().decode()}")
    
            conn.close()
    
    @pibooth.hookimpl
    def pibooth_cleanup(app):
        """Cleanup actions if necessary"""
        passCode language: Python (python)

    Custom Processing (More Fun!)

    Finally, I didn’t like the generic “PROCESSING” screen that happened. I wanted it to be more personal, so I rebuilt it so that it shows the last three images taken, along with a custom waiting message taken from an array:

    #!/usr/bin/env python3
    """
    custom_processing.py
    
    A PiBooth plugin that, during the 'processing' state, grabs the last
    three saved pictures from your output directory and shows them as thumbnails.
    """
    import os
    import glob
    import pygame
    import pibooth
    from pibooth.utils import LOGGER
    from pibooth.view import background
    from pibooth import pictures
    import random
    from pibooth import fonts
    
    __version__ = "0.0.1"
    
    # A list of fun processing messages
    PROCESSING_SAYINGS = [
        "Processing…",
        "Shaking it like a Polaroid picture",
        "Now developing…",
        "Cooking your memories",
        "Almost ready!",
        "Frame by frame…",
        "Developing brilliance…",
        "Hang tight, magic in progress…",
        "Give us a sec, art is brewing…",
        "Hold on, pixels aligning…",
        "Magic happening behind the scenes…",
        "One moment please…",
        "Crafting your memories…",
        "Almost there, thanks for your patience…",
        "Don’t blink, or you might miss it…",
        "Your masterpiece is on its way…",
    ]
    
    @pibooth.hookimpl(trylast=True)
    def state_processing_enter(cfg, app, win):
        """
        When entering the 'processing' screen, list the output folder,
        pick the 3 newest image files, and render them as thumbnails.
        """
    
        # 1) Determine your output directory (default: ~/Pictures/pibooth)
        outdir = os.path.expanduser(cfg.get('PATHS', 'output_dir') if
                                    cfg.has_option('PATHS', 'output_dir') else
                                    '~/Pictures/pibooth')
        LOGGER.info(f"[custom_processing] Scanning directory: {outdir}")
    
        # 2) Glob all image files (you can tweak extensions as needed)
        exts = ('*.jpg', '*.jpeg', '*.png', '*.avif')
        files = []
        for ext in exts:
            files.extend(glob.glob(os.path.join(outdir, ext)))
        if not files:
            LOGGER.info("[custom_processing] No files found in output dir")
            return
    
        # 3) Sort by modification time and pick the last three
        files.sort(key=lambda f: os.path.getmtime(f))
        thumbs = files[-3:]
        LOGGER.info(f"[custom_processing] Last 3 files: {thumbs}")
    
        # 4) Load, scale and display as before
        surface = win.surface
        # Redraw PiBooth’s default processing background
        win._update_background(background.Background(''))
    
        sw, sh = surface.get_size()
        thumb_h = int(sh * 0.3)
        scaled = []
        for path in thumbs:
            try:
                # Load with alpha so we can mask out black
                img = pygame.image.load(path).convert_alpha()
                ow, oh = img.get_size()
                tw = int(ow * thumb_h / oh)
                img = pygame.transform.smoothscale(img, (tw, thumb_h))
    
                # Make pure black transparent (so border and photo remain)
                img.set_colorkey((0, 0, 0))
    
                scaled.append(img)
                LOGGER.info(f"[custom_processing] Loaded & scaled {os.path.basename(path)} β†’ {(tw, thumb_h)}")
            except Exception as e:
                LOGGER.error(f"[custom_processing] Failed to load {path}: {e}")
    
        if not scaled:
            LOGGER.info("[custom_processing] No thumbnails rendered")
            return
    
        # 5) Blit and rotate thumbnails into the hanging frames with random tilt
        # Percent‐based anchor points so they adjust on resize:
        frame_positions = [
            (int(sw * 0.20), int(sh * 0.34)),  # left (moved up to 40% height)
            (int(sw * 0.50), int(sh * 0.36)),  # center (35% height)
            (int(sw * 0.80), int(sh * 0.34)),  # right (40% height)
        ]
        for img, (cx, cy) in zip(scaled, frame_positions):
            # Random rotation between -10Β° and +10Β°
            angle = random.uniform(-10, 10)
            rot_img = pygame.transform.rotozoom(img, angle, 1.0)
            rw, rh = rot_img.get_size()
            # Center the rotated thumbnail on its anchor point
            surface.blit(rot_img, (cx - rw // 2, cy - rh // 2))
    
        # Draw the transparent custom processing template over the thumbnails
        custom_bg = os.path.expanduser("~/.config/pibooth/assets/processing_custom.png")
        try:
            # Load with alpha to preserve transparency
            overlay_img = pygame.image.load(custom_bg).convert_alpha()
            # Scale to cover the full window
            overlay_img = pygame.transform.smoothscale(overlay_img, (sw, sh))
            # Blit on top of thumbnails
            surface.blit(overlay_img, (0, 0))
        except Exception as e:
            LOGGER.error(f"[custom_processing] Failed to load processing_custom overlay '{custom_bg}': {e}")
    
        # Pick and uppercase a random processing message
        say = random.choice(PROCESSING_SAYINGS).upper()
        # Define the bottom third with 5% side padding
        bottom_rect = pygame.Rect(
            int(sw * 0.05), 
            int(sh * 2/3), 
            int(sw * 0.9), 
            int(sh / 3)
        )
        # Choose a font that fits in that rectangle
        font = fonts.get_pygame_font(
            say,
            fonts.CURRENT,
            bottom_rect.width,
            bottom_rect.height
        )
        # Render the text in the window's text color
        text_color = cfg.gettuple('WINDOW', 'text_color', int)
        text_surf = font.render(say, True, text_color)
        # Center the message within the bottom third
        text_rect = text_surf.get_rect(center=bottom_rect.center)
        surface.blit(text_surf, text_rect)
    
        pygame.display.update()
        LOGGER.info("[custom_processing] Thumbnails hung with rotation")Code language: PHP (php)

    This is what it ends up looking like, anonymized:

    So yeah, use at your own risk. I don’t offer any support. Assume it will break things and burn down your house. I am not a professional. Don’t trust Python.

    Farewell!

  • Capturing MacOS Settings Changes

    Capturing MacOS Settings Changes

    Let me get this right out. I upgraded to MacOS 15 beta and it totally borked my machine. I had to do a full, fresh reinstall.

    Totally my fault, and I should have prepared better.

    So now, I’m trying to remember to capture my personal settings so I can use a shell script to restore them in case of emergency with defaults

    With the help of ChatGPT and a lot of stupidity, I have created this:

    #!/bin/bash
    
    # Set the DEBUG flag (default is false)
    DEBUG=false
    
    # Define a function for logging debug information
    log() {
        if [ "$DEBUG" = true ]; then
            echo "$@"
        fi
    }
    
    # Define a function for running commands with error suppression unless DEBUG is true
    run_command() {
        if [ "$DEBUG" = true ]; then
            "$@"
        else
            "$@" 2>/dev/null
        fi
    }
    
    # Step 1: Define directories for temporary files
    before_dir="/tmp/before_defaults"
    after_dir="/tmp/after_defaults"
    mkdir -p "$before_dir" "$after_dir"
    
    # Step 2: Capture system-wide and global preferences
    
    echo "Reading user-specific global preferences..."
    run_command defaults read -g > "$before_dir/user_global_defaults.plist"
    
    echo "Reading system-wide global preferences..."
    run_command sudo defaults read -g > "$before_dir/system_global_defaults.plist"
    
    echo "Reading user-specific system preferences..."
    run_command defaults read > "$before_dir/user_system_defaults.plist"
    
    echo "Reading system-wide system preferences..."
    run_command sudo defaults read > "$before_dir/system_system_defaults.plist"
    
    # Step 3: Get all domains and capture their preferences with and without sudo
    echo "Reading defaults for all domains..."
    domains=$(run_command defaults domains)
    for domain in $domains; do
        log "Reading user-specific defaults for domain: $domain"
        run_command defaults read "$domain" > "$before_dir/${domain}_user_defaults.plist"
        
        log "Reading system-wide defaults for domain: $domain"
        run_command sudo defaults read "$domain" > "$before_dir/${domain}_system_defaults.plist"
    done
    
    # Step 4: Capture network settings (excluding Launch Services to reduce noise)
    echo "Capturing network settings..."
    run_command sudo cp /Library/Preferences/SystemConfiguration/preferences.plist "$before_dir/network_settings.plist"
    
    echo "Current preferences saved. Please make your changes now."
    echo "Press any key to continue once you've made the changes..."
    read -n 1 -s
    
    # Step 5: Capture updated defaults
    echo "Reading updated user-specific global preferences..."
    run_command defaults read -g > "$after_dir/user_global_defaults.plist"
    
    echo "Reading updated system-wide global preferences..."
    run_command sudo defaults read -g > "$after_dir/system_global_defaults.plist"
    
    echo "Reading updated user-specific system preferences..."
    run_command defaults read > "$after_dir/user_system_defaults.plist"
    
    echo "Reading updated system-wide system preferences..."
    run_command sudo defaults read > "$after_dir/system_system_defaults.plist"
    
    echo "Reading updated defaults for all domains..."
    for domain in $domains; do
        log "Reading updated user-specific defaults for domain: $domain"
        run_command defaults read "$domain" > "$after_dir/${domain}_user_defaults.plist"
        
        log "Reading updated system-wide defaults for domain: $domain"
        run_command sudo defaults read "$domain" > "$after_dir/${domain}_system_defaults.plist"
    done
    
    echo "Capturing updated network settings..."
    run_command sudo cp /Library/Preferences/SystemConfiguration/preferences.plist "$after_dir/network_settings.plist"
    
    # Step 6: Compare before and after to identify changes with unified diffs and output results directly
    
    echo "Comparing preferences and generating diffs..."
    
    # Global and system diffs
    echo "Comparing global and system preferences..."
    diff -u "$before_dir/user_global_defaults.plist" "$after_dir/user_global_defaults.plist" | sed '/^\s*$/d'
    diff -u "$before_dir/system_global_defaults.plist" "$after_dir/system_global_defaults.plist" | sed '/^\s*$/d'
    
    diff -u "$before_dir/user_system_defaults.plist" "$after_dir/user_system_defaults.plist" | sed '/^\s*$/d'
    diff -u "$before_dir/system_system_defaults.plist" "$after_dir/system_system_defaults.plist" | sed '/^\s*$/d'
    
    echo "Comparing Domain Specific"
    # Domain-specific diffs
    for domain in $domains; do
        # Only run diff if both user-specific files exist
        if [ -f "$before_dir/${domain}_user_defaults.plist" ] && [ -f "$after_dir/${domain}_user_defaults.plist" ]; then
            
            #echo "Comparing user-specific defaults for domain: $domain"
            diff -u "$before_dir/${domain}_user_defaults.plist" "$after_dir/${domain}_user_defaults.plist" | sed '/^\s*$/d'
        fi
    
        # Only run diff if both system-wide files exist
        if [ -f "$before_dir/${domain}_system_defaults.plist" ] && [ -f "$after_dir/${domain}_system_defaults.plist" ]; then
            #echo "Comparing system-wide defaults for domain: $domain"
            diff -u "$before_dir/${domain}_system_defaults.plist" "$after_dir/${domain}_system_defaults.plist" | sed '/^\s*$/d'
        fi
    done
    
    diff -u "$before_dir/network_settings.plist" "$after_dir/network_settings.plist" | sed '/^\s*$/d'
    
    # Step 7: Clean up temporary files (optional, currently commented out)
    #log "Cleaning up temporary files..."
    #run_command sudo rm -r "$before_dir" "$after_dir"
    
    echo "Comparison complete."
    Code language: Bash (bash)

    This will basically do a scan of all defaults settings it can find, wait for you to make changes, and then show you a diff of what has changed.

    For a bad example, I wanted to auto hide the dock:

    $ bash default-changes.sh
    Reading user-specific global preferences...
    Reading system-wide global preferences...
    Reading user-specific system preferences...
    Reading system-wide system preferences...
    Reading defaults for all domains...
    Capturing network settings...
    Current preferences saved. Please make your changes now.
    Press any key to continue once you've made the changes...
    Reading updated user-specific global preferences...
    Reading updated system-wide global preferences...
    Reading updated user-specific system preferences...
    Reading updated system-wide system preferences...
    Reading updated defaults for all domains...
    Capturing updated network settings...
    Comparing preferences and generating diffs...
    Comparing global and system preferences...
    --- /tmp/before_defaults/user_system_defaults.plist	2024-09-30 21:23:17.183140919 -0400
    +++ /tmp/after_defaults/user_system_defaults.plist	2024-09-30 21:23:45.721177080 -0400
    @@ -95,9 +95,9 @@
         };
         ContextStoreAgent =     {
             "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertEventsNotification:/app/mediaUsageActivityDate" = "2024-08-15 01:49:34 +0000";
    -        "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertEventsNotification:/app/usageActivityDate" = "2024-10-01 01:22:53 +0000";
    +        "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertEventsNotification:/app/usageActivityDate" = "2024-10-01 01:23:42 +0000";
             "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertLocalEventsNotification:/app/mediaUsageActivityDate" = "2024-08-15 01:49:34 +0000";
    -        "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertLocalEventsNotification:/app/usageActivityDate" = "2024-10-01 01:22:53 +0000";
    +        "_DKThrottledActivityLast_DKKnowledgeStorageLogging_DKKnowledgeStorageDidInsertLocalEventsNotification:/app/usageActivityDate" = "2024-10-01 01:23:42 +0000";
         };
         LighthouseBitacoraFramework =     {
             "lastAggregationDate_MLHost" = "2024-08-31 00:00:00 +0000";
    @@ -6998,6 +6998,7 @@
             );
         };
         "com.apple.dock" =     {
    +        autohide = 1;
             "last-analytics-stamp" =         (
                 "748840077.891269"
             );
    @@ -12104,7 +12105,7 @@
                 "com.apple.photolibraryd.curatedlibraryprocessing" = "2024-09-30 11:58:18 +0000";
                 "com.apple.photolibraryd.periodicmaintenance" = "2024-09-29 18:28:19 +0000";
                 "com.apple.proactive.PersonalIntelligence.PersonalIntelligenceMetrics" = "2024-09-29 14:29:35 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.ClientLinkStatus" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.ClientLinkStatus" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.PersonalizationPortrait.ContactHandlesCache" = "2024-09-29 23:56:51 +0000";
                 "com.apple.proactive.PersonalizationPortrait.ContactsImport" = "2024-09-29 23:57:21 +0000";
                 "com.apple.proactive.PersonalizationPortrait.CoreRoutineImport" = "2024-09-29 23:56:50 +0000";
    @@ -12115,11 +12116,11 @@
                 "com.apple.proactive.PersonalizationPortrait.ExpiredLinkReview" = "2024-09-29 23:57:21 +0000";
                 "com.apple.proactive.PersonalizationPortrait.FeedbackProcessing" = "2024-09-29 23:56:50 +0000";
                 "com.apple.proactive.PersonalizationPortrait.FeedbackStreamReviewed" = "2024-09-29 23:56:51 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.LinkStatusGeneration" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.LinkStatusGeneration" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.PersonalizationPortrait.LogLocationPerplexity" = "2024-09-29 23:54:27 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.LogNamedEntityFirstSource" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.LogNamedEntityFirstSource" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.PersonalizationPortrait.LogNamedEntityPerplexity" = "2024-09-29 23:56:52 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.LogTopicFirstSource" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.LogTopicFirstSource" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.PersonalizationPortrait.LogTopicPerplexity" = "2024-09-29 23:57:22 +0000";
                 "com.apple.proactive.PersonalizationPortrait.MapsImport" = "2024-09-29 23:57:22 +0000";
                 "com.apple.proactive.PersonalizationPortrait.NamedEntityFiltering" = "2024-09-29 23:57:22 +0000";
    @@ -12136,7 +12137,7 @@
                 "com.apple.proactive.PersonalizationPortrait.TopicImport" = "2024-09-26 17:42:30 +0000";
                 "com.apple.proactive.PersonalizationPortrait.TopicRepairAndExport" = "2024-09-26 17:45:06 +0000";
                 "com.apple.proactive.PersonalizationPortrait.UnsupportedClient" = "2024-09-29 23:57:21 +0000";
    -            "com.apple.proactive.PersonalizationPortrait.VacuumDatabase" = "2024-09-22 01:54:07 +0000";
    +            "com.apple.proactive.PersonalizationPortrait.VacuumDatabase" = "2024-10-04 09:16:11 +0000";
                 "com.apple.proactive.ProactiveHarvesting.Cleanup" = "2024-09-29 14:29:35 +0000";
                 "com.apple.proactive.ProactiveHarvesting.Harvest.PeriodicBackground" = "2024-09-29 21:54:28 +0000";
                 "com.apple.proactived.contextualactions.training" = "2024-09-28 03:57:02 +0000";
    --- /tmp/before_defaults/system_system_defaults.plist	2024-09-30 21:23:17.281165387 -0400
    +++ /tmp/after_defaults/system_system_defaults.plist	2024-09-30 21:23:45.810771555 -0400
    @@ -374,7 +374,7 @@
                 "501:com.apple.mlhost.telemetry.daily" = "2024-09-26 21:59:29 +0000";
                 "501:com.apple.mlhostd.daily" = "2024-09-28 08:35:02 +0000";
                 "501:com.apple.parsec.SafariBrowsingAssistant" = "2024-09-27 02:53:00 +0000";
    -            "501:com.apple.photoanalysisd.backgroundanalysis" = "2024-09-30 20:14:26 +0000";
    +            "501:com.apple.photoanalysisd.backgroundanalysis" = "2024-10-01 00:02:14 +0000";
                 "501:com.apple.photoanalysisd.graphNonIntensiveTasks" = "2024-09-30 20:20:03 +0000";
                 "501:com.apple.photoanalysisd.internal" = "2024-09-15 01:53:51 +0000";
                 "501:com.apple.photoanalysisd.music" = "2024-09-30 17:05:59 +0000";
    @@ -2312,7 +2312,7 @@
             LastOSLaunchVersion = 24A335;
             "baseDate.LocalBeaconingManager" = "2023-06-01 15:07:33 +0000";
             lastAttemptDate = "2024-08-15 01:48:08 +0000";
    -        lastFinderAttemptDate = "2024-10-01 01:23:05 +0000";
    +        lastFinderAttemptDate = "2024-10-01 01:23:36 +0000";
             lastFinderPublishDates =         {
                 batteryWiFi =             (
                     "2024-09-21 01:07:39 +0000",
    @@ -3859,7 +3859,7 @@
                 "com.apple.awdd.publication" = "2024-09-29 18:17:55 +0000";
                 "com.apple.awdd.trigger:0x7f004:86400:2" = "2024-09-28 13:31:28 +0000";
                 "com.apple.awdd.trigger:0x7f006:14400:2" = "2024-09-30 05:24:18 +0000";
    -            "com.apple.backupd-auto" = "2024-10-01 01:08:37 +0000";
    +            "com.apple.backupd-auto" = "2024-10-01 01:38:37 +0000";
                 "com.apple.backupd-auto.dryspell" = "2024-09-28 01:22:46 +0000";
                 "com.apple.backupd.analytics" = "2024-09-28 05:36:06 +0000";
                 "com.apple.biome.prune-expired-events" = "2024-09-28 03:56:23 +0000";
    Comparing Domain Specific
    Comparison complete.Code language: PHP (php)

    You can see that there’s a lot of background noise, but in there is what I was looking for: com.apple.dock and autohide = 1.

    Now I can head over to macos-defaults.com and I can search for com.apple.doc and find the command to set the dock to auto hide.

    defaults write com.apple.dock "autohide" -bool "true" && killall Dock

    Thinking about it, this was probably a dumb example as it was easy to just go to macos-defaults.com and search for “autohide” to find it.

    Well, any way do what you want with this stupid thing.

  • Setting up Pibooth in 2024

    Setting up Pibooth in 2024

    I have an upcoming need for a photo booth next year 😏 so I started looking into some options to DIY rather than rent one or buy a pre-made version.

    The option I’m trying out right now is Pibooth, “A photobooth application out-of-the-box in pure Python.”

    https://pibooth.org/

    I’m going to set this up with an extra Raspberry Pi 400 I have that’s not doing anything, and see what we can do. Feel free to follow along!

    Since Pibooth only works on Raspbian Buster right now, I had to download an older version at https://downloads.raspberrypi.org/raspios_arm64/images/raspios_arm64-2021-05-28/

    I set up a few defaults I like to have for my Pis:

    # Updates, new software, and cleanup
    sudo apt update && sudo apt upgrade
    sudo apt install mc screen ack zsh locate git htop cockpit -y
    sudo apt autoremove
    
    # Add dotfile customizations. Sorry, it's currently private :D
    git clone git@github.com:emrikol/dotfiles.git
    cp -r ~/dotfiles/. ~/
    sudo usermod --shell /bin/zsh derrick
    zsh
    
    # Set up root access so I can SCP in from Transmit if I need to.
    sudo sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
    sudo passwd root
    
    # Customize Raspberry Pi settings.
    sudo raspi-configCode language: Bash (bash)

    After installing cockpit I believe, I had an issue where the MAC address of my Wifi kept changing randomly on each reboot. I had to follow these instructions and add this to my /etc/NetworkManager/NetworkManager.conf:

    [device]
    wifi.scan-rand-mac-address=no

    Disable Swap to save on SD Card wear:

    sudo dphys-swapfile swapoff
    sudo dphys-swapfile uninstall
    sudo update-rc.d dphys-swapfile remove
    sudo apt purge dphys-swapfile -y
    sudo sysctl -w vm.swappiness=0Code language: Bash (bash)

    Install Log2RAM for the same reason:

    echo "deb [signed-by=/usr/share/keyrings/azlux-archive-keyring.gpg] http://packages.azlux.fr/debian/ bookworm main" | sudo tee /etc/apt/sources.list.d/azlux.list
    sudo wget -O /usr/share/keyrings/azlux-archive-keyring.gpg  https://azlux.fr/repo.gpg
    sudo apt update
    sudo apt install log2ram
    sudo sed -i 's/SIZE=40M/SIZE=64M/' /etc/log2ram.conf
    sudo sed -i 's/#SystemMaxUse=/SystemMaxUse=32M/' /etc/systemd/journald.conf
    sudo systemctl restart systemd-journald
    Code language: Bash (bash)

    From here, we should have my default “base” Raspberry Pi setup. And now, we can work on figuring out how to install Pibooth. According to the install docs, we need to run a few commands:

    $ sudo apt install "libsdl2-*"
    Reading package lists... Done
    Building dependency tree
    Reading state information... Done
    Note, selecting 'libsdl2-mixer-dev' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-image-dev' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-gfx-dev' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-gfx-doc' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-mixer-2.0-0' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-dbg:armhf' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-dev' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-doc' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-ttf-dev' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-net-2.0-0' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-net-dev' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-image-2.0-0' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-2.0-0-dbgsym:armhf' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-2.0-0' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-gfx-1.0-0' for glob 'libsdl2-*'
    Note, selecting 'libsdl2-ttf-2.0-0' for glob 'libsdl2-*'
    libsdl2-2.0-0 is already the newest version (2.0.9+dfsg1-1+deb10u1).
    libsdl2-2.0-0 set to manually installed.
    Some packages could not be installed. This may mean that you have
    requested an impossible situation or if you are using the unstable
    distribution that some required packages have not yet been created
    or been moved out of Incoming.
    The following information may help to resolve the situation:
    
    The following packages have unmet dependencies:
     libsdl2-2.0-0-dbgsym:armhf : Depends: libsdl2-2.0-0:armhf (= 2.0.9+dfsg1-1+rpt1) but it is not going to be installed
    E: Unable to correct problems, you have held broken packages.Code language: JavaScript (javascript)

    Meh. Okay. Let’s just install all of them but the trouble package. Hopefully that won’t come back to bite us.

    sudo apt install libsdl2-mixer-dev libsdl2-image-dev libsdl2-gfx-dev libsdl2-gfx-doc libsdl2-mixer-2.0-0 libsdl2-dev libsdl2-doc libsdl2-ttf-dev libsdl2-net-2.0-0 libsdl2-net-dev libsdl2-image-2.0-0 libsdl2-2.0-0 libsdl2-gfx-1.0-0 libsdl2-ttf-2.0-0 -yCode language: Bash (bash)

    We did not install libsdl2-dbg and libsdl2-2.0-0-dbgsym

    I’m thinking about adding printer support, so I’ll go ahead and install CUPS: sudo apt-get install cups libcups2-dev

    And we might as well install OpenCV sudo apt-get install python3-opencv

    Installing gphoto2:

    cd ~
    git clone https://github.com/gonzalo/gphoto2-updater
    cd gphoto2-updater
    sudo ./gphoto2-updater.shCode language: Bash (bash)

    Now for pibooth: pip3 install "pibooth[dslr,printer]"

    Aww yeah! Success!

    Now all I need to do is customize it. Maybe we’ll have another post at a later time.

  • Code Sweep: A Simple Approach to a Neater WordPress User List

    Code Sweep: A Simple Approach to a Neater WordPress User List

    Feel like clearing out your spam users? With the snippet below we can make your job much easier!

    /**
     * Adds a new column to the user management screen for displaying the number of comments.
     *
     * @param array $columns The existing columns in the user management screen.
     *
     * @return array The modified columns array with the new 'comments_count' column added.
     */
    function emrikol_add_comments_column( array $columns ): array {
    	$columns['comments_count'] = esc_html__( text: 'Comments', domain: 'default' );
    	return $columns;
    }
    add_filter( 'manage_users_columns', 'emrikol_add_comments_column' );
    
    /**
     * Displays the number of comments for a user in the custom column.
     *
     * @param string $output       The value to be displayed in the column.
     * @param string $column_name  The name of the custom column.
     * @param int    $user_id      The ID of the user.
     *
     * @return string              The updated value to be displayed in the column.
     */
    function emrikol_show_comments_count( string $output, string $column_name, int $user_id ): string {
    	if ( 'comments_count' == $column_name ) {
    		$args           = array(
    			'user_id' => $user_id,
    			'count'   => true,
    		);
    		$comments_count = get_comments( args: $args );
    		return number_format_i18n( number: $comments_count );
    	}
    
    	return $output;
    }
    add_action( 'manage_users_custom_column', 'emrikol_show_comments_count', 10, 3 );
    Code language: PHP (php)

    This will add a “Comments” count to the WordPress user list so you can easily determine which users you can delete:

    What a sad state this blarg is in…

  • Quick Tip: Add Screen Name to Bash Prompt

    Quick Tip: Add Screen Name to Bash Prompt

    I often SSH into servers to get some work done, and one of the things I discovered recently is that I may not always know or remember if I’m in a screen session.

    So I had the bright idea to just add it to my shell prompt!

    Simply just add one of these to your RC file of choice:

    Bash

    # Add Screen name to PS1 if we're in a screen.
    if [ -n "$STY" ]; then
    	PS1="\[\e[1m\](Screen: $STY)\[\e[0m\]\n$PS1"
    fiCode language: PHP (php)

    ZSH

    # Add Screen name to PROMPT if we're in a screen.
    if [[ -n "$STY" ]]; then
    	PROMPT="%B(Screen: $STY)%b"$'\n'"$PROMPT"
    fiCode language: PHP (php)

    And remember, if you’re asking yourself if you should run something in a screen, you’re already too late!

  • Meet The Plugin That Lists All Your Multisite’s Sites: Multisite Site List

    Meet The Plugin That Lists All Your Multisite’s Sites: Multisite Site List

    Have you ever found yourself in this common situation?

    “I’ve got this lovely WordPress Multisite, but I’d like to list every site on it in a post or page!”

    Of course you have–we’ve all been there.

    Well, let me introduce you to my newest plugin: site-list

    Installing the Plugin

    Installing it is a breeze, and using it is as easy as pieβ€”assuming you find pie easy. Just clone the git repository, grab the code, upload it to your WordPress site, and enable it. Don’t forget to constantly check back in the repository for updates that will probably never come because I didn’t upload this to the WordPress.org Plugin Repository.

    Example Output

    This plugin does one thing: It adds a new block called “Multisite Site List” that lists the site in a multisite in an unordered list, like this:

    <ul class="ms-sites-list">
    	<li>
    		<a href="https://example.com/">My Website</a>
    		<p>The Super Awesome Site Tagline</p>
    	</li>
    	<li>
    		<a href="https://example.com/better-site">My Even Better Website</a>
    		<p>The Better Site Tagline</p>
    	</li>
    	<li>
    		<a href="https://example.com/garbage-site">My Blarg</a>
    		<p>Why are you even reading this?</p>
    	</li>
    </ul>Code language: HTML, XML (xml)

    Customizing the Output

    “But Derrick, what if I want to hide a site, or change something?”

    Why would you? This is perfect as-is. *sigh* Oh, you want to be the captain of your ship? Fine, here’s a filter for you:

    /**
     * Filters the list of sites retrieved by the `get_sites()` function.
     *
     * This filter allows developers to modify the list of sites before
     * they are processed and rendered in the `ms_sites_list_block_render()` function.
     * It can be useful for modifying, adding, or removing sites based on custom criteria
     * or to inject additional information into the sites' data.
     *
     * @param array $sites An array of `WP_Site` objects representing each site in the WordPress multisite network.
     */
    $sites = apply_filters( 'site_list_get_sites', get_sites() );Code language: PHP (php)

    Happy now?

    So yeah, this is a thing. Do what you want with it, and enjoy.

  • Macbook Battery Stats in Your ZSH Terminal Prompt

    Macbook Battery Stats in Your ZSH Terminal Prompt

    As a power user of my Macbook, I’ve found that I often overlook the small battery icon on my menu bar, especially when I’m immersed in a fun project. This minor inconvenience sparked a thought: why not incorporate the battery status directly into my terminal prompt? Thus, I embarked on a fun exploration into ZSH scripting. With a bit of coding magic, I was able to enhance my terminal prompt to dynamically display my Macbook’s battery percentage. Let me guide you through the process.

    We start by loading the ZSH hook module. This module allows us to add functions that run before and/or after each command, giving us the ability to update our battery status prompt in real-time.

    autoload -U add-zsh-hookCode language: Bash (bash)

    The Battery Status Function

    Next, I crafted a function, terminal_battery_stats, that retrieves battery information and displays it in the terminal prompt. Here’s how it works:

    function terminal_battery_stats {
        # Retrieve battery statistics using the pmset command. This is a macOS
        # command that allows power management and battery status retrieval.
        # The awk command is used to format the output into a useful string.
        bat_info=$(pmset -g batt | awk 'NR==2 {gsub(/;/,""); print $3 " " $4}')
        
        # Extract the battery percentage and state from the bat_info string.
        bat_percent=$(echo $bat_info | cut -d' ' -f1 | tr -d '%')
        bat_state=$(echo $bat_info | cut -d' ' -f2)
    
        # Check if the battery is charging or on AC power.
        if [ $bat_state = 'charging' ] || [ $bat_state = 'AC' ] || [ $bat_state = 'charged' ] || [ $bat_state = 'finishing' ]; then
            # If the battery is over 66%, don't display a battery prompt.
            if [ $bat_percent -gt 66 ]; then
                bat_prompt=""
            else
                # Otherwise, set the battery icon to a plug, and the color to green.
                bat_icon='πŸ”Œ'
                bat_color='%F{green}'
                # Format the prompt with the battery color, percentage, and icon.
                bat_prompt="γ€”$bat_color$bat_percent%% $bat_icon%f〕"
            fi
        else
            # If the battery is discharging, choose a battery icon and color based on the battery level.
            if [ $bat_percent -le 33 ]; then
                bat_icon='πŸͺ«'
                bat_color='%F{red}'
            elif [ $bat_percent -gt 66 ]; then
                bat_icon='πŸ”‹'
                bat_color='%F{green}'
            else
                bat_icon='πŸ”‹'
                bat_color='%F{yellow}'
            fi
            # Format the prompt with the battery color, percentage, and icon.
            bat_prompt="γ€”$bat_color$bat_percent%% $bat_icon%f〕"
        fi
    
        # Check if the current prompt already contains a battery status.
        if [[ "$PROMPT" == *"γ€”"* ]]; then
            # If it does, remove the existing battery status from the prompt.
            PROMPT=${PROMPT#*"〕"}
        fi
    
        # Add the new battery status to the prompt.
        PROMPT="${bat_prompt}${PROMPT}"
    }Code language: Bash (bash)

    To get some basic understanding of the magic behind ZSH scripting and manipulating the terminal prompt, check this helpful resource.

    Applying the Battery Status Function

    After creating the function, I added terminal_battery_stats to the command prompt via ZSH’s pre-command hook. Now, my function runs before each command entered in the terminal, keeping the battery stats up-to-date.

    add-zsh-hook precmd terminal_battery_statsCode language: Bash (bash)

    All the above code is added to my ~/.zshrc file, turning my terminal prompt into a dynamic display of my Macbook’s battery status. The resulting terminal prompt looks like this:

    Conclusion

    Through the power of ZSH scripting magic, my terminal now offers real-time updates of my Macbook’s battery status after every command. I set it to disappear once the battery reaches 67%, a level I consider to be within the safe zone. This is an excellent example of how minor inconveniences can lead to innovative solutions that enhance productivity.

    Here’s the full script ready to drop in to your own ~/.zshrc file:


    # Load the zsh hook module. This is a module that allows adding functions
    # that get run before and/or after each command.
    autoload -U add-zsh-hook
    
    # Function to retrieve and display battery statistics in the terminal prompt.
    # Uses the pmset command to retrieve battery information, and awk to format
    # it into a useful string. Depending on the battery's state and level, 
    # different icons and colors will be displayed in the terminal prompt.
    # 
    # @return void
    function terminal_battery_stats {
        # Retrieve battery statistics using the pmset command. This is a macOS
        # command that allows power management and battery status retrieval.
        # The awk command is used to format the output into a useful string.
        bat_info=$(pmset -g batt | awk 'NR==2 {gsub(/;/,""); print $3 " " $4}')
        
        # Extract the battery percentage and state from the bat_info string.
        bat_percent=$(echo $bat_info | cut -d' ' -f1 | tr -d '%')
        bat_state=$(echo $bat_info | cut -d' ' -f2)
    
        # Check if the battery is charging or on AC power.
        if [ $bat_state = 'charging' ] || [ $bat_state = 'AC' ] || [ $bat_state = 'charged' ] || [ $bat_state = 'finishing' ]; then
            # If the battery is over 66%, don't display a battery prompt.
            if [ $bat_percent -gt 66 ]; then
                bat_prompt=""
            else
                # Otherwise, set the battery icon to a plug, and the color to green.
                bat_icon='πŸ”Œ'
                bat_color='%F{green}'
                # Format the prompt with the battery color, percentage, and icon.
                bat_prompt="γ€”$bat_color$bat_percent%% $bat_icon%f〕"
            fi
        else
            # If the battery is discharging, choose a battery icon and color based on the battery level.
            if [ $bat_percent -le 33 ]; then
                bat_icon='πŸͺ«'
                bat_color='%F{red}'
            elif [ $bat_percent -gt 66 ]; then
                bat_icon='πŸ”‹'
                bat_color='%F{green}'
            else
                bat_icon='πŸ”‹'
                bat_color='%F{yellow}'
            fi
            # Format the prompt with the battery color, percentage, and icon.
            bat_prompt="γ€”$bat_color$bat_percent%% $bat_icon%f〕"
        fi
    
        # Check if the current prompt already contains a battery status.
        if [[ "$PROMPT" == *"γ€”"* ]]; then
            # If it does, remove the existing battery status from the prompt.
            PROMPT=${PROMPT#*"〕"}
        fi
    
        # Add the new battery status to the prompt.
        PROMPT="${bat_prompt}${PROMPT}"
    }
    
    # Adds the function terminal_battery_stats to the command prompt 
    # meaning it will be run before each command entered in the terminal. 
    add-zsh-hook precmd terminal_battery_stats
    
    Code language: Bash (bash)

    Good luck!

  • Command Timing in ZSH

    Command Timing in ZSH

    I’ve had a few snippets in my .zshrc file for a while now that will output how long a command takes to process.

    First off, I’d like to say that I did not come up with this idea, and I didn’t really write the code. I’ve snipped it from somewhere and modified it over time, so I am very sorry to the original author for not being able to give full credit. I didn’t save where I got it from–so if anyone comes across this in the future and might know where the idea came from, drop it in the comments.

    Now, on to the fun:

    The original script only went down to the second, but I wanted more granularity than that, so I went down to the millisecond.

    You’ll likely need to install gdate (brew install gdate) for this to work properly.

    The code (added to ~/.zshrc):

    function preexec() {
    	timer=$(($(gdate +%s%0N)/1000000))
    }
    
    function precmd() {
    	if [ "$timer" ]; then
    		now=$(($(gdate +%s%0N)/1000000))
    		elapsed=$now-$timer
    
    		reset_color=$'\e[00m'
    		RPROMPT="%F{cyan} $(converts "$elapsed") %{$reset_color%}"
    		export RPROMPT
    		unset timer
    	fi
    }
    
    converts() {
    	local t=$1
    
    	local d=$((t/1000/60/60/24))
    	local h=$((t/1000/60/60%24))
    	local m=$((t/100/60%60))
    	local s=$((t/1000%60))
    	local ms=$((t%1000))
    
    	if [[ $d -gt 0 ]]; then
    			echo -n " ${d}d"
    	fi
    	if [[ $h -gt 0 ]]; then
    			echo -n " ${h}h"
    	fi
    	if [[ $m -gt 0 ]]; then
    			echo -n " ${m}m"
    	fi
    	if [[ $s -gt 0 ]]; then
    		echo -n " ${s}s"
    	fi
    	if [[ $ms -gt 0 ]]; then
    		echo -n " ${ms}ms"
    	fi
    	echo
    }Code language: PHP (php)
  • Nano: The One True Editor

    Nano: The One True Editor

    Forget about vi/vim/edlin. Nano is the only editor that you need in the console. Sure, it may not be perfect, but with a few extra steps in .nanorc it can really be a great experience. Here’s my .nanorc with some comments showing what’s changed:

    ## Automatically indent a newly created line to the same number of tabs and/or spaces as the previous line (or as the next line if the previous line is the beginning of a paragraph).
    unset autoindent
    
    ## Use bold instead of reverse video
    set boldtext
    
    ## Save the last hundred search strings and replacement strings and executed commands, so they can be easily reused in later sessions.
    set historylog
    
    ## Display line numbers to the left of the text area.
    set linenumbers
    
    ## Enable mouse support, if available for your system. When enabled, mouse clicks can be used to place the cursor, set the mark (with a double click), and execute shortcuts. The mouse will work in the X Window System, and on the console when gpm is running. Text can still be selected through dragging by holding down the Shift key.
    set mouse
    
    ## Save the cursor position of files between editing sessions. The cursor position is remembered for the 200 most-recently edited files.
    set positionlog
    
    ## Use a tab size of number columns. The value of number must be greater than 0. The default value is 8.
    set tabsize 4
    
    ## Default Syntax Highlighting
    include "/usr/share/nano/asm.nanorc"
    include "/usr/share/nano/autoconf.nanorc"
    include "/usr/share/nano/awk.nanorc"
    include "/usr/share/nano/c.nanorc"
    include "/usr/share/nano/changelog.nanorc"
    include "/usr/share/nano/cmake.nanorc"
    include "/usr/share/nano/css.nanorc"
    include "/usr/share/nano/debian.nanorc"
    include "/usr/share/nano/default.nanorc"
    include "/usr/share/nano/elisp.nanorc"
    include "/usr/share/nano/fortran.nanorc"
    include "/usr/share/nano/gentoo.nanorc"
    include "/usr/share/nano/go.nanorc"
    include "/usr/share/nano/groff.nanorc"
    include "/usr/share/nano/guile.nanorc"
    include "/usr/share/nano/html.nanorc"
    include "/usr/share/nano/java.nanorc"
    include "/usr/share/nano/javascript.nanorc"
    include "/usr/share/nano/json.nanorc"
    include "/usr/share/nano/lua.nanorc"
    include "/usr/share/nano/makefile.nanorc"
    include "/usr/share/nano/man.nanorc"
    include "/usr/share/nano/mgp.nanorc"
    include "/usr/share/nano/mutt.nanorc"
    include "/usr/share/nano/nanorc.nanorc"
    include "/usr/share/nano/nftables.nanorc"
    include "/usr/share/nano/objc.nanorc"
    include "/usr/share/nano/ocaml.nanorc"
    include "/usr/share/nano/patch.nanorc"
    include "/usr/share/nano/perl.nanorc"
    include "/usr/share/nano/php.nanorc"
    include "/usr/share/nano/po.nanorc"
    include "/usr/share/nano/postgresql.nanorc"
    include "/usr/share/nano/pov.nanorc"
    include "/usr/share/nano/python.nanorc"
    include "/usr/share/nano/ruby.nanorc"
    include "/usr/share/nano/rust.nanorc"
    include "/usr/share/nano/sh.nanorc"
    include "/usr/share/nano/spec.nanorc"
    include "/usr/share/nano/tcl.nanorc"
    include "/usr/share/nano/tex.nanorc"
    include "/usr/share/nano/texinfo.nanorc"
    include "/usr/share/nano/xml.nanorc"Code language: PHP (php)