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
================================================




# 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.

## 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
```

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

> 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:

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

# 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 >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
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
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": "\n\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.