From 9c77c303b0492bc1c02e911479ecdf72a67a8447 Mon Sep 17 00:00:00 2001 From: Brett Terpstra Date: Thu, 14 Oct 2021 10:46:24 -0500 Subject: [PATCH] Pretty print JSON output - IMPROVED: Better formatting for JSON output - NEW: --no-menu option for select command to use --query as a filter and act on matching entries without displaying menu --- CHANGELOG.md | 5 +++ Gemfile.lock | 2 +- bin/doing | 9 +++- lib/doing/version.rb | 2 +- lib/doing/wwid.rb | 13 ++++-- test.txt | 2 + test/doing_day_test.rb | 64 +++++++++++++++++++++++++++ test/doing_last_test.rb | 57 ++++++++++++++++++++++++ test/doing_output_test.rb | 91 ++++++-------------------------------- test/doing_select_test.bak | 56 +++++++++++++++++++++++ test/doing_show_test.rb | 37 +++++++++++++--- test/doing_view_test.rb | 4 +- 12 files changed, 250 insertions(+), 92 deletions(-) create mode 100644 test.txt create mode 100644 test/doing_day_test.rb create mode 100644 test/doing_last_test.rb create mode 100644 test/doing_select_test.bak diff --git a/CHANGELOG.md b/CHANGELOG.md index dfe14827..e331ee8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### 1.0.89 + +- Pretty print JSON output +- --no-menu option for select command to use --query as a filter and act on matching entries without displaying menu + ### 1.0.88 - Add --before and --after time search to yesterday command diff --git a/Gemfile.lock b/Gemfile.lock index 4aa38d9b..cf477930 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - doing (1.0.87) + doing (1.0.88) chronic (~> 0.10, >= 0.10.2) deep_merge (~> 1.2, >= 1.2.1) gli (~> 2.19, >= 2.19.2) diff --git a/bin/doing b/bin/doing index a69edc57..b13d0124 100755 --- a/bin/doing +++ b/bin/doing @@ -292,10 +292,13 @@ command :select do |c| c.arg_name 'SECTION' c.flag %i[m move] - c.desc 'Initial search query for filtering' + c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"' c.arg_name 'QUERY' c.flag %i[q query] + c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.' + c.switch %i[menu], negatable: true, default_value: true + c.desc 'Cancel selected items (add @done without timestamp)' c.switch %i[c cancel], negatable: false, default_value: false @@ -311,7 +314,7 @@ command :select do |c| c.desc 'Add flag to selected item(s)' c.switch %i[flag], negatable: false, default_value: false - c.desc 'Perform action without confirmation' + c.desc 'Perform action without confirmation.' c.switch %i[force], negatable: false, default_value: false c.desc 'Save selected entries to file using --output format' @@ -323,6 +326,8 @@ command :select do |c| c.flag %i[o output], must_match: /^(?:doing|taskpaper|html|csv|json|template|timeline)$/i c.action do |_global_options, options, args| + exit_now! "--no-menu requires --query" if !options[:menu] && !options[:query] + wwid.interactive(options) end end diff --git a/lib/doing/version.rb b/lib/doing/version.rb index 7ba47e04..d2304850 100644 --- a/lib/doing/version.rb +++ b/lib/doing/version.rb @@ -1,3 +1,3 @@ module Doing - VERSION = '1.0.88' + VERSION = '1.0.89' end diff --git a/lib/doing/wwid.rb b/lib/doing/wwid.rb index f689b49a..916bf6e9 100755 --- a/lib/doing/wwid.rb +++ b/lib/doing/wwid.rb @@ -817,7 +817,7 @@ def interactive(opt = {}) ' | ', item['title'] ] - if opt[:section] =~ /^all/i + if section =~ /^all/i out.concat([ ' (', item['section'], @@ -834,7 +834,14 @@ def interactive(opt = {}) '--bind ctrl-a:select-all', %(-q "#{opt[:query]}") ] + if !opt[:menu] + exit_now! "Can't skip menu when no query is provided" unless opt[:query] + + fzf_args.concat([%(--filter="#{opt[:query]}"), '--no-sort']) + end + res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}` + `echo '#{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}' >> test.txt` selected = [] res.split(/\n/).each do |item| idx = item.match(/^(\d+)\)/)[1].to_i @@ -1856,11 +1863,11 @@ def list_section(opt = {}) end end if opt[:output] == 'json' - out = { + puts JSON.pretty_generate({ 'section' => section, 'items' => items_out, 'timers' => tag_times(format: 'json', sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) - }.to_json + }) elsif opt[:output] == 'timeline' template = <<~EOTEMPLATE diff --git a/test.txt b/test.txt new file mode 100644 index 00000000..087ab326 --- /dev/null +++ b/test.txt @@ -0,0 +1,2 @@ +/Users/ttscoff/Desktop/Code/doing/lib/doing/../helpers/fuzzyfilefinder +/Users/ttscoff/Desktop/Code/doing/lib/doing/../helpers/fuzzyfilefinder diff --git a/test/doing_day_test.rb b/test/doing_day_test.rb new file mode 100644 index 00000000..7fe5d619 --- /dev/null +++ b/test/doing_day_test.rb @@ -0,0 +1,64 @@ +require 'fileutils' +require 'tempfile' +require 'time' +require 'yaml' + +require 'doing-helpers' +require 'test_helper' + +# Tests for entry modifying commands +class DoingDayTest < Test::Unit::TestCase + include DoingHelpers + ENTRY_REGEX = /^\d{4}-\d\d-\d\d \d\d:\d\d \|/.freeze + + def setup + @tmpdirs = [] + @result = '' + @basedir = mktmpdir + @wwid_file = File.join(@basedir, 'wwid.md') + @config_file = File.join(File.dirname(__FILE__), 'test.doingrc') + end + + def teardown + FileUtils.rm_rf(@tmpdirs) + end + + def test_today_command + subject = 'Test new entry @tag1' + doing('done', subject) + subject2 = 'Test new entry 2 @tag2' + doing('now', subject2) + assert_count_entries(2, doing('today'), 'There should be 2 entries shown by `doing today`') + end + + def test_yesterday_command + doing('done', 'Adding an entry finished yesterday', '--took', '30m', '--back', 'yesterday 3pm') + assert_count_entries(1, doing('yesterday'), 'There should be 1 entry shown by `doing yesterday`') + end + + def test_on_command + # 1:42pm: Did a thing @done(2021-07-05 13:42) + doing('now', 'Test new entry @tag1') + doing('now', 'Test new entry 2 @tag2') + result = doing('--stdout', 'on', 'today') + assert_count_entries(2, result, 'There should be 2 entries') + end + + private + + def assert_count_entries(count, shown, message = 'Should be X entries shown') + assert_equal(count, shown.uncolor.strip.scan(ENTRY_REGEX).count, message) + end + + def mktmpdir + tmpdir = Dir.mktmpdir + @tmpdirs.push(tmpdir) + + tmpdir + end + + def doing(*args) + doing_with_env({}, '--config_file', @config_file, '--doing_file', @wwid_file, *args) + end +end + diff --git a/test/doing_last_test.rb b/test/doing_last_test.rb new file mode 100644 index 00000000..b51b1809 --- /dev/null +++ b/test/doing_last_test.rb @@ -0,0 +1,57 @@ +require 'fileutils' +require 'tempfile' +require 'time' +require 'yaml' + +require 'doing-helpers' +require 'test_helper' + +# Tests for entry modifying commands +class DoingLastTest < Test::Unit::TestCase + include DoingHelpers + + def setup + @tmpdirs = [] + @result = '' + @basedir = mktmpdir + @wwid_file = File.join(@basedir, 'wwid.md') + @config_file = File.join(File.dirname(__FILE__), 'test.doingrc') + @import_file = File.join(File.dirname(__FILE__), 'All Activities 2.json') + end + + def teardown + FileUtils.rm_rf(@tmpdirs) + end + + def test_last_command + subject = 'Test new entry @tag1' + doing('import', @import_file) + doing('now', subject) + assert_match(/#{subject}\s*$/, doing('last'), 'last entry should be entry just added') + end + + def test_last_search_and_tag + unique_keyword = 'jumping jesus' + unique_tag = 'balloonpants' + doing('now', "Test new entry @#{unique_tag} sad monkey") + doing('now', "Test new entry @tag2 #{unique_keyword}") + doing('now', 'Test new entry @tag3 burly man') + + assert_match(/#{unique_keyword}/, doing('last', '--search', unique_keyword), 'returned entry should contain unique keyword') + assert_match(/@#{unique_tag}/, doing('last', '--tag', unique_tag), 'returned entry should contain unique tag') + end + + private + + def mktmpdir + tmpdir = Dir.mktmpdir + @tmpdirs.push(tmpdir) + + tmpdir + end + + def doing(*args) + doing_with_env({}, '--config_file', @config_file, '--doing_file', @wwid_file, *args) + end +end + diff --git a/test/doing_output_test.rb b/test/doing_output_test.rb index 503d3892..bdc63722 100644 --- a/test/doing_output_test.rb +++ b/test/doing_output_test.rb @@ -10,6 +10,8 @@ class DoingOutputTest < Test::Unit::TestCase include DoingHelpers ENTRY_REGEX = /^\d{4}-\d\d-\d\d \d\d:\d\d \|/.freeze + ENTRY_TS_REGEX = /\s*(?[^|]+) \s*\|/.freeze + ENTRY_DONE_REGEX = /@done\((?.*?)\)/.freeze def setup @tmpdirs = [] @@ -17,6 +19,7 @@ def setup @basedir = mktmpdir @wwid_file = File.join(@basedir, 'wwid.md') @config_file = File.join(File.dirname(__FILE__), 'test.doingrc') + @import_file = File.join(File.dirname(__FILE__), 'All Activities 2.json') @config = YAML.load(IO.read(@config_file)) end @@ -24,76 +27,6 @@ def teardown FileUtils.rm_rf(@tmpdirs) end - def test_last_command - subject = 'Test new entry @tag1' - doing('now', subject) - assert_match(/#{subject}\s*$/, doing('last'), 'last entry should be entry just added') - end - - def test_last_search_and_tag - unique_keyword = 'jumping jesus' - unique_tag = 'balloonpants' - doing('now', "Test new entry @#{unique_tag} sad monkey") - doing('now', "Test new entry @tag2 #{unique_keyword}") - doing('now', 'Test new entry @tag3 burly man') - - assert_match(/#{unique_keyword}/, doing('last', '--search', unique_keyword), 'returned entry should contain unique keyword') - assert_match(/@#{unique_tag}/, doing('last', '--tag', unique_tag), 'returned entry should contain unique tag') - end - - def test_view_command - subject = 'Test new entry @tag1' - doing('done', subject) - subject2 = 'Test new entry 2 @tag2' - doing('done', subject2) - assert_count_entries(2, doing('view', 'done'), 'There should be 2 entries shown by `view done`') - end - - def test_show_command - subject = 'Test new entry @tag1' - doing('now', subject) - subject2 = 'Test new entry 2 @tag2' - doing('now', subject2) - result = doing('show').uncolor.strip - assert_count_entries(2, result, 'There should be 2 entries shown by `doing show`') - assert_match(/#{subject}\s*$/, result, 'doing show results should include test entry') - result = doing('show', '@tag1').uncolor.strip - assert_count_entries(1, result, 'There should be 1 entries shown by `doing show @tag1`') - assert_match(/#{subject}\s*$/, result, 'doing show @tag1 results should include test entry') - end - - def test_show_command_tag_boolean - subject = 'Test new entry @tag1' - doing('now', subject) - subject2 = 'Test new entry 2 @tag2' - doing('now', subject2) - subject3 = 'Test new entry 3 @tag1 @tag2 @tag3' - doing('now', subject3) - - result = doing('show', '--tag', 'tag1,tag2', '--bool', 'and').uncolor.strip - assert_count_entries(1, result, 'There should be 1 entry shown with both @tag1 and @tag2') - assert_match(/#{subject3}\s*$/, result, 'doing show results should include entry with both @tag1 and @tag2') - - result = doing('show', '--tag', 'tag1,tag2', '--bool', 'or').uncolor.strip - assert_count_entries(3, result, 'There should be 3 entries shown with either @tag1 or @tag2') - result = doing('show', '--tag', 'tag2', '--bool', 'not').uncolor.strip - assert_count_entries(1, result, 'There should be 1 entry shown without @tag2') - assert_match(/#{subject}\s*$/, result, 'doing show results should include entry without @tag2') - end - - def test_today_command - subject = 'Test new entry @tag1' - doing('done', subject) - subject2 = 'Test new entry 2 @tag2' - doing('now', subject2) - assert_count_entries(2, doing('today'), 'There should be 2 entries shown by `doing today`') - end - - def test_yesterday_command - doing('done', 'Adding an entry finished yesterday', '--took', '30m', '--back', 'yesterday 3pm') - assert_count_entries(1, doing('yesterday'), 'There should be 1 entry shown by `doing yesterday`') - end - def test_sections_command result = doing('sections').uncolor.strip assert_match(/^#{@config['current_section']}$/, result, "#{@config['current_section']} should be the only section shown") @@ -109,14 +42,6 @@ def test_recent_command assert_equal(matches.count, 2, 'There should be 2 entries shown by `doing recent`') end - def test_on_command - # 1:42pm: Did a thing @done(2021-07-05 13:42) - doing('now', 'Test new entry @tag1') - doing('now', 'Test new entry 2 @tag2') - result = doing('--stdout', 'on', 'today') - assert_count_entries(2, result, 'There should be 2 entries') - end - def test_template_command result = doing('template', 'haml') assert_match(/^!!!\s*\n%html/, result, 'Output should be a HAML template') @@ -126,6 +51,16 @@ def test_template_command private + def assert_matches(matches, shown) + matches.each do |regexp, msg, opt_refute| + if opt_refute + assert_no_match(regexp, shown, msg) + else + assert_match(regexp, shown, msg) + end + end + end + def assert_count_entries(count, shown, message = 'Should be X entries shown') assert_equal(count, shown.uncolor.strip.scan(ENTRY_REGEX).count, message) end diff --git a/test/doing_select_test.bak b/test/doing_select_test.bak new file mode 100644 index 00000000..9ce5c642 --- /dev/null +++ b/test/doing_select_test.bak @@ -0,0 +1,56 @@ +# This test is failing because it doesn't seem to be able to +# execute the subshell command required by the select +# command. +require 'fileutils' +require 'tempfile' +require 'time' +require 'json' + +require 'doing-helpers' +require 'test_helper' + +# Tests for entry modifying commands +class DoingSelectTest < Test::Unit::TestCase + include DoingHelpers + ENTRY_REGEX = /^\d{4}-\d\d-\d\d \d\d:\d\d \|/.freeze + + def setup + @tmpdirs = [] + @result = '' + @basedir = mktmpdir + @wwid_file = File.join(@basedir, 'wwid.md') + @config_file = File.join(File.dirname(__FILE__), 'test.doingrc') + @import_file = File.join(File.dirname(__FILE__), 'All Activities 2.json') + end + + def teardown + FileUtils.rm_rf(@tmpdirs) + end + + def test_select_filter + keyword = 'zoom call' + doing('import', @import_file) + json = JSON.parse(IO.read(@import_file)) + matching_entries = json.select { |entry| entry['activityTitle'] =~ /#{keyword}/i }.length + result = doing('select', '--no-menu', '--query', keyword, '--output', 'doing') + assert_count_entries(matching_entries, result, "There should be #{matching_entries} entries") + end + + private + + def assert_count_entries(count, shown, message = 'Should be X entries shown') + assert_equal(count, shown.uncolor.strip.scan(ENTRY_REGEX).count, message) + end + + def mktmpdir + tmpdir = Dir.mktmpdir + @tmpdirs.push(tmpdir) + + tmpdir + end + + def doing(*args) + doing_with_env({}, '--config_file', @config_file, '--doing_file', @wwid_file, *args) + end +end + diff --git a/test/doing_show_test.rb b/test/doing_show_test.rb index feead636..98c2e418 100644 --- a/test/doing_show_test.rb +++ b/test/doing_show_test.rb @@ -1,7 +1,7 @@ require 'fileutils' require 'tempfile' require 'time' -require 'json' +require 'yaml' require 'doing-helpers' require 'test_helper' @@ -20,16 +20,24 @@ def setup @wwid_file = File.join(@basedir, 'wwid.md') @config_file = File.join(File.dirname(__FILE__), 'test.doingrc') @import_file = File.join(File.dirname(__FILE__), 'All Activities 2.json') + @config = YAML.load(IO.read(@config_file)) end def teardown FileUtils.rm_rf(@tmpdirs) end - def test_show - doing('now', 'Adding a test entry') - entries = doing('show') - assert_count_entries(1, entries, '1 entry should be listed') + def test_show_command + subject = 'Test new entry @tag1' + doing('now', subject) + subject2 = 'Test new entry 2 @tag2' + doing('now', subject2) + result = doing('show').uncolor.strip + assert_count_entries(2, result, 'There should be 2 entries shown by `doing show`') + assert_match(/#{subject}\s*$/, result, 'doing show results should include test entry') + result = doing('show', '@tag1').uncolor.strip + assert_count_entries(1, result, 'There should be 1 entries shown by `doing show @tag1`') + assert_match(/#{subject}\s*$/, result, 'doing show @tag1 results should include test entry') end def test_show_tag_sort @@ -49,6 +57,25 @@ def test_show_date_limit assert_count_entries(5, result, 'There should be 5 entries between specified dates') end + def test_show_command_tag_boolean + subject = 'Test new entry @tag1' + doing('now', subject) + subject2 = 'Test new entry 2 @tag2' + doing('now', subject2) + subject3 = 'Test new entry 3 @tag1 @tag2 @tag3' + doing('now', subject3) + + result = doing('show', '--tag', 'tag1,tag2', '--bool', 'and').uncolor.strip + assert_count_entries(1, result, 'There should be 1 entry shown with both @tag1 and @tag2') + assert_match(/#{subject3}\s*$/, result, 'doing show results should include entry with both @tag1 and @tag2') + + result = doing('show', '--tag', 'tag1,tag2', '--bool', 'or').uncolor.strip + assert_count_entries(3, result, 'There should be 3 entries shown with either @tag1 or @tag2') + result = doing('show', '--tag', 'tag2', '--bool', 'not').uncolor.strip + assert_count_entries(1, result, 'There should be 1 entry shown without @tag2') + assert_match(/#{subject}\s*$/, result, 'doing show results should include entry without @tag2') + end + private def assert_matches(matches, shown) diff --git a/test/doing_view_test.rb b/test/doing_view_test.rb index 3f4da56f..b47993cb 100644 --- a/test/doing_view_test.rb +++ b/test/doing_view_test.rb @@ -26,12 +26,12 @@ def teardown FileUtils.rm_rf(@tmpdirs) end - def test_views + def test_views_command views = doing('views').strip.split(/\s+/).delete_if {|v| v.strip == ''} assert_equal(5, views.length, 'Should have 3 views as defined in test configuration') end - def test_view + def test_view_command doing('now', 'Adding a test entry') entries = doing('view', 'test') assert_count_entries(1, entries, '1 entry should be listed containing DOING TEST (added by the view)')