Repository URL to install this package:
|
Version:
3.10.0 ▾
|
#!/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)