Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
icalPal / bin / icalPal
Size: Mime:
#!/usr/bin/env ruby

# rubocop: disable Style/RedundantBegin

# require a gem
#
# @param gem [String] The gem
def r(gem)
  begin
    # puts "require \"#{gem}\""
    require gem
  rescue LoadError => e
    $stderr.puts "FATAL: icalPal is missing a dependency: #{gem}"
    $stderr.puts e
    $stderr.puts
    abort "Try installing with 'gem install #{gem}'"
  end
end

# require_relative a library
#
# @param library [String] The library
def rr(library)
  begin
    # puts "require_relative \"../lib/#{library}\""
    require_relative "../lib/#{library}"
  rescue LoadError => e
    $stderr.puts "FATAL: Could not load library: #{library}"
    $stderr.puts
    abort e.message
  end
end

# rubocop: enable Style/RedundantBegin

%w[ logger csv json rdoc sqlite3 yaml ].each { |g| r g }
%w[ icalPal defaults options utils ].each { |l| rr l }


##################################################
# Load options

# All kids love log!
$log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
$log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
  format("[%-5<sev>s] %<time>s [%<file>s:%<line>5s] - %<message>s\n",
         {
           sev: s,
           time: t.strftime('%H:%M:%S.%L'),
           file: caller(4, 1)[0].split('/')[-1].split(':')[0],
           line: caller(4, 1)[0].split('/')[-1].split(':')[1],
           message: m
         })
end

$opts = ICalPal::Options.new.parse_options

$rows = []                      # Rows from the database
$sections = []                  # Calendar list sections
$items = []                     # Items to be printed


##################################################
# All kids love log!

$log.info("Options:\n#{$opts.to_json}")


##################################################
# Add an item to the list
#
# @param item[Object]

def add(item)
  $log.debug("Adding #{item.dump} #{item['UUID']} (#{item['title']})") if item['UUID'] if $log.level <= Logger::DEBUG

  $items.push(item)
end


##################################################
# Load the data

# What are we getting?
klass = ICalPal.call($opts[:cmd])
success = false

# Get it
$opts[:db].each do |db|
  $log.debug("Trying #{db}")

  if klass == ICalPal::Reminder
    # Load all .sqlite files
    $log.debug("Loading *.sqlite in #{db}")
    Dir.glob("#{db}/*.sqlite").each do |d|
      success = true

      rows = klass.load_data(d, klass::QUERY)
      $rows += rows

      sections = klass.load_data(d, klass::SECTIONS_QUERY)
      $sections += sections

      $log.info("Loaded #{rows.length} rows and #{sections.length} sections from #{d}")
    end
  else
    # Load database
    rows = ICalPal.load_data(db, klass::QUERY)
    $rows += rows

    success = true

    $log.info("Loaded #{rows.length} rows from #{db}")
  end

rescue Errno::EPERM
  # Probably need Full Disk Access

rescue SQLite3::CantOpenException
  # Non-fatal exception, try the next one

rescue StandardError => e
  # Log the error and (try to) continue
  $log.error("#{db}: #{e.message}")
end

# Make sure we opened at least one database
unless success
  $log.fatal('Could not open database')

  # SQLite3 does not return useful error messages.  If any databases
  # failed because of EPERM (operation not permitted), our parent
  # process might need Full Disk Access, and we should suggest that.
  eperm = 0

  $opts[:db].each do |db|
    # Use a real open to get a useful error
    # rubocop: disable Style/FileOpen
    File.open(db).close
    # rubocop: enable Style/FileOpen
  rescue Exception => e
    $log.fatal("#{e}: #{db}")

    eperm = 1 if e.instance_of?(Errno::EPERM)
  end

  if eperm.positive?
    $stderr.puts
    $stderr.puts "Does #{ancestor} have Full Disk Access in System Settings?"
    $stderr.puts
    $stderr.puts "Try running: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles'"
  end

  abort
