summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fish/conf.d/supersync.fish4
-rw-r--r--fish/conf.d/taskwarrior.fish333
-rw-r--r--scripts/taskwarriorfeeder.rb218
3 files changed, 331 insertions, 224 deletions
diff --git a/fish/conf.d/supersync.fish b/fish/conf.d/supersync.fish
index 8f00e75..78c0bb0 100644
--- a/fish/conf.d/supersync.fish
+++ b/fish/conf.d/supersync.fish
@@ -54,9 +54,7 @@ function supersync::uprecords
end
function supersync::taskwarrior
- if test -f ~/scripts/taskwarriorfeeder.rb
- ruby ~/scripts/taskwarriorfeeder.rb
- end
+ taskwarrior::feeder
taskwarrior::export
taskwarrior::export::gos
diff --git a/fish/conf.d/taskwarrior.fish b/fish/conf.d/taskwarrior.fish
index e822d60..1a25fcc 100644
--- a/fish/conf.d/taskwarrior.fish
+++ b/fish/conf.d/taskwarrior.fish
@@ -66,6 +66,335 @@ function _taskwarrior::set_import_export_tags
end
end
+set -g TASKWARRIOR_FEEDER_PERSONAL_TIMESPAN_D 30
+set -g TASKWARRIOR_FEEDER_WORK_TIMESPAN_D 14
+set -g TASKWARRIOR_FEEDER_MAX_PENDING_RANDOM_TASKS 42
+set -g TASKWARRIOR_FEEDER_GOS_DIR ~/.gosdir
+
+function taskwarrior::feeder::is_personal_device
+ test (uname) = Linux
+end
+
+function taskwarrior::feeder::random_count
+ set -l pending (task status:pending +random -work count)
+ math $TASKWARRIOR_FEEDER_MAX_PENDING_RANDOM_TASKS - $pending
+end
+
+function taskwarrior::feeder::normalize_tags
+ set -l tags_csv "$argv[1]"
+ set -l normalized
+ for tag in (string split ',' -- "$tags_csv")
+ set -l trimmed (string trim -- "$tag")
+ if test -n "$trimmed"
+ set -a normalized "$trimmed"
+ end
+ end
+ printf '%s\n' $normalized
+end
+
+function taskwarrior::feeder::task_add
+ set -l tags_csv "$argv[1]"
+ set -l quote "$argv[2]"
+ set -l due "$argv[3]"
+ set -l tags (taskwarrior::feeder::normalize_tags "$tags_csv")
+ set -l normalized_tags
+
+ if test -z "$quote"
+ echo "Not adding task with empty quote"
+ return
+ end
+
+ if contains -- tr $tags
+ set tags (string match -v -- tr $tags)
+ set -a tags track
+ end
+ if contains -- mentoring $tags; or contains -- productivity $tags
+ set -a tags work
+ end
+ for tag in $tags
+ if not contains -- "$tag" $normalized_tags
+ set -a normalized_tags "$tag"
+ end
+ end
+ set tags $normalized_tags
+
+ if contains -- task $tags
+ eval "task $quote"
+ return
+ end
+
+ set -l project
+ for tag in $tags
+ if string match -rq '^[A-Z]' -- "$tag"
+ set project (string lower -- "$tag")
+ set -l remaining_tags
+ for entry in $tags
+ if test "$entry" != "$tag"
+ set -a remaining_tags "$entry"
+ end
+ end
+ set tags $remaining_tags
+ break
+ end
+ end
+
+ set -l priority
+ if contains -- high $tags
+ set priority H
+ end
+
+ set -l task_args add due:$due
+ if test -n "$priority"
+ set -a task_args priority:$priority
+ end
+ if test -n "$project"
+ set -a task_args project:$project
+ end
+ for tag in $tags
+ set -a task_args +$tag
+ end
+ set -a task_args "$quote"
+ task $task_args
+end
+
+function taskwarrior::feeder::skill_add
+ set -l skills_str "$argv[1]"
+ set -l skills_file "$WORKTIME_DIR/skills.txt"
+ set -l tmp_file "$skills_file.tmp"
+ set -l incoming_file "$tmp_file.incoming"
+ set -l existing_file "$tmp_file.existing"
+
+ touch "$skills_file"
+ printf '%s\n' (string split ',' -- "$skills_str" | string trim) | sed '/^$/d' >"$incoming_file"
+ cp "$skills_file" "$existing_file"
+
+ cat "$incoming_file" "$existing_file" | awk '{ k=tolower($0); if (!seen[k]++) print $0 }' >"$tmp_file"
+ mv "$tmp_file" "$skills_file"
+ rm -f "$incoming_file" "$existing_file"
+end
+
+function taskwarrior::feeder::worklog_add
+ set -l tag "$argv[1]"
+ set -l quote "$argv[2]"
+ set -l due "$argv[3]"
+ set -l file "$WORKTIME_DIR/wl-"(date +%s)"n.txt"
+ set -l due_days (string trim -r -c d -- "$due")
+ set -l content "$due_days $tag $quote"
+
+ echo "$file: $content"
+ printf '%s\n' "$content" >"$file"
+end
+
+function taskwarrior::feeder::gos_queue
+ set -l tags_csv "$argv[1]"
+ set -l message "$argv[2]"
+ set -l tags (taskwarrior::feeder::normalize_tags "$tags_csv")
+ set -l platforms
+ set -l normalized_tags
+
+ for tag in $tags
+ switch "$tag"
+ case share
+ continue
+ case linkedin li mastodon ma noop no
+ set -a platforms "$tag"
+ case '*'
+ set -a normalized_tags "$tag"
+ end
+ end
+
+ if test (count $platforms) -gt 0
+ set -l share_tags share
+ set -a share_tags $platforms
+ set normalized_tags (string join : -- $share_tags) $normalized_tags
+ else if test (count $normalized_tags) -eq 1
+ if not string match -rq '^share' -- "$normalized_tags[1]"
+ set normalized_tags share $normalized_tags
+ end
+ end
+
+ set -l tags_str (string join , -- $normalized_tags)
+ if test -n "$tags_str"
+ set message "$tags_str $message"
+ end
+
+ mkdir -p "$TASKWARRIOR_FEEDER_GOS_DIR"
+ set -l hash (printf '%s' "$message" | md5sum | awk '{print $1}')
+ set -l file "$TASKWARRIOR_FEEDER_GOS_DIR/$hash.txt"
+ echo "Writing $file with $message"
+ printf '%s\n' "$message" >"$file"
+end
+
+function taskwarrior::feeder::notes
+ set -l notes_dirs_csv "$argv[1]"
+ set -l prefix "$argv[2]"
+ set -l router_fn "$argv[3]"
+ set -l notes_dirs (string split ',' -- "$notes_dirs_csv")
+
+ for notes_dir in $notes_dirs
+ for notes_file in "$notes_dir"/"$prefix"-*
+ if not test -f "$notes_file"
+ continue
+ end
+
+ set -l content (string trim -- (string join \n -- (cat "$notes_file")))
+ set -l matches (string match -r '^(?:([0-9]+)[[:space:]]*)?([A-Z]?[a-z,-:]+)[[:space:]]*(.*)' -- "$content")
+ if test (count $matches) -lt 3
+ continue
+ end
+
+ set -l due_n
+ set -l tags_csv
+ set -l body
+ if test (count $matches) -eq 4
+ set due_n "$matches[2]"
+ set tags_csv "$matches[3]"
+ set body "$matches[4]"
+ else
+ set tags_csv "$matches[2]"
+ set body "$matches[3]"
+ end
+ set -l tags (taskwarrior::feeder::normalize_tags "$tags_csv")
+ set -a tags "$prefix"
+ set tags_csv (string join , -- $tags)
+
+ set -l due
+ if test -n "$due_n"
+ set due "$due_n"d
+ else if contains -- track $tags
+ set due eow
+ else
+ set due (random 0 $TASKWARRIOR_FEEDER_PERSONAL_TIMESPAN_D)d
+ end
+
+ $router_fn "$tags_csv" "$body" "$due"
+ rm -f "$notes_file"
+ end
+ end
+end
+
+function taskwarrior::feeder::random_quote
+ set -l md_file "$argv[1]"
+ set -l router_fn "$argv[2]"
+ set -l tag (string lower -- (path change-extension '' (path basename "$md_file")))
+ set -l timespan
+
+ if taskwarrior::feeder::is_personal_device
+ set timespan $TASKWARRIOR_FEEDER_PERSONAL_TIMESPAN_D
+ else
+ set timespan $TASKWARRIOR_FEEDER_WORK_TIMESPAN_D
+ end
+
+ set -l first_line (head -n 1 "$md_file")
+ set -l override (string match -r --groups-only '.*\(([0-9]+)\).*' -- "$first_line")
+ if test (count $override) -gt 0
+ set timespan $override[1]
+ end
+
+ set -l quote (string sub -s 3 -- (grep '^\* ' "$md_file" | shuf -n1))
+ if test -z "$quote"
+ return
+ end
+
+ set -l tags "$tag,random"
+ if test (random 1 4) -eq 1
+ set tags "$tags,work"
+ end
+ set -l due (random 0 $timespan)d
+
+ $router_fn "$tags" "$quote" "$due"
+end
+
+function taskwarrior::feeder::schedule_ids
+ set -l filter "$argv[1]"
+ set -l due "$argv[2]"
+ set -l filter_args (string split ' ' -- "$filter")
+ set -l ids (task $filter_args rc.verbose:nothing export | jq -r '.[]?.id')
+
+ for id in $ids
+ timeout 5s task modify "$id" due:$due
+ end
+end
+
+function taskwarrior::feeder::schedule
+ taskwarrior::feeder::schedule_ids "+track due:" eow
+ for id in (task -unsched -nosched -meeting -track due: rc.verbose:nothing export | jq -r '.[]?.id')
+ timeout 5s task modify "$id" due:(random 0 $TASKWARRIOR_FEEDER_PERSONAL_TIMESPAN_D)d
+ end
+end
+
+function taskwarrior::feeder::import_gos_json
+ if not test -d "$TASKWARRIOR_FEEDER_GOS_DIR"
+ return
+ end
+
+ for tw_gos in "$WORKTIME_DIR"/tw-gos-*.json
+ if not test -f "$tw_gos"
+ continue
+ end
+
+ jq -c '.[]' "$tw_gos" | while read -l entry
+ set -l tags_csv (echo "$entry" | jq -r '.tags | join(",")')
+ set -l description (echo "$entry" | jq -r '.description')
+ taskwarrior::feeder::gos_queue "$tags_csv" "$description"
+ end
+ rm -f "$tw_gos"
+ end
+end
+
+function taskwarrior::feeder::router
+ set -l tags_csv "$argv[1]"
+ set -l note "$argv[2]"
+ set -l due "$argv[3]"
+ set -l tags (taskwarrior::feeder::normalize_tags "$tags_csv")
+
+ if contains -- skill $tags; or contains -- skills $tags
+ taskwarrior::feeder::skill_add "$note"
+ else
+ for tag in $tags
+ if string match -rq '^share' -- "$tag"
+ taskwarrior::feeder::gos_queue "$tags_csv" "$note"
+ return
+ end
+ end
+ taskwarrior::feeder::task_add "$tags_csv" "$note" "$due"
+ end
+end
+
+function taskwarrior::feeder
+ set -l notes_dirs "$HOME/Notes,$HOME/Notes/Quicklogger,$WORKTIME_DIR"
+ set -l random_dir "$HOME/Notes/random"
+ set -l prefixes
+
+ if taskwarrior::feeder::is_personal_device
+ set prefixes ql pl
+ else
+ set prefixes wl
+ end
+
+ for prefix in $prefixes
+ taskwarrior::feeder::notes "$notes_dirs" "$prefix" taskwarrior::feeder::router
+ end
+
+ set -l count (taskwarrior::feeder::random_count)
+ if test -d "$random_dir"
+ for md_file in (find "$random_dir" -name '*.md' | sort -R)
+ if test $count -le 0
+ break
+ end
+ if test (random 0 1) -eq 0
+ continue
+ end
+
+ taskwarrior::feeder::random_quote "$md_file" taskwarrior::feeder::task_add
+ set count (math "$count - 1")
+ end
+ end
+
+ taskwarrior::feeder::import_gos_json
+ taskwarrior::feeder::schedule
+end
+
function taskwarrior::export::bd
if test -d ~/Notes/Bulgarian
# Export bulgarian dumi
@@ -134,9 +463,7 @@ function taskwarrior::db::prune
end
function taskwarrior::invoke
- if test -f ~/scripts/taskwarriorfeeder.rb
- ruby ~/scripts/taskwarriorfeeder.rb
- end
+ taskwarrior::feeder
tasksamurai
end
diff --git a/scripts/taskwarriorfeeder.rb b/scripts/taskwarriorfeeder.rb
deleted file mode 100644
index 4fa4fde..0000000
--- a/scripts/taskwarriorfeeder.rb
+++ /dev/null
@@ -1,218 +0,0 @@
-#!/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.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