Full Code of jarun/buku for AI

master f350cd6fb017 cached
135 files
1.9 MB
729.7k tokens
759 symbols
1 requests
Download .txt
Showing preview only (2,032K chars total). Download the full file or copy to clipboard to get everything.
Repository: jarun/buku
Branch: master
Commit: f350cd6fb017
Files: 135
Total size: 1.9 MB

Directory structure:
gitextract_q_0wh3lh/

├── .circleci/
│   └── config.yml
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       └── lock.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── CHANGELOG
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── auto-completion/
│   ├── bash/
│   │   └── buku-completion.bash
│   ├── fish/
│   │   └── buku.fish
│   └── zsh/
│       └── _buku
├── buku.1
├── buku.py
├── bukuserver/
│   ├── README.md
│   ├── __init__.py
│   ├── __main__.py
│   ├── api.py
│   ├── apidocs/
│   │   ├── bookmark/
│   │   │   ├── delete.yml
│   │   │   ├── get.yml
│   │   │   └── put.yml
│   │   ├── bookmark_range/
│   │   │   ├── delete.yml
│   │   │   ├── get.yml
│   │   │   └── put.yml
│   │   ├── bookmark_refresh/
│   │   │   └── post.yml
│   │   ├── bookmarks/
│   │   │   ├── delete.yml
│   │   │   ├── get.yml
│   │   │   └── post.yml
│   │   ├── bookmarks_refresh/
│   │   │   └── post.yml
│   │   ├── bookmarks_reorder/
│   │   │   └── post.yml
│   │   ├── bookmarks_search/
│   │   │   ├── delete.yml
│   │   │   └── get.yml
│   │   ├── fetch_data/
│   │   │   └── post.yml
│   │   ├── network_handle/
│   │   │   └── post.yml
│   │   ├── tag/
│   │   │   ├── delete.yml
│   │   │   ├── get.yml
│   │   │   └── put.yml
│   │   ├── tags/
│   │   │   └── get.yml
│   │   ├── template.yml
│   │   └── tiny_url/
│   │       └── get.yml
│   ├── bookmarklet.js
│   ├── filters.py
│   ├── forms.py
│   ├── middleware/
│   │   ├── __init__.py
│   │   └── flask_reverse_proxy_fix.py
│   ├── requirements.txt
│   ├── response.py
│   ├── server.py
│   ├── static/
│   │   └── bukuserver/
│   │       ├── css/
│   │       │   ├── bookmark.css
│   │       │   ├── list.css
│   │       │   └── modal.css
│   │       └── js/
│   │           ├── Chart.js
│   │           ├── bookmark.js
│   │           ├── buku_filter.js
│   │           ├── filters_fix.js
│   │           ├── last_page.js
│   │           └── order_filter.js
│   ├── templates/
│   │   └── bukuserver/
│   │       ├── bookmark_create.html
│   │       ├── bookmark_create_modal.html
│   │       ├── bookmark_details.html
│   │       ├── bookmark_details_modal.html
│   │       ├── bookmark_edit.html
│   │       ├── bookmark_edit_modal.html
│   │       ├── bookmarklet.url
│   │       ├── bookmarks_list.html
│   │       ├── home.html
│   │       ├── lib.html
│   │       ├── statistic.html
│   │       ├── tag_edit.html
│   │       └── tags_list.html
│   ├── translations/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   ├── babel.cfg
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── messages.mo
│   │   │       └── messages.po
│   │   ├── fr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── messages.mo
│   │   │       └── messages.po
│   │   ├── messages_custom.pot
│   │   └── ru/
│   │       └── LC_MESSAGES/
│   │           ├── messages.mo
│   │           └── messages.po
│   ├── util.py
│   └── views.py
├── bukuserver-runner/
│   ├── README.md
│   ├── buku-server-headless.desktop
│   ├── buku-server.desktop
│   └── buku-server.py
├── docker-compose/
│   └── docker-compose.yml
├── docs/
│   └── source/
│       ├── buku.rst
│       ├── bukuserver.rst
│       ├── conf.py
│       ├── index.rst
│       ├── modules.rst
│       └── tutorial_for_developer.md
├── mypy.ini
├── packagecore.yaml
├── pyproject.toml
├── requirements.txt
├── tests/
│   ├── .pylintrc
│   ├── __init__.py
│   ├── cassettes/
│   │   └── test_buku/
│   │       ├── test_fetch_data_with_url[http---example.com-exp_res1].yaml
│   │       ├── test_fetch_data_with_url[http---example.com-page1.txt-exp_res2].yaml
│   │       ├── test_fetch_data_with_url[http---www.vim.org-scripts-script.php~-exp_res7].yaml
│   │       └── test_fetch_data_with_url[https---www.google.ru-search~-exp_res6].yaml
│   ├── genbm.sh
│   ├── pytest.ini
│   ├── test_BukuCrypt.py
│   ├── test_ExtendedArgumentParser.py
│   ├── test_buku.py
│   ├── test_bukuDb/
│   │   ├── 25491522_res.yaml
│   │   ├── 25491522_res_nopt.yaml
│   │   ├── Bookmarks
│   │   ├── firefox_res.yaml
│   │   ├── firefox_res_nopt.yaml
│   │   └── places.sql
│   ├── test_bukuDb.py
│   ├── test_cli.py
│   ├── test_import_firefox_json.py
│   ├── test_requirements.py
│   ├── test_server.py
│   ├── test_views.py
│   ├── util.py
│   └── vcr_cassettes/
│       ├── test_browse_by_index.yaml
│       ├── test_delete_rec_range_and_delay_commit.yaml
│       ├── test_search_by_multiple_tags_search_all.yaml
│       ├── test_search_by_multiple_tags_search_any.yaml
│       └── test_search_by_tags_enforces_space_seprations_exclusion.yaml
├── tox.bat
└── tox.ini

================================================
FILE CONTENTS
================================================

================================================
FILE: .circleci/config.yml
================================================
version: 2.1

executors:
  docker-publisher:
    environment:
      IMAGE_NAME: bukuserver/bukuserver
    docker:
      - image: cimg/base:current

test-template: &test-template
  working_directory: ~/Buku
  environment:
    CI_FORCE_TEST: 1
  steps:
    - run: &init
        command: |
          apt update && apt install -y --no-install-recommends git
          pip install --upgrade pip
    - checkout
    - run: &deps
        command: |
          pip install -e . --group dev
    - run:
        command:
          python3 -m pytest ./tests/test_*.py --cov buku -vv --durations=0 -c ./tests/pytest.ini

lint-template: &lint-template
  working_directory: ~/Buku
  environment:
    CI_FORCE_TEST: 1
  steps:
    - run: *init
    - checkout
    - run: *deps
    - run:
        command: |
          python3 -m flake8
          echo buku | xargs pylint --rcfile tests/.pylintrc
          find . -iname "*.py" ! -path "./api/*" | xargs pylint --rcfile tests/.pylintrc

jobs:
  lint:
    docker:
      - image: python:3.13-slim
    <<: *lint-template

  py310:
    docker:
      - image: python:3.10-slim
    <<: *test-template

  py311:
    docker:
      - image: python:3.11-slim
    <<: *test-template

  py312:
    docker:
      - image: python:3.12-slim
    <<: *test-template

  py313:
    docker:
      - image: python:3.13-slim
    <<: *test-template

  py314:
    docker:
      - image: python:3.14-slim
    <<: *test-template

#  package-and-publish:
#    machine: true
#    working_directory: ~/Buku
#    steps:
#      - checkout
#      - run:
#          name: "package with packagecore"
#          command: |
#            # Use latest installed python3 from pyenv
#            export PYENV_VERSION="$(pyenv versions | grep -Po '\b3\.\d+\.\d+' | tail -1)"
#            pip install packagecore
#            packagecore -o ./dist/ ${CIRCLE_TAG#v}
#      - run:
#          name: "publish to GitHub"
#          command: |
#            go get github.com/tcnksm/ghr
#            ghr -t ${GITHUB_API_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -replace ${CIRCLE_TAG} ./dist/

  build-and-publish-docker-image:
    executor: docker-publisher
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: Login to Docker Hub
          command: 'echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin'
      - run:
          name: Create a builder instance
          command: docker buildx create --use
      - run:
          name: Build Docker image and publish it to Docker Hub
          command: |
            TAGS="--tag $IMAGE_NAME:latest"
            [ "$CIRCLE_TAG" ] && TAGS="--tag $IMAGE_NAME:$CIRCLE_TAG $TAGS"
            docker buildx build --platform=linux/amd64,linux/arm64 $TAGS --push .

workflows:
  version: 2.1

  CircleCI:
    jobs: &all-tests
      - lint
      - py310
      - py311
      - py312
      - py313
      - py314

  nightly:
    triggers:
      - schedule:
          cron: "0 0 * * 6"
          filters:
            branches:
              only:
                - master
    jobs: *all-tests

#  publish-github-release:
#    jobs:
#      - package-and-publish:
#          filters:
#            tags:
#              only: /^v.*/
#            branches:
#              ignore: /.*/

  publish-docker-image:
    jobs:
      - build-and-publish-docker-image:
          filters:
            tags:
              only: /^v.*/
            branches:
              ignore: /.*/


================================================
FILE: .github/FUNDING.yml
================================================
github: jarun


================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
#### Bug reports

