Querying grouping & sorting across several text files with the FoldingText Command Line Interface

Running this quickly through Jesse’s parser, it looks as though notes following a todo bullet are parsed as peers of the list item rather than children.

# Header1.todo
- task @next @read
	my note about the task
- my second task @read


# Header2
- task @next @read
		my note about the task
- my second task @read


┌ Tree
┠┰∅ [0, root]
┇┠ ∅ [1, empty]
┇┠┰# Header1.todo [2, heading]
┇┇┠ - task @next @read [3, unordered]
┇┇┠ ▸my note about the task [4, body]
┇┇┠ - my second task @read [5, unordered]
┇┠ ∅ [6, empty]
┇┠ ∅ [7, empty]
┇┠┰# Header2 [8, heading]
┇┇┠┰- task @next @read [9, unordered]
┇┇┇┠ ▸▸my note about the task [10, body]
┇┇┠ - my second task @read [11, unordered]
┇┇┠ ∅ [12, empty]

I’ll experiment with adding a @note property which harvests immediately following nodes which are body or empty

(editor.tree().toString() is a very helpful function in the FoldingText scripting API incidentally – it lets us look directly at the structure of the underlying tree):

property pstrJS : "
	function(editor, options) {return editor.tree().toString()}
"

on run
	set varResult to missing value
	tell application "FoldingText"
		tell (make new document with properties {text contents:"
# Header1.todo
- task @next @read
	my note about the task
- my second task @read


# Header2.todo
- task @next @read
		my note about the task
- my second task @read
"})
			
			set varResult to (evaluate script pstrJS)
		end tell
	end tell
	return varResult
end run

Cool, thanks! Looking forward to the results.

I’m still just tinkering with cut-and-paste with this (no formal javascript knowledge yet), so digging into Jesse’s tools are a bit out of reach yet. That’s why I appreciate your examples for this plugin so much–they’re rich and easy enough to learn from.

I’ll try to put out a fuller set.

In the meanwhile, Jesse has explained to me that notes are recognised as children of todo items when they have an extra tab indent (I misinterpreted what the tree diagram was showing in the case with an extra tab).

If you see nodePath searching as something which might become more central to your workflow, that means that you might well get some benefits from indenting your notes one tab more deeply under the list items.

(For the purposes of a @note attribute for the getAttrib() function for reports, we can make it recognise either layout, but might be worth bearing in mind that bigger queries (multiple files) are likely to run a little faster if note-children are made immediately recognisable by a further tab of indentation)

PS I forgot, of course, the stronger and more immediate argument for an extra tab to mark notes clearly as children of list items, rather than simply successors/peers

If you add one more tab, to mark them as children, ⌘/ collapsing of the notes under an item will then work properly.

Hmm. That’s interesting. I wonder why a note needs to be “double” indented to be recognized as a child? I thought I remembered at one point, notes would “fold up” to the todo item as long as they were indented under it–it wasn’t necessary to tab twice.

At any rate, it’s not a problem to double indent–just need to get in the habit.

It arises, I think, from the report-writing context in which bullet lists tend to be supporting material nested within body text.

If you look at FoldingText and Sublime displaying the same file, you can see that FoldingText’s rather clearer display of report structure involves that assumption, and grants a free (implicit) indent to bullet items.

In FT (1st of the following two images) the first tab brings a body para down to the same level as a bullet, and the second tab pushes it down to a lower level. (The 2nd shot, of Sublime, shows where the tabs are):

FoldingText:

Sublime:

(The payoff of the FoldingText approach is that a bullet list can always be folded under the preceding body text):

Ah, right. That payoff is worth it. Thanks for the explanation.

Query help?

I’m trying to write a node path query that returns only tasks with start dates within the next 7 days–no tasks prior to today and no tasks after today + 7. Here’s what works in the Filter plugin:

//@start <= "2014-08-06" except //@start < "2014-07-31"

However, this doesn’t work in the report, at least as much as I can figure. Here’s what I’ve got, which returns no results:

"Started (with ftdoc:// links)": {
		"title": "# Starting in the Next 7 Days",
		"for": "$item in //@start <= {today + 7d} except //@start < {today}",
		"let": "$day = fn:daypart($item@start)",
		"groupby": "$day",
		"orderby": "$day",
		"return": [
			"## {$day}",
			{
				"for": "$i in $item",
				"let": "$project = $item@parent",
				"groupby": "$project",
				"return": [
					"### {$project}.todo",
					{
						"for": "$j in $i",
						"return": "- {$j} :: [Link]({$j@url}) {$j@tags}",
					},
					""
				]
			},
			""
		]

	},

If possible, I’d also like to filter out @done items from this as well. I tried to add that, but couldn’t figure out the right query, even with the Filter plugin.

By the way, I’ve added my ongoing query work to a new branch of fork of Rob’s repository:

http://github.com/derekvan/txtquery-tools/tree/derek-new-queries

I’ve continued to work on this, but haven’t gotten to a solution yet. This query works in the filter panel, but not via the script:

(//@start >= "2014-08-06" intersect //@start <= "2014-08-13") except //@done : works

returns no results:

"Started (with ftdoc:// links)": {
		"title": "# Starting in the Next 7 Days",
		"for": "$item in //@start >= {today} intersect //@start <= {today + 7d} except //@done",
		"let": "$day = fn:daypart($item@start)",
		"groupby": "$day",
		"orderby": "$day",
		"return": [
			"## {$day}",
			{
				"for": "$i in $item",
				"let": "$project = $item@parent",
				"groupby": "$project",
				"return": [
					"### {$project}.todo",
					{
						"for": "$j in $i",
						"return": "- {$j} :: [Link]({$j@url}) {$j@tags}",
					},
					""
				]
			},
			""
		]

	},

I think the issue there is that you are trying to group elements by a property which they don’t all share. ($day is defined as a function of @start, but only a subset of the initially harvested lines have that property).

Your example is very useful, however, as an indication of where you are trying to get to, and I’ll look for a generalisation which can do that for you. I’ve written a @note property, but it looks as if you need fuller access, in specifying descendant lines, to the broader nodePath syntax.

Thanks!

I’m not sure if I’m on the right track here, but I guess more broadly I’m wondering if it’s possible via these queries to return bounded results within larger sets. I think the principle here is generalizable to many other queries:

All the nodes with start dates between today and today+7 days (exclude those before and after those days)
All the nodes with @mins between 60 and 90 (don’t return any less or more than that)
All the nodes with @percent between 30 and 50
etc.

Or maybe I’m missing the point here? At any rate, would love to figure out how to make these types of queries via this script.

Here’s a basic range query – starting in the next 7 days

(Note that a single filter with a boolean operator is simpler than a couple of filters with a set operator)

"Starting in the next 7 days": {
	"title": "# Starting in the Next 7 Days",
	"for": "$item in //@start <= {today + 7d} and @start < {today}",
	"let": "$day = fn:daypart($item@start)",
	"groupby": "$day",
	"orderby": "$day",
	"return": [
		"## fn:format_date({$day}, [FNn] [D] [MNn] [Y])",
		{
			"for": "$i in $item",
			"let": "$project = $item@parent",
			"groupby": "$project",
			"return": [
				"### {$project}.todo",
				{
					"for": "$j in $i",
					"return": "- {$j} :: [Link]({$j@url}) {$j@tags}"
				},
				""
			]
		},
		""
	]
}

This query works in the filter panel, but not via the script

I think the problem was just that your JSON expression wasn’t parsing – there was a redundant comma which would have thrown it (at the end of your innermost return)

"return": "- {$j} :: [Link]({$j@url}) {$j@tags}",
}

(Inside the {curly brackets } of a JSON object you need commas to separate the key:value pairs, but not between the final pair and the closing } )

Hi Rob,

I’ve installed txtquery.sh & related and even tried the example txt query.sh from Keyboard Maestro. It seems already OK, but I have no idea about how to exploit txtquery.sh to query across files. I know little about shell command. It’s appreciated if you could say more about it.

Henry

I’ll be pushing out more documentation with a new version of txtQuery in the next 10 days (conditional strings and formatting, reports made up of more than one query, better set of basic report templates). At some point I’ll also wrap a dialog around it.

In the meanwhile, line 140 of txtQuery.sh (I’ll bring it up to the top in the next version) there is a section called # Inputs
The simplest way to specify a set of files is to make them share the first part of the filename, and then use a * wildcard/‘glob’ character to mean ‘followed by any sequence of characters’

For example:

DEFAULTCOLLECTION="~/Library/Application Support/Notational Velocity/project*"

is querying across all of the files in my Notational Velocity text file folder with names which begin with the word 'project'.

I’m not personally using much CJK or RTL in my text file names, so let me know if you encounter any puzzles there. You can check a particular pattern by:

  • opening Terminal.app,
  • changing directory to the folder which contains your text files (with the cd command)
  • and using the ls file listing command with your filenamestart + * pattern.

(e.g. where "$HOME" and ~ represent your user/home folder: