├── export.doing ├── _config.yml ├── test ├── bad.doingrc ├── test_helper.rb ├── plugins │ ├── hooks.rb │ ├── test_import.rb │ └── test_export.rb ├── helpers │ ├── fake_std_out.rb │ └── doing-helpers.rb ├── doing_unit-normalize_test.rb ├── doing_unit-time_test.rb ├── doing_fzf_test.rb ├── doing_sections_test.rb ├── doing_unit-good_test.rb ├── All Activities.json ├── doing_last_test.rb ├── doing_unit-note_test.rb ├── doing_hook_test.rb ├── doing_config_test.rb ├── doing_reset_test.rb ├── doing_file_test.rb ├── doing_resume_test.rb ├── doing_undo_test.rb ├── doing_cancel_test.rb ├── doing_chronify_test.rb └── doing_unit-item_test.rb ├── docs ├── _config.yml └── doc │ ├── css │ └── common.css │ ├── frames.html │ ├── file_list.html │ └── GLI.html ├── .gitsecret ├── paths │ └── mapping.cfg └── keys │ ├── pubring.kbx │ └── trustdb.gpg ├── .rubocop.yml ├── scripts ├── runtests.sh ├── setting_replace.rb └── sort_commands.rb ├── generate_completions.sh ├── img ├── doing-colors.jpg ├── doing-printf-wrap-800.jpg └── doing-show-note-formatting-800.jpg ├── lib ├── doing │ ├── version.rb │ ├── plugins │ │ ├── import │ │ │ └── cal_to_json.scpt │ │ └── export │ │ │ ├── taskpaper_export.rb │ │ │ ├── doing_export.rb │ │ │ ├── csv_export.rb │ │ │ └── byday.rb │ ├── changelog │ │ ├── changelog.rb │ │ └── entry.rb │ ├── chronify │ │ ├── chronify.rb │ │ └── numeric.rb │ ├── array │ │ ├── nested_hash.rb │ │ ├── array.rb │ │ ├── cleanup.rb │ │ └── tags.rb │ ├── prompt │ │ ├── std.rb │ │ ├── prompt.rb │ │ └── yn.rb │ ├── section.rb │ ├── completion │ │ └── completion_string.rb │ ├── string │ │ ├── string.rb │ │ ├── truncate.rb │ │ └── url.rb │ ├── items │ │ ├── modify.rb │ │ ├── util.rb │ │ ├── items.rb │ │ ├── filter.rb │ │ └── sections.rb │ ├── help_monkey_patch.rb │ ├── types.rb │ ├── wwid │ │ └── tags.rb │ ├── item │ │ ├── state.rb │ │ └── tags.rb │ ├── good.rb │ ├── boolean_term_parser.rb │ ├── time.rb │ └── hooks.rb ├── helpers │ ├── fzf │ │ ├── src │ │ │ ├── protector │ │ │ │ ├── protector.go │ │ │ │ └── protector_openbsd.go │ │ │ ├── tui │ │ │ │ ├── ttyname_windows.go │ │ │ │ ├── tui_test.go │ │ │ │ ├── ttyname_unix.go │ │ │ │ └── dummy.go │ │ │ ├── util │ │ │ │ ├── slab.go │ │ │ │ ├── atomicbool_test.go │ │ │ │ ├── util_test.go │ │ │ │ ├── atomicbool.go │ │ │ │ ├── chars_test.go │ │ │ │ ├── eventbox_test.go │ │ │ │ ├── util_unix.go │ │ │ │ ├── eventbox.go │ │ │ │ └── util_windows.go │ │ │ ├── result_others.go │ │ │ ├── result_x86.go │ │ │ ├── terminal_unix.go │ │ │ ├── item_test.go │ │ │ ├── cache_test.go │ │ │ ├── item.go │ │ │ ├── LICENSE │ │ │ ├── terminal_windows.go │ │ │ ├── reader_test.go │ │ │ ├── history_test.go │ │ │ ├── cache.go │ │ │ ├── chunklist.go │ │ │ ├── chunklist_test.go │ │ │ ├── merger_test.go │ │ │ ├── history.go │ │ │ └── constants.go │ │ ├── main.go │ │ ├── go.mod │ │ ├── Dockerfile │ │ ├── .rubocop.yml │ │ ├── LICENSE │ │ ├── BUILD.md │ │ ├── install.ps1 │ │ └── man │ │ │ └── man1 │ │ │ └── fzf-tmux.1 │ └── threaded_tests_string.rb ├── templates │ ├── doing-dayone.erb │ ├── doing-dayone-entry.erb │ ├── doing-markdown.erb │ └── doing.haml └── examples │ ├── plugins │ ├── wiki_export │ │ └── templates │ │ │ ├── wiki_index.haml │ │ │ └── wiki.haml │ └── hooks.rb │ └── commands │ ├── todo.rb │ ├── later.rb │ └── autotag.rb ├── yard_templates └── default │ └── method_details │ └── setup.rb ├── Gemfile ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── codeql-analysis.yml ├── .yardopts ├── .irbrc ├── features ├── step_definitions │ └── doing_steps.rb ├── doing.feature └── support │ └── env.rb ├── .travis.yml ├── docker ├── bash_profile ├── Dockerfile ├── Dockerfile-2.6 ├── Dockerfile-2.7 ├── Dockerfile-3.0 └── inputrc ├── AUTHORS ├── rdocfixer.rb ├── bin └── commands │ ├── choose.rb │ ├── install_fzf.rb │ ├── colors.rb │ ├── redo.rb │ ├── plugins.rb │ ├── update.rb │ ├── yesterday.rb │ ├── today.rb │ ├── undo.rb │ ├── again.rb │ ├── since.rb │ ├── template.rb │ ├── cancel.rb │ ├── open.rb │ ├── commands_accepting.rb │ ├── archive.rb │ ├── tags.rb │ ├── rotate.rb │ └── on.rb ├── SECURITY.md ├── LICENSE.txt ├── rdoc_to_mmd.rb ├── .gitignore └── doing.gemspec /export.doing: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-dinky -------------------------------------------------------------------------------- /test/bad.doingrc: -------------------------------------------------------------------------------- 1 | THIS IS NOT YAML 2 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /.gitsecret/paths/mapping.cfg: -------------------------------------------------------------------------------- 1 | buildnotes.md: 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | -------------------------------------------------------------------------------- /docs/doc/css/common.css: -------------------------------------------------------------------------------- 1 | /* Override this file with custom rules */ -------------------------------------------------------------------------------- /scripts/runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bundle install 4 | rake test 5 | -------------------------------------------------------------------------------- /generate_completions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bundle exec bin/doing completion --type all 4 | -------------------------------------------------------------------------------- /img/doing-colors.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttscoff/doing/master/img/doing-colors.jpg -------------------------------------------------------------------------------- /.gitsecret/keys/pubring.kbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttscoff/doing/master/.gitsecret/keys/pubring.kbx -------------------------------------------------------------------------------- /.gitsecret/keys/trustdb.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttscoff/doing/master/.gitsecret/keys/trustdb.gpg -------------------------------------------------------------------------------- /img/doing-printf-wrap-800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttscoff/doing/master/img/doing-printf-wrap-800.jpg -------------------------------------------------------------------------------- /lib/doing/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | VERSION = '2.1.91' 5 | end 6 | -------------------------------------------------------------------------------- /yard_templates/default/method_details/setup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | def source 4 | nil 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'http://rubygems.org' 4 | 5 | gem 'rubocop' 6 | 7 | gemspec 8 | -------------------------------------------------------------------------------- /img/doing-show-note-formatting-800.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttscoff/doing/master/img/doing-show-note-formatting-800.jpg -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ttscoff] 2 | custom: ['https://brettterpstra.com/support/', 'https://brettterpstra.com/donate/'] 3 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --output-dir docs/doc --markup-provider=redcarpet --markup=markdown --no-private --exclude=README.md lib/doing/**/*.rb 2 | -------------------------------------------------------------------------------- /lib/doing/plugins/import/cal_to_json.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ttscoff/doing/master/lib/doing/plugins/import/cal_to_json.scpt -------------------------------------------------------------------------------- /.irbrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/doing' 4 | include Doing 5 | 6 | @wwid = WWID.new 7 | @wwid.init_doing_file 8 | -------------------------------------------------------------------------------- /lib/doing/changelog/changelog.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'version' 4 | require_relative 'entry' 5 | require_relative 'change' 6 | require_relative 'changes' 7 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/protector/protector.go: -------------------------------------------------------------------------------- 1 | // +build !openbsd 2 | 3 | package protector 4 | 5 | // Protect calls OS specific protections like pledge on OpenBSD 6 | func Protect() { 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /features/step_definitions/doing_steps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | When(/^I get help for "([^"]*)"$/) do |app_name| 4 | @app_name = app_name 5 | step %(I run `#{app_name} help`) 6 | end 7 | 8 | # Add more step definitions here 9 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/tui/ttyname_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package tui 4 | 5 | import "os" 6 | 7 | func ttyname() string { 8 | return "" 9 | } 10 | 11 | // TtyIn on Windows returns os.Stdin 12 | func TtyIn() *os.File { 13 | return os.Stdin 14 | } 15 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/util/slab.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | type Slab struct { 4 | I16 []int16 5 | I32 []int32 6 | } 7 | 8 | func MakeSlab(size16 int, size32 int) *Slab { 9 | return &Slab{ 10 | I16: make([]int16, size16), 11 | I32: make([]int32, size32)} 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | sudo: required 4 | dist: trusty 5 | cache: bundler 6 | rvm: 7 | - ruby-2.6.4 8 | - ruby-2.7.0 9 | - ruby-3.0.1 10 | install: 11 | - gem install bundler --version '2.2.29' 12 | - bundle install 13 | script: "bundle exec rake parallel:test" 14 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/protector/protector_openbsd.go: -------------------------------------------------------------------------------- 1 | // +build openbsd 2 | 3 | package protector 4 | 5 | import "golang.org/x/sys/unix" 6 | 7 | // Protect calls OS specific protections like pledge on OpenBSD 8 | func Protect() { 9 | unix.PledgePromises("stdio rpath tty proc exec") 10 | } 11 | -------------------------------------------------------------------------------- /features/doing.feature: -------------------------------------------------------------------------------- 1 | Feature: My bootstrapped app kinda works 2 | In order to get going on coding my awesome app 3 | I want to have aruba and cucumber setup 4 | So I don't have to do it myself 5 | 6 | Scenario: App just runs 7 | When I get help for "doing" 8 | Then the exit status should be 0 9 | -------------------------------------------------------------------------------- /docker/bash_profile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export GLI_DEBUG=true 3 | export EDITOR="/usr/bin/vim" 4 | alias bdoing="GLI_DEBUG=true bundle exec bin/doing" 5 | 6 | shopt -s nocaseglob 7 | shopt -s histappend 8 | shopt -s histreedit 9 | shopt -s histverify 10 | shopt -s cmdhist 11 | 12 | cd /doing 13 | bundle install 14 | -------------------------------------------------------------------------------- /lib/helpers/fzf/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | fzf "github.com/junegunn/fzf/src" 5 | "github.com/junegunn/fzf/src/protector" 6 | ) 7 | 8 | var version string = "0.28" 9 | var revision string = "devel" 10 | 11 | func main() { 12 | protector.Protect() 13 | fzf.Run(fzf.ParseOptions(), version, revision) 14 | } 15 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test/unit' 4 | 5 | # Add test libraries you want to use here, e.g. mocha 6 | 7 | module Test 8 | module Unit 9 | class TestCase 10 | ENV['TZ'] = 'UTC' 11 | # Add global extensions to the test case class here 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/templates/doing-dayone.erb: -------------------------------------------------------------------------------- 1 | # <%= @page_title %> 2 | <% @items.each do |i| %> 3 | - [<%= i[:done] %>] <%= i[:date] %> <%= i[:title] %> <% if i[:time] && i[:time] != "00:00:00" %>[**<%= i[:time] %>**]<% end %><% if i[:note].length.positive? %><%= "\n\n " + i[:note].map{|n| n.strip }.join("\n ") %><% end %><% end %> 4 | 5 | <%= @totals %> 6 | -------------------------------------------------------------------------------- /lib/templates/doing-dayone-entry.erb: -------------------------------------------------------------------------------- 1 | <% @items.each do |i| %>Doing on <%= i[:date_object].strftime('%A %m/%d/%y') %> 2 | 3 | <%= i[:title] %><% if i[:note].length.positive? %><%= "\n\n" + i[:note].map{|n| n.strip }.join("\n ") %><% end %> 4 | 5 | <% if i[:human_time] && i[:time] != "00:00:00" %>_Took <%= i[:human_time] %>._<% end %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /lib/templates/doing-markdown.erb: -------------------------------------------------------------------------------- 1 | # <%= @page_title %> 2 | <% @items.each do |i| %> 3 | - [<%= i[:done] %>] <%= i[:date] %> <%= i[:title] %> <% if i[:time] && i[:time] != "00:00:00" %>[**<%= i[:time] %>**]<% end %><% if i[:note].length.positive? %><%= "\n\n " + i[:note].map{|n| n.strip }.join("\n ") %><% end %><% end %> 4 | 5 | <%= @totals %> 6 | -------------------------------------------------------------------------------- /scripts/setting_replace.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | content = IO.read(ARGV[0]) 5 | 6 | content.gsub!(/Doing.settings((\[.*?\])+)/) do 7 | m = Regexp.last_match 8 | keypath = m[0].scan(/\['([^\]]+)'\]/).map { |e| e[0] }.join('.') 9 | "Doing.setting('#{keypath}')" 10 | end 11 | 12 | puts content 13 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Brett Terpstra 2 | Daniel Schildt 3 | Thomas Bradley 4 | zugzug 5 | Ben Tsai 6 | Gabe Anzelini 7 | Krzysztof Blacha 8 | vinney cavallo 9 | Benjamin Wuethrich 10 | Michael Johnston 11 | Pavlos Vinieratos 12 | Sean M. Collins 13 | Dmitry M 14 | Yasuhito Takamiya 15 | Zearin 16 | gustafekeberg 17 | led 18 | Guillaume BROGI 19 | Matte Edens 20 | -------------------------------------------------------------------------------- /rdocfixer.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | input = $stdin.read 5 | 6 | input.gsub!(/\n\n( +)##\n/, "\n\n") 7 | input.gsub!(/## +/, '## ') 8 | input.gsub!(/## @param +(\w+)(?: +(.*?))?$/, '## - +\1+ -- \2') 9 | input.gsub!(/## @returns? +(.*?)$/, '## Returns \1') 10 | input.gsub!(/(?<= )##(?= |\n)/, '#') 11 | 12 | puts input 13 | -------------------------------------------------------------------------------- /bin/commands/choose.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @@choose 4 | desc 'Select a section to display from a menu' 5 | command :choose do |c| 6 | c.action do |_global_options, _options, _args| 7 | section = @wwid.choose_section 8 | 9 | Doing::Pager.page @wwid.list_section({ section: section.cap_first, count: 0 }) if section 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/doing/chronify/chronify.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'array' 4 | require_relative 'numeric' 5 | require_relative 'string' 6 | 7 | class ::String 8 | include Doing::ChronifyString 9 | end 10 | 11 | class ::Array 12 | include Doing::ChronifyArray 13 | end 14 | 15 | class ::Numeric 16 | include Doing::ChronifyNumeric 17 | end 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 2.1.x | :white_check_mark: | 8 | | 1.0.x | :x: | 9 | | 0.x | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Please use Issues to report any vulnerability. Fixes will be published as soon as possible. 14 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0.1 2 | # RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 3 | RUN mkdir /doing 4 | WORKDIR /doing 5 | # COPY ./ /doing/ 6 | RUN gem install bundler:2.2.17 7 | RUN apt-get update -y 8 | RUN apt-get install -y less vim 9 | COPY ./docker/inputrc /root/.inputrc 10 | COPY ./docker/bash_profile /root/.bash_profile 11 | CMD ["scripts/runtests.sh"] 12 | -------------------------------------------------------------------------------- /docker/Dockerfile-2.6: -------------------------------------------------------------------------------- 1 | FROM ruby:2.6 2 | # RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 3 | RUN mkdir /doing 4 | WORKDIR /doing 5 | # COPY ./ /doing/ 6 | RUN gem install bundler:2.2.17 7 | RUN apt-get update -y 8 | RUN apt-get install -y less vim 9 | COPY ./docker/inputrc /root/.inputrc 10 | COPY ./docker/bash_profile /root/.bash_profile 11 | CMD ["scripts/runtests.sh"] 12 | -------------------------------------------------------------------------------- /docker/Dockerfile-2.7: -------------------------------------------------------------------------------- 1 | FROM ruby:2.7 2 | # RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 3 | RUN mkdir /doing 4 | WORKDIR /doing 5 | # COPY ./ /doing/ 6 | RUN gem install bundler:2.2.17 7 | RUN apt-get update -y 8 | RUN apt-get install -y less vim 9 | COPY ./docker/inputrc /root/.inputrc 10 | COPY ./docker/bash_profile /root/.bash_profile 11 | CMD ["scripts/runtests.sh"] 12 | -------------------------------------------------------------------------------- /docker/Dockerfile-3.0: -------------------------------------------------------------------------------- 1 | FROM ruby:3.0.0 2 | # RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - 3 | RUN mkdir /doing 4 | WORKDIR /doing 5 | # COPY ./ /doing/ 6 | RUN gem install bundler:2.2.17 7 | RUN apt-get update -y 8 | RUN apt-get install -y less vim 9 | COPY ./docker/inputrc /root/.inputrc 10 | COPY ./docker/bash_profile /root/.bash_profile 11 | CMD ["scripts/runtests.sh"] 12 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/util/atomicbool_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestAtomicBool(t *testing.T) { 6 | if !NewAtomicBool(true).Get() || NewAtomicBool(false).Get() { 7 | t.Error("Invalid initial value") 8 | } 9 | 10 | ab := NewAtomicBool(true) 11 | if ab.Set(false) { 12 | t.Error("Invalid return value") 13 | } 14 | if ab.Get() { 15 | t.Error("Invalid state") 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/result_others.go: -------------------------------------------------------------------------------- 1 | // +build !386,!amd64 2 | 3 | package fzf 4 | 5 | func compareRanks(irank Result, jrank Result, tac bool) bool { 6 | for idx := 3; idx >= 0; idx-- { 7 | left := irank.points[idx] 8 | right := jrank.points[idx] 9 | if left < right { 10 | return true 11 | } else if left > right { 12 | return false 13 | } 14 | } 15 | return (irank.item.Index() <= jrank.item.Index()) != tac 16 | } 17 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/result_x86.go: -------------------------------------------------------------------------------- 1 | // +build 386 amd64 2 | 3 | package fzf 4 | 5 | import "unsafe" 6 | 7 | func compareRanks(irank Result, jrank Result, tac bool) bool { 8 | left := *(*uint64)(unsafe.Pointer(&irank.points[0])) 9 | right := *(*uint64)(unsafe.Pointer(&jrank.points[0])) 10 | if left < right { 11 | return true 12 | } else if left > right { 13 | return false 14 | } 15 | return (irank.item.Index() <= jrank.item.Index()) != tac 16 | } 17 | -------------------------------------------------------------------------------- /test/plugins/hooks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Doing::Hooks.register :post_write do |filename| 4 | puts "Post write hook! #{filename} written." if ENV['HOOK_TEST'] 5 | end 6 | 7 | Doing::Hooks.register :post_read do |wwid| 8 | if ENV['HOOK_TEST'] 9 | total = 0 10 | wwid.content.sections.each { |section| total += wwid.content.in_section(section.title).count } 11 | puts "Post read hook! Read #{total} items." 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/doing/array/nested_hash.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | ## 5 | ## Array helpers 6 | ## 7 | module ArrayNestedHash 8 | ## 9 | ## Convert array to nested hash, setting last key to value 10 | ## 11 | ## @param value The value to set 12 | ## 13 | def nested_hash(value = nil) 14 | hsh = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) } 15 | hsh.dig(*self[0..-2])[fetch(-1)] = value 16 | hsh 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/examples/plugins/wiki_export/templates/wiki_index.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %meta{"charset" => "utf-8"}/ 5 | %meta{"content" => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}/ 6 | %title= @page_title 7 | %style= @style 8 | %body 9 | %header 10 | %h1= @page_title 11 | %article 12 | %ul 13 | - @tags.each do |tag| 14 | %li 15 | %a{href: "#{tag[:name]}.html"} 16 | %span.tag= tag[:name] 17 | %span.count= "(#{tag[:count]})" 18 | 19 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/tui/tui_test.go: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import "testing" 4 | 5 | func TestHexToColor(t *testing.T) { 6 | assert := func(expr string, r, g, b int) { 7 | color := HexToColor(expr) 8 | if !color.is24() || 9 | int((color>>16)&0xff) != r || 10 | int((color>>8)&0xff) != g || 11 | int((color)&0xff) != b { 12 | t.Fail() 13 | } 14 | } 15 | 16 | assert("#ff0000", 255, 0, 0) 17 | assert("#010203", 1, 2, 3) 18 | assert("#102030", 16, 32, 48) 19 | assert("#ffffff", 255, 255, 255) 20 | } 21 | -------------------------------------------------------------------------------- /lib/helpers/fzf/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/junegunn/fzf 2 | 3 | require ( 4 | github.com/gdamore/tcell v1.4.0 5 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 6 | github.com/mattn/go-isatty v0.0.14 7 | github.com/mattn/go-runewidth v0.0.13 8 | github.com/mattn/go-shellwords v1.0.12 9 | github.com/rivo/uniseg v0.2.0 10 | github.com/saracen/walker v0.1.2 11 | golang.org/x/sys v0.1.0 12 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 13 | golang.org/x/text v0.3.8 // indirect 14 | ) 15 | 16 | go 1.13 17 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'aruba/cucumber' 4 | 5 | ENV['PATH'] = "#{File.expand_path("#{File.dirname(__FILE__)}/../../bin")}#{File::PATH_SEPARATOR}#{ENV['PATH']}" 6 | LIB_DIR = File.join(__dir__, '..', '..', 'lib') 7 | 8 | Before do 9 | # Using "announce" causes massive warnings on 1.9.2 10 | @puts = true 11 | @original_rubylib = ENV['RUBYLIB'] 12 | ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s 13 | end 14 | 15 | After do 16 | ENV['RUBYLIB'] = @original_rubylib 17 | end 18 | -------------------------------------------------------------------------------- /lib/helpers/fzf/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux/base:latest 2 | RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc 3 | RUN gem install --no-document -v 5.14.2 minitest 4 | RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc 5 | RUN echo '. ~/.bashrc' >> ~/.bash_profile 6 | 7 | # Do not set default PS1 8 | RUN rm -f /etc/bash.bashrc 9 | COPY . /fzf 10 | RUN cd /fzf && make install && ./install --all 11 | CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ] 12 | -------------------------------------------------------------------------------- /bin/commands/install_fzf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @@install_fzf 4 | command :install_fzf do |c| 5 | c.desc 'Force reinstall' 6 | c.switch %i[r reinstall], default_value: false 7 | 8 | c.desc 'Uninstall' 9 | c.switch %i[u uninstall], default_value: false, negatable: false 10 | 11 | c.action do |_g, o, _a| 12 | if o[:uninstall] 13 | Doing::Prompt.uninstall_fzf 14 | else 15 | Doing.logger.warn('fzf:', 'force reinstall') if o[:reinstall] 16 | Doing::Prompt.install_fzf(force: o[:reinstall]) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/terminal_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package fzf 4 | 5 | import ( 6 | "os" 7 | "os/signal" 8 | "strings" 9 | "syscall" 10 | ) 11 | 12 | func notifyOnResize(resizeChan chan<- os.Signal) { 13 | signal.Notify(resizeChan, syscall.SIGWINCH) 14 | } 15 | 16 | func notifyStop(p *os.Process) { 17 | p.Signal(syscall.SIGSTOP) 18 | } 19 | 20 | func notifyOnCont(resizeChan chan<- os.Signal) { 21 | signal.Notify(resizeChan, syscall.SIGCONT) 22 | } 23 | 24 | func quoteEntry(entry string) string { 25 | return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" 26 | } 27 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/item_test.go: -------------------------------------------------------------------------------- 1 | package fzf 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/junegunn/fzf/src/util" 7 | ) 8 | 9 | func TestStringPtr(t *testing.T) { 10 | orig := []byte("\x1b[34mfoo") 11 | text := []byte("\x1b[34mbar") 12 | item := Item{origText: &orig, text: util.ToChars(text)} 13 | if item.AsString(true) != "foo" || item.AsString(false) != string(orig) { 14 | t.Fail() 15 | } 16 | if item.AsString(true) != "foo" { 17 | t.Fail() 18 | } 19 | item.origText = nil 20 | if item.AsString(true) != string(text) || item.AsString(false) != string(text) { 21 | t.Fail() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/doing/changelog/entry.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | # An individual changelog item 5 | class Entry 6 | attr_reader :type, :string 7 | 8 | attr_writer :prefix 9 | 10 | def initialize(string, type, prefix: false) 11 | @string = string 12 | @type = type 13 | @prefix = prefix 14 | end 15 | 16 | def clean(string) 17 | string.gsub(/\|/, '\|') 18 | end 19 | 20 | def print_prefix 21 | @prefix ? "#{@type}: " : '' 22 | end 23 | 24 | def to_s 25 | "- #{print_prefix}#{clean(@string)}" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/plugins/test_import.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | class TestImport 5 | include Doing::Util 6 | 7 | def self.settings 8 | { 9 | type: :import, 10 | trigger: 'tester' 11 | } 12 | end 13 | 14 | def self.import(_wwid, path, options: {}) 15 | if path.nil? 16 | Doing.logger.info('Test with no paths') 17 | else 18 | Doing.logger.info("Test with path: #{path}") 19 | end 20 | Doing.logger.info('Test Import Plugin Ran') 21 | end 22 | 23 | Doing::Plugins.register 'tester', :import, self 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/helpers/fake_std_out.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class FakeStdOut 4 | attr_reader :strings 5 | 6 | def initialize 7 | @strings = [] 8 | end 9 | 10 | def puts(string = nil) 11 | @strings << string unless string.nil? 12 | end 13 | 14 | def write(x) 15 | puts(x) 16 | end 17 | 18 | def printf(*args) 19 | puts(Kernel.printf(*args)) 20 | end 21 | 22 | # Returns true if the regexp matches anything in the output 23 | def contained?(regexp) 24 | strings.find { |x| x =~ regexp } 25 | end 26 | 27 | def flush; end 28 | 29 | def to_s 30 | @strings.join("\n") 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/helpers/fzf/.rubocop.yml: -------------------------------------------------------------------------------- 1 | Layout/LineLength: 2 | Enabled: false 3 | Metrics: 4 | Enabled: false 5 | Lint/ShadowingOuterLocalVariable: 6 | Enabled: false 7 | Style/MethodCallWithArgsParentheses: 8 | Enabled: true 9 | IgnoredMethods: 10 | - assert 11 | - exit 12 | - paste 13 | - puts 14 | - raise 15 | - refute 16 | - require 17 | - send_keys 18 | IgnoredPatterns: 19 | - ^assert_ 20 | - ^refute_ 21 | Style/NumericPredicate: 22 | Enabled: false 23 | Style/StringConcatenation: 24 | Enabled: false 25 | Style/OptionalBooleanParameter: 26 | Enabled: false 27 | Style/WordArray: 28 | MinSize: 1 29 | -------------------------------------------------------------------------------- /docs/doc/frames.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation by YARD 0.9.37 6 | 7 | 18 | 22 | 23 | -------------------------------------------------------------------------------- /lib/templates/doing.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %meta{"charset" => "utf-8"}/ 5 | %meta{"content" => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}/ 6 | %title what are you doing? 7 | %style= @style 8 | %body 9 | %header 10 | %h1= @page_title 11 | %article 12 | %ul 13 | - @items.each do |i| 14 | %li 15 | %span.date= i[:date] 16 | %div.entry 17 | = i[:title] 18 | %span.section= i[:section] 19 | - if i[:time] && i[:time] != "00:00:00" 20 | %span.time= i[:time] 21 | - if i[:note] 22 | %ul.note 23 | - i[:note].map{|n| n.strip }.each do |li| 24 | %li= li 25 | = @totals 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for Doing 4 | title: "[FEATURE REQUEST]" 5 | labels: '' 6 | assignees: ttscoff 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestMax(t *testing.T) { 6 | if Max(-2, 5) != 5 { 7 | t.Error("Invalid result") 8 | } 9 | } 10 | 11 | func TestContrain(t *testing.T) { 12 | if Constrain(-3, -1, 3) != -1 { 13 | t.Error("Expected", -1) 14 | } 15 | if Constrain(2, -1, 3) != 2 { 16 | t.Error("Expected", 2) 17 | } 18 | 19 | if Constrain(5, -1, 3) != 3 { 20 | t.Error("Expected", 3) 21 | } 22 | } 23 | 24 | func TestOnce(t *testing.T) { 25 | o := Once(false) 26 | if o() { 27 | t.Error("Expected: false") 28 | } 29 | if o() { 30 | t.Error("Expected: false") 31 | } 32 | 33 | o = Once(true) 34 | if !o() { 35 | t.Error("Expected: true") 36 | } 37 | if o() { 38 | t.Error("Expected: false") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/doing/prompt/std.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | # STDOUT and STDERR methods 5 | module PromptSTD 6 | ## 7 | ## Clear the terminal screen 8 | ## 9 | def clear_screen(msg = nil) 10 | puts "\e[H\e[2J" if $stdout.tty? 11 | puts msg if msg.good? 12 | end 13 | 14 | ## 15 | ## Redirect STDOUT and STDERR to /dev/null or file 16 | ## 17 | ## @param file [String] a file path to redirect to 18 | ## 19 | def silence_std(file = '/dev/null') 20 | $stdout = File.new(file, 'w') 21 | $stderr = File.new(file, 'w') 22 | end 23 | 24 | ## 25 | ## Restore silenced STDOUT and STDERR 26 | ## 27 | def restore_std 28 | $stdout = STDOUT 29 | $stderr = STDERR 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /bin/commands/colors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @@colors 4 | desc 'List available color variables for configuration templates and views' 5 | command :colors do |c| 6 | c.action do |_global_options, _options, _args| 7 | bgs = [] 8 | fgs = [] 9 | @colors.attributes.each do |color| 10 | colname = color.to_s 11 | colname << " (#{color.to_s.sub(/bold/, 'bright')})" if colname =~ /bold/ 12 | if color.to_s =~ /bg/ 13 | bgs.push("#{@colors.send(color, ' ')}#{@colors.default} <-- #{colname}") 14 | else 15 | fgs.push("#{@colors.send(color, 'XXXX')}#{@colors.default} <-- #{colname}") 16 | end 17 | end 18 | out = [] 19 | out << fgs.join("\n") 20 | out << bgs.join("\n") 21 | Doing::Pager.page out.join("\n") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/doing/section.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | # Section Object 5 | class Section 6 | attr_accessor :original, :title 7 | 8 | def initialize(title, original: nil) 9 | super() 10 | 11 | @title = title 12 | 13 | @original = if original.nil? 14 | "#{title}:" 15 | else 16 | original =~ /:(\s+@[^ (]+(\([^)]*\))?)*?$/ ? original : "#{original}:" 17 | end 18 | end 19 | 20 | def equal?(other) 21 | @title == other.title 22 | end 23 | 24 | # Outputs section title 25 | def to_s 26 | @title 27 | end 28 | 29 | # @private 30 | def inspect 31 | %(#) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/examples/plugins/wiki_export/templates/wiki.haml: -------------------------------------------------------------------------------- 1 | !!! 2 | %html 3 | %head 4 | %meta{"charset" => "utf-8"}/ 5 | %meta{"content" => "IE=edge,chrome=1", "http-equiv" => "X-UA-Compatible"}/ 6 | %title what are you doing? 7 | %style= @style 8 | %body 9 | %header 10 | %h1= @page_title 11 | %p 12 | %a{ href: 'index.html' }= "[[Home]]" 13 | %article 14 | %ul 15 | - @items.each do |i| 16 | %li 17 | %span.date= i[:date] 18 | %div.entry 19 | = i[:title] 20 | %span.section= i[:section] 21 | - if i[:time] && i[:time] != "00:00:00" 22 | %span.time= i[:time] 23 | - if i[:note] 24 | %ul.note 25 | - i[:note].map{|n| n.strip }.each do |li| 26 | %li= li 27 | = @totals 28 | -------------------------------------------------------------------------------- /lib/doing/array/array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'tags' 4 | require_relative 'nested_hash' 5 | require_relative 'cleanup' 6 | 7 | module Doing 8 | class ::Array 9 | include ArrayTags 10 | include ArrayNestedHash 11 | include ArrayCleanup 12 | ## 13 | ## Force UTF-8 encoding of strings in array 14 | ## 15 | ## @return [Array] Encoded lines 16 | ## 17 | def utf8 18 | c = self.class 19 | if String.method_defined? :force_encoding 20 | replace c.new(map(&:utf8)) 21 | else 22 | self 23 | end 24 | end 25 | 26 | ## 27 | ## Capitalize first letter of each element 28 | ## 29 | ## @return [Array] capitalized items 30 | ## 31 | def cap_first 32 | map(&:cap_first) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /bin/commands/redo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @@redo 4 | desc 'Redo an undo command' 5 | long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. Specify a count to redo multiple undos' 6 | arg_name 'COUNT' 7 | command :redo do |c| 8 | c.desc 'Specify alternate doing file' 9 | c.arg_name 'PATH' 10 | c.flag %i[f file], default_value: @wwid.doing_file 11 | 12 | c.desc 'Select from an interactive menu' 13 | c.switch %i[i interactive] 14 | 15 | c.action do |_global, options, args| 16 | file = options[:file] || @wwid.doing_file 17 | count = args.empty? ? 1 : args[0].to_i 18 | raise InvalidArgument, 'Invalid count specified for redo' unless count&.positive? 19 | 20 | if options[:interactive] 21 | Doing::Util::Backup.select_redo(file) 22 | else 23 | Doing::Util::Backup.redo_backup(file, count: count) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/doing/completion/completion_string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | module Completion 5 | module StringUtils 6 | ## 7 | ## Get short description for command completion 8 | ## 9 | ## @return [String] Short description 10 | ## 11 | def short_desc 12 | split(/[,.]/)[0].sub(/ \(.*?\)?$/, '').strip 13 | end 14 | 15 | ## 16 | ## Truncate string from left 17 | ## 18 | ## @param max The maximum number of characters 19 | ## 20 | def ltrunc(max) 21 | if length > max 22 | sub(/^.*?(.{#{max - 3}})$/, '...\1') 23 | else 24 | self 25 | end 26 | end 27 | 28 | def ltrunc!(max) 29 | replace ltrunc(max) 30 | end 31 | end 32 | end 33 | end 34 | 35 | class ::String 36 | include Doing::Completion::StringUtils 37 | end 38 | -------------------------------------------------------------------------------- /bin/commands/plugins.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @@plugins 4 | desc 'List installed plugins' 5 | long_desc %(Lists available plugins, including user-installed plugins. 6 | 7 | Export plugins are available with the `--output` flag on commands that support it. 8 | 9 | Import plugins are available using `doing import --type PLUGIN`. 10 | ) 11 | command :plugins do |c| 12 | c.example 'doing plugins', desc: 'List all plugins' 13 | c.example 'doing plugins -t import', desc: 'List all import plugins' 14 | 15 | c.desc 'List plugins of type (import, export)' 16 | c.arg_name 'TYPE' 17 | c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all' 18 | 19 | c.desc 'List in single column for completion' 20 | c.switch %i[c column], negatable: false, default_value: false 21 | 22 | c.action do |_global_options, options, _args| 23 | Doing::Plugins.list_plugins(options) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/util/atomicbool.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | func convertBoolToInt32(b bool) int32 { 8 | if b { 9 | return 1 10 | } 11 | return 0 12 | } 13 | 14 | // AtomicBool is a boxed-class that provides synchronized access to the 15 | // underlying boolean value 16 | type AtomicBool struct { 17 | state int32 // "1" is true, "0" is false 18 | } 19 | 20 | // NewAtomicBool returns a new AtomicBool 21 | func NewAtomicBool(initialState bool) *AtomicBool { 22 | return &AtomicBool{state: convertBoolToInt32(initialState)} 23 | } 24 | 25 | // Get returns the current boolean value synchronously 26 | func (a *AtomicBool) Get() bool { 27 | return atomic.LoadInt32(&a.state) == 1 28 | } 29 | 30 | // Set updates the boolean value synchronously 31 | func (a *AtomicBool) Set(newState bool) bool { 32 | atomic.StoreInt32(&a.state, convertBoolToInt32(newState)) 33 | return newState 34 | } 35 | -------------------------------------------------------------------------------- /lib/doing/array/cleanup.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | module ArrayCleanup 5 | ## 6 | ## Like Array#compact -- removes nil items, but also 7 | ## removes empty strings, zero or negative numbers and FalseClass items 8 | ## 9 | ## @return [Array] Array without "bad" elements 10 | ## 11 | def remove_bad 12 | compact.map { |x| x.is_a?(String) ? x.strip : x }.select(&:good?) 13 | end 14 | 15 | def remove_bad! 16 | replace remove_empty 17 | end 18 | 19 | ## 20 | ## Like Array#compact -- removes nil items, but also 21 | ## removes empty elements 22 | ## 23 | ## @return [Array] Array without empty elements 24 | ## 25 | def remove_empty 26 | compact.map { |x| x.is_a?(String) ? x.strip : x }.reject { |x| x.is_a?(String) ? x.empty? : false } 27 | end 28 | 29 | def remove_empty! 30 | replace remove_empty 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/doing/string/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'highlight' 4 | require_relative 'query' 5 | require_relative 'tags' 6 | require_relative 'transform' 7 | require_relative 'truncate' 8 | require_relative 'url' 9 | 10 | class ::String 11 | include Doing::Color 12 | include Doing::StringHighlight 13 | include Doing::StringQuery 14 | include Doing::StringTags 15 | include Doing::StringTransform 16 | include Doing::StringTruncate 17 | include Doing::StringURL 18 | 19 | ## 20 | ## Test if string is a valid 32-character MD5 id 21 | ## 22 | ## @return [Boolean] string is valid identifier 23 | ## 24 | def valid_id? 25 | strip =~ /^[a-z0-9]{32}$/ ? true : false 26 | end 27 | 28 | ## 29 | ## Force UTF-8 encoding if available 30 | ## 31 | ## @return [String] UTF-8 encoded string 32 | ## 33 | def utf8 34 | if String.method_defined? :force_encoding 35 | dup.force_encoding('utf-8') 36 | else 37 | self 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/doing/prompt/prompt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'choose' 4 | require_relative 'fzf' 5 | require_relative 'input' 6 | require_relative 'std' 7 | require_relative 'yn' 8 | 9 | module Doing 10 | # Terminal Prompt methods 11 | module Prompt 12 | class << self 13 | attr_writer :force_answer, :default_answer 14 | 15 | include Color 16 | include PromptSTD 17 | include PromptInput 18 | include PromptYN 19 | include PromptFZF 20 | include PromptChoose 21 | 22 | ## 23 | ## Value to return if prompt is skipped 24 | ## 25 | ## @return Force answer value 26 | ## 27 | def force_answer 28 | @force_answer ||= nil 29 | end 30 | 31 | ## 32 | ## If true, always return the default answer without prompting 33 | ## 34 | ## @return [Boolean] default answer 35 | ## 36 | def default_answer 37 | @default_answer ||= false 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/doing/plugins/export/taskpaper_export.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # title: TaskPaper Export 4 | # description: Export TaskPaper-friendly data 5 | # author: Brett Terpstra 6 | # url: https://brettterpstra.com 7 | module Doing 8 | class TaskPaperExport 9 | include Doing::Util 10 | 11 | def self.settings 12 | { 13 | trigger: 'task(?:paper)?|tp' 14 | } 15 | end 16 | 17 | def self.render(wwid, items, variables: {}) 18 | return if items.nil? 19 | 20 | options = variables[:options] 21 | 22 | options[:highlight] = false 23 | options[:wrap_width] = 0 24 | options[:tags_color] = false 25 | options[:output] = 'template' 26 | options[:template] = '- %title @date(%date)%note' 27 | options[:disable_color] = true 28 | 29 | Doing.logger.debug('TaskPaper Export:', "#{items.count} items output to TaskPaper format") 30 | @out = wwid.list_section(options) 31 | end 32 | 33 | Doing::Plugins.register 'taskpaper', :export, self 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/cache_test.go: -------------------------------------------------------------------------------- 1 | package fzf 2 | 3 | import "testing" 4 | 5 | func TestChunkCache(t *testing.T) { 6 | cache := NewChunkCache() 7 | chunk1p := &Chunk{} 8 | chunk2p := &Chunk{count: chunkSize} 9 | items1 := []Result{{}} 10 | items2 := []Result{{}, {}} 11 | cache.Add(chunk1p, "foo", items1) 12 | cache.Add(chunk2p, "foo", items1) 13 | cache.Add(chunk2p, "bar", items2) 14 | 15 | { // chunk1 is not full 16 | cached := cache.Lookup(chunk1p, "foo") 17 | if cached != nil { 18 | t.Error("Cached disabled for non-empty chunks", cached) 19 | } 20 | } 21 | { 22 | cached := cache.Lookup(chunk2p, "foo") 23 | if cached == nil || len(cached) != 1 { 24 | t.Error("Expected 1 item cached", cached) 25 | } 26 | } 27 | { 28 | cached := cache.Lookup(chunk2p, "bar") 29 | if cached == nil || len(cached) != 2 { 30 | t.Error("Expected 2 items cached", cached) 31 | } 32 | } 33 | { 34 | cached := cache.Lookup(chunk1p, "foobar") 35 | if cached != nil { 36 | t.Error("Expected 0 item cached", cached) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/doing_unit-normalize_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'helpers/doing-helpers' 4 | require 'test_helper' 5 | 6 | $LOAD_PATH.unshift File.join(__dir__, '..', 'lib') 7 | require 'doing' 8 | # require 'gli' 9 | 10 | # Tests for Item class 11 | class DoingUnitNormalizeTest < Test::Unit::TestCase 12 | def test_normalize_case 13 | assert_equal(:smart, :smar.normalize_case) 14 | assert_equal(:sensitive, 'case'.normalize_case) 15 | assert_equal(:smart, 's'.normalize_case) 16 | end 17 | 18 | def test_normalize_tag_sort 19 | assert_equal(:name, 'name'.normalize_tag_sort) 20 | assert_equal(:time, :t.normalize_tag_sort) 21 | end 22 | 23 | def test_normalize_age 24 | assert_equal(:oldest, 'old'.normalize_age) 25 | assert_equal(:newest, :newest.normalize_age) 26 | end 27 | 28 | def test_normalize_bool 29 | assert_equal(:and, 'AND'.normalize_bool) 30 | assert_equal(:or, :any.normalize_bool) 31 | assert_equal(:not, :not.normalize_bool) 32 | assert_equal(:not, 'none'.normalize_bool) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/doing/items/modify.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | class Items < Array 5 | ## 6 | ## Delete an item from the index 7 | ## 8 | ## @param item The item 9 | ## 10 | def delete_item(item, single: false) 11 | deleted = delete(item) 12 | Doing.logger.count(:deleted) 13 | Doing.logger.info('Entry deleted:', deleted.title) if single 14 | deleted 15 | end 16 | 17 | ## 18 | ## Update an item in the index with a modified item 19 | ## 20 | ## @param old_item The old item 21 | ## @param new_item The new item 22 | ## 23 | def update_item(old_item, new_item) 24 | s_idx = index { |item| item.equal?(old_item) } 25 | 26 | raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx 27 | 28 | return if fetch(s_idx).equal?(new_item) 29 | 30 | self[s_idx] = new_item 31 | Doing.logger.count(:updated) 32 | Doing.logger.info('Entry updated:', self[s_idx].title.trunc(60)) 33 | new_item 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /scripts/sort_commands.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'awesome_print' 5 | 6 | input = $stdin.read.force_encoding('utf-8') 7 | 8 | commands = input.split(/^# @@/).delete_if(&:empty?).sort 9 | # commands.each do |cmd| 10 | # puts cmd.split(/^(\w+)(.*)$/m)[1] 11 | # end 12 | indexes = %w[ 13 | again 14 | cancel 15 | done 16 | finish 17 | later 18 | mark 19 | meanwhile 20 | note 21 | now 22 | reset 23 | select 24 | tag 25 | choose 26 | grep 27 | last 28 | recent 29 | show 30 | tags 31 | today 32 | view 33 | yesterday 34 | open 35 | config 36 | archive 37 | import 38 | rotate 39 | colors 40 | completion 41 | plugins 42 | sections 43 | template 44 | views 45 | undo 46 | redo 47 | add_section 48 | tag_dir 49 | changelog 50 | ] 51 | 52 | result = Array.new(indexes.count) 53 | 54 | commands.each do |cmd| 55 | key = cmd.match(/^(\w+)/)[1] 56 | idx = indexes.index(key) 57 | result[idx] = "#@@#{cmd}" 58 | # puts commands.join('# @@') 59 | end 60 | 61 | puts result.join('') 62 | -------------------------------------------------------------------------------- /lib/examples/commands/todo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @@todo 4 | desc 'Add an item as a Todo' 5 | long_desc 'Adds an item to a Todo section, and tags it with @todo' 6 | arg_name 'ENTRY' 7 | command :todo do |c| 8 | c.example 'doing todo "Something I\'ll think about tomorrow"', desc: 'Add an entry to the Todo section with tag @todo' 9 | c.example 'doing later -e', desc: 'Open $EDITOR to create an entry and optional note' 10 | 11 | c.desc "Edit entry with #{Doing::Util.default_editor}" 12 | c.switch %i[e editor], negatable: false, default_value: false 13 | 14 | c.desc 'Note' 15 | c.arg_name 'TEXT' 16 | c.flag %i[n note] 17 | 18 | c.desc 'Prompt for note via multi-line input' 19 | c.switch %i[ask], negatable: false, default_value: false 20 | 21 | c.action do |global_options, options, args| 22 | cmd = commands[:now] 23 | options[:section] = 'Todo' 24 | options[:finish_last] = false 25 | action = cmd.send(:get_action, nil) 26 | string = args.join(' ').add_tags('todo') 27 | action.call(global_options, options, [string]) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help improve Doing 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: ttscoff 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | **To Reproduce** 15 | 16 | 17 | Steps to reproduce the behavior: 18 | 19 | 1. Execute command '...' 20 | 2. See error 21 | 22 | Please execute the problematic command with the prefix `GLI_DEBUG=true DOING_DEBUG=true` and include the output with your bug report. E.g. `GLI_DEBUG=true DOING_DEBUG=true doing completion --type zsh`. 23 | 24 | **Expected behavior** 25 | 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Desktop (please complete the following information):** 29 | 30 | - Platform: [e.g. Mac, Linux] 31 | - OS: [e.g. Monterrey, Unbuntu] 32 | - Version [e.g. 22] 33 | 34 | **Doing version** 35 | 36 | - Result of `doing -v` 37 | 38 | **Ruby version** 39 | 40 | - Result of `ruby -v` 41 | 42 | **Additional context** 43 | 44 | Add any other context about the problem here. 45 | -------------------------------------------------------------------------------- /lib/doing/plugins/export/doing_export.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # title: Doing File Export 4 | # description: Export Doing format data 5 | # author: Brett Terpstra 6 | # url: https://brettterpstra.com 7 | module Doing 8 | class DoingExport 9 | def self.settings 10 | { 11 | trigger: 'doing' 12 | } 13 | end 14 | 15 | def self.render(_wwid, items, variables: {}) 16 | return if items.nil? 17 | 18 | content = Doing::Items.new 19 | items.each do |item| 20 | content.add_section(item.section, log: false) 21 | content.push(item) 22 | end 23 | 24 | out = [] 25 | content.sections.each do |section| 26 | out.push(section.original) 27 | is = content.in_section(section.title).sort_by { |i| [i.date, i.title] } 28 | is.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc 29 | is.each { |item| out.push(item.to_s) } 30 | end 31 | 32 | Doing::Pager.page out.join("\n") 33 | end 34 | 35 | Doing::Plugins.register 'doing', :export, self 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/tui/ttyname_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package tui 4 | 5 | import ( 6 | "io/ioutil" 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | var devPrefixes = [...]string{"/dev/pts/", "/dev/"} 12 | 13 | func ttyname() string { 14 | var stderr syscall.Stat_t 15 | if syscall.Fstat(2, &stderr) != nil { 16 | return "" 17 | } 18 | 19 | for _, prefix := range devPrefixes { 20 | files, err := ioutil.ReadDir(prefix) 21 | if err != nil { 22 | continue 23 | } 24 | 25 | for _, file := range files { 26 | if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev { 27 | return prefix + file.Name() 28 | } 29 | } 30 | } 31 | return "" 32 | } 33 | 34 | // TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin 35 | func TtyIn() *os.File { 36 | in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0) 37 | if err != nil { 38 | tty := ttyname() 39 | if len(tty) > 0 { 40 | if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil { 41 | return in 42 | } 43 | } 44 | return os.Stdin 45 | } 46 | return in 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Brett Terpstra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/item.go: -------------------------------------------------------------------------------- 1 | package fzf 2 | 3 | import ( 4 | "github.com/junegunn/fzf/src/util" 5 | ) 6 | 7 | // Item represents each input line. 56 bytes. 8 | type Item struct { 9 | text util.Chars // 32 = 24 + 1 + 1 + 2 + 4 10 | transformed *[]Token // 8 11 | origText *[]byte // 8 12 | colors *[]ansiOffset // 8 13 | } 14 | 15 | // Index returns ordinal index of the Item 16 | func (item *Item) Index() int32 { 17 | return item.text.Index 18 | } 19 | 20 | var minItem = Item{text: util.Chars{Index: -1}} 21 | 22 | func (item *Item) TrimLength() uint16 { 23 | return item.text.TrimLength() 24 | } 25 | 26 | // Colors returns ansiOffsets of the Item 27 | func (item *Item) Colors() []ansiOffset { 28 | if item.colors == nil { 29 | return []ansiOffset{} 30 | } 31 | return *item.colors 32 | } 33 | 34 | // AsString returns the original string 35 | func (item *Item) AsString(stripAnsi bool) string { 36 | if item.origText != nil { 37 | if stripAnsi { 38 | trimmed, _, _ := extractColor(string(*item.origText), nil, nil) 39 | return trimmed 40 | } 41 | return string(*item.origText) 42 | } 43 | return item.text.ToString() 44 | } 45 | -------------------------------------------------------------------------------- /lib/doing/chronify/numeric.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | ## 5 | ## Number helpers 6 | ## 7 | module ChronifyNumeric 8 | ## 9 | ## Format human readable time from seconds 10 | ## 11 | ## @param human [Boolean] if True, don't convert 12 | ## hours into days 13 | ## 14 | def format_time(human: false) 15 | return [0, 0, 0] if nil? 16 | 17 | seconds = dup.to_i 18 | minutes = (seconds / 60).to_i 19 | hours = (minutes / 60).to_i 20 | if human 21 | minutes = (minutes % 60).to_i 22 | [0, hours, minutes] 23 | else 24 | days = (hours / 24).to_i 25 | hours = (hours % 24).to_i 26 | minutes = (minutes % 60).to_i 27 | [days, hours, minutes] 28 | end 29 | end 30 | 31 | ## 32 | ## Format seconds as natural language time string 33 | ## 34 | ## @param format [Symbol] The format to output 35 | ## (:dhm, :hm, :m, :clock, :natural) 36 | ## 37 | def time_string(format: :dhm) 38 | format_time(human: true).time_string(format: format) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/helpers/fzf/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2021 Junegunn Choi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2021 Junegunn Choi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/util/chars_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | func TestToCharsAscii(t *testing.T) { 6 | chars := ToChars([]byte("foobar")) 7 | if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes { 8 | t.Error() 9 | } 10 | } 11 | 12 | func TestCharsLength(t *testing.T) { 13 | chars := ToChars([]byte("\tabc한글 ")) 14 | if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 { 15 | t.Error() 16 | } 17 | } 18 | 19 | func TestCharsToString(t *testing.T) { 20 | text := "\tabc한글 " 21 | chars := ToChars([]byte(text)) 22 | if chars.ToString() != text { 23 | t.Error() 24 | } 25 | } 26 | 27 | func TestTrimLength(t *testing.T) { 28 | check := func(str string, exp uint16) { 29 | chars := ToChars([]byte(str)) 30 | trimmed := chars.TrimLength() 31 | if trimmed != exp { 32 | t.Errorf("Invalid TrimLength result for '%s': %d (expected %d)", 33 | str, trimmed, exp) 34 | } 35 | } 36 | check("hello", 5) 37 | check("hello ", 5) 38 | check("hello ", 5) 39 | check(" hello", 5) 40 | check(" hello", 5) 41 | check(" hello ", 5) 42 | check(" hello ", 5) 43 | check("h o", 5) 44 | check(" h o ", 5) 45 | check(" ", 0) 46 | } 47 | -------------------------------------------------------------------------------- /lib/examples/commands/later.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Example command that calls an existing command (tag) with 4 | # preset options 5 | desc 'Add an item to the Later section' 6 | arg_name 'ENTRY' 7 | command :later do |c| 8 | c.example 'doing later "Something I\'ll think about tomorrow"', desc: 'Add an entry to the Later section' 9 | c.example 'doing later -e', desc: 'Open $EDITOR to create an entry and optional note' 10 | 11 | c.desc "Edit entry with #{Doing::Util.default_editor}" 12 | c.switch %i[e editor], negatable: false, default_value: false 13 | 14 | c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]' 15 | c.arg_name 'DATE_STRING' 16 | c.flag %i[b back started], type: DateBeginString 17 | 18 | c.desc 'Note' 19 | c.arg_name 'TEXT' 20 | c.flag %i[n note] 21 | 22 | c.desc 'Prompt for note via multi-line input' 23 | c.switch %i[ask], negatable: false, default_value: false 24 | 25 | c.action do |global_options, options, args| 26 | cmd = commands[:now] 27 | options[:section] = 'Later' 28 | options[:finish_last] = false 29 | action = cmd.send(:get_action, nil) 30 | action.call(global_options, options, args) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/util/eventbox_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "testing" 4 | 5 | // fzf events 6 | const ( 7 | EvtReadNew EventType = iota 8 | EvtReadFin 9 | EvtSearchNew 10 | EvtSearchProgress 11 | EvtSearchFin 12 | EvtClose 13 | ) 14 | 15 | func TestEventBox(t *testing.T) { 16 | eb := NewEventBox() 17 | 18 | // Wait should return immediately 19 | ch := make(chan bool) 20 | 21 | go func() { 22 | eb.Set(EvtReadNew, 10) 23 | ch <- true 24 | <-ch 25 | eb.Set(EvtSearchNew, 10) 26 | eb.Set(EvtSearchNew, 15) 27 | eb.Set(EvtSearchNew, 20) 28 | eb.Set(EvtSearchProgress, 30) 29 | ch <- true 30 | <-ch 31 | eb.Set(EvtSearchFin, 40) 32 | ch <- true 33 | <-ch 34 | }() 35 | 36 | count := 0 37 | sum := 0 38 | looping := true 39 | for looping { 40 | <-ch 41 | eb.Wait(func(events *Events) { 42 | for _, value := range *events { 43 | switch val := value.(type) { 44 | case int: 45 | sum += val 46 | looping = sum < 100 47 | } 48 | } 49 | events.Clear() 50 | }) 51 | ch <- true 52 | count++ 53 | } 54 | 55 | if count != 3 { 56 | t.Error("Invalid number of events", count) 57 | } 58 | if sum != 100 { 59 | t.Error("Invalid sum", sum) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/doing/help_monkey_patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module GLI 4 | module Commands 5 | # Help Command Monkeypatch for paginated output 6 | class Help < Command 7 | def show_help(_global_options, options, arguments, out, error) 8 | Doing::Pager.paginate = true 9 | 10 | command_finder = HelpModules::CommandFinder.new(@app, arguments, error) 11 | if options[:c] 12 | help_output = HelpModules::HelpCompletionFormat.new(@app, command_finder, arguments).format 13 | out.puts help_output unless help_output.nil? 14 | elsif arguments.empty? || options[:c] 15 | Doing::Pager.page HelpModules::GlobalHelpFormat.new(@app, @sorter, @text_wrapping_class).format 16 | else 17 | name = arguments.shift 18 | command = command_finder.find_command(name) 19 | unless command.nil? 20 | Doing::Pager.page HelpModules::CommandHelpFormat.new( 21 | command, 22 | @app, 23 | @sorter, 24 | @synopsis_formatter_class, 25 | @text_wrapping_class 26 | ).format 27 | end 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/terminal_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package fzf 4 | 5 | import ( 6 | "os" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | func notifyOnResize(resizeChan chan<- os.Signal) { 12 | // TODO 13 | } 14 | 15 | func notifyStop(p *os.Process) { 16 | // NOOP 17 | } 18 | 19 | func notifyOnCont(resizeChan chan<- os.Signal) { 20 | // NOOP 21 | } 22 | 23 | func quoteEntry(entry string) string { 24 | shell := os.Getenv("SHELL") 25 | if len(shell) == 0 { 26 | shell = "cmd" 27 | } 28 | 29 | if strings.Contains(shell, "cmd") { 30 | // backslash escaping is done here for applications 31 | // (see ripgrep test case in terminal_test.go#TestWindowsCommands) 32 | escaped := strings.Replace(entry, `\`, `\\`, -1) 33 | escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"` 34 | // caret is the escape character for cmd shell 35 | r, _ := regexp.Compile(`[&|<>()@^%!"]`) 36 | return r.ReplaceAllStringFunc(escaped, func(match string) string { 37 | return "^" + match 38 | }) 39 | } else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") { 40 | escaped := strings.Replace(entry, `"`, `\"`, -1) 41 | return "'" + strings.Replace(escaped, "'", "''", -1) + "'" 42 | } else { 43 | return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/doing_unit-time_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'helpers/doing-helpers' 4 | require 'test_helper' 5 | 6 | $LOAD_PATH.unshift File.join(__dir__, '..', 'lib') 7 | require 'doing' 8 | require 'doing/time' 9 | 10 | # Tests for Item class 11 | class DoingTimeTest < Test::Unit::TestCase 12 | include DoingHelpers 13 | include Doing 14 | 15 | def setup; end 16 | 17 | def teardown; end 18 | 19 | def test_relative_date 20 | t = Time.parse("#{Time.now.year - 1}-12-21 15:00") 21 | assert_match(%r{12/21 3:00pm}, t.relative_date, 'Relative date should match') 22 | 23 | # Breaks if it's the first of the month 24 | # t = Time.parse("#{Time.now.year}-#{Time.now.month}-#{Time.now.day - 1} 12:00") 25 | # assert_match(%r{[a-z]{3} 12:00pm}i, t.relative_date, 'Relative date should match') 26 | 27 | t = Time.parse("#{Time.now.strftime('%F')} 01:00") 28 | assert_match(/^ 1:00am$/, t.relative_date, 'Relative date should match') 29 | end 30 | 31 | def test_humanize 32 | assert_match(/4 minutes, 5 seconds/, Time.now.humanize(245), 'String output should match') 33 | end 34 | 35 | def test_time_ago 36 | t = Time.now - (2 * 60 * 60) - (54 * 60) - 31 37 | assert_match(/2 hours, 54 minutes, 3[0-3] seconds ago/, t.time_ago, 'Time ago string should match') 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/util/util_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package util 4 | 5 | import ( 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | // ExecCommand executes the given command with $SHELL 12 | func ExecCommand(command string, setpgid bool) *exec.Cmd { 13 | shell := os.Getenv("SHELL") 14 | if len(shell) == 0 { 15 | shell = "sh" 16 | } 17 | return ExecCommandWith(shell, command, setpgid) 18 | } 19 | 20 | // ExecCommandWith executes the given command with the specified shell 21 | func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd { 22 | cmd := exec.Command(shell, "-c", command) 23 | if setpgid { 24 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 25 | } 26 | return cmd 27 | } 28 | 29 | // KillCommand kills the process for the given command 30 | func KillCommand(cmd *exec.Cmd) error { 31 | return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 32 | } 33 | 34 | // IsWindows returns true on Windows 35 | func IsWindows() bool { 36 | return false 37 | } 38 | 39 | // SetNonblock executes syscall.SetNonblock on file descriptor 40 | func SetNonblock(file *os.File, nonblock bool) { 41 | syscall.SetNonblock(int(file.Fd()), nonblock) 42 | } 43 | 44 | // Read executes syscall.Read on file descriptor 45 | func Read(fd int, b []byte) (int, error) { 46 | return syscall.Read(int(fd), b) 47 | } 48 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/tui/dummy.go: -------------------------------------------------------------------------------- 1 | // +build !ncurses 2 | // +build !tcell 3 | // +build !windows 4 | 5 | package tui 6 | 7 | type Attr int32 8 | 9 | func HasFullscreenRenderer() bool { 10 | return false 11 | } 12 | 13 | func (a Attr) Merge(b Attr) Attr { 14 | return a | b 15 | } 16 | 17 | const ( 18 | AttrUndefined = Attr(0) 19 | AttrRegular = Attr(1 << 7) 20 | AttrClear = Attr(1 << 8) 21 | 22 | Bold = Attr(1) 23 | Dim = Attr(1 << 1) 24 | Italic = Attr(1 << 2) 25 | Underline = Attr(1 << 3) 26 | Blink = Attr(1 << 4) 27 | Blink2 = Attr(1 << 5) 28 | Reverse = Attr(1 << 6) 29 | ) 30 | 31 | func (r *FullscreenRenderer) Init() {} 32 | func (r *FullscreenRenderer) Pause(bool) {} 33 | func (r *FullscreenRenderer) Resume(bool, bool) {} 34 | func (r *FullscreenRenderer) Clear() {} 35 | func (r *FullscreenRenderer) Refresh() {} 36 | func (r *FullscreenRenderer) Close() {} 37 | 38 | func (r *FullscreenRenderer) GetChar() Event { return Event{} } 39 | func (r *FullscreenRenderer) MaxX() int { return 0 } 40 | func (r *FullscreenRenderer) MaxY() int { return 0 } 41 | 42 | func (r *FullscreenRenderer) RefreshWindows(windows []Window) {} 43 | 44 | func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window { 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /lib/doing/plugins/export/csv_export.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # title: CSV Export 4 | # description: Export CSV formatted data with header row 5 | # author: Brett Terpstra 6 | # url: https://brettterpstra.com 7 | module Doing 8 | ## 9 | ## CSV Export 10 | ## 11 | class CSVExport 12 | include Doing::Util 13 | 14 | def self.settings 15 | { 16 | trigger: 'csv' 17 | } 18 | end 19 | 20 | def self.render(wwid, items, variables: {}) 21 | return if items.nil? 22 | 23 | opt = variables[:options] 24 | 25 | output = [CSV.generate_line(%w[start end title note timer section])] 26 | items.each do |i| 27 | note = format_note(i.note) 28 | end_date = i.end_date 29 | interval = end_date && opt[:times] ? wwid.get_interval(i, formatted: false) : 0 30 | output.push(CSV.generate_line([i.date, end_date, i.title, note, interval, i.section])) 31 | end 32 | Doing.logger.debug('CSV Export:', "#{items.count} items output to CSV") 33 | output.join('') 34 | end 35 | 36 | def self.format_note(note) 37 | out = '' 38 | if note 39 | arr = note.map(&:strip).delete_if { |e| e =~ /^\s*$/ } 40 | out = arr.join("\n") unless arr.empty? 41 | end 42 | 43 | out 44 | end 45 | 46 | Doing::Plugins.register 'csv', :export, self 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/doing_fzf_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | require 'tempfile' 5 | require 'time' 6 | 7 | require 'helpers/doing-helpers' 8 | require 'test_helper' 9 | 10 | # Tests for done commands 11 | class DoingFZFTest < Test::Unit::TestCase 12 | include DoingHelpers 13 | def setup 14 | @tmpdirs = [] 15 | @basedir = mktmpdir 16 | @wwid_file = File.join(@basedir, 'wwid.md') 17 | @backup_dir = File.join(@basedir, 'doing_backup') 18 | @config_file = File.join(File.dirname(__FILE__), 'test.doingrc') 19 | end 20 | 21 | def teardown 22 | FileUtils.rm_rf(@tmpdirs) 23 | end 24 | 25 | def test_fzf_install 26 | res = doing('--stdout', 'install_fzf', '--reinstall') 27 | assert_match(/fzf: installed to/, res, 'Should show successful install message') 28 | end 29 | 30 | def test_fzf_uninstall 31 | doing('--stdout', 'install_fzf', '--reinstall') 32 | res = doing('--stdout', 'install_fzf', '--uninstall') 33 | assert_match(/fzf: removed/, res, 'Should show successful uninstall message') 34 | end 35 | 36 | private 37 | 38 | def mktmpdir 39 | tmpdir = Dir.mktmpdir 40 | @tmpdirs.push(tmpdir) 41 | 42 | tmpdir 43 | end 44 | 45 | def doing(*args) 46 | doing_with_env({ 'DOING_CONFIG' => @config_file, 'DOING_BACKUP_DIR' => @backup_dir }, '--doing_file', @wwid_file, 47 | *args) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/doing/array/tags.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | ## 5 | ## Array helpers 6 | ## 7 | module ArrayTags 8 | ## 9 | ## Convert an array of @tags to plain strings 10 | ## 11 | ## @return [Array] array of strings without @ symbols 12 | ## 13 | ## @example Convert an array of tags to strings 14 | ## ['@one', '@two', 'three'].to_tags => ['one', 'two', 'three'] 15 | def tags_to_array 16 | map(&:remove_at).map(&:strip) 17 | end 18 | 19 | # Convert array of strings to array of @tags 20 | # 21 | # @return [Array] Array of @tags 22 | # 23 | # @example Convert an array of strings with or without @ symbols 24 | # ['one', '@two', 'three'].to_tags => ['@one', '@two', '@three'] 25 | def to_tags 26 | map(&:add_at) 27 | end 28 | 29 | ## 30 | ## Hightlight @tags in string for console output 31 | ## 32 | ## @param color [String] the color to highlight 33 | ## with 34 | ## 35 | ## @return [Array] Array of highlighted @tags 36 | ## 37 | def highlight_tags(color = 'cyan') 38 | to_tags.map { |t| Doing::Color.send(color.to_sym, t) } 39 | end 40 | 41 | ## 42 | ## Tag array for logging 43 | ## 44 | ## @return [String] Highlighted tag array joined with comma 45 | ## 46 | def log_tags(color = 'cyan') 47 | highlight_tags(color).join(', ') 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/doing_sections_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'fileutils' 4 | require 'tempfile' 5 | require 'time' 6 | require 'yaml' 7 | 8 | require 'helpers/doing-helpers' 9 | require 'test_helper' 10 | 11 | # Tests for entry modifying commands 12 | class DoingSectionTest < Test::Unit::TestCase 13 | include DoingHelpers 14 | ENTRY_REGEX = /^\d{4}-\d\d-\d\d \d\d:\d\d \|/.freeze 15 | 16 | def setup 17 | @tmpdirs = [] 18 | @result = '' 19 | @basedir = mktmpdir 20 | @wwid_file = File.join(@basedir, 'wwid.md') 21 | @backup_dir = File.join(@basedir, 'doing_backup') 22 | @config_file = File.join(File.dirname(__FILE__), 'test.doingrc') 23 | end 24 | 25 | def teardown 26 | FileUtils.rm_rf(@tmpdirs) 27 | end 28 | 29 | def test_sections_commands 30 | section = 'Test Section' 31 | doing('sections', 'add', section) 32 | assert_match(/#{section}/, doing('sections', 'list'), "Sections should contain #{section}") 33 | 34 | doing('--yes', 'sections', 'remove', section) 35 | assert_no_match(/#{section}/, doing('sections', 'list'), "Sections should not contain #{section}") 36 | end 37 | 38 | private 39 | 40 | def mktmpdir 41 | tmpdir = Dir.mktmpdir 42 | @tmpdirs.push(tmpdir) 43 | 44 | tmpdir 45 | end 46 | 47 | def doing(*args) 48 | doing_with_env({ 'DOING_CONFIG' => @config_file, 'DOING_BACKUP_DIR' => @backup_dir }, '--doing_file', @wwid_file, 49 | *args) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/helpers/fzf/BUILD.md: -------------------------------------------------------------------------------- 1 | Building fzf 2 | ============ 3 | 4 | Build instructions 5 | ------------------ 6 | 7 | ### Prerequisites 8 | 9 | - Go 1.13 or above 10 | 11 | ### Using Makefile 12 | 13 | ```sh 14 | # Build fzf binary for your platform in target 15 | make 16 | 17 | # Build fzf binary and copy it to bin directory 18 | make install 19 | 20 | # Build fzf binaries and archives for all platforms using goreleaser 21 | make build 22 | 23 | # Publish GitHub release 24 | make release 25 | ``` 26 | 27 | > :warning: Makefile uses git commands to determine the version and the 28 | > revision information for `fzf --version`. So if you're building fzf from an 29 | > environment where its git information is not available, you have to manually 30 | > set `$FZF_VERSION` and `$FZF_REVISION`. 31 | > 32 | > e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make` 33 | 34 | Third-party libraries used 35 | -------------------------- 36 | 37 | - [mattn/go-runewidth](https://github.com/mattn/go-runewidth) 38 | - Licensed under [MIT](http://mattn.mit-license.org) 39 | - [mattn/go-shellwords](https://github.com/mattn/go-shellwords) 40 | - Licensed under [MIT](http://mattn.mit-license.org) 41 | - [mattn/go-isatty](https://github.com/mattn/go-isatty) 42 | - Licensed under [MIT](http://mattn.mit-license.org) 43 | - [tcell](https://github.com/gdamore/tcell) 44 | - Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE) 45 | 46 | License 47 | ------- 48 | 49 | [MIT](LICENSE) 50 | -------------------------------------------------------------------------------- /test/doing_unit-good_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'helpers/doing-helpers' 4 | require 'test_helper' 5 | 6 | $LOAD_PATH.unshift File.join(__dir__, '..', 'lib') 7 | require 'doing' 8 | # require 'gli' 9 | 10 | # Tests for Item class 11 | class DoingUnitGoodTest < Test::Unit::TestCase 12 | def test_good 13 | good_string = 'has content' 14 | empty_string = '' 15 | nil_string = nil 16 | good_array = %w[has content] 17 | empty_array = [] 18 | true_bool = true 19 | false_bool = false 20 | good_hash = { has: 'content' } 21 | empty_hash = {} 22 | 23 | assert_equal(true, good_string.good?) 24 | assert_equal(true, good_array.good?) 25 | assert_equal(true, good_hash.good?) 26 | assert_equal(true, true_bool.good?) 27 | 28 | assert_not_equal(true, empty_string.good?) 29 | assert_not_equal(true, nil_string.good?) 30 | assert_not_equal(true, empty_array.good?) 31 | assert_not_equal(true, empty_hash.good?) 32 | assert_not_equal(true, false_bool.good?) 33 | end 34 | 35 | def test_truthy 36 | assert_equal(true, 'yes'.truthy?) 37 | assert_equal(true, 'Y'.truthy?) 38 | assert_equal(true, 'true'.truthy?) 39 | assert_equal(true, 'TRUE'.truthy?) 40 | assert_equal(true, '1'.truthy?) 41 | 42 | assert_not_equal(true, 'no'.truthy?) 43 | assert_not_equal(true, 'N'.truthy?) 44 | assert_not_equal(true, 'false'.truthy?) 45 | assert_not_equal(true, 'FALSE'.truthy?) 46 | assert_not_equal(true, '0'.truthy?) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /bin/commands/update.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @@update 4 | 5 | desc 'Update doing to the latest version' 6 | long_desc 'Checks for the latest available version of doing and updates your local install if needed.' 7 | command %i[update] do |c| 8 | c.example 'doing update', desc: 'Update to the latest version' 9 | 10 | c.desc 'Check for pre-release version' 11 | c.switch %i[p pre beta], negatable: false, default_value: false 12 | 13 | c.action do |_global_options, options, _args| 14 | my_version = `doing -v`.match(/doing version (?[\d.]+)(?:\.?pre[,)])?/)['v'] 15 | latest_version = if options[:beta] 16 | `gem search doing --pre`.match(/^doing \((?[\d.]+)\.?pre[,)]/)['v'] 17 | else 18 | `gem search doing`.match(/^doing \((?[\d.]+)\)/)['v'] 19 | end 20 | my_version = Doing::Version.new(my_version) 21 | latest_version = Doing::Version.new(latest_version) 22 | 23 | outdated = my_version.compare(latest_version, :older) 24 | 25 | if outdated 26 | pre = options[:beta] ? '--pre' : '' 27 | res = `gem install doing #{pre} 2> /dev/null` 28 | res ||= `sudo gem install doing #{pre}` 29 | ver = res.match(/doing-(?[\d.]+)\n/)['v'] 30 | if ver 31 | Doing.logger.info("Version #{ver} installed") 32 | else 33 | Doing.logger.error('Error installing latest version') 34 | end 35 | else 36 | Doing.logger.info("You have the latest version (#{my_version}) installed") 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/reader_test.go: -------------------------------------------------------------------------------- 1 | package fzf 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/junegunn/fzf/src/util" 8 | ) 9 | 10 | func TestReadFromCommand(t *testing.T) { 11 | strs := []string{} 12 | eb := util.NewEventBox() 13 | reader := NewReader( 14 | func(s []byte) bool { strs = append(strs, string(s)); return true }, 15 | eb, false, true) 16 | 17 | reader.startEventPoller() 18 | 19 | // Check EventBox 20 | if eb.Peek(EvtReadNew) { 21 | t.Error("EvtReadNew should not be set yet") 22 | } 23 | 24 | // Normal command 25 | reader.fin(reader.readFromCommand(nil, `echo abc&&echo def`)) 26 | if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" { 27 | t.Errorf("%s", strs) 28 | } 29 | 30 | // Check EventBox again 31 | eb.WaitFor(EvtReadFin) 32 | 33 | // Wait should return immediately 34 | eb.Wait(func(events *util.Events) { 35 | events.Clear() 36 | }) 37 | 38 | // EventBox is cleared 39 | if eb.Peek(EvtReadNew) { 40 | t.Error("EvtReadNew should not be set yet") 41 | } 42 | 43 | // Make sure that event poller is finished 44 | time.Sleep(readerPollIntervalMax) 45 | 46 | // Restart event poller 47 | reader.startEventPoller() 48 | 49 | // Failing command 50 | reader.fin(reader.readFromCommand(nil, `no-such-command`)) 51 | strs = []string{} 52 | if len(strs) > 0 { 53 | t.Errorf("%s", strs) 54 | } 55 | 56 | // Check EventBox again 57 | if eb.Peek(EvtReadNew) { 58 | t.Error("Command failed. EvtReadNew should not be set") 59 | } 60 | if !eb.Peek(EvtReadFin) { 61 | t.Error("EvtReadFin should be set") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /bin/commands/yesterday.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # @@yesterday 4 | desc 'List entries from yesterday' 5 | long_desc 'Show only entries with start times within the previous 24 hour period. Use --before, --after, and --from to limit to 6 | time spans within the day.' 7 | command :yesterday do |c| 8 | c.example 'doing yesterday', desc: 'List all entries from the previous day' 9 | c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm' 10 | c.example 'doing yesterday --totals', desc: 'List entries from previous day, including tag timers' 11 | 12 | c.desc 'Specify a section' 13 | c.arg_name 'NAME' 14 | c.flag %i[s section], default_value: 'All', multiple: true 15 | 16 | add_options(:output_template, c, default_template: 'today') 17 | add_options(:time_filter, c) 18 | add_options(:time_display, c) 19 | add_options(:save, c) 20 | 21 | c.action do |_global_options, options, _args| 22 | if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export) 23 | raise InvalidPlugin.new('output', 24 | options[:output]) 25 | end 26 | 27 | options[:sort_tags] = options[:tag_sort] 28 | 29 | opt = options.clone 30 | opt[:order] = Doing.setting(['templates', options[:config_template], 'order']) 31 | opt[:yesterday] = true 32 | Doing::Pager.page @wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp 33 | Doing.config.save_view(opt.to_view, options[:save].downcase) if options[:save] 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/doing/types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | module Types 5 | REGEX_CASE = /^[cis].*?$/i.freeze 6 | REGEX_TAG_SORT = /^(?:n(?:ame)?|t(?:ime)?)$/i.freeze 7 | REGEX_BOOL = /^(?:and|all|any|or|not|none|p(?:at(?:tern)?)?)$/i.freeze 8 | REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i.freeze 9 | REGEX_VALUE_QUERY = /^(?:!)?@?(?:\S+) +(?:!?[<>=][=*]?|[$*^]=) +(?:.*?)$/.freeze 10 | REGEX_CLOCK = '(?:\d{1,2}+(?::\d{1,2}+)?(?: *(?:am|pm))?|midnight|noon)' 11 | REGEX_TIME = /^#{REGEX_CLOCK}$/i.freeze 12 | REGEX_DAY = /^(mon|tue|wed|thur?|fri|sat|sun)(\w+(day)?)?$/i.freeze 13 | REGEX_RANGE_INDICATOR = ' +(?:to|through|thru|(?:un)?til|-+) +' 14 | REGEX_RANGE = /^\S+.*?#{REGEX_RANGE_INDICATOR}\S+.*?$/i.freeze 15 | REGEX_TIME_RANGE = /^#{REGEX_CLOCK}(?:#{REGEX_RANGE_INDICATOR}#{REGEX_CLOCK})?$/i.freeze 16 | 17 | InvalidExportType = Class.new(RuntimeError) 18 | MissingConfigFile = Class.new(RuntimeError) 19 | 20 | AgeSymbol = Class.new(String) 21 | BooleanSymbol = Class.new(Symbol) 22 | CaseSymbol = Class.new(Symbol) 23 | DateBeginString = Class.new(DateTime) 24 | DateEndString = Class.new(DateTime) 25 | DateIntervalString = Class.new(DateTime) 26 | DateRangeOptionalString = Class.new(Array) 27 | DateRangeString = Class.new(Array) 28 | ExportTemplate = Class.new(String) 29 | MatchingSymbol = Class.new(Symbol) 30 | OrderSymbol = Class.new(Symbol) 31 | TagArray = Class.new(Array) 32 | TagSortSymbol = Class.new(Symbol) 33 | TemplateName = Class.new(String) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/doing/wwid/tags.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | class WWID 5 | ## 6 | ## List all tags that exist on given items 7 | ## 8 | ## @param items [Array] array of Item 9 | ## @param opt [Hash] additional options 10 | ## @param counts [Boolean] Include tag counts in 11 | ## results 12 | ## 13 | ## @return [Hash] if counts is true, returns a hash with { 14 | ## tag: count }. 15 | ## @return [Array] If counts is false, returns a simple 16 | ## array of tags. 17 | ## 18 | def all_tags(items, opt: {}, counts: false) 19 | if counts 20 | all_tags = {} 21 | items.each do |item| 22 | item.tags.each do |tag| 23 | if all_tags.key?(tag.downcase) 24 | all_tags[tag.downcase] += 1 25 | else 26 | all_tags[tag.downcase] = 1 27 | end 28 | end 29 | end 30 | 31 | all_tags.sort_by { |_, count| count } 32 | else 33 | all_tags = [] 34 | items.each { |item| all_tags.concat(item.tags.map(&:downcase)).uniq! } 35 | all_tags.sort 36 | end 37 | end 38 | 39 | def tag_groups(items, opt: {}) 40 | all_items = filter_items(items, opt: opt) 41 | tags = all_tags(all_items, opt: {}) 42 | groups = {} 43 | tags.each do |tag| 44 | groups[tag] ||= [] 45 | groups[tag] = filter_items(all_items, opt: { tag: tag, tag_bool: :or }) 46 | end 47 | 48 | groups 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/doing/item/state.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | # State queries for a Doing entry 5 | module ItemState 6 | ## 7 | ## Test if item has a @done tag 8 | ## 9 | ## @return [Boolean] true item has @done tag 10 | ## 11 | def finished? 12 | tags?('done') 13 | end 14 | 15 | ## 16 | ## Test if item does not contain @done tag 17 | ## 18 | ## @return [Boolean] true if item is missing @done tag 19 | ## 20 | def unfinished? 21 | tags?('done', negate: true) 22 | end 23 | 24 | ## 25 | ## Test if item is included in never_finish config and 26 | ## thus should not receive a @done tag 27 | ## 28 | ## @return [Boolean] item should receive @done tag 29 | ## 30 | def should_finish? 31 | should?('never_finish') 32 | end 33 | 34 | ## 35 | ## Test if item is included in never_time config and 36 | ## thus should not receive a date on the @done tag 37 | ## 38 | ## @return [Boolean] item should receive @done date 39 | ## 40 | def should_time? 41 | should?('never_time') 42 | end 43 | 44 | private 45 | 46 | def should?(key) 47 | config = Doing.settings 48 | return true unless config[key].is_a?(Array) 49 | 50 | config[key].each do |tag| 51 | next if tag.nil? 52 | 53 | if tag =~ /^@/ 54 | return false if tags?(tag.sub(/^@/, '').downcase) 55 | elsif section&.downcase == tag.downcase 56 | return false 57 | end 58 | end 59 | 60 | true 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/All Activities.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "duration" : "1:06:49", 4 | "endDate" : "2021-07-18T11:57:20Z", 5 | "project" : "Bunch", 6 | "startDate" : "2021-07-18T10:50:30Z" 7 | }, 8 | { 9 | "duration" : "0:24:52", 10 | "endDate" : "2021-07-21T16:47:23Z", 11 | "project" : "Content Creation ▸ Writing", 12 | "startDate" : "2021-07-21T16:22:31Z" 13 | }, 14 | { 15 | "activityTitle" : "Editing Overtired 446", 16 | "duration" : "0:54:23", 17 | "endDate" : "2021-07-22T16:25:22Z", 18 | "notes" : "Second Episode with Christina and Ashley", 19 | "project" : "Podcasting", 20 | "startDate" : "2021-07-22T15:30:59Z" 21 | }, 22 | { 23 | "activityTitle" : "Editing Overtired 446", 24 | "duration" : "0:12:45", 25 | "endDate" : "2021-07-22T16:43:12Z", 26 | "project" : "Podcasting", 27 | "startDate" : "2021-07-22T16:30:27Z" 28 | }, 29 | { 30 | "activityTitle" : "Jeff Severns Guntzel", 31 | "duration" : "0:23:52", 32 | "endDate" : "2021-07-19T21:05:07Z", 33 | "project" : "Communication", 34 | "startDate" : "2021-07-19T20:41:15Z" 35 | }, 36 | { 37 | "duration" : "0:14:17", 38 | "endDate" : "2021-07-18T18:58:39Z", 39 | "project" : "Oracle ▸ Communication", 40 | "startDate" : "2021-07-18T18:44:22Z" 41 | }, 42 | { 43 | "duration" : "0:07:53", 44 | "endDate" : "2021-07-18T18:05:03Z", 45 | "project" : "Office & Business", 46 | "startDate" : "2021-07-18T17:57:10Z" 47 | }, 48 | { 49 | "activityTitle" : "Writing", 50 | "duration" : "0:00:20", 51 | "endDate" : "2021-07-19T10:20:15Z", 52 | "project" : "Oracle ▸ Writing", 53 | "startDate" : "2021-07-19T10:19:55Z" 54 | } 55 | ] 56 | -------------------------------------------------------------------------------- /lib/helpers/threaded_tests_string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ThreadedTestString 4 | class ::String 5 | include Doing::Color 6 | 7 | def highlight_errors 8 | cols = `tput cols`.strip.to_i 9 | 10 | string = dup 11 | 12 | errs = string.scan(/(?<==\n)(?:Failure|Error):.*?(?=\n=+)/m) 13 | 14 | errs.map! do |error| 15 | err = error.dup 16 | 17 | err.gsub!(%r{^(/.*?/)([^/:]+):(\d+):in (.*?)$}) do 18 | m = Regexp.last_match 19 | "#{m[1].white}#{m[2].bold.white}:#{m[3].yellow}:in #{m[4].cyan}" 20 | end 21 | err.gsub!(/(Failure|Error): (.*?)\((.*?)\):\n (.*?)(?=\n)/m) do 22 | m = Regexp.last_match 23 | [ 24 | m[1].bold.boldbgred.white, 25 | m[3].bold.boldbgcyan.white, 26 | m[2].bold.boldbgyellow.black, 27 | " #{m[4]} ".bold.boldbgwhite.black.reset 28 | ].join(':'.boldblack.boldbgblack.reset) 29 | end 30 | err.gsub!(/(<.*?>) (was expected to) (.*?)\n( *<.*?>)./m) do 31 | m = Regexp.last_match 32 | "#{m[1].bold.green} #{m[2].white} #{m[3].boldwhite.boldbgred.reset}\n#{m[4].bold.white}" 33 | end 34 | err.gsub!(/(Finished in) ([\d.]+) (seconds)/) do 35 | m = Regexp.last_match 36 | "#{m[1].green} #{m[2].bold.white} #{m[3].green}" 37 | end 38 | err.gsub!(/(\d+) (failures)/) do 39 | m = Regexp.last_match 40 | "#{m[1].bold.red} #{m[2].red}" 41 | end 42 | err.gsub!(/100% passed/) do |m| 43 | m.bold.green 44 | end 45 | 46 | err 47 | end 48 | 49 | errs.join("\n#{('=' * cols).blue}\n") 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /docs/doc/file_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | File List 19 | 20 | 21 | 22 |
23 |
24 |

