File List
25 | 40 | 41 |-
48 |
49 |
50 |
- 51 | 52 | 53 | 54 | 55 | 56 |
├── 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 |={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 |
37 |
45 |
46 |
47 |
48 |
50 |
51 |
56 |
57 |
58 |
59 |
60 |
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 |
--------------------------------------------------------------------------------