Updated script to create reminders or calendar events from tasks

Thanks a lot Victor Much appreciated
I use your script to create event. Only one thing is missing, the duration of the event I can’t set it
Any idea on this?
For reminder it is a very good approach when multiple reminder must be created
Thanks again!!

What do you mean? I am not sure what you are trying to accomplish.

Hi Victor
I m using time blocking concept in my agenda
Where I block a certain amount of time for a specific task
May be a @duration tag will help me to have an event duration in my calendar from a task in task paper
It will block the allowed time for this task in my agenda
AD I m not a dev and your script is just amazing providing a very good start in my workflow may be you can help here
Thanks
Patrick

Hey Patrick. This script is from Brett. I would recommend you to read his documentation and explanation (there was a link to it on the original post.) I am also not a dev, but I can figure things out with some help here and there. I think (Not sure right now if I or someone here help me to accomplish this) that all I did was add an AppleScript to add this to the calendar. The AppleScript is doing all the magic.

Right now the script reserves a block of 60 min. Adding a duration tag seems to be doable. Just let me ask you the following.

Is the script working right now in your computer?

If it is working, even if it is not doing what you want, then I can help you figure things out. It will just take me some time for me to remember. Add another post and tag me, and we will figure things there.

I checked the script. Honestly, it should be rewritten to use the ruby library to make it easy to understand and modify. Like I said, I didn’t write this script to begin with. I modified it.

Although for what you are trying to do, it may be better to make a script that you can run with Alfred or KeyboardMaestro or something like that. Maybe @complexpoint can help you with this. It seems as if what you want to do is look for a tag like @reserve_time(2021-03-03 13:01) @duration(60) where the number inside the duration tag is “minutes.” The script can change the @reserve_time to @reserved_time once the event has been added to the calendar. Is that what you are thinking of @Patrick68?

Hi Victor,
Thanks a lot for the time spend here
Yes it is exactly what I would like to have but no need to create a @reserve_time Tag just needed the @duration tag used in conjunction with @event tag If I have the duration tag I can change the default duration in “minutes” of the specified event I would create. I@duration exist the script will change the duration otherwyse it will keep the default duration.
This will allow me to use time block for my task where I know the duration / the allowed time.
Thanks again
Patrick

Hi All
I have amended the script provided by @Victor to add the @durartion in min

Enjoy with it

#!/usr/bin/ruby
# == Synopsis
#   This tool will search for @remind() tags in the specified notes folder.
#
#   It searches ".md", ".txt", ".ft" and ".taskpaper" files. It also works with Day One journal folders.
#
#   It expects an ISO 8601 format date (2013-05-01) with optional 24-hour time (2013-05-01 15:30).
#   Put `@remind(2013-05-01 06:00)` anywhere in a note to have a reminder go off on the first run after that time.
#
#
#   Reminders on their own line with no other text will send the entire note as the reminder with the filename being the subject line. If a @reminder tag is on a line with other text, only that line will be used as the title and the content.
#
#   If you include a double-quoted string at the end of the remind tag value, it will override the default reminder title. `@remind(2013-05-24 "This is the override")` would create a reminder called "This is the override", ignoring any other text on the line or the name of the file. Additional text on the line or the entire note (in the case of a @remind tag on its own line) will still be included in the note, if the notification method supports that.
#
#   This script is intended to be run on a schedule. Check for reminders every 30-60 minutes using cron or launchd.
#
#   Use the -n option to send Mountain Lion notifications instead of terminal output. Clicking a notification will open the related file in nvALT.
#   Notifications require that the 'terminal-notifier' gem be installed:
#
#       sudo gem install 'terminal-notifier'
#
#   Use the -e ADDRESS option to send an email with the title of the note as the subject and the contents of the note as the body to the specified address. Separate multiple emails with commas. The contents of the note will be rendered with MultiMarkdown, which needs to exist at /usr/local/bin/multimarkdown.
#
#   If the file has a ".taskpaper" extension, it will be converted to Markdown for formatting before processing with MultiMarkdown.
#
#   The `-m` option will add a reminder to Reminders.app in Mountain Lion, due immediately, that will show up on iCloud-synced iOS devices as well.
#
#   The `-f FOLDER` option allows you to specify a directory where a file named with the reminder title will be saved. The note for the reminder will be the file contents. This is useful, for example, with IFTTT.com. You can save a file to a public Dropbox folder, have IFTTT notice it and take any number of actions on it.
# == Examples
#
#     nvremind.rb ~/Dropbox/nvALT
#
#   Other examples:
#     nvremind.rb ~/Dropbox/nvALT
#     nvremind.rb -n ~/Dropbox/nvALT
#     nvremind.rb -e me@gmail.com ~/Dropbox/nvALT
#     nvremind.rb -mn -e me@gmail.com ~/Dropbox/nvALT
# == Usage
#   nvremind.rb [options] notes_folder
#
#   For help use: nvremind.rb -h
#
#   See <http://brettterpstra.com/projects/nvremind> for more information
#
# == Options
#   -h, --help            Displays help message
#   -v, --version         Display the version, then exit
#   -V, --verbose         Verbose output
#   -z, --no-replace      Don't updated @remind() tags with @reminded() after notification
#   -n, --notify          Use terminal-notifier to post Mountain Lion notifications
#   -m, --reminders       Add an item to the Reminders list in Reminders.app (due immediately)
#   --reminder-list LIST  List to use in Reminders.app (default "Reminders")
#   -f folder             Save a file to FOLDER named with the task title, note as contents
#   -e EMAIL[,EMAIL], --email EMAIL[,EMAIL] Send an email with note contents to the specified address
#
# == Author
#   Brett Terpstra
#
# == Copyright
#   Copyright (c) 2013 Brett Terpstra. Licensed under the MIT License:
#   http://www.opensource.org/licenses/mit-license.php

