diff options
| -rw-r--r-- | fish/conf.d/supersync.fish | 4 | ||||
| -rw-r--r-- | fish/conf.d/taskwarrior.fish | 333 | ||||
| -rw-r--r-- | scripts/taskwarriorfeeder.rb | 218 |
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 |
