From 471631933b2c14de8e06739b81fbf17e6375799d Mon Sep 17 00:00:00 2001
From: j-krl <63491353+j-krl@users.noreply.github.com>
Date: Sat, 1 Aug 2020 10:57:45 -0600
Subject: [PATCH 1/3] CLI: Resolves #3217: Added link navigation shortcuts
(#3275)
---
.eslintignore | 2 +
.gitignore | 2 +
CliClient/app/LinkSelector.ts | 134 ++++++++++++++++++++++++++++++++++
CliClient/app/app-gui.js | 28 +++++++
CliClient/app/app.js | 3 +
CliClient/package.json | 1 +
docs/terminal/index.html | 3 +
readme/terminal.md | 13 +++-
tsconfig.json | 1 +
9 files changed, 185 insertions(+), 2 deletions(-)
create mode 100644 CliClient/app/LinkSelector.ts
diff --git a/.eslintignore b/.eslintignore
index 560ef419a..d7e33cda0 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -61,6 +61,8 @@ Modules/TinyMCE/IconPack/postinstall.js
Modules/TinyMCE/langs/
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
+CliClient/app/LinkSelector.js
+CliClient/build/LinkSelector.js
ElectronClient/commands/focusElement.js
ElectronClient/commands/startExternalEditing.js
ElectronClient/commands/stopExternalEditing.js
diff --git a/.gitignore b/.gitignore
index 0b1d67e6b..cbe02a27c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,6 +52,8 @@ Tools/commit_hook.txt
*.map
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
+CliClient/app/LinkSelector.js
+CliClient/build/LinkSelector.js
ElectronClient/commands/focusElement.js
ElectronClient/commands/startExternalEditing.js
ElectronClient/commands/stopExternalEditing.js
diff --git a/CliClient/app/LinkSelector.ts b/CliClient/app/LinkSelector.ts
new file mode 100644
index 000000000..f51c728b8
--- /dev/null
+++ b/CliClient/app/LinkSelector.ts
@@ -0,0 +1,134 @@
+const open = require('open');
+interface LinkStoreEntry {
+ link: string;
+ noteX: number;
+ noteY: number;
+}
+
+class LinkSelector {
+ noteId_: string;
+ scrollTop_: number;
+ renderedText_: string;
+ currentLinkIndex_: number;
+ linkStore_: LinkStoreEntry[];
+ linkRegex_: RegExp;
+
+ constructor() {
+ this.noteId_ = null;
+ this.scrollTop_ = null; // used so 'o' won't open unhighlighted link after scrolling
+ this.renderedText_ = null;
+ this.currentLinkIndex_ = null;
+ this.linkStore_ = null;
+ this.linkRegex_ = /http:\/\/[0-9.]+:[0-9]+\/[0-9]+/g;
+ }
+
+ get link(): string | null {
+ if (this.currentLinkIndex_ === null) return null;
+ return this.linkStore_[this.currentLinkIndex_].link;
+ }
+
+ get noteX(): number | null {
+ if (this.currentLinkIndex_ === null) return null;
+ return this.linkStore_[this.currentLinkIndex_].noteX;
+ }
+
+ get noteY(): number | null {
+ if (this.currentLinkIndex_ === null) return null;
+ return this.linkStore_[this.currentLinkIndex_].noteY;
+ }
+
+ findLinks(renderedText: string): LinkStoreEntry[] {
+ const newLinkStore: LinkStoreEntry[] = [];
+ const lines: string[] = renderedText.split('\n');
+ for (let i = 0; i < lines.length; i++) {
+ const matches = [...lines[i].matchAll(this.linkRegex_)];
+ matches.forEach((_e, n) => {
+ newLinkStore.push(
+ {
+ link: matches[n][0],
+ noteX: matches[n].index,
+ noteY: i,
+ }
+ );
+ });
+ }
+ return newLinkStore;
+ }
+
+ updateText(renderedText: string): void {
+ this.currentLinkIndex_ = null;
+ this.renderedText_ = renderedText;
+ this.linkStore_ = this.findLinks(this.renderedText_);
+ }
+
+ updateNote(textWidget: any): void {
+ this.noteId_ = textWidget.noteId;
+ this.scrollTop_ = textWidget.scrollTop_;
+ this.updateText(textWidget.renderedText_);
+ }
+
+ scrollWidget(textWidget: any): void {
+ if (this.currentLinkIndex_ === null) return;
+
+ const noteY = this.linkStore_[this.currentLinkIndex_].noteY;
+
+ let viewBoxMin = textWidget.scrollTop_ + 1;
+ let viewBoxMax = viewBoxMin + textWidget.innerHeight - 2;
+
+ if (noteY < viewBoxMin) {
+ for (; noteY < viewBoxMin; textWidget.pageUp()) {
+ viewBoxMin = textWidget.scrollTop_;
+ viewBoxMax = viewBoxMin + textWidget.innerHeight;
+ }
+ return;
+
+ } else if (noteY > viewBoxMax) {
+ for (; noteY > viewBoxMax; textWidget.pageDown()) {
+ viewBoxMin = textWidget.scrollTop_;
+ viewBoxMax = viewBoxMin + textWidget.innerHeight;
+ }
+ return;
+ }
+ return;
+ }
+
+ changeLink(textWidget: any, offset: number): void | null {
+ if (textWidget.noteId !== this.noteId_) {
+ this.updateNote(textWidget);
+ this.changeLink(textWidget, offset);
+ return;
+ }
+ if (textWidget.renderedText_ !== this.renderedText_) {
+ this.updateText(textWidget.renderedText_);
+ this.changeLink(textWidget, offset);
+ return;
+ }
+ if (textWidget.scrollTop_ !== this.scrollTop_) this.scrollTop_ = textWidget.scrollTop_;
+
+ if (!this.linkStore_.length) return null;
+
+ let offsetMod = (offset + this.currentLinkIndex_) % this.linkStore_.length;
+
+ if (this.currentLinkIndex_ === null) {
+ if (offsetMod < 0) this.currentLinkIndex_ = this.linkStore_.length + offsetMod;
+ else if (!offsetMod) this.currentLinkIndex_ = 0;
+ else this.currentLinkIndex_ = offsetMod - 1;
+ return;
+ }
+
+ if (offsetMod < 0) offsetMod = this.linkStore_.length + offsetMod;
+
+ this.currentLinkIndex_ = offsetMod;
+ return;
+ }
+
+ openLink(textWidget: any): void {
+ if (textWidget.noteId !== this.noteId_) return;
+ if (textWidget.renderedText_ !== this.renderedText_) return;
+ if (textWidget.scrollTop_ !== this.scrollTop_) return;
+ open(this.linkStore_[this.currentLinkIndex_].link);
+ }
+}
+
+export default LinkSelector;
+
diff --git a/CliClient/app/app-gui.js b/CliClient/app/app-gui.js
index 1676b6a18..4b35d1ee0 100644
--- a/CliClient/app/app-gui.js
+++ b/CliClient/app/app-gui.js
@@ -33,6 +33,8 @@ const FolderListWidget = require('./gui/FolderListWidget.js');
const NoteListWidget = require('./gui/NoteListWidget.js');
const StatusBarWidget = require('./gui/StatusBarWidget.js');
const ConsoleWidget = require('./gui/ConsoleWidget.js');
+const LinkSelector = require('./LinkSelector.js').default;
+
class AppGui {
constructor(app, store, keymap) {
@@ -74,6 +76,8 @@ class AppGui {
this.currentShortcutKeys_ = [];
this.lastShortcutKeyTime_ = 0;
+ this.linkSelector_ = new LinkSelector();
+
// Recurrent sync is setup only when the GUI is started. In
// a regular command it's not necessary since the process
// exits right away.
@@ -455,6 +459,30 @@ class AppGui {
} else {
this.stdout(_('Please select the note or notebook to be deleted first.'));
}
+ } else if (cmd === 'next_link' || cmd === 'previous_link') {
+ const noteText = this.widget('noteText');
+
+ noteText.render();
+
+ if (cmd === 'next_link') this.linkSelector_.changeLink(noteText, 1);
+ else this.linkSelector_.changeLink(noteText, -1);
+
+ this.linkSelector_.scrollWidget(noteText);
+
+ const cursorOffsetX = this.widget('mainWindow').width - noteText.innerWidth - 8;
+ const cursorOffsetY = 1 - noteText.scrollTop_;
+
+ if (this.linkSelector_.link) {
+ this.term_.moveTo(
+ this.linkSelector_.noteX + cursorOffsetX,
+ this.linkSelector_.noteY + cursorOffsetY
+ );
+ setTimeout(() => this.term_.term().inverse(this.linkSelector_.link), 50);
+ }
+ } else if (cmd === 'open_link') {
+ if (this.widget('noteText').hasFocus) {
+ this.linkSelector_.openLink(this.widget('noteText'));
+ }
} else if (cmd === 'toggle_console') {
if (!this.consoleIsShown()) {
this.showConsole();
diff --git a/CliClient/app/app.js b/CliClient/app/app.js
index 5dcc099a5..56fe0cd9d 100644
--- a/CliClient/app/app.js
+++ b/CliClient/app/app.js
@@ -324,6 +324,9 @@ class Application extends BaseApplication {
{ keys: ['PAGE_DOWN'], type: 'function', command: 'page_down' },
{ keys: ['ENTER'], type: 'function', command: 'activate' },
{ keys: ['DELETE', 'BACKSPACE'], type: 'function', command: 'delete' },
+ { keys: ['n'], type: 'function', command: 'next_link' },
+ { keys: ['b'], type: 'function', command: 'previous_link' },
+ { keys: ['o'], type: 'function', command: 'open_link' },
{ keys: [' '], command: 'todo toggle $n' },
{ keys: ['tc'], type: 'function', command: 'toggle_console' },
{ keys: ['tm'], type: 'function', command: 'toggle_metadata' },
diff --git a/CliClient/package.json b/CliClient/package.json
index 9cf5d22ea..2e7d83bb8 100644
--- a/CliClient/package.json
+++ b/CliClient/package.json
@@ -85,6 +85,7 @@
"node-emoji": "^1.8.1",
"node-fetch": "^1.7.1",
"node-persist": "^2.1.0",
+ "open": "^7.0.4",
"patch-package": "^6.2.0",
"promise": "^7.1.1",
"proper-lockfile": "^2.0.1",
diff --git a/docs/terminal/index.html b/docs/terminal/index.html
index e8b777381..4b1190912 100644
--- a/docs/terminal/index.html
+++ b/docs/terminal/index.html
@@ -519,6 +519,9 @@ PAGE_DOWN page_down
ENTER activate
DELETE, BACKSPACE delete
(SPACE) todo toggle $n
+n next_link
+b previous_link
+o open_link
tc toggle_console
tm toggle_metadata
/ search ""
diff --git a/readme/terminal.md b/readme/terminal.md
index 1461ed41d..d685401ff 100644
--- a/readme/terminal.md
+++ b/readme/terminal.md
@@ -134,7 +134,7 @@ It is possible to also synchronise outside of the user interface by typing `jopl
# URLs
-When Ctrl+Clicking a URL, most terminals will open that URL in the default browser. However, one issue, especially with long URLs, is that they can end up like this:
+When Ctrl+Clicking a URL (or opening with the shortcut 'o' while it is highlighted), most terminals will open that URL in the default browser. However, one issue, especially with long URLs, is that they can end up like this:
@@ -183,6 +183,9 @@ There are two types of shortcuts: those that manipulate the user interface direc
ENTER activate
DELETE, BACKSPACE delete
(SPACE) todo toggle $n
+ n next_link
+ b previous_link
+ o open_link
tc toggle_console
tm toggle_metadata
/ search ""
@@ -208,6 +211,9 @@ As an example, this is the default keymap, but read below for a detailed explana
{ "keys": ["ENTER"], "type": "function", "command": "activate" },
{ "keys": ["DELETE", "BACKSPACE"], "type": "function", "command": "delete" },
{ "keys": [" "], "command": "todo toggle $n" },
+ { "keys": ["n"], "type": "function", "command": "next_link" },
+ { "keys": ["b"], "type": "function", "command": "previous_link" },
+ { "keys": ["o"], "type": "function", "command": "open_link" },
{ "keys": ["tc"], "type": "function", "command": "toggle_console" },
{ "keys": ["tm"], "type": "function", "command": "toggle_metadata" },
{ "keys": ["/"], "type": "prompt", "command": "search \"\"", "cursorPosition": -2 },
@@ -226,7 +232,7 @@ Name | Description
`keys` | The array of keys that will trigger the action. Special keys such as page up, down arrow, etc. needs to be specified UPPERCASE. See the [list of available special keys](https://github.com/cronvel/terminal-kit/blob/3114206a9556f518cc63abbcb3d188fe1995100d/lib/termconfig/xterm.js#L531). For example, `['DELETE', 'BACKSPACE']` means the command will run if the user pressed either the delete or backspace key. Key combinations can also be provided - in that case specify them lowercase. For example "tc" means that the command will be executed when the user pressed "t" then "c". Special keys can also be used in this fashion - simply write them one after the other. For instance, `CTRL_WCTRL_W` means the action would be executed if the user pressed "ctrl-w ctrl-w".
`type` | The command type. It can have the value "exec", "function" or "prompt". **exec**: Simply execute the provided [command](#commands). For example `edit $n` would edit the selected note. **function**: Run a special commands (see below for the list of functions). **prompt**: A bit similar to "exec", except that the command is not going to be executed immediately - this allows the user to provide additional data. For example `mknote ""` would fill the command line with this command and allow the user to set the title. A prompt command can also take a `cursorPosition` parameter (see below)
`command` | The command that needs to be executed
-`cusorPosition` | An integer. For prompt commands, tells where the cursor (caret) should start at. This is convenient for example to position the cursor between quotes. Use a negative value to set a position starting from the end. A value of "0" means positioning the caret at the first character. A value of "-1" means positioning it at the end.
+`cursorPosition` | An integer. For prompt commands, tells where the cursor (caret) should start at. This is convenient for example to position the cursor between quotes. Use a negative value to set a position starting from the end. A value of "0" means positioning the caret at the first character. A value of "-1" means positioning it at the end.
This is the list of special functions:
@@ -239,6 +245,9 @@ move_up | Move up (in a list for example)
move_down | Move down (in a list for example)
page_up | Page up
page_down | Page down
+next_link | Select the next link in the currently opened note (the first link will be selected if no link is currently selected)
+previous_link | Select the previous link in the currently opened note (the last link will be selected if no link is currently selected)
+open_link | Open the currently selected link externally
activate | Activates the selected item. If the item is a note for example it will be open in the editor
delete | Deletes the selected item
toggle_console | Toggle the console
diff --git a/tsconfig.json b/tsconfig.json
index fb3bf0186..122587e7a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -2,6 +2,7 @@
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
+ "lib": ["es2020.string", "dom", "dom.iterable"],
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
"listEmittedFiles": false,
From 1b0102f62c10aad2ae94e4308befab7706ed8aef Mon Sep 17 00:00:00 2001
From: Laurent
Date: Sat, 1 Aug 2020 18:15:17 +0100
Subject: [PATCH 2/3] Update stale.yml
---
.github/stale.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/stale.yml b/.github/stale.yml
index c6c37f38d..da894e372 100644
--- a/.github/stale.yml
+++ b/.github/stale.yml
@@ -9,7 +9,7 @@ exemptLabels:
- "upstream"
- "backlog"
- "high"
- - "full-spec"
+ - "spec"
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
From 9147b3061a9ecd139a9dc03cc631689504f7fc92 Mon Sep 17 00:00:00 2001
From: Laurent
Date: Sat, 1 Aug 2020 18:17:40 +0100
Subject: [PATCH 3/3] Desktop: Hide completed to-dos in GotoAnything (#3580)
---
ElectronClient/plugins/GotoAnything.jsx | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/ElectronClient/plugins/GotoAnything.jsx b/ElectronClient/plugins/GotoAnything.jsx
index f37dfb034..9290ab680 100644
--- a/ElectronClient/plugins/GotoAnything.jsx
+++ b/ElectronClient/plugins/GotoAnything.jsx
@@ -227,7 +227,7 @@ class Dialog extends React.PureComponent {
} else {
const limit = 20;
const searchKeywords = this.keywords(searchQuery);
- const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body', 'markup_language'] });
+ const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body', 'markup_language', 'is_todo', 'todo_completed'] });
const notesById = notes.reduce((obj, { id, body, markup_language }) => ((obj[[id]] = { id, body, markup_language }), obj), {});
for (let i = 0; i < results.length; i++) {
@@ -269,6 +269,10 @@ class Dialog extends React.PureComponent {
results[i] = Object.assign({}, row, { path: path, fragments: '' });
}
}
+
+ if (!this.props.showCompletedTodos) {
+ results = results.filter((row) => !row.is_todo || !row.todo_completed);
+ }
}
}
@@ -449,6 +453,7 @@ const mapStateToProps = (state) => {
return {
folders: state.folders,
theme: state.settings.theme,
+ showCompletedTodos: state.settings.showCompletedTodos,
};
};