require 'date'
require 'cgi'
require 'time'
require 'optparse'
require 'ostruct'
require 'shellwords'

NVR_VERSION = '1.0.6'.freeze

if RUBY_VERSION.to_f > 1.9
  Encoding.default_external = Encoding::UTF_8
  Encoding.default_internal = Encoding::UTF_8
end

class TaskPaper
  def tp2md(input)
header = input.scan(/Format\: .*$/)
output = ''
prevlevel = 0
begin
  input.split("\n").each do|line|
    if line =~ /^(\t+)?(.*?):(\s(.*?))?$/
      tabs = Regexp.last_match(1)
      project = Regexp.last_match(2)
      if tabs.nil?
        output += "\n## #{project} ##\n\n"
        prevlevel = 0
      else
        output += "#{tabs.gsub(/^\t/, '')}* **#{project.gsub(/^\s*-\s*/, '')}**\n"
        prevlevel = tabs.length
      end
    elsif line =~ /^(\t+)?\- (.*)$/
      task = Regexp.last_match(2)
      tabs = Regexp.last_match(1).nil? ? '' : Regexp.last_match(1)
      task = "*<del>#{task}</del>*" if task =~ /@done/
      if tabs.length - prevlevel > 1
        tabs = "\t"
        prevlevel.times { |_i| tabs += "\t" }
      end
      tabs = '' if prevlevel == 0 && tabs.length > 1
      output += "#{tabs.gsub(/^\t/, '')}* #{task.strip}\n"
      prevlevel = tabs.length
    else
      next if line =~ /^\s*$/
      tabs = ''
      (prevlevel - 1).times { |_i| tabs += "\t" }
      output += "\n#{tabs}*#{line.strip}*\n"
    end
  end
rescue => err
  puts "Exception: #{err}"
  err
end
o = ''
o += header.join("\n") + "\n" unless header.nil?
o += '<style>.tag strong {font-weight:normal;color:#555} .tag a {text-decoration:none;border:none;color:#777}</style>'
# o += output.gsub(/\[\[(.*?)\]\]/,"<a href=\"nvalt://find/\\1\">\\1</a>").gsub(/(@[^ \n\r\(]+)((\()([^\)]+)(\)))?/,"<em class=\"tag\"><a href=\"nvalt://find/\\0\">\\1\\3<strong>\\4</strong>\\5</a></em>")
o
  end
end

class Reminder
  attr_reader :options

  def initialize(arguments)
@arguments = arguments

@options = OpenStruct.new
@options.remove = true
@options.preserve_time = true
@options.verbose = false
@options.notify = false
@options.email = false
@options.file = false
@options.stdout = true
@options.reminders = false
@options.reminder_list = 'Reminders'
  end

  def run