end

$log.debug("Loaded #{$rows.length} #{klass} items")
$log.info("Window is #{$opts[:from]} to #{$opts[:to]}") if $opts[:from]


##################################################
# Process the data

# Add rows
$rows.each do |row|
  # --es/--is
  next if $opts[:es].any? row['account']
  next unless $opts[:is].empty? || ($opts[:is].any? row['account'])

  # --ec/--ic
  unless klass == ICalPal::Store || !row['calendar']
    next if $opts[:ec].any? row['calendar']
    next unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
  end

  begin
    # Instantiate an item
    item = klass.new(row)
  rescue StandardError => e
    $log.error("#{e.class}: #{e}")
    $log.error(row)
    next
  end

  # --et/--it
  next if $opts[:et].any? item['type']
  next unless $opts[:it].empty? || ($opts[:it].any? item['type'])

  # --el/--il
  next if $opts[:el].any? item['list_name']
  next unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])

  # --match
  if $opts[:match]
    r = $opts[:match].split('=')

    if item[r[0]].to_s.respond_to?(:match)
      next unless item[r[0]].to_s =~ Regexp.new(r[1], Regexp::IGNORECASE)
    end
  end

  if item.is_a?(ICalPal::Event)
    # Check for all-day and cancelled events
    next if $opts[:ea] && item['all_day'].positive?
    next if $opts[:ia] && !item['all_day'].positive?
    next if item['status'] == :canceled

    (item['has_recurrences'].positive?)?
      item.recurring.each { |j| add(j) } :
      item.non_recurring.each { |j| add(j) }
  else
    # Check for completed or dated reminders
    if item.is_a?(ICalPal::Reminder)
      next if $opts[:ed] && item['completed'].zero?
      next unless $opts[:id] || item['completed'].zero?

      next if $opts[:dated] == 'undatedTasks' && item['due_date'] && item['due_date'].positive?
      next if $opts[:dated] == 'datedTasks' && (!item['due_date'] || item['due_date'].zero?)

      if $opts[:dated] == 'tasksDueBefore'
        next unless item['due_date']
        next unless item['due_date'].between?($opts[:from].to_i, $opts[:to].to_i)
      end
    end

    add(item)
  end
end

# Add placeholders for empty days
if $opts[:sed] && $opts[:sd] && klass == ICalPal::Event
  days = $items.collect { |i| i['sday'] }.uniq.sort

  $opts[:days].times do |n|
    day = $opts[:from] + n
    $items.push(klass.new(day)) unless days.any? { |i| i.to_s == day.to_s }
  end
end

# Sort the rows
begin
  $sort_attrs = []
  $sort_attrs.push $opts[:sep] if $opts[:sep]
  $sort_attrs.push $opts[:sort] if $opts[:sort]
  $sort_attrs.push 'sdate'

  $log.info("Sorting #{$items.count} items by #{$sort_attrs}, reverse #{$opts[:reverse].inspect}")

  $items.sort!
  $items.reverse! if $opts[:reverse]
rescue Exception => e
  $log.info("Sorting failed: #{e}\n")
end

$log.debug("#{$items.count} items remain")

# Configure formatting
mu = case $opts[:output]
     when 'ansi' then RDoc::Markup::ToAnsi.new
     when 'default' then RDoc::Markup::ToICalPal.new($opts)
     when 'html'
       rdoc = RDoc::Options.new
       rdoc.pipe = true
       rdoc.output_decoration = false
       RDoc::Markup::ToHtml.new(rdoc)
     when 'md' then RDoc::Markup::ToMarkdown.new
     when 'rdoc' then RDoc::Markup::ToRdoc.new
     when 'toc' then RDoc::Markup::ToTableOfContents.new
     end


##################################################
# Print the data

items = $items[0..($opts[:li] - 1)]

# Machine-friendly formats
unless mu
  $log.debug("Output in #{$opts[:output]} format")

  puts case $opts[:output]
       when 'csv'
         # Get all headers
         headers = []
         items.each { |i| headers += i.keys }
         headers.uniq!

         # Populate a CSV::Table
         table = CSV::Table.new([], headers: headers)
         items.each { |i| table << i.to_csv(headers) }

         table
       when 'hash' then items.map { |i| i.self }
       when 'json' then items.map { |i| i.self }.to_json
       when 'xml'
         xml = items.map { |i| "<#{$opts[:cmd].chomp('s')}>#{i.to_xml}</#{$opts[:cmd].chomp('s')}>" }
         "<#{$opts[:cmd]}>\n#{xml.join}</#{$opts[:cmd]}>"
       when 'yaml' then items.map { |i| i.self }.to_yaml
       when 'remind' then items.map { |i|
           "REM #{i['sdate'].strftime('%F AT %R')} " +
             "DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i} " +
             "MSG #{i['title'].gsub(/([[:cntrl:]])/) { |c| c.dump[1..-2] }}"
         }.join("\n")
       else abort "No formatter for #{$opts[:output]}"
       end

  exit
end

# Human-readable formats
$log.debug("Formatting with #{mu.inspect}")

section = nil

items.each_with_index do |i, j|
  $log.debug("Print #{j}: #{i.inspect}") if $log.level <= Logger::DEBUG

  # --li
  break if $opts[:li].positive? && j >= $opts[:li]

  # Each item is a document
  doc = RDoc::Markup::Document.new

  # Sections
  if $opts[:sep] && section != i[$opts[:sep]]
    $log.debug("New section '#{$opts[:sep]}': #{i[$opts[:sep]]}") if $log.level <= Logger::DEBUG

    doc << RDoc::Markup::BlankLine.new if j.positive?
    doc << RDoc::Markup::Heading.new(1, "#{i[$opts[:sep]]}:")
    doc << RDoc::Markup::Rule.new(0)

    section = i[$opts[:sep]]
  end

  # Placeholder
  if i['placeholder']
    doc << RDoc::Markup::Raw.new(i['title'])
    doc << RDoc::Markup::BlankLine.new
    print doc.accept(mu)
    next
  end

  # Item
  props = RDoc::Markup::List.new(:LABEL)

  # Properties
  $opts[:props].each_with_index do |prop, k|
    value = i[prop]

    next unless value
    next if value.is_a?(Array) && !value[0]
    next if value.is_a?(String) && value.empty?

    $log.debug("#{prop}: #{value}") if $log.level <= Logger::DEBUG

    if k.positive?
      props << RDoc::Markup::BlankLine.new unless $opts[:ps]

      v = ((value.is_a?(Enumerable))? value.join('; ') : value)
      v = mu.colorize(*mu.DATE_COLOR, v) if prop == 'datetime' && mu.respond_to?('colorize')

      props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(v))
    else
      # Choose bullet
      if mu.is_a?(RDoc::Markup::ToICalPal) && !$opts[:nb]
        bullet = (i['due_date'] && i['due_date'].between?(0, $nowto_i))? $opts[:ab] : $opts[:bullet]
        props << RDoc::Markup::Raw.new("#{bullet} ")
      end

      # Maybe colorize label
      mu.is_a?(RDoc::Markup::ToICalPal) &&
        mu.COLOR_LABEL.any?(prop) &&
        value = mu.colorize(i['symbolic_color_name'], i['color'], value)

      # Maybe add calendar to title
      prop == 'title' &&
        i['calendar'] &&
        mu.respond_to?('bold') &&
        !$opts[:nc] &&
        value += mu.bold(" (#{i['calendar']})")

      props << RDoc::Markup::Heading.new(2, value.to_s)
    end
  end

  # Print it
  unless props.empty?
    doc << props
    doc << RDoc::Markup::BlankLine.new
  end

  print doc.accept(mu)
end