main 73837d998be8 cached
38 files
97.0 KB
23.1k tokens
96 symbols
1 requests
Download .txt
Repository: essembeh/gnome-extensions-cli
Branch: main
Commit: 73837d998be8
Files: 38
Total size: 97.0 KB

Directory structure:
gitextract_c9d6yaeg/

├── .envrc
├── .github/
│   └── workflows/
│       └── poetry.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── Justfile
├── LICENSE
├── README.md
├── gnome_extensions_cli/
│   ├── __init__.py
│   ├── cli.py
│   ├── commands/
│   │   ├── __init__.py
│   │   ├── disable.py
│   │   ├── enable.py
│   │   ├── install.py
│   │   ├── list_.py
│   │   ├── preferences.py
│   │   ├── search.py
│   │   ├── show.py
│   │   ├── uninstall.py
│   │   └── update.py
│   ├── dbus.py
│   ├── filesystem.py
│   ├── icons.py
│   ├── manager.py
│   ├── schema.py
│   ├── store.py
│   └── utils.py
├── poetry.toml
├── pyproject.toml
└── tests/
    ├── __init__.py
    ├── samples/
    │   ├── available-alt.json
    │   ├── available.json
    │   ├── installed.json
    │   └── search.json
    ├── test_cli.py
    ├── test_dbus.py
    ├── test_filesystem.py
    ├── test_model.py
    └── test_store.py

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

================================================
FILE: .envrc
================================================
export VIRTUAL_ENV=.venv
layout python python3


================================================
FILE: .github/workflows/poetry.yml
================================================
name: Build & Tests

on: [push]

jobs:
  test:
    name: Build and test App
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.9'
      - name: Install dependencies
        run: |
          sudo apt update
          sudo apt install -qy --no-install-recommends gnome-shell-extensions
          sudo apt install -qy libgirepository1.0-dev gcc libcairo2-dev pkg-config python3-dev gir1.2-gtk-3.0
      - name: Install poetry
        run: pip install poetry
      - name: Build app
        run: poetry build
      - name: Test app
        run: |
          poetry install
          poetry run pytest --cov=gnome_extensions_cli tests/

  publish:
    name: Publish App on PyPI
    if: startsWith(github.ref, 'refs/tags/')
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.9'
      - name: Install poetry
        run: pip install poetry
      - name: Publish app
        env:
          PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
        run: poetry publish --build --username "__token__" --password "$PYPI_TOKEN"

================================================
FILE: .gitignore
================================================
# Python
__pycache__
*.pyc

# Generated
/.coverage
/coverage.xml
/htmlcov/
/dist/

