icalPal 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +23 -12
- data/bin/icalPal +51 -90
- data/bin/icalpal +322 -0
- data/icalPal.gemspec +32 -2
- data/lib/ToICalPal.rb +24 -27
- data/lib/defaults.rb +8 -6
- data/lib/event.rb +101 -80
- data/lib/icalPal.rb +18 -43
- data/lib/options.rb +52 -9
- data/lib/rdt.rb +22 -4
- data/lib/utils.rb +38 -0
- data/lib/version.rb +1 -1
- metadata +36 -6
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 3a74d33ac1b8d65466f6ef4820813ad983c7bb539438c11d12eb61daaa2af83f
         | 
| 4 | 
            +
              data.tar.gz: 3c2790143a7b5abde6304312099988de264cc767fde30c34dd8b7682a80bd782
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 031fae41c01699e5eb757dd0f838e702fccd0d65fb3ab1aca3a6dde20083ed5ec43c88a1bc2c43e11e0a77399aea21e860d3f13f7590d90a00f912d3d87bff1b
         | 
| 7 | 
            +
              data.tar.gz: 9e1cfa76f56f735406553d40d13e16ce786461ab331eea9cc0757ffe0858e92ef3c9bae843934f9a55098ec1a28d6e03852aed3ce3bc244a56de362b584de5bd
         | 
    
        data/README.md
    CHANGED
    
    | @@ -4,11 +4,27 @@ | |
| 4 4 |  | 
| 5 5 | 
             
            ## Description
         | 
| 6 6 |  | 
| 7 | 
            -
            icalPal is a command-line tool to query  | 
| 7 | 
            +
            icalPal is a command-line tool to query macOS Calendar and Reminders
         | 
| 8 8 | 
             
            databases for accounts, calendars, events, and tasks.  It can be run
         | 