File List

25 |
26 | 27 | 28 | Classes 29 | 30 | 31 | 32 | Methods 33 | 34 | 35 | 36 | Files 37 | 38 | 39 |
40 | 41 | 45 |
46 | 47 |
    48 | 49 | 50 |
  • 51 | 52 |
  • 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /test/plugins/test_export.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | class TestExport 5 | def self.settings 6 | { 7 | trigger: 'trizzer', 8 | templates: [ 9 | { name: 'trizzer', trigger: 'tr(.*?)' } 10 | ], 11 | config: { 12 | 'trizzle' => 'test value' 13 | } 14 | } 15 | end 16 | 17 | def self.template(trigger) 18 | return unless trigger =~ /^tr/ 19 | 20 | "This was triggered with #{trigger}: %content" 21 | end 22 | 23 | def self.render(wwid, items, variables: {}) 24 | return if items.nil? || items.empty? 25 | 26 | opt = variables[:options] 27 | 28 | i = items[-1] 29 | 30 | if opt[:times] 31 | interval = i.interval 32 | 33 | if interval 34 | took = '. You finished on ' 35 | finished_at = i.end_date 36 | took += finished_at.strftime('%A %B %e at %I:%M%p') 37 | 38 | took += ' and it took' 39 | took += interval.time_string(format: :natural) 40 | end 41 | end 42 | 43 | date = i.date.strftime('%A %B %e at %I:%M%p') 44 | tpl = template('trizzer') 45 | 46 | if wwid.config['export_templates'].key?('trizzer') 47 | cfg_tpl = wwid.config['export_templates']['trizzer'] 48 | tpl = cfg_tpl unless cfg_tpl.nil? || cfg_tpl.empty? 49 | end 50 | content = "TEST PLUGIN. On #{date} you were #{i.title}#{took}" 51 | output = tpl.dup 52 | output.gsub!(/%content/, content) 53 | 54 | value = wwid.config['plugins']['trizzer']['trizzle'] || 'NO CONFIG' 55 | Doing.logger.info("Test export plugin complete. Config value: #{value}") 56 | 57 | output 58 | end 59 | 60 | Doing::Plugins.register 'trizzer', :export, self 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/helpers/fzf/src/history_test.go: -------------------------------------------------------------------------------- 1 | package fzf 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "runtime" 7 | "testing" 8 | ) 9 | 10 | func TestHistory(t *testing.T) { 11 | maxHistory := 50 12 | 13 | // Invalid arguments 14 | var paths []string 15 | if runtime.GOOS == "windows" { 16 | // GOPATH should exist, so we shouldn't be able to override it 17 | paths = []string{os.Getenv("GOPATH")} 18 | } else { 19 | paths = []string{"/etc", "/proc"} 20 | } 21 | 22 | for _, path := range paths { 23 | if _, e := NewHistory(path, maxHistory); e == nil { 24 | t.Error("Error expected for: " + path) 25 | } 26 | } 27 | 28 | f, _ := ioutil.TempFile("", "fzf-history") 29 | f.Close() 30 | 31 | { // Append lines 32 | h, _ := NewHistory(f.Name(), maxHistory) 33 | for i := 0; i < maxHistory+10; i++ { 34 | h.append("foobar") 35 | } 36 | } 37 | { // Read lines 38 | h, _ := NewHistory(f.Name(), maxHistory) 39 | if len(h.lines) != maxHistory+1 { 40 | t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) 41 | } 42 | for i := 0; i < maxHistory; i++ { 43 | if h.lines[i] != "foobar" { 44 | t.Error("Expected: foobar, actual: " + h.lines[i]) 45 | } 46 | } 47 | } 48 | { // Append lines 49 | h, _ := NewHistory(f.Name(), maxHistory) 50 | h.append("barfoo") 51 | h.append("") 52 | h.append("foobarbaz") 53 | } 54 | { // Read lines again 55 | h, _ := NewHistory(f.Name(), maxHistory) 56 | if len(h.lines) != maxHistory+1 { 57 | t.Errorf("Expected: %d, actual: %d\n", maxHistory+1, len(h.lines)) 58 | } 59 | compare := func(idx int, exp string) { 60 | if h.lines[idx] != exp { 61 | t.Errorf("Expected: %s, actual: %s\n", exp, h.lines[idx]) 62 | } 63 | } 64 | compare(maxHistory-3, "foobar") 65 | compare(maxHistory-2, "barfoo") 66 | compare(maxHistory-1, "foobarbaz") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /rdoc_to_mmd.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | input = IO.read('doing.rdoc') 5 | 6 | input.gsub!(/^======= Options/, "###### Options\n\n") 7 | input.gsub!(/^===== Options/, "##### Options\n\n") 8 | input.gsub!(/^===== Commands/, "### Commands\n") 9 | input.gsub!(/^=== Commands/, "## Commands\n") 10 | 11 | input.gsub!(%r{^(?
={4,6}) Command: (?.*?) (? .*?)?\n(?.*?)$}) do
12 |   m = Regexp.last_match
13 |   level = m['pre'].length == 6 ? '####' : '###'
14 |   r = "#{level} #{m['cmd'].sub(/\|(.*?)$/, ' (*or* \1)')}"
15 |   r += " #{m['arg']}" if m['arg']
16 |   r += " {##{m['cmd'].gsub(/\|.*?$/, '')}}" if m['pre'].length == 4
17 |   r += "\n\n"
18 |   "#{r}**#{m['after']}**{:.description}\n"
19 | end
20 | 
21 | input.gsub!(/(?<=\n)={5,7} (.*?)\n+((.|\n)+?)(?=\n=|$)/s) do
22 |   m = Regexp.last_match
23 |   "`#{m[1]}`\n: #{m[2].gsub(/\|/, '\\|')}"
24 | end
25 | 
26 | input.gsub!(/^=== Global Options/, "## Global Options\n")
27 | input.gsub!(/^=== (.*?)\n+(.*?)$/) do
28 |   m = Regexp.last_match
29 |   "`#{m[1]}`\n: #{m[2].gsub(/\|/, '\\|')}"
30 | end
31 | input.gsub!(/^== (.*?) - (.*?)$\n\n(.*?)$/,
32 |             "**\\1: \\2**\n\n*\\3*\n\n## Table of Contents\n{:.no_toc}\n\n* Table of Contents\n{:toc}")
33 | input.gsub!(/^\[(Default Value|Must Match)\] (.*?)$/, ': *\1:* `\2`')
34 | input.gsub!(/\n  (?=\S)/, ' ')
35 | input.gsub!(/^([^:`\n#*](.*?))$/, "\\1\n")
36 | input.gsub!(/\n{3,}/, "\n\n")
37 | input.gsub!(/^(: .*?)\n\n(:.*?)$/, "\\1\n\\2")
38 | input.gsub!(/^\[Default Command\] (.*?)$/, '> **Default Command:** [`\1`](#\1)')
39 | input.gsub!(%r{/Users/ttscoff/scripts/editor.sh}, '$EDITOR')
40 | input.gsub!(%r{/Users/ttscoff}, '~')
41 | puts %(---
42 | layout: page
43 | title: "Doing - All Commands"
44 | comments: false
45 | footer: true
46 | body_id: doingcommands
47 | ---
48 | )
49 | puts input
50 | 


--------------------------------------------------------------------------------
/bin/commands/today.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@today
 4 | desc 'List entries from today'
 5 | long_desc 'List entries from the current day. Use --before, --after, and
 6 | --from to specify time ranges.'
 7 | command :today do |c|
 8 |   c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
 9 |   c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
10 |   c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today'
11 |   c.example 'doing today --output json', desc: 'Output entries from today in JSON format'
12 | 
13 |   c.desc 'Specify a section'
14 |   c.arg_name 'NAME'
15 |   c.flag %i[s section], default_value: 'All', multiple: true
16 | 
17 |   add_options(:output_template, c, default_template: 'today')
18 |   add_options(:time_filter, c)
19 |   add_options(:time_display, c)
20 |   add_options(:save, c)
21 | 
22 |   c.action do |_global_options, options, _args|
23 |     if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
24 |       raise InvalidPlugin.new('output',
25 |                               options[:output])
26 |     end
27 | 
28 |     options[:times] = true if options[:totals]
29 |     options[:sort_tags] = options[:tag_sort]
30 |     filter_options = %i[after before times duration from section sort_tags totals tag_order template config_template
31 |                         only_timed].each_with_object({}) do |k, hsh|
32 |       hsh[k] = options[k]
33 |     end
34 |     filter_options[:today] = true
35 |     Doing::Pager.page @wwid.today(options[:times], options[:output], filter_options).chomp
36 |     filter_options[:title] = options[:title]
37 |     Doing.config.save_view(filter_options.to_view, options[:save].downcase) if options[:save]
38 |   end
39 | end
40 | 


--------------------------------------------------------------------------------
/docker/inputrc:
--------------------------------------------------------------------------------
 1 | "\e[3~": delete-char
 2 | "\ex": 'cd !$ \015ls\015'
 3 | "\ez": 'cd -\015'
 4 | "\e\C-m": '\C-a "$(\C-e|fzf)"\C-a'
 5 | "\e/": '"$(!!|fzf)"\C-a \C-m\C-m'
 6 | # these allow you to use alt+left/right arrow keys
 7 | # to jump the cursor over words
 8 | "\e[1;5C": forward-word
 9 | "\e[1;5D": backward-word
10 | # "\e[D": backward-word
11 | # "\e[C": forward-word
12 | "\ea": menu-complete
13 | # TAB: menu-complete
14 | # "\e[Z": "\e-1\C-i"
15 | 
16 | "\e\C-l": history-and-alias-expand-line
17 | 
18 | # these allow you to start typing a command and
19 | # use the up/down arrow to auto complete from
20 | # commands in your history
21 | "\e[B": history-search-forward
22 | "\e[A": history-search-backward
23 | "\ew": history-search-backward
24 | "\es": history-search-forward
25 | # this lets you hit tab to auto-complete a file or
26 | # directory name ignoring case
27 | set completion-ignore-case On
28 | set mark-symlinked-directories On
29 | set completion-prefix-display-length 2
30 | set bell-style none
31 | # set bell-style visible
32 | set meta-flag on
33 | set convert-meta off
34 | set input-meta on
35 | set output-meta on
36 | set show-all-if-ambiguous on
37 | set show-all-if-unmodified on
38 | set completion-map-case on
39 | set visible-stats on
40 | 
41 | # Do history expansion when space entered?
42 | $if bash
43 | 	Space: magic-space
44 | $endif
45 | 
46 | # Show extra file information when completing, like `ls -F` does
47 | set visible-stats on
48 | 
49 | # Be more intelligent when autocompleting by also looking at the text after
50 | # the cursor. For example, when the current line is "cd ~/src/mozil", and
51 | # the cursor is on the "z", pressing Tab will not autocomplete it to "cd
52 | # ~/src/mozillail", but to "cd ~/src/mozilla". (This is supported by the
53 | # Readline used by Bash 4.)
54 | set skip-completed-text on
55 | 
56 | # Use Alt/Meta + Delete to delete the preceding word
57 | "\e[3;3~": kill-word
58 | 


--------------------------------------------------------------------------------
/lib/doing/items/util.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   class Items < Array
 5 |     # # Create a deep copy of Items
 6 |     # def clone
 7 |     #   Marshal.load(Marshal.dump(self))
 8 |     # end
 9 | 
10 |     def delete(item)
11 |       deleted = nil
12 |       each_with_index do |i, idx|
13 |         if i.equal?(item, match_section: true)
14 |           deleted = delete_at(idx)
15 |           break
16 |         end
17 |       end
18 |       deleted
19 |     end
20 | 
21 |     ##
22 |     ## Get all tags on Items in self
23 |     ##
24 |     ## @return     [Array] array of tags
25 |     ##
26 |     def all_tags
27 |       each_with_object([]) do |entry, tags|
28 |         tags.concat(entry.tags).sort!.uniq!
29 |       end
30 |     end
31 | 
32 |     ##
33 |     ## Return Items containing items that don't exist in
34 |     ## receiver
35 |     ##
36 |     ## @param      items  [Items] Receiver
37 |     ##
38 |     ## @return     [Hash] Hash of added and deleted items
39 |     ##
40 |     def diff(items)
41 |       a = clone
42 |       b = items.clone
43 | 
44 |       a.delete_if do |item|
45 |         if b.include?(item)
46 |           b.delete(item)
47 |           true
48 |         else
49 |           false
50 |         end
51 |       end
52 |       { added: b, deleted: a }
53 |     end
54 | 
55 |     ##
56 |     ## Remove duplicated entries. Duplicate entries must have matching start date, title, note, and section
57 |     ##
58 |     ## @return     [Items] Items array with duplicate entries removed
59 |     ##
60 |     def dedup(match_section: true)
61 |       unique = Items.new
62 |       each do |item|
63 |         unique.push(item) unless unique.include?(item, match_section: match_section)
64 |       end
65 | 
66 |       unique
67 |     end
68 | 
69 |     # @see #dedup
70 |     def dedup!(match_section: true)
71 |       replace dedup(match_section: match_section)
72 |     end
73 |   end
74 | end
75 | 


--------------------------------------------------------------------------------
/bin/commands/undo.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@undo
 4 | desc 'Undo the last X changes to the Doing file'
 5 | long_desc 'Reverts the last X commands that altered the doing file.
 6 | All changes performed by a single command are undone at once.
 7 | 
 8 | Specify a number to jump back multiple revisions, or use --select for an interactive menu.'
 9 | arg_name 'COUNT'
10 | command :undo do |c|
11 |   c.example 'doing undo', desc: 'Undo the most recent change to the doing file'
12 |   c.example 'doing undo 5', desc: 'Undo the last 5 changes to the doing file'
13 |   c.example 'doing undo --interactive', desc: 'Select from a menu of available revisions'
14 |   c.example 'doing undo --redo', desc: 'Undo the last undo command'
15 | 
16 |   c.desc 'Specify alternate doing file'
17 |   c.arg_name 'PATH'
18 |   c.flag %i[f file], default_value: @wwid.doing_file
19 | 
20 |   c.desc 'Select from recent backups'
21 |   c.switch %i[i interactive], negatable: false
22 | 
23 |   c.desc 'Remove old backups, retaining X files'
24 |   c.arg_name 'COUNT'
25 |   c.flag %i[p prune], type: Integer
26 | 
27 |   c.desc 'Redo last undo. Note: you cannot undo a redo'
28 |   c.switch %i[r redo]
29 | 
30 |   c.action do |_global_options, options, args|
31 |     file = options[:file] || @wwid.doing_file
32 |     count = args.empty? ? 1 : args[0].to_i
33 |     raise InvalidArgument, 'Invalid count specified for undo' unless count&.positive?
34 | 
35 |     if options[:prune]
36 |       Doing::Util::Backup.prune_backups(file, options[:prune])
37 |     elsif options[:redo]
38 |       if options[:interactive]
39 |         Doing::Util::Backup.select_redo(file)
40 |       else
41 |         Doing::Util::Backup.redo_backup(file, count: count)
42 |       end
43 |     elsif options[:interactive]
44 |       Doing::Util::Backup.select_backup(file)
45 |     else
46 |       Doing::Util::Backup.restore_last_backup(file, count: count)
47 |     end
48 |   end
49 | end
50 | 


--------------------------------------------------------------------------------
/lib/doing/prompt/yn.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   # Request Yes/No answers on command line
 5 |   module PromptYN
 6 |     ##
 7 |     ## Ask a yes or no question in the terminal
 8 |     ##
 9 |     ## @param      question          [String] The question
10 |     ##                               to ask
11 |     ## @param      default_response  [Boolean]   default
12 |     ##                               response if no input
13 |     ##
14 |     ## @return     [Boolean] yes or no
15 |     ##
16 |     def yn(question, default_response: false)
17 |       return @force_answer == :yes unless @force_answer.nil?
18 | 
19 |       $stdin.reopen('/dev/tty')
20 | 
21 |       default = if default_response.is_a?(String)
22 |                   default_response =~ /y/i ? true : false
23 |                 else
24 |                   default_response
25 |                 end
26 | 
27 |       # if global --default is set, answer default
28 |       return default if @default_answer
29 | 
30 |       # if this isn't an interactive shell, answer default
31 |       return default unless $stdout.isatty
32 | 
33 |       # clear the buffer
34 |       ARGV.length&.times do
35 |         ARGV.shift
36 |       end
37 |       system 'stty cbreak'
38 | 
39 |       cw = Color.white
40 |       cbw = Color.boldwhite
41 |       cbg = Color.boldgreen
42 |       cd = Color.default
43 | 
44 |       options = if default.nil?
45 |                   "#{cw}[#{cbw}y#{cw}/#{cbw}n#{cw}]#{cd}"
46 |                 else
47 |                   "#{cw}[#{default ? "#{cbg}Y#{cw}/#{cbw}n" : "#{cbw}y#{cw}/#{cbg}N"}#{cw}]#{cd}"
48 |                 end
49 |       $stdout.syswrite "#{cbw}#{question.sub(/\?$/, '')} #{options}#{cbw}?#{cd} "
50 |       res = $stdin.sysread 1
51 |       puts
52 |       system 'stty cooked'
53 | 
54 |       res.chomp!
55 |       res.downcase!
56 | 
57 |       return default if res.empty?
58 | 
59 |       res =~ /y/i ? true : false
60 |     end
61 |   end
62 | end
63 | 


--------------------------------------------------------------------------------
/lib/examples/plugins/hooks.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   # Hooks.register :post_config do |wwid|
 5 |   #   wwid.config['twizzle'] = 'Fo shizzle'
 6 |   #   wwid.write_config(File.expand_path('~/Desktop/wwidconfig.yml'))
 7 |   # end
 8 | 
 9 |   # Hooks.register :post_read, priority: 10 do |wwid|
10 |   #   Doing.logger.warn('Hook 1:', 'triggered priority 10')
11 |   #   Doing.logger.warn('Hook 2:', wwid.config['twizzle'])
12 |   # end
13 | 
14 |   # Hooks.register :post_read, priority: 100 do |wwid|
15 |   #   Doing.logger.warn('Hook 2:', 'triggered priority 100')
16 |   # end
17 | 
18 |   Hooks.register :post_write do |filename|
19 |     res = `/bin/bash /Users/ttscoff/scripts/after_doing.sh`.strip
20 |     Doing.logger.debug('Hooks:', res) unless res =~ /^\.\.\.$/
21 | 
22 |     wwid = WWID.new
23 |     wwid.configure
24 |     if filename == wwid.config['doing_file']
25 |       diff = wwid.get_diff(filename)
26 |       puts diff
27 |     end
28 |   end
29 | 
30 |   Hooks.register :post_entry_added do |wwid, entry|
31 |     if wwid.config.key?('day_one_trigger') && entry.tags?(wwid.config['day_one_trigger'], :and)
32 | 
33 |       logger.info('New entry:', 'Adding to Day One')
34 |       add_to_day_one(entry, wwid.config)
35 |     end
36 |   end
37 | 
38 |   ##
39 |   ## Add the entry to Day One using the CLI
40 |   ##
41 |   ## @param      entry  The entry to add
42 |   ##
43 |   def self.add_to_day_one(entry, config)
44 |     dayone = TTY::Which.which('dayone2')
45 |     flagged = entry.tags?('flagged') ? ' -s' : ''
46 |     tags = entry.tags.map { |t| Shellwords.escape(t) }.join(' ')
47 |     tags = tags.length.positive? ? " -t #{tags}" : ''
48 |     date = " -d '#{entry.date.strftime('%Y-%m-%d %H:%M:%S')}'"
49 |     title = entry.title.tag(config['day_one_trigger'], remove: true)
50 |     title += "\n#{entry.note}" unless entry.note.empty?
51 |     `echo #{Shellwords.escape(title)} | #{dayone} new#{flagged}#{date}#{tags}`
52 |   end
53 | end
54 | 


--------------------------------------------------------------------------------
/test/doing_last_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'fileutils'
 4 | require 'tempfile'
 5 | require 'time'
 6 | require 'yaml'
 7 | 
 8 | require 'helpers/doing-helpers'
 9 | require 'test_helper'
10 | 
11 | # Tests for entry modifying commands
12 | class DoingLastTest < Test::Unit::TestCase
13 |   include DoingHelpers
14 | 
15 |   def setup
16 |     @tmpdirs = []
17 |     @result = ''
18 |     @basedir = mktmpdir
19 |     @wwid_file = File.join(@basedir, 'wwid.md')
20 |     @backup_dir = File.join(@basedir, 'doing_backup')
21 |     @config_file = File.join(File.dirname(__FILE__), 'test.doingrc')
22 |     @import_file = File.join(File.dirname(__FILE__), 'All Activities 2.json')
23 |   end
24 | 
25 |   def teardown
26 |     FileUtils.rm_rf(@tmpdirs)
27 |   end
28 | 
29 |   def test_last_command
30 |     subject = 'Test new entry @tag1'
31 |     doing('import', '--type', 'timing', @import_file)
32 |     doing('now', subject)
33 |     assert_match(/#{subject} \(at .*?\)\s*$/, doing('last'), 'last entry should be entry just added')
34 |   end
35 | 
36 |   def test_last_search_and_tag
37 |     unique_keyword = 'jumping jesus'
38 |     unique_tag = 'balloonpants'
39 |     doing('now', "Test new entry @#{unique_tag} sad monkey")
40 |     doing('now', "Test new entry @tag2 #{unique_keyword}")
41 |     doing('now', 'Test new entry @tag3 burly man')
42 | 
43 |     assert_match(/#{unique_keyword}/, doing('last', '--search', unique_keyword),
44 |                  'returned entry should contain unique keyword')
45 |     assert_match(/@#{unique_tag}/, doing('last', '--tag', unique_tag), 'returned entry should contain unique tag')
46 |   end
47 | 
48 |   private
49 | 
50 |   def mktmpdir
51 |     tmpdir = Dir.mktmpdir
52 |     @tmpdirs.push(tmpdir)
53 | 
54 |     tmpdir
55 |   end
56 | 
57 |   def doing(*args)
58 |     doing_with_env({ 'DOING_CONFIG' => @config_file, 'DOING_BACKUP_DIR' => @backup_dir }, '--doing_file', @wwid_file,
59 |                    *args)
60 |   end
61 | end
62 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | html
 2 | .rake_tasks~
 3 | .sass-cache
 4 | helpfile.txt
 5 | .doingrc
 6 | .irb-history
 7 | buildnotes.md
 8 | .tool-versions
 9 | wwid.md
10 | COMMANDS.bak
11 | .gitsecret/keys/random_seed
12 | !*.secret
13 | lib/helpers/fzf/bin/fzf
14 | test/doing_test_backup/
15 | results.log
16 | 
17 | # Created by https://www.toptal.com/developers/gitignore/api/ruby
18 | # Edit at https://www.toptal.com/developers/gitignore?templates=ruby
19 | 
20 | ### Ruby ###
21 | *.gem
22 | *.rbc
23 | /.config
24 | /coverage/
25 | /InstalledFiles
26 | /pkg/
27 | /spec/reports/
28 | /spec/examples.txt
29 | /test/tmp/
30 | /test/version_tmp/
31 | /tmp/
32 | 
33 | # Used by dotenv library to load environment variables.
34 | # .env
35 | 
36 | # Ignore Byebug command history file.
37 | .byebug_history
38 | 
39 | ## Specific to RubyMotion:
40 | .dat*
41 | .repl_history
42 | build/
43 | *.bridgesupport
44 | build-iPhoneOS/
45 | build-iPhoneSimulator/
46 | 
47 | ## Specific to RubyMotion (use of CocoaPods):
48 | #
49 | # We recommend against adding the Pods directory to your .gitignore. However
50 | # you should judge for yourself, the pros and cons are mentioned at:
51 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
52 | # vendor/Pods/
53 | 
54 | ## Documentation cache and generated files:
55 | /.yardoc/
56 | /_yardoc/
57 | /doc/
58 | /rdoc/
59 | 
60 | ## Environment normalization:
61 | /.bundle/
62 | /vendor/bundle
63 | /lib/bundler/man/
64 | 
65 | # for a library or gem, you might want to ignore these files since the code is
66 | # intended to run in multiple environments; otherwise, check them in:
67 | # Gemfile.lock
68 | # .ruby-version
69 | # .ruby-gemset
70 | 
71 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
72 | .rvmrc
73 | 
74 | # Used by RuboCop. Remote config files pulled in from inherit_from directive.
75 | # .rubocop-https?--*
76 | 
77 | # End of https://www.toptal.com/developers/gitignore/api/ruby
78 | .tags
79 | 


--------------------------------------------------------------------------------
/lib/doing/string/truncate.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   ##
 5 |   ## String truncation
 6 |   ##
 7 |   module StringTruncate
 8 |     ##
 9 |     ## Truncate to nearest word
10 |     ##
11 |     ## @param      len   The length
12 |     ##
13 |     def trunc(len, ellipsis: '...')
14 |       return self if length <= len
15 | 
16 |       total = 0
17 |       res = []
18 | 
19 |       split(/ /).each do |word|
20 |         break if total + 1 + word.length > len
21 | 
22 |         total += 1 + word.length
23 |         res.push(word)
24 |       end
25 |       res.join(' ') + ellipsis
26 |     end
27 | 
28 |     def trunc!(len, ellipsis: '...')
29 |       replace trunc(len, ellipsis: ellipsis)
30 |     end
31 | 
32 |     ##
33 |     ## Truncate from middle to end at nearest word
34 |     ##
35 |     ## @param      len   The length
36 |     ##
37 |     def truncend(len, ellipsis: '...')
38 |       return self if length <= len
39 | 
40 |       total = 0
41 |       res = []
42 | 
43 |       split(/ /).reverse.each do |word|
44 |         break if total + 1 + word.length > len
45 | 
46 |         total += 1 + word.length
47 |         res.unshift(word)
48 |       end
49 |       ellipsis + res.join(' ')
50 |     end
51 | 
52 |     def truncend!(len, ellipsis: '...')
53 |       replace truncend(len, ellipsis: ellipsis)
54 |     end
55 | 
56 |     ##
57 |     ## Truncate string in the middle, separating at nearest word
58 |     ##
59 |     ## @param      len       The length
60 |     ## @param      ellipsis  The ellipsis
61 |     ##
62 |     def truncmiddle(len, ellipsis: '...')
63 |       return self if length <= len
64 | 
65 |       len -= (ellipsis.length / 2).to_i
66 |       half = (len / 2).to_i
67 |       start = trunc(half, ellipsis: ellipsis)
68 |       finish = truncend(half, ellipsis: '')
69 |       start + finish
70 |     end
71 | 
72 |     def truncmiddle!(len, ellipsis: '...')
73 |       replace truncmiddle(len, ellipsis: ellipsis)
74 |     end
75 |   end
76 | end
77 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/src/cache.go:
--------------------------------------------------------------------------------
 1 | package fzf
 2 | 
 3 | import "sync"
 4 | 
 5 | // queryCache associates strings to lists of items
 6 | type queryCache map[string][]Result
 7 | 
 8 | // ChunkCache associates Chunk and query string to lists of items
 9 | type ChunkCache struct {
10 | 	mutex sync.Mutex
11 | 	cache map[*Chunk]*queryCache
12 | }
13 | 
14 | // NewChunkCache returns a new ChunkCache
15 | func NewChunkCache() ChunkCache {
16 | 	return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
17 | }
18 | 
19 | // Add adds the list to the cache
20 | func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
21 | 	if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
22 | 		return
23 | 	}
24 | 
25 | 	cc.mutex.Lock()
26 | 	defer cc.mutex.Unlock()
27 | 
28 | 	qc, ok := cc.cache[chunk]
29 | 	if !ok {
30 | 		cc.cache[chunk] = &queryCache{}
31 | 		qc = cc.cache[chunk]
32 | 	}
33 | 	(*qc)[key] = list
34 | }
35 | 
36 | // Lookup is called to lookup ChunkCache
37 | func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result {
38 | 	if len(key) == 0 || !chunk.IsFull() {
39 | 		return nil
40 | 	}
41 | 
42 | 	cc.mutex.Lock()
43 | 	defer cc.mutex.Unlock()
44 | 
45 | 	qc, ok := cc.cache[chunk]
46 | 	if ok {
47 | 		list, ok := (*qc)[key]
48 | 		if ok {
49 | 			return list
50 | 		}
51 | 	}
52 | 	return nil
53 | }
54 | 
55 | func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
56 | 	if len(key) == 0 || !chunk.IsFull() {
57 | 		return nil
58 | 	}
59 | 
60 | 	cc.mutex.Lock()
61 | 	defer cc.mutex.Unlock()
62 | 
63 | 	qc, ok := cc.cache[chunk]
64 | 	if !ok {
65 | 		return nil
66 | 	}
67 | 
68 | 	for idx := 1; idx < len(key); idx++ {
69 | 		// [---------| ] | [ |---------]
70 | 		// [--------|  ] | [  |--------]
71 | 		// [-------|   ] | [   |-------]
72 | 		prefix := key[:len(key)-idx]
73 | 		suffix := key[idx:]
74 | 		for _, substr := range [2]string{prefix, suffix} {
75 | 			if cached, found := (*qc)[substr]; found {
76 | 				return cached
77 | 			}
78 | 		}
79 | 	}
80 | 	return nil
81 | }
82 | 


--------------------------------------------------------------------------------
/test/doing_unit-note_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'helpers/doing-helpers'
 4 | require 'test_helper'
 5 | 
 6 | $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
 7 | require 'doing'
 8 | # require 'gli'
 9 | 
10 | # Tests for Item class
11 | class DoingUnitNoteTest < Test::Unit::TestCase
12 |   include Doing
13 | 
14 |   def test_note_with_string
15 |     note = ['This is a test note', 'With a couple lines', '', '  whitespace  ']
16 |     new_note = Note.new(note.join("\n"))
17 |     assert_equal(3, new_note.count, 'Note should have 3 lines')
18 |   end
19 | 
20 |   def test_note_with_array
21 |     note = ['This is a test note', 'With a couple lines', '', '  whitespace  ']
22 |     new_note = Note.new(note)
23 |     assert_equal(3, new_note.count, 'Note should have 3 lines')
24 |   end
25 | 
26 |   def test_note_append
27 |     note = ['This is a test note', 'With a couple lines', '', '  whitespace  ']
28 |     new_note = Note.new(note)
29 |     new_note.add('This is another line')
30 |     assert_equal(4, new_note.count, 'Note should have 4 lines')
31 |     new_note.add(['This is an array', 'With two elements'])
32 |     assert_equal(6, new_note.count, 'Note should have 6 lines')
33 |   end
34 | 
35 |   def test_note_replace
36 |     note = ['This is a test note', 'With a couple lines', '', '  whitespace  ']
37 |     new_note = Note.new(note)
38 |     new_note.add('This is one line', replace: true)
39 |     assert_equal(1, new_note.count, 'Note should have 1 lines')
40 |   end
41 | 
42 |   def test_note_compare
43 |     note1 = ['This is a test note', 'With a couple lines', '', '  whitespace  ']
44 |     note2 = ['This is a test note', 'With a different couple lines', '', '  whitespace  ']
45 |     new_note = Note.new(note1)
46 |     note_copy = Note.new(note1)
47 |     other_note = Note.new(note2)
48 |     assert_equal(true, new_note.equal?(note_copy), 'Notes should be the same')
49 |     assert_not_equal(true, new_note.equal?(other_note), 'Notes should be different')
50 |   end
51 | end
52 | 


--------------------------------------------------------------------------------
/test/helpers/doing-helpers.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'open3'
 4 | require 'time'
 5 | $LOAD_PATH.unshift File.join(__dir__, '..', '..', 'lib')
 6 | require 'doing/colors'
 7 | require 'doing/string/string'
 8 | require 'doing/errors'
 9 | 
10 | module DoingHelpers
11 |   DOING_EXEC = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'doing')
12 |   TEST_CONFIG = File.join(File.dirname(__FILE__), '..', 'test.doingrc')
13 | 
14 |   def trunc_minutes(ts)
15 |     ts.to_i / 60 * 60
16 |   end
17 | 
18 |   def doing_with_env(env, *args, stdin: nil)
19 |     pread(env, DOING_EXEC, *args, stdin: stdin)
20 |   end
21 | 
22 |   def pread(env, *cmd, stdin: nil)
23 |     out, err, status = Open3.capture3(env, *cmd, stdin_data: stdin)
24 |     unless status.success?
25 |       raise [
26 |         "Error (#{status}): #{cmd.inspect} failed", 'STDOUT:', out.inspect, 'STDERR:', err.inspect
27 |       ].join("\n")
28 |     end
29 | 
30 |     out
31 |   end
32 | 
33 |   def assert_valid_file(file)
34 |     contents = IO.read(file)
35 |     assert_no_match(/\e\[(?:(?:[349]|10)[0-7]|[0-9])?m/, contents, 'File should not contain any escape codes')
36 |   end
37 | 
38 |   def assert_count_entries(count, shown, message = 'Should be X entries shown')
39 |     assert_equal(count, shown.uncolor.strip.scan(/^\d{4}-\d\d-\d\d \d\d:\d\d \|/).count, message)
40 |   end
41 | 
42 |   def get_start_date(string)
43 |     date_str = string.uncolor.strip.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}) *\|/)
44 | 
45 |     return false unless date_str
46 | 
47 |     Time.parse(date_str[1])
48 |   end
49 | 
50 |   ##
51 |   ## Time helpers
52 |   ##
53 |   class ::Time
54 |     def round_time(min = 1)
55 |       t = self
56 |       Time.at(t.to_i - (t.to_i % (min * 60)))
57 |     end
58 | 
59 |     def close_enough?(other_time, tolerance: 2)
60 |       t = self
61 |       diff = if t > other_time
62 |                t - other_time
63 |              else
64 |                other_time - t
65 |              end
66 |       diff / 60 < tolerance
67 |     end
68 |   end
69 | end
70 | 


