From d7061f10b2ddc7359f769f1f06c889f4daab898e Mon Sep 17 00:00:00 2001 From: Johan Vromans Date: Fri, 2 Oct 2026 15:32:15 +0200 Subject: [PATCH 1/2] Quick Commands rewrite. --- quick-commands/README.md | 79 +++++++++ quick-commands/quick-commands.qml | 274 ++++++++++++++++++++---------- 2 files changed, 264 insertions(+), 89 deletions(-) create mode 100644 quick-commands/README.md diff --git a/quick-commands/README.md b/quick-commands/README.md new file mode 100644 index 0000000..89adaca --- /dev/null +++ b/quick-commands/README.md @@ -0,0 +1,79 @@ +# `jv-quick-commands` + +This is an augmented re-implementation of the QOwnNotes `quick-commands` script. + +## How does it work + +When this script is enabled, you can use the QOwnNotes auto-completion feature with a number of predefined completions. + +| key | value | +| :----------- | :----------------------------------- | +| `\today` | Formatted current date and time | +| `\tomorrow` | Formatted date and time of tomorrow | +| `\yesterday` | Formatted date and time of yesterday | +| `\week` | Formatted week | +| `\now` | 2026-10-01T17:04:01 | + +For date and time, a popup will allow choosing one of: +* `01-10` (day month) +* `01-10-2026` (day month year) +* `2026-10-01` (year month day) +* `2026-10-01T17:09:59` (ISO 8601) +* `Thursday 1 October 2026 17:10:17` + +For week, a popup will allow choosing one of: +* `40-2026` (ISO week number and year) +* `w40-2026` (ISO week number and year) + +## Custom completions + +In the script settings dialog, you can add your own completions. For example: +``` +app qownnotes +``` +Then `\app` can be completed to `qownnotes`. Multiple completions can be given as space separated words. If a completion must embed spaces you can enclose it with `""`, for example: +``` +thisapp qownnotes "QOwnNotes 26.9" +``` +Case matters: +``` +App qownnotes +``` +Now `\App` will complete to `Qownnotes`. Yes, you see that good, if +the command starts with an uppercase letter the result will have its +first letter uppercased too. + +### Placeholders + + In the completions you can use a number of _placeholders_ to obtain substitutions for actual date and time values (table adapted from [QOwnNotes](https://www.qownnotes.org/scripting/methods-and-objects.html#formatting-dates-and-times): + +| Placeholder | Meaning | Example | +| :----------- | :---------------------|:-------------- | +| `{yyyy}` | Year with four digits | 2026 | +| `{yy}` | Year with two digits | 26 | +| `{MM}` | Month with leading zero | 09 | +| `{M}` | Month without leading zero | 9 | +| `{MMM}` | Abbreviated localized month name | Sep | +| `{MMMM}` | Full localized month name | September | +| `{dd}` | Day with leading zero | 05 | +| `{d}` | Day without leading zero | 5 | +| `{ddd}` | Abbreviated localized day name | Tue | +| `{dddd}` | Full localized day name | Tuesday | +| `{HH}` | Hour (0-23) with leading zero | 07 | +| `{hh}` | Hour with leading zero (1-12 with AP) | 07 | +| `{mm}` | Minute with leading zero | 04 | +| `{ss}` | Second with leading zero | 09 | +| `{AP}` / `{ap}` | AM/PM or am/pm | AM | +| `{w}` | ISO week number without leadign zero | 5 | +| `{ww}` | ISO week number with leadign zero | 05 | +| `{LC:locale}` | Use the given locale | `{LC:nl_NL}`| +| `{+:offset}` | Offsets the date to the future | `{+:1D}` | +| `{-:offset}` | Offsets the date to the past | `{-:1D}` | + +The date offsets may be a number of days, as shown above, or a number of milliseconds. `1D` is equivalent to `86400000`. + +For example, the dutch version of tomorrows full date/time: + +``` +morgen "{lc:nl_NL}{+:1D}{dddd} {d} {MMMM} {yyyy} {hh}:{mm}:{ss}" +``` diff --git a/quick-commands/quick-commands.qml b/quick-commands/quick-commands.qml index 398e2e6..554e6ea 100644 --- a/quick-commands/quick-commands.qml +++ b/quick-commands/quick-commands.qml @@ -1,104 +1,220 @@ +// -*- mode: js2 -*- import QtQml 2.0 import QOwnNotesTypes 1.0 import "command-parser.js" as CommandParser /** - * This script creates some easy to access commands that leverage the autocomplete functionalities to add more predefined - * strings or formatted dates + * This script creates some easy to access commands that leverage the + * autocomplete functionalities to add more predefined strings or + * formatted dates. + * + * Original author: @LockeBirdsey + * + * Main differences with the original quick-commands script is that + * date and time values are substituted dynamically and can be part of + * custom quickies as well. + * It also uses Qt functions for date/time calculations, see + * https://www.qownnotes.org/scripting/methods-and-objects.html#formatting-dates-and-times, + * with added support for w and ww (ISO week numbers). * */ Script { - readonly property string _DDDDMY: "ddd (DD-MM-YYYY)" - readonly property string _DDMM: "DD-MM" - readonly property string _DDMMYYYY: "DD-MM-YYYY" - readonly property string _FULL: "YYYY-MM-DDTHH:mm:ss" - readonly property int _MILLI_DAY: 86400000 - readonly property variant _SHORT_DAYS_EN: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] - readonly property string _WEEKWWYYYY: "wWW-YYYY" - readonly property string _WWYYYY: "WW-YYYY" - readonly property string _YYYYMMDD: "YYYY-MM-DD" - property var commands - property string customCommands + + // For settings dialog. property variant settingsVariables: [ { - "identifier": "customCommands", - "name": "Custom Commands", - "description": "Custom quick commands. Each line starts with a command name followed by replacement values separated by spaces. Enclose multi-word values in double quotes. For example: 'myName first \"first last\" last-first'", - "type": "text", - "default": "" + identifier: "customCommands", + name: "Custom Commands", + description: "Custom quick commands. Each line starts with a command name followed by replacement values separated by spaces. Enclose multi-word values in double quotes. For example: 'myName first \"first last\" last-first'. Date and time values can be substituted with {dd}, {MM} and so on.", + type: "text", + default: "" } - ] + ]; + property string customCommands; + + // Common date formats for today and friends. + readonly property string _DDMM: "{dd}-{MM}"; + readonly property string _DDMMYYYY: "{dd}-{MM}-{yyyy}"; + readonly property string _FULL: "{yyyy}-{MM}-{dd}T{HH}:{mm}:{ss}"; + readonly property string _YYYYMMDD: "{yyyy}-{MM}-{dd}"; + readonly property string _VERYFULL: "{dddd} {d} {MMMM} {yyyy} {HH}:{mm}:{ss}"; + + // Common date formats for week and friends. + readonly property string _WEEKWWYYYY: "w{ww}-{yyyy}"; + readonly property string _WWYYYY: "{ww}-{yyyy}"; + + // Command list is static, runtime values are substituted at completion time. + property var commands: initCommands(); function autocompletionHook() { - var word = script.noteTextEditCurrentWord(true); + const word = script.noteTextEditCurrentWord(true); + if (!word.startsWith("\\")) { return []; } - // Have to re-init since reloading the script engine/restarting might not happen daily - reInitCommands(); + const command = word.substr(1); - var command = word.substr(1); + // Find the possible candidates. + let availableCommands = []; - var availableCommands = commands[command]; - if (availableCommands == null || availableCommands.length === 0) { + // script.log("command: " + command); + const av = commands[command]; + if ( av == null || av.length === 0 ) { return []; } + // script.log("commands: " + av); + + // Substitute actual values. + const ucfirst = command.match( /^[A-Z]/ ); // should use POSIX + for ( let i = 0; i < av.length; i++ ) { + if ( ucfirst ) { + availableCommands.push(capitalizeFirstLetter(substitute(av[i]))); + } + else { + availableCommands.push(substitute(av[i])); + } + } // QOwnNotes replaces only word characters during completion, so remove // the command prefix separately to keep it out of the result. - var cursorPosition = script.noteTextEditCursorPosition(); - var commandStart = cursorPosition - word.length; + const cursorPosition = script.noteTextEditCursorPosition(); + const commandStart = cursorPosition - word.length; script.noteTextEditSetSelection(commandStart, commandStart + 1); script.noteTextEditWrite(""); script.noteTextEditSetCursorPosition(cursorPosition - 1); return availableCommands; } - function buildTimeList(date) { - var timeList = []; - timeList.push(formatDate(date, _DDMM)); - timeList.push(formatDate(date, _YYYYMMDD)); - timeList.push(formatDate(date, _DDMMYYYY)); - timeList.push(formatDate(date, _DDDDMY)); - timeList.push(formatDate(date, _FULL)); - return timeList; + + // Initialze the list of commands. + function initCommands() { + var commands = { + today: buildList(0), + tomorrow: buildList(1), + yesterday: buildList(-1), + week: [ _WWYYYY, _WEEKWWYYYY ], + now: [ _FULL ], + }; + + var customRows = customCommands.split("\n"); + for (let i = 0; i < customRows.length; i++) { + var customCommand = CommandParser.parseCommandLine(customRows[i]); + if (customCommand === null) { + continue; + } + + commands[customCommand.name] = customCommand.values; + } + + return commands; + } + + // Build the list of candidates (for initCommands()). + function buildList(offset) { + + // If it is not today, add a magic modifier item. + let mod = ""; + if ( offset < 0 ) { + offset = -offset; + mod = "{-:" + offset.toString() + "D}"; + } + else if ( offset > 0 ) { + mod = "{+:" + offset.toString() + "D}"; + } + + return [ + mod + _DDMM, + mod + _YYYYMMDD, + mod + _DDMMYYYY, + mod + _FULL, + mod + _VERYFULL + ]; } - // Taken from https://github.com/qownnotes/scripts/blob/main/journal-entry/journal-entry.qml - function formatDate(date, format) { - let day = date.getDate(); - let dayOfWeek = _SHORT_DAYS_EN[date.getDay()]; - let month = date.getMonth() + 1; //getMonth() returns 0-11 so we must add 1 - let week = getWeekNumber(date); - let year = date.getFullYear(); - let hours = date.getHours(); - let minutes = date.getMinutes(); - let seconds = date.getSeconds(); - - // If day and month are less than 10, add a leading zero - day = (day < 10) ? '0' + day : day; - month = (month < 10) ? '0' + month : month; - week = (week < 10) ? '0' + week : week; - hours = (hours < 10) ? '0' + hours : hours; - minutes = (minutes < 10) ? '0' + minutes : minutes; - seconds = (seconds < 10) ? '0' + seconds : seconds; - - // Replace format placeholders by actual values - format = format.replace('WW', week); - format = format.replace('MM', month); - format = format.replace('DD', day); - format = format.replace('ddd', dayOfWeek); - format = format.replace('YYYY', year); - format = format.replace('HH', hours); - format = format.replace('mm', minutes); - format = format.replace('ss', seconds); - - return format; + // Perform the substitutions. + function substitute(format) { + + // Break out the {...} elements. + let m = format.split( /(\{.*?\})/g ); + if ( m.length == 1 ) { + return format; // nothing to substitute + } + // script.log("split: " + m + " (" + m.length + " matches)"); + + let date = new Date(); + let locale; + let result = ""; + + for( let i = 0; i < m.length; i++ ) { + let arg = m[i]; + // script.log("arg" + i + ": " + arg); + + // Is it a substitution element? + if ( arg.startsWith("{") && arg.endsWith("}") ) { + let target = arg.slice(1,-1); + + // Check for {LC:locale} or {+:offset}. + if ( target.indexOf(":") > 0 ) { + // script.log("opt: " + target); + let x = target.split(":"); + // script.log("x:" + x); + + if ( x.length == 2 ) { // ctrl:value + let ctrl = x[0]; + let value = x[1]; + + if ( ctrl.toLowerCase() == "lc" ) { + // Set a locale for substitutions. + locale = Qt.locale(value); + script.log("locale:" + locale); + continue; + } + + if ( ctrl == "+" || ctrl == "-" ) { + // Date offset, millisecs or nnD. + let now = new Date().getTime(); + if ( value.endsWith("D") ) { + value = parseInt(value) * 24*60*60*1000; + } + else { + value = parseInt(value); + } + date = new Date( now + value ); + //script.log("date " + date); + continue; + } + } + console.error("Invalid substitution control: " + m[0]); + continue; // makes sense? + } + // Perform the substition. + if ( target == "w" ) { + result += getWeekNumber(date); + } + else if ( target == "ww" ) { + result += getWeekNumber(date).toString().padStart(2,"0"); + } + else if ( locale != null ) { + result += date.toLocaleString( locale, target ); + } + else { + result += Qt.formatDateTime( date, target ); + } + } + + // Else append to result. + else { + result += arg; + } + } + + return result; } + // Helper: Calculate ISO stnadard week number. // Taken from https://github.com/qownnotes/scripts/blob/main/journal-entry/journal-entry.qml function getWeekNumber(d) { // Copy date so don't modify original @@ -108,30 +224,10 @@ Script { var weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); return weekNo; } - function init() { - commands = new Object(); - reInitCommands(); - } - function reInitCommands() { - var today = new Date(); - var todayMillis = today.getTime(); - var yesterday = new Date(todayMillis - _MILLI_DAY); - var tomorrow = new Date(todayMillis + _MILLI_DAY); - - commands["today"] = buildTimeList(today); - commands["tomorrow"] = buildTimeList(tomorrow); - commands["yesterday"] = buildTimeList(yesterday); - commands["week"] = [formatDate(today, _WWYYYY), formatDate(today, _WEEKWWYYYY)]; - commands["now"] = [formatDate(today, _FULL)]; - - var customRows = customCommands.split("\n"); - for (let i = 0; i < customRows.length; i++) { - var customCommand = CommandParser.parseCommandLine(customRows[i]); - if (customCommand === null) { - continue; - } - commands[customCommand.name] = customCommand.values; - } + // Helper: Capitalize first letter of a string. + function capitalizeFirstLetter(string) { + return string[0].toUpperCase() + string.slice(1); } + } From 8e5370451cfe92c29bc96d5889f63cd6e6887c37 Mon Sep 17 00:00:00 2001 From: Johan Vromans Date: Fri, 2 Oct 2026 21:39:55 +0200 Subject: [PATCH 2/2] (quick-commands) Update CHANGELOG and info.json. --- quick-commands/CHANGELOG.md | 7 +++++++ quick-commands/info.json | 6 +++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/quick-commands/CHANGELOG.md b/quick-commands/CHANGELOG.md index 263f0ee..9438da0 100644 --- a/quick-commands/CHANGELOG.md +++ b/quick-commands/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.3.0 - 2026-10-02 + +- Reworked for simplicity, efficiency and functionality. +- Custom commands can now have actual date/time values substituted by + using {dd}, {hh} and so on. +- Added README.md with extensive information. + ## 0.2.0 - 2026-08-24 - Support quoted multi-word custom command values ([#297](https://github.com/qownnotes/scripts/issues/297)). diff --git a/quick-commands/info.json b/quick-commands/info.json index e121d9e..011c66b 100644 --- a/quick-commands/info.json +++ b/quick-commands/info.json @@ -2,10 +2,10 @@ "name": "Quick Commands", "identifier": "quick-commands", "script": "quick-commands.qml", - "authors": ["@LockeBirdsey"], + "authors": ["@LockeBirdsey","@sciurius"], "platforms": ["linux", "macos", "windows"], - "version": "0.2.0", + "version": "0.3.0", "minAppVersion": "25.4.1", - "description": "Short commands to help with repetive and/or annoying things to write frequently such as timestamps.\n\nEach command starts with a backslash (\\) and a single word for the command. Select the autocomplete option you desire and the command text will be replace.\n\nDefault commands are: today, tomorrow, yesterday, now, week.\n\nCustom commands can also be specified. Each line starts with the command name followed by one or more replacement values separated by spaces. Enclose multi-word values in double quotes, for example: myname \"Firstname Lastname\".", + "description": "Short commands to help with repetive and/or annoying things to write frequently such as timestamps.\n\nEach command starts with a backslash (\\) and a single word for the command. Select the autocomplete option you desire and the command text will be replace.\n\nDefault commands are: today, tomorrow, yesterday, now, week.\n\nCustom commands can also be specified. Each line starts with the command name followed by one or more replacement values separated by spaces. Enclose multi-word values in double quotes, for example: myname \"Firstname Lastname\". Date and time values can be substituted with {dd}, {MM} and so on.", "resources": ["command-parser.js"] }