Before opening an issue, please try to reproduce on [the latest development version](https://github.com/jarun/Buku#from-source) first. The bug you noticed might have already been fixed.

If the issue can be reproduced on master, then please make sure you provide the following:
- Debug logs using the `-z` option;
- Details of operating system, Python version used, terminal emulator and shell;
- `locale` output, if relevant. It's a good idea to set your locale to UFT-8. Please refer to [Buku #131](https://github.com/jarun/Buku/issues/30).

If we need more information and there is no communication from the bug reporter within 7 days from the date of request, we will close the issue. If you have relevant information, resume discussion any time.


#### Feature requests
Please consider contributing the feature back to `Buku` yourself. Feel free to discuss. We are more than happy to help.

--- PLEASE DELETE THIS LINE AND EVERYTHING ABOVE ---


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Did you visit the [PR guidelines](https://github.com/jarun/Buku/wiki/PR-guidelines)?

--- PLEASE DELETE THIS LINE AND EVERYTHING ABOVE ---


================================================
FILE: .github/workflows/lock.yml
================================================
name: 'Lock threads'

on:
  schedule:
    - cron: '0 0 * * *'
  workflow_dispatch:

permissions:
  issues: write
  pull-requests: write
  discussions: write

concurrency:
  group: lock-threads

jobs:
  lock:
    runs-on: ubuntu-latest
    steps:
      - uses: dessant/lock-threads@v5
        with:
          github-token: ${{ github.token }}
          issue-inactive-days: '30'
          issue-lock-reason: ''
          pr-inactive-days: '30'
          pr-lock-reason: ''


================================================
FILE: .gitignore
================================================
*.py[co]
*.sw[po]
.cache/
.coverage
.hypothesis
buku.egg-info
dist
build
.tox
.history
.vscode/launch.json
/tests/test_bukuDb/places.sqlite*
/tests/vcr_cassettes/test_search_and_open_all_in_browser.yaml
/tests/vcr_cassettes/tests.test_bukuDb/
/bookmarks.db
/venv/
/docker-compose/data/*
!/docker-compose/data/nginx/


================================================
FILE: .pre-commit-config.yaml
================================================
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
  rev: v4.3.0
  hooks:
  - id: check-added-large-files
- repo: https://github.com/pycqa/isort
  rev: 5.10.1
  hooks:
  - id: isort
    name: isort (python)
    args: [--profile, black, --filter-files]
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
  rev: v2.4.0
  hooks:
  - id: pretty-format-yaml
    args: [--autofix]
- repo: https://github.com/akaihola/darker
  rev: 1.5.1
  hooks:
  - id: darker
    args: [--line-length, '139', --skip-string-normalization]
- repo: https://github.com/PyCQA/pylint/
  rev: v2.15.5
  hooks:
  - id: pylint
    name: pylint
    entry: pylint
    language: system
    types: [python]
    args: [-rn, -sn, --rcfile=tests/.pylintrc]
    # "-rn", # Only display messages
    # "-sn", # Don't display the score
    # based on
    # https://pylint.pycqa.org/en/latest/user_guide/pre-commit-integration.html
- repo: https://github.com/PyCQA/autoflake
  rev: v1.7.7
  hooks:
  - id: autoflake


================================================
FILE: .readthedocs.yaml
================================================
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Set the version of Python and other tools you might need
build:
  os: ubuntu-24.04
  tools:
    python: '3.12'
  jobs:
    install:
      - pip install --upgrade pip
      - pip install .[locales,server] --group docs

# Build documentation in the docs/ directory with Sphinx
sphinx:
  configuration: docs/source/conf.py

# If using Sphinx, optionally build your docs in additional formats such as PDF
# formats:
#    - pdf


================================================
FILE: CHANGELOG
================================================
buku (unreleased)

- Bukuserver: mitigate Host header poisoning via BUKUSERVER_SERVER_NAME config

buku v5.1
2025-12-07

- auto-import from Brave Browser
- `amd64` docker image (will be built starting from next release)
- sorting bookmarks by absence/presence of a specific tag
- reordering all bookmarks in DB (based on specified sorting)
- support overriding default URI scheme for `browse()` (@meonkeys)
- include a `Sec-Fetch-Mode` header when fetching a webpage (in case the website admin cares about that)
- source: renaming main file to `buku.py` (which is the name it gets installed as)
- packaging: migrating from `setup.py` to `pyproject.toml` (@branchvincent)
- packaging: installing the manpage into `pipx` venv (@branchvincent)
- Python: adding v3.14, removing v3.9
- API: improved BukuCrypt
- Bukuserver: Swagger-based interactive documentation for the web-API
- Bukuserver: option to change initial value of Fetch checkbox in Create Bookmark form
- Bukuserver: migrate from Bootstrap v3 to v4
- migrate to the upcoming flask-admin v2 (see #753)

buku v5.0
2025-04-37

- removed URL shorten/expand feature (see #815)
- fix "htttp" typos in tests (@chenrui333)
- fix default value of `--url` used with `--update` (see #729)
- add Python 3.12 support (@chenrui333)
- add Python 3.13 support + use it as default for testing
- list [BukuBot](https://git.xmpp-it.net/sch/BukuBot) in the docs as a related project (@sjehuda)
- fix pylint warnings
- fix parsing of `<meta name="keywords">` in fetched webpages (see #734)
- fix `get_rec_all_by_ids()`
- fix autoimport of Firefox profiles with a custom absolute path (see #791)
- fix `--db` parameter being ignored by encryption actions (see #796)
- fix inconsistent handling of netloc (see #799)
- fix import in single-parent-folder-tag mode (see #807)
- add import/export in the `.rss` (Atom) format (@vagos)
- add `.atom` as an RSS import/export extension
- add description to XBEL export format (@sjehuda)
- support raw (unnamed) URLs when parsing Markdown/OrgMode (i.e. `<url>`/`[[url]]` respectively)
- implement backwards navigation in the interactive shell mode (@20mattia02)
- implement DB switching in the interactive shell mode (with displaying non-default DB name)
- support opening interactive shell mode on a non-default DB
- implement custom-setting the default DB directory
- implement printing DB list in default directory when invoked specifically as `buku --db`
- implement handling `--db` parameter without `.` and directory separators as a _name_ (with omitted `.db` extension and located in default DB directory)
- Bukuserver: implement support for passing a DB name in `BUKUSERVER_DB_FILE`
- implement thread safety (e.g. for handling simultaneous requests in Bukuserver)
- Bukuserver: prevent `/api/tags` query from being spammed due to a Flask-Admin bug
- Bukuserver: increase size of the bookmarklet popup window
- Bukuserver: minor UI improvements
- Bukuserver: some readme improvements (@rachmadaniHaryono)
- Bukuserver: add "contains"/"not contains" modes for some filters (URL/title in Bookmarks, name in Tags)
- Bukuserver: fix input autofoxus on page/dialog opening
- Bukuserver: ensure that links in the bookmarklet popup window are always opened externally
- allow tags supplied by `--add` (or by `--tag`) to override fetched tags (unless started with `+`)
- implement search-with-markers (see #777)
- implement bookmarks output ordering [by fields/netloc] (see #777)
- implement random selection of bookmarks
- implement bookmark index swapping
- Bukuserver: implement translations
- Bukuserver: fix unstable ordering of the filters form
- Bukuserver: add a runner script
- Bukuserver: add support for tags/fetch fields to bookmarklet API endpoint
- Bukuserver: fix popup detection for sites that prevent exposing `opener`
- Bukuserver: fix disabling no-netloc URLs in rendering modes other than `full`
- Bukuserver: remove dependency on the stale package Flask-API
- support `urllib3>=2` as a dependency (see #776)
- fix various deprecation warnings
- Bukuserver: update screenshots in documentation

-------------------------------------------------------------------------------

buku v4.9
2024-04-07

- fixed profile detection for multiple Firefox installs (#711)
- added option `--offline` to add a bookmark without web connection
- added a mini-guide for quick keyboard access to the bookmarklet
- support environment variable `NO_COLOR`
- fixed HTML encoding detection (#713)
- fixed Windows profile detection (#683)
- support python 3.11 (support for python 3.7 removed)
- fixed readline internal error on Windows (#704)

-------------------------------------------------------------------------------

buku v4.8
2023-02-18

- support Vivaldi browser
- better XBEL compatibility
- check for empty search results in piped operations
- remove python 3.6 support, add 3.10
- API changes in bukudb (#660):
  - bookmark data tuples returned from methods `get_rec_all()`
    & `get_rec_by_id()`, now have user-friendly properties
    (`id`, `url`, `title`, `desc`, `tags`/`taglist`, `immutable`;
    as well as for raw DB fields – `tags_raw`, `flags`)
  - methods `get_rec_all()`, `list_using_id()`, `searchdb()`, `search_by_tag()`,
    `search_keywords_and_filter_by_tags()` & `exclude_results_from_search()`
    are now guaranteed to return a list (empty if no data is found)
  - methods `get_rec_id()`, `get_max_id()` & `add_rec()` now return `None` as
    the "no ID" value
  - methods `add_rec()`, `update_rec()` & `edit_update_rec()` now treat the
    value of `immutable` parameter as a boolean (the default/noop value for
    update calls is `None`)
  - a `FIELD_FILTER` dictionary is introduced that contains fields formatting
    description; also, in `format_json()` (and `print_json_safe()`), the output
    format now matches the one described in CLI help
- IMPACT: If you have a local repo clone, remove .tox/ subfolder if it's there
    before you run tests for the first time

-------------------------------------------------------------------------------

buku v4.7
2022-07-01

- support XBEL export/import (#569)
- support for Microsoft Edge bookmarks (#585)
- block web fetch on import
- many bukuserver fixes (#543, #545, #547, #548, #553, #554, #559)
- import nested directory names as tags on html import (#539)
- fix slow/failed markdown import (#538)
- fix SSL certificate identification not working on macOS (#528)
- import tags from markdown (#523)
- fix broken pipe error with oil (#520)

-------------------------------------------------------------------------------

buku v4.6
2021-06-16

- use textwrap to wrap comments and tags when printing in terminal
- show listing start and end index over prompt in interactive mode
- option `--nostdin`: don't wait for input (must be first arg) (#513)
- user-friendly prompt message when watiing for input in non-tty mode
- several test framework improvements

-------------------------------------------------------------------------------

buku v4.5
2020-12-29

- Fix encryption and decryption (#480)
- Fix Wayback Machine API query
- Support wayland native copier `wl-copy`
- Add bookmarklet for bukuserver (#385)
- Delete by tag without prompting for each bookmark (#458)
- Fix issue with utf-8 characters in bookmark titles (#456)
- Fix reomve all tags from prompt (#467)
- Example to fuzzy search and preview in Quickstart section
- Replace debug option `-z` with `-g`
- Support python 3.9, retire python 3.5

-------------------------------------------------------------------------------

buku v4.4
2020-06-16

- optionally specify output file with `--json`
- confirm auto-tag generation in chatty mode
- unblock GUI browsers when running on WSL
- handle up to 10 server redirects (#452)
- fix issue with reverse proxy (#435)
- use ImportError instead ModuleNotFoundError (#437)
- import pyreadline on windows (#441)
- auto-generated package refresh

-------------------------------------------------------------------------------

Buku v4.3
2020-01-31

- Project renamed to `buku` (small `b`)
- Export tags in markdown format as comments
- Tag support for Org import/export
- Better Windows 10 support
- Reverse proxy support for `bukuserver`
- Config `OPEN_IN_NEW_TAB` for `bukuserver`
- Documentation updated
- Fix Firefox default profile detection
- Fix export to DB failing after search
- Fix broken prompt colors
- User agent updated

-------------------------------------------------------------------------------

Buku v4.2.2
2019-05-02

- Fixes broken prompt due to PR #373

-------------------------------------------------------------------------------

Buku v4.2.1
2019-04-30

- A fix on top of v4.2 to address a packaging problem

-------------------------------------------------------------------------------

Buku v4.2
2019-04-30

- Disabled appending tags from page on update
- Improved Windows color support using colorama (optional dep)
- New format option to show only title and tag
- Python 3.4 is EOL, support discontinued
- Several fixes and code refactor

-------------------------------------------------------------------------------

Buku v4.1
2019-01-15

What's in?
- Import firefox-exported json
- Fix auto-import for firefox
- Fix write to GNU Screen paste buffer
- Some CVE fixes

-------------------------------------------------------------------------------

Buku v4.0
2018-11-01

What's in?
- Show records in pages with option `-p` (works with `-n`, default 10)
- Enhanced clipboard support: `xclip`, tmux, GNU Screen, Termux
- Prompt key `O` works with search results along with GUI browser toggling
- Search by taglist id with prompt key `g`
- Multiple fixes

-------------------------------------------------------------------------------

Buku v3.9
2018-08-30

What's in?
- Set number of search results per page (default 10)
- Retrieve description and tags from page, if available
- Visit cached version on Wayback Machine
- Export works with all search options now
- Changed user agent to Firefox on Ubuntu
- Several dependencies made _required_ for installation
- bukuserver will use Flask-Admin

-------------------------------------------------------------------------------

Buku v3.8
2018-05-24

What's in?
- A self-hosted http server, bukuserver, that exposes core functionality
    - browsable front-end on a local web host server
    - flask default cli interface is used instead custom one
    - handle not only API but also HTML request
    - statistic page
    - CRUD on bookmark
    - replaces the earlier API module
- Import complete folder hierarchy as tags during auto-import
- Merge tags on import even if bookmark URL exists
- Orgfile import/export
- Show bookmarks to be deleted before deletion
- Merge tags during import if bookmark exists
- Escape regex metacharacters in regex input

-------------------------------------------------------------------------------

Buku v3.7
2018-03-28

What's in?
- Exclude keywords in search (keyword filtering)
- Search and filter by tags
- Order search results by number of keyword matches
- Copy URL to clipboard
- Prompt shortcut 'O' to override text browsers
- New official packagers: Fedora, Gentoo, OpenBSD, openSUSE

-------------------------------------------------------------------------------

Buku v3.6
2018-01-09

What's in?
- Skip bookmark addition if edit is aborted
- Use urllib3 for handling http connections everywhere
- Fix auto-import on FreeBSD
- Generate packages for openSUSE Leap 42.3, Fedora 27

-------------------------------------------------------------------------------

Buku v3.5
2017-11-10

What's in?
- Buku now has its own user agent
- Search works with field filters
- Edit the last record with `-w=-1` (useful when adding bookmark from GUI)a
- Support for Chromium browser
- Colors disabled by default on cmd (Windows), option `--colors` has to be used
- Get default Firefox profile name from profiles.ini
- Bash scriptlet to autogen records for testing
- Some optimization in add record and suggest tags
- A fresh utility Pinku to import Pinboard bookmarks to Buku

-------------------------------------------------------------------------------

Buku v3.4
2017-09-18

What's in?
- Export bookmarks (including specific tags) to Buku DB file using `--export`
- Option `--import` can merge Buku DB files now, option `--merge` is retired
- Option `--suggest` now works at prompt as well
- Auto-import issue when Firefox is not installed fixed

-------------------------------------------------------------------------------

Buku v3.3.1
2017-09-11

This is for all purposes the same as v3.3. We had to re-upload a new version to
PyPi and hence the new tag. Functionality remains the same.

The tagline is changed to - `Powerful command-line bookmark manager.`

-------------------------------------------------------------------------------

Buku v3.3
2017-09-11

What's in?
- Auto-import (`--ai`) bookmarks from Firefox and Google Chrome
- Support custom colors (`--colors`)
- Search multiple tags (with exclusion)
- Timestamp (YYYYMonDD) tag in auto-imported bookmarks
- Enable browser output for text browsers
- Generate documentation in RTD using Sphinx (http://buku.readthedocs.io)
- Integrated flake8 and pylint in Travis CI
- Integrated PackageCore to auto-generate packages in Travis CI

-------------------------------------------------------------------------------

Buku v3.2
2017-08-03

What's in?
- Option `--suggest` to list and choose similar tags when adding a bookmark
- Ask for a unique tag when importing bookmarks
- Ignore non-generic URLs when importing browser exported bookmarks

-------------------------------------------------------------------------------

Buku v3.1
2017-06-30

What's in?
- Handle negative indices (like tail) with option `-p`
- Support browsing bookmarks from prompt (key `o`)
- Add program search keywords to history
- Support XDG_DATA_HOME and HOME as env vars on all platforms
- Replace %USERPROFILE% with %APPDATA% as install location on Windows

-------------------------------------------------------------------------------

Buku v3.0
2017-04-26

What's in?
- Edit bookmarks in EDITOR at prompt
- Import folder names as tags from browser HTML (thanks @mohammadKhalifa)
- Append, overwrite, delete tags at prompt using >>, >, << (familiar, eh? ;))
- Negative indices with `--print` (like `tail`)
- Update in EDITOR along with `--immutable`
- Request HTTP HEAD for immutable records
- Interface revamp (title on top in bold, colour changes...)
- Per-level colourful logs in colour mode
- Changes in program OPTIONS
  - `-t` stands for tag search (earlier `--title`)
  - `-r` stands for regex search (earlier `--replace`)
- Lots of new automated test cases (thanks @rachmadaniHaryono)
- REST APIs for server-side apps (thanks @kishore-narendran)
- Document, notify behaviour when not invoked from tty (thanks @The-Compiler)
- Fix Firefox tab-opening issues on Windows (thanks @dertuxmalwieder)

-------------------------------------------------------------------------------

Buku v2.9
2017-02-20

Modifications
- New option `--write` to compose and edit bookmarks in text editor
- Support positional arguments as search keywords
- New option `--oa` to search and open results directly in browser
- Autodetect Markdown mode by file extension during export, import
- Shortened options:
    - `--nc` replaces `--nocolor`
    - `--np` replaces `--noprompt`
    - `-V` replaces `--upstream`
- Option `--markdown` removed as the mode is autodetected now

-------------------------------------------------------------------------------

Buku v2.8
2017-01-11

Modifications
- Multithreaded full DB refresh with delayed writes
- Customize number of threads for full DB refresh (default 4)
- Support search and update search results in a go
- Support shortened URL expansion
- Support multiple bookmarks with `--open`
- Support `--nocolor` (for scripting, Windows users)
- Support https_proxy with `--upstream` and `--shorten`
- Remove trailing `/` from search tokens (like Google search)
- Support `--version` to show program version
- Fixed #109: Missing + when shortening URL
- Performance optimizations, few module dependency removals

-------------------------------------------------------------------------------

Buku v2.7
2016-11-30

Modifications
- Continuous search at (redesigned) prompt
- urllib3 for all HTTP operations
- Use HTTP HEAD method for pdf and txt mime types
- Add user agent (Firefox 50 on Ubuntu)
- Support URL shortening
- List bookmarks by tag index in tag list
- Show tag usage count in tag list
- Store tags in lowercase (use undocumented option `--fixtags` to fix old tags)
- Support environment variable *https_proxy*
- Support option `--immutable` to pin titles
- Keyword `immutable` to search (`-S`) pinned titles
- Show index in JSON output
- New key *q* to quit prompt
- Support deflate compression
- Add option `--tacit` to reduce verbosity of some operations
- **Removed** option `--st`, only `--stag` to search tags
- Support custom DB file location (for library, not exposed to user)

-------------------------------------------------------------------------------

Buku v2.6
2016-11-04

Modifications
- Support Markdown import/export
- Support regex search
- New option `--upstream` to check latest upstream version
- Fix search and delete behaviour
- Lot of code reformatting, performance improvements
- Use delayed commit wherever possible (e.g. bulk deletion cases)
- When a range is specified, consider 0 as ALL
- Added option to control verbosity in some APIs
- In-source documentation update

-------------------------------------------------------------------------------

Buku v2.5
2016-10-20

Modifications
- Export specific tags to HTML
- Fixed obvious issues on Windows
- Open a random bookmark with option --open
- Support lists and ranges with --print
- Show a bookmark on tag append
- Show only title with --format=3
- PEP8 compliance fixes
- Buku GUI integration documented

-------------------------------------------------------------------------------

Buku v2.4
2016-09-12

Modifications
- Exact word match support using regex (**default**)
- New option --deep to scan matching substrings
- Support DB index lists and ranges in update operation
- Open a list or range of search results in browser
- Open all search results in browser
- A more concise prompt
- PEP8 compliance (almost)
- Tons of new test cases added (thanks @wheresmyjetpack)

-------------------------------------------------------------------------------

Buku v2.3
2016-07-14

Modifications
- Delete a range or a list of indices
- Delete tag from tagset by bookmark index
- Delete results of a particular search
- Linked to rofi front-end script project for Buku
- Use the logging framework for debug info instead of print
- Fixed an issue with gzip stream decoding
- Using only relative path to fetch resource on server
- Fixed auto-completion errors with Zsh
- A lot of code cleanup and globals removed, additional test cases

-------------------------------------------------------------------------------

Buku v2.2
2016-06-12

Modifications
- Export bookmarks to Firefox bookmarks formatted HTML
- Merge Buku database
- .deb package for Debian and Ubuntu family
- Switch from PyCrypto to cryptography (thanks @asergi)
- Append tags support
- Filter tags for duplicates and sort alphabetically
- Travis CI integration, more test cases (thanks @poikjhn)
- Show DB index in bold in search results
- Several performance optimizations

-------------------------------------------------------------------------------

Buku v2.1
2016-05-28

Modifications
- Import bookmarks from Firefox, Google Chrome or IE HTML bookmark exports
- Support comments on bookmarks
- Prettier output using symbols (`>` title, `+` comments, `#` tags)
- New option (`--st`, `--stag`) to search by tag
- New option (`--noprompt`) for noninteractive mode
- New options (`--url` and `--tag`)
- `--update` now handles each option (url, tag, title, comment) independently
- Several messages removed or moved to debug

-------------------------------------------------------------------------------

Buku v2.0
2016-05-15

Modifications
To begin with, 2.0 is a significant release with respect to options. `Buku` now has fewer options with more (and merged) functionality. Please go through the program help at least once to understand the changes.

- Replace getopt with argparse for parsing arguments
- Long options for each short option
- Options changed
    - insert: removed as automatic DB compaction serves the purpose (previously `-i`)
    - iterations: removed as optional argument to `-l` and `-k` (previously `-t`)
    - title: `-t` is now the short option to set title manually (previously `-m`)
    - Special search keywords for ALL search (`-S`):
        - tags: show all tags (previously `-g`)
        - blank: show bookmarks with empty tags (previously `-e`)
    - lock/unlock: now accepts number of hash iterations to generate key
    - format: print formatting option changed to `-f` (previously `-x`)
    - help: option added to show program help
- Following options apply to ALL bookmarks without arguments
    - `-u`, `--update`
    - `-d`, `--delete`
    - `-p`, `--print`
- Shell-completion scripts for Bash, Fish and Zsh
- Warn if URL is not HTTP(S)
- More comprehensive help
- Fix a bug with deletion when only one entry in DB
- Some import dependencies removed or late loaded (if optional)
- Handle exception if DB file is encrypted or invalid

-------------------------------------------------------------------------------

Buku v1.9
2016-04-23

Modifications
- **New location for database file** (refer to README or man page). The old database file, if exists, is migrated automatically.
- **Removed options**
    - `-P`: (print all) is now `-p 0`
    - `-D`: (delete all) is now `-d 0`
    - `-R`: (update all) is now `-u 0`
    - `-w`: title web fetch is now the default behaviour, override with `-m title` option
- **Change in search behaviour**
    - `-s`: search bookmarks for ANY keyword in URL, title or tags
    - `-S`: search bookmarks for ALL keywords in URL, title or tags
- Update only title of a bookmark (`-u N`)
- Set empty title (`-m none`)
- Support HTTP(S) gzip compression
- Optional JSON output for `-p` and `-s` options (thanks @CaptainQuirk)
- Reformatted help and man page with general options on top
- Optimize add and insert: ensure URL is not in DB already
- Handle URLs passed with %xx escape
- Retry with truncated resource path on HTTP error 500
- Several code optimizations
- Catchier errors and warnings
- Version added to debug logs

-------------------------------------------------------------------------------

Buku v1.8
2016-03-26

Modifications
- Auto compact DB on single record removal
- Handle piped input
- Better tag management
    - Tag modify or delete support
    - Show unique tags alphabetically
- Full DB refresh
    - Fix stuff broken earlier
    - Optimize to update titles only
    - Update titles only if non-empty to preserve earlier data
- Redirection
    - Handle multiple redirections
    - Detect redirection loop and break
    - Show redirected link in bold
- List all bookmarks with no title or tags (for manual bookkeeping)
- Confirm full DB removal
- Better comma (`,`) separator handling for tags
- Help
    - Place regular options before power options in program help
    - Help added in man page for quick reference
    - Additional examples for new features
- Errors & warnings
    - Error out if both encrypted and flat DB files exist
    - Catchier error and warning messages

-------------------------------------------------------------------------------

Buku v1.7
2016-03-15

Modifications
- Add title manually using option `-m`
- Unquote redirected URL
- Quit on `Ctrl-d` at prompt
- More dynamic shebang for python3

-------------------------------------------------------------------------------

Buku v1.6
2016-01-22

Modifications
- Stronger encryption: 256-bit salt, multi-hash key.
- Allow user to specify number of iterations to generate key (check option `-t`).

-------------------------------------------------------------------------------

Buku v1.5
2015-12-20

Modifications
- Project name changed to `Buku` to avoid any copyright issues. This also means old users have to move the database file. Run:
<pre>$ mkdir ~/.cache/buku/
$ mv ~/.cache/markit/bookmarks.db ~/.cache/buku/bookmarks.db
$ rm -rf ~/.cache/markit/bookmarks.db</pre>
- Manual AES-256 encryption and decryption support (password protection) implemented. This adds dependency on PyCrypto module. Installation instructions updated in README.
- Some typos fixed (thanks @GuilhermeHideki)

-------------------------------------------------------------------------------

MarkIt v1.4
2015-11-13

Modifications
- Refresh full bookmark database. Fetch titles from the web, retain tags.
- Notify empty titles in red during online add or update.

-------------------------------------------------------------------------------

MarkIt v1.2
2015-11-11

Modifications
- Introduced `-S` search option to match ALL keywords in URL or title
- Introduced `-x` option to show unformatted selective output (for creating batch scripts)
- Added examples on batch add and update (refresh) scripts
- Handle multiple title tags in page
- Handle title data within another tag (e.g. head)
- Show DB index in search results, removal and update confirmation message

-------------------------------------------------------------------------------

MarkIt v1.1
2015-11-10

Modifications
- Replace Unicode chars in title data before UTF-8 decoding (for parser to succeed).

-------------------------------------------------------------------------------


================================================
FILE: Dockerfile
================================================
FROM python:3.14.0-alpine

LABEL org.opencontainers.image.authors="shenoy.ameya@gmail.com"

ENV BUKUSERVER_PORT=5001

COPY . /buku

ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1

RUN set -ex \
  && apk add --no-cache --virtual .build-deps \
    gcc \
    openssl-dev \
    musl-dev \
    libffi-dev \
  && pip install -U --no-cache-dir \
    pip \
    gunicorn \
    /buku[server] \
  && apk del .build-deps \
  && echo "import sys;  bind = '0.0.0.0:${BUKUSERVER_PORT}'" > 'gunicorn.conf.py' \
  && rm -rf /buku

HEALTHCHECK --interval=1m --timeout=10s \
  CMD nc -z 127.0.0.1 ${BUKUSERVER_PORT} || exit 1

ENTRYPOINT ["gunicorn", "bukuserver.server:create_app()"]
EXPOSE ${BUKUSERVER_PORT}


================================================
FILE: LICENSE
================================================
                    GNU GENERAL PUBLIC LICENSE
                       Version 3, 29 June 2007

 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU General Public License is a free, copyleft license for
software and other kinds of works.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.  We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors.  You can apply it to
your programs, too.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights.  Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.

  For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received.  You must make sure that they, too, receive
or can get the source code.  And you must show them these terms so they
know their rights.

  Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.

  For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software.  For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.

  Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so.  This is fundamentally incompatible with the aim of
protecting users' freedom to change the software.  The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable.  Therefore, we
have designed this version of the GPL to prohibit the practice for those
products.  If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.

  Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary.  To prevent this, the GPL assures that
patents cannot be used to render the program non-free.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Use with the GNU Affero General Public License.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time.  Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    {one line to give the program's name and a brief idea of what it does.}
    Copyright (C) {year}  {name of author}

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

Also add information on how to contact you by electronic and paper mail.

  If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

    {project}  Copyright (C) {year}  {fullname}
    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
    This is free software, and you are welcome to redistribute it
    under certain conditions; type `show c' for details.

The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License.  Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.

  The GNU General Public License does not permit incorporating your program
into proprietary programs.  If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library.  If this is what you want to do, use the GNU Lesser General
Public License instead of this License.  But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.



================================================
FILE: MANIFEST.in
================================================
include CHANGELOG LICENSE README.md buku.1 requirements.txt
include tests/test_bukuDb/Bookmarks
recursive-include tests *.py
recursive-include tests/test_bukuDb *.yaml
recursive-include auto-completion *
recursive-include bukuserver/apidocs *
recursive-include bukuserver/templates *
recursive-include bukuserver/static *


================================================
FILE: Makefile
================================================
PREFIX ?= /usr/local
BINDIR ?= $(DESTDIR)$(PREFIX)/bin
MANDIR ?= $(DESTDIR)$(PREFIX)/share/man/man1
DOCDIR ?= $(DESTDIR)$(PREFIX)/share/doc/buku

.PHONY: all install uninstall

all:

install:
	install -m755 -d $(BINDIR)
	install -m755 -d $(MANDIR)
	install -m755 -d $(DOCDIR)
	gzip -c buku.1 > buku.1.gz
	install -m755 buku.py $(BINDIR)/buku
	install -m644 buku.1.gz $(MANDIR)
	install -m644 README.md $(DOCDIR)
	rm -f buku.1.gz

uninstall:
	rm -f $(BINDIR)/buku
	rm -f $(MANDIR)/buku.1.gz
	rm -rf $(DOCDIR)


================================================
FILE: README.md
================================================
<h1 align="center">buku</h1>

<p align="center">
<a href="https://github.com/jarun/buku/releases/latest"><img src="https://img.shields.io/github/release/jarun/buku.svg?maxAge=600" alt="Latest release" /></a>
<a href="https://repology.org/project/buku/versions"><img src="https://repology.org/badge/tiny-repos/buku.svg?header=repos" alt="Availability"></a>
<a href="https://pypi.org/project/buku/"><img src="https://img.shields.io/pypi/v/buku.svg?maxAge=600" alt="PyPI" /></a>
<a href="https://circleci.com/gh/jarun/workflows/buku"><img src="https://img.shields.io/circleci/project/github/jarun/buku.svg" alt="Build Status" /></a>
<a href="https://buku.readthedocs.io/en/latest/?badge=latest"><img src="https://readthedocs.org/projects/buku/badge/?version=latest" alt="Docs Status" /></a>
<a href="https://en.wikipedia.org/wiki/Privacy-invasive_software"><img src="https://img.shields.io/badge/privacy-✓-crimson" alt="Privacy Awareness" /></a>
<a href="https://github.com/jarun/buku/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-GPLv3-yellowgreen.svg?maxAge=2592000" alt="License" /></a>
</p>

<p align="center">
<a href="https://github.com/user-attachments/assets/a65950a9-3c82-4c6d-9184-dead83f9c759"><img src="https://github.com/user-attachments/assets/6688d1eb-e268-4b00-8e8f-044803c3040f" alt="buku in action!" width="734"/></a>
</p>

<p align="center"><i>buku in action!</i></p>

### Introduction

`buku` is a powerful bookmark manager and a personal textual mini-web.

For those who prefer the GUI, `bukuserver` exposes a browsable front-end on a local web host server. See [bukuserver page](https://github.com/jarun/buku/tree/master/bukuserver#readme) for config and screenshots.

When I started writing it, I couldn't find a flexible command-line solution with a private, portable, merge-able database along with seamless GUI integration. Hence, `buku`.

`buku` can import bookmarks from browser(s) or fetch the title, tags and description of a URL from the web. Use your favourite editor to add, compose and update bookmarks. Search bookmarks instantly with multiple search options, including regex and a deep scan mode (handy with URLs).

It can look up broken links on the Wayback Machine. There's an Easter Egg to revisit random bookmarks.

There's no tracking, hidden history, obsolete records, usage analytics or homing.

To get started right away, jump to the [Quickstart](#quickstart) section. `buku` has one of the best documentation around. The man page comes with examples. For internal details, please refer to the [operational notes](https://github.com/jarun/buku/wiki/Operational-notes).

`buku` is a library too! There are several related projects, including a browser plug-in.

### Table of Contents

- [Features](#features)
- [Installation](#installation)
  - [Dependencies](#dependencies)
  - [From a package manager](#from-a-package-manager)
  - [Release packages](#release-packages)
  - [From source](#from-source)
  - [Running standalone](#running-standalone)
- [Shell completion](#shell-completion)
- [Usage](#usage)
  - [Command-line options](#command-line-options)
  - [Colors](#colors)
- [Quickstart](#quickstart)
- [Examples](#examples)
- [Automation](#automation)
- [Troubleshooting](#troubleshooting)
  - [Editor integration](#editor-integration)
- [Collaborators](#collaborators)
- [Contributions](#contributions)
- [Related projects](#related-projects)
- [In the Press](#in-the-press)

### Features

- Store bookmarks with auto-fetched title, tags and description
- Auto-import from Firefox, Google Chrome, Chromium, Vivaldi, Brave, and MS Edge
- Open bookmarks and search results in browser
- Browse cached page from the Wayback Machine
- Text editor integration
- Lightweight, clean interface, custom colors
- Powerful search options (regex, substring...)
- Continuous search with on the fly mode switch
- Portable, merge-able database to sync between systems
- Import/export bookmarks from/to HTML, XBEL, Markdown, RSS/Atom or Orgfile
- Smart tag management using redirection (>>, >, <<)
- Multi-threaded full DB refresh
- Manual encryption support
- Shell completion scripts, man page with handy examples
- Privacy-aware (no unconfirmed user data collection)
- Can be used as a Python library ([_API documentation_](https://buku.readthedocs.io/en/latest/?badge=latest))
- Has a compation Web-application ([Bukuserver](https://github.com/jarun/buku/wiki/Bukuserver-%28WebUI%29)) with an HTTP-based API (for personal use only)

### Installation

#### Dependencies

| Feature | Dependency |
| --- | --- |
| Lang, SQLite | Python 3.10+ |
| HTTPS | certifi, urllib3 |
| Encryption | cryptography |
| HTML | beautifulsoup4, html5lib |

To copy URL to clipboard `buku` uses `xsel` (or `xclip`) on Linux, `pbcopy` (default installed) on OS X, `clip` (default installed) on Windows, `termux-clipboard` on Termux (terminal emulation for Android), `wl-copy` on Wayland. If X11 is missing, GNU Screen or tmux copy-paste buffers are recognized.

#### From a package manager

To install buku with all its dependencies from PyPI, run:

    # pip3 install buku

You can also install `buku` from your package manager. If the version available is dated try an alternative installation method.

<details><summary>Packaging status (expand)</summary>
<p>
<br>
<a href="https://repology.org/project/buku/versions"><img src="https://repology.org/badge/vertical-allrepos/buku.svg" alt="Packaging status"></a>
</p>
Unlisted packagers:
<p>
<br>
● <a href="https://pypi.org/project/buku/">PyPI</a> (<code>pip3 install buku</code>)<br>
● Termux (<code>pip3 install buku</code>)<br>
</p>
</details>

#### Release packages

Auto-generated packages (with only the cli component) for Arch Linux, CentOS, Debian, Fedora, openSUSE Leap and Ubuntu are available with the [latest stable release](https://github.com/jarun/buku/releases/latest).

NOTE: CentOS may not have the python3-beautifulsoup4 package in the repos. Install it using pip3.

#### From source

If you have git installed, clone this repository. Otherwise download the [latest stable release](https://github.com/jarun/buku/releases/latest) or [development version](https://github.com/jarun/buku/archive/master.zip) (*risky*).

Install the dependencies. For example, on Ubuntu:

    $ apt-get install ca-certificates python3-urllib3 python3-cryptography python3-bs4

Install the cli component to default location (`/usr/local`):

    $ sudo make install

To remove, run:

    $ sudo make uninstall

`PREFIX` is supported, in case you want to install to a different location.

#### Running standalone

`buku` is a standalone utility. From the containing directory, run:

    $ chmod +x buku.py
    $ ./buku.py

### Shell completion

Shell completion scripts for Bash, Fish and Zsh can be found in respective subdirectories of [auto-completion/](https://github.com/jarun/buku/blob/master/auto-completion). Please refer to your shell's manual for installation instructions.

### Usage

#### Command-line options

```
usage: buku [OPTIONS] [KEYWORD [KEYWORD ...]]

Bookmark manager like a text-based mini-web.

POSITIONAL ARGUMENTS:
      KEYWORD              search keywords

GENERAL OPTIONS:
      -a, --add URL [+|-] [tag, ...]
                           bookmark URL with comma-separated tags
                           (prepend tags with '+' or '-' to use fetched tags)
      -u, --update [...]   update fields of an existing bookmark
                           accepts indices and ranges
                           refresh title and desc if no edit options
                           if no arguments:
                           - update results when used with search
                           - otherwise refresh all titles and desc
      -w, --write [editor|index]
                           edit and add a new bookmark in editor
                           else, edit bookmark at index in EDITOR
                           edit last bookmark, if index=-1
                           if no args, edit new bookmark in EDITOR
      -d, --delete [...]   remove bookmarks from DB
                           accepts indices or a single range
                           if no arguments:
                           - delete results when used with search
                           - otherwise delete all bookmarks
      --retain-order       prevents reordering after deleting a bookmark
      -h, --help           show this information and exit
      -v, --version        show the program version and exit

EDIT OPTIONS:
      --url keyword        bookmark link
      --tag [+|-] [...]    comma-separated tags
                           clear bookmark tagset, if no arguments
                           '+' appends to, '-' removes from tagset
      --title [...]        bookmark title; if no arguments:
                           -a: do not set title, -u: clear title
      -c, --comment [...]  notes or description of the bookmark
                           clears description, if no arguments
      --immutable N        disable web-fetch during auto-refresh
                           N=0: mutable (default), N=1: immutable
      --swap N M           swap two records at specified indices

SEARCH OPTIONS:
      -s, --sany [...]     find records with ANY matching keyword
                           this is the default search option
      -S, --sall [...]     find records matching ALL the keywords
                           special keywords -
                           "blank": entries with empty title/tag
                           "immutable": entries with locked title
      --deep               match substrings ('pen' matches 'opens')
      --markers            search for keywords in specific fields
                           based on (optional) prefix markers:
                           '.' - title, '>' - description, ':' - URL,
                           '#' - tags (comma-separated, PARTIAL matches)
                           '#,' - tags (comma-separated, EXACT matches)
                           '*' - any field (same as no prefix)
      -r, --sreg expr      run a regex search
      -t, --stag [tag [,|+] ...] [- tag, ...]
                           search bookmarks by tags
                           use ',' to find entries matching ANY tag
                           use '+' to find entries matching ALL tags
                           excludes entries with tags after ' - '
                           list all tags, if no search keywords
      -x, --exclude [...]  omit records matching specified keywords
      --random [N]         output random bookmarks out of the selection (default 1)
      --order fields [...] comma-separated list of fields to order the output by
                           (prepend with '+'/'-' to choose sort direction)

ENCRYPTION OPTIONS:
      -l, --lock [N]       encrypt DB in N (default 8) # iterations
      -k, --unlock [N]     decrypt DB in N (default 8) # iterations

POWER TOYS:
      --ai                 auto-import bookmarks from web browsers
                           Firefox, Chrome, Chromium, Vivaldi, Brave, Edge
                           (Firefox profile can be specified using
                           environment variable FIREFOX_PROFILE)
      -e, --export file    export bookmarks to Firefox format HTML
                           export XBEL, if file ends with '.xbel'
                           export Markdown, if file ends with '.md'
                           format: [title](url) <!-- TAGS -->
                           export Orgfile, if file ends with '.org'
                           format: *[[url][title]] :tags:
                           export rss feed if file ends with '.rss'/'.atom'
                           export buku DB, if file ends with '.db'
                           combines with search results, if opted
      -i, --import file    import bookmarks from file
                           supports .html .xbel .json .md .org .rss .atom .db
                           (.json = Firefox backup; .db = another Buku DB)
      -p, --print [...]    show record details by indices, ranges
                           print all bookmarks, if no arguments
                           -n shows the last n results (like tail)
      -f, --format N       limit fields in -p or JSON search output
                           N=1: URL; N=2: URL, tag; N=3: title;
                           N=4: URL, title, tag; N=5: title, tag;
                           N0 (10, 20, 30, 40, 50) omits DB index
      -j, --json [file]    JSON formatted output for -p and search.
                           prints to stdout if argument missing.
                           otherwise writes to given file
      --colors COLORS      set output colors in five-letter string
      --nc                 disable color output
      -n, --count N        show N results per page (default 10)
      --np                 do not show the subprompt, run and exit
      -o, --open [...]     browse bookmarks by indices and ranges
                           open a random bookmark, if no arguments
      --oa                 browse all search results immediately
      --default-scheme S   if scheme is missing from uri, assume S
                           when opening in browser (default http)
      --replace old new    replace old tag with new tag everywhere
                           delete old tag, if new tag not specified
      --url-redirect       when fetching an URL, use the resulting
                           URL from following *permanent* redirects
                           (when combined with --export, the old URL
                           is included as additional metadata)
      --tag-redirect [tag] when fetching an URL that causes permanent
                           redirect, add a tag in specified pattern
                           (using 'http:{}' if not specified)
      --tag-error [tag]    when fetching an URL that causes an HTTP
                           error, add a tag in specified pattern
                           (using 'http:{}' if not specified)
      --del-error [...]    when fetching an URL causes any (given)
                           HTTP error, delete/do not add it
      --export-on [...]    export records affected by the above
                           options, including removed info
                           (requires --update and --export; specific
                           HTTP response filter can be provided)
      --reorder order...   update DB indices to match specified order
      --cached index|URL   browse a cached page from Wayback Machine
      --offline            add a bookmark without connecting to web
      --suggest            show similar tags when adding bookmarks
      --tacit              reduce verbosity, skip some confirmations
      --nostdin            do not wait for input (must be first arg)
      --threads N          max network connections in full refresh
                           default N=4, min N=1, max N=10
      -V                   check latest upstream version available
      -g, --debug          show debug information and verbose logs

SYMBOLS:
      >                    url
      +                    comment
      #                    tags

PROMPT KEYS:
    1-N                    browse search result indices and/or ranges
    R [N]                  print out N random search results
                           (or random bookmarks if negative or N/A)
    ^ id1 id2              swap two records at specified indices
    O [id|range [...]]     open search results/indices in GUI browser
                           toggle try GUI browser if no arguments
    a                      open all results in browser
    s keyword [...]        search for records with ANY keyword
    S keyword [...]        search for records with ALL keywords
    d                      match substrings ('pen' matches 'opened')
    m                      search with markers - search string is split
                           into keywords by prefix markers, which determine
                           what field the keywords is searched in:
                           '.', '>' or ':' - title, description or URL
                           '#'/'#,' - tags (comma-separated, partial/full match)
                           '*' - all fields (can be omitted in the 1st keyword)
                           note: tag marker is not affected by 'd' (deep search)
    v fields               change sorting order (default is '+index')
                           multiple comma/space separated fields can be specified
    v! fields              update indices in DB to match specified order
    r expression           run a regex search
    t [tag, ...]           search by tags; show taglist, if no args
    g taglist id|range [...] [>>|>|<<] [record id|range ...]
                           append, set, remove (all or specific) tags
                           search by taglist id(s) if records are omitted
    n                      show next page of search results
    N                      show previous page of search results
    o id|range [...]       browse bookmarks by indices and/or ranges
    p id|range [...]       print bookmarks by indices and/or ranges
    w [editor|id]          edit and add or update a bookmark
    c id                   copy URL at search result index to clipboard
    DB [name]              check existing DB list or switch to another DB
                           (use full/dir path to switch folders)
                           '~.' can be used as shortcut for default DB
    ?                      show this help
    q, ^D, double Enter    exit buku
```

#### Colors

`buku` supports custom colors. Visit the wiki page on how to [customize colors](https://github.com/jarun/buku/wiki/Customize-colors) for more details.

### Quickstart

1. Export `VISUAL` or `EDITOR` to point to your favourite editor. Note that `VISUAL` takes precedence over `EDITOR`.
2. Create a sweeter shortcut with some convenience.

       alias b='buku --suggest'
3. Auto-import bookmarks from your browser(s). Please quit the relevant browsers beforehand to ensure the databases are not locked.

       b --ai
4. Manually add a bookmark (for hands-on).

       b -w
5. List your bookmarks with DB index.

       b -p
6. For GUI and browser integration (or to sync bookmarks with your favourite bookmark management service) refer to the wiki page on [System integration](https://github.com/jarun/buku/wiki/System-integration).
7. Quick (bash/zsh) commands to fuzzy search with fzf and open the selection in Firefox:

       firefox $(buku -p -f 10 | fzf)
       firefox $(buku -p -f 40 | fzf | cut -f1)

   POSIX script to show a preview of the bookmark as well:

   ```sh
   #!/usr/bin/env sh

   url=$(buku -p -f4 | fzf -m --reverse --preview "buku -p {1}" --preview-window=wrap | cut -f2)

   if [ -n "$url" ]; then
       echo "$url" | xargs firefox
   fi
   ```

### Examples

1. **Edit and add** a bookmark from editor:

       $ buku -w
       $ buku -w 'gedit -w'
       $ buku -w 'macvim -f' -a https://ddg.gg search engine, privacy
    The first command picks editor from the environment variable `EDITOR`. The second command opens gedit in blocking mode. The third command opens macvim with option -f and the URL and tags populated in template.
2. **Add** a simple bookmark:

       $ buku --nostdin -a https://github.com/
       2648. GitHub: Let’s build from here · GitHub
       > https://github.com/
       + GitHub is where over 94 million developers shape the future of software, together. Contribute to the open source community, manage your Git repositories, review code like a pro, track bugs
        and features, power your CI/CD and DevOps workflows, and secure code before you commit it.

       $ buku --nostdin -a https://github.com/
       [ERROR] URL [https://github.com/] already exists at index 2648

      `>`: URL, `+`: comment, `#`: tags

      Title, description and tags will be fetched from site. Buku only stores unique URLs and will raise error if the URL already present in the database:
3. **Add** a bookmark with **tags** `search engine` and `privacy`, **comment** `Search engine with perks`, **fetch page title** from the web:

       $ buku -a https://ddg.gg search engine, privacy -c Search engine with perks
       336. DuckDuckGo
       > https://ddg.gg
       + Alternative search engine with perks
       # privacy,search engine
    where, `>`: URL, `+`: comment, `#`: tags
4. **Add** a bookmark with tags `search engine` & `privacy` and **immutable custom title** `DDG`:

       $ buku -a https://ddg.gg search engine, privacy --title 'DDG' --immutable 1
       336. DDG (L)
       > https://ddg.gg
       # privacy,search engine
    Note that URL must precede tags.
5. **Add** a bookmark **without a title** (works for update too):

       $ buku -a https://ddg.gg search engine, privacy --title
6. **Edit and update** a bookmark from editor:

       $ buku -w 15012014
    This will open the existing bookmark's details in the editor for modifications. Environment variable `EDITOR` must be set.
7. **Update** existing bookmark at index 15012014 with new URL, tags and comments, fetch title from the web:

       $ buku -u 15012014 --url http://ddg.gg/ --tag web search, utilities -c Private search engine
8. **Fetch and update only title** for bookmark at 15012014:

       $ buku -u 15012014
9. **Update only comment** for bookmark at 15012014:

       $ buku -u 15012014 -c this is a new comment
    Applies to --url, --title and --tag too.
10. **Export** bookmarks tagged `tag 1` or `tag 2` to HTML, XBEL, Markdown, Orgfile or a new database:

       $ buku -e bookmarks.html --stag tag 1, tag 2
       $ buku -e bookmarks.xbel --stag tag 1, tag 2
       $ buku -e bookmarks.md --stag tag 1, tag 2
       $ buku -e bookmarks.org --stag tag 1, tag 2
       $ buku -e bookmarks.db --stag tag 1, tag 2
    All bookmarks are exported if search is not opted.
11. **Import** bookmarks from HTML, XBEL, Markdown or Orgfile:

        $ buku -i bookmarks.html
        $ buku -i bookmarks.xbel
        $ buku -i bookmarks.md
        $ buku -i bookmarks.org
        $ buku -i bookmarks.db
12. **Delete only comment** for bookmark at 15012014:

        $ buku -u 15012014 -c
    Applies to --title and --tag too. URL cannot be deleted without deleting the bookmark.
13. **Update** or refresh **full DB** with page titles from the web:

        $ buku -u
        $ buku -u --tacit (show only failures and exceptions)
    This operation can update the title or description fields of non-immutable bookmarks by parsing the fetched page. Fields are updated only if the fetched fields are non-empty. Tags remain untouched.
14. **Delete** bookmark at index 15012014:

        $ buku -d 15012014
        Index 15012020 moved to 15012014
    The last index is moved to the deleted index to keep the DB compact. Add `--tacit` to delete without confirmation.
15. **Delete all** bookmarks:

        $ buku -d
16. **Delete** a **range or list** of bookmarks:

        $ buku -d 100-200
        $ buku -d 100 15 200
17. **Search** bookmarks for **ANY** of the keywords `kernel` and `debugging` in URL, title or tags:

        $ buku kernel debugging
        $ buku -s kernel debugging
18. **Search** bookmarks with **ALL** the keywords `kernel` and `debugging` in URL, title or tags:

        $ buku -S kernel debugging
19. **Search** bookmarks **tagged** `general kernel concepts`:

        $ buku --stag general kernel concepts
20. **Search** for bookmarks matching **ANY** of the tags `kernel`, `debugging`, `general kernel concepts`:

        $ buku --stag kernel, debugging, general kernel concepts
21. **Search** for bookmarks matching **ALL** of the tags `kernel`, `debugging`, `general kernel concepts`:

        $ buku --stag kernel + debugging + general kernel concepts
22. **Search** for bookmarks matching any of the keywords `hello` or `world`, excluding the keywords `real` and `life`, matching both the tags `kernel` and `debugging`, but **excluding** the tags `general kernel concepts` and `books`:

        $ buku hello world --exclude real life --stag 'kernel + debugging - general kernel concepts, books'
23. **Search** for bookmarks with different tokens for each field, and print them out sorted by the tags (ascending) and URL (descending)

        $ buku --order +tags,-url --markers --sall 'global substring' '.title substring' ':url substring' :https '> description substring' '#partial,tags:' '#,exact,tags' '*another global substring'
24. List **all unique tags** alphabetically:

        $ buku --stag
25. Run a **search and update** the results:

        $ buku -s kernel debugging -u --tag + linux kernel
26. Run a **search and delete** the results:

        $ buku -s kernel debugging -d
27. **Encrypt or decrypt** DB with **custom number of iterations** (15) to generate key:

        $ buku -l 15
        $ buku -k 15
    The same number of iterations must be specified for one lock & unlock instance. Default is 8, if omitted.
28. **Show details** of bookmarks at index 15012014 and ranges 20-30, 40-50:

        $ buku -p 20-30 15012014 40-50
29. Show details of the **last 10 bookmarks**:

        $ buku -p -10
30. **Show all** bookmarks with real index from database:

        $ buku -p
        $ buku -p | more
31. **Replace tag** 'old tag' with 'new tag':

        $ buku --replace 'old tag' 'new tag'
32. **Delete tag** 'old tag' from DB:

        $ buku --replace 'old tag'
33. **Append (or delete) tags** 'tag 1', 'tag 2' to (or from) existing tags of bookmark at index 15012014:

        $ buku -u 15012014 --tag + tag 1, tag 2
        $ buku -u 15012014 --tag - tag 1, tag 2
34. **Open URL** at index 15012014 in browser:

        $ buku -o 15012014
35. List bookmarks with **no title or tags** for bookkeeping:

        $ buku -S blank
36. List bookmarks with **immutable title**:

        $ buku -S immutable
37. **Append, remove tags at prompt** (taglist index to the left, bookmark index to the right):

        // append tags at taglist indices 4 and 6-9 to existing tags in bookmarks at indices 5 and 2-3
        buku (? for help) g 4 9-6 >> 5 3-2
        // set tags at taglist indices 4 and 6-9 as tags in bookmarks at indices 5 and 2-3
        buku (? for help) g 4 9-6 > 5 3-2
        // remove all tags from bookmarks at indices 5 and 2-3
        buku (? for help) g > 5 3-2
        // remove tags at taglist indices 4 and 6-9 from tags in bookmarks at indices 5 and 2-3
        buku (? for help) g 4 9-6 << 5 3-2
38. List bookmarks with **colored output**:

        $ buku --colors oKlxm -p
39. Add a bookmark after following all permanent redirects, but only if the server doesn't respond with an error (and there's no network failure)

        $ buku --add http://wikipedia.net --url-redirect --del-error
        2. Wikipedia
           > https://www.wikipedia.org/
           + Wikipedia is a free online encyclopedia, created and edited by volunteers around the world and hosted by the Wikimedia Foundation.
40. Add a bookmark with tag `http redirect` if the server responds with a permanent redirect, or tag shaped like `http 404` on an error response:

        $ buku --add http://wikipedia.net/notfound --tag-redirect 'http redirect' --tag-error 'http {}'
        [ERROR] [404] Not Found
        3. Not Found
           > http://wikipedia.net/notfound
           # http 404,http redirect
41. Update all bookmarks matching the search by updating the URL if the server responds with a permanent redirect, deleting the bookmark if the server responds with HTTP error 400, 401, 402, 403, 404 or 500, or adding a tag shaped like `http:{}` in case of any other HTTP error; then export those affected by such changes into an HTML file, marking deleted records as well as old URLs for those replaced by redirect.

        $ buku -S ://wikipedia.net -u --url-redirect --tag-error --del-error 400-404,500 --export-on --export backup.html

42. Print out a single **random** bookmark:

        $ buku --random --print

43. Print out 3 **random** bookmarks **ordered** by netloc (reversed), title and url:

        $ buku --random 3 --order ,-netloc,title,+url --print

44. Print out a single **random** bookmark matching **search** criteria, and **export** into a Markdown file (in DB order):

        $ buku --random -S kernel debugging --export random.md

45. Swap positions of records #4 and #5:

        $ buku --swap 4 5

46. Update indices in all bookmarks to match specified order:

        $ buku --reorder ,-netloc,title,+url

47. More **help**:

        $ buku -h
        $ man buku

### Automation

Interactive workflows can be automated using expect. Issue [#368](https://github.com/jarun/buku/issues/368) has a working example on automating auto-import.

### Troubleshooting

#### Editor integration

You may encounter issues with GUI editors which maintain only one instance by default and return immediately from other instances. Use the appropriate editor option to block the caller when a new document is opened. See issue [#210](https://github.com/jarun/buku/issues/210) for gedit.

### Collaborators

- [Arun Prakash Jana](https://github.com/jarun)
- [Alexey Gulenko](https://github.com/LeXofLeviafan)
- [Rachmadani Haryono](https://github.com/rachmadaniHaryono)
- [Johnathan Jenkins](https://github.com/shaggytwodope)
- [SZ Lin](https://github.com/szlin)

Copyright © 2015-2026 [Arun Prakash Jana](mailto:engineerarun@gmail.com)
<br>
<p><a href="https://gitter.im/jarun/buku"><img src="https://img.shields.io/gitter/room/jarun/buku.svg?maxAge=2592000" alt="gitter chat" /></a></p>

### Contributions

Missing a feature? There's a rolling [ToDo List](https://github.com/jarun/buku/issues/484) with identified tasks. Contributions are welcome! Please follow the [PR guidelines](https://github.com/jarun/buku/wiki/PR-guidelines).

See also our documentation here <a href="http://buku.readthedocs.io/en/stable/?badge=stable"><img src="https://img.shields.io/badge/docs-stable-brightgreen.svg?maxAge=2592000" alt="Stable Docs" /></a>

### Related projects

- [bukubrow](https://github.com/SamHH/bukubrow), WebExtension for browser integration
- [oil](https://github.com/AndreiUlmeyda/oil), search-as-you-type cli front-end
- [buku_run](https://github.com/carnager/buku_run), rofi front-end
- [pinku](https://github.com/mosegontar/pinku), a Pinboard-to-buku import utility
- [buku-dmenu](https://gitlab.com/benoliver999/buku-dmenu), a simple bash dmenu wrapper
- [poku](https://github.com/shanedabes/poku), sync between Pocket and buku
- [Ebuku](https://github.com/flexibeast/ebuku), Emacs interface to buku
- [diigoku](https://github.com/dppdppd/diigoku), buku importer for Diigo
- [BukuBot](https://git.xmpp-it.net/sch/BukuBot), Chat bot for XMPP with an extended visual interface


### Videos

- [Buku: Take Your Bookmarks Everywhere You Go](https://www.youtube.com/embed/9HzEHrUBQXE)
- [Buku is a great open-source bookmark manager](https://www.youtube.com/embed/7VxgKMWm-J8)

### In the Press

- [2daygeek](http://www.2daygeek.com/buku-command-line-bookmark-manager-linux/)
- [Hacker Milk](https://hackermilk.blogspot.com/2020/01/how-to-manage-your-browsers-bookmarks.html)
- [It's F.O.S.S.](https://itsfoss.com/buku-command-line-bookmark-manager-linux/)
- [LinOxide](https://linoxide.com/linux-how-to/buku-browser-bookmarks-linux/)
- [LinuxUser Magazine 01/2017 Issue](http://www.linux-community.de/LU/2017/01/Das-Beste-aus-zwei-Welten)
- [Make Tech Easier](https://www.maketecheasier.com/manage-browser-bookmarks-ubuntu-command-line/)
- [One Thing Well](http://onethingwell.org/post/144952807044/buku)
- [Open Source For You](https://opensourceforu.com/2018/05/buku-a-bookmark-manager-in-the-command-line/)
- [ulno.net](https://ulno.net/blog/2017-07-19/of-bookmarks-tags-and-browsers/)


================================================
FILE: auto-completion/bash/buku-completion.bash
================================================
#
# Bash completion definition for buku.
#
# Author:
#   Arun Prakash Jana <engineerarun@gmail.com>
#

_buku () {
    COMPREPLY=()
    local IFS=$' \n'
    local cur=$2 prev=$3
    local -a opts opts_with_args
    opts=(
        -a --add
        --ai
        -c --comment
        --cached
        --colors
        -d --delete
        --deep
        --del-error
        -e --export
        --expand
        --export-on
        -f --format
        -h --help
        -i --import
        --immutable
        -j --json
        -k --unlock
        -l --lock
        --markers
        -n --count
        --nc
        --np
        -o --open
        --oa
        --offline
        --order
        -p --print
        -r --sreg
        --random
        --replace
        -s --sany
        -S --sall
        --shorten
        --suggest
        --swap
        -t --stag
        --tacit
        --tag
        --tag-error
        --tag-redirect
        --threads
        --title
        -u --update
        --url
        --url-redirect
        -V
        -v --version
        -w --write
        -x --exclude
        -g --debug
    )
    opts_with_arg=(
        -a --add
        --cached
        --colors
        -e --export
        --expand
        -f --format
        -i --import
        --immutable
        -n --count
        --order
        -r --sreg
        --replace
        -s --sany
        -S --sall
        --shorten
        --swap
        --threads
        --url
        -x --exclude
    )

    # Do not complete non option names
    [[ $cur == -* ]] || return 1

    # Do not complete when the previous arg is an option expecting an argument
    for opt in "${opts_with_arg[@]}"; do
        [[ $opt == $prev ]] && return 1
    done

    # Complete option names
    COMPREPLY=( $(compgen -W "${opts[*]}" -- "$cur") )
    return 0
}

complete -F _buku buku


================================================
FILE: auto-completion/fish/buku.fish
================================================
#
# Fish completion definition for buku.
#
# Author:
#   Arun Prakash Jana <engineerarun@gmail.com>
#
complete -c buku -s a -l add     -r --description 'add bookmark'
complete -c buku -l ai              --description 'auto-import bookmarks'
complete -c buku -s c -l comment    --description 'comment on bookmark'
complete -c buku -l cached       -r --description 'visit Wayback Machine cached version'
complete -c buku -l colors       -r --description 'set output colors in 5-letter string'
complete -c buku -s d -l delete     --description 'delete bookmark'
complete -c buku -l deep            --description 'search matching substrings'
complete -c buku -l del-error       --description 'delete bookmark on an HTTP error'
complete -c buku -s e -l export  -r --description 'export bookmarks'
complete -c buku -l expand       -r --description 'expand a tny.im shortened URL'
complete -c buku -l export-on       --description 'export bookmarks based on HTTP status'
complete -c buku -s f -l format  -r --description 'limit fields in print and JSON output'
complete -c buku -s h -l help       --description 'show help'
complete -c buku -s i -l import  -r --description 'import bookmarks'
complete -c buku -l immutable    -r --description 'disable title update from web'
complete -c buku -s j -l json       --description 'show JSON output for print and search'
complete -c buku -s k -l unlock     --description 'decrypt database'
complete -c buku -s l -l lock       --description 'encrypt database'
complete -c buku -l markers         --description 'enable search-with-markers mode (.>:#*)'
complete -c buku -s n -l count   -r --description 'results per page'
complete -c buku -l nc              --description 'disable color output'
complete -c buku -l np              --description 'non-interactive mode'
complete -c buku -s o -l open       --description 'open bookmarks in browser'
complete -c buku -l oa              --description 'browse all search results immediately'
complete -c buku -l offline         --description 'add a bookmark without connecting to web'
complete -c buku -l order        -r --description 'order by fields (+/- prefix for direction)'
complete -c buku -s p -l print      --description 'show bookmark details'
complete -c buku -s r -l sreg    -r --description 'match a regular expression'
complete -c buku -l random          --description 'random subset (of 1 or given amount)'
complete -c buku -l replace      -r --description 'replace a tag'
complete -c buku -s s -l sany    -r --description 'match any keyword'
complete -c buku -s S -l sall    -r --description 'match all keywords'
complete -c buku -l shorten      -r --description 'shorten a URL using tny.im'
complete -c buku -l suggest         --description 'show a list of similar tags'
complete -c buku -l swap         -r --description 'swap 2 given bookmark indices'
complete -c buku -s t -l stag       --description 'search by tag or show tags'
complete -c buku -l tacit           --description 'reduce verbosity'
complete -c buku -l tag             --description 'set tags, use + to append, - to remove'
complete -c buku -l tag-error       --description 'add tag on an HTTP error'
complete -c buku -l tag-redirect    --description 'add tag on a permanent redirect'
complete -c buku -l threads      -r --description 'max connections for full refresh'
complete -c buku -l title           --description 'set custom title'
complete -c buku -s u -l update     --description 'update bookmark'
complete -c buku -l url          -r --description 'set url'
complete -c buku -l url-redirect    --description 'update URL on a permanent redirect'
complete -c buku -s V               --description 'check latest upstream release'
complete -c buku -s v -l version    --description 'show program version'
complete -c buku -s w -l write      --description 'open editor'
complete -c buku -s x -l exclude -r --description 'exclude keywords'
complete -c buku -s g -l debug      --description 'enable debugging mode'


================================================
FILE: auto-completion/zsh/_buku
================================================
#compdef buku
#
# Completion definition for buku.
#
# Author:
#   Arun Prakash Jana <engineerarun@gmail.com>
#

setopt localoptions noshwordsplit noksharrays
local -a args
args=(
    '(-a --add)'{-a,--add}'[add bookmark]:URL tags'
    '(--ai)--ai[auto-import bookmarks]'
    '(-c --comment)'{-c,--comment}'[comment on bookmark]'
    '(--cached)--cached[visit Wayback Machine cached version]:index/url'
    '(--colors)--colors[set output colors in 5-letter string]:color string'
    '(-d --delete)'{-d,--delete}'[delete bookmark]'
    '(--deep)--deep[search matching substrings]'
    '(--del-error)--del-error[delete bookmark on an HTTP error]::HTTP codes'
    '(-e --export)'{-e,--export}'[export bookmarks]:html/md/db output file'
    '(--expand)--expand[expand a tny.im shortened URL]:index/shorturl'
    '(--export-on)--export-on[export bookmarks based on HTTP status]::HTTP codes'
    '(-f --format)'{-f,--format}'[limit fields in print and JSON output]:value'
    '(-h --help)'{-h,--help}'[show help]'
    '(-i --import)'{-i,--import}'[import bookmarks]:html/md/db input file'
    '(--immutable)--immutable[disable title update from web]:value'
    '(-j --json)'{-j,--json}'[show JSON output for print and search]::file'
    '(-k --unlock)'{-k,--unlock}'[decrypt database]'
    '(-l --lock)'{-l,--lock}'[encrypt database]'
    '(--markers)--markers[enable search-with-markers mode (.>:#*)]'
    '(-n --count)'{-n,--count}'[results per page]:value'
    '(--nc)--nc[disable color output]'
    '(--np)--np[noninteractive mode]'
    '(-o --open)'{-o,--open}'[open bookmarks in browser]'
    '(--oa)--oa[browse all search results immediately]'
    '(--offline)--offline[add a bookmark without connecting to web]'
    '(--order)--order[order by fields (+/- prefix for direction)]:fields'
    '(-p --print)'{-p,--print}'[show bookmark details]'
    '(-r --sreg)'{-r,--sreg}'[match a regular expression]:regex'
    '(--random)--random[random subset (of 1 or given amount)]::amount'
    '(--replace)--replace[replace a tag]:tag to replace'
    '(-s --sany)'{-s,--sany}'[match any keyword]:keyword(s)'
    '(-S --sall)'{-S,--sall}'[match all keywords]:keyword(s)'
    '(--shorten)--shorten[shorten a URL using tny.im]:index/url'
    '(--suggest)--suggest[show a list of similar tags]'
    '(--swap)--swap[swap 2 given bookmark indices]:index1 index2'
    '(-t --stag)'{-t,--stag}'[search by tag or show tags]'
    '(--tacit)--tacit[reduce verbosity]'
    '(--tag)--tag[set tags, use + to append, - to remove]'
    '(--tag-error)--tag-error[add tag on an HTTP error]::tag pattern'
    '(--tag-redirect)--tag-redirect[add tag on a permanent redirect]::tag pattern'
    '(--threads)--threads[max connections for full refresh]:value'
    '(--title)--title[set custom title]'
    '(-u --update)'{-u,--update}'[update bookmark]'
    '(--url)--url[set url]:url'
    '(--url-redirect)--url-redirect[update URL on a permanent redirect]'
    '(-V)-V[check latest upstream release]'
    '(-v --version)'{-v,--version}'[show program version]'
    '(-w --write)'{-w,--write}'[open editor]'
    '(-x --exclude)'{-x,--exclude}'[exclude keywords]:keyword(s)'
    '(-g --debug)'{-g,--debug}'[enable debugging mode]'
)
_arguments -S -s $args


================================================
FILE: buku.1
================================================
.TH "BUKU" "1" "07 Dec 2025" "Version 5.1" "User Commands"
.SH NAME
buku \- Bookmark manager like a text-based mini-web
.SH SYNOPSIS
.B buku [OPTIONS] [KEYWORD [KEYWORD ...]]
.SH DESCRIPTION
.B buku
is a command-line utility to store, tag, search and organize bookmarks.
.PP
.B Features
.PP
  * Store bookmarks with auto-fetched title, tags and description
  * Auto-import from Firefox, Google Chrome, Chromium, Vivaldi, Brave, and MS Edge
  * Open bookmarks and search results in browser
  * Browse cached page from the Wayback Machine
  * Text editor integration
  * Lightweight, clean interface, custom colors
  * Powerful search options (regex, substring...)
  * Continuous search with on the fly mode switch
  * Portable, merge-able database to sync between systems
  * Import/export bookmarks from/to HTML, XBEL, Markdown, RSS/Atom or Orgfile
  * Smart tag management using redirection (>>, >, <<)
  * Multi-threaded full DB refresh
  * Manual encryption support
  * Shell completion scripts, man page with handy examples
  * Privacy-aware (no unconfirmed user data collection)
  * Can be used as a Python library
  * Has a compation Web-application (Bukuserver) with an HTTP-based API (for personal use only)
.SH OPERATIONAL NOTES
.PP
.IP 1. 4
The database file is stored in:
  - \fI$BUKU_DEFAULT_DBDIR/bookmarks.db\fR, if BUKU_DEFAULT_DBDIR is defined (first preference), or
  - \fI$XDG_DATA_HOME/buku/bookmarks.db\fR, if XDG_DATA_HOME is defined (second preference), or
  - \fI$HOME/.local/share/buku/bookmarks.db\fR, if HOME is defined (third preference), or
  - \fI%APPDATA%\\buku\\bookmarks.db\fR, if you are on Windows and APPDATA is defined (fourth preference), or
  - the current directory.
.PP
.IP 2. 4
If the URL contains characters like ';', '&' or brackets they may be interpreted specially by the shell. To avoid it, add the URL within single or double quotes ('/").
.PP
.IP 3. 4
URLs are unique in DB. The same URL cannot be added twice.
.PP
.IP 4. 4
Bookmarks with immutable titles are listed with '(L)' after the title.
.PP
.IP 5. 4
\fBTags\fR:
  - Comma (',') is the tag delimiter in DB. A tag cannot have comma(s) in it. Tags are filtered (for unique tags) and sorted. Tags are stored in lower case and can be replaced, appended or deleted.
  - Page keywords having a word to comma ratio > 3 are appended to description rather than tags.
  - Parent folder (and subfolder) names are converted to all-lowercase tags during bookmarks HTML import.
  - Releases prior to v2.7 support both capital and lower cases in tags. From v2.7 all tags are stored in lowercase. An undocumented option --fixtags is introduced to modify the older tags. It also fixes another issue where the same tag appears multiple times in the tagset of a record. Run \fBbuku --fixtags\fR once.
  - Tags can be edited from the prompt very easily using '>>' (append), '>' (overwrite) and '<<' (remove) symbols. The LHS of the operands denotes the indices and ranges of tags to apply (as listed by --tag or key 't' at prompt) and the RHS denotes the actual DB indices and ranges of the bookmarks to apply the change to.
.PP
.IP 6. 4
\fBUpdate\fR operation:
  - If --title, --tag or --comment is passed without argument, clear the corresponding field from DB.
  - If --url is passed (and --title is omitted), update the title from web using the URL. Description is updated (if --comment is omitted). Tags remain untouched.
  - If indices are passed without any other options (--url, --title, --tag, --comment and --immutable), read the URLs from DB and update titles, description and append tags from web. Bookmarks marked immutable are skipped.
  - Can update bookmarks matching a search, when combined with any of the search options and no arguments to update are passed.
  - Additionally, --swap allows to modify records order (standalone operation).
.PP
.IP 7. 4
\fBDelete\fR operation:
  - When a record is deleted, the last record is moved to the index.
  - Delete doesn't work with range and indices provided together as arguments. It's an intentional decision to avoid extra sorting, in-range checks and to keep the auto-DB compaction functionality intact. On the same lines, indices are deleted in descending order.
  - Can delete bookmarks matching a search, when combined with any of the search options and no arguments to delete are passed.
.PP
.IP 8. 4
\fBSearch\fR works in mysterious ways:
  - Case-insensitive.
  - Matches words in URL, title and tags.
  - --sany : match any of the keywords in URL, title, description or tags. Default search option.
  - --sall : match all the keywords in URL, title, description or tags.
  - --deep : match \fBsubstrings\fR (`match` matches `rematched`) in URL, title, description and tags.
  - --markers : match each keyword to a \fBspecific\fR field, depending on its prefix.
  - --sreg : match a regular expression (ignores --deep).
  - --stag : search bookmarks by tags, or list all tags alphabetically with usage count (if no arguments). Delimit the list of tags in the query with `,` to search for bookmarks that match ANY of the listed tags. Delimit tags with `+` to search for bookmarks that match ALL of the listed tags. Note that `,` and `+` cannot be used together in the same search. Exclude bookmarks matching certain tags from the results by using ` - ` followed by the tags. Note that the ` - ` operator and the ` + ` delimiter must be space separated: ` - ` instead of `-` and ` + ` instead of `+`. This is to distinguish them from hyphenated tags (e.g., `some-tag-name`) and tags with '+'s (e.g., `some+tag+name`).
  - Search for keywords along with tag filtering is possible. Two searches are issued (one for keywords and another for tags) and the intersection of the 2 sets is returned as the resultset.
  - Search results are indexed incrementally. This index is different from actual database index of a bookmark record which is shown within '[]' after the title.
  - Results for \fIany\fR keyword matches are ordered by the number of keyword matches - results matching more keywords (\fImatch score\fR) will appear earlier in the list. Results having the same number of matches will be ranked by their record DB index. If only one keyword is searched, results will be ordered by DB index, since all matching records will have the same \fImatch score\fR.
  - Sorting order can be specified (for matches with same amount of matched keywords, if relevant). This option also works with regular printing/export.
.PP
.IP 9. 4
\fBImport\fR:
  - Auto-import looks in the default installation path and default user profile.
  - URLs starting with `place:`, `file://` and `apt:` are ignored during import.
  - Parent folder (and subfolder) names are automatically imported as tags if --tacit is used.
  - Tags are merged even if bookmark URL exists when --tacit is used.
  - JSON files exported from browser can be imported. Export to JSON is not supported.
.PP
.IP 10. 4
\fBEncryption\fR is optional and manual. AES256 algorithm is used. To use encryption, the database file should be unlocked (-k) before using \fBbuku\fR and locked (-l) afterwards. Between these 2 operations, the database file lies unencrypted on the disk, and NOT in memory. Also, note that the database file is \fBunencrypted on creation\fR.
.PP
.IP 11. 4
\fBEditor\fR support:
  - A single bookmark can be edited before adding. The editor can be set using the environment variable *EDITOR* or by explicitly specifying the editor. The latter takes precedence. If -a is used along with -w, the details are populated in the editor template.
  - In case of edit and update (a single bookmark), the existing record details are fetched from DB and populated in the editor template. The environment variable EDITOR must be set. Note that -u works independently of -w.
  - All lines beginning with "#" will be stripped. Then line 1 will be treated as the URL, line 2 will be the title, line 3 will be comma separated tags, and the rest of the lines will be parsed as descriptions.
.PP
.IP 12. 4
\fBProxy\fR support: please refer to the \fBENVIRONMENT\fR section.
.PP
.IP 13. 4
\fBAlternative DB file\fR:
  - The option \fB--db\fR (to specify an alternative database file location) is app-only. Manual usage is prone to issues arising from human error.
  - Note that this option is useful if you want to store the db file in cloud synced location. Another mechanism could be to have the db file synced and create a symlink to it at the default location.
  - When the argument to \fB--db\fR contains neither `.` nor directory separator, it's considered a \fIname\fR and is resolved as the matching file with `.db` extension within the default DB directory.
  - When invoked specifically as \fBbuku --db\fR, the program prints out the list of DB names in the default DB directory.
  - When running Bukuserver (webUI), alternative DB file can be specified via \fBBUKUSERVER_DB_FILE\fR environment variable. Additionally, the Bukuserver runner script supports switching between DB files within the default Buku DB folder.
  - In the interactive shell mode, the \fBDB\fR command can be used to similarly switch between DB files by name. (You can use non-standard extensions by specifying them, and switch directories by specifying a path – absolute or relative to the current DB. \fB~.\fR stands for the default database.)
.SH GENERAL OPTIONS
.TP
.BI \-a " " \--add " URL [+|-] [tag, ...]"
Bookmark
.I URL
along with comma-separated tags. A tag can have multiple words. (These tags \fBoverride\fR fetched tags, unless preceded with '+' or '-'.)
.TP
.BI \-u " " \--update " [...]"
Update fields of the bookmarks at specified indices in DB. If no arguments are specified, all titles and descriptions are refreshed from the web. Tags remain untouched. Works with update modifiers for the fields url, title, tag and comment. If only indices are passed without any edit options, titles and descriptions are fetched and updated (if not empty). Accepts hyphenated ranges and space-separated indices. Updates search results when used with search options, if no arguments.
.TP
.BI \-w " " \--write " [editor|index]"
Edit a bookmark in
.I editor
before adding it. To edit and update an existing bookmark, the
.I index
should be passed. In this case the environment variable EDITOR must be set. The last record is opened in EDITOR if index=-1.
.TP
.BI \-d " " \--delete " [...]"
Delete bookmarks. Accepts space-separated list of indices (e.g. 5 6 23 4 110 45) or a single hyphenated range (e.g. 100-200). Note that range and list don't work together. Deletes search results when combined with search options, if no arguments.
.TP
.BI \--retain-order
When deleting bookmarks, shift indices of multiple records instead of replacing the deleted record with the last one.
.TP
.BI \-v " " \--version
Show program version and exit.
.TP
.BI \-h " " \--help
Show program help and exit.
.SH EDIT OPTIONS
.TP
.BI \--url " [...]"
Specify the URL, works with --update only. Fetches and updates title if --title is not used.
.TP
.BI \--tag " [+|-] [...]"
Specify comma separated tags, works with --add, --update. Clears the tags, if no arguments passed. Appends or deletes tags, if list of tags is preceded by '+' or '-' respectively.
.TP
.BI \--title " [...]"
Manually specify the title, works with --add, --update. Omits or clears the title, if no arguments passed.
.TP
.BI \-c " " \--comment " [...]"
Add notes or description of the bookmark, works with --add, --update. Clears the comment, if no arguments passed.
.TP
.BI \--immutable " N"
Set the title, description and tags of a bookmark immutable during autorefresh. Works with --add, --update. N=1 sets the immutable flag, N=0 removes it. If omitted, bookmarks are added with N=0.
.TP
.BI \--swap " N M"
Swap two records at specified indices. This is a standalone operation (cannot be invoked along with any other).
.SH SEARCH OPTIONS
.TP
.BI \-s " " \--sany " keyword [...]"
Search bookmarks with ANY of the keyword(s) in URL, title or tags and show the results. Prompts to enter result number to open in browser. Note that the sequential result index is not the DB index. The DB index is shown within '[]' after the title.
.br
This is the default search option for positional arguments if no other search option is specified.
.TP
.BI \-S " " \--sall " keyword [...]"
Search bookmarks with ALL keywords in URL, title or tags and show the results. Behaviour same as --sany.
.br
Special keywords:
.br
"blank": list entries with empty title/tag
.br
"immutable": list entries with locked title
.br
NOTE: To search the keywords, use --sany
.TP
.BI \--deep
Search modifier to match substrings. Works with --sany, --sall.
.TP
.BI \--markers
Search modifier to match specific fields based on (optional) prefix markers (i.e. beginning of the keyword):
  - '.' : search in title
  - '>' : search in description
  - ':' : search in URL
  - '#' : search in tags (comma-separated, \fBpartial\fR matches; not affected by --deep)
  - '#,' : search in tags (comma-separated, \fBexact\fR matches; not affected by --deep)
  - '*' : search in all fields (same as no prefix)
.TP
.BI \-r " " \--sreg " expression"
Scan for a regular expression match.
.TP
.BI \-t " " \--stag " [tag [,|+] ...] [\- tag, ...]"
Search bookmarks by tags.
.br
Use ',' delimiter to find entries matching ANY of the tags
.br
Use ' + ' delimiter to find entries matching ALL of the tags. (Note that the ' + ' delimiter must be space separated)
.br
NOTE: Cannot combine ',' and '+' in the same search
.br
Use ' - ' to exclude bookmarks that match the tags that follow. (Note that the '-' operator must be space separated).
.br
List all tags alphabetically, if no arguments. The usage count (number of bookmarks having the tag) is shown within first brackets.
.TP
.BI \-x " " \--exclude " keyword [...]"
Exclude bookmarks matching the specified keywords. Works with --sany, --sall, --sreg and --stag.
.TP
.BI \--random " [N]"
Output random bookmarks out of the selection (1 unless amount is specified).
.TP
.BI \--order " fields [...]"
Order printed/exported records by the given fields (from DB or JSON) and/or netloc. You can specify sort direction for each by prepending '+'/'-' (default is '+').
.SH ENCRYPTION OPTIONS
.TP
.BI \-l " " \--lock " [N]"
Encrypt (lock) the DB file with
.I N
(> 0, default 8) hash passes to generate key.
.TP
.BI \-k " " \--unlock " [N]"
Decrypt (unlock) the DB file with
.I N
(> 0, default 8) hash passes to generate key.
.SH POWER OPTIONS
.TP
.BI \--ai
Auto-import bookmarks from Firefox, Google Chrome, Chromium, Vivaldi, Brave, and MS Edge browsers. (Firefox profile can be specified using environment variable FIREFOX_PROFILE.)
.TP
.BI \-e " " \--export " file"
Export bookmarks to Firefox bookmarks formatted HTML. Works with all search options.
.br
XBEL is used if
.I file
has extension '.xbel'.
.br
Markdown is used if
.I file
has extension '.md'. Markdown format: [title](url), 1 entry per line.
.br
Orgfile is used if
.I file
has extension '.org' Orgfile format: * [[url][title]], 1 entry per line.
.br
RSS is used if
.I file
has extension '.rss'/'.atom' RSS format: <entry> per bookmark with <title>, <link>, <category>, <content> elements
.br
A buku database is generated if
.I file
has extension '.db'.
.TP
.BI \-i " " \--import " file"
Import bookmarks from Firefox bookmarks formatted HTML.
.I file
is considered Firefox-exported JSON if it has '.json' extension, XBEL if it is '.xbel', Markdown (compliant with --export format) if it is '.md', Orgfile if the extension is '.org', RSS if the extension is '.rss'/'.atom' or another buku database if the extension is '.db'.
.TP
.BI \-p " " \--print " [...]"
Show details (DB index, URL, title, tags and comment) of bookmark record by DB index. If no arguments, all records with actual index from DB are shown. Accepts hyphenated ranges and space-separated indices. A negative value (introduced for convenience) behaves like the tail utility, e.g., -n shows the details of the last n bookmarks.
.TP
.BI \-f " " \--format " N"
Show selective monochrome output with specific fields. Works with --print. Search results honour the option when used along with --json. Useful for creating batch scripts.
.br
.I N
= 1, show only URL.
.br
.I N
= 2, show URL and tags in a single line.
.br
.I N
= 3, show only title.
.br
.I N
= 4, show URL, title and tags in a single line.
.br
.I N
= 5, show title and tags in a single line.
.br
To omit DB index from printed results, use N0, e.g., 10, 20, 30, 40, 50.
.TP
.BI \-j " " \--json
Output data formatted as JSON, works with --print output and search results.
.TP
.BI \--colors " COLORS"
Set output colors. Refer to the \fBCOLORS\fR section below for details.
.TP
.BI \--nc
Disable color output in all messages. Useful on terminals which can't handle ANSI color codes or scripted environments.
.TP
.BI \-n " " \--count " N"
Number of results to show per page (default 10).
.TP
.BI \--np
Do not show the prompt, run and exit.
.TP
.BI \-o " " \--open " [...]"
Open bookmarks by DB indices or ranges in browser. Open a random index if argument is omitted.
.TP
.BI \--oa
Open all search results immediately in the browser. Works best with --np. When used along with --update or --delete, URLs are opened in the browser first and then modified or deleted.
.TP
.BI \--default-scheme " scheme"
When opening a bookmark without a scheme in its URI, use this scheme (default http).
.TP
.BI \--replace " old new"
Replace
.I old
tag with
.I new
tag if both are passed; delete
.I old
tag if
.I new
tag is not specified.
.TP
.BI \--url-redirect
when fetching an URL, use the resulting URL from following \fBpermanent\fR redirects (when combined with --export, the old URL is included as additional metadata).
.TP
.BI \--tag-redirect " [tag]"
when fetching an URL that causes permanent redirect, add a
.I tag
in specified pattern (using 'http:{}' if not specified).
.TP
.BI \--tag-error " [tag]"
when fetching an URL that causes an HTTP error, add a
.I tag
in specified pattern (using 'http:{}' if not specified).
.TP
.BI \--del-error " [...]"
when fetching an URL causes any (given) HTTP error, delete/do not add it. (Use a parameter like '404' or '400-404,500')
.TP
.BI \--export-on " [...]"
export records affected by the above options, including removed info (requires --update and --export; specific HTTP response filter can be provided).
.TP
.BI \--reorder " order..."
update DB indices to match specified order (specified the same way as for --order)
.TP
.BI \--cached " index|URL"
Browse the latest cached version of the URL at DB
.I index
or an independent
.I URL
using the Wayback Machine. Useful for viewing the content of bookmarks which are not live any more.
.TP
.BI \--offline
Add a bookmark without connecting to the web.
.TP
.BI \--suggest
Show a list of similar tags to choose from when adding a new bookmark.
.TP
.BI \--tacit
Show lesser output. Reduces the verbosity of certain operations like add, update etc.
.TP
.BI \--nostdin
Do not attempt to read data from standard input e.g. when the program is not executed from a tty.
.TP
.BI \--threads
Maximum number of parallel network connection threads to use during full DB refresh. By default 4 connections are spawned.
.I N
can range from 1 to 10.
.TP
.BI \-V
Check the latest upstream version available. This is FYI. It is possible the latest upstream released version is still not available in your package manager as the process takes a while.
.TP
.BI \-g " " \--debug
Show debug information and additional logs.
.SH PROMPT KEYS
.TP
.BI "1-N"
Browse search results by indices and ranges.
.TP
.BI "O" " [id|range [...]]"
Try to open search results or indices (when not in a search context) in a GUI browser. Toggle try to open urls in a GUI based browser (even if BROWSER is set) if no arguments. Toggling is useful when trying to open bookmarks by DB index.
.TP
.BI "a"
Open all search results in browser.
.TP
.BI "s" " keyword [...]"
Search for records with ANY keyword.
.TP
.BI "S" " keyword [...]"
Search for records with ALL keywords.
.TP
.BI "d"
Toggle deep search to match substrings ('pen' matches 'opened').
.TP
.BI "m"
Search with markers - search string is split into keywords by prefix markers, which determine what field the keywords is searched in:
  - '.', '>' or ':' - title, description or URL
  - '#'/'#,' - tags (comma-separated, partial/full match)
  - '*' - all fields (can be omitted in the 1st keyword)

Note: tag marker is not affected by \fBd\fR (deep search)
.TP
.BI "v" " fields"
Change sorting order (default is '+index'). Multiple comma/space separated fields can be specified.
.TP
.BI "v!" " fields"
Update indices in DB to match specified order.
.TP
.BI "r" " expression"
Run a regular expression search.
.TP
.BI "t" " [...]"
Search bookmarks by a tag. List all tags alphabetically, if no arguments.
.TP
.BI "g" " taglist id|range [...] [>>|>|<<] [record id|range ...]"
Append, set, remove specific or all tags by indices and/or ranges to bookmark indices and/or ranges (see \fBEXAMPLES\fR section below). Search by space-separated taglist id(s) and/or range if records are omitted.
.TP
.BI "n"
Display the next page of search results.
.TP
.BI "N"
Display the previous page of search results.
.TP
.BI "o" " id|range [...]"
Browse bookmarks by indices and/or ranges.
.TP
.BI "p" " id|range [...]"
Print bookmarks by indices and/or ranges.
.TP
.BI "w" " [editor|id]"
Edit and add or update a bookmark.
.TP
.BI "c id"
Copy url at search result index to clipboard.
.TP
.BI "DB" " [name]"
If used without \fBname\fR, display list of available DBs (files with '.db' extension in the folder of the current DB). If used with \fBname\fR, switch to the specified DB.
You can omit file extension ('.db' will be used), and you can specify a path instead in order to switch a folder (if selected path is a folder, default filename is assumed).
If the specified DB file doesn't exist, it will be created. Note: you can use '~.' as a shortcut for default DB.
.TP
.BI "?"
Show help on prompt keys.
.TP
.BI "q, ^D, double Enter"
Exit buku.
.SH ENVIRONMENT
.TP
.BI "Completion scripts"
Shell completion scripts for Bash, Fish and Zsh can be found in:
.br
.I https://github.com/jarun/buku/blob/master/auto-completion
.TP
.BI BROWSER
Overrides the default browser. Refer to:
.br
.I http://docs.python.org/library/webbrowser.html
.TP
.BI EDITOR
If defined, will be used as the editor to edit bookmarks with option --write.
.TP
.BI https_proxy
If defined, will be used to access http and https resources through the configured proxy. Supported format:
.br
http[s]://[username:password@]proxyhost:proxyport/
.TP
.BI "GUI integration"
.B buku
can be integrated in a GUI environment with simple tweaks. Please refer to:
.br
.I https://github.com/jarun/buku/wiki/System-integration
.SH COLORS
\fBbuku\fR allows you to customize the color scheme via a five-letter string, reminiscent of BSD \fBLSCOLORS\fR. The five letters represent the colors of
.IP - 2
index
.PD 0 \" Change paragraph spacing to 0 in the list
.IP - 2
title
.IP - 2
URL
.IP - 2
description/comment/note
.IP - 2
tag
.PD 1 \" Restore paragraph spacing
.TP
respectively. The five-letter string is passed is as the argument to the \fB--colors\fR option, or as the value of the environment variable \fBBUKU_COLORS\fR.
.TP
We offer the following colors/styles:
.TS
tab(;) box;
l|l
-|-
l|l.
Letter;Color/Style
a;black
b;red
c;green
d;yellow
e;blue
f;magenta
g;cyan
h;white
i;bright black
j;bright red
k;bright green
l;bright yellow
m;bright blue
n;bright magenta
o;bright cyan
p;bright white
A-H;bold version of the lowercase-letter color
I-P;bold version of the lowercase-letter bright color
x;normal
X;bold
y;reverse video
Y;bold reverse video
.TE
.TP
.TP
The default colors string is \fIoKlxm\fR, which stands for
.IP - 2
bright cyan index
.PD 0 \" Change paragraph spacing to 0 in the list
.IP - 2
bold bright green title
.IP - 2
bright yellow URL
.IP - 2
normal description
.IP - 2
bright blue tag
.PD 1 \" Restore paragraph spacing
.TP
Note that
.IP - 2
Bright colors (implemented as \\x1b[90m - \\x1b[97m) may not be available in all color-capable terminal emulators;
.IP - 2
Some terminal emulators draw bold text in bright colors instead;
.IP - 2
Some terminal emulators only distinguish between bold and bright colors via a default-off switch.
.TP
Please consult the manual of your terminal emulator as well as \fIhttps://en.wikipedia.org/wiki/ANSI_escape_code\fR for details.

.SH EXAMPLES
.PP
.IP 1. 4
\fBEdit and add\fR a bookmark from editor:
.PP
.EX
.IP
.B buku -w
.br
.B buku -w 'gedit -w'
.br
.B buku -w 'macvim -f' -a https://ddg.gg search engine, privacy
.EE
.PP
.IP "" 4
The first command picks editor from the environment variable \fIEDITOR\fR. The second command opens gedit in blocking mode. The third command opens macvim with option -f and the URL and tags populated in template.
.PP
.IP 2. 4
\fBAdd\fR a simple bookmark:
.PP
.EX
.IP
.B buku --nostdin -a https://github.com/
.EE
.PP
.IP "" 4
In the output, >: url, +: comment, #: tags.
.PP
.IP 3. 4
\fBAdd\fR a bookmark with \fBtags\fR 'search engine' and 'privacy', \fBcomment\fR 'Search engine with perks', \fBfetch page title\fR from the web:
.PP
.EX
.IP
.B buku -a https://ddg.gg search engine, privacy -c Search engine with perks
.EE
.PP
.IP "" 4
In the output, >: url, +: comment, #: tags.
.PP
.IP 4. 4
\fBAdd\fR a bookmark with tags 'search engine' & 'privacy' and \fBimmutable custom title\fR 'DDG':
.PP
.EX
.IP
.B buku -a https://ddg.gg search engine, privacy --title 'DDG' --immutable 1
.EE
.PP
.IP "" 4
Note that URL must precede tags.
.PP
.IP 5. 4
\fBAdd\fR a bookmark \fBwithout a title\fR (works for update too):
.PP
.EX
.IP
.B buku -a https://ddg.gg search engine, privacy --title
.EE
.PP
.IP 6. 4
\fBEdit and update\fR a bookmark from editor:
.PP
.EX
.IP
.B buku -w 15012014
.EE
.PP
.IP "" 4
This will open the existing bookmark's details in the editor for modifications. Environment variable \fIEDITOR\fR must be set.
.PP
.IP 7. 4
\fBUpdate\fR existing bookmark at index 15012014 with new URL, tags and comments, fetch title from the web:
.PP
.EX
.IP
.B buku -u 15012014 --url http://ddg.gg/ --tag web search, utilities -c Private search engine
.EE
.PP
.IP 8. 4
\fBFetch and update only title\fR for bookmark at 15012014:
.PP
.EX
.IP
.B buku -u 15012014
.EE
.PP
.IP 9. 4
\fBUpdate only comment\fR for bookmark at 15012014:
.PP
.EX
.IP
.B buku -u 15012014 -c this is a new comment
.EE
.PP
.IP "" 4
Applies to --url, --title and --tag too.
.PP
.IP 10. 4
\fBExport\fR bookmarks tagged 'tag 1' or 'tag 2' to HTML, XBEL, Markdown, Orgfile or a new database:
.PP
.EX
.IP
.B buku -e bookmarks.html --stag tag 1, tag 2
.br
.B buku -e bookmarks.xbel --stag tag 1, tag 2
.br
.B buku -e bookmarks.md --stag tag 1, tag 2
.br
.B buku -e bookmarks.org --stag tag 1, tag 2
.br
.B buku -e bookmarks.db --stag tag 1, tag 2
.EE
.PP
.IP "" 4
All bookmarks are exported if search is not opted.
.PP
.IP 11. 4
\fBImport\fR bookmarks from HTML, XBEL, Markdown or Orgfile:
.PP
.EX
.IP
.B buku -i bookmarks.html
.br
.B buku -i bookmarks.xbel
.br
.B buku -i bookmarks.md
.br
.B buku -i bookmarks.db
.EE
.PP
.IP 12. 4
\fBDelete only comment\fR for bookmark at 15012014:
.PP
.EX
.IP
.B buku -u 15012014 -c
.EE
.PP
.IP "" 4
Applies to --title and --tag too. URL cannot be deleted without deleting the bookmark.
.PP
.IP 13. 4
\fBUpdate\fR or refresh \fBfull DB\fR with page titles from the web:
.PP
.EX
.IP
.B buku -u
.br
.B buku -u --tacit (show only failures and exceptions)
.EE
.PP
.IP "" 4
This operation can update the title or description fields of non-immutable bookmarks by parsing the fetched page. Fields are updated only if the fetched fields are non-empty. Tags remain untouched.
.PP
.IP 14. 4
\fBDelete\fR bookmark at index 15012014:
.PP
.EX
.IP
.B buku -d 15012014
.EE
.PP
.IP "" 4
The last index is moved to the deleted index to keep the DB compact. Add --tacit to delete without confirmation.
.PP
.IP 15. 4
\fBDelete all\fR bookmarks:
.PP
.EX
.IP
.B buku -d
.EE
.PP
.IP 16. 4
\fBDelete\fR a \fBrange or list\fR of bookmarks:
.PP
.EX
.IP
.B buku -d 100-200
.br
.B buku -d 100 15 200
.EE
.PP
.IP 17. 4
\fBSearch\fR bookmarks for \fBANY\fR of the keywords 'kernel' and 'debugging' in URL, title or tags:
.PP
.EX
.IP
.B buku kernel debugging
.br
.B buku -s kernel debugging
.EE
.PP
.IP 18. 4
\fBSearch\fR bookmarks with \fBALL\fR the keywords 'kernel' and 'debugging' in URL, title or tags:
.PP
.EX
.IP
.B buku -S kernel debugging
.EE
.PP
.IP 19. 4
\fBSearch\fR bookmarks \fBtagged\fR 'general kernel concepts':
.PP
.EX
.IP
.B buku --stag general kernel concepts
.EE
.PP
.IP 20. 4
\fBSearch\fR for bookmarks matching \fBANY\fR of the tags 'kernel', 'debugging', 'general kernel concepts':
.PP
.EX
.IP
.B buku --stag kernel, debugging, general kernel concepts
.EE
.PP
.IP 21. 4
\fBSearch\fR for bookmarks matching \fBALL\fR of the tags 'kernel', 'debugging', 'general kernel concepts':
.PP
.EX
.IP
.B buku --stag kernel + debugging + general kernel concepts
.EE
.PP
.IP 22. 4
\fBSearch\fR for bookmarks matching any of the keywords 'hello' or 'world', excluding the keywords 'real' and 'life', matching both the tags 'kernel' and 'debugging', but \fBexcluding\fR the tags 'general kernel concepts' and 'books':
.PP
.EX
.IP
.B buku hello world --exclude real life --stag 'kernel + debugging - general kernel concepts, books'
.EE
.PP
.IP 23. 4
\fBSearch\fR for bookmarks with different tokens for each field, and print them out sorted by the tags (ascending) and URL (descending)
.PP
.EX
.IP
.B buku --order +tags,-url --markers --sall 'global substring' '.title substring' ':url substring' :https '> description substring' '#partial,tags:' '#,exact,tags' '*another global substring'
.EE
.PP
.IP 24. 4
List \fBall unique tags\fR alphabetically:
.PP
.EX
.IP
.B buku --stag
.EE
.PP
.IP 25. 4
Run a \fBsearch and update\fR the results:
.PP
.EX
.IP
.B buku -s kernel debugging -u --tag + linux kernel
.EE
.PP
.IP 26. 4
Run a \fBsearch and delete\fR the results:
.PP
.EX
.IP
.B buku -s kernel debugging -d
.EE
.PP
.IP 27. 4
\fBEncrypt or decrypt\fR DB with \fBcustom number of iterations\fR (15) to generate key:
.PP
.EX
.IP
.B buku -l 15
.br
.B buku -k 15
.EE
.PP
.IP "" 4
The same number of iterations must be specified for one lock & unlock instance. Default is 8, if omitted.
.PP
.IP 28. 4
\fBShow details\fR of bookmarks at index 15012014 and ranges 20-30, 40-50:
.PP
.EX
.IP
.B buku -p 20-30 15012014 40-50
.EE
.PP
.IP 29. 4
Show details of the \fBlast 10 bookmarks\fR:
.PP
.EX
.IP
.B buku -p -10
.EE
.PP
.IP 30. 4
\fBShow all\fR bookmarks with real index from database:
.PP
.EX
.IP
.B buku -p
.br
.B buku -p | more
.EE
.PP
.IP 31. 4
\fBReplace tag\fR 'old tag' with 'new tag':
.PP
.EX
.IP
.B buku --replace 'old tag' 'new tag'
.EE
.PP
.IP 32. 4
\fBDelete tag\fR 'old tag' from DB:
.PP
.EX
.IP
.B buku --replace 'old tag'
.EE
.PP
.IP 33. 4
\fBAppend (or delete) tags\fR 'tag 1', 'tag 2' to (or from) existing tags of bookmark at index 15012014:
.PP
.EX
.IP
.B buku -u 15012014 --tag + tag 1, tag 2
.br
.B buku -u 15012014 --tag - tag 1, tag 2
.EE
.PP
.IP 34. 4
\fBOpen URL\fR at index 15012014 in browser:
.PP
.EX
.IP
.B buku -o 15012014
.EE
.PP
.IP 35. 4
List bookmarks with \fBno title or tags\fR for bookkeeping:
.PP
.EX
.IP
.B buku -S blank
.EE
.PP
.IP 36. 4
List bookmarks with \fBimmutable title\fR:
.PP
.EX
.IP
.B buku -S immutable
.EE
.PP
.IP 37. 4
\fBAppend, remove tags at prompt\fR (taglist index to the left, bookmark index to the right):
.PP
.EX
.IP
// append tags at taglist indices 4 and 6-9 to existing tags in bookmarks at indices 5 and 2-3
.br
.B buku (? for help) g 4 9-6 >> 5 3-2
.br
// set tags at taglist indices 4 and 6-9 as tags in bookmarks at indices 5 and 2-3
.br
.B buku (? for help) g 4 9-6 > 5 3-2
.br
// remove all tags from bookmarks at indices 5 and 2-3
.br
.B buku (? for help) g > 5 3-2
.br
// remove tags at taglist indices 4 and 6-9 from tags in bookmarks at indices 5 and 2-3
.br
.B buku (? for help) g 4 9-6 << 5 3-2
.EE
.PP
.IP 38. 4
List bookmarks with \fBcolored output\fR:
.PP
.EX
.IP
.B $ buku --colors oKlxm -p
.EE
.PP
.IP 39. 4
Add a bookmark after following all permanent redirects, but only if the server doesn't respond with an error (and there's no network failure)
.PP
.EX
.IP
.B buku --add http://wikipedia.net --url-redirect --del-error
.br
2. Wikipedia
.br
   > https://www.wikipedia.org/
.br
   + Wikipedia is a free online encyclopedia, created and edited by volunteers around the world and hosted by the Wikimedia Foundation.
.EE
.PP
.IP 40. 4
Add a bookmark with tag 'http redirect' if the server responds with a permanent redirect, or tag shaped like 'http 404' on an error response:
.PP
.EX
.IP
.B buku --add http://wikipedia.net/notfound --tag-redirect 'http redirect' --tag-error 'http {}'
.br
[ERROR] [404] Not Found
.br
3. Not Found
.br
   > http://wikipedia.net/notfound
.br
   # http 404,http redirect
.EE
.PP
.IP 41. 4
Update all bookmarks matching the search by updating the URL if the server responds with a permanent redirect, deleting the bookmark if the server responds with HTTP error 400, 401, 402, 403, 404 or 500, or adding a tag shaped like 'http:{}' in case of any other HTTP error; then export those affected by such changes into an HTML file, marking deleted records as well as old URLs for those replaced by redirect.
.PP
.EX
.IP
.B buku -S ://wikipedia.net -u --url-redirect --tag-error --del-error 400-404,500 --export-on --export backup.html
.EE
.PP
.IP 42. 4
Print out a single \fBrandom\fR bookmark:
.PP
.EX
.IP
.B buku --random
.EE
.PP
.IP 43. 4
Print out 3 \fBrandom\fR bookmarks \fBordered\fR by netloc (reversed), title and url:
.PP
.EX
.IP
.B buku --random 3 --order ,-netloc,title,+url
.EE
.PP
.IP 44. 4
Print out a single \fBrandom\fR bookmark matching \fBsearch\fR criteria, and \fBexport\fR into a Markdown file (in DB order):
.PP
.EX
.IP
.B buku --random -S kernel debugging --export random.md
.EE
.PP
.IP 45. 4
Swap positions of records #4 and #5:
.PP
.EX
.IP
.B buku --swap 4 5
.EE
.PP
.IP 46. 4
Update indices in all bookmarks to match specified order:
.PP
.EX
.IP
.B buku --reorder ,-netloc,title,+url
.EE
.PP


.SH AUTHOR
Arun Prakash Jana <engineerarun@gmail.com>
.SH HOME
.I https://github.com/jarun/buku
.SH WIKI
.I https://github.com/jarun/buku/wiki
.SH REPORTING BUGS
.I https://github.com/jarun/buku/issues
.SH LICENSE
Copyright \(co 2015-2026 Arun Prakash Jana <engineerarun@gmail.com>.
.PP
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
.br
This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.


================================================
FILE: buku.py
================================================
#!/usr/bin/env python3
#
# Bookmark management utility
#
# Copyright © 2015-2026 Arun Prakash Jana <engineerarun@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with buku.  If not, see <http://www.gnu.org/licenses/>.
from __future__ import annotations  # for |

import argparse
import calendar
import codecs
import collections
import contextlib
import email.message
import json
import locale
import logging
import os
import platform
import random
import re
import shutil
import signal
import sqlite3
import struct
import subprocess
import sys
import tempfile
import textwrap
import threading
import time
import unicodedata
import webbrowser
from enum import Enum
from itertools import chain
from functools import total_ordering
from subprocess import DEVNULL, PIPE, Popen
from typing import Any, Dict, List, Optional, Tuple, NamedTuple, TypeAlias, TypeVar
from collections.abc import Sequence, Set, Callable
from warnings import warn
import xml.etree.ElementTree as ET
from urllib.parse import urlparse  # urllib3.util.parse_url() encodes netloc

import urllib3
from bs4 import BeautifulSoup
from bs4.dammit import EncodingDetector
from urllib3.util import Retry, make_headers

try:
    from mypy_extensions import TypedDict
except ImportError:
    TypedDict = None  # type: ignore

__version__ = '5.1'
__author__ = 'Arun Prakash Jana <engineerarun@gmail.com>'
__license__ = 'GPLv3'

# Global variables
INTERRUPTED = False  # Received SIGINT
DELIM = ','  # Delimiter used to store tags in DB
SKIP_MIMES = {'.pdf', '.txt'}
PROMPTMSG = 'buku (? for help): '  # Prompt message string

strip_delim = lambda s, delim=DELIM, sub=' ': str(s).replace(delim, sub)
taglist = lambda ss: sorted(set(s.lower().strip() for s in ss if (s or '').strip()))
parse_order = lambda order: [s for ss in order for s in re.split(r'\s*,\s*', ss.strip()) if s]
like_escape = lambda s, c='`': s.replace(c, c+c).replace('_', c+'_').replace('%', c+'%')
split_by_marker = lambda s: re.split(r'\s+(?=[.:>#*])', s)

def taglist_str(tag_str, convert=None):
    tags = taglist(tag_str.split(DELIM))
    return delim_wrap(DELIM.join(tags if not convert else taglist(convert(tags))))

def filter_from(values, subset, *, exclude=False):
    subset, exclude = set(subset), bool(exclude)
    return [x for x in values if (x in subset) != exclude]


# Default format specifiers to print records
ID_STR = '%d. %s [%s]\n'
ID_DB_STR = '%d. %s'
MUTE_STR = '%s (L)\n'
URL_STR = '   > %s\n'
DESC_STR = '   + %s\n'
DESC_WRAP = '%s%s'
TAG_STR = '   # %s\n'
TAG_WRAP = '%s%s'

# Colormap for color output from "googler" project
COLORMAP = {k: '\x1b[%sm' % v for k, v in {
    'a': '30', 'b': '31', 'c': '32', 'd': '33',
    'e': '34', 'f': '35', 'g': '36', 'h': '37',
    'i': '90', 'j': '91', 'k': '92', 'l': '93',
    'm': '94', 'n': '95', 'o': '96', 'p': '97',
    'A': '30;1', 'B': '31;1', 'C': '32;1', 'D': '33;1',
    'E': '34;1', 'F': '35;1', 'G': '36;1', 'H': '37;1',
    'I': '90;1', 'J': '91;1', 'K': '92;1', 'L': '93;1',
    'M': '94;1', 'N': '95;1', 'O': '96;1', 'P': '97;1',
    'x': '0', 'X': '1', 'y': '7', 'Y': '7;1', 'z': '2',
}.items()}

# DB flagset values
[FLAG_NONE, FLAG_IMMUTABLE] = [0x00, 0x01]

FIELD_FILTER = {
    1: ('id', 'url'),
    2: ('id', 'url', 'tags'),
    3: ('id', 'title'),
    4: ('id', 'url', 'title', 'tags'),
    5: ('id', 'title', 'tags'),
    10: ('url',),
    20: ('url', 'tags'),
    30: ('title',),
    40: ('url', 'title', 'tags'),
    50: ('title', 'tags'),
}
ALL_FIELDS = ('id', 'url', 'title', 'desc', 'tags')
JSON_FIELDS = {'id': 'index', 'url': 'uri', 'desc': 'description'}

USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:145.0) Gecko/20100101 Firefox/145.0'
MYHEADERS = None  # Default dictionary of headers
MYPROXY = None  # Default proxy
TEXT_BROWSERS = ['elinks', 'links', 'links2', 'lynx', 'w3m', 'www-browser']
IGNORE_FF_BOOKMARK_FOLDERS = frozenset(["placesRoot", "bookmarksMenuFolder"])
PERMANENT_REDIRECTS = {301, 308}

SCHEME_HTTP = 'http'

IntSet: TypeAlias = Set[int] | range
Ints: TypeAlias = Sequence[int] | IntSet
IntOrInts: TypeAlias = int | Ints
_T = TypeVar('T')  # pylint: disable=typevar-name-mismatch
Values: TypeAlias = Sequence[_T] | Set[_T]

# Set up logging
LOGGER = logging.getLogger()
LOGDBG = LOGGER.debug
LOGERR = LOGGER.error

# Define the default path to ca-certificates
# In Linux distros with openssl, it is /etc/ssl/certs/ca-certificates.crt
# Fall back to use `certifi` otherwise
if sys.platform.startswith('linux') and os.path.isfile('/etc/ssl/certs/ca-certificates.crt'):
    CA_CERTS = '/etc/ssl/certs/ca-certificates.crt'
else:
    import certifi
    CA_CERTS = certifi.where()


class BukuCrypt:
    """Class to handle encryption and decryption of
    the database file. Functionally a separate entity.

    Involves late imports in the static functions but it
    saves ~100ms each time. Given that encrypt/decrypt are
    not done automatically and any one should be called at
    a time, this doesn't seem to be an outrageous approach.
    """

    # Crypto constants
    BLOCKSIZE = 0x10000  # 64 KB blocks
    SALT_SIZE = 0x20
    CHUNKSIZE = 0x80000  # Read/write 512 KB chunks

    @staticmethod
    def get_filehash(filepath):
        """Get the SHA256 hash of a file.

        Parameters
        ----------
        filepath : str
            Path to the file.

        Returns
        -------
        hash : bytes
            Hash digest of file.
        """

        from hashlib import sha256

        with open(filepath, 'rb') as fp:
            hasher = sha256()
            buf = fp.read(BukuCrypt.BLOCKSIZE)
            while len(buf) > 0:
                hasher.update(buf)
                buf = fp.read(BukuCrypt.BLOCKSIZE)

            return hasher.digest()

    @staticmethod
    def encrypt_file(iterations=8, dbfile=None, encfile=None, password=None, replace=True):
        """Encrypt the bookmarks database file.

        Parameters
        ----------
        iterations : int
            Number of iterations for key generation. (Defaults to 8)
        dbfile : str, optional
            Custom database file path (including filename). Fallback value is the default DB.
        encfile : str, optional
            Encoded dbfile. (Defaults to dbfile + '.enc')
        password : str, optional
            Password to use (if not provided, will be prompted from the user).
        replace : bool
            If True (default), the original file will be removed on success.
        """
        BukuCrypt(iterations, dbfile, encfile, password, replace)._encrypt_file()

    @staticmethod
    def decrypt_file(iterations=8, dbfile=None, encfile=None, password=None, replace=True):
        """Decrypt the bookmarks database file.

        Parameters
        ----------
        iterations : int
            Number of iterations for key generation. (Defaults to 8)
        dbfile : str, optional
            Custom database file path (including filename).
            The '.enc' suffix must be omitted.
        encfile : str, optional
            Encoded dbfile. (Defaults to dbfile + '.enc')
        password : str, optional
            Password to use (if not provided, will be prompted from the user).
        replace : bool
            If True (default), the original file will be removed on success.
        """
        BukuCrypt(iterations, dbfile, encfile, password, replace)._decrypt_file()

    def __init__(self, iterations=8, dbfile=None, encfile=None, password=None, replace=True):
        try:
            from getpass import getpass
            from hashlib import sha256

            from cryptography.hazmat.backends import default_backend
            from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
        except ImportError as e:
            raise RuntimeError('cryptography lib(s) missing') from e
        self._sha256, self._default_backend = sha256, default_backend
        self._Cipher, self._algorithms, self._modes = Cipher, algorithms, modes
        self._getpass = (getpass if sys.stdin.isatty() else (lambda: sys.stdin.readline().rstrip('\n')))

        if iterations < 1:
            raise RuntimeError('Iterations must be >= 1')

        self.iterations, self.password, self.replace = iterations, password, replace
        self.dbfile = dbfile or os.path.join(BukuDb.get_default_dbdir(), 'bookmarks.db')
        self.encfile = encfile or (self.dbfile + '.enc')

        self._db_exists = os.path.exists(self.dbfile)
        self._enc_exists = os.path.exists(self.encfile)
        if self._db_exists and self._enc_exists:
            raise RuntimeError('Both encrypted and flat DB files exist!')

    def _encrypt_file(self):
        if not self._db_exists:
            raise RuntimeError(f'{self.dbfile} missing. Already encrypted?')

        if not self.password:
            self.password = self._getpass()
            if not self.password:
                raise RuntimeError('Empty password')
            passconfirm = self._getpass()
            if not passconfirm:
                raise RuntimeError('Empty password')
            if self.password != passconfirm:
                raise RuntimeError('Passwords do not match')

        try:
            self._encrypt()
            if self.replace:
                os.remove(self.dbfile)
        except Exception as e:
            with contextlib.suppress(FileNotFoundError):
                os.remove(self.encfile)
            raise RuntimeError(e) from e

    def _decrypt_file(self):
        if not self._enc_exists:
            raise RuntimeError(f'{self.encfile} missing')

        self.password = self.password or self._getpass()
        if not self.password:
            raise RuntimeError('Empty password')

        try:
            enchash = self._decrypt()
            # Match hash of generated file with that of original DB file
            dbhash = BukuCrypt.get_filehash(self.dbfile)
            if dbhash != enchash:
                os.remove(self.dbfile)
                raise RuntimeError('Decryption failed')
            if self.replace:
                os.remove(self.encfile)
        except struct.error as e:
            with contextlib.suppress(FileNotFoundError):
                os.remove(self.dbfile)
            raise RuntimeError('Tainted file') from e
        except Exception as e:
            with contextlib.suppress(FileNotFoundError):
                os.remove(self.dbfile)
            raise RuntimeError(e) from e

    def _cipher(self, key, iv):
        return self._Cipher(self._algorithms.AES(key), self._modes.CBC(iv), backend=self._default_backend())

    def _key(self, salt):
        key = ('%s%s' % (self.password, salt.decode('utf-8', 'replace'))).encode('utf-8')
        for _ in range(self.iterations):
            key = self._sha256(key).digest()
        return key

    def _encrypt(self):
        # Get SHA256 hash of DB file
        dbhash = BukuCrypt.get_filehash(self.dbfile)

        # Generate random 256-bit salt and key
        salt = os.urandom(BukuCrypt.SALT_SIZE)
        iv = os.urandom(16)
        encryptor = self._cipher(self._key(salt), iv).encryptor()
        filesize = os.path.getsize(self.dbfile)

        with open(self.dbfile, 'rb') as infp, open(self.encfile, 'wb') as outfp:
            outfp.write(struct.pack('<Q', filesize))
            outfp.write(salt)
            outfp.write(iv)

            # Embed DB file hash in encrypted file
            outfp.write(dbhash)

            while chunk := infp.read(BukuCrypt.CHUNKSIZE):
                if len(chunk) % 16 != 0:
                    chunk = b'%b%b' % (chunk, b' ' * (16 - len(chunk) % 16))
                outfp.write(encryptor.update(chunk))

            outfp.write(encryptor.finalize())
        return dbhash

    def _decrypt(self):
        with open(self.encfile, 'rb') as infp:
            size = struct.unpack('<Q', infp.read(struct.calcsize('Q')))[0]

            # Read 256-bit salt and generate key
            salt = infp.read(32)
            iv = infp.read(16)
            decryptor = self._cipher(self._key(salt), iv).decryptor()

            # Get original DB file's SHA256 hash from encrypted file
            enchash = infp.read(32)

            with open(self.dbfile, 'wb') as outfp:
                while chunk := infp.read(BukuCrypt.CHUNKSIZE):
                    outfp.write(decryptor.update(chunk))
                outfp.write(decryptor.finalize())
                outfp.truncate(size)
        return enchash


@total_ordering
class SortKey:
    def __init__(self, value, ascending=True):
        self.value, self.ascending = value, bool(ascending)

    def __eq__(self, other):
        other = (other.value if isinstance(other, SortKey) else other)
        return self.value == other

    def __lt__(self, other):
        other = (other.value if isinstance(other, SortKey) else other)
        return self.value != other and ((self.value < other) == self.ascending)

    def __repr__(self):
        return ('+' if self.ascending else '-') + repr(self.value)


class FetchResult(NamedTuple):
    url: str                            # resulting URL after following PERMANENT redirects
    title: str = ''
    desc: str = ''
    keywords: str = ''
    mime: bool = False
    bad: bool = False
    fetch_status: Optional[int] = None  # None means no fetch occurred (e.g. due to a network error)

    def tag_redirect(self, pattern: str = None) -> str:
        return ('' if self.fetch_status not in PERMANENT_REDIRECTS else (pattern or 'http:{}').format(self.fetch_status))

    def tag_error(self, pattern: str = None) -> str:
        return ('' if (self.fetch_status or 0) < 400 else (pattern or 'http:{}').format(self.fetch_status))

    def tags(self, *, keywords: bool = True, redirect: bool | str = False, error: bool | str = False) -> str:
        _redirect = redirect and self.tag_redirect(None if redirect is True else redirect)
        _error = error and self.tag_error(None if error is True else error)
        return DELIM.join(taglist((keywords and self.keywords or '').split(DELIM) + [_redirect, _error]))


class BookmarkVar(NamedTuple):
    """Bookmark data named tuple"""
    id: int
    url: str
    title: Optional[str] = None
    tags_raw: str = ''
    desc: str = ''
    flags: int = FLAG_NONE

    @property
    def immutable(self) -> bool:
        return bool(self.flags & FLAG_IMMUTABLE)

    @property
    def tags(self) -> str:
        return self.tags_raw[1:-1]

    @property
    def taglist(self) -> List[str]:
        return [x for x in self.tags_raw.split(',') if x]

    @property
    def netloc(self) -> str:
        return get_netloc(self.url) or ''

bookmark_vars = lambda xs: ((x if isinstance(x, BookmarkVar) else BookmarkVar(*x)) for x in xs)


class BukuDb:
    """Abstracts all database operations.

    Attributes
    ----------
    conn : sqlite database connection.
    cur : sqlite database cursor.
    json : string
        Empty string if results should be printed in JSON format to stdout.
        Nonempty string if results should be printed in JSON format to file. The string has to be a valid path.
        None if the results should be printed as human-readable plaintext.
    field_filter : int
        Indicates format for displaying bookmarks. Default is 0.
    chatty : bool
        Sets the verbosity of the APIs. Default is False.
    """

    def __init__(
            self, json: Optional[str] = None, field_filter: int = 0, chatty: bool = False,
            dbfile: Optional[str] = None, colorize: bool = True, default_scheme: str = SCHEME_HTTP) -> None:
        """Database initialization API.

        Parameters
        ----------
        json : string
            Empty string if results should be printed in JSON format to stdout.
            Nonempty string if results should be printed in JSON format to file. The string has to be a valid path.
            None if the results should be printed as human-readable plaintext.
        field_filter : int
            Indicates format for displaying bookmarks. Default is 0.
        chatty : bool
            Sets the verbosity of the APIs. Default is False.
        colorize : bool
            Indicates whether color should be used in output. Default is True.
        default_scheme : str
            Scheme to assume if missing from bookmark's URI. Default is http.
        """

        self.json = json
        self.field_filter = field_filter
        self.chatty = chatty
        self.colorize = colorize
        self.conn, self.cur = BukuDb.initdb(dbfile, self.chatty)
        self.lock = threading.RLock()  # repeatable lock, only blocks *concurrent* access
        self._to_export = None  # type: Optional[Dict[str, str | BookmarkVar]]
        self._to_delete = None  # type: Optional[int | Sequence[int] | Set[int] | range]
        self.default_scheme = default_scheme

    @staticmethod
    def get_default_dbdir():
        """Determine the directory path where dbfile will be stored.

        If $BUKU_DEFAULT_DBDIR is specified, use it
        else if $XDG_DATA_HOME is defined, use $XDG_DATA_HOME/buku
        else if $HOME exists, use $HOME/.local/share/buku
        else if the platform is Windows and %APPDATA% exists, use %APPDATA%\\buku
        else use the current directory.

        Returns
        -------
        str
            Path to database file.
        """

        _get = os.environ.get
        if _get('BUKU_DEFAULT_DBDIR'):
            return os.path.abspath(_get('BUKU_DEFAULT_DBDIR'))
        home_locations = [
            _get('XDG_DATA_HOME'),
            _get('HOME') and os.path.join(_get('HOME'), '.local', 'share'),
            sys.platform == 'win32' and _get('APPDATA'),
        ]
        data_home = next((s for s in home_locations if s), None)
        return (os.path.join(data_home, 'buku') if data_home else os.getcwd())

    @staticmethod
    def initdb(dbfile: Optional[str] = None, chatty: bool = False) -> Tuple[sqlite3.Connection, sqlite3.Cursor]:
        """Initialize the database connection.

        Create DB file and/or bookmarks table if they don't exist.
        Alert on encryption options on first execution.

        Parameters
        ----------
        dbfile : str, optional
            Custom database file path (including filename).
        chatty : bool
            If True, shows informative message on DB creation.

        Returns
        -------
        tuple
            (connection, cursor).
        """

        if not dbfile:
            dbpath = BukuDb.get_default_dbdir()
            filename = 'bookmarks.db'
            dbfile = os.path.join(dbpath, filename)
        else:
            dbfile = os.path.abspath(dbfile)
            dbpath, filename = os.path.split(dbfile)

        try:
            if not os.path.exists(dbpath):
                os.makedirs(dbpath)
        except Exception as e:
            LOGERR(e)
            os._exit(1)

        db_exists = os.path.exists(dbfile)
        enc_exists = os.path.exists(dbfile + '.enc')

        if db_exists and not enc_exists:
            pass
        elif enc_exists and not db_exists:
            LOGERR('Unlock database first')
            sys.exit(1)
        elif db_exists and enc_exists:
            LOGERR('Both encrypted and flat DB files exist!')
            sys.exit(1)
        elif chatty:
            # not db_exists and not enc_exists
            print('DB file is being created at %s.\nYou should encrypt it.' % dbfile)

        try:
            # Create a connection
            conn = sqlite3.connect(dbfile, check_same_thread=False)
            conn.create_function('REGEXP', 2, regexp)
            conn.create_function('NETLOC', 1, get_netloc)
            cur = conn.cursor()

            # Create table if it doesn't exist
            # flags: designed to be extended in future using bitwise masks
            # Masks:
            #     0b00000001: set title immutable
            cur.execute('CREATE TABLE if not exists bookmarks ('
                        'id integer PRIMARY KEY, '
                        'URL text NOT NULL UNIQUE, '
                        'metadata text default \'\', '
                        'tags text default \',\', '
                        'desc text default \'\', '
                        'flags integer default 0)')
            conn.commit()
        except Exception as e:
            LOGERR('initdb(): %s', e)
            raise e

        return (conn, cur)

    @property
    def dbfile(self) -> str:
        return next(path for _, name, path in self.conn.execute('PRAGMA database_list') if name == 'main')

    @property
    def dbname(self) -> str:
        return os.path.basename(self.dbfile).removesuffix('.db')

    def _fetch(self, query: str, *args, lock: bool = True) -> List[BookmarkVar]:
        if not lock:
            self.cur.execute(query, args)
            return [BookmarkVar(*x) for x in self.cur.fetchall()]
        with self.lock:
            return self._fetch(query, *args, lock=False)

    def _fetch_first(self, query: str, *args, lock: bool = True) -> Optional[BookmarkVar]:
        rows = self._fetch(query + ' LIMIT 1', *args, lock=lock)
        return rows[0] if rows else None

    def _ordering(self, fields=['+id'], for_db=True) -> List[Tuple[str, bool]]:
        """Converts field list to ordering parameters (for DB query or entity list sorting).
        Fields are listed in priority order, with '+'/'-' prefix signifying ASC/DESC; assuming ASC if not specified.
        Other than names from DB, you can pass those from JSON export."""
        _field = lambda s: re.sub(r'^[+-]?(#)? *', r'\1', s).rstrip().lower()
        tags = {_field(s) for s in (fields or []) if re.fullmatch(r'[+-]?#[^,]+', s)}
        names = {'index': 'id', 'uri': 'url', 'description': 'desc', **({'title': 'metadata'} if for_db else {'metadata': 'title'})}
        valid = list(names) + list(names.values()) + ['tags', 'netloc'] + list(tags)
        _fields = [(_field(s), not s.startswith('-')) for s in (fields or [])]
        _fields = [(names.get(field, field), direction) for field, direction in _fields if field in valid]
        return _fields or [('id', True)]

    def _sort(self, records: List[BookmarkVar], fields=['+id'], ignore_case=True) -> List[BookmarkVar]:
        text_fields = (set() if not ignore_case else {'url', 'desc', 'title', 'tags', 'netloc'})
        get = lambda x, k: (k[1:] in x.taglist if k.startswith('#') else
                            getattr(x, k) if k not in text_fields else str(getattr(x, k) or '').lower())
        order = self._ordering(fields, for_db=False)
        return sorted(bookmark_vars(records), key=lambda x: [SortKey(get(x, k), ascending=asc) for k, asc in order])

    def _order(self, fields=['+id'], ignore_case=True) -> str:
        """Converts field list to SQL 'ORDER BY' parameters. (See also BukuDb._ordering().)"""
        text_fields = (set() if not ignore_case else {'url', 'desc', 'metadata', 'tags'})
        get = lambda field: ("tags LIKE '%,{0},%'".format(field[1:].replace("'", "''")) if field.startswith('#') else
                             'LOWER(NETLOC(url))' if field == 'netloc' else field if field not in text_fields else f'LOWER({field})')
        return ', '.join(f'{get(field)} {"ASC" if direction else "DESC"}' for field, direction in self._ordering(fields))

    def get_rec_all(self, *, lock: bool = True, order: List[str] = ['id'], ignore_case: bool = True):
        """Get all the bookmarks in the database.

        Parameters
        ----------
        lock : bool
            Whether to restrict concurrent access (True by default).
        order : list of str
            Order description (fields from JSON export or DB, prepended with '+'/'-' for ASC/DESC).
        ignore_case : bool
            Whether to ignore case when applying order (True by default).

        Returns
        -------
        list
            A list of tuples representing bookmark records.
        """

        return self._fetch(f'SELECT * FROM bookmarks ORDER BY {self._order(order, ignore_case)}', lock=lock)

    def get_rec_by_id(self, index: int, *, lock: bool = True) -> Optional[BookmarkVar]:
        """Get a bookmark from database by its ID.

        Parameters
        ----------
        index : int
            DB index of bookmark record.
        lock : bool
            Whether to restrict concurrent access (True by default).

        Returns
        -------
        BookmarkVar or None
            Bookmark data, or None if index is not found.
        """

        return self._fetch_first('SELECT * FROM bookmarks WHERE id = ?', index, lock=lock)

    def get_rec_all_by_ids(self, indices: Ints, *, lock: bool = True, order: List[str] = ['id']):
        """Get all the bookmarks in the database.

        Parameters
        ----------
        indices : int[] | int{} | range
            DB indices of bookmark records.
        lock : bool
            Whether to restrict concurrent access (True by default).
        order : list of str
            Order description (fields from JSON export or DB, prepended with '+'/'-' for ASC/DESC).

        Returns
        -------
        list
            A list of tuples representing bookmark records.
        """

        _order, placeholder = self._order(order), ', '.join(['?'] * len(indices))
        return indices and self._fetch(f'SELECT * FROM bookmarks WHERE id IN ({placeholder}) ORDER BY {_order}',
                                       *list(indices), lock=lock)

    def get_rec_id(self, url: str, *, lock: bool = True):
        """Check if URL already exists in DB.

        Parameters
        ----------
        url : str
            A URL to search for in the DB.
        lock : bool
            Whether to restrict concurrent access (True by default).

        Returns
        -------
        int
            DB index, or None if URL not found in DB.
        """

        row = self._fetch_first('SELECT * FROM bookmarks WHERE url = ?', url, lock=lock)
        return row and row.id

    def get_rec_ids(self, urls: Values[str], *, lock: bool = True):
        """Check if URL already exists in DB.

        Parameters
        ----------
        urls : str[] | str{}
            URLs to search for in the DB.
        lock : bool
            Whether to restrict concurrent access (True by default).

        Returns
        -------
        list
            A list of DB indices.
        """

        if not urls:
            return []
        if not lock:
            placeholder = ', '.join(['?'] * len(urls))
            self.cur.execute(f'SELECT id FROM bookmarks WHERE url IN ({placeholder})', list(urls))
            return [x[0] for x in self.cur.fetchall()]
        with self.lock:
            return self.get_rec_ids(urls, lock=False)

    def get_max_id(self, *, lock: bool = True) -> int:
        """Fetch the ID of the last record.

        Parameters
        ----------
        lock : bool
            Whether to restrict concurrent access (True by default).

        Returns
        -------
        int
            ID of the record if any record exists, else None.
        """

        if not lock:
            self.cur.execute('SELECT MAX(id) FROM bookmarks')
            return self.cur.fetchall()[0][0]
        with self.lock:
            return self.get_max_id(lock=False)

    def reorder(self, order: List[str], *, ignore_case=True):
        """Change indices of all records in DB to match the specified order.

        Parameters
        ----------
        order : list of str
            Order description (fields from JSON export or DB, prepended with '+'/'-' for ASC/DESC).
        ignore_case : bool
            Whether to ignore case when applying order (True by default).
        """
        with self.lock:
            sorted_urls = [x.url for x in self.get_rec_all(lock=False, order=order, ignore_case=ignore_case)]
            self.cur.execute('UPDATE bookmarks SET id = -id')
            for idx, url in enumerate(sorted_urls, start=1):
                self.cur.execute('UPDATE bookmarks SET id = ? WHERE url = ?', (idx, url))
            self.conn.commit()
            self.cur.execute('VACUUM')

    def add_rec(
            self,
            url: str,
            title_in: Optional[str] = None,
            tags_in: Optional[str] = None,
            desc: Optional[str] = None,
            immutable: bool = False,
            delay_commit: bool = False,
            fetch: bool = True,
            url_redirect: bool = False,
            tag_redirect: bool | str = False,
            tag_error: bool | str = False,
            del_error: Optional[IntSet] = None,
            tags_fetch: bool = True,
            tags_except: Optional[str] = None) -> int:
        """Add a new bookmark.

        Parameters
        ----------
        url : str
            URL to bookmark.
        title_in : str, optional
            Title to add manually. Default is None.
        tags_in : str, optional
            Comma-separated tags to add manually, instead of fetching them. Default is None.
        tags_except : str, optional
            These are removed from the resulting tags list. Default is None.
        tags_fetch : bool
            True if tags parsed from the fetched page should be included. Default is True.
        desc : str, optional
            Description of the bookmark. Default is None.
        immutable : bool
            Indicates whether to disable title fetch from web. Default is False.
        delay_commit : bool
            True if record should not be committed to the DB,
            leaving commit responsibility to caller. Default is False.
        fetch : bool
            Fetch page from web and parse for data. Required fetch-status params to take effect.
        url_redirect : bool
            Bookmark the URL produced after following all PERMANENT redirects.
        tag_redirect : bool | str
            Adds a tag by the given pattern if the url resolved to a PERMANENT
            redirect. (True means the default pattern 'http:{}'.)
        tag_error : bool | str
            Adds a tag by the given pattern if the url resolved to a HTTP error.
            (True means the default pattern 'http:{}'.)
        del_error : int{} | range, optional
            Do not add the bookmark if HTTP response status is in the given set or range.
            Also prevents the bookmark from being added on a network error.

        Returns
        -------
        int
            DB index of new bookmark on success, None on failure.
        """

        # Return error for empty URL
        if not url:
            LOGERR('Invalid URL')
            return None

        # Ensure that the URL does not exist in DB already
        id = self.get_rec_id(url)
        if id:
            LOGERR('URL [%s] already exists at index %d', url, id)
            return None

        if fetch:
            # Fetch data
            result = fetch_data(url)
            if result.bad:
                print('Malformed URL\n')
            elif result.mime:
                LOGDBG('HTTP HEAD requested')
            elif not result.title and title_in is None:
                print('No title\n')
            else:
                LOGDBG('Title: [%s]', result.title)
        else:
            result = FetchResult(url, fetch_status=200)
            LOGDBG('ptags: [%s]', result.tags(redirect=tag_redirect, error=tag_error))

        url = (result.url if url_redirect else url)
        title = (title_in if title_in is not None else result.title)

        # Fix up tags, if broken
        tags_exclude = set(taglist((tags_except or '').split(DELIM)))
        tags_fetched = result.tags(keywords=tags_fetch, redirect=tag_redirect, error=tag_error)
        tags = taglist_str((tags_in or '') + DELIM + tags_fetched,
                           lambda ss: [s for s in ss if s not in tags_exclude])
        LOGDBG('tags: [%s]', tags)

        # Process description
        desc = (desc if desc is not None else result.desc) or ''

        try:
            assert not del_error or result.fetch_status is not None, 'Network error'
            assert not del_error or result.fetch_status not in del_error, f'HTTP error {result.fetch_status}'
            flagset = FLAG_NONE
            if immutable:
                flagset |= FLAG_IMMUTABLE

            qry = 'INSERT INTO bookmarks(URL, metadata, tags, desc, flags) VALUES (?, ?, ?, ?, ?)'
            with self.lock:
                self.cur.execute(qry, (url, title, tags, desc, flagset))
                if not delay_commit:
                    self.conn.commit()
                if self.chatty:
                    self.print_rec(self.cur.lastrowid)
                return self.cur.lastrowid
        except Exception as e:
            LOGERR('add_rec(): %s', e)
            return None

    def append_tag_at_index(self, index, tags_in, delay_commit=False):
        """Append tags to bookmark tagset at index.

        Parameters
        ----------
        index : int | int[] | int{} | range, optional
            DB index of the record. 0 or empty indicates all records.
        tags_in : str
            Comma-separated tags to add manually.
        delay_commit : bool
            True if record should not be committed to the DB,
            leaving commit responsibility to caller. Default is False.

        Returns
        -------
        bool
            True on success, False on failure.
        """

        if tags_in is None or tags_in == DELIM:
            return True
        indices = (None if not index else [index] if isinstance(index, int) else index)

        with self.lock:
            if not indices:
                resp = read_in('Append the tags to ALL bookmarks? (y/n): ')
                if resp != 'y':
                    return False

                self.cur.execute('SELECT id, tags FROM bookmarks ORDER BY id ASC')
            else:
                placeholder = ', '.join(['?'] * len(indices))
                self.cur.execute(f'SELECT id, tags FROM bookmarks WHERE id IN ({placeholder}) ORDER BY id ASC', tuple(indices))

            resultset = self.cur.fetchall()
            if resultset:
                query = 'UPDATE bookmarks SET tags = ? WHERE id = ?'
                for row in resultset:
                    tags = row[1] + tags_in[1:]
                    tags = parse_tags([tags])
                    self.cur.execute(query, (tags, row[0],))
                    if self.chatty and not delay_commit:
                        self.print_rec(row[0])
            else:
                return False

            if not delay_commit:
                self.conn.commit()

        return True

    def delete_tag_at_index(self, index, tags_in, delay_commit=False, chatty=True):
        """Delete tags from bookmark tagset at index.

        Parameters
        ----------
        index : int | int[] | int{} | range, optional
            DB index of bookmark record. 0 or empty indicates all records.
        tags_in : str
            Comma-separated tags to delete manually.
        delay_commit : bool
            True if record should not be committed to the DB,
            leaving commit responsibility to caller. Default is False.
        chatty: bool
            Skip confirmation when set to False.

        Returns
        -------
        bool
            True on success, False on failure.
        """

        if tags_in is None or tags_in == DELIM:
            return True

        tags_to_delete = tags_in.strip(DELIM).split(DELIM)
        indices = (None if not index else [index] if isinstance(index, int) else index)

        if len(indices or []) != 1:
            if not indices and chatty:
                resp = read_in('Delete the tag(s) from ALL bookmarks? (y/n): ')
                if resp != 'y':
                    return False

            query = "UPDATE bookmarks SET tags = replace(tags, ?, ?) WHERE tags LIKE ? ESCAPE '`'"
            if indices:
                query += ' AND id IN ({})'.format(', '.join(['?'] * len(indices)))

            count = 0
            with self.lock:
                for tag in tags_to_delete:
                    tag = delim_wrap(tag)
                    args = (tag, DELIM, '%'+like_escape(tag, '`')+'%') + tuple(indices or [])
                    self.cur.execute(query, args)
                    count += self.cur.rowcount

                if count > 0 and not delay_commit:
                    self.conn.commit()
                    if self.chatty:
                        print('%d record(s) updated' % count)

            return True

        # Process a single index
        # Use SELECT and UPDATE to handle multiple tags at once
        with self.lock:
            query = 'SELECT id, tags FROM bookmarks WHERE id = ? LIMIT 1'
            self.cur.execute(query, list(indices))
            resultset = self.cur.fetchall()
            if not resultset:
                return False

            query = 'UPDATE bookmarks SET tags = ? WHERE id = ?'
            for row in resultset:
                tags = row[1]

                for tag in tags_to_delete:
                    tags = tags.replace(delim_wrap(tag), DELIM)

                self.cur.execute(query, (parse_tags([tags]), row[0],))
                if self.chatty and not delay_commit:
                    self.print_rec(row[0])

                if not delay_commit:
                    self.conn.commit()

        return True

    def update_rec(
            self,
            index: Optional[IntOrInts],
            url: Optional[str] = None,
            title_in: Optional[str] = None,
            tags_in: Optional[str] = None,
            desc: Optional[str] = None,
            immutable: Optional[bool] = None,
            threads: int = 4,
            url_redirect: bool = False,
            tag_redirect: bool | str = False,
            tag_error: bool | str = False,
            del_error: Optional[IntSet] = None,
            export_on: Optional[IntSet] = None,
            retain_order: bool = False) -> bool:
        """Update an existing record at (each) index.

        Update all records if index is 0 or empty, and url is not specified.
        URL is an exception because URLs are unique in DB.

        Parameters
        ----------
        index : int | int[] | int{} | range, optional
            DB index(es) of record(s). 0 or empty value indicates all records.
        url : str, optional
            Bookmark address.
        title_in : str, optional
            Title to add manually.
        tags_in : str, optional
            Comma-separated tags to add manually. Must start and end with comma.
            Prefix with '+,' to append to current tags.
            Prefix with '-,' to delete from current tags.
        desc : str, optional
            Description of bookmark.
        immutable : bool, optional
            Disable title fetch from web if True. Default is None (no change).
        threads : int
            Number of threads to use to refresh full DB. Default is 4.
        url_redirect : bool
            Update the URL to one produced after following all PERMANENT redirects.
            (This could fail if the new URL is bookmarked already.)
        tag_redirect : bool | str
            Adds a tag by the given pattern if the url resolved to a PERMANENT
            redirect. (True means the default pattern 'http:{}'.)
        tag_error : bool | str
            Adds a tag by the given pattern if the url resolved to a HTTP error.
            (True means the default pattern 'http:{}'.)
        del_error : int{} | range, optional
            Delete the bookmark if HTTP response status is in the given set or range.
            Does NOT cause deletion of the bookmark on a network error.
        export_on : int{} | range, optional
            Limit the export to URLs returning one of given HTTP codes; store old URLs.
        retain_order : bool
            If True, bookmark deletion will not result in their order being changed
            (multiple indices will be updated instead).

        Returns
        -------
        bool
            True on success, False on failure. (Deletion by del_error counts as success.)
        """

        arguments = []  # type: List[Any]
        query = 'UPDATE bookmarks SET'
        tag_modified = False
        ret = True
        indices = (None if not index else [index] if isinstance(index, int) else index)
        index = indices and list(indices or [])[0]
        single = len(indices or []) == 1
        export_on, self._to_export = (export_on or set()), ({} if export_on else None)
        tags_in = (tags_in or None if not tags_in or re.match('[+-],', tags_in) else delim_wrap(tags_in))

        if url and not single:
            LOGERR('All URLs cannot be same')
            return False

        if tags_in in ('+,', '-,'):
            LOGERR('Please specify a tag')
            return False

        if indices and min(indices) > (self.get_max_id() or 0):  # none of the indices exist in DB?
            return False

        # Update description if passed as an argument
        if desc is not None:
            query += ' desc = ?,'
            arguments += (desc,)

        # Update immutable flag if passed as argument
        if immutable is not None:
            if immutable:
                query += ' flags = flags | ?,'
                arguments += (FLAG_IMMUTABLE,)
            else:
                query += ' flags = flags & ?,'
                arguments += (~FLAG_IMMUTABLE,)

        # Update title
        #
        # 1. If --title has no arguments, delete existing title
        # 2. If --title has arguments, update existing title
        # 3. If --title option is omitted at cmdline:
        #    If URL is passed, update the title from web using the URL
        # 4. If no other argument (url, tag, comment, immutable) passed,
        #    update title from web using DB URL (if title is mutable)
        fetch_title = {url, title_in, tags_in, desc, immutable} == {None}
        network_test = url_redirect or tag_redirect or tag_error or del_error or export_on or fetch_title
        if url and title_in is None:
            network_test = False
            _url = url or self.get_rec_by_id(index).url
            result = fetch_data(_url)
            if result.bad:
                print('Malformed URL')
            elif result.mime:
                LOGDBG('HTTP HEAD requested')
            elif not result.title:
                print('No title')
            else:
                LOGDBG('Title: [%s]', result.title)

            if result.desc and not desc:
                query += ' desc = ?,'
                arguments += (result.desc,)

            if url_redirect and result.url != _url:
                url = result.url

            if result.fetch_status in export_on:  # storing the old URL
                self._to_export[url or _url] = _url
        else:
            result = FetchResult(url, title_in)

        if result.title is not None:
            query += ' metadata = ?,'
            arguments += (result.title,)

        # Update URL if passed as argument
        if url:
            query += ' URL = ?,'
            arguments += (url,)

        if result.fetch_status in (del_error or []):
            if result.fetch_status in export_on:  # storing the old record
                self._to_export[url] = self.get_rec_by_id(index)
            LOGERR('HTTP error %s', result.fetch_status)
            return self.delete_rec(index, retain_order=retain_order)

        if not indices and (arguments or tags_in):
            resp = read_in('Update ALL bookmarks? (y/n): ')
            if resp != 'y':
                return False

        if network_test:  # doing this before updates to backup records to-be-deleted in their original state
            custom_tags = (tags_in if (tags_in or '').startswith(DELIM) else None)
            ret = ret and self.refreshdb(indices, threads, url_redirect=url_redirect, tag_redirect=tag_redirect,
                                         tag_error=tag_error, del_error=del_error, export_on=export_on,
                                         update_title=fetch_title, custom_url=url, custom_tags=custom_tags, delay_delete=True)

        # Update tags if passed as argument
        _tags = result.tags(keywords=False, redirect=tag_redirect, error=tag_error)
        if tags_in or _tags:
            if not tags_in or tags_in.startswith('+,'):
                tags = taglist_str((tags_in or '')[1:] + _tags)
                chatty = self.chatty
                self.chatty = False
                ret = self.append_tag_at_index(indices, tags)
                self.chatty = chatty
                tag_modified = True
            elif tags_in.startswith('-,'):
                chatty = self.chatty
                self.chatty = False
                ret = self.delete_tag_at_index(indices, tags_in[1:])
                if _tags:
                    self.append_tag_at_index(indices, _tags)
                self.chatty = chatty
                tag_modified = True
            elif not network_test:  # rely on custom_tags to avoid overwriting fetch-status tags
                query += ' tags = ?,'
                arguments += (taglist_str(tags_in + _tags),)

        if not arguments:  # no arguments => nothing to update
            if (tag_modified or network_test) and self.chatty:
                self.print_rec(indices)
            self.commit_delete(retain_order=retain_order)
            return ret

        query = query[:-1]
        if indices:  # Only specified indices
            query += ' WHERE id IN ({})'.format(', '.join(['?'] * len(indices)))
            arguments += tuple(indices)

        LOGDBG('update_rec query: "%s", args: %s', query, arguments)

        with self.lock:
            try:
                self.cur.execute(query, arguments)
                self.conn.commit()
                if self.cur.rowcount > 0 and self.chatty:
                    self.print_rec(index)
                elif self.cur.rowcount == 0:
                    if single:
                        LOGERR('No matching index %d', index)
                    else:
                        LOGERR('No matches found')
                    return False
            except sqlite3.IntegrityError:
                LOGERR('URL already exists')
                return False
            except sqlite3.OperationalError as e:
                LOGERR(e)
                return False
            finally:
                self.commit_delete(retain_order=retain_order)

        return True

    def refreshdb(
            self,
            index: Optional[IntOrInts],
            threads: int,
            url_redirect: bool = False,
            tag_redirect: bool | str = False,
            tag_error: bool | str = False,
            del_error: Optional[IntSet] = None,
            export_on: Optional[IntSet] = None,
            update_title: bool = True,
            custom_url: Optional[str] = None,
            custom_tags: Optional[str] = None,
            delay_delete: bool = False,
            retain_order: bool = False) -> bool:
        """Refresh ALL (or specified) records in the database.

        Fetch title for each bookmark from the web and update the records.
        Doesn't update the title if fetched title is empty.

        Notes
        -----
            This API doesn't change DB index, URL or tags of a bookmark.
            (Unless one or more fetch-status parameters are supplied.)
            This API is verbose.

        Parameters
        ----------
        index : int | int[] | int{} | range, optional
            DB index(es) of record(s) to update. 0 or empty value indicates all records.
        threads: int
            Number of threads to use to refresh full DB. Default is 4.
        url_redirect : bool
            Update the URL to one produced after following all PERMANENT redirects.
            (This could fail if the new URL is bookmarked already.)
        tag_redirect : bool | str
            Adds a tag by the given pattern if the url resolved to a PERMANENT
            redirect. (True means the default pattern 'http:{}'.)
        tag_error : bool | str
            Adds a tag by the given pattern if the url resolved to a HTTP error.
            (True means the default pattern 'http:{}'.)
        del_error : int{} | range, optional
            Delete the bookmark if HTTP response status is in the given set or range.
        export_on : int{} | range, optional
            Limit the export to URLs returning one of given HTTP codes; store old URLs.
        update_title : bool
            Update titles/descriptions. (Can be turned off for network testing.)
        custom_url : str, optional
            Override URL to fetch. (Use for network testing of a single record before updating it.)
        custom_tags : str, optional
            Overwrite all tags. (Use to combine network testing with tags overwriting.)
        delay_delete : bool
            Delay scheduled deletions by del_error. (Use for network testing during update.)
        retain_order : bool
            If True, bookmark deletion will not result in their order being changed
            (multiple indices will be updated instead).

        Returns
        -------
        bool
            True on success, False on failure. (Deletion by del_error counts as success.)
        """

        indices = (None if not index else [index] if isinstance(index, int) else index)
        index = indices and list(indices)[0]
        export_on, self._to_export = (export_on or set()), ({} if export_on else None)
        self._to_delete = []

        if not update_title and not (url_redirect or tag_redirect or tag_error or del_error or export_on):
            LOGERR('Noop update request')
            return False
        if custom_url and len(indices or []) != 1:
            LOGERR('custom_url is only supported for a singular index')
            return False

        with self.lock:
            if not indices:
                self.cur.execute('SELECT id, url, tags, flags FROM bookmarks ORDER BY id ASC')
            else:
                placeholder = ', '.join(['?'] * len(indices))
                self.cur.execute(f'SELECT id, url, tags, flags FROM bookmarks WHERE id IN ({placeholder}) ORDER BY id ASC',
                                 tuple(indices))

            resultset = self.cur.fetchall()
            recs = len(resultset)
            if not recs:
                LOGERR('No matching index or title immutable or empty DB')
                return False

        # Set up strings to be printed
        if self.colorize:
            bad_url_str = '\x1b[1mIndex %d: Malformed URL\x1b[0m\n'
            mime_str = '\x1b[1mIndex %d: HTTP HEAD requested\x1b[0m\n'
            blank_url_str = '\x1b[1mIndex %d: No title\x1b[0m\n'
            success_str = 'Title: [%s]\n\x1b[92mIndex %d: updated\x1b[0m\n'
        else:
            bad_url_str = 'Index %d: Malformed URL\n'
            mime_str = 'Index %d: HTTP HEAD requested\n'
            blank_url_str = 'Index %d: No title\n'
            success_str = 'Title: [%s]\nIndex %d: updated\n'

        done = {'value': 0}  # count threads completed
        processed = {'value': 0}  # count number of records processed

        # An additional call to generate default headers
        # gen_headers() is called within fetch_data()
        # However, this initial call to setup headers
        # ensures there is no race condition among the
        # initial threads to setup headers
        if not MYHEADERS:
            gen_headers()

        def refresh(thread_idx, cond):
            """Inner function to fetch titles and update records.

            Parameters
            ----------
            thread_idx : int
                Thread index/ID.
            cond : threading condition object.
            """

            _count = 0

            while True:
                query = 'UPDATE bookmarks SET'
                arguments = []

                with cond:
                    if resultset:
                        id, url, tags, flags = resultset.pop()
                    else:
                      
Download .txt
gitextract_q_0wh3lh/

├── .circleci/
│   └── config.yml
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       └── lock.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── CHANGELOG
├── Dockerfile
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── auto-completion/
│   ├── bash/
│   │   └── buku-completion.bash
│   ├── fish/
│   │   └── buku.fish
│   └── zsh/
│       └── _buku
├── buku.1
├── buku.py
├── bukuserver/
│   ├── README.md
│   ├── __init__.py
│   ├── __main__.py
│   ├── api.py
│   ├── apidocs/
│   │   ├── bookmark/
│   │   │   ├── delete.yml
│   │   │   ├── get.yml
│   │   │   └── put.yml
│   │   ├── bookmark_range/
│   │   │   ├── delete.yml
│   │   │   ├── get.yml
│   │   │   └── put.yml
│   │   ├── bookmark_refresh/
│   │   │   └── post.yml
│   │   ├── bookmarks/
│   │   │   ├── delete.yml
│   │   │   ├── get.yml
│   │   │   └── post.yml
│   │   ├── bookmarks_refresh/
│   │   │   └── post.yml
│   │   ├── bookmarks_reorder/
│   │   │   └── post.yml
│   │   ├── bookmarks_search/
│   │   │   ├── delete.yml
│   │   │   └── get.yml
│   │   ├── fetch_data/
│   │   │   └── post.yml
│   │   ├── network_handle/
│   │   │   └── post.yml
│   │   ├── tag/
│   │   │   ├── delete.yml
│   │   │   ├── get.yml
│   │   │   └── put.yml
│   │   ├── tags/
│   │   │   └── get.yml
│   │   ├── template.yml
│   │   └── tiny_url/
│   │       └── get.yml
│   ├── bookmarklet.js
│   ├── filters.py
│   ├── forms.py
│   ├── middleware/
│   │   ├── __init__.py
│   │   └── flask_reverse_proxy_fix.py
│   ├── requirements.txt
│   ├── response.py
│   ├── server.py
│   ├── static/
│   │   └── bukuserver/
│   │       ├── css/
│   │       │   ├── bookmark.css
│   │       │   ├── list.css
│   │       │   └── modal.css
│   │       └── js/
│   │           ├── Chart.js
│   │           ├── bookmark.js
│   │           ├── buku_filter.js
│   │           ├── filters_fix.js
│   │           ├── last_page.js
│   │           └── order_filter.js
│   ├── templates/
│   │   └── bukuserver/
│   │       ├── bookmark_create.html
│   │       ├── bookmark_create_modal.html
│   │       ├── bookmark_details.html
│   │       ├── bookmark_details_modal.html
│   │       ├── bookmark_edit.html
│   │       ├── bookmark_edit_modal.html
│   │       ├── bookmarklet.url
│   │       ├── bookmarks_list.html
│   │       ├── home.html
│   │       ├── lib.html
│   │       ├── statistic.html
│   │       ├── tag_edit.html
│   │       └── tags_list.html
│   ├── translations/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   ├── babel.cfg
│   │   ├── de/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── messages.mo
│   │   │       └── messages.po
│   │   ├── fr/
│   │   │   └── LC_MESSAGES/
│   │   │       ├── messages.mo
│   │   │       └── messages.po
│   │   ├── messages_custom.pot
│   │   └── ru/
│   │       └── LC_MESSAGES/
│   │           ├── messages.mo
│   │           └── messages.po
│   ├── util.py
│   └── views.py
├── bukuserver-runner/
│   ├── README.md
│   ├── buku-server-headless.desktop
│   ├── buku-server.desktop
│   └── buku-server.py
├── docker-compose/
│   └── docker-compose.yml
├── docs/
│   └── source/
│       ├── buku.rst
│       ├── bukuserver.rst
│       ├── conf.py
│       ├── index.rst
│       ├── modules.rst
│       └── tutorial_for_developer.md
├── mypy.ini
├── packagecore.yaml
├── pyproject.toml
├── requirements.txt
├── tests/
│   ├── .pylintrc
│   ├── __init__.py
│   ├── cassettes/
│   │   └── test_buku/
│   │       ├── test_fetch_data_with_url[http---example.com-exp_res1].yaml
│   │       ├── test_fetch_data_with_url[http---example.com-page1.txt-exp_res2].yaml
│   │       ├── test_fetch_data_with_url[http---www.vim.org-scripts-script.php~-exp_res7].yaml
│   │       └── test_fetch_data_with_url[https---www.google.ru-search~-exp_res6].yaml
│   ├── genbm.sh
│   ├── pytest.ini
│   ├── test_BukuCrypt.py
│   ├── test_ExtendedArgumentParser.py
│   ├── test_buku.py
│   ├── test_bukuDb/
│   │   ├── 25491522_res.yaml
│   │   ├── 25491522_res_nopt.yaml
│   │   ├── Bookmarks
│   │   ├── firefox_res.yaml
│   │   ├── firefox_res_nopt.yaml
│   │   └── places.sql
│   ├── test_bukuDb.py
│   ├── test_cli.py
│   ├── test_import_firefox_json.py
│   ├── test_requirements.py
│   ├── test_server.py
│   ├── test_views.py
│   ├── util.py
│   └── vcr_cassettes/
│       ├── test_browse_by_index.yaml
│       ├── test_delete_rec_range_and_delay_commit.yaml
│       ├── test_search_by_multiple_tags_search_all.yaml
│       ├── test_search_by_multiple_tags_search_any.yaml
│       └── test_search_by_tags_enforces_space_seprations_exclusion.yaml
├── tox.bat
└── tox.ini
Download .txt
SYMBOL INDEX (759 symbols across 24 files)

FILE: buku.py
  function taglist_str (line 82) | def taglist_str(tag_str, convert=None):
  function filter_from (line 86) | def filter_from(values, subset, *, exclude=False):
  class BukuCrypt (line 162) | class BukuCrypt:
    method get_filehash (line 178) | def get_filehash(filepath):
    method encrypt_file (line 204) | def encrypt_file(iterations=8, dbfile=None, encfile=None, password=Non...
    method decrypt_file (line 223) | def decrypt_file(iterations=8, dbfile=None, encfile=None, password=Non...
    method __init__ (line 242) | def __init__(self, iterations=8, dbfile=None, encfile=None, password=N...
    method _encrypt_file (line 267) | def _encrypt_file(self):
    method _decrypt_file (line 290) | def _decrypt_file(self):
    method _cipher (line 316) | def _cipher(self, key, iv):
    method _key (line 319) | def _key(self, salt):
    method _encrypt (line 325) | def _encrypt(self):
    method _decrypt (line 351) | def _decrypt(self):
  class SortKey (line 372) | class SortKey:
    method __init__ (line 373) | def __init__(self, value, ascending=True):
    method __eq__ (line 376) | def __eq__(self, other):
    method __lt__ (line 380) | def __lt__(self, other):
    method __repr__ (line 384) | def __repr__(self):
  class FetchResult (line 388) | class FetchResult(NamedTuple):
    method tag_redirect (line 397) | def tag_redirect(self, pattern: str = None) -> str:
    method tag_error (line 400) | def tag_error(self, pattern: str = None) -> str:
    method tags (line 403) | def tags(self, *, keywords: bool = True, redirect: bool | str = False,...
  class BookmarkVar (line 409) | class BookmarkVar(NamedTuple):
    method immutable (line 419) | def immutable(self) -> bool:
    method tags (line 423) | def tags(self) -> str:
    method taglist (line 427) | def taglist(self) -> List[str]:
    method netloc (line 431) | def netloc(self) -> str:
  class BukuDb (line 437) | class BukuDb:
    method __init__ (line 454) | def __init__(
    method get_default_dbdir (line 486) | def get_default_dbdir():
    method initdb (line 513) | def initdb(dbfile: Optional[str] = None, chatty: bool = False) -> Tupl...
    method dbfile (line 588) | def dbfile(self) -> str:
    method dbname (line 592) | def dbname(self) -> str:
    method _fetch (line 595) | def _fetch(self, query: str, *args, lock: bool = True) -> List[Bookmar...
    method _fetch_first (line 602) | def _fetch_first(self, query: str, *args, lock: bool = True) -> Option...
    method _ordering (line 606) | def _ordering(self, fields=['+id'], for_db=True) -> List[Tuple[str, bo...
    method _sort (line 618) | def _sort(self, records: List[BookmarkVar], fields=['+id'], ignore_cas...
    method _order (line 625) | def _order(self, fields=['+id'], ignore_case=True) -> str:
    method get_rec_all (line 632) | def get_rec_all(self, *, lock: bool = True, order: List[str] = ['id'],...
    method get_rec_by_id (line 652) | def get_rec_by_id(self, index: int, *, lock: bool = True) -> Optional[...
    method get_rec_all_by_ids (line 670) | def get_rec_all_by_ids(self, indices: Ints, *, lock: bool = True, orde...
    method get_rec_id (line 692) | def get_rec_id(self, url: str, *, lock: bool = True):
    method get_rec_ids (line 711) | def get_rec_ids(self, urls: Values[str], *, lock: bool = True):
    method get_max_id (line 736) | def get_max_id(self, *, lock: bool = True) -> int:
    method reorder (line 756) | def reorder(self, order: List[str], *, ignore_case=True):
    method add_rec (line 774) | def add_rec(
    method append_tag_at_index (line 888) | def append_tag_at_index(self, index, tags_in, delay_commit=False):
    method delete_tag_at_index (line 939) | def delete_tag_at_index(self, index, tags_in, delay_commit=False, chat...
    method update_rec (line 1016) | def update_rec(
    method refreshdb (line 1232) | def refreshdb(
    method commit_delete (line 1469) | def commit_delete(self, apply: bool = True, retain_order: bool = False):
    method edit_update_rec (line 1479) | def edit_update_rec(self, index, immutable=None):
    method list_using_id (line 1525) | def list_using_id(self, ids=[], order=['+id']):
    method _search_tokens (line 1566) | def _search_tokens(self, keyword: str, deep=False, regex=False, marker...
    method _search_clause (line 1587) | def _search_clause(self, tokens, regex=False) -> Tuple[str, List[str]]:
    method searchdb (line 1610) | def searchdb(
    method search_by_tag (line 1685) | def search_by_tag(self, tags: Optional[str], order: List[str] = ['+id'...
    method search_keywords_and_filter_by_tags (line 1737) | def search_keywords_and_filter_by_tags(
    method exclude_results_from_search (line 1782) | def exclude_results_from_search(self, search_results, without, deep=Fa...
    method swap_recs (line 1806) | def swap_recs(self, index1: int, index2: int, *, lock: bool = True, de...
    method compactdb (line 1840) | def compactdb(self, index: int, delay_commit: bool = False, upto: Opti...
    method delete_rec (line 1880) | def delete_rec(
    method delete_resultset (line 2058) | def delete_resultset(self, results, retain_order=False):
    method delete_rec_all (line 2098) | def delete_rec_all(self, delay_commit=False):
    method cleardb (line 2124) | def cleardb(self, confirm=True):
    method print_rec (line 2149) | def print_rec(self, index: Optional[IntOrInts] = 0,
    method get_tag_all (line 2282) | def get_tag_all(self):
    method suggest_similar_tag (line 2316) | def suggest_similar_tag(self, tagstr):
    method replace_tag (line 2374) | def replace_tag(self, orig: str, new: List[str] = []):
    method get_tagstr_from_taglist (line 2422) | def get_tagstr_from_taglist(self, id_list, taglist):
    method set_tag (line 2455) | def set_tag(self, cmdstr, taglist):
    method browse_by_index (line 2550) | def browse_by_index(self, index=0, low=0, high=0, is_range=False):
    method exportdb (line 2618) | def exportdb(self, filepath: str, resultset: Optional[List[BookmarkVar...
    method traverse_bm_folder (line 2726) | def traverse_bm_folder(self, sublist, unique_tag, folder_name, add_par...
    method load_chrome_database (line 2768) | def load_chrome_database(self, path, unique_tag, add_parent_folder_as_...
    method load_firefox_database (line 2796) | def load_firefox_database(self, path, unique_tag, add_parent_folder_as...
    method load_edge_database (line 2860) | def load_edge_database(self, path, unique_tag, add_parent_folder_as_tag):
    method auto_import_from_browser (line 2888) | def auto_import_from_browser(self, firefox_profile=None):
    method importdb (line 2993) | def importdb(self, filepath, tacit=False):
    method mergedb (line 3123) | def mergedb(self, path):
    method tnyfy_url (line 3163) | def tnyfy_url(
    method browse_cached_url (line 3174) | def browse_cached_url(self, arg):
    method fixtags (line 3218) | def fixtags(self):
    method close (line 3248) | def close(self):
    method close_quit (line 3259) | def close_quit(self, exitval=0):
  class ExtendedArgumentParser (line 3278) | class ExtendedArgumentParser(argparse.ArgumentParser):
    method __init__ (line 3281) | def __init__(self, *args, **kwargs):
    method _add_argument (line 3286) | def _add_argument(self, old_add_arg, nodefaults, *args, **kwargs):
    method add_argument (line 3294) | def add_argument(self, *args, **kwargs):
    method add_argument_group (line 3297) | def add_argument_group(self, *args, **kwargs):
    method parse_args (line 3304) | def parse_args(self, *args, **kwargs):
    method program_info (line 3312) | def program_info(file=sys.stdout):
    method prompt_help (line 3336) | def prompt_help(file=sys.stdout):
    method is_colorstr (line 3386) | def is_colorstr(arg):
    method print_help (line 3413) | def print_help(self, file=sys.stdout):
  function convert_tags_to_org_mode_tags (line 3433) | def convert_tags_to_org_mode_tags(tags: str) -> str:
  function convert_bookmark_set (line 3445) | def convert_bookmark_set(
  function get_firefox_profile_names (line 3562) | def get_firefox_profile_names(path):
  function get_firefox_db_paths (line 3609) | def get_firefox_db_paths(default_ff_folder, specified=None):
  function walk (line 3615) | def walk(root):
  function import_md (line 3633) | def import_md(filepath: str, newtag: Optional[str]):
  function import_rss (line 3665) | def import_rss(filepath: str, newtag: Optional[str]):
  function import_org (line 3693) | def import_org(filepath: str, newtag: Optional[str]):
  function import_firefox_json (line 3758) | def import_firefox_json(json, add_bookmark_folder_as_tag=False, unique_t...
  function import_xbel (line 3890) | def import_xbel(html_soup: BeautifulSoup, add_parent_folder_as_tag: bool...
  function import_html (line 3974) | def import_html(html_soup: BeautifulSoup, add_parent_folder_as_tag: bool...
  function get_netloc (line 4043) | def get_netloc(url):
  function is_bad_url (line 4057) | def is_bad_url(url):
  function is_nongeneric_url (line 4087) | def is_nongeneric_url(url):
  function is_ignored_mime (line 4117) | def is_ignored_mime(url):
  function is_unusual_tag (line 4141) | def is_unusual_tag(tagstr):
  function parse_decoded_page (line 4167) | def parse_decoded_page(page):
  function get_data_from_page (line 4235) | def get_data_from_page(resp):
  function extract_auth (line 4270) | def extract_auth(url):
  function gen_headers (line 4278) | def gen_headers():
  function get_PoolManager (line 4308) | def get_PoolManager():
  function network_handler (line 4329) | def network_handler(
  function fetch_data (line 4347) | def fetch_data(
  function parse_tags (line 4431) | def parse_tags(keywords=[], *, edit_input=False):
  function prep_tag_search (line 4490) | def prep_tag_search(tags: str) -> Tuple[List[str], Optional[str], Option...
  function gen_auto_tag (line 4549) | def gen_auto_tag():
  function edit_at_prompt (line 4562) | def edit_at_prompt(obj, nav, suggest=False):
  function show_taglist (line 4593) | def show_taglist(obj):
  function prompt (line 4614) | def prompt(obj, results, noninteractive=False, deep=False, listtags=Fals...
  function copy_to_clipboard (line 4931) | def copy_to_clipboard(content):
  function print_rec_with_filter (line 4992) | def print_rec_with_filter(records, field_filter=0):
  function print_single_rec (line 5025) | def print_single_rec(row: BookmarkVar, idx: int=0, columns: int=0):  # NOQA
  function write_string_to_file (line 5100) | def write_string_to_file(content: str, filepath: str):
  function format_json (line 5119) | def format_json(resultset, single_record=False, field_filter=0):
  function print_json_safe (line 5146) | def print_json_safe(resultset, single_record=False, field_filter=0):
  function is_int (line 5176) | def is_int(string):
  function browse (line 5195) | def browse(url, default_scheme=SCHEME_HTTP):
  function check_upstream_release (line 5268) | def check_upstream_release():
  function regexp (line 5311) | def regexp(expr, item):
  function delim_wrap (line 5334) | def delim_wrap(token):
  function read_in (line 5360) | def read_in(msg):
  function sigint_handler (line 5380) | def sigint_handler(signum, frame):
  function disable_sigint_handler (line 5405) | def disable_sigint_handler():
  function enable_sigint_handler (line 5410) | def enable_sigint_handler():
  function get_system_editor (line 5419) | def get_system_editor():
  function is_editor_valid (line 5425) | def is_editor_valid(editor):
  function to_temp_file_content (line 5450) | def to_temp_file_content(url, title_in, tags_in, desc):
  function parse_temp_file_content (line 5506) | def parse_temp_file_content(content):
  function edit_rec (line 5568) | def edit_rec(editor, url, title_in, tags_in, desc):
  function setup_logger (line 5621) | def setup_logger(LOGGER):
  function piped_input (line 5656) | def piped_input(argv, pipeargs=None):
  function setcolors (line 5670) | def setcolors(args):
  function unwrap (line 5689) | def unwrap(text):
  function check_stdout_encoding (line 5706) | def check_stdout_encoding():
  function monkeypatch_textwrap_for_cjk (line 5745) | def monkeypatch_textwrap_for_cjk():
  function parse_range (line 5784) | def parse_range(tokens: Optional[str | Values[str]],
  function main (line 5828) | def main(argv=sys.argv[1:], *, program_name=os.path.basename(sys.argv[0])):

FILE: bukuserver-runner/buku-server.py
  class QueryList (line 29) | class QueryList(Dialog):
    method __init__ (line 30) | def __init__(self, title, prompt, values, initial=None, parent=None):
    method body (line 34) | def body(self, master):
    method select (line 49) | def select(self, index):
    method onkeypress (line 54) | def onkeypress(self, evt):
    method validate (line 60) | def validate(self):
  function is_valid_filepath (line 70) | def is_valid_filepath(path):
  function ignore_interrupt (line 83) | def ignore_interrupt():  # temporarily disables raising KeyboardInterrup...
  function run (line 88) | def run(command, *, shell=IS_WINDOWS, check=True):
  function parse_csv (line 97) | def parse_csv(text):
  function find_process (line 106) | def find_process(query, regex):
  function find_bukuserver_process (line 123) | def find_bukuserver_process():
  function kill_process (line 128) | def kill_process(pid):
  function get_buku_config_dir (line 131) | def get_buku_config_dir():
  function read_env_file (line 136) | def read_env_file(path):
  function selectdb (line 148) | def selectdb(dbdir, old=None, gui=GUI, title=TITLE):
  function load_virtualenv (line 212) | def load_virtualenv(virtualenv, devmode=False, reinstall=False):
  function prepare_vars (line 223) | def prepare_vars():
  function run_repeatedly (line 241) | def run_repeatedly(dbdir, devmode=False, gui=GUI, exec=None, workdir=Non...

FILE: bukuserver/__init__.py
  function _key (line 12) | def _key(s):  # replicates ad-hoc implementation of "get key from lazy s...

FILE: bukuserver/api.py
  function entity (line 31) | def entity(bookmark, index=False):
  function get_bukudb (line 45) | def get_bukudb():
  function search_tag (line 57) | def search_tag(
  function _fetch_data (line 97) | def _fetch_data(convert):
  function handle_network (line 108) | def handle_network():
  function fetch_data (line 113) | def fetch_data():
  function refresh_bookmark (line 119) | def refresh_bookmark(index: T.Optional[int]):
  function reorder_bookmarks (line 127) | def reorder_bookmarks():
  function get_all_tags (line 147) | def get_all_tags():
  class ApiTagView (line 151) | class ApiTagView(MethodView):
    method get (line 152) | def get(self, tag: str):
    method put (line 161) | def put(self, tag: str):
    method delete (line 181) | def delete(self, tag: str):
  class ApiBookmarksView (line 191) | class ApiBookmarksView(MethodView):
    method get (line 192) | def get(self):
    method post (line 199) | def post(self):
    method delete (line 215) | def delete(self):
  class ApiBookmarkView (line 220) | class ApiBookmarkView(MethodView):
    method get (line 221) | def get(self, index: int):
    method put (line 227) | def put(self, index: int):
    method delete (line 243) | def delete(self, index: int):
  class ApiBookmarkRangeView (line 249) | class ApiBookmarkRangeView(MethodView):
    method get (line 250) | def get(self, start_index: int, end_index: int):
    method put (line 259) | def put(self, start_index: int, end_index: int):
    method delete (line 290) | def delete(self, start_index: int, end_index: int):
  class ApiBookmarkSearchView (line 299) | class ApiBookmarkSearchView(MethodView):
    method get (line 300) | def get(self):
    method delete (line 309) | def delete(self):
  function bookmarklet_redirect (line 324) | def bookmarklet_redirect():

FILE: bukuserver/filters.py
  class BookmarkField (line 7) | class BookmarkField(Enum):
  function equal_func (line 15) | def equal_func(query, value, index):
  function not_equal_func (line 19) | def not_equal_func(query, value, index):
  function contains_func (line 23) | def contains_func(query, value, index):
  function not_contains_func (line 27) | def not_contains_func(query, value, index):
  function greater_func (line 31) | def greater_func(query, value, index):
  function smaller_func (line 35) | def smaller_func(query, value, index):
  function in_list_func (line 39) | def in_list_func(query, value, index):
  function not_in_list_func (line 43) | def not_in_list_func(query, value, index):
  function top_x_func (line 47) | def top_x_func(query, value, index):
  function bottom_x_func (line 53) | def bottom_x_func(query, value, index):
  class FilterType (line 59) | class FilterType(Enum):
  class BaseFilter (line 73) | class BaseFilter(filters.BaseFilter):
    method operation (line 75) | def operation(self):
    method apply (line 78) | def apply(self, query, value):
  class TagBaseFilter (line 82) | class TagBaseFilter(BaseFilter):
    method __init__ (line 84) | def __init__(
    method clean (line 108) | def clean(self, value):
  class BookmarkOrderFilter (line 123) | class BookmarkOrderFilter(BaseFilter):
    method __init__ (line 128) | def __init__(self, field, *args, **kwargs):
    method operation (line 132) | def operation(self):
    method apply (line 136) | def apply(self, query, value):
    method value (line 140) | def value(filters, values):
  class BookmarkBukuFilter (line 146) | class BookmarkBukuFilter(BaseFilter):
    method __init__ (line 154) | def __init__(self, *args, **kwargs):
    method operation (line 158) | def operation(self):
    method apply (line 163) | def apply(self, query, value):
  class BookmarkBaseFilter (line 167) | class BookmarkBaseFilter(BaseFilter):
    method __init__ (line 169) | def __init__(
    method clean (line 194) | def clean(self, value):
  class BookmarkTagNumberEqualFilter (line 209) | class BookmarkTagNumberEqualFilter(BookmarkBaseFilter):
    method __init__ (line 211) | def __init__(self, *args, **kwargs):
    method clean (line 222) | def clean(self, value):
  class BookmarkTagNumberGreaterFilter (line 229) | class BookmarkTagNumberGreaterFilter(BookmarkTagNumberEqualFilter):
    method __init__ (line 231) | def __init__(self, *args, **kwargs):
  class BookmarkTagNumberNotEqualFilter (line 243) | class BookmarkTagNumberNotEqualFilter(BookmarkTagNumberEqualFilter):
    method __init__ (line 245) | def __init__(self, *args, **kwargs):
  class BookmarkTagNumberSmallerFilter (line 257) | class BookmarkTagNumberSmallerFilter(BookmarkBaseFilter):
    method __init__ (line 259) | def __init__(self, *args, **kwargs):
    method clean (line 270) | def clean(self, value):

FILE: bukuserver/forms.py
  function optional_none (line 15) | def optional_none(form, field):
  function is_string (line 19) | def is_string(form, field):
  class ValueList (line 26) | class ValueList(SelectMultipleField):
    method __init__ (line 29) | def __init__(self, *args, item_validators=[], **kwargs):
    method process_data (line 33) | def process_data(self, value):
    method pre_validate (line 39) | def pre_validate(self, form):
  class SearchBookmarksForm (line 52) | class SearchBookmarksForm(FlaskForm):
  class HomeForm (line 67) | class HomeForm(SearchBookmarksForm):
  class BookmarkForm (line 71) | class BookmarkForm(FlaskForm):
  class SwapForm (line 79) | class SwapForm(FlaskForm):
  class ApiFetchDataForm (line 84) | class ApiFetchDataForm(Form):
  class ApiTagForm (line 88) | class ApiTagForm(Form):
    method tags_str (line 92) | def tags_str(self):
  class ApiBookmarkCreateForm (line 96) | class ApiBookmarkCreateForm(ApiTagForm):
    method data_values (line 104) | def data_values(self):
    method has_data (line 108) | def has_data(self):
  class ApiBookmarkEditForm (line 112) | class ApiBookmarkEditForm(ApiBookmarkCreateForm):
    method has_data (line 116) | def has_data(self):  # allowing to delete existing values
  class ApiBookmarkRangeEditForm (line 120) | class ApiBookmarkRangeEditForm(ApiBookmarkEditForm):
    method tags_in (line 124) | def tags_in(self):
    method data_values (line 128) | def data_values(self):  # ignoring empty tags list
  class ApiBookmarkSearchForm (line 132) | class ApiBookmarkSearchForm(Form):
  class ApiBookmarksReorderForm (line 140) | class ApiBookmarksReorderForm(Form):

FILE: bukuserver/middleware/flask_reverse_proxy_fix.py
  class ReverseProxyPrefixFix (line 11) | class ReverseProxyPrefixFix:  # pylint: disable=too-few-public-methods
    method __init__ (line 38) | def __init__(self, app: App, **kwargs):
    method __call__ (line 53) | def __call__(self, environ, start_response):

FILE: bukuserver/response.py
  class Response (line 9) | class Response(Enum):
    method invalid (line 21) | def invalid(errors):
    method from_flag (line 25) | def from_flag(flag: bool, *, data: Dict[str, Any] = None, errors: Dict...
    method status_code (line 30) | def status_code(self) -> int:
    method message (line 34) | def message(self) -> str:
    method status (line 38) | def status(self) -> int:
    method json (line 41) | def json(self, data: Dict[str, Any] = None) -> Dict[str, Any]:
    method __call__ (line 44) | def __call__(self, *, data: Dict[str, Any] = None):

FILE: bukuserver/server.py
  function get_bool_from_env_var (line 33) | def get_bool_from_env_var(key: str, default_value: bool = False) -> bool:
  function init_locale (line 38) | def init_locale(app, context_processor=lambda: {}):
  function before_request (line 51) | def before_request():
  function after_request (line 56) | def after_request(response):
  function create_app (line 66) | def create_app(db_file=None):
  class CustomFlaskGroup (line 158) | class CustomFlaskGroup(FlaskGroup):  # pylint: disable=too-few-public-me...
    method __init__ (line 159) | def __init__(self, **kwargs):
  function get_custom_version (line 167) | def get_custom_version(ctx, param, value):
  function cli (line 185) | def cli():

FILE: bukuserver/static/bukuserver/js/Chart.js
  function e (line 10) | function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof requi...
  function getRgba (line 35) | function getRgba(string) {
  function getHsla (line 95) | function getHsla(string) {
  function getHwb (line 111) | function getHwb(string) {
  function getRgb (line 127) | function getRgb(string) {
  function getHsl (line 132) | function getHsl(string) {
  function getAlpha (line 137) | function getAlpha(string) {
  function hexString (line 151) | function hexString(rgb) {
  function rgbString (line 156) | function rgbString(rgba, alpha) {
  function rgbaString (line 163) | function rgbaString(rgba, alpha) {
  function percentString (line 171) | function percentString(rgba, alpha) {
  function percentaString (line 182) | function percentaString(rgba, alpha) {
  function hslString (line 189) | function hslString(hsla, alpha) {
  function hslaString (line 196) | function hslaString(hsla, alpha) {
  function hwbString (line 206) | function hwbString(hwb, alpha) {
  function keyword (line 214) | function keyword(rgb) {
  function scale (line 219) | function scale(num, min, max) {
  function hexDouble (line 223) | function hexDouble(num) {
  function rgb2hsl (line 781) | function rgb2hsl(rgb) {
  function rgb2hsv (line 816) | function rgb2hsv(rgb) {
  function rgb2hwb (line 849) | function rgb2hwb(rgb) {
  function rgb2cmyk (line 860) | function rgb2cmyk(rgb) {
  function rgb2keyword (line 873) | function rgb2keyword(rgb) {
  function rgb2xyz (line 877) | function rgb2xyz(rgb) {
  function rgb2lab (line 894) | function rgb2lab(rgb) {
  function rgb2lch (line 916) | function rgb2lch(args) {
  function hsl2rgb (line 920) | function hsl2rgb(hsl) {
  function hsl2hsv (line 958) | function hsl2hsv(hsl) {
  function hsl2hwb (line 977) | function hsl2hwb(args) {
  function hsl2cmyk (line 981) | function hsl2cmyk(args) {
  function hsl2keyword (line 985) | function hsl2keyword(args) {
  function hsv2rgb (line 990) | function hsv2rgb(hsv) {
  function hsv2hsl (line 1018) | function hsv2hsl(hsv) {
  function hsv2hwb (line 1032) | function hsv2hwb(args) {
  function hsv2cmyk (line 1036) | function hsv2cmyk(args) {
  function hsv2keyword (line 1040) | function hsv2keyword(args) {
  function hwb2rgb (line 1045) | function hwb2rgb(hwb) {
  function hwb2hsl (line 1080) | function hwb2hsl(args) {
  function hwb2hsv (line 1084) | function hwb2hsv(args) {
  function hwb2cmyk (line 1088) | function hwb2cmyk(args) {
  function hwb2keyword (line 1092) | function hwb2keyword(args) {
  function cmyk2rgb (line 1096) | function cmyk2rgb(cmyk) {
  function cmyk2hsl (line 1109) | function cmyk2hsl(args) {
  function cmyk2hsv (line 1113) | function cmyk2hsv(args) {
  function cmyk2hwb (line 1117) | function cmyk2hwb(args) {
  function cmyk2keyword (line 1121) | function cmyk2keyword(args) {
  function xyz2rgb (line 1126) | function xyz2rgb(xyz) {
  function xyz2lab (line 1153) | function xyz2lab(xyz) {
  function xyz2lch (line 1174) | function xyz2lch(args) {
  function lab2xyz (line 1178) | function lab2xyz(lab) {
  function lab2lch (line 1199) | function lab2lch(lab) {
  function lab2rgb (line 1214) | function lab2rgb(args) {
  function lch2lab (line 1218) | function lch2lab(lch) {
  function lch2xyz (line 1230) | function lch2xyz(args) {
  function lch2rgb (line 1234) | function lch2rgb(args) {
  function keyword2rgb (line 1238) | function keyword2rgb(keyword) {
  function keyword2hsl (line 1242) | function keyword2hsl(args) {
  function keyword2hsv (line 1246) | function keyword2hsv(args) {
  function keyword2hwb (line 1250) | function keyword2hwb(args) {
  function keyword2cmyk (line 1254) | function keyword2cmyk(args) {
  function keyword2lab (line 1258) | function keyword2lab(args) {
  function keyword2xyz (line 1262) | function keyword2xyz(args) {
  function computeMinSampleSize (line 1982) | function computeMinSampleSize(scale, pixels) {
  function computeFitCategoryTraits (line 2006) | function computeFitCategoryTraits(index, ruler, options) {
  function computeFlexCategoryTraits (line 2036) | function computeFlexCategoryTraits(index, ruler, options) {
  function lineEnabled (line 2898) | function lineEnabled(dataset, options) {
  function capControlPoint (line 3114) | function capControlPoint(pt, min, max) {
  function initConfig (line 3841) | function initConfig(config) {
  function updateConfig (line 3862) | function updateConfig(chart) {
  function positionIsHorizontal (line 3882) | function positionIsHorizontal(position) {
  function listenArrayEvents (line 4779) | function listenArrayEvents(array, listener) {
  function unlistenArrayEvents (line 4820) | function unlistenArrayEvents(array, listener) {
  function interpolate (line 5117) | function interpolate(start, view, model, ease) {
  function parseMaxStyle (line 5649) | function parseMaxStyle(styleValue, node, parentProperty) {
  function isConstrainedValue (line 5669) | function isConstrainedValue(value) {
  function getConstraintDimension (line 5678) | function getConstraintDimension(domNode, maxStyle, percentageProperty) {
  function getRelativePosition (line 5852) | function getRelativePosition(e, chart) {
  function parseVisibleItems (line 5868) | function parseVisibleItems(chart, handler) {
  function getIntersectItems (line 5893) | function getIntersectItems(chart, position) {
  function getNearestItems (line 5913) | function getNearestItems(chart, position, intersect, distanceMetric) {
  function getDistanceMetricForAxis (line 5942) | function getDistanceMetricForAxis(axis) {
  function indexMode (line 5953) | function indexMode(chart, e, options) {
  function filterByPosition (line 6229) | function filterByPosition(array, position) {
  function sortByWeight (line 6235) | function sortByWeight(array, reverse) {
  function getMinimumBoxSize (line 6411) | function getMinimumBoxSize(box) {
  function fitBox (line 6463) | function fitBox(box) {
  function finalFitVerticalBox (line 6509) | function finalFitVerticalBox(box) {
  function placeBox (line 6594) | function placeBox(box) {
  function labelsFromTicks (line 7097) | function labelsFromTicks(ticks) {
  function getLineValue (line 7108) | function getLineValue(scale, index, offsetGridLines) {
  function computeTextSize (line 7123) | function computeTextSize(context, tick, font) {
  function parseFontOptions (line 7129) | function parseFontOptions(options) {
  function parseLineHeight (line 7144) | function parseLineHeight(options) {
  function mergeOpacity (line 8191) | function mergeOpacity(colorString, opacity) {
  function pushOrConcat (line 8197) | function pushOrConcat(base, toPush) {
  function createTooltipItem (line 8213) | function createTooltipItem(element) {
  function getBaseModel (line 8233) | function getBaseModel(tooltipOpts) {
  function getTooltipSize (line 8285) | function getTooltipSize(tooltip, model) {
  function determineAlignment (line 8353) | function determineAlignment(tooltip, size) {
  function getBackgroundPoint (line 8426) | function getBackgroundPoint(vm, size, alignment, chart) {
  function xRange (line 9264) | function xRange(mouseX) {
  function yRange (line 9269) | function yRange(mouseY) {
  function isVertical (line 9364) | function isVertical(bar) {
  function getBarBounds (line 9374) | function getBarBounds(bar) {
  function cornerAt (line 9475) | function cornerAt(index) {
  function readUsedSize (line 10548) | function readUsedSize(element, property) {
  function initCanvas (line 10559) | function initCanvas(canvas, config) {
  function addEventListener (line 10633) | function addEventListener(node, type, listener) {
  function removeEventListener (line 10637) | function removeEventListener(node, type, listener) {
  function createEvent (line 10641) | function createEvent(type, chart, x, y, nativeEvent) {
  function fromNativeEvent (line 10651) | function fromNativeEvent(event, chart) {
  function throttled (line 10657) | function throttled(fn, thisArg) {
  function createResizer (line 10676) | function createResizer(handler) {
  function watchForRender (line 10734) | function watchForRender(node, handler) {
  function unwatchForRender (line 10756) | function unwatchForRender(node) {
  function addResizeListener (line 10771) | function addResizeListener(node, listener, chart) {
  function removeResizeListener (line 10796) | function removeResizeListener(node) {
  function injectCSS (line 10808) | function injectCSS(platform, css) {
  function decodeFill (line 11100) | function decodeFill(el, index, count) {
  function computeBoundary (line 11149) | function computeBoundary(source) {
  function resolveTarget (line 11193) | function resolveTarget(sources, index, propagate) {
  function createMapper (line 11224) | function createMapper(source) {
  function isDrawable (line 11239) | function isDrawable(point) {
  function drawArea (line 11243) | function drawArea(ctx, curve0, curve1, len0, len1) {
  function doFill (line 11265) | function doFill(ctx, points, mapper, view, color, loop) {
  function getBoxWidth (line 11461) | function getBoxWidth(labelOpts, fontSize) {
  function createNewLegendAndAttach (line 11889) | function createNewLegendAndAttach(chart, legendOpts) {
  function createNewTitleBlockAndAttach (line 12150) | function createNewTitleBlockAndAttach(chart, titleOpts) {
  function IDMatches (line 12365) | function IDMatches(meta) {
  function generateTicks (line 12541) | function generateTicks(generationOptions, dataRange) {
  function generateTicks (line 12730) | function generateTicks(generationOptions, dataRange) {
  function IDMatches (line 12795) | function IDMatches(meta) {
  function getValueCount (line 13126) | function getValueCount(scale) {
  function getPointLabelFontOptions (line 13131) | function getPointLabelFontOptions(scale) {
  function measureLabelSize (line 13146) | function measureLabelSize(ctx, fontSize, label) {
  function determineLimits (line 13160) | function determineLimits(angle, pos, size, min, max) {
  function fitWithPointLabels (line 13182) | function fitWithPointLabels(scale) {
  function fit (line 13267) | function fit(scale) {
  function getTextAlignForAngle (line 13273) | function getTextAlignForAngle(angle) {
  function fillText (line 13283) | function fillText(ctx, text, position, fontSize) {
  function adjustPointPositionForLabelHeight (line 13297) | function adjustPointPositionForLabelHeight(angle, textSize, position) {
  function drawPointLabels (line 13305) | function drawPointLabels(scale) {
  function drawRadiusLine (line 13349) | function drawRadiusLine(scale, gridLineOpts, radius, index) {
  function numberOrZero (line 13382) | function numberOrZero(param) {
  function sorter (line 13661) | function sorter(a, b) {
  function arrayUnique (line 13665) | function arrayUnique(items) {
  function buildLookupTable (line 13696) | function buildLookupTable(timestamps, min, max, distribution) {
  function lookup (line 13732) | function lookup(table, key, value) {
  function interpolate (line 13764) | function interpolate(table, skey, sval, tkey) {
  function momentify (line 13782) | function momentify(value, options) {
  function parse (line 13811) | function parse(input, scale) {
  function determineStepSize (line 13833) | function determineStepSize(min, max, unit, capacity) {
  function determineUnitForAutoTicks (line 13857) | function determineUnitForAutoTicks(minUnit, min, max, capacity) {
  function determineUnitForFormatting (line 13876) | function determineUnitForFormatting(ticks, minUnit, min, max) {
  function determineMajorUnit (line 13891) | function determineMajorUnit(unit) {
  function generate (line 13905) | function generate(min, max, capacity, options) {
  function computeOffsets (line 13960) | function computeOffsets(table, ticks, min, max, options) {
  function ticksFromTimestamps (line 13987) | function ticksFromTimestamps(values, majorUnit) {
  function determineLabelFormat (line 14004) | function determineLabelFormat(data, timeOpts) {

FILE: bukuserver/translations/__init__.py
  function replace_obsolete (line 29) | def replace_obsolete(text):
  function translations_generate (line 39) | def translations_generate():
  function translations_update (line 52) | def translations_update(new_locales=[], generate=True, domain=DOMAIN, fu...
  function translations_compile (line 75) | def translations_compile(update=False, generate=True, domain=DOMAIN, new...

FILE: bukuserver/util.py
  function chunks (line 11) | def chunks(arr, n):
  function sorted_counter (line 15) | def sorted_counter(keys, *, min_count=0):

FILE: bukuserver/views.py
  class CustomAdminIndexView (line 44) | class CustomAdminIndexView(AdminIndexView):
    method index (line 46) | def index(self):
    method search (line 50) | def search(self):
  function last_page (line 61) | def last_page(self):
  function app_param (line 84) | def app_param(key, default=None):
  function readonly_check (line 87) | def readonly_check(self):
  class ApplyFiltersMixin (line 93) | class ApplyFiltersMixin:  # pylint: disable=too-few-public-methods
    method _apply_filters (line 94) | def _apply_filters(self, models, filters):
  class BookmarkModelView (line 102) | class BookmarkModelView(BaseModelView, ApplyFiltersMixin):
    method _filter_arg (line 104) | def _filter_arg(flt):
    method _saved (line 108) | def _saved(self, id, url, ok=True):
    method _create_ajax_loader (line 115) | def _create_ajax_loader(self, name, options):
    method _list_entry (line 118) | def _list_entry(self, context: Any, model: Namespace, name: str) -> Ma...
    method get_detail_value (line 152) | def get_detail_value(self, context, model, name):
    method __init__ (line 192) | def __init__(self, bukudb: buku.BukuDb, *args, **kwargs):
    method url_render_mode (line 199) | def url_render_mode(self):
    method page_size (line 203) | def page_size(self):
    method page_size_options (line 207) | def page_size_options(self):
    method get_safe_page_size (line 210) | def get_safe_page_size(self, page_size):  # un-enforcing the restriction
    method create_form (line 213) | def create_form(self, obj=None):
    method create_model (line 223) | def create_model(self, form):
    method delete_model (line 248) | def delete_model(self, model):
    method _from_filters (line 261) | def _from_filters(self, filters):
    method refresh (line 281) | def refresh(self):
    method get_list (line 288) | def get_list(self, page, sort_field, sort_desc, _, filters, page_size=...
    method get_one (line 300) | def get_one(self, id):
    method get_pk_value (line 314) | def get_pk_value(self, model):
    method swap (line 318) | def swap(self):
    method scaffold_list_columns (line 323) | def scaffold_list_columns(self):
    method scaffold_list_form (line 326) | def scaffold_list_form(self, widget=None, validators=None):
    method scaffold_sortable_columns (line 329) | def scaffold_sortable_columns(self):
    method scaffold_filters (line 337) | def scaffold_filters(self, name):
    method scaffold_form (line 411) | def scaffold_form(self):
    method update_model (line 414) | def update_model(self, form: forms.BookmarkForm, model: Namespace):
  class TagModelView (line 437) | class TagModelView(BaseModelView, ApplyFiltersMixin):
    method _create_ajax_loader (line 438) | def _create_ajax_loader(self, name, options):
    method _name_formatter (line 441) | def _name_formatter(self, context, model, name):
    method _refresh (line 459) | def _refresh(self):
    method __init__ (line 463) | def __init__(self, bukudb, *args, **kwargs):
    method page_size (line 471) | def page_size(self):
    method page_size_options (line 475) | def page_size_options(self):
    method get_safe_page_size (line 478) | def get_safe_page_size(self, page_size):  # un-enforcing the restriction
    method refresh (line 482) | def refresh(self):
    method scaffold_list_columns (line 486) | def scaffold_list_columns(self):
    method scaffold_sortable_columns (line 489) | def scaffold_sortable_columns(self):
    method scaffold_form (line 492) | def scaffold_form(self):
    method scaffold_list_form (line 498) | def scaffold_list_form(self, widget=None, validators=None):
    method get_list (line 501) | def get_list(
    method get_pk_value (line 528) | def get_pk_value(self, model):
    method get_one (line 531) | def get_one(self, id):
    method scaffold_filters (line 536) | def scaffold_filters(self, name):
    method delete_model (line 568) | def delete_model(self, model):
    method update_model (line 583) | def update_model(self, form, model):
    method create_model (line 601) | def create_model(self, form):
  class StatisticView (line 605) | class StatisticView(BaseView):  # pylint: disable=too-few-public-methods
    method __init__ (line 609) | def __init__(self, bukudb, *args, **kwargs):
    method index (line 614) | def index(self):
  function page_of (line 639) | def page_of(items, size, idx):
  function filter_key (line 645) | def filter_key(flt, idx=''):
  function format_value (line 650) | def format_value(field, bookmark, spacing=''):
  function link (line 654) | def link(text, url, new_tab=False, html=False, badge=''):
  class CountedData (line 662) | class CountedData(list):
    method __init__ (line 663) | def __init__(self, counter):
    method cropped (line 669) | def cropped(self):
    method all (line 673) | def all(self):

FILE: tests/test_BukuCrypt.py
  function test_get_filehash (line 8) | def test_get_filehash(tmpdir):
  function test_encrypt_decrypt (line 24) | def test_encrypt_decrypt(tmpdir, filesize):

FILE: tests/test_ExtendedArgumentParser.py
  function test_program_info (line 9) | def test_program_info(platform, file):
  function test_prompt_help (line 24) | def test_prompt_help():
  function test_print_help (line 32) | def test_print_help():

FILE: tests/test_buku.py
  function check_import_html_results_contains (line 19) | def check_import_html_results_contains(result, expected_result):
  function test_extract_auth (line 39) | def test_extract_auth(url, result):
  function test_get_netloc (line 62) | def test_get_netloc(url, netloc):
  function test_is_bad_url (line 80) | def test_is_bad_url(url, exp_res):
  function test_is_ignored_mime (line 95) | def test_is_ignored_mime(url, exp_res):
  function test_gen_headers (line 102) | def test_gen_headers():
  function test_get_PoolManager (line 120) | def test_get_PoolManager(m_myproxy):
  function test_parse_tags (line 160) | def test_parse_tags(prefix, keywords, exp_res):
  function test_parse_tags_no_args (line 172) | def test_parse_tags_no_args():
  function test_print_rec_with_filter (line 192) | def test_print_rec_with_filter(capfd, field_filter, exp_res):
  function test_prep_tag_search (line 207) | def test_prep_tag_search(taglist, exp_res):
  function test_edit_at_prompt (line 222) | def test_edit_at_prompt(nav, is_editor_valid_retval, edit_rec_retval):
  function test_format_json (line 249) | def test_format_json(field_filter, single_record):
  function test_is_int (line 282) | def test_is_int(string, exp_res):
  function test_browse (line 297) | def test_browse(url, opened_url, platform):
  function test_check_upstream_release (line 315) | def test_check_upstream_release(status_code, latest_release):
  function test_regexp (line 343) | def test_regexp(exp, item, exp_res):
  function test_delim_wrap (line 352) | def test_delim_wrap(token, exp_res):
  function test_read_in (line 360) | def test_read_in():
  function test_sigint_handler_with_mock (line 372) | def test_sigint_handler_with_mock():
  function test_get_system_editor (line 381) | def test_get_system_editor():
  function test_is_editor_valid (line 399) | def test_is_editor_valid(editor, exp_res):
  function test_to_temp_file_content (line 415) | def test_to_temp_file_content(url, title_in, tags_in, desc):
  function test_parse_temp_file_content (line 463) | def test_parse_temp_file_content(content, exp_res):
  function test_edit_rec (line 472) | def test_edit_rec():
  function test_piped_input (line 486) | def test_piped_input(argv, pipeargs, isatty):
  class TestHelpers (line 500) | class TestHelpers(unittest.TestCase):
    method test_is_int (line 504) | def test_is_int(self):
  function test_sigint_handler (line 514) | def test_sigint_handler(capsys):
  function test_fetch_data_with_url (line 560) | def test_fetch_data_with_url(url, exp_res):
  function test_is_nongeneric_url (line 584) | def test_is_nongeneric_url(url, exp_res):
  function test_import_md (line 598) | def test_import_md(tmpdir, url, title, tags, newtag, exp_tags):
  function test_import_rss (line 614) | def test_import_rss(tmpdir, extension, newtag, exp_res):
  function test_import_org (line 637) | def test_import_org(tmpdir, url, title, tags, newtag, exp_tags):
  function test_import_html (line 722) | def test_import_html(html_text, exp_res):
  function test_import_html_and_add_parent (line 734) | def test_import_html_and_add_parent():
  function test_import_html_and_add_all_parent (line 795) | def test_import_html_and_add_all_parent(add_all_parent, exp_res):
  function test_import_html_and_new_tag (line 829) | def test_import_html_and_new_tag():
  function test_get_firefox_profile_names (line 904) | def test_get_firefox_profile_names(_os_path_exists, profiles, expected):
  function test_get_firefox_db_paths (line 918) | def test_get_firefox_db_paths(profiles, specified, expected):
  function test_copy_to_clipboard (line 935) | def test_copy_to_clipboard(platform, params):
  function test_convert_bookmark_set (line 1036) | def test_convert_bookmark_set(export_type, exp_res, monkeypatch):
  function test_convert_tags_to_org_mode_tags (line 1071) | def test_convert_tags_to_org_mode_tags(tags, data):
  function test_get_data_from_page (line 1080) | def test_get_data_from_page(charset, mode):
  function test_parse_range (line 1109) | def test_parse_range(tokens, kwargs, expected):
  function test_split_by_marker (line 1121) | def test_split_by_marker():
  function test_SortKey (line 1130) | def test_SortKey():

FILE: tests/test_bukuDb.py
  function get_temp_dir_path (line 25) | def get_temp_dir_path():
  function vcr_cassette_dir (line 62) | def vcr_cassette_dir(request):
  function rmdb (line 67) | def rmdb(*bdbs):
  function bukuDb (line 75) | def bukuDb():
  class PrettySafeLoader (line 92) | class PrettySafeLoader(
    method construct_python_tuple (line 95) | def construct_python_tuple(self, node):
  class TestBukuDb (line 104) | class TestBukuDb(unittest.TestCase):
    method setUp (line 105) | def setUp(self):
    method tearDown (line 114) | def tearDown(self):
    method test_get_default_dbdir (line 119) | def test_get_default_dbdir(self):
    method test_initdb (line 150) | def test_initdb(self):
    method test_get_rec_by_id (line 162) | def test_get_rec_by_id(self):
    method test_get_rec_all_by_ids (line 175) | def test_get_rec_all_by_ids(self):
    method test_get_rec_id (line 183) | def test_get_rec_id(self):
    method test_add_rec (line 195) | def test_add_rec(self):
    method test_swap_recs (line 207) | def test_swap_recs(self):
    method test_suggest_tags (line 216) | def test_suggest_tags(self):
    method test_update_rec (line 232) | def test_update_rec(self):
    method test_append_tag_at_index (line 248) | def test_append_tag_at_index(self):
    method test_append_tag_at_all_indices (line 264) | def test_append_tag_at_all_indices(self):
    method test_delete_tag_at_index (line 289) | def test_delete_tag_at_index(self):
    method test_search_keywords_and_filter_by_tags (line 308) | def test_search_keywords_and_filter_by_tags(self):
    method test_searchdb (line 360) | def test_searchdb(self):
    method test_search_by_tag (line 382) | def test_search_by_tag(self):
    method test_search_by_multiple_tags_search_any (line 400) | def test_search_by_multiple_tags_search_any(self):
    method test_search_by_multiple_tags_search_all (line 451) | def test_search_by_multiple_tags_search_all(self):
    method test_search_by_tags_enforces_space_seprations_search_all (line 482) | def test_search_by_tags_enforces_space_seprations_search_all(self):
    method test_search_by_tags_exclusion (line 540) | def test_search_by_tags_exclusion(self):
    method test_search_by_tags_enforces_space_seprations_exclusion (line 581) | def test_search_by_tags_enforces_space_seprations_exclusion(self):
    method test_search_and_open_in_browser_by_range (line 639) | def test_search_and_open_in_browser_by_range(self):
    method test_search_and_open_all_in_browser (line 669) | def test_search_and_open_all_in_browser(self):
    method test_delete_rec (line 696) | def test_delete_rec(self):
    method test_delete_rec_yes (line 706) | def test_delete_rec_yes(self):
    method test_delete_rec_no (line 711) | def test_delete_rec_no(self):
    method test_cleardb (line 716) | def test_cleardb(self):
    method test_replace_tag (line 725) | def test_replace_tag(self):
    method test_close_quit (line 772) | def test_close_quit(self):
  function test_add_rec_fetch (line 798) | def test_add_rec_fetch(bukuDb, caplog, fetch, url_redirect, tag_redirect...
  function test_add_rec_tags (line 856) | def test_add_rec_tags(bukuDb, caplog, status, tags_fetched, tags_in, tag...
  function test_update_rec_fetch (line 881) | def test_update_rec_fetch(bukuDb, caplog, url_in, title_in, tags_in, url...
  function test_export_on (line 1103) | def test_export_on(bukuDb, ext, expected):
  function refreshdb_fixture (line 1126) | def refreshdb_fixture():
  function test_refreshdb (line 1151) | def test_refreshdb(refreshdb_fixture, title_in, exp_res):
  function test_print_caplog (line 1164) | def test_print_caplog(caplog):
  function test_print_rec (line 1208) | def test_print_rec(bukuDb, kwargs, rec, exp_res, tmp_path, caplog):
  function test_list_tags (line 1216) | def test_list_tags(capsys, bukuDb):
  function test_compactdb (line 1233) | def test_compactdb(bukuDb):
  function test_delete_rec_range_and_delay_commit (line 1296) | def test_delete_rec_range_and_delay_commit(
  function test_delete_rec_index_and_delay_commit (line 1329) | def test_delete_rec_index_and_delay_commit(bukuDb, index, delay_commit, ...
  function test_delete_rec_on_empty_database (line 1375) | def test_delete_rec_on_empty_database(bukuDb, index, is_range, low, high):
  function test_delete_rec_on_non_integer (line 1404) | def test_delete_rec_on_non_integer(
  function test_add_rec_add_invalid_url (line 1430) | def test_add_rec_add_invalid_url(bukuDb, caplog, url):
  function test_add_rec_exec_arg (line 1473) | def test_add_rec_exec_arg(bukuDb, kwargs, exp_arg):
  function test_update_rec_index_0 (line 1487) | def test_update_rec_index_0(bukuDb, caplog):
  function test_update_rec (line 1504) | def test_update_rec(bukuDb, tmp_path, kwargs, exp_res):
  function test_update_rec_invalid_tag (line 1511) | def test_update_rec_invalid_tag(bukuDb, caplog, invalid_tag):
  function test_update_rec_update_all_bookmark (line 1529) | def test_update_rec_update_all_bookmark(
  function test_edit_update_rec_with_invalid_input (line 1546) | def test_edit_update_rec_with_invalid_input(bukuDb, get_system_editor_re...
  function test_browse_by_index (line 1562) | def test_browse_by_index(low, high, index, is_range, empty_database):
  function chrome_db (line 1597) | def chrome_db():
  function test_load_chrome_database (line 1607) | def test_load_chrome_database(bukuDb, chrome_db, add_pt):
  function firefox_db (line 1635) | def firefox_db(tmpdir):
  function test_load_firefox_database (line 1649) | def test_load_firefox_database(bukuDb, firefox_db, add_pt):
  function test_sort_and_reorder (line 1694) | def test_sort_and_reorder(bukuDb, fields, ignore_case, expected):
  function test_order (line 1717) | def test_order(bukuDb, fields, ignore_case, expected):
  function test_order_by_netloc (line 1730) | def test_order_by_netloc(bukuDb, order, expected):
  function test_search_tokens (line 1788) | def test_search_tokens(bukuDb, keyword, params, expected):
  function test_search_clause (line 1815) | def test_search_clause(bukuDb, regex, tokens, args, clauses):
  function test_searchdb (line 1835) | def test_searchdb(bukuDb, keywords, params, expected):
  function test_search_keywords_and_filter_by_tags (line 1847) | def test_search_keywords_and_filter_by_tags(bukuDb, keyword_results, sta...
  function test_exclude_results_from_search (line 1859) | def test_exclude_results_from_search(bukuDb, search_results, exclude_res...
  function test_exportdb_empty_db (line 1864) | def test_exportdb_empty_db(bukuDb):
  function test_exportdb_single_rec (line 1872) | def test_exportdb_single_rec(bukuDb, tmpdir):
  function test_exportdb_to_db (line 1883) | def test_exportdb_to_db(bukuDb):
  function test_exportdb_pick (line 1903) | def test_exportdb_pick(_bukudb_sort, _convert_bookmark_set, _sample, _op...
  function test_get_max_id (line 1936) | def test_get_max_id(bukuDb, urls, exp_res):
  function split_and_test_membership (line 1947) | def split_and_test_membership(a, b):
  function inclusive_range (line 1953) | def inclusive_range(start, end):
  function normalize_range (line 1957) | def normalize_range(db_len, low, high):

FILE: tests/test_bukuDb/places.sql
  type moz_places (line 3) | CREATE TABLE moz_places (   id INTEGER PRIMARY KEY, url LONGVARCHAR, tit...
  type moz_historyvisits (line 56) | CREATE TABLE moz_historyvisits (  id INTEGER PRIMARY KEY, from_visit INT...
  type moz_inputhistory (line 57) | CREATE TABLE moz_inputhistory (  place_id INTEGER NOT NULL, input LONGVA...
  type moz_hosts (line 58) | CREATE TABLE moz_hosts (  id INTEGER PRIMARY KEY, host TEXT NOT NULL UNI...
  type moz_bookmarks (line 59) | CREATE TABLE moz_bookmarks (  id INTEGER PRIMARY KEY, type INTEGER, fk I...
  type moz_keywords (line 124) | CREATE TABLE moz_keywords (  id INTEGER PRIMARY KEY AUTOINCREMENT, keywo...
  type moz_favicons (line 125) | CREATE TABLE moz_favicons (  id INTEGER PRIMARY KEY, url LONGVARCHAR UNI...
  type moz_anno_attributes (line 126) | CREATE TABLE moz_anno_attributes (  id INTEGER PRIMARY KEY, name VARCHAR...
  type moz_annos (line 127) | CREATE TABLE moz_annos (  id INTEGER PRIMARY KEY, place_id INTEGER NOT N...
  type moz_items_annos (line 128) | CREATE TABLE moz_items_annos (  id INTEGER PRIMARY KEY, item_id INTEGER ...
  type moz_bookmarks_deleted (line 145) | CREATE TABLE moz_bookmarks_deleted (  guid TEXT PRIMARY KEY, dateRemoved...
  type moz_places_faviconindex (line 147) | CREATE INDEX moz_places_faviconindex ON moz_places (favicon_id)
  type moz_places_hostindex (line 148) | CREATE INDEX moz_places_hostindex ON moz_places (rev_host)
  type moz_places_visitcount (line 149) | CREATE INDEX moz_places_visitcount ON moz_places (visit_count)
  type moz_places_frecencyindex (line 150) | CREATE INDEX moz_places_frecencyindex ON moz_places (frecency)
  type moz_places_lastvisitdateindex (line 151) | CREATE INDEX moz_places_lastvisitdateindex ON moz_places (last_visit_date)
  type moz_historyvisits_placedateindex (line 152) | CREATE INDEX moz_historyvisits_placedateindex ON moz_historyvisits (plac...
  type moz_historyvisits_fromindex (line 153) | CREATE INDEX moz_historyvisits_fromindex ON moz_historyvisits (from_visit)
  type moz_historyvisits_dateindex (line 154) | CREATE INDEX moz_historyvisits_dateindex ON moz_historyvisits (visit_date)
  type moz_bookmarks_itemindex (line 155) | CREATE INDEX moz_bookmarks_itemindex ON moz_bookmarks (fk, type)
  type moz_bookmarks_parentindex (line 156) | CREATE INDEX moz_bookmarks_parentindex ON moz_bookmarks (parent, position)
  type moz_bookmarks_itemlastmodifiedindex (line 157) | CREATE INDEX moz_bookmarks_itemlastmodifiedindex ON moz_bookmarks (fk, l...
  type moz_places_url_hashindex (line 158) | CREATE INDEX moz_places_url_hashindex ON moz_places (url_hash)
  type moz_places_guid_uniqueindex (line 159) | CREATE UNIQUE INDEX moz_places_guid_uniqueindex ON moz_places (guid)
  type moz_bookmarks_guid_uniqueindex (line 160) | CREATE UNIQUE INDEX moz_bookmarks_guid_uniqueindex ON moz_bookmarks (guid)
  type moz_keywords_placepostdata_uniqueindex (line 161) | CREATE UNIQUE INDEX moz_keywords_placepostdata_uniqueindex ON moz_keywor...
  type moz_annos_placeattributeindex (line 162) | CREATE UNIQUE INDEX moz_annos_placeattributeindex ON moz_annos (place_id...
  type moz_items_annos_itemattributeindex (line 163) | CREATE UNIQUE INDEX moz_items_annos_itemattributeindex ON moz_items_anno...

FILE: tests/test_cli.py
  function stdin (line 10) | def stdin(monkeypatch):
  function BukuDb (line 16) | def BukuDb():
  function bdb (line 22) | def bdb(BukuDb):
  function piped_input (line 26) | def piped_input():
  function prompt (line 31) | def prompt():
  function exit (line 36) | def exit():
  function test_version (line 41) | def test_version(BukuDb, piped_input, capsys):
  function test_usage (line 46) | def test_usage(BukuDb, piped_input, monkeypatch, capsys):
  function test_help (line 56) | def test_help(BukuDb, exit, piped_input, argv):
  function test_prompt (line 66) | def test_prompt(BukuDb, bdb, piped_input, prompt, nostdin, db):
  function test_add (line 112) | def test_add(stdin, bdb, prompt, value_params, fetch_params):
  function _test_add (line 115) | def _test_add(bdb, prompt, *, add_tags=[], tag=[], tags_fetch=True, tags...
  function test_order_print (line 158) | def test_order_print(bdb, stdin, prompt, order, indices, command, count,...
  function test_order_search (line 185) | def test_order_search(bdb, stdin, prompt, search, exclude, keywords, rest):
  function test_random (line 221) | def test_random(_print_json_safe, _format_json, _write_string_to_file, _...
  function test_random_export (line 272) | def test_random_export(_print_rec_with_filter, _sample, bdb, stdin, prom...
  function test_custom_db (line 307) | def test_custom_db(_BukuCrypt, BukuDb, stdin, db, action):

FILE: tests/test_import_firefox_json.py
  function test_load_from_empty (line 5) | def test_load_from_empty():
  function test_load_full_entry (line 17) | def test_load_full_entry():
  function test_load_no_typecode (line 64) | def test_load_no_typecode():
  function test_load_invalid_typecode (line 97) | def test_load_invalid_typecode():
  function test_load_folder_with_no_children (line 124) | def test_load_folder_with_no_children():
  function test_load_one_child (line 144) | def test_load_one_child():
  function test_load_one_container_child (line 185) | def test_load_one_container_child():
  function test_load_many_children (line 215) | def test_load_many_children():
  function test_load_container_no_title (line 246) | def test_load_container_no_title():
  function test_load_hierarchical_container_without_ignore (line 276) | def test_load_hierarchical_container_without_ignore():
  function test_load_hierarchical_container_with_ignore (line 307) | def test_load_hierarchical_container_with_ignore():
  function test_load_separator (line 349) | def test_load_separator():
  function test_load_multiple_tags (line 380) | def test_load_multiple_tags():

FILE: tests/test_requirements.py
  function pyproject (line 16) | def pyproject() -> dict[str, Any]:
  function test_bukuserver_requirement (line 25) | def test_bukuserver_requirement(pyproject: dict[str, Any]):
  function test_buku_requirement (line 29) | def test_buku_requirement(pyproject: dict[str, Any]):

FILE: tests/test_server.py
  function assert_response (line 14) | def assert_response(response, exp_res: Response, data: Dict[str, Any] = ...
  function test_response_json (line 26) | def test_response_json(data, exp_json):
  function test_cli (line 37) | def test_cli(args, word):
  function client (line 45) | def client(tmp_path):
  function test_home (line 54) | def test_home(client):
  function test_api_empty_db (line 66) | def test_api_empty_db(client, method, url, exp_res, data):
  function test_api_not_allowed (line 80) | def test_api_not_allowed(client, url, methods):
  function test_api_invalid_id (line 101) | def test_api_invalid_id(client, method, url, json, exp_res):
  function test_api_tag (line 106) | def test_api_tag(client):
  function test_api_bookmark (line 144) | def test_api_bookmark(client):
  function test_api_bookmark_delete (line 174) | def test_api_bookmark_delete(client, d_url):
  function test_api_bookmark_refresh (line 183) | def test_api_bookmark_refresh(client, api_url):
  function test_api_fetch_data (line 210) | def test_api_fetch_data(client, endpoint, kwargs, kwmock, exp_res, data):
  function test_api_bookmark_range (line 221) | def test_api_bookmark_range(client):
  function test_api_bookmark_search (line 278) | def test_api_bookmark_search(client):
  function test_get_bool_from_env_var (line 299) | def test_get_bool_from_env_var(monkeypatch, env_val, exp_val):

FILE: tests/test_views.py
  function dbfile (line 22) | def dbfile(tmp_path):
  function app (line 26) | def app(dbfile):
  function env_fixture (line 36) | def env_fixture(name, **kwargs):  # place this fixture BEFORE app or its...
  function client (line 51) | def client(app):
  function runner (line 55) | def runner(app):
  function bukudb (line 59) | def bukudb(dbfile):
  function tmv_instance (line 67) | def tmv_instance(bukudb):
  function bmv_instance (line 72) | def bmv_instance(bukudb):
  function test_filter_key (line 78) | def test_filter_key(idx, char):
  function test_bookmark_model_view (line 84) | def test_bookmark_model_view(bukudb, disable_favicon, app):
  function test_tag_model_view_get_list_empty_db (line 92) | def test_tag_model_view_get_list_empty_db(tmv_instance):
  function test_tag_model_view_get_list (line 107) | def test_tag_model_view_get_list(tmv_instance, sort_field, sort_desc, fi...
  function test_bmv_create_form (line 119) | def test_bmv_create_form(bmv_instance, url, backlink, app):
  function assert_success_alert (line 133) | def assert_success_alert(dom, edit, id=1):
  function assert_failure_alert (line 138) | def assert_failure_alert(dom, edit):
  function assert_response (line 142) | def assert_response(response, uri, *, status=200, argnames=None, args=No...
  function assert_bookmark (line 151) | def assert_bookmark(bookmark, query, tags=None):
  function test_bookmarklet_view (line 164) | def test_bookmarklet_view(bukudb, client, exists, uri, tab, args):
  function test_create_and_fetch (line 186) | def test_create_and_fetch(bukudb, monkeypatch, client, fetch, title, desc):
  function test_create_redirect (line 210) | def test_create_redirect(client, redirect, uri, args):
  function test_create_duplicate (line 220) | def test_create_duplicate(bukudb, client):
  function test_update (line 232) | def test_update(bukudb, client, override):
  function test_update_redirect (line 255) | def test_update_redirect(bukudb, client, redirect, uri, args):
  function test_delete (line 269) | def test_delete(client, bukudb, exists):
  function test_env_per_page (line 296) | def test_env_per_page(bukudb, app, client, total, per_page, pages, last_...
  function test_env_entry_render_params (line 316) | def test_env_entry_render_params(bukudb, app, client, mode, favicons, ne...
  function test_env_entry_render_params_blanks (line 325) | def test_env_entry_render_params_blanks(bukudb, app, client, mode, url, ...
  function _test_env_entry_render_params (line 328) | def _test_env_entry_render_params(bukudb, app, client, mode, favicons, n...
  function test_env_readonly (line 363) | def test_env_readonly(bukudb, readonly, client):
  function test_env_reverse_proxy_path (line 387) | def test_env_reverse_proxy_path(proxy_path, client):
  function test_env_theme (line 405) | def test_env_theme(theme, client):
  function test_env_locale (line 436) | def test_env_locale(bukudb, locale, client):

FILE: tests/util.py
  function mock_http (line 9) | def mock_http(body=None, **kwargs):
  function mock_fetch (line 13) | def mock_fetch(custom=None, **kwargs):
  function _add_rec (line 19) | def _add_rec(db, *args, **kw):
  function _tagset (line 23) | def _tagset(s):
  function append (line 26) | def append(buffer, text):
Condensed preview — 135 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,134K chars).
[
  {
    "path": ".circleci/config.yml",
    "chars": 3487,
    "preview": "version: 2.1\n\nexecutors:\n  docker-publisher:\n    environment:\n      IMAGE_NAME: bukuserver/bukuserver\n    docker:\n      "
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 14,
    "preview": "github: jarun\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE.md",
    "chars": 967,
    "preview": "#### Bug reports\n\nBefore opening an issue, please try to reproduce on [the latest development version](https://github.co"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 139,
    "preview": "Did you visit the [PR guidelines](https://github.com/jarun/Buku/wiki/PR-guidelines)?\n\n--- PLEASE DELETE THIS LINE AND EV"
  },
  {
    "path": ".github/workflows/lock.yml",
    "chars": 472,
    "preview": "name: 'Lock threads'\n\non:\n  schedule:\n    - cron: '0 0 * * *'\n  workflow_dispatch:\n\npermissions:\n  issues: write\n  pull-"
  },
  {
    "path": ".gitignore",
    "chars": 316,
    "preview": "*.py[co]\n*.sw[po]\n.cache/\n.coverage\n.hypothesis\nbuku.egg-info\ndist\nbuild\n.tox\n.history\n.vscode/launch.json\n/tests/test_b"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 1116,
    "preview": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\nrepos:\n- repo: "
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 580,
    "preview": "# .readthedocs.yaml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html f"
  },
  {
    "path": "CHANGELOG",
    "chars": 25752,
    "preview": "buku (unreleased)\n\n- Bukuserver: mitigate Host header poisoning via BUKUSERVER_SERVER_NAME config\n\nbuku v5.1\n2025-12-07\n"
  },
  {
    "path": "Dockerfile",
    "chars": 680,
    "preview": "FROM python:3.14.0-alpine\n\nLABEL org.opencontainers.image.authors=\"shenoy.ameya@gmail.com\"\n\nENV BUKUSERVER_PORT=5001\n\nCO"
  },
  {
    "path": "LICENSE",
    "chars": 35142,
    "preview": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
  },
  {
    "path": "MANIFEST.in",
    "chars": 322,
    "preview": "include CHANGELOG LICENSE README.md buku.1 requirements.txt\ninclude tests/test_bukuDb/Bookmarks\nrecursive-include tests "
  },
  {
    "path": "Makefile",
    "chars": 508,
    "preview": "PREFIX ?= /usr/local\nBINDIR ?= $(DESTDIR)$(PREFIX)/bin\nMANDIR ?= $(DESTDIR)$(PREFIX)/share/man/man1\nDOCDIR ?= $(DESTDIR)"
  },
  {
    "path": "README.md",
    "chars": 31831,
    "preview": "<h1 align=\"center\">buku</h1>\n\n<p align=\"center\">\n<a href=\"https://github.com/jarun/buku/releases/latest\"><img src=\"https"
  },
  {
    "path": "auto-completion/bash/buku-completion.bash",
    "chars": 1851,
    "preview": "#\n# Bash completion definition for buku.\n#\n# Author:\n#   Arun Prakash Jana <engineerarun@gmail.com>\n#\n\n_buku () {\n    CO"
  },
  {
    "path": "auto-completion/fish/buku.fish",
    "chars": 3979,
    "preview": "#\n# Fish completion definition for buku.\n#\n# Author:\n#   Arun Prakash Jana <engineerarun@gmail.com>\n#\ncomplete -c buku -"
  },
  {
    "path": "auto-completion/zsh/_buku",
    "chars": 3219,
    "preview": "#compdef buku\n#\n# Completion definition for buku.\n#\n# Author:\n#   Arun Prakash Jana <engineerarun@gmail.com>\n#\n\nsetopt l"
  },
  {
    "path": "buku.1",
    "chars": 34627,
    "preview": ".TH \"BUKU\" \"1\" \"07 Dec 2025\" \"Version 5.1\" \"User Commands\"\n.SH NAME\nbuku \\- Bookmark manager like a text-based mini-web\n"
  },
  {
    "path": "buku.py",
    "chars": 235417,
    "preview": "#!/usr/bin/env python3\n#\n# Bookmark management utility\n#\n# Copyright © 2015-2026 Arun Prakash Jana <engineerarun@gmail.c"
  },
  {
    "path": "bukuserver/README.md",
    "chars": 12417,
    "preview": "## Bukuserver\n\n_**Note: see [the runner script](https://github.com/jarun/buku/wiki/Bukuserver-%28WebUI%29#runner-script)"
  },
  {
    "path": "bukuserver/__init__.py",
    "chars": 917,
    "preview": "try:\n    from flask_babel import gettext, ngettext, pgettext, lazy_gettext, lazy_pgettext, LazyString\nexcept ImportError"
  },
  {
    "path": "bukuserver/__main__.py",
    "chars": 130,
    "preview": "try:\n    from . import server\nexcept ImportError:\n    from bukuserver import server\n\n\nif __name__ == '__main__':\n    ser"
  },
  {
    "path": "bukuserver/api.py",
    "chars": 12719,
    "preview": "#!/usr/bin/env python\n# pylint: disable=wrong-import-order, ungrouped-imports\n\"\"\"Server module.\"\"\"\nimport collections\nim"
  },
  {
    "path": "bukuserver/apidocs/bookmark/delete.yml",
    "chars": 495,
    "preview": "Delete an existing bookmark at current index\n---\n#DELETE /api/bookmarks/{index}\n\ntags: [Bookmarks]\n\nparameters:\n  - name"
  },
  {
    "path": "bukuserver/apidocs/bookmark/get.yml",
    "chars": 472,
    "preview": "Fetch the data of a bookmark at current index\n---\n#GET /api/bookmarks/{index}\n\ntags: [Bookmarks]\n\nparameters:\n  - name: "
  },
  {
    "path": "bukuserver/apidocs/bookmark/put.yml",
    "chars": 935,
    "preview": "Update an existing bookmark at current index\n---\n#PUT /api/bookmarks/{index}  {Bookmark}\n\ntags: [Bookmarks]\n\nparameters:"
  },
  {
    "path": "bukuserver/apidocs/bookmark_range/delete.yml",
    "chars": 643,
    "preview": "Delete all bookmarks in specified index range\n---\n#DELETE /api/bookmarks/{start_index}/{end_index}\n\ntags: [Bookmarks]\n\np"
  },
  {
    "path": "bukuserver/apidocs/bookmark_range/get.yml",
    "chars": 1061,
    "preview": "Fetch the data of all bookmarks in specified index range\n---\n#GET /api/bookmarks/{start_index}/{end_index}\n\ntags: [Bookm"
  },
  {
    "path": "bukuserver/apidocs/bookmark_range/put.yml",
    "chars": 1643,
    "preview": "Update all bookmarks in specified index range\n_Note that this request **does not overwrite tags** (instead, tags are add"
  },
  {
    "path": "bukuserver/apidocs/bookmark_refresh/post.yml",
    "chars": 496,
    "preview": "Refresh bookmark data from fetched resource.\n---\n#POST /api/bookmarks/{index}/refresh\n\ntags: [Util]\n\nparameters:\n  - nam"
  },
  {
    "path": "bukuserver/apidocs/bookmarks/delete.yml",
    "chars": 276,
    "preview": "Delete all bookmarks from the database\n---\n#DELETE /api/bookmarks\n\ntags: [Bookmarks]\n\nresponses:\n  200:\n    description:"
  },
  {
    "path": "bukuserver/apidocs/bookmarks/get.yml",
    "chars": 990,
    "preview": "Fetch the list of all bookmarks\n---\n#GET /api/bookmarks?order=\n\ntags: [Bookmarks]\n\nparameters:\n  - name: order\n    in: q"
  },
  {
    "path": "bukuserver/apidocs/bookmarks/post.yml",
    "chars": 912,
    "preview": "Create a new bookmark\n---\n#POST /api/bookmarks  {Bookmark}\n\ntags: [Bookmarks]\n\nparameters:\n  - name: form\n    in: body\n "
  },
  {
    "path": "bukuserver/apidocs/bookmarks_refresh/post.yml",
    "chars": 288,
    "preview": "Refresh all bookmark data from fetched resources.\n---\n#POST /api/bookmarks/refresh\n\ntags: [Util]\n\nresponses:\n  200:\n    "
  },
  {
    "path": "bukuserver/apidocs/bookmarks_reorder/post.yml",
    "chars": 1260,
    "preview": "Update DB indices to match specified order.\n---\n#POST /api/bookmarks/reorder  order=\n\ntags: [Util]\n\nparameters:\n  - name"
  },
  {
    "path": "bukuserver/apidocs/bookmarks_search/delete.yml",
    "chars": 2834,
    "preview": "Delete all bookmarks matching the query\nYou can send a non-boolean value in a boolean field, but it is only considered t"
  },
  {
    "path": "bukuserver/apidocs/bookmarks_search/get.yml",
    "chars": 2786,
    "preview": "Fetch all bookmarks matching the query\nYou can send a non-boolean value in a boolean field, but it is only considered tr"
  },
  {
    "path": "bukuserver/apidocs/fetch_data/post.yml",
    "chars": 1761,
    "preview": "Fetch data from URL (i.e. to test parsing functionality)\n---\n#POST /api/fetch_data  url=\n\ntags: [Util]\nconsumes: ['appli"
  },
  {
    "path": "bukuserver/apidocs/network_handle/post.yml",
    "chars": 1526,
    "preview": "Fetch data from URL (i.e. to test parsing functionality)\n`[DEPRECATED]` prefer **/api/fetch_data**\n---\n#POST /api/networ"
  },
  {
    "path": "bukuserver/apidocs/tag/delete.yml",
    "chars": 667,
    "preview": "Remove the specified tag from all bookmarks\n---\n#DELETE /api/tags/{tag}\n\ntags: [Tags]\n\nparameters:\n  - name: tag\n    in:"
  },
  {
    "path": "bukuserver/apidocs/tag/get.yml",
    "chars": 768,
    "preview": "Get information on the specified tag\n---\n#GET /api/tags/{tag}\n\ntags: [Tags]\n\nparameters:\n  - name: tag\n    in: path\n    "
  },
  {
    "path": "bukuserver/apidocs/tag/put.yml",
    "chars": 912,
    "preview": "Replace the specified tag with one or more new tags\n---\n#PUT /api/tags/{tag}  {\"tags\": []}\n\ntags: [Tags]\n\nparameters:\n  "
  },
  {
    "path": "bukuserver/apidocs/tags/get.yml",
    "chars": 396,
    "preview": "Fetch the list of all tags\n---\n#GET /api/tags\n\ntags: [Tags]\n\nresponses:\n  200:\n    description: A list of tags (sorted l"
  },
  {
    "path": "bukuserver/apidocs/template.yml",
    "chars": 5793,
    "preview": "info:\n  title: \"Bukuserver API\"\n  description: \"RESTful API for managing your personal bookmarks.\"\n  version: 'v5.0'\n\nco"
  },
  {
    "path": "bukuserver/apidocs/tiny_url/get.yml",
    "chars": 321,
    "preview": "Fetch the shortened URL\n`[NO LONGER AVAILABLE]`\n---\n#GET /api/bookmarks/{index}/tiny\n\ntags: [Util]\n\nparameters:\n  - name"
  },
  {
    "path": "bukuserver/bookmarklet.js",
    "chars": 883,
    "preview": "// source for the bookmarklet in templates/bukuserver/bookmarklet.url:\n//\n// 1. paste this code in https://bookmarklets."
  },
  {
    "path": "bukuserver/filters.py",
    "chars": 8663,
    "preview": "from enum import Enum\n\nfrom flask_admin.model import filters\nfrom bukuserver import _l, _key\n\n\nclass BookmarkField(Enum)"
  },
  {
    "path": "bukuserver/forms.py",
    "chars": 5556,
    "preview": "\"\"\"Forms module.\"\"\"\n# pylint: disable=too-few-public-methods, missing-docstring\nimport re\nfrom flask_wtf import FlaskFor"
  },
  {
    "path": "bukuserver/middleware/__init__.py",
    "chars": 96,
    "preview": "from .flask_reverse_proxy_fix import ReverseProxyPrefixFix\n\n__all__ = ['ReverseProxyPrefixFix']\n"
  },
  {
    "path": "bukuserver/middleware/flask_reverse_proxy_fix.py",
    "chars": 2563,
    "preview": "# clone of flask-reverse-proxy-fix with github fixes from @rachmadaniHaryono and @Glocktober\n\nfrom flask import Flask as"
  },
  {
    "path": "bukuserver/requirements.txt",
    "chars": 106,
    "preview": "arrow>=1.2.2\nFlask-Admin>=2.0.0\nflask-paginate>=2022.1.8\nFlask-WTF>=1.0.1\nFlask>=2.2.2\nJinja2>=3\nflasgger\n"
  },
  {
    "path": "bukuserver/response.py",
    "chars": 2060,
    "preview": "from typing import Any, Dict\nfrom enum import Enum\nfrom http import HTTPStatus\nfrom flask import jsonify\n\nOK, FAIL = 0, "
  },
  {
    "path": "bukuserver/server.py",
    "chars": 8669,
    "preview": "#!/usr/bin/env python\n# pylint: disable=wrong-import-order, ungrouped-imports\n\"\"\"Server module.\"\"\"\nimport os\nimport sys\n"
  },
  {
    "path": "bukuserver/static/bukuserver/css/bookmark.css",
    "chars": 746,
    "preview": "/* preventing layout overflow in tables */\ntable tr td:not(:first-child) {\n    overflow-wrap: anywhere;\n}\n\n/* wider moda"
  },
  {
    "path": "bukuserver/static/bukuserver/css/list.css",
    "chars": 1510,
    "preview": "/* overriding icon-button text color with theme color */\nform.icon button {\n    color: inherit;\n}\n\n/* fixing table layou"
  },
  {
    "path": "bukuserver/static/bukuserver/css/modal.css",
    "chars": 651,
    "preview": "/* prevent unnecessary scrollbox from appearing at the main window */\nbody.modal-open {\n    overflow-y: inherit;\n}\n\n/* l"
  },
  {
    "path": "bukuserver/static/bukuserver/js/Chart.js",
    "chars": 403941,
    "preview": "/*!\n * Chart.js\n * http://chartjs.org/\n * Version: 2.7.2\n *\n * Copyright 2018 Chart.js Contributors\n * Released under th"
  },
  {
    "path": "bukuserver/static/bukuserver/js/bookmark.js",
    "chars": 378,
    "preview": "$(document).ready(function() {\n  window._tags = (Date.now() - (window._tagsQueried||0) < 1000 ? _tags :\n                "
  },
  {
    "path": "bukuserver/static/bukuserver/js/buku_filter.js",
    "chars": 1366,
    "preview": "$(document).ready(function () {  // synchronizing buku filters\n  let bukuFilters = () => $(`option[value^=\"buku_\"]`).par"
  },
  {
    "path": "bukuserver/static/bukuserver/js/filters_fix.js",
    "chars": 709,
    "preview": "$(document).ready(function () {\n  const IDX = [[36, 'a'], [10, 'A'], [0, '0']];\n  let idxChar = (i, [x, c]=IDX.find(([x]"
  },
  {
    "path": "bukuserver/static/bukuserver/js/last_page.js",
    "chars": 216,
    "preview": "$(document).ready(function() {\n  $(`.pagination :contains(\"»\") a`).not(`[href^=\"javascript:\"]`).attr('href', (idx, href)"
  },
  {
    "path": "bukuserver/static/bukuserver/js/order_filter.js",
    "chars": 1108,
    "preview": "$(document).ready(function () {  // retaining state of same-kind filters on switch\n  let config = JSON.parse(document.ge"
  },
  {
    "path": "bukuserver/templates/bukuserver/bookmark_create.html",
    "chars": 417,
    "preview": "{% extends 'admin/model/create.html' %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block tail %}\n  {{ s"
  },
  {
    "path": "bukuserver/templates/bukuserver/bookmark_create_modal.html",
    "chars": 530,
    "preview": "{% extends 'admin/model/modals/create.html' %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block create_"
  },
  {
    "path": "bukuserver/templates/bukuserver/bookmark_details.html",
    "chars": 394,
    "preview": "{% extends 'admin/model/details.html' %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block tail %}\n  {{ "
  },
  {
    "path": "bukuserver/templates/bukuserver/bookmark_details_modal.html",
    "chars": 653,
    "preview": "{% extends 'admin/model/modals/details.html' %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block header"
  },
  {
    "path": "bukuserver/templates/bukuserver/bookmark_edit.html",
    "chars": 1091,
    "preview": "{% extends 'admin/model/edit.html' %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block head %}\n  {{ sup"
  },
  {
    "path": "bukuserver/templates/bukuserver/bookmark_edit_modal.html",
    "chars": 250,
    "preview": "{% extends 'admin/model/modals/edit.html' %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block tail %}\n "
  },
  {
    "path": "bukuserver/templates/bukuserver/bookmarklet.url",
    "chars": 637,
    "preview": "javascript:void%20function(){var%20e=location.href,t=document.title.trim()||%22%22,o=document.getSelection().toString()."
  },
  {
    "path": "bukuserver/templates/bukuserver/bookmarks_list.html",
    "chars": 4342,
    "preview": "{% extends 'admin/model/list.html' %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block head %}\n  {{ sup"
  },
  {
    "path": "bukuserver/templates/bukuserver/home.html",
    "chars": 4457,
    "preview": "{% extends \"admin/index.html\" %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block head %}\n  {{ super() "
  },
  {
    "path": "bukuserver/templates/bukuserver/lib.html",
    "chars": 7960,
    "preview": "{% macro filter(name, value) %}{{ url_for('bookmark.index_view', **{'flt0_'+name: value}) }}{% endmacro %}\n\n{% macro boo"
  },
  {
    "path": "bukuserver/templates/bukuserver/statistic.html",
    "chars": 12236,
    "preview": "{% extends \"bukuserver/home.html\" %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block head %}\n  {{ supe"
  },
  {
    "path": "bukuserver/templates/bukuserver/tag_edit.html",
    "chars": 247,
    "preview": "{% extends 'admin/model/edit.html' %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block tail %}\n  {{ sup"
  },
  {
    "path": "bukuserver/templates/bukuserver/tags_list.html",
    "chars": 718,
    "preview": "{% extends 'admin/model/list.html' %}\n{% import 'bukuserver/lib.html' as buku with context %}\n\n{% block head %}\n  {{ sup"
  },
  {
    "path": "bukuserver/translations/.gitignore",
    "chars": 14,
    "preview": "/messages.pot\n"
  },
  {
    "path": "bukuserver/translations/README.md",
    "chars": 2790,
    "preview": "## Bukuserver translations\n\nThis directory contains translations activated by installing Flask-Babel(Ex) and providing `"
  },
  {
    "path": "bukuserver/translations/__init__.py",
    "chars": 4086,
    "preview": "import os\nimport re\nfrom babel.messages.frontend import CommandLineInterface as pybabel\ntry:\n    from buku import __vers"
  },
  {
    "path": "bukuserver/translations/__main__.py",
    "chars": 775,
    "preview": "#!/usr/bin/env python\nimport os\nimport sys\n\ntry:\n    from . import translations_compile, __version__\nexcept ImportError:"
  },
  {
    "path": "bukuserver/translations/babel.cfg",
    "chars": 47,
    "preview": "[python: **.py]\n[jinja2: **/templates/**.html]\n"
  },
  {
    "path": "bukuserver/translations/de/LC_MESSAGES/messages.po",
    "chars": 16474,
    "preview": "# German translations for bukuserver.\n# Copyright (C) 2024 buku\n# This file is distributed under the same license as the"
  },
  {
    "path": "bukuserver/translations/fr/LC_MESSAGES/messages.po",
    "chars": 16480,
    "preview": "# French translations for bukuserver.\n# Copyright (C) 2024 buku\n# This file is distributed under the same license as the"
  },
  {
    "path": "bukuserver/translations/messages_custom.pot",
    "chars": 764,
    "preview": "msgid \"Original and replacement tags are the same.\"\nmsgstr \"\"\n\nmsgid \"Tag name cannot be blank.\"\nmsgstr \"\"\n\nmsgid \"by in"
  },
  {
    "path": "bukuserver/translations/ru/LC_MESSAGES/messages.po",
    "chars": 20141,
    "preview": "# Russian translations for bukuserver.\n# Copyright (C) 2024 buku\n# This file is distributed under the same license as th"
  },
  {
    "path": "bukuserver/util.py",
    "chars": 543,
    "preview": "from collections import Counter\nfrom urllib.parse import urlparse\nimport re\n\nget_netloc = lambda x: urlparse(x).netloc  "
  },
  {
    "path": "bukuserver/views.py",
    "chars": 29100,
    "preview": "\"\"\"views module.\"\"\"\nimport functools\nimport itertools\nimport logging\nimport random\nimport re\nimport json\nimport types\nfr"
  },
  {
    "path": "bukuserver-runner/README.md",
    "chars": 4426,
    "preview": "# Bukuserver runner\n\nThis tool can be used to run and restart Bukuserver, switching databases between runs. It has no th"
  },
  {
    "path": "bukuserver-runner/buku-server-headless.desktop",
    "chars": 377,
    "preview": "# This file can be used as a windowless startup/restart shortcut on Linux desktop/menu (after editing the Exec= command "
  },
  {
    "path": "bukuserver-runner/buku-server.desktop",
    "chars": 400,
    "preview": "# This file can be used as a windowed startup/restart shortcut on Linux desktop/menu (after editing the Exec= command li"
  },
  {
    "path": "bukuserver-runner/buku-server.py",
    "chars": 11902,
    "preview": "#!/usr/bin/env python\n# Usage: `buku-server.py` starts up the server, `buku-server.py --stop` sends TERM to the already "
  },
  {
    "path": "docker-compose/docker-compose.yml",
    "chars": 579,
    "preview": "services:\n  bukuserver:\n    image: bukuserver/bukuserver\n    restart: unless-stopped\n    environment:\n      - BUKUSERVER"
  },
  {
    "path": "docs/source/buku.rst",
    "chars": 103,
    "preview": "buku module\n===========\n\n.. automodule:: buku\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/source/bukuserver.rst",
    "chars": 741,
    "preview": "bukuserver package\n==================\n\nbukuserver.filters module\n-------------------------\n\n.. automodule:: bukuserver.f"
  },
  {
    "path": "docs/source/conf.py",
    "chars": 5324,
    "preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n#\n# buku documentation build configuration file, created by\n# sphinx-quic"
  },
  {
    "path": "docs/source/index.rst",
    "chars": 679,
    "preview": ".. buku documentation master file, created by\n   sphinx-quickstart on Thu Sep  7 12:54:59 2017.\n   You can adapt this fi"
  },
  {
    "path": "docs/source/modules.rst",
    "chars": 124,
    "preview": "buku\n==========\n\n.. toctree::\n   :maxdepth: 4\n\n   buku\n\nbukuserver\n==========\n\n.. toctree::\n   :maxdepth: 4\n\n   bukuserv"
  },
  {
    "path": "docs/source/tutorial_for_developer.md",
    "chars": 603,
    "preview": "# Tutorial for Developer\n\n## get buku database\n\n```python\n>>> import buku\n>>> bdb = buku.BukuDb()\n```\n\n## simplest way t"
  },
  {
    "path": "mypy.ini",
    "chars": 37,
    "preview": "[mypy]\nignore_missing_imports = True\n"
  },
  {
    "path": "packagecore.yaml",
    "chars": 3062,
    "preview": "name: buku-cli\nmaintainer: Arun Prakash Jana <engineerarun@gmail.com>\nlicense: GPLv3\nsummary: Bookmark manager like a te"
  },
  {
    "path": "pyproject.toml",
    "chars": 2757,
    "preview": "[project]\nname = \"buku\"\ndescription = \"Bookmark manager like a text-based mini-web.\"\nkeywords = [\"cli\", \"bookmarks\", \"ta"
  },
  {
    "path": "requirements.txt",
    "chars": 215,
    "preview": "# use setup.py for latest required package\nbeautifulsoup4>=4.4.1\ncertifi\ncryptography>=1.2.3\nhtml5lib>=1.0.1\nsetuptools\n"
  },
  {
    "path": "tests/.pylintrc",
    "chars": 1035,
    "preview": "[MESSAGES CONTROL]\ndisable=\n  assigning-non-slot,\n  broad-except,\n  c-extension-no-member,\n  consider-using-f-string,\n  "
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/cassettes/test_buku/test_fetch_data_with_url[http---example.com-exp_res1].yaml",
    "chars": 2346,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip,def"
  },
  {
    "path": "tests/cassettes/test_buku/test_fetch_data_with_url[http---example.com-page1.txt-exp_res2].yaml",
    "chars": 787,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip,def"
  },
  {
    "path": "tests/cassettes/test_buku/test_fetch_data_with_url[http---www.vim.org-scripts-script.php~-exp_res7].yaml",
    "chars": 22031,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip,def"
  },
  {
    "path": "tests/cassettes/test_buku/test_fetch_data_with_url[https---www.google.ru-search~-exp_res6].yaml",
    "chars": 339704,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept:\n      - '*/*'\n      Accept-Encoding:\n      - gzip,def"
  },
  {
    "path": "tests/genbm.sh",
    "chars": 543,
    "preview": "#!/bin/bash\n\n# Scriptlet to auto-generate buku bookmarks\n# Usage: genbm.sh n\n#        where, n = number of records to ge"
  },
  {
    "path": "tests/pytest.ini",
    "chars": 131,
    "preview": "[pytest]\ntimeout = 10\ntimeout_method = thread\nmarkers =\n  non_tox: not run on tox\n  slow: slow tests\n  gui: GUI (functio"
  },
  {
    "path": "tests/test_BukuCrypt.py",
    "chars": 1306,
    "preview": "\"\"\"test module.\"\"\"\nimport os\nimport random\n\nimport pytest\n\n\ndef test_get_filehash(tmpdir):\n    \"\"\"test method.\"\"\"\n    ex"
  },
  {
    "path": "tests/test_ExtendedArgumentParser.py",
    "chars": 1089,
    "preview": "\"\"\"test module.\"\"\"\nfrom itertools import product\nfrom unittest import mock\n\nimport pytest\n\n\n@pytest.mark.parametrize(\"pl"
  },
  {
    "path": "tests/test_buku.py",
    "chars": 39172,
    "preview": "\"\"\"test module.\"\"\"\nimport json\nimport logging\nimport os\nimport signal\nimport unittest\nfrom itertools import product\nfrom"
  },
  {
    "path": "tests/test_bukuDb/25491522_res.yaml",
    "chars": 2491,
    "preview": "? !!python/tuple ['http://voyagerlive.org/', Voyager, ',bookmarks bar,', null, 0,\n  true, false]\n: {}\n? !!python/tuple ["
  },
  {
    "path": "tests/test_bukuDb/25491522_res_nopt.yaml",
    "chars": 1636,
    "preview": "? !!python/tuple ['http://voyagerlive.org/', Voyager, ',', null, 0, true, false]\n: {}\n? !!python/tuple ['http://wiki.ubu"
  },
  {
    "path": "tests/test_bukuDb/Bookmarks",
    "chars": 11374,
    "preview": "{\n   \"checksum\": \"159ce0a29c234510ba09c3091e0b513b\",\n   \"roots\": {\n      \"bookmark_bar\": {\n         \"children\": [ {\n    "
  },
  {
    "path": "tests/test_bukuDb/firefox_res.yaml",
    "chars": 2393,
    "preview": "? !!python/tuple ['http://voyagerlive.org/', Voyager, ',bookmarks toolbar,', null,\n  0, true, false]\n: {}\n? !!python/tup"
  },
  {
    "path": "tests/test_bukuDb/firefox_res_nopt.yaml",
    "chars": 1912,
    "preview": "? !!python/tuple ['http://voyagerlive.org/', Voyager, ',', null, 0, true, false]\n: {}\n? !!python/tuple ['http://wiki.ubu"
  },
  {
    "path": "tests/test_bukuDb/places.sql",
    "chars": 20021,
    "preview": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE moz_places (   id INTEGER PRIMARY KEY, url LONGVARCHAR, title L"
  },
  {
    "path": "tests/test_bukuDb.py",
    "chars": 82894,
    "preview": "#!/usr/bin/env python3\n#\n# Unit test cases for buku\n#\nimport math\nimport os\nimport re\nimport sqlite3\nimport sys\nimport u"
  },
  {
    "path": "tests/test_cli.py",
    "chars": 15693,
    "preview": "from unittest import mock\nfrom io import StringIO\nimport os\nimport pytest\n\nimport buku\n\n\n@pytest.fixture\ndef stdin(monke"
  },
  {
    "path": "tests/test_import_firefox_json.py",
    "chars": 10000,
    "preview": "import json\nfrom buku import import_firefox_json\n\n\ndef test_load_from_empty():\n    \"\"\"test method.\"\"\"\n    # Arrange\n    "
  },
  {
    "path": "tests/test_requirements.py",
    "chars": 862,
    "preview": "import pathlib\nimport sys\nfrom typing import Any\n\nimport pytest\n\nif sys.version_info >= (3, 11):\n    import tomllib\nelse"
  },
  {
    "path": "tests/test_server.py",
    "chars": 13211,
    "preview": "import os\nfrom typing import Any, Dict\nfrom http import HTTPStatus\nimport pytest\nimport flask\nfrom click.testing import "
  },
  {
    "path": "tests/test_views.py",
    "chars": 18823,
    "preview": "\"\"\"test for views.\n\nresources: https://flask.palletsprojects.com/en/2.2.x/testing/\n\"\"\"\nimport os\nfrom argparse import Na"
  },
  {
    "path": "tests/util.py",
    "chars": 1039,
    "preview": "from unittest import mock\nimport os\n\nfrom urllib3 import HTTPResponse\n\nfrom buku import FetchResult\n\n\ndef mock_http(body"
  },
  {
    "path": "tests/vcr_cassettes/test_browse_by_index.yaml",
    "chars": 103995,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept: ['*/*']\n      Accept-Encoding: ['gzip,deflate']\n     "
  },
  {
    "path": "tests/vcr_cassettes/test_delete_rec_range_and_delay_commit.yaml",
    "chars": 138506,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept: ['*/*']\n      Accept-Encoding: ['gzip,deflate']\n     "
  },
  {
    "path": "tests/vcr_cassettes/test_search_by_multiple_tags_search_all.yaml",
    "chars": 71073,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept: ['*/*']\n      Accept-Encoding: ['gzip,deflate']\n     "
  },
  {
    "path": "tests/vcr_cassettes/test_search_by_multiple_tags_search_any.yaml",
    "chars": 71612,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept: ['*/*']\n      Accept-Encoding: ['gzip,deflate']\n     "
  },
  {
    "path": "tests/vcr_cassettes/test_search_by_tags_enforces_space_seprations_exclusion.yaml",
    "chars": 926,
    "preview": "interactions:\n- request:\n    body: null\n    headers:\n      Accept: ['*/*']\n      Accept-Encoding: ['gzip,deflate']\n     "
  },
  {
    "path": "tox.bat",
    "chars": 63,
    "preview": "@if not defined BASEPYTHON set BASEPYTHON=python\r\n@tox.exe %*\r\n"
  },
  {
    "path": "tox.ini",
    "chars": 2072,
    "preview": "[tox]\nenvlist = py310,py311,py312,py313,py314,pylint,flake8\n\n[flake8]\nmax-line-length = 139\nexclude =\n    .tox\n    build"
  }
]

// ... and 3 more files (download for full content)

About this extraction

This page contains the full source code of the jarun/buku GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 135 files (1.9 MB), approximately 729.7k tokens, and a symbol index with 759 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!