--------------------------------------------------------------------------------
/lib/doing/good.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   # Numeric helpers
 5 |   class ::Numeric
 6 |     # Test of number is positive
 7 |     def good?
 8 |       self >= 0
 9 |     end
10 |   end
11 | 
12 |   # Object helpers
13 |   class ::Object
14 |     ##
15 |     ## Tests if object is nil or empty
16 |     ##
17 |     ## @return     [Boolean] true if object is defined and
18 |     ##             has content
19 |     ##
20 |     def good?
21 |       !nil? && !empty? || false
22 |     end
23 |   end
24 | 
25 |   # Time helpers
26 |   class ::Time
27 |     ##
28 |     ## Tests if object is nil
29 |     ##
30 |     ## @return     [Boolean] true if object is defined and
31 |     ##             has content
32 |     ##
33 |     def good?
34 |       !nil?
35 |     end
36 |   end
37 | 
38 |   # String helpers
39 |   class ::String
40 |     ##
41 |     ## Tests if object is nil or empty
42 |     ##
43 |     ## @return     [Boolean] true if object is defined and
44 |     ##             has content
45 |     ##
46 |     def good?
47 |       !strip.empty?
48 |     end
49 |   end
50 | 
51 |   # Array helpers
52 |   class ::Array
53 |     ##
54 |     ## Tests if object is nil or empty
55 |     ##
56 |     ## @return     [Boolean] true if object is defined and
57 |     ##             has content
58 |     ##
59 |     def good?
60 |       !nil? && !empty?
61 |     end
62 |   end
63 | 
64 |   # Boolean helpers
65 |   class ::FalseClass
66 |     ##
67 |     ## Tests if object is nil or empty
68 |     ##
69 |     ## @return     [Boolean] true if object is defined and
70 |     ##             has content
71 |     ##
72 |     def good?
73 |       false
74 |     end
75 | 
76 |     def normalize_tag_sort
77 |       :time
78 |     end
79 |   end
80 | 
81 |   # Boolean helpers
82 |   class ::TrueClass
83 |     ##
84 |     ## Tests if object is nil or empty
85 |     ##
86 |     ## @return     [Boolean] true if object is defined and
87 |     ##             has content
88 |     ##
89 |     def good?
90 |       true
91 |     end
92 | 
93 |     def normalize_tag_sort
94 |       :name
95 |     end
96 |   end
97 | end
98 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/install.ps1:
--------------------------------------------------------------------------------
 1 | $version="0.28.0"
 2 | 
 3 | $fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
 4 | 
 5 | function check_binary () {
 6 |   Write-Host "  - Checking fzf executable ... " -NoNewline
 7 |   $output=cmd /c $fzf_base\bin\fzf.exe --version 2>&1
 8 |   if (-not $?) {
 9 |     Write-Host "Error: $output"
10 |     $binary_error="Invalid binary"
11 |   } else {
12 |     $output=(-Split $output)[0]
13 |     if ($version -ne $output) {
14 |       Write-Host "$output != $version"
15 |       $binary_error="Invalid version"
16 |     } else {
17 |       Write-Host "$output"
18 |       $binary_error=""
19 |       return 1
20 |     }
21 |   }
22 |   Remove-Item "$fzf_base\bin\fzf.exe"
23 |   return 0
24 | }
25 | 
26 | function download {
27 |   param($file)
28 |   Write-Host "Downloading bin/fzf ..."
29 |   if (Test-Path "$fzf_base\bin\fzf.exe") {
30 |     Write-Host "  - Already exists"
31 |     if (check_binary) {
32 |       return
33 |     }
34 |   }
35 |   if (-not (Test-Path "$fzf_base\bin")) {
36 |     md "$fzf_base\bin"
37 |   }
38 |   if (-not $?) {
39 |     $binary_error="Failed to create bin directory"
40 |     return
41 |   }
42 |   cd "$fzf_base\bin"
43 |   $url="https://github.com/junegunn/fzf/releases/download/$version/$file"
44 |   $temp=$env:TMP + "\fzf.zip"
45 |   [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
46 |   if ($PSVersionTable.PSVersion.Major -ge 3) {
47 |     Invoke-WebRequest -Uri $url -OutFile $temp
48 |   } else {
49 |     (New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp"))
50 |   }
51 |   if ($?) {
52 |     (Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp)
53 |   } else {
54 |     $binary_error="Failed to download with powershell"
55 |   }
56 |   if (-not (Test-Path fzf.exe)) {
57 |     $binary_error="Failed to download $file"
58 |     return
59 |   }
60 |   echo y | icacls $fzf_base\bin\fzf.exe /grant Administrator:F ; check_binary >$null
61 | }
62 | 
63 | download "fzf-$version-windows_amd64.zip"
64 | 
65 | Write-Host 'For more information, see: https://github.com/junegunn/fzf'
66 | 


--------------------------------------------------------------------------------
/bin/commands/again.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@again @@resume
 4 | desc 'Repeat last entry as new entry'
 5 | long_desc 'This command is designed to allow multiple time intervals to be created
 6 |            for an entry by duplicating it with a new start (and end, eventually) time'
 7 | command %i[again resume] do |c|
 8 |   c.example 'doing resume',
 9 |             desc: 'Duplicate the most recent entry with a new start time, removing any @done tag'
10 |   c.example 'doing again',
11 |             desc: 'again is an alias for resume'
12 |   c.example 'doing resume --editor',
13 |             desc: 'Repeat the last entry, opening the new entry in the default editor'
14 |   c.example 'doing resume --tag project1 --in Projects',
15 |             desc: 'Repeat the last entry tagged @project1, creating the new entry in the Projects section'
16 |   c.example 'doing resume --interactive', desc: 'Select the entry to repeat from a menu'
17 | 
18 |   c.desc 'Get last entry from a specific section'
19 |   c.arg_name 'NAME'
20 |   c.flag %i[s section], default_value: 'All', multiple: true
21 | 
22 |   c.desc 'Add new entry to section (default: same section as repeated entry)'
23 |   c.arg_name 'SECTION_NAME'
24 |   c.flag [:in]
25 | 
26 |   c.desc 'Select item to resume from a menu of matching entries'
27 |   c.switch %i[i interactive], negatable: false, default_value: false
28 | 
29 |   add_options(:add_entry, c)
30 |   add_options(:search, c)
31 |   add_options(:tag_filter, c)
32 | 
33 |   c.action do |_global_options, options, _args|
34 |     options[:fuzzy] = false
35 | 
36 |     options[:search] = options[:search].sub(/^'?/, "'") if options[:search] && options[:exact]
37 | 
38 |     if options[:back]
39 |       options[:date] = options[:back]
40 |       raise InvalidTimeExpression, 'Unable to parse date string for --back' if options[:date].nil?
41 | 
42 |     else
43 |       options[:date] = Time.now
44 |     end
45 | 
46 |     note = Doing::Note.new(options[:note])
47 |     note.add(Doing::Prompt.read_lines(prompt: 'Add a note')) if options[:ask]
48 | 
49 |     options[:note] = note
50 |     options[:tag] ||= []
51 |     options[:tag_bool] = options[:bool]
52 | 
53 |     @wwid.repeat_last(options)
54 |   end
55 | end
56 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/src/chunklist.go:
--------------------------------------------------------------------------------
 1 | package fzf
 2 | 
 3 | import "sync"
 4 | 
 5 | // Chunk is a list of Items whose size has the upper limit of chunkSize
 6 | type Chunk struct {
 7 | 	items [chunkSize]Item
 8 | 	count int
 9 | }
10 | 
11 | // ItemBuilder is a closure type that builds Item object from byte array
12 | type ItemBuilder func(*Item, []byte) bool
13 | 
14 | // ChunkList is a list of Chunks
15 | type ChunkList struct {
16 | 	chunks []*Chunk
17 | 	mutex  sync.Mutex
18 | 	trans  ItemBuilder
19 | }
20 | 
21 | // NewChunkList returns a new ChunkList
22 | func NewChunkList(trans ItemBuilder) *ChunkList {
23 | 	return &ChunkList{
24 | 		chunks: []*Chunk{},
25 | 		mutex:  sync.Mutex{},
26 | 		trans:  trans}
27 | }
28 | 
29 | func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
30 | 	if trans(&c.items[c.count], data) {
31 | 		c.count++
32 | 		return true
33 | 	}
34 | 	return false
35 | }
36 | 
37 | // IsFull returns true if the Chunk is full
38 | func (c *Chunk) IsFull() bool {
39 | 	return c.count == chunkSize
40 | }
41 | 
42 | func (cl *ChunkList) lastChunk() *Chunk {
43 | 	return cl.chunks[len(cl.chunks)-1]
44 | }
45 | 
46 | // CountItems returns the total number of Items
47 | func CountItems(cs []*Chunk) int {
48 | 	if len(cs) == 0 {
49 | 		return 0
50 | 	}
51 | 	return chunkSize*(len(cs)-1) + cs[len(cs)-1].count
52 | }
53 | 
54 | // Push adds the item to the list
55 | func (cl *ChunkList) Push(data []byte) bool {
56 | 	cl.mutex.Lock()
57 | 
58 | 	if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
59 | 		cl.chunks = append(cl.chunks, &Chunk{})
60 | 	}
61 | 
62 | 	ret := cl.lastChunk().push(cl.trans, data)
63 | 	cl.mutex.Unlock()
64 | 	return ret
65 | }
66 | 
67 | // Clear clears the data
68 | func (cl *ChunkList) Clear() {
69 | 	cl.mutex.Lock()
70 | 	cl.chunks = nil
71 | 	cl.mutex.Unlock()
72 | }
73 | 
74 | // Snapshot returns immutable snapshot of the ChunkList
75 | func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
76 | 	cl.mutex.Lock()
77 | 
78 | 	ret := make([]*Chunk, len(cl.chunks))
79 | 	copy(ret, cl.chunks)
80 | 
81 | 	// Duplicate the last chunk
82 | 	if cnt := len(ret); cnt > 0 {
83 | 		newChunk := *ret[cnt-1]
84 | 		ret[cnt-1] = &newChunk
85 | 	}
86 | 
87 | 	cl.mutex.Unlock()
88 | 	return ret, CountItems(ret)
89 | }
90 | 