# IDE
/.vscode/*
!/.vscode/settings.json


================================================
FILE: .vscode/settings.json
================================================
{
    "python.pythonPath": ".venv/bin/python",
}


================================================
FILE: Justfile
================================================
release bump="patch":
    echo "{{bump}}" | grep -E "^(major|minor|patch)$"
    poetry version "{{bump}}"
    git add pyproject.toml
    git commit --message "🔖 New release: `poetry version -s`"
    git tag "`poetry version -s`"

[confirm('Confirm push --tags ?')]
publish:
    git log -1 --pretty="%B" | grep '^🔖 New release: '
    git push
    git push --tags


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
![Github](https://img.shields.io/github/tag/essembeh/gnome-extensions-cli.svg)
![PyPi](https://img.shields.io/pypi/v/gnome-extensions-cli.svg)
![Python](https://img.shields.io/pypi/pyversions/gnome-extensions-cli.svg)
![CI](https://github.com/essembeh/gnome-extensions-cli/actions/workflows/poetry.yml/badge.svg)

# gnome-extensions-cli

Install, update and manage your Gnome Shell extensions from your terminal !

# Features

- You can install any extension available on [Gnome website](https://extensions.gnome.org)
- Use _DBus_ to communicate with _Gnome Shell_ like the Firefox addon does
  - Also support non-DBus installations if needed
- Automatically select the compatible version to install for your Gnome Shell
- Update all your extensions with one command: `gext update`

Available commands:

- `gext list` to list you installed extensions
- `gext search` to search for extensions on [Gnome website](https://extensions.gnome.org)
- `gext install` to install extensions
- `gext update` to update any or all your extensions
- `gext uninstall` to uninstall extensions
- `gext show` to show details about extensions
- `gext enable` to enable extensions
- `gext disable` to disable extensions
- `gext preferences` to open the extension configuration window

> Note: `gext` is an alias of `gnome-extensions-cli`

# Install

## Releases

Releases are available on [PyPI](https://pypi.org/project/gnome-extensions-cli/)

> Note: [PipX](https://pypi.org/project/pipx/) is the recommended way to install 3rd-party apps in dedicated environments.

```sh
# install using pip
$ pip3 install --upgrade gnome-extensions-cli

# or using pipx (you need to install pipx first)
$ pipx install gnome-extensions-cli --system-site-packages

# gext is an alias for gnome-extensions-cli
$ gnome-extensions-cli --help
$ gext --help
```

## From the source

You can also install the _latest_ version from the Git repository:

```sh
$ pip3 install --upgrade git+https://github.com/essembeh/gnome-extensions-cli
```

You can setup a development environment with, requires [Poetry](https://python-poetry.org/)

```sh
# dependencies to install PyGObject with pip
$ sudo apt install libgirepository1.0-dev gcc libcairo2-dev pkg-config python3-dev gir1.2-gtk-3.0

# clone the repository
$ git clone https://github.com/essembeh/gnome-extensions-cli
$ cd gnome-extensions-cli

# install poetry if you don't have it yet
$ pipx install poetry

# create the venv using poetry
$ poetry install
$ poetry shell
(.venv) $ gnome-extensions-cli --help
```

# Using

By default commands output use terminal colors and styles for a better experience.
If you want to disable the colors and style, when using `gext` in shell scripts for example, you can 
- use `gext --no-color ...` 
- or set the environment variable `export NO_COLOR=1` in your shell script before calling `gext`


## List your extensions

By default, the `list` command only display the _enabled_ extensions, using `-a|--all` argument also displays _disabled_ ones.

![gext list](images/list.png)

## Install, update or uninstall extensions

The `install` commands allows you to install extensions from their _uuid_ or _pk_.

> Note: You can use `search` command to find extensions, `gext` prints _uuids_ in _yellow_ .

```sh
# Install extension by its UUID
$ gext install dash-to-panel@jderose9.github.com

# or use its package number from https://extensions.gnome.org
$ gext install 1160

# You can also install multiple extensions at once
$ gext install 1160 todo.txt@bart.libert.gmail.com

# Uninstall extensions
$ gext uninstall todo.txt@bart.libert.gmail.com

# You can enable and disable extensions
$ gext enable todo.txt@bart.libert.gmail.com
$ gext disable todo.txt@bart.libert.gmail.com dash-to-panel@jderose9.github.com
```

![gext install](images/install.gif)

The `update` command without arguments updates all _enabled_ extensions.
You can also `update` a specific extension by giving its _uuid_.

![gext update](images/update.gif)

> Note: the `--install` argument allow you to _install_ extensions given to `update` command if they are not installed.

## Search for extensions and show details

The `search` command searches from [Gnome website](https://extensions.gnome.org) and prints results in your terminal:

![gext search](images/search.png)

The `show` command fetches details from _Gnome website_ and prints them:

![gext show](images/show.png)

# Under the hood: DBus vs Filesystem

`gext` can interact with Gnome Shell using two different implementations, using `dbus` or using a `filesystem` operations.

> Note: By default, it uses `dbus` (as it is the official way), but switches to `filesystem` if `dbus` is not available (like with _ssh_ sessions)

## DBus

Using `--dbus`, the application uses _dbus_ messages with DBus Python API to communicate with Gnome Shell directly.

Installations are interactive, like when you install extensions from your browser on Gnome website, you are prompted with a Gnome _Yes/No_ dialog before installing the extensions

Pros:

- You are using the exact same way to install extensions as the Firefox addon
- Automatically restart the Gnome Shell when needed
- Very stable
- You can open the extension preference dialog with `gext edit EXTENSION_UUID`

Cons:

- You need to have a running Gnome session

## Filesystem backend

Using `--filesystem`, the application uses unzip packages from [Gnome website](https://extensions.gnome.org) directly in you `~/.local/share/gnome-shell/extensions/` folder, enable/disable them and restarting the Gnome Shell using subprocesses.

Pros:

- You can install extensions without any Gnome session running (using _ssh_ for example)
- Many `gext` alternatives CLI tools use this method

Cons:

- Some extensions might not install well


================================================
FILE: gnome_extensions_cli/__init__.py
================================================
"""
gnome-extensions-cli
"""

from importlib.metadata import version

__version__ = version(__name__)


================================================
FILE: gnome_extensions_cli/cli.py
================================================
"""
gnome-extensions-cli
"""

import sys
from argparse import ArgumentParser
from os import getenv

from colorama import init

from . import __version__
from .commands import (
    disable,
    enable,
    install,
    list_,
    preferences,
    search,
    show,
    uninstall,
    update,
)
from .dbus import DbusExtensionManager, test_dbus_available
from .filesystem import FilesystemExtensionManager
from .icons import Color, Icons
from .store import GnomeExtensionStore


def run():
    """
    entry point
    """
    parser = ArgumentParser(description="Gnome Shell extensions manager")

    parser.add_argument(
        "--version", action="version", version=f"%(prog)s {__version__}"
    )

    parser.add_argument(
        "--no-color",
        action="store_true",
        help="disable colors and style in output text "
        + "(you can also set NO_COLOR=1 instead of using this option)",
    )
    parser.add_argument(
        "--no-compile-schemas",
        action="store_false",
        help="when using filesystem backend, do not compile schemas with glib-compile-schemas if needed",
    )
    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        "-D",
        "--dbus",
        action="store_const",
        dest="backend",
        const="dbus",
        help="force DBus backend",
    )
    group.add_argument(
        "-F",
        "--filesystem",
        action="store_const",
        dest="backend",
        const="filesystem",
        help="force filesystem backend",
    )

    subparsers = parser.add_subparsers()

    list_.configure(
        subparsers.add_parser("list", aliases=["ls"], help="list installed extensions")
    )
    search.configure(
        subparsers.add_parser("search", aliases=[], help="search for extensions")
    )
    show.configure(
        subparsers.add_parser("show", aliases=[], help="show extensions details")
    )

    install.configure(
        subparsers.add_parser("install", aliases=["i"], help="install extensions")
    )
    uninstall.configure(
        subparsers.add_parser("uninstall", aliases=[], help="uninstall extensions")
    )
    update.configure(
        subparsers.add_parser("update", aliases=["u"], help="update extensions")
    )

    enable.configure(
        subparsers.add_parser("enable", aliases=[], help="enable extensions")
    )
    disable.configure(
        subparsers.add_parser("disable", aliases=[], help="disable extensions")
    )
    preferences.configure(
        subparsers.add_parser(
            "preferences",
            aliases=["p", "config"],
            help="edit preferences of extension",
        )
    )

    args = parser.parse_args()

    # handle nocolor in output
    if args.no_color or getenv("NO_COLOR") is not None:
        init(strip=True, convert=False)
    else:
        init()

    try:
        # instantiate store
        store = GnomeExtensionStore()

        # instantiate manager
        manager = None
        if args.backend == "dbus":
            manager = DbusExtensionManager()
        elif args.backend == "filesystem":
            manager = FilesystemExtensionManager(store)
        elif test_dbus_available(getenv("DEBUG") == "1"):
            manager = DbusExtensionManager()
        else:
            manager = FilesystemExtensionManager(store)
        handler = args.handler if "handler" in args else list_.run
        handler(args, manager, store)
    except KeyboardInterrupt:
        print(Icons.ERROR, "Process interrupted")
        sys.exit(1)
    except SystemExit:
        raise
    except BaseException as error:  # pylint: disable=broad-except
        print(Icons.BOOM, "Error:", Color.RED(error))
        raise error


================================================
FILE: gnome_extensions_cli/commands/__init__.py
================================================


================================================
FILE: gnome_extensions_cli/commands/disable.py
================================================
"""
gnome-extensions-cli
"""

from argparse import ZERO_OR_MORE, ArgumentParser, Namespace

from ..icons import Color
from ..manager import ExtensionManager
from ..store import GnomeExtensionStore


def configure(parser: ArgumentParser):
    """
    Configure parser for subcommand
    """
    parser.set_defaults(handler=run)

    parser.add_argument(
        "--not-installed",
        action="store_true",
        help="disable all extensions which are not installed",
    )
    parser.add_argument(
        "uuids",
        nargs=ZERO_OR_MORE,
        metavar="UUID",
        help="uuid of extensions to disable",
    )


def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensionStore):
    """
    Handler for subcommand
    """
    uuids = list(set(args.uuids))
    if args.not_installed:
        installed_uuids = [e.uuid for e in manager.list_installed_extensions()]
        uuids += [
            uuid
            for uuid in manager.list_enabled_uuids()
            if uuid not in installed_uuids and uuid not in args.uuids
        ]
    print("Disable extension(s):")
    for uuid in uuids:
        print(" -", Color.YELLOW(uuid))
    manager.disable_uuids(*uuids)


================================================
FILE: gnome_extensions_cli/commands/enable.py
================================================
"""
gnome-extensions-cli
"""

from argparse import ONE_OR_MORE, ArgumentParser, Namespace

from ..icons import Color
from ..manager import ExtensionManager
from ..store import GnomeExtensionStore


def configure(parser: ArgumentParser):
    """
    Configure parser for subcommand
    """
    parser.set_defaults(handler=run)

    parser.add_argument(
        "uuids",
        nargs=ONE_OR_MORE,
        metavar="UUID",
        help="uuid of extensions to enable",
    )


def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensionStore):
    """
    Handler for subcommand
    """
    uuids = list(set(args.uuids))
    print("Enable extension(s):")
    for uuid in uuids:
        print(" -", Color.YELLOW(uuid))
    manager.enable_uuids(*uuids)


================================================
FILE: gnome_extensions_cli/commands/install.py
================================================
"""
gnome-extensions-cli
"""

from argparse import ONE_OR_MORE, ArgumentParser, Namespace

from tqdm import tqdm

from ..icons import Color, Icons, Label
from ..manager import ExtensionManager
from ..store import GnomeExtensionStore
from ..utils import version_comparator


def configure(parser: ArgumentParser):
    """
    Configure parser for subcommand
    """
    parser.set_defaults(handler=run)

    parser.add_argument(
        "extensions",
        nargs=ONE_OR_MORE,
        metavar="UUID_OR_PK",
        help="uuid (or pk) of extensions to install",
    )


def run(args: Namespace, manager: ExtensionManager, store: GnomeExtensionStore):
    """
    Handler for subcommand
    """
    installed_extensions = {e.uuid: e for e in manager.list_installed_extensions()}
    enabled_uuids = manager.list_enabled_uuids()
    shell_version = manager.get_current_shell_version()

    extensions_to_fetch = dict.fromkeys(args.extensions)
    fetched_extensions = {
        uuid: ext
        for uuid, ext in tqdm(
            store.iter_fetch(extensions_to_fetch, shell_version=shell_version),
            unit=" extension(s) fetched",
            total=len(extensions_to_fetch),
        )
    }

    for motif, available_ext in fetched_extensions.items():
        if available_ext is None:
            print(Icons.ERROR, "Cannot find extension", Color.RED(motif))
        elif available_ext.uuid not in installed_extensions:
            print(Icons.PACKAGE, "Install", Label.available(available_ext))
            manager.install_extension(available_ext)
        elif (
            version_comparator(
                installed_extensions[available_ext.uuid].metadata.version,
                available_ext.version,
            )
            > 0
        ):
            print(Icons.PACKAGE, "Upgrade", Label.available(available_ext))
            manager.install_extension(available_ext)
        elif available_ext.uuid not in enabled_uuids:
            print(Icons.HINT, "Enable", Label.available(available_ext))
            manager.enable_uuids(available_ext.uuid)
        else:
            print(
                Icons.DRYRUN,
                "Extension",
                Label.available(available_ext),
                "is already installed",
            )
            manager.enable_uuids(available_ext.uuid)


================================================
FILE: gnome_extensions_cli/commands/list_.py
================================================
"""
gnome-extensions-cli
"""

from argparse import ArgumentParser, Namespace

from ..icons import Color, Icons, Label
from ..manager import ExtensionManager
from ..store import GnomeExtensionStore


def configure(parser: ArgumentParser):
    """
    Configure parser for subcommand
    """
    parser.set_defaults(handler=run)

    parser.add_argument(
        "-v", "--verbose", action="store_true", help="display more information"
    )
    parser.add_argument(
        "--only-uuid", action="store_true", help="display only uuid of extensions"
    )
    parser.add_argument(
        "-a",
        "--all",
        action="store_true",
        help="list all extensions, (by default only enabled are shown)",
    )


def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensionStore):
    """
    Handler for subcommand
    """
    verbose = "verbose" in args and args.verbose
    show_all = "all" in args and args.all
    only_uuid = "only_uuid" in args and args.only_uuid

    if verbose:
        print("Gnome Shell", Label.version(manager.get_current_shell_version()))
        print()
        print(
            "Installed Extensions:",
            f"(enabled: {Icons.DOT_BLUE}, disabled: {Icons.DOT_WHITE})",
        )

    installed_extensions = sorted(
        manager.list_installed_extensions(), key=lambda x: x.uuid.lower()
    )
    enabled_uuids = manager.list_enabled_uuids()

    for installed_ext in installed_extensions:
        if installed_ext.uuid in enabled_uuids:
            if only_uuid:
                print(installed_ext.metadata.uuid)
            else:
                print(Icons.DOT_BLUE, Label.installed(installed_ext, enabled=True))

    if show_all:
        for installed_ext in installed_extensions:
            if installed_ext.uuid not in enabled_uuids:
                if only_uuid:
                    print(installed_ext.metadata.uuid)
                else:
                    print(
                        Icons.DOT_WHITE, Label.installed(installed_ext, enabled=False)
                    )

    if verbose:
        print()
        print(
            "Enabled uuids:",
            ", ".join(map(Color.YELLOW, sorted(enabled_uuids, key=str.lower))),
        )


================================================
FILE: gnome_extensions_cli/commands/preferences.py
================================================
"""
gnome-extensions-cli
"""

from argparse import ArgumentParser, Namespace

from ..manager import ExtensionManager
from ..store import GnomeExtensionStore


def configure(parser: ArgumentParser):
    """
    Configure parser for subcommand
    """
    parser.set_defaults(handler=run)

    parser.add_argument(
        "uuid",
        metavar="UUID",
        help="uuid of extension to edit preferences",
    )


def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensionStore):
    """
    Handler for subcommand
    """
    ext = next(
        filter(lambda e: e.uuid == args.uuid, manager.list_installed_extensions()), None
    )
    assert ext is not None, f"Extension {args.uuid} is not installed"
    manager.edit_extension(ext)


================================================
FILE: gnome_extensions_cli/commands/search.py
================================================
"""
gnome-extensions-cli
"""

from argparse import ONE_OR_MORE, ArgumentParser, Namespace

from ..icons import Color, Icons, Label
from ..manager import ExtensionManager
from ..store import GnomeExtensionStore
from .show import print_key_value


def configure(parser: ArgumentParser):
    """
    Configure parser for subcommand
    """
    parser.set_defaults(handler=run)
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="search for extensions"
    )
    parser.add_argument("-l", "--limit", type=int, default=0, help="limit to N items")
    parser.add_argument(
        "motif",
        nargs=ONE_OR_MORE,
        help="search motif",
    )


def run(args: Namespace, manager: ExtensionManager, store: GnomeExtensionStore):
    """
    Handler for subcommand
    """
    installed_extensions = {e.uuid: e for e in manager.list_installed_extensions()}

    results = list(store.search(" ".join(args.motif), limit=args.limit))
    for index, available_ext in enumerate(results, 1):
        installed_ext = installed_extensions.get(available_ext.uuid)
        print(
            Icons.DOT_BLUE if installed_ext is not None else Icons.DOT_WHITE,
            f"[{index}/{len(results)}]",
            Color.DEFAULT(available_ext.name, style="bright"),
            Label.uuid(available_ext.uuid),
        )
        print_key_value("link", Label.url(store.url, available_ext.link), 1)
        print_key_value("screenshot", Label.url(store.url, available_ext.screenshot), 1)
        print_key_value("creator", available_ext.creator, 1)
        print_key_value("recommended version", Label.version(available_ext.version), 1)
        if installed_ext is not None:
            print_key_value(
                "installed version", Label.version(installed_ext.metadata.version), 1
            )
        if args.verbose:
            print_key_value("description", available_ext.description, 1)

        print(("-" * 80) if args.verbose else "")


================================================
FILE: gnome_extensions_cli/commands/show.py
================================================
"""
gnome-extensions-cli
"""

from argparse import ONE_OR_MORE, ArgumentParser, Namespace
from typing import Any, Dict, Iterable, List, Optional, Union

from gnome_extensions_cli.schema import AvailableExtension

from ..icons import Color, Icons, Label
from ..manager import ExtensionManager
from ..store import GnomeExtensionStore

INDENT = "  "


def configure(parser: ArgumentParser):
    """
    Configure parser for subcommand
    """
    parser.set_defaults(handler=run)
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="display more information"
    )
    parser.add_argument(
        "extensions",
        nargs=ONE_OR_MORE,
        metavar="UUID_OR_PK",
        help="uuid (or pk) of extensions",
    )


def print_form(data: Dict[str, Any], indent: str = ""):
    """
    Print a form-like from given data
    """
    maxlen = max(map(len, data.keys()))
    for key, value in data.items():
        if value is not None:
            # right align
            key = key.rjust(maxlen)
            # key style: dim
            key = Color.DEFAULT(key, style="dim")

            if isinstance(value, str) and "\n" in value:
                # multiline string, indent each line
                value = f"\n{indent}{' ' * (maxlen + 2)}".join(value.splitlines())
            elif isinstance(value, (list, set, dict)):
                # if value is a list-like, indent all items
                value = f"\n{indent}{' ' * (maxlen + 2)}".join(map(str, value))

            print(f"{indent}{key}: {value}")


def print_key_value(key: str, value: Optional[Any], indent: int = 0):
    """
    Print a key-value pair, handling indentation, formatting and colors
    """
    if value is not None:
        if isinstance(value, str) and "\n" in value:
            prefix = INDENT * (indent + 2)
            value = f"\n{prefix}" + value.replace("\n", f"\n{prefix}")
        print(
            INDENT * indent,
            Color.DEFAULT(key, style="dim"),
            ":",
            value if value is not None else "",
        )


def build_versions_dict(ext: AvailableExtension) -> Dict[int, List[str]]:
    """
    Build a disctionnary of supported Gnome Shell version per app version
    """
    out = {}
    for shell_version, app in ext.shell_version_map.items():
        app_version = app.version
        if app.version not in out:
            out[app_version] = []
        out[app_version].append(shell_version)
    return out


def run(args: Namespace, manager: ExtensionManager, store: GnomeExtensionStore):
    """
    Handler for subcommand
    """
    installed_extensions = {e.uuid: e for e in manager.list_installed_extensions()}

    shell_version = manager.get_current_shell_version()

    for motif, available_ext in store.iter_fetch(
        dict.fromkeys(args.extensions), shell_version=shell_version
    ):
        if available_ext is not None:
            installed_ext = installed_extensions.get(available_ext.uuid)
            print(
                Icons.DOT_BLUE if installed_ext is not None else Icons.DOT_WHITE,
                Color.DEFAULT(available_ext.name, style="bright"),
                Label.uuid(available_ext.uuid),
            )

            print_form(
                {
                    "link": Label.url(store.url, available_ext.link),
                    "screenshot": Label.url(store.url, available_ext.screenshot),
                    "creator": available_ext.creator,
                    "creator_url": Label.url(store.url, available_ext.creator_url),
                    "description": available_ext.description if args.verbose else None,
                    "tag": available_ext.version_tag,
                    "pk": available_ext.pk,
                    "recommended version": Label.version(available_ext.version),
                    "installed version": Label.version(installed_ext.metadata.version)
                    if installed_ext is not None
                    else None,
                    "versions": list(available_ext.shell_version_map.keys()),
                    "available versions": None,
                },
                indent="    ",
            )
            continue
            print_key_value("link", Label.url(store.url, available_ext.link), 1)
            print_key_value(
                "screenshot", Label.url(store.url, available_ext.screenshot), 1
            )
            print_key_value("creator", available_ext.creator, 1)
            print_key_value(
                "creator_url", Label.url(store.url, available_ext.creator_url), 1
            )
            if args.verbose:
                print_key_value("description", available_ext.description, 1)
            print_key_value("tag", available_ext.version_tag, 1)
            print_key_value("pk", available_ext.pk, 1)
            print_key_value(
                "recommended version", Label.version(available_ext.version), 1
            )
            if installed_ext is not None:
                print_key_value(
                    "installed version",
                    Label.version(installed_ext.metadata.version),
                    1,
                )
            if args.verbose:
                app_versions_dict = build_versions_dict(available_ext)
                print_key_value("available versions", "", 1)
                for app_version in sorted(app_versions_dict, reverse=True):
                    shell_versions = app_versions_dict[app_version]
                    print(
                        INDENT * 2,
                        Label.version(app_version),
                        "for Gnome Shell",
                        Label.version(shell_versions[0]),
                        f" to {Label.version(shell_versions[-1])}"
                        if len(shell_versions) > 1
                        else "",
                    )
                    if not args.verbose:
                        break
            print()
        else:
            print(Icons.DOT_RED, f"Cannot find extension {motif}")


================================================
FILE: gnome_extensions_cli/commands/uninstall.py
================================================
"""
gnome-extensions-cli
"""

from argparse import ONE_OR_MORE, ArgumentParser, Namespace

from ..icons import Color, Icons, Label
from ..manager import ExtensionManager
from ..store import GnomeExtensionStore


def configure(parser: ArgumentParser):
    """
    Configure parser for subcommand
    """
    parser.set_defaults(handler=run)

    parser.add_argument(
        "uuids",
        nargs=ONE_OR_MORE,
        metavar="UUID",
        help="uuid of extensions to uninstall",
    )


def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensionStore):
    """
    Handler for subcommand
    """
    installed_extensions = {e.uuid: e for e in manager.list_installed_extensions()}
    for uuid in dict.fromkeys(args.uuids):
        installed_extension = installed_extensions.get(uuid)
        if installed_extension is None:
            print(
                Icons.WARNING,
                f"Extension {Color.RED(uuid)} is not installed",
            )
        elif installed_extension.read_only:
            print(
                Icons.HINT,
                "Cannot uninstall",
                installed_extension.metadata.name,
                Label.uuid(installed_extension.uuid),
                ": it is a system extension",
            )
        else:
            print(
                Icons.TRASH,
                "Uninstall",
                installed_extension.metadata.name,
                Label.uuid(installed_extension.uuid),
            )
            manager.uninstall_extension(installed_extension)


================================================
FILE: gnome_extensions_cli/commands/update.py
================================================
"""
gnome-extensions-cli
"""

from argparse import ZERO_OR_MORE, ArgumentParser, Namespace

from tqdm import tqdm

from ..icons import Color, Icons, Label
from ..manager import ExtensionManager
from ..store import GnomeExtensionStore
from ..utils import confirm, version_comparator


def configure(parser: ArgumentParser):
    """
    Configure parser for subcommand
    """
    parser.set_defaults(handler=run)
    parser.add_argument(
        "-i",
        "--install",
        action="store_true",
        help="install extension if not installed",
    )
    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        "-y",
        "--yes",
        action="store_true",
        help="do not prompt confirmation for update/install",
    )
    group.add_argument(
        "-n",
        "--dry-run",
        action="store_true",
        help="do not update nor install anything, "
        + "a return code 17 is returned if updates are available",
    )
    parser.add_argument(
        "--user",
        action="store_true",
        help="only update /user extensions, ignore /system ones",
    )
    parser.add_argument(
        "extensions",
        nargs=ZERO_OR_MORE,
        metavar="UUID_OR_PK",
        help="uuid (or pk) of extensions to update (default: all enabled extensions)",
    )


def run(args: Namespace, manager: ExtensionManager, store: GnomeExtensionStore):
    """
    Handler for subcommand
    """
    installed_extensions = {e.uuid: e for e in manager.list_installed_extensions()}
    enabled_uuids = manager.list_enabled_uuids()
    shell_version = manager.get_current_shell_version()

    extensions_to_fetch = []
    if len(args.extensions):
        # Use extensions list given by the user
        extensions_to_fetch = args.extensions
    else:
        # Update all installed extensions that are enable and that have a version
        for uuid, ext in installed_extensions.items():
            if uuid in enabled_uuids and ext.metadata.version is not None:
                extensions_to_fetch.append(uuid)

    # fetch available
    fetched_extensions = {
        uuid: ext
        for uuid, ext in tqdm(
            store.iter_fetch(extensions_to_fetch, shell_version=shell_version),
            unit=" extension(s) fetched",
            total=len(extensions_to_fetch),
        )
    }
    extensions_to_update = []
    extensions_to_install = []
    count = 0
    for uuid, available_ext in fetched_extensions.items():
        count += 1
        progress = f"[{count}]"
        if available_ext is None:
            # cannot fetch extension
            print(progress, "Cannot find extension", Color.RED(uuid))
        elif available_ext.uuid not in installed_extensions:
            # extension is not installed
            print(
                progress,
                "Found extension",
                Label.available(available_ext),
                ": not installed",
            )
            if args.install:
                extensions_to_install.append(available_ext)
        elif args.user and installed_extensions[available_ext.uuid].read_only:
            # extension is readonly, update only user extensions
            print(
                progress,
                "Found extension",
                Label.available(available_ext),
                ": skip system extension",
            )
        elif (
            version_comparator(
                installed_extensions[available_ext.uuid].metadata.version,
                available_ext.version,
            )
            > 0
        ):
            # extension can be updated
            print(
                progress,
                "Found extension",
                Label.available(available_ext),
                ": outdated",
            )
            extensions_to_update.append(available_ext)
        else:
            # extension is up to date
            print(
                progress,
                "Found extension",
                Label.available(available_ext),
                ": up-to-date",
            )
    print("")

    if len(extensions_to_update) + len(extensions_to_install) == 0:
        print(Icons.THUMB_UP, "Nothing to update")
    else:
        if len(extensions_to_update) > 0:
            print(Icons.PACKAGE, "Extensions to update:")
            for ext in extensions_to_update:
                print("  ", Color.YELLOW(ext.uuid))

        if len(extensions_to_install) > 0:
            print(Icons.PACKAGE, "Extensions to install:")
            for ext in extensions_to_install:
                print("  ", Color.YELLOW(ext.uuid))

        if args.dry_run:
            # in dryrun mode, exit 17
            raise SystemExit(17)

        if args.yes or confirm("Continue?", default=True):
            for available_extension in extensions_to_update:
                installed_extension = installed_extensions[available_extension.uuid]
                print("Update", Label.available(available_extension))
                if installed_extension.metadata.version is not None:
                    print("  over", Label.version(installed_extension.metadata.version))
                manager.install_extension(available_extension)

            for available_extension in extensions_to_install:
                print("Install", Label.available(available_extension))
                manager.install_extension(available_extension)


================================================
FILE: gnome_extensions_cli/dbus.py
================================================
"""
gnome-extensions-cli
"""

from operator import itemgetter
from pathlib import Path
from traceback import print_exc
from typing import List

from .manager import ExtensionManager
from .schema import AvailableExtension, InstalledExtension

DBUS_INTERFACE = "org.gnome.Shell"
DBUS_PATH = "/org/gnome/Shell"


def test_dbus_available(debug: bool = False) -> bool:
    """
    Test if DBus is available
    """
    try:
        from gi.repository import Gio  # pylint: disable=import-outside-toplevel

        dbus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
        return dbus is not None
    except BaseException:  # pylint: disable=broad-except
        if debug:
            print_exc()
    return False


class DbusExtensionManager(ExtensionManager):
    """
    Handle extensions using DBus messages, just like recommended Firefox extensions on Gnome website

    dbus schema: /usr/share/dbus-1/interfaces/org.gnome.Shell.Extensions.xml
    """

    def __init__(self):
        from gi.repository import Gio  # pylint: disable=import-outside-toplevel

        dbus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
        self.proxy_extensions = Gio.DBusProxy.new_sync(
            dbus,
            Gio.DBusProxyFlags.NONE,
            None,
            DBUS_INTERFACE,
            "/org/gnome/Shell",
            "org.gnome.Shell.Extensions",
            None,
        )
        self.proxy_properties = Gio.DBusProxy.new_sync(
            dbus,
            Gio.DBusProxyFlags.NONE,
            None,
            DBUS_INTERFACE,
            "/org/gnome/Shell",
            "org.freedesktop.DBus.Properties",
            None,
        )
        self.settings = Gio.Settings.new("org.gnome.shell")

    def get_current_shell_version(self) -> str:
        return self.proxy_properties.Get("(ss)", DBUS_INTERFACE, "ShellVersion")

    def list_installed_extensions(self) -> List[InstalledExtension]:
        return list(
            map(
                InstalledExtension,
                filter(
                    Path.exists,
                    map(
                        Path,
                        filter(
                            None,
                            map(
                                itemgetter("path"),
                                self.proxy_extensions.ListExtensions().values(),
                            ),
                        ),
                    ),
                ),
            )
        )

    def install_extension(self, ext: AvailableExtension) -> bool:
        return (
            self.proxy_extensions.InstallRemoteExtension("(s)", ext.uuid)
            == "successful"
        )

    def uninstall_extension(self, ext: InstalledExtension):
        self.proxy_extensions.UninstallExtension("(s)", ext.uuid)

    def edit_extension(self, ext: InstalledExtension):
        self.proxy_extensions.LaunchExtensionPrefs("(s)", ext.uuid)

    def list_enabled_uuids(self) -> List[str]:
        return list(self.settings["enabled-extensions"])

    def set_enabled_uuids(self, uuids: List[str]) -> bool:
        self.settings["enabled-extensions"] = uuids
        return True


================================================
FILE: gnome_extensions_cli/filesystem.py
================================================
"""
gnome-extensions-cli
"""

import subprocess
import sys
from dataclasses import dataclass, field
from os.path import expanduser
from pathlib import Path
from re import finditer, fullmatch
from shutil import rmtree
from subprocess import DEVNULL, run
from tempfile import NamedTemporaryFile
from typing import List
from urllib.request import urlopen
from zipfile import ZipFile

from .icons import Color, Icons, Label
from .manager import ExtensionManager
from .schema import AvailableExtension, InstalledExtension
from .store import GnomeExtensionStore


@dataclass
class FilesystemExtensionManager(ExtensionManager):
    """
    Handle extensions with basic filesystem operations
    """

    store: GnomeExtensionStore
    user_folder: Path = field(
        default=Path(expanduser("~/.local/share/gnome-shell/extensions"))
    )
    system_folders: List[Path] = field(
        default_factory=lambda: [
            Path("/usr/share/gnome-shell/extensions"),
            Path("/usr/local/share/gnome-shell/extensions"),
        ]
    )
    auto_compile_schemas: bool = True

    def get_current_shell_version(self) -> str:
        stdout = subprocess.check_output(
            ["gnome-shell", "--version"],
            text=True,
        )
        matcher = fullmatch(
            r"GNOME Shell (?P<version>[0-9]+(?:\.[0-9]+)?)(?:\..+)?", stdout.strip()
        )
        assert matcher is not None, "Cannot retrieve Gnome Shell version"
        return matcher.group("version")

    def list_installed_extensions(self) -> List[InstalledExtension]:
        out = {}
        for folder in filter(Path.is_dir, self.system_folders + [self.user_folder]):
            for subfolder in sorted(folder.iterdir()):
                metadata_file = subfolder / "metadata.json"
                if metadata_file.is_file():
                    ext = InstalledExtension(subfolder)
                    out[ext.uuid] = ext
        return list(out.values())

    def install_extension(self, ext: AvailableExtension) -> bool:
        assert ext.download_url is not None, (
            f"Cannot find recommended version for {ext.uuid}"
        )
        if self.disable_uuids(ext.uuid):
            print("Disable extension", ext.uuid)
        target_dir = self.user_folder / ext.uuid
        if target_dir.exists():
            print("Remove existing folder:", Label.folder(target_dir))
            rmtree(target_dir)
        target_dir.mkdir(parents=True)
        try:
            with NamedTemporaryFile() as tmp:
                print(
                    "Download extensions from",
                    Label.url(self.store.url, ext.download_url),
                )
                with urlopen(self.store.url + ext.download_url) as stream:
                    tmp.write(stream.read())
                tmp.seek(0)
                print("Extract extension to", Label.folder(target_dir))
                with ZipFile(tmp.name) as zipfile:
                    for member in zipfile.namelist():
                        zipfile.extract(member, path=target_dir)
            if self.auto_compile_schemas:
                self.compile_schemas(target_dir)
            if self.enable_uuids(ext.uuid):
                print(
                    "Enable extension", Label.installed(InstalledExtension(target_dir))
                )
        except BaseException as error:  # pylint: disable=broad-except
            print(
                Icons.BOOM,
                f"Error while installing {ext.uuid}:",
                Color.RED(error),
                file=sys.stderr,
            )
            print("Remove temporary folder:", Label.folder(target_dir))
            rmtree(target_dir)
            return False
        return True

    def uninstall_extension(self, ext: InstalledExtension):
        assert not ext.read_only, f"Cannot uninstall a system extension {ext.uuid}"
        if self.disable_uuids(ext.uuid):
            print("Disable", Label.installed(ext))
        rmtree(ext.folder)
        print("Remove folder", Label.folder(ext.folder))

    def edit_extension(self, ext: InstalledExtension):
        self._run(["gnome-extensions-app"])

    def list_enabled_uuids(self) -> List[str]:
        stdout = subprocess.check_output(
            ["gsettings", "get", "org.gnome.shell", "enabled-extensions"],
            text=True,
        )
        uuids = [m.group("uuid") for m in finditer(r"'(?P<uuid>[^']+)'", stdout)]
        return uuids

    def set_enabled_uuids(self, uuids: List[str]) -> bool:
        uuids_text = ",".join((f'"{uuid}"' for uuid in uuids))
        command = [
            "gsettings",
            "set",
            "org.gnome.shell",
            "enabled-extensions",
            f"[{uuids_text}]",
        ]
        if self._run(command) != 0:
            print(
                Icons.WARNING,
                f"Error while enable extensions with {Color.YELLOW('gesttings')}",
                file=sys.stderr,
            )
            return False
        return self.restart_gnome_shell()

    def restart_gnome_shell(self) -> bool:
        """
        Manually restart Gnome Shell
        """
        command = [
            "dbus-send",
            "--session",
            "--type=method_call",
            "--dest=org.gnome.Shell",
            "/org/gnome/Shell",
            "org.gnome.Shell.Eval",
            'string:"global.reexec_self();"',
        ]
        if self._run(command) != 0:
            print(
                Icons.WARNING,
                "Could not restart Gnome Shell, you have to restart it manually",
                file=sys.stderr,
            )
            return False
        return True

    def _run(self, command: List[str]) -> int:
        """
        Run an external process
        """
        process = subprocess.run(
            command,
            check=False,
            stderr=subprocess.DEVNULL,
            stdout=subprocess.DEVNULL,
        )
        return process.returncode

    def compile_schemas(self, extension_folder: Path) -> bool:
        if not (schemas_folder := extension_folder / "schemas").exists():
            # No schemas to compile
            return True
        if (schemas_folder / "gschemas.compiled").exists():
            # Schemas already compiled
            return True
        if len(list(schemas_folder.glob("*.gschema.xml"))) == 0:
            # Folder exists, no schemas to compile
            return True

        # Check if glib-compile-schemas is available
        try:
            run(
                ["glib-compile-schemas", "--version"],
                check=True,
                stdout=DEVNULL,
                stderr=DEVNULL,
            )
        except BaseException:
            print(
                Icons.WARNING,
                "Cannot compile schemas, you may need to manually compile schemas in",
                Label.folder(extension_folder),
            )
            return False

        process = run(
            ["glib-compile-schemas", "schemas/"],
            cwd=extension_folder,
            check=False,
            stdout=DEVNULL,
            stderr=DEVNULL,
        )
        if process.returncode != 0:
            print(
                Icons.ERROR,
                "Error while compiling schemas, you may need to manually compile schemas in",
                Label.folder(extension_folder),
            )
            return False

        print("Schemas compiled in", Label.folder(extension_folder))
        return True


================================================
FILE: gnome_extensions_cli/icons.py
================================================
"""
gnome-extensions-cli
"""

from enum import Enum
from pathlib import Path
from typing import Any, Optional

from colorama import Back, Fore, Style

from .schema import AvailableExtension, InstalledExtension


class Icons(Enum):
    """
    UTF8 Icons
    """

    OK = "✅"
    ERROR = "❌"
    WARNING = "🚨"
    RED_FLAG = "🚩"
    BOOM = "💥"
    QUESTION = "❓"
    DRYRUN = "🙈"
    HINT = "💡"
    DOT_BLACK = "⚫"
    DOT_WHITE = "⚪"
    DOT_RED = "🔴"
    DOT_BLUE = "🔵"
    PACKAGE = "📦"
    THUMB_UP = "👍"
    TRASH = "🗑"

    def __str__(self):
        return self.value


def color(
    *message: str,
    fore: Optional[str] = None,
    back: Optional[str] = None,
    style: Optional[str] = None,
) -> str:
    """
    string formatter with color and style
    """
    pre = ""
    post = ""
    if isinstance(fore, str):
        pre += fore
        post += Fore.RESET
    if isinstance(back, str):
        pre += back
        post += Back.RESET
    if isinstance(style, str):
        pre += style
        post += Style.RESET_ALL
    return pre + " ".join(map(str, filter(None, message))) + post


class Color(Enum):
    """
    Utility tool to use colorama colors
    """

    DEFAULT = None
    BLACK = Fore.BLACK
    RED = Fore.RED
    GREEN = Fore.GREEN
    YELLOW = Fore.YELLOW
    BLUE = Fore.BLUE
    MAGENTA = Fore.MAGENTA
    CYAN = Fore.CYAN
    WHITE = Fore.WHITE
    LIGHTBLACK_EX = Fore.LIGHTBLACK_EX
    LIGHTRED_EX = Fore.LIGHTRED_EX
    LIGHTGREEN_EX = Fore.LIGHTGREEN_EX
    LIGHTYELLOW_EX = Fore.LIGHTYELLOW_EX
    LIGHTBLUE_EX = Fore.LIGHTBLUE_EX
    LIGHTMAGENTA_EX = Fore.LIGHTMAGENTA_EX
    LIGHTCYAN_EX = Fore.LIGHTCYAN_EX
    LIGHTWHITE_EX = Fore.LIGHTWHITE_EX

    def __call__(self, *args, style: Optional[str] = None) -> str:
        style = (
            {"dim": Style.DIM, "bright": Style.BRIGHT}.get(style.lower()) or style
            if style is not None
            else style
        )
        return color(*args, fore=self.value, style=style)


class Label:
    """
    __str__ builder for common types
    """

    @staticmethod
    def uuid(uuid: str) -> str:
        return f"({Color.YELLOW(uuid)})"

    @staticmethod
    def version(version: Optional[Any]) -> Optional[str]:
        return f"v{version}" if version is not None else None

    @staticmethod
    def url(base: str, path: Optional[str]) -> Optional[str]:
        return Color.BLUE(base + path) if path is not None else None

    @staticmethod
    def available(ext: AvailableExtension) -> str:
        return " ".join(
            filter(
                None,
                [
                    Color.DEFAULT(ext.name, style="bright"),
                    Label.uuid(ext.uuid),
                    Label.version(ext.version),
                ],
            )
        )

    @staticmethod
    def installed(ext: InstalledExtension, enabled: Optional[bool] = None) -> str:
        name = ext.metadata.name
        if enabled is True:
            name = Color.DEFAULT(name, style="bright")
        elif enabled is False:
            name = Color.DEFAULT(name, style="dim")
        return (
            f"{name} {Label.uuid(ext.metadata.uuid)} {Label.version(ext.metadata.version) or ''} "
            + (Color.RED("/system") if ext.read_only else Color.GREEN("/user"))
        )

    @staticmethod
    def folder(path: Path) -> str:
        return Color.BLUE(f"{path}/", style="bright")


================================================
FILE: gnome_extensions_cli/manager.py
================================================
"""
gnome-extensions-cli
"""

from abc import ABC, abstractmethod
from typing import List

from .schema import AvailableExtension, InstalledExtension


class ExtensionManager(ABC):
    """
    Abstract class to manipulate extensions

    """

    @abstractmethod
    def get_current_shell_version(self) -> str:
        """
        Return the current Gnome Shell version
        """

    @abstractmethod
    def list_installed_extensions(self) -> List[InstalledExtension]:
        """
        List installed extensions
        """

    @abstractmethod
    def install_extension(self, ext: AvailableExtension) -> bool:
        """
        Install given extension
        """

    @abstractmethod
    def uninstall_extension(self, ext: InstalledExtension):
        """
        Uninstall given extension
        """

    @abstractmethod
    def edit_extension(self, ext: InstalledExtension):
        """
        Edit preferences of given extension
        """

    @abstractmethod
    def list_enabled_uuids(self) -> List[str]:
        """
        List enabled extensions uuids
        """

    @abstractmethod
    def set_enabled_uuids(self, uuids: List[str]) -> bool:
        """
        Set enabled extensions uuids
        """

    def enable_uuids(self, *uuids: str) -> bool:
        """
        Enable given extensions
        """
        old_uuids = set(self.list_enabled_uuids())
        new_uuids = old_uuids | set(uuids)
        return old_uuids != new_uuids and self.set_enabled_uuids(list(new_uuids))

    def disable_uuids(self, *uuids: str) -> bool:
        """
        Disable given extensions
        """
        old_uuids = set(self.list_enabled_uuids())
        new_uuids = old_uuids - set(uuids)
        return old_uuids != new_uuids and self.set_enabled_uuids(list(new_uuids))


================================================
FILE: gnome_extensions_cli/schema.py
================================================
"""
gnome-extensions-cli
"""

import os
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import Dict, List, Optional, Union

from pydantic import BaseModel, Field


class Metadata(BaseModel):
    uuid: str
    name: str
    description: Optional[str] = None
    extension_id: Optional[str] = Field(alias="extension-id", default=None)
    shell_version: Optional[List[str]] = Field(alias="shell-version", default=None)
    url: Optional[str] = None
    version: Optional[Union[str, int, float]] = None
    path: Optional[Path] = None


class _Version(BaseModel):
    pk: int
    version: int


class AvailableExtension(BaseModel):
    uuid: str
    pk: int
    name: str
    description: str
    creator: str
    creator_url: Optional[str] = None
    link: Optional[str] = None
    icon: Optional[str] = None
    screenshot: Optional[str] = None
    shell_version_map: Dict[str, _Version]
    version: Optional[int] = None
    version_tag: Optional[int] = None
    download_url: Optional[str] = None


class Search(BaseModel):
    extensions: List[AvailableExtension]
    total: int
    numpages: int


@dataclass
class InstalledExtension:
    folder: Path

    @property
    def metadata_json(self) -> Path:
        return self.folder / "metadata.json"

    @property
    def read_only(self):
        return not os.access(str(self.folder), os.W_OK)

    @cached_property
    def metadata(self) -> Metadata:
        return Metadata.model_validate_json(self.metadata_json.read_text())

    @property
    def uuid(self) -> str:
        return self.metadata.uuid


================================================
FILE: gnome_extensions_cli/store.py
================================================
"""
gnome-extensions-cli
"""

from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from typing import Iterable, Optional, Tuple, Union

from requests import Session
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

from .schema import AvailableExtension, Search


@dataclass
class GnomeExtensionStore:
    """
    Interface to search for extensions on Gnome Website
    """

    url: str = "https://extensions.gnome.org"
    timeout: int = 20
    session: Session = field(init=False)

    def __post_init__(self):
        self.session = Session()
        self.session.mount(
            "https://",
            HTTPAdapter(
                max_retries=Retry(
                    total=2, backoff_factor=1, status_forcelist=[500, 502, 503, 504]
                )
            ),
        )

    def iter_fetch(
        self,
        extensions: Iterable[Union[str, int]],
        shell_version: Optional[str] = None,
        max_workers: Optional[int] = None,
    ) -> Iterable[Tuple[Union[str, int], Optional[AvailableExtension]]]:
        """
        Fetch multiple available extensions in parallel and yield when fetched
        """
        with ThreadPoolExecutor(max_workers=max_workers) as executor:
            jobs = {
                executor.submit(lambda u: self.find(u, shell_version), ext): ext
                for ext in extensions
            }
            for job in as_completed(jobs.keys()):
                uuid = jobs[job]
                yield (uuid, job.result())

    def find(
        self, ext: Union[str, int], shell_version: Optional[str] = None
    ) -> Optional[AvailableExtension]:
        """
        Find an extension by its uuid or pk
        """
        if isinstance(ext, int):
            return self.find_by_pk(ext, shell_version=shell_version)
        if isinstance(ext, str) and ext.isnumeric():
            return self.find_by_pk(int(ext), shell_version=shell_version)
        return self.find_by_uuid(ext, shell_version=shell_version)

    def find_by_uuid(
        self, uuid: str, shell_version: Optional[str] = None
    ) -> Optional[AvailableExtension]:
        """
        Find an extension by its uuid
        """
        params = {"uuid": uuid}
        if shell_version is not None:
            params["shell_version"] = str(shell_version)
        resp = self.session.get(
            f"{self.url}/extension-info/",
            params=params,
            timeout=self.timeout,
        )
        if resp.status_code == 404:
            return None
        resp.raise_for_status()

        return AvailableExtension.model_validate_json(resp.text)

    def find_by_pk(
        self, pk: int, shell_version: Optional[str] = None
    ) -> Optional[AvailableExtension]:
        """
        Find an extension by its pk
        """
        params = {"pk": str(pk)}
        if shell_version is not None:
            params["shell_version"] = str(shell_version)
        resp = self.session.get(
            f"{self.url}/extension-info/",
            params=params,
            timeout=self.timeout,
        )
        if resp.status_code == 404:
            return None
        resp.raise_for_status()
        return AvailableExtension.model_validate_json(resp.text)

    def search(
        self, motif: str, shell_version: str = "all", limit: int = 0
    ) -> Iterable[AvailableExtension]:
        """
        Search for extensions
        """
        params = {"search": motif, "shell_version": shell_version, "page": 1}
        found = 0
        while True:
            resp = self.session.get(
                f"{self.url}/extension-query/",
                params=params,
                timeout=self.timeout,
            )
            resp.raise_for_status()
            data = Search.model_validate_json(resp.text)
            for ext in data.extensions:
                yield ext
                found += 1
                if 0 < limit <= found:
                    return
            if params["page"] >= data.numpages:
                break
            params["page"] += 1


================================================
FILE: gnome_extensions_cli/utils.py
================================================
"""
gnome-extensions-cli
"""

from typing import Any, Optional

from packaging.version import Version


def version_comparator(left: Any, right: Any) -> int:
    """
    Compare two versions by handling None, integer, float or strings
    """
    if left == right:
        return 0
    if left is None:
        return 1
    if right is None:
        return -1
    vleft, vright = Version(str(left)), Version(str(right))
    if vleft < vright:
        return 1
    if vleft > vright:
        return -1
    return 0


def confirm(message: str, default: Optional[bool] = None) -> bool:
    """
    Simple interactive confirmation
    """
    while True:
        answer = input(
            f"💬  {message} ["
            + ("Y" if default is True else "y")
            + "/"
            + ("N" if default is False else "n")
            + "] "
        )
        if answer == "" and default is not None:
            return default
        if answer.lower() == "y":
            return True
        if answer.lower() == "n":
            return False


================================================
FILE: poetry.toml
================================================
[virtualenvs]
in-project = true

[virtualenvs.options]
system-site-packages = true

================================================
FILE: pyproject.toml
================================================
[tool.poetry]
name = "gnome-extensions-cli"
version = "0.11.0"
description = "Command line tool to manage your Gnome Shell extensions"
homepage = "https://github.com/essembeh/gnome-extensions-cli"
authors = ["Sébastien MB <seb@essembeh.org>"]
license = "Apache-2.0"
classifiers = [
    "Development Status :: 4 - Beta",
    "License :: OSI Approved :: Apache Software License",
    "Topic :: Utilities"
]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.8"
colorama = "^0.4.5"
pydantic = "^2.3.0"
requests = "^2.28.1"
packaging = "^25.0"
tqdm = "^4.66.1"

[tool.poetry.group.dev.dependencies]
black = "*"
pylint = "*"
pytest = "*"
pytest-dotenv = "*"
pytest-cov = "*"

[tool.poetry.scripts]
gnome-extensions-cli = "gnome_extensions_cli.cli:run"
gext = "gnome_extensions_cli.cli:run"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"


================================================
FILE: tests/__init__.py
================================================



================================================
FILE: tests/samples/available-alt.json
================================================
{
    "uuid": "todo.txt@bart.libert.gmail.com",
    "name": "Todo.txt",
    "creator": "BartL",
    "creator_url": "/accounts/profile/BartL",
    "pk": 570,
    "description": "A Gnome shell interface for todo.txt. \n\nTodo.txt is a future-proof syntax for tasks (not made by me), for more info: http://todotxt.com/\n\nSome examples:\nTask: Basic task\n(A) Task: High priority task\nTask @project +context: Task is part of project and has a certain context\nx 2013-08-22 Task: Task was completed on the 22nd of August\n\nFor more info about the syntax: https://github.com/ginatrapani/todo.txt-cli/wiki/The-Todo.txt-Format\n\nQuick start:\nWhen you first enable the extension, chances are high you'll see a [X] in your top panel. If you click the [X], you will be able to choose between creating the necessary files automatically or selecting your own existing files to be used with the extension.\n\nPlease use the issue tracker on the homepage to report bugs and/or file feature requests, this makes tracking easier for me. Thanks!\n\nSee the included CHANGELOG.md for info about changes between different versions, or see it online: https://gitlab.com/todo.txt-gnome-shell-extension/todo-txt-gnome-shell-extension/-/blob/master/CHANGELOG.md",
    "link": "/extension/570/todotxt/",
    "icon": "/static/images/plugin.png",
    "screenshot": "/extension-data/screenshots/screenshot_570_5X5YkZb.png",
    "shell_version_map": {
        "3.4": {
            "pk": 6238,
            "version": 21
        },
        "3.6": {
            "pk": 6238,
            "version": 21
        },
        "3.8": {
            "pk": 6238,
            "version": 21
        },
        "3.10": {
            "pk": 8141,
            "version": 25
        },
        "3.12": {
            "pk": 8141,
            "version": 25
        },
        "3.14": {
            "pk": 8141,
            "version": 25
        },
        "3.16": {
            "pk": 8141,
            "version": 25
        },
        "3.18": {
            "pk": 8141,
            "version": 25
        },
        "3.20": {
            "pk": 8141,
            "version": 25
        },
        "3.22": {
            "pk": 8141,
            "version": 25
        },
        "3.24": {
            "pk": 8141,
            "version": 25
        },
        "3.26": {
            "pk": 8141,
            "version": 25
        },
        "3.28": {
            "pk": 8141,
            "version": 25
        },
        "3.32": {
            "pk": 11240,
            "version": 28
        },
        "3.34": {
            "pk": 13591,
            "version": 29
        },
        "3.36": {
            "pk": 19473,
            "version": 33
        },
        "3.38": {
            "pk": 19473,
            "version": 33
        },
        "40.0": {
            "pk": 22868,
            "version": 34
        },
        "40": {
            "pk": 26250,
            "version": 35
        },
        "41": {
            "pk": 30207,
            "version": 37
        },
        "42": {
            "pk": 30207,
            "version": 37
        },
        "43": {
            "pk": 36826,
            "version": 39
        }
    }
}

================================================
FILE: tests/samples/available.json
================================================
{
    "uuid": "todo.txt@bart.libert.gmail.com",
    "name": "Todo.txt",
    "creator": "BartL",
    "creator_url": "/accounts/profile/BartL",
    "pk": 570,
    "description": "A Gnome shell interface for todo.txt. \n\nTodo.txt is a future-proof syntax for tasks (not made by me), for more info: http://todotxt.com/\n\nSome examples:\nTask: Basic task\n(A) Task: High priority task\nTask @project +context: Task is part of project and has a certain context\nx 2013-08-22 Task: Task was completed on the 22nd of August\n\nFor more info about the syntax: https://github.com/ginatrapani/todo.txt-cli/wiki/The-Todo.txt-Format\n\nQuick start:\nWhen you first enable the extension, chances are high you'll see a [X] in your top panel. If you click the [X], you will be able to choose between creating the necessary files automatically or selecting your own existing files to be used with the extension.\n\nPlease use the issue tracker on the homepage to report bugs and/or file feature requests, this makes tracking easier for me. Thanks!\n\nSee the included CHANGELOG.md for info about changes between different versions, or see it online: https://gitlab.com/todo.txt-gnome-shell-extension/todo-txt-gnome-shell-extension/-/blob/master/CHANGELOG.md",
    "link": "/extension/570/todotxt/",
    "icon": "/static/images/plugin.png",
    "screenshot": "/extension-data/screenshots/screenshot_570_5X5YkZb.png",
    "shell_version_map": {
        "3.4": {
            "pk": 6238,
            "version": 21
        },
        "3.6": {
            "pk": 6238,
            "version": 21
        },
        "3.8": {
            "pk": 6238,
            "version": 21
        },
        "3.10": {
            "pk": 8141,
            "version": 25
        },
        "3.12": {
            "pk": 8141,
            "version": 25
        },
        "3.14": {
            "pk": 8141,
            "version": 25
        },
        "3.16": {
            "pk": 8141,
            "version": 25
        },
        "3.18": {
            "pk": 8141,
            "version": 25
        },
        "3.20": {
            "pk": 8141,
            "version": 25
        },
        "3.22": {
            "pk": 8141,
            "version": 25
        },
        "3.24": {
            "pk": 8141,
            "version": 25
        },
        "3.26": {
            "pk": 8141,
            "version": 25
        },
        "3.28": {
            "pk": 8141,
            "version": 25
        },
        "3.32": {
            "pk": 11240,
            "version": 28
        },
        "3.34": {
            "pk": 13591,
            "version": 29
        },
        "3.36": {
            "pk": 19473,
            "version": 33
        },
        "3.38": {
            "pk": 19473,
            "version": 33
        },
        "40.0": {
            "pk": 22868,
            "version": 34
        },
        "40": {
            "pk": 26250,
            "version": 35
        },
        "41": {
            "pk": 30207,
            "version": 37
        },
        "42": {
            "pk": 30207,
            "version": 37
        },
        "43": {
            "pk": 36826,
            "version": 39
        }
    },
    "version": 35,
    "version_tag": 26250,
    "download_url": "/download-extension/todo.txt@bart.libert.gmail.com.shell-extension.zip?version_tag=26250"
}

================================================
FILE: tests/samples/installed.json
================================================
{
  "_generated": "Generated by SweetTooth, do not edit",
  "description": "A Gnome shell interface for todo.txt. \n\nTodo.txt is a future-proof syntax for tasks (not made by me), for more info: http://todotxt.com/\n\nSome examples:\nTask: Basic task\n(A) Task: High priority task\nTask @project +context: Task is part of project and has a certain context\nx 2013-08-22 Task: Task was completed on the 22nd of August\n\nFor more info about the syntax: https://github.com/ginatrapani/todo.txt-cli/wiki/The-Todo.txt-Format\n\nQuick start:\nWhen you first enable the extension, chances are high you'll see a [X] in your top panel. If you click the [X], you will be able to choose between creating the necessary files automatically or selecting your own existing files to be used with the extension.\n\nPlease use the issue tracker on the homepage to report bugs and/or file feature requests, this makes tracking easier for me. Thanks!\n\nSee the included CHANGELOG.md for info about changes between different versions, or see it online: https://gitlab.com/todo.txt-gnome-shell-extension/todo-txt-gnome-shell-extension/-/blob/master/CHANGELOG.md",
  "name": "Todo.txt",
  "shell-version": [
    "3.36",
    "3.38"
  ],
  "url": "https://gitlab.com/todo.txt-gnome-shell-extension/todo-txt-gnome-shell-extension",
  "uuid": "todo.txt@bart.libert.gmail.com",
  "version": 33
}

================================================
FILE: tests/samples/search.json
================================================
{
    "extensions": [
        {
            "uuid": "todolist@bsaleil.org",
            "name": "Todo list",
            "creator": "bsaleil",
            "creator_url": "/accounts/profile/bsaleil",
            "pk": 162,
            "description": "Simple todo list extension. You can add and remove tasks on your list.",
            "link": "/extension/162/todo-list/",
            "icon": "/extension-data/icons/icon_162_1.png",
            "screenshot": "/extension-data/screenshots/screenshot_162_1.png",
            "shell_version_map": {
                "3.2.1": {
                    "pk": 622,
                    "version": 4
                },
                "3.2": {
                    "pk": 726,
                    "version": 7
                },
                "3.3.4": {
                    "pk": 726,
                    "version": 7
                },
                "3.3.5": {
                    "pk": 726,
                    "version": 7
                },
                "3.4": {
                    "pk": 1319,
                    "version": 9
                },
                "3.6": {
                    "pk": 2390,
                    "version": 11
                },
                "3.8": {
                    "pk": 3233,
                    "version": 12
                },
                "3.10": {
                    "pk": 3569,
                    "version": 13
                },
                "3.14": {
                    "pk": 4703,
                    "version": 14
                },
                "3.32": {
                    "pk": 9898,
                    "version": 15
                }
            }
        },
        {
            "uuid": "todo_mail_UHA@vdebian",
            "name": "todo mail UHA",
            "creator": "projet_UHA",
            "creator_url": "/accounts/profile/projet_UHA",
            "pk": 778,
            "description": "You can quickly and easily send a mail via the taskbar GNOME.",
            "link": "/extension/778/todo-mail-uha/",
            "icon": "/static/images/plugin.png",
            "screenshot": "/extension-data/screenshots/screenshot_778_1.png",
            "shell_version_map": {
                "3.8": {
                    "pk": 3641,
                    "version": 1
                }
            }
        },
        {
            "uuid": "todolist@tomMoral.org",
            "name": "Section Todo List",
            "creator": "tomMoral",
            "creator_url": "/accounts/profile/tomMoral",
            "pk": 1104,
            "description": "Manage todo list with an applet\n\n* Add and remove task on your list in different sections.\n* Click an item to rename it.\n* Access the extension using Hot-Key (default: Ctrl+Space)\n\n**Note:** I don't come on this page often so if you need any help, please refer to the github repo and open an issue :) ",
            "link": "/extension/1104/section-todo-list/",
            "icon": "/static/images/plugin.png",
            "screenshot": "/extension-data/screenshots/screenshot_1104_0EguKVW.png",
            "shell_version_map": {
                "3.18": {
                    "pk": 8308,
                    "version": 6
                },
                "3.20": {
                    "pk": 8308,
                    "version": 6
                },
                "3.22": {
                    "pk": 8427,
                    "version": 8
                },
                "3.24": {
                    "pk": 10921,
                    "version": 9
                },
                "3.26": {
                    "pk": 10921,
                    "version": 9
                },
                "3.28": {
                    "pk": 10921,
                    "version": 9
                },
                "3.30": {
                    "pk": 14156,
                    "version": 10
                },
                "3.32": {
                    "pk": 36189,
                    "version": 13
                },
                "3.34": {
                    "pk": 36189,
                    "version": 13
                },
                "3.36": {
                    "pk": 36189,
                    "version": 13
                },
                "3.38": {
                    "pk": 36189,
                    "version": 13
                },
                "40": {
                    "pk": 36189,
                    "version": 13
                },
                "42": {
                    "pk": 36189,
                    "version": 13
                },
                "43": {
                    "pk": 36189,
                    "version": 13
                }
            }
        },
        {
            "uuid": "todo.txt@bart.libert.gmail.com",
            "name": "Todo.txt",
            "creator": "BartL",
            "creator_url": "/accounts/profile/BartL",
            "pk": 570,
            "description": "A Gnome shell interface for todo.txt. \n\nTodo.txt is a future-proof syntax for tasks (not made by me), for more info: http://todotxt.com/\n\nSome examples:\nTask: Basic task\n(A) Task: High priority task\nTask @project +context: Task is part of project and has a certain context\nx 2013-08-22 Task: Task was completed on the 22nd of August\n\nFor more info about the syntax: https://github.com/ginatrapani/todo.txt-cli/wiki/The-Todo.txt-Format\n\nQuick start:\nWhen you first enable the extension, chances are high you'll see a [X] in your top panel. If you click the [X], you will be able to choose between creating the necessary files automatically or selecting your own existing files to be used with the extension.\n\nPlease use the issue tracker on the homepage to report bugs and/or file feature requests, this makes tracking easier for me. Thanks!\n\nSee the included CHANGELOG.md for info about changes between different versions, or see it online: https://gitlab.com/todo.txt-gnome-shell-extension/todo-txt-gnome-shell-extension/-/blob/master/CHANGELOG.md",
            "link": "/extension/570/todotxt/",
            "icon": "/static/images/plugin.png",
            "screenshot": "/extension-data/screenshots/screenshot_570_5X5YkZb.png",
            "shell_version_map": {
                "3.4": {
                    "pk": 6238,
                    "version": 21
                },
                "3.6": {
                    "pk": 6238,
                    "version": 21
                },
                "3.8": {
                    "pk": 6238,
                    "version": 21
                },
                "3.10": {
                    "pk": 8141,
                    "version": 25
                },
                "3.12": {
                    "pk": 8141,
                    "version": 25
                },
                "3.14": {
                    "pk": 8141,
                    "version": 25
                },
                "3.16": {
                    "pk": 8141,
                    "version": 25
                },
                "3.18": {
                    "pk": 8141,
                    "version": 25
                },
                "3.20": {
                    "pk": 8141,
                    "version": 25
                },
                "3.22": {
                    "pk": 8141,
                    "version": 25
                },
                "3.24": {
                    "pk": 8141,
                    "version": 25
                },
                "3.26": {
                    "pk": 8141,
                    "version": 25
                },
                "3.28": {
                    "pk": 8141,
                    "version": 25
                },
                "3.32": {
                    "pk": 11240,
                    "version": 28
                },
                "3.34": {
                    "pk": 13591,
                    "version": 29
                },
                "3.36": {
                    "pk": 19473,
                    "version": 33
                },
                "3.38": {
                    "pk": 19473,
                    "version": 33
                },
                "40.0": {
                    "pk": 22868,
                    "version": 34
                },
                "40": {
                    "pk": 26250,
                    "version": 35
                },
                "41": {
                    "pk": 30207,
                    "version": 37
                },
                "42": {
                    "pk": 30207,
                    "version": 37
                },
                "43": {
                    "pk": 36826,
                    "version": 39
                }
            }
        },
        {
            "uuid": "timepp@zagortenay333",
            "name": "Time ++",
            "creator": "zagortenay33",
            "creator_url": "/accounts/profile/zagortenay33",
            "pk": 1238,
            "description": "A todo.txt manager, time tracker, timer, stopwatch, pomodoro, and alarm clock",
            "link": "/extension/1238/time/",
            "icon": "/extension-data/icons/icon_1238_etkhURE.png",
            "screenshot": "/extension-data/screenshots/screenshot_1238_VF4lJwP.png",
            "shell_version_map": {
                "3.24": {
                    "pk": 8671,
                    "version": 145
                },
                "3.26": {
                    "pk": 8671,
                    "version": 145
                },
                "3.28": {
                    "pk": 8671,
                    "version": 145
                },
                "3.30": {
                    "pk": 8671,
                    "version": 145
                },
                "3.32": {
                    "pk": 9874,
                    "version": 149
                },
                "3.34": {
                    "pk": 14591,
                    "version": 152
                },
                "3.36": {
                    "pk": 20574,
                    "version": 155
                },
                "3.38": {
                    "pk": 20574,
                    "version": 155
                },
                "40": {
                    "pk": 28972,
                    "version": 163
                },
                "41": {
                    "pk": 28972,
                    "version": 163
                },
                "42": {
                    "pk": 33529,
                    "version": 165
                }
            }
        },
        {
            "uuid": "rayday@jaypy.ir",
            "name": "Rayday",
            "creator": "jaypy",
            "creator_url": "/accounts/profile/jaypy",
            "pk": 4150,
            "description": "Rayday is a small CRM to manage your todos , deadlines and etc...",
            "link": "/extension/4150/rayday/",
            "icon": "/static/images/plugin.png",
            "screenshot": "/extension-data/screenshots/screenshot_4150_1RAoD0D.png",
            "shell_version_map": {
                "3.36": {
                    "pk": 23482,
                    "version": 1
                }
            }
        },
        {
            "uuid": "gnome-shell-Google-search-provider@MrNinso",
            "name": "Google Search Provider",
            "creator": "ninso",
            "creator_url": "/accounts/profile/ninso",
            "pk": 4132,
            "description": "Add Google search to Gnome Shell Search \n TODO: \n - Add google suggestions (For now using duckduckGo suggestions)",
            "link": "/extension/4132/google-search-provider/",
            "icon": "/static/images/plugin.png",
            "screenshot": "/extension-data/screenshots/screenshot_4132.png",
            "shell_version_map": {
                "3.36": {
                    "pk": 28657,
                    "version": 6
                },
                "3.38": {
                    "pk": 28657,
                    "version": 6
                },
                "40.0": {
                    "pk": 23583,
                    "version": 4
                },
                "40": {
                    "pk": 28657,
                    "version": 6
                },
                "41": {
                    "pk": 28657,
                    "version": 6
                }
            }
        },
        {
            "uuid": "battery-power-statistics-shortcut@l300lvl.co.nr",
            "name": "Battery Power Statistics Shortcut",
            "creator": "l300lvl",
            "creator_url": "/accounts/profile/l300lvl",
            "pk": 175,
            "description": "Adds a shortcut to open Power Statistics from the battery indicator. Incredibly useful for laptop users. Now compatible with the system-monitor extension http://tinyurl.com/sysmon Depends on: gnome-power-statistics todo:rework placement",
            "link": "/extension/175/battery-power-statistics-shortcut/",
            "icon": "/extension-data/icons/icon_175.png",
            "screenshot": "/extension-data/screenshots/screenshot_175_1.png",
            "shell_version_map": {
                "3.2": {
                    "pk": 4746,
                    "version": 9
                },
                "3.2.0": {
                    "pk": 4746,
                    "version": 9
                },
                "3.2.1": {
                    "pk": 4746,
                    "version": 9
                },
                "3.4": {
                    "pk": 4746,
                    "version": 9
                },
                "3.6": {
                    "pk": 4746,
                    "version": 9
                },
                "3.5.4": {
                    "pk": 4746,
                    "version": 9
                },
                "3.8": {
                    "pk": 4746,
                    "version": 9
                },
                "3.10": {
                    "pk": 4746,
                    "version": 9
                },
                "3.12": {
                    "pk": 4746,
                    "version": 9
                },
                "3.14": {
                    "pk": 4746,
                    "version": 9
                },
                "3.16": {
                    "pk": 4746,
                    "version": 9
                },
                "3.15.91": {
                    "pk": 4746,
                    "version": 9
                },
                "3.15.92": {
                    "pk": 4746,
                    "version": 9
                }
            }
        },
        {
            "uuid": "overview-on-startup@atz3.yahoo.com",
            "name": "Overview On Startup",
            "creator": "l300lvl",
            "creator_url": "/accounts/profile/l300lvl",
            "pk": 658,
            "description": "A simple extension with a primary task of launching the Overview at each start of Gnome Shell, e.g on login. Influenced by @gevera, the Applications/Frequent view is default, but has an option to show the Windows view. \n\ntodo: timeout option, eventually show the Overview when no windows are open or none are in focus, regardless of workspace/s and settings",
            "link": "/extension/658/overview-on-startup/",
            "icon": "/extension-data/icons/icon_658.png",
            "screenshot": "/extension-data/screenshots/screenshot_658.png",
            "shell_version_map": {
                "3.2": {
                    "pk": 2793,
                    "version": 1
                },
                "3.2.0": {
                    "pk": 2793,
                    "version": 1
                },
                "3.2.1": {
                    "pk": 2793,
                    "version": 1
                },
                "3.4": {
                    "pk": 2873,
                    "version": 6
                },
                "3.6": {
                    "pk": 2873,
                    "version": 6
                },
                "3.5.4": {
                    "pk": 2873,
                    "version": 6
                },
                "3.8": {
                    "pk": 2873,
                    "version": 6
                }
            }
        },
        {
            "uuid": "Analog_Clock@l300lvl.co.nr",
            "name": "Analog Neon Clock",
            "creator": "l300lvl",
            "creator_url": "/accounts/profile/l300lvl",
            "pk": 163,
            "description": "Replace digital clock with Analog Neon Clock for &gt;3.4. Replaces the normal time/date but not the calendar. Placement preference now available(except 3.8). Colors can be changed via css, for now I added color notes in the css, a preference to change themes will come soon.\n\nTodo: placement bug, Allow keeping time/date, add time to menu, color/size/thickness prefs/seconds optional. Major credit on the work of this fork goes to https://extensions.gnome.org/accounts/profile/mathematical.coffee Fork based on original work by \"obneq\" https://github.com/obneq/",
            "link": "/extension/163/analog-clock/",
            "icon": "/extension-data/icons/icon_163_1.png",
            "screenshot": "/extension-data/screenshots/screenshot_163.png",
            "shell_version_map": {
                "3.4": {
                    "pk": 2684,
                    "version": 6
                },
                "3.6": {
                    "pk": 2684,
                    "version": 6
                },
                "3.6.2": {
                    "pk": 2684,
                    "version": 6
                },
                "3.8": {
                    "pk": 2808,
                    "version": 7
                },
                "3.9.1": {
                    "pk": 3165,
                    "version": 8
                },
                "3.9.3": {
                    "pk": 3165,
                    "version": 8
                },
                "3.9.5": {
                    "pk": 3165,
                    "version": 8
                },
                "3.10": {
                    "pk": 3259,
                    "version": 9
                }
            }
        }
    ],
    "total": 10,
    "numpages": 2
}

================================================
FILE: tests/test_cli.py
================================================
import shlex
from typing import List, Tuple
from unittest import mock

import pytest

from gnome_extensions_cli import __version__ as version
from gnome_extensions_cli import cli


def run(capsys, args: str) -> Tuple[int, List[str], List[str]]:
    with pytest.raises(SystemExit) as error:
        with mock.patch("sys.argv", ["gext"] + shlex.split(args)):
            cli.run()
    captured = capsys.readouterr()
    return (
        error.value.code,
        captured.out.splitlines(),
        captured.err.splitlines(),
    )  # pyright: reportGeneralTypeIssues=false


def assert_no_error(
    rc: int, out: List[str], err: List[str]
) -> Tuple[int, List[str], List[str]]:
    assert rc == 0
    assert len(out) > 0
    assert len(err) == 0
    return rc, out, err


def test_help(capsys):
    assert_no_error(*run(capsys, "--help"))
    assert_no_error(*run(capsys, "disable --help"))
    assert_no_error(*run(capsys, "enable --help"))
    assert_no_error(*run(capsys, "install --help"))
    assert_no_error(*run(capsys, "list --help"))
    assert_no_error(*run(capsys, "preferences --help"))
    assert_no_error(*run(capsys, "search --help"))
    assert_no_error(*run(capsys, "show --help"))
    assert_no_error(*run(capsys, "uninstall --help"))
    assert_no_error(*run(capsys, "update --help"))


def test_version(capsys):
    _rc, out, _err = assert_no_error(*run(capsys, "--version"))
    assert len(out) == 1
    assert version in out[0]


def test_error(capsys):
    rc, out, err = run(capsys, "foo bar")
    assert rc > 0
    assert len(out) == 0
    assert len(err) > 0


================================================
FILE: tests/test_dbus.py
================================================
import pytest

from gnome_extensions_cli.dbus import DbusExtensionManager, test_dbus_available


@pytest.mark.skipif(not test_dbus_available(True), reason="DBus is not available")
def test_dbus():
    manager = DbusExtensionManager()

    assert manager.get_current_shell_version() is not None

    all_extensions = manager.list_installed_extensions()
    assert len(all_extensions) > 0

    enabled_extensions = manager.list_enabled_uuids()
    assert len(enabled_extensions) > 0

    assert len(all_extensions) > len(enabled_extensions)


================================================
FILE: tests/test_filesystem.py
================================================
from gnome_extensions_cli.filesystem import FilesystemExtensionManager
from gnome_extensions_cli.store import GnomeExtensionStore


def test_filesystem():
    manager = FilesystemExtensionManager(store=GnomeExtensionStore())

    assert manager.get_current_shell_version() is not None

    all_extensions = manager.list_installed_extensions()
    assert len(all_extensions) > 0

    enabled_extensions = manager.list_enabled_uuids()
    assert len(enabled_extensions) >= 0

    assert len(all_extensions) >= len(enabled_extensions)


================================================
FILE: tests/test_model.py
================================================
from pathlib import Path

from gnome_extensions_cli.schema import AvailableExtension, Metadata, Search
from gnome_extensions_cli.utils import version_comparator


def test_version():
    for a, b, out in (
        (1, 2, 1),
        (2, 10, 1),
        (None, 1, 1),
        ("1.2", "1.2.0", 0),
        (1, "1", 0),
        (1.2, "1.2", 0),
        (None, None, 0),
    ):
        assert version_comparator(a, b) == out
        assert version_comparator(b, a) == out * -1


def test_schema():
    for folder in filter(
        Path.is_dir,
        [
            Path("/usr/share/gnome-shell/extensions"),
            Path("/usr/local/share/gnome-shell/extensions"),
        ],
    ):
        for sub in folder.iterdir():
            metadata_file = sub / "metadata.json"
            if metadata_file.exists():
                metadata = Metadata.model_validate_json(metadata_file.read_text())
                assert metadata is not None


def test_samples():
    samples_dir = Path(__file__).parent / "samples"
    assert samples_dir.is_dir()

    assert (
        AvailableExtension.model_validate_json(
            (samples_dir / "available.json").read_text()
        )
        is not None
    )
    assert (
        AvailableExtension.model_validate_json(
            (samples_dir / "available-alt.json").read_text()
        )
        is not None
    )
    assert (
        Metadata.model_validate_json((samples_dir / "installed.json").read_text())
        is not None
    )
    assert (
        Search.model_validate_json((samples_dir / "search.json").read_text())
        is not None
    )


================================================
FILE: tests/test_store.py
================================================
from gnome_extensions_cli.store import GnomeExtensionStore


def test_find():
    store = GnomeExtensionStore()

    ext = store.find_by_uuid("todo.txt@bart.libert.gmail.com")
    assert ext is not None
    assert ext.pk == 570
    assert ext.uuid == "todo.txt@bart.libert.gmail.com"
    assert ext.download_url is None

    assert ext == store.find_by_pk(ext.pk)


def test_not_found():
    store = GnomeExtensionStore()

    ext = store.find_by_uuid("this-extension-does-not-exists")
    assert ext is None


def test_shell_version():
    store = GnomeExtensionStore()

    ext = store.find_by_uuid("todo.txt@bart.libert.gmail.com", shell_version="40")
    assert ext is not None
    assert ext.download_url is not None
Download .txt
gitextract_c9d6yaeg/

├── .envrc
├── .github/
│   └── workflows/
│       └── poetry.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── Justfile
├── LICENSE
├── README.md
├── gnome_extensions_cli/
│   ├── __init__.py
│   ├── cli.py
│   ├── commands/
│   │   ├── __init__.py
│   │   ├── disable.py
│   │   ├── enable.py
│   │   ├── install.py
│   │   ├── list_.py
│   │   ├── preferences.py
│   │   ├── search.py
│   │   ├── show.py
│   │   ├── uninstall.py
│   │   └── update.py
│   ├── dbus.py
│   ├── filesystem.py
│   ├── icons.py
│   ├── manager.py
│   ├── schema.py
│   ├── store.py
│   └── utils.py
├── poetry.toml
├── pyproject.toml
└── tests/
    ├── __init__.py
    ├── samples/
    │   ├── available-alt.json
    │   ├── available.json
    │   ├── installed.json
    │   └── search.json
    ├── test_cli.py
    ├── test_dbus.py
    ├── test_filesystem.py
    ├── test_model.py
    └── test_store.py
Download .txt
SYMBOL INDEX (96 symbols across 22 files)

FILE: gnome_extensions_cli/cli.py
  function run (line 29) | def run():

FILE: gnome_extensions_cli/commands/disable.py
  function configure (line 12) | def configure(parser: ArgumentParser):
  function run (line 31) | def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensi...

FILE: gnome_extensions_cli/commands/enable.py
  function configure (line 12) | def configure(parser: ArgumentParser):
  function run (line 26) | def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensi...

FILE: gnome_extensions_cli/commands/install.py
  function configure (line 15) | def configure(parser: ArgumentParser):
  function run (line 29) | def run(args: Namespace, manager: ExtensionManager, store: GnomeExtensio...

FILE: gnome_extensions_cli/commands/list_.py
  function configure (line 12) | def configure(parser: ArgumentParser):
  function run (line 32) | def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensi...

FILE: gnome_extensions_cli/commands/preferences.py
  function configure (line 11) | def configure(parser: ArgumentParser):
  function run (line 24) | def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensi...

FILE: gnome_extensions_cli/commands/search.py
  function configure (line 13) | def configure(parser: ArgumentParser):
  function run (line 29) | def run(args: Namespace, manager: ExtensionManager, store: GnomeExtensio...

FILE: gnome_extensions_cli/commands/show.py
  function configure (line 17) | def configure(parser: ArgumentParser):
  function print_form (line 33) | def print_form(data: Dict[str, Any], indent: str = ""):
  function print_key_value (line 55) | def print_key_value(key: str, value: Optional[Any], indent: int = 0):
  function build_versions_dict (line 71) | def build_versions_dict(ext: AvailableExtension) -> Dict[int, List[str]]:
  function run (line 84) | def run(args: Namespace, manager: ExtensionManager, store: GnomeExtensio...

FILE: gnome_extensions_cli/commands/uninstall.py
  function configure (line 12) | def configure(parser: ArgumentParser):
  function run (line 26) | def run(args: Namespace, manager: ExtensionManager, _store: GnomeExtensi...

FILE: gnome_extensions_cli/commands/update.py
  function configure (line 15) | def configure(parser: ArgumentParser):
  function run (line 53) | def run(args: Namespace, manager: ExtensionManager, store: GnomeExtensio...

FILE: gnome_extensions_cli/dbus.py
  function test_dbus_available (line 17) | def test_dbus_available(debug: bool = False) -> bool:
  class DbusExtensionManager (line 32) | class DbusExtensionManager(ExtensionManager):
    method __init__ (line 39) | def __init__(self):
    method get_current_shell_version (line 63) | def get_current_shell_version(self) -> str:
    method list_installed_extensions (line 66) | def list_installed_extensions(self) -> List[InstalledExtension]:
    method install_extension (line 86) | def install_extension(self, ext: AvailableExtension) -> bool:
    method uninstall_extension (line 92) | def uninstall_extension(self, ext: InstalledExtension):
    method edit_extension (line 95) | def edit_extension(self, ext: InstalledExtension):
    method list_enabled_uuids (line 98) | def list_enabled_uuids(self) -> List[str]:
    method set_enabled_uuids (line 101) | def set_enabled_uuids(self, uuids: List[str]) -> bool:

FILE: gnome_extensions_cli/filesystem.py
  class FilesystemExtensionManager (line 25) | class FilesystemExtensionManager(ExtensionManager):
    method get_current_shell_version (line 42) | def get_current_shell_version(self) -> str:
    method list_installed_extensions (line 53) | def list_installed_extensions(self) -> List[InstalledExtension]:
    method install_extension (line 63) | def install_extension(self, ext: AvailableExtension) -> bool:
    method uninstall_extension (line 105) | def uninstall_extension(self, ext: InstalledExtension):
    method edit_extension (line 112) | def edit_extension(self, ext: InstalledExtension):
    method list_enabled_uuids (line 115) | def list_enabled_uuids(self) -> List[str]:
    method set_enabled_uuids (line 123) | def set_enabled_uuids(self, uuids: List[str]) -> bool:
    method restart_gnome_shell (line 141) | def restart_gnome_shell(self) -> bool:
    method _run (line 163) | def _run(self, command: List[str]) -> int:
    method compile_schemas (line 175) | def compile_schemas(self, extension_folder: Path) -> bool:

FILE: gnome_extensions_cli/icons.py
  class Icons (line 14) | class Icons(Enum):
    method __str__ (line 35) | def __str__(self):
  function color (line 39) | def color(
  class Color (line 62) | class Color(Enum):
    method __call__ (line 85) | def __call__(self, *args, style: Optional[str] = None) -> str:
  class Label (line 94) | class Label:
    method uuid (line 100) | def uuid(uuid: str) -> str:
    method version (line 104) | def version(version: Optional[Any]) -> Optional[str]:
    method url (line 108) | def url(base: str, path: Optional[str]) -> Optional[str]:
    method available (line 112) | def available(ext: AvailableExtension) -> str:
    method installed (line 125) | def installed(ext: InstalledExtension, enabled: Optional[bool] = None)...
    method folder (line 137) | def folder(path: Path) -> str:

FILE: gnome_extensions_cli/manager.py
  class ExtensionManager (line 11) | class ExtensionManager(ABC):
    method get_current_shell_version (line 18) | def get_current_shell_version(self) -> str:
    method list_installed_extensions (line 24) | def list_installed_extensions(self) -> List[InstalledExtension]:
    method install_extension (line 30) | def install_extension(self, ext: AvailableExtension) -> bool:
    method uninstall_extension (line 36) | def uninstall_extension(self, ext: InstalledExtension):
    method edit_extension (line 42) | def edit_extension(self, ext: InstalledExtension):
    method list_enabled_uuids (line 48) | def list_enabled_uuids(self) -> List[str]:
    method set_enabled_uuids (line 54) | def set_enabled_uuids(self, uuids: List[str]) -> bool:
    method enable_uuids (line 59) | def enable_uuids(self, *uuids: str) -> bool:
    method disable_uuids (line 67) | def disable_uuids(self, *uuids: str) -> bool:

FILE: gnome_extensions_cli/schema.py
  class Metadata (line 14) | class Metadata(BaseModel):
  class _Version (line 25) | class _Version(BaseModel):
  class AvailableExtension (line 30) | class AvailableExtension(BaseModel):
  class Search (line 46) | class Search(BaseModel):
  class InstalledExtension (line 53) | class InstalledExtension:
    method metadata_json (line 57) | def metadata_json(self) -> Path:
    method read_only (line 61) | def read_only(self):
    method metadata (line 65) | def metadata(self) -> Metadata:
    method uuid (line 69) | def uuid(self) -> str:

FILE: gnome_extensions_cli/store.py
  class GnomeExtensionStore (line 17) | class GnomeExtensionStore:
    method __post_init__ (line 26) | def __post_init__(self):
    method iter_fetch (line 37) | def iter_fetch(
    method find (line 55) | def find(
    method find_by_uuid (line 67) | def find_by_uuid(
    method find_by_pk (line 87) | def find_by_pk(
    method search (line 106) | def search(

FILE: gnome_extensions_cli/utils.py
  function version_comparator (line 10) | def version_comparator(left: Any, right: Any) -> int:
  function confirm (line 28) | def confirm(message: str, default: Optional[bool] = None) -> bool:

FILE: tests/test_cli.py
  function run (line 11) | def run(capsys, args: str) -> Tuple[int, List[str], List[str]]:
  function assert_no_error (line 23) | def assert_no_error(
  function test_help (line 32) | def test_help(capsys):
  function test_version (line 45) | def test_version(capsys):
  function test_error (line 51) | def test_error(capsys):

FILE: tests/test_dbus.py
  function test_dbus (line 7) | def test_dbus():

FILE: tests/test_filesystem.py
  function test_filesystem (line 5) | def test_filesystem():

FILE: tests/test_model.py
  function test_version (line 7) | def test_version():
  function test_schema (line 21) | def test_schema():
  function test_samples (line 36) | def test_samples():

FILE: tests/test_store.py
  function test_find (line 4) | def test_find():
  function test_not_found (line 16) | def test_not_found():
  function test_shell_version (line 23) | def test_shell_version():
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (107K chars).
[
  {
    "path": ".envrc",
    "chars": 47,
    "preview": "export VIRTUAL_ENV=.venv\nlayout python python3\n"
  },
  {
    "path": ".github/workflows/poetry.yml",
    "chars": 1222,
    "preview": "name: Build & Tests\n\non: [push]\n\njobs:\n  test:\n    name: Build and test App\n    runs-on: ubuntu-latest\n    steps:\n      "
  },
  {
    "path": ".gitignore",
    "chars": 124,
    "preview": "# Python\n__pycache__\n*.pyc\n\n# Generated\n/.coverage\n/coverage.xml\n/htmlcov/\n/dist/\n\n# IDE\n/.vscode/*\n!/.vscode/settings.j"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 49,
    "preview": "{\n    \"python.pythonPath\": \".venv/bin/python\",\n}\n"
  },
  {
    "path": "Justfile",
    "chars": 362,
    "preview": "release bump=\"patch\":\n    echo \"{{bump}}\" | grep -E \"^(major|minor|patch)$\"\n    poetry version \"{{bump}}\"\n    git add py"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 5768,
    "preview": "![Github](https://img.shields.io/github/tag/essembeh/gnome-extensions-cli.svg)\n![PyPi](https://img.shields.io/pypi/v/gno"
  },
  {
    "path": "gnome_extensions_cli/__init__.py",
    "chars": 102,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom importlib.metadata import version\n\n__version__ = version(__name__)\n"
  },
  {
    "path": "gnome_extensions_cli/cli.py",
    "chars": 3691,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nimport sys\nfrom argparse import ArgumentParser\nfrom os import getenv\n\nfrom colorama import"
  },
  {
    "path": "gnome_extensions_cli/commands/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "gnome_extensions_cli/commands/disable.py",
    "chars": 1193,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom argparse import ZERO_OR_MORE, ArgumentParser, Namespace\n\nfrom ..icons import Color\nfr"
  },
  {
    "path": "gnome_extensions_cli/commands/enable.py",
    "chars": 762,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom argparse import ONE_OR_MORE, ArgumentParser, Namespace\n\nfrom ..icons import Color\nfro"
  },
  {
    "path": "gnome_extensions_cli/commands/install.py",
    "chars": 2314,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom argparse import ONE_OR_MORE, ArgumentParser, Namespace\n\nfrom tqdm import tqdm\n\nfrom ."
  },
  {
    "path": "gnome_extensions_cli/commands/list_.py",
    "chars": 2215,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom argparse import ArgumentParser, Namespace\n\nfrom ..icons import Color, Icons, Label\nfr"
  },
  {
    "path": "gnome_extensions_cli/commands/preferences.py",
    "chars": 753,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom argparse import ArgumentParser, Namespace\n\nfrom ..manager import ExtensionManager\nfro"
  },
  {
    "path": "gnome_extensions_cli/commands/search.py",
    "chars": 1963,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom argparse import ONE_OR_MORE, ArgumentParser, Namespace\n\nfrom ..icons import Color, Ic"
  },
  {
    "path": "gnome_extensions_cli/commands/show.py",
    "chars": 6001,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom argparse import ONE_OR_MORE, ArgumentParser, Namespace\nfrom typing import Any, Dict, "
  },
  {
    "path": "gnome_extensions_cli/commands/uninstall.py",
    "chars": 1535,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom argparse import ONE_OR_MORE, ArgumentParser, Namespace\n\nfrom ..icons import Color, Ic"
  },
  {
    "path": "gnome_extensions_cli/commands/update.py",
    "chars": 5385,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom argparse import ZERO_OR_MORE, ArgumentParser, Namespace\n\nfrom tqdm import tqdm\n\nfrom "
  },
  {
    "path": "gnome_extensions_cli/dbus.py",
    "chars": 3127,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom operator import itemgetter\nfrom pathlib import Path\nfrom traceback import print_exc\nf"
  },
  {
    "path": "gnome_extensions_cli/filesystem.py",
    "chars": 7460,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nimport subprocess\nimport sys\nfrom dataclasses import dataclass, field\nfrom os.path import "
  },
  {
    "path": "gnome_extensions_cli/icons.py",
    "chars": 3398,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom enum import Enum\nfrom pathlib import Path\nfrom typing import Any, Optional\n\nfrom colo"
  },
  {
    "path": "gnome_extensions_cli/manager.py",
    "chars": 1793,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom typing import List\n\nfrom .schema import Available"
  },
  {
    "path": "gnome_extensions_cli/schema.py",
    "chars": 1619,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nimport os\nfrom dataclasses import dataclass\nfrom functools import cached_property\nfrom pat"
  },
  {
    "path": "gnome_extensions_cli/store.py",
    "chars": 4094,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom dataclasses import da"
  },
  {
    "path": "gnome_extensions_cli/utils.py",
    "chars": 1042,
    "preview": "\"\"\"\ngnome-extensions-cli\n\"\"\"\n\nfrom typing import Any, Optional\n\nfrom packaging.version import Version\n\n\ndef version_comp"
  },
  {
    "path": "poetry.toml",
    "chars": 82,
    "preview": "[virtualenvs]\nin-project = true\n\n[virtualenvs.options]\nsystem-site-packages = true"
  },
  {
    "path": "pyproject.toml",
    "chars": 881,
    "preview": "[tool.poetry]\nname = \"gnome-extensions-cli\"\nversion = \"0.11.0\"\ndescription = \"Command line tool to manage your Gnome She"
  },
  {
    "path": "tests/__init__.py",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "tests/samples/available-alt.json",
    "chars": 3170,
    "preview": "{\n    \"uuid\": \"todo.txt@bart.libert.gmail.com\",\n    \"name\": \"Todo.txt\",\n    \"creator\": \"BartL\",\n    \"creator_url\": \"/acc"
  },
  {
    "path": "tests/samples/available.json",
    "chars": 3327,
    "preview": "{\n    \"uuid\": \"todo.txt@bart.libert.gmail.com\",\n    \"name\": \"Todo.txt\",\n    \"creator\": \"BartL\",\n    \"creator_url\": \"/acc"
  },
  {
    "path": "tests/samples/installed.json",
    "chars": 1369,
    "preview": "{\n  \"_generated\": \"Generated by SweetTooth, do not edit\",\n  \"description\": \"A Gnome shell interface for todo.txt. \\n\\nTo"
  },
  {
    "path": "tests/samples/search.json",
    "chars": 18108,
    "preview": "{\n    \"extensions\": [\n        {\n            \"uuid\": \"todolist@bsaleil.org\",\n            \"name\": \"Todo list\",\n           "
  },
  {
    "path": "tests/test_cli.py",
    "chars": 1584,
    "preview": "import shlex\nfrom typing import List, Tuple\nfrom unittest import mock\n\nimport pytest\n\nfrom gnome_extensions_cli import _"
  },
  {
    "path": "tests/test_dbus.py",
    "chars": 539,
    "preview": "import pytest\n\nfrom gnome_extensions_cli.dbus import DbusExtensionManager, test_dbus_available\n\n\n@pytest.mark.skipif(not"
  },
  {
    "path": "tests/test_filesystem.py",
    "chars": 532,
    "preview": "from gnome_extensions_cli.filesystem import FilesystemExtensionManager\nfrom gnome_extensions_cli.store import GnomeExten"
  },
  {
    "path": "tests/test_model.py",
    "chars": 1596,
    "preview": "from pathlib import Path\n\nfrom gnome_extensions_cli.schema import AvailableExtension, Metadata, Search\nfrom gnome_extens"
  },
  {
    "path": "tests/test_store.py",
    "chars": 722,
    "preview": "from gnome_extensions_cli.store import GnomeExtensionStore\n\n\ndef test_find():\n    store = GnomeExtensionStore()\n\n    ext"
  }
]

About this extraction

This page contains the full source code of the essembeh/gnome-extensions-cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (97.0 KB), approximately 23.1k tokens, and a symbol index with 96 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!