summaryrefslogtreecommitdiff
path: root/scripts/taskwarriorfeeder.rb
blob: 117ca690c99fb371a7080d3534187ddc8b2e9963 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#!/usr/bin/env ruby

require 'optparse'
require 'digest'
require 'json'
require 'set'

PERSONAL_TIMESPAN_D = 30
WORK_TIMESPAN_D = 14
WORKTIME_DIR = "#{ENV['HOME']}/git/worktime".freeze
GOS_DIR = "#{ENV['HOME']}/.gosdir".freeze
MAX_PENDING_RANDOM_TASKS = 42

def maybe?
  [true, false].sample
end

def run_from_personal_device?
  `uname`.chomp == 'Linux'
end

def random_count
  MAX_PENDING_RANDOM_TASKS - `task status:pending +random -work count`.to_i
end

def notes(notes_dirs, prefix, dry)
  notes_dirs.each do |notes_dir|
    Dir["#{notes_dir}/#{prefix}-*"].each do |notes_file|
      match = File.read(notes_file).strip.match(/(?<due>\d+)? *(?<tag>[A-Z]?[a-z,-:]+) *(?<body>.*)/m)
      next unless match

      tags = match[:tag].split(',') + [prefix]
      due = if match[:due].nil?
              tags.include?('track') ? 'eow' : "#{rand(0..PERSONAL_TIMESPAN_D)}d"
            else
              "#{match[:due]}d"
            end
      yield tags, match[:body], due
      File.delete(notes_file) unless dry
    end
  end
end

def random_quote(md_file)
  tag = File.basename(md_file, '.md').downcase
  lines = File.readlines(md_file)

  match = lines.first.match(/\((\d+)\)/)
  timespan = run_from_personal_device? ? PERSONAL_TIMESPAN_D : WORK_TIMESPAN_D
  timespan = match ? match[1].to_i : timespan

  quote = lines.select { |l| l.start_with? '*' }.map { |l| l.sub(/\* +/, '') }.sample
  tags = [tag, 'random']
  tags << 'work' if maybe? and maybe?
  yield tags, quote.chomp, "#{rand(0..timespan)}d"
end

def run!(cmd, dry)
  puts cmd
  return if dry

  puts `#{cmd}`
  raise "Command '#{cmd}' failed with #{$?.exitstatus}" if $?.exitstatus != 0
rescue StandardError => e
  puts "Error running command '#{cmd}': #{e.message}"
  exit 1
end

def skill_add!(skills_str, dry)
  skills_file = "#{WORKTIME_DIR}/skills.txt"
  skills_str.split(',').map(&:strip).each { skills[_1.to_s.downcase] = _1 }

  File.foreach(skills_file) do |line|
    line.chomp!
    skills[line.downcase] = line
  end
  File.open("#{skills_file}.tmp", 'w') do |file|
    skills.each_value { |skill| file.puts(skill) }
  end
  return if dry

  File.rename("#{skills_file}.tmp", skills_file)
end

def worklog_add!(tag, quote, due, dry)
  file = "#{WORKTIME_DIR}/wl-#{Time.now.to_i}n.txt"
  content = "#{due.chomp 'd'} #{tag} #{quote}"

  puts "#{file}: #{content}"
  File.write(file, content) unless dry
end

# Queue to Gos https://codeberg.org/snonux/gos
def gos_queue!(tags, message, dry)
  tags.delete('share')
  platforms = []
  %w[linkedin li mastodon ma noop no].select { tags.include?(_1) }.each do |platform|
    platforms << platform
    tags.delete(platform)
  end
  unless platforms.empty?
    platforms = %w[share] + platforms
    tags = ["#{platforms.join(':')}"] + tags
  end
  tags = %w[share] + tags if tags.size == 1 && !tags.first.start_with?('share')
  tags_str = tags.join(',')

  message = "#{tags_str.empty? ? '' : "#{tags_str} "}#{message}"
  file = "#{GOS_DIR}/#{Digest::MD5.hexdigest(message)}.txt"
  puts "Writing #{file} with #{message}"
  File.write(file, message) unless dry