--------------------------------------------------------------------------------
/lib/doing/items/items.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require_relative 'filter'
 4 | require_relative 'modify'
 5 | require_relative 'sections'
 6 | require_relative 'util'
 7 | 
 8 | module Doing
 9 |   # A collection of Item objects
10 |   class Items < Array
11 |     attr_accessor :sections
12 | 
13 |     def initialize
14 |       super
15 |       @sections = []
16 |     end
17 | 
18 |     ##
19 |     ## Test if self includes Item
20 |     ##
21 |     ## @param      item           [Item] The item to search for
22 |     ## @param      match_section  [Boolean] Section must match
23 |     ##
24 |     ## @return     [Boolean] True if Item exists
25 |     ##
26 |     def include?(item, match_section: true)
27 |       includes = false
28 |       each do |other_item|
29 |         if other_item.equal?(item, match_section: match_section)
30 |           includes = true
31 |           break
32 |         end
33 |       end
34 | 
35 |       includes
36 |     end
37 | 
38 |     # Find an item by ID
39 |     #
40 |     # @param      id    The identifier to match
41 |     #
42 |     def find_id(id)
43 |       select { |item| item.id == id }[0]
44 |     end
45 | 
46 |     ##
47 |     ## Return the index for an entry matching ID
48 |     ##
49 |     ## @param      id    The identifier to match
50 |     ##
51 |     def index_for_id(id)
52 |       i = nil
53 |       each_with_index do |item, idx|
54 |         if item.id == id
55 |           i = idx
56 |           break
57 |         end
58 |       end
59 |       i
60 |     end
61 | 
62 |     # Output sections and items in Doing file format
63 |     def to_s
64 |       out = []
65 |       @sections.each do |section|
66 |         out.push(section.original)
67 |         items = in_section(section.title).sort_by { |i| [i.date, i.title] }
68 |         items.reverse! if Doing.setting('doing_file_sort').normalize_order == :desc
69 |         items.each { |item| out.push(item.to_s) }
70 |       end
71 | 
72 |       out.join("\n")
73 |     end
74 | 
75 |     # @private
76 |     def inspect
77 |       sections = @sections.map { |s| "" }.join(', ')
78 |       "#"
79 |     end
80 |   end
81 | end
82 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/src/chunklist_test.go:
--------------------------------------------------------------------------------
 1 | package fzf
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"testing"
 6 | 
 7 | 	"github.com/junegunn/fzf/src/util"
 8 | )
 9 | 
10 | func TestChunkList(t *testing.T) {
11 | 	// FIXME global
12 | 	sortCriteria = []criterion{byScore, byLength}
13 | 
14 | 	cl := NewChunkList(func(item *Item, s []byte) bool {
15 | 		item.text = util.ToChars(s)
16 | 		return true
17 | 	})
18 | 
19 | 	// Snapshot
20 | 	snapshot, count := cl.Snapshot()
21 | 	if len(snapshot) > 0 || count > 0 {
22 | 		t.Error("Snapshot should be empty now")
23 | 	}
24 | 
25 | 	// Add some data
26 | 	cl.Push([]byte("hello"))
27 | 	cl.Push([]byte("world"))
28 | 
29 | 	// Previously created snapshot should remain the same
30 | 	if len(snapshot) > 0 {
31 | 		t.Error("Snapshot should not have changed")
32 | 	}
33 | 
34 | 	// But the new snapshot should contain the added items
35 | 	snapshot, count = cl.Snapshot()
36 | 	if len(snapshot) != 1 && count != 2 {
37 | 		t.Error("Snapshot should not be empty now")
38 | 	}
39 | 
40 | 	// Check the content of the ChunkList
41 | 	chunk1 := snapshot[0]
42 | 	if chunk1.count != 2 {
43 | 		t.Error("Snapshot should contain only two items")
44 | 	}
45 | 	if chunk1.items[0].text.ToString() != "hello" ||
46 | 		chunk1.items[1].text.ToString() != "world" {
47 | 		t.Error("Invalid data")
48 | 	}
49 | 	if chunk1.IsFull() {
50 | 		t.Error("Chunk should not have been marked full yet")
51 | 	}
52 | 
53 | 	// Add more data
54 | 	for i := 0; i < chunkSize*2; i++ {
55 | 		cl.Push([]byte(fmt.Sprintf("item %d", i)))
56 | 	}
57 | 
58 | 	// Previous snapshot should remain the same
59 | 	if len(snapshot) != 1 {
60 | 		t.Error("Snapshot should stay the same")
61 | 	}
62 | 
63 | 	// New snapshot
64 | 	snapshot, count = cl.Snapshot()
65 | 	if len(snapshot) != 3 || !snapshot[0].IsFull() ||
66 | 		!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
67 | 		t.Error("Expected two full chunks and one more chunk")
68 | 	}
69 | 	if snapshot[2].count != 2 {
70 | 		t.Error("Unexpected number of items")
71 | 	}
72 | 
73 | 	cl.Push([]byte("hello"))
74 | 	cl.Push([]byte("world"))
75 | 
76 | 	lastChunkCount := snapshot[len(snapshot)-1].count
77 | 	if lastChunkCount != 2 {
78 | 		t.Error("Unexpected number of items:", lastChunkCount)
79 | 	}
80 | }
81 | 


--------------------------------------------------------------------------------
/bin/commands/since.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @since
 4 | desc 'List entries since a date'
 5 | long_desc %(Date argument can be natural language and are always interpreted as being in the past. "thursday" would be interpreted as "last thursday,"
 6 | and "2d" would be interpreted as "two days ago.")
 7 | arg_name 'DATE_STRING'
 8 | command :since do |c|
 9 |   c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year'
10 |   c.example 'doing since "monday 3pm" --output json',
11 |             desc: 'Show entries since 3pm on Monday of the current week, output in JSON format'
12 | 
13 |   c.desc 'Section'
14 |   c.arg_name 'NAME'
15 |   c.flag %i[s section], default_value: 'All', multiple: true
16 | 
17 |   add_options(:output_template, c)
18 |   add_options(:time_display, c)
19 |   add_options(:tag_filter, c)
20 |   add_options(:search, c)
21 |   add_options(:save, c)
22 |   c.action do |_global_options, options, args|
23 |     if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
24 |       raise DoingRuntimeError,
25 |             %(Invalid output type "#{options[:output]}")
26 |     end
27 | 
28 |     raise MissingArgument, 'Missing date argument' if args.empty?
29 | 
30 |     date_string = args.join(' ')
31 | 
32 |     date_string.sub!(/(day) (\d)/, '\1 at \2')
33 |     date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
34 |     Doing.original_options[:date_begin] = date_string
35 |     start = date_string.chronify(guess: :begin)
36 |     finish = Time.now
37 | 
38 |     raise InvalidTimeExpression, 'Unrecognized date string' unless start
39 | 
40 |     Doing.logger.debug('Interpreter:', "date interpreted as #{start} through the current time")
41 | 
42 |     options[:times] = true if options[:totals]
43 |     options[:sort_tags] = options[:tag_sort]
44 | 
45 |     Doing::Pager.page @wwid.list_date([start, finish], options[:section], options[:times], options[:output],
46 |                                       options).chomp
47 |     if options[:save]
48 |       options[:after] = Doing.original_options[:date_begin] if Doing.original_options[:date_begin].good?
49 |       Doing.config.save_view(options.to_view, options[:save].downcase)
50 |     end
51 |   end
52 | end
53 | 


--------------------------------------------------------------------------------
/bin/commands/template.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@template
 4 | desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
 5 | long_desc %(
 6 |   Templates are printed to STDOUT for piping to a file.
 7 |   Save them and use them in the configuration file under export_templates.
 8 | )
 9 | arg_name 'TYPE', must_match: Doing::Plugins.template_regex
10 | command :template do |c|
11 |   c.example 'doing template haml > ~/styles/my_doing.haml', desc: 'Output the haml template and save it to a file'
12 | 
13 |   c.desc 'List all available templates'
14 |   c.switch %i[l list], negatable: false
15 | 
16 |   c.desc 'List in single column for completion'
17 |   c.switch %i[c column]
18 | 
19 |   c.desc 'Save template to file instead of STDOUT'
20 |   c.switch %i[s save], default_value: false, negatable: false
21 | 
22 |   c.desc 'Save template to alternate location'
23 |   c.arg_name 'DIRECTORY'
24 |   c.flag %i[p path], default_value: File.join(Doing::Util.user_home, '.config', 'doing', 'templates')
25 | 
26 |   c.action do |_global_options, options, args|
27 |     if options[:list] || options[:column]
28 |       if options[:column]
29 |         $stdout.print Doing::Plugins.plugin_templates.join("\n")
30 |       else
31 |         $stdout.puts "Available templates: #{Doing::Plugins.plugin_templates.join(', ')}"
32 |       end
33 |     else
34 | 
35 |       if args.empty?
36 |         type = Doing::Prompt.choose_from(Doing::Plugins.plugin_templates, sorted: false,
37 |                                                                           prompt: 'Select template type > ')
38 |         type.sub!(/ \(.*?\)$/, '').strip!
39 |         options[:save] = Doing::Prompt.yn("Save to #{options[:path]}? (No outputs to STDOUT)", default_response: false)
40 |       else
41 |         type = args[0]
42 |       end
43 | 
44 |       unless type
45 |         raise InvalidPluginType,
46 |               "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`"
47 |       end
48 | 
49 |       if options[:save]
50 |         Doing::Plugins.template_for_trigger(type, save_to: options[:path])
51 |       else
52 |         $stdout.puts Doing::Plugins.template_for_trigger(type, save_to: nil)
53 |       end
54 |     end
55 |   end
56 | end
57 | 


--------------------------------------------------------------------------------
/bin/commands/cancel.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@cancel
 4 | desc 'End last X entries with no time tracked'
 5 | long_desc 'Adds @done tag without datestamp so no elapsed time is recorded.
 6 |            Alias for `doing finish --no-date`'
 7 | arg_name 'COUNT'
 8 | command :cancel do |c|
 9 |   c.example 'doing cancel', desc: 'Cancel the last entry'
