├── .eslintrc ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ └── build.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── _layouts │ └── default.html ├── assets │ └── css │ │ └── style.scss ├── blacklist.md ├── console_commands.md ├── index.md ├── keymaps.md ├── properties.md └── search_engines.md ├── e2e ├── blacklist.test.ts ├── clipboard.test.ts ├── colorscheme.test.ts ├── command_addbookmark.test.ts ├── command_bdelete.test.ts ├── command_buffer.test.ts ├── command_help.test.ts ├── command_open.test.ts ├── command_quit.test.ts ├── command_tabopen.test.ts ├── command_winopen.test.ts ├── completion.test.ts ├── completion_buffers.test.ts ├── completion_open.test.ts ├── completion_set.test.ts ├── console.test.ts ├── eventually.ts ├── find.test.ts ├── follow.test.ts ├── follow_properties.test.ts ├── jest.config.ts ├── lib │ ├── Console.ts │ ├── FormOptionPage.ts │ ├── JSONOptionPage.ts │ ├── OptionPage.ts │ ├── Page.ts │ ├── SettingRepository.ts │ ├── TestServer.ts │ └── clipboard.ts ├── main.ts ├── mark.test.ts ├── navigate.test.ts ├── options.test.ts ├── options_form.test.ts ├── partial_blacklist.test.ts ├── repeat.test.ts ├── repeat_n_times.test.ts ├── scroll.test.ts ├── tab.test.ts └── zoom.test.ts ├── jest.config.ts ├── manifest.json ├── package.json ├── resources ├── disabled_32x32.png ├── enabled_32x32.png ├── icon.svg ├── icon_48x48.png └── icon_96x96.png ├── script ├── deploy └── package ├── src ├── @types │ ├── browser │ │ └── index.d.ts │ └── web-ext-api │ │ └── index.d.ts ├── background │ ├── Application.ts │ ├── clients │ │ ├── ConsoleFrameClient.ts │ │ ├── FindClient.ts │ │ └── NavigateClient.ts │ ├── completion │ │ ├── BookmarkRepository.ts │ │ ├── HistoryRepository.ts │ │ ├── OpenCompletionUseCase.ts │ │ ├── PropertyCompletionUseCase.ts │ │ ├── TabCompletionUseCase.ts │ │ ├── TabItem.ts │ │ ├── TabRepository.ts │ │ └── impl │ │ │ ├── BookmarkRepositoryImpl.ts │ │ │ ├── HistoryRepositoryImpl.ts │ │ │ ├── TabRepositoryImpl.ts │ │ │ └── filters.ts │ ├── controllers │ │ ├── AddonEnabledController.ts │ │ ├── CommandController.ts │ │ ├── CompletionController.ts │ │ ├── ConsoleController.ts │ │ ├── FindController.ts │ │ ├── LinkController.ts │ │ ├── MarkController.ts │ │ ├── OperationController.ts │ │ ├── SettingController.ts │ │ └── VersionController.ts │ ├── di.ts │ ├── domains │ │ └── GlobalMark.ts │ ├── index.ts │ ├── infrastructures │ │ ├── ConsoleClient.ts │ │ ├── ContentMessageClient.ts │ │ ├── ContentMessageListener.ts │ │ ├── FindPortListener.ts │ │ └── MemoryStorage.ts │ ├── operators │ │ ├── Operator.ts │ │ ├── OperatorFactory.ts │ │ ├── OperatorFactoryChain.ts │ │ └── impls │ │ │ ├── CancelOperator.ts │ │ │ ├── CloseTabOperator.ts │ │ │ ├── CloseTabRightOperator.ts │ │ │ ├── CommandOperatorFactoryChain.ts │ │ │ ├── DuplicateTabOperator.ts │ │ │ ├── FindNextOperator.ts │ │ │ ├── FindOperatorFactoryChain.ts │ │ │ ├── FindPrevOperator.ts │ │ │ ├── InternalOpenURLOperator.ts │ │ │ ├── InternalOperatorFactoryChain.ts │ │ │ ├── NavigateHistoryNextOperator.ts │ │ │ ├── NavigateHistoryPrevOperator.ts │ │ │ ├── NavigateLinkNextOperator.ts │ │ │ ├── NavigateLinkPrevOperator.ts │ │ │ ├── NavigateOperatorFactoryChain.ts │ │ │ ├── NavigateParentOperator.ts │ │ │ ├── NavigateRootOperator.ts │ │ │ ├── OpenHomeOperator.ts │ │ │ ├── OpenSourceOperator.ts │ │ │ ├── OperatorFactoryImpl.ts │ │ │ ├── PinTabOperator.ts │ │ │ ├── ReloadTabOperator.ts │ │ │ ├── ReopenTabOperator.ts │ │ │ ├── RepeatLastOperator.ts │ │ │ ├── RepeatOperatorFactoryChain.ts │ │ │ ├── ResetZoomOperator.ts │ │ │ ├── SelectFirstTabOperator.ts │ │ │ ├── SelectLastTabOperator.ts │ │ │ ├── SelectPreviousSelectedTabOperator.ts │ │ │ ├── SelectTabNextOperator.ts │ │ │ ├── SelectTabPrevOperator.ts │ │ │ ├── ShowAddBookmarkOperator.ts │ │ │ ├── ShowBufferCommandOperator.ts │ │ │ ├── ShowCommandOperator.ts │ │ │ ├── ShowOpenCommandOperator.ts │ │ │ ├── ShowTabOpenCommandOperator.ts │ │ │ ├── ShowWinOpenCommandOperator.ts │ │ │ ├── StartFindOperator.ts │ │ │ ├── TabOperatorFactoryChain.ts │ │ │ ├── TogglePinnedTabOperator.ts │ │ │ ├── ToggleReaderOperator.ts │ │ │ ├── UnpinTabOperator.ts │ │ │ ├── ZoomInOperator.ts │ │ │ ├── ZoomOperatorFactoryChain.ts │ │ │ └── ZoomOutOperator.ts │ ├── presenters │ │ ├── HelpPresenter.ts │ │ ├── IndicatorPresenter.ts │ │ ├── Notifier.ts │ │ ├── TabPresenter.ts │ │ ├── WindowPresenter.ts │ │ └── ZoomPresenter.ts │ ├── repositories │ │ ├── BookmarkRepository.ts │ │ ├── BrowserSettingRepository.ts │ │ ├── CachedSettingRepository.ts │ │ ├── FindRepository.ts │ │ ├── MarkRepository.ts │ │ ├── ReadyFrameRepository.ts │ │ ├── RepeatRepository.ts │ │ └── SettingRepository.ts │ └── usecases │ │ ├── AddonEnabledUseCase.ts │ │ ├── CommandUseCase.ts │ │ ├── ConsoleUseCase.ts │ │ ├── LinkUseCase.ts │ │ ├── MarkUseCase.ts │ │ ├── RepeatUseCase.ts │ │ ├── SettingUseCase.ts │ │ ├── StartFindUseCase.ts │ │ ├── VersionUseCase.ts │ │ └── parsers.ts ├── console │ ├── App.tsx │ ├── Completions.ts │ ├── app │ │ ├── actions.ts │ │ ├── contexts.ts │ │ ├── hooks.ts │ │ ├── provider.tsx │ │ └── recuer.ts │ ├── clients │ │ ├── CompletionClient.ts │ │ ├── ConsoleFrameClient.ts │ │ └── SettingClient.ts │ ├── colorscheme │ │ ├── contexts.tsx │ │ ├── hooks.ts │ │ ├── providers.tsx │ │ ├── styled.tsx │ │ └── theme.ts │ ├── commandline │ │ ├── CommandLineParser.ts │ │ └── CommandParser.ts │ ├── completion │ │ ├── actions.ts │ │ ├── context.ts │ │ ├── hooks.ts │ │ ├── hooks │ │ │ └── clients.ts │ │ ├── provider.tsx │ │ └── reducer.ts │ ├── components │ │ ├── CommandPrompt.tsx │ │ ├── Console.tsx │ │ ├── ErrorMessage.tsx │ │ ├── FindPrompt.tsx │ │ ├── InfoMessage.tsx │ │ └── console │ │ │ ├── Completion.tsx │ │ │ ├── CompletionItem.tsx │ │ │ ├── CompletionTitle.tsx │ │ │ └── Input.tsx │ ├── hooks │ │ ├── useAutoResize.ts │ │ └── useDebounce.ts │ ├── index.css │ ├── index.html │ └── index.tsx ├── content │ ├── Application.ts │ ├── Bootstrap.ts │ ├── InputDriver.ts │ ├── MessageListener.ts │ ├── client │ │ ├── AddonIndicatorClient.ts │ │ ├── ConsoleClient.ts │ │ ├── FollowMasterClient.ts │ │ ├── FollowSlaveClient.ts │ │ ├── FollowSlaveClientFactory.ts │ │ ├── MarkClient.ts │ │ ├── OperationClient.ts │ │ ├── SettingClient.ts │ │ └── TabsClient.ts │ ├── controllers │ │ ├── AddonEnabledController.ts │ │ ├── ConsoleFrameController.ts │ │ ├── FindController.ts │ │ ├── FollowKeyController.ts │ │ ├── FollowMasterController.ts │ │ ├── FollowSlaveController.ts │ │ ├── KeymapController.ts │ │ ├── MarkController.ts │ │ ├── MarkKeyController.ts │ │ ├── NavigateController.ts │ │ └── SettingController.ts │ ├── di.ts │ ├── domains │ │ ├── KeySequence.ts │ │ └── Mark.ts │ ├── index.ts │ ├── operators │ │ ├── Operator.ts │ │ ├── OperatorFactory.ts │ │ ├── OperatorFactoryChain.ts │ │ └── impls │ │ │ ├── AbstractScrollOperator.ts │ │ │ ├── AddonOperatorFactoryChain.ts │ │ │ ├── BackgroundOperationOperator.ts │ │ │ ├── ClipboardOperatorFactoryChain.ts │ │ │ ├── DisableAddonOperator.ts │ │ │ ├── EnableAddonOperator.ts │ │ │ ├── EnableJumpMarkOperator.ts │ │ │ ├── EnableSetMarkOperator.ts │ │ │ ├── FocusOperator.ts │ │ │ ├── FocusOperatorFactoryChain.ts │ │ │ ├── FollowOperatorFactoryChain.ts │ │ │ ├── HorizontalScrollOperator.ts │ │ │ ├── MarkOperatorFactoryChain.ts │ │ │ ├── OperatorFactoryImpl.ts │ │ │ ├── PageScrollOperator.ts │ │ │ ├── PasteOperator.ts │ │ │ ├── ScrollOperatorFactoryChain.ts │ │ │ ├── ScrollToBottomOperator.ts │ │ │ ├── ScrollToEndOperator.ts │ │ │ ├── ScrollToHomeOperator.ts │ │ │ ├── ScrollToTopOperator.ts │ │ │ ├── StartFollowOperator.ts │ │ │ ├── ToggleAddonOperator.ts │ │ │ ├── URLRepository.ts │ │ │ ├── VerticalScrollOperator.ts │ │ │ └── YankURLOperator.ts │ ├── presenters │ │ ├── ConsoleFramePresenter.ts │ │ ├── FindPresenter.ts │ │ ├── FocusPresenter.ts │ │ ├── FollowPresenter.ts │ │ ├── Hint.ts │ │ ├── NavigationPresenter.ts │ │ └── ScrollPresenter.ts │ ├── repositories │ │ ├── AddonEnabledRepository.ts │ │ ├── AddressRepository.ts │ │ ├── ClipboardRepository.ts │ │ ├── FollowKeyRepository.ts │ │ ├── FollowMasterRepository.ts │ │ ├── FollowSlaveRepository.ts │ │ ├── HintKeyRepository.ts │ │ ├── KeymapRepository.ts │ │ ├── MarkKeyRepository.ts │ │ ├── MarkRepository.ts │ │ └── SettingRepository.ts │ ├── site-style.ts │ └── usecases │ │ ├── AddonEnabledUseCase.ts │ │ ├── ConsoleFrameUseCase.ts │ │ ├── FindUseCase.ts │ │ ├── FollowMasterUseCase.ts │ │ ├── FollowSlaveUseCase.ts │ │ ├── KeymapUseCase.ts │ │ ├── MarkKeyUseCase.ts │ │ ├── MarkUseCase.ts │ │ ├── NavigateUseCase.ts │ │ └── SettingUseCase.ts ├── settings │ ├── actions │ │ ├── index.ts │ │ └── setting.ts │ ├── components │ │ ├── form │ │ │ ├── BlacklistForm.tsx │ │ │ ├── KeymapsForm.tsx │ │ │ ├── PartialBlacklistForm.tsx │ │ │ ├── PropertiesForm.tsx │ │ │ └── SearchForm.tsx │ │ ├── index.tsx │ │ └── ui │ │ │ ├── AddButton.tsx │ │ │ ├── DeleteButton.tsx │ │ │ ├── Radio.tsx │ │ │ ├── Text.tsx │ │ │ └── TextArea.tsx │ ├── index.html │ ├── index.tsx │ ├── keymaps.ts │ ├── reducers │ │ └── setting.ts │ └── storage.ts └── shared │ ├── ColorScheme.ts │ ├── Command.ts │ ├── CompletionType.ts │ ├── SettingData.ts │ ├── TabFlag.ts │ ├── messages.ts │ ├── operations.ts │ ├── settings │ ├── Blacklist.ts │ ├── Key.ts │ ├── Keymaps.ts │ ├── Properties.ts │ ├── Search.ts │ ├── Settings.ts │ ├── schema.json │ └── validate.js │ ├── urls.ts │ └── utils │ └── dom.ts ├── test ├── background │ ├── completion │ │ ├── OpenCompletionUseCase.test.ts │ │ ├── PropertyCompletionUseCase.test.ts │ │ ├── TabCompletionUseCase.test.ts │ │ └── impl │ │ │ └── filters.test.ts │ ├── infrastructures │ │ └── MemoryStorage.test.ts │ ├── mock │ │ ├── MockBrowserSettingRepository.ts │ │ ├── MockConsoleClient.ts │ │ ├── MockFindClient.ts │ │ ├── MockFindRepository.ts │ │ ├── MockNavigateClient.ts │ │ ├── MockReadyFrameRepository.ts │ │ ├── MockRepeatRepository.ts │ │ ├── MockTabPresenter.ts │ │ ├── MockWindowPresenter.ts │ │ └── MockZoomPresenter.ts │ ├── operators │ │ └── impls │ │ │ ├── CancelOperator.test.ts │ │ │ ├── CloseTabOperator.test.ts │ │ │ ├── CloseTabRightOperator.test.ts │ │ │ ├── CommandOperatorFactoryChain.test.ts │ │ │ ├── DuplicateTabOperator.test.ts │ │ │ ├── FindNextOperator.test.ts │ │ │ ├── FindOperatorFactoryChain.ts │ │ │ ├── FindPrevOperator.test.ts │ │ │ ├── InternalOperatorFactoryChain.test.ts │ │ │ ├── NavigateHistoryNextOperator.test.ts │ │ │ ├── NavigateHistoryPrevOperator.test.ts │ │ │ ├── NavigateLinkNextOperator.test.ts │ │ │ ├── NavigateLinkPrevOperator.test.ts │ │ │ ├── NavigateOperatorFactoryChain.test.ts │ │ │ ├── NavigateParentOperator.test.ts │ │ │ ├── NavigateRootOperator.test.ts │ │ │ ├── OpenHomeOperator.test.ts │ │ │ ├── OpenSourceOperator.test.ts │ │ │ ├── PinTabOperator.test.ts │ │ │ ├── ReloadTabOperator.test.ts │ │ │ ├── ReopenTabOperator.test.ts │ │ │ ├── RepeatLastOperator.test.ts │ │ │ ├── RepeatOperatorFactoryChain.test.ts │ │ │ ├── ResetZoomOperator.test.ts │ │ │ ├── SelectFirstTabOperator.test.ts │ │ │ ├── SelectLastTabOperator.test.ts │ │ │ ├── SelectPreviousSelectedTabOperator.test.ts │ │ │ ├── SelectTabNextOperator.test.ts │ │ │ ├── SelectTabPrevOperator.test.ts │ │ │ ├── ShowAddBookmarkOperator.test.ts │ │ │ ├── ShowBufferCommandOperator.test.ts │ │ │ ├── ShowCommandOperator.test.ts │ │ │ ├── ShowOpenCommandOperator.test.ts │ │ │ ├── ShowTabOpenCommandOperator.test.ts │ │ │ ├── ShowWinOpenCommandOperator.test.ts │ │ │ ├── StartFindOperator.test.ts │ │ │ ├── TabOperatorFactoryChain.test.ts │ │ │ ├── TogglePinnedTabOperator.test.ts │ │ │ ├── UnpinTabOperator.test.ts │ │ │ ├── ZoomInOperator.test.ts │ │ │ ├── ZoomOperatorFactoryChain.test.ts │ │ │ └── ZoomOutOperator.test.ts │ ├── repositories │ │ ├── FindRepository.test.ts │ │ ├── Mark.test.ts │ │ └── ReadyFrameRepository.test.ts │ └── usecases │ │ ├── SettingUseCase.test.ts │ │ ├── StartFindUseCase.test.ts │ │ └── parsers.test.ts ├── console │ ├── app │ │ ├── actions.test.ts │ │ └── reducer.test.ts │ ├── commandline │ │ ├── CommandLineParser.test.ts │ │ └── CommandParser.test.ts │ ├── completion │ │ └── reducer.test.ts │ └── components │ │ ├── ErrorMessage.test.tsx │ │ ├── InfoMessage.test.tsx │ │ └── console │ │ ├── Completion.test.tsx │ │ ├── CompletionItem.test.tsx │ │ └── CompletionTitle.test.tsx ├── content │ ├── InputDriver.test.ts │ ├── domains │ │ └── KeySequence.test.ts │ ├── mock │ │ ├── MockAddonEnabledRepository.ts │ │ ├── MockAddonIndicatorClient.ts │ │ ├── MockClipboardRepository.ts │ │ ├── MockConsoleClient.ts │ │ ├── MockFocusPresenter.ts │ │ ├── MockFollowMasterClient.ts │ │ ├── MockMarkKeyRepository.ts │ │ ├── MockOperationClient.ts │ │ ├── MockScrollPresenter.ts │ │ ├── MockSettingRepository.ts │ │ └── MockURLRepository.ts │ ├── operators │ │ └── impls │ │ │ ├── AddonOperatorFactoryChain.test.ts │ │ │ ├── BackgroundOperationOperator.test.ts │ │ │ ├── ClipboardOperatorFactoryChain.test.ts │ │ │ ├── DisableAddonOperator.test.ts │ │ │ ├── EnableAddonOperator.test.ts │ │ │ ├── EnableJumpMarkOperator.test.ts │ │ │ ├── EnableSetMarkOperator.test.ts │ │ │ ├── FocusOperator.test.ts │ │ │ ├── FocusOperatorFactoryChain.test.ts │ │ │ ├── FollowOperatorFactoryChain.test.ts │ │ │ ├── HorizontalScrollOperator.test.ts │ │ │ ├── MarkOperatorFactoryChain.test.ts │ │ │ ├── MockConsoleFramePresenter.ts │ │ │ ├── PageScrollOperator.test.ts │ │ │ ├── PasteOperator.test.ts │ │ │ ├── ScrollOperatorFactoryChain.test.ts │ │ │ ├── ScrollToBottomOperator.test.ts │ │ │ ├── ScrollToEndOperator.test.ts │ │ │ ├── ScrollToHomeOperator.test.ts │ │ │ ├── ScrollToTopOperator.test.ts │ │ │ ├── StartFollowOperator.test.ts │ │ │ ├── ToggleAddonOperator.test.ts │ │ │ ├── VerticalScrollOperator.test.ts │ │ │ └── YankURLOperator.test.ts │ ├── presenters │ │ ├── Hint.test.ts │ │ └── NavigationPresenter.test.ts │ ├── repositories │ │ ├── AddonEnabledRepository.test.ts │ │ ├── FollowKeyRepository.test.ts │ │ ├── FollowMasterRepository.test.ts │ │ ├── FollowSlaveRepository.test.ts │ │ ├── KeymapRepository.test.ts │ │ ├── MarkKeyRepository.test.ts │ │ ├── MarkRepository.test.ts │ │ └── SettingRepository.test.ts │ └── usecases │ │ ├── AddonEnabledUseCase.test.ts │ │ ├── HintKeyProducer.test.ts │ │ ├── KeymapUseCase.test.ts │ │ ├── MarkUseCase.test.ts │ │ └── SettingUseCaase.test.ts ├── main.ts ├── settings │ ├── components │ │ ├── form │ │ │ ├── BlacklistForm.test.tsx │ │ │ ├── KeymapsForm.test.tsx │ │ │ ├── PropertiesForm.test.tsx │ │ │ └── SearchEngineForm.test.tsx │ │ └── ui │ │ │ ├── Radio.test.tsx │ │ │ ├── Text.test.tsx │ │ │ └── TextArea.test.tsx │ └── reducers │ │ └── setting.test.ts └── shared │ ├── SettingData.test.ts │ ├── operations.test.ts │ ├── settings │ ├── Blacklist.test.ts │ ├── Key.test.ts │ ├── Keymaps.test.ts │ ├── Properties.test.ts │ ├── Search.test.ts │ └── Settings.test.ts │ └── urls.test.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "browser" : true, 6 | "webextensions": true 7 | }, 8 | 9 | "extends": [ 10 | "eslint:recommended", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:react/recommended", 13 | "plugin:prettier/recommended", 14 | ], 15 | "plugins": [ 16 | "@typescript-eslint", 17 | "prettier", 18 | "react", 19 | "standard" 20 | ], 21 | "parserOptions": { 22 | "sourceType": "module", 23 | "ecmaFeatures": { 24 | "jsx": true 25 | } 26 | }, 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | } 31 | }, 32 | "rules": { 33 | "@typescript-eslint/explicit-module-boundary-types": "off", 34 | "@typescript-eslint/no-empty-function": "off", 35 | "@typescript-eslint/no-explicit-any": "off", 36 | "@typescript-eslint/no-non-null-assertion": "off", 37 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] 38 | }, 39 | "overrides": [ 40 | { 41 | "files": ["**/*.tsx"], 42 | "rules": { 43 | "react/prop-types": "off" 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### I'm opening this issue because: 2 | 3 | - [ ] I'll report a bug 4 | - [ ] I'll propose a new feature 5 | 6 | ### Description 7 | 8 | ### Failure Information (for bugs) 9 | 10 | #### Steps to Reproduce 11 | 12 | Please provide detailed steps for reproducing the issue. 13 | 14 | 1. step 1 15 | 2. step 2 16 | 3. you get it... 17 | 18 | #### System configuration 19 | 20 | - Operating system: 21 | - Firefox version: 22 | - Vim-Vixen version: 23 | 24 | #### Console logs 25 | 26 | Any relevant log in developer tools: 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | versioning-strategy: increase 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /build/ 3 | *.zip 4 | lanthan-driver.log 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Report a bug or propose a new feature 4 | 5 | Open a new issue from [issues](https://github.com/ueokande/vim-vixen/issues). 6 | **Ensure the issue was not already reported** by searching on GitHub under Issues. 7 | The issue should include a title and clear description. 8 | 9 | Pull request is also welcome to send a patch from [Pull Requests](https://github.com/ueokande/vim-vixen/pulls). 10 | Ensure the pull request includes description, and passes tests in CI. 11 | 12 | ## Start a development 13 | 14 | Clone sources into local 15 | 16 | git clone https://github.com/ueokande/vim-vixen 17 | 18 | Install dependencies: 19 | 20 | yarn install 21 | 22 | Start webpack: 23 | 24 | yarn start 25 | 26 | Then open `about:debugging` in Firefox, and choose directory from "Load Temporary Add-on". 27 | To run tests and lint: 28 | 29 | yarn test 30 | yarn lint 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Shin'ya Ueoka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /_site/ 3 | /vendor/bundle/ 4 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'github-pages', group: :jekyll_plugins 4 | -------------------------------------------------------------------------------- /docs/search_engines.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Search engines 3 | --- 4 | 5 | # Search engines 6 | 7 | Vim Vixen supports searching with search engines such as Google and Yahoo. 8 | 9 | You can configure search engines, including the default search engine, in the add-on's preferences. 10 | The URLs specified in `"engines"` must contain a `{}`-placeholder, which will be 11 | replaced with the search keyword parameters of the command. 12 | 13 | ```json 14 | { 15 | "search": { 16 | "default": "google", 17 | "engines": { 18 | "google": "https://google.com/search?q={}", 19 | "yahoo": "https://search.yahoo.com/search?p={}", 20 | "bing": "https://www.bing.com/search?q={}", 21 | "duckduckgo": "https://duckduckgo.com/?q={}", 22 | "twitter": "https://twitter.com/search?q={}", 23 | "wikipedia": "https://en.wikipedia.org/w/index.php?search={}" 24 | } 25 | } 26 | } 27 | ``` 28 | -------------------------------------------------------------------------------- /e2e/eventually.ts: -------------------------------------------------------------------------------- 1 | const defaultInterval = 100; 2 | const defaultTimeout = 2000; 3 | 4 | type Handler = () => void; 5 | 6 | const sleep = (ms: number): Promise => { 7 | return new Promise((resolve) => setTimeout(resolve, ms)); 8 | }; 9 | 10 | const eventually = async ( 11 | fn: Handler, 12 | timeout = defaultTimeout, 13 | interval = defaultInterval 14 | ): Promise => { 15 | const start = Date.now(); 16 | const loop = async () => { 17 | try { 18 | await fn(); 19 | } catch (err) { 20 | if (Date.now() - start > timeout) { 21 | throw err; 22 | } 23 | await sleep(interval); 24 | await loop(); 25 | } 26 | }; 27 | await loop(); 28 | }; 29 | 30 | export default eventually; 31 | -------------------------------------------------------------------------------- /e2e/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | 3 | const config: Config.InitialOptions = { 4 | verbose: true, 5 | testMatch: ["**/e2e/**/*.test.+(ts|tsx|js|jsx)"], 6 | transform: { 7 | "^.+\\.(ts|tsx)$": "ts-jest", 8 | }, 9 | setupFiles: ["./main.ts"], 10 | testTimeout: 20000, 11 | maxConcurrency: 1, 12 | }; 13 | export default config; 14 | -------------------------------------------------------------------------------- /e2e/lib/JSONOptionPage.ts: -------------------------------------------------------------------------------- 1 | import { Lanthan } from "lanthan"; 2 | import { WebDriver, By } from "selenium-webdriver"; 3 | 4 | export default class JSONOptionPage { 5 | private webdriver: WebDriver; 6 | 7 | constructor(lanthan: Lanthan) { 8 | this.webdriver = lanthan.getWebDriver(); 9 | } 10 | 11 | async updateSettings(value: string): Promise { 12 | const textarea = await this.webdriver.findElement(By.css("textarea")); 13 | await this.webdriver.executeScript( 14 | `document.querySelector('textarea').value = '${value}'` 15 | ); 16 | await textarea.sendKeys(" "); 17 | await this.webdriver.executeScript(() => 18 | document.querySelector("textarea")!.blur() 19 | ); 20 | } 21 | 22 | async getErrorMessage(): Promise { 23 | const error = await this.webdriver.findElement(By.css("p[role=alert]")); 24 | return error.getText(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /e2e/lib/SettingRepository.ts: -------------------------------------------------------------------------------- 1 | import { JSONTextSettings, SettingSource } from "../../src/shared/SettingData"; 2 | import Settings from "../../src/shared/settings/Settings"; 3 | 4 | export default class SettingRepository { 5 | constructor(private readonly browser: any) {} 6 | 7 | async saveJSON(settings: Settings): Promise { 8 | await this.browser.storage.sync.set({ 9 | settings: { 10 | source: SettingSource.JSON, 11 | json: JSONTextSettings.fromSettings(settings).toJSONText(), 12 | }, 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /e2e/main.ts: -------------------------------------------------------------------------------- 1 | jest.retryTimes(10); 2 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | 3 | const config: Config.InitialOptions = { 4 | verbose: true, 5 | testMatch: ["**/test/**/*.test.+(ts|tsx|js|jsx)"], 6 | transform: { 7 | "^.+\\.(ts|tsx)$": "ts-jest", 8 | }, 9 | setupFiles: ["./test/main.ts"], 10 | }; 11 | export default config; 12 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Vim Vixen", 4 | "description": "Vim Vixen", 5 | "version": "1.2.4", 6 | "icons": { 7 | "48": "resources/icon_48x48.png", 8 | "96": "resources/icon_96x96.png" 9 | }, 10 | "applications": { 11 | "gecko": { 12 | "id": "vim-vixen@i-beam.org" 13 | } 14 | }, 15 | "content_scripts": [ 16 | { 17 | "all_frames": true, 18 | "matches": [ "" ], 19 | "js": [ "build/content.js" ], 20 | "run_at": "document_start", 21 | "match_about_blank": true 22 | } 23 | ], 24 | "background": { 25 | "scripts": [ 26 | "build/background.js" 27 | ] 28 | }, 29 | "permissions": [ 30 | "history", 31 | "sessions", 32 | "storage", 33 | "tabs", 34 | "clipboardRead", 35 | "notifications", 36 | "bookmarks", 37 | "browserSettings" 38 | ], 39 | "web_accessible_resources": [ 40 | "build/console.html", 41 | "build/console.js" 42 | ], 43 | "options_ui": { 44 | "page": "build/settings.html" 45 | }, 46 | "browser_action": { 47 | "default_icon": { 48 | "32": "resources/enabled_32x32.png" 49 | }, 50 | "default_title": "Vim Vixen" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /resources/disabled_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ueokande/vim-vixen/bf2ce3f574ed673134b29ec431f1b783d7f56bef/resources/disabled_32x32.png -------------------------------------------------------------------------------- /resources/enabled_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ueokande/vim-vixen/bf2ce3f574ed673134b29ec431f1b783d7f56bef/resources/enabled_32x32.png -------------------------------------------------------------------------------- /resources/icon_48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ueokande/vim-vixen/bf2ce3f574ed673134b29ec431f1b783d7f56bef/resources/icon_48x48.png -------------------------------------------------------------------------------- /resources/icon_96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ueokande/vim-vixen/bf2ce3f574ed673134b29ec431f1b783d7f56bef/resources/icon_96x96.png -------------------------------------------------------------------------------- /script/package: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const manifest = require('../manifest'); 6 | const JSZip = require('jszip'); 7 | 8 | const resources = () => { 9 | return [ 10 | 'manifest.json', 11 | 'resources/disabled_32x32.png', 12 | 'resources/enabled_32x32.png', 13 | manifest.options_ui.page, 14 | 'build/settings.js', 15 | ].concat( 16 | Object.values(manifest.icons), 17 | manifest.background.scripts, 18 | manifest.content_scripts.map(cs => cs.js).reduce((a1, a2) => a1.concat(a2), []), 19 | manifest.web_accessible_resources, 20 | ).sort(); 21 | }; 22 | 23 | const output = `vim-vixen-${manifest.version}.zip` 24 | 25 | let basedir = path.join(__dirname, '..'); 26 | let zip = new JSZip(); 27 | 28 | for (let r of resources()) { 29 | console.log(` adding: ${r}`) 30 | let data = fs.readFileSync(path.join(basedir, r)); 31 | zip.file(r, data); 32 | } 33 | 34 | zip 35 | .generateNodeStream({ type: 'nodebuffer', streamFiles: true }) 36 | .pipe(fs.createWriteStream(output)) 37 | .on('finish', function () { 38 | console.log(`${output} created`); 39 | }); 40 | -------------------------------------------------------------------------------- /src/@types/browser/index.d.ts: -------------------------------------------------------------------------------- 1 | // NOTE: window.find is not standard API 2 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/find 3 | interface Window { 4 | find( 5 | aString: string, 6 | aCaseSensitive?: boolean, 7 | aBackwards?: boolean, 8 | aWrapAround?: boolean, 9 | aWholeWord?: boolean, 10 | aSearchInFrames?: boolean, 11 | aShowDialog?: boolean 12 | ): boolean; 13 | } 14 | -------------------------------------------------------------------------------- /src/@types/web-ext-api/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace browser.tabs { 2 | function toggleReaderMode(tabId?: number): Promise; 3 | } 4 | 5 | declare namespace browser.browserSettings.homepageOverride { 6 | type BrowserSettings = { 7 | value: string; 8 | levelOfControl: LevelOfControlType; 9 | }; 10 | 11 | type LevelOfControlType = 12 | | "not_controllable" 13 | | "controlled_by_other_extensions" 14 | | "controllable_by_this_extension" 15 | | "controlled_by_this_extension"; 16 | 17 | function get(param: { [key: string]: string }): Promise; 18 | } 19 | -------------------------------------------------------------------------------- /src/background/clients/ConsoleFrameClient.ts: -------------------------------------------------------------------------------- 1 | import * as messages from "../../shared/messages"; 2 | 3 | export default interface ConsoleFrameClient { 4 | resize(tabId: number, width: number, height: number): Promise; 5 | } 6 | 7 | export class ConsoleFrameClientImpl implements ConsoleFrameClient { 8 | async resize(tabId: number, width: number, height: number): Promise { 9 | await browser.tabs.sendMessage(tabId, { 10 | type: messages.CONSOLE_RESIZE, 11 | width, 12 | height, 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/clients/FindClient.ts: -------------------------------------------------------------------------------- 1 | import * as messages from "../../shared/messages"; 2 | 3 | export default interface FindClient { 4 | findNext(tabId: number, frameId: number, keyword: string): Promise; 5 | 6 | findPrev(tabId: number, frameId: number, keyword: string): Promise; 7 | 8 | clearSelection(tabId: number, frameId: number): Promise; 9 | } 10 | 11 | export class FindClientImpl implements FindClient { 12 | async findNext( 13 | tabId: number, 14 | frameId: number, 15 | keyword: string 16 | ): Promise { 17 | const found = (await browser.tabs.sendMessage( 18 | tabId, 19 | { type: messages.FIND_NEXT, keyword }, 20 | { frameId } 21 | )) as boolean; 22 | return found; 23 | } 24 | 25 | async findPrev( 26 | tabId: number, 27 | frameId: number, 28 | keyword: string 29 | ): Promise { 30 | const found = (await browser.tabs.sendMessage( 31 | tabId, 32 | { type: messages.FIND_PREV, keyword }, 33 | { frameId } 34 | )) as boolean; 35 | return found; 36 | } 37 | 38 | clearSelection(tabId: number, frameId: number): Promise { 39 | return browser.tabs.sendMessage( 40 | tabId, 41 | { type: messages.FIND_CLEAR_SELECTION }, 42 | { frameId } 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/background/clients/NavigateClient.ts: -------------------------------------------------------------------------------- 1 | import * as messages from "../../shared/messages"; 2 | 3 | export default interface NavigateClient { 4 | historyNext(tabId: number): Promise; 5 | 6 | historyPrev(tabId: number): Promise; 7 | 8 | linkNext(tabId: number): Promise; 9 | 10 | linkPrev(tabId: number): Promise; 11 | } 12 | 13 | export class NavigateClientImpl implements NavigateClient { 14 | async historyNext(tabId: number): Promise { 15 | await browser.tabs.sendMessage(tabId, { 16 | type: messages.NAVIGATE_HISTORY_NEXT, 17 | }); 18 | } 19 | 20 | async historyPrev(tabId: number): Promise { 21 | await browser.tabs.sendMessage(tabId, { 22 | type: messages.NAVIGATE_HISTORY_PREV, 23 | }); 24 | } 25 | 26 | async linkNext(tabId: number): Promise { 27 | await browser.tabs.sendMessage(tabId, { 28 | type: messages.NAVIGATE_LINK_NEXT, 29 | }); 30 | } 31 | 32 | async linkPrev(tabId: number): Promise { 33 | await browser.tabs.sendMessage(tabId, { 34 | type: messages.NAVIGATE_LINK_PREV, 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/background/completion/BookmarkRepository.ts: -------------------------------------------------------------------------------- 1 | export type BookmarkItem = { 2 | title: string; 3 | url: string; 4 | }; 5 | 6 | export default interface BookmarkRepository { 7 | queryBookmarks(query: string): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/background/completion/HistoryRepository.ts: -------------------------------------------------------------------------------- 1 | export type HistoryItem = { 2 | title: string; 3 | url: string; 4 | }; 5 | 6 | export default interface HistoryRepository { 7 | queryHistories(keywords: string): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/background/completion/PropertyCompletionUseCase.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import Properties from "../../shared/settings/Properties"; 3 | 4 | type Property = { 5 | name: string; 6 | type: "string" | "boolean" | "number"; 7 | }; 8 | @injectable() 9 | export default class PropertyCompletionUseCase { 10 | async getProperties(): Promise { 11 | return Properties.defs().map((def) => ({ 12 | name: def.name, 13 | type: def.type, 14 | })); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/background/completion/TabItem.ts: -------------------------------------------------------------------------------- 1 | import TabFlag from "../../shared/TabFlag"; 2 | 3 | type TabItem = { 4 | index: number; 5 | flag: TabFlag; 6 | title: string; 7 | url: string; 8 | faviconUrl?: string; 9 | }; 10 | 11 | export default TabItem; 12 | -------------------------------------------------------------------------------- /src/background/completion/TabRepository.ts: -------------------------------------------------------------------------------- 1 | export type Tab = { 2 | id: number; 3 | index: number; 4 | active: boolean; 5 | title: string; 6 | url: string; 7 | faviconUrl?: string; 8 | }; 9 | 10 | export default interface TabRepository { 11 | queryTabs(query: string, excludePinned: boolean): Promise; 12 | 13 | getAllTabs(excludePinned: boolean): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/background/completion/impl/BookmarkRepositoryImpl.ts: -------------------------------------------------------------------------------- 1 | import BookmarkRepository, { BookmarkItem } from "../BookmarkRepository"; 2 | 3 | const COMPLETION_ITEM_LIMIT = 10; 4 | 5 | export default class CachedBookmarkRepository implements BookmarkRepository { 6 | async queryBookmarks(query: string): Promise { 7 | const items = await browser.bookmarks.search({ query }); 8 | return items 9 | .filter((item) => item.title && item.title.length > 0) 10 | .filter((item) => item.type === "bookmark" && item.url) 11 | .filter((item) => { 12 | let url = undefined; 13 | try { 14 | url = new URL(item.url!); 15 | } catch (e) { 16 | return false; 17 | } 18 | return url.protocol !== "place:"; 19 | }) 20 | .slice(0, COMPLETION_ITEM_LIMIT) 21 | .map((item) => ({ 22 | title: item.title!, 23 | url: item.url!, 24 | })); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/background/completion/impl/HistoryRepositoryImpl.ts: -------------------------------------------------------------------------------- 1 | import * as filters from "./filters"; 2 | import HistoryRepository, { HistoryItem } from "../HistoryRepository"; 3 | 4 | const COMPLETION_ITEM_LIMIT = 10; 5 | 6 | export default class HistoryRepositoryImpl implements HistoryRepository { 7 | async queryHistories(keywords: string): Promise { 8 | const items = await browser.history.search({ 9 | text: keywords, 10 | startTime: 0, 11 | }); 12 | 13 | return [items] 14 | .map(filters.filterBlankTitle) 15 | .map(filters.filterHttp) 16 | .map(filters.filterByTailingSlash) 17 | .map((pages) => filters.filterByPathname(pages, COMPLETION_ITEM_LIMIT)) 18 | .map((pages) => filters.filterByOrigin(pages, COMPLETION_ITEM_LIMIT))[0] 19 | .sort((x, y) => Number(y.visitCount) - Number(x.visitCount)) 20 | .slice(0, COMPLETION_ITEM_LIMIT) 21 | .map((item) => ({ 22 | title: item.title!, 23 | url: item.url!, 24 | })); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/background/controllers/AddonEnabledController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import AddonEnabledUseCase from "../usecases/AddonEnabledUseCase"; 3 | 4 | @injectable() 5 | export default class AddonEnabledController { 6 | constructor(private addonEnabledUseCase: AddonEnabledUseCase) {} 7 | 8 | indicate(enabled: boolean): Promise { 9 | return this.addonEnabledUseCase.indicate(enabled); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/controllers/ConsoleController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import ConsoleUseCase from "../usecases/ConsoleUseCase"; 3 | 4 | @injectable() 5 | export default class ConsoleController { 6 | constructor(private readonly consoleUseCase: ConsoleUseCase) {} 7 | 8 | resize(senderTabId: number, width: number, height: number) { 9 | return this.consoleUseCase.resize(senderTabId, width, height); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/controllers/FindController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import StartFindUseCase from "../usecases/StartFindUseCase"; 3 | 4 | @injectable() 5 | export default class FindController { 6 | constructor(private startFindUseCase: StartFindUseCase) {} 7 | 8 | startFind(tabId: number, keyword?: string): Promise { 9 | return this.startFindUseCase.startFind(tabId, keyword); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/controllers/LinkController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import LinkUseCase from "../usecases/LinkUseCase"; 3 | 4 | @injectable() 5 | export default class LinkController { 6 | constructor(private linkUseCase: LinkUseCase) {} 7 | 8 | openToTab(url: string, tabId: number): Promise { 9 | return this.linkUseCase.openToTab(url, tabId); 10 | } 11 | 12 | openNewTab( 13 | url: string, 14 | openerId: number, 15 | background: boolean 16 | ): Promise { 17 | return this.linkUseCase.openNewTab(url, openerId, background); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/background/controllers/MarkController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import MarkUseCase from "../usecases/MarkUseCase"; 3 | 4 | @injectable() 5 | export default class MarkController { 6 | constructor(private markUseCase: MarkUseCase) {} 7 | 8 | setGlobal(key: string, x: number, y: number): Promise { 9 | return this.markUseCase.setGlobal(key, x, y); 10 | } 11 | 12 | jumpGlobal(key: string): Promise { 13 | return this.markUseCase.jumpGlobal(key); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/controllers/OperationController.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import * as operations from "../../shared/operations"; 3 | import OperatorFactory from "../operators/OperatorFactory"; 4 | import RepeatUseCase from "../usecases/RepeatUseCase"; 5 | 6 | @injectable() 7 | export default class OperationController { 8 | constructor( 9 | private readonly repeatUseCase: RepeatUseCase, 10 | @inject("OperatorFactory") 11 | private readonly operatorFactory: OperatorFactory 12 | ) {} 13 | 14 | async exec(repeat: number, op: operations.Operation): Promise { 15 | await this.doOperation(repeat, op); 16 | if (this.repeatUseCase.isRepeatable(op)) { 17 | this.repeatUseCase.storeLastOperation(op); 18 | } 19 | } 20 | 21 | private async doOperation( 22 | repeat: number, 23 | operation: operations.Operation 24 | ): Promise { 25 | const operator = this.operatorFactory.create(operation); 26 | for (let i = 0; i < repeat; ++i) { 27 | // eslint-disable-next-line no-await-in-loop 28 | await operator.run(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/background/controllers/SettingController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import SettingUseCase from "../usecases/SettingUseCase"; 3 | import ContentMessageClient from "../infrastructures/ContentMessageClient"; 4 | import Settings from "../../shared/settings/Settings"; 5 | 6 | @injectable() 7 | export default class SettingController { 8 | constructor( 9 | private settingUseCase: SettingUseCase, 10 | private contentMessageClient: ContentMessageClient 11 | ) {} 12 | 13 | getSetting(): Promise { 14 | return this.settingUseCase.getCached(); 15 | } 16 | 17 | async reload(): Promise { 18 | await this.settingUseCase.reload(); 19 | this.contentMessageClient.broadcastSettingsChanged(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/background/controllers/VersionController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import VersionUseCase from "../usecases/VersionUseCase"; 3 | 4 | @injectable() 5 | export default class VersionController { 6 | constructor(private versionUseCase: VersionUseCase) {} 7 | 8 | notify(): Promise { 9 | return this.versionUseCase.notify(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/domains/GlobalMark.ts: -------------------------------------------------------------------------------- 1 | export default interface GlobalMark { 2 | readonly tabId: number; 3 | readonly url: string; 4 | readonly x: number; 5 | readonly y: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { container } from "tsyringe"; 3 | import Application from "./Application"; 4 | import "./di"; 5 | 6 | const app = container.resolve(Application); 7 | app.run(); 8 | -------------------------------------------------------------------------------- /src/background/infrastructures/ContentMessageClient.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import * as messages from "../../shared/messages"; 3 | 4 | @injectable() 5 | export default class ContentMessageClient { 6 | async broadcastSettingsChanged(): Promise { 7 | const tabs = await browser.tabs.query({}); 8 | for (const tab of tabs) { 9 | if (!tab.id || (tab.url && tab.url.startsWith("about:"))) { 10 | continue; 11 | } 12 | browser.tabs.sendMessage(tab.id, { 13 | type: messages.SETTINGS_CHANGED, 14 | }); 15 | } 16 | } 17 | 18 | async getAddonEnabled(tabId: number): Promise { 19 | const enabled = await browser.tabs.sendMessage(tabId, { 20 | type: messages.ADDON_ENABLED_QUERY, 21 | }); 22 | return enabled as any as boolean; 23 | } 24 | 25 | async toggleAddonEnabled(tabId: number): Promise { 26 | await browser.tabs.sendMessage(tabId, { 27 | type: messages.ADDON_TOGGLE_ENABLED, 28 | }); 29 | } 30 | 31 | async scrollTo(tabId: number, x: number, y: number): Promise { 32 | await browser.tabs.sendMessage(tabId, { 33 | type: messages.TAB_SCROLL_TO, 34 | x, 35 | y, 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/background/infrastructures/FindPortListener.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | 3 | type OnConnectFunc = (port: browser.runtime.Port) => void; 4 | type OnDisconnectFunc = (port: browser.runtime.Port) => void; 5 | 6 | @injectable() 7 | export default class FindPortListener { 8 | constructor( 9 | private readonly onConnect: OnConnectFunc, 10 | private readonly onDisconnect: OnDisconnectFunc 11 | ) {} 12 | 13 | run(): void { 14 | browser.runtime.onConnect.addListener((port) => { 15 | if (port.name !== "vimvixen-find") { 16 | return; 17 | } 18 | 19 | port.onDisconnect.addListener(this.onDisconnect); 20 | this.onConnect(port); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/background/infrastructures/MemoryStorage.ts: -------------------------------------------------------------------------------- 1 | const db: { [key: string]: any } = {}; 2 | 3 | export default class MemoryStorage { 4 | set(name: string, value: any): void { 5 | const data = JSON.stringify(value); 6 | if (typeof data === "undefined") { 7 | throw new Error("value is not serializable"); 8 | } 9 | db[name] = data; 10 | } 11 | 12 | get(name: string): any { 13 | const data = db[name]; 14 | if (!data) { 15 | return undefined; 16 | } 17 | return JSON.parse(data); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/background/operators/Operator.ts: -------------------------------------------------------------------------------- 1 | interface Operator { 2 | run(): Promise; 3 | } 4 | 5 | export default Operator; 6 | -------------------------------------------------------------------------------- /src/background/operators/OperatorFactory.ts: -------------------------------------------------------------------------------- 1 | import Operator from "./Operator"; 2 | import { Operation } from "../../shared/operations"; 3 | 4 | export default interface OperatorFactory { 5 | create(op: Operation): Operator; 6 | } 7 | -------------------------------------------------------------------------------- /src/background/operators/OperatorFactoryChain.ts: -------------------------------------------------------------------------------- 1 | import Operator from "./Operator"; 2 | import { Operation } from "../../shared/operations"; 3 | 4 | export default interface OperatorFactoryChain { 5 | create(op: Operation): Operator | null; 6 | } 7 | -------------------------------------------------------------------------------- /src/background/operators/impls/CancelOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import ConsoleClient from "../../infrastructures/ConsoleClient"; 4 | 5 | export default class CancelOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly consoleClient: ConsoleClient 9 | ) {} 10 | 11 | async run(): Promise { 12 | const tab = await this.tabPresenter.getCurrent(); 13 | return this.consoleClient.hide(tab.id as number); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/operators/impls/CloseTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class CloseTabOperator implements Operator { 5 | constructor( 6 | private readonly tabPresenter: TabPresenter, 7 | private readonly force: boolean = false, 8 | private readonly selectLeft: boolean = false 9 | ) {} 10 | 11 | async run(): Promise { 12 | const tab = await this.tabPresenter.getCurrent(); 13 | if (!this.force && tab.pinned) { 14 | return Promise.resolve(); 15 | } 16 | if (this.selectLeft && tab.index > 0) { 17 | const tabs = await this.tabPresenter.getAll(); 18 | await this.tabPresenter.select(tabs[tab.index - 1].id as number); 19 | } 20 | return this.tabPresenter.remove([tab.id as number]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/background/operators/impls/CloseTabRightOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class CloseTabRightOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tabs = await this.tabPresenter.getAll(); 9 | tabs.sort((t1, t2) => t1.index - t2.index); 10 | const index = tabs.findIndex((t) => t.active); 11 | if (index < 0) { 12 | return; 13 | } 14 | for (let i = index + 1; i < tabs.length; ++i) { 15 | const tab = tabs[i]; 16 | if (!tab.pinned) { 17 | await this.tabPresenter.remove([tab.id as number]); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/background/operators/impls/DuplicateTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class DuplicateTabOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tab = await this.tabPresenter.getCurrent(); 9 | await this.tabPresenter.duplicate(tab.id as number); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/operators/impls/InternalOpenURLOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import WindowPresenter from "../../presenters/WindowPresenter"; 3 | import TabPresenter from "../../presenters/TabPresenter"; 4 | 5 | export default class InternalOpenURLOperator implements Operator { 6 | constructor( 7 | private readonly windowPresenter: WindowPresenter, 8 | private readonly tabPresenter: TabPresenter, 9 | private readonly url: string, 10 | private readonly newTab?: boolean, 11 | private readonly newWindow?: boolean 12 | ) {} 13 | 14 | async run(): Promise { 15 | if (this.newWindow) { 16 | await this.windowPresenter.create(this.url); 17 | } else if (this.newTab) { 18 | await this.tabPresenter.create(this.url); 19 | } else { 20 | const tab = await this.tabPresenter.getCurrent(); 21 | await this.tabPresenter.open(this.url, tab.id); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/background/operators/impls/NavigateHistoryNextOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import NavigateClient from "../../clients/NavigateClient"; 4 | 5 | export default class NavigateHistoryNextOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly navigateClient: NavigateClient 9 | ) {} 10 | 11 | async run(): Promise { 12 | const tab = await this.tabPresenter.getCurrent(); 13 | await this.navigateClient.historyNext(tab.id!); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/operators/impls/NavigateHistoryPrevOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import NavigateClient from "../../clients/NavigateClient"; 4 | 5 | export default class NavigateHistoryPrevOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly navigateClient: NavigateClient 9 | ) {} 10 | 11 | async run(): Promise { 12 | const tab = await this.tabPresenter.getCurrent(); 13 | await this.navigateClient.historyPrev(tab.id!); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/operators/impls/NavigateLinkNextOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import NavigateClient from "../../clients/NavigateClient"; 3 | import TabPresenter from "../../presenters/TabPresenter"; 4 | 5 | export default class NavigateLinkNextOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly navigateClient: NavigateClient 9 | ) {} 10 | 11 | async run(): Promise { 12 | const tab = await this.tabPresenter.getCurrent(); 13 | await this.navigateClient.linkNext(tab.id!); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/operators/impls/NavigateLinkPrevOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import NavigateClient from "../../clients/NavigateClient"; 4 | 5 | export default class NavigateLinkPrevOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly navigateClient: NavigateClient 9 | ) {} 10 | 11 | async run(): Promise { 12 | const tab = await this.tabPresenter.getCurrent(); 13 | await this.navigateClient.linkPrev(tab.id!); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/operators/impls/NavigateParentOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class NavigateParentOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tab = await this.tabPresenter.getCurrent(); 9 | const url = new URL(tab.url!); 10 | if (url.hash.length > 0) { 11 | url.hash = ""; 12 | } else if (url.search.length > 0) { 13 | url.search = ""; 14 | } else { 15 | const basenamePattern = /\/[^/]+$/; 16 | const lastDirPattern = /\/[^/]+\/$/; 17 | if (basenamePattern.test(url.pathname)) { 18 | url.pathname = url.pathname.replace(basenamePattern, "/"); 19 | } else if (lastDirPattern.test(url.pathname)) { 20 | url.pathname = url.pathname.replace(lastDirPattern, "/"); 21 | } 22 | } 23 | await this.tabPresenter.open(url.href); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/background/operators/impls/NavigateRootOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class NavigateRootOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tab = await this.tabPresenter.getCurrent(); 9 | const url = new URL(tab.url!); 10 | await this.tabPresenter.open(url.origin); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/background/operators/impls/OpenHomeOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import BrowserSettingRepository from "../../repositories/BrowserSettingRepository"; 4 | 5 | export default class OpenHomeOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly browserSettingRepository: BrowserSettingRepository, 9 | private readonly newTab: boolean 10 | ) {} 11 | 12 | async run(): Promise { 13 | const tab = await this.tabPresenter.getCurrent(); 14 | const urls = await this.browserSettingRepository.getHomepageUrls(); 15 | if (urls.length === 1 && urls[0] === "about:home") { 16 | // eslint-disable-next-line max-len 17 | throw new Error( 18 | "Cannot open Firefox Home (about:home) by WebExtensions, set your custom URLs" 19 | ); 20 | } 21 | if (urls.length === 1 && !this.newTab) { 22 | await this.tabPresenter.open(urls[0], tab.id); 23 | return; 24 | } 25 | for (const url of urls) { 26 | await this.tabPresenter.create(url); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/background/operators/impls/OpenSourceOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class OpenSourceOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tab = await this.tabPresenter.getCurrent(); 9 | const url = "view-source:" + tab.url; 10 | await this.tabPresenter.create(url); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/background/operators/impls/PinTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class PinTabOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tab = await this.tabPresenter.getCurrent(); 9 | return this.tabPresenter.setPinned(tab.id as number, true); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/operators/impls/ReloadTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class ReloadTabOperator implements Operator { 5 | constructor( 6 | private readonly tabPresenter: TabPresenter, 7 | private readonly cache: boolean 8 | ) {} 9 | 10 | async run(): Promise { 11 | const tab = await this.tabPresenter.getCurrent(); 12 | return this.tabPresenter.reload(tab.id as number, this.cache); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/background/operators/impls/ReopenTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class ReopenTabOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | run(): Promise { 8 | return this.tabPresenter.reopen(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/background/operators/impls/RepeatLastOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import RepeatRepository from "../../repositories/RepeatRepository"; 3 | import OperatorFactory from "../OperatorFactory"; 4 | 5 | export default class RepeatLastOperator implements Operator { 6 | constructor( 7 | private readonly repeatRepository: RepeatRepository, 8 | private readonly operatorFactory: OperatorFactory 9 | ) {} 10 | 11 | run(): Promise { 12 | const op = this.repeatRepository.getLastOperation(); 13 | if (typeof op === "undefined") { 14 | return Promise.resolve(); 15 | } 16 | return this.operatorFactory.create(op).run(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/background/operators/impls/RepeatOperatorFactoryChain.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import Operator from "../Operator"; 3 | import OperatorFactoryChain from "../OperatorFactoryChain"; 4 | import RepeatLastOperator from "./RepeatLastOperator"; 5 | import RepeatRepository from "../../repositories/RepeatRepository"; 6 | import OperatorFactory from "../OperatorFactory"; 7 | import * as operations from "../../../shared/operations"; 8 | 9 | @injectable() 10 | export default class RepeatOperatorFactoryChain 11 | implements OperatorFactoryChain 12 | { 13 | constructor( 14 | @inject("RepeatRepository") 15 | private readonly repeatRepository: RepeatRepository, 16 | @inject("OperatorFactory") 17 | private readonly operatorFactory: OperatorFactory 18 | ) {} 19 | 20 | create(op: operations.Operation): Operator | null { 21 | switch (op.type) { 22 | case operations.REPEAT_LAST: 23 | return new RepeatLastOperator( 24 | this.repeatRepository, 25 | this.operatorFactory 26 | ); 27 | } 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/background/operators/impls/ResetZoomOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import ZoomPresenter from "../../presenters/ZoomPresenter"; 3 | 4 | export default class ResetZoomOperator implements Operator { 5 | constructor(private readonly zoomPresenter: ZoomPresenter) {} 6 | 7 | run(): Promise { 8 | return this.zoomPresenter.resetZoom(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/background/operators/impls/SelectFirstTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class SelectFirstTabOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tabs = await this.tabPresenter.getAll(); 9 | return this.tabPresenter.select(tabs[0].id as number); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/operators/impls/SelectLastTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class SelectLastTabOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tabs = await this.tabPresenter.getAll(); 9 | return this.tabPresenter.select(tabs[tabs.length - 1].id as number); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/operators/impls/SelectPreviousSelectedTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class SelectPreviousSelectedTabOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tabId = await this.tabPresenter.getLastSelectedId(); 9 | if (tabId === null || typeof tabId === "undefined") { 10 | return Promise.resolve(); 11 | } 12 | return this.tabPresenter.select(tabId); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/background/operators/impls/SelectTabNextOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class SelectTabNextOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tabs = await this.tabPresenter.getAll(); 9 | if (tabs.length < 2) { 10 | return; 11 | } 12 | const tab = tabs.find((t) => t.active); 13 | if (!tab) { 14 | return; 15 | } 16 | const select = (tab.index + 1) % tabs.length; 17 | return this.tabPresenter.select(tabs[select].id as number); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/background/operators/impls/SelectTabPrevOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class SelectTabPrevOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tabs = await this.tabPresenter.getAll(); 9 | if (tabs.length < 2) { 10 | return; 11 | } 12 | const tab = tabs.find((t) => t.active); 13 | if (!tab) { 14 | return; 15 | } 16 | const select = (tab.index - 1 + tabs.length) % tabs.length; 17 | return this.tabPresenter.select(tabs[select].id as number); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/background/operators/impls/ShowAddBookmarkOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import ConsoleClient from "../../infrastructures/ConsoleClient"; 4 | 5 | export default class ShowAddBookmarkOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly consoleClient: ConsoleClient, 9 | private readonly alter: boolean 10 | ) {} 11 | 12 | async run(): Promise { 13 | const tab = await this.tabPresenter.getCurrent(); 14 | let command = "addbookmark "; 15 | if (this.alter) { 16 | command += tab.title || ""; 17 | } 18 | return this.consoleClient.showCommand(tab.id as number, command); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/background/operators/impls/ShowBufferCommandOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import ConsoleClient from "../../infrastructures/ConsoleClient"; 4 | 5 | export default class ShowBufferCommandOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly consoleClient: ConsoleClient 9 | ) {} 10 | 11 | async run(): Promise { 12 | const tab = await this.tabPresenter.getCurrent(); 13 | const command = "buffer "; 14 | return this.consoleClient.showCommand(tab.id as number, command); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/background/operators/impls/ShowCommandOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import ConsoleClient from "../../infrastructures/ConsoleClient"; 4 | 5 | export default class ShowCommandOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly consoleClient: ConsoleClient 9 | ) {} 10 | 11 | async run(): Promise { 12 | const tab = await this.tabPresenter.getCurrent(); 13 | return this.consoleClient.showCommand(tab.id as number, ""); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/operators/impls/ShowOpenCommandOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import ConsoleClient from "../../infrastructures/ConsoleClient"; 4 | 5 | export default class ShowOpenCommandOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly consoleClient: ConsoleClient, 9 | private readonly alter: boolean 10 | ) {} 11 | 12 | async run(): Promise { 13 | const tab = await this.tabPresenter.getCurrent(); 14 | let command = "open "; 15 | if (this.alter) { 16 | command += tab.url || ""; 17 | } 18 | return this.consoleClient.showCommand(tab.id as number, command); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/background/operators/impls/ShowTabOpenCommandOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import ConsoleClient from "../../infrastructures/ConsoleClient"; 4 | 5 | export default class ShowTabOpenCommandOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly consoleClient: ConsoleClient, 9 | private readonly alter: boolean 10 | ) {} 11 | 12 | async run(): Promise { 13 | const tab = await this.tabPresenter.getCurrent(); 14 | let command = "tabopen "; 15 | if (this.alter) { 16 | command += tab.url || ""; 17 | } 18 | return this.consoleClient.showCommand(tab.id as number, command); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/background/operators/impls/ShowWinOpenCommandOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import ConsoleClient from "../../infrastructures/ConsoleClient"; 4 | 5 | export default class ShowWinOpenCommandOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly consoleClient: ConsoleClient, 9 | private readonly alter: boolean 10 | ) {} 11 | 12 | async run(): Promise { 13 | const tab = await this.tabPresenter.getCurrent(); 14 | let command = "winopen "; 15 | if (this.alter) { 16 | command += tab.url || ""; 17 | } 18 | return this.consoleClient.showCommand(tab.id as number, command); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/background/operators/impls/StartFindOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | import ConsoleClient from "../../infrastructures/ConsoleClient"; 4 | 5 | export default class StartFindOperator implements Operator { 6 | constructor( 7 | private readonly tabPresenter: TabPresenter, 8 | private readonly consoleClient: ConsoleClient 9 | ) {} 10 | 11 | async run(): Promise { 12 | const tab = await this.tabPresenter.getCurrent(); 13 | return this.consoleClient.showFind(tab.id as number); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/background/operators/impls/TogglePinnedTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class TogglePinnedTabOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tab = await this.tabPresenter.getCurrent(); 9 | return this.tabPresenter.setPinned(tab.id as number, !tab.pinned); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/operators/impls/ToggleReaderOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class ToggleReaderOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tab = await this.tabPresenter.getCurrent(); 9 | return this.tabPresenter.toggleReaderMode(tab.id as number); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/operators/impls/UnpinTabOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import TabPresenter from "../../presenters/TabPresenter"; 3 | 4 | export default class UnpinTabOperator implements Operator { 5 | constructor(private readonly tabPresenter: TabPresenter) {} 6 | 7 | async run(): Promise { 8 | const tab = await this.tabPresenter.getCurrent(); 9 | return this.tabPresenter.setPinned(tab.id as number, false); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/background/operators/impls/ZoomInOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import ZoomPresenter from "../../presenters/ZoomPresenter"; 3 | 4 | export default class ZoomInOperator implements Operator { 5 | constructor(private readonly zoomPresenter: ZoomPresenter) {} 6 | 7 | run(): Promise { 8 | return this.zoomPresenter.zoomIn(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/background/operators/impls/ZoomOperatorFactoryChain.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import Operator from "../Operator"; 3 | import OperatorFactoryChain from "../OperatorFactoryChain"; 4 | import ZoomInOperator from "./ZoomInOperator"; 5 | import ZoomOutOperator from "./ZoomOutOperator"; 6 | import ResetZoomOperator from "./ResetZoomOperator"; 7 | import ZoomPresenter from "../../presenters/ZoomPresenter"; 8 | import * as operations from "../../../shared/operations"; 9 | 10 | @injectable() 11 | export default class ZoomOperatorFactoryChain implements OperatorFactoryChain { 12 | constructor( 13 | @inject("ZoomPresenter") 14 | private readonly zoomPresenter: ZoomPresenter 15 | ) {} 16 | 17 | create(op: operations.Operation): Operator | null { 18 | switch (op.type) { 19 | case operations.ZOOM_IN: 20 | return new ZoomInOperator(this.zoomPresenter); 21 | case operations.ZOOM_OUT: 22 | return new ZoomOutOperator(this.zoomPresenter); 23 | case operations.ZOOM_NEUTRAL: 24 | return new ResetZoomOperator(this.zoomPresenter); 25 | } 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/background/operators/impls/ZoomOutOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import ZoomPresenter from "../../presenters/ZoomPresenter"; 3 | 4 | export default class ZoomOutOperator implements Operator { 5 | constructor(private readonly zoomPresenter: ZoomPresenter) {} 6 | 7 | run(): Promise { 8 | return this.zoomPresenter.zoomOut(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/background/presenters/HelpPresenter.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | 3 | const url = "https://ueokande.github.io/vim-vixen/"; 4 | 5 | @injectable() 6 | export default class HelpPresenter { 7 | async open(): Promise { 8 | await browser.tabs.create({ url, active: true }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/background/presenters/IndicatorPresenter.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | 3 | @injectable() 4 | export default class IndicatorPresenter { 5 | indicate(enabled: boolean): Promise { 6 | const path = enabled 7 | ? "resources/enabled_32x32.png" 8 | : "resources/disabled_32x32.png"; 9 | if (typeof browser.browserAction.setIcon === "function") { 10 | return browser.browserAction.setIcon({ path }); 11 | } 12 | 13 | // setIcon not supported on Android 14 | return Promise.resolve(); 15 | } 16 | 17 | onClick(listener: (arg: browser.tabs.Tab) => void): void { 18 | browser.browserAction.onClicked.addListener(listener); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/background/presenters/WindowPresenter.ts: -------------------------------------------------------------------------------- 1 | export default interface WindowPresenter { 2 | create(url: string): Promise; 3 | } 4 | 5 | export class WindowPresenterImpl implements WindowPresenter { 6 | async create(url: string): Promise { 7 | await browser.windows.create({ url }); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/background/repositories/BookmarkRepository.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | 3 | @injectable() 4 | export default class BookmarkRepository { 5 | async create( 6 | title: string, 7 | url: string 8 | ): Promise { 9 | const item = await browser.bookmarks.create({ 10 | type: "bookmark", 11 | title, 12 | url, 13 | }); 14 | if (!item) { 15 | throw new Error("Could not create a bookmark"); 16 | } 17 | return item; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/background/repositories/BrowserSettingRepository.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import * as urls from "../../shared/urls"; 3 | 4 | export default interface BrowserSettingRepository { 5 | getHomepageUrls(): Promise; 6 | } 7 | 8 | @injectable() 9 | export class BrowserSettingRepositoryImpl implements BrowserSettingRepository { 10 | async getHomepageUrls(): Promise { 11 | const { value } = await browser.browserSettings.homepageOverride.get({}); 12 | return value.split("|").map(urls.normalizeUrl); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/background/repositories/MarkRepository.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import MemoryStorage from "../infrastructures/MemoryStorage"; 3 | import GlobalMark from "../domains/GlobalMark"; 4 | 5 | const MARK_KEY = "mark"; 6 | 7 | @injectable() 8 | export default class MarkRepository { 9 | private cache: MemoryStorage; 10 | 11 | constructor() { 12 | this.cache = new MemoryStorage(); 13 | } 14 | 15 | getMark(key: string): Promise { 16 | const marks = this.getOrEmptyMarks(); 17 | const data = marks[key]; 18 | if (!data) { 19 | return Promise.resolve(undefined); 20 | } 21 | const mark = { tabId: data.tabId, url: data.url, x: data.x, y: data.y }; 22 | return Promise.resolve(mark); 23 | } 24 | 25 | setMark(key: string, mark: GlobalMark): Promise { 26 | const marks = this.getOrEmptyMarks(); 27 | marks[key] = { tabId: mark.tabId, url: mark.url, x: mark.x, y: mark.y }; 28 | this.cache.set(MARK_KEY, marks); 29 | 30 | return Promise.resolve(); 31 | } 32 | 33 | getOrEmptyMarks() { 34 | return this.cache.get(MARK_KEY) || {}; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/background/repositories/RepeatRepository.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import { Operation } from "../../shared/operations"; 3 | import MemoryStorage from "../infrastructures/MemoryStorage"; 4 | 5 | const REPEAT_KEY = "repeat"; 6 | 7 | export default interface RepeatRepository { 8 | getLastOperation(): Operation | undefined; 9 | 10 | setLastOperation(op: Operation): void; 11 | } 12 | 13 | @injectable() 14 | export class RepeatRepositoryImpl implements RepeatRepository { 15 | private cache: MemoryStorage; 16 | 17 | constructor() { 18 | this.cache = new MemoryStorage(); 19 | } 20 | 21 | getLastOperation(): Operation | undefined { 22 | return this.cache.get(REPEAT_KEY); 23 | } 24 | 25 | setLastOperation(op: Operation): void { 26 | this.cache.set(REPEAT_KEY, op); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/background/usecases/AddonEnabledUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import IndicatorPresenter from "../presenters/IndicatorPresenter"; 3 | import TabPresenter from "../presenters/TabPresenter"; 4 | import ContentMessageClient from "../infrastructures/ContentMessageClient"; 5 | 6 | @injectable() 7 | export default class AddonEnabledUseCase { 8 | constructor( 9 | private indicatorPresentor: IndicatorPresenter, 10 | @inject("TabPresenter") private tabPresenter: TabPresenter, 11 | private contentMessageClient: ContentMessageClient 12 | ) { 13 | this.indicatorPresentor.onClick((tab) => { 14 | if (tab.id) { 15 | this.onIndicatorClick(tab.id); 16 | } 17 | }); 18 | this.tabPresenter.onSelected((info) => this.onTabSelected(info.tabId)); 19 | } 20 | 21 | indicate(enabled: boolean): Promise { 22 | return this.indicatorPresentor.indicate(enabled); 23 | } 24 | 25 | private onIndicatorClick(tabId: number): Promise { 26 | return this.contentMessageClient.toggleAddonEnabled(tabId); 27 | } 28 | 29 | async onTabSelected(tabId: number): Promise { 30 | const enabled = await this.contentMessageClient.getAddonEnabled(tabId); 31 | return this.indicatorPresentor.indicate(enabled); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/background/usecases/ConsoleUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import ConsoleFrameClient from "../clients/ConsoleFrameClient"; 3 | 4 | @injectable() 5 | export default class ConsoleUseCase { 6 | constructor( 7 | @inject("ConsoleFrameClient") 8 | private readonly consoleFrameClient: ConsoleFrameClient 9 | ) {} 10 | 11 | async resize(tabId: number, width: number, height: number): Promise { 12 | return this.consoleFrameClient.resize(tabId, width, height); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/background/usecases/LinkUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import TabPresenter from "../presenters/TabPresenter"; 3 | 4 | @injectable() 5 | export default class LinkUseCase { 6 | constructor(@inject("TabPresenter") private tabPresenter: TabPresenter) {} 7 | 8 | async openToTab(url: string, tabId: number): Promise { 9 | await this.tabPresenter.open(url, tabId); 10 | } 11 | 12 | async openNewTab( 13 | url: string, 14 | openerId: number, 15 | background: boolean 16 | ): Promise { 17 | const properties: any = { active: !background }; 18 | 19 | const platform = await browser.runtime.getPlatformInfo(); 20 | if (platform.os !== "android") { 21 | // openerTabId not supported on Android 22 | properties.openerTabId = openerId; 23 | } 24 | 25 | await this.tabPresenter.create(url, properties); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/background/usecases/VersionUseCase.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "tsyringe"; 2 | import TabPresenter from "../presenters/TabPresenter"; 3 | import Notifier from "../presenters/Notifier"; 4 | 5 | @injectable() 6 | export default class VersionUseCase { 7 | constructor( 8 | @inject("TabPresenter") private tabPresenter: TabPresenter, 9 | @inject("Notifier") private notifier: Notifier 10 | ) {} 11 | 12 | notify(): Promise { 13 | const manifest = browser.runtime.getManifest(); 14 | const url = this.releaseNoteUrl(manifest.version); 15 | return this.notifier.notifyUpdated(manifest.version, () => { 16 | this.tabPresenter.create(url); 17 | }); 18 | } 19 | 20 | releaseNoteUrl(version?: string): string { 21 | if (version) { 22 | return `https://github.com/ueokande/vim-vixen/releases/tag/v${version}`; 23 | } 24 | return "https://github.com/ueokande/vim-vixen/releases/"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/background/usecases/parsers.ts: -------------------------------------------------------------------------------- 1 | import Properties from "../../shared/settings/Properties"; 2 | 3 | const mustNumber = (v: any): number => { 4 | const num = Number(v); 5 | if (isNaN(num)) { 6 | throw new Error("Not number: " + v); 7 | } 8 | return num; 9 | }; 10 | 11 | const parseSetOption = (args: string): any[] => { 12 | let [key, value]: any[] = args.split("="); 13 | if (value === undefined) { 14 | value = !key.startsWith("no"); 15 | key = value ? key : key.slice(2); 16 | } 17 | const def = Properties.def(key); 18 | if (!def) { 19 | throw new Error("Unknown property: " + key); 20 | } 21 | if ( 22 | (def.type === "boolean" && typeof value !== "boolean") || 23 | (def.type !== "boolean" && typeof value === "boolean") 24 | ) { 25 | throw new Error("Invalid argument: " + args); 26 | } 27 | 28 | switch (def.type) { 29 | case "string": 30 | return [key, value]; 31 | case "number": 32 | return [key, mustNumber(value)]; 33 | case "boolean": 34 | return [key, value]; 35 | default: 36 | throw new Error("Unknown property type: " + def.type); 37 | } 38 | }; 39 | 40 | export { parseSetOption }; 41 | -------------------------------------------------------------------------------- /src/console/Completions.ts: -------------------------------------------------------------------------------- 1 | type Completions = { 2 | readonly name: string; 3 | readonly items: { 4 | readonly primary?: string; 5 | readonly secondary?: string; 6 | readonly value?: string; 7 | readonly icon?: string; 8 | }[]; 9 | }[]; 10 | 11 | export default Completions; 12 | -------------------------------------------------------------------------------- /src/console/app/contexts.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { State, defaultState } from "./recuer"; 3 | import { AppAction } from "./actions"; 4 | 5 | export const AppStateContext = React.createContext(defaultState); 6 | 7 | export const AppDispatchContext = React.createContext< 8 | (action: AppAction) => void 9 | >(() => {}); 10 | -------------------------------------------------------------------------------- /src/console/app/provider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import reducer, { defaultState } from "./recuer"; 3 | import { AppDispatchContext, AppStateContext } from "./contexts"; 4 | 5 | export const AppProvider: React.FC = ({ children }) => { 6 | const [state, dispatch] = React.useReducer(reducer, defaultState); 7 | return ( 8 | 9 | 10 | {children} 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/console/clients/ConsoleFrameClient.ts: -------------------------------------------------------------------------------- 1 | import * as messages from "../../shared/messages"; 2 | 3 | export default class ConsoleFrameClient { 4 | async resize(width: number, height: number): Promise { 5 | await browser.runtime.sendMessage({ 6 | type: messages.CONSOLE_RESIZE, 7 | width, 8 | height, 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/console/clients/SettingClient.ts: -------------------------------------------------------------------------------- 1 | import Settings from "../../shared/settings/Settings"; 2 | import * as messages from "../../shared/messages"; 3 | import ColorScheme from "../../shared/ColorScheme"; 4 | 5 | export default class SettingClient { 6 | async getColorScheme(): Promise { 7 | const json = await browser.runtime.sendMessage({ 8 | type: messages.SETTINGS_QUERY, 9 | }); 10 | const settings = Settings.fromJSON(json); 11 | return settings.properties.colorscheme; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/console/colorscheme/contexts.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ColorScheme from "../../shared/ColorScheme"; 3 | 4 | export const ColorSchemeContext = React.createContext( 5 | ColorScheme.System 6 | ); 7 | 8 | export const ColorSchemeUpdateContext = React.createContext< 9 | (colorscheme: ColorScheme) => void 10 | >(() => {}); 11 | -------------------------------------------------------------------------------- /src/console/colorscheme/hooks.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ColorSchemeUpdateContext } from "./contexts"; 3 | import SettingClient from "../clients/SettingClient"; 4 | 5 | export const useColorSchemeRefresh = () => { 6 | const update = React.useContext(ColorSchemeUpdateContext); 7 | const settingClient = new SettingClient(); 8 | const refresh = React.useCallback(() => { 9 | settingClient.getColorScheme().then((newScheme) => { 10 | update(newScheme); 11 | }); 12 | }, []); 13 | 14 | return refresh; 15 | }; 16 | -------------------------------------------------------------------------------- /src/console/colorscheme/providers.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ColorScheme from "../../shared/ColorScheme"; 3 | import { DarkTheme, LightTheme } from "./theme"; 4 | import { ColorSchemeContext, ColorSchemeUpdateContext } from "./contexts"; 5 | import { ThemeProvider } from "styled-components"; 6 | 7 | export const ColorSchemeProvider: React.FC = ({ children }) => { 8 | const [colorscheme, setColorScheme] = React.useState(ColorScheme.System); 9 | const theme = React.useMemo(() => { 10 | if (colorscheme === ColorScheme.System) { 11 | if ( 12 | window.matchMedia && 13 | window.matchMedia("(prefers-color-scheme: dark)").matches 14 | ) { 15 | return DarkTheme; 16 | } 17 | } else if (colorscheme === ColorScheme.Dark) { 18 | return DarkTheme; 19 | } 20 | return LightTheme; 21 | }, [colorscheme]); 22 | 23 | return ( 24 | 25 | 26 | {children} 27 | 28 | 29 | ); 30 | }; 31 | export default ColorSchemeProvider; 32 | -------------------------------------------------------------------------------- /src/console/colorscheme/styled.tsx: -------------------------------------------------------------------------------- 1 | import baseStyled, { ThemedStyledInterface } from "styled-components"; 2 | import { ThemeProperties } from "./theme"; 3 | 4 | const styled = baseStyled as ThemedStyledInterface; 5 | 6 | export default styled; 7 | -------------------------------------------------------------------------------- /src/console/commandline/CommandLineParser.ts: -------------------------------------------------------------------------------- 1 | import CommandParser from "./CommandParser"; 2 | import { Command } from "../../shared/Command"; 3 | 4 | export type CommandLine = { 5 | readonly command: Command; 6 | readonly args: string; 7 | }; 8 | 9 | export enum InputPhase { 10 | OnCommand, 11 | OnArgs, 12 | } 13 | 14 | export default class CommandLineParser { 15 | private commandParser: CommandParser = new CommandParser(); 16 | 17 | inputPhase(line: string): InputPhase { 18 | line = line.trimLeft(); 19 | if (line.length == 0) { 20 | return InputPhase.OnCommand; 21 | } 22 | const command = line.split(/\s+/, 1)[0]; 23 | if (line.length == command.length) { 24 | return InputPhase.OnCommand; 25 | } 26 | return InputPhase.OnArgs; 27 | } 28 | 29 | parse(line: string): CommandLine { 30 | const trimLeft = line.trimLeft(); 31 | const command = trimLeft.split(/\s+/, 1)[0]; 32 | const args = trimLeft.slice(command.length).trimLeft(); 33 | return { 34 | command: this.commandParser.parse(command), 35 | args: args, 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/console/completion/context.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { State, defaultState } from "./reducer"; 3 | import { CompletionAction } from "./actions"; 4 | 5 | export const CompletionStateContext = React.createContext(defaultState); 6 | 7 | export const CompletionDispatchContext = React.createContext< 8 | (action: CompletionAction) => void 9 | >(() => {}); 10 | -------------------------------------------------------------------------------- /src/console/completion/hooks/clients.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CompletionClient from "../../clients/CompletionClient"; 3 | import CompletionType from "../../../shared/CompletionType"; 4 | 5 | const completionClient = new CompletionClient(); 6 | 7 | export const useGetCompletionTypes = (): [ 8 | CompletionType[] | undefined, 9 | boolean 10 | ] => { 11 | type State = { 12 | loading: boolean; 13 | result?: CompletionType[]; 14 | }; 15 | const [state, setState] = React.useState({ loading: true }); 16 | 17 | React.useEffect(() => { 18 | completionClient.getCompletionTypes().then((result) => { 19 | setState({ loading: false, result }); 20 | }); 21 | }, []); 22 | return [state.result, state.loading]; 23 | }; 24 | -------------------------------------------------------------------------------- /src/console/completion/provider.tsx: -------------------------------------------------------------------------------- 1 | import reducer, { defaultState } from "./reducer"; 2 | import React from "react"; 3 | import { CompletionDispatchContext, CompletionStateContext } from "./context"; 4 | 5 | interface Props { 6 | initialInputValue: string; 7 | } 8 | 9 | export const CompletionProvider: React.FC = ({ 10 | initialInputValue, 11 | children, 12 | }) => { 13 | const initialState = { 14 | ...defaultState, 15 | completionSource: initialInputValue, 16 | }; 17 | const [state, dispatch] = React.useReducer(reducer, initialState); 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/console/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "../colorscheme/styled"; 3 | 4 | const Wrapper = styled.p` 5 | border-top: 1px solid gray; 6 | background-color: ${({ theme }) => theme.consoleErrorBackground}; 7 | color: ${({ theme }) => theme.consoleErrorForeground}; 8 | font-weight: bold; 9 | `; 10 | 11 | const ErrorMessage: React.FC = ({ children }) => { 12 | return {children}; 13 | }; 14 | 15 | export default ErrorMessage; 16 | -------------------------------------------------------------------------------- /src/console/components/InfoMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "../colorscheme/styled"; 3 | 4 | const Wrapper = styled.p` 5 | border-top: 1px solid gray; 6 | background-color: ${({ theme }) => theme.consoleInfoBackground}; 7 | color: ${({ theme }) => theme.consoleInfoForeground}; 8 | font-weight: normal; 9 | `; 10 | 11 | const InfoMessage: React.FC = ({ children }) => { 12 | return {children}; 13 | }; 14 | 15 | export default InfoMessage; 16 | -------------------------------------------------------------------------------- /src/console/components/console/CompletionTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "../../colorscheme/styled"; 3 | 4 | const Li = styled.li<{ shown: boolean }>` 5 | display: ${({ shown }) => (shown ? "display" : "none")}; 6 | background-color: ${({ theme }) => theme.completionTitleBackground}; 7 | color: ${({ theme }) => theme.completionTitleForeground}; 8 | font-weight: bold; 9 | margin: 0; 10 | padding: 0; 11 | `; 12 | 13 | interface Props extends React.HTMLAttributes { 14 | shown: boolean; 15 | title: string; 16 | } 17 | 18 | const CompletionTitle: React.FC = (props) => ( 19 |
  • {props.title}
  • 20 | ); 21 | 22 | export default CompletionTitle; 23 | -------------------------------------------------------------------------------- /src/console/hooks/useAutoResize.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ConsoleFrameClient from "../clients/ConsoleFrameClient"; 3 | 4 | const useAutoResize = () => { 5 | const [prevWidth, setPrevWidth] = React.useState(-1); 6 | const [prevHeight, setPrevHeight] = React.useState(-1); 7 | 8 | const consoleFrameClient = React.useMemo(() => { 9 | return new ConsoleFrameClient(); 10 | }, []); 11 | 12 | React.useLayoutEffect(() => { 13 | const { scrollWidth: width, scrollHeight: height } = 14 | document.getElementById("vimvixen-console")!; 15 | consoleFrameClient.resize(width, height); 16 | 17 | if (width === prevWidth && height === prevHeight) { 18 | return; 19 | } 20 | 21 | setPrevWidth(width); 22 | setPrevHeight(height); 23 | }); 24 | }; 25 | 26 | export default useAutoResize; 27 | -------------------------------------------------------------------------------- /src/console/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const useDebounce = (value: T, delay: number) => { 4 | const [debouncedValue, setDebouncedValue] = React.useState(value); 5 | 6 | React.useEffect(() => { 7 | const timer = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(timer); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | }; 18 | 19 | export default useDebounce; 20 | -------------------------------------------------------------------------------- /src/console/index.css: -------------------------------------------------------------------------------- 1 | html, body, * { 2 | margin: 0; 3 | padding: 0; 4 | 5 | font-style: normal; 6 | font-family: monospace; 7 | font-size: 12px; 8 | line-height: 16px; 9 | } 10 | 11 | input { 12 | font-style: normal; 13 | font-family: monospace; 14 | font-size: 12px; 15 | line-height: 16px; 16 | } 17 | 18 | body { 19 | position: absolute; 20 | bottom: 0; 21 | left: 0; 22 | right: 0; 23 | overflow: hidden; 24 | } 25 | 26 | .vimvixen-console { 27 | bottom: 0; 28 | margin: 0; 29 | padding: 0; 30 | } 31 | -------------------------------------------------------------------------------- /src/console/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | VimVixen console 6 | 7 | 8 | 9 |
    10 | 11 | 12 | -------------------------------------------------------------------------------- /src/console/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import ColorSchemeProvider from "./colorscheme/providers"; 4 | import { AppProvider } from "./app/provider"; 5 | import App from "./App"; 6 | import "./index.css"; 7 | 8 | window.addEventListener("DOMContentLoaded", () => { 9 | const wrapper = document.getElementById("vimvixen-console"); 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | , 18 | wrapper 19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /src/content/Bootstrap.ts: -------------------------------------------------------------------------------- 1 | type Callback = () => void; 2 | 3 | export default class Bootstrap { 4 | constructor() {} 5 | 6 | isReady(): boolean { 7 | return document.body !== null; 8 | } 9 | 10 | waitForReady(callback: Callback): void { 11 | const observer = new MutationObserver(() => { 12 | if (document.body != null) { 13 | observer.disconnect(); 14 | callback(); 15 | } 16 | }); 17 | 18 | observer.observe(document, { 19 | attributes: false, 20 | attributeOldValue: false, 21 | characterData: false, 22 | characterDataOldValue: false, 23 | childList: true, 24 | subtree: true, 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/content/MessageListener.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import { Message, valueOf } from "../shared/messages"; 3 | 4 | export type WebExtMessageSender = browser.runtime.MessageSender; 5 | 6 | @injectable() 7 | export default class MessageListener { 8 | onWebMessage(listener: (msg: Message, sender: Window) => void) { 9 | window.addEventListener("message", (event: MessageEvent) => { 10 | const sender = event.source; 11 | if (!(sender instanceof Window)) { 12 | return; 13 | } 14 | let message = null; 15 | try { 16 | message = JSON.parse(event.data); 17 | } catch (e) { 18 | // ignore unexpected message 19 | return; 20 | } 21 | listener(message, sender); 22 | }); 23 | } 24 | 25 | onBackgroundMessage( 26 | listener: (msg: Message, sender: WebExtMessageSender) => any 27 | ) { 28 | browser.runtime.onMessage.addListener( 29 | (msg: any, sender: WebExtMessageSender) => { 30 | try { 31 | return Promise.resolve(listener(valueOf(msg), sender)); 32 | } catch (e) { 33 | console.warn(e); 34 | return; 35 | } 36 | } 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/content/client/AddonIndicatorClient.ts: -------------------------------------------------------------------------------- 1 | import * as messages from "../../shared/messages"; 2 | 3 | export default interface AddonIndicatorClient { 4 | setEnabled(enabled: boolean): Promise; 5 | } 6 | 7 | export class AddonIndicatorClientImpl implements AddonIndicatorClient { 8 | setEnabled(enabled: boolean): Promise { 9 | return browser.runtime.sendMessage({ 10 | type: messages.ADDON_ENABLED_RESPONSE, 11 | enabled, 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/content/client/ConsoleClient.ts: -------------------------------------------------------------------------------- 1 | import * as messages from "../../shared/messages"; 2 | 3 | export default interface ConsoleClient { 4 | info(text: string): Promise; 5 | error(text: string): Promise; 6 | } 7 | 8 | export class ConsoleClientImpl implements ConsoleClient { 9 | async info(text: string): Promise { 10 | await browser.runtime.sendMessage({ 11 | type: messages.CONSOLE_FRAME_MESSAGE, 12 | message: { 13 | type: messages.CONSOLE_SHOW_INFO, 14 | text, 15 | }, 16 | }); 17 | } 18 | 19 | async error(text: string): Promise { 20 | await browser.runtime.sendMessage({ 21 | type: messages.CONSOLE_FRAME_MESSAGE, 22 | message: { 23 | type: messages.CONSOLE_SHOW_ERROR, 24 | text, 25 | }, 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/content/client/FollowMasterClient.ts: -------------------------------------------------------------------------------- 1 | import * as messages from "../../shared/messages"; 2 | import Key from "../../shared/settings/Key"; 3 | 4 | export default interface FollowMasterClient { 5 | startFollow(newTab: boolean, background: boolean): void; 6 | 7 | responseHintCount(count: number): void; 8 | 9 | sendKey(key: Key): void; 10 | } 11 | 12 | export class FollowMasterClientImpl implements FollowMasterClient { 13 | private window: Window; 14 | 15 | constructor(window: Window) { 16 | this.window = window; 17 | } 18 | 19 | startFollow(newTab: boolean, background: boolean): void { 20 | this.postMessage({ 21 | type: messages.FOLLOW_START, 22 | newTab, 23 | background, 24 | }); 25 | } 26 | 27 | responseHintCount(count: number): void { 28 | this.postMessage({ 29 | type: messages.FOLLOW_RESPONSE_COUNT_TARGETS, 30 | count, 31 | }); 32 | } 33 | 34 | sendKey(key: Key): void { 35 | this.postMessage({ 36 | type: messages.FOLLOW_KEY_PRESS, 37 | key: key.key, 38 | ctrlKey: key.ctrl || false, 39 | }); 40 | } 41 | 42 | private postMessage(msg: messages.Message): void { 43 | this.window.postMessage(JSON.stringify(msg), "*"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/content/client/FollowSlaveClientFactory.ts: -------------------------------------------------------------------------------- 1 | import FollowSlaveClient, { FollowSlaveClientImpl } from "./FollowSlaveClient"; 2 | 3 | export default interface FollowSlaveClientFactory { 4 | create(window: Window): FollowSlaveClient; 5 | } 6 | 7 | export class FollowSlaveClientFactoryImpl implements FollowSlaveClientFactory { 8 | create(window: Window): FollowSlaveClient { 9 | return new FollowSlaveClientImpl(window); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/content/client/MarkClient.ts: -------------------------------------------------------------------------------- 1 | import Mark from "../domains/Mark"; 2 | import * as messages from "../../shared/messages"; 3 | 4 | export default interface MarkClient { 5 | setGloablMark(key: string, mark: Mark): Promise; 6 | 7 | jumpGlobalMark(key: string): Promise; 8 | } 9 | 10 | export class MarkClientImpl implements MarkClient { 11 | async setGloablMark(key: string, mark: Mark): Promise { 12 | await browser.runtime.sendMessage({ 13 | type: messages.MARK_SET_GLOBAL, 14 | key, 15 | x: mark.x, 16 | y: mark.y, 17 | }); 18 | } 19 | 20 | async jumpGlobalMark(key: string): Promise { 21 | await browser.runtime.sendMessage({ 22 | type: messages.MARK_JUMP_GLOBAL, 23 | key, 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/content/client/OperationClient.ts: -------------------------------------------------------------------------------- 1 | import * as operations from "../../shared/operations"; 2 | import * as messages from "../../shared/messages"; 3 | 4 | export default interface OperationClient { 5 | execBackgroundOp(repeat: number, op: operations.Operation): Promise; 6 | 7 | internalOpenUrl( 8 | url: string, 9 | newTab?: boolean, 10 | background?: boolean 11 | ): Promise; 12 | } 13 | 14 | export class OperationClientImpl implements OperationClient { 15 | execBackgroundOp(repeat: number, op: operations.Operation): Promise { 16 | return browser.runtime.sendMessage({ 17 | type: messages.BACKGROUND_OPERATION, 18 | repeat, 19 | operation: op, 20 | }); 21 | } 22 | 23 | internalOpenUrl( 24 | url: string, 25 | newTab?: boolean, 26 | background?: boolean 27 | ): Promise { 28 | return browser.runtime.sendMessage({ 29 | type: messages.BACKGROUND_OPERATION, 30 | repeat: 1, 31 | operation: { 32 | type: operations.INTERNAL_OPEN_URL, 33 | url, 34 | newTab, 35 | background, 36 | }, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/content/client/SettingClient.ts: -------------------------------------------------------------------------------- 1 | import Settings from "../../shared/settings/Settings"; 2 | import * as messages from "../../shared/messages"; 3 | 4 | export default interface SettingClient { 5 | load(): Promise; 6 | } 7 | 8 | export class SettingClientImpl { 9 | async load(): Promise { 10 | const settings = await browser.runtime.sendMessage({ 11 | type: messages.SETTINGS_QUERY, 12 | }); 13 | return Settings.fromJSON(settings); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/content/client/TabsClient.ts: -------------------------------------------------------------------------------- 1 | import * as messages from "../../shared/messages"; 2 | 3 | export default interface TabsClient { 4 | openUrl(url: string, newTab: boolean, background?: boolean): Promise; 5 | } 6 | 7 | export class TabsClientImpl implements TabsClient { 8 | async openUrl( 9 | url: string, 10 | newTab: boolean, 11 | background?: boolean 12 | ): Promise { 13 | await browser.runtime.sendMessage({ 14 | type: messages.OPEN_URL, 15 | url, 16 | newTab, 17 | background, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/content/controllers/AddonEnabledController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import * as messages from "../../shared/messages"; 3 | import AddonEnabledUseCase from "../usecases/AddonEnabledUseCase"; 4 | 5 | @injectable() 6 | export default class AddonEnabledController { 7 | constructor(private addonEnabledUseCase: AddonEnabledUseCase) {} 8 | 9 | getAddonEnabled( 10 | _message: messages.AddonEnabledQueryMessage 11 | ): Promise { 12 | const enabled = this.addonEnabledUseCase.getEnabled(); 13 | return Promise.resolve(enabled); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/content/controllers/ConsoleFrameController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import ConsoleFrameUseCase from "../usecases/ConsoleFrameUseCase"; 3 | import * as messages from "../../shared/messages"; 4 | 5 | @injectable() 6 | export default class ConsoleFrameController { 7 | constructor(private consoleFrameUseCase: ConsoleFrameUseCase) {} 8 | 9 | unfocus(_message: messages.Message) { 10 | this.consoleFrameUseCase.unfocus(); 11 | } 12 | 13 | resize(message: messages.ConsoleResizeMessage) { 14 | this.consoleFrameUseCase.resize(message.width, message.height); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/content/controllers/FindController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import FindUseCase from "../usecases/FindUseCase"; 3 | 4 | @injectable() 5 | export default class FindController { 6 | constructor(private findUseCase: FindUseCase) {} 7 | 8 | findNext(keyword: string): boolean { 9 | return this.findUseCase.findNext(keyword); 10 | } 11 | 12 | findPrev(keyword: string): boolean { 13 | return this.findUseCase.findPrev(keyword); 14 | } 15 | 16 | clearSelection() { 17 | return this.findUseCase.clearSelection(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/content/controllers/FollowKeyController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import FollowSlaveUseCase from "../usecases/FollowSlaveUseCase"; 3 | import Key from "../../shared/settings/Key"; 4 | 5 | @injectable() 6 | export default class FollowKeyController { 7 | constructor(private followSlaveUseCase: FollowSlaveUseCase) {} 8 | 9 | press(key: Key): boolean { 10 | if (!this.followSlaveUseCase.isFollowMode()) { 11 | return false; 12 | } 13 | 14 | this.followSlaveUseCase.sendKey(key); 15 | return true; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/content/controllers/FollowMasterController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import FollowMasterUseCase from "../usecases/FollowMasterUseCase"; 3 | import * as messages from "../../shared/messages"; 4 | 5 | @injectable() 6 | export default class FollowMasterController { 7 | constructor(private followMasterUseCase: FollowMasterUseCase) {} 8 | 9 | followStart(m: messages.FollowStartMessage): void { 10 | this.followMasterUseCase.startFollow(m.newTab, m.background); 11 | } 12 | 13 | responseCountTargets( 14 | m: messages.FollowResponseCountTargetsMessage, 15 | sender: Window 16 | ): void { 17 | this.followMasterUseCase.createSlaveHints(m.count, sender); 18 | } 19 | 20 | keyPress(message: messages.FollowKeyPressMessage): void { 21 | if (message.key === "[" && message.ctrlKey) { 22 | this.followMasterUseCase.cancelFollow(); 23 | } else { 24 | this.followMasterUseCase.enqueue(message.key); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/content/controllers/FollowSlaveController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import * as messages from "../../shared/messages"; 3 | import FollowSlaveUseCase from "../usecases/FollowSlaveUseCase"; 4 | 5 | @injectable() 6 | export default class FollowSlaveController { 7 | constructor(private usecase: FollowSlaveUseCase) {} 8 | 9 | countTargets(m: messages.FollowRequestCountTargetsMessage): void { 10 | this.usecase.countTargets(m.viewSize, m.framePosition); 11 | } 12 | 13 | createHints(m: messages.FollowCreateHintsMessage): void { 14 | this.usecase.createHints(m.viewSize, m.framePosition, m.tags); 15 | } 16 | 17 | showHints(m: messages.FollowShowHintsMessage): void { 18 | this.usecase.showHints(m.prefix); 19 | } 20 | 21 | activate(m: messages.FollowActivateMessage): void { 22 | this.usecase.activate(m.tag, m.newTab, m.background); 23 | } 24 | 25 | clear(_m: messages.FollowRemoveHintsMessage) { 26 | this.usecase.clear(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/content/controllers/KeymapController.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "tsyringe"; 2 | import KeymapUseCase from "../usecases/KeymapUseCase"; 3 | import Key from "../../shared/settings/Key"; 4 | import OperatorFactory from "../operators/OperatorFactory"; 5 | 6 | @injectable() 7 | export default class KeymapController { 8 | constructor( 9 | private keymapUseCase: KeymapUseCase, 10 | 11 | @inject("OperatorFactory") 12 | private readonly operatorFactory: OperatorFactory 13 | ) {} 14 | 15 | // eslint-disable-next-line complexity, max-lines-per-function 16 | press(key: Key): boolean { 17 | const nextOp = this.keymapUseCase.nextOps(key); 18 | if (nextOp === null) { 19 | return false; 20 | } 21 | 22 | // Do not await asynchronous methods to return a boolean immidiately. The 23 | // caller requires the synchronous response from the callback to identify 24 | // to continue of abandon the event propagation. 25 | this.operatorFactory 26 | .create(nextOp.op, nextOp.repeat) 27 | .run() 28 | .catch(console.error); 29 | 30 | return true; 31 | } 32 | 33 | onBlurWindow() { 34 | this.keymapUseCase.cancel(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/content/controllers/MarkController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import * as messages from "../../shared/messages"; 3 | import MarkUseCase from "../usecases/MarkUseCase"; 4 | 5 | @injectable() 6 | export default class MarkController { 7 | constructor(private markUseCase: MarkUseCase) {} 8 | 9 | scrollTo(message: messages.TabScrollToMessage) { 10 | this.markUseCase.scroll(message.x, message.y); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/content/controllers/MarkKeyController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import MarkUseCase from "../usecases/MarkUseCase"; 3 | import MarkKeyyUseCase from "../usecases/MarkKeyUseCase"; 4 | import Key from "../../shared/settings/Key"; 5 | 6 | @injectable() 7 | export default class MarkKeyController { 8 | constructor( 9 | private markUseCase: MarkUseCase, 10 | private markKeyUseCase: MarkKeyyUseCase 11 | ) {} 12 | 13 | press(key: Key): boolean { 14 | if (this.markKeyUseCase.isSetMode()) { 15 | this.markUseCase.set(key.key); 16 | this.markKeyUseCase.disableSetMode(); 17 | return true; 18 | } 19 | if (this.markKeyUseCase.isJumpMode()) { 20 | this.markUseCase.jump(key.key); 21 | this.markKeyUseCase.disableJumpMode(); 22 | return true; 23 | } 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/content/controllers/NavigateController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import { Message } from "../../shared/messages"; 3 | import NavigateUseCase from "../usecases/NavigateUseCase"; 4 | 5 | @injectable() 6 | export default class NavigateController { 7 | constructor(private navigateUseCase: NavigateUseCase) {} 8 | 9 | openHistoryNext(_m: Message): Promise { 10 | this.navigateUseCase.openHistoryNext(); 11 | return Promise.resolve(); 12 | } 13 | 14 | openHistoryPrev(_m: Message): Promise { 15 | this.navigateUseCase.openHistoryPrev(); 16 | return Promise.resolve(); 17 | } 18 | 19 | openLinkNext(_m: Message): Promise { 20 | this.navigateUseCase.openLinkNext(); 21 | return Promise.resolve(); 22 | } 23 | 24 | openLinkPrev(_m: Message): Promise { 25 | this.navigateUseCase.openLinkPrev(); 26 | return Promise.resolve(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/content/controllers/SettingController.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from "tsyringe"; 2 | import AddonEnabledUseCase from "../usecases/AddonEnabledUseCase"; 3 | import SettingUseCase from "../usecases/SettingUseCase"; 4 | import * as messages from "../../shared/messages"; 5 | 6 | @injectable() 7 | export default class SettingController { 8 | constructor( 9 | private addonEnabledUseCase: AddonEnabledUseCase, 10 | private settingUseCase: SettingUseCase 11 | ) {} 12 | 13 | async initSettings(): Promise { 14 | try { 15 | const current = await this.settingUseCase.reload(); 16 | const url = new URL(window.location.href); 17 | const disabled = current.blacklist.includesEntireBlacklist(url); 18 | if (disabled) { 19 | await this.addonEnabledUseCase.disable(); 20 | } else { 21 | await this.addonEnabledUseCase.enable(); 22 | } 23 | } catch (e) { 24 | // Sometime sendMessage fails when background script is not ready. 25 | console.warn(e); 26 | setTimeout(() => this.initSettings(), 500); 27 | } 28 | } 29 | 30 | async reloadSettings(_message: messages.Message): Promise { 31 | await this.settingUseCase.reload(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/content/domains/Mark.ts: -------------------------------------------------------------------------------- 1 | export default interface Mark { 2 | x: number; 3 | y: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/content/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | 3 | import Application from "./Application"; 4 | import Bootstrap from "./Bootstrap"; 5 | import consoleFrameStyle from "./site-style"; 6 | import { container } from "tsyringe"; 7 | import "./di"; 8 | 9 | const initDom = () => { 10 | (async () => { 11 | try { 12 | const app = container.resolve(Application); 13 | await app.init(); 14 | } catch (e) { 15 | console.error(e); 16 | } 17 | })(); 18 | 19 | const style = window.document.createElement("style"); 20 | style.textContent = consoleFrameStyle; 21 | window.document.head.appendChild(style); 22 | }; 23 | 24 | const bootstrap = new Bootstrap(); 25 | if (bootstrap.isReady()) { 26 | initDom(); 27 | } else { 28 | bootstrap.waitForReady(() => initDom()); 29 | } 30 | -------------------------------------------------------------------------------- /src/content/operators/Operator.ts: -------------------------------------------------------------------------------- 1 | interface Operator { 2 | run(): Promise; 3 | } 4 | 5 | export default Operator; 6 | -------------------------------------------------------------------------------- /src/content/operators/OperatorFactory.ts: -------------------------------------------------------------------------------- 1 | import * as operations from "../../shared/operations"; 2 | import Operator from "./Operator"; 3 | 4 | export default interface OperatorFactory { 5 | create(op: operations.Operation, repeat: number): Operator; 6 | } 7 | -------------------------------------------------------------------------------- /src/content/operators/OperatorFactoryChain.ts: -------------------------------------------------------------------------------- 1 | import * as operations from "../../shared/operations"; 2 | import Operator from "./Operator"; 3 | 4 | export default interface OperatorFactoryChain { 5 | create(op: operations.Operation, repeat: number): Operator | null; 6 | } 7 | -------------------------------------------------------------------------------- /src/content/operators/impls/AbstractScrollOperator.ts: -------------------------------------------------------------------------------- 1 | import SettingRepository from "../../repositories/SettingRepository"; 2 | 3 | export default class AbstractScrollOperator { 4 | constructor(private readonly settingRepository: SettingRepository) {} 5 | 6 | protected getSmoothScroll(): boolean { 7 | const settings = this.settingRepository.get(); 8 | return settings.properties.smoothscroll; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/content/operators/impls/BackgroundOperationOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import OperationClient from "../../client/OperationClient"; 3 | import * as operations from "../../../shared/operations"; 4 | 5 | export default class BackgroundOperationOperator implements Operator { 6 | constructor( 7 | private readonly operationClient: OperationClient, 8 | private readonly repeat: number, 9 | private readonly op: operations.Operation 10 | ) {} 11 | 12 | async run(): Promise { 13 | await this.operationClient.execBackgroundOp(this.repeat, this.op); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/content/operators/impls/DisableAddonOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import AddonIndicatorClient from "../../client/AddonIndicatorClient"; 3 | import AddonEnabledRepository from "../../repositories/AddonEnabledRepository"; 4 | import ConsoleFramePresenter from "../../presenters/ConsoleFramePresenter"; 5 | 6 | export default class DisableAddonOperator implements Operator { 7 | constructor( 8 | private readonly indicator: AddonIndicatorClient, 9 | private readonly repository: AddonEnabledRepository, 10 | private readonly consoleFramePresenter: ConsoleFramePresenter 11 | ) {} 12 | 13 | async run(): Promise { 14 | this.repository.set(false); 15 | this.consoleFramePresenter.detach(); 16 | await this.indicator.setEnabled(false); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/content/operators/impls/EnableAddonOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import AddonIndicatorClient from "../../client/AddonIndicatorClient"; 3 | import AddonEnabledRepository from "../../repositories/AddonEnabledRepository"; 4 | import ConsoleFramePresenter from "../../presenters/ConsoleFramePresenter"; 5 | 6 | export default class EnableAddonOperator implements Operator { 7 | constructor( 8 | private readonly indicator: AddonIndicatorClient, 9 | private readonly repository: AddonEnabledRepository, 10 | private readonly consoleFramePresenter: ConsoleFramePresenter 11 | ) {} 12 | 13 | async run(): Promise { 14 | this.repository.set(true); 15 | this.consoleFramePresenter.attach(); 16 | await this.indicator.setEnabled(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/content/operators/impls/EnableJumpMarkOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import MarkKeyRepository from "../../repositories/MarkKeyRepository"; 3 | 4 | export default class EnableJumpMarkOperator implements Operator { 5 | constructor(private readonly repository: MarkKeyRepository) {} 6 | 7 | async run(): Promise { 8 | this.repository.enableJumpMode(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/content/operators/impls/EnableSetMarkOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import MarkKeyRepository from "../../repositories/MarkKeyRepository"; 3 | 4 | export default class EnableSetMarkOperator implements Operator { 5 | constructor(private readonly repository: MarkKeyRepository) {} 6 | 7 | async run(): Promise { 8 | this.repository.enableSetMode(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/content/operators/impls/FocusOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import FocusPresenter from "../../presenters/FocusPresenter"; 3 | 4 | export default class FocusOperator implements Operator { 5 | constructor(private readonly presenter: FocusPresenter) {} 6 | 7 | async run(): Promise { 8 | this.presenter.focusFirstElement(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/content/operators/impls/FocusOperatorFactoryChain.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import OperatorFactoryChain from "../OperatorFactoryChain"; 3 | import Operator from "../Operator"; 4 | import FocusOperator from "./FocusOperator"; 5 | import FocusPresenter from "../../presenters/FocusPresenter"; 6 | import * as operations from "../../../shared/operations"; 7 | 8 | @injectable() 9 | export default class FocusOperatorFactoryChain implements OperatorFactoryChain { 10 | constructor( 11 | @inject("FocusPresenter") 12 | private readonly focusPresenter: FocusPresenter 13 | ) {} 14 | 15 | create(op: operations.Operation, _repeat: number): Operator | null { 16 | switch (op.type) { 17 | case operations.FOCUS_INPUT: 18 | return new FocusOperator(this.focusPresenter); 19 | } 20 | return null; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/content/operators/impls/FollowOperatorFactoryChain.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import StartFollowOperator from "./StartFollowOperator"; 3 | import Operator from "../Operator"; 4 | import OperatorFactoryChain from "../OperatorFactoryChain"; 5 | import FollowMasterClient from "../../client/FollowMasterClient"; 6 | import * as operations from "../../../shared/operations"; 7 | 8 | @injectable() 9 | export default class FollowOperatorFactoryChain 10 | implements OperatorFactoryChain 11 | { 12 | constructor( 13 | @inject("FollowMasterClient") 14 | private followMasterClient: FollowMasterClient 15 | ) {} 16 | 17 | create(op: operations.Operation, _repeat: number): Operator | null { 18 | switch (op.type) { 19 | case operations.FOLLOW_START: 20 | return new StartFollowOperator( 21 | this.followMasterClient, 22 | op.newTab, 23 | op.background 24 | ); 25 | } 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/content/operators/impls/HorizontalScrollOperator.ts: -------------------------------------------------------------------------------- 1 | import AbstractScrollOperator from "./AbstractScrollOperator"; 2 | import Operator from "../Operator"; 3 | import ScrollPresenter from "../../presenters/ScrollPresenter"; 4 | import SettingRepository from "../../repositories/SettingRepository"; 5 | 6 | export default class HorizontalScrollOperator 7 | extends AbstractScrollOperator 8 | implements Operator 9 | { 10 | constructor( 11 | private readonly presenter: ScrollPresenter, 12 | settingRepository: SettingRepository, 13 | private readonly count: number 14 | ) { 15 | super(settingRepository); 16 | } 17 | 18 | async run(): Promise { 19 | const smooth = this.getSmoothScroll(); 20 | this.presenter.scrollHorizonally(this.count, smooth); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/content/operators/impls/MarkOperatorFactoryChain.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import EnableSetMarkOperator from "./EnableSetMarkOperator"; 3 | import EnableJumpMarkOperator from "./EnableJumpMarkOperator"; 4 | import Operator from "../Operator"; 5 | import OperatorFactoryChain from "../OperatorFactoryChain"; 6 | import MarkKeyRepository from "../../repositories/MarkKeyRepository"; 7 | import * as operations from "../../../shared/operations"; 8 | 9 | @injectable() 10 | export default class MarkOperatorFactoryChain implements OperatorFactoryChain { 11 | constructor( 12 | @inject("MarkKeyRepository") 13 | private readonly markKeyRepository: MarkKeyRepository 14 | ) {} 15 | 16 | create(op: operations.Operation, _repeat: number): Operator | null { 17 | switch (op.type) { 18 | case operations.MARK_SET_PREFIX: 19 | return new EnableSetMarkOperator(this.markKeyRepository); 20 | case operations.MARK_JUMP_PREFIX: 21 | return new EnableJumpMarkOperator(this.markKeyRepository); 22 | } 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/content/operators/impls/PageScrollOperator.ts: -------------------------------------------------------------------------------- 1 | import AbstractScrollOperator from "./AbstractScrollOperator"; 2 | import Operator from "../Operator"; 3 | import ScrollPresenter from "../../presenters/ScrollPresenter"; 4 | import SettingRepository from "../../repositories/SettingRepository"; 5 | 6 | export default class PageScrollOperator 7 | extends AbstractScrollOperator 8 | implements Operator 9 | { 10 | constructor( 11 | private readonly presenter: ScrollPresenter, 12 | settingRepository: SettingRepository, 13 | private readonly count: number 14 | ) { 15 | super(settingRepository); 16 | } 17 | 18 | async run(): Promise { 19 | const smooth = this.getSmoothScroll(); 20 | this.presenter.scrollPages(this.count, smooth); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/content/operators/impls/PasteOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import ClipboardRepository from "../../repositories/ClipboardRepository"; 3 | import SettingRepository from "../../repositories/SettingRepository"; 4 | import OperationClient from "../../client/OperationClient"; 5 | import * as urls from "../../../shared/urls"; 6 | 7 | export default class PasteOperator implements Operator { 8 | constructor( 9 | private readonly repository: ClipboardRepository, 10 | private readonly settingRepository: SettingRepository, 11 | private readonly operationClient: OperationClient, 12 | private readonly newTab: boolean 13 | ) {} 14 | 15 | async run(): Promise { 16 | const search = this.settingRepository.get().search; 17 | const text = this.repository.read(); 18 | const url = urls.searchUrl(text, search); 19 | 20 | // NOTE: Repeat pasting from clipboard instead of opening a certain url. 21 | // 'Repeat last' command is implemented in the background script and cannot 22 | // access to clipboard until Firefox 63. 23 | await this.operationClient.internalOpenUrl(url, this.newTab); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/content/operators/impls/ScrollToBottomOperator.ts: -------------------------------------------------------------------------------- 1 | import AbstractScrollOperator from "./AbstractScrollOperator"; 2 | import Operator from "../Operator"; 3 | import ScrollPresenter from "../../presenters/ScrollPresenter"; 4 | import SettingRepository from "../../repositories/SettingRepository"; 5 | 6 | export default class ScrollToBottomOperator 7 | extends AbstractScrollOperator 8 | implements Operator 9 | { 10 | constructor( 11 | private readonly presenter: ScrollPresenter, 12 | settingRepository: SettingRepository 13 | ) { 14 | super(settingRepository); 15 | } 16 | 17 | async run(): Promise { 18 | const smooth = this.getSmoothScroll(); 19 | this.presenter.scrollToBottom(smooth); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/content/operators/impls/ScrollToEndOperator.ts: -------------------------------------------------------------------------------- 1 | import AbstractScrollOperator from "./AbstractScrollOperator"; 2 | import Operator from "../Operator"; 3 | import ScrollPresenter from "../../presenters/ScrollPresenter"; 4 | import SettingRepository from "../../repositories/SettingRepository"; 5 | 6 | export default class ScrollToEndOperator 7 | extends AbstractScrollOperator 8 | implements Operator 9 | { 10 | constructor( 11 | private readonly presenter: ScrollPresenter, 12 | settingRepository: SettingRepository 13 | ) { 14 | super(settingRepository); 15 | } 16 | 17 | async run(): Promise { 18 | const smooth = this.getSmoothScroll(); 19 | this.presenter.scrollToEnd(smooth); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/content/operators/impls/ScrollToHomeOperator.ts: -------------------------------------------------------------------------------- 1 | import AbstractScrollOperator from "./AbstractScrollOperator"; 2 | import Operator from "../Operator"; 3 | import ScrollPresenter from "../../presenters/ScrollPresenter"; 4 | import SettingRepository from "../../repositories/SettingRepository"; 5 | 6 | export default class ScrollToHomeOperator 7 | extends AbstractScrollOperator 8 | implements Operator 9 | { 10 | constructor( 11 | private readonly presenter: ScrollPresenter, 12 | settingRepository: SettingRepository 13 | ) { 14 | super(settingRepository); 15 | } 16 | 17 | async run(): Promise { 18 | const smooth = this.getSmoothScroll(); 19 | this.presenter.scrollToHome(smooth); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/content/operators/impls/ScrollToTopOperator.ts: -------------------------------------------------------------------------------- 1 | import AbstractScrollOperator from "./AbstractScrollOperator"; 2 | import Operator from "../Operator"; 3 | import ScrollPresenter from "../../presenters/ScrollPresenter"; 4 | import SettingRepository from "../../repositories/SettingRepository"; 5 | 6 | export default class ScrollToTopOperator 7 | extends AbstractScrollOperator 8 | implements Operator 9 | { 10 | constructor( 11 | private readonly presenter: ScrollPresenter, 12 | settingRepository: SettingRepository 13 | ) { 14 | super(settingRepository); 15 | } 16 | 17 | async run(): Promise { 18 | const smooth = this.getSmoothScroll(); 19 | this.presenter.scrollToTop(smooth); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/content/operators/impls/StartFollowOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import FollowMasterClient from "../../client/FollowMasterClient"; 3 | 4 | export default class StartFollowOperator implements Operator { 5 | constructor( 6 | private readonly followMasterClient: FollowMasterClient, 7 | private readonly newTab: boolean, 8 | private readonly background: boolean 9 | ) {} 10 | 11 | async run(): Promise { 12 | this.followMasterClient.startFollow(this.newTab, this.background); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/content/operators/impls/ToggleAddonOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import AddonIndicatorClient from "../../client/AddonIndicatorClient"; 3 | import AddonEnabledRepository from "../../repositories/AddonEnabledRepository"; 4 | import ConsoleFramePresenter from "../../presenters/ConsoleFramePresenter"; 5 | 6 | export default class ToggleAddonOperator implements Operator { 7 | constructor( 8 | private readonly indicator: AddonIndicatorClient, 9 | private readonly repository: AddonEnabledRepository, 10 | private readonly consoleFramePresenter: ConsoleFramePresenter 11 | ) {} 12 | 13 | async run(): Promise { 14 | const enabled = !this.repository.get(); 15 | this.repository.set(enabled); 16 | if (enabled) { 17 | this.consoleFramePresenter.attach(); 18 | } else { 19 | this.consoleFramePresenter.detach(); 20 | } 21 | await this.indicator.setEnabled(enabled); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/content/operators/impls/URLRepository.ts: -------------------------------------------------------------------------------- 1 | export default interface URLRepository { 2 | getCurrentURL(): string; 3 | } 4 | 5 | export class URLRepositoryImpl implements URLRepository { 6 | getCurrentURL(): string { 7 | return window.location.href; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/content/operators/impls/VerticalScrollOperator.ts: -------------------------------------------------------------------------------- 1 | import AbstractScrollOperator from "./AbstractScrollOperator"; 2 | import Operator from "../Operator"; 3 | import ScrollPresenter from "../../presenters/ScrollPresenter"; 4 | import SettingRepository from "../../repositories/SettingRepository"; 5 | 6 | export default class VerticalScrollOperator 7 | extends AbstractScrollOperator 8 | implements Operator 9 | { 10 | constructor( 11 | private readonly presenter: ScrollPresenter, 12 | settingRepository: SettingRepository, 13 | private readonly count: number 14 | ) { 15 | super(settingRepository); 16 | } 17 | 18 | async run(): Promise { 19 | const smooth = this.getSmoothScroll(); 20 | this.presenter.scrollVertically(this.count, smooth); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/content/operators/impls/YankURLOperator.ts: -------------------------------------------------------------------------------- 1 | import Operator from "../Operator"; 2 | import ClipboardRepository from "../../repositories/ClipboardRepository"; 3 | import ConsoleClient from "../../client/ConsoleClient"; 4 | import URLRepository from "./URLRepository"; 5 | 6 | export default class YankURLOperator implements Operator { 7 | constructor( 8 | private readonly repository: ClipboardRepository, 9 | private readonly consoleClient: ConsoleClient, 10 | private readonly urlRepository: URLRepository 11 | ) {} 12 | 13 | async run(): Promise { 14 | const url = this.urlRepository.getCurrentURL(); 15 | this.repository.write(url); 16 | await this.consoleClient.info("Yanked " + url); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/content/presenters/FindPresenter.ts: -------------------------------------------------------------------------------- 1 | export default interface FindPresenter { 2 | find(keyword: string, backwards: boolean): boolean; 3 | 4 | clearSelection(): void; 5 | } 6 | 7 | export class FindPresenterImpl implements FindPresenter { 8 | find(keyword: string, backwards: boolean): boolean { 9 | const caseSensitive = false; 10 | const wrapScan = false; 11 | 12 | // NOTE: aWholeWord dows not implemented, and aSearchInFrames does not work 13 | // because of same origin policy 14 | return window.find(keyword, caseSensitive, backwards, wrapScan); 15 | } 16 | 17 | clearSelection(): void { 18 | const sel = window.getSelection(); 19 | if (sel) { 20 | sel.removeAllRanges(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/content/presenters/FocusPresenter.ts: -------------------------------------------------------------------------------- 1 | import * as doms from "../../shared/utils/dom"; 2 | 3 | export default interface FocusPresenter { 4 | focusFirstElement(): boolean; 5 | } 6 | 7 | export class FocusPresenterImpl implements FocusPresenter { 8 | focusFirstElement(): boolean { 9 | const inputTypes = ["email", "number", "search", "tel", "text", "url"]; 10 | const inputSelector = inputTypes 11 | .map((type) => `input[type=${type}]`) 12 | .join(","); 13 | const targets = window.document.querySelectorAll( 14 | inputSelector + ",input:not([type]),textarea" 15 | ); 16 | const target = Array.from(targets).find(doms.isVisible); 17 | if (target instanceof HTMLInputElement) { 18 | target.focus(); 19 | return true; 20 | } else if (target instanceof HTMLTextAreaElement) { 21 | target.focus(); 22 | return true; 23 | } 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/content/repositories/AddonEnabledRepository.ts: -------------------------------------------------------------------------------- 1 | let enabled = false; 2 | 3 | export default interface AddonEnabledRepository { 4 | set(on: boolean): void; 5 | 6 | get(): boolean; 7 | } 8 | 9 | export class AddonEnabledRepositoryImpl implements AddonEnabledRepository { 10 | set(on: boolean): void { 11 | enabled = on; 12 | } 13 | 14 | get(): boolean { 15 | return enabled; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/content/repositories/AddressRepository.ts: -------------------------------------------------------------------------------- 1 | export default interface AddressRepository { 2 | getCurrentURL(): URL; 3 | } 4 | 5 | export class AddressRepositoryImpl implements AddressRepository { 6 | getCurrentURL(): URL { 7 | return new URL(window.location.href); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/content/repositories/ClipboardRepository.ts: -------------------------------------------------------------------------------- 1 | export default interface ClipboardRepository { 2 | read(): string; 3 | 4 | write(text: string): void; 5 | } 6 | 7 | export class ClipboardRepositoryImpl { 8 | read(): string { 9 | const textarea = window.document.createElement("textarea"); 10 | window.document.body.append(textarea); 11 | 12 | textarea.style.position = "fixed"; 13 | textarea.style.top = "-100px"; 14 | textarea.contentEditable = "true"; 15 | textarea.focus(); 16 | 17 | const ok = window.document.execCommand("paste"); 18 | const value = textarea.value; 19 | textarea.remove(); 20 | 21 | if (!ok) { 22 | throw new Error("failed to access clipbaord"); 23 | } 24 | 25 | return value; 26 | } 27 | 28 | write(text: string): void { 29 | const input = window.document.createElement("input"); 30 | window.document.body.append(input); 31 | 32 | input.style.position = "fixed"; 33 | input.style.top = "-100px"; 34 | input.value = text; 35 | input.select(); 36 | 37 | const ok = window.document.execCommand("copy"); 38 | input.remove(); 39 | 40 | if (!ok) { 41 | throw new Error("failed to access clipbaord"); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/content/repositories/FollowKeyRepository.ts: -------------------------------------------------------------------------------- 1 | export default interface FollowKeyRepository { 2 | getKeys(): string[]; 3 | 4 | pushKey(key: string): void; 5 | 6 | popKey(): void; 7 | 8 | clearKeys(): void; 9 | } 10 | 11 | const current: { 12 | keys: string[]; 13 | } = { 14 | keys: [], 15 | }; 16 | 17 | export class FollowKeyRepositoryImpl implements FollowKeyRepository { 18 | getKeys(): string[] { 19 | return current.keys; 20 | } 21 | 22 | pushKey(key: string): void { 23 | current.keys.push(key); 24 | } 25 | 26 | popKey(): void { 27 | current.keys.pop(); 28 | } 29 | 30 | clearKeys(): void { 31 | current.keys = []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/content/repositories/FollowSlaveRepository.ts: -------------------------------------------------------------------------------- 1 | export default interface FollowSlaveRepository { 2 | enableFollowMode(): void; 3 | 4 | disableFollowMode(): void; 5 | 6 | isFollowMode(): boolean; 7 | } 8 | 9 | const current: { 10 | enabled: boolean; 11 | } = { 12 | enabled: false, 13 | }; 14 | 15 | export class FollowSlaveRepositoryImpl implements FollowSlaveRepository { 16 | enableFollowMode(): void { 17 | current.enabled = true; 18 | } 19 | 20 | disableFollowMode(): void { 21 | current.enabled = false; 22 | } 23 | 24 | isFollowMode(): boolean { 25 | return current.enabled; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/content/repositories/KeymapRepository.ts: -------------------------------------------------------------------------------- 1 | import Key from "../../shared/settings/Key"; 2 | import KeySequence from "../domains/KeySequence"; 3 | 4 | export default interface KeymapRepository { 5 | enqueueKey(key: Key): KeySequence; 6 | 7 | clear(): void; 8 | } 9 | 10 | let current: KeySequence = new KeySequence([]); 11 | 12 | export class KeymapRepositoryImpl { 13 | enqueueKey(key: Key): KeySequence { 14 | current.push(key); 15 | return current; 16 | } 17 | 18 | clear(): void { 19 | current = new KeySequence([]); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/content/repositories/MarkKeyRepository.ts: -------------------------------------------------------------------------------- 1 | export default interface MarkKeyRepository { 2 | isSetMode(): boolean; 3 | 4 | enableSetMode(): void; 5 | 6 | disabeSetMode(): void; 7 | 8 | isJumpMode(): boolean; 9 | 10 | enableJumpMode(): void; 11 | 12 | disabeJumpMode(): void; 13 | } 14 | 15 | interface Mode { 16 | setMode: boolean; 17 | jumpMode: boolean; 18 | } 19 | 20 | const current: Mode = { 21 | setMode: false, 22 | jumpMode: false, 23 | }; 24 | 25 | export class MarkKeyRepositoryImpl implements MarkKeyRepository { 26 | isSetMode(): boolean { 27 | return current.setMode; 28 | } 29 | 30 | enableSetMode(): void { 31 | current.setMode = true; 32 | } 33 | 34 | disabeSetMode(): void { 35 | current.setMode = false; 36 | } 37 | 38 | isJumpMode(): boolean { 39 | return current.jumpMode; 40 | } 41 | 42 | enableJumpMode(): void { 43 | current.jumpMode = true; 44 | } 45 | 46 | disabeJumpMode(): void { 47 | current.jumpMode = false; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/content/repositories/MarkRepository.ts: -------------------------------------------------------------------------------- 1 | import Mark from "../domains/Mark"; 2 | 3 | export default interface MarkRepository { 4 | set(key: string, mark: Mark): void; 5 | 6 | get(key: string): Mark | null; 7 | } 8 | 9 | const saved: { [key: string]: Mark } = {}; 10 | 11 | export class MarkRepositoryImpl implements MarkRepository { 12 | set(key: string, mark: Mark): void { 13 | saved[key] = mark; 14 | } 15 | 16 | get(key: string): Mark | null { 17 | const v = saved[key]; 18 | if (!v) { 19 | return null; 20 | } 21 | return { ...v }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/content/repositories/SettingRepository.ts: -------------------------------------------------------------------------------- 1 | import Settings, { DefaultSetting } from "../../shared/settings/Settings"; 2 | 3 | let current: Settings = DefaultSetting; 4 | 5 | export default interface SettingRepository { 6 | set(setting: Settings): void; 7 | 8 | get(): Settings; 9 | } 10 | 11 | export class SettingRepositoryImpl implements SettingRepository { 12 | set(setting: Settings): void { 13 | current = setting; 14 | } 15 | 16 | get(): Settings { 17 | return current; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/content/site-style.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | .vimvixen-console-frame { 3 | margin: 0; 4 | padding: 0; 5 | bottom: 0; 6 | left: 0; 7 | width: 100%; 8 | position: fixed; 9 | z-index: 2147483647; 10 | border: none !important; 11 | background-color: unset !important; 12 | pointer-events:none; 13 | } 14 | 15 | .vimvixen-hint { 16 | background-color: yellow; 17 | border: 1px solid gold; 18 | font-weight: bold; 19 | position: absolute; 20 | text-transform: uppercase; 21 | z-index: 2147483647; 22 | font-size: 12px; 23 | color: black; 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /src/content/usecases/ConsoleFrameUseCase.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "tsyringe"; 2 | import ConsoleFramePresenter from "../presenters/ConsoleFramePresenter"; 3 | 4 | @injectable() 5 | export default class ConsoleFrameUseCase { 6 | constructor( 7 | @inject("ConsoleFramePresenter") 8 | private consoleFramePresenter: ConsoleFramePresenter 9 | ) {} 10 | 11 | unfocus() { 12 | window.focus(); 13 | this.consoleFramePresenter.blur(); 14 | } 15 | 16 | resize(width: number, height: number) { 17 | this.consoleFramePresenter.resize(width, height); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/content/usecases/FindUseCase.ts: -------------------------------------------------------------------------------- 1 | import { inject, injectable } from "tsyringe"; 2 | import FindPresenter from "../presenters/FindPresenter"; 3 | 4 | @injectable() 5 | export default class FindUseCase { 6 | constructor( 7 | @inject("FindPresenter") 8 | private readonly findPresenter: FindPresenter 9 | ) {} 10 | 11 | findNext(keyword: string): boolean { 12 | return this.findPresenter.find(keyword, false); 13 | } 14 | 15 | findPrev(keyword: string): boolean { 16 | return this.findPresenter.find(keyword, true); 17 | } 18 | 19 | clearSelection() { 20 | this.findPresenter.clearSelection(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/content/usecases/MarkKeyUseCase.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "tsyringe"; 2 | import MarkKeyRepository from "../repositories/MarkKeyRepository"; 3 | 4 | @injectable() 5 | export default class MarkKeyUseCase { 6 | constructor( 7 | @inject("MarkKeyRepository") private repository: MarkKeyRepository 8 | ) {} 9 | 10 | isSetMode(): boolean { 11 | return this.repository.isSetMode(); 12 | } 13 | 14 | isJumpMode(): boolean { 15 | return this.repository.isJumpMode(); 16 | } 17 | 18 | disableSetMode(): void { 19 | this.repository.disabeSetMode(); 20 | } 21 | 22 | disableJumpMode(): void { 23 | this.repository.disabeJumpMode(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/content/usecases/NavigateUseCase.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "tsyringe"; 2 | import NavigationPresenter from "../presenters/NavigationPresenter"; 3 | 4 | @injectable() 5 | export default class NavigateUseCase { 6 | constructor( 7 | @inject("NavigationPresenter") 8 | private navigationPresenter: NavigationPresenter 9 | ) {} 10 | 11 | openHistoryPrev(): void { 12 | this.navigationPresenter.openHistoryPrev(); 13 | } 14 | 15 | openHistoryNext(): void { 16 | this.navigationPresenter.openHistoryNext(); 17 | } 18 | 19 | openLinkPrev(): void { 20 | this.navigationPresenter.openLinkPrev(); 21 | } 22 | 23 | openLinkNext(): void { 24 | this.navigationPresenter.openLinkNext(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/content/usecases/SettingUseCase.ts: -------------------------------------------------------------------------------- 1 | import { injectable, inject } from "tsyringe"; 2 | import SettingRepository from "../repositories/SettingRepository"; 3 | import SettingClient from "../client/SettingClient"; 4 | import Settings from "../../shared/settings/Settings"; 5 | 6 | @injectable() 7 | export default class SettingUseCase { 8 | constructor( 9 | @inject("SettingRepository") private repository: SettingRepository, 10 | @inject("SettingClient") private client: SettingClient 11 | ) {} 12 | 13 | async reload(): Promise { 14 | const settings = await this.client.load(); 15 | this.repository.set(settings); 16 | return settings; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/settings/actions/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JSONTextSettings, 3 | FormSettings, 4 | SettingSource, 5 | } from "../../shared/SettingData"; 6 | 7 | // Settings 8 | export const SETTING_SET_SETTINGS = "setting.set.settings"; 9 | export const SETTING_SHOW_ERROR = "setting.show.error"; 10 | export const SETTING_SWITCH_TO_FORM = "setting.switch.to.form"; 11 | export const SETTING_SWITCH_TO_JSON = "setting.switch.to.json"; 12 | 13 | interface SettingSetSettingsAcion { 14 | type: typeof SETTING_SET_SETTINGS; 15 | source: SettingSource; 16 | json?: JSONTextSettings; 17 | form?: FormSettings; 18 | } 19 | 20 | interface SettingShowErrorAction { 21 | type: typeof SETTING_SHOW_ERROR; 22 | error: string; 23 | json: JSONTextSettings; 24 | } 25 | 26 | interface SettingSwitchToFormAction { 27 | type: typeof SETTING_SWITCH_TO_FORM; 28 | form: FormSettings; 29 | } 30 | 31 | interface SettingSwitchToJsonAction { 32 | type: typeof SETTING_SWITCH_TO_JSON; 33 | json: JSONTextSettings; 34 | } 35 | 36 | export type SettingAction = 37 | | SettingSetSettingsAcion 38 | | SettingShowErrorAction 39 | | SettingSwitchToFormAction 40 | | SettingSwitchToJsonAction; 41 | -------------------------------------------------------------------------------- /src/settings/components/ui/AddButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Button = styled.input` 5 | border: none; 6 | padding: 4; 7 | display: inline; 8 | background: none; 9 | font-weight: bold; 10 | color: green; 11 | cursor: pointer; 12 | 13 | &:hover { 14 | color: darkgreen; 15 | } 16 | `; 17 | 18 | type Props = React.InputHTMLAttributes; 19 | 20 | const AddButton: React.FC = (props) => ( 21 |