Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
e4830b41be
66
.ci/release
Executable file
66
.ci/release
Executable file
|
@ -0,0 +1,66 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
'''
|
||||||
|
Run [[file:scripts/release][scripts/release]] to deploy Python package onto [[https://pypi.org][PyPi]] and [[https://test.pypi.org][test PyPi]].
|
||||||
|
|
||||||
|
The script expects =TWINE_PASSWORD= environment variable to contain the [[https://pypi.org/help/#apitoken][PyPi token]] (not the password!).
|
||||||
|
|
||||||
|
The script can be run manually.
|
||||||
|
It's also running as =pypi= job in [[file:.github/workflows/main.yml][Github Actions config]]. Packages are deployed on:
|
||||||
|
- every master commit, onto test pypi
|
||||||
|
- every new tag, onto production pypi
|
||||||
|
|
||||||
|
You'll need to set =TWINE_PASSWORD= and =TWINE_PASSWORD_TEST= in [[https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets#creating-encrypted-secrets][secrets]]
|
||||||
|
for Github Actions deployment to work.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import check_call
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
is_ci = os.environ.get('CI') is not None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
import argparse
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument('--test', action='store_true', help='use test pypi')
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
extra = []
|
||||||
|
if args.test:
|
||||||
|
extra.extend(['--repository-url', 'https://test.pypi.org/legacy/'])
|
||||||
|
|
||||||
|
root = Path(__file__).absolute().parent.parent
|
||||||
|
os.chdir(root) # just in case
|
||||||
|
|
||||||
|
if is_ci:
|
||||||
|
# see https://github.com/actions/checkout/issues/217
|
||||||
|
check_call('git fetch --prune --unshallow'.split())
|
||||||
|
|
||||||
|
dist = root / 'dist'
|
||||||
|
if dist.exists():
|
||||||
|
shutil.rmtree(dist)
|
||||||
|
|
||||||
|
check_call('python3 setup.py sdist bdist_wheel', shell=True)
|
||||||
|
|
||||||
|
TP = 'TWINE_PASSWORD'
|
||||||
|
password = os.environ.get(TP)
|
||||||
|
if password is None:
|
||||||
|
print(f"WARNING: no {TP} passed", file=sys.stderr)
|
||||||
|
import pip_secrets
|
||||||
|
password = pip_secrets.token_test if args.test else pip_secrets.token # meh
|
||||||
|
|
||||||
|
check_call([
|
||||||
|
'python3', '-m', 'twine',
|
||||||
|
'upload', *dist.iterdir(),
|
||||||
|
*extra,
|
||||||
|
], env={
|
||||||
|
'TWINE_USERNAME': '__token__',
|
||||||
|
TP: password,
|
||||||
|
**os.environ,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
40
.ci/run
Executable file
40
.ci/run
Executable file
|
@ -0,0 +1,40 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
cd .. # git root
|
||||||
|
|
||||||
|
if ! command -v sudo; then
|
||||||
|
# CI or Docker sometimes doesn't have it, so useful to have a dummy
|
||||||
|
function sudo {
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${CI-}" ]; then
|
||||||
|
# install OS specific stuff here
|
||||||
|
case "$OSTYPE" in
|
||||||
|
darwin*)
|
||||||
|
# macos
|
||||||
|
:
|
||||||
|
;;
|
||||||
|
cygwin* | msys* | win*)
|
||||||
|
# windows
|
||||||
|
:
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# must be linux?
|
||||||
|
:
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
PY_BIN="python3"
|
||||||
|
# some systems might have python pointing to python3
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
PY_BIN="python"
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$PY_BIN" -m pip install --user tox
|
||||||
|
"$PY_BIN" -m tox
|
|
@ -1,100 +0,0 @@
|
||||||
# TODO should I use yarn as in circleci node example??
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
client_build_and_check:
|
|
||||||
docker:
|
|
||||||
- image: circleci/node:latest
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run: sudo npm install -g npm web-ext
|
|
||||||
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- dependency-cache-{{ checksum "package.json" }}
|
|
||||||
# fallback to using the latest cache if no exact match is found
|
|
||||||
- dependency-cache-
|
|
||||||
- run: npm install
|
|
||||||
- save_cache:
|
|
||||||
key: dependency-cache-{{ checksum "package.json" }}
|
|
||||||
paths:
|
|
||||||
- ./node_modules
|
|
||||||
|
|
||||||
- run: ./build --chrome --lint
|
|
||||||
|
|
||||||
# TODO ugh. is there a way for a job to inherit a cloned repo after npm run build has ran?? I'd like to separate build and check steps
|
|
||||||
# TODO ok, apparently it's a custom executor? https://circleci.com/docs/2.0/workflows/#using-workspaces-to-share-data-among-jobs
|
|
||||||
- run:
|
|
||||||
name: js flow
|
|
||||||
command: npm run flow
|
|
||||||
when: always
|
|
||||||
- run:
|
|
||||||
name: eslint
|
|
||||||
command: npm run eslint
|
|
||||||
when: always
|
|
||||||
|
|
||||||
- run:
|
|
||||||
# TODO need to do some kind of build matrix over [firefox, chrome] x [anyhost, no_anyhost]
|
|
||||||
# it should be somehow achievable via workspaces, but I've spent 10 minutes and couldn't figure it out...
|
|
||||||
name: pack
|
|
||||||
command: |
|
|
||||||
|
|
||||||
for browser in 'chrome'; do
|
|
||||||
zip -r -j "/tmp/grasp.zip" dist/*
|
|
||||||
done
|
|
||||||
|
|
||||||
# TODO ugh, also can't use env variables in artifacts...
|
|
||||||
- store_artifacts:
|
|
||||||
path: "/tmp/grasp.zip"
|
|
||||||
destination: "grasp.zip"
|
|
||||||
|
|
||||||
|
|
||||||
server_build_and_check:
|
|
||||||
docker:
|
|
||||||
- image: circleci/python:latest
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
|
|
||||||
# TODO preserve apt cache..
|
|
||||||
- run: sudo apt install python3-pip
|
|
||||||
- run: python3 -m pip install --user mypy pylint pytest
|
|
||||||
|
|
||||||
# test only dependencies
|
|
||||||
- run: sudo apt install httpie
|
|
||||||
- run: python3 -m pip install --user selenium pyautogui
|
|
||||||
#
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: pylint
|
|
||||||
command: ci/pylint
|
|
||||||
when: always
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: mypy
|
|
||||||
command: ci/mypy
|
|
||||||
when: always
|
|
||||||
|
|
||||||
- run:
|
|
||||||
name: pytest
|
|
||||||
command: ci/pytest
|
|
||||||
when: always
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
|
|
||||||
build_and_test:
|
|
||||||
jobs:
|
|
||||||
- client_build_and_check
|
|
||||||
- server_build_and_check
|
|
||||||
|
|
||||||
|
|
||||||
# # ugh. circleci only got python3.6 on this node package??? and it's unclear how to combine multiple circleci docker configurations...
|
|
||||||
|
|
||||||
# # - store_artifacts:
|
|
||||||
# # path: test-results.xml
|
|
||||||
# # prefix: tests
|
|
||||||
# # - store_artifacts:
|
|
||||||
# # path: coverage
|
|
||||||
# # prefix: coverage
|
|
||||||
# # - store_test_results:
|
|
||||||
# # path: test-results.xml
|
|
86
.github/workflows/main.yml
vendored
Normal file
86
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
# see https://github.com/karlicoss/pymplate for up-to-date reference
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: '*'
|
||||||
|
tags: 'v[0-9]+.*' # only trigger on 'release' tags for PyPi
|
||||||
|
# Ideally I would put this in the pypi job... but github syntax doesn't allow for regexes there :shrug:
|
||||||
|
# P.S. fuck made up yaml DSLs.
|
||||||
|
pull_request: # needed to trigger on others' PRs
|
||||||
|
# Note that people who fork it need to go to "Actions" tab on their fork and click "I understand my workflows, go ahead and enable them".
|
||||||
|
workflow_dispatch: # needed to trigger workflows manually
|
||||||
|
# todo cron?
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform: [ubuntu-latest, macos-latest] # , windows-latest]
|
||||||
|
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11']
|
||||||
|
# 3.11 on windows has this bug, lxml setup fails
|
||||||
|
#https://bugs.launchpad.net/lxml/+bug/1977998
|
||||||
|
exclude: [{platform: windows-latest, python-version: '3.11'}]
|
||||||
|
# vvv just an example of excluding stuff from matrix
|
||||||
|
# exclude: [{platform: macos-latest, python-version: '3.6'}]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.platform }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# ugh https://github.com/actions/toolkit/blob/main/docs/commands.md#path-manipulation
|
||||||
|
- run: echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
# uncomment for SSH debugging
|
||||||
|
# - uses: mxschmitt/action-tmate@v3
|
||||||
|
|
||||||
|
# explicit bash command is necessary for Windows CI runner, otherwise it thinks it's cmd...
|
||||||
|
- run: bash .ci/run
|
||||||
|
|
||||||
|
- if: matrix.platform == 'ubuntu-latest' # no need to compute coverage for other platforms
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: .coverage.mypy_${{ matrix.platform }}_${{ matrix.python-version }}
|
||||||
|
path: .coverage.mypy/
|
||||||
|
|
||||||
|
###
|
||||||
|
build_extension:
|
||||||
|
env:
|
||||||
|
name: 'grasp'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
- run: extension/.ci/build --lint # debug version
|
||||||
|
- run: extension/.ci/build --lint --release
|
||||||
|
|
||||||
|
# TODO ugh. can't share github actions artifacts publicly...
|
||||||
|
# TODO for fuck's sake... why does it end up named as .zip.zip ????
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: '${{ env.name }}-chrome-debug-latest.zip'
|
||||||
|
path: 'extension/dist/artifacts/chrome/${{ env.name }}_dev_-*.zip'
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
.name : '${{ env.name }}-chrome-release-latest.zip'
|
||||||
|
path: 'extension/dist/artifacts/chrome/${{ env.name }}-*.zip'
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name : '${{ env.name }}-firefox-debug-latest.zip'
|
||||||
|
path: 'extension/dist/artifacts/firefox/${{ env.name }}_dev_-*.zip'
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name : '${{ env.name }}-firefox-release-latest.zip'
|
||||||
|
path: 'extension/dist/artifacts/firefox/${{ env.name }}-*.zip'
|
|
@ -1,5 +1,3 @@
|
||||||
Build status: [![CircleCI](https://circleci.com/gh/karlicoss/grasp.svg?style=svg)](https://circleci.com/gh/karlicoss/grasp)
|
|
||||||
|
|
||||||
Grasp is a browser extension for [Chrome](https://chrome.google.com/webstore/detail/org-grasp/ohhbcfjmnbmgkajljopdjcaokbpgbgfa) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/grasp), which adds a button/keybinding to capture current page title and url,
|
Grasp is a browser extension for [Chrome](https://chrome.google.com/webstore/detail/org-grasp/ohhbcfjmnbmgkajljopdjcaokbpgbgfa) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/grasp), which adds a button/keybinding to capture current page title and url,
|
||||||
possibly selected text, additional comments or tags and adds it into your [Org Mode](https://orgmode.org/) file.
|
possibly selected text, additional comments or tags and adds it into your [Org Mode](https://orgmode.org/) file.
|
||||||
|
|
||||||
|
|
48
TODO.org
48
TODO.org
|
@ -1,27 +1,43 @@
|
||||||
* TODO [#A] simple auth for server bit?
|
* TODO [#A] simple auth for server bit?
|
||||||
* TODO [#B] doesn't look like hotkey working unless you manually rebind.. wtf?
|
|
||||||
|
|
||||||
* TODO [#C] how to test it? try on different pages?
|
* TODO [#C] how to test it? try on different pages?
|
||||||
https://www.blazemeter.com/blog/6-easy-steps-testing-your-chrome-extension-selenium
|
https://www.blazemeter.com/blog/6-easy-steps-testing-your-chrome-extension-selenium
|
||||||
looks tricky...
|
looks tricky...
|
||||||
* TODO [#C] some sort of delayed append logic? e.g. don't dump immediately and add as subitems?
|
* TODO [#C] some sort of delayed append logic? e.g. don't dump immediately and add as subitems?
|
||||||
* TODO [#C] or, add menu to extension?
|
* TODO [#C] or, add menu to extension?
|
||||||
|
|
||||||
* TODO [#C] hmm. if template is specified on client site, it's easier to dispatch todo vs non-todo etc?..
|
* TODO [#C] hmm. if template is specified on client site, it's easier to dispatch todo vs non-todo etc?..
|
||||||
* TODO [#D] do not minfy for dev
|
* CNCL not sure, maybe extension press should be default? control it via option?
|
||||||
* TODO [#D] make manifest templated so we don't have to hardcode actions
|
:LOGBOOK:
|
||||||
|
- State "CNCL" from "TODO" [2022-12-29 Thu 17:41]
|
||||||
|
:END:
|
||||||
|
|
||||||
* TODO [#D] production build?
|
* DONE [#B] doesn't look like hotkey working unless you manually rebind.. wtf?
|
||||||
## Packing
|
:LOGBOOK:
|
||||||
After the development of your extension run the command
|
- State "DONE" from "TODO" [2022-12-29 Thu 17:41]
|
||||||
|
:END:
|
||||||
|
|
||||||
```
|
* DONE [#D] do not minfy for dev
|
||||||
$ NODE_ENV=production npm run build
|
:LOGBOOK:
|
||||||
```
|
- State "DONE" from "TODO" [2022-12-29 Thu 17:40]
|
||||||
Now, the content of `build` folder will be the extension ready to be submitted to the Chrome Web Store. Just take a look at the [official guide](https://developer.chrome.com/webstore/publish) to more infos about publishing.
|
:END:
|
||||||
|
|
||||||
|
* DONE [#D] make manifest templated so we don't have to hardcode actions
|
||||||
|
:LOGBOOK:
|
||||||
|
- State "DONE" from "TODO" [2022-12-29 Thu 17:40]
|
||||||
|
:END:
|
||||||
|
|
||||||
|
* DONE [#D] production build?
|
||||||
|
:LOGBOOK:
|
||||||
|
- State "DONE" from "TODO" [2022-12-29 Thu 17:40]
|
||||||
|
:END:
|
||||||
|
: ## Packing
|
||||||
|
: After the development of your extension run the command
|
||||||
|
:
|
||||||
|
: ```
|
||||||
|
: $ NODE_ENV=production npm run build
|
||||||
|
: ```
|
||||||
|
: Now, the content of `build` folder will be the extension ready to be submitted to the Chrome Web Store. Just take a look at the [official guide](https://developer.chrome.com/webstore/publish) to more infos about publishing.
|
||||||
|
|
||||||
|
|
||||||
* TODO not sure, maybe extension press should be default? control it via option?
|
|
||||||
* DONE [#A] adjust permissions... I guess gonna have to make it include /capture endpoint?
|
* DONE [#A] adjust permissions... I guess gonna have to make it include /capture endpoint?
|
||||||
CLOSED: [2019-01-06 Sun 23:57]
|
CLOSED: [2019-01-06 Sun 23:57]
|
||||||
|
|
||||||
|
@ -35,7 +51,11 @@ Now, the content of `build` folder will be the extension ready to be submitted t
|
||||||
CLOSED: [2019-01-13 Sun 13:01]
|
CLOSED: [2019-01-13 Sun 13:01]
|
||||||
* DONE [#B] release somewhere? not sure if necessary
|
* DONE [#B] release somewhere? not sure if necessary
|
||||||
CLOSED: [2019-01-07 Mon 23:57]
|
CLOSED: [2019-01-07 Mon 23:57]
|
||||||
** TODO [#C] post on reddit?
|
** DONE [#C] post on reddit?
|
||||||
|
:LOGBOOK:
|
||||||
|
- State "DONE" from "TODO" [2022-12-29 Thu 17:40]
|
||||||
|
:END:
|
||||||
|
|
||||||
* DONE [#B] get rid of kython, run ci for python too?
|
* DONE [#B] get rid of kython, run ci for python too?
|
||||||
CLOSED: [2019-01-07 Mon 23:57]
|
CLOSED: [2019-01-07 Mon 23:57]
|
||||||
|
|
||||||
|
|
74
build
74
build
|
@ -1,74 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
from subprocess import check_call
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
# TODO FIXME copypaste from promnesia
|
|
||||||
# TODO FIXME use it in ci script
|
|
||||||
TARGETS = ['chrome', 'firefox']
|
|
||||||
def main():
|
|
||||||
p = argparse.ArgumentParser()
|
|
||||||
p.add_argument('--release', action='store_true')
|
|
||||||
p.add_argument('--sign', action='store_true')
|
|
||||||
p.add_argument('--lint', action='store_true')
|
|
||||||
|
|
||||||
tg = p.add_mutually_exclusive_group(required=True)
|
|
||||||
tg.add_argument('--target', type=str, choices=TARGETS)
|
|
||||||
for b in TARGETS:
|
|
||||||
tg.add_argument('--' + b, action='store_const', const=b, dest='target')
|
|
||||||
args = p.parse_args()
|
|
||||||
target = args.target
|
|
||||||
|
|
||||||
assert target is not None
|
|
||||||
|
|
||||||
dist_dir = Path(__file__).absolute().parent / 'dist'
|
|
||||||
def webext(*args, **kwargs):
|
|
||||||
src_dir = dist_dir # / target
|
|
||||||
src_dir = src_dir.resolve() # TODO ugh. webext can't into symlinks???
|
|
||||||
check_call([
|
|
||||||
'web-ext',
|
|
||||||
'--source-dir', str(src_dir),
|
|
||||||
*args,
|
|
||||||
], **kwargs)
|
|
||||||
|
|
||||||
env = {
|
|
||||||
'TARGET': target,
|
|
||||||
'RELEASE': 'YES' if args.release else 'NO',
|
|
||||||
**os.environ,
|
|
||||||
}
|
|
||||||
check_call([
|
|
||||||
'npm', 'run', 'build',
|
|
||||||
], env=env, cwd=str(Path(__file__).absolute().parent))
|
|
||||||
|
|
||||||
if args.lint:
|
|
||||||
# TODO --self-hosted
|
|
||||||
# TODO warnings as errors??
|
|
||||||
webext('lint')
|
|
||||||
|
|
||||||
if args.release:
|
|
||||||
assert args.lint # TODO not sure..
|
|
||||||
|
|
||||||
if args.sign:
|
|
||||||
assert args.release
|
|
||||||
if target not in ['firefox', 'firefox-mobile']:
|
|
||||||
raise RuntimeError("{target} is not supported for signing yet".format(target=target))
|
|
||||||
|
|
||||||
dist_dir = Path(__file__).absolute().parent / 'dist'
|
|
||||||
src_dir = dist_dir / target
|
|
||||||
|
|
||||||
from firefox_dev_secrets import API_KEY, API_SECRET
|
|
||||||
webext(
|
|
||||||
'--source-dir' , str(src_dir),
|
|
||||||
'--artifacts-dir', str(dist_dir),
|
|
||||||
'--api-key' , API_KEY,
|
|
||||||
'--api-secret', API_SECRET,
|
|
||||||
'sign',
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|
||||||
# TODO use UUID? https://addons.mozilla.org/en-GB/developers/addon/grasp/edit
|
|
||||||
# TODO right, wouldn't work because it's listed..
|
|
8
ci/mypy
8
ci/mypy
|
@ -1,8 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
# TODO ugh. doesn't work from circleci...
|
|
||||||
# cd "$(git rev-parse --show-toplevel)"
|
|
||||||
|
|
||||||
# TODO use strict?
|
|
||||||
python3 -m mypy --check-untyped-defs "$@" server/*.py server/setup
|
|
|
@ -1,7 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
# TODO ugh. doesn't work from circleci...
|
|
||||||
# cd "$(git rev-parse --show-toplevel)"
|
|
||||||
|
|
||||||
python3 -m pylint -E "$@" server/*.py server/setup
|
|
|
@ -1,7 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
# cd "$(git rev-parse --show-toplevel)"
|
|
||||||
|
|
||||||
python3 -m pytest "$@" server/*.py
|
|
||||||
# TODO force it to collect server/setup
|
|
22
extension/.ci/build
Executable file
22
extension/.ci/build
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -eux
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
cd ../.. # git root
|
||||||
|
|
||||||
|
cd extension
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
npm run flow || FAILED=1
|
||||||
|
npm run eslint || FAILED=1
|
||||||
|
# TODO later
|
||||||
|
#npm run test || FAILED=1
|
||||||
|
|
||||||
|
for browser in 'firefox' 'chrome'; do
|
||||||
|
./build --target "$browser" "$@"
|
||||||
|
done
|
||||||
|
|
||||||
|
exit "$FAILED"
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"parser": "babel-eslint",
|
"parser": "@babel/eslint-parser",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"flowtype"
|
"flowtype"
|
||||||
],
|
],
|
||||||
|
@ -27,12 +27,12 @@ module.exports = {
|
||||||
"indent": "off",
|
"indent": "off",
|
||||||
"comma-dangle": "off",
|
"comma-dangle": "off",
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
|
"no-inner-declarations": "off",
|
||||||
|
"flowtype/space-before-type-colon": "off",
|
||||||
|
"no-unused-vars": ["error", {
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_"
|
||||||
|
}],
|
||||||
}
|
}
|
||||||
// TODO use flow?
|
// TODO use flow?
|
||||||
};
|
};
|
||||||
//module.exports = {
|
|
||||||
// // "extends": "google",
|
|
||||||
// 'extends': [
|
|
||||||
// "plugin:react/recommended",
|
|
||||||
// ],
|
|
||||||
//};
|
|
|
@ -2,6 +2,7 @@
|
||||||
.*/node_modules/.*
|
.*/node_modules/.*
|
||||||
|
|
||||||
[include]
|
[include]
|
||||||
|
src/
|
||||||
|
|
||||||
[libs]
|
[libs]
|
||||||
node_modules/flow-interfaces-chrome/interfaces/
|
node_modules/flow-interfaces-chrome/interfaces/
|
10
extension/babel.config.js
Normal file
10
extension/babel.config.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
const presets = [
|
||||||
|
'@babel/preset-flow',
|
||||||
|
]
|
||||||
|
const plugins = []
|
||||||
|
|
||||||
|
// if (process.env["ENV"] === "prod") {
|
||||||
|
// plugins.push(...);
|
||||||
|
// }
|
||||||
|
|
||||||
|
module.exports = { presets, plugins }
|
117
extension/build
Executable file
117
extension/build
Executable file
|
@ -0,0 +1,117 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
from subprocess import check_call
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# right, specifying id in manifest doesn't seem to work
|
||||||
|
# AMO responds with: Server response: Duplicate add-on ID found. (status: 400)
|
||||||
|
IDS = {
|
||||||
|
'firefox' : '{37e42980-a7c9-473c-96d5-13f18e0efc74}',
|
||||||
|
'chrome' : 'ohhbcfjmnbmgkajljopdjcaokbpgbgfa',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TARGETS = [
|
||||||
|
'chrome',
|
||||||
|
'firefox',
|
||||||
|
]
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument('--release', action='store_true', help="Use release flavor of build")
|
||||||
|
p.add_argument('--watch' , action='store_true')
|
||||||
|
p.add_argument('--lint' , action='store_true')
|
||||||
|
p.add_argument('--publish', action='store_true', help="Publish on chrome web store/addons.mozilla.org")
|
||||||
|
|
||||||
|
tg = p.add_mutually_exclusive_group(required=True)
|
||||||
|
tg.add_argument('--target', type=str, choices=TARGETS)
|
||||||
|
for b in TARGETS:
|
||||||
|
tg.add_argument('--' + b, action='store_const', const=b, dest='target')
|
||||||
|
args = p.parse_args()
|
||||||
|
target = args.target
|
||||||
|
|
||||||
|
assert target is not None
|
||||||
|
|
||||||
|
base_ext_dir = Path(__file__).absolute().parent / 'dist'
|
||||||
|
ext_dir = (base_ext_dir / target).resolve() # webext can't into symlinks
|
||||||
|
# sadly no way to specify zip name in the regex..
|
||||||
|
artifacts_dir = (base_ext_dir / 'artifacts' / target).resolve()
|
||||||
|
def webext(*args, **kwargs):
|
||||||
|
check_call([
|
||||||
|
'npm', 'run', 'web-ext',
|
||||||
|
'--',
|
||||||
|
'--source-dir' , ext_dir,
|
||||||
|
'--artifacts-dir', artifacts_dir,
|
||||||
|
*args,
|
||||||
|
], **kwargs)
|
||||||
|
|
||||||
|
env = {
|
||||||
|
'TARGET' : target,
|
||||||
|
'RELEASE': 'YES' if args.release else 'NO',
|
||||||
|
'PUBLISH': 'YES' if args.publish else 'NO',
|
||||||
|
'EXT_ID' : IDS[target],
|
||||||
|
**os.environ,
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.watch:
|
||||||
|
check_call([
|
||||||
|
'npm', 'run', 'watch',
|
||||||
|
], env=env, cwd=str(Path(__file__).absolute().parent))
|
||||||
|
# TODO exec instead?
|
||||||
|
return
|
||||||
|
|
||||||
|
check_call([
|
||||||
|
'npm', 'run', 'build',
|
||||||
|
], env=env, cwd=str(Path(__file__).absolute().parent))
|
||||||
|
|
||||||
|
if args.lint:
|
||||||
|
# TODO --self-hosted
|
||||||
|
# TODO warnings as errors??
|
||||||
|
webext('lint')
|
||||||
|
|
||||||
|
# TODO move somewhere more appropriate..
|
||||||
|
webext('build', '-o') # -o overwrites existing artifact
|
||||||
|
|
||||||
|
|
||||||
|
if args.release:
|
||||||
|
assert args.lint # TODO not sure..
|
||||||
|
|
||||||
|
def firefox_release_args():
|
||||||
|
from firefox_dev_secrets import API_KEY, API_SECRET
|
||||||
|
return [
|
||||||
|
'--artifacts-dir', str(artifacts_dir),
|
||||||
|
'--api-key' , API_KEY,
|
||||||
|
'--api-secret' , API_SECRET,
|
||||||
|
'--id' , IDS[target],
|
||||||
|
]
|
||||||
|
|
||||||
|
if args.publish:
|
||||||
|
assert args.release
|
||||||
|
if 'firefox' in target:
|
||||||
|
check_call([
|
||||||
|
'npm', 'run', 'release:amo',
|
||||||
|
'--',
|
||||||
|
'--channel', 'listed',
|
||||||
|
'--source-dir', str(ext_dir),
|
||||||
|
*firefox_release_args(),
|
||||||
|
])
|
||||||
|
elif target == 'chrome':
|
||||||
|
from chrome_dev_secrets import CLIENT_ID, CLIENT_SECRET, REFRESH_TOKEN
|
||||||
|
check_call([
|
||||||
|
'npm', 'run', 'release:cws',
|
||||||
|
'--',
|
||||||
|
'upload',
|
||||||
|
# '--auto-publish',
|
||||||
|
'--source' , str(ext_dir),
|
||||||
|
'--client-id' , CLIENT_ID,
|
||||||
|
'--client-secret' , CLIENT_SECRET,
|
||||||
|
'--refresh--token', REFRESH_TOKEN,
|
||||||
|
'--extension-id' , IDS[target],
|
||||||
|
# TODO trusted testers?
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
raise RuntimeError("{target} is not supported for publishing yet".format(target=target))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
101
extension/flow-typed/webextension-polyfill.js
vendored
Normal file
101
extension/flow-typed/webextension-polyfill.js
vendored
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Based on https://github.com/Shraymonks/flow-interfaces-chrome
|
||||||
|
* not sure if browser polyfill has flow types??
|
||||||
|
* https://github.com/mozilla/webextension-polyfill/issues/421
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
type browser$StorageArea = {
|
||||||
|
get: (keys: string | Array<string> | Object | null) => Promise<Object>,
|
||||||
|
set: (items: Object) => Promise<void>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type browser$storage = {
|
||||||
|
local: browser$StorageArea,
|
||||||
|
managed: browser$StorageArea,
|
||||||
|
sync: browser$StorageArea,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type result = boolean
|
||||||
|
type granted = boolean
|
||||||
|
|
||||||
|
|
||||||
|
type browser$permissions = {
|
||||||
|
contains: (permissions: chrome$Permissions) => Promise<result>,
|
||||||
|
request : (permissions: chrome$Permissions) => Promise<granted>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type browser$tabs = {
|
||||||
|
query(queryInfo: {
|
||||||
|
active?: boolean,
|
||||||
|
audible?: boolean,
|
||||||
|
currentWindow?: boolean,
|
||||||
|
highlighted?: boolean,
|
||||||
|
index?: number,
|
||||||
|
lastFocusedWindow?: boolean,
|
||||||
|
muted?: boolean,
|
||||||
|
pinned?: boolean,
|
||||||
|
status?: chrome$TabStatus,
|
||||||
|
title?: string,
|
||||||
|
url?: string | Array<string>,
|
||||||
|
windowId?: number,
|
||||||
|
windowType?: chrome$WindowType
|
||||||
|
}): Promise<Array<chrome$Tab>>,
|
||||||
|
executeScript: (
|
||||||
|
((
|
||||||
|
tabId?: number,
|
||||||
|
details: {
|
||||||
|
allFrames?: boolean,
|
||||||
|
code?: string,
|
||||||
|
file?: string,
|
||||||
|
frameId?: number,
|
||||||
|
matchAboutBlank?: boolean,
|
||||||
|
runAt?: chrome$RunAt
|
||||||
|
},
|
||||||
|
) => Promise<void>) &
|
||||||
|
((
|
||||||
|
details: {
|
||||||
|
allFrames?: boolean,
|
||||||
|
code?: string,
|
||||||
|
file?: string,
|
||||||
|
frameId?: number,
|
||||||
|
matchAboutBlank?: boolean,
|
||||||
|
runAt?: chrome$RunAt
|
||||||
|
},
|
||||||
|
) => Promise<void>)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type ExecuteResult = {
|
||||||
|
result: any,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type browser$scripting = {
|
||||||
|
executeScript: ({
|
||||||
|
target: {tabId?: number},
|
||||||
|
func: () => void,
|
||||||
|
}) => Promise<Array<ExecuteResult>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
declare var browser: {
|
||||||
|
storage: browser$storage,
|
||||||
|
permissions: browser$permissions,
|
||||||
|
tabs: browser$tabs,
|
||||||
|
scripting: browser$scripting,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
declare module "webextension-polyfill" {
|
||||||
|
declare var storage: browser$storage;
|
||||||
|
declare var permissions: browser$permissions;
|
||||||
|
declare var tabs: browser$tabs;
|
||||||
|
declare var scripting: browser$scripting;
|
||||||
|
}
|
188
extension/jest.config.js
Normal file
188
extension/jest.config.js
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
// For a detailed explanation regarding each configuration property, visit:
|
||||||
|
// https://jestjs.io/docs/en/configuration.html
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// All imported modules in your tests should be mocked automatically
|
||||||
|
// automock: false,
|
||||||
|
|
||||||
|
// Stop running tests after `n` failures
|
||||||
|
// bail: 0,
|
||||||
|
|
||||||
|
// Respect "browser" field in package.json when resolving modules
|
||||||
|
// browser: false,
|
||||||
|
|
||||||
|
// The directory where Jest should store its cached dependency information
|
||||||
|
// cacheDirectory: "/tmp/jest_rs",
|
||||||
|
|
||||||
|
// Automatically clear mock calls and instances between every test
|
||||||
|
clearMocks: true,
|
||||||
|
|
||||||
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
|
// collectCoverage: false,
|
||||||
|
|
||||||
|
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||||
|
// collectCoverageFrom: null,
|
||||||
|
|
||||||
|
// The directory where Jest should output its coverage files
|
||||||
|
// coverageDirectory: null,
|
||||||
|
|
||||||
|
// An array of regexp pattern strings used to skip coverage collection
|
||||||
|
// coveragePathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// A list of reporter names that Jest uses when writing coverage reports
|
||||||
|
// coverageReporters: [
|
||||||
|
// "json",
|
||||||
|
// "text",
|
||||||
|
// "lcov",
|
||||||
|
// "clover"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An object that configures minimum threshold enforcement for coverage results
|
||||||
|
// coverageThreshold: null,
|
||||||
|
|
||||||
|
// A path to a custom dependency extractor
|
||||||
|
// dependencyExtractor: null,
|
||||||
|
|
||||||
|
// Make calling deprecated APIs throw helpful error messages
|
||||||
|
// errorOnDeprecated: false,
|
||||||
|
|
||||||
|
// Force coverage collection from ignored files using an array of glob patterns
|
||||||
|
// forceCoverageMatch: [],
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once before all test suites
|
||||||
|
// globalSetup: null,
|
||||||
|
|
||||||
|
// A path to a module which exports an async function that is triggered once after all test suites
|
||||||
|
// globalTeardown: null,
|
||||||
|
|
||||||
|
// A set of global variables that need to be available in all test environments
|
||||||
|
// globals: {},
|
||||||
|
|
||||||
|
// An array of directory names to be searched recursively up from the requiring module's location
|
||||||
|
// moduleDirectories: [
|
||||||
|
// "node_modules"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of file extensions your modules use
|
||||||
|
// moduleFileExtensions: [
|
||||||
|
// "js",
|
||||||
|
// "json",
|
||||||
|
// "jsx",
|
||||||
|
// "ts",
|
||||||
|
// "tsx",
|
||||||
|
// "node"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||||
|
// moduleNameMapper: {},
|
||||||
|
|
||||||
|
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||||
|
// modulePathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Activates notifications for test results
|
||||||
|
// notify: false,
|
||||||
|
|
||||||
|
// An enum that specifies notification mode. Requires { notify: true }
|
||||||
|
// notifyMode: "failure-change",
|
||||||
|
|
||||||
|
// A preset that is used as a base for Jest's configuration
|
||||||
|
// preset: null,
|
||||||
|
|
||||||
|
// Run tests from one or more projects
|
||||||
|
// projects: null,
|
||||||
|
|
||||||
|
// Use this configuration option to add custom reporters to Jest
|
||||||
|
// reporters: undefined,
|
||||||
|
|
||||||
|
// Automatically reset mock state between every test
|
||||||
|
// resetMocks: false,
|
||||||
|
|
||||||
|
// Reset the module registry before running each individual test
|
||||||
|
// resetModules: false,
|
||||||
|
|
||||||
|
// A path to a custom resolver
|
||||||
|
// resolver: null,
|
||||||
|
|
||||||
|
// Automatically restore mock state between every test
|
||||||
|
// restoreMocks: false,
|
||||||
|
|
||||||
|
// The root directory that Jest should scan for tests and modules within
|
||||||
|
// rootDir: null,
|
||||||
|
|
||||||
|
// A list of paths to directories that Jest should use to search for files in
|
||||||
|
// roots: [
|
||||||
|
// "<rootDir>"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// Allows you to use a custom runner instead of Jest's default test runner
|
||||||
|
// runner: "jest-runner",
|
||||||
|
|
||||||
|
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||||
|
// setupFiles: [],
|
||||||
|
|
||||||
|
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||||
|
// setupFilesAfterEnv: [],
|
||||||
|
|
||||||
|
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||||
|
// snapshotSerializers: [],
|
||||||
|
|
||||||
|
// The test environment that will be used for testing
|
||||||
|
testEnvironment: "jsdom",
|
||||||
|
|
||||||
|
// Options that will be passed to the testEnvironment
|
||||||
|
// testEnvironmentOptions: {},
|
||||||
|
|
||||||
|
// Adds a location field to test results
|
||||||
|
// testLocationInResults: false,
|
||||||
|
|
||||||
|
// The glob patterns Jest uses to detect test files
|
||||||
|
// testMatch: [
|
||||||
|
// "**/__tests__/**/*.[jt]s?(x)",
|
||||||
|
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
|
// testPathIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||||
|
// testRegex: [],
|
||||||
|
|
||||||
|
// This option allows the use of a custom results processor
|
||||||
|
// testResultsProcessor: null,
|
||||||
|
|
||||||
|
// This option allows use of a custom test runner
|
||||||
|
// testRunner: "jasmine2",
|
||||||
|
|
||||||
|
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||||
|
// testURL: "http://localhost",
|
||||||
|
|
||||||
|
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||||
|
// timers: "real",
|
||||||
|
|
||||||
|
// A map from regular expressions to paths to transformers
|
||||||
|
// transform: null,
|
||||||
|
|
||||||
|
// ??? https://github.com/facebook/jest/issues/9395#issuecomment-665239070
|
||||||
|
// otherwise fails on importing
|
||||||
|
transformIgnorePatterns: ['node_modules/?!@webext-options-sync/']
|
||||||
|
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||||
|
// transformIgnorePatterns: [
|
||||||
|
// "/node_modules/"
|
||||||
|
// ],
|
||||||
|
|
||||||
|
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||||
|
// unmockedModulePathPatterns: undefined,
|
||||||
|
|
||||||
|
// Indicates whether each individual test should be reported during the run
|
||||||
|
// verbose: null,
|
||||||
|
|
||||||
|
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||||
|
// watchPathIgnorePatterns: [],
|
||||||
|
|
||||||
|
// Whether to use watchman for file crawling
|
||||||
|
// watchman: true,
|
||||||
|
};
|
57
extension/package.json
Normal file
57
extension/package.json
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
{
|
||||||
|
"name": "grasp",
|
||||||
|
"version": "0.7.1",
|
||||||
|
"version_name": "released on 2022.12.28",
|
||||||
|
"description": "Reliably capture links and webpage content in a plaintext file (org-mode/markdown)",
|
||||||
|
"main": "dist/background.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "jest",
|
||||||
|
"build": "webpack --progress --profile",
|
||||||
|
"watch": "webpack --watch --progress",
|
||||||
|
"dev": "npm run watch",
|
||||||
|
"eslint": "eslint src",
|
||||||
|
"flow": "flow",
|
||||||
|
"web-ext": "web-ext",
|
||||||
|
"release:cws": "chrome-webstore-upload",
|
||||||
|
"release:amo": "web-ext-submit"
|
||||||
|
},
|
||||||
|
"browserslist": ["defaults and supports es6-module"],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/karlicoss/grasp.git"
|
||||||
|
},
|
||||||
|
"author": "Dmitrii Gerasimov <karlicoss@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/karlicoss/grasp/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/karlicoss/grasp#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.16.0",
|
||||||
|
"@babel/eslint-parser": "^7.16.3",
|
||||||
|
"@babel/preset-env": "^7.16.4",
|
||||||
|
"@babel/preset-flow": "^7.16.0",
|
||||||
|
"babel-loader": "^9.1.0",
|
||||||
|
"chrome-webstore-upload-cli": "^2.1.0",
|
||||||
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"css-loader": "^6.5.1",
|
||||||
|
"eslint": "^8.15.0",
|
||||||
|
"eslint-plugin-flowtype": "^8.0.3",
|
||||||
|
"flow-bin": "^0.196.3",
|
||||||
|
"flow-interfaces-chrome": "^0.7.0",
|
||||||
|
"jest": "^28.1.0",
|
||||||
|
"jest-environment-jsdom": "^28.1.0",
|
||||||
|
"jest-fetch-mock": "^3.0.3",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"web-ext": "^7.4.0",
|
||||||
|
"web-ext-submit": "^7.4.0",
|
||||||
|
"webextension-polyfill": "^0.10.0",
|
||||||
|
"webpack": "^5.75.0",
|
||||||
|
"webpack-cli": "^5.0.1",
|
||||||
|
"webpack-dev-server": "^4.11.1",
|
||||||
|
"webpack-extension-manifest-plugin": "^0.8.0"
|
||||||
|
},
|
||||||
|
"private": "true"
|
||||||
|
}
|
BIN
extension/src/img/unicorn.png
Normal file
BIN
extension/src/img/unicorn.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
162
extension/src/js/background.js
Normal file
162
extension/src/js/background.js
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
|
||||||
|
// hmm this works if you declare type: module in manifest
|
||||||
|
// but it injects the script's contents statically?
|
||||||
|
// I assume this is only processed with webpack
|
||||||
|
// import * as browser from "./browser-polyfill"
|
||||||
|
|
||||||
|
// this works with v3 but NOT if type: module
|
||||||
|
// also unclear how to make it work with webpack??
|
||||||
|
// const manifestVersion = chrome.runtime.getManifest().manifest_version
|
||||||
|
// if (manifestVersion == 3) {
|
||||||
|
// // for v3 it's provided via manifest
|
||||||
|
// try {
|
||||||
|
// /* eslint-disable no-undef */
|
||||||
|
// importScripts('./browser-polyfill.js')
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error(e)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// this works for v2 both in chrome and firefox
|
||||||
|
// for v3
|
||||||
|
// - in chrome it works if type: module, but somehow NOT if we define chunks??
|
||||||
|
// without type: module this isn't working
|
||||||
|
// - in firefox it just works :shrug:
|
||||||
|
// but firefox foesn't allow type: module??
|
||||||
|
// could it be bug in chrome??
|
||||||
|
import * as browser from "webextension-polyfill"
|
||||||
|
|
||||||
|
|
||||||
|
import {COMMAND_CAPTURE_SIMPLE, METHOD_CAPTURE_WITH_EXTRAS, showNotification} from './common'
|
||||||
|
import {getOptions} from './options'
|
||||||
|
import type {Options} from './options'
|
||||||
|
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
url: string,
|
||||||
|
title: ?string,
|
||||||
|
selection: ?string,
|
||||||
|
comment: ?string,
|
||||||
|
tag_str: ?string,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function makeCaptureRequest(
|
||||||
|
params: Params,
|
||||||
|
options: Options,
|
||||||
|
) {
|
||||||
|
if (params.tag_str == null) {
|
||||||
|
params.tag_str = options.default_tags
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = options.endpoint
|
||||||
|
|
||||||
|
const data = JSON.stringify(params)
|
||||||
|
console.log(`capturing ${data} via ${endpoint}`)
|
||||||
|
|
||||||
|
|
||||||
|
// TODO maybe handle catch first and then again?
|
||||||
|
|
||||||
|
await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: data,
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
// fetch only rejects when host is unavailable
|
||||||
|
console.error(err)
|
||||||
|
throw new Error(`${endpoint} unavailable, check if server is running`)
|
||||||
|
}).then(async response => {
|
||||||
|
if (!response.ok) { // http error
|
||||||
|
throw new Error(`${endpoint}: HTTP ${response.status} ${response.statusText}`)
|
||||||
|
}
|
||||||
|
const jres = await response.json()
|
||||||
|
const path = jres.path
|
||||||
|
console.log(`success: ${jres}`);
|
||||||
|
if (options.notification) {
|
||||||
|
showNotification(`OK: captured to ${path}`);
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => {
|
||||||
|
console.error(err)
|
||||||
|
// todo make sure we catch errors from then clause too?
|
||||||
|
const error_message = `ERROR: ${err.toString()}`
|
||||||
|
console.error(error_message);
|
||||||
|
showNotification(error_message, 1);
|
||||||
|
// TODO crap, doesn't really seem to respect urgency...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO ugh. need defensive error handling on the very top...
|
||||||
|
async function capture(comment: ?string = null, tag_str: ?string = null) {
|
||||||
|
const tabs = await browser.tabs.query({currentWindow: true, active: true})
|
||||||
|
const tab = tabs[0]
|
||||||
|
if (tab.url == null) {
|
||||||
|
// todo await?
|
||||||
|
showNotification('ERROR: trying to capture null')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const url: string = tab.url
|
||||||
|
const title: ?string = tab.title
|
||||||
|
|
||||||
|
|
||||||
|
const payload = (selection: ?string) => {
|
||||||
|
return {
|
||||||
|
url: url,
|
||||||
|
title: title,
|
||||||
|
selection: selection,
|
||||||
|
comment: comment,
|
||||||
|
tag_str: tag_str,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = await getOptions()
|
||||||
|
|
||||||
|
const has_scripting = 'scripting' in chrome
|
||||||
|
let selection
|
||||||
|
if (has_scripting) {
|
||||||
|
// TODO switch to polyfill and add flow types
|
||||||
|
// scripting is already promise based so it should be oly change to types
|
||||||
|
const results = await browser.scripting.executeScript({
|
||||||
|
target: {tabId: tab.id},
|
||||||
|
func: () => window.getSelection().toString()
|
||||||
|
})
|
||||||
|
const [res] = results // should only inject in one frame, so just one result
|
||||||
|
selection = res.result
|
||||||
|
} else {
|
||||||
|
const selections = await browser.tabs.executeScript({
|
||||||
|
code: "window.getSelection().toString();"
|
||||||
|
})
|
||||||
|
selection = selections == null ? null : selections[0]
|
||||||
|
}
|
||||||
|
await makeCaptureRequest(payload(selection), opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
chrome.commands.onCommand.addListener(command => {
|
||||||
|
if (command === COMMAND_CAPTURE_SIMPLE) {
|
||||||
|
// todo await
|
||||||
|
capture(null, null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((message: any, sender: chrome$MessageSender, sendResponse) => {
|
||||||
|
if (message.method === 'logging') {
|
||||||
|
console.error("[%s] %o", message.source, message.data)
|
||||||
|
}
|
||||||
|
if (message.method === METHOD_CAPTURE_WITH_EXTRAS) {
|
||||||
|
const comment = message.comment;
|
||||||
|
const tag_str = message.tag_str;
|
||||||
|
// todo await
|
||||||
|
capture(comment, tag_str)
|
||||||
|
sendResponse()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO handle cannot access chrome:// url??
|
||||||
|
|
|
@ -9,13 +9,14 @@ export function showNotification(text: string, priority: number=0) {
|
||||||
'title': "grasp",
|
'title': "grasp",
|
||||||
'message': text,
|
'message': text,
|
||||||
'priority': priority,
|
'priority': priority,
|
||||||
'iconUrl': 'unicorn.png',
|
'iconUrl': 'img/unicorn.png',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
export function awrap(fn, ...args: Array<any>): Promise<any> {
|
export function awrap(fn, ...args: Array<any>): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// $FlowFixMe
|
||||||
const cbb = (...xxx) => {
|
const cbb = (...xxx) => {
|
||||||
const err = chrome.runtime.lastError;
|
const err = chrome.runtime.lastError;
|
||||||
if (err) {
|
if (err) {
|
30
extension/src/js/options.js
Normal file
30
extension/src/js/options.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/* @flow */
|
||||||
|
import browser from "webextension-polyfill"
|
||||||
|
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
endpoint: string;
|
||||||
|
default_tags: string;
|
||||||
|
notification: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function defaultOptions(): Options {
|
||||||
|
return {
|
||||||
|
endpoint: "http://localhost:12212/capture",
|
||||||
|
default_tags: "grasp",
|
||||||
|
notification: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getOptions(): Promise<Options> {
|
||||||
|
const res = await browser.storage.local.get(null)
|
||||||
|
return {...defaultOptions(), ...res}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function setOptions(opts: Options): Promise<void> {
|
||||||
|
console.log('Saving %o', opts)
|
||||||
|
await browser.storage.local.set(opts)
|
||||||
|
}
|
84
extension/src/js/options_page.js
Normal file
84
extension/src/js/options_page.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
/* @flow */
|
||||||
|
import {getOptions, setOptions} from './options'
|
||||||
|
import {ensurePermissions, hasPermissions} from './permissions'
|
||||||
|
|
||||||
|
|
||||||
|
const ENDPOINT_ID = 'endpoint_id';
|
||||||
|
const HAS_PERMISSION_ID = 'has_permission_id';
|
||||||
|
const NOTIFICATION_ID = 'notification_id';
|
||||||
|
const DEFAULT_TAGS_ID = 'default_tags_id';
|
||||||
|
const SAVE_ID = 'save_id';
|
||||||
|
|
||||||
|
|
||||||
|
function getEndpoint(): HTMLInputElement {
|
||||||
|
return ((document.getElementById(ENDPOINT_ID): any): HTMLInputElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultTags(): HTMLInputElement {
|
||||||
|
return ((document.getElementById(DEFAULT_TAGS_ID): any): HTMLInputElement);
|
||||||
|
// TODO if empty, return null?
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEnableNotification(): HTMLInputElement {
|
||||||
|
return ((document.getElementById(NOTIFICATION_ID): any): HTMLInputElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSaveButton(): HTMLInputElement {
|
||||||
|
return ((document.getElementById(SAVE_ID): any): HTMLInputElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHasPermission(): HTMLElement {
|
||||||
|
return ((document.getElementById(HAS_PERMISSION_ID): any): HTMLElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ugh. needs a better name..
|
||||||
|
async function refreshPermissionValidation(endpoint: string) {
|
||||||
|
const has = await hasPermissions(endpoint)
|
||||||
|
const pstyle = getHasPermission().style
|
||||||
|
if (has) {
|
||||||
|
console.debug('Got permisssions, nothing to worry about.')
|
||||||
|
pstyle.display = 'none'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.debug('Whoops, no permissions to access %s', endpoint)
|
||||||
|
// TODO maybe just show button? but then it would need to be reactive..
|
||||||
|
pstyle.display = 'block'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function restoreOptions() {
|
||||||
|
getSaveButton().addEventListener('click', saveOptions)
|
||||||
|
|
||||||
|
const opts = await getOptions()
|
||||||
|
getEndpoint().value = opts.endpoint
|
||||||
|
getDefaultTags().value = opts.default_tags
|
||||||
|
getEnableNotification().checked = opts.notification
|
||||||
|
await refreshPermissionValidation(opts.endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function saveOptions() {
|
||||||
|
// TODO could also check for permissions and display message?
|
||||||
|
|
||||||
|
const endpoint = getEndpoint().value
|
||||||
|
await ensurePermissions(endpoint)
|
||||||
|
refreshPermissionValidation(endpoint)
|
||||||
|
|
||||||
|
const opts = {
|
||||||
|
endpoint: endpoint,
|
||||||
|
default_tags: getDefaultTags().value,
|
||||||
|
notification: getEnableNotification().checked,
|
||||||
|
}
|
||||||
|
await setOptions(opts)
|
||||||
|
|
||||||
|
// NOTE: seems that alert() only works here when we use open_in_tab: true for settings page
|
||||||
|
// when open_in_tab: false, then
|
||||||
|
// in chrome browser.extension.getBackgroundPage().alert works
|
||||||
|
// in firefox that doesn't work, seems that it refuses to alert from bg page
|
||||||
|
// so it seems easier to just always use opne_in_tab: true for setgings page
|
||||||
|
alert('Saved!')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', restoreOptions)
|
|
@ -1,15 +1,5 @@
|
||||||
/* @flow */
|
/* @flow */
|
||||||
import {showNotification, awrap} from './common';
|
import browser from "webextension-polyfill"
|
||||||
import {getOptions} from './options';
|
|
||||||
|
|
||||||
|
|
||||||
export function chromePermissionsRequest(params: any) {
|
|
||||||
return awrap(chrome.permissions.request, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function chromePermissionsContains(params: any) {
|
|
||||||
return awrap(chrome.permissions.contains, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function urlForPermissionsCheck(url: string): string {
|
function urlForPermissionsCheck(url: string): string {
|
||||||
|
@ -23,23 +13,24 @@ function urlForPermissionsCheck(url: string): string {
|
||||||
return u.toString();
|
return u.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasPermissions(endpoint: string) {
|
export async function hasPermissions(endpoint: string): Promise<boolean> {
|
||||||
const perms = {
|
const perms = {
|
||||||
origins: [
|
origins: [
|
||||||
urlForPermissionsCheck(endpoint),
|
urlForPermissionsCheck(endpoint),
|
||||||
],
|
],
|
||||||
};
|
}
|
||||||
return chromePermissionsContains(perms);
|
return browser.permissions.contains(perms)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensurePermissions(endpoint: string) {
|
|
||||||
|
export function ensurePermissions(endpoint: string): Promise<boolean> {
|
||||||
const perms = {
|
const perms = {
|
||||||
origins: [
|
origins: [
|
||||||
urlForPermissionsCheck(endpoint),
|
urlForPermissionsCheck(endpoint),
|
||||||
],
|
],
|
||||||
};
|
}
|
||||||
// shouldn't prompt if we already have the permission
|
// shouldn't prompt if we already have the permission
|
||||||
return chromePermissionsRequest(perms);
|
return browser.permissions.request(perms)
|
||||||
}
|
}
|
||||||
|
|
||||||
// just keeping the old one for the reference...
|
// just keeping the old one for the reference...
|
||||||
|
@ -47,6 +38,7 @@ export function ensurePermissions(endpoint: string) {
|
||||||
// TODO fuck. doesn't seem to work for Firefox at all...
|
// TODO fuck. doesn't seem to work for Firefox at all...
|
||||||
// just getting Error: "An unexpected error occurred" background.bundle.js
|
// just getting Error: "An unexpected error occurred" background.bundle.js
|
||||||
// seems like this bug https://bugzilla.mozilla.org/show_bug.cgi?id=1382953
|
// seems like this bug https://bugzilla.mozilla.org/show_bug.cgi?id=1382953
|
||||||
|
/*
|
||||||
function ensurePermissions_old(cb: () => void) { // eslint-disable-line no-unused-vars
|
function ensurePermissions_old(cb: () => void) { // eslint-disable-line no-unused-vars
|
||||||
const back = chrome.extension.getBackgroundPage().console;
|
const back = chrome.extension.getBackgroundPage().console;
|
||||||
|
|
||||||
|
@ -82,3 +74,4 @@ function ensurePermissions_old(cb: () => void) { // eslint-disable-line no-unuse
|
||||||
});
|
});
|
||||||
// TODO improve message somehow???
|
// TODO improve message somehow???
|
||||||
}
|
}
|
||||||
|
*/
|
161
extension/src/js/popup.js
Normal file
161
extension/src/js/popup.js
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
|
||||||
|
import {METHOD_CAPTURE_WITH_EXTRAS} from './common'
|
||||||
|
import {getOptions} from './options'
|
||||||
|
|
||||||
|
// TODO template it in html too?
|
||||||
|
const BUTTON_ID = 'button_id';
|
||||||
|
const COMMENT_ID = 'comment_id';
|
||||||
|
const TAGS_ID = 'tags_id';
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* normal popup logging is pretty annoying because chrome closes devtools
|
||||||
|
*/
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
function logbg(msg: any) {
|
||||||
|
console.error('%o', msg)
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
method: 'logging',
|
||||||
|
source: 'popup.js',
|
||||||
|
data: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type State = {|
|
||||||
|
comment: string,
|
||||||
|
tag_str: string,
|
||||||
|
|}
|
||||||
|
|
||||||
|
|
||||||
|
function saveState(state: ?State) {
|
||||||
|
window.localStorage.setItem('state', JSON.stringify(state))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function clearState() {
|
||||||
|
saveState(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadState(): ?State {
|
||||||
|
const sts = window.localStorage.getItem('state') || null
|
||||||
|
return JSON.parse(sts)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getComment(): HTMLInputElement {
|
||||||
|
const comment = ((document.getElementById(COMMENT_ID): any): HTMLInputElement);
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getTags(): HTMLInputElement {
|
||||||
|
const tags = ((document.getElementById(TAGS_ID): any): HTMLInputElement);
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getButton(): HTMLElement {
|
||||||
|
const button = ((document.getElementById(BUTTON_ID): any): HTMLElement);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function getUiState(): State {
|
||||||
|
const comment_text = getComment().value;
|
||||||
|
const tag_str = getTags().value;
|
||||||
|
return {
|
||||||
|
'comment': comment_text,
|
||||||
|
'tag_str': tag_str,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function restoreState(state: ?State) {
|
||||||
|
window.justSubmitted = false
|
||||||
|
if (state == null) {
|
||||||
|
// comment just relies on default
|
||||||
|
const opts = await getOptions()
|
||||||
|
getTags().value = opts.default_tags
|
||||||
|
} else {
|
||||||
|
getComment().value = state.comment
|
||||||
|
getTags().value = state.tag_str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function submitCapture () {
|
||||||
|
const state = getUiState()
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage({
|
||||||
|
'method': METHOD_CAPTURE_WITH_EXTRAS,
|
||||||
|
...state,
|
||||||
|
}, () => {
|
||||||
|
window.justSubmitted = true
|
||||||
|
clearState()
|
||||||
|
|
||||||
|
// TODO not sure about this?
|
||||||
|
window.close()
|
||||||
|
console.log("[popup] captured!");
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// todo not sure if can define const here??
|
||||||
|
var ctrlEnterSubmit: KeyboardEventListener = (e) => {
|
||||||
|
// note: there is also e.metaKey which triggers on mac when cmd is pressed
|
||||||
|
// but it doesn't seem to allow chords like cmd+Enter
|
||||||
|
if ((e.ctrlKey || e.shiftKey) && e.key === 'Enter') {
|
||||||
|
submitCapture()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/6003829/706389
|
||||||
|
function moveCaretToEnd(el: HTMLInputElement) {
|
||||||
|
if (typeof el.selectionStart == "number") {
|
||||||
|
el.selectionStart = el.selectionEnd = el.value.length;
|
||||||
|
// $FlowFixMe https://github.com/facebook/flow/issues/8705
|
||||||
|
} else if (typeof el.createTextRange != "undefined") {
|
||||||
|
el.focus();
|
||||||
|
var range = el.createTextRange();
|
||||||
|
range.collapse(false);
|
||||||
|
range.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function setupPage () {
|
||||||
|
const comment = getComment();
|
||||||
|
comment.focus();
|
||||||
|
comment.addEventListener('keydown', ctrlEnterSubmit);
|
||||||
|
|
||||||
|
const tags = getTags();
|
||||||
|
tags.addEventListener('keydown', ctrlEnterSubmit);
|
||||||
|
tags.addEventListener('focus', () => moveCaretToEnd(tags)); // to put cursor to the end of tags when tabbed
|
||||||
|
|
||||||
|
getButton().addEventListener('click', submitCapture)
|
||||||
|
|
||||||
|
const state = loadState()
|
||||||
|
restoreState(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// hmm interesting that on chrome we can use visibilitychange for both
|
||||||
|
// on firefox dev edition however 'visible' doesn't fire, only 'hidden' :shrug:
|
||||||
|
document.addEventListener('DOMContentLoaded', setupPage)
|
||||||
|
window.addEventListener('visibilitychange', (_: Event) => {
|
||||||
|
// event doesn't contain visibility status
|
||||||
|
// docs recomment to use this instead of unload
|
||||||
|
const visible = document.visibilityState === 'visible'
|
||||||
|
if (!visible) {
|
||||||
|
// if we just sent, no need to save anything
|
||||||
|
if (!window.justSubmitted) {
|
||||||
|
saveState(getUiState())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
11
extension/src/manifest.json
Normal file
11
extension/src/manifest.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "<webpack managed>",
|
||||||
|
"version": "<webpack managed>",
|
||||||
|
"options_ui": {
|
||||||
|
"page": "options.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"128": "img/unicorn.png"
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<script type="text/javascript" src="webextension-polyfill.js"></script>
|
||||||
<script type="text/javascript" src="options_page.js"/></script>
|
<script type="text/javascript" src="options_page.js"/></script>
|
||||||
<style>
|
<style>
|
||||||
input:invalid {
|
input:invalid {
|
|
@ -2,6 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<script type="text/javascript" src="webextension-polyfill.js"></script>
|
||||||
<script type="text/javascript" src="popup.js"></script>
|
<script type="text/javascript" src="popup.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
241
extension/webpack.config.js
Normal file
241
extension/webpack.config.js
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
const webpack = require('webpack'),
|
||||||
|
path = require('path'),
|
||||||
|
{CleanWebpackPlugin} = require('clean-webpack-plugin'),
|
||||||
|
CopyWebpackPlugin = require('copy-webpack-plugin'),
|
||||||
|
WebpackExtensionManifestPlugin = require('webpack-extension-manifest-plugin');
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
CHROME : 'chrome',
|
||||||
|
FIREFOX: 'firefox',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// TODO will be conditional on T.CHROME at some point
|
||||||
|
const v3 = false
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
TARGET : process.env.TARGET,
|
||||||
|
RELEASE: process.env.RELEASE,
|
||||||
|
PUBLISH: process.env.PUBLISH,
|
||||||
|
}
|
||||||
|
const ext_id = process.env.EXT_ID
|
||||||
|
|
||||||
|
const pkg = require('./package.json');
|
||||||
|
const baseManifest = require('./src/manifest.json');
|
||||||
|
|
||||||
|
const release = env.RELEASE == 'YES' ? true : false;
|
||||||
|
const publish = env.PUBLISH == 'YES' ? true : false;
|
||||||
|
const dev = !release; // meh. maybe make up my mind?
|
||||||
|
const target = env.TARGET;
|
||||||
|
|
||||||
|
const name = "grasp" + (dev ? ' [dev]' : '');
|
||||||
|
|
||||||
|
|
||||||
|
// ugh. declarative formats are shit.
|
||||||
|
|
||||||
|
// Firefox wouldn't let you rebind its default shortcuts most of which use Shift
|
||||||
|
// On the other hand, Chrome wouldn't let you use Alt
|
||||||
|
const modifier = target === T.CHROME ? 'Shift' : 'Alt'
|
||||||
|
|
||||||
|
const action_name = v3 ? 'action' : 'browser_action'
|
||||||
|
|
||||||
|
const commands = {
|
||||||
|
"capture-simple": {
|
||||||
|
"description": "Quick capture: url, title and selection",
|
||||||
|
"suggested_key": {
|
||||||
|
"default": `Ctrl+${modifier}+C`,
|
||||||
|
"mac": `Command+${modifier}+C`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commands[`_execute_${action_name}`] = {
|
||||||
|
"description": "Capture page, with extra information",
|
||||||
|
"suggested_key": {
|
||||||
|
"default": `Ctrl+${modifier}+Y`,
|
||||||
|
"mac": `Command+${modifier}+Y`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const action = {
|
||||||
|
"default_icon": "img/unicorn.png",
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_title": "Capture page, with extra information",
|
||||||
|
}
|
||||||
|
if (target == T.FIREFOX) {
|
||||||
|
action['browser_style'] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const endpoints = (domain) => [
|
||||||
|
"http://" + domain + "/capture",
|
||||||
|
"https://" + domain + "/capture",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
// prepare for manifest v3
|
||||||
|
const host_permissions = endpoints('localhost')
|
||||||
|
const optional_host_permissions = endpoints('*')
|
||||||
|
|
||||||
|
|
||||||
|
// TODO make permissions literate
|
||||||
|
const permissions = [
|
||||||
|
"storage",
|
||||||
|
"notifications",
|
||||||
|
|
||||||
|
// need to query active tab and get its url/title
|
||||||
|
"activeTab",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const optional_permissions = []
|
||||||
|
|
||||||
|
if (target === T.FIREFOX || v3) {
|
||||||
|
// chrome v2 doesn't support scripting api
|
||||||
|
// code has a fallback just for that
|
||||||
|
// (needed to get selected text)
|
||||||
|
permissions.push("scripting")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const content_security_policy = [
|
||||||
|
"default-src 'self'",
|
||||||
|
"connect-src " + endpoints('*:*').join(' '),
|
||||||
|
|
||||||
|
// FFS, otherwise <style> directives on extension's pages not working??
|
||||||
|
"style-src 'unsafe-inline'",
|
||||||
|
].join('; ')
|
||||||
|
|
||||||
|
|
||||||
|
const manifestExtra = {
|
||||||
|
name: name,
|
||||||
|
version: pkg.version,
|
||||||
|
description: pkg.description,
|
||||||
|
permissions: permissions,
|
||||||
|
commands: commands,
|
||||||
|
optional_permissions: optional_permissions,
|
||||||
|
manifest_version: v3 ? 3 : 2,
|
||||||
|
background: (v3 ? {
|
||||||
|
service_worker: 'background.js',
|
||||||
|
// wtf -- firefox doesn't support module??
|
||||||
|
// it just fails to load manifest in this case atm
|
||||||
|
...(target === T.FIREFOX? {} : {type: 'module'}),
|
||||||
|
} : {
|
||||||
|
scripts: [
|
||||||
|
'webextension-polyfill.js',
|
||||||
|
'background.js',
|
||||||
|
],
|
||||||
|
persistent: false,
|
||||||
|
}),
|
||||||
|
content_security_policy: v3 ? {
|
||||||
|
extension_pages: content_security_policy,
|
||||||
|
} : content_security_policy,
|
||||||
|
}
|
||||||
|
manifestExtra[action_name] = action
|
||||||
|
|
||||||
|
if (v3) {
|
||||||
|
manifestExtra['host_permissions'] = host_permissions
|
||||||
|
manifestExtra['optional_host_permissions'] = optional_host_permissions
|
||||||
|
if (target === T.FIREFOX) {
|
||||||
|
manifestExtra['browser_specific_settings'] = {
|
||||||
|
"gecko": {
|
||||||
|
"id": ext_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
manifestExtra.permissions.push(...host_permissions)
|
||||||
|
manifestExtra.optional_permissions.push(...optional_host_permissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (target === T.FIREFOX) {
|
||||||
|
manifestExtra.options_ui = {browser_style: true};
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildPath = path.join(__dirname, 'dist', target);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
mode: dev ? 'development' : 'production', // TODO map directly from MODE env var?
|
||||||
|
node: {
|
||||||
|
// no idea what does it mean... https://github.com/webpack/webpack/issues/5627#issuecomment-394290231
|
||||||
|
// but it does get rid of some Function() which webpacka apparently generates
|
||||||
|
global: false,
|
||||||
|
},
|
||||||
|
entry: {
|
||||||
|
background : path.join(__dirname, "src", "js", "background"),
|
||||||
|
popup : path.join(__dirname, "src", "js", "popup"),
|
||||||
|
options_page: path.join(__dirname, "src", "js", "options_page"),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: buildPath,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
// https://webpack.js.org/configuration/optimization
|
||||||
|
// don't think minimize worth it for such a tiny extension
|
||||||
|
minimize: false,
|
||||||
|
|
||||||
|
// split chunks is so vendors split out into separate js files
|
||||||
|
// to prevent bloating individual source files
|
||||||
|
// seems that at the moment we need to manually load the chunks in the corresponding
|
||||||
|
// html files or manifest
|
||||||
|
// split chunks doc recommend using webpack html plugin??
|
||||||
|
splitChunks: {
|
||||||
|
// seems necessary, otherwise doesn't split out polyfill??
|
||||||
|
chunks: 'all',
|
||||||
|
cacheGroups: {
|
||||||
|
vendor: {
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
name(module) {
|
||||||
|
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
|
||||||
|
return packageName
|
||||||
|
},
|
||||||
|
// create chunk regardless size etc
|
||||||
|
enforce: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: 'style-loader!css-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.html$/,
|
||||||
|
loader: 'html-loader',
|
||||||
|
exclude: /node_modules/
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CleanWebpackPlugin(), // ok, respects symlinks
|
||||||
|
|
||||||
|
// without copy pluging, webpack only bundles js/json files
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{ context: 'src', from: '**/*.html' },
|
||||||
|
{ context: 'src', from: '**/*.png' },
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
new WebpackExtensionManifestPlugin({
|
||||||
|
config: {
|
||||||
|
base: baseManifest,
|
||||||
|
extend: manifestExtra,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
// docs claim it's the slowest but pretty fast anyway
|
||||||
|
devtool: 'source-map',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = options;
|
10
mypy.ini
Normal file
10
mypy.ini
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[mypy]
|
||||||
|
pretty = True
|
||||||
|
show_error_context = True
|
||||||
|
show_error_codes = True
|
||||||
|
check_untyped_defs = True
|
||||||
|
namespace_packages = True
|
||||||
|
|
||||||
|
# an example of suppressing
|
||||||
|
# [mypy-my.config.repos.pdfannots.pdfannots]
|
||||||
|
# ignore_errors = True
|
33
package.json
33
package.json
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"name": "grasp",
|
|
||||||
"version": "0.6.6",
|
|
||||||
"description": "Reliably capture links and webpage content in a plaintext file (org-mode/markdown)",
|
|
||||||
"main": "dist/background.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack --progress --profile --colors",
|
|
||||||
"watch": "webpack --watch --progress --colors",
|
|
||||||
"dev": "npm run watch",
|
|
||||||
"eslint": "eslint src",
|
|
||||||
"flow": "flow"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.2.2",
|
|
||||||
"@babel/preset-env": "^7.2.3",
|
|
||||||
"@babel/preset-flow": "^7.0.0",
|
|
||||||
"babel-loader": "^8.0.5",
|
|
||||||
"babel-eslint": "^10.0.1",
|
|
||||||
"clean-webpack-plugin": "^1.0.0",
|
|
||||||
"copy-webpack-plugin": "^4.2.0",
|
|
||||||
"css-loader": "^2.1.0",
|
|
||||||
"eslint": "^5.12.0",
|
|
||||||
"eslint-config-google": "^0.11.0",
|
|
||||||
"eslint-plugin-flowtype": "^3.2.0",
|
|
||||||
"flow-bin": "^0.89.0",
|
|
||||||
"flow-interfaces-chrome": "^0.5.3",
|
|
||||||
"style-loader": "^1.0.0",
|
|
||||||
"webpack": "^4.28.3",
|
|
||||||
"webpack-cli": "^3.2.0",
|
|
||||||
"webpack-dev-server": "^3.1.14",
|
|
||||||
"webpack-extension-manifest-plugin": "^0.4.3"
|
|
||||||
}
|
|
||||||
}
|
|
22
publish
22
publish
|
@ -1,22 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -eux
|
|
||||||
|
|
||||||
|
|
||||||
VERSION="$(git tag --points-at)"
|
|
||||||
if [[ ! -n $VERSION ]]; then
|
|
||||||
>&2 echo "Dirty build!"
|
|
||||||
VERSION="$(git rev-parse --short HEAD)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
function rebuild {
|
|
||||||
local res="$1"
|
|
||||||
local browser="$2"
|
|
||||||
./build --target "$browser" --release --lint
|
|
||||||
zip -r -j "$res" dist/*
|
|
||||||
unzip -l "$res"
|
|
||||||
}
|
|
||||||
|
|
||||||
for browser in "chrome" "firefox"; do
|
|
||||||
rebuild "grasp-$VERSION-$browser.zip" "$browser"
|
|
||||||
done
|
|
13
pytest.ini
Normal file
13
pytest.ini
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[pytest]
|
||||||
|
# discover files that don't follow test_ naming. Useful to keep tests along with the source code
|
||||||
|
python_files = *.py
|
||||||
|
addopts =
|
||||||
|
# -rap to print tests summary even when they are successful
|
||||||
|
-rap
|
||||||
|
--verbose
|
||||||
|
|
||||||
|
# otherwise it won't discover doctests
|
||||||
|
--doctest-modules
|
||||||
|
|
||||||
|
# show all test durations (unless they are too short)
|
||||||
|
--durations=0
|
|
@ -23,11 +23,11 @@ def empty(s) -> bool:
|
||||||
# TODO put template in config??
|
# TODO put template in config??
|
||||||
class Config:
|
class Config:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_selection(selection: str) -> List[str]:
|
def format_selection(selection: str) -> List[str]: # type: ignore[empty-body]
|
||||||
...
|
...
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_comment(comment: str) -> List[str]:
|
def format_comment(comment: str) -> List[str]: # type: ignore[empty-body]
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,6 +56,8 @@ def as_org(
|
||||||
tags: List[str],
|
tags: List[str],
|
||||||
org_template: str,
|
org_template: str,
|
||||||
config: Optional[Config] = None,
|
config: Optional[Config] = None,
|
||||||
|
*,
|
||||||
|
_now=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Formats captured results according to org template. Supports all sensible (e.g. non-interactive) template expansions from https://orgmode.org/manual/Template-expansion.html#Template-expansion.
|
Formats captured results according to org template. Supports all sensible (e.g. non-interactive) template expansions from https://orgmode.org/manual/Template-expansion.html#Template-expansion.
|
||||||
|
@ -64,10 +66,11 @@ Additionally, supports :selection, :comment and :tags expansions.
|
||||||
|
|
||||||
You can look at `test_templates` for some specific examples.
|
You can look at `test_templates` for some specific examples.
|
||||||
"""
|
"""
|
||||||
# py_template = re.sub(r'%[:]?([?\w]+)', r'{\1}', org_template)
|
py_template = re.sub(r'([^\\])%[:]?([?\w]+)', r'\1{\2}', org_template)
|
||||||
py_template = re.sub(r'[^\\]%[:]?([?\w]+)', r'{\1}', org_template)
|
# replace all escaped % with regular %, safe to do after substitution
|
||||||
|
py_template = py_template.replace(r'\%', '%')
|
||||||
|
|
||||||
NOW = datetime.now()
|
NOW = datetime.now() if _now is None else _now
|
||||||
org_date = date2org(NOW)
|
org_date = date2org(NOW)
|
||||||
org_datetime = datetime2org(NOW)
|
org_datetime = datetime2org(NOW)
|
||||||
|
|
||||||
|
@ -104,9 +107,8 @@ You can look at `test_templates` for some specific examples.
|
||||||
)
|
)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
# TODO escaping?
|
|
||||||
# just checks it's not crashing for now
|
def test_templates() -> None:
|
||||||
def test_templates():
|
|
||||||
url = 'https://whatever'
|
url = 'https://whatever'
|
||||||
title = 'hello'
|
title = 'hello'
|
||||||
selection = """some
|
selection = """some
|
||||||
|
@ -116,6 +118,7 @@ def test_templates():
|
||||||
comment = 'fafewfewf'
|
comment = 'fafewfewf'
|
||||||
tags = ['aba', 'caba']
|
tags = ['aba', 'caba']
|
||||||
|
|
||||||
|
vimzettel_template = r'zettel \%title [%:description] testing'
|
||||||
# https://orgmode.org/guide/Capture-templates.html
|
# https://orgmode.org/guide/Capture-templates.html
|
||||||
org_templates = [
|
org_templates = [
|
||||||
DEFAULT_TEMPLATE,
|
DEFAULT_TEMPLATE,
|
||||||
|
@ -135,7 +138,9 @@ def test_templates():
|
||||||
%:selection
|
%:selection
|
||||||
#+END_QUOTE
|
#+END_QUOTE
|
||||||
""",
|
""",
|
||||||
|
vimzettel_template,
|
||||||
]
|
]
|
||||||
|
# check they aren't crashing
|
||||||
for org_template in org_templates:
|
for org_template in org_templates:
|
||||||
res = as_org(
|
res = as_org(
|
||||||
url,
|
url,
|
||||||
|
@ -146,3 +151,39 @@ def test_templates():
|
||||||
org_template=org_template
|
org_template=org_template
|
||||||
|
|
||||||
)
|
)
|
||||||
|
<<<<<<< HEAD
|
||||||
|
=======
|
||||||
|
print()
|
||||||
|
print(res)
|
||||||
|
res = as_org(
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
selection,
|
||||||
|
comment,
|
||||||
|
tags,
|
||||||
|
org_template=DEFAULT_TEMPLATE,
|
||||||
|
_now=datetime(1111, 11, 11),
|
||||||
|
)
|
||||||
|
expected = '''
|
||||||
|
* [1111-11-11 Sat 00:00] hello :aba:caba:
|
||||||
|
https://whatever
|
||||||
|
Selection:
|
||||||
|
some
|
||||||
|
selected
|
||||||
|
text
|
||||||
|
|
||||||
|
Comment:
|
||||||
|
fafewfewf
|
||||||
|
'''.lstrip()
|
||||||
|
assert res == expected
|
||||||
|
|
||||||
|
res = as_org(
|
||||||
|
url,
|
||||||
|
title,
|
||||||
|
selection,
|
||||||
|
comment,
|
||||||
|
tags,
|
||||||
|
org_template=vimzettel_template,
|
||||||
|
)
|
||||||
|
assert res == 'zettel %title [hello] testing'
|
||||||
|
>>>>>>> upstream/master
|
||||||
|
|
113
server/setup
113
server/setup
|
@ -1,12 +1,18 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import shlex
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import check_call, run
|
import platform
|
||||||
|
import shlex
|
||||||
|
from subprocess import check_call, run, DEVNULL
|
||||||
|
|
||||||
from grasp_server import setup_parser
|
from grasp_server import setup_parser
|
||||||
|
|
||||||
SYSTEMD_CONFIG = """
|
|
||||||
|
SYSTEM = platform.system()
|
||||||
|
|
||||||
|
|
||||||
|
SYSTEMD_TEMPLATE = """
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Grasp extension server conterpart
|
Description=Grasp extension server conterpart
|
||||||
|
|
||||||
|
@ -17,7 +23,29 @@ WantedBy=default.target
|
||||||
ExecStart={server} {extra_args}
|
ExecStart={server} {extra_args}
|
||||||
Type=simple
|
Type=simple
|
||||||
Restart=always
|
Restart=always
|
||||||
"""
|
""".lstrip()
|
||||||
|
|
||||||
|
|
||||||
|
LAUNCHD_TEMPLATE = '''
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>{service_name}</string>
|
||||||
|
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
{arguments}
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
'''.lstrip()
|
||||||
|
|
||||||
|
|
||||||
def systemd(*args, method=check_call):
|
def systemd(*args, method=check_call):
|
||||||
|
@ -25,31 +53,36 @@ def systemd(*args, method=check_call):
|
||||||
'systemctl', '--user', *args,
|
'systemctl', '--user', *args,
|
||||||
])
|
])
|
||||||
|
|
||||||
def setup(args):
|
|
||||||
|
def get_command(args) -> list[str]:
|
||||||
|
return [
|
||||||
|
'--port', args.port,
|
||||||
|
'--path', args.path,
|
||||||
|
'--template', args.template,
|
||||||
|
*([] if args.config is None else ['--config' , args.config]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def setup_systemd(args) -> None:
|
||||||
|
args.template = args.template.replace('%', '%%').replace('\n', r'\n') # escape for systemd...
|
||||||
|
args.unit_name = args.unit_name.removesuffix('.service') # legacy unit_name default had .service
|
||||||
|
|
||||||
base_path = '~/.config/systemd/user'
|
base_path = '~/.config/systemd/user'
|
||||||
if not Path(base_path).expanduser().is_dir():
|
if not Path(base_path).expanduser().is_dir():
|
||||||
Path(base_path).expanduser().mkdir(parents=True, exist_ok=True)
|
Path(base_path).expanduser().mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
unit_name = args.unit_name
|
unit_name = args.unit_name
|
||||||
out = Path(f'{base_path}/{unit_name}').expanduser()
|
out = Path(f'{base_path}/{unit_name}.service').expanduser()
|
||||||
print(f"Writing systemd config to {out}:")
|
print(f"Writing unit file to {out}")
|
||||||
|
|
||||||
server_bin = Path(__file__).parent.joinpath('grasp_server.py').absolute()
|
server_bin = Path(__file__).parent.joinpath('grasp_server.py').absolute()
|
||||||
|
command = get_command(args)
|
||||||
|
extra_args = ' '.join(map(shlex.quote, command))
|
||||||
|
|
||||||
template = args.template.replace('%', '%%').replace('\n', r'\n') # escape for systemd...
|
out.write_text(SYSTEMD_TEMPLATE.format(
|
||||||
args = [
|
|
||||||
'--port', args.port,
|
|
||||||
'--path', args.path,
|
|
||||||
'--template', template,
|
|
||||||
*([] if args.config is None else ['--config' , args.config]),
|
|
||||||
]
|
|
||||||
extra_args = ' '.join(map(shlex.quote, args))
|
|
||||||
|
|
||||||
with out.open('w') as fo:
|
|
||||||
fo.write(SYSTEMD_CONFIG.format(
|
|
||||||
server=server_bin,
|
server=server_bin,
|
||||||
extra_args=extra_args,
|
extra_args=extra_args,
|
||||||
))
|
))
|
||||||
try:
|
try:
|
||||||
systemd('stop' , unit_name, method=run) # ignore errors here if it wasn't running in the first place
|
systemd('stop' , unit_name, method=run) # ignore errors here if it wasn't running in the first place
|
||||||
systemd('daemon-reload')
|
systemd('daemon-reload')
|
||||||
|
@ -60,12 +93,48 @@ def setup(args):
|
||||||
print(f"Something has gone wrong... you might want to use 'journalctl --user -u {unit_name}' to debug")
|
print(f"Something has gone wrong... you might want to use 'journalctl --user -u {unit_name}' to debug")
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def main():
|
|
||||||
|
def setup_launchd(args) -> None:
|
||||||
|
# launchd keep everything as xml and treats whitespace literally
|
||||||
|
args.template = args.template.replace(r'\n', '\n')
|
||||||
|
|
||||||
|
base_path = Path('~/Library/LaunchAgents')
|
||||||
|
|
||||||
|
unit_name = args.unit_name
|
||||||
|
out = Path(f'{base_path}/{unit_name}.plist').expanduser()
|
||||||
|
print(f"Writing unit file to {out}")
|
||||||
|
|
||||||
|
server_bin = Path(__file__).parent.joinpath('grasp_server.py').absolute()
|
||||||
|
command = get_command(args)
|
||||||
|
|
||||||
|
|
||||||
|
command = [str(server_bin)] + command
|
||||||
|
arguments = '\n'.join(f'<string>{a}</string>' for a in command)
|
||||||
|
|
||||||
|
out.write_text(LAUNCHD_TEMPLATE.format(
|
||||||
|
service_name=unit_name,
|
||||||
|
arguments=arguments,
|
||||||
|
))
|
||||||
|
|
||||||
|
DOMAIN = f'gui/{os.getuid()}'
|
||||||
|
# defensive since might not have been running
|
||||||
|
run(['launchctl', 'bootout', DOMAIN + '/' + unit_name], stdout=DEVNULL, stderr=DEVNULL)
|
||||||
|
check_call(['launchctl', 'bootstrap', DOMAIN, out])
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
default_unit_name = 'grasp' if SYSTEM == 'Linux' else 'com.github.karlicoss.grasp'
|
||||||
|
|
||||||
p = argparse.ArgumentParser('grasp server setup', formatter_class=lambda prog: argparse.ArgumentDefaultsHelpFormatter(prog, width=100)) # type: ignore
|
p = argparse.ArgumentParser('grasp server setup', formatter_class=lambda prog: argparse.ArgumentDefaultsHelpFormatter(prog, width=100)) # type: ignore
|
||||||
p.add_argument('--unit-name', type=str, default='grasp.service', help='Systemd unit name')
|
p.add_argument('--unit-name', type=str, default=default_unit_name, help='systemd/launchd unit name')
|
||||||
setup_parser(p)
|
setup_parser(p)
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
setup(args)
|
if SYSTEM == 'Linux':
|
||||||
|
setup_systemd(args)
|
||||||
|
else: # assume macos
|
||||||
|
setup_launchd(args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -4,16 +4,10 @@ from subprocess import Popen, check_call
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
|
|
||||||
# ugh.
|
import requests
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def killmepls(*args, **kwargs):
|
|
||||||
with Popen(*args, **kwargs) as p:
|
|
||||||
try:
|
|
||||||
yield p
|
|
||||||
finally:
|
|
||||||
p.kill()
|
|
||||||
|
|
||||||
|
|
||||||
def grasp_test_server(capture_file: Path, port: str, template=None):
|
def grasp_test_server(capture_file: Path, port: str, template=None):
|
||||||
server = str((Path(__file__).parent / 'grasp_server.py').absolute())
|
server = str((Path(__file__).parent / 'grasp_server.py').absolute())
|
||||||
cmdline = [
|
cmdline = [
|
||||||
|
@ -21,23 +15,29 @@ def grasp_test_server(capture_file: Path, port: str, template=None):
|
||||||
]
|
]
|
||||||
if template is not None:
|
if template is not None:
|
||||||
cmdline.extend(['--template', template])
|
cmdline.extend(['--template', template])
|
||||||
return killmepls(cmdline)
|
with Popen(cmdline) as p:
|
||||||
|
try:
|
||||||
|
yield p
|
||||||
|
finally:
|
||||||
|
p.kill()
|
||||||
|
|
||||||
|
|
||||||
def test_server(tmp_path: Path):
|
def test_server(tmp_path: Path) -> None:
|
||||||
cfile = tmp_path / 'test-capture.org'
|
cfile = tmp_path / 'test-capture.org'
|
||||||
PORT = '17890'
|
PORT = '17890'
|
||||||
|
|
||||||
def send(url: str, title: str):
|
def send(url: str, title: str):
|
||||||
# TODO eh, not sure if there is anything easier than httpie
|
r = requests.post(
|
||||||
check_call([
|
f'http://localhost:{PORT}',
|
||||||
'http', '--ignore-stdin', 'POST', f'http://localhost:{PORT}',
|
json = {
|
||||||
f'url={url}',
|
'url': url,
|
||||||
f'title={title}',
|
'title': title,
|
||||||
'selection=null',
|
'selection': None,
|
||||||
'comment=null',
|
'comment': None,
|
||||||
'tag_str=null',
|
'tag_str': None,
|
||||||
])
|
},
|
||||||
|
)
|
||||||
|
assert r.status_code == 200, r
|
||||||
|
|
||||||
|
|
||||||
with grasp_test_server(capture_file=cfile, port=PORT):
|
with grasp_test_server(capture_file=cfile, port=PORT):
|
||||||
|
|
57
setup.py
Normal file
57
setup.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
# see https://github.com/karlicoss/pymplate for up-to-date reference
|
||||||
|
|
||||||
|
|
||||||
|
from setuptools import setup, find_namespace_packages # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
# works with both ordinary and namespace packages
|
||||||
|
#pkgs = find_namespace_packages('src')
|
||||||
|
pkgs = ['server']
|
||||||
|
pkg = min(pkgs) # lexicographically smallest is the correct one usually?
|
||||||
|
setup(
|
||||||
|
name=pkg,
|
||||||
|
use_scm_version={
|
||||||
|
'version_scheme': 'python-simplified-semver',
|
||||||
|
'local_scheme': 'dirty-tag',
|
||||||
|
},
|
||||||
|
setup_requires=['setuptools_scm'],
|
||||||
|
|
||||||
|
# otherwise mypy won't work
|
||||||
|
# https://mypy.readthedocs.io/en/stable/installed_packages.html#making-pep-561-compatible-packages
|
||||||
|
zip_safe=False,
|
||||||
|
|
||||||
|
packages=pkgs,
|
||||||
|
#package_dir={'': 'src'},
|
||||||
|
# necessary so that package works with mypy
|
||||||
|
package_data={pkg: ['py.typed']},
|
||||||
|
|
||||||
|
## ^^^ this should be mostly automatic and not requiring any changes
|
||||||
|
|
||||||
|
install_requires=[
|
||||||
|
# vvv example of git repo dependency
|
||||||
|
# 'repo @ git+https://github.com/karlicoss/repo.git',
|
||||||
|
|
||||||
|
# vvv example of local file dependency. yes, DUMMY is necessary for some reason
|
||||||
|
# 'repo @ git+file://DUMMY/path/to/repo',
|
||||||
|
],
|
||||||
|
extras_require={
|
||||||
|
'testing': ['pytest', 'requests'],
|
||||||
|
'linting': ['pytest', 'mypy', 'lxml'], # lxml for mypy coverage report
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
# this needs to be set if you're planning to upload to pypi
|
||||||
|
# url='',
|
||||||
|
# author='',
|
||||||
|
# author_email='',
|
||||||
|
# description='',
|
||||||
|
|
||||||
|
# Rest of the stuff -- classifiers, license, etc, I don't think it matters for pypi
|
||||||
|
# it's just unnecessary duplication
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.8 KiB |
|
@ -1,124 +0,0 @@
|
||||||
/* @flow */
|
|
||||||
// TODO do I really need to annotate all files with @flow??
|
|
||||||
|
|
||||||
import {COMMAND_CAPTURE_SIMPLE, METHOD_CAPTURE_WITH_EXTRAS, showNotification} from './common';
|
|
||||||
import {get_options} from './options';
|
|
||||||
|
|
||||||
type Params = {
|
|
||||||
url: string,
|
|
||||||
title: ?string,
|
|
||||||
selection: ?string,
|
|
||||||
comment: ?string,
|
|
||||||
tag_str: ?string,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function makeCaptureRequest(
|
|
||||||
params: Params,
|
|
||||||
options,
|
|
||||||
) {
|
|
||||||
if (params.tag_str == null) {
|
|
||||||
params.tag_str = options.default_tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = JSON.stringify(params);
|
|
||||||
console.log(`[background] capturing ${data}`);
|
|
||||||
|
|
||||||
var request = new XMLHttpRequest();
|
|
||||||
const curl = options.endpoint;
|
|
||||||
|
|
||||||
request.timeout = 10 * 1000; // TODO should it be configurable?
|
|
||||||
request.open('POST', curl, true);
|
|
||||||
request.onreadystatechange = () => {
|
|
||||||
const XHR_DONE = 4;
|
|
||||||
if (request.readyState != XHR_DONE) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const status = request.status;
|
|
||||||
const rtext = request.responseText;
|
|
||||||
var had_error = false;
|
|
||||||
var error_message = `status ${status}, response ${rtext}`;
|
|
||||||
console.log(`[background] status: ${status}, response: ${rtext}`);
|
|
||||||
if (status >= 200 && status < 400) { // success
|
|
||||||
try {
|
|
||||||
// TODO handle json parsing defensively here
|
|
||||||
const response = JSON.parse(rtext);
|
|
||||||
const path = response.path;
|
|
||||||
console.log(`[background] success: ${rtext}`);
|
|
||||||
if (options.notification) {
|
|
||||||
showNotification(`OK: captured to ${path}`);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
had_error = true;
|
|
||||||
error_message = error_message.concat(String(err));
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
had_error = true;
|
|
||||||
if (status == 0) {
|
|
||||||
error_message = error_message.concat(` ${curl} must be unavailable `);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (had_error) {
|
|
||||||
console.error(`[background] ERROR: ${error_message}`);
|
|
||||||
showNotification(`ERROR: ${error_message}`, 1);
|
|
||||||
// TODO crap, doesn't really seem to respect urgency...
|
|
||||||
}
|
|
||||||
};
|
|
||||||
request.onerror = () => {
|
|
||||||
console.error(request);
|
|
||||||
};
|
|
||||||
|
|
||||||
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
|
|
||||||
request.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TODO FIXME ugh. need defensive error handling on the very top...
|
|
||||||
function capture(comment: ?string = null, tag_str: ?string = null) {
|
|
||||||
chrome.tabs.query({currentWindow: true, active: true }, tabs => {
|
|
||||||
const tab = tabs[0];
|
|
||||||
if (tab.url == null) {
|
|
||||||
showNotification('ERROR: trying to capture null');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const url: string = tab.url;
|
|
||||||
const title: ?string = tab.title;
|
|
||||||
|
|
||||||
get_options(opts => {
|
|
||||||
// console.log('action!');
|
|
||||||
// ugh.. https://stackoverflow.com/a/19165930/706389
|
|
||||||
chrome.tabs.executeScript( {
|
|
||||||
code: "window.getSelection().toString();"
|
|
||||||
}, selections => {
|
|
||||||
const selection = selections == null ? null : selections[0];
|
|
||||||
makeCaptureRequest({
|
|
||||||
url: url,
|
|
||||||
title: title,
|
|
||||||
selection: selection,
|
|
||||||
comment: comment,
|
|
||||||
tag_str: tag_str,
|
|
||||||
}, opts);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
chrome.commands.onCommand.addListener(command => {
|
|
||||||
if (command === COMMAND_CAPTURE_SIMPLE) {
|
|
||||||
capture(null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message: any, sender: chrome$MessageSender, sendResponse) => { // eslint-disable-line no-unused-vars
|
|
||||||
if (message.method === METHOD_CAPTURE_WITH_EXTRAS) {
|
|
||||||
const comment = message.comment;
|
|
||||||
const tag_str = message.tag_str;
|
|
||||||
capture(comment, tag_str);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO handle cannot access chrome:// url??
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
/* @flow */
|
|
||||||
|
|
||||||
type Options = {
|
|
||||||
endpoint: string;
|
|
||||||
default_tags: string;
|
|
||||||
notification: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function default_options(): Options {
|
|
||||||
return {
|
|
||||||
endpoint: "http://localhost:12212/capture",
|
|
||||||
default_tags: "grasp",
|
|
||||||
notification: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function get_options(cb: (Options) => void) {
|
|
||||||
chrome.storage.local.get(null, res => {
|
|
||||||
res = {...default_options(), ...res};
|
|
||||||
cb(res);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function set_options(opts: Options, cb: () => void) {
|
|
||||||
console.log('Saving %s', JSON.stringify(opts));
|
|
||||||
chrome.storage.local.set(opts, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function getOptions(): Promise<Options> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// TODO FIXME reject?
|
|
||||||
get_options(resolve);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
/* @flow */
|
|
||||||
import {get_options, set_options} from './options';
|
|
||||||
import {ensurePermissions, hasPermissions} from './permissions';
|
|
||||||
|
|
||||||
const ENDPOINT_ID = 'endpoint_id';
|
|
||||||
const HAS_PERMISSION_ID = 'has_permission_id';
|
|
||||||
const NOTIFICATION_ID = 'notification_id';
|
|
||||||
const DEFAULT_TAGS_ID = 'default_tags_id';
|
|
||||||
// TODO specify capture path here?
|
|
||||||
|
|
||||||
const SAVE_ID = 'save_id';
|
|
||||||
|
|
||||||
function getEndpoint(): HTMLInputElement {
|
|
||||||
return ((document.getElementById(ENDPOINT_ID): any): HTMLInputElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultTags(): HTMLInputElement {
|
|
||||||
return ((document.getElementById(DEFAULT_TAGS_ID): any): HTMLInputElement);
|
|
||||||
// TODO if empty, return null?
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEnableNotification(): HTMLInputElement {
|
|
||||||
return ((document.getElementById(NOTIFICATION_ID): any): HTMLInputElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSaveButton(): HTMLInputElement {
|
|
||||||
return ((document.getElementById(SAVE_ID): any): HTMLInputElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHasPermission(): HTMLElement {
|
|
||||||
return ((document.getElementById(HAS_PERMISSION_ID): any): HTMLElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ugh. needs a better name..
|
|
||||||
function refreshPermissionValidation(endpoint: string) {
|
|
||||||
hasPermissions(endpoint).then(has => {
|
|
||||||
const pstyle = getHasPermission().style;
|
|
||||||
if (has) {
|
|
||||||
console.debug('Got permisssions, nothing to worry about.');
|
|
||||||
pstyle.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.debug('Whoops, no permissions to access %s', endpoint);
|
|
||||||
// TODO maybe just show button? but then it would need to be reactive..
|
|
||||||
pstyle.display = 'block';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreOptions() {
|
|
||||||
getSaveButton().addEventListener('click', saveOptions);
|
|
||||||
get_options(opts => {
|
|
||||||
const ep = opts.endpoint;
|
|
||||||
getEndpoint().value = ep;
|
|
||||||
getDefaultTags().value = opts.default_tags;
|
|
||||||
getEnableNotification().checked = opts.notification;
|
|
||||||
refreshPermissionValidation(ep);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveOptions() {
|
|
||||||
// TODO could also check for permissions and display message?
|
|
||||||
|
|
||||||
const endpoint = getEndpoint().value;
|
|
||||||
ensurePermissions(endpoint).finally(() => {
|
|
||||||
refreshPermissionValidation(endpoint);
|
|
||||||
});
|
|
||||||
|
|
||||||
const opts = {
|
|
||||||
endpoint: endpoint,
|
|
||||||
default_tags: getDefaultTags().value,
|
|
||||||
notification: getEnableNotification().checked,
|
|
||||||
};
|
|
||||||
set_options(opts, () => { alert('Saved!'); });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', restoreOptions);
|
|
120
src/js/popup.js
120
src/js/popup.js
|
@ -1,120 +0,0 @@
|
||||||
/* @flow */
|
|
||||||
|
|
||||||
import {METHOD_CAPTURE_WITH_EXTRAS} from './common';
|
|
||||||
import {get_options} from './options';
|
|
||||||
|
|
||||||
// TODO template it in html too?
|
|
||||||
const BUTTON_ID = 'button_id';
|
|
||||||
const COMMENT_ID = 'comment_id';
|
|
||||||
const TAGS_ID = 'tags_id';
|
|
||||||
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
comment: string,
|
|
||||||
tag_str: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function save_state(state: ?State) {
|
|
||||||
window.localStorage.setItem('state', JSON.stringify(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
function load_state(): ?State {
|
|
||||||
const sts = window.localStorage.getItem('state') || null;
|
|
||||||
return JSON.parse(sts);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getComment(): HTMLInputElement {
|
|
||||||
const comment = ((document.getElementById(COMMENT_ID): any): HTMLInputElement);
|
|
||||||
return comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTags(): HTMLInputElement {
|
|
||||||
const tags = ((document.getElementById(TAGS_ID): any): HTMLInputElement);
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getButton(): HTMLElement {
|
|
||||||
const button = ((document.getElementById(BUTTON_ID): any): HTMLElement);
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getState(): State {
|
|
||||||
const comment_text = getComment().value;
|
|
||||||
const tag_str = getTags().value;
|
|
||||||
return {
|
|
||||||
'comment': comment_text,
|
|
||||||
'tag_str': tag_str,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreState(state: ?State) {
|
|
||||||
if (state == null) {
|
|
||||||
// comment just relies on default
|
|
||||||
get_options(opts => {getTags().value = opts.default_tags;});
|
|
||||||
} else {
|
|
||||||
getComment().value = state.comment;
|
|
||||||
getTags().value = state.tag_str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitComment () {
|
|
||||||
// TODO focus
|
|
||||||
const state = getState();
|
|
||||||
|
|
||||||
chrome.runtime.sendMessage({
|
|
||||||
'method': METHOD_CAPTURE_WITH_EXTRAS,
|
|
||||||
...state,
|
|
||||||
}, () => {
|
|
||||||
console.log("[popup] captured!");
|
|
||||||
});
|
|
||||||
window.submitted = true;
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
function ctrlEnterSubmit(e) {
|
|
||||||
if (e.ctrlKey && e.key === 'Enter') {
|
|
||||||
submitComment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/6003829/706389
|
|
||||||
function moveCaretToEnd(el) {
|
|
||||||
if (typeof el.selectionStart == "number") {
|
|
||||||
el.selectionStart = el.selectionEnd = el.value.length;
|
|
||||||
} else if (typeof el.createTextRange != "undefined") {
|
|
||||||
el.focus();
|
|
||||||
var range = el.createTextRange();
|
|
||||||
range.collapse(false);
|
|
||||||
range.select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupPage () {
|
|
||||||
const comment = getComment();
|
|
||||||
comment.focus();
|
|
||||||
comment.addEventListener('keydown', ctrlEnterSubmit);
|
|
||||||
|
|
||||||
const tags = getTags();
|
|
||||||
tags.addEventListener('keydown', ctrlEnterSubmit);
|
|
||||||
tags.addEventListener('focus', () => moveCaretToEnd(tags)); // to put cursor to the end of tags when tabbed
|
|
||||||
|
|
||||||
getButton().addEventListener('click', submitComment);
|
|
||||||
|
|
||||||
window.submitted = false;
|
|
||||||
|
|
||||||
const state = load_state();
|
|
||||||
restoreState(state);
|
|
||||||
save_state(null); // clean
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', setupPage);
|
|
||||||
|
|
||||||
|
|
||||||
window.addEventListener('unload', () => {
|
|
||||||
if (!window.submitted) {
|
|
||||||
save_state(getState());
|
|
||||||
}
|
|
||||||
}, true);
|
|
|
@ -1,28 +0,0 @@
|
||||||
{
|
|
||||||
"name": "<webpack managed>",
|
|
||||||
"version": "<webpack managed>",
|
|
||||||
"background": {
|
|
||||||
"scripts": ["background.js"],
|
|
||||||
"persistent": false
|
|
||||||
},
|
|
||||||
"options_ui": {
|
|
||||||
"page": "options.html"
|
|
||||||
},
|
|
||||||
"browser_action": {
|
|
||||||
"default_icon": "unicorn.png",
|
|
||||||
"default_popup": "popup.html",
|
|
||||||
"default_title": "Capture page, with extra information"
|
|
||||||
},
|
|
||||||
"commands": {
|
|
||||||
"capture-simple": {
|
|
||||||
"description": "Quick capture: url, title and selection"
|
|
||||||
},
|
|
||||||
"_execute_browser_action": {
|
|
||||||
"description": "Capture page, with extra information"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"icons": {
|
|
||||||
"128": "unicorn.png"
|
|
||||||
},
|
|
||||||
"manifest_version": 2
|
|
||||||
}
|
|
32
tox.ini
Normal file
32
tox.ini
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[tox]
|
||||||
|
minversion = 3.5
|
||||||
|
# relies on the correct version of Python installed
|
||||||
|
envlist = tests,mypy
|
||||||
|
# https://github.com/tox-dev/tox/issues/20#issuecomment-247788333
|
||||||
|
# hack to prevent .tox from crapping to the project directory
|
||||||
|
toxworkdir={env:TOXWORKDIR_BASE:}{toxinidir}/.tox
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
passenv =
|
||||||
|
# useful for tests to know they are running under ci
|
||||||
|
CI
|
||||||
|
CI_*
|
||||||
|
# respect user's cache dirs to prevent tox from crapping into project dir
|
||||||
|
MYPY_CACHE_DIR
|
||||||
|
PYTHONPYCACHEPREFIX
|
||||||
|
|
||||||
|
[testenv:tests]
|
||||||
|
commands =
|
||||||
|
pip install -e .[testing]
|
||||||
|
# posargs allow test filtering, e.g. tox ... -- -k test_name
|
||||||
|
python -m pytest server --ignore server/test_with_browser.py {posargs}
|
||||||
|
|
||||||
|
[testenv:mypy]
|
||||||
|
commands =
|
||||||
|
pip install -e .[linting]
|
||||||
|
python -m mypy --install-types --non-interactive \
|
||||||
|
server \
|
||||||
|
# txt report is a bit more convenient to view on CI
|
||||||
|
--txt-report .coverage.mypy \
|
||||||
|
--html-report .coverage.mypy \
|
||||||
|
{posargs}
|
|
@ -1,140 +0,0 @@
|
||||||
const webpack = require('webpack'),
|
|
||||||
path = require('path'),
|
|
||||||
CleanWebpackPlugin = require('clean-webpack-plugin'),
|
|
||||||
CopyWebpackPlugin = require('copy-webpack-plugin'),
|
|
||||||
WebpackExtensionManifestPlugin = require('webpack-extension-manifest-plugin');
|
|
||||||
// TODO remove plugins from package.json
|
|
||||||
|
|
||||||
const T = {
|
|
||||||
CHROME : 'chrome',
|
|
||||||
FIREFOX: 'firefox',
|
|
||||||
};
|
|
||||||
|
|
||||||
const env = {
|
|
||||||
TARGET : process.env.TARGET, // TODO assert not null?
|
|
||||||
RELEASE: process.env.RELEASE,
|
|
||||||
};
|
|
||||||
|
|
||||||
const pkg = require('./package.json');
|
|
||||||
const baseManifest = require('./src/manifest.json');
|
|
||||||
|
|
||||||
const release = env.RELEASE == 'YES' ? true : false;
|
|
||||||
const dev = !release; // meh. maybe make up my mind?
|
|
||||||
const target = env.TARGET;
|
|
||||||
|
|
||||||
const name = "grasp" + (dev ? ' [dev]' : '');
|
|
||||||
|
|
||||||
// Firefox wouldn't let you rebind its default shortcuts most of which use Shift
|
|
||||||
// On the other hand, Chrome wouldn't let you use Alt
|
|
||||||
const modifier = target === T.CHROME ? 'Shift' : 'Alt';
|
|
||||||
|
|
||||||
// ugh. declarative formats are shit.
|
|
||||||
const commandsExtra = {
|
|
||||||
"capture-simple": {
|
|
||||||
"suggested_key": {
|
|
||||||
"default": `Ctrl+${modifier}+C`,
|
|
||||||
"mac": `Command+${modifier}+C`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"_execute_browser_action": {
|
|
||||||
"suggested_key": {
|
|
||||||
"default": `Ctrl+${modifier}+Y`,
|
|
||||||
"mac": `Command+${modifier}+Y`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO make permissions literate
|
|
||||||
const permissions = [
|
|
||||||
"storage",
|
|
||||||
"notifications",
|
|
||||||
"activeTab",
|
|
||||||
"http://localhost/capture",
|
|
||||||
"https://localhost/capture"
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
const manifestExtra = {
|
|
||||||
name: name,
|
|
||||||
version: pkg.version,
|
|
||||||
description: pkg.description,
|
|
||||||
permissions: permissions,
|
|
||||||
commands: commandsExtra,
|
|
||||||
optional_permissions: [
|
|
||||||
"http://*/capture",
|
|
||||||
"https://*/capture",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (dev) {
|
|
||||||
manifestExtra.content_security_policy = "script-src 'self' 'unsafe-eval'; object-src 'self'";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target === T.CHROME) {
|
|
||||||
manifestExtra.options_ui = {chrome_style: true};
|
|
||||||
}
|
|
||||||
if (target === T.FIREFOX) {
|
|
||||||
manifestExtra.options_ui = {browser_style: true};
|
|
||||||
manifestExtra.browser_action = {browser_style: true};
|
|
||||||
}
|
|
||||||
|
|
||||||
const build_path = path.join(__dirname, "dist"); // TODO target??
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
mode: dev ? 'development' : 'production', // TODO map directly from MODE env var?
|
|
||||||
optimization: {
|
|
||||||
// https://webpack.js.org/configuration/optimization
|
|
||||||
// don't think minimize worth it for such a tiny extension
|
|
||||||
minimize: false
|
|
||||||
},
|
|
||||||
entry: {
|
|
||||||
background : path.join(__dirname, "src", "js", "background"),
|
|
||||||
popup : path.join(__dirname, "src", "js", "popup"),
|
|
||||||
options_page: path.join(__dirname, "src", "js", "options_page"),
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: build_path,
|
|
||||||
filename: "[name].js",
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/,
|
|
||||||
loader: "style-loader!css-loader", // TODO different from promnesia??
|
|
||||||
exclude: /node_modules/
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.html$/,
|
|
||||||
loader: "html-loader",
|
|
||||||
exclude: /node_modules/
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new CleanWebpackPlugin([build_path + "/*"]),
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{ from: 'src/img/*.png', flatten: true },
|
|
||||||
{ from: 'src/*.html' , flatten: true },
|
|
||||||
]),
|
|
||||||
new WebpackExtensionManifestPlugin({
|
|
||||||
config: {
|
|
||||||
base: baseManifest,
|
|
||||||
extend: manifestExtra,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO https://webpack.js.org/configuration/devtool
|
|
||||||
if (dev) {
|
|
||||||
options.devtool = "cheap-module-eval-source-map";
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = options;
|
|
Loading…
Reference in a new issue