AndyObtiva/glimmer-dsl-libui

View on GitHub
lib/glimmer/launcher.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# Copyright (c) 2007-2023 Andy Maleh
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

if ARGV.include?('--bundler') && File.exist?(File.expand_path('./Gemfile'))
  require 'bundler'
  Bundler.setup(:default)
end
require 'fileutils'
require 'os'

module Glimmer
  # Launcher of glimmer applications and main entry point for the `glimmer` command.
  class Launcher
    # TODO update the verbiage below for Glimmer DSL for LibUI
    TEXT_USAGE = <<~MULTI_LINE_STRING
      Glimmer DSL for LibUI (Prerequisite-Free Ruby Desktop Development Cross-Platform Native GUI Library) - Ruby Gem: glimmer-dsl-libui v#{File.read(File.expand_path('../../../VERSION', __FILE__))}
      Usage: glimmer [--bundler] [--pd] [--quiet] [--debug] [--log-level=VALUE] [[ENV_VAR=VALUE]...] [[-ruby-option]...] (application.rb or task[task_args])
    
      Runs Glimmer applications and tasks.
    
      When applications are specified, they are run using Ruby,
      automatically preloading the glimmer-dsl-libui Ruby gem.
    
      Optionally, extra Glimmer options, Ruby options, and/or environment variables may be passed in.
    
      Glimmer options:
      - "--bundler=GROUP"   : Activates gems in Bundler default group in Gemfile
      - "--pd=BOOLEAN"      : Requires puts_debuggerer to enable pd method
      - "--quiet=BOOLEAN"   : Does not announce file path of Glimmer application being launched
      - "--debug"           : Displays extra debugging information and enables debug logging
      - "--log-level=VALUE" : Sets Glimmer's Ruby logger level ("ERROR" / "WARN" / "INFO" / "DEBUG"; default is none)
    
      Tasks are run via rake. Some tasks take arguments in square brackets (surround with double-quotes if using Zsh).
    
      Available tasks are below (if you do not see any, please add `require 'glimmer/rake_task'` to Rakefile and rerun or run rake -T):
      
    MULTI_LINE_STRING

    GLIMMER_LIB_GEM = 'glimmer-dsl-libui'
    GLIMMER_LIB_LOCAL = File.expand_path(File.join('lib', "#{GLIMMER_LIB_GEM}.rb"))
    GLIMMER_OPTIONS = %w[--log-level --quiet --bundler --pd]
    GLIMMER_OPTION_ENV_VAR_MAPPING = {
      '--log-level' => 'GLIMMER_LOGGER_LEVEL'   ,
      '--bundler'   => 'GLIMMER_BUNDLER_SETUP'  ,
      '--pd'        => 'PD'  ,
    }
    REGEX_RAKE_TASK_WITH_ARGS = /^([^\[]+)\[?([^\]]*)\]?$/

    class << self
      def is_arm64?
        host_cpu = OS.host_cpu.downcase
        host_cpu.include?('aarch64') || host_cpu.include?('arm')
      end
      
      def glimmer_lib
        unless @glimmer_lib
          @glimmer_lib = GLIMMER_LIB_GEM
          if File.exist?(GLIMMER_LIB_LOCAL)
            @glimmer_lib = GLIMMER_LIB_LOCAL
            puts "[DEVELOPMENT MODE] (detected #{@glimmer_lib})"
          end
        end
        @glimmer_lib
      end
      
      def dev_mode?
        glimmer_lib == GLIMMER_LIB_LOCAL
      end

      def glimmer_option_env_vars(glimmer_options)
        GLIMMER_OPTION_ENV_VAR_MAPPING.reduce({}) do |hash, pair|
          glimmer_options[pair.first] ? hash.merge(GLIMMER_OPTION_ENV_VAR_MAPPING[pair.first] => glimmer_options[pair.first]) : hash
        end
      end

      def load_env_vars(env_vars)
        env_vars.each do |key, value|
          ENV[key] = value
        end
      end

      def launch(application, ruby_options: [], env_vars: {}, glimmer_options: {})
        ruby_options_string = ruby_options.join(' ') + ' ' if ruby_options.any?
        env_vars = env_vars.merge(glimmer_option_env_vars(glimmer_options))
        load_env_vars(env_vars)
        the_glimmer_lib = glimmer_lib
        require 'puts_debuggerer' if (ENV['PD'] || ENV['pd']).to_s.downcase == 'true' || the_glimmer_lib == GLIMMER_LIB_LOCAL
        is_rake_task = !application.end_with?('.rb')
        rake_tasks = []
        if is_rake_task
          load File.expand_path('./Rakefile') if File.exist?(File.expand_path('./Rakefile')) && caller.join("\n").include?('/bin/glimmer:')
          require_relative 'rake_task'
          rake_tasks = Rake.application.tasks.map(&:to_s).map {|t| t.sub('glimmer:', '')}
           
          potential_rake_task_parts = application.match(REGEX_RAKE_TASK_WITH_ARGS)
          application = potential_rake_task_parts[1]
          rake_task_args = potential_rake_task_parts[2].split(',')
        end
        if rake_tasks.include?(application)
          rake_task = "glimmer:#{application}"
          puts "Running Glimmer rake task: #{rake_task}" if ruby_options_string.to_s.include?('--debug')
          Rake::Task[rake_task].invoke(*rake_task_args)
        else
          puts "Launching Glimmer Application: #{application}" if ruby_options_string.to_s.include?('--debug') || glimmer_options['--quiet'].to_s.downcase != 'true'
          require the_glimmer_lib
          load File.expand_path(application)
        end
      end
    end
    
    attr_reader :application_paths
    attr_reader :env_vars
    attr_reader :glimmer_options
    attr_reader :ruby_options

    def initialize(raw_options)
      raw_options << '--quiet' if !caller.join("\n").include?('/bin/glimmer:') && !raw_options.join.include?('--quiet=')
      raw_options << '--log-level=DEBUG' if raw_options.join.include?('--debug') && !raw_options.join.include?('--log-level=')
      @application_path = extract_application_path(raw_options)
      @env_vars = extract_env_vars(raw_options)
      @glimmer_options = extract_glimmer_options(raw_options)
      @ruby_options = raw_options
    end

    def launch
      if @application_path.nil?
        display_usage
      else
        launch_application
      end
    end

    private

    def launch_application
      self.class.launch(
        @application_path,
        ruby_options: @ruby_options,
        env_vars: @env_vars,
        glimmer_options: @glimmer_options
      )
    end

    def display_usage
      puts TEXT_USAGE
      display_tasks
    end
    
    def display_tasks
      if OS.windows? || Launcher.is_arm64?
        require 'rake'
        Rake::TaskManager.record_task_metadata = true
        require_relative 'rake_task'
        tasks = Rake.application.tasks
        task_lines = tasks.reject do |task|
          task.comment.nil?
        end.map do |task|
          max_task_size = tasks.map(&:name_with_args).map(&:size).max + 1
          task_name = task.name_with_args.sub('glimmer:', '')
          line = "glimmer #{task_name.ljust(max_task_size)} # #{task.comment}"
        end
        puts task_lines.to_a
      else
        require 'rake-tui'
        require 'tty-screen'
        require_relative 'rake_task'
        Rake::TUI.run(branding_header: nil, prompt_question: 'Select a Glimmer task to run:') do |task, tasks|
          max_task_size = tasks.map(&:name_with_args).map(&:size).max + 1
          task_name = task.name_with_args.sub('glimmer:', '')
          line = "glimmer #{task_name.ljust(max_task_size)} # #{task.comment}"
          bound = TTY::Screen.width - 6
          line.size <= bound ? line : "#{line[0..(bound - 3)]}..."
        end
      end
    end

    # Extract application path (which can also be a rake task, basically a non-arg)
    def extract_application_path(options)
      application_path = options.detect do |option|
        !option.start_with?('-') && !option.include?('=')
      end.tap do
        options.delete(application_path)
      end
    end

    def extract_env_vars(options)
      options.select do |option|
        !option.start_with?('-') && option.include?('=')
      end.each do |env_var|
        options.delete(env_var)
      end.reduce({}) do |hash, env_var_string|
        match = env_var_string.match(/^([^=]+)=(.+)$/)
        hash.merge(match[1] => match[2])
      end
    end

    def extract_glimmer_options(options)
      options.select do |option|
        GLIMMER_OPTIONS.reduce(false) do |result, glimmer_option|
          result || option.include?(glimmer_option)
        end
      end.each do |glimmer_option|
        options.delete(glimmer_option)
      end.reduce({}) do |hash, glimmer_option_string|
        match = glimmer_option_string.match(/^([^=]+)=?(.+)?$/)
        hash.merge(match[1] => (match[2] || 'true'))
      end
    end
  end
end