if parsed_options? && arguments_valid?

  puts "Start at #{DateTime.now}\n\n" if @options.verbose

  output_options if @options.verbose # [Optional]

  process_arguments
  process_command

  puts "\nFinished at #{DateTime.now}" if @options.verbose

else
  output_usage
end
  end

  def e_as(str)
str.to_s.gsub(/(?=["\\])/, '\\')
  end

  protected

  def parsed_options?
opts = OptionParser.new
opts.on('-v', '--version', 'Display version information') { output_version; exit 0 }
opts.on('-V', '--verbose', 'Verbose output') { @options.verbose = true }
opts.on('-z', '--no-replace', "Don't updated @remind() tags with @reminded() after notification") { @options.remove = false }
opts.on('--no-preserve-time', 'Allow file modification time to change') { @options.preserve_time = false }
opts.on('-n', '--notify', 'Use terminal-notifier to post Mountain Lion notifications') { @options.notify = true }
opts.on('-r', '--replace', 'Deprecated, no effect')      {} # deprecated, backward compatibility only
opts.on('-m', '--reminders', 'Add an item to the Reminders list in Reminders.app (due immediately)') { @options.reminders = true }
opts.on('--reminder-list LIST', "List to use in Reminders.app (default 'Reminders')") { |list| @options.reminder_list = list }
opts.on('-f FOLDER', '--file FOLDER', 'Add a file to the specified folder') do |folder|
  if File.exist?(File.expand_path(folder))
    @options.file = File.expand_path(folder)
  else
    puts "Invalid folder specified for -f (#{folder} does not exist)"
    Process.exit 1
  end
end
opts.on('-e EMAIL[,EMAIL]', '--email EMAIL[,EMAIL]', 'Send an email with note contents to the specified address') do |emails|
  @options.email = []
  emails.split(/,/).each do|email|
    @options.email.push(email.strip)
  end
end
opts.on('-h', '--help', 'Display this screen') do
  puts opts
  puts
  output_usage
end
begin
  opts.parse!(@arguments)
rescue
  return false
end

true
  end

  def output_options
puts "Options:\n"

@options.marshal_dump.each do |name, val|
  puts "  #{name} = #{val}"
end
  end

  def arguments_valid?
@notes_dir = []
unless @arguments[0].nil?
  @arguments[0].split(',').each do|path|
    @notes_dir.push(File.expand_path(path)) if File.exist?(File.expand_path(path))
  end
  true unless @notes_dir.empty?
else
  false
end
  end

  def process_arguments
if @options.notify
  begin
    require 'rubygems'
    gem 'terminal-notifier', '>=1.4'
    require 'terminal-notifier'
    @options.notify = 'terminal-notifier'
  rescue Gem::LoadError
    @options.notify = `growlnotify &>/dev/null && echo $? || echo false`.strip == '0' ? 'growlnotify' : false
    $stderr.puts 'Either terminal-notifier gem or growlnotify must be installed to use Notifications' unless @options.notify
  end
end
if (@options.notify || @options.email || @options.reminders) && !@options.verbose
  @options.stdout = false
end
  end

  def output_usage
output_version
usage = <<ENDUSAGE
nvremind.rb [options] notes_folder

For help use: nvremind.rb -h

See <http://brettterpstra.com/projects/nvremind> for more information
ENDUSAGE
puts usage
Process.exit
  end

  def output_version
puts "#{File.basename(__FILE__)} version #{NVR_VERSION}"
  end

  def process_command
@notes_dir.each do|notes_dir|
  Dir.chdir(notes_dir)
  puts
  `grep -El "[^\\s]remind\\(.*?\\)|[^\\s]event\\(.*?\\)" *.{md,txt,taskpaper,ft,doentry} 2>/dev/null`.split("\n").each do|file|
    mod_time = File.mtime(file)
    input = if RUBY_VERSION.to_f > 1.9
              IO.read(file).force_encoding('utf-8')
            else
              IO.read(file)
    end
    lines = input.split(/\n/)
    counter = 0
    lines.map! do|contents|
      counter += 1
      # don't remind if the line contains @done or @canceled
      unless contents =~ /\s@(done|cancell?ed)/
        remind_match = contents.match(/([^\s"`'\(\[])remind\((.*?)(\s"(.*?)")?\)/)
        list_match = contents.match(/([^\s"`'\(\[])list\((.*?)(\s"(.*?)")?\)/)
        unless list_match.nil?
            @list_name = list_match[2]
        end
        unless remind_match.nil?
          remind_date = Time.parse(remind_match[2])
          #Remind only if is still something that will happen.
          if remind_date >= Time.now
            # This strips the @remind tag from the reminder title or body
            remind_stripped_line = contents.gsub(/["`'\(\[]?#{Regexp.escape(remind_match[0])}["`'\)\]]?\s*/, '').gsub(/<\/?string>/, '').strip
            # If you don't want to include the reminder list in your note, uncomment the code bellow
            unless list_match.nil?
                remind_stripped_line = remind_stripped_line.gsub(/["`'\(\[]?#{Regexp.escape(list_match[0])}["`'\)\]]?\s*/, '').gsub(/<\/?string>/, '').strip
            end
            # remove leading - or * in case it's in a TaskPaper or Markdown list
            remind_stripped_line.sub!(/^[\-\*\+] /, '')
            filename = "#{notes_dir}/#{file}".gsub(/\+/, '%20')
            is_day_one = File.extname(file) =~ /doentry$/
            if is_day_one
              xml = IO.read(file)
              dayone_content = xml.match(/<key>Entry Text<\/key>\s*<string>(.*?)<\/string>/m)[1]
              if dayone_content
                note_title = dayone_content.split(/\n/)[0].gsub(/[#<>\-\*\+]/, '')[0..30].strip
              else
                note_title = 'From Day One'
              end
            else
              note_title = File.basename(file).gsub(/\.(txt|md|taskpaper|ft|doentry)$/, '')
            end
            if remind_stripped_line == ''
              @remind_title = remind_match[4] || note_title
              @remind_extension = File.extname(file)
              @remind_message = "#{@remind_title} [#{remind_date.strftime('%F')}]"
              @remind_note = is_day_one ? dayone_content : IO.read(file)
              if @extension =~ /(md|txt)$/
                #  @note += "\n\n- <nvalt://find/#{CGI.escape(note_title).gsub(/\+/,"%20")}>\n"
              end
            else
              @remind_title = remind_match[4] || remind_stripped_line
              @remind_extension = ''
              @remind_message = "#{@remind_title} [#{remind_date.strftime('%F')}]"
              @remind_taskDate = remind_match[2]
              # add :#{counter} after #{filename} to include line number below
              if is_day_one
                @remind_note = stripped_line 
              else
                # @note = "#{stripped_line}\n\n- file://#{filename}\n- nvalt://find/#{CGI.escape(note_title).gsub(/\+/,"%20")}\n"
                @remind_note = "#{remind_stripped_line}\n- file://#{filename}\n"
              end
            end
            if @options.verbose
              puts "Title: #{@remind_title}"
              puts "Extension: #{@remind_extension}"
              puts "Message: #{@remind_message}"
              puts "Note: #{@remind_note}"
            end
            notify_remind
            if @options.remove
              print 'REMOVED'
              contents.gsub!(/([^\s"`'\(\[])remind\((.*?)(\s"(.*?)")?\)/) do |match|
                if Time.parse(Regexp.last_match(2)) > Time.now
                  "#{Regexp.last_match(1)}reminded(#{Time.now.strftime('%Y-%m-%d %H:%M')}#{Regexp.last_match(3)})"
                else
                  match
                end
              end
            end
          end
        end
      end
      unless contents =~ /\s@(done|cancell?ed)/
        event_match = contents.match(/([^\s"`'\(\[])event\((.*?)(\s"(.*?)")?\)/)
			event_title_pat = contents.match(/((?<=^..).)(.+?(?=\@))/)
			# event_title_pat.sub(/((?<=^..).*$)/)
        calendar_match = contents.match(/([^\s"`'\(\[])calendar\((.*?)(\s"(.*?)")?\)/)
			duration_match = contents.match(/([^\s"`'\(\[])duration\((.*?)(\s"(.*?)")?\)/)
        unless calendar_match.nil?
            @calendar_name = calendar_match[2]
			end
				# duration start
				
        unless duration_match.nil?
            @duration = duration_match[2]
        end
        unless event_match.nil?
          event_date = Time.parse(event_match[2])
          if event_date >= Time.now
            event_stripped_line = contents.gsub(/["`'\(\[]?#{Regexp.escape(event_match[0])}["`'\)\]]?\s*/, '').gsub(/<\/?string>/, '').strip
            # If you don't want to include the calendar name in your event, uncomment the following code
            unless calendar_match.nil?
                event_stripped_line = contents.gsub(/["`'\(\[]?#{Regexp.escape(calendar_match[0])}["`'\)\]]?\s*/, '').gsub(/<\/?string>/, '').strip
            end
            # remove leading - or * in case it's in a TaskPaper or Markdown list
            event_stripped_line.sub!(/^[\-\*\+] /, '')
            filename = "#{notes_dir}/#{file}".gsub(/\+/, '%20')
            is_day_one = File.extname(file) =~ /doentry$/
            if is_day_one
            else
              event_note_title = File.basename(file).gsub(/\.(txt|md|taskpaper|ft|doentry)$/, '')
            end
            if event_stripped_line ==''
				  @event_title_pat = event_title_pat[4] || event_note_title
              @event_title = event_match[4] || event_note_title
              @event_extension = File.extname(file)
              @event_message = "#{@event_title} [#{event_date.strftime('%F')}]"
              @event_note = is_day_one ? dayone_content : IO.read(file)
              if @event_extension =~ /(md|txt)$/
                #  @note += "\n\n- <nvalt://find/#{CGI.escape(event_note_title).gsub(/\+/,"%20")}>\n"
              end
            else
					@event_title_pat = event_title_pat
              @event_title = event_match[4] || event_stripped_line
              @event_extension = ''
              @event_message = "#{@event_title} [#{event_date.strftime('%F')}]"
              @event_taskDate = event_match[2]
              # add :#{counter} after #{filename} to include line number below
              if is_day_one
                @event_note = event_stripped_line
              else
                # @note = "#{stripped_line}\n\n- file://#{filename}\n- nvalt://find/#{CGI.escape(event_note_title).gsub(/\+/,"%20")}\n"
                @event_note = "#{event_stripped_line}\n- file://#{filename}\n"
              end
            end
            if @options.verbose
              puts "Title: #{@event_title}"
              puts "Extension: #{@event_extension}"
              puts "Message: #{@event_message}"
              puts "Note: #{@event_note}"
            end
            notify_event
            if @options.remove
              contents.gsub!(/([^\s"`'\(\[])event\((.*?)(\s"(.*?)")?\)/) do |match|
                if Time.parse(Regexp.last_match(2)) > Time.now
                  "#{Regexp.last_match(1)}event_created(#{Time.now.strftime('%Y-%m-%d %H:%M')}#{Regexp.last_match(3)})"
                else
                  match
                end
              end
            end
          end
        end
      end
      contents
    end
    File.open(file, 'w+') do |f|
      f.puts lines.join("\n")
    end
    if @options.preserve_time
      `touch -m "#{file}"`
    end
  end
end
  end

  def notify_event
puts @remind_message if @options.stdout
if @options.notify == 'terminal-notifier'
  TerminalNotifier.notify(@event_title, title: 'Event')
elsif @options.notify == 'growlnotify'
  `growlnotify -m "#{@event_message}" -t "Event" -s`
end
if @options.reminders
  `osascript <<'APPLESCRIPT'
    tell application "Calendar"
		        if name of calendars does not contain "#{@calendar_name}" then
      set _calendars to item 1 of calendars
    else
      set _calendars to calendar "#{@calendar_name}"
    end if
    set textDate to "#{@event_taskDate}"
		set duration to "#{@duration}"
    set resultDate to the current date
    set the year of resultDate to (text 1 thru 4 of textDate)
    set the month of resultDate to (text 6 thru 7 of textDate)
    set the day of resultDate to (text 9 thru 10 of textDate)
    set the time of resultDate to 0

    if (length of textDate) > 10 then
      set the hours of resultDate to (text 12 thru 13 of textDate)
      set the minutes of resultDate to (text 15 thru 16 of textDate)

      if (length of textDate) > 16 then
        set the seconds of resultDate to (text 18 thru 19 of textDate)
      end if
    end if
    make new event at end of _calendars with properties {summary:"#{@event_title_pat}", start date:resultDate, end date:resultDate +duration * Minutes }
		delay 5
		quit
  end tell
APPLESCRIPT`
end
unless @options.file == false
  filename = File.join(@options.file, @title)
  File.open(filename, 'w+') do |f|
    f.puts @remind_note
  end
end
unless @options.email == false
  subject = @title
  content = @note
  if @extension == '.taskpaper'
    if File.exist?('/usr/local/bin/multimarkdown')
      md = "format: complete\n\n#{TaskPaper.new.tp2md(@note)}"
      content = `echo #{Shellwords.escape(md)}|/usr/local/bin/multimarkdown`
    end
  else
    if File.exist?('/usr/local/bin/multimarkdown')
      content = `echo #{Shellwords.escape("format: complete\n\n" + @note)}|/usr/local/bin/multimarkdown`
    end
  end

  template = <<ENDTEMPLATE
Subject: #{@title}
From: nvreminder@system.net
MIME-Version: 1.0
Content-Type: text/html;

#{content}

ENDTEMPLATE
  @options.email.each do|email|
    `echo #{Shellwords.escape(template)}|/usr/sbin/sendmail #{email}`
  end
end
  end

  def notify_remind
puts @remind_message if @options.stdout
if @options.notify == 'terminal-notifier'
  TerminalNotifier.notify(@remind_message, title: 'Remind')
elsif @options.notify == 'growlnotify'
  `growlnotify -m "#{@remind_message}" -t "Reminder" -s`
end
if @options.reminders
  `osascript <<'APPLESCRIPT'
	  tell application "Reminders"
    if name of lists does not contain "#{@list_name}" then
      set _reminders to item 1 of lists
    else
      set _reminders to list "#{@list_name}"
    end if
    set textDate to "#{@remind_taskDate}"
    set resultDate to the current date
    set the year of resultDate to (text 1 thru 4 of textDate)
    set the month of resultDate to (text 6 thru 7 of textDate)
    set the day of resultDate to (text 9 thru 10 of textDate)
    set the time of resultDate to 0

    if (length of textDate) > 10 then
      set the hours of resultDate to (text 12 thru 13 of textDate)
      set the minutes of resultDate to (text 15 thru 16 of textDate)

      if (length of textDate) > 16 then
        set the seconds of resultDate to (text 18 thru 19 of textDate)
      end if
    end if
    make new reminder at end of _reminders with properties {name:"#{@remind_title}", remind me date:resultDate}
		delay 5
		quit
  end tell
APPLESCRIPT`
end
unless @options.file == false
  filename = File.join(@options.file, @title)
  File.open(filename, 'w+') do |f|
    f.puts @remind_note
  end
end
unless @options.email == false
  subject = @title
  content = @note
  if @extension == '.taskpaper'
    if File.exist?('/usr/local/bin/multimarkdown')
      md = "format: complete\n\n#{TaskPaper.new.tp2md(@note)}"
      content = `echo #{Shellwords.escape(md)}|/usr/local/bin/multimarkdown`
    end
  else
    if File.exist?('/usr/local/bin/multimarkdown')
      content = `echo #{Shellwords.escape("format: complete\n\n" + @note)}|/usr/local/bin/multimarkdown`
    end
  end

  template = <<ENDTEMPLATE
Subject: #{@title}
From: nvreminder@system.net
MIME-Version: 1.0
Content-Type: text/html;

#{content}

ENDTEMPLATE
  @options.email.each do|email|
    `echo #{Shellwords.escape(template)}|/usr/sbin/sendmail #{email}`
  end
end
  end

end

r = Reminder.new(ARGV)
r.run
1 Like

Hi Guys, its a old post, but still like the idea of adding reminders from text notes. I have tried it get an error

`/System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/time.rb:194:in `make_time': no time information in "" (ArgumentError)`

i have used @remind (2021-08-18 18:00) but same error
any suggestions or any alternatives to achieve this

You may have seen this, but if you type the letters rem in the TaskPaper Help menu search field, you will find various elements of built-in automation for interactions with Reminders now:

image

1 Like

Thank you for your reply. I was hoping to use text files. I use windows for work, i wud have created short notes in text file, synced with One Drive for this Script to create reminders
I know its really not a taskpaper Question :slight_smile: