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']

  $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
    File.open(db).close
  rescue Exception => e
    $log.fatal("#{e.class}: #{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

  # Instantiate an item
  item = klass.new(row)

  # --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] == 1 && item['due_date'] && item['due_date'].positive?
      next if $opts[:dated] == 2 && (!item['due_date'] || item['due_date'].zero?)

      if $opts[:dated] == 3
        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)]

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

$log.debug("Formatting with #{mu.inspect}")

doc = RDoc::Markup::Document.new
section = nil

items.each_with_index do |i, j|
  $log.debug("Print #{j}: #{i.inspect}")

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

  # Use RDoc::Markup::Verbatim to save the item
  v = RDoc::Markup::Verbatim.new
  v.format = i
  doc << v

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

    doc << RDoc::Markup::Raw.new($opts[:sep])

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

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

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

  # 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}")

    # Use Raw to save the property
    props << RDoc::Markup::Raw.new(prop)

    if k.positive?
      props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
      props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless (i['placeholder'])
    else
      # First property, value only
      props << RDoc::Markup::Heading.new(2, value.to_s)
    end
  end

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

  doc << props
end

print doc.accept(mu)