end

def task_add!(tags, quote, due, dry)
  if quote.empty?
    puts 'Not adding task with empty quote'
    return
  end
  if tags.include?('tr')
    tags << 'track'
    tags.delete('tr')
  end
  tags << 'work' if tags.include?('mentoring') || tags.include?('productivity')
  tags.uniq!

  if tags.include?('task')
    run! "task #{quote}", dry
  else
    project = tags.find { |t| t =~ /^[A-Z]/ }
    project = if project.nil?
                ''
              else
                tags.delete(project)
                " project:#{project.downcase}"
              end
    priority = tags.include?('high') ? 'H' : ''
    run! "task add due:#{due} priority:#{priority}#{project} +#{tags.join(' +')} '#{quote.gsub("'", '"')}'", dry
  end
end

def task_schedule!(id, due, dry)
  run! "timeout 5s task modify #{id} due:#{due}", dry
end

def filter_tasks(filter)
  lines = `task #{filter} 2>/dev/null`.split("\n").drop(1)
  lines.pop
  lines.map { |foo| foo.split.first }.each do |id|
    yield id if id.to_i.positive?
  end
end

begin
  opts = {
    random_dir: "#{ENV['HOME']}/Notes/random",
    notes_dirs: "#{ENV['HOME']}/Notes,#{ENV['HOME']}/Notes/Quicklogger,#{ENV['HOME']}/git/worktime",
    dry_run: false,
    no_random: false
  }

  opt_parser = OptionParser.new do |o|
    o.banner = 'Usage: ruby taskwarriorfeeder.rb [options]'
    o.on('-d', '--random-dir DIR', 'The random quotes directory') { |v| opts[:random_dir] = v }
    o.on('-n', '--notes-dirs DIR1,DIR2,...', 'The notes directories') { |v| opts[:notes_dirs] = v }
    o.on('-D', '--dry-run', 'Dry run mode') { opts[:dry_run] = true }
    o.on('-R', '--no-randoms', 'No random entries') { opts[:no_random] = true }
    o.on_tail('-h', '--help', 'Show this help message and exit') { puts o and exit }
  end

  opt_parser.parse!(ARGV)

  (run_from_personal_device? ? %w[ql pl] : %w[wl]).each do |prefix|
    notes(opts[:notes_dirs].split(','), prefix, opts[:dry_run]) do |tags, note, due|
      if tags.include?('skill') || tags.include?('skills')
        skill_add!(note, opts[:dry_run])
      elsif tags.include? 'work'
        worklog_add!(:log, note, due, opts[:dry_run])
      elsif tags.any? { |tag| tag.start_with?('share') }
        gos_queue!(tags, note, opts[:dry_run])
      else
        task_add!(tags, note, due, opts[:dry_run])
      end
    end
  end

  unless opts[:no_random]
    count = random_count

    Dir["#{opts[:random_dir]}/*.md"].shuffle.each do |md_file|
      next unless maybe?
      break if count <= 0

      random_quote(md_file) do |tags, quote, due|
        task_add!(tags, quote, due, opts[:dry_run])
        count -= 1
      end
    end
  end

  if Dir.exist?(GOS_DIR) && !opts[:dry_run]
    Dir["#{WORKTIME_DIR}/tw-gos-*.json"].each do |tw_gos|
      JSON.parse(File.read(tw_gos)).each do |entry|
        gos_queue!(entry['tags'], entry['description'], opts[:dry_run])
      end
      File.delete(tw_gos)
    rescue StandardError => e
      puts e
    end
  end

  # Schedule track tasks to end of week
  filter_tasks('+track due:') do |id|
    task_schedule!(id, 'eow', opts[:dry_run])
  end

  # Randomly schedule other unscheduled tasks
  filter_tasks('-unsched -nosched -meeting -track due:') do |id|
    task_schedule!(id, "#{rand(0..PERSONAL_TIMESPAN_D)}d", opts[:dry_run])
  end
end