| 9 9 | 
             
            on any system with [Ruby](https://www.ruby-lang.org/) and access to a
         | 
| 10 10 | 
             
            Calendar or Reminders database.
         | 
| 11 11 |  | 
| 12 | 
            +
            <!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            **Table of Contents**
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            - [Installation](#installation)
         | 
| 17 | 
            +
            - [Features](#features)
         | 
| 18 | 
            +
              - [Compatability with icalBuddy](#compatability-with-icalbuddy)
         | 
| 19 | 
            +
              - [Additional commands](#additional-commands)
         | 
| 20 | 
            +
              - [Additional options](#additional-options)
         | 
| 21 | 
            +
            - [Usage](#usage)
         | 
| 22 | 
            +
            - [Output formats](#output-formats)
         | 
| 23 | 
            +
            - [History](#history)
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            <!-- markdown-toc end -->
         | 
| 26 | 
            +
             | 
| 27 | 
            +
             | 
| 12 28 | 
             
            ## Installation
         | 
| 13 29 |  | 
| 14 30 | 
             
            As a system-wide Ruby gem:
         | 
| @@ -23,18 +39,11 @@ or in your home diretory: | |
| 23 39 | 
             
            gem install --user-install icalPal
         | 
| 24 40 | 
             
            ```
         | 
| 25 41 |  | 
| 26 | 
            -
            As a Homebrew formula:
         | 
| 27 | 
            -
             | 
| 28 | 
            -
            ```
         | 
| 29 | 
            -
            brew tap ajrosen/icalPal
         | 
| 30 | 
            -
            brew install icalPal
         | 
| 31 | 
            -
            ```
         | 
| 32 | 
            -
             | 
| 33 42 | 
             
            ## Features
         | 
| 34 43 |  | 
| 35 | 
            -
            ### Compatability with  | 
| 44 | 
            +
            ### Compatability with icalBuddy
         | 
| 36 45 |  | 
| 37 | 
            -
            icalPal tries to be compatible with icalBuddy for command-line options
         | 
| 46 | 
            +
            icalPal tries to be compatible with [icalBuddy](https://github.com/ali-rantakari/icalBuddy) for command-line options
         | 
| 38 47 | 
             
            and for output.  There are a some important differences to be aware
         | 
| 39 48 | 
             
            of.
         | 
| 40 49 |  | 
| @@ -62,7 +71,7 @@ Shows only reminders that have a due date. | |
| 62 71 | 
             
            * The ```-c``` part is optional, but you cannot abbreviate the command if you leave it off.
         | 
| 63 72 | 
             
            * Use ```-o``` to print the output in different formats.  CSV or JSON are intertesting choices.
         | 
| 64 73 | 
             
            * Copy your Calendar database file and use ```--db``` on it.
         | 
| 65 | 
            -
            * ```--it``` and ```--et``` will filter by Calendar *type*.  Types are **Local**, **Exchange**, **CalDAV**, **MobileMe**, **Subscribed**, and ** | 
| 74 | 
            +
            * ```--it``` and ```--et``` will filter by Calendar *type*.  Types are **Local**, **Exchange**, **CalDAV**, **MobileMe**, **Subscribed**, **Birthdays**, and **Reminders**
         | 
| 66 75 | 
             
            * ```--il``` and ```-el``` will filter by Reminder list
         | 
| 67 76 | 
             
            * ```--ia``` includes *only* all-day events (opposite of ```--ea```)
         | 
| 68 77 | 
             
            * ```--aep``` is like ```--iep```, but *adds* to the default property list instead of replacing it.
         | 
| @@ -201,6 +210,8 @@ Environment variables: | |
| 201 210 | 
             
                ICALPAL                 Additional arguments
         | 
| 202 211 | 
             
                ICALPAL_CONFIG          Additional arguments from a file
         | 
| 203 212 | 
             
                                        (default: /Users/ajr/.icalpal)
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                Do not quote or escape values.  Options set in ICALPAL override ICALPAL_CONFIG.  Options on the command line override ICALPAL.
         | 
| 204 215 | 
             
            ```
         | 
| 205 216 |  | 
| 206 217 | 
             
            ## Output formats
         | 
| @@ -212,7 +223,7 @@ CSV, Hash, JSON, XML, and YAML print all fields for all items in their | |
| 212 223 | 
             
            respective formats.  From that you can analyze the results any way you
         | 
| 213 224 | 
             
            like.
         | 
| 214 225 |  | 
| 215 | 
            -
            [Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built  | 
| 226 | 
            +
            [Remind](https://dianne.skoll.ca/projects/remind/) format uses a minimal implementation built into icalPal.
         | 
| 216 227 |  | 
| 217 228 | 
             
            Other formats such as ANSI, HTML, Markdown, RDoc, and TOC, use Ruby's
         | 
| 218 229 | 
             
            [RDoc::Markup](https://ruby-doc.org/stdlib-2.6.10/libdoc/rdoc/rdoc/RDoc/Markup.html)
         | 
    
        data/bin/icalPal
    CHANGED
    
    | @@ -11,6 +11,7 @@ begin | |
| 11 11 |  | 
| 12 12 | 
             
              require_relative '../lib/icalpal'
         | 
| 13 13 | 
             
              require_relative '../lib/options'
         | 
| 14 | 
            +
              require_relative '../lib/utils'
         | 
| 14 15 | 
             
            rescue LoadError => e
         | 
| 15 16 | 
             
              dep = e.message[/-- (.*)/, 1]
         | 
| 16 17 |  | 
| @@ -28,14 +29,14 @@ end | |
| 28 29 | 
             
            # All kids love log!
         | 
| 29 30 | 
             
            $log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
         | 
| 30 31 | 
             
            $log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
         | 
| 31 | 
            -
              ( | 
| 32 | 
            -
             | 
| 33 | 
            -
                        | 
| 34 | 
            -
             | 
| 35 | 
            -
             | 
| 36 | 
            -
             | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 32 | 
            +
              format("[%-5<sev>s] %<time>s [%<file>s:%<line>5s] - %<message>s\n",
         | 
| 33 | 
            +
                     {
         | 
| 34 | 
            +
                       sev: s,
         | 
| 35 | 
            +
                       time: t.strftime('%H:%M:%S.%L'),
         | 
| 36 | 
            +
                       file: caller(4, 1)[0].split('/')[-1].split(':')[0],
         | 
| 37 | 
            +
                       line: caller(4, 1)[0].split('/')[-1].split(':')[1],
         | 
| 38 | 
            +
                       message: m
         | 
| 39 | 
            +
                     })
         | 
| 39 40 | 
             
            end
         | 
| 40 41 |  | 
| 41 42 | 
             
            $opts = ICalPal::Options.new.parse_options
         | 
| @@ -56,9 +57,8 @@ $log.info("Options: #{$opts}") | |
| 56 57 | 
             
            # @param item[Object]
         | 
| 57 58 |  | 
| 58 59 | 
             
            def add(item)
         | 
| 59 | 
            -
              $log. | 
| 60 | 
            +
              $log.debug("Adding #{item.dump} #{item['UUID']} (#{item['title']})") if item['UUID']
         | 
| 60 61 |  | 
| 61 | 
            -
              item['sday'] = ICalPal::RDT.new(*item['sdate'].to_a[0..2]) if ICalPal::Event === item && item['sdate']
         | 
| 62 62 | 
             
              $items.push(item)
         | 
| 63 63 | 
             
            end
         | 
| 64 64 |  | 
| @@ -101,114 +101,84 @@ end | |
| 101 101 | 
             
            # Make sure we opened at least one database
         | 
| 102 102 | 
             
            unless success
         | 
| 103 103 | 
             
              $log.fatal('Could not open database')
         | 
| 104 | 
            -
             | 
| 104 | 
            +
             | 
| 105 | 
            +
              # SQLite3 does not return useful error messages.  If any databases
         | 
| 106 | 
            +
              # failed because of EPERM (operation not permitted), our parent
         | 
| 107 | 
            +
              # process might need Full Disk Access, and we should suggest that.
         | 
| 108 | 
            +
              eperm = 0
         | 
| 109 | 
            +
             | 
| 110 | 
            +
              $opts[:db].each do |db|
         | 
| 111 | 
            +
                # Use a real open to get a useful error
         | 
| 112 | 
            +
                File.open(db).close
         | 
| 113 | 
            +
              rescue Exception => e
         | 
| 114 | 
            +
                $log.fatal("#{e.class}: #{db}")
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                eperm = 1 if e.instance_of?(Errno::EPERM)
         | 
| 117 | 
            +
              end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
              if eperm.positive?
         | 
| 120 | 
            +
                $stderr.puts
         | 
| 121 | 
            +
                $stderr.puts "Does #{ancestor} have Full Disk Access in System Settings?"
         | 
| 122 | 
            +
                $stderr.puts
         | 
| 123 | 
            +
                $stderr.puts "Try running: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles'"
         | 
| 124 | 
            +
              end
         | 
| 105 125 |  | 
| 106 126 | 
             
              abort
         | 
| 107 127 | 
             
            end
         | 
| 108 128 |  | 
| 109 129 | 
             
            $log.info("Loaded #{$rows.count} #{klass} rows")
         | 
| 130 | 
            +
            $log.info("Window is #{$opts[:from]} to #{$opts[:to]}")
         | 
| 110 131 |  | 
| 111 132 |  | 
| 112 133 | 
             
            ##################################################
         | 
| 113 134 | 
             
            # Process the data
         | 
| 114 135 |  | 
| 115 136 | 
             
            # Add rows
         | 
| 116 | 
            -
            $rows. | 
| 117 | 
            -
              $log.debug("Row #{i}: #{row['ROWID']}:#{row['UUID']} - #{row['account']}/#{row['calendar']}/#{row['title']}")
         | 
| 118 | 
            -
             | 
| 137 | 
            +
            $rows.each do |row|
         | 
| 119 138 | 
             
              # --es/--is
         | 
| 120 | 
            -
              if $opts[:es].any? row['account']
         | 
| 121 | 
            -
             | 
| 122 | 
            -
                next
         | 
| 123 | 
            -
              end
         | 
| 124 | 
            -
             | 
| 125 | 
            -
              unless $opts[:is].empty? || ($opts[:is].any? row['account'])
         | 
| 126 | 
            -
                $log.debug(':is')
         | 
| 127 | 
            -
                next
         | 
| 128 | 
            -
              end
         | 
| 139 | 
            +
              next if $opts[:es].any? row['account']
         | 
| 140 | 
            +
              next unless $opts[:is].empty? || ($opts[:is].any? row['account'])
         | 
| 129 141 |  | 
| 130 142 | 
             
              # --ec/--ic
         | 
| 131 143 | 
             
              unless klass == ICalPal::Store || !row['calendar']
         | 
| 132 | 
            -
                if $opts[:ec].any? row['calendar']
         | 
| 133 | 
            -
             | 
| 134 | 
            -
                  next
         | 
| 135 | 
            -
                end
         | 
| 136 | 
            -
             | 
| 137 | 
            -
                unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
         | 
| 138 | 
            -
                  $log.debug(':ic')
         | 
| 139 | 
            -
                  next
         | 
| 140 | 
            -
                end
         | 
| 144 | 
            +
                next if $opts[:ec].any? row['calendar']
         | 
| 145 | 
            +
                next unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
         | 
| 141 146 | 
             
              end
         | 
| 142 147 |  | 
| 143 148 | 
             
              # Instantiate an item
         | 
| 144 149 | 
             
              item = klass.new(row)
         | 
| 145 150 |  | 
| 146 151 | 
             
              # --et/--it
         | 
| 147 | 
            -
              if $opts[:et].any? item['type']
         | 
| 148 | 
            -
             | 
| 149 | 
            -
                next
         | 
| 150 | 
            -
              end
         | 
| 151 | 
            -
             | 
| 152 | 
            -
              unless $opts[:it].empty? || ($opts[:it].any? item['type'])
         | 
| 153 | 
            -
                $log.debug(':it')
         | 
| 154 | 
            -
                next
         | 
| 155 | 
            -
              end
         | 
| 152 | 
            +
              next if $opts[:et].any? item['type']
         | 
| 153 | 
            +
              next unless $opts[:it].empty? || ($opts[:it].any? item['type'])
         | 
| 156 154 |  | 
| 157 155 | 
             
              # --el/--il
         | 
| 158 | 
            -
              if $opts[:el].any? item['list_name']
         | 
| 159 | 
            -
             | 
| 160 | 
            -
                next
         | 
| 161 | 
            -
              end
         | 
| 162 | 
            -
             | 
| 163 | 
            -
              unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
         | 
| 164 | 
            -
                $log.debug(':il')
         | 
| 165 | 
            -
                next
         | 
| 166 | 
            -
              end
         | 
| 156 | 
            +
              next if $opts[:el].any? item['list_name']
         | 
| 157 | 
            +
              next unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
         | 
| 167 158 |  | 
| 168 159 | 
             
              # --match
         | 
| 169 160 | 
             
              if $opts[:match]
         | 
| 170 161 | 
             
                r = $opts[:match].split('=')
         | 
| 171 162 |  | 
| 172 163 | 
             
                if item[r[0]].to_s.respond_to?(:match)
         | 
| 173 | 
            -
                  unless item[r[0]].to_s | 
| 174 | 
            -
                    $log.debug(':match')
         | 
| 175 | 
            -
                    next
         | 
| 176 | 
            -
                  end
         | 
| 164 | 
            +
                  next unless item[r[0]].to_s =~ Regexp.new(r[1], Regexp::IGNORECASE)
         | 
| 177 165 | 
             
                end
         | 
| 178 166 | 
             
              end
         | 
| 179 167 |  | 
| 180 168 | 
             
              if ICalPal::Event === item
         | 
| 181 169 | 
             
                # Check for all-day and cancelled events
         | 
| 182 | 
            -
                if $opts[:ea] && item['all_day'].positive?
         | 
| 183 | 
            -
             | 
| 184 | 
            -
             | 
| 185 | 
            -
                end
         | 
| 186 | 
            -
             | 
| 187 | 
            -
                if $opts[:ia] && !item['all_day'].positive?
         | 
| 188 | 
            -
                  $log.debug(':ia')
         | 
| 189 | 
            -
                  next
         | 
| 190 | 
            -
                end
         | 
| 191 | 
            -
             | 
| 192 | 
            -
                if item['status'] == :canceled
         | 
| 193 | 
            -
                  $log.debug(':canceled')
         | 
| 194 | 
            -
                  next
         | 
| 195 | 
            -
                end
         | 
| 170 | 
            +
                next if $opts[:ea] && item['all_day'].positive?
         | 
| 171 | 
            +
                next if $opts[:ia] && !item['all_day'].positive?
         | 
| 172 | 
            +
                next if item['status'] == :canceled
         | 
| 196 173 |  | 
| 197 174 | 
             
                (item['has_recurrences'].positive?)?
         | 
| 198 | 
            -
                  item.recurring.each { | | 
| 199 | 
            -
                  item.non_recurring.each { | | 
| 175 | 
            +
                  item.recurring.each { |j| add(j) } :
         | 
| 176 | 
            +
                  item.non_recurring.each { |j| add(j) }
         | 
| 200 177 | 
             
              else
         | 
| 201 178 | 
             
                # Check for dated reminders
         | 
| 202 179 | 
             
                if ICalPal::Reminder === item
         | 
| 203 | 
            -
                  if $opts[:dated] == 1 && item['due_date'].positive?
         | 
| 204 | 
            -
             | 
| 205 | 
            -
                    next
         | 
| 206 | 
            -
                  end
         | 
| 207 | 
            -
             | 
| 208 | 
            -
                  if $opts[:dated] == 2 && item['due_date'].zero?
         | 
| 209 | 
            -
                    $log.debug(':dated')
         | 
| 210 | 
            -
                    next
         | 
| 211 | 
            -
                  end
         | 
| 180 | 
            +
                  next if $opts[:dated] == 1 && item['due_date'].positive?
         | 
| 181 | 
            +
                  next if $opts[:dated] == 2 && item['due_date'].zero?
         | 
| 212 182 | 
             
                end
         | 
| 213 183 |  | 
| 214 184 | 
             
                add(item)
         | 
| @@ -227,11 +197,10 @@ end | |
| 227 197 |  | 
| 228 198 | 
             
            # Sort the rows
         | 
| 229 199 | 
             
            begin
         | 
| 230 | 
            -
              $log.info("Sorting | 
| 200 | 
            +
              $log.info("Sorting #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
         | 
| 231 201 |  | 
| 232 202 | 
             
              $items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
         | 
| 233 203 | 
             
              $items.reverse! if $opts[:reverse]
         | 
| 234 | 
            -
              $items.uniq!
         | 
| 235 204 | 
             
            rescue Exception => e
         | 
| 236 205 | 
             
              $log.info("Sorting failed: #{e}\n")
         | 
| 237 206 | 
             
            end
         | 
| @@ -277,7 +246,7 @@ unless mu | |
| 277 246 | 
             
                   when 'json' then items.map { |i| i.self }.to_json
         | 
| 278 247 | 
             
                   when 'xml'
         | 
| 279 248 | 
             
                     xml = items.map { |i| "<#{$opts[:cmd].chomp('s')}>#{i.to_xml}</#{$opts[:cmd].chomp('s')}>" }
         | 
| 280 | 
            -
                     "<#{$opts[:cmd]}>\n#{xml.join | 
| 249 | 
            +
                     "<#{$opts[:cmd]}>\n#{xml.join}</#{$opts[:cmd]}>"
         | 
| 281 250 | 
             
                   when 'yaml' then items.map { |i| i.self }.to_yaml
         | 
| 282 251 | 
             
                   when 'remind' then items.map { |i|
         | 
| 283 252 | 
             
                       "REM #{i['sdate'].strftime('%F AT %R')} " +
         | 
| @@ -342,14 +311,6 @@ items.each_with_index do |i, j| | |
| 342 311 | 
             
                  # First property, value only
         | 
| 343 312 | 
             
                  props << RDoc::Markup::Heading.new(2, value.to_s)
         | 
| 344 313 | 
             
                end
         | 
| 345 | 
            -
             | 
| 346 | 
            -
                # unless k.positive?
         | 
| 347 | 
            -
                #   # First property, value only
         | 
| 348 | 
            -
                #   props << RDoc::Markup::Heading.new(2, value.to_s)
         | 
| 349 | 
            -
                # else
         | 
| 350 | 
            -
                #   props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
         | 
| 351 | 
            -
                #   props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless (i['placeholder'])
         | 
| 352 | 
            -
                # end
         | 
| 353 314 | 
             
              end
         | 
| 354 315 |  | 
| 355 316 | 
             
              # Print it
         | 
    
        data/bin/icalpal
    ADDED
    
    | @@ -0,0 +1,322 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            begin
         | 
| 4 | 
            +
              require 'logger'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              require 'csv'
         | 
| 7 | 
            +
              require 'json'
         | 
| 8 | 
            +
              require 'rdoc'
         | 
| 9 | 
            +
              require 'sqlite3'
         | 
| 10 | 
            +
              require 'yaml'
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              require_relative '../lib/icalpal'
         | 
| 13 | 
            +
              require_relative '../lib/options'
         | 
| 14 | 
            +
              require_relative '../lib/utils'
         | 
| 15 | 
            +
            rescue LoadError => e
         | 
| 16 | 
            +
              dep = e.message[/-- (.*)/, 1]
         | 
| 17 | 
            +
             | 
| 18 | 
            +
              $stderr.puts "FATAL: icalpal is missing a dependency: #{dep}"
         | 
| 19 | 
            +
              $stderr.puts
         | 
| 20 | 
            +
              $stderr.puts "Install with 'gem install --user-install #{dep}'"
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              exit
         | 
| 23 | 
            +
            end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
             | 
| 26 | 
            +
            ##################################################
         | 
| 27 | 
            +
            # Load options
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            # All kids love log!
         | 
| 30 | 
            +
            $log = Logger.new(STDERR, { level: $defaults[:common][:debug] })
         | 
| 31 | 
            +
            $log.formatter = proc do |s, t, _p, m| # Severity, time, progname, msg
         | 
| 32 | 
            +
              format("[%-5<sev>s] %<time>s [%<file>s:%<line>5s] - %<message>s\n",
         | 
| 33 | 
            +
                     {
         | 
| 34 | 
            +
                       sev: s,
         | 
| 35 | 
            +
                       time: t.strftime('%H:%M:%S.%L'),
         | 
| 36 | 
            +
                       file: caller(4, 1)[0].split('/')[-1].split(':')[0],
         | 
| 37 | 
            +
                       line: caller(4, 1)[0].split('/')[-1].split(':')[1],
         | 
| 38 | 
            +
                       message: m
         | 
| 39 | 
            +
                     })
         | 
| 40 | 
            +
            end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
            $opts = ICalPal::Options.new.parse_options
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            $rows = []                      # Rows from the database
         | 
| 45 | 
            +
            $items = []                     # Items to be printed
         | 
| 46 | 
            +
             | 
| 47 | 
            +
             | 
| 48 | 
            +
            ##################################################
         | 
| 49 | 
            +
            # All kids love log!
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            $log.info("Options: #{$opts}")
         | 
| 52 | 
            +
             | 
| 53 | 
            +
             | 
| 54 | 
            +
            ##################################################
         | 
| 55 | 
            +
            # Add an item to the list
         | 
| 56 | 
            +
            #
         | 
| 57 | 
            +
            # @param item[Object]
         | 
| 58 | 
            +
             | 
| 59 | 
            +
            def add(item)
         | 
| 60 | 
            +
              $log.debug("Adding #{item.dump} #{item['UUID']} (#{item['title']})") if item['UUID']
         | 
| 61 | 
            +
             | 
| 62 | 
            +
              $items.push(item)
         | 
| 63 | 
            +
            end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
             | 
| 66 | 
            +
            ##################################################
         | 
| 67 | 
            +
            # Load the data
         | 
| 68 | 
            +
             | 
| 69 | 
            +
            # What are we getting?
         | 
| 70 | 
            +
            klass = ICalPal.call($opts[:cmd])
         | 
| 71 | 
            +
            success = false
         | 
| 72 | 
            +
             | 
| 73 | 
            +
            # Get it
         | 
| 74 | 
            +
            $opts[:db].each do |db|
         | 
| 75 | 
            +
              $log.debug("Trying #{db}")
         | 
| 76 | 
            +
             | 
| 77 | 
            +
              if klass == ICalPal::Reminder
         | 
| 78 | 
            +
                begin
         | 
| 79 | 
            +
                  # Load all .sqlite files
         | 
| 80 | 
            +
                  $log.debug("Loading *.sqlite in #{db}")
         | 
| 81 | 
            +
                  Dir.glob("#{db}/*.sqlite").each do |d|
         | 
| 82 | 
            +
                    $rows += ICalPal.load_data(d, klass::QUERY)
         | 
| 83 | 
            +
                    success = true
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  rescue SQLite3::CantOpenException
         | 
| 86 | 
            +
                    # Non-fatal exception, try the next one
         | 
| 87 | 
            +
                  end
         | 
| 88 | 
            +
                end
         | 
| 89 | 
            +
              else
         | 
| 90 | 
            +
                # Load database
         | 
| 91 | 
            +
                begin
         | 
| 92 | 
            +
                  $rows += ICalPal.load_data(db, klass::QUERY)
         | 
| 93 | 
            +
                  success = true
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                rescue SQLite3::CantOpenException
         | 
| 96 | 
            +
                  # Non-fatal exception, try the next one
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
              end
         | 
| 99 | 
            +
            end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
            # Make sure we opened at least one database
         | 
| 102 | 
            +
            unless success
         | 
| 103 | 
            +
              $log.fatal('Could not open database')
         | 
| 104 | 
            +
             | 
| 105 | 
            +
              # SQLite3 does not return useful error messages.  If any databases
         | 
| 106 | 
            +
              # failed because of EPERM (operation not permitted), our parent
         | 
| 107 | 
            +
              # process might need Full Disk Access, and we should suggest that.
         | 
| 108 | 
            +
              eperm = 0
         | 
| 109 | 
            +
             | 
| 110 | 
            +
              $opts[:db].each do |db|
         | 
| 111 | 
            +
                # Use a real open to get a useful error
         | 
| 112 | 
            +
                File.open(db).close
         | 
| 113 | 
            +
              rescue Exception => e
         | 
| 114 | 
            +
                $log.fatal("#{e.class}: #{db}")
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                eperm = 1 if e.instance_of?(Errno::EPERM)
         | 
| 117 | 
            +
              end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
              if eperm.positive?
         | 
| 120 | 
            +
                $stderr.puts
         | 
| 121 | 
            +
                $stderr.puts "Does #{ancestor} have Full Disk Access in System Settings?"
         | 
| 122 | 
            +
                $stderr.puts
         | 
| 123 | 
            +
                $stderr.puts "Try running: open 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles'"
         | 
| 124 | 
            +
              end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
              abort
         | 
| 127 | 
            +
            end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
            $log.info("Loaded #{$rows.count} #{klass} rows")
         | 
| 130 | 
            +
            $log.info("Window is #{$opts[:from]} to #{$opts[:to]}")
         | 
| 131 | 
            +
             | 
| 132 | 
            +
             | 
| 133 | 
            +
            ##################################################
         | 
| 134 | 
            +
            # Process the data
         | 
| 135 | 
            +
             | 
| 136 | 
            +
            # Add rows
         | 
| 137 | 
            +
            $rows.each do |row|
         | 
| 138 | 
            +
              # --es/--is
         | 
| 139 | 
            +
              next if $opts[:es].any? row['account']
         | 
| 140 | 
            +
              next unless $opts[:is].empty? || ($opts[:is].any? row['account'])
         | 
| 141 | 
            +
             | 
| 142 | 
            +
              # --ec/--ic
         | 
| 143 | 
            +
              unless klass == ICalPal::Store || !row['calendar']
         | 
| 144 | 
            +
                next if $opts[:ec].any? row['calendar']
         | 
| 145 | 
            +
                next unless $opts[:ic].empty? || ($opts[:ic].any? row['calendar'])
         | 
| 146 | 
            +
              end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
              # Instantiate an item
         | 
| 149 | 
            +
              item = klass.new(row)
         | 
| 150 | 
            +
             | 
| 151 | 
            +
              # --et/--it
         | 
| 152 | 
            +
              next if $opts[:et].any? item['type']
         | 
| 153 | 
            +
              next unless $opts[:it].empty? || ($opts[:it].any? item['type'])
         | 
| 154 | 
            +
             | 
| 155 | 
            +
              # --el/--il
         | 
| 156 | 
            +
              next if $opts[:el].any? item['list_name']
         | 
| 157 | 
            +
              next unless $opts[:il].empty? || ($opts[:il].any? item['list_name'])
         | 
| 158 | 
            +
             | 
| 159 | 
            +
              # --match
         | 
| 160 | 
            +
              if $opts[:match]
         | 
| 161 | 
            +
                r = $opts[:match].split('=')
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                if item[r[0]].to_s.respond_to?(:match)
         | 
| 164 | 
            +
                  next unless item[r[0]].to_s =~ Regexp.new(r[1], Regexp::IGNORECASE)
         | 
| 165 | 
            +
                end
         | 
| 166 | 
            +
              end
         | 
| 167 | 
            +
             | 
| 168 | 
            +
              if ICalPal::Event === item
         | 
| 169 | 
            +
                # Check for all-day and cancelled events
         | 
| 170 | 
            +
                next if $opts[:ea] && item['all_day'].positive?
         | 
| 171 | 
            +
                next if $opts[:ia] && !item['all_day'].positive?
         | 
| 172 | 
            +
                next if item['status'] == :canceled
         | 
| 173 | 
            +
             | 
| 174 | 
            +
                (item['has_recurrences'].positive?)?
         | 
| 175 | 
            +
                  item.recurring.each { |j| add(j) } :
         | 
| 176 | 
            +
                  item.non_recurring.each { |j| add(j) }
         | 
| 177 | 
            +
              else
         | 
| 178 | 
            +
                # Check for dated reminders
         | 
| 179 | 
            +
                if ICalPal::Reminder === item
         | 
| 180 | 
            +
                  next if $opts[:dated] == 1 && item['due_date'].positive?
         | 
| 181 | 
            +
                  next if $opts[:dated] == 2 && item['due_date'].zero?
         | 
| 182 | 
            +
                end
         | 
| 183 | 
            +
             | 
| 184 | 
            +
                add(item)
         | 
| 185 | 
            +
              end
         | 
| 186 | 
            +
            end
         | 
| 187 | 
            +
             | 
| 188 | 
            +
            # Add placeholders for empty days
         | 
| 189 | 
            +
            if $opts[:sed] && $opts[:sd] && klass == ICalPal::Event
         | 
| 190 | 
            +
              days = $items.collect { |i| i['sday'] }.uniq.sort
         | 
| 191 | 
            +
             | 
| 192 | 
            +
              $opts[:days].times do |n|
         | 
| 193 | 
            +
                day = $opts[:from] + n
         | 
| 194 | 
            +
                $items.push(klass.new(day)) unless days.any? { |i| i.to_s == day.to_s }
         | 
| 195 | 
            +
              end
         | 
| 196 | 
            +
            end
         | 
| 197 | 
            +
             | 
| 198 | 
            +
            # Sort the rows
         | 
| 199 | 
            +
            begin
         | 
| 200 | 
            +
              $log.info("Sorting #{$items.count} items by #{[ $opts[:sep], $opts[:sort], 'sdate' ]}, reverse #{$opts[:reverse].inspect}")
         | 
| 201 | 
            +
             | 
| 202 | 
            +
              $items.sort_by! { |i| [ i[$opts[:sep]], i[$opts[:sort]], i['sdate'] ] }
         | 
| 203 | 
            +
              $items.reverse! if $opts[:reverse]
         | 
| 204 | 
            +
            rescue Exception => e
         | 
| 205 | 
            +
              $log.info("Sorting failed: #{e}\n")
         | 
| 206 | 
            +
            end
         | 
| 207 | 
            +
             | 
| 208 | 
            +
            $log.debug("#{$items.count} items remain")
         | 
| 209 | 
            +
             | 
| 210 | 
            +
            # Configure formatting
         | 
| 211 | 
            +
            mu = case $opts[:output]
         | 
| 212 | 
            +
                 when 'ansi' then RDoc::Markup::ToAnsi.new
         | 
| 213 | 
            +
                 when 'default' then RDoc::Markup::ToICalPal.new($opts)
         | 
| 214 | 
            +
                 when 'html'
         | 
| 215 | 
            +
                   rdoc = RDoc::Options.new
         | 
| 216 | 
            +
                   rdoc.pipe = true
         | 
| 217 | 
            +
                   rdoc.output_decoration = false
         | 
| 218 | 
            +
                   RDoc::Markup::ToHtml.new(rdoc)
         | 
| 219 | 
            +
                 when 'md' then RDoc::Markup::ToMarkdown.new
         | 
| 220 | 
            +
                 when 'rdoc' then RDoc::Markup::ToRdoc.new
         | 
| 221 | 
            +
                 when 'toc' then RDoc::Markup::ToTableOfContents.new
         | 
| 222 | 
            +
                 end
         | 
| 223 | 
            +
             | 
| 224 | 
            +
             | 
| 225 | 
            +
            ##################################################
         | 
| 226 | 
            +
            # Print the data
         | 
| 227 | 
            +
             | 
| 228 | 
            +
            items = $items[0..$opts[:li] - 1]
         | 
| 229 | 
            +
             | 
| 230 | 
            +
            unless mu
         | 
| 231 | 
            +
              $log.debug("Output in #{$opts[:output]} format")
         | 
| 232 | 
            +
             | 
| 233 | 
            +
              puts case $opts[:output]
         | 
| 234 | 
            +
                   when 'csv'
         | 
| 235 | 
            +
                     # Get all headers
         | 
| 236 | 
            +
                     headers = []
         | 
| 237 | 
            +
                     items.each { |i| headers += i.keys }
         | 
| 238 | 
            +
                     headers.uniq!
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                     # Populate a CSV::Table
         | 
| 241 | 
            +
                     table = CSV::Table.new([], headers: headers)
         | 
| 242 | 
            +
                     items.each { |i| table << i.to_csv(headers) }
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                     table
         | 
| 245 | 
            +
                   when 'hash' then items.map { |i| i.self }
         | 
| 246 | 
            +
                   when 'json' then items.map { |i| i.self }.to_json
         | 
| 247 | 
            +
                   when 'xml'
         | 
| 248 | 
            +
                     xml = items.map { |i| "<#{$opts[:cmd].chomp('s')}>#{i.to_xml}</#{$opts[:cmd].chomp('s')}>" }
         | 
| 249 | 
            +
                     "<#{$opts[:cmd]}>\n#{xml.join}</#{$opts[:cmd]}>"
         | 
| 250 | 
            +
                   when 'yaml' then items.map { |i| i.self }.to_yaml
         | 
| 251 | 
            +
                   when 'remind' then items.map { |i|
         | 
| 252 | 
            +
                       "REM #{i['sdate'].strftime('%F AT %R')} " +
         | 
| 253 | 
            +
                         "DURATION #{((i['edate'] - i['sdate']).to_f * 1440).to_i} " +
         | 
| 254 | 
            +
                         "MSG #{i['title']}"
         | 
| 255 | 
            +
                     }.join("\n")
         | 
| 256 | 
            +
                   else abort "No formatter for #{$opts[:output]}"
         | 
| 257 | 
            +
                   end
         | 
| 258 | 
            +
             | 
| 259 | 
            +
              exit
         | 
| 260 | 
            +
            end
         | 
| 261 | 
            +
             | 
| 262 | 
            +
            $log.debug("Formatting with #{mu.inspect}")
         | 
| 263 | 
            +
             | 
| 264 | 
            +
            doc = RDoc::Markup::Document.new
         | 
| 265 | 
            +
            section = nil
         | 
| 266 | 
            +
             | 
| 267 | 
            +
            items.each_with_index do |i, j|
         | 
| 268 | 
            +
              $log.debug("Print #{j}: #{i.inspect}")
         | 
| 269 | 
            +
             | 
| 270 | 
            +
              # --li
         | 
| 271 | 
            +
              break if $opts[:li].positive? && j >= $opts[:li]
         | 
| 272 | 
            +
             | 
| 273 | 
            +
              # Use RDoc::Markup::Verbatim to save the item
         | 
| 274 | 
            +
              v = RDoc::Markup::Verbatim.new
         | 
| 275 | 
            +
              v.format = i
         | 
| 276 | 
            +
              doc << v
         | 
| 277 | 
            +
             | 
| 278 | 
            +
              # Sections
         | 
| 279 | 
            +
              if $opts[:sep] && section != i[$opts[:sep]]
         | 
| 280 | 
            +
                $log.debug("New section '#{$opts[:sep]}': #{i[$opts[:sep]]}")
         | 
| 281 | 
            +
             | 
| 282 | 
            +
                doc << RDoc::Markup::Raw.new($opts[:sep])
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                doc << RDoc::Markup::BlankLine.new if j.positive?
         | 
| 285 | 
            +
                doc << RDoc::Markup::Heading.new(1, i[$opts[:sep]].to_s)
         | 
| 286 | 
            +
                doc << RDoc::Markup::Rule.new(0)
         | 
| 287 | 
            +
             | 
| 288 | 
            +
                section = i[$opts[:sep]]
         | 
| 289 | 
            +
              end
         | 
| 290 | 
            +
             | 
| 291 | 
            +
              # Item
         | 
| 292 | 
            +
              props = RDoc::Markup::List.new(:BULLET)
         | 
| 293 | 
            +
             | 
| 294 | 
            +
              # Properties
         | 
| 295 | 
            +
              $opts[:props].each_with_index do |prop, k|
         | 
| 296 | 
            +
                value = i[prop]
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                next unless value
         | 
| 299 | 
            +
                next if Array === value && !value[0]
         | 
| 300 | 
            +
                next if String === value && value.empty?
         | 
| 301 | 
            +
             | 
| 302 | 
            +
                $log.debug("#{prop}: #{value}")
         | 
| 303 | 
            +
             | 
| 304 | 
            +
                # Use Raw to save the property
         | 
| 305 | 
            +
                props << RDoc::Markup::Raw.new(prop)
         | 
| 306 | 
            +
             | 
| 307 | 
            +
                if k.positive?
         | 
| 308 | 
            +
                  props << RDoc::Markup::BlankLine.new unless (i['placeholder'] || $opts[:ps])
         | 
| 309 | 
            +
                  props << RDoc::Markup::ListItem.new(prop, RDoc::Markup::Paragraph.new(value)) unless (i['placeholder'])
         | 
| 310 | 
            +
                else
         | 
| 311 | 
            +
                  # First property, value only
         | 
| 312 | 
            +
                  props << RDoc::Markup::Heading.new(2, value.to_s)
         | 
| 313 | 
            +
                end
         | 
| 314 | 
            +
              end
         | 
| 315 | 
            +
             | 
| 316 | 
            +
              # Print it
         | 
| 317 | 
            +
              props << RDoc::Markup::BlankLine.new unless props.empty?
         | 
| 318 | 
            +
             | 
| 319 | 
            +
              doc << props
         | 
| 320 | 
            +
            end
         | 
| 321 | 
            +
             | 
| 322 | 
            +
            print doc.accept(mu)
         | 
    
        data/icalPal.gemspec
    CHANGED
    
    | @@ -15,13 +15,43 @@ EOF | |
| 15 15 | 
             
              s.homepage	= "https://github.com/ajrosen/#{s.name}"
         | 
| 16 16 | 
             
              s.licenses	= [ 'GPL-3.0-or-later' ]
         | 
| 17 17 |  | 
| 18 | 
            +
              s.metadata = {
         | 
| 19 | 
            +
                'bug_tracker_uri' => "https://github.com/ajrosen/#{s.name}/issues",
         | 
| 20 | 
            +
                'rubygems_mfa_required' => 'true'
         | 
| 21 | 
            +
              }
         | 
| 22 | 
            +
             | 
| 18 23 | 
             
              s.files	= Dir["#{s.name}.gemspec", 'bin/*', 'lib/*.rb']
         | 
| 19 24 | 
             
              s.executables	= [ "#{s.name}" ]
         | 
| 20 25 | 
             
              s.extra_rdoc_files = [ 'README.md' ]
         | 
| 21 26 |  | 
| 22 | 
            -
               | 
| 23 | 
            -
               | 
| 27 | 
            +
              # The macOS and Homebrew versions of rubygems have incompatible
         | 
| 28 | 
            +
              # requirements for sqlite3.
         | 
| 29 | 
            +
              #
         | 
| 30 | 
            +
              # macOS comes with version 1.3.13, so it does not need to be added
         | 
| 31 | 
            +
              # as a dependency, but it cannot install anything newer:
         | 
| 32 | 
            +
              #
         | 
| 33 | 
            +
              # requires Ruby version >= 3.0, < 3.4.dev. The current ruby version is 2.6.10.
         | 
| 34 | 
            +
              #
         | 
| 35 | 
            +
              # Homebrew's Ruby formula does not come with sqlite3, so it does
         | 
| 36 | 
            +
              # need to be added as a dependency, but it cannot install version
         | 
| 37 | 
            +
              # 1.3.13:
         | 
| 38 | 
            +
              #
         | 
| 39 | 
            +
              # error: call to undeclared function
         | 
| 40 | 
            +
              #
         | 
| 41 | 
            +
              # So we must call add_dependency, but iff we are not building with
         | 
| 42 | 
            +
              # macOS' Ruby installation.
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              s.add_dependency 'nokogiri-plist', '~> 0.5.0'
         | 
| 45 | 
            +
              s.add_dependency 'sqlite3', '~> 2.6.0' unless s.rubygems_version == `/usr/bin/gem --version`.strip
         | 
| 46 | 
            +
              s.add_dependency 'timezone', '>= 0.99', '~> 1.3.0'
         | 
| 24 47 |  | 
| 25 48 | 
             
              s.bindir = 'bin'
         | 
| 26 49 | 
             
              s.required_ruby_version = '>= 2.6.0'
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              s.post_install_message = <<-EOF
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            Note: #{ICalPal::NAME} requires "Full Disk Access" in System Settings to access your calendar.
         | 
| 54 | 
            +
            Make sure the program that runs #{ICalPal::NAME}, not #{ICalPal::NAME} itself, has these permissions.
         | 
| 55 | 
            +
             | 
| 56 | 
            +
            EOF
         | 
| 27 57 | 
             
            end
         |