lib/utils/xdg.rb



require 'pathname'
require 'fileutils'
require 'stringio'

# Module for handling XDG base directory specifications and application
# directory management.
#
# Provides constants and methods for working with XDG (Cross-Desktop Group)
# base directories including data home, configuration home, state home, and
# cache home directories.
#
# The module defines standard XDG directory paths and includes functionality
# for creating application-specific directories within these base locations.
#
# @example
#   Utils::XDG::XDG_DATA_HOME # => Pathname object for the data home directory
#   Utils::XDG::XDG_CONFIG_HOME # => Pathname object for the config home directory
#   Utils::XDG::XDG_STATE_HOME # => Pathname object for the state home directory
#   Utils::XDG::XDG_CACHE_HOME # => Pathname object for the cache home directory
module Utils::XDG
  # A Pathname subclass that provides additional XDG directory functionality
  #
  # This class extends the standard Pathname class to include methods for
  # working with XDG (Cross-Desktop Group) base directory specifications. It
  # adds capabilities for creating subdirectories, reading files, and handling
  # path operations within the context of XDG-compliant directory structures.
  #
  # @example
  #   pathname = XDGPathname.new('/home/user')
  #   sub_dir = pathname.sub_dir_path('documents')
  #   content = pathname.read('config.txt')
  class XDGPathname < ::Pathname
    # The sub_dir_path method creates a subdirectory path and ensures it exists
    #
    # This method takes a directory name, combines it with the current path to
    # form a subdirectory path, and then checks if the path already exists. If
    # the path exists but is not a directory, it raises an ArgumentError. If
    # the path does not exist, it creates the directory structure using
    # FileUtils.
    #
    # @param dirname [ String ] the name of the subdirectory to create or access
    #
    # @return [ Pathname ] the Pathname object representing the subdirectory path
    #
    # @raise [ ArgumentError ] if the path exists but is not a directory
    def sub_dir_path(dirname)
      path = self + dirname
      if path.exist?
        path.directory? or raise ArgumentError,
          "path #{path.to_s.inspect} exists and is not a directory"
      else
        FileUtils.mkdir_p path
      end
      path
    end

    # The read method reads file contents or yields to a block for processing.
    #
    # This method attempts to read a file at the specified path, returning the
    # file's contents as a string.
    # If a block is provided, it opens the file and yields to the block with a
    # File object.
    # If the file does not exist and a default value is provided, it returns
    # the default value.
    # When a default value is provided along with a block, the block is invoked
    # with a StringIO object containing the default value.
    #
    # @param path [ String ] the path to the file to read
    # @param default [ String, nil ] the default value to return if the file
    #   does not exist
    #
    # @yield [ file ] optional block to process the file
    # @yieldparam file [ File ] the file object for processing
    #
    # @return [ String, nil ] the file contents if no block is given, the
    #   result of the block if given, or the default value if the file does not
    #   exist
    def read(path, default: nil, &block)
      full_path = join(path)
      if File.exist?(full_path)
        if block
          File.new(full_path, &block)
        else
          File.read(full_path, encoding: 'UTF-8')
        end
      else
        if default && block
          block.(StringIO.new(default))
        else
          default
        end
      end
    end

    %i[
      join + dirname basename realpath expand_path cleanpath
      relative_path_from
    ].each do |id|
      define_method(id) do |*args, **kw, &block|
        self.class.new(super(*args, **kw, &block))
      end
    end

    alias to_str to_s
  end

  # Module for handling XDG application directories.
  #
  # This module provides methods for creating and managing application-specific
  # directories within the XDG base directories.
  module AppDir
    # Converts a path string to a XDGPathname object with expanded path
    #
    # @param path [String] The path to convert
    # @return [XDGPathname] A path as XDGPathname
    def self.pathify(path)
      XDGPathname.new(path).expand_path
    end
  end

  class << self
    private

    # Retrieves an environment variable value or returns a default.
    #
    # @param name [String] The name of the environment variable
    # @param default [String] The default value if the environment variable is not set
    # @return [String] The value of the environment variable or the default
    def env_for(name, default:)
      ENV.fetch(name, default)
    end
  end

  # XDG Data Home directory path.
  #
  # This is the base directory relative to which user-specific data files should be stored.
  # The default value is `~/.local/share`.
  #
  # @return [Pathname] The data home directory path
  XDG_DATA_HOME = AppDir.pathify(env_for('XDG_DATA_HOME', default: '~/.local/share'))

  # XDG Configuration Home directory path.
  #
  # This is the base directory relative to which user-specific configuration files should be stored.
  # The default value is `~/.config`.
  #
  # @return [Pathname] The configuration home directory path
  XDG_CONFIG_HOME = AppDir.pathify(env_for('XDG_CONFIG_HOME', default: '~/.config'))

  # XDG State Home directory path.
  #
  # This is the base directory relative to which user-specific state files should be stored.
  # The default value is `~/.local/state`.
  #
  # @return [Pathname] The state home directory path
  XDG_STATE_HOME  = AppDir.pathify(env_for('XDG_STATE_HOME', default: '~/.local/state'))

  # XDG Cache Home directory path.
  #
  # This is the base directory relative to which user-specific non-essential cache files should be stored.
  # The default value is `~/.cache`.
  #
  # @return [Pathname] The cache home directory path
  XDG_CACHE_HOME  = AppDir.pathify(env_for('XDG_CACHE_HOME', default: '~/.cache'))
end