10 |   c.example 'doing cancel --tag project1 -u 5', desc: 'Cancel the last 5 unfinished entries containing @project1'
11 | 
12 |   c.desc 'Archive entries'
13 |   c.switch %i[a archive], negatable: false, default_value: false
14 | 
15 |   c.desc 'Section'
16 |   c.arg_name 'NAME'
17 |   c.flag %i[s section], multiple: true
18 | 
19 |   c.desc 'Cancel last entry (or entries) not already marked @done'
20 |   c.switch %i[u unfinished], negatable: false, default_value: false
21 | 
22 |   c.desc 'Select item(s) to cancel from a menu of matching entries'
23 |   c.switch %i[i interactive], negatable: false, default_value: false
24 | 
25 |   add_options(:search, c)
26 |   add_options(:tag_filter, c)
27 | 
28 |   c.action do |_global_options, options, args|
29 |     options[:fuzzy] = false
30 |     options[:section] = if options[:section]
31 |                           @wwid.guess_section(options[:section]) || options[:section].cap_first
32 |                         else
33 |                           Doing.setting('current_section')
34 |                         end
35 | 
36 |     raise InvalidArgument, 'Only one argument allowed' if args.length > 1
37 | 
38 |     unless args.empty? || args[0] =~ /\d+/
39 |       raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)'
40 | 
41 |     end
42 | 
43 |     options[:count] = if options[:interactive]
44 |                         0
45 |                       else
46 |                         args[0] ? args[0].to_i : 1
47 |                       end
48 | 
49 |     options[:search] = options[:search].sub(/^'?/, "'") if options[:search] && options[:exact]
50 | 
51 |     options[:case] = options[:case].normalize_case
52 |     options[:date] = false
53 |     options[:sequential] = false
54 |     options[:tag] ||= []
55 |     options[:tag_bool] = options[:bool].normalize_bool
56 |     options[:tags] = ['done']
57 | 
58 |     @wwid.tag_last(options)
59 |   end
60 | end
61 | 


--------------------------------------------------------------------------------
/bin/commands/open.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@open
 4 | desc 'Open the "doing" file in an editor'
 5 | long_desc "`doing open` defaults to using the editors.doing_file setting
 6 | in #{Doing.config.config_file} (#{Doing::Util.find_default_editor('doing_file')})."
 7 | command :open do |c|
 8 |   c.example 'doing open', desc: 'Open the doing file in the default editor'
 9 |   c.desc 'Open with editor command (e.g. vim, mate)'
10 |   c.arg_name 'COMMAND'
11 |   c.flag %i[e editor]
12 | 
13 |   if `uname` =~ /Darwin/
14 |     c.desc 'Open with app name'
15 |     c.arg_name 'APP_NAME'
16 |     c.flag %i[a app]
17 | 
18 |     c.desc 'Open with app bundle id'
19 |     c.arg_name 'BUNDLE_ID'
20 |     c.flag %i[b bundle_id]
21 |   end
22 | 
23 |   c.action do |_global_options, options, _args|
24 |     params = options.clone
25 |     params.delete_if do |k, v|
26 |       k.instance_of?(String) || v.nil? || v == false
27 |     end
28 | 
29 |     if options[:editor]
30 |       unless Doing::Util.exec_available(options[:editor].split(/ /).first)
31 |         raise MissingEditor,
32 |               "Editor #{options[:editor]} not found"
33 |       end
34 | 
35 |       editor = TTY::Which.which(options[:editor])
36 |       system %(#{editor} "#{File.expand_path(@wwid.doing_file)}")
37 |     elsif `uname` =~ /Darwin/
38 |       if options[:app]
39 |         system %(open -a "#{options[:app]}" "#{File.expand_path(@wwid.doing_file)}")
40 |       elsif options[:bundle_id]
41 |         system %(open -b "#{options[:bundle_id]}" "#{File.expand_path(@wwid.doing_file)}")
42 |       elsif Doing::Util.find_default_editor('doing_file')
43 |         editor = Doing::Util.find_default_editor('doing_file')
44 |         if Doing::Util.exec_available(editor.split(/ /).first)
45 |           system %(#{editor} "#{File.expand_path(@wwid.doing_file)}")
46 |         else
47 |           system %(open -a "#{editor}" "#{File.expand_path(@wwid.doing_file)}")
48 |         end
49 |       else
50 |         system %(open "#{File.expand_path(@wwid.doing_file)}")
51 |       end
52 |     else
53 |       raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
54 | 
55 |       system %(#{Doing::Util.default_editor} "#{File.expand_path(@wwid.doing_file)}")
56 |     end
57 |   end
58 | end
59 | 


--------------------------------------------------------------------------------
/lib/examples/commands/autotag.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # Example command that calls an existing command (tag) with
 4 | # preset options
 5 | desc 'Autotag last entry or filtered entries'
 6 | command :autotag do |c|
 7 |   # Preserve some switches and flags. Values will be passed
 8 |   # to tag command.
 9 |   c.desc 'Section'
10 |   c.arg_name 'SECTION_NAME'
11 |   c.flag %i[s section], default_value: 'All', multiple: true
12 | 
13 |   c.desc 'How many recent entries to autotag (0 for all)'
14 |   c.arg_name 'COUNT'
15 |   c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
16 | 
17 |   c.desc 'Don\'t ask permission to autotag all entries when count is 0'
18 |   c.switch %i[force], negatable: false, default_value: false
19 | 
20 |   c.desc 'Autotag last entry (or entries) not marked @done'
21 |   c.switch %i[u unfinished], negatable: false, default_value: false
22 | 
23 |   c.desc 'Autotag the last X entries containing TAG.
24 |   Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
25 |   c.arg_name 'TAG'
26 |   c.flag [:tag]
27 | 
28 |   c.desc 'Autotag entries matching search filter,
29 |   surround with slashes for regex (e.g. "/query.*/"),
30 |   start with single quote for exact match ("\'query")'
31 |   c.arg_name 'QUERY'
32 |   c.flag [:search]
33 | 
34 |   c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
35 |   c.arg_name 'BOOLEAN'
36 |   c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
37 | 
38 |   c.desc 'Select item(s) to tag from a menu of matching entries'
39 |   c.switch %i[i interactive], negatable: false, default_value: false
40 | 
41 |   c.action do |global, options, _args|
42 |     # Force some switches and flags. We're using the tag
43 |     # command with settings that would invoke autotagging.
44 | 
45 |     # Force enable autotag
46 |     options[:a] = true
47 |     options[:autotag] = true
48 | 
49 |     # No need for date values
50 |     options[:d] = false
51 |     options[:date] = false
52 | 
53 |     # Don't remove any tags
54 |     options[:rename] = nil
55 |     options[:regex] = false
56 |     options[:r] = false
57 |     options[:remove] = false
58 | 
59 |     cmd = commands[:tag]
60 |     action = cmd.send(:get_action, nil)
61 |     action.call(global, options, [])
62 |   end
63 | end
64 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/man/man1/fzf-tmux.1:
--------------------------------------------------------------------------------
 1 | .ig
 2 | The MIT License (MIT)
 3 | 
 4 | Copyright (c) 2013-2021 Junegunn Choi
 5 | 
 6 | Permission is hereby granted, free of charge, to any person obtaining a copy
 7 | of this software and associated documentation files (the "Software"), to deal
 8 | in the Software without restriction, including without limitation the rights
 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 | 
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 | 
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 | ..
24 | .TH fzf-tmux 1 "Nov 2021" "fzf 0.28.0" "fzf-tmux - open fzf in tmux split pane"
25 | 
26 | .SH NAME
27 | fzf-tmux - open fzf in tmux split pane
28 | 
29 | .SH SYNOPSIS
30 | .B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
31 | 
32 | .SH DESCRIPTION
33 | fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
34 | a tmux popup window. It is designed to work just like fzf except that it does
35 | not take up the whole screen. You can safely use fzf-tmux instead of fzf in
36 | your scripts as the extra options will be silently ignored if you're not on
37 | tmux.
38 | 
39 | .SH LAYOUT OPTIONS
40 | 
41 | (default layout: \fB-d 50%\fR)
42 | 
43 | .SS Popup window
44 | (requires tmux 3.2 or above)
45 | .TP
46 | .B "-p [WIDTH[%][,HEIGHT[%]]]"
47 | .TP
48 | .B "-w WIDTH[%]"
49 | .TP
50 | .B "-h WIDTH[%]"
51 | .TP
52 | .B "-x COL"
53 | .TP
54 | .B "-y ROW"
55 | 
56 | .SS Split pane
57 | .TP
58 | .B "-u [height[%]]"
59 | Split above (up)
60 | .TP
61 | .B "-d [height[%]]"
62 | Split below (down)
63 | .TP
64 | .B "-l [width[%]]"
65 | Split left
66 | .TP
67 | .B "-r [width[%]]"
68 | Split right
69 | 


--------------------------------------------------------------------------------
/test/doing_hook_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'fileutils'
 4 | require 'tempfile'
 5 | 
 6 | require 'helpers/doing-helpers'
 7 | require 'test_helper'
 8 | 
 9 | # Tests for archive commands
10 | class DoingHookTest < Test::Unit::TestCase
11 |   include DoingHelpers
12 | 
13 |   def setup
14 |     @tmpdirs = []
15 |     @result = ''
16 |     @basedir = mktmpdir
17 |     @wwid_file = File.join(@basedir, 'wwid.md')
18 |     @backup_dir = File.join(@basedir, 'doing_backup')
19 |     @config_file = File.join(File.dirname(__FILE__), 'test.doingrc')
20 |   end
21 | 
22 |   def teardown
23 |     FileUtils.rm_rf(@tmpdirs)
24 |   end
25 | 
26 |   def test_hook_register
27 |     assert_matches([
28 |                      [/Hook Manager: Registered post_write hook/, 'Should have registered a post_write hook', false],
29 |                      [/Hook Manager: Registered post_read hook/, 'Should have registered a post_read hook', false]
30 |                    ],
31 |                    doing('last'))
32 |   end
33 | 
34 |   def test_read_hook
35 |     assert_matches([[/Post read hook!/, 'Should have triggered post_read hook', false],
36 |                     [/Post write hook!/, 'Should not have triggered post_write hook', true]], doing('last'))
37 |   end
38 | 
39 |   def test_write_hook
40 |     assert_matches([[/Post write hook!/, 'Should have triggered post_write hook', false]],
41 |                    doing('now', 'testing hooks'))
42 |   end
43 | 
44 |   private
45 | 
46 |   def assert_matches(matches, shown)
47 |     matches.each do |regexp, msg, opt_refute|
48 |       if opt_refute
49 |         assert_no_match(regexp, shown, msg)
50 |       else
51 |         assert_match(regexp, shown, msg)
52 |       end
53 |     end
54 |   end
55 | 
56 |   def assert_count_entries(count, shown, message = 'Should be X entries shown')
57 |     assert_equal(count, shown.uncolor.strip.scan(ENTRY_REGEX).count, message)
58 |   end
59 | 
60 |   def mktmpdir
61 |     tmpdir = Dir.mktmpdir
62 |     @tmpdirs.push(tmpdir)
63 | 
64 |     tmpdir
65 |   end
66 | 
67 |   def doing(*args)
68 |     doing_with_env(
69 |       { 'HOOK_TEST' => 'true', 'DOING_PLUGIN_DEBUG' => 'true', 'DOING_CONFIG' => @config_file,
70 |         'DOING_BACKUP_DIR' => @backup_dir }, '--doing_file', @wwid_file, '--stdout', *args
71 |     )
72 |   end
73 | end
74 | 


--------------------------------------------------------------------------------
/lib/doing/string/url.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   ##
 5 |   ## URL linking and formatting
 6 |   ##
 7 |   module StringURL
 8 |     ##
 9 |     ## Turn raw urls into HTML links
10 |     ##
11 |     ## @param      opt   [Hash] Additional Options
12 |     ##
13 |     ## @option opt [Symbol] :format can be :markdown or
14 |     ## :html (default)
15 |     ##
16 |     def link_urls(**opt)
17 |       fmt = opt.fetch(:format, :html)
18 |       return self unless fmt
19 | 
20 |       str = dup
21 | 
22 |       str = str.remove_self_links if fmt == :markdown
23 | 
24 |       str.replace_qualified_urls(format: fmt).clean_unlinked_urls
25 |     end
26 | 
27 |     ## @see #link_urls
28 |     def link_urls!(**opt)
29 |       fmt = opt.fetch(:format, :html)
30 |       replace link_urls(format: fmt)
31 |     end
32 | 
33 |     # Remove  formatting
34 |     def remove_self_links
35 |       gsub(/<(.*?)>/) do |match|
36 |         m = Regexp.last_match
37 |         if m[1] =~ /^https?:/
38 |           m[1]
39 |         else
40 |           match
41 |         end
42 |       end
43 |     end
44 | 
45 |     # Replace qualified urls
46 |     def replace_qualified_urls(**options)
47 |       fmt = options.fetch(:format, :html)
48 |       gsub(%r{(?mi)(?x:
49 |       (?(?:http|https)://)
51 |       (?[\w-]+(?:\.[\w-]+)+)
52 |       (?[\w\-.,@?^=%&;:/~+#]*[\w\-@^=%&;/~+#])?
53 |       )}) do |_match|
54 |         m = Regexp.last_match
55 |         url = "#{m['domain']}#{m['path']}"
56 |         proto = m['protocol'].nil? ? 'http://' : m['protocol']
57 |         case fmt
58 |         when :terminal
59 |           TTY::Link.link_to("#{proto}#{url}", "#{proto}#{url}")
60 |         when :html
61 |           %([#{url}])
62 |         when :markdown
63 |           "[#{url}](#{proto}#{url})"
64 |         else
65 |           m[0]
66 |         end
67 |       end
68 |     end
69 | 
70 |     # Clean up unlinked 
71 |     def clean_unlinked_urls
72 |       gsub(/<(\w+:.*?)>/) do |match|
73 |         m = Regexp.last_match
74 |         if m[1] =~ /[link])
78 |         end
79 |       end
80 |     end
81 |   end
82 | end
83 | 


--------------------------------------------------------------------------------
/test/doing_config_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'fileutils'
 4 | require 'tempfile'
 5 | require 'yaml'
 6 | require 'helpers/doing-helpers'
 7 | require 'test_helper'
 8 | 
 9 | $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
10 | require 'doing'
11 | # require 'gli'
12 | 
13 | # Tests for archive commands
14 | class DoingConfigTest < Test::Unit::TestCase
15 |   include DoingHelpers
16 |   # include GLI::App
17 | 
18 |   def setup
19 |     @tmpdirs = []
20 |     @result = ''
21 |     @basedir = mktmpdir
22 |     @wwid_file = File.join(@basedir, 'wwid.md')
23 |     @temp_config = File.join(@basedir, 'temp.doingrc')
24 |     @config_file = File.join(File.dirname(__FILE__), 'test.doingrc')
25 |     @backup_dir = File.join(@basedir, 'doing_backup')
26 |     @bad_config = File.join(File.dirname(__FILE__), 'bad.doingrc')
27 |   end
28 | 
29 |   def teardown
30 |     FileUtils.rm_rf(@tmpdirs)
31 |   end
32 | 
33 |   def test_missing_config
34 |     res = doing_with_env({ 'DOING_DEBUG' => 'true', 'DOING_CONFIG' => @temp_config }, '--stdout', '--doing_file',
35 |                          @wwid_file)
36 |     assert_match(/Config file written to .*?#{File.basename(@temp_config)}/, res,
37 |                  'Missing config file should have been written')
38 |   end
39 | 
40 |   # def test_bad_config
41 |   #   res = doing_with_env({'DOING_DEBUG' => 'true', 'DOING_CONFIG' => @bad_config}, '--stdout', 'config', '-d', 'doing_file')
42 |   #   assert_match(/Error reading default configuration/, res, 'Non-YAML file should log an error')
43 |   #   assert_match(/what_was_i_doing.md/, res, 'Default config should have been loaded')
44 |   # end
45 | 
46 |   def test_user_config
47 |     user_config = YAML.safe_load(IO.read(@config_file))
48 |     path = %w[plugins say say_voice]
49 |     setting = user_config.dig(*path)
50 |     res = doing('config', 'get', path.join('.').to_s)
51 |     assert_match(/#{setting}/, res, 'Correct config setting should be returned to STDOUT')
52 |   end
53 | 
54 |   private
55 | 
56 |   def mktmpdir
57 |     tmpdir = Dir.mktmpdir
58 |     @tmpdirs.push(tmpdir)
59 | 
60 |     tmpdir
61 |   end
62 | 
63 |   def doing(*args)
64 |     doing_with_env({ 'DOING_DEBUG' => 'true', 'DOING_CONFIG' => @config_file, 'DOING_BACKUP_DIR' => @backup_dir },
65 |                    '--doing_file', @wwid_file, *args)
66 |   end
67 | end
68 | 


--------------------------------------------------------------------------------
/lib/doing/items/filter.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   class Items < Array
 5 |     # Get a new Items object containing only items in a
 6 |     # specified section
 7 |     #
 8 |     # @param      section  [String] section title
 9 |     #
10 |     # @return     [Items] Array of items
11 |     #
12 |     def in_section(section)
13 |       sect = section.is_a?(Section) ? section.title : section
14 |       if sect =~ /^all$/i
15 |         dup
16 |       else
17 |         items = Items.new.concat(select { |item| !item.nil? && item.section == section })
18 |         items.add_section(section, log: false)
19 |         items
20 |       end
21 |     end
22 | 
23 |     ##
24 |     ## Search Items for a string (title and note)
25 |     ##
26 |     ## @param      query      [String] The query
27 |     ## @param      case_type  [Symbol] The case type
28 |     ##                        (:smart, :sensitive, :ignore)
29 |     ##
30 |     ## @return     [Items] array of items matching search
31 |     ##
32 |     def search(query, case_type: :smart)
33 |       WWID.new.fuzzy_filter_items(self, query, case_type: case_type)
34 |     end
35 | 
36 |     ##
37 |     ## Search items by tags
38 |     ##
39 |     ## @param      tags  [Array,String] The tags by which to
40 |     ##                   filter
41 |     ## @param      bool  [Symbol] The bool with which to
42 |     ##                   combine multiple tags
43 |     ##
44 |     ## @return     [Items] array of items matching tag filter
45 |     ##
46 |     def tagged(tags, bool: :and)
47 |       WWID.new.filter_items(self, opt: { tag: tags, bool: bool })
48 |     end
49 | 
50 |     ##
51 |     ## Filter Items by date. String arguments will be
52 |     ## chronified
53 |     ##
54 |     ## @param      start   [Time,String] Filter items after
55 |     ##                     this date
56 |     ## @param      finish  [Time,String] Filter items before
57 |     ##                     this date
58 |     ##
59 |     ## @return     [Items] array of items with dates between
60 |     ##             targets
61 |     ##
62 |     def between_dates(start, finish)
63 |       start = start.chronify(guess: :begin, future: false) if start.is_a?(String)
64 |       finish = finish.chronify(guess: :end) if finish.is_a?(String)
65 |       WWID.new.filter_items(self, opt: { date_filter: [start, finish] })
66 |     end
67 |   end
68 | end
69 | 


--------------------------------------------------------------------------------
/test/doing_reset_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'fileutils'
 4 | require 'tempfile'
 5 | require 'time'
 6 | require 'yaml'
 7 | 
 8 | require 'helpers/doing-helpers'
 9 | require 'test_helper'
10 | 
11 | # Tests for entry modifying commands
12 | class DoingResetTest < Test::Unit::TestCase
13 |   include DoingHelpers
14 |   ENTRY_TS_REGEX = /\s*(?[^|]+) \s*\|/.freeze
15 |   ENTRY_DONE_REGEX = /@done\((?.*?)\)/.freeze
16 | 
17 |   def setup
18 |     @tmpdirs = []
19 |     @result = ''
20 |     @basedir = mktmpdir
21 |     @wwid_file = File.join(@basedir, 'wwid.md')
22 |     @backup_dir = File.join(@basedir, 'doing_backup')
23 |     @config_file = File.join(File.dirname(__FILE__), 'test.doingrc')
24 |     @config = YAML.safe_load(IO.read(@config_file))
25 |   end
26 | 
27 |   def teardown
28 |     FileUtils.rm_rf(@tmpdirs)
29 |   end
30 | 
31 |   def test_reset_entry
32 |     subject = 'Test entry'
33 |     doing('done', subject)
34 |     result = doing('--stdout', '--debug', 'reset')
35 | 
36 |     assert_match(/Reset: Reset and resumed "#{subject}" in #{@config['current_section']}/, result,
37 |                  'Entry should be reset and resumed')
38 |   end
39 | 
40 |   def test_reset_tag
41 |     3.times { |i| doing('done', '--back', "#{i + 5}m", "Entry #{i + 1} with @tag#{i + 1}") }
42 |     result = doing('--stdout', 'reset', '--tag', 'tag2', '--no-resume', '10am')
43 |     assert_match(/Reset: Reset "Entry 2 with @tag2/, result, 'Entry 2 should be reset')
44 | 
45 |     result = doing('show', '@tag2').uncolor.strip
46 | 
47 |     assert_match(/10:00 \|/, result, 'Entry 2 time should be 10am')
48 |     assert_match(ENTRY_DONE_REGEX, result, 'Entry 2 should still be @done')
49 |   end
50 | 
51 |   def test_reset_from
52 |     doing('now', 'Test entry')
53 |     doing('reset', '--from', '8am to 10am')
54 |     result = doing('last').uncolor.strip
55 |     assert_match(/at 8:00am/, result, 'Should have started at 8am')
56 |     assert_match(/@done\(.*?10:00\)/, result, 'Should have @done date of 10am')
57 |   end
58 | 
59 |   private
60 | 
61 |   def mktmpdir
62 |     tmpdir = Dir.mktmpdir
63 |     @tmpdirs.push(tmpdir)
64 | 
65 |     tmpdir
66 |   end
67 | 
68 |   def doing(*args)
69 |     doing_with_env({ 'DOING_CONFIG' => @config_file, 'DOING_BACKUP_DIR' => @backup_dir }, '--doing_file', @wwid_file,
70 |                    *args)
71 |   end
72 | end
73 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/src/merger_test.go:
--------------------------------------------------------------------------------
 1 | package fzf
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"math/rand"
 6 | 	"sort"
 7 | 	"testing"
 8 | 
 9 | 	"github.com/junegunn/fzf/src/util"
10 | )
11 | 
12 | func assert(t *testing.T, cond bool, msg ...string) {
13 | 	if !cond {
14 | 		t.Error(msg)
15 | 	}
16 | }
17 | 
18 | func randResult() Result {
19 | 	str := fmt.Sprintf("%d", rand.Uint32())
20 | 	chars := util.ToChars([]byte(str))
21 | 	chars.Index = rand.Int31()
22 | 	return Result{item: &Item{text: chars}}
23 | }
24 | 
25 | func TestEmptyMerger(t *testing.T) {
26 | 	assert(t, EmptyMerger.Length() == 0, "Not empty")
27 | 	assert(t, EmptyMerger.count == 0, "Invalid count")
28 | 	assert(t, len(EmptyMerger.lists) == 0, "Invalid lists")
29 | 	assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
30 | }
31 | 
32 | func buildLists(partiallySorted bool) ([][]Result, []Result) {
33 | 	numLists := 4
34 | 	lists := make([][]Result, numLists)
35 | 	cnt := 0
36 | 	for i := 0; i < numLists; i++ {
37 | 		numResults := rand.Int() % 20
38 | 		cnt += numResults
39 | 		lists[i] = make([]Result, numResults)
40 | 		for j := 0; j < numResults; j++ {
41 | 			item := randResult()
42 | 			lists[i][j] = item
43 | 		}
44 | 		if partiallySorted {
45 | 			sort.Sort(ByRelevance(lists[i]))
46 | 		}
47 | 	}
48 | 	items := []Result{}
49 | 	for _, list := range lists {
50 | 		items = append(items, list...)
51 | 	}
52 | 	return lists, items
53 | }
54 | 
55 | func TestMergerUnsorted(t *testing.T) {
56 | 	lists, items := buildLists(false)
57 | 	cnt := len(items)
58 | 
59 | 	// Not sorted: same order
60 | 	mg := NewMerger(nil, lists, false, false)
61 | 	assert(t, cnt == mg.Length(), "Invalid Length")
62 | 	for i := 0; i < cnt; i++ {
63 | 		assert(t, items[i] == mg.Get(i), "Invalid Get")
64 | 	}
65 | }
66 | 
67 | func TestMergerSorted(t *testing.T) {
68 | 	lists, items := buildLists(true)
69 | 	cnt := len(items)
70 | 
71 | 	// Sorted sorted order
72 | 	mg := NewMerger(nil, lists, true, false)
73 | 	assert(t, cnt == mg.Length(), "Invalid Length")
74 | 	sort.Sort(ByRelevance(items))
75 | 	for i := 0; i < cnt; i++ {
76 | 		if items[i] != mg.Get(i) {
77 | 			t.Error("Not sorted", items[i], mg.Get(i))
78 | 		}
79 | 	}
80 | 
81 | 	// Inverse order
82 | 	mg2 := NewMerger(nil, lists, true, false)
83 | 	for i := cnt - 1; i >= 0; i-- {
84 | 		if items[i] != mg2.Get(i) {
85 | 			t.Error("Not sorted", items[i], mg2.Get(i))
86 | 		}
87 | 	}
88 | }
89 | 


--------------------------------------------------------------------------------
/test/doing_file_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'fileutils'
 4 | require 'tempfile'
 5 | require 'yaml'
 6 | require 'helpers/doing-helpers'
 7 | require 'test_helper'
 8 | 
 9 | $LOAD_PATH.unshift File.join(__dir__, '..', 'lib')
10 | require 'doing'
11 | # require 'gli'
12 | 
13 | # Tests for archive commands
14 | class DoingFileTest < Test::Unit::TestCase
15 |   include DoingHelpers
16 |   # include GLI::App
17 | 
18 |   def setup
19 |     @tmpdirs = []
20 |     @result = ''
21 |     @basedir = mktmpdir
22 |     @wwid_file = File.join(@basedir, 'wwid.md')
23 |     @temp_config = File.join(@basedir, 'temp.doingrc')
24 |     @config_file = File.join(File.dirname(__FILE__), 'test.doingrc')
25 |     @backup_dir = File.join(@basedir, 'doing_backup')
26 |     @bad_config = File.join(File.dirname(__FILE__), 'bad.doingrc')
27 |   end
28 | 
29 |   def teardown
30 |     FileUtils.rm_rf(@tmpdirs)
31 |   end
32 | 
33 |   def date_order(file)
34 |     content = IO.read(file)
35 |     dates = content.scan(/(?<=- )(?:\d\d\d\d-\d\d-\d\d \d\d:\d\d)(?= |)/)
36 |     t1 = Time.parse(dates[0])
37 |     t2 = Time.parse(dates[-1])
38 |     t1 < t2 ? 'asc' : 'desc'
39 |   end
40 | 
41 |   def test_sort_order
42 |     doing('--yes', 'config', 'set', 'doing_file_sort', 'asc')
43 |     setting = doing('config', 'get', 'doing_file_sort', '-o', 'raw').strip
44 |     assert_match(/^asc/, setting, 'doing_file_sort config should be "asc"')
45 |     3.times { |i| doing('now', '--back', "#{i}h", "Test entry #{i}") }
46 |     assert_equal('asc', date_order(@wwid_file), 'File should be in ascending order')
47 | 
48 |     doing('--yes', 'config', 'set', 'doing_file_sort', 'desc')
49 |     setting = doing('config', 'get', 'doing_file_sort', '-o', 'raw').strip
50 |     assert_match(/^desc/, setting, 'doing_file_sort should be "desc"')
51 |     doing('now', 'Test entry 4')
52 |     assert_equal('desc', date_order(@wwid_file), 'File should be in descending order')
53 | 
54 |     doing('--yes', 'config', 'set', '-r', 'doing_file_sort')
55 |   end
56 | 
57 |   private
58 | 
59 |   def mktmpdir
60 |     tmpdir = Dir.mktmpdir
61 |     @tmpdirs.push(tmpdir)
62 | 
63 |     tmpdir
64 |   end
65 | 
66 |   def doing(*args)
67 |     doing_with_env({ 'DOING_DEBUG' => 'true', 'DOING_CONFIG' => @config_file, 'DOING_BACKUP_DIR' => @backup_dir },
68 |                    '--doing_file', @wwid_file, *args)
69 |   end
70 | end
71 | 


--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
 1 | # For most projects, this workflow file will not need changing; you simply need
 2 | # to commit it to your repository.
 3 | #
 4 | # You may wish to alter this file to override the set of languages analyzed,
 5 | # or to provide custom queries or build logic.
 6 | #
 7 | # ******** NOTE ********
 8 | # We have attempted to detect the languages in your repository. Please check
 9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 | 
14 | on:
15 |   push:
16 |     branches: [ develop, master ]
17 |   pull_request:
18 |     # The branches below must be a subset of the branches above
19 |     branches: [ develop, master ]
20 |   schedule:
21 |     - cron: '28 18 * * 6'
22 | 
23 | jobs:
24 |   analyze:
25 |     name: Analyze
26 |     runs-on: ubuntu-latest
27 |     permissions:
28 |       actions: read
29 |       contents: read
30 |       security-events: write
31 | 
32 |     strategy:
33 |       fail-fast: false
34 |       matrix:
35 |         language: [ 'ruby' ]
36 |         # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 |         # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 | 
39 |     steps:
40 |     - name: Checkout repository
41 |       uses: actions/checkout@v2
42 | 
43 |     # Initializes the CodeQL tools for scanning.
44 |     - name: Initialize CodeQL
45 |       uses: github/codeql-action/init@v1
46 |       with:
47 |         languages: ${{ matrix.language }}
48 |         # If you wish to specify custom queries, you can do so here or in a config file.
49 |         # By default, queries listed here will override any specified in a config file.
50 |         # Prefix the list here with "+" to use these queries and those in the config file.
51 |         # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 | 
53 |     # ℹ️ Command-line programs to run using the OS shell.
54 |     # 📚 https://git.io/JvXDl
55 | 
56 |     # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
57 |     #    and modify them (or add more) to build your code if your project
58 |     #    uses a compiled language
59 | 
60 |     #- run: |
61 |     #   make bootstrap
62 |     #   make release
63 | 
64 |     - name: Perform CodeQL Analysis
65 |       uses: github/codeql-action/analyze@v1
66 | 


--------------------------------------------------------------------------------
/bin/commands/commands_accepting.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   # commands_accepting command methods
 5 |   class CommandsAcceptingCommand
 6 |     def flags?(options, args, bool)
 7 |       case bool
 8 |       when :and
 9 |         all_flags?(options, args)
10 |       when :not
11 |         no_flags?(options, args)
12 |       else
13 |         any_flags?(options, args)
14 |       end
15 |     end
16 | 
17 |     def all_flags?(options, args)
18 |       args.each do |arg|
19 |         has_flag = false
20 |         options.flags.merge(options.switches).each_value do |flag|
21 |           if flag.name == arg.to_sym || flag.aliases&.include?(arg.to_sym)
22 |             has_flag = true
23 |             break
24 |           end
25 |         end
26 |         return false unless has_flag
27 |       end
28 | 
29 |       true
30 |     end
31 | 
32 |     def any_flags?(options, args)
33 |       args.each do |option|
34 |         options.flags.merge(options.switches).each_value do |flag|
35 |           return true if flag.name == option.to_sym || flag.aliases&.include?(option.to_sym)
36 |         end
37 |       end
38 | 
39 |       false
40 |     end
41 | 
42 |     def no_flags?(options, args)
43 |       args.each do |option|
44 |         options.flags.merge(options.switches).each_value do |flag|
45 |           return false if flag.name == option.to_sym || flag.aliases&.include?(option.to_sym)
46 |         end
47 |       end
48 | 
49 |       true
50 |     end
51 |   end
52 | end
53 | 
54 | # @@commands_accepting
55 | arg_name 'OPTION'
56 | command :commands_accepting do |c|
57 |   c.desc 'Output in single column for completion'
58 |   c.switch %i[c column]
59 | 
60 |   c.desc 'Join multiple arguments using boolean (AND|OR|NOT)'
61 |   c.flag [:bool], must_match: REGEX_BOOL,
62 |                   default_value: :and,
63 |                   type: BooleanSymbol
64 | 
65 |   c.action do |_g, o, a|
66 |     cac = Doing::CommandsAcceptingCommand.new
67 |     cmds = []
68 |     commands.each { |cmd, v| cmds.push(cmd) if cac.flags?(v, a, o[:bool]) }
69 | 
70 |     if o[:column]
71 |       puts cmds.sort
72 |     else
73 |       description = 'Commands '
74 |       description += 'not ' if o[:bool] == :not
75 |       description += 'accepting '
76 |       description += a.map { |arg| "--#{arg}" }.join(o[:bool] == :and ? ' and ' : ' or ')
77 |       puts "#{description}: #{cmds.sort.join(', ')}"
78 |     end
79 |   end
80 | end
81 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/src/util/eventbox.go:
--------------------------------------------------------------------------------
 1 | package util
 2 | 
 3 | import "sync"
 4 | 
 5 | // EventType is the type for fzf events
 6 | type EventType int
 7 | 
 8 | // Events is a type that associates EventType to any data
 9 | type Events map[EventType]interface{}
10 | 
11 | // EventBox is used for coordinating events
12 | type EventBox struct {
13 | 	events Events
14 | 	cond   *sync.Cond
15 | 	ignore map[EventType]bool
16 | }
17 | 
18 | // NewEventBox returns a new EventBox
19 | func NewEventBox() *EventBox {
20 | 	return &EventBox{
21 | 		events: make(Events),
22 | 		cond:   sync.NewCond(&sync.Mutex{}),
23 | 		ignore: make(map[EventType]bool)}
24 | }
25 | 
26 | // Wait blocks the goroutine until signaled
27 | func (b *EventBox) Wait(callback func(*Events)) {
28 | 	b.cond.L.Lock()
29 | 
30 | 	if len(b.events) == 0 {
31 | 		b.cond.Wait()
32 | 	}
33 | 
34 | 	callback(&b.events)
35 | 	b.cond.L.Unlock()
36 | }
37 | 
38 | // Set turns on the event type on the box
39 | func (b *EventBox) Set(event EventType, value interface{}) {
40 | 	b.cond.L.Lock()
41 | 	b.events[event] = value
42 | 	if _, found := b.ignore[event]; !found {
43 | 		b.cond.Broadcast()
44 | 	}
45 | 	b.cond.L.Unlock()
46 | }
47 | 
48 | // Clear clears the events
49 | // Unsynchronized; should be called within Wait routine
50 | func (events *Events) Clear() {
51 | 	for event := range *events {
52 | 		delete(*events, event)
53 | 	}
54 | }
55 | 
56 | // Peek peeks at the event box if the given event is set
57 | func (b *EventBox) Peek(event EventType) bool {
58 | 	b.cond.L.Lock()
59 | 	_, ok := b.events[event]
60 | 	b.cond.L.Unlock()
61 | 	return ok
62 | }
63 | 
64 | // Watch deletes the events from the ignore list
65 | func (b *EventBox) Watch(events ...EventType) {
66 | 	b.cond.L.Lock()
67 | 	for _, event := range events {
68 | 		delete(b.ignore, event)
69 | 	}
70 | 	b.cond.L.Unlock()
71 | }
72 | 
73 | // Unwatch adds the events to the ignore list
74 | func (b *EventBox) Unwatch(events ...EventType) {
75 | 	b.cond.L.Lock()
76 | 	for _, event := range events {
77 | 		b.ignore[event] = true
78 | 	}
79 | 	b.cond.L.Unlock()
80 | }
81 | 
82 | // WaitFor blocks the execution until the event is received
83 | func (b *EventBox) WaitFor(event EventType) {
84 | 	looping := true
85 | 	for looping {
86 | 		b.Wait(func(events *Events) {
87 | 			for evt := range *events {
88 | 				switch evt {
89 | 				case event:
90 | 					looping = false
91 | 					return
92 | 				}
93 | 			}
94 | 		})
95 | 	}
96 | }
97 | 


--------------------------------------------------------------------------------
/bin/commands/archive.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@archive @@move
 4 | desc 'Move entries between sections'
 5 | long_desc %(Argument can be a section name to move all entries from a section,
 6 | or start with an "@" to move entries matching a tag.
 7 | 
 8 | Default with no argument moves items from the "#{Doing.setting('current_section')}" section to Archive.)
 9 | arg_name 'SECTION_OR_TAG'
10 | default_value Doing.setting('current_section')
11 | command %i[archive move] do |c|
12 |   c.example 'doing archive Currently', desc: 'Move all entries in the Currently section to Archive section'
13 |   c.example 'doing archive @done', desc: 'Move all entries tagged @done to Archive'
14 |   c.example 'doing archive --to Later @project1', desc: 'Move all entries tagged @project1 to Later section'
15 |   c.example 'doing move Later --tag project1 --to Currently',
16 |             desc: 'Move entries in Later tagged @project1 to Currently (move is an alias for archive)'
17 | 
18 |   c.desc 'How many items to keep (ignored if archiving by tag or search)'
19 |   c.arg_name 'X'
20 |   c.flag %i[k keep], must_match: /^\d+$/, type: Integer
21 | 
22 |   c.desc 'Move entries to'
23 |   c.arg_name 'SECTION_NAME'
24 |   c.flag %i[t to], default_value: 'Archive'
25 | 
26 |   c.desc 'Label moved items with @from(SECTION_NAME)'
27 |   c.switch [:label], default_value: true, negatable: true
28 | 
29 |   add_options(:search, c)
30 |   add_options(:tag_filter, c)
31 |   add_options(:date_filter, c)
32 | 
33 |   c.action do |_global_options, options, args|
34 |     options[:fuzzy] = false
35 |     section, tags = if args.empty?
36 |                       [Doing.setting('current_section'), []]
37 |                     elsif args[0] =~ /^all/i
38 |                       ['all', []]
39 |                     elsif args[0] =~ /^@\S+/
40 |                       ['all', args.tags_to_array]
41 |                     else
42 |                       [@wwid.guess_section(args.shift.cap_first), args.tags_to_array]
43 |                     end
44 | 
45 |     raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
46 | 
47 |     tags.concat(options[:tag]) if options[:tag]
48 | 
49 |     options[:search] = options[:search].sub(/^'?/, "'") if options[:search] && options[:exact]
50 |     options[:destination] = options[:to]
51 |     options[:tags] = tags
52 | 
53 |     @wwid.archive(section, options)
54 |   end
55 | end
56 | 


--------------------------------------------------------------------------------
/lib/doing/boolean_term_parser.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'parslet'
 4 | 
 5 | module BooleanTermParser
 6 |   # This query parser adds an optional operator ("+" or "-") to the simple term
 7 |   # parser. In order to do that, a new "clause" node is added to the parse tree.
 8 |   class QueryParser < Parslet::Parser
 9 |     rule(:term) { match('[^\s]').repeat(1).as(:term) }
10 |     rule(:operator) { (str('+') | str('-')).as(:operator) }
11 |     rule(:clause) { (operator.maybe >> term).as(:clause) }
12 |     rule(:space) { match('\s').repeat(1) }
13 |     rule(:query) { (clause >> space.maybe).repeat.as(:query) }
14 |     root(:query)
15 |   end
16 | 
17 |   class QueryTransformer < Parslet::Transform
18 |     rule(clause: subtree(:clause)) do
19 |       Clause.new(clause[:operator]&.to_s, clause[:term].to_s)
20 |     end
21 |     rule(query: sequence(:clauses)) { Query.new(clauses) }
22 |   end
23 | 
24 |   class Operator
25 |     def self.symbol(str)
26 |       case str
27 |       when '+'
28 |         :must
29 |       when '-'
30 |         :must_not
31 |       when nil
32 |         :should
33 |       else
34 |         raise "Unknown operator: #{str}"
35 |       end
36 |     end
37 |   end
38 | 
39 |   class Clause
40 |     attr_accessor :operator, :term
41 | 
42 |     def initialize(operator, term)
43 |       self.operator = Operator.symbol(operator)
44 |       self.term = term
45 |     end
46 |   end
47 | 
48 |   class Query
49 |     attr_accessor :should_terms, :must_not_terms, :must_terms
50 | 
51 |     def initialize(clauses)
52 |       grouped = clauses.chunk(&:operator).to_h
53 |       self.should_terms = grouped.fetch(:should, []).map(&:term)
54 |       self.must_not_terms = grouped.fetch(:must_not, []).map(&:term)
55 |       self.must_terms = grouped.fetch(:must, []).map(&:term)
56 |     end
57 | 
58 |     def to_elasticsearch
59 |       query = {}
60 | 
61 |       if should_terms.any?
62 |         query[:should] = should_terms.map do |term|
63 |           match(term)
64 |         end
65 |       end
66 | 
67 |       if must_terms.any?
68 |         query[:must] = must_terms.map do |term|
69 |           match(term)
70 |         end
71 |       end
72 | 
73 |       if must_not_terms.any?
74 |         query[:must_not] = must_not_terms.map do |term|
75 |           match(term)
76 |         end
77 |       end
78 | 
79 |       query
80 |     end
81 | 
82 |     def match(term)
83 |       term
84 |     end
85 |   end
86 | end
87 | 


--------------------------------------------------------------------------------
/test/doing_resume_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'fileutils'
 4 | require 'tempfile'
 5 | require 'time'
 6 | require 'yaml'
 7 | 
 8 | require 'helpers/doing-helpers'
 9 | require 'test_helper'
10 | 
11 | # Tests for entry modifying commands
12 | class DoingResumeTest < Test::Unit::TestCase
13 |   include DoingHelpers
14 |   ENTRY_TS_REGEX = /\s*(?[^|]+) \s*\|/.freeze
15 |   ENTRY_DONE_REGEX = /@done\((?.*?)\)/.freeze
16 | 
17 |   def setup
18 |     @tmpdirs = []
19 |     @result = ''
20 |     @basedir = mktmpdir
21 |     @wwid_file = File.join(@basedir, 'wwid.md')
22 |     @backup_dir = File.join(@basedir, 'doing_backup')
23 |     @config_file = File.join(File.dirname(__FILE__), 'test.doingrc')
24 |     @config = YAML.safe_load(IO.read(@config_file))
25 |   end
26 | 
27 |   def teardown
28 |     FileUtils.rm_rf(@tmpdirs)
29 |   end
30 | 
31 |   def test_resume_entry
32 |     subject = 'Test entry'
33 |     doing('done', subject)
34 |     result = doing('--stdout', '--debug', 'again')
35 | 
36 |     assert_match(/New entry: added "(.*?)?: #{subject}" to #{@config['current_section']}/, result,
37 |                  'Entry should be added again')
38 |   end
39 | 
40 |   def test_resume_tag
41 |     3.times { |i| doing('done', '--back', "#{i + 5}m", "Entry #{i + 1} with @tag#{i + 1}") }
42 |     result = doing('--stdout', '--debug', 'again', '--tag', 'tag2')
43 |     assert_match(/New entry: added "(.*?)?: Entry 2 with @tag2"/, result, 'Entry 2 should be repeated')
44 | 
45 |     result = doing('last').uncolor.strip
46 | 
47 |     assert_match(/Entry 2 with @tag2/, result, 'Entry 2 should be added again')
48 |     assert_no_match(ENTRY_DONE_REGEX, result, 'Entry 2 should not be @done')
49 |   end
50 | 
51 |   def test_finish_and_resume
52 |     doing('now', '--back', '5m', 'Entry 4 with @tag4')
53 |     doing('again')
54 |     result = doing('show', '@done').uncolor.strip
55 |     assert_match(/Entry 4 with @tag4 @done/, result, 'Entry 4 should be completed')
56 |     result = doing('last').uncolor.strip
57 |     assert_no_match(ENTRY_DONE_REGEX, result, 'New Entry 4 should not be @done')
58 |   end
59 | 
60 |   private
61 | 
62 |   def mktmpdir
63 |     tmpdir = Dir.mktmpdir
64 |     @tmpdirs.push(tmpdir)
65 | 
66 |     tmpdir
67 |   end
68 | 
69 |   def doing(*args)
70 |     doing_with_env({ 'DOING_CONFIG' => @config_file, 'DOING_BACKUP_DIR' => @backup_dir }, '--doing_file', @wwid_file,
71 |                    *args)
72 |   end
73 | end
74 | 


--------------------------------------------------------------------------------
/lib/doing/time.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   ##
 5 |   ## Date helpers
 6 |   ##
 7 |   class ::Time
 8 |     # Format time as a relative date. Dates from today get
 9 |     # just a time, from the last week get a time and day,
10 |     # from the last year get a month/day/time, and older
11 |     # entries get month/day/year/time
12 |     #
13 |     # @return     [String] formatted date
14 |     #
15 |     def relative_date
16 |       if self > Date.today.to_time
17 |         strftime(Doing.setting('shortdate_format.today', '%_I:%M%P', exact: true))
18 |       elsif self > (Date.today - 6).to_time
19 |         strftime(Doing.setting('shortdate_format.this_week', '%a %_I:%M%P', exact: true))
20 |       elsif year == Date.today.year || (year + 1 == Date.today.year && month > Date.today.month)
21 |         strftime(Doing.setting('shortdate_format.this_month', '%m/%d %_I:%M%P', exact: true))
22 |       else
23 |         strftime(Doing.setting('shortdate_format.older', '%m/%d/%y %_I:%M%P', exact: true))
24 |       end
25 |     end
26 | 
27 |     ##
28 |     ## Format seconds as a natural language string
29 |     ##
30 |     ## @param      seconds  [Integer] number of seconds
31 |     ##
32 |     ## @return [String] Date formatted as "X days, X hours, X minutes, X seconds"
33 |     def humanize(seconds)
34 |       s = seconds
35 |       m = (s / 60).floor
36 |       s = (s % 60).floor
37 |       h = (m / 60).floor
38 |       m = (m % 60).floor
39 |       d = (h / 24).floor
40 |       h %= 24
41 | 
42 |       output = []
43 |       output.push("#{d} #{'day'.to_p(d)}") if d.positive?
44 |       output.push("#{h} #{'hour'.to_p(h)}") if h.positive?
45 |       output.push("#{m} #{'minute'.to_p(m)}") if m.positive?
46 |       output.push("#{s} #{'second'.to_p(s)}") if s.positive?
47 |       output.join(', ')
48 |     end
49 | 
50 |     ##
51 |     ## Format date as "X hours ago"
52 |     ##
53 |     ## @return     [String] Formatted date
54 |     ##
55 |     def time_ago
56 |       if self > Date.today.to_time
57 |         output = humanize(Time.now - self)
58 |         "#{output} ago"
59 |       elsif self > (Date.today - 1).to_time
60 |         "Yesterday at #{strftime('%_I:%M:%S%P')}"
61 |       elsif self > (Date.today - 6).to_time
62 |         strftime('%a %I:%M:%S%P')
63 |       elsif year == Date.today.year
64 |         strftime('%m/%d %I:%M:%S%P')
65 |       else
66 |         strftime('%m/%d/%Y %I:%M:%S%P')
67 |       end
68 |     end
69 |   end
70 | end
71 | 


--------------------------------------------------------------------------------
/test/doing_undo_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'fileutils'
 4 | require 'tempfile'
 5 | 
 6 | require 'helpers/doing-helpers'
 7 | require 'test_helper'
 8 | 
 9 | # Tests for done commands
10 | class DoingUndoTest < Test::Unit::TestCase
11 |   include DoingHelpers
12 |   ENTRY_REGEX = /^\d{4}-\d\d-\d\d \d\d:\d\d \|/.freeze
13 | 
14 |   def setup
15 |     @tmpdirs = []
16 |     @basedir = mktmpdir
17 |     @wwid_file = File.join(@basedir, 'wwid_undo.md')
18 |     @backup_dir = File.join(@basedir, 'doing_backup')
19 |     @config_file = File.join(File.dirname(__FILE__), 'test.doingrc')
20 |   end
21 | 
22 |   def teardown
23 |     FileUtils.rm_rf(@tmpdirs)
24 |   end
25 | 
26 |   def test_undo
27 |     entries = [
28 |       'backlog entry 1',
29 |       'backlog entry 2',
30 |       'Begin history',
31 |       'Test entry 1',
32 |       'Test entry 2'
33 |     ]
34 |     entries.each do |e|
35 |       doing('now', e)
36 |       sleep 0.5
37 |     end
38 | 
39 |     assert_count_entries(entries.count, doing('show'))
40 | 
41 |     doing('undo')
42 |     shown = doing('show')
43 |     assert_not_contains_entry('Test entry 2', shown)
44 |     assert_contains_entry('Test entry 1', shown)
45 | 
46 |     doing('undo')
47 |     shown = doing('show')
48 |     assert_not_contains_entry('Test entry 1', shown)
49 |     assert_contains_entry('Begin history', shown)
50 | 
51 |     doing('undo', '--redo')
52 |     shown = doing('show')
53 |     assert_contains_entry('Test entry 1', shown)
54 | 
55 |     doing('undo', '--prune', '0')
56 |     assert_equal(0, Dir.glob('*.md', base: @backup_dir).count)
57 |   end
58 | 
59 |   private
60 | 
61 |   def assert_contains_entry(string, shown, message = 'Entry containing string should exist')
62 |     assert_match(/#{string}/, shown, "#{message}: #{string}")
63 |   end
64 | 
65 |   def assert_not_contains_entry(string, shown, message = 'Entry containing string should exist')
66 |     assert_no_match(/#{string}/, shown, "#{message}: #{string}")
67 |   end
68 | 
69 |   def assert_count_entries(count, shown, message = 'Should be X entries shown')
70 |     assert_equal(count, shown.uncolor.strip.scan(ENTRY_REGEX).count, message)
71 |   end
72 | 
73 |   def mktmpdir
74 |     tmpdir = Dir.mktmpdir
75 |     @tmpdirs.push(tmpdir)
76 | 
77 |     tmpdir
78 |   end
79 | 
80 |   def doing(*args)
81 |     doing_with_env({ 'DOING_BACKUP_DIR' => @backup_dir, 'DOING_CONFIG' => @config_file }, '--doing_file', @wwid_file,
82 |                    *args)
83 |   end
84 | end
85 | 


--------------------------------------------------------------------------------
/bin/commands/tags.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@tags
 4 | desc 'List all tags in the current Doing file'
 5 | arg_name 'MAX_COUNT', optional: true, type: Integer
 6 | command :tags do |c|
 7 |   c.desc 'Section'
 8 |   c.arg_name 'SECTION_NAME'
 9 |   c.flag %i[s section], default_value: 'All', multiple: true
10 | 
11 |   c.desc 'Show count of occurrences'
12 |   c.switch %i[c counts]
13 | 
14 |   c.desc 'Output in a single line with @ symbols. Ignored if --counts is specified.'
15 |   c.switch %i[l line]
16 | 
17 |   c.desc 'Sort by name or count'
18 |   c.arg_name 'SORT_ORDER'
19 |   c.flag %i[sort], default_value: 'name', must_match: /^(?:n(?:ame)?|c(?:ount)?)$/
20 | 
21 |   c.desc 'Sort order (asc/desc)'
22 |   c.arg_name 'ORDER'
23 |   c.flag %i[o order], must_match: REGEX_SORT_ORDER, default_value: :asc, type: OrderSymbol
24 | 
25 |   c.desc 'Select items to scan from a menu of matching entries'
26 |   c.switch %i[i interactive], negatable: false, default_value: false
27 | 
28 |   add_options(:search, c)
29 |   add_options(:tag_filter, c)
30 | 
31 |   c.action do |_global, options, args|
32 |     @wwid.guess_section(options[:section]) || options[:section].cap_first
33 |     options[:count] = args.count.positive? ? args[0].to_i : 0
34 | 
35 |     items = @wwid.filter_items([], opt: options)
36 | 
37 |     if options[:interactive]
38 |       items = Doing::Prompt.choose_from_items(items, include_section: options[:section].nil?,
39 |                                                      menu: true,
40 |                                                      header: '',
41 |                                                      prompt: 'Select entries to scan > ',
42 |                                                      multiple: true,
43 |                                                      sort: true,
44 |                                                      show_if_single: true)
45 |     end
46 | 
47 |     # items = @wwid.content.in_section(section)
48 |     tags = @wwid.all_tags(items, counts: true)
49 | 
50 |     tags = if options[:sort] =~ /^n/i
51 |              tags.sort_by { |tag, _count| tag }
52 |            else
53 |              tags.sort_by { |_tag, count| count }
54 |            end
55 | 
56 |     tags.reverse! if options[:order] == :desc
57 | 
58 |     if options[:counts]
59 |       tags.each { |t, c| puts "#{t} (#{c})" }
60 |     elsif options[:line]
61 |       puts tags.map { |t, _c| t }.to_tags.join(' ')
62 |     else
63 |       tags.each { |t, _| puts t }
64 |     end
65 |   end
66 | end
67 | 


--------------------------------------------------------------------------------
/lib/doing/plugins/export/byday.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # title: By Day Export
 4 | # description: Export a table of items grouped by day with daily totals
 5 | # author: Brett Terpstra
 6 | # url: https://brettterpstra.com
 7 | module Doing
 8 |   class ByDayExport
 9 |     def self.settings
10 |       {
11 |         trigger: 'byday',
12 |         config: {
13 |           'item_width' => 60
14 |         }
15 |       }
16 |     end
17 | 
18 |     def self.render(wwid, items, variables: {})
19 |       return if items.nil?
20 | 
21 |       days = {}
22 | 
23 |       items.each do |item|
24 |         date = item.date.strftime('%Y-%m-%d')
25 |         days[date] ||= []
26 |         days[date].push(item)
27 |       end
28 | 
29 |       totals = {}
30 |       total = 0
31 | 
32 |       days.each do |day, day_items|
33 |         day_items.each do |item|
34 |           totals[day] ||= 0
35 |           duration = item.interval || 0
36 |           totals[day] += duration
37 |           total += duration
38 |         end
39 |       end
40 |       width = wwid.config['plugins']['byday']['item_width'].to_i || 60
41 |       divider = "{wd}+{xk}#{'-' * 10}{wd}+{xk}#{'-' * width}{wd}+{xk}#{'-' * 8}{wd}+{x}"
42 |       out = []
43 |       out << divider
44 |       out << "{wd}|{xm}date      {wd}|{xbw}item#{' ' * (width - 4)}{wd}|{xy}duration{wd}|{x}"
45 |       out << divider
46 |       days.each do |day, day_items|
47 |         first = day_items.slice!(0, 1)[0]
48 |         interval = wwid.get_interval(first, formatted: true) || '00:00:00'
49 |         title = first.title.tag('done', remove: true).trunc(width - 2).ljust(width)
50 |         out << "{wd}|{xm}#{day}{wd}|{xbw}#{title}{wd}|{xy}#{interval}{wd}|{x}"
51 |         day_items.each do |item|
52 |           interval = wwid.get_interval(item, formatted: true) || '00:00:00'
53 |           title = item.title.tag('done', remove: true).trunc(width - 2).ljust(width)
54 |           out << "{wd}|          |{xbw}#{title}{wd}|{xy}#{interval}{wd}|{x}"
55 |         end
56 |         day_total = "Total: #{totals[day].time_string(format: :clock)}"
57 |         out << divider
58 |         out << "{wd}|{xg}#{day_total.rjust(width + 20)}{wd}|{x}"
59 |         out << divider
60 |       end
61 |       all_total = "Grand Total: #{total.time_string(format: :clock)}"
62 |       out << "{wd}|{xrb}#{all_total.rjust(width + 20)}{wd}|{x}"
63 |       out << divider
64 |       Doing::Color.template(out.join("\n"))
65 |     end
66 | 
67 |     Doing::Plugins.register 'byday', :export, self
68 |   end
69 | end
70 | 


--------------------------------------------------------------------------------
/bin/commands/rotate.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@rotate
 4 | desc 'Move entries to archive file'
 5 | long_desc 'As your doing file grows, commands can get slow. Given that your historical data (and your archive section)
 6 | probably aren\'t providing any useful insights a year later, use this command to "rotate" old entries out to an archive
 7 | file. You\'ll still have access to all historical data, but it won\'t be slowing down daily operation.'
 8 | command :rotate do |c|
 9 |   c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
10 |   c.example 'doing rotate --section Archive --keep 10',
11 |             desc: 'Move entries in the Archive section to a secondary file, keeping the most recent 10 entries'
12 |   c.example 'doing rotate --tag project1,done --bool AND',
13 |             desc: 'Move entries tagged @project1 and @done to a secondary file'
14 | 
15 |   c.desc 'How many items to keep in each section (most recent)'
16 |   c.arg_name 'X'
17 |   c.flag %i[k keep], must_match: /^\d+$/, type: Integer
18 | 
19 |   c.desc 'Section to rotate'
20 |   c.arg_name 'SECTION_NAME'
21 |   c.flag %i[s section], default_value: 'All'
22 | 
23 |   c.desc 'Rotate entries older than date
24 |     (Flexible date format, e.g. 1/27/2021, 2020-07-19, or Monday 3pm)'
25 |   c.arg_name 'DATE_STRING'
26 |   c.flag [:before]
27 | 
28 |   add_options(:search, c)
29 |   add_options(:tag_filter, c)
30 | 
31 |   c.action do |_global_options, options, _args|
32 |     options[:fuzzy] = false
33 | 
34 |     options[:section] = @wwid.guess_section(options[:section]) if options[:section] && options[:section] !~ /^all$/i
35 | 
36 |     search = nil
37 | 
38 |     if options[:search]
39 |       search = options[:search]
40 |       search.sub!(/^'?/, "'") if options[:exact]
41 |       options[:search] = search
42 |     end
43 | 
44 |     @wwid.rotate(options)
45 |   end
46 | end
47 | 
48 | # # @@doctor
49 | 
50 | # desc 'Doing file maintenance.'
51 | # long_desc %(Duplicate entries compressed to a single entry. This will modify the doing file.)
52 | # command :doctor do |c|
53 | #   c.example 'doing doctor', desc: 'Clean up the Doing file, sorting and removing duplicates'
54 | 
55 | #   c.desc 'Only remove duplicates in the same section'
56 | #   c.switch %i[s same_section], negatable: false, default_value: false
57 | 
58 | #   c.action do |_global_options, options, _args|
59 | #     @wwid.content.dedup!(match_section: options[:same_section])
60 | #     @wwid.write(@wwid.doing_file)
61 | #   end
62 | # end
63 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/src/history.go:
--------------------------------------------------------------------------------
 1 | package fzf
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"io/ioutil"
 6 | 	"os"
 7 | 	"strings"
 8 | )
 9 | 
10 | // History struct represents input history
11 | type History struct {
12 | 	path     string
13 | 	lines    []string
14 | 	modified map[int]string
15 | 	maxSize  int
16 | 	cursor   int
17 | }
18 | 
19 | // NewHistory returns the pointer to a new History struct
20 | func NewHistory(path string, maxSize int) (*History, error) {
21 | 	fmtError := func(e error) error {
22 | 		if os.IsPermission(e) {
23 | 			return errors.New("permission denied: " + path)
24 | 		}
25 | 		return errors.New("invalid history file: " + e.Error())
26 | 	}
27 | 
28 | 	// Read history file
29 | 	data, err := ioutil.ReadFile(path)
30 | 	if err != nil {
31 | 		// If it doesn't exist, check if we can create a file with the name
32 | 		if os.IsNotExist(err) {
33 | 			data = []byte{}
34 | 			if err := ioutil.WriteFile(path, data, 0600); err != nil {
35 | 				return nil, fmtError(err)
36 | 			}
37 | 		} else {
38 | 			return nil, fmtError(err)
39 | 		}
40 | 	}
41 | 	// Split lines and limit the maximum number of lines
42 | 	lines := strings.Split(strings.Trim(string(data), "\n"), "\n")
43 | 	if len(lines[len(lines)-1]) > 0 {
44 | 		lines = append(lines, "")
45 | 	}
46 | 	return &History{
47 | 		path:     path,
48 | 		maxSize:  maxSize,
49 | 		lines:    lines,
50 | 		modified: make(map[int]string),
51 | 		cursor:   len(lines) - 1}, nil
52 | }
53 | 
54 | func (h *History) append(line string) error {
55 | 	// We don't append empty lines
56 | 	if len(line) == 0 {
57 | 		return nil
58 | 	}
59 | 
60 | 	lines := append(h.lines[:len(h.lines)-1], line)
61 | 	if len(lines) > h.maxSize {
62 | 		lines = lines[len(lines)-h.maxSize:]
63 | 	}
64 | 	h.lines = append(lines, "")
65 | 	return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
66 | }
67 | 
68 | func (h *History) override(str string) {
69 | 	// You can update the history but they're not written to the file
70 | 	if h.cursor == len(h.lines)-1 {
71 | 		h.lines[h.cursor] = str
72 | 	} else if h.cursor < len(h.lines)-1 {
73 | 		h.modified[h.cursor] = str
74 | 	}
75 | }
76 | 
77 | func (h *History) current() string {
78 | 	if str, prs := h.modified[h.cursor]; prs {
79 | 		return str
80 | 	}
81 | 	return h.lines[h.cursor]
82 | }
83 | 
84 | func (h *History) previous() string {
85 | 	if h.cursor > 0 {
86 | 		h.cursor--
87 | 	}
88 | 	return h.current()
89 | }
90 | 
91 | func (h *History) next() string {
92 | 	if h.cursor < len(h.lines)-1 {
93 | 		h.cursor++
94 | 	}
95 | 	return h.current()
96 | }
97 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/src/constants.go:
--------------------------------------------------------------------------------
 1 | package fzf
 2 | 
 3 | import (
 4 | 	"math"
 5 | 	"os"
 6 | 	"time"
 7 | 
 8 | 	"github.com/junegunn/fzf/src/util"
 9 | )
10 | 
11 | const (
12 | 	// Core
13 | 	coordinatorDelayMax  time.Duration = 100 * time.Millisecond
14 | 	coordinatorDelayStep time.Duration = 10 * time.Millisecond
15 | 
16 | 	// Reader
17 | 	readerBufferSize       = 64 * 1024
18 | 	readerPollIntervalMin  = 10 * time.Millisecond
19 | 	readerPollIntervalStep = 5 * time.Millisecond
20 | 	readerPollIntervalMax  = 50 * time.Millisecond
21 | 
22 | 	// Terminal
23 | 	initialDelay      = 20 * time.Millisecond
24 | 	initialDelayTac   = 100 * time.Millisecond
25 | 	spinnerDuration   = 100 * time.Millisecond
26 | 	previewCancelWait = 500 * time.Millisecond
27 | 	previewChunkDelay = 100 * time.Millisecond
28 | 	previewDelayed    = 500 * time.Millisecond
29 | 	maxPatternLength  = 300
30 | 	maxMulti          = math.MaxInt32
31 | 
32 | 	// Matcher
33 | 	numPartitionsMultiplier = 8
34 | 	maxPartitions           = 32
35 | 	progressMinDuration     = 200 * time.Millisecond
36 | 
37 | 	// Capacity of each chunk
38 | 	chunkSize int = 100
39 | 
40 | 	// Pre-allocated memory slices to minimize GC
41 | 	slab16Size int = 100 * 1024 // 200KB * 32 = 12.8MB
42 | 	slab32Size int = 2048       // 8KB * 32 = 256KB
43 | 
44 | 	// Do not cache results of low selectivity queries
45 | 	queryCacheMax int = chunkSize / 5
46 | 
47 | 	// Not to cache mergers with large lists
48 | 	mergerCacheMax int = 100000
49 | 
50 | 	// History
51 | 	defaultHistoryMax int = 1000
52 | 
53 | 	// Jump labels
54 | 	defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
55 | )
56 | 
57 | var defaultCommand string
58 | 
59 | func init() {
60 | 	if !util.IsWindows() {
61 | 		defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-`
62 | 	} else if os.Getenv("TERM") == "cygwin" {
63 | 		defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
64 | 	}
65 | }
66 | 
67 | // fzf events
68 | const (
69 | 	EvtReadNew util.EventType = iota
70 | 	EvtReadFin
71 | 	EvtSearchNew
72 | 	EvtSearchProgress
73 | 	EvtSearchFin
74 | 	EvtHeader
75 | 	EvtReady
76 | 	EvtQuit
77 | )
78 | 
79 | const (
80 | 	exitCancel    = -1
81 | 	exitOk        = 0
82 | 	exitNoMatch   = 1
83 | 	exitError     = 2
84 | 	exitInterrupt = 130
85 | )
86 | 


--------------------------------------------------------------------------------
/lib/doing/hooks.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | module Doing
 4 |   # Hook manager
 5 |   module Hooks
 6 |     DEFAULT_PRIORITY = 20
 7 | 
 8 |     @registry = {
 9 |       post_config: [],        # wwid
10 |       post_local_config: [],  # wwid
11 |       post_read: [],          # wwid
12 |       pre_entry_add: [],      # wwid, new_entry
13 |       post_entry_added: [],   # wwid, new_entry
14 |       post_entry_updated: [], # wwid, entry, old_entry
15 |       post_entry_removed: [], # wwid, entry.dup
16 |       pre_export: [],         # wwid, format, entries
17 |       pre_write: [],          # wwid, file
18 |       post_write: []          # file
19 |     }
20 | 
21 |     # map of all hooks and their priorities
22 |     @hook_priority = {}
23 | 
24 |     # register hook(s) to be called later, public API
25 |     def self.register(event, priority: DEFAULT_PRIORITY, &block)
26 |       if event.is_a?(Array)
27 |         event.each { |ev| register_one(ev, priority_value(priority), &block) }
28 |       else
29 |         register_one(event, priority_value(priority), &block)
30 |       end
31 |     end
32 | 
33 |     # Ensure the priority is a Fixnum
34 |     def self.priority_value(priority)
35 |       return priority if priority.is_a?(Integer)
36 | 
37 |       PRIORITY_MAP[priority] || DEFAULT_PRIORITY
38 |     end
39 | 
40 |     # register a single hook to be called later, internal API
41 |     def self.register_one(event, priority, &block)
42 |       unless @registry[event]
43 |         raise Doing::Errors::HookUnavailable.new("Invalid hook. Doing only supports #{@registry.keys.inspect}", 'hook',
44 |                                                  event)
45 |       end
46 | 
47 |       unless block.respond_to? :call
48 |         raise Doing::Errors::PluginUncallable.new('Hooks must respond to :call', 'hook',
49 |                                                   event)
50 |       end
51 | 
52 |       Doing.logger.debug('Hook Manager:', "Registered #{event} hook") if ENV['DOING_PLUGIN_DEBUG']
53 | 
54 |       insert_hook event, priority, &block
55 |     end
56 | 
57 |     def self.insert_hook(event, priority, &block)
58 |       @hook_priority[block] = [-priority, @hook_priority.size]
59 |       @registry[event] << block
60 |     end
61 | 
62 |     def self.trigger(event, *args)
63 |       hooks = @registry[event]
64 |       return unless hooks.good?
65 | 
66 |       # sort and call hooks according to priority and load order
67 |       hooks.sort_by { |h| @hook_priority[h] }.each do |hook|
68 |         hook.call(*args)
69 |       end
70 |     end
71 |   end
72 | end
73 | 


--------------------------------------------------------------------------------
/lib/helpers/fzf/src/util/util_windows.go:
--------------------------------------------------------------------------------
 1 | // +build windows
 2 | 
 3 | package util
 4 | 
 5 | import (
 6 | 	"fmt"
 7 | 	"os"
 8 | 	"os/exec"
 9 | 	"strings"
10 | 	"sync/atomic"
11 | 	"syscall"
12 | )
13 | 
14 | var shellPath atomic.Value
15 | 
16 | // ExecCommand executes the given command with $SHELL
17 | func ExecCommand(command string, setpgid bool) *exec.Cmd {
18 | 	var shell string
19 | 	if cached := shellPath.Load(); cached != nil {
20 | 		shell = cached.(string)
21 | 	} else {
22 | 		shell = os.Getenv("SHELL")
23 | 		if len(shell) == 0 {
24 | 			shell = "cmd"
25 | 		} else if strings.Contains(shell, "/") {
26 | 			out, err := exec.Command("cygpath", "-w", shell).Output()
27 | 			if err == nil {
28 | 				shell = strings.Trim(string(out), "\n")
29 | 			}
30 | 		}
31 | 		shellPath.Store(shell)
32 | 	}
33 | 	return ExecCommandWith(shell, command, setpgid)
34 | }
35 | 
36 | // ExecCommandWith executes the given command with the specified shell
37 | // FIXME: setpgid is unused. We set it in the Unix implementation so that we
38 | // can kill preview process with its child processes at once.
39 | // NOTE: For "powershell", we should ideally set output encoding to UTF8,
40 | // but it is left as is now because no adverse effect has been observed.
41 | func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
42 | 	var cmd *exec.Cmd
43 | 	if strings.Contains(shell, "cmd") {
44 | 		cmd = exec.Command(shell)
45 | 		cmd.SysProcAttr = &syscall.SysProcAttr{
46 | 			HideWindow:    false,
47 | 			CmdLine:       fmt.Sprintf(` /v:on/s/c "%s"`, command),
48 | 			CreationFlags: 0,
49 | 		}
50 | 		return cmd
51 | 	}
52 | 
53 | 	if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
54 | 		cmd = exec.Command(shell, "-NoProfile", "-Command", command)
55 | 	} else {
56 | 		cmd = exec.Command(shell, "-c", command)
57 | 	}
58 | 	cmd.SysProcAttr = &syscall.SysProcAttr{
59 | 		HideWindow:    false,
60 | 		CreationFlags: 0,
61 | 	}
62 | 	return cmd
63 | }
64 | 
65 | // KillCommand kills the process for the given command
66 | func KillCommand(cmd *exec.Cmd) error {
67 | 	return cmd.Process.Kill()
68 | }
69 | 
70 | // IsWindows returns true on Windows
71 | func IsWindows() bool {
72 | 	return true
73 | }
74 | 
75 | // SetNonblock executes syscall.SetNonblock on file descriptor
76 | func SetNonblock(file *os.File, nonblock bool) {
77 | 	syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
78 | }
79 | 
80 | // Read executes syscall.Read on file descriptor
81 | func Read(fd int, b []byte) (int, error) {
82 | 	return syscall.Read(syscall.Handle(fd), b)
83 | }
84 | 


--------------------------------------------------------------------------------
/bin/commands/on.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | # @@on
 4 | desc 'List entries for a date'
 5 | long_desc %(Date argument can be natural language. "thursday" would be interpreted as "last thursday,"
 6 | and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates,
 7 | it will create a range.)
 8 | arg_name 'DATE_STRING'
 9 | command :on do |c|
10 |   c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
11 |   c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
12 |   c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'
13 | 
14 |   c.desc 'Section'
15 |   c.arg_name 'NAME'
16 |   c.flag %i[s section], default_value: 'All', multiple: true
17 | 
18 |   add_options(:output_template, c)
19 |   add_options(:time_display, c)
20 |   add_options(:search, c)
21 |   add_options(:tag_filter, c)
22 |   add_options(:time_filter, c)
23 |   add_options(:save, c)
24 | 
25 |   c.action do |_global_options, options, args|
26 |     if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
27 |       raise InvalidPlugin.new('output', options[:output])
28 | 
29 |     end
30 | 
31 |     raise MissingArgument, 'Missing date argument' if args.empty?
32 | 
33 |     date_string = args.join(' ').strip
34 |     date_string = 'midnight to today 23:59' if date_string =~ /^tod(?:ay)?/i
35 | 
36 |     start, finish = date_string.split_date_range
37 | 
38 |     raise InvalidTimeExpression, "Unrecognized date string (#{date_string})" unless start
39 | 
40 |     message = "date interpreted as #{start}"
41 |     message += " to #{finish}" if finish
42 |     Doing.logger.debug('Interpreter:', message)
43 | 
44 |     options[:times] = true if options[:totals]
45 |     options[:sort_tags] = options[:tag_sort]
46 | 
47 |     Doing::Pager.page @wwid.list_date([start, finish],
48 |                                       options[:section],
49 |                                       options[:times],
50 |                                       options[:output],
51 |                                       options).chomp
52 | 
53 |     if options[:save]
54 |       options[:before] = Doing.original_options[:date_end] if Doing.original_options[:date_end].good?
55 |       options[:after] = Doing.original_options[:date_begin] if Doing.original_options[:date_begin].good?
56 |       options[:from] = Doing.original_options[:date_range] if Doing.original_options[:date_range].good?
57 |       Doing.config.save_view(options.to_view, options[:save].downcase)
58 |     end
59 |   end
60 | end
61 | 


--------------------------------------------------------------------------------
/test/doing_cancel_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'fileutils'
 4 | require 'tempfile'
 5 | require 'time'
 6 | require 'yaml'
 7 | 
 8 | require 'helpers/doing-helpers'
 9 | require 'test_helper'
10 | 
11 | # Tests for entry modifying commands
12 | class DoingCancelTest < Test::Unit::TestCase
13 |   include DoingHelpers
14 |   ENTRY_REGEX = /^\d{4}-\d\d-\d\d \d\d:\d\d \|/.freeze
15 |   ENTRY_TS_REGEX = /\s*(?[^|]+) \s*\|/.freeze
16 |   ENTRY_DONE_REGEX = /@done\((?.*?)\)/.freeze
17 | 
18 |   def setup
19 |     @tmpdirs = []
20 |     @result = ''
21 |     @basedir = mktmpdir
22 |     @wwid_file = File.join(@basedir, 'wwid.md')
23 |     @backup_dir = File.join(@basedir, 'doing_backup')
24 |     @config_file = File.join(File.dirname(__FILE__), 'test.doingrc')
25 |     @config = YAML.safe_load(IO.read(@config_file))
26 |   end
27 | 
28 |   def teardown
29 |     FileUtils.rm_rf(@tmpdirs)
30 |   end
31 | 
32 |   def test_cancel_entry
33 |     doing('now', 'Test entry')
34 |     doing('cancel')
35 |     assert_match(/@done$/, doing('show'), 'should have @done tag with no timestamp')
36 |   end
37 | 
38 |   def test_cancel_search
39 |     unique = 'unique string'
40 |     doing('now', '1 Test entry @tag1')
41 |     doing('now', "3 Test entry #{unique}")
42 |     doing('now', '2 Test entry @tag2')
43 |     res = doing('--stdout', 'cancel', '--tag', 'tag1')
44 |     assert_match(/added tag @done to 1 Test/, res, 'should have cancelled tagged entry')
45 |     res = doing('--stdout', 'cancel', '--search', unique)
46 |     assert_match(/added tag @done to 3 Test/, res, 'should have @done tag with no timestamp')
47 |   end
48 | 
49 |   def test_cancel_multiple_args
50 |     doing('now', 'Test entry')
51 |     assert_raises(RuntimeError, 'Multiple arguments should cause error') { doing('cancel', '1', 'arg2') }
52 |   end
53 | 
54 |   private
55 | 
56 |   def assert_matches(matches, shown)
57 |     matches.each do |regexp, msg, opt_refute|
58 |       if opt_refute
59 |         assert_no_match(regexp, shown, msg)
60 |       else
61 |         assert_match(regexp, shown, msg)
62 |       end
63 |     end
64 |   end
65 | 
66 |   def assert_count_entries(count, shown, message = 'Should be X entries shown')
67 |     assert_equal(count, shown.uncolor.strip.scan(ENTRY_REGEX).count, message)
68 |   end
69 | 
70 |   def mktmpdir
71 |     tmpdir = Dir.mktmpdir
72 |     @tmpdirs.push(tmpdir)
73 | 
74 |     tmpdir
75 |   end
76 | 
77 |   def doing(*args)
78 |     doing_with_env({ 'DOING_CONFIG' => @config_file, 'DOING_BACKUP_DIR' => @backup_dir }, '--doing_file', @wwid_file,
79 |                    *args)
80 |   end
81 | end
82 | 


--------------------------------------------------------------------------------
/test/doing_chronify_test.rb:
--------------------------------------------------------------------------------
 1 | # frozen_string_literal: true
 2 | 
 3 | require 'fileutils'
 4 | require 'tempfile'
 5 | require 'time'
 6 | 
 7 | require 'helpers/doing-helpers'
 8 | require 'test_helper'
 9 | 
10 | # Tests for natural language date processing
11 | class DoingChronifyTest < Test::Unit::TestCase
12 |   include DoingHelpers
13 |   ENTRY_TS_REGEX = /\s*(?[^|]+) \s*\|/.freeze
14 |   ENTRY_DONE_REGEX = /@done\((?.*?)\)/.freeze
15 | 
16 |   def setup
17 |     @tmpdirs = []
18 |     @basedir = mktmpdir
19 |     @wwid_file = File.join(@basedir, 'wwid.md')
20 |     @backup_dir = File.join(@basedir, 'doing_backup')
21 |     @config_file = File.join(File.dirname(__FILE__), 'test.doingrc')
22 |   end
23 | 
24 |   def teardown
25 |     FileUtils.rm_rf(@tmpdirs)
26 |   end
27 | 
28 |   def test_back_rejects_empty_args
29 |     assert_raises(RuntimeError) { doing('now', '--back', '', 'should fail') }
30 |   end
31 | 
32 |   def test_back_interval
33 |     now = Time.now
34 |     doing('now', '--back', '20m', 'test interval format')
35 |     m = doing('show').match(ENTRY_TS_REGEX)
36 |     assert(m)
37 |     assert_within_tolerance(Time.parse(m['ts']), now - (20 * 60), tolerance: 2,
38 |                                                                   message: 'New entry should be equal to the nearest minute')
39 |   end
40 | 
41 |   def test_back_strftime
42 |     ts = '2016-03-15 15:32:04'
43 |     doing('now', '--back', ts, 'test strftime format')
44 |     m = doing('show').match(ENTRY_TS_REGEX)
45 |     assert(m)
46 |     assert_within_tolerance(Time.parse(m['ts']), Time.parse(ts), tolerance: 2,
47 |                                                                  message: 'New entry should be equal to the nearest minute')
48 |   end
49 | 
50 |   def test_back_semantic
51 |     yesterday = (Time.now - (60 * 60 * 24)).strftime('%Y-%m-%d 18:30 %Z')
52 |     doing('now', '--back', 'yesterday 6:30pm', 'test semantic format')
53 |     m = doing('show').match(ENTRY_TS_REGEX)
54 |     assert(m)
55 |     entry_time = Time.parse(m['ts']).strftime('%Y-%m-%d %H:%M %Z')
56 |     assert_equal(entry_time, yesterday, 'new entry is the wrong time')
57 |   end
58 | 
59 |   private
60 | 
61 |   def assert_within_tolerance(t1, t2, message: 'Times should be within tolerance of each other', tolerance: 2)
62 |     assert(t1.close_enough?(t2, tolerance: tolerance), message)
63 |   end
64 | 
65 |   def mktmpdir
66 |     tmpdir = Dir.mktmpdir
67 |     @tmpdirs.push(tmpdir)
68 | 
69 |     tmpdir
70 |   end
71 | 
72 |   def doing(*args)
73 |     doing_with_env({ 'DOING_CONFIG' => @config_file, 'DOING_BACKUP_DIR' => @backup_dir }, '--doing_file', @wwid_file,
74 |                    *args)
75 |   end
76 | end
77 | 


--------------------------------------------------------------------------------
/docs/doc/GLI.html:
--------------------------------------------------------------------------------
  1 | 
  2 | 
  3 |   
  4 |     
  5 | 
  6 | 
  7 |   Module: GLI
  8 |   
  9 |     — Documentation by YARD 0.9.37
 10 |   
 11 | 
 12 | 
 13 |   
 14 | 
 15 |   
 16 | 
 17 | 
 21 | 
 22 | 
 23 |   
 24 | 
 25 |   
 26 | 
 27 | 
 28 |   
 29 |   
 30 |     
 34 | 
 35 |     
36 | 61 | 62 |

Module: GLI 63 | 64 | 65 | 66 |

67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 |
Defined in:
81 |
lib/doing/help_monkey_patch.rb,
82 | lib/doing/markdown_document_listener.rb
83 |
84 |
85 | 86 |
87 | 88 |

Defined Under Namespace

89 |

90 | 91 | 92 | Modules: Commands 93 | 94 | 95 | 96 | 97 |

98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
108 | 109 | 114 | 115 |
116 | 117 | -------------------------------------------------------------------------------- /test/doing_unit-item_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'helpers/doing-helpers' 4 | require 'test_helper' 5 | 6 | $LOAD_PATH.unshift File.join(__dir__, '..', 'lib') 7 | require 'doing' 8 | require 'doing/item/item' 9 | # require 'gli' 10 | 11 | # Tests for Item class 12 | class DoingItemTest < Test::Unit::TestCase 13 | include DoingHelpers 14 | include Doing 15 | 16 | def setup 17 | @wwid = WWID.new 18 | end 19 | 20 | def teardown; end 21 | 22 | # TODO: tests for duration, interval, end_date, overlapping_time?, tags?, tag_values? 23 | 24 | def test_tag_item 25 | item = Item.new(Time.now - 3600, "Test item @done(#{(Time.now - 1200).strftime('%F %R')})", @wwid.current_section) 26 | item.tag(%w[testtag1 testtag2]) 27 | assert(item.tags?(%w[testtag1 testtag2], :and), 'Item should have both tags') 28 | item.tag(['testtag2'], remove: true) 29 | assert_equal(false, item.tags?(%w[testtag1 testtag2], :and), 'Item should not have both tags') 30 | end 31 | 32 | def test_search_item 33 | item = Item.new(Time.now - 3600, "Test item with search string @done(#{(Time.now - 1200).strftime('%F %R')})", 34 | @wwid.current_section) 35 | assert(item.search('search string'), 'Item should match search string') 36 | assert(item.search('/s.*?ch s.*?g/'), 'Item should match regex query') 37 | assert_equal(false, item.search('Search String', case_type: :smart), 'Item should not match case') 38 | assert(item.search('string search'), 'Pattern matching should work') 39 | end 40 | 41 | def test_value_comparison 42 | item = Item.new(Time.now - 3600, 43 | 'Test item with search string @tag1(50%) @tag2(2021-03-03 12:00) @tag3(string value)', @wwid.current_section, ['note content']) 44 | assert(item.tag_values?(['tag1 > 25']), 'Item should match value comparison') 45 | assert_equal(false, item.tag_values?(['tag1 < 25']), 'Item should not match value comparison') 46 | 47 | assert(item.tag_values?(['tag2 < 2021-03-04']), 'Item should match date comparison') 48 | assert_equal(false, item.tag_values?(['tag2 < 2021-03-01']), 'Item should not match date comparison') 49 | 50 | assert(item.tag_values?(['tag3 ^= string']), 'Item should match string comparison') 51 | assert_equal(false, item.tag_values?(['tag3 $= testing']), 'Item should not match string comparison') 52 | end 53 | 54 | def test_move_item 55 | section = 'Test Section' 56 | item = Item.new(Time.now - 3600, "Test item with search string @done(#{(Time.now - 1200).strftime('%F %R')})", 57 | @wwid.current_section) 58 | item.move_to(section, label: true, log: false) 59 | assert_equal(section, item.section, 'Section should match') 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/doing/items/sections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | class Items < Array 5 | # List sections, title only 6 | # 7 | # @return [Array] section titles 8 | # 9 | def section_titles 10 | @sections.map(&:title) 11 | end 12 | 13 | # Test if section already exists 14 | # 15 | # @param section [String] section title 16 | # 17 | # @return [Boolean] true if section exists 18 | # 19 | def section?(section) 20 | section = section.is_a?(Section) ? section.title.downcase : section.downcase 21 | @sections.map { |i| i.title.downcase }.include?(section) 22 | end 23 | 24 | ## 25 | ## Return the best section match for a search query 26 | ## 27 | ## @param frag The search query 28 | ## @param distance The distance apart characters can be (fuzziness) 29 | ## 30 | ## @return [Section] (first) matching section object 31 | ## 32 | def guess_section(frag, distance: 2) 33 | section = nil 34 | re = frag.to_rx(distance: distance, case_type: :ignore) 35 | @sections.each do |sect| 36 | next unless sect.title =~ /#{re}/i 37 | 38 | Doing.logger.debug('Match:', %(Assuming "#{sect.title}" from "#{frag}")) 39 | section = sect 40 | break 41 | end 42 | 43 | section 44 | end 45 | 46 | # Add a new section to the sections array. Accepts 47 | # either a Section object, or a title string that will 48 | # be converted into a Section. 49 | # 50 | # @param section [Section] The section to add. A 51 | # String value will be converted to 52 | # Section automatically. 53 | # @param log [Boolean] Add a log message 54 | # notifying the user about the 55 | # creation of the section. 56 | # 57 | # @return nothing 58 | # 59 | def add_section(section, log: false) 60 | section = section.is_a?(Section) ? section : Section.new(section.cap_first) 61 | 62 | return if section?(section) 63 | 64 | @sections.push(section) 65 | Doing.logger.info('New section:', %("#{section}" added)) if log 66 | end 67 | 68 | def delete_section(section, log: false) 69 | return unless section?(section) 70 | 71 | raise DoingRuntimeError, 'Section not empty' if in_section(section).count.positive? 72 | 73 | @sections.each do |sect| 74 | next unless sect.title == section && in_section(sect).count.zero? 75 | 76 | @sections.delete(sect) 77 | Doing.logger.info('Removed section:', %("#{section}" removed)) if log 78 | end 79 | 80 | Doing.logger.error('Not found:', %("#{section}" not found)) 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /doing.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Ensure we require the local version and not one we might have installed already 4 | require './lib/doing/version' 5 | Gem::Specification.new do |s| 6 | s.name = 'doing' 7 | s.version = Doing::VERSION 8 | s.author = 'Brett Terpstra' 9 | s.email = 'me@brettterpstra.com' 10 | s.homepage = 'http://brettterpstra.com/project/doing/' 11 | s.platform = Gem::Platform::RUBY 12 | s.summary = 'A command line tool for managing What Was I Doing reminders' 13 | s.description = [ 14 | 'A tool for managing a TaskPaper-like file of recent activites.', 15 | 'Perfect for the late-night hacker on too much caffeine to remember', 16 | 'what they accomplished at 2 in the morning.' 17 | ].join(' ') 18 | s.license = 'MIT' 19 | s.files = `git ls-files -z`.split("\x0").reject { |f| f.strip =~ %r{^((test|spec|features)/|\.git|buildnotes)} } 20 | s.require_paths << 'lib' 21 | s.extra_rdoc_files = ['README.md'] 22 | s.rdoc_options << '--title' << 'doing' << '--main' << 'README.md' << '--markup' << 'markdown' 23 | s.bindir = 'bin' 24 | s.executables << 'doing' 25 | s.add_development_dependency('github-markup', '~> 4.0', '>= 4.0.0') 26 | s.add_development_dependency('parallel_tests', '~> 3.7', '>= 3.7.3') 27 | s.add_development_dependency('rake', '~> 13.0', '>= 13.0.1') 28 | s.add_development_dependency('rdoc', '~> 6.3.1') 29 | s.add_development_dependency('redcarpet', '~> 3.5', '>= 3.5.1') 30 | s.add_development_dependency('test-unit', '~> 3.4.4') 31 | s.add_development_dependency('tty-spinner', '~> 0.9', '>= 0.9.3') 32 | s.add_development_dependency('yard', '~> 0.9', '>= 0.9.36') 33 | s.add_runtime_dependency('base64', '~> 0.2') 34 | s.add_runtime_dependency('chronic', '~> 0.10', '>= 0.10.2') 35 | s.add_runtime_dependency('csv', '~> 3.3') 36 | s.add_runtime_dependency('deep_merge', '~> 1.2', '>= 1.2.1') 37 | s.add_runtime_dependency('gli', '~> 2.20', '>= 2.20.1') 38 | s.add_runtime_dependency('haml', '~>5.0.0', '>= 5.0.0') 39 | s.add_runtime_dependency('logger', '~> 1.4', '>= 1.4.2') 40 | s.add_runtime_dependency('ostruct', '~> 0.6') 41 | s.add_runtime_dependency('parslet', '~> 2.0', '>= 2.0.0') 42 | s.add_runtime_dependency('plist', '~> 3.6', '>= 3.6.0') 43 | s.add_runtime_dependency('reline', '~> 0.6') 44 | s.add_runtime_dependency('safe_yaml', '~> 1.0') 45 | s.add_runtime_dependency('tty-link', '~> 0.1', '>= 0.1.1') 46 | s.add_runtime_dependency('tty-markdown', '~> 0.7', '>= 0.7.0') 47 | s.add_runtime_dependency('tty-progressbar', '~> 0.18', '>= 0.18.2') 48 | s.add_runtime_dependency('tty-reader', '~> 0.9', '>= 0.9.0') 49 | s.add_runtime_dependency('tty-screen', '~> 0.8', '>= 0.8.1') 50 | s.add_runtime_dependency('tty-which', '~> 0.5', '>= 0.5.0') 51 | 52 | # s.add_runtime_dependency('amatch', '~> 0.4', '>= 0.4.0') 53 | end 54 | -------------------------------------------------------------------------------- /lib/doing/item/tags.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Doing 4 | # A Doing entry 5 | module ItemTags 6 | ## 7 | ## Add (or remove) tags from the title of the item 8 | ## 9 | ## @param tags [Array] The tags to apply 10 | ## @param options Additional options 11 | ## 12 | ## @option options :date [Boolean] Include timestamp? 13 | ## @option options :single [Boolean] Log as a single change? 14 | ## @option options :value [String] A value to include as @tag(value) 15 | ## @option options :remove [Boolean] if true remove instead of adding 16 | ## @option options :rename_to [String] if not nil, rename target tag to this tag name 17 | ## @option options :regex [Boolean] treat target tag string as regex pattern 18 | ## @option options :force [Boolean] with rename_to, add tag if it doesn't exist 19 | ## 20 | def tag(tags, **options) 21 | added = [] 22 | removed = [] 23 | 24 | date = options.fetch(:date, false) 25 | options[:value] ||= date ? Time.now.strftime('%F %R') : nil 26 | options.delete(:date) 27 | 28 | single = options.fetch(:single, false) 29 | options.delete(:single) 30 | 31 | tags = tags.to_tags if tags.is_a? ::String 32 | 33 | remove = options.fetch(:remove, false) 34 | tags.each do |tag| 35 | if tag =~ /^(\S+)\((.*?)\)$/ 36 | m = Regexp.last_match 37 | tag = m[1] 38 | options[:value] ||= m[2] 39 | end 40 | 41 | bool = remove ? :and : :not 42 | if tags?(tag, bool) || options[:value] 43 | @title = @title.tag(tag, **options).strip 44 | remove ? removed.push(tag) : added.push(tag) 45 | end 46 | end 47 | 48 | Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single) 49 | 50 | self 51 | end 52 | 53 | ## 54 | ## Get a list of tags on the item 55 | ## 56 | ## @return [Array] array of tags (no values) 57 | ## 58 | def tags 59 | @title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq 60 | end 61 | 62 | ## 63 | ## Return all tags including parenthetical values 64 | ## 65 | ## @return [Array] Array of array pairs, 66 | ## [[tag1, value], [tag2, value]] 67 | ## 68 | def tags_with_values 69 | @title.scan(/(?<= |\A)@([^\s(]+)(?:\((.*?)\))?/).map { |tag| [tag[0], tag[1]] }.sort.uniq 70 | end 71 | 72 | ## 73 | ## convert tags on item to an array with @ symbols removed 74 | ## 75 | ## @return [Array] array of tags 76 | ## 77 | def tag_array 78 | tags.tags_to_array 79 | end 80 | 81 | private 82 | 83 | def split_tags(tags) 84 | tags.to_tags.tags_to_array 85 | end 86 | end 87 | end 88 | --------------------------------------------------------------------------------