Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: extended hints support #302

Merged
merged 14 commits into from
Jan 4, 2024
26 changes: 24 additions & 2 deletions docs/keymaps.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ See the [console commands](./console_commands) section for a more detailed descr

## Navigation

- <kbd>f</kbd>: follow links in the page in the current tab
- <kbd>F</kbd>: follow links in the page in a new tab
- <kbd>H</kbd>: go back in history
- <kbd>L</kbd>: go forward in history
- <kbd>[</kbd><kbd>[</kbd>, <kbd>]</kbd><kbd>]</kbd>: find a link to the previous/next page and open it
Expand All @@ -86,6 +84,30 @@ action to `true`, e.g.:
}
```

## Hints

Hint mode is a way to follow links or select elements on a page by typing
characters. Users can type the sequence of characters to select a hint, or
press. <kbd>Enter</kbd> selects the hint which is currently typed, and <kbd>
Esc</kbd> or <kbd>Ctrl</kbd>+<kbd>[</kbd> cancels hint mode.

The following keymaps are available in hint mode:

- <kbd>f</kbd>: start a quick hint mode to open links in the current tab or select elements
- <kbd>F</kbd>: start a quick hint mode to open links in a new tab
- <kbd>;</kbd><kbd>i</kbd>: open an image in the current tab
- <kbd>;</kbd><kbd>I</kbd>: open an image in a new tab
- <kbd>;</kbd><kbd>y</kbd>: copy a link URL to the clipboard
- <kbd>;</kbd><kbd>Y</kbd>: copy a link text to the clipboard
- <kbd>;</kbd><kbd>v</kbd>: open a source URL in the current tab
- <kbd>;</kbd><kbd>V</kbd>: open a source URL in a new tab
- <kbd>;</kbd><kbd>o</kbd>: open a URL in the current tab
- <kbd>;</kbd><kbd>t</kbd>: open a URL in a new tab
- <kbd>;</kbd><kbd>w</kbd>: open a URL in a new window
- <kbd>;</kbd><kbd>O</kbd>: open the console with `:open` and the selected URL
- <kbd>;</kbd><kbd>T</kbd>: open the console with `:tabopen` and the selected URL
- <kbd>;</kbd><kbd>W</kbd>: open the console with `:winopen` and the selected URL

## Misc

- <kbd>y</kbd>: copy the URL of the current tab to the clipboard
Expand Down
20 changes: 18 additions & 2 deletions src/background/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import SettingsEventUseCase from "./usecases/SettingsEventUseCase";
import FrameClient from "./clients/FrameClient";
import AddonEnabledEventUseCase from "./usecases/AddonEnabledEventUseCase";
import LastSelectedTabRepository from "./repositories/LastSelectedTabRepository";
import ModeUseCase from "./usecases/ModeUseCase";
import HintModeUseCase from "./usecases/HintModeUseCase";

@injectable()
export default class Application {
Expand All @@ -28,6 +30,10 @@ export default class Application {
private readonly frameClient: FrameClient,
@inject(AddonEnabledEventUseCase)
private readonly addonEnabledEventUseCase: AddonEnabledEventUseCase,
@inject(ModeUseCase)
private readonly modeUseCase: ModeUseCase,
@inject(HintModeUseCase)
private readonly hintModeUseCase: HintModeUseCase,
) {}

private readonly findPortListener = new FindPortListener(
Expand All @@ -39,9 +45,14 @@ export default class Application {
this.settingsEventUseCase.registerEvents();
this.addonEnabledEventUseCase.registerEvents();

chrome.tabs.onUpdated.addListener((tabId: number, info) => {
chrome.tabs.onUpdated.addListener(async (tabId: number, info) => {
if (info.status == "complete") {
await this.modeUseCase.resetMode(tabId);
await this.hintModeUseCase.stop(tabId);
}

if (info.status == "loading") {
this.findRepository.deleteLocalState(tabId);
await this.findRepository.deleteLocalState(tabId);
}
});
chrome.runtime.onStartup.addListener(() => {
Expand All @@ -55,6 +66,11 @@ export default class Application {
});
chrome.tabs.onActivated.addListener(({ tabId }) => {
this.lastSelectedTabRepository.setCurrentTabId(tabId);
const lastTabId = this.lastSelectedTabRepository.getLastSelectedTabId();
if (typeof lastTabId !== "undefined") {
this.modeUseCase.resetMode(tabId);
this.hintModeUseCase.stop(tabId);
}
});

this.backgroundMessageListener.listen();
Expand Down
91 changes: 0 additions & 91 deletions src/background/clients/FollowClient.ts

This file was deleted.

100 changes: 100 additions & 0 deletions src/background/clients/HintClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { injectable } from "inversify";
import { newSender } from "./ContentMessageSender";
import type HTMLElementType from "../../shared/HTMLElementType";

export type Point = {
x: number;
y: number;
};

export type Size = {
width: number;
height: number;
};

export default interface HintClient {
lookupTargets(
tabId: number,
frameId: number,
cssSelector: string,
viewSize: Size,
framePosition: Point,
): Promise<string[]>;

assignTags(
tabId: number,
frameId: number,
elementTags: Record<string, string>,
): Promise<void>;

showHints(tabId: number, frameId: number, elements: string[]): Promise<void>;

clearHints(tabId: number): Promise<void>;

getElement(
tabId: number,
frameId: number,
element: string,
): Promise<HTMLElementType | undefined>;

focusElement(tabId: number, frameId: number, element: string): Promise<void>;

clickElement(tabId: number, frameId: number, element: string): Promise<void>;
}

@injectable()
export class HintClientImpl implements HintClient {
async lookupTargets(
tabId: number,
frameId: number,
cssSelector: string,
viewSize: Size,
framePosition: Point,
): Promise<string[]> {
const sender = newSender(tabId, frameId);
const { elements } = await sender.send("hint.lookup", {
viewSize,
framePosition,
cssSelector,
});
return elements;
}

assignTags(
tabId: number,
frameId: number,
elementTags: Record<string, string>,
): Promise<void> {
const sender = newSender(tabId, frameId);
return sender.send("hint.assign", { elementTags });
}

showHints(tabId: number, frameId: number, elements: string[]): Promise<void> {
const sender = newSender(tabId, frameId);
return sender.send("hint.show", { elements });
}

clearHints(tabId: number): Promise<void> {
const sender = newSender(tabId);
return sender.send("hint.clear");
}

getElement(
tabId: number,
frameId: number,
element: string,
): Promise<HTMLElementType | undefined> {
const sender = newSender(tabId, frameId);
return sender.send("hint.getElement", { element });
}

focusElement(tabId: number, frameId: number, element: string): Promise<void> {
const sender = newSender(tabId, frameId);
return sender.send("hint.focus", { element });
}

clickElement(tabId: number, frameId: number, element: string): Promise<void> {
const sender = newSender(tabId, frameId);
return sender.send("hint.click", { element });
}
}
21 changes: 0 additions & 21 deletions src/background/clients/KeyCaptureClient.ts

This file was deleted.

15 changes: 15 additions & 0 deletions src/background/clients/ModeClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { injectable } from "inversify";
import { newSender } from "./ContentMessageSender";
import Mode from "../../shared/Mode";

export default interface ModeClient {
setMode(tabid: number, mode: Mode): Promise<void>;
}

@injectable()
export class ModeClientImpl implements ModeClient {
async setMode(tabId: number, mode: Mode): Promise<void> {
const sender = newSender(tabId);
sender.send("set.mode", { mode });
}
}
41 changes: 21 additions & 20 deletions src/background/controllers/KeyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { injectable, inject } from "inversify";
import RequestContext from "../messaging/RequestContext";
import MarkJumpUseCase from "../usecases/MarkJumpUseCase";
import MarkSetUseCase from "../usecases/MarkSetUseCase";
import MarkModeUseCase from "../usecases/MarkModeUseCase";
import FollowModeUseCaes from "../usecases/FollowModeUseCase";
import FollowKeyUseCase from "../usecases/FollowKeyUseCase";
import HintModeUseCaes from "../usecases/HintModeUseCase";
import HintKeyUseCase from "../usecases/HintKeyUseCase";
import ModeUseCase from "../usecases/ModeUseCase";

@injectable()
export default class KeyController {
Expand All @@ -13,31 +13,32 @@ export default class KeyController {
private readonly markSetUseCase: MarkSetUseCase,
@inject(MarkJumpUseCase)
private readonly markJumpUseCase: MarkJumpUseCase,
@inject(MarkModeUseCase)
private readonly markModeUseCase: MarkModeUseCase,
@inject(FollowModeUseCaes)
private readonly followModeUseCaes: FollowModeUseCaes,
@inject(FollowKeyUseCase)
private readonly followKeyUseCase: FollowKeyUseCase,
@inject(HintModeUseCaes)
private readonly hintModeUseCaes: HintModeUseCaes,
@inject(HintKeyUseCase)
private readonly hintKeyUseCase: HintKeyUseCase,
@inject(ModeUseCase)
private readonly modeUseCase: ModeUseCase,
) {}

async pressKey({ sender }: RequestContext, { key }: { key: string }) {
if (typeof sender.tab?.id === "undefined") {
return;
}
if (await this.markModeUseCase.isSetMode()) {
await this.markSetUseCase.setMark(sender.tab, key);
await this.markModeUseCase.clearMarkMode(sender.tab.id);
} else if (await this.markModeUseCase.isJumpMode()) {
await this.markJumpUseCase.jumpToMark(key);
await this.markModeUseCase.clearMarkMode(sender.tab.id);
}

if (await this.followModeUseCaes.isFollowMode()) {
const cont = await this.followKeyUseCase.pressKey(sender.tab.id, key);
if (!cont) {
await this.followModeUseCaes.stop(sender.tab.id);
const mode = await this.modeUseCase.getMode();
if (mode === "follow") {
const continued = await this.hintKeyUseCase.pressKey(sender.tab.id, key);
if (!continued) {
await this.hintModeUseCaes.stop(sender.tab.id);
await this.modeUseCase.resetMode(sender.tab.id);
}
} else if (mode === "mark-set") {
await this.markSetUseCase.setMark(sender.tab, key);
await this.modeUseCase.resetMode(sender.tab.id);
} else if (mode === "mark-jump") {
await this.markJumpUseCase.jumpToMark(key);
await this.modeUseCase.resetMode(sender.tab.id);
}
}
}
Loading