791cd547ae
It's not fully hooked up to the react app, and it's not quite complete either, but it's a start. Currently, it will parse filter implicitly, and there's also the done and add command. For now, all commands must place the command name in the right location. Additionally, I've hooked up the react table's selection state to the command parser. Filter based on _ids_ explicitly does work. Manually selecting an ID will also place it in the input field for discoverability. It does not remove ids that were manually deselected yet, nor does filter based on any other attribute work. The command parser also parses times, but it does so very poorly currently. More work will need to go into that, for now it's basically a stub. I'll also need to convert the parser to Go. The react table conviently handles virtual tables based on it's internal `row.id`. I'm still wondering how to implement virtual ids, but at least on the web app, they are constantly updated since the view is always visible, which is different to the CLI (though perhaps the same as a TUI?) Notably, there's no semantics for the difference between selection and filtering. I think perhaps the implicit filter command should default to selection, but the explicit filter command should be handled differently, and actually execute filtering. I haven't yet looked into how the TanStack Table handles filtering, but I am aware that it does. I think for an MVP, once done is implemented, I can deploy this, the web socket server, and try and start using it.
144 lines
3.1 KiB
Plaintext
144 lines
3.1 KiB
Plaintext
{
|
|
function makeCommand(type, filters, parts) {
|
|
|
|
const validParts = parts || [];
|
|
// Extract pure description (without attributes)
|
|
const description = validParts
|
|
.filter(part => typeof part === 'string')
|
|
.join(' ')
|
|
.trim();
|
|
|
|
// Collect all attributes
|
|
const attributes = validParts
|
|
.filter(part => typeof part === 'object')
|
|
.filter(part => part !== null);
|
|
|
|
// Store original parts for reconstruction
|
|
const originalParts = validParts.filter(part => part !== null);
|
|
|
|
|
|
return {
|
|
type: type,
|
|
filters: filters.flat() || [],
|
|
description: description || [],
|
|
attributes: attributes || [],
|
|
parts: parts || [],
|
|
reconstruct: function() {
|
|
const filterStr = this.filters.map(f => f.reconstruct()).join(',');
|
|
const partsStr = this.parts.map(p => p.reconstruct()).join(' ');
|
|
return [filterStr, this.type, partsStr].filter(Boolean).join(' ');
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
Start = AddCommand / DoneCommand / ExplicitFilterCommand / ImplicitFilterCommand
|
|
|
|
// ADD COMMAND
|
|
AddCommand = "add" _ parts:(Part / _)+ EOF {
|
|
return makeCommand('add', null, parts.filter(p => p !== null));
|
|
}
|
|
|
|
// DONE COMMAND
|
|
DoneCommand = filters:Filters _ "done" EOF {
|
|
return makeCommand('done', filters, null);
|
|
}
|
|
|
|
ExplicitFilterCommand = filters:Filters _ "filter" _* moreFilters:Filters* EOF {
|
|
return makeCommand('filter', [...filters, ...moreFilters], null);
|
|
}
|
|
|
|
ImplicitFilterCommand = filters:Filters+ EOF {
|
|
return makeCommand('filter', filters, null);
|
|
}
|
|
|
|
Filters = first:Filter rest:(_ Filter)* {
|
|
return [first, ...rest.map(r => r[1])];
|
|
}
|
|
|
|
|
|
IdRange = start:Integer "-" end:Integer {
|
|
const ids = [];
|
|
for (let i = start; i <= end; i++) {
|
|
ids.push(i);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
SingleId = id:Integer {
|
|
return [id];
|
|
}
|
|
|
|
Filter = IdFilter / Tag / Project / Priority / Due
|
|
|
|
IdFilter = first:(IdRange / SingleId) rest:("," (IdRange / SingleId))* trailing:"," ? {
|
|
const ids = [first, ...rest.map(r => r[1])].flat();
|
|
return {
|
|
type: 'id',
|
|
ids: ids,
|
|
reconstruct: function() { return this.ids.join(','); }
|
|
};
|
|
}
|
|
|
|
Part = Attribute / TextPart
|
|
|
|
TextPart = chars:Word {
|
|
return {
|
|
type: "text",
|
|
value: chars,
|
|
reconstruct: function() { return this.value; }
|
|
};
|
|
}
|
|
|
|
Attribute = Due / Tag / Project / Priority
|
|
|
|
Due = "@" value:TimeValue {
|
|
return {
|
|
type: "due",
|
|
value: value,
|
|
reconstruct: function() { return `@${this.value}`; }
|
|
};
|
|
}
|
|
|
|
Project = ("pro:" / "project:" / "+") value:Word {
|
|
return {
|
|
type: "project",
|
|
value: value,
|
|
reconstruct: function() { return `+${this.value}`; }
|
|
};
|
|
}
|
|
|
|
Priority = "priority:" value:[HML] {
|
|
return {
|
|
type: "priority",
|
|
value: value,
|
|
reconstruct: function() { return `priority:${this.value}`; }
|
|
};
|
|
}
|
|
|
|
Tag = "#" value:Word {
|
|
return {
|
|
type: "tag",
|
|
value: value,
|
|
reconstruct: function() { return `#${this.value}`; }
|
|
};
|
|
}
|
|
|
|
Integer = digits:[0-9]+ {
|
|
return parseInt(digits.join(''), 10);
|
|
}
|
|
|
|
TimeValue = chars:[0-9:]+ ("am" / "pm")? {
|
|
return chars.join('') + (text() || '');
|
|
}
|
|
|
|
Word = chars:[a-zA-Z0-9_-]+ {
|
|
return chars.join('');
|
|
}
|
|
|
|
_ = [ \t]+ {
|
|
return null;
|
|
}
|
|
|
|
EOF = !.
|